@sagentlab/navarch-runtime 0.1.120 → 0.1.122

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/README.md +27 -5
  2. package/dist/adapters/claude.cjs +77 -6
  3. package/dist/adapters/codex.cjs +66 -2
  4. package/dist/adapters/opencode-providers.cjs +110 -0
  5. package/dist/adapters/opencode.cjs +9 -2
  6. package/dist/delivery.cjs +16 -2
  7. package/dist/prompt.cjs +44 -7
  8. package/dist/session.cjs +131 -53
  9. package/node_modules/jsonc-parser/CHANGELOG.md +76 -0
  10. package/node_modules/jsonc-parser/LICENSE.md +21 -0
  11. package/node_modules/jsonc-parser/README.md +364 -0
  12. package/node_modules/jsonc-parser/SECURITY.md +41 -0
  13. package/node_modules/jsonc-parser/lib/esm/impl/edit.js +185 -0
  14. package/node_modules/jsonc-parser/lib/esm/impl/format.js +261 -0
  15. package/node_modules/jsonc-parser/lib/esm/impl/parser.js +659 -0
  16. package/node_modules/jsonc-parser/lib/esm/impl/scanner.js +443 -0
  17. package/node_modules/jsonc-parser/lib/esm/impl/string-intern.js +29 -0
  18. package/node_modules/jsonc-parser/lib/esm/main.d.ts +351 -0
  19. package/node_modules/jsonc-parser/lib/esm/main.js +178 -0
  20. package/node_modules/jsonc-parser/lib/umd/impl/edit.js +201 -0
  21. package/node_modules/jsonc-parser/lib/umd/impl/format.js +275 -0
  22. package/node_modules/jsonc-parser/lib/umd/impl/parser.js +682 -0
  23. package/node_modules/jsonc-parser/lib/umd/impl/scanner.js +456 -0
  24. package/node_modules/jsonc-parser/lib/umd/impl/string-intern.js +42 -0
  25. package/node_modules/jsonc-parser/lib/umd/main.d.ts +351 -0
  26. package/node_modules/jsonc-parser/lib/umd/main.js +194 -0
  27. package/node_modules/jsonc-parser/package.json +37 -0
  28. package/package.json +5 -3
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
@@ -815,6 +822,21 @@ to OpenCode's remote-server config with OAuth disabled and environment-backed
815
822
  headers. Missing accounting fields stay absent rather than becoming a
816
823
  fabricated zero.
817
824
 
