docs(events): Phase 9 as built — UO wave 1, and a budgetMs rule the phase found #217

Merged
whitlocktech merged 1 commits from docs/events-phase-9 into edge 2026-09-04 12:55:53 +00:00
Member

The docs half of Module-uo#28. Three files, all additive.

MODULE_API.md §2.4 — a new contract rule

budgetMs must EXCEED the timeout of whatever the action talks to. The dispatcher classifies a
budget timeout as retry unconditionally and does not ask the action — it cannot, the action is
still awaiting a socket. So an action whose own client gives up after core's deadline never gets
to classify its own failure, and retry: false in its envelope is unreachable code.

The default budgetMs is 10s and module-uo's sidecar client waits 12s — the wrong way round, so
every slow shard produced a retry the module had explicitly refused. That is a general rule about a
call with a far end, not a fact about one sidecar, so it belongs beside the envelope rules rather
than in the plan. The "no shape a failure can take reads as success" bullet now cross-references it,
because the two are halves of one thing: retry: false is only meaningful if it is reachable.

EVENTS.md §F — "What Phase 9 settled, as the contract's first module registrant"

Wave 1 is the first time anything but core registered an action. Four findings, three of them rules:

  • the budgetMs rule above;
  • on_failure is not a retry policy — it is what happens after EVENT_STEP_MAX_ATTEMPTS, so
    skip does not stop a broadcast going out three times, and an action that must be attempted once
    has to answer retry: false itself. That is a heavier thing to ask of a declaration than the plan
    assumed, and it is why the budgetMs rule is load-bearing rather than a tuning note;
  • reconcile() need not be a query — a module that knows when its resources are lost can
    answer from a stamp. Wave 1 records the shard bootId in the payload and reports in force exactly
    the rows whose stamp still matches: correct for the module's own trigger and for core's boot sweep,
    where the game may not have restarted at all and "all gone" would abandon live rows;
  • a module's reconcile() trigger must fire on a LIVE signal, never a replay. §L says the module
    says when; it did not say a module's feed may replay history. Any module wiring this seam owes the
    same gate — see the defect below.

EVENTS_PLAN.md — Phase 9 as built

The four org-lead decisions (2026-09-04, all as recommended); the plan's own broadcast trap and why
its stated fix does not work; the live walk — a real two-phase event against ServUO + the sidecar +
the website, with the shard's audit reading web:event:3692 broadcast, the dry run refusing a cap
breach before publish, the option sources resolving 387/558/800 rows from the real atlas, teardown
reverting both keyed resources, and core orphaning them 31ms after a real shard restart.

Three defects the walk found, two of them data loss, none visible to a unit test — the backfill
replay that would have orphaned every live resource on a WS reconnect; the shard's own refusal
reason being dropped from the run log; and a "not retried" clause that explained the wrong thing on a
permanent status.

And one in Phase 8's shipped code, recorded and deliberately not fixed. UNRESOLVED in
eventRunResources.db.js includes orphaned, so cleanup selects an orphaned row, calls revert()
on it and records it reverted — contradicting §L's own "orphaned, never reverted" rule, and
spending a sidecar round trip per row on a shard that has just restarted. It is core's file and wants
its own change.

