fix(growth,kernel,runtime,desktop): 50 轮功能链路审计 7 项断链修复
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
P0 修复: - B-MEM-2: 跨会话记忆丢失 — 添加 IdentityRecall 查询意图检测, 身份类查询绕过 FTS5/LIKE 文本搜索,直接按 scope 检索全部偏好+知识记忆; 缓存 GrowthIntegration 到 Kernel 避免每次请求重建空 scorer - B-HAND-1: Hands 未触发 — 创建 HandTool wrapper 实现 Tool trait, 在 create_tool_registry() 中注册所有已启用 Hands 为 LLM 可调用工具 P1 修复: - B-SCHED-4: 一次性定时未拦截 — 添加 RE_ONE_SHOT_TODAY 正则匹配 "下午3点半提醒我..."类无日期前缀的同日触发模式 - B-CHAT-2: 工具调用循环 — ToolErrorMiddleware 添加连续失败计数器, 3 次连续失败后自动 AbortLoop 防止无限重试 - B-CHAT-5: Stream 竞态 — cancelStream 后添加 500ms cancelCooldown, 防止后端 active-stream 检查竞态
This commit is contained in:
@@ -36,6 +36,9 @@ pub enum QueryIntent {
|
||||
Code,
|
||||
/// Configuration query
|
||||
Configuration,
|
||||
/// Identity/personal recall — user asks about themselves or past conversations
|
||||
/// Triggers broad retrieval of all preference + knowledge memories
|
||||
IdentityRecall,
|
||||
}
|
||||
|
||||
/// Query analyzer
|
||||
@@ -50,6 +53,8 @@ pub struct QueryAnalyzer {
|
||||
code_indicators: HashSet<String>,
|
||||
/// Stop words to filter out
|
||||
stop_words: HashSet<String>,
|
||||
/// Patterns indicating identity/personal recall queries
|
||||
identity_patterns: Vec<String>,
|
||||
}
|
||||
|
||||
impl QueryAnalyzer {
|
||||
@@ -99,13 +104,38 @@ impl QueryAnalyzer {
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect(),
|
||||
identity_patterns: [
|
||||
// Chinese identity recall patterns
|
||||
"我是谁", "我叫什么", "我之前", "我告诉过你", "我之前告诉",
|
||||
"还记得我", "你还记得", "我的名字", "我的身份", "我的信息",
|
||||
"我的工作", "我在哪", "我的偏好", "我喜欢什么",
|
||||
"关于我", "了解我", "记得我", "我之前说过",
|
||||
// English identity recall patterns
|
||||
"who am i", "what is my name", "what do you know about me",
|
||||
"what did i tell", "do you remember me", "what do you remember",
|
||||
"my preferences", "about me", "what have i shared",
|
||||
]
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Analyze a query string
|
||||
pub fn analyze(&self, query: &str) -> AnalyzedQuery {
|
||||
let keywords = self.extract_keywords(query);
|
||||
let intent = self.classify_intent(&keywords);
|
||||
|
||||
// Check for identity recall patterns first (highest priority)
|
||||
let query_lower = query.to_lowercase();
|
||||
let is_identity = self.identity_patterns.iter()
|
||||
.any(|pattern| query_lower.contains(&pattern.to_lowercase()));
|
||||
|
||||
let intent = if is_identity {
|
||||
QueryIntent::IdentityRecall
|
||||
} else {
|
||||
self.classify_intent(&keywords)
|
||||
};
|
||||
|
||||
let target_types = self.infer_memory_types(intent, &keywords);
|
||||
let expansions = self.expand_query(&keywords);
|
||||
|
||||
@@ -189,6 +219,12 @@ impl QueryAnalyzer {
|
||||
types.push(MemoryType::Preference);
|
||||
types.push(MemoryType::Knowledge);
|
||||
}
|
||||
QueryIntent::IdentityRecall => {
|
||||
// Identity recall needs all memory types
|
||||
types.push(MemoryType::Preference);
|
||||
types.push(MemoryType::Knowledge);
|
||||
types.push(MemoryType::Experience);
|
||||
}
|
||||
}
|
||||
|
||||
types
|
||||
|
||||
@@ -67,6 +67,11 @@ impl MemoryRetriever {
|
||||
analyzed.keywords
|
||||
);
|
||||
|
||||
// Identity recall uses broad scope-based retrieval (bypasses text search)
|
||||
if analyzed.intent == crate::retrieval::query::QueryIntent::IdentityRecall {
|
||||
return self.retrieve_broad_identity(agent_id).await;
|
||||
}
|
||||
|
||||
// Retrieve each type with budget constraints and reranking
|
||||
let preferences = self
|
||||
.retrieve_and_rerank(
|
||||
@@ -230,6 +235,107 @@ impl MemoryRetriever {
|
||||
scored.into_iter().map(|(_, entry)| entry).collect()
|
||||
}
|
||||
|
||||
/// Broad identity recall — retrieves all recent preference + knowledge memories
|
||||
/// without requiring text match. Used when the user asks about themselves.
|
||||
///
|
||||
/// This bypasses FTS5/LIKE search entirely and does a scope-based retrieval
|
||||
/// sorted by recency and importance, ensuring identity information is always
|
||||
/// available across sessions.
|
||||
async fn retrieve_broad_identity(&self, agent_id: &AgentId) -> Result<RetrievalResult> {
|
||||
tracing::info!(
|
||||
"[MemoryRetriever] Broad identity recall for agent: {}",
|
||||
agent_id
|
||||
);
|
||||
|
||||
let agent_str = agent_id.to_string();
|
||||
|
||||
// Retrieve preferences (scope-only, no text search)
|
||||
let preferences = self.retrieve_by_scope(
|
||||
&agent_str,
|
||||
MemoryType::Preference,
|
||||
self.config.max_results_per_type,
|
||||
self.config.preference_budget,
|
||||
).await?;
|
||||
|
||||
// Retrieve knowledge (scope-only)
|
||||
let knowledge = self.retrieve_by_scope(
|
||||
&agent_str,
|
||||
MemoryType::Knowledge,
|
||||
self.config.max_results_per_type,
|
||||
self.config.knowledge_budget,
|
||||
).await?;
|
||||
|
||||
// Retrieve recent experiences (scope-only, limited)
|
||||
let experience = self.retrieve_by_scope(
|
||||
&agent_str,
|
||||
MemoryType::Experience,
|
||||
self.config.max_results_per_type / 2,
|
||||
self.config.experience_budget,
|
||||
).await?;
|
||||
|
||||
let total_tokens = preferences.iter()
|
||||
.chain(knowledge.iter())
|
||||
.chain(experience.iter())
|
||||
.map(|m| m.estimated_tokens())
|
||||
.sum();
|
||||
|
||||
tracing::info!(
|
||||
"[MemoryRetriever] Identity recall: {} preferences, {} knowledge, {} experience",
|
||||
preferences.len(),
|
||||
knowledge.len(),
|
||||
experience.len()
|
||||
);
|
||||
|
||||
Ok(RetrievalResult {
|
||||
preferences,
|
||||
knowledge,
|
||||
experience,
|
||||
total_tokens,
|
||||
})
|
||||
}
|
||||
|
||||
/// Retrieve memories by scope only (no text search).
|
||||
/// Returns entries sorted by importance and recency, limited by budget.
|
||||
async fn retrieve_by_scope(
|
||||
&self,
|
||||
agent_id: &str,
|
||||
memory_type: MemoryType,
|
||||
max_results: usize,
|
||||
token_budget: usize,
|
||||
) -> Result<Vec<MemoryEntry>> {
|
||||
let scope = format!("agent://{}/{}", agent_id, memory_type);
|
||||
let options = FindOptions {
|
||||
scope: Some(scope),
|
||||
limit: Some(max_results * 3), // Fetch more candidates for filtering
|
||||
min_similarity: None, // No similarity threshold for scope-only
|
||||
};
|
||||
|
||||
// Empty query triggers scope-only fetch in SqliteStorage::find()
|
||||
let entries = self.viking.find("", options).await?;
|
||||
|
||||
// Sort by importance (desc) and apply token budget
|
||||
let mut sorted = entries;
|
||||
sorted.sort_by(|a, b| {
|
||||
b.importance.cmp(&a.importance)
|
||||
.then_with(|| b.access_count.cmp(&a.access_count))
|
||||
});
|
||||
|
||||
let mut filtered = Vec::new();
|
||||
let mut used_tokens = 0;
|
||||
for entry in sorted {
|
||||
let tokens = entry.estimated_tokens();
|
||||
if used_tokens + tokens <= token_budget {
|
||||
used_tokens += tokens;
|
||||
filtered.push(entry);
|
||||
}
|
||||
if filtered.len() >= max_results {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(filtered)
|
||||
}
|
||||
|
||||
/// Retrieve a specific memory by URI (with cache)
|
||||
pub async fn get_by_uri(&self, uri: &str) -> Result<Option<MemoryEntry>> {
|
||||
// Check cache first
|
||||
|
||||
Reference in New Issue
Block a user