{"version":3,"file":"run-history.d.ts","sourceRoot":"","sources":["../../../../src/runs/shared/run-history.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,QAAQ;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,IAAI,GAAG,OAAO,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;CACd;AAqID,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAuBjG;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,EAAE,CAkC1D","sourcesContent":["import { createHash } from \"node:crypto\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { getAgentDir } from \"../../shared/utils.ts\";\n\nexport interface RunEntry {\n\tagent: string;\n\ttask: string;\n\ttaskHash?: string;\n\tts: number;\n\tstatus: \"ok\" | \"error\";\n\tduration: number;\n\texit?: number;\n}\n\nconst ROTATE_READ_THRESHOLD = 1200;\nconst ROTATE_KEEP = 1000;\nconst PRIVATE_DIR_MODE = 0o700;\nconst PRIVATE_FILE_MODE = 0o600;\nconst REDACTED_TASK = \"[redacted]\";\nconst historyFileStates = new Map<\n\tstring,\n\t{ mtimeMs: number; ctimeMs: number; size: number; ino: number; lineCount: number }\n>();\n\nfunction getHistoryPath(): string {\n\treturn path.join(getAgentDir(), \"run-history.jsonl\");\n}\n\nfunction hashTask(task: string): string {\n\treturn createHash(\"sha256\").update(task).digest(\"hex\");\n}\n\nfunction hardenHistoryStorage(historyPath: string): void {\n\tconst historyDir = path.dirname(historyPath);\n\tfs.mkdirSync(historyDir, { recursive: true, mode: PRIVATE_DIR_MODE });\n\ttry {\n\t\tif ((fs.statSync(historyDir).mode & 0o777) !== PRIVATE_DIR_MODE) fs.chmodSync(historyDir, PRIVATE_DIR_MODE);\n\t} catch {}\n\ttry {\n\t\tif ((fs.statSync(historyPath).mode & 0o777) !== PRIVATE_FILE_MODE) fs.chmodSync(historyPath, PRIVATE_FILE_MODE);\n\t} catch {}\n}\n\nfunction sanitizeHistoryLine(line: string): string | undefined {\n\tlet value: unknown;\n\ttry {\n\t\tvalue = JSON.parse(line);\n\t} catch {\n\t\treturn undefined;\n\t}\n\tif (!value || typeof value !== \"object\") return undefined;\n\n\tconst record = value as Record<string, unknown>;\n\tconst task = typeof record.task === \"string\" ? record.task : \"\";\n\tconst taskHash =\n\t\ttypeof record.taskHash === \"string\" && record.taskHash\n\t\t\t? record.taskHash\n\t\t\t: task && task !== REDACTED_TASK\n\t\t\t\t? hashTask(task)\n\t\t\t\t: undefined;\n\n\treturn JSON.stringify({\n\t\t...record,\n\t\ttask: REDACTED_TASK,\n\t\t...(taskHash ? { taskHash } : {}),\n\t});\n}\n\nfunction sanitizeHistoryLines(raw: string): { lines: string[]; changed: boolean } {\n\tconst lines: string[] = [];\n\tlet changed = false;\n\tfor (const line of raw.split(\"\\n\")) {\n\t\tconst trimmed = line.trim();\n\t\tif (!trimmed) continue;\n\t\tconst sanitized = sanitizeHistoryLine(trimmed);\n\t\tif (!sanitized) {\n\t\t\tchanged = true;\n\t\t\tcontinue;\n\t\t}\n\t\tif (sanitized !== trimmed) changed = true;\n\t\tlines.push(sanitized);\n\t}\n\treturn { lines, changed };\n}\n\nfunction writePrivateHistory(historyPath: string, lines: string[]): void {\n\tfs.writeFileSync(historyPath, lines.length ? `${lines.join(\"\\n\")}\\n` : \"\", {\n\t\tencoding: \"utf-8\",\n\t\tmode: PRIVATE_FILE_MODE,\n\t});\n\ttry {\n\t\tfs.chmodSync(historyPath, PRIVATE_FILE_MODE);\n\t} catch {}\n}\n\nfunction rememberHistoryFile(historyPath: string, lineCount: number): void {\n\tconst stat = fs.statSync(historyPath);\n\thistoryFileStates.set(historyPath, {\n\t\tmtimeMs: stat.mtimeMs,\n\t\tctimeMs: stat.ctimeMs,\n\t\tsize: stat.size,\n\t\tino: stat.ino,\n\t\tlineCount,\n\t});\n\tif (historyFileStates.size > 8) historyFileStates.delete(historyFileStates.keys().next().value!);\n}\n\nfunction sanitizeHistoryFile(historyPath: string): number {\n\tlet stat: fs.Stats;\n\ttry {\n\t\tstat = fs.statSync(historyPath);\n\t} catch (error) {\n\t\tif ((error as NodeJS.ErrnoException).code === \"ENOENT\") return 0;\n\t\tthrow error;\n\t}\n\tconst cached = historyFileStates.get(historyPath);\n\tif (\n\t\tcached &&\n\t\tcached.mtimeMs === stat.mtimeMs &&\n\t\tcached.ctimeMs === stat.ctimeMs &&\n\t\tcached.size === stat.size &&\n\t\tcached.ino === stat.ino\n\t) {\n\t\treturn cached.lineCount;\n\t}\n\tconst raw = fs.readFileSync(historyPath, \"utf-8\");\n\tconst { lines, changed } = sanitizeHistoryLines(raw);\n\tif (changed) writePrivateHistory(historyPath, lines);\n\trememberHistoryFile(historyPath, lines.length);\n\treturn lines.length;\n}\n\nfunction appendPrivateHistoryLine(historyPath: string, line: string): void {\n\tconst fd = fs.openSync(\n\t\thistoryPath,\n\t\tfs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_WRONLY,\n\t\tPRIVATE_FILE_MODE,\n\t);\n\ttry {\n\t\tfs.writeSync(fd, `${line}\\n`);\n\t} finally {\n\t\tfs.closeSync(fd);\n\t}\n}\n\nexport function recordRun(agent: string, task: string, exitCode: number, durationMs: number): void {\n\ttry {\n\t\tconst entry: RunEntry = {\n\t\t\tagent,\n\t\t\ttask: REDACTED_TASK,\n\t\t\ttaskHash: hashTask(task),\n\t\t\tts: Math.floor(Date.now() / 1000),\n\t\t\tstatus: exitCode === 0 ? \"ok\" : \"error\",\n\t\t\tduration: durationMs,\n\t\t\t...(exitCode !== 0 ? { exit: exitCode } : {}),\n\t\t};\n\t\tconst historyPath = getHistoryPath();\n\t\thardenHistoryStorage(historyPath);\n\t\tlet lineCount: number | undefined;\n\t\ttry {\n\t\t\tlineCount = sanitizeHistoryFile(historyPath);\n\t\t} catch {}\n\t\tappendPrivateHistoryLine(historyPath, JSON.stringify(entry));\n\t\tif (lineCount === undefined) historyFileStates.delete(historyPath);\n\t\telse rememberHistoryFile(historyPath, lineCount + 1);\n\t} catch {\n\t\t// Best-effort — never crash the execution flow for history recording\n\t}\n}\n\nexport function loadRunsForAgent(agent: string): RunEntry[] {\n\tconst historyPath = getHistoryPath();\n\ttry {\n\t\thardenHistoryStorage(historyPath);\n\t} catch {}\n\tif (!fs.existsSync(historyPath)) return [];\n\tlet raw: string;\n\ttry {\n\t\traw = fs.readFileSync(historyPath, \"utf-8\");\n\t} catch {\n\t\treturn [];\n\t}\n\n\tlet { lines, changed } = sanitizeHistoryLines(raw);\n\n\tif (lines.length > ROTATE_READ_THRESHOLD) {\n\t\tlines = lines.slice(-ROTATE_KEEP);\n\t\tchanged = true;\n\t}\n\ttry {\n\t\tif (changed) writePrivateHistory(historyPath, lines);\n\t\trememberHistoryFile(historyPath, lines.length);\n\t} catch {}\n\n\treturn lines\n\t\t.map((line) => {\n\t\t\ttry {\n\t\t\t\treturn JSON.parse(line) as RunEntry;\n\t\t\t} catch {\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t})\n\t\t.filter((entry): entry is RunEntry => entry !== undefined && entry.agent === agent)\n\t\t.reverse();\n}\n"]}