From a43806ccc28bf27c174509b7fdc63830ba82b438 Mon Sep 17 00:00:00 2001 From: iven Date: Tue, 21 Apr 2026 17:27:37 +0800 Subject: [PATCH] =?UTF-8?q?fix(growth,kernel,runtime):=20=E7=A9=B7?= =?UTF-8?q?=E5=B0=BD=E5=AE=A1=E8=AE=A1=E5=90=8E=207=20=E9=A1=B9=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=20=E2=80=94=20body=20=E6=8C=81=E4=B9=85=E5=8C=96=20+?= =?UTF-8?q?=20embedding=20=E6=AD=BB=E8=B7=AF=E5=BE=84=20+=20=E5=AE=89?= =?UTF-8?q?=E5=85=A8=E5=8A=A0=E5=9B=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CRITICAL 修复: - body_markdown 数据丢失: SkillManifest.body 字段 + serialize_skill_md 使用 body 替代默认内容 - embedding 检索死路径: rerank_entries 使用异步 index_entry_with_embedding + score_similarity_with_embedding (70/30 混合) - try_write 静默丢失: pending_embedding 字段 + apply_pending_embedding() 延迟应用 IMPORTANT 修复: - auto_mode 内存泄漏: add_pending 容量限制 100 + 溢出时丢弃最旧 - name_to_slug 空 ID: uuid fallback for empty/whitespace-only names - compaction embedding 缺失: compaction GrowthIntegration 也接收 embedding - kernel 未初始化警告: viking_configure_embedding warn log 验证: 934+ tests PASS, 0 failures --- crates/zclaw-growth/src/retriever.rs | 60 +++++++++++++++---- .../src/kernel/evolution_bridge.rs | 11 +++- crates/zclaw-kernel/src/kernel/mod.rs | 3 + .../zclaw-runtime/src/middleware/evolution.rs | 9 ++- crates/zclaw-skills/src/loader.rs | 2 + crates/zclaw-skills/src/registry.rs | 13 ++-- crates/zclaw-skills/src/semantic_router.rs | 1 + crates/zclaw-skills/src/skill.rs | 3 + crates/zclaw-skills/src/wasm_runner.rs | 1 + crates/zclaw-skills/tests/runner_tests.rs | 1 + .../zclaw-skills/tests/skill_types_tests.rs | 2 + .../src-tauri/src/kernel_commands/skill.rs | 6 +- desktop/src-tauri/src/viking_commands.rs | 5 ++ 13 files changed, 97 insertions(+), 20 deletions(-) diff --git a/crates/zclaw-growth/src/retriever.rs b/crates/zclaw-growth/src/retriever.rs index c73b041..7d1a110 100644 --- a/crates/zclaw-growth/src/retriever.rs +++ b/crates/zclaw-growth/src/retriever.rs @@ -19,6 +19,8 @@ pub struct MemoryRetriever { config: RetrievalConfig, /// Semantic scorer for similarity computation scorer: RwLock, + /// Pending embedding client (applied on next scorer access if try_write failed) + pending_embedding: std::sync::Mutex>>, /// Query analyzer analyzer: QueryAnalyzer, /// Memory cache @@ -32,6 +34,7 @@ impl MemoryRetriever { viking, config: RetrievalConfig::default(), scorer: RwLock::new(SemanticScorer::new()), + pending_embedding: std::sync::Mutex::new(None), analyzer: QueryAnalyzer::new(), cache: MemoryCache::default_config(), } @@ -244,19 +247,40 @@ impl MemoryRetriever { let mut scorer = self.scorer.write().await; + // Apply any pending embedding client + self.apply_pending_embedding(&mut scorer); + + // Check if embedding is available for enhanced scoring + let use_embedding = scorer.is_embedding_available(); + // Index entries for semantic search - for entry in &entries { - scorer.index_entry(entry); + if use_embedding { + for entry in &entries { + scorer.index_entry_with_embedding(entry).await; + } + } else { + for entry in &entries { + scorer.index_entry(entry); + } } // Score each entry - let mut scored: Vec<(f32, MemoryEntry)> = entries - .into_iter() - .map(|entry| { - let score = scorer.score_similarity(query, &entry); - (score, entry) - }) - .collect(); + let mut scored: Vec<(f32, MemoryEntry)> = if use_embedding { + let mut results = Vec::with_capacity(entries.len()); + for entry in entries { + let score = scorer.score_similarity_with_embedding(query, &entry).await; + results.push((score, entry)); + } + results + } else { + entries + .into_iter() + .map(|entry| { + let score = scorer.score_similarity(query, &entry); + (score, entry) + }) + .collect() + }; // Sort by score (descending), then by importance and access count scored.sort_by(|a, b| { @@ -420,7 +444,8 @@ impl MemoryRetriever { /// Configure embedding client for semantic similarity /// /// Stores the client for lazy application on first scorer use. - /// Safe to call from non-async contexts. + /// If the scorer lock is busy, the client is stored as pending + /// and applied on the next successful lock acquisition. pub fn set_embedding_client( &self, client: Arc, @@ -429,7 +454,20 @@ impl MemoryRetriever { *scorer = SemanticScorer::with_embedding(client); tracing::info!("[MemoryRetriever] Embedding client configured for semantic scorer"); } else { - tracing::warn!("[MemoryRetriever] Scorer lock busy, embedding will be applied on next access"); + tracing::warn!("[MemoryRetriever] Scorer lock busy, storing embedding client as pending"); + if let Ok(mut pending) = self.pending_embedding.lock() { + *pending = Some(client); + } + } + } + + /// Apply any pending embedding client to the scorer. + fn apply_pending_embedding(&self, scorer: &mut SemanticScorer) { + if let Ok(mut pending) = self.pending_embedding.lock() { + if let Some(client) = pending.take() { + *scorer = SemanticScorer::with_embedding(client); + tracing::info!("[MemoryRetriever] Pending embedding client applied to scorer"); + } } } diff --git a/crates/zclaw-kernel/src/kernel/evolution_bridge.rs b/crates/zclaw-kernel/src/kernel/evolution_bridge.rs index 9202f7a..7e6cedd 100644 --- a/crates/zclaw-kernel/src/kernel/evolution_bridge.rs +++ b/crates/zclaw-kernel/src/kernel/evolution_bridge.rs @@ -13,7 +13,7 @@ use zclaw_types::SkillId; /// Safety invariants: /// - `mode` is always `PromptOnly` (auto-generated skills cannot execute code) /// - `enabled` is `false` (requires one explicit positive feedback to activate) -/// - `body_markdown` becomes the SKILL.md body content (stored by serialize_skill_md) +/// - `body_markdown` is stored in `manifest.body` and persisted by `serialize_skill_md` pub fn candidate_to_manifest(candidate: &SkillCandidate) -> SkillManifest { let slug = name_to_slug(&candidate.name); @@ -32,6 +32,7 @@ pub fn candidate_to_manifest(candidate: &SkillCandidate) -> SkillManifest { triggers: candidate.triggers.clone(), tools: candidate.tools.clone(), enabled: false, + body: Some(candidate.body_markdown.clone()), } } @@ -48,7 +49,13 @@ fn name_to_slug(name: &str) -> String { result.push_str(&format!("{:x}", c as u32)); } } - result.trim_matches('-').to_string() + let slug = result.trim_matches('-').to_string(); + if slug.is_empty() { + // Fallback for empty or whitespace-only names + format!("skill-{}", &uuid::Uuid::new_v4().to_string()[..8]) + } else { + slug + } } #[cfg(test)] diff --git a/crates/zclaw-kernel/src/kernel/mod.rs b/crates/zclaw-kernel/src/kernel/mod.rs index cfc631a..c69f5ad 100644 --- a/crates/zclaw-kernel/src/kernel/mod.rs +++ b/crates/zclaw-kernel/src/kernel/mod.rs @@ -324,6 +324,9 @@ impl Kernel { if let Some(ref driver) = self.extraction_driver { growth_for_compaction = growth_for_compaction.with_llm_driver(driver.clone()); } + if let Some(ref embed_client) = self.embedding_client { + growth_for_compaction.configure_embedding(embed_client.clone()); + } let mw = zclaw_runtime::middleware::compaction::CompactionMiddleware::new( threshold, zclaw_runtime::CompactionConfig::default(), diff --git a/crates/zclaw-runtime/src/middleware/evolution.rs b/crates/zclaw-runtime/src/middleware/evolution.rs index 8c59d42..3b12f9c 100644 --- a/crates/zclaw-runtime/src/middleware/evolution.rs +++ b/crates/zclaw-runtime/src/middleware/evolution.rs @@ -50,7 +50,14 @@ impl EvolutionMiddleware { /// 添加一个待确认的进化事件 pub async fn add_pending(&self, evolution: PendingEvolution) { - self.pending.write().await.push(evolution); + let mut pending = self.pending.write().await; + if pending.len() >= 100 { + tracing::warn!( + "[EvolutionMiddleware] Pending queue full (100), dropping oldest event" + ); + pending.remove(0); + } + pending.push(evolution); } /// 获取并清除所有待确认事件 diff --git a/crates/zclaw-skills/src/loader.rs b/crates/zclaw-skills/src/loader.rs index 9ac6653..7b7a1f0 100644 --- a/crates/zclaw-skills/src/loader.rs +++ b/crates/zclaw-skills/src/loader.rs @@ -191,6 +191,7 @@ pub fn parse_skill_md(content: &str) -> Result { triggers, tools, enabled: true, + body: None, }) } @@ -292,6 +293,7 @@ pub fn parse_skill_toml(content: &str) -> Result { triggers, tools, enabled: true, + body: None, }) } diff --git a/crates/zclaw-skills/src/registry.rs b/crates/zclaw-skills/src/registry.rs index 85c8f0f..15e547b 100644 --- a/crates/zclaw-skills/src/registry.rs +++ b/crates/zclaw-skills/src/registry.rs @@ -241,6 +241,7 @@ impl SkillRegistry { // P2-19: Preserve tools field during update (was silently dropped) tools: if updates.tools.is_empty() { existing.tools } else { updates.tools }, enabled: updates.enabled, + body: existing.body, }; // Rewrite SKILL.md @@ -318,10 +319,14 @@ fn serialize_skill_md(manifest: &SkillManifest) -> String { parts.push("---".to_string()); parts.push(String::new()); - // Body: use description as the skill content - parts.push(format!("# {}", manifest.name)); - parts.push(String::new()); - parts.push(manifest.description.clone()); + // Body: use custom body if provided, otherwise default to "# {name}\n\n{description}" + if let Some(ref body) = manifest.body { + parts.push(body.clone()); + } else { + parts.push(format!("# {}", manifest.name)); + parts.push(String::new()); + parts.push(manifest.description.clone()); + } parts.join("\n") } diff --git a/crates/zclaw-skills/src/semantic_router.rs b/crates/zclaw-skills/src/semantic_router.rs index bd4424c..c27dadd 100644 --- a/crates/zclaw-skills/src/semantic_router.rs +++ b/crates/zclaw-skills/src/semantic_router.rs @@ -534,6 +534,7 @@ mod tests { triggers: triggers.into_iter().map(|s| s.to_string()).collect(), tools: vec![], enabled: true, + body: None, } } diff --git a/crates/zclaw-skills/src/skill.rs b/crates/zclaw-skills/src/skill.rs index 5b44715..172a04d 100644 --- a/crates/zclaw-skills/src/skill.rs +++ b/crates/zclaw-skills/src/skill.rs @@ -95,6 +95,9 @@ pub struct SkillManifest { /// Whether the skill is enabled #[serde(default = "default_enabled")] pub enabled: bool, + /// Custom body content for SKILL.md (overrides default "# {name}\n\n{description}") + #[serde(default, skip)] + pub body: Option, } fn default_enabled() -> bool { true } diff --git a/crates/zclaw-skills/src/wasm_runner.rs b/crates/zclaw-skills/src/wasm_runner.rs index a6b6841..8e6e948 100644 --- a/crates/zclaw-skills/src/wasm_runner.rs +++ b/crates/zclaw-skills/src/wasm_runner.rs @@ -468,6 +468,7 @@ mod tests { triggers: vec![], tools: vec![], enabled: true, + body: None, } } diff --git a/crates/zclaw-skills/tests/runner_tests.rs b/crates/zclaw-skills/tests/runner_tests.rs index d93b509..7ceed16 100644 --- a/crates/zclaw-skills/tests/runner_tests.rs +++ b/crates/zclaw-skills/tests/runner_tests.rs @@ -20,6 +20,7 @@ fn test_manifest(mode: SkillMode) -> SkillManifest { triggers: vec![], tools: vec![], enabled: true, + body: None, } } diff --git a/crates/zclaw-skills/tests/skill_types_tests.rs b/crates/zclaw-skills/tests/skill_types_tests.rs index c03b7bb..bea2a6e 100644 --- a/crates/zclaw-skills/tests/skill_types_tests.rs +++ b/crates/zclaw-skills/tests/skill_types_tests.rs @@ -81,6 +81,7 @@ fn skill_manifest_full_roundtrip() { triggers: vec!["test trigger".to_string()], tools: vec!["bash".to_string()], enabled: true, + body: None, }; let json = serde_json::to_string(&manifest).unwrap(); let parsed: SkillManifest = serde_json::from_str(&json).unwrap(); @@ -126,6 +127,7 @@ fn skill_manifest_all_modes_roundtrip() { triggers: vec![], tools: vec![], enabled: true, + body: None, }; let json = serde_json::to_string(&manifest).unwrap(); let parsed: SkillManifest = serde_json::from_str(&json).unwrap(); diff --git a/desktop/src-tauri/src/kernel_commands/skill.rs b/desktop/src-tauri/src/kernel_commands/skill.rs index baefef8..fef24fa 100644 --- a/desktop/src-tauri/src/kernel_commands/skill.rs +++ b/desktop/src-tauri/src/kernel_commands/skill.rs @@ -174,8 +174,9 @@ pub async fn skill_create( tags: vec![], category: None, triggers: request.triggers, - tools: vec![], // P2-19: Include tools field + tools: vec![], enabled: request.enabled.unwrap_or(true), + body: None, }; kernel.create_skill(manifest.clone()) @@ -221,8 +222,9 @@ pub async fn skill_update( tags: existing.tags.clone(), category: existing.category.clone(), triggers: request.triggers.unwrap_or(existing.triggers), - tools: existing.tools.clone(), // P2-19: Preserve tools on update + tools: existing.tools.clone(), enabled: request.enabled.unwrap_or(existing.enabled), + body: existing.body.clone(), }; let result = kernel.update_skill(&SkillId::new(&id), updated) diff --git a/desktop/src-tauri/src/viking_commands.rs b/desktop/src-tauri/src/viking_commands.rs index 509f0f1..0138756 100644 --- a/desktop/src-tauri/src/viking_commands.rs +++ b/desktop/src-tauri/src/viking_commands.rs @@ -637,6 +637,11 @@ pub async fn viking_configure_embedding( if let Some(ref mut k) = *kernel_lock { k.set_embedding_client(arc_adapter); tracing::info!("[VikingCommands] Embedding propagated to Kernel skill router + memory retriever"); + } else { + tracing::warn!( + "[VikingCommands] Kernel not initialized, embedding only applied to SqliteStorage. \ + It will be applied when Kernel boots." + ); } }