agent-Specialization/docs/conversation_level_terminal_design.md

20 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 无关)

8. 边界排查与修复2026-07-20 第二轮)

针对隔离后边界情况的专项排查结论与修复:

  1. 压缩迁移孤儿化(排查结论:不存在):深层压缩已全面 in-placerun_deep_compression,对话 id 不变);compression_mixin.compress_conversation 创建新对话的旧路径已无调用方(死代码)。compression_finishedrec.conversation_id 迁移是同 id 覆写,无孤儿风险。
  2. 手动压缩走错 terminal已修复/api/conversations/<id>/compress 的 id 在路径里,with_terminal 只读 query/body → 原先用工作区级 terminal 执行并把对话加载进去,与对话级 terminal 双持同一对话(可串写)。修复:路由内显式 get_user_resources(username, conversation_id=normalized_id) 换到对话级 terminalserver/conversation.py)。
  3. 回收器竞态(已修复)原实现判定→关闭→pop 存在窗口,期间新请求可能拿到正在关闭的实例建任务。修复:关闭前打 _reaper_closing 标记(get_user_resources 见标记原地重建新实例)+ 关闭前二次确认 last_activity/运行任务(取消时清除标记)+ pop 前校验实例身份(server/context.py)。验证脚本新增 8a/8b 断言。
  4. 多智能体 idle 语义(第三轮修正,见 §9.1:初判认为 idle 计入运行可防误回收进一步排查发现其副作用更大REST 对账幽灵运行态 + 回收器永久阻塞),已在 commit e8bbaccf 修正为 idle 不算运行(对齐 socket 语义idle 实例的上下文靠 restore_running_tasks 磁盘恢复兜底。
  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.py11 项断言全过(新增 8a closing 重建 / 8b 二次确认)
  • _experiments/verify_model_fix.py:带 cid 切换落盘、不带 cid 不污染、重启后保持、/api/status?conversation_id= 对话级模型恢复全过
  • _experiments/test_no_cid_guard.py:空 cid 补建对话全过

9. 边界排查与修复2026-07-20 第三轮)

9.1 idle 多智能体误判运行已修复commit e8bbaccf

  • 现象get_conversation_running_statusserver/tasks/models.py的任务循环把 status="idle" 的多智能体任务计入 has_running_multi_agentidle 不在 TERMINAL_STATUSES={completed,failed,timeout} 内)。
  • 危害 1前端幽灵运行REST 对账对全 idle 的多智能体对话永远返回 active与 socket task_complete 语义chat_flow_task_main.py:2309 仅 running 实例+pending 消息)不一致 → 前端对账循环每 2.5s 误判失步 → 反复恢复轮询。
  • 危害 2回收器永久阻塞:回收判定依赖该方法,多智能体对话 terminal 永远无法被 TTL 回收,内存无界增长。
  • 修复:多智能体任务分支遇 idle 跳过(传统子智能体无 idle 态不受影响。idle 实例回收后由 restore_running_tasksidle ∉ 终态集合,会被恢复)按磁盘上下文重建,与进程重启路径一致。
  • 验证_experiments/test_reaper_unit.py 用例 5/5bidle 回收、running 保留)通过。

9.2 短 TTL 回收实测的环境教训(重要)

  • 8092 端口不是本项目服务:实测发现 8092 的进程 cwd 是 /Users/jojo/Desktop/外置/agents用户另一检出的服务03:02 启动),本仓库的 5 分钟 TTL 实测打在了错误目标上,结果全部无效。验证前必须用 lsof -nP -p <pid> | grep cwd 确认进程工作目录。
  • 沙箱限制(当前未解除)kill/ps 被 "Operation not permitted" 拦截;新服务实例在 socketio.run 绑定端口时被沙箱杀掉8093 实测)。因此无法杀旧服务、无法起新端口,短 TTL 实测只能在用户下次自然重启服务后进行。
  • 替代验证_experiments/test_reaper_unit.py 进程内直调 reap_idle_conversation_terminals19 断言全过):超 TTL 回收/未超保留/工作区级两段 key 跳过/运行中子智能体阻塞且 closing 复位/idle MA 回收/running MA 保留/无时间戳补齐。脚本用 ASTRION_DATA_ROOT 重定向运行态根,避免截断共享 debug_stream.log。
  • 日志共享坑host 模式所有实例(含外置检出)共写 ~/.astrion/astrion/host/logs/debug_stream.log,任何实例启动/导入都会截断import server.app 的测试脚本需设 ASTRION_DATA_ROOT 隔离。