@aiden-ade/sandbox-agent 0.1.42 → 0.1.44
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 +1486 -210
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -13315,13 +13315,13 @@ function resolveViaWhere(command, env) {
|
|
|
13315
13315
|
}
|
|
13316
13316
|
return null;
|
|
13317
13317
|
}
|
|
13318
|
-
function runCliVersionProbe(executable, env, args = ["--version"]) {
|
|
13318
|
+
function runCliVersionProbe(executable, env, args = ["--version"], timeoutMs = 3e3) {
|
|
13319
13319
|
const isWin = (0, import_node_os3.platform)() === "win32";
|
|
13320
13320
|
if (isWin && /\.(cmd|bat)$/i.test(executable)) {
|
|
13321
13321
|
const comSpec = env.ComSpec ?? process.env.ComSpec ?? "cmd.exe";
|
|
13322
13322
|
return (0, import_node_child_process2.spawnSync)(comSpec, ["/d", "/s", "/c", executable, ...args], {
|
|
13323
13323
|
encoding: "utf8",
|
|
13324
|
-
timeout:
|
|
13324
|
+
timeout: timeoutMs,
|
|
13325
13325
|
env,
|
|
13326
13326
|
windowsHide: true
|
|
13327
13327
|
});
|
|
@@ -13329,7 +13329,7 @@ function runCliVersionProbe(executable, env, args = ["--version"]) {
|
|
|
13329
13329
|
const useShell = isWin && !/[\\/]/.test(executable);
|
|
13330
13330
|
return (0, import_node_child_process2.spawnSync)(executable, args, {
|
|
13331
13331
|
encoding: "utf8",
|
|
13332
|
-
timeout:
|
|
13332
|
+
timeout: timeoutMs,
|
|
13333
13333
|
env,
|
|
13334
13334
|
shell: useShell,
|
|
13335
13335
|
windowsHide: isWin ? true : void 0
|
|
@@ -14555,15 +14555,6 @@ If the index returns no useful path, say so and run one targeted local search in
|
|
|
14555
14555
|
function buildAlanTeamCodeContextOverlay(teamId) {
|
|
14556
14556
|
return [buildAlanTeamScopePrompt(teamId), ALAN_CODE_CONTEXT_PROMPT].join("\n\n");
|
|
14557
14557
|
}
|
|
14558
|
-
function buildCodeDiscoveryEnforcementTail() {
|
|
14559
|
-
return [
|
|
14560
|
-
"## MANDATORY \u2014 index once, then use local source",
|
|
14561
|
-
"1. For codebase discovery, make one `mcp__alan__search_code_context` call (hybrid, topK 5). Omit teamId \u2014 Alan session headers resolve it.",
|
|
14562
|
-
"2. Once useful paths are returned, stop querying the index and read those local files. The current worktree is the source of truth.",
|
|
14563
|
-
"3. Use narrow local `Grep`/graph queries only after the orientation pass. If the index has no useful path, run one targeted local search in the likely package.",
|
|
14564
|
-
"Do not repeat or rephrase index searches once paths are known, and do not claim current behavior from index excerpts alone."
|
|
14565
|
-
].join("\n");
|
|
14566
|
-
}
|
|
14567
14558
|
var PLAN_MODE_PROMPT = [
|
|
14568
14559
|
"You are in Alan plan mode.",
|
|
14569
14560
|
"Analyze the task, inspect the relevant code and context, and produce a concrete implementation plan.",
|
|
@@ -15305,6 +15296,8 @@ var import_path5 = require("path");
|
|
|
15305
15296
|
var import_fs6 = require("fs");
|
|
15306
15297
|
var import_os6 = require("os");
|
|
15307
15298
|
var import_path6 = require("path");
|
|
15299
|
+
var import_readline2 = require("readline");
|
|
15300
|
+
var import_url2 = require("url");
|
|
15308
15301
|
var import_fs7 = require("fs");
|
|
15309
15302
|
var import_os7 = require("os");
|
|
15310
15303
|
var import_path7 = require("path");
|
|
@@ -15443,6 +15436,11 @@ function buildSessionHeaders(input) {
|
|
|
15443
15436
|
if (input.allowedTools?.length) {
|
|
15444
15437
|
headers["x-allowed-tools"] = input.allowedTools.join(",");
|
|
15445
15438
|
}
|
|
15439
|
+
if (input.runHeaders) {
|
|
15440
|
+
for (const [key, value2] of Object.entries(input.runHeaders)) {
|
|
15441
|
+
if (typeof value2 === "string" && value2.length > 0) headers[key] = value2;
|
|
15442
|
+
}
|
|
15443
|
+
}
|
|
15446
15444
|
return headers;
|
|
15447
15445
|
}
|
|
15448
15446
|
function getTomlSectionName(line) {
|
|
@@ -15653,6 +15651,23 @@ function syncAlanMcpSessionHeaders(input) {
|
|
|
15653
15651
|
}
|
|
15654
15652
|
return written;
|
|
15655
15653
|
}
|
|
15654
|
+
var mcpConfigWriteGateTail = Promise.resolve();
|
|
15655
|
+
function acquireMcpConfigWriteGate() {
|
|
15656
|
+
const previous = mcpConfigWriteGateTail;
|
|
15657
|
+
let releaseCurrent;
|
|
15658
|
+
const current = new Promise((resolve22) => {
|
|
15659
|
+
releaseCurrent = resolve22;
|
|
15660
|
+
});
|
|
15661
|
+
mcpConfigWriteGateTail = previous.then(() => current);
|
|
15662
|
+
return previous.then(() => {
|
|
15663
|
+
let released = false;
|
|
15664
|
+
return () => {
|
|
15665
|
+
if (released) return;
|
|
15666
|
+
released = true;
|
|
15667
|
+
releaseCurrent();
|
|
15668
|
+
};
|
|
15669
|
+
});
|
|
15670
|
+
}
|
|
15656
15671
|
function buildPromptWithSystem(config, promptText) {
|
|
15657
15672
|
const parts2 = [
|
|
15658
15673
|
config.systemPrompt?.trim(),
|
|
@@ -15661,6 +15676,13 @@ function buildPromptWithSystem(config, promptText) {
|
|
|
15661
15676
|
].filter((value2) => Boolean(value2 && value2.length > 0));
|
|
15662
15677
|
return parts2.join("\n\n");
|
|
15663
15678
|
}
|
|
15679
|
+
function buildResumeAwarePrompt(config, promptText, options) {
|
|
15680
|
+
const systemParts = options.isResume ? [config.systemPromptAppend?.trim()] : [config.systemPrompt?.trim()];
|
|
15681
|
+
const parts2 = [...systemParts, promptText.trim()].filter(
|
|
15682
|
+
(value2) => Boolean(value2 && value2.length > 0)
|
|
15683
|
+
);
|
|
15684
|
+
return parts2.join("\n\n");
|
|
15685
|
+
}
|
|
15664
15686
|
function buildPlanModePrefix(promptText) {
|
|
15665
15687
|
return [
|
|
15666
15688
|
"You are in plan-only mode.",
|
|
@@ -15827,7 +15849,7 @@ var ERROR_SPECS = {
|
|
|
15827
15849
|
recoveryClass: "needs_env"
|
|
15828
15850
|
},
|
|
15829
15851
|
auth_expired: {
|
|
15830
|
-
message: "The provider connection expired.
|
|
15852
|
+
message: "The provider connection expired. Re-authenticate the agent CLI on this runtime (or reconnect it in your provider/Cloud settings), then retry.",
|
|
15831
15853
|
recoveryClass: "user_fixable"
|
|
15832
15854
|
},
|
|
15833
15855
|
subscription_required: {
|
|
@@ -15917,7 +15939,7 @@ function normalizeCodexCliErrorMessage(message) {
|
|
|
15917
15939
|
return "Codex connection to OpenAI timed out while waiting for the turn to continue. Retry the message; if it repeats, check network/API latency or reduce slow MCP/tool calls in the turn.";
|
|
15918
15940
|
}
|
|
15919
15941
|
if (lower.includes("access token could not be refreshed") || lower.includes("refresh_token_invalidated") || lower.includes("refresh token has been invalidated")) {
|
|
15920
|
-
return "Codex CLI connection expired.
|
|
15942
|
+
return "Codex CLI connection expired. Re-authenticate the Codex CLI on this computer (run `codex login`) or reconnect it in Cloud settings, then retry.";
|
|
15921
15943
|
}
|
|
15922
15944
|
return message;
|
|
15923
15945
|
}
|
|
@@ -16298,6 +16320,9 @@ function createGenericCliBackend(options) {
|
|
|
16298
16320
|
} else if (state.resultDeferredOnBackgroundWork && state.activeBackgroundTaskIds?.size) {
|
|
16299
16321
|
idleTimeoutReason = "background_task";
|
|
16300
16322
|
resolve22("idle_timeout");
|
|
16323
|
+
} else if (options.postStartupSilenceTimeoutMs && sawStdoutLine && typeof state.lastRawOutputAtMs === "number" && Date.now() - state.lastRawOutputAtMs >= options.postStartupSilenceTimeoutMs) {
|
|
16324
|
+
idleTimeoutReason = "post_startup_silence";
|
|
16325
|
+
resolve22("idle_timeout");
|
|
16301
16326
|
} else {
|
|
16302
16327
|
armIdleTimer();
|
|
16303
16328
|
}
|
|
@@ -16310,8 +16335,9 @@ function createGenericCliBackend(options) {
|
|
|
16310
16335
|
if (idleTimer) clearTimeout(idleTimer);
|
|
16311
16336
|
if (raceResult === "idle_timeout") {
|
|
16312
16337
|
idleTimedOut = true;
|
|
16338
|
+
const idleWaitLabel = idleTimeoutReason === "background_task" ? "a background task terminal event" : idleTimeoutReason === "post_startup_silence" ? "any further output (silent hang)" : "a pending user answer";
|
|
16313
16339
|
console.warn(
|
|
16314
|
-
`[${options.kind}] Idle timeout (${IDLE_MS}ms) waiting for ${
|
|
16340
|
+
`[${options.kind}] Idle timeout (${IDLE_MS}ms) waiting for ${idleWaitLabel} \u2014 force-killing orphaned process`
|
|
16315
16341
|
);
|
|
16316
16342
|
killProcessTree(child.pid, { signal: "SIGKILL", child });
|
|
16317
16343
|
exitCode = await Promise.race([
|
|
@@ -16384,6 +16410,25 @@ function createGenericCliBackend(options) {
|
|
|
16384
16410
|
console.error(`[${options.kind}] Process force-killed by startup watchdog`, {
|
|
16385
16411
|
stderrLines: stderrLines.slice(-10)
|
|
16386
16412
|
});
|
|
16413
|
+
const startupStderr = stderrLines.join("\n").trim();
|
|
16414
|
+
if (startupStderr) {
|
|
16415
|
+
const classified = classifyCliErrorDetailed(startupStderr, exitCode);
|
|
16416
|
+
return {
|
|
16417
|
+
success: false,
|
|
16418
|
+
summary: state.summary.trim() || classified.message,
|
|
16419
|
+
filesModified: [],
|
|
16420
|
+
planFilesCreated: [],
|
|
16421
|
+
iterations: Math.max(state.iterations, 1),
|
|
16422
|
+
error: classified.message,
|
|
16423
|
+
...classified.errorKind !== "unknown_cli_error" ? { errorKind: classified.errorKind } : {},
|
|
16424
|
+
recoveryClass: classified.recoveryClass,
|
|
16425
|
+
providerSessionId: state.runtimeSessionId,
|
|
16426
|
+
runtimeSessionId: state.runtimeSessionId,
|
|
16427
|
+
backendKind: options.kind,
|
|
16428
|
+
supportTier: options.supportTier,
|
|
16429
|
+
usage: state.usage
|
|
16430
|
+
};
|
|
16431
|
+
}
|
|
16387
16432
|
return {
|
|
16388
16433
|
success: false,
|
|
16389
16434
|
summary: "Agent produced no output",
|
|
@@ -16433,7 +16478,7 @@ function createGenericCliBackend(options) {
|
|
|
16433
16478
|
filesModified: [],
|
|
16434
16479
|
planFilesCreated: [],
|
|
16435
16480
|
iterations: Math.max(state.iterations, 1),
|
|
16436
|
-
error: idleTimeoutReason === "background_task" ? "Agent timed out waiting for a background task to finish \u2014 no terminal event arrived, so the run was stopped. Send your message again to continue." : "Agent timed out waiting for your answer to its question \u2014 no response arrived, so the run was stopped. Send your reply as a new message to continue.",
|
|
16481
|
+
error: idleTimeoutReason === "background_task" ? "Agent timed out waiting for a background task to finish \u2014 no terminal event arrived, so the run was stopped. Send your message again to continue." : idleTimeoutReason === "post_startup_silence" ? "The agent went silent and stopped responding, so the run was stopped. Send your message again to retry." : "Agent timed out waiting for your answer to its question \u2014 no response arrived, so the run was stopped. Send your reply as a new message to continue.",
|
|
16437
16482
|
providerSessionId: state.runtimeSessionId,
|
|
16438
16483
|
runtimeSessionId: state.runtimeSessionId,
|
|
16439
16484
|
backendKind: options.kind,
|
|
@@ -17141,6 +17186,16 @@ var TERMINAL_TASK_NOTIFICATION_STATUSES = /* @__PURE__ */ new Set([
|
|
|
17141
17186
|
"cancelled",
|
|
17142
17187
|
"canceled"
|
|
17143
17188
|
]);
|
|
17189
|
+
var NON_TERMINAL_TASK_NOTIFICATION_STATUSES = /* @__PURE__ */ new Set([
|
|
17190
|
+
"running",
|
|
17191
|
+
"in_progress",
|
|
17192
|
+
"inprogress",
|
|
17193
|
+
"started",
|
|
17194
|
+
"pending",
|
|
17195
|
+
"active",
|
|
17196
|
+
"working",
|
|
17197
|
+
"queued"
|
|
17198
|
+
]);
|
|
17144
17199
|
function extractToolResultContent(content) {
|
|
17145
17200
|
if (typeof content === "string") return content;
|
|
17146
17201
|
if (!Array.isArray(content)) return "";
|
|
@@ -17158,6 +17213,7 @@ function stringField2(record, key) {
|
|
|
17158
17213
|
function isTerminalTaskNotification(record) {
|
|
17159
17214
|
const status = stringField2(record, "status")?.toLowerCase();
|
|
17160
17215
|
if (status && TERMINAL_TASK_NOTIFICATION_STATUSES.has(status)) return true;
|
|
17216
|
+
if (status && NON_TERMINAL_TASK_NOTIFICATION_STATUSES.has(status)) return false;
|
|
17161
17217
|
return typeof record.duration_ms === "number" || typeof record.durationMs === "number" || typeof record.total_tokens === "number" || typeof record.totalTokens === "number" || typeof record.tool_uses === "number" || typeof record.toolUses === "number";
|
|
17162
17218
|
}
|
|
17163
17219
|
function isFailedTaskNotification(record) {
|
|
@@ -17249,20 +17305,25 @@ function handleClaudeStructuredEvent(parsed, context, state) {
|
|
|
17249
17305
|
state.iterations += 1;
|
|
17250
17306
|
const message = typeof parsed.message === "object" && parsed.message !== null ? parsed.message : null;
|
|
17251
17307
|
const content = Array.isArray(message?.content) ? message.content : [];
|
|
17308
|
+
let previousBlockWasText = false;
|
|
17252
17309
|
for (const block of content) {
|
|
17253
17310
|
if (typeof block !== "object" || block === null || !("type" in block)) continue;
|
|
17254
17311
|
if (block.type === "text" && typeof block.text === "string") {
|
|
17255
17312
|
const text = block.text;
|
|
17256
17313
|
state.summary += `${text}
|
|
17257
17314
|
`;
|
|
17258
|
-
void presenter.onAssistantText(
|
|
17315
|
+
void presenter.onAssistantText(previousBlockWasText ? `
|
|
17316
|
+
${text}` : text);
|
|
17317
|
+
previousBlockWasText = true;
|
|
17259
17318
|
continue;
|
|
17260
17319
|
}
|
|
17261
17320
|
if (block.type === "thinking" && typeof block.thinking === "string") {
|
|
17321
|
+
previousBlockWasText = false;
|
|
17262
17322
|
void presenter.onThinking(block.thinking);
|
|
17263
17323
|
continue;
|
|
17264
17324
|
}
|
|
17265
17325
|
if (block.type === "tool_use" && typeof block.id === "string" && typeof block.name === "string") {
|
|
17326
|
+
previousBlockWasText = false;
|
|
17266
17327
|
const toolBlock = block;
|
|
17267
17328
|
void presenter.onToolUse(
|
|
17268
17329
|
toolBlock.name,
|
|
@@ -17378,7 +17439,7 @@ function handleClaudeStructuredEvent(parsed, context, state) {
|
|
|
17378
17439
|
state.iterations = parsed.num_turns;
|
|
17379
17440
|
}
|
|
17380
17441
|
const subtype = typeof parsed.subtype === "string" ? parsed.subtype : "";
|
|
17381
|
-
if ((
|
|
17442
|
+
if (subtype.startsWith("error") && !state.error) {
|
|
17382
17443
|
const errObj = typeof parsed.error === "object" && parsed.error !== null ? parsed.error : null;
|
|
17383
17444
|
const resumeId = context.config.runtimeSessionId?.trim() || context.config.providerSessionId?.trim();
|
|
17384
17445
|
const errMsg = subtype === "error_during_execution" && resumeId ? SESSION_RESUME_FAILED_MESSAGE : typeof parsed.error === "string" && parsed.error.trim() || errObj && typeof errObj.message === "string" && errObj.message.trim() || typeof parsed.message === "string" && parsed.message.trim() || typeof parsed.result === "string" && parsed.result.trim() || subtype;
|
|
@@ -17529,7 +17590,7 @@ function createClaudeCliBackend(command = "claude", defaultArgs = []) {
|
|
|
17529
17590
|
const basePromptText = resumeContextPrefix ? `${resumeContextPrefix}
|
|
17530
17591
|
|
|
17531
17592
|
${ctx.promptText}` : ctx.promptText;
|
|
17532
|
-
const promptText = ctx.config.mode === "plan" ? buildPlanModePrefix(
|
|
17593
|
+
const promptText = ctx.config.mode === "plan" ? buildPlanModePrefix(basePromptText) : basePromptText;
|
|
17533
17594
|
if (promptText.trim()) {
|
|
17534
17595
|
contentBlocks.push({ type: "text", text: promptText });
|
|
17535
17596
|
}
|
|
@@ -17620,6 +17681,25 @@ function buildGeneratedImageFromCodexPayload(payload) {
|
|
|
17620
17681
|
idFields: []
|
|
17621
17682
|
});
|
|
17622
17683
|
}
|
|
17684
|
+
function extractRolloutMessageText(payload) {
|
|
17685
|
+
const content = payload.content;
|
|
17686
|
+
if (typeof content === "string") return content;
|
|
17687
|
+
if (Array.isArray(content)) {
|
|
17688
|
+
for (const block of content) {
|
|
17689
|
+
if (block && typeof block === "object" && typeof block.text === "string") {
|
|
17690
|
+
return block.text;
|
|
17691
|
+
}
|
|
17692
|
+
}
|
|
17693
|
+
}
|
|
17694
|
+
return "";
|
|
17695
|
+
}
|
|
17696
|
+
function isSyntheticUserRolloutMessage(payload) {
|
|
17697
|
+
const text = extractRolloutMessageText(payload).trimStart();
|
|
17698
|
+
if (!text) return false;
|
|
17699
|
+
const lower = text.toLowerCase();
|
|
17700
|
+
return lower.startsWith("<environment_context") || lower.startsWith("<user_instructions") || // Auto-compaction bridge summaries are tagged in the rollout content.
|
|
17701
|
+
lower.startsWith("<compact") || lower.startsWith("[compact");
|
|
17702
|
+
}
|
|
17623
17703
|
function latestUserMessageLineIndex(lines) {
|
|
17624
17704
|
let latestIndex = -1;
|
|
17625
17705
|
for (let index = 0; index < lines.length; index += 1) {
|
|
@@ -17627,7 +17707,7 @@ function latestUserMessageLineIndex(lines) {
|
|
|
17627
17707
|
if (!line) continue;
|
|
17628
17708
|
const entry = parseJsonObject(line);
|
|
17629
17709
|
const payload = entry && typeof entry.payload === "object" && entry.payload !== null ? entry.payload : null;
|
|
17630
|
-
if (payload?.type === "message" && payload.role === "user") {
|
|
17710
|
+
if (payload?.type === "message" && payload.role === "user" && !isSyntheticUserRolloutMessage(payload)) {
|
|
17631
17711
|
latestIndex = index;
|
|
17632
17712
|
}
|
|
17633
17713
|
}
|
|
@@ -17691,7 +17771,9 @@ async function replayCodexMcpToolEventsFromSessionLog(context, state) {
|
|
|
17691
17771
|
}
|
|
17692
17772
|
}
|
|
17693
17773
|
var CODEX_EXIT_GRACE_MS = 3e4;
|
|
17694
|
-
|
|
17774
|
+
var CODEX_BACKGROUND_EXIT_GRACE_MS = 10 * 6e4;
|
|
17775
|
+
var CODEX_EXIT_GRACE_SIGKILL_MS = 3e3;
|
|
17776
|
+
function armCodexExitGraceKill(state, graceMs = CODEX_EXIT_GRACE_MS) {
|
|
17695
17777
|
if (state.exitGraceKillTimer) return;
|
|
17696
17778
|
const child = state.process;
|
|
17697
17779
|
if (!child) return;
|
|
@@ -17701,7 +17783,15 @@ function armCodexExitGraceKill(state) {
|
|
|
17701
17783
|
"[codex_app_server] Process did not exit after turn completion; killing process group"
|
|
17702
17784
|
);
|
|
17703
17785
|
killProcessTree(child.pid, { signal: "SIGTERM", child });
|
|
17704
|
-
|
|
17786
|
+
const killTimer = setTimeout(() => {
|
|
17787
|
+
if (child.exitCode !== null) return;
|
|
17788
|
+
console.warn(
|
|
17789
|
+
"[codex_app_server] Process still alive after grace SIGTERM; escalating to SIGKILL"
|
|
17790
|
+
);
|
|
17791
|
+
killProcessTree(child.pid, { signal: "SIGKILL", child });
|
|
17792
|
+
}, CODEX_EXIT_GRACE_SIGKILL_MS);
|
|
17793
|
+
killTimer.unref?.();
|
|
17794
|
+
}, graceMs);
|
|
17705
17795
|
timer.unref?.();
|
|
17706
17796
|
state.exitGraceKillTimer = timer;
|
|
17707
17797
|
}
|
|
@@ -17710,6 +17800,167 @@ function disarmCodexExitGraceKill(state) {
|
|
|
17710
17800
|
clearTimeout(state.exitGraceKillTimer);
|
|
17711
17801
|
state.exitGraceKillTimer = void 0;
|
|
17712
17802
|
}
|
|
17803
|
+
function completeCodexTurnRespectingWorkers(state) {
|
|
17804
|
+
disarmCodexExitGraceKill(state);
|
|
17805
|
+
const activeWorkers = state.activeBackgroundTaskIds?.size ?? 0;
|
|
17806
|
+
if (activeWorkers > 0) {
|
|
17807
|
+
state.resultDeferredOnBackgroundWork = true;
|
|
17808
|
+
state.codexAwaitingWorkersAfterTurnEnd = true;
|
|
17809
|
+
console.info(
|
|
17810
|
+
`[codex_app_server] Turn completed with ${activeWorkers} background worker id(s) still active \u2014 deferring exit grace`
|
|
17811
|
+
);
|
|
17812
|
+
armCodexExitGraceKill(state, CODEX_BACKGROUND_EXIT_GRACE_MS);
|
|
17813
|
+
return;
|
|
17814
|
+
}
|
|
17815
|
+
armCodexExitGraceKill(state);
|
|
17816
|
+
}
|
|
17817
|
+
function touchCodexBackgroundGrace(state) {
|
|
17818
|
+
if (!state.codexAwaitingWorkersAfterTurnEnd) return;
|
|
17819
|
+
disarmCodexExitGraceKill(state);
|
|
17820
|
+
armCodexExitGraceKill(state, CODEX_BACKGROUND_EXIT_GRACE_MS);
|
|
17821
|
+
}
|
|
17822
|
+
function maybeReleaseCodexDeferredTurn(state) {
|
|
17823
|
+
if (!state.codexAwaitingWorkersAfterTurnEnd) return;
|
|
17824
|
+
if (state.activeBackgroundTaskIds?.size) return;
|
|
17825
|
+
state.codexAwaitingWorkersAfterTurnEnd = false;
|
|
17826
|
+
state.resultDeferredOnBackgroundWork = false;
|
|
17827
|
+
disarmCodexExitGraceKill(state);
|
|
17828
|
+
armCodexExitGraceKill(state);
|
|
17829
|
+
console.info(
|
|
17830
|
+
"[codex_app_server] All background workers finished after turn completion \u2014 arming normal exit grace"
|
|
17831
|
+
);
|
|
17832
|
+
}
|
|
17833
|
+
var TERMINAL_COLLAB_AGENT_STATUSES = /* @__PURE__ */ new Set([
|
|
17834
|
+
"completed",
|
|
17835
|
+
"errored",
|
|
17836
|
+
"shutdown",
|
|
17837
|
+
"interrupted",
|
|
17838
|
+
"not_found"
|
|
17839
|
+
]);
|
|
17840
|
+
var FAILED_COLLAB_AGENT_STATUSES = /* @__PURE__ */ new Set(["errored", "interrupted", "not_found"]);
|
|
17841
|
+
var COLLAB_COORDINATION_TOOL_NAMES = {
|
|
17842
|
+
send_input: "collab_send_input",
|
|
17843
|
+
wait: "collab_wait",
|
|
17844
|
+
close_agent: "collab_close_agent"
|
|
17845
|
+
};
|
|
17846
|
+
function firstLine(text, maxLength = 140) {
|
|
17847
|
+
const line = text.split("\n").find((candidate) => candidate.trim().length > 0)?.trim() ?? "";
|
|
17848
|
+
return line.length > maxLength ? `${line.slice(0, maxLength - 1)}\u2026` : line;
|
|
17849
|
+
}
|
|
17850
|
+
function finishCodexWorker(context, state, options) {
|
|
17851
|
+
const launcherId = options.launcherId ?? (options.threadId ? state.backgroundToolIdByTaskId?.get(options.threadId) : void 0) ?? (options.agentPath ? state.codexWorkerToolIdByPath?.get(options.agentPath) : void 0);
|
|
17852
|
+
if (!launcherId) {
|
|
17853
|
+
if (options.threadId) clearBackgroundTaskIds(state, [options.threadId]);
|
|
17854
|
+
maybeReleaseCodexDeferredTurn(state);
|
|
17855
|
+
return;
|
|
17856
|
+
}
|
|
17857
|
+
const launcherStillActive = state.activeBackgroundTaskIds?.has(launcherId) === true || (state.backgroundTaskIdsByToolId?.get(launcherId)?.size ?? 0) > 0;
|
|
17858
|
+
if (!launcherStillActive) {
|
|
17859
|
+
if (options.threadId) clearBackgroundTaskIds(state, [options.threadId]);
|
|
17860
|
+
maybeReleaseCodexDeferredTurn(state);
|
|
17861
|
+
return;
|
|
17862
|
+
}
|
|
17863
|
+
clearBackgroundTaskIds(state, [options.threadId ?? launcherId]);
|
|
17864
|
+
const remainingSiblings = state.backgroundTaskIdsByToolId?.get(launcherId)?.size ?? 0;
|
|
17865
|
+
if (remainingSiblings === 0) {
|
|
17866
|
+
clearBackgroundTaskIds(state, [launcherId]);
|
|
17867
|
+
const isError = FAILED_COLLAB_AGENT_STATUSES.has(options.status);
|
|
17868
|
+
const resultText = options.message?.trim() || `Background worker ${options.status}.`;
|
|
17869
|
+
void context.presenter.onToolResult?.(launcherId, resultText, isError);
|
|
17870
|
+
}
|
|
17871
|
+
maybeReleaseCodexDeferredTurn(state);
|
|
17872
|
+
}
|
|
17873
|
+
function applyCodexCollabAgentStates(context, state, agentsStates) {
|
|
17874
|
+
for (const [threadId, raw] of Object.entries(agentsStates)) {
|
|
17875
|
+
if (!raw || typeof raw !== "object") continue;
|
|
17876
|
+
const entry = raw;
|
|
17877
|
+
const status = typeof entry.status === "string" ? entry.status : "";
|
|
17878
|
+
if (!TERMINAL_COLLAB_AGENT_STATUSES.has(status)) continue;
|
|
17879
|
+
finishCodexWorker(context, state, {
|
|
17880
|
+
threadId,
|
|
17881
|
+
status,
|
|
17882
|
+
message: typeof entry.message === "string" ? entry.message : void 0
|
|
17883
|
+
});
|
|
17884
|
+
}
|
|
17885
|
+
}
|
|
17886
|
+
function summarizeCodexAgentStates(agentsStates) {
|
|
17887
|
+
return Object.entries(agentsStates).map(([threadId, raw]) => {
|
|
17888
|
+
const entry = raw && typeof raw === "object" ? raw : {};
|
|
17889
|
+
const status = typeof entry.status === "string" ? entry.status : "unknown";
|
|
17890
|
+
const message = typeof entry.message === "string" && entry.message.trim() ? ` \u2014 ${firstLine(entry.message)}` : "";
|
|
17891
|
+
return `${threadId}: ${status}${message}`;
|
|
17892
|
+
}).join("\n");
|
|
17893
|
+
}
|
|
17894
|
+
function handleCodexCollabToolCallItem(context, state, item, phase) {
|
|
17895
|
+
const itemId = typeof item.id === "string" ? item.id : "";
|
|
17896
|
+
const tool = typeof item.tool === "string" ? item.tool : "";
|
|
17897
|
+
if (!itemId || !tool) return true;
|
|
17898
|
+
const senderThreadId = typeof item.sender_thread_id === "string" ? item.sender_thread_id : "";
|
|
17899
|
+
const receiverThreadIds = Array.isArray(item.receiver_thread_ids) ? item.receiver_thread_ids.filter(
|
|
17900
|
+
(value2) => typeof value2 === "string" && value2.length > 0
|
|
17901
|
+
) : [];
|
|
17902
|
+
const prompt = typeof item.prompt === "string" ? item.prompt : "";
|
|
17903
|
+
const agentsStates = typeof item.agents_states === "object" && item.agents_states !== null ? item.agents_states : {};
|
|
17904
|
+
const itemStatus = typeof item.status === "string" ? item.status : "";
|
|
17905
|
+
if (tool === "spawn_agent") {
|
|
17906
|
+
if (trackCodexStreamedToolId(state, itemId)) {
|
|
17907
|
+
const parentToolUseId = senderThreadId ? state.backgroundToolIdByTaskId?.get(senderThreadId) ?? null : null;
|
|
17908
|
+
void context.presenter.onToolUse(
|
|
17909
|
+
"Agent",
|
|
17910
|
+
{
|
|
17911
|
+
subagent_type: "codex-worker",
|
|
17912
|
+
description: firstLine(prompt) || "Codex background worker",
|
|
17913
|
+
...prompt ? { prompt } : {},
|
|
17914
|
+
...receiverThreadIds.length > 0 ? { agent_thread_ids: receiverThreadIds } : {}
|
|
17915
|
+
},
|
|
17916
|
+
itemId,
|
|
17917
|
+
parentToolUseId
|
|
17918
|
+
);
|
|
17919
|
+
registerBackgroundLauncher(state, itemId);
|
|
17920
|
+
}
|
|
17921
|
+
for (const threadId of receiverThreadIds) {
|
|
17922
|
+
if (state.backgroundToolIdByTaskId?.get(threadId)) continue;
|
|
17923
|
+
registerBackgroundTaskRecord(state, { task_id: threadId, tool_use_id: itemId });
|
|
17924
|
+
}
|
|
17925
|
+
applyCodexCollabAgentStates(context, state, agentsStates);
|
|
17926
|
+
if (phase === "completed" && itemStatus === "failed") {
|
|
17927
|
+
finishCodexWorker(context, state, {
|
|
17928
|
+
launcherId: itemId,
|
|
17929
|
+
status: "errored",
|
|
17930
|
+
message: "Failed to spawn background worker."
|
|
17931
|
+
});
|
|
17932
|
+
}
|
|
17933
|
+
return true;
|
|
17934
|
+
}
|
|
17935
|
+
const coordinationToolName = COLLAB_COORDINATION_TOOL_NAMES[tool];
|
|
17936
|
+
if (!coordinationToolName) return false;
|
|
17937
|
+
if (trackCodexStreamedToolId(state, itemId)) {
|
|
17938
|
+
void context.presenter.onToolUse(
|
|
17939
|
+
coordinationToolName,
|
|
17940
|
+
{
|
|
17941
|
+
...receiverThreadIds.length > 0 ? { receiver_thread_ids: receiverThreadIds } : {},
|
|
17942
|
+
...prompt ? { prompt } : {}
|
|
17943
|
+
},
|
|
17944
|
+
itemId
|
|
17945
|
+
);
|
|
17946
|
+
}
|
|
17947
|
+
if (phase === "completed") {
|
|
17948
|
+
const isError = itemStatus === "failed";
|
|
17949
|
+
const summary = summarizeCodexAgentStates(agentsStates);
|
|
17950
|
+
void context.presenter.onToolResult?.(
|
|
17951
|
+
itemId,
|
|
17952
|
+
summary || (isError ? "Failed." : "Done."),
|
|
17953
|
+
isError
|
|
17954
|
+
);
|
|
17955
|
+
}
|
|
17956
|
+
applyCodexCollabAgentStates(context, state, agentsStates);
|
|
17957
|
+
if (tool === "close_agent" && phase === "completed" && itemStatus !== "failed") {
|
|
17958
|
+
for (const threadId of receiverThreadIds) {
|
|
17959
|
+
finishCodexWorker(context, state, { threadId, status: "shutdown" });
|
|
17960
|
+
}
|
|
17961
|
+
}
|
|
17962
|
+
return true;
|
|
17963
|
+
}
|
|
17713
17964
|
function trackCodexStreamedToolId(state, toolId) {
|
|
17714
17965
|
if (!state.codexStreamedToolIds) state.codexStreamedToolIds = /* @__PURE__ */ new Set();
|
|
17715
17966
|
if (state.codexStreamedToolIds.has(toolId)) return false;
|
|
@@ -17740,6 +17991,7 @@ function handleCodexStructuredEvent(parsed, context, state) {
|
|
|
17740
17991
|
const presenter = context.presenter;
|
|
17741
17992
|
const type = typeof parsed.type === "string" ? parsed.type : "";
|
|
17742
17993
|
if (!type) return false;
|
|
17994
|
+
touchCodexBackgroundGrace(state);
|
|
17743
17995
|
if (typeof parsed.thread_id === "string") {
|
|
17744
17996
|
state.runtimeSessionId = parsed.thread_id;
|
|
17745
17997
|
}
|
|
@@ -17764,7 +18016,12 @@ function handleCodexStructuredEvent(parsed, context, state) {
|
|
|
17764
18016
|
return true;
|
|
17765
18017
|
case "turn.started":
|
|
17766
18018
|
disarmCodexExitGraceKill(state);
|
|
17767
|
-
state.
|
|
18019
|
+
state.codexAwaitingWorkersAfterTurnEnd = false;
|
|
18020
|
+
state.resultDeferredOnBackgroundWork = false;
|
|
18021
|
+
if (state.codexIterationEventFamily !== "legacy") {
|
|
18022
|
+
state.codexIterationEventFamily = "modern";
|
|
18023
|
+
state.iterations += 1;
|
|
18024
|
+
}
|
|
17768
18025
|
return true;
|
|
17769
18026
|
case "session_configured":
|
|
17770
18027
|
if (typeof parsed.session_id === "string") {
|
|
@@ -17773,7 +18030,12 @@ function handleCodexStructuredEvent(parsed, context, state) {
|
|
|
17773
18030
|
return true;
|
|
17774
18031
|
case "task_started":
|
|
17775
18032
|
disarmCodexExitGraceKill(state);
|
|
17776
|
-
state.
|
|
18033
|
+
state.codexAwaitingWorkersAfterTurnEnd = false;
|
|
18034
|
+
state.resultDeferredOnBackgroundWork = false;
|
|
18035
|
+
if (state.codexIterationEventFamily !== "modern") {
|
|
18036
|
+
state.codexIterationEventFamily = "legacy";
|
|
18037
|
+
state.iterations += 1;
|
|
18038
|
+
}
|
|
17777
18039
|
return true;
|
|
17778
18040
|
case "item.started": {
|
|
17779
18041
|
const item = typeof parsed.item === "object" && parsed.item !== null ? parsed.item : null;
|
|
@@ -17800,6 +18062,8 @@ function handleCodexStructuredEvent(parsed, context, state) {
|
|
|
17800
18062
|
}
|
|
17801
18063
|
return true;
|
|
17802
18064
|
}
|
|
18065
|
+
case "collab_tool_call":
|
|
18066
|
+
return handleCodexCollabToolCallItem(context, state, item, "started");
|
|
17803
18067
|
// Text-bearing and patch/todo items carry no useful live-start payload; they
|
|
17804
18068
|
// are surfaced on item.completed. Recognized (do not count as unhandled).
|
|
17805
18069
|
case "reasoning":
|
|
@@ -17811,6 +18075,21 @@ function handleCodexStructuredEvent(parsed, context, state) {
|
|
|
17811
18075
|
return false;
|
|
17812
18076
|
}
|
|
17813
18077
|
}
|
|
18078
|
+
case "item.updated": {
|
|
18079
|
+
const item = typeof parsed.item === "object" && parsed.item !== null ? parsed.item : null;
|
|
18080
|
+
if (!item || typeof item.type !== "string") return true;
|
|
18081
|
+
switch (item.type) {
|
|
18082
|
+
case "collab_tool_call":
|
|
18083
|
+
return handleCodexCollabToolCallItem(context, state, item, "updated");
|
|
18084
|
+
case "todo_list": {
|
|
18085
|
+
const todos = Array.isArray(item.items) ? item.items : [];
|
|
18086
|
+
void presenter.onTodoWrite?.(todos);
|
|
18087
|
+
return true;
|
|
18088
|
+
}
|
|
18089
|
+
default:
|
|
18090
|
+
return false;
|
|
18091
|
+
}
|
|
18092
|
+
}
|
|
17814
18093
|
case "item.completed": {
|
|
17815
18094
|
const item = typeof parsed.item === "object" && parsed.item !== null ? parsed.item : null;
|
|
17816
18095
|
if (!item || typeof item.type !== "string") return true;
|
|
@@ -17875,6 +18154,8 @@ function handleCodexStructuredEvent(parsed, context, state) {
|
|
|
17875
18154
|
void presenter.onTodoWrite?.(todos);
|
|
17876
18155
|
return true;
|
|
17877
18156
|
}
|
|
18157
|
+
case "collab_tool_call":
|
|
18158
|
+
return handleCodexCollabToolCallItem(context, state, item, "completed");
|
|
17878
18159
|
default:
|
|
17879
18160
|
return false;
|
|
17880
18161
|
}
|
|
@@ -17896,7 +18177,9 @@ function handleCodexStructuredEvent(parsed, context, state) {
|
|
|
17896
18177
|
return true;
|
|
17897
18178
|
}
|
|
17898
18179
|
case "exec_command_begin": {
|
|
17899
|
-
const
|
|
18180
|
+
const hasCallId = typeof parsed.call_id === "string" && parsed.call_id.length > 0;
|
|
18181
|
+
const toolId = hasCallId ? parsed.call_id : `exec-${Date.now()}`;
|
|
18182
|
+
if (!hasCallId) state.lastAnonymousExecId = toolId;
|
|
17900
18183
|
const command = Array.isArray(parsed.command) ? parsed.command.join(" ") : "";
|
|
17901
18184
|
const cwd = typeof parsed.cwd === "string" ? parsed.cwd : context.cwd;
|
|
17902
18185
|
trackCodexStreamedToolId(state, toolId);
|
|
@@ -17904,7 +18187,10 @@ function handleCodexStructuredEvent(parsed, context, state) {
|
|
|
17904
18187
|
return true;
|
|
17905
18188
|
}
|
|
17906
18189
|
case "exec_command_end": {
|
|
17907
|
-
const
|
|
18190
|
+
const hasCallId = typeof parsed.call_id === "string" && parsed.call_id.length > 0;
|
|
18191
|
+
const toolId = hasCallId ? parsed.call_id : state.lastAnonymousExecId;
|
|
18192
|
+
if (!hasCallId) state.lastAnonymousExecId = void 0;
|
|
18193
|
+
if (!toolId) return true;
|
|
17908
18194
|
const output = typeof parsed.formatted_output === "string" ? parsed.formatted_output : typeof parsed.aggregated_output === "string" ? parsed.aggregated_output : "";
|
|
17909
18195
|
void presenter.onToolResult?.(toolId, output);
|
|
17910
18196
|
return true;
|
|
@@ -17912,9 +18198,8 @@ function handleCodexStructuredEvent(parsed, context, state) {
|
|
|
17912
18198
|
case "mcp_tool_call_begin": {
|
|
17913
18199
|
const toolId = typeof parsed.call_id === "string" ? parsed.call_id : `mcp-${Date.now()}`;
|
|
17914
18200
|
const invocation = typeof parsed.invocation === "object" && parsed.invocation !== null ? parsed.invocation : {};
|
|
17915
|
-
const tool = typeof invocation.tool_name === "string" ? invocation.tool_name : typeof invocation.tool === "string" ? invocation.tool : "MCP Tool";
|
|
17916
18201
|
trackCodexStreamedToolId(state, toolId);
|
|
17917
|
-
void presenter.onToolUse(
|
|
18202
|
+
void presenter.onToolUse(buildCodexMcpToolName(invocation), invocation, toolId);
|
|
17918
18203
|
return true;
|
|
17919
18204
|
}
|
|
17920
18205
|
case "mcp_tool_call_end": {
|
|
@@ -17952,7 +18237,7 @@ function handleCodexStructuredEvent(parsed, context, state) {
|
|
|
17952
18237
|
const lastMessage = typeof parsed.last_agent_message === "string" ? parsed.last_agent_message : "";
|
|
17953
18238
|
if (lastMessage.length > 0) state.summary = lastMessage;
|
|
17954
18239
|
state.error = void 0;
|
|
17955
|
-
|
|
18240
|
+
completeCodexTurnRespectingWorkers(state);
|
|
17956
18241
|
return true;
|
|
17957
18242
|
}
|
|
17958
18243
|
case "turn.completed": {
|
|
@@ -17975,7 +18260,108 @@ function handleCodexStructuredEvent(parsed, context, state) {
|
|
|
17975
18260
|
cacheCreationTokens: state.usage.cacheCreationTokens
|
|
17976
18261
|
});
|
|
17977
18262
|
state.error = void 0;
|
|
17978
|
-
|
|
18263
|
+
completeCodexTurnRespectingWorkers(state);
|
|
18264
|
+
return true;
|
|
18265
|
+
}
|
|
18266
|
+
case "sub_agent_activity": {
|
|
18267
|
+
const kind = typeof parsed.kind === "string" ? parsed.kind : "";
|
|
18268
|
+
const agentThreadId = typeof parsed.agent_thread_id === "string" ? parsed.agent_thread_id : "";
|
|
18269
|
+
const agentPath = typeof parsed.agent_path === "string" ? parsed.agent_path : "";
|
|
18270
|
+
const eventId = typeof parsed.event_id === "string" ? parsed.event_id : "";
|
|
18271
|
+
const isRootPath = !agentPath || agentPath === "/root" || agentPath === "/";
|
|
18272
|
+
switch (kind) {
|
|
18273
|
+
case "started": {
|
|
18274
|
+
if (isRootPath) return true;
|
|
18275
|
+
if (agentThreadId && state.backgroundToolIdByTaskId?.has(agentThreadId)) return true;
|
|
18276
|
+
const toolId = eventId || agentThreadId;
|
|
18277
|
+
if (!toolId) return true;
|
|
18278
|
+
if (trackCodexStreamedToolId(state, toolId)) {
|
|
18279
|
+
const workerName = agentPath.slice(agentPath.lastIndexOf("/") + 1) || "codex-worker";
|
|
18280
|
+
const parentPath = agentPath.slice(0, agentPath.lastIndexOf("/"));
|
|
18281
|
+
const parentToolUseId = parentPath && parentPath !== "/root" ? state.codexWorkerToolIdByPath?.get(parentPath) ?? null : null;
|
|
18282
|
+
void presenter.onToolUse(
|
|
18283
|
+
"Agent",
|
|
18284
|
+
{
|
|
18285
|
+
subagent_type: workerName,
|
|
18286
|
+
description: agentPath,
|
|
18287
|
+
...agentThreadId ? { agent_thread_id: agentThreadId } : {}
|
|
18288
|
+
},
|
|
18289
|
+
toolId,
|
|
18290
|
+
parentToolUseId
|
|
18291
|
+
);
|
|
18292
|
+
registerBackgroundLauncher(state, toolId);
|
|
18293
|
+
if (agentThreadId) {
|
|
18294
|
+
registerBackgroundTaskRecord(state, { task_id: agentThreadId, tool_use_id: toolId });
|
|
18295
|
+
}
|
|
18296
|
+
if (!state.codexWorkerToolIdByPath) state.codexWorkerToolIdByPath = /* @__PURE__ */ new Map();
|
|
18297
|
+
state.codexWorkerToolIdByPath.set(agentPath, toolId);
|
|
18298
|
+
}
|
|
18299
|
+
return true;
|
|
18300
|
+
}
|
|
18301
|
+
case "interacted":
|
|
18302
|
+
return true;
|
|
18303
|
+
case "interrupted": {
|
|
18304
|
+
if (isRootPath) return true;
|
|
18305
|
+
finishCodexWorker(context, state, {
|
|
18306
|
+
threadId: agentThreadId || void 0,
|
|
18307
|
+
agentPath: agentPath || void 0,
|
|
18308
|
+
status: "interrupted",
|
|
18309
|
+
message: "Background worker interrupted."
|
|
18310
|
+
});
|
|
18311
|
+
return true;
|
|
18312
|
+
}
|
|
18313
|
+
default:
|
|
18314
|
+
return false;
|
|
18315
|
+
}
|
|
18316
|
+
}
|
|
18317
|
+
// App-server EventMsg collab lifecycle notices. Begin/interaction/resume events
|
|
18318
|
+
// carry no state Alan tracks beyond liveness (touched above); spawn_end and the
|
|
18319
|
+
// waiting/close terminals map into the shared worker registry below.
|
|
18320
|
+
case "collab_agent_spawn_begin":
|
|
18321
|
+
case "collab_agent_interaction_begin":
|
|
18322
|
+
case "collab_agent_interaction_end":
|
|
18323
|
+
case "collab_waiting_begin":
|
|
18324
|
+
case "collab_resume_begin":
|
|
18325
|
+
case "collab_resume_end":
|
|
18326
|
+
case "collab_close_begin":
|
|
18327
|
+
return true;
|
|
18328
|
+
case "collab_agent_spawn_end": {
|
|
18329
|
+
const newThreadId = typeof parsed.new_thread_id === "string" ? parsed.new_thread_id : "";
|
|
18330
|
+
if (!newThreadId || state.backgroundToolIdByTaskId?.has(newThreadId)) return true;
|
|
18331
|
+
const callId = typeof parsed.call_id === "string" && parsed.call_id ? parsed.call_id : typeof parsed.event_id === "string" ? parsed.event_id : "";
|
|
18332
|
+
const toolId = callId || `collab-spawn-${newThreadId}`;
|
|
18333
|
+
if (trackCodexStreamedToolId(state, toolId)) {
|
|
18334
|
+
const nickname = typeof parsed.new_agent_nickname === "string" ? parsed.new_agent_nickname : "";
|
|
18335
|
+
const role = typeof parsed.new_agent_role === "string" ? parsed.new_agent_role : "";
|
|
18336
|
+
void presenter.onToolUse(
|
|
18337
|
+
"Agent",
|
|
18338
|
+
{
|
|
18339
|
+
subagent_type: nickname || role || "codex-worker",
|
|
18340
|
+
description: [nickname, role].filter(Boolean).join(" \u2014 ") || "Codex background worker",
|
|
18341
|
+
agent_thread_id: newThreadId
|
|
18342
|
+
},
|
|
18343
|
+
toolId
|
|
18344
|
+
);
|
|
18345
|
+
registerBackgroundLauncher(state, toolId);
|
|
18346
|
+
}
|
|
18347
|
+
registerBackgroundTaskRecord(state, { task_id: newThreadId, tool_use_id: toolId });
|
|
18348
|
+
return true;
|
|
18349
|
+
}
|
|
18350
|
+
case "collab_waiting_end": {
|
|
18351
|
+
const entries = Array.isArray(parsed.agent_statuses) ? parsed.agent_statuses : [];
|
|
18352
|
+
for (const raw of entries) {
|
|
18353
|
+
if (!raw || typeof raw !== "object") continue;
|
|
18354
|
+
const entry = raw;
|
|
18355
|
+
const threadId = typeof entry.agent_thread_id === "string" ? entry.agent_thread_id : typeof entry.thread_id === "string" ? entry.thread_id : "";
|
|
18356
|
+
const status = typeof entry.status === "string" ? entry.status.toLowerCase() : "";
|
|
18357
|
+
if (!threadId || !TERMINAL_COLLAB_AGENT_STATUSES.has(status)) continue;
|
|
18358
|
+
finishCodexWorker(context, state, { threadId, status });
|
|
18359
|
+
}
|
|
18360
|
+
return true;
|
|
18361
|
+
}
|
|
18362
|
+
case "collab_close_end": {
|
|
18363
|
+
const threadId = typeof parsed.receiver_thread_id === "string" ? parsed.receiver_thread_id : "";
|
|
18364
|
+
if (threadId) finishCodexWorker(context, state, { threadId, status: "shutdown" });
|
|
17979
18365
|
return true;
|
|
17980
18366
|
}
|
|
17981
18367
|
case "error": {
|
|
@@ -18094,10 +18480,11 @@ function createCodexRuntimeBackend(command = "codex", defaultArgs = []) {
|
|
|
18094
18480
|
},
|
|
18095
18481
|
promptViaStdin: true,
|
|
18096
18482
|
augmentPrompt: (ctx) => {
|
|
18483
|
+
const isResume = Boolean(resumableSessionId);
|
|
18097
18484
|
const promptWithContext = resumeContextPrefix ? `${resumeContextPrefix}
|
|
18098
18485
|
|
|
18099
18486
|
${ctx.promptText}` : ctx.promptText;
|
|
18100
|
-
const basePrompt =
|
|
18487
|
+
const basePrompt = buildResumeAwarePrompt(ctx.config, promptWithContext, { isResume });
|
|
18101
18488
|
return ctx.config.mode === "plan" ? buildPlanModePrefix(basePrompt) : basePrompt;
|
|
18102
18489
|
},
|
|
18103
18490
|
parseStructuredLine: parseCodexStructuredLine,
|
|
@@ -18303,10 +18690,21 @@ function handleCursorStructuredEvent(parsed, context, state) {
|
|
|
18303
18690
|
}
|
|
18304
18691
|
case "tool_call": {
|
|
18305
18692
|
const subtype = typeof parsed.subtype === "string" ? parsed.subtype : "";
|
|
18306
|
-
const
|
|
18693
|
+
const hasCallId = typeof parsed.call_id === "string" && parsed.call_id.length > 0;
|
|
18307
18694
|
const toolCall = typeof parsed.tool_call === "object" && parsed.tool_call !== null ? parsed.tool_call : null;
|
|
18308
18695
|
const entry = toolCall ? extractCursorToolEntry(toolCall) : null;
|
|
18309
18696
|
if (!entry) return true;
|
|
18697
|
+
let toolId;
|
|
18698
|
+
if (hasCallId) {
|
|
18699
|
+
toolId = parsed.call_id;
|
|
18700
|
+
} else if (subtype === "started") {
|
|
18701
|
+
state.cursorAnonymousToolCounter = (state.cursorAnonymousToolCounter ?? 0) + 1;
|
|
18702
|
+
toolId = `cursor-tool-${state.cursorAnonymousToolCounter}`;
|
|
18703
|
+
state.lastAnonymousCursorToolId = toolId;
|
|
18704
|
+
} else {
|
|
18705
|
+
toolId = state.lastAnonymousCursorToolId;
|
|
18706
|
+
}
|
|
18707
|
+
if (!toolId) return true;
|
|
18310
18708
|
state.iterations = Math.max(state.iterations, 1);
|
|
18311
18709
|
const toolName = formatCursorToolName(entry.rawName, entry.payload);
|
|
18312
18710
|
const toolArgs = typeof entry.payload.args === "object" && entry.payload.args !== null ? entry.payload.args : typeof entry.payload.arguments === "object" && entry.payload.arguments !== null ? entry.payload.arguments : entry.payload;
|
|
@@ -18315,6 +18713,7 @@ function handleCursorStructuredEvent(parsed, context, state) {
|
|
|
18315
18713
|
return true;
|
|
18316
18714
|
}
|
|
18317
18715
|
if (subtype === "completed") {
|
|
18716
|
+
if (!hasCallId) state.lastAnonymousCursorToolId = void 0;
|
|
18318
18717
|
void presenter.onToolResult?.(toolId, buildCursorToolResultText(entry.payload.result));
|
|
18319
18718
|
}
|
|
18320
18719
|
return true;
|
|
@@ -18393,7 +18792,10 @@ function buildCursorAgentModelArg(modelId, options) {
|
|
|
18393
18792
|
const wireEffort = resolveWireEffort({
|
|
18394
18793
|
harness: "cursor_agent_cli",
|
|
18395
18794
|
modelId: baseId,
|
|
18396
|
-
|
|
18795
|
+
// Fall back to an effort baked into the model id (e.g. `gpt-5.5-high` from a
|
|
18796
|
+
// session import or raw discovered id) when no explicit selection overrides it,
|
|
18797
|
+
// so the SKU is preserved instead of silently downgraded to default effort.
|
|
18798
|
+
selectedEffortLevel: options?.selectedEffortLevel ?? parsed.effort,
|
|
18397
18799
|
effortLevels
|
|
18398
18800
|
});
|
|
18399
18801
|
let id = baseId;
|
|
@@ -18407,7 +18809,8 @@ function buildCursorAgentModelArg(modelId, options) {
|
|
|
18407
18809
|
}
|
|
18408
18810
|
}
|
|
18409
18811
|
const contextWindow = asContextWindow(options?.selectedContextWindow);
|
|
18410
|
-
|
|
18812
|
+
const modelSupports1m = (options?.contextWindows ?? []).includes("1m");
|
|
18813
|
+
if (contextWindow === "1m" && id === baseId && modelSupports1m) {
|
|
18411
18814
|
return `${baseId}[context=1m]`;
|
|
18412
18815
|
}
|
|
18413
18816
|
if (parsed.fast) return `${id}-fast`;
|
|
@@ -18497,7 +18900,8 @@ function createCursorAgentCliBackend(command = "cursor-agent", defaultArgs = [])
|
|
|
18497
18900
|
buildCursorAgentModelArg(model, {
|
|
18498
18901
|
selectedEffortLevel: ctx.config.selectedEffortLevel,
|
|
18499
18902
|
selectedContextWindow: ctx.config.selectedContextWindow,
|
|
18500
|
-
effortLevels: modelDef?.capabilities?.effortLevels
|
|
18903
|
+
effortLevels: modelDef?.capabilities?.effortLevels,
|
|
18904
|
+
contextWindows: modelDef?.capabilities?.contextWindows
|
|
18501
18905
|
})
|
|
18502
18906
|
);
|
|
18503
18907
|
}
|
|
@@ -18515,11 +18919,20 @@ function createCursorAgentCliBackend(command = "cursor-agent", defaultArgs = [])
|
|
|
18515
18919
|
// with zero output; without this watchdog only the 30-min server reaper
|
|
18516
18920
|
// would end the run.
|
|
18517
18921
|
startupTimeoutMs: 18e4,
|
|
18922
|
+
// Post-startup hang bound: cursor's asks use the non-blocking MCP model and it
|
|
18923
|
+
// never populates the idle timer's ask/background fire conditions, so a cursor
|
|
18924
|
+
// process that produced output then went silent (no result event) is otherwise
|
|
18925
|
+
// bounded only by the ~30-min external reaper. Poll every 5 min; kill after 15
|
|
18926
|
+
// min of continuous silence.
|
|
18927
|
+
resultIdleTimeoutMs: 5 * 6e4,
|
|
18928
|
+
postStartupSilenceTimeoutMs: 15 * 6e4,
|
|
18518
18929
|
augmentPrompt: (ctx) => {
|
|
18519
|
-
const
|
|
18520
|
-
|
|
18521
|
-
|
|
18522
|
-
|
|
18930
|
+
const isResume = Boolean(
|
|
18931
|
+
ctx.config.runtimeSessionId?.trim() || ctx.config.providerSessionId?.trim()
|
|
18932
|
+
);
|
|
18933
|
+
const base = buildResumeAwarePrompt(ctx.config, ctx.promptText, { isResume });
|
|
18934
|
+
const prompt = ctx.config.mode === "plan" ? buildPlanModePrefix(base) : base;
|
|
18935
|
+
return appendImagePathReferences(prompt, imageFiles);
|
|
18523
18936
|
},
|
|
18524
18937
|
parseStructuredLine: parseCursorStructuredLine
|
|
18525
18938
|
}).run(context);
|
|
@@ -18746,7 +19159,7 @@ function createGrokAcpDriver(imageFiles) {
|
|
|
18746
19159
|
}
|
|
18747
19160
|
newSession(context);
|
|
18748
19161
|
}
|
|
18749
|
-
function
|
|
19162
|
+
function sendPrompt2(context, state) {
|
|
18750
19163
|
const text = buildPromptWithImagePathReferences(context, imageFiles);
|
|
18751
19164
|
isPromptActive = true;
|
|
18752
19165
|
promptId = request("session/prompt", {
|
|
@@ -18789,7 +19202,7 @@ function createGrokAcpDriver(imageFiles) {
|
|
|
18789
19202
|
const sessionId = asString(result?.sessionId);
|
|
18790
19203
|
if (sessionId) state.runtimeSessionId = sessionId;
|
|
18791
19204
|
state.iterations = Math.max(state.iterations, 1);
|
|
18792
|
-
|
|
19205
|
+
sendPrompt2(context, state);
|
|
18793
19206
|
return;
|
|
18794
19207
|
}
|
|
18795
19208
|
if (message.id === promptId) {
|
|
@@ -19135,23 +19548,59 @@ function createKimiCliBackend(command = "kimi-cli", defaultArgs = []) {
|
|
|
19135
19548
|
}
|
|
19136
19549
|
};
|
|
19137
19550
|
}
|
|
19551
|
+
var AUTONOMOUS_OPENCODE_CONFIG_CONTENT = JSON.stringify({
|
|
19552
|
+
$schema: "https://opencode.ai/config.json",
|
|
19553
|
+
permission: "allow"
|
|
19554
|
+
});
|
|
19555
|
+
function buildAutonomousOpenCodeConfigContent(existing) {
|
|
19556
|
+
if (!existing?.trim()) return AUTONOMOUS_OPENCODE_CONFIG_CONTENT;
|
|
19557
|
+
try {
|
|
19558
|
+
const parsed = JSON.parse(existing);
|
|
19559
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
19560
|
+
return JSON.stringify({
|
|
19561
|
+
...parsed,
|
|
19562
|
+
permission: "allow"
|
|
19563
|
+
});
|
|
19564
|
+
}
|
|
19565
|
+
} catch {
|
|
19566
|
+
}
|
|
19567
|
+
return AUTONOMOUS_OPENCODE_CONFIG_CONTENT;
|
|
19568
|
+
}
|
|
19569
|
+
function withAutonomousOpenCodePermissions(context) {
|
|
19570
|
+
if (shouldUseReadOnlyRuntimePermissions(context.config)) return context;
|
|
19571
|
+
return {
|
|
19572
|
+
...context,
|
|
19573
|
+
env: {
|
|
19574
|
+
...context.env,
|
|
19575
|
+
OPENCODE_CONFIG_CONTENT: buildAutonomousOpenCodeConfigContent(
|
|
19576
|
+
context.env.OPENCODE_CONFIG_CONTENT
|
|
19577
|
+
)
|
|
19578
|
+
}
|
|
19579
|
+
};
|
|
19580
|
+
}
|
|
19138
19581
|
function getRecord(value2) {
|
|
19139
19582
|
return typeof value2 === "object" && value2 !== null ? value2 : null;
|
|
19140
19583
|
}
|
|
19141
19584
|
function getString(value2) {
|
|
19142
19585
|
return typeof value2 === "string" ? value2.trim() : "";
|
|
19143
19586
|
}
|
|
19144
|
-
function getOpenCodeToolError(
|
|
19145
|
-
const state = getRecord(part
|
|
19146
|
-
const error = getRecord(part
|
|
19147
|
-
return getString(part
|
|
19587
|
+
function getOpenCodeToolError(part) {
|
|
19588
|
+
const state = getRecord(part.state);
|
|
19589
|
+
const error = getRecord(part.error) ?? getRecord(state?.error);
|
|
19590
|
+
return getString(part.error) || getString(state?.error) || getString(error?.message);
|
|
19591
|
+
}
|
|
19592
|
+
function isOpenCodeToolError(part) {
|
|
19593
|
+
const state = getRecord(part.state);
|
|
19594
|
+
return part.is_error === true || part.isError === true || state?.status === "error" || Boolean(getOpenCodeToolError(part));
|
|
19595
|
+
}
|
|
19596
|
+
function getOpenCodePartId(part) {
|
|
19597
|
+
return getString(part.id) || getString(part.partID);
|
|
19148
19598
|
}
|
|
19149
|
-
function
|
|
19150
|
-
|
|
19151
|
-
return part?.is_error === true || part?.isError === true || state?.status === "error" || Boolean(getOpenCodeToolError(parsed, part));
|
|
19599
|
+
function getOpenCodeToolId(part) {
|
|
19600
|
+
return getString(part.callID) || getString(part.id) || `opencode-tool-${Date.now()}`;
|
|
19152
19601
|
}
|
|
19153
|
-
function
|
|
19154
|
-
return getString(part
|
|
19602
|
+
function getOpenCodeToolName(part) {
|
|
19603
|
+
return getString(part.tool) || (typeof part.name === "string" ? part.name.trim() : "") || "Tool";
|
|
19155
19604
|
}
|
|
19156
19605
|
function resolveOpenCodePartSnapshotDelta(text, partId, snapshots) {
|
|
19157
19606
|
if (!partId) return text;
|
|
@@ -19168,93 +19617,92 @@ function resolveOpenCodeReasoningDelta(text, partId, state) {
|
|
|
19168
19617
|
state.opencodeReasoningByPartId ??= /* @__PURE__ */ new Map();
|
|
19169
19618
|
return resolveOpenCodePartSnapshotDelta(text, partId, state.opencodeReasoningByPartId);
|
|
19170
19619
|
}
|
|
19171
|
-
|
|
19620
|
+
var TASK_TOOL_NAME = "task";
|
|
19621
|
+
var TASK_RESULT_TAG_PATTERN = /<task_result>\n?([\s\S]*?)\n?<\/task_result>/;
|
|
19622
|
+
var TASK_ERROR_TAG_PATTERN = /<task_error>\n?([\s\S]*?)\n?<\/task_error>/;
|
|
19623
|
+
function unwrapOpenCodeTaskOutput(output) {
|
|
19624
|
+
const result = output.match(TASK_RESULT_TAG_PATTERN);
|
|
19625
|
+
if (result?.[1] !== void 0) return result[1].trim();
|
|
19626
|
+
const error = output.match(TASK_ERROR_TAG_PATTERN);
|
|
19627
|
+
if (error?.[1] !== void 0) return error[1].trim();
|
|
19628
|
+
return output;
|
|
19629
|
+
}
|
|
19630
|
+
function emitOpenCodeToolPart(presenter, state, part, opts) {
|
|
19631
|
+
const stateRecord = getRecord(part.state);
|
|
19632
|
+
const status = getString(stateRecord?.status);
|
|
19633
|
+
if (status === "pending") return;
|
|
19634
|
+
const toolName = getOpenCodeToolName(part);
|
|
19635
|
+
const toolId = getOpenCodeToolId(part);
|
|
19636
|
+
const isTask = toolName.toLowerCase() === TASK_TOOL_NAME;
|
|
19637
|
+
const parentToolUseId = opts.parentToolUseId ?? void 0;
|
|
19638
|
+
state.opencodeToolUseIds ??= /* @__PURE__ */ new Set();
|
|
19639
|
+
if (!state.opencodeToolUseIds.has(toolId)) {
|
|
19640
|
+
state.opencodeToolUseIds.add(toolId);
|
|
19641
|
+
void presenter.onToolUse(toolName, stateRecord?.input ?? part, toolId, parentToolUseId);
|
|
19642
|
+
if (isTask) registerBackgroundLauncher(state, toolId);
|
|
19643
|
+
}
|
|
19644
|
+
if (status !== "completed" && status !== "error") return;
|
|
19645
|
+
const toolError = isOpenCodeToolError(part);
|
|
19646
|
+
const rawOutput = getString(stateRecord?.output) || getString(stateRecord?.error) || JSON.stringify(stateRecord ?? part);
|
|
19647
|
+
const output = isTask ? unwrapOpenCodeTaskOutput(rawOutput) : rawOutput;
|
|
19648
|
+
void presenter.onToolResult?.(toolId, output, toolError, parentToolUseId);
|
|
19649
|
+
if (isTask) clearBackgroundTaskIds(state, [toolId]);
|
|
19650
|
+
if (toolError && !state.error) {
|
|
19651
|
+
const message = getOpenCodeToolError(part) || "OpenCode tool call failed.";
|
|
19652
|
+
state.error = message;
|
|
19653
|
+
void presenter.onError(message);
|
|
19654
|
+
}
|
|
19655
|
+
}
|
|
19656
|
+
function mapOpencodePart(part, context, state, opts = {}) {
|
|
19172
19657
|
const presenter = context.presenter;
|
|
19173
|
-
const type =
|
|
19174
|
-
if (!type) return false;
|
|
19175
|
-
const sessionID = typeof parsed.sessionID === "string" ? parsed.sessionID : void 0;
|
|
19176
|
-
if (sessionID) state.runtimeSessionId = sessionID;
|
|
19177
|
-
const part = typeof parsed.part === "object" && parsed.part !== null ? parsed.part : null;
|
|
19658
|
+
const type = getString(part.type);
|
|
19178
19659
|
switch (type) {
|
|
19179
|
-
case "
|
|
19660
|
+
case "step-start": {
|
|
19180
19661
|
state.iterations += 1;
|
|
19181
19662
|
void presenter.onLog(`[opencode] Step ${state.iterations} started`);
|
|
19182
19663
|
return true;
|
|
19183
19664
|
}
|
|
19184
19665
|
case "text": {
|
|
19185
|
-
const text =
|
|
19666
|
+
const text = typeof part.text === "string" ? part.text : "";
|
|
19186
19667
|
if (text) {
|
|
19187
|
-
const delta = resolveOpenCodeTextDelta(text, getOpenCodePartId(
|
|
19188
|
-
if (
|
|
19189
|
-
|
|
19190
|
-
|
|
19668
|
+
const delta = resolveOpenCodeTextDelta(text, getOpenCodePartId(part), state);
|
|
19669
|
+
if (delta) {
|
|
19670
|
+
state.summary += delta;
|
|
19671
|
+
void presenter.onAssistantText(delta);
|
|
19672
|
+
}
|
|
19191
19673
|
}
|
|
19192
19674
|
return true;
|
|
19193
19675
|
}
|
|
19676
|
+
// Real Part schema only ever uses "reasoning"; "thinking" is a defensive alias in
|
|
19677
|
+
// case a CLI dialect ever names the part itself that way.
|
|
19194
19678
|
case "reasoning":
|
|
19195
19679
|
case "thinking": {
|
|
19196
|
-
const text =
|
|
19680
|
+
const text = typeof part.text === "string" ? part.text : "";
|
|
19197
19681
|
if (!text) return true;
|
|
19198
|
-
const delta = resolveOpenCodeReasoningDelta(text, getOpenCodePartId(
|
|
19682
|
+
const delta = resolveOpenCodeReasoningDelta(text, getOpenCodePartId(part), state);
|
|
19199
19683
|
if (!delta) return true;
|
|
19200
19684
|
state.iterations = Math.max(state.iterations, 1);
|
|
19201
19685
|
void presenter.onThinking(delta);
|
|
19202
19686
|
return true;
|
|
19203
19687
|
}
|
|
19204
|
-
case "tool_use":
|
|
19205
|
-
case "tool.execute": {
|
|
19206
|
-
const toolName = getString(parsed.tool) || (part && typeof part.name === "string" ? part.name : "") || getString(parsed.name) || (part && typeof part.tool === "string" ? part.tool : "") || "Tool";
|
|
19207
|
-
const toolId = part && typeof part.id === "string" ? part.id : `opencode-tool-${Date.now()}`;
|
|
19208
|
-
void presenter.onToolUse(toolName, part ?? {}, toolId);
|
|
19209
|
-
return true;
|
|
19210
|
-
}
|
|
19211
19688
|
case "tool": {
|
|
19212
|
-
|
|
19213
|
-
const toolName = getString(parsed.tool) || (part && typeof part.name === "string" ? part.name : "") || getString(parsed.name) || (part && typeof part.tool === "string" ? part.tool : "") || "Tool";
|
|
19214
|
-
const toolId = getString(parsed.id) || getString(parsed.callID) || `opencode-tool-${Date.now()}`;
|
|
19215
|
-
state.opencodeToolUseIds ??= /* @__PURE__ */ new Set();
|
|
19216
|
-
if (!state.opencodeToolUseIds.has(toolId)) {
|
|
19217
|
-
state.opencodeToolUseIds.add(toolId);
|
|
19218
|
-
void presenter.onToolUse(toolName, stateRecord?.input ?? part ?? parsed, toolId);
|
|
19219
|
-
}
|
|
19220
|
-
const status = getString(stateRecord?.status);
|
|
19221
|
-
if (status === "completed" || status === "error") {
|
|
19222
|
-
const toolError = isOpenCodeToolError(parsed, part);
|
|
19223
|
-
const output = getString(stateRecord?.output) || getString(stateRecord?.error) || JSON.stringify(stateRecord ?? part ?? parsed);
|
|
19224
|
-
void presenter.onToolResult?.(toolId, output, toolError);
|
|
19225
|
-
if (toolError && !state.error) {
|
|
19226
|
-
const message = getOpenCodeToolError(parsed, part) || "OpenCode tool call failed.";
|
|
19227
|
-
state.error = message;
|
|
19228
|
-
void presenter.onError(message);
|
|
19229
|
-
}
|
|
19230
|
-
}
|
|
19689
|
+
emitOpenCodeToolPart(presenter, state, part, opts);
|
|
19231
19690
|
return true;
|
|
19232
19691
|
}
|
|
19233
|
-
case "
|
|
19234
|
-
|
|
19235
|
-
|
|
19236
|
-
|
|
19237
|
-
|
|
19238
|
-
void presenter.onToolResult?.(toolId, output, toolError);
|
|
19239
|
-
if (toolError && !state.error) {
|
|
19240
|
-
const message = getOpenCodeToolError(parsed, part) || "OpenCode tool call failed.";
|
|
19241
|
-
state.error = message;
|
|
19242
|
-
void presenter.onError(message);
|
|
19243
|
-
}
|
|
19244
|
-
return true;
|
|
19245
|
-
}
|
|
19246
|
-
case "step_finish": {
|
|
19247
|
-
if (part && typeof part.tokens === "object" && part.tokens !== null) {
|
|
19248
|
-
const tokens = part.tokens;
|
|
19692
|
+
case "step-finish": {
|
|
19693
|
+
const tokens = getRecord(part.tokens);
|
|
19694
|
+
if (tokens) {
|
|
19695
|
+
const cache2 = getRecord(tokens.cache);
|
|
19696
|
+
const cost = typeof part.cost === "number" ? part.cost : 0;
|
|
19249
19697
|
state.usage = {
|
|
19250
19698
|
model: typeof context.config.selectedModel === "string" && context.config.selectedModel.trim() || "opencode",
|
|
19251
19699
|
numTurns: Math.max(state.iterations, 1),
|
|
19252
19700
|
durationMs: 0,
|
|
19253
|
-
inputTokens: tokens.input
|
|
19254
|
-
outputTokens: tokens.output
|
|
19255
|
-
cacheReadTokens:
|
|
19256
|
-
cacheCreationTokens:
|
|
19257
|
-
costUsd:
|
|
19701
|
+
inputTokens: Number(tokens.input) || 0,
|
|
19702
|
+
outputTokens: Number(tokens.output) || 0,
|
|
19703
|
+
cacheReadTokens: Number(cache2?.read) || 0,
|
|
19704
|
+
cacheCreationTokens: Number(cache2?.write) || 0,
|
|
19705
|
+
costUsd: cost
|
|
19258
19706
|
};
|
|
19259
19707
|
void presenter.onUsageUpdate?.({
|
|
19260
19708
|
model: state.usage.model,
|
|
@@ -19266,6 +19714,57 @@ function handleOpencodeStructuredEvent(parsed, context, state) {
|
|
|
19266
19714
|
}
|
|
19267
19715
|
return true;
|
|
19268
19716
|
}
|
|
19717
|
+
default:
|
|
19718
|
+
return false;
|
|
19719
|
+
}
|
|
19720
|
+
}
|
|
19721
|
+
function normalizeOpenCodePart(parsed, part, partType) {
|
|
19722
|
+
return { ...parsed, ...part ?? {}, type: partType };
|
|
19723
|
+
}
|
|
19724
|
+
function handleOpencodeStructuredEvent(parsed, context, state) {
|
|
19725
|
+
const presenter = context.presenter;
|
|
19726
|
+
const type = typeof parsed.type === "string" ? parsed.type : "";
|
|
19727
|
+
if (!type) return false;
|
|
19728
|
+
const sessionID = typeof parsed.sessionID === "string" ? parsed.sessionID : void 0;
|
|
19729
|
+
if (sessionID) state.runtimeSessionId = sessionID;
|
|
19730
|
+
const part = typeof parsed.part === "object" && parsed.part !== null ? parsed.part : null;
|
|
19731
|
+
switch (type) {
|
|
19732
|
+
case "step_start":
|
|
19733
|
+
return mapOpencodePart(normalizeOpenCodePart(parsed, part, "step-start"), context, state);
|
|
19734
|
+
case "text":
|
|
19735
|
+
return mapOpencodePart(normalizeOpenCodePart(parsed, part, "text"), context, state);
|
|
19736
|
+
case "reasoning":
|
|
19737
|
+
case "thinking":
|
|
19738
|
+
return mapOpencodePart(normalizeOpenCodePart(parsed, part, "reasoning"), context, state);
|
|
19739
|
+
// Real event: `opencode run --format json` emits exactly this shape — a single
|
|
19740
|
+
// terminal event per tool call, carrying part.state.{input,output|error}. See
|
|
19741
|
+
// notes/opencode-cli-reverse-engineering.md for the live-captured schema.
|
|
19742
|
+
case "tool_use":
|
|
19743
|
+
case "tool.execute":
|
|
19744
|
+
case "tool":
|
|
19745
|
+
return mapOpencodePart(normalizeOpenCodePart(parsed, part, "tool"), context, state);
|
|
19746
|
+
case "step_finish":
|
|
19747
|
+
return mapOpencodePart(normalizeOpenCodePart(parsed, part, "step-finish"), context, state);
|
|
19748
|
+
// Defensive fallback only: no observed OpenCode version emits a separate
|
|
19749
|
+
// tool_result/tool.result event in `--format json` mode (tool state always
|
|
19750
|
+
// arrives via `tool_use` above). Kept in case a future/older CLI build splits
|
|
19751
|
+
// the terminal tool event into a distinct result message. Deliberately NOT routed
|
|
19752
|
+
// through mapOpencodePart — this shape has no part.state wrapper at all (output
|
|
19753
|
+
// sits directly on the part), so it isn't a real Part-schema shape.
|
|
19754
|
+
case "tool_result":
|
|
19755
|
+
case "tool.result": {
|
|
19756
|
+
const toolId = getString(part?.callID) || getString(parsed.callID) || getString(part?.id) || getString(parsed.id) || `opencode-tool-${Date.now()}`;
|
|
19757
|
+
const output = part && typeof part.output === "string" ? part.output : JSON.stringify(part);
|
|
19758
|
+
const stateRecord = getRecord(part?.state) ?? getRecord(parsed.state);
|
|
19759
|
+
const toolError = part?.is_error === true || part?.isError === true || stateRecord?.status === "error" || Boolean(getString(part?.error) || getString(stateRecord?.error));
|
|
19760
|
+
void presenter.onToolResult?.(toolId, output, toolError);
|
|
19761
|
+
if (toolError && !state.error) {
|
|
19762
|
+
const message = getString(part?.error) || getString(stateRecord?.error) || "OpenCode tool call failed.";
|
|
19763
|
+
state.error = message;
|
|
19764
|
+
void presenter.onError(message);
|
|
19765
|
+
}
|
|
19766
|
+
return true;
|
|
19767
|
+
}
|
|
19269
19768
|
case "session.error":
|
|
19270
19769
|
case "error": {
|
|
19271
19770
|
const errObj = typeof parsed.error === "object" && parsed.error !== null ? parsed.error : null;
|
|
@@ -19290,85 +19789,450 @@ var parseOpencodeStructuredLine = createStructuredLineParser(
|
|
|
19290
19789
|
"opencode_cli",
|
|
19291
19790
|
handleOpencodeStructuredEvent
|
|
19292
19791
|
);
|
|
19293
|
-
|
|
19294
|
-
|
|
19295
|
-
|
|
19296
|
-
|
|
19297
|
-
|
|
19298
|
-
|
|
19792
|
+
function buildOpencodeEffortArgs(selectedEffortLevel) {
|
|
19793
|
+
const trimmed = selectedEffortLevel?.trim().toLowerCase();
|
|
19794
|
+
if (!trimmed || !isEffortLevel(trimmed)) return [];
|
|
19795
|
+
return ["--variant", trimmed];
|
|
19796
|
+
}
|
|
19797
|
+
function createOpencodeCliBackend(command = "opencode", defaultArgs = []) {
|
|
19798
|
+
return {
|
|
19799
|
+
kind: "opencode_cli",
|
|
19800
|
+
supportTier: "structured",
|
|
19801
|
+
async run(context) {
|
|
19802
|
+
const { paths: imagePaths, cleanup } = writeImagesToTempFiles(
|
|
19803
|
+
context.config.images,
|
|
19804
|
+
context.cwd
|
|
19805
|
+
);
|
|
19806
|
+
const { paths: filePaths, cleanup: cleanupFiles } = writeFilesToTempFiles(
|
|
19807
|
+
context.config.files,
|
|
19808
|
+
context.cwd
|
|
19809
|
+
);
|
|
19810
|
+
try {
|
|
19811
|
+
const runContext = withAutonomousOpenCodePermissions(context);
|
|
19812
|
+
return await createGenericCliBackend({
|
|
19813
|
+
kind: "opencode_cli",
|
|
19814
|
+
supportTier: "structured",
|
|
19815
|
+
command,
|
|
19816
|
+
args: [],
|
|
19817
|
+
buildArgs: (ctx, prompt) => {
|
|
19818
|
+
const args = ["run", "--format", "json", "--thinking"];
|
|
19819
|
+
if (!shouldUseReadOnlyRuntimePermissions(ctx.config)) {
|
|
19820
|
+
args.push("--dangerously-skip-permissions");
|
|
19821
|
+
}
|
|
19822
|
+
const resumeId = ctx.config.runtimeSessionId?.trim() || ctx.config.providerSessionId?.trim();
|
|
19823
|
+
if (resumeId) args.push("--session", resumeId);
|
|
19824
|
+
const model = ctx.config.selectedModel?.trim();
|
|
19825
|
+
if (model?.includes("/")) args.push("--model", model);
|
|
19826
|
+
args.push(...buildOpencodeEffortArgs(ctx.config.selectedEffortLevel));
|
|
19827
|
+
const attachmentPaths = [...imagePaths, ...filePaths];
|
|
19828
|
+
const fileArgs = attachmentPaths.flatMap((p) => ["--file", p]);
|
|
19829
|
+
if (fileArgs.length > 0) {
|
|
19830
|
+
return [...args, ...fileArgs, ...defaultArgs, "--", prompt];
|
|
19831
|
+
}
|
|
19832
|
+
return [...args, ...defaultArgs, prompt];
|
|
19833
|
+
},
|
|
19834
|
+
augmentPrompt: (ctx) => {
|
|
19835
|
+
const base = buildPromptWithSystem(ctx.config, ctx.promptText);
|
|
19836
|
+
return ctx.config.mode === "plan" ? buildPlanModePrefix(base) : base;
|
|
19837
|
+
},
|
|
19838
|
+
parseStructuredLine: parseOpencodeStructuredLine
|
|
19839
|
+
}).run(runContext);
|
|
19840
|
+
} finally {
|
|
19841
|
+
cleanup();
|
|
19842
|
+
cleanupFiles();
|
|
19843
|
+
}
|
|
19844
|
+
}
|
|
19845
|
+
};
|
|
19846
|
+
}
|
|
19847
|
+
function parseSseEventBlock(block) {
|
|
19848
|
+
const dataLines = block.split("\n").map((line) => line.endsWith("\r") ? line.slice(0, -1) : line).filter((line) => line.startsWith("data:")).map((line) => line.slice("data:".length).replace(/^ /, ""));
|
|
19849
|
+
if (dataLines.length === 0) return null;
|
|
19299
19850
|
try {
|
|
19300
|
-
const parsed = JSON.parse(
|
|
19301
|
-
if (parsed && typeof parsed === "object" &&
|
|
19302
|
-
|
|
19303
|
-
|
|
19304
|
-
|
|
19305
|
-
|
|
19851
|
+
const parsed = JSON.parse(dataLines.join("\n"));
|
|
19852
|
+
if (parsed && typeof parsed === "object" && typeof parsed.type === "string") {
|
|
19853
|
+
const record = parsed;
|
|
19854
|
+
return {
|
|
19855
|
+
id: typeof record.id === "string" ? record.id : void 0,
|
|
19856
|
+
type: record.type,
|
|
19857
|
+
properties: typeof record.properties === "object" && record.properties !== null ? record.properties : {}
|
|
19858
|
+
};
|
|
19306
19859
|
}
|
|
19307
19860
|
} catch {
|
|
19308
19861
|
}
|
|
19309
|
-
return
|
|
19862
|
+
return null;
|
|
19863
|
+
}
|
|
19864
|
+
async function connectOpenCodeSse(url2, signal) {
|
|
19865
|
+
const response = await fetch(url2, {
|
|
19866
|
+
signal,
|
|
19867
|
+
headers: { Accept: "text/event-stream" }
|
|
19868
|
+
});
|
|
19869
|
+
if (!response.ok || !response.body) {
|
|
19870
|
+
throw new Error(`OpenCode SSE connection failed: HTTP ${response.status}`);
|
|
19871
|
+
}
|
|
19872
|
+
const reader = response.body.getReader();
|
|
19873
|
+
const decoder = new TextDecoder();
|
|
19874
|
+
let closed = false;
|
|
19875
|
+
async function* generate() {
|
|
19876
|
+
let buffer = "";
|
|
19877
|
+
try {
|
|
19878
|
+
while (!closed) {
|
|
19879
|
+
const { value: value2, done } = await reader.read();
|
|
19880
|
+
if (done) return;
|
|
19881
|
+
buffer += decoder.decode(value2, { stream: true });
|
|
19882
|
+
let separatorIndex = buffer.indexOf("\n\n");
|
|
19883
|
+
while (separatorIndex !== -1) {
|
|
19884
|
+
const block = buffer.slice(0, separatorIndex);
|
|
19885
|
+
buffer = buffer.slice(separatorIndex + 2);
|
|
19886
|
+
const event = parseSseEventBlock(block);
|
|
19887
|
+
if (event) yield event;
|
|
19888
|
+
separatorIndex = buffer.indexOf("\n\n");
|
|
19889
|
+
}
|
|
19890
|
+
}
|
|
19891
|
+
} finally {
|
|
19892
|
+
try {
|
|
19893
|
+
reader.releaseLock();
|
|
19894
|
+
} catch {
|
|
19895
|
+
}
|
|
19896
|
+
}
|
|
19897
|
+
}
|
|
19898
|
+
return {
|
|
19899
|
+
events: generate(),
|
|
19900
|
+
close: () => {
|
|
19901
|
+
if (closed) return;
|
|
19902
|
+
closed = true;
|
|
19903
|
+
reader.cancel().catch(() => {
|
|
19904
|
+
});
|
|
19905
|
+
}
|
|
19906
|
+
};
|
|
19907
|
+
}
|
|
19908
|
+
var SERVER_LISTENING_PATTERN = /listening on (https?:\/\/\S+)/i;
|
|
19909
|
+
var STARTUP_TIMEOUT_MS = 2e4;
|
|
19910
|
+
var HEALTH_POLL_INTERVAL_MS = 200;
|
|
19911
|
+
var HEALTH_POLL_MAX_ATTEMPTS = 50;
|
|
19912
|
+
function startOpenCodeServer(command, context) {
|
|
19913
|
+
const child = spawnCli(command, ["serve", "--port", "0", "--hostname", "127.0.0.1"], context);
|
|
19914
|
+
const ready = new Promise((resolve22, reject) => {
|
|
19915
|
+
let settled = false;
|
|
19916
|
+
const timeout = setTimeout(() => {
|
|
19917
|
+
if (settled) return;
|
|
19918
|
+
settled = true;
|
|
19919
|
+
reject(
|
|
19920
|
+
new Error(`opencode serve did not report a listening URL within ${STARTUP_TIMEOUT_MS}ms`)
|
|
19921
|
+
);
|
|
19922
|
+
}, STARTUP_TIMEOUT_MS);
|
|
19923
|
+
const stdoutRl = (0, import_readline2.createInterface)({ input: child.stdout });
|
|
19924
|
+
const stderrRl = (0, import_readline2.createInterface)({ input: child.stderr });
|
|
19925
|
+
const stderrLines = [];
|
|
19926
|
+
const checkLine = (line) => {
|
|
19927
|
+
if (settled) return;
|
|
19928
|
+
const match = line.match(SERVER_LISTENING_PATTERN);
|
|
19929
|
+
if (!match) return;
|
|
19930
|
+
settled = true;
|
|
19931
|
+
clearTimeout(timeout);
|
|
19932
|
+
resolve22({ baseUrl: match[1].replace(/\/$/, "") });
|
|
19933
|
+
};
|
|
19934
|
+
stdoutRl.on("line", checkLine);
|
|
19935
|
+
stderrRl.on("line", (line) => {
|
|
19936
|
+
stderrLines.push(line);
|
|
19937
|
+
checkLine(line);
|
|
19938
|
+
});
|
|
19939
|
+
child.once("error", (error) => {
|
|
19940
|
+
if (settled) return;
|
|
19941
|
+
settled = true;
|
|
19942
|
+
clearTimeout(timeout);
|
|
19943
|
+
reject(error);
|
|
19944
|
+
});
|
|
19945
|
+
child.once("exit", (code) => {
|
|
19946
|
+
if (settled) return;
|
|
19947
|
+
settled = true;
|
|
19948
|
+
clearTimeout(timeout);
|
|
19949
|
+
reject(
|
|
19950
|
+
new Error(
|
|
19951
|
+
`opencode serve exited (code ${code}) before reporting a listening URL. stderr: ${stderrLines.slice(-5).join("\n")}`
|
|
19952
|
+
)
|
|
19953
|
+
);
|
|
19954
|
+
});
|
|
19955
|
+
});
|
|
19956
|
+
return { child, ready };
|
|
19957
|
+
}
|
|
19958
|
+
async function waitForHealthy(baseUrl, signal) {
|
|
19959
|
+
for (let attempt = 0; attempt < HEALTH_POLL_MAX_ATTEMPTS; attempt++) {
|
|
19960
|
+
if (signal.aborted)
|
|
19961
|
+
throw new Error("Aborted while waiting for opencode server to become healthy");
|
|
19962
|
+
try {
|
|
19963
|
+
const response = await fetch(`${baseUrl}/global/health`, { signal });
|
|
19964
|
+
if (response.ok) {
|
|
19965
|
+
const body = await response.json().catch(() => null);
|
|
19966
|
+
if (body?.healthy) return;
|
|
19967
|
+
}
|
|
19968
|
+
} catch {
|
|
19969
|
+
}
|
|
19970
|
+
await new Promise((resolve22) => setTimeout(resolve22, HEALTH_POLL_INTERVAL_MS));
|
|
19971
|
+
}
|
|
19972
|
+
throw new Error(`opencode server at ${baseUrl} did not become healthy in time`);
|
|
19973
|
+
}
|
|
19974
|
+
var NON_INTERACTIVE_DENY_RULES = [
|
|
19975
|
+
{ permission: "question", pattern: "*", action: "deny" },
|
|
19976
|
+
{ permission: "plan_enter", pattern: "*", action: "deny" },
|
|
19977
|
+
{ permission: "plan_exit", pattern: "*", action: "deny" }
|
|
19978
|
+
];
|
|
19979
|
+
async function resolveSession(baseUrl, context, signal) {
|
|
19980
|
+
const resumeId = context.config.runtimeSessionId?.trim() || context.config.providerSessionId?.trim();
|
|
19981
|
+
if (resumeId) {
|
|
19982
|
+
const existing = await fetch(`${baseUrl}/session/${encodeURIComponent(resumeId)}`, {
|
|
19983
|
+
signal
|
|
19984
|
+
}).catch(() => null);
|
|
19985
|
+
if (existing?.ok) return { id: resumeId };
|
|
19986
|
+
}
|
|
19987
|
+
const title = context.promptText.slice(0, 50) + (context.promptText.length > 50 ? "..." : "");
|
|
19988
|
+
const response = await fetch(`${baseUrl}/session`, {
|
|
19989
|
+
method: "POST",
|
|
19990
|
+
signal,
|
|
19991
|
+
headers: { "Content-Type": "application/json" },
|
|
19992
|
+
body: JSON.stringify({ title, permission: NON_INTERACTIVE_DENY_RULES })
|
|
19993
|
+
});
|
|
19994
|
+
if (!response.ok) {
|
|
19995
|
+
throw new Error(`Failed to create OpenCode session: HTTP ${response.status}`);
|
|
19996
|
+
}
|
|
19997
|
+
const info = await response.json();
|
|
19998
|
+
if (!info.id) throw new Error("OpenCode session create response had no id");
|
|
19999
|
+
return { id: info.id };
|
|
20000
|
+
}
|
|
20001
|
+
function buildPromptParts(promptText, imageFiles, attachmentFiles) {
|
|
20002
|
+
const attachments = [...imageFiles, ...attachmentFiles].map(
|
|
20003
|
+
(file) => ({
|
|
20004
|
+
type: "file",
|
|
20005
|
+
mime: file.mimeType,
|
|
20006
|
+
filename: file.filename,
|
|
20007
|
+
url: (0, import_url2.pathToFileURL)(file.path).href
|
|
20008
|
+
})
|
|
20009
|
+
);
|
|
20010
|
+
return [...attachments, { type: "text", text: promptText }];
|
|
20011
|
+
}
|
|
20012
|
+
async function sendPrompt(baseUrl, sessionId, context, parts2, signal) {
|
|
20013
|
+
const model = context.config.selectedModel?.trim();
|
|
20014
|
+
const modelBody = model && model.includes("/") ? (() => {
|
|
20015
|
+
const slashIndex = model.indexOf("/");
|
|
20016
|
+
return { providerID: model.slice(0, slashIndex), modelID: model.slice(slashIndex + 1) };
|
|
20017
|
+
})() : void 0;
|
|
20018
|
+
const variant = context.config.selectedEffortLevel?.trim().toLowerCase();
|
|
20019
|
+
const response = await fetch(`${baseUrl}/session/${encodeURIComponent(sessionId)}/message`, {
|
|
20020
|
+
method: "POST",
|
|
20021
|
+
signal,
|
|
20022
|
+
headers: { "Content-Type": "application/json" },
|
|
20023
|
+
body: JSON.stringify({
|
|
20024
|
+
parts: parts2,
|
|
20025
|
+
...modelBody ? { model: modelBody } : {},
|
|
20026
|
+
...variant && isEffortLevel(variant) ? { variant } : {}
|
|
20027
|
+
})
|
|
20028
|
+
});
|
|
20029
|
+
if (!response.ok) {
|
|
20030
|
+
const text = await response.text().catch(() => "");
|
|
20031
|
+
throw new Error(`OpenCode prompt request failed: HTTP ${response.status} ${text}`.trim());
|
|
20032
|
+
}
|
|
19310
20033
|
}
|
|
19311
|
-
function
|
|
19312
|
-
|
|
20034
|
+
async function replyToPermission(baseUrl, requestId, reply, signal) {
|
|
20035
|
+
await fetch(`${baseUrl}/permission/${encodeURIComponent(requestId)}/reply`, {
|
|
20036
|
+
method: "POST",
|
|
20037
|
+
signal,
|
|
20038
|
+
headers: { "Content-Type": "application/json" },
|
|
20039
|
+
body: JSON.stringify({ reply })
|
|
20040
|
+
}).catch((error) => {
|
|
20041
|
+
console.warn("[opencode_serve] Failed to reply to permission request", requestId, error);
|
|
20042
|
+
});
|
|
20043
|
+
}
|
|
20044
|
+
function buildAgentResult(state, sessionId, aborted2) {
|
|
20045
|
+
if (aborted2) {
|
|
20046
|
+
return {
|
|
20047
|
+
success: false,
|
|
20048
|
+
summary: "Interrupted by user",
|
|
20049
|
+
filesModified: [],
|
|
20050
|
+
planFilesCreated: [],
|
|
20051
|
+
iterations: Math.max(state.iterations, 1),
|
|
20052
|
+
error: "Interrupted by user",
|
|
20053
|
+
providerSessionId: sessionId,
|
|
20054
|
+
runtimeSessionId: sessionId,
|
|
20055
|
+
backendKind: "opencode_serve",
|
|
20056
|
+
supportTier: "structured",
|
|
20057
|
+
usage: state.usage
|
|
20058
|
+
};
|
|
20059
|
+
}
|
|
20060
|
+
const failed = Boolean(state.error?.trim());
|
|
20061
|
+
const summary = state.summary.trim() || state.error?.trim() || (failed ? "Task failed" : "Task completed");
|
|
19313
20062
|
return {
|
|
19314
|
-
|
|
19315
|
-
|
|
19316
|
-
|
|
19317
|
-
|
|
19318
|
-
|
|
19319
|
-
|
|
19320
|
-
|
|
20063
|
+
success: !failed,
|
|
20064
|
+
summary,
|
|
20065
|
+
filesModified: [],
|
|
20066
|
+
planFilesCreated: [],
|
|
20067
|
+
iterations: Math.max(state.iterations, 1),
|
|
20068
|
+
...state.error?.trim() ? { error: state.error.trim() } : {},
|
|
20069
|
+
providerSessionId: sessionId,
|
|
20070
|
+
runtimeSessionId: sessionId,
|
|
20071
|
+
backendKind: "opencode_serve",
|
|
20072
|
+
supportTier: "structured",
|
|
20073
|
+
usage: state.usage
|
|
19321
20074
|
};
|
|
19322
20075
|
}
|
|
19323
|
-
function
|
|
20076
|
+
function createOpencodeServeBackend(command = "opencode") {
|
|
19324
20077
|
return {
|
|
19325
|
-
kind: "
|
|
20078
|
+
kind: "opencode_serve",
|
|
19326
20079
|
supportTier: "structured",
|
|
19327
20080
|
async run(context) {
|
|
19328
|
-
const {
|
|
20081
|
+
const { files: imageFiles, cleanup: cleanupImages } = writeImagesToTempFiles(
|
|
19329
20082
|
context.config.images,
|
|
19330
20083
|
context.cwd
|
|
19331
20084
|
);
|
|
19332
|
-
const {
|
|
20085
|
+
const { files: attachmentFiles, cleanup: cleanupFiles } = writeFilesToTempFiles(
|
|
19333
20086
|
context.config.files,
|
|
19334
20087
|
context.cwd
|
|
19335
20088
|
);
|
|
20089
|
+
const runContext = withAutonomousOpenCodePermissions(context);
|
|
20090
|
+
const readOnly = shouldUseReadOnlyRuntimePermissions(context.config);
|
|
20091
|
+
const { child, ready } = startOpenCodeServer(command, runContext);
|
|
20092
|
+
const state = { process: child, iterations: 0, summary: "" };
|
|
20093
|
+
context.onProcessSpawned?.(child);
|
|
20094
|
+
context.registerLivenessProbe?.(() => ({
|
|
20095
|
+
providerAlive: child.exitCode === null,
|
|
20096
|
+
lastRawOutputAgoMs: typeof state.lastRawOutputAtMs === "number" ? Date.now() - state.lastRawOutputAtMs : null
|
|
20097
|
+
}));
|
|
20098
|
+
let sseClose = null;
|
|
20099
|
+
let sessionId = "";
|
|
20100
|
+
let aborted2 = false;
|
|
20101
|
+
const onAbort = () => {
|
|
20102
|
+
aborted2 = true;
|
|
20103
|
+
sseClose?.();
|
|
20104
|
+
killProcessTree(child.pid, { signal: "SIGTERM", child });
|
|
20105
|
+
};
|
|
20106
|
+
context.abortController.signal.addEventListener("abort", onAbort, { once: true });
|
|
19336
20107
|
try {
|
|
19337
|
-
const
|
|
19338
|
-
|
|
19339
|
-
|
|
19340
|
-
|
|
19341
|
-
|
|
19342
|
-
|
|
19343
|
-
|
|
19344
|
-
|
|
19345
|
-
|
|
19346
|
-
|
|
19347
|
-
|
|
19348
|
-
|
|
19349
|
-
|
|
19350
|
-
|
|
19351
|
-
|
|
19352
|
-
|
|
19353
|
-
|
|
19354
|
-
|
|
19355
|
-
|
|
19356
|
-
|
|
19357
|
-
|
|
19358
|
-
|
|
19359
|
-
|
|
19360
|
-
|
|
19361
|
-
|
|
19362
|
-
|
|
19363
|
-
|
|
19364
|
-
}
|
|
20108
|
+
const server = await ready;
|
|
20109
|
+
state.lastRawOutputAtMs = Date.now();
|
|
20110
|
+
await waitForHealthy(server.baseUrl, context.abortController.signal);
|
|
20111
|
+
const session = await resolveSession(
|
|
20112
|
+
server.baseUrl,
|
|
20113
|
+
runContext,
|
|
20114
|
+
context.abortController.signal
|
|
20115
|
+
);
|
|
20116
|
+
sessionId = session.id;
|
|
20117
|
+
state.runtimeSessionId = sessionId;
|
|
20118
|
+
const prompt = context.config.mode === "plan" ? buildPlanModePrefix(buildPromptWithSystem(context.config, context.promptText)) : buildPromptWithSystem(context.config, context.promptText);
|
|
20119
|
+
const parts2 = buildPromptParts(prompt, imageFiles, attachmentFiles);
|
|
20120
|
+
const connection = await connectOpenCodeSse(
|
|
20121
|
+
`${server.baseUrl}/event`,
|
|
20122
|
+
context.abortController.signal
|
|
20123
|
+
);
|
|
20124
|
+
sseClose = connection.close;
|
|
20125
|
+
const promptError = await sendPrompt(
|
|
20126
|
+
server.baseUrl,
|
|
20127
|
+
sessionId,
|
|
20128
|
+
runContext,
|
|
20129
|
+
parts2,
|
|
20130
|
+
context.abortController.signal
|
|
20131
|
+
).then(() => null).catch((error) => ({
|
|
20132
|
+
message: error instanceof Error ? error.message : String(error)
|
|
20133
|
+
}));
|
|
20134
|
+
if (context.abortController.signal.aborted) {
|
|
20135
|
+
} else if (promptError) {
|
|
20136
|
+
state.error = promptError.message;
|
|
20137
|
+
void context.presenter.onError(promptError.message);
|
|
20138
|
+
} else {
|
|
20139
|
+
for await (const event of connection.events) {
|
|
20140
|
+
if (aborted2) break;
|
|
20141
|
+
const done = handleOpenCodeServeEvent(
|
|
20142
|
+
event,
|
|
20143
|
+
sessionId,
|
|
20144
|
+
runContext,
|
|
20145
|
+
state,
|
|
20146
|
+
readOnly,
|
|
20147
|
+
server.baseUrl
|
|
20148
|
+
);
|
|
20149
|
+
if (done) break;
|
|
20150
|
+
}
|
|
20151
|
+
}
|
|
20152
|
+
return buildAgentResult(state, sessionId, aborted2);
|
|
20153
|
+
} catch (error) {
|
|
20154
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
20155
|
+
if (!aborted2 && !context.abortController.signal.aborted) {
|
|
20156
|
+
console.error("[opencode_serve] Run failed:", message);
|
|
20157
|
+
state.error = message;
|
|
20158
|
+
void context.presenter.onError(message);
|
|
20159
|
+
}
|
|
20160
|
+
return buildAgentResult(
|
|
20161
|
+
state,
|
|
20162
|
+
sessionId,
|
|
20163
|
+
aborted2 || context.abortController.signal.aborted
|
|
20164
|
+
);
|
|
19365
20165
|
} finally {
|
|
19366
|
-
|
|
20166
|
+
context.abortController.signal.removeEventListener("abort", onAbort);
|
|
20167
|
+
sseClose?.();
|
|
20168
|
+
killProcessTree(child.pid, { signal: "SIGTERM", child });
|
|
20169
|
+
cleanupImages();
|
|
19367
20170
|
cleanupFiles();
|
|
19368
20171
|
}
|
|
19369
20172
|
}
|
|
19370
20173
|
};
|
|
19371
20174
|
}
|
|
20175
|
+
function handleOpenCodeServeEvent(event, sessionId, context, state, readOnly, baseUrl) {
|
|
20176
|
+
const properties = event.properties;
|
|
20177
|
+
const eventSessionId = getString(properties.sessionID);
|
|
20178
|
+
if (eventSessionId && eventSessionId !== sessionId) return false;
|
|
20179
|
+
switch (event.type) {
|
|
20180
|
+
// Live-verified: the SSE bus emits a message.updated (and matching
|
|
20181
|
+
// message.part.updated) for the USER's own message too, not just the
|
|
20182
|
+
// assistant's — track user message ids so their echoed text is never
|
|
20183
|
+
// mistaken for assistant output below.
|
|
20184
|
+
case "message.updated": {
|
|
20185
|
+
const info = getRecord(properties.info);
|
|
20186
|
+
if (getString(info?.role) === "user") {
|
|
20187
|
+
const messageId = getString(info?.id);
|
|
20188
|
+
if (messageId) {
|
|
20189
|
+
state.opencodeUserMessageIds ??= /* @__PURE__ */ new Set();
|
|
20190
|
+
state.opencodeUserMessageIds.add(messageId);
|
|
20191
|
+
}
|
|
20192
|
+
}
|
|
20193
|
+
return false;
|
|
20194
|
+
}
|
|
20195
|
+
case "message.part.updated": {
|
|
20196
|
+
const part = getRecord(properties.part);
|
|
20197
|
+
if (!part) return false;
|
|
20198
|
+
const messageId = getString(part.messageID);
|
|
20199
|
+
if (messageId && state.opencodeUserMessageIds?.has(messageId)) return false;
|
|
20200
|
+
mapOpencodePart(part, context, state);
|
|
20201
|
+
return false;
|
|
20202
|
+
}
|
|
20203
|
+
// Live token-by-token streaming is explicitly out of scope — only the
|
|
20204
|
+
// start/complete snapshots from message.part.updated are consumed.
|
|
20205
|
+
case "message.part.delta":
|
|
20206
|
+
return false;
|
|
20207
|
+
case "permission.asked": {
|
|
20208
|
+
const requestId = getString(properties.id);
|
|
20209
|
+
if (requestId) {
|
|
20210
|
+
void replyToPermission(
|
|
20211
|
+
baseUrl,
|
|
20212
|
+
requestId,
|
|
20213
|
+
readOnly ? "reject" : "once",
|
|
20214
|
+
context.abortController.signal
|
|
20215
|
+
);
|
|
20216
|
+
}
|
|
20217
|
+
return false;
|
|
20218
|
+
}
|
|
20219
|
+
case "session.error": {
|
|
20220
|
+
const errorRecord = getRecord(properties.error);
|
|
20221
|
+
const message = getString(errorRecord?.message) || getString(properties.error) || "OpenCode session error.";
|
|
20222
|
+
if (!state.error) {
|
|
20223
|
+
state.error = message;
|
|
20224
|
+
void context.presenter.onError(message);
|
|
20225
|
+
}
|
|
20226
|
+
return false;
|
|
20227
|
+
}
|
|
20228
|
+
case "session.status": {
|
|
20229
|
+
const status = getRecord(properties.status);
|
|
20230
|
+
return getString(status?.type) === "idle";
|
|
20231
|
+
}
|
|
20232
|
+
default:
|
|
20233
|
+
return false;
|
|
20234
|
+
}
|
|
20235
|
+
}
|
|
19372
20236
|
function createGenericCliPassthroughBackend(command, defaultArgs = []) {
|
|
19373
20237
|
return {
|
|
19374
20238
|
kind: "generic_cli",
|
|
@@ -19473,7 +20337,7 @@ function createSupatestCliBackend(command = "supatest", defaultArgs = []) {
|
|
|
19473
20337
|
const basePromptText = resumeContextPrefix ? `${resumeContextPrefix}
|
|
19474
20338
|
|
|
19475
20339
|
${ctx.promptText}` : ctx.promptText;
|
|
19476
|
-
const promptText = ctx.config.mode === "plan" ? buildPlanModePrefix(
|
|
20340
|
+
const promptText = ctx.config.mode === "plan" ? buildPlanModePrefix(basePromptText) : basePromptText;
|
|
19477
20341
|
if (promptText.trim()) {
|
|
19478
20342
|
contentBlocks.push({ type: "text", text: promptText });
|
|
19479
20343
|
}
|
|
@@ -19597,6 +20461,7 @@ function defaultSelectedModelForBackend(backendKind) {
|
|
|
19597
20461
|
case "grok_cli":
|
|
19598
20462
|
return firstCatalogModelId(backendKind) ?? "grok-4.5";
|
|
19599
20463
|
case "opencode_cli":
|
|
20464
|
+
case "opencode_serve":
|
|
19600
20465
|
return void 0;
|
|
19601
20466
|
case "antigravity_cli":
|
|
19602
20467
|
return void 0;
|
|
@@ -19658,6 +20523,8 @@ var BaseMachineAgent = class _BaseMachineAgent {
|
|
|
19658
20523
|
presenter;
|
|
19659
20524
|
runtime;
|
|
19660
20525
|
abortController = null;
|
|
20526
|
+
/** Log the "synced MCP session headers" line once per run(), not per respawn. */
|
|
20527
|
+
mcpHeadersLogged = false;
|
|
19661
20528
|
constructor(presenter, runtime) {
|
|
19662
20529
|
this.presenter = presenter;
|
|
19663
20530
|
this.runtime = runtime;
|
|
@@ -19714,7 +20581,52 @@ var BaseMachineAgent = class _BaseMachineAgent {
|
|
|
19714
20581
|
getActivityHeartbeatIntervalMs() {
|
|
19715
20582
|
return AGENT_ACTIVITY_HEARTBEAT_MS;
|
|
19716
20583
|
}
|
|
20584
|
+
/**
|
|
20585
|
+
* Home directory whose CLI config files receive the Alan MCP session headers.
|
|
20586
|
+
* Defaults to the process home (production behaviour); tests override to a
|
|
20587
|
+
* temp dir so runs never touch the developer's real CLI configs.
|
|
20588
|
+
*/
|
|
20589
|
+
getMcpConfigHomeDir() {
|
|
20590
|
+
return void 0;
|
|
20591
|
+
}
|
|
20592
|
+
/**
|
|
20593
|
+
* Re-assert this run's Alan MCP identity in the shared CLI config under the
|
|
20594
|
+
* serialization gate, holding the gate until the provider spawns. Returns the
|
|
20595
|
+
* gate release. Concurrent runs on one machine share ~/.codex/config.toml,
|
|
20596
|
+
* ~/.cursor/mcp.json, and ~/.claude.json; the CLI reads its config once at
|
|
20597
|
+
* spawn, so without serialization a concurrent run could rewrite the shared
|
|
20598
|
+
* file between this write and this spawn and steal this run's session identity.
|
|
20599
|
+
*/
|
|
20600
|
+
async beginMcpConfigCriticalSection(config) {
|
|
20601
|
+
const conversationId = config.conversationId;
|
|
20602
|
+
if (!conversationId) return () => {
|
|
20603
|
+
};
|
|
20604
|
+
const releaseGate = await acquireMcpConfigWriteGate();
|
|
20605
|
+
try {
|
|
20606
|
+
const writtenClis = syncAlanMcpSessionHeaders({
|
|
20607
|
+
conversationId,
|
|
20608
|
+
teamId: config.teamId,
|
|
20609
|
+
runHeaders: config.alanMcp?.headers,
|
|
20610
|
+
homeDir: this.getMcpConfigHomeDir()
|
|
20611
|
+
});
|
|
20612
|
+
if (writtenClis.length > 0 && !this.mcpHeadersLogged) {
|
|
20613
|
+
this.mcpHeadersLogged = true;
|
|
20614
|
+
void this.presenter.onLog(`Synced Alan MCP session headers to ${writtenClis.join(", ")}`);
|
|
20615
|
+
}
|
|
20616
|
+
} catch (error) {
|
|
20617
|
+
console.warn("[base-machine-agent] Failed to sync MCP session headers", error);
|
|
20618
|
+
}
|
|
20619
|
+
return releaseGate;
|
|
20620
|
+
}
|
|
19717
20621
|
async runBackendWithActivityHeartbeat(backend, context) {
|
|
20622
|
+
const releaseMcpGate = await this.beginMcpConfigCriticalSection(context.config);
|
|
20623
|
+
let mcpGateReleased = false;
|
|
20624
|
+
const releaseMcpGateOnce = () => {
|
|
20625
|
+
if (mcpGateReleased) return;
|
|
20626
|
+
mcpGateReleased = true;
|
|
20627
|
+
releaseMcpGate();
|
|
20628
|
+
};
|
|
20629
|
+
const callerOnProcessSpawned = context.onProcessSpawned;
|
|
19718
20630
|
const intervalMs = this.getActivityHeartbeatIntervalMs();
|
|
19719
20631
|
let heartbeat = null;
|
|
19720
20632
|
let livenessProbe = null;
|
|
@@ -19733,9 +20645,17 @@ var BaseMachineAgent = class _BaseMachineAgent {
|
|
|
19733
20645
|
}
|
|
19734
20646
|
}, intervalMs);
|
|
19735
20647
|
}
|
|
20648
|
+
const spawnAwareContext = {
|
|
20649
|
+
...context,
|
|
20650
|
+
onProcessSpawned: (child) => {
|
|
20651
|
+
releaseMcpGateOnce();
|
|
20652
|
+
callerOnProcessSpawned?.(child);
|
|
20653
|
+
}
|
|
20654
|
+
};
|
|
19736
20655
|
try {
|
|
19737
|
-
return await backend.run(
|
|
20656
|
+
return await backend.run(spawnAwareContext);
|
|
19738
20657
|
} finally {
|
|
20658
|
+
releaseMcpGateOnce();
|
|
19739
20659
|
if (heartbeat) clearInterval(heartbeat);
|
|
19740
20660
|
}
|
|
19741
20661
|
}
|
|
@@ -19758,6 +20678,7 @@ var BaseMachineAgent = class _BaseMachineAgent {
|
|
|
19758
20678
|
case "cursor_agent_cli":
|
|
19759
20679
|
case "droid_cli":
|
|
19760
20680
|
case "opencode_cli":
|
|
20681
|
+
case "opencode_serve":
|
|
19761
20682
|
case "copilot_cli":
|
|
19762
20683
|
case "supatest_cli":
|
|
19763
20684
|
case "kimi_cli":
|
|
@@ -19825,7 +20746,7 @@ Prefer relative paths and keep your work scoped to this project.`
|
|
|
19825
20746
|
const promptText = this.buildPromptText(runtimeConfig);
|
|
19826
20747
|
const runtimeCommand = runtimeConfig.runtimeCommand || this.runtime.runtimeCommand;
|
|
19827
20748
|
const runtimeArgs = runtimeConfig.runtimeArgs || this.runtime.runtimeArgs || [];
|
|
19828
|
-
const backend = backendKind === "claude_cli" ? createClaudeCliBackend(runtimeCommand || "claude", runtimeArgs) : backendKind === "codex_app_server" ? createCodexRuntimeBackend(runtimeCommand || "codex", runtimeArgs) : backendKind === "copilot_cli" ? createCopilotCliBackend(runtimeCommand || "copilot", runtimeArgs) : backendKind === "cursor_agent_cli" ? createCursorAgentCliBackend(runtimeCommand || "cursor-agent", runtimeArgs) : backendKind === "antigravity_cli" ? createAntigravityCliBackend(runtimeCommand || "agy", runtimeArgs) : backendKind === "droid_cli" ? createDroidCliBackend(runtimeCommand || "droid", runtimeArgs) : backendKind === "opencode_cli" ? createOpencodeCliBackend(runtimeCommand || "opencode", runtimeArgs) : backendKind === "kimi_cli" ? createKimiCliBackend(runtimeCommand || "kimi-cli", runtimeArgs) : backendKind === "grok_cli" ? createGrokCliBackend(runtimeCommand || "grok", runtimeArgs) : backendKind === "supatest_cli" ? createSupatestCliBackend(runtimeCommand || "supatest", runtimeArgs) : createGenericCliPassthroughBackend(
|
|
20749
|
+
const backend = backendKind === "claude_cli" ? createClaudeCliBackend(runtimeCommand || "claude", runtimeArgs) : backendKind === "codex_app_server" ? createCodexRuntimeBackend(runtimeCommand || "codex", runtimeArgs) : backendKind === "copilot_cli" ? createCopilotCliBackend(runtimeCommand || "copilot", runtimeArgs) : backendKind === "cursor_agent_cli" ? createCursorAgentCliBackend(runtimeCommand || "cursor-agent", runtimeArgs) : backendKind === "antigravity_cli" ? createAntigravityCliBackend(runtimeCommand || "agy", runtimeArgs) : backendKind === "droid_cli" ? createDroidCliBackend(runtimeCommand || "droid", runtimeArgs) : backendKind === "opencode_cli" ? createOpencodeCliBackend(runtimeCommand || "opencode", runtimeArgs) : backendKind === "opencode_serve" ? createOpencodeServeBackend(runtimeCommand || "opencode") : backendKind === "kimi_cli" ? createKimiCliBackend(runtimeCommand || "kimi-cli", runtimeArgs) : backendKind === "grok_cli" ? createGrokCliBackend(runtimeCommand || "grok", runtimeArgs) : backendKind === "supatest_cli" ? createSupatestCliBackend(runtimeCommand || "supatest", runtimeArgs) : createGenericCliPassthroughBackend(
|
|
19829
20750
|
runtimeCommand || "generic-cli",
|
|
19830
20751
|
runtimeArgs
|
|
19831
20752
|
);
|
|
@@ -19969,19 +20890,7 @@ ${runtimeConfig.task}` } : {}
|
|
|
19969
20890
|
this.presenter.onComplete(result);
|
|
19970
20891
|
return result;
|
|
19971
20892
|
}
|
|
19972
|
-
|
|
19973
|
-
try {
|
|
19974
|
-
const writtenClis = syncAlanMcpSessionHeaders({
|
|
19975
|
-
conversationId: initialConfig.conversationId,
|
|
19976
|
-
teamId: initialConfig.teamId
|
|
19977
|
-
});
|
|
19978
|
-
if (writtenClis.length > 0) {
|
|
19979
|
-
void this.presenter.onLog(`Synced Alan MCP session headers to ${writtenClis.join(", ")}`);
|
|
19980
|
-
}
|
|
19981
|
-
} catch (error) {
|
|
19982
|
-
console.warn("[base-machine-agent] Failed to sync MCP session headers", error);
|
|
19983
|
-
}
|
|
19984
|
-
}
|
|
20893
|
+
this.mcpHeadersLogged = false;
|
|
19985
20894
|
try {
|
|
19986
20895
|
return await this.runCliBackend(initialConfig, safeProjectPath);
|
|
19987
20896
|
} catch (error) {
|
|
@@ -20550,13 +21459,20 @@ ${meta.description}`);
|
|
|
20550
21459
|
}
|
|
20551
21460
|
if (conversationId) artifactIds.push(`- conversationId: "${conversationId}"`);
|
|
20552
21461
|
if (artifactIds.length > 0) {
|
|
21462
|
+
const artifactTools = ["create_plan", "create_document_artifact", "create_test_report"];
|
|
21463
|
+
if (taskId || isWorkflowSession) artifactTools.push("upsert_prototype_version");
|
|
20553
21464
|
parts2.push(
|
|
20554
21465
|
[
|
|
20555
|
-
|
|
21466
|
+
`When creating artifacts using MCP tools (${artifactTools.join(", ")}), always pass these IDs:`,
|
|
20556
21467
|
...artifactIds
|
|
20557
21468
|
].join("\n")
|
|
20558
21469
|
);
|
|
20559
21470
|
}
|
|
21471
|
+
if (teamId && conversationId && !taskId && !isWorkflowSession) {
|
|
21472
|
+
parts2.push(
|
|
21473
|
+
"This is a standalone chat. For create_plan, create_document_artifact, and create_test_report, omit taskId and workflowId; conversationId is the artifact owner. Do not use create_document unless the user explicitly asks for a standalone team Document."
|
|
21474
|
+
);
|
|
21475
|
+
}
|
|
20560
21476
|
if (workflowId && conversationId && teamId) {
|
|
20561
21477
|
parts2.push(
|
|
20562
21478
|
[
|
|
@@ -20669,7 +21585,7 @@ function encryptedFileSize(entries) {
|
|
|
20669
21585
|
);
|
|
20670
21586
|
return emptyEnvelopeBytes + base64UrlLength(plaintextBytes);
|
|
20671
21587
|
}
|
|
20672
|
-
var EncryptedEventOutbox = class {
|
|
21588
|
+
var EncryptedEventOutbox = class _EncryptedEventOutbox {
|
|
20673
21589
|
constructor(path, encodedKey, pushLog = () => {
|
|
20674
21590
|
}, options = {}) {
|
|
20675
21591
|
this.path = path;
|
|
@@ -20684,6 +21600,11 @@ var EncryptedEventOutbox = class {
|
|
|
20684
21600
|
throw new Error("event outbox maxIntermediateEventsPerRun must be a non-negative integer");
|
|
20685
21601
|
}
|
|
20686
21602
|
this.entries = this.read();
|
|
21603
|
+
if (!(0, import_node_fs6.existsSync)(this.path)) {
|
|
21604
|
+
this.lastPersistedSignature = _EncryptedEventOutbox.EMPTY_SIGNATURE;
|
|
21605
|
+
} else if (this.entries.length > 0) {
|
|
21606
|
+
this.lastPersistedSignature = this.serializeForPersist().signature;
|
|
21607
|
+
}
|
|
20687
21608
|
this.enforceCaps();
|
|
20688
21609
|
this.persist();
|
|
20689
21610
|
}
|
|
@@ -20693,6 +21614,17 @@ var EncryptedEventOutbox = class {
|
|
|
20693
21614
|
retryTimer = null;
|
|
20694
21615
|
/** Latch so a persistent disk failure logs once per streak, not per event. */
|
|
20695
21616
|
persistFailureLogged = false;
|
|
21617
|
+
/**
|
|
21618
|
+
* Signature of the entries last durably written, so a `persist()` whose entries
|
|
21619
|
+
* are byte-identical to what is already on disk skips the AES-GCM re-encrypt +
|
|
21620
|
+
* temp-file write + rename entirely. Under link flapping the spool is persisted
|
|
21621
|
+
* on every enqueue/ack; this collapses redundant writes (e.g. a reconnect that
|
|
21622
|
+
* replays from head without mutating the queue) to O(delta), not O(spool). Set
|
|
21623
|
+
* to null on any write failure so the next persist retries rather than skips.
|
|
21624
|
+
*/
|
|
21625
|
+
lastPersistedSignature = null;
|
|
21626
|
+
/** Sentinel signature for the empty (no-entries) on-disk state. */
|
|
21627
|
+
static EMPTY_SIGNATURE = "empty";
|
|
20696
21628
|
key;
|
|
20697
21629
|
maxEncryptedBytes;
|
|
20698
21630
|
maxIntermediateEventsPerRun;
|
|
@@ -20895,20 +21827,28 @@ var EncryptedEventOutbox = class {
|
|
|
20895
21827
|
throw new Error(`unable to decrypt durable event outbox: ${detail}`);
|
|
20896
21828
|
}
|
|
20897
21829
|
}
|
|
21830
|
+
/** Serialize the current entries to their plaintext + content signature (once). */
|
|
21831
|
+
serializeForPersist() {
|
|
21832
|
+
if (this.entries.length === 0) {
|
|
21833
|
+
return { plaintext: "", signature: _EncryptedEventOutbox.EMPTY_SIGNATURE };
|
|
21834
|
+
}
|
|
21835
|
+
const plaintext = JSON.stringify({ entries: this.entries });
|
|
21836
|
+
return { plaintext, signature: (0, import_node_crypto.createHash)("sha256").update(plaintext).digest("base64url") };
|
|
21837
|
+
}
|
|
20898
21838
|
persist() {
|
|
21839
|
+
const { plaintext, signature } = this.serializeForPersist();
|
|
21840
|
+
if (signature === this.lastPersistedSignature) return;
|
|
20899
21841
|
try {
|
|
20900
21842
|
if (this.entries.length === 0) {
|
|
20901
21843
|
(0, import_node_fs6.rmSync)(this.path, { force: true });
|
|
21844
|
+
this.lastPersistedSignature = signature;
|
|
20902
21845
|
this.persistFailureLogged = false;
|
|
20903
21846
|
return;
|
|
20904
21847
|
}
|
|
20905
21848
|
(0, import_node_fs6.mkdirSync)((0, import_node_path5.dirname)(this.path), { recursive: true, mode: 448 });
|
|
20906
21849
|
const iv = (0, import_node_crypto.randomBytes)(12);
|
|
20907
21850
|
const cipher = (0, import_node_crypto.createCipheriv)("aes-256-gcm", this.key, iv);
|
|
20908
|
-
const ciphertext = Buffer.concat([
|
|
20909
|
-
cipher.update(JSON.stringify({ entries: this.entries }), "utf8"),
|
|
20910
|
-
cipher.final()
|
|
20911
|
-
]);
|
|
21851
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
20912
21852
|
const envelope = {
|
|
20913
21853
|
version: OUTBOX_VERSION,
|
|
20914
21854
|
iv: iv.toString("base64url"),
|
|
@@ -20923,8 +21863,10 @@ var EncryptedEventOutbox = class {
|
|
|
20923
21863
|
} finally {
|
|
20924
21864
|
(0, import_node_fs6.rmSync)(pendingPath, { force: true });
|
|
20925
21865
|
}
|
|
21866
|
+
this.lastPersistedSignature = signature;
|
|
20926
21867
|
this.persistFailureLogged = false;
|
|
20927
21868
|
} catch (error) {
|
|
21869
|
+
this.lastPersistedSignature = null;
|
|
20928
21870
|
if (!this.persistFailureLogged) {
|
|
20929
21871
|
this.persistFailureLogged = true;
|
|
20930
21872
|
const detail = error instanceof Error ? error.message : String(error);
|
|
@@ -21637,8 +22579,8 @@ var CONFIG_LOCK_STALE_MS = 3e4;
|
|
|
21637
22579
|
var CONFIG_LOCK_WAIT_MS = 2e3;
|
|
21638
22580
|
var CONFIG_LOCK_POLL_MS = 20;
|
|
21639
22581
|
function compactProviderVersion(output) {
|
|
21640
|
-
const
|
|
21641
|
-
return
|
|
22582
|
+
const firstLine2 = output.split(/\r?\n/).map((line) => line.trim()).find(Boolean);
|
|
22583
|
+
return firstLine2?.slice(0, 120);
|
|
21642
22584
|
}
|
|
21643
22585
|
var inspectAlanMcpThroughProviderCli = (backendKind, expectedUrl, home) => {
|
|
21644
22586
|
const plan = getMcpCliInspectionPlan(backendKind);
|
|
@@ -21678,7 +22620,30 @@ var inspectAlanMcpThroughProviderCli = (backendKind, expectedUrl, home) => {
|
|
|
21678
22620
|
};
|
|
21679
22621
|
}
|
|
21680
22622
|
const providerVersion = compactProviderVersion(versionProbe.stdout);
|
|
21681
|
-
const
|
|
22623
|
+
const inspectTimeoutMs = Number(process.env.ALAN_MCP_INSPECT_TIMEOUT_MS) || 2e4;
|
|
22624
|
+
const probe = runCliVersionProbe(executable, env, plan.args, inspectTimeoutMs);
|
|
22625
|
+
const timedOut = probe.status === null && (probe.signal === "SIGTERM" || probe.error?.code === "ETIMEDOUT");
|
|
22626
|
+
if (timedOut) {
|
|
22627
|
+
const fileCheck = verifyAlanMcpRegistered(backendKind, home, () => ({
|
|
22628
|
+
ok: true,
|
|
22629
|
+
verification: { method: "file" }
|
|
22630
|
+
}));
|
|
22631
|
+
console.warn("[alan-agent] MCP CLI inspection timed out; falling back to file check", {
|
|
22632
|
+
backendKind,
|
|
22633
|
+
command,
|
|
22634
|
+
inspectTimeoutMs,
|
|
22635
|
+
fileOk: fileCheck.ok
|
|
22636
|
+
});
|
|
22637
|
+
return {
|
|
22638
|
+
ok: fileCheck.ok,
|
|
22639
|
+
issue: fileCheck.ok ? void 0 : {
|
|
22640
|
+
code: "provider_mcp_repair_required",
|
|
22641
|
+
command,
|
|
22642
|
+
message: `Alan tools could not be verified for ${plan.providerLabel}. Repair Alan tools, then retry.`
|
|
22643
|
+
},
|
|
22644
|
+
verification: { method: "file", command, providerVersion }
|
|
22645
|
+
};
|
|
22646
|
+
}
|
|
21682
22647
|
const classification = classifyMcpCliInspection(
|
|
21683
22648
|
plan,
|
|
21684
22649
|
{
|
|
@@ -21771,9 +22736,7 @@ var CODEX_ALAN_TOML_SECTION = "mcp_servers.alan";
|
|
|
21771
22736
|
function isCodexAlanTomlSection(section) {
|
|
21772
22737
|
return section === CODEX_ALAN_TOML_SECTION || section.startsWith(`${CODEX_ALAN_TOML_SECTION}.`);
|
|
21773
22738
|
}
|
|
21774
|
-
function
|
|
21775
|
-
if (existingContent === null || existingContent.trim() === "") return desiredContent;
|
|
21776
|
-
assertValidExistingToml(path, existingContent);
|
|
22739
|
+
function stripCodexAlanSection(existingContent) {
|
|
21777
22740
|
const lines = existingContent.split("\n");
|
|
21778
22741
|
const retained = [];
|
|
21779
22742
|
let insideAlanSection = false;
|
|
@@ -21784,7 +22747,12 @@ function mergeCodexAlanSection(path, existingContent, desiredContent) {
|
|
|
21784
22747
|
}
|
|
21785
22748
|
if (!insideAlanSection) retained.push(line);
|
|
21786
22749
|
}
|
|
21787
|
-
|
|
22750
|
+
return retained.join("\n").trimEnd();
|
|
22751
|
+
}
|
|
22752
|
+
function mergeCodexAlanSection(path, existingContent, desiredContent) {
|
|
22753
|
+
if (existingContent === null || existingContent.trim() === "") return desiredContent;
|
|
22754
|
+
assertValidExistingToml(path, existingContent);
|
|
22755
|
+
const prefix = stripCodexAlanSection(existingContent);
|
|
21788
22756
|
const merged = `${prefix}${prefix ? "\n\n" : ""}${desiredContent.trim()}
|
|
21789
22757
|
`;
|
|
21790
22758
|
try {
|
|
@@ -21870,6 +22838,45 @@ function verifyAlanMcpRegistered(backendKind, home = (0, import_node_os6.homedir
|
|
|
21870
22838
|
verification: inspection.verification
|
|
21871
22839
|
};
|
|
21872
22840
|
}
|
|
22841
|
+
function removeAlanFromJsonConfig(path, existingContent) {
|
|
22842
|
+
const config = parseJsonObject2(path, existingContent);
|
|
22843
|
+
for (const containerKey of ["mcpServers", "mcp"]) {
|
|
22844
|
+
const container = config[containerKey];
|
|
22845
|
+
if (container && typeof container === "object" && !Array.isArray(container)) {
|
|
22846
|
+
delete container.alan;
|
|
22847
|
+
}
|
|
22848
|
+
}
|
|
22849
|
+
return JSON.stringify(config);
|
|
22850
|
+
}
|
|
22851
|
+
function unregisterAlanMcp(backendKind, home = (0, import_node_os6.homedir)()) {
|
|
22852
|
+
const removedPaths = [];
|
|
22853
|
+
const errors = [];
|
|
22854
|
+
const files = alanMcpConfigFilesForBackend(backendKind, {}, home);
|
|
22855
|
+
for (const file of files) {
|
|
22856
|
+
if (!(0, import_node_fs7.existsSync)(file.path)) continue;
|
|
22857
|
+
const releaseLock = waitForConfigLock(`${file.path}.alan-lock`);
|
|
22858
|
+
try {
|
|
22859
|
+
const current = (0, import_node_fs7.readFileSync)(file.path, "utf8");
|
|
22860
|
+
let next;
|
|
22861
|
+
if (file.path.endsWith(".toml")) {
|
|
22862
|
+
const stripped = stripCodexAlanSection(current);
|
|
22863
|
+
next = stripped ? `${stripped}
|
|
22864
|
+
` : "";
|
|
22865
|
+
} else {
|
|
22866
|
+
next = removeAlanFromJsonConfig(file.path, current);
|
|
22867
|
+
}
|
|
22868
|
+
if (next !== current) {
|
|
22869
|
+
atomicWriteManagedConfig(file.path, next);
|
|
22870
|
+
removedPaths.push(file.path);
|
|
22871
|
+
}
|
|
22872
|
+
} catch (err) {
|
|
22873
|
+
errors.push(err instanceof Error ? err.message : String(err));
|
|
22874
|
+
} finally {
|
|
22875
|
+
releaseLock();
|
|
22876
|
+
}
|
|
22877
|
+
}
|
|
22878
|
+
return { removedPaths, errors };
|
|
22879
|
+
}
|
|
21873
22880
|
function isHttpUrl(value2) {
|
|
21874
22881
|
if (typeof value2 !== "string") return false;
|
|
21875
22882
|
try {
|
|
@@ -22216,6 +23223,88 @@ function extractCodexDiscoveredModelIdsFromDebugJson(value2) {
|
|
|
22216
23223
|
}
|
|
22217
23224
|
return [...new Set(ids)];
|
|
22218
23225
|
}
|
|
23226
|
+
var OPENCODE_MODEL_ID_LINE_PATTERN = /^[a-z0-9][\w.-]*\/[\w.-]+$/i;
|
|
23227
|
+
var OPENCODE_KNOWN_VARIANT_KEYS = /* @__PURE__ */ new Set([
|
|
23228
|
+
"minimal",
|
|
23229
|
+
"none",
|
|
23230
|
+
"low",
|
|
23231
|
+
"medium",
|
|
23232
|
+
"high",
|
|
23233
|
+
"xhigh",
|
|
23234
|
+
"extra-high",
|
|
23235
|
+
"max",
|
|
23236
|
+
"ultra"
|
|
23237
|
+
]);
|
|
23238
|
+
function parseOpenCodeVerboseModelOutput(text) {
|
|
23239
|
+
const entries = [];
|
|
23240
|
+
const lines = text.split(/\r?\n/);
|
|
23241
|
+
let i = 0;
|
|
23242
|
+
while (i < lines.length) {
|
|
23243
|
+
const line = (lines[i] ?? "").trim();
|
|
23244
|
+
i += 1;
|
|
23245
|
+
if (!OPENCODE_MODEL_ID_LINE_PATTERN.test(line)) continue;
|
|
23246
|
+
let jsonStart = i;
|
|
23247
|
+
while (jsonStart < lines.length && (lines[jsonStart] ?? "").trim() === "") jsonStart += 1;
|
|
23248
|
+
if (jsonStart >= lines.length || !(lines[jsonStart] ?? "").trim().startsWith("{")) continue;
|
|
23249
|
+
let depth = 0;
|
|
23250
|
+
let inString = false;
|
|
23251
|
+
let escaped = false;
|
|
23252
|
+
let endLineIdx = -1;
|
|
23253
|
+
let buffer = "";
|
|
23254
|
+
for (let j = jsonStart; j < lines.length; j += 1) {
|
|
23255
|
+
const raw = lines[j] ?? "";
|
|
23256
|
+
buffer += (buffer ? "\n" : "") + raw;
|
|
23257
|
+
for (const ch of raw) {
|
|
23258
|
+
if (escaped) {
|
|
23259
|
+
escaped = false;
|
|
23260
|
+
continue;
|
|
23261
|
+
}
|
|
23262
|
+
if (ch === "\\" && inString) {
|
|
23263
|
+
escaped = true;
|
|
23264
|
+
continue;
|
|
23265
|
+
}
|
|
23266
|
+
if (ch === '"') {
|
|
23267
|
+
inString = !inString;
|
|
23268
|
+
continue;
|
|
23269
|
+
}
|
|
23270
|
+
if (inString) continue;
|
|
23271
|
+
if (ch === "{") depth += 1;
|
|
23272
|
+
else if (ch === "}") depth -= 1;
|
|
23273
|
+
}
|
|
23274
|
+
if (depth === 0) {
|
|
23275
|
+
endLineIdx = j;
|
|
23276
|
+
break;
|
|
23277
|
+
}
|
|
23278
|
+
}
|
|
23279
|
+
if (endLineIdx === -1) break;
|
|
23280
|
+
try {
|
|
23281
|
+
const obj = JSON.parse(buffer);
|
|
23282
|
+
entries.push({
|
|
23283
|
+
id: line,
|
|
23284
|
+
reasoning: obj.capabilities?.reasoning === true,
|
|
23285
|
+
variants: obj.variants ? Object.keys(obj.variants) : []
|
|
23286
|
+
});
|
|
23287
|
+
} catch {
|
|
23288
|
+
}
|
|
23289
|
+
i = endLineIdx + 1;
|
|
23290
|
+
}
|
|
23291
|
+
return entries;
|
|
23292
|
+
}
|
|
23293
|
+
function extractOpenCodeDiscoveredModelIdsFromVerboseOutput(text) {
|
|
23294
|
+
const entries = parseOpenCodeVerboseModelOutput(text);
|
|
23295
|
+
const ids = [];
|
|
23296
|
+
for (const entry of entries) {
|
|
23297
|
+
const knownVariants = entry.reasoning ? entry.variants.filter((variant) => OPENCODE_KNOWN_VARIANT_KEYS.has(variant)) : [];
|
|
23298
|
+
if (knownVariants.length === 0) {
|
|
23299
|
+
ids.push(entry.id);
|
|
23300
|
+
continue;
|
|
23301
|
+
}
|
|
23302
|
+
for (const variant of knownVariants) {
|
|
23303
|
+
ids.push(`${entry.id}-${variant}`);
|
|
23304
|
+
}
|
|
23305
|
+
}
|
|
23306
|
+
return [...new Set(ids)];
|
|
23307
|
+
}
|
|
22219
23308
|
function extractModelIdsFromText(value2) {
|
|
22220
23309
|
const ansiPattern = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
22221
23310
|
const ignoredLinePattern = /^(available models|models|usage|error|flag provided|try |agy\b|warning:)/i;
|
|
@@ -22326,12 +23415,24 @@ async function probeJsonOrTextModels(executable, env, argSets, options) {
|
|
|
22326
23415
|
return [];
|
|
22327
23416
|
}
|
|
22328
23417
|
async function discoverOpenCodeModels(executable, env) {
|
|
22329
|
-
const
|
|
23418
|
+
const verbose = await runCliProbeAsync(
|
|
23419
|
+
executable,
|
|
23420
|
+
withModelProbeEnv(env),
|
|
23421
|
+
["models", "--verbose"],
|
|
23422
|
+
MODEL_DISCOVERY_TIMEOUT_MS
|
|
23423
|
+
);
|
|
23424
|
+
if (verbose && !verbose.error && verbose.status === 0 && verbose.stdout.trim()) {
|
|
23425
|
+
const withEffort = extractOpenCodeDiscoveredModelIdsFromVerboseOutput(verbose.stdout);
|
|
23426
|
+
if (withEffort.length > 0) return withEffort;
|
|
23427
|
+
}
|
|
23428
|
+
const plain = await probeJsonOrTextModels(executable, env, [["models"]]);
|
|
23429
|
+
if (plain.length > 0) return plain;
|
|
23430
|
+
const legacy = await probeJsonOrTextModels(executable, env, [
|
|
22330
23431
|
["models", "--json"],
|
|
22331
23432
|
["models", "list", "--json"],
|
|
22332
23433
|
["model", "list", "--json"]
|
|
22333
23434
|
]);
|
|
22334
|
-
return
|
|
23435
|
+
return legacy.length > 0 ? legacy : OPENCODE_ZEN_MODEL_IDS;
|
|
22335
23436
|
}
|
|
22336
23437
|
async function discoverAntigravityModels(executable, env) {
|
|
22337
23438
|
return probeJsonOrTextModels(executable, env, [["models"]], {
|
|
@@ -22577,7 +23678,7 @@ var RunStartGate = class {
|
|
|
22577
23678
|
};
|
|
22578
23679
|
|
|
22579
23680
|
// src/version.ts
|
|
22580
|
-
var AGENT_VERSION = "0.1.
|
|
23681
|
+
var AGENT_VERSION = "0.1.44";
|
|
22581
23682
|
|
|
22582
23683
|
// src/workspace-relocation.ts
|
|
22583
23684
|
var import_node_child_process3 = require("child_process");
|
|
@@ -22653,11 +23754,19 @@ var import_node_child_process4 = require("child_process");
|
|
|
22653
23754
|
var import_node_fs11 = require("fs");
|
|
22654
23755
|
var import_node_path9 = require("path");
|
|
22655
23756
|
var WORKSPACE_LEASE_CHECK_INTERVAL_MS = 1e3;
|
|
23757
|
+
function parsePositiveMs(value2, fallback) {
|
|
23758
|
+
const parsed = value2 ? Number(value2) : Number.NaN;
|
|
23759
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
23760
|
+
}
|
|
23761
|
+
var WORKSPACE_LEASE_GIT_TIMEOUT_MS = parsePositiveMs(
|
|
23762
|
+
process.env.ALAN_WORKSPACE_LEASE_GIT_TIMEOUT_MS,
|
|
23763
|
+
2e3
|
|
23764
|
+
);
|
|
22656
23765
|
function gitValue(cwd, args) {
|
|
22657
23766
|
const result = (0, import_node_child_process4.spawnSync)("git", ["-C", cwd, ...args], {
|
|
22658
23767
|
encoding: "utf8",
|
|
22659
23768
|
env: getDaemonCliEnvironment(),
|
|
22660
|
-
timeout:
|
|
23769
|
+
timeout: WORKSPACE_LEASE_GIT_TIMEOUT_MS
|
|
22661
23770
|
});
|
|
22662
23771
|
if (result.status !== 0) return null;
|
|
22663
23772
|
return result.stdout.trim() || null;
|
|
@@ -22693,6 +23802,13 @@ var DEFAULT_PROBE2 = {
|
|
|
22693
23802
|
} catch {
|
|
22694
23803
|
return null;
|
|
22695
23804
|
}
|
|
23805
|
+
},
|
|
23806
|
+
pathExists: (workspacePath) => {
|
|
23807
|
+
try {
|
|
23808
|
+
return (0, import_node_fs11.existsSync)((0, import_node_path9.resolve)(workspacePath));
|
|
23809
|
+
} catch {
|
|
23810
|
+
return true;
|
|
23811
|
+
}
|
|
22696
23812
|
}
|
|
22697
23813
|
};
|
|
22698
23814
|
function unavailableFailure(target) {
|
|
@@ -22740,6 +23856,57 @@ var WorkspaceRunLease = class _WorkspaceRunLease {
|
|
|
22740
23856
|
failure: null
|
|
22741
23857
|
};
|
|
22742
23858
|
}
|
|
23859
|
+
/**
|
|
23860
|
+
* Classify the lease state for the run monitor.
|
|
23861
|
+
*
|
|
23862
|
+
* - `transient: true` — a null snapshot while the workspace path still exists:
|
|
23863
|
+
* the git probe timed out or failed transiently (OS throttling / IO
|
|
23864
|
+
* contention), NOT a vanished checkout. The monitor should retry (require N
|
|
23865
|
+
* consecutive transient failures) before declaring the workspace lost.
|
|
23866
|
+
* - `transient: false` — a definitive change: the path is genuinely missing
|
|
23867
|
+
* (ENOENT), or the workspace/repository/worktree/branch identity changed.
|
|
23868
|
+
* These are real and fail fast.
|
|
23869
|
+
*/
|
|
23870
|
+
verifyDetailed() {
|
|
23871
|
+
for (const captured of this.captured) {
|
|
23872
|
+
const current = this.probe.snapshot(captured.target.workspacePath);
|
|
23873
|
+
if (!current) {
|
|
23874
|
+
const pathGone = this.probe.pathExists?.(captured.target.workspacePath) === false;
|
|
23875
|
+
return { failure: unavailableFailure(captured.target), transient: !pathGone };
|
|
23876
|
+
}
|
|
23877
|
+
if (current.workspaceIdentity !== captured.identity.workspaceIdentity) {
|
|
23878
|
+
return {
|
|
23879
|
+
failure: {
|
|
23880
|
+
code: "workspace_lease_path_changed",
|
|
23881
|
+
message: "The selected workspace folder changed while the run was active. The run stopped before continuing; choose the correct folder and start a new run.",
|
|
23882
|
+
workspacePath: captured.target.workspacePath,
|
|
23883
|
+
expectedBranch: captured.identity.branch,
|
|
23884
|
+
actualBranch: current.branch
|
|
23885
|
+
},
|
|
23886
|
+
transient: false
|
|
23887
|
+
};
|
|
23888
|
+
}
|
|
23889
|
+
if (current.repositoryIdentity !== captured.identity.repositoryIdentity || current.worktreeIdentity !== captured.identity.worktreeIdentity) {
|
|
23890
|
+
return {
|
|
23891
|
+
failure: {
|
|
23892
|
+
code: "workspace_lease_repository_changed",
|
|
23893
|
+
message: "The workspace now points to a different repository or worktree. The run stopped before continuing; restore the original checkout and start a new run.",
|
|
23894
|
+
workspacePath: captured.target.workspacePath,
|
|
23895
|
+
expectedBranch: captured.identity.branch,
|
|
23896
|
+
actualBranch: current.branch
|
|
23897
|
+
},
|
|
23898
|
+
transient: false
|
|
23899
|
+
};
|
|
23900
|
+
}
|
|
23901
|
+
if (current.branch !== captured.identity.branch) {
|
|
23902
|
+
return {
|
|
23903
|
+
failure: branchFailure(captured.target, captured.identity.branch, current.branch),
|
|
23904
|
+
transient: false
|
|
23905
|
+
};
|
|
23906
|
+
}
|
|
23907
|
+
}
|
|
23908
|
+
return { failure: null, transient: false };
|
|
23909
|
+
}
|
|
22743
23910
|
verify() {
|
|
22744
23911
|
for (const captured of this.captured) {
|
|
22745
23912
|
const current = this.probe.snapshot(captured.target.workspacePath);
|
|
@@ -22769,6 +23936,40 @@ var WorkspaceRunLease = class _WorkspaceRunLease {
|
|
|
22769
23936
|
return null;
|
|
22770
23937
|
}
|
|
22771
23938
|
};
|
|
23939
|
+
var FRESH_LEASE_MONITOR_STATE = {
|
|
23940
|
+
transientStreak: 0,
|
|
23941
|
+
nextCheckAtMs: 0
|
|
23942
|
+
};
|
|
23943
|
+
function isLeaseCheckDue(state, nowMs) {
|
|
23944
|
+
return nowMs >= state.nextCheckAtMs;
|
|
23945
|
+
}
|
|
23946
|
+
function applyLeaseCheckResult(input) {
|
|
23947
|
+
const { detailed, nowMs, policy } = input;
|
|
23948
|
+
if (!detailed.failure) {
|
|
23949
|
+
return { action: "ok", state: { ...FRESH_LEASE_MONITOR_STATE } };
|
|
23950
|
+
}
|
|
23951
|
+
if (!detailed.transient) {
|
|
23952
|
+
return { action: "fail", state: input.state, failure: detailed.failure };
|
|
23953
|
+
}
|
|
23954
|
+
const transientStreak = input.state.transientStreak + 1;
|
|
23955
|
+
if (transientStreak >= policy.maxTransientFailures) {
|
|
23956
|
+
return {
|
|
23957
|
+
action: "fail",
|
|
23958
|
+
state: { transientStreak, nextCheckAtMs: input.state.nextCheckAtMs },
|
|
23959
|
+
failure: detailed.failure
|
|
23960
|
+
};
|
|
23961
|
+
}
|
|
23962
|
+
const backoffMs = Math.min(
|
|
23963
|
+
policy.backoffBaseMs * 2 ** (transientStreak - 1),
|
|
23964
|
+
policy.backoffMaxMs
|
|
23965
|
+
);
|
|
23966
|
+
return {
|
|
23967
|
+
action: "transient",
|
|
23968
|
+
state: { transientStreak, nextCheckAtMs: nowMs + backoffMs },
|
|
23969
|
+
failure: detailed.failure,
|
|
23970
|
+
backoffMs
|
|
23971
|
+
};
|
|
23972
|
+
}
|
|
22772
23973
|
|
|
22773
23974
|
// src/daemon.ts
|
|
22774
23975
|
var STAGING_API_URL = process.env.ALAN_STAGING_API_URL ?? "https://staging-api.tryalan.ai";
|
|
@@ -24087,9 +25288,12 @@ function reconcileActiveRuns(input) {
|
|
|
24087
25288
|
if (!staleByGrace && !staleByDeadPid) continue;
|
|
24088
25289
|
if (awaitingAsk && staleByGrace && !staleByDeadPid) continue;
|
|
24089
25290
|
if (staleByDeadPid && awaitingAsk) {
|
|
25291
|
+
entry.presenter.onComplete(buildAbortResult("provider_exited"));
|
|
24090
25292
|
entry.agent.kill();
|
|
24091
25293
|
input.activeAgents.delete(runId);
|
|
24092
|
-
input.pushLog?.(
|
|
25294
|
+
input.pushLog?.(
|
|
25295
|
+
`reconciled dead provider pid run=${runId} (awaiting ask \u2014 finalized for resume)`
|
|
25296
|
+
);
|
|
24093
25297
|
continue;
|
|
24094
25298
|
}
|
|
24095
25299
|
abortActiveAgent(entry, { reason: staleByDeadPid ? "provider_exited" : "runner_stalled" });
|
|
@@ -24618,6 +25822,21 @@ async function startDaemon(args) {
|
|
|
24618
25822
|
recentLogs.push(`${(/* @__PURE__ */ new Date()).toISOString()} ${message}`);
|
|
24619
25823
|
if (recentLogs.length > 200) recentLogs.splice(0, recentLogs.length - 200);
|
|
24620
25824
|
};
|
|
25825
|
+
const alanMcpRegisteredRuns = /* @__PURE__ */ new Set();
|
|
25826
|
+
const alanMcpRegisteredBackends = /* @__PURE__ */ new Set();
|
|
25827
|
+
const finalizeAlanMcpForRun = (runId) => {
|
|
25828
|
+
if (!alanMcpRegisteredRuns.delete(runId)) return;
|
|
25829
|
+
if (alanMcpRegisteredRuns.size > 0) return;
|
|
25830
|
+
for (const backend of alanMcpRegisteredBackends) {
|
|
25831
|
+
try {
|
|
25832
|
+
const { removedPaths } = unregisterAlanMcp(backend);
|
|
25833
|
+
if (removedPaths.length > 0) pushLog(`mcp-unregistered backend=${backend} run=${runId}`);
|
|
25834
|
+
} catch (err) {
|
|
25835
|
+
pushLog(`mcp-unregister-failed backend=${backend} ${err.message}`);
|
|
25836
|
+
}
|
|
25837
|
+
}
|
|
25838
|
+
alanMcpRegisteredBackends.clear();
|
|
25839
|
+
};
|
|
24621
25840
|
const workspaceAccessTracker = new WorkspaceAccessTracker();
|
|
24622
25841
|
const recheckWorkspaceIssue = () => {
|
|
24623
25842
|
const previousPath = workspaceAccessTracker.recoveryPath();
|
|
@@ -24830,6 +26049,10 @@ async function startDaemon(args) {
|
|
|
24830
26049
|
const versionRefresh = setInterval(() => {
|
|
24831
26050
|
void refreshProviderVersions();
|
|
24832
26051
|
}, VERSION_REFRESH_INTERVAL_MS);
|
|
26052
|
+
const parsedMaxTransient = Number(process.env.ALAN_WORKSPACE_LEASE_MAX_TRANSIENT_FAILURES);
|
|
26053
|
+
const WORKSPACE_LEASE_MAX_TRANSIENT_FAILURES = Number.isFinite(parsedMaxTransient) && parsedMaxTransient >= 1 ? Math.floor(parsedMaxTransient) : 3;
|
|
26054
|
+
const WORKSPACE_LEASE_FAILURE_BACKOFF_BASE_MS = 1e3;
|
|
26055
|
+
const WORKSPACE_LEASE_FAILURE_BACKOFF_MAX_MS = 15e3;
|
|
24833
26056
|
const stopRunForWorkspaceLease = (runId, entry, failure) => {
|
|
24834
26057
|
entry.stage = "failed";
|
|
24835
26058
|
if (!entry.presenter.failWorkspaceLease(failure)) return;
|
|
@@ -24837,15 +26060,39 @@ async function startDaemon(args) {
|
|
|
24837
26060
|
activeAgents.delete(runId);
|
|
24838
26061
|
pushLog(`workspace-lease-lost run=${runId} code=${failure.code}`);
|
|
24839
26062
|
};
|
|
24840
|
-
const
|
|
24841
|
-
|
|
24842
|
-
|
|
24843
|
-
|
|
24844
|
-
|
|
26063
|
+
const leaseCheckPolicy = {
|
|
26064
|
+
maxTransientFailures: WORKSPACE_LEASE_MAX_TRANSIENT_FAILURES,
|
|
26065
|
+
backoffBaseMs: WORKSPACE_LEASE_FAILURE_BACKOFF_BASE_MS,
|
|
26066
|
+
backoffMaxMs: WORKSPACE_LEASE_FAILURE_BACKOFF_MAX_MS
|
|
26067
|
+
};
|
|
26068
|
+
const verifyActiveWorkspaceLease = (runId, entry, nowMs) => {
|
|
26069
|
+
const lease = entry.workspaceLease;
|
|
26070
|
+
if (!lease) return true;
|
|
26071
|
+
const state = entry.leaseCheck ?? { ...FRESH_LEASE_MONITOR_STATE };
|
|
26072
|
+
entry.leaseCheck = state;
|
|
26073
|
+
if (!isLeaseCheckDue(state, nowMs)) return true;
|
|
26074
|
+
const decision = applyLeaseCheckResult({
|
|
26075
|
+
state,
|
|
26076
|
+
detailed: lease.verifyDetailed(),
|
|
26077
|
+
nowMs,
|
|
26078
|
+
policy: leaseCheckPolicy
|
|
26079
|
+
});
|
|
26080
|
+
entry.leaseCheck = decision.state;
|
|
26081
|
+
if (decision.action === "fail" && decision.failure) {
|
|
26082
|
+
stopRunForWorkspaceLease(runId, entry, decision.failure);
|
|
26083
|
+
return false;
|
|
26084
|
+
}
|
|
26085
|
+
if (decision.action === "transient" && decision.failure) {
|
|
26086
|
+
pushLog(
|
|
26087
|
+
`workspace-lease-transient run=${runId} streak=${decision.state.transientStreak}/${WORKSPACE_LEASE_MAX_TRANSIENT_FAILURES} code=${decision.failure.code} backoff_ms=${decision.backoffMs ?? 0}`
|
|
26088
|
+
);
|
|
26089
|
+
}
|
|
26090
|
+
return true;
|
|
24845
26091
|
};
|
|
24846
26092
|
const workspaceLeaseMonitor = setInterval(() => {
|
|
26093
|
+
const nowMs = Date.now();
|
|
24847
26094
|
for (const [runId, entry] of activeAgents) {
|
|
24848
|
-
verifyActiveWorkspaceLease(runId, entry);
|
|
26095
|
+
verifyActiveWorkspaceLease(runId, entry, nowMs);
|
|
24849
26096
|
}
|
|
24850
26097
|
}, WORKSPACE_LEASE_CHECK_INTERVAL_MS);
|
|
24851
26098
|
workspaceLeaseMonitor.unref?.();
|
|
@@ -25322,7 +26569,7 @@ async function startDaemon(args) {
|
|
|
25322
26569
|
},
|
|
25323
26570
|
() => {
|
|
25324
26571
|
const entry = activeAgents.get(payload.runId);
|
|
25325
|
-
return entry ? verifyActiveWorkspaceLease(payload.runId, entry) : false;
|
|
26572
|
+
return entry ? verifyActiveWorkspaceLease(payload.runId, entry, Date.now()) : false;
|
|
25326
26573
|
}
|
|
25327
26574
|
);
|
|
25328
26575
|
let agent;
|
|
@@ -25372,6 +26619,10 @@ async function startDaemon(args) {
|
|
|
25372
26619
|
workspaceLease: workspaceLeaseCapture.lease
|
|
25373
26620
|
});
|
|
25374
26621
|
pendingRunStarts.delete(payload.runId);
|
|
26622
|
+
if (payload.alanMcp && backendKind) {
|
|
26623
|
+
alanMcpRegisteredRuns.add(payload.runId);
|
|
26624
|
+
alanMcpRegisteredBackends.add(backendKind);
|
|
26625
|
+
}
|
|
25375
26626
|
void agent.run({
|
|
25376
26627
|
task: payload.content,
|
|
25377
26628
|
maxIterations: payload.maxIterations ?? 50,
|
|
@@ -25390,6 +26641,7 @@ async function startDaemon(args) {
|
|
|
25390
26641
|
taskMeta: payload.taskMeta,
|
|
25391
26642
|
prMeta: payload.prMeta,
|
|
25392
26643
|
conversationId: payload.conversationId,
|
|
26644
|
+
alanMcp: payload.alanMcp,
|
|
25393
26645
|
taskId: payload.taskId ?? payload.taskMeta?.id,
|
|
25394
26646
|
teamId: payload.teamId,
|
|
25395
26647
|
currentUser: payload.currentUser,
|
|
@@ -25416,6 +26668,7 @@ async function startDaemon(args) {
|
|
|
25416
26668
|
} catch {
|
|
25417
26669
|
pushLog(`run-journal-remove-failed run=${payload.runId}`);
|
|
25418
26670
|
}
|
|
26671
|
+
finalizeAlanMcpForRun(payload.runId);
|
|
25419
26672
|
releaseActiveRun();
|
|
25420
26673
|
});
|
|
25421
26674
|
});
|
|
@@ -25441,7 +26694,7 @@ async function startDaemon(args) {
|
|
|
25441
26694
|
([, entry]) => entry.conversationId === payload.conversationId
|
|
25442
26695
|
) : [...activeAgents.entries()];
|
|
25443
26696
|
const delivered = entries.some(([runId, entry]) => {
|
|
25444
|
-
if (!entry || !verifyActiveWorkspaceLease(runId, entry)) return false;
|
|
26697
|
+
if (!entry || !verifyActiveWorkspaceLease(runId, entry, Date.now())) return false;
|
|
25445
26698
|
return entry.agent.sendToolResponse(payload.toolId, payload.response) === true;
|
|
25446
26699
|
});
|
|
25447
26700
|
if (!delivered) {
|
|
@@ -26208,6 +27461,18 @@ function omitUndefined(record) {
|
|
|
26208
27461
|
return Object.fromEntries(Object.entries(record).filter(([, value2]) => value2 !== void 0));
|
|
26209
27462
|
}
|
|
26210
27463
|
|
|
27464
|
+
// src/resume-fallback-env.ts
|
|
27465
|
+
var RESUME_FALLBACK_CONTEXT_ENV = "ALAN_RESUME_FALLBACK_CONTEXT_B64";
|
|
27466
|
+
function decodeResumeFallbackContext(encoded) {
|
|
27467
|
+
if (!encoded) return void 0;
|
|
27468
|
+
try {
|
|
27469
|
+
const decoded = Buffer.from(encoded, "base64").toString("utf8").trim();
|
|
27470
|
+
return decoded.length > 0 ? decoded : void 0;
|
|
27471
|
+
} catch {
|
|
27472
|
+
return void 0;
|
|
27473
|
+
}
|
|
27474
|
+
}
|
|
27475
|
+
|
|
26211
27476
|
// src/web-presenter.ts
|
|
26212
27477
|
var WebPresenter = class {
|
|
26213
27478
|
constructor(wsClient, _conversationId, cwd) {
|
|
@@ -26500,6 +27765,7 @@ var SandboxEventDispatcher = class {
|
|
|
26500
27765
|
fireAndForget(event);
|
|
26501
27766
|
return;
|
|
26502
27767
|
}
|
|
27768
|
+
if (isBackgroundHeartbeat(event) && !this.outboxSocket.connected) return;
|
|
26503
27769
|
this.outbox.enqueue(this.conversationId, this.currentRunId ?? this.conversationId, event);
|
|
26504
27770
|
this.outbox.flush(this.outboxSocket);
|
|
26505
27771
|
}
|
|
@@ -26619,7 +27885,7 @@ var WSClient = class {
|
|
|
26619
27885
|
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
|
26620
27886
|
this.heartbeatTimer = setInterval(() => {
|
|
26621
27887
|
if (!this.socket.connected) return;
|
|
26622
|
-
this.socket.emit("agent.heartbeat", this.buildHeartbeat());
|
|
27888
|
+
this.socket.volatile.emit("agent.heartbeat", this.buildHeartbeat());
|
|
26623
27889
|
}, AGENT_HEARTBEAT_INTERVAL_MS);
|
|
26624
27890
|
this.heartbeatTimer.unref?.();
|
|
26625
27891
|
}
|
|
@@ -26702,6 +27968,7 @@ var WSClient = class {
|
|
|
26702
27968
|
images: data.images,
|
|
26703
27969
|
files: data.files,
|
|
26704
27970
|
providerSessionId: data.providerSessionId,
|
|
27971
|
+
resumeFallbackContext: data.resumeFallbackContext,
|
|
26705
27972
|
backendKind: data.backendKind,
|
|
26706
27973
|
agentId: data.agentId,
|
|
26707
27974
|
agentPrompt: data.agentPrompt,
|
|
@@ -26893,6 +28160,7 @@ async function runSandbox(config) {
|
|
|
26893
28160
|
const presenter = new WebPresenter(wsClient, config.sessionId, config.projectPath);
|
|
26894
28161
|
presenter.setSuppressSessionLifecycle(true);
|
|
26895
28162
|
let lastProviderSessionId = config.providerSessionId;
|
|
28163
|
+
let resumeFallbackContext = config.resumeFallbackContext;
|
|
26896
28164
|
let activeBackendKind = config.backendKind || "claude_cli";
|
|
26897
28165
|
let activeAgentId = config.agentId;
|
|
26898
28166
|
let activeAgentPrompt = config.agentPrompt;
|
|
@@ -26976,10 +28244,12 @@ async function runSandbox(config) {
|
|
|
26976
28244
|
selectedContextWindow: activeContextWindow ?? null,
|
|
26977
28245
|
selectedEffortLevel: activeEffortLevel ?? null,
|
|
26978
28246
|
providerSessionId: lastProviderSessionId ?? void 0,
|
|
28247
|
+
resumeFallbackContext,
|
|
26979
28248
|
taskMeta: currentTaskMeta,
|
|
26980
28249
|
prMeta: currentPrMeta,
|
|
26981
28250
|
taskId: currentTaskId,
|
|
26982
28251
|
conversationId: currentConversationId,
|
|
28252
|
+
alanMcp: currentAlanMcp,
|
|
26983
28253
|
teamId: currentTeamId,
|
|
26984
28254
|
workflowId,
|
|
26985
28255
|
workflowExecutionId,
|
|
@@ -27027,6 +28297,7 @@ async function runSandbox(config) {
|
|
|
27027
28297
|
}
|
|
27028
28298
|
}
|
|
27029
28299
|
currentAgent = null;
|
|
28300
|
+
resumeFallbackContext = void 0;
|
|
27030
28301
|
lifecycle.info("sandbox_agent_run_complete", {
|
|
27031
28302
|
success: result.success,
|
|
27032
28303
|
iterations: result.iterations,
|
|
@@ -27053,6 +28324,7 @@ async function runSandbox(config) {
|
|
|
27053
28324
|
currentFiles = nextPayload.files ?? [];
|
|
27054
28325
|
if (nextPayload.providerSessionId) {
|
|
27055
28326
|
lastProviderSessionId = nextPayload.providerSessionId;
|
|
28327
|
+
resumeFallbackContext = nextPayload.resumeFallbackContext;
|
|
27056
28328
|
}
|
|
27057
28329
|
if (nextPayload.backendKind) {
|
|
27058
28330
|
activeBackendKind = nextPayload.backendKind;
|
|
@@ -27383,7 +28655,7 @@ async function runSessionFromEnv() {
|
|
|
27383
28655
|
const task = process.env.ALAN_TASK;
|
|
27384
28656
|
const teamId = process.env.ALAN_TEAM_ID || void 0;
|
|
27385
28657
|
const projectPath = process.env.ALAN_PROJECT_PATH || "/home/user/workspace";
|
|
27386
|
-
const backendKind = process.env.ALAN_BACKEND_KIND || "
|
|
28658
|
+
const backendKind = process.env.ALAN_BACKEND_KIND || "codex_app_server";
|
|
27387
28659
|
const agentId = process.env.ALAN_AGENT_ID || void 0;
|
|
27388
28660
|
const agentPrompt = process.env.ALAN_AGENT_PROMPT || void 0;
|
|
27389
28661
|
const mode = process.env.ALAN_MODE || "agent";
|
|
@@ -27391,6 +28663,9 @@ async function runSessionFromEnv() {
|
|
|
27391
28663
|
const contextWindow = process.env.ALAN_CONTEXT_WINDOW || void 0;
|
|
27392
28664
|
const effortLevel = process.env.ALAN_EFFORT_LEVEL || void 0;
|
|
27393
28665
|
const providerSessionId = process.env.ALAN_PROVIDER_SESSION_ID || void 0;
|
|
28666
|
+
const resumeFallbackContext = decodeResumeFallbackContext(
|
|
28667
|
+
process.env[RESUME_FALLBACK_CONTEXT_ENV]
|
|
28668
|
+
);
|
|
27394
28669
|
const workflowId = process.env.ALAN_WORKFLOW_ID || void 0;
|
|
27395
28670
|
const workflowExecutionId = process.env.ALAN_WORKFLOW_EXECUTION_ID || void 0;
|
|
27396
28671
|
const sandboxId = process.env.ALAN_SANDBOX_ID || void 0;
|
|
@@ -27432,6 +28707,7 @@ async function runSessionFromEnv() {
|
|
|
27432
28707
|
contextWindow,
|
|
27433
28708
|
effortLevel,
|
|
27434
28709
|
providerSessionId,
|
|
28710
|
+
resumeFallbackContext,
|
|
27435
28711
|
workflowId,
|
|
27436
28712
|
workflowExecutionId,
|
|
27437
28713
|
sandboxId,
|