AgentTool 清理顺序与泄漏防线
这页回答什么
AgentTool的 sync / async / foreground→background 路径里,资源清理到底谁负责?代码里有哪些明确的防泄漏设计?还有哪些地方只是“尽量降低风险”,并不等于彻底无泄漏?
关键文件:
src/tools/AgentTool/AgentTool.tsxsrc/tools/AgentTool/agentToolUtils.tssrc/tasks/LocalAgentTask/LocalAgentTask.tsxsrc/tools/AgentTool/runAgent.ts
一句话结论
AgentTool 这一层已经明显在按“状态先落地,重清理后置”的思路写:
- task status 先完成迁移,避免 UI / TaskOutput 永远卡住
- 通知晚一点也行,但不能阻塞状态完成
- worktree cleanup / transcript classifier / summarization stop 都被当成可慢、可挂的附属步骤
- foreground→background 专门做了一次 iterator.return() + finally 转交,目的就是防止两套壳同时持有资源
所以它的设计重点不是“绝不出问题”,而是:
就算某些清理或 embellishment 卡住,也尽量别把主任务状态拖死。
1. 最核心的防线:先 completeAsyncAgent(),再做慢动作
无论是:
- async-from-start 路径(
runAsyncAgentLifecycle()) - foreground 中途 background 的 closure
代码都在强调同一件事:
completeAsyncAgent(agentResult, rootSetAppState)必须先发生。
然后才做:
classifyHandoffIfNeeded(...)cleanupWorktreeIfNeeded()/getWorktreeResult()enqueueAgentNotification(...)
源码注释已经点明:
这些 API call / git exec 可能 hang,不能 gate 住 task status transition。
这说明 Claude Code 把风险分成两类:
不能卡住的
- task 从
running→completed/failed/killed - TaskOutput(block=true) 解锁
- UI 不再显示任务还在跑
可以慢一点的
- handoff classifier
- worktree 检查与清理
- 结果通知装饰
这是整个清理设计最重要的一条主线。
2. summarization 的 stop 句柄是“每个壳一份”,不是共用
sync foreground 路径里有:
stopForegroundSummarization
background closure 里单独有:
stopBackgroundedSummarization
runAsyncAgentLifecycle() 里又有自己的:
stopSummarization
这不是代码重复,而是明确在切生命周期边界。
这说明什么
每个 execution shell:
- 自己启动 summarization
- 自己停止 summarization
- 不和另一壳共享 stop 句柄
这样可以避免:
- foreground stop 误伤 background summarizer
- background finally 再 stop 一次前台 summarizer
- 某个 closure 持有过期 stop handle
源码里甚至直接写了:
NOT shared with the backgrounded closure
这基本就是在明说:
summarization 生命周期是按壳隔离管理的。
3. foreground→background 的关键清理动作:agentIterator.return()
前台热切后台时,最重要的防泄漏动作是:
await Promise.race([
agentIterator.return(undefined).catch(() => {}),
sleep(1000)
])代码注释明确给出清理目标:
- MCP connections
- session hooks
- prompt cache tracking
- 其它
runAgent()/query()finally 里的资源
所以这里的设计不是“暂停前台 iterator”,而是:
尽可能让旧 iterator 正常走 finally,再把执行权交给后台壳。
这就是 foreground→background 不容易双持资源的核心原因。
4. backgroundPromise 放在循环外,是一个小但重要的泄漏修复点
AgentTool.tsx 里有非常直白的注释:
如果每轮 loop 都对同一个 pending
backgroundSignal再.then()一次,就会在 agent 生命周期里不断累积 reaction callback。
所以它改成:
- 在 loop 外只创建一次
backgroundPromise - loop 内只
Promise.race([nextMessagePromise, backgroundPromise])
这意味着它防的不是业务错误,而是更底层的:
长期运行 sync agent 时,对同一 pending promise 的监听器累积。
这个点很小,但说明作者确实在看“隐性资源增长”。
5. wasBackgrounded 是清理职责转交的开关位
sync foreground finally 里有几条关键逻辑:
- 如果
wasBackgrounded === true- 不清
dumpState - 不做 worktree cleanup
- 不把自己当 terminal sync task 去发 SDK 完成事件
- 不清
因为这些职责已经转交给 background closure 了。
这意味着
wasBackgrounded 不是 UI 标记,而是:
cleanup ownership handoff bit
没有这个位,就容易出现:
- foreground finally 清一次
- background finally 再清一次
- worktree 被提早删掉
- dumpState / skills 被重复清掉
所以这其实是热切换路径避免 double-cleanup 的总闸门。
6. worktree cleanup 被刻意放在“通知之前”
在 async/background terminal 路径里,代码顺序大致是:
- complete / fail / kill task status
cleanupWorktreeIfNeeded()enqueueAgentNotification(...),并把worktreePath/worktreeBranch带进去
这样设计的原因不是一定要先删目录,而是:
通知文本需要包含“最终 worktree 结论”。
具体语义:
- 没改动 → worktree 可删除
- 有改动 → worktree 要保留,并把 path/branch 传给通知
所以 worktree cleanup 同时承担两件事:
- 回收隔离环境
- 产出 terminal metadata
7. 但 worktree cleanup 仍然是潜在风险点
虽然主状态已经先完成,但 cleanupWorktreeIfNeeded() 依然可能:
- git hang
- 文件系统卡住
- hook-based worktree 无法可靠判定变化
代码里的处理方式是:
- 不让它阻塞 task status
- 但仍然 await 它,以便通知里能带 worktree 信息
所以这条风险被降级了,但没有被消灭。
准确说法应该是:
Claude Code 已经避免“任务假死”,但没有完全避免“cleanup 慢/挂”。
8. kill 路径的一个明显设计:状态迁移优先于 git cleanup
无论 runAsyncAgentLifecycle() 还是 backgrounded closure,AbortError 路径都强调:
killAsyncAgent(taskId, rootSetAppState)先做,再 cleanup worktree。
而 killAsyncAgent() 本身会:
abortController.abort()unregisterCleanup?.()- 状态改成
killed - 清掉 controller / cleanup handle
这里的意思非常明确:
先把任务从“运行中”摘掉,剩下的清理慢慢做。
所以 kill 路径的主目标是:
- 不让 UI / task system 继续认为任务还活着
- 之后再去处理 worktree、partial result、notification
9. enqueueAgentNotification() 有 notified 原子门,防重复消息
LocalAgentTask.tsx 里,enqueueAgentNotification() 会先原子检查:
- 如果 task.notified 已经是 true,就直接跳过
- 否则先标记,再入队
这说明通知系统也在防重入,特别是:
- TaskStopTool
- abort catch
- terminal path
- bulk kill / markAgentsNotified
这些路径之间可能重叠。
所以它的目标是:
允许多处“想发通知”,但最终只发一次。
这也是一种资源/状态一致性防线。
10. killAsyncAgent() 会清 abortController 和 unregisterCleanup
这是个很值得注意的小点。
kill 后任务状态里会被清掉:
abortControllerunregisterCleanupselectedAgent
意义是:
- 避免 task object 长期持有已经无用的控制器/cleanup closure
- 减少 resume / panel / retain 状态里继续挂老引用
- 明确 terminal task 的轻量化状态
这不是“彻底内存管理”,但确实是显式释放引用的动作。
11. runAsyncAgentLifecycle() 的 finally 很克制,只做全局映射清理
runAsyncAgentLifecycle() finally 里只做:
clearInvokedSkillsForAgent(agentIdForCleanup)clearDumpState(agentIdForCleanup)
它不碰:
- task status
- notification
- worktree cleanup
因为这些必须在 try/catch 的 terminal 分支里带着上下文做。
这其实是个不错的分层:
finally 负责
- 全局 side-map / registry 清理
try/catch terminal branch 负责
- status
- notification
- worktree metadata
- partial result
所以 finally 不是大杂烩,而是“无论成功失败都该清掉的 side registry”。
12. 当前看到的防泄漏设计清单
已明确存在的防线
backgroundPromise循环外创建,避免.then()累积- foreground→background 先
agentIterator.return() - foreground / background summarization stop handle 分离
wasBackgrounded防 double-cleanup- task status 先 terminal,再做慢 cleanup
enqueueAgentNotification()用notified防重复通知killAsyncAgent()清掉 abort / cleanup 引用- finally 清
clearInvokedSkillsForAgent+clearDumpState
已明确承认但未彻底消灭的风险
- git/worktree cleanup 仍可能 hang
- transcript classifier 仍可能慢
agentIterator.return()只等 1 秒,属于 best-effort,不是强保证- hook-based worktree 无法可靠判断变化,只能保留
13. 目前最值得继续追的未尽问题
A. runAgent() finally 里到底还释放了什么
目前只从注释知道有:
- MCP
- session hooks
- prompt cache tracking
可以继续进 src/tools/AgentTool/runAgent.ts 看 finally 块,把真实 cleanup 列表补全。
B. registerCleanup() / unregisterCleanup() 背后的 cleanup registry 语义
killAsyncAgent() 会调 unregisterCleanup?.(),但 registry 本身的粒度和生命周期还没完全展开。
C. stale worktree 回收与 resume 的竞态
resumeAgent.ts 里已经有 “bump mtime 防 stale-worktree cleanup 删掉刚恢复的 worktree” 的注释,这说明 worktree 生命周期管理还有另一层后台清理逻辑值得单独分析。
当前结论
AgentTool 这一层已经很明显不是“跑完再说”的粗放实现,而是带着几条非常明确的工程边界:
- 状态迁移优先,不要让任务假死
- 清理职责按 lifecycle shell 分配,不要前后台双清/漏清
- 全局 side registry 统一 finally 清
- 慢 cleanup 可以拖,但不该阻塞 terminal state
所以如果要评价它:
它不是绝对无泄漏,而是已经把最危险的“任务卡死、重复通知、前后台双持资源、worktree 误删/漏报”这些坑,尽量拆开并单独设了防线。