Filing the Tier-2 contract from #6 as a durable artifact rather than leaving it in a closed thread. Phase 1 is merged (#61); this is the gate that must pass before the daemon sets bit 6, and it is not started — so this is a settled contract awaiting build, not an implementation ask.
Why a probe is needed at all
Phase 2 has one hard precondition: every transport-direct consumer must already tolerate flag bit 6. Five modules decode subc frames without the SDK — astrocyte, broca, cerebellum, claustrum, insula — and none of them owns a decoder. subc-protocol is a path dependency for all five, so Phase-1 adoption per module is rebuild against a sibling containing Phase 1, redeploy, restart. There are no consumer-repo PRs to track.
That is what makes deploy chronology useless as a gate. A rebuild at the boundary can produce a binary that looks deployed and still hard-fails, because neither the repo nor the binary carries readable evidence of which sibling revision it compiled against. "Phase 1 merged" is not a go-signal; a go-signal must carry the sibling SHA containing Phase 1, since at least one consumer pins by SHA and resolves at build time.
The trap: the obvious probe severs the connection
The natural probe — send one frame with bit 6 set, see if it comes back ReservedFlagBits — is not a passive read. In the Rust SDK, read_frame propagates DecodeHeader errors and the client loop bubbles them with ? (see the frame-read arm in subc-client-rs's client loop, adjacent to the FrameType::Goodbye handling). A rejected header does not produce an error response; it kills the module's connection.
So a naive sweep of N unverified modules disconnects every module that fails — which is precisely the population the sweep exists to find. The probe would damage exactly the modules it identifies.
Two tiers
Tier 1 — free, non-destructive, no new mechanism. Once #59 lands, supervisor.provenance serves each module's declared build_git_sha. Check whether that SHA's ancestry contains the Phase-1 commit. This answers the question for every module that declares provenance, at zero risk.
Caveat, correctly placed: the declaration is module-authored and untrusted — the daemon serves the claim without vouching for it, and the operator's tooling judges it. That is adequate for a go/no-go, because a module lying about its build SHA fails at Tier 2, not in production.
Tier 2 — behavioural, only for modules Tier 1 cannot answer. A module that declares nothing is unverifiable, and no amount of reading closes that. Only a frame the deployed binary actually decodes proves the deployed binary decodes it.
The four properties Tier 2 must satisfy
- Non-destructive by construction. The probe must not rely on a rejected decode to learn the answer, since rejection severs the connection. Prefer a
ModuleControlPush carrying bit 6 — unknown push ops are delivered and must-ignored, so a conformant module absorbs it silently while a non-conformant one reveals itself by how it fails, not by being killed.
- Three-state result, each rendered distinctly:
decoded-and-ignored / dropped / no-answer. No answer is not an answer — a silent module must never render as a pass. (This is the third absence-read-as-answer case in a week, alongside drain notifications and epoch-drop records.)
- Bounded and operator-initiated. No implicit sweep on a timer that could walk the fleet.
- Probe outcomes MUST NOT consume a module's crash/restart budget. (Maintainer's addition, non-negotiable — and the one I missed.) Supervised modules exit on terminal connection state so the daemon owns resurrection, which means an induced disconnect is a counted respawn. Without an exemption, a Tier-2 sweep of N unverifiable modules walks each one toward its disable threshold — the same class of failure that took
aft down under health-probe misses, with a different trigger. The probe needs the same exemption operator-initiated restarts already have.
Sequencing
Phase 2 emission waits on a Tier-1 census plus scheduled Tier-2 for stragglers, and the emission change ships as a separate reviewed PR with the census attached. Two scheduling facts from the consumer seats: one module runs a 4-hour rotation whose capture series a mid-latch restart would confound, so the boundary should avoid it; another is ready to rebuild on demand and its flag-decode path is byte-identical to shared head today, making Phase 1 a genuine delta there.
Not proposing an implementation here
Whether the daemon should own a probe verb at all is open. It is the only component that holds both the module connection and the supervisor state, which argues for it — but it also means the daemon would be probing its own children, and property 4 exists because that relationship has teeth. Happy to take this once #59 merges and the Tier-1 census is real.
Filing the Tier-2 contract from #6 as a durable artifact rather than leaving it in a closed thread. Phase 1 is merged (#61); this is the gate that must pass before the daemon sets bit 6, and it is not started — so this is a settled contract awaiting build, not an implementation ask.
Why a probe is needed at all
Phase 2 has one hard precondition: every transport-direct consumer must already tolerate flag bit 6. Five modules decode subc frames without the SDK —
astrocyte,broca,cerebellum,claustrum,insula— and none of them owns a decoder.subc-protocolis a path dependency for all five, so Phase-1 adoption per module is rebuild against a sibling containing Phase 1, redeploy, restart. There are no consumer-repo PRs to track.That is what makes deploy chronology useless as a gate. A rebuild at the boundary can produce a binary that looks deployed and still hard-fails, because neither the repo nor the binary carries readable evidence of which sibling revision it compiled against. "Phase 1 merged" is not a go-signal; a go-signal must carry the sibling SHA containing Phase 1, since at least one consumer pins by SHA and resolves at build time.
The trap: the obvious probe severs the connection
The natural probe — send one frame with bit 6 set, see if it comes back
ReservedFlagBits— is not a passive read. In the Rust SDK,read_framepropagatesDecodeHeadererrors and the client loop bubbles them with?(see the frame-read arm insubc-client-rs's client loop, adjacent to theFrameType::Goodbyehandling). A rejected header does not produce an error response; it kills the module's connection.So a naive sweep of N unverified modules disconnects every module that fails — which is precisely the population the sweep exists to find. The probe would damage exactly the modules it identifies.
Two tiers
Tier 1 — free, non-destructive, no new mechanism. Once #59 lands,
supervisor.provenanceserves each module's declaredbuild_git_sha. Check whether that SHA's ancestry contains the Phase-1 commit. This answers the question for every module that declares provenance, at zero risk.Caveat, correctly placed: the declaration is module-authored and untrusted — the daemon serves the claim without vouching for it, and the operator's tooling judges it. That is adequate for a go/no-go, because a module lying about its build SHA fails at Tier 2, not in production.
Tier 2 — behavioural, only for modules Tier 1 cannot answer. A module that declares nothing is
unverifiable, and no amount of reading closes that. Only a frame the deployed binary actually decodes proves the deployed binary decodes it.The four properties Tier 2 must satisfy
ModuleControlPushcarrying bit 6 — unknown push ops are delivered and must-ignored, so a conformant module absorbs it silently while a non-conformant one reveals itself by how it fails, not by being killed.decoded-and-ignored/dropped/no-answer. No answer is not an answer — a silent module must never render as a pass. (This is the third absence-read-as-answer case in a week, alongside drain notifications and epoch-drop records.)aftdown under health-probe misses, with a different trigger. The probe needs the same exemption operator-initiated restarts already have.Sequencing
Phase 2 emission waits on a Tier-1 census plus scheduled Tier-2 for stragglers, and the emission change ships as a separate reviewed PR with the census attached. Two scheduling facts from the consumer seats: one module runs a 4-hour rotation whose capture series a mid-latch restart would confound, so the boundary should avoid it; another is ready to rebuild on demand and its flag-decode path is byte-identical to shared head today, making Phase 1 a genuine delta there.
Not proposing an implementation here
Whether the daemon should own a probe verb at all is open. It is the only component that holds both the module connection and the supervisor state, which argues for it — but it also means the daemon would be probing its own children, and property 4 exists because that relationship has teeth. Happy to take this once #59 merges and the Tier-1 census is real.