@boardwalk-labs/runner 0.1.21 → 0.2.1

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.
Files changed (35) hide show
  1. package/README.md +11 -0
  2. package/dist/contract.d.ts +2 -0
  3. package/dist/contract.js +14 -0
  4. package/dist/daemon/container.d.ts +3 -2
  5. package/dist/daemon/container.js +15 -2
  6. package/dist/daemon/daemon.d.ts +1 -0
  7. package/dist/daemon/daemon.js +16 -1
  8. package/dist/runtime/agent/budget.d.ts +14 -1
  9. package/dist/runtime/agent/budget.js +23 -0
  10. package/dist/runtime/broker_child_dispatcher.d.ts +0 -2
  11. package/dist/runtime/broker_child_dispatcher.js +0 -7
  12. package/dist/runtime/budget_gate.d.ts +51 -0
  13. package/dist/runtime/budget_gate.js +114 -0
  14. package/dist/runtime/freeze_coordinator.js +8 -0
  15. package/dist/runtime/index.d.ts +15 -2
  16. package/dist/runtime/index.js +80 -57
  17. package/dist/runtime/leaf_executor.d.ts +20 -0
  18. package/dist/runtime/leaf_executor.js +13 -4
  19. package/dist/runtime/local_workspace_store.d.ts +22 -0
  20. package/dist/runtime/local_workspace_store.js +103 -0
  21. package/dist/runtime/program_runner.d.ts +32 -28
  22. package/dist/runtime/program_runner.js +75 -44
  23. package/dist/runtime/program_worker.d.ts +5 -18
  24. package/dist/runtime/program_worker.js +3 -20
  25. package/dist/runtime/runner_control_client.d.ts +2 -22
  26. package/dist/runtime/runner_control_client.js +2 -49
  27. package/dist/runtime/suspension.d.ts +20 -109
  28. package/dist/runtime/suspension.js +18 -111
  29. package/dist/runtime/wire/human_input.d.ts +1 -1
  30. package/dist/runtime/wire/human_input.js +2 -2
  31. package/dist/runtime/workflow_host.d.ts +65 -62
  32. package/dist/runtime/workflow_host.js +124 -199
  33. package/dist/runtime/workspace_store.d.ts +31 -3
  34. package/dist/runtime/workspace_store.js +43 -4
  35. package/package.json +3 -3
@@ -1,120 +1,27 @@
1
- // Durable-suspension primitives for the JS-body worker (the durable-suspension design).
1
+ // Suspension primitives for the JS-body worker (the snapshot-substrate model).
2
2
  //
3
- // A run SUSPENDS at a durable seam — a long `sleep`, a `humanInput()` gate, or the in-leaf
4
- // `human_input` tool — by releasing its Fargate task and re-acquiring one on wake. The mechanism is
5
- // a JOURNAL: every memoizable seam (`agent` / `step` / `humanInput` / `sleep`) is keyed by a
6
- // synchronous monotonic `seq` and records its whole result, so a resumed run RESTARTS from the top
7
- // and replays through the journal (a hit returns the memoized value instantly; we never replay an
8
- // LLM turn — we memoize whole results, the `findChildCall` pattern generalized). This is NOT
9
- // Temporal-style turn replay, so it does not violate the no-replay-engine rule (CLAUDE.md).
3
+ // A run SUSPENDS at a waiting seam — a long `sleep`, a `humanInput()` gate, or the in-leaf
4
+ // `human_input` tool — by freezing the WHOLE microVM: the freeze coordinator quiesces platform
5
+ // streams, the host snapshots memory, and the wake restores the literal heap, so the seam's
6
+ // `await` resolves in place. There is no journal and no replay the heap is the durable unit.
10
7
  //
