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"); +}