From b263f0a8561dc57a7a2e4fe38503779396af6cc3 Mon Sep 17 00:00:00 2001 From: Joel Teply Date: Mon, 26 Oct 2026 19:39:55 -0500 Subject: [PATCH] The placement switch asks the seat she routes to whether it takes her learned tool head Every resident is bound through PlacementSwitch, which forwards each capability to current() but not accepts_tail_tools, so it answered the trait default (false) and no resident's learned tool head was ever sent, even on the engine that takes tail_tools (M5 2026-10-27: delib.tool_head.epoch sent=false, Aiko head 5,047 tokens of which 4,514 never called, engine 1c6681593 serving). #5491's tests checked the engine adapter directly, never through the switch. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo --- .../src/persona/placement_switch/adapter.rs | 8 ++++ .../src/persona/placement_switch/tests.rs | 40 +++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/core/continuum-mind/src/persona/placement_switch/adapter.rs b/core/continuum-mind/src/persona/placement_switch/adapter.rs index f6ac25cf1e..d763bb2002 100644 --- a/core/continuum-mind/src/persona/placement_switch/adapter.rs +++ b/core/continuum-mind/src/persona/placement_switch/adapter.rs @@ -526,6 +526,14 @@ impl AIProviderAdapter for PlacementSwitch { fn live_served_window(&self) -> Option { self.current().live_served_window() } + /// Asked of the adapter her request actually goes to, like every other capability here: a + /// home lane on our fork's gateway may take her learned tool head, a remote seat may not. + /// Left to the trait default, every resident read `false` behind this switch, so the head + /// stayed a shadow on the M5 after #5491 deployed (`delib.tool_head.epoch sent=false`, + /// Aiko 2026-10-27 00:2xZ, engine 1c6681593 serving). + fn accepts_tail_tools(&self, model: &str) -> bool { + self.current().accepts_tail_tools(model) + } /// Her home model: the request carries the lane's model on the wire when remote. fn default_model(&self) -> &str { &self.home_model diff --git a/core/continuum-mind/src/persona/placement_switch/tests.rs b/core/continuum-mind/src/persona/placement_switch/tests.rs index 56fee6789e..7a65eac6fa 100644 --- a/core/continuum-mind/src/persona/placement_switch/tests.rs +++ b/core/continuum-mind/src/persona/placement_switch/tests.rs @@ -738,3 +738,43 @@ async fn capacity_park_resumes_when_local_1_5b_routes_to_larger_remote_seat() { .expect("the actual forwarding target changed before the waiter was first polled"); assert_eq!(binding.load().requested_model(), "local-1.5B"); } + +// A home lane on the fork's gateway: it takes a learned tool head. +struct TailLane; + +#[async_trait::async_trait] +impl AIProviderAdapter for TailLane { + fn provider_id(&self) -> &str { + "tail-lane-fixture" + } + fn name(&self) -> &str { + "tail lane" + } + fn default_model(&self) -> &str { + "fixture" + } + fn accepts_tail_tools(&self, _model: &str) -> bool { + true + } + async fn generate_text( + &self, + _request: TextGenerationRequest, + ) -> Result { + Err("capability fixture must not generate".into()) + } +} + +// what this catches: the switch every resident is bound through answering the trait default +// (`false`) for `accepts_tail_tools`, so no resident's learned tool head is ever sent even +// when her home lane takes it (M5 2026-10-27, after #5491 deployed). The answer must follow +// the seat: true at a home lane that takes tails, false once she routes to a remote seat. +#[tokio::test] +async fn the_switch_asks_the_seat_she_routes_to_whether_it_takes_her_tool_head() { + let now = continuum_base::time::EpochMs::now().millis(); + let home: Arc = Arc::new(TailLane); + let factory = Arc::new(ScriptedPersonaAdapterFactory::custom(move |_| Ok(home.clone()))); + let route = Arc::new(switch(factory, remote())); + assert!(!route.accepts_tail_tools("fixture"), "routed to the remote seat, no tail"); + route.apply(&PlacementMove::FallHome { reason: "tail fixture" }, now, SeatFit::default()).await; + assert!(route.accepts_tail_tools("fixture"), "her home lane takes the tail"); +}