diff --git a/crates/spt-daemon/src/brain.rs b/crates/spt-daemon/src/brain.rs index 54a2b022..1169dfbf 100644 --- a/crates/spt-daemon/src/brain.rs +++ b/crates/spt-daemon/src/brain.rs @@ -1646,6 +1646,24 @@ impl Brain { } + /// DIAGNOSTIC releases#302: query existing reactor gauges on a bounded + /// carrier. Older brokers return an error, never a fabricated healthy sample. + // [impl->REQ-RC-HITCH-DISCRIMINATOR] + pub fn net_reactor_diag(&mut self) -> io::Result { + refuse_unbounded_carrier(self, "reactor diagnostics")?; + self.send(crate::msg::KIND_NET_REACTOR_DIAG, serde_json::Value::Null)?; + let deadline = self.call_deadline(); + loop { + match self.read_event_until(deadline)? { + BrokerEvent::Other(env) if env.kind == crate::msg::KIND_NET_REACTOR_DIAG_REPLY => { + return Ok(env.payload); + } + BrokerEvent::Error { message } => return Err(io::Error::other(message)), + _ => continue, + } + } + } + /// Query the broker-owned net endpoint's status (D4a): node id, dialable /// address, conn count — or `enabled: false` on a net-less broker. Reads /// until the reply (consuming interleaved events like [`Brain::spawn_session`]). diff --git a/crates/spt-daemon/src/broker.rs b/crates/spt-daemon/src/broker.rs index bafb8eff..951b19b1 100644 --- a/crates/spt-daemon/src/broker.rs +++ b/crates/spt-daemon/src/broker.rs @@ -6139,6 +6139,7 @@ impl Broker { } } KIND_NET_STATUS => self.dispatch_net_status(&send), + crate::msg::KIND_NET_REACTOR_DIAG => self.dispatch_net_reactor_diag(&send), // [impl->REQ-UPDATE-RUNNING-IMAGE-SURFACE] KIND_BROKER_IMAGE => self.dispatch_broker_image(&send), // [impl->REQ-UPDATE-RUNNING-IMAGE-SURFACE] @@ -9242,6 +9243,23 @@ impl Broker { } } + // DIAGNOSTIC releases#302: sampling must not queue behind the suspect runtime, + // sessions, connection table, or output logs. No diagnosis is inferred here. + // [impl->REQ-RC-HITCH-DISCRIMINATOR] + fn dispatch_net_reactor_diag(&self, send: &SharedSend) { + let host = self.net.get(); + let reply = serde_json::json!({ + "diagnostic": "rc-hitch", + "broker_pid": std::process::id(), + "broker_version": env!("CARGO_PKG_VERSION"), + "sampled_at_ms": crate::brain::now_ms(), + "net_enabled": host.is_some(), + "net_canary_age_ms": host.map(|h| h.net_canary_age_ms()), + "active_dial_tasks": host.map(|h| h.active_dial_tasks()), + }); + send_frame(send, &Envelope::new(crate::msg::KIND_NET_REACTOR_DIAG_REPLY, reply)); + } + /// Report the broker-owned net endpoint's status (D4a). Answered even when /// the broker has no network host (`enabled: false`) so a brain can probe /// capability without treating absence as an error. diff --git a/crates/spt-daemon/src/msg.rs b/crates/spt-daemon/src/msg.rs index bc93f60f..b8dbcb97 100644 --- a/crates/spt-daemon/src/msg.rs +++ b/crates/spt-daemon/src/msg.rs @@ -112,6 +112,9 @@ pub const KIND_ERROR: &str = "error"; pub const KIND_NET_STATUS: &str = "net-status"; /// Broker→brain: the net endpoint status reply. pub const KIND_NET_STATUS_REPLY: &str = "net-status-reply"; +/// Opt-in releases#302 field diagnostic; no runtime scheduling or session locks. +pub const KIND_NET_REACTOR_DIAG: &str = "net-reactor-diag"; +pub const KIND_NET_REACTOR_DIAG_REPLY: &str = "net-reactor-diag-reply"; /// Brain→broker: report the broker's OWN compiled image and process identity. /// The broker SURVIVES `spt update apply` (only the brain restarts — ADR-0018 D3-3), /// so a freshly-applied node may still be RUNNING an older broker binary; the diff --git a/crates/spt/src/cli.rs b/crates/spt/src/cli.rs index 01043d63..1806c2ec 100644 --- a/crates/spt/src/cli.rs +++ b/crates/spt/src/cli.rs @@ -8652,7 +8652,33 @@ fn render_daemon_status_lines( out } +// DIAGNOSTIC releases#302: opt-in standalone snapshot, not the ordinary status +// gather (which asks other subsystems and could outlast the freeze being sampled). +// [impl->REQ-RC-HITCH-DISCRIMINATOR] +fn cmd_rc_hitch_diag() -> i32 { + let started = std::time::Instant::now(); + let result = spt_daemon::brain::Brain::cold_start_pump( + &spt_daemon::broker_socket_name(), + now_ms(), + Duration::from_secs(2), + spt_daemon::brain::PumpTrace::Silent, + ).and_then(|mut brain| brain.net_reactor_diag()); + let (mut sample, code) = match result { + Ok(sample) => (sample, 0), + Err(error) => (serde_json::json!({ + "diagnostic": "rc-hitch", + "probe_error": error.to_string(), + }), 1), + }; + sample["query_elapsed_ms"] = serde_json::json!(started.elapsed().as_millis()); + print_json(&sample); + code +} + fn cmd_daemon_status(json: bool) -> i32 { + if std::env::var("SPT_RC_HITCH_DIAG").as_deref() == Ok("1") { + return cmd_rc_hitch_diag(); + } // [impl->REQ-STATUS-LIVE-SUPERVISOR-PID] Gather once for both views. // The split carrier bounds every reply wait, including older peers that // do not answer an image query. No PID file participates in liveness. diff --git a/traceable-reqs.toml b/traceable-reqs.toml index ae473abe..05aa5f14 100644 --- a/traceable-reqs.toml +++ b/traceable-reqs.toml @@ -7639,3 +7639,8 @@ required_stages = [] # NOT ACTIVATED -- doyle 2026-09-10, registry-first per th id = "REQ-INPUT-PROVENANCE-ACCEPTANCE-REPORT" title = "A REMOTE SUBMITTER'S QUOTED ABSOLUTE OR ~-ROOTED PATHS ARE SERVED FROM THE SUBMITTER'S MACHINE, BOUND BY CORE ALONE AT RECEIPT OF THE EXISTING USER_INPUT REPORT -- NO TOKEN, NO DECLARATION, NOTHING ASKED OF ADAPTERS (releases#300; operator-ruled 2026-09-13; INPUT-PROVENANCE-CONTRACT section 8; ADR-0058 Amendment 2). NO QUOTED PATHS means no broker IPC and no line. For a path-bearing report, core binds origin to the session's current authenticated REMOTE controller seat, the broker-owned driven_by. An absent hosted session, local or viewer-only seat, or no controller binds nothing SILENTLY. OWN-INJECTION EXCLUSION: a report matching a peer delivery core physically wrote into that session's PTY binds no origin, comparing exact bytes OR both byte sequences trimmed of ASCII whitespace at their ends, never payload-shape parsing or interior normalization. Keep fixed-size streaming evidence, physical-write publication order, and fail-closed unproven/capacity handling. Resumed-session and external automation remain uncharacterised and bind the seat per the submitter rule. Core-side dedup: a repeated payload in the same session while its reference is live re-registers nothing, emits no second helper notice, extends no TTL. Serving uses the submitter's machine, bounded 24h TTL, audience the receiving endpoint, and one helper notice per submission and path. Missing files are silently skipped, with no prompt or fallback origin. CLI broker-connect and unanswered-receipt failures are silent unless SPT_PUMP_TRACE enables diagnostics; named declines apply only to failures with a remote controller, including the broker's 10-second owner-reply timeout. STATED CEILING, not an adapter promise: reports lag acceptance by the harness's own path (historical claude-spt measurement 0.5-1.0 s), and a cross-operator Take inside that lag attributes to the new holder. Gate: doc -- section 8 and ADR-0058 Am.2; impl -- receipt-time binding, physical-delivery exclusion, dedup, existing live-reference serving; unit -- remote binding survives Take, local/viewer/none/absent session bind nothing silently, no paths never call receipt transport, exact and ASCII-edge-trimmed core-written deliveries bind nothing without interior normalization, repeated payloads cannot renew TTL or repeat notices, missing files emit no line. The separately admitted two-node field leg is PENDING: remote controller's quoted path serves only to the receiving agent; local input does not serve; with a remote controller seated, a path-bearing peer message physically delivered by core must cause NO SERVE on its input report; measure added hook cost. No execution or hook-cost result is claimed and no adapter integration work is required." required_stages = ["doc", "impl", "unit"] # releases#300 core-only lane; int remains deferred until the separately admitted field leg, including remote-seated physical peer-delivery exclusion and added hook-cost measurement. + +[[requirements]] +id = "REQ-RC-HITCH-DISCRIMINATOR" +title = "releases#302 diagnostic-only: an explicitly opted-in daemon-status probe reads the live broker's existing net_canary_age_ms and active_dial_tasks atomics on a fresh bounded IPC connection, without session/output-log locks or work scheduled on the net runtime. Report broker sample time and client elapsed time; absent net hosts and unsupported or unresponsive brokers must not appear healthy. Ordinary status is unchanged. This instruments the field discriminator, not a remedy or a claim that the rc hitch is attributed." +required_stages = ["impl"] # Instrumentation only; no bug-fix or field acceptance stage claimed.