@sagentlab/navarch-runtime 0.1.85 → 0.1.87

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
@@ -677,21 +677,30 @@ not the account's subscription charge or long-context/fast-mode uplifts.
677
677
 
678
678
 
679
679
  The Codex CLI invocation was verified against `codex-cli 0.144.1` on
680
- 2026-07-18. The runtime uses `codex exec "<prompt>" --json` and translates
680
+ 2026-07-18. The runtime uses `codex exec - --json` and translates
681
681
  the existing per-session MCP JSON into one-off `-c mcp_servers.*` overrides.
682
682
  Machine and lease credentials are referenced through environment variables,
683
683
  not placed in argv. The JSONL parser accepts the verified top-level
684
684
  `item.completed` / `turn.completed` shape and retains the older `msg`
685
685
  envelope as a compatibility fallback.
686
686
 
687
- The Gemini adapter uses official headless `--prompt` and `--output-format
688
- stream-json` flags. It resets pre-tool assistant text, retains the final
687
+ The Gemini adapter runs headlessly with `--output-format stream-json` and the
688
+ prompt piped on stdin, which Gemini uses as the prompt when stdin is not a
689
+ TTY. It resets pre-tool assistant text, retains the final
689
690
  post-tool answer as the report, and reads aggregate `input_tokens` and
690
691
  `output_tokens` from the terminal `result` event. Gemini does not report USD
691
692
  cost, so an unknown value remains absent rather than becoming zero.
692
693
 
693
- The OpenCode adapter uses `opencode run "<prompt>" --format json`, closes
694
- stdin immediately, and normalizes `text` and `step_finish` JSONL events into
694
+ Gemini's native sandbox launcher converts piped stdin back into a `--prompt`
695
+ argument for its child. The adapter rejects prompts of 128 KiB or more when
696
+ that sandbox is enabled, with an explicit diagnostic; reducing context or
697
+ selecting another runtime preserves the worktree boundary. Navarch Docker
698
+ mode disables the nested Gemini sandbox by default and can still pipe larger
699
+ prompts. All Gemini runs reject prompts above 8 MiB because Gemini's stdin
700
+ reader silently truncates input beyond that limit. These checks use UTF-8 bytes.
701
+
702
+ The OpenCode adapter uses `opencode run --format json` with the prompt on
703
+ stdin, and normalizes `text` and `step_finish` JSONL events into
695
704
  the shared report/token/cost boundary. It translates the lease MCP document
696
705
  to OpenCode's remote-server config with OAuth disabled and environment-backed
697
706
  headers. Missing accounting fields stay absent rather than becoming a
