Skip to content

fix(daemon): durable relay role + honest doctor + daemon-authoritative endpoints (#267) - #1302

Merged
joelteply merged 10 commits into
canaryfrom
fix/relay-port-stability-267
Jul 31, 2026
Merged

fix(daemon): durable relay role + honest doctor + daemon-authoritative endpoints (#267)#1302
joelteply merged 10 commits into
canaryfrom
fix/relay-port-stability-267

Conversation

@joelteply

Copy link
Copy Markdown
Contributor

Lands the #267 relay-stability arc in canary so airc update distributes it to every node — today's glass-boxed incident: this fix sat on its feature branch while both grid nodes ran pre-fix binaries, the relay listener died, routes went 0/44, and cross-node messages (including a paper hand-off) were silently dropped for the afternoon.

Contents (bidirectionally verified across M5 <-> BigMama on 2026-07-30, re-verified live today):

  • relay-hood is a DURABLE ROLE: persisted relay-port file, re-assumed on every tick — an ephemeral listener death no longer orphans the grid's route records
  • stable relay bind candidates instead of ephemeral ports
  • honest doctor: 0 healthy routes reports no-routes/DOWN, never [ok]
  • endpoints are daemon-authoritative (Request::RouteEndpoints) — the CLI scope view no longer lies 'endpoints: none' while the daemon listens
  • dial-time self-relay port substitution for records pinning our own stale relay

Supersedes #1301 (same branch, was targeting main and went CONFLICTING as canary advanced; this branch already contains canary so this merge is clean).

🤖 Generated with Claude Code

https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

joelteply and others added 8 commits July 28, 2026 16:28
…ival — kill the 0↔1 route flap (self-heal #2)

Glass-boxed live on the M5↔bigmama grid 2026-07-28: transport health oscillated
0↔1 healthy route every ~20s. Root cause: record_terminal_failure (the
cert-identity-mismatch path, #1296) evicts the orphan endpoint on the first
strike but left BOTH revival signals live — a fresher advert OR proof-of-life.
Proof-of-life revival is wrong for a DETERMINISTIC failure: the peer being alive
changes nothing about a cert that mismatches forever on the SAME stored endpoint.
So every presence beacon (last_seen_ms advances each registry import) revived the
corpse → re-dial → re-mismatch → re-kill → next beacon revives again. That IS the
flap ('cert is for 71dcc50f, expected 2f0aed7f' cycling).

Fix: mark terminal entries `terminal: true` on QuarantineEntry; gate()'s
alive_since_failure is now `!entry.terminal && proof_of_life_ms > failed_at_ms`.
A terminal eviction revives SOLELY on a genuinely fresher endpoint ADVERTISEMENT
(new addr/port/cert) — the only thing that can actually clear a deterministic cert
mismatch. Non-terminal (transient) failures keep both signals, preserving the #240
live-peer heal untouched.

Tests: renamed the terminal test to _revives_only_on_fresher_advert (drops the now-
wrong proof-of-life-revives assertion); added terminal_eviction_is_not_revived_by_
proof_of_life_no_flap pinning the flap regression (advancing PoL stays DEAD every
tick; only a fresher advert lifts it). #240 non-terminal PoL-heal test stays green.
12 dial_quarantine + 44 route tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…ve streaming, throttled 1/s

The seam summary was the ONLY advance a consumer ever saw: during live
streaming the watermark never moved, so a cursor-persisting consumer
(continuum #261, found by its PR #2057 review) re-received the WHOLE
session's events on every reattach — five reboots of full-session
redelivery produced a persona echo storm on 2026-07-30. After each
forwarded Event the daemon now emits an advance frame carrying that
event's cursor, throttled to one per second (kinds:None attaches carry
StreamChunk bursts — never a frame storm). The unadvanced tail at
shutdown shrinks from the whole session to ≤1s of events. skipped=0 on
heartbeats (nothing suppressed).

Contract change, test re-pinned: AttachCursorAdvanced may interleave on
ANY attach shape; clients must tolerate it. The no-skip property the
test now asserts: an advance NEVER precedes the delivery of the event
it points at — persisting advanced_to can only resume at-or-before what
was seen. attach_backlog_coalesce 3/3, airc-daemon suite green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…nest doctor on zero routes

Three layers of the 'airc always broken' shape, glass-boxed live today
(peers dialing relay :65280 while the live listener sat on :57958,
Connection refused, 0 healthy routes — and doctor stamping [ok] on it):

1. doctor: 'degraded == 0' was vacuously true for an EMPTY health list —
   zero routes with enrolled remote peers now WARNS 'remote delivery is
   DOWN' instead of [ok] 0 route(s) healthy.
2. dial: a relay endpoint pinning THIS node's peer id is rewritten onto
   the LIVE relay listener port before dialing — the node is the
   authority on its own listener; it must never dial (then quarantine)
   a previous incarnation's dead port.
3. election: become_relay_with_stable_port persists the bound port in
   the runtime dir and re-binds it on restart, so every peer's imported
   airc-relay://me@ip:port card stays valid across daemon restarts —
   OS-assigned is only the first-election / port-stolen fallback.

Tests: self-relay substitution (live port wins / foreign relay dials as
recorded) + bind-candidate order (persisted first, no double-zero).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…howing (#270)

The day's fourth silent-drop layer was pure presentation: bare 'airc room'
printed only the CURRENT room (read as a membership list) and 'airc inbox'
silently paged only the current room — so a subscribed-but-not-current
channel's messages sat invisible while both agents diagnosed a transport
failure that did not exist. Membership must be visible to be trusted:
room now lists every subscription with the current one marked; inbox
headlines 'room X ONLY — N other subscribed room(s) NOT shown: ...'.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
… APIs

The #270 rename to 'current:' broke continuum-core's airc bootstrap, which
parses `airc room` stdout for the 'room: <name>' line (degraded probe on
next boot). Restore the stable label; the subscription list stays beneath.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…ks /^channel:/

Second downstream parser of the same human output (continuum's
start-server.sh derives AIRC_DEFAULT_CHANNEL via awk anchored at line
start); the #270 indent broke it and fail-loud full-citizen boot refused.
Exact legacy shape restored; subscription list still appended after.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…t the CLI scope

The five-month 'airc is broken' pattern was mostly this class: short-lived
CLI probes building their own scope-local view of state the machine daemon
owns. 'endpoints: none' printed while the daemon listened on two ports with
live inbound connections — a lie of omission that misdirected every
transport debug toward routes that were fine. Now: ask the daemon
(Request::RouteEndpoints, the op card 4b6a0ffa already built for exactly
this reason); print each advertised endpoint; empty-from-daemon says
loudly 'NOT dialable'; daemon-unreachable falls back to the scope view
LABELED non-authoritative. Verified live: daemon-advertised LanTcp
192.168.1.249:57958 now visible where 'none' printed before.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…elay on every tick

Regression glass-boxed 2026-07-31 morning: overnight daemon restart dropped
the :65280 relay listener because self-election only fires when NO peer is
reachable — and a direct LAN peer was connected. But peers hold
airc-relay://me@ip:port cards; a relay that only exists during total
isolation refuses every card-holder the rest of the time. The persisted
relay-port file IS the role record: when it exists, become_relay runs every
tick unconditionally (idempotent — already-relaying is a cheap re-advertise).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
@joelteply
joelteply enabled auto-merge (squash) July 31, 2026 18:22
joelteply and others added 2 commits July 31, 2026 13:46
…nt the live feed

Glass-boxed 2026-07-31 after a full day of it: a multi-paragraph room
message printed its body raw, so every line-tailing consumer (agent
monitors, Codex hooks, grep pipelines, both grid agents' feeds) saw one
attributed '[Message] a -> b: <first line>' and then a storm of orphan
fragments ('1. **', '- Investigate', 'Let') with no sender, no channel,
no kind — hours of unattributable noise per message, on BOTH nodes
('the version-drift rendering'). render_feed_line now flattens newlines
to a visible pilcrow so the line contract holds unconditionally;
regression test pins it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…s assertion

Two CI-only reds on #1302, both genuinely ours:

- clippy (-D clippy::expect-used): become_relay_with_stable_port ended in
  last_err.expect(). Restructured so the OS-assigned bind (port 0 — always
  the terminal candidate, pinned by the relay_bind_candidates tests) is the
  final attempt whose error propagates via ?. No Option, no panic path.

- transport_health_reports_no_routes_on_fresh_scope asserted the OLD
  scope-local 'endpoints: none' literal — exactly the line this PR replaced
  with daemon-authoritative rendering. A fresh scope now prints one of two
  honest states (daemon advertising nothing / daemon unreachable, labeled
  scope-local) depending on whether the environment auto-spawned the scope
  daemon; the test accepts both and still rejects the killed lie.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
@joelteply
joelteply merged commit 1cb0195 into canary Jul 31, 2026
11 of 12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant