6.7 KiB
6.7 KiB
调研记录
用户现象
- 助手正文和两个 Shell Command 均已完整结束,工具退出码为 0。
- 页面底部仍显示“运行中”,停止按钮仍存在。
- 随后发送“还在么”时插入失败,服务端提示:
Codex 任务失败:no active turn to steer。
初始假设
- cc-web 的本地/持久化会话仍认为 turn 正在运行,而 Codex app-server 已释放 active turn。
- 发送路径依据陈旧运行态选择
steer,缺少对 app-server 明确“无活动 turn”响应的状态修复与普通turn/start降级。
工作区约束
- 检查开始时 Git 工作树干净。
.planning/.active_plan属于并发中的subagent-card-metadata,本任务使用独立 scoped 目录且不覆盖 active 指针。- Trellis 当前任务也属于其他工作,本任务不切换共享指针。
项目上下文
- 独立计划审查已通过,无阻塞问题;建议在进度记录中保留实际测试命令与 60 秒超时证据。
codebase-memory-mcp项目home-cc-web索引状态为ready,当前包含 3246 个节点、7821 条边,无需重新全量索引。
初步代码命中
- 后端
handleCodexAppSteerMessage直接读取activeCodexAppTurns中的threadId/turnId,请求turn/steer;失败分支目前只发送failed状态与codexapp_steer_failed错误,未见状态自愈或普通 turn 降级。 - 前端
handleServerMessage只把codex_app_steer_status映射成 pending/inserted/failed 展示;是否运行的权威来源仍需沿done、session_info和session_list消息继续确认。 handleLoadSession返回的isRunning来自服务端isSessionRunning(sessionId),说明截图底部“运行中”不是单纯 DOM 残留,核心漂移大概率位于后端活动 turn Map 或其终态通知处理。- mock 已有
turn/steer和turn/completed基础能力,scripts/regression.js也已有运行中 steer 正常路径,可在其附近追加竞态回归。
状态机证据
isSessionRunning直接以activeCodexAppTurns.has(sessionId)判定 Codex App 运行态;只要 Map 条目没有删除,所有页面和再次发送都会继续认为 turn 活跃。processCodexAppNotification只有收到turn/completed或error才返回done: true;handleCodexAppNotification随后调用handleCodexAppTurnComplete删除活动条目并广播done。handleMessage在持久化新普通消息之前先检查activeCodexAppTurns,存在条目便无条件转入handleCodexAppSteerMessage。- steer 路径会先把用户消息持久化,再异步请求
turn/steer。失败分支目前仅显示错误,不删除陈旧活动条目,也不为已经持久化的用户消息启动新 turn,因此会同时造成“持续运行中”和“消息插入失败”。 - 最小安全修复应只识别 app-server 的权威终态/turn 不匹配错误:先收敛仍指向同一旧 entry 的本地状态,再用已持久化的用户消息启动新 turn;不能再次经过
handleMessage,否则会重复写入用户历史。
前端恢复态证据
- 前端用
isGenerating控制停止按钮和“插入”发送分支,用currentSessionRunning控制“运行中”徽标,两者不是同一状态源。 - 正常
done会调用finishGenerating同时清理两者;但断线/切后台后收到权威resume_session_result(isRunning=false)时,仅清理徽标并重新读取仍为 true 的isGenerating,因此停止按钮和 steer 视觉状态可继续残留。 - 前端最小修复是在当前会话的 idle 恢复结果中调用
finishGenerating;该函数已有空 streaming bubble 与历史计数保护,无需单独操作停止按钮。 - 本次先不扩大到 resume requestId/epoch 的乱序重构;现有服务端会以同一 requestId 先发
resume_generating再发最终resume_session_result,直接在前一个事件清 pending 会让简单匹配方案误拒绝最终响应。
修复范围决策
- 后端:只对
no active turn to steer这类明确“无活动 turn”的权威错误自动收敛并新开 turn;不把一般 expectedTurnId 不匹配、超时或网络错误视为安全降级条件。 - 前端:用权威 idle 恢复结果收敛本地生成态,解决漏收
done后的停止按钮残留。 - 暂不扩展
thread/status/changed;其非 systemError 状态可能出现在 turn 开始前,笼统作为终态会误结束真实任务。
测试入口
- 项目完整回归入口为
timeout 60s npm run regression。 - 现有
scripts/regression.js --target只有 composer、未路由通知和子代理卡片三个定向目标;本次应新增独立 stale-turn 目标,避免每次红绿循环都启动完整回归。 - 前端已有
assertFrontendGenerationControlsContract与assertSessionSwitchResilienceContract源码合同,可在前者或本次新目标中断言 idle resume 必须调用完整生成态收敛函数。
实现结果
- mock 新增确定性场景:app-server 内部 turn 已结束但故意不发
turn/completed,下一次 steer 返回真实no active turn to steer。 - 后端新增窄匹配分类器,仅在旧 entry 身份仍一致时调用现有完成流程保存输出并清理状态,再直接以已持久化消息启动新 turn;一般 expectedTurnId 不匹配继续报错。
- 前端 idle
resume_session_result改为调用finishGenerating,同步清理isGenerating、停止按钮、streaming 状态与运行徽标。 - 定向回归覆盖消息/输出不重复、自动转新 turn、最终 idle、无失败 toast,以及 expectedTurnId 不匹配负例。
独立审查修正
- 首轮实现会在 follow-up 用户消息之后追加旧 turn 助手输出,刷新后历史顺序错误;现通过 timestamp+content 定位,把旧助手消息插入 follow-up 之前,并补严格索引顺序断言。
- 完成旧 turn 时同步 flush 跨会话回复可能先创建新 entry;现仅在 stale replacement 期间延迟 flush,并在替代 turn 启动前再次确认 Map 为空。
done会短暂清掉前端isGenerating;现替代 turn 创建成功后、首个 delta 前发送空resume_generating,恢复停止/插入控制状态。- 替代 turn 优先沿用旧 entry 的
mcpContext,避免线程级 MCP 上下文退化。 - 独立质量审查最终通过,无阻塞问题。
部署验证
- 重启前通过 ccweb 会话列表确认只有当前对话处于
running,不存在会被中断的其他会话。 pm2 restart ccweb --update-env导致调用连接中断,但 PM2 进程创建时间、restart 计数与启动日志确认重启已经执行。- 重启后
ccweb状态为online,unstable restart 为 0,事件循环 P95 约 1.61 ms。 - 本地
http://127.0.0.1:8002/返回 HTTP 200,探针耗时约 3.9 ms。