@@ -716,6 +725,15 @@ ships that server as `dsh --profile acp`. This is intentionally not the
716
725
  REST-based [Agent Communication Protocol](https://agentcommunicationprotocol.dev),
717
726
  which is a different protocol now maintained as part of A2A.
718
727
 
728
+ Every CLI adapter writes the prompt to the agent's stdin, never argv. Linux
729
+ rejects any single argv or environment string over 128 KiB
730
+ (`MAX_ARG_STRLEN`), so an argv prompt made large review sessions fail with
731
+ `spawn E2BIG` before the agent started. Docker-mode sessions forward the same
732
+ stdin through `docker exec -i`.
733
+ Gemini's own sandbox relaunch is an exception to this transport guarantee,
734
+ as described above; moving the initial spawn to stdin does not remove its
735
+ internal argv limit.
736
+
719
737
  ## Architecture
720
738
 
721
739
  ```
@@ -733,12 +751,12 @@ cli.cts
733
751
  into Docker when NAVARCH_SANDBOX_MODE=docker
734
752
  4. selectAdapter(config.agentType) (adapters/index.cts) picks one AgentAdapter
735
753
  (adapters/types.cts) by NAVARCH_AGENT, then .run(...):
736
- - claudeCodeAdapter (adapters/claude.cts) — `claude -p <prompt> --mcp-config <path>`
737
- - codexAdapter (adapters/codex.cts) — `codex exec <prompt> --json -c mcp_servers.*=...`
738
- - geminiAdapter (adapters/gemini.cts) — `gemini --prompt <prompt> --output-format stream-json`
739
- - openCodeAdapter (adapters/opencode.cts) — `opencode run <prompt> --format json`
754
+ - claudeCodeAdapter (adapters/claude.cts) — `claude -p --mcp-config <path> < prompt`
755
+ - codexAdapter (adapters/codex.cts) — `codex exec - --json -c mcp_servers.*=... < prompt`
756
+ - geminiAdapter (adapters/gemini.cts) — `gemini --output-format stream-json < prompt`
757
+ - openCodeAdapter (adapters/opencode.cts) — `opencode run --format json < prompt`
740
758
  - acpAdapter (adapters/acp.cts) — ACP v1 JSON-RPC/stdio; defaults to `dsh --profile acp`
741
- - qoderAdapter (adapters/qoder.cts) — `qoder -p <prompt> --output-format json`
759
+ - qoderAdapter (adapters/qoder.cts) — `qoder -p --output-format json < prompt`
742
760
  heartbeating the lease every NAVARCH_LEASE_HEARTBEAT_INTERVAL_MS throughout either;
743
761
  a failed heartbeat aborts the run (kills the process) and marks the outcome as lease-lost
744
762
  5. mapExitCondition (exit-conditions.cts) → redact.cts scrubs the transcript → upload.cts PUTs it
@@ -13,8 +13,10 @@ function hasFlag(args, flag) {
13
13
  }
14
14
  /**
15
15
  * Headless Claude Code adapter (project-plan.md §3.9 / implementation-plan.md
16
- * WP-07): `claude -p "<context bundle>" --mcp-config platform-mcp.json
17
- * --output-format json`. Returns raw stdout/stderr/exit signals (plus
16
+ * WP-07): `claude -p --mcp-config platform-mcp.json --output-format json`
17
+ * with the context bundle on stdin (`claude -p` reads its prompt from stdin
18
+ * when none is given; Linux rejects any single argv string over 128 KiB, so
19
+ * an argv prompt failed `spawn E2BIG` before Claude started). Returns raw stdout/stderr/exit signals (plus
18
20
  * best-effort tokensIn/tokensOut/costUsd parsed from the JSON result -- see
19
21
  * exit-conditions.cts#parseClaudeJsonResult) for exit-conditions.cts to map
20
22
  * onto a complete()/fail() call.
@@ -91,7 +93,7 @@ function addUsage(first, second) {
91
93
  return first === undefined && second === undefined ? undefined : (first ?? 0) + (second ?? 0);
92
94
  }
93
95
  async function runClaudeAttempt(options, automaticFallback) {
94
- const args = ["-p", options.prompt];
96
+ const args = ["-p"];
95
97
  const hasSettingSources = options.extraArgs.some((arg) => arg === "--setting-sources" || arg.startsWith("--setting-sources="));
96
98
  const hasExplicitPermissionMode = options.extraArgs.some((arg) => ["--permission-mode", "--permission-prompt-tool", "--dangerously-skip-permissions"].some((flag) => arg === flag || arg.startsWith(`${flag}=`)));
97
99
  // Runtime sessions must not inherit an operator's personal or project-local
@@ -180,11 +182,11 @@ async function runOnHost(options, args) {
180
182
  cwd: options.cwd,
181
183
  env: { ...process.env, ...options.env },
182
184
  });
183
- // `claude -p` checks stdin for an additional piped prompt. Node opens a
184
- // pipe by default, so close it immediately when the complete prompt was
185
- // supplied as an argument. Leaving it open makes Claude wait and emit a
186
- // "no stdin data received" warning that the runtime treats as failure.
187
- child.stdin?.end();
185
+ // `claude -p` reads the whole prompt from stdin and starts at EOF. A
186
+ // child that exits before reading surfaces through `close`; ignore the
187
+ // resulting EPIPE here.
188
+ child.stdin?.on("error", () => undefined);
189
+ child.stdin?.end(options.prompt);
188
190
  options.onProcessStarted?.(child.pid);
189
191
  const timer = options.timeoutMs > 0 ? setTimeout(() => {
190
192
  timedOut = true;
@@ -234,7 +236,9 @@ async function runViaDocker(options, args) {
234
236
  if (options.signal?.aborted)
235
237
  onAbort();
236
238
  try {
237
- const result = await runner.run("docker", ["exec", containerName, "sh", "-c", command], {
239
+ // `-i` forwards the prompt to `claude -p` on stdin (see runOnHost).
240
+ const result = await runner.run("docker", ["exec", "-i", containerName, "sh", "-c", command], {
241
+ input: options.prompt,
238
242
  timeoutMs: options.timeoutMs,
239
243
  onActivity: options.onActivity,
240
244
  signal: options.signal,
@@ -24,8 +24,8 @@ const kill_agent_cjs_1 = require("../kill-agent.cjs");
24
24
  * - `exec <prompt>` — ASSUMED to be Codex CLI's non-interactive/headless
25
25
  * subcommand (the `codex exec` "automation mode" analog of `claude -p`):
26
26
  * runs the prompt to completion without the interactive TUI and exits,
27
- * printing its result to stdout. On the host the prompt is `-` and arrives
28
- * on stdin: Linux rejects any single argv string over 128 KiB
27
+ * printing its result to stdout. The prompt is `-` and arrives on stdin
28
+ * (through `docker exec -i` in sandbox mode): Linux rejects any single argv string over 128 KiB
29
29
  * (MAX_ARG_STRLEN), so a large review prompt failed `spawn E2BIG` before
30
30
  * Codex started (tobi-agent-5571, 2026-10-06).
31
31
  * - `--json` emits top-level `thread.started`, `turn.started`,
@@ -48,7 +48,7 @@ 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", options.dockerExec ? options.prompt : "-"];
51
+ const args = ["exec", "-"];
52
52
  const env = { ...options.env };
53
53
  if (options.mcpConfigPath)
54
54
  args.push(...(await codexMcpArgs(options.mcpConfigPath, env)));
@@ -233,7 +233,8 @@ async function runViaDocker(options, args) {
233
233
  // variable names in argv. This keeps generated MCP bearer/header values
234
234
  // out of `ps` while making the host and Docker paths equivalent.
235
235
  const forwardedEnv = Object.keys(options.env).flatMap((name) => ["--env", name]);
236
- const result = await runner.run("docker", ["exec", ...forwardedEnv, containerName, "sh", "-c", command], {
236
+ const result = await runner.run("docker", ["exec", "-i", ...forwardedEnv, containerName, "sh", "-c", command], {
237
+ input: options.prompt,
237
238
  timeoutMs: options.timeoutMs,
238
239
  onActivity: options.onActivity,
239
240
  signal: options.signal,
@@ -14,9 +14,23 @@ const kill_agent_cjs_1 = require("../kill-agent.cjs");
14
14
  * The CLI reads MCP servers from settings rather than accepting Claude's
15
15
  * `--mcp-config` flag. The adapter converts the lease-scoped config into a
16
16
  * temporary system-settings file and restricts the run to those server names.
17
+ *
18
+ * The prompt arrives on stdin rather than `--prompt`: Gemini runs headlessly
19
+ * whenever stdin is not a TTY and uses the piped text as the prompt. Its
20
+ * native sandbox puts stdin back into argv, so that path still has Linux's
21
+ * 128 KiB per-string limit. Gemini also caps stdin at 8 MiB.
17
22
  */
18
23
  async function runGeminiAdapter(options) {
19
- const args = ["--prompt", options.prompt];
24
+ if (options.signal?.aborted) {
25
+ return { exitCode: null, timedOut: false, killedByLeaseLoss: true, stdout: "", stderr: "" };
26
+ }
27
+ const promptBytes = Buffer.byteLength(options.prompt, "utf8");
28
+ // Gemini's readStdin() silently truncates at 8 MiB. Reject before any
29
+ // settings writes or process launch rather than execute a partial task.
30
+ if (promptBytes > 8 * 1024 * 1024) {
31
+ return inputFailure(`Gemini CLI stdin is limited to 8 MiB; the task prompt is ${promptBytes} bytes. Reduce the context bundle or select another runtime.`);
32
+ }
33
+ const args = [];
20
34
  const env = { ...options.env };
21
35
  if (!hasArg(options.extraArgs, "--output-format", "-o")) {
22
36
  args.push("--output-format", "stream-json");
@@ -53,12 +67,41 @@ async function runGeminiAdapter(options) {
53
67
  args.push(...options.extraArgs);
54
68
  if (options.model)
55
69
  args.push("--model", options.model);
70
+ // Gemini's sandbox launcher reads stdin and injects it back into --prompt
71
+ // for its child process (packages/cli/src/gemini.tsx injectStdinIntoArgs).
72
+ // Moving our first spawn's prompt to stdin cannot fix that second spawn.
73
+ // Check the effective flag, including operator overrides, without disabling
74
+ // the worktree guard. Even on macOS a Docker sandbox may launch on Linux.
75
+ if (geminiSandboxEnabled(args) && promptBytes >= 128 * 1024) {
76
+ return inputFailure(`Gemini CLI re-encodes stdin as --prompt when launching its native sandbox; the ${promptBytes}-byte task prompt exceeds Linux's argv limit. Reduce the context bundle or select another runtime that supports large prompts in its sandbox.`);
77
+ }
56
78
  const runOptions = { ...options, env };
