@sagentlab/navarch-runtime 0.1.87 → 0.1.89
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -7
- package/dist/adapters/acp.cjs +2 -0
- package/dist/adapters/claude.cjs +22 -12
- package/dist/adapters/codex.cjs +8 -2
- package/dist/adapters/gemini.cjs +9 -4
- package/dist/adapters/opencode.cjs +8 -4
- package/dist/adapters/pi.cjs +8 -2
- package/dist/adapters/qoder.cjs +8 -2
- package/dist/exit-conditions.cjs +6 -2
- package/dist/live-transcript.cjs +143 -0
- package/dist/session.cjs +31 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -72,7 +72,11 @@ machine.
|
|
|
72
72
|
During a connected run, the runtime reports preparation, agent start, output
|
|
73
73
|
activity, and completion checks to the control plane. Project members can see
|
|
74
74
|
these phase updates in the task view. The activity signal contains no prompt
|
|
75
|
-
or CLI output
|
|
75
|
+
or CLI output. The redacted transcript is uploaded to the session's private
|
|
76
|
+
transcript object every 10 seconds while the agent produces output (backing
|
|
77
|
+
off to 30–60 seconds for multi-megabyte transcripts), and once more in full
|
|
78
|
+
when the run finishes, so the task view can follow a connected run live.
|
|
79
|
+
Live snapshots hold the current turn's stderr back until the turn ends.
|
|
76
80
|
|
|
77
81
|
The machine operator selects **Claude Code**, **OpenAI Codex**, **Google
|
|
78
82
|
Gemini CLI**, **OpenCode**, or an **Agent Client Protocol (ACP)** agent when connecting a worker. Any selected agent can
|
|
@@ -751,7 +755,7 @@ cli.cts
|
|
|
751
755
|
into Docker when NAVARCH_SANDBOX_MODE=docker
|
|
752
756
|
4. selectAdapter(config.agentType) (adapters/index.cts) picks one AgentAdapter
|
|
753
757
|
(adapters/types.cts) by NAVARCH_AGENT, then .run(...):
|
|
754
|
-
- claudeCodeAdapter (adapters/claude.cts) — `claude -p --mcp-config <path> < prompt`
|
|
758
|
+
- claudeCodeAdapter (adapters/claude.cts) — `claude -p --mcp-config <path> --output-format stream-json --verbose < prompt`
|
|
755
759
|
- codexAdapter (adapters/codex.cts) — `codex exec - --json -c mcp_servers.*=... < prompt`
|
|
756
760
|
- geminiAdapter (adapters/gemini.cts) — `gemini --output-format stream-json < prompt`
|
|
757
761
|
- openCodeAdapter (adapters/opencode.cts) — `opencode run --format json < prompt`
|
|
@@ -871,16 +875,17 @@ Claude Code headless → PR opens on GitHub → complete lands with evidence;
|
|
|
871
875
|
secrets absent from disk after exit"):
|
|
872
876
|
|
|
873
877
|
- Actually running `claude -p` headless and confirming its real
|
|
874
|
-
|
|
875
|
-
`--output-format json` by default
|
|
876
|
-
|
|
878
|
+
result shape (WP-13). `adapters/claude.cts` appends
|
|
879
|
+
`--output-format stream-json --verbose` by default, so the uploaded
|
|
880
|
+
transcript records every message and tool call, and best-effort parses the
|
|
881
|
+
last `type: "result"` record on stdout as a JSON result object shaped like `{ type, subtype, is_error, result,
|
|
877
882
|
session_id, total_cost_usd, usage: { input_tokens, output_tokens,
|
|
878
883
|
cache_creation_input_tokens, cache_read_input_tokens } }` (see
|
|
879
884
|
`exit-conditions.cts#parseClaudeJsonResult` for the exact assumption and
|
|
880
885
|
fallback behavior). Unit tests (`tests/adapter.test.cts`,
|
|
881
886
|
`tests/exit-conditions.test.cts`) cover the parsing and arg-construction
|
|
882
|
-
logic against fixed JSON strings
|
|
883
|
-
|
|
887
|
+
logic against fixed JSON strings recorded from Claude Code 2.1; a future
|
|
888
|
+
CLI release could still change this shape.
|
|
884
889
|
If it doesn't, parsing degrades to `null` and `tokensIn`/`tokensOut`/
|
|
885
890
|
`costUsd` fall back to 0 (session.cts's existing default) — never a thrown
|
|
886
891
|
error — but cost/spend data silently goes missing until the shape is
|
package/dist/adapters/acp.cjs
CHANGED
|
@@ -51,6 +51,7 @@ async function runAcpAdapter(options) {
|
|
|
51
51
|
child.stdout.on("data", (chunk) => {
|
|
52
52
|
options.onActivity?.();
|
|
53
53
|
rawStdout.push(chunk);
|
|
54
|
+
options.onOutput?.("stdout", chunk.toString("utf8"));
|
|
54
55
|
protocolInput.write(chunk);
|
|
55
56
|
});
|
|
56
57
|
child.stdout.on("end", () => protocolInput.end());
|
|
@@ -58,6 +59,7 @@ async function runAcpAdapter(options) {
|
|
|
58
59
|
child.stderr.on("data", (chunk) => {
|
|
59
60
|
options.onActivity?.();
|
|
60
61
|
stderr.push(chunk);
|
|
62
|
+
options.onOutput?.("stderr", chunk);
|
|
61
63
|
});
|
|
62
64
|
child.on("error", (error) => {
|
|
63
65
|
stderr.push(`\n${describeError(error)}`);
|
package/dist/adapters/claude.cjs
CHANGED
|
@@ -13,7 +13,7 @@ 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 --mcp-config platform-mcp.json --output-format json`
|
|
16
|
+
* WP-07): `claude -p --mcp-config platform-mcp.json --output-format stream-json --verbose`
|
|
17
17
|
* with the context bundle on stdin (`claude -p` reads its prompt from stdin
|
|
18
18
|
* when none is given; Linux rejects any single argv string over 128 KiB, so
|
|
19
19
|
* an argv prompt failed `spawn E2BIG` before Claude started). Returns raw stdout/stderr/exit signals (plus
|
|
@@ -25,11 +25,10 @@ function hasFlag(args, flag) {
|
|
|
25
25
|
* (adapters/types.cts) — see adapters/codex.cts for the Codex CLI sibling
|
|
26
26
|
* session.cts picks between via NAVARCH_AGENT (config.cts's `agentType`).
|
|
27
27
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* real shape differs, parseClaudeJsonResult() returns null and
|
|
28
|
+
* `--output-format stream-json` prints one JSON record per line and ends with
|
|
29
|
+
* the same result object (`type: "result"`, `usage`, `total_cost_usd`) that
|
|
30
|
+
* `--output-format json` prints alone; parseClaudeJsonResult() reads that
|
|
31
|
+
* last record. If the real shape differs, parseClaudeJsonResult() returns null and
|
|
33
32
|
* tokensIn/tokensOut/costUsd fall back to 0 (session.cts's existing
|
|
34
33
|
* default) rather than throwing -- see runtime/README.md.
|
|
35
34
|
*/
|
|
@@ -135,9 +134,14 @@ async function runClaudeAttempt(options, automaticFallback) {
|
|
|
135
134
|
// Only append the default when the caller hasn't already asked for a
|
|
136
135
|
// specific --output-format (extraArgs wins so an operator can opt back
|
|
137
136
|
// into plain-text output, e.g. while diagnosing a live-verification
|
|
138
|
-
// mismatch, without an adapter code change).
|
|
139
|
-
|
|
140
|
-
|
|
137
|
+
// mismatch, without an adapter code change). stream-json records every
|
|
138
|
+
// message and tool call, which the task transcript viewer renders; plain
|
|
139
|
+
// `json` printed only the final result. Claude Code requires --verbose for
|
|
140
|
+
// stream-json under --print, and its last record is the same result object.
|
|
141
|
+
if (!hasFlag(options.extraArgs, "--output-format")) {
|
|
142
|
+
args.push("--output-format", "stream-json");
|
|
143
|
+
if (!hasFlag(options.extraArgs, "--verbose"))
|
|
144
|
+
args.push("--verbose");
|
|
141
145
|
}
|
|
142
146
|
args.push(...options.extraArgs);
|
|
143
147
|
// Project/task policy is appended after machine-wide extra arguments so a
|
|
@@ -157,7 +161,7 @@ async function runClaudeAttempt(options, automaticFallback) {
|
|
|
157
161
|
const raw = runOptions.dockerExec ? await runViaDocker(runOptions, args) : await runOnHost(runOptions, args);
|
|
158
162
|
return attachUsage(raw);
|
|
159
163
|
}
|
|
160
|
-
/** Parses
|
|
164
|
+
/** 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). */
|
|
161
165
|
function attachUsage(result) {
|
|
162
166
|
const parsed = (0, exit_conditions_cjs_1.parseClaudeJsonResult)(result.stdout);
|
|
163
167
|
if (!parsed)
|
|
@@ -201,11 +205,15 @@ async function runOnHost(options, args) {
|
|
|
201
205
|
onAbort();
|
|
202
206
|
child.stdout.on("data", (d) => {
|
|
203
207
|
options.onActivity?.();
|
|
204
|
-
|
|
208
|
+
const text = d.toString();
|
|
209
|
+
stdout += text;
|
|
210
|
+
options.onOutput?.("stdout", text);
|
|
205
211
|
});
|
|
206
212
|
child.stderr.on("data", (d) => {
|
|
207
213
|
options.onActivity?.();
|
|
208
|
-
|
|
214
|
+
const text = d.toString();
|
|
215
|
+
stderr += text;
|
|
216
|
+
options.onOutput?.("stderr", text);
|
|
209
217
|
});
|
|
210
218
|
child.on("error", (err) => {
|
|
211
219
|
clearTimeout(timer);
|
|
@@ -241,6 +249,8 @@ async function runViaDocker(options, args) {
|
|
|
241
249
|
input: options.prompt,
|
|
242
250
|
timeoutMs: options.timeoutMs,
|
|
243
251
|
onActivity: options.onActivity,
|
|
252
|
+
onStdout: (chunk) => options.onOutput?.("stdout", chunk),
|
|
253
|
+
onStderr: (chunk) => options.onOutput?.("stderr", chunk),
|
|
244
254
|
signal: options.signal,
|
|
245
255
|
});
|
|
246
256
|
return {
|
package/dist/adapters/codex.cjs
CHANGED
|
@@ -195,11 +195,15 @@ async function runOnHost(options, args) {
|
|
|
195
195
|
onAbort();
|
|
196
196
|
child.stdout.on("data", (d) => {
|
|
197
197
|
options.onActivity?.();
|
|
198
|
-
|
|
198
|
+
const text = d.toString();
|
|
199
|
+
stdout += text;
|
|
200
|
+
options.onOutput?.("stdout", text);
|
|
199
201
|
});
|
|
200
202
|
child.stderr.on("data", (d) => {
|
|
201
203
|
options.onActivity?.();
|
|
202
|
-
|
|
204
|
+
const text = d.toString();
|
|
205
|
+
stderr += text;
|
|
206
|
+
options.onOutput?.("stderr", text);
|
|
203
207
|
});
|
|
204
208
|
child.on("error", (err) => {
|
|
205
209
|
clearTimeout(timer);
|
|
@@ -237,6 +241,8 @@ async function runViaDocker(options, args) {
|
|
|
237
241
|
input: options.prompt,
|
|
238
242
|
timeoutMs: options.timeoutMs,
|
|
239
243
|
onActivity: options.onActivity,
|
|
244
|
+
onStdout: (chunk) => options.onOutput?.("stdout", chunk),
|
|
245
|
+
onStderr: (chunk) => options.onOutput?.("stderr", chunk),
|
|
240
246
|
signal: options.signal,
|
|
241
247
|
env: { ...process.env, ...options.env },
|
|
242
248
|
});
|
package/dist/adapters/gemini.cjs
CHANGED
|
@@ -217,11 +217,15 @@ async function runOnHost(options, args) {
|
|
|
217
217
|
onAbort();
|
|
218
218
|
child.stdout.on("data", (data) => {
|
|
219
219
|
options.onActivity?.();
|
|
220
|
-
|
|
220
|
+
const text = data.toString();
|
|
221
|
+
stdout += text;
|
|
222
|
+
options.onOutput?.("stdout", text);
|
|
221
223
|
});
|
|
222
224
|
child.stderr.on("data", (data) => {
|
|
223
225
|
options.onActivity?.();
|
|
224
|
-
|
|
226
|
+
const text = data.toString();
|
|
227
|
+
stderr += text;
|
|
228
|
+
options.onOutput?.("stderr", text);
|
|
225
229
|
});
|
|
226
230
|
child.on("error", (err) => {
|
|
227
231
|
clearTimeout(timer);
|
|
@@ -255,9 +259,10 @@ async function runViaDocker(options, args) {
|
|
|
255
259
|
const result = await runner.run("docker", ["exec", "-i", ...forwardedEnv, containerName, "sh", "-c", command], { input: options.prompt,
|
|
256
260
|
timeoutMs: options.timeoutMs,
|
|
257
261
|
onActivity: options.onActivity,
|
|
262
|
+
onStdout: (chunk) => options.onOutput?.("stdout", chunk),
|
|
263
|
+
onStderr: (chunk) => options.onOutput?.("stderr", chunk),
|
|
258
264
|
signal: options.signal,
|
|
259
|
-
env: { ...process.env, ...options.env },
|
|
260
|
-
});
|
|
265
|
+
env: { ...process.env, ...options.env }, });
|
|
261
266
|
return {
|
|
262
267
|
exitCode: result.code,
|
|
263
268
|
timedOut: false,
|
|
@@ -356,12 +356,16 @@ async function runOnHost(options, args, input = "") {
|
|
|
356
356
|
onAbort();
|
|
357
357
|
child.stdout.on("data", (data) => {
|
|
358
358
|
options.onActivity?.();
|
|
359
|
-
|
|
359
|
+
const text = data.toString();
|
|
360
|
+
stdout += text;
|
|
361
|
+
options.onOutput?.("stdout", text);
|
|
360
362
|
monitor.push("stdout", data.toString());
|
|
361
363
|
});
|
|
362
364
|
child.stderr.on("data", (data) => {
|
|
363
365
|
options.onActivity?.();
|
|
364
|
-
|
|
366
|
+
const text = data.toString();
|
|
367
|
+
stderr += text;
|
|
368
|
+
options.onOutput?.("stderr", text);
|
|
365
369
|
monitor.push("stderr", data.toString());
|
|
366
370
|
});
|
|
367
371
|
child.on("error", (error) => {
|
|
@@ -408,8 +412,8 @@ async function runViaDocker(options, args, input) {
|
|
|
408
412
|
timeoutMs: options.timeoutMs,
|
|
409
413
|
onActivity: options.onActivity,
|
|
410
414
|
signal: controller.signal,
|
|
411
|
-
onStdout: (chunk) => { streamed.stdout = true; monitor.push("stdout", chunk); },
|
|
412
|
-
onStderr: (chunk) => { streamed.stderr = true; monitor.push("stderr", chunk); },
|
|
415
|
+
onStdout: (chunk) => { streamed.stdout = true; monitor.push("stdout", chunk); options.onOutput?.("stdout", chunk); },
|
|
416
|
+
onStderr: (chunk) => { streamed.stderr = true; monitor.push("stderr", chunk); options.onOutput?.("stderr", chunk); },
|
|
413
417
|
env: { ...process.env, ...options.env },
|
|
414
418
|
});
|
|
415
419
|
// Also inspect runners that only return buffered output.
|
package/dist/adapters/pi.cjs
CHANGED
|
@@ -344,11 +344,15 @@ async function runOnHost(options, args, local = false) {
|
|
|
344
344
|
onAbort();
|
|
345
345
|
child.stdout.on("data", (data) => {
|
|
346
346
|
options.onActivity?.();
|
|
347
|
-
|
|
347
|
+
const text = data.toString();
|
|
348
|
+
stdout += text;
|
|
349
|
+
options.onOutput?.("stdout", text);
|
|
348
350
|
});
|
|
349
351
|
child.stderr.on("data", (data) => {
|
|
350
352
|
options.onActivity?.();
|
|
351
|
-
|
|
353
|
+
const text = data.toString();
|
|
354
|
+
stderr += text;
|
|
355
|
+
options.onOutput?.("stderr", text);
|
|
352
356
|
});
|
|
353
357
|
child.on("error", (err) => {
|
|
354
358
|
clearTimeout(timer);
|
|
@@ -408,6 +412,8 @@ async function runViaDocker(options, args) {
|
|
|
408
412
|
input: options.prompt,
|
|
409
413
|
timeoutMs: options.timeoutMs,
|
|
410
414
|
onActivity: options.onActivity,
|
|
415
|
+
onStdout: (chunk) => options.onOutput?.("stdout", chunk),
|
|
416
|
+
onStderr: (chunk) => options.onOutput?.("stderr", chunk),
|
|
411
417
|
signal: options.signal,
|
|
412
418
|
env: { ...process.env, ...options.env },
|
|
413
419
|
});
|
package/dist/adapters/qoder.cjs
CHANGED
|
@@ -102,11 +102,15 @@ async function runOnHost(options, args) {
|
|
|
102
102
|
onAbort();
|
|
103
103
|
child.stdout.on("data", (d) => {
|
|
104
104
|
options.onActivity?.();
|
|
105
|
-
|
|
105
|
+
const text = d.toString();
|
|
106
|
+
stdout += text;
|
|
107
|
+
options.onOutput?.("stdout", text);
|
|
106
108
|
});
|
|
107
109
|
child.stderr.on("data", (d) => {
|
|
108
110
|
options.onActivity?.();
|
|
109
|
-
|
|
111
|
+
const text = d.toString();
|
|
112
|
+
stderr += text;
|
|
113
|
+
options.onOutput?.("stderr", text);
|
|
110
114
|
});
|
|
111
115
|
child.on("error", (err) => {
|
|
112
116
|
clearTimeout(timer);
|
|
@@ -139,6 +143,8 @@ async function runViaDocker(options, args) {
|
|
|
139
143
|
input: options.prompt,
|
|
140
144
|
timeoutMs: options.timeoutMs,
|
|
141
145
|
onActivity: options.onActivity,
|
|
146
|
+
onStdout: (chunk) => options.onOutput?.("stdout", chunk),
|
|
147
|
+
onStderr: (chunk) => options.onOutput?.("stderr", chunk),
|
|
142
148
|
signal: options.signal,
|
|
143
149
|
});
|
|
144
150
|
return {
|
package/dist/exit-conditions.cjs
CHANGED
|
@@ -24,8 +24,9 @@ function agentAuthFailureReport(detail) {
|
|
|
24
24
|
"Log the coding agent in again on this machine (`claude /login` or `codex login`), then resume the agent.";
|
|
25
25
|
}
|
|
26
26
|
/**
|
|
27
|
-
* Best-effort parse of `claude -p --output-format json`
|
|
28
|
-
*
|
|
27
|
+
* Best-effort parse of `claude -p --output-format json` (or `stream-json`,
|
|
28
|
+
* whose last result record has the same shape) stdout into the assumed
|
|
29
|
+
* result shape above. Tries the whole trimmed stdout first (the
|
|
29
30
|
* expected case); if that fails, tries each non-empty line that looks like a
|
|
30
31
|
* JSON object, in case a warning/log line preceded the result (last such
|
|
31
32
|
* line wins, since the final result is expected last). Returns null rather
|
|
@@ -39,6 +40,9 @@ function parseClaudeJsonResult(stdout) {
|
|
|
39
40
|
const isUsableResult = (value) => typeof value === "object" &&
|
|
40
41
|
value !== null &&
|
|
41
42
|
!Array.isArray(value) &&
|
|
43
|
+
// stream-json output interleaves typed message/tool records with the
|
|
44
|
+
// final `type: "result"` record; only that record carries the outcome.
|
|
45
|
+
(!("type" in value) || value.type === "result") &&
|
|
42
46
|
(!("result" in value) || typeof value.result === "string") &&
|
|
43
47
|
("result" in value || "usage" in value || "cost_usd" in value || "total_cost_usd" in value);
|
|
44
48
|
try {
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LiveTranscriptPublisher = void 0;
|
|
4
|
+
exports.renderSessionTranscript = renderSessionTranscript;
|
|
5
|
+
exports.renderLiveTranscript = renderLiveTranscript;
|
|
6
|
+
exports.liveUploadIntervalMs = liveUploadIntervalMs;
|
|
7
|
+
/** The shape both the live snapshots and the completion upload use. */
|
|
8
|
+
function renderSessionTranscript(attempts, redact) {
|
|
9
|
+
return attempts
|
|
10
|
+
.flatMap((attempt, index) => [
|
|
11
|
+
`# agent turn ${index + 1} stdout`,
|
|
12
|
+
redact(attempt.stdout),
|
|
13
|
+
"",
|
|
14
|
+
`# agent turn ${index + 1} stderr`,
|
|
15
|
+
redact(attempt.stderr),
|
|
16
|
+
"",
|
|
17
|
+
])
|
|
18
|
+
.join("\n");
|
|
19
|
+
}
|
|
20
|
+
function renderLiveTranscript(attempts, currentStdout, redact, knownSecrets = []) {
|
|
21
|
+
const finished = attempts.length ? `${renderSessionTranscript(attempts, redact)}\n` : "";
|
|
22
|
+
const end = currentStdout.lastIndexOf("\n");
|
|
23
|
+
// A secret split across the cut would escape redaction, so redact whole lines only.
|
|
24
|
+
let complete = end >= 0 ? currentStdout.slice(0, end + 1) : "";
|
|
25
|
+
// Complete lines can still contain the beginning of a multiline secret.
|
|
26
|
+
// Hold that line and everything after it until redaction can see the value.
|
|
27
|
+
let safeEnd = complete.length;
|
|
28
|
+
for (const secret of knownSecrets) {
|
|
29
|
+
const newline = secret.indexOf("\n");
|
|
30
|
+
if (newline < 0)
|
|
31
|
+
continue;
|
|
32
|
+
const start = complete.lastIndexOf(secret.slice(0, newline + 1));
|
|
33
|
+
if (start >= 0 && complete.length - start < secret.length && secret.startsWith(complete.slice(start))) {
|
|
34
|
+
safeEnd = Math.min(safeEnd, complete.lastIndexOf("\n", start - 1) + 1);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const pem = /-----BEGIN ([A-Z0-9 ]*PRIVATE KEY)-----/g;
|
|
38
|
+
for (const match of complete.matchAll(pem)) {
|
|
39
|
+
if (!complete.includes(`-----END ${match[1]}-----`, match.index + match[0].length)) {
|
|
40
|
+
safeEnd = Math.min(safeEnd, complete.lastIndexOf("\n", match.index - 1) + 1);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
complete = complete.slice(0, safeEnd);
|
|
44
|
+
return `${finished}# agent turn ${attempts.length + 1} stdout\n${redact(complete)}`;
|
|
45
|
+
}
|
|
46
|
+
/** Signed upload URLs last an hour; refresh well before that. */
|
|
47
|
+
const UPLOAD_URL_TTL_MS = 45 * 60_000;
|
|
48
|
+
const DEFAULT_INTERVAL_MS = 10_000;
|
|
49
|
+
function liveUploadIntervalMs(bytes, baseMs = DEFAULT_INTERVAL_MS) {
|
|
50
|
+
if (bytes > 8 * 1024 * 1024)
|
|
51
|
+
return Math.max(baseMs, 60_000);
|
|
52
|
+
if (bytes > 1024 * 1024)
|
|
53
|
+
return Math.max(baseMs, 30_000);
|
|
54
|
+
return baseMs;
|
|
55
|
+
}
|
|
56
|
+
class LiveTranscriptPublisher {
|
|
57
|
+
options;
|
|
58
|
+
now;
|
|
59
|
+
intervalMs;
|
|
60
|
+
timer = null;
|
|
61
|
+
inFlight = null;
|
|
62
|
+
dirty = false;
|
|
63
|
+
stopped = false;
|
|
64
|
+
paused = false;
|
|
65
|
+
lastUploadedAt = 0;
|
|
66
|
+
lastUploaded = "";
|
|
67
|
+
uploadUrl = null;
|
|
68
|
+
published = null;
|
|
69
|
+
constructor(options) {
|
|
70
|
+
this.options = options;
|
|
71
|
+
this.now = options.now ?? Date.now;
|
|
72
|
+
this.intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
73
|
+
}
|
|
74
|
+
/** Call whenever output arrives or a turn ends. */
|
|
75
|
+
markDirty() {
|
|
76
|
+
if (this.stopped)
|
|
77
|
+
return;
|
|
78
|
+
this.paused = false;
|
|
79
|
+
this.dirty = true;
|
|
80
|
+
this.schedule();
|
|
81
|
+
}
|
|
82
|
+
/** Locator of the last live snapshot that reached storage, if any. */
|
|
83
|
+
get publishedUrl() {
|
|
84
|
+
return this.published;
|
|
85
|
+
}
|
|
86
|
+
/** Cancels a pending upload and waits for an in-flight one, so neither can
|
|
87
|
+
* overwrite the completion transcript. A later markDirty() re-arms it. */
|
|
88
|
+
async idle() {
|
|
89
|
+
this.paused = true;
|
|
90
|
+
this.dirty = false;
|
|
91
|
+
if (this.timer)
|
|
92
|
+
clearTimeout(this.timer);
|
|
93
|
+
this.timer = null;
|
|
94
|
+
await this.inFlight;
|
|
95
|
+
}
|
|
96
|
+
async stop() {
|
|
97
|
+
this.stopped = true;
|
|
98
|
+
await this.idle();
|
|
99
|
+
}
|
|
100
|
+
schedule() {
|
|
101
|
+
if (this.timer || this.inFlight || this.stopped || this.paused)
|
|
102
|
+
return;
|
|
103
|
+
const gap = liveUploadIntervalMs(this.lastUploaded.length, this.intervalMs);
|
|
104
|
+
const wait = Math.max(0, this.lastUploadedAt + gap - this.now());
|
|
105
|
+
this.timer = setTimeout(() => {
|
|
106
|
+
this.timer = null;
|
|
107
|
+
this.inFlight = this.flush().finally(() => {
|
|
108
|
+
this.inFlight = null;
|
|
109
|
+
if (this.dirty)
|
|
110
|
+
this.schedule();
|
|
111
|
+
});
|
|
112
|
+
}, wait);
|
|
113
|
+
this.timer.unref?.();
|
|
114
|
+
}
|
|
115
|
+
async flush() {
|
|
116
|
+
if (this.stopped)
|
|
117
|
+
return;
|
|
118
|
+
this.dirty = false;
|
|
119
|
+
const content = this.options.render();
|
|
120
|
+
if (content === this.lastUploaded)
|
|
121
|
+
return;
|
|
122
|
+
try {
|
|
123
|
+
if (!this.uploadUrl || this.now() - this.uploadUrl.at > UPLOAD_URL_TTL_MS) {
|
|
124
|
+
const { uploadUrl, publicUrl } = await this.options.requestUploadUrl();
|
|
125
|
+
this.uploadUrl = { url: uploadUrl, publicUrl, at: this.now() };
|
|
126
|
+
}
|
|
127
|
+
if (this.stopped)
|
|
128
|
+
return;
|
|
129
|
+
await this.options.upload(this.uploadUrl.url, content);
|
|
130
|
+
this.lastUploaded = content;
|
|
131
|
+
this.published = this.uploadUrl.publicUrl;
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
// A rejected signature is refreshed on the next attempt.
|
|
135
|
+
this.uploadUrl = null;
|
|
136
|
+
this.options.onError?.(error);
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
this.lastUploadedAt = this.now();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
exports.LiveTranscriptPublisher = LiveTranscriptPublisher;
|
package/dist/session.cjs
CHANGED
|
@@ -15,6 +15,7 @@ const index_cjs_1 = require("./adapters/index.cjs");
|
|
|
15
15
|
const exit_conditions_cjs_1 = require("./exit-conditions.cjs");
|
|
16
16
|
const redact_cjs_1 = require("./redact.cjs");
|
|
17
17
|
const upload_cjs_1 = require("./upload.cjs");
|
|
18
|
+
const live_transcript_cjs_1 = require("./live-transcript.cjs");
|
|
18
19
|
const prompt_cjs_1 = require("./prompt.cjs");
|
|
19
20
|
const mcp_config_cjs_1 = require("./mcp-config.cjs");
|
|
20
21
|
const logger_cjs_1 = require("./logger.cjs");
|
|
@@ -441,6 +442,7 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
441
442
|
abortForRuntimeShutdown();
|
|
442
443
|
lifecycle.enteredMainSessionTry = true;
|
|
443
444
|
let browserEvidence;
|
|
445
|
+
let liveTranscript;
|
|
444
446
|
try {
|
|
445
447
|
reportActivity("preparing");
|
|
446
448
|
await gitWorktree.prepare();
|
|
@@ -494,6 +496,18 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
494
496
|
const adapter = (0, index_cjs_1.selectAdapter)(runtime);
|
|
495
497
|
const { bin, extraArgs } = adapterCommand(config, runtime);
|
|
496
498
|
const attempts = [];
|
|
499
|
+
// The task view follows the run through periodic redacted snapshots of
|
|
500
|
+
// the same object the completion upload below finally overwrites.
|
|
501
|
+
let liveStdout = "";
|
|
502
|
+
liveTranscript = new live_transcript_cjs_1.LiveTranscriptPublisher({
|
|
503
|
+
requestUploadUrl: async () => {
|
|
504
|
+
const { upload_url, public_url } = await api.getTranscriptUploadUrl(leaseId);
|
|
505
|
+
return { uploadUrl: upload_url, publicUrl: public_url };
|
|
506
|
+
},
|
|
507
|
+
upload: upload_cjs_1.uploadTranscript,
|
|
508
|
+
render: () => (0, live_transcript_cjs_1.renderLiveTranscript)(attempts, liveStdout, (text) => (0, redact_cjs_1.redactText)(text, registry.list()), registry.list()),
|
|
509
|
+
onError: (error) => log.warn(`live transcript upload failed for ${leaseId}: ${(0, redact_cjs_1.redactText)(String(error), registry.list())}`),
|
|
510
|
+
});
|
|
497
511
|
const agentBudgetStartedAt = node_perf_hooks_1.performance.now();
|
|
498
512
|
const maxRuntimeMs = task.max_runtime_ms ?? config.sessionTimeoutMs;
|
|
499
513
|
let nextPrompt = null;
|
|
@@ -544,6 +558,12 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
544
558
|
dockerExec: sandbox ? { containerName: sandbox.name, runner: sandbox_cjs_1.nodeCommandRunner } : undefined,
|
|
545
559
|
signal: activeAbortController.signal,
|
|
546
560
|
onActivity: () => { outputObservedAt = Date.now(); outputUpdates += 1; },
|
|
561
|
+
onOutput: (stream, chunk) => {
|
|
562
|
+
if (stream !== "stdout")
|
|
563
|
+
return;
|
|
564
|
+
liveStdout += chunk;
|
|
565
|
+
liveTranscript?.markDirty();
|
|
566
|
+
},
|
|
547
567
|
}, task.idle_timeout_ms ?? config.sessionIdleTimeoutMs ?? 45 * 60_000, runtime === "opencode" ? config.opencodeStartupTimeoutMs : 0);
|
|
548
568
|
activeAbortController = null;
|
|
549
569
|
adapterRunning = false;
|
|
@@ -554,6 +574,8 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
554
574
|
executionReport = { ...executionReport, model: turnResult.model };
|
|
555
575
|
}
|
|
556
576
|
attempts.push(turnResult);
|
|
577
|
+
liveStdout = "";
|
|
578
|
+
liveTranscript.markDirty();
|
|
557
579
|
reportActivity("finishing");
|
|
558
580
|
const capacityLimit = (0, adapter_capacity_cjs_1.detectAdapterCapacityLimit)(turnResult);
|
|
559
581
|
if (capacityLimit) {
|
|
@@ -691,16 +713,8 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
691
713
|
if (mapping.leaseOutcome === "failed") {
|
|
692
714
|
log.warn(`adapter failed for ${leaseId}: ${(0, redact_cjs_1.redactText)(mapping.reportSummary, knownSecrets)}`);
|
|
693
715
|
}
|
|
694
|
-
const transcript = attempts
|
|
695
|
-
|
|
696
|
-
`# agent turn ${index + 1} stdout`,
|
|
697
|
-
(0, redact_cjs_1.redactText)(attempt.stdout, knownSecrets),
|
|
698
|
-
"",
|
|
699
|
-
`# agent turn ${index + 1} stderr`,
|
|
700
|
-
(0, redact_cjs_1.redactText)(attempt.stderr, knownSecrets),
|
|
701
|
-
"",
|
|
702
|
-
])
|
|
703
|
-
.join("\n");
|
|
716
|
+
const transcript = (0, live_transcript_cjs_1.renderSessionTranscript)(attempts, (text) => (0, redact_cjs_1.redactText)(text, knownSecrets));
|
|
717
|
+
await liveTranscript.idle();
|
|
704
718
|
let transcriptUrl;
|
|
705
719
|
try {
|
|
706
720
|
const { upload_url, public_url } = await api.getTranscriptUploadUrl(leaseId);
|
|
@@ -711,6 +725,8 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
711
725
|
// The message carries up to 500 chars of the storage response body, so
|
|
712
726
|
// redact it like every other warn in this block.
|
|
713
727
|
log.warn(`transcript upload failed for ${leaseId}: ${(0, redact_cjs_1.redactText)(String(err), knownSecrets)}`);
|
|
728
|
+
// The last live snapshot is still in storage; keep it reviewable.
|
|
729
|
+
transcriptUrl = liveTranscript.publishedUrl ?? undefined;
|
|
714
730
|
}
|
|
715
731
|
const redactedReport = (0, redact_cjs_1.redactText)(mapping.reportSummary, knownSecrets);
|
|
716
732
|
const completion = {
|
|
@@ -800,6 +816,9 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
800
816
|
}
|
|
801
817
|
}
|
|
802
818
|
catch (err) {
|
|
819
|
+
await liveTranscript?.stop();
|
|
820
|
+
// A live snapshot already in storage is the best transcript a crash has.
|
|
821
|
+
const crashTranscriptUrl = lastCompletion?.transcript_url ?? liveTranscript?.publishedUrl;
|
|
803
822
|
const knownSecrets = registry.list();
|
|
804
823
|
// NavarchApiError's message is only the status line; the control plane's
|
|
805
824
|
// explanation rides in `err.body`. Keep it in the agent log (redacted like
|
|
@@ -820,9 +839,7 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
820
839
|
failed_before_start: !adapterStarted,
|
|
821
840
|
evidence_urls: lastCompletion?.evidence_urls ?? [],
|
|
822
841
|
cost: lastCompletion?.cost ?? { tokens_in: 0, tokens_out: 0, cost_usd: 0 },
|
|
823
|
-
...(
|
|
824
|
-
? { transcript_url: lastCompletion.transcript_url }
|
|
825
|
-
: {}),
|
|
842
|
+
...(crashTranscriptUrl ? { transcript_url: crashTranscriptUrl } : {}),
|
|
826
843
|
exit_status: "crashed",
|
|
827
844
|
agent_type: runtime,
|
|
828
845
|
...executionReport,
|
|
@@ -830,6 +847,7 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
830
847
|
.catch((completeErr) => log.warn(`complete() after crash also failed: ${String(completeErr)}`));
|
|
831
848
|
}
|
|
832
849
|
finally {
|
|
850
|
+
await liveTranscript?.stop();
|
|
833
851
|
deps.shutdownSignal?.removeEventListener("abort", abortForRuntimeShutdown);
|
|
834
852
|
sessionClosing = true;
|
|
835
853
|
clearInterval(heartbeatTimer);
|
package/package.json
CHANGED