feat(sidecar): protocol 6 — carry the key, answer bridge.busy (Phase 11a) #36

Merged
whitlocktech merged 1 commits from feature/protocol-v6-idempotency into edge 2026-09-04 23:06:45 +00:00
Member

EVENTS_PLAN.md Phase 11a, the sidecar half. PROTOCOL_VERSION 5 → 6, and one behaviour: bridge.busy maps to 425 Too Early in all three responders.

Everything else is free, and that is the interesting part. The idempotencyKey rides in the command body, which every write endpoint already passes through verbatim; champ.boss.killed lands in events and on the feed through the generic forward path with no arm of its own. No store migration — nothing gains a column.

That is the dumb-forwarder property doing its job (v3 §3), and it is worth naming what it means here specifically:

The sidecar makes no idempotency promise of its own. It does not dedupe, does not cache, and does not know what a key means. The guarantee is the shard's, end to end — which is the only place it can be, because the shard is where the world write happens.

Why 425 and not 409

409 is already the protocol-version gate's answer, and the two want opposite dispositions from a client: a version mismatch is a deployment fault nobody should retry, and a busy shard is a retry that will succeed on its own. Sharing a status would make the difference readable only by inspecting the body — which is exactly how a retry loop ends up quietly swallowing a mismatched deployment.

425 is what that status is for: a server unwilling to risk processing a request that might be a replay. It is unambiguous here because nothing else on this surface uses it.

A replayed reply, by contrast, is an ordinary 200. The caller must be able to treat it exactly as it would have treated the answer it lost; replayed: true is for the log.

The passthrough is documented rather than left implicit

admin_call's doc comment already said the body's "remaining fields pass straight through", and idempotencyKey is one of those — so this half needed no code. It is now named explicitly, because a later refactor that narrowed the body to a known field list would silently turn every retried world write back into a possible duplicate, and nothing here would fail.

The same comment records the fact that makes the shard's replay logic necessary: reqId is regenerated on every call, so a retry carries the same idempotency key under a new correlation id, and the shard has to re-stamp a replayed reply rather than echo the id the first attempt used.

Verification

  • cargo test43 pass, 0 fail (4 new): 425 on all three responders, 425 ≠ the version gate's 409, a replayed reply is still a 200, and the pre-existing error mapping the busy arm is threaded in front of is unchanged.
  • cargo fmt --check and cargo clippy --all-targets -- -D warnings clean.
  • The release binary run against a real ServUO on the local rig: --version reports protocol 6, GET /health reports "protocol":6, and both the key and the new kind arrived through the generic paths with no code here touching either.
  • Live: three POST /admin/broadcast (two sharing a key) produced two rows in the shard's audit trail, with the repeat answered {"kind":"admin.ok","replayed":true,"reqId":"r-2","t":<the first attempt's t>}. The id-correlated plane walked too, via POST /towncrier.

bridge.busy was not reachable on the live rig, and that is a property of the shard rather than of this change: with today's synchronous handlers a repeat cannot arrive while the original is still running. The mapping is unit-tested; Phase 11b's leases are the first handlers that defer and therefore the first that can produce it.

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

Plugin: RunicGateway/servuo-plugins#21 · Module: RunicGateway/Module-uo#29 · Docs: RunicGateway/docs#219

🤖 Generated with Claude Code

