fix(desktop): Codex 流断开后会话残留 active,输入与模型切换失效
- Dominant language
- TypeScript
- Stars
- 2.7k
- Forks
- 401
- Avg merge
- 21h 48m
- Merged PRs (30d)
- 776
Description
## 摘要
Cindy Desktop 0.1.36 在 Codex 会话的响应流被客户端断开后,未能把会话状态收口到可恢复状态。受影响会话会在侧边栏显示红点,但仍保留 `active` / 忙碌态;用户无法继续发送消息,也无法切换模型。
这不是单纯的模型服务失败:同一时间上游请求返回 200,但客户端流被断开,随后本地会话没有得到可操作的终态或恢复入口。
## 环境
- 客户端:Cindy Desktop 0.1.36
- 平台:macOS darwin arm64
- 会话标题:`PR Review Follow-up`
- sessionId:`1d28a9aa-7db7-4901-8515-ea9725788c81`
- agent:Codex
- model:`codex/gpt-5.6-sol`
- threadId:`019fd7c3-f176-7462-a16f-13a1690133fb`
- 工作目录:FiloMail 项目
## 复现路径
1. 在 Desktop 中创建或打开一个 Codex 会话,并让其执行较长的 PR review/follow-up 工作。
2. 在响应流进行期间触发客户端连接重建、device-link relay 重启、窗口/renderer 重新订阅,或其他会导致响应客户端断开的场景。
3. 等待会话出现红点或响应流停止。
4. 在同一会话中发送新消息,或尝试切换模型。
当前事故的可观测触发线索是 device-link 服务重启和 relay 504;需要补充确定性测试,覆盖纯本地 renderer/响应流断开场景。
## 实际行为
- 会话边上出现红点。
- 发消息没有可见响应,AI 不继续处理。
- 模型选择器不可切换或切换无效。
- 会话仍被 UI/状态层视为 active/busy。
- 后端可能仍产生 Codex 请求或输出,但客户端没有收到完整终态。
- 用户只能重启 Cindy,或切换到新会话才能恢复操作。
## 期望行为
响应客户端断开后,系统必须在有界时间内完成以下之一:
1. 重新连接并 rehydrate 原会话,恢复正常输入和模型切换;或
2. 将当前 turn 收口为明确、可操作的失败/中断状态,清除 active-turn、recovery、steering 和队列阻塞,并提供一次安全的重试入口。
红点应能解释具体原因;不能让会话永久停留在 active/busy,也不能要求用户重启整个应用才能继续。
## 日志证据
本机 `main-2026-08-07.log`:
- 18:26:42:`device-link disconnected (code=1012, reason=service restart)`
- 18:27:01:`device-link request timeout`,请求为 `local-db:sessions:list`
- 18:27:03:device-link API 返回 504
- 18:27:05:relay connection error,HTTP 504
- 18:27:17:`device-link:subscribe` 连续超时,目标设备 responsiveness circuit opened
本机 `agent-2026-08-07.ndjson`:
- 同一 Codex thread 的上游请求多次记录 `status: 200`。
- 随后反复出现:`client disconnected mid-response — aborting upstream request`。
- 这说明至少有一条路径是“上游请求正常返回/正在生成,但客户端响应订阅先断开”,而不是单纯的 401/403/429/500 模型错误。
## 初步根因判断
疑似是 Desktop Main、renderer 输入协调器和 Codex response stream 的生命周期没有在客户端断开边界统一收口:
- proxy 在客户端断开后会 abort upstream request;
- session/coordinator 没有可靠地产生对应的 terminal error/interrupted/recovery checkpoint;
- 持久化或内存中的 active-turn / queue / steering 状态可能残留;
- renderer 只收到红点或部分状态,却没有恢复输入和模型选择的状态广播。
需要重点确认:
1. `client disconnected mid-response` 到底由 renderer 重订阅、device-link 重连、窗口生命周期,还是 session stream listener 被替换触发。
2. 断开后是否进入了统一的 turn error/abort cleanup 路径。
3. `activeTurn`、`isTurnRunning`、recovery、pending queue、steering marker、`sessions.status` 和 UI projection 是否在同一 generation 下收口。
4. 当上游已经返回 200、但客户端断开时,是否存在“请求可能已经执行”的重复重试风险。
## 建议修复方向
- 将 response client disconnect 接入统一的 session terminal/recovery 协议,不允许只在 proxy 层 abort upstream 后静默结束。
- 为该路径持久化明确的失败/中断状态和可重试 checkpoint。
- 原子清理旧 turn 的 active/recovery/steering/queue 状态,同时使用 session generation/clientId 防止迟到事件清理新 turn。
- renderer 重新连接或 rehydrate 后,重新拉取 authoritative session/input projection,而不是依赖断连前的本地 running 快照。
- 仅对明确未接受的请求自动重试;对已经进入上游或结果未知的请求,保留幂等边界,避免重复执行 PR review 操作。
- 红点应关联可操作的错误原因和恢复动作,而不是成为唯一反馈。
## 风险
不能通过无条件清除 active 状态或无限重发消息来修复,否则可能在上游已经接受请求时造成重复执行、重复修改文件或重复发送 PR 评论。修复应优先保证 generation、clientId 和 accepted/unknown 状态边界正确。
## 相关记录
- Issue #2045:上游 403 后会话仍显示运行中,继续消息进入队列且无法正常派发;用户表现和 active-turn/队列失同步相近,但本事故日志显示上游请求也可能返回 200。
- Issue #1932:回合完成信号丢失后会话永久显示思考中,后续消息无法派发;属于相同的会话生命周期/状态收口问题族。
- PR #1642:Codex retry recovery stateful,最接近恢复路径,但未明确覆盖客户端 response stream 断开。
- PR #2041:queue-head recovery 解锁,覆盖相邻的 rehydrate/队列卡死路径。
- PR #1385:增加 device-link 断开原因可观测性,但明确不修 device-link 根因。
- PR #1643:不属于本问题,主要处理晚到 agent 终态与模型目录快照。
## 验收标准
- 新增 Desktop/Codex 回归测试,模拟上游正常返回或生成期间客户端 response stream disconnect。
- 断开后旧 turn 在有界时间内进入明确的 completed/failed/interrupted/recoverable 状态,不再永久 active/busy。
- 用户发送的新消息可以正常派发,或明确显示可重试/可取消状态。
- 模型选择器恢复可用;如果当前 turn 仍确实在执行,界面必须展示真实状态和可操作的停止/恢复入口。
- renderer/device-link rehydrate 后能从 Main 的 authoritative snapshot 恢复 session 和 input queue projection。
- 迟到的旧 response/event 不得清理或覆盖新的 turn。
- 对 response 已可能到达上游的请求,不发生无幂等保护的重复派发。
- 红点包含明确原因或可进入恢复动作,不再只有无解释的视觉标记。
- 覆盖普通本地 Desktop、device-link 重连、renderer 重订阅和应用重启后的恢复场景。
Contributor guide
Research direction
No source files or tests are named. Trace the `client disconnected mid-response` path through Desktop Main, the renderer input coordinator, and the Codex response stream, then inspect the related recovery work in PRs #1642 and #2041. Add deterministic disconnect and rehydrate tests covering terminal cleanup, authoritative session projection, stale events, and retry safety.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- electron, typescript
- Domain
- backend-api-design, desktop, testing-qa
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100