@boardwalk-labs/runner 0.1.20 → 0.1.21

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.
@@ -7,7 +7,6 @@
7
7
  // per-run workspace, then tears the workspace down (contract: cleanup "always").
8
8
  import { mkdir, rm } from "node:fs/promises";
9
9
  import * as path from "node:path";
10
- import * as os from "node:os";
11
10
  import { createLogger } from "../runtime/support/index.js";
12
11
  /** Assignment-lease heartbeat cadence — comfortably beats the 300s lease. */
13
12
  const HEARTBEAT_MS = 60_000;
@@ -23,7 +22,10 @@ export function runProcessEnv(claim, opts) {
23
22
  BOARDWALK_CONTROL_PLANE_URL: claim.control_plane.base_url,
24
23
  BOARDWALK_RUN_TOKEN: claim.control_plane.run_token,
25
24
  BOARDWALK_API_KEY: claim.control_plane.api_token,
26
- BOARDWALK_TASK_CPU_UNITS: String(os.cpus().length * 1024),
25
+ // BOARDWALK_TASK_CPU_UNITS is deliberately ABSENT: self-hosted compute is Boardwalk-unbilled
26
+ // (SELF_HOSTED_RUNNERS.md D5), so the runtime's wall-clock fallback (1 vCPU) is the right
27
+ // display metric. Stamping the host's core count here billed a run at cores × the compute rate
28
+ // for hardware the org owns.
27
29
  // The org's BYO inference providers, as data (the runner-direct BYO design) — consumed by the runtime's direct
28
30
  // model path. Names + endpoints + secret NAMES only; never a credential value.
29
31
  BOARDWALK_BYO_PROVIDERS: JSON.stringify(claim.byo_providers),
@@ -65,6 +65,11 @@ export interface FreezeCoordinatorHooks {
65
65
  /** Runs when a wake lands, before the seam resolves: swap the run/api tokens onto the
66
66
  * broker client and rebase the runtime meter past the frozen window. */
67
67
  onAfterWake?: (wake: WakePayload) => void | Promise<void>;
68
+ /** Runs when a REQUESTED freeze aborts (`suspend_abort` — snapshot/store failure) before the
69
+ * parked seam resolves: undo onBeforeFreeze's reversible effects (resume the paused runtime
70
+ * flusher) since the run now HOLDS in-process instead of freezing. Not called for a failure
71
+ * inside onBeforeFreeze itself — that hook owns its own unwind. Must not throw. */
72
+ onFreezeAborted?: () => void;
68
73
  }
69
74
  export interface FreezeCoordinatorDeps {
70
75
  channel: RelayChannel;
@@ -237,6 +237,9 @@ export class FreezeCoordinator {
237
237
  }
238
238
  const resolve = this.parked;
239
239
  this.parked = null;
240
+ // The freeze is off — the seam will hold in-process, so onBeforeFreeze's reversible prep
241
+ // (the paused runtime flusher) must unwind before the hold starts metering-blind.
242
+ this.hooks.onFreezeAborted?.();
240
243
  resolve({ kind: "aborted", reason });
241
244
  }
242
245
  awaitQuiescence(abort) {
@@ -406,6 +406,16 @@ export function assembleWorkerDeps(runtime) {
406
406
  // The recorder never spans a snapshot: finalize + upload the in-flight segment before the
407
407
  // freeze (SCREEN_CAPTURE §4.3). Bounded internally, so a slow upload delays, not blocks, it.
408
408
  await capture?.stopAndFlush();
409
+ // Pause LAST (nothing after it can throw, so a failed persist never strands a paused
410
+ // flusher): with the timer off, no tick can land in the post-wake sliver between the
411
+ // guest clock resync and excludeIdle below — a tick there would compute its delta over
412
+ // the whole frozen window and bill suspended time. Resumed on wake and on suspend_abort.
413
+ activeFlusher?.pause();
414
+ },
415
+ // The freeze died (snapshot/store failure) — the seam holds in-process, which must keep
416
+ // metering: undo the pre-freeze pause.
417
+ onFreezeAborted: () => {
418
+ activeFlusher?.resume();
409
419
  },
410
420
  // On wake: reseed the userspace CSPRNG (clause 3 — a suspend snapshot restored more than
411
421
  // once, e.g. a re-dispatch retry, would otherwise repeat its post-wake `crypto.*` draws),
@@ -426,6 +436,8 @@ export function assembleWorkerDeps(runtime) {
426
436
  redactor.record(wake.api_token);
427
437
  }
428
438
  activeFlusher?.excludeIdle(wake.wall_clock_ms - freezeWallMs);
439
+ // Only now that the frozen window is excluded may the flush timer run again.
440
+ activeFlusher?.resume();
429
441
  // Resume capture in a fresh segment epoch (a suspend/resume boundary is a segment boundary).
430
442
  void capture?.startFresh();
431
443
  },
@@ -38,6 +38,17 @@ export declare class RuntimeFlusher {
38
38
  excludeIdle(ms: number): void;
39
39
  /** Begin periodic delta flushing. */
40
40
  start(): void;
41
+ /**
42
+ * Suspend the periodic timer across a VM freeze — the pre-freeze hook pauses AFTER the tail flush
43
+ * (as its last, non-throwing step), and the wake path resumes AFTER {@link excludeIdle}. Without
44
+ * this, a tick landing in the sliver between the guest clock resync and the idle rebase would
45
+ * compute its delta over the whole frozen window and bill suspended time. Reversible, unlike
46
+ * {@link stop}; a freeze that aborts (snapshot failure → the seam holds in-process) must resume so
47
+ * a long hold keeps metering.
48
+ */
49
+ pause(): void;
50
+ /** Restart periodic flushing after {@link pause} (wake, or a freeze abort). No-op once stopped. */
51
+ resume(): void;
41
52
  /** Stop the periodic timer and drain any in-flight flush. Does NOT book the final tail — call
42
53
  * {@link flushFinal} for that on a clean terminal. Idempotent. */
43
54
  stop(): Promise<void>;
@@ -58,6 +58,26 @@ export class RuntimeFlusher {
58
58
  // Don't keep the worker process alive solely for the flush timer.
59
59
  this.timer.unref();
60
60
  }
61
+ /**
62
+ * Suspend the periodic timer across a VM freeze — the pre-freeze hook pauses AFTER the tail flush
63
+ * (as its last, non-throwing step), and the wake path resumes AFTER {@link excludeIdle}. Without
64
+ * this, a tick landing in the sliver between the guest clock resync and the idle rebase would
65
+ * compute its delta over the whole frozen window and bill suspended time. Reversible, unlike
66
+ * {@link stop}; a freeze that aborts (snapshot failure → the seam holds in-process) must resume so
67
+ * a long hold keeps metering.
68
+ */
69
+ pause() {
70
+ if (this.timer === null)
71
+ return;
72
+ clearInterval(this.timer);
73
+ this.timer = null;
74
+ }
75
+ /** Restart periodic flushing after {@link pause} (wake, or a freeze abort). No-op once stopped. */
76
+ resume() {
77
+ if (this.stopped)
78
+ return;
79
+ this.start();
80
+ }
61
81
  /** Stop the periodic timer and drain any in-flight flush. Does NOT book the final tail — call
62
82
  * {@link flushFinal} for that on a clean terminal. Idempotent. */
63
83
  async stop() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boardwalk-labs/runner",
3
- "version": "0.1.20",
3
+ "version": "0.1.21",
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": {