From c5f98beb7c85025cce9f8f12aacdb0981edbe333 Mon Sep 17 00:00:00 2001 From: iven Date: Tue, 21 Apr 2026 20:39:32 +0800 Subject: [PATCH] =?UTF-8?q?fix(growth):=20=E8=AE=B0=E5=BF=86=E5=8F=AC?= =?UTF-8?q?=E5=9B=9E=E8=B7=A8=20agent=20fallback=20=E2=80=94=20IdentityRec?= =?UTF-8?q?all=20=E5=85=A8=E5=B1=80=20scope=20=E6=A3=80=E7=B4=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit B14 根因: 记忆按 agent_id 隔离存储,用户换对话/Agent 后 新 agent_id scope 下无记忆可检索,导致"我叫什么"无法召回。 修复: retrieve_broad_identity 在当前 agent 无结果时 fallback 到 retrieve_by_scope_any_agent,跨所有 agent 检索身份相关 的 preference/knowledge 记忆(用户名、工作单位等)。 影响范围: 仅 IdentityRecall 路径("我是谁"/"我叫什么"类查询), 普通 keyword 检索仍按 agent_id scope 隔离。 --- crates/zclaw-growth/src/retriever.rs | 67 ++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/crates/zclaw-growth/src/retriever.rs b/crates/zclaw-growth/src/retriever.rs index 7d1a110..814f48e 100644 --- a/crates/zclaw-growth/src/retriever.rs +++ b/crates/zclaw-growth/src/retriever.rs @@ -331,6 +331,36 @@ impl MemoryRetriever { self.config.experience_budget, ).await?; + // Fallback: if no results for this agent, search across ALL agents + // for identity-critical info (user name, workplace, preferences) + if preferences.is_empty() && knowledge.is_empty() && experience.is_empty() { + tracing::info!( + "[MemoryRetriever] No memories for agent {}, falling back to global scope", + agent_str + ); + let global_prefs = self.retrieve_by_scope_any_agent( + MemoryType::Preference, + self.config.max_results_per_type, + self.config.preference_budget, + ).await?; + let global_knowledge = self.retrieve_by_scope_any_agent( + MemoryType::Knowledge, + self.config.max_results_per_type, + self.config.knowledge_budget, + ).await?; + let total: usize = global_prefs.iter() + .chain(global_knowledge.iter()) + .map(|m| m.estimated_tokens()) + .sum(); + + return Ok(RetrievalResult { + preferences: global_prefs, + knowledge: global_knowledge, + experience, + total_tokens: total, + }); + } + let total_tokens = preferences.iter() .chain(knowledge.iter()) .chain(experience.iter()) @@ -352,6 +382,43 @@ impl MemoryRetriever { }) } + /// Retrieve memories across ALL agents for a given type. + /// Used as fallback when agent-scoped retrieval returns nothing for identity recall. + async fn retrieve_by_scope_any_agent( + &self, + memory_type: MemoryType, + max_results: usize, + token_budget: usize, + ) -> Result> { + // Match any agent by using only the type suffix as scope pattern + let scope_pattern = format!("/{}", memory_type); + let options = FindOptions { + scope: None, // No scope filter — search all agents + limit: Some(max_results * 3), + min_similarity: None, + }; + let entries = self.viking.find("", options).await?; + // Filter to only matching memory type + let mut filtered: Vec = entries + .into_iter() + .filter(|e| e.uri.contains(&scope_pattern) || e.memory_type == memory_type) + .collect(); + filtered.sort_by(|a, b| { + b.importance.cmp(&a.importance) + .then_with(|| b.access_count.cmp(&a.access_count)) + }); + let mut result = Vec::new(); + let mut used_tokens = 0; + for entry in filtered { + let tokens = entry.estimated_tokens(); + if used_tokens + tokens > token_budget { break; } + used_tokens += tokens; + result.push(entry); + if result.len() >= max_results { break; } + } + Ok(result) + } + /// Retrieve memories by scope only (no text search). /// Returns entries sorted by importance and recency, limited by budget. async fn retrieve_by_scope(