From c5e689e12eee5ae3a0ddd8f9e69806eff088fbb0 Mon Sep 17 00:00:00 2001 From: Joel Teply Date: Fri, 14 Aug 2026 16:31:17 -0500 Subject: [PATCH] =?UTF-8?q?feat(cognition):=20bind=20the=20benchmark=20boa?= =?UTF-8?q?rd=20to=20citizen=20eyes=20=E2=80=94=20BenchViewState=20reaches?= =?UTF-8?q?=20minds=20via=20the=20ViewState=20pipe=20(#426)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The renderable existed with a doctrine-citing comment ("a citizen standing in the run's room can perceive the run's state through the same pipe the human's screen uses") and was NEVER BOUND — supervisor bound only the Roster. Worse, binding alone would have read an empty store: the bench emitter published only into the websocket substrate, so the mind-side had no data to read. Citizens' only route to run state was the benchmark/runs command, whose implementation scrapes the progress dir — the exact acceptance-test failure BENCHMARKS-ARE-ADAPTERS-NOT-A-RUNNER.md names. The fix is the roster repair's one-definition-two-render-targets contract applied to the bench outlier: - ipc::global_bench_substrate() — the ONE mind-side handle. The bench board is a single global fold (unlike the per-room roster), so its handle is one substrate, not PerRoomSubstrates. - spawn_bench_emitter dual-publishes the SAME builder.session(view) revision into the websocket substrate (human eyes) and the global bench substrate (citizen minds) — a screen and a mind can never disagree about the board. - PersonaCognition gains bench_source + set_bench_source (same capture-sink decoration as roster/doctrine — deliveries recorded + replayable), pushed through THE budgeter in compose_for_turn; budget rides the generic floor_tokens arm (the renderable's own 18-token floor), no new constants. - supervisor binds ViewStateRagSource:: at persona boot. // what this catches (new test): a bound bench source delivers REAL run rows through the same compose path as every other source — if the push or setter regresses, minds go blind to the board again and only this fails. Found by the 2026-08-14 citizenship audit (AXIS 1c). Siblings tracked: #425 (retire the detached-runner auto-dispatch, Joel's timing call), #427 (L1 fork capture contamination), #428 (doc/dead-wire hygiene). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo --- core/continuum-core/src/ipc/mod.rs | 29 +++++- .../src/ipc/positron_bench_source.rs | 17 +++- core/continuum-core/src/persona/supervisor.rs | 15 +++ core/continuum-core/src/persona/unified.rs | 91 +++++++++++++++++++ 4 files changed, 148 insertions(+), 4 deletions(-) diff --git a/core/continuum-core/src/ipc/mod.rs b/core/continuum-core/src/ipc/mod.rs index a9abda06d..3294eae8d 100644 --- a/core/continuum-core/src/ipc/mod.rs +++ b/core/continuum-core/src/ipc/mod.rs @@ -147,6 +147,24 @@ pub fn global_room_substrates() -> std::sync::Arc` over it at +/// persona boot (task #426). Without this handle the bench renderable was +/// implemented and never reachable by any mind — citizens could only learn run +/// state via a command that scrapes the progress dir, which fails the doctrine's +/// acceptance test ([[benchmarks-must-be-positronic-activities-not-a-parallel-subsystem]]). +pub fn global_bench_substrate() -> continuum_positron::Substrate { + use std::sync::OnceLock; + static G: OnceLock = OnceLock::new(); + G.get_or_init(continuum_positron::Substrate::new).clone() +} pub mod room_purpose; pub mod stream_rail; pub mod vitals_emitter; @@ -3193,8 +3211,15 @@ pub fn start_server( // Benchmark board (#329): fold the run-ledger projection into // kind="bench" — the academy right-rail's live rows (who is - // solving what, attempt N/M, patch forming, verdicts). - positron_bench_source::spawn_bench_emitter(&state.rt_handle, ws_substrate.clone()); + // solving what, attempt N/M, patch forming, verdicts). Dual- + // published: the websocket substrate for human eyes AND the + // global bench substrate for citizen minds (#426) — one + // definition, two render targets, same as the roster repair. + positron_bench_source::spawn_bench_emitter( + &state.rt_handle, + ws_substrate.clone(), + global_bench_substrate(), + ); // Live-call glass box (#58): folds the TRANSPORT's calls against // the ORCHESTRATOR's registered sessions. Their disagreement is diff --git a/core/continuum-core/src/ipc/positron_bench_source.rs b/core/continuum-core/src/ipc/positron_bench_source.rs index ad4227c6c..351ae4e93 100644 --- a/core/continuum-core/src/ipc/positron_bench_source.rs +++ b/core/continuum-core/src/ipc/positron_bench_source.rs @@ -52,7 +52,18 @@ fn row_of(card: BenchRunCard) -> BenchRunRow { } /// Spawn the bench-board emitter: scan → fold → publish `kind="bench"`. -pub fn spawn_bench_emitter(rt: &tokio::runtime::Handle, substrate: Substrate) { +/// +/// Dual render targets from ONE fold (#426): `substrate` is the websocket +/// store human eyes read; `mind_substrate` is `global_bench_substrate()`, +/// the store a citizen's `ViewStateRagSource::` reads. The +/// SAME `builder.session(view)` revision lands in both, so a mind and a +/// screen can never disagree about the board — the roster repair's +/// one-definition-two-targets contract applied to the bench outlier. +pub fn spawn_bench_emitter( + rt: &tokio::runtime::Handle, + substrate: Substrate, + mind_substrate: Substrate, +) { rt.spawn(async move { // Sole writer of the "bench" kind → its own standalone Revisions well. let builder = StateBuilder::standalone(); @@ -89,7 +100,9 @@ pub fn spawn_bench_emitter(rt: &tokio::runtime::Handle, substrate: Substrate) { continue; } last = Some(view.clone()); - substrate.store(builder.session(view)); + let envelope = builder.session(view); + substrate.store(envelope.clone()); + mind_substrate.store(envelope); } }); } diff --git a/core/continuum-core/src/persona/supervisor.rs b/core/continuum-core/src/persona/supervisor.rs index 071b4971c..479f48f91 100644 --- a/core/continuum-core/src/persona/supervisor.rs +++ b/core/continuum-core/src/persona/supervisor.rs @@ -635,6 +635,21 @@ pub async fn materialize_adapters( )); cognition.set_roster_source(roster_source.clone()); + // The benchmark board, read from the SAME `BenchViewState` fold the + // academy rail renders (#426) — the second RenderTarget of the bench + // outlier. The renderable existed with a comment promising exactly + // this and was never bound; without it a citizen's only route to run + // state was a command that scrapes the progress dir, which fails the + // doctrine's acceptance test + // ([[benchmarks-must-be-positronic-activities-not-a-parallel-subsystem]]). + // The board is ONE global fold (unlike the per-room roster), so the + // handle is the global bench substrate the emitter dual-publishes into. + let bench_source: Arc = + Arc::new(crate::persona::viewstate_rag::ViewStateRagSource::< + continuum_positron::bench::BenchViewState, + >::new(crate::ipc::global_bench_substrate())); + cognition.set_bench_source(bench_source); + // Bind the room-doctrine source from the same runtime (upcasts to // `AircDoctrineReader`). Grounds the persona in the room's nature // — the airc-published operating contract. Slice 2. diff --git a/core/continuum-core/src/persona/unified.rs b/core/continuum-core/src/persona/unified.rs index 822b9f0b5..257e8187c 100644 --- a/core/continuum-core/src/persona/unified.rs +++ b/core/continuum-core/src/persona/unified.rs @@ -115,6 +115,15 @@ pub struct PersonaCognition { /// persona confabulating other citizens' turns. See /// docs/grid/AIRC-NATIVE-IDENTITY-ROOMS-SECURITY.md §5 slice 1. pub roster_source: Option>, + /// The persona's benchmark-board RAG source — the live run rows + /// (`ViewStateRagSource::` over + /// `ipc::global_bench_substrate()`), the SAME fold the academy + /// rail renders. Bound at supervisor boot (#426); `None` pre-attach + /// / in tests. This is the benchmarks-as-activity acceptance test + /// made real: a citizen perceives run state through the same pipe + /// the human's screen uses, never a file read + /// ([[benchmarks-must-be-positronic-activities-not-a-parallel-subsystem]]). + pub bench_source: Option>, /// The persona's room-doctrine RAG source — "what KIND of room is /// this" (the airc-published operating contract via /// `Airc::room_doctrine`). Bound at supervisor boot from the same @@ -214,6 +223,7 @@ impl PersonaCognition { engram_source, airc_source: None, roster_source: None, + bench_source: None, doctrine_source: None, capture_sink, } @@ -274,6 +284,19 @@ impl PersonaCognition { self.roster_source = Some(decorated); } + /// Bind the brain's benchmark-board RAG source + /// (`ViewStateRagSource::` over the global bench + /// substrate). Same boot-time wire and capture decoration as + /// `set_roster_source` — bench deliveries are recorded + replayable + /// on the same wire (task #426). + pub fn set_bench_source(&mut self, raw_source: Arc) { + let decorated: Arc = Arc::new(RecordingRagSource::new( + ArcRagSource::new(raw_source), + self.capture_sink.clone(), + )); + self.bench_source = Some(decorated); + } + /// Bind the brain's room-doctrine RAG source (`RoomDoctrineSource`). /// Same boot-time wire as `set_roster_source`, from the same `Airc` /// handle (satisfies `AircDoctrineReader`), decorated with the @@ -366,6 +389,12 @@ impl PersonaCognition { if let Some(ref doctrine) = self.doctrine_source { sources.push(doctrine.clone()); } + // The benchmark board (#426): live run rows through the same + // budgeter as everything else. Its budget rides the generic + // floor_tokens arm — a handful of run lines, never a heavyweight. + if let Some(ref bench) = self.bench_source { + sources.push(bench.clone()); + } // Per-source budget claims. The two HEAVYWEIGHT sources (engram // long-term memory + airc recent conversation) split idle @@ -847,6 +876,68 @@ mod tests { } } + /// what this catches: the #426 wiring itself. `BenchViewState` had a + /// doctrine-citing `RagRenderable` impl and ZERO bindings — citizens could + /// only learn run state from a progress-dir scrape command. This pins that + /// a bound bench source delivers REAL run rows through the SAME budgeter + /// as every other source; if the compose push or setter regresses, minds + /// go blind to the board again and only this fails. + #[tokio::test] + async fn compose_for_turn_delivers_the_benchmark_board() { + use continuum_positron::bench::{BenchRunRow, BenchViewState}; + use continuum_positron::StateBuilder; + + let id = Uuid::new_v4(); + let rag = Arc::new(RagEngine::new()); + let mut pc = PersonaCognition::new(id, "TestBot".into(), rag); + + // One run row in the mind-side substrate — the same envelope shape the + // emitter dual-publishes (a session revision of the global fold). + let substrate = continuum_positron::Substrate::new(); + substrate.store(StateBuilder::standalone().session(BenchViewState { + runs: vec![BenchRunRow { + run_id: "run-7".into(), + instance: Some("sympy__sympy-24152".into()), + solver: Some("Asha".into()), + phase: "solving".into(), + stalled: false, + attempt: Some(1), + max_attempts: Some(3), + age_secs: 42, + acts: Some(5), + patch_bytes: None, + resolved: None, + fail_to_pass: None, + pass_to_pass: None, + failed_tests: Vec::new(), + infra_error: None, + }], + sample_interval_ms: 1000, + })); + let bench: Arc = Arc::new( + crate::persona::viewstate_rag::ViewStateRagSource::::new(substrate), + ); + pc.set_bench_source(bench); + + let composed = pc.compose_for_turn(&lcd_profile(), 1_000_000, None).await; + let bench_delivery = composed + .deliveries + .iter() + .find(|d| d.source_id == "bench") + .expect("bench board must be composed alongside the other sources"); + let rendered = bench_delivery + .items + .iter() + .map(|i| i.content.clone()) + .collect::>() + .join("\n"); + assert!(rendered.contains("run-7"), "run row missing: {rendered}"); + assert!( + rendered.contains("sympy__sympy-24152"), + "instance missing: {rendered}" + ); + } + /// The brain's capture sink records the TurnStart / BudgetAllocated /// / TurnEnd events the substrate-replay pipeline expects. Proves /// compose_for_turn participates in the same capture/replay loop