57
79
  const raw = runOptions.dockerExec
58
80
  ? await runViaDocker(runOptions, args)
59
81
  : await runOnHost(runOptions, args);
60
82
  return attachGeminiOutput(raw);
61
83
  }
84
+ function inputFailure(stderr) {
85
+ return { exitCode: 1, timedOut: false, killedByLeaseLoss: false, stdout: "", stderr };
86
+ }
87
+ function geminiSandboxEnabled(args) {
88
+ // There is always a generated --sandbox flag unless extraArgs supplies one.
89
+ // Like yargs, later explicit values override earlier ones.
90
+ let enabled = false;
91
+ for (let index = 0; index < args.length; index++) {
92
+ const arg = args[index];
93
+ if (arg === "--sandbox" || arg === "-s") {
94
+ enabled = args[index + 1] !== "false";
95
+ }
96
+ else if (arg.startsWith("--sandbox=") || arg.startsWith("-s=")) {
97
+ enabled = arg.slice(arg.indexOf("=") + 1) !== "false";
98
+ }
99
+ else if (arg === "--no-sandbox") {
100
+ enabled = false;
101
+ }
102
+ }
103
+ return enabled;
104
+ }
62
105
  function hasArg(args, ...flags) {
63
106
  return args.some((arg) => flags.some((flag) => arg === flag || arg.startsWith(`${flag}=`)));
64
107
  }
