@rallycry/conveyor-agent 10.13.64 → 10.13.66

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/{boot-7OPX55AO.js → boot-UUPTBQ6R.js} +7 -8
  2. package/dist/{chunk-JIGG755T.js → chunk-6Q6LQBWO.js} +0 -1
  3. package/dist/{chunk-ULS4QPRE.js → chunk-6W6UZ4SJ.js} +0 -1
  4. package/dist/{chunk-YSALHJTS.js → chunk-75FUOUYX.js} +515 -214
  5. package/dist/{chunk-AXA55U4Z.js → chunk-E2SHIH6Y.js} +3 -4
  6. package/dist/{chunk-QOJTJCYZ.js → chunk-EXQ6AHOY.js} +2 -3
  7. package/dist/{chunk-5OQQSDVT.js → chunk-IA45XHOA.js} +0 -1
  8. package/dist/{chunk-QZN6HVUY.js → chunk-KG4ORL3Y.js} +1 -2
  9. package/dist/{chunk-4VUQ2NPF.js → chunk-KMB3BU4S.js} +0 -1
  10. package/dist/{chunk-NAK6FY5U.js → chunk-LE6ZUDZT.js} +37 -6
  11. package/dist/{chunk-6GS5ADIY.js → chunk-R3FDJQL6.js} +0 -1
  12. package/dist/cli.js +35 -43
  13. package/dist/{client-LRVVHTNG.js → client-IG6C5F2G.js} +3 -4
  14. package/dist/heartbeat-worker.js +1 -2
  15. package/dist/index.d.ts +95 -13
  16. package/dist/index.js +7 -8
  17. package/dist/{mode-6RL3SVMV.js → mode-ZJSOSLGU.js} +1 -2
  18. package/dist/{oom-watchdog-U7JERHA2.js → oom-watchdog-PAC5OJJG.js} +1 -2
  19. package/dist/{protocol-QLVS5W6O.js → protocol-QBCYO4GI.js} +1 -2
  20. package/dist/server-US2DDQSW.js +9 -0
  21. package/package.json +1 -1
  22. package/dist/boot-7OPX55AO.js.map +0 -1
  23. package/dist/chunk-4VUQ2NPF.js.map +0 -1
  24. package/dist/chunk-5OQQSDVT.js.map +0 -1
  25. package/dist/chunk-6GS5ADIY.js.map +0 -1
  26. package/dist/chunk-AXA55U4Z.js.map +0 -1
  27. package/dist/chunk-JIGG755T.js.map +0 -1
  28. package/dist/chunk-NAK6FY5U.js.map +0 -1
  29. package/dist/chunk-QOJTJCYZ.js.map +0 -1
  30. package/dist/chunk-QZN6HVUY.js.map +0 -1
  31. package/dist/chunk-ULS4QPRE.js.map +0 -1
  32. package/dist/chunk-YSALHJTS.js.map +0 -1
  33. package/dist/cli.js.map +0 -1
  34. package/dist/client-LRVVHTNG.js.map +0 -1
  35. package/dist/heartbeat-worker.js.map +0 -1
  36. package/dist/index.js.map +0 -1
  37. package/dist/mode-6RL3SVMV.js.map +0 -1
  38. package/dist/oom-watchdog-U7JERHA2.js.map +0 -1
  39. package/dist/protocol-QLVS5W6O.js.map +0 -1
  40. package/dist/server-ZACB5T5S.js +0 -10
  41. package/dist/server-ZACB5T5S.js.map +0 -1
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/workbench/client.ts","../src/workbench/errors.ts","../src/workbench/remote-process.ts"],"sourcesContent":["/**\n * Client half of the workbench launcher protocol. Runs in the protected agent\n * container; every method opens one loopback connection, sends the request\n * frame, and adapts the stream back to the local signature it replaces\n * (runSetupCommand, runStartCommand, execFile, PtySpawn, workspace file\n * reads). See server.ts / protocol.ts.\n */\n\nimport { connect, type Socket } from \"node:net\";\nimport type { PtyProcess, PtySpawnOptions } from \"../harness/pty/pty-support.js\";\nimport { WorkbenchError } from \"./errors.js\";\nimport { RemoteProcessHandle } from \"./remote-process.js\";\n\nexport { WorkbenchError } from \"./errors.js\";\nexport { RemoteProcessHandle } from \"./remote-process.js\";\nimport { workbenchPort, workbenchToken } from \"./mode.js\";\nimport {\n DEFAULT_WORKBENCH_PORT,\n FrameReader,\n writeFrame,\n type GitStatusFrame,\n type WorkbenchFrame,\n type WorkbenchRequest,\n} from \"./protocol.js\";\n\nexport interface WorkbenchClientOptions {\n port?: number;\n token?: string;\n host?: string;\n}\n\n/** Omit that distributes over a discriminated union (plain Omit collapses\n * the union to its common properties). */\ntype DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;\n\nfunction frameError(frame: Extract<WorkbenchFrame, { t: \"error\" }>): WorkbenchError {\n return new WorkbenchError(frame.message, frame.code);\n}\n\ntype ExecFailure = Error & {\n code?: number | null;\n signal?: string | null;\n stdout: string;\n stderr: string;\n};\n\n/** Build a rejection carrying the same stdout/stderr/code/signal fields node's\n * promisified execFile attaches, so callers can inspect them. */\nfunction execFailure(\n message: string,\n fields: { stdout: string; stderr: string; code?: number | null; signal?: string | null },\n): ExecFailure {\n const failure = new Error(message) as ExecFailure;\n failure.stdout = fields.stdout;\n failure.stderr = fields.stderr;\n if (fields.code !== undefined) failure.code = fields.code;\n if (fields.signal !== undefined) failure.signal = fields.signal;\n return failure;\n}\n\n/** Settle an execFile promise from the collected stream + exit state. Extracted\n * from `execFile` so the method body stays within the per-function line cap;\n * behavior is identical to the inline close handler it replaces. */\nfunction settleExecOutcome(\n state: {\n file: string;\n args: string[];\n stdout: Buffer[];\n stderr: Buffer[];\n overflowed: boolean;\n exited: boolean;\n exitCode: number | null;\n exitSignal: string | null;\n timedOut: boolean;\n maxBuffer: number | undefined;\n error: Error | null;\n },\n resolve: (value: { stdout: string; stderr: string }) => void,\n reject: (reason: unknown) => void,\n): void {\n const out = Buffer.concat(state.stdout).toString(\"utf8\");\n const errText = Buffer.concat(state.stderr).toString(\"utf8\");\n if (state.overflowed) {\n reject(\n execFailure(\n `maxBuffer length exceeded (${state.maxBuffer} bytes): ${state.file} ${state.args.join(\" \")}`,\n { stdout: out, stderr: errText },\n ),\n );\n } else if (!state.exited) {\n reject(state.error ?? new WorkbenchError(\"connection closed before exit\"));\n } else if (state.exitCode === 0) {\n resolve({ stdout: out, stderr: errText });\n } else {\n reject(\n execFailure(\n state.timedOut\n ? `Command timed out: ${state.file}`\n : `Command failed: ${state.file} ${state.args.join(\" \")}\\n${errText}`,\n { stdout: out, stderr: errText, code: state.exitCode, signal: state.exitSignal },\n ),\n );\n }\n}\n\nexport class WorkbenchClient {\n private readonly port: number;\n private readonly host: string;\n private readonly token: string;\n\n constructor(options: WorkbenchClientOptions = {}) {\n this.port = options.port ?? workbenchPort() ?? DEFAULT_WORKBENCH_PORT;\n this.host = options.host ?? \"127.0.0.1\";\n this.token = options.token ?? workbenchToken();\n }\n\n /** Open a connection, send the request, route response frames. */\n private open(\n request: DistributiveOmit<WorkbenchRequest, \"token\">,\n onFrame: (frame: WorkbenchFrame, socket: Socket) => void,\n onClose: (error?: Error) => void,\n ): Socket {\n const socket = connect(this.port, this.host);\n let sawError: Error | undefined;\n const reader = new FrameReader((raw) => {\n const frame = raw as unknown as WorkbenchFrame;\n if (frame.t === \"error\") {\n sawError = frameError(frame as Extract<WorkbenchFrame, { t: \"error\" }>);\n }\n onFrame(frame, socket);\n });\n socket.on(\"connect\", () => {\n writeFrame(socket, { ...request, token: this.token } as WorkbenchRequest);\n });\n socket.on(\"data\", (chunk) => reader.push(chunk));\n socket.on(\"error\", (err) => {\n sawError ??= err;\n });\n socket.on(\"close\", () => onClose(sawError));\n return socket;\n }\n\n ping(): Promise<string> {\n return new Promise((resolve, reject) => {\n let version: string | null = null;\n this.open(\n { op: \"ping\" },\n (frame) => {\n if (frame.t === \"pong\") version = frame.version;\n },\n (error) => {\n if (version === null) {\n reject(error ?? new WorkbenchError(\"connection closed before pong\"));\n } else {\n resolve(version);\n }\n },\n );\n });\n }\n\n /** One-shot poll of the workbench's git-prep state. A daemon without a\n * provider answers `ready` (non-pod servers must not wedge callers). */\n gitStatus(): Promise<GitStatusFrame> {\n return new Promise((resolve, reject) => {\n let status: GitStatusFrame | null = null;\n this.open(\n { op: \"gitStatus\" },\n (frame) => {\n if (frame.t === \"gitStatus\") status = frame;\n },\n (error) => {\n if (status) resolve(status);\n else reject(error ?? new WorkbenchError(\"connection closed before gitStatus\"));\n },\n );\n });\n }\n\n /** Mirrors setup/commands.ts runSetupCommand (shell form, streamed output,\n * abort → graceful term-group kill, non-zero exit rejects). */\n runSetupCommand(\n cmd: string,\n cwd: string,\n onOutput: (stream: \"stdout\" | \"stderr\", data: string) => void,\n signal?: AbortSignal,\n ): Promise<void> {\n return new Promise((resolve, reject) => {\n if (signal?.aborted) {\n const error = new Error(\"Operation aborted\");\n error.name = \"AbortError\";\n reject(error);\n return;\n }\n let exitCode: number | null = null;\n let exited = false;\n let aborting = false;\n const socket = this.open(\n { op: \"exec\", command: cmd, cwd },\n (frame) => {\n if (frame.t === \"out\" && !aborting) {\n onOutput(frame.s, Buffer.from(frame.d, \"base64\").toString(\"utf8\"));\n } else if (frame.t === \"exit\") {\n exited = true;\n exitCode = frame.code;\n }\n },\n (error) => {\n signal?.removeEventListener(\"abort\", onAbort);\n if (aborting) {\n const abortError = new Error(\"Operation aborted\");\n abortError.name = \"AbortError\";\n reject(abortError);\n } else if (!exited) {\n reject(error ?? new WorkbenchError(\"connection closed before exit\"));\n } else if (exitCode === 0) {\n resolve();\n } else {\n reject(new Error(`Setup command exited with code ${exitCode}`));\n }\n },\n );\n const onAbort = (): void => {\n if (exited || aborting) return;\n aborting = true;\n writeFrame(socket, { t: \"signal\", mode: \"term-group\" });\n };\n signal?.addEventListener(\"abort\", onAbort, { once: true });\n });\n }\n\n /** Mirrors setup/commands.ts runStartCommand: returns a live handle that\n * emits exit/error and whose kill() runs the graceful term-group stop. */\n runStartCommand(\n cmd: string,\n cwd: string,\n onOutput: (stream: \"stdout\" | \"stderr\", data: string) => void,\n ): RemoteProcessHandle {\n let socket: Socket | undefined;\n const handle = new RemoteProcessHandle(() => {\n if (socket) writeFrame(socket, { t: \"signal\", mode: \"term-group\" });\n });\n let exited = false;\n socket = this.open(\n { op: \"exec\", command: cmd, cwd },\n (frame) => {\n if (frame.t === \"out\") {\n onOutput(frame.s, Buffer.from(frame.d, \"base64\").toString(\"utf8\"));\n } else if (frame.t === \"exit\") {\n exited = true;\n handle.exitCode = frame.code ?? (frame.signal ? null : 0);\n handle.emit(\"exit\", frame.code, frame.signal);\n }\n },\n (error) => {\n if (!exited) {\n // Connection died without an exit frame (launcher restart, network\n // teardown): surface as an error + synthetic exit so the supervisor\n // observes the process ending rather than hanging on it forever.\n exited = true;\n handle.exitCode = handle.exitCode ?? 1;\n handle.emit(\n \"error\",\n error ?? new WorkbenchError(\"workbench connection lost\", \"workbench_gone\"),\n );\n handle.emit(\"exit\", handle.exitCode, null);\n }\n },\n );\n return handle;\n }\n\n /** Mirrors promisified execFile (argv form, no shell): resolves stdout,\n * rejects with stderr-bearing error on non-zero exit. */\n execFile(\n file: string,\n args: string[],\n opts: { cwd?: string; timeout?: number; maxBuffer?: number } = {},\n ): Promise<{ stdout: string; stderr: string }> {\n return new Promise((resolve, reject) => {\n const stdout: Buffer[] = [];\n const stderr: Buffer[] = [];\n let stdoutBytes = 0;\n let stderrBytes = 0;\n const maxBuffer = opts.maxBuffer;\n let overflowed = false;\n let exitCode: number | null = null;\n let exitSignal: string | null = null;\n let exited = false;\n let timedOut = false;\n const socket = this.open(\n { op: \"exec\", argv: [file, ...args], cwd: opts.cwd ?? process.cwd() },\n (frame) => {\n if (frame.t === \"out\") {\n const buf = Buffer.from(frame.d, \"base64\");\n if (frame.s === \"stdout\") {\n stdout.push(buf);\n stdoutBytes += buf.length;\n } else {\n stderr.push(buf);\n stderrBytes += buf.length;\n }\n // Enforce maxBuffer per-stream, mirroring node's execFile: kill the\n // command group once either stream overflows so a runaway process\n // can't buffer unbounded memory in-process.\n if (\n maxBuffer !== undefined &&\n !overflowed &&\n (stdoutBytes > maxBuffer || stderrBytes > maxBuffer)\n ) {\n overflowed = true;\n if (timer) clearTimeout(timer);\n writeFrame(socket, { t: \"signal\", mode: \"term-group\" });\n }\n } else if (frame.t === \"exit\") {\n exited = true;\n exitCode = frame.code;\n exitSignal = frame.signal;\n }\n },\n (error) => {\n if (timer) clearTimeout(timer);\n settleExecOutcome(\n {\n file,\n args,\n stdout,\n stderr,\n overflowed,\n exited,\n exitCode,\n exitSignal,\n timedOut,\n maxBuffer,\n error: error ?? null,\n },\n resolve,\n reject,\n );\n },\n );\n const timer = opts.timeout\n ? setTimeout(() => {\n timedOut = true;\n writeFrame(socket, { t: \"signal\", mode: \"term-group\" });\n }, opts.timeout)\n : null;\n timer?.unref();\n });\n }\n\n /**\n * Synchronous PtySpawn adapter: returns a PtyProcess immediately; writes\n * and resizes are queued until the connection opens. Exit surfaces through\n * onExit exactly as node-pty's would (a lost connection = exit code 1 —\n * the session's finalizeOnExit path treats it as a crashed CLI).\n */\n spawnPty(file: string, args: string[], options: PtySpawnOptions): PtyProcess {\n const dataListeners: Array<(data: string) => void> = [];\n const exitListeners: Array<(event: { exitCode: number }) => void> = [];\n let connected = false;\n let exited = false;\n const preConnectQueue: WorkbenchFrame[] = [];\n\n const socket = this.open(\n {\n op: \"pty\",\n file,\n args,\n cwd: options.cwd,\n env: options.env,\n cols: options.cols,\n rows: options.rows,\n },\n (frame) => {\n if (frame.t === \"data\") {\n const text = Buffer.from(frame.d, \"base64\").toString(\"utf8\");\n for (const listener of dataListeners) listener(text);\n } else if (frame.t === \"exit\") {\n exited = true;\n for (const listener of exitListeners) listener({ exitCode: frame.code ?? 1 });\n }\n },\n () => {\n if (!exited) {\n exited = true;\n for (const listener of exitListeners) listener({ exitCode: 1 });\n }\n },\n );\n socket.on(\"connect\", () => {\n connected = true;\n for (const frame of preConnectQueue.splice(0)) writeFrame(socket, frame);\n });\n\n const send = (frame: WorkbenchFrame): void => {\n if (exited) return;\n if (connected) writeFrame(socket, frame);\n else preConnectQueue.push(frame);\n };\n\n return {\n onData: (listener) => dataListeners.push(listener),\n onExit: (listener) => exitListeners.push(listener),\n write: (data) => send({ t: \"input\", d: Buffer.from(data, \"utf8\").toString(\"base64\") }),\n resize: (cols, rows) => send({ t: \"resize\", cols, rows }),\n kill: (sig) => send({ t: \"kill\", ...(sig ? { sig } : {}) }),\n };\n }\n\n readFile(path: string): Promise<Buffer> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n let ended = false;\n this.open(\n { op: \"readFile\", path },\n (frame) => {\n if (frame.t === \"data\") chunks.push(Buffer.from(frame.d, \"base64\"));\n else if (frame.t === \"end\") ended = true;\n },\n (error) => {\n if (ended) resolve(Buffer.concat(chunks));\n else reject(error ?? new WorkbenchError(\"connection closed before end\"));\n },\n );\n });\n }\n\n stat(path: string): Promise<{\n exists: boolean;\n isFile: boolean;\n isDirectory: boolean;\n size: number;\n mtimeMs: number;\n }> {\n return new Promise((resolve, reject) => {\n let stat: {\n exists: boolean;\n isFile: boolean;\n isDirectory: boolean;\n size: number;\n mtimeMs: number;\n } | null = null;\n this.open(\n { op: \"stat\", path },\n (frame) => {\n if (frame.t === \"stat\") {\n stat = {\n exists: frame.exists,\n isFile: frame.isFile,\n isDirectory: frame.isDirectory,\n size: frame.size,\n mtimeMs: frame.mtimeMs,\n };\n }\n },\n (error) => {\n if (stat) resolve(stat);\n else reject(error ?? new WorkbenchError(\"connection closed before stat\"));\n },\n );\n });\n }\n\n readdir(path: string): Promise<string[]> {\n return new Promise((resolve, reject) => {\n let entries: string[] | null = null;\n this.open(\n { op: \"readdir\", path },\n (frame) => {\n if (frame.t === \"entries\") entries = frame.names;\n },\n (error) => {\n if (entries) resolve(entries);\n else reject(error ?? new WorkbenchError(\"connection closed before entries\"));\n },\n );\n });\n }\n}\n\nlet singleton: WorkbenchClient | null = null;\n\n/** Lazy env-configured client (split-mode pods). */\nexport function getWorkbenchClient(): WorkbenchClient {\n singleton ??= new WorkbenchClient();\n return singleton;\n}\n\n/** Test seam. */\nexport function resetWorkbenchClient(): void {\n singleton = null;\n}\n","/** Error surfaced by the workbench client for launcher-side failures; `code`\n * carries the remote error code (e.g. \"ENOENT\", \"unauthorized\") so callers'\n * existing errno handling keeps working across the container boundary. */\nexport class WorkbenchError extends Error {\n code?: string;\n constructor(message: string, code?: string) {\n super(message);\n this.name = \"WorkbenchError\";\n if (code) this.code = code;\n }\n}\n","import { EventEmitter } from \"node:events\";\n\n/**\n * Structural stand-in for the ChildProcess surface the workspace-command\n * supervisor actually uses (pid/exitCode/kill + exit/error events) — see\n * ManagedChildProcess in setup/commands.ts. kill() requests the launcher's\n * graceful term-group sequence regardless of the signal argument.\n */\nexport class RemoteProcessHandle extends EventEmitter {\n pid: number | undefined = undefined;\n exitCode: number | null = null;\n\n constructor(private readonly sendSignal: () => void) {\n super();\n }\n\n kill(_signal?: NodeJS.Signals | number): boolean {\n this.sendSignal();\n return true;\n }\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/runner/session-runner-helpers.ts","../src/boot/git-prep.ts","../src/setup/boot-milestone.ts"],"sourcesContent":["import { readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type { ChatMessage, TaskContextDTO } from \"@project/shared\";\n\nexport function mapChatHistory(\n messages: TaskContextDTO[\"chatHistory\"] | undefined | null,\n): ChatMessage[] {\n if (!messages) return [];\n return messages.map((m) => ({\n id: m.id,\n role: (m.role ?? \"user\") as \"user\" | \"assistant\" | \"system\",\n content: m.content ?? \"\",\n userId: m.userId,\n userName: m.user?.name ?? undefined,\n createdAt: m.createdAt,\n ...(m.source ? { source: m.source } : {}),\n ...(m.files && m.files.length > 0\n ? {\n files: m.files.map((f) => ({\n fileId: f.id,\n fileName: f.fileName,\n mimeType: f.mimeType,\n fileSize: f.fileSize,\n downloadUrl: f.downloadUrl ?? \"\",\n content: f.content,\n contentEncoding: f.contentEncoding,\n })),\n }\n : {}),\n }));\n}\n\n/** Read this agent's version from its bundled package.json. */\nexport function readAgentVersion(): string | null {\n try {\n const here = dirname(fileURLToPath(import.meta.url));\n // Walk up: dist/runner/session-runner.js → dist/ → package.json\n for (const rel of [\"../package.json\", \"../../package.json\"]) {\n try {\n const pkg = JSON.parse(readFileSync(join(here, rel), \"utf-8\")) as { version?: string };\n if (pkg.version) return pkg.version;\n } catch {\n /* try next candidate */\n }\n }\n } catch {\n /* ignore */\n }\n return null;\n}\n","import { execFile } from \"node:child_process\";\nimport { existsSync, mkdirSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { promisify } from \"node:util\";\nimport type { BootstrapBundle } from \"../setup/bootstrap-bundle-types.js\";\nimport type { BootLogger } from \"./types.js\";\nimport { gitCredentialHelper, syncGithubTokenFiles, writeGitCredential } from \"./git-credential.js\";\n\n/**\n * Ports `prepare_workspace_git` + `sync_task_branch_to_repo` +\n * `reset_tracked_repo_changes_before_assignment_checkout`\n * (entrypoint.sh:623-829) and the workbench bounded retry loop\n * (entrypoint.sh:877-893). The bash signalled via marker files on the shared\n * emptyDir; vnext replaces the markers with daemon-owned state — `GitPrepJob`\n * holds it and the workbench daemon serves it over the `gitStatus` op.\n *\n * Contract carried from the old entrypoint-git-prep.test.ts (verbatim — the\n * agent side depends on it):\n * - `syncTaskBranchToRepo` NEVER touches BRANCH/CHECKOUT_REF and does NO\n * merge. It refreshes the origin remote with the fresh installation token,\n * warms `origin/<base>` (warn-only on failure — the ref may already be\n * present from the bake), and resets tracked changes. The agent's\n * `ensureOnTaskBranch` owns the authoritative checkout.\n * - Clone paths: with checkoutRef → depth-1 base clone, fetch the ref to\n * refs/remotes/origin/pr-checkout, `checkout -f -B <branch>` onto it.\n * Without → clone the BASE branch at FULL depth (a naive `--branch <task>`\n * dies when the task branch was never pushed; `--depth 1` caused the\n * \"refusing to merge unrelated histories\" incident), then sync.\n * - No git plan (empty token/owner/name/branch) → immediate ready.\n *\n * Every fallible git call is guarded — `prepareWorkspaceGit` NEVER throws\n * (the bash equivalent: every error path wrote the failed marker instead of\n * exiting the backgrounded subshell). Reasons stay the short bash marker\n * strings; redacted detail goes to the log.\n */\n\nexport type GitPrepState =\n | { state: \"pending\" }\n | { state: \"ready\" }\n | { state: \"failed\"; reason: string };\n\n/** Async git runner — always `execFile` with a timeout, never execSync (a\n * sync child freezes the event loop; see runner/git-utils.ts history). */\nexport type GitFn = (\n args: string[],\n opts?: { cwd?: string; timeoutMs?: number },\n) => Promise<{ stdout: string }>;\n\nexport interface GitPrepDeps {\n git: GitFn;\n bundle: BootstrapBundle;\n /** env CONVEYOR_POD_IMAGE === \"1\" — log-line fidelity only; behavior matches. */\n podImage: boolean;\n /** default \"/workspaces\" */\n workspacesDir?: string;\n log: BootLogger;\n}\n\nconst QUICK_GIT_TIMEOUT_MS = 60_000;\n// FETCH/CLONE timeouts are exported so setup/git-ready.ts can DERIVE its gate\n// deadline from the daemon's actual retry envelope instead of hand-picking a\n// number that silently drifts when these change.\nexport const FETCH_TIMEOUT_MS = 300_000;\nexport const CLONE_TIMEOUT_MS = 600_000;\n\nconst execFileAsync = promisify(execFile);\n\n/** `mkdir -p`. Shared with the workbench boot's `ensureWorkspaceDir` default so\n * its deps aggregator reuses this module rather than pulling in node:fs. */\nexport function ensureDir(dir: string): void {\n mkdirSync(dir, { recursive: true });\n}\n\n/** Production GitFn. */\nexport function defaultGit(\n args: string[],\n opts: { cwd?: string; timeoutMs?: number } = {},\n): Promise<{ stdout: string }> {\n return execFileAsync(\"git\", args, {\n cwd: opts.cwd,\n timeout: opts.timeoutMs ?? QUICK_GIT_TIMEOUT_MS,\n maxBuffer: 10 * 1024 * 1024,\n });\n}\n\n/** Remote URLs embed the installation token; execFile error messages embed\n * the command line. Redact before ANY log/reason sink. */\nexport function redactToken(text: string): string {\n return text.replace(/x-access-token:[^@]*@/g, \"x-access-token:***@\");\n}\n\nfunction errText(err: unknown): string {\n return redactToken(err instanceof Error ? err.message : String(err));\n}\n\ninterface PrepPaths {\n workspacesDir: string;\n repoDir: string;\n remoteUrl: string;\n credentialHelper: string;\n}\n\n/** Refresh the remote token + warm origin/<base> + reset tracked changes.\n * Deliberately no BRANCH/CHECKOUT_REF handling — see module doc. */\nasync function syncTaskBranchToRepo(deps: GitPrepDeps, paths: PrepPaths): Promise<GitPrepState> {\n const { git, log } = deps;\n const { branch, baseBranch } = deps.bundle.gitPlan;\n try {\n await git([\"remote\", \"set-url\", \"origin\", paths.remoteUrl], {\n cwd: paths.repoDir,\n timeoutMs: QUICK_GIT_TIMEOUT_MS,\n });\n } catch (err) {\n log.error(`[boot] ERROR: remote set-url failed: ${errText(err)}`);\n return { state: \"failed\", reason: \"remote set-url failed\" };\n }\n const credentialFailure = await persistCredentialHelper(deps, paths);\n if (credentialFailure) return credentialFailure;\n // Warm origin/<base> so the agent's checkout/fetch is a fast-forward.\n // Warn-only: a stale-but-present origin/<base> from the bake still works.\n try {\n await git([\"fetch\", \"origin\", `+refs/heads/${baseBranch}:refs/remotes/origin/${baseBranch}`], {\n cwd: paths.repoDir,\n timeoutMs: FETCH_TIMEOUT_MS,\n });\n } catch (err) {\n log.warn(`[boot] WARN: fetch origin/${baseBranch} failed: ${errText(err)}`);\n }\n // The baked/pooled repo is not user-owned until this gate succeeds: reset\n // stale tracked image dirt so the agent's checkout isn't blocked. No\n // `git clean` — untracked prebake artifacts may be intentional.\n try {\n await git([\"reset\", \"--hard\", \"HEAD\"], { cwd: paths.repoDir, timeoutMs: QUICK_GIT_TIMEOUT_MS });\n } catch (err) {\n log.error(\n `[boot] ERROR: failed to clean tracked repo changes before checkout: ${errText(err)}`,\n );\n return { state: \"failed\", reason: \"pre-checkout reset failed\" };\n }\n log.info(`[boot] Repo remote ready; agent will checkout ${branch}`);\n return { state: \"ready\" };\n}\n\n/**\n * `git -c credential.helper=… clone` scopes the setting to that ONE invocation;\n * it is NOT written into the new repo's config (only `git clone --config` does\n * that). The remote URL is credential-free by design now, so without this the\n * cloned repo has no way to authenticate and every later fetch/push — the\n * agent's `ensureOnTaskBranch`, the WIP snapshot flush — fails.\n */\nasync function persistCredentialHelper(\n deps: GitPrepDeps,\n paths: PrepPaths,\n): Promise<GitPrepState | null> {\n try {\n await deps.git([\"config\", \"--local\", \"credential.helper\", paths.credentialHelper], {\n cwd: paths.repoDir,\n timeoutMs: QUICK_GIT_TIMEOUT_MS,\n });\n return null;\n } catch (err) {\n deps.log.error(`[boot] ERROR: credential helper config failed: ${errText(err)}`);\n return { state: \"failed\", reason: \"credential helper config failed\" };\n }\n}\n\nasync function clonePostAssignment(deps: GitPrepDeps, paths: PrepPaths): Promise<GitPrepState> {\n const { git, log } = deps;\n const { branch, baseBranch, checkoutRef } = deps.bundle.gitPlan;\n log.info(\"[boot] Cloning repo post-assignment (pre-clone was missing)...\");\n if (checkoutRef) {\n try {\n await git(\n [\n \"-c\",\n `credential.helper=${paths.credentialHelper}`,\n \"clone\",\n \"--depth\",\n \"1\",\n \"--single-branch\",\n \"--branch\",\n baseBranch,\n paths.remoteUrl,\n \"repo\",\n ],\n { cwd: paths.workspacesDir, timeoutMs: CLONE_TIMEOUT_MS },\n );\n } catch (err) {\n log.error(`[boot] ERROR: post-assignment clone failed: ${errText(err)}`);\n return { state: \"failed\", reason: \"post-assignment clone failed\" };\n }\n // Before the very next fetch — it authenticates against origin too.\n const credentialFailure = await persistCredentialHelper(deps, paths);\n if (credentialFailure) return credentialFailure;\n try {\n await git([\"fetch\", \"origin\", `+${checkoutRef}:refs/remotes/origin/pr-checkout`], {\n cwd: paths.repoDir,\n timeoutMs: FETCH_TIMEOUT_MS,\n });\n } catch (err) {\n log.error(`[boot] ERROR: post-assignment fetch of ${checkoutRef} failed: ${errText(err)}`);\n return { state: \"failed\", reason: `post-assignment fetch of ${checkoutRef} failed` };\n }\n // -f: an untracked bake artifact can collide with a path the target ref\n // tracks; the repo is not user-owned yet, so forcing is safe.\n try {\n await git([\"checkout\", \"-f\", \"-B\", branch, \"refs/remotes/origin/pr-checkout\"], {\n cwd: paths.repoDir,\n timeoutMs: QUICK_GIT_TIMEOUT_MS,\n });\n } catch (err) {\n log.error(`[boot] ERROR: post-assignment checkout of ${checkoutRef} failed: ${errText(err)}`);\n return { state: \"failed\", reason: `post-assignment checkout of ${checkoutRef} failed` };\n }\n return { state: \"ready\" };\n }\n // FULL depth base-branch clone — see module doc for both incidents.\n try {\n await git(\n [\n \"-c\",\n `credential.helper=${paths.credentialHelper}`,\n \"clone\",\n \"--single-branch\",\n \"--branch\",\n baseBranch,\n paths.remoteUrl,\n \"repo\",\n ],\n { cwd: paths.workspacesDir, timeoutMs: CLONE_TIMEOUT_MS },\n );\n } catch (err) {\n log.error(\n `[boot] ERROR: post-assignment clone of base '${baseBranch}' failed: ${errText(err)}`,\n );\n return { state: \"failed\", reason: \"post-assignment clone failed\" };\n }\n return syncTaskBranchToRepo(deps, paths);\n}\n\n/** ONE preparation attempt. Never throws. */\nexport async function prepareWorkspaceGit(deps: GitPrepDeps): Promise<GitPrepState> {\n const { bundle, log } = deps;\n const workspacesDir = deps.workspacesDir ?? \"/workspaces\";\n const repoDir = join(workspacesDir, \"repo\");\n const { branch, cloneUrl, repoOwner, repoName } = bundle.gitPlan;\n const credential = bundle.gitCredential;\n const paths: PrepPaths = {\n workspacesDir,\n repoDir,\n remoteUrl: cloneUrl,\n credentialHelper: gitCredentialHelper(repoDir),\n };\n try {\n if (\n !cloneUrl ||\n !repoOwner ||\n !repoName ||\n !branch ||\n !credential.username ||\n !credential.secret\n ) {\n log.info(\"[boot] No git plan to prepare — marking git ready.\");\n return { state: \"ready\" };\n }\n writeGitCredential(repoDir, cloneUrl, credential);\n // Recompute AFTER the write: `writeGitCredential` installs the managed\n // helper script, and `gitCredentialHelper` only returns it once it exists.\n // Reading it before the write pinned every fresh pod to the plain `store`\n // helper, which deletes its own file when GitHub rejects an expired token.\n paths.credentialHelper = gitCredentialHelper(repoDir);\n // Give the `gh` CLI a file-based credential from the first turn. Every\n // later token refresh rewrites the same files, so `gh` never depends on\n // the frozen env var the spawned CLI inherits.\n if (bundle.gitPlan.provider === \"github\") {\n syncGithubTokenFiles(bundle.githubToken ?? credential.secret);\n }\n if (existsSync(join(repoDir, \".git\"))) {\n // Do NOT silently fall through to the image snapshot on failure — a\n // stale image repo has bitten us before (old scripts, wrong deps) and is\n // brutal to diagnose from pod logs. Fail loud via the returned state.\n log.info(\n deps.podImage\n ? `[boot] Pod image — updating repo to latest (branch=${branch})...`\n : `[boot] Repo present (non-pod-image) — updating repo to latest (branch=${branch})...`,\n );\n return await syncTaskBranchToRepo(deps, paths);\n }\n try {\n mkdirSync(workspacesDir, { recursive: true });\n } catch {\n /* clone below surfaces the real failure */\n }\n return await clonePostAssignment(deps, paths);\n } catch (err) {\n // Belt-and-braces: nothing above should throw, but this function's\n // contract is \"never throws\" (the bash never `exit`ed the subshell).\n log.error(`[boot] ERROR: git prep failed unexpectedly: ${errText(err)}`);\n return { state: \"failed\", reason: \"git prep failed unexpectedly\" };\n }\n}\n\n// Brief-mandated attempt count: 3 total. Not a literal match for the bash\n// workbench retry loop (entrypoint.sh:883) — bash did 1 initial attempt + 3\n// retries = 4 attempts total; the TS port intentionally caps at 3.\nexport const GIT_PREP_MAX_RETRIES = 3;\n/** Exported for setup/git-ready.ts's derived gate deadline (see above). */\nexport const DEFAULT_RETRY_DELAY_MS = 10_000;\n\nexport interface GitPrepJobExtras {\n /** Awaited BEFORE status flips ready — graphify bind + grimoire submodule +\n * skill links. Claude must not spawn before skills exist. */\n onReady: () => Promise<void>;\n /** Runs AFTER ready — reference-repo clones must never block Claude. */\n afterReady: () => Promise<void>;\n /** default 10s (the bash loop's poll cadence); tests shrink it. */\n retryDelayMs?: number;\n}\n\n/**\n * Daemon-owned replacement for the marker files + workbench retry loop: up to\n * `GIT_PREP_MAX_RETRIES` `prepareWorkspaceGit` attempts, then give up leaving\n * `status` failed (the agent surfaces it). While a retry is still possible the\n * status stays `pending`, never transiently `failed` — the agent's gitStatus\n * gate treats `failed` as fatal, and the bash marker dance had exactly this\n * race (agent glimpses the failed marker before the retry loop clears it).\n */\nexport class GitPrepJob {\n private current: GitPrepState = { state: \"pending\" };\n private started = false;\n\n constructor(\n private readonly deps: GitPrepDeps,\n private readonly extras: GitPrepJobExtras,\n ) {}\n\n get status(): GitPrepState {\n return this.current;\n }\n\n /** Kick off the background attempts. Idempotent. */\n start(): void {\n if (this.started) return;\n this.started = true;\n void this.run();\n }\n\n private async run(): Promise<void> {\n const { log } = this.deps;\n const retryDelayMs = this.extras.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;\n for (let attempt = 1; attempt <= GIT_PREP_MAX_RETRIES; attempt++) {\n const result = await prepareWorkspaceGit(this.deps);\n if (result.state === \"ready\") {\n // Binds gate readiness but are best-effort — a graphify/grimoire\n // failure must never fail the git gate itself.\n try {\n await this.extras.onReady();\n } catch (err) {\n log.warn(`[boot] WARN: pre-ready binds failed: ${errText(err)}`);\n }\n this.current = { state: \"ready\" };\n try {\n await this.extras.afterReady();\n } catch (err) {\n log.warn(`[boot] WARN: post-ready work failed: ${errText(err)}`);\n }\n return;\n }\n if (attempt >= GIT_PREP_MAX_RETRIES) {\n log.error(\n `[boot] workbench git prep failed ${GIT_PREP_MAX_RETRIES} times — giving up (agent surfaces the failure).`,\n );\n this.current = result;\n return;\n }\n log.warn(\n `[boot] workbench git prep failed — retrying (attempt ${attempt}/${GIT_PREP_MAX_RETRIES})...`,\n );\n await new Promise<void>((resolve) => {\n setTimeout(resolve, retryDelayMs);\n });\n }\n }\n}\n","/**\n * Pod-side bootstrap-milestone reporter. The pod alone observes when the\n * workspace git is up to date, when its sidecars are ready, and when the\n * start command has launched; it reports those milestones to the API over the\n * same bootstrap-token channel the bundle poll and crash reporter use\n * (`POST /api/v3/pods/boot-milestone`). The API\n * records them on `Workspace.bootTimeline`, which drives the agent-tab progress\n * meter. Server-owned milestones (pod_created/pod_scheduled/containers_ready/\n * agent_connected/app_serving) are never reported from here — the API enforces\n * the allow-list.\n *\n * Fire-and-forget: a failed or slow report must never delay start, so\n * every path swallows errors and the whole thing no-ops off-pod (GitHub\n * Codespaces / local), where the bootstrap token is absent.\n */\nimport type { BootStepKey } from \"@project/shared\";\n\nconst REPORT_TIMEOUT_MS = 5_000;\n\n/** The steps a pod may report. Mirrors the API's `POD_REPORTABLE_BOOT_STEPS`. */\nexport type PodReportableBootStep = Extract<\n BootStepKey,\n | \"workbench_ready\"\n | \"repo_synced\"\n | \"sidecars_ready\"\n | \"branch_ready\"\n | \"agent_live\"\n | \"start_command_launched\"\n>;\n\nexport interface ReportBootMilestoneOptions {\n key: PodReportableBootStep;\n /** Defaults to `process.env`. Injected for tests. */\n env?: NodeJS.ProcessEnv;\n /** Injected for tests; defaults to global fetch. */\n fetchFn?: typeof fetch;\n timeoutMs?: number;\n}\n\n/**\n * Off-pod fallback sender (GitHub Codespaces): no bootstrap token exists\n * there, but once the agent socket is up its authenticated channel can carry\n * the same milestones. Registered by SessionRunner after connect; milestones\n * fired before registration are dropped — on the codespace step list those\n * early keys aren't rendered anyway.\n */\nlet socketFallback: ((key: PodReportableBootStep) => void) | null = null;\n\nexport function registerBootMilestoneSocketFallback(\n fn: ((key: PodReportableBootStep) => void) | null,\n): void {\n socketFallback = fn;\n}\n\n/**\n * Best-effort POST of a boot milestone. Resolves to `true` when the API\n * acknowledged (HTTP 2xx), `false` otherwise — including the off-pod paths.\n * Never throws. Off-pod (no bootstrap token), the registered socket fallback\n * carries the milestone instead of the HTTP route.\n */\nexport async function reportBootMilestone(opts: ReportBootMilestoneOptions): Promise<boolean> {\n const env = opts.env ?? process.env;\n const apiUrl = env.CONVEYOR_API_URL;\n const token = env.POD_BOOTSTRAP_TOKEN;\n // Only claudespace v3 pods carry both — elsewhere the socket fallback (when\n // registered) feeds the meter instead.\n if (!apiUrl || !token) {\n try {\n socketFallback?.(opts.key);\n } catch {\n // fire-and-forget contract\n }\n return false;\n }\n\n const fetchFn = opts.fetchFn ?? fetch;\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? REPORT_TIMEOUT_MS);\n try {\n const res = await fetchFn(`${apiUrl.replace(/\\/$/, \"\")}/api/v3/pods/boot-milestone`, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n authorization: `Bearer ${token}`,\n },\n body: JSON.stringify({ key: opts.key }),\n signal: controller.signal,\n });\n return res.ok;\n } catch {\n return false;\n } finally {\n clearTimeout(timer);\n }\n}\n"],"mappings":";;;;;;;AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS,YAAY;AAC9B,SAAS,qBAAqB;AAGvB,SAAS,eACd,UACe;AACf,MAAI,CAAC,SAAU,QAAO,CAAC;AACvB,SAAO,SAAS,IAAI,CAAC,OAAO;AAAA,IAC1B,IAAI,EAAE;AAAA,IACN,MAAO,EAAE,QAAQ;AAAA,IACjB,SAAS,EAAE,WAAW;AAAA,IACtB,QAAQ,EAAE;AAAA,IACV,UAAU,EAAE,MAAM,QAAQ;AAAA,IAC1B,WAAW,EAAE;AAAA,IACb,GAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,OAAO,IAAI,CAAC;AAAA,IACvC,GAAI,EAAE,SAAS,EAAE,MAAM,SAAS,IAC5B;AAAA,MACE,OAAO,EAAE,MAAM,IAAI,CAAC,OAAO;AAAA,QACzB,QAAQ,EAAE;AAAA,QACV,UAAU,EAAE;AAAA,QACZ,UAAU,EAAE;AAAA,QACZ,UAAU,EAAE;AAAA,QACZ,aAAa,EAAE,eAAe;AAAA,QAC9B,SAAS,EAAE;AAAA,QACX,iBAAiB,EAAE;AAAA,MACrB,EAAE;AAAA,IACJ,IACA,CAAC;AAAA,EACP,EAAE;AACJ;AAGO,SAAS,mBAAkC;AAChD,MAAI;AACF,UAAM,OAAO,QAAQ,cAAc,YAAY,GAAG,CAAC;AAEnD,eAAW,OAAO,CAAC,mBAAmB,oBAAoB,GAAG;AAC3D,UAAI;AACF,cAAM,MAAM,KAAK,MAAM,aAAa,KAAK,MAAM,GAAG,GAAG,OAAO,CAAC;AAC7D,YAAI,IAAI,QAAS,QAAO,IAAI;AAAA,MAC9B,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO;AACT;;;AClDA,SAAS,gBAAgB;AACzB,SAAS,YAAY,iBAAiB;AACtC,SAAS,QAAAA,aAAY;AACrB,SAAS,iBAAiB;AAuD1B,IAAM,uBAAuB;AAItB,IAAM,mBAAmB;AACzB,IAAM,mBAAmB;AAEhC,IAAM,gBAAgB,UAAU,QAAQ;AAIjC,SAAS,UAAU,KAAmB;AAC3C,YAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AACpC;AAGO,SAAS,WACd,MACA,OAA6C,CAAC,GACjB;AAC7B,SAAO,cAAc,OAAO,MAAM;AAAA,IAChC,KAAK,KAAK;AAAA,IACV,SAAS,KAAK,aAAa;AAAA,IAC3B,WAAW,KAAK,OAAO;AAAA,EACzB,CAAC;AACH;AAIO,SAAS,YAAY,MAAsB;AAChD,SAAO,KAAK,QAAQ,0BAA0B,qBAAqB;AACrE;AAEA,SAAS,QAAQ,KAAsB;AACrC,SAAO,YAAY,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AACrE;AAWA,eAAe,qBAAqB,MAAmB,OAAyC;AAC9F,QAAM,EAAE,KAAK,IAAI,IAAI;AACrB,QAAM,EAAE,QAAQ,WAAW,IAAI,KAAK,OAAO;AAC3C,MAAI;AACF,UAAM,IAAI,CAAC,UAAU,WAAW,UAAU,MAAM,SAAS,GAAG;AAAA,MAC1D,KAAK,MAAM;AAAA,MACX,WAAW;AAAA,IACb,CAAC;AAAA,EACH,SAAS,KAAK;AACZ,QAAI,MAAM,wCAAwC,QAAQ,GAAG,CAAC,EAAE;AAChE,WAAO,EAAE,OAAO,UAAU,QAAQ,wBAAwB;AAAA,EAC5D;AACA,QAAM,oBAAoB,MAAM,wBAAwB,MAAM,KAAK;AACnE,MAAI,kBAAmB,QAAO;AAG9B,MAAI;AACF,UAAM,IAAI,CAAC,SAAS,UAAU,eAAe,UAAU,wBAAwB,UAAU,EAAE,GAAG;AAAA,MAC5F,KAAK,MAAM;AAAA,MACX,WAAW;AAAA,IACb,CAAC;AAAA,EACH,SAAS,KAAK;AACZ,QAAI,KAAK,6BAA6B,UAAU,YAAY,QAAQ,GAAG,CAAC,EAAE;AAAA,EAC5E;AAIA,MAAI;AACF,UAAM,IAAI,CAAC,SAAS,UAAU,MAAM,GAAG,EAAE,KAAK,MAAM,SAAS,WAAW,qBAAqB,CAAC;AAAA,EAChG,SAAS,KAAK;AACZ,QAAI;AAAA,MACF,uEAAuE,QAAQ,GAAG,CAAC;AAAA,IACrF;AACA,WAAO,EAAE,OAAO,UAAU,QAAQ,4BAA4B;AAAA,EAChE;AACA,MAAI,KAAK,iDAAiD,MAAM,EAAE;AAClE,SAAO,EAAE,OAAO,QAAQ;AAC1B;AASA,eAAe,wBACb,MACA,OAC8B;AAC9B,MAAI;AACF,UAAM,KAAK,IAAI,CAAC,UAAU,WAAW,qBAAqB,MAAM,gBAAgB,GAAG;AAAA,MACjF,KAAK,MAAM;AAAA,MACX,WAAW;AAAA,IACb,CAAC;AACD,WAAO;AAAA,EACT,SAAS,KAAK;AACZ,SAAK,IAAI,MAAM,kDAAkD,QAAQ,GAAG,CAAC,EAAE;AAC/E,WAAO,EAAE,OAAO,UAAU,QAAQ,kCAAkC;AAAA,EACtE;AACF;AAEA,eAAe,oBAAoB,MAAmB,OAAyC;AAC7F,QAAM,EAAE,KAAK,IAAI,IAAI;AACrB,QAAM,EAAE,QAAQ,YAAY,YAAY,IAAI,KAAK,OAAO;AACxD,MAAI,KAAK,gEAAgE;AACzE,MAAI,aAAa;AACf,QAAI;AACF,YAAM;AAAA,QACJ;AAAA,UACE;AAAA,UACA,qBAAqB,MAAM,gBAAgB;AAAA,UAC3C;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA,MAAM;AAAA,UACN;AAAA,QACF;AAAA,QACA,EAAE,KAAK,MAAM,eAAe,WAAW,iBAAiB;AAAA,MAC1D;AAAA,IACF,SAAS,KAAK;AACZ,UAAI,MAAM,+CAA+C,QAAQ,GAAG,CAAC,EAAE;AACvE,aAAO,EAAE,OAAO,UAAU,QAAQ,+BAA+B;AAAA,IACnE;AAEA,UAAM,oBAAoB,MAAM,wBAAwB,MAAM,KAAK;AACnE,QAAI,kBAAmB,QAAO;AAC9B,QAAI;AACF,YAAM,IAAI,CAAC,SAAS,UAAU,IAAI,WAAW,kCAAkC,GAAG;AAAA,QAChF,KAAK,MAAM;AAAA,QACX,WAAW;AAAA,MACb,CAAC;AAAA,IACH,SAAS,KAAK;AACZ,UAAI,MAAM,0CAA0C,WAAW,YAAY,QAAQ,GAAG,CAAC,EAAE;AACzF,aAAO,EAAE,OAAO,UAAU,QAAQ,4BAA4B,WAAW,UAAU;AAAA,IACrF;AAGA,QAAI;AACF,YAAM,IAAI,CAAC,YAAY,MAAM,MAAM,QAAQ,iCAAiC,GAAG;AAAA,QAC7E,KAAK,MAAM;AAAA,QACX,WAAW;AAAA,MACb,CAAC;AAAA,IACH,SAAS,KAAK;AACZ,UAAI,MAAM,6CAA6C,WAAW,YAAY,QAAQ,GAAG,CAAC,EAAE;AAC5F,aAAO,EAAE,OAAO,UAAU,QAAQ,+BAA+B,WAAW,UAAU;AAAA,IACxF;AACA,WAAO,EAAE,OAAO,QAAQ;AAAA,EAC1B;AAEA,MAAI;AACF,UAAM;AAAA,MACJ;AAAA,QACE;AAAA,QACA,qBAAqB,MAAM,gBAAgB;AAAA,QAC3C;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,MAAM;AAAA,QACN;AAAA,MACF;AAAA,MACA,EAAE,KAAK,MAAM,eAAe,WAAW,iBAAiB;AAAA,IAC1D;AAAA,EACF,SAAS,KAAK;AACZ,QAAI;AAAA,MACF,gDAAgD,UAAU,aAAa,QAAQ,GAAG,CAAC;AAAA,IACrF;AACA,WAAO,EAAE,OAAO,UAAU,QAAQ,+BAA+B;AAAA,EACnE;AACA,SAAO,qBAAqB,MAAM,KAAK;AACzC;AAGA,eAAsB,oBAAoB,MAA0C;AAClF,QAAM,EAAE,QAAQ,IAAI,IAAI;AACxB,QAAM,gBAAgB,KAAK,iBAAiB;AAC5C,QAAM,UAAUC,MAAK,eAAe,MAAM;AAC1C,QAAM,EAAE,QAAQ,UAAU,WAAW,SAAS,IAAI,OAAO;AACzD,QAAM,aAAa,OAAO;AAC1B,QAAM,QAAmB;AAAA,IACvB;AAAA,IACA;AAAA,IACA,WAAW;AAAA,IACX,kBAAkB,oBAAoB,OAAO;AAAA,EAC/C;AACA,MAAI;AACF,QACE,CAAC,YACD,CAAC,aACD,CAAC,YACD,CAAC,UACD,CAAC,WAAW,YACZ,CAAC,WAAW,QACZ;AACA,UAAI,KAAK,yDAAoD;AAC7D,aAAO,EAAE,OAAO,QAAQ;AAAA,IAC1B;AACA,uBAAmB,SAAS,UAAU,UAAU;AAKhD,UAAM,mBAAmB,oBAAoB,OAAO;AAIpD,QAAI,OAAO,QAAQ,aAAa,UAAU;AACxC,2BAAqB,OAAO,eAAe,WAAW,MAAM;AAAA,IAC9D;AACA,QAAI,WAAWA,MAAK,SAAS,MAAM,CAAC,GAAG;AAIrC,UAAI;AAAA,QACF,KAAK,WACD,2DAAsD,MAAM,SAC5D,8EAAyE,MAAM;AAAA,MACrF;AACA,aAAO,MAAM,qBAAqB,MAAM,KAAK;AAAA,IAC/C;AACA,QAAI;AACF,gBAAU,eAAe,EAAE,WAAW,KAAK,CAAC;AAAA,IAC9C,QAAQ;AAAA,IAER;AACA,WAAO,MAAM,oBAAoB,MAAM,KAAK;AAAA,EAC9C,SAAS,KAAK;AAGZ,QAAI,MAAM,+CAA+C,QAAQ,GAAG,CAAC,EAAE;AACvE,WAAO,EAAE,OAAO,UAAU,QAAQ,+BAA+B;AAAA,EACnE;AACF;AAKO,IAAM,uBAAuB;AAE7B,IAAM,yBAAyB;AAoB/B,IAAM,aAAN,MAAiB;AAAA,EAItB,YACmB,MACA,QACjB;AAFiB;AACA;AAAA,EAChB;AAAA,EAFgB;AAAA,EACA;AAAA,EALX,UAAwB,EAAE,OAAO,UAAU;AAAA,EAC3C,UAAU;AAAA,EAOlB,IAAI,SAAuB;AACzB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,QAAc;AACZ,QAAI,KAAK,QAAS;AAClB,SAAK,UAAU;AACf,SAAK,KAAK,IAAI;AAAA,EAChB;AAAA,EAEA,MAAc,MAAqB;AACjC,UAAM,EAAE,IAAI,IAAI,KAAK;AACrB,UAAM,eAAe,KAAK,OAAO,gBAAgB;AACjD,aAAS,UAAU,GAAG,WAAW,sBAAsB,WAAW;AAChE,YAAM,SAAS,MAAM,oBAAoB,KAAK,IAAI;AAClD,UAAI,OAAO,UAAU,SAAS;AAG5B,YAAI;AACF,gBAAM,KAAK,OAAO,QAAQ;AAAA,QAC5B,SAAS,KAAK;AACZ,cAAI,KAAK,wCAAwC,QAAQ,GAAG,CAAC,EAAE;AAAA,QACjE;AACA,aAAK,UAAU,EAAE,OAAO,QAAQ;AAChC,YAAI;AACF,gBAAM,KAAK,OAAO,WAAW;AAAA,QAC/B,SAAS,KAAK;AACZ,cAAI,KAAK,wCAAwC,QAAQ,GAAG,CAAC,EAAE;AAAA,QACjE;AACA;AAAA,MACF;AACA,UAAI,WAAW,sBAAsB;AACnC,YAAI;AAAA,UACF,oCAAoC,oBAAoB;AAAA,QAC1D;AACA,aAAK,UAAU;AACf;AAAA,MACF;AACA,UAAI;AAAA,QACF,6DAAwD,OAAO,IAAI,oBAAoB;AAAA,MACzF;AACA,YAAM,IAAI,QAAc,CAAC,YAAY;AACnC,mBAAW,SAAS,YAAY;AAAA,MAClC,CAAC;AAAA,IACH;AAAA,EACF;AACF;;;AC9WA,IAAM,oBAAoB;AA6B1B,IAAI,iBAAgE;AAE7D,SAAS,oCACd,IACM;AACN,mBAAiB;AACnB;AAQA,eAAsB,oBAAoB,MAAoD;AAC5F,QAAM,MAAM,KAAK,OAAO,QAAQ;AAChC,QAAM,SAAS,IAAI;AACnB,QAAM,QAAQ,IAAI;AAGlB,MAAI,CAAC,UAAU,CAAC,OAAO;AACrB,QAAI;AACF,uBAAiB,KAAK,GAAG;AAAA,IAC3B,QAAQ;AAAA,IAER;AACA,WAAO;AAAA,EACT;AAEA,QAAM,UAAU,KAAK,WAAW;AAChC,QAAM,aAAa,IAAI,gBAAgB;AACvC,QAAM,QAAQ,WAAW,MAAM,WAAW,MAAM,GAAG,KAAK,aAAa,iBAAiB;AACtF,MAAI;AACF,UAAM,MAAM,MAAM,QAAQ,GAAG,OAAO,QAAQ,OAAO,EAAE,CAAC,+BAA+B;AAAA,MACnF,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA,QAChB,eAAe,UAAU,KAAK;AAAA,MAChC;AAAA,MACA,MAAM,KAAK,UAAU,EAAE,KAAK,KAAK,IAAI,CAAC;AAAA,MACtC,QAAQ,WAAW;AAAA,IACrB,CAAC;AACD,WAAO,IAAI;AAAA,EACb,QAAQ;AACN,WAAO;AAAA,EACT,UAAE;AACA,iBAAa,KAAK;AAAA,EACpB;AACF;","names":["join","join"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/workbench/oom-watchdog.ts"],"sourcesContent":["/**\n * Early-OOM watchdog for the workbench container.\n *\n * On cgroup v2, Kubernetes sets `memory.oom.group=1` on every container:\n * when the workbench hits its memory limit the kernel kills EVERY process in\n * the container — dev servers, sshd, the claude PTY, the launcher itself —\n * then kubelet restart backoff and janitor restart-strikes compound the\n * damage. Autopilot exposes no kubelet knob to soften this and the cgroup\n * subtree is read-only in-container, so the fix is userspace: watch our own\n * cgroup's memory and kill the hungriest workload process group before the\n * kernel takes the whole container.\n *\n * Trigger design (validated on the real cluster, wb-oom-canary 2026-07-20):\n * a flat high threshold loses the race against fast allocators (a 2.5 GB/s\n * hog beat a 90%/250ms poll to the limit), while a flat low threshold wastes\n * memory for well-behaved workloads. So two conditions, checked every poll:\n * - headroom: less than `headroomBytes` left in the cgroup, or\n * - projection: current + 2×(last poll's growth) would cross the limit.\n * Fast spikes trip the projection early (~83% in the canary); slow growth\n * runs to high utilization (~88%) before the headroom floor fires.\n *\n * Victim selection: the largest-RSS process group other than the launcher's\n * own — the launcher already runs every workload as its own detached process\n * group precisely so it can be killed as a unit (see server.ts).\n */\n\nimport { readdirSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { BootLogger } from \"../boot/types.js\";\n\nexport interface OomKillEvent {\n pgid: number;\n victimRssBytes: number;\n usedBytes: number;\n limitBytes: number;\n reason: \"headroom\" | \"projection\";\n}\n\nexport interface OomWatchdogOptions {\n log: BootLogger;\n /** Fired synchronously just before the SIGKILL, so a report frame can be\n * written to the victim's connection ahead of its exit frame. */\n onKill?: (event: OomKillEvent) => void;\n pollMs?: number;\n headroomBytes?: number;\n /** Post-kill quiet period — lets memory.current fall before re-evaluating. */\n cooldownMs?: number;\n /** Groups below this RSS are never killed: reaping them can't meaningfully\n * relieve pressure, and it protects sshd/idle shells from a kill spiral\n * when the pressure is actually page cache or the launcher itself. */\n minVictimRssBytes?: number;\n /** Injection points for tests. */\n cgroupDir?: string;\n procDir?: string;\n kill?: (pgid: number, signal: NodeJS.Signals) => void;\n}\n\nexport interface OomWatchdogHandle {\n stop(): void;\n}\n\n/**\n * Production config for the boot paths: armed by default in workbench pods,\n * `CONVEYOR_OOM_WATCHDOG=0` is the ops kill-switch (no republish needed to\n * turn it off), `CONVEYOR_OOM_HEADROOM_MB` tunes the floor. Returns undefined\n * when disabled so callers can pass it straight to WorkbenchServerOptions.\n */\nexport function oomWatchdogOptionsFromEnv(\n log: BootLogger,\n env: NodeJS.ProcessEnv = process.env,\n): Omit<OomWatchdogOptions, \"onKill\"> | undefined {\n if (env.CONVEYOR_OOM_WATCHDOG === \"0\") {\n log.info(\"[oom-watchdog] disabled via CONVEYOR_OOM_WATCHDOG=0\");\n return undefined;\n }\n const headroomMb = Number(env.CONVEYOR_OOM_HEADROOM_MB);\n return {\n log,\n ...(Number.isFinite(headroomMb) && headroomMb > 0\n ? { headroomBytes: headroomMb * 1024 * 1024 }\n : {}),\n };\n}\n\nconst DEFAULT_POLL_MS = 100;\nconst DEFAULT_HEADROOM_BYTES = 256 * 1024 * 1024;\nconst DEFAULT_COOLDOWN_MS = 1000;\nconst DEFAULT_MIN_VICTIM_RSS_BYTES = 64 * 1024 * 1024;\n// Reading page size at runtime needs a syscall binding node doesn't expose;\n// every linux target we deploy to (and the canary validated on) is 4KiB.\nconst PAGE_BYTES = 4096;\n\nfunction readTrimmed(path: string): string | null {\n try {\n return readFileSync(path, \"utf8\").trim();\n } catch {\n return null;\n }\n}\n\n/** Fields after the `(comm)` in /proc/<pid>/stat: [2]=pgrp, [21]=rss pages.\n * comm can contain spaces/parens, so split after the LAST ')'. */\nfunction parseProcStat(raw: string): { pgid: number; rssBytes: number } | null {\n const rest = raw.slice(raw.lastIndexOf(\")\") + 2).split(\" \");\n const pgid = Number(rest[2]);\n const rssPages = Number(rest[21]);\n if (!Number.isFinite(pgid) || !Number.isFinite(rssPages)) return null;\n return { pgid, rssBytes: rssPages * PAGE_BYTES };\n}\n\n/** Sum RSS per process group across the container, excluding `selfPgid` —\n * grandchildren that re-setsid (dev servers under a start command shell)\n * show up as their own groups and are eligible victims individually. */\nexport function biggestForeignProcessGroup(\n procDir: string,\n selfPgid: number,\n): { pgid: number; rssBytes: number } | null {\n const groups = new Map<number, number>();\n let entries: string[];\n try {\n entries = readdirSync(procDir);\n } catch {\n return null;\n }\n for (const entry of entries) {\n if (!/^\\d+$/.test(entry)) continue;\n const raw = readTrimmed(join(procDir, entry, \"stat\"));\n // Missing stat file = the process exited mid-scan.\n if (!raw) continue;\n const stat = parseProcStat(raw);\n if (!stat || stat.pgid === selfPgid) continue;\n groups.set(stat.pgid, (groups.get(stat.pgid) ?? 0) + stat.rssBytes);\n }\n let best: { pgid: number; rssBytes: number } | null = null;\n for (const [pgid, rssBytes] of groups) {\n if (!best || rssBytes > best.rssBytes) best = { pgid, rssBytes };\n }\n return best;\n}\n\n/** `memory.current` counts reclaimable page cache the kernel would evict\n * before OOMing; subtract inactive file cache so a git/build IO burst can't\n * read as memory pressure and trigger a false kill. */\nfunction readUsedBytes(cgroupDir: string): number | null {\n const current = Number(readTrimmed(join(cgroupDir, \"memory.current\")));\n if (!Number.isFinite(current)) return null;\n const stat = readTrimmed(join(cgroupDir, \"memory.stat\"));\n const inactiveFile = Number(/^inactive_file (\\d+)$/m.exec(stat ?? \"\")?.[1] ?? 0);\n return Math.max(0, current - inactiveFile);\n}\n\nfunction readSelfPgid(procDir: string): number {\n const raw = readTrimmed(join(procDir, \"self\", \"stat\"));\n const stat = raw ? parseProcStat(raw) : null;\n return stat?.pgid ?? process.pid;\n}\n\n/**\n * Arm the watchdog. Returns null (disabled) when the cgroup has no finite\n * memory limit — local dev, tests, and non-container embedders have nothing\n * to defend against and no meaningful `memory.max` to poll.\n */\nexport function startOomWatchdog(opts: OomWatchdogOptions): OomWatchdogHandle | null {\n const cgroupDir = opts.cgroupDir ?? \"/sys/fs/cgroup\";\n const procDir = opts.procDir ?? \"/proc\";\n const pollMs = opts.pollMs ?? DEFAULT_POLL_MS;\n const headroomBytes = opts.headroomBytes ?? DEFAULT_HEADROOM_BYTES;\n const cooldownMs = opts.cooldownMs ?? DEFAULT_COOLDOWN_MS;\n const minVictimRssBytes = opts.minVictimRssBytes ?? DEFAULT_MIN_VICTIM_RSS_BYTES;\n const kill = opts.kill ?? ((pgid, signal) => process.kill(-pgid, signal));\n\n const limitRaw = readTrimmed(join(cgroupDir, \"memory.max\"));\n const limitBytes = Number(limitRaw);\n if (!limitRaw || limitRaw === \"max\" || !Number.isFinite(limitBytes) || limitBytes <= 0) {\n opts.log.info(`[oom-watchdog] disabled — no finite memory limit (memory.max=${limitRaw})`);\n return null;\n }\n const selfPgid = readSelfPgid(procDir);\n\n // First poll is baseline-only for the projection: seeding prevUsed with 0\n // would read all existing memory as one poll's growth and false-kill any\n // container that arms while already warm.\n let prevUsed: number | null = null;\n let quietUntil = 0;\n const tick = (): void => {\n const now = Date.now();\n if (now < quietUntil) return;\n const used = readUsedBytes(cgroupDir);\n if (used === null) return;\n const delta = prevUsed === null ? 0 : Math.max(0, used - prevUsed);\n prevUsed = used;\n const headroomHit = limitBytes - used <= headroomBytes;\n if (!headroomHit && used + delta * 2 < limitBytes) return;\n\n const victim = biggestForeignProcessGroup(procDir, selfPgid);\n if (!victim || victim.rssBytes < minVictimRssBytes) {\n // Pressure without a killable workload (launcher-owned memory, page\n // cache churn). Nothing safe to do — back off so this doesn't spam.\n quietUntil = now + cooldownMs;\n prevUsed = null;\n opts.log.warn(\n `[oom-watchdog] memory pressure (used=${used} limit=${limitBytes}) but no eligible victim group`,\n );\n return;\n }\n const event: OomKillEvent = {\n pgid: victim.pgid,\n victimRssBytes: victim.rssBytes,\n usedBytes: used,\n limitBytes,\n reason: headroomHit ? \"headroom\" : \"projection\",\n };\n opts.log.warn(\n `[oom-watchdog] killing pgid=${event.pgid} rss=${Math.round(event.victimRssBytes / 1048576)}MiB ` +\n `used=${Math.round(used / 1048576)}/${Math.round(limitBytes / 1048576)}MiB reason=${event.reason}`,\n );\n try {\n opts.onKill?.(event);\n } catch {\n /* reporting must never block the kill */\n }\n try {\n kill(victim.pgid, \"SIGKILL\");\n } catch (err) {\n opts.log.warn(`[oom-watchdog] kill pgid=${victim.pgid} failed: ${String(err)}`);\n }\n quietUntil = now + cooldownMs;\n prevUsed = null;\n };\n\n const interval = setInterval(tick, pollMs);\n interval.unref();\n opts.log.info(\n `[oom-watchdog] armed limit=${Math.round(limitBytes / 1048576)}MiB ` +\n `headroom=${Math.round(headroomBytes / 1048576)}MiB poll=${pollMs}ms`,\n );\n return { stop: () => clearInterval(interval) };\n}\n"],"mappings":"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","names":[]}