[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