11
- // This file holds the SHARED pieces the host + worker + broker client agree on: the suspend signal a
12
- // seam raises, the journal seam the host reads/writes, the seq sequencer driving silent replay, and
13
- // the determinism fingerprint. The behavior is conformance-pinned to the OSS engine's
14
- // `src/run/child_host.ts` — the two runtimes must suspend + resume identically.
15
- import { createHash } from "node:crypto";
16
- import { z } from "zod";
17
- import { AppError, ErrorCode } from "./support/index.js";
8
+ // A runtime with NO freeze substrate (a self-hosted runner daemon, unit tests, the Fargate
9
+ // break-glass) HOLDS instead: the seam blocks the live process until its condition is met. One
10
+ // mechanism serves every non-snapshot surface; the run pays for the idle wait.
11
+ //
12
+ // This file holds the SHARED pieces the host + worker + broker client agree on: the suspend
13
+ // signal a seam raises (what the broker persists as the wake condition) and the human-input gate
14
+ // it carries.
18
15
  /**
19
- * Sleeps at/above this hold-vs-release boundary SUSPEND (release the task; a timer re-dispatches the
20
- * run when due); shorter ones HOLD the process in-memory, where a release + replay round-trip costs
21
- * more than it saves. Mirrors the engine's `SUSPEND_THRESHOLD_MS` so the two runtimes draw the line
22
- * in the same place.
16
+ * Sleeps at/above this boundary SUSPEND on the snapshot substrate (freeze the VM; the wake fires
17
+ * when the sleep is due); shorter ones HOLD the process in-memory, where a snapshot round-trip
18
+ * costs more than it saves. Without a freeze substrate every sleep holds, whatever its length.
23
19
  */
24
20
  export const SUSPEND_THRESHOLD_MS = 30_000;
