maker-core: 后台 subagent 自动续跑 turn 结束后 turnInFlight 未清,UI 挂死 14 分钟且 Stop 失效(靠 60s manual-abort 兜底恢复)
- Dominant language
- TypeScript
- Stars
- 2.7k
- Forks
- 395
- Avg merge
- 21h 48m
- Merged PRs (30d)
- 776
Description
## 现象
2026-08-07(UTC,本地 +8)一个 Claude Code 会话在两个 `run_in_background: true` 审查 subagent 的自动续跑 turn 全部完成后,**UI 持续显示运行中约 14 分钟**;按 Stop 无效,最终由 `manual-abort` 的 60 秒兜底看门狗强关会话才恢复。用户体感为"turn 卡了 40 多分钟且停不掉"(其中 15:16→15:44 是真实工作,15:44→15:59 是挂死区)。
- Cindy session:`48182eb3-c649-46c9-91fe-8bc01a729d9e`
- Claude Code transcript:`5fb72562-3722-45f1-832d-fa1608304b08.jsonl`
## 时间线(UTC)
| 时间 | 事件 | 证据 |
|---|---|---|
| 15:16:04 / 15:16:13 | 一次 turn 内 spawn 两个后台审查 subagent(PR 2078 / 2080) | transcript `tool_use:Agent run_in_background:true` |
| 15:23:24 | subagent 1 完成,task-notification 送达,SDK 自动续跑 turn(不经 `handle.send`) | transcript queue-operation + user task-notification |
| 15:30:43 | subagent 2 完成,再次自动续跑;后续完成审查修复、push、PR 评论 | 同上 |
| **15:44:51** | 最后一条 assistant 消息,`stop_reason=end_turn` —— CLI 侧 turn 干净结束,之后 transcript 零活动 | transcript 末条 assistant |
| 15:44→15:58 | 死区:host 侧 `isTurnRunning()` 仍为 true,UI 转圈 | session ndjson 该区间无任何 turn end 记录 |
| ~15:58:34 | 用户(手机端)按 Stop → `session.abort()` → `q.interrupt()` 对已结束 turn 无事可中断,标记不清 | — |
| 15:59:34 | 会话日志唯一 error:`turn still running after abort — closing session so the next send can rebuild it, trigger: manual-abort, graceMs: 60000` | session ndjson |
| 15:59:58 | 下一条用户消息触发 `startSession` 重建,恢复正常 | session ndjson |
已排除:日志中的 502 重试与 `rate_limit_event`(seven_day 警告)均发生在死区之前,非诱因。两个 subagent 均正常完成并交付,不是 subagent 悬挂。
## 根因分析
**自动续跑 turn 的终态没有在 host 侧落账。**
后台 subagent 完成后,SDK 经 task_notification 自动续跑新 turn,不经过 `handle.send`,turn 记账依赖 `packages/maker-core/src/agents/claude-code/index.ts` 中的补登记逻辑(`turnInFlight` 补登记 + `beginNewTurn`)。该处注释自己已指出风险:`beginNewTurn` 的 generation++ 可能让 translator 把随后的 result 当作"已被新 send 接管"而**吞掉终态,turn 永远收不了尾**。本次表现与此一致——最后一个自动续跑 turn 在 CLI 侧以 `end_turn` 干净结束,但 host 侧 `turnInFlight` 恒 true。
无法从现有日志进一步区分"CLI 未发 result"还是"translator 吞掉了 result":`SDK ◀ turn end (result)` 是 **debug 级**(`translator.ts:1550`),session ndjson 只保留 info+,正常 turn 结束在日志里完全不可见。
Stop 停不掉是既知路径:`session.ts` `abort()` 的注释明确写了"claude 的 q.interrupt() 可能失败且不清 turn-in-flight 标记……isTurnRunning() 恒 true(review #944 第十一轮 P1)",`MANUAL_ABORT_RECOVERY_GRACE_MS = 60_000` 的兜底**按设计工作了**,但:① 要等满 60 秒;② 强关前 UI 无任何"正在强制恢复"反馈,体感即"停不掉"。
## 建议修复
1. **自动续跑 turn 的终态对账**:CLI 子进程输出空闲 + 无 pending continuation claim 但 `turnInFlight === true` 超过阈值 → 自动走 stall 恢复。现状是 `STALL_ABORT_RECOVERY_GRACE_MS` 看门狗只在 abort 之后才排,平时无人监管此状态。
2. **`SDK ◀ turn end (result)` 从 debug 提到 info**:`session.ts` 的探针注释自己说明 claim 创建是"continuation 悬挂 vs done 未到达"的关键区分点,但正常终态在留存日志里不可见,复现时无法归因。
3. **Stop 的可感知恢复**:按下 Stop 后 UI 提示"若 60 秒内无响应将强制重建会话",把兜底从"看起来失灵"变成明确的恢复流程。
## 影响
- 后台 subagent + 自动续跑是常用模式(长审查/长测试),每次踩中都是 10+ 分钟不可用 + 手动 Stop 失灵。
- 兜底能自愈,不丢数据(重建后 resume 正常),故非 P0;但复现率与 continuation 补登记路径的复杂度正相关,建议优先补第 2 条日志改动以便下次归因。
Contributor guide
Research direction
Read packages/maker-core/src/agents/claude-code/index.ts and trace automatic continuation registration through translator.ts and session.ts. Check how turnInFlight, beginNewTurn, abort(), and the stall watchdog interact; use the supplied session and transcript identifiers to compare the host state with CLI turn completion. Done means automatic turns cannot remain marked running after completion, and the retained logs and Stop recovery provide the proposed visibility.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- ai, backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100