utils.ts 7.0 KB

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