@@ -157,7 +200,9 @@ async function runOnHost(options, args) {
157
200
  cwd: options.cwd,
158
201
  env: { ...process.env, ...options.env },
159
202
  });
160
- child.stdin?.end();
203
+ // Ignore EPIPE from a child that exits before reading; `close` reports it.
204
+ child.stdin?.on("error", () => undefined);
205
+ child.stdin?.end(options.prompt);
161
206
  options.onProcessStarted?.(child.pid);
162
207
  const timer = options.timeoutMs > 0 ? setTimeout(() => {
163
208
  timedOut = true;
@@ -207,7 +252,8 @@ async function runViaDocker(options, args) {
207
252
  onAbort();
208
253
  try {
209
254
  const forwardedEnv = Object.keys(options.env).flatMap((name) => ["--env", name]);
210
- const result = await runner.run("docker", ["exec", ...forwardedEnv, containerName, "sh", "-c", command], { timeoutMs: options.timeoutMs,
255
+ const result = await runner.run("docker", ["exec", "-i", ...forwardedEnv, containerName, "sh", "-c", command], { input: options.prompt,
256
+ timeoutMs: options.timeoutMs,
211
257
  onActivity: options.onActivity,
212
258
  signal: options.signal,
213
259
  env: { ...process.env, ...options.env },
@@ -16,7 +16,12 @@ const kill_agent_cjs_1 = require("../kill-agent.cjs");
16
16
  * Headless OpenCode adapter, fixture-pinned to the OpenCode JSON/config
17
17
  * contract shared by V1 and V2 (verified against 2.0.6):
18
18
  *
19
- * opencode run "<prompt>" --format json [--model provider/model[#variant]]
19
+ * opencode run --format json [--model provider/model[#variant]] < prompt
20
+ *
21
+ * The prompt goes in on stdin, which `opencode run` reads as the message when
22
+ * none is given (verified against 2.0.17). Linux rejects any single argv
23
+ * string over 128 KiB (`spawn E2BIG`), and an argv prompt also let an
24
+ * agent's `pkill -f <word>` match the CLI itself (#1587).
20
25
  *
21
26
  * OpenCode merges configuration from several machine and project locations,
22
27
  * so each Navarch session gets an isolated XDG/config directory, disables
@@ -56,7 +61,7 @@ async function runOpenCodeAdapter(options) {
56
61
  const binaryKey = `${options.dockerExec ? "docker:" : ""}${options.bin ?? "opencode"}`;
57
62
  const defaultLogLevel = !hasArg(options.extraArgs, "--log-level");
58
63
  const logLevel = defaultLogLevel ? (openCodeLogLevels.get(binaryKey) ?? "ERROR") : null;
59
- const args = ["run", options.prompt];
64
+ const args = ["run"];
60
65
  if (/^\s*--standalone\b/m.test(help.stdout) && !hasArg(options.extraArgs, "--standalone")) {
61
66
  args.push("--standalone");
62
67
  }
@@ -76,7 +81,9 @@ async function runOpenCodeAdapter(options) {
76
81
  // use the model's own default.
77
82
  args.push("--model", options.reasoningEffort ? `${options.model}#${options.reasoningEffort}` : options.model);
78
83
  }
79
- const run = (runArgs) => runOptions.dockerExec ? runViaDocker(runOptions, runArgs) : runOnHost(runOptions, runArgs);
84
+ const run = (runArgs) => runOptions.dockerExec
85
+ ? runViaDocker(runOptions, runArgs, options.prompt)
86
+ : runOnHost(runOptions, runArgs, options.prompt);
80
87
  let raw = await run(args);
81
88
  if (logLevel && raw.exitCode !== 0 && isLogLevelRejection(raw.stderr)) {
82
89
  // The CLI rejected our default before starting any work, so one retry with
@@ -315,7 +322,8 @@ function capacityMonitor(onLimit) {
315
322
  },
316
323
  };
317
324
  }
318
- async function runOnHost(options, args) {
325
+ /** `input` is written to the child's stdin; the `run --help` probe sends none. */
326
+ async function runOnHost(options, args, input = "") {
319
327
  return new Promise((resolve) => {
320
328
  let stdout = "";
321
329
  let stderr = "";
@@ -331,7 +339,9 @@ async function runOnHost(options, args) {
331
339
  capacityError ??= detail;
332
340
  (0, kill_agent_cjs_1.killAgent)(child);
333
341
  });
334
- child.stdin?.end();
342
+ // Ignore EPIPE from a child that exits before reading; `close` reports it.
343
+ child.stdin?.on("error", () => undefined);
344
+ child.stdin?.end(input);
335
345
  options.onProcessStarted?.(child.pid);
336
346
  const timer = options.timeoutMs > 0 ? setTimeout(() => {
337
347
  timedOut = true;
@@ -369,7 +379,7 @@ async function runOnHost(options, args) {
369
379
  });
370
380
  });
371
381
  }
372
- async function runViaDocker(options, args) {
382
+ async function runViaDocker(options, args, input) {
373
383
  const { containerName, runner } = options.dockerExec;
374
384
  const quoted = [options.bin, ...args].map(shellQuote).join(" ");
375
385
  const command = `[ -f /tmp/session.env ] && . /tmp/session.env; cd repo 2>/dev/null; ${quoted}`;
@@ -394,7 +404,8 @@ async function runViaDocker(options, args) {
394
404
  onAbort();
395
405
  try {
396
406
  const forwardedEnv = Object.keys(options.env).flatMap((name) => ["--env", name]);
397
- const result = await runner.run("docker", ["exec", ...forwardedEnv, containerName, "sh", "-c", command], { timeoutMs: options.timeoutMs,
407
+ const result = await runner.run("docker", ["exec", ...(input === undefined ? [] : ["-i"]), ...forwardedEnv, containerName, "sh", "-c", command], { input,
408
+ timeoutMs: options.timeoutMs,
398
409
  onActivity: options.onActivity,
399
410
  signal: controller.signal,
400
411
  onStdout: (chunk) => { streamed.stdout = true; monitor.push("stdout", chunk); },
@@ -14,7 +14,8 @@ async function runLocalPiAdapter(options) {
14
14
  "--model", model.includes("/") ? model : `local/${model}`];
15
15
  if (options.reasoningEffort)
16
16
  args.push("--thinking", options.reasoningEffort === "max" ? "xhigh" : options.reasoningEffort);
17
- args.push("-p", options.prompt);
17
+ // The prompt goes in on stdin (pi.cts runOnHost), never argv.
18
+ args.push("-p");
18
19
  return (0, pi_cjs_1.attachPiOutput)(await (0, pi_cjs_1.runOnHost)(options, args, true));
19
20
  }
20
21
  exports.localPiAdapter = { agentType: "pi-local", run: runLocalPiAdapter };
@@ -19,9 +19,11 @@ const kill_agent_cjs_1 = require("../kill-agent.cjs");
19
19
  /**
20
20
  * Headless Pi coding-agent adapter (#885) — the process-side twin of the
21
21
  * Cloudflare hosted executor (worker/hosted-pi/lifecycle.ts). It invokes
22
- * `pi --mode json -p <prompt>` against DeepSeek V4 through a generated
22
+ * `pi --mode json -p < prompt` against DeepSeek V4 through a generated
23
23
  * OpenAI-compatible custom-provider config and normalizes the JSON event
24
- * stream into the common AdapterResult boundary.
24
+ * stream into the common AdapterResult boundary. `-p` is Pi's print flag and
25
+ * the prompt arrives on stdin, as in the hosted executor (#1587): Linux
26
+ * rejects any single argv string over 128 KiB (`spawn E2BIG`).
25
27
  *
26
28
  * Hosted Pi is control-plane provisioned (docs/navarch/runtime-adapter-contract.md
27
29
  * — "Hosted Pi is not a fifth AgentAdapter" for BYO workers): config.cts
@@ -254,7 +256,7 @@ async function runPiAdapter(options) {
254
256
  // than passing an invalid value that Pi warns about and ignores.
255
257
  args.push("--thinking", options.reasoningEffort === "max" ? "xhigh" : options.reasoningEffort);
256
258
  }
257
- args.push("-p", options.prompt);
259
+ args.push("-p");
258
260
  const raw = options.dockerExec
259
261
  ? await runViaDocker(options, args)
260
262
  : await runOnHost(options, args);
@@ -325,7 +327,9 @@ async function runOnHost(options, args, local = false) {
325
327
  cwd: options.cwd,
326
328
  env: { ...process.env, ...env },
327
329
  });
328
- child.stdin?.end();
330
+ // Ignore EPIPE from a child that exits before reading; `close` reports it.
331
+ child.stdin?.on("error", () => undefined);
332
+ child.stdin?.end(options.prompt);
329
333
  options.onProcessStarted?.(child.pid);
330
334
  const timer = options.timeoutMs > 0 ? setTimeout(() => {
331
335
  timedOut = true;
@@ -400,7 +404,8 @@ async function runViaDocker(options, args) {
400
404
  onAbort();
401
405
  try {
402
406
  const forwardedEnv = Object.keys(options.env).flatMap((name) => ["--env", name]);
403
- const result = await runner.run("docker", ["exec", ...forwardedEnv, containerName, "sh", "-c", command], {
407
+ const result = await runner.run("docker", ["exec", "-i", ...forwardedEnv, containerName, "sh", "-c", command], {
408
+ input: options.prompt,
404
409
  timeoutMs: options.timeoutMs,
405
410
  onActivity: options.onActivity,
406
411
  signal: options.signal,
@@ -9,7 +9,10 @@ function hasFlag(args, flag) {
9
9
  return args.some((arg) => arg === flag || arg.startsWith(`${flag}=`));
10
10
  }
11
11
  /**
12
- * Headless Qoder CLI adapter: `qoder -p "<prompt>" --output-format json`.
12
+ * Headless Qoder CLI adapter: `qoder -p --output-format json < prompt`.
13
+ * Like `claude -p`, print mode reads the prompt from stdin when no query is
14
+ * given (verified against qodercli 1.1.64), which keeps large prompts clear
15
+ * of Linux's 128 KiB per-argv-string limit (`spawn E2BIG`).
13
16
  * Qoder's print mode emits the same single-JSON result shape as Claude Code
14
17
  * (verified against qodercli 1.1.56), so usage/cost parsing reuses
15
18
  * parseClaudeJsonResult and its result text lands in the ordinary
@@ -24,7 +27,7 @@ async function runQoderAdapter(options) {
24
27
  if (options.signal?.aborted) {
25
28
  return { exitCode: null, timedOut: false, killedByLeaseLoss: true, stdout: "", stderr: "" };
26
29
  }
27
- const args = ["-p", options.prompt];
30
+ const args = ["-p"];
28
31
  // Runtime sessions must not inherit an operator's personal or project-local
29
32
  // Qoder hooks, for the same reason as the Claude adapter: they make
30
33
  // execution machine-dependent and can fail otherwise-good sessions. The
@@ -81,9 +84,10 @@ async function runOnHost(options, args) {
81
84
  cwd: options.cwd,
82
85
  env: { ...process.env, ...options.env },
83
86
  });
84
- // `qoder -p` checks stdin for a piped prompt; close it so the child does
85
- // not wait when the complete prompt was supplied as an argument.
86
- child.stdin?.end();
87
+ // `qoder -p` reads the whole prompt from stdin and starts at EOF. A child
88
+ // that exits before reading surfaces through `close`; ignore the EPIPE.
89
+ child.stdin?.on("error", () => undefined);
90
+ child.stdin?.end(options.prompt);
87
91
  options.onProcessStarted?.(child.pid);
88
92
  const timer = options.timeoutMs > 0 ? setTimeout(() => {
89
93
  timedOut = true;
@@ -130,7 +134,9 @@ async function runViaDocker(options, args) {
130
134
  if (options.signal?.aborted)
131
135
  onAbort();
132
136
  try {
133
- const result = await runner.run("docker", ["exec", containerName, "sh", "-c", command], {
137
+ // `-i` forwards the prompt to `qoder -p` on stdin (see runOnHost).
138
+ const result = await runner.run("docker", ["exec", "-i", containerName, "sh", "-c", command], {
139
+ input: options.prompt,
134
140
  timeoutMs: options.timeoutMs,
135
141
  onActivity: options.onActivity,
136
142
  signal: options.signal,
package/dist/sandbox.cjs CHANGED
@@ -61,6 +61,9 @@ exports.nodeCommandRunner = {
61
61
  cleanup();
62
62
  resolve({ code: code ?? -1, stdout, stderr });
63
63
  });
64
+ // A child that exits before reading all input (e.g. `docker exec -i`
65
+ // carrying an agent prompt) raises EPIPE here; `close` reports the exit.
66
+ child.stdin?.on("error", () => undefined);
64
67
  if (opts.input !== undefined) {
65
68
  child.stdin?.write(opts.input);
66
69
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sagentlab/navarch-runtime",
3
- "version": "0.1.85",
3
+ "version": "0.1.87",
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",