import * as fs from "node:fs"; import type { RunEvent, RunPaths } from "./status.ts"; import { sanitizeInspectionField } from "./trajectory.ts"; export const MAX_WORKER_TRACE_BYTES = 8 * 1024 * 1024; const MAX_ASSISTANT_BYTES = 768; const MAX_RESULT_BYTES = 1_024; const MAX_TOOL_BYTES = 120; const MAX_TARGET_BYTES = 320; const MAX_PATH_BYTES = 160; const MAX_DETAIL_BYTES = 512; function field(value: unknown, maxBytes: number): string | null { return sanitizeInspectionField(value, maxBytes); } /** One concise physical line suitable for tail, grep, awk, and incremental reads. */ export function workerTraceBody(event: RunEvent): string | null { const kind = typeof event.kind === "string" ? event.kind : ""; if (kind === "assistant") { const preview = field(event.preview, MAX_ASSISTANT_BYTES); return preview === null ? null : `assistant\t${preview}${event.truncated === true ? " …" : ""}`; } if (kind === "tool_start") { const tool = field(event.tool, MAX_TOOL_BYTES) ?? "tool"; const target = field(event.target, MAX_TARGET_BYTES); const logPath = field(event.logPath, MAX_PATH_BYTES); return ["tool_start", tool, target, logPath].filter((part): part is string => part !== null).join("\t"); } if (kind === "tool_end" || kind === "late_tool_end") { if (kind === "tool_end" && event.matched === false) return null; const tool = field(event.tool, MAX_TOOL_BYTES) ?? "tool"; const target = field(event.target, MAX_TARGET_BYTES); const preview = field(event.preview, MAX_RESULT_BYTES); const logPath = field(event.logPath, MAX_PATH_BYTES); const label = kind === "late_tool_end" ? "tool_late" : event.isError === true ? "tool_error" : "tool_ok"; return [label, tool, target, logPath, ...(logPath !== null ? [] : [preview === null ? "no text output" : `${preview}${event.truncated === true ? " …" : ""}`])] .filter((part): part is string => part !== null) .join("\t"); } if (kind === "compaction") return "context_compacted"; if (kind === "retry") { const detail = field(event.detail, MAX_DETAIL_BYTES); return detail === null ? "provider_retry" : `provider_retry\t${detail}`; } if (kind === "deadline") return "deadline_reached"; if (kind === "spawn_error") { const detail = field(event.detail, MAX_DETAIL_BYTES); return detail === null ? "spawn_error" : `spawn_error\t${detail}`; } if (kind === "report_error") { const detail = field(event.detail, MAX_DETAIL_BYTES); return detail === null ? "report_error" : `report_error\t${detail}`; } if (kind === "interrupt_requested" || kind === "stop_requested" || kind === "steering_handoff") return kind; return null; } function lastSequenceIn(file: string): number { let raw: string; try { raw = fs.readFileSync(file, "utf8"); } catch { return 0; } const lines = raw.trimEnd().split("\n"); for (let index = lines.length - 1; index >= 0; index -= 1) { const match = /^(\d+)\t/.exec(lines[index] ?? ""); if (match === null) continue; const value = Number(match[1]); if (Number.isSafeInteger(value) && value >= 0) return value; } return 0; } export function readWorkerTraceSequence(paths: RunPaths): number { return Math.max(lastSequenceIn(paths.trace), lastSequenceIn(`${paths.trace}.1`)); } /** Best-effort append. The caller advances its sequence only after a successful write. */ export function appendWorkerTrace(paths: RunPaths, sequence: number, event: RunEvent): boolean { const body = workerTraceBody(event); if (body === null) return false; const line = `${sequence}\t${body}\n`; try { fs.mkdirSync(paths.dir, { recursive: true, mode: 0o700 }); let size = 0; try { size = fs.statSync(paths.trace).size; } catch {} if (size + Buffer.byteLength(line, "utf8") > MAX_WORKER_TRACE_BYTES) { try { fs.renameSync(paths.trace, `${paths.trace}.1`); } catch {} } fs.appendFileSync(paths.trace, line, { encoding: "utf8", mode: 0o600 }); return true; } catch { return false; } }