@aiden-ade/sandbox-agent 0.1.36 → 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 +872 -202
  2. package/package.json +2 -2
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":
@@ -13534,6 +13535,14 @@ var AGENT_CONFIG_KEYS = /* @__PURE__ */ new Map([
13534
13535
  ["localApiToken", "string"],
13535
13536
  ["displayName", "string"]
13536
13537
  ]);
13538
+ function sanitizeAgentConfig(config) {
13539
+ const sanitized = { ...config };
13540
+ delete sanitized.teamId;
13541
+ delete sanitized.organizationId;
13542
+ delete sanitized.currentTeamId;
13543
+ delete sanitized.currentOrganizationId;
13544
+ return sanitized;
13545
+ }
13537
13546
  function isValidAgentConfig(configPath) {
13538
13547
  try {
13539
13548
  const stat = (0, import_node_fs4.lstatSync)(configPath);
@@ -13571,6 +13580,10 @@ function migrateLegacyAgentConfig(input) {
13571
13580
  if (!alanDirectoryExisted) (0, import_node_fs4.chmodSync)(alanBaseDir, sourceDirectoryStat.mode & 511);
13572
13581
  (0, import_node_fs4.copyFileSync)(legacyPath, alanPath, import_node_fs4.constants.COPYFILE_EXCL);
13573
13582
  copied = true;
13583
+ const migrated = sanitizeAgentConfig(
13584
+ JSON.parse((0, import_node_fs4.readFileSync)(alanPath, "utf8"))
13585
+ );
13586
+ (0, import_node_fs4.writeFileSync)(alanPath, JSON.stringify(migrated, null, 2));
13574
13587
  (0, import_node_fs4.chmodSync)(alanPath, sourceFileStat.mode & 511);
13575
13588
  (0, import_node_fs4.utimesSync)(alanPath, sourceFileStat.atime, sourceFileStat.mtime);
13576
13589
  if (!isValidAgentConfig(alanPath)) {
@@ -14498,33 +14511,6 @@ Deliverables persist as Alan artifacts and documents \u2014 not as chat. Plans,
14498
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.
14499
14512
 
14500
14513
  The specific work you do \u2014 implementation, planning, review, coordination, verification \u2014 is determined by your agent role, described next.`;
14501
- var ALAN_CURSOR_ASK_USER_OVERLAY = `### Cursor runtime note
14502
- 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.`;
14503
- var ALAN_ASK_USER_PROMPT = `## Asking the user
14504
-
14505
- 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.
14506
-
14507
- ### Tool preference (required)
14508
- 1. Prefer the **native** ask tool when it appears in your tool list (\`AskUserQuestion\`, or the backend's equivalent built-in ask tool).
14509
- 2. Only if native ask is missing, call \`mcp__alan__ask_user_question\` (server tool name: \`ask_user_question\`).
14510
-
14511
- Required arguments (both native and MCP forms):
14512
- - \`questions\`: non-empty array. Each item needs a clear \`question\` string.
14513
- - Prefer 2\u20135 selectable \`options\` per question (\`label\` required; optional \`value\` / \`description\`). Free-text-only questions are allowed when options do not fit.
14514
- - Do NOT include an "Other" option \u2014 the UI always adds a freeform Other\u2026 field.
14515
- - Set \`multiSelect: true\` on a question when the user may pick more than one option; omit or set false for single-select (default).
14516
- - Optional \`header\` per question and optional \`title\` for the card header.
14517
-
14518
- 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.
14519
-
14520
- ### Never do this
14521
- - Do NOT paste multiple-choice lists, A/B/C tables, or "reply with 1A / 1B" in chat.
14522
- - 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.
14523
- - Do NOT call the tool with empty \`questions\`, placeholder text, or missing options when choices exist.
14524
- - 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).
14525
-
14526
- ### After the tool call
14527
- 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.`;
14528
14514
  function buildAlanTeamScopePrompt(teamId) {
14529
14515
  return [
14530
14516
  `You are operating within team \`${teamId}\`.`,
@@ -15216,20 +15202,23 @@ var import_path3 = require("path");
15216
15202
  var import_readline = require("readline");
15217
15203
  var import_child_process2 = require("child_process");
15218
15204
  var import_crypto2 = require("crypto");
15219
- var import_child_process3 = require("child_process");
15220
- var import_util10 = require("util");
15221
15205
  var import_fs4 = require("fs");
15222
15206
  var import_os4 = require("os");
15223
15207
  var import_path4 = require("path");
15208
+ var import_child_process3 = require("child_process");
15209
+ var import_util10 = require("util");
15224
15210
  var import_fs5 = require("fs");
15225
15211
  var import_os5 = require("os");
15226
15212
  var import_path5 = require("path");
15227
15213
  var import_fs6 = require("fs");
15228
15214
  var import_os6 = require("os");
15229
15215
  var import_path6 = require("path");
15230
- var import_crypto3 = require("crypto");
15231
15216
  var import_fs7 = require("fs");
15217
+ var import_os7 = require("os");
15232
15218
  var import_path7 = require("path");
15219
+ var import_crypto3 = require("crypto");
15220
+ var import_fs8 = require("fs");
15221
+ var import_path8 = require("path");
15233
15222
  var MANAGED_ALAN_MCP_SERVER_NAMES = [
15234
15223
  "alan",
15235
15224
  "alan-prod",
@@ -15724,6 +15713,10 @@ function getClaudePermissionMode(config) {
15724
15713
  function getClaudeDisallowedTools(config) {
15725
15714
  return isReadOnlyAgent(config.agentId) ? ["Edit", "Write", "MultiEdit", "NotebookEdit"] : [];
15726
15715
  }
15716
+ function getCodexPermissionArgs(config) {
15717
+ const sandbox = shouldUseReadOnlyRuntimePermissions(config) ? "read-only" : "danger-full-access";
15718
+ return ["--ask-for-approval", "never", "--sandbox", sandbox];
15719
+ }
15727
15720
  function isLikelyProviderAuthError(stderr) {
15728
15721
  const lower = stderr.toLowerCase();
15729
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");
@@ -15820,6 +15813,16 @@ function spawnCli(command, args, context) {
15820
15813
  detached: process.platform !== "win32"
15821
15814
  });
15822
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
+ }
15823
15826
  function structuredEventKey(event) {
15824
15827
  const type = typeof event.type === "string" && event.type ? event.type : "";
15825
15828
  const role = typeof event.role === "string" && event.role ? event.role : "";
@@ -15889,7 +15892,12 @@ function createGenericCliBackend(options) {
15889
15892
  const args = options.buildArgs?.(context, prompt) ?? options.args.map((arg) => arg === "{{prompt}}" ? prompt : arg);
15890
15893
  const child = spawnCli(options.command, args, context);
15891
15894
  state.process = child;
15895
+ state.lastRawOutputAtMs = Date.now();
15892
15896
  context.onProcessSpawned?.(child);
15897
+ context.registerLivenessProbe?.(() => ({
15898
+ providerAlive: child.exitCode === null,
15899
+ lastRawOutputAgoMs: typeof state.lastRawOutputAtMs === "number" ? Date.now() - state.lastRawOutputAtMs : null
15900
+ }));
15893
15901
  const safeArgs = args.map(
15894
15902
  (a, i) => i > 0 && args[i - 1] === "--system-prompt" ? `"<system-prompt ${a.length} chars>"` : a
15895
15903
  );
@@ -15938,6 +15946,7 @@ function createGenericCliBackend(options) {
15938
15946
  }
15939
15947
  stdoutRl.on("line", (line) => {
15940
15948
  sawStdoutLine = true;
15949
+ state.lastRawOutputAtMs = Date.now();
15941
15950
  presenter.recordRawTranscript?.("stdout", line);
15942
15951
  if (options.parseStructuredLine) {
15943
15952
  options.parseStructuredLine(line, context, state);
@@ -15955,6 +15964,7 @@ function createGenericCliBackend(options) {
15955
15964
  }
15956
15965
  });
15957
15966
  stderrRl.on("line", (line) => {
15967
+ state.lastRawOutputAtMs = Date.now();
15958
15968
  presenter.recordRawTranscript?.("stderr", line);
15959
15969
  if (options.parseStderrLine) {
15960
15970
  options.parseStderrLine(line, context, state);
@@ -16001,7 +16011,7 @@ function createGenericCliBackend(options) {
16001
16011
  if (presenter.pendingAskUserToolIds?.size) {
16002
16012
  idleTimeoutReason = "pending_user_answer";
16003
16013
  resolve22("idle_timeout");
16004
- } else if (state.activeBackgroundTaskIds?.size) {
16014
+ } else if (state.resultDeferredOnBackgroundWork && state.activeBackgroundTaskIds?.size) {
16005
16015
  idleTimeoutReason = "background_task";
16006
16016
  resolve22("idle_timeout");
16007
16017
  } else {
@@ -16064,10 +16074,26 @@ function createGenericCliBackend(options) {
16064
16074
  logUnhandledStructuredEventSummary(options.kind, state);
16065
16075
  }
16066
16076
  const requestedResumeId = context.config.runtimeSessionId?.trim() || context.config.providerSessionId?.trim();
16077
+ let cursorResumeSilentlyFailed = false;
16067
16078
  if (requestedResumeId && state.runtimeSessionId && state.runtimeSessionId !== requestedResumeId) {
16068
16079
  console.warn(
16069
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`
16070
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
+ }
16071
16097
  }
16072
16098
  if (context.abortController.signal.aborted) {
16073
16099
  return {
@@ -16161,6 +16187,22 @@ function createGenericCliBackend(options) {
16161
16187
  const stderrText = stderrLines.join("\n").trim();
16162
16188
  const hasStructuredError = exitCode === 0 && !!state.error?.trim();
16163
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
+ }
16164
16206
  const summary = state.summary.trim() || state.error?.trim() || (failed ? "Task failed" : "Task completed");
16165
16207
  return {
16166
16208
  success: !failed,
@@ -16610,6 +16652,21 @@ function createAntigravityCliBackend(command = "agy", defaultArgs = []) {
16610
16652
  }
16611
16653
  };
16612
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
+ }
16613
16670
  var execFileAsync = (0, import_util10.promisify)(import_child_process3.execFile);
16614
16671
  var flagSupportCache = /* @__PURE__ */ new Map();
16615
16672
  function helpAdvertisesFlag(helpText, flag) {
@@ -16660,15 +16717,38 @@ function registerBackgroundTaskIds(state, keys) {
16660
16717
  state.activeBackgroundTaskIds.add(key);
16661
16718
  }
16662
16719
  }
16663
- 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) {
16664
16734
  const taskId = backgroundTaskId(record);
16665
- let toolUseId = backgroundToolUseId(record);
16666
- if (!toolUseId && taskId && state.activeBackgroundTaskIds?.size === 1) {
16667
- const [candidate] = state.activeBackgroundTaskIds;
16668
- 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
+ }
16669
16744
  }
16670
16745
  registerBackgroundTaskIds(state, backgroundTaskKeys(record));
16671
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
+ }
16672
16752
  if (!state.backgroundToolIdByTaskId) state.backgroundToolIdByTaskId = /* @__PURE__ */ new Map();
16673
16753
  if (!state.backgroundTaskIdsByToolId) state.backgroundTaskIdsByToolId = /* @__PURE__ */ new Map();
16674
16754
  state.backgroundToolIdByTaskId.set(taskId, toolUseId);
@@ -16699,6 +16779,10 @@ function clearBackgroundTaskIds(state, keys) {
16699
16779
  state.backgroundTaskIdsByToolId?.delete(key);
16700
16780
  }
16701
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
+ }
16702
16786
  if (activeIds.size === 0) state.activeBackgroundTaskIds = void 0;
16703
16787
  }
16704
16788
  function resolveBackgroundTaskToolUseId(state, record) {
@@ -16721,6 +16805,7 @@ function deferOrFinalizeStructuredResult(kind, context, state) {
16721
16805
  console.info(
16722
16806
  `[${kind}] Keeping stdin open \u2014 ${pendingAsks} interactive question(s) awaiting user response`
16723
16807
  );
16808
+ state.resultDeferredOnPendingAnswer = true;
16724
16809
  return "deferred_pending_answer";
16725
16810
  }
16726
16811
  const activeBackgroundTasks = state.activeBackgroundTaskIds?.size ?? 0;
@@ -16735,9 +16820,10 @@ function deferOrFinalizeStructuredResult(kind, context, state) {
16735
16820
  return "finalized";
16736
16821
  }
16737
16822
  function maybeFinalizeDeferredResult(kind, context, state) {
16738
- if (!state.resultDeferredOnBackgroundWork) return;
16823
+ if (!state.resultDeferredOnBackgroundWork && !state.resultDeferredOnPendingAnswer) return;
16739
16824
  if (context.presenter.pendingAskUserToolIds?.size || state.activeBackgroundTaskIds?.size) return;
16740
16825
  state.resultDeferredOnBackgroundWork = false;
16826
+ state.resultDeferredOnPendingAnswer = false;
16741
16827
  finalizeStructuredResult(kind, state);
16742
16828
  }
16743
16829
  var DEBUG_ASK = process.env.ALAN_DEBUG_ASK === "1" || process.env.ALAN_DEBUG === "1";
@@ -16807,21 +16893,44 @@ function handleClaudeStructuredEvent(parsed, context, state) {
16807
16893
  return true;
16808
16894
  }
16809
16895
  if (parsed.subtype === "task_started") {
16810
- 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
+ );
16811
16909
  return true;
16812
16910
  }
16813
16911
  if (parsed.subtype === "task_notification") {
16814
16912
  if (isTerminalTaskNotification(parsed)) {
16815
16913
  const toolUseId = resolveBackgroundTaskToolUseId(state, parsed);
16816
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;
16817
16916
  if (toolUseId && wasActive) {
16818
16917
  void presenter.onToolResult?.(
16819
16918
  toolUseId,
16820
16919
  formatClaudeTaskNotificationResult(parsed),
16821
- isFailedTaskNotification(parsed) ? true : void 0
16920
+ isFailed
16822
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
+ }
16823
16933
  }
16824
- clearBackgroundTaskIds(state, backgroundTaskKeys(parsed));
16825
16934
  maybeFinalizeDeferredResult("claude_cli", context, state);
16826
16935
  }
16827
16936
  return true;
@@ -16829,9 +16938,10 @@ function handleClaudeStructuredEvent(parsed, context, state) {
16829
16938
  return false;
16830
16939
  }
16831
16940
  case "assistant": {
16832
- if (presenter.pendingAskUserToolIds?.size) {
16941
+ if (state.awaitingAskDenialFallback) {
16942
+ state.awaitingAskDenialFallback = false;
16833
16943
  console.info(
16834
- `[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"
16835
16945
  );
16836
16946
  return true;
16837
16947
  }
@@ -16860,7 +16970,7 @@ function handleClaudeStructuredEvent(parsed, context, state) {
16860
16970
  parentToolUseId
16861
16971
  );
16862
16972
  if (CLAUDE_SUBAGENT_TOOL_NAMES.has(toolBlock.name.toLowerCase())) {
16863
- registerBackgroundTaskIds(state, [toolBlock.id]);
16973
+ registerBackgroundLauncher(state, toolBlock.id);
16864
16974
  }
16865
16975
  const normalizedName = normalizeToolName(toolBlock.name);
16866
16976
  if (isInteractiveToolName(toolBlock.name) || INTERACTIVE_TOOL_NAMES.has(normalizedName)) {
@@ -16871,6 +16981,10 @@ function handleClaudeStructuredEvent(parsed, context, state) {
16871
16981
  if (isAskUserQuestionTool(toolBlock.name)) {
16872
16982
  if (!presenter.pendingAskUserToolIds) presenter.pendingAskUserToolIds = /* @__PURE__ */ new Set();
16873
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
+ }
16874
16988
  debugAskLog(
16875
16989
  `[claude_cli] AskUserQuestion tool_use detected \u2014 id=${toolBlock.id}, name=${toolBlock.name}, pendingSize=${presenter.pendingAskUserToolIds.size}, presenterHasProp=${Object.hasOwn(presenter, "pendingAskUserToolIds")}`
16876
16990
  );
@@ -16895,6 +17009,22 @@ function handleClaudeStructuredEvent(parsed, context, state) {
16895
17009
  for (const block of content) {
16896
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") {
16897
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
+ }
16898
17028
  console.info(
16899
17029
  `[claude_cli] Suppressed auto-generated tool_result for ${block.tool_use_id}`
16900
17030
  );
@@ -17002,7 +17132,31 @@ function createClaudeCliBackend(command = "claude", defaultArgs = []) {
17002
17132
  if (disallowedTools.length > 0) {
17003
17133
  args.push("--disallowedTools", disallowedTools.join(","));
17004
17134
  }
17005
- 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
+ }
17006
17160
  if (!resumeId && context.config.systemPrompt?.trim()) {
17007
17161
  args.push("--system-prompt", context.config.systemPrompt.trim());
17008
17162
  } else if (resumeId && context.config.systemPromptAppend?.trim()) {
@@ -17022,6 +17176,10 @@ function createClaudeCliBackend(command = "claude", defaultArgs = []) {
17022
17176
  if (resumeId) {
17023
17177
  args.push("--resume", resumeId);
17024
17178
  console.info("[claude_cli] Resuming session", { resumeId });
17179
+ logResumeObservability("agent_resume_requested", {
17180
+ backendKind: "claude_cli",
17181
+ requestedResumeId: resumeId
17182
+ });
17025
17183
  }
17026
17184
  args.push("--chrome");
17027
17185
  console.info("[claude_cli] Chrome flag added \u2014 final args:", args.join(" "));
@@ -17042,7 +17200,10 @@ function createClaudeCliBackend(command = "claude", defaultArgs = []) {
17042
17200
  });
17043
17201
  }
17044
17202
  }
17045
- 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);
17046
17207
  if (promptText.trim()) {
17047
17208
  contentBlocks.push({ type: "text", text: promptText });
17048
17209
  }
@@ -17093,27 +17254,30 @@ function normalizeCodexMcpToolResult(output) {
17093
17254
  }
17094
17255
  return raw;
17095
17256
  }
17257
+ function resolveCodexHome(env) {
17258
+ return env.CODEX_HOME || process.env.CODEX_HOME || (0, import_path5.join)((0, import_os5.homedir)(), ".codex");
17259
+ }
17096
17260
  function findCodexSessionLog(runtimeSessionId, codexHome) {
17097
- if (!runtimeSessionId || !(0, import_fs4.existsSync)(codexHome)) return null;
17098
- const root = (0, import_path4.join)(codexHome, "sessions");
17099
- 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;
17100
17264
  const matches = [];
17101
17265
  const stack = [root];
17102
17266
  while (stack.length > 0) {
17103
17267
  const dir = stack.pop();
17104
17268
  let entries;
17105
17269
  try {
17106
- entries = (0, import_fs4.readdirSync)(dir, { withFileTypes: true });
17270
+ entries = (0, import_fs5.readdirSync)(dir, { withFileTypes: true });
17107
17271
  } catch {
17108
17272
  continue;
17109
17273
  }
17110
17274
  for (const entry of entries) {
17111
- const path = (0, import_path4.join)(dir, entry.name);
17275
+ const path = (0, import_path5.join)(dir, entry.name);
17112
17276
  if (entry.isDirectory()) {
17113
17277
  stack.push(path);
17114
17278
  } else if (entry.isFile() && entry.name.includes(runtimeSessionId) && entry.name.endsWith(".jsonl")) {
17115
17279
  try {
17116
- matches.push({ path, mtimeMs: (0, import_fs4.statSync)(path).mtimeMs });
17280
+ matches.push({ path, mtimeMs: (0, import_fs5.statSync)(path).mtimeMs });
17117
17281
  } catch {
17118
17282
  matches.push({ path, mtimeMs: 0 });
17119
17283
  }
@@ -17146,12 +17310,14 @@ function latestUserMessageLineIndex(lines) {
17146
17310
  async function replayCodexMcpToolEventsFromSessionLog(context, state) {
17147
17311
  const runtimeSessionId = state.runtimeSessionId;
17148
17312
  if (!runtimeSessionId) return;
17149
- 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);
17150
17315
  const logPath = findCodexSessionLog(runtimeSessionId, codexHome);
17151
17316
  if (!logPath) return;
17152
- 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);
17153
17318
  const latestUserLineIndex = latestUserMessageLineIndex(allLines);
17154
17319
  const lines = latestUserLineIndex >= 0 ? allLines.slice(latestUserLineIndex + 1) : allLines;
17320
+ const streamedToolIds = state.codexStreamedToolIds ?? /* @__PURE__ */ new Set();
17155
17321
  const emittedToolIds = /* @__PURE__ */ new Set();
17156
17322
  for (const line of lines) {
17157
17323
  const entry = parseJsonObject(line);
@@ -17162,6 +17328,7 @@ async function replayCodexMcpToolEventsFromSessionLog(context, state) {
17162
17328
  const name = typeof payload.name === "string" ? payload.name : "";
17163
17329
  const namespace = typeof payload.namespace === "string" ? payload.namespace : "";
17164
17330
  if (!callId || !name || !namespace.startsWith("mcp__")) continue;
17331
+ if (streamedToolIds.has(callId)) continue;
17165
17332
  const toolName = `${namespace.replace(/_+$/, "")}__${name}`;
17166
17333
  context.presenter.onToolUse(toolName, parseMaybeJson(payload.arguments), callId);
17167
17334
  emittedToolIds.add(callId);
@@ -17170,6 +17337,7 @@ async function replayCodexMcpToolEventsFromSessionLog(context, state) {
17170
17337
  if (payload.type === "tool_search_call") {
17171
17338
  const callId = typeof payload.call_id === "string" ? payload.call_id : "";
17172
17339
  if (!callId) continue;
17340
+ if (streamedToolIds.has(callId)) continue;
17173
17341
  context.presenter.onToolUse("tool_search", payload.arguments ?? {}, callId);
17174
17342
  emittedToolIds.add(callId);
17175
17343
  continue;
@@ -17196,6 +17364,63 @@ async function replayCodexMcpToolEventsFromSessionLog(context, state) {
17196
17364
  }
17197
17365
  }
17198
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
+ }
17199
17424
  function handleCodexStructuredEvent(parsed, context, state) {
17200
17425
  const presenter = context.presenter;
17201
17426
  const type = typeof parsed.type === "string" ? parsed.type : "";
@@ -17223,6 +17448,7 @@ function handleCodexStructuredEvent(parsed, context, state) {
17223
17448
  case "thread.started":
17224
17449
  return true;
17225
17450
  case "turn.started":
17451
+ disarmCodexExitGraceKill(state);
17226
17452
  state.iterations += 1;
17227
17453
  return true;
17228
17454
  case "session_configured":
@@ -17231,36 +17457,112 @@ function handleCodexStructuredEvent(parsed, context, state) {
17231
17457
  }
17232
17458
  return true;
17233
17459
  case "task_started":
17460
+ disarmCodexExitGraceKill(state);
17234
17461
  state.iterations += 1;
17235
17462
  return true;
17236
17463
  case "item.started": {
17237
17464
  const item = typeof parsed.item === "object" && parsed.item !== null ? parsed.item : null;
17238
- if (!item || item.type !== "command_execution" || typeof item.id !== "string") return true;
17239
- const command = typeof item.command === "string" ? item.command : "";
17240
- void presenter.onToolUse("Bash", { command, cwd: context.cwd }, item.id);
17241
- 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
+ }
17242
17498
  }
17243
17499
  case "item.completed": {
17244
17500
  const item = typeof parsed.item === "object" && parsed.item !== null ? parsed.item : null;
17245
17501
  if (!item || typeof item.type !== "string") return true;
17246
- if (item.type === "reasoning" && typeof item.text === "string") {
17247
- void presenter.onThinking(item.text);
17248
- return true;
17249
- }
17250
- if (item.type === "agent_message" && typeof item.text === "string") {
17251
- 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}
17252
17510
  `;
17253
- void presenter.onAssistantText(item.text);
17254
- return true;
17255
- }
17256
- if (item.type === "command_execution" && typeof item.id === "string") {
17257
- const output = typeof item.aggregated_output === "string" ? item.aggregated_output : "";
17258
- const itemExitCode = typeof item.exit_code === "number" ? item.exit_code : null;
17259
- const resultText = output.trim().length > 0 ? output : itemExitCode === null ? "" : `Exit code: ${itemExitCode}`;
17260
- void presenter.onToolResult?.(item.id, resultText);
17261
- 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;
17262
17565
  }
17263
- return true;
17264
17566
  }
17265
17567
  case "agent_message_delta":
17266
17568
  case "agent_message_content_delta": {
@@ -17282,6 +17584,7 @@ function handleCodexStructuredEvent(parsed, context, state) {
17282
17584
  const toolId = typeof parsed.call_id === "string" ? parsed.call_id : `exec-${Date.now()}`;
17283
17585
  const command = Array.isArray(parsed.command) ? parsed.command.join(" ") : "";
17284
17586
  const cwd = typeof parsed.cwd === "string" ? parsed.cwd : context.cwd;
17587
+ trackCodexStreamedToolId(state, toolId);
17285
17588
  void presenter.onToolUse("Bash", { command, cwd }, toolId);
17286
17589
  return true;
17287
17590
  }
@@ -17295,6 +17598,7 @@ function handleCodexStructuredEvent(parsed, context, state) {
17295
17598
  const toolId = typeof parsed.call_id === "string" ? parsed.call_id : `mcp-${Date.now()}`;
17296
17599
  const invocation = typeof parsed.invocation === "object" && parsed.invocation !== null ? parsed.invocation : {};
17297
17600
  const tool = typeof invocation.tool_name === "string" ? invocation.tool_name : typeof invocation.tool === "string" ? invocation.tool : "MCP Tool";
17601
+ trackCodexStreamedToolId(state, toolId);
17298
17602
  void presenter.onToolUse(tool, invocation, toolId);
17299
17603
  return true;
17300
17604
  }
@@ -17332,6 +17636,8 @@ function handleCodexStructuredEvent(parsed, context, state) {
17332
17636
  case "task_complete": {
17333
17637
  const lastMessage = typeof parsed.last_agent_message === "string" ? parsed.last_agent_message : "";
17334
17638
  if (lastMessage.length > 0) state.summary = lastMessage;
17639
+ state.error = void 0;
17640
+ armCodexExitGraceKill(state);
17335
17641
  return true;
17336
17642
  }
17337
17643
  case "turn.completed": {
@@ -17353,6 +17659,8 @@ function handleCodexStructuredEvent(parsed, context, state) {
17353
17659
  cacheReadTokens: state.usage.cacheReadTokens,
17354
17660
  cacheCreationTokens: state.usage.cacheCreationTokens
17355
17661
  });
17662
+ state.error = void 0;
17663
+ armCodexExitGraceKill(state);
17356
17664
  return true;
17357
17665
  }
17358
17666
  case "error": {
@@ -17397,6 +17705,33 @@ function createCodexRuntimeBackend(command = "codex", defaultArgs = []) {
17397
17705
  context.config.images,
17398
17706
  context.cwd
17399
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
+ }
17400
17735
  try {
17401
17736
  return await createGenericCliBackend({
17402
17737
  kind: "codex_app_server",
@@ -17405,15 +17740,10 @@ function createCodexRuntimeBackend(command = "codex", defaultArgs = []) {
17405
17740
  args: [],
17406
17741
  startupTimeoutMs: CODEX_STARTUP_TIMEOUT_MS,
17407
17742
  buildArgs: (ctx) => {
17408
- const resumeId = ctx.config.runtimeSessionId?.trim() || ctx.config.providerSessionId?.trim();
17743
+ const resumeId = resumableSessionId;
17409
17744
  const modelArgs = ctx.config.selectedModel?.trim() ? ["--model", ctx.config.selectedModel.trim()] : [];
17410
17745
  const effortArgs = buildCodexEffortArgs(ctx.config.selectedEffortLevel);
17411
- const permissionArgs = [
17412
- "--ask-for-approval",
17413
- "never",
17414
- "--sandbox",
17415
- "danger-full-access"
17416
- ];
17746
+ const permissionArgs = getCodexPermissionArgs(ctx.config);
17417
17747
  const imageArgs = imagePaths.flatMap((p) => ["--image", p]);
17418
17748
  const baseArgs = resumeId ? [
17419
17749
  ...permissionArgs,
@@ -17440,7 +17770,10 @@ function createCodexRuntimeBackend(command = "codex", defaultArgs = []) {
17440
17770
  },
17441
17771
  promptViaStdin: true,
17442
17772
  augmentPrompt: (ctx) => {
17443
- 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);
17444
17777
  return ctx.config.mode === "plan" ? buildPlanModePrefix(basePrompt) : basePrompt;
17445
17778
  },
17446
17779
  parseStructuredLine: parseCodexStructuredLine,
@@ -17767,7 +18100,10 @@ function buildCursorAgentModelArg(modelId, options) {
17767
18100
  return `${parsed.baseId}[${overrides.join(",")}]`;
17768
18101
  }
17769
18102
  function isCursorAgentCliModelId(model) {
17770
- 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));
17771
18107
  }
17772
18108
  function shouldForceCursorAgent(config) {
17773
18109
  if (shouldUseReadOnlyRuntimePermissions(config)) return false;
@@ -17785,6 +18121,21 @@ function createCursorAgentCliBackend(command = "cursor-agent", defaultArgs = [])
17785
18121
  kind: "cursor_agent_cli",
17786
18122
  supportTier: "structured",
17787
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
+ }
17788
18139
  const { files: imageFiles, cleanup } = writeImagesToTempFiles(
17789
18140
  context.config.images,
17790
18141
  context.cwd
@@ -17814,10 +18165,18 @@ function createCursorAgentCliBackend(command = "cursor-agent", defaultArgs = [])
17814
18165
  args.push("--mode", "plan");
17815
18166
  } else if (ctx.config.mode === "ask") {
17816
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
+ });
17817
18177
  }
17818
- if (resumeId) args.push("--resume", resumeId);
17819
18178
  const model = ctx.config.selectedModel?.trim();
17820
- if (model && isCursorAgentCliModelId(model)) {
18179
+ if (model && isDispatchableCursorModel(model)) {
17821
18180
  const modelDef = findModelDef("cursor_agent_cli", model);
17822
18181
  args.push(
17823
18182
  "--model",
@@ -18280,9 +18639,9 @@ function buildGrokAgentArgs(config, defaultArgs = []) {
18280
18639
  return args;
18281
18640
  }
18282
18641
  var DEFAULT_SKILL_ROOTS = [
18283
- (0, import_path5.join)((0, import_os5.homedir)(), ".agents", "skills"),
18284
- (0, import_path5.join)((0, import_os5.homedir)(), ".claude", "skills"),
18285
- (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")
18286
18645
  ];
18287
18646
  function parseFrontmatter(content) {
18288
18647
  if (!content.startsWith("---\n")) return {};
@@ -18298,12 +18657,12 @@ function parseFrontmatter(content) {
18298
18657
  return result;
18299
18658
  }
18300
18659
  function listSkillFiles(root) {
18301
- if (!(0, import_fs5.existsSync)(root)) return [];
18660
+ if (!(0, import_fs6.existsSync)(root)) return [];
18302
18661
  const files = [];
18303
- for (const entry of (0, import_fs5.readdirSync)(root, { withFileTypes: true })) {
18662
+ for (const entry of (0, import_fs6.readdirSync)(root, { withFileTypes: true })) {
18304
18663
  if (!entry.isDirectory()) continue;
18305
- const skillPath = (0, import_path5.join)(root, entry.name, "SKILL.md");
18306
- 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);
18307
18666
  }
18308
18667
  return files;
18309
18668
  }
@@ -18312,7 +18671,7 @@ function loadInstalledSkills(roots = DEFAULT_SKILL_ROOTS) {
18312
18671
  for (const root of roots) {
18313
18672
  for (const skillPath of listSkillFiles(root)) {
18314
18673
  try {
18315
- const frontmatter = parseFrontmatter((0, import_fs5.readFileSync)(skillPath, "utf8"));
18674
+ const frontmatter = parseFrontmatter((0, import_fs6.readFileSync)(skillPath, "utf8"));
18316
18675
  const name = frontmatter.name;
18317
18676
  const description = frontmatter.description;
18318
18677
  if (!name || !description || skills.has(name)) continue;
@@ -18745,7 +19104,31 @@ function createSupatestCliBackend(command = "supatest", defaultArgs = []) {
18745
19104
  "--permission-mode",
18746
19105
  getClaudePermissionMode(context.config)
18747
19106
  ];
18748
- 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
+ }
18749
19132
  if (!resumeId) {
18750
19133
  if (context.config.systemPrompt?.trim()) {
18751
19134
  args.push("--system-prompt", context.config.systemPrompt.trim());
@@ -18761,6 +19144,10 @@ function createSupatestCliBackend(command = "supatest", defaultArgs = []) {
18761
19144
  if (resumeId) {
18762
19145
  args.push("--resume", resumeId);
18763
19146
  console.info("[supatest_cli] Resuming session", { resumeId });
19147
+ logResumeObservability("agent_resume_requested", {
19148
+ backendKind: "supatest_cli",
19149
+ requestedResumeId: resumeId
19150
+ });
18764
19151
  }
18765
19152
  args.push(...defaultArgs);
18766
19153
  return createGenericCliBackend({
@@ -18779,7 +19166,10 @@ function createSupatestCliBackend(command = "supatest", defaultArgs = []) {
18779
19166
  });
18780
19167
  }
18781
19168
  }
18782
- 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);
18783
19173
  if (promptText.trim()) {
18784
19174
  contentBlocks.push({ type: "text", text: promptText });
18785
19175
  }
@@ -18809,40 +19199,40 @@ function parseSkillInvocation(text) {
18809
19199
  function candidateRoots(cwd) {
18810
19200
  const roots = [];
18811
19201
  if (cwd) {
18812
- roots.push((0, import_path6.join)(cwd, ".agents", "skills"));
18813
- roots.push((0, import_path6.join)(cwd, ".claude", "skills"));
18814
- roots.push((0, import_path6.join)(cwd, ".codex", "skills"));
18815
- 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)) {
18816
19206
  let entries = [];
18817
19207
  try {
18818
- entries = (0, import_fs6.readdirSync)(cwd, { withFileTypes: true });
19208
+ entries = (0, import_fs7.readdirSync)(cwd, { withFileTypes: true });
18819
19209
  } catch {
18820
19210
  }
18821
19211
  for (const entry of entries) {
18822
19212
  if (!entry.isDirectory()) continue;
18823
19213
  if (entry.name.startsWith(".")) continue;
18824
- const child = (0, import_path6.join)(cwd, entry.name);
18825
- roots.push((0, import_path6.join)(child, ".agents", "skills"));
18826
- roots.push((0, import_path6.join)(child, ".claude", "skills"));
18827
- 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"));
18828
19218
  }
18829
19219
  }
18830
19220
  }
18831
- const home = (0, import_os6.homedir)();
18832
- roots.push((0, import_path6.join)(home, ".agents", "skills"));
18833
- roots.push((0, import_path6.join)(home, ".claude", "skills"));
18834
- 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"));
18835
19225
  return Array.from(new Set(roots));
18836
19226
  }
18837
19227
  function resolveSkillInvocation(text, cwd) {
18838
19228
  const parsed = parseSkillInvocation(text);
18839
19229
  if (!parsed) return null;
18840
19230
  for (const root of candidateRoots(cwd)) {
18841
- const skillPath = (0, import_path6.join)(root, parsed.command, "SKILL.md");
18842
- 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;
18843
19233
  let content;
18844
19234
  try {
18845
- content = (0, import_fs6.readFileSync)(skillPath, "utf-8");
19235
+ content = (0, import_fs7.readFileSync)(skillPath, "utf-8");
18846
19236
  } catch {
18847
19237
  continue;
18848
19238
  }
@@ -19023,11 +19413,19 @@ var BaseMachineAgent = class _BaseMachineAgent {
19023
19413
  async runBackendWithActivityHeartbeat(backend, context) {
19024
19414
  const intervalMs = this.getActivityHeartbeatIntervalMs();
19025
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
+ };
19026
19424
  if (this.presenter.onActivity && intervalMs > 0) {
19027
- void this.presenter.onActivity("background_work");
19425
+ emitHeartbeat();
19028
19426
  heartbeat = setInterval(() => {
19029
19427
  if (!context.abortController.signal.aborted) {
19030
- void this.presenter.onActivity?.("background_work");
19428
+ emitHeartbeat();
19031
19429
  }
19032
19430
  }, intervalMs);
19033
19431
  }
@@ -19076,10 +19474,6 @@ var BaseMachineAgent = class _BaseMachineAgent {
19076
19474
  if (agentPrompt) systemPromptParts.push(agentPrompt);
19077
19475
  const modeHint = getAlanModePrompt(config.mode);
19078
19476
  if (modeHint) systemPromptParts.push(modeHint);
19079
- systemPromptParts.push(ALAN_ASK_USER_PROMPT);
19080
- if (backendKind === "cursor_agent_cli") {
19081
- systemPromptParts.push(ALAN_CURSOR_ASK_USER_OVERLAY);
19082
- }
19083
19477
  const operatorSystemPrompt = config.systemPrompt?.trim();
19084
19478
  if (operatorSystemPrompt) systemPromptParts.push(operatorSystemPrompt);
19085
19479
  const workingDir = config.worktreePath || safeProjectPath;
@@ -19152,6 +19546,7 @@ Prefer relative paths and keep your work scoped to this project.`
19152
19546
  };
19153
19547
  if (lastResult.success || !lastResult.error) break;
19154
19548
  if (this.abortController?.signal.aborted) break;
19549
+ if (lastResult.errorKind === "model_mismatch") break;
19155
19550
  const hadResumeId = runtimeConfig.runtimeSessionId || runtimeConfig.providerSessionId;
19156
19551
  if (hadResumeId && !isTransientError(lastResult.error)) {
19157
19552
  const overrideResult = await this.onSessionResumeFailure(runtimeConfig, lastResult);
@@ -19163,10 +19558,25 @@ Prefer relative paths and keep your work scoped to this project.`
19163
19558
  "[base-machine-agent] Resume failed, retrying without session ID:",
19164
19559
  lastResult.error
19165
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
+ );
19166
19573
  runtimeConfig = {
19167
19574
  ...runtimeConfig,
19168
19575
  runtimeSessionId: void 0,
19169
- providerSessionId: void 0
19576
+ providerSessionId: void 0,
19577
+ ...fallbackContext ? { task: `${fallbackContext}
19578
+
19579
+ ${runtimeConfig.task}` } : {}
19170
19580
  };
19171
19581
  const retryResult = await this.runBackendWithActivityHeartbeat(backend, {
19172
19582
  presenter: this.presenter,
@@ -19174,7 +19584,7 @@ Prefer relative paths and keep your work scoped to this project.`
19174
19584
  abortController: this.abortController,
19175
19585
  cwd,
19176
19586
  env,
19177
- promptText,
19587
+ promptText: fallbackContext ? this.buildPromptText(runtimeConfig) : promptText,
19178
19588
  onProcessSpawned: this.getOnProcessSpawned()
19179
19589
  });
19180
19590
  lastResult = {
@@ -19370,14 +19780,14 @@ function normalizePath(candidate, cwd) {
19370
19780
  } catch {
19371
19781
  }
19372
19782
  if (path.startsWith("~/")) return null;
19373
- if ((0, import_path7.isAbsolute)(path)) return path;
19374
- 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;
19375
19785
  }
19376
19786
  function resolveToolCwd(cwd, toolInput) {
19377
19787
  if (!isRecord(toolInput)) return cwd;
19378
19788
  const raw = typeof toolInput.cwd === "string" ? toolInput.cwd : typeof toolInput.workdir === "string" ? toolInput.workdir : typeof toolInput.workingDirectory === "string" ? toolInput.workingDirectory : null;
19379
19789
  if (!raw) return cwd;
19380
- 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);
19381
19791
  }
19382
19792
  function extractBrowserMediaPathHints(content, cwd, toolName, options = {}, kind = "video") {
19383
19793
  if (!isAgentBrowserMediaInvocation(toolName, content, options.toolInput, kind)) return [];
@@ -19420,19 +19830,19 @@ function extractGeneratedImagesFromToolResult(toolId, content, cwd, toolName, op
19420
19830
  const effectiveCwd = resolveToolCwd(cwd, options.toolInput);
19421
19831
  for (const candidate of extractPathCandidates(content, options)) {
19422
19832
  const path = normalizePath(candidate, effectiveCwd);
19423
- if (!path || seen.has(path) || !(0, import_fs7.existsSync)(path)) continue;
19833
+ if (!path || seen.has(path) || !(0, import_fs8.existsSync)(path)) continue;
19424
19834
  seen.add(path);
19425
- const ext = (0, import_path7.extname)(path).toLowerCase();
19835
+ const ext = (0, import_path8.extname)(path).toLowerCase();
19426
19836
  const mimeType = IMAGE_MIME_BY_EXT[ext];
19427
19837
  if (!mimeType) continue;
19428
- const stat = (0, import_fs7.statSync)(path);
19838
+ const stat = (0, import_fs8.statSync)(path);
19429
19839
  if (!stat.isFile() || stat.size <= 0 || stat.size > MAX_GENERATED_IMAGE_BYTES) continue;
19430
- const buffer = (0, import_fs7.readFileSync)(path);
19840
+ const buffer = (0, import_fs8.readFileSync)(path);
19431
19841
  const { width, height } = readImageDimensions2(buffer, mimeType);
19432
19842
  images.push({
19433
19843
  type: "generated_image",
19434
19844
  id: (0, import_crypto3.randomUUID)(),
19435
- filename: (0, import_path7.basename)(path),
19845
+ filename: (0, import_path8.basename)(path),
19436
19846
  mimeType,
19437
19847
  size: buffer.length,
19438
19848
  width,
@@ -19459,20 +19869,20 @@ function extractSessionMediaFromToolResult(toolId, content, cwd, toolName, optio
19459
19869
  const mimeByExt = kind === "video" ? VIDEO_MIME_BY_EXT : IMAGE_MIME_BY_EXT;
19460
19870
  for (const candidate of extractPathCandidates(content, options, extensionsPattern)) {
19461
19871
  const path = normalizePath(candidate, effectiveCwd);
19462
- if (!path || seen.has(path) || !(0, import_fs7.existsSync)(path)) continue;
19872
+ if (!path || seen.has(path) || !(0, import_fs8.existsSync)(path)) continue;
19463
19873
  seen.add(path);
19464
- const ext = (0, import_path7.extname)(path).toLowerCase();
19874
+ const ext = (0, import_path8.extname)(path).toLowerCase();
19465
19875
  const mimeType = mimeByExt[ext];
19466
19876
  if (!mimeType) continue;
19467
- const stat = (0, import_fs7.statSync)(path);
19877
+ const stat = (0, import_fs8.statSync)(path);
19468
19878
  if (!stat.isFile() || stat.size <= 0 || stat.size > maxBytes) continue;
19469
- const buffer = (0, import_fs7.readFileSync)(path);
19879
+ const buffer = (0, import_fs8.readFileSync)(path);
19470
19880
  const dimensions = kind === "image" ? readImageDimensions2(buffer, mimeType) : null;
19471
19881
  media.push({
19472
19882
  type: "session_media",
19473
19883
  kind,
19474
19884
  id: (0, import_crypto3.randomUUID)(),
19475
- filename: (0, import_path7.basename)(path),
19885
+ filename: (0, import_path8.basename)(path),
19476
19886
  mimeType,
19477
19887
  size: buffer.length,
19478
19888
  width: dimensions?.width,
@@ -19636,6 +20046,10 @@ var CoreAgent = class _CoreAgent extends BaseMachineAgent {
19636
20046
  if (!this.childProcess) return false;
19637
20047
  const stdin = this.childProcess.stdin;
19638
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
+ }
19639
20053
  try {
19640
20054
  let message;
19641
20055
  if (this.presenter.pendingAskUserToolIds?.has(toolId)) {
@@ -19890,6 +20304,9 @@ function decodeKey(encodedKey) {
19890
20304
  function isTerminal(event) {
19891
20305
  return TERMINAL_EVENT_TYPES.has(event.type);
19892
20306
  }
20307
+ function isBackgroundHeartbeat(event) {
20308
+ return event.type === "activity" && event.status === "background_work";
20309
+ }
19893
20310
  function truncateUtf8(value2, maxBytes) {
19894
20311
  const encoded = Buffer.from(value2, "utf8");
19895
20312
  if (encoded.length <= maxBytes) return value2;
@@ -20038,6 +20455,23 @@ var EncryptedEventOutbox = class {
20038
20455
  isDroppable(entry) {
20039
20456
  return entry.eventId !== this.inFlightEventId && entry.kind !== "condensation_marker" && !isTerminal(entry.event);
20040
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
+ }
20041
20475
  ensureCondensationMarker(runId, conversationId) {
20042
20476
  if (this.entries.some((entry) => entry.runId === runId && entry.kind === "condensation_marker")) {
20043
20477
  return;
@@ -20065,7 +20499,7 @@ var EncryptedEventOutbox = class {
20065
20499
  const runIds = preferredRunId ? [preferredRunId] : [...new Set(this.entries.map((entry) => entry.runId))];
20066
20500
  for (const runId of runIds) {
20067
20501
  while (this.entries.filter((entry) => entry.runId === runId && this.isDroppable(entry)).length > this.maxIntermediateEventsPerRun) {
20068
- const drop = this.entries.find((entry) => entry.runId === runId && this.isDroppable(entry));
20502
+ const drop = this.pickDropCandidate((entry) => entry.runId === runId);
20069
20503
  if (!drop) break;
20070
20504
  this.entries = this.entries.filter((entry) => entry.eventId !== drop.eventId);
20071
20505
  lossCountByRunId.set(runId, (lossCountByRunId.get(runId) ?? 0) + 1);
@@ -20073,7 +20507,7 @@ var EncryptedEventOutbox = class {
20073
20507
  }
20074
20508
  }
20075
20509
  while (encryptedFileSize(this.entries) > this.maxEncryptedBytes) {
20076
- const drop = this.entries.find((entry) => this.isDroppable(entry));
20510
+ const drop = this.pickDropCandidate(() => true);
20077
20511
  if (drop) {
20078
20512
  this.entries = this.entries.filter((entry) => entry.eventId !== drop.eventId);
20079
20513
  lossCountByRunId.set(drop.runId, (lossCountByRunId.get(drop.runId) ?? 0) + 1);
@@ -20092,7 +20526,7 @@ var EncryptedEventOutbox = class {
20092
20526
  this.ensureCondensationMarker(runId, lossConversationByRunId.get(runId));
20093
20527
  }
20094
20528
  while (encryptedFileSize(this.entries) > this.maxEncryptedBytes) {
20095
- const drop = this.entries.find((entry) => this.isDroppable(entry));
20529
+ const drop = this.pickDropCandidate(() => true);
20096
20530
  if (!drop) break;
20097
20531
  this.entries = this.entries.filter((entry) => entry.eventId !== drop.eventId);
20098
20532
  lossCountByRunId.set(drop.runId, (lossCountByRunId.get(drop.runId) ?? 0) + 1);
@@ -21017,6 +21451,10 @@ function assertValidExistingToml(path, content) {
21017
21451
  `Existing config is malformed; no changes were applied. Review the backup at ${backupPath}`
21018
21452
  );
21019
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
+ }
21020
21458
  function mergeCodexAlanSection(path, existingContent, desiredContent) {
21021
21459
  if (existingContent === null || existingContent.trim() === "") return desiredContent;
21022
21460
  assertValidExistingToml(path, existingContent);
@@ -21026,7 +21464,7 @@ function mergeCodexAlanSection(path, existingContent, desiredContent) {
21026
21464
  for (const line of lines) {
21027
21465
  const section = line.trim().match(/^\[([^\]]+)]$/)?.[1];
21028
21466
  if (section) {
21029
- insideAlanSection = section === "mcp_servers.alan";
21467
+ insideAlanSection = isCodexAlanTomlSection(section);
21030
21468
  }
21031
21469
  if (!insideAlanSection) retained.push(line);
21032
21470
  }
@@ -21812,7 +22250,7 @@ var RunStartGate = class {
21812
22250
  };
21813
22251
 
21814
22252
  // src/version.ts
21815
- var AGENT_VERSION = "0.1.36";
22253
+ var AGENT_VERSION = "0.1.38";
21816
22254
 
21817
22255
  // src/workspace-relocation.ts
21818
22256
  var import_node_child_process3 = require("child_process");
@@ -22456,7 +22894,12 @@ function getEndpointDefaultsPath() {
22456
22894
  function readConfig() {
22457
22895
  const configPath = getConfigPath();
22458
22896
  if (!(0, import_node_fs12.existsSync)(configPath)) return {};
22459
- return JSON.parse((0, import_node_fs12.readFileSync)(configPath, "utf8"));
22897
+ const parsed = JSON.parse((0, import_node_fs12.readFileSync)(configPath, "utf8"));
22898
+ const sanitized = sanitizeAgentConfig(parsed);
22899
+ if (Object.keys(parsed).some((key) => !Object.hasOwn(sanitized, key))) {
22900
+ writeConfig(sanitized);
22901
+ }
22902
+ return sanitized;
22460
22903
  }
22461
22904
  function readConfigForRegistration() {
22462
22905
  try {
@@ -22479,7 +22922,11 @@ function writeConfig(config) {
22479
22922
  (0, import_node_fs12.mkdirSync)((0, import_node_path10.dirname)(configPath), { recursive: true, mode: 448 });
22480
22923
  const pendingPath = `${configPath}.pending-${process.pid}-${(0, import_node_crypto3.randomBytes)(6).toString("hex")}`;
22481
22924
  try {
22482
- (0, import_node_fs12.writeFileSync)(pendingPath, JSON.stringify(config, null, 2), { mode: 384 });
22925
+ (0, import_node_fs12.writeFileSync)(
22926
+ pendingPath,
22927
+ JSON.stringify(sanitizeAgentConfig(config), null, 2),
22928
+ { mode: 384 }
22929
+ );
22483
22930
  (0, import_node_fs12.chmodSync)(pendingPath, 384);
22484
22931
  (0, import_node_fs12.renameSync)(pendingPath, configPath);
22485
22932
  } finally {
@@ -23159,14 +23606,29 @@ var USER_INTERRUPTED_RESULT = {
23159
23606
  filesModified: [],
23160
23607
  planFilesCreated: [],
23161
23608
  iterations: 0,
23162
- error: "Interrupted by user"
23609
+ error: "Interrupted by user",
23610
+ errorKind: "user"
23163
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
+ }
23164
23625
  function abortActiveAgent(entry, options) {
23165
23626
  const awaitingAsk = (entry.presenter.pendingAskUserToolIds?.size ?? 0) > 0;
23166
23627
  if (awaitingAsk && !options?.userInitiated) {
23167
23628
  return;
23168
23629
  }
23169
- entry.presenter.onComplete(USER_INTERRUPTED_RESULT);
23630
+ const reason = options?.reason ?? (options?.userInitiated ? "user" : "runner_stalled");
23631
+ entry.presenter.onComplete(buildAbortResult(reason));
23170
23632
  entry.agent.kill();
23171
23633
  }
23172
23634
  function isProviderProcessAlive(pid) {
@@ -23275,7 +23737,11 @@ function flushPendingDeliveryLostEvents(input) {
23275
23737
  if (input.pending.length === 0) return;
23276
23738
  for (const item of input.pending) {
23277
23739
  if (input.eventOutbox.hasTerminalEvent(item.runId)) continue;
23278
- 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
+ }
23279
23745
  input.eventOutbox.enqueue(item.conversationId, item.runId, {
23280
23746
  type: "session_error",
23281
23747
  error: DELIVERY_LOST_ERROR
@@ -23299,7 +23765,7 @@ function reconcileActiveRuns(input) {
23299
23765
  input.pushLog?.(`reconciled dead provider pid run=${runId} (awaiting ask, no interrupt)`);
23300
23766
  continue;
23301
23767
  }
23302
- abortActiveAgent(entry);
23768
+ abortActiveAgent(entry, { reason: staleByDeadPid ? "provider_exited" : "runner_stalled" });
23303
23769
  input.activeAgents.delete(runId);
23304
23770
  if (staleByDeadPid) {
23305
23771
  input.pushLog?.(`reconciled dead provider pid run=${runId}`);
@@ -23363,6 +23829,7 @@ var RuntimePresenter = class {
23363
23829
  emitEvent(event) {
23364
23830
  if (this.terminalFenced) return;
23365
23831
  this.touchRunnerActivity?.();
23832
+ if (isBackgroundHeartbeat(event) && !this.socket.connected) return;
23366
23833
  this.eventOutbox.enqueue(this.conversationId, this.runId, event);
23367
23834
  this.eventOutbox.flush(this.socket);
23368
23835
  }
@@ -23390,6 +23857,9 @@ var RuntimePresenter = class {
23390
23857
  onLog(message) {
23391
23858
  console.error(message);
23392
23859
  }
23860
+ onNotice(kind, message) {
23861
+ this.emitEvent({ type: "session_notice", kind, message, ts: Date.now() });
23862
+ }
23393
23863
  onAssistantText(text) {
23394
23864
  this.emitEvent({ type: "text", text, ts: Date.now() });
23395
23865
  }
@@ -23489,8 +23959,8 @@ var RuntimePresenter = class {
23489
23959
  onCheckpoint(id) {
23490
23960
  this.emitEvent({ type: "checkpoint", id });
23491
23961
  }
23492
- onActivity(status) {
23493
- this.emitEvent({ type: "activity", status, ts: Date.now() });
23962
+ onActivity(status, evidence) {
23963
+ this.emitEvent({ type: "activity", status, ts: Date.now(), ...evidence ? { evidence } : {} });
23494
23964
  }
23495
23965
  onOpenBrowserTab(browserId, url2) {
23496
23966
  this.emitEvent({ type: "open_browser_tab", browserId, ...url2 ? { url: url2 } : {} });
@@ -23513,14 +23983,14 @@ async function setupDaemon(args) {
23513
23983
  const setupToken = argValue(args, "--setup-token") ?? process.env.ALAN_RUNTIME_SETUP_TOKEN;
23514
23984
  const token = argValue(args, "--token") ?? process.env.ALAN_SETUP_TOKEN;
23515
23985
  const displayName = argValue(args, "--name") ?? (0, import_node_os7.hostname)();
23516
- const scope = argValue(args, "--scope") ?? process.env.ALAN_RUNTIME_SCOPE ?? "team";
23986
+ const scope = argValue(args, "--scope") ?? process.env.ALAN_RUNTIME_SCOPE ?? (setupToken ? "team" : "user");
23517
23987
  const installationId = argValue(args, "--installation-id") ?? previousConfig.installationId ?? (0, import_node_crypto3.randomUUID)();
23518
23988
  if (scope !== "team" && scope !== "user") {
23519
23989
  throw new Error("setup --scope must be either 'team' or 'user'");
23520
23990
  }
23521
- if (!setupToken && (!teamId || !token)) {
23991
+ if (!setupToken && !token) {
23522
23992
  throw new Error(
23523
- "setup requires --setup-token <token> or --team <team-id> and --token <alan PAT>.\nFor normal setup, copy the setup token from Alan and run: alan-agent setup --setup-token <token>\nFor browser authorization, run: alan-agent login"
23993
+ "setup requires --setup-token <token> or --token <Alan access token>.\nFor normal setup, copy the setup token from Alan and run: alan-agent setup --setup-token <token>\nFor browser authorization, run: alan-agent login"
23524
23994
  );
23525
23995
  }
23526
23996
  const registrationAttemptId = setupToken ? getOrCreateSetupAttemptId(setupToken) : (0, import_node_crypto3.randomUUID)();
@@ -23540,8 +24010,6 @@ async function setupDaemon(args) {
23540
24010
  managementKind: "user_managed",
23541
24011
  hostKind: "daemon",
23542
24012
  lifecycle: "durable",
23543
- ownerType: scope,
23544
- visibility: scope,
23545
24013
  capabilities: discoverCapabilities(),
23546
24014
  metadata: {
23547
24015
  ...await collectRuntimeMetadataWithProviderLimits(),
@@ -23587,7 +24055,6 @@ async function setupDaemon(args) {
23587
24055
  apiUrl,
23588
24056
  wsUrl,
23589
24057
  endpointProfile,
23590
- teamId,
23591
24058
  runtimeId: body.runtime.id,
23592
24059
  runtimeToken: body.runtimeToken,
23593
24060
  runtimeRenewalToken: body.runtimeRenewalToken,
@@ -23697,7 +24164,6 @@ async function loginWithDeviceCode(args) {
23697
24164
  apiUrl,
23698
24165
  wsUrl,
23699
24166
  endpointProfile,
23700
- teamId: body.runtime.teamId ?? void 0,
23701
24167
  runtimeId: body.runtime.id,
23702
24168
  runtimeToken: body.runtimeToken,
23703
24169
  runtimeRenewalToken: body.runtimeRenewalToken,
@@ -24280,16 +24746,22 @@ async function startDaemon(args) {
24280
24746
  flushWorkspaceRelocations();
24281
24747
  }
24282
24748
  }
24283
- if (workspaceReadiness.ready && expectedRepoUrls.length > 0 && !isAlanDefaultWorkspace(cwd) && !workspaceMatchesExpectedRepositories({
24284
- workspacePath: cwd,
24285
- expectedRepoUrls
24286
- })) {
24287
- socket.emit("agent.rejected", {
24288
- runId: payload.runId,
24289
- message: "This folder belongs to a different repository. Choose the correct project folder and try again.",
24290
- code: "workspace_repository_mismatch"
24291
- });
24292
- 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
+ }
24293
24765
  }
24294
24766
  if (workspaceRequired) workspaceAccessTracker.track(cwd, workspaceReadiness);
24295
24767
  const runnerSummary = summarizeRunnerStatus({
@@ -24587,6 +25059,7 @@ async function startDaemon(args) {
24587
25059
  selectedContextWindow: payload.selectedContextWindow ?? null,
24588
25060
  selectedEffortLevel: payload.selectedEffortLevel ?? null,
24589
25061
  providerSessionId: payload.providerSessionId,
25062
+ resumeFallbackContext: payload.resumeFallbackContext,
24590
25063
  taskMeta: payload.taskMeta,
24591
25064
  prMeta: payload.prMeta,
24592
25065
  conversationId: payload.conversationId,
@@ -24623,11 +25096,13 @@ async function startDaemon(args) {
24623
25096
  if (!payload?.runId) return;
24624
25097
  const entry = activeAgents.get(payload.runId);
24625
25098
  if (!entry) return;
24626
- abortActiveAgent(entry, { userInitiated: true });
25099
+ const reason = payload.reason === "watchdog_timeout" ? "watchdog_timeout" : "user";
25100
+ abortActiveAgent(entry, { userInitiated: true, reason });
24627
25101
  activeAgents.delete(payload.runId);
24628
- pushLog(`aborted run=${payload.runId}`);
25102
+ pushLog(`aborted run=${payload.runId} reason=${reason}`);
24629
25103
  console.info("[alan-agent] Agent run aborted", {
24630
25104
  runId: payload.runId,
25105
+ reason,
24631
25106
  agentVersion: AGENT_VERSION
24632
25107
  });
24633
25108
  });
@@ -24635,7 +25110,9 @@ async function startDaemon(args) {
24635
25110
  "agent.tool_response",
24636
25111
  (payload) => {
24637
25112
  if (!payload?.toolId || typeof payload.response !== "string") return;
24638
- 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()];
24639
25116
  const delivered = entries.some(([runId, entry]) => {
24640
25117
  if (!entry || !verifyActiveWorkspaceLease(runId, entry)) return false;
24641
25118
  return entry.agent.sendToolResponse(payload.toolId, payload.response) === true;
@@ -24643,8 +25120,8 @@ async function startDaemon(args) {
24643
25120
  if (!delivered) {
24644
25121
  pushLog(`tool_response missed tool=${payload.toolId}`);
24645
25122
  const fallbackRun = [...activeAgents.entries()][0];
24646
- const resolvedRunId = payload.runId ?? fallbackRun?.[0];
24647
- 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;
24648
25125
  if (conversationId && resolvedRunId) {
24649
25126
  eventOutbox.enqueue(conversationId, resolvedRunId, {
24650
25127
  type: "error",
@@ -25427,12 +25904,18 @@ var WebPresenter = class {
25427
25904
  onLog(message) {
25428
25905
  console.error(message);
25429
25906
  }
25907
+ onNotice(kind, message) {
25908
+ this.wsClient.emitEvent({ type: "session_notice", kind, message, ts: Date.now() });
25909
+ }
25430
25910
  onAssistantText(text) {
25431
25911
  this.wsClient.emitEvent({ type: "text", text, ts: Date.now() });
25432
25912
  }
25433
25913
  onThinking(text) {
25434
25914
  this.wsClient.emitEvent({ type: "thinking", text, ts: Date.now() });
25435
25915
  }
25916
+ hasKnownToolUse(toolId) {
25917
+ return this.toolNamesById.has(toolId);
25918
+ }
25436
25919
  onToolUse(tool, input, toolId, parentToolUseId) {
25437
25920
  this.toolNamesById.set(toolId, tool);
25438
25921
  this.toolInputsById.set(toolId, input);
@@ -25524,8 +26007,13 @@ var WebPresenter = class {
25524
26007
  onCheckpoint(id) {
25525
26008
  this.wsClient.emitEvent({ type: "checkpoint", id });
25526
26009
  }
25527
- onActivity(status) {
25528
- 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
+ });
25529
26017
  }
25530
26018
  onOpenBrowserTab(browserId, url2) {
25531
26019
  this.wsClient.emitEvent({ type: "open_browser_tab", browserId, ...url2 ? { url: url2 } : {} });
@@ -25566,7 +26054,7 @@ var WebPresenter = class {
25566
26054
  };
25567
26055
 
25568
26056
  // src/ws-client.ts
25569
- var import_node_crypto4 = require("crypto");
26057
+ var import_node_crypto5 = require("crypto");
25570
26058
 
25571
26059
  // ../shared/dist/agent-liveness.js
25572
26060
  var AGENT_LIVENESS_PROTOCOL_VERSION = 1;
@@ -25593,7 +26081,117 @@ var agentProbeAckSchema = external_exports.object({
25593
26081
  protocolVersion: external_exports.number().int().positive().optional()
25594
26082
  });
25595
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
+
25596
26193
  // src/ws-client.ts
26194
+ var ACCEPTED_MESSAGE_ID_CACHE_SIZE = 200;
25597
26195
  var WSClient = class {
25598
26196
  constructor(wsUrl, sessionId, taskId, token, callbacks, agentVersion, lifecycle) {
25599
26197
  this.lifecycle = lifecycle;
@@ -25612,16 +26210,63 @@ var WSClient = class {
25612
26210
  upgrade: false,
25613
26211
  extraHeaders: { "ngrok-skip-browser-warning": "1" }
25614
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
+ }
25615
26227
  this.setupListeners(callbacks);
25616
26228
  }
25617
26229
  socket;
25618
26230
  /** Stable id for this agent process; fences stale heartbeats server-side. */
25619
- agentSessionId = (0, import_node_crypto4.randomUUID)();
26231
+ agentSessionId = (0, import_node_crypto5.randomUUID)();
25620
26232
  /** Monotonic heartbeat sequence — advances on every hello + heartbeat. */
25621
26233
  heartbeatSeq = 0;
25622
26234
  heartbeatTimer = null;
25623
26235
  agentVersion;
25624
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
+ }
25625
26270
  currentRunState() {
25626
26271
  try {
25627
26272
  return this.getRunState?.() ?? { idle: true, activeRunIds: [] };
@@ -25658,7 +26303,9 @@ var WSClient = class {
25658
26303
  }
25659
26304
  }
25660
26305
  emitEvent(event) {
25661
- this.socket.emit("agent_event", event);
26306
+ this.eventDispatcher.emit(event, (payload) => {
26307
+ this.socket.emit("agent_event", payload);
26308
+ });
25662
26309
  }
25663
26310
  waitForConnection(timeoutMs = 15e3) {
25664
26311
  return new Promise((resolve6, reject) => {
@@ -25695,11 +26342,13 @@ var WSClient = class {
25695
26342
  this.socket.on("connect", () => {
25696
26343
  this.lifecycle?.info("sandbox_agent_ws_socket_connected", { socketId: this.socket.id });
25697
26344
  this.startHeartbeat();
26345
+ this.eventDispatcher.flush();
25698
26346
  callbacks.onConnect?.();
25699
26347
  });
25700
26348
  this.socket.on("disconnect", (reason) => {
25701
26349
  this.lifecycle?.info("sandbox_agent_ws_socket_disconnected", { reason });
25702
26350
  this.stopHeartbeat();
26351
+ this.eventDispatcher.disconnect();
25703
26352
  callbacks.onDisconnect?.(reason);
25704
26353
  });
25705
26354
  this.socket.on("agent.probe", (_data, ack) => {
@@ -25717,8 +26366,11 @@ var WSClient = class {
25717
26366
  this.socket.on(
25718
26367
  "user_message",
25719
26368
  (data, ack) => {
25720
- ack?.({ ok: true });
25721
- callbacks.onUserMessage({
26369
+ if (data.messageId && this.acceptedMessageIds.has(data.messageId)) {
26370
+ ack?.({ ok: true });
26371
+ return;
26372
+ }
26373
+ const payload = {
25722
26374
  text: data.text,
25723
26375
  images: data.images,
25724
26376
  files: data.files,
@@ -25742,7 +26394,24 @@ var WSClient = class {
25742
26394
  conversationId: data.conversationId,
25743
26395
  teamId: data.teamId,
25744
26396
  currentUser: data.currentUser
25745
- });
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
+ }
25746
26415
  }
25747
26416
  );
25748
26417
  this.socket.on("stop", () => {
@@ -25911,6 +26580,7 @@ async function runSandbox(config) {
25911
26580
  backendKind: activeBackendKind
25912
26581
  });
25913
26582
  currentAgent = agent;
26583
+ wsClient.setCurrentRunId(currentRunId ?? config.runId);
25914
26584
  const correlation = correlationLogFields2({
25915
26585
  taskId: currentTaskId,
25916
26586
  conversationId: currentConversationId,
@@ -26128,7 +26798,7 @@ async function runSandbox(config) {
26128
26798
 
26129
26799
  // src/service-manager.ts
26130
26800
  var import_node_child_process6 = require("child_process");
26131
- var import_node_fs13 = require("fs");
26801
+ var import_node_fs14 = require("fs");
26132
26802
  var import_node_os8 = require("os");
26133
26803
  var import_node_path11 = require("path");
26134
26804
  var SERVICE_LABEL = "ai.tryalan.agent";
@@ -26327,14 +26997,14 @@ function currentServicePlan(args) {
26327
26997
  });
26328
26998
  }
26329
26999
  function writeManifest(path, contents) {
26330
- (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 });
26331
27001
  const pendingPath = `${path}.pending-${process.pid}`;
26332
27002
  try {
26333
- (0, import_node_fs13.writeFileSync)(pendingPath, contents, { mode: 384 });
26334
- (0, import_node_fs13.chmodSync)(pendingPath, 384);
26335
- (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);
26336
27006
  } finally {
26337
- (0, import_node_fs13.rmSync)(pendingPath, { force: true });
27007
+ (0, import_node_fs14.rmSync)(pendingPath, { force: true });
26338
27008
  }
26339
27009
  }
26340
27010
  function runServiceCommand(command) {
@@ -26357,7 +27027,7 @@ function installDaemonService(args = []) {
26357
27027
  const plan = currentServicePlan(args);
26358
27028
  if (plan.manifestPath && plan.manifest) {
26359
27029
  if ((0, import_node_os8.platform)() === "darwin") {
26360
- (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 });
26361
27031
  }
26362
27032
  writeManifest(plan.manifestPath, plan.manifest);
26363
27033
  }
@@ -26367,7 +27037,7 @@ function installDaemonService(args = []) {
26367
27037
  function uninstallDaemonService(args = []) {
26368
27038
  const plan = currentServicePlan(args);
26369
27039
  for (const command of plan.uninstallCommands) runServiceCommand(command);
26370
- 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 });
26371
27041
  console.info("[alan-agent] Per-user daemon service removed");
26372
27042
  }
26373
27043
  function printDaemonServiceStatus(args = []) {