@aiden-ade/sandbox-agent 0.1.36 → 0.1.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +872 -202
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -3404,7 +3404,7 @@ var require_websocket = __commonJS({
|
|
|
3404
3404
|
var http2 = require("http");
|
|
3405
3405
|
var net = require("net");
|
|
3406
3406
|
var tls = require("tls");
|
|
3407
|
-
var { randomBytes: randomBytes5, createHash:
|
|
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 =
|
|
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:
|
|
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 =
|
|
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([
|
|
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
|
-
|
|
4875
|
-
|
|
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)
|
|
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)
|
|
4904
|
+
if (nested && nested !== name && HARNESS_ASK_TOOL_NAMES.has(normalizeToolName(nested))) {
|
|
4906
4905
|
return true;
|
|
4907
4906
|
}
|
|
4908
|
-
|
|
4909
|
-
return true;
|
|
4910
|
-
}
|
|
4911
|
-
return false;
|
|
4907
|
+
return inputLooksLikeAskUserQuestion(input);
|
|
4912
4908
|
}
|
|
4913
4909
|
function isInteractiveToolName(name, input) {
|
|
4914
4910
|
const normalized = normalizeToolName(name);
|
|
@@ -9574,6 +9570,11 @@ var RUNNER_STATUS_PROTOCOL_VERSION = 2;
|
|
|
9574
9570
|
var RUNNER_STATUS_MIN_SUPPORTED_PROTOCOL_VERSION = Math.max(1, RUNNER_STATUS_PROTOCOL_VERSION - 1);
|
|
9575
9571
|
var RUNNER_STATUS_MAX_SUPPORTED_PROTOCOL_VERSION = RUNNER_STATUS_PROTOCOL_VERSION;
|
|
9576
9572
|
var RUNNER_RECOVERY_GRACE_MS = 2 * 60 * 1e3;
|
|
9573
|
+
var SYSTEM_ABORT_RUN_ERRORS = {
|
|
9574
|
+
watchdog_timeout: "Agent run was stopped by the server after a period of no stream activity",
|
|
9575
|
+
provider_exited: "Agent process exited unexpectedly",
|
|
9576
|
+
runner_stalled: "Agent run stalled with no recent activity and was recovered"
|
|
9577
|
+
};
|
|
9577
9578
|
function resolveAgentConfigFileName(profile) {
|
|
9578
9579
|
switch (profile) {
|
|
9579
9580
|
case "production":
|
|
@@ -13534,6 +13535,14 @@ var AGENT_CONFIG_KEYS = /* @__PURE__ */ new Map([
|
|
|
13534
13535
|
["localApiToken", "string"],
|
|
13535
13536
|
["displayName", "string"]
|
|
13536
13537
|
]);
|
|
13538
|
+
function sanitizeAgentConfig(config) {
|
|
13539
|
+
const sanitized = { ...config };
|
|
13540
|
+
delete sanitized.teamId;
|
|
13541
|
+
delete sanitized.organizationId;
|
|
13542
|
+
delete sanitized.currentTeamId;
|
|
13543
|
+
delete sanitized.currentOrganizationId;
|
|
13544
|
+
return sanitized;
|
|
13545
|
+
}
|
|
13537
13546
|
function isValidAgentConfig(configPath) {
|
|
13538
13547
|
try {
|
|
13539
13548
|
const stat = (0, import_node_fs4.lstatSync)(configPath);
|
|
@@ -13571,6 +13580,10 @@ function migrateLegacyAgentConfig(input) {
|
|
|
13571
13580
|
if (!alanDirectoryExisted) (0, import_node_fs4.chmodSync)(alanBaseDir, sourceDirectoryStat.mode & 511);
|
|
13572
13581
|
(0, import_node_fs4.copyFileSync)(legacyPath, alanPath, import_node_fs4.constants.COPYFILE_EXCL);
|
|
13573
13582
|
copied = true;
|
|
13583
|
+
const migrated = sanitizeAgentConfig(
|
|
13584
|
+
JSON.parse((0, import_node_fs4.readFileSync)(alanPath, "utf8"))
|
|
13585
|
+
);
|
|
13586
|
+
(0, import_node_fs4.writeFileSync)(alanPath, JSON.stringify(migrated, null, 2));
|
|
13574
13587
|
(0, import_node_fs4.chmodSync)(alanPath, sourceFileStat.mode & 511);
|
|
13575
13588
|
(0, import_node_fs4.utimesSync)(alanPath, sourceFileStat.atime, sourceFileStat.mtime);
|
|
13576
13589
|
if (!isValidAgentConfig(alanPath)) {
|
|
@@ -14498,33 +14511,6 @@ Deliverables persist as Alan artifacts and documents \u2014 not as chat. Plans,
|
|
|
14498
14511
|
For non-trivial tasks, log high-signal events as they happen: decisions, changed assumptions, validation results, known-good IDs, blockers, risks, and direction changes. Keep one current task-state summary updated with current state, known-good IDs, open risks, and next action. Do not log routine commands or file reads. Before stopping, ensure the timeline and current state are enough for a human to understand progress and for another agent to resume. If a prior decision is stale, write a newer event that supersedes it.
|
|
14499
14512
|
|
|
14500
14513
|
The specific work you do \u2014 implementation, planning, review, coordination, verification \u2014 is determined by your agent role, described next.`;
|
|
14501
|
-
var ALAN_CURSOR_ASK_USER_OVERLAY = `### Cursor runtime note
|
|
14502
|
-
Cursor native \`AskQuestion\` does not block stdin for Alan yet. When you need clickable question cards on Cursor, use \`mcp__alan__ask_user_question\` (server tool: \`ask_user_question\`) instead of native AskQuestion.`;
|
|
14503
|
-
var ALAN_ASK_USER_PROMPT = `## Asking the user
|
|
14504
|
-
|
|
14505
|
-
When you need a decision or information only the user can provide, do not guess \u2014 ask with an interactive ask tool so Alan can render clickable question cards.
|
|
14506
|
-
|
|
14507
|
-
### Tool preference (required)
|
|
14508
|
-
1. Prefer the **native** ask tool when it appears in your tool list (\`AskUserQuestion\`, or the backend's equivalent built-in ask tool).
|
|
14509
|
-
2. Only if native ask is missing, call \`mcp__alan__ask_user_question\` (server tool name: \`ask_user_question\`).
|
|
14510
|
-
|
|
14511
|
-
Required arguments (both native and MCP forms):
|
|
14512
|
-
- \`questions\`: non-empty array. Each item needs a clear \`question\` string.
|
|
14513
|
-
- Prefer 2\u20135 selectable \`options\` per question (\`label\` required; optional \`value\` / \`description\`). Free-text-only questions are allowed when options do not fit.
|
|
14514
|
-
- Do NOT include an "Other" option \u2014 the UI always adds a freeform Other\u2026 field.
|
|
14515
|
-
- Set \`multiSelect: true\` on a question when the user may pick more than one option; omit or set false for single-select (default).
|
|
14516
|
-
- Optional \`header\` per question and optional \`title\` for the card header.
|
|
14517
|
-
|
|
14518
|
-
If neither native ask nor the MCP ask tool is available, ask ONE short plain-text question \u2014 do not invent cards or pretend the UI exists.
|
|
14519
|
-
|
|
14520
|
-
### Never do this
|
|
14521
|
-
- Do NOT paste multiple-choice lists, A/B/C tables, or "reply with 1A / 1B" in chat.
|
|
14522
|
-
- Do NOT say "answer in the UI", "see the cards", "questions are above", or similar unless you just successfully called an ask tool with real \`questions\` in this turn.
|
|
14523
|
-
- Do NOT call the tool with empty \`questions\`, placeholder text, or missing options when choices exist.
|
|
14524
|
-
- Do NOT invent defaults when the user skips. If the next user message says they skipped, continue without those answers (or re-ask only if truly required).
|
|
14525
|
-
|
|
14526
|
-
### After the tool call
|
|
14527
|
-
The ask tool returns an empty/ack result and does NOT wait. Immediately end your turn. At most one short line like "Please pick an option above." \u2014 no restating the full questions in markdown. The user's answer (or skip) arrives as the next user message; continue from there.`;
|
|
14528
14514
|
function buildAlanTeamScopePrompt(teamId) {
|
|
14529
14515
|
return [
|
|
14530
14516
|
`You are operating within team \`${teamId}\`.`,
|
|
@@ -15216,20 +15202,23 @@ var import_path3 = require("path");
|
|
|
15216
15202
|
var import_readline = require("readline");
|
|
15217
15203
|
var import_child_process2 = require("child_process");
|
|
15218
15204
|
var import_crypto2 = require("crypto");
|
|
15219
|
-
var import_child_process3 = require("child_process");
|
|
15220
|
-
var import_util10 = require("util");
|
|
15221
15205
|
var import_fs4 = require("fs");
|
|
15222
15206
|
var import_os4 = require("os");
|
|
15223
15207
|
var import_path4 = require("path");
|
|
15208
|
+
var import_child_process3 = require("child_process");
|
|
15209
|
+
var import_util10 = require("util");
|
|
15224
15210
|
var import_fs5 = require("fs");
|
|
15225
15211
|
var import_os5 = require("os");
|
|
15226
15212
|
var import_path5 = require("path");
|
|
15227
15213
|
var import_fs6 = require("fs");
|
|
15228
15214
|
var import_os6 = require("os");
|
|
15229
15215
|
var import_path6 = require("path");
|
|
15230
|
-
var import_crypto3 = require("crypto");
|
|
15231
15216
|
var import_fs7 = require("fs");
|
|
15217
|
+
var import_os7 = require("os");
|
|
15232
15218
|
var import_path7 = require("path");
|
|
15219
|
+
var import_crypto3 = require("crypto");
|
|
15220
|
+
var import_fs8 = require("fs");
|
|
15221
|
+
var import_path8 = require("path");
|
|
15233
15222
|
var MANAGED_ALAN_MCP_SERVER_NAMES = [
|
|
15234
15223
|
"alan",
|
|
15235
15224
|
"alan-prod",
|
|
@@ -15724,6 +15713,10 @@ function getClaudePermissionMode(config) {
|
|
|
15724
15713
|
function getClaudeDisallowedTools(config) {
|
|
15725
15714
|
return isReadOnlyAgent(config.agentId) ? ["Edit", "Write", "MultiEdit", "NotebookEdit"] : [];
|
|
15726
15715
|
}
|
|
15716
|
+
function getCodexPermissionArgs(config) {
|
|
15717
|
+
const sandbox = shouldUseReadOnlyRuntimePermissions(config) ? "read-only" : "danger-full-access";
|
|
15718
|
+
return ["--ask-for-approval", "never", "--sandbox", sandbox];
|
|
15719
|
+
}
|
|
15727
15720
|
function isLikelyProviderAuthError(stderr) {
|
|
15728
15721
|
const lower = stderr.toLowerCase();
|
|
15729
15722
|
return lower.includes("api key") || lower.includes("api_key") || lower.includes("invalid api key") || lower.includes("api key invalid") || lower.includes("unauthorized") || lower.includes("authentication failed") || lower.includes("authentication error") || lower.includes("provider settings") || lower.includes("invalid token") || lower.includes("token expired");
|
|
@@ -15820,6 +15813,16 @@ function spawnCli(command, args, context) {
|
|
|
15820
15813
|
detached: process.platform !== "win32"
|
|
15821
15814
|
});
|
|
15822
15815
|
}
|
|
15816
|
+
function emitSessionNotice(presenter, kind, message) {
|
|
15817
|
+
if (presenter.onNotice) {
|
|
15818
|
+
void presenter.onNotice(kind, message);
|
|
15819
|
+
return;
|
|
15820
|
+
}
|
|
15821
|
+
presenter.onLog(`[session_notice:${kind}] ${message}`);
|
|
15822
|
+
}
|
|
15823
|
+
function logResumeObservability(event, fields) {
|
|
15824
|
+
console.info(`[agent-resume] ${event}`, { event, ...fields });
|
|
15825
|
+
}
|
|
15823
15826
|
function structuredEventKey(event) {
|
|
15824
15827
|
const type = typeof event.type === "string" && event.type ? event.type : "";
|
|
15825
15828
|
const role = typeof event.role === "string" && event.role ? event.role : "";
|
|
@@ -15889,7 +15892,12 @@ function createGenericCliBackend(options) {
|
|
|
15889
15892
|
const args = options.buildArgs?.(context, prompt) ?? options.args.map((arg) => arg === "{{prompt}}" ? prompt : arg);
|
|
15890
15893
|
const child = spawnCli(options.command, args, context);
|
|
15891
15894
|
state.process = child;
|
|
15895
|
+
state.lastRawOutputAtMs = Date.now();
|
|
15892
15896
|
context.onProcessSpawned?.(child);
|
|
15897
|
+
context.registerLivenessProbe?.(() => ({
|
|
15898
|
+
providerAlive: child.exitCode === null,
|
|
15899
|
+
lastRawOutputAgoMs: typeof state.lastRawOutputAtMs === "number" ? Date.now() - state.lastRawOutputAtMs : null
|
|
15900
|
+
}));
|
|
15893
15901
|
const safeArgs = args.map(
|
|
15894
15902
|
(a, i) => i > 0 && args[i - 1] === "--system-prompt" ? `"<system-prompt ${a.length} chars>"` : a
|
|
15895
15903
|
);
|
|
@@ -15938,6 +15946,7 @@ function createGenericCliBackend(options) {
|
|
|
15938
15946
|
}
|
|
15939
15947
|
stdoutRl.on("line", (line) => {
|
|
15940
15948
|
sawStdoutLine = true;
|
|
15949
|
+
state.lastRawOutputAtMs = Date.now();
|
|
15941
15950
|
presenter.recordRawTranscript?.("stdout", line);
|
|
15942
15951
|
if (options.parseStructuredLine) {
|
|
15943
15952
|
options.parseStructuredLine(line, context, state);
|
|
@@ -15955,6 +15964,7 @@ function createGenericCliBackend(options) {
|
|
|
15955
15964
|
}
|
|
15956
15965
|
});
|
|
15957
15966
|
stderrRl.on("line", (line) => {
|
|
15967
|
+
state.lastRawOutputAtMs = Date.now();
|
|
15958
15968
|
presenter.recordRawTranscript?.("stderr", line);
|
|
15959
15969
|
if (options.parseStderrLine) {
|
|
15960
15970
|
options.parseStderrLine(line, context, state);
|
|
@@ -16001,7 +16011,7 @@ function createGenericCliBackend(options) {
|
|
|
16001
16011
|
if (presenter.pendingAskUserToolIds?.size) {
|
|
16002
16012
|
idleTimeoutReason = "pending_user_answer";
|
|
16003
16013
|
resolve22("idle_timeout");
|
|
16004
|
-
} else if (state.activeBackgroundTaskIds?.size) {
|
|
16014
|
+
} else if (state.resultDeferredOnBackgroundWork && state.activeBackgroundTaskIds?.size) {
|
|
16005
16015
|
idleTimeoutReason = "background_task";
|
|
16006
16016
|
resolve22("idle_timeout");
|
|
16007
16017
|
} else {
|
|
@@ -16064,10 +16074,26 @@ function createGenericCliBackend(options) {
|
|
|
16064
16074
|
logUnhandledStructuredEventSummary(options.kind, state);
|
|
16065
16075
|
}
|
|
16066
16076
|
const requestedResumeId = context.config.runtimeSessionId?.trim() || context.config.providerSessionId?.trim();
|
|
16077
|
+
let cursorResumeSilentlyFailed = false;
|
|
16067
16078
|
if (requestedResumeId && state.runtimeSessionId && state.runtimeSessionId !== requestedResumeId) {
|
|
16068
16079
|
console.warn(
|
|
16069
16080
|
`[${options.kind}] CLI reported session id ${state.runtimeSessionId} after resume was requested with ${requestedResumeId} \u2014 expected for claude session forks, a failed-resume signal for cursor-agent`
|
|
16070
16081
|
);
|
|
16082
|
+
if (options.kind === "cursor_agent_cli") {
|
|
16083
|
+
cursorResumeSilentlyFailed = true;
|
|
16084
|
+
logResumeObservability("agent_resume_failed", {
|
|
16085
|
+
backendKind: options.kind,
|
|
16086
|
+
reason: "cli_forked_fresh_session",
|
|
16087
|
+
requestedResumeId,
|
|
16088
|
+
runtimeSessionId: state.runtimeSessionId,
|
|
16089
|
+
hadFallbackContext: Boolean(context.config.resumeFallbackContext?.trim())
|
|
16090
|
+
});
|
|
16091
|
+
emitSessionNotice(
|
|
16092
|
+
context.presenter,
|
|
16093
|
+
"session_resume_failed",
|
|
16094
|
+
"The previous session could not be resumed, so this reply was generated without earlier conversation context. Retrying with the recent history re-included."
|
|
16095
|
+
);
|
|
16096
|
+
}
|
|
16071
16097
|
}
|
|
16072
16098
|
if (context.abortController.signal.aborted) {
|
|
16073
16099
|
return {
|
|
@@ -16161,6 +16187,22 @@ function createGenericCliBackend(options) {
|
|
|
16161
16187
|
const stderrText = stderrLines.join("\n").trim();
|
|
16162
16188
|
const hasStructuredError = exitCode === 0 && !!state.error?.trim();
|
|
16163
16189
|
const failed = exitCode !== 0 || hasStructuredError;
|
|
16190
|
+
if (cursorResumeSilentlyFailed && !failed) {
|
|
16191
|
+
return {
|
|
16192
|
+
success: false,
|
|
16193
|
+
summary: state.summary.trim() || "Session resume failed",
|
|
16194
|
+
filesModified: [],
|
|
16195
|
+
planFilesCreated: [],
|
|
16196
|
+
iterations: Math.max(state.iterations, 1),
|
|
16197
|
+
error: SESSION_RESUME_FAILED_MESSAGE,
|
|
16198
|
+
errorKind: "resume_failed",
|
|
16199
|
+
providerSessionId: state.runtimeSessionId,
|
|
16200
|
+
runtimeSessionId: state.runtimeSessionId,
|
|
16201
|
+
backendKind: options.kind,
|
|
16202
|
+
supportTier: options.supportTier,
|
|
16203
|
+
usage: state.usage
|
|
16204
|
+
};
|
|
16205
|
+
}
|
|
16164
16206
|
const summary = state.summary.trim() || state.error?.trim() || (failed ? "Task failed" : "Task completed");
|
|
16165
16207
|
return {
|
|
16166
16208
|
success: !failed,
|
|
@@ -16610,6 +16652,21 @@ function createAntigravityCliBackend(command = "agy", defaultArgs = []) {
|
|
|
16610
16652
|
}
|
|
16611
16653
|
};
|
|
16612
16654
|
}
|
|
16655
|
+
function resolveClaudeHome(env, variant = "claude") {
|
|
16656
|
+
if (variant === "supatest") {
|
|
16657
|
+
return (0, import_path4.join)((0, import_os4.homedir)(), ".supatest", "claude-internal");
|
|
16658
|
+
}
|
|
16659
|
+
return env.CLAUDE_CONFIG_DIR || process.env.CLAUDE_CONFIG_DIR || (0, import_path4.join)((0, import_os4.homedir)(), ".claude");
|
|
16660
|
+
}
|
|
16661
|
+
function encodeClaudeProjectDir(cwd) {
|
|
16662
|
+
return cwd.replace(/[^a-zA-Z0-9]/g, "-");
|
|
16663
|
+
}
|
|
16664
|
+
function claudeSessionLogExists(input) {
|
|
16665
|
+
if (!input.sessionId || !input.cwd) return false;
|
|
16666
|
+
const projectDir = (0, import_path4.join)(input.home, "projects", encodeClaudeProjectDir(input.cwd));
|
|
16667
|
+
if (!(0, import_fs4.existsSync)(projectDir)) return false;
|
|
16668
|
+
return (0, import_fs4.existsSync)((0, import_path4.join)(projectDir, `${input.sessionId}.jsonl`));
|
|
16669
|
+
}
|
|
16613
16670
|
var execFileAsync = (0, import_util10.promisify)(import_child_process3.execFile);
|
|
16614
16671
|
var flagSupportCache = /* @__PURE__ */ new Map();
|
|
16615
16672
|
function helpAdvertisesFlag(helpText, flag) {
|
|
@@ -16660,15 +16717,38 @@ function registerBackgroundTaskIds(state, keys) {
|
|
|
16660
16717
|
state.activeBackgroundTaskIds.add(key);
|
|
16661
16718
|
}
|
|
16662
16719
|
}
|
|
16663
|
-
function
|
|
16720
|
+
function registerBackgroundLauncher(state, toolUseId) {
|
|
16721
|
+
registerBackgroundTaskIds(state, [toolUseId]);
|
|
16722
|
+
if (!state.pendingLauncherToolIds) state.pendingLauncherToolIds = [];
|
|
16723
|
+
if (!state.pendingLauncherToolIds.includes(toolUseId)) {
|
|
16724
|
+
state.pendingLauncherToolIds.push(toolUseId);
|
|
16725
|
+
}
|
|
16726
|
+
}
|
|
16727
|
+
function drainNextUnlinkedBackgroundLauncher(state) {
|
|
16728
|
+
const launcherId = state.pendingLauncherToolIds?.shift();
|
|
16729
|
+
if (state.pendingLauncherToolIds?.length === 0) state.pendingLauncherToolIds = void 0;
|
|
16730
|
+
if (launcherId) clearBackgroundTaskIds(state, [launcherId]);
|
|
16731
|
+
return launcherId;
|
|
16732
|
+
}
|
|
16733
|
+
function registerBackgroundTaskRecord(state, record, launcherToolUseId) {
|
|
16664
16734
|
const taskId = backgroundTaskId(record);
|
|
16665
|
-
let toolUseId = backgroundToolUseId(record);
|
|
16666
|
-
if (!toolUseId && taskId
|
|
16667
|
-
const
|
|
16668
|
-
if (
|
|
16735
|
+
let toolUseId = backgroundToolUseId(record) ?? launcherToolUseId ?? void 0;
|
|
16736
|
+
if (!toolUseId && taskId) {
|
|
16737
|
+
const nextLauncher = state.pendingLauncherToolIds?.[0];
|
|
16738
|
+
if (nextLauncher) {
|
|
16739
|
+
toolUseId = nextLauncher;
|
|
16740
|
+
} else if (state.activeBackgroundTaskIds?.size === 1) {
|
|
16741
|
+
const [candidate] = state.activeBackgroundTaskIds;
|
|
16742
|
+
if (candidate && candidate !== taskId) toolUseId = candidate;
|
|
16743
|
+
}
|
|
16669
16744
|
}
|
|
16670
16745
|
registerBackgroundTaskIds(state, backgroundTaskKeys(record));
|
|
16671
16746
|
if (!taskId || !toolUseId) return;
|
|
16747
|
+
if (state.pendingLauncherToolIds) {
|
|
16748
|
+
const idx = state.pendingLauncherToolIds.indexOf(toolUseId);
|
|
16749
|
+
if (idx >= 0) state.pendingLauncherToolIds.splice(idx, 1);
|
|
16750
|
+
if (state.pendingLauncherToolIds.length === 0) state.pendingLauncherToolIds = void 0;
|
|
16751
|
+
}
|
|
16672
16752
|
if (!state.backgroundToolIdByTaskId) state.backgroundToolIdByTaskId = /* @__PURE__ */ new Map();
|
|
16673
16753
|
if (!state.backgroundTaskIdsByToolId) state.backgroundTaskIdsByToolId = /* @__PURE__ */ new Map();
|
|
16674
16754
|
state.backgroundToolIdByTaskId.set(taskId, toolUseId);
|
|
@@ -16699,6 +16779,10 @@ function clearBackgroundTaskIds(state, keys) {
|
|
|
16699
16779
|
state.backgroundTaskIdsByToolId?.delete(key);
|
|
16700
16780
|
}
|
|
16701
16781
|
}
|
|
16782
|
+
if (state.pendingLauncherToolIds) {
|
|
16783
|
+
state.pendingLauncherToolIds = state.pendingLauncherToolIds.filter((id) => activeIds.has(id));
|
|
16784
|
+
if (state.pendingLauncherToolIds.length === 0) state.pendingLauncherToolIds = void 0;
|
|
16785
|
+
}
|
|
16702
16786
|
if (activeIds.size === 0) state.activeBackgroundTaskIds = void 0;
|
|
16703
16787
|
}
|
|
16704
16788
|
function resolveBackgroundTaskToolUseId(state, record) {
|
|
@@ -16721,6 +16805,7 @@ function deferOrFinalizeStructuredResult(kind, context, state) {
|
|
|
16721
16805
|
console.info(
|
|
16722
16806
|
`[${kind}] Keeping stdin open \u2014 ${pendingAsks} interactive question(s) awaiting user response`
|
|
16723
16807
|
);
|
|
16808
|
+
state.resultDeferredOnPendingAnswer = true;
|
|
16724
16809
|
return "deferred_pending_answer";
|
|
16725
16810
|
}
|
|
16726
16811
|
const activeBackgroundTasks = state.activeBackgroundTaskIds?.size ?? 0;
|
|
@@ -16735,9 +16820,10 @@ function deferOrFinalizeStructuredResult(kind, context, state) {
|
|
|
16735
16820
|
return "finalized";
|
|
16736
16821
|
}
|
|
16737
16822
|
function maybeFinalizeDeferredResult(kind, context, state) {
|
|
16738
|
-
if (!state.resultDeferredOnBackgroundWork) return;
|
|
16823
|
+
if (!state.resultDeferredOnBackgroundWork && !state.resultDeferredOnPendingAnswer) return;
|
|
16739
16824
|
if (context.presenter.pendingAskUserToolIds?.size || state.activeBackgroundTaskIds?.size) return;
|
|
16740
16825
|
state.resultDeferredOnBackgroundWork = false;
|
|
16826
|
+
state.resultDeferredOnPendingAnswer = false;
|
|
16741
16827
|
finalizeStructuredResult(kind, state);
|
|
16742
16828
|
}
|
|
16743
16829
|
var DEBUG_ASK = process.env.ALAN_DEBUG_ASK === "1" || process.env.ALAN_DEBUG === "1";
|
|
@@ -16807,21 +16893,44 @@ function handleClaudeStructuredEvent(parsed, context, state) {
|
|
|
16807
16893
|
return true;
|
|
16808
16894
|
}
|
|
16809
16895
|
if (parsed.subtype === "task_started") {
|
|
16810
|
-
registerBackgroundTaskRecord(state, parsed);
|
|
16896
|
+
registerBackgroundTaskRecord(state, parsed, parentToolUseId);
|
|
16897
|
+
return true;
|
|
16898
|
+
}
|
|
16899
|
+
if (parsed.subtype === "compact_boundary") {
|
|
16900
|
+
const compactMetadata = typeof parsed.compact_metadata === "object" && parsed.compact_metadata !== null ? parsed.compact_metadata : {};
|
|
16901
|
+
const trigger = typeof compactMetadata.trigger === "string" ? compactMetadata.trigger : typeof parsed.trigger === "string" ? parsed.trigger : "auto";
|
|
16902
|
+
const preTokens = typeof compactMetadata.pre_tokens === "number" ? compactMetadata.pre_tokens : typeof parsed.pre_tokens === "number" ? parsed.pre_tokens : void 0;
|
|
16903
|
+
const tokensPart = typeof preTokens === "number" && preTokens > 0 ? ` (${Math.round(preTokens / 1e3)}k tokens summarized)` : "";
|
|
16904
|
+
emitSessionNotice(
|
|
16905
|
+
presenter,
|
|
16906
|
+
"context_compacted",
|
|
16907
|
+
trigger === "manual" ? `Conversation context was compacted${tokensPart} \u2014 earlier detail was replaced with a summary.` : `Conversation context was automatically compacted${tokensPart} to stay within the model's limit \u2014 earlier detail was replaced with a summary.`
|
|
16908
|
+
);
|
|
16811
16909
|
return true;
|
|
16812
16910
|
}
|
|
16813
16911
|
if (parsed.subtype === "task_notification") {
|
|
16814
16912
|
if (isTerminalTaskNotification(parsed)) {
|
|
16815
16913
|
const toolUseId = resolveBackgroundTaskToolUseId(state, parsed);
|
|
16816
16914
|
const wasActive = !!toolUseId && state.activeBackgroundTaskIds?.has(toolUseId) ? true : backgroundTaskKeys(parsed).some((key) => state.activeBackgroundTaskIds?.has(key));
|
|
16915
|
+
const isFailed = isFailedTaskNotification(parsed) ? true : void 0;
|
|
16817
16916
|
if (toolUseId && wasActive) {
|
|
16818
16917
|
void presenter.onToolResult?.(
|
|
16819
16918
|
toolUseId,
|
|
16820
16919
|
formatClaudeTaskNotificationResult(parsed),
|
|
16821
|
-
|
|
16920
|
+
isFailed
|
|
16822
16921
|
);
|
|
16922
|
+
clearBackgroundTaskIds(state, backgroundTaskKeys(parsed));
|
|
16923
|
+
} else {
|
|
16924
|
+
clearBackgroundTaskIds(state, backgroundTaskKeys(parsed));
|
|
16925
|
+
const drainedLauncher = drainNextUnlinkedBackgroundLauncher(state);
|
|
16926
|
+
if (drainedLauncher) {
|
|
16927
|
+
void presenter.onToolResult?.(
|
|
16928
|
+
drainedLauncher,
|
|
16929
|
+
formatClaudeTaskNotificationResult(parsed),
|
|
16930
|
+
isFailed
|
|
16931
|
+
);
|
|
16932
|
+
}
|
|
16823
16933
|
}
|
|
16824
|
-
clearBackgroundTaskIds(state, backgroundTaskKeys(parsed));
|
|
16825
16934
|
maybeFinalizeDeferredResult("claude_cli", context, state);
|
|
16826
16935
|
}
|
|
16827
16936
|
return true;
|
|
@@ -16829,9 +16938,10 @@ function handleClaudeStructuredEvent(parsed, context, state) {
|
|
|
16829
16938
|
return false;
|
|
16830
16939
|
}
|
|
16831
16940
|
case "assistant": {
|
|
16832
|
-
if (
|
|
16941
|
+
if (state.awaitingAskDenialFallback) {
|
|
16942
|
+
state.awaitingAskDenialFallback = false;
|
|
16833
16943
|
console.info(
|
|
16834
|
-
|
|
16944
|
+
"[claude_cli] Suppressing auto-denial fallback assistant turn after native AskUserQuestion"
|
|
16835
16945
|
);
|
|
16836
16946
|
return true;
|
|
16837
16947
|
}
|
|
@@ -16860,7 +16970,7 @@ function handleClaudeStructuredEvent(parsed, context, state) {
|
|
|
16860
16970
|
parentToolUseId
|
|
16861
16971
|
);
|
|
16862
16972
|
if (CLAUDE_SUBAGENT_TOOL_NAMES.has(toolBlock.name.toLowerCase())) {
|
|
16863
|
-
|
|
16973
|
+
registerBackgroundLauncher(state, toolBlock.id);
|
|
16864
16974
|
}
|
|
16865
16975
|
const normalizedName = normalizeToolName(toolBlock.name);
|
|
16866
16976
|
if (isInteractiveToolName(toolBlock.name) || INTERACTIVE_TOOL_NAMES.has(normalizedName)) {
|
|
@@ -16871,6 +16981,10 @@ function handleClaudeStructuredEvent(parsed, context, state) {
|
|
|
16871
16981
|
if (isAskUserQuestionTool(toolBlock.name)) {
|
|
16872
16982
|
if (!presenter.pendingAskUserToolIds) presenter.pendingAskUserToolIds = /* @__PURE__ */ new Set();
|
|
16873
16983
|
presenter.pendingAskUserToolIds.add(toolBlock.id);
|
|
16984
|
+
if (!toolBlock.name.toLowerCase().startsWith("mcp__")) {
|
|
16985
|
+
if (!state.nativeAskToolIds) state.nativeAskToolIds = /* @__PURE__ */ new Set();
|
|
16986
|
+
state.nativeAskToolIds.add(toolBlock.id);
|
|
16987
|
+
}
|
|
16874
16988
|
debugAskLog(
|
|
16875
16989
|
`[claude_cli] AskUserQuestion tool_use detected \u2014 id=${toolBlock.id}, name=${toolBlock.name}, pendingSize=${presenter.pendingAskUserToolIds.size}, presenterHasProp=${Object.hasOwn(presenter, "pendingAskUserToolIds")}`
|
|
16876
16990
|
);
|
|
@@ -16895,6 +17009,22 @@ function handleClaudeStructuredEvent(parsed, context, state) {
|
|
|
16895
17009
|
for (const block of content) {
|
|
16896
17010
|
if (typeof block === "object" && block !== null && "type" in block && block.type === "tool_result" && "tool_use_id" in block && typeof block.tool_use_id === "string") {
|
|
16897
17011
|
if (state.suppressedToolResultIds?.has(block.tool_use_id)) {
|
|
17012
|
+
const isSuppressedError = "is_error" in block && block.is_error === true;
|
|
17013
|
+
const isNativeAsk = state.nativeAskToolIds?.has(block.tool_use_id) === true;
|
|
17014
|
+
if (isSuppressedError && !isNativeAsk && presenter.pendingAskUserToolIds?.has(block.tool_use_id)) {
|
|
17015
|
+
presenter.pendingAskUserToolIds.delete(block.tool_use_id);
|
|
17016
|
+
state.suppressedToolResultIds.delete(block.tool_use_id);
|
|
17017
|
+
const resultText2 = extractToolResultContent(block.content);
|
|
17018
|
+
console.info(
|
|
17019
|
+
`[claude_cli] Ask tool ${block.tool_use_id} returned an error \u2014 clearing pending ask so the run can finish`
|
|
17020
|
+
);
|
|
17021
|
+
void presenter.onToolResult?.(block.tool_use_id, resultText2, true, parentToolUseId);
|
|
17022
|
+
maybeFinalizeDeferredResult("claude_cli", context, state);
|
|
17023
|
+
continue;
|
|
17024
|
+
}
|
|
17025
|
+
if (isNativeAsk) {
|
|
17026
|
+
state.awaitingAskDenialFallback = true;
|
|
17027
|
+
}
|
|
16898
17028
|
console.info(
|
|
16899
17029
|
`[claude_cli] Suppressed auto-generated tool_result for ${block.tool_use_id}`
|
|
16900
17030
|
);
|
|
@@ -17002,7 +17132,31 @@ function createClaudeCliBackend(command = "claude", defaultArgs = []) {
|
|
|
17002
17132
|
if (disallowedTools.length > 0) {
|
|
17003
17133
|
args.push("--disallowedTools", disallowedTools.join(","));
|
|
17004
17134
|
}
|
|
17005
|
-
const
|
|
17135
|
+
const requestedResumeId = context.config.runtimeSessionId?.trim() || context.config.providerSessionId?.trim();
|
|
17136
|
+
let resumeId = requestedResumeId;
|
|
17137
|
+
let resumeContextPrefix;
|
|
17138
|
+
if (requestedResumeId && !claudeSessionLogExists({
|
|
17139
|
+
sessionId: requestedResumeId,
|
|
17140
|
+
cwd: context.cwd,
|
|
17141
|
+
home: resolveClaudeHome(context.env)
|
|
17142
|
+
})) {
|
|
17143
|
+
resumeId = void 0;
|
|
17144
|
+
resumeContextPrefix = context.config.resumeFallbackContext?.trim() || void 0;
|
|
17145
|
+
console.warn(
|
|
17146
|
+
`[claude_cli] No session log for ${requestedResumeId} under cwd ${context.cwd}; starting a fresh session instead of resuming`
|
|
17147
|
+
);
|
|
17148
|
+
logResumeObservability("agent_resume_fallback_used", {
|
|
17149
|
+
backendKind: "claude_cli",
|
|
17150
|
+
reason: "session_log_missing_for_cwd",
|
|
17151
|
+
requestedResumeId,
|
|
17152
|
+
hadFallbackContext: Boolean(resumeContextPrefix)
|
|
17153
|
+
});
|
|
17154
|
+
emitSessionNotice(
|
|
17155
|
+
context.presenter,
|
|
17156
|
+
"session_resume_failed",
|
|
17157
|
+
resumeContextPrefix ? "The previous Claude session was created in a different working directory and could not be resumed here, so a fresh session was started with the recent conversation history re-included." : "The previous Claude session was created in a different working directory and could not be resumed here, so a fresh session was started. Earlier conversation context may be missing from this reply."
|
|
17158
|
+
);
|
|
17159
|
+
}
|
|
17006
17160
|
if (!resumeId && context.config.systemPrompt?.trim()) {
|
|
17007
17161
|
args.push("--system-prompt", context.config.systemPrompt.trim());
|
|
17008
17162
|
} else if (resumeId && context.config.systemPromptAppend?.trim()) {
|
|
@@ -17022,6 +17176,10 @@ function createClaudeCliBackend(command = "claude", defaultArgs = []) {
|
|
|
17022
17176
|
if (resumeId) {
|
|
17023
17177
|
args.push("--resume", resumeId);
|
|
17024
17178
|
console.info("[claude_cli] Resuming session", { resumeId });
|
|
17179
|
+
logResumeObservability("agent_resume_requested", {
|
|
17180
|
+
backendKind: "claude_cli",
|
|
17181
|
+
requestedResumeId: resumeId
|
|
17182
|
+
});
|
|
17025
17183
|
}
|
|
17026
17184
|
args.push("--chrome");
|
|
17027
17185
|
console.info("[claude_cli] Chrome flag added \u2014 final args:", args.join(" "));
|
|
@@ -17042,7 +17200,10 @@ function createClaudeCliBackend(command = "claude", defaultArgs = []) {
|
|
|
17042
17200
|
});
|
|
17043
17201
|
}
|
|
17044
17202
|
}
|
|
17045
|
-
const
|
|
17203
|
+
const basePromptText = resumeContextPrefix ? `${resumeContextPrefix}
|
|
17204
|
+
|
|
17205
|
+
${ctx.promptText}` : ctx.promptText;
|
|
17206
|
+
const promptText = ctx.config.mode === "plan" ? buildPlanModePrefix(buildPromptWithSystem(ctx.config, basePromptText)) : buildPromptWithSystem(ctx.config, basePromptText);
|
|
17046
17207
|
if (promptText.trim()) {
|
|
17047
17208
|
contentBlocks.push({ type: "text", text: promptText });
|
|
17048
17209
|
}
|
|
@@ -17093,27 +17254,30 @@ function normalizeCodexMcpToolResult(output) {
|
|
|
17093
17254
|
}
|
|
17094
17255
|
return raw;
|
|
17095
17256
|
}
|
|
17257
|
+
function resolveCodexHome(env) {
|
|
17258
|
+
return env.CODEX_HOME || process.env.CODEX_HOME || (0, import_path5.join)((0, import_os5.homedir)(), ".codex");
|
|
17259
|
+
}
|
|
17096
17260
|
function findCodexSessionLog(runtimeSessionId, codexHome) {
|
|
17097
|
-
if (!runtimeSessionId || !(0,
|
|
17098
|
-
const root = (0,
|
|
17099
|
-
if (!(0,
|
|
17261
|
+
if (!runtimeSessionId || !(0, import_fs5.existsSync)(codexHome)) return null;
|
|
17262
|
+
const root = (0, import_path5.join)(codexHome, "sessions");
|
|
17263
|
+
if (!(0, import_fs5.existsSync)(root)) return null;
|
|
17100
17264
|
const matches = [];
|
|
17101
17265
|
const stack = [root];
|
|
17102
17266
|
while (stack.length > 0) {
|
|
17103
17267
|
const dir = stack.pop();
|
|
17104
17268
|
let entries;
|
|
17105
17269
|
try {
|
|
17106
|
-
entries = (0,
|
|
17270
|
+
entries = (0, import_fs5.readdirSync)(dir, { withFileTypes: true });
|
|
17107
17271
|
} catch {
|
|
17108
17272
|
continue;
|
|
17109
17273
|
}
|
|
17110
17274
|
for (const entry of entries) {
|
|
17111
|
-
const path = (0,
|
|
17275
|
+
const path = (0, import_path5.join)(dir, entry.name);
|
|
17112
17276
|
if (entry.isDirectory()) {
|
|
17113
17277
|
stack.push(path);
|
|
17114
17278
|
} else if (entry.isFile() && entry.name.includes(runtimeSessionId) && entry.name.endsWith(".jsonl")) {
|
|
17115
17279
|
try {
|
|
17116
|
-
matches.push({ path, mtimeMs: (0,
|
|
17280
|
+
matches.push({ path, mtimeMs: (0, import_fs5.statSync)(path).mtimeMs });
|
|
17117
17281
|
} catch {
|
|
17118
17282
|
matches.push({ path, mtimeMs: 0 });
|
|
17119
17283
|
}
|
|
@@ -17146,12 +17310,14 @@ function latestUserMessageLineIndex(lines) {
|
|
|
17146
17310
|
async function replayCodexMcpToolEventsFromSessionLog(context, state) {
|
|
17147
17311
|
const runtimeSessionId = state.runtimeSessionId;
|
|
17148
17312
|
if (!runtimeSessionId) return;
|
|
17149
|
-
|
|
17313
|
+
if (!state.iterations || state.iterations <= 0) return;
|
|
17314
|
+
const codexHome = resolveCodexHome(context.env);
|
|
17150
17315
|
const logPath = findCodexSessionLog(runtimeSessionId, codexHome);
|
|
17151
17316
|
if (!logPath) return;
|
|
17152
|
-
const allLines = (0,
|
|
17317
|
+
const allLines = (0, import_fs5.readFileSync)(logPath, "utf8").split(/\r?\n/).filter(Boolean);
|
|
17153
17318
|
const latestUserLineIndex = latestUserMessageLineIndex(allLines);
|
|
17154
17319
|
const lines = latestUserLineIndex >= 0 ? allLines.slice(latestUserLineIndex + 1) : allLines;
|
|
17320
|
+
const streamedToolIds = state.codexStreamedToolIds ?? /* @__PURE__ */ new Set();
|
|
17155
17321
|
const emittedToolIds = /* @__PURE__ */ new Set();
|
|
17156
17322
|
for (const line of lines) {
|
|
17157
17323
|
const entry = parseJsonObject(line);
|
|
@@ -17162,6 +17328,7 @@ async function replayCodexMcpToolEventsFromSessionLog(context, state) {
|
|
|
17162
17328
|
const name = typeof payload.name === "string" ? payload.name : "";
|
|
17163
17329
|
const namespace = typeof payload.namespace === "string" ? payload.namespace : "";
|
|
17164
17330
|
if (!callId || !name || !namespace.startsWith("mcp__")) continue;
|
|
17331
|
+
if (streamedToolIds.has(callId)) continue;
|
|
17165
17332
|
const toolName = `${namespace.replace(/_+$/, "")}__${name}`;
|
|
17166
17333
|
context.presenter.onToolUse(toolName, parseMaybeJson(payload.arguments), callId);
|
|
17167
17334
|
emittedToolIds.add(callId);
|
|
@@ -17170,6 +17337,7 @@ async function replayCodexMcpToolEventsFromSessionLog(context, state) {
|
|
|
17170
17337
|
if (payload.type === "tool_search_call") {
|
|
17171
17338
|
const callId = typeof payload.call_id === "string" ? payload.call_id : "";
|
|
17172
17339
|
if (!callId) continue;
|
|
17340
|
+
if (streamedToolIds.has(callId)) continue;
|
|
17173
17341
|
context.presenter.onToolUse("tool_search", payload.arguments ?? {}, callId);
|
|
17174
17342
|
emittedToolIds.add(callId);
|
|
17175
17343
|
continue;
|
|
@@ -17196,6 +17364,63 @@ async function replayCodexMcpToolEventsFromSessionLog(context, state) {
|
|
|
17196
17364
|
}
|
|
17197
17365
|
}
|
|
17198
17366
|
}
|
|
17367
|
+
var CODEX_EXIT_GRACE_MS = 3e4;
|
|
17368
|
+
function armCodexExitGraceKill(state) {
|
|
17369
|
+
if (state.exitGraceKillTimer) return;
|
|
17370
|
+
const child = state.process;
|
|
17371
|
+
if (!child) return;
|
|
17372
|
+
const timer = setTimeout(() => {
|
|
17373
|
+
if (child.exitCode !== null || child.killed) return;
|
|
17374
|
+
console.warn(
|
|
17375
|
+
"[codex_app_server] Process did not exit after turn completion; killing process group"
|
|
17376
|
+
);
|
|
17377
|
+
const pid = child.pid;
|
|
17378
|
+
if (pid) {
|
|
17379
|
+
try {
|
|
17380
|
+
process.kill(-pid, "SIGTERM");
|
|
17381
|
+
return;
|
|
17382
|
+
} catch {
|
|
17383
|
+
}
|
|
17384
|
+
}
|
|
17385
|
+
try {
|
|
17386
|
+
child.kill("SIGTERM");
|
|
17387
|
+
} catch {
|
|
17388
|
+
}
|
|
17389
|
+
}, CODEX_EXIT_GRACE_MS);
|
|
17390
|
+
timer.unref?.();
|
|
17391
|
+
state.exitGraceKillTimer = timer;
|
|
17392
|
+
}
|
|
17393
|
+
function disarmCodexExitGraceKill(state) {
|
|
17394
|
+
if (!state.exitGraceKillTimer) return;
|
|
17395
|
+
clearTimeout(state.exitGraceKillTimer);
|
|
17396
|
+
state.exitGraceKillTimer = void 0;
|
|
17397
|
+
}
|
|
17398
|
+
function trackCodexStreamedToolId(state, toolId) {
|
|
17399
|
+
if (!state.codexStreamedToolIds) state.codexStreamedToolIds = /* @__PURE__ */ new Set();
|
|
17400
|
+
if (state.codexStreamedToolIds.has(toolId)) return false;
|
|
17401
|
+
state.codexStreamedToolIds.add(toolId);
|
|
17402
|
+
return true;
|
|
17403
|
+
}
|
|
17404
|
+
function buildCodexMcpToolName(item) {
|
|
17405
|
+
const server = typeof item.server === "string" ? item.server.replace(/_+$/, "") : "";
|
|
17406
|
+
const tool = typeof item.tool === "string" ? item.tool : typeof item.tool_name === "string" ? item.tool_name : "";
|
|
17407
|
+
if (server && tool) return `mcp__${server}__${tool}`;
|
|
17408
|
+
return tool || "MCP Tool";
|
|
17409
|
+
}
|
|
17410
|
+
function emitCodexFileChanges(context, state, itemId, changes) {
|
|
17411
|
+
changes.forEach((change, index) => {
|
|
17412
|
+
if (!change || typeof change !== "object") return;
|
|
17413
|
+
const record = change;
|
|
17414
|
+
const filePath = typeof record.path === "string" ? record.path : "";
|
|
17415
|
+
if (!filePath) return;
|
|
17416
|
+
const kind = typeof record.kind === "string" ? record.kind : "update";
|
|
17417
|
+
const toolName = kind === "add" ? "write_file" : "edit_file";
|
|
17418
|
+
const toolId = changes.length > 1 ? `${itemId}:${index}` : itemId;
|
|
17419
|
+
trackCodexStreamedToolId(state, toolId);
|
|
17420
|
+
void context.presenter.onToolUse(toolName, { file_path: filePath, kind }, toolId);
|
|
17421
|
+
void context.presenter.onToolResult?.(toolId, `${kind} ${filePath}`);
|
|
17422
|
+
});
|
|
17423
|
+
}
|
|
17199
17424
|
function handleCodexStructuredEvent(parsed, context, state) {
|
|
17200
17425
|
const presenter = context.presenter;
|
|
17201
17426
|
const type = typeof parsed.type === "string" ? parsed.type : "";
|
|
@@ -17223,6 +17448,7 @@ function handleCodexStructuredEvent(parsed, context, state) {
|
|
|
17223
17448
|
case "thread.started":
|
|
17224
17449
|
return true;
|
|
17225
17450
|
case "turn.started":
|
|
17451
|
+
disarmCodexExitGraceKill(state);
|
|
17226
17452
|
state.iterations += 1;
|
|
17227
17453
|
return true;
|
|
17228
17454
|
case "session_configured":
|
|
@@ -17231,36 +17457,112 @@ function handleCodexStructuredEvent(parsed, context, state) {
|
|
|
17231
17457
|
}
|
|
17232
17458
|
return true;
|
|
17233
17459
|
case "task_started":
|
|
17460
|
+
disarmCodexExitGraceKill(state);
|
|
17234
17461
|
state.iterations += 1;
|
|
17235
17462
|
return true;
|
|
17236
17463
|
case "item.started": {
|
|
17237
17464
|
const item = typeof parsed.item === "object" && parsed.item !== null ? parsed.item : null;
|
|
17238
|
-
if (!item || item.type !== "
|
|
17239
|
-
|
|
17240
|
-
|
|
17241
|
-
|
|
17465
|
+
if (!item || typeof item.type !== "string" || typeof item.id !== "string") return true;
|
|
17466
|
+
switch (item.type) {
|
|
17467
|
+
case "command_execution": {
|
|
17468
|
+
if (trackCodexStreamedToolId(state, item.id)) {
|
|
17469
|
+
const command = typeof item.command === "string" ? item.command : "";
|
|
17470
|
+
void presenter.onToolUse("Bash", { command, cwd: context.cwd }, item.id);
|
|
17471
|
+
}
|
|
17472
|
+
return true;
|
|
17473
|
+
}
|
|
17474
|
+
case "mcp_tool_call": {
|
|
17475
|
+
if (trackCodexStreamedToolId(state, item.id)) {
|
|
17476
|
+
const input = typeof item.arguments === "object" && item.arguments !== null ? item.arguments : {};
|
|
17477
|
+
void presenter.onToolUse(buildCodexMcpToolName(item), input, item.id);
|
|
17478
|
+
}
|
|
17479
|
+
return true;
|
|
17480
|
+
}
|
|
17481
|
+
case "web_search": {
|
|
17482
|
+
if (trackCodexStreamedToolId(state, item.id)) {
|
|
17483
|
+
const query = typeof item.query === "string" ? item.query : "";
|
|
17484
|
+
void presenter.onToolUse("web_search", { query }, item.id);
|
|
17485
|
+
}
|
|
17486
|
+
return true;
|
|
17487
|
+
}
|
|
17488
|
+
// Text-bearing and patch/todo items carry no useful live-start payload; they
|
|
17489
|
+
// are surfaced on item.completed. Recognized (do not count as unhandled).
|
|
17490
|
+
case "reasoning":
|
|
17491
|
+
case "agent_message":
|
|
17492
|
+
case "file_change":
|
|
17493
|
+
case "todo_list":
|
|
17494
|
+
return true;
|
|
17495
|
+
default:
|
|
17496
|
+
return false;
|
|
17497
|
+
}
|
|
17242
17498
|
}
|
|
17243
17499
|
case "item.completed": {
|
|
17244
17500
|
const item = typeof parsed.item === "object" && parsed.item !== null ? parsed.item : null;
|
|
17245
17501
|
if (!item || typeof item.type !== "string") return true;
|
|
17246
|
-
|
|
17247
|
-
|
|
17248
|
-
|
|
17249
|
-
|
|
17250
|
-
|
|
17251
|
-
|
|
17502
|
+
switch (item.type) {
|
|
17503
|
+
case "reasoning": {
|
|
17504
|
+
if (typeof item.text === "string") void presenter.onThinking(item.text);
|
|
17505
|
+
return true;
|
|
17506
|
+
}
|
|
17507
|
+
case "agent_message": {
|
|
17508
|
+
if (typeof item.text === "string") {
|
|
17509
|
+
state.summary += `${item.text}
|
|
17252
17510
|
`;
|
|
17253
|
-
|
|
17254
|
-
|
|
17255
|
-
|
|
17256
|
-
|
|
17257
|
-
|
|
17258
|
-
|
|
17259
|
-
|
|
17260
|
-
|
|
17261
|
-
|
|
17511
|
+
void presenter.onAssistantText(item.text);
|
|
17512
|
+
}
|
|
17513
|
+
return true;
|
|
17514
|
+
}
|
|
17515
|
+
case "command_execution": {
|
|
17516
|
+
if (typeof item.id !== "string") return true;
|
|
17517
|
+
if (trackCodexStreamedToolId(state, item.id)) {
|
|
17518
|
+
const command = typeof item.command === "string" ? item.command : "";
|
|
17519
|
+
void presenter.onToolUse("Bash", { command, cwd: context.cwd }, item.id);
|
|
17520
|
+
}
|
|
17521
|
+
const output = typeof item.aggregated_output === "string" ? item.aggregated_output : "";
|
|
17522
|
+
const itemExitCode = typeof item.exit_code === "number" ? item.exit_code : null;
|
|
17523
|
+
const resultText = output.trim().length > 0 ? output : itemExitCode === null ? "" : `Exit code: ${itemExitCode}`;
|
|
17524
|
+
void presenter.onToolResult?.(
|
|
17525
|
+
item.id,
|
|
17526
|
+
resultText,
|
|
17527
|
+
itemExitCode != null && itemExitCode !== 0
|
|
17528
|
+
);
|
|
17529
|
+
return true;
|
|
17530
|
+
}
|
|
17531
|
+
case "mcp_tool_call": {
|
|
17532
|
+
if (typeof item.id !== "string") return true;
|
|
17533
|
+
if (trackCodexStreamedToolId(state, item.id)) {
|
|
17534
|
+
const input = typeof item.arguments === "object" && item.arguments !== null ? item.arguments : {};
|
|
17535
|
+
void presenter.onToolUse(buildCodexMcpToolName(item), input, item.id);
|
|
17536
|
+
}
|
|
17537
|
+
const isError = item.status === "failed" || item.error != null;
|
|
17538
|
+
const payload = item.result ?? item.output ?? item.error ?? {};
|
|
17539
|
+
const resultText = typeof payload === "string" ? payload : JSON.stringify(payload ?? {}, null, 2);
|
|
17540
|
+
void presenter.onToolResult?.(item.id, resultText, isError);
|
|
17541
|
+
return true;
|
|
17542
|
+
}
|
|
17543
|
+
case "file_change": {
|
|
17544
|
+
if (typeof item.id !== "string") return true;
|
|
17545
|
+
const changes = Array.isArray(item.changes) ? item.changes : [];
|
|
17546
|
+
emitCodexFileChanges(context, state, item.id, changes);
|
|
17547
|
+
return true;
|
|
17548
|
+
}
|
|
17549
|
+
case "web_search": {
|
|
17550
|
+
if (typeof item.id !== "string") return true;
|
|
17551
|
+
const query = typeof item.query === "string" ? item.query : "";
|
|
17552
|
+
if (trackCodexStreamedToolId(state, item.id)) {
|
|
17553
|
+
void presenter.onToolUse("web_search", { query }, item.id);
|
|
17554
|
+
}
|
|
17555
|
+
void presenter.onToolResult?.(item.id, query ? `Searched: ${query}` : "");
|
|
17556
|
+
return true;
|
|
17557
|
+
}
|
|
17558
|
+
case "todo_list": {
|
|
17559
|
+
const todos = Array.isArray(item.items) ? item.items : [];
|
|
17560
|
+
void presenter.onTodoWrite?.(todos);
|
|
17561
|
+
return true;
|
|
17562
|
+
}
|
|
17563
|
+
default:
|
|
17564
|
+
return false;
|
|
17262
17565
|
}
|
|
17263
|
-
return true;
|
|
17264
17566
|
}
|
|
17265
17567
|
case "agent_message_delta":
|
|
17266
17568
|
case "agent_message_content_delta": {
|
|
@@ -17282,6 +17584,7 @@ function handleCodexStructuredEvent(parsed, context, state) {
|
|
|
17282
17584
|
const toolId = typeof parsed.call_id === "string" ? parsed.call_id : `exec-${Date.now()}`;
|
|
17283
17585
|
const command = Array.isArray(parsed.command) ? parsed.command.join(" ") : "";
|
|
17284
17586
|
const cwd = typeof parsed.cwd === "string" ? parsed.cwd : context.cwd;
|
|
17587
|
+
trackCodexStreamedToolId(state, toolId);
|
|
17285
17588
|
void presenter.onToolUse("Bash", { command, cwd }, toolId);
|
|
17286
17589
|
return true;
|
|
17287
17590
|
}
|
|
@@ -17295,6 +17598,7 @@ function handleCodexStructuredEvent(parsed, context, state) {
|
|
|
17295
17598
|
const toolId = typeof parsed.call_id === "string" ? parsed.call_id : `mcp-${Date.now()}`;
|
|
17296
17599
|
const invocation = typeof parsed.invocation === "object" && parsed.invocation !== null ? parsed.invocation : {};
|
|
17297
17600
|
const tool = typeof invocation.tool_name === "string" ? invocation.tool_name : typeof invocation.tool === "string" ? invocation.tool : "MCP Tool";
|
|
17601
|
+
trackCodexStreamedToolId(state, toolId);
|
|
17298
17602
|
void presenter.onToolUse(tool, invocation, toolId);
|
|
17299
17603
|
return true;
|
|
17300
17604
|
}
|
|
@@ -17332,6 +17636,8 @@ function handleCodexStructuredEvent(parsed, context, state) {
|
|
|
17332
17636
|
case "task_complete": {
|
|
17333
17637
|
const lastMessage = typeof parsed.last_agent_message === "string" ? parsed.last_agent_message : "";
|
|
17334
17638
|
if (lastMessage.length > 0) state.summary = lastMessage;
|
|
17639
|
+
state.error = void 0;
|
|
17640
|
+
armCodexExitGraceKill(state);
|
|
17335
17641
|
return true;
|
|
17336
17642
|
}
|
|
17337
17643
|
case "turn.completed": {
|
|
@@ -17353,6 +17659,8 @@ function handleCodexStructuredEvent(parsed, context, state) {
|
|
|
17353
17659
|
cacheReadTokens: state.usage.cacheReadTokens,
|
|
17354
17660
|
cacheCreationTokens: state.usage.cacheCreationTokens
|
|
17355
17661
|
});
|
|
17662
|
+
state.error = void 0;
|
|
17663
|
+
armCodexExitGraceKill(state);
|
|
17356
17664
|
return true;
|
|
17357
17665
|
}
|
|
17358
17666
|
case "error": {
|
|
@@ -17397,6 +17705,33 @@ function createCodexRuntimeBackend(command = "codex", defaultArgs = []) {
|
|
|
17397
17705
|
context.config.images,
|
|
17398
17706
|
context.cwd
|
|
17399
17707
|
);
|
|
17708
|
+
const requestedResumeId = context.config.runtimeSessionId?.trim() || context.config.providerSessionId?.trim();
|
|
17709
|
+
let resumableSessionId = requestedResumeId;
|
|
17710
|
+
let resumeContextPrefix;
|
|
17711
|
+
if (requestedResumeId && !findCodexSessionLog(requestedResumeId, resolveCodexHome(context.env))) {
|
|
17712
|
+
resumableSessionId = void 0;
|
|
17713
|
+
resumeContextPrefix = context.config.resumeFallbackContext?.trim() || void 0;
|
|
17714
|
+
console.warn(
|
|
17715
|
+
`[codex_app_server] No rollout log found for session ${requestedResumeId}; starting a fresh session instead of resuming`
|
|
17716
|
+
);
|
|
17717
|
+
logResumeObservability("agent_resume_fallback_used", {
|
|
17718
|
+
backendKind: "codex_app_server",
|
|
17719
|
+
reason: "rollout_log_missing",
|
|
17720
|
+
requestedResumeId,
|
|
17721
|
+
hadFallbackContext: Boolean(resumeContextPrefix)
|
|
17722
|
+
});
|
|
17723
|
+
emitSessionNotice(
|
|
17724
|
+
context.presenter,
|
|
17725
|
+
"session_resume_failed",
|
|
17726
|
+
resumeContextPrefix ? "The previous Codex session log was not found on this machine, so a fresh session was started with the recent conversation history re-included." : "The previous Codex session log was not found on this machine, so a fresh session was started. Earlier conversation context may be missing from this reply."
|
|
17727
|
+
);
|
|
17728
|
+
}
|
|
17729
|
+
if (resumableSessionId) {
|
|
17730
|
+
logResumeObservability("agent_resume_requested", {
|
|
17731
|
+
backendKind: "codex_app_server",
|
|
17732
|
+
requestedResumeId: resumableSessionId
|
|
17733
|
+
});
|
|
17734
|
+
}
|
|
17400
17735
|
try {
|
|
17401
17736
|
return await createGenericCliBackend({
|
|
17402
17737
|
kind: "codex_app_server",
|
|
@@ -17405,15 +17740,10 @@ function createCodexRuntimeBackend(command = "codex", defaultArgs = []) {
|
|
|
17405
17740
|
args: [],
|
|
17406
17741
|
startupTimeoutMs: CODEX_STARTUP_TIMEOUT_MS,
|
|
17407
17742
|
buildArgs: (ctx) => {
|
|
17408
|
-
const resumeId =
|
|
17743
|
+
const resumeId = resumableSessionId;
|
|
17409
17744
|
const modelArgs = ctx.config.selectedModel?.trim() ? ["--model", ctx.config.selectedModel.trim()] : [];
|
|
17410
17745
|
const effortArgs = buildCodexEffortArgs(ctx.config.selectedEffortLevel);
|
|
17411
|
-
const permissionArgs =
|
|
17412
|
-
"--ask-for-approval",
|
|
17413
|
-
"never",
|
|
17414
|
-
"--sandbox",
|
|
17415
|
-
"danger-full-access"
|
|
17416
|
-
];
|
|
17746
|
+
const permissionArgs = getCodexPermissionArgs(ctx.config);
|
|
17417
17747
|
const imageArgs = imagePaths.flatMap((p) => ["--image", p]);
|
|
17418
17748
|
const baseArgs = resumeId ? [
|
|
17419
17749
|
...permissionArgs,
|
|
@@ -17440,7 +17770,10 @@ function createCodexRuntimeBackend(command = "codex", defaultArgs = []) {
|
|
|
17440
17770
|
},
|
|
17441
17771
|
promptViaStdin: true,
|
|
17442
17772
|
augmentPrompt: (ctx) => {
|
|
17443
|
-
const
|
|
17773
|
+
const promptWithContext = resumeContextPrefix ? `${resumeContextPrefix}
|
|
17774
|
+
|
|
17775
|
+
${ctx.promptText}` : ctx.promptText;
|
|
17776
|
+
const basePrompt = buildPromptWithSystem(ctx.config, promptWithContext);
|
|
17444
17777
|
return ctx.config.mode === "plan" ? buildPlanModePrefix(basePrompt) : basePrompt;
|
|
17445
17778
|
},
|
|
17446
17779
|
parseStructuredLine: parseCodexStructuredLine,
|
|
@@ -17767,7 +18100,10 @@ function buildCursorAgentModelArg(modelId, options) {
|
|
|
17767
18100
|
return `${parsed.baseId}[${overrides.join(",")}]`;
|
|
17768
18101
|
}
|
|
17769
18102
|
function isCursorAgentCliModelId(model) {
|
|
17770
|
-
return model === "auto" || /^composer-/.test(model) || /^gpt
|
|
18103
|
+
return model === "auto" || /^composer-/.test(model) || /^gpt-/.test(model) || /^claude-/.test(model) || /^gemini-/.test(model) || /^grok-/.test(model) || /^kimi-/.test(model);
|
|
18104
|
+
}
|
|
18105
|
+
function isDispatchableCursorModel(model) {
|
|
18106
|
+
return isCursorAgentCliModelId(model) || Boolean(findModelDef("cursor_agent_cli", model));
|
|
17771
18107
|
}
|
|
17772
18108
|
function shouldForceCursorAgent(config) {
|
|
17773
18109
|
if (shouldUseReadOnlyRuntimePermissions(config)) return false;
|
|
@@ -17785,6 +18121,21 @@ function createCursorAgentCliBackend(command = "cursor-agent", defaultArgs = [])
|
|
|
17785
18121
|
kind: "cursor_agent_cli",
|
|
17786
18122
|
supportTier: "structured",
|
|
17787
18123
|
async run(context) {
|
|
18124
|
+
const selectedModel = context.config.selectedModel?.trim();
|
|
18125
|
+
if (selectedModel && !isDispatchableCursorModel(selectedModel)) {
|
|
18126
|
+
const message = `Model "${selectedModel}" is not supported by cursor-agent. Pick a cursor-agent model (or "auto") and send your message again.`;
|
|
18127
|
+
return {
|
|
18128
|
+
success: false,
|
|
18129
|
+
summary: message,
|
|
18130
|
+
filesModified: [],
|
|
18131
|
+
planFilesCreated: [],
|
|
18132
|
+
iterations: 0,
|
|
18133
|
+
error: message,
|
|
18134
|
+
errorKind: "model_mismatch",
|
|
18135
|
+
backendKind: "cursor_agent_cli",
|
|
18136
|
+
supportTier: "structured"
|
|
18137
|
+
};
|
|
18138
|
+
}
|
|
17788
18139
|
const { files: imageFiles, cleanup } = writeImagesToTempFiles(
|
|
17789
18140
|
context.config.images,
|
|
17790
18141
|
context.cwd
|
|
@@ -17814,10 +18165,18 @@ function createCursorAgentCliBackend(command = "cursor-agent", defaultArgs = [])
|
|
|
17814
18165
|
args.push("--mode", "plan");
|
|
17815
18166
|
} else if (ctx.config.mode === "ask") {
|
|
17816
18167
|
args.push("--mode", "ask");
|
|
18168
|
+
} else if (shouldUseReadOnlyRuntimePermissions(ctx.config)) {
|
|
18169
|
+
args.push("--mode", "plan");
|
|
18170
|
+
}
|
|
18171
|
+
if (resumeId) {
|
|
18172
|
+
args.push("--resume", resumeId);
|
|
18173
|
+
logResumeObservability("agent_resume_requested", {
|
|
18174
|
+
backendKind: "cursor_agent_cli",
|
|
18175
|
+
requestedResumeId: resumeId
|
|
18176
|
+
});
|
|
17817
18177
|
}
|
|
17818
|
-
if (resumeId) args.push("--resume", resumeId);
|
|
17819
18178
|
const model = ctx.config.selectedModel?.trim();
|
|
17820
|
-
if (model &&
|
|
18179
|
+
if (model && isDispatchableCursorModel(model)) {
|
|
17821
18180
|
const modelDef = findModelDef("cursor_agent_cli", model);
|
|
17822
18181
|
args.push(
|
|
17823
18182
|
"--model",
|
|
@@ -18280,9 +18639,9 @@ function buildGrokAgentArgs(config, defaultArgs = []) {
|
|
|
18280
18639
|
return args;
|
|
18281
18640
|
}
|
|
18282
18641
|
var DEFAULT_SKILL_ROOTS = [
|
|
18283
|
-
(0,
|
|
18284
|
-
(0,
|
|
18285
|
-
(0,
|
|
18642
|
+
(0, import_path6.join)((0, import_os6.homedir)(), ".agents", "skills"),
|
|
18643
|
+
(0, import_path6.join)((0, import_os6.homedir)(), ".claude", "skills"),
|
|
18644
|
+
(0, import_path6.join)((0, import_os6.homedir)(), ".config", "opencode", "skills")
|
|
18286
18645
|
];
|
|
18287
18646
|
function parseFrontmatter(content) {
|
|
18288
18647
|
if (!content.startsWith("---\n")) return {};
|
|
@@ -18298,12 +18657,12 @@ function parseFrontmatter(content) {
|
|
|
18298
18657
|
return result;
|
|
18299
18658
|
}
|
|
18300
18659
|
function listSkillFiles(root) {
|
|
18301
|
-
if (!(0,
|
|
18660
|
+
if (!(0, import_fs6.existsSync)(root)) return [];
|
|
18302
18661
|
const files = [];
|
|
18303
|
-
for (const entry of (0,
|
|
18662
|
+
for (const entry of (0, import_fs6.readdirSync)(root, { withFileTypes: true })) {
|
|
18304
18663
|
if (!entry.isDirectory()) continue;
|
|
18305
|
-
const skillPath = (0,
|
|
18306
|
-
if ((0,
|
|
18664
|
+
const skillPath = (0, import_path6.join)(root, entry.name, "SKILL.md");
|
|
18665
|
+
if ((0, import_fs6.existsSync)(skillPath)) files.push(skillPath);
|
|
18307
18666
|
}
|
|
18308
18667
|
return files;
|
|
18309
18668
|
}
|
|
@@ -18312,7 +18671,7 @@ function loadInstalledSkills(roots = DEFAULT_SKILL_ROOTS) {
|
|
|
18312
18671
|
for (const root of roots) {
|
|
18313
18672
|
for (const skillPath of listSkillFiles(root)) {
|
|
18314
18673
|
try {
|
|
18315
|
-
const frontmatter = parseFrontmatter((0,
|
|
18674
|
+
const frontmatter = parseFrontmatter((0, import_fs6.readFileSync)(skillPath, "utf8"));
|
|
18316
18675
|
const name = frontmatter.name;
|
|
18317
18676
|
const description = frontmatter.description;
|
|
18318
18677
|
if (!name || !description || skills.has(name)) continue;
|
|
@@ -18745,7 +19104,31 @@ function createSupatestCliBackend(command = "supatest", defaultArgs = []) {
|
|
|
18745
19104
|
"--permission-mode",
|
|
18746
19105
|
getClaudePermissionMode(context.config)
|
|
18747
19106
|
];
|
|
18748
|
-
const
|
|
19107
|
+
const requestedResumeId = context.config.runtimeSessionId?.trim() || context.config.providerSessionId?.trim();
|
|
19108
|
+
let resumeId = requestedResumeId;
|
|
19109
|
+
let resumeContextPrefix;
|
|
19110
|
+
if (requestedResumeId && !claudeSessionLogExists({
|
|
19111
|
+
sessionId: requestedResumeId,
|
|
19112
|
+
cwd: context.cwd,
|
|
19113
|
+
home: resolveClaudeHome(context.env, "supatest")
|
|
19114
|
+
})) {
|
|
19115
|
+
resumeId = void 0;
|
|
19116
|
+
resumeContextPrefix = context.config.resumeFallbackContext?.trim() || void 0;
|
|
19117
|
+
console.warn(
|
|
19118
|
+
`[supatest_cli] No session log for ${requestedResumeId} under cwd ${context.cwd}; starting a fresh session instead of resuming`
|
|
19119
|
+
);
|
|
19120
|
+
logResumeObservability("agent_resume_fallback_used", {
|
|
19121
|
+
backendKind: "supatest_cli",
|
|
19122
|
+
reason: "session_log_missing_for_cwd",
|
|
19123
|
+
requestedResumeId,
|
|
19124
|
+
hadFallbackContext: Boolean(resumeContextPrefix)
|
|
19125
|
+
});
|
|
19126
|
+
emitSessionNotice(
|
|
19127
|
+
context.presenter,
|
|
19128
|
+
"session_resume_failed",
|
|
19129
|
+
resumeContextPrefix ? "The previous session was created in a different working directory and could not be resumed here, so a fresh session was started with the recent conversation history re-included." : "The previous session was created in a different working directory and could not be resumed here, so a fresh session was started. Earlier conversation context may be missing from this reply."
|
|
19130
|
+
);
|
|
19131
|
+
}
|
|
18749
19132
|
if (!resumeId) {
|
|
18750
19133
|
if (context.config.systemPrompt?.trim()) {
|
|
18751
19134
|
args.push("--system-prompt", context.config.systemPrompt.trim());
|
|
@@ -18761,6 +19144,10 @@ function createSupatestCliBackend(command = "supatest", defaultArgs = []) {
|
|
|
18761
19144
|
if (resumeId) {
|
|
18762
19145
|
args.push("--resume", resumeId);
|
|
18763
19146
|
console.info("[supatest_cli] Resuming session", { resumeId });
|
|
19147
|
+
logResumeObservability("agent_resume_requested", {
|
|
19148
|
+
backendKind: "supatest_cli",
|
|
19149
|
+
requestedResumeId: resumeId
|
|
19150
|
+
});
|
|
18764
19151
|
}
|
|
18765
19152
|
args.push(...defaultArgs);
|
|
18766
19153
|
return createGenericCliBackend({
|
|
@@ -18779,7 +19166,10 @@ function createSupatestCliBackend(command = "supatest", defaultArgs = []) {
|
|
|
18779
19166
|
});
|
|
18780
19167
|
}
|
|
18781
19168
|
}
|
|
18782
|
-
const
|
|
19169
|
+
const basePromptText = resumeContextPrefix ? `${resumeContextPrefix}
|
|
19170
|
+
|
|
19171
|
+
${ctx.promptText}` : ctx.promptText;
|
|
19172
|
+
const promptText = ctx.config.mode === "plan" ? buildPlanModePrefix(buildPromptWithSystem(ctx.config, basePromptText)) : buildPromptWithSystem(ctx.config, basePromptText);
|
|
18783
19173
|
if (promptText.trim()) {
|
|
18784
19174
|
contentBlocks.push({ type: "text", text: promptText });
|
|
18785
19175
|
}
|
|
@@ -18809,40 +19199,40 @@ function parseSkillInvocation(text) {
|
|
|
18809
19199
|
function candidateRoots(cwd) {
|
|
18810
19200
|
const roots = [];
|
|
18811
19201
|
if (cwd) {
|
|
18812
|
-
roots.push((0,
|
|
18813
|
-
roots.push((0,
|
|
18814
|
-
roots.push((0,
|
|
18815
|
-
if ((0,
|
|
19202
|
+
roots.push((0, import_path7.join)(cwd, ".agents", "skills"));
|
|
19203
|
+
roots.push((0, import_path7.join)(cwd, ".claude", "skills"));
|
|
19204
|
+
roots.push((0, import_path7.join)(cwd, ".codex", "skills"));
|
|
19205
|
+
if ((0, import_fs7.existsSync)(cwd)) {
|
|
18816
19206
|
let entries = [];
|
|
18817
19207
|
try {
|
|
18818
|
-
entries = (0,
|
|
19208
|
+
entries = (0, import_fs7.readdirSync)(cwd, { withFileTypes: true });
|
|
18819
19209
|
} catch {
|
|
18820
19210
|
}
|
|
18821
19211
|
for (const entry of entries) {
|
|
18822
19212
|
if (!entry.isDirectory()) continue;
|
|
18823
19213
|
if (entry.name.startsWith(".")) continue;
|
|
18824
|
-
const child = (0,
|
|
18825
|
-
roots.push((0,
|
|
18826
|
-
roots.push((0,
|
|
18827
|
-
roots.push((0,
|
|
19214
|
+
const child = (0, import_path7.join)(cwd, entry.name);
|
|
19215
|
+
roots.push((0, import_path7.join)(child, ".agents", "skills"));
|
|
19216
|
+
roots.push((0, import_path7.join)(child, ".claude", "skills"));
|
|
19217
|
+
roots.push((0, import_path7.join)(child, ".codex", "skills"));
|
|
18828
19218
|
}
|
|
18829
19219
|
}
|
|
18830
19220
|
}
|
|
18831
|
-
const home = (0,
|
|
18832
|
-
roots.push((0,
|
|
18833
|
-
roots.push((0,
|
|
18834
|
-
roots.push((0,
|
|
19221
|
+
const home = (0, import_os7.homedir)();
|
|
19222
|
+
roots.push((0, import_path7.join)(home, ".agents", "skills"));
|
|
19223
|
+
roots.push((0, import_path7.join)(home, ".claude", "skills"));
|
|
19224
|
+
roots.push((0, import_path7.join)(home, ".codex", "skills"));
|
|
18835
19225
|
return Array.from(new Set(roots));
|
|
18836
19226
|
}
|
|
18837
19227
|
function resolveSkillInvocation(text, cwd) {
|
|
18838
19228
|
const parsed = parseSkillInvocation(text);
|
|
18839
19229
|
if (!parsed) return null;
|
|
18840
19230
|
for (const root of candidateRoots(cwd)) {
|
|
18841
|
-
const skillPath = (0,
|
|
18842
|
-
if (!(0,
|
|
19231
|
+
const skillPath = (0, import_path7.join)(root, parsed.command, "SKILL.md");
|
|
19232
|
+
if (!(0, import_fs7.existsSync)(skillPath)) continue;
|
|
18843
19233
|
let content;
|
|
18844
19234
|
try {
|
|
18845
|
-
content = (0,
|
|
19235
|
+
content = (0, import_fs7.readFileSync)(skillPath, "utf-8");
|
|
18846
19236
|
} catch {
|
|
18847
19237
|
continue;
|
|
18848
19238
|
}
|
|
@@ -19023,11 +19413,19 @@ var BaseMachineAgent = class _BaseMachineAgent {
|
|
|
19023
19413
|
async runBackendWithActivityHeartbeat(backend, context) {
|
|
19024
19414
|
const intervalMs = this.getActivityHeartbeatIntervalMs();
|
|
19025
19415
|
let heartbeat = null;
|
|
19416
|
+
let livenessProbe = null;
|
|
19417
|
+
context.registerLivenessProbe = (probe) => {
|
|
19418
|
+
livenessProbe = probe;
|
|
19419
|
+
};
|
|
19420
|
+
const emitHeartbeat = () => {
|
|
19421
|
+
const evidence = livenessProbe?.();
|
|
19422
|
+
void this.presenter.onActivity?.("background_work", evidence);
|
|
19423
|
+
};
|
|
19026
19424
|
if (this.presenter.onActivity && intervalMs > 0) {
|
|
19027
|
-
|
|
19425
|
+
emitHeartbeat();
|
|
19028
19426
|
heartbeat = setInterval(() => {
|
|
19029
19427
|
if (!context.abortController.signal.aborted) {
|
|
19030
|
-
|
|
19428
|
+
emitHeartbeat();
|
|
19031
19429
|
}
|
|
19032
19430
|
}, intervalMs);
|
|
19033
19431
|
}
|
|
@@ -19076,10 +19474,6 @@ var BaseMachineAgent = class _BaseMachineAgent {
|
|
|
19076
19474
|
if (agentPrompt) systemPromptParts.push(agentPrompt);
|
|
19077
19475
|
const modeHint = getAlanModePrompt(config.mode);
|
|
19078
19476
|
if (modeHint) systemPromptParts.push(modeHint);
|
|
19079
|
-
systemPromptParts.push(ALAN_ASK_USER_PROMPT);
|
|
19080
|
-
if (backendKind === "cursor_agent_cli") {
|
|
19081
|
-
systemPromptParts.push(ALAN_CURSOR_ASK_USER_OVERLAY);
|
|
19082
|
-
}
|
|
19083
19477
|
const operatorSystemPrompt = config.systemPrompt?.trim();
|
|
19084
19478
|
if (operatorSystemPrompt) systemPromptParts.push(operatorSystemPrompt);
|
|
19085
19479
|
const workingDir = config.worktreePath || safeProjectPath;
|
|
@@ -19152,6 +19546,7 @@ Prefer relative paths and keep your work scoped to this project.`
|
|
|
19152
19546
|
};
|
|
19153
19547
|
if (lastResult.success || !lastResult.error) break;
|
|
19154
19548
|
if (this.abortController?.signal.aborted) break;
|
|
19549
|
+
if (lastResult.errorKind === "model_mismatch") break;
|
|
19155
19550
|
const hadResumeId = runtimeConfig.runtimeSessionId || runtimeConfig.providerSessionId;
|
|
19156
19551
|
if (hadResumeId && !isTransientError(lastResult.error)) {
|
|
19157
19552
|
const overrideResult = await this.onSessionResumeFailure(runtimeConfig, lastResult);
|
|
@@ -19163,10 +19558,25 @@ Prefer relative paths and keep your work scoped to this project.`
|
|
|
19163
19558
|
"[base-machine-agent] Resume failed, retrying without session ID:",
|
|
19164
19559
|
lastResult.error
|
|
19165
19560
|
);
|
|
19561
|
+
const fallbackContext = runtimeConfig.resumeFallbackContext?.trim();
|
|
19562
|
+
logResumeObservability("agent_resume_fallback_used", {
|
|
19563
|
+
backendKind: backend.kind,
|
|
19564
|
+
reason: "retry_fresh_after_resume_failure",
|
|
19565
|
+
errorKind: lastResult.errorKind,
|
|
19566
|
+
hadFallbackContext: Boolean(fallbackContext)
|
|
19567
|
+
});
|
|
19568
|
+
emitSessionNotice(
|
|
19569
|
+
this.presenter,
|
|
19570
|
+
"session_resume_failed",
|
|
19571
|
+
fallbackContext ? "Could not resume the previous session; started a fresh session with the recent conversation history re-included." : "Could not resume the previous session; retrying with a fresh session. Earlier conversation context may be missing from this reply."
|
|
19572
|
+
);
|
|
19166
19573
|
runtimeConfig = {
|
|
19167
19574
|
...runtimeConfig,
|
|
19168
19575
|
runtimeSessionId: void 0,
|
|
19169
|
-
providerSessionId: void 0
|
|
19576
|
+
providerSessionId: void 0,
|
|
19577
|
+
...fallbackContext ? { task: `${fallbackContext}
|
|
19578
|
+
|
|
19579
|
+
${runtimeConfig.task}` } : {}
|
|
19170
19580
|
};
|
|
19171
19581
|
const retryResult = await this.runBackendWithActivityHeartbeat(backend, {
|
|
19172
19582
|
presenter: this.presenter,
|
|
@@ -19174,7 +19584,7 @@ Prefer relative paths and keep your work scoped to this project.`
|
|
|
19174
19584
|
abortController: this.abortController,
|
|
19175
19585
|
cwd,
|
|
19176
19586
|
env,
|
|
19177
|
-
promptText,
|
|
19587
|
+
promptText: fallbackContext ? this.buildPromptText(runtimeConfig) : promptText,
|
|
19178
19588
|
onProcessSpawned: this.getOnProcessSpawned()
|
|
19179
19589
|
});
|
|
19180
19590
|
lastResult = {
|
|
@@ -19370,14 +19780,14 @@ function normalizePath(candidate, cwd) {
|
|
|
19370
19780
|
} catch {
|
|
19371
19781
|
}
|
|
19372
19782
|
if (path.startsWith("~/")) return null;
|
|
19373
|
-
if ((0,
|
|
19374
|
-
return cwd ? (0,
|
|
19783
|
+
if ((0, import_path8.isAbsolute)(path)) return path;
|
|
19784
|
+
return cwd ? (0, import_path8.join)(cwd, path) : null;
|
|
19375
19785
|
}
|
|
19376
19786
|
function resolveToolCwd(cwd, toolInput) {
|
|
19377
19787
|
if (!isRecord(toolInput)) return cwd;
|
|
19378
19788
|
const raw = typeof toolInput.cwd === "string" ? toolInput.cwd : typeof toolInput.workdir === "string" ? toolInput.workdir : typeof toolInput.workingDirectory === "string" ? toolInput.workingDirectory : null;
|
|
19379
19789
|
if (!raw) return cwd;
|
|
19380
|
-
return (0,
|
|
19790
|
+
return (0, import_path8.isAbsolute)(raw) || !cwd ? raw : (0, import_path8.join)(cwd, raw);
|
|
19381
19791
|
}
|
|
19382
19792
|
function extractBrowserMediaPathHints(content, cwd, toolName, options = {}, kind = "video") {
|
|
19383
19793
|
if (!isAgentBrowserMediaInvocation(toolName, content, options.toolInput, kind)) return [];
|
|
@@ -19420,19 +19830,19 @@ function extractGeneratedImagesFromToolResult(toolId, content, cwd, toolName, op
|
|
|
19420
19830
|
const effectiveCwd = resolveToolCwd(cwd, options.toolInput);
|
|
19421
19831
|
for (const candidate of extractPathCandidates(content, options)) {
|
|
19422
19832
|
const path = normalizePath(candidate, effectiveCwd);
|
|
19423
|
-
if (!path || seen.has(path) || !(0,
|
|
19833
|
+
if (!path || seen.has(path) || !(0, import_fs8.existsSync)(path)) continue;
|
|
19424
19834
|
seen.add(path);
|
|
19425
|
-
const ext = (0,
|
|
19835
|
+
const ext = (0, import_path8.extname)(path).toLowerCase();
|
|
19426
19836
|
const mimeType = IMAGE_MIME_BY_EXT[ext];
|
|
19427
19837
|
if (!mimeType) continue;
|
|
19428
|
-
const stat = (0,
|
|
19838
|
+
const stat = (0, import_fs8.statSync)(path);
|
|
19429
19839
|
if (!stat.isFile() || stat.size <= 0 || stat.size > MAX_GENERATED_IMAGE_BYTES) continue;
|
|
19430
|
-
const buffer = (0,
|
|
19840
|
+
const buffer = (0, import_fs8.readFileSync)(path);
|
|
19431
19841
|
const { width, height } = readImageDimensions2(buffer, mimeType);
|
|
19432
19842
|
images.push({
|
|
19433
19843
|
type: "generated_image",
|
|
19434
19844
|
id: (0, import_crypto3.randomUUID)(),
|
|
19435
|
-
filename: (0,
|
|
19845
|
+
filename: (0, import_path8.basename)(path),
|
|
19436
19846
|
mimeType,
|
|
19437
19847
|
size: buffer.length,
|
|
19438
19848
|
width,
|
|
@@ -19459,20 +19869,20 @@ function extractSessionMediaFromToolResult(toolId, content, cwd, toolName, optio
|
|
|
19459
19869
|
const mimeByExt = kind === "video" ? VIDEO_MIME_BY_EXT : IMAGE_MIME_BY_EXT;
|
|
19460
19870
|
for (const candidate of extractPathCandidates(content, options, extensionsPattern)) {
|
|
19461
19871
|
const path = normalizePath(candidate, effectiveCwd);
|
|
19462
|
-
if (!path || seen.has(path) || !(0,
|
|
19872
|
+
if (!path || seen.has(path) || !(0, import_fs8.existsSync)(path)) continue;
|
|
19463
19873
|
seen.add(path);
|
|
19464
|
-
const ext = (0,
|
|
19874
|
+
const ext = (0, import_path8.extname)(path).toLowerCase();
|
|
19465
19875
|
const mimeType = mimeByExt[ext];
|
|
19466
19876
|
if (!mimeType) continue;
|
|
19467
|
-
const stat = (0,
|
|
19877
|
+
const stat = (0, import_fs8.statSync)(path);
|
|
19468
19878
|
if (!stat.isFile() || stat.size <= 0 || stat.size > maxBytes) continue;
|
|
19469
|
-
const buffer = (0,
|
|
19879
|
+
const buffer = (0, import_fs8.readFileSync)(path);
|
|
19470
19880
|
const dimensions = kind === "image" ? readImageDimensions2(buffer, mimeType) : null;
|
|
19471
19881
|
media.push({
|
|
19472
19882
|
type: "session_media",
|
|
19473
19883
|
kind,
|
|
19474
19884
|
id: (0, import_crypto3.randomUUID)(),
|
|
19475
|
-
filename: (0,
|
|
19885
|
+
filename: (0, import_path8.basename)(path),
|
|
19476
19886
|
mimeType,
|
|
19477
19887
|
size: buffer.length,
|
|
19478
19888
|
width: dimensions?.width,
|
|
@@ -19636,6 +20046,10 @@ var CoreAgent = class _CoreAgent extends BaseMachineAgent {
|
|
|
19636
20046
|
if (!this.childProcess) return false;
|
|
19637
20047
|
const stdin = this.childProcess.stdin;
|
|
19638
20048
|
if (!stdin || stdin.destroyed) return false;
|
|
20049
|
+
if (this.presenter.hasKnownToolUse && !this.presenter.hasKnownToolUse(toolId)) {
|
|
20050
|
+
console.warn(`[CoreAgent] Ignoring tool_response for unknown tool ${toolId}`);
|
|
20051
|
+
return false;
|
|
20052
|
+
}
|
|
19639
20053
|
try {
|
|
19640
20054
|
let message;
|
|
19641
20055
|
if (this.presenter.pendingAskUserToolIds?.has(toolId)) {
|
|
@@ -19890,6 +20304,9 @@ function decodeKey(encodedKey) {
|
|
|
19890
20304
|
function isTerminal(event) {
|
|
19891
20305
|
return TERMINAL_EVENT_TYPES.has(event.type);
|
|
19892
20306
|
}
|
|
20307
|
+
function isBackgroundHeartbeat(event) {
|
|
20308
|
+
return event.type === "activity" && event.status === "background_work";
|
|
20309
|
+
}
|
|
19893
20310
|
function truncateUtf8(value2, maxBytes) {
|
|
19894
20311
|
const encoded = Buffer.from(value2, "utf8");
|
|
19895
20312
|
if (encoded.length <= maxBytes) return value2;
|
|
@@ -20038,6 +20455,23 @@ var EncryptedEventOutbox = class {
|
|
|
20038
20455
|
isDroppable(entry) {
|
|
20039
20456
|
return entry.eventId !== this.inFlightEventId && entry.kind !== "condensation_marker" && !isTerminal(entry.event);
|
|
20040
20457
|
}
|
|
20458
|
+
/**
|
|
20459
|
+
* Pick the next droppable entry to evict. Heartbeats are always evicted
|
|
20460
|
+
* first (oldest heartbeat first) since they carry no transcript content;
|
|
20461
|
+
* only once none remain do we fall back to the oldest droppable entry of
|
|
20462
|
+
* any kind. Without this, oldest-first eviction treats a background_work
|
|
20463
|
+
* heartbeat exactly like real transcript content, so a long offline stretch
|
|
20464
|
+
* (heartbeats every 10s, never removed by an ack since nothing is
|
|
20465
|
+
* connected) evicts genuine early events — including `start` — ahead of
|
|
20466
|
+
* the heartbeats that caused the overflow.
|
|
20467
|
+
*/
|
|
20468
|
+
pickDropCandidate(matches) {
|
|
20469
|
+
const heartbeat = this.entries.find(
|
|
20470
|
+
(entry) => matches(entry) && this.isDroppable(entry) && isBackgroundHeartbeat(entry.event)
|
|
20471
|
+
);
|
|
20472
|
+
if (heartbeat) return heartbeat;
|
|
20473
|
+
return this.entries.find((entry) => matches(entry) && this.isDroppable(entry));
|
|
20474
|
+
}
|
|
20041
20475
|
ensureCondensationMarker(runId, conversationId) {
|
|
20042
20476
|
if (this.entries.some((entry) => entry.runId === runId && entry.kind === "condensation_marker")) {
|
|
20043
20477
|
return;
|
|
@@ -20065,7 +20499,7 @@ var EncryptedEventOutbox = class {
|
|
|
20065
20499
|
const runIds = preferredRunId ? [preferredRunId] : [...new Set(this.entries.map((entry) => entry.runId))];
|
|
20066
20500
|
for (const runId of runIds) {
|
|
20067
20501
|
while (this.entries.filter((entry) => entry.runId === runId && this.isDroppable(entry)).length > this.maxIntermediateEventsPerRun) {
|
|
20068
|
-
const drop = this.
|
|
20502
|
+
const drop = this.pickDropCandidate((entry) => entry.runId === runId);
|
|
20069
20503
|
if (!drop) break;
|
|
20070
20504
|
this.entries = this.entries.filter((entry) => entry.eventId !== drop.eventId);
|
|
20071
20505
|
lossCountByRunId.set(runId, (lossCountByRunId.get(runId) ?? 0) + 1);
|
|
@@ -20073,7 +20507,7 @@ var EncryptedEventOutbox = class {
|
|
|
20073
20507
|
}
|
|
20074
20508
|
}
|
|
20075
20509
|
while (encryptedFileSize(this.entries) > this.maxEncryptedBytes) {
|
|
20076
|
-
const drop = this.
|
|
20510
|
+
const drop = this.pickDropCandidate(() => true);
|
|
20077
20511
|
if (drop) {
|
|
20078
20512
|
this.entries = this.entries.filter((entry) => entry.eventId !== drop.eventId);
|
|
20079
20513
|
lossCountByRunId.set(drop.runId, (lossCountByRunId.get(drop.runId) ?? 0) + 1);
|
|
@@ -20092,7 +20526,7 @@ var EncryptedEventOutbox = class {
|
|
|
20092
20526
|
this.ensureCondensationMarker(runId, lossConversationByRunId.get(runId));
|
|
20093
20527
|
}
|
|
20094
20528
|
while (encryptedFileSize(this.entries) > this.maxEncryptedBytes) {
|
|
20095
|
-
const drop = this.
|
|
20529
|
+
const drop = this.pickDropCandidate(() => true);
|
|
20096
20530
|
if (!drop) break;
|
|
20097
20531
|
this.entries = this.entries.filter((entry) => entry.eventId !== drop.eventId);
|
|
20098
20532
|
lossCountByRunId.set(drop.runId, (lossCountByRunId.get(drop.runId) ?? 0) + 1);
|
|
@@ -21017,6 +21451,10 @@ function assertValidExistingToml(path, content) {
|
|
|
21017
21451
|
`Existing config is malformed; no changes were applied. Review the backup at ${backupPath}`
|
|
21018
21452
|
);
|
|
21019
21453
|
}
|
|
21454
|
+
var CODEX_ALAN_TOML_SECTION = "mcp_servers.alan";
|
|
21455
|
+
function isCodexAlanTomlSection(section) {
|
|
21456
|
+
return section === CODEX_ALAN_TOML_SECTION || section.startsWith(`${CODEX_ALAN_TOML_SECTION}.`);
|
|
21457
|
+
}
|
|
21020
21458
|
function mergeCodexAlanSection(path, existingContent, desiredContent) {
|
|
21021
21459
|
if (existingContent === null || existingContent.trim() === "") return desiredContent;
|
|
21022
21460
|
assertValidExistingToml(path, existingContent);
|
|
@@ -21026,7 +21464,7 @@ function mergeCodexAlanSection(path, existingContent, desiredContent) {
|
|
|
21026
21464
|
for (const line of lines) {
|
|
21027
21465
|
const section = line.trim().match(/^\[([^\]]+)]$/)?.[1];
|
|
21028
21466
|
if (section) {
|
|
21029
|
-
insideAlanSection = section
|
|
21467
|
+
insideAlanSection = isCodexAlanTomlSection(section);
|
|
21030
21468
|
}
|
|
21031
21469
|
if (!insideAlanSection) retained.push(line);
|
|
21032
21470
|
}
|
|
@@ -21812,7 +22250,7 @@ var RunStartGate = class {
|
|
|
21812
22250
|
};
|
|
21813
22251
|
|
|
21814
22252
|
// src/version.ts
|
|
21815
|
-
var AGENT_VERSION = "0.1.
|
|
22253
|
+
var AGENT_VERSION = "0.1.38";
|
|
21816
22254
|
|
|
21817
22255
|
// src/workspace-relocation.ts
|
|
21818
22256
|
var import_node_child_process3 = require("child_process");
|
|
@@ -22456,7 +22894,12 @@ function getEndpointDefaultsPath() {
|
|
|
22456
22894
|
function readConfig() {
|
|
22457
22895
|
const configPath = getConfigPath();
|
|
22458
22896
|
if (!(0, import_node_fs12.existsSync)(configPath)) return {};
|
|
22459
|
-
|
|
22897
|
+
const parsed = JSON.parse((0, import_node_fs12.readFileSync)(configPath, "utf8"));
|
|
22898
|
+
const sanitized = sanitizeAgentConfig(parsed);
|
|
22899
|
+
if (Object.keys(parsed).some((key) => !Object.hasOwn(sanitized, key))) {
|
|
22900
|
+
writeConfig(sanitized);
|
|
22901
|
+
}
|
|
22902
|
+
return sanitized;
|
|
22460
22903
|
}
|
|
22461
22904
|
function readConfigForRegistration() {
|
|
22462
22905
|
try {
|
|
@@ -22479,7 +22922,11 @@ function writeConfig(config) {
|
|
|
22479
22922
|
(0, import_node_fs12.mkdirSync)((0, import_node_path10.dirname)(configPath), { recursive: true, mode: 448 });
|
|
22480
22923
|
const pendingPath = `${configPath}.pending-${process.pid}-${(0, import_node_crypto3.randomBytes)(6).toString("hex")}`;
|
|
22481
22924
|
try {
|
|
22482
|
-
(0, import_node_fs12.writeFileSync)(
|
|
22925
|
+
(0, import_node_fs12.writeFileSync)(
|
|
22926
|
+
pendingPath,
|
|
22927
|
+
JSON.stringify(sanitizeAgentConfig(config), null, 2),
|
|
22928
|
+
{ mode: 384 }
|
|
22929
|
+
);
|
|
22483
22930
|
(0, import_node_fs12.chmodSync)(pendingPath, 384);
|
|
22484
22931
|
(0, import_node_fs12.renameSync)(pendingPath, configPath);
|
|
22485
22932
|
} finally {
|
|
@@ -23159,14 +23606,29 @@ var USER_INTERRUPTED_RESULT = {
|
|
|
23159
23606
|
filesModified: [],
|
|
23160
23607
|
planFilesCreated: [],
|
|
23161
23608
|
iterations: 0,
|
|
23162
|
-
error: "Interrupted by user"
|
|
23609
|
+
error: "Interrupted by user",
|
|
23610
|
+
errorKind: "user"
|
|
23163
23611
|
};
|
|
23612
|
+
function buildAbortResult(reason) {
|
|
23613
|
+
if (reason === "user") return USER_INTERRUPTED_RESULT;
|
|
23614
|
+
const message = SYSTEM_ABORT_RUN_ERRORS[reason];
|
|
23615
|
+
return {
|
|
23616
|
+
success: false,
|
|
23617
|
+
summary: message,
|
|
23618
|
+
filesModified: [],
|
|
23619
|
+
planFilesCreated: [],
|
|
23620
|
+
iterations: 0,
|
|
23621
|
+
error: message,
|
|
23622
|
+
errorKind: reason
|
|
23623
|
+
};
|
|
23624
|
+
}
|
|
23164
23625
|
function abortActiveAgent(entry, options) {
|
|
23165
23626
|
const awaitingAsk = (entry.presenter.pendingAskUserToolIds?.size ?? 0) > 0;
|
|
23166
23627
|
if (awaitingAsk && !options?.userInitiated) {
|
|
23167
23628
|
return;
|
|
23168
23629
|
}
|
|
23169
|
-
|
|
23630
|
+
const reason = options?.reason ?? (options?.userInitiated ? "user" : "runner_stalled");
|
|
23631
|
+
entry.presenter.onComplete(buildAbortResult(reason));
|
|
23170
23632
|
entry.agent.kill();
|
|
23171
23633
|
}
|
|
23172
23634
|
function isProviderProcessAlive(pid) {
|
|
@@ -23275,7 +23737,11 @@ function flushPendingDeliveryLostEvents(input) {
|
|
|
23275
23737
|
if (input.pending.length === 0) return;
|
|
23276
23738
|
for (const item of input.pending) {
|
|
23277
23739
|
if (input.eventOutbox.hasTerminalEvent(item.runId)) continue;
|
|
23278
|
-
|
|
23740
|
+
const activeEntry = input.activeAgents.get(item.runId);
|
|
23741
|
+
if (activeEntry) {
|
|
23742
|
+
if (activeEntry.stage === "delivery_lost") activeEntry.stage = "running";
|
|
23743
|
+
continue;
|
|
23744
|
+
}
|
|
23279
23745
|
input.eventOutbox.enqueue(item.conversationId, item.runId, {
|
|
23280
23746
|
type: "session_error",
|
|
23281
23747
|
error: DELIVERY_LOST_ERROR
|
|
@@ -23299,7 +23765,7 @@ function reconcileActiveRuns(input) {
|
|
|
23299
23765
|
input.pushLog?.(`reconciled dead provider pid run=${runId} (awaiting ask, no interrupt)`);
|
|
23300
23766
|
continue;
|
|
23301
23767
|
}
|
|
23302
|
-
abortActiveAgent(entry);
|
|
23768
|
+
abortActiveAgent(entry, { reason: staleByDeadPid ? "provider_exited" : "runner_stalled" });
|
|
23303
23769
|
input.activeAgents.delete(runId);
|
|
23304
23770
|
if (staleByDeadPid) {
|
|
23305
23771
|
input.pushLog?.(`reconciled dead provider pid run=${runId}`);
|
|
@@ -23363,6 +23829,7 @@ var RuntimePresenter = class {
|
|
|
23363
23829
|
emitEvent(event) {
|
|
23364
23830
|
if (this.terminalFenced) return;
|
|
23365
23831
|
this.touchRunnerActivity?.();
|
|
23832
|
+
if (isBackgroundHeartbeat(event) && !this.socket.connected) return;
|
|
23366
23833
|
this.eventOutbox.enqueue(this.conversationId, this.runId, event);
|
|
23367
23834
|
this.eventOutbox.flush(this.socket);
|
|
23368
23835
|
}
|
|
@@ -23390,6 +23857,9 @@ var RuntimePresenter = class {
|
|
|
23390
23857
|
onLog(message) {
|
|
23391
23858
|
console.error(message);
|
|
23392
23859
|
}
|
|
23860
|
+
onNotice(kind, message) {
|
|
23861
|
+
this.emitEvent({ type: "session_notice", kind, message, ts: Date.now() });
|
|
23862
|
+
}
|
|
23393
23863
|
onAssistantText(text) {
|
|
23394
23864
|
this.emitEvent({ type: "text", text, ts: Date.now() });
|
|
23395
23865
|
}
|
|
@@ -23489,8 +23959,8 @@ var RuntimePresenter = class {
|
|
|
23489
23959
|
onCheckpoint(id) {
|
|
23490
23960
|
this.emitEvent({ type: "checkpoint", id });
|
|
23491
23961
|
}
|
|
23492
|
-
onActivity(status) {
|
|
23493
|
-
this.emitEvent({ type: "activity", status, ts: Date.now() });
|
|
23962
|
+
onActivity(status, evidence) {
|
|
23963
|
+
this.emitEvent({ type: "activity", status, ts: Date.now(), ...evidence ? { evidence } : {} });
|
|
23494
23964
|
}
|
|
23495
23965
|
onOpenBrowserTab(browserId, url2) {
|
|
23496
23966
|
this.emitEvent({ type: "open_browser_tab", browserId, ...url2 ? { url: url2 } : {} });
|
|
@@ -23513,14 +23983,14 @@ async function setupDaemon(args) {
|
|
|
23513
23983
|
const setupToken = argValue(args, "--setup-token") ?? process.env.ALAN_RUNTIME_SETUP_TOKEN;
|
|
23514
23984
|
const token = argValue(args, "--token") ?? process.env.ALAN_SETUP_TOKEN;
|
|
23515
23985
|
const displayName = argValue(args, "--name") ?? (0, import_node_os7.hostname)();
|
|
23516
|
-
const scope = argValue(args, "--scope") ?? process.env.ALAN_RUNTIME_SCOPE ?? "team";
|
|
23986
|
+
const scope = argValue(args, "--scope") ?? process.env.ALAN_RUNTIME_SCOPE ?? (setupToken ? "team" : "user");
|
|
23517
23987
|
const installationId = argValue(args, "--installation-id") ?? previousConfig.installationId ?? (0, import_node_crypto3.randomUUID)();
|
|
23518
23988
|
if (scope !== "team" && scope !== "user") {
|
|
23519
23989
|
throw new Error("setup --scope must be either 'team' or 'user'");
|
|
23520
23990
|
}
|
|
23521
|
-
if (!setupToken &&
|
|
23991
|
+
if (!setupToken && !token) {
|
|
23522
23992
|
throw new Error(
|
|
23523
|
-
"setup requires --setup-token <token> or --
|
|
23993
|
+
"setup requires --setup-token <token> or --token <Alan access token>.\nFor normal setup, copy the setup token from Alan and run: alan-agent setup --setup-token <token>\nFor browser authorization, run: alan-agent login"
|
|
23524
23994
|
);
|
|
23525
23995
|
}
|
|
23526
23996
|
const registrationAttemptId = setupToken ? getOrCreateSetupAttemptId(setupToken) : (0, import_node_crypto3.randomUUID)();
|
|
@@ -23540,8 +24010,6 @@ async function setupDaemon(args) {
|
|
|
23540
24010
|
managementKind: "user_managed",
|
|
23541
24011
|
hostKind: "daemon",
|
|
23542
24012
|
lifecycle: "durable",
|
|
23543
|
-
ownerType: scope,
|
|
23544
|
-
visibility: scope,
|
|
23545
24013
|
capabilities: discoverCapabilities(),
|
|
23546
24014
|
metadata: {
|
|
23547
24015
|
...await collectRuntimeMetadataWithProviderLimits(),
|
|
@@ -23587,7 +24055,6 @@ async function setupDaemon(args) {
|
|
|
23587
24055
|
apiUrl,
|
|
23588
24056
|
wsUrl,
|
|
23589
24057
|
endpointProfile,
|
|
23590
|
-
teamId,
|
|
23591
24058
|
runtimeId: body.runtime.id,
|
|
23592
24059
|
runtimeToken: body.runtimeToken,
|
|
23593
24060
|
runtimeRenewalToken: body.runtimeRenewalToken,
|
|
@@ -23697,7 +24164,6 @@ async function loginWithDeviceCode(args) {
|
|
|
23697
24164
|
apiUrl,
|
|
23698
24165
|
wsUrl,
|
|
23699
24166
|
endpointProfile,
|
|
23700
|
-
teamId: body.runtime.teamId ?? void 0,
|
|
23701
24167
|
runtimeId: body.runtime.id,
|
|
23702
24168
|
runtimeToken: body.runtimeToken,
|
|
23703
24169
|
runtimeRenewalToken: body.runtimeRenewalToken,
|
|
@@ -24280,16 +24746,22 @@ async function startDaemon(args) {
|
|
|
24280
24746
|
flushWorkspaceRelocations();
|
|
24281
24747
|
}
|
|
24282
24748
|
}
|
|
24283
|
-
if (workspaceReadiness.ready && expectedRepoUrls.length > 0 && !isAlanDefaultWorkspace(cwd)
|
|
24284
|
-
|
|
24285
|
-
|
|
24286
|
-
|
|
24287
|
-
|
|
24288
|
-
|
|
24289
|
-
|
|
24290
|
-
|
|
24291
|
-
|
|
24292
|
-
|
|
24749
|
+
if (workspaceReadiness.ready && expectedRepoUrls.length > 0 && !isAlanDefaultWorkspace(cwd)) {
|
|
24750
|
+
const worktreePaths = payload.worktreeInfo?.allWorktrees?.map((worktree) => worktree.worktreePath).filter((worktreePath) => Boolean(worktreePath));
|
|
24751
|
+
const repositoryMismatch = worktreePaths && worktreePaths.length > 0 ? worktreePaths.some(
|
|
24752
|
+
(worktreePath) => !workspaceMatchesExpectedRepositories({
|
|
24753
|
+
workspacePath: worktreePath,
|
|
24754
|
+
expectedRepoUrls
|
|
24755
|
+
})
|
|
24756
|
+
) : !workspaceMatchesExpectedRepositories({ workspacePath: cwd, expectedRepoUrls });
|
|
24757
|
+
if (repositoryMismatch) {
|
|
24758
|
+
socket.emit("agent.rejected", {
|
|
24759
|
+
runId: payload.runId,
|
|
24760
|
+
message: "This folder belongs to a different repository. Choose the correct project folder and try again.",
|
|
24761
|
+
code: "workspace_repository_mismatch"
|
|
24762
|
+
});
|
|
24763
|
+
return;
|
|
24764
|
+
}
|
|
24293
24765
|
}
|
|
24294
24766
|
if (workspaceRequired) workspaceAccessTracker.track(cwd, workspaceReadiness);
|
|
24295
24767
|
const runnerSummary = summarizeRunnerStatus({
|
|
@@ -24587,6 +25059,7 @@ async function startDaemon(args) {
|
|
|
24587
25059
|
selectedContextWindow: payload.selectedContextWindow ?? null,
|
|
24588
25060
|
selectedEffortLevel: payload.selectedEffortLevel ?? null,
|
|
24589
25061
|
providerSessionId: payload.providerSessionId,
|
|
25062
|
+
resumeFallbackContext: payload.resumeFallbackContext,
|
|
24590
25063
|
taskMeta: payload.taskMeta,
|
|
24591
25064
|
prMeta: payload.prMeta,
|
|
24592
25065
|
conversationId: payload.conversationId,
|
|
@@ -24623,11 +25096,13 @@ async function startDaemon(args) {
|
|
|
24623
25096
|
if (!payload?.runId) return;
|
|
24624
25097
|
const entry = activeAgents.get(payload.runId);
|
|
24625
25098
|
if (!entry) return;
|
|
24626
|
-
|
|
25099
|
+
const reason = payload.reason === "watchdog_timeout" ? "watchdog_timeout" : "user";
|
|
25100
|
+
abortActiveAgent(entry, { userInitiated: true, reason });
|
|
24627
25101
|
activeAgents.delete(payload.runId);
|
|
24628
|
-
pushLog(`aborted run=${payload.runId}`);
|
|
25102
|
+
pushLog(`aborted run=${payload.runId} reason=${reason}`);
|
|
24629
25103
|
console.info("[alan-agent] Agent run aborted", {
|
|
24630
25104
|
runId: payload.runId,
|
|
25105
|
+
reason,
|
|
24631
25106
|
agentVersion: AGENT_VERSION
|
|
24632
25107
|
});
|
|
24633
25108
|
});
|
|
@@ -24635,7 +25110,9 @@ async function startDaemon(args) {
|
|
|
24635
25110
|
"agent.tool_response",
|
|
24636
25111
|
(payload) => {
|
|
24637
25112
|
if (!payload?.toolId || typeof payload.response !== "string") return;
|
|
24638
|
-
const entries = payload.runId ? [[payload.runId, activeAgents.get(payload.runId)]] : [...activeAgents.entries()]
|
|
25113
|
+
const entries = payload.runId ? [[payload.runId, activeAgents.get(payload.runId)]] : payload.conversationId ? [...activeAgents.entries()].filter(
|
|
25114
|
+
([, entry]) => entry.conversationId === payload.conversationId
|
|
25115
|
+
) : [...activeAgents.entries()];
|
|
24639
25116
|
const delivered = entries.some(([runId, entry]) => {
|
|
24640
25117
|
if (!entry || !verifyActiveWorkspaceLease(runId, entry)) return false;
|
|
24641
25118
|
return entry.agent.sendToolResponse(payload.toolId, payload.response) === true;
|
|
@@ -24643,8 +25120,8 @@ async function startDaemon(args) {
|
|
|
24643
25120
|
if (!delivered) {
|
|
24644
25121
|
pushLog(`tool_response missed tool=${payload.toolId}`);
|
|
24645
25122
|
const fallbackRun = [...activeAgents.entries()][0];
|
|
24646
|
-
const resolvedRunId = payload.runId ?? fallbackRun?.[0];
|
|
24647
|
-
const conversationId = (payload.runId ? activeAgents.get(payload.runId)?.conversationId : void 0) ?? fallbackRun?.[1].conversationId;
|
|
25123
|
+
const resolvedRunId = payload.runId ?? entries[0]?.[0] ?? fallbackRun?.[0];
|
|
25124
|
+
const conversationId = payload.conversationId ?? (payload.runId ? activeAgents.get(payload.runId)?.conversationId : void 0) ?? fallbackRun?.[1].conversationId;
|
|
24648
25125
|
if (conversationId && resolvedRunId) {
|
|
24649
25126
|
eventOutbox.enqueue(conversationId, resolvedRunId, {
|
|
24650
25127
|
type: "error",
|
|
@@ -25427,12 +25904,18 @@ var WebPresenter = class {
|
|
|
25427
25904
|
onLog(message) {
|
|
25428
25905
|
console.error(message);
|
|
25429
25906
|
}
|
|
25907
|
+
onNotice(kind, message) {
|
|
25908
|
+
this.wsClient.emitEvent({ type: "session_notice", kind, message, ts: Date.now() });
|
|
25909
|
+
}
|
|
25430
25910
|
onAssistantText(text) {
|
|
25431
25911
|
this.wsClient.emitEvent({ type: "text", text, ts: Date.now() });
|
|
25432
25912
|
}
|
|
25433
25913
|
onThinking(text) {
|
|
25434
25914
|
this.wsClient.emitEvent({ type: "thinking", text, ts: Date.now() });
|
|
25435
25915
|
}
|
|
25916
|
+
hasKnownToolUse(toolId) {
|
|
25917
|
+
return this.toolNamesById.has(toolId);
|
|
25918
|
+
}
|
|
25436
25919
|
onToolUse(tool, input, toolId, parentToolUseId) {
|
|
25437
25920
|
this.toolNamesById.set(toolId, tool);
|
|
25438
25921
|
this.toolInputsById.set(toolId, input);
|
|
@@ -25524,8 +26007,13 @@ var WebPresenter = class {
|
|
|
25524
26007
|
onCheckpoint(id) {
|
|
25525
26008
|
this.wsClient.emitEvent({ type: "checkpoint", id });
|
|
25526
26009
|
}
|
|
25527
|
-
onActivity(status) {
|
|
25528
|
-
this.wsClient.emitEvent({
|
|
26010
|
+
onActivity(status, evidence) {
|
|
26011
|
+
this.wsClient.emitEvent({
|
|
26012
|
+
type: "activity",
|
|
26013
|
+
status,
|
|
26014
|
+
ts: Date.now(),
|
|
26015
|
+
...evidence ? { evidence } : {}
|
|
26016
|
+
});
|
|
25529
26017
|
}
|
|
25530
26018
|
onOpenBrowserTab(browserId, url2) {
|
|
25531
26019
|
this.wsClient.emitEvent({ type: "open_browser_tab", browserId, ...url2 ? { url: url2 } : {} });
|
|
@@ -25566,7 +26054,7 @@ var WebPresenter = class {
|
|
|
25566
26054
|
};
|
|
25567
26055
|
|
|
25568
26056
|
// src/ws-client.ts
|
|
25569
|
-
var
|
|
26057
|
+
var import_node_crypto5 = require("crypto");
|
|
25570
26058
|
|
|
25571
26059
|
// ../shared/dist/agent-liveness.js
|
|
25572
26060
|
var AGENT_LIVENESS_PROTOCOL_VERSION = 1;
|
|
@@ -25593,7 +26081,117 @@ var agentProbeAckSchema = external_exports.object({
|
|
|
25593
26081
|
protocolVersion: external_exports.number().int().positive().optional()
|
|
25594
26082
|
});
|
|
25595
26083
|
|
|
26084
|
+
// src/sandbox-outbox.ts
|
|
26085
|
+
var import_node_crypto4 = require("crypto");
|
|
26086
|
+
var import_node_fs13 = require("fs");
|
|
26087
|
+
var SANDBOX_EVENT_OUTBOX_ENV = "ALAN_EVENT_OUTBOX_ENABLED";
|
|
26088
|
+
function isSandboxEventOutboxEnabled(env = process.env) {
|
|
26089
|
+
return env[SANDBOX_EVENT_OUTBOX_ENV] === "true";
|
|
26090
|
+
}
|
|
26091
|
+
function deriveSandboxOutboxKey(sessionToken) {
|
|
26092
|
+
if (!sessionToken) {
|
|
26093
|
+
throw new Error("sandbox event outbox requires ALAN_SESSION_TOKEN for key derivation");
|
|
26094
|
+
}
|
|
26095
|
+
return (0, import_node_crypto4.createHash)("sha256").update(sessionToken, "utf8").digest("base64url");
|
|
26096
|
+
}
|
|
26097
|
+
function sandboxOutboxSpoolPath(conversationId) {
|
|
26098
|
+
return `/tmp/alan-agent-outbox-${conversationId}.enc`;
|
|
26099
|
+
}
|
|
26100
|
+
function createSandboxEventOutbox(input) {
|
|
26101
|
+
const path = input.spoolPath ?? sandboxOutboxSpoolPath(input.conversationId);
|
|
26102
|
+
const key = deriveSandboxOutboxKey(input.sessionToken);
|
|
26103
|
+
try {
|
|
26104
|
+
return new EncryptedEventOutbox(path, key, input.pushLog, input.options);
|
|
26105
|
+
} catch (error) {
|
|
26106
|
+
input.pushLog?.(
|
|
26107
|
+
`sandbox_outbox_spool_reset ${error instanceof Error ? error.message : String(error)}`
|
|
26108
|
+
);
|
|
26109
|
+
(0, import_node_fs13.rmSync)(path, { force: true });
|
|
26110
|
+
return new EncryptedEventOutbox(path, key, input.pushLog, input.options);
|
|
26111
|
+
}
|
|
26112
|
+
}
|
|
26113
|
+
var SandboxOutboxSocket = class {
|
|
26114
|
+
constructor(socket) {
|
|
26115
|
+
this.socket = socket;
|
|
26116
|
+
}
|
|
26117
|
+
seqByEventId = /* @__PURE__ */ new Map();
|
|
26118
|
+
seqCounter = 0;
|
|
26119
|
+
get connected() {
|
|
26120
|
+
return this.socket.connected;
|
|
26121
|
+
}
|
|
26122
|
+
emit(_event, payload, acknowledge) {
|
|
26123
|
+
let seq = this.seqByEventId.get(payload.eventId);
|
|
26124
|
+
if (seq === void 0) {
|
|
26125
|
+
this.seqCounter += 1;
|
|
26126
|
+
seq = this.seqCounter;
|
|
26127
|
+
this.seqByEventId.set(payload.eventId, seq);
|
|
26128
|
+
}
|
|
26129
|
+
const wirePayload = { ...payload.event, eventId: payload.eventId, seq };
|
|
26130
|
+
this.socket.emit("agent_event", wirePayload, (result) => {
|
|
26131
|
+
if (result?.ok) this.seqByEventId.delete(payload.eventId);
|
|
26132
|
+
acknowledge({ ok: Boolean(result?.ok), retryable: result?.retryable });
|
|
26133
|
+
});
|
|
26134
|
+
}
|
|
26135
|
+
};
|
|
26136
|
+
var SandboxEventDispatcher = class {
|
|
26137
|
+
conversationId;
|
|
26138
|
+
outbox;
|
|
26139
|
+
outboxSocket;
|
|
26140
|
+
currentRunId;
|
|
26141
|
+
constructor(input) {
|
|
26142
|
+
this.conversationId = input.conversationId;
|
|
26143
|
+
if (!input.enabled) return;
|
|
26144
|
+
try {
|
|
26145
|
+
this.outbox = createSandboxEventOutbox({
|
|
26146
|
+
conversationId: input.conversationId,
|
|
26147
|
+
sessionToken: input.sessionToken,
|
|
26148
|
+
pushLog: input.pushLog,
|
|
26149
|
+
options: input.options,
|
|
26150
|
+
spoolPath: input.spoolPath
|
|
26151
|
+
});
|
|
26152
|
+
this.outboxSocket = new SandboxOutboxSocket(input.socket);
|
|
26153
|
+
} catch (error) {
|
|
26154
|
+
input.onInitError?.(error);
|
|
26155
|
+
this.outbox = void 0;
|
|
26156
|
+
this.outboxSocket = void 0;
|
|
26157
|
+
}
|
|
26158
|
+
}
|
|
26159
|
+
/** True when events are being durably queued rather than fired-and-forgotten. */
|
|
26160
|
+
get active() {
|
|
26161
|
+
return this.outbox !== void 0 && this.outboxSocket !== void 0;
|
|
26162
|
+
}
|
|
26163
|
+
/** Set the run id used to bucket per-run caps / terminal condensation. */
|
|
26164
|
+
setRunId(runId) {
|
|
26165
|
+
this.currentRunId = runId;
|
|
26166
|
+
}
|
|
26167
|
+
/**
|
|
26168
|
+
* Durably deliver one event (enqueue-before-send, remove-on-ack). When the
|
|
26169
|
+
* outbox is inactive, defers to `fireAndForget` so behaviour is unchanged.
|
|
26170
|
+
*/
|
|
26171
|
+
emit(event, fireAndForget) {
|
|
26172
|
+
if (!this.outbox || !this.outboxSocket) {
|
|
26173
|
+
fireAndForget(event);
|
|
26174
|
+
return;
|
|
26175
|
+
}
|
|
26176
|
+
this.outbox.enqueue(this.conversationId, this.currentRunId ?? this.conversationId, event);
|
|
26177
|
+
this.outbox.flush(this.outboxSocket);
|
|
26178
|
+
}
|
|
26179
|
+
/** Replay un-acked events in order — called on (re)connect before new events. */
|
|
26180
|
+
flush() {
|
|
26181
|
+
if (this.outbox && this.outboxSocket) this.outbox.flush(this.outboxSocket);
|
|
26182
|
+
}
|
|
26183
|
+
/** Reset in-flight state on disconnect so the next flush replays from the head. */
|
|
26184
|
+
disconnect() {
|
|
26185
|
+
this.outbox?.disconnect();
|
|
26186
|
+
}
|
|
26187
|
+
/** Diagnostics: number of events still awaiting a server ack. */
|
|
26188
|
+
pendingCount() {
|
|
26189
|
+
return this.outbox?.pendingCount() ?? 0;
|
|
26190
|
+
}
|
|
26191
|
+
};
|
|
26192
|
+
|
|
25596
26193
|
// src/ws-client.ts
|
|
26194
|
+
var ACCEPTED_MESSAGE_ID_CACHE_SIZE = 200;
|
|
25597
26195
|
var WSClient = class {
|
|
25598
26196
|
constructor(wsUrl, sessionId, taskId, token, callbacks, agentVersion, lifecycle) {
|
|
25599
26197
|
this.lifecycle = lifecycle;
|
|
@@ -25612,16 +26210,63 @@ var WSClient = class {
|
|
|
25612
26210
|
upgrade: false,
|
|
25613
26211
|
extraHeaders: { "ngrok-skip-browser-warning": "1" }
|
|
25614
26212
|
});
|
|
26213
|
+
const outboxEnabled = isSandboxEventOutboxEnabled();
|
|
26214
|
+
this.eventDispatcher = new SandboxEventDispatcher({
|
|
26215
|
+
conversationId: sessionId,
|
|
26216
|
+
sessionToken: token,
|
|
26217
|
+
enabled: outboxEnabled,
|
|
26218
|
+
socket: this.socket,
|
|
26219
|
+
pushLog: (message) => this.lifecycle?.info("sandbox_agent_event_outbox", { detail: message }),
|
|
26220
|
+
onInitError: (error) => this.lifecycle?.warn("sandbox_agent_event_outbox_init_failed", {
|
|
26221
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
26222
|
+
})
|
|
26223
|
+
});
|
|
26224
|
+
if (this.eventDispatcher.active) {
|
|
26225
|
+
this.lifecycle?.info("sandbox_agent_event_outbox_enabled", { conversationId: sessionId });
|
|
26226
|
+
}
|
|
25615
26227
|
this.setupListeners(callbacks);
|
|
25616
26228
|
}
|
|
25617
26229
|
socket;
|
|
25618
26230
|
/** Stable id for this agent process; fences stale heartbeats server-side. */
|
|
25619
|
-
agentSessionId = (0,
|
|
26231
|
+
agentSessionId = (0, import_node_crypto5.randomUUID)();
|
|
25620
26232
|
/** Monotonic heartbeat sequence — advances on every hello + heartbeat. */
|
|
25621
26233
|
heartbeatSeq = 0;
|
|
25622
26234
|
heartbeatTimer = null;
|
|
25623
26235
|
agentVersion;
|
|
25624
26236
|
getRunState;
|
|
26237
|
+
/**
|
|
26238
|
+
* Durable at-least-once delivery for stream events. Active only when
|
|
26239
|
+
* `ALAN_EVENT_OUTBOX_ENABLED === "true"`; otherwise events are fired-and-
|
|
26240
|
+
* forgotten exactly as before (see {@link SandboxEventDispatcher}).
|
|
26241
|
+
*/
|
|
26242
|
+
eventDispatcher;
|
|
26243
|
+
/**
|
|
26244
|
+
* Bounded record of `messageId`s whose handoff to `onUserMessage` already
|
|
26245
|
+
* completed successfully. A redelivered `user_message` for one of these ids
|
|
26246
|
+
* (e.g. the original `ok:true` ack was lost in transit and the server's
|
|
26247
|
+
* ≤5-attempt redelivery re-armed the run) is re-acked `{ ok: true }`
|
|
26248
|
+
* without invoking the handler again, so the message is never processed
|
|
26249
|
+
* twice. A `Set`'s insertion order doubles as recency, so eviction is just
|
|
26250
|
+
* "drop the oldest key" — a minimal LRU without extra bookkeeping.
|
|
26251
|
+
*/
|
|
26252
|
+
acceptedMessageIds = /* @__PURE__ */ new Set();
|
|
26253
|
+
/** Record a successfully-handed-off messageId, evicting the oldest entry past the cap. */
|
|
26254
|
+
rememberAcceptedMessageId(messageId) {
|
|
26255
|
+
this.acceptedMessageIds.delete(messageId);
|
|
26256
|
+
this.acceptedMessageIds.add(messageId);
|
|
26257
|
+
if (this.acceptedMessageIds.size > ACCEPTED_MESSAGE_ID_CACHE_SIZE) {
|
|
26258
|
+
const oldest = this.acceptedMessageIds.keys().next().value;
|
|
26259
|
+
if (oldest !== void 0) this.acceptedMessageIds.delete(oldest);
|
|
26260
|
+
}
|
|
26261
|
+
}
|
|
26262
|
+
/**
|
|
26263
|
+
* Record the run id for events emitted from here on, so the durable outbox can
|
|
26264
|
+
* bucket per-run caps and terminal-event condensation correctly. No-op when the
|
|
26265
|
+
* outbox is disabled.
|
|
26266
|
+
*/
|
|
26267
|
+
setCurrentRunId(runId) {
|
|
26268
|
+
this.eventDispatcher.setRunId(runId);
|
|
26269
|
+
}
|
|
25625
26270
|
currentRunState() {
|
|
25626
26271
|
try {
|
|
25627
26272
|
return this.getRunState?.() ?? { idle: true, activeRunIds: [] };
|
|
@@ -25658,7 +26303,9 @@ var WSClient = class {
|
|
|
25658
26303
|
}
|
|
25659
26304
|
}
|
|
25660
26305
|
emitEvent(event) {
|
|
25661
|
-
this.
|
|
26306
|
+
this.eventDispatcher.emit(event, (payload) => {
|
|
26307
|
+
this.socket.emit("agent_event", payload);
|
|
26308
|
+
});
|
|
25662
26309
|
}
|
|
25663
26310
|
waitForConnection(timeoutMs = 15e3) {
|
|
25664
26311
|
return new Promise((resolve6, reject) => {
|
|
@@ -25695,11 +26342,13 @@ var WSClient = class {
|
|
|
25695
26342
|
this.socket.on("connect", () => {
|
|
25696
26343
|
this.lifecycle?.info("sandbox_agent_ws_socket_connected", { socketId: this.socket.id });
|
|
25697
26344
|
this.startHeartbeat();
|
|
26345
|
+
this.eventDispatcher.flush();
|
|
25698
26346
|
callbacks.onConnect?.();
|
|
25699
26347
|
});
|
|
25700
26348
|
this.socket.on("disconnect", (reason) => {
|
|
25701
26349
|
this.lifecycle?.info("sandbox_agent_ws_socket_disconnected", { reason });
|
|
25702
26350
|
this.stopHeartbeat();
|
|
26351
|
+
this.eventDispatcher.disconnect();
|
|
25703
26352
|
callbacks.onDisconnect?.(reason);
|
|
25704
26353
|
});
|
|
25705
26354
|
this.socket.on("agent.probe", (_data, ack) => {
|
|
@@ -25717,8 +26366,11 @@ var WSClient = class {
|
|
|
25717
26366
|
this.socket.on(
|
|
25718
26367
|
"user_message",
|
|
25719
26368
|
(data, ack) => {
|
|
25720
|
-
|
|
25721
|
-
|
|
26369
|
+
if (data.messageId && this.acceptedMessageIds.has(data.messageId)) {
|
|
26370
|
+
ack?.({ ok: true });
|
|
26371
|
+
return;
|
|
26372
|
+
}
|
|
26373
|
+
const payload = {
|
|
25722
26374
|
text: data.text,
|
|
25723
26375
|
images: data.images,
|
|
25724
26376
|
files: data.files,
|
|
@@ -25742,7 +26394,24 @@ var WSClient = class {
|
|
|
25742
26394
|
conversationId: data.conversationId,
|
|
25743
26395
|
teamId: data.teamId,
|
|
25744
26396
|
currentUser: data.currentUser
|
|
25745
|
-
}
|
|
26397
|
+
};
|
|
26398
|
+
const acceptDelivery = () => {
|
|
26399
|
+
if (data.messageId) this.rememberAcceptedMessageId(data.messageId);
|
|
26400
|
+
ack?.({ ok: true });
|
|
26401
|
+
};
|
|
26402
|
+
const rejectDelivery = (error) => {
|
|
26403
|
+
ack?.({ ok: false, message: error instanceof Error ? error.message : String(error) });
|
|
26404
|
+
};
|
|
26405
|
+
try {
|
|
26406
|
+
const result = callbacks.onUserMessage(payload);
|
|
26407
|
+
if (result instanceof Promise) {
|
|
26408
|
+
result.then(acceptDelivery, rejectDelivery);
|
|
26409
|
+
} else {
|
|
26410
|
+
acceptDelivery();
|
|
26411
|
+
}
|
|
26412
|
+
} catch (error) {
|
|
26413
|
+
rejectDelivery(error);
|
|
26414
|
+
}
|
|
25746
26415
|
}
|
|
25747
26416
|
);
|
|
25748
26417
|
this.socket.on("stop", () => {
|
|
@@ -25911,6 +26580,7 @@ async function runSandbox(config) {
|
|
|
25911
26580
|
backendKind: activeBackendKind
|
|
25912
26581
|
});
|
|
25913
26582
|
currentAgent = agent;
|
|
26583
|
+
wsClient.setCurrentRunId(currentRunId ?? config.runId);
|
|
25914
26584
|
const correlation = correlationLogFields2({
|
|
25915
26585
|
taskId: currentTaskId,
|
|
25916
26586
|
conversationId: currentConversationId,
|
|
@@ -26128,7 +26798,7 @@ async function runSandbox(config) {
|
|
|
26128
26798
|
|
|
26129
26799
|
// src/service-manager.ts
|
|
26130
26800
|
var import_node_child_process6 = require("child_process");
|
|
26131
|
-
var
|
|
26801
|
+
var import_node_fs14 = require("fs");
|
|
26132
26802
|
var import_node_os8 = require("os");
|
|
26133
26803
|
var import_node_path11 = require("path");
|
|
26134
26804
|
var SERVICE_LABEL = "ai.tryalan.agent";
|
|
@@ -26327,14 +26997,14 @@ function currentServicePlan(args) {
|
|
|
26327
26997
|
});
|
|
26328
26998
|
}
|
|
26329
26999
|
function writeManifest(path, contents) {
|
|
26330
|
-
(0,
|
|
27000
|
+
(0, import_node_fs14.mkdirSync)((0, import_node_path11.dirname)(path), { recursive: true, mode: 448 });
|
|
26331
27001
|
const pendingPath = `${path}.pending-${process.pid}`;
|
|
26332
27002
|
try {
|
|
26333
|
-
(0,
|
|
26334
|
-
(0,
|
|
26335
|
-
(0,
|
|
27003
|
+
(0, import_node_fs14.writeFileSync)(pendingPath, contents, { mode: 384 });
|
|
27004
|
+
(0, import_node_fs14.chmodSync)(pendingPath, 384);
|
|
27005
|
+
(0, import_node_fs14.renameSync)(pendingPath, path);
|
|
26336
27006
|
} finally {
|
|
26337
|
-
(0,
|
|
27007
|
+
(0, import_node_fs14.rmSync)(pendingPath, { force: true });
|
|
26338
27008
|
}
|
|
26339
27009
|
}
|
|
26340
27010
|
function runServiceCommand(command) {
|
|
@@ -26357,7 +27027,7 @@ function installDaemonService(args = []) {
|
|
|
26357
27027
|
const plan = currentServicePlan(args);
|
|
26358
27028
|
if (plan.manifestPath && plan.manifest) {
|
|
26359
27029
|
if ((0, import_node_os8.platform)() === "darwin") {
|
|
26360
|
-
(0,
|
|
27030
|
+
(0, import_node_fs14.mkdirSync)((0, import_node_path11.join)((0, import_node_os8.homedir)(), ".alan", "agent", "logs"), { recursive: true, mode: 448 });
|
|
26361
27031
|
}
|
|
26362
27032
|
writeManifest(plan.manifestPath, plan.manifest);
|
|
26363
27033
|
}
|
|
@@ -26367,7 +27037,7 @@ function installDaemonService(args = []) {
|
|
|
26367
27037
|
function uninstallDaemonService(args = []) {
|
|
26368
27038
|
const plan = currentServicePlan(args);
|
|
26369
27039
|
for (const command of plan.uninstallCommands) runServiceCommand(command);
|
|
26370
|
-
if (plan.manifestPath) (0,
|
|
27040
|
+
if (plan.manifestPath) (0, import_node_fs14.rmSync)(plan.manifestPath, { force: true });
|
|
26371
27041
|
console.info("[alan-agent] Per-user daemon service removed");
|
|
26372
27042
|
}
|
|
26373
27043
|
function printDaemonServiceStatus(args = []) {
|