@rallycry/conveyor-agent 8.4.1 → 8.5.1

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.
@@ -5,13 +5,14 @@ import {
5
5
  textResult
6
6
  } from "./chunk-FDWECEDJ.js";
7
7
  import {
8
+ configHomePlansDir,
8
9
  createHarness,
9
10
  createServiceLogger,
10
11
  defineTool,
11
12
  ensureClaudeCredentials,
12
13
  removeConveyorCredentials,
13
14
  sessionTranscriptPath
14
- } from "./chunk-VDH55LTT.js";
15
+ } from "./chunk-6B545CHM.js";
15
16
 
16
17
  // src/setup/bootstrap.ts
17
18
  var BOOTSTRAP_TIMEOUT_MS = 3e4;
@@ -133,11 +134,13 @@ function applyBootstrapToEnv(config) {
133
134
  import { io } from "socket.io-client";
134
135
  var EVENT_BATCH_MS = 500;
135
136
  var MAX_EVENT_BUFFER = 5e3;
137
+ var TOKEN_REFRESH_INTERVAL_MS = 6 * 60 * 60 * 1e3;
136
138
  var AgentConnection = class _AgentConnection {
137
139
  socket = null;
138
140
  config;
139
141
  eventBuffer = [];
140
142
  flushTimer = null;
143
+ tokenRefreshTimer = null;
141
144
  lastEmittedStatus = null;
142
145
  lastReportedStatus = null;
143
146
  droppedEventCount = 0;
@@ -201,6 +204,7 @@ var AgentConnection = class _AgentConnection {
201
204
  if (!this.config.apiUrl) {
202
205
  return Promise.reject(new Error("Cannot connect: apiUrl is empty"));
203
206
  }
207
+ this.startProactiveTokenRefresh();
204
208
  return new Promise((resolve2, reject) => {
205
209
  let settled = false;
206
210
  let attempts = 0;
@@ -227,7 +231,8 @@ var AgentConnection = class _AgentConnection {
227
231
  content: msg.content,
228
232
  userId: msg.userId,
229
233
  ...msg.source && { source: msg.source },
230
- ...msg.files && { files: msg.files }
234
+ ...msg.files && { files: msg.files },
235
+ ...msg.delivery === "prefill" && { delivery: msg.delivery }
231
236
  };
232
237
  if (this.messageCallback) this.messageCallback(incoming);
233
238
  else this.earlyMessages.push(incoming);
@@ -314,6 +319,7 @@ var AgentConnection = class _AgentConnection {
314
319
  });
315
320
  }
316
321
  disconnect() {
322
+ this.stopProactiveTokenRefresh();
317
323
  void this.flushEvents();
318
324
  if (this.socket) {
319
325
  this.socket.io.reconnection(false);
@@ -397,7 +403,32 @@ var AgentConnection = class _AgentConnection {
397
403
  }
398
404
  }
399
405
  looksLikeAuthError(message) {
400
- return /unauthor|forbid|auth|token/i.test(message);
406
+ return /unauthor|forbid|auth|token|session (?:not found|expired|invalid)|invalid session/i.test(
407
+ message
408
+ );
409
+ }
410
+ // ── Proactive task-token refresh ────────────────────────────────────────
411
+ //
412
+ // Socket.IO only re-presents the taskToken on a (re)connect handshake, and
413
+ // the server only re-validates the JWT then. So a token that expires while
414
+ // the socket stays connected goes unnoticed until the next RPC fails. Re-mint
415
+ // periodically from the bootstrap endpoint — refreshFromBootstrap() updates
416
+ // both this.config.taskToken and socket.auth.taskToken, so any later
417
+ // reconnect carries a fresh token. No-ops for project mode / missing
418
+ // codespace env, and is rate-limited to once/60s inside refreshFromBootstrap.
419
+ startProactiveTokenRefresh() {
420
+ if (this.tokenRefreshTimer) return;
421
+ this.tokenRefreshTimer = setInterval(() => {
422
+ void this.refreshTaskTokenFromBootstrap().catch(() => {
423
+ });
424
+ }, TOKEN_REFRESH_INTERVAL_MS);
425
+ this.tokenRefreshTimer.unref?.();
426
+ }
427
+ stopProactiveTokenRefresh() {
428
+ if (this.tokenRefreshTimer) {
429
+ clearInterval(this.tokenRefreshTimer);
430
+ this.tokenRefreshTimer = null;
431
+ }
401
432
  }
402
433
  drainPendingMessages(messages) {
403
434
  for (const msg of messages) {
@@ -1316,7 +1347,7 @@ var PlanSync = class {
1316
1347
  this.workspaceDir = workspaceDir;
1317
1348
  }
1318
1349
  getPlanDirs() {
1319
- return [join(this.workspaceDir, ".claude", "plans")];
1350
+ return [join(this.workspaceDir, ".claude", "plans"), configHomePlansDir()];
1320
1351
  }
1321
1352
  snapshotPlanFiles() {
1322
1353
  this.planFileSnapshot.clear();
@@ -1934,6 +1965,11 @@ var StopProjectBuildRequestSchema = z2.object({
1934
1965
  taskId: z2.string(),
1935
1966
  requestingUserId: z2.string().optional()
1936
1967
  });
1968
+ var CreateProjectReleaseRequestSchema = z2.object({
1969
+ projectId: z2.string(),
1970
+ taskIds: z2.array(z2.string()).optional(),
1971
+ requestingUserId: z2.string().optional()
1972
+ });
1937
1973
  var ApproveProjectMergePRRequestSchema = z2.object({
1938
1974
  projectId: z2.string(),
1939
1975
  childTaskId: z2.string(),
@@ -2009,6 +2045,22 @@ var GetProjectAttachmentRequestSchema = z2.object({
2009
2045
  /** Max bytes of text content to return from `offset`. Server default applies. */
2010
2046
  maxBytes: z2.number().int().positive().optional()
2011
2047
  });
2048
+ var RequestProjectFileUploadRequestSchema = z2.object({
2049
+ projectId: z2.string(),
2050
+ taskId: z2.string(),
2051
+ fileName: z2.string().min(1).max(255),
2052
+ mimeType: z2.string().min(1).max(128),
2053
+ fileSize: z2.number().int().positive().max(MAX_FILE_SIZE_BYTES),
2054
+ requestingUserId: z2.string().optional()
2055
+ });
2056
+ var ConfirmProjectFileUploadRequestSchema = z2.object({
2057
+ projectId: z2.string(),
2058
+ taskId: z2.string(),
2059
+ fileId: z2.string(),
2060
+ /** When set, the attachment is also posted to the task chat with this text. */
2061
+ comment: z2.string().max(2e3).optional(),
2062
+ requestingUserId: z2.string().optional()
2063
+ });
2012
2064
  var CreateProjectPullRequestRequestSchema = z2.object({
2013
2065
  projectId: z2.string(),
2014
2066
  taskId: z2.string(),
@@ -2033,6 +2085,16 @@ var RemoveProjectTaskReviewerRequestSchema = z2.object({
2033
2085
  userId: z2.string(),
2034
2086
  requestingUserId: z2.string().optional()
2035
2087
  });
2088
+ var ListProjectManualTestsRequestSchema = z2.object({
2089
+ projectId: z2.string(),
2090
+ taskId: z2.string()
2091
+ });
2092
+ var SetProjectManualTestsRequestSchema = z2.object({
2093
+ projectId: z2.string(),
2094
+ taskId: z2.string(),
2095
+ items: z2.array(z2.object({ title: z2.string().min(1) })).min(1),
2096
+ requestingUserId: z2.string().optional()
2097
+ });
2036
2098
  var StartTaskAuditRequestSchema = z3.object({
2037
2099
  projectId: z3.string(),
2038
2100
  taskIds: z3.array(z3.string()).min(1)
@@ -6259,7 +6321,11 @@ async function handleExitPlanMode(host, input) {
6259
6321
  await host.connection.postChatMessageAwait(
6260
6322
  "Planning complete \u2014 awaiting team approval. Icon and agent assignment will be set automatically."
6261
6323
  );
6262
- host.requestStop();
6324
+ if (host.harnessKind === "pty") {
6325
+ setTimeout(() => host.requestStop(), 250);
6326
+ } else {
6327
+ host.requestStop();
6328
+ }
6263
6329
  return { behavior: "allow", updatedInput: input };
6264
6330
  }
6265
6331
  try {
@@ -6533,9 +6599,10 @@ function repairTornSessionFile(path2) {
6533
6599
  }
6534
6600
  function resolvePromptDelivery(inputs) {
6535
6601
  if (inputs.harnessKind !== "pty") return "submit";
6536
- if ((inputs.runnerMode ?? "task") !== "task") return "submit";
6602
+ if (inputs.runnerMode === "code-review") return "submit";
6537
6603
  if (inputs.isFollowUp || inputs.hasExistingSession) return "submit";
6538
6604
  if (inputs.isAuto || inputs.agentMode === "auto") return "submit";
6605
+ if (inputs.agentMode !== "discovery" && inputs.agentMode !== "help") return "submit";
6539
6606
  return "prefill";
6540
6607
  }
6541
6608
  function isReadOnlyMode(mode, hasExitedPlanMode) {
@@ -6572,6 +6639,14 @@ function buildQueryOptions(host, context) {
6572
6639
  permissionMode: needsCanUseTool ? "plan" : "bypassPermissions",
6573
6640
  allowDangerouslySkipPermissions: !needsCanUseTool,
6574
6641
  canUseTool: buildCanUseTool(host),
6642
+ // The spawned CLI never sees `systemPrompt` (an SDK-only option) — deliver
6643
+ // the same text via `--append-system-prompt`. PTY-only: the SDK harness
6644
+ // already receives it through systemPrompt.append.
6645
+ ...host.harnessKind === "pty" && systemPromptText ? { appendSystemPrompt: systemPromptText } : {},
6646
+ // Auto mode pre-exit: after the ExitPlanMode hook allows the call, the
6647
+ // CLI's plan dialog still renders — press Enter so the autonomous agent
6648
+ // continues building in the same session (Conveyor validated in the hook).
6649
+ planDialogAutoAccept: mode === "auto" && !host.hasExitedPlanMode,
6575
6650
  tools: { type: "preset", preset: "claude_code" },
6576
6651
  mcpServers: {
6577
6652
  conveyor: createConveyorMcpServer(host.harness, host.connection, host.config, context, mode)
@@ -6712,13 +6787,7 @@ async function runSdkQuery(host, context, followUpContent, promptDeliveryOverrid
6712
6787
  };
6713
6788
  const resume = hasExistingSession ? sessionUuid : void 0;
6714
6789
  if (followUpContent) {
6715
- const prompt = await buildFollowUpPrompt(host, context, followUpContent);
6716
- const agentQuery = host.harness.executeQuery({
6717
- prompt: typeof prompt === "string" ? prompt : host.createInputStream(prompt),
6718
- options: { ...options },
6719
- resume
6720
- });
6721
- await trackAndRun(host, context, options, agentQuery);
6790
+ await runFollowUpQuery(host, context, options, resume, followUpContent);
6722
6791
  } else if (isDiscoveryLike && promptDelivery !== "prefill") {
6723
6792
  return;
6724
6793
  } else {
@@ -6728,6 +6797,42 @@ async function runSdkQuery(host, context, followUpContent, promptDeliveryOverrid
6728
6797
  await host.syncPlanFile();
6729
6798
  }
6730
6799
  }
6800
+ async function runFollowUpQuery(host, context, options, resume, followUpContent) {
6801
+ if (options.promptDelivery === "prefill") {
6802
+ await runPrefilledFollowUp(host, context, options, resume, followUpContent);
6803
+ return;
6804
+ }
6805
+ const prompt = await buildFollowUpPrompt(host, context, followUpContent);
6806
+ const agentQuery = host.harness.executeQuery({
6807
+ prompt: typeof prompt === "string" ? prompt : host.createInputStream(prompt),
6808
+ options: { ...options },
6809
+ resume
6810
+ });
6811
+ await trackAndRun(host, context, options, agentQuery);
6812
+ }
6813
+ async function runPrefilledFollowUp(host, context, options, resume, followUpContent) {
6814
+ const followUpText = typeof followUpContent === "string" ? followUpContent : followUpContent.filter((b) => b.type === "text").map((b) => b.text).join("\n");
6815
+ const queryOptions = { ...options };
6816
+ if (host.config.mode === "pm" && !resume) {
6817
+ const initialPrompt = await buildInitialPrompt(
6818
+ host.config.mode,
6819
+ context,
6820
+ host.isAuto,
6821
+ host.agentMode
6822
+ );
6823
+ queryOptions.appendSystemPrompt = [queryOptions.appendSystemPrompt, initialPrompt].filter(Boolean).join("\n\n").slice(0, APPEND_SYSTEM_PROMPT_MAX_CHARS);
6824
+ }
6825
+ let agentQuery = host.harness.executeQuery({
6826
+ prompt: followUpText,
6827
+ options: queryOptions,
6828
+ resume
6829
+ });
6830
+ agentQuery = notifyOnFirstEvent(agentQuery, async () => {
6831
+ host.connection.emitStatus("running");
6832
+ await host.callbacks.onStatusChange("running");
6833
+ });
6834
+ await trackAndRun(host, context, queryOptions, agentQuery);
6835
+ }
6731
6836
  async function trackAndRun(host, context, options, agentQuery) {
6732
6837
  if (host.harnessKind === "pty" && options.promptDelivery !== "prefill") {
6733
6838
  agentQuery = watchForParkedTui(agentQuery, host);
@@ -6739,6 +6844,14 @@ async function trackAndRun(host, context, options, agentQuery) {
6739
6844
  host.activeQuery = null;
6740
6845
  }
6741
6846
  }
6847
+ var APPEND_SYSTEM_PROMPT_MAX_CHARS = 96e3;
6848
+ function latestUserMessageText(context) {
6849
+ for (let i = context.chatHistory.length - 1; i >= 0; i--) {
6850
+ const msg = context.chatHistory[i];
6851
+ if (msg.role === "user" && msg.content.trim()) return msg.content.trim();
6852
+ }
6853
+ return null;
6854
+ }
6742
6855
  async function runInitialQuery(host, context, options, resume, promptDelivery) {
6743
6856
  const initialPrompt = await buildInitialPrompt(
6744
6857
  host.config.mode,
@@ -6746,10 +6859,18 @@ async function runInitialQuery(host, context, options, resume, promptDelivery) {
6746
6859
  host.isAuto,
6747
6860
  host.agentMode
6748
6861
  );
6749
- const prompt = buildMultimodalPrompt(initialPrompt, context);
6862
+ let prompt;
6863
+ const queryOptions = { ...options };
6864
+ const prefillMessage = promptDelivery === "prefill" ? latestUserMessageText(context) : null;
6865
+ if (prefillMessage) {
6866
+ prompt = prefillMessage;
6867
+ queryOptions.appendSystemPrompt = [queryOptions.appendSystemPrompt, initialPrompt].filter(Boolean).join("\n\n").slice(0, APPEND_SYSTEM_PROMPT_MAX_CHARS);
6868
+ } else {
6869
+ prompt = buildMultimodalPrompt(initialPrompt, context);
6870
+ }
6750
6871
  let agentQuery = host.harness.executeQuery({
6751
6872
  prompt: host.createInputStream(prompt),
6752
- options: { ...options },
6873
+ options: queryOptions,
6753
6874
  resume
6754
6875
  });
6755
6876
  if (promptDelivery === "prefill") {
@@ -6758,7 +6879,7 @@ async function runInitialQuery(host, context, options, resume, promptDelivery) {
6758
6879
  await host.callbacks.onStatusChange("running");
6759
6880
  });
6760
6881
  }
6761
- await trackAndRun(host, context, options, agentQuery);
6882
+ await trackAndRun(host, context, queryOptions, agentQuery);
6762
6883
  }
6763
6884
  async function buildRetryQuery(host, context, options, lastErrorWasImage) {
6764
6885
  if (lastErrorWasImage) {
@@ -7111,6 +7232,7 @@ var QueryBridge = class {
7111
7232
  runnerConfig;
7112
7233
  callbacks;
7113
7234
  harness;
7235
+ /** Which harness drives this bridge ("pty" task chat vs "sdk" rollback). */
7114
7236
  harnessKind;
7115
7237
  costTracker;
7116
7238
  planSync;
@@ -7516,10 +7638,13 @@ var SessionRunner = class _SessionRunner {
7516
7638
  this.queryBridge = this.createQueryBridge();
7517
7639
  await this.seedCostTrackerFromServer();
7518
7640
  this.logInitialization();
7519
- const staleMessageCount = this.pendingMessages.length;
7641
+ const staleBatch = [...this.pendingMessages];
7520
7642
  const didExecuteInitialQuery = await this.executeInitialMode();
7521
- if (staleMessageCount > 0 && didExecuteInitialQuery) {
7522
- this.pendingMessages.splice(0, staleMessageCount);
7643
+ if (staleBatch.length > 0 && didExecuteInitialQuery) {
7644
+ for (const stale of staleBatch) {
7645
+ const idx = this.pendingMessages.indexOf(stale);
7646
+ if (idx !== -1) this.pendingMessages.splice(idx, 1);
7647
+ }
7523
7648
  }
7524
7649
  if (this.queryBridge?.isDiscoveryCompleted) {
7525
7650
  process.stderr.write(
@@ -7567,14 +7692,18 @@ var SessionRunner = class _SessionRunner {
7567
7692
  }
7568
7693
  break;
7569
7694
  }
7570
- await this.setState("running");
7571
7695
  this.interrupted = false;
7572
7696
  await this.callbacks.onEvent({
7573
7697
  type: "user_message",
7574
7698
  content: msg.content,
7575
7699
  userId: msg.userId
7576
7700
  });
7577
- await this.executeQuery(msg.content);
7701
+ if (this.prefillEligible(msg)) {
7702
+ await this.runPrefilledMessage(msg, this.mode.effectiveMode);
7703
+ } else {
7704
+ await this.setState("running");
7705
+ await this.executeQuery(msg.content);
7706
+ }
7578
7707
  if (this.queryBridge?.isDiscoveryCompleted) {
7579
7708
  process.stderr.write(
7580
7709
  "[conveyor-agent] Discovery completed \u2014 entering dormant idle (staying connected)\n"
@@ -7647,7 +7776,13 @@ var SessionRunner = class _SessionRunner {
7647
7776
  async executeInitialMode() {
7648
7777
  if (!this.taskContext || !this.fullContext) return false;
7649
7778
  const effectiveMode = this.mode.effectiveMode;
7650
- const delivery = this.pendingMessages.length > 0 ? "submit" : this.queryBridge?.initialPromptDelivery(this.fullContext) ?? "submit";
7779
+ const intrinsicDelivery = this.queryBridge?.initialPromptDelivery(this.fullContext) ?? "submit";
7780
+ const hinted = this.prepareInitialPendingMessages(intrinsicDelivery);
7781
+ if (hinted) {
7782
+ await this.runPrefilledMessage(hinted, effectiveMode);
7783
+ return true;
7784
+ }
7785
+ const delivery = this.pendingMessages.length > 0 ? "submit" : intrinsicDelivery;
7651
7786
  const shouldRun = effectiveMode === "building" || effectiveMode === "auto" || effectiveMode === "review" || delivery === "prefill";
7652
7787
  if (!shouldRun) {
7653
7788
  await this.setState("idle");
@@ -7674,6 +7809,88 @@ var SessionRunner = class _SessionRunner {
7674
7809
  if (!this.stopped) await this.setState("idle");
7675
7810
  return true;
7676
7811
  }
7812
+ // ── Prefill-delivery helpers ───────────────────────────────────────
7813
+ /**
7814
+ * Startup-queue preamble: the card's initial message reaches a fresh pod
7815
+ * twice — inside the task context (chat history) AND as a queued pending
7816
+ * message. When the intrinsic delivery is prefill (manual-mode fresh TUI)
7817
+ * or a hinted message is queued (Refine), drop those context-duplicates so
7818
+ * they don't force submit semantics — a genuinely-new live message still
7819
+ * does. Returns the lone hinted message to park, if any.
7820
+ */
7821
+ prepareInitialPendingMessages(intrinsicDelivery) {
7822
+ const hasHintedPending = this.pendingMessages.some((m) => m.delivery === "prefill");
7823
+ if (this.pendingMessages.length > 0 && (intrinsicDelivery === "prefill" || hasHintedPending)) {
7824
+ this.foldContextDuplicatePendingMessages();
7825
+ }
7826
+ return this.takePrefillHintedMessage();
7827
+ }
7828
+ /**
7829
+ * Drop pending messages whose content is already present as a user message
7830
+ * in the task-context chat history (the card's initial message is delivered
7831
+ * both ways). Hinted (prefill) messages are kept — they are consumed by the
7832
+ * dedicated prefill path.
7833
+ */
7834
+ foldContextDuplicatePendingMessages() {
7835
+ const userContents = new Set(
7836
+ (this.fullContext?.chatHistory ?? []).filter((m) => m.role === "user" && m.content.trim()).map((m) => m.content.trim())
7837
+ );
7838
+ const kept = this.pendingMessages.filter(
7839
+ (m) => m.delivery === "prefill" || !m.content.trim() || !userContents.has(m.content.trim())
7840
+ );
7841
+ const dropped = this.pendingMessages.length - kept.length;
7842
+ if (dropped > 0) {
7843
+ this.pendingMessages.length = 0;
7844
+ this.pendingMessages.push(...kept);
7845
+ process.stderr.write(
7846
+ `[conveyor-agent] Folded ${dropped} pending message(s) already present in chat context
7847
+ `
7848
+ );
7849
+ }
7850
+ }
7851
+ /**
7852
+ * Honor a prefill hint only when it is unambiguous: PTY harness, a manual
7853
+ * (human-facing) agent mode, no other message queued behind it, and not an
7854
+ * automated-critical source. Everything else keeps submit semantics.
7855
+ */
7856
+ prefillEligible(msg) {
7857
+ if (msg.delivery !== "prefill") return false;
7858
+ if (this.pendingMessages.length > 0) return false;
7859
+ if (this.queryBridge?.harnessKind !== "pty") return false;
7860
+ if (msg.source && msg.source !== "mode_change" && msg.source !== "user") return false;
7861
+ const m = this.mode.effectiveMode;
7862
+ return m === "discovery" || m === "review" || m === "help";
7863
+ }
7864
+ /** Consume the sole pending message when it should park as a prefill. */
7865
+ takePrefillHintedMessage() {
7866
+ if (this.pendingMessages.length !== 1) return null;
7867
+ const [first] = this.pendingMessages;
7868
+ if (!first || first.delivery !== "prefill") return null;
7869
+ this.pendingMessages.length = 0;
7870
+ if (this.prefillEligible(first)) return first;
7871
+ this.pendingMessages.push(first);
7872
+ return null;
7873
+ }
7874
+ /**
7875
+ * Park a message in the Connected-TUI input (prefill) and wait for the human
7876
+ * to submit. Empty content prefills the mode's initial prompt instead (the
7877
+ * latest user chat message — e.g. a Refine wake on a dormant agent). The
7878
+ * caller restores the idle state afterwards.
7879
+ */
7880
+ async runPrefilledMessage(msg, effectiveMode) {
7881
+ await this.setState("waiting_for_input");
7882
+ await this.callbacks.onEvent({
7883
+ type: "execute_mode",
7884
+ mode: effectiveMode,
7885
+ delivery: "prefill"
7886
+ });
7887
+ this.lifecycle.startIdleTimer();
7888
+ try {
7889
+ await this.executeQuery(msg.content.trim() ? msg.content : void 0, "prefill");
7890
+ } finally {
7891
+ this.lifecycle.cancelIdleTimer();
7892
+ }
7893
+ }
7677
7894
  // ── Message waiting ────────────────────────────────────────────────
7678
7895
  waitForMessage() {
7679
7896
  if (this.pendingMessages.length > 0) {
@@ -8043,7 +8260,10 @@ var SessionRunner = class _SessionRunner {
8043
8260
  `);
8044
8261
  this.connection.sendEvent({ type: "shutdown", reason: finalState });
8045
8262
  this.lifecycle.destroy();
8046
- await this.setState(finalState);
8263
+ try {
8264
+ await this.setState(finalState);
8265
+ } catch {
8266
+ }
8047
8267
  this.connection.disconnect();
8048
8268
  this._finalState = finalState;
8049
8269
  }
@@ -8102,20 +8322,31 @@ var ProjectConnection = class {
8102
8322
  return this.socket?.connected ?? false;
8103
8323
  }
8104
8324
  // ── Typed service method call ──────────────────────────────────────────
8105
- call(method, payload) {
8325
+ call(method, payload, opts) {
8106
8326
  const socket = this.requireSocket();
8107
8327
  return new Promise((resolve2, reject) => {
8108
- socket.emit(
8109
- `agentSessionService:${String(method)}`,
8110
- payload,
8111
- (response) => {
8112
- if (response.success && response.data !== void 0) {
8113
- resolve2(response.data);
8114
- } else {
8115
- reject(new Error(response.error ?? `Service call failed: ${String(method)}`));
8116
- }
8328
+ const handleResponse = (response) => {
8329
+ if (response.success && response.data !== void 0) {
8330
+ resolve2(response.data);
8331
+ } else {
8332
+ reject(new Error(response.error ?? `Service call failed: ${String(method)}`));
8117
8333
  }
8118
- );
8334
+ };
8335
+ if (opts?.timeoutMs) {
8336
+ socket.timeout(opts.timeoutMs).emit(
8337
+ `agentSessionService:${String(method)}`,
8338
+ payload,
8339
+ (err, response) => {
8340
+ if (err) {
8341
+ reject(new Error(`Service call timed out: ${String(method)}`));
8342
+ } else {
8343
+ handleResponse(response);
8344
+ }
8345
+ }
8346
+ );
8347
+ return;
8348
+ }
8349
+ socket.emit(`agentSessionService:${String(method)}`, payload, handleResponse);
8119
8350
  });
8120
8351
  }
8121
8352
  // ── Connection lifecycle ───────────────────────────────────────────────
@@ -8173,8 +8404,13 @@ var ProjectConnection = class {
8173
8404
  this.flushTimer = null;
8174
8405
  }
8175
8406
  this.flushEvents();
8176
- this.socket?.disconnect();
8177
- this.socket = null;
8407
+ if (this.socket) {
8408
+ this.socket.io.reconnection(false);
8409
+ this.socket.removeAllListeners();
8410
+ this.socket.disconnect();
8411
+ this.socket = null;
8412
+ }
8413
+ this.reconnectCallbacks = [];
8178
8414
  }
8179
8415
  onReconnect(callback) {
8180
8416
  this.reconnectCallbacks.push(callback);
@@ -9771,6 +10007,10 @@ function spawnChildAgent(assignment, workDir) {
9771
10007
  CONVEYOR_MODE: mode,
9772
10008
  CONVEYOR_WORKSPACE: workDir,
9773
10009
  CONVEYOR_USE_WORKTREE: "false",
10010
+ // The project runner flushes git for every child working tree after the
10011
+ // kill (see ProjectRunner.stop), so the child skips its own shutdown
10012
+ // flush — keeps SIGTERM teardown well inside the SIGKILL window.
10013
+ CONVEYOR_SKIP_SHUTDOWN_GIT_FLUSH: "true",
9774
10014
  CONVEYOR_AGENT_MODE: effectiveAgentMode,
9775
10015
  CONVEYOR_IS_AUTO: isAuto ? "true" : "false",
9776
10016
  CONVEYOR_USE_SANDBOX: useSandbox === true ? "true" : "false",
@@ -9895,6 +10135,8 @@ function handleStopTask(taskId, activeAgents, projectDir) {
9895
10135
  // src/runner/project-runner.ts
9896
10136
  var logger10 = createServiceLogger("ProjectRunner");
9897
10137
  var HEARTBEAT_INTERVAL_MS = 3e4;
10138
+ var DISCONNECT_ACK_TIMEOUT_MS = 5e3;
10139
+ var SHUTDOWN_WATCHDOG_MS = 9e4;
9898
10140
  var ProjectRunner = class {
9899
10141
  connection;
9900
10142
  projectDir;
@@ -9978,11 +10220,17 @@ var ProjectRunner = class {
9978
10220
  }
9979
10221
  /** Best-effort WIP commit + push of every tracked working tree on shutdown.
9980
10222
  * Iterates the main project dir plus any active agent worktrees so nothing
9981
- * pending is lost when the pod is killed. Never throws. */
9982
- async flushGitOnShutdown() {
9983
- const dirs = /* @__PURE__ */ new Set([this.projectDir]);
9984
- for (const agent of this.activeAgents.values()) {
9985
- if (agent.worktreePath) dirs.add(agent.worktreePath);
10223
+ * pending is lost when the pod is killed. Never throws.
10224
+ *
10225
+ * Callers that kill child agents first must snapshot the worktree dirs
10226
+ * BEFORE the kill the child exit handler removes entries from
10227
+ * `activeAgents`, so reading it after the kill misses the worktrees. */
10228
+ async flushGitOnShutdown(dirs) {
10229
+ if (!dirs) {
10230
+ dirs = /* @__PURE__ */ new Set([this.projectDir]);
10231
+ for (const agent of this.activeAgents.values()) {
10232
+ if (agent.worktreePath) dirs.add(agent.worktreePath);
10233
+ }
9986
10234
  }
9987
10235
  for (const dir of dirs) {
9988
10236
  try {
@@ -10006,38 +10254,62 @@ var ProjectRunner = class {
10006
10254
  if (this.stopping) return;
10007
10255
  this.stopping = true;
10008
10256
  logger10.info("Shutting down");
10009
- this.commitWatcher.stop();
10010
- await killStartCommand(this.startCmd);
10011
- if (this.heartbeatTimer) {
10012
- clearInterval(this.heartbeatTimer);
10013
- this.heartbeatTimer = null;
10257
+ const watchdog = setTimeout(() => {
10258
+ logger10.warn("Shutdown watchdog fired, forcing lifecycle resolution");
10259
+ this.finishShutdown();
10260
+ }, SHUTDOWN_WATCHDOG_MS);
10261
+ watchdog.unref();
10262
+ const flushDirs = /* @__PURE__ */ new Set([this.projectDir]);
10263
+ for (const agent of this.activeAgents.values()) {
10264
+ if (agent.worktreePath) flushDirs.add(agent.worktreePath);
10014
10265
  }
10015
- const stopPromises = [...this.activeAgents.keys()].map(
10016
- (key) => new Promise((resolve2) => {
10017
- const agent = this.activeAgents.get(key);
10018
- if (!agent) {
10019
- resolve2();
10020
- return;
10021
- }
10022
- agent.process.on("exit", () => resolve2());
10023
- handleStopTask(key, this.activeAgents, this.projectDir);
10024
- })
10025
- );
10026
- await Promise.race([
10027
- Promise.all(stopPromises),
10028
- new Promise((resolve2) => {
10029
- setTimeout(resolve2, STOP_TIMEOUT_MS + 5e3);
10030
- })
10031
- ]);
10032
- await this.flushGitOnShutdown();
10033
10266
  try {
10034
- await this.connection.call("disconnectProjectRunner", {
10035
- projectId: this.connection.projectId
10036
- });
10267
+ this.commitWatcher.stop();
10268
+ await killStartCommand(this.startCmd);
10269
+ if (this.heartbeatTimer) {
10270
+ clearInterval(this.heartbeatTimer);
10271
+ this.heartbeatTimer = null;
10272
+ }
10273
+ const stopPromises = [...this.activeAgents.keys()].map(
10274
+ (key) => new Promise((resolve2) => {
10275
+ const agent = this.activeAgents.get(key);
10276
+ if (!agent) {
10277
+ resolve2();
10278
+ return;
10279
+ }
10280
+ agent.process.on("exit", () => resolve2());
10281
+ handleStopTask(key, this.activeAgents, this.projectDir);
10282
+ })
10283
+ );
10284
+ await Promise.race([
10285
+ Promise.all(stopPromises),
10286
+ new Promise((resolve2) => {
10287
+ setTimeout(resolve2, STOP_TIMEOUT_MS + 5e3);
10288
+ })
10289
+ ]);
10290
+ await this.flushGitOnShutdown(flushDirs);
10291
+ try {
10292
+ await this.connection.call(
10293
+ "disconnectProjectRunner",
10294
+ { projectId: this.connection.projectId },
10295
+ { timeoutMs: DISCONNECT_ACK_TIMEOUT_MS }
10296
+ );
10297
+ } catch {
10298
+ }
10299
+ logger10.info("Shutdown complete");
10300
+ } finally {
10301
+ clearTimeout(watchdog);
10302
+ this.finishShutdown();
10303
+ }
10304
+ }
10305
+ /** Idempotent final step: tear down the socket and resolve the lifecycle
10306
+ * promise so start() returns. Called from stop()'s finally and from the
10307
+ * shutdown watchdog. */
10308
+ finishShutdown() {
10309
+ try {
10310
+ this.connection.disconnect();
10037
10311
  } catch {
10038
10312
  }
10039
- this.connection.disconnect();
10040
- logger10.info("Shutdown complete");
10041
10313
  if (this.resolveLifecycle) {
10042
10314
  this.resolveLifecycle();
10043
10315
  this.resolveLifecycle = null;
@@ -10111,7 +10383,7 @@ var ProjectRunner = class {
10111
10383
  async handleAuditTags(request) {
10112
10384
  this.connection.emitStatus("busy");
10113
10385
  try {
10114
- const { handleTagAudit } = await import("./tag-audit-handler-SWVMCAJH.js");
10386
+ const { handleTagAudit } = await import("./tag-audit-handler-3IFB7YDV.js");
10115
10387
  await handleTagAudit(request, this.connection, this.projectDir);
10116
10388
  } catch (error) {
10117
10389
  const msg = parseErrorMessage(error);
@@ -10134,7 +10406,7 @@ var ProjectRunner = class {
10134
10406
  async handleAuditTasks(request) {
10135
10407
  this.connection.emitStatus("busy");
10136
10408
  try {
10137
- const { handleTaskAudit } = await import("./task-audit-handler-CZ2WWJFO.js");
10409
+ const { handleTaskAudit } = await import("./task-audit-handler-U5Q52YT2.js");
10138
10410
  await handleTaskAudit(request, this.connection, this.projectDir);
10139
10411
  } catch (error) {
10140
10412
  const msg = parseErrorMessage(error);
@@ -10275,4 +10547,4 @@ export {
10275
10547
  loadConveyorConfig,
10276
10548
  unshallowRepo
10277
10549
  };
10278
- //# sourceMappingURL=chunk-B4QHEMMV.js.map
10550
+ //# sourceMappingURL=chunk-WZCO64YR.js.map