diff --git a/src/agent/summary.rs b/src/agent/summary.rs index 85b0e7c..699174f 100644 --- a/src/agent/summary.rs +++ b/src/agent/summary.rs @@ -124,7 +124,9 @@ fn truncate_total_chars(s: &str, max_chars: usize) -> String { let dropped: String = s.chars().take(skip).collect(); let mut kept = String::with_capacity(max_chars + 8); kept.push_str("[... earlier messages truncated ...]\n"); - kept.push_str(&s[dropped.len()..]); // 字节切:dropped.len() 字节一定在 char 边界 + // 字节切安全:dropped 由 s.chars().take(skip).collect() 构建, + // 每个 char 的 UTF-8 字节序列完整保留,故 dropped.len() 恰好是 s 的某个 char 边界字节偏移。 + kept.push_str(&s[dropped.len()..]); kept } diff --git a/src/engine/session_manager.rs b/src/engine/session_manager.rs index 721f122..4859a36 100644 --- a/src/engine/session_manager.rs +++ b/src/engine/session_manager.rs @@ -446,6 +446,8 @@ impl SessionManager { /// 查询某 parent 的所有直接子 session 的 ID 列表。 /// /// 实现:prefix 查询所有 `session:*:meta`,过滤 `parent_id == parent_id`。 + // ponytail: O(N) 全表扫描,当前规模(≤10K session)可接受。 + // 如有性能需求,可维护 `parent:{parent_id}:children` 索引 key 替代扫描。 pub async fn children(&self, parent_id: &str) -> Result, EngineError> { let filter = MemoryFilter { prefix: Some("session:".to_string()), diff --git a/src/engine/snapshot.rs b/src/engine/snapshot.rs index 5ab8fa1..3ad7605 100644 --- a/src/engine/snapshot.rs +++ b/src/engine/snapshot.rs @@ -12,6 +12,9 @@ pub struct SessionMemoryEntry { #[serde(default)] pub metadata: serde_json::Value, /// 创建时间(Unix 时间戳秒;`None` 兼容旧快照)。 + /// + /// 类型为 `i64` 而非 `u64`:`time` crate 的 `OffsetDateTime::from_unix_timestamp` 接收 `i64`, + /// 这里保持与 `SessionMemory::set_with_meta` 签名一致,避免来回转换。 #[serde(default)] pub created_at: Option, } diff --git a/src/engine/sub_agent.rs b/src/engine/sub_agent.rs index abd12b3..0022207 100644 --- a/src/engine/sub_agent.rs +++ b/src/engine/sub_agent.rs @@ -9,6 +9,8 @@ //! - 可增加 `cancel_all(parent_id)` 中止正在运行的子 session //! - 可暴露 `child_session_tree` API 支持会话树查询 +use serde::{Deserialize, Serialize}; + use crate::llm::stream::StreamEvent; use crate::llm::types::response_v2::MessageResponse; use crate::llm::types::usage::CostTracker; @@ -53,7 +55,7 @@ impl Default for DispatchConfig { /// dispatch 成功后子 session **保留在 SessionManager 中**,调用方可 /// 通过 `sm.get(&result.child_id)` 获取子 session 引用,进而通过 /// `session_memory()` 读取子 SessionMemory(如 `"result_summary"`)。 -#[derive(Debug)] +#[derive(Debug, Serialize, Deserialize)] pub struct SubTaskResult { /// 子 session ID。可通过此 ID 在 SessionManager 中读取子 session。 pub child_id: String, @@ -273,7 +275,6 @@ impl SessionManager { let sm = self.clone(); let parent_id = parent_id_owned.clone(); let config = config.clone(); - let task = task.clone(); handles.push(tokio::spawn(async move { let _permit = sem.acquire_owned().await.expect("Semaphore closed"); diff --git a/src/memory/graph.rs b/src/memory/graph.rs index 4ac71bf..c013be9 100644 --- a/src/memory/graph.rs +++ b/src/memory/graph.rs @@ -253,7 +253,9 @@ impl KnowledgeGraph for InMemoryGraph { if entity.id.is_empty() { return Err(MemoryError::InvalidInput("entity id is empty".into())); } - let mut inner = self.inner.lock().unwrap(); + let mut inner = self.inner.lock().map_err(|e| { + MemoryError::RetrievalError(format!("lock poisoned: {e}")) + })?; // upsert:若已存在,先收集旧标签用于清理反向引用(避免同时借用 entities 和 tag_index) let old_tags: Vec = inner .entities @@ -281,12 +283,16 @@ impl KnowledgeGraph for InMemoryGraph { } async fn get_entity(&self, id: &str) -> Result, MemoryError> { - let inner = self.inner.lock().unwrap(); + let inner = self.inner.lock().map_err(|e| { + MemoryError::RetrievalError(format!("lock poisoned: {e}")) + })?; Ok(inner.entities.get(id).cloned()) } async fn remove_entity(&self, id: &str) -> Result<(), MemoryError> { - let mut inner = self.inner.lock().unwrap(); + let mut inner = self.inner.lock().map_err(|e| { + MemoryError::RetrievalError(format!("lock poisoned: {e}")) + })?; // 移除实体并清理其标签反向引用 if let Some(entity) = inner.entities.remove(id) { for tag in &entity.tags { @@ -306,7 +312,9 @@ impl KnowledgeGraph for InMemoryGraph { } async fn add_relation(&self, relation: GraphRelation) -> Result<(), MemoryError> { - let mut inner = self.inner.lock().unwrap(); + let mut inner = self.inner.lock().map_err(|e| { + MemoryError::RetrievalError(format!("lock poisoned: {e}")) + })?; // 校验两端实体存在 if !inner.entities.contains_key(&relation.source_id) { return Err(MemoryError::InvalidInput(format!( @@ -340,7 +348,9 @@ impl KnowledgeGraph for InMemoryGraph { target_id: &str, relation_type: &str, ) -> Result<(), MemoryError> { - let mut inner = self.inner.lock().unwrap(); + let mut inner = self.inner.lock().map_err(|e| { + MemoryError::RetrievalError(format!("lock poisoned: {e}")) + })?; inner.relations.retain(|r| { !(r.source_id == source_id && r.target_id == target_id && r.relation_type == relation_type) }); @@ -354,7 +364,9 @@ impl KnowledgeGraph for InMemoryGraph { direction: RelationDirection, relation_types: Option<&[&str]>, ) -> Result, MemoryError> { - let inner = self.inner.lock().unwrap(); + let inner = self.inner.lock().map_err(|e| { + MemoryError::RetrievalError(format!("lock poisoned: {e}")) + })?; // 1. 验证起点存在 if !inner.entities.contains_key(entity_id) { @@ -458,7 +470,9 @@ impl KnowledgeGraph for InMemoryGraph { } async fn find_by_keywords(&self, keywords: &[String]) -> Result, MemoryError> { - let inner = self.inner.lock().unwrap(); + let inner = self.inner.lock().map_err(|e| { + MemoryError::RetrievalError(format!("lock poisoned: {e}")) + })?; if keywords.is_empty() { return Ok(Vec::new()); } @@ -481,7 +495,9 @@ impl KnowledgeGraph for InMemoryGraph { } async fn find_tags(&self, prefix: &str) -> Result, MemoryError> { - let inner = self.inner.lock().unwrap(); + let inner = self.inner.lock().map_err(|e| { + MemoryError::RetrievalError(format!("lock poisoned: {e}")) + })?; let prefix_l = prefix.to_lowercase(); let mut tags: Vec = inner .tag_index @@ -498,7 +514,9 @@ impl KnowledgeGraph for InMemoryGraph { entity_id: &str, tags: Vec, ) -> Result { - let mut inner = self.inner.lock().unwrap(); + let mut inner = self.inner.lock().map_err(|e| { + MemoryError::RetrievalError(format!("lock poisoned: {e}")) + })?; // 先收集旧标签(避免同时借用 entities 和 tag_index) let old_tags: Vec = inner @@ -536,7 +554,9 @@ impl KnowledgeGraph for InMemoryGraph { } async fn entity_count_by_tag(&self, tag: &str) -> Result { - let inner = self.inner.lock().unwrap(); + let inner = self.inner.lock().map_err(|e| { + MemoryError::RetrievalError(format!("lock poisoned: {e}")) + })?; Ok(inner .tag_index .get(tag)