OpenWOP openwop.dev

Status: Stable.

Normative home: eventLog.

Why this exists

How a v2 host reads what a v1 host wrote, handles a run in flight at the cut, and maps each persisted store.

The codemap is data

spec/v2/event-codemap.json, shipped in @openwop/spec-artifacts, is the only authority for the v1→v2 event-type mapping. Every row is decided.

  • A host MUST NOT carry a private mapping.
  • A vendor-prefixed v1 type the codemap does not name MUST be read under its own name, unchanged.
  • "Vendor-prefixed" means the first segment is an org registered in the extensions object of spec/v2/declaration.json (events.md §Rules). openwop. is the only reserved prefix. An unregistered first segment is not a vendor prefix and falls to the refusal in §"The reader rule".
  • An org is registered by pull request against the corpus and takes effect on the @openwop/spec-artifacts release that carries it. A shipped entry is append-only.

The era key

eventLogSchemaVersion is the era key. It is required on every run snapshot (schemas/v2/run-snapshot.schema.json).

ValueMeaning
absentOn a store a v1 host has ever written, the run MUST read as 2 (v1 era).
2v1 era; every reader translates through the codemap.
3v2 era; a v2 host MUST stamp 3 on every run it creates.
< 2The v1 rule is unchanged: snapshot fallback, no projection write-through.

Discovery MUST advertise the value the host writes for new runs and nothing else; a host MUST hold one constant for this axis.

Absent stays era 2 forever.

  • A host MUST NOT rewrite historical rows to add an explicit 2, and a reader MUST NOT require one.
  • A host with more than one creation path MUST begin stamping 3 on all of them in the same change. An unstamped path's runs read as era 2.

The snapshot field is required on the wire, and MAY be synthesized: for an era-2 run with nothing stored, the host MUST supply 2 from the absent-⇒-2 rule rather than fail the read.

The eventLog family

eventLog is the capability record by which a host advertises the era contract above. A host that advertises eventLog:

  • MUST stamp eventLogSchemaVersion on every run it creates;
  • MUST serve the cursor contract of events.md §"Poll" over that log;
  • MUST NOT emit an event type the codemap does not name.

Its crossEngineOrdering facet is a replay property, specified in replay.md §"Cross-engine ordering".

The reader rule

A v2 host reading a run in era 2 MUST translate every event through the codemap at the storage boundary:

  • type is mapped, and the payload projected with it.
  • sequence MUST be preserved verbatim, including 0.
  • eventId, timestamp, causationId, and vendor fields pass through.
  • A type the codemap does not name and that carries no reserved vendor prefix MUST fail the read with event_type_unmapped (spec/v2/errors.json, 500).
  • A malformed row MUST fail the read rather than default any field.

The rule binds every reader: poll, SSE, fork, replay divergence, debug bundle, summary memory.

The translation is a read projection. A host MUST NOT rewrite era-2 rows in place. The one exception is a background backfill that stamps 3 and rewrites type under the same (runId, sequence) key: it is permitted only as an atomic per-run operation with the original preserved, so a fork prefix stays byte-equivalent (replay.md).

The writer rule

The era key is fixed when the run is created and fixes the log's vocabulary for the run's lifetime.

  • An append to a run in era 2 MUST use v1 vocabulary — the name the codemap maps from, not the v2 name it maps to.
  • A host that upgrades mid-flight MUST NOT begin writing v2 names into a log the reader translates as era 2.
  • A run created after the upgrade is era 3 and is written in v2 vocabulary, untranslated.
  • A writer that emits a property a closed def cannot seat MUST mark the row with what it could not seat, so the refusal names the writer.

This binds every writer for as long as an era-2 run stays open (§"Runs pinned to v1"). Its witness is v2-era-2-append-vocabulary.

The v1 wire of an era-3 log

Through the overlap (versioning.md §5), an era-3 log, stored in v2 vocabulary, must still read on /v1/… exactly as before the cut. A host serving both majors:

  • MUST map an era-3 log's type back to its v1 spelling on the v1 read path, through the same codemap row, inverted.
  • MUST verify at load that spec/v2/event-codemap.json is a bijection. If a row folds two v1 names onto one v2 name, the host MUST refuse to serve the v1 representation rather than guess a spelling.
  • MUST emit a type with no codemap row (v2-only vocabulary, which has no v1 spelling) unchanged on the v1 read path, MUST NOT drop the row, and MUST NOT refuse the read for it.

The seat

  • The adapter MUST sit at the storage boundary every reader passes through — the storage interface's event-list method, not a wrapper some call sites bypass.
  • A host leg MUST name its seat in its ADR.

The seat is a claims-check (conformance.md §Witness class): it is discharged by that disclosure and by audit, never by the wire. It binds every reader, including ones the suite has no name for.

