todlando -> doyle. #49/#267 OBSERVABLES IMPLEMENTED. Worktree .worktrees/49-267-obs (detached at main ba993b7a). NOT COMPILED - see verification needs. FILES: crates/spt-daemon/src/brain.rs (+19) | brainproc.rs (+36) | broker.rs (+83/-29) | docs/49-267-REFRESH-OBSERVATION-PLAN.md (+55) | traceable-reqs.toml (+6) GATE: brain::diag_trace_enabled() - the EXISTING SPT_PUMP_TRACE=1 gate, read ONCE via OnceLock (these sit on control-plane hot edges; an env lookup per closure would be a cost the diagnosis imposed on its own subject). No second switch. The daemon reads its own env and still exports nothing. Q1 - CONTROLLER SLOT CLOSURE. clear_controller now takes cause: &'static str and emits INSIDE the taken branch only: - fn clear_controller(&mut self) { + /// + /// `cause` is the CALLER'S OWN knowledge of which edge closed the slot, and it + /// is a parameter for the reason releases#49/#267 needed this line at all: at + /// this site a writer that exited, an evicted wedge and a deliberate detach are + /// indistinguishable, so a cause DERIVED here would be a guess wearing a label. + /// Each caller names what it knows; nothing infers. + // [impl->REQ-REFRESH-WAIT-ATTRIBUTION] + fn clear_controller(&mut self, cause: &'static str) { let prev_gen = self.controller.as_ref().map(|c| c.attach_gen); + // Captured BEFORE the take: after it there is no sink left to ask. + let conn = self.controller.as_ref().map(|c| c.send.id()); if self.controller.take().is_some() { + // ONLY ON THE REAL TRANSITION (releases#49/#267 Q1). The epoch-guarded + // no-op above returns without taking anything, and a line there would + // report a closure that did not happen — which is exactly the confusion + // the question exists to remove: today a clean close and a slot that was + // already empty leave identical stderr. + // [impl->REQ-REFRESH-WAIT-ATTRIBUTION] + if crate::brain::diag_trace_enabled() { + spt_proto::emit_line_err!( + "CONTROLLER_SLOT_CLOSED:{} conn={} cause={cause} {} — the controller slot \ + was occupied and is now empty [REQ-REFRESH-WAIT-ATTRIBUTION]", + self.session_id, + conn.unwrap_or(0), + crate::conn::log_stamp() + ); + } self.stamp_driven_by(); // Detach is a drop edge: the posture goes offline (derived from the // stamp just written) and the empowerments go with it. @@ -2784,7 +2823,7 @@ impl OutputLog { The three callers pass what THEY know, as associated consts: CLOSE_WRITER_GONE (mark_controller_gone, the liveness reap - this is Q1's edge), CLOSE_STALL_EVICT (stall_evict_controller, StampMode::Inline), CLOSE_DETACH (detach_if). Nothing is inferred at the shared site. The epoch-guarded no-op emits NOTHING, so the event means a slot that was occupied is now empty - never that something tried. NOTE, deliberately left alone: StampMode::Deferred clears the slot directly (self.controller = None) and does NOT route through clear_controller, so it emits no closure line; BRAIN_SUBSCRIBER_STALL_EVICT already names that path and I did not widen the change to reroute it. Q3 - PROMOTION GATE. run_trial takes ONE read and uses it twice: + if ready_seen { + // ONE observation, read once and used twice — the promote decision and + // the diagnostic line are the SAME read. A second `old_gen_drained()` + // call to describe what the first one decided could disagree with it, + // and a diagnostic that can contradict the branch it reports on is + // worse than no diagnostic. + let drained = env.old_gen_drained(); + if drained { + return TrialStep::Promoted; + } + // THE NARROWER FACT, DELIBERATELY. The gate answers one bool over every + // hosted session; naming WHICH session is wedged would need a second, + // separately-taken scan, so this reports what this observation actually + // established — that the gate was EVALUATED and read not-drained — and + // nothing about which conn held it. It is also not evidence that the + // heartbeat's drain-driving `brain.sessions()` call completed: this says + // the gate was consulted, never that the reap ran. + // [impl->REQ-REFRESH-WAIT-ATTRIBUTION] + if !not_drained_reported { + not_drained_reported = true; + if crate::brain::diag_trace_enabled() { + spt_proto::emit_line_err!( + "BRAIN_PROMOTE_GATE_NOT_DRAINED: generation {generation} — ready latched \ + and the DRAINED gate read NOT drained on its first evaluation after \ + ready; the trial keeps polling to its window \ + [REQ-REFRESH-WAIT-ATTRIBUTION]" + ); + } + } } Once per trial, correlated to the generation, from the same observation the promote decision uses - no second scan. It reports the NARROWER FACT and names no session, because naming one needs a separately-taken scan that could disagree with the read that decided the branch. Your distinction is in the code comment AND in the doc: it establishes the gate was EVALUATED, never that the heartbeat's drain-driving brain.sessions() call completed. EVIDENCE: REQ-REFRESH-WAIT-ATTRIBUTION minted and ACTIVATED at doc+impl (mirroring REQ-ROSTER-WAIT-ATTRIBUTION's shape), added to the endpoint-presence-truth group. doc = the plan's new section 4.1, which states what each event does and does not establish and keeps the gate-evaluated vs reap-completed distinction in the investigation record; section 8's Q1/Q3 entries are CORRECTED BY REPLACEMENT rather than annotated - both now read OBSERVABLE, STILL UNANSWERED, because a run answers them, not this change. NO unit stage, and the reason is on the row: both sites are stderr emissions on branches whose behaviour is unchanged, so a unit test would assert that a formatter formats. TARGETED VERIFICATION NEEDS, smallest first: 1. cargo check -p spt-daemon in this worktree under its own claimed pool - type-level only, no suite, no link. This is the one I actually need: I have written Rust I cannot compile, including an associated-const reference I already had to qualify as Self:: by inspection. 2. traceable-reqs check (the CI-pinned version) for the new row and its tags. 3. NO behavioural test proposed: the honest evidence for an observability claim is the field observation it exists to take, which is the #49/#267 run - not a test that the line renders. #299 build still compiling in .worktrees/299-instr (pool claimed, jobs=2). Nothing else of mine is running.