@aiden-ade/sandbox-agent 0.1.37 → 0.1.38

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 +846 -193
  2. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -3404,7 +3404,7 @@ var require_websocket = __commonJS({
3404
3404
  var http2 = require("http");
3405
3405
  var net = require("net");
3406
3406
  var tls = require("tls");
3407
- var { randomBytes: randomBytes5, createHash: createHash6 } = require("crypto");
3407
+ var { randomBytes: randomBytes5, createHash: createHash7 } = require("crypto");
3408
3408
  var { Duplex, Readable } = require("stream");
3409
3409
  var { URL: URL2 } = require("url");
3410
3410
  var PerMessageDeflate = require_permessage_deflate();
@@ -4061,7 +4061,7 @@ var require_websocket = __commonJS({
4061
4061
  abortHandshake(websocket, socket, "Invalid Upgrade header");
4062
4062
  return;
4063
4063
  }
4064
- const digest = createHash6("sha1").update(key + GUID).digest("base64");
4064
+ const digest = createHash7("sha1").update(key + GUID).digest("base64");
4065
4065
  if (res.headers["sec-websocket-accept"] !== digest) {
4066
4066
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
4067
4067
  return;
@@ -4428,7 +4428,7 @@ var require_websocket_server = __commonJS({
4428
4428
  var EventEmitter = require("events");
4429
4429
  var http2 = require("http");
4430
4430
  var { Duplex } = require("stream");
4431
- var { createHash: createHash6 } = require("crypto");
4431
+ var { createHash: createHash7 } = require("crypto");
4432
4432
  var extension = require_extension();
4433
4433
  var PerMessageDeflate = require_permessage_deflate();
4434
4434
  var subprotocol = require_subprotocol();
@@ -4725,7 +4725,7 @@ var require_websocket_server = __commonJS({
4725
4725
  );
4726
4726
  }
4727
4727
  if (this._state > RUNNING) return abortHandshake(socket, 503);
4728
- const digest = createHash6("sha1").update(key + GUID).digest("base64");
4728
+ const digest = createHash7("sha1").update(key + GUID).digest("base64");
4729
4729
  const headers = [
4730
4730
  "HTTP/1.1 101 Switching Protocols",
4731
4731
  "Upgrade: websocket",
@@ -4867,19 +4867,18 @@ var import_node_os7 = require("os");
4867
4867
  var import_node_path10 = require("path");
4868
4868
 
4869
4869
  // ../shared/dist/constants/agent.js
4870
- var INTERACTIVE_TOOL_NAMES = /* @__PURE__ */ new Set(["askuserquestion", "exitplanmode", "enterplanmode"]);
4870
+ var INTERACTIVE_TOOL_NAMES = /* @__PURE__ */ new Set([
4871
+ "askuserquestion",
4872
+ "askquestion",
4873
+ "exitplanmode",
4874
+ "enterplanmode"
4875
+ ]);
4876
+ var HARNESS_ASK_TOOL_NAMES = /* @__PURE__ */ new Set(["askuserquestion", "askquestion"]);
4871
4877
  function normalizeToolName(name) {
4872
4878
  const lower = name.toLowerCase();
4873
4879
  const mcp = lower.match(/^mcp__[a-z0-9-]+__(.+)$/);
4874
- let bare = mcp ? mcp[1] : lower;
4875
- bare = bare.replace(/[_\s-]/g, "");
4876
- for (const vendor of ["alan", "alan"]) {
4877
- if (bare.startsWith(vendor) && bare.length > vendor.length) {
4878
- bare = bare.slice(vendor.length);
4879
- break;
4880
- }
4881
- }
4882
- return bare;
4880
+ const bare = mcp ? mcp[1] : lower;
4881
+ return bare.replace(/[_\s-]/g, "");
4883
4882
  }
4884
4883
  function toolNameFromInput(input) {
4885
4884
  if (!input || typeof input !== "object")
@@ -4899,16 +4898,13 @@ function inputLooksLikeAskUserQuestion(input) {
4899
4898
  return Array.isArray(args.questions) && args.questions.length > 0;
4900
4899
  }
4901
4900
  function isAskUserQuestionTool(name, input) {
4902
- if (normalizeToolName(name) === "askuserquestion")
4901
+ if (HARNESS_ASK_TOOL_NAMES.has(normalizeToolName(name)))
4903
4902
  return true;
4904
4903
  const nested = toolNameFromInput(input);
4905
- if (nested && nested !== name && normalizeToolName(nested) === "askuserquestion") {
4904
+ if (nested && nested !== name && HARNESS_ASK_TOOL_NAMES.has(normalizeToolName(nested))) {
4906
4905
  return true;
4907
4906
  }
4908
- if (normalizeToolName(name) === "mcp" && inputLooksLikeAskUserQuestion(input)) {
4909
- return true;
4910
- }
4911
- return false;
4907
+ return inputLooksLikeAskUserQuestion(input);
4912
4908
  }
4913
4909
  function isInteractiveToolName(name, input) {
4914
4910
  const normalized = normalizeToolName(name);
@@ -9574,6 +9570,11 @@ var RUNNER_STATUS_PROTOCOL_VERSION = 2;
9574
9570
  var RUNNER_STATUS_MIN_SUPPORTED_PROTOCOL_VERSION = Math.max(1, RUNNER_STATUS_PROTOCOL_VERSION - 1);
9575
9571
  var RUNNER_STATUS_MAX_SUPPORTED_PROTOCOL_VERSION = RUNNER_STATUS_PROTOCOL_VERSION;
9576
9572
  var RUNNER_RECOVERY_GRACE_MS = 2 * 60 * 1e3;
9573
+ var SYSTEM_ABORT_RUN_ERRORS = {
9574
+ watchdog_timeout: "Agent run was stopped by the server after a period of no stream activity",
9575
+ provider_exited: "Agent process exited unexpectedly",
9576
+ runner_stalled: "Agent run stalled with no recent activity and was recovered"
9577
+ };
9577
9578
  function resolveAgentConfigFileName(profile) {
9578
9579
  switch (profile) {
9579
9580
  case "production":
@@ -14510,33 +14511,6 @@ Deliverables persist as Alan artifacts and documents \u2014 not as chat. Plans,
14510
14511
  For non-trivial tasks, log high-signal events as they happen: decisions, changed assumptions, validation results, known-good IDs, blockers, risks, and direction changes. Keep one current task-state summary updated with current state, known-good IDs, open risks, and next action. Do not log routine commands or file reads. Before stopping, ensure the timeline and current state are enough for a human to understand progress and for another agent to resume. If a prior decision is stale, write a newer event that supersedes it.
14511
14512
 
14512
14513
  The specific work you do \u2014 implementation, planning, review, coordination, verification \u2014 is determined by your agent role, described next.`;
14513
- var ALAN_CURSOR_ASK_USER_OVERLAY = `### Cursor runtime note
14514
- Cursor native \`AskQuestion\` does not block stdin for Alan yet. When you need clickable question cards on Cursor, use \`mcp__alan__ask_user_question\` (server tool: \`ask_user_question\`) instead of native AskQuestion.`;
14515
- var ALAN_ASK_USER_PROMPT = `## Asking the user
14516
-
14517
- When you need a decision or information only the user can provide, do not guess \u2014 ask with an interactive ask tool so Alan can render clickable question cards.
14518
-
14519
- ### Tool preference (required)
14520
- 1. Prefer the **native** ask tool when it appears in your tool list (\`AskUserQuestion\`, or the backend's equivalent built-in ask tool).
14521
- 2. Only if native ask is missing, call \`mcp__alan__ask_user_question\` (server tool name: \`ask_user_question\`).
14522
-
14523
- Required arguments (both native and MCP forms):
14524
- - \`questions\`: non-empty array. Each item needs a clear \`question\` string.
14525
- - Prefer 2\u20135 selectable \`options\` per question (\`label\` required; optional \`value\` / \`description\`). Free-text-only questions are allowed when options do not fit.
14526
- - Do NOT include an "Other" option \u2014 the UI always adds a freeform Other\u2026 field.
14527
- - Set \`multiSelect: true\` on a question when the user may pick more than one option; omit or set false for single-select (default).
14528
- - Optional \`header\` per question and optional \`title\` for the card header.
14529
-
14530
- If neither native ask nor the MCP ask tool is available, ask ONE short plain-text question \u2014 do not invent cards or pretend the UI exists.
14531
-
14532
- ### Never do this
14533
- - Do NOT paste multiple-choice lists, A/B/C tables, or "reply with 1A / 1B" in chat.
14534
- - Do NOT say "answer in the UI", "see the cards", "questions are above", or similar unless you just successfully called an ask tool with real \`questions\` in this turn.
14535
- - Do NOT call the tool with empty \`questions\`, placeholder text, or missing options when choices exist.
14536
- - Do NOT invent defaults when the user skips. If the next user message says they skipped, continue without those answers (or re-ask only if truly required).
14537
-
14538
- ### After the tool call
14539
- The ask tool returns an empty/ack result and does NOT wait. Immediately end your turn. At most one short line like "Please pick an option above." \u2014 no restating the full questions in markdown. The user's answer (or skip) arrives as the next user message; continue from there.`;
14540
14514
  function buildAlanTeamScopePrompt(teamId) {
14541
14515
  return [
14542
14516
  `You are operating within team \`${teamId}\`.`,
@@ -15228,20 +15202,23 @@ var import_path3 = require("path");
15228
15202
  var import_readline = require("readline");
15229
15203
  var import_child_process2 = require("child_process");
15230
15204
  var import_crypto2 = require("crypto");
15231
- var import_child_process3 = require("child_process");
15232
- var import_util10 = require("util");
15233
15205
  var import_fs4 = require("fs");
15234
15206
  var import_os4 = require("os");
15235
15207
  var import_path4 = require("path");
15208
+ var import_child_process3 = require("child_process");
15209
+ var import_util10 = require("util");
15236
15210
  var import_fs5 = require("fs");
15237
15211
  var import_os5 = require("os");
15238
15212
  var import_path5 = require("path");
15239
15213
  var import_fs6 = require("fs");
15240
15214
  var import_os6 = require("os");
15241
15215
  var import_path6 = require("path");
15242
- var import_crypto3 = require("crypto");
15243
15216
  var import_fs7 = require("fs");
15217
+ var import_os7 = require("os");
15244
15218
  var import_path7 = require("path");
15219
+ var import_crypto3 = require("crypto");
15220
+ var import_fs8 = require("fs");
15221
+ var import_path8 = require("path");
15245
15222
  var MANAGED_ALAN_MCP_SERVER_NAMES = [
15246
15223
  "alan",
15247
15224
  "alan-prod",
@@ -15736,6 +15713,10 @@ function getClaudePermissionMode(config) {
15736
15713
  function getClaudeDisallowedTools(config) {
15737
15714
  return isReadOnlyAgent(config.agentId) ? ["Edit", "Write", "MultiEdit", "NotebookEdit"] : [];
15738
15715
  }
15716
+ function getCodexPermissionArgs(config) {
15717
+ const sandbox = shouldUseReadOnlyRuntimePermissions(config) ? "read-only" : "danger-full-access";
15718
+ return ["--ask-for-approval", "never", "--sandbox", sandbox];
15719
+ }
15739
15720
  function isLikelyProviderAuthError(stderr) {
15740
15721
  const lower = stderr.toLowerCase();
15741
15722
  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");
@@ -15832,6 +15813,16 @@ function spawnCli(command, args, context) {
15832
15813
  detached: process.platform !== "win32"
15833
15814
  });
15834
15815
  }
15816
+ function emitSessionNotice(presenter, kind, message) {
15817
+ if (presenter.onNotice) {
15818
+ void presenter.onNotice(kind, message);
15819
+ return;
15820
+ }
15821
+ presenter.onLog(`[session_notice:${kind}] ${message}`);
15822
+ }
15823
+ function logResumeObservability(event, fields) {
15824
+ console.info(`[agent-resume] ${event}`, { event, ...fields });
15825
+ }
15835
15826
  function structuredEventKey(event) {
15836
15827
  const type = typeof event.type === "string" && event.type ? event.type : "";
15837
15828
  const role = typeof event.role === "string" && event.role ? event.role : "";
@@ -15901,7 +15892,12 @@ function createGenericCliBackend(options) {
15901
15892
  const args = options.buildArgs?.(context, prompt) ?? options.args.map((arg) => arg === "{{prompt}}" ? prompt : arg);
15902
15893
  const child = spawnCli(options.command, args, context);
15903
15894
  state.process = child;
15895
+ state.lastRawOutputAtMs = Date.now();
15904
15896
  context.onProcessSpawned?.(child);
15897
+ context.registerLivenessProbe?.(() => ({
15898
+ providerAlive: child.exitCode === null,
15899
+ lastRawOutputAgoMs: typeof state.lastRawOutputAtMs === "number" ? Date.now() - state.lastRawOutputAtMs : null
15900
+ }));
15905
15901
  const safeArgs = args.map(
15906
15902
  (a, i) => i > 0 && args[i - 1] === "--system-prompt" ? `"<system-prompt ${a.length} chars>"` : a
15907
15903
  );
@@ -15950,6 +15946,7 @@ function createGenericCliBackend(options) {
15950
15946
  }
15951
15947
  stdoutRl.on("line", (line) => {
15952
15948
  sawStdoutLine = true;
15949
+ state.lastRawOutputAtMs = Date.now();
15953
15950
  presenter.recordRawTranscript?.("stdout", line);
15954
15951
  if (options.parseStructuredLine) {
15955
15952
  options.parseStructuredLine(line, context, state);
@@ -15967,6 +15964,7 @@ function createGenericCliBackend(options) {
15967
15964
  }
15968
15965
  });
15969
15966
  stderrRl.on("line", (line) => {
15967
+ state.lastRawOutputAtMs = Date.now();
15970
15968
  presenter.recordRawTranscript?.("stderr", line);
15971
15969
  if (options.parseStderrLine) {
15972
15970
  options.parseStderrLine(line, context, state);
@@ -16013,7 +16011,7 @@ function createGenericCliBackend(options) {
16013
16011
  if (presenter.pendingAskUserToolIds?.size) {
16014
16012
  idleTimeoutReason = "pending_user_answer";
16015
16013
  resolve22("idle_timeout");
16016
- } else if (state.activeBackgroundTaskIds?.size) {
16014
+ } else if (state.resultDeferredOnBackgroundWork && state.activeBackgroundTaskIds?.size) {
16017
16015
  idleTimeoutReason = "background_task";
16018
16016
  resolve22("idle_timeout");
16019
16017
  } else {
@@ -16076,10 +16074,26 @@ function createGenericCliBackend(options) {
16076
16074
  logUnhandledStructuredEventSummary(options.kind, state);
16077
16075
  }
16078
16076
  const requestedResumeId = context.config.runtimeSessionId?.trim() || context.config.providerSessionId?.trim();
16077
+ let cursorResumeSilentlyFailed = false;
16079
16078
  if (requestedResumeId && state.runtimeSessionId && state.runtimeSessionId !== requestedResumeId) {
16080
16079
  console.warn(
16081
16080
  `[${options.kind}] CLI reported session id ${state.runtimeSessionId} after resume was requested with ${requestedResumeId} \u2014 expected for claude session forks, a failed-resume signal for cursor-agent`
16082
16081
  );
16082
+ if (options.kind === "cursor_agent_cli") {
16083
+ cursorResumeSilentlyFailed = true;
16084
+ logResumeObservability("agent_resume_failed", {
16085
+ backendKind: options.kind,
16086
+ reason: "cli_forked_fresh_session",
16087
+ requestedResumeId,
16088
+ runtimeSessionId: state.runtimeSessionId,
16089
+ hadFallbackContext: Boolean(context.config.resumeFallbackContext?.trim())
16090
+ });
16091
+ emitSessionNotice(
16092
+ context.presenter,
16093
+ "session_resume_failed",
16094
+ "The previous session could not be resumed, so this reply was generated without earlier conversation context. Retrying with the recent history re-included."
16095
+ );
16096
+ }
16083
16097
  }
16084
16098
  if (context.abortController.signal.aborted) {
16085
16099
  return {
@@ -16173,6 +16187,22 @@ function createGenericCliBackend(options) {
16173
16187
  const stderrText = stderrLines.join("\n").trim();
16174
16188
  const hasStructuredError = exitCode === 0 && !!state.error?.trim();
16175
16189
  const failed = exitCode !== 0 || hasStructuredError;
16190
+ if (cursorResumeSilentlyFailed && !failed) {
16191
+ return {
16192
+ success: false,
16193
+ summary: state.summary.trim() || "Session resume failed",
16194
+ filesModified: [],
16195
+ planFilesCreated: [],
16196
+ iterations: Math.max(state.iterations, 1),
16197
+ error: SESSION_RESUME_FAILED_MESSAGE,
16198
+ errorKind: "resume_failed",
16199
+ providerSessionId: state.runtimeSessionId,
16200
+ runtimeSessionId: state.runtimeSessionId,
16201
+ backendKind: options.kind,
16202
+ supportTier: options.supportTier,
16203
+ usage: state.usage
16204
+ };
16205
+ }
16176
16206
  const summary = state.summary.trim() || state.error?.trim() || (failed ? "Task failed" : "Task completed");
16177
16207
  return {
16178
16208
  success: !failed,
@@ -16622,6 +16652,21 @@ function createAntigravityCliBackend(command = "agy", defaultArgs = []) {
16622
16652
  }
16623
16653
  };
16624
16654
  }
16655
+ function resolveClaudeHome(env, variant = "claude") {
16656
+ if (variant === "supatest") {
16657
+ return (0, import_path4.join)((0, import_os4.homedir)(), ".supatest", "claude-internal");
16658
+ }
16659
+ return env.CLAUDE_CONFIG_DIR || process.env.CLAUDE_CONFIG_DIR || (0, import_path4.join)((0, import_os4.homedir)(), ".claude");
16660
+ }
16661
+ function encodeClaudeProjectDir(cwd) {
16662
+ return cwd.replace(/[^a-zA-Z0-9]/g, "-");
16663
+ }
16664
+ function claudeSessionLogExists(input) {
16665
+ if (!input.sessionId || !input.cwd) return false;
16666
+ const projectDir = (0, import_path4.join)(input.home, "projects", encodeClaudeProjectDir(input.cwd));
16667
+ if (!(0, import_fs4.existsSync)(projectDir)) return false;
16668
+ return (0, import_fs4.existsSync)((0, import_path4.join)(projectDir, `${input.sessionId}.jsonl`));
16669
+ }
16625
16670
  var execFileAsync = (0, import_util10.promisify)(import_child_process3.execFile);
16626
16671
  var flagSupportCache = /* @__PURE__ */ new Map();
16627
16672
  function helpAdvertisesFlag(helpText, flag) {
@@ -16672,15 +16717,38 @@ function registerBackgroundTaskIds(state, keys) {
16672
16717
  state.activeBackgroundTaskIds.add(key);
16673
16718
  }
16674
16719
  }
16675
- function registerBackgroundTaskRecord(state, record) {
16720
+ function registerBackgroundLauncher(state, toolUseId) {
16721
+ registerBackgroundTaskIds(state, [toolUseId]);
16722
+ if (!state.pendingLauncherToolIds) state.pendingLauncherToolIds = [];
16723
+ if (!state.pendingLauncherToolIds.includes(toolUseId)) {
16724
+ state.pendingLauncherToolIds.push(toolUseId);
16725
+ }
16726
+ }
16727
+ function drainNextUnlinkedBackgroundLauncher(state) {
16728
+ const launcherId = state.pendingLauncherToolIds?.shift();
16729
+ if (state.pendingLauncherToolIds?.length === 0) state.pendingLauncherToolIds = void 0;
16730
+ if (launcherId) clearBackgroundTaskIds(state, [launcherId]);
16731
+ return launcherId;
16732
+ }
16733
+ function registerBackgroundTaskRecord(state, record, launcherToolUseId) {
16676
16734
  const taskId = backgroundTaskId(record);
16677
- let toolUseId = backgroundToolUseId(record);
16678
- if (!toolUseId && taskId && state.activeBackgroundTaskIds?.size === 1) {
16679
- const [candidate] = state.activeBackgroundTaskIds;
16680
- if (candidate && candidate !== taskId) toolUseId = candidate;
16735
+ let toolUseId = backgroundToolUseId(record) ?? launcherToolUseId ?? void 0;
16736
+ if (!toolUseId && taskId) {
16737
+ const nextLauncher = state.pendingLauncherToolIds?.[0];
16738
+ if (nextLauncher) {
16739
+ toolUseId = nextLauncher;
16740
+ } else if (state.activeBackgroundTaskIds?.size === 1) {
16741
+ const [candidate] = state.activeBackgroundTaskIds;
16742
+ if (candidate && candidate !== taskId) toolUseId = candidate;
16743
+ }
16681
16744
  }
16682
16745
  registerBackgroundTaskIds(state, backgroundTaskKeys(record));
16683
16746
  if (!taskId || !toolUseId) return;
16747
+ if (state.pendingLauncherToolIds) {
16748
+ const idx = state.pendingLauncherToolIds.indexOf(toolUseId);
16749
+ if (idx >= 0) state.pendingLauncherToolIds.splice(idx, 1);
16750
+ if (state.pendingLauncherToolIds.length === 0) state.pendingLauncherToolIds = void 0;
16751
+ }
16684
16752
  if (!state.backgroundToolIdByTaskId) state.backgroundToolIdByTaskId = /* @__PURE__ */ new Map();
16685
16753
  if (!state.backgroundTaskIdsByToolId) state.backgroundTaskIdsByToolId = /* @__PURE__ */ new Map();
16686
16754
  state.backgroundToolIdByTaskId.set(taskId, toolUseId);
@@ -16711,6 +16779,10 @@ function clearBackgroundTaskIds(state, keys) {
16711
16779
  state.backgroundTaskIdsByToolId?.delete(key);
16712
16780
  }
16713
16781
  }
16782
+ if (state.pendingLauncherToolIds) {
16783
+ state.pendingLauncherToolIds = state.pendingLauncherToolIds.filter((id) => activeIds.has(id));
16784
+ if (state.pendingLauncherToolIds.length === 0) state.pendingLauncherToolIds = void 0;
16785
+ }
16714
16786
  if (activeIds.size === 0) state.activeBackgroundTaskIds = void 0;
16715
16787
  }
16716
16788
  function resolveBackgroundTaskToolUseId(state, record) {
@@ -16733,6 +16805,7 @@ function deferOrFinalizeStructuredResult(kind, context, state) {
16733
16805
  console.info(
16734
16806
  `[${kind}] Keeping stdin open \u2014 ${pendingAsks} interactive question(s) awaiting user response`
16735
16807
  );
16808
+ state.resultDeferredOnPendingAnswer = true;
16736
16809
  return "deferred_pending_answer";
16737
16810
  }
16738
16811
  const activeBackgroundTasks = state.activeBackgroundTaskIds?.size ?? 0;
@@ -16747,9 +16820,10 @@ function deferOrFinalizeStructuredResult(kind, context, state) {
16747
16820
  return "finalized";
16748
16821
  }
16749
16822
  function maybeFinalizeDeferredResult(kind, context, state) {
16750
- if (!state.resultDeferredOnBackgroundWork) return;
16823
+ if (!state.resultDeferredOnBackgroundWork && !state.resultDeferredOnPendingAnswer) return;
16751
16824
  if (context.presenter.pendingAskUserToolIds?.size || state.activeBackgroundTaskIds?.size) return;
16752
16825
  state.resultDeferredOnBackgroundWork = false;
16826
+ state.resultDeferredOnPendingAnswer = false;
16753
16827
  finalizeStructuredResult(kind, state);
16754
16828
  }
16755
16829
  var DEBUG_ASK = process.env.ALAN_DEBUG_ASK === "1" || process.env.ALAN_DEBUG === "1";
@@ -16819,21 +16893,44 @@ function handleClaudeStructuredEvent(parsed, context, state) {
16819
16893
  return true;
16820
16894
  }
16821
16895
  if (parsed.subtype === "task_started") {
16822
- registerBackgroundTaskRecord(state, parsed);
16896
+ registerBackgroundTaskRecord(state, parsed, parentToolUseId);
16897
+ return true;
16898
+ }
16899
+ if (parsed.subtype === "compact_boundary") {
16900
+ const compactMetadata = typeof parsed.compact_metadata === "object" && parsed.compact_metadata !== null ? parsed.compact_metadata : {};
16901
+ const trigger = typeof compactMetadata.trigger === "string" ? compactMetadata.trigger : typeof parsed.trigger === "string" ? parsed.trigger : "auto";
16902
+ const preTokens = typeof compactMetadata.pre_tokens === "number" ? compactMetadata.pre_tokens : typeof parsed.pre_tokens === "number" ? parsed.pre_tokens : void 0;
16903
+ const tokensPart = typeof preTokens === "number" && preTokens > 0 ? ` (${Math.round(preTokens / 1e3)}k tokens summarized)` : "";
16904
+ emitSessionNotice(
16905
+ presenter,
16906
+ "context_compacted",
16907
+ trigger === "manual" ? `Conversation context was compacted${tokensPart} \u2014 earlier detail was replaced with a summary.` : `Conversation context was automatically compacted${tokensPart} to stay within the model's limit \u2014 earlier detail was replaced with a summary.`
16908
+ );
16823
16909
  return true;
16824
16910
  }
16825
16911
  if (parsed.subtype === "task_notification") {
16826
16912
  if (isTerminalTaskNotification(parsed)) {
16827
16913
  const toolUseId = resolveBackgroundTaskToolUseId(state, parsed);
16828
16914
  const wasActive = !!toolUseId && state.activeBackgroundTaskIds?.has(toolUseId) ? true : backgroundTaskKeys(parsed).some((key) => state.activeBackgroundTaskIds?.has(key));
16915
+ const isFailed = isFailedTaskNotification(parsed) ? true : void 0;
16829
16916
  if (toolUseId && wasActive) {
16830
16917
  void presenter.onToolResult?.(
16831
16918
  toolUseId,
16832
16919
  formatClaudeTaskNotificationResult(parsed),
16833
- isFailedTaskNotification(parsed) ? true : void 0
16920
+ isFailed
16834
16921
  );
16922
+ clearBackgroundTaskIds(state, backgroundTaskKeys(parsed));
16923
+ } else {
16924
+ clearBackgroundTaskIds(state, backgroundTaskKeys(parsed));
16925
+ const drainedLauncher = drainNextUnlinkedBackgroundLauncher(state);
16926
+ if (drainedLauncher) {
16927
+ void presenter.onToolResult?.(
16928
+ drainedLauncher,
16929
+ formatClaudeTaskNotificationResult(parsed),
16930
+ isFailed
16931
+ );
16932
+ }
16835
16933
  }
16836
- clearBackgroundTaskIds(state, backgroundTaskKeys(parsed));
16837
16934
  maybeFinalizeDeferredResult("claude_cli", context, state);
16838
16935
  }
16839
16936
  return true;
@@ -16841,9 +16938,10 @@ function handleClaudeStructuredEvent(parsed, context, state) {
16841
16938
  return false;
16842
16939
  }
16843
16940
  case "assistant": {
16844
- if (presenter.pendingAskUserToolIds?.size) {
16941
+ if (state.awaitingAskDenialFallback) {
16942
+ state.awaitingAskDenialFallback = false;
16845
16943
  console.info(
16846
- `[claude_cli] Suppressing fallback assistant turn \u2014 ${presenter.pendingAskUserToolIds.size} AskUserQuestion(s) still pending`
16944
+ "[claude_cli] Suppressing auto-denial fallback assistant turn after native AskUserQuestion"
16847
16945
  );
16848
16946
  return true;
16849
16947
  }
@@ -16872,7 +16970,7 @@ function handleClaudeStructuredEvent(parsed, context, state) {
16872
16970
  parentToolUseId
16873
16971
  );
16874
16972
  if (CLAUDE_SUBAGENT_TOOL_NAMES.has(toolBlock.name.toLowerCase())) {
16875
- registerBackgroundTaskIds(state, [toolBlock.id]);
16973
+ registerBackgroundLauncher(state, toolBlock.id);
16876
16974
  }
16877
16975
  const normalizedName = normalizeToolName(toolBlock.name);
16878
16976
  if (isInteractiveToolName(toolBlock.name) || INTERACTIVE_TOOL_NAMES.has(normalizedName)) {
@@ -16883,6 +16981,10 @@ function handleClaudeStructuredEvent(parsed, context, state) {
16883
16981
  if (isAskUserQuestionTool(toolBlock.name)) {
16884
16982
  if (!presenter.pendingAskUserToolIds) presenter.pendingAskUserToolIds = /* @__PURE__ */ new Set();
16885
16983
  presenter.pendingAskUserToolIds.add(toolBlock.id);
16984
+ if (!toolBlock.name.toLowerCase().startsWith("mcp__")) {
16985
+ if (!state.nativeAskToolIds) state.nativeAskToolIds = /* @__PURE__ */ new Set();
16986
+ state.nativeAskToolIds.add(toolBlock.id);
16987
+ }
16886
16988
  debugAskLog(
16887
16989
  `[claude_cli] AskUserQuestion tool_use detected \u2014 id=${toolBlock.id}, name=${toolBlock.name}, pendingSize=${presenter.pendingAskUserToolIds.size}, presenterHasProp=${Object.hasOwn(presenter, "pendingAskUserToolIds")}`
16888
16990
  );
@@ -16907,6 +17009,22 @@ function handleClaudeStructuredEvent(parsed, context, state) {
16907
17009
  for (const block of content) {
16908
17010
  if (typeof block === "object" && block !== null && "type" in block && block.type === "tool_result" && "tool_use_id" in block && typeof block.tool_use_id === "string") {
16909
17011
  if (state.suppressedToolResultIds?.has(block.tool_use_id)) {
17012
+ const isSuppressedError = "is_error" in block && block.is_error === true;
17013
+ const isNativeAsk = state.nativeAskToolIds?.has(block.tool_use_id) === true;
17014
+ if (isSuppressedError && !isNativeAsk && presenter.pendingAskUserToolIds?.has(block.tool_use_id)) {
17015
+ presenter.pendingAskUserToolIds.delete(block.tool_use_id);
17016
+ state.suppressedToolResultIds.delete(block.tool_use_id);
17017
+ const resultText2 = extractToolResultContent(block.content);
17018
+ console.info(
17019
+ `[claude_cli] Ask tool ${block.tool_use_id} returned an error \u2014 clearing pending ask so the run can finish`
17020
+ );
17021
+ void presenter.onToolResult?.(block.tool_use_id, resultText2, true, parentToolUseId);
17022
+ maybeFinalizeDeferredResult("claude_cli", context, state);
17023
+ continue;
17024
+ }
17025
+ if (isNativeAsk) {
17026
+ state.awaitingAskDenialFallback = true;
17027
+ }
16910
17028
  console.info(
16911
17029
  `[claude_cli] Suppressed auto-generated tool_result for ${block.tool_use_id}`
16912
17030
  );
@@ -17014,7 +17132,31 @@ function createClaudeCliBackend(command = "claude", defaultArgs = []) {
17014
17132
  if (disallowedTools.length > 0) {
17015
17133
  args.push("--disallowedTools", disallowedTools.join(","));
17016
17134
  }
17017
- const resumeId = context.config.runtimeSessionId?.trim() || context.config.providerSessionId?.trim();
17135
+ const requestedResumeId = context.config.runtimeSessionId?.trim() || context.config.providerSessionId?.trim();
17136
+ let resumeId = requestedResumeId;
17137
+ let resumeContextPrefix;
17138
+ if (requestedResumeId && !claudeSessionLogExists({
17139
+ sessionId: requestedResumeId,
17140
+ cwd: context.cwd,
17141
+ home: resolveClaudeHome(context.env)
17142
+ })) {
17143
+ resumeId = void 0;
17144
+ resumeContextPrefix = context.config.resumeFallbackContext?.trim() || void 0;
17145
+ console.warn(
17146
+ `[claude_cli] No session log for ${requestedResumeId} under cwd ${context.cwd}; starting a fresh session instead of resuming`
17147
+ );
17148
+ logResumeObservability("agent_resume_fallback_used", {
17149
+ backendKind: "claude_cli",
17150
+ reason: "session_log_missing_for_cwd",
17151
+ requestedResumeId,
17152
+ hadFallbackContext: Boolean(resumeContextPrefix)
17153
+ });
17154
+ emitSessionNotice(
17155
+ context.presenter,
17156
+ "session_resume_failed",
17157
+ resumeContextPrefix ? "The previous Claude session was created in a different working directory and could not be resumed here, so a fresh session was started with the recent conversation history re-included." : "The previous Claude session was created in a different working directory and could not be resumed here, so a fresh session was started. Earlier conversation context may be missing from this reply."
17158
+ );
17159
+ }
17018
17160
  if (!resumeId && context.config.systemPrompt?.trim()) {
17019
17161
  args.push("--system-prompt", context.config.systemPrompt.trim());
17020
17162
  } else if (resumeId && context.config.systemPromptAppend?.trim()) {
@@ -17034,6 +17176,10 @@ function createClaudeCliBackend(command = "claude", defaultArgs = []) {
17034
17176
  if (resumeId) {
17035
17177
  args.push("--resume", resumeId);
17036
17178
  console.info("[claude_cli] Resuming session", { resumeId });
17179
+ logResumeObservability("agent_resume_requested", {
17180
+ backendKind: "claude_cli",
17181
+ requestedResumeId: resumeId
17182
+ });
17037
17183
  }
17038
17184
  args.push("--chrome");
17039
17185
  console.info("[claude_cli] Chrome flag added \u2014 final args:", args.join(" "));
@@ -17054,7 +17200,10 @@ function createClaudeCliBackend(command = "claude", defaultArgs = []) {
17054
17200
  });
17055
17201
  }
17056
17202
  }
17057
- const promptText = ctx.config.mode === "plan" ? buildPlanModePrefix(buildPromptWithSystem(ctx.config, ctx.promptText)) : buildPromptWithSystem(ctx.config, ctx.promptText);
17203
+ const basePromptText = resumeContextPrefix ? `${resumeContextPrefix}
17204
+
17205
+ ${ctx.promptText}` : ctx.promptText;
17206
+ const promptText = ctx.config.mode === "plan" ? buildPlanModePrefix(buildPromptWithSystem(ctx.config, basePromptText)) : buildPromptWithSystem(ctx.config, basePromptText);
17058
17207
  if (promptText.trim()) {
17059
17208
  contentBlocks.push({ type: "text", text: promptText });
17060
17209
  }
@@ -17105,27 +17254,30 @@ function normalizeCodexMcpToolResult(output) {
17105
17254
  }
17106
17255
  return raw;
17107
17256
  }
17257
+ function resolveCodexHome(env) {
17258
+ return env.CODEX_HOME || process.env.CODEX_HOME || (0, import_path5.join)((0, import_os5.homedir)(), ".codex");
17259
+ }
17108
17260
  function findCodexSessionLog(runtimeSessionId, codexHome) {
17109
- if (!runtimeSessionId || !(0, import_fs4.existsSync)(codexHome)) return null;
17110
- const root = (0, import_path4.join)(codexHome, "sessions");
17111
- if (!(0, import_fs4.existsSync)(root)) return null;
17261
+ if (!runtimeSessionId || !(0, import_fs5.existsSync)(codexHome)) return null;
17262
+ const root = (0, import_path5.join)(codexHome, "sessions");
17263
+ if (!(0, import_fs5.existsSync)(root)) return null;
17112
17264
  const matches = [];
17113
17265
  const stack = [root];
17114
17266
  while (stack.length > 0) {
17115
17267
  const dir = stack.pop();
17116
17268
  let entries;
17117
17269
  try {
17118
- entries = (0, import_fs4.readdirSync)(dir, { withFileTypes: true });
17270
+ entries = (0, import_fs5.readdirSync)(dir, { withFileTypes: true });
17119
17271
  } catch {
17120
17272
  continue;
17121
17273
  }
17122
17274
  for (const entry of entries) {
17123
- const path = (0, import_path4.join)(dir, entry.name);
17275
+ const path = (0, import_path5.join)(dir, entry.name);
17124
17276
  if (entry.isDirectory()) {
17125
17277
  stack.push(path);
17126
17278
  } else if (entry.isFile() && entry.name.includes(runtimeSessionId) && entry.name.endsWith(".jsonl")) {
17127
17279
  try {
17128
- matches.push({ path, mtimeMs: (0, import_fs4.statSync)(path).mtimeMs });
17280
+ matches.push({ path, mtimeMs: (0, import_fs5.statSync)(path).mtimeMs });
17129
17281
  } catch {
17130
17282
  matches.push({ path, mtimeMs: 0 });
17131
17283
  }
@@ -17158,12 +17310,14 @@ function latestUserMessageLineIndex(lines) {
17158
17310
  async function replayCodexMcpToolEventsFromSessionLog(context, state) {
17159
17311
  const runtimeSessionId = state.runtimeSessionId;
17160
17312
  if (!runtimeSessionId) return;
17161
- const codexHome = context.env.CODEX_HOME || process.env.CODEX_HOME || (0, import_path4.join)((0, import_os4.homedir)(), ".codex");
17313
+ if (!state.iterations || state.iterations <= 0) return;
17314
+ const codexHome = resolveCodexHome(context.env);
17162
17315
  const logPath = findCodexSessionLog(runtimeSessionId, codexHome);
17163
17316
  if (!logPath) return;
17164
- const allLines = (0, import_fs4.readFileSync)(logPath, "utf8").split(/\r?\n/).filter(Boolean);
17317
+ const allLines = (0, import_fs5.readFileSync)(logPath, "utf8").split(/\r?\n/).filter(Boolean);
17165
17318
  const latestUserLineIndex = latestUserMessageLineIndex(allLines);
17166
17319
  const lines = latestUserLineIndex >= 0 ? allLines.slice(latestUserLineIndex + 1) : allLines;
17320
+ const streamedToolIds = state.codexStreamedToolIds ?? /* @__PURE__ */ new Set();
17167
17321
  const emittedToolIds = /* @__PURE__ */ new Set();
17168
17322
  for (const line of lines) {
17169
17323
  const entry = parseJsonObject(line);
@@ -17174,6 +17328,7 @@ async function replayCodexMcpToolEventsFromSessionLog(context, state) {
17174
17328
  const name = typeof payload.name === "string" ? payload.name : "";
17175
17329
  const namespace = typeof payload.namespace === "string" ? payload.namespace : "";
17176
17330
  if (!callId || !name || !namespace.startsWith("mcp__")) continue;
17331
+ if (streamedToolIds.has(callId)) continue;
17177
17332
  const toolName = `${namespace.replace(/_+$/, "")}__${name}`;
17178
17333
  context.presenter.onToolUse(toolName, parseMaybeJson(payload.arguments), callId);
17179
17334
  emittedToolIds.add(callId);
@@ -17182,6 +17337,7 @@ async function replayCodexMcpToolEventsFromSessionLog(context, state) {
17182
17337
  if (payload.type === "tool_search_call") {
17183
17338
  const callId = typeof payload.call_id === "string" ? payload.call_id : "";
17184
17339
  if (!callId) continue;
17340
+ if (streamedToolIds.has(callId)) continue;
17185
17341
  context.presenter.onToolUse("tool_search", payload.arguments ?? {}, callId);
17186
17342
  emittedToolIds.add(callId);
17187
17343
  continue;
@@ -17208,6 +17364,63 @@ async function replayCodexMcpToolEventsFromSessionLog(context, state) {
17208
17364
  }
17209
17365
  }
17210
17366
  }
17367
+ var CODEX_EXIT_GRACE_MS = 3e4;
17368
+ function armCodexExitGraceKill(state) {
17369
+ if (state.exitGraceKillTimer) return;
17370
+ const child = state.process;
17371
+ if (!child) return;
17372
+ const timer = setTimeout(() => {
17373
+ if (child.exitCode !== null || child.killed) return;
17374
+ console.warn(
17375
+ "[codex_app_server] Process did not exit after turn completion; killing process group"
17376
+ );
17377
+ const pid = child.pid;
17378
+ if (pid) {
17379
+ try {
17380
+ process.kill(-pid, "SIGTERM");
17381
+ return;
17382
+ } catch {
17383
+ }
17384
+ }
17385
+ try {
17386
+ child.kill("SIGTERM");
17387
+ } catch {
17388
+ }
17389
+ }, CODEX_EXIT_GRACE_MS);
17390
+ timer.unref?.();
17391
+ state.exitGraceKillTimer = timer;
17392
+ }
17393
+ function disarmCodexExitGraceKill(state) {
17394
+ if (!state.exitGraceKillTimer) return;
17395
+ clearTimeout(state.exitGraceKillTimer);
17396
+ state.exitGraceKillTimer = void 0;
17397
+ }
17398
+ function trackCodexStreamedToolId(state, toolId) {
17399
+ if (!state.codexStreamedToolIds) state.codexStreamedToolIds = /* @__PURE__ */ new Set();
17400
+ if (state.codexStreamedToolIds.has(toolId)) return false;
17401
+ state.codexStreamedToolIds.add(toolId);
17402
+ return true;
17403
+ }
17404
+ function buildCodexMcpToolName(item) {
17405
+ const server = typeof item.server === "string" ? item.server.replace(/_+$/, "") : "";
17406
+ const tool = typeof item.tool === "string" ? item.tool : typeof item.tool_name === "string" ? item.tool_name : "";
17407
+ if (server && tool) return `mcp__${server}__${tool}`;
17408
+ return tool || "MCP Tool";
17409
+ }
17410
+ function emitCodexFileChanges(context, state, itemId, changes) {
17411
+ changes.forEach((change, index) => {
17412
+ if (!change || typeof change !== "object") return;
17413
+ const record = change;
17414
+ const filePath = typeof record.path === "string" ? record.path : "";
17415
+ if (!filePath) return;
17416
+ const kind = typeof record.kind === "string" ? record.kind : "update";
17417
+ const toolName = kind === "add" ? "write_file" : "edit_file";
17418
+ const toolId = changes.length > 1 ? `${itemId}:${index}` : itemId;
17419
+ trackCodexStreamedToolId(state, toolId);
17420
+ void context.presenter.onToolUse(toolName, { file_path: filePath, kind }, toolId);
17421
+ void context.presenter.onToolResult?.(toolId, `${kind} ${filePath}`);
17422
+ });
17423
+ }
17211
17424
  function handleCodexStructuredEvent(parsed, context, state) {
17212
17425
  const presenter = context.presenter;
17213
17426
  const type = typeof parsed.type === "string" ? parsed.type : "";
@@ -17235,6 +17448,7 @@ function handleCodexStructuredEvent(parsed, context, state) {
17235
17448
  case "thread.started":
17236
17449
  return true;
17237
17450
  case "turn.started":
17451
+ disarmCodexExitGraceKill(state);
17238
17452
  state.iterations += 1;
17239
17453
  return true;
17240
17454
  case "session_configured":
@@ -17243,36 +17457,112 @@ function handleCodexStructuredEvent(parsed, context, state) {
17243
17457
  }
17244
17458
  return true;
17245
17459
  case "task_started":
17460
+ disarmCodexExitGraceKill(state);
17246
17461
  state.iterations += 1;
17247
17462
  return true;
17248
17463
  case "item.started": {
17249
17464
  const item = typeof parsed.item === "object" && parsed.item !== null ? parsed.item : null;
17250
- if (!item || item.type !== "command_execution" || typeof item.id !== "string") return true;
17251
- const command = typeof item.command === "string" ? item.command : "";
17252
- void presenter.onToolUse("Bash", { command, cwd: context.cwd }, item.id);
17253
- return true;
17465
+ if (!item || typeof item.type !== "string" || typeof item.id !== "string") return true;
17466
+ switch (item.type) {
17467
+ case "command_execution": {
17468
+ if (trackCodexStreamedToolId(state, item.id)) {
17469
+ const command = typeof item.command === "string" ? item.command : "";
17470
+ void presenter.onToolUse("Bash", { command, cwd: context.cwd }, item.id);
17471
+ }
17472
+ return true;
17473
+ }
17474
+ case "mcp_tool_call": {
17475
+ if (trackCodexStreamedToolId(state, item.id)) {
17476
+ const input = typeof item.arguments === "object" && item.arguments !== null ? item.arguments : {};
17477
+ void presenter.onToolUse(buildCodexMcpToolName(item), input, item.id);
17478
+ }
17479
+ return true;
17480
+ }
17481
+ case "web_search": {
17482
+ if (trackCodexStreamedToolId(state, item.id)) {
17483
+ const query = typeof item.query === "string" ? item.query : "";
17484
+ void presenter.onToolUse("web_search", { query }, item.id);
17485
+ }
17486
+ return true;
17487
+ }
17488
+ // Text-bearing and patch/todo items carry no useful live-start payload; they
17489
+ // are surfaced on item.completed. Recognized (do not count as unhandled).
17490
+ case "reasoning":
17491
+ case "agent_message":
17492
+ case "file_change":
17493
+ case "todo_list":
17494
+ return true;
17495
+ default:
17496
+ return false;
17497
+ }
17254
17498
  }
17255
17499
  case "item.completed": {
17256
17500
  const item = typeof parsed.item === "object" && parsed.item !== null ? parsed.item : null;
17257
17501
  if (!item || typeof item.type !== "string") return true;
17258
- if (item.type === "reasoning" && typeof item.text === "string") {
17259
- void presenter.onThinking(item.text);
17260
- return true;
17261
- }
17262
- if (item.type === "agent_message" && typeof item.text === "string") {
17263
- state.summary += `${item.text}
17502
+ switch (item.type) {
17503
+ case "reasoning": {
17504
+ if (typeof item.text === "string") void presenter.onThinking(item.text);
17505
+ return true;
17506
+ }
17507
+ case "agent_message": {
17508
+ if (typeof item.text === "string") {
17509
+ state.summary += `${item.text}
17264
17510
  `;
17265
- void presenter.onAssistantText(item.text);
17266
- return true;
17267
- }
17268
- if (item.type === "command_execution" && typeof item.id === "string") {
17269
- const output = typeof item.aggregated_output === "string" ? item.aggregated_output : "";
17270
- const itemExitCode = typeof item.exit_code === "number" ? item.exit_code : null;
17271
- const resultText = output.trim().length > 0 ? output : itemExitCode === null ? "" : `Exit code: ${itemExitCode}`;
17272
- void presenter.onToolResult?.(item.id, resultText);
17273
- return true;
17511
+ void presenter.onAssistantText(item.text);
17512
+ }
17513
+ return true;
17514
+ }
17515
+ case "command_execution": {
17516
+ if (typeof item.id !== "string") return true;
17517
+ if (trackCodexStreamedToolId(state, item.id)) {
17518
+ const command = typeof item.command === "string" ? item.command : "";
17519
+ void presenter.onToolUse("Bash", { command, cwd: context.cwd }, item.id);
17520
+ }
17521
+ const output = typeof item.aggregated_output === "string" ? item.aggregated_output : "";
17522
+ const itemExitCode = typeof item.exit_code === "number" ? item.exit_code : null;
17523
+ const resultText = output.trim().length > 0 ? output : itemExitCode === null ? "" : `Exit code: ${itemExitCode}`;
17524
+ void presenter.onToolResult?.(
17525
+ item.id,
17526
+ resultText,
17527
+ itemExitCode != null && itemExitCode !== 0
17528
+ );
17529
+ return true;
17530
+ }
17531
+ case "mcp_tool_call": {
17532
+ if (typeof item.id !== "string") return true;
17533
+ if (trackCodexStreamedToolId(state, item.id)) {
17534
+ const input = typeof item.arguments === "object" && item.arguments !== null ? item.arguments : {};
17535
+ void presenter.onToolUse(buildCodexMcpToolName(item), input, item.id);
17536
+ }
17537
+ const isError = item.status === "failed" || item.error != null;
17538
+ const payload = item.result ?? item.output ?? item.error ?? {};
17539
+ const resultText = typeof payload === "string" ? payload : JSON.stringify(payload ?? {}, null, 2);
17540
+ void presenter.onToolResult?.(item.id, resultText, isError);
17541
+ return true;
17542
+ }
17543
+ case "file_change": {
17544
+ if (typeof item.id !== "string") return true;
17545
+ const changes = Array.isArray(item.changes) ? item.changes : [];
17546
+ emitCodexFileChanges(context, state, item.id, changes);
17547
+ return true;
17548
+ }
17549
+ case "web_search": {
17550
+ if (typeof item.id !== "string") return true;
17551
+ const query = typeof item.query === "string" ? item.query : "";
17552
+ if (trackCodexStreamedToolId(state, item.id)) {
17553
+ void presenter.onToolUse("web_search", { query }, item.id);
17554
+ }
17555
+ void presenter.onToolResult?.(item.id, query ? `Searched: ${query}` : "");
17556
+ return true;
17557
+ }
17558
+ case "todo_list": {
17559
+ const todos = Array.isArray(item.items) ? item.items : [];
17560
+ void presenter.onTodoWrite?.(todos);
17561
+ return true;
17562
+ }
17563
+ default:
17564
+ return false;
17274
17565
  }
17275
- return true;
17276
17566
  }
17277
17567
  case "agent_message_delta":
17278
17568
  case "agent_message_content_delta": {
@@ -17294,6 +17584,7 @@ function handleCodexStructuredEvent(parsed, context, state) {
17294
17584
  const toolId = typeof parsed.call_id === "string" ? parsed.call_id : `exec-${Date.now()}`;
17295
17585
  const command = Array.isArray(parsed.command) ? parsed.command.join(" ") : "";
17296
17586
  const cwd = typeof parsed.cwd === "string" ? parsed.cwd : context.cwd;
17587
+ trackCodexStreamedToolId(state, toolId);
17297
17588
  void presenter.onToolUse("Bash", { command, cwd }, toolId);
17298
17589
  return true;
17299
17590
  }
@@ -17307,6 +17598,7 @@ function handleCodexStructuredEvent(parsed, context, state) {
17307
17598
  const toolId = typeof parsed.call_id === "string" ? parsed.call_id : `mcp-${Date.now()}`;
17308
17599
  const invocation = typeof parsed.invocation === "object" && parsed.invocation !== null ? parsed.invocation : {};
17309
17600
  const tool = typeof invocation.tool_name === "string" ? invocation.tool_name : typeof invocation.tool === "string" ? invocation.tool : "MCP Tool";
17601
+ trackCodexStreamedToolId(state, toolId);
17310
17602
  void presenter.onToolUse(tool, invocation, toolId);
17311
17603
  return true;
17312
17604
  }
@@ -17344,6 +17636,8 @@ function handleCodexStructuredEvent(parsed, context, state) {
17344
17636
  case "task_complete": {
17345
17637
  const lastMessage = typeof parsed.last_agent_message === "string" ? parsed.last_agent_message : "";
17346
17638
  if (lastMessage.length > 0) state.summary = lastMessage;
17639
+ state.error = void 0;
17640
+ armCodexExitGraceKill(state);
17347
17641
  return true;
17348
17642
  }
17349
17643
  case "turn.completed": {
@@ -17365,6 +17659,8 @@ function handleCodexStructuredEvent(parsed, context, state) {
17365
17659
  cacheReadTokens: state.usage.cacheReadTokens,
17366
17660
  cacheCreationTokens: state.usage.cacheCreationTokens
17367
17661
  });
17662
+ state.error = void 0;
17663
+ armCodexExitGraceKill(state);
17368
17664
  return true;
17369
17665
  }
17370
17666
  case "error": {
@@ -17409,6 +17705,33 @@ function createCodexRuntimeBackend(command = "codex", defaultArgs = []) {
17409
17705
  context.config.images,
17410
17706
  context.cwd
17411
17707
  );
17708
+ const requestedResumeId = context.config.runtimeSessionId?.trim() || context.config.providerSessionId?.trim();
17709
+ let resumableSessionId = requestedResumeId;
17710
+ let resumeContextPrefix;
17711
+ if (requestedResumeId && !findCodexSessionLog(requestedResumeId, resolveCodexHome(context.env))) {
17712
+ resumableSessionId = void 0;
17713
+ resumeContextPrefix = context.config.resumeFallbackContext?.trim() || void 0;
17714
+ console.warn(
17715
+ `[codex_app_server] No rollout log found for session ${requestedResumeId}; starting a fresh session instead of resuming`
17716
+ );
17717
+ logResumeObservability("agent_resume_fallback_used", {
17718
+ backendKind: "codex_app_server",
17719
+ reason: "rollout_log_missing",
17720
+ requestedResumeId,
17721
+ hadFallbackContext: Boolean(resumeContextPrefix)
17722
+ });
17723
+ emitSessionNotice(
17724
+ context.presenter,
17725
+ "session_resume_failed",
17726
+ resumeContextPrefix ? "The previous Codex session log was not found on this machine, so a fresh session was started with the recent conversation history re-included." : "The previous Codex session log was not found on this machine, so a fresh session was started. Earlier conversation context may be missing from this reply."
17727
+ );
17728
+ }
17729
+ if (resumableSessionId) {
17730
+ logResumeObservability("agent_resume_requested", {
17731
+ backendKind: "codex_app_server",
17732
+ requestedResumeId: resumableSessionId
17733
+ });
17734
+ }
17412
17735
  try {
17413
17736
  return await createGenericCliBackend({
17414
17737
  kind: "codex_app_server",
@@ -17417,15 +17740,10 @@ function createCodexRuntimeBackend(command = "codex", defaultArgs = []) {
17417
17740
  args: [],
17418
17741
  startupTimeoutMs: CODEX_STARTUP_TIMEOUT_MS,
17419
17742
  buildArgs: (ctx) => {
17420
- const resumeId = ctx.config.runtimeSessionId?.trim() || ctx.config.providerSessionId?.trim();
17743
+ const resumeId = resumableSessionId;
17421
17744
  const modelArgs = ctx.config.selectedModel?.trim() ? ["--model", ctx.config.selectedModel.trim()] : [];
17422
17745
  const effortArgs = buildCodexEffortArgs(ctx.config.selectedEffortLevel);
17423
- const permissionArgs = [
17424
- "--ask-for-approval",
17425
- "never",
17426
- "--sandbox",
17427
- "danger-full-access"
17428
- ];
17746
+ const permissionArgs = getCodexPermissionArgs(ctx.config);
17429
17747
  const imageArgs = imagePaths.flatMap((p) => ["--image", p]);
17430
17748
  const baseArgs = resumeId ? [
17431
17749
  ...permissionArgs,
@@ -17452,7 +17770,10 @@ function createCodexRuntimeBackend(command = "codex", defaultArgs = []) {
17452
17770
  },
17453
17771
  promptViaStdin: true,
17454
17772
  augmentPrompt: (ctx) => {
17455
- const basePrompt = buildPromptWithSystem(ctx.config, ctx.promptText);
17773
+ const promptWithContext = resumeContextPrefix ? `${resumeContextPrefix}
17774
+
17775
+ ${ctx.promptText}` : ctx.promptText;
17776
+ const basePrompt = buildPromptWithSystem(ctx.config, promptWithContext);
17456
17777
  return ctx.config.mode === "plan" ? buildPlanModePrefix(basePrompt) : basePrompt;
17457
17778
  },
17458
17779
  parseStructuredLine: parseCodexStructuredLine,
@@ -17779,7 +18100,10 @@ function buildCursorAgentModelArg(modelId, options) {
17779
18100
  return `${parsed.baseId}[${overrides.join(",")}]`;
17780
18101
  }
17781
18102
  function isCursorAgentCliModelId(model) {
17782
- return model === "auto" || /^composer-/.test(model) || /^gpt-\d/.test(model) || /^claude-\d/.test(model) || /^gemini-\d/.test(model) || /^grok-\d/.test(model) || /^kimi-/.test(model);
18103
+ return model === "auto" || /^composer-/.test(model) || /^gpt-/.test(model) || /^claude-/.test(model) || /^gemini-/.test(model) || /^grok-/.test(model) || /^kimi-/.test(model);
18104
+ }
18105
+ function isDispatchableCursorModel(model) {
18106
+ return isCursorAgentCliModelId(model) || Boolean(findModelDef("cursor_agent_cli", model));
17783
18107
  }
17784
18108
  function shouldForceCursorAgent(config) {
17785
18109
  if (shouldUseReadOnlyRuntimePermissions(config)) return false;
@@ -17797,6 +18121,21 @@ function createCursorAgentCliBackend(command = "cursor-agent", defaultArgs = [])
17797
18121
  kind: "cursor_agent_cli",
17798
18122
  supportTier: "structured",
17799
18123
  async run(context) {
18124
+ const selectedModel = context.config.selectedModel?.trim();
18125
+ if (selectedModel && !isDispatchableCursorModel(selectedModel)) {
18126
+ const message = `Model "${selectedModel}" is not supported by cursor-agent. Pick a cursor-agent model (or "auto") and send your message again.`;
18127
+ return {
18128
+ success: false,
18129
+ summary: message,
18130
+ filesModified: [],
18131
+ planFilesCreated: [],
18132
+ iterations: 0,
18133
+ error: message,
18134
+ errorKind: "model_mismatch",
18135
+ backendKind: "cursor_agent_cli",
18136
+ supportTier: "structured"
18137
+ };
18138
+ }
17800
18139
  const { files: imageFiles, cleanup } = writeImagesToTempFiles(
17801
18140
  context.config.images,
17802
18141
  context.cwd
@@ -17826,10 +18165,18 @@ function createCursorAgentCliBackend(command = "cursor-agent", defaultArgs = [])
17826
18165
  args.push("--mode", "plan");
17827
18166
  } else if (ctx.config.mode === "ask") {
17828
18167
  args.push("--mode", "ask");
18168
+ } else if (shouldUseReadOnlyRuntimePermissions(ctx.config)) {
18169
+ args.push("--mode", "plan");
18170
+ }
18171
+ if (resumeId) {
18172
+ args.push("--resume", resumeId);
18173
+ logResumeObservability("agent_resume_requested", {
18174
+ backendKind: "cursor_agent_cli",
18175
+ requestedResumeId: resumeId
18176
+ });
17829
18177
  }
17830
- if (resumeId) args.push("--resume", resumeId);
17831
18178
  const model = ctx.config.selectedModel?.trim();
17832
- if (model && isCursorAgentCliModelId(model)) {
18179
+ if (model && isDispatchableCursorModel(model)) {
17833
18180
  const modelDef = findModelDef("cursor_agent_cli", model);
17834
18181
  args.push(
17835
18182
  "--model",
@@ -18292,9 +18639,9 @@ function buildGrokAgentArgs(config, defaultArgs = []) {
18292
18639
  return args;
18293
18640
  }
18294
18641
  var DEFAULT_SKILL_ROOTS = [
18295
- (0, import_path5.join)((0, import_os5.homedir)(), ".agents", "skills"),
18296
- (0, import_path5.join)((0, import_os5.homedir)(), ".claude", "skills"),
18297
- (0, import_path5.join)((0, import_os5.homedir)(), ".config", "opencode", "skills")
18642
+ (0, import_path6.join)((0, import_os6.homedir)(), ".agents", "skills"),
18643
+ (0, import_path6.join)((0, import_os6.homedir)(), ".claude", "skills"),
18644
+ (0, import_path6.join)((0, import_os6.homedir)(), ".config", "opencode", "skills")
18298
18645
  ];
18299
18646
  function parseFrontmatter(content) {
18300
18647
  if (!content.startsWith("---\n")) return {};
@@ -18310,12 +18657,12 @@ function parseFrontmatter(content) {
18310
18657
  return result;
18311
18658
  }
18312
18659
  function listSkillFiles(root) {
18313
- if (!(0, import_fs5.existsSync)(root)) return [];
18660
+ if (!(0, import_fs6.existsSync)(root)) return [];
18314
18661
  const files = [];
18315
- for (const entry of (0, import_fs5.readdirSync)(root, { withFileTypes: true })) {
18662
+ for (const entry of (0, import_fs6.readdirSync)(root, { withFileTypes: true })) {
18316
18663
  if (!entry.isDirectory()) continue;
18317
- const skillPath = (0, import_path5.join)(root, entry.name, "SKILL.md");
18318
- if ((0, import_fs5.existsSync)(skillPath)) files.push(skillPath);
18664
+ const skillPath = (0, import_path6.join)(root, entry.name, "SKILL.md");
18665
+ if ((0, import_fs6.existsSync)(skillPath)) files.push(skillPath);
18319
18666
  }
18320
18667
  return files;
18321
18668
  }
@@ -18324,7 +18671,7 @@ function loadInstalledSkills(roots = DEFAULT_SKILL_ROOTS) {
18324
18671
  for (const root of roots) {
18325
18672
  for (const skillPath of listSkillFiles(root)) {
18326
18673
  try {
18327
- const frontmatter = parseFrontmatter((0, import_fs5.readFileSync)(skillPath, "utf8"));
18674
+ const frontmatter = parseFrontmatter((0, import_fs6.readFileSync)(skillPath, "utf8"));
18328
18675
  const name = frontmatter.name;
18329
18676
  const description = frontmatter.description;
18330
18677
  if (!name || !description || skills.has(name)) continue;
@@ -18757,7 +19104,31 @@ function createSupatestCliBackend(command = "supatest", defaultArgs = []) {
18757
19104
  "--permission-mode",
18758
19105
  getClaudePermissionMode(context.config)
18759
19106
  ];
18760
- const resumeId = context.config.runtimeSessionId?.trim() || context.config.providerSessionId?.trim();
19107
+ const requestedResumeId = context.config.runtimeSessionId?.trim() || context.config.providerSessionId?.trim();
19108
+ let resumeId = requestedResumeId;
19109
+ let resumeContextPrefix;
19110
+ if (requestedResumeId && !claudeSessionLogExists({
19111
+ sessionId: requestedResumeId,
19112
+ cwd: context.cwd,
19113
+ home: resolveClaudeHome(context.env, "supatest")
19114
+ })) {
19115
+ resumeId = void 0;
19116
+ resumeContextPrefix = context.config.resumeFallbackContext?.trim() || void 0;
19117
+ console.warn(
19118
+ `[supatest_cli] No session log for ${requestedResumeId} under cwd ${context.cwd}; starting a fresh session instead of resuming`
19119
+ );
19120
+ logResumeObservability("agent_resume_fallback_used", {
19121
+ backendKind: "supatest_cli",
19122
+ reason: "session_log_missing_for_cwd",
19123
+ requestedResumeId,
19124
+ hadFallbackContext: Boolean(resumeContextPrefix)
19125
+ });
19126
+ emitSessionNotice(
19127
+ context.presenter,
19128
+ "session_resume_failed",
19129
+ resumeContextPrefix ? "The previous session was created in a different working directory and could not be resumed here, so a fresh session was started with the recent conversation history re-included." : "The previous session was created in a different working directory and could not be resumed here, so a fresh session was started. Earlier conversation context may be missing from this reply."
19130
+ );
19131
+ }
18761
19132
  if (!resumeId) {
18762
19133
  if (context.config.systemPrompt?.trim()) {
18763
19134
  args.push("--system-prompt", context.config.systemPrompt.trim());
@@ -18773,6 +19144,10 @@ function createSupatestCliBackend(command = "supatest", defaultArgs = []) {
18773
19144
  if (resumeId) {
18774
19145
  args.push("--resume", resumeId);
18775
19146
  console.info("[supatest_cli] Resuming session", { resumeId });
19147
+ logResumeObservability("agent_resume_requested", {
19148
+ backendKind: "supatest_cli",
19149
+ requestedResumeId: resumeId
19150
+ });
18776
19151
  }
18777
19152
  args.push(...defaultArgs);
18778
19153
  return createGenericCliBackend({
@@ -18791,7 +19166,10 @@ function createSupatestCliBackend(command = "supatest", defaultArgs = []) {
18791
19166
  });
18792
19167
  }
18793
19168
  }
18794
- const promptText = ctx.config.mode === "plan" ? buildPlanModePrefix(buildPromptWithSystem(ctx.config, ctx.promptText)) : buildPromptWithSystem(ctx.config, ctx.promptText);
19169
+ const basePromptText = resumeContextPrefix ? `${resumeContextPrefix}
19170
+
19171
+ ${ctx.promptText}` : ctx.promptText;
19172
+ const promptText = ctx.config.mode === "plan" ? buildPlanModePrefix(buildPromptWithSystem(ctx.config, basePromptText)) : buildPromptWithSystem(ctx.config, basePromptText);
18795
19173
  if (promptText.trim()) {
18796
19174
  contentBlocks.push({ type: "text", text: promptText });
18797
19175
  }
@@ -18821,40 +19199,40 @@ function parseSkillInvocation(text) {
18821
19199
  function candidateRoots(cwd) {
18822
19200
  const roots = [];
18823
19201
  if (cwd) {
18824
- roots.push((0, import_path6.join)(cwd, ".agents", "skills"));
18825
- roots.push((0, import_path6.join)(cwd, ".claude", "skills"));
18826
- roots.push((0, import_path6.join)(cwd, ".codex", "skills"));
18827
- if ((0, import_fs6.existsSync)(cwd)) {
19202
+ roots.push((0, import_path7.join)(cwd, ".agents", "skills"));
19203
+ roots.push((0, import_path7.join)(cwd, ".claude", "skills"));
19204
+ roots.push((0, import_path7.join)(cwd, ".codex", "skills"));
19205
+ if ((0, import_fs7.existsSync)(cwd)) {
18828
19206
  let entries = [];
18829
19207
  try {
18830
- entries = (0, import_fs6.readdirSync)(cwd, { withFileTypes: true });
19208
+ entries = (0, import_fs7.readdirSync)(cwd, { withFileTypes: true });
18831
19209
  } catch {
18832
19210
  }
18833
19211
  for (const entry of entries) {
18834
19212
  if (!entry.isDirectory()) continue;
18835
19213
  if (entry.name.startsWith(".")) continue;
18836
- const child = (0, import_path6.join)(cwd, entry.name);
18837
- roots.push((0, import_path6.join)(child, ".agents", "skills"));
18838
- roots.push((0, import_path6.join)(child, ".claude", "skills"));
18839
- roots.push((0, import_path6.join)(child, ".codex", "skills"));
19214
+ const child = (0, import_path7.join)(cwd, entry.name);
19215
+ roots.push((0, import_path7.join)(child, ".agents", "skills"));
19216
+ roots.push((0, import_path7.join)(child, ".claude", "skills"));
19217
+ roots.push((0, import_path7.join)(child, ".codex", "skills"));
18840
19218
  }
18841
19219
  }
18842
19220
  }
18843
- const home = (0, import_os6.homedir)();
18844
- roots.push((0, import_path6.join)(home, ".agents", "skills"));
18845
- roots.push((0, import_path6.join)(home, ".claude", "skills"));
18846
- roots.push((0, import_path6.join)(home, ".codex", "skills"));
19221
+ const home = (0, import_os7.homedir)();
19222
+ roots.push((0, import_path7.join)(home, ".agents", "skills"));
19223
+ roots.push((0, import_path7.join)(home, ".claude", "skills"));
19224
+ roots.push((0, import_path7.join)(home, ".codex", "skills"));
18847
19225
  return Array.from(new Set(roots));
18848
19226
  }
18849
19227
  function resolveSkillInvocation(text, cwd) {
18850
19228
  const parsed = parseSkillInvocation(text);
18851
19229
  if (!parsed) return null;
18852
19230
  for (const root of candidateRoots(cwd)) {
18853
- const skillPath = (0, import_path6.join)(root, parsed.command, "SKILL.md");
18854
- if (!(0, import_fs6.existsSync)(skillPath)) continue;
19231
+ const skillPath = (0, import_path7.join)(root, parsed.command, "SKILL.md");
19232
+ if (!(0, import_fs7.existsSync)(skillPath)) continue;
18855
19233
  let content;
18856
19234
  try {
18857
- content = (0, import_fs6.readFileSync)(skillPath, "utf-8");
19235
+ content = (0, import_fs7.readFileSync)(skillPath, "utf-8");
18858
19236
  } catch {
18859
19237
  continue;
18860
19238
  }
@@ -19035,11 +19413,19 @@ var BaseMachineAgent = class _BaseMachineAgent {
19035
19413
  async runBackendWithActivityHeartbeat(backend, context) {
19036
19414
  const intervalMs = this.getActivityHeartbeatIntervalMs();
19037
19415
  let heartbeat = null;
19416
+ let livenessProbe = null;
19417
+ context.registerLivenessProbe = (probe) => {
19418
+ livenessProbe = probe;
19419
+ };
19420
+ const emitHeartbeat = () => {
19421
+ const evidence = livenessProbe?.();
19422
+ void this.presenter.onActivity?.("background_work", evidence);
19423
+ };
19038
19424
  if (this.presenter.onActivity && intervalMs > 0) {
19039
- void this.presenter.onActivity("background_work");
19425
+ emitHeartbeat();
19040
19426
  heartbeat = setInterval(() => {
19041
19427
  if (!context.abortController.signal.aborted) {
19042
- void this.presenter.onActivity?.("background_work");
19428
+ emitHeartbeat();
19043
19429
  }
19044
19430
  }, intervalMs);
19045
19431
  }
@@ -19088,10 +19474,6 @@ var BaseMachineAgent = class _BaseMachineAgent {
19088
19474
  if (agentPrompt) systemPromptParts.push(agentPrompt);
19089
19475
  const modeHint = getAlanModePrompt(config.mode);
19090
19476
  if (modeHint) systemPromptParts.push(modeHint);
19091
- systemPromptParts.push(ALAN_ASK_USER_PROMPT);
19092
- if (backendKind === "cursor_agent_cli") {
19093
- systemPromptParts.push(ALAN_CURSOR_ASK_USER_OVERLAY);
19094
- }
19095
19477
  const operatorSystemPrompt = config.systemPrompt?.trim();
19096
19478
  if (operatorSystemPrompt) systemPromptParts.push(operatorSystemPrompt);
19097
19479
  const workingDir = config.worktreePath || safeProjectPath;
@@ -19164,6 +19546,7 @@ Prefer relative paths and keep your work scoped to this project.`
19164
19546
  };
19165
19547
  if (lastResult.success || !lastResult.error) break;
19166
19548
  if (this.abortController?.signal.aborted) break;
19549
+ if (lastResult.errorKind === "model_mismatch") break;
19167
19550
  const hadResumeId = runtimeConfig.runtimeSessionId || runtimeConfig.providerSessionId;
19168
19551
  if (hadResumeId && !isTransientError(lastResult.error)) {
19169
19552
  const overrideResult = await this.onSessionResumeFailure(runtimeConfig, lastResult);
@@ -19175,10 +19558,25 @@ Prefer relative paths and keep your work scoped to this project.`
19175
19558
  "[base-machine-agent] Resume failed, retrying without session ID:",
19176
19559
  lastResult.error
19177
19560
  );
19561
+ const fallbackContext = runtimeConfig.resumeFallbackContext?.trim();
19562
+ logResumeObservability("agent_resume_fallback_used", {
19563
+ backendKind: backend.kind,
19564
+ reason: "retry_fresh_after_resume_failure",
19565
+ errorKind: lastResult.errorKind,
19566
+ hadFallbackContext: Boolean(fallbackContext)
19567
+ });
19568
+ emitSessionNotice(
19569
+ this.presenter,
19570
+ "session_resume_failed",
19571
+ fallbackContext ? "Could not resume the previous session; started a fresh session with the recent conversation history re-included." : "Could not resume the previous session; retrying with a fresh session. Earlier conversation context may be missing from this reply."
19572
+ );
19178
19573
  runtimeConfig = {
19179
19574
  ...runtimeConfig,
19180
19575
  runtimeSessionId: void 0,
19181
- providerSessionId: void 0
19576
+ providerSessionId: void 0,
19577
+ ...fallbackContext ? { task: `${fallbackContext}
19578
+
19579
+ ${runtimeConfig.task}` } : {}
19182
19580
  };
19183
19581
  const retryResult = await this.runBackendWithActivityHeartbeat(backend, {
19184
19582
  presenter: this.presenter,
@@ -19186,7 +19584,7 @@ Prefer relative paths and keep your work scoped to this project.`
19186
19584
  abortController: this.abortController,
19187
19585
  cwd,
19188
19586
  env,
19189
- promptText,
19587
+ promptText: fallbackContext ? this.buildPromptText(runtimeConfig) : promptText,
19190
19588
  onProcessSpawned: this.getOnProcessSpawned()
19191
19589
  });
19192
19590
  lastResult = {
@@ -19382,14 +19780,14 @@ function normalizePath(candidate, cwd) {
19382
19780
  } catch {
19383
19781
  }
19384
19782
  if (path.startsWith("~/")) return null;
19385
- if ((0, import_path7.isAbsolute)(path)) return path;
19386
- return cwd ? (0, import_path7.join)(cwd, path) : null;
19783
+ if ((0, import_path8.isAbsolute)(path)) return path;
19784
+ return cwd ? (0, import_path8.join)(cwd, path) : null;
19387
19785
  }
19388
19786
  function resolveToolCwd(cwd, toolInput) {
19389
19787
  if (!isRecord(toolInput)) return cwd;
19390
19788
  const raw = typeof toolInput.cwd === "string" ? toolInput.cwd : typeof toolInput.workdir === "string" ? toolInput.workdir : typeof toolInput.workingDirectory === "string" ? toolInput.workingDirectory : null;
19391
19789
  if (!raw) return cwd;
19392
- return (0, import_path7.isAbsolute)(raw) || !cwd ? raw : (0, import_path7.join)(cwd, raw);
19790
+ return (0, import_path8.isAbsolute)(raw) || !cwd ? raw : (0, import_path8.join)(cwd, raw);
19393
19791
  }
19394
19792
  function extractBrowserMediaPathHints(content, cwd, toolName, options = {}, kind = "video") {
19395
19793
  if (!isAgentBrowserMediaInvocation(toolName, content, options.toolInput, kind)) return [];
@@ -19432,19 +19830,19 @@ function extractGeneratedImagesFromToolResult(toolId, content, cwd, toolName, op
19432
19830
  const effectiveCwd = resolveToolCwd(cwd, options.toolInput);
19433
19831
  for (const candidate of extractPathCandidates(content, options)) {
19434
19832
  const path = normalizePath(candidate, effectiveCwd);
19435
- if (!path || seen.has(path) || !(0, import_fs7.existsSync)(path)) continue;
19833
+ if (!path || seen.has(path) || !(0, import_fs8.existsSync)(path)) continue;
19436
19834
  seen.add(path);
19437
- const ext = (0, import_path7.extname)(path).toLowerCase();
19835
+ const ext = (0, import_path8.extname)(path).toLowerCase();
19438
19836
  const mimeType = IMAGE_MIME_BY_EXT[ext];
19439
19837
  if (!mimeType) continue;
19440
- const stat = (0, import_fs7.statSync)(path);
19838
+ const stat = (0, import_fs8.statSync)(path);
19441
19839
  if (!stat.isFile() || stat.size <= 0 || stat.size > MAX_GENERATED_IMAGE_BYTES) continue;
19442
- const buffer = (0, import_fs7.readFileSync)(path);
19840
+ const buffer = (0, import_fs8.readFileSync)(path);
19443
19841
  const { width, height } = readImageDimensions2(buffer, mimeType);
19444
19842
  images.push({
19445
19843
  type: "generated_image",
19446
19844
  id: (0, import_crypto3.randomUUID)(),
19447
- filename: (0, import_path7.basename)(path),
19845
+ filename: (0, import_path8.basename)(path),
19448
19846
  mimeType,
19449
19847
  size: buffer.length,
19450
19848
  width,
@@ -19471,20 +19869,20 @@ function extractSessionMediaFromToolResult(toolId, content, cwd, toolName, optio
19471
19869
  const mimeByExt = kind === "video" ? VIDEO_MIME_BY_EXT : IMAGE_MIME_BY_EXT;
19472
19870
  for (const candidate of extractPathCandidates(content, options, extensionsPattern)) {
19473
19871
  const path = normalizePath(candidate, effectiveCwd);
19474
- if (!path || seen.has(path) || !(0, import_fs7.existsSync)(path)) continue;
19872
+ if (!path || seen.has(path) || !(0, import_fs8.existsSync)(path)) continue;
19475
19873
  seen.add(path);
19476
- const ext = (0, import_path7.extname)(path).toLowerCase();
19874
+ const ext = (0, import_path8.extname)(path).toLowerCase();
19477
19875
  const mimeType = mimeByExt[ext];
19478
19876
  if (!mimeType) continue;
19479
- const stat = (0, import_fs7.statSync)(path);
19877
+ const stat = (0, import_fs8.statSync)(path);
19480
19878
  if (!stat.isFile() || stat.size <= 0 || stat.size > maxBytes) continue;
19481
- const buffer = (0, import_fs7.readFileSync)(path);
19879
+ const buffer = (0, import_fs8.readFileSync)(path);
19482
19880
  const dimensions = kind === "image" ? readImageDimensions2(buffer, mimeType) : null;
19483
19881
  media.push({
19484
19882
  type: "session_media",
19485
19883
  kind,
19486
19884
  id: (0, import_crypto3.randomUUID)(),
19487
- filename: (0, import_path7.basename)(path),
19885
+ filename: (0, import_path8.basename)(path),
19488
19886
  mimeType,
19489
19887
  size: buffer.length,
19490
19888
  width: dimensions?.width,
@@ -19648,6 +20046,10 @@ var CoreAgent = class _CoreAgent extends BaseMachineAgent {
19648
20046
  if (!this.childProcess) return false;
19649
20047
  const stdin = this.childProcess.stdin;
19650
20048
  if (!stdin || stdin.destroyed) return false;
20049
+ if (this.presenter.hasKnownToolUse && !this.presenter.hasKnownToolUse(toolId)) {
20050
+ console.warn(`[CoreAgent] Ignoring tool_response for unknown tool ${toolId}`);
20051
+ return false;
20052
+ }
19651
20053
  try {
19652
20054
  let message;
19653
20055
  if (this.presenter.pendingAskUserToolIds?.has(toolId)) {
@@ -19902,6 +20304,9 @@ function decodeKey(encodedKey) {
19902
20304
  function isTerminal(event) {
19903
20305
  return TERMINAL_EVENT_TYPES.has(event.type);
19904
20306
  }
20307
+ function isBackgroundHeartbeat(event) {
20308
+ return event.type === "activity" && event.status === "background_work";
20309
+ }
19905
20310
  function truncateUtf8(value2, maxBytes) {
19906
20311
  const encoded = Buffer.from(value2, "utf8");
19907
20312
  if (encoded.length <= maxBytes) return value2;
@@ -20050,6 +20455,23 @@ var EncryptedEventOutbox = class {
20050
20455
  isDroppable(entry) {
20051
20456
  return entry.eventId !== this.inFlightEventId && entry.kind !== "condensation_marker" && !isTerminal(entry.event);
20052
20457
  }
20458
+ /**
20459
+ * Pick the next droppable entry to evict. Heartbeats are always evicted
20460
+ * first (oldest heartbeat first) since they carry no transcript content;
20461
+ * only once none remain do we fall back to the oldest droppable entry of
20462
+ * any kind. Without this, oldest-first eviction treats a background_work
20463
+ * heartbeat exactly like real transcript content, so a long offline stretch
20464
+ * (heartbeats every 10s, never removed by an ack since nothing is
20465
+ * connected) evicts genuine early events — including `start` — ahead of
20466
+ * the heartbeats that caused the overflow.
20467
+ */
20468
+ pickDropCandidate(matches) {
20469
+ const heartbeat = this.entries.find(
20470
+ (entry) => matches(entry) && this.isDroppable(entry) && isBackgroundHeartbeat(entry.event)
20471
+ );
20472
+ if (heartbeat) return heartbeat;
20473
+ return this.entries.find((entry) => matches(entry) && this.isDroppable(entry));
20474
+ }
20053
20475
  ensureCondensationMarker(runId, conversationId) {
20054
20476
  if (this.entries.some((entry) => entry.runId === runId && entry.kind === "condensation_marker")) {
20055
20477
  return;
@@ -20077,7 +20499,7 @@ var EncryptedEventOutbox = class {
20077
20499
  const runIds = preferredRunId ? [preferredRunId] : [...new Set(this.entries.map((entry) => entry.runId))];
20078
20500
  for (const runId of runIds) {
20079
20501
  while (this.entries.filter((entry) => entry.runId === runId && this.isDroppable(entry)).length > this.maxIntermediateEventsPerRun) {
20080
- const drop = this.entries.find((entry) => entry.runId === runId && this.isDroppable(entry));
20502
+ const drop = this.pickDropCandidate((entry) => entry.runId === runId);
20081
20503
  if (!drop) break;
20082
20504
  this.entries = this.entries.filter((entry) => entry.eventId !== drop.eventId);
20083
20505
  lossCountByRunId.set(runId, (lossCountByRunId.get(runId) ?? 0) + 1);
@@ -20085,7 +20507,7 @@ var EncryptedEventOutbox = class {
20085
20507
  }
20086
20508
  }
20087
20509
  while (encryptedFileSize(this.entries) > this.maxEncryptedBytes) {
20088
- const drop = this.entries.find((entry) => this.isDroppable(entry));
20510
+ const drop = this.pickDropCandidate(() => true);
20089
20511
  if (drop) {
20090
20512
  this.entries = this.entries.filter((entry) => entry.eventId !== drop.eventId);
20091
20513
  lossCountByRunId.set(drop.runId, (lossCountByRunId.get(drop.runId) ?? 0) + 1);
@@ -20104,7 +20526,7 @@ var EncryptedEventOutbox = class {
20104
20526
  this.ensureCondensationMarker(runId, lossConversationByRunId.get(runId));
20105
20527
  }
20106
20528
  while (encryptedFileSize(this.entries) > this.maxEncryptedBytes) {
20107
- const drop = this.entries.find((entry) => this.isDroppable(entry));
20529
+ const drop = this.pickDropCandidate(() => true);
20108
20530
  if (!drop) break;
20109
20531
  this.entries = this.entries.filter((entry) => entry.eventId !== drop.eventId);
20110
20532
  lossCountByRunId.set(drop.runId, (lossCountByRunId.get(drop.runId) ?? 0) + 1);
@@ -21029,6 +21451,10 @@ function assertValidExistingToml(path, content) {
21029
21451
  `Existing config is malformed; no changes were applied. Review the backup at ${backupPath}`
21030
21452
  );
21031
21453
  }
21454
+ var CODEX_ALAN_TOML_SECTION = "mcp_servers.alan";
21455
+ function isCodexAlanTomlSection(section) {
21456
+ return section === CODEX_ALAN_TOML_SECTION || section.startsWith(`${CODEX_ALAN_TOML_SECTION}.`);
21457
+ }
21032
21458
  function mergeCodexAlanSection(path, existingContent, desiredContent) {
21033
21459
  if (existingContent === null || existingContent.trim() === "") return desiredContent;
21034
21460
  assertValidExistingToml(path, existingContent);
@@ -21038,7 +21464,7 @@ function mergeCodexAlanSection(path, existingContent, desiredContent) {
21038
21464
  for (const line of lines) {
21039
21465
  const section = line.trim().match(/^\[([^\]]+)]$/)?.[1];
21040
21466
  if (section) {
21041
- insideAlanSection = section === "mcp_servers.alan";
21467
+ insideAlanSection = isCodexAlanTomlSection(section);
21042
21468
  }
21043
21469
  if (!insideAlanSection) retained.push(line);
21044
21470
  }
@@ -21824,7 +22250,7 @@ var RunStartGate = class {
21824
22250
  };
21825
22251
 
21826
22252
  // src/version.ts
21827
- var AGENT_VERSION = "0.1.37";
22253
+ var AGENT_VERSION = "0.1.38";
21828
22254
 
21829
22255
  // src/workspace-relocation.ts
21830
22256
  var import_node_child_process3 = require("child_process");
@@ -23180,14 +23606,29 @@ var USER_INTERRUPTED_RESULT = {
23180
23606
  filesModified: [],
23181
23607
  planFilesCreated: [],
23182
23608
  iterations: 0,
23183
- error: "Interrupted by user"
23609
+ error: "Interrupted by user",
23610
+ errorKind: "user"
23184
23611
  };
23612
+ function buildAbortResult(reason) {
23613
+ if (reason === "user") return USER_INTERRUPTED_RESULT;
23614
+ const message = SYSTEM_ABORT_RUN_ERRORS[reason];
23615
+ return {
23616
+ success: false,
23617
+ summary: message,
23618
+ filesModified: [],
23619
+ planFilesCreated: [],
23620
+ iterations: 0,
23621
+ error: message,
23622
+ errorKind: reason
23623
+ };
23624
+ }
23185
23625
  function abortActiveAgent(entry, options) {
23186
23626
  const awaitingAsk = (entry.presenter.pendingAskUserToolIds?.size ?? 0) > 0;
23187
23627
  if (awaitingAsk && !options?.userInitiated) {
23188
23628
  return;
23189
23629
  }
23190
- entry.presenter.onComplete(USER_INTERRUPTED_RESULT);
23630
+ const reason = options?.reason ?? (options?.userInitiated ? "user" : "runner_stalled");
23631
+ entry.presenter.onComplete(buildAbortResult(reason));
23191
23632
  entry.agent.kill();
23192
23633
  }
23193
23634
  function isProviderProcessAlive(pid) {
@@ -23296,7 +23737,11 @@ function flushPendingDeliveryLostEvents(input) {
23296
23737
  if (input.pending.length === 0) return;
23297
23738
  for (const item of input.pending) {
23298
23739
  if (input.eventOutbox.hasTerminalEvent(item.runId)) continue;
23299
- if (!input.activeAgents.has(item.runId)) continue;
23740
+ const activeEntry = input.activeAgents.get(item.runId);
23741
+ if (activeEntry) {
23742
+ if (activeEntry.stage === "delivery_lost") activeEntry.stage = "running";
23743
+ continue;
23744
+ }
23300
23745
  input.eventOutbox.enqueue(item.conversationId, item.runId, {
23301
23746
  type: "session_error",
23302
23747
  error: DELIVERY_LOST_ERROR
@@ -23320,7 +23765,7 @@ function reconcileActiveRuns(input) {
23320
23765
  input.pushLog?.(`reconciled dead provider pid run=${runId} (awaiting ask, no interrupt)`);
23321
23766
  continue;
23322
23767
  }
23323
- abortActiveAgent(entry);
23768
+ abortActiveAgent(entry, { reason: staleByDeadPid ? "provider_exited" : "runner_stalled" });
23324
23769
  input.activeAgents.delete(runId);
23325
23770
  if (staleByDeadPid) {
23326
23771
  input.pushLog?.(`reconciled dead provider pid run=${runId}`);
@@ -23384,6 +23829,7 @@ var RuntimePresenter = class {
23384
23829
  emitEvent(event) {
23385
23830
  if (this.terminalFenced) return;
23386
23831
  this.touchRunnerActivity?.();
23832
+ if (isBackgroundHeartbeat(event) && !this.socket.connected) return;
23387
23833
  this.eventOutbox.enqueue(this.conversationId, this.runId, event);
23388
23834
  this.eventOutbox.flush(this.socket);
23389
23835
  }
@@ -23411,6 +23857,9 @@ var RuntimePresenter = class {
23411
23857
  onLog(message) {
23412
23858
  console.error(message);
23413
23859
  }
23860
+ onNotice(kind, message) {
23861
+ this.emitEvent({ type: "session_notice", kind, message, ts: Date.now() });
23862
+ }
23414
23863
  onAssistantText(text) {
23415
23864
  this.emitEvent({ type: "text", text, ts: Date.now() });
23416
23865
  }
@@ -23510,8 +23959,8 @@ var RuntimePresenter = class {
23510
23959
  onCheckpoint(id) {
23511
23960
  this.emitEvent({ type: "checkpoint", id });
23512
23961
  }
23513
- onActivity(status) {
23514
- this.emitEvent({ type: "activity", status, ts: Date.now() });
23962
+ onActivity(status, evidence) {
23963
+ this.emitEvent({ type: "activity", status, ts: Date.now(), ...evidence ? { evidence } : {} });
23515
23964
  }
23516
23965
  onOpenBrowserTab(browserId, url2) {
23517
23966
  this.emitEvent({ type: "open_browser_tab", browserId, ...url2 ? { url: url2 } : {} });
@@ -24297,16 +24746,22 @@ async function startDaemon(args) {
24297
24746
  flushWorkspaceRelocations();
24298
24747
  }
24299
24748
  }
24300
- if (workspaceReadiness.ready && expectedRepoUrls.length > 0 && !isAlanDefaultWorkspace(cwd) && !workspaceMatchesExpectedRepositories({
24301
- workspacePath: cwd,
24302
- expectedRepoUrls
24303
- })) {
24304
- socket.emit("agent.rejected", {
24305
- runId: payload.runId,
24306
- message: "This folder belongs to a different repository. Choose the correct project folder and try again.",
24307
- code: "workspace_repository_mismatch"
24308
- });
24309
- return;
24749
+ if (workspaceReadiness.ready && expectedRepoUrls.length > 0 && !isAlanDefaultWorkspace(cwd)) {
24750
+ const worktreePaths = payload.worktreeInfo?.allWorktrees?.map((worktree) => worktree.worktreePath).filter((worktreePath) => Boolean(worktreePath));
24751
+ const repositoryMismatch = worktreePaths && worktreePaths.length > 0 ? worktreePaths.some(
24752
+ (worktreePath) => !workspaceMatchesExpectedRepositories({
24753
+ workspacePath: worktreePath,
24754
+ expectedRepoUrls
24755
+ })
24756
+ ) : !workspaceMatchesExpectedRepositories({ workspacePath: cwd, expectedRepoUrls });
24757
+ if (repositoryMismatch) {
24758
+ socket.emit("agent.rejected", {
24759
+ runId: payload.runId,
24760
+ message: "This folder belongs to a different repository. Choose the correct project folder and try again.",
24761
+ code: "workspace_repository_mismatch"
24762
+ });
24763
+ return;
24764
+ }
24310
24765
  }
24311
24766
  if (workspaceRequired) workspaceAccessTracker.track(cwd, workspaceReadiness);
24312
24767
  const runnerSummary = summarizeRunnerStatus({
@@ -24604,6 +25059,7 @@ async function startDaemon(args) {
24604
25059
  selectedContextWindow: payload.selectedContextWindow ?? null,
24605
25060
  selectedEffortLevel: payload.selectedEffortLevel ?? null,
24606
25061
  providerSessionId: payload.providerSessionId,
25062
+ resumeFallbackContext: payload.resumeFallbackContext,
24607
25063
  taskMeta: payload.taskMeta,
24608
25064
  prMeta: payload.prMeta,
24609
25065
  conversationId: payload.conversationId,
@@ -24640,11 +25096,13 @@ async function startDaemon(args) {
24640
25096
  if (!payload?.runId) return;
24641
25097
  const entry = activeAgents.get(payload.runId);
24642
25098
  if (!entry) return;
24643
- abortActiveAgent(entry, { userInitiated: true });
25099
+ const reason = payload.reason === "watchdog_timeout" ? "watchdog_timeout" : "user";
25100
+ abortActiveAgent(entry, { userInitiated: true, reason });
24644
25101
  activeAgents.delete(payload.runId);
24645
- pushLog(`aborted run=${payload.runId}`);
25102
+ pushLog(`aborted run=${payload.runId} reason=${reason}`);
24646
25103
  console.info("[alan-agent] Agent run aborted", {
24647
25104
  runId: payload.runId,
25105
+ reason,
24648
25106
  agentVersion: AGENT_VERSION
24649
25107
  });
24650
25108
  });
@@ -24652,7 +25110,9 @@ async function startDaemon(args) {
24652
25110
  "agent.tool_response",
24653
25111
  (payload) => {
24654
25112
  if (!payload?.toolId || typeof payload.response !== "string") return;
24655
- const entries = payload.runId ? [[payload.runId, activeAgents.get(payload.runId)]] : [...activeAgents.entries()];
25113
+ const entries = payload.runId ? [[payload.runId, activeAgents.get(payload.runId)]] : payload.conversationId ? [...activeAgents.entries()].filter(
25114
+ ([, entry]) => entry.conversationId === payload.conversationId
25115
+ ) : [...activeAgents.entries()];
24656
25116
  const delivered = entries.some(([runId, entry]) => {
24657
25117
  if (!entry || !verifyActiveWorkspaceLease(runId, entry)) return false;
24658
25118
  return entry.agent.sendToolResponse(payload.toolId, payload.response) === true;
@@ -24660,8 +25120,8 @@ async function startDaemon(args) {
24660
25120
  if (!delivered) {
24661
25121
  pushLog(`tool_response missed tool=${payload.toolId}`);
24662
25122
  const fallbackRun = [...activeAgents.entries()][0];
24663
- const resolvedRunId = payload.runId ?? fallbackRun?.[0];
24664
- const conversationId = (payload.runId ? activeAgents.get(payload.runId)?.conversationId : void 0) ?? fallbackRun?.[1].conversationId;
25123
+ const resolvedRunId = payload.runId ?? entries[0]?.[0] ?? fallbackRun?.[0];
25124
+ const conversationId = payload.conversationId ?? (payload.runId ? activeAgents.get(payload.runId)?.conversationId : void 0) ?? fallbackRun?.[1].conversationId;
24665
25125
  if (conversationId && resolvedRunId) {
24666
25126
  eventOutbox.enqueue(conversationId, resolvedRunId, {
24667
25127
  type: "error",
@@ -25444,12 +25904,18 @@ var WebPresenter = class {
25444
25904
  onLog(message) {
25445
25905
  console.error(message);
25446
25906
  }
25907
+ onNotice(kind, message) {
25908
+ this.wsClient.emitEvent({ type: "session_notice", kind, message, ts: Date.now() });
25909
+ }
25447
25910
  onAssistantText(text) {
25448
25911
  this.wsClient.emitEvent({ type: "text", text, ts: Date.now() });
25449
25912
  }
25450
25913
  onThinking(text) {
25451
25914
  this.wsClient.emitEvent({ type: "thinking", text, ts: Date.now() });
25452
25915
  }
25916
+ hasKnownToolUse(toolId) {
25917
+ return this.toolNamesById.has(toolId);
25918
+ }
25453
25919
  onToolUse(tool, input, toolId, parentToolUseId) {
25454
25920
  this.toolNamesById.set(toolId, tool);
25455
25921
  this.toolInputsById.set(toolId, input);
@@ -25541,8 +26007,13 @@ var WebPresenter = class {
25541
26007
  onCheckpoint(id) {
25542
26008
  this.wsClient.emitEvent({ type: "checkpoint", id });
25543
26009
  }
25544
- onActivity(status) {
25545
- this.wsClient.emitEvent({ type: "activity", status, ts: Date.now() });
26010
+ onActivity(status, evidence) {
26011
+ this.wsClient.emitEvent({
26012
+ type: "activity",
26013
+ status,
26014
+ ts: Date.now(),
26015
+ ...evidence ? { evidence } : {}
26016
+ });
25546
26017
  }
25547
26018
  onOpenBrowserTab(browserId, url2) {
25548
26019
  this.wsClient.emitEvent({ type: "open_browser_tab", browserId, ...url2 ? { url: url2 } : {} });
@@ -25583,7 +26054,7 @@ var WebPresenter = class {
25583
26054
  };
25584
26055
 
25585
26056
  // src/ws-client.ts
25586
- var import_node_crypto4 = require("crypto");
26057
+ var import_node_crypto5 = require("crypto");
25587
26058
 
25588
26059
  // ../shared/dist/agent-liveness.js
25589
26060
  var AGENT_LIVENESS_PROTOCOL_VERSION = 1;
@@ -25610,7 +26081,117 @@ var agentProbeAckSchema = external_exports.object({
25610
26081
  protocolVersion: external_exports.number().int().positive().optional()
25611
26082
  });
25612
26083
 
26084
+ // src/sandbox-outbox.ts
26085
+ var import_node_crypto4 = require("crypto");
26086
+ var import_node_fs13 = require("fs");
26087
+ var SANDBOX_EVENT_OUTBOX_ENV = "ALAN_EVENT_OUTBOX_ENABLED";
26088
+ function isSandboxEventOutboxEnabled(env = process.env) {
26089
+ return env[SANDBOX_EVENT_OUTBOX_ENV] === "true";
26090
+ }
26091
+ function deriveSandboxOutboxKey(sessionToken) {
26092
+ if (!sessionToken) {
26093
+ throw new Error("sandbox event outbox requires ALAN_SESSION_TOKEN for key derivation");
26094
+ }
26095
+ return (0, import_node_crypto4.createHash)("sha256").update(sessionToken, "utf8").digest("base64url");
26096
+ }
26097
+ function sandboxOutboxSpoolPath(conversationId) {
26098
+ return `/tmp/alan-agent-outbox-${conversationId}.enc`;
26099
+ }
26100
+ function createSandboxEventOutbox(input) {
26101
+ const path = input.spoolPath ?? sandboxOutboxSpoolPath(input.conversationId);
26102
+ const key = deriveSandboxOutboxKey(input.sessionToken);
26103
+ try {
26104
+ return new EncryptedEventOutbox(path, key, input.pushLog, input.options);
26105
+ } catch (error) {
26106
+ input.pushLog?.(
26107
+ `sandbox_outbox_spool_reset ${error instanceof Error ? error.message : String(error)}`
26108
+ );
26109
+ (0, import_node_fs13.rmSync)(path, { force: true });
26110
+ return new EncryptedEventOutbox(path, key, input.pushLog, input.options);
26111
+ }
26112
+ }
26113
+ var SandboxOutboxSocket = class {
26114
+ constructor(socket) {
26115
+ this.socket = socket;
26116
+ }
26117
+ seqByEventId = /* @__PURE__ */ new Map();
26118
+ seqCounter = 0;
26119
+ get connected() {
26120
+ return this.socket.connected;
26121
+ }
26122
+ emit(_event, payload, acknowledge) {
26123
+ let seq = this.seqByEventId.get(payload.eventId);
26124
+ if (seq === void 0) {
26125
+ this.seqCounter += 1;
26126
+ seq = this.seqCounter;
26127
+ this.seqByEventId.set(payload.eventId, seq);
26128
+ }
26129
+ const wirePayload = { ...payload.event, eventId: payload.eventId, seq };
26130
+ this.socket.emit("agent_event", wirePayload, (result) => {
26131
+ if (result?.ok) this.seqByEventId.delete(payload.eventId);
26132
+ acknowledge({ ok: Boolean(result?.ok), retryable: result?.retryable });
26133
+ });
26134
+ }
26135
+ };
26136
+ var SandboxEventDispatcher = class {
26137
+ conversationId;
26138
+ outbox;
26139
+ outboxSocket;
26140
+ currentRunId;
26141
+ constructor(input) {
26142
+ this.conversationId = input.conversationId;
26143
+ if (!input.enabled) return;
26144
+ try {
26145
+ this.outbox = createSandboxEventOutbox({
26146
+ conversationId: input.conversationId,
26147
+ sessionToken: input.sessionToken,
26148
+ pushLog: input.pushLog,
26149
+ options: input.options,
26150
+ spoolPath: input.spoolPath
26151
+ });
26152
+ this.outboxSocket = new SandboxOutboxSocket(input.socket);
26153
+ } catch (error) {
26154
+ input.onInitError?.(error);
26155
+ this.outbox = void 0;
26156
+ this.outboxSocket = void 0;
26157
+ }
26158
+ }
26159
+ /** True when events are being durably queued rather than fired-and-forgotten. */
26160
+ get active() {
26161
+ return this.outbox !== void 0 && this.outboxSocket !== void 0;
26162
+ }
26163
+ /** Set the run id used to bucket per-run caps / terminal condensation. */
26164
+ setRunId(runId) {
26165
+ this.currentRunId = runId;
26166
+ }
26167
+ /**
26168
+ * Durably deliver one event (enqueue-before-send, remove-on-ack). When the
26169
+ * outbox is inactive, defers to `fireAndForget` so behaviour is unchanged.
26170
+ */
26171
+ emit(event, fireAndForget) {
26172
+ if (!this.outbox || !this.outboxSocket) {
26173
+ fireAndForget(event);
26174
+ return;
26175
+ }
26176
+ this.outbox.enqueue(this.conversationId, this.currentRunId ?? this.conversationId, event);
26177
+ this.outbox.flush(this.outboxSocket);
26178
+ }
26179
+ /** Replay un-acked events in order — called on (re)connect before new events. */
26180
+ flush() {
26181
+ if (this.outbox && this.outboxSocket) this.outbox.flush(this.outboxSocket);
26182
+ }
26183
+ /** Reset in-flight state on disconnect so the next flush replays from the head. */
26184
+ disconnect() {
26185
+ this.outbox?.disconnect();
26186
+ }
26187
+ /** Diagnostics: number of events still awaiting a server ack. */
26188
+ pendingCount() {
26189
+ return this.outbox?.pendingCount() ?? 0;
26190
+ }
26191
+ };
26192
+
25613
26193
  // src/ws-client.ts
26194
+ var ACCEPTED_MESSAGE_ID_CACHE_SIZE = 200;
25614
26195
  var WSClient = class {
25615
26196
  constructor(wsUrl, sessionId, taskId, token, callbacks, agentVersion, lifecycle) {
25616
26197
  this.lifecycle = lifecycle;
@@ -25629,16 +26210,63 @@ var WSClient = class {
25629
26210
  upgrade: false,
25630
26211
  extraHeaders: { "ngrok-skip-browser-warning": "1" }
25631
26212
  });
26213
+ const outboxEnabled = isSandboxEventOutboxEnabled();
26214
+ this.eventDispatcher = new SandboxEventDispatcher({
26215
+ conversationId: sessionId,
26216
+ sessionToken: token,
26217
+ enabled: outboxEnabled,
26218
+ socket: this.socket,
26219
+ pushLog: (message) => this.lifecycle?.info("sandbox_agent_event_outbox", { detail: message }),
26220
+ onInitError: (error) => this.lifecycle?.warn("sandbox_agent_event_outbox_init_failed", {
26221
+ error: error instanceof Error ? error : new Error(String(error))
26222
+ })
26223
+ });
26224
+ if (this.eventDispatcher.active) {
26225
+ this.lifecycle?.info("sandbox_agent_event_outbox_enabled", { conversationId: sessionId });
26226
+ }
25632
26227
  this.setupListeners(callbacks);
25633
26228
  }
25634
26229
  socket;
25635
26230
  /** Stable id for this agent process; fences stale heartbeats server-side. */
25636
- agentSessionId = (0, import_node_crypto4.randomUUID)();
26231
+ agentSessionId = (0, import_node_crypto5.randomUUID)();
25637
26232
  /** Monotonic heartbeat sequence — advances on every hello + heartbeat. */
25638
26233
  heartbeatSeq = 0;
25639
26234
  heartbeatTimer = null;
25640
26235
  agentVersion;
25641
26236
  getRunState;
26237
+ /**
26238
+ * Durable at-least-once delivery for stream events. Active only when
26239
+ * `ALAN_EVENT_OUTBOX_ENABLED === "true"`; otherwise events are fired-and-
26240
+ * forgotten exactly as before (see {@link SandboxEventDispatcher}).
26241
+ */
26242
+ eventDispatcher;
26243
+ /**
26244
+ * Bounded record of `messageId`s whose handoff to `onUserMessage` already
26245
+ * completed successfully. A redelivered `user_message` for one of these ids
26246
+ * (e.g. the original `ok:true` ack was lost in transit and the server's
26247
+ * ≤5-attempt redelivery re-armed the run) is re-acked `{ ok: true }`
26248
+ * without invoking the handler again, so the message is never processed
26249
+ * twice. A `Set`'s insertion order doubles as recency, so eviction is just
26250
+ * "drop the oldest key" — a minimal LRU without extra bookkeeping.
26251
+ */
26252
+ acceptedMessageIds = /* @__PURE__ */ new Set();
26253
+ /** Record a successfully-handed-off messageId, evicting the oldest entry past the cap. */
26254
+ rememberAcceptedMessageId(messageId) {
26255
+ this.acceptedMessageIds.delete(messageId);
26256
+ this.acceptedMessageIds.add(messageId);
26257
+ if (this.acceptedMessageIds.size > ACCEPTED_MESSAGE_ID_CACHE_SIZE) {
26258
+ const oldest = this.acceptedMessageIds.keys().next().value;
26259
+ if (oldest !== void 0) this.acceptedMessageIds.delete(oldest);
26260
+ }
26261
+ }
26262
+ /**
26263
+ * Record the run id for events emitted from here on, so the durable outbox can
26264
+ * bucket per-run caps and terminal-event condensation correctly. No-op when the
26265
+ * outbox is disabled.
26266
+ */
26267
+ setCurrentRunId(runId) {
26268
+ this.eventDispatcher.setRunId(runId);
26269
+ }
25642
26270
  currentRunState() {
25643
26271
  try {
25644
26272
  return this.getRunState?.() ?? { idle: true, activeRunIds: [] };
@@ -25675,7 +26303,9 @@ var WSClient = class {
25675
26303
  }
25676
26304
  }
25677
26305
  emitEvent(event) {
25678
- this.socket.emit("agent_event", event);
26306
+ this.eventDispatcher.emit(event, (payload) => {
26307
+ this.socket.emit("agent_event", payload);
26308
+ });
25679
26309
  }
25680
26310
  waitForConnection(timeoutMs = 15e3) {
25681
26311
  return new Promise((resolve6, reject) => {
@@ -25712,11 +26342,13 @@ var WSClient = class {
25712
26342
  this.socket.on("connect", () => {
25713
26343
  this.lifecycle?.info("sandbox_agent_ws_socket_connected", { socketId: this.socket.id });
25714
26344
  this.startHeartbeat();
26345
+ this.eventDispatcher.flush();
25715
26346
  callbacks.onConnect?.();
25716
26347
  });
25717
26348
  this.socket.on("disconnect", (reason) => {
25718
26349
  this.lifecycle?.info("sandbox_agent_ws_socket_disconnected", { reason });
25719
26350
  this.stopHeartbeat();
26351
+ this.eventDispatcher.disconnect();
25720
26352
  callbacks.onDisconnect?.(reason);
25721
26353
  });
25722
26354
  this.socket.on("agent.probe", (_data, ack) => {
@@ -25734,8 +26366,11 @@ var WSClient = class {
25734
26366
  this.socket.on(
25735
26367
  "user_message",
25736
26368
  (data, ack) => {
25737
- ack?.({ ok: true });
25738
- callbacks.onUserMessage({
26369
+ if (data.messageId && this.acceptedMessageIds.has(data.messageId)) {
26370
+ ack?.({ ok: true });
26371
+ return;
26372
+ }
26373
+ const payload = {
25739
26374
  text: data.text,
25740
26375
  images: data.images,
25741
26376
  files: data.files,
@@ -25759,7 +26394,24 @@ var WSClient = class {
25759
26394
  conversationId: data.conversationId,
25760
26395
  teamId: data.teamId,
25761
26396
  currentUser: data.currentUser
25762
- });
26397
+ };
26398
+ const acceptDelivery = () => {
26399
+ if (data.messageId) this.rememberAcceptedMessageId(data.messageId);
26400
+ ack?.({ ok: true });
26401
+ };
26402
+ const rejectDelivery = (error) => {
26403
+ ack?.({ ok: false, message: error instanceof Error ? error.message : String(error) });
26404
+ };
26405
+ try {
26406
+ const result = callbacks.onUserMessage(payload);
26407
+ if (result instanceof Promise) {
26408
+ result.then(acceptDelivery, rejectDelivery);
26409
+ } else {
26410
+ acceptDelivery();
26411
+ }
26412
+ } catch (error) {
26413
+ rejectDelivery(error);
26414
+ }
25763
26415
  }
25764
26416
  );
25765
26417
  this.socket.on("stop", () => {
@@ -25928,6 +26580,7 @@ async function runSandbox(config) {
25928
26580
  backendKind: activeBackendKind
25929
26581
  });
25930
26582
  currentAgent = agent;
26583
+ wsClient.setCurrentRunId(currentRunId ?? config.runId);
25931
26584
  const correlation = correlationLogFields2({
25932
26585
  taskId: currentTaskId,
25933
26586
  conversationId: currentConversationId,
@@ -26145,7 +26798,7 @@ async function runSandbox(config) {
26145
26798
 
26146
26799
  // src/service-manager.ts
26147
26800
  var import_node_child_process6 = require("child_process");
26148
- var import_node_fs13 = require("fs");
26801
+ var import_node_fs14 = require("fs");
26149
26802
  var import_node_os8 = require("os");
26150
26803
  var import_node_path11 = require("path");
26151
26804
  var SERVICE_LABEL = "ai.tryalan.agent";
@@ -26344,14 +26997,14 @@ function currentServicePlan(args) {
26344
26997
  });
26345
26998
  }
26346
26999
  function writeManifest(path, contents) {
26347
- (0, import_node_fs13.mkdirSync)((0, import_node_path11.dirname)(path), { recursive: true, mode: 448 });
27000
+ (0, import_node_fs14.mkdirSync)((0, import_node_path11.dirname)(path), { recursive: true, mode: 448 });
26348
27001
  const pendingPath = `${path}.pending-${process.pid}`;
26349
27002
  try {
26350
- (0, import_node_fs13.writeFileSync)(pendingPath, contents, { mode: 384 });
26351
- (0, import_node_fs13.chmodSync)(pendingPath, 384);
26352
- (0, import_node_fs13.renameSync)(pendingPath, path);
27003
+ (0, import_node_fs14.writeFileSync)(pendingPath, contents, { mode: 384 });
27004
+ (0, import_node_fs14.chmodSync)(pendingPath, 384);
27005
+ (0, import_node_fs14.renameSync)(pendingPath, path);
26353
27006
  } finally {
26354
- (0, import_node_fs13.rmSync)(pendingPath, { force: true });
27007
+ (0, import_node_fs14.rmSync)(pendingPath, { force: true });
26355
27008
  }
26356
27009
  }
26357
27010
  function runServiceCommand(command) {
@@ -26374,7 +27027,7 @@ function installDaemonService(args = []) {
26374
27027
  const plan = currentServicePlan(args);
26375
27028
  if (plan.manifestPath && plan.manifest) {
26376
27029
  if ((0, import_node_os8.platform)() === "darwin") {
26377
- (0, import_node_fs13.mkdirSync)((0, import_node_path11.join)((0, import_node_os8.homedir)(), ".alan", "agent", "logs"), { recursive: true, mode: 448 });
27030
+ (0, import_node_fs14.mkdirSync)((0, import_node_path11.join)((0, import_node_os8.homedir)(), ".alan", "agent", "logs"), { recursive: true, mode: 448 });
26378
27031
  }
26379
27032
  writeManifest(plan.manifestPath, plan.manifest);
26380
27033
  }
@@ -26384,7 +27037,7 @@ function installDaemonService(args = []) {
26384
27037
  function uninstallDaemonService(args = []) {
26385
27038
  const plan = currentServicePlan(args);
26386
27039
  for (const command of plan.uninstallCommands) runServiceCommand(command);
26387
- if (plan.manifestPath) (0, import_node_fs13.rmSync)(plan.manifestPath, { force: true });
27040
+ if (plan.manifestPath) (0, import_node_fs14.rmSync)(plan.manifestPath, { force: true });
26388
27041
  console.info("[alan-agent] Per-user daemon service removed");
26389
27042
  }
26390
27043
  function printDaemonServiceStatus(args = []) {