---
id: PRD-0012
title: "M11 — same-server dedupe and supersede-storm fixes"
status: done
owner: "Aaron Roney"
created: 2026-07-03
updated: 2026-07-03

principles:
- "Root cause over mitigation: detect that two URLs are the same server, don't just slow the fight down."
- "Zero wire-protocol risk: the server instance ID travels as an HTTP upgrade header, out-of-band of the bincode frames."
- "Failures must self-describe: a superseded session should say so, not report a generic auth error."

references:
- name: "Live repro (production, 2026-07-03)"
  url: https://github.com/twitchax/conclave
- name: "DESIGN.md §16 (reconnect), §5 (supersede)"
  url: https://github.com/twitchax/conclave/blob/main/docs/DESIGN.md

acceptance_tests:
- id: uat-001
  name: "A short-lived connect does not reset the reconnect backoff; a stable link does"
  command: cargo make test
  uat_status: verified
- id: uat-002
  name: "A superseded session receives a self-describing termination reason"
  command: cargo make test
  uat_status: verified
- id: uat-003
  name: "A bridge dialing one server under two URLs disables the duplicate link with a single notice and keeps exactly one live session"
  command: cargo make test
  uat_status: verified
- id: uat-004
  name: "conclave server list/remove manage local registrations (overrides removed with the server)"
  command: cargo make test
  uat_status: verified

tasks:
- id: T-001
  title: "Reconnect backoff resets only after a stable uptime, not on connect"
  priority: 1
  status: done
  notes: "Supersede fights produce successful-but-instant connects; reset-on-connect keeps the loop at 200ms forever. Reset only after the link stays up >= 30s."
- id: T-002
  title: "Force-drop reasons: superseded / revoked / idle / slow-consumer self-describe"
  priority: 1
  status: done
  notes: "Replace the bare kill Notify with a reason-carrying handle; the session driver reports the reason in the termination frame."
- id: T-003
  title: "Server instance ID on the WS upgrade; bridge dedupes same-server URLs at startup"
  priority: 1
  status: done
  notes: "Persisted random ID, sent as the x-conclave-server-id upgrade header (old bridges ignore it). The bridge claims IDs pre-auth; a URL landing on an already-claimed server disables that link permanently with one notice."
- id: T-004
  title: "conclave server list / remove (local registration management)"
  priority: 2
  status: done
  notes: "There is no way to drop a stale registration without hand-editing config.toml — which is how the double-registration got stranded. remove also drops the server's permission overrides."
- id: T-005
  title: "Docs: skill CLI surface, DESIGN transport note, CHANGELOG"
  priority: 3
  status: done
  notes: "Skill lists the server verbs; DESIGN documents the upgrade header; CHANGELOG under [Unreleased]."
---

# Summary

Fix the reconnect storm discovered live on 2026-07-03: a machine registered on the same physical
server under two URLs (fly.dev + custom domain) runs a bare `conclave bridge`, which dials both,
authenticates the same `user/machine/session` path twice, and the two links supersede each other in
a hot loop (the backoff resets on every successful connect, so the fight runs at the 200ms base
forever, spewing link notices into the session).

# Problem

The supersede-on-attach behavior (#16) is correct for its purpose — a reconnect after a half-open
drop must take over immediately — but when *one process* holds two links to the same server under
different URLs, supersede becomes a perpetual-motion machine. Three compounding gaps: the bridge
cannot tell two URLs are the same server; the backoff treats an instantly-killed connect as
success; and the termination frame ("session terminated") reads like an auth failure, hiding the
actual mechanism. A fourth gap stranded the state: registrations cannot be removed locally except
by hand-editing `config.toml`.

# Goals

1. A bridge never fights itself: same-server URLs are detected and deduped at connect time.
2. Any residual supersede fight (e.g. two machines sharing a fixed `--as`) is slow and legible,
   not a hot spew.
3. Local registrations are manageable from the CLI.

# Technical Approach

- **T-001**: `Backoff::on_disconnect(uptime)` replaces reset-on-connect; reset only when
  `uptime >= 30s`. Measured with `tokio::time::Instant` around the pump.
- **T-002**: `Kill { notify, reason: OnceLock<&'static str> }` replaces the bare `Notify` in
  `SessionEntry`; `fire(reason)` records why, the session driver sends
  `Unauthorized(reason)` on kill.
- **T-003**: the store persists a random instance ID (generated once, `identity::generate_token`);
  `serve` returns it as the `x-conclave-server-id` header on the WS upgrade response. The bridge
  keeps a process-wide claim map (instance ID → first URL); `connect_ws` checks the header
  *pre-auth* (so a duplicate never attaches, never supersedes) and a claimed ID under a different
  URL permanently disables that link with a single `LinkEvent::Duplicate` notice. Old
  bridges/servers ignore the header entirely — no wire change.
- **T-004**: `conclave server list` prints registrations; `conclave server remove <url>` drops the
  registration and its permission overrides atomically.

# Assumptions

- One bridge process per session; claims are process-lifetime (startup dedupe, no live handoff).
- Against a pre-T-003 server (no header), behavior degrades to today's (T-001 makes it slow).

# Constraints

- No bincode frame changes: struct fields cannot be appended compatibly, so the ID rides the HTTP
  upgrade, not the protocol.

# References to Code

- `src/bridge/client.rs` — backoff, `run_link`, `connect_ws`.
- `src/server/hub.rs` — `attach` supersede, force-drop sites; `src/server/session.rs` — kill frame.
- `src/server/wss.rs` — upgrade handler; `src/store.rs` — instance ID persistence.
- `src/bin.rs` — CLI verbs; `src/identity.rs` — `Config` load/save.

# Non-Goals (MVP)

- Live claim handoff (duplicate link taking over if the canonical link dies): the canonical link
  already reconnects forever; a dormant standby adds state machinery for no observed need.
- Server-side multi-link detection (the server cannot distinguish two URLs from two processes).

# History

- 2026-07-03: Created from the live production repro (supersede storm between two URLs of the same server).
- 2026-07-03: All five tasks landed and verified in one pass: backoff stability, self-describing kill reasons, upgrade-header dedupe (store ID + wss header + bridge claims), `server list/remove`, and docs. Gate green (183 tests).
