新增: namespace淘汰治理与可恢复会话分组

- namespace 淘汰治理: pin 机制(关键产出 LRU 永不淘汰, 超池 50% 最旧降级) + EvictionLog 留档富提示(原工具名+内容摘要+重调指引) + owner 软隔离(无主最旧先淘); 子代理报告出口 pin 收编
- 错误路径广播 AiAgentFinished(status=error): 错误任务有终态, watcher/任务卡可收敛
- 桌面侧栏可恢复分组: parked 会话独立分组+点击恢复, 断点续跑读侧闭环
- 验收: cargo check 零 error + vue-tsc EXIT 0
This commit is contained in:
lxy
2026-10-06 23:48:45 +08:00
parent 1ce1360a0b
commit ece7cd3fa8
8 changed files with 369 additions and 17 deletions
@@ -152,7 +152,10 @@ pub(super) async fn maybe_auto_compress(
path = %path,
"[namespace] 引用已淘汰,替换为提示文案"
);
m.content = df_ai::namespace_store::EVICTED_PLACEHOLDER.to_string();
m.content = session
.namespace_store
.eviction_rich_notice(&path)
.unwrap_or_else(|| df_ai::namespace_store::EVICTED_PLACEHOLDER.to_string());
}
}
}
+19 -1
View File
@@ -1835,7 +1835,10 @@ async fn prepare_round_inputs(
path = %path,
"[namespace] 引用已淘汰,替换为提示文案"
);
m.content = df_ai::namespace_store::EVICTED_PLACEHOLDER.to_string();
m.content = session
.namespace_store
.eviction_rich_notice(&path)
.unwrap_or_else(|| df_ai::namespace_store::EVICTED_PLACEHOLDER.to_string());
}
}
}
@@ -2704,6 +2707,21 @@ pub(crate) async fn emit_fatal_error(app_handle: &AppHandle, conv_id: &str, erro
error_type: Some(error_type),
conversation_id: Some(conv_id.to_string()),
});
// #23 修复:错误路径也广播 AiAgentFinished(status=error)——原只发 AiError,飞书
// completion_watcher 只消费 AiAgentFinished → 错误任务永不收敛(watcher 空等到二次
// 超时弃守,且无失败终态)。单一事实源在引擎层,订阅方(飞书 watcher/桌面任务卡)各自消费。
app_handle.state::<AppState>().ai_event_bus.publish_event(
AiChatEvent::AiAgentFinished {
agent_id: conv_id.to_string(),
conversation_id: Some(conv_id.to_string()),
summary: format!("任务失败: {}", error.chars().take(300).collect::<String>()),
status: "error".to_string(),
rounds: 0,
tokens: 0,
rejected_tools: None,
stop_kind: Some("error".to_string()),
},
);
// P1 bg 化:失败系统通知(条件发送,失败静默)。正文截断防长堆栈撑爆系统气泡。
let brief: String = error.chars().take(160).collect();
crate::commands::ai::notify::notify_session_event(
+15 -2
View File
@@ -532,7 +532,7 @@ pub(crate) async fn run_sub_agent_loop(
// 统一出口:挂父会话 emit 终态(前端展示二期消费;报告本体经父 tool_result 返回值交付)
emit_ai_chat_event(&app_handle, AiChatEvent::AiAgentFinished {
agent_id: child_conv_id.clone(),
conversation_id: Some(parent_conv_id),
conversation_id: Some(parent_conv_id.clone()),
summary: outcome.summary.clone(),
status: outcome.status.as_str().to_string(),
rounds: outcome.rounds,
@@ -566,8 +566,16 @@ pub(crate) async fn run_sub_agent_loop(
}
// 清理子会话内存条目(轨迹已持久化或已随级联丢弃;origin 标记随 per_conv 一并回收)
// + namespace 治理:报告若经工具结果入库(namespace 引用路径),统一出口 pin 保护
// (store hash 去重:已入库条目原地升级 pinned;未入库则 store_pinned 收编,
// 超大报告也不因父会话 LRU 淘汰丢报告)。owner=父会话,软隔离归当前主。
{
let mut session = session_arc.lock().await;
if df_ai::namespace_store::should_use_namespace(&outcome.summary, "sub_agent_report") {
session.namespace_store.store_pinned(
"sub_agent_report", &outcome.summary, Some(&parent_conv_id),
);
}
session.per_conv.remove(&child_conv_id);
session.save_locks.remove(&child_conv_id);
session.conv_origins.remove(&child_conv_id);
@@ -744,7 +752,12 @@ async fn run_sub_agent_loop_inner(
match session.namespace_store.read_only(&path) {
Some(original) => m.content = original.to_string(),
None => {
m.content = df_ai::namespace_store::EVICTED_PLACEHOLDER.to_string();
// 淘汰治理富提示:Log 命中给原工具名+内容 hint+重调指引,
// 未命中(跨会话残留/未知 key)落回既有占位
m.content = session
.namespace_store
.eviction_rich_notice(&path)
.unwrap_or_else(|| df_ai::namespace_store::EVICTED_PLACEHOLDER.to_string());
}
}
}
+5 -1
View File
@@ -361,7 +361,11 @@ async fn save_conversation_inner(
let session = session_arc.lock().await;
for m in &mut persist_msgs {
if df_ai::namespace_store::is_namespace_ref(&m.content) {
if let Some(original) = session.namespace_store.read_only(&m.content) {
// 淘汰治理富提示:Log 命中给原工具名+hint,未命中原样保留引用
// (落库持久化路径,不落占位文案——占位只在请求组装视图层替换)
if let Some(notice) = session.namespace_store.eviction_rich_notice(&m.content) {
m.content = notice;
} else if let Some(original) = session.namespace_store.read_only(&m.content) {
m.content = original.to_string();
}
}