@sagentlab/navarch-runtime 0.1.88 → 0.1.90
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 +5 -1
- package/dist/adapters/acp.cjs +2 -0
- package/dist/adapters/claude.cjs +8 -2
- 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/browser-probe.cjs +25 -2
- package/dist/live-transcript.cjs +143 -0
- package/dist/session.cjs +31 -13
- package/dist/worktree-guard.cjs +13 -1
- 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
|
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
|
@@ -205,11 +205,15 @@ async function runOnHost(options, args) {
|
|
|
205
205
|
onAbort();
|
|
206
206
|
child.stdout.on("data", (d) => {
|
|
207
207
|
options.onActivity?.();
|
|
208
|
-
|
|
208
|
+
const text = d.toString();
|
|
209
|
+
stdout += text;
|
|
210
|
+
options.onOutput?.("stdout", text);
|
|
209
211
|
});
|
|
210
212
|
child.stderr.on("data", (d) => {
|
|
211
213
|
options.onActivity?.();
|
|
212
|
-
|
|
214
|
+
const text = d.toString();
|
|
215
|
+
stderr += text;
|
|
216
|
+
options.onOutput?.("stderr", text);
|
|
213
217
|
});
|
|
214
218
|
child.on("error", (err) => {
|
|
215
219
|
clearTimeout(timer);
|
|
@@ -245,6 +249,8 @@ async function runViaDocker(options, args) {
|
|
|
245
249
|
input: options.prompt,
|
|
246
250
|
timeoutMs: options.timeoutMs,
|
|
247
251
|
onActivity: options.onActivity,
|
|
252
|
+
onStdout: (chunk) => options.onOutput?.("stdout", chunk),
|
|
253
|
+
onStderr: (chunk) => options.onOutput?.("stderr", chunk),
|
|
248
254
|
signal: options.signal,
|
|
249
255
|
});
|
|
250
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/browser-probe.cjs
CHANGED
|
@@ -192,7 +192,17 @@ async function probeCodexSandbox(config, deps) {
|
|
|
192
192
|
readRoots: [node_path_1.default.dirname(await node_fs_1.promises.realpath(process.execPath)), node_path_1.default.dirname(playwrightDir)],
|
|
193
193
|
});
|
|
194
194
|
const env = { NAVARCH_PROBE_PLAYWRIGHT: playwrightDir };
|
|
195
|
-
for (const key of [
|
|
195
|
+
for (const key of [
|
|
196
|
+
"HOME",
|
|
197
|
+
"PATH",
|
|
198
|
+
"TMPDIR",
|
|
199
|
+
"CODEX_HOME",
|
|
200
|
+
"PLAYWRIGHT_BROWSERS_PATH",
|
|
201
|
+
"XDG_CACHE_HOME",
|
|
202
|
+
// Agent sessions inherit it; without it Node falls back to the default
|
|
203
|
+
// config path, which can disagree with what sessions actually load.
|
|
204
|
+
"OPENSSL_CONF",
|
|
205
|
+
]) {
|
|
196
206
|
if (process.env[key])
|
|
197
207
|
env[key] = process.env[key];
|
|
198
208
|
}
|
|
@@ -204,12 +214,16 @@ async function probeCodexSandbox(config, deps) {
|
|
|
204
214
|
// bwrap could not find the program it was asked to start inside the
|
|
205
215
|
// sandbox view: a path outside the granted read roots, not an old CLI.
|
|
206
216
|
const hidden = /bwrap: execvp .*No such file or directory/.test(output);
|
|
217
|
+
const opensslConfig = /OpenSSL configuration error/.test(output);
|
|
207
218
|
return ("Could not verify a browser inside the Codex sandbox: `codex sandbox` did not run the probe " +
|
|
208
219
|
`(${result.timedOut ? "timed out" : output.trim() ? summarizeLaunchFailure(output) : `exit ${result.code}`}). ` +
|
|
209
220
|
(hidden
|
|
210
221
|
? "The sandbox cannot see that executable; add its install directory to NAVARCH_GUARD_EXTRA_ROOTS, " +
|
|
211
222
|
"or run browser tasks with another agent."
|
|
212
|
-
:
|
|
223
|
+
: opensslConfig
|
|
224
|
+
? "Node cannot read its OpenSSL config inside the sandbox, so Codex sessions cannot run node either; " +
|
|
225
|
+
"update the Navarch runtime, or add that file's directory to NAVARCH_GUARD_EXTRA_ROOTS."
|
|
226
|
+
: "Update the Codex CLI, or run browser tasks with another agent."));
|
|
213
227
|
}
|
|
214
228
|
const detail = result.timedOut ? "the launch timed out." : summarizeLaunchFailure(output);
|
|
215
229
|
const machPort = /bootstrap_check_in|MachPortRendezvous|mach_port_rendezvous/i.test(output);
|
|
@@ -236,6 +250,15 @@ function summarizeLaunchFailure(output) {
|
|
|
236
250
|
// "[pid=1][err] [1001/210406.593317:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: ..."
|
|
237
251
|
return fatal.replace(/^.*FATAL:[^\]]*\]\s*/, "");
|
|
238
252
|
}
|
|
253
|
+
const opensslIndex = lines.findIndex((line) => /OpenSSL configuration error/.test(line));
|
|
254
|
+
if (opensslIndex >= 0) {
|
|
255
|
+
// "<node>: OpenSSL configuration error:" with the reason on the next line:
|
|
256
|
+
// "...:error:80000001:system library:BIO_new_file:Operation not permitted:...:calling fopen(<path>, rb)"
|
|
257
|
+
const reason = lines[opensslIndex + 1] ?? "";
|
|
258
|
+
const cause = /:system library:[^:]*:([^:]+):/.exec(reason)?.[1];
|
|
259
|
+
const file = /fopen\(([^,]+),/.exec(reason)?.[1];
|
|
260
|
+
return `OpenSSL configuration error${cause ? `: ${cause}` : ""}${file ? ` reading ${node_path_1.default.normalize(file)}` : ""}`;
|
|
261
|
+
}
|
|
239
262
|
const missing = lines.find((line) => /Executable doesn't exist/.test(line));
|
|
240
263
|
if (missing)
|
|
241
264
|
return "Chromium is not installed for the runtime's Playwright.";
|
|
@@ -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/dist/worktree-guard.cjs
CHANGED
|
@@ -138,6 +138,9 @@ function codexToolReadRoots(env = process.env, platform = process.platform) {
|
|
|
138
138
|
node_path_1.default.join(miseDataDir(env, homeDir), "installs"),
|
|
139
139
|
node_path_1.default.join(env.NVM_DIR?.trim() || node_path_1.default.join(homeDir, ".nvm"), "versions"),
|
|
140
140
|
...(platform === "darwin" ? MACOS_TOOLCHAIN_READ_ROOTS : []),
|
|
141
|
+
// An operator-chosen OpenSSL config is read by every Node (and OpenSSL
|
|
142
|
+
// CLI) start; the agent inherits OPENSSL_CONF, so the file must be too.
|
|
143
|
+
...(env.OPENSSL_CONF?.trim() ? [env.OPENSSL_CONF.trim()] : []),
|
|
141
144
|
];
|
|
142
145
|
}
|
|
143
146
|
function miseDataDir(env, homeDir) {
|
|
@@ -209,7 +212,16 @@ function resolveExecutable(bin, env) {
|
|
|
209
212
|
* system prefixes for the Claude hook (bin/worktree-guard-hook.cjs), so this
|
|
210
213
|
* only brings the Codex profile level with it.
|
|
211
214
|
*/
|
|
212
|
-
const MACOS_TOOLCHAIN_READ_ROOTS = [
|
|
215
|
+
const MACOS_TOOLCHAIN_READ_ROOTS = [
|
|
216
|
+
"/Library/Developer",
|
|
217
|
+
"/Applications",
|
|
218
|
+
// Official nodejs.org macOS builds (nvm, fnm, volta, the .pkg installer)
|
|
219
|
+
// load OPENSSLDIR/openssl.cnf on every start and exit with "OpenSSL
|
|
220
|
+
// configuration error: ... Operation not permitted" when seatbelt denies
|
|
221
|
+
// the read, so no `node` or `npm` runs. Grant only the file: the directory
|
|
222
|
+
// also holds private/.
|
|
223
|
+
"/System/Library/OpenSSL/openssl.cnf",
|
|
224
|
+
];
|
|
213
225
|
/**
|
|
214
226
|
* Per-user macOS build state that Xcode and SwiftPM write during a build or
|
|
215
227
|
* test run: simulator devices and DerivedData under `~/Library/Developer`,
|
package/package.json
CHANGED