* [core] Make a duplicate attr_set inert instead of terminal A workflow-body attribute write draws a correlation id that resolves exactly once: the dispatcher's consumer takes the matching event and deregisters. A second event under that id therefore has no callback left and never will. `attr_set` had no entry in ENTITY_EVENT_CLASS_BY_TYPE, so the duplicate skip could not take it, and `PARKABLE_EVENT_TYPES` does list the type, so it was parked for a consumer that could never come. Parking is settled by the workflow function returning, and a survivor there is reported through `strandedEvent` as a replay divergence. So the run did all of its work, every step succeeded, and the final replay failed it, deterministically enough to burn the whole replay-divergence recovery budget and terminate with CORRUPTED_EVENT_LOG. Give `attr_set` a class so the straggler is skipped like every other one: committed but inert. Parking still covers the first arrival, for a replay that walks past an attribute event before the body reaches the call that claims it. An attribute write from a step body carries no correlation id and is consumed by the structural lifecycle consumer, so it is unaffected. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * [core] Release a parked duplicate, and agree with the UI about one The class map alone decides a straggler only where the walk meets it after a consumption recorded the class. When neither copy has a consumer yet both park — the walk steps over the first and re-enters in the same tick, with nothing consumed and so no class recorded — and the drain then claims one and holds the other for a callback that will never be registered. That survivor is `strandedEvent`, which is the CORRUPTED_EVENT_LOG this branch set out to stop, reached by the other road. `dropParkedDuplicates` releases it on the same terms the walk skips one. Not an `attr_set` property: `wait_completed` parks in pairs too, and `ONE_SHOT_EVENT_TYPES` only sees the order where the consumption came first. Giving `attr_set` a class also moved the observability UI, which reads the same `entityEventClass` to grey out events a run passed over. It kept treating the straggler as live, because its terminal-class set had no `attr_set` while the dispatcher's consumer does deregister on the first event under an id. The two now share `classifyEntityEvent` and `TERMINAL_EVENT_CLASSES` rather than each keeping a copy of the rule. That sharing needs the entity rule to be exact, because a step-written `attr_set` carries no correlation id: keyed on the run it would collapse every attribute write a run made into one class, and a captured production log in `__fixtures__` holds forty. `classifyEntityEvent` gives such an event no class at all, so neither side can read the second as a repeat of the first. The shared fixture corpus had nothing for `attr_set`, which is why the drift between the two halves went unseen. It has four now, and each of them fails on both sides without the fix above it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Pranay Prakash <pranay.gp@gmail.com>
@workflow/world
Core interfaces and types for Workflow SDK storage backends.
This package defines the World interface that abstracts workflow storage, queuing, authentication, and streaming operations. Implementation packages like @workflow/world-local and @workflow/world-vercel provide concrete implementations.
Used internally by @workflow/core and world implementations. Should not be used directly in application code.
Implementation constraint: no mutable module state
A World implementation must not keep mutable state at module scope. Hold it on
the World instance, or, when it is genuinely process-wide (an ID generator
whose sequence must not fork, a log-once latch), on globalThis via
globalSingleton() from @workflow/utils.
@workflow/world-local and @workflow/world-vercel are bundled into the host
application's server build, and a bundler keys module identity on
(resource, layer): Next.js alone compiles instrument, app-route, ssr and
edge as separate module graphs, so one process holds one copy of every module
in these packages per layer. A top-level let, or a const holding a Map,
is therefore per-copy state rather than the singleton it reads as.
A world loaded at runtime through WORKFLOW_TARGET_WORLD is deduped by Node's
module cache and does not have this problem today, but that is a property of
how it is loaded, not of how it is written, and it has changed before
(vercel/workflow#3493). scripts/lint/module-scope-state.mjs enforces the rule
across every published world package; see
docs/content/worlds/*/building-a-world.mdx for the author-facing version.