agent-Specialization/docs/conversation_level_terminal_design.md

178 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Conversation 级 WebTerminal 隔离改造——交接文档(压缩后必读)
> 写于 2026-07-19供上下文压缩后的对话继续工作。
> **读完本文档即可直接开工,无需重新调研代码**。行号基于 2026-07-19 的代码,若有漂移以搜索符号名为准。
> 相关项目记忆:`runtime_state_loading_concurrency_redesign`4 议题讨论总索引)。
---
## 0. 当前状态一句话
议题 1运行状态 REST 对账)✅ 已完成并端到端验证;**议题 4conversation 级 WebTerminal 隔离)✅ 已于 2026-07-20 完成并端到端验证**(见 §7。议题 2子智能体按需加载、议题 3worktree用户明确暂缓是未来可做项。
## 1. 用户已拍板的设计决策(不要重新讨论)
1. **单对话单 WebTerminal**:每个对话有自己的 context_manager、file_manager、TerminalManagershell、SubAgentManager、BackgroundCommandManager。
2. **生命周期:常驻 + 24 小时 TTL**。实例随对话首次运行创建并常驻;仅当「超过 24 小时无活动 且 无运行中任务」才回收。不追求精确释放(原因见 §4 不可序列化清单)。
3. **worktree 后置**:先做 terminal 隔离。用户观点worktree 只能硬隔离文件编辑工具路径锚定run_command 只能软隔离cwd 锚定+agent 自觉Claude Code/Codex 也一样,工作量不大,后面再做。
4. 对话互斥维度:**同工作区互斥 → 同对话互斥**(同一对话仍禁止两个主 chat 任务并行,防止串写对话历史)。
## 2. 议题 1 已完成内容(文件 + 行号,勿重复调研)
### 后端
- `server/tasks/models.py`
- `TaskManager.get_conversation_running_status(terminal, conversation_id)`(约 604-665 行):新增。聚合 3 类后台状态,返回 `{has_running_sub_agents, has_running_background_commands, has_running_multi_agent}`。sub_agents 排除 multi_agent_mode 任务multi_agent 判定 = 非终态多智能体任务 或 `MultiAgentState` 有 running 实例 或 `has_pending_master_messages()`
- `_has_running_background(rec, terminal)`(约 667-682 行):改为委托上面的方法,保持旧语义(多智能体计入 sub_agents
- `create_chat_task` 互斥逻辑在 **147-155 行**(⚠️ 议题 4 要改这里)。
- `list_tasks(username, workspace_id)` 在约 206 行。
- `server/tasks/api.py`
- `GET /api/conversations/<conversation_id>/running-status?workspace_id=`(约 38-90 行,插在 `create_task_api` 之前)。返回 `{is_main_running, main_task_id, main_task_type, has_running_sub_agents, has_running_background_commands, has_running_multi_agent, is_truly_active}`
### 前端
- `static/src/app/methods/taskPolling/probe.ts`(已整体重写,@ts-nocheck现约 180 行)
- `startRunningStateReconcile()`:幂等启动 2.5s 对账循环 + 立即对账一次。
- `reconcileRunningStateOnce()`核心逻辑。恢复方向立即执行server active 本地空闲 → `taskStore.resumeTask(main_task_id)` + 设 waiting 标志;多智能体只设 taskInProgress 不设 waitingForSubAgent清理方向保守`taskStore.isPolling` 活跃则不干预;否则需连续 2 次 server idle 确认才清 streamingMessage/taskInProgress/waiting 标志 + `clearTaskState()`)。网络失败一律不变更状态。
- `startWaitingTaskProbe/stopWaitingTaskProbe/startMultiAgentTaskProbe/stopMultiAgentTaskProbe`:保留为委托/no-op调用点未动lifecycle.ts 400/407/418/419/502/531、messaging.ts 163/165、scroll.ts 31-32
- `restoreSubAgentWaitingState(retry)`:简化为启动对账循环(被 compression.ts 调用)。
- `static/src/app/state.ts` ~29 行:`runningStateReconcileTimer: null` + `_runningStateIdleStreak: 0`
- `static/src/app/methods/taskPolling/compression.ts` ~96-103 行:`restoreTaskState()` 入口启动对账。
- `static/src/app/methods/message/send.ts` ~312-316 行:`taskInProgress = true` 后启动对账。
### 验证资产
- 验证脚本:`_experiments/verify_running_status_api.py`test_client 4 项断言401/空闲/活动/终态,全过)。
- 已验证:冒烟 6/6、前端构建、真实服务 playwright 端到端2.5s 精确间隔、外部建任务前端自动接管轮询、完成后正常收尾)。
- 测试副作用:`conv_20260718_005231_752`(张雪峰对话)多了一条"对账测试通过"消息,可删。
- **既有 bug与议题 1 无关,未修)**assistant 消息 `reasoning_content` 前端渲染两遍(对话文件数据正常,刷新仍复现,历史加载渲染路径问题)。
## 3. 议题 4 改动地图(已普查,直接照此施工)
### 3.1 核心缓存与资源链路
- `server/state.py:28``user_terminals: Dict[str, WebTerminal] = {}` —— 全局缓存,当前 key = `username::workspace_id`
- `server/context.py`
- `_make_terminal_key(username, workspace_id)` **49 行**:返回 `f"{username}::{workspace_id}"`。→ 议题 4 改为加 conversation_id 段。
- `get_user_resources(username, workspace_id, update_session)` **131 行**:所有资源获取的统一入口。
- host 模式分支 ~143-280 行term_key 在 **205 行**生成、**280 行**写入缓存含「project_path 变化时重建 terminal」逻辑~210-260 行)。
- web 模式分支:**377 行**生成 term_key、**413 行**写入缓存。
- ⚠️ 调用点普查:**34 处调用 / 14 个文件**。分布:`server/api_v1.py` 14 处、`server/multi_agent.py` 6 处、`server/tasks/api.py` 4 处、`server/tasks/models.py` 3 处、`server/context.py` 2 处、`server/app_legacy.py` 2 处、`server/tasks/skills.py` 1 处、`server/socket_handlers.py` 1 处。
- 改造策略建议:不改 `get_user_resources` 签名,新增 `get_conversation_resources(username, workspace_id, conversation_id)`逐调用点迁移chat 任务链路tasks/models.py、tasks/api.py、chat_flow*)优先。
### 3.2 WebTerminal 本体
- `core/web_terminal.py`
- `class WebTerminal(MainTerminal)` ~30 行。
- `_ensure_conversation()` **33-52 行**:当前自动加载「最近对话」→ 议题 4 改为加载**指定 conversation_id**(或新对话则 start_new
- `__init__` **55-100 行**:调用父类 init → 创建 TerminalManager83 行)→ attach。
- `create_new_conversation()` ~110 行起。
- `close()` ~809 行:`terminal_manager.close_all()`。
- `core/main_terminal.py:142`SubAgentManager 每 WebTerminal 一个(非全局单例)——对话级化天然兼容。
- `modules/terminal_manager.py:65`TerminalManagermax_terminals 限制是 per-manager 的,对话级后每对话一个 manager互不干扰
- `modules/persistent_terminal/start.py`shell 是**惰性启动**Popen 在 167/208/269/346 行,首次用才起进程)——这是内存可控的关键,保持不变。
### 3.3 互斥与前端
- `server/tasks/models.py:147-155`chat 任务互斥(`task_type="chat"` 时同用户同工作区禁止并发409 "当前工作区已有运行中的任务")。→ 改为同 conversation_id 互斥。
- `static/src/app/methods/message/send.ts:25`:前端发送拦截。
- `static/src/app/computed.ts:220``currentWorkspaceHasRunningTask`(仅 host/docker 模式生效)→ 改为按对话判定,**可直接复用议题 1 的 running-status 接口**。
- 限制 B运行中禁切传统/多智能体模式,纯前端):`static/src/app/methods/ui/mode.ts:73` + QuickMenu.vue disabled。用户尚未拍板何时放开默认暂不动。
### 3.4 回收器(新建)
- 定期扫描 `state.user_terminals`,回收条件:当前时间 - 最后活动时间 > 24h 且该对话无运行任务(可用 `TaskManager.get_conversation_running_status` 判定)。
- 回收动作:`terminal.close()`(关 shell→ 从 user_terminals 移除。对话历史本来就有磁盘持久化,无损。
- 需要给 WebTerminal 加 `last_activity_at` 字段(任务创建/工具调用时刷新)。
### 3.5 需要注意的关联点
- `restore_running_tasks()``modules/sub_agent/manager.py:279`):启动时恢复多智能体子智能体,当前按 terminal 恢复 → 对话级后恢复逻辑要按对话 terminal 走。
- `stop_flags``server/state.py`):按 task_id 全局 dicttask 本就属于具体对话,理论上不用大改,但要检查清理时机。
- 多智能体模式:`multi_agent_mode` 是对话级 metadata对话级 terminal 后 `web_terminal.multi_agent_mode` 属性语义要重新对齐(当前是 terminal 级属性chat_flow_task_main.py 多处 `getattr(web_terminal, "multi_agent_mode", False)`)。
- 后台通知轮询(`poll_completion_notifications` / `poll_multi_agent_notifications`server/chat_flow_task_main.py持有 terminal 引用,对话级后注意引用来源。
## 4. 为什么选「常驻+TTL」而不是「空闲释放」勿重新争论
不可序列化的状态(释放即丢,无法 100% 恢复):
1. **shell 进程状态**cwd、`export` 变量、激活的 venv、后台 jobOS 进程态)。快照重放只能覆盖 90%且错误是静默的AI 在错误目录执行命令)。
2. **子智能体 asyncio 状态**:运行中/idle 的 Task、等待唤醒的 Event、ask_master 的 Future。现有 restore 也只能恢复到「强制 idle」。
3. **后台命令进程句柄**
内存估算(已给用户确认):单对话常驻 ~10-25MBshell 惰性30 活跃对话 ~300-750MB本机单用户可控。
## 5. 服务端当前状态
- 服务当前**未在运行**(用户关过,我起的 8091 后台进程也已结束)。重启命令:`python3 -m server.app --port 8091`host 模式,`/host-login` 免密登录POST 需先 GET `/api/csrf-token``token` 字段放 `X-CSRF-Token` 头)。
- 前端构建命令:`npm run build --silent 2>&1 | tail -n 5`。
- 验证命令:`python3 -m py_compile <files>` + `python3 -m unittest test.test_server_refactor_smoke`
## 6. 下一步(压缩后从这里继续)
议题 4 已全部完成。未来可做:议题 2子智能体按需加载、议题 3worktree、`currentWorkspaceHasRunningTask` 更名为 `currentConversationHasRunningTask`(语义已变、名字未改)。
---
## 7. 议题 4 实现记录2026-07-20 完成)
### 实际改动(与原计划偏差:未新增 `get_conversation_resources`,改为 `get_user_resources` 加 `conversation_id=None` 可选参数——向后兼容、更 DRY迁移即传参
**后端**
- `server/context.py`
- `_make_terminal_key(username, workspace_id, conversation_id=None)`:三段键 `u::w::cid`
- `_wrap_callback_with_conversation_id(callback, cid)`:广播 data 注入 cidsetdefault
- `_touch_terminal_activity(terminal, cid)`:刷新 `last_activity_at`
- `attach_user_broadcast`:读 `terminal._bound_conversation_id` 自动包装
- `get_user_resources(..., conversation_id=None)`host/web 两分支 term_key 带对话段;**container_key 保持工作区级**docker 一工作区一容器用户确认WebTerminal 创建传 cidreturn 前 touch
- `with_terminal`:从 query/JSON body 读 conversation_id/api/terminals 等自动支持)
- `get_terminal_for_sid(sid, conversation_id=None)`
- 回收器(文件末尾):`CONVERSATION_TERMINAL_TTL_SECONDS`(默认 24henv 可调)/ `reap_idle_conversation_terminals(now)`(可测试)/ `_conversation_terminal_reaper_loop` / `start_conversation_terminal_reaper`(幂等);回收=保存对话+close_all shell+close MCP+pop仅三段键、过期、无运行工作主任务+3类后台判定失败保守不回收
- `core/web_terminal.py``__init__` 加 `conversation_id=None`super 前设 `_bound_conversation_id`、`last_activity_at`message_callback 包装注入 cid`_ensure_conversation` 绑定对话优先加载conv_ 前缀规范化,失败不 fallback
- `core/main_terminal.py`:创建 SubAgentManager 透传 `owner_conversation_id=getattr(self,'_bound_conversation_id',None)`
- `modules/sub_agent/manager.py``owner_conversation_id` 属性;`restore_running_tasks` 过滤——仅恢复本对话任务,**工作区级 manager 不再恢复(行为变化:重启后多智能体任务延迟到对话激活时恢复)**
- `server/tasks/models.py`chat 互斥改同对话(`_norm_cid` 规范化比较文案“当前对话已有运行中的任务”235/264/739 三处传 `conversation_id=rec.conversation_id`
- `server/tasks/api.py`running-status、create_task(×2)、cancel 四处传 cid
- `server/multi_agent.py``list_active_sub_agents_api` 传 cid其余 5 处保持工作区级——无对话上下文/仅 workspace
- `server/socket_handlers.py``terminal_subscribe`/`get_terminal_output` 从 data 读 cid
- `server/app_legacy.py``start_background_jobs` 接入回收器启动
- 保持工作区级不动:`server/conversation.py`(对话列表/标题/创建)、`server/api_v1.py` 14 处、`server/tasks/skills.py`、socket connect轻量服务实例shell 惰性)
**前端**
- `static/src/app/computed.ts` `currentWorkspaceHasRunningTask``!==` → `===`当前对话判定displayLockEngaged 随之按对话锁)
- `static/src/app/methods/message/send.ts`:拦截文案“当前对话正在运行”
- `static/src/components/panels/TerminalPanel.vue`+`conversationId` prop`acceptTerminalBroadcast` 过滤 7 个广播事件started/list_update/output/input/closed/reset/switched响应类 subscribed/history 不过滤watch conversationId 清空重订阅emit 带 cid
- `static/src/App.vue`TerminalPanel 传 `:conversation-id="currentConversationId"`
- `static/src/app/methods/ui/terminal.ts`fetchTerminalCount/subscribeTerminalEvents/switchTerminalSession 带 cid
- `static/src/composables/useLegacySocket.ts`connect 时 terminal_subscribe 带 cid
### 验证结果(全部通过)
- py_compile + 冒烟 6/6 + 前端构建
- `_experiments/verify_conversation_level_terminal.py`可复用回归9 项断言:键隔离/容器共享/绑定/Manager 独立/同对话互斥/异对话并行/广播注入/回收器 3 态
- 真实服务 playwright 原子脚本:**PARALLEL_PROOF {a_running:true, b_running:true}**A 运行中 B 页面零 is-disabledB 发送不被拦;同对话 409
- 终端隔离实测A 创建 `test-shell-a` 后 A 列表 1 个 / B 列表 0 个;`/api/terminals` 返回 `max_allowed:3`(每对话 3 终端)
- 议题 1 回归:双任务完成后双方 is_truly_active=false对账循环正常收尾
### 遗留与注意
- **行为变化**重启后多智能体任务不再立即恢复延迟到对话激活restore 过滤的必然结果)
- 测试副作用:语音工作区的 conv_20260720_001402_595/857 两个测试对话A 内有运行中的 test-shell-a张雪峰对话conv_20260718_005231_752有议题 1 时的测试消息
- 24h 回收器真实时长未实测单元覆盖三态env `CONVERSATION_TERMINAL_TTL_SECONDS` 可调短测)
- TerminalPanel 广播过滤前端 UI 未单独截图(逻辑随构建通过;后端 API 层隔离已实测)
- 既有 bug 仍未修assistant reasoning_content 前端渲染两遍(与议题 1/4 无关)
## 8. 边界排查与修复2026-07-20 第二轮)
针对隔离后边界情况的专项排查结论与修复:
1. **压缩迁移孤儿化(排查结论:不存在)**:深层压缩已全面 in-place`run_deep_compression`,对话 id 不变);`compression_mixin.compress_conversation` 创建新对话的旧路径已无调用方(死代码)。`compression_finished` 的 `rec.conversation_id` 迁移是同 id 覆写,无孤儿风险。
2. **手动压缩走错 terminal已修复**`/api/conversations/<id>/compress` 的 id 在路径里,`with_terminal` 只读 query/body → 原先用工作区级 terminal 执行并把对话加载进去,与对话级 terminal 双持同一对话(可串写)。修复:路由内显式 `get_user_resources(username, conversation_id=normalized_id)` 换到对话级 terminal`server/conversation.py`)。
3. **回收器竞态(已修复)**原实现判定→关闭→pop 存在窗口,期间新请求可能拿到正在关闭的实例建任务。修复:关闭前打 `_reaper_closing` 标记(`get_user_resources` 见标记原地重建新实例)+ 关闭前二次确认 last_activity/运行任务(取消时清除标记)+ pop 前校验实例身份(`server/context.py`)。验证脚本新增 8a/8b 断言。
4. **多智能体 idle 不会被误回收(排查结论:安全)**`get_conversation_running_status` 中 idle 状态的多智能体任务记录不在终态集合内,仍计入 `has_running_multi_agent`;回收判定依赖该方法,故 idle 等待中的多智能体对话不会被回收。
5. **空 cid chat 任务(已修复)**:未带 conversation_id 的 chat 任务原先落在工作区级 terminal 跑(`ensure_conversation_loaded` 在其上新建对话),占用服务 terminal 并造成双持。修复:`create_task_api` 在 cid 缺失时先补建对话文件再建任务(`server/tasks/api.py`。socket chat 路径前端已不使用(无 `emit('chat')`)。
6. **模型持久化四缺陷已修复commit 1289c31d**:见项目记忆 `conversation_model_persistence` 四条防线。
### 第二轮验证
- `_experiments/verify_conversation_level_terminal.py`11 项断言全过(新增 8a closing 重建 / 8b 二次确认)
- `_experiments/verify_model_fix.py`:带 cid 切换落盘、不带 cid 不污染、重启后保持、`/api/status?conversation_id=` 对话级模型恢复全过
- `_experiments/test_no_cid_guard.py`:空 cid 补建对话全过
- `_experiments/test_reaper_live.sh`:短 TTL 真实回收实测