Notes

  • docs has no CI; links and anchors checked by hand. MODULE_API.md §2.4 is a real heading
    (### 2.4 api — what the module registers) and both new references land inside it.

  • CRLF preserved throughout — git diff --numstat reports 31/70/13 added lines and one changed,
    which matches the real content diff rather than a whole-file rewrite.

  • AI-assisted: Claude Code (Opus 5).

Code: RunicGateway/Module-uo#28.

🤖 Generated with Claude Code

The docs half of `Module-uo#28`. Three files, all additive. ## `MODULE_API.md` §2.4 — a new contract rule **`budgetMs` must EXCEED the timeout of whatever the action talks to.** The dispatcher classifies a budget timeout as `retry` *unconditionally* and does not ask the action — it cannot, the action is still awaiting a socket. So an action whose own client gives up **after** core's deadline never gets to classify its own failure, and `retry: false` in its envelope is unreachable code. The default `budgetMs` is 10s and `module-uo`'s sidecar client waits 12s — the wrong way round, so every slow shard produced a retry the module had explicitly refused. That is a general rule about a call with a far end, not a fact about one sidecar, so it belongs beside the envelope rules rather than in the plan. The "no shape a failure can take reads as success" bullet now cross-references it, because the two are halves of one thing: `retry: false` is only meaningful if it is reachable. ## `EVENTS.md` §F — "What Phase 9 settled, as the contract's first module registrant" Wave 1 is the first time anything but core registered an action. Four findings, three of them rules: - the `budgetMs` rule above; - **`on_failure` is not a retry policy** — it is what happens *after* `EVENT_STEP_MAX_ATTEMPTS`, so `skip` does not stop a broadcast going out three times, and an action that must be attempted once has to answer `retry: false` itself. That is a heavier thing to ask of a declaration than the plan assumed, and it is why the `budgetMs` rule is load-bearing rather than a tuning note; - **`reconcile()` need not be a query** — a module that knows *when* its resources are lost can answer from a stamp. Wave 1 records the shard `bootId` in the payload and reports in force exactly the rows whose stamp still matches: correct for the module's own trigger and for core's boot sweep, where the game may not have restarted at all and "all gone" would abandon live rows; - **a module's `reconcile()` trigger must fire on a LIVE signal, never a replay.** §L says the module says when; it did not say a module's feed may replay history. Any module wiring this seam owes the same gate — see the defect below. ## `EVENTS_PLAN.md` — Phase 9 as built The four org-lead decisions (2026-09-04, all as recommended); the plan's own broadcast trap and why its stated fix does not work; the live walk — a real two-phase event against ServUO + the sidecar + the website, with the shard's audit reading `web:event:3692 broadcast`, the dry run refusing a cap breach before publish, the option sources resolving 387/558/800 rows from the real atlas, teardown reverting both keyed resources, and core orphaning them **31ms after a real shard restart**. **Three defects the walk found, two of them data loss, none visible to a unit test** — the backfill replay that would have orphaned every live resource on a WS reconnect; the shard's own refusal reason being dropped from the run log; and a "not retried" clause that explained the wrong thing on a permanent status. **And one in Phase 8's shipped code, recorded and deliberately not fixed.** `UNRESOLVED` in `eventRunResources.db.js` includes `orphaned`, so cleanup selects an orphaned row, calls `revert()` on it and records it `reverted` — contradicting §L's own *"orphaned, never reverted"* rule, and spending a sidecar round trip per row on a shard that has just restarted. It is core's file and wants its own change. ## Notes - `docs` has no CI; links and anchors checked by hand. `MODULE_API.md` §2.4 is a real heading (`### 2.4 api — what the module registers`) and both new references land inside it. - CRLF preserved throughout — `git diff --numstat` reports 31/70/13 added lines and one changed, which matches the real content diff rather than a whole-file rewrite. - [x] AI-assisted: Claude Code (Opus 5). Code: RunicGateway/Module-uo#28. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
wtclaude added 1 commit 2026-09-04 12:39:19 +00:00
`EVENTS_PLAN.md` Phase 9 as built, plus the two findings that are contract rather
than facts about UO.

- **MODULE_API.md §2.4: `budgetMs` must exceed the timeout of whatever the action
  talks to.** The dispatcher classifies a budget timeout as `retry`
  unconditionally and cannot ask the action, so an action whose own client gives
  up after core's deadline never classifies its own failure and `retry: false` is
  unreachable. The default (10s) is shorter than module-uo's sidecar client (12s),
  the wrong way round.
- **EVENTS.md §F gains "What Phase 9 settled, as the contract's first module
  registrant"**: the rule above; that `on_failure` is not a retry policy; that
  `reconcile()` can answer from a stamp rather than a query; and that a module's
  reconcile trigger must fire on a live signal, never a replay.

The plan block records the four org-lead decisions, the live walk, the three
defects it found — two of them data loss — and the Phase 8 defect in core it
reproduced and deliberately did not fix here.

Co-Authored-By: Claude <noreply@anthropic.com>
whitlocktech merged commit c2fba44ffd into edge 2026-09-04 12:55:53 +00:00
whitlocktech deleted branch docs/events-phase-9 2026-09-04 12:55:54 +00:00
Sign in to join this conversation.
No description provided.