@boardwalk-labs/runner 0.1.17 → 0.1.18

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.
@@ -162,7 +162,12 @@ export function makeGuestBrowserBackend(cfg) {
162
162
  mcp.once("error", (err) => {
163
163
  log.error("browser_mcp_spawn_error", { error: err.message });
164
164
  });
165
- await waitForHttp(`http://127.0.0.1:${String(mcpPort)}/mcp`, cfg.readyTimeoutMs);
165
+ // Probe the EXACT url the program's client will connect to (the `localhost` form), not a
166
+ // `127.0.0.1` stand-in: if `localhost` doesn't resolve in the guest, connectSessionMcp would
167
+ // otherwise throw a bare `TypeError: fetch failed` while this readiness check passed on a
168
+ // different address (masking the fault). Probing mcpUrl turns that into a diagnosable
169
+ // `browser_backend_timeout`. (The guest ships /etc/hosts mapping localhost → 127.0.0.1.)
170
+ await waitForHttp(mcpUrl, cfg.readyTimeoutMs);
166
171
  let killed = false;
167
172
  return {
168
173
  cdpUrl,
@@ -49,6 +49,7 @@ import { RunnerControlClient } from "./runner_control_client.js";
49
49
  import { BrokerChildDispatcher } from "./broker_child_dispatcher.js";
50
50
  import { BrokerEventPublisher } from "./broker_event_publisher.js";
51
51
  import { createProgramLogSink } from "./program_log_capture.js";
52
+ import { makeRunLogFileSink } from "./run_log_file_sink.js";
52
53
  import { BrokerArtifactStore } from "./broker_artifact_store.js";
53
54
  import { buildBrokerToolHost } from "./broker_tool_host.js";
54
55
  import { CreditWatcher } from "./credit_watcher.js";
@@ -130,7 +131,18 @@ export function assembleWorkerDeps(runtime) {
130
131
  // declared output, and every agent turn stamp through this emitter, so cursors are run-globally
131
132
  // monotonic with no separate program band. Frames publish as `{cursor, event}` rows via the
132
133
  // broker telemetry path. The claim wrapper below bumps it past a previous session's frames.
133
- const runEvents = new WorkerRunEventEmitter({ runId: runtime.runId, publisher: eventPublisher });
134
+ // Optional on-screen mirror: append the (already-redacted) event stream to a local file an xterm in
135
+ // the ambient desktop tails, so the live-view/recording shows the run working. Opt-in via the env the
136
+ // desktop guest image sets (BOARDWALK_RUN_LOG_FILE); absent everywhere else, so no cost off-desktop.
137
+ const runLogFilePath = process.env.BOARDWALK_RUN_LOG_FILE?.trim();
138
+ const runLogLocalSink = runLogFilePath !== undefined && runLogFilePath !== ""
139
+ ? makeRunLogFileSink(runLogFilePath)
140
+ : undefined;
141
+ const runEvents = new WorkerRunEventEmitter({
142
+ runId: runtime.runId,
143
+ publisher: eventPublisher,
144
+ ...(runLogLocalSink !== undefined ? { localSink: runLogLocalSink } : {}),
145
+ });
134
146
  const phaseTracker = new PhaseTracker({ sink: runEvents });
135
147
  // The run's public-API credential (was BOARDWALK_API_KEY). It is NO LONGER in process.env — the
136
148
  // bootstrap captured + scrubbed it (capturePlatformContext) so the agent's bash / subprocesses
@@ -6,11 +6,16 @@ export interface RunEventEmitterOptions {
6
6
  resumeAfterCursor?: number;
7
7
  /** Injected clock for deterministic tests. Defaults to Date.now. */
8
8
  now?: () => number;
9
+ /** Optional local mirror of every (already-redacted) event — used to surface the run's output in an
10
+ * on-screen terminal in the ambient desktop (docs/SCREEN_CAPTURE.md). Best-effort: a sink throw never
11
+ * affects the run or the broker publish. Only fed events that already passed the run's redactor. */
12
+ localSink?: (event: RunEvent) => void;
9
13
  }
10
14
  export declare class WorkerRunEventEmitter {
11
15
  private readonly runId;
12
16
  private readonly publisher;
13
17
  private readonly now;
18
+ private readonly localSink;
14
19
  private turn;
15
20
  private seq;
16
21
  constructor(opts: RunEventEmitterOptions);
@@ -20,12 +20,14 @@ export class WorkerRunEventEmitter {
20
20
  runId;
21
21
  publisher;
22
22
  now;
23
+ localSink;
23
24
  turn;
24
25
  seq;
25
26
  constructor(opts) {
26
27
  this.runId = opts.runId;
27
28
  this.publisher = opts.publisher;
28
29
  this.now = opts.now ?? Date.now;
30
+ this.localSink = opts.localSink;
29
31
  const max = opts.resumeAfterCursor ?? 0;
30
32
  this.turn = max > 0 ? Math.floor(max / TURN_CURSOR_STRIDE) + 1 : 0;
31
33
  this.seq = 0;
@@ -44,6 +46,14 @@ export class WorkerRunEventEmitter {
44
46
  this.publisher
45
47
  .publish(`run:${this.runId}`, JSON.stringify({ cursor, event }))
46
48
  .catch(() => undefined); // best-effort; the publisher logs its own failures
49
+ if (this.localSink !== undefined) {
50
+ try {
51
+ this.localSink(event); // best-effort on-screen mirror; must never affect the run
52
+ }
53
+ catch {
54
+ /* ignore */
55
+ }
56
+ }
47
57
  return event;
48
58
  }
49
59
  /** Restart resume: jump past the previous session's frames (claim carries the store max). */
@@ -0,0 +1,9 @@
1
+ import type { RunEvent } from "./agent/events.js";
2
+ /** One readable, ANSI-colored line for a run event, or null to skip it (streaming text/reasoning
3
+ * deltas are buffered by the sink, not formatted here). Reads only a few fields off the SDK's typed
4
+ * event and degrades gracefully on anything unexpected. */
5
+ export declare function formatRunEventLine(event: RunEvent): string | null;
6
+ /** Build the {@link WorkerRunEventEmitter} local sink: append formatted lines to `path`, buffering
7
+ * streaming agent text/reasoning into whole lines. Best-effort — a broken stream (full disk, closed
8
+ * pipe) is swallowed and disables the sink; it never throws into the run. */
9
+ export declare function makeRunLogFileSink(path: string): (event: RunEvent) => void;
@@ -0,0 +1,106 @@
1
+ // On-screen run-output mirror. Formats the run's (already-redacted) event stream into readable,
2
+ // ANSI-colored lines appended to a local file, which an xterm in the ambient desktop tails so the
3
+ // live-view / recording shows the run working (docs/SCREEN_CAPTURE.md). Gated on
4
+ // BOARDWALK_RUN_LOG_FILE — the desktop guest image sets it. This ONLY ever sees events that already
5
+ // passed the run's redactor (the same stream the web run-detail renders), so the file is safe to
6
+ // capture in a durable recording.
7
+ import { createWriteStream } from "node:fs";
8
+ function oneLine(s, n) {
9
+ const t = s.replace(/\s+/g, " ").trim();
10
+ return t.length > n ? t.slice(0, n - 1) + "…" : t;
11
+ }
12
+ function safeJson(v) {
13
+ try {
14
+ return typeof v === "string" ? v : JSON.stringify(v);
15
+ }
16
+ catch {
17
+ return String(v);
18
+ }
19
+ }
20
+ /** One readable, ANSI-colored line for a run event, or null to skip it (streaming text/reasoning
21
+ * deltas are buffered by the sink, not formatted here). Reads only a few fields off the SDK's typed
22
+ * event and degrades gracefully on anything unexpected. */
23
+ export function formatRunEventLine(event) {
24
+ const e = event;
25
+ const k = typeof e.kind === "string" ? e.kind : "";
26
+ const s = (v) => (typeof v === "string" ? v : "");
27
+ switch (k) {
28
+ case "run_status":
29
+ return `\x1b[1;32m● ${s(e.status)}\x1b[0m`;
30
+ case "phase":
31
+ return `\x1b[1;36m▸ ${s(e.name)}\x1b[0m`;
32
+ case "program_output": {
33
+ const t = s(e.text).replace(/\s+$/, "");
34
+ return t ? ` ${t}` : null;
35
+ }
36
+ case "turn_started":
37
+ return `\x1b[35m· agent ${s(e.agentName) || s(e.agentId)}\x1b[0m`;
38
+ case "tool_call_start":
39
+ return ` \x1b[33m⚙ ${s(e.toolName)}\x1b[0m`;
40
+ case "tool_output_delta": {
41
+ const t = s(e.text).replace(/\s+$/, "");
42
+ return t ? ` \x1b[90m${oneLine(t, 200)}\x1b[0m` : null;
43
+ }
44
+ case "tool_call_result":
45
+ return ` \x1b[32m✓ done\x1b[0m`;
46
+ case "tool_call_error":
47
+ return ` \x1b[31m✗ tool error\x1b[0m`;
48
+ case "output":
49
+ return `\x1b[1;32m✔ output ${oneLine(safeJson(e.value), 300)}\x1b[0m`;
50
+ case "human_input_requested":
51
+ return `\x1b[1;33m⏸ awaiting human input\x1b[0m`;
52
+ case "human_input_resolved":
53
+ return `\x1b[33m▶ input received\x1b[0m`;
54
+ case "suspended":
55
+ return `\x1b[90m⏸ suspended\x1b[0m`;
56
+ case "resumed":
57
+ return `\x1b[90m▶ resumed\x1b[0m`;
58
+ case "egress_denied":
59
+ return ` \x1b[31m⛔ egress denied\x1b[0m`;
60
+ default:
61
+ return null; // turn_ended, text_*, tool_call_input_*, tool_call_executing — noise for the terminal
62
+ }
63
+ }
64
+ /** Build the {@link WorkerRunEventEmitter} local sink: append formatted lines to `path`, buffering
65
+ * streaming agent text/reasoning into whole lines. Best-effort — a broken stream (full disk, closed
66
+ * pipe) is swallowed and disables the sink; it never throws into the run. */
67
+ export function makeRunLogFileSink(path) {
68
+ let stream = null;
69
+ try {
70
+ stream = createWriteStream(path, { flags: "a" });
71
+ stream.on("error", () => {
72
+ stream = null;
73
+ });
74
+ stream.write("\n\x1b[1;36m── boardwalk run ──\x1b[0m\n");
75
+ }
76
+ catch {
77
+ stream = null;
78
+ }
79
+ let textBuf = "";
80
+ const flushText = () => {
81
+ if (textBuf.trim() !== "" && stream !== null) {
82
+ stream.write(` \x1b[37m${oneLine(textBuf, 2000)}\x1b[0m\n`);
83
+ }
84
+ textBuf = "";
85
+ };
86
+ return (event) => {
87
+ if (stream === null)
88
+ return;
89
+ const e = event;
90
+ const k = typeof e.kind === "string" ? e.kind : "";
91
+ if (k === "text_delta" || k === "reasoning_delta") {
92
+ textBuf += typeof e.text === "string" ? e.text : "";
93
+ return;
94
+ }
95
+ if (k === "text_start")
96
+ return;
97
+ if (k === "text_end") {
98
+ flushText();
99
+ return;
100
+ }
101
+ flushText(); // a non-text event ends any in-flight text line
102
+ const line = formatRunEventLine(event);
103
+ if (line !== null)
104
+ stream.write(line + "\n");
105
+ };
106
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boardwalk-labs/runner",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
4
4
  "description": "Boardwalk self-hosted runner: execute cloud-scheduled runs on your own machines. Currently: the canonical runner contract types.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {