@boardwalk-labs/runner 0.3.0 → 0.3.2

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.
@@ -33,7 +33,7 @@ import { randomBytes } from "node:crypto";
33
33
  import { tmpdir } from "node:os";
34
34
  import { join } from "node:path";
35
35
  import { rm } from "node:fs/promises";
36
- import { Ajv } from "ajv";
36
+ import { Ajv2020 } from "ajv/dist/2020.js";
37
37
  import { clientToHostRequests, clientToHostNotifications, rpcFrameSchema, } from "@boardwalk-labs/workflow/runtime";
38
38
  import { AppError, ErrorCode, createLogger, errorCodeOf } from "./support/index.js";
39
39
  import { RunAbortedError } from "./run_abort.js";
@@ -544,13 +544,15 @@ export function protocolErrorOf(err) {
544
544
  return { code, message, ...(Object.keys(dataParts).length > 0 ? { data: dataParts } : {}) };
545
545
  }
546
546
  /** Compile the output-schema validator: structural (formats off — same honesty as the revive
547
- * pass), lax about unknown keywords (`contentEncoding` etc.). A schema that will not compile is
548
- * a platform bug in the deriver warned and skipped (fail-soft), never a run failure. */
547
+ * pass), lax about unknown keywords (`contentEncoding` etc.), and in the DERIVER'S DIALECT
548
+ * JSON Schema draft 2020-12 (Ajv2020): a draft-07 Ajv ignores `prefixItems` and applies a
549
+ * tuple's `items: false` to every element, failing ALL tuples. A schema that will not compile
550
+ * is a platform bug in the deriver — warned and skipped (fail-soft), never a run failure. */
549
551
  function compileOutputValidator(schema) {
550
552
  if (schema === null)
551
553
  return null;
552
554
  try {
553
- const ajv = new Ajv({ strict: false, validateFormats: false });
555
+ const ajv = new Ajv2020({ strict: false, validateFormats: false });
554
556
  return ajv.compile(schema);
555
557
  }
556
558
  catch (err) {
@@ -44,8 +44,18 @@ export async function runProgramWorker(runId, deps) {
44
44
  }
45
45
  log.info("worker_claimed", { runId, workerId: deps.workerId });
46
46
  const claimed = claim.run;
47
+ /** Drain the buffered event stream BEFORE the terminal write. Finalize flips the run terminal on
48
+ * the control plane, and from that instant the fleet reconciler may classify this still-running
49
+ * VM as a zombie and destroy it — so anything observable still sitting in the telemetry buffer
50
+ * when finalize lands can be lost forever (found live 2026-07-24: a destroy 89ms after finalize
51
+ * ate a run's whole post-wake event tail). Nothing observable may remain unflushed once the run
52
+ * turns terminal. Best-effort like every telemetry path — never fails the run. */
53
+ const drainEventsBeforeFinalize = async () => {
54
+ await deps.flushTelemetry?.().catch(() => undefined);
55
+ };
47
56
  /** Pre-flight/setup failure: no program ran (and none will) — finalize failed and exit. */
48
57
  const failEarly = async (reason, error, logEvent, logFields) => {
58
+ await drainEventsBeforeFinalize();
49
59
  await deps.finalizer.finalize(runId, "failed", { error });
50
60
  log.error(logEvent, { runId, ...logFields });
51
61
  return { kind: "failed", reason };
@@ -241,6 +251,7 @@ export async function runProgramWorker(runId, deps) {
241
251
  if (controller.signal.aborted) {
242
252
  const reason = abortReason(controller.signal) ?? "cancelled";
243
253
  phases?.close("failed");
254
+ await drainEventsBeforeFinalize();
244
255
  await deps.finalizer.finalize(runId, "failed", {
245
256
  error: { code: "RUN_ABORTED", reason, message: `Run stopped: ${reason}` },
246
257
  });
@@ -249,10 +260,12 @@ export async function runProgramWorker(runId, deps) {
249
260
  }
250
261
  if (result.kind === "completed") {
251
262
  phases?.close("completed");
263
+ await drainEventsBeforeFinalize();
252
264
  await deps.finalizer.finalize(runId, "completed", result.output);
253
265
  return { kind: "completed" };
254
266
  }
255
267
  phases?.close("failed");
268
+ await drainEventsBeforeFinalize();
256
269
  await deps.finalizer.finalize(runId, "failed", { error: result.error });
257
270
  return { kind: "failed", reason: result.error.code };
258
271
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boardwalk-labs/runner",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
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": {