fix(daemon): durable relay role + honest doctor + daemon-authoritative endpoints (#267) - #1302
Merged
Conversation
…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
enabled auto-merge (squash)
July 31, 2026 18:22
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Lands the #267 relay-stability arc in canary so
airc updatedistributes 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):
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