25
- /**
26
- * The control signal a host seam raises to suspend the run. Unlike the engine (a child process the
27
- * supervisor kills out-of-band), the backend worker runs the program IN-PROCESS, so a suspend is
28
- * surfaced by the host calling `onSuspend(signal)` and returning a NEVER-resolving promise; the
29
- * worker races that against the program body and tears down. This class exists so a seam that has no
30
- * `onSuspend` wired (the local/test path) can still raise a typed, catchable signal.
31
- */
32
- export class SuspendError extends Error {
33
- signal;
34
- constructor(signal) {
35
- super(`run suspended (${signal.reason}) at seam ${String(signal.seq)}`);
36
- this.name = "SuspendError";
37
- this.signal = signal;
38
- }
39
- }
40
- /** Journal-entry states (mirrors `run_journal.state` + the engine's IPC). */
41
- export const JOURNAL_STATES = ["pending", "suspended", "resolved"];
42
- /** Validate the broker's journal_get response (the worker's run token doesn't exempt the channel from
43
- * validation). The result is genuinely heterogeneous JSON (an agent return / a LeafResume / a
44
- * HumanInputResult), parsed per-kind downstream — so it stays `unknown` here, the validation seam. */
45
- export const journalLookupSchema = z.object({
46
- seq: z.number().int().positive(),
47
- kind: z.enum(["agent", "step", "human_input", "sleep", "workflow_call"]),
48
- fingerprint: z.string(),
49
- state: z.enum(JOURNAL_STATES),
50
- result: z.unknown(),
51
- });
52
- /**
53
- * The synchronous, monotonic durable-seam counter. Incremented at each journaled seam's ENTRY:
54
- * because a program's synchronous call order is deterministic (even under `Promise.all`, whose
55
- * `.map(...)` runs left-to-right synchronously), the same logical call gets the same `seq` on every
56
- * execution — the journal key that lets a resumed run return a memoized result.
57
- *
58
- * It also drives SILENT REPLAY: a resume starts suppressed (observability — console output, phase
59
- * markers — was already emitted last segment) and goes `live` the moment it reaches the suspending
60
- * seam (the frontier = the highest journaled seq). A fresh run (frontier 0) is live immediately.
61
- */
62
- export class SeamSequencer {
63
- replayFrontier;
21
+ /** A monotonic per-run counter for suspension/gate keys (the `seq` on {@link SuspendSignal}). */
22
+ export class SuspensionCounter {
64
23
  count = 0;
65
- liveFlag;
66
- constructor(replayFrontier = 0) {
67
- this.replayFrontier = replayFrontier;
68
- this.liveFlag = replayFrontier === 0;
69
- }
70
24
  next() {
71
- const seq = ++this.count;
72
- if (seq >= this.replayFrontier)
73
- this.liveFlag = true;
74
- return seq;
75
- }
76
- /** True once execution has crossed the replay frontier — output after this point is NEW. */
77
- get isLive() {
78
- return this.liveFlag;
25
+ return ++this.count;
79
26
  }
80
- /** True while re-running already-journaled seams on a resume (observability suppressed). */
81
- isReplaying() {
82
- return !this.liveFlag;
83
- }
84
- }
85
- /** The child run id a `workflow_call` seam journals while it waits (parsed back on resume). */
86
- export const childRunIdSchema = z.string().min(1);
87
- /** A stable content hash of a seam's salient args — the determinism check on replay. Identical
88
- * construction to the engine's `seamFingerprint` so a journal written by one runtime validates in
89
- * the other (the conformance promise). */
90
- export function seamFingerprint(parts) {
91
- return createHash("sha256").update(JSON.stringify(parts)).digest("hex");
92
27
  }
93
- /** A seam reached on replay didn't match what the journal recorded at that seq — the workflow's code
94
- * on the path to a suspend changed (a different prompt/model/step name, or a different seam kind).
95
- * Fails the run loudly rather than returning a stale memoized result for the wrong call. */
96
- export function determinismError(seq, got, recorded) {
97
- const detail = got === recorded
98
- ? `the same "${got}" seam but with different arguments (a changed prompt, model, or step name)`
99
- : `a "${recorded}" call, but this execution reached a "${got}" call`;
100
- return new AppError(ErrorCode.VALIDATION_FAILED, `Nondeterministic replay at seam ${String(seq)}: the journal recorded ${detail}. A workflow's ` +
101
- `code on the path to a suspend/resume must be deterministic — route nondeterministic I/O ` +
102
- `through agent(), step.run(), or workflows.call so it is journaled.`, { kind: "nondeterministic_replay", seq });
103
- }
104
- /**
105
- * The shape of a SUSPENDED agent leaf's journal result on resume: the transcript checkpoint plus the
106
- * answers the broker joined from the resolved request rows, keyed by tool-call id. `messages` is the
107
- * engine's own serialized transcript round-tripping through JSON — handed straight back to the leaf,
108
- * not re-validated field-by-field. Structurally a {@link LeafResume}.
109
- */
110
- export const leafResumeSchema = z.object({
111
- checkpoint: z.object({
112
- messages: z.array(z.custom()),
113
- iteration: z.number().int(),
114
- totals: z.object({
115
- inputTokens: z.number().int(),
116
- outputTokens: z.number().int(),
117
- }),
118
- }),
119
- answers: z.record(z.string(), z.unknown()),
120
- });
@@ -28,7 +28,7 @@ export type ParsedHumanInputSpec = z.infer<typeof humanInputSpecSchema>;
28
28
  export declare function parseHumanInputSpec(raw: unknown): ParsedHumanInputSpec;
29
29
  /** Re-hydrate a stored (jsonb) human-input result into the exact-optional SDK {@link HumanInputResult}
30
30
  * (the host returns it from a resumed `humanInput()` seam). The value was validated on submit, so a
31
- * parse failure here is a corrupt journal — surfaced as a clear program error. */
31
+ * parse failure here is a corrupt stored answer — surfaced as a clear program error. */
32
32
  export declare function normalizeHumanInputResult(raw: unknown): HumanInputResult;
33
33
  /** Convert a validated {@link HumanInputResult} to a {@link JsonValue} for storage (the result is an
34
34
  * interface with optional props, which TS won't structurally accept as JsonValue; an explicit object
@@ -3,7 +3,7 @@
3
3
  // The `HumanInputSpec` is the single source of truth for BOTH the UI form and server-side
4
4
  // validation, so a `humanInput()` gate needs no JSON Schema. A responder's raw submission (from
5
5
  // REST / MCP / CLI / web) is validated here against the gate's stored spec into a typed
6
- // `HumanInputResult`, which becomes the seam's memoized journal value. Pure + exhaustively testable.
6
+ // `HumanInputResult`, the value the seam returns to the program. Pure + exhaustively testable.
7
7
  //
8
8
  // These mirror the SDK's `@boardwalk-labs/workflow` types exactly (the wire contract); we re-derive
9
9
  // them as Zod schemas because the stored `input_spec` is `unknown` jsonb that must be parsed at the
@@ -51,7 +51,7 @@ export function parseHumanInputSpec(raw) {
51
51
  }
52
52
  /** Re-hydrate a stored (jsonb) human-input result into the exact-optional SDK {@link HumanInputResult}
53
53
  * (the host returns it from a resumed `humanInput()` seam). The value was validated on submit, so a
54
- * parse failure here is a corrupt journal — surfaced as a clear program error. */
54
+ * parse failure here is a corrupt stored answer — surfaced as a clear program error. */
55
55
  export function normalizeHumanInputResult(raw) {
56
56
  const parsed = storedResultSchema.safeParse(raw);
57
57
  if (!parsed.success) {
@@ -1,7 +1,7 @@
1
1
  import type { WorkflowHost, AgentOptions, ArtifactBody, ArtifactRef, BrowserSession, BrowserSessionOptions, CallOptions, HumanInputOptions, HumanInputResult, PhaseOptions, SleepArg } from "@boardwalk-labs/workflow/runtime";
2
2
  import type { BrowserSessionManager } from "./browser_session.js";
3
3
  import { type LeafResume } from "@boardwalk-labs/engine/core";
4
- import { type JournalSeam, type SuspendSignal } from "./suspension.js";
4
+ import { type SuspendSignal } from "./suspension.js";
5
5
  import type { FreezeCoordinator } from "./freeze_coordinator.js";
6
6
  /** Parse a `humanInput({ timeout })` string (`"48h"`, `"30m"`, `"90s"`, `"7d"`) to milliseconds, or
7
7
  * null when absent/unparseable (the gate then waits indefinitely). */
@@ -36,16 +36,15 @@ export interface ChildResult {
36
36
  status: string;
37
37
  output: unknown;
38
38
  }
39
- /** Dispatches child runs: `call` holds + returns output (the no-journal path); `start`/`poll` back the
40
- * DURABLE callWorkflow seam (start once, suspend `waiting_for_child` on a non-terminal child, poll on
41
- * resume); `run` is fire-and-forget → run id; `schedule` provisions a durable future/recurring run →
42
- * schedule id. The `signal` lets a hold/start abort promptly when the parent run is cancelled. */
39
+ /** Dispatches child runs: `call` holds in-process + returns the completed child's output (the
40
+ * hold path); `start`/`poll` back the snapshot-substrate callWorkflow seam (start once, freeze
41
+ * `waiting_for_child` on a non-terminal child); `run` is fire-and-forget → run id; `schedule`
42
+ * provisions a durable future/recurring run → schedule id. The `signal` lets a hold/start abort
43
+ * promptly when the parent run is cancelled. */
43
44
  export interface ChildDispatcher {
44
45
  call(slug: string, input: unknown, opts: CallOptions | undefined, signal?: AbortSignal): Promise<unknown>;
45
46
  /** Start (or idempotently re-attach to) a child run; resolves its current state. */
46
47
  start(slug: string, input: unknown, opts: CallOptions | undefined, signal?: AbortSignal): Promise<ChildResult>;
47
- /** Poll a child run's current state by id, or null when it isn't this run's child. */
48
- poll(childRunId: string): Promise<ChildResult | null>;
49
48
  run(slug: string, input: unknown, opts: CallOptions | undefined): Promise<string>;
50
49
  schedule(slug: string, input: unknown, opts: ScheduleOptions): Promise<string>;
51
50
  }
@@ -53,9 +52,10 @@ export interface ChildDispatcher {
53
52
  export interface SecretAccessor {
54
53
  get(name: string): Promise<string>;
55
54
  }
56
- /** Register-without-release: register a HELD HITL gate so it is
57
- * answerable while the run keeps running, and poll for the answer. Backed by the broker's
58
- * `inputs` endpoints. Absent a held gate is only answerable once it freezes. */
55
+ /** Register a HELD HITL gate so it is answerable while the run keeps its process, and poll for
56
+ * the answer. Backed by the broker's `inputs` endpoints. On the snapshot substrate this is the
57
+ * register-without-release half of a freeze; on a no-freeze runtime (a self-hosted daemon) it is
58
+ * the WHOLE mechanism — the seam registers, then holds and polls until answered. */
59
59
  export interface HeldInputPort {
60
60
  register(seq: number, gate: SuspendSignal["humanInput"]): Promise<unknown>;
61
61
  poll(seq: number): Promise<Record<string, unknown>>;
@@ -130,39 +130,21 @@ export interface WorkerWorkflowHostDeps {
130
130
  now?: () => number;
131
131
  /** Override the 7-day hold ceiling (tests). */
132
132
  maxSleepMs?: number;
133
- /**
134
- * Durable-suspension journal (the durable-suspension design): the host memoizes each `agent`/`step`/`sleep`/
135
- * `humanInput` seam here, so a resumed run replays journaled seams instantly instead of re-running
136
- * them. Backed by the broker over the run token on hosted runs. Absent ⇒ no memoization (the
137
- * local/test path): seams run live every time and a suspend can't be resumed.
138
- */
139
- journal?: JournalSeam;
140
- /**
141
- * Raise a durable suspension: the host calls this from a suspending seam, then returns a
142
- * never-resolving promise; the worker races that against the program body and tears the task down
143
- * (the program's own `try/catch` can't swallow a suspend this way). Absent ⇒ a suspending seam
144
- * rejects with {@link SuspendError} instead (the local/test path).
145
- */
146
- onSuspend?: (signal: SuspendSignal) => void;
147
133
  /**
148
134
  * Snapshot-substrate suspension (the microVM freeze model): when present, a suspending seam
149
- * BLOCKS on the coordinator instead of raising `onSuspend` — the platform freezes the whole VM
150
- * and a wake resolves the seam in place, heap intact (no exit, no journal replay). Every hook
151
- * also runs under the coordinator's quiescence gate: a freeze never captures a live platform
152
- * stream, and work arriving while a freeze is pending queues until the wake.
135
+ * BLOCKS on the coordinator — the platform freezes the whole VM and a wake resolves the seam in
136
+ * place, heap intact. Every hook also runs under the coordinator's quiescence gate: a freeze
137
+ * never captures a live platform stream, and work arriving while a freeze is pending queues
138
+ * until the wake. Absent the no-substrate HOLD path: a waiting seam blocks the live process
139
+ * for the whole wait (self-hosted daemons, the Fargate break-glass, unit tests).
153
140
  */
154
141
  freeze?: FreezeCoordinator;
155
- /** Register-without-release for HELD human-input gates. Only
156
- * meaningful alongside `freeze`; absent ⇒ a held gate is answerable only once it freezes. */
142
+ /** Register + poll for HELD human-input gates (see {@link HeldInputPort}). Absent alongside
143
+ * `freeze` ⇒ a held gate is answerable only once it freezes; absent WITHOUT `freeze` ⇒
144
+ * humanInput is unsupported in this runtime and rejects clearly. */
157
145
  heldInput?: HeldInputPort;
158
146
  /** Poll interval for a held gate's answer (default 3s). */
159
147
  heldPollIntervalMs?: number;
160
- /**
161
- * The highest journaled seq at claim (0 on a fresh run). While re-running seams up to this
162
- * frontier on a resume, the host is REPLAYING and observability (phase markers, program logs) is
163
- * suppressed — those lines were emitted in the prior segment.
164
- */
165
- replayFrontier?: number;
166
148
  }
167
149
  export declare class WorkerWorkflowHost implements WorkflowHost {
168
150
  private readonly deps;
@@ -170,18 +152,11 @@ export declare class WorkerWorkflowHost implements WorkflowHost {
170
152
  private readonly now;
171
153
  private readonly maxSleepMs;
172
154
  private readonly heldPollIntervalMs;
173
- /** Synchronous durable-seam counter + silent-replay live flag (the durable-suspension design). */
155
+ /** Monotonic per-run counter keying suspensions + their HITL gate rows. */
174
156
  private readonly seq;
175
157
  /** Run context + on-demand public-API bearer the SDK `runtime` accessor reads off the host. */
176
158
  readonly runtime: RuntimeContext;
177
159
  constructor(deps: WorkerWorkflowHostDeps);
178
- /** True while re-running already-journaled seams on a resume (the worker suppresses program-log +
179
- * phase observability during this window — those lines were emitted in the prior segment). */
180
- isReplaying(): boolean;
181
- /** Raise a durable suspension. With `onSuspend` wired (the real worker) the host signals out-of-band
182
- * and returns a never-resolving promise, so the program's own try/catch can't swallow it; without
183
- * it (the local/test path) it rejects with {@link SuspendError}. */
184
- private suspend;
185
160
  /** Run `fn` only if the run isn't aborted; otherwise REJECT (never throw synchronously) with the
186
161
  * signal's RunAbortedError — every Promise-returning hook funnels through this so callers always
187
162
  * get a rejected promise on abort, not a sync throw. On the snapshot substrate the body also runs
@@ -207,6 +182,13 @@ export declare class WorkerWorkflowHost implements WorkflowHost {
207
182
  * the answer value; never rejects the run (a transient poll error just retries next tick). Runs
208
183
  * OUTSIDE the quiescence gate (it is not run work — it must not block a freeze). */
209
184
  private pollHeldAnswer;
185
+ /**
186
+ * The no-freeze HOLD for a human-input gate: register it with the broker (it surfaces in the
187
+ * inbox/API at once), then poll until the answer arrives — the process stays alive and pays
188
+ * for the wait. Rejects promptly when the run's abort signal fires (cancel / credit stop),
189
+ * which is also how a server-side gate expiry that fails the run unwinds this loop.
190
+ */
191
+ private holdForAnswer;
210
192
  /** Map a froze/aborted freeze outcome to the gate's answer (or throw on an unexpected abort). */
211
193
  private resolveFreezeAnswer;
212
194
  /** The wake's answer for one gate key. A wake whose value is missing the parked gate means the
@@ -215,26 +197,46 @@ export declare class WorkerWorkflowHost implements WorkflowHost {
215
197
  private gateAnswer;
216
198
  setPhase(name: string, opts: PhaseOptions | undefined): void;
217
199
  agent(prompt: string, opts: AgentOptions | undefined): Promise<unknown>;
218
- /** The `agent()` durable seam: a journal hit returns the memoized result (never re-runs the LLM); a
219
- * `suspended` hit resumes a parked leaf from its checkpoint + answers; a miss runs the leaf and
220
- * journals the result. A leaf that PARKS (the model called `human_input`) suspends the run. */
200
+ /** The `agent()` seam: run the leaf to completion. A leaf that PARKS (the model called the
201
+ * `human_input` tool) waits for the answer a freeze on the snapshot substrate, a held poll
202
+ * otherwise and re-enters from its in-memory checkpoint. Nothing is memoized: a crash-restart
203
+ * re-runs the leaf (restart-from-top semantics). */
221
204
  private agentSeam;
222
- /** Program-level `humanInput()`: suspend the run on a gate, resume with the validated answer. The
223
- * SDK marks this optional; the hosted host always implements it. */
205
+ /** Program-level `humanInput()`: wait on a gate a freeze on the snapshot substrate, a held
206
+ * poll otherwise — and return the validated answer. The SDK marks this optional; the hosted
207
+ * host always implements it. */
224
208
  humanInput(opts: HumanInputOptions): Promise<HumanInputResult>;
225
209
  private humanInputSeam;
210
+ /**
211
+ * Park the run at a BUDGET gate and resolve with the responder's answer (docs/SUSPEND_POLICY.md
212
+ * Decision 3). Called by the leaf executor's `streamModel` seam when the run's `max_usd` cap is
213
+ * breached, i.e. from INSIDE an in-flight `agent()` — which drives two deliberate differences from
214
+ * {@link humanInputSeam}:
215
+ *
216
+ * - **No `guarded()` wrapper.** The enclosing `agent()` seam already counted this leaf as work via
217
+ * `trackWork`. Wrapping again would double-count it, and the extra count would never be released
218
+ * — quiescence would never be reached and the freeze would hang forever. `freezeHumanInput` →
219
+ * `freezeWait` does the right thing here: it `endWork`s for the duration of the park (this leaf
220
+ * is waiting, not working, which is exactly what lets the run reach quiescence and freeze) and
221
+ * rejoins on resume.
222
+ * - **Abort is not re-checked up front.** `streamModel` has just done it; a park is not a new
223
+ * entry point.
224
+ *
225
+ * The gate itself is an ordinary {@link HumanInputGate} keyed `budget`, so it persists, surfaces in
226
+ * the inbox, and is answered by the same machinery as any other gate. No timeout: an unanswered
227
+ * budget gate is aged out by the control plane's inactive-cancel reaper, not by a wake we schedule.
228
+ */
229
+ budgetClearance(gate: {
230
+ prompt: string;
231
+ inputSpec: unknown;
232
+ }): Promise<HumanInputResult>;
226
233
  /** Absolute wake time for a `humanInput({ timeout })`, or null when there is none / it's unparseable. */
227
234
  private timeoutExpiry;
228
- /** `step.run(name, fn)`: run `fn` exactly once across restarts, memoizing its result in the journal
229
- * (the escape hatch for nondeterministic work on a suspend/resume path). */
230
- step(name: string, fn: () => unknown): Promise<unknown>;
231
- private stepSeam;
232
235
  callWorkflow(slug: string, input: unknown, opts: CallOptions | undefined): Promise<unknown>;
233
- /** The `workflows.call` durable seam (the durable-suspension design): start the child once + memoize its
234
- * output; a non-terminal child SUSPENDS the parent (`waiting_for_child`, the child's id journaled)
235
- * the parent releases its task and is woken when the child finalizes. On resume the seam polls
236
- * the journaled child and returns its output (or throws on a failed child). Without a journal (the
237
- * local/test path) it falls back to the in-process hold-and-poll. */
236
+ /** The `workflows.call` seam: start the child once (idempotently the child's run row is the
237
+ * durable memo, so a crash-restarted parent re-attaches instead of re-spawning). A non-terminal
238
+ * child suspends the parent `waiting_for_child` on the snapshot substrate (the wake carries the
239
+ * finalized child, heap intact); without one the parent HOLDS in-process and polls. */
238
240
  private callWorkflowSeam;
239
241
  /** Fire-and-forget trigger of another workflow; resolves to the new run's id (no hold/poll). */
240
242
  runWorkflow(slug: string, input: unknown, opts: CallOptions | undefined): Promise<string>;
@@ -245,7 +247,7 @@ export declare class WorkerWorkflowHost implements WorkflowHost {
245
247
  writeArtifact(name: string, contentType: string, body: ArtifactBody, metadata: Record<string, unknown> | undefined): Promise<ArtifactRef>;
246
248
  /** `computer.openBrowser()`: open a program-owned, in-VM browser session (the browser tier of
247
249
  * computer use). Not a durable seam — a session is a live resource, reaped at run end, never
248
- * journaled/replayed. Absent backend ⇒ a clear "not available" error. */
250
+ * persisted. Absent backend ⇒ a clear "not available" error. */
249
251
  openBrowserSession(opts: BrowserSessionOptions | undefined): Promise<BrowserSession>;
250
252
  /** Translate `agent({ session })` into the leaf's `mcp`: append the browser session's in-VM
251
253
  * Playwright MCP (its http ref, which passes assertHostedMcpAllowed and reaches localhost without
@@ -253,9 +255,10 @@ export declare class WorkerWorkflowHost implements WorkflowHost {
253
255
  * no session is bound. */
254
256
  private bindBrowserSession;
255
257
  sleep(arg: SleepArg): Promise<void>;
256
- /** A short sleep HOLDS the task in-process (cheaper than a release + replay round-trip); a long one
257
- * (≥ {@link SUSPEND_THRESHOLD_MS}) SUSPENDS releases the task, and a timer re-dispatches the run
258
- * when due. Journaled so a resumed run replays past an already-elapsed sleep instantly. */
258
+ /** A short sleep HOLDS the task in-process (cheaper than a snapshot round-trip); a long one
259
+ * (≥ {@link SUSPEND_THRESHOLD_MS}) SUSPENDS on the snapshot substrate the VM freezes and the
260
+ * wake resolves this very await, heap intact. Without a freeze substrate EVERY sleep holds,
261
+ * whatever its length (the no-substrate rule: snapshot or hold, never replay). */
259
262
  private sleepSeam;
260
263
  /** Resolve any {@link SleepArg} shape to a millisecond duration from now. */
261
264
  private resolveSleepMs;