825
+ Host OpenCode sessions preserve operator provider definitions from
826
+ `$XDG_CONFIG_HOME/opencode/opencode.json(c)` (default
827
+ `~/.config/opencode/opencode.json(c)`), then `OPENCODE_CONFIG`, configs in
828
+ `OPENCODE_CONFIG_DIR`, and finally `OPENCODE_CONFIG_CONTENT`. JSONC comments
829
+ and trailing commas are supported. Nested provider settings merge in that
830
+ order; later arrays replace earlier arrays. Legacy `provider` entries work
831
+ with V1 and V2; native `providers` entries are included for V2. Custom model
832
+ IDs, metadata, and variants are preserved, so a pin such as
833
+ `--model magpie/openrouter/deepseek/deepseek-v4.1-flash-1017` works without
834
+ duplicating the provider configuration. API keys and header values become
835
+ child environment references, and file references resolve relative to their
836
+ original config file. Provider settings are the only imported configuration:
837
+ Navarch still generates the session's MCP and permissions and disables
838
+ project config discovery. Docker sessions do not import host providers.
839
+
818
840
  OpenCode also runs with `--print-logs --log-level ERROR` by default (see its
819
841
  [CLI flags](https://opencode.ai/docs/cli/#global-flags)), so provider errors
820
842
  that are missing from assistant messages still reach the runtime. A rate or
@@ -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,110 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.loadOpenCodeProviders = loadOpenCodeProviders;
7
+ const node_fs_1 = require("node:fs");
8
+ const node_os_1 = __importDefault(require("node:os"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const jsonc_parser_1 = require("jsonc-parser");
11
+ function isObject(value) {
12
+ return value !== null && typeof value === "object" && !Array.isArray(value);
13
+ }
14
+ function merge(base, override) {
15
+ return Object.fromEntries([...new Set([...Object.keys(base), ...Object.keys(override)])].map((key) => {
16
+ const previous = base[key];
17
+ const next = override[key];
18
+ if (!Object.hasOwn(override, key))
19
+ return [key, previous];
20
+ return [key, isObject(previous) && isObject(next) ? merge(previous, next) : next];
21
+ }));
22
+ }
23
+ /** Import operator providers only, before the adapter replaces OpenCode's paths.
24
+ * OpenCode V2 accepts legacy `provider` entries alongside native `providers`;
25
+ * let its own compatibility loader handle model capabilities and variants.
26
+ * Never discover repository config or import its plugins, MCP, or permissions.
27
+ */
28
+ async function loadOpenCodeProviders(env, generation) {
29
+ const setting = (name) => env[name] ?? process.env[name];
30
+ const home = setting("HOME") || node_os_1.default.homedir();
31
+ const expand = (file) => file.startsWith("~/") ? node_path_1.default.join(home, file.slice(2)) : file;
32
+ const globalDir = node_path_1.default.join(setting("XDG_CONFIG_HOME") || node_path_1.default.join(home, ".config"), "opencode");
33
+ let result = {};
34
+ let secretIndex = 0;
35
+ // Resolve file references against their original config, then keep credentials
36
+ // out of both the generated file and OPENCODE_CONFIG_CONTENT. Existing env
37
+ // references remain references and are resolved by the OpenCode child.
38
+ async function prepare(value, directory, sensitive = false) {
39
+ if (Array.isArray(value))
40
+ return Promise.all(value.map((item) => prepare(item, directory, sensitive)));
41
+ if (isObject(value)) {
42
+ return Object.fromEntries(await Promise.all(Object.entries(value).map(async ([key, item]) => [
43
+ key, await prepare(item, directory, sensitive || /^(?:api[_-]?key|token|password|secret|headers)$/i.test(key)),
44
+ ])));
45
+ }
46
+ if (typeof value !== "string")
47
+ return value;
48
+ const fromFile = /\{file:[^}]+\}/.test(value);
49
+ if (!fromFile && (!sensitive || !value || /^\{env:[^}]+\}$/.test(value)))
50
+ return value;
51
+ // Mixed strings such as "Bearer {env:TOKEN}" need expansion before moving
52
+ // the whole header into another env var; substitutions are not recursive.
53
+ let resolved = value.replace(/\{env:([^}]+)\}/g, (_match, name) => setting(name) ?? "");
54
+ for (const match of value.matchAll(/\{file:([^}]+)\}/g)) {
55
+ const file = node_path_1.default.resolve(directory, expand(match[1]));
56
+ const contents = (await node_fs_1.promises.readFile(file, "utf8")).trim();
57
+ resolved = resolved.replace(match[0], () => contents);
58
+ }
59
+ let name;
60
+ do {
61
+ name = `NAVARCH_OPENCODE_PROVIDER_${secretIndex++}`;
62
+ } while (setting(name) !== undefined);
63
+ env[name] = resolved;
64
+ return `{env:${name}}`;
65
+ }
66
+ async function include(text, source, directory) {
67
+ const errors = [];
68
+ const config = (0, jsonc_parser_1.parse)(text, errors, { allowTrailingComma: true });
69
+ if (errors.length || !isObject(config)) {
70
+ // Parser diagnostics can contain literal credentials; report only source.
71
+ throw new Error(`Invalid OpenCode JSON/JSONC configuration in ${source}.`);
72
+ }
73
+ const picked = {};
74
+ for (const key of generation === 1 ? ["provider"] : ["provider", "providers"]) {
75
+ if (config[key] === undefined)
76
+ continue;
77
+ if (!isObject(config[key]))
78
+ throw new Error(`OpenCode ${key} must be an object in ${source}.`);
79
+ picked[key] = await prepare(config[key], directory);
80
+ }
81
+ result = merge(result, picked);
82
+ }
83
+ async function file(filePath, optional) {
84
+ const absolute = node_path_1.default.resolve(expand(filePath));
85
+ let text;
86
+ try {
87
+ text = await node_fs_1.promises.readFile(absolute, "utf8");
88
+ }
89
+ catch (error) {
90
+ if (optional && error.code === "ENOENT")
91
+ return;
92
+ throw error;
93
+ }
94
+ await include(text, absolute, node_path_1.default.dirname(absolute));
95
+ }
96
+ for (const name of ["opencode.json", "opencode.jsonc"])
97
+ await file(node_path_1.default.join(globalDir, name), true);
98
+ const explicit = setting("OPENCODE_CONFIG");
99
+ if (explicit)
100
+ await file(explicit, false);
101
+ const directory = setting("OPENCODE_CONFIG_DIR");
102
+ if (directory) {
103
+ for (const name of ["opencode.json", "opencode.jsonc"])
104
+ await file(node_path_1.default.join(expand(directory), name), true);
105
+ }
106
+ const inline = setting("OPENCODE_CONFIG_CONTENT");
107
+ if (inline)
108
+ await include(inline, "OPENCODE_CONFIG_CONTENT", process.cwd());
109
+ return result;
110
+ }
@@ -12,6 +12,7 @@ const node_os_1 = __importDefault(require("node:os"));
12
12
  const node_path_1 = __importDefault(require("node:path"));
