议题1 运行状态对账: - 后端新增 GET /api/conversations/<id>/running-status 聚合主任务/子智能体/ 后台命令/多智能体四类运行状态 - 前端 3 个 1s probe 收敛为单个 2.5s 对账循环,事件为主、对账纠偏, 清理方向连续 2 次确认,恢复方向立即接管,修复轮询 404 卡死 议题4 conversation 级隔离: - terminal 缓存键改为 username::workspace::conversation 三段, 每个对话独立 WebTerminal/文件管理/终端/子智能体 - 同工作区多对话可并行运行,互斥维度从工作区改为同对话 - socket 终端面板按对话过滤广播,terminal 工具每对话最多 3 个 - 常驻内存 + 24h TTL 回收器(无活动且无运行任务才回收) - 子智能体 restore 按 owner_conversation_id 过滤,避免多 manager 重复恢复
15 KiB
15 KiB
Conversation 级 WebTerminal 隔离改造——交接文档(压缩后必读)
写于 2026-07-19,供上下文压缩后的对话继续工作。 读完本文档即可直接开工,无需重新调研代码。行号基于 2026-07-19 的代码,若有漂移以搜索符号名为准。 相关项目记忆:
runtime_state_loading_concurrency_redesign(4 议题讨论总索引)。
0. 当前状态一句话
议题 1(运行状态 REST 对账)✅ 已完成并端到端验证;议题 4(conversation 级 WebTerminal 隔离)✅ 已于 2026-07-20 完成并端到端验证(见 §7)。议题 2(子智能体按需加载)、议题 3(worktree)用户明确暂缓,是未来可做项。
1. 用户已拍板的设计决策(不要重新讨论)
- 单对话单 WebTerminal:每个对话有自己的 context_manager、file_manager、TerminalManager(shell)、SubAgentManager、BackgroundCommandManager。
- 生命周期:常驻 + 24 小时 TTL。实例随对话首次运行创建并常驻;仅当「超过 24 小时无活动 且 无运行中任务」才回收。不追求精确释放(原因见 §4 不可序列化清单)。
- worktree 后置:先做 terminal 隔离。用户观点:worktree 只能硬隔离文件编辑工具(路径锚定),run_command 只能软隔离(cwd 锚定+agent 自觉),Claude Code/Codex 也一样,工作量不大,后面再做。
- 对话互斥维度:同工作区互斥 → 同对话互斥(同一对话仍禁止两个主 chat 任务并行,防止串写对话历史)。
2. 议题 1 已完成内容(文件 + 行号,勿重复调研)
后端
server/tasks/models.pyTaskManager.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.pyGET /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.py14 处、server/multi_agent.py6 处、server/tasks/api.py4 处、server/tasks/models.py3 处、server/context.py2 处、server/app_legacy.py2 处、server/tasks/skills.py1 处、server/socket_handlers.py1 处。 - 改造策略建议:不改
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.pyclass WebTerminal(MainTerminal)~30 行。_ensure_conversation()33-52 行:当前自动加载「最近对话」→ 议题 4 改为加载指定 conversation_id(或新对话则 start_new)。__init__55-100 行:调用父类 init → 创建 TerminalManager(83 行)→ attach。create_new_conversation()~110 行起。close()~809 行:terminal_manager.close_all()。
core/main_terminal.py:142:SubAgentManager 每 WebTerminal 一个(非全局单例)——对话级化天然兼容。modules/terminal_manager.py:65:TerminalManager(max_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 全局 dict,task 本就属于具体对话,理论上不用大改,但要检查清理时机。- 多智能体模式:
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% 恢复):
- shell 进程状态:cwd、
export变量、激活的 venv、后台 job(OS 进程态)。快照重放只能覆盖 90%,且错误是静默的(AI 在错误目录执行命令)。 - 子智能体 asyncio 状态:运行中/idle 的 Task、等待唤醒的 Event、ask_master 的 Future。现有 restore 也只能恢复到「强制 idle」。
- 后台命令进程句柄。
内存估算(已给用户确认):单对话常驻 ~10-25MB(shell 惰性),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(子智能体按需加载)、议题 3(worktree)、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 注入 cid(setdefault)_touch_terminal_activity(terminal, cid):刷新last_activity_atattach_user_broadcast:读terminal._bound_conversation_id自动包装get_user_resources(..., conversation_id=None):host/web 两分支 term_key 带对话段;container_key 保持工作区级(docker 一工作区一容器,用户确认);WebTerminal 创建传 cid;return 前 touchwith_terminal:从 query/JSON body 读 conversation_id(/api/terminals 等自动支持)get_terminal_for_sid(sid, conversation_id=None)- 回收器(文件末尾):
CONVERSATION_TERMINAL_TTL_SECONDS(默认 24h,env 可调)/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_idserver/tasks/api.py:running-status、create_task(×2)、cancel 四处传 cidserver/multi_agent.py:list_active_sub_agents_api传 cid(其余 5 处保持工作区级——无对话上下文/仅 workspace)server/socket_handlers.py:terminal_subscribe/get_terminal_output从 data 读 cidserver/app_legacy.py:start_background_jobs接入回收器启动- 保持工作区级不动:
server/conversation.py(对话列表/标题/创建)、server/api_v1.py14 处、server/tasks/skills.py、socket connect(轻量服务实例,shell 惰性)
前端
static/src/app/computed.tscurrentWorkspaceHasRunningTask:!==→===(当前对话判定;displayLockEngaged 随之按对话锁)static/src/app/methods/message/send.ts:拦截文案“当前对话正在运行”static/src/components/panels/TerminalPanel.vue:+conversationIdprop;acceptTerminalBroadcast过滤 7 个广播事件(started/list_update/output/input/closed/reset/switched;响应类 subscribed/history 不过滤);watch conversationId 清空重订阅;emit 带 cidstatic/src/App.vue:TerminalPanel 传:conversation-id="currentConversationId"static/src/app/methods/ui/terminal.ts:fetchTerminalCount/subscribeTerminalEvents/switchTerminalSession 带 cidstatic/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-disabled;B 发送不被拦;同对话 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 无关)