Runs pinned to v1

A non-terminal run a v2 host inherits carries version.pinned events naming change ids. The host MUST continue it or cancel it, never follow a pin silently.

  • Every pinned change id is still implemented — the run MUST continue under the reader rule. The pin is honored verbatim and version.pinned is never rewritten.
  • Any pinned change id is no longer implemented — the host MUST cancel the run with run.cancelled reason v1_pin_unsupported and cancelledBy: "v2-cutover". The certification bundle reports the count.
  • Suspended on an interrupt at the cut — the run continues under the rules above; its token drains per §"Everything else a v1 host persisted".

Multi-region skew is read-side only: after the cut, a v2 region MUST NOT accept an era-2 write for a run it has already stamped 3. Discovery's minClientVersion rule is in versioning.md §1.5.

Everything else a v1 host persisted

  • Certification bundles — never upgraded. A v1 bundle substantiates no new certification after 2026-11-10; every host produces a fresh v2-rc bundle before the cut (conformance.md).
  • Webhook deliveries — dual-emitted, and queued deliveries drained, per webhooks.md §"Dual emission through the overlap".
  • Interrupt resume tokens — drained, per identity.md §4.
  • Layer-1 and Layer-2 records (idempotency, idempotent responses, invocation claims and logs, effect-escape ledger, dispatch outbox) — unchanged; keyed on ids the cut does not rename. GET /runs/{runId}/effects and GET /runs/{runId}/compensation are new reads over them (security-defaults.md).
  • Owner stamps — a run without a Subject is legacy-stamped per identity.md §1.2. A host's stored owner fields are projected to the Subject; the projection is the host's to name.
  • Audit log — never upgraded.

Per-store disposition

A host's ADR MUST name every store it persists and give each one disposition from the closed set below. A host MUST NOT decide a store's disposition during the migration.

DispositionMeaning
unchangedRows keep their shape and keys; a v2 reader consumes them as they are.
translatedRead through the codemap at the storage boundary; rewritten in place only by the atomic per-run backfill.
drainedRows complete under their own v1 contract until exhausted or expired; no new v1-shaped rows are written.
legacy-stampedA missing v2 field gets its legacy value at first v2 read and is never rewritten.
never-upgradedRows remain v1 evidence only; v2 evidence is produced fresh.
not-persistedNothing to migrate.

Template — one row per store:

Storev1 artifactDisposition
eventsv1 vocabulary; UNIQUE (runId, sequence)translated
runsno eventLogSchemaVersion; owner fieldslegacy-stamped
interruptsun-prefixed (v1) tokensdrained
webhook subscriptions and queued deliveriessubscriptions; serialized deliveriesunchanged; drained
idempotency, invocation, outbox, correlation tableskeyed recordsunchanged
audit logaudit factsnever-upgraded
certification bundlesv1 bundlesnever-upgraded
host-internal tablesoutside the wireunchanged

Durable acceptance and recovery

  • Acceptance — a host that returns success for work it accepted MUST have made the intent to perform it durable in the same transaction as the work record. A wakeup, hint, or in-process dispatch MUST NOT be the only record. The host MUST resume accepted-but-unstarted work after the accepting process dies, without client action.
  • Liveness — a host MUST distinguish how long a unit of work may legitimately run from the interval in which a live worker demonstrates liveness, and MUST NOT use the duration bound as the sole liveness signal.
  • Recovery bound — a host MUST declare the longest interval between an instance ceasing to make progress and another becoming eligible to resume its work. It MUST derive the bound from the mechanism that enforces it: one bound per enforcing mechanism, never a single aggregate. Any length is conformant; an undeclared or unenforced bound is not.
  • Duplicates — duplicate delivery of accepted work MUST NOT produce duplicate external effects. The host MUST dedupe on an identity that survives redelivery (idempotency.md).
  • Poison work — work that fails deterministically MUST reach a terminal, operator-visible state within a bounded number of attempts.

A host MAY claim a qualification rung (durable-single-instance, durable-multi-instance, multi-region-qualified; cumulative) only with the evidence RFC 0158 §D names for it, and MUST NOT claim one from tests in which no process was terminated. Discovery carries no rung; the certification bundle publishes it and its bounds (conformance.md §Bundle v3).

Sources: RFCs 0041, 0158, 0170, 0171, 0172, 0176, 0180, 0185, 0186, 0187.

Next in Core.

Edit this page on GitHub ↗

  • EventsA run is its append-only event log. Every snapshot, stream, poll, fork and diff is a…
  • Replay and ForkPOST /runs/{runId}:fork re-executes a past run state: a replay reproduces recorded…
  • Versioning and ReleaseHow a v2 host selects a major, what each version axis means, and what a release is.
  • WebhooksA client registers a URL and an event filter once; the host POSTs matching events,…