@rallycry/conveyor-agent 10.13.65 → 10.13.67
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.
- package/dist/{boot-7OPX55AO.js → boot-UUPTBQ6R.js} +7 -8
- package/dist/{chunk-JIGG755T.js → chunk-6Q6LQBWO.js} +0 -1
- package/dist/{chunk-ULS4QPRE.js → chunk-6W6UZ4SJ.js} +0 -1
- package/dist/{chunk-AXA55U4Z.js → chunk-E2SHIH6Y.js} +3 -4
- package/dist/{chunk-QOJTJCYZ.js → chunk-EXQ6AHOY.js} +2 -3
- package/dist/{chunk-5OQQSDVT.js → chunk-IA45XHOA.js} +0 -1
- package/dist/{chunk-QZN6HVUY.js → chunk-KG4ORL3Y.js} +1 -2
- package/dist/{chunk-4VUQ2NPF.js → chunk-KMB3BU4S.js} +0 -1
- package/dist/{chunk-NAK6FY5U.js → chunk-LE6ZUDZT.js} +37 -6
- package/dist/{chunk-6GS5ADIY.js → chunk-R3FDJQL6.js} +0 -1
- package/dist/{chunk-BAQ2OAKB.js → chunk-XPZPR6NS.js} +591 -216
- package/dist/cli.js +35 -43
- package/dist/{client-LRVVHTNG.js → client-IG6C5F2G.js} +3 -4
- package/dist/heartbeat-worker.js +1 -2
- package/dist/index.d.ts +95 -13
- package/dist/index.js +7 -8
- package/dist/{mode-6RL3SVMV.js → mode-ZJSOSLGU.js} +1 -2
- package/dist/{oom-watchdog-U7JERHA2.js → oom-watchdog-PAC5OJJG.js} +1 -2
- package/dist/{protocol-QLVS5W6O.js → protocol-QBCYO4GI.js} +1 -2
- package/dist/server-US2DDQSW.js +9 -0
- package/package.json +2 -2
- package/dist/boot-7OPX55AO.js.map +0 -1
- package/dist/chunk-4VUQ2NPF.js.map +0 -1
- package/dist/chunk-5OQQSDVT.js.map +0 -1
- package/dist/chunk-6GS5ADIY.js.map +0 -1
- package/dist/chunk-AXA55U4Z.js.map +0 -1
- package/dist/chunk-BAQ2OAKB.js.map +0 -1
- package/dist/chunk-JIGG755T.js.map +0 -1
- package/dist/chunk-NAK6FY5U.js.map +0 -1
- package/dist/chunk-QOJTJCYZ.js.map +0 -1
- package/dist/chunk-QZN6HVUY.js.map +0 -1
- package/dist/chunk-ULS4QPRE.js.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/client-LRVVHTNG.js.map +0 -1
- package/dist/heartbeat-worker.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/mode-6RL3SVMV.js.map +0 -1
- package/dist/oom-watchdog-U7JERHA2.js.map +0 -1
- package/dist/protocol-QLVS5W6O.js.map +0 -1
- package/dist/server-ZACB5T5S.js +0 -10
- package/dist/server-ZACB5T5S.js.map +0 -1
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/workbench/protocol.ts"],"sourcesContent":["/**\n * Wire protocol for the workbench launcher — the loopback control channel\n * between the protected agent container and the restartable workbench\n * container (see docs/superpowers/specs/2026-07-16-workbench-container-split-design.md).\n *\n * Newline-delimited JSON frames over a plain TCP socket. Every connection\n * carries exactly ONE operation: the first client frame is a WorkbenchRequest\n * (with the shared token); everything after is a WorkbenchFrame in either\n * direction. Binary payloads (pty bytes, file contents) ride base64 `d`\n * fields — intra-pod loopback makes the 33% inflation cheap,\n * and NDJSON keeps the framing trivially robust across partial reads.\n */\n\nimport crypto from \"node:crypto\";\nimport type { Socket } from \"node:net\";\n\n/** First frame of every connection (client → server). */\nexport type WorkbenchRequest =\n | { op: \"ping\"; token: string }\n | {\n op: \"exec\";\n token: string;\n /** Shell form — spawned as `sh -c command` (setup/start commands). */\n command?: string;\n /** Argv form — spawned directly, no shell (git and friends). */\n argv?: string[];\n cwd: string;\n env?: Record<string, string>;\n }\n | {\n op: \"pty\";\n token: string;\n file: string;\n args: string[];\n cwd: string;\n env: Record<string, string>;\n cols: number;\n rows: number;\n }\n | { op: \"readFile\"; token: string; path: string }\n | { op: \"stat\"; token: string; path: string }\n | { op: \"readdir\"; token: string; path: string }\n | { op: \"gitStatus\"; token: string };\n\n/** State of the workbench-owned background git preparation. Mirrors\n * GitPrepState in boot/git-prep.ts, flattened onto the wire frame. */\nexport type GitPrepStateKind = \"pending\" | \"ready\" | \"failed\";\n\n/** Response to the `gitStatus` op — the agent container polls it to gate\n * spawning Claude on the workbench finishing its git checkout. */\nexport interface GitStatusFrame {\n t: \"gitStatus\";\n state: GitPrepStateKind;\n reason?: string;\n}\n\n/** Mid-stream frames (either direction after the opening request). */\nexport type WorkbenchFrame =\n | { t: \"out\"; s: \"stdout\" | \"stderr\"; d: string }\n | { t: \"exit\"; code: number | null; signal: string | null }\n | { t: \"data\"; d: string }\n | { t: \"end\" }\n | { t: \"input\"; d: string }\n | { t: \"resize\"; cols: number; rows: number }\n | { t: \"kill\"; sig?: string }\n | { t: \"signal\"; mode: \"term-group\" }\n | {\n t: \"stat\";\n exists: boolean;\n isFile: boolean;\n isDirectory: boolean;\n size: number;\n mtimeMs: number;\n }\n | { t: \"entries\"; names: string[] }\n | { t: \"pong\"; version: string }\n | GitStatusFrame\n | { t: \"error\"; message: string; code?: string };\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\n/** Parse one NDJSON line into a frame-shaped record, or null when malformed. */\nexport function parseLine(line: string): Record<string, unknown> | null {\n if (!line) return null;\n try {\n const parsed: unknown = JSON.parse(line);\n return isRecord(parsed) ? parsed : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Incremental NDJSON reader. Feed raw socket chunks; complete lines are\n * parsed and handed to the callback (malformed lines are dropped — the same\n * contract as the hook socket's envelope parsing).\n */\nexport class FrameReader {\n private buffer = \"\";\n\n constructor(private readonly onFrame: (frame: Record<string, unknown>) => void) {}\n\n push(chunk: Buffer | string): void {\n this.buffer += chunk.toString();\n let index = this.buffer.indexOf(\"\\n\");\n while (index >= 0) {\n const line = this.buffer.slice(0, index);\n this.buffer = this.buffer.slice(index + 1);\n const frame = parseLine(line);\n if (frame) this.onFrame(frame);\n index = this.buffer.indexOf(\"\\n\");\n }\n }\n}\n\n/** Serialize + write one frame. Write errors are the socket's problem —\n * callers handle teardown via the socket's own error/close events. */\nexport function writeFrame(\n socket: Pick<Socket, \"write\">,\n frame: WorkbenchRequest | WorkbenchFrame,\n): void {\n try {\n socket.write(`${JSON.stringify(frame)}\\n`);\n } catch {\n /* socket already destroyed — close handling owns cleanup */\n }\n}\n\nexport const DEFAULT_WORKBENCH_PORT = 7411;\n\n/** Matches the timingSafeEqualStr helper used for every other shared-secret\n * comparison in the codebase (preview-resolve.ts, workspace-attach-token.ts,\n * card-image-link.ts) — constant-time so the loopback auth check can't leak\n * the token via response-time. */\nexport function timingSafeTokenEqual(a: string, b: string): boolean {\n const ab = Buffer.from(a);\n const bb = Buffer.from(b);\n if (ab.length !== bb.length) return false;\n return crypto.timingSafeEqual(ab, bb);\n}\n"],"mappings":";AAaA,OAAO,YAAY;AAkEnB,SAAS,SAAS,OAAkD;AAClE,SAAO,OAAO,UAAU,YAAY,UAAU;AAChD;AAGO,SAAS,UAAU,MAA8C;AACtE,MAAI,CAAC,KAAM,QAAO;AAClB,MAAI;AACF,UAAM,SAAkB,KAAK,MAAM,IAAI;AACvC,WAAO,SAAS,MAAM,IAAI,SAAS;AAAA,EACrC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAOO,IAAM,cAAN,MAAkB;AAAA,EAGvB,YAA6B,SAAmD;AAAnD;AAAA,EAAoD;AAAA,EAApD;AAAA,EAFrB,SAAS;AAAA,EAIjB,KAAK,OAA8B;AACjC,SAAK,UAAU,MAAM,SAAS;AAC9B,QAAI,QAAQ,KAAK,OAAO,QAAQ,IAAI;AACpC,WAAO,SAAS,GAAG;AACjB,YAAM,OAAO,KAAK,OAAO,MAAM,GAAG,KAAK;AACvC,WAAK,SAAS,KAAK,OAAO,MAAM,QAAQ,CAAC;AACzC,YAAM,QAAQ,UAAU,IAAI;AAC5B,UAAI,MAAO,MAAK,QAAQ,KAAK;AAC7B,cAAQ,KAAK,OAAO,QAAQ,IAAI;AAAA,IAClC;AAAA,EACF;AACF;AAIO,SAAS,WACd,QACA,OACM;AACN,MAAI;AACF,WAAO,MAAM,GAAG,KAAK,UAAU,KAAK,CAAC;AAAA,CAAI;AAAA,EAC3C,QAAQ;AAAA,EAER;AACF;AAEO,IAAM,yBAAyB;AAM/B,SAAS,qBAAqB,GAAW,GAAoB;AAClE,QAAM,KAAK,OAAO,KAAK,CAAC;AACxB,QAAM,KAAK,OAAO,KAAK,CAAC;AACxB,MAAI,GAAG,WAAW,GAAG,OAAQ,QAAO;AACpC,SAAO,OAAO,gBAAgB,IAAI,EAAE;AACtC;","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/harness/pty/pty-support.ts","../src/harness/pty/spawn-args.ts","../src/boot/git-credential.ts","../src/utils/sleep.ts","../src/setup/commands.ts"],"sourcesContent":["/**\n * Pure support helpers for PtySession — node-pty process loading, prompt-byte\n * encoding, the per-turn options projection, and the AskUserQuestion parser.\n * Extracted from `session.ts` so the orchestrator stays focused on lifecycle;\n * everything here is side-effect-free (or a thin wrapper over one) and unit\n * tested in isolation.\n */\n\nimport { stat } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport type { HarnessQueryOptions, HarnessUserQuestion } from \"../types.js\";\n// spawn-args does not import this module, so this direction introduces no cycle.\nimport { cleanTerminalOutput } from \"./spawn-args.js\";\nimport { ghHostsManagedByConveyor, githubTokenFilePath } from \"../../boot/git-credential.js\";\n\n// Cap on the rolling tail of raw terminal output retained for diagnostics when\n// `claude` exits without a result. Bounded so a long-running session can't grow\n// this unboundedly; only the most recent bytes (where a failure surfaces) matter.\nexport const MAX_DIAGNOSTIC_OUTPUT = 4000;\n\n// Cap on transcript events buffered while no turn is draining (the parked\n// window between a completed turn and the next beginTurn / a passive turn).\n// Bounded, drop-oldest — a stale buffered `result` must never terminate a\n// fresh turn, and beginTurn clears the buffer anyway.\nexport const MAX_BETWEEN_TURN_BUFFER = 500;\n\n/** Settle window between the paste write and the submitting Enter write. */\nexport const SUBMIT_SETTLE_MS = 300;\n// Re-press cadence/bounds for the submit nudge (see armSubmitNudge). Fast\n// presses cover the Enter-swallowed-at-startup race; the slow phase covers\n// startup dialogs that render tens of seconds in on a cold pod (folder\n// trust, onboarding) — observed live 2026-07-02 parking code reviews ~13min+\n// when the trust dialog mounted after the fast window had expired.\nexport const SUBMIT_NUDGE_INTERVAL_MS = 2000;\nexport const SUBMIT_NUDGE_MAX_PRESSES = 5;\nexport const SUBMIT_NUDGE_SLOW_INTERVAL_MS = 5000;\nexport const SUBMIT_NUDGE_WINDOW_MS = 90_000;\n\n// How many times a failed prompt submission is recovered by tearing the CLI\n// down and respawning it with the same prompt. The nudge only presses Enter,\n// which cannot clear a startup dialog needing an arrow-key selection (the\n// 2026-07-22 fable-model-picker outage parked TUIs exactly that way) — a fresh\n// process is the only in-pod escape from that state. Bounded because a spawn\n// that reliably parks will park again, and an unbounded respawn loop would burn\n// the pod instead of surfacing the failure.\nexport const SUBMIT_REDELIVERY_MAX_ATTEMPTS = 2;\n\n// Plan-dialog auto-accept press cadence/bounds (see armPlanDialogAutoAccept).\n// The dialog usually renders within a second of the ExitPlanMode hook verdict,\n// but the window is deliberately wide: the only stop signal is the ExitPlanMode\n// PostToolUse envelope, and a missed dialog parks an auto card indefinitely\n// (observed live 2026-07-07: an approval dialog sat 42 minutes until a human\n// pressed Enter).\nexport const PLAN_DIALOG_FIRST_PRESS_MS = 700;\nexport const PLAN_DIALOG_INTERVAL_MS = 1500;\nexport const PLAN_DIALOG_SLOW_INTERVAL_MS = 5000;\nexport const PLAN_DIALOG_FAST_WINDOW_MS = 10_000;\nexport const PLAN_DIALOG_WINDOW_MS = 90_000;\n\n// Input-readiness DETECTION before the FIRST prompt write to a raw-relay TUI.\n//\n// A structured-events TUI recovers a prompt lost to startup via the submit\n// nudge (re-press Enter until a transcript record proves the turn began). A raw\n// adapter has no such evidence, so the nudge is never armed for it (see\n// armSubmitNudge's caller) — the single paste + Enter it gets must therefore\n// land on an input box that is actually accepting keystrokes, or the turn is\n// lost silently with the prompt nowhere and the card parked on an empty box.\n//\n// We DETECT that state rather than infer it from elapsed time. The method:\n// type a short sentinel, and watch for the TUI to render THAT SENTINEL back.\n// Only a live input box echoes typed characters into its own repaint, so a\n// rendered sentinel is positive proof; then it is erased and the real prompt is\n// pasted exactly once.\n//\n// Signals that were measured and rejected (opencode 1.18.15 / Claude Code\n// 2.1.226, macOS, driving node-pty directly):\n// - DECSET 2004 (bracketed-paste enable). Both TUIs set it in their FIRST\n// paint — opencode at 554ms, Claude at 267ms — ~2s before opencode accepts\n// input. It reports terminal setup, not input readiness.\n// - \"quiet since the last output frame\". opencode's startup paint contains an\n// internal ~1175ms gap (1172/1174/1177ms over three runs), so any quiet\n// window short enough to be responsive fires INSIDE the paint.\n// - \"any output right after our keystroke\". While the app is still painting,\n// output arrives coincidentally rather than causally: this reported ready at\n// 981ms and the prompt was then dropped. The ack must be the sentinel\n// itself, not merely activity.\n// - echoing the PROMPT back. opencode renders a multi-line paste as\n// \"[Pasted ~4 lines]\" and never shows the text, so the prompt cannot serve\n// as its own acknowledgement.\n//\n// Sentinel content is deliberately three inert lowercase letters: no digits\n// (Claude Code's folder-trust dialog is a numbered menu, where a stray \"1\"\n// would select \"Yes, I trust this folder\"), and no Enter/Esc/arrow keys.\nexport const RAW_TUI_PROBE_SENTINEL = \"zqx\";\n/** How long a probe waits for the TUI to render the sentinel back. */\nexport const RAW_TUI_PROBE_ACK_MS = 500;\n/** Gap between unacknowledged probes. */\nexport const RAW_TUI_PROBE_RETRY_MS = 250;\n/** Poll cadence while watching for the sentinel / for first output. */\nexport const RAW_TUI_PROBE_POLL_MS = 20;\n/** Bound on waiting for the process to paint anything at all. */\nexport const RAW_TUI_FIRST_OUTPUT_MAX_MS = 10_000;\n// Overall cap. On expiry we paste anyway rather than hang the turn: the adapter's\n// own exit diagnostics are a better failure signal than a silent stall.\nexport const RAW_TUI_INPUT_LIVE_MAX_MS = 20_000;\n\nfunction envMs(name: string, fallback: number): number {\n const raw = Number(process.env[name]);\n return Number.isFinite(raw) && raw > 0 ? raw : fallback;\n}\n\nexport function resolveSubmitSettleMs(): number {\n return envMs(\"CONVEYOR_PTY_SUBMIT_SETTLE_MS\", SUBMIT_SETTLE_MS);\n}\n\n/**\n * Effective raw-TUI input-probe timing. Overridable for ops and so tests can\n * drive the detector in milliseconds instead of seconds of real wall-clock.\n * `sentinel` is overridable too, for a TUI that ever treats \"zqx\" specially.\n */\nexport function resolveRawTuiProbeTiming(): {\n sentinel: string;\n ackMs: number;\n retryMs: number;\n pollMs: number;\n firstOutputMaxMs: number;\n maxMs: number;\n} {\n const sentinel = process.env.CONVEYOR_PTY_RAW_PROBE_SENTINEL;\n return {\n // A sentinel must be non-empty (it is what we search for) and must stay\n // free of digits and control chars — see the note on menu dialogs above.\n sentinel: sentinel && /^[a-z]{1,8}$/.test(sentinel) ? sentinel : RAW_TUI_PROBE_SENTINEL,\n ackMs: envMs(\"CONVEYOR_PTY_RAW_PROBE_ACK_MS\", RAW_TUI_PROBE_ACK_MS),\n retryMs: envMs(\"CONVEYOR_PTY_RAW_PROBE_RETRY_MS\", RAW_TUI_PROBE_RETRY_MS),\n pollMs: envMs(\"CONVEYOR_PTY_RAW_PROBE_POLL_MS\", RAW_TUI_PROBE_POLL_MS),\n firstOutputMaxMs: envMs(\"CONVEYOR_PTY_RAW_FIRST_OUTPUT_MAX_MS\", RAW_TUI_FIRST_OUTPUT_MAX_MS),\n maxMs: envMs(\"CONVEYOR_PTY_RAW_INPUT_LIVE_MAX_MS\", RAW_TUI_INPUT_LIVE_MAX_MS),\n };\n}\n\n/**\n * Did a TUI render our probe sentinel back? Positive proof that a live input box\n * consumed the keystrokes — the one signal that discriminated correctly in\n * testing (see the rejected-signals note above).\n *\n * Pure so the matching rule is testable without a pty. Compares against ANSI-\n * stripped output because the sentinel is repainted inside styling runs.\n *\n * PRECONDITION: the child must already own the terminal. Until a TUI switches\n * the tty out of canonical mode the line discipline echoes our own keystrokes\n * straight back, which is indistinguishable from a repaint and false-positives\n * immediately. `sawTerminalSetup` is the gate for that — always wait for it\n * first.\n */\nexport function sentinelEchoed(rawOutput: string, sentinel: string): boolean {\n return cleanTerminalOutput(rawOutput, Number.MAX_SAFE_INTEGER).includes(sentinel);\n}\n\n/** DEC private mode set/reset — `ESC [ ? <params> h|l`. */\nconst DEC_PRIVATE_MODE = new RegExp(`${String.fromCharCode(27)}\\\\[\\\\?[0-9;]+[hl]`);\n\n/**\n * Has the child taken control of the terminal?\n *\n * A DEC private-mode write (alternate screen, bracketed paste, mouse tracking,\n * cursor visibility…) is the child reconfiguring the tty for full-screen use,\n * which is also when it drops canonical mode and echo. Both TUIs do it in their\n * first paint — opencode at 554ms, Claude Code at 267ms — so this is a cheap,\n * observed precondition rather than another timer.\n *\n * It is deliberately NOT used as a readiness signal on its own: opencode sets\n * bracketed-paste ~2s before it will accept input. It only tells us our own\n * keystrokes will no longer be echoed by the kernel, making a sentinel echo\n * attributable to the app.\n */\nexport function sawTerminalSetup(rawOutput: string): boolean {\n return DEC_PRIVATE_MODE.test(rawOutput);\n}\n\n/**\n * Does this adapter's first prompt write need the readiness gate?\n *\n * A per-adapter capability, NOT derived from structuredEvents: opencode keeps\n * the gate even with the events plugin wired, because its TUI silently\n * discards early stdin while painting — the pasted text itself is lost, which\n * no submit nudge can recover. Claude skips it (the nudge suffices, and its\n * numbered startup menus make stray probe keystrokes actively unsafe).\n * Exported so the contract is pinned by a test rather than resting on one\n * `if` inside the session.\n */\nexport function needsRawReadyGate(caps: { rawPromptGate: boolean }): boolean {\n return caps.rawPromptGate;\n}\n\n/**\n * Effective submit-nudge timing. The exported constants above are the production\n * defaults; `CONVEYOR_PTY_NUDGE_INTERVAL_MS` / `_SLOW_INTERVAL_MS` / `_WINDOW_MS`\n * override them. Resolved at nudge-arm time (not import) so an integration test\n * can drive the two-phase fast→slow behavior in milliseconds instead of ~15s of\n * real wall-clock. `maxPresses` is deliberately NOT overridable — the fast→slow\n * phase-boundary count is the behavior under test.\n */\nexport function resolveSubmitNudgeTiming(): {\n intervalMs: number;\n slowIntervalMs: number;\n maxPresses: number;\n windowMs: number;\n} {\n return {\n intervalMs: envMs(\"CONVEYOR_PTY_NUDGE_INTERVAL_MS\", SUBMIT_NUDGE_INTERVAL_MS),\n slowIntervalMs: envMs(\"CONVEYOR_PTY_NUDGE_SLOW_INTERVAL_MS\", SUBMIT_NUDGE_SLOW_INTERVAL_MS),\n maxPresses: SUBMIT_NUDGE_MAX_PRESSES,\n windowMs: envMs(\"CONVEYOR_PTY_NUDGE_WINDOW_MS\", SUBMIT_NUDGE_WINDOW_MS),\n };\n}\n\n/**\n * How many respawn-and-redeliver attempts a failed prompt submission gets.\n * `CONVEYOR_PTY_SUBMIT_REDELIVERY_MAX` overrides it; `0` disables recovery\n * entirely (the failure is still detected and reported). Resolved at use time\n * so tests and ops can change it without a restart-order dependency.\n */\nexport function resolveSubmitRedeliveryMaxAttempts(): number {\n const value = process.env.CONVEYOR_PTY_SUBMIT_REDELIVERY_MAX;\n // An empty string is \"unset\", not \"0\" — `Number(\"\")` is 0, which would\n // silently disable recovery for anyone exporting the var blank.\n if (!value) return SUBMIT_REDELIVERY_MAX_ATTEMPTS;\n const raw = Number(value);\n return Number.isFinite(raw) && raw >= 0 ? raw : SUBMIT_REDELIVERY_MAX_ATTEMPTS;\n}\n\nexport function resolvePlanDialogTiming(): {\n firstPressMs: number;\n intervalMs: number;\n slowIntervalMs: number;\n fastWindowMs: number;\n windowMs: number;\n} {\n return {\n firstPressMs: envMs(\"CONVEYOR_PTY_PLAN_DIALOG_FIRST_PRESS_MS\", PLAN_DIALOG_FIRST_PRESS_MS),\n intervalMs: envMs(\"CONVEYOR_PTY_PLAN_DIALOG_INTERVAL_MS\", PLAN_DIALOG_INTERVAL_MS),\n slowIntervalMs: envMs(\n \"CONVEYOR_PTY_PLAN_DIALOG_SLOW_INTERVAL_MS\",\n PLAN_DIALOG_SLOW_INTERVAL_MS,\n ),\n fastWindowMs: envMs(\"CONVEYOR_PTY_PLAN_DIALOG_FAST_WINDOW_MS\", PLAN_DIALOG_FAST_WINDOW_MS),\n windowMs: envMs(\"CONVEYOR_PTY_PLAN_DIALOG_WINDOW_MS\", PLAN_DIALOG_WINDOW_MS),\n };\n}\n\n/**\n * The per-turn subset of HarnessQueryOptions — everything that legitimately\n * changes from one turn to the next while the same `claude` process serves them\n * all. The process-level fields (model, cwd, sessionId, appendSystemPrompt, …)\n * are fixed at spawn and captured separately in the spawn options.\n */\nexport interface TurnOptions {\n canUseTool?: HarnessQueryOptions[\"canUseTool\"];\n promptDelivery?: \"submit\" | \"prefill\";\n planDialogAutoAccept?: boolean;\n abortController?: AbortController;\n}\n\nexport function turnOptionsFrom(options: HarnessQueryOptions): TurnOptions {\n return {\n canUseTool: options.canUseTool,\n promptDelivery: options.promptDelivery,\n planDialogAutoAccept: options.planDialogAutoAccept,\n abortController: options.abortController,\n };\n}\n\nexport interface PtyProcess {\n /** node-pty exposes the child's pid (its own session/process-group leader\n * via forkpty). Optional because test fakes don't model a real process. */\n pid?: number;\n onData(listener: (data: string) => void): void;\n onExit(listener: (event: { exitCode: number }) => void): void;\n write(data: string): void;\n resize(cols: number, rows: number): void;\n kill(signal?: string): void;\n}\n\nexport const KILL_ESCALATION_MS = 5_000;\n\n/**\n * Kill a pty, escalating to SIGKILL if it hasn't exited within `escalationMs`.\n * node-pty's default kill (SIGHUP) can be ignored by a wedged `claude` (e.g.\n * mid-OOM), and a fire-and-forget kill would then leak the ~1GB process on the\n * pod. `hasExited()` (backed by the caller's onExit handler) cancels the\n * escalation on a clean exit; the timer is unref'd so it never holds the\n * process open on its own.\n */\nexport function killPtyWithEscalation(\n pty: Pick<PtyProcess, \"kill\">,\n hasExited: () => boolean,\n escalationMs: number = KILL_ESCALATION_MS,\n): void {\n try {\n pty.kill();\n } catch {\n /* already exited */\n }\n const timer = setTimeout(() => {\n if (hasExited()) return;\n try {\n pty.kill(\"SIGKILL\");\n } catch {\n /* already exited */\n }\n }, escalationMs);\n timer.unref?.();\n}\n\nexport interface PtySpawnOptions {\n name: string;\n cols: number;\n rows: number;\n cwd: string;\n env: Record<string, string>;\n}\n\nexport type PtySpawn = (file: string, args: string[], options: PtySpawnOptions) => PtyProcess;\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction extractSpawn(mod: unknown): PtySpawn | null {\n if (!isRecord(mod)) return null;\n if (typeof mod.spawn === \"function\") return mod.spawn as PtySpawn;\n const def = mod.default;\n if (isRecord(def) && typeof def.spawn === \"function\") return def.spawn as PtySpawn;\n return null;\n}\n\nexport async function loadPtySpawn(): Promise<PtySpawn> {\n const mod: unknown = await import(\"node-pty\");\n const spawn = extractSpawn(mod);\n if (!spawn) throw new Error(\"node-pty: spawn export not found\");\n return spawn;\n}\n\n/**\n * The spawn implementation for this pod topology: node-pty locally, or the\n * workbench launcher's remote PTY in split-mode pods (the CLI must run in the\n * workbench container, next to the repo and inside the restartable cgroup).\n * Same PtySpawn contract either way — PtySession is topology-blind.\n */\nexport async function resolvePtySpawn(): Promise<PtySpawn> {\n const { workbenchEnabled } = await import(\"../../workbench/mode.js\");\n if (!workbenchEnabled()) return loadPtySpawn();\n const { getWorkbenchClient } = await import(\"../../workbench/client.js\");\n return (file, args, options) => getWorkbenchClient().spawnPty(file, args, options);\n}\n\n/**\n * Base directory for a PTY session's control files (settings.json,\n * mcp-config, hook socket). In split-mode pods this MUST be the shared\n * emptyDir — the spawned CLI reads these paths from the workbench container,\n * and the hook helper connects back over the unix socket, which crosses\n * containers only on a shared volume.\n */\nexport function sessionTempBase(): string {\n return process.env.CONVEYOR_SHARED_DIR ?? tmpdir();\n}\n\nexport function inheritedEnv(socketPath?: string): Record<string, string> {\n const env: Record<string, string> = {};\n for (const [key, value] of Object.entries(process.env)) {\n if (typeof value === \"string\") env[key] = value;\n }\n // Do NOT force CLAUDE_CONFIG_DIR. Forcing it to ~/.claude makes the CLI read\n // <dir>/.claude.json — a fresh, un-onboarded config — instead of the user's\n // real ~/.claude.json, which re-triggers the first-run theme picker + login\n // even though valid credentials already exist in ~/.claude/. The loop above\n // already propagates an explicitly-set CLAUDE_CONFIG_DIR (pods/codespaces),\n // and the config dir is unchanged either way, so transcript tailing (which\n // uses claudeConfigHome()) still resolves to the same projects/ directory.\n // Belt-and-braces against the ANTHROPIC_API_KEY leak: when a subscription\n // OAuth token is in play, the TUI authenticates via the synthesized\n // credentials.json — an inherited ANTHROPIC_API_KEY would be treated as an\n // EXTERNAL API key and, once interactively approved, poison auth (the token\n // is sent via x-api-key → 401 forever). The API bundle already stops serving\n // the OAuth token under that name; stripping it here means no upstream leak\n // can reach the spawned CLI regardless. The two auth modes are mutually\n // exclusive (see session-runner's onApiKeyUpdate), so a real api_key launch\n // has no CLAUDE_CODE_OAUTH_TOKEN and keeps its ANTHROPIC_API_KEY.\n if (env.CLAUDE_CODE_OAUTH_TOKEN) {\n delete env.ANTHROPIC_API_KEY;\n }\n // A GitHub App installation token dies at ~1h, but this CLI is spawned once\n // and parked for hours — its env is a snapshot we can never update. Handing\n // it `GH_TOKEN`/`GITHUB_TOKEN` therefore guarantees a 401 on every `gh` call\n // past the first hour, and `gh` prefers those variables over its config\n // file, so a fresh hosts.yml would be ignored while they are set. Drop them\n // and let `gh` read the file the refresh timer keeps current. Only ever done\n // when Conveyor owns that file — otherwise dropping them leaves no\n // credential at all. `CONVEYOR_KEEP_GH_TOKEN_ENV=1` restores the old\n // behavior for debugging.\n if (ghHostsManagedByConveyor() && env.CONVEYOR_KEEP_GH_TOKEN_ENV !== \"1\") {\n delete env.GH_TOKEN;\n delete env.GITHUB_TOKEN;\n delete env.CONVEYOR_GITHUB_TOKEN;\n // Shells and scripts that need the value read it from here, fresh.\n env.CONVEYOR_GITHUB_TOKEN_FILE = githubTokenFilePath();\n }\n if (socketPath) {\n env.CONVEYOR_HOOK_SOCKET = socketPath;\n }\n // The conveyor MCP tool handlers proxy over the agent's API socket, and a\n // mid-flap call can block up to ~50s (20s reconnect-wait + 30s ack) before\n // failing with a retryable error. The CLI's default MCP timeouts are\n // shorter — hitting them made it abandon the MCP session mid-flap\n // (2026-07-08 wedge). Default both timeouts past the worst-case hang;\n // operator-set values win via the inherit loop above.\n env.MCP_TIMEOUT ??= \"60000\";\n env.MCP_TOOL_TIMEOUT ??= \"180000\";\n return env;\n}\n\n/**\n * The bracketed-paste bytes written to the CLI's stdin to deliver a prompt.\n * The text is wrapped in paste markers so embedded newlines land as literal\n * lines in the input box instead of submitting it early. The submitting Enter\n * is deliberately NOT part of these bytes: deliverPrompt() sends it as a\n * separate write after a settle window, because an Enter folded into the same\n * chunk as the paste can be swallowed by the CLI's paste-burst handling —\n * observed in production as an auto-mode prompt parked unsubmitted in the\n * input box. \"prefill\" delivery never sends an Enter at all.\n */\nexport function buildPromptBytes(text: string): string {\n return `\\x1b[200~${text}\\x1b[201~`;\n}\n\n/**\n * Render structured (multimodal) prompt content as pasteable text. Image\n * blocks are replaced with a tool reference — their base64 payload must NEVER\n * be pasted into the TUI input box. Upstream prompt builders already skip\n * image blocks for the PTY harness; this is the last line of defense.\n */\nexport function renderPromptContentText(content: unknown[]): string {\n return content\n .map((block) => {\n const b = block as { type?: string; text?: string };\n if (b?.type === \"text\" && typeof b.text === \"string\") return b.text;\n if (b?.type === \"image\") {\n return `[Image attachment — use list_task_files / get_attachment to view]`;\n }\n return JSON.stringify(block);\n })\n .join(\"\\n\\n\");\n}\n\nexport { sleep } from \"../../utils/sleep.js\";\n\n/** Current size of the transcript, or 0 if it does not exist yet. */\nexport async function transcriptSize(path: string): Promise<number> {\n try {\n return (await stat(path)).size;\n } catch {\n return 0;\n }\n}\n\n/**\n * Defensive extraction of AskUserQuestion's `questions` input from the hook\n * payload. Malformed entries are dropped rather than thrown — the event is\n * observe-only, so an empty list still arms the waiting_for_input report.\n */\nexport function parseUserQuestions(input: Record<string, unknown>): HarnessUserQuestion[] {\n if (!Array.isArray(input.questions)) return [];\n const questions: HarnessUserQuestion[] = [];\n for (const entry of input.questions) {\n if (!isRecord(entry)) continue;\n if (typeof entry.question !== \"string\") continue;\n const options = Array.isArray(entry.options)\n ? entry.options\n .filter(isRecord)\n .filter((o) => typeof o.label === \"string\")\n .map((o) => ({\n label: o.label as string,\n description: typeof o.description === \"string\" ? o.description : \"\",\n }))\n : [];\n questions.push({\n question: entry.question,\n header: typeof entry.header === \"string\" ? entry.header : \"\",\n options,\n ...(typeof entry.multiSelect === \"boolean\" ? { multiSelect: entry.multiSelect } : {}),\n });\n }\n return questions;\n}\n","/**\n * The `claude` CLI process boundary: resolve the binary, build its argv, and\n * interpret its exit when it dies before producing a result. Kept pure so it\n * can be unit-tested without a real spawn.\n */\n\nexport interface SpawnArgsInput {\n resume?: string;\n sessionId?: string;\n model: string;\n permissionMode: \"plan\" | \"bypassPermissions\";\n settingsPath: string;\n /** Extra system-prompt text appended via `--append-system-prompt`. */\n appendSystemPrompt?: string;\n mcpConfigPath?: string;\n /** When set with mcpConfigPath, the CLI uses ONLY that config and ignores the\n * user's `~/.claude.json` / project `.mcp.json` servers. */\n strictMcpConfig?: boolean;\n}\n\nexport function resolveClaudeBinary(): string {\n return process.env.CONVEYOR_CLAUDE_BIN ?? \"claude\";\n}\n\nexport function buildSpawnArgs(input: SpawnArgsInput): string[] {\n const args: string[] = [];\n if (input.resume) {\n args.push(\"--resume\", input.resume);\n } else if (input.sessionId) {\n args.push(\"--session-id\", input.sessionId);\n }\n args.push(\"--model\", input.model);\n if (input.permissionMode === \"bypassPermissions\") {\n args.push(\"--dangerously-skip-permissions\");\n } else {\n args.push(\"--permission-mode\", \"plan\");\n }\n args.push(\"--settings\", input.settingsPath);\n if (input.appendSystemPrompt) {\n args.push(\"--append-system-prompt\", input.appendSystemPrompt);\n }\n if (input.mcpConfigPath) {\n args.push(\"--mcp-config\", input.mcpConfigPath);\n if (input.strictMcpConfig) {\n args.push(\"--strict-mcp-config\");\n }\n }\n return args;\n}\n\n/**\n * A stable fingerprint of the spawn arguments that a REUSED (kept-alive) CLI\n * process cannot change — model, permission mode, appended system prompt, and\n * cwd are all baked into the live `claude` process at spawn. When a follow-up\n * turn would spawn with different values (most commonly a drifted\n * `appendSystemPrompt` after a plan/status edit), the parked process cannot\n * serve it, so the harness respawns instead of reusing. Keeping this beside\n * `buildSpawnArgs` ensures the two can't silently diverge.\n *\n * `settingsPath`, `mcpConfigPath`, and the hook socket path are deliberately\n * excluded: they are per-process resources that stay stable for the process's\n * whole life, so they never differentiate one turn from the next.\n */\nexport function spawnOptionsFingerprint(input: {\n model: string;\n permissionMode: \"plan\" | \"bypassPermissions\";\n appendSystemPrompt?: string;\n cwd: string;\n}): string {\n return JSON.stringify([\n input.model,\n input.permissionMode,\n input.appendSystemPrompt ?? \"\",\n input.cwd,\n ]);\n}\n\n// ─── Exit diagnostics ──────────────────────────────────────────────────────\n// The PTY harness never turns raw `claude` stdout/stderr into HarnessEvents —\n// it relays them to the S5 terminal instead. So when `claude` dies before\n// emitting a transcript result, the captured agent logs show only the generic\n// \"claude exited (code N) without a result\"; the real reason (a missing binary,\n// an auth/onboarding stop, an unknown CLI flag) scrolled past in the live\n// terminal only. These helpers fold a bounded, ANSI-stripped tail of that\n// scrollback back into the error so the failure is self-describing in the logs.\n\n// ESC (0x1B) built via fromCharCode so no control char appears as a source\n// literal (keeps the no-control-regex lint rule happy). Matches CSI sequences\n// (ESC [ ... final-byte) — the bulk of TUI escape noise; any stray ESC bytes\n// from other sequences are removed by the control-char pass below.\nconst ANSI_CSI = new RegExp(`${String.fromCharCode(27)}\\\\[[0-9;?]*[ -/]*[@-~]`, \"g\");\n\n/**\n * Strip ANSI escapes + terminal control noise and collapse to the last few\n * readable lines, capped at `maxChars`. Returns \"\" when nothing printable\n * remains (e.g. the process produced no output at all).\n */\nexport function cleanTerminalOutput(raw: string, maxChars = 1200): string {\n const noAnsi = raw.replace(ANSI_CSI, \"\");\n // Drop C0 control chars (and DEL) except tab; fold CR into newline.\n let out = \"\";\n for (const ch of noAnsi) {\n const code = ch.charCodeAt(0);\n if (ch === \"\\r\" || ch === \"\\n\") out += \"\\n\";\n else if (ch === \"\\t\") out += ch;\n else if (code < 0x20 || code === 0x7f) continue;\n else out += ch;\n }\n const lines = out\n .split(\"\\n\")\n .map((line) => line.trimEnd())\n .filter((line) => line.trim().length > 0);\n const text = lines.join(\"\\n\").trim();\n return text.length > maxChars ? `…${text.slice(-maxChars)}` : text;\n}\n\n/**\n * True when the terminal tail shows node-pty failed to exec the target binary\n * (missing `claude` CLI on PATH). node-pty prints `execvp(3) failed.: No such\n * file or directory` to the pty and the child exits with code 1.\n */\nexport function isMissingBinaryFailure(tail: string): boolean {\n return /execvp\\(\\d+\\) failed|no such file or directory|command not found/i.test(tail);\n}\n\n/**\n * Build the `errors[]` for a `claude` process that exited before emitting a\n * transcript result. The first entry is kept byte-for-byte stable so existing\n * consumers/log scrapers that key off it keep working; richer context is\n * appended after it.\n */\nexport function buildExitErrors(exitCode: number, rawOutput: string, binary: string): string[] {\n const errors = [`claude exited (code ${exitCode}) without a result`];\n const tail = cleanTerminalOutput(rawOutput);\n if (isMissingBinaryFailure(tail)) {\n errors.push(\n `The \\`${binary}\\` CLI could not be started — it is not installed or not on PATH. ` +\n `Install the Claude Code CLI in this environment (npm i -g @anthropic-ai/claude-code) ` +\n `or set CONVEYOR_CLAUDE_BIN to its absolute path.`,\n );\n }\n if (tail) {\n errors.push(`Last terminal output before exit:\\n${tail}`);\n }\n return errors;\n}\n","import { createHash } from \"node:crypto\";\nimport { chmodSync, existsSync, mkdirSync, renameSync, writeFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport type { GitCredential } from \"@project/shared\";\n\nfunction credentialDir(cwd: string): string {\n return join(dirname(cwd), \".conveyor-git-credentials\");\n}\n\n/** Per-checkout key: the credential directory is shared by every repo that\n * sits beside this one, so both files it holds must be keyed by the checkout\n * they belong to. */\nfunction credentialKey(cwd: string): string {\n return createHash(\"sha256\").update(cwd).digest(\"hex\").slice(0, 16);\n}\n\nexport function gitCredentialFile(cwd: string): string {\n return join(credentialDir(cwd), `${credentialKey(cwd)}.store`);\n}\n\n/** Path of the managed helper script (see `writeGitCredentialHelperScript`). */\nexport function gitCredentialHelperScript(cwd: string): string {\n return join(credentialDir(cwd), `conveyor-credential-helper-${credentialKey(cwd)}.sh`);\n}\n\n/**\n * What `credential.helper` should be set to for this checkout.\n *\n * The managed script when it exists, and the plain `store` helper otherwise —\n * a pod that could not write the script (read-only dir, no `sh`) must keep the\n * behavior it had before rather than losing git auth entirely.\n */\nexport function gitCredentialHelper(cwd: string): string {\n const script = gitCredentialHelperScript(cwd);\n if (existsSync(script)) return script;\n return `store --file=${gitCredentialFile(cwd)}`;\n}\n\n/**\n * Rewrite a clone URL into the credential-free HTTPS form the store needs.\n *\n * Throwing here used to make a whole token refresh a silent no-op: an origin\n * carrying embedded credentials (`https://x-access-token:ghs_…@github.com/…`)\n * or an `ssh://`/`git@host:path` remote failed the check, and the caller's\n * blanket catch discarded the error. Normalizing covers every form we can\n * legitimately authenticate over HTTPS; only a genuinely unusable URL throws.\n */\nexport function normalizeCloneUrl(cloneUrl: string): string {\n const scp = /^(?:ssh:\\/\\/)?(?:[^@/]+@)?([^:/]+):(?!\\/)(.+)$/.exec(cloneUrl.trim());\n const candidate = scp ? `https://${scp[1]}/${scp[2]}` : cloneUrl.trim();\n let url: URL;\n try {\n url = new URL(candidate.replace(/^ssh:\\/\\//, \"https://\"));\n } catch {\n throw new Error(\"Git clone URL is not a valid URL\");\n }\n if (!/^https?:$/.test(url.protocol)) {\n throw new Error(\"Git clone URL must be an HTTP(S) or SSH GitHub URL\");\n }\n url.username = \"\";\n url.password = \"\";\n url.search = \"\";\n url.hash = \"\";\n return url.toString();\n}\n\n/**\n * Persist a pod-lifetime credential outside the repository with owner-only\n * permissions, and install the managed helper script alongside it. Returns the\n * normalized (credential-free) clone URL so the caller can re-point `origin`.\n */\nexport function writeGitCredential(\n cwd: string,\n cloneUrl: string,\n credential: GitCredential,\n): string {\n const cleanUrl = new URL(normalizeCloneUrl(cloneUrl));\n if (!credential.username || !credential.secret) {\n throw new Error(\"Git credential username and secret are required\");\n }\n\n const dir = credentialDir(cwd);\n mkdirSync(dir, { recursive: true, mode: 0o700 });\n chmodSync(dir, 0o700);\n\n const normalized = cleanUrl.toString();\n // `host` carries the port when the remote uses one, which is exactly what git\n // puts in the credential request's `host=` line.\n const remoteHost = cleanUrl.host;\n cleanUrl.username = credential.username;\n cleanUrl.password = credential.secret;\n cleanUrl.pathname = \"/\";\n\n const target = gitCredentialFile(cwd);\n writeSecretFile(target, `${cleanUrl.toString()}\\n`);\n try {\n writeGitCredentialHelperScript(cwd, { username: credential.username, host: remoteHost });\n } catch {\n // best effort — `gitCredentialHelper` falls back to the `store` helper\n }\n return normalized;\n}\n\n/**\n * Install the credential helper git actually runs.\n *\n * `git credential-store` implements `erase`, and git invokes it whenever the\n * host rejects a credential. An expired installation token therefore made git\n * DELETE its own credential, leaving pods with a 0-byte store and no path back\n * — the exact failure this script exists to prevent. It:\n *\n * - answers `get` from `~/.conveyor/github-token`, the same file every token\n * refresh rewrites, so git is always as fresh as `gh` — but ONLY when this\n * checkout's remote is on the GitHub host, because that file holds a GitHub\n * token and Conveyor also drives self-hosted Forgejo remotes;\n * - falls back to the static store file, which every credential write\n * refreshes for GitHub and Forgejo alike;\n * - answers only for the host of its own remote, so a token can never leak to\n * a third-party remote or submodule;\n * - exits without writing for `store` and `erase`, so a rejection can never\n * wipe the credential again.\n */\nexport interface CredentialHelperOptions {\n /** Username to answer with. Defaults to the pod's configured git username. */\n username?: string;\n /**\n * Host the helper answers for, port included when the remote uses one.\n * Defaults to the GitHub host. Passing the remote's own host is what keeps\n * Forgejo projects working: the guard rejected every non-`github.com` remote\n * and left those pods with a helper that answered nothing at all.\n */\n host?: string;\n}\n\nexport function writeGitCredentialHelperScript(\n cwd: string,\n options: CredentialHelperOptions = {},\n): string {\n const script = gitCredentialHelperScript(cwd);\n const user = options.username || process.env.CONVEYOR_GIT_USERNAME || \"x-access-token\";\n const host = options.host || githubHost();\n // A self-hosted forge must never be handed the GitHub token file. Its secret\n // lives only in the store file, which `writeGitCredential` refreshes on every\n // token refresh, so freshness is preserved either way.\n const tokenFile = host === githubHost() ? githubTokenFilePath() : \"\";\n const contents = [\n \"#!/bin/sh\",\n \"# Written by conveyor-agent. Do not edit — every credential write rewrites it.\",\n \"# `get` reads the current token; `store`/`erase` are deliberately no-ops so a\",\n \"# rejected (expired) token can never delete the pod's git credential.\",\n 'if [ \"$1\" != \"get\" ]; then exit 0; fi',\n `TOKEN_FILE=${shellQuote(tokenFile)}`,\n `STORE_FILE=${shellQuote(gitCredentialFile(cwd))}`,\n `EXPECTED_HOST=${shellQuote(host)}`,\n `GIT_USERNAME=${shellQuote(user)}`,\n \"host=\",\n \"while IFS= read -r line; do\",\n ' case \"$line\" in',\n \" host=*) host=${line#host=} ;;\",\n \" esac\",\n ' [ -n \"$line\" ] || break',\n \"done\",\n 'if [ -n \"$host\" ] && [ \"$host\" != \"$EXPECTED_HOST\" ]; then exit 0; fi',\n \"secret=\",\n 'if [ -n \"$TOKEN_FILE\" ] && [ -r \"$TOKEN_FILE\" ]; then',\n ` secret=$(tr -d '\\\\r\\\\n' < \"$TOKEN_FILE\")`,\n \"fi\",\n 'if [ -z \"$secret\" ] && [ -r \"$STORE_FILE\" ]; then',\n ` secret=$(sed -n '1s|.*://[^:]*:\\\\([^@]*\\\\)@.*|\\\\1|p' \"$STORE_FILE\")`,\n \"fi\",\n 'if [ -z \"$secret\" ]; then exit 0; fi',\n 'echo \"username=$GIT_USERNAME\"',\n 'echo \"password=$secret\"',\n \"\",\n ].join(\"\\n\");\n writeSecretFile(script, contents);\n chmodSync(script, 0o700);\n return script;\n}\n\n/** Single-quote a value for safe interpolation into the helper script. */\nfunction shellQuote(value: string): string {\n return `'${value.replace(/'/g, `'\\\\''`)}'`;\n}\n\nfunction githubHost(): string {\n return process.env.GH_HOST || \"github.com\";\n}\n\n// ── Refreshable GitHub credential files ────────────────────────────────────\n//\n// A GitHub App installation token dies at ~1h, but the pod's `claude` CLI is\n// spawned once and parked for hours (keep-alive PTY). A child process cannot\n// see its parent's later `process.env` writes, so a token exported into the\n// child env at spawn time is frozen: every `gh` call after the first hour 401s\n// and no re-export inside the agent's shell can fix it.\n//\n// The fix is to keep the credential in FILES that every refresh path rewrites,\n// the same way `writeGitCredential` above already keeps `git push` working:\n// - `~/.config/gh/hosts.yml` — what the `gh` CLI reads when no token env var\n// shadows it (see `inheritedEnv` in harness/pty/pty-support.ts).\n// - `~/.conveyor/github-token` — a plain file any shell or script can read\n// for a value that is fresh right now.\n\n/** Plain-text file holding the current GitHub token, for shells and scripts. */\nexport function githubTokenFilePath(): string {\n return process.env.CONVEYOR_GITHUB_TOKEN_FILE || join(homedir(), \".conveyor\", \"github-token\");\n}\n\nfunction ghConfigDir(): string {\n return process.env.GH_CONFIG_DIR || join(homedir(), \".config\", \"gh\");\n}\n\n/** The `gh` CLI's credential file. */\nexport function ghHostsFilePath(): string {\n return join(ghConfigDir(), \"hosts.yml\");\n}\n\n/** Marker proving the hosts.yml alongside it is ours, not a user's own login. */\nfunction ghManagedMarkerPath(): string {\n return join(ghConfigDir(), \".conveyor-managed\");\n}\n\n/** gh's general settings file. We only care that it declares a config version. */\nfunction ghConfigFilePath(): string {\n return join(ghConfigDir(), \"config.yml\");\n}\n\n/**\n * A test run must never write real credential files into the machine's home.\n * The suites that exercise these writers point `GH_CONFIG_DIR` /\n * `CONVEYOR_GITHUB_TOKEN_FILE` at a temp dir; without an override, a vitest\n * process writes nothing. This is not theoretical — a `pushToOrigin` unit test\n * refreshing to the literal token \"new-token\" wrote that value into a live\n * pod's `~/.config/gh/hosts.yml` and broke `gh` for the whole session.\n */\nfunction blockedDefaultWrite(overrideEnvKey: string): boolean {\n return Boolean(process.env.VITEST) && !process.env[overrideEnvKey];\n}\n\n/**\n * True when Conveyor owns the `gh` credential file. Callers use this to decide\n * whether dropping a frozen token env var is safe: without our own hosts.yml,\n * removing it would leave `gh` with no credential at all.\n */\nexport function ghHostsManagedByConveyor(): boolean {\n return existsSync(ghManagedMarkerPath()) && existsSync(ghHostsFilePath());\n}\n\n/**\n * True when a `gh` login this pod did not create owns `hosts.yml`.\n *\n * `writeGhHostsConfig` deliberately declines to overwrite that file — a GitHub\n * Codespace and a developer's own machine both authenticate `gh` for us. A\n * refresh that skips it there is working as designed, so callers must not\n * report it as a failed credential copy.\n */\nexport function ghHostsExternallyOwned(): boolean {\n return existsSync(ghHostsFilePath()) && !existsSync(ghManagedMarkerPath());\n}\n\n/** Atomic owner-only write, creating the parent directory owner-only. */\nfunction writeSecretFile(target: string, contents: string): void {\n const dir = dirname(target);\n mkdirSync(dir, { recursive: true, mode: 0o700 });\n chmodSync(dir, 0o700);\n const temporary = `${target}.${process.pid}.tmp`;\n writeFileSync(temporary, contents, { encoding: \"utf8\", mode: 0o600 });\n chmodSync(temporary, 0o600);\n renameSync(temporary, target);\n}\n\nfunction writeGithubTokenFile(token: string): boolean {\n if (blockedDefaultWrite(\"CONVEYOR_GITHUB_TOKEN_FILE\")) return false;\n writeSecretFile(githubTokenFilePath(), `${token}\\n`);\n return true;\n}\n\n/**\n * Declare the config version so gh treats hosts.yml as already migrated.\n * Without this, every gh invocation runs the multi-account migration, which\n * calls the API to resolve the account name — and an expired token then fails\n * the whole command with \"cowardly refusing to continue with multi account\n * migration\" instead of a plain 401. The agent is told to answer a 401 by\n * refreshing the token, so the plain 401 is the failure we want. Only written\n * when absent: config.yml also holds user preferences we must not overwrite.\n */\nfunction ensureGhConfigVersion(): void {\n if (existsSync(ghConfigFilePath())) return;\n writeSecretFile(ghConfigFilePath(), 'version: \"1\"\\n');\n}\n\n/**\n * Write the `gh` CLI credential file. Both the top-level keys and the `users`\n * map are emitted: gh reads the flat `oauth_token` and newer versions also\n * expect the per-user entry, so writing both keeps one file valid for either.\n */\nfunction writeGhHostsConfig(token: string): boolean {\n if (blockedDefaultWrite(\"GH_CONFIG_DIR\")) return false;\n // Never clobber a `gh` login this pod did not create — an environment that\n // authenticates gh for us (a GitHub Codespace, a developer's own machine)\n // keeps its own file, and the env-stripping caller then leaves that\n // environment's token variables in place.\n if (existsSync(ghHostsFilePath()) && !existsSync(ghManagedMarkerPath())) return false;\n const host = process.env.GH_HOST || \"github.com\";\n const user = process.env.CONVEYOR_GIT_USERNAME || \"x-access-token\";\n const contents = [\n \"# Written by conveyor-agent — refreshed on every GitHub token refresh.\",\n \"# Edits are overwritten. Do not add a second host entry here by hand.\",\n `${host}:`,\n ` oauth_token: ${token}`,\n ` user: ${user}`,\n \" git_protocol: https\",\n \" users:\",\n ` ${user}:`,\n ` oauth_token: ${token}`,\n \"\",\n ].join(\"\\n\");\n writeSecretFile(ghHostsFilePath(), contents);\n writeSecretFile(ghManagedMarkerPath(), \"conveyor-agent\\n\");\n ensureGhConfigVersion();\n return true;\n}\n\nexport interface GithubTokenFileSync {\n tokenFile: boolean;\n ghHosts: boolean;\n}\n\n/**\n * Refresh every file-based copy of the GitHub token. Never throws — a failed\n * write must not break the caller's real work (a push, a boot, a refresh\n * tick); the return value says which copies are now current.\n */\nexport function syncGithubTokenFiles(token: string | undefined): GithubTokenFileSync {\n const result: GithubTokenFileSync = { tokenFile: false, ghHosts: false };\n if (!token) return result;\n try {\n result.tokenFile = writeGithubTokenFile(token);\n } catch {\n // best effort\n }\n try {\n result.ghHosts = writeGhHostsConfig(token);\n } catch {\n // best effort\n }\n return result;\n}\n","export function sleep(ms: number): Promise<void> {\n return new Promise((resolve) => {\n setTimeout(resolve, ms);\n });\n}\n","import { spawn, execSync, type ChildProcess } from \"node:child_process\";\n\n/**\n * The structural surface of a start-command child the supervisor manages —\n * satisfied by a local ChildProcess and by the workbench client's\n * RemoteProcessHandle (split-mode pods, where the process lives in the\n * workbench container).\n */\nexport interface ManagedChildProcess extends NodeJS.EventEmitter {\n pid?: number | undefined;\n exitCode: number | null;\n kill(signal?: NodeJS.Signals | number): boolean;\n}\n\nconst PROCESS_TERMINATION_GRACE_MS = 5_000;\n\nfunction abortError(): Error {\n const error = new Error(\"Operation aborted\");\n error.name = \"AbortError\";\n return error;\n}\n\nfunction signalProcessGroup(child: ManagedChildProcess, signal: NodeJS.Signals): void {\n try {\n if (child.pid) process.kill(-child.pid, signal);\n else child.kill(signal);\n } catch {\n try {\n child.kill(signal);\n } catch {\n // The process already exited.\n }\n }\n}\n\nexport function terminateProcessGroup(\n // ChildProcess structurally satisfies ManagedChildProcess, so the single type\n // covers both callers; keeping them as a union makes the shared EventEmitter\n // methods (once/removeListener) non-callable under @types/node's overloads.\n child: ManagedChildProcess,\n graceMs = PROCESS_TERMINATION_GRACE_MS,\n): Promise<void> {\n if (child.exitCode !== null) return Promise.resolve();\n return new Promise((resolve) => {\n let settled = false;\n const finish = (): void => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n child.removeListener(\"exit\", finish);\n resolve();\n };\n const timer = setTimeout(() => {\n signalProcessGroup(child, \"SIGKILL\");\n finish();\n }, graceMs);\n timer.unref();\n child.once(\"exit\", finish);\n signalProcessGroup(child, \"SIGTERM\");\n });\n}\n\nexport function runSetupCommand(\n cmd: string,\n cwd: string,\n onOutput: (stream: \"stdout\" | \"stderr\", data: string) => void,\n signal?: AbortSignal,\n): Promise<void> {\n if (signal?.aborted) return Promise.reject(abortError());\n return new Promise((resolve, reject) => {\n const child = spawn(\"sh\", [\"-c\", cmd], {\n cwd,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n detached: true,\n env: { ...process.env },\n });\n let settled = false;\n let aborting = false;\n const cleanup = (): void => signal?.removeEventListener(\"abort\", onAbort);\n const settle = (error?: Error): void => {\n if (settled) return;\n settled = true;\n cleanup();\n if (error) reject(error);\n else resolve();\n };\n const onAbort = (): void => {\n if (settled || aborting) return;\n aborting = true;\n void terminateProcessGroup(child).then(() => settle(abortError()));\n };\n signal?.addEventListener(\"abort\", onAbort, { once: true });\n if (signal?.aborted) onAbort();\n\n child.stdout.on(\"data\", (chunk: Buffer) => {\n if (aborting || signal?.aborted) return;\n onOutput(\"stdout\", chunk.toString());\n });\n\n child.stderr.on(\"data\", (chunk: Buffer) => {\n if (aborting || signal?.aborted) return;\n onOutput(\"stderr\", chunk.toString());\n });\n\n child.on(\"close\", (code) => {\n if (aborting) return;\n settle(code === 0 ? undefined : new Error(`Setup command exited with code ${code}`));\n });\n\n child.on(\"error\", (err) => {\n if (!aborting) settle(err);\n });\n });\n}\n\nconst AUTH_TOKEN_TIMEOUT_MS = 30_000;\n\nexport function runAuthTokenCommand(cmd: string, userEmail: string, cwd: string): string | null {\n try {\n const output = execSync(`${cmd} ${JSON.stringify(userEmail)}`, {\n cwd,\n timeout: AUTH_TOKEN_TIMEOUT_MS,\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n env: { ...process.env },\n });\n const token = output.toString().trim();\n return token || null;\n } catch {\n return null;\n }\n}\n\nexport function runStartCommand(\n cmd: string,\n cwd: string,\n onOutput: (stream: \"stdout\" | \"stderr\", data: string) => void,\n): ChildProcess {\n const child = spawn(\"sh\", [\"-c\", cmd], {\n cwd,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n detached: true,\n env: { ...process.env },\n });\n\n child.stdout.on(\"data\", (chunk: Buffer) => {\n onOutput(\"stdout\", chunk.toString());\n });\n\n child.stderr.on(\"data\", (chunk: Buffer) => {\n onOutput(\"stderr\", chunk.toString());\n });\n\n child.unref();\n return child;\n}\n"],"mappings":"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","names":["spawn"]}
|
|
@@ -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":[]}
|