@sagentlab/navarch-runtime 0.1.51 → 0.1.53
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 +27 -9
- package/dist/adapter-capacity.cjs +9 -2
- package/dist/adapters/index.cjs +6 -1
- package/dist/adapters/opencode.cjs +92 -20
- package/dist/adapters/qoder.cjs +166 -0
- package/dist/cli.cjs +30 -28
- package/dist/config.cjs +4 -1
- package/dist/exit-conditions.cjs +10 -0
- package/dist/machine-store.cjs +2 -1
- package/dist/sandbox.cjs +2 -0
- package/dist/session.cjs +8 -3
- package/dist/upload.cjs +3 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -152,10 +152,10 @@ The from-source flow — `git clone` + `./install.sh` + `node bin/navarch.cjs
|
|
|
152
152
|
|
|
153
153
|
| Command | Purpose |
|
|
154
154
|
|---|---|
|
|
155
|
-
| `register --token <t> --name <n> [--agent claude-code\|codex\|gemini\|opencode\|acp] […]` | Registers this machine, saves its local agent choice, and prints the token once. |
|
|
156
|
-
| `connect --token <t> --name <n> [--agent claude-code\|codex\|gemini\|opencode\|acp] [--project <id>] […]` | Connects this machine to one project, saves its local agent choice, and prints the token once. |
|
|
157
|
-
| `start [--agent claude-code\|codex\|gemini\|opencode\|acp]` | Runs the daemon. A start-time agent choice overrides the saved choice. |
|
|
158
|
-
| `supervise [--agent claude-code\|codex\|gemini\|opencode\|acp]` | Runs the daemon under the update supervisor, enabling drain-safe automatic updates and rollback. |
|
|
155
|
+
| `register --token <t> --name <n> [--agent claude-code\|codex\|gemini\|opencode\|acp\|qoder] […]` | Registers this machine, saves its local agent choice, and prints the token once. |
|
|
156
|
+
| `connect --token <t> --name <n> [--agent claude-code\|codex\|gemini\|opencode\|acp\|qoder] [--project <id>] […]` | Connects this machine to one project, saves its local agent choice, and prints the token once. |
|
|
157
|
+
| `start [--agent claude-code\|codex\|gemini\|opencode\|acp\|qoder]` | Runs the daemon. A start-time agent choice overrides the saved choice. |
|
|
158
|
+
| `supervise [--agent claude-code\|codex\|gemini\|opencode\|acp\|qoder]` | Runs the daemon under the update supervisor, enabling drain-safe automatic updates and rollback. |
|
|
159
159
|
| `doctor` | Prints resolved config + Docker/registration status; no side effects. |
|
|
160
160
|
|
|
161
161
|
### Running multiple agents on one machine
|
|
@@ -368,8 +368,8 @@ unchanged across the deployment.
|
|
|
368
368
|
| `NAVARCH_SANDBOX_PROFILE` | `trusted-development` | Named security profile for Docker sessions (`src/sandbox-profile.cts`): `trusted-development` (image-default user, uncapped, open egress), `untrusted-code` (non-root, 2 CPU / 4g / 512 PIDs, deny-by-default egress, read-only shared git), `elevated-verification` (non-root, 4 CPU / 8g / 2048 PIDs, egress limited to GitHub plus package registries). The default is the exact pre-profile flag set. |
|
|
369
369
|
| `NAVARCH_SANDBOX_EGRESS_NETWORK` | _(unset)_ | Docker network that enforces a profile's egress allowlist. Docker cannot filter by domain itself, so an allowlist profile without this fails closed to `--network=none` and records the denial. |
|
|
370
370
|
| `NAVARCH_DOCKER_IMAGE` | `ghcr.io/sagentlab/navarch-sandbox-agent:0.1.0` | Version-pinned per-session image with Node 20, git, GitHub CLI, ripgrep, jq, SSH, and Claude Code 2.1.218. Override with an image tag or digest you control. |
|
|
371
|
-
| `NAVARCH_AGENT` | saved choice, then `claude-code` | Local choice of agent adapter: `claude-code`, `codex`, `gemini`, `opencode`, or `
|
|
372
|
-
| `NAVARCH_RUNTIMES` |
|
|
371
|
+
| `NAVARCH_AGENT` | saved choice, then `claude-code` | Local choice of agent adapter: `claude-code`, `codex`, `gemini`, `opencode`, `acp`, or `qoder`. Overrides the choice saved by `connect`/`register`; `start --agent` has highest priority. |
|
|
372
|
+
| `NAVARCH_RUNTIMES` | resolved local agent choice | Comma list of installed/authenticated adapters advertised to dispatch. The control plane chooses among these per project/task. |
|
|
373
373
|
| `NAVARCH_UPDATE_CHANNEL` | `stable` | Release channel advertised by the worker (`stable` or `canary`); the server-managed machine channel remains authoritative. |
|
|
374
374
|
| `NAVARCH_AUTO_UPDATE` | on under `supervise` | Set `off`, `false`, or `0` to report releases without staging or activating them. Automatic activation is always off under plain `start`. |
|
|
375
375
|
| `NAVARCH_CLAUDE_BIN` | `claude` | Path/name of the Claude Code CLI binary. |
|
|
@@ -382,6 +382,8 @@ unchanged across the deployment.
|
|
|
382
382
|
| `NAVARCH_OPENCODE_EXTRA_ARGS` | — | Comma list of extra CLI args appended after `run`, the prompt, and the generated `--format json` argument. Explicit `--format`, `--model`, or `--variant` values replace the corresponding per-session default. |
|
|
383
383
|
| `NAVARCH_ACP_BIN` | `dsh` | Path/name of an Agent Client Protocol v1 stdio server. DeepSeek Harness is the default implementation. |
|
|
384
384
|
| `NAVARCH_ACP_EXTRA_ARGS` | `--profile,acp` | Comma list of arguments used to start the ACP server. Override this together with `NAVARCH_ACP_BIN` for another ACP-compatible coding agent. |
|
|
385
|
+
| `NAVARCH_QODER_BIN` | `qoder` | Path/name of the Qoder CLI binary. |
|
|
386
|
+
| `NAVARCH_QODER_EXTRA_ARGS` | — | Comma list of extra CLI args appended after the unattended defaults (`--setting-sources`, `--permission-mode auto`, `--output-format json`). An explicit `--output-format`, `--permission-mode`, `--settings`, `--model`, or `--reasoning-effort` here replaces the per-session default. Qoder selects its own account models; pin one via `--model` here or `connect --model` to override. |
|
|
385
387
|
| `NAVARCH_MCP_CONFIG_PATH` | — | Path to the platform MCP config passed as `--mcp-config`. |
|
|
386
388
|
| `NAVARCH_WORKTREE_GUARD` | on | Host-mode sessions get an adapter-native per-session worktree boundary guard (see below). Set `off` to disable it for every runtime on the machine — required to run OpenCode on the host. |
|
|
387
389
|
| `NAVARCH_GUARD_EXTRA_ROOTS` | — | `path.delimiter`-separated (`:` on POSIX) extra directories the worktree guard allows beyond the session worktree, shared bare repo, and temp dirs. |
|
|
@@ -514,10 +516,15 @@ export NAVARCH_AGENT=opencode
|
|
|
514
516
|
# Override NAVARCH_ACP_BIN / NAVARCH_ACP_EXTRA_ARGS for another ACP v1 server.
|
|
515
517
|
export NAVARCH_AGENT=acp
|
|
516
518
|
|
|
519
|
+
# Qoder — requires an authenticated `qoder` CLI (or NAVARCH_QODER_BIN pointing
|
|
520
|
+
# at it). Model selection is account-owned; NAVARCH_QODER_EXTRA_ARGS or
|
|
521
|
+
# `connect --model` can pin one explicitly.
|
|
522
|
+
export NAVARCH_AGENT=qoder
|
|
523
|
+
|
|
517
524
|
# Advanced compatibility mode: advertise every installed adapter. On a guarded
|
|
518
525
|
# host, `opencode` is dropped from this list (see the boundary notes above);
|
|
519
526
|
# the rest are advertised as written.
|
|
520
|
-
export NAVARCH_RUNTIMES=claude-code,codex,gemini,opencode,acp
|
|
527
|
+
export NAVARCH_RUNTIMES=claude-code,codex,gemini,opencode,acp,qoder
|
|
521
528
|
```
|
|
522
529
|
|
|
523
530
|
For the legacy single-runtime setting, priority is `start --agent` →
|
|
@@ -601,6 +608,16 @@ to OpenCode's remote-server config with OAuth disabled and environment-backed
|
|
|
601
608
|
headers. Missing accounting fields stay absent rather than becoming a
|
|
602
609
|
fabricated zero.
|
|
603
610
|
|
|
611
|
+
OpenCode also runs with `--print-logs --log-level ERROR` by default (see its
|
|
612
|
+
[CLI flags](https://opencode.ai/docs/cli/#global-flags)), so provider errors
|
|
613
|
+
that are missing from assistant messages still reach the runtime. A rate or
|
|
614
|
+
usage limit in a provider error event or stream-error log stops the host
|
|
615
|
+
process or Docker container immediately. The runtime checkpoints local work,
|
|
616
|
+
records the error, and releases the lease with a failed session outcome.
|
|
617
|
+
Pending guidance does not restart that session. New claims on this machine
|
|
618
|
+
pause until the reported reset time (including relative resets such as
|
|
619
|
+
“in 23 days”), or for 15 minutes when no reset time is available.
|
|
620
|
+
|
|
604
621
|
The ACP adapter speaks the standard [Agent Client Protocol](https://agentclientprotocol.com)
|
|
605
622
|
v1 JSON-RPC transport over stdio. It creates one protocol session in the task
|
|
606
623
|
worktree, converts Navarch's HTTP/stdio MCP entries, consumes semantic message,
|
|
@@ -632,6 +649,7 @@ cli.cts
|
|
|
632
649
|
- geminiAdapter (adapters/gemini.cts) — `gemini --prompt <prompt> --output-format stream-json`
|
|
633
650
|
- openCodeAdapter (adapters/opencode.cts) — `opencode run <prompt> --format json`
|
|
634
651
|
- acpAdapter (adapters/acp.cts) — ACP v1 JSON-RPC/stdio; defaults to `dsh --profile acp`
|
|
652
|
+
- qoderAdapter (adapters/qoder.cts) — `qoder -p <prompt> --output-format json`
|
|
635
653
|
heartbeating the lease every NAVARCH_LEASE_HEARTBEAT_INTERVAL_MS throughout either;
|
|
636
654
|
a failed heartbeat aborts the run (kills the process) and marks the outcome as lease-lost
|
|
637
655
|
5. mapExitCondition (exit-conditions.cts) → redact.cts scrubs the transcript → upload.cts PUTs it
|
|
@@ -680,7 +698,7 @@ contract used by `api.cts`, including:
|
|
|
680
698
|
distinct from the per-lease heartbeat, needed because `machines.last_heartbeat_at`
|
|
681
699
|
/`status` must update even when no task is claimed.
|
|
682
700
|
3. **`POST /api/dispatch/:leaseId/transcript-upload-url`** — a signed
|
|
683
|
-
|
|
701
|
+
R2 upload URL for the session's redacted transcript. The
|
|
684
702
|
bucket is private; the legacy `public_url` response field is only an
|
|
685
703
|
attested locator returned at completion, not an anonymous read URL.
|
|
686
704
|
|
|
@@ -782,7 +800,7 @@ secrets absent from disk after exit"):
|
|
|
782
800
|
already-reassigned lease) — `session.cts` aborts the running adapter
|
|
783
801
|
process and still attempts a best-effort `complete()` call, which the
|
|
784
802
|
real server may reject; that path is untested against real semantics.
|
|
785
|
-
- Transcript upload against a real signed
|
|
803
|
+
- Transcript upload against a real signed R2 URL.
|
|
786
804
|
- The end-to-end "Connect an agent to a project" flow against a real
|
|
787
805
|
Supabase instance: an owner minting a token from `ConnectAgentPanel`
|
|
788
806
|
(or the onboarding wizard's Agent step), `navarch-runtime connect`
|
|
@@ -14,9 +14,9 @@ const MAX_RESET_SEARCH_MINUTES = 48 * 60;
|
|
|
14
14
|
function detectAdapterCapacityLimit(result, nowMs = Date.now()) {
|
|
15
15
|
const parsed = (0, exit_conditions_cjs_1.parseClaudeJsonResult)(result.stdout) ??
|
|
16
16
|
(0, exit_conditions_cjs_1.parseClaudeJsonResult)(result.stderr);
|
|
17
|
-
if (parsed?.api_error_status !== 429)
|
|
17
|
+
if (!result.capacityError && parsed?.api_error_status !== 429)
|
|
18
18
|
return null;
|
|
19
|
-
const detail = parsed
|
|
19
|
+
const detail = result.capacityError ?? parsed?.result ?? "";
|
|
20
20
|
if (!/\b(?:session|usage|rate)\s+limit\b|\btoo many requests\b/i.test(detail)) {
|
|
21
21
|
return null;
|
|
22
22
|
}
|
|
@@ -32,6 +32,13 @@ function detectAdapterCapacityLimit(result, nowMs = Date.now()) {
|
|
|
32
32
|
* the platform's IANA timezone database.
|
|
33
33
|
*/
|
|
34
34
|
function parseResetTime(detail, nowMs) {
|
|
35
|
+
const relative = detail.match(/\bresets?\s+in\s+(\d+(?:\.\d+)?)\s*(seconds?|minutes?|hours?|days?)\b/i);
|
|
36
|
+
if (relative) {
|
|
37
|
+
const units = { second: 1000, minute: 60_000, hour: 3_600_000, day: 86_400_000 };
|
|
38
|
+
const duration = Number(relative[1]) * units[relative[2].toLowerCase().replace(/s$/, "")];
|
|
39
|
+
if (Number.isFinite(duration) && duration > 0)
|
|
40
|
+
return nowMs + duration + RESET_GRACE_MS;
|
|
41
|
+
}
|
|
35
42
|
const match = detail.match(/\bresets?\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)\s*\(([^)]+)\)/i);
|
|
36
43
|
if (!match)
|
|
37
44
|
return null;
|
package/dist/adapters/index.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.runAcpAdapter = exports.acpAdapter = exports.runPiAdapter = exports.piAdapter = exports.runOpenCodeAdapter = exports.openCodeAdapter = exports.runGeminiAdapter = exports.geminiAdapter = exports.runCodexAdapter = exports.codexAdapter = exports.runClaudeCodeAdapter = exports.claudeCodeAdapter = void 0;
|
|
3
|
+
exports.runQoderAdapter = exports.qoderAdapter = exports.runAcpAdapter = exports.acpAdapter = exports.runPiAdapter = exports.piAdapter = exports.runOpenCodeAdapter = exports.openCodeAdapter = exports.runGeminiAdapter = exports.geminiAdapter = exports.runCodexAdapter = exports.codexAdapter = exports.runClaudeCodeAdapter = exports.claudeCodeAdapter = void 0;
|
|
4
4
|
exports.selectAdapter = selectAdapter;
|
|
5
5
|
const claude_cjs_1 = require("./claude.cjs");
|
|
6
6
|
Object.defineProperty(exports, "claudeCodeAdapter", { enumerable: true, get: function () { return claude_cjs_1.claudeCodeAdapter; } });
|
|
@@ -20,6 +20,9 @@ Object.defineProperty(exports, "runPiAdapter", { enumerable: true, get: function
|
|
|
20
20
|
const acp_cjs_1 = require("./acp.cjs");
|
|
21
21
|
Object.defineProperty(exports, "acpAdapter", { enumerable: true, get: function () { return acp_cjs_1.acpAdapter; } });
|
|
22
22
|
Object.defineProperty(exports, "runAcpAdapter", { enumerable: true, get: function () { return acp_cjs_1.runAcpAdapter; } });
|
|
23
|
+
const qoder_cjs_1 = require("./qoder.cjs");
|
|
24
|
+
Object.defineProperty(exports, "qoderAdapter", { enumerable: true, get: function () { return qoder_cjs_1.qoderAdapter; } });
|
|
25
|
+
Object.defineProperty(exports, "runQoderAdapter", { enumerable: true, get: function () { return qoder_cjs_1.runQoderAdapter; } });
|
|
23
26
|
/**
|
|
24
27
|
* Picks the AgentAdapter (adapters/types.cts) session.cts should run a
|
|
25
28
|
* session with, keyed off config.cts's `agentType` (NAVARCH_AGENT). This is
|
|
@@ -37,6 +40,8 @@ function selectAdapter(agentType) {
|
|
|
37
40
|
return opencode_cjs_1.openCodeAdapter;
|
|
38
41
|
case "acp":
|
|
39
42
|
return acp_cjs_1.acpAdapter;
|
|
43
|
+
case "qoder":
|
|
44
|
+
return qoder_cjs_1.qoderAdapter;
|
|
40
45
|
case "claude-code":
|
|
41
46
|
return claude_cjs_1.claudeCodeAdapter;
|
|
42
47
|
case "pi":
|
|
@@ -7,10 +7,10 @@ const node_fs_1 = require("node:fs");
|
|
|
7
7
|
const config_cjs_1 = require("../config.cjs");
|
|
8
8
|
const kill_agent_cjs_1 = require("../kill-agent.cjs");
|
|
9
9
|
/**
|
|
10
|
-
* Headless OpenCode adapter, fixture-pinned to the OpenCode
|
|
11
|
-
* contract:
|
|
10
|
+
* Headless OpenCode adapter, fixture-pinned to the OpenCode JSON/config
|
|
11
|
+
* contract shared by V1 and V2 (verified against 2.0.6):
|
|
12
12
|
*
|
|
13
|
-
* opencode run "<prompt>" --format json [--model provider/model]
|
|
13
|
+
* opencode run "<prompt>" --format json [--model provider/model[#variant]]
|
|
14
14
|
*
|
|
15
15
|
* OpenCode merges configuration from several machine and project locations,
|
|
16
16
|
* so each Navarch session gets an isolated XDG/config directory, disables
|
|
@@ -32,14 +32,21 @@ async function runOpenCodeAdapter(options) {
|
|
|
32
32
|
const env = { ...options.env };
|
|
33
33
|
await prepareOpenCodeConfig(options.mcpConfigPath, env);
|
|
34
34
|
const args = ["run", options.prompt];
|
|
35
|
+
// Provider failures may only appear in logs, absent from assistant events.
|
|
36
|
+
if (!hasArg(options.extraArgs, "--print-logs"))
|
|
37
|
+
args.push("--print-logs");
|
|
38
|
+
if (!hasArg(options.extraArgs, "--log-level"))
|
|
39
|
+
args.push("--log-level", "ERROR");
|
|
35
40
|
if (!hasArg(options.extraArgs, "--format"))
|
|
36
41
|
args.push("--format", "json");
|
|
37
42
|
args.push(...options.extraArgs);
|
|
38
43
|
if (options.model && options.model !== "default" && !hasArg(options.extraArgs, "--model", "-m")) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
44
|
+
// OpenCode V2 folds the reasoning variant into the model reference
|
|
45
|
+
// (`provider/model#variant`) and hard-fails on a variant the model does
|
|
46
|
+
// not advertise. The claim route clamps the effort to the model's
|
|
47
|
+
// advertised variants, so a present value is always valid; absent means
|
|
48
|
+
// use the model's own default.
|
|
49
|
+
args.push("--model", options.reasoningEffort ? `${options.model}#${options.reasoningEffort}` : options.model);
|
|
43
50
|
}
|
|
44
51
|
const runOptions = { ...options, env };
|
|
45
52
|
const raw = runOptions.dockerExec
|
|
@@ -80,23 +87,22 @@ async function prepareOpenCodeConfig(mcpConfigPath, env) {
|
|
|
80
87
|
mcp[name] = {
|
|
81
88
|
type: "remote",
|
|
82
89
|
url,
|
|
83
|
-
|
|
90
|
+
disabled: false,
|
|
84
91
|
oauth: false,
|
|
85
92
|
...(Object.keys(headers).length > 0 ? { headers } : {}),
|
|
86
93
|
};
|
|
87
94
|
}
|
|
88
95
|
const config = {
|
|
89
96
|
$schema: "https://opencode.ai/config.json",
|
|
90
|
-
|
|
97
|
+
update: "disable",
|
|
91
98
|
share: "disabled",
|
|
92
|
-
|
|
93
|
-
mcp,
|
|
94
|
-
|
|
95
|
-
"*": "allow",
|
|
96
|
-
external_directory: "deny",
|
|
97
|
-
question: "deny",
|
|
98
|
-
|
|
99
|
-
},
|
|
99
|
+
plugins: [],
|
|
100
|
+
mcp: { servers: mcp },
|
|
101
|
+
permissions: [
|
|
102
|
+
{ action: "*", resource: "*", effect: "allow" },
|
|
103
|
+
{ action: "external_directory", resource: "*", effect: "deny" },
|
|
104
|
+
{ action: "question", resource: "*", effect: "deny" },
|
|
105
|
+
],
|
|
100
106
|
};
|
|
101
107
|
const configPath = `${mcpConfigPath}.opencode.json`;
|
|
102
108
|
const configDir = `${mcpConfigPath}.opencode-config`;
|
|
@@ -186,6 +192,42 @@ function parseOpenCodeEvents(stdout) {
|
|
|
186
192
|
function nonNegativeMetric(value) {
|
|
187
193
|
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
188
194
|
}
|
|
195
|
+
/** Inspect only provider error envelopes/logs, never tool output or assistant text. */
|
|
196
|
+
function capacityMonitor(onLimit) {
|
|
197
|
+
const pending = { stdout: "", stderr: "" };
|
|
198
|
+
const inspect = (line, stream) => {
|
|
199
|
+
let detail = "";
|
|
200
|
+
try {
|
|
201
|
+
const event = JSON.parse(line);
|
|
202
|
+
if (event.type === "error" || event.type === "session.error") {
|
|
203
|
+
const error = event.error ?? event.properties?.error;
|
|
204
|
+
detail = typeof error === "string" ? error : error?.data?.message ?? error?.message ?? "";
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
// OpenCode's log format varies by version. Require a stream-error log
|
|
209
|
+
// to avoid confusing a tool's own HTTP 429 with provider exhaustion.
|
|
210
|
+
if (stream === "stderr" && /\bERROR\b/.test(line) && /stream error/i.test(line)) {
|
|
211
|
+
detail = line;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (typeof detail === "string" && /\b(?:rate|usage|session)\s+limit\b|\btoo many requests\b/i.test(detail)) {
|
|
215
|
+
onLimit(detail);
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
return {
|
|
219
|
+
push(stream, chunk) {
|
|
220
|
+
const lines = (pending[stream] + chunk).split(/\r?\n/);
|
|
221
|
+
pending[stream] = lines.pop() ?? "";
|
|
222
|
+
for (const line of lines)
|
|
223
|
+
inspect(line, stream);
|
|
224
|
+
},
|
|
225
|
+
flush() {
|
|
226
|
+
inspect(pending.stdout, "stdout");
|
|
227
|
+
inspect(pending.stderr, "stderr");
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
}
|
|
189
231
|
async function runOnHost(options, args) {
|
|
190
232
|
return new Promise((resolve) => {
|
|
191
233
|
let stdout = "";
|
|
@@ -197,6 +239,11 @@ async function runOnHost(options, args) {
|
|
|
197
239
|
cwd: options.cwd,
|
|
198
240
|
env: { ...process.env, ...options.env },
|
|
199
241
|
});
|
|
242
|
+
let capacityError;
|
|
243
|
+
const monitor = capacityMonitor((detail) => {
|
|
244
|
+
capacityError ??= detail;
|
|
245
|
+
(0, kill_agent_cjs_1.killAgent)(child);
|
|
246
|
+
});
|
|
200
247
|
child.stdin?.end();
|
|
201
248
|
options.onProcessStarted?.(child.pid);
|
|
202
249
|
const timer = options.timeoutMs > 0 ? setTimeout(() => {
|
|
@@ -213,21 +260,25 @@ async function runOnHost(options, args) {
|
|
|
213
260
|
child.stdout.on("data", (data) => {
|
|
214
261
|
options.onActivity?.();
|
|
215
262
|
stdout += data.toString();
|
|
263
|
+
monitor.push("stdout", data.toString());
|
|
216
264
|
});
|
|
217
265
|
child.stderr.on("data", (data) => {
|
|
218
266
|
options.onActivity?.();
|
|
219
267
|
stderr += data.toString();
|
|
268
|
+
monitor.push("stderr", data.toString());
|
|
220
269
|
});
|
|
221
270
|
child.on("error", (error) => {
|
|
222
271
|
clearTimeout(timer);
|
|
223
272
|
options.signal?.removeEventListener("abort", onAbort);
|
|
224
273
|
stderr += `\n${String(error)}`;
|
|
225
|
-
|
|
274
|
+
monitor.flush();
|
|
275
|
+
resolve({ exitCode: null, timedOut, killedByLeaseLoss, stdout, stderr, capacityError });
|
|
226
276
|
});
|
|
227
277
|
child.on("close", (code) => {
|
|
228
278
|
clearTimeout(timer);
|
|
229
279
|
options.signal?.removeEventListener("abort", onAbort);
|
|
230
|
-
|
|
280
|
+
monitor.flush();
|
|
281
|
+
resolve({ exitCode: code, timedOut, killedByLeaseLoss, stdout, stderr, capacityError });
|
|
231
282
|
});
|
|
232
283
|
});
|
|
233
284
|
}
|
|
@@ -236,8 +287,19 @@ async function runViaDocker(options, args) {
|
|
|
236
287
|
const quoted = [options.bin, ...args].map(shellQuote).join(" ");
|
|
237
288
|
const command = `[ -f /tmp/session.env ] && . /tmp/session.env; cd repo 2>/dev/null; ${quoted}`;
|
|
238
289
|
let killedByLeaseLoss = false;
|
|
290
|
+
let capacityError;
|
|
291
|
+
const controller = new AbortController();
|
|
292
|
+
const streamed = { stdout: false, stderr: false };
|
|
293
|
+
const monitor = capacityMonitor((detail) => {
|
|
294
|
+
if (capacityError)
|
|
295
|
+
return;
|
|
296
|
+
capacityError = detail;
|
|
297
|
+
controller.abort();
|
|
298
|
+
runner.run("docker", ["kill", containerName], { timeoutMs: 10_000 }).catch(() => undefined);
|
|
299
|
+
});
|
|
239
300
|
const onAbort = () => {
|
|
240
301
|
killedByLeaseLoss = true;
|
|
302
|
+
controller.abort();
|
|
241
303
|
runner.run("docker", ["kill", containerName], { timeoutMs: 10_000 }).catch(() => undefined);
|
|
242
304
|
};
|
|
243
305
|
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
@@ -247,10 +309,19 @@ async function runViaDocker(options, args) {
|
|
|
247
309
|
const forwardedEnv = Object.keys(options.env).flatMap((name) => ["--env", name]);
|
|
248
310
|
const result = await runner.run("docker", ["exec", ...forwardedEnv, containerName, "sh", "-c", command], { timeoutMs: options.timeoutMs,
|
|
249
311
|
onActivity: options.onActivity,
|
|
250
|
-
signal:
|
|
312
|
+
signal: controller.signal,
|
|
313
|
+
onStdout: (chunk) => { streamed.stdout = true; monitor.push("stdout", chunk); },
|
|
314
|
+
onStderr: (chunk) => { streamed.stderr = true; monitor.push("stderr", chunk); },
|
|
251
315
|
env: { ...process.env, ...options.env },
|
|
252
316
|
});
|
|
317
|
+
// Also inspect runners that only return buffered output.
|
|
318
|
+
if (!streamed.stdout)
|
|
319
|
+
monitor.push("stdout", result.stdout);
|
|
320
|
+
if (!streamed.stderr)
|
|
321
|
+
monitor.push("stderr", result.stderr);
|
|
322
|
+
monitor.flush();
|
|
253
323
|
return {
|
|
324
|
+
capacityError,
|
|
254
325
|
exitCode: result.code,
|
|
255
326
|
timedOut: false,
|
|
256
327
|
killedByLeaseLoss,
|
|
@@ -260,6 +331,7 @@ async function runViaDocker(options, args) {
|
|
|
260
331
|
}
|
|
261
332
|
catch (error) {
|
|
262
333
|
return {
|
|
334
|
+
capacityError,
|
|
263
335
|
exitCode: null,
|
|
264
336
|
timedOut: false,
|
|
265
337
|
killedByLeaseLoss,
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.qoderAdapter = void 0;
|
|
4
|
+
exports.runQoderAdapter = runQoderAdapter;
|
|
5
|
+
const node_child_process_1 = require("node:child_process");
|
|
6
|
+
const exit_conditions_cjs_1 = require("../exit-conditions.cjs");
|
|
7
|
+
const kill_agent_cjs_1 = require("../kill-agent.cjs");
|
|
8
|
+
function hasFlag(args, flag) {
|
|
9
|
+
return args.some((arg) => arg === flag || arg.startsWith(`${flag}=`));
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Headless Qoder CLI adapter: `qoder -p "<prompt>" --output-format json`.
|
|
13
|
+
* Qoder's print mode emits the same single-JSON result shape as Claude Code
|
|
14
|
+
* (verified against qodercli 1.1.56), so usage/cost parsing reuses
|
|
15
|
+
* parseClaudeJsonResult and its result text lands in the ordinary
|
|
16
|
+
* exit-conditions path.
|
|
17
|
+
*
|
|
18
|
+
* Model selection is account-owned (`qoder --list-models`), so the claim-time
|
|
19
|
+
* `"default"` sentinel (execution-policy.ts / session.cts) is passed through
|
|
20
|
+
* as no `--model` flag; an explicit pin from `--model` at enroll or
|
|
21
|
+
* NAVARCH_QODER_EXTRA_ARGS still reaches the CLI.
|
|
22
|
+
*/
|
|
23
|
+
async function runQoderAdapter(options) {
|
|
24
|
+
if (options.signal?.aborted) {
|
|
25
|
+
return { exitCode: null, timedOut: false, killedByLeaseLoss: true, stdout: "", stderr: "" };
|
|
26
|
+
}
|
|
27
|
+
const args = ["-p", options.prompt];
|
|
28
|
+
// Runtime sessions must not inherit an operator's personal or project-local
|
|
29
|
+
// Qoder hooks, for the same reason as the Claude adapter: they make
|
|
30
|
+
// execution machine-dependent and can fail otherwise-good sessions. The
|
|
31
|
+
// empty source list still permits an explicit --settings file below.
|
|
32
|
+
if (!hasFlag(options.extraArgs, "--setting-sources")) {
|
|
33
|
+
args.push("--setting-sources", "");
|
|
34
|
+
}
|
|
35
|
+
// Unattended sessions route permission decisions through Qoder's native
|
|
36
|
+
// auto classifier rather than prompting or bypassing checks.
|
|
37
|
+
if (!["--permission-mode", "--permission-prompt-tool", "--dangerously-skip-permissions"].some((flag) => hasFlag(options.extraArgs, flag))) {
|
|
38
|
+
args.push("--permission-mode", "auto");
|
|
39
|
+
}
|
|
40
|
+
if (options.settingsPath && !hasFlag(options.extraArgs, "--settings")) {
|
|
41
|
+
args.push("--settings", options.settingsPath);
|
|
42
|
+
}
|
|
43
|
+
if (options.mcpConfigPath) {
|
|
44
|
+
args.push("--mcp-config", options.mcpConfigPath);
|
|
45
|
+
}
|
|
46
|
+
if (!hasFlag(options.extraArgs, "--output-format")) {
|
|
47
|
+
args.push("--output-format", "json");
|
|
48
|
+
}
|
|
49
|
+
args.push(...options.extraArgs);
|
|
50
|
+
if (options.model && options.model !== "default" && !hasFlag(options.extraArgs, "--model")) {
|
|
51
|
+
args.push("--model", options.model);
|
|
52
|
+
}
|
|
53
|
+
if (options.reasoningEffort && !hasFlag(options.extraArgs, "--reasoning-effort")) {
|
|
54
|
+
args.push("--reasoning-effort", options.reasoningEffort);
|
|
55
|
+
}
|
|
56
|
+
const raw = options.dockerExec ? await runViaDocker(options, args) : await runOnHost(options, args);
|
|
57
|
+
return attachUsage(raw);
|
|
58
|
+
}
|
|
59
|
+
/** Parses stdout for `qoder -p --output-format json` usage and folds it onto the raw result (best-effort). */
|
|
60
|
+
function attachUsage(result) {
|
|
61
|
+
const parsed = (0, exit_conditions_cjs_1.parseClaudeJsonResult)(result.stdout);
|
|
62
|
+
if (!parsed)
|
|
63
|
+
return result;
|
|
64
|
+
const usage = (0, exit_conditions_cjs_1.extractUsageFromClaudeJson)(parsed);
|
|
65
|
+
return {
|
|
66
|
+
...result,
|
|
67
|
+
tokensIn: usage.tokensIn,
|
|
68
|
+
tokensOut: usage.tokensOut,
|
|
69
|
+
cacheHitTokensIn: usage.cacheHitTokensIn,
|
|
70
|
+
costUsd: usage.costUsd,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
async function runOnHost(options, args) {
|
|
74
|
+
return new Promise((resolve) => {
|
|
75
|
+
let stdout = "";
|
|
76
|
+
let stderr = "";
|
|
77
|
+
let timedOut = false;
|
|
78
|
+
let killedByLeaseLoss = false;
|
|
79
|
+
const child = (0, node_child_process_1.spawn)(options.bin, args, {
|
|
80
|
+
detached: process.platform !== "win32",
|
|
81
|
+
cwd: options.cwd,
|
|
82
|
+
env: { ...process.env, ...options.env },
|
|
83
|
+
});
|
|
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
|
+
options.onProcessStarted?.(child.pid);
|
|
88
|
+
const timer = options.timeoutMs > 0 ? setTimeout(() => {
|
|
89
|
+
timedOut = true;
|
|
90
|
+
(0, kill_agent_cjs_1.killAgent)(child);
|
|
91
|
+
}, options.timeoutMs) : undefined;
|
|
92
|
+
const onAbort = () => {
|
|
93
|
+
killedByLeaseLoss = true;
|
|
94
|
+
(0, kill_agent_cjs_1.killAgent)(child);
|
|
95
|
+
};
|
|
96
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
97
|
+
if (options.signal?.aborted)
|
|
98
|
+
onAbort();
|
|
99
|
+
child.stdout.on("data", (d) => {
|
|
100
|
+
options.onActivity?.();
|
|
101
|
+
stdout += d.toString();
|
|
102
|
+
});
|
|
103
|
+
child.stderr.on("data", (d) => {
|
|
104
|
+
options.onActivity?.();
|
|
105
|
+
stderr += d.toString();
|
|
106
|
+
});
|
|
107
|
+
child.on("error", (err) => {
|
|
108
|
+
clearTimeout(timer);
|
|
109
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
110
|
+
stderr += `\n${String(err)}`;
|
|
111
|
+
resolve({ exitCode: null, timedOut, killedByLeaseLoss, stdout, stderr });
|
|
112
|
+
});
|
|
113
|
+
child.on("close", (code) => {
|
|
114
|
+
clearTimeout(timer);
|
|
115
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
116
|
+
resolve({ exitCode: code, timedOut, killedByLeaseLoss, stdout, stderr });
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
async function runViaDocker(options, args) {
|
|
121
|
+
const { containerName, runner } = options.dockerExec;
|
|
122
|
+
const quoted = [options.bin, ...args].map(shellQuote).join(" ");
|
|
123
|
+
const command = `[ -f /tmp/session.env ] && . /tmp/session.env; cd repo 2>/dev/null; ${quoted}`;
|
|
124
|
+
let killedByLeaseLoss = false;
|
|
125
|
+
const onAbort = () => {
|
|
126
|
+
killedByLeaseLoss = true;
|
|
127
|
+
runner.run("docker", ["kill", containerName], { timeoutMs: 10_000 }).catch(() => undefined);
|
|
128
|
+
};
|
|
129
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
130
|
+
if (options.signal?.aborted)
|
|
131
|
+
onAbort();
|
|
132
|
+
try {
|
|
133
|
+
const result = await runner.run("docker", ["exec", containerName, "sh", "-c", command], {
|
|
134
|
+
timeoutMs: options.timeoutMs,
|
|
135
|
+
onActivity: options.onActivity,
|
|
136
|
+
signal: options.signal,
|
|
137
|
+
});
|
|
138
|
+
return {
|
|
139
|
+
exitCode: result.code,
|
|
140
|
+
timedOut: false,
|
|
141
|
+
killedByLeaseLoss,
|
|
142
|
+
stdout: result.stdout,
|
|
143
|
+
stderr: result.stderr,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
return {
|
|
148
|
+
exitCode: null,
|
|
149
|
+
timedOut: false,
|
|
150
|
+
killedByLeaseLoss,
|
|
151
|
+
stdout: "",
|
|
152
|
+
stderr: String(err),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function shellQuote(value) {
|
|
160
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
161
|
+
}
|
|
162
|
+
/** The AgentAdapter (adapters/types.cts) wrapper session.cts selects via NAVARCH_AGENT=qoder. */
|
|
163
|
+
exports.qoderAdapter = {
|
|
164
|
+
agentType: "qoder",
|
|
165
|
+
run: runQoderAdapter,
|
|
166
|
+
};
|
package/dist/cli.cjs
CHANGED
|
@@ -61,18 +61,24 @@ function superviseInvocation(configDir) {
|
|
|
61
61
|
return `${invocation("supervise")} --config-dir ${configDir}`;
|
|
62
62
|
}
|
|
63
63
|
/** Resolve CLI-local configuration without mutating the parent shell environment. */
|
|
64
|
-
function configFromFlags(flags) {
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
function configFromFlags(flags, identity) {
|
|
65
|
+
const env = { ...process.env };
|
|
66
|
+
if (flags["config-dir"])
|
|
67
|
+
env.NAVARCH_CONFIG_DIR = node_path_1.default.resolve(flags["config-dir"]);
|
|
68
|
+
// Resolve the preferred adapter before deriving the advertised runtimes and
|
|
69
|
+
// applying host guards. Keep an explicit NAVARCH_RUNTIMES list intact.
|
|
70
|
+
const agentType = agentFromFlag(flags) ?? (env.NAVARCH_AGENT || identity?.agent_type);
|
|
71
|
+
if (agentType)
|
|
72
|
+
env.NAVARCH_AGENT = agentType;
|
|
73
|
+
const config = (0, config_cjs_1.loadRuntimeConfig)(env);
|
|
74
|
+
return { ...config, model: modelFromFlags(flags, config.agentType, identity) };
|
|
69
75
|
}
|
|
70
76
|
function agentFromFlag(flags) {
|
|
71
77
|
const value = flags.agent;
|
|
72
78
|
if (value === undefined)
|
|
73
79
|
return undefined;
|
|
74
80
|
if (!(0, config_cjs_1.isRuntimeAgentType)(value)) {
|
|
75
|
-
throw new Error("--agent must be one of 'claude-code', 'codex', 'gemini', 'opencode', or '
|
|
81
|
+
throw new Error("--agent must be one of 'claude-code', 'codex', 'gemini', 'opencode', 'acp', or 'qoder'.");
|
|
76
82
|
}
|
|
77
83
|
return value;
|
|
78
84
|
}
|
|
@@ -202,12 +208,7 @@ async function connectCommand(flags) {
|
|
|
202
208
|
async function startCommand(flags) {
|
|
203
209
|
const baseConfig = configFromFlags(flags);
|
|
204
210
|
const identity = await (0, machine_store_cjs_1.resolveMachineIdentity)(baseConfig.configDir, baseConfig.apiBase);
|
|
205
|
-
|
|
206
|
-
// wins, followed by NAVARCH_AGENT, the choice saved at connect/register
|
|
207
|
-
// time, and finally the backwards-compatible Claude Code default.
|
|
208
|
-
const agentType = agentFromFlag(flags) ??
|
|
209
|
-
(process.env.NAVARCH_AGENT ? baseConfig.agentType : identity.agent_type ?? baseConfig.agentType);
|
|
210
|
-
const config = { ...baseConfig, agentType, model: modelFromFlags(flags, agentType, identity) };
|
|
211
|
+
const config = configFromFlags(flags, identity);
|
|
211
212
|
const api = new api_cjs_1.NavarchApiClient({ baseUrl: identity.api_base, token: identity.token });
|
|
212
213
|
const capacity = new capacity_cjs_1.CapacityTracker(config.maxSessions);
|
|
213
214
|
const runtimeShutdown = new AbortController();
|
|
@@ -283,19 +284,17 @@ async function startCommand(flags) {
|
|
|
283
284
|
process.on("SIGTERM", shutdown);
|
|
284
285
|
}
|
|
285
286
|
async function superviseCommand(flags) {
|
|
286
|
-
const
|
|
287
|
-
const identity = await (0, machine_store_cjs_1.resolveMachineIdentity)(
|
|
287
|
+
const baseConfig = configFromFlags(flags);
|
|
288
|
+
const identity = await (0, machine_store_cjs_1.resolveMachineIdentity)(baseConfig.configDir, baseConfig.apiBase);
|
|
289
|
+
const config = configFromFlags(flags, identity);
|
|
288
290
|
// Resolve and pin the adapter before spawning the worker. The supervisor
|
|
289
291
|
// passes machine credentials through the environment, so the child no
|
|
290
292
|
// longer reads machine.json for identity fields (including agent_type).
|
|
291
293
|
// Without an explicit worker argument, any saved non-Claude selection
|
|
292
294
|
// would therefore fall back to Claude Code.
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
const model = modelFromFlags(flags, agentType, identity);
|
|
297
|
-
if (model)
|
|
298
|
-
workerArgs.push("--model", model);
|
|
295
|
+
const workerArgs = ["--agent", config.agentType];
|
|
296
|
+
if (config.model)
|
|
297
|
+
workerArgs.push("--model", config.model);
|
|
299
298
|
// Pin the identity for this supervisor's lifetime. Without this snapshot, a
|
|
300
299
|
// replacement worker rereads machine.json after an automatic update and can
|
|
301
300
|
// silently become a different machine if another terminal reused the same
|
|
@@ -315,8 +314,17 @@ async function doctorCommand(flags) {
|
|
|
315
314
|
// start (config.cts). Report that as the diagnosis instead of rethrowing it
|
|
316
315
|
// as a bare CLI error.
|
|
317
316
|
let config;
|
|
317
|
+
let identity;
|
|
318
318
|
try {
|
|
319
319
|
config = configFromFlags(flags);
|
|
320
|
+
try {
|
|
321
|
+
identity = await (0, machine_store_cjs_1.resolveMachineIdentity)(config.configDir, config.apiBase);
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
// Unregistered machines can still inspect their environment configuration.
|
|
325
|
+
}
|
|
326
|
+
if (identity)
|
|
327
|
+
config = configFromFlags(flags, identity);
|
|
320
328
|
}
|
|
321
329
|
catch (err) {
|
|
322
330
|
console.log(`config: UNUSABLE — ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -333,18 +341,12 @@ async function doctorCommand(flags) {
|
|
|
333
341
|
console.log(`worktree_guard: ${config.worktreeGuard ? "on" : "off"}`);
|
|
334
342
|
console.log(`runtimes: ${config.runtimes?.join(", ") ?? config.agentType}`);
|
|
335
343
|
console.log(`docker: ${dockerOk ? "available" : "NOT AVAILABLE (docker-backed sessions will fail)"}`);
|
|
336
|
-
|
|
337
|
-
try {
|
|
338
|
-
identity = await (0, machine_store_cjs_1.resolveMachineIdentity)(config.configDir, config.apiBase);
|
|
339
|
-
}
|
|
340
|
-
catch {
|
|
344
|
+
if (!identity) {
|
|
341
345
|
console.log(`machine: not registered — run \`${invocation("register")}\``);
|
|
342
346
|
return;
|
|
343
347
|
}
|
|
344
|
-
const agentType = agentFromFlag(flags) ??
|
|
345
|
-
(process.env.NAVARCH_AGENT ? config.agentType : identity.agent_type ?? config.agentType);
|
|
346
348
|
console.log(`machine: ${identity.name} (${identity.machine_id})`);
|
|
347
|
-
console.log(`agent: ${agentType}`);
|
|
349
|
+
console.log(`agent: ${config.agentType}`);
|
|
348
350
|
}
|
|
349
351
|
function helpText() {
|
|
350
352
|
return `navarch-runtime — Navarch machine-side session manager
|
package/dist/config.cjs
CHANGED
|
@@ -17,7 +17,8 @@ function isRuntimeAgentType(value) {
|
|
|
17
17
|
value === "codex" ||
|
|
18
18
|
value === "gemini" ||
|
|
19
19
|
value === "opencode" ||
|
|
20
|
-
value === "acp"
|
|
20
|
+
value === "acp" ||
|
|
21
|
+
value === "qoder");
|
|
21
22
|
}
|
|
22
23
|
function envInt(env, name, fallback) {
|
|
23
24
|
const raw = env[name];
|
|
@@ -123,6 +124,8 @@ function loadRuntimeConfig(env = process.env) {
|
|
|
123
124
|
opencodeExtraArgs: envList(env, "NAVARCH_OPENCODE_EXTRA_ARGS", []),
|
|
124
125
|
acpBin: env.NAVARCH_ACP_BIN ?? "dsh",
|
|
125
126
|
acpExtraArgs: envList(env, "NAVARCH_ACP_EXTRA_ARGS", ["--profile", "acp"]),
|
|
127
|
+
qoderBin: env.NAVARCH_QODER_BIN ?? "qoder",
|
|
128
|
+
qoderExtraArgs: envList(env, "NAVARCH_QODER_EXTRA_ARGS", []),
|
|
126
129
|
gitAuthorName: env.NAVARCH_GIT_AUTHOR_NAME ?? "sagentlab",
|
|
127
130
|
gitAuthorEmail: env.NAVARCH_GIT_AUTHOR_EMAIL ?? "z@sagentlab.com",
|
|
128
131
|
mcpConfigPath: env.NAVARCH_MCP_CONFIG_PATH ?? null,
|
package/dist/exit-conditions.cjs
CHANGED
|
@@ -229,6 +229,16 @@ function mapExitCondition(result) {
|
|
|
229
229
|
evidenceUrls,
|
|
230
230
|
};
|
|
231
231
|
}
|
|
232
|
+
if (result.capacityError) {
|
|
233
|
+
return {
|
|
234
|
+
leaseOutcome: "failed",
|
|
235
|
+
exitStatus: "failed",
|
|
236
|
+
reportSummary: summarize(`Session stopped: provider capacity exhausted. ${result.capacityError}`) +
|
|
237
|
+
(result.checkpointPath ? ` Local recovery checkpoint: ${result.checkpointPath}` : "") +
|
|
238
|
+
(result.retainedWorktreePath ? ` Recovery worktree retained: ${result.retainedWorktreePath}` : ""),
|
|
239
|
+
evidenceUrls,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
232
242
|
if (result.timedOut) {
|
|
233
243
|
return {
|
|
234
244
|
leaseOutcome: "failed",
|
package/dist/machine-store.cjs
CHANGED
|
@@ -52,7 +52,8 @@ async function resolveMachineIdentity(configDir, apiBaseFallback) {
|
|
|
52
52
|
process.env.NAVARCH_AGENT === "claude-code" ||
|
|
53
53
|
process.env.NAVARCH_AGENT === "gemini" ||
|
|
54
54
|
process.env.NAVARCH_AGENT === "opencode" ||
|
|
55
|
-
process.env.NAVARCH_AGENT === "acp"
|
|
55
|
+
process.env.NAVARCH_AGENT === "acp" ||
|
|
56
|
+
process.env.NAVARCH_AGENT === "qoder"
|
|
56
57
|
? process.env.NAVARCH_AGENT
|
|
57
58
|
: undefined,
|
|
58
59
|
};
|
package/dist/sandbox.cjs
CHANGED
|
@@ -40,10 +40,12 @@ exports.nodeCommandRunner = {
|
|
|
40
40
|
child.stdout?.on("data", (d) => {
|
|
41
41
|
opts.onActivity?.();
|
|
42
42
|
stdout += d.toString();
|
|
43
|
+
opts.onStdout?.(d.toString());
|
|
43
44
|
});
|
|
44
45
|
child.stderr?.on("data", (d) => {
|
|
45
46
|
opts.onActivity?.();
|
|
46
47
|
stderr += d.toString();
|
|
48
|
+
opts.onStderr?.(d.toString());
|
|
47
49
|
});
|
|
48
50
|
child.on("error", (err) => {
|
|
49
51
|
if (settled)
|
package/dist/session.cjs
CHANGED
|
@@ -44,7 +44,7 @@ function resolveSessionExecution(config, claimed) {
|
|
|
44
44
|
profile: claimed.task.execution_profile ?? "standard",
|
|
45
45
|
model: runtime === "codex" ? "gpt-6-astra"
|
|
46
46
|
: runtime === "gemini" ? "auto"
|
|
47
|
-
: runtime === "opencode" || runtime === "acp" ? "default"
|
|
47
|
+
: runtime === "opencode" || runtime === "acp" || runtime === "qoder" ? "default"
|
|
48
48
|
: "claude-opus-5",
|
|
49
49
|
reasoning_effort: "medium",
|
|
50
50
|
};
|
|
@@ -229,7 +229,7 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
229
229
|
return;
|
|
230
230
|
try {
|
|
231
231
|
result.checkpointPath = await gitWorktree.saveCheckpoint();
|
|
232
|
-
log.warn(`session ${leaseId} stopped for ${leaseLost ? "lease loss" : result.timeoutReason ?? "duration"}; recovery checkpoint: ${result.checkpointPath}`);
|
|
232
|
+
log.warn(`session ${leaseId} stopped for ${leaseLost ? "lease loss" : result.capacityError ? "provider capacity" : result.timeoutReason ?? "duration"}; recovery checkpoint: ${result.checkpointPath}`);
|
|
233
233
|
}
|
|
234
234
|
catch (error) {
|
|
235
235
|
retainWorktree = true;
|
|
@@ -481,6 +481,7 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
481
481
|
attempts.push(turnResult);
|
|
482
482
|
const capacityLimit = (0, adapter_capacity_cjs_1.detectAdapterCapacityLimit)(turnResult);
|
|
483
483
|
if (capacityLimit) {
|
|
484
|
+
await checkpointForRecovery(turnResult);
|
|
484
485
|
sessionOutcome.claimCooldownUntil = Math.max(sessionOutcome.claimCooldownUntil ?? 0, capacityLimit.retryAtMs);
|
|
485
486
|
}
|
|
486
487
|
// Close the small race between a naturally completed turn and the next
|
|
@@ -495,7 +496,7 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
495
496
|
log.warn(`session ${leaseId} stopped without completion because its lease is no longer active.`);
|
|
496
497
|
return sessionOutcome;
|
|
497
498
|
}
|
|
498
|
-
if (!leaseLost && !turnResult.timedOut && pendingGuidance.length > 0)
|
|
499
|
+
if (!capacityLimit && !leaseLost && !turnResult.timedOut && pendingGuidance.length > 0)
|
|
499
500
|
continue;
|
|
500
501
|
const result = {
|
|
501
502
|
...turnResult,
|
|
@@ -762,6 +763,8 @@ function adapterCommand(config, runtime) {
|
|
|
762
763
|
return { bin: config.opencodeBin, extraArgs: config.opencodeExtraArgs };
|
|
763
764
|
case "acp":
|
|
764
765
|
return { bin: config.acpBin, extraArgs: config.acpExtraArgs };
|
|
766
|
+
case "qoder":
|
|
767
|
+
return { bin: config.qoderBin, extraArgs: config.qoderExtraArgs };
|
|
765
768
|
}
|
|
766
769
|
}
|
|
767
770
|
/**
|
|
@@ -806,6 +809,8 @@ function verificationFailureReport(taskType, report) {
|
|
|
806
809
|
}
|
|
807
810
|
/** Rejection codes another agent turn in the same worktree can plausibly fix. */
|
|
808
811
|
const REMEDIABLE_REJECTION_CODES = new Set([
|
|
812
|
+
"review_fix_required",
|
|
813
|
+
"review_fix_unavailable",
|
|
809
814
|
"pr_required",
|
|
810
815
|
// A video-deliverable build that completed without registering its render:
|
|
811
816
|
// the file usually already exists in the worktree, so the remediation turn
|
package/dist/upload.cjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.uploadTranscript = uploadTranscript;
|
|
4
4
|
/**
|
|
5
|
-
* PUTs an already-redacted transcript to the
|
|
5
|
+
* PUTs an already-redacted transcript to the presigned R2 URL the
|
|
6
6
|
* control plane hands back (schema-design.md §4 sessions.transcript_url;
|
|
7
7
|
* implementation-plan.md WP-07). Kept as its own module so the storage
|
|
8
8
|
* mechanism is a one-function swap if it ever changes.
|
|
@@ -16,9 +16,8 @@ async function uploadTranscript(uploadUrl, content, fetchImpl = fetch) {
|
|
|
16
16
|
body: content,
|
|
17
17
|
});
|
|
18
18
|
if (!response.ok) {
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
// from a bucket misconfiguration. Without it a 403 is unattributable.
|
|
19
|
+
// Include the bounded storage response to distinguish expired signatures
|
|
20
|
+
// from bucket misconfiguration.
|
|
22
21
|
throw new Error(`Transcript upload failed with ${response.status}: ${await readErrorBody(response)}`);
|
|
23
22
|
}
|
|
24
23
|
}
|
package/package.json
CHANGED