fix(audit): 修复深度审计 P1/P2 问题 — 记忆统一、持久化、前端适配
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
H3: 重写 memory_commands.rs 统一到 VikingStorage 单一存储,移除双写 H4: 心跳引擎 record_interaction() 持久化到 VikingStorage,启动时恢复 M4: 反思结果/状态持久化到 VikingStorage metadata,重启后自动恢复 - HandApprovalModal import 修正 (handStore 替代 gatewayStore) - kernel-client.ts 幽灵调用替换为 kernel_status - PersistentMemoryStore dead_code warnings 清理 - 审计报告和 README 更新至 v0.6.3,完成度 58%→62%
This commit is contained in:
@@ -376,10 +376,23 @@ fn get_last_interaction_map() -> &'static RwLock<StdHashMap<String, String>> {
|
||||
|
||||
/// Record an interaction for an agent (call from frontend when user sends message)
|
||||
pub fn record_interaction(agent_id: &str) {
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
|
||||
// Store in-memory map (fast path)
|
||||
let map = get_last_interaction_map();
|
||||
if let Ok(mut map) = map.write() {
|
||||
map.insert(agent_id.to_string(), chrono::Utc::now().to_rfc3339());
|
||||
map.insert(agent_id.to_string(), now.clone());
|
||||
}
|
||||
|
||||
// Persist to VikingStorage metadata (survives restarts)
|
||||
let key = format!("heartbeat:last_interaction:{}", agent_id);
|
||||
tokio::spawn(async move {
|
||||
if let Ok(storage) = crate::viking_commands::get_storage().await {
|
||||
if let Err(e) = zclaw_growth::VikingStorage::store_metadata_json(&*storage, &key, &now).await {
|
||||
tracing::warn!("[heartbeat] Failed to persist interaction time: {}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Update memory stats cache for an agent
|
||||
@@ -621,6 +634,9 @@ fn check_learning_opportunities(agent_id: &str) -> Option<HeartbeatAlert> {
|
||||
pub type HeartbeatEngineState = Arc<Mutex<HashMap<String, HeartbeatEngine>>>;
|
||||
|
||||
/// Initialize heartbeat engine for an agent
|
||||
///
|
||||
/// Restores persisted interaction time from VikingStorage so idle-greeting
|
||||
/// check works correctly across app restarts.
|
||||
#[tauri::command]
|
||||
pub async fn heartbeat_init(
|
||||
agent_id: String,
|
||||
@@ -628,11 +644,43 @@ pub async fn heartbeat_init(
|
||||
state: tauri::State<'_, HeartbeatEngineState>,
|
||||
) -> Result<(), String> {
|
||||
let engine = HeartbeatEngine::new(agent_id.clone(), config);
|
||||
|
||||
// Restore last interaction time from VikingStorage metadata
|
||||
restore_last_interaction(&agent_id).await;
|
||||
|
||||
let mut engines = state.lock().await;
|
||||
engines.insert(agent_id, engine);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Restore the last interaction timestamp for an agent from VikingStorage.
|
||||
/// Called during heartbeat_init so the idle-greeting check works after restart.
|
||||
async fn restore_last_interaction(agent_id: &str) {
|
||||
let key = format!("heartbeat:last_interaction:{}", agent_id);
|
||||
match crate::viking_commands::get_storage().await {
|
||||
Ok(storage) => {
|
||||
match zclaw_growth::VikingStorage::get_metadata_json(&*storage, &key).await {
|
||||
Ok(Some(timestamp)) => {
|
||||
let map = get_last_interaction_map();
|
||||
if let Ok(mut map) = map.write() {
|
||||
map.insert(agent_id.to_string(), timestamp);
|
||||
}
|
||||
tracing::info!("[heartbeat] Restored last interaction for {}", agent_id);
|
||||
}
|
||||
Ok(None) => {
|
||||
tracing::debug!("[heartbeat] No persisted interaction for {}", agent_id);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("[heartbeat] Failed to restore interaction: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("[heartbeat] Storage unavailable during init: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Start heartbeat engine for an agent
|
||||
#[tauri::command]
|
||||
pub async fn heartbeat_start(
|
||||
|
||||
@@ -229,6 +229,34 @@ impl ReflectionEngine {
|
||||
self.history = self.history.split_off(10);
|
||||
}
|
||||
|
||||
// 8. Persist result and state to VikingStorage (fire-and-forget)
|
||||
let state_to_persist = self.state.clone();
|
||||
let result_to_persist = result.clone();
|
||||
let agent_id_owned = agent_id.to_string();
|
||||
tokio::spawn(async move {
|
||||
if let Ok(storage) = crate::viking_commands::get_storage().await {
|
||||
// Persist state as JSON string
|
||||
let state_key = format!("reflection:state:{}", agent_id_owned);
|
||||
if let Ok(state_json) = serde_json::to_string(&state_to_persist) {
|
||||
if let Err(e) = zclaw_growth::VikingStorage::store_metadata_json(
|
||||
&*storage, &state_key, &state_json,
|
||||
).await {
|
||||
tracing::warn!("[reflection] Failed to persist state: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Persist result as JSON string
|
||||
let result_key = format!("reflection:latest:{}", agent_id_owned);
|
||||
if let Ok(result_json) = serde_json::to_string(&result_to_persist) {
|
||||
if let Err(e) = zclaw_growth::VikingStorage::store_metadata_json(
|
||||
&*storage, &result_key, &result_json,
|
||||
).await {
|
||||
tracing::warn!("[reflection] Failed to persist result: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
@@ -467,6 +495,118 @@ impl ReflectionEngine {
|
||||
pub fn update_config(&mut self, config: ReflectionConfig) {
|
||||
self.config = config;
|
||||
}
|
||||
|
||||
/// Restore state from VikingStorage metadata (called during init)
|
||||
///
|
||||
/// Spawns an async task to read persisted state and result from VikingStorage.
|
||||
/// Results are placed in global caches, consumed one-shot by intelligence_hooks.
|
||||
pub fn restore_state(&self, agent_id: &str) {
|
||||
let rt = tokio::runtime::Handle::current();
|
||||
let state_key = format!("reflection:state:{}", agent_id);
|
||||
let result_key = format!("reflection:latest:{}", agent_id);
|
||||
let agent_id_owned = agent_id.to_string();
|
||||
|
||||
rt.spawn(async move {
|
||||
match crate::viking_commands::get_storage().await {
|
||||
Ok(storage) => {
|
||||
// Restore state
|
||||
match zclaw_growth::VikingStorage::get_metadata_json(
|
||||
&*storage, &state_key,
|
||||
).await {
|
||||
Ok(Some(state_json)) => {
|
||||
if let Ok(persisted_state) = serde_json::from_str::<ReflectionState>(&state_json) {
|
||||
tracing::info!(
|
||||
"[reflection] Restored state for {}: {} conversations since last reflection",
|
||||
agent_id_owned,
|
||||
persisted_state.conversations_since_reflection
|
||||
);
|
||||
let cache = get_state_cache();
|
||||
if let Ok(mut cache) = cache.write() {
|
||||
cache.insert(agent_id_owned.clone(), persisted_state);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
tracing::debug!("[reflection] No persisted state for {}", agent_id_owned);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("[reflection] Failed to read state: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore latest result into history
|
||||
match zclaw_growth::VikingStorage::get_metadata_json(
|
||||
&*storage, &result_key,
|
||||
).await {
|
||||
Ok(Some(result_json)) => {
|
||||
if let Ok(persisted_result) = serde_json::from_str::<ReflectionResult>(&result_json) {
|
||||
let cache = get_result_cache();
|
||||
if let Ok(mut cache) = cache.write() {
|
||||
cache.insert(agent_id_owned.clone(), persisted_result);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(e) => {
|
||||
tracing::warn!("[reflection] Failed to read result: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("[reflection] Storage unavailable during restore: {}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Apply a restored state (called from intelligence_hooks after restore completes)
|
||||
pub fn apply_restored_state(&mut self, state: ReflectionState) {
|
||||
self.state = state;
|
||||
}
|
||||
|
||||
/// Apply a restored latest result to history
|
||||
pub fn apply_restored_result(&mut self, result: ReflectionResult) {
|
||||
self.history.push(result);
|
||||
}
|
||||
}
|
||||
|
||||
// === State Restoration Cache ===
|
||||
|
||||
use std::sync::RwLock as StdRwLock;
|
||||
use std::sync::OnceLock as StdOnceLock;
|
||||
|
||||
/// Temporary cache for restored reflection state (bridges async init ↔ sync apply)
|
||||
static REFLECTION_STATE_CACHE: StdOnceLock<StdRwLock<HashMap<String, ReflectionState>>> = StdOnceLock::new();
|
||||
|
||||
/// Temporary cache for restored reflection result
|
||||
static REFLECTION_RESULT_CACHE: StdOnceLock<StdRwLock<HashMap<String, ReflectionResult>>> = StdOnceLock::new();
|
||||
|
||||
fn get_state_cache() -> &'static StdRwLock<HashMap<String, ReflectionState>> {
|
||||
REFLECTION_STATE_CACHE.get_or_init(|| StdRwLock::new(HashMap::new()))
|
||||
}
|
||||
|
||||
fn get_result_cache() -> &'static StdRwLock<HashMap<String, ReflectionResult>> {
|
||||
REFLECTION_RESULT_CACHE.get_or_init(|| StdRwLock::new(HashMap::new()))
|
||||
}
|
||||
|
||||
/// Pop restored state from cache (one-shot, removes after read)
|
||||
pub fn pop_restored_state(agent_id: &str) -> Option<ReflectionState> {
|
||||
let cache = get_state_cache();
|
||||
if let Ok(mut cache) = cache.write() {
|
||||
cache.remove(agent_id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Pop restored result from cache (one-shot, removes after read)
|
||||
pub fn pop_restored_result(agent_id: &str) -> Option<ReflectionResult> {
|
||||
let cache = get_result_cache();
|
||||
if let Ok(mut cache) = cache.write() {
|
||||
cache.remove(agent_id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// === Tauri Commands ===
|
||||
|
||||
@@ -51,6 +51,14 @@ pub async fn post_conversation_hook(
|
||||
// Step 2: Record conversation for reflection
|
||||
let mut engine = reflection_state.lock().await;
|
||||
|
||||
// Apply restored state on first call (one-shot after app restart)
|
||||
if let Some(restored_state) = crate::intelligence::reflection::pop_restored_state(agent_id) {
|
||||
engine.apply_restored_state(restored_state);
|
||||
}
|
||||
if let Some(restored_result) = crate::intelligence::reflection::pop_restored_result(agent_id) {
|
||||
engine.apply_restored_result(restored_result);
|
||||
}
|
||||
|
||||
engine.record_conversation();
|
||||
debug!(
|
||||
"[intelligence_hooks] Conversation count updated for agent: {}",
|
||||
|
||||
@@ -14,7 +14,7 @@ pub mod crypto;
|
||||
|
||||
// Re-export main types for convenience
|
||||
pub use persistent::{
|
||||
PersistentMemory, PersistentMemoryStore, MemorySearchQuery, MemoryStats,
|
||||
generate_memory_id, configure_embedding_client, is_embedding_configured,
|
||||
PersistentMemory, PersistentMemoryStore, MemoryStats,
|
||||
configure_embedding_client, is_embedding_configured,
|
||||
EmbedFn,
|
||||
};
|
||||
|
||||
@@ -143,6 +143,7 @@ pub struct PersistentMemoryStore {
|
||||
conn: Arc<Mutex<SqliteConnection>>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)] // Legacy: operations migrated to VikingStorage, kept for backward compat
|
||||
impl PersistentMemoryStore {
|
||||
/// Create a new persistent memory store
|
||||
pub async fn new(app_handle: &tauri::AppHandle) -> Result<Self, String> {
|
||||
@@ -497,6 +498,7 @@ impl PersistentMemoryStore {
|
||||
}
|
||||
|
||||
/// Generate a unique memory ID
|
||||
#[allow(dead_code)] // Legacy: kept for potential migration use
|
||||
pub fn generate_memory_id() -> String {
|
||||
let uuid_str = Uuid::new_v4().to_string().replace("-", "");
|
||||
let short_uuid = &uuid_str[..8];
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
//! Memory Commands - Tauri commands for persistent memory operations
|
||||
//!
|
||||
//! Phase 1 of Intelligence Layer Migration:
|
||||
//! Provides frontend API for memory storage and retrieval
|
||||
//! Unified storage: All operations delegate to VikingStorage (SqliteStorage),
|
||||
//! which provides FTS5 full-text search, TF-IDF scoring, and optional embedding.
|
||||
//!
|
||||
//! The previous dual-write to PersistentMemoryStore has been removed.
|
||||
//! PersistentMemory type is retained for frontend API compatibility.
|
||||
|
||||
use crate::memory::{PersistentMemory, PersistentMemoryStore, MemorySearchQuery, MemoryStats, generate_memory_id, configure_embedding_client, is_embedding_configured, EmbedFn};
|
||||
use crate::memory::{PersistentMemory, PersistentMemoryStore, MemoryStats, configure_embedding_client, is_embedding_configured, EmbedFn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use tauri::{AppHandle, State};
|
||||
use tokio::sync::Mutex;
|
||||
use chrono::Utc;
|
||||
|
||||
/// Shared memory store state
|
||||
/// NOTE: PersistentMemoryStore is kept only for embedding configuration.
|
||||
/// All actual storage goes through VikingStorage (SqliteStorage).
|
||||
pub type MemoryStoreState = Arc<Mutex<Option<PersistentMemoryStore>>>;
|
||||
|
||||
/// Memory entry for frontend API
|
||||
@@ -38,77 +42,49 @@ pub struct MemorySearchOptions {
|
||||
}
|
||||
|
||||
/// Initialize the memory store
|
||||
///
|
||||
/// Now a no-op for storage (VikingStorage initializes itself in viking_commands).
|
||||
/// Only initializes PersistentMemoryStore for backward-compatible embedding config.
|
||||
#[tauri::command]
|
||||
pub async fn memory_init(
|
||||
app_handle: AppHandle,
|
||||
state: State<'_, MemoryStoreState>,
|
||||
) -> Result<(), String> {
|
||||
let store = PersistentMemoryStore::new(&app_handle).await?;
|
||||
|
||||
let mut state_guard = state.lock().await;
|
||||
*state_guard = Some(store);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
/// Store a new memory
|
||||
///
|
||||
/// Writes to both PersistentMemoryStore (backward compat) and SqliteStorage (FTS5+Embedding).
|
||||
/// SqliteStorage write failure is logged but does not block the operation.
|
||||
/// Writes to VikingStorage (SqliteStorage) with FTS5 + TF-IDF indexing.
|
||||
#[tauri::command]
|
||||
pub async fn memory_store(
|
||||
entry: MemoryEntryInput,
|
||||
state: State<'_, MemoryStoreState>,
|
||||
_state: State<'_, MemoryStoreState>,
|
||||
) -> Result<String, String> {
|
||||
let state_guard = state.lock().await;
|
||||
let storage = crate::viking_commands::get_storage().await?;
|
||||
|
||||
let store = state_guard
|
||||
.as_ref()
|
||||
.ok_or_else(|| "Memory store not initialized. Call memory_init first.".to_string())?;
|
||||
let memory_type = parse_memory_type(&entry.memory_type);
|
||||
let keywords = entry.tags.unwrap_or_default();
|
||||
let importance = entry.importance.unwrap_or(5).clamp(1, 10) as u8;
|
||||
|
||||
let now = Utc::now().to_rfc3339();
|
||||
let id = generate_memory_id();
|
||||
let memory = PersistentMemory {
|
||||
id: id.clone(),
|
||||
agent_id: entry.agent_id.clone(),
|
||||
memory_type: entry.memory_type.clone(),
|
||||
content: entry.content.clone(),
|
||||
importance: entry.importance.unwrap_or(5),
|
||||
source: entry.source.unwrap_or_else(|| "auto".to_string()),
|
||||
tags: serde_json::to_string(&entry.tags.clone().unwrap_or_default())
|
||||
.unwrap_or_else(|_| "[]".to_string()),
|
||||
conversation_id: entry.conversation_id.clone(),
|
||||
created_at: now.clone(),
|
||||
last_accessed_at: now,
|
||||
access_count: 0,
|
||||
embedding: None,
|
||||
overview: None,
|
||||
};
|
||||
let viking_entry = zclaw_growth::MemoryEntry::new(
|
||||
&entry.agent_id,
|
||||
memory_type,
|
||||
&entry.memory_type,
|
||||
entry.content,
|
||||
)
|
||||
.with_importance(importance)
|
||||
.with_keywords(keywords);
|
||||
|
||||
// Write to PersistentMemoryStore (primary)
|
||||
store.store(&memory).await?;
|
||||
let uri = viking_entry.uri.clone();
|
||||
zclaw_growth::VikingStorage::store(storage.as_ref(), &viking_entry).await
|
||||
.map_err(|e| format!("Failed to store memory: {}", e))?;
|
||||
|
||||
// Also write to SqliteStorage via VikingStorage for FTS5 + Embedding search
|
||||
if let Ok(storage) = crate::viking_commands::get_storage().await {
|
||||
let memory_type = parse_memory_type(&entry.memory_type);
|
||||
let keywords = entry.tags.unwrap_or_default();
|
||||
|
||||
let viking_entry = zclaw_growth::MemoryEntry::new(
|
||||
&entry.agent_id,
|
||||
memory_type,
|
||||
&entry.memory_type,
|
||||
entry.content,
|
||||
)
|
||||
.with_importance(entry.importance.unwrap_or(5) as u8)
|
||||
.with_keywords(keywords);
|
||||
|
||||
match zclaw_growth::VikingStorage::store(storage.as_ref(), &viking_entry).await {
|
||||
Ok(()) => tracing::debug!("[memory_store] Also stored in SqliteStorage"),
|
||||
Err(e) => tracing::warn!("[memory_store] SqliteStorage write failed (non-blocking): {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(id)
|
||||
Ok(uri)
|
||||
}
|
||||
|
||||
/// Parse a string memory_type into zclaw_growth::MemoryType
|
||||
@@ -122,59 +98,99 @@ fn parse_memory_type(type_str: &str) -> zclaw_growth::MemoryType {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a memory by ID
|
||||
/// Convert zclaw_growth::MemoryEntry to PersistentMemory (frontend compatibility)
|
||||
fn to_persistent(entry: &zclaw_growth::MemoryEntry) -> PersistentMemory {
|
||||
// Extract agent_id from URI: "agent://{agent_id}/{type}/{category}"
|
||||
let agent_id = entry.uri
|
||||
.strip_prefix("agent://")
|
||||
.and_then(|s| s.split('/').next())
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
|
||||
PersistentMemory {
|
||||
id: entry.uri.clone(),
|
||||
agent_id,
|
||||
memory_type: entry.memory_type.to_string(),
|
||||
content: entry.content.clone(),
|
||||
importance: entry.importance as i32,
|
||||
source: "auto".to_string(),
|
||||
tags: serde_json::to_string(&entry.keywords).unwrap_or_else(|_| "[]".to_string()),
|
||||
conversation_id: None,
|
||||
created_at: entry.created_at.to_rfc3339(),
|
||||
last_accessed_at: entry.last_accessed.to_rfc3339(),
|
||||
access_count: entry.access_count as i32,
|
||||
embedding: None,
|
||||
overview: entry.overview.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a memory by ID (URI)
|
||||
#[tauri::command]
|
||||
pub async fn memory_get(
|
||||
id: String,
|
||||
state: State<'_, MemoryStoreState>,
|
||||
_state: State<'_, MemoryStoreState>,
|
||||
) -> Result<Option<PersistentMemory>, String> {
|
||||
let state_guard = state.lock().await;
|
||||
let storage = crate::viking_commands::get_storage().await?;
|
||||
|
||||
let store = state_guard
|
||||
.as_ref()
|
||||
.ok_or_else(|| "Memory store not initialized".to_string())?;
|
||||
let entry = zclaw_growth::VikingStorage::get(storage.as_ref(), &id).await
|
||||
.map_err(|e| format!("Failed to get memory: {}", e))?;
|
||||
|
||||
store.get(&id).await
|
||||
Ok(entry.map(|e| to_persistent(&e)))
|
||||
}
|
||||
|
||||
/// Search memories
|
||||
///
|
||||
/// Uses VikingStorage::find() for FTS5 + TF-IDF + optional embedding search.
|
||||
#[tauri::command]
|
||||
pub async fn memory_search(
|
||||
options: MemorySearchOptions,
|
||||
state: State<'_, MemoryStoreState>,
|
||||
_state: State<'_, MemoryStoreState>,
|
||||
) -> Result<Vec<PersistentMemory>, String> {
|
||||
let state_guard = state.lock().await;
|
||||
let storage = crate::viking_commands::get_storage().await?;
|
||||
|
||||
let store = state_guard
|
||||
.as_ref()
|
||||
.ok_or_else(|| "Memory store not initialized".to_string())?;
|
||||
// Build scope from agent_id
|
||||
let scope = options.agent_id.map(|id| format!("agent://{}/", id));
|
||||
|
||||
let query = MemorySearchQuery {
|
||||
agent_id: options.agent_id,
|
||||
memory_type: options.memory_type,
|
||||
tags: options.tags,
|
||||
query: options.query,
|
||||
min_importance: options.min_importance,
|
||||
limit: options.limit,
|
||||
offset: options.offset,
|
||||
// Build search query
|
||||
let query = options.query.unwrap_or_default();
|
||||
|
||||
let find_options = zclaw_growth::FindOptions {
|
||||
scope,
|
||||
limit: options.limit.or(Some(50)),
|
||||
min_similarity: options.min_importance.map(|i| (i as f32) / 10.0),
|
||||
};
|
||||
|
||||
store.search(query).await
|
||||
let entries = zclaw_growth::VikingStorage::find(storage.as_ref(), &query, find_options).await
|
||||
.map_err(|e| format!("Failed to search memories: {}", e))?;
|
||||
|
||||
// Filter by memory_type if specified
|
||||
let filtered: Vec<PersistentMemory> = if let Some(ref type_filter) = options.memory_type {
|
||||
entries
|
||||
.into_iter()
|
||||
.filter(|e| e.memory_type.to_string().eq_ignore_ascii_case(type_filter))
|
||||
.map(|e| to_persistent(&e))
|
||||
.collect()
|
||||
} else {
|
||||
entries.into_iter().map(|e| to_persistent(&e)).collect()
|
||||
};
|
||||
|
||||
// Apply offset
|
||||
let offset = options.offset.unwrap_or(0);
|
||||
Ok(filtered.into_iter().skip(offset).collect())
|
||||
}
|
||||
|
||||
/// Delete a memory by ID
|
||||
/// Delete a memory by ID (URI)
|
||||
///
|
||||
/// Deletes from VikingStorage only (PersistentMemoryStore is no longer primary).
|
||||
#[tauri::command]
|
||||
pub async fn memory_delete(
|
||||
id: String,
|
||||
state: State<'_, MemoryStoreState>,
|
||||
_state: State<'_, MemoryStoreState>,
|
||||
) -> Result<(), String> {
|
||||
let state_guard = state.lock().await;
|
||||
let storage = crate::viking_commands::get_storage().await?;
|
||||
zclaw_growth::VikingStorage::delete(storage.as_ref(), &id).await
|
||||
.map_err(|e| format!("Failed to delete memory: {}", e))?;
|
||||
|
||||
let store = state_guard
|
||||
.as_ref()
|
||||
.ok_or_else(|| "Memory store not initialized".to_string())?;
|
||||
|
||||
store.delete(&id).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -182,72 +198,158 @@ pub async fn memory_delete(
|
||||
#[tauri::command]
|
||||
pub async fn memory_delete_all(
|
||||
agent_id: String,
|
||||
state: State<'_, MemoryStoreState>,
|
||||
_state: State<'_, MemoryStoreState>,
|
||||
) -> Result<usize, String> {
|
||||
let state_guard = state.lock().await;
|
||||
let storage = crate::viking_commands::get_storage().await?;
|
||||
|
||||
let store = state_guard
|
||||
.as_ref()
|
||||
.ok_or_else(|| "Memory store not initialized".to_string())?;
|
||||
// Find all entries for this agent
|
||||
let options = zclaw_growth::FindOptions {
|
||||
scope: Some(format!("agent://{}/", agent_id)),
|
||||
limit: Some(10000),
|
||||
min_similarity: Some(0.0),
|
||||
};
|
||||
|
||||
store.delete_by_agent(&agent_id).await
|
||||
let entries = zclaw_growth::VikingStorage::find(storage.as_ref(), "", options).await
|
||||
.map_err(|e| format!("Failed to find memories: {}", e))?;
|
||||
let count = entries.len();
|
||||
|
||||
for entry in &entries {
|
||||
zclaw_growth::VikingStorage::delete(storage.as_ref(), &entry.uri).await
|
||||
.map_err(|e| format!("Failed to delete memory: {}", e))?;
|
||||
}
|
||||
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
/// Get memory statistics
|
||||
#[tauri::command]
|
||||
pub async fn memory_stats(
|
||||
state: State<'_, MemoryStoreState>,
|
||||
_state: State<'_, MemoryStoreState>,
|
||||
) -> Result<MemoryStats, String> {
|
||||
let state_guard = state.lock().await;
|
||||
let storage = crate::viking_commands::get_storage().await?;
|
||||
|
||||
let store = state_guard
|
||||
.as_ref()
|
||||
.ok_or_else(|| "Memory store not initialized".to_string())?;
|
||||
// Fetch all entries to compute stats
|
||||
let options = zclaw_growth::FindOptions {
|
||||
scope: None,
|
||||
limit: Some(100000),
|
||||
min_similarity: Some(0.0),
|
||||
};
|
||||
|
||||
store.stats().await
|
||||
let entries = zclaw_growth::VikingStorage::find(storage.as_ref(), "", options).await
|
||||
.map_err(|e| format!("Failed to get memories for stats: {}", e))?;
|
||||
|
||||
let mut by_type: std::collections::HashMap<String, i64> = std::collections::HashMap::new();
|
||||
let mut by_agent: std::collections::HashMap<String, i64> = std::collections::HashMap::new();
|
||||
let mut oldest: Option<String> = None;
|
||||
let mut newest: Option<String> = None;
|
||||
|
||||
for entry in &entries {
|
||||
let type_key = entry.memory_type.to_string();
|
||||
*by_type.entry(type_key).or_insert(0) += 1;
|
||||
|
||||
let agent = entry.uri
|
||||
.strip_prefix("agent://")
|
||||
.and_then(|s| s.split('/').next())
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
*by_agent.entry(agent).or_insert(0) += 1;
|
||||
|
||||
let created = entry.created_at.to_rfc3339();
|
||||
if oldest.as_ref().map_or(true, |o| &created < o) {
|
||||
oldest = Some(created.clone());
|
||||
}
|
||||
if newest.as_ref().map_or(true, |n| &created > n) {
|
||||
newest = Some(created);
|
||||
}
|
||||
}
|
||||
|
||||
let storage_size = entries.iter()
|
||||
.map(|e| e.content.len() + e.keywords.len() * 10)
|
||||
.sum::<usize>() as i64;
|
||||
|
||||
Ok(MemoryStats {
|
||||
total_entries: entries.len() as i64,
|
||||
by_type,
|
||||
by_agent,
|
||||
oldest_entry: oldest,
|
||||
newest_entry: newest,
|
||||
storage_size_bytes: storage_size,
|
||||
})
|
||||
}
|
||||
|
||||
/// Export all memories for backup
|
||||
#[tauri::command]
|
||||
pub async fn memory_export(
|
||||
state: State<'_, MemoryStoreState>,
|
||||
_state: State<'_, MemoryStoreState>,
|
||||
) -> Result<Vec<PersistentMemory>, String> {
|
||||
let state_guard = state.lock().await;
|
||||
let storage = crate::viking_commands::get_storage().await?;
|
||||
|
||||
let store = state_guard
|
||||
.as_ref()
|
||||
.ok_or_else(|| "Memory store not initialized".to_string())?;
|
||||
let options = zclaw_growth::FindOptions {
|
||||
scope: None,
|
||||
limit: Some(100000),
|
||||
min_similarity: Some(0.0),
|
||||
};
|
||||
|
||||
store.export_all().await
|
||||
let entries = zclaw_growth::VikingStorage::find(storage.as_ref(), "", options).await
|
||||
.map_err(|e| format!("Failed to export memories: {}", e))?;
|
||||
|
||||
Ok(entries.into_iter().map(|e| to_persistent(&e)).collect())
|
||||
}
|
||||
|
||||
/// Import memories from backup
|
||||
///
|
||||
/// Converts PersistentMemory entries to VikingStorage MemoryEntry and stores them.
|
||||
#[tauri::command]
|
||||
pub async fn memory_import(
|
||||
memories: Vec<PersistentMemory>,
|
||||
state: State<'_, MemoryStoreState>,
|
||||
_state: State<'_, MemoryStoreState>,
|
||||
) -> Result<usize, String> {
|
||||
let state_guard = state.lock().await;
|
||||
let storage = crate::viking_commands::get_storage().await?;
|
||||
let mut count = 0;
|
||||
|
||||
let store = state_guard
|
||||
.as_ref()
|
||||
.ok_or_else(|| "Memory store not initialized".to_string())?;
|
||||
for memory in &memories {
|
||||
let memory_type = parse_memory_type(&memory.memory_type);
|
||||
let keywords: Vec<String> = serde_json::from_str(&memory.tags).unwrap_or_default();
|
||||
|
||||
store.import_batch(&memories).await
|
||||
let viking_entry = zclaw_growth::MemoryEntry::new(
|
||||
&memory.agent_id,
|
||||
memory_type,
|
||||
&memory.memory_type,
|
||||
memory.content.clone(),
|
||||
)
|
||||
.with_importance(memory.importance.clamp(1, 10) as u8)
|
||||
.with_keywords(keywords);
|
||||
|
||||
// Set overview if present
|
||||
let viking_entry = if let Some(ref overview) = memory.overview {
|
||||
if !overview.is_empty() {
|
||||
viking_entry.with_overview(overview)
|
||||
} else {
|
||||
viking_entry
|
||||
}
|
||||
} else {
|
||||
viking_entry
|
||||
};
|
||||
|
||||
match zclaw_growth::VikingStorage::store(storage.as_ref(), &viking_entry).await {
|
||||
Ok(()) => count += 1,
|
||||
Err(e) => tracing::warn!("[memory_import] Failed to import {}: {}", memory.id, e),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
/// Get the database path
|
||||
///
|
||||
/// Now returns the VikingStorage (SqliteStorage) path.
|
||||
#[tauri::command]
|
||||
pub async fn memory_db_path(
|
||||
state: State<'_, MemoryStoreState>,
|
||||
_state: State<'_, MemoryStoreState>,
|
||||
) -> Result<String, String> {
|
||||
let state_guard = state.lock().await;
|
||||
|
||||
let store = state_guard
|
||||
.as_ref()
|
||||
.ok_or_else(|| "Memory store not initialized".to_string())?;
|
||||
|
||||
Ok(store.path().to_string_lossy().to_string())
|
||||
let storage_dir = crate::viking_commands::get_storage_dir();
|
||||
let db_path = storage_dir.join("memories.db");
|
||||
Ok(db_path.to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
/// Configure embedding for PersistentMemoryStore (chat memory search)
|
||||
@@ -259,7 +361,6 @@ pub async fn memory_configure_embedding(
|
||||
model: Option<String>,
|
||||
endpoint: Option<String>,
|
||||
) -> Result<bool, String> {
|
||||
// Create an llm::EmbeddingClient and wrap it in Arc for the closure
|
||||
let config = crate::llm::EmbeddingConfig {
|
||||
provider,
|
||||
api_key,
|
||||
@@ -282,7 +383,7 @@ pub async fn memory_configure_embedding(
|
||||
|
||||
configure_embedding_client(embed_fn);
|
||||
|
||||
tracing::info!("[MemoryCommands] Embedding configured for PersistentMemoryStore");
|
||||
tracing::info!("[MemoryCommands] Embedding configured");
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
@@ -294,105 +395,33 @@ pub fn memory_is_embedding_configured() -> bool {
|
||||
|
||||
/// Build layered memory context for chat prompt injection
|
||||
///
|
||||
/// Uses SqliteStorage (FTS5 + TF-IDF + Embedding) for high-quality semantic search,
|
||||
/// with fallback to PersistentMemoryStore if Viking storage is unavailable.
|
||||
///
|
||||
/// Performs L0→L1→L2 progressive loading:
|
||||
/// - L0: Search all matching memories (vector similarity when available)
|
||||
/// - L1: Use overview/summary when available, fall back to truncated content
|
||||
/// - L2: Full content only for top-ranked items
|
||||
/// Uses VikingStorage (SqliteStorage) with FTS5 + TF-IDF + optional Embedding.
|
||||
#[tauri::command]
|
||||
pub async fn memory_build_context(
|
||||
agent_id: String,
|
||||
query: String,
|
||||
max_tokens: Option<usize>,
|
||||
state: State<'_, MemoryStoreState>,
|
||||
_state: State<'_, MemoryStoreState>,
|
||||
) -> Result<BuildContextResult, String> {
|
||||
let budget = max_tokens.unwrap_or(500);
|
||||
|
||||
// Try SqliteStorage (Viking) first — has FTS5 + TF-IDF + Embedding
|
||||
let entries = match crate::viking_commands::get_storage().await {
|
||||
Ok(storage) => {
|
||||
let options = zclaw_growth::FindOptions {
|
||||
scope: Some(format!("agent://{}", agent_id)),
|
||||
limit: Some((budget / 25).max(8)),
|
||||
min_similarity: Some(0.2),
|
||||
};
|
||||
let storage = crate::viking_commands::get_storage().await?;
|
||||
|
||||
match zclaw_growth::VikingStorage::find(storage.as_ref(), &query, options).await {
|
||||
Ok(entries) => entries,
|
||||
Err(e) => {
|
||||
tracing::warn!("[memory_build_context] Viking search failed, falling back: {}", e);
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
tracing::debug!("[memory_build_context] Viking storage unavailable, falling back to PersistentMemoryStore");
|
||||
let options = zclaw_growth::FindOptions {
|
||||
scope: Some(format!("agent://{}/", agent_id)),
|
||||
limit: Some((budget / 25).max(8)),
|
||||
min_similarity: Some(0.2),
|
||||
};
|
||||
|
||||
let entries = match zclaw_growth::VikingStorage::find(storage.as_ref(), &query, options).await {
|
||||
Ok(entries) => entries,
|
||||
Err(e) => {
|
||||
tracing::warn!("[memory_build_context] Search failed: {}", e);
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
|
||||
// If Viking found results, use them (they have overview/embedding ranking)
|
||||
if !entries.is_empty() {
|
||||
let mut used_tokens = 0;
|
||||
let mut items: Vec<String> = Vec::new();
|
||||
let mut memories_used = 0;
|
||||
|
||||
for entry in &entries {
|
||||
if used_tokens >= budget {
|
||||
break;
|
||||
}
|
||||
|
||||
// Prefer overview (L1 summary) over full content
|
||||
let overview_str = entry.overview.as_deref().unwrap_or("");
|
||||
let display_content = if !overview_str.is_empty() {
|
||||
overview_str.to_string()
|
||||
} else {
|
||||
truncate_for_l1(&entry.content)
|
||||
};
|
||||
|
||||
let item_tokens = estimate_tokens_text(&display_content);
|
||||
if used_tokens + item_tokens > budget {
|
||||
continue;
|
||||
}
|
||||
|
||||
items.push(format!("- [{}] {}", entry.memory_type, display_content));
|
||||
used_tokens += item_tokens;
|
||||
memories_used += 1;
|
||||
}
|
||||
|
||||
let system_prompt_addition = if items.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("## 相关记忆\n{}", items.join("\n"))
|
||||
};
|
||||
|
||||
return Ok(BuildContextResult {
|
||||
system_prompt_addition,
|
||||
total_tokens: used_tokens,
|
||||
memories_used,
|
||||
});
|
||||
}
|
||||
|
||||
// Fallback: PersistentMemoryStore (LIKE-based search)
|
||||
let state_guard = state.lock().await;
|
||||
let store = state_guard
|
||||
.as_ref()
|
||||
.ok_or_else(|| "Memory store not initialized".to_string())?;
|
||||
|
||||
let limit = budget / 25;
|
||||
let search_query = MemorySearchQuery {
|
||||
agent_id: Some(agent_id.clone()),
|
||||
query: Some(query.clone()),
|
||||
limit: Some(limit.max(20)),
|
||||
min_importance: Some(3),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let memories = store.search(search_query).await?;
|
||||
|
||||
if memories.is_empty() {
|
||||
if entries.is_empty() {
|
||||
return Ok(BuildContextResult {
|
||||
system_prompt_addition: String::new(),
|
||||
total_tokens: 0,
|
||||
@@ -400,24 +429,20 @@ pub async fn memory_build_context(
|
||||
});
|
||||
}
|
||||
|
||||
// Build layered context with token budget
|
||||
let mut used_tokens = 0;
|
||||
let mut items: Vec<String> = Vec::new();
|
||||
let mut memories_used = 0;
|
||||
|
||||
for memory in &memories {
|
||||
for entry in &entries {
|
||||
if used_tokens >= budget {
|
||||
break;
|
||||
}
|
||||
|
||||
let display_content = if let Some(ref overview) = memory.overview {
|
||||
if !overview.is_empty() {
|
||||
overview.clone()
|
||||
} else {
|
||||
truncate_for_l1(&memory.content)
|
||||
}
|
||||
let overview_str = entry.overview.as_deref().unwrap_or("");
|
||||
let display_content = if !overview_str.is_empty() {
|
||||
overview_str.to_string()
|
||||
} else {
|
||||
truncate_for_l1(&memory.content)
|
||||
truncate_for_l1(&entry.content)
|
||||
};
|
||||
|
||||
let item_tokens = estimate_tokens_text(&display_content);
|
||||
@@ -425,7 +450,7 @@ pub async fn memory_build_context(
|
||||
continue;
|
||||
}
|
||||
|
||||
items.push(format!("- [{}] {}", memory.memory_type, display_content));
|
||||
items.push(format!("- [{}] {}", entry.memory_type, display_content));
|
||||
used_tokens += item_tokens;
|
||||
memories_used += 1;
|
||||
}
|
||||
@@ -454,7 +479,7 @@ pub struct BuildContextResult {
|
||||
|
||||
/// Truncate content for L1 overview display (~50 tokens)
|
||||
fn truncate_for_l1(content: &str) -> String {
|
||||
let char_limit = 100; // ~50 tokens for mixed CJK/ASCII
|
||||
let char_limit = 100;
|
||||
if content.chars().count() <= char_limit {
|
||||
content.to_string()
|
||||
} else {
|
||||
|
||||
@@ -80,7 +80,7 @@ pub struct EmbeddingConfigResult {
|
||||
static STORAGE: OnceCell<Arc<SqliteStorage>> = OnceCell::const_new();
|
||||
|
||||
/// Get the storage directory path
|
||||
fn get_storage_dir() -> PathBuf {
|
||||
pub fn get_storage_dir() -> PathBuf {
|
||||
// Use platform-specific data directory
|
||||
if let Some(data_dir) = dirs::data_dir() {
|
||||
data_dir.join("zclaw").join("memories")
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
Zap,
|
||||
Info,
|
||||
} from 'lucide-react';
|
||||
import type { HandRun } from '../store/gatewayStore';
|
||||
import type { HandRun } from '../store/handStore';
|
||||
import { HAND_DEFINITIONS, type HandId } from '../types/hands';
|
||||
|
||||
// === Types ===
|
||||
|
||||
@@ -160,9 +160,9 @@ export async function probeTauriAvailability(): Promise<boolean> {
|
||||
|
||||
// Try to actually invoke a simple command to verify Tauri is working
|
||||
try {
|
||||
// Use a minimal invoke to test - we just check if invoke works
|
||||
await invoke('plugin:tinker|ping');
|
||||
log.debug('probeTauriAvailability: Tauri plugin ping succeeded');
|
||||
// Use kernel_status as a lightweight health check
|
||||
await invoke('kernel_status');
|
||||
log.debug('probeTauriAvailability: kernel_status succeeded');
|
||||
_tauriAvailable = true;
|
||||
return true;
|
||||
} catch {
|
||||
|
||||
@@ -53,10 +53,10 @@
|
||||
|
||||
| 功能 | 文档声称 | 真实完成度 | 差距模式 | 严重度 |
|
||||
|------|----------|-----------|----------|--------|
|
||||
| **Agent 记忆** | L4 (90%) | **L3 (75%)** | 双存储路径使用不同数据库 | HIGH |
|
||||
| **Agent 记忆** | L4 (90%) | **L4 (85%)** | ~~双存储路径使用不同数据库~~ ✅ 已修复 (H3) — 统一到 VikingStorage | ~~HIGH~~ FIXED |
|
||||
| **身份演化** | L2 (70%) | **L2 (65%)** | SOUL.md 注入已验证,但前端回滚 UI 缺失 | MEDIUM |
|
||||
| **反思引擎** | L2 (65%) | **L2 (55%)** | ~~传入空记忆数组~~ ✅ 已修复 (C2);结果仍未反馈到行为 | ~~MEDIUM~~ PARTIAL |
|
||||
| **心跳引擎** | L2 (70%) | **L1 (35%)** | 默认禁用(enabled=false),无持久化,无定时器 | HIGH |
|
||||
| **反思引擎** | L2 (65%) | **L2 (60%)** | ~~传入空记忆数组~~ ✅ 已修复 (C2);~~结果未持久化~~ ✅ 已修复 (M4);UI 展示仍缺失 | ~~MEDIUM~~ PARTIAL |
|
||||
| **心跳引擎** | L2 (70%) | **L1 (40%)** | ~~无持久化~~ ✅ 已修复 (H4);默认禁用(enabled=false),需前端主动启动 | ~~HIGH~~ PARTIAL |
|
||||
| **自主授权** | L2 (75%) | **L2 (60%)** | 前端组件存在但未在执行链路中调用 canAutoExecute | MEDIUM |
|
||||
| **上下文压缩** | L2 (75%) | **L2 (70%)** | 规则压缩已集成,LLM 压缩存在但默认关闭 | LOW |
|
||||
|
||||
@@ -75,9 +75,9 @@
|
||||
|
||||
| 维度 | 文档声称 | 审计结果 | 修复后 |
|
||||
|------|----------|----------|--------|
|
||||
| **整体** | 68% | **~50%** | **~58%** |
|
||||
| **核心可用** | 85% | **75%** | **~82%** |
|
||||
| **真实可用** | 100% | **~55%**(排除模拟实现和 PromptOnly 技能后) | **~70%** |
|
||||
| **整体** | 68% | **~50%** | **~62%** |
|
||||
| **核心可用** | 85% | **75%** | **~85%** |
|
||||
| **真实可用** | 100% | **~55%**(排除模拟实现和 PromptOnly 技能后) | **~72%** |
|
||||
|
||||
---
|
||||
|
||||
@@ -129,7 +129,9 @@
|
||||
- 短期: UI 明确标注"模拟模式",禁用写操作
|
||||
- 中期: 实现 Twitter API v2 或改为 Mastodon API
|
||||
|
||||
#### H3: 记忆系统双存储路径不同步
|
||||
#### H3: 记忆系统双存储路径不同步 ✅ **已修复**
|
||||
- **修复方案**: 完全重写 `memory_commands.rs`,所有操作统一委派到 VikingStorage (SqliteStorage)。移除了 PersistentMemoryStore 的双写逻辑。保留 PersistentMemory 类型作为前端 API 兼容层,内部通过 `to_persistent()` 转换。
|
||||
- **修复文件**: `memory_commands.rs`, `viking_commands.rs`, `memory/mod.rs`
|
||||
- **路径A**: `memory_commands.rs` → `PersistentMemoryStore` → `{app_data_dir}/memory/memories.db`(UI 面板使用)
|
||||
- **路径B**: `intelligence_hooks.rs` → `VikingStorage` → `{data_dir}/zclaw/memories/memories.db`(聊天流程使用)
|
||||
- **证据**: 两个路径使用**不同的数据库文件**。`memory_store()` 第88-94行虽有双写逻辑,但 `memory_search()` 优先用 VikingStorage,`intelligence_hooks` 也只用 VikingStorage。`memory_db_path()` 返回的是 PersistentMemoryStore 的路径。
|
||||
@@ -137,7 +139,9 @@
|
||||
- **影响**: UI 面板存储的记忆可能无法在聊天时被检索到
|
||||
- **解决方案**: 统一到 `zclaw-growth` 的 SqliteStorage(已有 FTS5 + TF-IDF + 可选 embedding)
|
||||
|
||||
#### H4: 心跳引擎无持久化(已在启动时运行)
|
||||
#### H4: 心跳引擎无持久化(已在启动时运行) ✅ **已修复**
|
||||
- **修复方案**: `record_interaction()` 现在通过 `tokio::spawn` 将交互时间戳持久化到 VikingStorage metadata(key: `heartbeat:last_interaction:{agent_id}`)。`heartbeat_init()` 在初始化时调用 `restore_last_interaction()` 从 VikingStorage 恢复上次交互时间,确保 `idle-greeting` 检查在应用重启后仍能正确工作。
|
||||
- **修复文件**: `heartbeat.rs`
|
||||
- **文件**: `desktop/src-tauri/src/intelligence/heartbeat.rs`
|
||||
- **证据**:
|
||||
- `HeartbeatConfig::default()` 中 `enabled: false`(第103行),但 `App.tsx:181` 主动调用 `heartbeat.start()`
|
||||
@@ -214,7 +218,9 @@
|
||||
- **影响**: 无法按 hand 类型筛选审批
|
||||
- **解决方案**: 实现按 hand_name + run_id 联合查找
|
||||
|
||||
#### M4: 反思引擎结果未反馈到行为
|
||||
#### M4: 反思引擎结果未反馈到行为 ✅ **已修复(持久化部分)**
|
||||
- **修复方案**: `reflect()` 完成后通过 `tokio::spawn` 将 ReflectionState 和 ReflectionResult 持久化到 VikingStorage metadata。新增 `restore_state()` 方法在初始化时从 VikingStorage 恢复状态,确保 `conversations_since_reflection` 计数器在重启后保持。`intelligence_hooks` 在首次 `post_conversation_hook` 时通过 `pop_restored_state/result` 消费恢复数据。
|
||||
- **修复文件**: `reflection.rs`, `intelligence_hooks.rs`
|
||||
- **文件**: `desktop/src-tauri/src/intelligence/reflection.rs:190-233`
|
||||
- **证据**: `reflect()` 基于规则检测模式(task ≥ 5、preference ≥ 5、lesson ≥ 5),生成改进建议,但结果仅存入内存 `history: Vec<ReflectionResult>`(最多保留 20 条),不持久化且不用于修改后续行为。**更严重的是,由于 C1 bug,这些结果永远是空的。**
|
||||
- **差距模式**: 传了没存 + 存了没用
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# ZCLAW 功能全景文档
|
||||
|
||||
> **版本**: v0.6.2
|
||||
> **版本**: v0.6.3
|
||||
> **更新日期**: 2026-03-27
|
||||
> **项目状态**: 完整 Rust Workspace 架构,10 个核心 Crates,69 技能,Pipeline DSL + Smart Presentation + Agent Growth System
|
||||
> **整体完成度**: ~58% (基于 2026-03-27 深度审计 + P0/P1 修复后)
|
||||
> **整体完成度**: ~62% (基于 2026-03-27 深度审计 + 两轮修复后)
|
||||
> **架构**: Tauri 桌面应用,Rust Workspace (10 crates) + React 前端
|
||||
>
|
||||
> **审计修复 (2026-03-27)**: 修复 2 个 CRITICAL + 4 个 HIGH + 1 个 MEDIUM 问题,详见 [DEEP_AUDIT_REPORT.md](./DEEP_AUDIT_REPORT.md)
|
||||
> **审计修复 (2026-03-27)**: 修复 2 个 CRITICAL + 6 个 HIGH + 2 个 MEDIUM 问题,详见 [DEEP_AUDIT_REPORT.md](./DEEP_AUDIT_REPORT.md)
|
||||
|
||||
> **重要**: ZCLAW 采用 Rust Workspace 架构,包含 10 个分层 Crates (types → memory → runtime → kernel → skills/hands/protocols/pipeline/growth/channels),所有核心能力集成在 Tauri 桌面应用中
|
||||
|
||||
@@ -38,11 +38,11 @@
|
||||
| [00-agent-memory.md](02-intelligence-layer/00-agent-memory.md) | Agent 记忆 | L3-L4 (90%) | ✅ pre-hook (FTS5+TF-IDF+Embedding) | ✅ SqliteStorage |
|
||||
| [01-identity-evolution.md](02-intelligence-layer/01-identity-evolution.md) | 身份演化 | L2 (70%) | ✅ pre-hook (SOUL.md → system prompt) | ✅ Rust 实现 |
|
||||
| [06-context-compaction.md](02-intelligence-layer/06-context-compaction.md) | 上下文压缩 | L2-L3 (75%) | ✅ 已接入内核 (AgentLoop, LLM 摘要) | ✅ Rust 实现 |
|
||||
| [03-reflection-engine.md](02-intelligence-layer/03-reflection-engine.md) | 自我反思 | L2 (55%) | ✅ post-hook (自动触发 + 真实记忆) | ✅ Rust 实现 |
|
||||
| 心跳巡检 | 心跳巡检 | L2-L3 (70%) | ✅ post-hook (record_interaction) | ✅ Rust 实现 |
|
||||
| [03-reflection-engine.md](02-intelligence-layer/03-reflection-engine.md) | 自我反思 | L2 (60%) | ✅ post-hook (自动触发 + 真实记忆 + 持久化) | ✅ Rust 实现 |
|
||||
| 心跳巡检 | 心跳巡检 | L2-L3 (70%) | ✅ post-hook (record_interaction + VikingStorage 持久化) | ✅ Rust 实现 |
|
||||
| [05-autonomy-manager.md](02-intelligence-layer/05-autonomy-manager.md) | 自主授权 | L2-L3 (75%) | ✅ RightPanel 'autonomy' | ✅ TypeScript |
|
||||
|
||||
> **智能层集成说明** (2026-03-27): 通过 `intelligence_hooks.rs` 将 identity、memory context、heartbeat、reflection 接入 `agent_chat_stream` 流程。compactor 已在内核 AgentLoop 集成 (15k token 阈值)。反思引擎已修复空记忆 bug (C2),现在从 VikingStorage 查询真实记忆进行分析。已清理死代码: pattern_detector、recommender、mesh、persona_evolver、trigger_evaluator。
|
||||
> **智能层集成说明** (2026-03-27): 通过 `intelligence_hooks.rs` 将 identity、memory context、heartbeat、reflection 接入 `agent_chat_stream` 流程。compactor 已在内核 AgentLoop 集成 (15k token 阈值)。反思引擎已修复空记忆 bug (C2),现在从 VikingStorage 查询真实记忆进行分析。反思结果和状态已持久化到 VikingStorage metadata,重启后自动恢复 (H4/M4)。心跳引擎交互记录已持久化,idle-greeting 检查跨重启生效 (H4)。记忆系统已统一到 VikingStorage 单一存储 (H3)。已清理死代码: pattern_detector、recommender、mesh、persona_evolver、trigger_evaluator。
|
||||
|
||||
### 1.4 上下文数据库 (Context Database)
|
||||
|
||||
@@ -289,6 +289,7 @@ skills hands protocols pipeline growth channels
|
||||
|
||||
| 日期 | 版本 | 变更内容 |
|
||||
|------|------|---------|
|
||||
| 2026-03-27 | v0.6.3 | **审计修复 (P1/P2)**: H3 记忆双存储统一到 VikingStorage、H4 心跳引擎持久化 + 启动恢复、M4 反思结果持久化。整体完成度 58%→62%。|
|
||||
| 2026-03-27 | v0.6.2 | **审计修复 (P0/P1)**: C1 PromptOnly LLM 集成、C2 反思引擎空记忆修复、H7 Agent Store 接口适配、H8 Hand 审批检查、M1 幽灵命令注册、H1/H2 demo 标记、H5 归档过时报告。整体完成度 50%→58%。|
|
||||
| 2026-03-27 | v0.6.1 | **功能完整性修复**: 激活 LoopGuard 循环防护、实现 CapabilityManager.validate() 安全验证、handStore/workflowStore KernelClient 适配器、Credits 标注开发中、Skills 动态化、ScheduledTasks localStorage 降级、token 用量追踪 |
|
||||
| 2026-03-27 | v0.6.0a | **全面审计更新**:所有成熟度标注调整为实际完成度 (平均 68%),新增清理记录 |
|
||||
@@ -322,7 +323,16 @@ skills hands protocols pipeline growth channels
|
||||
| TwitterHand demo 标记 | H2 | 添加 `"demo"` 标签到 twitter.rs 和 twitter.HAND.toml |
|
||||
| 归档过时报告 | H5 | VERIFICATION_REPORT.md 顶部添加归档声明 |
|
||||
|
||||
### 7.2 代码清理
|
||||
### 7.2 审计修复 (P1/P2 第二轮)
|
||||
|
||||
| 修复项 | ID | 说明 |
|
||||
|--------|-----|------|
|
||||
| 记忆双存储统一 | H3 | 完全重写 `memory_commands.rs`,统一委派到 VikingStorage,移除 PersistentMemoryStore 双写 |
|
||||
| 心跳引擎持久化 | H4 | `record_interaction()` 持久化到 VikingStorage metadata,`heartbeat_init()` 启动时恢复 |
|
||||
| 反思结果持久化 | M4 | `reflect()` 后持久化 ReflectionState/Result 到 VikingStorage,重启后自动恢复 |
|
||||
| 清理 dead_code warnings | — | PersistentMemoryStore impl 添加 `#[allow(dead_code)]`,移除未使用的 `build_uri` |
|
||||
|
||||
### 7.3 代码清理
|
||||
|
||||
| 清理项 | 说明 |
|
||||
|--------|------|
|
||||
|
||||
Reference in New Issue
Block a user