Package: @workflow/world-postgres@5.0.0-beta.34 (@workflow/world@5.0.0-beta.27), Node 22, PostgreSQL 17 (Cloud SQL).
@workflow/world-postgres@5.0.0-beta.34 can create a current slot-numbered run with a legacy ULID event because run creation is split across three autocommits.
allocateEventId() treats absence of a workflow_event_slots row as the signal that a run predates slot numbering and returns wevt_<ulid>. Both run creation paths currently make the run visible before inserting that marker:
INSERT workflow_runs -- commits
INSERT workflow_event_slots -- commits later
INSERT workflow_events (run_created) -- commits later
The affected paths are the resilient run_started creation path and the ordinary run_created path in packages/world-postgres/src/storage.ts.
A deterministic reproduction is:
- delay
INSERT on workflow.workflow_event_slots with a test trigger;
- call
world.events.create(runId, { eventType: "run_created", ... });
- as soon as the
workflow_runs row is visible, concurrently call world.events.create(runId, { eventType: "run_started" });
- inspect
workflow.workflow_events.
On beta.34 the persisted log contains slot 1 run_created plus a wevt_<ulid> run_started. The runtime later rejects that log because the event ID is not slot-numbered.
Please wrap the run row, slot marker, and first run_created event in one drizzle.transaction() at both creation sites. Preserve onConflictDoNothing() and the resilient path's re-read when a concurrent ordinary run_created wins. The ordinary path must also insert its run_created event inside the transaction and skip the shared tail insertion, otherwise the marker can commit before slot 1 and a concurrent writer can take the first slot.
The invariant should be: for a slot-era run, no transaction can observe the run row without also observing its slot marker and slot-1 run_created event.
We are carrying this as a local pnpm patch (both creation sites wrapped in one drizzle.transaction()) with a real-Postgres concurrency regression; happy to open a PR if that shape is acceptable.
Package:
@workflow/world-postgres@5.0.0-beta.34(@workflow/world@5.0.0-beta.27), Node 22, PostgreSQL 17 (Cloud SQL).@workflow/world-postgres@5.0.0-beta.34can create a current slot-numbered run with a legacy ULID event because run creation is split across three autocommits.allocateEventId()treats absence of aworkflow_event_slotsrow as the signal that a run predates slot numbering and returnswevt_<ulid>. Both run creation paths currently make the run visible before inserting that marker:The affected paths are the resilient
run_startedcreation path and the ordinaryrun_createdpath inpackages/world-postgres/src/storage.ts.A deterministic reproduction is:
INSERTonworkflow.workflow_event_slotswith a test trigger;world.events.create(runId, { eventType: "run_created", ... });workflow_runsrow is visible, concurrently callworld.events.create(runId, { eventType: "run_started" });workflow.workflow_events.On beta.34 the persisted log contains slot 1
run_createdplus awevt_<ulid>run_started. The runtime later rejects that log because the event ID is not slot-numbered.Please wrap the run row, slot marker, and first
run_createdevent in onedrizzle.transaction()at both creation sites. PreserveonConflictDoNothing()and the resilient path's re-read when a concurrent ordinaryrun_createdwins. The ordinary path must also insert itsrun_createdevent inside the transaction and skip the shared tail insertion, otherwise the marker can commit before slot 1 and a concurrent writer can take the first slot.The invariant should be: for a slot-era run, no transaction can observe the run row without also observing its slot marker and slot-1
run_createdevent.We are carrying this as a local pnpm patch (both creation sites wrapped in one
drizzle.transaction()) with a real-Postgres concurrency regression; happy to open a PR if that shape is acceptable.