@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 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; the redacted transcript is uploaded when the run finishes.
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
@@ -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)}`);
@@ -205,11 +205,15 @@ async function runOnHost(options, args) {
205
205
  onAbort();
206
206
  child.stdout.on("data", (d) => {
207
207
  options.onActivity?.();
208
- stdout += d.toString();
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
- stderr += d.toString();
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 {
@@ -195,11 +195,15 @@ async function runOnHost(options, args) {
195
195
  onAbort();
196
196
  child.stdout.on("data", (d) => {
197
197
  options.onActivity?.();
198
- stdout += d.toString();
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
- stderr += d.toString();
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
  });
@@ -217,11 +217,15 @@ async function runOnHost(options, args) {
217
217
  onAbort();
218
218
  child.stdout.on("data", (data) => {
219
219
  options.onActivity?.();
220
- stdout += data.toString();
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
- stderr += data.toString();
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
- stdout += data.toString();
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
- stderr += data.toString();
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.
@@ -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
- stdout += data.toString();
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
- stderr += data.toString();
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
  });
@@ -102,11 +102,15 @@ async function runOnHost(options, args) {
102
102
  onAbort();
103
103
  child.stdout.on("data", (d) => {
104
104
  options.onActivity?.();
105
- stdout += d.toString();
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
- stderr += d.toString();
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 {
@@ -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 ["HOME", "PATH", "TMPDIR", "CODEX_HOME", "PLAYWRIGHT_BROWSERS_PATH", "XDG_CACHE_HOME"]) {
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
- : "Update the Codex CLI, or run browser tasks with another agent."));
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
- .flatMap((attempt, index) => [
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
- ...(lastCompletion?.transcript_url
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);
@@ -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 = ["/Library/Developer", "/Applications"];
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sagentlab/navarch-runtime",
3
- "version": "0.1.88",
3
+ "version": "0.1.90",
4
4
  "description": "Navarch machine-side session manager: claims delivery tasks and runs them through local coding agents.",
5
5
  "type": "commonjs",
6
6
  "license": "MIT",