feat(events): send the idempotency key, declare champ.boss.killed (Phase 11a) #29
Reference in New Issue
Block a user
No description provided.
Delete Branch "feature/protocol-v6-idempotency"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
EVENTS_PLAN.mdPhase 11a, the website's half of protocol 6. No route added, no schema change.uo.broadcaststops being un-retryable, and that is the whole point of the phasePhase 9 shipped it answering
retry: falseto everything — including a 503 from a shard that was merely restarting — and said so in a comment naming the line that would change once the wire could refuse a repeat:This is that line. Every event-driven write now carries the step's
idempotencyKey, the shard executes a key at most once, and a retry collects the answer the first attempt never delivered rather than announcing to everyone twice.The classification is now
sidecarFailure's — the same helper its two siblings already used — so the hand-rolled variant that forced every outcome terminal is gone rather than merely re-tuned. That is most of the diff inuoEventActions.js.425 is the new status in that path (
bridge.busy: a command under this key is still in flight). It is not inPERMANENT_STATUSESandclassify()falls through to retry, so it needed no arm — but it is named in a comment so a future tightening of that list has to decide about it deliberately.One verb was less idempotent than its own
idmade it lookBoth keyed verbs post under a run-scoped id and a repeat REPLACES, which is why Phase 9 called them safe to retry. True for the crier. Not quite true for news:
news.addwithannounce: truemakes the criers proclaim the article's title, andBridgeNewsremoves the old entry, inserts the new one and then announces — so a retry replaced the article silently and proclaimed it again. The key stops the second proclamation. Read in the plugin rather than inferred.champ.boss.killedchampsfeature. Not optional: rule 2 fails an unmapped kind closed to admin-only, and a champion falling is exactly what the public board is for.damagersis a nestedstafffield rule. The split is the point — a shard announces that its champion fell without publishing a ranked roll of who was strong enough to fell it. One rule covers the whole table, andacct/webIdinside each entry stay admin-locked by suffix whatever an admin sets it to.killeris deliberately not listed: one actor, whose blow everyone present saw, and the same disclosuremob.killedhas made on the public activity feed for years.uo.champ.boss_killedis declared as a trigger, which is what makes it usable as an event phase condition — a condition is written over a trigger firing, so a kind nothing declares is a kind no event can wait on. "Advance when the boss falls" is the canonical UO phase boundary and it did not exist.damagerCount, never a damager name. A trigger variable is interpolated into mail an operator may address to every subscriber, so a name in the trigger data would undo thestafffield rule one layer up — a distinct hole from the SSE path that rule guards, reachable without touching the visibility config at all. There is a test that asserts no damager name or account appears anywhere in the firing.uo.champ.boss_up, so a cooldown on one altar counts the boss going up and coming down as one subject. A kill the shard could not attribute falls back to the boss's own serial — a subject that exists exactly once, which is all a cooldown needs.Two things the seeds forced, both of them rules
A new rule needs a new seed GROUP, never an appended one.
triggers-v1is stamped once under a settings guard, so a twenty-seventh entry would have reached fresh installs and nothing else.champ-boss-killed-v1is its own group — the same remedy core applied in Events Phase 10, and the third application of Engagement Phase 11's seed-key finding. Its test now asserts the two groups partitionRULESexactly.A trigger is not a stream, and in this module they are disjoint sets. ⚠️ For a reviewer. Push delivery is keyed on the subscription id, which core's
notificationChannelPrefs.cataloggrants only to registered STREAMS. This module's stream ids (champ.start,idoc.warning,vendor.sale, …) and its trigger ids (uo.champ.started,uo.house.idoc_warning, …) share no member. So no engagement rule here can push:publishToUsersresolves zero endpoints whilepushChannel.deliveranswersok: true, 'tickle published'.That is the Events Phase 10 defect, pre-existing here in twenty rules that list
CHANNELS_BROADCAST. This PR does not fix them — it declines to add a twenty-first. The new rule shipsCHANNELS_CONTENT(['email','inapp']), a constant that exists to carry that reason rather than to shareCHANNELS_OWNER's value. Flagged for the org lead as its own change.Verification
npm test --prefix server— 571 pass, 0 fail (9 new).npm test --prefix client— 42 pass.npm run build --prefix clientgreen.check:importsclean.The real wire bytes, not a fixture. The frame captured off the live sidecar during the plugin's rig walk was fed back through this module: trigger
uo.champ.boss_killedwithlocation: "Felucca 1496, 1628 (Britain)"anddamagerCount: 2; projecteddamagersabsent foranonymousandplayer, present forstaff, andacctstripped from every actor at every rung below admin.check:swaggerreports STALE on Windows and is a known false positive — regenerating produces a byte-identical file apart from line endings, and this PR adds no route.AI-assisted: Claude Code (Opus 5).
Plugin: RunicGateway/servuo-plugins#21 · Sidecar: RunicGateway/link#36 · Docs: RunicGateway/docs#219
🤖 Generated with Claude Code