@aiden-ade/sandbox-agent 0.1.37 → 0.1.39

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 +1066 -227
  2. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -3404,7 +3404,7 @@ var require_websocket = __commonJS({
3404
3404
  var http2 = require("http");
3405
3405
  var net = require("net");
3406
3406
  var tls = require("tls");
3407
- var { randomBytes: randomBytes5, createHash: createHash6 } = require("crypto");
3407
+ var { randomBytes: randomBytes5, createHash: createHash7 } = require("crypto");
3408
3408
  var { Duplex, Readable } = require("stream");
3409
3409
  var { URL: URL2 } = require("url");
3410
3410
  var PerMessageDeflate = require_permessage_deflate();
@@ -4061,7 +4061,7 @@ var require_websocket = __commonJS({
4061
4061
  abortHandshake(websocket, socket, "Invalid Upgrade header");
4062
4062
  return;
4063
4063
  }
4064
- const digest = createHash6("sha1").update(key + GUID).digest("base64");
4064
+ const digest = createHash7("sha1").update(key + GUID).digest("base64");
4065
4065
  if (res.headers["sec-websocket-accept"] !== digest) {
4066
4066
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
4067
4067
  return;
@@ -4428,7 +4428,7 @@ var require_websocket_server = __commonJS({
4428
4428
  var EventEmitter = require("events");
4429
4429
  var http2 = require("http");
4430
4430
  var { Duplex } = require("stream");
4431
- var { createHash: createHash6 } = require("crypto");
4431
+ var { createHash: createHash7 } = require("crypto");
4432
4432
  var extension = require_extension();
4433
4433
  var PerMessageDeflate = require_permessage_deflate();
4434
4434
  var subprotocol = require_subprotocol();
@@ -4725,7 +4725,7 @@ var require_websocket_server = __commonJS({
4725
4725
  );
4726
4726
  }
4727
4727
  if (this._state > RUNNING) return abortHandshake(socket, 503);
4728
- const digest = createHash6("sha1").update(key + GUID).digest("base64");
4728
+ const digest = createHash7("sha1").update(key + GUID).digest("base64");
4729
4729
  const headers = [
4730
4730
  "HTTP/1.1 101 Switching Protocols",
4731
4731
  "Upgrade: websocket",
@@ -4867,19 +4867,18 @@ var import_node_os7 = require("os");
4867
4867
  var import_node_path10 = require("path");
4868
4868
 
4869
4869
  // ../shared/dist/constants/agent.js
4870
- var INTERACTIVE_TOOL_NAMES = /* @__PURE__ */ new Set(["askuserquestion", "exitplanmode", "enterplanmode"]);
4870
+ var INTERACTIVE_TOOL_NAMES = /* @__PURE__ */ new Set([
4871
+ "askuserquestion",
4872
+ "askquestion",
4873
+ "exitplanmode",
4874
+ "enterplanmode"
4875
+ ]);
4876
+ var HARNESS_ASK_TOOL_NAMES = /* @__PURE__ */ new Set(["askuserquestion", "askquestion"]);
4871
4877
  function normalizeToolName(name) {
4872
4878
  const lower = name.toLowerCase();
4873
4879
  const mcp = lower.match(/^mcp__[a-z0-9-]+__(.+)$/);
4874
- let bare = mcp ? mcp[1] : lower;
4875
- bare = bare.replace(/[_\s-]/g, "");
4876
- for (const vendor of ["alan", "alan"]) {
4877
- if (bare.startsWith(vendor) && bare.length > vendor.length) {
4878
- bare = bare.slice(vendor.length);
4879
- break;
4880
- }
4881
- }
4882
- return bare;
4880
+ const bare = mcp ? mcp[1] : lower;
4881
+ return bare.replace(/[_\s-]/g, "");
4883
4882
  }
4884
4883
  function toolNameFromInput(input) {
4885
4884
  if (!input || typeof input !== "object")
@@ -4899,16 +4898,13 @@ function inputLooksLikeAskUserQuestion(input) {
4899
4898
  return Array.isArray(args.questions) && args.questions.length > 0;
4900
4899
  }
4901
4900
  function isAskUserQuestionTool(name, input) {
4902
- if (normalizeToolName(name) === "askuserquestion")
4901
+ if (HARNESS_ASK_TOOL_NAMES.has(normalizeToolName(name)))
4903
4902
  return true;
4904
4903
  const nested = toolNameFromInput(input);
4905
- if (nested && nested !== name && normalizeToolName(nested) === "askuserquestion") {
4906
- return true;
4907
- }
4908
- if (normalizeToolName(name) === "mcp" && inputLooksLikeAskUserQuestion(input)) {
4904
+ if (nested && nested !== name && HARNESS_ASK_TOOL_NAMES.has(normalizeToolName(nested))) {
4909
4905
  return true;
4910
4906
  }
4911
- return false;
4907
+ return inputLooksLikeAskUserQuestion(input);
4912
4908
  }
4913
4909
  function isInteractiveToolName(name, input) {
4914
4910
  const normalized = normalizeToolName(name);
@@ -9574,6 +9570,11 @@ var RUNNER_STATUS_PROTOCOL_VERSION = 2;
9574
9570
  var RUNNER_STATUS_MIN_SUPPORTED_PROTOCOL_VERSION = Math.max(1, RUNNER_STATUS_PROTOCOL_VERSION - 1);
9575
9571
  var RUNNER_STATUS_MAX_SUPPORTED_PROTOCOL_VERSION = RUNNER_STATUS_PROTOCOL_VERSION;
9576
9572
  var RUNNER_RECOVERY_GRACE_MS = 2 * 60 * 1e3;
9573
+ var SYSTEM_ABORT_RUN_ERRORS = {
9574
+ watchdog_timeout: "Agent run was stopped by the server after a period of no stream activity",
9575
+ provider_exited: "Agent process exited unexpectedly",
9576
+ runner_stalled: "Agent run stalled with no recent activity and was recovered"
9577
+ };
9577
9578
  function resolveAgentConfigFileName(profile) {
9578
9579
  switch (profile) {
9579
9580
  case "production":
@@ -14510,33 +14511,6 @@ Deliverables persist as Alan artifacts and documents \u2014 not as chat. Plans,
14510
14511
  For non-trivial tasks, log high-signal events as they happen: decisions, changed assumptions, validation results, known-good IDs, blockers, risks, and direction changes. Keep one current task-state summary updated with current state, known-good IDs, open risks, and next action. Do not log routine commands or file reads. Before stopping, ensure the timeline and current state are enough for a human to understand progress and for another agent to resume. If a prior decision is stale, write a newer event that supersedes it.
14511
14512
 
14512
14513
  The specific work you do \u2014 implementation, planning, review, coordination, verification \u2014 is determined by your agent role, described next.`;
14513
- var ALAN_CURSOR_ASK_USER_OVERLAY = `### Cursor runtime note
14514
- Cursor native \`AskQuestion\` does not block stdin for Alan yet. When you need clickable question cards on Cursor, use \`mcp__alan__ask_user_question\` (server tool: \`ask_user_question\`) instead of native AskQuestion.`;
14515
- var ALAN_ASK_USER_PROMPT = `## Asking the user
14516
-
14517
- When you need a decision or information only the user can provide, do not guess \u2014 ask with an interactive ask tool so Alan can render clickable question cards.
14518
-
14519
- ### Tool preference (required)
14520
- 1. Prefer the **native** ask tool when it appears in your tool list (\`AskUserQuestion\`, or the backend's equivalent built-in ask tool).
14521
- 2. Only if native ask is missing, call \`mcp__alan__ask_user_question\` (server tool name: \`ask_user_question\`).
14522
-
14523
- Required arguments (both native and MCP forms):
14524
- - \`questions\`: non-empty array. Each item needs a clear \`question\` string.
14525
- - Prefer 2\u20135 selectable \`options\` per question (\`label\` required; optional \`value\` / \`description\`). Free-text-only questions are allowed when options do not fit.
14526
- - Do NOT include an "Other" option \u2014 the UI always adds a freeform Other\u2026 field.
14527
- - Set \`multiSelect: true\` on a question when the user may pick more than one option; omit or set false for single-select (default).
14528
- - Optional \`header\` per question and optional \`title\` for the card header.
14529
-
14530
- If neither native ask nor the MCP ask tool is available, ask ONE short plain-text question \u2014 do not invent cards or pretend the UI exists.
14531
-
14532
- ### Never do this
14533
- - Do NOT paste multiple-choice lists, A/B/C tables, or "reply with 1A / 1B" in chat.
14534
- - Do NOT say "answer in the UI", "see the cards", "questions are above", or similar unless you just successfully called an ask tool with real \`questions\` in this turn.
14535
- - Do NOT call the tool with empty \`questions\`, placeholder text, or missing options when choices exist.
14536
- - Do NOT invent defaults when the user skips. If the next user message says they skipped, continue without those answers (or re-ask only if truly required).
14537
-
14538
- ### After the tool call
14539
- The ask tool returns an empty/ack result and does NOT wait. Immediately end your turn. At most one short line like "Please pick an option above." \u2014 no restating the full questions in markdown. The user's answer (or skip) arrives as the next user message; continue from there.`;
14540
14514
  function buildAlanTeamScopePrompt(teamId) {
14541
14515
  return [
14542
14516
  `You are operating within team \`${teamId}\`.`,
@@ -15228,20 +15202,23 @@ var import_path3 = require("path");
15228
15202
  var import_readline = require("readline");
15229
15203
  var import_child_process2 = require("child_process");
15230
15204
  var import_crypto2 = require("crypto");
15231
- var import_child_process3 = require("child_process");
15232
- var import_util10 = require("util");
15233
15205
  var import_fs4 = require("fs");
15234
15206
  var import_os4 = require("os");
15235
15207
  var import_path4 = require("path");
15208
+ var import_child_process3 = require("child_process");
15209
+ var import_util10 = require("util");
15236
15210
  var import_fs5 = require("fs");
15237
15211
  var import_os5 = require("os");
15238
15212
  var import_path5 = require("path");
15239
15213
  var import_fs6 = require("fs");
15240
15214
  var import_os6 = require("os");
15241
15215
  var import_path6 = require("path");
15242
- var import_crypto3 = require("crypto");
15243
15216
  var import_fs7 = require("fs");
15217
+ var import_os7 = require("os");
15244
15218
  var import_path7 = require("path");
15219
+ var import_crypto3 = require("crypto");
15220
+ var import_fs8 = require("fs");
15221
+ var import_path8 = require("path");
15245
15222
  var MANAGED_ALAN_MCP_SERVER_NAMES = [
15246
15223
  "alan",
15247
15224
  "alan-prod",
@@ -15736,10 +15713,111 @@ function getClaudePermissionMode(config) {
15736
15713
  function getClaudeDisallowedTools(config) {
15737
15714
  return isReadOnlyAgent(config.agentId) ? ["Edit", "Write", "MultiEdit", "NotebookEdit"] : [];
15738
15715
  }
15716
+ function getCodexPermissionArgs(config) {
15717
+ const sandbox = shouldUseReadOnlyRuntimePermissions(config) ? "read-only" : "danger-full-access";
15718
+ return ["--ask-for-approval", "never", "--sandbox", sandbox];
15719
+ }
15720
+ var ERROR_SPECS = {
15721
+ model_mismatch: {
15722
+ message: "The selected model isn't available, or you don't have access to it. Pick a different model, then send your message again.",
15723
+ recoveryClass: "user_fixable"
15724
+ },
15725
+ auth_invalid: {
15726
+ message: "API key invalid or expired. Check your provider settings, then retry.",
15727
+ recoveryClass: "user_fixable"
15728
+ },
15729
+ not_logged_in: {
15730
+ message: "Not logged in to this provider. Sign in from provider settings, then retry.",
15731
+ recoveryClass: "user_fixable"
15732
+ },
15733
+ not_authenticated: {
15734
+ message: "The agent CLI isn't signed in on this runtime. Authenticate it there, then retry.",
15735
+ recoveryClass: "needs_env"
15736
+ },
15737
+ auth_expired: {
15738
+ message: "The provider connection expired. Reconnect it in Cloud settings, then restart or recreate the machine.",
15739
+ recoveryClass: "user_fixable"
15740
+ },
15741
+ subscription_required: {
15742
+ message: "This model requires a paid subscription. Switch to a supported model or upgrade your plan.",
15743
+ recoveryClass: "user_fixable"
15744
+ },
15745
+ quota_exceeded: {
15746
+ message: "API quota exceeded. Wait for it to reset or check your billing, then retry.",
15747
+ recoveryClass: "user_fixable"
15748
+ },
15749
+ usage_limit: {
15750
+ message: "Provider usage limit reached. Switch model or wait for the limit to reset, then retry.",
15751
+ recoveryClass: "user_fixable"
15752
+ },
15753
+ context_length: {
15754
+ message: "Context window exceeded \u2014 the conversation is too long. Start a new session or send a shorter message.",
15755
+ recoveryClass: "user_fixable"
15756
+ },
15757
+ rate_limited: {
15758
+ message: "Rate limited by the provider. Retrying automatically shortly.",
15759
+ recoveryClass: "auto_recovering"
15760
+ },
15761
+ overloaded: {
15762
+ message: "The provider is overloaded. Retrying automatically.",
15763
+ recoveryClass: "auto_recovering"
15764
+ },
15765
+ network: {
15766
+ message: "Network connection failed. Check your internet connection \u2014 retrying automatically.",
15767
+ recoveryClass: "auto_recovering"
15768
+ },
15769
+ provider_timeout: {
15770
+ message: "The provider timed out while continuing the turn. Send your message again; if it repeats, check network/API latency or reduce slow tool calls.",
15771
+ recoveryClass: "retry"
15772
+ },
15773
+ resume_failed: {
15774
+ message: "The previous session could not be resumed \u2014 its stored session was missing or expired. The next message starts a fresh session without earlier context.",
15775
+ recoveryClass: "retry"
15776
+ },
15777
+ permission_denied: {
15778
+ message: "Permission denied. Check file or command permissions on the runtime.",
15779
+ recoveryClass: "needs_env"
15780
+ },
15781
+ out_of_memory: {
15782
+ message: "Out of memory on the runtime. Close other work or use a larger machine, then retry.",
15783
+ recoveryClass: "needs_env"
15784
+ },
15785
+ provider_error: {
15786
+ message: "The provider returned an error. Send your message again to retry.",
15787
+ recoveryClass: "retry"
15788
+ },
15789
+ unknown_cli_error: {
15790
+ message: "The agent stopped unexpectedly. Send your message again to retry.",
15791
+ recoveryClass: "retry"
15792
+ }
15793
+ };
15794
+ function specForErrorKind(errorKind) {
15795
+ const spec = ERROR_SPECS[errorKind];
15796
+ return { message: spec.message, errorKind, recoveryClass: spec.recoveryClass };
15797
+ }
15739
15798
  function isLikelyProviderAuthError(stderr) {
15740
15799
  const lower = stderr.toLowerCase();
15741
15800
  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");
15742
15801
  }
15802
+ function mapProviderApiError(errorCode, apiStatus) {
15803
+ const code = errorCode?.trim().toLowerCase();
15804
+ if (code) {
15805
+ if (code.includes("model_not_found") || code.includes("model_not_available")) {
15806
+ return "model_mismatch";
15807
+ }
15808
+ if (code.includes("authentication") || code.includes("permission_error")) return "auth_invalid";
15809
+ if (code.includes("rate_limit")) return "rate_limited";
15810
+ if (code.includes("overloaded")) return "overloaded";
15811
+ if (code.includes("insufficient_quota") || code.includes("billing")) return "quota_exceeded";
15812
+ }
15813
+ if (typeof apiStatus === "number") {
15814
+ if (apiStatus === 404) return "model_mismatch";
15815
+ if (apiStatus === 401 || apiStatus === 403) return "auth_invalid";
15816
+ if (apiStatus === 429) return "rate_limited";
15817
+ if (apiStatus >= 500) return "overloaded";
15818
+ }
15819
+ return null;
15820
+ }
15743
15821
  function normalizeCodexCliErrorMessage(message) {
15744
15822
  const lower = message.toLowerCase();
15745
15823
  if (lower.includes("reconnecting") && lower.includes("request timed out")) {
@@ -15750,40 +15828,73 @@ function normalizeCodexCliErrorMessage(message) {
15750
15828
  }
15751
15829
  return message;
15752
15830
  }
15753
- function classifyCliError(stderr, exitCode) {
15831
+ function classifyCliErrorDetailed(stderr, _exitCode, opts) {
15754
15832
  const normalizedCodexError = normalizeCodexCliErrorMessage(stderr);
15755
- if (normalizedCodexError !== stderr) return normalizedCodexError;
15756
- const lower = stderr.toLowerCase();
15757
- if (lower.includes("authentication required") || lower.includes("not authenticated"))
15758
- return "Cursor Agent is not authenticated. Log in with `cursor-agent login` on this machine, then retry.";
15759
- if (lower.includes("out of usage") || lower.includes("increase your limit"))
15760
- return "Provider usage limit reached. Switch model/plan or wait for the limit to reset, then retry.";
15761
- if (lower.includes("no chat found") || lower.includes("chat not found") || lower.includes("session not found") || lower.includes("could not resume"))
15762
- return "The previous session could not be resumed \u2014 the CLI's stored session was missing or expired. The next message starts a fresh session without earlier context.";
15763
- if (lower.includes("rate_limit") || lower.includes("rate limit") || lower.includes("429"))
15764
- return "Rate limited by API. Try again shortly.";
15765
- if (isLikelyProviderAuthError(stderr))
15766
- return "API key invalid or expired. Check your provider settings.";
15767
- if (lower.includes("not logged in") || lower.includes("login required") || lower.includes("please sign in") || lower.includes("please log in") || lower.includes("sign in to") || lower.includes("log in to"))
15768
- return "Not logged in. Please authenticate with this provider first.";
15769
- if (lower.includes("model not supported") || lower.includes("unsupported model") || lower.includes("model not found") || lower.includes("model not available") || lower.includes("model is not available") || lower.includes("invalid model") || lower.includes("unknown model") || lower.includes("no such model") || lower.includes("llm not set"))
15770
- return "Model not supported by this provider. Check your provider's model list.";
15771
- if (lower.includes("subscription") || lower.includes("paid plan") || lower.includes("plan required") || lower.includes("upgrade your plan") || lower.includes("only supports free") || lower.includes("not available on your") || lower.includes("requires a paid"))
15772
- return "This model requires a paid subscription. Switch to a supported model or upgrade your plan.";
15773
- if (lower.includes("quota exceeded") || lower.includes("quota limit") || lower.includes("daily limit") || lower.includes("monthly limit") || lower.includes("usage limit"))
15774
- return "API quota exceeded. Wait for it to reset or check your billing.";
15775
- if (lower.includes("overloaded") || lower.includes("503") || lower.includes("service unavailable"))
15776
- return "API is overloaded. Will retry automatically.";
15777
- if (lower.includes("context_length") || lower.includes("too long") || lower.includes("max tokens") || lower.includes("context window"))
15778
- return "Context window exceeded. Try a shorter prompt or start a new session.";
15779
- if (lower.includes("econnrefused") || lower.includes("network") || lower.includes("enotfound") || lower.includes("timeout") || lower.includes("etimedout"))
15780
- return "Network connection failed. Check your internet connection.";
15781
- if (lower.includes("permission denied") || lower.includes("eacces"))
15782
- return "Permission denied. Check file or command permissions.";
15783
- if (lower.includes("out of memory") || lower.includes("enomem"))
15784
- return "Out of memory. Close other applications and try again.";
15833
+ if (normalizedCodexError !== stderr) {
15834
+ const kind = /timed out/i.test(normalizedCodexError) ? "provider_timeout" : "auth_expired";
15835
+ return {
15836
+ message: normalizedCodexError,
15837
+ errorKind: kind,
15838
+ recoveryClass: ERROR_SPECS[kind].recoveryClass
15839
+ };
15840
+ }
15841
+ const haystack = [stderr, ...opts?.extraSignals ?? []].join("\n").toLowerCase();
15842
+ const has = (...needles) => needles.some((n) => haystack.includes(n));
15843
+ if (has("authentication required", "not authenticated"))
15844
+ return specForErrorKind("not_authenticated");
15845
+ if (has("out of usage", "increase your limit")) return specForErrorKind("usage_limit");
15846
+ if (has("no chat found", "chat not found", "session not found", "could not resume"))
15847
+ return specForErrorKind("resume_failed");
15848
+ if (has("rate_limit", "rate limit", "429")) return specForErrorKind("rate_limited");
15849
+ if (isLikelyProviderAuthError(haystack)) return specForErrorKind("auth_invalid");
15850
+ if (has(
15851
+ "not logged in",
15852
+ "login required",
15853
+ "please sign in",
15854
+ "please log in",
15855
+ "sign in to",
15856
+ "log in to"
15857
+ ))
15858
+ return specForErrorKind("not_logged_in");
15859
+ if (has(
15860
+ "model not supported",
15861
+ "unsupported model",
15862
+ "model not found",
15863
+ "model not available",
15864
+ "model is not available",
15865
+ "invalid model",
15866
+ "unknown model",
15867
+ "no such model",
15868
+ "llm not set",
15869
+ // Claude Code phrasings (safety net; the structured decoder catches these first).
15870
+ "issue with the selected model",
15871
+ "may not exist or you may not have access"
15872
+ ))
15873
+ return specForErrorKind("model_mismatch");
15874
+ if (has(
15875
+ "subscription",
15876
+ "paid plan",
15877
+ "plan required",
15878
+ "upgrade your plan",
15879
+ "only supports free",
15880
+ "not available on your",
15881
+ "requires a paid"
15882
+ ))
15883
+ return specForErrorKind("subscription_required");
15884
+ if (has("quota exceeded", "quota limit", "daily limit", "monthly limit", "usage limit"))
15885
+ return specForErrorKind("quota_exceeded");
15886
+ if (has("overloaded", "503", "service unavailable")) return specForErrorKind("overloaded");
15887
+ if (has("context_length", "too long", "max tokens", "context window"))
15888
+ return specForErrorKind("context_length");
15889
+ if (has("econnrefused", "network", "enotfound", "timeout", "etimedout"))
15890
+ return specForErrorKind("network");
15891
+ if (has("permission denied", "eacces")) return specForErrorKind("permission_denied");
15892
+ if (has("out of memory", "enomem")) return specForErrorKind("out_of_memory");
15785
15893
  const raw = stderr.trim().slice(0, 200);
15786
- return raw || `Process exited with code ${exitCode}`;
15894
+ if (raw) {
15895
+ return { message: raw, errorKind: "unknown_cli_error", recoveryClass: "unknown" };
15896
+ }
15897
+ return specForErrorKind("unknown_cli_error");
15787
15898
  }
15788
15899
  function isTransientError(errorMessage) {
15789
15900
  const lower = errorMessage.toLowerCase();
@@ -15832,6 +15943,16 @@ function spawnCli(command, args, context) {
15832
15943
  detached: process.platform !== "win32"
15833
15944
  });
15834
15945
  }
15946
+ function emitSessionNotice(presenter, kind, message) {
15947
+ if (presenter.onNotice) {
15948
+ void presenter.onNotice(kind, message);
15949
+ return;
15950
+ }
15951
+ presenter.onLog(`[session_notice:${kind}] ${message}`);
15952
+ }
15953
+ function logResumeObservability(event, fields) {
15954
+ console.info(`[agent-resume] ${event}`, { event, ...fields });
15955
+ }
15835
15956
  function structuredEventKey(event) {
15836
15957
  const type = typeof event.type === "string" && event.type ? event.type : "";
15837
15958
  const role = typeof event.role === "string" && event.role ? event.role : "";
@@ -15901,7 +16022,12 @@ function createGenericCliBackend(options) {
15901
16022
  const args = options.buildArgs?.(context, prompt) ?? options.args.map((arg) => arg === "{{prompt}}" ? prompt : arg);
15902
16023
  const child = spawnCli(options.command, args, context);
15903
16024
  state.process = child;
16025
+ state.lastRawOutputAtMs = Date.now();
15904
16026
  context.onProcessSpawned?.(child);
16027
+ context.registerLivenessProbe?.(() => ({
16028
+ providerAlive: child.exitCode === null,
16029
+ lastRawOutputAgoMs: typeof state.lastRawOutputAtMs === "number" ? Date.now() - state.lastRawOutputAtMs : null
16030
+ }));
15905
16031
  const safeArgs = args.map(
15906
16032
  (a, i) => i > 0 && args[i - 1] === "--system-prompt" ? `"<system-prompt ${a.length} chars>"` : a
15907
16033
  );
@@ -15950,6 +16076,7 @@ function createGenericCliBackend(options) {
15950
16076
  }
15951
16077
  stdoutRl.on("line", (line) => {
15952
16078
  sawStdoutLine = true;
16079
+ state.lastRawOutputAtMs = Date.now();
15953
16080
  presenter.recordRawTranscript?.("stdout", line);
15954
16081
  if (options.parseStructuredLine) {
15955
16082
  options.parseStructuredLine(line, context, state);
@@ -15967,6 +16094,7 @@ function createGenericCliBackend(options) {
15967
16094
  }
15968
16095
  });
15969
16096
  stderrRl.on("line", (line) => {
16097
+ state.lastRawOutputAtMs = Date.now();
15970
16098
  presenter.recordRawTranscript?.("stderr", line);
15971
16099
  if (options.parseStderrLine) {
15972
16100
  options.parseStderrLine(line, context, state);
@@ -16013,7 +16141,7 @@ function createGenericCliBackend(options) {
16013
16141
  if (presenter.pendingAskUserToolIds?.size) {
16014
16142
  idleTimeoutReason = "pending_user_answer";
16015
16143
  resolve22("idle_timeout");
16016
- } else if (state.activeBackgroundTaskIds?.size) {
16144
+ } else if (state.resultDeferredOnBackgroundWork && state.activeBackgroundTaskIds?.size) {
16017
16145
  idleTimeoutReason = "background_task";
16018
16146
  resolve22("idle_timeout");
16019
16147
  } else {
@@ -16076,10 +16204,26 @@ function createGenericCliBackend(options) {
16076
16204
  logUnhandledStructuredEventSummary(options.kind, state);
16077
16205
  }
16078
16206
  const requestedResumeId = context.config.runtimeSessionId?.trim() || context.config.providerSessionId?.trim();
16207
+ let cursorResumeSilentlyFailed = false;
16079
16208
  if (requestedResumeId && state.runtimeSessionId && state.runtimeSessionId !== requestedResumeId) {
16080
16209
  console.warn(
16081
16210
  `[${options.kind}] CLI reported session id ${state.runtimeSessionId} after resume was requested with ${requestedResumeId} \u2014 expected for claude session forks, a failed-resume signal for cursor-agent`
16082
16211
  );
16212
+ if (options.kind === "cursor_agent_cli") {
16213
+ cursorResumeSilentlyFailed = true;
16214
+ logResumeObservability("agent_resume_failed", {
16215
+ backendKind: options.kind,
16216
+ reason: "cli_forked_fresh_session",
16217
+ requestedResumeId,
16218
+ runtimeSessionId: state.runtimeSessionId,
16219
+ hadFallbackContext: Boolean(context.config.resumeFallbackContext?.trim())
16220
+ });
16221
+ emitSessionNotice(
16222
+ context.presenter,
16223
+ "session_resume_failed",
16224
+ "The previous session could not be resumed, so this reply was generated without earlier conversation context. Retrying with the recent history re-included."
16225
+ );
16226
+ }
16083
16227
  }
16084
16228
  if (context.abortController.signal.aborted) {
16085
16229
  return {
@@ -16173,14 +16317,51 @@ function createGenericCliBackend(options) {
16173
16317
  const stderrText = stderrLines.join("\n").trim();
16174
16318
  const hasStructuredError = exitCode === 0 && !!state.error?.trim();
16175
16319
  const failed = exitCode !== 0 || hasStructuredError;
16320
+ if (cursorResumeSilentlyFailed && !failed) {
16321
+ return {
16322
+ success: false,
16323
+ summary: state.summary.trim() || "Session resume failed",
16324
+ filesModified: [],
16325
+ planFilesCreated: [],
16326
+ iterations: Math.max(state.iterations, 1),
16327
+ error: SESSION_RESUME_FAILED_MESSAGE,
16328
+ errorKind: "resume_failed",
16329
+ providerSessionId: state.runtimeSessionId,
16330
+ runtimeSessionId: state.runtimeSessionId,
16331
+ backendKind: options.kind,
16332
+ supportTier: options.supportTier,
16333
+ usage: state.usage
16334
+ };
16335
+ }
16176
16336
  const summary = state.summary.trim() || state.error?.trim() || (failed ? "Task failed" : "Task completed");
16337
+ let classifiedError;
16338
+ let classifiedErrorKind;
16339
+ let classifiedRecoveryClass;
16340
+ if (failed) {
16341
+ if (state.errorKind && state.error?.trim()) {
16342
+ classifiedError = state.error.trim();
16343
+ classifiedErrorKind = state.errorKind;
16344
+ classifiedRecoveryClass = state.recoveryClass;
16345
+ } else {
16346
+ const detailed = classifyCliErrorDetailed(
16347
+ state.error?.trim() || stderrText || "",
16348
+ exitCode,
16349
+ { extraSignals: state.summary ? [state.summary] : void 0 }
16350
+ );
16351
+ classifiedError = detailed.message;
16352
+ classifiedErrorKind = detailed.errorKind;
16353
+ classifiedRecoveryClass = detailed.recoveryClass;
16354
+ }
16355
+ }
16177
16356
  return {
16178
16357
  success: !failed,
16179
16358
  summary,
16180
16359
  filesModified: [],
16181
16360
  planFilesCreated: [],
16182
16361
  iterations: Math.max(state.iterations, 1),
16183
- error: failed ? classifyCliError(state.error?.trim() || stderrText || "", exitCode) : void 0,
16362
+ error: classifiedError,
16363
+ ...classifiedErrorKind ? { errorKind: classifiedErrorKind } : {},
16364
+ ...classifiedRecoveryClass ? { recoveryClass: classifiedRecoveryClass } : {},
16184
16365
  providerSessionId: state.runtimeSessionId,
16185
16366
  runtimeSessionId: state.runtimeSessionId,
16186
16367
  backendKind: options.kind,
@@ -16622,6 +16803,21 @@ function createAntigravityCliBackend(command = "agy", defaultArgs = []) {
16622
16803
  }
16623
16804
  };
16624
16805
  }
16806
+ function resolveClaudeHome(env, variant = "claude") {
16807
+ if (variant === "supatest") {
16808
+ return (0, import_path4.join)((0, import_os4.homedir)(), ".supatest", "claude-internal");
16809
+ }
16810
+ return env.CLAUDE_CONFIG_DIR || process.env.CLAUDE_CONFIG_DIR || (0, import_path4.join)((0, import_os4.homedir)(), ".claude");
16811
+ }
16812
+ function encodeClaudeProjectDir(cwd) {
16813
+ return cwd.replace(/[^a-zA-Z0-9]/g, "-");
16814
+ }
16815
+ function claudeSessionLogExists(input) {
16816
+ if (!input.sessionId || !input.cwd) return false;
16817
+ const projectDir = (0, import_path4.join)(input.home, "projects", encodeClaudeProjectDir(input.cwd));
16818
+ if (!(0, import_fs4.existsSync)(projectDir)) return false;
16819
+ return (0, import_fs4.existsSync)((0, import_path4.join)(projectDir, `${input.sessionId}.jsonl`));
16820
+ }
16625
16821
  var execFileAsync = (0, import_util10.promisify)(import_child_process3.execFile);
16626
16822
  var flagSupportCache = /* @__PURE__ */ new Map();
16627
16823
  function helpAdvertisesFlag(helpText, flag) {
@@ -16672,15 +16868,38 @@ function registerBackgroundTaskIds(state, keys) {
16672
16868
  state.activeBackgroundTaskIds.add(key);
16673
16869
  }
16674
16870
  }
16675
- function registerBackgroundTaskRecord(state, record) {
16871
+ function registerBackgroundLauncher(state, toolUseId) {
16872
+ registerBackgroundTaskIds(state, [toolUseId]);
16873
+ if (!state.pendingLauncherToolIds) state.pendingLauncherToolIds = [];
16874
+ if (!state.pendingLauncherToolIds.includes(toolUseId)) {
16875
+ state.pendingLauncherToolIds.push(toolUseId);
16876
+ }
16877
+ }
16878
+ function drainNextUnlinkedBackgroundLauncher(state) {
16879
+ const launcherId = state.pendingLauncherToolIds?.shift();
16880
+ if (state.pendingLauncherToolIds?.length === 0) state.pendingLauncherToolIds = void 0;
16881
+ if (launcherId) clearBackgroundTaskIds(state, [launcherId]);
16882
+ return launcherId;
16883
+ }
16884
+ function registerBackgroundTaskRecord(state, record, launcherToolUseId) {
16676
16885
  const taskId = backgroundTaskId(record);
16677
- let toolUseId = backgroundToolUseId(record);
16678
- if (!toolUseId && taskId && state.activeBackgroundTaskIds?.size === 1) {
16679
- const [candidate] = state.activeBackgroundTaskIds;
16680
- if (candidate && candidate !== taskId) toolUseId = candidate;
16886
+ let toolUseId = backgroundToolUseId(record) ?? launcherToolUseId ?? void 0;
16887
+ if (!toolUseId && taskId) {
16888
+ const nextLauncher = state.pendingLauncherToolIds?.[0];
16889
+ if (nextLauncher) {
16890
+ toolUseId = nextLauncher;
16891
+ } else if (state.activeBackgroundTaskIds?.size === 1) {
16892
+ const [candidate] = state.activeBackgroundTaskIds;
16893
+ if (candidate && candidate !== taskId) toolUseId = candidate;
16894
+ }
16681
16895
  }
16682
16896
  registerBackgroundTaskIds(state, backgroundTaskKeys(record));
16683
16897
  if (!taskId || !toolUseId) return;
16898
+ if (state.pendingLauncherToolIds) {
16899
+ const idx = state.pendingLauncherToolIds.indexOf(toolUseId);
16900
+ if (idx >= 0) state.pendingLauncherToolIds.splice(idx, 1);
16901
+ if (state.pendingLauncherToolIds.length === 0) state.pendingLauncherToolIds = void 0;
16902
+ }
16684
16903
  if (!state.backgroundToolIdByTaskId) state.backgroundToolIdByTaskId = /* @__PURE__ */ new Map();
16685
16904
  if (!state.backgroundTaskIdsByToolId) state.backgroundTaskIdsByToolId = /* @__PURE__ */ new Map();
16686
16905
  state.backgroundToolIdByTaskId.set(taskId, toolUseId);
@@ -16711,6 +16930,10 @@ function clearBackgroundTaskIds(state, keys) {
16711
16930
  state.backgroundTaskIdsByToolId?.delete(key);
16712
16931
  }
16713
16932
  }
16933
+ if (state.pendingLauncherToolIds) {
16934
+ state.pendingLauncherToolIds = state.pendingLauncherToolIds.filter((id) => activeIds.has(id));
16935
+ if (state.pendingLauncherToolIds.length === 0) state.pendingLauncherToolIds = void 0;
16936
+ }
16714
16937
  if (activeIds.size === 0) state.activeBackgroundTaskIds = void 0;
16715
16938
  }
16716
16939
  function resolveBackgroundTaskToolUseId(state, record) {
@@ -16733,6 +16956,7 @@ function deferOrFinalizeStructuredResult(kind, context, state) {
16733
16956
  console.info(
16734
16957
  `[${kind}] Keeping stdin open \u2014 ${pendingAsks} interactive question(s) awaiting user response`
16735
16958
  );
16959
+ state.resultDeferredOnPendingAnswer = true;
16736
16960
  return "deferred_pending_answer";
16737
16961
  }
16738
16962
  const activeBackgroundTasks = state.activeBackgroundTaskIds?.size ?? 0;
@@ -16747,9 +16971,10 @@ function deferOrFinalizeStructuredResult(kind, context, state) {
16747
16971
  return "finalized";
16748
16972
  }
16749
16973
  function maybeFinalizeDeferredResult(kind, context, state) {
16750
- if (!state.resultDeferredOnBackgroundWork) return;
16974
+ if (!state.resultDeferredOnBackgroundWork && !state.resultDeferredOnPendingAnswer) return;
16751
16975
  if (context.presenter.pendingAskUserToolIds?.size || state.activeBackgroundTaskIds?.size) return;
16752
16976
  state.resultDeferredOnBackgroundWork = false;
16977
+ state.resultDeferredOnPendingAnswer = false;
16753
16978
  finalizeStructuredResult(kind, state);
16754
16979
  }
16755
16980
  var DEBUG_ASK = process.env.ALAN_DEBUG_ASK === "1" || process.env.ALAN_DEBUG === "1";
@@ -16810,6 +17035,9 @@ function handleClaudeStructuredEvent(parsed, context, state) {
16810
17035
  if (typeof parsed.session_id === "string") {
16811
17036
  state.runtimeSessionId = parsed.session_id;
16812
17037
  }
17038
+ if (typeof parsed.error === "string" && parsed.error.trim()) {
17039
+ state.providerErrorCode = parsed.error.trim();
17040
+ }
16813
17041
  const parentToolUseId = typeof parsed.parent_tool_use_id === "string" ? parsed.parent_tool_use_id : null;
16814
17042
  switch (type) {
16815
17043
  case "system": {
@@ -16819,21 +17047,44 @@ function handleClaudeStructuredEvent(parsed, context, state) {
16819
17047
  return true;
16820
17048
  }
16821
17049
  if (parsed.subtype === "task_started") {
16822
- registerBackgroundTaskRecord(state, parsed);
17050
+ registerBackgroundTaskRecord(state, parsed, parentToolUseId);
17051
+ return true;
17052
+ }
17053
+ if (parsed.subtype === "compact_boundary") {
17054
+ const compactMetadata = typeof parsed.compact_metadata === "object" && parsed.compact_metadata !== null ? parsed.compact_metadata : {};
17055
+ const trigger = typeof compactMetadata.trigger === "string" ? compactMetadata.trigger : typeof parsed.trigger === "string" ? parsed.trigger : "auto";
17056
+ const preTokens = typeof compactMetadata.pre_tokens === "number" ? compactMetadata.pre_tokens : typeof parsed.pre_tokens === "number" ? parsed.pre_tokens : void 0;
17057
+ const tokensPart = typeof preTokens === "number" && preTokens > 0 ? ` (${Math.round(preTokens / 1e3)}k tokens summarized)` : "";
17058
+ emitSessionNotice(
17059
+ presenter,
17060
+ "context_compacted",
17061
+ 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.`
17062
+ );
16823
17063
  return true;
16824
17064
  }
16825
17065
  if (parsed.subtype === "task_notification") {
16826
17066
  if (isTerminalTaskNotification(parsed)) {
16827
17067
  const toolUseId = resolveBackgroundTaskToolUseId(state, parsed);
16828
17068
  const wasActive = !!toolUseId && state.activeBackgroundTaskIds?.has(toolUseId) ? true : backgroundTaskKeys(parsed).some((key) => state.activeBackgroundTaskIds?.has(key));
17069
+ const isFailed = isFailedTaskNotification(parsed) ? true : void 0;
16829
17070
  if (toolUseId && wasActive) {
16830
17071
  void presenter.onToolResult?.(
16831
17072
  toolUseId,
16832
17073
  formatClaudeTaskNotificationResult(parsed),
16833
- isFailedTaskNotification(parsed) ? true : void 0
17074
+ isFailed
16834
17075
  );
17076
+ clearBackgroundTaskIds(state, backgroundTaskKeys(parsed));
17077
+ } else {
17078
+ clearBackgroundTaskIds(state, backgroundTaskKeys(parsed));
17079
+ const drainedLauncher = drainNextUnlinkedBackgroundLauncher(state);
17080
+ if (drainedLauncher) {
17081
+ void presenter.onToolResult?.(
17082
+ drainedLauncher,
17083
+ formatClaudeTaskNotificationResult(parsed),
17084
+ isFailed
17085
+ );
17086
+ }
16835
17087
  }
16836
- clearBackgroundTaskIds(state, backgroundTaskKeys(parsed));
16837
17088
  maybeFinalizeDeferredResult("claude_cli", context, state);
16838
17089
  }
16839
17090
  return true;
@@ -16841,9 +17092,10 @@ function handleClaudeStructuredEvent(parsed, context, state) {
16841
17092
  return false;
16842
17093
  }
16843
17094
  case "assistant": {
16844
- if (presenter.pendingAskUserToolIds?.size) {
17095
+ if (state.awaitingAskDenialFallback) {
17096
+ state.awaitingAskDenialFallback = false;
16845
17097
  console.info(
16846
- `[claude_cli] Suppressing fallback assistant turn \u2014 ${presenter.pendingAskUserToolIds.size} AskUserQuestion(s) still pending`
17098
+ "[claude_cli] Suppressing auto-denial fallback assistant turn after native AskUserQuestion"
16847
17099
  );
16848
17100
  return true;
16849
17101
  }
@@ -16872,7 +17124,7 @@ function handleClaudeStructuredEvent(parsed, context, state) {
16872
17124
  parentToolUseId
16873
17125
  );
16874
17126
  if (CLAUDE_SUBAGENT_TOOL_NAMES.has(toolBlock.name.toLowerCase())) {
16875
- registerBackgroundTaskIds(state, [toolBlock.id]);
17127
+ registerBackgroundLauncher(state, toolBlock.id);
16876
17128
  }
16877
17129
  const normalizedName = normalizeToolName(toolBlock.name);
16878
17130
  if (isInteractiveToolName(toolBlock.name) || INTERACTIVE_TOOL_NAMES.has(normalizedName)) {
@@ -16883,6 +17135,10 @@ function handleClaudeStructuredEvent(parsed, context, state) {
16883
17135
  if (isAskUserQuestionTool(toolBlock.name)) {
16884
17136
  if (!presenter.pendingAskUserToolIds) presenter.pendingAskUserToolIds = /* @__PURE__ */ new Set();
16885
17137
  presenter.pendingAskUserToolIds.add(toolBlock.id);
17138
+ if (!toolBlock.name.toLowerCase().startsWith("mcp__")) {
17139
+ if (!state.nativeAskToolIds) state.nativeAskToolIds = /* @__PURE__ */ new Set();
17140
+ state.nativeAskToolIds.add(toolBlock.id);
17141
+ }
16886
17142
  debugAskLog(
16887
17143
  `[claude_cli] AskUserQuestion tool_use detected \u2014 id=${toolBlock.id}, name=${toolBlock.name}, pendingSize=${presenter.pendingAskUserToolIds.size}, presenterHasProp=${Object.hasOwn(presenter, "pendingAskUserToolIds")}`
16888
17144
  );
@@ -16907,6 +17163,22 @@ function handleClaudeStructuredEvent(parsed, context, state) {
16907
17163
  for (const block of content) {
16908
17164
  if (typeof block === "object" && block !== null && "type" in block && block.type === "tool_result" && "tool_use_id" in block && typeof block.tool_use_id === "string") {
16909
17165
  if (state.suppressedToolResultIds?.has(block.tool_use_id)) {
17166
+ const isSuppressedError = "is_error" in block && block.is_error === true;
17167
+ const isNativeAsk = state.nativeAskToolIds?.has(block.tool_use_id) === true;
17168
+ if (isSuppressedError && !isNativeAsk && presenter.pendingAskUserToolIds?.has(block.tool_use_id)) {
17169
+ presenter.pendingAskUserToolIds.delete(block.tool_use_id);
17170
+ state.suppressedToolResultIds.delete(block.tool_use_id);
17171
+ const resultText2 = extractToolResultContent(block.content);
17172
+ console.info(
17173
+ `[claude_cli] Ask tool ${block.tool_use_id} returned an error \u2014 clearing pending ask so the run can finish`
17174
+ );
17175
+ void presenter.onToolResult?.(block.tool_use_id, resultText2, true, parentToolUseId);
17176
+ maybeFinalizeDeferredResult("claude_cli", context, state);
17177
+ continue;
17178
+ }
17179
+ if (isNativeAsk) {
17180
+ state.awaitingAskDenialFallback = true;
17181
+ }
16910
17182
  console.info(
16911
17183
  `[claude_cli] Suppressed auto-generated tool_result for ${block.tool_use_id}`
16912
17184
  );
@@ -16978,6 +17250,27 @@ function handleClaudeStructuredEvent(parsed, context, state) {
16978
17250
  })
16979
17251
  );
16980
17252
  }
17253
+ const isApiError = parsed.is_error === true || parsed.terminal_reason === "api_error" || typeof parsed.api_error_status === "number";
17254
+ if (isApiError && !state.error) {
17255
+ const apiStatus = typeof parsed.api_error_status === "number" ? parsed.api_error_status : void 0;
17256
+ state.apiErrorStatus = apiStatus;
17257
+ const mappedKind = mapProviderApiError(state.providerErrorCode, apiStatus);
17258
+ const spec = mappedKind ? specForErrorKind(mappedKind) : specForErrorKind("provider_error");
17259
+ state.error = mappedKind || typeof parsed.result !== "string" || !parsed.result.trim() ? spec.message : parsed.result.trim();
17260
+ state.errorKind = spec.errorKind;
17261
+ state.recoveryClass = spec.recoveryClass;
17262
+ console.error(
17263
+ "[claude_cli] result flagged API error:",
17264
+ JSON.stringify({
17265
+ is_error: parsed.is_error,
17266
+ terminal_reason: parsed.terminal_reason,
17267
+ api_error_status: parsed.api_error_status,
17268
+ providerErrorCode: state.providerErrorCode,
17269
+ mappedKind,
17270
+ result: typeof parsed.result === "string" ? parsed.result.slice(0, 200) : void 0
17271
+ })
17272
+ );
17273
+ }
16981
17274
  debugAskLog(
16982
17275
  `[claude_cli] result event \u2014 pendingAskUserToolIds size=${presenter.pendingAskUserToolIds?.size ?? "undefined"}, hasOwnProp=${Object.hasOwn(presenter, "pendingAskUserToolIds")}`
16983
17276
  );
@@ -17014,7 +17307,31 @@ function createClaudeCliBackend(command = "claude", defaultArgs = []) {
17014
17307
  if (disallowedTools.length > 0) {
17015
17308
  args.push("--disallowedTools", disallowedTools.join(","));
17016
17309
  }
17017
- const resumeId = context.config.runtimeSessionId?.trim() || context.config.providerSessionId?.trim();
17310
+ const requestedResumeId = context.config.runtimeSessionId?.trim() || context.config.providerSessionId?.trim();
17311
+ let resumeId = requestedResumeId;
17312
+ let resumeContextPrefix;
17313
+ if (requestedResumeId && !claudeSessionLogExists({
17314
+ sessionId: requestedResumeId,
17315
+ cwd: context.cwd,
17316
+ home: resolveClaudeHome(context.env)
17317
+ })) {
17318
+ resumeId = void 0;
17319
+ resumeContextPrefix = context.config.resumeFallbackContext?.trim() || void 0;
17320
+ console.warn(
17321
+ `[claude_cli] No session log for ${requestedResumeId} under cwd ${context.cwd}; starting a fresh session instead of resuming`
17322
+ );
17323
+ logResumeObservability("agent_resume_fallback_used", {
17324
+ backendKind: "claude_cli",
17325
+ reason: "session_log_missing_for_cwd",
17326
+ requestedResumeId,
17327
+ hadFallbackContext: Boolean(resumeContextPrefix)
17328
+ });
17329
+ emitSessionNotice(
17330
+ context.presenter,
17331
+ "session_resume_failed",
17332
+ 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."
17333
+ );
17334
+ }
17018
17335
  if (!resumeId && context.config.systemPrompt?.trim()) {
17019
17336
  args.push("--system-prompt", context.config.systemPrompt.trim());
17020
17337
  } else if (resumeId && context.config.systemPromptAppend?.trim()) {
@@ -17034,6 +17351,10 @@ function createClaudeCliBackend(command = "claude", defaultArgs = []) {
17034
17351
  if (resumeId) {
17035
17352
  args.push("--resume", resumeId);
17036
17353
  console.info("[claude_cli] Resuming session", { resumeId });
17354
+ logResumeObservability("agent_resume_requested", {
17355
+ backendKind: "claude_cli",
17356
+ requestedResumeId: resumeId
17357
+ });
17037
17358
  }
17038
17359
  args.push("--chrome");
17039
17360
  console.info("[claude_cli] Chrome flag added \u2014 final args:", args.join(" "));
@@ -17054,7 +17375,10 @@ function createClaudeCliBackend(command = "claude", defaultArgs = []) {
17054
17375
  });
17055
17376
  }
17056
17377
  }
17057
- const promptText = ctx.config.mode === "plan" ? buildPlanModePrefix(buildPromptWithSystem(ctx.config, ctx.promptText)) : buildPromptWithSystem(ctx.config, ctx.promptText);
17378
+ const basePromptText = resumeContextPrefix ? `${resumeContextPrefix}
17379
+
17380
+ ${ctx.promptText}` : ctx.promptText;
17381
+ const promptText = ctx.config.mode === "plan" ? buildPlanModePrefix(buildPromptWithSystem(ctx.config, basePromptText)) : buildPromptWithSystem(ctx.config, basePromptText);
17058
17382
  if (promptText.trim()) {
17059
17383
  contentBlocks.push({ type: "text", text: promptText });
17060
17384
  }
@@ -17105,27 +17429,30 @@ function normalizeCodexMcpToolResult(output) {
17105
17429
  }
17106
17430
  return raw;
17107
17431
  }
17432
+ function resolveCodexHome(env) {
17433
+ return env.CODEX_HOME || process.env.CODEX_HOME || (0, import_path5.join)((0, import_os5.homedir)(), ".codex");
17434
+ }
17108
17435
  function findCodexSessionLog(runtimeSessionId, codexHome) {
17109
- if (!runtimeSessionId || !(0, import_fs4.existsSync)(codexHome)) return null;
17110
- const root = (0, import_path4.join)(codexHome, "sessions");
17111
- if (!(0, import_fs4.existsSync)(root)) return null;
17436
+ if (!runtimeSessionId || !(0, import_fs5.existsSync)(codexHome)) return null;
17437
+ const root = (0, import_path5.join)(codexHome, "sessions");
17438
+ if (!(0, import_fs5.existsSync)(root)) return null;
17112
17439
  const matches = [];
17113
17440
  const stack = [root];
17114
17441
  while (stack.length > 0) {
17115
17442
  const dir = stack.pop();
17116
17443
  let entries;
17117
17444
  try {
17118
- entries = (0, import_fs4.readdirSync)(dir, { withFileTypes: true });
17445
+ entries = (0, import_fs5.readdirSync)(dir, { withFileTypes: true });
17119
17446
  } catch {
17120
17447
  continue;
17121
17448
  }
17122
17449
  for (const entry of entries) {
17123
- const path = (0, import_path4.join)(dir, entry.name);
17450
+ const path = (0, import_path5.join)(dir, entry.name);
17124
17451
  if (entry.isDirectory()) {
17125
17452
  stack.push(path);
17126
17453
  } else if (entry.isFile() && entry.name.includes(runtimeSessionId) && entry.name.endsWith(".jsonl")) {
17127
17454
  try {
17128
- matches.push({ path, mtimeMs: (0, import_fs4.statSync)(path).mtimeMs });
17455
+ matches.push({ path, mtimeMs: (0, import_fs5.statSync)(path).mtimeMs });
17129
17456
  } catch {
17130
17457
  matches.push({ path, mtimeMs: 0 });
17131
17458
  }
@@ -17158,12 +17485,14 @@ function latestUserMessageLineIndex(lines) {
17158
17485
  async function replayCodexMcpToolEventsFromSessionLog(context, state) {
17159
17486
  const runtimeSessionId = state.runtimeSessionId;
17160
17487
  if (!runtimeSessionId) return;
17161
- const codexHome = context.env.CODEX_HOME || process.env.CODEX_HOME || (0, import_path4.join)((0, import_os4.homedir)(), ".codex");
17488
+ if (!state.iterations || state.iterations <= 0) return;
17489
+ const codexHome = resolveCodexHome(context.env);
17162
17490
  const logPath = findCodexSessionLog(runtimeSessionId, codexHome);
17163
17491
  if (!logPath) return;
17164
- const allLines = (0, import_fs4.readFileSync)(logPath, "utf8").split(/\r?\n/).filter(Boolean);
17492
+ const allLines = (0, import_fs5.readFileSync)(logPath, "utf8").split(/\r?\n/).filter(Boolean);
17165
17493
  const latestUserLineIndex = latestUserMessageLineIndex(allLines);
17166
17494
  const lines = latestUserLineIndex >= 0 ? allLines.slice(latestUserLineIndex + 1) : allLines;
17495
+ const streamedToolIds = state.codexStreamedToolIds ?? /* @__PURE__ */ new Set();
17167
17496
  const emittedToolIds = /* @__PURE__ */ new Set();
17168
17497
  for (const line of lines) {
17169
17498
  const entry = parseJsonObject(line);
@@ -17174,6 +17503,7 @@ async function replayCodexMcpToolEventsFromSessionLog(context, state) {
17174
17503
  const name = typeof payload.name === "string" ? payload.name : "";
17175
17504
  const namespace = typeof payload.namespace === "string" ? payload.namespace : "";
17176
17505
  if (!callId || !name || !namespace.startsWith("mcp__")) continue;
17506
+ if (streamedToolIds.has(callId)) continue;
17177
17507
  const toolName = `${namespace.replace(/_+$/, "")}__${name}`;
17178
17508
  context.presenter.onToolUse(toolName, parseMaybeJson(payload.arguments), callId);
17179
17509
  emittedToolIds.add(callId);
@@ -17182,6 +17512,7 @@ async function replayCodexMcpToolEventsFromSessionLog(context, state) {
17182
17512
  if (payload.type === "tool_search_call") {
17183
17513
  const callId = typeof payload.call_id === "string" ? payload.call_id : "";
17184
17514
  if (!callId) continue;
17515
+ if (streamedToolIds.has(callId)) continue;
17185
17516
  context.presenter.onToolUse("tool_search", payload.arguments ?? {}, callId);
17186
17517
  emittedToolIds.add(callId);
17187
17518
  continue;
@@ -17208,6 +17539,63 @@ async function replayCodexMcpToolEventsFromSessionLog(context, state) {
17208
17539
  }
17209
17540
  }
17210
17541
  }
17542
+ var CODEX_EXIT_GRACE_MS = 3e4;
17543
+ function armCodexExitGraceKill(state) {
17544
+ if (state.exitGraceKillTimer) return;
17545
+ const child = state.process;
17546
+ if (!child) return;
17547
+ const timer = setTimeout(() => {
17548
+ if (child.exitCode !== null || child.killed) return;
17549
+ console.warn(
17550
+ "[codex_app_server] Process did not exit after turn completion; killing process group"
17551
+ );
17552
+ const pid = child.pid;
17553
+ if (pid) {
17554
+ try {
17555
+ process.kill(-pid, "SIGTERM");
17556
+ return;
17557
+ } catch {
17558
+ }
17559
+ }
17560
+ try {
17561
+ child.kill("SIGTERM");
17562
+ } catch {
17563
+ }
17564
+ }, CODEX_EXIT_GRACE_MS);
17565
+ timer.unref?.();
17566
+ state.exitGraceKillTimer = timer;
17567
+ }
17568
+ function disarmCodexExitGraceKill(state) {
17569
+ if (!state.exitGraceKillTimer) return;
17570
+ clearTimeout(state.exitGraceKillTimer);
17571
+ state.exitGraceKillTimer = void 0;
17572
+ }
17573
+ function trackCodexStreamedToolId(state, toolId) {
17574
+ if (!state.codexStreamedToolIds) state.codexStreamedToolIds = /* @__PURE__ */ new Set();
17575
+ if (state.codexStreamedToolIds.has(toolId)) return false;
17576
+ state.codexStreamedToolIds.add(toolId);
17577
+ return true;
17578
+ }
17579
+ function buildCodexMcpToolName(item) {
17580
+ const server = typeof item.server === "string" ? item.server.replace(/_+$/, "") : "";
17581
+ const tool = typeof item.tool === "string" ? item.tool : typeof item.tool_name === "string" ? item.tool_name : "";
17582
+ if (server && tool) return `mcp__${server}__${tool}`;
17583
+ return tool || "MCP Tool";
17584
+ }
17585
+ function emitCodexFileChanges(context, state, itemId, changes) {
17586
+ changes.forEach((change, index) => {
17587
+ if (!change || typeof change !== "object") return;
17588
+ const record = change;
17589
+ const filePath = typeof record.path === "string" ? record.path : "";
17590
+ if (!filePath) return;
17591
+ const kind = typeof record.kind === "string" ? record.kind : "update";
17592
+ const toolName = kind === "add" ? "write_file" : "edit_file";
17593
+ const toolId = changes.length > 1 ? `${itemId}:${index}` : itemId;
17594
+ trackCodexStreamedToolId(state, toolId);
17595
+ void context.presenter.onToolUse(toolName, { file_path: filePath, kind }, toolId);
17596
+ void context.presenter.onToolResult?.(toolId, `${kind} ${filePath}`);
17597
+ });
17598
+ }
17211
17599
  function handleCodexStructuredEvent(parsed, context, state) {
17212
17600
  const presenter = context.presenter;
17213
17601
  const type = typeof parsed.type === "string" ? parsed.type : "";
@@ -17235,6 +17623,7 @@ function handleCodexStructuredEvent(parsed, context, state) {
17235
17623
  case "thread.started":
17236
17624
  return true;
17237
17625
  case "turn.started":
17626
+ disarmCodexExitGraceKill(state);
17238
17627
  state.iterations += 1;
17239
17628
  return true;
17240
17629
  case "session_configured":
@@ -17243,36 +17632,112 @@ function handleCodexStructuredEvent(parsed, context, state) {
17243
17632
  }
17244
17633
  return true;
17245
17634
  case "task_started":
17635
+ disarmCodexExitGraceKill(state);
17246
17636
  state.iterations += 1;
17247
17637
  return true;
17248
17638
  case "item.started": {
17249
17639
  const item = typeof parsed.item === "object" && parsed.item !== null ? parsed.item : null;
17250
- if (!item || item.type !== "command_execution" || typeof item.id !== "string") return true;
17251
- const command = typeof item.command === "string" ? item.command : "";
17252
- void presenter.onToolUse("Bash", { command, cwd: context.cwd }, item.id);
17253
- return true;
17640
+ if (!item || typeof item.type !== "string" || typeof item.id !== "string") return true;
17641
+ switch (item.type) {
17642
+ case "command_execution": {
17643
+ if (trackCodexStreamedToolId(state, item.id)) {
17644
+ const command = typeof item.command === "string" ? item.command : "";
17645
+ void presenter.onToolUse("Bash", { command, cwd: context.cwd }, item.id);
17646
+ }
17647
+ return true;
17648
+ }
17649
+ case "mcp_tool_call": {
17650
+ if (trackCodexStreamedToolId(state, item.id)) {
17651
+ const input = typeof item.arguments === "object" && item.arguments !== null ? item.arguments : {};
17652
+ void presenter.onToolUse(buildCodexMcpToolName(item), input, item.id);
17653
+ }
17654
+ return true;
17655
+ }
17656
+ case "web_search": {
17657
+ if (trackCodexStreamedToolId(state, item.id)) {
17658
+ const query = typeof item.query === "string" ? item.query : "";
17659
+ void presenter.onToolUse("web_search", { query }, item.id);
17660
+ }
17661
+ return true;
17662
+ }
17663
+ // Text-bearing and patch/todo items carry no useful live-start payload; they
17664
+ // are surfaced on item.completed. Recognized (do not count as unhandled).
17665
+ case "reasoning":
17666
+ case "agent_message":
17667
+ case "file_change":
17668
+ case "todo_list":
17669
+ return true;
17670
+ default:
17671
+ return false;
17672
+ }
17254
17673
  }
17255
17674
  case "item.completed": {
17256
17675
  const item = typeof parsed.item === "object" && parsed.item !== null ? parsed.item : null;
17257
17676
  if (!item || typeof item.type !== "string") return true;
17258
- if (item.type === "reasoning" && typeof item.text === "string") {
17259
- void presenter.onThinking(item.text);
17260
- return true;
17261
- }
17262
- if (item.type === "agent_message" && typeof item.text === "string") {
17263
- state.summary += `${item.text}
17677
+ switch (item.type) {
17678
+ case "reasoning": {
17679
+ if (typeof item.text === "string") void presenter.onThinking(item.text);
17680
+ return true;
17681
+ }
17682
+ case "agent_message": {
17683
+ if (typeof item.text === "string") {
17684
+ state.summary += `${item.text}
17264
17685
  `;
17265
- void presenter.onAssistantText(item.text);
17266
- return true;
17267
- }
17268
- if (item.type === "command_execution" && typeof item.id === "string") {
17269
- const output = typeof item.aggregated_output === "string" ? item.aggregated_output : "";
17270
- const itemExitCode = typeof item.exit_code === "number" ? item.exit_code : null;
17271
- const resultText = output.trim().length > 0 ? output : itemExitCode === null ? "" : `Exit code: ${itemExitCode}`;
17272
- void presenter.onToolResult?.(item.id, resultText);
17273
- return true;
17686
+ void presenter.onAssistantText(item.text);
17687
+ }
17688
+ return true;
17689
+ }
17690
+ case "command_execution": {
17691
+ if (typeof item.id !== "string") return true;
17692
+ if (trackCodexStreamedToolId(state, item.id)) {
17693
+ const command = typeof item.command === "string" ? item.command : "";
17694
+ void presenter.onToolUse("Bash", { command, cwd: context.cwd }, item.id);
17695
+ }
17696
+ const output = typeof item.aggregated_output === "string" ? item.aggregated_output : "";
17697
+ const itemExitCode = typeof item.exit_code === "number" ? item.exit_code : null;
17698
+ const resultText = output.trim().length > 0 ? output : itemExitCode === null ? "" : `Exit code: ${itemExitCode}`;
17699
+ void presenter.onToolResult?.(
17700
+ item.id,
17701
+ resultText,
17702
+ itemExitCode != null && itemExitCode !== 0
17703
+ );
17704
+ return true;
17705
+ }
17706
+ case "mcp_tool_call": {
17707
+ if (typeof item.id !== "string") return true;
17708
+ if (trackCodexStreamedToolId(state, item.id)) {
17709
+ const input = typeof item.arguments === "object" && item.arguments !== null ? item.arguments : {};
17710
+ void presenter.onToolUse(buildCodexMcpToolName(item), input, item.id);
17711
+ }
17712
+ const isError = item.status === "failed" || item.error != null;
17713
+ const payload = item.result ?? item.output ?? item.error ?? {};
17714
+ const resultText = typeof payload === "string" ? payload : JSON.stringify(payload ?? {}, null, 2);
17715
+ void presenter.onToolResult?.(item.id, resultText, isError);
17716
+ return true;
17717
+ }
17718
+ case "file_change": {
17719
+ if (typeof item.id !== "string") return true;
17720
+ const changes = Array.isArray(item.changes) ? item.changes : [];
17721
+ emitCodexFileChanges(context, state, item.id, changes);
17722
+ return true;
17723
+ }
17724
+ case "web_search": {
17725
+ if (typeof item.id !== "string") return true;
17726
+ const query = typeof item.query === "string" ? item.query : "";
17727
+ if (trackCodexStreamedToolId(state, item.id)) {
17728
+ void presenter.onToolUse("web_search", { query }, item.id);
17729
+ }
17730
+ void presenter.onToolResult?.(item.id, query ? `Searched: ${query}` : "");
17731
+ return true;
17732
+ }
17733
+ case "todo_list": {
17734
+ const todos = Array.isArray(item.items) ? item.items : [];
17735
+ void presenter.onTodoWrite?.(todos);
17736
+ return true;
17737
+ }
17738
+ default:
17739
+ return false;
17274
17740
  }
17275
- return true;
17276
17741
  }
17277
17742
  case "agent_message_delta":
17278
17743
  case "agent_message_content_delta": {
@@ -17294,6 +17759,7 @@ function handleCodexStructuredEvent(parsed, context, state) {
17294
17759
  const toolId = typeof parsed.call_id === "string" ? parsed.call_id : `exec-${Date.now()}`;
17295
17760
  const command = Array.isArray(parsed.command) ? parsed.command.join(" ") : "";
17296
17761
  const cwd = typeof parsed.cwd === "string" ? parsed.cwd : context.cwd;
17762
+ trackCodexStreamedToolId(state, toolId);
17297
17763
  void presenter.onToolUse("Bash", { command, cwd }, toolId);
17298
17764
  return true;
17299
17765
  }
@@ -17307,6 +17773,7 @@ function handleCodexStructuredEvent(parsed, context, state) {
17307
17773
  const toolId = typeof parsed.call_id === "string" ? parsed.call_id : `mcp-${Date.now()}`;
17308
17774
  const invocation = typeof parsed.invocation === "object" && parsed.invocation !== null ? parsed.invocation : {};
17309
17775
  const tool = typeof invocation.tool_name === "string" ? invocation.tool_name : typeof invocation.tool === "string" ? invocation.tool : "MCP Tool";
17776
+ trackCodexStreamedToolId(state, toolId);
17310
17777
  void presenter.onToolUse(tool, invocation, toolId);
17311
17778
  return true;
17312
17779
  }
@@ -17344,6 +17811,8 @@ function handleCodexStructuredEvent(parsed, context, state) {
17344
17811
  case "task_complete": {
17345
17812
  const lastMessage = typeof parsed.last_agent_message === "string" ? parsed.last_agent_message : "";
17346
17813
  if (lastMessage.length > 0) state.summary = lastMessage;
17814
+ state.error = void 0;
17815
+ armCodexExitGraceKill(state);
17347
17816
  return true;
17348
17817
  }
17349
17818
  case "turn.completed": {
@@ -17365,6 +17834,8 @@ function handleCodexStructuredEvent(parsed, context, state) {
17365
17834
  cacheReadTokens: state.usage.cacheReadTokens,
17366
17835
  cacheCreationTokens: state.usage.cacheCreationTokens
17367
17836
  });
17837
+ state.error = void 0;
17838
+ armCodexExitGraceKill(state);
17368
17839
  return true;
17369
17840
  }
17370
17841
  case "error": {
@@ -17409,6 +17880,33 @@ function createCodexRuntimeBackend(command = "codex", defaultArgs = []) {
17409
17880
  context.config.images,
17410
17881
  context.cwd
17411
17882
  );
17883
+ const requestedResumeId = context.config.runtimeSessionId?.trim() || context.config.providerSessionId?.trim();
17884
+ let resumableSessionId = requestedResumeId;
17885
+ let resumeContextPrefix;
17886
+ if (requestedResumeId && !findCodexSessionLog(requestedResumeId, resolveCodexHome(context.env))) {
17887
+ resumableSessionId = void 0;
17888
+ resumeContextPrefix = context.config.resumeFallbackContext?.trim() || void 0;
17889
+ console.warn(
17890
+ `[codex_app_server] No rollout log found for session ${requestedResumeId}; starting a fresh session instead of resuming`
17891
+ );
17892
+ logResumeObservability("agent_resume_fallback_used", {
17893
+ backendKind: "codex_app_server",
17894
+ reason: "rollout_log_missing",
17895
+ requestedResumeId,
17896
+ hadFallbackContext: Boolean(resumeContextPrefix)
17897
+ });
17898
+ emitSessionNotice(
17899
+ context.presenter,
17900
+ "session_resume_failed",
17901
+ 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."
17902
+ );
17903
+ }
17904
+ if (resumableSessionId) {
17905
+ logResumeObservability("agent_resume_requested", {
17906
+ backendKind: "codex_app_server",
17907
+ requestedResumeId: resumableSessionId
17908
+ });
17909
+ }
17412
17910
  try {
17413
17911
  return await createGenericCliBackend({
17414
17912
  kind: "codex_app_server",
@@ -17417,15 +17915,10 @@ function createCodexRuntimeBackend(command = "codex", defaultArgs = []) {
17417
17915
  args: [],
17418
17916
  startupTimeoutMs: CODEX_STARTUP_TIMEOUT_MS,
17419
17917
  buildArgs: (ctx) => {
17420
- const resumeId = ctx.config.runtimeSessionId?.trim() || ctx.config.providerSessionId?.trim();
17918
+ const resumeId = resumableSessionId;
17421
17919
  const modelArgs = ctx.config.selectedModel?.trim() ? ["--model", ctx.config.selectedModel.trim()] : [];
17422
17920
  const effortArgs = buildCodexEffortArgs(ctx.config.selectedEffortLevel);
17423
- const permissionArgs = [
17424
- "--ask-for-approval",
17425
- "never",
17426
- "--sandbox",
17427
- "danger-full-access"
17428
- ];
17921
+ const permissionArgs = getCodexPermissionArgs(ctx.config);
17429
17922
  const imageArgs = imagePaths.flatMap((p) => ["--image", p]);
17430
17923
  const baseArgs = resumeId ? [
17431
17924
  ...permissionArgs,
@@ -17452,7 +17945,10 @@ function createCodexRuntimeBackend(command = "codex", defaultArgs = []) {
17452
17945
  },
17453
17946
  promptViaStdin: true,
17454
17947
  augmentPrompt: (ctx) => {
17455
- const basePrompt = buildPromptWithSystem(ctx.config, ctx.promptText);
17948
+ const promptWithContext = resumeContextPrefix ? `${resumeContextPrefix}
17949
+
17950
+ ${ctx.promptText}` : ctx.promptText;
17951
+ const basePrompt = buildPromptWithSystem(ctx.config, promptWithContext);
17456
17952
  return ctx.config.mode === "plan" ? buildPlanModePrefix(basePrompt) : basePrompt;
17457
17953
  },
17458
17954
  parseStructuredLine: parseCodexStructuredLine,
@@ -17779,7 +18275,10 @@ function buildCursorAgentModelArg(modelId, options) {
17779
18275
  return `${parsed.baseId}[${overrides.join(",")}]`;
17780
18276
  }
17781
18277
  function isCursorAgentCliModelId(model) {
17782
- return model === "auto" || /^composer-/.test(model) || /^gpt-\d/.test(model) || /^claude-\d/.test(model) || /^gemini-\d/.test(model) || /^grok-\d/.test(model) || /^kimi-/.test(model);
18278
+ return model === "auto" || /^composer-/.test(model) || /^gpt-/.test(model) || /^claude-/.test(model) || /^gemini-/.test(model) || /^grok-/.test(model) || /^kimi-/.test(model);
18279
+ }
18280
+ function isDispatchableCursorModel(model) {
18281
+ return isCursorAgentCliModelId(model) || Boolean(findModelDef("cursor_agent_cli", model));
17783
18282
  }
17784
18283
  function shouldForceCursorAgent(config) {
17785
18284
  if (shouldUseReadOnlyRuntimePermissions(config)) return false;
@@ -17797,6 +18296,21 @@ function createCursorAgentCliBackend(command = "cursor-agent", defaultArgs = [])
17797
18296
  kind: "cursor_agent_cli",
17798
18297
  supportTier: "structured",
17799
18298
  async run(context) {
18299
+ const selectedModel = context.config.selectedModel?.trim();
18300
+ if (selectedModel && !isDispatchableCursorModel(selectedModel)) {
18301
+ const message = `Model "${selectedModel}" is not supported by cursor-agent. Pick a cursor-agent model (or "auto") and send your message again.`;
18302
+ return {
18303
+ success: false,
18304
+ summary: message,
18305
+ filesModified: [],
18306
+ planFilesCreated: [],
18307
+ iterations: 0,
18308
+ error: message,
18309
+ errorKind: "model_mismatch",
18310
+ backendKind: "cursor_agent_cli",
18311
+ supportTier: "structured"
18312
+ };
18313
+ }
17800
18314
  const { files: imageFiles, cleanup } = writeImagesToTempFiles(
17801
18315
  context.config.images,
17802
18316
  context.cwd
@@ -17826,10 +18340,18 @@ function createCursorAgentCliBackend(command = "cursor-agent", defaultArgs = [])
17826
18340
  args.push("--mode", "plan");
17827
18341
  } else if (ctx.config.mode === "ask") {
17828
18342
  args.push("--mode", "ask");
18343
+ } else if (shouldUseReadOnlyRuntimePermissions(ctx.config)) {
18344
+ args.push("--mode", "plan");
18345
+ }
18346
+ if (resumeId) {
18347
+ args.push("--resume", resumeId);
18348
+ logResumeObservability("agent_resume_requested", {
18349
+ backendKind: "cursor_agent_cli",
18350
+ requestedResumeId: resumeId
18351
+ });
17829
18352
  }
17830
- if (resumeId) args.push("--resume", resumeId);
17831
18353
  const model = ctx.config.selectedModel?.trim();
17832
- if (model && isCursorAgentCliModelId(model)) {
18354
+ if (model && isDispatchableCursorModel(model)) {
17833
18355
  const modelDef = findModelDef("cursor_agent_cli", model);
17834
18356
  args.push(
17835
18357
  "--model",
@@ -18292,9 +18814,9 @@ function buildGrokAgentArgs(config, defaultArgs = []) {
18292
18814
  return args;
18293
18815
  }
18294
18816
  var DEFAULT_SKILL_ROOTS = [
18295
- (0, import_path5.join)((0, import_os5.homedir)(), ".agents", "skills"),
18296
- (0, import_path5.join)((0, import_os5.homedir)(), ".claude", "skills"),
18297
- (0, import_path5.join)((0, import_os5.homedir)(), ".config", "opencode", "skills")
18817
+ (0, import_path6.join)((0, import_os6.homedir)(), ".agents", "skills"),
18818
+ (0, import_path6.join)((0, import_os6.homedir)(), ".claude", "skills"),
18819
+ (0, import_path6.join)((0, import_os6.homedir)(), ".config", "opencode", "skills")
18298
18820
  ];
18299
18821
  function parseFrontmatter(content) {
18300
18822
  if (!content.startsWith("---\n")) return {};
@@ -18310,12 +18832,12 @@ function parseFrontmatter(content) {
18310
18832
  return result;
18311
18833
  }
18312
18834
  function listSkillFiles(root) {
18313
- if (!(0, import_fs5.existsSync)(root)) return [];
18835
+ if (!(0, import_fs6.existsSync)(root)) return [];
18314
18836
  const files = [];
18315
- for (const entry of (0, import_fs5.readdirSync)(root, { withFileTypes: true })) {
18837
+ for (const entry of (0, import_fs6.readdirSync)(root, { withFileTypes: true })) {
18316
18838
  if (!entry.isDirectory()) continue;
18317
- const skillPath = (0, import_path5.join)(root, entry.name, "SKILL.md");
18318
- if ((0, import_fs5.existsSync)(skillPath)) files.push(skillPath);
18839
+ const skillPath = (0, import_path6.join)(root, entry.name, "SKILL.md");
18840
+ if ((0, import_fs6.existsSync)(skillPath)) files.push(skillPath);
18319
18841
  }
18320
18842
  return files;
18321
18843
  }
@@ -18324,7 +18846,7 @@ function loadInstalledSkills(roots = DEFAULT_SKILL_ROOTS) {
18324
18846
  for (const root of roots) {
18325
18847
  for (const skillPath of listSkillFiles(root)) {
18326
18848
  try {
18327
- const frontmatter = parseFrontmatter((0, import_fs5.readFileSync)(skillPath, "utf8"));
18849
+ const frontmatter = parseFrontmatter((0, import_fs6.readFileSync)(skillPath, "utf8"));
18328
18850
  const name = frontmatter.name;
18329
18851
  const description = frontmatter.description;
18330
18852
  if (!name || !description || skills.has(name)) continue;
@@ -18757,7 +19279,31 @@ function createSupatestCliBackend(command = "supatest", defaultArgs = []) {
18757
19279
  "--permission-mode",
18758
19280
  getClaudePermissionMode(context.config)
18759
19281
  ];
18760
- const resumeId = context.config.runtimeSessionId?.trim() || context.config.providerSessionId?.trim();
19282
+ const requestedResumeId = context.config.runtimeSessionId?.trim() || context.config.providerSessionId?.trim();
19283
+ let resumeId = requestedResumeId;
19284
+ let resumeContextPrefix;
19285
+ if (requestedResumeId && !claudeSessionLogExists({
19286
+ sessionId: requestedResumeId,
19287
+ cwd: context.cwd,
19288
+ home: resolveClaudeHome(context.env, "supatest")
19289
+ })) {
19290
+ resumeId = void 0;
19291
+ resumeContextPrefix = context.config.resumeFallbackContext?.trim() || void 0;
19292
+ console.warn(
19293
+ `[supatest_cli] No session log for ${requestedResumeId} under cwd ${context.cwd}; starting a fresh session instead of resuming`
19294
+ );
19295
+ logResumeObservability("agent_resume_fallback_used", {
19296
+ backendKind: "supatest_cli",
19297
+ reason: "session_log_missing_for_cwd",
19298
+ requestedResumeId,
19299
+ hadFallbackContext: Boolean(resumeContextPrefix)
19300
+ });
19301
+ emitSessionNotice(
19302
+ context.presenter,
19303
+ "session_resume_failed",
19304
+ 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."
19305
+ );
19306
+ }
18761
19307
  if (!resumeId) {
18762
19308
  if (context.config.systemPrompt?.trim()) {
18763
19309
  args.push("--system-prompt", context.config.systemPrompt.trim());
@@ -18773,6 +19319,10 @@ function createSupatestCliBackend(command = "supatest", defaultArgs = []) {
18773
19319
  if (resumeId) {
18774
19320
  args.push("--resume", resumeId);
18775
19321
  console.info("[supatest_cli] Resuming session", { resumeId });
19322
+ logResumeObservability("agent_resume_requested", {
19323
+ backendKind: "supatest_cli",
19324
+ requestedResumeId: resumeId
19325
+ });
18776
19326
  }
18777
19327
  args.push(...defaultArgs);
18778
19328
  return createGenericCliBackend({
@@ -18791,7 +19341,10 @@ function createSupatestCliBackend(command = "supatest", defaultArgs = []) {
18791
19341
  });
18792
19342
  }
18793
19343
  }
18794
- const promptText = ctx.config.mode === "plan" ? buildPlanModePrefix(buildPromptWithSystem(ctx.config, ctx.promptText)) : buildPromptWithSystem(ctx.config, ctx.promptText);
19344
+ const basePromptText = resumeContextPrefix ? `${resumeContextPrefix}
19345
+
19346
+ ${ctx.promptText}` : ctx.promptText;
19347
+ const promptText = ctx.config.mode === "plan" ? buildPlanModePrefix(buildPromptWithSystem(ctx.config, basePromptText)) : buildPromptWithSystem(ctx.config, basePromptText);
18795
19348
  if (promptText.trim()) {
18796
19349
  contentBlocks.push({ type: "text", text: promptText });
18797
19350
  }
@@ -18821,40 +19374,40 @@ function parseSkillInvocation(text) {
18821
19374
  function candidateRoots(cwd) {
18822
19375
  const roots = [];
18823
19376
  if (cwd) {
18824
- roots.push((0, import_path6.join)(cwd, ".agents", "skills"));
18825
- roots.push((0, import_path6.join)(cwd, ".claude", "skills"));
18826
- roots.push((0, import_path6.join)(cwd, ".codex", "skills"));
18827
- if ((0, import_fs6.existsSync)(cwd)) {
19377
+ roots.push((0, import_path7.join)(cwd, ".agents", "skills"));
19378
+ roots.push((0, import_path7.join)(cwd, ".claude", "skills"));
19379
+ roots.push((0, import_path7.join)(cwd, ".codex", "skills"));
19380
+ if ((0, import_fs7.existsSync)(cwd)) {
18828
19381
  let entries = [];
18829
19382
  try {
18830
- entries = (0, import_fs6.readdirSync)(cwd, { withFileTypes: true });
19383
+ entries = (0, import_fs7.readdirSync)(cwd, { withFileTypes: true });
18831
19384
  } catch {
18832
19385
  }
18833
19386
  for (const entry of entries) {
18834
19387
  if (!entry.isDirectory()) continue;
18835
19388
  if (entry.name.startsWith(".")) continue;
18836
- const child = (0, import_path6.join)(cwd, entry.name);
18837
- roots.push((0, import_path6.join)(child, ".agents", "skills"));
18838
- roots.push((0, import_path6.join)(child, ".claude", "skills"));
18839
- roots.push((0, import_path6.join)(child, ".codex", "skills"));
19389
+ const child = (0, import_path7.join)(cwd, entry.name);
19390
+ roots.push((0, import_path7.join)(child, ".agents", "skills"));
19391
+ roots.push((0, import_path7.join)(child, ".claude", "skills"));
19392
+ roots.push((0, import_path7.join)(child, ".codex", "skills"));
18840
19393
  }
18841
19394
  }
18842
19395
  }
18843
- const home = (0, import_os6.homedir)();
18844
- roots.push((0, import_path6.join)(home, ".agents", "skills"));
18845
- roots.push((0, import_path6.join)(home, ".claude", "skills"));
18846
- roots.push((0, import_path6.join)(home, ".codex", "skills"));
19396
+ const home = (0, import_os7.homedir)();
19397
+ roots.push((0, import_path7.join)(home, ".agents", "skills"));
19398
+ roots.push((0, import_path7.join)(home, ".claude", "skills"));
19399
+ roots.push((0, import_path7.join)(home, ".codex", "skills"));
18847
19400
  return Array.from(new Set(roots));
18848
19401
  }
18849
19402
  function resolveSkillInvocation(text, cwd) {
18850
19403
  const parsed = parseSkillInvocation(text);
18851
19404
  if (!parsed) return null;
18852
19405
  for (const root of candidateRoots(cwd)) {
18853
- const skillPath = (0, import_path6.join)(root, parsed.command, "SKILL.md");
18854
- if (!(0, import_fs6.existsSync)(skillPath)) continue;
19406
+ const skillPath = (0, import_path7.join)(root, parsed.command, "SKILL.md");
19407
+ if (!(0, import_fs7.existsSync)(skillPath)) continue;
18855
19408
  let content;
18856
19409
  try {
18857
- content = (0, import_fs6.readFileSync)(skillPath, "utf-8");
19410
+ content = (0, import_fs7.readFileSync)(skillPath, "utf-8");
18858
19411
  } catch {
18859
19412
  continue;
18860
19413
  }
@@ -19035,11 +19588,19 @@ var BaseMachineAgent = class _BaseMachineAgent {
19035
19588
  async runBackendWithActivityHeartbeat(backend, context) {
19036
19589
  const intervalMs = this.getActivityHeartbeatIntervalMs();
19037
19590
  let heartbeat = null;
19591
+ let livenessProbe = null;
19592
+ context.registerLivenessProbe = (probe) => {
19593
+ livenessProbe = probe;
19594
+ };
19595
+ const emitHeartbeat = () => {
19596
+ const evidence = livenessProbe?.();
19597
+ void this.presenter.onActivity?.("background_work", evidence);
19598
+ };
19038
19599
  if (this.presenter.onActivity && intervalMs > 0) {
19039
- void this.presenter.onActivity("background_work");
19600
+ emitHeartbeat();
19040
19601
  heartbeat = setInterval(() => {
19041
19602
  if (!context.abortController.signal.aborted) {
19042
- void this.presenter.onActivity?.("background_work");
19603
+ emitHeartbeat();
19043
19604
  }
19044
19605
  }, intervalMs);
19045
19606
  }
@@ -19088,10 +19649,6 @@ var BaseMachineAgent = class _BaseMachineAgent {
19088
19649
  if (agentPrompt) systemPromptParts.push(agentPrompt);
19089
19650
  const modeHint = getAlanModePrompt(config.mode);
19090
19651
  if (modeHint) systemPromptParts.push(modeHint);
19091
- systemPromptParts.push(ALAN_ASK_USER_PROMPT);
19092
- if (backendKind === "cursor_agent_cli") {
19093
- systemPromptParts.push(ALAN_CURSOR_ASK_USER_OVERLAY);
19094
- }
19095
19652
  const operatorSystemPrompt = config.systemPrompt?.trim();
19096
19653
  if (operatorSystemPrompt) systemPromptParts.push(operatorSystemPrompt);
19097
19654
  const workingDir = config.worktreePath || safeProjectPath;
@@ -19164,6 +19721,7 @@ Prefer relative paths and keep your work scoped to this project.`
19164
19721
  };
19165
19722
  if (lastResult.success || !lastResult.error) break;
19166
19723
  if (this.abortController?.signal.aborted) break;
19724
+ if (lastResult.errorKind === "model_mismatch") break;
19167
19725
  const hadResumeId = runtimeConfig.runtimeSessionId || runtimeConfig.providerSessionId;
19168
19726
  if (hadResumeId && !isTransientError(lastResult.error)) {
19169
19727
  const overrideResult = await this.onSessionResumeFailure(runtimeConfig, lastResult);
@@ -19175,10 +19733,25 @@ Prefer relative paths and keep your work scoped to this project.`
19175
19733
  "[base-machine-agent] Resume failed, retrying without session ID:",
19176
19734
  lastResult.error
19177
19735
  );
19736
+ const fallbackContext = runtimeConfig.resumeFallbackContext?.trim();
19737
+ logResumeObservability("agent_resume_fallback_used", {
19738
+ backendKind: backend.kind,
19739
+ reason: "retry_fresh_after_resume_failure",
19740
+ errorKind: lastResult.errorKind,
19741
+ hadFallbackContext: Boolean(fallbackContext)
19742
+ });
19743
+ emitSessionNotice(
19744
+ this.presenter,
19745
+ "session_resume_failed",
19746
+ 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."
19747
+ );
19178
19748
  runtimeConfig = {
19179
19749
  ...runtimeConfig,
19180
19750
  runtimeSessionId: void 0,
19181
- providerSessionId: void 0
19751
+ providerSessionId: void 0,
19752
+ ...fallbackContext ? { task: `${fallbackContext}
19753
+
19754
+ ${runtimeConfig.task}` } : {}
19182
19755
  };
19183
19756
  const retryResult = await this.runBackendWithActivityHeartbeat(backend, {
19184
19757
  presenter: this.presenter,
@@ -19186,7 +19759,7 @@ Prefer relative paths and keep your work scoped to this project.`
19186
19759
  abortController: this.abortController,
19187
19760
  cwd,
19188
19761
  env,
19189
- promptText,
19762
+ promptText: fallbackContext ? this.buildPromptText(runtimeConfig) : promptText,
19190
19763
  onProcessSpawned: this.getOnProcessSpawned()
19191
19764
  });
19192
19765
  lastResult = {
@@ -19382,14 +19955,14 @@ function normalizePath(candidate, cwd) {
19382
19955
  } catch {
19383
19956
  }
19384
19957
  if (path.startsWith("~/")) return null;
19385
- if ((0, import_path7.isAbsolute)(path)) return path;
19386
- return cwd ? (0, import_path7.join)(cwd, path) : null;
19958
+ if ((0, import_path8.isAbsolute)(path)) return path;
19959
+ return cwd ? (0, import_path8.join)(cwd, path) : null;
19387
19960
  }
19388
19961
  function resolveToolCwd(cwd, toolInput) {
19389
19962
  if (!isRecord(toolInput)) return cwd;
19390
19963
  const raw = typeof toolInput.cwd === "string" ? toolInput.cwd : typeof toolInput.workdir === "string" ? toolInput.workdir : typeof toolInput.workingDirectory === "string" ? toolInput.workingDirectory : null;
19391
19964
  if (!raw) return cwd;
19392
- return (0, import_path7.isAbsolute)(raw) || !cwd ? raw : (0, import_path7.join)(cwd, raw);
19965
+ return (0, import_path8.isAbsolute)(raw) || !cwd ? raw : (0, import_path8.join)(cwd, raw);
19393
19966
  }
19394
19967
  function extractBrowserMediaPathHints(content, cwd, toolName, options = {}, kind = "video") {
19395
19968
  if (!isAgentBrowserMediaInvocation(toolName, content, options.toolInput, kind)) return [];
@@ -19432,19 +20005,19 @@ function extractGeneratedImagesFromToolResult(toolId, content, cwd, toolName, op
19432
20005
  const effectiveCwd = resolveToolCwd(cwd, options.toolInput);
19433
20006
  for (const candidate of extractPathCandidates(content, options)) {
19434
20007
  const path = normalizePath(candidate, effectiveCwd);
19435
- if (!path || seen.has(path) || !(0, import_fs7.existsSync)(path)) continue;
20008
+ if (!path || seen.has(path) || !(0, import_fs8.existsSync)(path)) continue;
19436
20009
  seen.add(path);
19437
- const ext = (0, import_path7.extname)(path).toLowerCase();
20010
+ const ext = (0, import_path8.extname)(path).toLowerCase();
19438
20011
  const mimeType = IMAGE_MIME_BY_EXT[ext];
19439
20012
  if (!mimeType) continue;
19440
- const stat = (0, import_fs7.statSync)(path);
20013
+ const stat = (0, import_fs8.statSync)(path);
19441
20014
  if (!stat.isFile() || stat.size <= 0 || stat.size > MAX_GENERATED_IMAGE_BYTES) continue;
19442
- const buffer = (0, import_fs7.readFileSync)(path);
20015
+ const buffer = (0, import_fs8.readFileSync)(path);
19443
20016
  const { width, height } = readImageDimensions2(buffer, mimeType);
19444
20017
  images.push({
19445
20018
  type: "generated_image",
19446
20019
  id: (0, import_crypto3.randomUUID)(),
19447
- filename: (0, import_path7.basename)(path),
20020
+ filename: (0, import_path8.basename)(path),
19448
20021
  mimeType,
19449
20022
  size: buffer.length,
19450
20023
  width,
@@ -19471,20 +20044,20 @@ function extractSessionMediaFromToolResult(toolId, content, cwd, toolName, optio
19471
20044
  const mimeByExt = kind === "video" ? VIDEO_MIME_BY_EXT : IMAGE_MIME_BY_EXT;
19472
20045
  for (const candidate of extractPathCandidates(content, options, extensionsPattern)) {
19473
20046
  const path = normalizePath(candidate, effectiveCwd);
19474
- if (!path || seen.has(path) || !(0, import_fs7.existsSync)(path)) continue;
20047
+ if (!path || seen.has(path) || !(0, import_fs8.existsSync)(path)) continue;
19475
20048
  seen.add(path);
19476
- const ext = (0, import_path7.extname)(path).toLowerCase();
20049
+ const ext = (0, import_path8.extname)(path).toLowerCase();
19477
20050
  const mimeType = mimeByExt[ext];
19478
20051
  if (!mimeType) continue;
19479
- const stat = (0, import_fs7.statSync)(path);
20052
+ const stat = (0, import_fs8.statSync)(path);
19480
20053
  if (!stat.isFile() || stat.size <= 0 || stat.size > maxBytes) continue;
19481
- const buffer = (0, import_fs7.readFileSync)(path);
20054
+ const buffer = (0, import_fs8.readFileSync)(path);
19482
20055
  const dimensions = kind === "image" ? readImageDimensions2(buffer, mimeType) : null;
19483
20056
  media.push({
19484
20057
  type: "session_media",
19485
20058
  kind,
19486
20059
  id: (0, import_crypto3.randomUUID)(),
19487
- filename: (0, import_path7.basename)(path),
20060
+ filename: (0, import_path8.basename)(path),
19488
20061
  mimeType,
19489
20062
  size: buffer.length,
19490
20063
  width: dimensions?.width,
@@ -19648,6 +20221,10 @@ var CoreAgent = class _CoreAgent extends BaseMachineAgent {
19648
20221
  if (!this.childProcess) return false;
19649
20222
  const stdin = this.childProcess.stdin;
19650
20223
  if (!stdin || stdin.destroyed) return false;
20224
+ if (this.presenter.hasKnownToolUse && !this.presenter.hasKnownToolUse(toolId)) {
20225
+ console.warn(`[CoreAgent] Ignoring tool_response for unknown tool ${toolId}`);
20226
+ return false;
20227
+ }
19651
20228
  try {
19652
20229
  let message;
19653
20230
  if (this.presenter.pendingAskUserToolIds?.has(toolId)) {
@@ -19902,6 +20479,9 @@ function decodeKey(encodedKey) {
19902
20479
  function isTerminal(event) {
19903
20480
  return TERMINAL_EVENT_TYPES.has(event.type);
19904
20481
  }
20482
+ function isBackgroundHeartbeat(event) {
20483
+ return event.type === "activity" && event.status === "background_work";
20484
+ }
19905
20485
  function truncateUtf8(value2, maxBytes) {
19906
20486
  const encoded = Buffer.from(value2, "utf8");
19907
20487
  if (encoded.length <= maxBytes) return value2;
@@ -20050,6 +20630,23 @@ var EncryptedEventOutbox = class {
20050
20630
  isDroppable(entry) {
20051
20631
  return entry.eventId !== this.inFlightEventId && entry.kind !== "condensation_marker" && !isTerminal(entry.event);
20052
20632
  }
20633
+ /**
20634
+ * Pick the next droppable entry to evict. Heartbeats are always evicted
20635
+ * first (oldest heartbeat first) since they carry no transcript content;
20636
+ * only once none remain do we fall back to the oldest droppable entry of
20637
+ * any kind. Without this, oldest-first eviction treats a background_work
20638
+ * heartbeat exactly like real transcript content, so a long offline stretch
20639
+ * (heartbeats every 10s, never removed by an ack since nothing is
20640
+ * connected) evicts genuine early events — including `start` — ahead of
20641
+ * the heartbeats that caused the overflow.
20642
+ */
20643
+ pickDropCandidate(matches) {
20644
+ const heartbeat = this.entries.find(
20645
+ (entry) => matches(entry) && this.isDroppable(entry) && isBackgroundHeartbeat(entry.event)
20646
+ );
20647
+ if (heartbeat) return heartbeat;
20648
+ return this.entries.find((entry) => matches(entry) && this.isDroppable(entry));
20649
+ }
20053
20650
  ensureCondensationMarker(runId, conversationId) {
20054
20651
  if (this.entries.some((entry) => entry.runId === runId && entry.kind === "condensation_marker")) {
20055
20652
  return;
@@ -20077,7 +20674,7 @@ var EncryptedEventOutbox = class {
20077
20674
  const runIds = preferredRunId ? [preferredRunId] : [...new Set(this.entries.map((entry) => entry.runId))];
20078
20675
  for (const runId of runIds) {
20079
20676
  while (this.entries.filter((entry) => entry.runId === runId && this.isDroppable(entry)).length > this.maxIntermediateEventsPerRun) {
20080
- const drop = this.entries.find((entry) => entry.runId === runId && this.isDroppable(entry));
20677
+ const drop = this.pickDropCandidate((entry) => entry.runId === runId);
20081
20678
  if (!drop) break;
20082
20679
  this.entries = this.entries.filter((entry) => entry.eventId !== drop.eventId);
20083
20680
  lossCountByRunId.set(runId, (lossCountByRunId.get(runId) ?? 0) + 1);
@@ -20085,7 +20682,7 @@ var EncryptedEventOutbox = class {
20085
20682
  }
20086
20683
  }
20087
20684
  while (encryptedFileSize(this.entries) > this.maxEncryptedBytes) {
20088
- const drop = this.entries.find((entry) => this.isDroppable(entry));
20685
+ const drop = this.pickDropCandidate(() => true);
20089
20686
  if (drop) {
20090
20687
  this.entries = this.entries.filter((entry) => entry.eventId !== drop.eventId);
20091
20688
  lossCountByRunId.set(drop.runId, (lossCountByRunId.get(drop.runId) ?? 0) + 1);
@@ -20104,7 +20701,7 @@ var EncryptedEventOutbox = class {
20104
20701
  this.ensureCondensationMarker(runId, lossConversationByRunId.get(runId));
20105
20702
  }
20106
20703
  while (encryptedFileSize(this.entries) > this.maxEncryptedBytes) {
20107
- const drop = this.entries.find((entry) => this.isDroppable(entry));
20704
+ const drop = this.pickDropCandidate(() => true);
20108
20705
  if (!drop) break;
20109
20706
  this.entries = this.entries.filter((entry) => entry.eventId !== drop.eventId);
20110
20707
  lossCountByRunId.set(drop.runId, (lossCountByRunId.get(drop.runId) ?? 0) + 1);
@@ -21029,6 +21626,10 @@ function assertValidExistingToml(path, content) {
21029
21626
  `Existing config is malformed; no changes were applied. Review the backup at ${backupPath}`
21030
21627
  );
21031
21628
  }
21629
+ var CODEX_ALAN_TOML_SECTION = "mcp_servers.alan";
21630
+ function isCodexAlanTomlSection(section) {
21631
+ return section === CODEX_ALAN_TOML_SECTION || section.startsWith(`${CODEX_ALAN_TOML_SECTION}.`);
21632
+ }
21032
21633
  function mergeCodexAlanSection(path, existingContent, desiredContent) {
21033
21634
  if (existingContent === null || existingContent.trim() === "") return desiredContent;
21034
21635
  assertValidExistingToml(path, existingContent);
@@ -21038,7 +21639,7 @@ function mergeCodexAlanSection(path, existingContent, desiredContent) {
21038
21639
  for (const line of lines) {
21039
21640
  const section = line.trim().match(/^\[([^\]]+)]$/)?.[1];
21040
21641
  if (section) {
21041
- insideAlanSection = section === "mcp_servers.alan";
21642
+ insideAlanSection = isCodexAlanTomlSection(section);
21042
21643
  }
21043
21644
  if (!insideAlanSection) retained.push(line);
21044
21645
  }
@@ -21403,6 +22004,10 @@ var ANTIGRAVITY_MODEL_DISCOVERY_TIMEOUT_MS = 12e3;
21403
22004
  var OPENCODE_ZEN_MODEL_IDS = OPENCODE_ZEN_MODELS.map((model) => model.id);
21404
22005
  var CLAUDE_MODEL_ID_PATTERN = /claude-(?:sonnet|opus|haiku|fable)-[a-z0-9][a-z0-9-]*/g;
21405
22006
  var CLAUDE_MODEL_ALIASES = ["sonnet", "opus", "haiku", "fable"];
22007
+ var CLAUDE_MODEL_QUALIFIER_SEGMENTS = /* @__PURE__ */ new Set(["fast", "thinking", "latest", "preview"]);
22008
+ var PROTECTED_CATALOG_MODEL_IDS = new Set(
22009
+ AVAILABLE_MODELS.map((model) => model.id).filter((id) => id.length > 0)
22010
+ );
21406
22011
  var CATALOG_MODEL_IDS_BY_PROVIDER = Object.fromEntries(
21407
22012
  Object.entries(PROVIDER_MODELS).map(([provider, models]) => [
21408
22013
  provider,
@@ -21493,15 +22098,22 @@ function extractModelIdsFromLabeledList(value2) {
21493
22098
  }
21494
22099
  function isValidClaudeModelId(id) {
21495
22100
  if (CLAUDE_MODEL_ALIASES.includes(id)) return true;
21496
- if (!/^claude-(sonnet|opus|haiku|fable)-[a-z0-9][a-z0-9-]*$/.test(id)) return false;
22101
+ const match = id.match(/^claude-(sonnet|opus|haiku|fable)-([a-z0-9][a-z0-9-]*)$/);
22102
+ if (!match) return false;
21497
22103
  if (id.includes(".")) return false;
21498
22104
  if (/-\d{8}$/.test(id)) return false;
21499
22105
  if (/-v\d+$/.test(id)) return false;
22106
+ for (const segment of match[2].split("-")) {
22107
+ if (/^\d+$/.test(segment)) continue;
22108
+ if (CLAUDE_MODEL_QUALIFIER_SEGMENTS.has(segment)) continue;
22109
+ return false;
22110
+ }
21500
22111
  return true;
21501
22112
  }
21502
22113
  function filterClaudePrefixModelIds(ids) {
21503
22114
  const set = new Set(ids);
21504
22115
  return ids.filter((id) => {
22116
+ if (PROTECTED_CATALOG_MODEL_IDS.has(id)) return true;
21505
22117
  for (const other of set) {
21506
22118
  if (other !== id && other.startsWith(`${id}-`)) return false;
21507
22119
  }
@@ -21824,7 +22436,7 @@ var RunStartGate = class {
21824
22436
  };
21825
22437
 
21826
22438
  // src/version.ts
21827
- var AGENT_VERSION = "0.1.37";
22439
+ var AGENT_VERSION = "0.1.39";
21828
22440
 
21829
22441
  // src/workspace-relocation.ts
21830
22442
  var import_node_child_process3 = require("child_process");
@@ -23180,14 +23792,29 @@ var USER_INTERRUPTED_RESULT = {
23180
23792
  filesModified: [],
23181
23793
  planFilesCreated: [],
23182
23794
  iterations: 0,
23183
- error: "Interrupted by user"
23795
+ error: "Interrupted by user",
23796
+ errorKind: "user"
23184
23797
  };
23798
+ function buildAbortResult(reason) {
23799
+ if (reason === "user") return USER_INTERRUPTED_RESULT;
23800
+ const message = SYSTEM_ABORT_RUN_ERRORS[reason];
23801
+ return {
23802
+ success: false,
23803
+ summary: message,
23804
+ filesModified: [],
23805
+ planFilesCreated: [],
23806
+ iterations: 0,
23807
+ error: message,
23808
+ errorKind: reason
23809
+ };
23810
+ }
23185
23811
  function abortActiveAgent(entry, options) {
23186
23812
  const awaitingAsk = (entry.presenter.pendingAskUserToolIds?.size ?? 0) > 0;
23187
23813
  if (awaitingAsk && !options?.userInitiated) {
23188
23814
  return;
23189
23815
  }
23190
- entry.presenter.onComplete(USER_INTERRUPTED_RESULT);
23816
+ const reason = options?.reason ?? (options?.userInitiated ? "user" : "runner_stalled");
23817
+ entry.presenter.onComplete(buildAbortResult(reason));
23191
23818
  entry.agent.kill();
23192
23819
  }
23193
23820
  function isProviderProcessAlive(pid) {
@@ -23296,7 +23923,11 @@ function flushPendingDeliveryLostEvents(input) {
23296
23923
  if (input.pending.length === 0) return;
23297
23924
  for (const item of input.pending) {
23298
23925
  if (input.eventOutbox.hasTerminalEvent(item.runId)) continue;
23299
- if (!input.activeAgents.has(item.runId)) continue;
23926
+ const activeEntry = input.activeAgents.get(item.runId);
23927
+ if (activeEntry) {
23928
+ if (activeEntry.stage === "delivery_lost") activeEntry.stage = "running";
23929
+ continue;
23930
+ }
23300
23931
  input.eventOutbox.enqueue(item.conversationId, item.runId, {
23301
23932
  type: "session_error",
23302
23933
  error: DELIVERY_LOST_ERROR
@@ -23320,7 +23951,7 @@ function reconcileActiveRuns(input) {
23320
23951
  input.pushLog?.(`reconciled dead provider pid run=${runId} (awaiting ask, no interrupt)`);
23321
23952
  continue;
23322
23953
  }
23323
- abortActiveAgent(entry);
23954
+ abortActiveAgent(entry, { reason: staleByDeadPid ? "provider_exited" : "runner_stalled" });
23324
23955
  input.activeAgents.delete(runId);
23325
23956
  if (staleByDeadPid) {
23326
23957
  input.pushLog?.(`reconciled dead provider pid run=${runId}`);
@@ -23384,6 +24015,7 @@ var RuntimePresenter = class {
23384
24015
  emitEvent(event) {
23385
24016
  if (this.terminalFenced) return;
23386
24017
  this.touchRunnerActivity?.();
24018
+ if (isBackgroundHeartbeat(event) && !this.socket.connected) return;
23387
24019
  this.eventOutbox.enqueue(this.conversationId, this.runId, event);
23388
24020
  this.eventOutbox.flush(this.socket);
23389
24021
  }
@@ -23411,6 +24043,9 @@ var RuntimePresenter = class {
23411
24043
  onLog(message) {
23412
24044
  console.error(message);
23413
24045
  }
24046
+ onNotice(kind, message) {
24047
+ this.emitEvent({ type: "session_notice", kind, message, ts: Date.now() });
24048
+ }
23414
24049
  onAssistantText(text) {
23415
24050
  this.emitEvent({ type: "text", text, ts: Date.now() });
23416
24051
  }
@@ -23510,8 +24145,8 @@ var RuntimePresenter = class {
23510
24145
  onCheckpoint(id) {
23511
24146
  this.emitEvent({ type: "checkpoint", id });
23512
24147
  }
23513
- onActivity(status) {
23514
- this.emitEvent({ type: "activity", status, ts: Date.now() });
24148
+ onActivity(status, evidence) {
24149
+ this.emitEvent({ type: "activity", status, ts: Date.now(), ...evidence ? { evidence } : {} });
23515
24150
  }
23516
24151
  onOpenBrowserTab(browserId, url2) {
23517
24152
  this.emitEvent({ type: "open_browser_tab", browserId, ...url2 ? { url: url2 } : {} });
@@ -24297,16 +24932,22 @@ async function startDaemon(args) {
24297
24932
  flushWorkspaceRelocations();
24298
24933
  }
24299
24934
  }
24300
- if (workspaceReadiness.ready && expectedRepoUrls.length > 0 && !isAlanDefaultWorkspace(cwd) && !workspaceMatchesExpectedRepositories({
24301
- workspacePath: cwd,
24302
- expectedRepoUrls
24303
- })) {
24304
- socket.emit("agent.rejected", {
24305
- runId: payload.runId,
24306
- message: "This folder belongs to a different repository. Choose the correct project folder and try again.",
24307
- code: "workspace_repository_mismatch"
24308
- });
24309
- return;
24935
+ if (workspaceReadiness.ready && expectedRepoUrls.length > 0 && !isAlanDefaultWorkspace(cwd)) {
24936
+ const worktreePaths = payload.worktreeInfo?.allWorktrees?.map((worktree) => worktree.worktreePath).filter((worktreePath) => Boolean(worktreePath));
24937
+ const repositoryMismatch = worktreePaths && worktreePaths.length > 0 ? worktreePaths.some(
24938
+ (worktreePath) => !workspaceMatchesExpectedRepositories({
24939
+ workspacePath: worktreePath,
24940
+ expectedRepoUrls
24941
+ })
24942
+ ) : !workspaceMatchesExpectedRepositories({ workspacePath: cwd, expectedRepoUrls });
24943
+ if (repositoryMismatch) {
24944
+ socket.emit("agent.rejected", {
24945
+ runId: payload.runId,
24946
+ message: "This folder belongs to a different repository. Choose the correct project folder and try again.",
24947
+ code: "workspace_repository_mismatch"
24948
+ });
24949
+ return;
24950
+ }
24310
24951
  }
24311
24952
  if (workspaceRequired) workspaceAccessTracker.track(cwd, workspaceReadiness);
24312
24953
  const runnerSummary = summarizeRunnerStatus({
@@ -24604,6 +25245,7 @@ async function startDaemon(args) {
24604
25245
  selectedContextWindow: payload.selectedContextWindow ?? null,
24605
25246
  selectedEffortLevel: payload.selectedEffortLevel ?? null,
24606
25247
  providerSessionId: payload.providerSessionId,
25248
+ resumeFallbackContext: payload.resumeFallbackContext,
24607
25249
  taskMeta: payload.taskMeta,
24608
25250
  prMeta: payload.prMeta,
24609
25251
  conversationId: payload.conversationId,
@@ -24640,11 +25282,13 @@ async function startDaemon(args) {
24640
25282
  if (!payload?.runId) return;
24641
25283
  const entry = activeAgents.get(payload.runId);
24642
25284
  if (!entry) return;
24643
- abortActiveAgent(entry, { userInitiated: true });
25285
+ const reason = payload.reason === "watchdog_timeout" ? "watchdog_timeout" : "user";
25286
+ abortActiveAgent(entry, { userInitiated: true, reason });
24644
25287
  activeAgents.delete(payload.runId);
24645
- pushLog(`aborted run=${payload.runId}`);
25288
+ pushLog(`aborted run=${payload.runId} reason=${reason}`);
24646
25289
  console.info("[alan-agent] Agent run aborted", {
24647
25290
  runId: payload.runId,
25291
+ reason,
24648
25292
  agentVersion: AGENT_VERSION
24649
25293
  });
24650
25294
  });
@@ -24652,7 +25296,9 @@ async function startDaemon(args) {
24652
25296
  "agent.tool_response",
24653
25297
  (payload) => {
24654
25298
  if (!payload?.toolId || typeof payload.response !== "string") return;
24655
- const entries = payload.runId ? [[payload.runId, activeAgents.get(payload.runId)]] : [...activeAgents.entries()];
25299
+ const entries = payload.runId ? [[payload.runId, activeAgents.get(payload.runId)]] : payload.conversationId ? [...activeAgents.entries()].filter(
25300
+ ([, entry]) => entry.conversationId === payload.conversationId
25301
+ ) : [...activeAgents.entries()];
24656
25302
  const delivered = entries.some(([runId, entry]) => {
24657
25303
  if (!entry || !verifyActiveWorkspaceLease(runId, entry)) return false;
24658
25304
  return entry.agent.sendToolResponse(payload.toolId, payload.response) === true;
@@ -24660,8 +25306,8 @@ async function startDaemon(args) {
24660
25306
  if (!delivered) {
24661
25307
  pushLog(`tool_response missed tool=${payload.toolId}`);
24662
25308
  const fallbackRun = [...activeAgents.entries()][0];
24663
- const resolvedRunId = payload.runId ?? fallbackRun?.[0];
24664
- const conversationId = (payload.runId ? activeAgents.get(payload.runId)?.conversationId : void 0) ?? fallbackRun?.[1].conversationId;
25309
+ const resolvedRunId = payload.runId ?? entries[0]?.[0] ?? fallbackRun?.[0];
25310
+ const conversationId = payload.conversationId ?? (payload.runId ? activeAgents.get(payload.runId)?.conversationId : void 0) ?? fallbackRun?.[1].conversationId;
24665
25311
  if (conversationId && resolvedRunId) {
24666
25312
  eventOutbox.enqueue(conversationId, resolvedRunId, {
24667
25313
  type: "error",
@@ -25444,12 +26090,18 @@ var WebPresenter = class {
25444
26090
  onLog(message) {
25445
26091
  console.error(message);
25446
26092
  }
26093
+ onNotice(kind, message) {
26094
+ this.wsClient.emitEvent({ type: "session_notice", kind, message, ts: Date.now() });
26095
+ }
25447
26096
  onAssistantText(text) {
25448
26097
  this.wsClient.emitEvent({ type: "text", text, ts: Date.now() });
25449
26098
  }
25450
26099
  onThinking(text) {
25451
26100
  this.wsClient.emitEvent({ type: "thinking", text, ts: Date.now() });
25452
26101
  }
26102
+ hasKnownToolUse(toolId) {
26103
+ return this.toolNamesById.has(toolId);
26104
+ }
25453
26105
  onToolUse(tool, input, toolId, parentToolUseId) {
25454
26106
  this.toolNamesById.set(toolId, tool);
25455
26107
  this.toolInputsById.set(toolId, input);
@@ -25541,8 +26193,13 @@ var WebPresenter = class {
25541
26193
  onCheckpoint(id) {
25542
26194
  this.wsClient.emitEvent({ type: "checkpoint", id });
25543
26195
  }
25544
- onActivity(status) {
25545
- this.wsClient.emitEvent({ type: "activity", status, ts: Date.now() });
26196
+ onActivity(status, evidence) {
26197
+ this.wsClient.emitEvent({
26198
+ type: "activity",
26199
+ status,
26200
+ ts: Date.now(),
26201
+ ...evidence ? { evidence } : {}
26202
+ });
25546
26203
  }
25547
26204
  onOpenBrowserTab(browserId, url2) {
25548
26205
  this.wsClient.emitEvent({ type: "open_browser_tab", browserId, ...url2 ? { url: url2 } : {} });
@@ -25583,7 +26240,7 @@ var WebPresenter = class {
25583
26240
  };
25584
26241
 
25585
26242
  // src/ws-client.ts
25586
- var import_node_crypto4 = require("crypto");
26243
+ var import_node_crypto5 = require("crypto");
25587
26244
 
25588
26245
  // ../shared/dist/agent-liveness.js
25589
26246
  var AGENT_LIVENESS_PROTOCOL_VERSION = 1;
@@ -25610,7 +26267,117 @@ var agentProbeAckSchema = external_exports.object({
25610
26267
  protocolVersion: external_exports.number().int().positive().optional()
25611
26268
  });
25612
26269
 
26270
+ // src/sandbox-outbox.ts
26271
+ var import_node_crypto4 = require("crypto");
26272
+ var import_node_fs13 = require("fs");
26273
+ var SANDBOX_EVENT_OUTBOX_ENV = "ALAN_EVENT_OUTBOX_ENABLED";
26274
+ function isSandboxEventOutboxEnabled(env = process.env) {
26275
+ return env[SANDBOX_EVENT_OUTBOX_ENV] === "true";
26276
+ }
26277
+ function deriveSandboxOutboxKey(sessionToken) {
26278
+ if (!sessionToken) {
26279
+ throw new Error("sandbox event outbox requires ALAN_SESSION_TOKEN for key derivation");
26280
+ }
26281
+ return (0, import_node_crypto4.createHash)("sha256").update(sessionToken, "utf8").digest("base64url");
26282
+ }
26283
+ function sandboxOutboxSpoolPath(conversationId) {
26284
+ return `/tmp/alan-agent-outbox-${conversationId}.enc`;
26285
+ }
26286
+ function createSandboxEventOutbox(input) {
26287
+ const path = input.spoolPath ?? sandboxOutboxSpoolPath(input.conversationId);
26288
+ const key = deriveSandboxOutboxKey(input.sessionToken);
26289
+ try {
26290
+ return new EncryptedEventOutbox(path, key, input.pushLog, input.options);
26291
+ } catch (error) {
26292
+ input.pushLog?.(
26293
+ `sandbox_outbox_spool_reset ${error instanceof Error ? error.message : String(error)}`
26294
+ );
26295
+ (0, import_node_fs13.rmSync)(path, { force: true });
26296
+ return new EncryptedEventOutbox(path, key, input.pushLog, input.options);
26297
+ }
26298
+ }
26299
+ var SandboxOutboxSocket = class {
26300
+ constructor(socket) {
26301
+ this.socket = socket;
26302
+ }
26303
+ seqByEventId = /* @__PURE__ */ new Map();
26304
+ seqCounter = 0;
26305
+ get connected() {
26306
+ return this.socket.connected;
26307
+ }
26308
+ emit(_event, payload, acknowledge) {
26309
+ let seq = this.seqByEventId.get(payload.eventId);
26310
+ if (seq === void 0) {
26311
+ this.seqCounter += 1;
26312
+ seq = this.seqCounter;
26313
+ this.seqByEventId.set(payload.eventId, seq);
26314
+ }
26315
+ const wirePayload = { ...payload.event, eventId: payload.eventId, seq };
26316
+ this.socket.emit("agent_event", wirePayload, (result) => {
26317
+ if (result?.ok) this.seqByEventId.delete(payload.eventId);
26318
+ acknowledge({ ok: Boolean(result?.ok), retryable: result?.retryable });
26319
+ });
26320
+ }
26321
+ };
26322
+ var SandboxEventDispatcher = class {
26323
+ conversationId;
26324
+ outbox;
26325
+ outboxSocket;
26326
+ currentRunId;
26327
+ constructor(input) {
26328
+ this.conversationId = input.conversationId;
26329
+ if (!input.enabled) return;
26330
+ try {
26331
+ this.outbox = createSandboxEventOutbox({
26332
+ conversationId: input.conversationId,
26333
+ sessionToken: input.sessionToken,
26334
+ pushLog: input.pushLog,
26335
+ options: input.options,
26336
+ spoolPath: input.spoolPath
26337
+ });
26338
+ this.outboxSocket = new SandboxOutboxSocket(input.socket);
26339
+ } catch (error) {
26340
+ input.onInitError?.(error);
26341
+ this.outbox = void 0;
26342
+ this.outboxSocket = void 0;
26343
+ }
26344
+ }
26345
+ /** True when events are being durably queued rather than fired-and-forgotten. */
26346
+ get active() {
26347
+ return this.outbox !== void 0 && this.outboxSocket !== void 0;
26348
+ }
26349
+ /** Set the run id used to bucket per-run caps / terminal condensation. */
26350
+ setRunId(runId) {
26351
+ this.currentRunId = runId;
26352
+ }
26353
+ /**
26354
+ * Durably deliver one event (enqueue-before-send, remove-on-ack). When the
26355
+ * outbox is inactive, defers to `fireAndForget` so behaviour is unchanged.
26356
+ */
26357
+ emit(event, fireAndForget) {
26358
+ if (!this.outbox || !this.outboxSocket) {
26359
+ fireAndForget(event);
26360
+ return;
26361
+ }
26362
+ this.outbox.enqueue(this.conversationId, this.currentRunId ?? this.conversationId, event);
26363
+ this.outbox.flush(this.outboxSocket);
26364
+ }
26365
+ /** Replay un-acked events in order — called on (re)connect before new events. */
26366
+ flush() {
26367
+ if (this.outbox && this.outboxSocket) this.outbox.flush(this.outboxSocket);
26368
+ }
26369
+ /** Reset in-flight state on disconnect so the next flush replays from the head. */
26370
+ disconnect() {
26371
+ this.outbox?.disconnect();
26372
+ }
26373
+ /** Diagnostics: number of events still awaiting a server ack. */
26374
+ pendingCount() {
26375
+ return this.outbox?.pendingCount() ?? 0;
26376
+ }
26377
+ };
26378
+
25613
26379
  // src/ws-client.ts
26380
+ var ACCEPTED_MESSAGE_ID_CACHE_SIZE = 200;
25614
26381
  var WSClient = class {
25615
26382
  constructor(wsUrl, sessionId, taskId, token, callbacks, agentVersion, lifecycle) {
25616
26383
  this.lifecycle = lifecycle;
@@ -25629,16 +26396,63 @@ var WSClient = class {
25629
26396
  upgrade: false,
25630
26397
  extraHeaders: { "ngrok-skip-browser-warning": "1" }
25631
26398
  });
26399
+ const outboxEnabled = isSandboxEventOutboxEnabled();
26400
+ this.eventDispatcher = new SandboxEventDispatcher({
26401
+ conversationId: sessionId,
26402
+ sessionToken: token,
26403
+ enabled: outboxEnabled,
26404
+ socket: this.socket,
26405
+ pushLog: (message) => this.lifecycle?.info("sandbox_agent_event_outbox", { detail: message }),
26406
+ onInitError: (error) => this.lifecycle?.warn("sandbox_agent_event_outbox_init_failed", {
26407
+ error: error instanceof Error ? error : new Error(String(error))
26408
+ })
26409
+ });
26410
+ if (this.eventDispatcher.active) {
26411
+ this.lifecycle?.info("sandbox_agent_event_outbox_enabled", { conversationId: sessionId });
26412
+ }
25632
26413
  this.setupListeners(callbacks);
25633
26414
  }
25634
26415
  socket;
25635
26416
  /** Stable id for this agent process; fences stale heartbeats server-side. */
25636
- agentSessionId = (0, import_node_crypto4.randomUUID)();
26417
+ agentSessionId = (0, import_node_crypto5.randomUUID)();
25637
26418
  /** Monotonic heartbeat sequence — advances on every hello + heartbeat. */
25638
26419
  heartbeatSeq = 0;
25639
26420
  heartbeatTimer = null;
25640
26421
  agentVersion;
25641
26422
  getRunState;
26423
+ /**
26424
+ * Durable at-least-once delivery for stream events. Active only when
26425
+ * `ALAN_EVENT_OUTBOX_ENABLED === "true"`; otherwise events are fired-and-
26426
+ * forgotten exactly as before (see {@link SandboxEventDispatcher}).
26427
+ */
26428
+ eventDispatcher;
26429
+ /**
26430
+ * Bounded record of `messageId`s whose handoff to `onUserMessage` already
26431
+ * completed successfully. A redelivered `user_message` for one of these ids
26432
+ * (e.g. the original `ok:true` ack was lost in transit and the server's
26433
+ * ≤5-attempt redelivery re-armed the run) is re-acked `{ ok: true }`
26434
+ * without invoking the handler again, so the message is never processed
26435
+ * twice. A `Set`'s insertion order doubles as recency, so eviction is just
26436
+ * "drop the oldest key" — a minimal LRU without extra bookkeeping.
26437
+ */
26438
+ acceptedMessageIds = /* @__PURE__ */ new Set();
26439
+ /** Record a successfully-handed-off messageId, evicting the oldest entry past the cap. */
26440
+ rememberAcceptedMessageId(messageId) {
26441
+ this.acceptedMessageIds.delete(messageId);
26442
+ this.acceptedMessageIds.add(messageId);
26443
+ if (this.acceptedMessageIds.size > ACCEPTED_MESSAGE_ID_CACHE_SIZE) {
26444
+ const oldest = this.acceptedMessageIds.keys().next().value;
26445
+ if (oldest !== void 0) this.acceptedMessageIds.delete(oldest);
26446
+ }
26447
+ }
26448
+ /**
26449
+ * Record the run id for events emitted from here on, so the durable outbox can
26450
+ * bucket per-run caps and terminal-event condensation correctly. No-op when the
26451
+ * outbox is disabled.
26452
+ */
26453
+ setCurrentRunId(runId) {
26454
+ this.eventDispatcher.setRunId(runId);
26455
+ }
25642
26456
  currentRunState() {
25643
26457
  try {
25644
26458
  return this.getRunState?.() ?? { idle: true, activeRunIds: [] };
@@ -25675,7 +26489,9 @@ var WSClient = class {
25675
26489
  }
25676
26490
  }
25677
26491
  emitEvent(event) {
25678
- this.socket.emit("agent_event", event);
26492
+ this.eventDispatcher.emit(event, (payload) => {
26493
+ this.socket.emit("agent_event", payload);
26494
+ });
25679
26495
  }
25680
26496
  waitForConnection(timeoutMs = 15e3) {
25681
26497
  return new Promise((resolve6, reject) => {
@@ -25712,11 +26528,13 @@ var WSClient = class {
25712
26528
  this.socket.on("connect", () => {
25713
26529
  this.lifecycle?.info("sandbox_agent_ws_socket_connected", { socketId: this.socket.id });
25714
26530
  this.startHeartbeat();
26531
+ this.eventDispatcher.flush();
25715
26532
  callbacks.onConnect?.();
25716
26533
  });
25717
26534
  this.socket.on("disconnect", (reason) => {
25718
26535
  this.lifecycle?.info("sandbox_agent_ws_socket_disconnected", { reason });
25719
26536
  this.stopHeartbeat();
26537
+ this.eventDispatcher.disconnect();
25720
26538
  callbacks.onDisconnect?.(reason);
25721
26539
  });
25722
26540
  this.socket.on("agent.probe", (_data, ack) => {
@@ -25734,8 +26552,11 @@ var WSClient = class {
25734
26552
  this.socket.on(
25735
26553
  "user_message",
25736
26554
  (data, ack) => {
25737
- ack?.({ ok: true });
25738
- callbacks.onUserMessage({
26555
+ if (data.messageId && this.acceptedMessageIds.has(data.messageId)) {
26556
+ ack?.({ ok: true });
26557
+ return;
26558
+ }
26559
+ const payload = {
25739
26560
  text: data.text,
25740
26561
  images: data.images,
25741
26562
  files: data.files,
@@ -25759,7 +26580,24 @@ var WSClient = class {
25759
26580
  conversationId: data.conversationId,
25760
26581
  teamId: data.teamId,
25761
26582
  currentUser: data.currentUser
25762
- });
26583
+ };
26584
+ const acceptDelivery = () => {
26585
+ if (data.messageId) this.rememberAcceptedMessageId(data.messageId);
26586
+ ack?.({ ok: true });
26587
+ };
26588
+ const rejectDelivery = (error) => {
26589
+ ack?.({ ok: false, message: error instanceof Error ? error.message : String(error) });
26590
+ };
26591
+ try {
26592
+ const result = callbacks.onUserMessage(payload);
26593
+ if (result instanceof Promise) {
26594
+ result.then(acceptDelivery, rejectDelivery);
26595
+ } else {
26596
+ acceptDelivery();
26597
+ }
26598
+ } catch (error) {
26599
+ rejectDelivery(error);
26600
+ }
25763
26601
  }
25764
26602
  );
25765
26603
  this.socket.on("stop", () => {
@@ -25928,6 +26766,7 @@ async function runSandbox(config) {
25928
26766
  backendKind: activeBackendKind
25929
26767
  });
25930
26768
  currentAgent = agent;
26769
+ wsClient.setCurrentRunId(currentRunId ?? config.runId);
25931
26770
  const correlation = correlationLogFields2({
25932
26771
  taskId: currentTaskId,
25933
26772
  conversationId: currentConversationId,
@@ -26145,7 +26984,7 @@ async function runSandbox(config) {
26145
26984
 
26146
26985
  // src/service-manager.ts
26147
26986
  var import_node_child_process6 = require("child_process");
26148
- var import_node_fs13 = require("fs");
26987
+ var import_node_fs14 = require("fs");
26149
26988
  var import_node_os8 = require("os");
26150
26989
  var import_node_path11 = require("path");
26151
26990
  var SERVICE_LABEL = "ai.tryalan.agent";
@@ -26344,14 +27183,14 @@ function currentServicePlan(args) {
26344
27183
  });
26345
27184
  }
26346
27185
  function writeManifest(path, contents) {
26347
- (0, import_node_fs13.mkdirSync)((0, import_node_path11.dirname)(path), { recursive: true, mode: 448 });
27186
+ (0, import_node_fs14.mkdirSync)((0, import_node_path11.dirname)(path), { recursive: true, mode: 448 });
26348
27187
  const pendingPath = `${path}.pending-${process.pid}`;
26349
27188
  try {
26350
- (0, import_node_fs13.writeFileSync)(pendingPath, contents, { mode: 384 });
26351
- (0, import_node_fs13.chmodSync)(pendingPath, 384);
26352
- (0, import_node_fs13.renameSync)(pendingPath, path);
27189
+ (0, import_node_fs14.writeFileSync)(pendingPath, contents, { mode: 384 });
27190
+ (0, import_node_fs14.chmodSync)(pendingPath, 384);
27191
+ (0, import_node_fs14.renameSync)(pendingPath, path);
26353
27192
  } finally {
26354
- (0, import_node_fs13.rmSync)(pendingPath, { force: true });
27193
+ (0, import_node_fs14.rmSync)(pendingPath, { force: true });
26355
27194
  }
26356
27195
  }
26357
27196
  function runServiceCommand(command) {
@@ -26374,7 +27213,7 @@ function installDaemonService(args = []) {
26374
27213
  const plan = currentServicePlan(args);
26375
27214
  if (plan.manifestPath && plan.manifest) {
26376
27215
  if ((0, import_node_os8.platform)() === "darwin") {
26377
- (0, import_node_fs13.mkdirSync)((0, import_node_path11.join)((0, import_node_os8.homedir)(), ".alan", "agent", "logs"), { recursive: true, mode: 448 });
27216
+ (0, import_node_fs14.mkdirSync)((0, import_node_path11.join)((0, import_node_os8.homedir)(), ".alan", "agent", "logs"), { recursive: true, mode: 448 });
26378
27217
  }
26379
27218
  writeManifest(plan.manifestPath, plan.manifest);
26380
27219
  }
@@ -26384,7 +27223,7 @@ function installDaemonService(args = []) {
26384
27223
  function uninstallDaemonService(args = []) {
26385
27224
  const plan = currentServicePlan(args);
26386
27225
  for (const command of plan.uninstallCommands) runServiceCommand(command);
26387
- if (plan.manifestPath) (0, import_node_fs13.rmSync)(plan.manifestPath, { force: true });
27226
+ if (plan.manifestPath) (0, import_node_fs14.rmSync)(plan.manifestPath, { force: true });
26388
27227
  console.info("[alan-agent] Per-user daemon service removed");
26389
27228
  }
26390
27229
  function printDaemonServiceStatus(args = []) {