This repository was archived by the owner on May 15, 2026. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Expand file tree
/
Copy pathutils.ts
More file actions
251 lines (215 loc) · 7.04 KB
/
Copy pathutils.ts
File metadata and controls
251 lines (215 loc) · 7.04 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
import * as fs from "fs"
import * as fsp from "fs/promises"
import * as path from "path"
import { execa, type ResultPromise } from "execa"
import type { ToolUsage } from "@roo-code/types"
import type { Run, Task } from "../db/index"
import { SubprocessTimeoutError } from "./types"
export const getTag = (caller: string, { run, task }: { run: Run; task?: Task }) =>
task
? `${caller} | pid:${process.pid} | run:${run.id} | task:${task.id} | ${task.language}/${task.exercise}`
: `${caller} | pid:${process.pid} | run:${run.id}`
export const isDockerContainer = () => {
try {
return fs.existsSync("/.dockerenv")
} catch (_error) {
return false
}
}
export const resetEvalsRepo = async ({ run, cwd }: { run: Run; cwd: string }) => {
await execa({ cwd })`git config user.name "Roo Code"`
await execa({ cwd })`git config user.email "noreply@example.com"`
await execa({ cwd })`git checkout -f`
await execa({ cwd })`git clean -fd`
await execa({ cwd })`git checkout -b runs/${run.id}-${crypto.randomUUID().slice(0, 8)} main`
}
export const commitEvalsRepoChanges = async ({ run, cwd }: { run: Run; cwd: string }) => {
await execa({ cwd })`git add .`
await execa({ cwd })`git commit -m ${`Run #${run.id}`} --no-verify`
}
enum LogLevel {
INFO = "INFO",
ERROR = "ERROR",
WARN = "WARN",
DEBUG = "DEBUG",
}
interface LoggerOptions {
logDir: string
filename: string
tag: string
}
export class Logger {
private logStream: fs.WriteStream | undefined
private logFilePath: string
private tag: string
constructor({ logDir, filename, tag }: LoggerOptions) {
this.tag = tag
this.logFilePath = path.join(logDir, filename)
this.initializeLogger(logDir)
}
private initializeLogger(logDir: string): void {
try {
fs.mkdirSync(logDir, { recursive: true })
} catch (error) {
console.error(`Failed to create log directory ${logDir}:`, error)
}
try {
this.logStream = fs.createWriteStream(this.logFilePath, { flags: "a" })
} catch (error) {
console.error(`Failed to create log file ${this.logFilePath}:`, error)
}
}
private writeToLog(level: LogLevel, message: string, ...args: unknown[]) {
try {
const timestamp = new Date().toISOString()
const logLine = `[${timestamp} | ${level} | ${this.tag}] ${message} ${
args.length > 0 ? JSON.stringify(args) : ""
}\n`
console.log(logLine.trim())
if (this.logStream) {
this.logStream.write(logLine)
}
} catch (error) {
console.error(`Failed to write to log file ${this.logFilePath}:`, error)
}
}
public info(message: string, ...args: unknown[]): void {
this.writeToLog(LogLevel.INFO, message, ...args)
}
public error(message: string, ...args: unknown[]): void {
this.writeToLog(LogLevel.ERROR, message, ...args)
}
public warn(message: string, ...args: unknown[]): void {
this.writeToLog(LogLevel.WARN, message, ...args)
}
public debug(message: string, ...args: unknown[]): void {
this.writeToLog(LogLevel.DEBUG, message, ...args)
}
public log(message: string, ...args: unknown[]): void {
this.info(message, ...args)
}
/**
* Write raw output without any prefix (timestamp, level, tag).
* Useful for streaming CLI output where the prefix would be noise.
*/
public raw(message: string): void {
try {
console.log(message)
if (this.logStream) {
this.logStream.write(message + "\n")
}
} catch (error) {
console.error(`Failed to write to log file ${this.logFilePath}:`, error)
}
}
public close(): void {
if (this.logStream) {
this.logStream.end()
this.logStream = undefined
}
}
}
/**
* Copy conversation history files from VS Code extension storage to the log directory.
* This allows us to preserve the api_conversation_history.json and ui_messages.json
* files for post-mortem analysis alongside the log files.
*/
export async function copyConversationHistory({
rooTaskId,
logDir,
language,
exercise,
iteration,
logger,
}: {
rooTaskId: string
logDir: string
language: string
exercise: string
iteration: number
logger: Logger
}): Promise<void> {
// VS Code extension global storage path within the container
const extensionStoragePath = "/roo/.vscode/User/globalStorage/rooveterinaryinc.roo-cline"
const taskStoragePath = path.join(extensionStoragePath, "tasks", rooTaskId)
const filesToCopy = ["api_conversation_history.json", "ui_messages.json"]
for (const filename of filesToCopy) {
const sourcePath = path.join(taskStoragePath, filename)
// Use sanitized exercise name (replace slashes with dashes) for the destination filename
// Include iteration number to handle multiple attempts at the same exercise
const sanitizedExercise = exercise.replace(/\//g, "-")
const destFilename = `${language}-${sanitizedExercise}.${iteration}_${filename}`
const destPath = path.join(logDir, destFilename)
try {
// Check if source file exists
await fsp.access(sourcePath)
// Copy the file
await fsp.copyFile(sourcePath, destPath)
logger.info(`copied ${filename} to ${destPath}`)
} catch (error) {
// File may not exist if task didn't complete properly - this is not fatal
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
logger.info(`${filename} not found at ${sourcePath} - skipping`)
} else {
logger.error(`failed to copy ${filename}:`, error)
}
}
}
}
/**
* Merge incoming tool usage with accumulated data using MAX strategy.
* This handles the case where a task is rehydrated after abort:
* - Empty rehydrated data won't overwrite existing: max(5, 0) = 5
* - Legitimate restart with additional work is captured: max(5, 8) = 8
* Each task instance tracks its own cumulative values, so we take the max
* to preserve the highest values seen across all instances.
*/
export function mergeToolUsage(accumulated: ToolUsage, incoming: ToolUsage): void {
for (const [toolName, usage] of Object.entries(incoming)) {
const existing = accumulated[toolName as keyof ToolUsage]
if (existing) {
accumulated[toolName as keyof ToolUsage] = {
attempts: Math.max(existing.attempts, usage.attempts),
failures: Math.max(existing.failures, usage.failures),
}
} else {
accumulated[toolName as keyof ToolUsage] = { ...usage }
}
}
}
/**
* Wait for a subprocess to finish gracefully, with a timeout.
* If the subprocess doesn't finish within the timeout, force kill it with SIGKILL.
*/
export async function waitForSubprocessWithTimeout({
subprocess,
timeoutMs = 10_000,
logger,
}: {
subprocess: ResultPromise
timeoutMs?: number
logger: Logger
}): Promise<void> {
try {
await Promise.race([
subprocess,
new Promise((_, reject) => setTimeout(() => reject(new SubprocessTimeoutError(timeoutMs)), timeoutMs)),
])
logger.info("subprocess finished gracefully")
} catch (error) {
if (error instanceof SubprocessTimeoutError) {
logger.error("subprocess did not finish within timeout, force killing")
try {
if (subprocess.kill("SIGKILL")) {
logger.info("SIGKILL sent to subprocess")
} else {
logger.error("failed to send SIGKILL to subprocess")
}
} catch (killError) {
logger.error("subprocess.kill(SIGKILL) failed:", killError)
}
} else {
throw error
}
}
}