@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.
Files changed (39) hide show
  1. package/dist/{boot-YZCOIV3Q.js → boot-I353EAK6.js} +156 -125
  2. package/dist/boot-I353EAK6.js.map +1 -0
  3. package/dist/{chunk-3GDXX3OX.js → chunk-36VMMHYD.js} +5578 -2111
  4. package/dist/chunk-36VMMHYD.js.map +1 -0
  5. package/dist/{chunk-AGGVNADW.js → chunk-3Z4YQNJV.js} +59 -13
  6. package/dist/chunk-3Z4YQNJV.js.map +1 -0
  7. package/dist/chunk-5OQQSDVT.js +142 -0
  8. package/dist/chunk-5OQQSDVT.js.map +1 -0
  9. package/dist/{chunk-UZTJJD7Y.js → chunk-KCB7CSWJ.js} +43 -2
  10. package/dist/chunk-KCB7CSWJ.js.map +1 -0
  11. package/dist/{chunk-UDSRAHKP.js → chunk-QOJTJCYZ.js} +67 -19
  12. package/dist/chunk-QOJTJCYZ.js.map +1 -0
  13. package/dist/chunk-ULS4QPRE.js +138 -0
  14. package/dist/chunk-ULS4QPRE.js.map +1 -0
  15. package/dist/{chunk-72AEN6LB.js → chunk-XR6H326I.js} +26 -2
  16. package/dist/chunk-XR6H326I.js.map +1 -0
  17. package/dist/cli.js +196 -334
  18. package/dist/cli.js.map +1 -1
  19. package/dist/{client-BU4XA7CV.js → client-LRVVHTNG.js} +2 -2
  20. package/dist/heartbeat-worker.js +10 -12
  21. package/dist/heartbeat-worker.js.map +1 -1
  22. package/dist/index.d.ts +153 -8
  23. package/dist/index.js +5 -5
  24. package/dist/oom-watchdog-U7JERHA2.js +11 -0
  25. package/dist/server-NIOBJ46G.js +10 -0
  26. package/dist/server-NIOBJ46G.js.map +1 -0
  27. package/package.json +3 -3
  28. package/runtime/entrypoint.sh +5 -4
  29. package/dist/boot-YZCOIV3Q.js.map +0 -1
  30. package/dist/chunk-3GDXX3OX.js.map +0 -1
  31. package/dist/chunk-72AEN6LB.js.map +0 -1
  32. package/dist/chunk-7TQO4ZF4.js +0 -60
  33. package/dist/chunk-7TQO4ZF4.js.map +0 -1
  34. package/dist/chunk-AGGVNADW.js.map +0 -1
  35. package/dist/chunk-UDSRAHKP.js.map +0 -1
  36. package/dist/chunk-UZTJJD7Y.js.map +0 -1
  37. package/dist/server-BXAFMYDM.js +0 -9
  38. /package/dist/{client-BU4XA7CV.js.map → client-LRVVHTNG.js.map} +0 -0
  39. /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-UDSRAHKP.js";
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-BU4XA7CV.js.map
17
+ //# sourceMappingURL=client-LRVVHTNG.js.map
@@ -1,7 +1,9 @@
1
1
  import {
2
+ buildConveyorSocketOptions,
3
+ heartbeatStatusFor,
2
4
  lagFromBuffer,
3
5
  statusFromBuffer
4
- } from "./chunk-7TQO4ZF4.js";
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(data.apiUrl, {
13
- auth: (cb) => cb({ taskToken, runnerMode: data.runnerMode }),
14
- transports: ["websocket"],
15
- reconnection: true,
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
- status: statusFromBuffer(view),
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, {\n auth: (cb) => cb({ taskToken, runnerMode: data.runnerMode }),\n transports: [\"websocket\"],\n reconnection: true,\n reconnectionAttempts: Infinity,\n reconnectionDelay: 2000,\n reconnectionDelayMax: 30000,\n randomizationFactor: 0.3,\n extraHeaders: { \"ngrok-skip-browser-warning\": \"true\" },\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 status: 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;AAYnB,IAAM,OAAO;AACb,IAAM,OAAO,IAAI,aAAa,KAAK,YAAY;AAC/C,IAAI,YAAY,KAAK;AAErB,IAAM,SAAS,GAAG,KAAK,QAAQ;AAAA,EAC7B,MAAM,CAAC,OAAO,GAAG,EAAE,WAAW,YAAY,KAAK,WAAW,CAAC;AAAA,EAC3D,YAAY,CAAC,WAAW;AAAA,EACxB,cAAc;AAAA,EACd,sBAAsB;AAAA,EACtB,mBAAmB;AAAA,EACnB,sBAAsB;AAAA,EACtB,qBAAqB;AAAA,EACrB,cAAc,EAAE,8BAA8B,OAAO;AACvD,CAAC;AAKD,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,MAClC,QAAQ,iBAAiB,IAAI;AAAA,MAC7B,WAAW,KAAK,MAAM,cAAc,IAAI,CAAC;AAAA,IAC3C;AAAA;AAAA;AAAA,IAGA,MAAM;AAAA,IAAC;AAAA,EACT;AACF;AAEA,YAAY,MAAM,KAAK,UAAU;","names":[]}
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 (SEC-9).
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 (SEC-9).
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 SEC-9 guard runs on
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 SEC-9 guard runs on it.
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
- sendHeartbeat(loopLagMs?: number): void;
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. Never throws. */
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-3GDXX3OX.js";
17
- import "./chunk-7TQO4ZF4.js";
16
+ } from "./chunk-36VMMHYD.js";
17
+ import "./chunk-KCB7CSWJ.js";
18
+ import "./chunk-5OQQSDVT.js";
18
19
  import {
19
20
  getWorkbenchClient
20
- } from "./chunk-UDSRAHKP.js";
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-72AEN6LB.js";
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,10 @@
1
+ import {
2
+ startWorkbenchServer
3
+ } from "./chunk-3Z4YQNJV.js";
4
+ import "./chunk-XR6H326I.js";
5
+ import "./chunk-JIGG755T.js";
6
+ import "./chunk-ULS4QPRE.js";
7
+ export {
8
+ startWorkbenchServer
9
+ };
10
+ //# sourceMappingURL=server-NIOBJ46G.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.18",
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.197",
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",
@@ -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
- # best-effort self-update preflight, exec. The preflight runs baked code and
5
- # only installs; the exec picks up whichever global bin is now current.
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