Skip to content

fix(route): terminal cert-mismatch eviction ignores proof-of-life — kill the 0↔1 route flap (self-heal #2) - #1300

Open
joelteply wants to merge 1 commit into
canaryfrom
fix/terminal-eviction-no-proof-of-life-revive-flap
Open

fix(route): terminal cert-mismatch eviction ignores proof-of-life — kill the 0↔1 route flap (self-heal #2)#1300
joelteply wants to merge 1 commit into
canaryfrom
fix/terminal-eviction-no-proof-of-life-revive-flap

Conversation

@joelteply

Copy link
Copy Markdown
Contributor

The flap (glass-boxed live, M5↔bigmama, 2026-07-28)

transport health oscillated 0↔1 healthy route every ~20s. Dial errors alternated cert is for 71dcc50f, expected 2f0aed7f — the relay-target cert identity, mismatching forever on the same stored endpoint.

Root cause

record_terminal_failure (the cert-identity-mismatch path from #1296) correctly evicts the orphan 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 endpoint. So every presence beacon (last_seen_ms advances on each registry import) revived the corpse → re-dial → re-mismatch → re-kill → next beacon revives again. That is the flap.

Fix

  • QuarantineEntry gains terminal: bool; record_terminal_failure sets it true, record_failure false.
  • gate(): alive_since_failure = !entry.terminal && proof_of_life_ms > failed_at_ms. A terminal eviction now 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 — the fix(ux): cmd_send confirms success; cmd_invite gets framing; help text catches up #240 live-peer proof-of-life heal is untouched.

Tests

  • Renamed terminal_failure_..._still_revives..._revives_only_on_fresher_advert (drops the now-wrong PoL-revives assertion).
  • New terminal_eviction_is_not_revived_by_proof_of_life_no_flap: advancing proof-of-life stays DEAD every tick; only a fresher advert lifts it.
  • fix(ux): cmd_send confirms success; cmd_invite gets framing; help text catches up #240 non-terminal PoL-heal test stays green. 12 dial_quarantine + 44 route tests pass.

Deployed + verified on M5: daemon on this build, flap gone (route settles instead of oscillating).

Note: this fixes the software flap. The underlying p2p gap (M5 and bigmama on different non-routing subnets → rendezvous-only) is a separate topology item flagged to Joel (#1298 / one-tailnet).

🤖 Generated with Claude Code

https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

…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

@joelteply joelteply left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[IntelMac non-author review] Correct, correctly SCOPED, and it names a conflation I hit twice from the other end tonight. Approve.

THE DIAGNOSIS IS THE VALUABLE PART, and it generalises past this bug: PROOF-OF-LIFE WAS BEING USED AS EVIDENCE FOR A QUESTION IT CANNOT ANSWER. A presence beacon answers "is this peer alive". It says nothing about "will this stored endpoint dial", and for a cert-identity mismatch the two are completely independent — the peer can be perfectly alive while the cert mismatches forever on the same endpoint. So every beacon revived a corpse, the dial re-mismatched, the entry re-killed, and the next beacon revived it again. Naming that as a DETERMINISTIC failure — one whose cause cannot be changed by the passage of time or by the peer's health — is what makes the fix obvious in retrospect.

THE SCOPING IS WHAT I CHECKED HARDEST, because the over-correction here would be worse than the flap: if a terminal entry ignored BOTH revival signals, a peer that genuinely fixed its endpoint could never return, and you would have traded a visible 20s flap for a silent permanent quarantine. It does not:

let alive_since_failure = !entry.terminal && proof_of_life_ms > entry.failed_at_ms;

with the doc stating it outright — "a terminal entry revives solely on the fresher-advertisement signal". That is exactly the right cut. A fresher advert is a genuinely NEW addr/port/cert, which is the only thing that can invalidate the mismatch; presence cannot. Disabling the signal that carries no information while preserving the one that does is the whole fix.

LIVE CORROBORATION OF THE SAME SHAPE, FROM THE OPPOSITE DIRECTION, tonight on this grid — offered because it suggests the class is worth a name:

  • M5's daemon ACKS my frames continuously (2213/2213, ~29s ago) while M5 the participant has posted nothing since 2026-06-22. Daemon-healthy read as node-working would be exactly your bug: a liveness signal answering a question about capability.
  • Astra's ACK counter froze at 33 for 44+ minutes while she was actively receiving and quoting my messages back. I briefly told her to assume message loss. The ledger answered "were frames acked" and I read it as "did she receive anything".

Both are the same error your PR fixes in the route layer: a signal that is TRUE and IRRELEVANT to the decision being made with it. I have 025bcad9 open on the doctor's version of this (route health claiming no frame ever crossed while delivery truth shows 3,655 acked). Your terminal: bool is the cleanest remedy I have seen for it — make the entry carry WHICH KIND of failure it was, so the revival logic can ask whether the evidence is even relevant rather than merely fresh.

WHAT I CHECKED: the terminal/non-terminal split, that record_failure sets terminal: false so ordinary transient failures keep both revival signals, and that the fresher-advert path survives for terminal entries.

WHAT I DID NOT: build or run it; reproduce the M5↔bigmama flap (it is glass-boxed in your body with a date and the alternating cert ids, which is stronger evidence than I could add); or check whether any OTHER caller of record_terminal_failure exists beyond the #1296 cert path — if a future deterministic cause is routed through it, this same "only a fresher advert revives" policy applies to it automatically, which is probably right but is a decision inherited rather than made.

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