13
13
  const config_cjs_1 = require("../config.cjs");
14
14
  const kill_agent_cjs_1 = require("../kill-agent.cjs");
15
+ const opencode_providers_cjs_1 = require("./opencode-providers.cjs");
15
16
  /**
16
17
  * Headless OpenCode adapter, fixture-pinned to the OpenCode JSON event
17
18
  * contract shared by V1 and V2 (verified against 1.19.1, 2.0.6 and 2.0.17):
@@ -34,8 +35,9 @@ const kill_agent_cjs_1 = require("../kill-agent.cjs");
34
35
  * so each Navarch session gets an isolated XDG/config directory, disables
35
36
  * project discovery, and supplies its complete policy as both a private file
36
37
  * and the highest-precedence inline config. Provider credentials remain in
37
- * OpenCode's data store or the child environment; only MCP env references are
38
- * written to disk.
38
+ * OpenCode's data store or the child environment. Operator provider definitions
39
+ * are copied without importing unrelated global config; credential and MCP
40
+ * values use env references in the generated file.
39
41
  */
40
42
  async function runOpenCodeAdapter(options) {
41
43
  if (options.opencodeHostGuard && !options.dockerExec) {
@@ -178,10 +180,14 @@ async function prepareOpenCodeConfig(mcpConfigPath, env, inDocker, generation) {
178
180
  };
179
181
  }
180
182
  const managedDirectories = await openCodeManagedDirectories(env, inDocker);
183
+ // Host provider endpoints and credentials belong to the operator's host.
184
+ // Docker continues to use its own provider setup, never the host's files.
185
+ const providers = inDocker ? {} : await (0, opencode_providers_cjs_1.loadOpenCodeProviders)(env, generation);
181
186
  // Same policy in both dialects; in each the last matching rule wins.
182
187
  const config = generation === 1
183
188
  ? {
184
189
  $schema: "https://opencode.ai/config.json",
190
+ ...providers,
185
191
  autoupdate: false,
186
192
  share: "disabled",
187
193
  plugin: [],
@@ -197,6 +203,7 @@ async function prepareOpenCodeConfig(mcpConfigPath, env, inDocker, generation) {
197
203
  }
198
204
  : {
199
205
  $schema: "https://opencode.ai/config.json",
206
+ ...providers,
200
207
  update: "disable",
201
208
  share: "disabled",
202
209
  plugins: [],
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
  }
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