@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 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** with the
282
- original task context plus all guidance received so far. This is a turn
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
- - The replacement turn is not a resumed agent CLI conversation.
290
- The corrected prompt carries the prior task context and guidance instead.
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
@@ -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
- /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i.test(parsed.session_id) &&
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 (["--resume", "-r", "--continue", "-c", "--session-id", "--fork-session"].some((flag) => hasFlag(options.extraArgs, flag)))
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
- extraArgs: canResume
72
- ? [...options.extraArgs, "--resume", parsed.session_id]
73
- : options.extraArgs,
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
  };
@@ -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 args = ["exec", "-"];
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
- ...(clippedReport ? ["", "### Previous turn's final report", "", clippedReport] : []),
185
- ].join("\n");
199
+ ];
186
200
  }
@@ -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 above is unchanged, and you are in the`,
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
- ...(clipped
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
- let nextPrompt = null;
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
- activeAbortController = new AbortController();
559
+ const turnAbort = new AbortController();
560
+ activeAbortController = turnAbort;
544
561
  if (leaseLost || deps.shutdownSignal?.aborted)
545
- activeAbortController.abort();
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 briefPrompt = deliveredGuidance.length > (bundle.guidance?.length ?? 0)
549
- ? (0, prompt_cjs_1.renderGuidanceCorrectionPrompt)(promptText, deliveredGuidance)
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 runPrompt = nextPrompt ?? briefPrompt;
552
- nextPrompt = null;
553
- await node_fs_1.promises.writeFile(node_path_1.default.join(workDir, "prompt.md"), runPrompt, "utf8");
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 remainingRuntimeMs = maxRuntimeMs > 0
561
- ? maxRuntimeMs - (node_perf_hooks_1.performance.now() - agentBudgetStartedAt)
562
- : 0;
563
- const turnResult = maxRuntimeMs > 0 && remainingRuntimeMs <= 0
564
- ? { exitCode: null, timedOut: true, timeoutReason: "duration", killedByLeaseLoss: false, stdout: "", stderr: "" }
565
- : await (0, adapter_watchdog_cjs_1.runWithActivityWatchdog)(adapter, {
566
- prompt: runPrompt,
567
- mcpConfigPath,
568
- mcpMetadataDir: workDir,
569
- bin,
570
- extraArgs,
571
- model: execution.model,
572
- reasoningEffort: execution.reasoning_effort,
573
- timeoutMs: remainingRuntimeMs,
574
- env: sessionEnv,
575
- settingsPath: claudeSettingsPath,
576
- codexGuardArgs,
577
- antigravityHostGuard: runtime === "antigravity" && config.sandboxMode === "host" && config.worktreeGuard,
578
- antigravityResume,
579
- museHostGuard: config.sandboxMode === "host" && config.worktreeGuard,
580
- resumeSessionId: runtime === "muse" ? museSessionId : undefined,
581
- opencodeHostGuard: runtime === "opencode" && config.sandboxMode === "host" && config.worktreeGuard,
582
- // ACP requires an absolute session cwd. Docker mounts the worktree at
583
- // this same absolute path, while the other Docker adapters ignore cwd.
584
- cwd: gitWorktree.worktreePath,
585
- dockerExec: sandbox ? { containerName: sandbox.name, runner: sandbox_cjs_1.nodeCommandRunner } : undefined,
586
- signal: activeAbortController.signal,
587
- onActivity: () => { outputObservedAt = Date.now(); outputUpdates += 1; },
588
- onOutput: (stream, chunk) => {
589
- if (stream !== "stdout")
590
- return;
591
- liveStdout += chunk;
592
- liveTranscript?.markDirty();
593
- },
594
- }, task.idle_timeout_ms ?? config.sessionIdleTimeoutMs ?? 45 * 60_000, runtime === "opencode" ? config.opencodeStartupTimeoutMs : 0);
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
- nextPrompt = (0, delivery_cjs_1.renderDeliveryCorrectionPrompt)({
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 new turn is a fresh agent invocation: the rejection alone would
835
- // reach an agent that has never seen the task.
836
- nextPrompt = (0, prompt_cjs_1.renderCompletionRemediationPrompt)({
837
- originalPrompt: briefPrompt,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sagentlab/navarch-runtime",
3
- "version": "0.1.121",
3
+ "version": "0.1.123",
4
4
  "description": "Navarch machine-side session manager: claims delivery tasks and runs them through local coding agents.",
5
5
  "type": "commonjs",
6
6
  "license": "MIT",