@aiden-ade/sandbox-agent 0.1.42 → 0.1.43

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 (2) hide show
  1. package/dist/index.cjs +732 -74
  2. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -13315,13 +13315,13 @@ function resolveViaWhere(command, env) {
13315
13315
  }
13316
13316
  return null;
13317
13317
  }
13318
- function runCliVersionProbe(executable, env, args = ["--version"]) {
13318
+ function runCliVersionProbe(executable, env, args = ["--version"], timeoutMs = 3e3) {
13319
13319
  const isWin = (0, import_node_os3.platform)() === "win32";
13320
13320
  if (isWin && /\.(cmd|bat)$/i.test(executable)) {
13321
13321
  const comSpec = env.ComSpec ?? process.env.ComSpec ?? "cmd.exe";
13322
13322
  return (0, import_node_child_process2.spawnSync)(comSpec, ["/d", "/s", "/c", executable, ...args], {
13323
13323
  encoding: "utf8",
13324
- timeout: 3e3,
13324
+ timeout: timeoutMs,
13325
13325
  env,
13326
13326
  windowsHide: true
13327
13327
  });
@@ -13329,7 +13329,7 @@ function runCliVersionProbe(executable, env, args = ["--version"]) {
13329
13329
  const useShell = isWin && !/[\\/]/.test(executable);
13330
13330
  return (0, import_node_child_process2.spawnSync)(executable, args, {
13331
13331
  encoding: "utf8",
13332
- timeout: 3e3,
13332
+ timeout: timeoutMs,
13333
13333
  env,
13334
13334
  shell: useShell,
13335
13335
  windowsHide: isWin ? true : void 0
@@ -14555,15 +14555,6 @@ If the index returns no useful path, say so and run one targeted local search in
14555
14555
  function buildAlanTeamCodeContextOverlay(teamId) {
14556
14556
  return [buildAlanTeamScopePrompt(teamId), ALAN_CODE_CONTEXT_PROMPT].join("\n\n");
14557
14557
  }
14558
- function buildCodeDiscoveryEnforcementTail() {
14559
- return [
14560
- "## MANDATORY \u2014 index once, then use local source",
14561
- "1. For codebase discovery, make one `mcp__alan__search_code_context` call (hybrid, topK 5). Omit teamId \u2014 Alan session headers resolve it.",
14562
- "2. Once useful paths are returned, stop querying the index and read those local files. The current worktree is the source of truth.",
14563
- "3. Use narrow local `Grep`/graph queries only after the orientation pass. If the index has no useful path, run one targeted local search in the likely package.",
14564
- "Do not repeat or rephrase index searches once paths are known, and do not claim current behavior from index excerpts alone."
14565
- ].join("\n");
14566
- }
14567
14558
  var PLAN_MODE_PROMPT = [
14568
14559
  "You are in Alan plan mode.",
14569
14560
  "Analyze the task, inspect the relevant code and context, and produce a concrete implementation plan.",
@@ -15443,6 +15434,11 @@ function buildSessionHeaders(input) {
15443
15434
  if (input.allowedTools?.length) {
15444
15435
  headers["x-allowed-tools"] = input.allowedTools.join(",");
15445
15436
  }
15437
+ if (input.runHeaders) {
15438
+ for (const [key, value2] of Object.entries(input.runHeaders)) {
15439
+ if (typeof value2 === "string" && value2.length > 0) headers[key] = value2;
15440
+ }
15441
+ }
15446
15442
  return headers;
15447
15443
  }
15448
15444
  function getTomlSectionName(line) {
@@ -15653,6 +15649,23 @@ function syncAlanMcpSessionHeaders(input) {
15653
15649
  }
15654
15650
  return written;
15655
15651
  }
15652
+ var mcpConfigWriteGateTail = Promise.resolve();
15653
+ function acquireMcpConfigWriteGate() {
15654
+ const previous = mcpConfigWriteGateTail;
15655
+ let releaseCurrent;
15656
+ const current = new Promise((resolve22) => {
15657
+ releaseCurrent = resolve22;
15658
+ });
15659
+ mcpConfigWriteGateTail = previous.then(() => current);
15660
+ return previous.then(() => {
15661
+ let released = false;
15662
+ return () => {
15663
+ if (released) return;
15664
+ released = true;
15665
+ releaseCurrent();
15666
+ };
15667
+ });
15668
+ }
15656
15669
  function buildPromptWithSystem(config, promptText) {
15657
15670
  const parts2 = [
15658
15671
  config.systemPrompt?.trim(),
@@ -15661,6 +15674,13 @@ function buildPromptWithSystem(config, promptText) {
15661
15674
  ].filter((value2) => Boolean(value2 && value2.length > 0));
15662
15675
  return parts2.join("\n\n");
15663
15676
  }
15677
+ function buildResumeAwarePrompt(config, promptText, options) {
15678
+ const systemParts = options.isResume ? [config.systemPromptAppend?.trim()] : [config.systemPrompt?.trim()];
15679
+ const parts2 = [...systemParts, promptText.trim()].filter(
15680
+ (value2) => Boolean(value2 && value2.length > 0)
15681
+ );
15682
+ return parts2.join("\n\n");
15683
+ }
15664
15684
  function buildPlanModePrefix(promptText) {
15665
15685
  return [
15666
15686
  "You are in plan-only mode.",
@@ -15827,7 +15847,7 @@ var ERROR_SPECS = {
15827
15847
  recoveryClass: "needs_env"
15828
15848
  },
15829
15849
  auth_expired: {
15830
- message: "The provider connection expired. Reconnect it in Cloud settings, then restart or recreate the machine.",
15850
+ message: "The provider connection expired. Re-authenticate the agent CLI on this runtime (or reconnect it in your provider/Cloud settings), then retry.",
15831
15851
  recoveryClass: "user_fixable"
15832
15852
  },
15833
15853
  subscription_required: {
@@ -15917,7 +15937,7 @@ function normalizeCodexCliErrorMessage(message) {
15917
15937
  return "Codex connection to OpenAI timed out while waiting for the turn to continue. Retry the message; if it repeats, check network/API latency or reduce slow MCP/tool calls in the turn.";
15918
15938
  }
15919
15939
  if (lower.includes("access token could not be refreshed") || lower.includes("refresh_token_invalidated") || lower.includes("refresh token has been invalidated")) {
15920
- return "Codex CLI connection expired. Reconnect OpenAI Codex CLI in Cloud settings, then restart or recreate the machine.";
15940
+ return "Codex CLI connection expired. Re-authenticate the Codex CLI on this computer (run `codex login`) or reconnect it in Cloud settings, then retry.";
15921
15941
  }
15922
15942
  return message;
15923
15943
  }
@@ -16298,6 +16318,9 @@ function createGenericCliBackend(options) {
16298
16318
  } else if (state.resultDeferredOnBackgroundWork && state.activeBackgroundTaskIds?.size) {
16299
16319
  idleTimeoutReason = "background_task";
16300
16320
  resolve22("idle_timeout");
16321
+ } else if (options.postStartupSilenceTimeoutMs && sawStdoutLine && typeof state.lastRawOutputAtMs === "number" && Date.now() - state.lastRawOutputAtMs >= options.postStartupSilenceTimeoutMs) {
16322
+ idleTimeoutReason = "post_startup_silence";
16323
+ resolve22("idle_timeout");
16301
16324
  } else {
16302
16325
  armIdleTimer();
16303
16326
  }
@@ -16310,8 +16333,9 @@ function createGenericCliBackend(options) {
16310
16333
  if (idleTimer) clearTimeout(idleTimer);
16311
16334
  if (raceResult === "idle_timeout") {
16312
16335
  idleTimedOut = true;
16336
+ const idleWaitLabel = idleTimeoutReason === "background_task" ? "a background task terminal event" : idleTimeoutReason === "post_startup_silence" ? "any further output (silent hang)" : "a pending user answer";
16313
16337
  console.warn(
16314
- `[${options.kind}] Idle timeout (${IDLE_MS}ms) waiting for ${idleTimeoutReason === "background_task" ? "a background task terminal event" : "a pending user answer"} \u2014 force-killing orphaned process`
16338
+ `[${options.kind}] Idle timeout (${IDLE_MS}ms) waiting for ${idleWaitLabel} \u2014 force-killing orphaned process`
16315
16339
  );
16316
16340
  killProcessTree(child.pid, { signal: "SIGKILL", child });
16317
16341
  exitCode = await Promise.race([
@@ -16384,6 +16408,25 @@ function createGenericCliBackend(options) {
16384
16408
  console.error(`[${options.kind}] Process force-killed by startup watchdog`, {
16385
16409
  stderrLines: stderrLines.slice(-10)
16386
16410
  });
16411
+ const startupStderr = stderrLines.join("\n").trim();
16412
+ if (startupStderr) {
16413
+ const classified = classifyCliErrorDetailed(startupStderr, exitCode);
16414
+ return {
16415
+ success: false,
16416
+ summary: state.summary.trim() || classified.message,
16417
+ filesModified: [],
16418
+ planFilesCreated: [],
16419
+ iterations: Math.max(state.iterations, 1),
16420
+ error: classified.message,
16421
+ ...classified.errorKind !== "unknown_cli_error" ? { errorKind: classified.errorKind } : {},
16422
+ recoveryClass: classified.recoveryClass,
16423
+ providerSessionId: state.runtimeSessionId,
16424
+ runtimeSessionId: state.runtimeSessionId,
16425
+ backendKind: options.kind,
16426
+ supportTier: options.supportTier,
16427
+ usage: state.usage
16428
+ };
16429
+ }
16387
16430
  return {
16388
16431
  success: false,
16389
16432
  summary: "Agent produced no output",
@@ -16433,7 +16476,7 @@ function createGenericCliBackend(options) {
16433
16476
  filesModified: [],
16434
16477
  planFilesCreated: [],
16435
16478
  iterations: Math.max(state.iterations, 1),
16436
- error: idleTimeoutReason === "background_task" ? "Agent timed out waiting for a background task to finish \u2014 no terminal event arrived, so the run was stopped. Send your message again to continue." : "Agent timed out waiting for your answer to its question \u2014 no response arrived, so the run was stopped. Send your reply as a new message to continue.",
16479
+ error: idleTimeoutReason === "background_task" ? "Agent timed out waiting for a background task to finish \u2014 no terminal event arrived, so the run was stopped. Send your message again to continue." : idleTimeoutReason === "post_startup_silence" ? "The agent went silent and stopped responding, so the run was stopped. Send your message again to retry." : "Agent timed out waiting for your answer to its question \u2014 no response arrived, so the run was stopped. Send your reply as a new message to continue.",
16437
16480
  providerSessionId: state.runtimeSessionId,
16438
16481
  runtimeSessionId: state.runtimeSessionId,
16439
16482
  backendKind: options.kind,
@@ -17141,6 +17184,16 @@ var TERMINAL_TASK_NOTIFICATION_STATUSES = /* @__PURE__ */ new Set([
17141
17184
  "cancelled",
17142
17185
  "canceled"
17143
17186
  ]);
17187
+ var NON_TERMINAL_TASK_NOTIFICATION_STATUSES = /* @__PURE__ */ new Set([
17188
+ "running",
17189
+ "in_progress",
17190
+ "inprogress",
17191
+ "started",
17192
+ "pending",
17193
+ "active",
17194
+ "working",
17195
+ "queued"
17196
+ ]);
17144
17197
  function extractToolResultContent(content) {
17145
17198
  if (typeof content === "string") return content;
17146
17199
  if (!Array.isArray(content)) return "";
@@ -17158,6 +17211,7 @@ function stringField2(record, key) {
17158
17211
  function isTerminalTaskNotification(record) {
17159
17212
  const status = stringField2(record, "status")?.toLowerCase();
17160
17213
  if (status && TERMINAL_TASK_NOTIFICATION_STATUSES.has(status)) return true;
17214
+ if (status && NON_TERMINAL_TASK_NOTIFICATION_STATUSES.has(status)) return false;
17161
17215
  return typeof record.duration_ms === "number" || typeof record.durationMs === "number" || typeof record.total_tokens === "number" || typeof record.totalTokens === "number" || typeof record.tool_uses === "number" || typeof record.toolUses === "number";
17162
17216
  }
17163
17217
  function isFailedTaskNotification(record) {
@@ -17249,20 +17303,25 @@ function handleClaudeStructuredEvent(parsed, context, state) {
17249
17303
  state.iterations += 1;
17250
17304
  const message = typeof parsed.message === "object" && parsed.message !== null ? parsed.message : null;
17251
17305
  const content = Array.isArray(message?.content) ? message.content : [];
17306
+ let previousBlockWasText = false;
17252
17307
  for (const block of content) {
17253
17308
  if (typeof block !== "object" || block === null || !("type" in block)) continue;
17254
17309
  if (block.type === "text" && typeof block.text === "string") {
17255
17310
  const text = block.text;
17256
17311
  state.summary += `${text}
17257
17312
  `;
17258
- void presenter.onAssistantText(text);
17313
+ void presenter.onAssistantText(previousBlockWasText ? `
17314
+ ${text}` : text);
17315
+ previousBlockWasText = true;
17259
17316
  continue;
17260
17317
  }
17261
17318
  if (block.type === "thinking" && typeof block.thinking === "string") {
17319
+ previousBlockWasText = false;
17262
17320
  void presenter.onThinking(block.thinking);
17263
17321
  continue;
17264
17322
  }
17265
17323
  if (block.type === "tool_use" && typeof block.id === "string" && typeof block.name === "string") {
17324
+ previousBlockWasText = false;
17266
17325
  const toolBlock = block;
17267
17326
  void presenter.onToolUse(
17268
17327
  toolBlock.name,
@@ -17378,7 +17437,7 @@ function handleClaudeStructuredEvent(parsed, context, state) {
17378
17437
  state.iterations = parsed.num_turns;
17379
17438
  }
17380
17439
  const subtype = typeof parsed.subtype === "string" ? parsed.subtype : "";
17381
- if ((subtype === "error_during_execution" || subtype === "error_max_turns") && !state.error) {
17440
+ if (subtype.startsWith("error") && !state.error) {
17382
17441
  const errObj = typeof parsed.error === "object" && parsed.error !== null ? parsed.error : null;
17383
17442
  const resumeId = context.config.runtimeSessionId?.trim() || context.config.providerSessionId?.trim();
17384
17443
  const errMsg = subtype === "error_during_execution" && resumeId ? SESSION_RESUME_FAILED_MESSAGE : typeof parsed.error === "string" && parsed.error.trim() || errObj && typeof errObj.message === "string" && errObj.message.trim() || typeof parsed.message === "string" && parsed.message.trim() || typeof parsed.result === "string" && parsed.result.trim() || subtype;
@@ -17529,7 +17588,7 @@ function createClaudeCliBackend(command = "claude", defaultArgs = []) {
17529
17588
  const basePromptText = resumeContextPrefix ? `${resumeContextPrefix}
17530
17589
 
17531
17590
  ${ctx.promptText}` : ctx.promptText;
17532
- const promptText = ctx.config.mode === "plan" ? buildPlanModePrefix(buildPromptWithSystem(ctx.config, basePromptText)) : buildPromptWithSystem(ctx.config, basePromptText);
17591
+ const promptText = ctx.config.mode === "plan" ? buildPlanModePrefix(basePromptText) : basePromptText;
17533
17592
  if (promptText.trim()) {
17534
17593
  contentBlocks.push({ type: "text", text: promptText });
17535
17594
  }
@@ -17620,6 +17679,25 @@ function buildGeneratedImageFromCodexPayload(payload) {
17620
17679
  idFields: []
17621
17680
  });
17622
17681
  }
17682
+ function extractRolloutMessageText(payload) {
17683
+ const content = payload.content;
17684
+ if (typeof content === "string") return content;
17685
+ if (Array.isArray(content)) {
17686
+ for (const block of content) {
17687
+ if (block && typeof block === "object" && typeof block.text === "string") {
17688
+ return block.text;
17689
+ }
17690
+ }
17691
+ }
17692
+ return "";
17693
+ }
17694
+ function isSyntheticUserRolloutMessage(payload) {
17695
+ const text = extractRolloutMessageText(payload).trimStart();
17696
+ if (!text) return false;
17697
+ const lower = text.toLowerCase();
17698
+ return lower.startsWith("<environment_context") || lower.startsWith("<user_instructions") || // Auto-compaction bridge summaries are tagged in the rollout content.
17699
+ lower.startsWith("<compact") || lower.startsWith("[compact");
17700
+ }
17623
17701
  function latestUserMessageLineIndex(lines) {
17624
17702
  let latestIndex = -1;
17625
17703
  for (let index = 0; index < lines.length; index += 1) {
@@ -17627,7 +17705,7 @@ function latestUserMessageLineIndex(lines) {
17627
17705
  if (!line) continue;
17628
17706
  const entry = parseJsonObject(line);
17629
17707
  const payload = entry && typeof entry.payload === "object" && entry.payload !== null ? entry.payload : null;
17630
- if (payload?.type === "message" && payload.role === "user") {
17708
+ if (payload?.type === "message" && payload.role === "user" && !isSyntheticUserRolloutMessage(payload)) {
17631
17709
  latestIndex = index;
17632
17710
  }
17633
17711
  }
@@ -17691,7 +17769,9 @@ async function replayCodexMcpToolEventsFromSessionLog(context, state) {
17691
17769
  }
17692
17770
  }
17693
17771
  var CODEX_EXIT_GRACE_MS = 3e4;
17694
- function armCodexExitGraceKill(state) {
17772
+ var CODEX_BACKGROUND_EXIT_GRACE_MS = 10 * 6e4;
17773
+ var CODEX_EXIT_GRACE_SIGKILL_MS = 3e3;
17774
+ function armCodexExitGraceKill(state, graceMs = CODEX_EXIT_GRACE_MS) {
17695
17775
  if (state.exitGraceKillTimer) return;
17696
17776
  const child = state.process;
17697
17777
  if (!child) return;
@@ -17701,7 +17781,15 @@ function armCodexExitGraceKill(state) {
17701
17781
  "[codex_app_server] Process did not exit after turn completion; killing process group"
17702
17782
  );
17703
17783
  killProcessTree(child.pid, { signal: "SIGTERM", child });
17704
- }, CODEX_EXIT_GRACE_MS);
17784
+ const killTimer = setTimeout(() => {
17785
+ if (child.exitCode !== null) return;
17786
+ console.warn(
17787
+ "[codex_app_server] Process still alive after grace SIGTERM; escalating to SIGKILL"
17788
+ );
17789
+ killProcessTree(child.pid, { signal: "SIGKILL", child });
17790
+ }, CODEX_EXIT_GRACE_SIGKILL_MS);
17791
+ killTimer.unref?.();
17792
+ }, graceMs);
17705
17793
  timer.unref?.();
17706
17794
  state.exitGraceKillTimer = timer;
17707
17795
  }
@@ -17710,6 +17798,167 @@ function disarmCodexExitGraceKill(state) {
17710
17798
  clearTimeout(state.exitGraceKillTimer);
17711
17799
  state.exitGraceKillTimer = void 0;
17712
17800
  }
17801
+ function completeCodexTurnRespectingWorkers(state) {
17802
+ disarmCodexExitGraceKill(state);
17803
+ const activeWorkers = state.activeBackgroundTaskIds?.size ?? 0;
17804
+ if (activeWorkers > 0) {
17805
+ state.resultDeferredOnBackgroundWork = true;
17806
+ state.codexAwaitingWorkersAfterTurnEnd = true;
17807
+ console.info(
17808
+ `[codex_app_server] Turn completed with ${activeWorkers} background worker id(s) still active \u2014 deferring exit grace`
17809
+ );
17810
+ armCodexExitGraceKill(state, CODEX_BACKGROUND_EXIT_GRACE_MS);
17811
+ return;
17812
+ }
17813
+ armCodexExitGraceKill(state);
17814
+ }
17815
+ function touchCodexBackgroundGrace(state) {
17816
+ if (!state.codexAwaitingWorkersAfterTurnEnd) return;
17817
+ disarmCodexExitGraceKill(state);
17818
+ armCodexExitGraceKill(state, CODEX_BACKGROUND_EXIT_GRACE_MS);
17819
+ }
17820
+ function maybeReleaseCodexDeferredTurn(state) {
17821
+ if (!state.codexAwaitingWorkersAfterTurnEnd) return;
17822
+ if (state.activeBackgroundTaskIds?.size) return;
17823
+ state.codexAwaitingWorkersAfterTurnEnd = false;
17824
+ state.resultDeferredOnBackgroundWork = false;
17825
+ disarmCodexExitGraceKill(state);
17826
+ armCodexExitGraceKill(state);
17827
+ console.info(
17828
+ "[codex_app_server] All background workers finished after turn completion \u2014 arming normal exit grace"
17829
+ );
17830
+ }
17831
+ var TERMINAL_COLLAB_AGENT_STATUSES = /* @__PURE__ */ new Set([
17832
+ "completed",
17833
+ "errored",
17834
+ "shutdown",
17835
+ "interrupted",
17836
+ "not_found"
17837
+ ]);
17838
+ var FAILED_COLLAB_AGENT_STATUSES = /* @__PURE__ */ new Set(["errored", "interrupted", "not_found"]);
17839
+ var COLLAB_COORDINATION_TOOL_NAMES = {
17840
+ send_input: "collab_send_input",
17841
+ wait: "collab_wait",
17842
+ close_agent: "collab_close_agent"
17843
+ };
17844
+ function firstLine(text, maxLength = 140) {
17845
+ const line = text.split("\n").find((candidate) => candidate.trim().length > 0)?.trim() ?? "";
17846
+ return line.length > maxLength ? `${line.slice(0, maxLength - 1)}\u2026` : line;
17847
+ }
17848
+ function finishCodexWorker(context, state, options) {
17849
+ const launcherId = options.launcherId ?? (options.threadId ? state.backgroundToolIdByTaskId?.get(options.threadId) : void 0) ?? (options.agentPath ? state.codexWorkerToolIdByPath?.get(options.agentPath) : void 0);
17850
+ if (!launcherId) {
17851
+ if (options.threadId) clearBackgroundTaskIds(state, [options.threadId]);
17852
+ maybeReleaseCodexDeferredTurn(state);
17853
+ return;
17854
+ }
17855
+ const launcherStillActive = state.activeBackgroundTaskIds?.has(launcherId) === true || (state.backgroundTaskIdsByToolId?.get(launcherId)?.size ?? 0) > 0;
17856
+ if (!launcherStillActive) {
17857
+ if (options.threadId) clearBackgroundTaskIds(state, [options.threadId]);
17858
+ maybeReleaseCodexDeferredTurn(state);
17859
+ return;
17860
+ }
17861
+ clearBackgroundTaskIds(state, [options.threadId ?? launcherId]);
17862
+ const remainingSiblings = state.backgroundTaskIdsByToolId?.get(launcherId)?.size ?? 0;
17863
+ if (remainingSiblings === 0) {
17864
+ clearBackgroundTaskIds(state, [launcherId]);
17865
+ const isError = FAILED_COLLAB_AGENT_STATUSES.has(options.status);
17866
+ const resultText = options.message?.trim() || `Background worker ${options.status}.`;
17867
+ void context.presenter.onToolResult?.(launcherId, resultText, isError);
17868
+ }
17869
+ maybeReleaseCodexDeferredTurn(state);
17870
+ }
17871
+ function applyCodexCollabAgentStates(context, state, agentsStates) {
17872
+ for (const [threadId, raw] of Object.entries(agentsStates)) {
17873
+ if (!raw || typeof raw !== "object") continue;
17874
+ const entry = raw;
17875
+ const status = typeof entry.status === "string" ? entry.status : "";
17876
+ if (!TERMINAL_COLLAB_AGENT_STATUSES.has(status)) continue;
17877
+ finishCodexWorker(context, state, {
17878
+ threadId,
17879
+ status,
17880
+ message: typeof entry.message === "string" ? entry.message : void 0
17881
+ });
17882
+ }
17883
+ }
17884
+ function summarizeCodexAgentStates(agentsStates) {
17885
+ return Object.entries(agentsStates).map(([threadId, raw]) => {
17886
+ const entry = raw && typeof raw === "object" ? raw : {};
17887
+ const status = typeof entry.status === "string" ? entry.status : "unknown";
17888
+ const message = typeof entry.message === "string" && entry.message.trim() ? ` \u2014 ${firstLine(entry.message)}` : "";
17889
+ return `${threadId}: ${status}${message}`;
17890
+ }).join("\n");
17891
+ }
17892
+ function handleCodexCollabToolCallItem(context, state, item, phase) {
17893
+ const itemId = typeof item.id === "string" ? item.id : "";
17894
+ const tool = typeof item.tool === "string" ? item.tool : "";
17895
+ if (!itemId || !tool) return true;
17896
+ const senderThreadId = typeof item.sender_thread_id === "string" ? item.sender_thread_id : "";
17897
+ const receiverThreadIds = Array.isArray(item.receiver_thread_ids) ? item.receiver_thread_ids.filter(
17898
+ (value2) => typeof value2 === "string" && value2.length > 0
17899
+ ) : [];
17900
+ const prompt = typeof item.prompt === "string" ? item.prompt : "";
17901
+ const agentsStates = typeof item.agents_states === "object" && item.agents_states !== null ? item.agents_states : {};
17902
+ const itemStatus = typeof item.status === "string" ? item.status : "";
17903
+ if (tool === "spawn_agent") {
17904
+ if (trackCodexStreamedToolId(state, itemId)) {
17905
+ const parentToolUseId = senderThreadId ? state.backgroundToolIdByTaskId?.get(senderThreadId) ?? null : null;
17906
+ void context.presenter.onToolUse(
17907
+ "Agent",
17908
+ {
17909
+ subagent_type: "codex-worker",
17910
+ description: firstLine(prompt) || "Codex background worker",
17911
+ ...prompt ? { prompt } : {},
17912
+ ...receiverThreadIds.length > 0 ? { agent_thread_ids: receiverThreadIds } : {}
17913
+ },
17914
+ itemId,
17915
+ parentToolUseId
17916
+ );
17917
+ registerBackgroundLauncher(state, itemId);
17918
+ }
17919
+ for (const threadId of receiverThreadIds) {
17920
+ if (state.backgroundToolIdByTaskId?.get(threadId)) continue;
17921
+ registerBackgroundTaskRecord(state, { task_id: threadId, tool_use_id: itemId });
17922
+ }
17923
+ applyCodexCollabAgentStates(context, state, agentsStates);
17924
+ if (phase === "completed" && itemStatus === "failed") {
17925
+ finishCodexWorker(context, state, {
17926
+ launcherId: itemId,
17927
+ status: "errored",
17928
+ message: "Failed to spawn background worker."
17929
+ });
17930
+ }
17931
+ return true;
17932
+ }
17933
+ const coordinationToolName = COLLAB_COORDINATION_TOOL_NAMES[tool];
17934
+ if (!coordinationToolName) return false;
17935
+ if (trackCodexStreamedToolId(state, itemId)) {
17936
+ void context.presenter.onToolUse(
17937
+ coordinationToolName,
17938
+ {
17939
+ ...receiverThreadIds.length > 0 ? { receiver_thread_ids: receiverThreadIds } : {},
17940
+ ...prompt ? { prompt } : {}
17941
+ },
17942
+ itemId
17943
+ );
17944
+ }
17945
+ if (phase === "completed") {
17946
+ const isError = itemStatus === "failed";
17947
+ const summary = summarizeCodexAgentStates(agentsStates);
17948
+ void context.presenter.onToolResult?.(
17949
+ itemId,
17950
+ summary || (isError ? "Failed." : "Done."),
17951
+ isError
17952
+ );
17953
+ }
17954
+ applyCodexCollabAgentStates(context, state, agentsStates);
17955
+ if (tool === "close_agent" && phase === "completed" && itemStatus !== "failed") {
17956
+ for (const threadId of receiverThreadIds) {
17957
+ finishCodexWorker(context, state, { threadId, status: "shutdown" });
17958
+ }
17959
+ }
17960
+ return true;
17961
+ }
17713
17962
  function trackCodexStreamedToolId(state, toolId) {
17714
17963
  if (!state.codexStreamedToolIds) state.codexStreamedToolIds = /* @__PURE__ */ new Set();
17715
17964
  if (state.codexStreamedToolIds.has(toolId)) return false;
@@ -17740,6 +17989,7 @@ function handleCodexStructuredEvent(parsed, context, state) {
17740
17989
  const presenter = context.presenter;
17741
17990
  const type = typeof parsed.type === "string" ? parsed.type : "";
17742
17991
  if (!type) return false;
17992
+ touchCodexBackgroundGrace(state);
17743
17993
  if (typeof parsed.thread_id === "string") {
17744
17994
  state.runtimeSessionId = parsed.thread_id;
17745
17995
  }
@@ -17764,7 +18014,12 @@ function handleCodexStructuredEvent(parsed, context, state) {
17764
18014
  return true;
17765
18015
  case "turn.started":
17766
18016
  disarmCodexExitGraceKill(state);
17767
- state.iterations += 1;
18017
+ state.codexAwaitingWorkersAfterTurnEnd = false;
18018
+ state.resultDeferredOnBackgroundWork = false;
18019
+ if (state.codexIterationEventFamily !== "legacy") {
18020
+ state.codexIterationEventFamily = "modern";
18021
+ state.iterations += 1;
18022
+ }
17768
18023
  return true;
17769
18024
  case "session_configured":
17770
18025
  if (typeof parsed.session_id === "string") {
@@ -17773,7 +18028,12 @@ function handleCodexStructuredEvent(parsed, context, state) {
17773
18028
  return true;
17774
18029
  case "task_started":
17775
18030
  disarmCodexExitGraceKill(state);
17776
- state.iterations += 1;
18031
+ state.codexAwaitingWorkersAfterTurnEnd = false;
18032
+ state.resultDeferredOnBackgroundWork = false;
18033
+ if (state.codexIterationEventFamily !== "modern") {
18034
+ state.codexIterationEventFamily = "legacy";
18035
+ state.iterations += 1;
18036
+ }
17777
18037
  return true;
17778
18038
  case "item.started": {
17779
18039
  const item = typeof parsed.item === "object" && parsed.item !== null ? parsed.item : null;
@@ -17800,6 +18060,8 @@ function handleCodexStructuredEvent(parsed, context, state) {
17800
18060
  }
17801
18061
  return true;
17802
18062
  }
18063
+ case "collab_tool_call":
18064
+ return handleCodexCollabToolCallItem(context, state, item, "started");
17803
18065
  // Text-bearing and patch/todo items carry no useful live-start payload; they
17804
18066
  // are surfaced on item.completed. Recognized (do not count as unhandled).
17805
18067
  case "reasoning":
@@ -17811,6 +18073,21 @@ function handleCodexStructuredEvent(parsed, context, state) {
17811
18073
  return false;
17812
18074
  }
17813
18075
  }
18076
+ case "item.updated": {
18077
+ const item = typeof parsed.item === "object" && parsed.item !== null ? parsed.item : null;
18078
+ if (!item || typeof item.type !== "string") return true;
18079
+ switch (item.type) {
18080
+ case "collab_tool_call":
18081
+ return handleCodexCollabToolCallItem(context, state, item, "updated");
18082
+ case "todo_list": {
18083
+ const todos = Array.isArray(item.items) ? item.items : [];
18084
+ void presenter.onTodoWrite?.(todos);
18085
+ return true;
18086
+ }
18087
+ default:
18088
+ return false;
18089
+ }
18090
+ }
17814
18091
  case "item.completed": {
17815
18092
  const item = typeof parsed.item === "object" && parsed.item !== null ? parsed.item : null;
17816
18093
  if (!item || typeof item.type !== "string") return true;
@@ -17875,6 +18152,8 @@ function handleCodexStructuredEvent(parsed, context, state) {
17875
18152
  void presenter.onTodoWrite?.(todos);
17876
18153
  return true;
17877
18154
  }
18155
+ case "collab_tool_call":
18156
+ return handleCodexCollabToolCallItem(context, state, item, "completed");
17878
18157
  default:
17879
18158
  return false;
17880
18159
  }
@@ -17896,7 +18175,9 @@ function handleCodexStructuredEvent(parsed, context, state) {
17896
18175
  return true;
17897
18176
  }
17898
18177
  case "exec_command_begin": {
17899
- const toolId = typeof parsed.call_id === "string" ? parsed.call_id : `exec-${Date.now()}`;
18178
+ const hasCallId = typeof parsed.call_id === "string" && parsed.call_id.length > 0;
18179
+ const toolId = hasCallId ? parsed.call_id : `exec-${Date.now()}`;
18180
+ if (!hasCallId) state.lastAnonymousExecId = toolId;
17900
18181
  const command = Array.isArray(parsed.command) ? parsed.command.join(" ") : "";
17901
18182
  const cwd = typeof parsed.cwd === "string" ? parsed.cwd : context.cwd;
17902
18183
  trackCodexStreamedToolId(state, toolId);
@@ -17904,7 +18185,10 @@ function handleCodexStructuredEvent(parsed, context, state) {
17904
18185
  return true;
17905
18186
  }
17906
18187
  case "exec_command_end": {
17907
- const toolId = typeof parsed.call_id === "string" ? parsed.call_id : `exec-${Date.now()}`;
18188
+ const hasCallId = typeof parsed.call_id === "string" && parsed.call_id.length > 0;
18189
+ const toolId = hasCallId ? parsed.call_id : state.lastAnonymousExecId;
18190
+ if (!hasCallId) state.lastAnonymousExecId = void 0;
18191
+ if (!toolId) return true;
17908
18192
  const output = typeof parsed.formatted_output === "string" ? parsed.formatted_output : typeof parsed.aggregated_output === "string" ? parsed.aggregated_output : "";
17909
18193
  void presenter.onToolResult?.(toolId, output);
17910
18194
  return true;
@@ -17912,9 +18196,8 @@ function handleCodexStructuredEvent(parsed, context, state) {
17912
18196
  case "mcp_tool_call_begin": {
17913
18197
  const toolId = typeof parsed.call_id === "string" ? parsed.call_id : `mcp-${Date.now()}`;
17914
18198
  const invocation = typeof parsed.invocation === "object" && parsed.invocation !== null ? parsed.invocation : {};
17915
- const tool = typeof invocation.tool_name === "string" ? invocation.tool_name : typeof invocation.tool === "string" ? invocation.tool : "MCP Tool";
17916
18199
  trackCodexStreamedToolId(state, toolId);
17917
- void presenter.onToolUse(tool, invocation, toolId);
18200
+ void presenter.onToolUse(buildCodexMcpToolName(invocation), invocation, toolId);
17918
18201
  return true;
17919
18202
  }
17920
18203
  case "mcp_tool_call_end": {
@@ -17952,7 +18235,7 @@ function handleCodexStructuredEvent(parsed, context, state) {
17952
18235
  const lastMessage = typeof parsed.last_agent_message === "string" ? parsed.last_agent_message : "";
17953
18236
  if (lastMessage.length > 0) state.summary = lastMessage;
17954
18237
  state.error = void 0;
17955
- armCodexExitGraceKill(state);
18238
+ completeCodexTurnRespectingWorkers(state);
17956
18239
  return true;
17957
18240
  }
17958
18241
  case "turn.completed": {
@@ -17975,7 +18258,108 @@ function handleCodexStructuredEvent(parsed, context, state) {
17975
18258
  cacheCreationTokens: state.usage.cacheCreationTokens
17976
18259
  });
17977
18260
  state.error = void 0;
17978
- armCodexExitGraceKill(state);
18261
+ completeCodexTurnRespectingWorkers(state);
18262
+ return true;
18263
+ }
18264
+ case "sub_agent_activity": {
18265
+ const kind = typeof parsed.kind === "string" ? parsed.kind : "";
18266
+ const agentThreadId = typeof parsed.agent_thread_id === "string" ? parsed.agent_thread_id : "";
18267
+ const agentPath = typeof parsed.agent_path === "string" ? parsed.agent_path : "";
18268
+ const eventId = typeof parsed.event_id === "string" ? parsed.event_id : "";
18269
+ const isRootPath = !agentPath || agentPath === "/root" || agentPath === "/";
18270
+ switch (kind) {
18271
+ case "started": {
18272
+ if (isRootPath) return true;
18273
+ if (agentThreadId && state.backgroundToolIdByTaskId?.has(agentThreadId)) return true;
18274
+ const toolId = eventId || agentThreadId;
18275
+ if (!toolId) return true;
18276
+ if (trackCodexStreamedToolId(state, toolId)) {
18277
+ const workerName = agentPath.slice(agentPath.lastIndexOf("/") + 1) || "codex-worker";
18278
+ const parentPath = agentPath.slice(0, agentPath.lastIndexOf("/"));
18279
+ const parentToolUseId = parentPath && parentPath !== "/root" ? state.codexWorkerToolIdByPath?.get(parentPath) ?? null : null;
18280
+ void presenter.onToolUse(
18281
+ "Agent",
18282
+ {
18283
+ subagent_type: workerName,
18284
+ description: agentPath,
18285
+ ...agentThreadId ? { agent_thread_id: agentThreadId } : {}
18286
+ },
18287
+ toolId,
18288
+ parentToolUseId
18289
+ );
18290
+ registerBackgroundLauncher(state, toolId);
18291
+ if (agentThreadId) {
18292
+ registerBackgroundTaskRecord(state, { task_id: agentThreadId, tool_use_id: toolId });
18293
+ }
18294
+ if (!state.codexWorkerToolIdByPath) state.codexWorkerToolIdByPath = /* @__PURE__ */ new Map();
18295
+ state.codexWorkerToolIdByPath.set(agentPath, toolId);
18296
+ }
18297
+ return true;
18298
+ }
18299
+ case "interacted":
18300
+ return true;
18301
+ case "interrupted": {
18302
+ if (isRootPath) return true;
18303
+ finishCodexWorker(context, state, {
18304
+ threadId: agentThreadId || void 0,
18305
+ agentPath: agentPath || void 0,
18306
+ status: "interrupted",
18307
+ message: "Background worker interrupted."
18308
+ });
18309
+ return true;
18310
+ }
18311
+ default:
18312
+ return false;
18313
+ }
18314
+ }
18315
+ // App-server EventMsg collab lifecycle notices. Begin/interaction/resume events
18316
+ // carry no state Alan tracks beyond liveness (touched above); spawn_end and the
18317
+ // waiting/close terminals map into the shared worker registry below.
18318
+ case "collab_agent_spawn_begin":
18319
+ case "collab_agent_interaction_begin":
18320
+ case "collab_agent_interaction_end":
18321
+ case "collab_waiting_begin":
18322
+ case "collab_resume_begin":
18323
+ case "collab_resume_end":
18324
+ case "collab_close_begin":
18325
+ return true;
18326
+ case "collab_agent_spawn_end": {
18327
+ const newThreadId = typeof parsed.new_thread_id === "string" ? parsed.new_thread_id : "";
18328
+ if (!newThreadId || state.backgroundToolIdByTaskId?.has(newThreadId)) return true;
18329
+ const callId = typeof parsed.call_id === "string" && parsed.call_id ? parsed.call_id : typeof parsed.event_id === "string" ? parsed.event_id : "";
18330
+ const toolId = callId || `collab-spawn-${newThreadId}`;
18331
+ if (trackCodexStreamedToolId(state, toolId)) {
18332
+ const nickname = typeof parsed.new_agent_nickname === "string" ? parsed.new_agent_nickname : "";
18333
+ const role = typeof parsed.new_agent_role === "string" ? parsed.new_agent_role : "";
18334
+ void presenter.onToolUse(
18335
+ "Agent",
18336
+ {
18337
+ subagent_type: nickname || role || "codex-worker",
18338
+ description: [nickname, role].filter(Boolean).join(" \u2014 ") || "Codex background worker",
18339
+ agent_thread_id: newThreadId
18340
+ },
18341
+ toolId
18342
+ );
18343
+ registerBackgroundLauncher(state, toolId);
18344
+ }
18345
+ registerBackgroundTaskRecord(state, { task_id: newThreadId, tool_use_id: toolId });
18346
+ return true;
18347
+ }
18348
+ case "collab_waiting_end": {
18349
+ const entries = Array.isArray(parsed.agent_statuses) ? parsed.agent_statuses : [];
18350
+ for (const raw of entries) {
18351
+ if (!raw || typeof raw !== "object") continue;
18352
+ const entry = raw;
18353
+ const threadId = typeof entry.agent_thread_id === "string" ? entry.agent_thread_id : typeof entry.thread_id === "string" ? entry.thread_id : "";
18354
+ const status = typeof entry.status === "string" ? entry.status.toLowerCase() : "";
18355
+ if (!threadId || !TERMINAL_COLLAB_AGENT_STATUSES.has(status)) continue;
18356
+ finishCodexWorker(context, state, { threadId, status });
18357
+ }
18358
+ return true;
18359
+ }
18360
+ case "collab_close_end": {
18361
+ const threadId = typeof parsed.receiver_thread_id === "string" ? parsed.receiver_thread_id : "";
18362
+ if (threadId) finishCodexWorker(context, state, { threadId, status: "shutdown" });
17979
18363
  return true;
17980
18364
  }
17981
18365
  case "error": {
@@ -18094,10 +18478,11 @@ function createCodexRuntimeBackend(command = "codex", defaultArgs = []) {
18094
18478
  },
18095
18479
  promptViaStdin: true,
18096
18480
  augmentPrompt: (ctx) => {
18481
+ const isResume = Boolean(resumableSessionId);
18097
18482
  const promptWithContext = resumeContextPrefix ? `${resumeContextPrefix}
18098
18483
 
18099
18484
  ${ctx.promptText}` : ctx.promptText;
18100
- const basePrompt = buildPromptWithSystem(ctx.config, promptWithContext);
18485
+ const basePrompt = buildResumeAwarePrompt(ctx.config, promptWithContext, { isResume });
18101
18486
  return ctx.config.mode === "plan" ? buildPlanModePrefix(basePrompt) : basePrompt;
18102
18487
  },
18103
18488
  parseStructuredLine: parseCodexStructuredLine,
@@ -18303,10 +18688,21 @@ function handleCursorStructuredEvent(parsed, context, state) {
18303
18688
  }
18304
18689
  case "tool_call": {
18305
18690
  const subtype = typeof parsed.subtype === "string" ? parsed.subtype : "";
18306
- const toolId = typeof parsed.call_id === "string" ? parsed.call_id : `cursor-tool-${Date.now()}`;
18691
+ const hasCallId = typeof parsed.call_id === "string" && parsed.call_id.length > 0;
18307
18692
  const toolCall = typeof parsed.tool_call === "object" && parsed.tool_call !== null ? parsed.tool_call : null;
18308
18693
  const entry = toolCall ? extractCursorToolEntry(toolCall) : null;
18309
18694
  if (!entry) return true;
18695
+ let toolId;
18696
+ if (hasCallId) {
18697
+ toolId = parsed.call_id;
18698
+ } else if (subtype === "started") {
18699
+ state.cursorAnonymousToolCounter = (state.cursorAnonymousToolCounter ?? 0) + 1;
18700
+ toolId = `cursor-tool-${state.cursorAnonymousToolCounter}`;
18701
+ state.lastAnonymousCursorToolId = toolId;
18702
+ } else {
18703
+ toolId = state.lastAnonymousCursorToolId;
18704
+ }
18705
+ if (!toolId) return true;
18310
18706
  state.iterations = Math.max(state.iterations, 1);
18311
18707
  const toolName = formatCursorToolName(entry.rawName, entry.payload);
18312
18708
  const toolArgs = typeof entry.payload.args === "object" && entry.payload.args !== null ? entry.payload.args : typeof entry.payload.arguments === "object" && entry.payload.arguments !== null ? entry.payload.arguments : entry.payload;
@@ -18315,6 +18711,7 @@ function handleCursorStructuredEvent(parsed, context, state) {
18315
18711
  return true;
18316
18712
  }
18317
18713
  if (subtype === "completed") {
18714
+ if (!hasCallId) state.lastAnonymousCursorToolId = void 0;
18318
18715
  void presenter.onToolResult?.(toolId, buildCursorToolResultText(entry.payload.result));
18319
18716
  }
18320
18717
  return true;
@@ -18393,7 +18790,10 @@ function buildCursorAgentModelArg(modelId, options) {
18393
18790
  const wireEffort = resolveWireEffort({
18394
18791
  harness: "cursor_agent_cli",
18395
18792
  modelId: baseId,
18396
- selectedEffortLevel: options?.selectedEffortLevel,
18793
+ // Fall back to an effort baked into the model id (e.g. `gpt-5.5-high` from a
18794
+ // session import or raw discovered id) when no explicit selection overrides it,
18795
+ // so the SKU is preserved instead of silently downgraded to default effort.
18796
+ selectedEffortLevel: options?.selectedEffortLevel ?? parsed.effort,
18397
18797
  effortLevels
18398
18798
  });
18399
18799
  let id = baseId;
@@ -18407,7 +18807,8 @@ function buildCursorAgentModelArg(modelId, options) {
18407
18807
  }
18408
18808
  }
18409
18809
  const contextWindow = asContextWindow(options?.selectedContextWindow);
18410
- if (contextWindow === "1m" && id === baseId) {
18810
+ const modelSupports1m = (options?.contextWindows ?? []).includes("1m");
18811
+ if (contextWindow === "1m" && id === baseId && modelSupports1m) {
18411
18812
  return `${baseId}[context=1m]`;
18412
18813
  }
18413
18814
  if (parsed.fast) return `${id}-fast`;
@@ -18497,7 +18898,8 @@ function createCursorAgentCliBackend(command = "cursor-agent", defaultArgs = [])
18497
18898
  buildCursorAgentModelArg(model, {
18498
18899
  selectedEffortLevel: ctx.config.selectedEffortLevel,
18499
18900
  selectedContextWindow: ctx.config.selectedContextWindow,
18500
- effortLevels: modelDef?.capabilities?.effortLevels
18901
+ effortLevels: modelDef?.capabilities?.effortLevels,
18902
+ contextWindows: modelDef?.capabilities?.contextWindows
18501
18903
  })
18502
18904
  );
18503
18905
  }
@@ -18515,11 +18917,20 @@ function createCursorAgentCliBackend(command = "cursor-agent", defaultArgs = [])
18515
18917
  // with zero output; without this watchdog only the 30-min server reaper
18516
18918
  // would end the run.
18517
18919
  startupTimeoutMs: 18e4,
18920
+ // Post-startup hang bound: cursor's asks use the non-blocking MCP model and it
18921
+ // never populates the idle timer's ask/background fire conditions, so a cursor
18922
+ // process that produced output then went silent (no result event) is otherwise
18923
+ // bounded only by the ~30-min external reaper. Poll every 5 min; kill after 15
18924
+ // min of continuous silence.
18925
+ resultIdleTimeoutMs: 5 * 6e4,
18926
+ postStartupSilenceTimeoutMs: 15 * 6e4,
18518
18927
  augmentPrompt: (ctx) => {
18519
- const prompt = buildPromptWithImagePathReferences(ctx, imageFiles);
18520
- return ctx.config.teamId?.trim() ? `${prompt}
18521
-
18522
- ${buildCodeDiscoveryEnforcementTail()}` : prompt;
18928
+ const isResume = Boolean(
18929
+ ctx.config.runtimeSessionId?.trim() || ctx.config.providerSessionId?.trim()
18930
+ );
18931
+ const base = buildResumeAwarePrompt(ctx.config, ctx.promptText, { isResume });
18932
+ const prompt = ctx.config.mode === "plan" ? buildPlanModePrefix(base) : base;
18933
+ return appendImagePathReferences(prompt, imageFiles);
18523
18934
  },
18524
18935
  parseStructuredLine: parseCursorStructuredLine
18525
18936
  }).run(context);
@@ -19473,7 +19884,7 @@ function createSupatestCliBackend(command = "supatest", defaultArgs = []) {
19473
19884
  const basePromptText = resumeContextPrefix ? `${resumeContextPrefix}
19474
19885
 
19475
19886
  ${ctx.promptText}` : ctx.promptText;
19476
- const promptText = ctx.config.mode === "plan" ? buildPlanModePrefix(buildPromptWithSystem(ctx.config, basePromptText)) : buildPromptWithSystem(ctx.config, basePromptText);
19887
+ const promptText = ctx.config.mode === "plan" ? buildPlanModePrefix(basePromptText) : basePromptText;
19477
19888
  if (promptText.trim()) {
19478
19889
  contentBlocks.push({ type: "text", text: promptText });
19479
19890
  }
@@ -19658,6 +20069,8 @@ var BaseMachineAgent = class _BaseMachineAgent {
19658
20069
  presenter;
19659
20070
  runtime;
19660
20071
  abortController = null;
20072
+ /** Log the "synced MCP session headers" line once per run(), not per respawn. */
20073
+ mcpHeadersLogged = false;
19661
20074
  constructor(presenter, runtime) {
19662
20075
  this.presenter = presenter;
19663
20076
  this.runtime = runtime;
@@ -19714,7 +20127,52 @@ var BaseMachineAgent = class _BaseMachineAgent {
19714
20127
  getActivityHeartbeatIntervalMs() {
19715
20128
  return AGENT_ACTIVITY_HEARTBEAT_MS;
19716
20129
  }
20130
+ /**
20131
+ * Home directory whose CLI config files receive the Alan MCP session headers.
20132
+ * Defaults to the process home (production behaviour); tests override to a
20133
+ * temp dir so runs never touch the developer's real CLI configs.
20134
+ */
20135
+ getMcpConfigHomeDir() {
20136
+ return void 0;
20137
+ }
20138
+ /**
20139
+ * Re-assert this run's Alan MCP identity in the shared CLI config under the
20140
+ * serialization gate, holding the gate until the provider spawns. Returns the
20141
+ * gate release. Concurrent runs on one machine share ~/.codex/config.toml,
20142
+ * ~/.cursor/mcp.json, and ~/.claude.json; the CLI reads its config once at
20143
+ * spawn, so without serialization a concurrent run could rewrite the shared
20144
+ * file between this write and this spawn and steal this run's session identity.
20145
+ */
20146
+ async beginMcpConfigCriticalSection(config) {
20147
+ const conversationId = config.conversationId;
20148
+ if (!conversationId) return () => {
20149
+ };
20150
+ const releaseGate = await acquireMcpConfigWriteGate();
20151
+ try {
20152
+ const writtenClis = syncAlanMcpSessionHeaders({
20153
+ conversationId,
20154
+ teamId: config.teamId,
20155
+ runHeaders: config.alanMcp?.headers,
20156
+ homeDir: this.getMcpConfigHomeDir()
20157
+ });
20158
+ if (writtenClis.length > 0 && !this.mcpHeadersLogged) {
20159
+ this.mcpHeadersLogged = true;
20160
+ void this.presenter.onLog(`Synced Alan MCP session headers to ${writtenClis.join(", ")}`);
20161
+ }
20162
+ } catch (error) {
20163
+ console.warn("[base-machine-agent] Failed to sync MCP session headers", error);
20164
+ }
20165
+ return releaseGate;
20166
+ }
19717
20167
  async runBackendWithActivityHeartbeat(backend, context) {
20168
+ const releaseMcpGate = await this.beginMcpConfigCriticalSection(context.config);
20169
+ let mcpGateReleased = false;
20170
+ const releaseMcpGateOnce = () => {
20171
+ if (mcpGateReleased) return;
20172
+ mcpGateReleased = true;
20173
+ releaseMcpGate();
20174
+ };
20175
+ const callerOnProcessSpawned = context.onProcessSpawned;
19718
20176
  const intervalMs = this.getActivityHeartbeatIntervalMs();
19719
20177
  let heartbeat = null;
19720
20178
  let livenessProbe = null;
@@ -19733,9 +20191,17 @@ var BaseMachineAgent = class _BaseMachineAgent {
19733
20191
  }
19734
20192
  }, intervalMs);
19735
20193
  }
20194
+ const spawnAwareContext = {
20195
+ ...context,
20196
+ onProcessSpawned: (child) => {
20197
+ releaseMcpGateOnce();
20198
+ callerOnProcessSpawned?.(child);
20199
+ }
20200
+ };
19736
20201
  try {
19737
- return await backend.run(context);
20202
+ return await backend.run(spawnAwareContext);
19738
20203
  } finally {
20204
+ releaseMcpGateOnce();
19739
20205
  if (heartbeat) clearInterval(heartbeat);
19740
20206
  }
19741
20207
  }
@@ -19969,19 +20435,7 @@ ${runtimeConfig.task}` } : {}
19969
20435
  this.presenter.onComplete(result);
19970
20436
  return result;
19971
20437
  }
19972
- if (initialConfig.conversationId) {
19973
- try {
19974
- const writtenClis = syncAlanMcpSessionHeaders({
19975
- conversationId: initialConfig.conversationId,
19976
- teamId: initialConfig.teamId
19977
- });
19978
- if (writtenClis.length > 0) {
19979
- void this.presenter.onLog(`Synced Alan MCP session headers to ${writtenClis.join(", ")}`);
19980
- }
19981
- } catch (error) {
19982
- console.warn("[base-machine-agent] Failed to sync MCP session headers", error);
19983
- }
19984
- }
20438
+ this.mcpHeadersLogged = false;
19985
20439
  try {
19986
20440
  return await this.runCliBackend(initialConfig, safeProjectPath);
19987
20441
  } catch (error) {
@@ -20669,7 +21123,7 @@ function encryptedFileSize(entries) {
20669
21123
  );
20670
21124
  return emptyEnvelopeBytes + base64UrlLength(plaintextBytes);
20671
21125
  }
20672
- var EncryptedEventOutbox = class {
21126
+ var EncryptedEventOutbox = class _EncryptedEventOutbox {
20673
21127
  constructor(path, encodedKey, pushLog = () => {
20674
21128
  }, options = {}) {
20675
21129
  this.path = path;
@@ -20684,6 +21138,11 @@ var EncryptedEventOutbox = class {
20684
21138
  throw new Error("event outbox maxIntermediateEventsPerRun must be a non-negative integer");
20685
21139
  }
20686
21140
  this.entries = this.read();
21141
+ if (!(0, import_node_fs6.existsSync)(this.path)) {
21142
+ this.lastPersistedSignature = _EncryptedEventOutbox.EMPTY_SIGNATURE;
21143
+ } else if (this.entries.length > 0) {
21144
+ this.lastPersistedSignature = this.serializeForPersist().signature;
21145
+ }
20687
21146
  this.enforceCaps();
20688
21147
  this.persist();
20689
21148
  }
@@ -20693,6 +21152,17 @@ var EncryptedEventOutbox = class {
20693
21152
  retryTimer = null;
20694
21153
  /** Latch so a persistent disk failure logs once per streak, not per event. */
20695
21154
  persistFailureLogged = false;
21155
+ /**
21156
+ * Signature of the entries last durably written, so a `persist()` whose entries
21157
+ * are byte-identical to what is already on disk skips the AES-GCM re-encrypt +
21158
+ * temp-file write + rename entirely. Under link flapping the spool is persisted
21159
+ * on every enqueue/ack; this collapses redundant writes (e.g. a reconnect that
21160
+ * replays from head without mutating the queue) to O(delta), not O(spool). Set
21161
+ * to null on any write failure so the next persist retries rather than skips.
21162
+ */
21163
+ lastPersistedSignature = null;
21164
+ /** Sentinel signature for the empty (no-entries) on-disk state. */
21165
+ static EMPTY_SIGNATURE = "empty";
20696
21166
  key;
20697
21167
  maxEncryptedBytes;
20698
21168
  maxIntermediateEventsPerRun;
@@ -20895,20 +21365,28 @@ var EncryptedEventOutbox = class {
20895
21365
  throw new Error(`unable to decrypt durable event outbox: ${detail}`);
20896
21366
  }
20897
21367
  }
21368
+ /** Serialize the current entries to their plaintext + content signature (once). */
21369
+ serializeForPersist() {
21370
+ if (this.entries.length === 0) {
21371
+ return { plaintext: "", signature: _EncryptedEventOutbox.EMPTY_SIGNATURE };
21372
+ }
21373
+ const plaintext = JSON.stringify({ entries: this.entries });
21374
+ return { plaintext, signature: (0, import_node_crypto.createHash)("sha256").update(plaintext).digest("base64url") };
21375
+ }
20898
21376
  persist() {
21377
+ const { plaintext, signature } = this.serializeForPersist();
21378
+ if (signature === this.lastPersistedSignature) return;
20899
21379
  try {
20900
21380
  if (this.entries.length === 0) {
20901
21381
  (0, import_node_fs6.rmSync)(this.path, { force: true });
21382
+ this.lastPersistedSignature = signature;
20902
21383
  this.persistFailureLogged = false;
20903
21384
  return;
20904
21385
  }
20905
21386
  (0, import_node_fs6.mkdirSync)((0, import_node_path5.dirname)(this.path), { recursive: true, mode: 448 });
20906
21387
  const iv = (0, import_node_crypto.randomBytes)(12);
20907
21388
  const cipher = (0, import_node_crypto.createCipheriv)("aes-256-gcm", this.key, iv);
20908
- const ciphertext = Buffer.concat([
20909
- cipher.update(JSON.stringify({ entries: this.entries }), "utf8"),
20910
- cipher.final()
20911
- ]);
21389
+ const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
20912
21390
  const envelope = {
20913
21391
  version: OUTBOX_VERSION,
20914
21392
  iv: iv.toString("base64url"),
@@ -20923,8 +21401,10 @@ var EncryptedEventOutbox = class {
20923
21401
  } finally {
20924
21402
  (0, import_node_fs6.rmSync)(pendingPath, { force: true });
20925
21403
  }
21404
+ this.lastPersistedSignature = signature;
20926
21405
  this.persistFailureLogged = false;
20927
21406
  } catch (error) {
21407
+ this.lastPersistedSignature = null;
20928
21408
  if (!this.persistFailureLogged) {
20929
21409
  this.persistFailureLogged = true;
20930
21410
  const detail = error instanceof Error ? error.message : String(error);
@@ -21637,8 +22117,8 @@ var CONFIG_LOCK_STALE_MS = 3e4;
21637
22117
  var CONFIG_LOCK_WAIT_MS = 2e3;
21638
22118
  var CONFIG_LOCK_POLL_MS = 20;
21639
22119
  function compactProviderVersion(output) {
21640
- const firstLine = output.split(/\r?\n/).map((line) => line.trim()).find(Boolean);
21641
- return firstLine?.slice(0, 120);
22120
+ const firstLine2 = output.split(/\r?\n/).map((line) => line.trim()).find(Boolean);
22121
+ return firstLine2?.slice(0, 120);
21642
22122
  }
21643
22123
  var inspectAlanMcpThroughProviderCli = (backendKind, expectedUrl, home) => {
21644
22124
  const plan = getMcpCliInspectionPlan(backendKind);
@@ -21678,7 +22158,30 @@ var inspectAlanMcpThroughProviderCli = (backendKind, expectedUrl, home) => {
21678
22158
  };
21679
22159
  }
21680
22160
  const providerVersion = compactProviderVersion(versionProbe.stdout);
21681
- const probe = runCliVersionProbe(executable, env, plan.args);
22161
+ const inspectTimeoutMs = Number(process.env.ALAN_MCP_INSPECT_TIMEOUT_MS) || 2e4;
22162
+ const probe = runCliVersionProbe(executable, env, plan.args, inspectTimeoutMs);
22163
+ const timedOut = probe.status === null && (probe.signal === "SIGTERM" || probe.error?.code === "ETIMEDOUT");
22164
+ if (timedOut) {
22165
+ const fileCheck = verifyAlanMcpRegistered(backendKind, home, () => ({
22166
+ ok: true,
22167
+ verification: { method: "file" }
22168
+ }));
22169
+ console.warn("[alan-agent] MCP CLI inspection timed out; falling back to file check", {
22170
+ backendKind,
22171
+ command,
22172
+ inspectTimeoutMs,
22173
+ fileOk: fileCheck.ok
22174
+ });
22175
+ return {
22176
+ ok: fileCheck.ok,
22177
+ issue: fileCheck.ok ? void 0 : {
22178
+ code: "provider_mcp_repair_required",
22179
+ command,
22180
+ message: `Alan tools could not be verified for ${plan.providerLabel}. Repair Alan tools, then retry.`
22181
+ },
22182
+ verification: { method: "file", command, providerVersion }
22183
+ };
22184
+ }
21682
22185
  const classification = classifyMcpCliInspection(
21683
22186
  plan,
21684
22187
  {
@@ -22577,7 +23080,7 @@ var RunStartGate = class {
22577
23080
  };
22578
23081
 
22579
23082
  // src/version.ts
22580
- var AGENT_VERSION = "0.1.42";
23083
+ var AGENT_VERSION = "0.1.43";
22581
23084
 
22582
23085
  // src/workspace-relocation.ts
22583
23086
  var import_node_child_process3 = require("child_process");
@@ -22653,11 +23156,19 @@ var import_node_child_process4 = require("child_process");
22653
23156
  var import_node_fs11 = require("fs");
22654
23157
  var import_node_path9 = require("path");
22655
23158
  var WORKSPACE_LEASE_CHECK_INTERVAL_MS = 1e3;
23159
+ function parsePositiveMs(value2, fallback) {
23160
+ const parsed = value2 ? Number(value2) : Number.NaN;
23161
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
23162
+ }
23163
+ var WORKSPACE_LEASE_GIT_TIMEOUT_MS = parsePositiveMs(
23164
+ process.env.ALAN_WORKSPACE_LEASE_GIT_TIMEOUT_MS,
23165
+ 2e3
23166
+ );
22656
23167
  function gitValue(cwd, args) {
22657
23168
  const result = (0, import_node_child_process4.spawnSync)("git", ["-C", cwd, ...args], {
22658
23169
  encoding: "utf8",
22659
23170
  env: getDaemonCliEnvironment(),
22660
- timeout: 2e3
23171
+ timeout: WORKSPACE_LEASE_GIT_TIMEOUT_MS
22661
23172
  });
22662
23173
  if (result.status !== 0) return null;
22663
23174
  return result.stdout.trim() || null;
@@ -22693,6 +23204,13 @@ var DEFAULT_PROBE2 = {
22693
23204
  } catch {
22694
23205
  return null;
22695
23206
  }
23207
+ },
23208
+ pathExists: (workspacePath) => {
23209
+ try {
23210
+ return (0, import_node_fs11.existsSync)((0, import_node_path9.resolve)(workspacePath));
23211
+ } catch {
23212
+ return true;
23213
+ }
22696
23214
  }
22697
23215
  };
22698
23216
  function unavailableFailure(target) {
@@ -22740,6 +23258,57 @@ var WorkspaceRunLease = class _WorkspaceRunLease {
22740
23258
  failure: null
22741
23259
  };
22742
23260
  }
23261
+ /**
23262
+ * Classify the lease state for the run monitor.
23263
+ *
23264
+ * - `transient: true` — a null snapshot while the workspace path still exists:
23265
+ * the git probe timed out or failed transiently (OS throttling / IO
23266
+ * contention), NOT a vanished checkout. The monitor should retry (require N
23267
+ * consecutive transient failures) before declaring the workspace lost.
23268
+ * - `transient: false` — a definitive change: the path is genuinely missing
23269
+ * (ENOENT), or the workspace/repository/worktree/branch identity changed.
23270
+ * These are real and fail fast.
23271
+ */
23272
+ verifyDetailed() {
23273
+ for (const captured of this.captured) {
23274
+ const current = this.probe.snapshot(captured.target.workspacePath);
23275
+ if (!current) {
23276
+ const pathGone = this.probe.pathExists?.(captured.target.workspacePath) === false;
23277
+ return { failure: unavailableFailure(captured.target), transient: !pathGone };
23278
+ }
23279
+ if (current.workspaceIdentity !== captured.identity.workspaceIdentity) {
23280
+ return {
23281
+ failure: {
23282
+ code: "workspace_lease_path_changed",
23283
+ message: "The selected workspace folder changed while the run was active. The run stopped before continuing; choose the correct folder and start a new run.",
23284
+ workspacePath: captured.target.workspacePath,
23285
+ expectedBranch: captured.identity.branch,
23286
+ actualBranch: current.branch
23287
+ },
23288
+ transient: false
23289
+ };
23290
+ }
23291
+ if (current.repositoryIdentity !== captured.identity.repositoryIdentity || current.worktreeIdentity !== captured.identity.worktreeIdentity) {
23292
+ return {
23293
+ failure: {
23294
+ code: "workspace_lease_repository_changed",
23295
+ message: "The workspace now points to a different repository or worktree. The run stopped before continuing; restore the original checkout and start a new run.",
23296
+ workspacePath: captured.target.workspacePath,
23297
+ expectedBranch: captured.identity.branch,
23298
+ actualBranch: current.branch
23299
+ },
23300
+ transient: false
23301
+ };
23302
+ }
23303
+ if (current.branch !== captured.identity.branch) {
23304
+ return {
23305
+ failure: branchFailure(captured.target, captured.identity.branch, current.branch),
23306
+ transient: false
23307
+ };
23308
+ }
23309
+ }
23310
+ return { failure: null, transient: false };
23311
+ }
22743
23312
  verify() {
22744
23313
  for (const captured of this.captured) {
22745
23314
  const current = this.probe.snapshot(captured.target.workspacePath);
@@ -22769,6 +23338,40 @@ var WorkspaceRunLease = class _WorkspaceRunLease {
22769
23338
  return null;
22770
23339
  }
22771
23340
  };
23341
+ var FRESH_LEASE_MONITOR_STATE = {
23342
+ transientStreak: 0,
23343
+ nextCheckAtMs: 0
23344
+ };
23345
+ function isLeaseCheckDue(state, nowMs) {
23346
+ return nowMs >= state.nextCheckAtMs;
23347
+ }
23348
+ function applyLeaseCheckResult(input) {
23349
+ const { detailed, nowMs, policy } = input;
23350
+ if (!detailed.failure) {
23351
+ return { action: "ok", state: { ...FRESH_LEASE_MONITOR_STATE } };
23352
+ }
23353
+ if (!detailed.transient) {
23354
+ return { action: "fail", state: input.state, failure: detailed.failure };
23355
+ }
23356
+ const transientStreak = input.state.transientStreak + 1;
23357
+ if (transientStreak >= policy.maxTransientFailures) {
23358
+ return {
23359
+ action: "fail",
23360
+ state: { transientStreak, nextCheckAtMs: input.state.nextCheckAtMs },
23361
+ failure: detailed.failure
23362
+ };
23363
+ }
23364
+ const backoffMs = Math.min(
23365
+ policy.backoffBaseMs * 2 ** (transientStreak - 1),
23366
+ policy.backoffMaxMs
23367
+ );
23368
+ return {
23369
+ action: "transient",
23370
+ state: { transientStreak, nextCheckAtMs: nowMs + backoffMs },
23371
+ failure: detailed.failure,
23372
+ backoffMs
23373
+ };
23374
+ }
22772
23375
 
22773
23376
  // src/daemon.ts
22774
23377
  var STAGING_API_URL = process.env.ALAN_STAGING_API_URL ?? "https://staging-api.tryalan.ai";
@@ -24087,9 +24690,12 @@ function reconcileActiveRuns(input) {
24087
24690
  if (!staleByGrace && !staleByDeadPid) continue;
24088
24691
  if (awaitingAsk && staleByGrace && !staleByDeadPid) continue;
24089
24692
  if (staleByDeadPid && awaitingAsk) {
24693
+ entry.presenter.onComplete(buildAbortResult("provider_exited"));
24090
24694
  entry.agent.kill();
24091
24695
  input.activeAgents.delete(runId);
24092
- input.pushLog?.(`reconciled dead provider pid run=${runId} (awaiting ask, no interrupt)`);
24696
+ input.pushLog?.(
24697
+ `reconciled dead provider pid run=${runId} (awaiting ask \u2014 finalized for resume)`
24698
+ );
24093
24699
  continue;
24094
24700
  }
24095
24701
  abortActiveAgent(entry, { reason: staleByDeadPid ? "provider_exited" : "runner_stalled" });
@@ -24830,6 +25436,10 @@ async function startDaemon(args) {
24830
25436
  const versionRefresh = setInterval(() => {
24831
25437
  void refreshProviderVersions();
24832
25438
  }, VERSION_REFRESH_INTERVAL_MS);
25439
+ const parsedMaxTransient = Number(process.env.ALAN_WORKSPACE_LEASE_MAX_TRANSIENT_FAILURES);
25440
+ const WORKSPACE_LEASE_MAX_TRANSIENT_FAILURES = Number.isFinite(parsedMaxTransient) && parsedMaxTransient >= 1 ? Math.floor(parsedMaxTransient) : 3;
25441
+ const WORKSPACE_LEASE_FAILURE_BACKOFF_BASE_MS = 1e3;
25442
+ const WORKSPACE_LEASE_FAILURE_BACKOFF_MAX_MS = 15e3;
24833
25443
  const stopRunForWorkspaceLease = (runId, entry, failure) => {
24834
25444
  entry.stage = "failed";
24835
25445
  if (!entry.presenter.failWorkspaceLease(failure)) return;
@@ -24837,15 +25447,39 @@ async function startDaemon(args) {
24837
25447
  activeAgents.delete(runId);
24838
25448
  pushLog(`workspace-lease-lost run=${runId} code=${failure.code}`);
24839
25449
  };
24840
- const verifyActiveWorkspaceLease = (runId, entry) => {
24841
- const failure = entry.workspaceLease?.verify() ?? null;
24842
- if (!failure) return true;
24843
- stopRunForWorkspaceLease(runId, entry, failure);
24844
- return false;
25450
+ const leaseCheckPolicy = {
25451
+ maxTransientFailures: WORKSPACE_LEASE_MAX_TRANSIENT_FAILURES,
25452
+ backoffBaseMs: WORKSPACE_LEASE_FAILURE_BACKOFF_BASE_MS,
25453
+ backoffMaxMs: WORKSPACE_LEASE_FAILURE_BACKOFF_MAX_MS
25454
+ };
25455
+ const verifyActiveWorkspaceLease = (runId, entry, nowMs) => {
25456
+ const lease = entry.workspaceLease;
25457
+ if (!lease) return true;
25458
+ const state = entry.leaseCheck ?? { ...FRESH_LEASE_MONITOR_STATE };
25459
+ entry.leaseCheck = state;
25460
+ if (!isLeaseCheckDue(state, nowMs)) return true;
25461
+ const decision = applyLeaseCheckResult({
25462
+ state,
25463
+ detailed: lease.verifyDetailed(),
25464
+ nowMs,
25465
+ policy: leaseCheckPolicy
25466
+ });
25467
+ entry.leaseCheck = decision.state;
25468
+ if (decision.action === "fail" && decision.failure) {
25469
+ stopRunForWorkspaceLease(runId, entry, decision.failure);
25470
+ return false;
25471
+ }
25472
+ if (decision.action === "transient" && decision.failure) {
25473
+ pushLog(
25474
+ `workspace-lease-transient run=${runId} streak=${decision.state.transientStreak}/${WORKSPACE_LEASE_MAX_TRANSIENT_FAILURES} code=${decision.failure.code} backoff_ms=${decision.backoffMs ?? 0}`
25475
+ );
25476
+ }
25477
+ return true;
24845
25478
  };
24846
25479
  const workspaceLeaseMonitor = setInterval(() => {
25480
+ const nowMs = Date.now();
24847
25481
  for (const [runId, entry] of activeAgents) {
24848
- verifyActiveWorkspaceLease(runId, entry);
25482
+ verifyActiveWorkspaceLease(runId, entry, nowMs);
24849
25483
  }
24850
25484
  }, WORKSPACE_LEASE_CHECK_INTERVAL_MS);
24851
25485
  workspaceLeaseMonitor.unref?.();
@@ -25322,7 +25956,7 @@ async function startDaemon(args) {
25322
25956
  },
25323
25957
  () => {
25324
25958
  const entry = activeAgents.get(payload.runId);
25325
- return entry ? verifyActiveWorkspaceLease(payload.runId, entry) : false;
25959
+ return entry ? verifyActiveWorkspaceLease(payload.runId, entry, Date.now()) : false;
25326
25960
  }
25327
25961
  );
25328
25962
  let agent;
@@ -25390,6 +26024,7 @@ async function startDaemon(args) {
25390
26024
  taskMeta: payload.taskMeta,
25391
26025
  prMeta: payload.prMeta,
25392
26026
  conversationId: payload.conversationId,
26027
+ alanMcp: payload.alanMcp,
25393
26028
  taskId: payload.taskId ?? payload.taskMeta?.id,
25394
26029
  teamId: payload.teamId,
25395
26030
  currentUser: payload.currentUser,
@@ -25441,7 +26076,7 @@ async function startDaemon(args) {
25441
26076
  ([, entry]) => entry.conversationId === payload.conversationId
25442
26077
  ) : [...activeAgents.entries()];
25443
26078
  const delivered = entries.some(([runId, entry]) => {
25444
- if (!entry || !verifyActiveWorkspaceLease(runId, entry)) return false;
26079
+ if (!entry || !verifyActiveWorkspaceLease(runId, entry, Date.now())) return false;
25445
26080
  return entry.agent.sendToolResponse(payload.toolId, payload.response) === true;
25446
26081
  });
25447
26082
  if (!delivered) {
@@ -26208,6 +26843,18 @@ function omitUndefined(record) {
26208
26843
  return Object.fromEntries(Object.entries(record).filter(([, value2]) => value2 !== void 0));
26209
26844
  }
26210
26845
 
26846
+ // src/resume-fallback-env.ts
26847
+ var RESUME_FALLBACK_CONTEXT_ENV = "ALAN_RESUME_FALLBACK_CONTEXT_B64";
26848
+ function decodeResumeFallbackContext(encoded) {
26849
+ if (!encoded) return void 0;
26850
+ try {
26851
+ const decoded = Buffer.from(encoded, "base64").toString("utf8").trim();
26852
+ return decoded.length > 0 ? decoded : void 0;
26853
+ } catch {
26854
+ return void 0;
26855
+ }
26856
+ }
26857
+
26211
26858
  // src/web-presenter.ts
26212
26859
  var WebPresenter = class {
26213
26860
  constructor(wsClient, _conversationId, cwd) {
@@ -26500,6 +27147,7 @@ var SandboxEventDispatcher = class {
26500
27147
  fireAndForget(event);
26501
27148
  return;
26502
27149
  }
27150
+ if (isBackgroundHeartbeat(event) && !this.outboxSocket.connected) return;
26503
27151
  this.outbox.enqueue(this.conversationId, this.currentRunId ?? this.conversationId, event);
26504
27152
  this.outbox.flush(this.outboxSocket);
26505
27153
  }
@@ -26619,7 +27267,7 @@ var WSClient = class {
26619
27267
  if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
26620
27268
  this.heartbeatTimer = setInterval(() => {
26621
27269
  if (!this.socket.connected) return;
26622
- this.socket.emit("agent.heartbeat", this.buildHeartbeat());
27270
+ this.socket.volatile.emit("agent.heartbeat", this.buildHeartbeat());
26623
27271
  }, AGENT_HEARTBEAT_INTERVAL_MS);
26624
27272
  this.heartbeatTimer.unref?.();
26625
27273
  }
@@ -26702,6 +27350,7 @@ var WSClient = class {
26702
27350
  images: data.images,
26703
27351
  files: data.files,
26704
27352
  providerSessionId: data.providerSessionId,
27353
+ resumeFallbackContext: data.resumeFallbackContext,
26705
27354
  backendKind: data.backendKind,
26706
27355
  agentId: data.agentId,
26707
27356
  agentPrompt: data.agentPrompt,
@@ -26893,6 +27542,7 @@ async function runSandbox(config) {
26893
27542
  const presenter = new WebPresenter(wsClient, config.sessionId, config.projectPath);
26894
27543
  presenter.setSuppressSessionLifecycle(true);
26895
27544
  let lastProviderSessionId = config.providerSessionId;
27545
+ let resumeFallbackContext = config.resumeFallbackContext;
26896
27546
  let activeBackendKind = config.backendKind || "claude_cli";
26897
27547
  let activeAgentId = config.agentId;
26898
27548
  let activeAgentPrompt = config.agentPrompt;
@@ -26976,10 +27626,12 @@ async function runSandbox(config) {
26976
27626
  selectedContextWindow: activeContextWindow ?? null,
26977
27627
  selectedEffortLevel: activeEffortLevel ?? null,
26978
27628
  providerSessionId: lastProviderSessionId ?? void 0,
27629
+ resumeFallbackContext,
26979
27630
  taskMeta: currentTaskMeta,
26980
27631
  prMeta: currentPrMeta,
26981
27632
  taskId: currentTaskId,
26982
27633
  conversationId: currentConversationId,
27634
+ alanMcp: currentAlanMcp,
26983
27635
  teamId: currentTeamId,
26984
27636
  workflowId,
26985
27637
  workflowExecutionId,
@@ -27027,6 +27679,7 @@ async function runSandbox(config) {
27027
27679
  }
27028
27680
  }
27029
27681
  currentAgent = null;
27682
+ resumeFallbackContext = void 0;
27030
27683
  lifecycle.info("sandbox_agent_run_complete", {
27031
27684
  success: result.success,
27032
27685
  iterations: result.iterations,
@@ -27053,6 +27706,7 @@ async function runSandbox(config) {
27053
27706
  currentFiles = nextPayload.files ?? [];
27054
27707
  if (nextPayload.providerSessionId) {
27055
27708
  lastProviderSessionId = nextPayload.providerSessionId;
27709
+ resumeFallbackContext = nextPayload.resumeFallbackContext;
27056
27710
  }
27057
27711
  if (nextPayload.backendKind) {
27058
27712
  activeBackendKind = nextPayload.backendKind;
@@ -27391,6 +28045,9 @@ async function runSessionFromEnv() {
27391
28045
  const contextWindow = process.env.ALAN_CONTEXT_WINDOW || void 0;
27392
28046
  const effortLevel = process.env.ALAN_EFFORT_LEVEL || void 0;
27393
28047
  const providerSessionId = process.env.ALAN_PROVIDER_SESSION_ID || void 0;
28048
+ const resumeFallbackContext = decodeResumeFallbackContext(
28049
+ process.env[RESUME_FALLBACK_CONTEXT_ENV]
28050
+ );
27394
28051
  const workflowId = process.env.ALAN_WORKFLOW_ID || void 0;
27395
28052
  const workflowExecutionId = process.env.ALAN_WORKFLOW_EXECUTION_ID || void 0;
27396
28053
  const sandboxId = process.env.ALAN_SANDBOX_ID || void 0;
@@ -27432,6 +28089,7 @@ async function runSessionFromEnv() {
27432
28089
  contextWindow,
27433
28090
  effortLevel,
27434
28091
  providerSessionId,
28092
+ resumeFallbackContext,
27435
28093
  workflowId,
27436
28094
  workflowExecutionId,
27437
28095
  sandboxId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiden-ade/sandbox-agent",
3
- "version": "0.1.42",
3
+ "version": "0.1.43",
4
4
  "type": "module",
5
5
  "description": "Alan agent runtime — cloud sandbox and local daemon (alan-agent CLI)",
6
6
  "bin": {