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 ===
|
||||
|
||||
Reference in New Issue
Block a user