agent-Specialization/docs/conversation_level_terminal_design.md
JOJO a4eb688814 feat(server,static): 对话运行状态 REST 对账与 conversation 级 WebTerminal 隔离
议题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 重复恢复
2026-07-20 01:05:17 +08:00

15 KiB
Raw Blame History

Conversation 级 WebTerminal 隔离改造——交接文档(压缩后必读)

写于 2026-07-19供上下文压缩后的对话继续工作。 读完本文档即可直接开工,无需重新调研代码。行号基于 2026-07-19 的代码,若有漂移以搜索符号名为准。 相关项目记忆:runtime_state_loading_concurrency_redesign4 议题讨论总索引)。


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.pytest_client 4 项断言401/空闲/活动/终态,全过)。
  • 已验证:冒烟 6/6、前端构建、真实服务 playwright 端到端2.5s 精确间隔、外部建任务前端自动接管轮询、完成后正常收尾)。
  • 测试副作用:conv_20260718_005231_752(张雪峰对话)多了一条"对账测试通过"消息,可删。
  • 既有 bug与议题 1 无关,未修)assistant 消息 reasoning_content 前端渲染两遍(对话文件数据正常,刷新仍复现,历史加载渲染路径问题)。

3. 议题 4 改动地图(已普查,直接照此施工)

3.1 核心缓存与资源链路

  • server/state.py:28user_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:142SubAgentManager 每 WebTerminal 一个(非全局单例)——对话级化天然兼容。
  • modules/terminal_manager.py:65TerminalManagermax_terminals 限制是 per-manager 的,对话级后每对话一个 manager互不干扰
  • modules/persistent_terminal/start.pyshell 是惰性启动Popen 在 167/208/269/346 行,首次用才起进程)——这是内存可控的关键,保持不变。

3.3 互斥与前端

  • server/tasks/models.py:147-155chat 任务互斥(task_type="chat" 时同用户同工作区禁止并发409 "当前工作区已有运行中的任务")。→ 改为同 conversation_id 互斥。
  • static/src/app/methods/message/send.ts:25:前端发送拦截。
  • static/src/app/computed.ts:220currentWorkspaceHasRunningTask(仅 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_flagsserver/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_notificationsserver/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 8091host 模式,/host-login 免密登录POST 需先 GET /api/csrf-tokentoken 字段放 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子智能体按需加载、议题 3worktreecurrentWorkspaceHasRunningTask 更名为 currentConversationHasRunningTask(语义已变、名字未改)。


7. 议题 4 实现记录2026-07-20 完成)

实际改动(与原计划偏差:未新增 get_conversation_resources,改为 get_user_resourcesconversation_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=Nonesuper 前设 _bound_conversation_idlast_activity_atmessage_callback 包装注入 cid_ensure_conversation 绑定对话优先加载conv_ 前缀规范化,失败不 fallback
  • core/main_terminal.py:创建 SubAgentManager 透传 owner_conversation_id=getattr(self,'_bound_conversation_id',None)
  • modules/sub_agent/manager.pyowner_conversation_id 属性;restore_running_tasks 过滤——仅恢复本对话任务,工作区级 manager 不再恢复(行为变化:重启后多智能体任务延迟到对话激活时恢复)
  • server/tasks/models.pychat 互斥改同对话(_norm_cid 规范化比较文案“当前对话已有运行中的任务”235/264/739 三处传 conversation_id=rec.conversation_id
  • server/tasks/api.pyrunning-status、create_task(×2)、cancel 四处传 cid
  • server/multi_agent.pylist_active_sub_agents_api 传 cid其余 5 处保持工作区级——无对话上下文/仅 workspace
  • server/socket_handlers.pyterminal_subscribe/get_terminal_output 从 data 读 cid
  • server/app_legacy.pystart_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 propacceptTerminalBroadcast 过滤 7 个广播事件started/list_update/output/input/closed/reset/switched响应类 subscribed/history 不过滤watch conversationId 清空重订阅emit 带 cid
  • static/src/App.vueTerminalPanel 传 :conversation-id="currentConversationId"
  • static/src/app/methods/ui/terminal.tsfetchTerminalCount/subscribeTerminalEvents/switchTerminalSession 带 cid
  • static/src/composables/useLegacySocket.tsconnect 时 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 无关)