@aiden-ade/sandbox-agent 0.1.42 → 0.1.44

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 +1486 -210
  2. package/package.json +3 -3
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.",
@@ -15305,6 +15296,8 @@ var import_path5 = require("path");
15305
15296
  var import_fs6 = require("fs");
15306
15297
  var import_os6 = require("os");
15307
15298
  var import_path6 = require("path");
15299
+ var import_readline2 = require("readline");
15300
+ var import_url2 = require("url");
15308
15301
  var import_fs7 = require("fs");
15309
15302
  var import_os7 = require("os");
15310
15303
  var import_path7 = require("path");
@@ -15443,6 +15436,11 @@ function buildSessionHeaders(input) {
15443
15436
  if (input.allowedTools?.length) {
15444
15437
  headers["x-allowed-tools"] = input.allowedTools.join(",");
15445
15438
  }
15439
+ if (input.runHeaders) {
15440
+ for (const [key, value2] of Object.entries(input.runHeaders)) {
15441
+ if (typeof value2 === "string" && value2.length > 0) headers[key] = value2;
15442
+ }
15443
+ }
15446
15444
  return headers;
15447
15445
  }
15448
15446
  function getTomlSectionName(line) {
@@ -15653,6 +15651,23 @@ function syncAlanMcpSessionHeaders(input) {
15653
15651
  }
15654
15652
  return written;
15655
15653
  }
