From a9949ccb9ea9e4edeae1127b8c47b3acabc50b3e Mon Sep 17 00:00:00 2001 From: "Andrei G." Date: Mon, 18 May 2026 18:24:10 +0200 Subject: [PATCH 1/5] feat(memory,subagent): add A* node cap and sub-agent fleet registration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add MAX_GRAPH_NODES (500) constant to zeph-memory graph retrieval: node_map is truncated to the highest-scored nodes before the A* inner loop, bounding worst-case complexity from O(seeds × n²) to O(seeds × n log n). Closes #4368. Introduce FleetRegistry trait and SqliteFleetRegistry adapter in zeph-subagent. SubAgentManager now calls register_active on spawn and mark_terminal on completion or cancellation, making sub-agents visible in the fleet dashboard. cancel_all also marks sessions as cancelled during shutdown. Closes #4370. --- CHANGELOG.md | 9 + .../zeph-memory/src/graph/retrieval_astar.rs | 73 +++++ crates/zeph-subagent/src/fleet.rs | 68 +++++ crates/zeph-subagent/src/lib.rs | 2 + crates/zeph-subagent/src/manager.rs | 260 +++++++++++++++++- src/fleet_session.rs | 65 +++++ src/runner.rs | 7 + 7 files changed, 478 insertions(+), 6 deletions(-) create mode 100644 crates/zeph-subagent/src/fleet.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f9825c08..8aaba11cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +### Added + +- `zeph-memory`: `MAX_GRAPH_NODES` constant (500) added to A* graph retrieval; `node_map` is + truncated to the highest-scored 500 nodes before the inner loop, bounding worst-case complexity + from O(n²) per seed to O(n log n) (closes #4368). +- `zeph-subagent`: `FleetRegistry` trait and `SqliteFleetRegistry` adapter; sub-agents spawned by + `SubAgentManager` are now registered in the fleet `agent_sessions` table and visible in the fleet + dashboard. `cancel_all` marks all active sub-agent sessions as cancelled on shutdown (closes #4370). + ## [0.21.2] - 2026-05-18 ### Added diff --git a/crates/zeph-memory/src/graph/retrieval_astar.rs b/crates/zeph-memory/src/graph/retrieval_astar.rs index 2074b1e89..5049ac04e 100644 --- a/crates/zeph-memory/src/graph/retrieval_astar.rs +++ b/crates/zeph-memory/src/graph/retrieval_astar.rs @@ -21,6 +21,13 @@ use crate::graph::types::{EdgeType, GraphFact}; const ENTITY_COLLECTION: &str = "zeph_graph_entities"; +/// Maximum number of graph nodes passed to the A* traversal loop. +/// +/// Without a cap the A* inner loop is O(|nodes|²) per seed, which becomes +/// prohibitively slow on large or highly-connected graphs. Nodes are ranked by +/// their seed score before truncation so only the least-relevant nodes are dropped. +const MAX_GRAPH_NODES: usize = 500; + /// Cosine similarity of two equal-length slices. Returns `0.0` when either norm is zero. fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 { let dot: f32 = a.iter().zip(b.iter()).map(|(&x, &y)| x * y).sum(); @@ -36,6 +43,35 @@ const DEFAULT_COMMUNITY_CAP: usize = 3; /// Query embedding paired with per-entity embedding map, produced by the PRISM path. type PrismEmbeddings = Option<(Vec, HashMap>)>; +/// Truncate `node_map` in-place to at most `cap` entries, keeping those with the highest score. +/// +/// Score is looked up from `entity_scores`; entities absent from that map receive `0.0`. +/// When `node_map.len() <= cap` the map is unchanged. +pub(crate) fn cap_node_map( + node_map: &mut HashMap, + entity_scores: &HashMap, + cap: usize, +) { + if node_map.len() <= cap { + return; + } + let mut scored: Vec<(i64, f32)> = node_map + .keys() + .map(|&id| { + let s = entity_scores.get(&id).copied().unwrap_or(0.0); + (id, s) + }) + .collect(); + scored.sort_unstable_by(|a, b| b.1.total_cmp(&a.1)); + scored.truncate(cap); + let keep: HashSet = scored.into_iter().map(|(id, _)| id).collect(); + node_map.retain(|id, _| keep.contains(id)); + tracing::debug!( + retained = node_map.len(), + "graph_recall_astar: node_map capped" + ); +} + /// Retrieve graph facts using A* shortest-path traversal. /// /// Algorithm: @@ -251,6 +287,9 @@ pub async fn graph_recall_astar( graph.add_edge(src, tgt, cost); } + // Cap node_map to the highest-scored nodes to bound A* time complexity. + cap_node_map(&mut node_map, &entity_scores, MAX_GRAPH_NODES); + // Run A* from each seed; collect path node pairs. let mut path_pairs: HashSet<(NodeIndex, NodeIndex)> = HashSet::new(); @@ -549,4 +588,38 @@ mod tests { assert!(!result.is_empty()); assert_eq!(result[0].entity_name, "alice"); } + + #[test] + fn cap_node_map_truncates_to_cap_keeping_highest_scored() { + use petgraph::graph::NodeIndex; + + let mut node_map: HashMap = (0..600i64) + .map(|id| (id, NodeIndex::new(id as usize))) + .collect(); + // Assign scores: entity 599 gets 1.0, 598 gets 0.999, ..., 0 gets ~0.0. + let entity_scores: HashMap = + (0..600i64).map(|id| (id, id as f32 / 599.0)).collect(); + + cap_node_map(&mut node_map, &entity_scores, MAX_GRAPH_NODES); + + assert_eq!(node_map.len(), MAX_GRAPH_NODES); + // The 500 retained nodes must all have id >= 100 (top 500 by score are ids 100..=599). + for &id in node_map.keys() { + assert!(id >= 100, "low-scored node {id} should have been dropped"); + } + } + + #[test] + fn cap_node_map_noop_when_within_limit() { + use petgraph::graph::NodeIndex; + + let mut node_map: HashMap = (0..10i64) + .map(|id| (id, NodeIndex::new(id as usize))) + .collect(); + let entity_scores: HashMap = HashMap::new(); + + cap_node_map(&mut node_map, &entity_scores, MAX_GRAPH_NODES); + + assert_eq!(node_map.len(), 10); + } } diff --git a/crates/zeph-subagent/src/fleet.rs b/crates/zeph-subagent/src/fleet.rs new file mode 100644 index 000000000..c1f5b4366 --- /dev/null +++ b/crates/zeph-subagent/src/fleet.rs @@ -0,0 +1,68 @@ +// SPDX-FileCopyrightText: 2026 Andrei G +// SPDX-License-Identifier: MIT OR Apache-2.0 + +//! Fleet registry abstraction for sub-agent session tracking. +//! +//! [`FleetRegistry`] is a narrow trait that decouples `zeph-subagent` from the +//! `zeph-memory` `SqliteStore`. The concrete implementation lives in `zeph-core` +//! and is injected via [`SubAgentManager::set_fleet_registry`]. + +use std::future::Future; +use std::pin::Pin; +use std::sync::Arc; + +/// Terminal lifecycle status of an agent session visible in the fleet dashboard. +/// +/// Only terminal states are represented because [`FleetRegistry::mark_terminal`] is only +/// called when a session ends. Active sessions are registered via +/// [`FleetRegistry::register_active`]. +/// +/// Mirrors the terminal variants of `zeph_memory::SessionStatus` without creating a +/// dependency on that crate. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FleetSessionStatus { + /// Session ended normally. + Completed, + /// Session ended due to an unrecoverable error. + Failed, + /// Session was cancelled by the user. + Cancelled, +} + +/// Minimal data needed to register a sub-agent session in the fleet dashboard. +#[derive(Debug, Clone)] +pub struct FleetSessionInfo { + /// Stable session ID (UUID string). + pub id: String, + /// Human-readable agent name (from the definition). + pub agent_name: String, + /// ISO-8601 UTC timestamp of session start. + pub started_at: String, +} + +/// Trait that abstracts fleet session persistence for `SubAgentManager`. +/// +/// Implementors must be `Send + Sync` and handle their own internal error recovery; +/// the manager logs failures at `warn` level but never propagates them to the caller. +pub trait FleetRegistry: Send + Sync { + /// Register or update a sub-agent session as active. + /// + /// Called once at sub-agent spawn time. Fire-and-forget: errors are logged + /// by the manager and do not abort the spawn. + fn register_active<'a>( + &'a self, + info: &'a FleetSessionInfo, + ) -> Pin> + Send + 'a>>; + + /// Mark a sub-agent session as terminal. + /// + /// Called when a sub-agent finishes (collect) or is cancelled. + fn mark_terminal<'a>( + &'a self, + session_id: &'a str, + status: FleetSessionStatus, + ) -> Pin> + Send + 'a>>; +} + +/// Shared, type-erased fleet registry injected into the manager. +pub type SharedFleetRegistry = Arc; diff --git a/crates/zeph-subagent/src/lib.rs b/crates/zeph-subagent/src/lib.rs index bf075e08c..648a45e2a 100644 --- a/crates/zeph-subagent/src/lib.rs +++ b/crates/zeph-subagent/src/lib.rs @@ -43,6 +43,7 @@ pub mod command; pub mod def; pub mod error; pub mod filter; +pub mod fleet; pub mod grants; pub mod hooks; pub mod manager; @@ -58,6 +59,7 @@ pub use def::{ }; pub use error::SubAgentError; pub use filter::{FilteredToolExecutor, PlanModeExecutor, filter_skills}; +pub use fleet::{FleetRegistry, FleetSessionInfo, FleetSessionStatus, SharedFleetRegistry}; pub use grants::{Grant, GrantKind, PermissionGrants, SecretRequest}; pub use hooks::{ HookAction, HookDef, HookError, HookMatcher, HookOutput, HookRunResult, McpDispatch, diff --git a/crates/zeph-subagent/src/manager.rs b/crates/zeph-subagent/src/manager.rs index 76b1fbfd0..53e67c9ea 100644 --- a/crates/zeph-subagent/src/manager.rs +++ b/crates/zeph-subagent/src/manager.rs @@ -21,6 +21,7 @@ use zeph_tools::executor::{ErasedToolExecutor, ToolError, ToolOutput}; use zeph_config::{ContentIsolationConfig, McpServerConfig, SubAgentConfig}; use crate::agent_loop::{AgentLoopArgs, run_agent_loop}; +use crate::fleet::{FleetSessionInfo, FleetSessionStatus, SharedFleetRegistry}; use super::def::{MemoryScope, PermissionMode, SubAgentDef, ToolPolicy}; use super::error::SubAgentError; @@ -394,6 +395,11 @@ pub struct SubAgentManager { transcript_dir: Option, /// Maximum number of transcript files to keep (0 = unlimited). transcript_max_files: usize, + /// Optional fleet registry for registering sub-agents in the fleet dashboard. + /// + /// When `None`, fleet registration is skipped silently. Inject via + /// [`set_fleet_registry`][Self::set_fleet_registry]. + fleet_registry: Option, } impl std::fmt::Debug for SubAgentManager { @@ -406,6 +412,7 @@ impl std::fmt::Debug for SubAgentManager { .field("stop_hooks_count", &self.stop_hooks.len()) .field("transcript_dir", &self.transcript_dir) .field("transcript_max_files", &self.transcript_max_files) + .field("fleet_registry", &self.fleet_registry.is_some()) .finish() } } @@ -696,6 +703,7 @@ impl SubAgentManager { stop_hooks: Vec::new(), transcript_dir: None, transcript_max_files: 50, + fleet_registry: None, } } @@ -724,6 +732,15 @@ impl SubAgentManager { self.stop_hooks = hooks; } + /// Inject a fleet registry so spawned sub-agents appear in the fleet dashboard. + /// + /// When set, [`spawn`][Self::spawn] registers the session as `Active` and + /// [`collect`][Self::collect] / [`cancel`][Self::cancel] mark it terminal. + /// Errors from the registry are logged at `warn` level and never propagate to callers. + pub fn set_fleet_registry(&mut self, registry: SharedFleetRegistry) { + self.fleet_registry = Some(registry); + } + /// Load sub-agent definitions from the given directories. /// /// Higher-priority directories should appear first. Name conflicts are resolved @@ -1007,6 +1024,22 @@ impl SubAgentManager { }; self.agents.insert(task_id.clone(), handle); + + // Register sub-agent in the fleet dashboard (fire-and-forget). + if let Some(ref registry) = self.fleet_registry { + let registry = Arc::clone(registry); + let info = FleetSessionInfo { + id: task_id.clone(), + agent_name: def_name.to_owned(), + started_at: crate::transcript::utc_now_pub(), + }; + tokio::spawn(async move { + if let Err(e) = registry.register_active(&info).await { + tracing::warn!(error = %e, task_id = %info.id, "fleet: register_active failed"); + } + }); + } + // FIX-6: log permission_mode so operators can audit privilege escalation at spawn time. // Per-mode runtime enforcement is split across three sites and intentionally NOT done here: // - `Plan` is enforced by `PlanModeExecutor` (build_filtered_executor, ~line 68). @@ -1120,6 +1153,20 @@ impl SubAgentManager { handle.grants.revoke_all(); tracing::info!(task_id, "sub-agent cancelled"); + // Mark session as cancelled in the fleet dashboard (fire-and-forget). + if let Some(ref registry) = self.fleet_registry { + let registry = Arc::clone(registry); + let tid = task_id.to_owned(); + tokio::spawn(async move { + if let Err(e) = registry + .mark_terminal(&tid, FleetSessionStatus::Cancelled) + .await + { + tracing::warn!(error = %e, task_id = %tid, "fleet: mark_terminal(Cancelled) failed"); + } + }); + } + // Fire SubagentStop lifecycle hooks (fire-and-forget). if !self.stop_hooks.is_empty() { let stop_hooks = self.stop_hooks.clone(); @@ -1148,6 +1195,24 @@ impl SubAgentManager { handle.state = SubAgentState::Canceled; handle.grants.revoke_all(); tracing::info!(task_id, "sub-agent cancelled (cancel_all)"); + + // Mark session as cancelled in the fleet dashboard (fire-and-forget). + if let Some(ref registry) = self.fleet_registry { + let registry = Arc::clone(registry); + let tid = task_id.clone(); + tokio::spawn(async move { + if let Err(e) = registry + .mark_terminal(&tid, FleetSessionStatus::Cancelled) + .await + { + tracing::warn!( + error = %e, + task_id = %tid, + "fleet: mark_terminal(Cancelled) failed (cancel_all)" + ); + } + }); + } } } } @@ -1282,24 +1347,42 @@ impl SubAgentManager { Ok(String::new()) }; - // Write terminal meta sidecar if transcripts were enabled at spawn time. - if let Some(ref dir) = handle.transcript_dir.clone() { + // Determine terminal state for both transcript meta and fleet registration. + let final_state = { let status = handle.status_rx.borrow(); - let final_status = if result.is_err() { + if result.is_err() { SubAgentState::Failed } else if status.state == SubAgentState::Canceled { SubAgentState::Canceled } else { SubAgentState::Completed + } + }; + + // Mark session as terminal in the fleet dashboard (fire-and-forget). + if let Some(ref registry) = self.fleet_registry { + let registry = Arc::clone(registry); + let tid = task_id.to_owned(); + let fleet_status = match final_state { + SubAgentState::Failed => FleetSessionStatus::Failed, + SubAgentState::Canceled => FleetSessionStatus::Cancelled, + _ => FleetSessionStatus::Completed, }; - let turns_used = status.turns_used; - drop(status); + tokio::spawn(async move { + if let Err(e) = registry.mark_terminal(&tid, fleet_status).await { + tracing::warn!(error = %e, task_id = %tid, "fleet: mark_terminal failed"); + } + }); + } + // Write terminal meta sidecar if transcripts were enabled at spawn time. + if let Some(ref dir) = handle.transcript_dir.clone() { + let turns_used = handle.status_rx.borrow().turns_used; let meta = TranscriptMeta { agent_id: task_id.to_owned(), agent_name: handle.def.name.clone(), def_name: handle.def.name.clone(), - status: final_status, + status: final_state, started_at: handle.started_at_str.clone(), finished_at: Some(crate::transcript::utc_now_pub()), resumed_from: None, @@ -4538,4 +4621,169 @@ mod tests { mgr.cancel(&new_id).unwrap(); } + + // ---- Fleet registry tests (#4370) ---- + + use crate::fleet::{FleetRegistry, FleetSessionInfo, FleetSessionStatus, SharedFleetRegistry}; + use std::sync::Mutex; + use tokio::sync::Notify; + + /// Records every fleet call for later assertion and signals via `Notify`. + struct MockFleetRegistry { + registered: Mutex>, + terminated: Mutex>, + register_notify: Notify, + terminal_notify: Notify, + } + + impl MockFleetRegistry { + fn new() -> Arc { + Arc::new(Self { + registered: Mutex::new(Vec::new()), + terminated: Mutex::new(Vec::new()), + register_notify: Notify::new(), + terminal_notify: Notify::new(), + }) + } + } + + impl FleetRegistry for MockFleetRegistry { + fn register_active<'a>( + &'a self, + info: &'a FleetSessionInfo, + ) -> Pin> + Send + 'a>> { + self.registered.lock().unwrap().push(info.id.clone()); + self.register_notify.notify_one(); + Box::pin(std::future::ready(Ok(()))) + } + + fn mark_terminal<'a>( + &'a self, + session_id: &'a str, + status: FleetSessionStatus, + ) -> Pin> + Send + 'a>> { + self.terminated + .lock() + .unwrap() + .push((session_id.to_owned(), status)); + self.terminal_notify.notify_one(); + Box::pin(std::future::ready(Ok(()))) + } + } + + fn make_manager_with_fleet(registry: SharedFleetRegistry) -> SubAgentManager { + let mut mgr = SubAgentManager::new(4); + mgr.set_fleet_registry(registry); + mgr + } + + #[tokio::test] + async fn fleet_register_active_called_on_spawn() { + let registry = MockFleetRegistry::new(); + let mut mgr = make_manager_with_fleet(Arc::clone(®istry) as SharedFleetRegistry); + mgr.definitions.push(sample_def()); + + let task_id = mgr + .spawn( + "bot", + "task", + mock_provider(vec!["done"]), + noop_executor(), + None, + &SubAgentConfig::default(), + SpawnContext::default(), + ) + .unwrap(); + + // Wait until the background task calls register_active. + tokio::time::timeout( + tokio::time::Duration::from_secs(2), + registry.register_notify.notified(), + ) + .await + .expect("register_active was not called within 2s"); + + let registered = registry.registered.lock().unwrap(); + assert!( + registered.contains(&task_id), + "register_active must be called with the spawned task_id" + ); + } + + #[tokio::test] + async fn fleet_mark_terminal_completed_on_collect() { + let registry = MockFleetRegistry::new(); + let mut mgr = make_manager_with_fleet(Arc::clone(®istry) as SharedFleetRegistry); + mgr.definitions.push(sample_def()); + + let task_id = mgr + .spawn( + "bot", + "task", + mock_provider(vec!["done"]), + noop_executor(), + None, + &SubAgentConfig::default(), + SpawnContext::default(), + ) + .unwrap(); + + tokio::time::sleep(tokio::time::Duration::from_millis(200)).await; + let _ = mgr.collect(&task_id).await; + + // Wait until the background task calls mark_terminal. + tokio::time::timeout( + tokio::time::Duration::from_secs(2), + registry.terminal_notify.notified(), + ) + .await + .expect("mark_terminal was not called within 2s after collect"); + + let terminated = registry.terminated.lock().unwrap(); + assert!( + terminated.iter().any(|(id, s)| id == &task_id + && matches!( + s, + FleetSessionStatus::Completed | FleetSessionStatus::Failed + )), + "mark_terminal must be called with a terminal status after collect" + ); + } + + #[tokio::test] + async fn fleet_mark_terminal_cancelled_on_cancel() { + let registry = MockFleetRegistry::new(); + let mut mgr = make_manager_with_fleet(Arc::clone(®istry) as SharedFleetRegistry); + mgr.definitions.push(sample_def()); + + let task_id = mgr + .spawn( + "bot", + "task", + mock_provider(vec!["done"]), + noop_executor(), + None, + &SubAgentConfig::default(), + SpawnContext::default(), + ) + .unwrap(); + + mgr.cancel(&task_id).unwrap(); + + // Wait until the background task calls mark_terminal. + tokio::time::timeout( + tokio::time::Duration::from_secs(2), + registry.terminal_notify.notified(), + ) + .await + .expect("mark_terminal was not called within 2s after cancel"); + + let terminated = registry.terminated.lock().unwrap(); + assert!( + terminated + .iter() + .any(|(id, s)| id == &task_id && *s == FleetSessionStatus::Cancelled), + "mark_terminal must be called with Cancelled after cancel" + ); + } } diff --git a/src/fleet_session.rs b/src/fleet_session.rs index 66ce38d31..cbf0b88d9 100644 --- a/src/fleet_session.rs +++ b/src/fleet_session.rs @@ -6,10 +6,75 @@ //! Provides helpers for registering, updating, and reconciling agent sessions //! in the `agent_sessions` table so the fleet dashboard has accurate data. +use std::pin::Pin; +use std::sync::Arc; + use zeph_memory::store::SqliteStore; use zeph_memory::store::agent_sessions::{ AgentSessionRow, SessionChannel, SessionKind, SessionStatus, }; +use zeph_subagent::fleet::{FleetRegistry, FleetSessionInfo, FleetSessionStatus}; + +/// Adapts [`SqliteStore`] to the [`FleetRegistry`] trait used by [`SubAgentManager`]. +/// +/// Wrap a `SqliteStore` with this adapter and inject it via +/// [`SubAgentManager::set_fleet_registry`] so spawned sub-agents appear in the +/// fleet dashboard. +pub(crate) struct SqliteFleetRegistry(Arc); + +impl SqliteFleetRegistry { + /// Create a new adapter from a shared store. + pub(crate) fn new(store: Arc) -> Self { + Self(store) + } +} + +impl FleetRegistry for SqliteFleetRegistry { + fn register_active<'a>( + &'a self, + info: &'a FleetSessionInfo, + ) -> Pin> + Send + 'a>> { + Box::pin(async move { + let row = AgentSessionRow { + id: info.id.clone(), + kind: SessionKind::Autonomous, + status: SessionStatus::Active, + channel: SessionChannel::Cli, + model: String::new(), + created_at: info.started_at.clone(), + last_active_at: info.started_at.clone(), + turns: 0, + prompt_tokens: 0, + completion_tokens: 0, + reasoning_tokens: 0, + cost_cents: 0.0, + goal_text: Some(format!("sub-agent: {}", info.agent_name)), + }; + self.0 + .upsert_agent_session(&row) + .await + .map_err(|e| e.to_string()) + }) + } + + fn mark_terminal<'a>( + &'a self, + session_id: &'a str, + status: FleetSessionStatus, + ) -> Pin> + Send + 'a>> { + Box::pin(async move { + let s = match status { + FleetSessionStatus::Completed => SessionStatus::Completed, + FleetSessionStatus::Failed => SessionStatus::Failed, + FleetSessionStatus::Cancelled => SessionStatus::Cancelled, + }; + self.0 + .update_agent_session_status(session_id, s) + .await + .map_err(|e| e.to_string()) + }) + } +} /// Register a new session in the fleet table and reconcile any stale sessions. /// diff --git a/src/runner.rs b/src/runner.rs index d3c16e83d..48c7d17d0 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -2682,6 +2682,13 @@ pub(crate) async fn run(cli: Cli) -> anyhow::Result<()> { ) { tracing::warn!("sub-agent definition loading failed: {e:#}"); } + // Register sub-agents in the fleet dashboard (#4370). + if !exec_mode.bare { + let fleet_store = std::sync::Arc::new(memory.sqlite().clone()); + let registry = + std::sync::Arc::new(crate::fleet_session::SqliteFleetRegistry::new(fleet_store)); + mgr.set_fleet_registry(registry); + } agent.with_orchestration(config.orchestration.clone(), config.agents.clone(), mgr) }; let agent = { From 907237a0d5ca3158d720f73fa2bb9ff35439fe24 Mon Sep 17 00:00:00 2001 From: "Andrei G." Date: Mon, 18 May 2026 18:29:48 +0200 Subject: [PATCH 2/5] docs(subagent): fix broken intra-doc link in fleet.rs module comment --- crates/zeph-subagent/src/fleet.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/zeph-subagent/src/fleet.rs b/crates/zeph-subagent/src/fleet.rs index c1f5b4366..e42c43409 100644 --- a/crates/zeph-subagent/src/fleet.rs +++ b/crates/zeph-subagent/src/fleet.rs @@ -5,7 +5,7 @@ //! //! [`FleetRegistry`] is a narrow trait that decouples `zeph-subagent` from the //! `zeph-memory` `SqliteStore`. The concrete implementation lives in `zeph-core` -//! and is injected via [`SubAgentManager::set_fleet_registry`]. +//! and is injected via `SubAgentManager::set_fleet_registry`. use std::future::Future; use std::pin::Pin; From 49c2db6e9b78e0e07363a5be4419ffa58d4a7a1b Mon Sep 17 00:00:00 2001 From: "Andrei G." Date: Mon, 18 May 2026 18:35:53 +0200 Subject: [PATCH 3/5] docs: fix broken intra-doc links in fleet_session.rs --- src/fleet_session.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/fleet_session.rs b/src/fleet_session.rs index cbf0b88d9..45d5e5505 100644 --- a/src/fleet_session.rs +++ b/src/fleet_session.rs @@ -15,10 +15,10 @@ use zeph_memory::store::agent_sessions::{ }; use zeph_subagent::fleet::{FleetRegistry, FleetSessionInfo, FleetSessionStatus}; -/// Adapts [`SqliteStore`] to the [`FleetRegistry`] trait used by [`SubAgentManager`]. +/// Adapts [`SqliteStore`] to the [`FleetRegistry`] trait used by `SubAgentManager`. /// /// Wrap a `SqliteStore` with this adapter and inject it via -/// [`SubAgentManager::set_fleet_registry`] so spawned sub-agents appear in the +/// `SubAgentManager::set_fleet_registry` so spawned sub-agents appear in the /// fleet dashboard. pub(crate) struct SqliteFleetRegistry(Arc); From a85c4752ec7bd1c63e4737318b3265feae23365d Mon Sep 17 00:00:00 2001 From: "Andrei G." Date: Mon, 18 May 2026 18:45:28 +0200 Subject: [PATCH 4/5] fix(memory): fix clippy cast_sign_loss and cast_precision_loss in retrieval_astar tests --- crates/zeph-memory/src/graph/retrieval_astar.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/zeph-memory/src/graph/retrieval_astar.rs b/crates/zeph-memory/src/graph/retrieval_astar.rs index 5049ac04e..57b190b41 100644 --- a/crates/zeph-memory/src/graph/retrieval_astar.rs +++ b/crates/zeph-memory/src/graph/retrieval_astar.rs @@ -594,11 +594,12 @@ mod tests { use petgraph::graph::NodeIndex; let mut node_map: HashMap = (0..600i64) - .map(|id| (id, NodeIndex::new(id as usize))) + .map(|id| (id, NodeIndex::new(usize::try_from(id).unwrap()))) .collect(); // Assign scores: entity 599 gets 1.0, 598 gets 0.999, ..., 0 gets ~0.0. - let entity_scores: HashMap = - (0..600i64).map(|id| (id, id as f32 / 599.0)).collect(); + let entity_scores: HashMap = (0..600i64) + .map(|id| (id, id as f64 as f32 / 599.0)) + .collect(); cap_node_map(&mut node_map, &entity_scores, MAX_GRAPH_NODES); @@ -614,7 +615,7 @@ mod tests { use petgraph::graph::NodeIndex; let mut node_map: HashMap = (0..10i64) - .map(|id| (id, NodeIndex::new(id as usize))) + .map(|id| (id, NodeIndex::new(usize::try_from(id).unwrap()))) .collect(); let entity_scores: HashMap = HashMap::new(); From 83367d1f8a42993eacd023ae11e4c261b6f7d9cc Mon Sep 17 00:00:00 2001 From: "Andrei G." Date: Mon, 18 May 2026 18:51:58 +0200 Subject: [PATCH 5/5] fix(memory): avoid cast_precision_loss in cap_node_map test via i16 conversion --- crates/zeph-memory/src/graph/retrieval_astar.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/zeph-memory/src/graph/retrieval_astar.rs b/crates/zeph-memory/src/graph/retrieval_astar.rs index 57b190b41..a20d9e0f6 100644 --- a/crates/zeph-memory/src/graph/retrieval_astar.rs +++ b/crates/zeph-memory/src/graph/retrieval_astar.rs @@ -596,9 +596,10 @@ mod tests { let mut node_map: HashMap = (0..600i64) .map(|id| (id, NodeIndex::new(usize::try_from(id).unwrap()))) .collect(); - // Assign scores: entity 599 gets 1.0, 598 gets 0.999, ..., 0 gets ~0.0. + // Assign scores: entity 599 gets highest, 0 gets lowest. + // i16 covers 0..600 exactly; the i16→f32 cast is lossless (f32 has 23 mantissa bits). let entity_scores: HashMap = (0..600i64) - .map(|id| (id, id as f64 as f32 / 599.0)) + .map(|id| (id, f32::from(i16::try_from(id).unwrap()) / 599.0)) .collect(); cap_node_map(&mut node_map, &entity_scores, MAX_GRAPH_NODES);