@aiden-ade/sandbox-agent 0.1.41 → 0.1.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.cjs +891 -183
  2. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -13315,13 +13315,13 @@ function resolveViaWhere(command, env) {
13315
13315
  }
13316
13316
  return null;
13317
13317
  }
13318
- function runCliVersionProbe(executable, env, args = ["--version"]) {
13318
+ function runCliVersionProbe(executable, env, args = ["--version"], timeoutMs = 3e3) {
13319
13319
  const isWin = (0, import_node_os3.platform)() === "win32";
13320
13320
  if (isWin && /\.(cmd|bat)$/i.test(executable)) {
13321
13321
  const comSpec = env.ComSpec ?? process.env.ComSpec ?? "cmd.exe";
13322
13322
  return (0, import_node_child_process2.spawnSync)(comSpec, ["/d", "/s", "/c", executable, ...args], {
13323
13323
  encoding: "utf8",
13324
- timeout: 3e3,
13324
+ timeout: timeoutMs,
13325
13325
  env,
13326
13326
  windowsHide: true
13327
13327
  });
@@ -13329,7 +13329,7 @@ function runCliVersionProbe(executable, env, args = ["--version"]) {
13329
13329
  const useShell = isWin && !/[\\/]/.test(executable);
13330
13330
  return (0, import_node_child_process2.spawnSync)(executable, args, {
13331
13331
  encoding: "utf8",
13332
- timeout: 3e3,
13332
+ timeout: timeoutMs,
13333
13333
  env,
13334
13334
  shell: useShell,
13335
13335
  windowsHide: isWin ? true : void 0
@@ -13514,6 +13514,16 @@ function resolveBackendRuntimeCommand(backendKind, env = getDaemonCliEnvironment
13514
13514
  cacheResolvedCli(command, resolved);
13515
13515
  return resolved ?? void 0;
13516
13516
  }
13517
+ function revalidateBackendRuntimeCommand(backendKind, env = getDaemonCliEnvironment()) {
13518
+ if (!backendKind) return void 0;
13519
+ const command = BACKEND_CLI_COMMANDS[backendKind];
13520
+ if (!command) return void 0;
13521
+ resolvedCliCache.delete(command);
13522
+ const spec = Object.values(PROVIDER_CLI_COMMANDS).find((entry) => entry.command === command);
13523
+ const envOverride = spec ? spec.envVars.map((name) => env[name]?.trim()).find(Boolean) : void 0;
13524
+ if (envOverride) resolvedCliCache.delete(envOverride);
13525
+ return resolveBackendRuntimeCommand(backendKind, env);
13526
+ }
13517
13527
 
13518
13528
  // src/computer-readiness.ts
13519
13529
  var import_node_fs5 = require("fs");
@@ -14545,15 +14555,6 @@ If the index returns no useful path, say so and run one targeted local search in
14545
14555
  function buildAlanTeamCodeContextOverlay(teamId) {
14546
14556
  return [buildAlanTeamScopePrompt(teamId), ALAN_CODE_CONTEXT_PROMPT].join("\n\n");
14547
14557
  }
14548
- function buildCodeDiscoveryEnforcementTail() {
14549
- return [
14550
- "## MANDATORY \u2014 index once, then use local source",
14551
- "1. For codebase discovery, make one `mcp__alan__search_code_context` call (hybrid, topK 5). Omit teamId \u2014 Alan session headers resolve it.",
14552
- "2. Once useful paths are returned, stop querying the index and read those local files. The current worktree is the source of truth.",
14553
- "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.",
14554
- "Do not repeat or rephrase index searches once paths are known, and do not claim current behavior from index excerpts alone."
14555
- ].join("\n");
14556
- }
14557
14558
  var PLAN_MODE_PROMPT = [
14558
14559
  "You are in Alan plan mode.",
14559
14560
  "Analyze the task, inspect the relevant code and context, and produce a concrete implementation plan.",
@@ -15282,11 +15283,12 @@ var import_os3 = require("os");
15282
15283
  var import_path3 = require("path");
15283
15284
  var import_readline = require("readline");
15284
15285
  var import_child_process2 = require("child_process");
15286
+ var import_child_process3 = require("child_process");
15285
15287
  var import_crypto2 = require("crypto");
15286
15288
  var import_fs4 = require("fs");
15287
15289
  var import_os4 = require("os");
15288
15290
  var import_path4 = require("path");
15289
- var import_child_process3 = require("child_process");
15291
+ var import_child_process4 = require("child_process");
15290
15292
  var import_util10 = require("util");
15291
15293
  var import_fs5 = require("fs");
15292
15294
  var import_os5 = require("os");
@@ -15432,6 +15434,11 @@ function buildSessionHeaders(input) {
15432
15434
  if (input.allowedTools?.length) {
15433
15435
  headers["x-allowed-tools"] = input.allowedTools.join(",");
15434
15436
  }
15437
+ if (input.runHeaders) {
15438
+ for (const [key, value2] of Object.entries(input.runHeaders)) {
15439
+ if (typeof value2 === "string" && value2.length > 0) headers[key] = value2;
15440
+ }
15441
+ }
15435
15442
  return headers;
15436
15443
  }
15437
15444
  function getTomlSectionName(line) {
@@ -15642,6 +15649,23 @@ function syncAlanMcpSessionHeaders(input) {
15642
15649
  }
15643
15650
  return written;
15644
15651
  }
15652
+ var mcpConfigWriteGateTail = Promise.resolve();
15653
+ function acquireMcpConfigWriteGate() {
15654
+ const previous = mcpConfigWriteGateTail;
15655
+ let releaseCurrent;
15656
+ const current = new Promise((resolve22) => {
15657
+ releaseCurrent = resolve22;
15658
+ });
15659
+ mcpConfigWriteGateTail = previous.then(() => current);
15660
+ return previous.then(() => {
15661
+ let released = false;
15662
+ return () => {
15663
+ if (released) return;
15664
+ released = true;
15665
+ releaseCurrent();
15666
+ };
15667
+ });
15668
+ }
15645
15669
  function buildPromptWithSystem(config, promptText) {
15646
15670
  const parts2 = [
15647
15671
  config.systemPrompt?.trim(),
@@ -15650,6 +15674,13 @@ function buildPromptWithSystem(config, promptText) {
15650
15674
  ].filter((value2) => Boolean(value2 && value2.length > 0));
15651
15675
  return parts2.join("\n\n");
15652
15676
  }
15677
+ function buildResumeAwarePrompt(config, promptText, options) {
15678
+ const systemParts = options.isResume ? [config.systemPromptAppend?.trim()] : [config.systemPrompt?.trim()];
15679
+ const parts2 = [...systemParts, promptText.trim()].filter(
15680
+ (value2) => Boolean(value2 && value2.length > 0)
15681
+ );
15682
+ return parts2.join("\n\n");
15683
+ }
15653
15684
  function buildPlanModePrefix(promptText) {
15654
15685
  return [
15655
15686
  "You are in plan-only mode.",
@@ -15816,7 +15847,7 @@ var ERROR_SPECS = {
15816
15847
  recoveryClass: "needs_env"
15817
15848
  },
15818
15849
  auth_expired: {
15819
- message: "The provider connection expired. Reconnect it in Cloud settings, then restart or recreate the machine.",
15850
+ message: "The provider connection expired. Re-authenticate the agent CLI on this runtime (or reconnect it in your provider/Cloud settings), then retry.",
15820
15851
  recoveryClass: "user_fixable"
15821
15852
  },
15822
15853
  subscription_required: {
@@ -15876,6 +15907,7 @@ function specForErrorKind(errorKind) {
15876
15907
  const spec = ERROR_SPECS[errorKind];
15877
15908
  return { message: spec.message, errorKind, recoveryClass: spec.recoveryClass };
15878
15909
  }
15910
+ var RESUME_SESSION_UNAVAILABLE_MESSAGE = ERROR_SPECS.resume_failed.message;
15879
15911
  function isLikelyProviderAuthError(stderr) {
15880
15912
  const lower = stderr.toLowerCase();
15881
15913
  return lower.includes("api key") || lower.includes("api_key") || lower.includes("invalid api key") || lower.includes("api key invalid") || lower.includes("unauthorized") || lower.includes("authentication failed") || lower.includes("authentication error") || lower.includes("provider settings") || lower.includes("invalid token") || lower.includes("token expired");
@@ -15905,7 +15937,7 @@ function normalizeCodexCliErrorMessage(message) {
15905
15937
  return "Codex connection to OpenAI timed out while waiting for the turn to continue. Retry the message; if it repeats, check network/API latency or reduce slow MCP/tool calls in the turn.";
15906
15938
  }
15907
15939
  if (lower.includes("access token could not be refreshed") || lower.includes("refresh_token_invalidated") || lower.includes("refresh token has been invalidated")) {
15908
- return "Codex CLI connection expired. Reconnect OpenAI Codex CLI in Cloud settings, then restart or recreate the machine.";
15940
+ return "Codex CLI connection expired. Re-authenticate the Codex CLI on this computer (run `codex login`) or reconnect it in Cloud settings, then retry.";
15909
15941
  }
15910
15942
  return message;
15911
15943
  }
@@ -16031,6 +16063,54 @@ function spawnCli(command, args, context) {
16031
16063
  detached: process.platform !== "win32"
16032
16064
  });
16033
16065
  }
16066
+ function isAlreadyDead(error) {
16067
+ return error?.code === "ESRCH";
16068
+ }
16069
+ function killProcessTree(pid, options = {}) {
16070
+ const { signal = "SIGTERM", child, onError } = options;
16071
+ if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) {
16072
+ if (child) {
16073
+ try {
16074
+ child.kill(signal);
16075
+ } catch (err) {
16076
+ if (!isAlreadyDead(err)) onError?.("fallback", err);
16077
+ }
16078
+ }
16079
+ return;
16080
+ }
16081
+ if (process.platform === "win32") {
16082
+ try {
16083
+ const killer = (0, import_child_process3.spawn)("taskkill", ["/pid", String(pid), "/t", "/f"], {
16084
+ stdio: "ignore",
16085
+ windowsHide: true
16086
+ });
16087
+ killer.once("error", (err) => {
16088
+ if (child) {
16089
+ try {
16090
+ child.kill();
16091
+ } catch {
16092
+ }
16093
+ }
16094
+ onError?.("taskkill", err);
16095
+ });
16096
+ } catch (err) {
16097
+ onError?.("taskkill", err);
16098
+ }
16099
+ return;
16100
+ }
16101
+ try {
16102
+ process.kill(-pid, signal);
16103
+ } catch (groupErr) {
16104
+ if (!isAlreadyDead(groupErr)) onError?.("group", groupErr);
16105
+ if (child) {
16106
+ try {
16107
+ child.kill(signal);
16108
+ } catch (fallbackErr) {
16109
+ if (!isAlreadyDead(fallbackErr)) onError?.("fallback", fallbackErr);
16110
+ }
16111
+ }
16112
+ }
16113
+ }
16034
16114
  function emitSessionNotice(presenter, kind, message) {
16035
16115
  if (presenter.onNotice) {
16036
16116
  void presenter.onNotice(kind, message);
@@ -16095,6 +16175,12 @@ function formatCliSpawnError(command, error) {
16095
16175
  }
16096
16176
  return error;
16097
16177
  }
16178
+ function waitForSpawnOutcome(child) {
16179
+ return new Promise((resolve22) => {
16180
+ child.once("spawn", () => resolve22(null));
16181
+ child.once("error", (error) => resolve22(error));
16182
+ });
16183
+ }
16098
16184
  function createGenericCliBackend(options) {
16099
16185
  return {
16100
16186
  kind: options.kind,
@@ -16108,7 +16194,23 @@ function createGenericCliBackend(options) {
16108
16194
  };
16109
16195
  const prompt = options.augmentPrompt?.(context) ?? (context.config.mode === "plan" ? buildPlanModePrefix(buildPromptWithSystem(context.config, context.promptText)) : buildPromptWithSystem(context.config, context.promptText));
16110
16196
  const args = options.buildArgs?.(context, prompt) ?? options.args.map((arg) => arg === "{{prompt}}" ? prompt : arg);
16111
- const child = spawnCli(options.command, args, context);
16197
+ let command = options.command;
16198
+ let child = spawnCli(command, args, context);
16199
+ if (context.revalidateCommand) {
16200
+ const spawnError = await waitForSpawnOutcome(child);
16201
+ if (spawnError) {
16202
+ const fresh = spawnError.code === "ENOENT" ? context.revalidateCommand()?.trim() || null : null;
16203
+ if (fresh && fresh !== command) {
16204
+ console.warn(
16205
+ `[${options.kind}] Spawn failed ENOENT for "${command}" \u2014 re-resolved to "${fresh}", retrying once`
16206
+ );
16207
+ command = fresh;
16208
+ child = spawnCli(command, args, context);
16209
+ } else {
16210
+ throw formatCliSpawnError(command, spawnError);
16211
+ }
16212
+ }
16213
+ }
16112
16214
  state.process = child;
16113
16215
  state.lastRawOutputAtMs = Date.now();
16114
16216
  context.onProcessSpawned?.(child);
@@ -16119,8 +16221,8 @@ function createGenericCliBackend(options) {
16119
16221
  const safeArgs = args.map(
16120
16222
  (a, i) => i > 0 && args[i - 1] === "--system-prompt" ? `"<system-prompt ${a.length} chars>"` : a
16121
16223
  );
16122
- console.info(`[${options.kind}] Spawning: ${options.command} ${safeArgs.join(" ")}`);
16123
- presenter.recordRawTranscript?.("system", `${options.command} ${args.join(" ")}`, {
16224
+ console.info(`[${options.kind}] Spawning: ${command} ${safeArgs.join(" ")}`);
16225
+ presenter.recordRawTranscript?.("system", `${command} ${args.join(" ")}`, {
16124
16226
  backendKind: options.kind
16125
16227
  });
16126
16228
  child.stdin.on("error", (err) => {
@@ -16152,14 +16254,7 @@ function createGenericCliBackend(options) {
16152
16254
  console.warn(
16153
16255
  `[${options.kind}] No stdout within ${options.startupTimeoutMs}ms of spawn \u2014 killing hung process`
16154
16256
  );
16155
- try {
16156
- if (process.platform === "win32") {
16157
- child.kill("SIGKILL");
16158
- } else if (child.pid) {
16159
- process.kill(-child.pid, "SIGKILL");
16160
- }
16161
- } catch {
16162
- }
16257
+ killProcessTree(child.pid, { signal: "SIGKILL", child });
16163
16258
  }, options.startupTimeoutMs);
16164
16259
  }
16165
16260
  stdoutRl.on("line", (line) => {
@@ -16202,16 +16297,7 @@ function createGenericCliBackend(options) {
16202
16297
  });
16203
16298
  child.on("exit", (code) => resolve22(code ?? 0));
16204
16299
  const onAbort = () => {
16205
- const pid = child.pid;
16206
- if (!pid) return;
16207
- try {
16208
- process.kill(-pid, "SIGTERM");
16209
- } catch {
16210
- try {
16211
- child.kill("SIGTERM");
16212
- } catch {
16213
- }
16214
- }
16300
+ killProcessTree(child.pid, { signal: "SIGTERM", child });
16215
16301
  };
16216
16302
  context.abortController.signal.addEventListener("abort", onAbort, { once: true });
16217
16303
  });
@@ -16232,6 +16318,9 @@ function createGenericCliBackend(options) {
16232
16318
  } else if (state.resultDeferredOnBackgroundWork && state.activeBackgroundTaskIds?.size) {
16233
16319
  idleTimeoutReason = "background_task";
16234
16320
  resolve22("idle_timeout");
16321
+ } else if (options.postStartupSilenceTimeoutMs && sawStdoutLine && typeof state.lastRawOutputAtMs === "number" && Date.now() - state.lastRawOutputAtMs >= options.postStartupSilenceTimeoutMs) {
16322
+ idleTimeoutReason = "post_startup_silence";
16323
+ resolve22("idle_timeout");
16235
16324
  } else {
16236
16325
  armIdleTimer();
16237
16326
  }
@@ -16244,17 +16333,11 @@ function createGenericCliBackend(options) {
16244
16333
  if (idleTimer) clearTimeout(idleTimer);
16245
16334
  if (raceResult === "idle_timeout") {
16246
16335
  idleTimedOut = true;
16336
+ const idleWaitLabel = idleTimeoutReason === "background_task" ? "a background task terminal event" : idleTimeoutReason === "post_startup_silence" ? "any further output (silent hang)" : "a pending user answer";
16247
16337
  console.warn(
16248
- `[${options.kind}] Idle timeout (${IDLE_MS}ms) waiting for ${idleTimeoutReason === "background_task" ? "a background task terminal event" : "a pending user answer"} \u2014 force-killing orphaned process`
16338
+ `[${options.kind}] Idle timeout (${IDLE_MS}ms) waiting for ${idleWaitLabel} \u2014 force-killing orphaned process`
16249
16339
  );
16250
- try {
16251
- if (process.platform === "win32") {
16252
- child.kill("SIGKILL");
16253
- } else if (child.pid) {
16254
- process.kill(-child.pid, "SIGKILL");
16255
- }
16256
- } catch {
16257
- }
16340
+ killProcessTree(child.pid, { signal: "SIGKILL", child });
16258
16341
  exitCode = await Promise.race([
16259
16342
  exitPromise,
16260
16343
  new Promise((resolve22) => setTimeout(() => resolve22(1), 5e3))
@@ -16270,14 +16353,7 @@ function createGenericCliBackend(options) {
16270
16353
  console.warn(
16271
16354
  `[${options.kind}] Process still alive ${GRACE_MS}ms after result event, killing`
16272
16355
  );
16273
- try {
16274
- if (process.platform === "win32") {
16275
- child.kill("SIGKILL");
16276
- } else if (child.pid) {
16277
- process.kill(-child.pid, "SIGKILL");
16278
- }
16279
- } catch {
16280
- }
16356
+ killProcessTree(child.pid, { signal: "SIGKILL", child });
16281
16357
  }
16282
16358
  } else {
16283
16359
  exitCode = raceResult;
@@ -16332,6 +16408,25 @@ function createGenericCliBackend(options) {
16332
16408
  console.error(`[${options.kind}] Process force-killed by startup watchdog`, {
16333
16409
  stderrLines: stderrLines.slice(-10)
16334
16410
  });
16411
+ const startupStderr = stderrLines.join("\n").trim();
16412
+ if (startupStderr) {
16413
+ const classified = classifyCliErrorDetailed(startupStderr, exitCode);
16414
+ return {
16415
+ success: false,
16416
+ summary: state.summary.trim() || classified.message,
16417
+ filesModified: [],
16418
+ planFilesCreated: [],
16419
+ iterations: Math.max(state.iterations, 1),
16420
+ error: classified.message,
16421
+ ...classified.errorKind !== "unknown_cli_error" ? { errorKind: classified.errorKind } : {},
16422
+ recoveryClass: classified.recoveryClass,
16423
+ providerSessionId: state.runtimeSessionId,
16424
+ runtimeSessionId: state.runtimeSessionId,
16425
+ backendKind: options.kind,
16426
+ supportTier: options.supportTier,
16427
+ usage: state.usage
16428
+ };
16429
+ }
16335
16430
  return {
16336
16431
  success: false,
16337
16432
  summary: "Agent produced no output",
@@ -16381,7 +16476,7 @@ function createGenericCliBackend(options) {
16381
16476
  filesModified: [],
16382
16477
  planFilesCreated: [],
16383
16478
  iterations: Math.max(state.iterations, 1),
16384
- error: idleTimeoutReason === "background_task" ? "Agent timed out waiting for a background task to finish \u2014 no terminal event arrived, so the run was stopped. Send your message again to continue." : "Agent timed out waiting for your answer to its question \u2014 no response arrived, so the run was stopped. Send your reply as a new message to continue.",
16479
+ error: idleTimeoutReason === "background_task" ? "Agent timed out waiting for a background task to finish \u2014 no terminal event arrived, so the run was stopped. Send your message again to continue." : idleTimeoutReason === "post_startup_silence" ? "The agent went silent and stopped responding, so the run was stopped. Send your message again to retry." : "Agent timed out waiting for your answer to its question \u2014 no response arrived, so the run was stopped. Send your reply as a new message to continue.",
16385
16480
  providerSessionId: state.runtimeSessionId,
16386
16481
  runtimeSessionId: state.runtimeSessionId,
16387
16482
  backendKind: options.kind,
@@ -16440,6 +16535,9 @@ function createGenericCliBackend(options) {
16440
16535
  classifiedErrorKind = detailed.errorKind;
16441
16536
  classifiedRecoveryClass = detailed.recoveryClass;
16442
16537
  }
16538
+ if (classifiedErrorKind === "resume_failed" && !requestedResumeId) {
16539
+ classifiedErrorKind = void 0;
16540
+ }
16443
16541
  }
16444
16542
  return {
16445
16543
  success: !failed,
@@ -16448,7 +16546,11 @@ function createGenericCliBackend(options) {
16448
16546
  planFilesCreated: [],
16449
16547
  iterations: Math.max(state.iterations, 1),
16450
16548
  error: classifiedError,
16451
- ...classifiedErrorKind ? { errorKind: classifiedErrorKind } : {},
16549
+ // Surface every recognized, machine-actionable kind (model_mismatch,
16550
+ // resume_failed, auth_*, …). The catch-all unknown_cli_error carries no routing
16551
+ // signal — nothing downstream keys on it — so leave errorKind absent for it,
16552
+ // keeping an unrelated failure untagged even when a resume id was set.
16553
+ ...classifiedErrorKind && classifiedErrorKind !== "unknown_cli_error" ? { errorKind: classifiedErrorKind } : {},
16452
16554
  ...classifiedRecoveryClass ? { recoveryClass: classifiedRecoveryClass } : {},
16453
16555
  providerSessionId: state.runtimeSessionId,
16454
16556
  runtimeSessionId: state.runtimeSessionId,
@@ -16906,7 +17008,7 @@ function claudeSessionLogExists(input) {
16906
17008
  if (!(0, import_fs4.existsSync)(projectDir)) return false;
16907
17009
  return (0, import_fs4.existsSync)((0, import_path4.join)(projectDir, `${input.sessionId}.jsonl`));
16908
17010
  }
16909
- var execFileAsync = (0, import_util10.promisify)(import_child_process3.execFile);
17011
+ var execFileAsync = (0, import_util10.promisify)(import_child_process4.execFile);
16910
17012
  var flagSupportCache = /* @__PURE__ */ new Map();
16911
17013
  function helpAdvertisesFlag(helpText, flag) {
16912
17014
  return helpText.includes(flag);
@@ -17082,6 +17184,16 @@ var TERMINAL_TASK_NOTIFICATION_STATUSES = /* @__PURE__ */ new Set([
17082
17184
  "cancelled",
17083
17185
  "canceled"
17084
17186
  ]);
17187
+ var NON_TERMINAL_TASK_NOTIFICATION_STATUSES = /* @__PURE__ */ new Set([
17188
+ "running",
17189
+ "in_progress",
17190
+ "inprogress",
17191
+ "started",
17192
+ "pending",
17193
+ "active",
17194
+ "working",
17195
+ "queued"
17196
+ ]);
17085
17197
  function extractToolResultContent(content) {
17086
17198
  if (typeof content === "string") return content;
17087
17199
  if (!Array.isArray(content)) return "";
@@ -17099,6 +17211,7 @@ function stringField2(record, key) {
17099
17211
  function isTerminalTaskNotification(record) {
17100
17212
  const status = stringField2(record, "status")?.toLowerCase();
17101
17213
  if (status && TERMINAL_TASK_NOTIFICATION_STATUSES.has(status)) return true;
17214
+ if (status && NON_TERMINAL_TASK_NOTIFICATION_STATUSES.has(status)) return false;
17102
17215
  return typeof record.duration_ms === "number" || typeof record.durationMs === "number" || typeof record.total_tokens === "number" || typeof record.totalTokens === "number" || typeof record.tool_uses === "number" || typeof record.toolUses === "number";
17103
17216
  }
17104
17217
  function isFailedTaskNotification(record) {
@@ -17190,20 +17303,25 @@ function handleClaudeStructuredEvent(parsed, context, state) {
17190
17303
  state.iterations += 1;
17191
17304
  const message = typeof parsed.message === "object" && parsed.message !== null ? parsed.message : null;
17192
17305
  const content = Array.isArray(message?.content) ? message.content : [];
17306
+ let previousBlockWasText = false;
17193
17307
  for (const block of content) {
17194
17308
  if (typeof block !== "object" || block === null || !("type" in block)) continue;
17195
17309
  if (block.type === "text" && typeof block.text === "string") {
17196
17310
  const text = block.text;
17197
17311
  state.summary += `${text}
17198
17312
  `;
17199
- void presenter.onAssistantText(text);
17313
+ void presenter.onAssistantText(previousBlockWasText ? `
17314
+ ${text}` : text);
17315
+ previousBlockWasText = true;
17200
17316
  continue;
17201
17317
  }
17202
17318
  if (block.type === "thinking" && typeof block.thinking === "string") {
17319
+ previousBlockWasText = false;
17203
17320
  void presenter.onThinking(block.thinking);
17204
17321
  continue;
17205
17322
  }
17206
17323
  if (block.type === "tool_use" && typeof block.id === "string" && typeof block.name === "string") {
17324
+ previousBlockWasText = false;
17207
17325
  const toolBlock = block;
17208
17326
  void presenter.onToolUse(
17209
17327
  toolBlock.name,
@@ -17319,7 +17437,7 @@ function handleClaudeStructuredEvent(parsed, context, state) {
17319
17437
  state.iterations = parsed.num_turns;
17320
17438
  }
17321
17439
  const subtype = typeof parsed.subtype === "string" ? parsed.subtype : "";
17322
- if ((subtype === "error_during_execution" || subtype === "error_max_turns") && !state.error) {
17440
+ if (subtype.startsWith("error") && !state.error) {
17323
17441
  const errObj = typeof parsed.error === "object" && parsed.error !== null ? parsed.error : null;
17324
17442
  const resumeId = context.config.runtimeSessionId?.trim() || context.config.providerSessionId?.trim();
17325
17443
  const errMsg = subtype === "error_during_execution" && resumeId ? SESSION_RESUME_FAILED_MESSAGE : typeof parsed.error === "string" && parsed.error.trim() || errObj && typeof errObj.message === "string" && errObj.message.trim() || typeof parsed.message === "string" && parsed.message.trim() || typeof parsed.result === "string" && parsed.result.trim() || subtype;
@@ -17470,7 +17588,7 @@ function createClaudeCliBackend(command = "claude", defaultArgs = []) {
17470
17588
  const basePromptText = resumeContextPrefix ? `${resumeContextPrefix}
17471
17589
 
17472
17590
  ${ctx.promptText}` : ctx.promptText;
17473
- const promptText = ctx.config.mode === "plan" ? buildPlanModePrefix(buildPromptWithSystem(ctx.config, basePromptText)) : buildPromptWithSystem(ctx.config, basePromptText);
17591
+ const promptText = ctx.config.mode === "plan" ? buildPlanModePrefix(basePromptText) : basePromptText;
17474
17592
  if (promptText.trim()) {
17475
17593
  contentBlocks.push({ type: "text", text: promptText });
17476
17594
  }
@@ -17561,6 +17679,25 @@ function buildGeneratedImageFromCodexPayload(payload) {
17561
17679
  idFields: []
17562
17680
  });
17563
17681
  }
17682
+ function extractRolloutMessageText(payload) {
17683
+ const content = payload.content;
17684
+ if (typeof content === "string") return content;
17685
+ if (Array.isArray(content)) {
17686
+ for (const block of content) {
17687
+ if (block && typeof block === "object" && typeof block.text === "string") {
17688
+ return block.text;
17689
+ }
17690
+ }
17691
+ }
17692
+ return "";
17693
+ }
17694
+ function isSyntheticUserRolloutMessage(payload) {
17695
+ const text = extractRolloutMessageText(payload).trimStart();
17696
+ if (!text) return false;
17697
+ const lower = text.toLowerCase();
17698
+ return lower.startsWith("<environment_context") || lower.startsWith("<user_instructions") || // Auto-compaction bridge summaries are tagged in the rollout content.
17699
+ lower.startsWith("<compact") || lower.startsWith("[compact");
17700
+ }
17564
17701
  function latestUserMessageLineIndex(lines) {
17565
17702
  let latestIndex = -1;
17566
17703
  for (let index = 0; index < lines.length; index += 1) {
@@ -17568,7 +17705,7 @@ function latestUserMessageLineIndex(lines) {
17568
17705
  if (!line) continue;
17569
17706
  const entry = parseJsonObject(line);
17570
17707
  const payload = entry && typeof entry.payload === "object" && entry.payload !== null ? entry.payload : null;
17571
- if (payload?.type === "message" && payload.role === "user") {
17708
+ if (payload?.type === "message" && payload.role === "user" && !isSyntheticUserRolloutMessage(payload)) {
17572
17709
  latestIndex = index;
17573
17710
  }
17574
17711
  }
@@ -17632,7 +17769,9 @@ async function replayCodexMcpToolEventsFromSessionLog(context, state) {
17632
17769
  }
17633
17770
  }
17634
17771
  var CODEX_EXIT_GRACE_MS = 3e4;
17635
- function armCodexExitGraceKill(state) {
17772
+ var CODEX_BACKGROUND_EXIT_GRACE_MS = 10 * 6e4;
17773
+ var CODEX_EXIT_GRACE_SIGKILL_MS = 3e3;
17774
+ function armCodexExitGraceKill(state, graceMs = CODEX_EXIT_GRACE_MS) {
17636
17775
  if (state.exitGraceKillTimer) return;
17637
17776
  const child = state.process;
17638
17777
  if (!child) return;
@@ -17641,19 +17780,16 @@ function armCodexExitGraceKill(state) {
17641
17780
  console.warn(
17642
17781
  "[codex_app_server] Process did not exit after turn completion; killing process group"
17643
17782
  );
17644
- const pid = child.pid;
17645
- if (pid) {
17646
- try {
17647
- process.kill(-pid, "SIGTERM");
17648
- return;
17649
- } catch {
17650
- }
17651
- }
17652
- try {
17653
- child.kill("SIGTERM");
17654
- } catch {
17655
- }
17656
- }, CODEX_EXIT_GRACE_MS);
17783
+ killProcessTree(child.pid, { signal: "SIGTERM", child });
17784
+ const killTimer = setTimeout(() => {
17785
+ if (child.exitCode !== null) return;
17786
+ console.warn(
17787
+ "[codex_app_server] Process still alive after grace SIGTERM; escalating to SIGKILL"
17788
+ );
17789
+ killProcessTree(child.pid, { signal: "SIGKILL", child });
17790
+ }, CODEX_EXIT_GRACE_SIGKILL_MS);
17791
+ killTimer.unref?.();
17792
+ }, graceMs);
17657
17793
  timer.unref?.();
17658
17794
  state.exitGraceKillTimer = timer;
17659
17795
  }
@@ -17662,6 +17798,167 @@ function disarmCodexExitGraceKill(state) {
17662
17798
  clearTimeout(state.exitGraceKillTimer);
17663
17799
  state.exitGraceKillTimer = void 0;
17664
17800
  }
17801
+ function completeCodexTurnRespectingWorkers(state) {
17802
+ disarmCodexExitGraceKill(state);
17803
+ const activeWorkers = state.activeBackgroundTaskIds?.size ?? 0;
17804
+ if (activeWorkers > 0) {
17805
+ state.resultDeferredOnBackgroundWork = true;
17806
+ state.codexAwaitingWorkersAfterTurnEnd = true;
17807
+ console.info(
17808
+ `[codex_app_server] Turn completed with ${activeWorkers} background worker id(s) still active \u2014 deferring exit grace`
17809
+ );
17810
+ armCodexExitGraceKill(state, CODEX_BACKGROUND_EXIT_GRACE_MS);
17811
+ return;
17812
+ }
17813
+ armCodexExitGraceKill(state);
17814
+ }
17815
+ function touchCodexBackgroundGrace(state) {
17816
+ if (!state.codexAwaitingWorkersAfterTurnEnd) return;
17817
+ disarmCodexExitGraceKill(state);
17818
+ armCodexExitGraceKill(state, CODEX_BACKGROUND_EXIT_GRACE_MS);
17819
+ }
17820
+ function maybeReleaseCodexDeferredTurn(state) {
17821
+ if (!state.codexAwaitingWorkersAfterTurnEnd) return;
17822
+ if (state.activeBackgroundTaskIds?.size) return;
17823
+ state.codexAwaitingWorkersAfterTurnEnd = false;
17824
+ state.resultDeferredOnBackgroundWork = false;
17825
+ disarmCodexExitGraceKill(state);
17826
+ armCodexExitGraceKill(state);
17827
+ console.info(
17828
+ "[codex_app_server] All background workers finished after turn completion \u2014 arming normal exit grace"
17829
+ );
17830
+ }
17831
+ var TERMINAL_COLLAB_AGENT_STATUSES = /* @__PURE__ */ new Set([
17832
+ "completed",
17833
+ "errored",
17834
+ "shutdown",
17835
+ "interrupted",
17836
+ "not_found"
17837
+ ]);
17838
+ var FAILED_COLLAB_AGENT_STATUSES = /* @__PURE__ */ new Set(["errored", "interrupted", "not_found"]);
17839
+ var COLLAB_COORDINATION_TOOL_NAMES = {
17840
+ send_input: "collab_send_input",
17841
+ wait: "collab_wait",
17842
+ close_agent: "collab_close_agent"
17843
+ };
17844
+ function firstLine(text, maxLength = 140) {
17845
+ const line = text.split("\n").find((candidate) => candidate.trim().length > 0)?.trim() ?? "";
17846
+ return line.length > maxLength ? `${line.slice(0, maxLength - 1)}\u2026` : line;
17847
+ }
17848
+ function finishCodexWorker(context, state, options) {
17849
+ const launcherId = options.launcherId ?? (options.threadId ? state.backgroundToolIdByTaskId?.get(options.threadId) : void 0) ?? (options.agentPath ? state.codexWorkerToolIdByPath?.get(options.agentPath) : void 0);
17850
+ if (!launcherId) {
17851
+ if (options.threadId) clearBackgroundTaskIds(state, [options.threadId]);
17852
+ maybeReleaseCodexDeferredTurn(state);
17853
+ return;
17854
+ }
17855
+ const launcherStillActive = state.activeBackgroundTaskIds?.has(launcherId) === true || (state.backgroundTaskIdsByToolId?.get(launcherId)?.size ?? 0) > 0;
17856
+ if (!launcherStillActive) {
17857
+ if (options.threadId) clearBackgroundTaskIds(state, [options.threadId]);
17858
+ maybeReleaseCodexDeferredTurn(state);
17859
+ return;
17860
+ }
17861
+ clearBackgroundTaskIds(state, [options.threadId ?? launcherId]);
17862
+ const remainingSiblings = state.backgroundTaskIdsByToolId?.get(launcherId)?.size ?? 0;
17863
+ if (remainingSiblings === 0) {
17864
+ clearBackgroundTaskIds(state, [launcherId]);
17865
+ const isError = FAILED_COLLAB_AGENT_STATUSES.has(options.status);
17866
+ const resultText = options.message?.trim() || `Background worker ${options.status}.`;
17867
+ void context.presenter.onToolResult?.(launcherId, resultText, isError);
17868
+ }
17869
+ maybeReleaseCodexDeferredTurn(state);
17870
+ }
17871
+ function applyCodexCollabAgentStates(context, state, agentsStates) {
17872
+ for (const [threadId, raw] of Object.entries(agentsStates)) {
17873
+ if (!raw || typeof raw !== "object") continue;
17874
+ const entry = raw;
17875
+ const status = typeof entry.status === "string" ? entry.status : "";
17876
+ if (!TERMINAL_COLLAB_AGENT_STATUSES.has(status)) continue;
17877
+ finishCodexWorker(context, state, {
17878
+ threadId,
17879
+ status,
17880
+ message: typeof entry.message === "string" ? entry.message : void 0
17881
+ });
17882
+ }
17883
+ }
17884
+ function summarizeCodexAgentStates(agentsStates) {
17885
+ return Object.entries(agentsStates).map(([threadId, raw]) => {
17886
+ const entry = raw && typeof raw === "object" ? raw : {};
17887
+ const status = typeof entry.status === "string" ? entry.status : "unknown";
17888
+ const message = typeof entry.message === "string" && entry.message.trim() ? ` \u2014 ${firstLine(entry.message)}` : "";
17889
+ return `${threadId}: ${status}${message}`;
17890
+ }).join("\n");
17891
+ }
17892
+ function handleCodexCollabToolCallItem(context, state, item, phase) {
17893
+ const itemId = typeof item.id === "string" ? item.id : "";
17894
+ const tool = typeof item.tool === "string" ? item.tool : "";
17895
+ if (!itemId || !tool) return true;
17896
+ const senderThreadId = typeof item.sender_thread_id === "string" ? item.sender_thread_id : "";
17897
+ const receiverThreadIds = Array.isArray(item.receiver_thread_ids) ? item.receiver_thread_ids.filter(
17898
+ (value2) => typeof value2 === "string" && value2.length > 0
17899
+ ) : [];
17900
+ const prompt = typeof item.prompt === "string" ? item.prompt : "";
17901
+ const agentsStates = typeof item.agents_states === "object" && item.agents_states !== null ? item.agents_states : {};
17902
+ const itemStatus = typeof item.status === "string" ? item.status : "";
17903
+ if (tool === "spawn_agent") {
17904
+ if (trackCodexStreamedToolId(state, itemId)) {
17905
+ const parentToolUseId = senderThreadId ? state.backgroundToolIdByTaskId?.get(senderThreadId) ?? null : null;
17906
+ void context.presenter.onToolUse(
17907
+ "Agent",
17908
+ {
17909
+ subagent_type: "codex-worker",
17910
+ description: firstLine(prompt) || "Codex background worker",
17911
+ ...prompt ? { prompt } : {},
17912
+ ...receiverThreadIds.length > 0 ? { agent_thread_ids: receiverThreadIds } : {}
17913
+ },
17914
+ itemId,
17915
+ parentToolUseId
17916
+ );
17917
+ registerBackgroundLauncher(state, itemId);
17918
+ }
17919
+ for (const threadId of receiverThreadIds) {
17920
+ if (state.backgroundToolIdByTaskId?.get(threadId)) continue;
17921
+ registerBackgroundTaskRecord(state, { task_id: threadId, tool_use_id: itemId });
17922
+ }
17923
+ applyCodexCollabAgentStates(context, state, agentsStates);
17924
+ if (phase === "completed" && itemStatus === "failed") {
17925
+ finishCodexWorker(context, state, {
17926
+ launcherId: itemId,
17927
+ status: "errored",
17928
+ message: "Failed to spawn background worker."
17929
+ });
17930
+ }
17931
+ return true;
17932
+ }
17933
+ const coordinationToolName = COLLAB_COORDINATION_TOOL_NAMES[tool];
17934
+ if (!coordinationToolName) return false;
17935
+ if (trackCodexStreamedToolId(state, itemId)) {
17936
+ void context.presenter.onToolUse(
17937
+ coordinationToolName,
17938
+ {
17939
+ ...receiverThreadIds.length > 0 ? { receiver_thread_ids: receiverThreadIds } : {},
17940
+ ...prompt ? { prompt } : {}
17941
+ },
17942
+ itemId
17943
+ );
17944
+ }
17945
+ if (phase === "completed") {
17946
+ const isError = itemStatus === "failed";
17947
+ const summary = summarizeCodexAgentStates(agentsStates);
17948
+ void context.presenter.onToolResult?.(
17949
+ itemId,
17950
+ summary || (isError ? "Failed." : "Done."),
17951
+ isError
17952
+ );
17953
+ }
17954
+ applyCodexCollabAgentStates(context, state, agentsStates);
17955
+ if (tool === "close_agent" && phase === "completed" && itemStatus !== "failed") {
17956
+ for (const threadId of receiverThreadIds) {
17957
+ finishCodexWorker(context, state, { threadId, status: "shutdown" });
17958
+ }
17959
+ }
17960
+ return true;
17961
+ }
17665
17962
  function trackCodexStreamedToolId(state, toolId) {
17666
17963
  if (!state.codexStreamedToolIds) state.codexStreamedToolIds = /* @__PURE__ */ new Set();
17667
17964
  if (state.codexStreamedToolIds.has(toolId)) return false;
@@ -17692,6 +17989,7 @@ function handleCodexStructuredEvent(parsed, context, state) {
17692
17989
  const presenter = context.presenter;
17693
17990
  const type = typeof parsed.type === "string" ? parsed.type : "";
17694
17991
  if (!type) return false;
17992
+ touchCodexBackgroundGrace(state);
17695
17993
  if (typeof parsed.thread_id === "string") {
17696
17994
  state.runtimeSessionId = parsed.thread_id;
17697
17995
  }
@@ -17716,7 +18014,12 @@ function handleCodexStructuredEvent(parsed, context, state) {
17716
18014
  return true;
17717
18015
  case "turn.started":
17718
18016
  disarmCodexExitGraceKill(state);
17719
- state.iterations += 1;
18017
+ state.codexAwaitingWorkersAfterTurnEnd = false;
18018
+ state.resultDeferredOnBackgroundWork = false;
18019
+ if (state.codexIterationEventFamily !== "legacy") {
18020
+ state.codexIterationEventFamily = "modern";
18021
+ state.iterations += 1;
18022
+ }
17720
18023
  return true;
17721
18024
  case "session_configured":
17722
18025
  if (typeof parsed.session_id === "string") {
@@ -17725,7 +18028,12 @@ function handleCodexStructuredEvent(parsed, context, state) {
17725
18028
  return true;
17726
18029
  case "task_started":
17727
18030
  disarmCodexExitGraceKill(state);
17728
- state.iterations += 1;
18031
+ state.codexAwaitingWorkersAfterTurnEnd = false;
18032
+ state.resultDeferredOnBackgroundWork = false;
18033
+ if (state.codexIterationEventFamily !== "modern") {
18034
+ state.codexIterationEventFamily = "legacy";
18035
+ state.iterations += 1;
18036
+ }
17729
18037
  return true;
17730
18038
  case "item.started": {
17731
18039
  const item = typeof parsed.item === "object" && parsed.item !== null ? parsed.item : null;
@@ -17752,6 +18060,8 @@ function handleCodexStructuredEvent(parsed, context, state) {
17752
18060
  }
17753
18061
  return true;
17754
18062
  }
18063
+ case "collab_tool_call":
18064
+ return handleCodexCollabToolCallItem(context, state, item, "started");
17755
18065
  // Text-bearing and patch/todo items carry no useful live-start payload; they
17756
18066
  // are surfaced on item.completed. Recognized (do not count as unhandled).
17757
18067
  case "reasoning":
@@ -17763,6 +18073,21 @@ function handleCodexStructuredEvent(parsed, context, state) {
17763
18073
  return false;
17764
18074
  }
17765
18075
  }
18076
+ case "item.updated": {
18077
+ const item = typeof parsed.item === "object" && parsed.item !== null ? parsed.item : null;
18078
+ if (!item || typeof item.type !== "string") return true;
18079
+ switch (item.type) {
18080
+ case "collab_tool_call":
18081
+ return handleCodexCollabToolCallItem(context, state, item, "updated");
18082
+ case "todo_list": {
18083
+ const todos = Array.isArray(item.items) ? item.items : [];
18084
+ void presenter.onTodoWrite?.(todos);
18085
+ return true;
18086
+ }
18087
+ default:
18088
+ return false;
18089
+ }
18090
+ }
17766
18091
  case "item.completed": {
17767
18092
  const item = typeof parsed.item === "object" && parsed.item !== null ? parsed.item : null;
17768
18093
  if (!item || typeof item.type !== "string") return true;
@@ -17827,6 +18152,8 @@ function handleCodexStructuredEvent(parsed, context, state) {
17827
18152
  void presenter.onTodoWrite?.(todos);
17828
18153
  return true;
17829
18154
  }
18155
+ case "collab_tool_call":
18156
+ return handleCodexCollabToolCallItem(context, state, item, "completed");
17830
18157
  default:
17831
18158
  return false;
17832
18159
  }
@@ -17848,7 +18175,9 @@ function handleCodexStructuredEvent(parsed, context, state) {
17848
18175
  return true;
17849
18176
  }
17850
18177
  case "exec_command_begin": {
17851
- const toolId = typeof parsed.call_id === "string" ? parsed.call_id : `exec-${Date.now()}`;
18178
+ const hasCallId = typeof parsed.call_id === "string" && parsed.call_id.length > 0;
18179
+ const toolId = hasCallId ? parsed.call_id : `exec-${Date.now()}`;
18180
+ if (!hasCallId) state.lastAnonymousExecId = toolId;
17852
18181
  const command = Array.isArray(parsed.command) ? parsed.command.join(" ") : "";
17853
18182
  const cwd = typeof parsed.cwd === "string" ? parsed.cwd : context.cwd;
17854
18183
  trackCodexStreamedToolId(state, toolId);
@@ -17856,7 +18185,10 @@ function handleCodexStructuredEvent(parsed, context, state) {
17856
18185
  return true;
17857
18186
  }
17858
18187
  case "exec_command_end": {
17859
- const toolId = typeof parsed.call_id === "string" ? parsed.call_id : `exec-${Date.now()}`;
18188
+ const hasCallId = typeof parsed.call_id === "string" && parsed.call_id.length > 0;
18189
+ const toolId = hasCallId ? parsed.call_id : state.lastAnonymousExecId;
18190
+ if (!hasCallId) state.lastAnonymousExecId = void 0;
18191
+ if (!toolId) return true;
17860
18192
  const output = typeof parsed.formatted_output === "string" ? parsed.formatted_output : typeof parsed.aggregated_output === "string" ? parsed.aggregated_output : "";
17861
18193
  void presenter.onToolResult?.(toolId, output);
17862
18194
  return true;
@@ -17864,9 +18196,8 @@ function handleCodexStructuredEvent(parsed, context, state) {
17864
18196
  case "mcp_tool_call_begin": {
17865
18197
  const toolId = typeof parsed.call_id === "string" ? parsed.call_id : `mcp-${Date.now()}`;
17866
18198
  const invocation = typeof parsed.invocation === "object" && parsed.invocation !== null ? parsed.invocation : {};
17867
- const tool = typeof invocation.tool_name === "string" ? invocation.tool_name : typeof invocation.tool === "string" ? invocation.tool : "MCP Tool";
17868
18199
  trackCodexStreamedToolId(state, toolId);
17869
- void presenter.onToolUse(tool, invocation, toolId);
18200
+ void presenter.onToolUse(buildCodexMcpToolName(invocation), invocation, toolId);
17870
18201
  return true;
17871
18202
  }
17872
18203
  case "mcp_tool_call_end": {
@@ -17904,7 +18235,7 @@ function handleCodexStructuredEvent(parsed, context, state) {
17904
18235
  const lastMessage = typeof parsed.last_agent_message === "string" ? parsed.last_agent_message : "";
17905
18236
  if (lastMessage.length > 0) state.summary = lastMessage;
17906
18237
  state.error = void 0;
17907
- armCodexExitGraceKill(state);
18238
+ completeCodexTurnRespectingWorkers(state);
17908
18239
  return true;
17909
18240
  }
17910
18241
  case "turn.completed": {
@@ -17927,7 +18258,108 @@ function handleCodexStructuredEvent(parsed, context, state) {
17927
18258
  cacheCreationTokens: state.usage.cacheCreationTokens
17928
18259
  });
17929
18260
  state.error = void 0;
17930
- armCodexExitGraceKill(state);
18261
+ completeCodexTurnRespectingWorkers(state);
18262
+ return true;
18263
+ }
18264
+ case "sub_agent_activity": {
18265
+ const kind = typeof parsed.kind === "string" ? parsed.kind : "";
18266
+ const agentThreadId = typeof parsed.agent_thread_id === "string" ? parsed.agent_thread_id : "";
18267
+ const agentPath = typeof parsed.agent_path === "string" ? parsed.agent_path : "";
18268
+ const eventId = typeof parsed.event_id === "string" ? parsed.event_id : "";
18269
+ const isRootPath = !agentPath || agentPath === "/root" || agentPath === "/";
18270
+ switch (kind) {
18271
+ case "started": {
18272
+ if (isRootPath) return true;
18273
+ if (agentThreadId && state.backgroundToolIdByTaskId?.has(agentThreadId)) return true;
18274
+ const toolId = eventId || agentThreadId;
18275
+ if (!toolId) return true;
18276
+ if (trackCodexStreamedToolId(state, toolId)) {
18277
+ const workerName = agentPath.slice(agentPath.lastIndexOf("/") + 1) || "codex-worker";
18278
+ const parentPath = agentPath.slice(0, agentPath.lastIndexOf("/"));
18279
+ const parentToolUseId = parentPath && parentPath !== "/root" ? state.codexWorkerToolIdByPath?.get(parentPath) ?? null : null;
18280
+ void presenter.onToolUse(
18281
+ "Agent",
18282
+ {
18283
+ subagent_type: workerName,
18284
+ description: agentPath,
18285
+ ...agentThreadId ? { agent_thread_id: agentThreadId } : {}
18286
+ },
18287
+ toolId,
18288
+ parentToolUseId
18289
+ );
18290
+ registerBackgroundLauncher(state, toolId);
18291
+ if (agentThreadId) {
18292
+ registerBackgroundTaskRecord(state, { task_id: agentThreadId, tool_use_id: toolId });
18293
+ }
18294
+ if (!state.codexWorkerToolIdByPath) state.codexWorkerToolIdByPath = /* @__PURE__ */ new Map();
18295
+ state.codexWorkerToolIdByPath.set(agentPath, toolId);
18296
+ }
18297
+ return true;
18298
+ }
18299
+ case "interacted":
18300
+ return true;
18301
+ case "interrupted": {
18302
+ if (isRootPath) return true;
18303
+ finishCodexWorker(context, state, {
18304
+ threadId: agentThreadId || void 0,
18305
+ agentPath: agentPath || void 0,
18306
+ status: "interrupted",
18307
+ message: "Background worker interrupted."
18308
+ });
18309
+ return true;
18310
+ }
18311
+ default:
18312
+ return false;
18313
+ }
18314
+ }
18315
+ // App-server EventMsg collab lifecycle notices. Begin/interaction/resume events
18316
+ // carry no state Alan tracks beyond liveness (touched above); spawn_end and the
18317
+ // waiting/close terminals map into the shared worker registry below.
18318
+ case "collab_agent_spawn_begin":
18319
+ case "collab_agent_interaction_begin":
18320
+ case "collab_agent_interaction_end":
18321
+ case "collab_waiting_begin":
18322
+ case "collab_resume_begin":
18323
+ case "collab_resume_end":
18324
+ case "collab_close_begin":
18325
+ return true;
18326
+ case "collab_agent_spawn_end": {
18327
+ const newThreadId = typeof parsed.new_thread_id === "string" ? parsed.new_thread_id : "";
18328
+ if (!newThreadId || state.backgroundToolIdByTaskId?.has(newThreadId)) return true;
18329
+ const callId = typeof parsed.call_id === "string" && parsed.call_id ? parsed.call_id : typeof parsed.event_id === "string" ? parsed.event_id : "";
18330
+ const toolId = callId || `collab-spawn-${newThreadId}`;
18331
+ if (trackCodexStreamedToolId(state, toolId)) {
18332
+ const nickname = typeof parsed.new_agent_nickname === "string" ? parsed.new_agent_nickname : "";
18333
+ const role = typeof parsed.new_agent_role === "string" ? parsed.new_agent_role : "";
18334
+ void presenter.onToolUse(
18335
+ "Agent",
18336
+ {
18337
+ subagent_type: nickname || role || "codex-worker",
18338
+ description: [nickname, role].filter(Boolean).join(" \u2014 ") || "Codex background worker",
18339
+ agent_thread_id: newThreadId
18340
+ },
18341
+ toolId
18342
+ );
18343
+ registerBackgroundLauncher(state, toolId);
18344
+ }
18345
+ registerBackgroundTaskRecord(state, { task_id: newThreadId, tool_use_id: toolId });
18346
+ return true;
18347
+ }
18348
+ case "collab_waiting_end": {
18349
+ const entries = Array.isArray(parsed.agent_statuses) ? parsed.agent_statuses : [];
18350
+ for (const raw of entries) {
18351
+ if (!raw || typeof raw !== "object") continue;
18352
+ const entry = raw;
18353
+ const threadId = typeof entry.agent_thread_id === "string" ? entry.agent_thread_id : typeof entry.thread_id === "string" ? entry.thread_id : "";
18354
+ const status = typeof entry.status === "string" ? entry.status.toLowerCase() : "";
18355
+ if (!threadId || !TERMINAL_COLLAB_AGENT_STATUSES.has(status)) continue;
18356
+ finishCodexWorker(context, state, { threadId, status });
18357
+ }
18358
+ return true;
18359
+ }
18360
+ case "collab_close_end": {
18361
+ const threadId = typeof parsed.receiver_thread_id === "string" ? parsed.receiver_thread_id : "";
18362
+ if (threadId) finishCodexWorker(context, state, { threadId, status: "shutdown" });
17931
18363
  return true;
17932
18364
  }
17933
18365
  case "error": {
@@ -18046,10 +18478,11 @@ function createCodexRuntimeBackend(command = "codex", defaultArgs = []) {
18046
18478
  },
18047
18479
  promptViaStdin: true,
18048
18480
  augmentPrompt: (ctx) => {
18481
+ const isResume = Boolean(resumableSessionId);
18049
18482
  const promptWithContext = resumeContextPrefix ? `${resumeContextPrefix}
18050
18483
 
18051
18484
  ${ctx.promptText}` : ctx.promptText;
18052
- const basePrompt = buildPromptWithSystem(ctx.config, promptWithContext);
18485
+ const basePrompt = buildResumeAwarePrompt(ctx.config, promptWithContext, { isResume });
18053
18486
  return ctx.config.mode === "plan" ? buildPlanModePrefix(basePrompt) : basePrompt;
18054
18487
  },
18055
18488
  parseStructuredLine: parseCodexStructuredLine,
@@ -18255,10 +18688,21 @@ function handleCursorStructuredEvent(parsed, context, state) {
18255
18688
  }
18256
18689
  case "tool_call": {
18257
18690
  const subtype = typeof parsed.subtype === "string" ? parsed.subtype : "";
18258
- const toolId = typeof parsed.call_id === "string" ? parsed.call_id : `cursor-tool-${Date.now()}`;
18691
+ const hasCallId = typeof parsed.call_id === "string" && parsed.call_id.length > 0;
18259
18692
  const toolCall = typeof parsed.tool_call === "object" && parsed.tool_call !== null ? parsed.tool_call : null;
18260
18693
  const entry = toolCall ? extractCursorToolEntry(toolCall) : null;
18261
18694
  if (!entry) return true;
18695
+ let toolId;
18696
+ if (hasCallId) {
18697
+ toolId = parsed.call_id;
18698
+ } else if (subtype === "started") {
18699
+ state.cursorAnonymousToolCounter = (state.cursorAnonymousToolCounter ?? 0) + 1;
18700
+ toolId = `cursor-tool-${state.cursorAnonymousToolCounter}`;
18701
+ state.lastAnonymousCursorToolId = toolId;
18702
+ } else {
18703
+ toolId = state.lastAnonymousCursorToolId;
18704
+ }
18705
+ if (!toolId) return true;
18262
18706
  state.iterations = Math.max(state.iterations, 1);
18263
18707
  const toolName = formatCursorToolName(entry.rawName, entry.payload);
18264
18708
  const toolArgs = typeof entry.payload.args === "object" && entry.payload.args !== null ? entry.payload.args : typeof entry.payload.arguments === "object" && entry.payload.arguments !== null ? entry.payload.arguments : entry.payload;
@@ -18267,6 +18711,7 @@ function handleCursorStructuredEvent(parsed, context, state) {
18267
18711
  return true;
18268
18712
  }
18269
18713
  if (subtype === "completed") {
18714
+ if (!hasCallId) state.lastAnonymousCursorToolId = void 0;
18270
18715
  void presenter.onToolResult?.(toolId, buildCursorToolResultText(entry.payload.result));
18271
18716
  }
18272
18717
  return true;
@@ -18345,7 +18790,10 @@ function buildCursorAgentModelArg(modelId, options) {
18345
18790
  const wireEffort = resolveWireEffort({
18346
18791
  harness: "cursor_agent_cli",
18347
18792
  modelId: baseId,
18348
- selectedEffortLevel: options?.selectedEffortLevel,
18793
+ // Fall back to an effort baked into the model id (e.g. `gpt-5.5-high` from a
18794
+ // session import or raw discovered id) when no explicit selection overrides it,
18795
+ // so the SKU is preserved instead of silently downgraded to default effort.
18796
+ selectedEffortLevel: options?.selectedEffortLevel ?? parsed.effort,
18349
18797
  effortLevels
18350
18798
  });
18351
18799
  let id = baseId;
@@ -18359,7 +18807,8 @@ function buildCursorAgentModelArg(modelId, options) {
18359
18807
  }
18360
18808
  }
18361
18809
  const contextWindow = asContextWindow(options?.selectedContextWindow);
18362
- if (contextWindow === "1m" && id === baseId) {
18810
+ const modelSupports1m = (options?.contextWindows ?? []).includes("1m");
18811
+ if (contextWindow === "1m" && id === baseId && modelSupports1m) {
18363
18812
  return `${baseId}[context=1m]`;
18364
18813
  }
18365
18814
  if (parsed.fast) return `${id}-fast`;
@@ -18449,7 +18898,8 @@ function createCursorAgentCliBackend(command = "cursor-agent", defaultArgs = [])
18449
18898
  buildCursorAgentModelArg(model, {
18450
18899
  selectedEffortLevel: ctx.config.selectedEffortLevel,
18451
18900
  selectedContextWindow: ctx.config.selectedContextWindow,
18452
- effortLevels: modelDef?.capabilities?.effortLevels
18901
+ effortLevels: modelDef?.capabilities?.effortLevels,
18902
+ contextWindows: modelDef?.capabilities?.contextWindows
18453
18903
  })
18454
18904
  );
18455
18905
  }
@@ -18467,11 +18917,20 @@ function createCursorAgentCliBackend(command = "cursor-agent", defaultArgs = [])
18467
18917
  // with zero output; without this watchdog only the 30-min server reaper
18468
18918
  // would end the run.
18469
18919
  startupTimeoutMs: 18e4,
18920
+ // Post-startup hang bound: cursor's asks use the non-blocking MCP model and it
18921
+ // never populates the idle timer's ask/background fire conditions, so a cursor
18922
+ // process that produced output then went silent (no result event) is otherwise
18923
+ // bounded only by the ~30-min external reaper. Poll every 5 min; kill after 15
18924
+ // min of continuous silence.
18925
+ resultIdleTimeoutMs: 5 * 6e4,
18926
+ postStartupSilenceTimeoutMs: 15 * 6e4,
18470
18927
  augmentPrompt: (ctx) => {
18471
- const prompt = buildPromptWithImagePathReferences(ctx, imageFiles);
18472
- return ctx.config.teamId?.trim() ? `${prompt}
18473
-
18474
- ${buildCodeDiscoveryEnforcementTail()}` : prompt;
18928
+ const isResume = Boolean(
18929
+ ctx.config.runtimeSessionId?.trim() || ctx.config.providerSessionId?.trim()
18930
+ );
18931
+ const base = buildResumeAwarePrompt(ctx.config, ctx.promptText, { isResume });
18932
+ const prompt = ctx.config.mode === "plan" ? buildPlanModePrefix(base) : base;
18933
+ return appendImagePathReferences(prompt, imageFiles);
18475
18934
  },
18476
18935
  parseStructuredLine: parseCursorStructuredLine
18477
18936
  }).run(context);
@@ -19231,17 +19690,7 @@ function handleOpencodeStructuredEvent(parsed, context, state) {
19231
19690
  void presenter.onError(message);
19232
19691
  state.error = message;
19233
19692
  }
19234
- const pid = state.process?.pid;
19235
- if (pid) {
19236
- try {
19237
- process.kill(-pid, "SIGTERM");
19238
- } catch {
19239
- try {
19240
- state.process?.kill("SIGTERM");
19241
- } catch {
19242
- }
19243
- }
19244
- }
19693
+ killProcessTree(state.process?.pid, { signal: "SIGTERM", child: state.process });
19245
19694
  return true;
19246
19695
  }
19247
19696
  default:
@@ -19435,7 +19884,7 @@ function createSupatestCliBackend(command = "supatest", defaultArgs = []) {
19435
19884
  const basePromptText = resumeContextPrefix ? `${resumeContextPrefix}
19436
19885
 
19437
19886
  ${ctx.promptText}` : ctx.promptText;
19438
- const promptText = ctx.config.mode === "plan" ? buildPlanModePrefix(buildPromptWithSystem(ctx.config, basePromptText)) : buildPromptWithSystem(ctx.config, basePromptText);
19887
+ const promptText = ctx.config.mode === "plan" ? buildPlanModePrefix(basePromptText) : basePromptText;
19439
19888
  if (promptText.trim()) {
19440
19889
  contentBlocks.push({ type: "text", text: promptText });
19441
19890
  }
@@ -19620,6 +20069,8 @@ var BaseMachineAgent = class _BaseMachineAgent {
19620
20069
  presenter;
19621
20070
  runtime;
19622
20071
  abortController = null;
20072
+ /** Log the "synced MCP session headers" line once per run(), not per respawn. */
20073
+ mcpHeadersLogged = false;
19623
20074
  constructor(presenter, runtime) {
19624
20075
  this.presenter = presenter;
19625
20076
  this.runtime = runtime;
@@ -19676,7 +20127,52 @@ var BaseMachineAgent = class _BaseMachineAgent {
19676
20127
  getActivityHeartbeatIntervalMs() {
19677
20128
  return AGENT_ACTIVITY_HEARTBEAT_MS;
19678
20129
  }
20130
+ /**
20131
+ * Home directory whose CLI config files receive the Alan MCP session headers.
20132
+ * Defaults to the process home (production behaviour); tests override to a
20133
+ * temp dir so runs never touch the developer's real CLI configs.
20134
+ */
20135
+ getMcpConfigHomeDir() {
20136
+ return void 0;
20137
+ }
20138
+ /**
20139
+ * Re-assert this run's Alan MCP identity in the shared CLI config under the
20140
+ * serialization gate, holding the gate until the provider spawns. Returns the
20141
+ * gate release. Concurrent runs on one machine share ~/.codex/config.toml,
20142
+ * ~/.cursor/mcp.json, and ~/.claude.json; the CLI reads its config once at
20143
+ * spawn, so without serialization a concurrent run could rewrite the shared
20144
+ * file between this write and this spawn and steal this run's session identity.
20145
+ */
20146
+ async beginMcpConfigCriticalSection(config) {
20147
+ const conversationId = config.conversationId;
20148
+ if (!conversationId) return () => {
20149
+ };
20150
+ const releaseGate = await acquireMcpConfigWriteGate();
20151
+ try {
20152
+ const writtenClis = syncAlanMcpSessionHeaders({
20153
+ conversationId,
20154
+ teamId: config.teamId,
20155
+ runHeaders: config.alanMcp?.headers,
20156
+ homeDir: this.getMcpConfigHomeDir()
20157
+ });
20158
+ if (writtenClis.length > 0 && !this.mcpHeadersLogged) {
20159
+ this.mcpHeadersLogged = true;
20160
+ void this.presenter.onLog(`Synced Alan MCP session headers to ${writtenClis.join(", ")}`);
20161
+ }
20162
+ } catch (error) {
20163
+ console.warn("[base-machine-agent] Failed to sync MCP session headers", error);
20164
+ }
20165
+ return releaseGate;
20166
+ }
19679
20167
  async runBackendWithActivityHeartbeat(backend, context) {
20168
+ const releaseMcpGate = await this.beginMcpConfigCriticalSection(context.config);
20169
+ let mcpGateReleased = false;
20170
+ const releaseMcpGateOnce = () => {
20171
+ if (mcpGateReleased) return;
20172
+ mcpGateReleased = true;
20173
+ releaseMcpGate();
20174
+ };
20175
+ const callerOnProcessSpawned = context.onProcessSpawned;
19680
20176
  const intervalMs = this.getActivityHeartbeatIntervalMs();
19681
20177
  let heartbeat = null;
19682
20178
  let livenessProbe = null;
@@ -19695,9 +20191,17 @@ var BaseMachineAgent = class _BaseMachineAgent {
19695
20191
  }
19696
20192
  }, intervalMs);
19697
20193
  }
20194
+ const spawnAwareContext = {
20195
+ ...context,
20196
+ onProcessSpawned: (child) => {
20197
+ releaseMcpGateOnce();
20198
+ callerOnProcessSpawned?.(child);
20199
+ }
20200
+ };
19698
20201
  try {
19699
- return await backend.run(context);
20202
+ return await backend.run(spawnAwareContext);
19700
20203
  } finally {
20204
+ releaseMcpGateOnce();
19701
20205
  if (heartbeat) clearInterval(heartbeat);
19702
20206
  }
19703
20207
  }
@@ -19791,6 +20295,8 @@ Prefer relative paths and keep your work scoped to this project.`
19791
20295
  runtimeCommand || "generic-cli",
19792
20296
  runtimeArgs
19793
20297
  );
20298
+ const revalidateRuntimeCommand = this.revalidateRuntimeCommand;
20299
+ const revalidateCommand = revalidateRuntimeCommand ? () => revalidateRuntimeCommand.call(this) : void 0;
19794
20300
  let lastResult = null;
19795
20301
  for (let attempt = 0; attempt <= _BaseMachineAgent.MAX_RETRIES; attempt++) {
19796
20302
  if (this.abortController?.signal.aborted) break;
@@ -19801,7 +20307,8 @@ Prefer relative paths and keep your work scoped to this project.`
19801
20307
  cwd,
19802
20308
  env,
19803
20309
  promptText,
19804
- onProcessSpawned: this.getOnProcessSpawned()
20310
+ onProcessSpawned: this.getOnProcessSpawned(),
20311
+ revalidateCommand
19805
20312
  });
19806
20313
  lastResult = {
19807
20314
  ...result,
@@ -19851,7 +20358,8 @@ ${runtimeConfig.task}` } : {}
19851
20358
  cwd,
19852
20359
  env,
19853
20360
  promptText: fallbackContext ? this.buildPromptText(runtimeConfig) : promptText,
19854
- onProcessSpawned: this.getOnProcessSpawned()
20361
+ onProcessSpawned: this.getOnProcessSpawned(),
20362
+ revalidateCommand
19855
20363
  });
19856
20364
  lastResult = {
19857
20365
  ...retryResult,
@@ -19927,19 +20435,7 @@ ${runtimeConfig.task}` } : {}
19927
20435
  this.presenter.onComplete(result);
19928
20436
  return result;
19929
20437
  }
19930
- if (initialConfig.conversationId) {
19931
- try {
19932
- const writtenClis = syncAlanMcpSessionHeaders({
19933
- conversationId: initialConfig.conversationId,
19934
- teamId: initialConfig.teamId
19935
- });
19936
- if (writtenClis.length > 0) {
19937
- void this.presenter.onLog(`Synced Alan MCP session headers to ${writtenClis.join(", ")}`);
19938
- }
19939
- } catch (error) {
19940
- console.warn("[base-machine-agent] Failed to sync MCP session headers", error);
19941
- }
19942
- }
20438
+ this.mcpHeadersLogged = false;
19943
20439
  try {
19944
20440
  return await this.runCliBackend(initialConfig, safeProjectPath);
19945
20441
  } catch (error) {
@@ -20271,38 +20767,32 @@ var CoreAgent = class _CoreAgent extends BaseMachineAgent {
20271
20767
  const pid = child.pid;
20272
20768
  if (!pid) return false;
20273
20769
  console.info("[CoreAgent] Killing agent process", { pid });
20274
- try {
20275
- if (process.platform === "win32") {
20276
- child.kill("SIGTERM");
20277
- } else {
20278
- process.kill(-pid, "SIGTERM");
20279
- }
20280
- } catch (err) {
20281
- if (err.code !== "ESRCH") {
20282
- console.warn("[CoreAgent] SIGTERM failed", { pid, err });
20283
- }
20284
- try {
20285
- child.kill("SIGTERM");
20286
- } catch {
20287
- }
20288
- }
20770
+ killProcessTree(pid, {
20771
+ signal: "SIGTERM",
20772
+ child,
20773
+ onError: (stage, err) => console.warn(`[CoreAgent] SIGTERM failed (${stage})`, { pid, err })
20774
+ });
20289
20775
  setTimeout(() => {
20290
20776
  if (child.killed || child.exitCode !== null) return;
20291
20777
  console.warn("[CoreAgent] Escalating to SIGKILL", { pid });
20292
- try {
20293
- if (process.platform === "win32") {
20294
- child.kill("SIGKILL");
20295
- } else {
20296
- process.kill(-pid, "SIGKILL");
20297
- }
20298
- } catch (err) {
20299
- if (err.code !== "ESRCH") {
20300
- console.warn("[CoreAgent] SIGKILL failed", { pid, err });
20301
- }
20302
- }
20778
+ killProcessTree(pid, {
20779
+ signal: "SIGKILL",
20780
+ child,
20781
+ onError: (stage, err) => console.warn(`[CoreAgent] SIGKILL failed (${stage})`, { pid, err })
20782
+ });
20303
20783
  }, _CoreAgent.SIGKILL_DELAY_MS);
20304
20784
  return true;
20305
20785
  }
20786
+ /**
20787
+ * Re-resolve this agent's CLI executable from scratch when a spawn fails
20788
+ * ENOENT. The daemon caches resolved CLI paths, so a provider binary that was
20789
+ * moved or reinstalled mid-session would otherwise fail every turn with
20790
+ * "CLI command not found" until the daemon restarts. Invalidating the cache
20791
+ * and re-resolving once lets the current run recover against the new location.
20792
+ */
20793
+ revalidateRuntimeCommand() {
20794
+ return revalidateBackendRuntimeCommand(this.runtime.backendKind) ?? null;
20795
+ }
20306
20796
  // ── Interactive tool response (WS relay → stdin) ───────────────────────
20307
20797
  /**
20308
20798
  * Send a tool_result to the CLI's stdin (stream-json format).
@@ -20633,7 +21123,7 @@ function encryptedFileSize(entries) {
20633
21123
  );
20634
21124
  return emptyEnvelopeBytes + base64UrlLength(plaintextBytes);
20635
21125
  }
20636
- var EncryptedEventOutbox = class {
21126
+ var EncryptedEventOutbox = class _EncryptedEventOutbox {
20637
21127
  constructor(path, encodedKey, pushLog = () => {
20638
21128
  }, options = {}) {
20639
21129
  this.path = path;
@@ -20648,6 +21138,11 @@ var EncryptedEventOutbox = class {
20648
21138
  throw new Error("event outbox maxIntermediateEventsPerRun must be a non-negative integer");
20649
21139
  }
20650
21140
  this.entries = this.read();
21141
+ if (!(0, import_node_fs6.existsSync)(this.path)) {
21142
+ this.lastPersistedSignature = _EncryptedEventOutbox.EMPTY_SIGNATURE;
21143
+ } else if (this.entries.length > 0) {
21144
+ this.lastPersistedSignature = this.serializeForPersist().signature;
21145
+ }
20651
21146
  this.enforceCaps();
20652
21147
  this.persist();
20653
21148
  }
@@ -20655,6 +21150,19 @@ var EncryptedEventOutbox = class {
20655
21150
  inFlightEventId = null;
20656
21151
  ackTimer = null;
20657
21152
  retryTimer = null;
21153
+ /** Latch so a persistent disk failure logs once per streak, not per event. */
21154
+ persistFailureLogged = false;
21155
+ /**
21156
+ * Signature of the entries last durably written, so a `persist()` whose entries
21157
+ * are byte-identical to what is already on disk skips the AES-GCM re-encrypt +
21158
+ * temp-file write + rename entirely. Under link flapping the spool is persisted
21159
+ * on every enqueue/ack; this collapses redundant writes (e.g. a reconnect that
21160
+ * replays from head without mutating the queue) to O(delta), not O(spool). Set
21161
+ * to null on any write failure so the next persist retries rather than skips.
21162
+ */
21163
+ lastPersistedSignature = null;
21164
+ /** Sentinel signature for the empty (no-entries) on-disk state. */
21165
+ static EMPTY_SIGNATURE = "empty";
20658
21166
  key;
20659
21167
  maxEncryptedBytes;
20660
21168
  maxIntermediateEventsPerRun;
@@ -20857,31 +21365,53 @@ var EncryptedEventOutbox = class {
20857
21365
  throw new Error(`unable to decrypt durable event outbox: ${detail}`);
20858
21366
  }
20859
21367
  }
20860
- persist() {
21368
+ /** Serialize the current entries to their plaintext + content signature (once). */
21369
+ serializeForPersist() {
20861
21370
  if (this.entries.length === 0) {
20862
- (0, import_node_fs6.rmSync)(this.path, { force: true });
20863
- return;
21371
+ return { plaintext: "", signature: _EncryptedEventOutbox.EMPTY_SIGNATURE };
20864
21372
  }
20865
- (0, import_node_fs6.mkdirSync)((0, import_node_path5.dirname)(this.path), { recursive: true, mode: 448 });
20866
- const iv = (0, import_node_crypto.randomBytes)(12);
20867
- const cipher = (0, import_node_crypto.createCipheriv)("aes-256-gcm", this.key, iv);
20868
- const ciphertext = Buffer.concat([
20869
- cipher.update(JSON.stringify({ entries: this.entries }), "utf8"),
20870
- cipher.final()
20871
- ]);
20872
- const envelope = {
20873
- version: OUTBOX_VERSION,
20874
- iv: iv.toString("base64url"),
20875
- authTag: cipher.getAuthTag().toString("base64url"),
20876
- ciphertext: ciphertext.toString("base64url")
20877
- };
20878
- const pendingPath = `${this.path}.pending-${process.pid}-${(0, import_node_crypto.randomBytes)(6).toString("hex")}`;
21373
+ const plaintext = JSON.stringify({ entries: this.entries });
21374
+ return { plaintext, signature: (0, import_node_crypto.createHash)("sha256").update(plaintext).digest("base64url") };
21375
+ }
21376
+ persist() {
21377
+ const { plaintext, signature } = this.serializeForPersist();
21378
+ if (signature === this.lastPersistedSignature) return;
20879
21379
  try {
20880
- (0, import_node_fs6.writeFileSync)(pendingPath, JSON.stringify(envelope), { mode: 384 });
20881
- (0, import_node_fs6.chmodSync)(pendingPath, 384);
20882
- (0, import_node_fs6.renameSync)(pendingPath, this.path);
20883
- } finally {
20884
- (0, import_node_fs6.rmSync)(pendingPath, { force: true });
21380
+ if (this.entries.length === 0) {
21381
+ (0, import_node_fs6.rmSync)(this.path, { force: true });
21382
+ this.lastPersistedSignature = signature;
21383
+ this.persistFailureLogged = false;
21384
+ return;
21385
+ }
21386
+ (0, import_node_fs6.mkdirSync)((0, import_node_path5.dirname)(this.path), { recursive: true, mode: 448 });
21387
+ const iv = (0, import_node_crypto.randomBytes)(12);
21388
+ const cipher = (0, import_node_crypto.createCipheriv)("aes-256-gcm", this.key, iv);
21389
+ const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
21390
+ const envelope = {
21391
+ version: OUTBOX_VERSION,
21392
+ iv: iv.toString("base64url"),
21393
+ authTag: cipher.getAuthTag().toString("base64url"),
21394
+ ciphertext: ciphertext.toString("base64url")
21395
+ };
21396
+ const pendingPath = `${this.path}.pending-${process.pid}-${(0, import_node_crypto.randomBytes)(6).toString("hex")}`;
21397
+ try {
21398
+ (0, import_node_fs6.writeFileSync)(pendingPath, JSON.stringify(envelope), { mode: 384 });
21399
+ (0, import_node_fs6.chmodSync)(pendingPath, 384);
21400
+ (0, import_node_fs6.renameSync)(pendingPath, this.path);
21401
+ } finally {
21402
+ (0, import_node_fs6.rmSync)(pendingPath, { force: true });
21403
+ }
21404
+ this.lastPersistedSignature = signature;
21405
+ this.persistFailureLogged = false;
21406
+ } catch (error) {
21407
+ this.lastPersistedSignature = null;
21408
+ if (!this.persistFailureLogged) {
21409
+ this.persistFailureLogged = true;
21410
+ const detail = error instanceof Error ? error.message : String(error);
21411
+ this.pushLog(
21412
+ `event_outbox_persist_failed error=${detail} pending_entries=${this.entries.length}`
21413
+ );
21414
+ }
20885
21415
  }
20886
21416
  }
20887
21417
  };
@@ -21587,8 +22117,8 @@ var CONFIG_LOCK_STALE_MS = 3e4;
21587
22117
  var CONFIG_LOCK_WAIT_MS = 2e3;
21588
22118
  var CONFIG_LOCK_POLL_MS = 20;
21589
22119
  function compactProviderVersion(output) {
21590
- const firstLine = output.split(/\r?\n/).map((line) => line.trim()).find(Boolean);
21591
- return firstLine?.slice(0, 120);
22120
+ const firstLine2 = output.split(/\r?\n/).map((line) => line.trim()).find(Boolean);
22121
+ return firstLine2?.slice(0, 120);
21592
22122
  }
21593
22123
  var inspectAlanMcpThroughProviderCli = (backendKind, expectedUrl, home) => {
21594
22124
  const plan = getMcpCliInspectionPlan(backendKind);
@@ -21628,7 +22158,30 @@ var inspectAlanMcpThroughProviderCli = (backendKind, expectedUrl, home) => {
21628
22158
  };
21629
22159
  }
21630
22160
  const providerVersion = compactProviderVersion(versionProbe.stdout);
21631
- const probe = runCliVersionProbe(executable, env, plan.args);
22161
+ const inspectTimeoutMs = Number(process.env.ALAN_MCP_INSPECT_TIMEOUT_MS) || 2e4;
22162
+ const probe = runCliVersionProbe(executable, env, plan.args, inspectTimeoutMs);
22163
+ const timedOut = probe.status === null && (probe.signal === "SIGTERM" || probe.error?.code === "ETIMEDOUT");
22164
+ if (timedOut) {
22165
+ const fileCheck = verifyAlanMcpRegistered(backendKind, home, () => ({
22166
+ ok: true,
22167
+ verification: { method: "file" }
22168
+ }));
22169
+ console.warn("[alan-agent] MCP CLI inspection timed out; falling back to file check", {
22170
+ backendKind,
22171
+ command,
22172
+ inspectTimeoutMs,
22173
+ fileOk: fileCheck.ok
22174
+ });
22175
+ return {
22176
+ ok: fileCheck.ok,
22177
+ issue: fileCheck.ok ? void 0 : {
22178
+ code: "provider_mcp_repair_required",
22179
+ command,
22180
+ message: `Alan tools could not be verified for ${plan.providerLabel}. Repair Alan tools, then retry.`
22181
+ },
22182
+ verification: { method: "file", command, providerVersion }
22183
+ };
22184
+ }
21632
22185
  const classification = classifyMcpCliInspection(
21633
22186
  plan,
21634
22187
  {
@@ -22527,7 +23080,7 @@ var RunStartGate = class {
22527
23080
  };
22528
23081
 
22529
23082
  // src/version.ts
22530
- var AGENT_VERSION = "0.1.41";
23083
+ var AGENT_VERSION = "0.1.43";
22531
23084
 
22532
23085
  // src/workspace-relocation.ts
22533
23086
  var import_node_child_process3 = require("child_process");
@@ -22603,11 +23156,19 @@ var import_node_child_process4 = require("child_process");
22603
23156
  var import_node_fs11 = require("fs");
22604
23157
  var import_node_path9 = require("path");
22605
23158
  var WORKSPACE_LEASE_CHECK_INTERVAL_MS = 1e3;
23159
+ function parsePositiveMs(value2, fallback) {
23160
+ const parsed = value2 ? Number(value2) : Number.NaN;
23161
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
23162
+ }
23163
+ var WORKSPACE_LEASE_GIT_TIMEOUT_MS = parsePositiveMs(
23164
+ process.env.ALAN_WORKSPACE_LEASE_GIT_TIMEOUT_MS,
23165
+ 2e3
23166
+ );
22606
23167
  function gitValue(cwd, args) {
22607
23168
  const result = (0, import_node_child_process4.spawnSync)("git", ["-C", cwd, ...args], {
22608
23169
  encoding: "utf8",
22609
23170
  env: getDaemonCliEnvironment(),
22610
- timeout: 2e3
23171
+ timeout: WORKSPACE_LEASE_GIT_TIMEOUT_MS
22611
23172
  });
22612
23173
  if (result.status !== 0) return null;
22613
23174
  return result.stdout.trim() || null;
@@ -22643,6 +23204,13 @@ var DEFAULT_PROBE2 = {
22643
23204
  } catch {
22644
23205
  return null;
22645
23206
  }
23207
+ },
23208
+ pathExists: (workspacePath) => {
23209
+ try {
23210
+ return (0, import_node_fs11.existsSync)((0, import_node_path9.resolve)(workspacePath));
23211
+ } catch {
23212
+ return true;
23213
+ }
22646
23214
  }
22647
23215
  };
22648
23216
  function unavailableFailure(target) {
@@ -22690,6 +23258,57 @@ var WorkspaceRunLease = class _WorkspaceRunLease {
22690
23258
  failure: null
22691
23259
  };
22692
23260
  }
23261
+ /**
23262
+ * Classify the lease state for the run monitor.
23263
+ *
23264
+ * - `transient: true` — a null snapshot while the workspace path still exists:
23265
+ * the git probe timed out or failed transiently (OS throttling / IO
23266
+ * contention), NOT a vanished checkout. The monitor should retry (require N
23267
+ * consecutive transient failures) before declaring the workspace lost.
23268
+ * - `transient: false` — a definitive change: the path is genuinely missing
23269
+ * (ENOENT), or the workspace/repository/worktree/branch identity changed.
23270
+ * These are real and fail fast.
23271
+ */
23272
+ verifyDetailed() {
23273
+ for (const captured of this.captured) {
23274
+ const current = this.probe.snapshot(captured.target.workspacePath);
23275
+ if (!current) {
23276
+ const pathGone = this.probe.pathExists?.(captured.target.workspacePath) === false;
23277
+ return { failure: unavailableFailure(captured.target), transient: !pathGone };
23278
+ }
23279
+ if (current.workspaceIdentity !== captured.identity.workspaceIdentity) {
23280
+ return {
23281
+ failure: {
23282
+ code: "workspace_lease_path_changed",
23283
+ message: "The selected workspace folder changed while the run was active. The run stopped before continuing; choose the correct folder and start a new run.",
23284
+ workspacePath: captured.target.workspacePath,
23285
+ expectedBranch: captured.identity.branch,
23286
+ actualBranch: current.branch
23287
+ },
23288
+ transient: false
23289
+ };
23290
+ }
23291
+ if (current.repositoryIdentity !== captured.identity.repositoryIdentity || current.worktreeIdentity !== captured.identity.worktreeIdentity) {
23292
+ return {
23293
+ failure: {
23294
+ code: "workspace_lease_repository_changed",
23295
+ message: "The workspace now points to a different repository or worktree. The run stopped before continuing; restore the original checkout and start a new run.",
23296
+ workspacePath: captured.target.workspacePath,
23297
+ expectedBranch: captured.identity.branch,
23298
+ actualBranch: current.branch
23299
+ },
23300
+ transient: false
23301
+ };
23302
+ }
23303
+ if (current.branch !== captured.identity.branch) {
23304
+ return {
23305
+ failure: branchFailure(captured.target, captured.identity.branch, current.branch),
23306
+ transient: false
23307
+ };
23308
+ }
23309
+ }
23310
+ return { failure: null, transient: false };
23311
+ }
22693
23312
  verify() {
22694
23313
  for (const captured of this.captured) {
22695
23314
  const current = this.probe.snapshot(captured.target.workspacePath);
@@ -22719,6 +23338,40 @@ var WorkspaceRunLease = class _WorkspaceRunLease {
22719
23338
  return null;
22720
23339
  }
22721
23340
  };
23341
+ var FRESH_LEASE_MONITOR_STATE = {
23342
+ transientStreak: 0,
23343
+ nextCheckAtMs: 0
23344
+ };
23345
+ function isLeaseCheckDue(state, nowMs) {
23346
+ return nowMs >= state.nextCheckAtMs;
23347
+ }
23348
+ function applyLeaseCheckResult(input) {
23349
+ const { detailed, nowMs, policy } = input;
23350
+ if (!detailed.failure) {
23351
+ return { action: "ok", state: { ...FRESH_LEASE_MONITOR_STATE } };
23352
+ }
23353
+ if (!detailed.transient) {
23354
+ return { action: "fail", state: input.state, failure: detailed.failure };
23355
+ }
23356
+ const transientStreak = input.state.transientStreak + 1;
23357
+ if (transientStreak >= policy.maxTransientFailures) {
23358
+ return {
23359
+ action: "fail",
23360
+ state: { transientStreak, nextCheckAtMs: input.state.nextCheckAtMs },
23361
+ failure: detailed.failure
23362
+ };
23363
+ }
23364
+ const backoffMs = Math.min(
23365
+ policy.backoffBaseMs * 2 ** (transientStreak - 1),
23366
+ policy.backoffMaxMs
23367
+ );
23368
+ return {
23369
+ action: "transient",
23370
+ state: { transientStreak, nextCheckAtMs: nowMs + backoffMs },
23371
+ failure: detailed.failure,
23372
+ backoffMs
23373
+ };
23374
+ }
22722
23375
 
22723
23376
  // src/daemon.ts
22724
23377
  var STAGING_API_URL = process.env.ALAN_STAGING_API_URL ?? "https://staging-api.tryalan.ai";
@@ -24037,9 +24690,12 @@ function reconcileActiveRuns(input) {
24037
24690
  if (!staleByGrace && !staleByDeadPid) continue;
24038
24691
  if (awaitingAsk && staleByGrace && !staleByDeadPid) continue;
24039
24692
  if (staleByDeadPid && awaitingAsk) {
24693
+ entry.presenter.onComplete(buildAbortResult("provider_exited"));
24040
24694
  entry.agent.kill();
24041
24695
  input.activeAgents.delete(runId);
24042
- input.pushLog?.(`reconciled dead provider pid run=${runId} (awaiting ask, no interrupt)`);
24696
+ input.pushLog?.(
24697
+ `reconciled dead provider pid run=${runId} (awaiting ask \u2014 finalized for resume)`
24698
+ );
24043
24699
  continue;
24044
24700
  }
24045
24701
  abortActiveAgent(entry, { reason: staleByDeadPid ? "provider_exited" : "runner_stalled" });
@@ -24780,6 +25436,10 @@ async function startDaemon(args) {
24780
25436
  const versionRefresh = setInterval(() => {
24781
25437
  void refreshProviderVersions();
24782
25438
  }, VERSION_REFRESH_INTERVAL_MS);
25439
+ const parsedMaxTransient = Number(process.env.ALAN_WORKSPACE_LEASE_MAX_TRANSIENT_FAILURES);
25440
+ const WORKSPACE_LEASE_MAX_TRANSIENT_FAILURES = Number.isFinite(parsedMaxTransient) && parsedMaxTransient >= 1 ? Math.floor(parsedMaxTransient) : 3;
25441
+ const WORKSPACE_LEASE_FAILURE_BACKOFF_BASE_MS = 1e3;
25442
+ const WORKSPACE_LEASE_FAILURE_BACKOFF_MAX_MS = 15e3;
24783
25443
  const stopRunForWorkspaceLease = (runId, entry, failure) => {
24784
25444
  entry.stage = "failed";
24785
25445
  if (!entry.presenter.failWorkspaceLease(failure)) return;
@@ -24787,15 +25447,39 @@ async function startDaemon(args) {
24787
25447
  activeAgents.delete(runId);
24788
25448
  pushLog(`workspace-lease-lost run=${runId} code=${failure.code}`);
24789
25449
  };
24790
- const verifyActiveWorkspaceLease = (runId, entry) => {
24791
- const failure = entry.workspaceLease?.verify() ?? null;
24792
- if (!failure) return true;
24793
- stopRunForWorkspaceLease(runId, entry, failure);
24794
- return false;
25450
+ const leaseCheckPolicy = {
25451
+ maxTransientFailures: WORKSPACE_LEASE_MAX_TRANSIENT_FAILURES,
25452
+ backoffBaseMs: WORKSPACE_LEASE_FAILURE_BACKOFF_BASE_MS,
25453
+ backoffMaxMs: WORKSPACE_LEASE_FAILURE_BACKOFF_MAX_MS
25454
+ };
25455
+ const verifyActiveWorkspaceLease = (runId, entry, nowMs) => {
25456
+ const lease = entry.workspaceLease;
25457
+ if (!lease) return true;
25458
+ const state = entry.leaseCheck ?? { ...FRESH_LEASE_MONITOR_STATE };
25459
+ entry.leaseCheck = state;
25460
+ if (!isLeaseCheckDue(state, nowMs)) return true;
25461
+ const decision = applyLeaseCheckResult({
25462
+ state,
25463
+ detailed: lease.verifyDetailed(),
25464
+ nowMs,
25465
+ policy: leaseCheckPolicy
25466
+ });
25467
+ entry.leaseCheck = decision.state;
25468
+ if (decision.action === "fail" && decision.failure) {
25469
+ stopRunForWorkspaceLease(runId, entry, decision.failure);
25470
+ return false;
25471
+ }
25472
+ if (decision.action === "transient" && decision.failure) {
25473
+ pushLog(
25474
+ `workspace-lease-transient run=${runId} streak=${decision.state.transientStreak}/${WORKSPACE_LEASE_MAX_TRANSIENT_FAILURES} code=${decision.failure.code} backoff_ms=${decision.backoffMs ?? 0}`
25475
+ );
25476
+ }
25477
+ return true;
24795
25478
  };
24796
25479
  const workspaceLeaseMonitor = setInterval(() => {
25480
+ const nowMs = Date.now();
24797
25481
  for (const [runId, entry] of activeAgents) {
24798
- verifyActiveWorkspaceLease(runId, entry);
25482
+ verifyActiveWorkspaceLease(runId, entry, nowMs);
24799
25483
  }
24800
25484
  }, WORKSPACE_LEASE_CHECK_INTERVAL_MS);
24801
25485
  workspaceLeaseMonitor.unref?.();
@@ -25272,7 +25956,7 @@ async function startDaemon(args) {
25272
25956
  },
25273
25957
  () => {
25274
25958
  const entry = activeAgents.get(payload.runId);
25275
- return entry ? verifyActiveWorkspaceLease(payload.runId, entry) : false;
25959
+ return entry ? verifyActiveWorkspaceLease(payload.runId, entry, Date.now()) : false;
25276
25960
  }
25277
25961
  );
25278
25962
  let agent;
@@ -25340,6 +26024,7 @@ async function startDaemon(args) {
25340
26024
  taskMeta: payload.taskMeta,
25341
26025
  prMeta: payload.prMeta,
25342
26026
  conversationId: payload.conversationId,
26027
+ alanMcp: payload.alanMcp,
25343
26028
  taskId: payload.taskId ?? payload.taskMeta?.id,
25344
26029
  teamId: payload.teamId,
25345
26030
  currentUser: payload.currentUser,
@@ -25391,7 +26076,7 @@ async function startDaemon(args) {
25391
26076
  ([, entry]) => entry.conversationId === payload.conversationId
25392
26077
  ) : [...activeAgents.entries()];
25393
26078
  const delivered = entries.some(([runId, entry]) => {
25394
- if (!entry || !verifyActiveWorkspaceLease(runId, entry)) return false;
26079
+ if (!entry || !verifyActiveWorkspaceLease(runId, entry, Date.now())) return false;
25395
26080
  return entry.agent.sendToolResponse(payload.toolId, payload.response) === true;
25396
26081
  });
25397
26082
  if (!delivered) {
@@ -26158,6 +26843,18 @@ function omitUndefined(record) {
26158
26843
  return Object.fromEntries(Object.entries(record).filter(([, value2]) => value2 !== void 0));
26159
26844
  }
26160
26845
 
26846
+ // src/resume-fallback-env.ts
26847
+ var RESUME_FALLBACK_CONTEXT_ENV = "ALAN_RESUME_FALLBACK_CONTEXT_B64";
26848
+ function decodeResumeFallbackContext(encoded) {
26849
+ if (!encoded) return void 0;
26850
+ try {
26851
+ const decoded = Buffer.from(encoded, "base64").toString("utf8").trim();
26852
+ return decoded.length > 0 ? decoded : void 0;
26853
+ } catch {
26854
+ return void 0;
26855
+ }
26856
+ }
26857
+
26161
26858
  // src/web-presenter.ts
26162
26859
  var WebPresenter = class {
26163
26860
  constructor(wsClient, _conversationId, cwd) {
@@ -26450,6 +27147,7 @@ var SandboxEventDispatcher = class {
26450
27147
  fireAndForget(event);
26451
27148
  return;
26452
27149
  }
27150
+ if (isBackgroundHeartbeat(event) && !this.outboxSocket.connected) return;
26453
27151
  this.outbox.enqueue(this.conversationId, this.currentRunId ?? this.conversationId, event);
26454
27152
  this.outbox.flush(this.outboxSocket);
26455
27153
  }
@@ -26569,7 +27267,7 @@ var WSClient = class {
26569
27267
  if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
26570
27268
  this.heartbeatTimer = setInterval(() => {
26571
27269
  if (!this.socket.connected) return;
26572
- this.socket.emit("agent.heartbeat", this.buildHeartbeat());
27270
+ this.socket.volatile.emit("agent.heartbeat", this.buildHeartbeat());
26573
27271
  }, AGENT_HEARTBEAT_INTERVAL_MS);
26574
27272
  this.heartbeatTimer.unref?.();
26575
27273
  }
@@ -26652,6 +27350,7 @@ var WSClient = class {
26652
27350
  images: data.images,
26653
27351
  files: data.files,
26654
27352
  providerSessionId: data.providerSessionId,
27353
+ resumeFallbackContext: data.resumeFallbackContext,
26655
27354
  backendKind: data.backendKind,
26656
27355
  agentId: data.agentId,
26657
27356
  agentPrompt: data.agentPrompt,
@@ -26843,6 +27542,7 @@ async function runSandbox(config) {
26843
27542
  const presenter = new WebPresenter(wsClient, config.sessionId, config.projectPath);
26844
27543
  presenter.setSuppressSessionLifecycle(true);
26845
27544
  let lastProviderSessionId = config.providerSessionId;
27545
+ let resumeFallbackContext = config.resumeFallbackContext;
26846
27546
  let activeBackendKind = config.backendKind || "claude_cli";
26847
27547
  let activeAgentId = config.agentId;
26848
27548
  let activeAgentPrompt = config.agentPrompt;
@@ -26926,10 +27626,12 @@ async function runSandbox(config) {
26926
27626
  selectedContextWindow: activeContextWindow ?? null,
26927
27627
  selectedEffortLevel: activeEffortLevel ?? null,
26928
27628
  providerSessionId: lastProviderSessionId ?? void 0,
27629
+ resumeFallbackContext,
26929
27630
  taskMeta: currentTaskMeta,
26930
27631
  prMeta: currentPrMeta,
26931
27632
  taskId: currentTaskId,
26932
27633
  conversationId: currentConversationId,
27634
+ alanMcp: currentAlanMcp,
26933
27635
  teamId: currentTeamId,
26934
27636
  workflowId,
26935
27637
  workflowExecutionId,
@@ -26977,6 +27679,7 @@ async function runSandbox(config) {
26977
27679
  }
26978
27680
  }
26979
27681
  currentAgent = null;
27682
+ resumeFallbackContext = void 0;
26980
27683
  lifecycle.info("sandbox_agent_run_complete", {
26981
27684
  success: result.success,
26982
27685
  iterations: result.iterations,
@@ -27003,6 +27706,7 @@ async function runSandbox(config) {
27003
27706
  currentFiles = nextPayload.files ?? [];
27004
27707
  if (nextPayload.providerSessionId) {
27005
27708
  lastProviderSessionId = nextPayload.providerSessionId;
27709
+ resumeFallbackContext = nextPayload.resumeFallbackContext;
27006
27710
  }
27007
27711
  if (nextPayload.backendKind) {
27008
27712
  activeBackendKind = nextPayload.backendKind;
@@ -27341,6 +28045,9 @@ async function runSessionFromEnv() {
27341
28045
  const contextWindow = process.env.ALAN_CONTEXT_WINDOW || void 0;
27342
28046
  const effortLevel = process.env.ALAN_EFFORT_LEVEL || void 0;
27343
28047
  const providerSessionId = process.env.ALAN_PROVIDER_SESSION_ID || void 0;
28048
+ const resumeFallbackContext = decodeResumeFallbackContext(
28049
+ process.env[RESUME_FALLBACK_CONTEXT_ENV]
28050
+ );
27344
28051
  const workflowId = process.env.ALAN_WORKFLOW_ID || void 0;
27345
28052
  const workflowExecutionId = process.env.ALAN_WORKFLOW_EXECUTION_ID || void 0;
27346
28053
  const sandboxId = process.env.ALAN_SANDBOX_ID || void 0;
@@ -27382,6 +28089,7 @@ async function runSessionFromEnv() {
27382
28089
  contextWindow,
27383
28090
  effortLevel,
27384
28091
  providerSessionId,
28092
+ resumeFallbackContext,
27385
28093
  workflowId,
27386
28094
  workflowExecutionId,
27387
28095
  sandboxId,