SIGN IN SIGN UP

[core] Anchor the replay clock to deliveries, not consumption (#4213)

* [core] Anchor the replay clock to deliveries, not consumption

The VM's Date.now() advanced in EventsConsumer.onConsumedEvent, i.e. when
the walk read an event. The walk runs ahead of delivery: within one drain
window it consumes every event whose consumer exists, so a hook_received for
a hook the workflow has not read, or a sibling step's later result, was
consumed while an earlier delivery still sat on its barrier, and its
timestamp leaked into that delivery's cascade. Date.now() at a body position
then depended on how much log the replay held: the writing execution saw the
heartbeat's time, a later replay holding one more payload saw the payload's.
A workflow whose control flow reads the clock (an idle loop budgeted by
Date.now(), a deadline) drew different ordinals in different replays and
died CORRUPTED_EVENT_LOG. This is the production wake-loop family of the
2026-08-31 beta.46 spike: step and heartbeat wait bound to one ordinal by the
same invocation a second apart, with the wait's resumeAt computed from the
earlier step's completion time.

The clock now advances in registerDeliveryBarrier's markDelivered, with the
delivered event's createdAt, for step results, hook payloads, wait
completions and aborts. Deliveries reach the workflow in log order, so the
clock a cascade observes is a function of the log prefix. The long-parked
it.fails "should maintain determinism of Date across executions" (a race of
two sleeps) passes and is flipped to it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Apply suggestion from @VaguelySerious

Signed-off-by: Peter Wielander <mittgfu@gmail.com>

* docs: say when the workflow clock advances

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Address review: registration outcomes advance the clock; deliveredAt required; more cases

hook_created and hook_conflict settle awaiters the workflow is blocked on,
so they are deliveries and now advance the clock in their settlement slots
(they registered no barrier on this branch and had stopped moving it, a
regression against main). registerDeliveryBarrier requires deliveredAt so a
new delivery site cannot forget the clock, with one hoisted deliver()
closure behind both handle shapes. Tests cover the buffered-payload claim
(a delivery carrying an older time than the clock), both registration
outcomes, and aborts. Comments that described the consumption-anchored
clock are updated, and the QuickJS note now says that engine still advances
per event and why that is prefix-stable there.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Rebase onto main: the registration barrier from #4215 carries deliveredAt

hook_created and hook_conflict now advance the clock through the barrier
release that main already hands them over on, instead of the explicit
advanceClock calls this branch added before that barrier existed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Apply suggestion from @VaguelySerious

Signed-off-by: Peter Wielander <mittgfu@gmail.com>

---------

Signed-off-by: Peter Wielander <mittgfu@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
P
Peter Wielander committed
d3ea4a6275a7782df44ea92dac2da51aa064cb75
Parent: f160d64
Committed by GitHub <noreply@github.com> on 9/21/2026, 2:17:43 PM