@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
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
WorkbenchError,
|
|
5
5
|
getWorkbenchClient,
|
|
6
6
|
resetWorkbenchClient
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-QOJTJCYZ.js";
|
|
8
8
|
import "./chunk-4VUQ2NPF.js";
|
|
9
9
|
import "./chunk-JIGG755T.js";
|
|
10
10
|
export {
|
|
@@ -14,4 +14,4 @@ export {
|
|
|
14
14
|
getWorkbenchClient,
|
|
15
15
|
resetWorkbenchClient
|
|
16
16
|
};
|
|
17
|
-
//# sourceMappingURL=client-
|
|
17
|
+
//# sourceMappingURL=client-LRVVHTNG.js.map
|
package/dist/heartbeat-worker.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
|
+
buildConveyorSocketOptions,
|
|
3
|
+
heartbeatStatusFor,
|
|
2
4
|
lagFromBuffer,
|
|
3
5
|
statusFromBuffer
|
|
4
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-5OQQSDVT.js";
|
|
5
7
|
|
|
6
8
|
// src/connection/heartbeat-worker.ts
|
|
7
9
|
import { parentPort, workerData } from "worker_threads";
|
|
@@ -9,16 +11,10 @@ import { io } from "socket.io-client";
|
|
|
9
11
|
var data = workerData;
|
|
10
12
|
var view = new Float64Array(data.sharedBuffer);
|
|
11
13
|
var taskToken = data.taskToken;
|
|
12
|
-
var socket = io(
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
reconnectionAttempts: Infinity,
|
|
17
|
-
reconnectionDelay: 2e3,
|
|
18
|
-
reconnectionDelayMax: 3e4,
|
|
19
|
-
randomizationFactor: 0.3,
|
|
20
|
-
extraHeaders: { "ngrok-skip-browser-warning": "true" }
|
|
21
|
-
});
|
|
14
|
+
var socket = io(
|
|
15
|
+
data.apiUrl,
|
|
16
|
+
buildConveyorSocketOptions((cb) => cb({ taskToken, runnerMode: data.runnerMode }))
|
|
17
|
+
);
|
|
22
18
|
parentPort?.on("message", (msg) => {
|
|
23
19
|
if (msg?.taskToken) taskToken = msg.taskToken;
|
|
24
20
|
});
|
|
@@ -34,7 +30,9 @@ function beat() {
|
|
|
34
30
|
{
|
|
35
31
|
sessionId: data.sessionId,
|
|
36
32
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
37
|
-
|
|
33
|
+
// `waiting` (runner idle, background work still running in the pod)
|
|
34
|
+
// goes out as `active` — see heartbeatStatusFor.
|
|
35
|
+
status: heartbeatStatusFor(statusFromBuffer(view)),
|
|
38
36
|
loopLagMs: Math.round(lagFromBuffer(view))
|
|
39
37
|
},
|
|
40
38
|
// Ack intentionally ignored: a missed beat self-heals on the next tick,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/connection/heartbeat-worker.ts"],"sourcesContent":["/**\n * Starvation-proof heartbeat: a worker thread with its OWN event loop and its\n * OWN Socket.IO connection, so a stalled/starved main loop cannot silence the\n * session's liveness signal.\n *\n * Why this exists: the v3 lease chain (heartbeat → renewSessionLease, TTL\n * 150s + 120s stranded grace) reaps a session whose agent goes quiet for\n * ~4.5 minutes. A main-loop stall during a heavy gate used to do exactly\n * that — the session was declared stranded and the restart tore down the\n * in-flight run. This worker keeps beating through such stalls, reporting\n * `loopLagMs` (staleness of the main loop's shared tick) so the API can tell\n * \"alive but busy\" (renew the lease) from \"wedged\" (stop renewing past the\n * wedge ceiling and let the normal recovery run). The main loop still sends\n * its richer heartbeat when healthy — duplicate renewals are idempotent.\n *\n * Deliberately minimal: no bootstrap token refresh (the main thread posts\n * rotated tokens via postMessage), no reconnect ceremony beyond Socket.IO's\n * built-in retry, and it never throws — a broken worker just means heartbeats\n * degrade to main-loop-only, which is yesterday's behavior.\n */\nimport { parentPort, workerData } from \"node:worker_threads\";\nimport { io } from \"socket.io-client\";\nimport { lagFromBuffer, statusFromBuffer } from \"./loop-lag.js\";\n\ninterface HeartbeatWorkerData {\n apiUrl: string;\n taskToken: string;\n sessionId: string;\n runnerMode: string;\n sharedBuffer: SharedArrayBuffer;\n intervalMs: number;\n}\n\nconst data = workerData as HeartbeatWorkerData;\nconst view = new Float64Array(data.sharedBuffer);\nlet taskToken = data.taskToken;\n\nconst socket = io(data.apiUrl
|
|
1
|
+
{"version":3,"sources":["../src/connection/heartbeat-worker.ts"],"sourcesContent":["/**\n * Starvation-proof heartbeat: a worker thread with its OWN event loop and its\n * OWN Socket.IO connection, so a stalled/starved main loop cannot silence the\n * session's liveness signal.\n *\n * Why this exists: the v3 lease chain (heartbeat → renewSessionLease, TTL\n * 150s + 120s stranded grace) reaps a session whose agent goes quiet for\n * ~4.5 minutes. A main-loop stall during a heavy gate used to do exactly\n * that — the session was declared stranded and the restart tore down the\n * in-flight run. This worker keeps beating through such stalls, reporting\n * `loopLagMs` (staleness of the main loop's shared tick) so the API can tell\n * \"alive but busy\" (renew the lease) from \"wedged\" (stop renewing past the\n * wedge ceiling and let the normal recovery run). The main loop still sends\n * its richer heartbeat when healthy — duplicate renewals are idempotent.\n *\n * Deliberately minimal: no bootstrap token refresh (the main thread posts\n * rotated tokens via postMessage), no reconnect ceremony beyond Socket.IO's\n * built-in retry, and it never throws — a broken worker just means heartbeats\n * degrade to main-loop-only, which is yesterday's behavior.\n */\nimport { parentPort, workerData } from \"node:worker_threads\";\nimport { io } from \"socket.io-client\";\nimport { buildConveyorSocketOptions } from \"@project/shared/socket-core\";\nimport { heartbeatStatusFor, lagFromBuffer, statusFromBuffer } from \"./loop-lag.js\";\n\ninterface HeartbeatWorkerData {\n apiUrl: string;\n taskToken: string;\n sessionId: string;\n runnerMode: string;\n sharedBuffer: SharedArrayBuffer;\n intervalMs: number;\n}\n\nconst data = workerData as HeartbeatWorkerData;\nconst view = new Float64Array(data.sharedBuffer);\nlet taskToken = data.taskToken;\n\nconst socket = io(\n data.apiUrl,\n buildConveyorSocketOptions((cb) => cb({ taskToken, runnerMode: data.runnerMode })),\n);\n\n// The main thread posts rotated task tokens (bootstrap refresh) so the worker\n// socket survives token expiry. The next (re)connect picks it up via the auth\n// callback above; a server-initiated disconnect needs a manual nudge.\nparentPort?.on(\"message\", (msg: { taskToken?: string }) => {\n if (msg?.taskToken) taskToken = msg.taskToken;\n});\n\nsocket.on(\"disconnect\", (reason: string) => {\n if (reason === \"io server disconnect\" || reason === \"server namespace disconnect\") {\n socket.connect();\n }\n});\n\nfunction beat(): void {\n if (!socket.connected) return;\n socket.emit(\n \"agentSessionService:heartbeat\",\n {\n sessionId: data.sessionId,\n timestamp: new Date().toISOString(),\n // `waiting` (runner idle, background work still running in the pod)\n // goes out as `active` — see heartbeatStatusFor.\n status: heartbeatStatusFor(statusFromBuffer(view)),\n loopLagMs: Math.round(lagFromBuffer(view)),\n },\n // Ack intentionally ignored: a missed beat self-heals on the next tick,\n // and the worker must never accumulate state or throw.\n () => {},\n );\n}\n\nsetInterval(beat, data.intervalMs);\n"],"mappings":";;;;;;;;AAoBA,SAAS,YAAY,kBAAkB;AACvC,SAAS,UAAU;AAanB,IAAM,OAAO;AACb,IAAM,OAAO,IAAI,aAAa,KAAK,YAAY;AAC/C,IAAI,YAAY,KAAK;AAErB,IAAM,SAAS;AAAA,EACb,KAAK;AAAA,EACL,2BAA2B,CAAC,OAAO,GAAG,EAAE,WAAW,YAAY,KAAK,WAAW,CAAC,CAAC;AACnF;AAKA,YAAY,GAAG,WAAW,CAAC,QAAgC;AACzD,MAAI,KAAK,UAAW,aAAY,IAAI;AACtC,CAAC;AAED,OAAO,GAAG,cAAc,CAAC,WAAmB;AAC1C,MAAI,WAAW,0BAA0B,WAAW,+BAA+B;AACjF,WAAO,QAAQ;AAAA,EACjB;AACF,CAAC;AAED,SAAS,OAAa;AACpB,MAAI,CAAC,OAAO,UAAW;AACvB,SAAO;AAAA,IACL;AAAA,IACA;AAAA,MACE,WAAW,KAAK;AAAA,MAChB,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA;AAAA;AAAA,MAGlC,QAAQ,mBAAmB,iBAAiB,IAAI,CAAC;AAAA,MACjD,WAAW,KAAK,MAAM,cAAc,IAAI,CAAC;AAAA,IAC3C;AAAA;AAAA;AAAA,IAGA,MAAM;AAAA,IAAC;AAAA,EACT;AACF;AAEA,YAAY,MAAM,KAAK,UAAU;","names":[]}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,11 @@ import { RunnerMode, AgentSessionServiceMethods, AgentMode, PtyChatEventPayload,
|
|
|
3
3
|
export * from '@project/shared';
|
|
4
4
|
import { ChildProcess } from 'node:child_process';
|
|
5
5
|
|
|
6
|
+
/** Append-only: the index into this array is what crosses the SharedArrayBuffer
|
|
7
|
+
* to the heartbeat worker, so reordering would silently remap live statuses. */
|
|
8
|
+
declare const LOOP_STATUS_VALUES: readonly ["active", "idle", "building", "waiting"];
|
|
9
|
+
type LoopStatus = (typeof LOOP_STATUS_VALUES)[number];
|
|
10
|
+
|
|
6
11
|
/**
|
|
7
12
|
* The milestones an agent may declare — narrower than the server's full slug
|
|
8
13
|
* set, which also covers server-generated review verdicts.
|
|
@@ -41,7 +46,7 @@ interface ApiKeyUpdateData {
|
|
|
41
46
|
* Server push asking the builder pod to spawn a same-pod review child: a
|
|
42
47
|
* second conveyor-agent process bound to a fresh review WorkspaceSession
|
|
43
48
|
* (sessionId + sessionJwt). No taskId on the wire — the child inherits the
|
|
44
|
-
* parent's CONVEYOR_TASK_ID and the server derives it from the JWT
|
|
49
|
+
* parent's CONVEYOR_TASK_ID and the server derives it from the JWT.
|
|
45
50
|
*/
|
|
46
51
|
interface SpawnReviewData {
|
|
47
52
|
sessionId: string;
|
|
@@ -56,7 +61,7 @@ interface SpawnReviewData {
|
|
|
56
61
|
* this pod: a second Claude TUI (mode "adhoc") or a raw login shell (mode
|
|
57
62
|
* "shell"), each bound to a fresh WorkspaceSession (sessionId + sessionJwt).
|
|
58
63
|
* No taskId on the wire — the child inherits the parent's CONVEYOR_TASK_ID
|
|
59
|
-
* and the server derives it from the JWT
|
|
64
|
+
* and the server derives it from the JWT.
|
|
60
65
|
*/
|
|
61
66
|
interface SpawnTuiData {
|
|
62
67
|
sessionId: string;
|
|
@@ -124,6 +129,12 @@ declare class AgentConnection {
|
|
|
124
129
|
private static readonly RECONNECT_MAX_DELAY_MS;
|
|
125
130
|
private static readonly RECONNECT_STATUS_EVERY_N;
|
|
126
131
|
private isReconnecting;
|
|
132
|
+
private reconnectingAfterServerDisconnect;
|
|
133
|
+
/** Capped exponential backoff (2s, 4s, 8s, 16s, 32s, then 60s steady) shared
|
|
134
|
+
* by both reconnect loops (connectAgent-RPC and server-disconnect). */
|
|
135
|
+
private static backoffDelayMs;
|
|
136
|
+
/** Sleep `ms`, unref'd so it never holds the process open on its own. */
|
|
137
|
+
private static delay;
|
|
127
138
|
/**
|
|
128
139
|
* Invoked after every successful session reconnect (the `connectAgent` RPC
|
|
129
140
|
* re-established the session room). The runner uses this to force a TUI
|
|
@@ -133,6 +144,16 @@ declare class AgentConnection {
|
|
|
133
144
|
*/
|
|
134
145
|
onReconnected?: () => void;
|
|
135
146
|
private reconnectToSession;
|
|
147
|
+
/**
|
|
148
|
+
* Drive a bounded reconnect after a server-initiated disconnect. Loops until
|
|
149
|
+
* the socket reconnects or is torn down, nudging socket.connect() on each
|
|
150
|
+
* pass with a capped exponential backoff. A token refresh is attempted every
|
|
151
|
+
* pass (rate-limited to once/60s inside refreshTaskTokenFromBootstrap) but
|
|
152
|
+
* its result NEVER gates the reconnect — the socket must recover even when
|
|
153
|
+
* there is no fresh token to apply.
|
|
154
|
+
*/
|
|
155
|
+
private scheduleReconnectAfterServerDisconnect;
|
|
156
|
+
private reconnectAfterServerDisconnect;
|
|
136
157
|
private looksLikeAuthError;
|
|
137
158
|
private startProactiveTokenRefresh;
|
|
138
159
|
private stopProactiveTokenRefresh;
|
|
@@ -149,10 +170,19 @@ declare class AgentConnection {
|
|
|
149
170
|
/**
|
|
150
171
|
* Report that a same-pod review child failed to spawn (fire-and-forget).
|
|
151
172
|
* The server Ends the orphaned review session and falls back to a dedicated
|
|
152
|
-
* review pod. sessionId is OUR (builder) session — the
|
|
173
|
+
* review pod. sessionId is OUR (builder) session — the task-identity guard runs on
|
|
153
174
|
* it; the review session is identified separately.
|
|
154
175
|
*/
|
|
155
176
|
reportReviewSpawnFailure(reviewSessionId: string, error?: string): void;
|
|
177
|
+
/**
|
|
178
|
+
* Ask the server to destroy and recreate this pod (fire-and-forget). The
|
|
179
|
+
* agent calls this only when it has proven it cannot recover in place — the
|
|
180
|
+
* shared `~/.claude` GCS FUSE mount is dead and no in-container action can
|
|
181
|
+
* remount it. The server rate-limits the recycle and posts `reason` to the
|
|
182
|
+
* card; old servers that don't know the method reject harmlessly, leaving
|
|
183
|
+
* today's behavior (a failed turn with a chat warning).
|
|
184
|
+
*/
|
|
185
|
+
requestWorkspaceRecycle(reason: string): void;
|
|
156
186
|
onSpawnTui(callback: (data: SpawnTuiData) => void): void;
|
|
157
187
|
/** Register the on-demand usage-refresh handler; drains an early-buffered
|
|
158
188
|
* `session:probeUsage` that arrived before the runner was ready. */
|
|
@@ -160,7 +190,7 @@ declare class AgentConnection {
|
|
|
160
190
|
/**
|
|
161
191
|
* Report that a same-pod TUI/shell child failed to spawn (fire-and-forget).
|
|
162
192
|
* The server Ends the orphaned session — no fallback pod (unlike review).
|
|
163
|
-
* sessionId is OUR (builder) session — the
|
|
193
|
+
* sessionId is OUR (builder) session — the task-identity guard runs on it.
|
|
164
194
|
*/
|
|
165
195
|
reportSessionSpawnFailure(spawnedSessionId: string, error?: string): void;
|
|
166
196
|
onRunStartCommand(callback: () => void): void;
|
|
@@ -186,6 +216,14 @@ declare class AgentConnection {
|
|
|
186
216
|
* servers that don't know the method reject harmlessly.
|
|
187
217
|
*/
|
|
188
218
|
sendPtyEnded(): void;
|
|
219
|
+
/**
|
|
220
|
+
* Report the port this pod's in-pod PTY stream server bound to, or null when
|
|
221
|
+
* it stopped (fire-and-forget). The server persists it so a viewer can be
|
|
222
|
+
* handed a port-scoped tunnel URL and stream the TUI straight from the pod.
|
|
223
|
+
* Old servers that don't know the method reject harmlessly — the session then
|
|
224
|
+
* just stays on the relay transport.
|
|
225
|
+
*/
|
|
226
|
+
reportPtyStream(port: number | null): void;
|
|
189
227
|
/** Subscribe to relayed keystrokes. Returns an unsubscribe fn. */
|
|
190
228
|
onPtyInput(handler: (data: string) => void): () => void;
|
|
191
229
|
/** Subscribe to relayed (reconciled) terminal resizes. Returns an unsubscribe fn. */
|
|
@@ -200,7 +238,24 @@ declare class AgentConnection {
|
|
|
200
238
|
duplicate: true;
|
|
201
239
|
matchedMessagePreview: string;
|
|
202
240
|
};
|
|
203
|
-
|
|
241
|
+
/**
|
|
242
|
+
* @param loopStatus overrides the status derived from the last emitted
|
|
243
|
+
* runner status. SessionRunner passes it so an idle runner that still has
|
|
244
|
+
* background work outstanding in the pod beats as `waiting` (→ `active` on
|
|
245
|
+
* the wire) rather than `idle`, which would let the workspace activity
|
|
246
|
+
* clock expire mid-gate. See connection/loop-lag.ts `heartbeatStatusFor`.
|
|
247
|
+
*
|
|
248
|
+
* Without an override the status comes from `loopStatusForRunnerStatus`, the
|
|
249
|
+
* same total classifier SessionRunner uses, so both paths agree. This used to
|
|
250
|
+
* be a partial map covering 5 of the 11 `AgentRunnerStatus` values with a
|
|
251
|
+
* `?? "active"` fallback, which meant a parked runner (`waiting_for_input`,
|
|
252
|
+
* `finished`, `error`, `stopping`, `disconnected`) beat as ACTIVE on every
|
|
253
|
+
* no-arg call site — the reconnect paths below, and the shell/project/adhoc
|
|
254
|
+
* runners, which never pass a loop status at all. That renewed the workspace
|
|
255
|
+
* activity clock for an agent doing nothing, so the card stayed "active" and
|
|
256
|
+
* its pod stayed up long past the project's inactivity window.
|
|
257
|
+
*/
|
|
258
|
+
sendHeartbeat(loopLagMs?: number, loopStatus?: LoopStatus): void;
|
|
204
259
|
private heartbeatWorker;
|
|
205
260
|
startHeartbeatWorker(sharedBuffer: SharedArrayBuffer, intervalMs?: number): void;
|
|
206
261
|
stopHeartbeatWorker(): void;
|
|
@@ -217,6 +272,10 @@ declare class AgentConnection {
|
|
|
217
272
|
* Throws on failure so the PortDiscovery poller can retry on its next
|
|
218
273
|
* tick (a swallowed error here would silently drop the delta). */
|
|
219
274
|
reportDiscoveredPorts(ports: WorkspaceDiscoveredPort[]): Promise<void>;
|
|
275
|
+
/** Boot-milestone report over the socket — the codespace-parity fallback
|
|
276
|
+
* for the GKE pod bootstrap-token route. Fire-and-forget: a failed report
|
|
277
|
+
* must never delay or fail the boot path. */
|
|
278
|
+
reportBootMilestone(key: string): void;
|
|
220
279
|
sendTypingStart(): void;
|
|
221
280
|
sendTypingStop(): void;
|
|
222
281
|
emitRateLimitPause(resetsAt: string): void;
|
|
@@ -285,7 +344,14 @@ declare class AgentConnection {
|
|
|
285
344
|
type: string;
|
|
286
345
|
[key: string]: unknown;
|
|
287
346
|
}): void;
|
|
347
|
+
/** Append (or, on `toFront`, prepend for a failed-flush re-queue) events to
|
|
348
|
+
* the buffer, then cap + arm the flush timer. Single owner of the overflow
|
|
349
|
+
* policy so append and re-queue can't diverge on the drop accounting. */
|
|
350
|
+
private enqueueEvents;
|
|
288
351
|
flushEvents(): Promise<void>;
|
|
352
|
+
/** Put a failed flush's events back at the FRONT of the buffer, preserving
|
|
353
|
+
* order, via the shared cap-and-arm path. */
|
|
354
|
+
private requeueFailedEvents;
|
|
289
355
|
}
|
|
290
356
|
|
|
291
357
|
type ModeAction = {
|
|
@@ -375,6 +441,11 @@ interface LifecycleConfig {
|
|
|
375
441
|
* the running subscription key. Deliberately NOT the heartbeat cadence — the
|
|
376
442
|
* endpoint is rate-limited. Set to `0` to disable the timer entirely. */
|
|
377
443
|
usageSampleIntervalMs: number;
|
|
444
|
+
/** Delay before the FIRST usage sample (default: 30s). The sample spawns a
|
|
445
|
+
* full `claude -p "/usage"` subprocess; sampling immediately at startup put
|
|
446
|
+
* that CPU cost inside the boot-critical window on the throttled controller
|
|
447
|
+
* container. Usage freshness 30s later costs nothing. */
|
|
448
|
+
usageSampleInitialDelayMs: number;
|
|
378
449
|
}
|
|
379
450
|
interface LifecycleCallbacks {
|
|
380
451
|
onHeartbeat: () => void;
|
|
@@ -465,6 +536,9 @@ declare class SessionRunner {
|
|
|
465
536
|
private dormantDeadline;
|
|
466
537
|
private taskContext;
|
|
467
538
|
private fullContext;
|
|
539
|
+
/** getTaskContext promise started in connect() so the longest server
|
|
540
|
+
* roundtrip overlaps the room join + port scan; run() awaits it. */
|
|
541
|
+
private contextPrefetch;
|
|
468
542
|
private queryBridge;
|
|
469
543
|
private inputResolver;
|
|
470
544
|
private pendingMessages;
|
|
@@ -478,12 +552,20 @@ declare class SessionRunner {
|
|
|
478
552
|
private readonly portDiscovery;
|
|
479
553
|
/** Main event-loop lag measurement, shared with the heartbeat worker. */
|
|
480
554
|
private readonly loopLag;
|
|
555
|
+
/** Background work (a `run_in_background` gate, a backgrounded subagent) the
|
|
556
|
+
* turn launched that is still running in the pod after the turn ended. While
|
|
557
|
+
* it reports pending, heartbeats go out non-idle so the workspace activity
|
|
558
|
+
* clock keeps being bumped and the reconciler can't sleep the pod mid-gate. */
|
|
559
|
+
private readonly backgroundWork;
|
|
481
560
|
/** Boot supervisor handle, set post-construction once it's known (it is
|
|
482
561
|
* started alongside connect(), before the runner itself exists in some
|
|
483
562
|
* call sites) — notified when the core loop goes live so it can release
|
|
484
563
|
* the app's start command. Optional: never set in tests/paths that don't
|
|
485
564
|
* wire a supervisor. */
|
|
486
565
|
private workspaceCommands;
|
|
566
|
+
/** Guards the one-shot `agent_live` boot-milestone report — fired on the
|
|
567
|
+
* first harness event (createQueryBridge's onEvent hook). */
|
|
568
|
+
private agentLiveReported;
|
|
487
569
|
constructor(config: SessionRunnerConfig, callbacks: SessionRunnerCallbacks, deps?: SessionRunnerDependencies);
|
|
488
570
|
get state(): AgentRunnerStatus;
|
|
489
571
|
get sessionId(): string;
|
|
@@ -498,6 +580,15 @@ declare class SessionRunner {
|
|
|
498
580
|
* command output) before the main agent lifecycle begins.
|
|
499
581
|
*/
|
|
500
582
|
connect(): Promise<boolean>;
|
|
583
|
+
/**
|
|
584
|
+
* The `connectAgent` handshake (joins the session room, drains pending
|
|
585
|
+
* messages), run concurrently with the preview-port baseline scan. Returns
|
|
586
|
+
* the server's pending messages, or `null` when the session is not authorized
|
|
587
|
+
* for the project — a permission denial no retry or token refresh can fix, so
|
|
588
|
+
* the caller parks cleanly rather than crash-looping. Any other error is
|
|
589
|
+
* rethrown (transient — the normal startup-failure path handles it).
|
|
590
|
+
*/
|
|
591
|
+
private connectAgentSession;
|
|
501
592
|
/**
|
|
502
593
|
* Run the main agent lifecycle: fetch context, resolve mode, execute, loop.
|
|
503
594
|
* Requires connect() to have been called first.
|
|
@@ -520,6 +611,21 @@ declare class SessionRunner {
|
|
|
520
611
|
private handleDormantIdle;
|
|
521
612
|
/** Returns true if an initial query was executed, false otherwise. */
|
|
522
613
|
private executeInitialMode;
|
|
614
|
+
/**
|
|
615
|
+
* The mid-turn wedge watchdog aborts a turn that produced no events for 30
|
|
616
|
+
* minutes. Its documented recovery — "the next message respawns with
|
|
617
|
+
* `--resume`" — assumes a human or a follow-up is coming, which is exactly
|
|
618
|
+
* what an autonomous card does NOT have: after the abort the runner goes
|
|
619
|
+
* idle and the card parks until a pack watchdog or a human notices.
|
|
620
|
+
*
|
|
621
|
+
* So for an autonomous card whose INITIAL query was killed as wedged, re-run
|
|
622
|
+
* that initial query once against the (durable, on-disk) resumed session.
|
|
623
|
+
* Bounded to a single retry: a second wedge is a real failure that should
|
|
624
|
+
* surface as a parked card rather than loop the pod. Skipped when a pending
|
|
625
|
+
* message already exists — the core loop delivers that instead, which is the
|
|
626
|
+
* path the watchdog's original recovery assumed.
|
|
627
|
+
*/
|
|
628
|
+
private requeueWedgedInitialQuery;
|
|
523
629
|
/**
|
|
524
630
|
* Startup-queue preamble: the card's initial message reaches a fresh pod
|
|
525
631
|
* twice — inside the task context (chat history) AND as a queued pending
|
|
@@ -593,10 +699,22 @@ declare class SessionRunner {
|
|
|
593
699
|
* `selectBestKey` rotation honest. Best-effort — never throws, no-op when the
|
|
594
700
|
* pod has no OAuth token (e.g. API-key projects). */
|
|
595
701
|
private sampleAndReportKeyUsage;
|
|
702
|
+
/** Wait (bounded) for any in-flight periodic/turn flush to release the shared
|
|
703
|
+
* guard, so a shutdown flush never races it on `.git/index.lock`. */
|
|
704
|
+
private waitForFlushSlot;
|
|
596
705
|
/** Best-effort WIP commit + push on shutdown so in-flight work isn't lost
|
|
597
706
|
* when a claudespace pod is killed. Must be called BEFORE stop() so the
|
|
598
|
-
* connection is still alive for token refresh.
|
|
707
|
+
* connection is still alive for token refresh. Shares the in-flight guard
|
|
708
|
+
* with the periodic/turn flushes (waits one out, then claims the slot) so a
|
|
709
|
+
* SIGTERM mid-flush can't run two concurrent flushes racing the index lock.
|
|
710
|
+
* Never throws. */
|
|
599
711
|
flushGitOnShutdown(): Promise<void>;
|
|
712
|
+
/** Server-pushed `session:stop`: flush WIP BEFORE tearing down. Plain `stop()`
|
|
713
|
+
* sets `stopped=true` and disconnects with no flush, losing everything since
|
|
714
|
+
* the last periodic flush (≤2 min, up to ~20 min behind a gate-skipping
|
|
715
|
+
* backstop). Flush while the connection is still alive (token refresh), then
|
|
716
|
+
* stop. Idempotent + best-effort; `stop()` always runs. */
|
|
717
|
+
private stopWithFlush;
|
|
600
718
|
stop(): void;
|
|
601
719
|
softStop(): void;
|
|
602
720
|
private buildFullContext;
|
|
@@ -606,6 +724,32 @@ declare class SessionRunner {
|
|
|
606
724
|
private refreshGithubToken;
|
|
607
725
|
/** Re-fetch task context to pick up a newly created branch and check it out. */
|
|
608
726
|
private refreshBranchForBuilding;
|
|
727
|
+
/**
|
|
728
|
+
* The loop status the heartbeats should carry right now.
|
|
729
|
+
*
|
|
730
|
+
* `idle` requires BOTH a quiet runner and a quiet pod: an idle runner with a
|
|
731
|
+
* `run_in_background` gate still running reports `waiting`, which reaches the
|
|
732
|
+
* API as `active` and therefore bumps `Workspace.activityExpiresAt` (an idle
|
|
733
|
+
* heartbeat does not — `heartbeatBumps` is `status !== "idle"`). Without this
|
|
734
|
+
* the reconciler sleeps the pod one activity window after the turn ends and
|
|
735
|
+
* kills the gate. Note the runner's own `AgentRunnerStatus` stays `idle`:
|
|
736
|
+
* this is a liveness signal, not a UI one, so cards still read as idle.
|
|
737
|
+
*
|
|
738
|
+
* "Quiet runner" is `loopStatusForRunnerStatus`, NOT `_state === "idle"`.
|
|
739
|
+
* The old `_state !== "idle" → active` test made every parked state report
|
|
740
|
+
* work — above all `waiting_for_input`, a prefilled TUI sitting on a human,
|
|
741
|
+
* which renewed the clock on every beat and kept the card "active" on the
|
|
742
|
+
* board for the runner's whole 30-minute idle timer regardless of the
|
|
743
|
+
* project's inactivity window. See connection/loop-lag.ts for the table and
|
|
744
|
+
* for why an unknown status still fails open to `active`.
|
|
745
|
+
*/
|
|
746
|
+
private resolveLoopStatus;
|
|
747
|
+
/** Mirror the resolved status into the loop-lag buffer so the starvation-proof
|
|
748
|
+
* heartbeat worker reports it too. Returns what it wrote. */
|
|
749
|
+
private refreshLoopStatus;
|
|
750
|
+
/** One `tool_use` from the turn stream — registers a background launch when
|
|
751
|
+
* the tool starts work that outlives the turn. */
|
|
752
|
+
private noteToolUseForBackgroundWork;
|
|
609
753
|
private setState;
|
|
610
754
|
private shutdown;
|
|
611
755
|
private _finalState;
|
|
@@ -672,7 +816,6 @@ declare function detachWorktreeBranch(projectDir: string, branch: string): Promi
|
|
|
672
816
|
declare function removeWorktree(projectDir: string, taskId: string): Promise<void>;
|
|
673
817
|
|
|
674
818
|
interface ConveyorConfig {
|
|
675
|
-
setupCommand?: string;
|
|
676
819
|
startCommand?: string;
|
|
677
820
|
}
|
|
678
821
|
interface ForwardPortsResult {
|
|
@@ -683,7 +826,9 @@ interface ForwardPortsResult {
|
|
|
683
826
|
}>;
|
|
684
827
|
}
|
|
685
828
|
declare function loadForwardPorts(workspaceDir: string): Promise<ForwardPortsResult>;
|
|
686
|
-
/** Load config from env vars (project-level settings injected via bootstrap).
|
|
829
|
+
/** Load config from env vars (project-level settings injected via bootstrap).
|
|
830
|
+
* Note: the project's setupCommand runs at image-bake time (server-side Cloud
|
|
831
|
+
* Build) and is never delivered to pods — only startCommand arrives via env. */
|
|
687
832
|
declare function loadConveyorConfig(): ConveyorConfig | null;
|
|
688
833
|
|
|
689
834
|
declare function runSetupCommand(cmd: string, cwd: string, onOutput: (stream: "stdout" | "stderr", data: string) => void, signal?: AbortSignal): Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -13,12 +13,12 @@ import {
|
|
|
13
13
|
unshallowRepo,
|
|
14
14
|
updateRemoteToken,
|
|
15
15
|
workspacePathExists
|
|
16
|
-
} from "./chunk-
|
|
17
|
-
import "./chunk-
|
|
16
|
+
} from "./chunk-36VMMHYD.js";
|
|
17
|
+
import "./chunk-KCB7CSWJ.js";
|
|
18
|
+
import "./chunk-5OQQSDVT.js";
|
|
18
19
|
import {
|
|
19
20
|
getWorkbenchClient
|
|
20
|
-
} from "./chunk-
|
|
21
|
-
import "./chunk-UZTJJD7Y.js";
|
|
21
|
+
} from "./chunk-QOJTJCYZ.js";
|
|
22
22
|
import {
|
|
23
23
|
workbenchEnabled
|
|
24
24
|
} from "./chunk-4VUQ2NPF.js";
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
runAuthTokenCommand,
|
|
27
27
|
runSetupCommand,
|
|
28
28
|
runStartCommand
|
|
29
|
-
} from "./chunk-
|
|
29
|
+
} from "./chunk-XR6H326I.js";
|
|
30
30
|
import "./chunk-JIGG755T.js";
|
|
31
31
|
|
|
32
32
|
// src/runner/worktree.ts
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import {
|
|
2
|
+
biggestForeignProcessGroup,
|
|
3
|
+
oomWatchdogOptionsFromEnv,
|
|
4
|
+
startOomWatchdog
|
|
5
|
+
} from "./chunk-ULS4QPRE.js";
|
|
6
|
+
export {
|
|
7
|
+
biggestForeignProcessGroup,
|
|
8
|
+
oomWatchdogOptionsFromEnv,
|
|
9
|
+
startOomWatchdog
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=oom-watchdog-U7JERHA2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rallycry/conveyor-agent",
|
|
3
|
-
"version": "10.13.
|
|
3
|
+
"version": "10.13.50",
|
|
4
4
|
"description": "Conveyor Agent Runner v10 - PTY harness for the task chat (SDK harness for audit/project-chat). Agent-as-User architecture with BaseService patterns. Works locally too.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"prepublishOnly": "tsup",
|
|
28
28
|
"build": "tsup",
|
|
29
29
|
"build:js": "tsup --no-dts --no-clean",
|
|
30
|
-
"dev": "tsup --watch --no-dts",
|
|
30
|
+
"dev": "tsup --watch --no-dts --no-clean",
|
|
31
31
|
"test": "vitest run --passWithNoTests",
|
|
32
32
|
"test:unit": "vitest run --passWithNoTests",
|
|
33
33
|
"test:watch": "vitest",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"typecheck": "tsgo --noEmit"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@anthropic-ai/claude-agent-sdk": "^0.3.
|
|
39
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.219",
|
|
40
40
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
41
41
|
"node-pty": "^1.0.0",
|
|
42
42
|
"socket.io-client": "^4.8.3",
|
package/runtime/entrypoint.sh
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
2
|
# vnext shim — ALL boot logic lives in `conveyor-agent boot`
|
|
3
|
-
# (packages/conveyor-agent/src/boot/). Keep this file dumb: env floor,
|
|
4
|
-
#
|
|
5
|
-
#
|
|
3
|
+
# (packages/conveyor-agent/src/boot/). Keep this file dumb: env floor, exec.
|
|
4
|
+
# The npm self-update preflight is no longer a separate blocking process here —
|
|
5
|
+
# `conveyor-agent boot` runs it concurrently with the boot pipeline and gates
|
|
6
|
+
# only the runner spawn / workbench daemon on the result (self-update.ts), so
|
|
7
|
+
# each container saves a full node+npm process launch on the boot critical path.
|
|
6
8
|
set -eu
|
|
7
9
|
export HOME=/home/conveyor
|
|
8
10
|
export PATH="/home/conveyor/.bun/bin:${PATH}"
|
|
9
11
|
export NO_UPDATE_NOTIFIER=1
|
|
10
|
-
conveyor-agent boot --preflight-update || echo "[shim] preflight failed; booting baked agent"
|
|
11
12
|
exec conveyor-agent boot
|