Split leaf of #16737 (S3 roster) — leg 2 of the four remaining: the identity-binding third-signal render. Sibling of #17153 (leg 1, PR #17154 in review).
Context
#16737's AC: "Three independent signals, none inferring another: presence-fresh ≠ wake-route-healthy ≠ identity-bound. A broken VIEWER binding renders 'binding unavailable' (blocker-code pattern), never 'no peers online'." Today the information survives only as redacted prose: a binding-class failure in the plane presence read throws, the adapter catches, and the capability degrades with the error text as reason — indistinguishable BY TYPE from "plane unreachable". A renderer that wants to show "binding unavailable" distinctly would have to string-match prose, which the codebase's own precedent explicitly warns against.
The Problem
- The presence read path (
planeWhoIsOnlineReader → planeMailboxClient.callTool('who_is_online')) has binding-class failure modes the CLIENT already recognizes at throw time: the reconnect identity re-verify refuses on a changed/unprovable identity (client-owned throw, no replay), and plane-side admission/ingress denials surface as tool-error text ("that text is OUR plane's service message", the client doc notes).
- None of these throws carries a typed marker, so every consumer downstream sees an opaque
Error — the type knowledge dies at the throw site.
- The established classification pattern (the "blocker-code pattern" the parent cites) lives in
fleetMailboxMirrorAdapter.mjs: scoped-specific contract-text classification with an explicit anti-pattern warning — "Matching bare Unauthorized would misclassify any unrelated authorization failure as the named admission decision" (ADMISSION_SCOPE = 'CAN_READ_INBOX_OF').
The Architectural Reality
ai/services/fleet/planeMailboxClient.mjs — owns the identity re-verify throw (knows it is binding-class at throw time) and maps tool-error text (the plane's own service messages).
ai/services/fleet/planeWhoIsOnlineReader.mjs — the thin reader; stays thin.
ai/services/fleet/fleetPresenceStateAdapter.mjs — catches thrown reads into the degraded envelope (capability.reason, redacted); rows go unknown with the read reason.
ai/services/fleet/fleetCockpitStatus.mjs — passes the SUPPLIED presence capability through to the cockpit DTO (verified: supplied object wins over the not-wired default).
- Tier-degradation contract governs: absence of classification produces absence of the typed code, never a guessed verdict.
The Fix
- Producer classifies at the site that knows (never downstream message-matching): the client stamps a typed property (e.g.
error.planeBlockerCode = 'viewer-binding-unavailable') on the throws it ALREADY knows are binding-class — the identity re-verify refusal, and the scoped-specific admission/ingress denial texts per the mirror-adapter pattern (exact-text contract match, never bare Unauthorized). First implementation step: enumerate the client's throw sites and bind the exact contract texts (the mirror-adapter discipline).
- Adapter passes through, never infers: a recognized typed code on the caught error lands verbatim as
capability.reasonCode beside the existing redacted reason; absent code ⇒ absent field (closed passthrough, no adapter-side vocabulary invention).
- Cockpit DTO carries it: the presence capability's
reasonCode reaches the cockpit snapshot unchanged (expected zero-diff in fleetCockpitStatus — verify by spec, not assumption).
- Render-side consumption ("binding unavailable" pane state) is C3's job and out of scope here — this leg makes the distinction RENDERABLE (typed in the DTO), which is what the parent AC requires of the roster surface.
Acceptance Criteria
Out of Scope
C3 render-side pane states · legs 3–4 of #16737 (wake-route health, S5-gated scoping) · any change to the plane's own service messages · retrofitting typed codes onto non-presence consumers of the client (mirror adapter keeps its own established classification).
Related
#16737 (parent) · #17153 / PR #17154 (leg-1 sibling) · #16824 (scoped-empty roster — the C3-adjacent cousin) · D#16720 (source discussion).
Live latest-open sweep: latest 15 checked 2026-08-15T09:24Z (newest #17155); no equivalent. A2A in-flight sweep: recent traffic re-checked this hour; active claims (Vega #16972 · Grace #17141 · Phoebe #17147/PR #17156 · Ada #17148 · Euclid #17132-analysis) are disjoint.
Origin Session ID: 1deebbe1-b7e6-4f76-b39d-9cfcbe342596
Retrieval Hint: "viewer binding unavailable blocker code presence capability reasonCode" · "planeMailboxClient identity re-verify typed throw"
Split leaf of #16737 (S3 roster) — leg 2 of the four remaining: the identity-binding third-signal render. Sibling of #17153 (leg 1, PR #17154 in review).
Context
#16737's AC: "Three independent signals, none inferring another: presence-fresh ≠ wake-route-healthy ≠ identity-bound. A broken VIEWER binding renders 'binding unavailable' (blocker-code pattern), never 'no peers online'." Today the information survives only as redacted prose: a binding-class failure in the plane presence read throws, the adapter catches, and the capability degrades with the error text as
reason— indistinguishable BY TYPE from "plane unreachable". A renderer that wants to show "binding unavailable" distinctly would have to string-match prose, which the codebase's own precedent explicitly warns against.The Problem
planeWhoIsOnlineReader→planeMailboxClient.callTool('who_is_online')) has binding-class failure modes the CLIENT already recognizes at throw time: the reconnect identity re-verify refuses on a changed/unprovable identity (client-owned throw, no replay), and plane-side admission/ingress denials surface as tool-error text ("that text is OUR plane's service message", the client doc notes).Error— the type knowledge dies at the throw site.fleetMailboxMirrorAdapter.mjs: scoped-specific contract-text classification with an explicit anti-pattern warning — "Matching bareUnauthorizedwould misclassify any unrelated authorization failure as the named admission decision" (ADMISSION_SCOPE = 'CAN_READ_INBOX_OF').The Architectural Reality
ai/services/fleet/planeMailboxClient.mjs— owns the identity re-verify throw (knows it is binding-class at throw time) and maps tool-error text (the plane's own service messages).ai/services/fleet/planeWhoIsOnlineReader.mjs— the thin reader; stays thin.ai/services/fleet/fleetPresenceStateAdapter.mjs— catches thrown reads into the degraded envelope (capability.reason, redacted); rows gounknownwith the read reason.ai/services/fleet/fleetCockpitStatus.mjs— passes the SUPPLIED presence capability through to the cockpit DTO (verified: supplied object wins over the not-wired default).The Fix
error.planeBlockerCode = 'viewer-binding-unavailable') on the throws it ALREADY knows are binding-class — the identity re-verify refusal, and the scoped-specific admission/ingress denial texts per the mirror-adapter pattern (exact-text contract match, never bareUnauthorized). First implementation step: enumerate the client's throw sites and bind the exact contract texts (the mirror-adapter discipline).capability.reasonCodebeside the existing redactedreason; absent code ⇒ absent field (closed passthrough, no adapter-side vocabulary invention).reasonCodereaches the cockpit snapshot unchanged (expected zero-diff infleetCockpitStatus— verify by spec, not assumption).Acceptance Criteria
capability.reasonCodepassthrough — present iff the caught error carries a recognized code;reasonprose behavior unchanged; rows unchanged.reasonCodereaches the DTO intact (spec-verified).degraded+reasonCode: 'viewer-binding-unavailable'+ rows unknown (never an empty/fabricated fleet); unclassified failure → degraded WITHOUTreasonCode; the anti-misclassification case (an unrelated auth-ish error text does NOT classify).Out of Scope
C3 render-side pane states · legs 3–4 of #16737 (wake-route health, S5-gated scoping) · any change to the plane's own service messages · retrofitting typed codes onto non-presence consumers of the client (mirror adapter keeps its own established classification).
Related
#16737 (parent) · #17153 / PR #17154 (leg-1 sibling) · #16824 (scoped-empty roster — the C3-adjacent cousin) · D#16720 (source discussion).
Live latest-open sweep: latest 15 checked 2026-08-15T09:24Z (newest #17155); no equivalent. A2A in-flight sweep: recent traffic re-checked this hour; active claims (Vega #16972 · Grace #17141 · Phoebe #17147/PR #17156 · Ada #17148 · Euclid #17132-analysis) are disjoint.
Origin Session ID: 1deebbe1-b7e6-4f76-b39d-9cfcbe342596
Retrieval Hint: "viewer binding unavailable blocker code presence capability reasonCode" · "planeMailboxClient identity re-verify typed throw"