Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
45c7bcd
perf(core): retain workflow VM across inline steps
NathanColosimo Jul 17, 2026
bab996c
refactor(core): simplify retained-session control flow
NathanColosimo Jul 17, 2026
bc5a318
fix(core): decline retention for VMs that ran host-timed async work
NathanColosimo Jul 17, 2026
106936a
fix(core): track all host-timed async VM APIs for retention
NathanColosimo Jul 17, 2026
9001a31
feat(core): compute crypto.subtle.digest synchronously in the sandbox
NathanColosimo Jul 17, 2026
2abf8b8
fix(core): remove WeakRef and FinalizationRegistry from the sandbox
NathanColosimo Jul 17, 2026
4905577
fix(core): enforce the BufferSource contract in the sandbox digest
NathanColosimo Jul 17, 2026
73ee4ae
fix(core): demote retention when suspension serialization draws rando…
NathanColosimo Jul 17, 2026
d83b651
refactor(core): make VM quiescence unconditional, cut tracking machinery
NathanColosimo Jul 17, 2026
b2c33d2
refactor(core): gate retention on passively cloneable step inputs
NathanColosimo Jul 17, 2026
744f0b7
fix(core): harden the passive step-input walker
NathanColosimo Jul 17, 2026
8d96711
fix(core): guard proxied constructors in the passive-input walker
NathanColosimo Jul 17, 2026
fd73574
fix(core): preserve retention gate after rebase
NathanColosimo Jul 17, 2026
bd07e11
fix(core): all-or-nothing clone batches; reject SAB views in digest
NathanColosimo Jul 17, 2026
fe425e6
fix(core): narrow the fast path to prototype-independent types
NathanColosimo Jul 17, 2026
c6d6cb5
Merge branch 'main' into nathanc/workflow-replay-perf-43e0a8
VaguelySerious Jul 17, 2026
9a5f070
fix(core): freeze serialization-consulted sandbox intrinsics
NathanColosimo Jul 17, 2026
3bda890
fix(core): freeze every non-shared serialization constructor
NathanColosimo Jul 17, 2026
43788b8
fix(core): build retained clones in a pristine realm
NathanColosimo Jul 17, 2026
2b73676
fix(core): verify host dispatch pristineness before retained cloning
NathanColosimo Jul 17, 2026
caf6a1a
fix(core): reject symbol properties from retained step inputs
NathanColosimo Jul 17, 2026
e961466
fix(core): retained inputs accept only own enumerable data properties
NathanColosimo Jul 17, 2026
3498706
fix(core): freeze binding prototype chains for hasInstance lookup
NathanColosimo Jul 17, 2026
33f8e1a
Merge branch 'main' into nathanc/workflow-replay-perf-43e0a8
VaguelySerious Jul 20, 2026
89494d1
Merge branch 'main' into nathanc/workflow-replay-perf-43e0a8
VaguelySerious Jul 20, 2026
59aaebb
refactor(core): single-path retained serialization via pinned members…
NathanColosimo Jul 21, 2026
58514fd
refactor(core): freeze built-in prototypes instead of pinning members…
NathanColosimo Jul 21, 2026
93aa3a5
fix(core): harden the passivity checker's own execution surface
NathanColosimo Jul 21, 2026
fb78452
fix(core): checker uses module-load primordials; verify inherited ser…
NathanColosimo Jul 21, 2026
59e21d6
fix(core): stale-suspension generation token; cover BigInt toString
NathanColosimo Jul 21, 2026
2e78951
Merge branch 'main' into nathanc/workflow-replay-perf-43e0a8
NathanColosimo Jul 21, 2026
4765dcf
Merge retained-VM feature branch (vm sandbox files resolve to PR1's p…
NathanColosimo Jul 22, 2026
bb2098a
feat(core): retain the workflow VM across inline steps (primitive args)
NathanColosimo Jul 22, 2026
c61f988
chore: retrigger vercel deployments
NathanColosimo Jul 23, 2026
c574d51
Merge branch 'nathanc/vm-determinism-hardening' into nathanc/retained…
NathanColosimo Jul 23, 2026
b1183fc
Merge branch 'nathanc/vm-determinism-hardening' into nathanc/retained…
NathanColosimo Jul 23, 2026
9e46069
Merge branch 'nathanc/vm-determinism-hardening' into nathanc/retained…
NathanColosimo Jul 24, 2026
3cd4d07
simplify retention: single decision site in suspension catch, steps-o…
NathanColosimo Jul 24, 2026
7ecf79c
Merge branch 'nathanc/vm-determinism-hardening' into nathanc/retained…
NathanColosimo Jul 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/retain-workflow-vm.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@workflow/core': minor
'workflow': minor
---

Retain workflow execution across inline steps within one invocation; `WORKFLOW_RETAINED_VM=0` disables retention. Boundaries whose step arguments are not primitive values fall back to ordinary replay.
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ This method runs inside the workflow context and is subject to the same constrai
- No Node.js-specific APIs (like `fs`, `path`, `crypto`, etc.)
- No non-deterministic operations (like `Math.random()` or `Date.now()`)
- No external network calls
- No side effects on workflow state — the method may run outside deterministic replay, so mutations would not be reconstructed

Keep this method simple and focused on extracting data from the instance.
</Callout>
8 changes: 8 additions & 0 deletions docs/content/docs/v5/configuration/runtime-tuning.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,14 @@ For example, a workflow can run a 10-minute inline step even with `WORKFLOW_REPL
- Use only when step side effects are idempotent.
- Set `0` or `false` to force it off, including the first-delivery fast path used by `WORKFLOW_TURBO`.

### `WORKFLOW_RETAINED_VM`

- Default: enabled
- Keeps the suspended workflow VM alive across inline steps within one invocation, so each iteration of the inline loop appends only the newly written events instead of replaying the whole event log in a fresh VM.
- Suspensions involving hooks, waits, or attributes — and any replay divergence — always fall back to a full replay.
- Step inputs of primitive values remain retainable; anything whose serialization could execute workflow code falls back to replay for that boundary. (Support for plain objects, arrays, and standard built-ins lands in a follow-up.)
- Set `0` or `false` to replay from scratch in a fresh VM on every iteration.

### `WORKFLOW_INLINE_OWNERSHIP`

- Default: enabled
Expand Down
27 changes: 27 additions & 0 deletions packages/core/src/events-consumer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,15 @@ describe('EventsConsumer', () => {
expect(consumer.eventIndex).toBe(0);
expect(consumer.callbacks).toEqual([]);
});

it('should own its event array', () => {
const events = [createMockEvent()];
const consumer = new EventsConsumer(events, defaultOptions);

events.push(createMockEvent({ id: 'event-2' }));

expect(consumer.events).toHaveLength(1);
});
});

describe('subscribe', () => {
Expand Down Expand Up @@ -82,6 +91,24 @@ describe('EventsConsumer', () => {
expect(callback).toHaveBeenCalledWith(event);
expect(callback).toHaveBeenCalledTimes(1);
});

it('should consume appended events', async () => {
const event = createMockEvent();
const consumer = new EventsConsumer([], defaultOptions);
const callback = vi
.fn()
.mockImplementation((value: Event | null) =>
value ? EventConsumerResult.Finished : EventConsumerResult.NotConsumed
);
consumer.subscribe(callback);
await waitForNextTick();

consumer.append([event]);
await waitForNextTick();

expect(callback).toHaveBeenLastCalledWith(event);
expect(consumer.eventIndex).toBe(1);
});
});

describe('consume (implicit)', () => {
Expand Down
7 changes: 6 additions & 1 deletion packages/core/src/events-consumer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,18 @@ export class EventsConsumer {
private unconsumedCheckVersion = 0;

constructor(events: Event[], options: EventsConsumerOptions) {
this.events = events;
this.events = [...events];
this.eventIndex = 0;
this.onConsumedEvent = options.onConsumedEvent;
this.onUnconsumedEvent = options.onUnconsumedEvent;
this.getPromiseQueue = options.getPromiseQueue;
}

append(events: Event[]): void {
this.events.push(...events);
process.nextTick(this.consume);
}

/**
* Registers a callback function to be called after an event has been consumed
* by a different callback. The callback can return:
Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/private.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,13 @@ export interface WorkflowOrchestratorContext {
encryptionKey: CryptoKey | undefined;
worldCapabilities?: WorldCapabilities;
globalThis: typeof globalThis;
/**
* Increments on every retained-session resume. Suspension signals capture
* it when scheduled and no-op if it moved — a timer queued at boundary N
* must not signal after the session resumed into boundary N+1, so the
* state machine never has to reason about cross-boundary timer ordering.
*/
suspensionGeneration: number;
eventsConsumer: EventsConsumer;
/**
* Map of pending invocations keyed by correlationId.
Expand Down
Loading
Loading