15654
+ var mcpConfigWriteGateTail = Promise.resolve();
15655
+ function acquireMcpConfigWriteGate() {
15656
+ const previous = mcpConfigWriteGateTail;
15657
+ let releaseCurrent;
15658
+ const current = new Promise((resolve22) => {
15659
+ releaseCurrent = resolve22;
15660
+ });
15661
+ mcpConfigWriteGateTail = previous.then(() => current);
15662
+ return previous.then(() => {
15663
+ let released = false;
15664
+ return () => {
15665
+ if (released) return;
15666
+ released = true;
15667
+ releaseCurrent();
15668
+ };
15669
+ });
15670
+ }
15656
15671
  function buildPromptWithSystem(config, promptText) {
15657
15672
  const parts2 = [
15658
15673
  config.systemPrompt?.trim(),
@@ -15661,6 +15676,13 @@ function buildPromptWithSystem(config, promptText) {
15661
15676
  ].filter((value2) => Boolean(value2 && value2.length > 0));
15662
15677
  return parts2.join("\n\n");
15663
15678
  }
15679
+ function buildResumeAwarePrompt(config, promptText, options) {
15680
+ const systemParts = options.isResume ? [config.systemPromptAppend?.trim()] : [config.systemPrompt?.trim()];
15681
+ const parts2 = [...systemParts, promptText.trim()].filter(
15682
+ (value2) => Boolean(value2 && value2.length > 0)
15683
+ );
15684
+ return parts2.join("\n\n");
15685
+ }
15664
15686
  function buildPlanModePrefix(promptText) {
15665
15687
  return [
15666
15688
  "You are in plan-only mode.",
@@ -15827,7 +15849,7 @@ var ERROR_SPECS = {
15827
15849
  recoveryClass: "needs_env"
15828
15850
  },
15829
15851
  auth_expired: {
15830
- message: "The provider connection expired. Reconnect it in Cloud settings, then restart or recreate the machine.",
15852
+ message: "The provider connection expired. Re-authenticate the agent CLI on this runtime (or reconnect it in your provider/Cloud settings), then retry.",
15831
15853
  recoveryClass: "user_fixable"
15832
15854
  },
15833
15855
  subscription_required: {
@@ -15917,7 +15939,7 @@ function normalizeCodexCliErrorMessage(message) {
15917
15939
  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
15940
  }
15919
15941
  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.";
15942
+ 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
15943
  }
15922
15944
  return message;
15923
15945
  }
@@ -16298,6 +16320,9 @@ function createGenericCliBackend(options) {
16298
16320
  } else if (state.resultDeferredOnBackgroundWork && state.activeBackgroundTaskIds?.size) {
16299
16321
  idleTimeoutReason = "background_task";
16300
16322
  resolve22("idle_timeout");
16323
+ } else if (options.postStartupSilenceTimeoutMs && sawStdoutLine && typeof state.lastRawOutputAtMs === "number" && Date.now() - state.lastRawOutputAtMs >= options.postStartupSilenceTimeoutMs) {
16324
+ idleTimeoutReason = "post_startup_silence";
16325
+ resolve22("idle_timeout");
16301
16326
  } else {
16302
16327
  armIdleTimer();
16303
16328
  }
@@ -16310,8 +16335,9 @@ function createGenericCliBackend(options) {
16310
16335
  if (idleTimer) clearTimeout(idleTimer);
16311
16336
  if (raceResult === "idle_timeout") {
16312
16337
  idleTimedOut = true;
16338
+ const idleWaitLabel = idleTimeoutReason === "background_task" ? "a background task terminal event" : idleTimeoutReason === "post_startup_silence" ? "any further output (silent hang)" : "a pending user answer";
16313
16339
  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`
16340
+ `[${options.kind}] Idle timeout (${IDLE_MS}ms) waiting for ${idleWaitLabel} \u2014 force-killing orphaned process`
16315
16341
  );
16316
16342
  killProcessTree(child.pid, { signal: "SIGKILL", child });
16317
16343
  exitCode = await Promise.race([
@@ -16384,6 +16410,25 @@ function createGenericCliBackend(options) {
16384
16410
  console.error(`[${options.kind}] Process force-killed by startup watchdog`, {
16385
16411
  stderrLines: stderrLines.slice(-10)
16386
16412
  });
16413
+ const startupStderr = stderrLines.join("\n").trim();
16414
+ if (startupStderr) {
16415
+ const classified = classifyCliErrorDetailed(startupStderr, exitCode);
16416
+ return {
16417
+ success: false,
16418
+ summary: state.summary.trim() || classified.message,
16419
+ filesModified: [],
16420
+ planFilesCreated: [],
16421
+ iterations: Math.max(state.iterations, 1),
16422
+ error: classified.message,
16423
+ ...classified.errorKind !== "unknown_cli_error" ? { errorKind: classified.errorKind } : {},
16424
+ recoveryClass: classified.recoveryClass,
16425
+ providerSessionId: state.runtimeSessionId,
16426
+ runtimeSessionId: state.runtimeSessionId,
16427
+ backendKind: options.kind,
16428
+ supportTier: options.supportTier,
16429
+ usage: state.usage
16430
+ };
16431
+ }
16387
16432
  return {
16388
16433
  success: false,
16389
16434
  summary: "Agent produced no output",
@@ -16433,7 +16478,7 @@ function createGenericCliBackend(options) {
16433
16478
  filesModified: [],
16434
16479
  planFilesCreated: [],
16435
16480
  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.",
16481
+ 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
16482
  providerSessionId: state.runtimeSessionId,
16438
16483
  runtimeSessionId: state.runtimeSessionId,
16439
16484
  backendKind: options.kind,
@@ -17141,6 +17186,16 @@ var TERMINAL_TASK_NOTIFICATION_STATUSES = /* @__PURE__ */ new Set([
17141
17186
  "cancelled",
17142
17187
  "canceled"
17143
17188
  ]);
17189
+ var NON_TERMINAL_TASK_NOTIFICATION_STATUSES = /* @__PURE__ */ new Set([
17190
+ "running",
17191
+ "in_progress",
17192
+ "inprogress",
17193
+ "started",
17194
+ "pending",
17195
+ "active",
17196
+ "working",
17197
+ "queued"
17198
+ ]);
17144
17199
  function extractToolResultContent(content) {
17145
17200
  if (typeof content === "string") return content;
17146
17201
  if (!Array.isArray(content)) return "";
@@ -17158,6 +17213,7 @@ function stringField2(record, key) {
17158
17213
  function isTerminalTaskNotification(record) {
17159
17214
  const status = stringField2(record, "status")?.toLowerCase();
17160
17215
  if (status && TERMINAL_TASK_NOTIFICATION_STATUSES.has(status)) return true;
17216
+ if (status && NON_TERMINAL_TASK_NOTIFICATION_STATUSES.has(status)) return false;
17161
17217
  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
17218
  }
17163
17219
  function isFailedTaskNotification(record) {
@@ -17249,20 +17305,25 @@ function handleClaudeStructuredEvent(parsed, context, state) {
17249
17305
  state.iterations += 1;
17250
17306
  const message = typeof parsed.message === "object" && parsed.message !== null ? parsed.message : null;
17251
17307
  const content = Array.isArray(message?.content) ? message.content : [];
17308
+ let previousBlockWasText = false;
17252
17309
  for (const block of content) {
17253
17310
  if (typeof block !== "object" || block === null || !("type" in block)) continue;
17254
17311
  if (block.type === "text" && typeof block.text === "string") {
17255
17312
  const text = block.text;
17256
17313
  state.summary += `${text}
17257
17314
  `;
17258
- void presenter.onAssistantText(text);
17315
+ void presenter.onAssistantText(previousBlockWasText ? `
17316
+ ${text}` : text);
17317
+ previousBlockWasText = true;
17259
17318
  continue;
17260
17319
  }
17261
17320
  if (block.type === "thinking" && typeof block.thinking === "string") {
17321
+ previousBlockWasText = false;
17262
17322
  void presenter.onThinking(block.thinking);
17263
17323
  continue;
17264
17324
  }
17265
17325
  if (block.type === "tool_use" && typeof block.id === "string" && typeof block.name === "string") {
17326
+ previousBlockWasText = false;
17266
17327
  const toolBlock = block;
17267
17328
  void presenter.onToolUse(
17268
17329
  toolBlock.name,
@@ -17378,7 +17439,7 @@ function handleClaudeStructuredEvent(parsed, context, state) {
17378
17439
  state.iterations = parsed.num_turns;
17379
17440
  }
17380
17441
  const subtype = typeof parsed.subtype === "string" ? parsed.subtype : "";
17381
- if ((subtype === "error_during_execution" || subtype === "error_max_turns") && !state.error) {
17442
+ if (subtype.startsWith("error") && !state.error) {
17382
17443
  const errObj = typeof parsed.error === "object" && parsed.error !== null ? parsed.error : null;
17383
17444
  const resumeId = context.config.runtimeSessionId?.trim() || context.config.providerSessionId?.trim();
17384
17445
  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 +17590,7 @@ function createClaudeCliBackend(command = "claude", defaultArgs = []) {
17529
17590
  const basePromptText = resumeContextPrefix ? `${resumeContextPrefix}
17530
17591
 
17531
17592
  ${ctx.promptText}` : ctx.promptText;
17532
- const promptText = ctx.config.mode === "plan" ? buildPlanModePrefix(buildPromptWithSystem(ctx.config, basePromptText)) : buildPromptWithSystem(ctx.config, basePromptText);
17593
+ const promptText = ctx.config.mode === "plan" ? buildPlanModePrefix(basePromptText) : basePromptText;
17533
17594
  if (promptText.trim()) {
17534
17595
  contentBlocks.push({ type: "text", text: promptText });
17535
17596
  }
@@ -17620,6 +17681,25 @@ function buildGeneratedImageFromCodexPayload(payload) {
17620
17681
  idFields: []
17621
17682
  });
17622
17683
  }
17684
+ function extractRolloutMessageText(payload) {
17685
+ const content = payload.content;
17686
+ if (typeof content === "string") return content;
17687
+ if (Array.isArray(content)) {
17688
+ for (const block of content) {
17689
+ if (block && typeof block === "object" && typeof block.text === "string") {
17690
+ return block.text;
17691
+ }
17692
+ }
17693
+ }
17694
+ return "";
17695
+ }
17696
+ function isSyntheticUserRolloutMessage(payload) {
17697
+ const text = extractRolloutMessageText(payload).trimStart();
17698
+ if (!text) return false;
17699
+ const lower = text.toLowerCase();
17700
+ return lower.startsWith("<environment_context") || lower.startsWith("<user_instructions") || // Auto-compaction bridge summaries are tagged in the rollout content.
17701
+ lower.startsWith("<compact") || lower.startsWith("[compact");
17702
+ }
17623
17703
  function latestUserMessageLineIndex(lines) {
17624
17704
  let latestIndex = -1;
17625
17705
  for (let index = 0; index < lines.length; index += 1) {
@@ -17627,7 +17707,7 @@ function latestUserMessageLineIndex(lines) {
17627
17707
  if (!line) continue;
17628
17708
  const entry = parseJsonObject(line);
17629
17709
  const payload = entry && typeof entry.payload === "object" && entry.payload !== null ? entry.payload : null;
17630
- if (payload?.type === "message" && payload.role === "user") {
17710
+ if (payload?.type === "message" && payload.role === "user" && !isSyntheticUserRolloutMessage(payload)) {
17631
17711
  latestIndex = index;
17632
17712
  }
17633
17713
  }
@@ -17691,7 +17771,9 @@ async function replayCodexMcpToolEventsFromSessionLog(context, state) {
17691
17771
  }
17692
17772
  }
17693
17773
  var CODEX_EXIT_GRACE_MS = 3e4;
17694
- function armCodexExitGraceKill(state) {
17774
+ var CODEX_BACKGROUND_EXIT_GRACE_MS = 10 * 6e4;
17775
+ var CODEX_EXIT_GRACE_SIGKILL_MS = 3e3;
17776
+ function armCodexExitGraceKill(state, graceMs = CODEX_EXIT_GRACE_MS) {
17695
17777
  if (state.exitGraceKillTimer) return;
17696
17778
  const child = state.process;
17697
17779
  if (!child) return;
@@ -17701,7 +17783,15 @@ function armCodexExitGraceKill(state) {
17701
17783
  "[codex_app_server] Process did not exit after turn completion; killing process group"
17702
17784
  );
17703
17785
  killProcessTree(child.pid, { signal: "SIGTERM", child });
17704
- }, CODEX_EXIT_GRACE_MS);
17786
+ const killTimer = setTimeout(() => {
17787
+ if (child.exitCode !== null) return;
17788
+ console.warn(
17789
+ "[codex_app_server] Process still alive after grace SIGTERM; escalating to SIGKILL"
17790
+ );
17791
+ killProcessTree(child.pid, { signal: "SIGKILL", child });
17792
+ }, CODEX_EXIT_GRACE_SIGKILL_MS);
17793
+ killTimer.unref?.();
17794
+ }, graceMs);
17705
17795
  timer.unref?.();
17706
17796
  state.exitGraceKillTimer = timer;
17707
17797
  }
@@ -17710,6 +17800,167 @@ function disarmCodexExitGraceKill(state) {
17710
17800
  clearTimeout(state.exitGraceKillTimer);
17711
17801
  state.exitGraceKillTimer = void 0;
17712
17802
  }
17803
+ function completeCodexTurnRespectingWorkers(state) {
17804
+ disarmCodexExitGraceKill(state);
17805
+ const activeWorkers = state.activeBackgroundTaskIds?.size ?? 0;
17806
+ if (activeWorkers > 0) {
17807
+ state.resultDeferredOnBackgroundWork = true;
17808
+ state.codexAwaitingWorkersAfterTurnEnd = true;
17809
+ console.info(
17810
+ `[codex_app_server] Turn completed with ${activeWorkers} background worker id(s) still active \u2014 deferring exit grace`
17811
+ );
17812
+ armCodexExitGraceKill(state, CODEX_BACKGROUND_EXIT_GRACE_MS);
17813
+ return;
17814
+ }
17815
+ armCodexExitGraceKill(state);
17816
+ }
17817
+ function touchCodexBackgroundGrace(state) {
17818
+ if (!state.codexAwaitingWorkersAfterTurnEnd) return;
17819
+ disarmCodexExitGraceKill(state);
17820
+ armCodexExitGraceKill(state, CODEX_BACKGROUND_EXIT_GRACE_MS);
17821
+ }
17822
+ function maybeReleaseCodexDeferredTurn(state) {
17823
+ if (!state.codexAwaitingWorkersAfterTurnEnd) return;
17824
+ if (state.activeBackgroundTaskIds?.size) return;
17825
+ state.codexAwaitingWorkersAfterTurnEnd = false;
17826
+ state.resultDeferredOnBackgroundWork = false;
17827
+ disarmCodexExitGraceKill(state);
17828
+ armCodexExitGraceKill(state);
17829
+ console.info(
17830
+ "[codex_app_server] All background workers finished after turn completion \u2014 arming normal exit grace"
17831
+ );
17832
+ }
17833
+ var TERMINAL_COLLAB_AGENT_STATUSES = /* @__PURE__ */ new Set([
17834
+ "completed",
17835
+ "errored",
17836
+ "shutdown",
17837
+ "interrupted",
17838
+ "not_found"
17839
+ ]);
17840
+ var FAILED_COLLAB_AGENT_STATUSES = /* @__PURE__ */ new Set(["errored", "interrupted", "not_found"]);
17841
+ var COLLAB_COORDINATION_TOOL_NAMES = {
17842
+ send_input: "collab_send_input",
17843
+ wait: "collab_wait",
17844
+ close_agent: "collab_close_agent"
17845
+ };
17846
+ function firstLine(text, maxLength = 140) {
17847
+ const line = text.split("\n").find((candidate) => candidate.trim().length > 0)?.trim() ?? "";
17848
+ return line.length > maxLength ? `${line.slice(0, maxLength - 1)}\u2026` : line;
17849
+ }
17850
+ function finishCodexWorker(context, state, options) {
17851
+ const launcherId = options.launcherId ?? (options.threadId ? state.backgroundToolIdByTaskId?.get(options.threadId) : void 0) ?? (options.agentPath ? state.codexWorkerToolIdByPath?.get(options.agentPath) : void 0);
17852
+ if (!launcherId) {
17853
+ if (options.threadId) clearBackgroundTaskIds(state, [options.threadId]);
17854
+ maybeReleaseCodexDeferredTurn(state);
17855
+ return;
17856
+ }
17857
+ const launcherStillActive = state.activeBackgroundTaskIds?.has(launcherId) === true || (state.backgroundTaskIdsByToolId?.get(launcherId)?.size ?? 0) > 0;
17858
+ if (!launcherStillActive) {
17859
+ if (options.threadId) clearBackgroundTaskIds(state, [options.threadId]);
17860
+ maybeReleaseCodexDeferredTurn(state);
17861
+ return;
17862
+ }
17863
+ clearBackgroundTaskIds(state, [options.threadId ?? launcherId]);
17864
+ const remainingSiblings = state.backgroundTaskIdsByToolId?.get(launcherId)?.size ?? 0;
17865
+ if (remainingSiblings === 0) {
17866
+ clearBackgroundTaskIds(state, [launcherId]);
17867
+ const isError = FAILED_COLLAB_AGENT_STATUSES.has(options.status);
17868
+ const resultText = options.message?.trim() || `Background worker ${options.status}.`;
17869
+ void context.presenter.onToolResult?.(launcherId, resultText, isError);
17870
+ }
17871
+ maybeReleaseCodexDeferredTurn(state);
17872
+ }
17873
+ function applyCodexCollabAgentStates(context, state, agentsStates) {
17874
+ for (const [threadId, raw] of Object.entries(agentsStates)) {
17875
+ if (!raw || typeof raw !== "object") continue;
17876
+ const entry = raw;
17877
+ const status = typeof entry.status === "string" ? entry.status : "";
17878
+ if (!TERMINAL_COLLAB_AGENT_STATUSES.has(status)) continue;
17879
+ finishCodexWorker(context, state, {
17880
+ threadId,
17881
+ status,
17882
+ message: typeof entry.message === "string" ? entry.message : void 0
17883
+ });
17884
+ }
17885
+ }
17886
+ function summarizeCodexAgentStates(agentsStates) {
17887
+ return Object.entries(agentsStates).map(([threadId, raw]) => {
17888
+ const entry = raw && typeof raw === "object" ? raw : {};
17889
+ const status = typeof entry.status === "string" ? entry.status : "unknown";
17890
+ const message = typeof entry.message === "string" && entry.message.trim() ? ` \u2014 ${firstLine(entry.message)}` : "";
17891
+ return `${threadId}: ${status}${message}`;
17892
+ }).join("\n");
17893
+ }
17894
+ function handleCodexCollabToolCallItem(context, state, item, phase) {
17895
+ const itemId = typeof item.id === "string" ? item.id : "";
17896
+ const tool = typeof item.tool === "string" ? item.tool : "";
17897
+ if (!itemId || !tool) return true;
17898
+ const senderThreadId = typeof item.sender_thread_id === "string" ? item.sender_thread_id : "";
17899
+ const receiverThreadIds = Array.isArray(item.receiver_thread_ids) ? item.receiver_thread_ids.filter(
17900
+ (value2) => typeof value2 === "string" && value2.length > 0
17901
+ ) : [];
17902
+ const prompt = typeof item.prompt === "string" ? item.prompt : "";
17903
+ const agentsStates = typeof item.agents_states === "object" && item.agents_states !== null ? item.agents_states : {};
17904
+ const itemStatus = typeof item.status === "string" ? item.status : "";
17905
+ if (tool === "spawn_agent") {
17906
+ if (trackCodexStreamedToolId(state, itemId)) {
17907
+ const parentToolUseId = senderThreadId ? state.backgroundToolIdByTaskId?.get(senderThreadId) ?? null : null;
17908
+ void context.presenter.onToolUse(
17909
+ "Agent",
17910
+ {
17911
+ subagent_type: "codex-worker",
17912
+ description: firstLine(prompt) || "Codex background worker",
17913
+ ...prompt ? { prompt } : {},
17914
+ ...receiverThreadIds.length > 0 ? { agent_thread_ids: receiverThreadIds } : {}
17915
+ },
17916
+ itemId,
17917
+ parentToolUseId
17918
+ );
17919
+ registerBackgroundLauncher(state, itemId);
17920
+ }
17921
+ for (const threadId of receiverThreadIds) {
17922
+ if (state.backgroundToolIdByTaskId?.get(threadId)) continue;
17923
+ registerBackgroundTaskRecord(state, { task_id: threadId, tool_use_id: itemId });
17924
+ }
17925
+ applyCodexCollabAgentStates(context, state, agentsStates);
17926
+ if (phase === "completed" && itemStatus === "failed") {
17927
+ finishCodexWorker(context, state, {
17928
+ launcherId: itemId,
17929
+ status: "errored",
17930
+ message: "Failed to spawn background worker."
17931
+ });
17932
+ }
17933
+ return true;
17934
+ }
17935
+ const coordinationToolName = COLLAB_COORDINATION_TOOL_NAMES[tool];
17936
+ if (!coordinationToolName) return false;
17937
+ if (trackCodexStreamedToolId(state, itemId)) {
17938
+ void context.presenter.onToolUse(
17939
+ coordinationToolName,
17940
+ {
17941
+ ...receiverThreadIds.length > 0 ? { receiver_thread_ids: receiverThreadIds } : {},
17942
+ ...prompt ? { prompt } : {}
17943
+ },
17944
+ itemId
17945
+ );
17946
+ }
17947
+ if (phase === "completed") {
17948
+ const isError = itemStatus === "failed";
17949
+ const summary = summarizeCodexAgentStates(agentsStates);
17950
+ void context.presenter.onToolResult?.(
17951
+ itemId,
17952
+ summary || (isError ? "Failed." : "Done."),
17953
+ isError
17954
+ );
17955
+ }
17956
+ applyCodexCollabAgentStates(context, state, agentsStates);
17957
+ if (tool === "close_agent" && phase === "completed" && itemStatus !== "failed") {
17958
+ for (const threadId of receiverThreadIds) {
17959
+ finishCodexWorker(context, state, { threadId, status: "shutdown" });
17960
+ }
17961
+ }
17962
+ return true;
17963
+ }
17713
17964
  function trackCodexStreamedToolId(state, toolId) {
17714
17965
  if (!state.codexStreamedToolIds) state.codexStreamedToolIds = /* @__PURE__ */ new Set();
17715
17966
  if (state.codexStreamedToolIds.has(toolId)) return false;
@@ -17740,6 +17991,7 @@ function handleCodexStructuredEvent(parsed, context, state) {
17740
17991
  const presenter = context.presenter;
17741
17992
  const type = typeof parsed.type === "string" ? parsed.type : "";
17742
17993
  if (!type) return false;
17994
+ touchCodexBackgroundGrace(state);
17743
17995
  if (typeof parsed.thread_id === "string") {
17744
17996
  state.runtimeSessionId = parsed.thread_id;
17745
17997
  }
@@ -17764,7 +18016,12 @@ function handleCodexStructuredEvent(parsed, context, state) {
17764
18016
  return true;
17765
18017
  case "turn.started":
17766
18018
  disarmCodexExitGraceKill(state);
17767
- state.iterations += 1;
18019
+ state.codexAwaitingWorkersAfterTurnEnd = false;
18020
+ state.resultDeferredOnBackgroundWork = false;
18021
+ if (state.codexIterationEventFamily !== "legacy") {
18022
+ state.codexIterationEventFamily = "modern";
18023
+ state.iterations += 1;
18024
+ }
17768
18025
  return true;
17769
18026
  case "session_configured":
17770
18027
  if (typeof parsed.session_id === "string") {
@@ -17773,7 +18030,12 @@ function handleCodexStructuredEvent(parsed, context, state) {
17773
18030
  return true;
17774
18031
  case "task_started":
17775
18032
  disarmCodexExitGraceKill(state);
17776
- state.iterations += 1;
18033
+ state.codexAwaitingWorkersAfterTurnEnd = false;
18034
+ state.resultDeferredOnBackgroundWork = false;
18035
+ if (state.codexIterationEventFamily !== "modern") {
18036
+ state.codexIterationEventFamily = "legacy";
18037
+ state.iterations += 1;
18038
+ }
17777
18039
  return true;
17778
18040
  case "item.started": {
17779
18041
  const item = typeof parsed.item === "object" && parsed.item !== null ? parsed.item : null;
@@ -17800,6 +18062,8 @@ function handleCodexStructuredEvent(parsed, context, state) {
17800
18062
  }
17801
18063
  return true;
17802
18064
  }
18065
+ case "collab_tool_call":
18066
+ return handleCodexCollabToolCallItem(context, state, item, "started");
17803
18067
  // Text-bearing and patch/todo items carry no useful live-start payload; they
17804
18068
  // are surfaced on item.completed. Recognized (do not count as unhandled).
17805
18069
  case "reasoning":
@@ -17811,6 +18075,21 @@ function handleCodexStructuredEvent(parsed, context, state) {
17811
18075
  return false;
17812
18076
  }
17813
18077
  }
18078
+ case "item.updated": {
18079
+ const item = typeof parsed.item === "object" && parsed.item !== null ? parsed.item : null;
18080
+ if (!item || typeof item.type !== "string") return true;
18081
+ switch (item.type) {
18082
+ case "collab_tool_call":
18083
+ return handleCodexCollabToolCallItem(context, state, item, "updated");
18084
+ case "todo_list": {
18085
+ const todos = Array.isArray(item.items) ? item.items : [];
18086
+ void presenter.onTodoWrite?.(todos);
18087
+ return true;
18088
+ }
18089
+ default:
18090
+ return false;
18091
+ }
18092
+ }
17814
18093
  case "item.completed": {
17815
18094
  const item = typeof parsed.item === "object" && parsed.item !== null ? parsed.item : null;
17816
18095
  if (!item || typeof item.type !== "string") return true;
@@ -17875,6 +18154,8 @@ function handleCodexStructuredEvent(parsed, context, state) {
17875
18154
  void presenter.onTodoWrite?.(todos);
17876
18155
  return true;
17877
18156
  }
18157
+ case "collab_tool_call":
18158
+ return handleCodexCollabToolCallItem(context, state, item, "completed");
17878
18159
  default:
17879
18160
  return false;
17880
18161
  }
@@ -17896,7 +18177,9 @@ function handleCodexStructuredEvent(parsed, context, state) {
17896
18177
  return true;
17897
18178
  }
17898
18179
  case "exec_command_begin": {
17899
- const toolId = typeof parsed.call_id === "string" ? parsed.call_id : `exec-${Date.now()}`;
18180
+ const hasCallId = typeof parsed.call_id === "string" && parsed.call_id.length > 0;
18181
+ const toolId = hasCallId ? parsed.call_id : `exec-${Date.now()}`;
18182
+ if (!hasCallId) state.lastAnonymousExecId = toolId;
17900
18183
  const command = Array.isArray(parsed.command) ? parsed.command.join(" ") : "";
17901
18184
  const cwd = typeof parsed.cwd === "string" ? parsed.cwd : context.cwd;
17902
18185
  trackCodexStreamedToolId(state, toolId);
@@ -17904,7 +18187,10 @@ function handleCodexStructuredEvent(parsed, context, state) {
17904
18187
  return true;
17905
18188
  }
17906
18189
  case "exec_command_end": {
17907
- const toolId = typeof parsed.call_id === "string" ? parsed.call_id : `exec-${Date.now()}`;
18190
+ const hasCallId = typeof parsed.call_id === "string" && parsed.call_id.length > 0;
18191
+ const toolId = hasCallId ? parsed.call_id : state.lastAnonymousExecId;
18192
+ if (!hasCallId) state.lastAnonymousExecId = void 0;
18193
+ if (!toolId) return true;
17908
18194
  const output = typeof parsed.formatted_output === "string" ? parsed.formatted_output : typeof parsed.aggregated_output === "string" ? parsed.aggregated_output : "";
17909
18195
  void presenter.onToolResult?.(toolId, output);
17910
18196
  return true;
@@ -17912,9 +18198,8 @@ function handleCodexStructuredEvent(parsed, context, state) {
17912
18198
  case "mcp_tool_call_begin": {
17913
18199
  const toolId = typeof parsed.call_id === "string" ? parsed.call_id : `mcp-${Date.now()}`;
17914
18200
  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
18201
  trackCodexStreamedToolId(state, toolId);
17917
- void presenter.onToolUse(tool, invocation, toolId);
18202
+ void presenter.onToolUse(buildCodexMcpToolName(invocation), invocation, toolId);
17918
18203
  return true;
17919
18204
  }
17920
18205
  case "mcp_tool_call_end": {
@@ -17952,7 +18237,7 @@ function handleCodexStructuredEvent(parsed, context, state) {
17952
18237
  const lastMessage = typeof parsed.last_agent_message === "string" ? parsed.last_agent_message : "";
17953
18238
  if (lastMessage.length > 0) state.summary = lastMessage;
17954
18239
  state.error = void 0;
17955
- armCodexExitGraceKill(state);
18240
+ completeCodexTurnRespectingWorkers(state);
17956
18241
  return true;
17957
18242
  }
17958
18243
  case "turn.completed": {
@@ -17975,7 +18260,108 @@ function handleCodexStructuredEvent(parsed, context, state) {
17975
18260
  cacheCreationTokens: state.usage.cacheCreationTokens
17976
18261
  });
17977
18262
  state.error = void 0;
17978
- armCodexExitGraceKill(state);
18263
+ completeCodexTurnRespectingWorkers(state);
18264
+ return true;
18265
+ }
18266
+ case "sub_agent_activity": {
18267
+ const kind = typeof parsed.kind === "string" ? parsed.kind : "";
18268
+ const agentThreadId = typeof parsed.agent_thread_id === "string" ? parsed.agent_thread_id : "";
18269
+ const agentPath = typeof parsed.agent_path === "string" ? parsed.agent_path : "";
18270
+ const eventId = typeof parsed.event_id === "string" ? parsed.event_id : "";
18271
+ const isRootPath = !agentPath || agentPath === "/root" || agentPath === "/";
18272
+ switch (kind) {
18273
+ case "started": {
18274
+ if (isRootPath) return true;
18275
+ if (agentThreadId && state.backgroundToolIdByTaskId?.has(agentThreadId)) return true;
18276
+ const toolId = eventId || agentThreadId;
18277
+ if (!toolId) return true;
18278
+ if (trackCodexStreamedToolId(state, toolId)) {
18279
+ const workerName = agentPath.slice(agentPath.lastIndexOf("/") + 1) || "codex-worker";
18280
+ const parentPath = agentPath.slice(0, agentPath.lastIndexOf("/"));
18281
+ const parentToolUseId = parentPath && parentPath !== "/root" ? state.codexWorkerToolIdByPath?.get(parentPath) ?? null : null;
18282
+ void presenter.onToolUse(
18283
+ "Agent",
18284
+ {
18285
+ subagent_type: workerName,
18286
+ description: agentPath,
18287
+ ...agentThreadId ? { agent_thread_id: agentThreadId } : {}
18288
+ },
18289
+ toolId,
18290
+ parentToolUseId
18291
+ );
18292
+ registerBackgroundLauncher(state, toolId);
18293
+ if (agentThreadId) {
18294
+ registerBackgroundTaskRecord(state, { task_id: agentThreadId, tool_use_id: toolId });
18295
+ }
18296
+ if (!state.codexWorkerToolIdByPath) state.codexWorkerToolIdByPath = /* @__PURE__ */ new Map();
18297
+ state.codexWorkerToolIdByPath.set(agentPath, toolId);
18298
+ }
18299
+ return true;
18300
+ }
18301
+ case "interacted":
18302
+ return true;
18303
+ case "interrupted": {
18304
+ if (isRootPath) return true;
18305
+ finishCodexWorker(context, state, {
18306
+ threadId: agentThreadId || void 0,
18307
+ agentPath: agentPath || void 0,
18308
+ status: "interrupted",
18309
+ message: "Background worker interrupted."
18310
+ });
18311
+ return true;
18312
+ }
18313
+ default:
18314
+ return false;
18315
+ }
18316
+ }
18317
+ // App-server EventMsg collab lifecycle notices. Begin/interaction/resume events
18318
+ // carry no state Alan tracks beyond liveness (touched above); spawn_end and the
18319
+ // waiting/close terminals map into the shared worker registry below.
18320
+ case "collab_agent_spawn_begin":
18321
+ case "collab_agent_interaction_begin":
18322
+ case "collab_agent_interaction_end":
18323
+ case "collab_waiting_begin":
18324
+ case "collab_resume_begin":
18325
+ case "collab_resume_end":
18326
+ case "collab_close_begin":
18327
+ return true;
18328
+ case "collab_agent_spawn_end": {
18329
+ const newThreadId = typeof parsed.new_thread_id === "string" ? parsed.new_thread_id : "";
18330
+ if (!newThreadId || state.backgroundToolIdByTaskId?.has(newThreadId)) return true;
18331
+ const callId = typeof parsed.call_id === "string" && parsed.call_id ? parsed.call_id : typeof parsed.event_id === "string" ? parsed.event_id : "";
18332
+ const toolId = callId || `collab-spawn-${newThreadId}`;
18333
+ if (trackCodexStreamedToolId(state, toolId)) {
18334
+ const nickname = typeof parsed.new_agent_nickname === "string" ? parsed.new_agent_nickname : "";
18335
+ const role = typeof parsed.new_agent_role === "string" ? parsed.new_agent_role : "";
18336
+ void presenter.onToolUse(
18337
+ "Agent",
18338
+ {
18339
+ subagent_type: nickname || role || "codex-worker",
18340
+ description: [nickname, role].filter(Boolean).join(" \u2014 ") || "Codex background worker",
18341
+ agent_thread_id: newThreadId
18342
+ },
18343
+ toolId
18344
+ );
18345
+ registerBackgroundLauncher(state, toolId);
18346
+ }
18347
+ registerBackgroundTaskRecord(state, { task_id: newThreadId, tool_use_id: toolId });
18348
+ return true;
18349
+ }
18350
+ case "collab_waiting_end": {
18351
+ const entries = Array.isArray(parsed.agent_statuses) ? parsed.agent_statuses : [];
18352
+ for (const raw of entries) {
18353
+ if (!raw || typeof raw !== "object") continue;
18354
+ const entry = raw;
18355
+ const threadId = typeof entry.agent_thread_id === "string" ? entry.agent_thread_id : typeof entry.thread_id === "string" ? entry.thread_id : "";
18356
+ const status = typeof entry.status === "string" ? entry.status.toLowerCase() : "";
18357
+ if (!threadId || !TERMINAL_COLLAB_AGENT_STATUSES.has(status)) continue;
18358
+ finishCodexWorker(context, state, { threadId, status });
18359
+ }
18360
+ return true;
18361
+ }
18362
+ case "collab_close_end": {
18363
+ const threadId = typeof parsed.receiver_thread_id === "string" ? parsed.receiver_thread_id : "";
18364
+ if (threadId) finishCodexWorker(context, state, { threadId, status: "shutdown" });
17979
18365
  return true;
17980
18366
  }
17981
18367
  case "error": {
@@ -18094,10 +18480,11 @@ function createCodexRuntimeBackend(command = "codex", defaultArgs = []) {
18094
18480
  },
18095
18481
  promptViaStdin: true,
18096
18482
  augmentPrompt: (ctx) => {
18483
+ const isResume = Boolean(resumableSessionId);
18097
18484
  const promptWithContext = resumeContextPrefix ? `${resumeContextPrefix}
18098
18485
 
18099
18486
  ${ctx.promptText}` : ctx.promptText;
18100
- const basePrompt = buildPromptWithSystem(ctx.config, promptWithContext);
18487
+ const basePrompt = buildResumeAwarePrompt(ctx.config, promptWithContext, { isResume });
18101
18488
  return ctx.config.mode === "plan" ? buildPlanModePrefix(basePrompt) : basePrompt;
18102
18489
  },
18103
18490
  parseStructuredLine: parseCodexStructuredLine,
@@ -18303,10 +18690,21 @@ function handleCursorStructuredEvent(parsed, context, state) {
18303
18690
  }
18304
18691
  case "tool_call": {
18305
18692
  const subtype = typeof parsed.subtype === "string" ? parsed.subtype : "";
18306
- const toolId = typeof parsed.call_id === "string" ? parsed.call_id : `cursor-tool-${Date.now()}`;
18693
+ const hasCallId = typeof parsed.call_id === "string" && parsed.call_id.length > 0;
18307
18694
  const toolCall = typeof parsed.tool_call === "object" && parsed.tool_call !== null ? parsed.tool_call : null;
18308
18695
  const entry = toolCall ? extractCursorToolEntry(toolCall) : null;
18309
18696
  if (!entry) return true;
18697
+ let toolId;
18698
+ if (hasCallId) {
18699
+ toolId = parsed.call_id;
18700
+ } else if (subtype === "started") {
18701
+ state.cursorAnonymousToolCounter = (state.cursorAnonymousToolCounter ?? 0) + 1;
18702
+ toolId = `cursor-tool-${state.cursorAnonymousToolCounter}`;
18703
+ state.lastAnonymousCursorToolId = toolId;
18704
+ } else {
18705
+ toolId = state.lastAnonymousCursorToolId;
18706
+ }
18707
+ if (!toolId) return true;
18310
18708
  state.iterations = Math.max(state.iterations, 1);
18311
18709
  const toolName = formatCursorToolName(entry.rawName, entry.payload);
18312
18710
  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 +18713,7 @@ function handleCursorStructuredEvent(parsed, context, state) {
18315
18713
  return true;
18316
18714
  }
18317
18715
  if (subtype === "completed") {
18716
+ if (!hasCallId) state.lastAnonymousCursorToolId = void 0;
18318
18717
  void presenter.onToolResult?.(toolId, buildCursorToolResultText(entry.payload.result));
18319
18718
  }
18320
18719
  return true;
@@ -18393,7 +18792,10 @@ function buildCursorAgentModelArg(modelId, options) {
18393
18792
  const wireEffort = resolveWireEffort({
18394
18793
  harness: "cursor_agent_cli",
18395
18794
  modelId: baseId,
18396
- selectedEffortLevel: options?.selectedEffortLevel,
18795
+ // Fall back to an effort baked into the model id (e.g. `gpt-5.5-high` from a
18796
+ // session import or raw discovered id) when no explicit selection overrides it,
18797
+ // so the SKU is preserved instead of silently downgraded to default effort.
18798
+ selectedEffortLevel: options?.selectedEffortLevel ?? parsed.effort,
18397
18799
  effortLevels
18398
18800
  });
18399
18801
  let id = baseId;
@@ -18407,7 +18809,8 @@ function buildCursorAgentModelArg(modelId, options) {
18407
18809
  }
18408
18810
  }
18409
18811
  const contextWindow = asContextWindow(options?.selectedContextWindow);
18410
- if (contextWindow === "1m" && id === baseId) {
18812
+ const modelSupports1m = (options?.contextWindows ?? []).includes("1m");
18813
+ if (contextWindow === "1m" && id === baseId && modelSupports1m) {
18411
18814
  return `${baseId}[context=1m]`;
18412
18815
  }
18413
18816
  if (parsed.fast) return `${id}-fast`;
@@ -18497,7 +18900,8 @@ function createCursorAgentCliBackend(command = "cursor-agent", defaultArgs = [])
18497
18900
  buildCursorAgentModelArg(model, {
18498
18901
  selectedEffortLevel: ctx.config.selectedEffortLevel,
18499
18902
  selectedContextWindow: ctx.config.selectedContextWindow,
18500
- effortLevels: modelDef?.capabilities?.effortLevels
18903
+ effortLevels: modelDef?.capabilities?.effortLevels,
18904
+ contextWindows: modelDef?.capabilities?.contextWindows
18501
18905
  })
18502
18906
  );
18503
18907
  }
@@ -18515,11 +18919,20 @@ function createCursorAgentCliBackend(command = "cursor-agent", defaultArgs = [])
18515
18919
  // with zero output; without this watchdog only the 30-min server reaper
18516
18920
  // would end the run.
18517
18921
  startupTimeoutMs: 18e4,
18922
+ // Post-startup hang bound: cursor's asks use the non-blocking MCP model and it
18923
+ // never populates the idle timer's ask/background fire conditions, so a cursor
18924
+ // process that produced output then went silent (no result event) is otherwise
18925
+ // bounded only by the ~30-min external reaper. Poll every 5 min; kill after 15
18926
+ // min of continuous silence.
18927
+ resultIdleTimeoutMs: 5 * 6e4,
18928
+ postStartupSilenceTimeoutMs: 15 * 6e4,
18518
18929
  augmentPrompt: (ctx) => {
18519
- const prompt = buildPromptWithImagePathReferences(ctx, imageFiles);
18520
- return ctx.config.teamId?.trim() ? `${prompt}
18521
-
18522
- ${buildCodeDiscoveryEnforcementTail()}` : prompt;
18930
+ const isResume = Boolean(
18931
+ ctx.config.runtimeSessionId?.trim() || ctx.config.providerSessionId?.trim()
18932
+ );
18933
+ const base = buildResumeAwarePrompt(ctx.config, ctx.promptText, { isResume });
18934
+ const prompt = ctx.config.mode === "plan" ? buildPlanModePrefix(base) : base;
18935
+ return appendImagePathReferences(prompt, imageFiles);
18523
18936
  },
18524
18937
  parseStructuredLine: parseCursorStructuredLine
18525
18938
  }).run(context);
@@ -18746,7 +19159,7 @@ function createGrokAcpDriver(imageFiles) {
18746
19159
  }
18747
19160
  newSession(context);
18748
19161
  }
18749
- function sendPrompt(context, state) {
19162
+ function sendPrompt2(context, state) {
18750
19163
  const text = buildPromptWithImagePathReferences(context, imageFiles);
18751
19164
  isPromptActive = true;
18752
19165
  promptId = request("session/prompt", {
@@ -18789,7 +19202,7 @@ function createGrokAcpDriver(imageFiles) {
18789
19202
  const sessionId = asString(result?.sessionId);
18790
19203
  if (sessionId) state.runtimeSessionId = sessionId;
18791
19204
  state.iterations = Math.max(state.iterations, 1);
18792
- sendPrompt(context, state);
19205
+ sendPrompt2(context, state);
18793
19206
  return;
18794
19207
  }
18795
19208
  if (message.id === promptId) {
@@ -19135,23 +19548,59 @@ function createKimiCliBackend(command = "kimi-cli", defaultArgs = []) {
19135
19548
  }
19136
19549
  };
19137
19550
  }
19551
+ var AUTONOMOUS_OPENCODE_CONFIG_CONTENT = JSON.stringify({
19552
+ $schema: "https://opencode.ai/config.json",
19553
+ permission: "allow"
19554
+ });
19555
+ function buildAutonomousOpenCodeConfigContent(existing) {
19556
+ if (!existing?.trim()) return AUTONOMOUS_OPENCODE_CONFIG_CONTENT;
19557
+ try {
19558
+ const parsed = JSON.parse(existing);
19559
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
19560
+ return JSON.stringify({
19561
+ ...parsed,
19562
+ permission: "allow"
19563
+ });
19564
+ }
19565
+ } catch {
19566
+ }
19567
+ return AUTONOMOUS_OPENCODE_CONFIG_CONTENT;
19568
+ }
19569
+ function withAutonomousOpenCodePermissions(context) {
19570
+ if (shouldUseReadOnlyRuntimePermissions(context.config)) return context;
19571
+ return {
19572
+ ...context,
19573
+ env: {
19574
+ ...context.env,
19575
+ OPENCODE_CONFIG_CONTENT: buildAutonomousOpenCodeConfigContent(
19576
+ context.env.OPENCODE_CONFIG_CONTENT
19577
+ )
19578
+ }
19579
+ };
19580
+ }
19138
19581
  function getRecord(value2) {
19139
19582
  return typeof value2 === "object" && value2 !== null ? value2 : null;
19140
19583
  }
19141
19584
  function getString(value2) {
19142
19585
  return typeof value2 === "string" ? value2.trim() : "";
19143
19586
  }
19144
- function getOpenCodeToolError(parsed, part) {
19145
- const state = getRecord(part?.state) ?? getRecord(parsed.state);
19146
- const error = getRecord(part?.error) ?? getRecord(parsed.error) ?? getRecord(state?.error);
19147
- return getString(part?.error) || getString(state?.error) || getString(error?.message) || getString(parsed.error);
19587
+ function getOpenCodeToolError(part) {
19588
+ const state = getRecord(part.state);
19589
+ const error = getRecord(part.error) ?? getRecord(state?.error);
19590
+ return getString(part.error) || getString(state?.error) || getString(error?.message);
19591
+ }
19592
+ function isOpenCodeToolError(part) {
19593
+ const state = getRecord(part.state);
19594
+ return part.is_error === true || part.isError === true || state?.status === "error" || Boolean(getOpenCodeToolError(part));
19595
+ }
19596
+ function getOpenCodePartId(part) {
19597
+ return getString(part.id) || getString(part.partID);
19148
19598
  }
19149
- function isOpenCodeToolError(parsed, part) {
19150
- const state = getRecord(part?.state) ?? getRecord(parsed.state);
19151
- return part?.is_error === true || part?.isError === true || state?.status === "error" || Boolean(getOpenCodeToolError(parsed, part));
19599
+ function getOpenCodeToolId(part) {
19600
+ return getString(part.callID) || getString(part.id) || `opencode-tool-${Date.now()}`;
19152
19601
  }
19153
- function getOpenCodePartId(parsed, part) {
19154
- return getString(part?.id) || getString(parsed.id) || getString(parsed.partID);
19602
+ function getOpenCodeToolName(part) {
19603
+ return getString(part.tool) || (typeof part.name === "string" ? part.name.trim() : "") || "Tool";
19155
19604
  }
19156
19605
  function resolveOpenCodePartSnapshotDelta(text, partId, snapshots) {
19157
19606
  if (!partId) return text;
@@ -19168,93 +19617,92 @@ function resolveOpenCodeReasoningDelta(text, partId, state) {
19168
19617
  state.opencodeReasoningByPartId ??= /* @__PURE__ */ new Map();
19169
19618
  return resolveOpenCodePartSnapshotDelta(text, partId, state.opencodeReasoningByPartId);
19170
19619
  }
19171
- function handleOpencodeStructuredEvent(parsed, context, state) {
19620
+ var TASK_TOOL_NAME = "task";
19621
+ var TASK_RESULT_TAG_PATTERN = /<task_result>\n?([\s\S]*?)\n?<\/task_result>/;
19622
+ var TASK_ERROR_TAG_PATTERN = /<task_error>\n?([\s\S]*?)\n?<\/task_error>/;
19623
+ function unwrapOpenCodeTaskOutput(output) {
19624
+ const result = output.match(TASK_RESULT_TAG_PATTERN);
19625
+ if (result?.[1] !== void 0) return result[1].trim();
19626
+ const error = output.match(TASK_ERROR_TAG_PATTERN);
19627
+ if (error?.[1] !== void 0) return error[1].trim();
19628
+ return output;
19629
+ }
19630
+ function emitOpenCodeToolPart(presenter, state, part, opts) {
19631
+ const stateRecord = getRecord(part.state);
19632
+ const status = getString(stateRecord?.status);
19633
+ if (status === "pending") return;
19634
+ const toolName = getOpenCodeToolName(part);
19635
+ const toolId = getOpenCodeToolId(part);
19636
+ const isTask = toolName.toLowerCase() === TASK_TOOL_NAME;
19637
+ const parentToolUseId = opts.parentToolUseId ?? void 0;
19638
+ state.opencodeToolUseIds ??= /* @__PURE__ */ new Set();
19639
+ if (!state.opencodeToolUseIds.has(toolId)) {
19640
+ state.opencodeToolUseIds.add(toolId);
19641
+ void presenter.onToolUse(toolName, stateRecord?.input ?? part, toolId, parentToolUseId);
19642
+ if (isTask) registerBackgroundLauncher(state, toolId);
19643
+ }
19644
+ if (status !== "completed" && status !== "error") return;
19645
+ const toolError = isOpenCodeToolError(part);
19646
+ const rawOutput = getString(stateRecord?.output) || getString(stateRecord?.error) || JSON.stringify(stateRecord ?? part);
19647
+ const output = isTask ? unwrapOpenCodeTaskOutput(rawOutput) : rawOutput;
19648
+ void presenter.onToolResult?.(toolId, output, toolError, parentToolUseId);
19649
+ if (isTask) clearBackgroundTaskIds(state, [toolId]);
19650
+ if (toolError && !state.error) {
19651
+ const message = getOpenCodeToolError(part) || "OpenCode tool call failed.";
19652
+ state.error = message;
19653
+ void presenter.onError(message);
19654
+ }
19655
+ }
19656
+ function mapOpencodePart(part, context, state, opts = {}) {
19172
19657
  const presenter = context.presenter;
19173
- const type = typeof parsed.type === "string" ? parsed.type : "";
19174
- if (!type) return false;
19175
- const sessionID = typeof parsed.sessionID === "string" ? parsed.sessionID : void 0;
19176
- if (sessionID) state.runtimeSessionId = sessionID;
19177
- const part = typeof parsed.part === "object" && parsed.part !== null ? parsed.part : null;
19658
+ const type = getString(part.type);
19178
19659
  switch (type) {
19179
- case "step_start": {
19660
+ case "step-start": {
19180
19661
  state.iterations += 1;
19181
19662
  void presenter.onLog(`[opencode] Step ${state.iterations} started`);
19182
19663
  return true;
19183
19664
  }
19184
19665
  case "text": {
19185
- const text = part && typeof part.text === "string" ? part.text : "";
19666
+ const text = typeof part.text === "string" ? part.text : "";
19186
19667
  if (text) {
19187
- const delta = resolveOpenCodeTextDelta(text, getOpenCodePartId(parsed, part), state);
19188
- if (!delta) return true;
19189
- state.summary += delta;
19190
- void presenter.onAssistantText(delta);
19668
+ const delta = resolveOpenCodeTextDelta(text, getOpenCodePartId(part), state);
19669
+ if (delta) {
19670
+ state.summary += delta;
19671
+ void presenter.onAssistantText(delta);
19672
+ }
19191
19673
  }
19192
19674
  return true;
19193
19675
  }
19676
+ // Real Part schema only ever uses "reasoning"; "thinking" is a defensive alias in
19677
+ // case a CLI dialect ever names the part itself that way.
19194
19678
  case "reasoning":
19195
19679
  case "thinking": {
19196
- const text = part && typeof part.text === "string" ? part.text : "";
19680
+ const text = typeof part.text === "string" ? part.text : "";
19197
19681
  if (!text) return true;
19198
- const delta = resolveOpenCodeReasoningDelta(text, getOpenCodePartId(parsed, part), state);
19682
+ const delta = resolveOpenCodeReasoningDelta(text, getOpenCodePartId(part), state);
19199
19683
  if (!delta) return true;
19200
19684
  state.iterations = Math.max(state.iterations, 1);
19201
19685
  void presenter.onThinking(delta);
19202
19686
  return true;
19203
19687
  }
19204
- case "tool_use":
19205
- case "tool.execute": {
19206
- const toolName = getString(parsed.tool) || (part && typeof part.name === "string" ? part.name : "") || getString(parsed.name) || (part && typeof part.tool === "string" ? part.tool : "") || "Tool";
19207
- const toolId = part && typeof part.id === "string" ? part.id : `opencode-tool-${Date.now()}`;
19208
- void presenter.onToolUse(toolName, part ?? {}, toolId);
19209
- return true;
19210
- }
19211
19688
  case "tool": {
19212
- const stateRecord = getRecord(parsed.state);
19213
- const toolName = getString(parsed.tool) || (part && typeof part.name === "string" ? part.name : "") || getString(parsed.name) || (part && typeof part.tool === "string" ? part.tool : "") || "Tool";
19214
- const toolId = getString(parsed.id) || getString(parsed.callID) || `opencode-tool-${Date.now()}`;
19215
- state.opencodeToolUseIds ??= /* @__PURE__ */ new Set();
19216
- if (!state.opencodeToolUseIds.has(toolId)) {
19217
- state.opencodeToolUseIds.add(toolId);
19218
- void presenter.onToolUse(toolName, stateRecord?.input ?? part ?? parsed, toolId);
19219
- }
19220
- const status = getString(stateRecord?.status);
19221
- if (status === "completed" || status === "error") {
19222
- const toolError = isOpenCodeToolError(parsed, part);
19223
- const output = getString(stateRecord?.output) || getString(stateRecord?.error) || JSON.stringify(stateRecord ?? part ?? parsed);
19224
- void presenter.onToolResult?.(toolId, output, toolError);
19225
- if (toolError && !state.error) {
19226
- const message = getOpenCodeToolError(parsed, part) || "OpenCode tool call failed.";
19227
- state.error = message;
19228
- void presenter.onError(message);
19229
- }
19230
- }
19689
+ emitOpenCodeToolPart(presenter, state, part, opts);
19231
19690
  return true;
19232
19691
  }
19233
- case "tool_result":
19234
- case "tool.result": {
19235
- const toolId = part && typeof part.id === "string" ? part.id : `opencode-tool-${Date.now()}`;
19236
- const output = part && typeof part.output === "string" ? part.output : JSON.stringify(part);
19237
- const toolError = isOpenCodeToolError(parsed, part);
19238
- void presenter.onToolResult?.(toolId, output, toolError);
19239
- if (toolError && !state.error) {
19240
- const message = getOpenCodeToolError(parsed, part) || "OpenCode tool call failed.";
19241
- state.error = message;
19242
- void presenter.onError(message);
19243
- }
19244
- return true;
19245
- }
19246
- case "step_finish": {
19247
- if (part && typeof part.tokens === "object" && part.tokens !== null) {
19248
- const tokens = part.tokens;
19692
+ case "step-finish": {
19693
+ const tokens = getRecord(part.tokens);
19694
+ if (tokens) {
19695
+ const cache2 = getRecord(tokens.cache);
19696
+ const cost = typeof part.cost === "number" ? part.cost : 0;
19249
19697
  state.usage = {
19250
19698
  model: typeof context.config.selectedModel === "string" && context.config.selectedModel.trim() || "opencode",
19251
19699
  numTurns: Math.max(state.iterations, 1),
19252
19700
  durationMs: 0,
19253
- inputTokens: tokens.input ?? 0,
19254
- outputTokens: tokens.output ?? 0,
19255
- cacheReadTokens: tokens.cache_read ?? 0,
19256
- cacheCreationTokens: tokens.cache_creation ?? 0,
19257
- costUsd: 0
19701
+ inputTokens: Number(tokens.input) || 0,
19702
+ outputTokens: Number(tokens.output) || 0,
19703
+ cacheReadTokens: Number(cache2?.read) || 0,
19704
+ cacheCreationTokens: Number(cache2?.write) || 0,
19705
+ costUsd: cost
19258
19706
  };
19259
19707
  void presenter.onUsageUpdate?.({
19260
19708
  model: state.usage.model,
@@ -19266,6 +19714,57 @@ function handleOpencodeStructuredEvent(parsed, context, state) {
19266
19714
  }
19267
19715
  return true;
19268
19716
  }
19717
+ default:
19718
+ return false;
19719
+ }
19720
+ }
19721
+ function normalizeOpenCodePart(parsed, part, partType) {
19722
+ return { ...parsed, ...part ?? {}, type: partType };
19723
+ }
19724
+ function handleOpencodeStructuredEvent(parsed, context, state) {
19725
+ const presenter = context.presenter;
19726
+ const type = typeof parsed.type === "string" ? parsed.type : "";
19727
+ if (!type) return false;
19728
+ const sessionID = typeof parsed.sessionID === "string" ? parsed.sessionID : void 0;
19729
+ if (sessionID) state.runtimeSessionId = sessionID;
19730
+ const part = typeof parsed.part === "object" && parsed.part !== null ? parsed.part : null;
19731
+ switch (type) {
19732
+ case "step_start":
19733
+ return mapOpencodePart(normalizeOpenCodePart(parsed, part, "step-start"), context, state);
19734
+ case "text":
19735
+ return mapOpencodePart(normalizeOpenCodePart(parsed, part, "text"), context, state);
19736
+ case "reasoning":
19737
+ case "thinking":
19738
+ return mapOpencodePart(normalizeOpenCodePart(parsed, part, "reasoning"), context, state);
19739
+ // Real event: `opencode run --format json` emits exactly this shape — a single
19740
+ // terminal event per tool call, carrying part.state.{input,output|error}. See
19741
+ // notes/opencode-cli-reverse-engineering.md for the live-captured schema.
19742
+ case "tool_use":
19743
+ case "tool.execute":
19744
+ case "tool":
19745
+ return mapOpencodePart(normalizeOpenCodePart(parsed, part, "tool"), context, state);
19746
+ case "step_finish":
19747
+ return mapOpencodePart(normalizeOpenCodePart(parsed, part, "step-finish"), context, state);
19748
+ // Defensive fallback only: no observed OpenCode version emits a separate
19749
+ // tool_result/tool.result event in `--format json` mode (tool state always
19750
+ // arrives via `tool_use` above). Kept in case a future/older CLI build splits
19751
+ // the terminal tool event into a distinct result message. Deliberately NOT routed
19752
+ // through mapOpencodePart — this shape has no part.state wrapper at all (output
19753
+ // sits directly on the part), so it isn't a real Part-schema shape.
19754
+ case "tool_result":
19755
+ case "tool.result": {
19756
+ const toolId = getString(part?.callID) || getString(parsed.callID) || getString(part?.id) || getString(parsed.id) || `opencode-tool-${Date.now()}`;
19757
+ const output = part && typeof part.output === "string" ? part.output : JSON.stringify(part);
19758
+ const stateRecord = getRecord(part?.state) ?? getRecord(parsed.state);
19759
+ const toolError = part?.is_error === true || part?.isError === true || stateRecord?.status === "error" || Boolean(getString(part?.error) || getString(stateRecord?.error));
19760
+ void presenter.onToolResult?.(toolId, output, toolError);
19761
+ if (toolError && !state.error) {
19762
+ const message = getString(part?.error) || getString(stateRecord?.error) || "OpenCode tool call failed.";
19763
+ state.error = message;
19764
+ void presenter.onError(message);
19765
+ }
19766
+ return true;
19767
+ }
19269
19768
  case "session.error":
19270
19769
  case "error": {
19271
19770
  const errObj = typeof parsed.error === "object" && parsed.error !== null ? parsed.error : null;
@@ -19290,85 +19789,450 @@ var parseOpencodeStructuredLine = createStructuredLineParser(
19290
19789
  "opencode_cli",
19291
19790
  handleOpencodeStructuredEvent
19292
19791
  );
19293
- var AUTONOMOUS_OPENCODE_CONFIG_CONTENT = JSON.stringify({
19294
- $schema: "https://opencode.ai/config.json",
19295
- permission: "allow"
19296
- });
19297
- function buildAutonomousOpenCodeConfigContent(existing) {
19298
- if (!existing?.trim()) return AUTONOMOUS_OPENCODE_CONFIG_CONTENT;
19792
+ function buildOpencodeEffortArgs(selectedEffortLevel) {
19793
+ const trimmed = selectedEffortLevel?.trim().toLowerCase();
19794
+ if (!trimmed || !isEffortLevel(trimmed)) return [];
19795
+ return ["--variant", trimmed];
19796
+ }
19797
+ function createOpencodeCliBackend(command = "opencode", defaultArgs = []) {
19798
+ return {
19799
+ kind: "opencode_cli",
19800
+ supportTier: "structured",
19801
+ async run(context) {
19802
+ const { paths: imagePaths, cleanup } = writeImagesToTempFiles(
19803
+ context.config.images,
19804
+ context.cwd
19805
+ );
19806
+ const { paths: filePaths, cleanup: cleanupFiles } = writeFilesToTempFiles(
19807
+ context.config.files,
19808
+ context.cwd
19809
+ );
19810
+ try {
19811
+ const runContext = withAutonomousOpenCodePermissions(context);
19812
+ return await createGenericCliBackend({
19813
+ kind: "opencode_cli",
19814
+ supportTier: "structured",
19815
+ command,
19816
+ args: [],
19817
+ buildArgs: (ctx, prompt) => {
19818
+ const args = ["run", "--format", "json", "--thinking"];
19819
+ if (!shouldUseReadOnlyRuntimePermissions(ctx.config)) {
19820
+ args.push("--dangerously-skip-permissions");
19821
+ }
19822
+ const resumeId = ctx.config.runtimeSessionId?.trim() || ctx.config.providerSessionId?.trim();
19823
+ if (resumeId) args.push("--session", resumeId);
19824
+ const model = ctx.config.selectedModel?.trim();
19825
+ if (model?.includes("/")) args.push("--model", model);
19826
+ args.push(...buildOpencodeEffortArgs(ctx.config.selectedEffortLevel));
19827
+ const attachmentPaths = [...imagePaths, ...filePaths];
19828
+ const fileArgs = attachmentPaths.flatMap((p) => ["--file", p]);
19829
+ if (fileArgs.length > 0) {
19830
+ return [...args, ...fileArgs, ...defaultArgs, "--", prompt];
19831
+ }
19832
+ return [...args, ...defaultArgs, prompt];
19833
+ },
19834
+ augmentPrompt: (ctx) => {
19835
+ const base = buildPromptWithSystem(ctx.config, ctx.promptText);
19836
+ return ctx.config.mode === "plan" ? buildPlanModePrefix(base) : base;
19837
+ },
19838
+ parseStructuredLine: parseOpencodeStructuredLine
19839
+ }).run(runContext);
19840
+ } finally {
19841
+ cleanup();
19842
+ cleanupFiles();
19843
+ }
19844
+ }
19845
+ };
19846
+ }
19847
+ function parseSseEventBlock(block) {
19848
+ const dataLines = block.split("\n").map((line) => line.endsWith("\r") ? line.slice(0, -1) : line).filter((line) => line.startsWith("data:")).map((line) => line.slice("data:".length).replace(/^ /, ""));
19849
+ if (dataLines.length === 0) return null;
19299
19850
  try {
19300
- const parsed = JSON.parse(existing);
19301
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
19302
- return JSON.stringify({
19303
- ...parsed,
19304
- permission: "allow"
19305
- });
19851
+ const parsed = JSON.parse(dataLines.join("\n"));
19852
+ if (parsed && typeof parsed === "object" && typeof parsed.type === "string") {
19853
+ const record = parsed;
19854
+ return {
19855
+ id: typeof record.id === "string" ? record.id : void 0,
19856
+ type: record.type,
19857
+ properties: typeof record.properties === "object" && record.properties !== null ? record.properties : {}
19858
+ };
19306
19859
  }
19307
19860
  } catch {
19308
19861
  }
19309
- return AUTONOMOUS_OPENCODE_CONFIG_CONTENT;
19862
+ return null;
19863
+ }
19864
+ async function connectOpenCodeSse(url2, signal) {
19865
+ const response = await fetch(url2, {
19866
+ signal,
19867
+ headers: { Accept: "text/event-stream" }
19868
+ });
19869
+ if (!response.ok || !response.body) {
19870
+ throw new Error(`OpenCode SSE connection failed: HTTP ${response.status}`);
19871
+ }
19872
+ const reader = response.body.getReader();
19873
+ const decoder = new TextDecoder();
19874
+ let closed = false;
19875
+ async function* generate() {
19876
+ let buffer = "";
19877
+ try {
19878
+ while (!closed) {
19879
+ const { value: value2, done } = await reader.read();
19880
+ if (done) return;
19881
+ buffer += decoder.decode(value2, { stream: true });
19882
+ let separatorIndex = buffer.indexOf("\n\n");
19883
+ while (separatorIndex !== -1) {
19884
+ const block = buffer.slice(0, separatorIndex);
19885
+ buffer = buffer.slice(separatorIndex + 2);
19886
+ const event = parseSseEventBlock(block);
19887
+ if (event) yield event;
19888
+ separatorIndex = buffer.indexOf("\n\n");
19889
+ }
19890
+ }
19891
+ } finally {
19892
+ try {
19893
+ reader.releaseLock();
19894
+ } catch {
19895
+ }
19896
+ }
19897
+ }
19898
+ return {
19899
+ events: generate(),
19900
+ close: () => {
19901
+ if (closed) return;
19902
+ closed = true;
19903
+ reader.cancel().catch(() => {
19904
+ });
19905
+ }
19906
+ };
19907
+ }
19908
+ var SERVER_LISTENING_PATTERN = /listening on (https?:\/\/\S+)/i;
19909
+ var STARTUP_TIMEOUT_MS = 2e4;
19910
+ var HEALTH_POLL_INTERVAL_MS = 200;
19911
+ var HEALTH_POLL_MAX_ATTEMPTS = 50;
19912
+ function startOpenCodeServer(command, context) {
19913
+ const child = spawnCli(command, ["serve", "--port", "0", "--hostname", "127.0.0.1"], context);
19914
+ const ready = new Promise((resolve22, reject) => {
19915
+ let settled = false;
19916
+ const timeout = setTimeout(() => {
19917
+ if (settled) return;
19918
+ settled = true;
19919
+ reject(
19920
+ new Error(`opencode serve did not report a listening URL within ${STARTUP_TIMEOUT_MS}ms`)
19921
+ );
19922
+ }, STARTUP_TIMEOUT_MS);
19923
+ const stdoutRl = (0, import_readline2.createInterface)({ input: child.stdout });
19924
+ const stderrRl = (0, import_readline2.createInterface)({ input: child.stderr });
19925
+ const stderrLines = [];
19926
+ const checkLine = (line) => {
19927
+ if (settled) return;
19928
+ const match = line.match(SERVER_LISTENING_PATTERN);
19929
+ if (!match) return;
19930
+ settled = true;
19931
+ clearTimeout(timeout);
19932
+ resolve22({ baseUrl: match[1].replace(/\/$/, "") });
19933
+ };
19934
+ stdoutRl.on("line", checkLine);
19935
+ stderrRl.on("line", (line) => {
19936
+ stderrLines.push(line);
19937
+ checkLine(line);
19938
+ });
19939
+ child.once("error", (error) => {
19940
+ if (settled) return;
19941
+ settled = true;
19942
+ clearTimeout(timeout);
19943
+ reject(error);
19944
+ });
19945
+ child.once("exit", (code) => {
19946
+ if (settled) return;
19947
+ settled = true;
19948
+ clearTimeout(timeout);
19949
+ reject(
19950
+ new Error(
19951
+ `opencode serve exited (code ${code}) before reporting a listening URL. stderr: ${stderrLines.slice(-5).join("\n")}`
19952
+ )
19953
+ );
19954
+ });
19955
+ });
19956
+ return { child, ready };
19957
+ }
19958
+ async function waitForHealthy(baseUrl, signal) {
19959
+ for (let attempt = 0; attempt < HEALTH_POLL_MAX_ATTEMPTS; attempt++) {
19960
+ if (signal.aborted)
19961
+ throw new Error("Aborted while waiting for opencode server to become healthy");
19962
+ try {
19963
+ const response = await fetch(`${baseUrl}/global/health`, { signal });
19964
+ if (response.ok) {
19965
+ const body = await response.json().catch(() => null);
19966
+ if (body?.healthy) return;
19967
+ }
19968
+ } catch {
19969
+ }
19970
+ await new Promise((resolve22) => setTimeout(resolve22, HEALTH_POLL_INTERVAL_MS));
19971
+ }
19972
+ throw new Error(`opencode server at ${baseUrl} did not become healthy in time`);
19973
+ }
19974
+ var NON_INTERACTIVE_DENY_RULES = [
19975
+ { permission: "question", pattern: "*", action: "deny" },
19976
+ { permission: "plan_enter", pattern: "*", action: "deny" },
19977
+ { permission: "plan_exit", pattern: "*", action: "deny" }
19978
+ ];
19979
+ async function resolveSession(baseUrl, context, signal) {
19980
+ const resumeId = context.config.runtimeSessionId?.trim() || context.config.providerSessionId?.trim();
19981
+ if (resumeId) {
19982
+ const existing = await fetch(`${baseUrl}/session/${encodeURIComponent(resumeId)}`, {
19983
+ signal
19984
+ }).catch(() => null);
19985
+ if (existing?.ok) return { id: resumeId };
19986
+ }
19987
+ const title = context.promptText.slice(0, 50) + (context.promptText.length > 50 ? "..." : "");
19988
+ const response = await fetch(`${baseUrl}/session`, {
19989
+ method: "POST",
19990
+ signal,
19991
+ headers: { "Content-Type": "application/json" },
19992
+ body: JSON.stringify({ title, permission: NON_INTERACTIVE_DENY_RULES })
19993
+ });
19994
+ if (!response.ok) {
19995
+ throw new Error(`Failed to create OpenCode session: HTTP ${response.status}`);
19996
+ }
19997
+ const info = await response.json();
19998
+ if (!info.id) throw new Error("OpenCode session create response had no id");
19999
+ return { id: info.id };
20000
+ }
20001
+ function buildPromptParts(promptText, imageFiles, attachmentFiles) {
20002
+ const attachments = [...imageFiles, ...attachmentFiles].map(
20003
+ (file) => ({
20004
+ type: "file",
20005
+ mime: file.mimeType,
20006
+ filename: file.filename,
20007
+ url: (0, import_url2.pathToFileURL)(file.path).href
20008
+ })
20009
+ );
20010
+ return [...attachments, { type: "text", text: promptText }];
20011
+ }
20012
+ async function sendPrompt(baseUrl, sessionId, context, parts2, signal) {
20013
+ const model = context.config.selectedModel?.trim();
20014
+ const modelBody = model && model.includes("/") ? (() => {
20015
+ const slashIndex = model.indexOf("/");
20016
+ return { providerID: model.slice(0, slashIndex), modelID: model.slice(slashIndex + 1) };
20017
+ })() : void 0;
20018
+ const variant = context.config.selectedEffortLevel?.trim().toLowerCase();
20019
+ const response = await fetch(`${baseUrl}/session/${encodeURIComponent(sessionId)}/message`, {
20020
+ method: "POST",
20021
+ signal,
20022
+ headers: { "Content-Type": "application/json" },
20023
+ body: JSON.stringify({
20024
+ parts: parts2,
20025
+ ...modelBody ? { model: modelBody } : {},
20026
+ ...variant && isEffortLevel(variant) ? { variant } : {}
20027
+ })
20028
+ });
20029
+ if (!response.ok) {
20030
+ const text = await response.text().catch(() => "");
20031
+ throw new Error(`OpenCode prompt request failed: HTTP ${response.status} ${text}`.trim());
20032
+ }
19310
20033
  }
19311
- function withAutonomousOpenCodePermissions(context) {
19312
- if (shouldUseReadOnlyRuntimePermissions(context.config)) return context;
20034
+ async function replyToPermission(baseUrl, requestId, reply, signal) {
20035
+ await fetch(`${baseUrl}/permission/${encodeURIComponent(requestId)}/reply`, {
20036
+ method: "POST",
20037
+ signal,
20038
+ headers: { "Content-Type": "application/json" },
20039
+ body: JSON.stringify({ reply })
20040
+ }).catch((error) => {
20041
+ console.warn("[opencode_serve] Failed to reply to permission request", requestId, error);
20042
+ });
20043
+ }
20044
+ function buildAgentResult(state, sessionId, aborted2) {
20045
+ if (aborted2) {
20046
+ return {
20047
+ success: false,
20048
+ summary: "Interrupted by user",
20049
+ filesModified: [],
20050
+ planFilesCreated: [],
20051
+ iterations: Math.max(state.iterations, 1),
20052
+ error: "Interrupted by user",
20053
+ providerSessionId: sessionId,
20054
+ runtimeSessionId: sessionId,
20055
+ backendKind: "opencode_serve",
20056
+ supportTier: "structured",
20057
+ usage: state.usage
20058
+ };
20059
+ }
20060
+ const failed = Boolean(state.error?.trim());
20061
+ const summary = state.summary.trim() || state.error?.trim() || (failed ? "Task failed" : "Task completed");
19313
20062
  return {
19314
- ...context,
19315
- env: {
19316
- ...context.env,
19317
- OPENCODE_CONFIG_CONTENT: buildAutonomousOpenCodeConfigContent(
19318
- context.env.OPENCODE_CONFIG_CONTENT
19319
- )
19320
- }
20063
+ success: !failed,
20064
+ summary,
20065
+ filesModified: [],
20066
+ planFilesCreated: [],
20067
+ iterations: Math.max(state.iterations, 1),
20068
+ ...state.error?.trim() ? { error: state.error.trim() } : {},
20069
+ providerSessionId: sessionId,
20070
+ runtimeSessionId: sessionId,
20071
+ backendKind: "opencode_serve",
20072
+ supportTier: "structured",
20073
+ usage: state.usage
19321
20074
  };
19322
20075
  }
19323
- function createOpencodeCliBackend(command = "opencode", defaultArgs = []) {
20076
+ function createOpencodeServeBackend(command = "opencode") {
19324
20077
  return {
19325
- kind: "opencode_cli",
20078
+ kind: "opencode_serve",
19326
20079
  supportTier: "structured",
19327
20080
  async run(context) {
19328
- const { paths: imagePaths, cleanup } = writeImagesToTempFiles(
20081
+ const { files: imageFiles, cleanup: cleanupImages } = writeImagesToTempFiles(
19329
20082
  context.config.images,
19330
20083
  context.cwd
19331
20084
  );
19332
- const { paths: filePaths, cleanup: cleanupFiles } = writeFilesToTempFiles(
20085
+ const { files: attachmentFiles, cleanup: cleanupFiles } = writeFilesToTempFiles(
19333
20086
  context.config.files,
19334
20087
  context.cwd
19335
20088
  );
20089
+ const runContext = withAutonomousOpenCodePermissions(context);
20090
+ const readOnly = shouldUseReadOnlyRuntimePermissions(context.config);
20091
+ const { child, ready } = startOpenCodeServer(command, runContext);
20092
+ const state = { process: child, iterations: 0, summary: "" };
20093
+ context.onProcessSpawned?.(child);
20094
+ context.registerLivenessProbe?.(() => ({
20095
+ providerAlive: child.exitCode === null,
20096
+ lastRawOutputAgoMs: typeof state.lastRawOutputAtMs === "number" ? Date.now() - state.lastRawOutputAtMs : null
20097
+ }));
20098
+ let sseClose = null;
20099
+ let sessionId = "";
20100
+ let aborted2 = false;
20101
+ const onAbort = () => {
20102
+ aborted2 = true;
20103
+ sseClose?.();
20104
+ killProcessTree(child.pid, { signal: "SIGTERM", child });
20105
+ };
20106
+ context.abortController.signal.addEventListener("abort", onAbort, { once: true });
19336
20107
  try {
19337
- const runContext = withAutonomousOpenCodePermissions(context);
19338
- return await createGenericCliBackend({
19339
- kind: "opencode_cli",
19340
- supportTier: "structured",
19341
- command,
19342
- args: [],
19343
- buildArgs: (ctx, prompt) => {
19344
- const args = ["run", "--format", "json"];
19345
- if (!shouldUseReadOnlyRuntimePermissions(ctx.config)) {
19346
- args.push("--dangerously-skip-permissions");
19347
- }
19348
- const resumeId = ctx.config.runtimeSessionId?.trim() || ctx.config.providerSessionId?.trim();
19349
- if (resumeId) args.push("--session", resumeId);
19350
- const model = ctx.config.selectedModel?.trim();
19351
- if (model?.includes("/")) args.push("--model", model);
19352
- const attachmentPaths = [...imagePaths, ...filePaths];
19353
- const fileArgs = attachmentPaths.flatMap((p) => ["--file", p]);
19354
- if (fileArgs.length > 0) {
19355
- return [...args, ...fileArgs, ...defaultArgs, "--", prompt];
19356
- }
19357
- return [...args, ...defaultArgs, prompt];
19358
- },
19359
- augmentPrompt: (ctx) => {
19360
- const base = buildPromptWithSystem(ctx.config, ctx.promptText);
19361
- return ctx.config.mode === "plan" ? buildPlanModePrefix(base) : base;
19362
- },
19363
- parseStructuredLine: parseOpencodeStructuredLine
19364
- }).run(runContext);
20108
+ const server = await ready;
20109
+ state.lastRawOutputAtMs = Date.now();
20110
+ await waitForHealthy(server.baseUrl, context.abortController.signal);
20111
+ const session = await resolveSession(
20112
+ server.baseUrl,
20113
+ runContext,
20114
+ context.abortController.signal
20115
+ );
20116
+ sessionId = session.id;
20117
+ state.runtimeSessionId = sessionId;
20118
+ const prompt = context.config.mode === "plan" ? buildPlanModePrefix(buildPromptWithSystem(context.config, context.promptText)) : buildPromptWithSystem(context.config, context.promptText);
20119
+ const parts2 = buildPromptParts(prompt, imageFiles, attachmentFiles);
20120
+ const connection = await connectOpenCodeSse(
20121
+ `${server.baseUrl}/event`,
20122
+ context.abortController.signal
20123
+ );
20124
+ sseClose = connection.close;
20125
+ const promptError = await sendPrompt(
20126
+ server.baseUrl,
20127
+ sessionId,
20128
+ runContext,
20129
+ parts2,
20130
+ context.abortController.signal
20131
+ ).then(() => null).catch((error) => ({
20132
+ message: error instanceof Error ? error.message : String(error)
20133
+ }));
20134
+ if (context.abortController.signal.aborted) {
20135
+ } else if (promptError) {
20136
+ state.error = promptError.message;
20137
+ void context.presenter.onError(promptError.message);
20138
+ } else {
20139
+ for await (const event of connection.events) {
20140
+ if (aborted2) break;
20141
+ const done = handleOpenCodeServeEvent(
20142
+ event,
20143
+ sessionId,
20144
+ runContext,
20145
+ state,
20146
+ readOnly,
20147
+ server.baseUrl
20148
+ );
20149
+ if (done) break;
20150
+ }
20151
+ }
20152
+ return buildAgentResult(state, sessionId, aborted2);
20153
+ } catch (error) {
20154
+ const message = error instanceof Error ? error.message : String(error);
20155
+ if (!aborted2 && !context.abortController.signal.aborted) {
20156
+ console.error("[opencode_serve] Run failed:", message);
20157
+ state.error = message;
20158
+ void context.presenter.onError(message);
20159
+ }
20160
+ return buildAgentResult(
20161
+ state,
20162
+ sessionId,
20163
+ aborted2 || context.abortController.signal.aborted
20164
+ );
19365
20165
  } finally {
19366
- cleanup();
20166
+ context.abortController.signal.removeEventListener("abort", onAbort);
20167
+ sseClose?.();
20168
+ killProcessTree(child.pid, { signal: "SIGTERM", child });
20169
+ cleanupImages();
19367
20170
  cleanupFiles();
19368
20171
  }
19369
20172
  }
19370
20173
  };
19371
20174
  }
20175
+ function handleOpenCodeServeEvent(event, sessionId, context, state, readOnly, baseUrl) {
20176
+ const properties = event.properties;
20177
+ const eventSessionId = getString(properties.sessionID);
20178
+ if (eventSessionId && eventSessionId !== sessionId) return false;
20179
+ switch (event.type) {
20180
+ // Live-verified: the SSE bus emits a message.updated (and matching
20181
+ // message.part.updated) for the USER's own message too, not just the
20182
+ // assistant's — track user message ids so their echoed text is never
20183
+ // mistaken for assistant output below.
20184
+ case "message.updated": {
20185
+ const info = getRecord(properties.info);
20186
+ if (getString(info?.role) === "user") {
20187
+ const messageId = getString(info?.id);
20188
+ if (messageId) {
20189
+ state.opencodeUserMessageIds ??= /* @__PURE__ */ new Set();
20190
+ state.opencodeUserMessageIds.add(messageId);
20191
+ }
20192
+ }
20193
+ return false;
20194
+ }
20195
+ case "message.part.updated": {
20196
+ const part = getRecord(properties.part);
20197
+ if (!part) return false;
20198
+ const messageId = getString(part.messageID);
20199
+ if (messageId && state.opencodeUserMessageIds?.has(messageId)) return false;
20200
+ mapOpencodePart(part, context, state);
20201
+ return false;
20202
+ }
20203
+ // Live token-by-token streaming is explicitly out of scope — only the
20204
+ // start/complete snapshots from message.part.updated are consumed.
20205
+ case "message.part.delta":
20206
+ return false;
20207
+ case "permission.asked": {
20208
+ const requestId = getString(properties.id);
20209
+ if (requestId) {
20210
+ void replyToPermission(
20211
+ baseUrl,
20212
+ requestId,
20213
+ readOnly ? "reject" : "once",
20214
+ context.abortController.signal
20215
+ );
20216
+ }
20217
+ return false;
20218
+ }
20219
+ case "session.error": {
20220
+ const errorRecord = getRecord(properties.error);
20221
+ const message = getString(errorRecord?.message) || getString(properties.error) || "OpenCode session error.";
20222
+ if (!state.error) {
20223
+ state.error = message;
20224
+ void context.presenter.onError(message);
20225
+ }
20226
+ return false;
20227
+ }
20228
+ case "session.status": {
20229
+ const status = getRecord(properties.status);
20230
+ return getString(status?.type) === "idle";
20231
+ }
20232
+ default:
20233
+ return false;
20234
+ }
20235
+ }
19372
20236
  function createGenericCliPassthroughBackend(command, defaultArgs = []) {
19373
20237
  return {
19374
20238
  kind: "generic_cli",
@@ -19473,7 +20337,7 @@ function createSupatestCliBackend(command = "supatest", defaultArgs = []) {
19473
20337
  const basePromptText = resumeContextPrefix ? `${resumeContextPrefix}
19474
20338
 
19475
20339
  ${ctx.promptText}` : ctx.promptText;
19476
- const promptText = ctx.config.mode === "plan" ? buildPlanModePrefix(buildPromptWithSystem(ctx.config, basePromptText)) : buildPromptWithSystem(ctx.config, basePromptText);
20340
+ const promptText = ctx.config.mode === "plan" ? buildPlanModePrefix(basePromptText) : basePromptText;
19477
20341
  if (promptText.trim()) {
19478
20342
  contentBlocks.push({ type: "text", text: promptText });
19479
20343
  }
@@ -19597,6 +20461,7 @@ function defaultSelectedModelForBackend(backendKind) {
19597
20461
  case "grok_cli":
19598
20462
  return firstCatalogModelId(backendKind) ?? "grok-4.5";
19599
20463
  case "opencode_cli":
20464
+ case "opencode_serve":
19600
20465
  return void 0;
19601
20466
  case "antigravity_cli":
19602
20467
  return void 0;
@@ -19658,6 +20523,8 @@ var BaseMachineAgent = class _BaseMachineAgent {
19658
20523
  presenter;
19659
20524
  runtime;
19660
20525
  abortController = null;
20526
+ /** Log the "synced MCP session headers" line once per run(), not per respawn. */
20527
+ mcpHeadersLogged = false;
19661
20528
  constructor(presenter, runtime) {
19662
20529
  this.presenter = presenter;
19663
20530
  this.runtime = runtime;
@@ -19714,7 +20581,52 @@ var BaseMachineAgent = class _BaseMachineAgent {
19714
20581
  getActivityHeartbeatIntervalMs() {
19715
20582
  return AGENT_ACTIVITY_HEARTBEAT_MS;
19716
20583
  }
20584
+ /**
20585
+ * Home directory whose CLI config files receive the Alan MCP session headers.
20586
+ * Defaults to the process home (production behaviour); tests override to a
20587
+ * temp dir so runs never touch the developer's real CLI configs.
20588
+ */
20589
+ getMcpConfigHomeDir() {
20590
+ return void 0;
20591
+ }
20592
+ /**
20593
+ * Re-assert this run's Alan MCP identity in the shared CLI config under the
20594
+ * serialization gate, holding the gate until the provider spawns. Returns the
20595
+ * gate release. Concurrent runs on one machine share ~/.codex/config.toml,
20596
+ * ~/.cursor/mcp.json, and ~/.claude.json; the CLI reads its config once at
20597
+ * spawn, so without serialization a concurrent run could rewrite the shared
20598
+ * file between this write and this spawn and steal this run's session identity.
20599
+ */
20600
+ async beginMcpConfigCriticalSection(config) {
20601
+ const conversationId = config.conversationId;
20602
+ if (!conversationId) return () => {
20603
+ };
20604
+ const releaseGate = await acquireMcpConfigWriteGate();
20605
+ try {
20606
+ const writtenClis = syncAlanMcpSessionHeaders({
20607
+ conversationId,
20608
+ teamId: config.teamId,
20609
+ runHeaders: config.alanMcp?.headers,
20610
+ homeDir: this.getMcpConfigHomeDir()
20611
+ });
20612
+ if (writtenClis.length > 0 && !this.mcpHeadersLogged) {
20613
+ this.mcpHeadersLogged = true;
20614
+ void this.presenter.onLog(`Synced Alan MCP session headers to ${writtenClis.join(", ")}`);
20615
+ }
20616
+ } catch (error) {
20617
+ console.warn("[base-machine-agent] Failed to sync MCP session headers", error);
20618
+ }
20619
+ return releaseGate;
20620
+ }
19717
20621
  async runBackendWithActivityHeartbeat(backend, context) {
20622
+ const releaseMcpGate = await this.beginMcpConfigCriticalSection(context.config);
20623
+ let mcpGateReleased = false;
20624
+ const releaseMcpGateOnce = () => {
20625
+ if (mcpGateReleased) return;
20626
+ mcpGateReleased = true;
20627
+ releaseMcpGate();
20628
+ };
20629
+ const callerOnProcessSpawned = context.onProcessSpawned;
19718
20630
  const intervalMs = this.getActivityHeartbeatIntervalMs();
19719
20631
  let heartbeat = null;
19720
20632
  let livenessProbe = null;
@@ -19733,9 +20645,17 @@ var BaseMachineAgent = class _BaseMachineAgent {
19733
20645
  }
19734
20646
  }, intervalMs);
19735
20647
  }
20648
+ const spawnAwareContext = {
20649
+ ...context,
20650
+ onProcessSpawned: (child) => {
20651
+ releaseMcpGateOnce();
20652
+ callerOnProcessSpawned?.(child);
20653
+ }
20654
+ };
19736
20655
  try {
19737
- return await backend.run(context);
20656
+ return await backend.run(spawnAwareContext);
19738
20657
  } finally {
20658
+ releaseMcpGateOnce();
19739
20659
  if (heartbeat) clearInterval(heartbeat);
19740
20660
  }
19741
20661
  }
@@ -19758,6 +20678,7 @@ var BaseMachineAgent = class _BaseMachineAgent {
19758
20678
  case "cursor_agent_cli":
19759
20679
  case "droid_cli":
19760
20680
  case "opencode_cli":
20681
+ case "opencode_serve":
19761
20682
  case "copilot_cli":
19762
20683
  case "supatest_cli":
19763
20684
  case "kimi_cli":
@@ -19825,7 +20746,7 @@ Prefer relative paths and keep your work scoped to this project.`
19825
20746
  const promptText = this.buildPromptText(runtimeConfig);
19826
20747
  const runtimeCommand = runtimeConfig.runtimeCommand || this.runtime.runtimeCommand;
19827
20748
  const runtimeArgs = runtimeConfig.runtimeArgs || this.runtime.runtimeArgs || [];
19828
- const backend = backendKind === "claude_cli" ? createClaudeCliBackend(runtimeCommand || "claude", runtimeArgs) : backendKind === "codex_app_server" ? createCodexRuntimeBackend(runtimeCommand || "codex", runtimeArgs) : backendKind === "copilot_cli" ? createCopilotCliBackend(runtimeCommand || "copilot", runtimeArgs) : backendKind === "cursor_agent_cli" ? createCursorAgentCliBackend(runtimeCommand || "cursor-agent", runtimeArgs) : backendKind === "antigravity_cli" ? createAntigravityCliBackend(runtimeCommand || "agy", runtimeArgs) : backendKind === "droid_cli" ? createDroidCliBackend(runtimeCommand || "droid", runtimeArgs) : backendKind === "opencode_cli" ? createOpencodeCliBackend(runtimeCommand || "opencode", runtimeArgs) : backendKind === "kimi_cli" ? createKimiCliBackend(runtimeCommand || "kimi-cli", runtimeArgs) : backendKind === "grok_cli" ? createGrokCliBackend(runtimeCommand || "grok", runtimeArgs) : backendKind === "supatest_cli" ? createSupatestCliBackend(runtimeCommand || "supatest", runtimeArgs) : createGenericCliPassthroughBackend(
20749
+ const backend = backendKind === "claude_cli" ? createClaudeCliBackend(runtimeCommand || "claude", runtimeArgs) : backendKind === "codex_app_server" ? createCodexRuntimeBackend(runtimeCommand || "codex", runtimeArgs) : backendKind === "copilot_cli" ? createCopilotCliBackend(runtimeCommand || "copilot", runtimeArgs) : backendKind === "cursor_agent_cli" ? createCursorAgentCliBackend(runtimeCommand || "cursor-agent", runtimeArgs) : backendKind === "antigravity_cli" ? createAntigravityCliBackend(runtimeCommand || "agy", runtimeArgs) : backendKind === "droid_cli" ? createDroidCliBackend(runtimeCommand || "droid", runtimeArgs) : backendKind === "opencode_cli" ? createOpencodeCliBackend(runtimeCommand || "opencode", runtimeArgs) : backendKind === "opencode_serve" ? createOpencodeServeBackend(runtimeCommand || "opencode") : backendKind === "kimi_cli" ? createKimiCliBackend(runtimeCommand || "kimi-cli", runtimeArgs) : backendKind === "grok_cli" ? createGrokCliBackend(runtimeCommand || "grok", runtimeArgs) : backendKind === "supatest_cli" ? createSupatestCliBackend(runtimeCommand || "supatest", runtimeArgs) : createGenericCliPassthroughBackend(
19829
20750
  runtimeCommand || "generic-cli",
19830
20751
  runtimeArgs
19831
20752
  );
@@ -19969,19 +20890,7 @@ ${runtimeConfig.task}` } : {}
19969
20890
  this.presenter.onComplete(result);
19970
20891
  return result;
19971
20892
  }
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
- }
20893
+ this.mcpHeadersLogged = false;
19985
20894
  try {
19986
20895
  return await this.runCliBackend(initialConfig, safeProjectPath);
19987
20896
  } catch (error) {
@@ -20550,13 +21459,20 @@ ${meta.description}`);
20550
21459
  }
20551
21460
  if (conversationId) artifactIds.push(`- conversationId: "${conversationId}"`);
20552
21461
  if (artifactIds.length > 0) {
21462
+ const artifactTools = ["create_plan", "create_document_artifact", "create_test_report"];
21463
+ if (taskId || isWorkflowSession) artifactTools.push("upsert_prototype_version");
20553
21464
  parts2.push(
20554
21465
  [
20555
- "When creating artifacts using MCP tools (create_plan, create_document_artifact, create_test_report, upsert_prototype_version), always pass these IDs:",
21466
+ `When creating artifacts using MCP tools (${artifactTools.join(", ")}), always pass these IDs:`,
20556
21467
  ...artifactIds
20557
21468
  ].join("\n")
20558
21469
  );
20559
21470
  }
21471
+ if (teamId && conversationId && !taskId && !isWorkflowSession) {
21472
+ parts2.push(
21473
+ "This is a standalone chat. For create_plan, create_document_artifact, and create_test_report, omit taskId and workflowId; conversationId is the artifact owner. Do not use create_document unless the user explicitly asks for a standalone team Document."
21474
+ );
21475
+ }
20560
21476
  if (workflowId && conversationId && teamId) {
20561
21477
  parts2.push(
20562
21478
  [
@@ -20669,7 +21585,7 @@ function encryptedFileSize(entries) {
20669
21585
  );
20670
21586
  return emptyEnvelopeBytes + base64UrlLength(plaintextBytes);
20671
21587
  }
20672
- var EncryptedEventOutbox = class {
21588
+ var EncryptedEventOutbox = class _EncryptedEventOutbox {
20673
21589
  constructor(path, encodedKey, pushLog = () => {
20674
21590
  }, options = {}) {
20675
21591
  this.path = path;
@@ -20684,6 +21600,11 @@ var EncryptedEventOutbox = class {
20684
21600
  throw new Error("event outbox maxIntermediateEventsPerRun must be a non-negative integer");
20685
21601
  }
20686
21602
  this.entries = this.read();
21603
+ if (!(0, import_node_fs6.existsSync)(this.path)) {
21604
+ this.lastPersistedSignature = _EncryptedEventOutbox.EMPTY_SIGNATURE;
21605
+ } else if (this.entries.length > 0) {
21606
+ this.lastPersistedSignature = this.serializeForPersist().signature;
21607
+ }
20687
21608
  this.enforceCaps();
20688
21609
  this.persist();
20689
21610
  }
@@ -20693,6 +21614,17 @@ var EncryptedEventOutbox = class {
20693
21614
  retryTimer = null;
20694
21615
  /** Latch so a persistent disk failure logs once per streak, not per event. */
20695
21616
  persistFailureLogged = false;
21617
+ /**
21618
+ * Signature of the entries last durably written, so a `persist()` whose entries
21619
+ * are byte-identical to what is already on disk skips the AES-GCM re-encrypt +
21620
+ * temp-file write + rename entirely. Under link flapping the spool is persisted
21621
+ * on every enqueue/ack; this collapses redundant writes (e.g. a reconnect that
21622
+ * replays from head without mutating the queue) to O(delta), not O(spool). Set
21623
+ * to null on any write failure so the next persist retries rather than skips.
21624
+ */
21625
+ lastPersistedSignature = null;
21626
+ /** Sentinel signature for the empty (no-entries) on-disk state. */
21627
+ static EMPTY_SIGNATURE = "empty";
20696
21628
  key;
20697
21629
  maxEncryptedBytes;
20698
21630
  maxIntermediateEventsPerRun;
@@ -20895,20 +21827,28 @@ var EncryptedEventOutbox = class {
20895
21827
  throw new Error(`unable to decrypt durable event outbox: ${detail}`);
20896
21828
  }
20897
21829
  }
21830
+ /** Serialize the current entries to their plaintext + content signature (once). */
21831
+ serializeForPersist() {
21832
+ if (this.entries.length === 0) {
21833
+ return { plaintext: "", signature: _EncryptedEventOutbox.EMPTY_SIGNATURE };
21834
+ }
21835
+ const plaintext = JSON.stringify({ entries: this.entries });
21836
+ return { plaintext, signature: (0, import_node_crypto.createHash)("sha256").update(plaintext).digest("base64url") };
21837
+ }
20898
21838
  persist() {
21839
+ const { plaintext, signature } = this.serializeForPersist();
21840
+ if (signature === this.lastPersistedSignature) return;
20899
21841
  try {
20900
21842
  if (this.entries.length === 0) {
20901
21843
  (0, import_node_fs6.rmSync)(this.path, { force: true });
21844
+ this.lastPersistedSignature = signature;
20902
21845
  this.persistFailureLogged = false;
20903
21846
  return;
20904
21847
  }
20905
21848
  (0, import_node_fs6.mkdirSync)((0, import_node_path5.dirname)(this.path), { recursive: true, mode: 448 });
20906
21849
  const iv = (0, import_node_crypto.randomBytes)(12);
20907
21850
  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
- ]);
21851
+ const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
20912
21852
  const envelope = {
20913
21853
  version: OUTBOX_VERSION,
20914
21854
  iv: iv.toString("base64url"),
@@ -20923,8 +21863,10 @@ var EncryptedEventOutbox = class {
20923
21863
  } finally {
20924
21864
  (0, import_node_fs6.rmSync)(pendingPath, { force: true });
20925
21865
  }
21866
+ this.lastPersistedSignature = signature;
20926
21867
  this.persistFailureLogged = false;
20927
21868
  } catch (error) {
21869
+ this.lastPersistedSignature = null;
20928
21870
  if (!this.persistFailureLogged) {
20929
21871
  this.persistFailureLogged = true;
20930
21872
  const detail = error instanceof Error ? error.message : String(error);
@@ -21637,8 +22579,8 @@ var CONFIG_LOCK_STALE_MS = 3e4;
21637
22579
  var CONFIG_LOCK_WAIT_MS = 2e3;
21638
22580
  var CONFIG_LOCK_POLL_MS = 20;
21639
22581
  function compactProviderVersion(output) {
21640
- const firstLine = output.split(/\r?\n/).map((line) => line.trim()).find(Boolean);
21641
- return firstLine?.slice(0, 120);
22582
+ const firstLine2 = output.split(/\r?\n/).map((line) => line.trim()).find(Boolean);
22583
+ return firstLine2?.slice(0, 120);
21642
22584
  }
21643
22585
  var inspectAlanMcpThroughProviderCli = (backendKind, expectedUrl, home) => {
21644
22586
  const plan = getMcpCliInspectionPlan(backendKind);
@@ -21678,7 +22620,30 @@ var inspectAlanMcpThroughProviderCli = (backendKind, expectedUrl, home) => {
21678
22620
  };
21679
22621
  }
21680
22622
  const providerVersion = compactProviderVersion(versionProbe.stdout);
21681
- const probe = runCliVersionProbe(executable, env, plan.args);
22623
+ const inspectTimeoutMs = Number(process.env.ALAN_MCP_INSPECT_TIMEOUT_MS) || 2e4;
22624
+ const probe = runCliVersionProbe(executable, env, plan.args, inspectTimeoutMs);
22625
+ const timedOut = probe.status === null && (probe.signal === "SIGTERM" || probe.error?.code === "ETIMEDOUT");
22626
+ if (timedOut) {
22627
+ const fileCheck = verifyAlanMcpRegistered(backendKind, home, () => ({
22628
+ ok: true,
22629
+ verification: { method: "file" }
22630
+ }));
22631
+ console.warn("[alan-agent] MCP CLI inspection timed out; falling back to file check", {
22632
+ backendKind,
22633
+ command,
22634
+ inspectTimeoutMs,
22635
+ fileOk: fileCheck.ok
22636
+ });
22637
+ return {
22638
+ ok: fileCheck.ok,
22639
+ issue: fileCheck.ok ? void 0 : {
22640
+ code: "provider_mcp_repair_required",
22641
+ command,
22642
+ message: `Alan tools could not be verified for ${plan.providerLabel}. Repair Alan tools, then retry.`
22643
+ },
22644
+ verification: { method: "file", command, providerVersion }
22645
+ };
22646
+ }
21682
22647
  const classification = classifyMcpCliInspection(
21683
22648
  plan,
21684
22649
  {
@@ -21771,9 +22736,7 @@ var CODEX_ALAN_TOML_SECTION = "mcp_servers.alan";
21771
22736
  function isCodexAlanTomlSection(section) {
21772
22737
  return section === CODEX_ALAN_TOML_SECTION || section.startsWith(`${CODEX_ALAN_TOML_SECTION}.`);
21773
22738
  }
21774
- function mergeCodexAlanSection(path, existingContent, desiredContent) {
21775
- if (existingContent === null || existingContent.trim() === "") return desiredContent;
21776
- assertValidExistingToml(path, existingContent);
22739
+ function stripCodexAlanSection(existingContent) {
21777
22740
  const lines = existingContent.split("\n");
21778
22741
  const retained = [];
21779
22742
  let insideAlanSection = false;
@@ -21784,7 +22747,12 @@ function mergeCodexAlanSection(path, existingContent, desiredContent) {
21784
22747
  }
21785
22748
  if (!insideAlanSection) retained.push(line);
21786
22749
  }
21787
- const prefix = retained.join("\n").trimEnd();
22750
+ return retained.join("\n").trimEnd();
22751
+ }
22752
+ function mergeCodexAlanSection(path, existingContent, desiredContent) {
22753
+ if (existingContent === null || existingContent.trim() === "") return desiredContent;
22754
+ assertValidExistingToml(path, existingContent);
22755
+ const prefix = stripCodexAlanSection(existingContent);
21788
22756
  const merged = `${prefix}${prefix ? "\n\n" : ""}${desiredContent.trim()}
21789
22757
  `;
21790
22758
  try {
@@ -21870,6 +22838,45 @@ function verifyAlanMcpRegistered(backendKind, home = (0, import_node_os6.homedir
21870
22838
  verification: inspection.verification
21871
22839
  };
21872
22840
  }
22841
+ function removeAlanFromJsonConfig(path, existingContent) {
22842
+ const config = parseJsonObject2(path, existingContent);
22843
+ for (const containerKey of ["mcpServers", "mcp"]) {
22844
+ const container = config[containerKey];
22845
+ if (container && typeof container === "object" && !Array.isArray(container)) {
22846
+ delete container.alan;
22847
+ }
22848
+ }
22849
+ return JSON.stringify(config);
22850
+ }
22851
+ function unregisterAlanMcp(backendKind, home = (0, import_node_os6.homedir)()) {
22852
+ const removedPaths = [];
22853
+ const errors = [];
22854
+ const files = alanMcpConfigFilesForBackend(backendKind, {}, home);
22855
+ for (const file of files) {
22856
+ if (!(0, import_node_fs7.existsSync)(file.path)) continue;
22857
+ const releaseLock = waitForConfigLock(`${file.path}.alan-lock`);
22858
+ try {
22859
+ const current = (0, import_node_fs7.readFileSync)(file.path, "utf8");
22860
+ let next;
22861
+ if (file.path.endsWith(".toml")) {
22862
+ const stripped = stripCodexAlanSection(current);
22863
+ next = stripped ? `${stripped}
22864
+ ` : "";
22865
+ } else {
22866
+ next = removeAlanFromJsonConfig(file.path, current);
22867
+ }
22868
+ if (next !== current) {
22869
+ atomicWriteManagedConfig(file.path, next);
22870
+ removedPaths.push(file.path);
22871
+ }
22872
+ } catch (err) {
22873
+ errors.push(err instanceof Error ? err.message : String(err));
22874
+ } finally {
22875
+ releaseLock();
22876
+ }
22877
+ }
22878
+ return { removedPaths, errors };
22879
+ }
21873
22880
  function isHttpUrl(value2) {
21874
22881
  if (typeof value2 !== "string") return false;
21875
22882
  try {
@@ -22216,6 +23223,88 @@ function extractCodexDiscoveredModelIdsFromDebugJson(value2) {
22216
23223
  }
22217
23224
  return [...new Set(ids)];
22218
23225
  }
23226
+ var OPENCODE_MODEL_ID_LINE_PATTERN = /^[a-z0-9][\w.-]*\/[\w.-]+$/i;
23227
+ var OPENCODE_KNOWN_VARIANT_KEYS = /* @__PURE__ */ new Set([
23228
+ "minimal",
23229
+ "none",
23230
+ "low",
23231
+ "medium",
23232
+ "high",
23233
+ "xhigh",
23234
+ "extra-high",
23235
+ "max",
23236
+ "ultra"
23237
+ ]);
23238
+ function parseOpenCodeVerboseModelOutput(text) {
23239
+ const entries = [];
23240
+ const lines = text.split(/\r?\n/);
23241
+ let i = 0;
23242
+ while (i < lines.length) {
23243
+ const line = (lines[i] ?? "").trim();
23244
+ i += 1;
23245
+ if (!OPENCODE_MODEL_ID_LINE_PATTERN.test(line)) continue;
23246
+ let jsonStart = i;
23247
+ while (jsonStart < lines.length && (lines[jsonStart] ?? "").trim() === "") jsonStart += 1;
23248
+ if (jsonStart >= lines.length || !(lines[jsonStart] ?? "").trim().startsWith("{")) continue;
23249
+ let depth = 0;
23250
+ let inString = false;
23251
+ let escaped = false;
23252
+ let endLineIdx = -1;
23253
+ let buffer = "";
23254
+ for (let j = jsonStart; j < lines.length; j += 1) {
23255
+ const raw = lines[j] ?? "";
23256
+ buffer += (buffer ? "\n" : "") + raw;
23257
+ for (const ch of raw) {
23258
+ if (escaped) {
23259
+ escaped = false;
23260
+ continue;
23261
+ }
23262
+ if (ch === "\\" && inString) {
23263
+ escaped = true;
23264
+ continue;
23265
+ }
23266
+ if (ch === '"') {
23267
+ inString = !inString;
23268
+ continue;
23269
+ }
23270
+ if (inString) continue;
23271
+ if (ch === "{") depth += 1;
23272
+ else if (ch === "}") depth -= 1;
23273
+ }
23274
+ if (depth === 0) {
23275
+ endLineIdx = j;
23276
+ break;
23277
+ }
23278
+ }
23279
+ if (endLineIdx === -1) break;
23280
+ try {
23281
+ const obj = JSON.parse(buffer);
23282
+ entries.push({
23283
+ id: line,
23284
+ reasoning: obj.capabilities?.reasoning === true,
23285
+ variants: obj.variants ? Object.keys(obj.variants) : []
23286
+ });
23287
+ } catch {
23288
+ }
23289
+ i = endLineIdx + 1;
23290
+ }
23291
+ return entries;
23292
+ }
23293
+ function extractOpenCodeDiscoveredModelIdsFromVerboseOutput(text) {
23294
+ const entries = parseOpenCodeVerboseModelOutput(text);
23295
+ const ids = [];
23296
+ for (const entry of entries) {
23297
+ const knownVariants = entry.reasoning ? entry.variants.filter((variant) => OPENCODE_KNOWN_VARIANT_KEYS.has(variant)) : [];
23298
+ if (knownVariants.length === 0) {
23299
+ ids.push(entry.id);
23300
+ continue;
23301
+ }
23302
+ for (const variant of knownVariants) {
23303
+ ids.push(`${entry.id}-${variant}`);
23304
+ }
23305
+ }
23306
+ return [...new Set(ids)];
23307
+ }
22219
23308
  function extractModelIdsFromText(value2) {
22220
23309
  const ansiPattern = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
22221
23310
  const ignoredLinePattern = /^(available models|models|usage|error|flag provided|try |agy\b|warning:)/i;
@@ -22326,12 +23415,24 @@ async function probeJsonOrTextModels(executable, env, argSets, options) {
22326
23415
  return [];
22327
23416
  }
22328
23417
  async function discoverOpenCodeModels(executable, env) {
22329
- const probed = await probeJsonOrTextModels(executable, env, [
23418
+ const verbose = await runCliProbeAsync(
23419
+ executable,
23420
+ withModelProbeEnv(env),
23421
+ ["models", "--verbose"],
23422
+ MODEL_DISCOVERY_TIMEOUT_MS
23423
+ );
23424
+ if (verbose && !verbose.error && verbose.status === 0 && verbose.stdout.trim()) {
23425
+ const withEffort = extractOpenCodeDiscoveredModelIdsFromVerboseOutput(verbose.stdout);
23426
+ if (withEffort.length > 0) return withEffort;
23427
+ }
23428
+ const plain = await probeJsonOrTextModels(executable, env, [["models"]]);
23429
+ if (plain.length > 0) return plain;
23430
+ const legacy = await probeJsonOrTextModels(executable, env, [
22330
23431
  ["models", "--json"],
22331
23432
  ["models", "list", "--json"],
22332
23433
  ["model", "list", "--json"]
22333
23434
  ]);
22334
- return probed.length > 0 ? probed : OPENCODE_ZEN_MODEL_IDS;
23435
+ return legacy.length > 0 ? legacy : OPENCODE_ZEN_MODEL_IDS;
22335
23436
  }
22336
23437
  async function discoverAntigravityModels(executable, env) {
22337
23438
  return probeJsonOrTextModels(executable, env, [["models"]], {
@@ -22577,7 +23678,7 @@ var RunStartGate = class {
22577
23678
  };
22578
23679
 
22579
23680
  // src/version.ts
22580
- var AGENT_VERSION = "0.1.42";
23681
+ var AGENT_VERSION = "0.1.44";
22581
23682
 
22582
23683
  // src/workspace-relocation.ts
22583
23684
  var import_node_child_process3 = require("child_process");
@@ -22653,11 +23754,19 @@ var import_node_child_process4 = require("child_process");
22653
23754
  var import_node_fs11 = require("fs");
22654
23755
  var import_node_path9 = require("path");
22655
23756
  var WORKSPACE_LEASE_CHECK_INTERVAL_MS = 1e3;
23757
+ function parsePositiveMs(value2, fallback) {
23758
+ const parsed = value2 ? Number(value2) : Number.NaN;
23759
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
23760
+ }
23761
+ var WORKSPACE_LEASE_GIT_TIMEOUT_MS = parsePositiveMs(
23762
+ process.env.ALAN_WORKSPACE_LEASE_GIT_TIMEOUT_MS,
23763
+ 2e3
23764
+ );
22656
23765
  function gitValue(cwd, args) {
22657
23766
  const result = (0, import_node_child_process4.spawnSync)("git", ["-C", cwd, ...args], {
22658
23767
  encoding: "utf8",
22659
23768
  env: getDaemonCliEnvironment(),
22660
- timeout: 2e3
23769
+ timeout: WORKSPACE_LEASE_GIT_TIMEOUT_MS
22661
23770
  });
22662
23771
  if (result.status !== 0) return null;
22663
23772
  return result.stdout.trim() || null;
@@ -22693,6 +23802,13 @@ var DEFAULT_PROBE2 = {
22693
23802
  } catch {
22694
23803
  return null;
22695
23804
  }
23805
+ },
23806
+ pathExists: (workspacePath) => {
23807
+ try {
23808
+ return (0, import_node_fs11.existsSync)((0, import_node_path9.resolve)(workspacePath));
23809
+ } catch {
23810
+ return true;
23811
+ }
22696
23812
  }
22697
23813
  };
22698
23814
  function unavailableFailure(target) {
@@ -22740,6 +23856,57 @@ var WorkspaceRunLease = class _WorkspaceRunLease {
22740
23856
  failure: null
22741
23857
  };
22742
23858
  }
23859
+ /**
23860
+ * Classify the lease state for the run monitor.
23861
+ *
23862
+ * - `transient: true` — a null snapshot while the workspace path still exists:
23863
+ * the git probe timed out or failed transiently (OS throttling / IO
23864
+ * contention), NOT a vanished checkout. The monitor should retry (require N
23865
+ * consecutive transient failures) before declaring the workspace lost.
23866
+ * - `transient: false` — a definitive change: the path is genuinely missing
23867
+ * (ENOENT), or the workspace/repository/worktree/branch identity changed.
23868
+ * These are real and fail fast.
23869
+ */
23870
+ verifyDetailed() {
23871
+ for (const captured of this.captured) {
23872
+ const current = this.probe.snapshot(captured.target.workspacePath);
23873
+ if (!current) {
23874
+ const pathGone = this.probe.pathExists?.(captured.target.workspacePath) === false;
23875
+ return { failure: unavailableFailure(captured.target), transient: !pathGone };
23876
+ }
23877
+ if (current.workspaceIdentity !== captured.identity.workspaceIdentity) {
23878
+ return {
23879
+ failure: {
23880
+ code: "workspace_lease_path_changed",
23881
+ 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.",
23882
+ workspacePath: captured.target.workspacePath,
23883
+ expectedBranch: captured.identity.branch,
23884
+ actualBranch: current.branch
23885
+ },
23886
+ transient: false
23887
+ };
23888
+ }
23889
+ if (current.repositoryIdentity !== captured.identity.repositoryIdentity || current.worktreeIdentity !== captured.identity.worktreeIdentity) {
23890
+ return {
23891
+ failure: {
23892
+ code: "workspace_lease_repository_changed",
23893
+ 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.",
23894
+ workspacePath: captured.target.workspacePath,
23895
+ expectedBranch: captured.identity.branch,
23896
+ actualBranch: current.branch
23897
+ },
23898
+ transient: false
23899
+ };
23900
+ }
23901
+ if (current.branch !== captured.identity.branch) {
23902
+ return {
23903
+ failure: branchFailure(captured.target, captured.identity.branch, current.branch),
23904
+ transient: false
23905
+ };
23906
+ }
23907
+ }
23908
+ return { failure: null, transient: false };
23909
+ }
22743
23910
  verify() {
22744
23911
  for (const captured of this.captured) {
22745
23912
  const current = this.probe.snapshot(captured.target.workspacePath);
@@ -22769,6 +23936,40 @@ var WorkspaceRunLease = class _WorkspaceRunLease {
22769
23936
  return null;
22770
23937
  }
22771
23938
  };
23939
+ var FRESH_LEASE_MONITOR_STATE = {
23940
+ transientStreak: 0,
23941
+ nextCheckAtMs: 0
23942
+ };
23943
+ function isLeaseCheckDue(state, nowMs) {
23944
+ return nowMs >= state.nextCheckAtMs;
23945
+ }
23946
+ function applyLeaseCheckResult(input) {
23947
+ const { detailed, nowMs, policy } = input;
23948
+ if (!detailed.failure) {
23949
+ return { action: "ok", state: { ...FRESH_LEASE_MONITOR_STATE } };
23950
+ }
23951
+ if (!detailed.transient) {
23952
+ return { action: "fail", state: input.state, failure: detailed.failure };
23953
+ }
23954
+ const transientStreak = input.state.transientStreak + 1;
23955
+ if (transientStreak >= policy.maxTransientFailures) {
23956
+ return {
23957
+ action: "fail",
23958
+ state: { transientStreak, nextCheckAtMs: input.state.nextCheckAtMs },
23959
+ failure: detailed.failure
23960
+ };
23961
+ }
23962
+ const backoffMs = Math.min(
23963
+ policy.backoffBaseMs * 2 ** (transientStreak - 1),
23964
+ policy.backoffMaxMs
23965
+ );
23966
+ return {
23967
+ action: "transient",
23968
+ state: { transientStreak, nextCheckAtMs: nowMs + backoffMs },
23969
+ failure: detailed.failure,
23970
+ backoffMs
23971
+ };
23972
+ }
22772
23973
 
22773
23974
  // src/daemon.ts
22774
23975
  var STAGING_API_URL = process.env.ALAN_STAGING_API_URL ?? "https://staging-api.tryalan.ai";
@@ -24087,9 +25288,12 @@ function reconcileActiveRuns(input) {
24087
25288
  if (!staleByGrace && !staleByDeadPid) continue;
24088
25289
  if (awaitingAsk && staleByGrace && !staleByDeadPid) continue;
24089
25290
  if (staleByDeadPid && awaitingAsk) {
25291
+ entry.presenter.onComplete(buildAbortResult("provider_exited"));
24090
25292
  entry.agent.kill();
24091
25293
  input.activeAgents.delete(runId);
24092
- input.pushLog?.(`reconciled dead provider pid run=${runId} (awaiting ask, no interrupt)`);
25294
+ input.pushLog?.(
25295
+ `reconciled dead provider pid run=${runId} (awaiting ask \u2014 finalized for resume)`
25296
+ );
24093
25297
  continue;
24094
25298
  }
24095
25299
  abortActiveAgent(entry, { reason: staleByDeadPid ? "provider_exited" : "runner_stalled" });
@@ -24618,6 +25822,21 @@ async function startDaemon(args) {
24618
25822
  recentLogs.push(`${(/* @__PURE__ */ new Date()).toISOString()} ${message}`);
24619
25823
  if (recentLogs.length > 200) recentLogs.splice(0, recentLogs.length - 200);
24620
25824
  };
25825
+ const alanMcpRegisteredRuns = /* @__PURE__ */ new Set();
25826
+ const alanMcpRegisteredBackends = /* @__PURE__ */ new Set();
25827
+ const finalizeAlanMcpForRun = (runId) => {
25828
+ if (!alanMcpRegisteredRuns.delete(runId)) return;
25829
+ if (alanMcpRegisteredRuns.size > 0) return;
25830
+ for (const backend of alanMcpRegisteredBackends) {
25831
+ try {
25832
+ const { removedPaths } = unregisterAlanMcp(backend);
25833
+ if (removedPaths.length > 0) pushLog(`mcp-unregistered backend=${backend} run=${runId}`);
25834
+ } catch (err) {
25835
+ pushLog(`mcp-unregister-failed backend=${backend} ${err.message}`);
25836
+ }
25837
+ }
25838
+ alanMcpRegisteredBackends.clear();
25839
+ };
24621
25840
  const workspaceAccessTracker = new WorkspaceAccessTracker();
24622
25841
  const recheckWorkspaceIssue = () => {
24623
25842
  const previousPath = workspaceAccessTracker.recoveryPath();
@@ -24830,6 +26049,10 @@ async function startDaemon(args) {
24830
26049
  const versionRefresh = setInterval(() => {
24831
26050
  void refreshProviderVersions();
24832
26051
  }, VERSION_REFRESH_INTERVAL_MS);
26052
+ const parsedMaxTransient = Number(process.env.ALAN_WORKSPACE_LEASE_MAX_TRANSIENT_FAILURES);
26053
+ const WORKSPACE_LEASE_MAX_TRANSIENT_FAILURES = Number.isFinite(parsedMaxTransient) && parsedMaxTransient >= 1 ? Math.floor(parsedMaxTransient) : 3;
26054
+ const WORKSPACE_LEASE_FAILURE_BACKOFF_BASE_MS = 1e3;
26055
+ const WORKSPACE_LEASE_FAILURE_BACKOFF_MAX_MS = 15e3;
24833
26056
  const stopRunForWorkspaceLease = (runId, entry, failure) => {
24834
26057
  entry.stage = "failed";
24835
26058
  if (!entry.presenter.failWorkspaceLease(failure)) return;
@@ -24837,15 +26060,39 @@ async function startDaemon(args) {
24837
26060
  activeAgents.delete(runId);
24838
26061
  pushLog(`workspace-lease-lost run=${runId} code=${failure.code}`);
24839
26062
  };
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;
26063
+ const leaseCheckPolicy = {
26064
+ maxTransientFailures: WORKSPACE_LEASE_MAX_TRANSIENT_FAILURES,
26065
+ backoffBaseMs: WORKSPACE_LEASE_FAILURE_BACKOFF_BASE_MS,
26066
+ backoffMaxMs: WORKSPACE_LEASE_FAILURE_BACKOFF_MAX_MS
26067
+ };
26068
+ const verifyActiveWorkspaceLease = (runId, entry, nowMs) => {
26069
+ const lease = entry.workspaceLease;
26070
+ if (!lease) return true;
26071
+ const state = entry.leaseCheck ?? { ...FRESH_LEASE_MONITOR_STATE };
26072
+ entry.leaseCheck = state;
26073
+ if (!isLeaseCheckDue(state, nowMs)) return true;
26074
+ const decision = applyLeaseCheckResult({
26075
+ state,
26076
+ detailed: lease.verifyDetailed(),
26077
+ nowMs,
26078
+ policy: leaseCheckPolicy
26079
+ });
26080
+ entry.leaseCheck = decision.state;
26081
+ if (decision.action === "fail" && decision.failure) {
26082
+ stopRunForWorkspaceLease(runId, entry, decision.failure);
26083
+ return false;
26084
+ }
26085
+ if (decision.action === "transient" && decision.failure) {
26086
+ pushLog(
26087
+ `workspace-lease-transient run=${runId} streak=${decision.state.transientStreak}/${WORKSPACE_LEASE_MAX_TRANSIENT_FAILURES} code=${decision.failure.code} backoff_ms=${decision.backoffMs ?? 0}`
26088
+ );
26089
+ }
26090
+ return true;
24845
26091
  };
24846
26092
  const workspaceLeaseMonitor = setInterval(() => {
26093
+ const nowMs = Date.now();
24847
26094
  for (const [runId, entry] of activeAgents) {
24848
- verifyActiveWorkspaceLease(runId, entry);
26095
+ verifyActiveWorkspaceLease(runId, entry, nowMs);
24849
26096
  }
24850
26097
  }, WORKSPACE_LEASE_CHECK_INTERVAL_MS);
24851
26098
  workspaceLeaseMonitor.unref?.();
@@ -25322,7 +26569,7 @@ async function startDaemon(args) {
25322
26569
  },
25323
26570
  () => {
25324
26571
  const entry = activeAgents.get(payload.runId);
25325
- return entry ? verifyActiveWorkspaceLease(payload.runId, entry) : false;
26572
+ return entry ? verifyActiveWorkspaceLease(payload.runId, entry, Date.now()) : false;
25326
26573
  }
25327
26574
  );
25328
26575
  let agent;
@@ -25372,6 +26619,10 @@ async function startDaemon(args) {
25372
26619
  workspaceLease: workspaceLeaseCapture.lease
25373
26620
  });
25374
26621
  pendingRunStarts.delete(payload.runId);
26622
+ if (payload.alanMcp && backendKind) {
26623
+ alanMcpRegisteredRuns.add(payload.runId);
26624
+ alanMcpRegisteredBackends.add(backendKind);
26625
+ }
25375
26626
  void agent.run({
25376
26627
  task: payload.content,
25377
26628
  maxIterations: payload.maxIterations ?? 50,
@@ -25390,6 +26641,7 @@ async function startDaemon(args) {
25390
26641
  taskMeta: payload.taskMeta,
25391
26642
  prMeta: payload.prMeta,
25392
26643
  conversationId: payload.conversationId,
26644
+ alanMcp: payload.alanMcp,
25393
26645
  taskId: payload.taskId ?? payload.taskMeta?.id,
25394
26646
  teamId: payload.teamId,
25395
26647
  currentUser: payload.currentUser,
@@ -25416,6 +26668,7 @@ async function startDaemon(args) {
25416
26668
  } catch {
25417
26669
  pushLog(`run-journal-remove-failed run=${payload.runId}`);
25418
26670
  }
26671
+ finalizeAlanMcpForRun(payload.runId);
25419
26672
  releaseActiveRun();
25420
26673
  });
25421
26674
  });
@@ -25441,7 +26694,7 @@ async function startDaemon(args) {
25441
26694
  ([, entry]) => entry.conversationId === payload.conversationId
25442
26695
  ) : [...activeAgents.entries()];
25443
26696
  const delivered = entries.some(([runId, entry]) => {
25444
- if (!entry || !verifyActiveWorkspaceLease(runId, entry)) return false;
26697
+ if (!entry || !verifyActiveWorkspaceLease(runId, entry, Date.now())) return false;
25445
26698
  return entry.agent.sendToolResponse(payload.toolId, payload.response) === true;
25446
26699
  });
25447
26700
  if (!delivered) {
@@ -26208,6 +27461,18 @@ function omitUndefined(record) {
26208
27461
  return Object.fromEntries(Object.entries(record).filter(([, value2]) => value2 !== void 0));
26209
27462
  }
26210
27463
 
27464
+ // src/resume-fallback-env.ts
27465
+ var RESUME_FALLBACK_CONTEXT_ENV = "ALAN_RESUME_FALLBACK_CONTEXT_B64";
27466
+ function decodeResumeFallbackContext(encoded) {
27467
+ if (!encoded) return void 0;
27468
+ try {
27469
+ const decoded = Buffer.from(encoded, "base64").toString("utf8").trim();
27470
+ return decoded.length > 0 ? decoded : void 0;
27471
+ } catch {
27472
+ return void 0;
27473
+ }
27474
+ }
27475
+
26211
27476
  // src/web-presenter.ts
26212
27477
  var WebPresenter = class {
26213
27478
  constructor(wsClient, _conversationId, cwd) {
@@ -26500,6 +27765,7 @@ var SandboxEventDispatcher = class {
26500
27765
  fireAndForget(event);
26501
27766
  return;
26502
27767
  }
27768
+ if (isBackgroundHeartbeat(event) && !this.outboxSocket.connected) return;
26503
27769
  this.outbox.enqueue(this.conversationId, this.currentRunId ?? this.conversationId, event);
26504
27770
  this.outbox.flush(this.outboxSocket);
26505
27771
  }
@@ -26619,7 +27885,7 @@ var WSClient = class {
26619
27885
  if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
26620
27886
  this.heartbeatTimer = setInterval(() => {
26621
27887
  if (!this.socket.connected) return;
26622
- this.socket.emit("agent.heartbeat", this.buildHeartbeat());
27888
+ this.socket.volatile.emit("agent.heartbeat", this.buildHeartbeat());
26623
27889
  }, AGENT_HEARTBEAT_INTERVAL_MS);
26624
27890
  this.heartbeatTimer.unref?.();
26625
27891
  }
@@ -26702,6 +27968,7 @@ var WSClient = class {
26702
27968
  images: data.images,
26703
27969
  files: data.files,
26704
27970
  providerSessionId: data.providerSessionId,
27971
+ resumeFallbackContext: data.resumeFallbackContext,
26705
27972
  backendKind: data.backendKind,
26706
27973
  agentId: data.agentId,
26707
27974
  agentPrompt: data.agentPrompt,
@@ -26893,6 +28160,7 @@ async function runSandbox(config) {
26893
28160
  const presenter = new WebPresenter(wsClient, config.sessionId, config.projectPath);
26894
28161
  presenter.setSuppressSessionLifecycle(true);
26895
28162
  let lastProviderSessionId = config.providerSessionId;
28163
+ let resumeFallbackContext = config.resumeFallbackContext;
26896
28164
  let activeBackendKind = config.backendKind || "claude_cli";
26897
28165
  let activeAgentId = config.agentId;
26898
28166
  let activeAgentPrompt = config.agentPrompt;
@@ -26976,10 +28244,12 @@ async function runSandbox(config) {
26976
28244
  selectedContextWindow: activeContextWindow ?? null,
26977
28245
  selectedEffortLevel: activeEffortLevel ?? null,
26978
28246
  providerSessionId: lastProviderSessionId ?? void 0,
28247
+ resumeFallbackContext,
26979
28248
  taskMeta: currentTaskMeta,
26980
28249
  prMeta: currentPrMeta,
26981
28250
  taskId: currentTaskId,
26982
28251
  conversationId: currentConversationId,
28252
+ alanMcp: currentAlanMcp,
26983
28253
  teamId: currentTeamId,
26984
28254
  workflowId,
26985
28255
  workflowExecutionId,
@@ -27027,6 +28297,7 @@ async function runSandbox(config) {
27027
28297
  }
27028
28298
  }
27029
28299
  currentAgent = null;
28300
+ resumeFallbackContext = void 0;
27030
28301
  lifecycle.info("sandbox_agent_run_complete", {
27031
28302
  success: result.success,
27032
28303
  iterations: result.iterations,
@@ -27053,6 +28324,7 @@ async function runSandbox(config) {
27053
28324
  currentFiles = nextPayload.files ?? [];
27054
28325
  if (nextPayload.providerSessionId) {
27055
28326
  lastProviderSessionId = nextPayload.providerSessionId;
28327
+ resumeFallbackContext = nextPayload.resumeFallbackContext;
27056
28328
  }
27057
28329
  if (nextPayload.backendKind) {
27058
28330
  activeBackendKind = nextPayload.backendKind;
@@ -27383,7 +28655,7 @@ async function runSessionFromEnv() {
27383
28655
  const task = process.env.ALAN_TASK;
27384
28656
  const teamId = process.env.ALAN_TEAM_ID || void 0;
27385
28657
  const projectPath = process.env.ALAN_PROJECT_PATH || "/home/user/workspace";
27386
- const backendKind = process.env.ALAN_BACKEND_KIND || "claude_cli";
28658
+ const backendKind = process.env.ALAN_BACKEND_KIND || "codex_app_server";
27387
28659
  const agentId = process.env.ALAN_AGENT_ID || void 0;
27388
28660
  const agentPrompt = process.env.ALAN_AGENT_PROMPT || void 0;
27389
28661
  const mode = process.env.ALAN_MODE || "agent";
@@ -27391,6 +28663,9 @@ async function runSessionFromEnv() {
27391
28663
  const contextWindow = process.env.ALAN_CONTEXT_WINDOW || void 0;
27392
28664
  const effortLevel = process.env.ALAN_EFFORT_LEVEL || void 0;
27393
28665
  const providerSessionId = process.env.ALAN_PROVIDER_SESSION_ID || void 0;
28666
+ const resumeFallbackContext = decodeResumeFallbackContext(
28667
+ process.env[RESUME_FALLBACK_CONTEXT_ENV]
28668
+ );
27394
28669
  const workflowId = process.env.ALAN_WORKFLOW_ID || void 0;
27395
28670
  const workflowExecutionId = process.env.ALAN_WORKFLOW_EXECUTION_ID || void 0;
27396
28671
  const sandboxId = process.env.ALAN_SANDBOX_ID || void 0;
@@ -27432,6 +28707,7 @@ async function runSessionFromEnv() {
27432
28707
  contextWindow,
27433
28708
  effortLevel,
27434
28709
  providerSessionId,
28710
+ resumeFallbackContext,
27435
28711
  workflowId,
27436
28712
  workflowExecutionId,
27437
28713
  sandboxId,