新增: F-09多会话并发架构B阶段设计文档 + UX-15审查闭环
- F-09 设计文档(docs/02-架构设计/F-09-多会话并发架构设计-2026-06-19.md) 现状核验+设计选项+实施路径(阶段2批1-8)+风险R1-9+验收清单 - 核验: A路线补漏已落地(阶段1跳过), AiSession 12字段 - 待决策: b-1 messages per-conv(技术必然,偏离决策b原文待拍板) + c-1 global=3+Settings + e-1 save原路径(主代定) - CR-260619-02 UX-15第一批 审查回填 PASS
This commit is contained in:
605
docs/02-架构设计/F-09-多会话并发架构设计-2026-06-19.md
Normal file
605
docs/02-架构设计/F-09-多会话并发架构设计-2026-06-19.md
Normal file
@@ -0,0 +1,605 @@
|
||||
# F-260616-09 AiSession 多会话并发架构 — B 阶段设计(草案)
|
||||
|
||||
> **状态**:设计草案(2026-06-19) | **范围**:只读分析 + 设计文档,**不改源码**
|
||||
> **关联**:[F-09B-多会话并发设计-2026-06-16.md](./F-09B-多会话并发设计-2026-06-16.md)(旧版,行号已过期,本文为 06-19 更新核验版)
|
||||
> **决策基线**(2026-06-16 已决·待归档):
|
||||
> - **a**:A 隔离修复已做(06-19 核验:A 路线补漏已落地) + B 立项 🔥 高优先尽快实施
|
||||
> - **b**:形态 — 仅拆 `generating`/`stop_flag` per-conv,messages/pending 按 conv reload(⚠️ 06-19 核验修正:messages 必须同 per-conv,见 §2.1.1)
|
||||
> - **c**:并发上限 — 复用 `llm_concurrency.global` 改「并发会话数」语义
|
||||
> - **d**:UI — d1 单面板侧栏切换 + d2 独立 Tauri 窗口都做(已决先 d1 后 d2)
|
||||
> - **e**:旧 loop 切换处理 — 不退出各自跑完(真并发,⚠️ 依赖 b 修正)
|
||||
>
|
||||
> **审查防污染铁律**:本文 file:line 均为 2026-06-19 独立 grep/read 核验源码当前形态,**不沿用 06-16 旧文档结论**。
|
||||
|
||||
---
|
||||
|
||||
## 0. 与 06-16 旧文档的差异(为什么重写)
|
||||
|
||||
06-16 旧文档结构完整但行号已全部过期(代码改动较多)。06-19 重新核验关键差异:
|
||||
|
||||
| 项目 | 06-16 文档说法 | 06-19 核验当前形态 |
|
||||
|---|---|---|
|
||||
| `AiSession` 字段 | mod.rs:166-216(10 字段) | mod.rs:282-350(12 字段,新增 `notify`、`model_override`、`session_trust`) |
|
||||
| 单例锚点 | state.rs:165 | state.rs:227(`pub ai_session: Arc<Mutex<AiSession>>`) |
|
||||
| init | state.rs:216 | state.rs:278 `AiSession::new()` |
|
||||
| ai_chat_send 互斥 | commands.rs:144-152 | commands.rs:78-88(签名已改,conv_id 是参数) |
|
||||
| ai_conversation_create 漏清 | 旧文标"未补" | **已补** stop_flag(line 1404)+ agent_language(line 1405)+ model_override(line 1407)+ session_trust(line 1409)(F-260616-09 A 路线已落地) |
|
||||
| 前端 newConversation 漏清 | 旧文标"未补" | **已补** queue/generatingConvId/agentRound/searchQuery(useAiConversations.ts:80-83) |
|
||||
| B-260615-11 退出点 | agentic.rs:197-211 等 | agentic.rs:486-500(每轮)/ 786-793(MidStream)/ 837-844(push 前) |
|
||||
| `notify` 字段 | 旧文未独立列出 | mod.rs:309-319 独立字段(B-260615-14 即时停止唤醒,stream_recv select! 监听) |
|
||||
| `iteration_used` 语义 | "累计计数" | mod.rs:320-331 双语义(审批续跑累计 / 达 max 续跑重计 / 新消息 0),已预留"per-conv"注释 |
|
||||
| 前端 events 路由 | useAiEvents.ts:125-140 | useAiEvents.ts:125-144(同款,isCurrent 路由就绪) |
|
||||
|
||||
**核心结论**:06-16 设计骨架(b/c/d/e 决策)依然成立,本文更新行号 + 补 06-16 后的演进(`notify`/`model_override`/`session_trust` 字段、SW-260618-02 finalize_pending_placeholders、A 路线补漏已落),并明确 ⚠️ 待裁决点。
|
||||
|
||||
---
|
||||
|
||||
## 1. 现状梳理(独立核验)
|
||||
|
||||
### 1.1 单例结构与字段
|
||||
|
||||
**单例锚点**:
|
||||
|
||||
```rust
|
||||
// src/state.rs:227
|
||||
pub ai_session: Arc<Mutex<AiSession>>,
|
||||
// src/state.rs:278(init)
|
||||
ai_session: Arc::new(Mutex::new(AiSession::new())),
|
||||
```
|
||||
|
||||
整个应用一份 `AiSession`,所有 IPC 命令竞争同一把 `tokio::Mutex`。
|
||||
|
||||
**`AiSession` 字段**(src/commands/ai/mod.rs:282-350,共 12 字段):
|
||||
|
||||
| 字段 | 行 | 类型 | 多会话语义 | B 阶段处理 |
|
||||
|---|---|---|---|---|
|
||||
| `messages` | 284 | `ContextManager` | **会话级**(每对话一份历史) | ⚠️ 必须拆 per-conv(见 §2.1.1) |
|
||||
| `active_provider_id` | 286 | `Option<String>` | 应用级(provider 全局共享) | 保持单例 |
|
||||
| `active_conversation_id` | 288 | `Option<String>` | 应用级路由键(当前展示的 conv) | 保持单例(切走即变,不影响后台 loop) |
|
||||
| `active_conv_created_at` | 290 | `Option<String>` | 会话级(懒创建时间戳) | 入 PerConvState |
|
||||
| `pending_approvals` | 302 | `HashMap<tool_call_id, PendingApproval>` | 已带 `conversation_id`(mod.rs:412) | 保持单层 + retain 过滤 |
|
||||
| `generating` | 304 | `bool` | 会话级(单例=全局互斥) | **核心拆点** 入 PerConvState |
|
||||
| `agent_language` | 306 | `Option<String>` | 会话级 | 入 PerConvState |
|
||||
| `stop_flag` | 308 | `Arc<AtomicBool>` | 会话级 | **核心拆点** 入 PerConvState |
|
||||
| `notify` | 319 | `Arc<tokio::sync::Notify>` | 会话级(B-260615-14 即时唤醒) | 入 PerConvState(随 stop_flag 配对) |
|
||||
| `iteration_used` | 331 | `usize` | 会话级(mod.rs:320-331 注释已标"F-09 B 改 per-conv") | 入 PerConvState |
|
||||
| `model_override` | 344 | `Option<String>` | 会话级(F-01 阶段6 主对话指定模型) | 入 PerConvState |
|
||||
| `session_trust` | 349 | `HashSet<TrustKey>` | 会话级(AE-2025-04,不跨对话) | 入 PerConvState |
|
||||
|
||||
**关键观察**:`mod.rs:320-331` 注释自身已写「单例一份:当前 AiSession 是全局单例(F-09 B 多会话架构落地时改 per-conv)」——本设计是早已预留的债。
|
||||
|
||||
### 1.2 切换软隔离(readonly)实现
|
||||
|
||||
**`ai_conversation_switch`**(src/commands/ai/commands.rs:1454-1522):
|
||||
|
||||
- 生成中(`session.generating` 为 true,commands.rs:1474)→ 直接 `return Ok(...{ readonly: true })`(commands.rs:1475-1481):**只返回目标对话的 messages 给前端展示,不改 session 状态**。
|
||||
- 非生成中(commands.rs:1482-):正常切 `active_conversation_id` + `messages.restore_from_messages` + retain pending_approvals + 清 model_override/session_trust。
|
||||
|
||||
**这是单例下的不得已妥协**:active_conversation_id 是 loop 写消息的路由键(B-260615-11 校验点),切走即会让旧 loop 退出;故生成中只能 readonly,无法真切换。
|
||||
|
||||
**B 阶段改造**:删除 readonly 分支,切走直接改 `active_conversation_id`(应用级路由键),旧 conv 的 per-conv state 保留,后台 loop 继续跑(决策 e)。
|
||||
|
||||
### 1.3 B-260615-11 旧 loop 退出逻辑(单例下防污染)
|
||||
|
||||
**三处 push 前一致性校验**(全部读单例 `active_conversation_id`):
|
||||
|
||||
| 文件:行 | 触发点 | 当前行为 |
|
||||
|---|---|---|
|
||||
| `agentic.rs:486-500` | 每轮 loop 开始 | `if session.active_conversation_id != conv_id { return }` + `iteration_used = iteration + 1` |
|
||||
| `agentic.rs:786-793` | MidStream 保文 push 前 | 同款校验 + return |
|
||||
| `agentic.rs:837-844` | stream 后正常 push 前 | 同款校验 + return |
|
||||
|
||||
**当前语义**:loop 持快照 `conv_id`(入参),每轮查单例 `active_conversation_id`,不一致即 return 退出 loop。
|
||||
|
||||
**决策 e 真并发下**:旧 loop 跑的是自己的 conv,若 messages/stop_flag 已 per-conv,旧 loop 写自己的 conv 不污染他人,该退出校验**反成阻碍**——B 阶段须改为「conv 是否仍存在」判据(被删则退出)。
|
||||
|
||||
### 1.4 `llm_concurrency` per_conv 当前语义(已退化为单对话内并发)
|
||||
|
||||
**`state.rs:84-193` LlmConcurrency 结构核验**:
|
||||
|
||||
```rust
|
||||
// state.rs:106-113
|
||||
pub struct LlmConcurrency {
|
||||
global: Arc<Mutex<Arc<Semaphore>>>, // state.rs:108
|
||||
per_conv: Arc<Mutex<Arc<Semaphore>>>, // state.rs:109 — 应用级单信号量(非 HashMap)
|
||||
per_provider: Arc<Mutex<HashMap<String, Arc<Semaphore>>>>, // state.rs:112(F-260614-04c)
|
||||
}
|
||||
```
|
||||
|
||||
**state.rs:94-98 注释自陈**(关键证据):
|
||||
|
||||
> per_conv 当前是应用级单一信号量(非 per-conv map)。因 AiSession 为单例 + generating 互斥,同一时刻仅一个对话的 loop 在跑,per_conv 退化为"单对话内并发"(主循环 stream_llm + 标题生成 + 知识提炼)。**未来若支持多对话并发,需改为 `HashMap<conv_id, Semaphore>`。**
|
||||
|
||||
**初始化**:`state.rs:283` `LlmConcurrency::new(3, 2)` —— global=3 / per_conv=2。
|
||||
|
||||
**acquire 路径**(state.rs:125-134):`acquire_global()` 和 `acquire_per_conv()` 均是 lock 内层 Arc + clone + acquire_owned。
|
||||
|
||||
**loop 内 permit 持有**(agentic.rs:659-660):
|
||||
|
||||
```rust
|
||||
let _global_permit = llm_concurrency.acquire_global().await;
|
||||
let _per_conv_permit = llm_concurrency.acquire_per_conv().await;
|
||||
```
|
||||
|
||||
重试期间(agentic.rs:207-303 `stream_one_provider` 内)持有两 permit 不释放,stream 后(agentic.rs:824-825)显式 drop。**F-260616-12 依赖项**:重试期持 permit 阻塞其他会话入槽。
|
||||
|
||||
**决策 c 改造方向**:global 语义从「LLM 调用并发上限」改为「并发会话数上限」(每 loop 入口 acquire 1 整 loop 持有);per_conv 改 `HashMap<conv_id, Semaphore>`。
|
||||
|
||||
### 1.5 残留 bug 核验(A 阶段补漏状态)
|
||||
|
||||
**后端 `ai_conversation_create` 漏清字段**(src/commands/ai/commands.rs:1347-1412):
|
||||
|
||||
- 06-16 文档标"漏清 agent_language/stop_flag" —— **06-19 核验已补**:
|
||||
- line 1404 `session.stop_flag.store(false, Ordering::SeqCst)`
|
||||
- line 1405 `session.agent_language = None`
|
||||
- line 1407 `session.model_override = None`
|
||||
- line 1409 `session.session_trust.clear()`
|
||||
- A 路线补漏注释 line 1401-1409 明确:「F-260616-09(A 路线):补漏清字段维持单例软隔离」
|
||||
- **后端 A 路线补漏已完成,B 阶段此处仅剩「删生成中强制结束旧 loop 块」(commands.rs:1360-1379)与决策 e 的冲突**(见 §5.2)
|
||||
|
||||
**前端 `useAiConversations.ts:newConversation` 漏清字段**(src/composables/ai/useAiConversations.ts:69-86):
|
||||
|
||||
- 06-16 文档标"漏清 queue/generatingConvId/agentRound/searchQuery" —— **06-19 核验已补**:
|
||||
- line 80 `state.queue = []`
|
||||
- line 81 `state.generatingConvId = null`
|
||||
- line 82 `state.agentRound = 0`
|
||||
- line 83 `state.searchQuery = ''`
|
||||
- 注释 line 77-79 明确:「F-260616-09(A 路线):补漏清字段维持单例软隔离」
|
||||
- **前端 A 路线补漏已完成**
|
||||
|
||||
**结论**:A 路线补漏全部落地,**阶段 1(A 隔离残留 bug 修复)实际已无残留 bug**,B 阶段可直接进入阶段 2(多会话并发核心改造)。
|
||||
|
||||
### 1.6 前端事件路由(已就绪)
|
||||
|
||||
**`useAiEvents.ts:handleEvent`**(src/composables/ai/useAiEvents.ts:124-366):
|
||||
|
||||
```typescript
|
||||
// line 125
|
||||
const convId = event.conversation_id
|
||||
// line 133
|
||||
const isCurrent = !convId || convId === state.activeConversationId
|
||||
// line 134-140
|
||||
if (!isCurrent) {
|
||||
if (event.type === 'AiCompleted' || event.type === 'AiError') {
|
||||
state.generatingConvId = null
|
||||
void loadConversations()
|
||||
}
|
||||
return // 非当前会话事件不污染当前视图
|
||||
}
|
||||
```
|
||||
|
||||
**事件按 conversation_id 路由就绪**:非当前会话事件只刷侧栏不污染视图。**这是 B 阶段 d1(侧栏切换 + 后台并行)的前置保障——零改动可用**。
|
||||
|
||||
**AiChatEvent 11 变体 conversation_id 全覆盖**(mod.rs:92-157):每个变体都有 `conversation_id: Option<String>` 字段,后端 emit 全部传 `Some(conv_id)`(agentic.rs 多处 emit 均核验:`Some(conv_id.clone())`)。
|
||||
|
||||
### 1.7 前端 ai_is_generating 单值耦合
|
||||
|
||||
**`commands.rs:ai_is_generating`**(src/commands/ai/commands.rs:156-159):
|
||||
|
||||
```rust
|
||||
pub async fn ai_is_generating(state: State<'_, AppState>) -> Result<bool, String> {
|
||||
let session = state.ai_session.lock().await;
|
||||
Ok(session.generating) // 返回全局单 bool
|
||||
}
|
||||
```
|
||||
|
||||
**消费点**:
|
||||
- 前端 `useAiSend.ts:314-321` sendMessage 发送前查后端真值(`backendGenerating = await invoke('ai_is_generating')`)
|
||||
- 前端 `useAiWindow.ts:121` `restoreGeneratingState` 核对后端真值(分离窗口接管生成态)
|
||||
|
||||
**B 阶段必须打破**:改 `ai_is_generating(conv_id) -> bool`,前端两处调用传 conv_id。否则多会话下,查 A 的生成态会读到 B 的 generating 单值,误判阻塞。
|
||||
|
||||
### 1.8 detached 窗口基础(useAiWindow.ts)
|
||||
|
||||
**`WebviewWindow('ai-detached', ...)`**(src/composables/ai/useAiWindow.ts:43-52):单 label 单窗口。
|
||||
|
||||
- `WebviewWindow.getByLabel('ai-detached')`(useAiWindow.ts:23):复用已存在窗口
|
||||
- detach 时快照 `df-ai-gen` / `df-ai-text` 到 localStorage(useAiWindow.ts:30-31)
|
||||
- `resumeInDetached`(useAiWindow.ts:151-156):核验 `ai_is_generating` 后恢复
|
||||
- 注释 useAiWindow.ts:33-34:「主窗口 state 与分离窗口 state 独立(各自 webview 独立 JS context)」
|
||||
|
||||
**d2 多窗口扩展点**:label 改 `ai-detached-${convId}` 支持每会话独立窗口。
|
||||
|
||||
---
|
||||
|
||||
## 2. 设计选项 + 推荐
|
||||
|
||||
### 2.1 b 形态:HashMap 整体 vs 拆字段 per-conv
|
||||
|
||||
#### 2.1.1 ⚠️ 决策点 b-1:messages 是否 per-conv(偏离决策 b 原文,须用户裁决)
|
||||
|
||||
**决策 b 原文**:「仅拆 `generating`/`stop_flag` per-conv,messages/pending 按 conv reload」。
|
||||
|
||||
**06-19 核验结论**:**messages 必须同 per-conv**,否则决策 e「不退出各自跑完」落空。
|
||||
|
||||
**证据链**:
|
||||
1. 单例 `messages` 下,旧 loop(已切走的 conv A)继续跑会 `session.messages.push(...)`(agentic.rs:858/799/863)——push 到**单例 messages**(此时已被新 active conv B 的 `restore_from_messages` 覆盖过)。
|
||||
2. 这正是 §1.3 B-260615-11 三处退出校验防的污染:loop 写自己的 conv 但单例 messages 被新 conv 覆盖了 → 写错地方。
|
||||
3. 若 messages 保持单例,旧 loop 退出校验**必须保留**(否则污染);但决策 e 要求切换不退出,矛盾。
|
||||
4. 若 messages per-conv(`session.conv(&conv_a).messages.push(...)`),旧 loop 写自己的 conv 不污染 B,退出校验可改为「conv 存在性」判据,决策 e 成立。
|
||||
|
||||
**侵入面增量评估**:极小。`messages` 本就是 `ContextManager` 自包含结构(mod.rs:284),挪进 HashMap 即可;`restore_from_messages` 路径(switch 时从 DB reload)在 per-conv 下变为「建/已存在则跳过」(决策 e 下后台 conv 的 messages 不应被 reload 覆盖)。
|
||||
|
||||
**推荐**:**修正决策 b 为「拆 `generating`/`stop_flag`/`notify`/`iteration_used`/`agent_language`/`model_override`/`session_trust`/`messages` 全 per-conv,仅 `pending_approvals` 保持单层 HashMap(已带 conversation_id 符合决策 b 原意)」**。**此偏离决策 b 原文,列入待决策.md 须用户拍板**。
|
||||
|
||||
#### 2.1.2 b 形态数据结构推荐
|
||||
|
||||
```rust
|
||||
// src/commands/ai/mod.rs 新增
|
||||
pub struct PerConvState {
|
||||
pub messages: ContextManager,
|
||||
pub generating: bool,
|
||||
pub stop_flag: Arc<AtomicBool>,
|
||||
pub notify: Arc<tokio::sync::Notify>,
|
||||
pub iteration_used: usize,
|
||||
pub agent_language: Option<String>,
|
||||
pub model_override: Option<String>,
|
||||
pub session_trust: HashSet<TrustKey>,
|
||||
pub created_at: Option<String>, // 懒创建时间戳(随 active_conv 走)
|
||||
}
|
||||
|
||||
pub struct AiSession {
|
||||
// ── 应用级(保持单例)──
|
||||
pub active_provider_id: Option<String>,
|
||||
pub active_conversation_id: Option<String>, // 路由键:当前展示的 conv
|
||||
// ── 会话级(per-conv)──
|
||||
pub pending_approvals: HashMap<String, PendingApproval>, // 单层(已带 conversation_id)
|
||||
pub per_conv: HashMap<String, PerConvState>,
|
||||
}
|
||||
```
|
||||
|
||||
**访问器收敛(避免 41 处 lock 各自写 HashMap 索引)**:
|
||||
|
||||
```rust
|
||||
impl AiSession {
|
||||
/// 取某 conv 的 PerConvState(不存在则惰性创建)
|
||||
pub fn conv(&mut self, conv_id: &str) -> &mut PerConvState { ... }
|
||||
/// 只读快照(供 IPC 查询,不创建)
|
||||
pub fn conv_read(&self, conv_id: &str) -> Option<&PerConvState> { ... }
|
||||
}
|
||||
```
|
||||
|
||||
**锁粒度推荐**:维持单一 `Arc<Mutex<AiSession>>`(不拆 per-conv 细锁):
|
||||
- loop 内锁持有时间极短(push 一条 / 查 stop_flag 均 O(1)),真实竞争窗口小
|
||||
- stream_llm 期间不持锁(agentic.rs:631-637 build messages 后释放锁,stream 内无锁)
|
||||
- per-conv 细锁理论并发更好,但死锁风险高(锁顺序难保证 + pending_approvals 单层 HashMap 锁域交叉)
|
||||
- 后续若 profiling 显示锁竞争,再细化到 per-conv Mutex(渐进路径)
|
||||
|
||||
### 2.2 c 并发上限:复用 `llm_concurrency.global`
|
||||
|
||||
**当前 global**:permits=3(state.rs:283 `LlmConcurrency::new(3, 2)`),限**全局 LLM 调用并发**。
|
||||
|
||||
**B 阶段新语义**:permits = **并发会话数上限**(默认 3)。每会话 loop 入口 `acquire_global()` 拿 1 permit,持有整个 loop 生命周期(含工具执行/审批等待/重试)。
|
||||
|
||||
- 含义:同一时刻最多 3 个对话并发跑 loop,第 4 个排队(acquire_global await 阻塞)。
|
||||
- 收益:token 暴增护栏(决策 c 原意)。
|
||||
- 实现:`run_agentic_loop` 入口 `let _conv_permit = llm_concurrency.acquire_global().await;`,permit 绑 guard Drop 释放。
|
||||
|
||||
**per_conv Semaphore 改 HashMap**:
|
||||
|
||||
```rust
|
||||
per_conv: Arc<Mutex<HashMap<String, Arc<Semaphore>>>>,
|
||||
```
|
||||
|
||||
每对话内限流不变(permits=2,主循环 + 标题 + 提炼),按 conv_id 各自一份。
|
||||
|
||||
**`acquire_per_conv(conv_id)`**:lock HashMap → 若无则建(permits=2)→ clone Arc → 释放 lock → acquire_owned。
|
||||
|
||||
**conv 退出清理**:loop 结束 + 无 pending 审批时,remove 该 conv 的 Semaphore 条目(防 HashMap 无限增长)。
|
||||
|
||||
**⚠️ 决策点 c-1**:global permits=3 是否合理?多会话并发下 3 个会话同时跑 LLM,token 成本是单会话的 ~3 倍。是否需调低默认(如 2)或暴露 Settings 配置?**建议保持默认 3,Settings 加「并发会话数上限」数字配置项**,列入待决策.md。
|
||||
|
||||
### 2.3 d UI:d1 侧栏 + d2 独立窗口(已决先 d1 后 d2)
|
||||
|
||||
**d1 单面板侧栏切换 + 后台并行**(先做):
|
||||
- 现状基础已就绪:useAiEvents.ts:133-140 事件按 conversation_id 路由不污染当前视图
|
||||
- 改动小:侧栏会话项显示生成态指示器 + state 加 `generatingConvs: Set<string>` 替代单值 generatingConvId
|
||||
- 不需要多窗口、状态同步、localStorage 跨窗口
|
||||
|
||||
**d2 每会话独立 Tauri 窗口**(后做):
|
||||
- 现状基础:useAiWindow.ts `WebviewWindow('ai-detached')` 单 label + getByLabel 复用
|
||||
- 改动:label 改 `ai-detached-${convId}` + detachPanel 加 conv_id 参数 + localStorage 多 key + 窗口间状态同步
|
||||
|
||||
**实施顺序推荐**:d1 先(纯前端 + 后端 §2-§5 改造的最小 UI 呈现层)→ 验证后台并行成立 → d2 后(独立窗口扩展,风险更高:多窗口内存、窗口生命周期管理)。
|
||||
|
||||
**理由**:d1 先做可让后端 per-conv 改造有最小验证界面(双会话并发验收),d2 风险点多(多 webview 内存 + 跨窗口同步)放后面。
|
||||
|
||||
### 2.4 e 旧 loop 切换处理:不退出各自跑完(真并发)
|
||||
|
||||
**决策 e 原文**:切换不退出旧 loop,各自跑完(真并发)。
|
||||
|
||||
**前置依赖**:必须 b-1 决策(messages per-conv)成立,否则单例 messages 下旧 loop 写 push 必污染新 active conv(见 §2.1.1)。
|
||||
|
||||
**改造点**(见 §4.1):agentic.rs:486-500/786-793/837-844 三处退出校验改为「conv 是否仍存在」判据(被删则退出),而非 `active_conversation_id != conv_id`。
|
||||
|
||||
**⚠️ 决策点 e-1**:旧 loop 跑完后的 save_conversation 是否仍走原路径?原路径(agentic.rs:474/807/874/928/958)save 传 conv_id,与 active 无关,**零改动可用**(save_conversation 签名接 conv_id,见 conversation.rs)。**推荐保持原路径**,列入待决策.md 备案。
|
||||
|
||||
---
|
||||
|
||||
## 3. 实施路径(分阶段,每阶段文件 + 风险)
|
||||
|
||||
### 阶段 1:A 隔离残留 bug 修复 — **实际已完成,无工作**
|
||||
|
||||
**核验结论**(§1.5):A 路线补漏全部落地:
|
||||
- 后端 ai_conversation_create 漏清 stop_flag/agent_language/model_override/session_trust — **已补**(commands.rs:1404-1409)
|
||||
- 前端 newConversation 漏清 queue/generatingConvId/agentRound/searchQuery — **已补**(useAiConversations.ts:80-83)
|
||||
|
||||
**阶段 1 实际无残留 bug**,B 阶段直接进入阶段 2。**列为待决策.md 备案项**:阶段 1 完成确认。
|
||||
|
||||
### 阶段 2:B 多会话并发核心 — **B 阶段主体**
|
||||
|
||||
**主改文件**:
|
||||
- `src/commands/ai/mod.rs`:新增 `PerConvState` struct + AiSession 字段迁移 + `conv()`/`conv_read()` 访问器
|
||||
- `src/commands/ai/commands.rs`:41+ 处 `session.lock()` 调用点迁移到 `conv(&conv_id)` 索引(分小批 commit)
|
||||
- `src/commands/ai/agentic.rs`:GeneratingGuard 加 conv_id 字段 + stop_flag/notify/messages 取用改 per-conv + §4 三处退出校验改 conv 存在性 + try_continue_agent_loop 改 per-conv 续跑
|
||||
- `src/commands/ai/audit.rs`:restore_pending_approvals 启动重建路径适配
|
||||
- `src/commands/ai/conversation.rs`:save_conversation 签名接 conv_id(已是,零改动)
|
||||
- `src/state.rs`:`LlmConcurrency` global 语义改会话级 + per_conv 改 HashMap
|
||||
|
||||
**关键改造**(详 §4):
|
||||
|
||||
1. PerConvState 数据结构 + 访问器(批 1,纯重构,无行为变化)
|
||||
2. 41 处 lock 调用点迁移到 conv() 索引(批 2,高风险批,单会话全功能回归)
|
||||
3. run_agentic_loop per-conv 改造(批 3,GeneratingGuard + 三处退出校验 + try_continue)
|
||||
4. commands.rs 切换/新建/停止/发送改 per-conv(批 4,IPC 签名变更)
|
||||
5. llm_concurrency 改造 + F-260616-12 处理(批 5)
|
||||
|
||||
**风险**:
|
||||
- R-1(b-1 决策偏离,messages 必须同 per-conv)——须用户确认
|
||||
- R-2(单 Mutex 多 conv 抢锁,loop 内锁持有短,可控)
|
||||
- R-3(事件路由已就绪,新增 emit 须核对 conv_id)
|
||||
- R-5(回归面广,批 2/4 高风险,逐命令人工验收)
|
||||
- R-6(F-260616-12 retry 持 permit,global 语义切换后核验)
|
||||
- R-8(iteration_used 跨审批续跑在多会话下正确性,挪入 PerConvState)
|
||||
- R-9(PendingApproval.conversation_id 一致性,None 视为无主审批归类)
|
||||
|
||||
**验收**:双会话并发(开 A 跑 → 切 B 发 → A 后台跑完不污染 B)+ 单会话全功能回归。
|
||||
|
||||
### 阶段 3:UI d1 侧栏切换 + 后台并行
|
||||
|
||||
**主改文件**:
|
||||
- `src/stores/ai.ts`:新增 `generatingConvs: Set<string>` 替代单值 generatingConvId(line 66)
|
||||
- `src/composables/ai/useAiEvents.ts`:line 142-144 写入逻辑改 Set + 各清零点(line 136/273/317/342)全改
|
||||
- `src/composables/ai/useAiConversations.ts`:switch/newConversation 切走不清零他 conv 的生成态
|
||||
- 前端侧栏组件(Sidebar.vue / 会话列表项):显示生成态指示器(读 `generatingConvs.has(conv.id)`)
|
||||
- `src/composables/ai/useAiSend.ts`:line 314-321 `ai_is_generating` 改传 conv_id
|
||||
|
||||
**风险**:
|
||||
- R-4(前端 generatingConvId 单值 → Set,漏改清零点致残留)
|
||||
|
||||
**验收**:双会话并发 UI(侧栏见两 conv 流式指示器,切走后台继续)。
|
||||
|
||||
### 阶段 4:UI d2 独立 Tauri 窗口
|
||||
|
||||
**主改文件**:
|
||||
- `src/composables/ai/useAiWindow.ts`:
|
||||
- `detachPanel(convId)` 加 conv_id 参数
|
||||
- WebviewWindow label 改 `ai-detached-${convId}`(line 43)
|
||||
- getByLabel 查重改按 convId 查(line 23)
|
||||
- localStorage 多 key(`df-ai-gen-${convId}` / `df-ai-text-${convId}`)
|
||||
- 窗口间状态同步(Tauri `emit` 自定义事件 + 各窗口 listen)
|
||||
- 侧栏组件:每会话加「在新窗口打开」按钮(右键菜单或图标)
|
||||
- `src/composables/ai/useAiSend.ts` / `useAiConversations.ts`:窗口内操作路由到正确 conv_id
|
||||
|
||||
**风险**:
|
||||
- 多窗口 + 多 webview 内存占用(每窗口一份 Vue app + store)
|
||||
- 窗口生命周期管理(关窗是否停 loop?决策 e 下不停,但须确认 UI 提示)
|
||||
- 跨窗口 provider 变更同步(自定义事件桥接)
|
||||
|
||||
**验收**:每会话独立窗口 + 多窗口并发 + 窗口间状态同步。
|
||||
|
||||
---
|
||||
|
||||
## 4. 关键改造点(代码级,实施时参照)
|
||||
|
||||
### 4.1 B-260615-11 陈旧 loop 退出逻辑改 conv 存在性
|
||||
|
||||
**改造前**(agentic.rs:486-500):
|
||||
|
||||
```rust
|
||||
let mut session = session_arc.lock().await;
|
||||
if session.active_conversation_id.as_deref() != Some(conv_id.as_str()) {
|
||||
tracing::warn!("[ai] 对话已切换,旧 loop 退出(B-260615-11)避免污染新对话");
|
||||
return;
|
||||
}
|
||||
session.iteration_used = iteration + 1;
|
||||
```
|
||||
|
||||
**改造后**(决策 e 真并发):
|
||||
|
||||
```rust
|
||||
{
|
||||
let mut session = session_arc.lock().await;
|
||||
if !session.per_conv.contains_key(&conv_id) {
|
||||
tracing::warn!(conv_id = %conv_id, "[ai] conv 已删除,旧 loop 退出");
|
||||
return;
|
||||
}
|
||||
session.conv(&conv_id).iteration_used = iteration + 1;
|
||||
}
|
||||
```
|
||||
|
||||
同样改造 agentic.rs:786-793(MidStream 保文后 push)/837-844(stream 后 push):三处 push 前校验**全部改为 conv 存在性**而非 active 一致性。
|
||||
|
||||
### 4.2 stop_flag/notify/messages 取用改 per-conv 索引
|
||||
|
||||
**改造前**(agentic.rs:446-449):
|
||||
|
||||
```rust
|
||||
let (stop_flag, notify) = {
|
||||
let session = session_arc.lock().await;
|
||||
(session.stop_flag.clone(), session.notify.clone())
|
||||
};
|
||||
```
|
||||
|
||||
**改造后**:
|
||||
|
||||
```rust
|
||||
let (stop_flag, notify) = {
|
||||
let session = session_arc.lock().await;
|
||||
let conv = session.per_conv.get(&conv_id).expect("loop 启动前 conv 已建");
|
||||
(conv.stop_flag.clone(), conv.notify.clone())
|
||||
};
|
||||
```
|
||||
|
||||
**build_for_request 改 per-conv messages**(agentic.rs:631-637):
|
||||
|
||||
```rust
|
||||
let messages = {
|
||||
let session = session_arc.lock().await;
|
||||
let conv = session.per_conv.get(&conv_id).expect("...");
|
||||
let (history_msgs, _trimmed) = conv.messages.build_for_request(sys_tokens);
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
### 4.3 GeneratingGuard 改 per-conv 复位
|
||||
|
||||
**改造前**(agentic.rs:73-108):guard 持 `Arc<Mutex<AiSession>>`,Drop 时 `session.lock().await.generating = false`(全局)。
|
||||
|
||||
**改造后**:guard 加 `conv_id: String` 字段,`reset()` / Drop 时 `session.conv(&conv_id).generating = false`(只复位该 conv)。
|
||||
|
||||
### 4.4 try_continue_agent_loop 改 per-conv 续跑
|
||||
|
||||
**改造前**(agentic.rs:992-1112):读全局 generating + pending 决定续跑,conv_id 取自 `active_conversation_id` 或 pending_approvals。
|
||||
|
||||
**改造后**:加 `conv_id: &str` 参数,读 `conv_read(conv_id).generating` + `pending_approvals.values().any(|a| a.conversation_id == Some(conv_id))` 决定续跑,spawn run_agentic_loop 传同一 conv_id。
|
||||
|
||||
**调用点**:ai_approve(commands.rs 330/411)调用处传 `approval.conversation_id`(已有,见 commands.rs:484/548 校验)。
|
||||
|
||||
### 4.5 ai_conversation_switch 删 readonly 分支
|
||||
|
||||
**改造前**(commands.rs:1474-1481):生成中 readonly 切换。
|
||||
|
||||
**改造后**:
|
||||
|
||||
```rust
|
||||
let mut session = state.ai_session.lock().await;
|
||||
session.active_conversation_id = Some(conversation_id.clone()); // 直接切,不动 per-conv
|
||||
// 旧 conv 的 per-conv state 保留(后台 loop 继续跑)
|
||||
// 新 conv 从 DB reload 到其 per-conv(已存在则跳过,防覆盖后台 conv 的内存 messages)
|
||||
let conv_state = session.per_conv.entry(conversation_id.clone())
|
||||
.or_insert_with(|| PerConvState::from_messages(messages)); // 仅惰性建
|
||||
session.pending_approvals.retain(|_, a| a.conversation_id.as_deref() != Some(&conversation_id));
|
||||
// 删 readonly 分支
|
||||
```
|
||||
|
||||
### 4.6 ai_conversation_create 删强制结束旧 loop 块
|
||||
|
||||
**改造前**(commands.rs:1360-1379):生成中 `generating=false` + `stop_flag=true` 杀旧 loop + emit AiCompleted。
|
||||
|
||||
**改造后**:
|
||||
|
||||
```rust
|
||||
let mut session = state.ai_session.lock().await;
|
||||
// B 阶段:新建会话不杀旧 loop(决策 e)。新会话建独立 per-conv state。
|
||||
let id = new_id();
|
||||
session.active_conversation_id = Some(id.clone());
|
||||
session.per_conv.insert(id.clone(), PerConvState::new()); // 全新 state
|
||||
session.pending_approvals.retain(|_, a| a.conversation_id.as_deref() != Some(&id));
|
||||
// 删 if session.generating { 强制结束 } 块
|
||||
```
|
||||
|
||||
**注意**:旧块前还有 SW-260618-02 `finalize_pending_placeholders`(commands.rs:1365/1398)——保留其语义但改为针对**旧 conv** 的占位终态化(若旧 conv 的 messages 即将被新建覆盖,占位须终态化;per-conv 后旧 conv 的 messages 保留,占位无须终态化,**可直接删**)。
|
||||
|
||||
### 4.7 ai_chat_send / ai_is_generating / ai_chat_stop / force / continue / stop_loop 改 per-conv
|
||||
|
||||
所有命令**加 conv_id 参数**(前端传 activeConversationId 或目标 conv),操作仅针对该 conv 的 per-conv state:
|
||||
|
||||
- `ai_is_generating(conv_id)` → `conv_read(conv_id).map(|c| c.generating).unwrap_or(false)`
|
||||
- `ai_chat_send(conv_id, ...)` → 互斥仅查该 conv 的 generating
|
||||
- `ai_chat_stop(conv_id)` → 置该 conv 的 stop_flag + notify
|
||||
- `ai_chat_force_send(conv_id, ...)` → 复位该 conv 的 generating + 重发
|
||||
- `ai_continue_loop(conv_id)` / `ai_stop_loop(conv_id)` → 操作该 conv
|
||||
|
||||
**IPC 签名变更**:更新 `#[tauri::command]` 签名 + lib.rs invoke_handler + 前端 api/ai.ts wrapper + useAiSend.ts/useAiConversations.ts 调用处。
|
||||
|
||||
---
|
||||
|
||||
## 5. 风险点 + 回归测试点
|
||||
|
||||
### 5.1 风险清单
|
||||
|
||||
| 编号 | 风险 | 评估 | 缓解 |
|
||||
|---|---|---|---|
|
||||
| R-1 | ⚠️ 决策 b 偏离:messages 必须同 per-conv | 高(若不偏离决策 e 落空) | **列入待决策.md,须用户拍板** |
|
||||
| R-2 | 单 Mutex 多 conv 抢锁 | 中(loop 内锁持有短) | profiling 后再细化 per-conv Mutex |
|
||||
| R-3 | 事件路由新增 emit 漏 conv_id | 低(11 变体已全覆盖) | 批 3/4 改造时 grep emit checklist |
|
||||
| R-4 | 前端 generatingConvId 单值 → Set 漏改清零点 | 中(4 处清零点) | 批 6 集中改 + 双会话 UI 验收 |
|
||||
| R-5 | 回归面广(批 2/4 高风险) | 高 | 批间 cargo + vue-tsc + 逐命令人工验收 |
|
||||
| R-6 | F-260616-12 retry 持 permit | 低(global 改会话级后语义自洽) | 批 5 验证 global 新语义下行为符合预期即关闭 |
|
||||
| R-7 | 启动恢复 + L0 握手多 conv 适配 | 中(restore_pending_approvals + lib.rs L0) | 批 8 集中处理 |
|
||||
| R-8 | iteration_used 跨审批续跑在多会话下正确性 | 中(挪入 PerConvState) | 批 2 checklist 含 ai_approve 两处读(commands.rs:330/411) |
|
||||
| R-9 | PendingApproval.conversation_id 一致性 | 低(insert 点均带 Some(conv_id)) | 批 2 核验所有 insert 点,None 视为无主审批归类 |
|
||||
|
||||
### 5.2 回归测试点
|
||||
|
||||
**单会话全功能回归**(批 2/4 后必做,逐命令):
|
||||
- send / regenerate / edit_last / approve / stop / continue_loop / stop_loop / switch / create / delete / rename / archive / set_pinned
|
||||
|
||||
**双会话并发验收**(批 3 后):
|
||||
- 开 conv A 跑长任务(工具调用) → 切 conv B 发消息 → A 后台跑完不污染 B 的 messages/pending
|
||||
- A 跑审批等待 → 切 B 发 → A 审批仍可处理(approve 后 A 续跑)
|
||||
- A 跑达 max_iterations → 切 B 发 → A 仍可 continue_loop / stop_loop
|
||||
|
||||
**3 会话并发验收**(批 5 后):
|
||||
- 同时跑 3 个会话(第 4 个排队)+ 重试期不阻塞他对话(F-260616-12)
|
||||
- token 暴增护栏:3 会话并发 token ≈ 3 倍单会话
|
||||
|
||||
**d1 UI 验收**(批 6):
|
||||
- 侧栏见两 conv 流式指示器(generatingConvs.has)
|
||||
- 切走后台继续,A 完成后指示器消失
|
||||
|
||||
**d2 UI 验收**(批 7):
|
||||
- 每会话独立窗口(`ai-detached-${convId}`)+ 多窗口并发 + 窗口间 provider 变更同步
|
||||
|
||||
**启动恢复验收**(批 8):
|
||||
- 重启恢复多 conv pending 审批(各归各的 per_conv)
|
||||
- HMR 清多 conv 残留 generating(L0 握手遍历 per_conv)
|
||||
|
||||
**关键回归点**:
|
||||
- **切换不丢上下文**:conv A 跑一半切走再切回,messages 完整(per-conv 保留)
|
||||
- **并发不串话**:conv A 的 AiTextDelta 不写入 conv B 的视图(事件 conversation_id 路由)
|
||||
- **事件路由 conversation_id**:所有 emit 带 conv_id(useAiEvents.ts:125 已路由)
|
||||
- **token 暴增护栏**:global permits=3 限并发会话数
|
||||
|
||||
---
|
||||
|
||||
## 6. 与已决架构债的关系
|
||||
|
||||
- **memory `aichat-arch-extensibility`**:「AiSession 单例未动」是登记的架构债,本设计正式清偿。
|
||||
- **mod.rs:320-331 注释**:「当前 AiSession 是全局单例(F-09 B 多会话架构落地时改 per-conv)」——预留债标记,本设计落地。
|
||||
- **state.rs:94-98 注释**:per_conv「未来若支持多对话并发,需改为 HashMap<conv_id, Semaphore>」——本设计 §2.2 落地。
|
||||
- **不冲突**:本设计与 F-260616-11(iteration 累计,已 per-conv 化预留)、F-260616-07(流式重试,F-260616-12 依赖项 §2.2 处理)、generating 状态机加固(RAII guard,§4.3 改 per-conv)、B-260615-14(notify 即时唤醒,随 stop_flag 入 PerConvState)均兼容。
|
||||
|
||||
---
|
||||
|
||||
## 7. 待裁决决策点清单(供主代裁决 / 待决策.md)
|
||||
|
||||
| 编号 | 决策点 | 推荐 | 影响范围 |
|
||||
|---|---|---|---|
|
||||
| ⚠️ b-1 | messages 是否 per-conv(偏离决策 b 原文) | **必须 per-conv**(否则决策 e 落空) | 阶段 2 批 1-3 |
|
||||
| ⚠️ c-1 | global permits=3 默认值是否合理 / 暴露 Settings | 保持 3 + Settings 加「并发会话数上限」配置项 | 阶段 2 批 5 |
|
||||
| ⚠️ e-1 | 旧 loop 跑完 save_conversation 是否走原路径 | 保持原路径(save 接 conv_id 零改动) | 阶段 2 批 3 备案 |
|
||||
| (备案) | 阶段 1(A 隔离残留 bug)实际无残留,确认跳过 | 确认跳过 | 阶段 1 |
|
||||
| (备案) | d2 关窗是否停 loop(决策 e 下不停) | 不停,UI 提示「窗口关闭后后台继续」 | 阶段 4 |
|
||||
| (备案) | d2 多窗口内存上限(是否限制最多 N 个独立窗口) | 建议限 3-5 个,超限提示 | 阶段 4 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 验收清单(每批完成后)
|
||||
|
||||
- [ ] 阶段 1:(无工作,A 路线补漏已落地,确认即可)
|
||||
- [ ] 阶段 2 批 1:cargo build + PerConvState 访问器单测(惰性建/已存在/conv 删除)
|
||||
- [ ] 阶段 2 批 2:cargo + vue-tsc 0err + 单会话全功能回归(逐命令)
|
||||
- [ ] 阶段 2 批 3:双会话并发(开 A 跑 → 切 B 发 → A 后台跑完不污染 B)+ 单会话回归
|
||||
- [ ] 阶段 2 批 4:双会话并发(切走不杀旧 loop,各自跑完)+ 全 IPC 签名前端调用核对
|
||||
- [ ] 阶段 2 批 5:3 会话并发(第 4 排队)+ 重试期不阻塞他对话(F-260616-12)
|
||||
- [ ] 阶段 2 批 8:重启恢复多 conv pending 审批 + HMR 清多 conv 残留
|
||||
- [ ] 阶段 3:双会话并发 UI(侧栏两 conv 流式指示器,切走后台继续)
|
||||
- [ ] 阶段 4:每会话独立窗口 + 多窗口并发 + 窗口间状态同步
|
||||
|
||||
---
|
||||
|
||||
**设计完。本文件为设计草案,不含代码改动。实施时按阶段 2(批 1→8)→ 阶段 3 → 阶段 4 顺序,每批独立可验证。**
|
||||
|
||||
**核心待裁决**:⚠️ b-1(messages per-conv 偏离决策 b 原文)是 B 阶段能否落地的关键,须用户拍板后方可进入阶段 2 批 1。
|
||||
@@ -112,7 +112,7 @@
|
||||
|
||||
> 功能需求,已走查定位链路 + 方案记录,待用户确认后实施。
|
||||
|
||||
- [ ] F-260616-09 [P1/架构·**A路线✅(batch60·2026-06-16·workflow whae812z5+主代核查),B已决高优先级·待立项实施(待决策归档)**] — **AiSession 单例 → 多会话并发架构(多窗口来回切 + 并行执行)**。用户需求(3 条消息强化):「应用级支持同时具备多会话」+「同时具备多会话的能力」+「多个会话窗口来回切,**并行执行**」。现状(Explore 走查确认):`AiSession` **应用级单例**(`state.rs:164` `ai_session: Arc<Mutex<AiSession>>`,init:205),messages/generating/stop_flag/pending_approvals/agent_language 全一份。设计「单例 + 软隔离」——generating 互斥(同一时刻**只一个对话能跑**) + 切换旧 loop 退出(`agentic.rs:177-190` B-260615-11) + 切换时 generating→readonly(`commands.rs:873-881`)。`llm_concurrency` per_conv 因此退化为单对话内并发(`state.rs:93-96` 注释已标注预留改 HashMap)。**memory 已标记「AiSession 单例未动」架构债**(aichat-arch-extensibility)。**残留 bug**(表象=新建会话上下文残留):前端 `newConversation`(`useAiConversations.ts:31-41`) 漏清 queue/generatingConvId/agentRound/searchQuery;后端 `ai_conversation_create`(`commands.rs:784-821`) 漏清 agent_language/stop_flag(设 true 停旧 loop 没复位 false)。**用户「并行执行」明确=B 方向**(A 只解切换残留不满足并行)。**分档**:**A 短期隔离修复**(补漏清字段,维持单例软隔离,解残留,低风险立即做,**B 前置**) + **B 多会话并发架构**(单例 → `HashMap<conv_id, SessionState>`,每会话独立 messages/generating/stop_flag/pending_approvals + llm_concurrency per_conv 改真 per-conv HashMap + loop 内校验改 conv_id 索引各自 session + 切换不退出旧 loop 各自跑完真并发 + 事件路由核对 conversation_id 全覆盖)。**决策点(待定)**:(a) A 先做解燃眉 vs 直接 B——倾向 **A 先**(残留真 bug 立即解,B 架构另立项) (b) B 形态——AiSession 整体 HashMap vs 拆字段(messages/pending 已可按 conv reload,只拆 generating/stop_flag per-conv)——倾向**后者侵入小** (c) B 并发上限——复用 llm_concurrency.global 限并发会话数防 token 暴增——倾向是 (d) **多窗口 UI 形态**——(d1) 单面板侧栏会话列表切换+后台并行(改动小) vs (d2) 每会话独立 Tauri 窗口(`useAiWindow.ts` detached 已有基础可扩,改动大但「窗口来回切」语义最贴)——**待用户定**,倾向先 d1 后 d2 (e) 旧 loop 切换处理——不退出各自跑完(真并发,倾向) vs 仍退出。— state.rs(AiSession 去单例化) + commands.rs(create/switch 重构 + 漏清补) + agentic.rs(:177-190 校验改 conv_id 索引) + context.rs(ContextManager per-conv) + useAiConversations.ts(漏清补) + stores/ai.ts(per-conv state) + llm_concurrency per_conv + useAiWindow.ts(若 d2 多窗口)。**B 是大改架构升级,需单独设计文档 + 立项**。**✅ 已决(2026-06-16·待决策归档):a — A 隔离修复 + B 立项(🔥 B 阶段高优先级尽快实施),UI d1+d2 都做(侧栏切换 + 独立 Tauri 窗口)**。
|
||||
- [ ] F-260616-09 [P1/架构·**A路线✅(batch60 + 06-19核验补漏已落地:commands.rs:1404/useAiConversations.ts:80 → 阶段1跳过),B 设计完成 2026-06-19([F-09设计文档](./02-架构设计/F-09-多会话并发架构设计-2026-06-19.md))·阶段2 批1-8 待拍板 b-1(messages per-conv)启动(见待决策.md)**] — **AiSession 单例 → 多会话并发架构(多窗口来回切 + 并行执行)**。用户需求(3 条消息强化):「应用级支持同时具备多会话」+「同时具备多会话的能力」+「多个会话窗口来回切,**并行执行**」。现状(Explore 走查确认):`AiSession` **应用级单例**(`state.rs:164` `ai_session: Arc<Mutex<AiSession>>`,init:205),messages/generating/stop_flag/pending_approvals/agent_language 全一份。设计「单例 + 软隔离」——generating 互斥(同一时刻**只一个对话能跑**) + 切换旧 loop 退出(`agentic.rs:177-190` B-260615-11) + 切换时 generating→readonly(`commands.rs:873-881`)。`llm_concurrency` per_conv 因此退化为单对话内并发(`state.rs:93-96` 注释已标注预留改 HashMap)。**memory 已标记「AiSession 单例未动」架构债**(aichat-arch-extensibility)。**残留 bug**(表象=新建会话上下文残留):前端 `newConversation`(`useAiConversations.ts:31-41`) 漏清 queue/generatingConvId/agentRound/searchQuery;后端 `ai_conversation_create`(`commands.rs:784-821`) 漏清 agent_language/stop_flag(设 true 停旧 loop 没复位 false)。**用户「并行执行」明确=B 方向**(A 只解切换残留不满足并行)。**分档**:**A 短期隔离修复**(补漏清字段,维持单例软隔离,解残留,低风险立即做,**B 前置**) + **B 多会话并发架构**(单例 → `HashMap<conv_id, SessionState>`,每会话独立 messages/generating/stop_flag/pending_approvals + llm_concurrency per_conv 改真 per-conv HashMap + loop 内校验改 conv_id 索引各自 session + 切换不退出旧 loop 各自跑完真并发 + 事件路由核对 conversation_id 全覆盖)。**决策点(待定)**:(a) A 先做解燃眉 vs 直接 B——倾向 **A 先**(残留真 bug 立即解,B 架构另立项) (b) B 形态——AiSession 整体 HashMap vs 拆字段(messages/pending 已可按 conv reload,只拆 generating/stop_flag per-conv)——倾向**后者侵入小** (c) B 并发上限——复用 llm_concurrency.global 限并发会话数防 token 暴增——倾向是 (d) **多窗口 UI 形态**——(d1) 单面板侧栏会话列表切换+后台并行(改动小) vs (d2) 每会话独立 Tauri 窗口(`useAiWindow.ts` detached 已有基础可扩,改动大但「窗口来回切」语义最贴)——**待用户定**,倾向先 d1 后 d2 (e) 旧 loop 切换处理——不退出各自跑完(真并发,倾向) vs 仍退出。— state.rs(AiSession 去单例化) + commands.rs(create/switch 重构 + 漏清补) + agentic.rs(:177-190 校验改 conv_id 索引) + context.rs(ContextManager per-conv) + useAiConversations.ts(漏清补) + stores/ai.ts(per-conv state) + llm_concurrency per_conv + useAiWindow.ts(若 d2 多窗口)。**B 是大改架构升级,需单独设计文档 + 立项**。**✅ 已决(2026-06-16·待决策归档):a — A 隔离修复 + B 立项(🔥 B 阶段高优先级尽快实施),UI d1+d2 都做(侧栏切换 + 独立 Tauri 窗口)**。
|
||||
|
||||
- [ ] F-260616-12 [P2/依赖F-09] — **retry 持 permit 不释放(多会话隐患)**。F-260616-07 落地的流式重试循环(`agentic.rs:238-239`)重试期间持有 global+per_conv permit 不释放(注释「防新请求挤占」)。当前 AiSession 单例 + 主 loop 串行无影响,但 **F-260616-09 多会话并发后**,重试期间阻塞其他对话 LLM 调用。**方向**:多会话落地时核对——主 loop 串行下重试持 permit 防自己挤占无意义,倾向重试不持 permit 或仅持 per_conv。— agentic.rs:238 + F-260616-09 多会话架构。**依赖 F-260616-09 立项后一并处理**。
|
||||
|
||||
|
||||
10
docs/待决策.md
10
docs/待决策.md
@@ -143,6 +143,16 @@
|
||||
- **关联**:批次7 transition_status 已删 / CR-22 删 risk_level / CR-23 标 allow 5 处
|
||||
- **状态**:✅ 已实施 b(2026-06-18·主代自主决策·risk_level 删 + 预留/diff 标 allow·cargo 0 warning)
|
||||
|
||||
#### F-260616-09-B 多会话并发架构 B 阶段实施决策(设计完成 2026-06-19·待拍板)
|
||||
- **背景**:F-09 B 阶段设计文档完成 [F-09-多会话并发架构设计-2026-06-19.md](./02-架构设计/F-09-多会话并发架构设计-2026-06-19.md)。核验发现 **A 路线补漏已全部落地**(commands.rs:1404-1409 + useAiConversations.ts:80-83)→ **阶段1 跳过**。AiSession 12 字段。阶段2(B 主体,批1-8)待启动。
|
||||
- **决策点**:
|
||||
- **⚠️ b-1(关键·须拍板)**:messages 是否 per-conv。原决策 b「messages 按 conv reload(不 per-conv)」与决策 e「切换不退出各自跑完」**矛盾**(单例 messages 下旧 loop push 污染新 conv,B-260615-11 退出校验必须保留→与 e 冲突)。设计推荐**修正 b 为 messages per-conv**(e 的技术必然必要条件,侵入面增量极小:ContextManager 挪 HashMap)。
|
||||
- **⚠️ c-1**:global permits=3 默认值 + Settings 加「并发会话数上限」配置项。
|
||||
- **e-1(主代已定✅)**:旧 loop save_conversation 保持原路径(save 接 conv_id 零改动)。低风险技术细节,主代裁决采纳。
|
||||
- **推荐**:**✅ b-1 采纳(messages per-conv,技术必然)+ c-1 保持 3 + Settings 配置 + e-1 原路径**。拍板后启动阶段2 批1(PerConvState 数据结构 + 访问器)。
|
||||
- **关联**:todo F-260616-09 / 设计文档 / memory aichat-arch-extensibility
|
||||
- **状态**:🟡 待用户拍板(b-1/c-1)
|
||||
|
||||
---
|
||||
|
||||
## 已决归档
|
||||
|
||||
25
docs/待审查.md
25
docs/待审查.md
@@ -661,7 +661,7 @@
|
||||
- **⚪ WATCH-1**: run_workflow 审批 reason 走轻量「触发工作流」动词(不含 task 名/target_status),UX-14 范围外;若后续需 run_workflow reason 显任务名,需同时从 tool_display_hint :1577 移除 run_workflow(让 display_hint_for_tool :1550 模板生效)+ build_approval_reason task_id key 特化走 resolve_task_label。当前接受动词级。
|
||||
- **待修项回流 todo**: **无** 🔴/🟡 项
|
||||
|
||||
### CR-260619-02 UX-260618-15 第一批 流式重试N+1气泡根治·方案A(agent ux15-batch1 实施 + 主代兜底核验 + 注释修正) — 🟡 待审
|
||||
### CR-260619-02 UX-260618-15 第一批 流式重试N+1气泡根治·方案A(agent ux15-batch1 实施 + 主代兜底核验 + 注释修正) — ✅ 已审(PASS·0)
|
||||
|
||||
- **范围**(3 文件 + 主代注释修正):
|
||||
- **stream_recv.rs**:`InitFailed` 变体加 `error: String` 字段(:125);5 处重试路径 AiError emit 全移除(idle timeout :203 / stream-error :262 / chunk Err :303 / 流尽无 finished :362 / provider.stream() Err :402),retryable=true 重试路径 emit 权交 agentic,retryable=false Fatal 也交 agentic 统一 emit。**主代修正注释残留** :251/:299(旧"emit AiError"→"返回 InitFailed")。
|
||||
@@ -671,6 +671,29 @@
|
||||
- **审查要点(供审查 agent)**:① N+1 根治——重试过程不再每轮 push AiError,改 AiStreamRetry 单气泡(首次创建+更新),耗尽/Fatal emit 1 最终 AiError(N+1→1重试+1最终);② stream_recv 5 处移除判定(retryable=true 重试路径移除 / Fatal 交 agentic);③ agentic Fatal/耗尽 emit 单气泡(Network 分类);④ **riskNote error_type 统一 Network**——401 鉴权 Fatal 原本 error_type=None(前端 canOpenSettings 只认 {auth,provider_config} 本不显「去设置」),现 Network 也不显,**不恶化既有行为**(记 low WATCH:理想 401 显 auth 按钮需单独做);⑤ InitFailed 枚举加 error 字段 pub(crate) 内部无破坏。
|
||||
- **关联**:todo UX-260618-15 第一批销账;第二批(方案C 聚合气泡显示错误原因)可选增强待定。
|
||||
|
||||
**复审结论(CR-260619-02)**: verdict **PASS**(0 high / 0 med / 0 low)。独立 grep/read 核验 3 文件当前形态,N+1 气泡根治落地完整,emit 权移交无 Fatal swallow,单气泡聚合无重复 emit,保留 emit 未误删,注释与代码一致。
|
||||
|
||||
**逐项核验表**(源码 file:line 佐证):
|
||||
|
||||
| 审查点 | 核验结论 | 佐证 |
|
||||
|---|---|---|
|
||||
| N+1 根治:重试不再每轮 push AiError | ✅ PASS | stream_recv.rs 全文 `emit` 仅 AiTextDelta(:229)/AiHeartbeat(:329);5 处 InitFailed 路径(:206/:265/:306/:364/:407)均 `return StreamResult::InitFailed{...}` 不 emit |
|
||||
| 5 处移除完整无遗漏 InitFailed emit | ✅ PASS | grep `AiError` 在 stream_recv.rs 仅命中注释/Doc,无运行时 emit 调用 |
|
||||
| retryable=true 重试路径移除判定正确 | ✅ PASS | agentic.rs:243-300 解构 `InitFailed{retryable,error}`,retryable=true 走重试 emit AiStreamRetry(:293);retryable=false Fatal 返回 `StreamOutcome::Fatal{error}`(:254) |
|
||||
| retryable=false Fatal 正确交 agentic(无 swallow) | ✅ PASS | 外层 agentic.rs:735-744 `StreamOutcome::Fatal` 分支 guard.reset + emit AiError(Network) + return,终态单 emit |
|
||||
| Fatal/耗尽单气泡聚合无重复 emit | ✅ PASS | Fatal 分支 :739 单 emit;全 candidate 耗尽 :761 单 emit;`last_exhausted_error` 累积 Exhaused 文本(:688 init / :732 write),Exhausted 分支 :724 注释「不在此 emit」防残留 |
|
||||
| 保留 emit 未误删 | ✅ PASS | ensure_resolved_key Auth :401 / 压缩失败 Unknown :618 / try_continue ProviderConfig :1058 三处 emit 完整 |
|
||||
| useAiEvents AiStreamRetry 首次创建边界 | ✅ PASS | useAiEvents.ts:185 `if (lastMsg && lastMsg.isError) 更新 content else push 新错误气泡`;重试过程不再先 emit AiError 故首条到达末条非 isError,正确创建;后续 isError 更新 |
|
||||
| 401 鉴权走 ensure_resolved_key(Auth)非 stream_llm Fatal(Network) | ✅ PASS | 401 key 缺失/损坏早失败在 agentic.rs:390 `ensure_resolved_key` Err → :401 emit Auth;stream_llm Fatal(Network) 仅覆盖 key 已过 ensure 但被 provider 拒等场景 |
|
||||
| 注释 :251/:299 修正一致 | ✅ PASS | stream_recv.rs :203-204/:262-263/:303-305/:362-363/:402-404 均修正为「不再 emit AiError/返回 InitFailed」,与代码行为一致 |
|
||||
|
||||
**对抗印证**:① stream_recv.rs 无运行时 AiError emit(grep 仅命中注释/Doc);② agentic.rs AiStreamRetry 单点 emit(:293)位于 retryable 重试分支,Fatal/Exhaused 分支均不重复;③ InitFailed 枚举字段 `error:String`(:125)pub(crate) 内部,跨模块解构(agentic.rs:243)正确。
|
||||
|
||||
**WATCH**:
|
||||
- **⚪ WATCH-1(沿用 riskNote ④,low)**:stream_llm Fatal 路径 error_type 统一 Network,401 经此路径时不显「去设置」按钮(前端 canOpenSettings 仅认 {auth,provider_config})。401 主路径已由 ensure_resolved_key Auth 兜底,不恶化既有行为;理想 401 显 auth 按钮需单独做。
|
||||
|
||||
**待修项回流 todo**: **无** 🔴/🟡 项
|
||||
|
||||
---
|
||||
|
||||
## 已审归档
|
||||
|
||||
Reference in New Issue
Block a user