@sagentlab/navarch-runtime 0.1.121 → 0.1.123
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/README.md +12 -5
- package/dist/adapters/claude.cjs +77 -6
- package/dist/adapters/codex.cjs +66 -2
- package/dist/agent-model.cjs +80 -0
- package/dist/delivery.cjs +16 -2
- package/dist/exit-conditions.cjs +67 -3
- package/dist/prompt.cjs +44 -7
- package/dist/session.cjs +140 -54
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -278,16 +278,23 @@ instance during an automatic-update handoff.
|
|
|
278
278
|
Guidance added to a task that is already running is delivered on that
|
|
279
279
|
task's next lease heartbeat (every
|
|
280
280
|
`NAVARCH_LEASE_HEARTBEAT_INTERVAL_MS`, five minutes by default). The runtime
|
|
281
|
-
stops the current agent process and starts a **new agent turn
|
|
282
|
-
|
|
283
|
-
restart, not a new dispatch:
|
|
281
|
+
stops the current agent process and starts a **new agent turn**. This is a
|
|
282
|
+
turn restart, not a new dispatch:
|
|
284
283
|
|
|
285
284
|
- The task keeps the same session and lease, and lease heartbeats continue.
|
|
286
285
|
- The replacement turn uses the same worktree (and the same sandbox container
|
|
287
286
|
in Docker mode), so committed and uncommitted changes from the interrupted
|
|
288
287
|
turn remain available. It should inspect those changes before continuing.
|
|
289
|
-
-
|
|
290
|
-
|
|
288
|
+
- Claude Code and Codex resume the interrupted turn's CLI conversation
|
|
289
|
+
(`claude -p --resume <session>`, `codex exec resume <thread> -`) and are
|
|
290
|
+
sent only the task's one-line summary and the guidance that conversation has
|
|
291
|
+
not seen. Delivery-correction and completion-remediation turns resume the
|
|
292
|
+
same way. Every other agent, a turn that left no session id, and a resume
|
|
293
|
+
the CLI refuses before starting get a fresh invocation with the full task
|
|
294
|
+
context plus every guidance entry received since the claim. Guidance present
|
|
295
|
+
at claim time is already in the task context and is not repeated.
|
|
296
|
+
`prompt.md` in the session directory always holds what the last invocation
|
|
297
|
+
was actually sent.
|
|
291
298
|
- Transcript, token, and cost accounting is cumulative across turns: the
|
|
292
299
|
upload includes every turn under a separate label, and reported token and
|
|
293
300
|
cost totals include every turn. The lease is completed only after the
|
package/dist/adapters/claude.cjs
CHANGED
|
@@ -12,6 +12,12 @@ const FALLBACK_CLAUDE_MODEL = "claude-opus-5-5";
|
|
|
12
12
|
function hasFlag(args, flag) {
|
|
13
13
|
return args.some((arg) => arg === flag || arg.startsWith(`${flag}=`));
|
|
14
14
|
}
|
|
15
|
+
const CLAUDE_SESSION_ID = /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i;
|
|
16
|
+
/** Operator flags that pick the conversation themselves; Navarch then never resumes one. */
|
|
17
|
+
const OPERATOR_SESSION_FLAGS = ["--resume", "-r", "--continue", "-c", "--session-id", "--fork-session"];
|
|
18
|
+
function operatorOwnsSession(extraArgs) {
|
|
19
|
+
return OPERATOR_SESSION_FLAGS.some((flag) => hasFlag(extraArgs, flag));
|
|
20
|
+
}
|
|
15
21
|
/**
|
|
16
22
|
* Headless Claude Code adapter (project-plan.md §3.9 / implementation-plan.md
|
|
17
23
|
* WP-07): `claude -p --mcp-config platform-mcp.json --output-format stream-json --verbose`
|
|
@@ -53,13 +59,13 @@ async function runClaudeCodeAdapter(options) {
|
|
|
53
59
|
if (!parsed?.is_error || !(0, adapter_capacity_cjs_1.detectAdapterCapacityLimit)(primary))
|
|
54
60
|
return primary;
|
|
55
61
|
const canResume = typeof parsed.session_id === "string" &&
|
|
56
|
-
|
|
62
|
+
CLAUDE_SESSION_ID.test(parsed.session_id) &&
|
|
57
63
|
!hasFlag(options.extraArgs, "--no-session-persistence");
|
|
58
64
|
// Never replay a task that may already have side effects without its saved
|
|
59
65
|
// conversation. A structured zero-turn rejection is safe to start afresh.
|
|
60
66
|
if (!canResume && parsed.num_turns !== 0)
|
|
61
67
|
return primary;
|
|
62
|
-
if (
|
|
68
|
+
if (operatorOwnsSession(options.extraArgs))
|
|
63
69
|
return primary;
|
|
64
70
|
const fallback = await runClaudeAttempt({
|
|
65
71
|
...options,
|
|
@@ -68,9 +74,9 @@ async function runClaudeCodeAdapter(options) {
|
|
|
68
74
|
prompt: canResume
|
|
69
75
|
? "Continue the interrupted task from the saved conversation. Check existing work before taking further action."
|
|
70
76
|
: options.prompt,
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
77
|
+
// A zero-turn rejection without a session id reruns this same turn, which
|
|
78
|
+
// may itself be a resumed correction turn.
|
|
79
|
+
resumeSessionId: canResume ? parsed.session_id : options.resumeSessionId,
|
|
74
80
|
}, false);
|
|
75
81
|
return {
|
|
76
82
|
...fallback,
|
|
@@ -93,6 +99,19 @@ function addUsage(first, second) {
|
|
|
93
99
|
return first === undefined && second === undefined ? undefined : (first ?? 0) + (second ?? 0);
|
|
94
100
|
}
|
|
95
101
|
async function runClaudeAttempt(options, automaticFallback) {
|
|
102
|
+
if (options.resumeSessionId &&
|
|
103
|
+
(!CLAUDE_SESSION_ID.test(options.resumeSessionId) || operatorOwnsSession(options.extraArgs))) {
|
|
104
|
+
// Never hand an unvalidated id to argv, and never stack a second
|
|
105
|
+
// conversation selector on an operator's own. Nothing ran.
|
|
106
|
+
return {
|
|
107
|
+
exitCode: null,
|
|
108
|
+
timedOut: false,
|
|
109
|
+
killedByLeaseLoss: false,
|
|
110
|
+
stdout: "",
|
|
111
|
+
stderr: "Navarch did not resume the Claude session: the session id is invalid or the operator selects the conversation.",
|
|
112
|
+
resumeRejected: true,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
96
115
|
const args = ["-p"];
|
|
97
116
|
const hasSettingSources = options.extraArgs.some((arg) => arg === "--setting-sources" || arg.startsWith("--setting-sources="));
|
|
98
117
|
const hasExplicitPermissionMode = options.extraArgs.some((arg) => ["--permission-mode", "--permission-prompt-tool", "--dangerously-skip-permissions"].some((flag) => arg === flag || arg.startsWith(`${flag}=`)));
|
|
@@ -145,6 +164,8 @@ async function runClaudeAttempt(options, automaticFallback) {
|
|
|
145
164
|
args.push("--verbose");
|
|
146
165
|
}
|
|
147
166
|
args.push(...options.extraArgs);
|
|
167
|
+
if (options.resumeSessionId)
|
|
168
|
+
args.push("--resume", options.resumeSessionId);
|
|
148
169
|
// Project/task policy is appended after machine-wide extra arguments so a
|
|
149
170
|
// dispatched session consistently uses the settings recorded by Navarch.
|
|
150
171
|
if (options.model)
|
|
@@ -160,7 +181,54 @@ async function runClaudeAttempt(options, automaticFallback) {
|
|
|
160
181
|
}
|
|
161
182
|
: options;
|
|
162
183
|
const raw = runOptions.dockerExec ? await runViaDocker(runOptions, args) : await runOnHost(runOptions, args);
|
|
163
|
-
return attachUsage(raw);
|
|
184
|
+
return attachSession(attachUsage(raw), options);
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Reports the conversation this attempt ran in, so a later correction turn in
|
|
188
|
+
* the same lease can `--resume` it and send only what changed. Claude Code
|
|
189
|
+
* prints the session id on its first stream-json record (`system`/`init`) and
|
|
190
|
+
* again on the final result, so a turn cut short by guidance still has one.
|
|
191
|
+
* Nothing is reported when session persistence is off or an operator flag
|
|
192
|
+
* selects the conversation: there is then nothing Navarch may resume.
|
|
193
|
+
*
|
|
194
|
+
* `--resume` with an unknown id exits 1 with a zero-turn error result and no
|
|
195
|
+
* `init` record ("No conversation found with session ID"); that is the only
|
|
196
|
+
* case flagged `resumeRejected`, since no agent work can have started.
|
|
197
|
+
*/
|
|
198
|
+
function attachSession(result, options) {
|
|
199
|
+
let sessionId;
|
|
200
|
+
let started = false;
|
|
201
|
+
for (const line of result.stdout.split("\n")) {
|
|
202
|
+
const trimmed = line.trim();
|
|
203
|
+
if (!trimmed.startsWith("{"))
|
|
204
|
+
continue;
|
|
205
|
+
let record;
|
|
206
|
+
try {
|
|
207
|
+
record = JSON.parse(trimmed);
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (typeof record !== "object" || record === null || Array.isArray(record))
|
|
213
|
+
continue;
|
|
214
|
+
const { type, subtype, session_id: id, num_turns: turns } = record;
|
|
215
|
+
if ((type === "system" && subtype === "init") || type === "assistant" || type === "user" ||
|
|
216
|
+
(type === "result" && typeof turns === "number" && turns > 0)) {
|
|
217
|
+
started = true;
|
|
218
|
+
}
|
|
219
|
+
if (typeof id === "string" && CLAUDE_SESSION_ID.test(id) && (type === "system" || type === "result")) {
|
|
220
|
+
sessionId = id;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const resumable = !hasFlag(options.extraArgs, "--no-session-persistence") &&
|
|
224
|
+
!operatorOwnsSession(options.extraArgs);
|
|
225
|
+
const rejected = Boolean(options.resumeSessionId) && !started && result.exitCode !== 0 &&
|
|
226
|
+
!result.timedOut && !result.killedByLeaseLoss;
|
|
227
|
+
return {
|
|
228
|
+
...result,
|
|
229
|
+
...(resumable && sessionId && !rejected ? { agentSessionId: sessionId } : {}),
|
|
230
|
+
...(rejected ? { resumeRejected: true } : {}),
|
|
231
|
+
};
|
|
164
232
|
}
|
|
165
233
|
/** Parses the final `claude -p --output-format stream-json` result record for usage and folds it onto the raw result (best-effort; leaves tokensIn/tokensOut/costUsd unset when parsing fails). */
|
|
166
234
|
function attachUsage(result) {
|
|
@@ -282,5 +350,8 @@ function shellQuote(value) {
|
|
|
282
350
|
exports.claudeCodeAdapter = {
|
|
283
351
|
agentType: "claude-code",
|
|
284
352
|
run: runClaudeCodeAdapter,
|
|
353
|
+
resumesTurns: true,
|
|
354
|
+
// `usage` is per turn; `total_cost_usd` covers the whole resumed conversation.
|
|
355
|
+
cumulativeResumeUsage: ["costUsd"],
|
|
285
356
|
conversation: claude_conversation_cjs_1.claudeConversationRunner,
|
|
286
357
|
};
|
package/dist/adapters/codex.cjs
CHANGED
|
@@ -48,7 +48,22 @@ const kill_agent_cjs_1 = require("../kill-agent.cjs");
|
|
|
48
48
|
* approval-bypass flag) without an adapter code change.
|
|
49
49
|
*/
|
|
50
50
|
async function runCodexAdapter(options) {
|
|
51
|
-
const
|
|
51
|
+
const resumable = codexCanResume(options.extraArgs);
|
|
52
|
+
if (options.resumeSessionId && (!resumable || !CODEX_THREAD_ID.test(options.resumeSessionId))) {
|
|
53
|
+
return {
|
|
54
|
+
exitCode: null,
|
|
55
|
+
timedOut: false,
|
|
56
|
+
killedByLeaseLoss: false,
|
|
57
|
+
stdout: "",
|
|
58
|
+
stderr: "Navarch did not resume the Codex thread: the thread id is invalid or the operator arguments do not apply to `codex exec resume`.",
|
|
59
|
+
resumeRejected: true,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
// `codex exec resume <thread> -` continues a recorded thread with the
|
|
63
|
+
// stdin prompt (verified against codex-cli 0.163.0). Every other argument
|
|
64
|
+
// below is accepted by both forms, so the guard profile and MCP servers are
|
|
65
|
+
// re-applied identically on the resumed turn.
|
|
66
|
+
const args = options.resumeSessionId ? ["exec", "resume", options.resumeSessionId, "-"] : ["exec", "-"];
|
|
52
67
|
const env = { ...options.env };
|
|
53
68
|
if (options.mcpConfigPath)
|
|
54
69
|
args.push(...(await codexMcpArgs(options.mcpConfigPath, env)));
|
|
@@ -68,7 +83,53 @@ async function runCodexAdapter(options) {
|
|
|
68
83
|
const raw = runOptions.dockerExec
|
|
69
84
|
? await runViaDocker(runOptions, args)
|
|
70
85
|
: await runOnHost(runOptions, args);
|
|
71
|
-
return attachUsage(raw, options.model);
|
|
86
|
+
return attachThread(attachUsage(raw, options.model), options, resumable);
|
|
87
|
+
}
|
|
88
|
+
const CODEX_THREAD_ID = /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i;
|
|
89
|
+
/**
|
|
90
|
+
* `codex exec` options that `codex exec resume` does not accept (codex-cli
|
|
91
|
+
* 0.163.0 `--help`), plus `--ephemeral`, which records no thread to resume.
|
|
92
|
+
* An operator argument from this list keeps every turn a fresh `codex exec`.
|
|
93
|
+
*/
|
|
94
|
+
const CODEX_NON_RESUMABLE_ARGS = [
|
|
95
|
+
"--ephemeral",
|
|
96
|
+
"--sandbox",
|
|
97
|
+
"-s",
|
|
98
|
+
"--profile",
|
|
99
|
+
"-p",
|
|
100
|
+
"--cd",
|
|
101
|
+
"-C",
|
|
102
|
+
"--add-dir",
|
|
103
|
+
"--oss",
|
|
104
|
+
"--local-provider",
|
|
105
|
+
"--color",
|
|
106
|
+
"--approve-for-me",
|
|
107
|
+
];
|
|
108
|
+
function codexCanResume(extraArgs) {
|
|
109
|
+
return !extraArgs.some((arg) => CODEX_NON_RESUMABLE_ARGS.some((flag) => arg === flag || (flag.startsWith("--") && arg.startsWith(`${flag}=`))));
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Reports the thread this run recorded (`thread.started.thread_id`) so a later
|
|
113
|
+
* correction turn in the same lease can resume it. `codex exec resume` with an
|
|
114
|
+
* unknown id exits 1 before `turn.started` ("no rollout found for thread
|
|
115
|
+
* id"); only that shape is flagged `resumeRejected`, since no agent work ran.
|
|
116
|
+
*/
|
|
117
|
+
function attachThread(result, options, resumable) {
|
|
118
|
+
const events = (0, exit_conditions_cjs_1.parseCodexJsonEvents)(result.stdout);
|
|
119
|
+
const started = events.some((event) => event.type === "turn.started");
|
|
120
|
+
let threadId;
|
|
121
|
+
for (const event of events) {
|
|
122
|
+
const id = event.thread_id;
|
|
123
|
+
if (event.type === "thread.started" && typeof id === "string" && CODEX_THREAD_ID.test(id))
|
|
124
|
+
threadId = id;
|
|
125
|
+
}
|
|
126
|
+
const rejected = Boolean(options.resumeSessionId) && !started && result.exitCode !== 0 &&
|
|
127
|
+
!result.timedOut && !result.killedByLeaseLoss;
|
|
128
|
+
return {
|
|
129
|
+
...result,
|
|
130
|
+
...(resumable && threadId && !rejected ? { agentSessionId: threadId } : {}),
|
|
131
|
+
...(rejected ? { resumeRejected: true } : {}),
|
|
132
|
+
};
|
|
72
133
|
}
|
|
73
134
|
/** Operator policy wins over Navarch's generated host-mode profile. */
|
|
74
135
|
function hasExplicitPermissionPolicy(args) {
|
|
@@ -274,4 +335,7 @@ function shellQuote(value) {
|
|
|
274
335
|
exports.codexAdapter = {
|
|
275
336
|
agentType: "codex",
|
|
276
337
|
run: runCodexAdapter,
|
|
338
|
+
resumesTurns: true,
|
|
339
|
+
// `exec resume` reports the thread's total_token_usage, not the turn's.
|
|
340
|
+
cumulativeResumeUsage: ["tokensIn", "tokensOut", "cacheHitTokensIn", "costUsd"],
|
|
277
341
|
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.detectAgentModelUnavailable = detectAgentModelUnavailable;
|
|
4
|
+
/**
|
|
5
|
+
* OpenCode's words for "no configured provider serves this model": its JSON
|
|
6
|
+
* error event (`{"type":"error","error":{"type":"provider.no-route",
|
|
7
|
+
* "message":"Model unavailable: opencode-go/deepseek-v4.1-flash"}}`) and the
|
|
8
|
+
* matching ERROR log line (`"Failed to drain Session" cause
|
|
9
|
+
* "SessionRunnerModel.ModelUnavailableError: Model unavailable: ..."`).
|
|
10
|
+
* Verified against OpenCode 2.0.29 on flotilla-agent-1435 (2026-10-19).
|
|
11
|
+
*/
|
|
12
|
+
const NO_ROUTE_PATTERN = /\bprovider\.no-route\b|\bModelUnavailableError\b|\bModel unavailable:/i;
|
|
13
|
+
const MODEL_NAME_PATTERN = /\bModel unavailable:\s*([^\s"'\\,;)]+)/i;
|
|
14
|
+
function clip(text) {
|
|
15
|
+
const trimmed = text.trim().replace(/\s+/g, " ");
|
|
16
|
+
return trimmed.length > 300 ? `${trimmed.slice(0, 299)}…` : trimmed;
|
|
17
|
+
}
|
|
18
|
+
function failure(detail) {
|
|
19
|
+
const model = detail.match(MODEL_NAME_PATTERN)?.[1]?.replace(/[.:]+$/, "") ?? null;
|
|
20
|
+
return { model, detail: clip(detail) };
|
|
21
|
+
}
|
|
22
|
+
/** Messages of the CLI's own error events on stdout; never assistant text events. */
|
|
23
|
+
function errorEventMessages(stdout) {
|
|
24
|
+
const messages = [];
|
|
25
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
26
|
+
if (!line.trim().startsWith("{"))
|
|
27
|
+
continue;
|
|
28
|
+
let event;
|
|
29
|
+
try {
|
|
30
|
+
event = JSON.parse(line);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (event.type !== "error" && event.type !== "session.error")
|
|
36
|
+
continue;
|
|
37
|
+
const properties = event.properties;
|
|
38
|
+
const error = (event.error ?? properties?.error);
|
|
39
|
+
if (typeof error === "string") {
|
|
40
|
+
messages.push(error);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const message = error?.data?.message ?? error?.message;
|
|
44
|
+
const kind = [error?.type, error?.name].filter((value) => typeof value === "string").join(" ");
|
|
45
|
+
const text = [kind, typeof message === "string" ? message : ""].filter(Boolean).join(": ");
|
|
46
|
+
if (text)
|
|
47
|
+
messages.push(text);
|
|
48
|
+
}
|
|
49
|
+
return messages;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Detects a turn the coding agent CLI could not start because the model it
|
|
53
|
+
* was told to use has no route on this machine (OpenCode `provider.no-route`).
|
|
54
|
+
* Like a dead login (agent-auth.cts), every task the machine claims then fails
|
|
55
|
+
* the same way within seconds, so the control plane requeues the task without
|
|
56
|
+
* spending a retry and pauses the machine until the model is fixed.
|
|
57
|
+
*
|
|
58
|
+
* Matched only against CLI-authored channels — OpenCode's error events and
|
|
59
|
+
* ERROR log lines — and only for a turn that failed or produced no report, so
|
|
60
|
+
* an agent writing about this very error cannot pause its own machine.
|
|
61
|
+
*/
|
|
62
|
+
function detectAgentModelUnavailable(result) {
|
|
63
|
+
if (result.killedByLeaseLoss || result.timedOut || result.capacityError || result.authError)
|
|
64
|
+
return null;
|
|
65
|
+
if (result.exitCode === 0 && result.reportText)
|
|
66
|
+
return null;
|
|
67
|
+
for (const message of errorEventMessages(result.stdout)) {
|
|
68
|
+
if (NO_ROUTE_PATTERN.test(message))
|
|
69
|
+
return failure(message);
|
|
70
|
+
}
|
|
71
|
+
if (result.exitCode === 0)
|
|
72
|
+
return null;
|
|
73
|
+
for (const line of result.stderr.split(/\r?\n/)) {
|
|
74
|
+
if (/ERROR/.test(line) && NO_ROUTE_PATTERN.test(line)) {
|
|
75
|
+
const cause = line.match(/Model unavailable:[^"\n]*/i)?.[0];
|
|
76
|
+
return failure(cause ?? line);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
package/dist/delivery.cjs
CHANGED
|
@@ -24,7 +24,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
24
24
|
exports.probeWorktreeDelivery = probeWorktreeDelivery;
|
|
25
25
|
exports.evaluateDelivery = evaluateDelivery;
|
|
26
26
|
exports.renderDeliveryCorrectionPrompt = renderDeliveryCorrectionPrompt;
|
|
27
|
+
exports.renderResumedDeliveryCorrectionPrompt = renderResumedDeliveryCorrectionPrompt;
|
|
27
28
|
const sandbox_cjs_1 = require("./sandbox.cjs");
|
|
29
|
+
const prompt_cjs_1 = require("./prompt.cjs");
|
|
28
30
|
const GIT_TIMEOUT_MS = 30_000;
|
|
29
31
|
/**
|
|
30
32
|
* Reads the worktree's delivery state.
|
|
@@ -153,6 +155,19 @@ function renderDeliveryCorrectionPrompt(args) {
|
|
|
153
155
|
return [
|
|
154
156
|
args.originalPrompt.trim(),
|
|
155
157
|
"",
|
|
158
|
+
...deliveryIncompleteSection(args),
|
|
159
|
+
...(clippedReport ? ["", "### Previous turn's final report", "", clippedReport] : []),
|
|
160
|
+
].join("\n");
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* The resumed counterpart of renderDeliveryCorrectionPrompt: the brief and
|
|
164
|
+
* the previous turn's final report are both already in the conversation.
|
|
165
|
+
*/
|
|
166
|
+
function renderResumedDeliveryCorrectionPrompt(args) {
|
|
167
|
+
return [(0, prompt_cjs_1.renderResumedTurnPreamble)(args.task), "", ...deliveryIncompleteSection(args)].join("\n");
|
|
168
|
+
}
|
|
169
|
+
function deliveryIncompleteSection(args) {
|
|
170
|
+
return [
|
|
156
171
|
"## Delivery is incomplete",
|
|
157
172
|
"",
|
|
158
173
|
`Your previous turn ended without delivering the task: ${args.reason}.`,
|
|
@@ -181,6 +196,5 @@ function renderDeliveryCorrectionPrompt(args) {
|
|
|
181
196
|
"",
|
|
182
197
|
"Your final report replaces the previous one. Restate it in full in the required format, preserving",
|
|
183
198
|
"the verdict, reviewed head, findings, and validation limitations. Do not claim delivery you cannot verify.",
|
|
184
|
-
|
|
185
|
-
].join("\n");
|
|
199
|
+
];
|
|
186
200
|
}
|
package/dist/exit-conditions.cjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.AGENT_AUTH_FAILURE_PREFIX = void 0;
|
|
3
|
+
exports.AGENT_MODEL_UNAVAILABLE_PREFIX = exports.AGENT_AUTH_FAILURE_PREFIX = void 0;
|
|
4
4
|
exports.agentAuthFailureReport = agentAuthFailureReport;
|
|
5
|
+
exports.agentModelUnavailableReport = agentModelUnavailableReport;
|
|
5
6
|
exports.parseClaudeJsonResult = parseClaudeJsonResult;
|
|
6
7
|
exports.extractUsageFromClaudeJson = extractUsageFromClaudeJson;
|
|
7
8
|
exports.parseCodexJsonEvents = parseCodexJsonEvents;
|
|
@@ -10,6 +11,7 @@ exports.extractFinalMessageFromCodexEvents = extractFinalMessageFromCodexEvents;
|
|
|
10
11
|
exports.leadingVerificationVerdict = leadingVerificationVerdict;
|
|
11
12
|
exports.leadWithReviewFixMarker = leadWithReviewFixMarker;
|
|
12
13
|
exports.leadWithReviewAttestation = leadWithReviewAttestation;
|
|
14
|
+
exports.failureOutputText = failureOutputText;
|
|
13
15
|
exports.mapExitCondition = mapExitCondition;
|
|
14
16
|
/**
|
|
15
17
|
* Leading text of every report for a turn the coding agent CLI refused because
|
|
@@ -24,6 +26,19 @@ function agentAuthFailureReport(detail) {
|
|
|
24
26
|
return `${exports.AGENT_AUTH_FAILURE_PREFIX}: ${detail} ` +
|
|
25
27
|
"Log the coding agent in again on this machine (`claude /login`, `codex login`, an interactive `agy` session, or open `muse` and run `/login`), then resume the agent.";
|
|
26
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Leading text of every report for a turn the coding agent CLI could not start
|
|
31
|
+
* because the model it was given has no route on this machine (agent-model.cts,
|
|
32
|
+
* OpenCode `provider.no-route`). The control plane recognizes it and, as for a
|
|
33
|
+
* dead login, requeues the task without spending a retry while pausing this
|
|
34
|
+
* machine's claims (lib/navarch/failure-attribution.ts
|
|
35
|
+
* `provider.model_unavailable`).
|
|
36
|
+
*/
|
|
37
|
+
exports.AGENT_MODEL_UNAVAILABLE_PREFIX = "Coding agent model unavailable";
|
|
38
|
+
function agentModelUnavailableReport(detail) {
|
|
39
|
+
return `${exports.AGENT_MODEL_UNAVAILABLE_PREFIX}: ${detail} ` +
|
|
40
|
+
"Make that model reachable from the coding agent on this machine (log in to its provider, e.g. `opencode auth login`, or update the CLI), or connect the agent with a model it can reach, then resume the agent.";
|
|
41
|
+
}
|
|
27
42
|
/**
|
|
28
43
|
* Best-effort parse of `claude -p --output-format json` (or `stream-json`,
|
|
29
44
|
* whose last result record has the same shape) stdout into the assumed
|
|
@@ -298,6 +313,47 @@ function declaresBuildFailure(report) {
|
|
|
298
313
|
const firstLine = report.split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? "";
|
|
299
314
|
return /^(?:Build|Task) outcome:[*_`~]*\s*[*_`~]*(?:FAILED|BLOCKED)(?![a-z0-9_])/i.test(firstLine.replace(/^(?:#{1,6}\s+|>\s*|[*_`~]+\s*)+/, ""));
|
|
300
315
|
}
|
|
316
|
+
/**
|
|
317
|
+
* A CLI-authored error line: a JSON error event (OpenCode/Codex `error`,
|
|
318
|
+
* `session.error`, `turn.failed`), an ERROR/FATAL log line, or a leading
|
|
319
|
+
* `error:`/`fatal:`. Case-sensitive on the log levels so prose saying "error"
|
|
320
|
+
* stays where it was.
|
|
321
|
+
*/
|
|
322
|
+
function isFailureLine(line) {
|
|
323
|
+
return /(?:^|[^A-Za-z])(?:ERROR|FATAL)(?:[^A-Za-z]|$)/.test(line) ||
|
|
324
|
+
/"type"\s*:\s*"(?:error|session\.error|turn\.failed)"|^\s*(?:error|fatal)\b\s*[:\[]/i.test(line);
|
|
325
|
+
}
|
|
326
|
+
/** A JSON error event on stdout; text and tool events never qualify, whatever they say. */
|
|
327
|
+
function isErrorEventLine(line) {
|
|
328
|
+
if (!line.startsWith("{"))
|
|
329
|
+
return false;
|
|
330
|
+
try {
|
|
331
|
+
const type = JSON.parse(line)?.type;
|
|
332
|
+
return type === "error" || type === "session.error" || type === "turn.failed";
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* The failure output of a CLI that exited badly, error lines first. OpenCode
|
|
340
|
+
* prints its INFO startup log ("cli starting ... serve --stdio") ahead of the
|
|
341
|
+
* real error and puts its JSON error event on stdout, so summarizing stderr
|
|
342
|
+
* from the top showed only the INFO lines on the board (2026-10-19). The JSON
|
|
343
|
+
* error events on stdout are CLI-authored, unlike its text events, so they are
|
|
344
|
+
* safe to lift into the summary.
|
|
345
|
+
*/
|
|
346
|
+
function failureOutputText(stdout, stderr) {
|
|
347
|
+
const stdoutErrors = stdout
|
|
348
|
+
.split(/\r?\n/)
|
|
349
|
+
.map((line) => line.trim())
|
|
350
|
+
.filter(isErrorEventLine);
|
|
351
|
+
const lines = [...stdoutErrors, ...stderr.split(/\r?\n/)].map((line) => line.trim()).filter(Boolean);
|
|
352
|
+
const errors = lines.filter(isFailureLine);
|
|
353
|
+
if (errors.length === 0)
|
|
354
|
+
return stderr;
|
|
355
|
+
return [...errors, ...lines.filter((line) => !isFailureLine(line))].join("\n");
|
|
356
|
+
}
|
|
301
357
|
function summarize(text, maxLen = 500) {
|
|
302
358
|
const trimmed = text.trim();
|
|
303
359
|
if (!trimmed)
|
|
@@ -344,6 +400,14 @@ function mapExitCondition(result) {
|
|
|
344
400
|
evidenceUrls,
|
|
345
401
|
};
|
|
346
402
|
}
|
|
403
|
+
if (result.modelUnavailableError) {
|
|
404
|
+
return {
|
|
405
|
+
leaseOutcome: "failed",
|
|
406
|
+
exitStatus: "failed",
|
|
407
|
+
reportSummary: summarize(agentModelUnavailableReport(result.modelUnavailableError)),
|
|
408
|
+
evidenceUrls,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
347
411
|
if (result.timedOut) {
|
|
348
412
|
return {
|
|
349
413
|
leaseOutcome: "failed",
|
|
@@ -362,7 +426,7 @@ function mapExitCondition(result) {
|
|
|
362
426
|
return {
|
|
363
427
|
leaseOutcome: "failed",
|
|
364
428
|
exitStatus: "crashed",
|
|
365
|
-
reportSummary: summarize(result.stderr) || "Adapter process crashed before exiting.",
|
|
429
|
+
reportSummary: summarize(failureOutputText(result.stdout, result.stderr)) || "Adapter process crashed before exiting.",
|
|
366
430
|
evidenceUrls,
|
|
367
431
|
};
|
|
368
432
|
}
|
|
@@ -373,7 +437,7 @@ function mapExitCondition(result) {
|
|
|
373
437
|
return {
|
|
374
438
|
leaseOutcome: "failed",
|
|
375
439
|
exitStatus: "failed",
|
|
376
|
-
reportSummary: summarize(parsedJson?.result || result.stderr || result.stdout) ||
|
|
440
|
+
reportSummary: summarize(parsedJson?.result || failureOutputText(result.stdout, result.stderr) || result.stdout) ||
|
|
377
441
|
`Adapter exited with code ${result.exitCode}.`,
|
|
378
442
|
evidenceUrls,
|
|
379
443
|
};
|
package/dist/prompt.cjs
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.renderPrompt = renderPrompt;
|
|
4
4
|
exports.renderGuidanceCorrectionPrompt = renderGuidanceCorrectionPrompt;
|
|
5
|
+
exports.renderMidFlightGuidanceSection = renderMidFlightGuidanceSection;
|
|
6
|
+
exports.renderResumedTurnPreamble = renderResumedTurnPreamble;
|
|
7
|
+
exports.renderResumedGuidancePrompt = renderResumedGuidancePrompt;
|
|
5
8
|
exports.renderCompletionRemediationPrompt = renderCompletionRemediationPrompt;
|
|
9
|
+
exports.renderResumedCompletionRemediationPrompt = renderResumedCompletionRemediationPrompt;
|
|
6
10
|
exports.renderHostedCheckpointNote = renderHostedCheckpointNote;
|
|
7
11
|
const CODE_GRAPH_GUIDANCE = "For JS/TS dependency inspection, the runtime provides `navarch-runtime code-graph --query <symbol-or-file> --mode search|callers|callees|impact --commit HEAD`. If the runtime is not on PATH, use its installed entry point; do not install another copy mid-task. Results describe committed source and include commit/freshness warnings; inspect current files for uncommitted changes and unresolved dependencies.";
|
|
8
12
|
const PROCESS_GUIDANCE = "Process management: this host may run other agent sessions. Keep the PID or tool handle of each preview server you start and stop only that owned process. Never use broad process-name or command-line matching (pkill, killall) to clean up servers: the agent launcher carries the task prompt in its arguments and can match those patterns, terminating your own session. Use a different port when a port is occupied by a process you do not own. Keep scratch files in the current worktree; paths in previous transcripts belong to earlier sessions. Before your final report, inspect `git status --short`, including untracked files. Preserve required evidence in task artifacts, then remove only temporary files you created and restore only tracked edits you made solely for local checks. Do not delete unfamiliar or pre-existing changes, hide leftovers with ignore rules, or commit scratch files merely to make the worktree clean. Review tasks deliver a native review and attestation, never commits, pushes, or a new PR.";
|
|
@@ -64,9 +68,12 @@ function renderPrompt(task, bundle) {
|
|
|
64
68
|
* partial edits instead of discarding mid-flight work.
|
|
65
69
|
*/
|
|
66
70
|
function renderGuidanceCorrectionPrompt(originalPrompt, guidance) {
|
|
71
|
+
return [originalPrompt.trim(), renderMidFlightGuidanceSection(guidance)].join("\n");
|
|
72
|
+
}
|
|
73
|
+
/** The guidance section shared by the full-brief and resumed guidance turns. */
|
|
74
|
+
function renderMidFlightGuidanceSection(guidance) {
|
|
67
75
|
const corrections = guidance.map((entry) => `- **${entry.given_at}**: ${entry.content}`);
|
|
68
76
|
return [
|
|
69
|
-
originalPrompt.trim(),
|
|
70
77
|
"## Mid-flight human guidance",
|
|
71
78
|
"",
|
|
72
79
|
"The task was actively steered while you were working. Human guidance takes precedence over earlier context. Continue in the same worktree: inspect the current changes, correct course, and then finish the task.",
|
|
@@ -75,6 +82,24 @@ function renderGuidanceCorrectionPrompt(originalPrompt, guidance) {
|
|
|
75
82
|
"",
|
|
76
83
|
].join("\n");
|
|
77
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Opens every turn that resumes the previous agent conversation
|
|
87
|
+
* (`AgentAdapter.resumesTurns`). The brief is already in that conversation,
|
|
88
|
+
* so the turn names the task in one line and says the brief still applies
|
|
89
|
+
* instead of re-sending 35–80K characters of it. A turn that cannot resume
|
|
90
|
+
* uses the full-brief renderers, which keep #1978's fix: a fresh agent never
|
|
91
|
+
* receives a correction without its task.
|
|
92
|
+
*/
|
|
93
|
+
function renderResumedTurnPreamble(task) {
|
|
94
|
+
return [
|
|
95
|
+
`Task: ${task.summary} (Navarch task ${task.id}).`,
|
|
96
|
+
"This turn continues the conversation above. The full task brief earlier in this conversation still applies and is not repeated here.",
|
|
97
|
+
].join("\n");
|
|
98
|
+
}
|
|
99
|
+
/** A resumed guidance turn: only the guidance this conversation has not seen. */
|
|
100
|
+
function renderResumedGuidancePrompt(task, guidance) {
|
|
101
|
+
return [renderResumedTurnPreamble(task), "", renderMidFlightGuidanceSection(guidance)].join("\n");
|
|
102
|
+
}
|
|
78
103
|
/** Keeps a long previous report from crowding the brief out of the prompt. */
|
|
79
104
|
const REMEDIATION_REPORT_LIMIT = 8_000;
|
|
80
105
|
/**
|
|
@@ -93,21 +118,33 @@ function renderCompletionRemediationPrompt(args) {
|
|
|
93
118
|
return [
|
|
94
119
|
args.originalPrompt.trim(),
|
|
95
120
|
"",
|
|
121
|
+
...completionRejectedSection(args),
|
|
122
|
+
...(clipped
|
|
123
|
+
? ["", "### Previous turn's final report", "", clipped]
|
|
124
|
+
: []),
|
|
125
|
+
"",
|
|
126
|
+
].join("\n");
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The resumed counterpart of renderCompletionRemediationPrompt: the brief and
|
|
130
|
+
* the previous turn's final report are both already in the conversation.
|
|
131
|
+
*/
|
|
132
|
+
function renderResumedCompletionRemediationPrompt(args) {
|
|
133
|
+
return [renderResumedTurnPreamble(args.task), "", ...completionRejectedSection(args), ""].join("\n");
|
|
134
|
+
}
|
|
135
|
+
function completionRejectedSection(args) {
|
|
136
|
+
return [
|
|
96
137
|
"## Completion was rejected",
|
|
97
138
|
"",
|
|
98
139
|
"Your previous turn on this task finished, but the Navarch control plane rejected its completion:",
|
|
99
140
|
"",
|
|
100
141
|
...args.rejection.trim().split("\n").map((line) => `> ${line}`),
|
|
101
142
|
"",
|
|
102
|
-
`This is correction turn ${args.attempt} of ${args.maxAttempts}. The task
|
|
143
|
+
`This is correction turn ${args.attempt} of ${args.maxAttempts}. The task is unchanged, and you are in the`,
|
|
103
144
|
"same worktree, so earlier edits, commits, and branches are still here. Reuse the work and findings from",
|
|
104
145
|
"the previous turn instead of starting over, fix what the rejection names, and finish the task.",
|
|
105
146
|
"Your final report replaces the previous one, so restate it in full in the required format.",
|
|
106
|
-
|
|
107
|
-
? ["", "### Previous turn's final report", "", clipped]
|
|
108
|
-
: []),
|
|
109
|
-
"",
|
|
110
|
-
].join("\n");
|
|
147
|
+
];
|
|
111
148
|
}
|
|
112
149
|
/**
|
|
113
150
|
* Tells the agent its worktree starts from a hosted attempt's checkpoint
|
package/dist/session.cjs
CHANGED
|
@@ -31,6 +31,7 @@ const lease_heartbeat_cjs_1 = require("./lease-heartbeat.cjs");
|
|
|
31
31
|
const completion_retry_cjs_1 = require("./completion-retry.cjs");
|
|
32
32
|
const adapter_capacity_cjs_1 = require("./adapter-capacity.cjs");
|
|
33
33
|
const agent_auth_cjs_1 = require("./agent-auth.cjs");
|
|
34
|
+
const agent_model_cjs_1 = require("./agent-model.cjs");
|
|
34
35
|
const browser_evidence_cjs_1 = require("./browser-evidence.cjs");
|
|
35
36
|
/** Filename the generated platform MCP config is written under inside the session metadata directory. */
|
|
36
37
|
const MCP_CONFIG_FILENAME = "mcp-config.json";
|
|
@@ -529,7 +530,22 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
529
530
|
});
|
|
530
531
|
const agentBudgetStartedAt = node_perf_hooks_1.performance.now();
|
|
531
532
|
const maxRuntimeMs = task.max_runtime_ms ?? config.sessionTimeoutMs;
|
|
532
|
-
|
|
533
|
+
// A correction turn in two forms: `full` restates the task brief for a
|
|
534
|
+
// fresh agent; `resumed` is only the correction, for an adapter that
|
|
535
|
+
// continues the previous turn's conversation (AgentAdapter.resumesTurns).
|
|
536
|
+
let nextCorrection = null;
|
|
537
|
+
// The conversation the next turn may resume, and the mid-flight guidance
|
|
538
|
+
// that conversation has already been sent.
|
|
539
|
+
let resumableSessionId;
|
|
540
|
+
let conversationGuidanceIds = new Set();
|
|
541
|
+
// Running usage totals the resumable conversation last reported, for the
|
|
542
|
+
// fields its adapter reports cumulatively (AgentAdapter.cumulativeResumeUsage).
|
|
543
|
+
let conversationUsage = {};
|
|
544
|
+
// Claim-time guidance is part of the brief (task_context's "Human
|
|
545
|
+
// guidance" section, lib/navarch/context.ts), so guidance turns carry only
|
|
546
|
+
// what arrived after the claim.
|
|
547
|
+
const briefGuidanceIds = new Set((bundle.guidance ?? []).map((entry) => entry.id));
|
|
548
|
+
const resumedTask = { id: task.id, summary: task.summary };
|
|
533
549
|
let completionRemediationRetries = 0;
|
|
534
550
|
let deliveryCorrectionRetries = 0;
|
|
535
551
|
let deliveredPullRequest = false;
|
|
@@ -540,58 +556,85 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
540
556
|
// It is already included in deliveredGuidance, so clear the pending
|
|
541
557
|
// notification and start the first turn with the corrected prompt.
|
|
542
558
|
pendingGuidance = [];
|
|
543
|
-
|
|
559
|
+
const turnAbort = new AbortController();
|
|
560
|
+
activeAbortController = turnAbort;
|
|
544
561
|
if (leaseLost || deps.shutdownSignal?.aborted)
|
|
545
|
-
|
|
562
|
+
turnAbort.abort();
|
|
546
563
|
// The task brief plus any mid-flight guidance. Correction turns build on
|
|
547
564
|
// this rather than on the previous turn's prompt so they never nest.
|
|
548
|
-
const
|
|
549
|
-
|
|
565
|
+
const midFlightGuidance = deliveredGuidance.filter((entry) => !briefGuidanceIds.has(entry.id));
|
|
566
|
+
const briefPrompt = midFlightGuidance.length > 0
|
|
567
|
+
? (0, prompt_cjs_1.renderGuidanceCorrectionPrompt)(promptText, midFlightGuidance)
|
|
550
568
|
: promptText;
|
|
551
|
-
const
|
|
552
|
-
|
|
553
|
-
|
|
569
|
+
const fullPrompt = nextCorrection ? nextCorrection.full(briefPrompt) : briefPrompt;
|
|
570
|
+
// Resume only to deliver something new: a correction, or guidance this
|
|
571
|
+
// conversation has not been sent. The brief is already in it.
|
|
572
|
+
const unsentGuidance = midFlightGuidance.filter((entry) => !conversationGuidanceIds.has(entry.id));
|
|
573
|
+
const resumeSessionId = adapter.resumesTurns && (nextCorrection || unsentGuidance.length > 0)
|
|
574
|
+
? resumableSessionId
|
|
575
|
+
: undefined;
|
|
576
|
+
const resumedPrompt = resumeSessionId
|
|
577
|
+
? nextCorrection
|
|
578
|
+
? [nextCorrection.resumed, ...(unsentGuidance.length > 0 ? [(0, prompt_cjs_1.renderMidFlightGuidanceSection)(unsentGuidance)] : [])].join("\n\n")
|
|
579
|
+
: (0, prompt_cjs_1.renderResumedGuidancePrompt)(resumedTask, unsentGuidance)
|
|
580
|
+
: null;
|
|
581
|
+
nextCorrection = null;
|
|
554
582
|
adapterStarted = true;
|
|
555
583
|
adapterRunning = true;
|
|
556
584
|
agentTurnStartedAt = Date.now();
|
|
557
585
|
outputReportedAt = agentTurnStartedAt;
|
|
558
586
|
outputUpdates = 0;
|
|
559
587
|
reportActivity("running");
|
|
560
|
-
const
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
588
|
+
const runTurn = async (prompt, resumeId) => {
|
|
589
|
+
// prompt.md records exactly what this invocation is sent.
|
|
590
|
+
await node_fs_1.promises.writeFile(node_path_1.default.join(workDir, "prompt.md"), prompt, "utf8");
|
|
591
|
+
const remainingRuntimeMs = maxRuntimeMs > 0
|
|
592
|
+
? maxRuntimeMs - (node_perf_hooks_1.performance.now() - agentBudgetStartedAt)
|
|
593
|
+
: 0;
|
|
594
|
+
return maxRuntimeMs > 0 && remainingRuntimeMs <= 0
|
|
595
|
+
? { exitCode: null, timedOut: true, timeoutReason: "duration", killedByLeaseLoss: false, stdout: "", stderr: "" }
|
|
596
|
+
: await (0, adapter_watchdog_cjs_1.runWithActivityWatchdog)(adapter, {
|
|
597
|
+
prompt,
|
|
598
|
+
mcpConfigPath,
|
|
599
|
+
mcpMetadataDir: workDir,
|
|
600
|
+
bin,
|
|
601
|
+
extraArgs,
|
|
602
|
+
model: execution.model,
|
|
603
|
+
reasoningEffort: execution.reasoning_effort,
|
|
604
|
+
timeoutMs: remainingRuntimeMs,
|
|
605
|
+
env: sessionEnv,
|
|
606
|
+
settingsPath: claudeSettingsPath,
|
|
607
|
+
codexGuardArgs,
|
|
608
|
+
antigravityHostGuard: runtime === "antigravity" && config.sandboxMode === "host" && config.worktreeGuard,
|
|
609
|
+
antigravityResume,
|
|
610
|
+
museHostGuard: config.sandboxMode === "host" && config.worktreeGuard,
|
|
611
|
+
resumeSessionId: runtime === "muse" ? museSessionId : resumeId,
|
|
612
|
+
opencodeHostGuard: runtime === "opencode" && config.sandboxMode === "host" && config.worktreeGuard,
|
|
613
|
+
// ACP requires an absolute session cwd. Docker mounts the worktree at
|
|
614
|
+
// this same absolute path, while the other Docker adapters ignore cwd.
|
|
615
|
+
cwd: gitWorktree.worktreePath,
|
|
616
|
+
dockerExec: sandbox ? { containerName: sandbox.name, runner: sandbox_cjs_1.nodeCommandRunner } : undefined,
|
|
617
|
+
signal: turnAbort.signal,
|
|
618
|
+
onActivity: () => { outputObservedAt = Date.now(); outputUpdates += 1; },
|
|
619
|
+
onOutput: (stream, chunk) => {
|
|
620
|
+
if (stream !== "stdout")
|
|
621
|
+
return;
|
|
622
|
+
liveStdout += chunk;
|
|
623
|
+
liveTranscript?.markDirty();
|
|
624
|
+
},
|
|
625
|
+
}, task.idle_timeout_ms ?? config.sessionIdleTimeoutMs ?? 45 * 60_000, runtime === "opencode" ? config.opencodeStartupTimeoutMs : 0);
|
|
626
|
+
};
|
|
627
|
+
let turnResult = await runTurn(resumedPrompt ?? fullPrompt, resumeSessionId);
|
|
628
|
+
let resumed = Boolean(resumeSessionId);
|
|
629
|
+
if (resumeSessionId && turnResult.resumeRejected && !leaseLost && !turnAbort.signal.aborted) {
|
|
630
|
+
// The CLI refused the session before any work started (expired or
|
|
631
|
+
// unknown id). Nothing ran, so send the full brief to a fresh agent.
|
|
632
|
+
log.warn(`could not resume the agent conversation for ${leaseId} ` +
|
|
633
|
+
`(${(0, redact_cjs_1.redactText)(turnResult.stderr.trim().slice(0, 300), registry.list())}); re-sending the full task brief.`);
|
|
634
|
+
liveStdout = "";
|
|
635
|
+
turnResult = await runTurn(fullPrompt, undefined);
|
|
636
|
+
resumed = false;
|
|
637
|
+
}
|
|
595
638
|
activeAbortController = null;
|
|
596
639
|
adapterRunning = false;
|
|
597
640
|
if (turnResult.timedOut || leaseLost)
|
|
@@ -603,6 +646,34 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
603
646
|
antigravityResume = turnResult.antigravityResume;
|
|
604
647
|
if (runtime === "muse")
|
|
605
648
|
museSessionId = turnResult.agentSessionId;
|
|
649
|
+
if (adapter.cumulativeResumeUsage) {
|
|
650
|
+
// A resumed run reports the conversation's running total: record only
|
|
651
|
+
// this turn's increase. A fresh conversation starts its own total.
|
|
652
|
+
if (!resumed)
|
|
653
|
+
conversationUsage = {};
|
|
654
|
+
for (const key of adapter.cumulativeResumeUsage) {
|
|
655
|
+
const total = turnResult[key];
|
|
656
|
+
if (total === undefined)
|
|
657
|
+
continue;
|
|
658
|
+
turnResult[key] = Math.max(0, total - (conversationUsage[key] ?? 0));
|
|
659
|
+
conversationUsage[key] = total;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
if (adapter.resumesTurns) {
|
|
663
|
+
resumableSessionId = turnResult.resumeRejected ? undefined : turnResult.agentSessionId;
|
|
664
|
+
if (!resumableSessionId) {
|
|
665
|
+
conversationGuidanceIds = new Set();
|
|
666
|
+
}
|
|
667
|
+
else if (resumed) {
|
|
668
|
+
for (const entry of unsentGuidance)
|
|
669
|
+
conversationGuidanceIds.add(entry.id);
|
|
670
|
+
}
|
|
671
|
+
else {
|
|
672
|
+
// A fresh conversation was sent the full brief, which carries every
|
|
673
|
+
// mid-flight guidance entry received so far.
|
|
674
|
+
conversationGuidanceIds = new Set(midFlightGuidance.map((entry) => entry.id));
|
|
675
|
+
}
|
|
676
|
+
}
|
|
606
677
|
attempts.push(turnResult);
|
|
607
678
|
liveStdout = "";
|
|
608
679
|
liveTranscript.markDirty();
|
|
@@ -621,6 +692,13 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
621
692
|
turnResult.authError = authFailure.detail;
|
|
622
693
|
log.warn(`session ${leaseId}: coding agent authentication failed: ${authFailure.detail}`);
|
|
623
694
|
}
|
|
695
|
+
// A model the CLI has no route to (OpenCode `provider.no-route`) fails
|
|
696
|
+
// every task the same way, so it gets the same treatment as a dead login.
|
|
697
|
+
const modelUnavailable = capacityLimit || authFailure ? null : (0, agent_model_cjs_1.detectAgentModelUnavailable)(turnResult);
|
|
698
|
+
if (modelUnavailable) {
|
|
699
|
+
turnResult.modelUnavailableError = modelUnavailable.detail;
|
|
700
|
+
log.warn(`session ${leaseId}: coding agent model unavailable: ${modelUnavailable.detail}`);
|
|
701
|
+
}
|
|
624
702
|
// Close the small race between a naturally completed turn and the next
|
|
625
703
|
// scheduled heartbeat. If guidance landed, run another turn before the
|
|
626
704
|
// lease can be completed. A terminal heartbeat response means the
|
|
@@ -633,7 +711,7 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
633
711
|
log.warn(`session ${leaseId} stopped without completion because its lease is no longer active.`);
|
|
634
712
|
return sessionOutcome;
|
|
635
713
|
}
|
|
636
|
-
if (!capacityLimit && !authFailure && !leaseLost && !turnResult.timedOut && pendingGuidance.length > 0) {
|
|
714
|
+
if (!capacityLimit && !authFailure && !modelUnavailable && !leaseLost && !turnResult.timedOut && pendingGuidance.length > 0) {
|
|
637
715
|
// Muse cancellation kills the whole task-owned container. Guidance
|
|
638
716
|
// still holds the lease, so restart that SAME container and restore
|
|
639
717
|
// its tmpfs environment before the next turn; never launch into a
|
|
@@ -735,14 +813,18 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
735
813
|
deliveryCorrectionRetries += 1;
|
|
736
814
|
log.warn(`session ${leaseId} finished without delivering (${delivery.reason}); ` +
|
|
737
815
|
`restarting agent turn ${deliveryCorrectionRetries}/${DELIVERY_CORRECTION_RETRIES} in the same worktree.`);
|
|
738
|
-
|
|
739
|
-
originalPrompt: briefPrompt,
|
|
816
|
+
const correction = {
|
|
740
817
|
taskType: task.task_type,
|
|
741
|
-
previousReport: previousTurnReport(),
|
|
742
818
|
reason: delivery.reason,
|
|
743
819
|
attempt: deliveryCorrectionRetries,
|
|
744
820
|
maxAttempts: DELIVERY_CORRECTION_RETRIES,
|
|
745
|
-
}
|
|
821
|
+
};
|
|
822
|
+
const previousReport = previousTurnReport();
|
|
823
|
+
nextCorrection = {
|
|
824
|
+
full: (brief) => (0, delivery_cjs_1.renderDeliveryCorrectionPrompt)({ ...correction, originalPrompt: brief, previousReport }),
|
|
825
|
+
// The resumed conversation already holds the previous report.
|
|
826
|
+
resumed: (0, delivery_cjs_1.renderResumedDeliveryCorrectionPrompt)({ ...correction, task: resumedTask }),
|
|
827
|
+
};
|
|
746
828
|
continue;
|
|
747
829
|
}
|
|
748
830
|
await checkpointForRecovery(result, "incomplete delivery");
|
|
@@ -831,15 +913,19 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
831
913
|
if (remediable && completionRemediationRetries < COMPLETION_REMEDIATION_RETRIES) {
|
|
832
914
|
completionRemediationRetries += 1;
|
|
833
915
|
log.warn(`completion for ${leaseId} needs more work (${rejectionDetail}); restarting agent turn ${completionRemediationRetries}/${COMPLETION_REMEDIATION_RETRIES} in the same worktree.`);
|
|
834
|
-
// A
|
|
835
|
-
// reach an agent that has never seen the task.
|
|
836
|
-
|
|
837
|
-
|
|
916
|
+
// A fresh agent invocation needs the brief: the rejection alone would
|
|
917
|
+
// reach an agent that has never seen the task (#1978). A resumed
|
|
918
|
+
// conversation already holds the brief and the previous report.
|
|
919
|
+
const correction = {
|
|
838
920
|
rejection: redactedRejection,
|
|
839
|
-
previousReport: previousTurnReport(),
|
|
840
921
|
attempt: completionRemediationRetries,
|
|
841
922
|
maxAttempts: COMPLETION_REMEDIATION_RETRIES,
|
|
842
|
-
}
|
|
923
|
+
};
|
|
924
|
+
const previousReport = previousTurnReport();
|
|
925
|
+
nextCorrection = {
|
|
926
|
+
full: (brief) => (0, prompt_cjs_1.renderCompletionRemediationPrompt)({ ...correction, originalPrompt: brief, previousReport }),
|
|
927
|
+
resumed: (0, prompt_cjs_1.renderResumedCompletionRemediationPrompt)({ ...correction, task: resumedTask }),
|
|
928
|
+
};
|
|
843
929
|
continue;
|
|
844
930
|
}
|
|
845
931
|
if (remediable) {
|
package/package.json
CHANGED