`EVENTS_PLAN.md` Phase 11a, the sidecar half. **`PROTOCOL_VERSION` 5 → 6, and one behaviour**: `bridge.busy` maps to **425 Too Early** in all three responders. Everything else is free, and that is the interesting part. The `idempotencyKey` rides in the command body, which every write endpoint already passes through verbatim; `champ.boss.killed` lands in `events` and on the feed through the generic forward path with no arm of its own. **No store migration** — nothing gains a column. That is the dumb-forwarder property doing its job (v3 §3), and it is worth naming what it means here specifically: > **The sidecar makes no idempotency promise of its own.** It does not dedupe, does not cache, and does not know what a key means. The guarantee is the shard's, end to end — which is the only place it can be, because the shard is where the world write happens. ## Why 425 and not 409 409 is already the protocol-version gate's answer, and the two want **opposite dispositions** from a client: a version mismatch is a deployment fault nobody should retry, and a busy shard is a retry that will succeed on its own. Sharing a status would make the difference readable only by inspecting the body — which is exactly how a retry loop ends up quietly swallowing a mismatched deployment. 425 is what that status is for: *a server unwilling to risk processing a request that might be a replay.* It is unambiguous here because nothing else on this surface uses it. A **replayed** reply, by contrast, is an ordinary **200**. The caller must be able to treat it exactly as it would have treated the answer it lost; `replayed: true` is for the log. ## The passthrough is documented rather than left implicit `admin_call`'s doc comment already said the body's *"remaining fields pass straight through"*, and `idempotencyKey` is one of those — so this half needed no code. It is now named explicitly, because a later refactor that narrowed the body to a known field list would silently turn every retried world write back into a possible duplicate, and nothing here would fail. The same comment records the fact that makes the shard's replay logic necessary: **`reqId` is regenerated on every call**, so a retry carries the same idempotency key under a *new* correlation id, and the shard has to re-stamp a replayed reply rather than echo the id the first attempt used. ## Verification - `cargo test` — **43 pass, 0 fail** (4 new): 425 on all three responders, 425 ≠ the version gate's 409, a replayed reply is still a 200, and the pre-existing error mapping the busy arm is threaded in front of is unchanged. - `cargo fmt --check` and `cargo clippy --all-targets -- -D warnings` clean. - The **release** binary run against a real ServUO on the local rig: `--version` reports `protocol 6`, `GET /health` reports `"protocol":6`, and both the key and the new kind arrived through the generic paths with no code here touching either. - Live: three `POST /admin/broadcast` (two sharing a key) produced **two** rows in the shard's audit trail, with the repeat answered `{"kind":"admin.ok","replayed":true,"reqId":"r-2","t":<the first attempt's t>}`. The `id`-correlated plane walked too, via `POST /towncrier`. **`bridge.busy` was not reachable on the live rig**, and that is a property of the shard rather than of this change: with today's synchronous handlers a repeat cannot arrive while the original is still running. The mapping is unit-tested; Phase 11b's leases are the first handlers that defer and therefore the first that can produce it. - [x] AI-assisted: Claude Code (Opus 5). Plugin: RunicGateway/servuo-plugins#21 · Module: RunicGateway/Module-uo#29 · Docs: RunicGateway/docs#219 🤖 Generated with [Claude Code](https://claude.com/claude-code)
wtclaude added 1 commit 2026-09-04 23:01:01 +00:00
feat(sidecar): protocol 6 — carry the idempotency key, answer bridge.busy (Phase 11a)
Some checks failed
PR Checks / rust-gates (pull_request) Failing after -16s
5612fba744
PROTOCOL_VERSION 5 -> 6, and one behaviour: `bridge.busy` maps to 425 Too Early in
all three responders.

Everything else is free, and that is the point. The key rides in the command body,
which every write endpoint already passes through verbatim; `champ.boss.killed`
lands in `events` and on the feed through the generic forward path with no arm of
its own. No store migration — nothing gains a column.

Worth naming what the dumb-forwarder property means here specifically: the sidecar
makes no idempotency promise of its own. It does not dedupe, does not cache, and
does not know what a key means. The guarantee is the shard's, end to end, which is
the only place it can be.

425 rather than 409 because 409 is already the protocol-version gate's answer, and
the two want opposite dispositions from a client: a version mismatch is a
deployment fault nobody should retry, a busy shard is a retry that will succeed.
Sharing a status would make the difference readable only by inspecting the body,
which is how a retry loop ends up hiding a mismatched deployment. A replayed reply
is an ordinary 200 — `replayed: true` is for the log.

Co-Authored-By: Claude <noreply@anthropic.com>
whitlocktech merged commit d13ad11eb0 into edge 2026-09-04 23:06:45 +00:00
whitlocktech deleted branch feature/protocol-v6-idempotency 2026-09-04 23:06:46 +00:00
Sign in to join this conversation.
No description provided.