@rallycry/conveyor-agent 10.13.18 → 10.13.50
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-YZCOIV3Q.js → boot-I353EAK6.js} +156 -125
- package/dist/boot-I353EAK6.js.map +1 -0
- package/dist/{chunk-3GDXX3OX.js → chunk-36VMMHYD.js} +5578 -2111
- package/dist/chunk-36VMMHYD.js.map +1 -0
- package/dist/{chunk-AGGVNADW.js → chunk-3Z4YQNJV.js} +59 -13
- package/dist/chunk-3Z4YQNJV.js.map +1 -0
- package/dist/chunk-5OQQSDVT.js +142 -0
- package/dist/chunk-5OQQSDVT.js.map +1 -0
- package/dist/{chunk-UZTJJD7Y.js → chunk-KCB7CSWJ.js} +43 -2
- package/dist/chunk-KCB7CSWJ.js.map +1 -0
- package/dist/{chunk-UDSRAHKP.js → chunk-QOJTJCYZ.js} +67 -19
- package/dist/chunk-QOJTJCYZ.js.map +1 -0
- package/dist/chunk-ULS4QPRE.js +138 -0
- package/dist/chunk-ULS4QPRE.js.map +1 -0
- package/dist/{chunk-72AEN6LB.js → chunk-XR6H326I.js} +26 -2
- package/dist/chunk-XR6H326I.js.map +1 -0
- package/dist/cli.js +196 -334
- package/dist/cli.js.map +1 -1
- package/dist/{client-BU4XA7CV.js → client-LRVVHTNG.js} +2 -2
- package/dist/heartbeat-worker.js +10 -12
- package/dist/heartbeat-worker.js.map +1 -1
- package/dist/index.d.ts +153 -8
- package/dist/index.js +5 -5
- package/dist/oom-watchdog-U7JERHA2.js +11 -0
- package/dist/server-NIOBJ46G.js +10 -0
- package/dist/server-NIOBJ46G.js.map +1 -0
- package/package.json +3 -3
- package/runtime/entrypoint.sh +5 -4
- package/dist/boot-YZCOIV3Q.js.map +0 -1
- package/dist/chunk-3GDXX3OX.js.map +0 -1
- package/dist/chunk-72AEN6LB.js.map +0 -1
- package/dist/chunk-7TQO4ZF4.js +0 -60
- package/dist/chunk-7TQO4ZF4.js.map +0 -1
- package/dist/chunk-AGGVNADW.js.map +0 -1
- package/dist/chunk-UDSRAHKP.js.map +0 -1
- package/dist/chunk-UZTJJD7Y.js.map +0 -1
- package/dist/server-BXAFMYDM.js +0 -9
- /package/dist/{client-BU4XA7CV.js.map → client-LRVVHTNG.js.map} +0 -0
- /package/dist/{server-BXAFMYDM.js.map → oom-watchdog-U7JERHA2.js.map} +0 -0
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
import {
|
|
2
2
|
loadPtySpawn,
|
|
3
3
|
terminateProcessGroup
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-XR6H326I.js";
|
|
5
5
|
import {
|
|
6
6
|
FrameReader,
|
|
7
7
|
timingSafeTokenEqual,
|
|
8
8
|
writeFrame
|
|
9
9
|
} from "./chunk-JIGG755T.js";
|
|
10
|
+
import {
|
|
11
|
+
startOomWatchdog
|
|
12
|
+
} from "./chunk-ULS4QPRE.js";
|
|
10
13
|
|
|
11
14
|
// src/workbench/server.ts
|
|
12
15
|
import { spawn } from "child_process";
|
|
13
16
|
import { readdir, readFile, stat } from "fs/promises";
|
|
14
17
|
import { createServer } from "net";
|
|
15
18
|
var READ_CHUNK_BYTES = 64 * 1024;
|
|
19
|
+
function oomKillMessage(event, label) {
|
|
20
|
+
const mib = (n) => Math.round(n / 1048576);
|
|
21
|
+
return `
|
|
22
|
+
[workbench] OOM watchdog killed this process group (${label}, rss ~${mib(event.victimRssBytes)}MiB); container memory ${mib(event.usedBytes)}/${mib(event.limitBytes)}MiB, reason=${event.reason}. Killing the largest workload keeps the workbench container (and every other process in it) alive.
|
|
23
|
+
`;
|
|
24
|
+
}
|
|
16
25
|
function errCode(err) {
|
|
17
26
|
return typeof err?.code === "string" ? err.code : void 0;
|
|
18
27
|
}
|
|
@@ -40,21 +49,41 @@ var FrameSink = class {
|
|
|
40
49
|
}
|
|
41
50
|
};
|
|
42
51
|
async function startWorkbenchServer(opts) {
|
|
43
|
-
const
|
|
52
|
+
const liveOps = /* @__PURE__ */ new Map();
|
|
53
|
+
const server = createServer((socket) => handleConnection(socket, opts, liveOps));
|
|
44
54
|
await new Promise((resolve, reject) => {
|
|
45
55
|
server.once("error", reject);
|
|
46
56
|
server.listen(opts.port, "127.0.0.1", () => resolve());
|
|
47
57
|
});
|
|
48
58
|
const address = server.address();
|
|
49
59
|
const port = typeof address === "object" && address ? address.port : opts.port;
|
|
60
|
+
const notifyOomKill = (event) => {
|
|
61
|
+
const op = liveOps.get(event.pgid);
|
|
62
|
+
if (!op) return false;
|
|
63
|
+
const message = Buffer.from(oomKillMessage(event, op.label), "utf8").toString("base64");
|
|
64
|
+
writeFrame(
|
|
65
|
+
op.socket,
|
|
66
|
+
op.kind === "exec" ? { t: "out", s: "stderr", d: message } : { t: "data", d: message }
|
|
67
|
+
);
|
|
68
|
+
return true;
|
|
69
|
+
};
|
|
70
|
+
let watchdog = null;
|
|
71
|
+
if (opts.oomWatchdog) {
|
|
72
|
+
watchdog = startOomWatchdog({ ...opts.oomWatchdog, onKill: notifyOomKill });
|
|
73
|
+
}
|
|
50
74
|
return {
|
|
51
75
|
port,
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
76
|
+
notifyOomKill,
|
|
77
|
+
liveOps: () => Array.from(liveOps, ([pgid, op]) => ({ pgid, kind: op.kind, label: op.label })),
|
|
78
|
+
close: () => {
|
|
79
|
+
watchdog?.stop();
|
|
80
|
+
return new Promise((resolve) => {
|
|
81
|
+
server.close(() => resolve());
|
|
82
|
+
});
|
|
83
|
+
}
|
|
55
84
|
};
|
|
56
85
|
}
|
|
57
|
-
function handleConnection(socket, opts) {
|
|
86
|
+
function handleConnection(socket, opts, liveOps) {
|
|
58
87
|
socket.on("error", () => void 0);
|
|
59
88
|
const sink = new FrameSink();
|
|
60
89
|
let dispatched = false;
|
|
@@ -64,11 +93,11 @@ function handleConnection(socket, opts) {
|
|
|
64
93
|
return;
|
|
65
94
|
}
|
|
66
95
|
dispatched = true;
|
|
67
|
-
dispatch(socket, frame, sink, opts);
|
|
96
|
+
dispatch(socket, frame, sink, opts, liveOps);
|
|
68
97
|
});
|
|
69
98
|
socket.on("data", (chunk) => reader.push(chunk));
|
|
70
99
|
}
|
|
71
|
-
function dispatch(socket, req, sink, opts) {
|
|
100
|
+
function dispatch(socket, req, sink, opts, liveOps) {
|
|
72
101
|
if (!req || typeof req !== "object" || !timingSafeTokenEqual(req.token ?? "", opts.token)) {
|
|
73
102
|
writeFrame(socket, { t: "error", message: "unauthorized", code: "unauthorized" });
|
|
74
103
|
socket.destroy();
|
|
@@ -84,10 +113,10 @@ function dispatch(socket, req, sink, opts) {
|
|
|
84
113
|
socket.end();
|
|
85
114
|
return;
|
|
86
115
|
case "exec":
|
|
87
|
-
runExec(socket, req, sink);
|
|
116
|
+
runExec(socket, req, sink, liveOps);
|
|
88
117
|
return;
|
|
89
118
|
case "pty":
|
|
90
|
-
void runPty(socket, req, sink);
|
|
119
|
+
void runPty(socket, req, sink, liveOps);
|
|
91
120
|
return;
|
|
92
121
|
case "readFile":
|
|
93
122
|
void runReadFile(socket, req);
|
|
@@ -103,7 +132,7 @@ function dispatch(socket, req, sink, opts) {
|
|
|
103
132
|
socket.end();
|
|
104
133
|
}
|
|
105
134
|
}
|
|
106
|
-
function runExec(socket, req, sink) {
|
|
135
|
+
function runExec(socket, req, sink, liveOps) {
|
|
107
136
|
let child;
|
|
108
137
|
try {
|
|
109
138
|
const [argvFile, ...argvRest] = req.argv ?? [];
|
|
@@ -123,6 +152,13 @@ function runExec(socket, req, sink) {
|
|
|
123
152
|
socket.end();
|
|
124
153
|
return;
|
|
125
154
|
}
|
|
155
|
+
if (child.pid) {
|
|
156
|
+
liveOps.set(child.pid, {
|
|
157
|
+
kind: "exec",
|
|
158
|
+
label: (req.argv?.join(" ") ?? req.command ?? "").slice(0, 120),
|
|
159
|
+
socket
|
|
160
|
+
});
|
|
161
|
+
}
|
|
126
162
|
let settled = false;
|
|
127
163
|
sink.setHandler((frame) => {
|
|
128
164
|
if (frame.t === "signal" && frame.mode === "term-group" && !settled) {
|
|
@@ -137,6 +173,7 @@ function runExec(socket, req, sink) {
|
|
|
137
173
|
});
|
|
138
174
|
child.on("close", (code, signal) => {
|
|
139
175
|
settled = true;
|
|
176
|
+
if (child.pid) liveOps.delete(child.pid);
|
|
140
177
|
writeFrame(socket, { t: "exit", code, signal });
|
|
141
178
|
socket.end();
|
|
142
179
|
});
|
|
@@ -150,7 +187,7 @@ function runExec(socket, req, sink) {
|
|
|
150
187
|
if (!settled) void terminateProcessGroup(child);
|
|
151
188
|
});
|
|
152
189
|
}
|
|
153
|
-
async function runPty(socket, req, sink) {
|
|
190
|
+
async function runPty(socket, req, sink, liveOps) {
|
|
154
191
|
let pty;
|
|
155
192
|
try {
|
|
156
193
|
const ptySpawn = await loadPtySpawn();
|
|
@@ -166,6 +203,14 @@ async function runPty(socket, req, sink) {
|
|
|
166
203
|
socket.end();
|
|
167
204
|
return;
|
|
168
205
|
}
|
|
206
|
+
const ptyPid = pty.pid;
|
|
207
|
+
if (ptyPid !== void 0) {
|
|
208
|
+
liveOps.set(ptyPid, {
|
|
209
|
+
kind: "pty",
|
|
210
|
+
label: [req.file, ...req.args].join(" ").slice(0, 120),
|
|
211
|
+
socket
|
|
212
|
+
});
|
|
213
|
+
}
|
|
169
214
|
let exited = false;
|
|
170
215
|
sink.setHandler((frame) => {
|
|
171
216
|
try {
|
|
@@ -184,6 +229,7 @@ async function runPty(socket, req, sink) {
|
|
|
184
229
|
});
|
|
185
230
|
pty.onExit((event) => {
|
|
186
231
|
exited = true;
|
|
232
|
+
if (ptyPid !== void 0) liveOps.delete(ptyPid);
|
|
187
233
|
writeFrame(socket, { t: "exit", code: event.exitCode, signal: null });
|
|
188
234
|
socket.end();
|
|
189
235
|
});
|
|
@@ -250,4 +296,4 @@ async function runReaddir(socket, req) {
|
|
|
250
296
|
export {
|
|
251
297
|
startWorkbenchServer
|
|
252
298
|
};
|
|
253
|
-
//# sourceMappingURL=chunk-
|
|
299
|
+
//# sourceMappingURL=chunk-3Z4YQNJV.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/workbench/server.ts"],"sourcesContent":["/**\n * The workbench launcher — PID 1 of the workbench container in split-mode\n * pods (CONVEYOR_MODE=workbench). A token-authed, loopback-only exec daemon:\n * the agent container drives every workspace-touching operation through it\n * (shell commands, the claude PTY, git, file reads) while staying insulated\n * from workload OOMs in its own container cgroup.\n *\n * One connection = one operation. A dropped connection kills the operation's\n * process (group) — the agent dying, restarting, or timing out must never\n * leave orphans accumulating against the workbench memory budget.\n */\n\nimport { spawn, type ChildProcess } from \"node:child_process\";\nimport { readdir, readFile, stat } from \"node:fs/promises\";\nimport { createServer, type Socket } from \"node:net\";\nimport { terminateProcessGroup } from \"../setup/commands.js\";\nimport { loadPtySpawn, type PtyProcess } from \"../harness/pty/pty-support.js\";\nimport {\n startOomWatchdog,\n type OomKillEvent,\n type OomWatchdogHandle,\n type OomWatchdogOptions,\n} from \"./oom-watchdog.js\";\nimport {\n FrameReader,\n timingSafeTokenEqual,\n writeFrame,\n type GitStatusFrame,\n type WorkbenchFrame,\n type WorkbenchRequest,\n} from \"./protocol.js\";\n\nconst READ_CHUNK_BYTES = 64 * 1024;\n\nexport interface WorkbenchServerOptions {\n port: number;\n token: string;\n version: string;\n /**\n * Live view of the workbench's background git preparation (boot/git-prep\n * GitPrepJob). Absent → the `gitStatus` op answers `ready`: non-pod servers\n * (tests, local dev, future embedders) have no git prep to wait for, and a\n * daemon that answered `pending` forever would wedge every caller gating\n * Claude spawn on git readiness.\n */\n getGitStatus?: () => GitStatusFrame;\n /**\n * Arm the early-OOM watchdog (see oom-watchdog.ts). Opt-in: production pod\n * boots pass it; tests and embedders that don't want a process killer\n * polling their cgroup simply omit it. The server supplies the kill\n * reporting (`onKill`) — a kill lands on the victim operation's own\n * connection as an stderr/data frame, so the agent transcript shows WHAT\n * died and why instead of a silent whole-container OOM.\n */\n oomWatchdog?: Omit<OomWatchdogOptions, \"onKill\">;\n}\n\nexport interface WorkbenchServerHandle {\n port: number;\n close(): Promise<void>;\n /** Write the OOM-kill notice to the victim pgid's owning connection, if the\n * group belongs to a live exec/pty operation. Returns whether it did.\n * Called by the watchdog just before the SIGKILL; exposed for tests. */\n notifyOomKill(event: OomKillEvent): boolean;\n /** Snapshot of the live exec/pty operations, keyed by process-group id. */\n liveOps(): Array<{ pgid: number; kind: \"exec\" | \"pty\"; label: string }>;\n}\n\n/** A live exec/pty operation: the direct child is its own process-group\n * leader (detached spawn / pty setsid), so its pid doubles as the pgid the\n * watchdog scores and kills. */\ninterface LiveOp {\n kind: \"exec\" | \"pty\";\n label: string;\n socket: Socket;\n}\n\ntype LiveOps = Map<number, LiveOp>;\n\nfunction oomKillMessage(event: OomKillEvent, label: string): string {\n const mib = (n: number): number => Math.round(n / 1048576);\n return (\n `\\n[workbench] OOM watchdog killed this process group (${label}, rss ~${mib(event.victimRssBytes)}MiB); ` +\n `container memory ${mib(event.usedBytes)}/${mib(event.limitBytes)}MiB, reason=${event.reason}. ` +\n `Killing the largest workload keeps the workbench container (and every other process in it) alive.\\n`\n );\n}\n\nfunction errCode(err: unknown): string | undefined {\n return typeof (err as { code?: unknown })?.code === \"string\"\n ? (err as { code: string }).code\n : undefined;\n}\n\nfunction sendError(socket: Socket, err: unknown): void {\n const frame: WorkbenchFrame = {\n t: \"error\",\n message: err instanceof Error ? err.message : String(err),\n ...(errCode(err) ? { code: errCode(err) } : {}),\n };\n writeFrame(socket, frame);\n}\n\n/** Merge request env over the daemon's own (mirrors the local spawn paths,\n * which inherit process.env and let callers override). */\nfunction mergedEnv(overrides?: Record<string, string>): NodeJS.ProcessEnv {\n return overrides ? { ...process.env, ...overrides } : { ...process.env };\n}\n\n/**\n * Post-dispatch inbound frames for the connection's operation. Handlers\n * assign `onNext`; frames arriving before assignment are queued so an input\n * racing the (async) pty spawn is never dropped.\n */\nclass FrameSink {\n private pending: WorkbenchFrame[] = [];\n private handler: ((frame: WorkbenchFrame) => void) | null = null;\n\n push(frame: WorkbenchFrame): void {\n if (this.handler) this.handler(frame);\n else this.pending.push(frame);\n }\n\n setHandler(handler: (frame: WorkbenchFrame) => void): void {\n this.handler = handler;\n for (const frame of this.pending.splice(0)) handler(frame);\n }\n}\n\nexport async function startWorkbenchServer(\n opts: WorkbenchServerOptions,\n): Promise<WorkbenchServerHandle> {\n const liveOps: LiveOps = new Map();\n const server = createServer((socket) => handleConnection(socket, opts, liveOps));\n await new Promise<void>((resolve, reject) => {\n server.once(\"error\", reject);\n server.listen(opts.port, \"127.0.0.1\", () => resolve());\n });\n const address = server.address();\n const port = typeof address === \"object\" && address ? address.port : opts.port;\n const notifyOomKill = (event: OomKillEvent): boolean => {\n const op = liveOps.get(event.pgid);\n if (!op) return false;\n const message = Buffer.from(oomKillMessage(event, op.label), \"utf8\").toString(\"base64\");\n writeFrame(\n op.socket,\n op.kind === \"exec\" ? { t: \"out\", s: \"stderr\", d: message } : { t: \"data\", d: message },\n );\n return true;\n };\n let watchdog: OomWatchdogHandle | null = null;\n if (opts.oomWatchdog) {\n watchdog = startOomWatchdog({ ...opts.oomWatchdog, onKill: notifyOomKill });\n }\n return {\n port,\n notifyOomKill,\n liveOps: () => Array.from(liveOps, ([pgid, op]) => ({ pgid, kind: op.kind, label: op.label })),\n close: () => {\n watchdog?.stop();\n return new Promise<void>((resolve) => {\n server.close(() => resolve());\n });\n },\n };\n}\n\nfunction handleConnection(socket: Socket, opts: WorkbenchServerOptions, liveOps: LiveOps): void {\n socket.on(\"error\", () => undefined);\n const sink = new FrameSink();\n let dispatched = false;\n const reader = new FrameReader((frame) => {\n if (dispatched) {\n sink.push(frame as unknown as WorkbenchFrame);\n return;\n }\n dispatched = true;\n dispatch(socket, frame as unknown as WorkbenchRequest, sink, opts, liveOps);\n });\n socket.on(\"data\", (chunk: Buffer) => reader.push(chunk));\n}\n\nfunction dispatch(\n socket: Socket,\n req: WorkbenchRequest,\n sink: FrameSink,\n opts: WorkbenchServerOptions,\n liveOps: LiveOps,\n): void {\n if (!req || typeof req !== \"object\" || !timingSafeTokenEqual(req.token ?? \"\", opts.token)) {\n writeFrame(socket, { t: \"error\", message: \"unauthorized\", code: \"unauthorized\" });\n socket.destroy();\n return;\n }\n switch (req.op) {\n case \"ping\":\n writeFrame(socket, { t: \"pong\", version: opts.version });\n socket.end();\n return;\n case \"gitStatus\":\n // No provider → default ready (see WorkbenchServerOptions.getGitStatus).\n writeFrame(socket, opts.getGitStatus?.() ?? { t: \"gitStatus\", state: \"ready\" });\n socket.end();\n return;\n case \"exec\":\n runExec(socket, req, sink, liveOps);\n return;\n case \"pty\":\n void runPty(socket, req, sink, liveOps);\n return;\n case \"readFile\":\n void runReadFile(socket, req);\n return;\n case \"stat\":\n void runStat(socket, req);\n return;\n case \"readdir\":\n void runReaddir(socket, req);\n return;\n default:\n writeFrame(socket, { t: \"error\", message: \"unknown op\", code: \"bad_request\" });\n socket.end();\n }\n}\n\nfunction runExec(\n socket: Socket,\n req: Extract<WorkbenchRequest, { op: \"exec\" }>,\n sink: FrameSink,\n liveOps: LiveOps,\n): void {\n let child: ChildProcess;\n try {\n // Argv form (git and friends) spawns directly with no shell; shell form\n // (setup/start commands) mirrors the local `spawn(\"sh\", [\"-c\", cmd])`.\n const [argvFile, ...argvRest] = req.argv ?? [];\n child = argvFile\n ? spawn(argvFile, argvRest, {\n cwd: req.cwd,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n detached: true,\n env: mergedEnv(req.env),\n })\n : spawn(\"sh\", [\"-c\", req.command ?? \"\"], {\n cwd: req.cwd,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n detached: true,\n env: mergedEnv(req.env),\n });\n } catch (err) {\n sendError(socket, err);\n socket.end();\n return;\n }\n\n if (child.pid) {\n liveOps.set(child.pid, {\n kind: \"exec\",\n label: (req.argv?.join(\" \") ?? req.command ?? \"\").slice(0, 120),\n socket,\n });\n }\n let settled = false;\n sink.setHandler((frame) => {\n if (frame.t === \"signal\" && frame.mode === \"term-group\" && !settled) {\n void terminateProcessGroup(child);\n }\n });\n child.stdout?.on(\"data\", (chunk: Buffer) => {\n writeFrame(socket, { t: \"out\", s: \"stdout\", d: chunk.toString(\"base64\") });\n });\n child.stderr?.on(\"data\", (chunk: Buffer) => {\n writeFrame(socket, { t: \"out\", s: \"stderr\", d: chunk.toString(\"base64\") });\n });\n child.on(\"close\", (code, signal) => {\n settled = true;\n if (child.pid) liveOps.delete(child.pid);\n writeFrame(socket, { t: \"exit\", code, signal });\n socket.end();\n });\n child.on(\"error\", (err) => {\n if (settled) return;\n settled = true;\n sendError(socket, err);\n socket.end();\n });\n // Agent gone (crash, timeout-abort, container death) → no orphans.\n socket.on(\"close\", () => {\n if (!settled) void terminateProcessGroup(child);\n });\n}\n\nasync function runPty(\n socket: Socket,\n req: Extract<WorkbenchRequest, { op: \"pty\" }>,\n sink: FrameSink,\n liveOps: LiveOps,\n): Promise<void> {\n let pty: PtyProcess;\n try {\n const ptySpawn = await loadPtySpawn();\n pty = ptySpawn(req.file, req.args, {\n name: \"xterm-color\",\n cols: req.cols,\n rows: req.rows,\n cwd: req.cwd,\n env: req.env,\n });\n } catch (err) {\n sendError(socket, err);\n socket.end();\n return;\n }\n\n // node-pty children get their own session (forkpty → setsid), so pid==pgid.\n const ptyPid = pty.pid;\n if (ptyPid !== undefined) {\n liveOps.set(ptyPid, {\n kind: \"pty\",\n label: [req.file, ...req.args].join(\" \").slice(0, 120),\n socket,\n });\n }\n let exited = false;\n sink.setHandler((frame) => {\n try {\n if (frame.t === \"input\" && typeof frame.d === \"string\") {\n pty.write(Buffer.from(frame.d, \"base64\").toString(\"utf8\"));\n } else if (\n frame.t === \"resize\" &&\n typeof frame.cols === \"number\" &&\n typeof frame.rows === \"number\"\n ) {\n pty.resize(frame.cols, frame.rows);\n } else if (frame.t === \"kill\") {\n pty.kill(typeof frame.sig === \"string\" ? frame.sig : undefined);\n }\n } catch {\n /* pty already dead — exit handler owns teardown */\n }\n });\n pty.onData((data) => {\n writeFrame(socket, { t: \"data\", d: Buffer.from(data, \"utf8\").toString(\"base64\") });\n });\n pty.onExit((event) => {\n exited = true;\n if (ptyPid !== undefined) liveOps.delete(ptyPid);\n writeFrame(socket, { t: \"exit\", code: event.exitCode, signal: null });\n socket.end();\n });\n socket.on(\"close\", () => {\n if (!exited) {\n try {\n pty.kill();\n } catch {\n /* already dead */\n }\n }\n });\n}\n\nasync function runReadFile(\n socket: Socket,\n req: Extract<WorkbenchRequest, { op: \"readFile\" }>,\n): Promise<void> {\n try {\n const content = await readFile(req.path);\n for (let offset = 0; offset < content.length; offset += READ_CHUNK_BYTES) {\n writeFrame(socket, {\n t: \"data\",\n d: content.subarray(offset, offset + READ_CHUNK_BYTES).toString(\"base64\"),\n });\n }\n writeFrame(socket, { t: \"end\" });\n } catch (err) {\n sendError(socket, err);\n }\n socket.end();\n}\n\nasync function runStat(\n socket: Socket,\n req: Extract<WorkbenchRequest, { op: \"stat\" }>,\n): Promise<void> {\n try {\n const s = await stat(req.path);\n writeFrame(socket, {\n t: \"stat\",\n exists: true,\n isFile: s.isFile(),\n isDirectory: s.isDirectory(),\n size: s.size,\n mtimeMs: s.mtimeMs,\n });\n } catch (err) {\n if (errCode(err) === \"ENOENT\") {\n writeFrame(socket, {\n t: \"stat\",\n exists: false,\n isFile: false,\n isDirectory: false,\n size: 0,\n mtimeMs: 0,\n });\n } else {\n sendError(socket, err);\n }\n }\n socket.end();\n}\n\nasync function runReaddir(\n socket: Socket,\n req: Extract<WorkbenchRequest, { op: \"readdir\" }>,\n): Promise<void> {\n try {\n writeFrame(socket, { t: \"entries\", names: await readdir(req.path) });\n } catch (err) {\n sendError(socket, err);\n }\n socket.end();\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// ../shared/dist/socket-core/index.js
|
|
2
|
+
function buildConveyorSocketOptions(auth) {
|
|
3
|
+
return {
|
|
4
|
+
auth,
|
|
5
|
+
transports: ["websocket"],
|
|
6
|
+
reconnection: true,
|
|
7
|
+
reconnectionAttempts: Infinity,
|
|
8
|
+
reconnectionDelay: 2e3,
|
|
9
|
+
reconnectionDelayMax: 3e4,
|
|
10
|
+
randomizationFactor: 0.3,
|
|
11
|
+
extraHeaders: { "ngrok-skip-browser-warning": "true" }
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function callWithAck(socket, event, payload, options) {
|
|
15
|
+
const { timeoutMs, requireData = false, makeTimeoutError, makeFailureError } = options;
|
|
16
|
+
return new Promise((resolve, reject) => {
|
|
17
|
+
let settled = false;
|
|
18
|
+
const timer = setTimeout(() => {
|
|
19
|
+
if (settled) return;
|
|
20
|
+
settled = true;
|
|
21
|
+
reject(makeTimeoutError());
|
|
22
|
+
}, timeoutMs);
|
|
23
|
+
socket.emit(event, payload, (response) => {
|
|
24
|
+
if (settled) return;
|
|
25
|
+
settled = true;
|
|
26
|
+
clearTimeout(timer);
|
|
27
|
+
if (response.success && (!requireData || response.data !== void 0)) {
|
|
28
|
+
resolve(response.data);
|
|
29
|
+
} else {
|
|
30
|
+
reject(makeFailureError(response.error));
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function waitForConnected(socket, timeoutMs, makeTimeoutError) {
|
|
36
|
+
if (socket.connected) return Promise.resolve();
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
const cleanup = () => {
|
|
39
|
+
clearTimeout(timer);
|
|
40
|
+
socket.off("connect", onConnect);
|
|
41
|
+
};
|
|
42
|
+
const onConnect = () => {
|
|
43
|
+
cleanup();
|
|
44
|
+
resolve();
|
|
45
|
+
};
|
|
46
|
+
const timer = setTimeout(() => {
|
|
47
|
+
cleanup();
|
|
48
|
+
reject(makeTimeoutError());
|
|
49
|
+
}, timeoutMs);
|
|
50
|
+
socket.once("connect", onConnect);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/connection/loop-lag.ts
|
|
55
|
+
var LOOP_TICK_INTERVAL_MS = 250;
|
|
56
|
+
var LOOP_STALL_LOG_MS = 1e3;
|
|
57
|
+
var LOOP_STATUS_VALUES = ["active", "idle", "building", "waiting"];
|
|
58
|
+
function heartbeatStatusFor(status) {
|
|
59
|
+
return status === "waiting" ? "active" : status;
|
|
60
|
+
}
|
|
61
|
+
var WORKING_RUNNER_STATUSES = /* @__PURE__ */ new Set([
|
|
62
|
+
"connecting",
|
|
63
|
+
"connected",
|
|
64
|
+
"setup",
|
|
65
|
+
"fetching_context",
|
|
66
|
+
"running"
|
|
67
|
+
]);
|
|
68
|
+
var RESTING_RUNNER_STATUSES = /* @__PURE__ */ new Set([
|
|
69
|
+
"idle",
|
|
70
|
+
"waiting_for_input",
|
|
71
|
+
"stopping",
|
|
72
|
+
"finished",
|
|
73
|
+
"disconnected",
|
|
74
|
+
"error"
|
|
75
|
+
]);
|
|
76
|
+
function loopStatusForRunnerStatus(status) {
|
|
77
|
+
if (status === null || status === void 0) return "idle";
|
|
78
|
+
if (WORKING_RUNNER_STATUSES.has(status)) return "active";
|
|
79
|
+
if (RESTING_RUNNER_STATUSES.has(status)) return "idle";
|
|
80
|
+
return "active";
|
|
81
|
+
}
|
|
82
|
+
function lagFromBuffer(view) {
|
|
83
|
+
const last = view[0];
|
|
84
|
+
if (!last) return 0;
|
|
85
|
+
return Math.max(0, Date.now() - last - LOOP_TICK_INTERVAL_MS);
|
|
86
|
+
}
|
|
87
|
+
function statusFromBuffer(view) {
|
|
88
|
+
return LOOP_STATUS_VALUES[view[1]] ?? "active";
|
|
89
|
+
}
|
|
90
|
+
var LoopLagMonitor = class {
|
|
91
|
+
sharedBuffer = new SharedArrayBuffer(2 * Float64Array.BYTES_PER_ELEMENT);
|
|
92
|
+
view = new Float64Array(this.sharedBuffer);
|
|
93
|
+
timer = null;
|
|
94
|
+
maxLagMs = 0;
|
|
95
|
+
start() {
|
|
96
|
+
if (this.timer) return;
|
|
97
|
+
this.view[0] = Date.now();
|
|
98
|
+
this.timer = setInterval(() => {
|
|
99
|
+
const now = Date.now();
|
|
100
|
+
const lag = Math.max(0, now - this.view[0] - LOOP_TICK_INTERVAL_MS);
|
|
101
|
+
if (lag >= LOOP_STALL_LOG_MS) {
|
|
102
|
+
this.maxLagMs = Math.max(this.maxLagMs, lag);
|
|
103
|
+
process.stderr.write(
|
|
104
|
+
`[conveyor-agent] event-loop stall: ${Math.round(lag)}ms (${(/* @__PURE__ */ new Date()).toISOString()})
|
|
105
|
+
`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
this.view[0] = now;
|
|
109
|
+
}, LOOP_TICK_INTERVAL_MS);
|
|
110
|
+
this.timer.unref();
|
|
111
|
+
}
|
|
112
|
+
stop() {
|
|
113
|
+
if (this.timer) clearInterval(this.timer);
|
|
114
|
+
this.timer = null;
|
|
115
|
+
}
|
|
116
|
+
/** Mirror the runner's current status for the worker to report. */
|
|
117
|
+
setStatus(status) {
|
|
118
|
+
const idx = LOOP_STATUS_VALUES.indexOf(status);
|
|
119
|
+
this.view[1] = idx === -1 ? 0 : idx;
|
|
120
|
+
}
|
|
121
|
+
/** Worst stall observed since the previous call (then reset). Includes the
|
|
122
|
+
* live lag so a beat sent mid-stall from this thread — possible when the
|
|
123
|
+
* heartbeat timer fires before the refresher in the same tick batch — is
|
|
124
|
+
* still truthful. */
|
|
125
|
+
takeMaxLagMs() {
|
|
126
|
+
const max = Math.max(this.maxLagMs, lagFromBuffer(this.view));
|
|
127
|
+
this.maxLagMs = 0;
|
|
128
|
+
return max;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export {
|
|
133
|
+
buildConveyorSocketOptions,
|
|
134
|
+
callWithAck,
|
|
135
|
+
waitForConnected,
|
|
136
|
+
heartbeatStatusFor,
|
|
137
|
+
loopStatusForRunnerStatus,
|
|
138
|
+
lagFromBuffer,
|
|
139
|
+
statusFromBuffer,
|
|
140
|
+
LoopLagMonitor
|
|
141
|
+
};
|
|
142
|
+
//# sourceMappingURL=chunk-5OQQSDVT.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../shared/dist/socket-core/index.js","../src/connection/loop-lag.ts"],"sourcesContent":["// src/socket-core/socket-options.ts\nfunction buildConveyorSocketOptions(auth) {\n return {\n auth,\n transports: [\"websocket\"],\n reconnection: true,\n reconnectionAttempts: Infinity,\n reconnectionDelay: 2e3,\n reconnectionDelayMax: 3e4,\n randomizationFactor: 0.3,\n extraHeaders: { \"ngrok-skip-browser-warning\": \"true\" }\n };\n}\n\n// src/socket-core/call-with-ack.ts\nfunction callWithAck(socket, event, payload, options) {\n const { timeoutMs, requireData = false, makeTimeoutError, makeFailureError } = options;\n return new Promise((resolve, reject) => {\n let settled = false;\n const timer = setTimeout(() => {\n if (settled) return;\n settled = true;\n reject(makeTimeoutError());\n }, timeoutMs);\n socket.emit(event, payload, (response) => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n if (response.success && (!requireData || response.data !== void 0)) {\n resolve(response.data);\n } else {\n reject(makeFailureError(response.error));\n }\n });\n });\n}\nfunction waitForConnected(socket, timeoutMs, makeTimeoutError) {\n if (socket.connected) return Promise.resolve();\n return new Promise((resolve, reject) => {\n const cleanup = () => {\n clearTimeout(timer);\n socket.off(\"connect\", onConnect);\n };\n const onConnect = () => {\n cleanup();\n resolve();\n };\n const timer = setTimeout(() => {\n cleanup();\n reject(makeTimeoutError());\n }, timeoutMs);\n socket.once(\"connect\", onConnect);\n });\n}\nexport {\n buildConveyorSocketOptions,\n callWithAck,\n waitForConnected\n};\n","/**\n * Main event-loop lag measurement, shared with the heartbeat worker.\n *\n * The monitor owns a SharedArrayBuffer the main thread refreshes on a fast\n * timer. Two consumers read it:\n * - the MAIN-loop heartbeat reports `takeMaxLagMs()` — the worst stall\n * observed since the last beat (post-hoc: by the time the heartbeat timer\n * fires after a stall, the refresher has already run, so instantaneous\n * lag would read ~0);\n * - the heartbeat WORKER reads the buffer live from its own thread, where a\n * stalled main loop shows up as a stale tick — that is the signal that\n * keeps the session lease alive while the main loop is starved.\n *\n * Layout (Float64Array): [0] = last main-loop tick (epoch ms),\n * [1] = status index into LOOP_STATUS_VALUES (what the worker should report).\n */\n\nexport const LOOP_TICK_INTERVAL_MS = 250;\n/** Stalls at or above this are logged (and remembered for the next beat). */\nexport const LOOP_STALL_LOG_MS = 1_000;\n\n/** Append-only: the index into this array is what crosses the SharedArrayBuffer\n * to the heartbeat worker, so reordering would silently remap live statuses. */\nexport const LOOP_STATUS_VALUES = [\"active\", \"idle\", \"building\", \"waiting\"] as const;\nexport type LoopStatus = (typeof LOOP_STATUS_VALUES)[number];\n\n/** What a `LoopStatus` reports as on the wire (`AgentHeartbeat.status`).\n *\n * `waiting` — the runner's turn is over but background work it launched is\n * still running in the pod (see runner/background-work.ts) — reports as\n * `active`, deliberately:\n * - The API's `AgentHeartbeatSchema` enum is `active | idle | building`. A\n * newer agent sending a value an older API doesn't know would have its\n * heartbeat REJECTED, which stops renewing the session lease — strictly\n * worse than the idle-sleep bug this whole mechanism exists to fix.\n * - `active` is also the semantically correct answer for the one consumer\n * that matters: `heartbeatBumps` (apps/api/src/services/workspace/\n * activity-clock.ts) is `status !== \"idle\"`, so the workspace activity\n * clock stays fresh and the reconciler can't sleep the pod mid-gate. */\nexport function heartbeatStatusFor(status: LoopStatus): \"active\" | \"idle\" | \"building\" {\n return status === \"waiting\" ? \"active\" : status;\n}\n\n/**\n * The runner states (`AgentRunnerStatus`) that mean the agent is actually doing\n * work in the pod. Everything else is a state where the pod is burning compute\n * with nothing running in it.\n *\n * `waiting_for_input` is deliberately NOT here. It is a prefilled-but-\n * unsubmitted Connected TUI (or an agent question) parked on a HUMAN — the CLI\n * process is alive and repainting, but no turn is in flight. Reporting it as\n * `active` renewed `Workspace.activityExpiresAt` on every 30s beat, so a card\n * whose agent was waiting on a person never idled out and stayed \"active\" on\n * the board until the runner's own 30-minute idle timer fired, overriding the\n * project's (much shorter) inactivity window. A human who comes back wakes the\n * card the normal way — a chat post or `ensurePrimaryWorkspace` intent write\n * beats the clock outright — and live keystrokes in the terminal bump it\n * directly (agent-session-service `bumpPtyInputActivity`).\n */\nconst WORKING_RUNNER_STATUSES: ReadonlySet<string> = new Set([\n \"connecting\",\n \"connected\",\n \"setup\",\n \"fetching_context\",\n \"running\",\n]);\n\n/** Runner states where the pod is parked: the process is alive, nothing runs. */\nconst RESTING_RUNNER_STATUSES: ReadonlySet<string> = new Set([\n \"idle\",\n \"waiting_for_input\",\n \"stopping\",\n \"finished\",\n \"disconnected\",\n \"error\",\n]);\n\n/**\n * Classify an `AgentRunnerStatus` (taken as a plain string so this module stays\n * dependency-free) into the loop status its heartbeats should carry.\n *\n * FAIL OPEN: an unrecognized status reports `active`. A status this classifier\n * has never heard of — a newer runner state, a value from an older build — must\n * keep the pod alive, because over-filtering reaps a pod that is genuinely\n * working, which is strictly worse than a pod that sleeps late.\n */\nexport function loopStatusForRunnerStatus(status: string | null | undefined): LoopStatus {\n if (status === null || status === undefined) return \"idle\";\n if (WORKING_RUNNER_STATUSES.has(status)) return \"active\";\n // Known-parked states — idle, waiting_for_input, stopping, finished,\n // disconnected, error — and only those, report idle.\n if (RESTING_RUNNER_STATUSES.has(status)) return \"idle\";\n return \"active\";\n}\n\n/** Live lag as seen from ANY thread holding the buffer: how stale the main\n * loop's tick is beyond its refresh interval. ~0 while the loop is healthy. */\nexport function lagFromBuffer(view: Float64Array): number {\n const last = view[0];\n if (!last) return 0;\n return Math.max(0, Date.now() - last - LOOP_TICK_INTERVAL_MS);\n}\n\nexport function statusFromBuffer(view: Float64Array): LoopStatus {\n return LOOP_STATUS_VALUES[view[1]] ?? \"active\";\n}\n\nexport class LoopLagMonitor {\n readonly sharedBuffer = new SharedArrayBuffer(2 * Float64Array.BYTES_PER_ELEMENT);\n private readonly view = new Float64Array(this.sharedBuffer);\n private timer: NodeJS.Timeout | null = null;\n private maxLagMs = 0;\n\n start(): void {\n if (this.timer) return;\n this.view[0] = Date.now();\n this.timer = setInterval(() => {\n const now = Date.now();\n const lag = Math.max(0, now - this.view[0] - LOOP_TICK_INTERVAL_MS);\n if (lag >= LOOP_STALL_LOG_MS) {\n this.maxLagMs = Math.max(this.maxLagMs, lag);\n process.stderr.write(\n `[conveyor-agent] event-loop stall: ${Math.round(lag)}ms (${new Date().toISOString()})\\n`,\n );\n }\n this.view[0] = now;\n }, LOOP_TICK_INTERVAL_MS);\n // Never keep the process alive just to measure it.\n this.timer.unref();\n }\n\n stop(): void {\n if (this.timer) clearInterval(this.timer);\n this.timer = null;\n }\n\n /** Mirror the runner's current status for the worker to report. */\n setStatus(status: LoopStatus): void {\n const idx = LOOP_STATUS_VALUES.indexOf(status);\n this.view[1] = idx === -1 ? 0 : idx;\n }\n\n /** Worst stall observed since the previous call (then reset). Includes the\n * live lag so a beat sent mid-stall from this thread — possible when the\n * heartbeat timer fires before the refresher in the same tick batch — is\n * still truthful. */\n takeMaxLagMs(): number {\n const max = Math.max(this.maxLagMs, lagFromBuffer(this.view));\n this.maxLagMs = 0;\n return max;\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -11,6 +11,7 @@ function mapChatHistory(messages) {
|
|
|
11
11
|
userId: m.userId,
|
|
12
12
|
userName: m.user?.name ?? void 0,
|
|
13
13
|
createdAt: m.createdAt,
|
|
14
|
+
...m.source ? { source: m.source } : {},
|
|
14
15
|
...m.files && m.files.length > 0 ? {
|
|
15
16
|
files: m.files.map((f) => ({
|
|
16
17
|
fileId: f.id,
|
|
@@ -239,6 +240,44 @@ var GitPrepJob = class {
|
|
|
239
240
|
}
|
|
240
241
|
};
|
|
241
242
|
|
|
243
|
+
// src/setup/boot-milestone.ts
|
|
244
|
+
var REPORT_TIMEOUT_MS = 5e3;
|
|
245
|
+
var socketFallback = null;
|
|
246
|
+
function registerBootMilestoneSocketFallback(fn) {
|
|
247
|
+
socketFallback = fn;
|
|
248
|
+
}
|
|
249
|
+
async function reportBootMilestone(opts) {
|
|
250
|
+
const env = opts.env ?? process.env;
|
|
251
|
+
const apiUrl = env.CONVEYOR_API_URL;
|
|
252
|
+
const token = env.POD_BOOTSTRAP_TOKEN;
|
|
253
|
+
if (!apiUrl || !token) {
|
|
254
|
+
try {
|
|
255
|
+
socketFallback?.(opts.key);
|
|
256
|
+
} catch {
|
|
257
|
+
}
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
const fetchFn = opts.fetchFn ?? fetch;
|
|
261
|
+
const controller = new AbortController();
|
|
262
|
+
const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? REPORT_TIMEOUT_MS);
|
|
263
|
+
try {
|
|
264
|
+
const res = await fetchFn(`${apiUrl.replace(/\/$/, "")}/api/v3/pods/boot-milestone`, {
|
|
265
|
+
method: "POST",
|
|
266
|
+
headers: {
|
|
267
|
+
"content-type": "application/json",
|
|
268
|
+
authorization: `Bearer ${token}`
|
|
269
|
+
},
|
|
270
|
+
body: JSON.stringify({ key: opts.key }),
|
|
271
|
+
signal: controller.signal
|
|
272
|
+
});
|
|
273
|
+
return res.ok;
|
|
274
|
+
} catch {
|
|
275
|
+
return false;
|
|
276
|
+
} finally {
|
|
277
|
+
clearTimeout(timer);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
242
281
|
export {
|
|
243
282
|
mapChatHistory,
|
|
244
283
|
readAgentVersion,
|
|
@@ -249,6 +288,8 @@ export {
|
|
|
249
288
|
redactToken,
|
|
250
289
|
GIT_PREP_MAX_RETRIES,
|
|
251
290
|
DEFAULT_RETRY_DELAY_MS,
|
|
252
|
-
GitPrepJob
|
|
291
|
+
GitPrepJob,
|
|
292
|
+
registerBootMilestoneSocketFallback,
|
|
293
|
+
reportBootMilestone
|
|
253
294
|
};
|
|
254
|
-
//# sourceMappingURL=chunk-
|
|
295
|
+
//# sourceMappingURL=chunk-KCB7CSWJ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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\";\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}\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 // 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\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 \"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 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([\"clone\", \"--single-branch\", \"--branch\", baseBranch, paths.remoteUrl, \"repo\"], {\n cwd: paths.workspacesDir,\n 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 { repoOwner, repoName, branch } = bundle.gitPlan;\n const token = bundle.githubToken;\n const paths: PrepPaths = {\n workspacesDir,\n repoDir,\n remoteUrl: `https://x-access-token:${token}@github.com/${repoOwner}/${repoName}.git`,\n };\n try {\n if (existsSync(join(repoDir, \".git\")) && token) {\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 if (token && repoOwner && repoName && branch) {\n try {\n mkdirSync(workspacesDir, { recursive: true });\n } catch {\n /* clone below surfaces the real failure */\n }\n return await clonePostAssignment(deps, paths);\n }\n // No git plan (task-less pod, or no token/repo) — nothing to prepare;\n // signal ready so the agent doesn't wait out its gate timeout.\n log.info(\"[boot] No git plan to prepare — marking git ready.\");\n return { state: \"ready\" };\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":"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","names":["join"]}
|
|
@@ -41,6 +41,38 @@ var RemoteProcessHandle = class extends EventEmitter {
|
|
|
41
41
|
function frameError(frame) {
|
|
42
42
|
return new WorkbenchError(frame.message, frame.code);
|
|
43
43
|
}
|
|
44
|
+
function execFailure(message, fields) {
|
|
45
|
+
const failure = new Error(message);
|
|
46
|
+
failure.stdout = fields.stdout;
|
|
47
|
+
failure.stderr = fields.stderr;
|
|
48
|
+
if (fields.code !== void 0) failure.code = fields.code;
|
|
49
|
+
if (fields.signal !== void 0) failure.signal = fields.signal;
|
|
50
|
+
return failure;
|
|
51
|
+
}
|
|
52
|
+
function settleExecOutcome(state, resolve, reject) {
|
|
53
|
+
const out = Buffer.concat(state.stdout).toString("utf8");
|
|
54
|
+
const errText = Buffer.concat(state.stderr).toString("utf8");
|
|
55
|
+
if (state.overflowed) {
|
|
56
|
+
reject(
|
|
57
|
+
execFailure(
|
|
58
|
+
`maxBuffer length exceeded (${state.maxBuffer} bytes): ${state.file} ${state.args.join(" ")}`,
|
|
59
|
+
{ stdout: out, stderr: errText }
|
|
60
|
+
)
|
|
61
|
+
);
|
|
62
|
+
} else if (!state.exited) {
|
|
63
|
+
reject(state.error ?? new WorkbenchError("connection closed before exit"));
|
|
64
|
+
} else if (state.exitCode === 0) {
|
|
65
|
+
resolve({ stdout: out, stderr: errText });
|
|
66
|
+
} else {
|
|
67
|
+
reject(
|
|
68
|
+
execFailure(
|
|
69
|
+
state.timedOut ? `Command timed out: ${state.file}` : `Command failed: ${state.file} ${state.args.join(" ")}
|
|
70
|
+
${errText}`,
|
|
71
|
+
{ stdout: out, stderr: errText, code: state.exitCode, signal: state.exitSignal }
|
|
72
|
+
)
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
44
76
|
var WorkbenchClient = class {
|
|
45
77
|
port;
|
|
46
78
|
host;
|
|
@@ -191,6 +223,10 @@ var WorkbenchClient = class {
|
|
|
191
223
|
return new Promise((resolve, reject) => {
|
|
192
224
|
const stdout = [];
|
|
193
225
|
const stderr = [];
|
|
226
|
+
let stdoutBytes = 0;
|
|
227
|
+
let stderrBytes = 0;
|
|
228
|
+
const maxBuffer = opts.maxBuffer;
|
|
229
|
+
let overflowed = false;
|
|
194
230
|
let exitCode = null;
|
|
195
231
|
let exitSignal = null;
|
|
196
232
|
let exited = false;
|
|
@@ -199,7 +235,19 @@ var WorkbenchClient = class {
|
|
|
199
235
|
{ op: "exec", argv: [file, ...args], cwd: opts.cwd ?? process.cwd() },
|
|
200
236
|
(frame) => {
|
|
201
237
|
if (frame.t === "out") {
|
|
202
|
-
|
|
238
|
+
const buf = Buffer.from(frame.d, "base64");
|
|
239
|
+
if (frame.s === "stdout") {
|
|
240
|
+
stdout.push(buf);
|
|
241
|
+
stdoutBytes += buf.length;
|
|
242
|
+
} else {
|
|
243
|
+
stderr.push(buf);
|
|
244
|
+
stderrBytes += buf.length;
|
|
245
|
+
}
|
|
246
|
+
if (maxBuffer !== void 0 && !overflowed && (stdoutBytes > maxBuffer || stderrBytes > maxBuffer)) {
|
|
247
|
+
overflowed = true;
|
|
248
|
+
if (timer) clearTimeout(timer);
|
|
249
|
+
writeFrame(socket, { t: "signal", mode: "term-group" });
|
|
250
|
+
}
|
|
203
251
|
} else if (frame.t === "exit") {
|
|
204
252
|
exited = true;
|
|
205
253
|
exitCode = frame.code;
|
|
@@ -208,23 +256,23 @@ var WorkbenchClient = class {
|
|
|
208
256
|
},
|
|
209
257
|
(error) => {
|
|
210
258
|
if (timer) clearTimeout(timer);
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
reject
|
|
227
|
-
|
|
259
|
+
settleExecOutcome(
|
|
260
|
+
{
|
|
261
|
+
file,
|
|
262
|
+
args,
|
|
263
|
+
stdout,
|
|
264
|
+
stderr,
|
|
265
|
+
overflowed,
|
|
266
|
+
exited,
|
|
267
|
+
exitCode,
|
|
268
|
+
exitSignal,
|
|
269
|
+
timedOut,
|
|
270
|
+
maxBuffer,
|
|
271
|
+
error: error ?? null
|
|
272
|
+
},
|
|
273
|
+
resolve,
|
|
274
|
+
reject
|
|
275
|
+
);
|
|
228
276
|
}
|
|
229
277
|
);
|
|
230
278
|
const timer = opts.timeout ? setTimeout(() => {
|
|
@@ -361,4 +409,4 @@ export {
|
|
|
361
409
|
getWorkbenchClient,
|
|
362
410
|
resetWorkbenchClient
|
|
363
411
|
};
|
|
364
|
-
//# sourceMappingURL=chunk-
|
|
412
|
+
//# sourceMappingURL=chunk-QOJTJCYZ.js.map
|