runAgent finally 实际清理项
这页回答什么
之前在
AgentTool分析里只从注释知道 foreground→background 会触发runAgent()的 finally,从而释放 MCP / session hooks / prompt cache tracking。那runAgent.ts里 finally 到底真实清了什么?
关键文件:
src/tools/AgentTool/runAgent.ts
一句话结论
runAgent() 的 finally 不是一句空泛的“做些清理”,而是确实在收一串 agent 级 side state:
- agent-specific MCP servers
- agent session hooks
- prompt cache break detection tracking
- cloned read-file state cache
- cloned fork context messages
- perfetto agent registry entry
- transcript subdir mapping
- AppState.todos 里的 agent entry
- 该 agent 启动的 background shell tasks
- 该 agent 启动的 monitor MCP tasks(feature gate 开启时)
所以前面说 foreground→background 要先 agentIterator.return(),不是抽象洁癖,而是真为了把这批 per-agent 资源交给 finally 释放。
runAgent finally 原文骨架
src/tools/AgentTool/runAgent.ts 末尾 finally 的顺序大致是:
await mcpCleanup()
if (agentDefinition.hooks) clearSessionHooks(...)
if (feature('PROMPT_CACHE_BREAK_DETECTION')) cleanupAgentTracking(agentId)
agentToolUseContext.readFileState.clear()
initialMessages.length = 0
unregisterPerfettoAgent(agentId)
clearAgentTranscriptSubdir(agentId)
remove prev.todos[agentId]
killShellTasksForAgent(agentId, ...)
if (feature('MONITOR_TOOL')) killMonitorMcpTasksForAgent(agentId, ...)这已经足够说明:
runAgent()自己就是一层“agent-scope resource owner”。
1. await mcpCleanup()
这是 finally 里的第一项,而且是 await。
语义很明确:
- 清理这个 agent 专属的 MCP server / MCP 连接
- normal completion / abort / error 都要跑
这也解释了为什么 foreground→background 时必须尽量让旧 iterator 的 finally 跑起来:
否则前台壳拉起的 MCP 资源可能继续悬着。
2. clearSessionHooks(rootSetAppState, agentId)
只有 agent 定义里有 hooks 时才清。
说明 session hooks 不是全局一次性状态,而是:
- agent 启动时可注入
- agent 结束时按
agentId回收
这也是典型的 per-agent side registry。
3. cleanupAgentTracking(agentId)
这条只在 PROMPT_CACHE_BREAK_DETECTION feature 开启时发生。
含义是:
- prompt cache break detection 会为 agent 建 tracking state
- finally 负责把该 tracking state 回收
所以前面注释里说 foreground iterator finally 会释放 prompt cache tracking,不是概念描述,代码里确实有对应项。
4. agentToolUseContext.readFileState.clear()
这条很重要,但之前容易被忽略。
它清的不是磁盘文件,而是:
agent clone 出来的 read-file state cache memory
也就是说,AgentTool 运行时对 FileRead / read state 做了 agent-scope 缓存,这块不会完全依赖 GC 自己慢慢回收,而是 finally 主动 clear()。
这类缓存如果不清,在多 subagent 场景下会是很典型的小内存泄漏源。
5. initialMessages.length = 0
这条更细。
它不是新建数组替换,而是直接把:
- cloned fork context messages
原地截断到 0。
说明作者就是在主动释放:
- fork / inherited conversation context
- 这些消息对象上的引用链
为什么不是等 GC? 因为长会话里 agent 会很多,保留这些 inherited messages 会把历史上下文链挂得很长。
6. unregisterPerfettoAgent(agentId)
这说明 agent 还会进 perfetto tracing / registry。
finally 主动 unregister 的意义很简单:
- tracing registry 不会因为 agent 结束而残留 entry
- 避免“已经结束的 agent 还在 registry 里被当成活跃实体”
这属于 observability state cleanup。
7. clearAgentTranscriptSubdir(agentId)
说明 transcript sidechain/side storage 还维护了:
- agentId → transcript subdir 的映射
finally 会把这条映射清掉。
注意这不是删 transcript 文件,而是:
清理 runtime mapping,而不是抹掉持久记录。
这和前面的 recordSidechainTranscript(...) 很配套。
8. 清 AppState.todos[agentId]
finally 里有一段非常直白的注释,大意是:
- subagent 只要调用过
TodoWrite - 即使最后 todos 变成空数组
prev.todos里那个 key 也会一直留着- whale sessions 会产生几百个 agent
- 每个 orphaned key 都是小 leak,积少成多
所以 finally 直接:
- 检查
agentId in prev.todos - 存在就把这个 key 删掉
这几乎是最明确的一条“作者亲自承认过的 leak 修复”。
9. killShellTasksForAgent(agentId, ...)
finally 还会主动杀掉这个 agent 拉起的 background shell tasks。
注释也很直:
否则
run_in_background的 shell loop 会在 agent 结束后继续活着,等主 session 退出后甚至可能变成 PPID=1 孤儿/zombie 风格残留。
这说明 Claude Code 明确意识到:
agent 生命周期结束,不等于其子 shell 生命周期会自动结束。
所以这里必须显式按 agentId 回收 shell task。
这是非常实打实的泄漏/孤儿进程防线。
10. killMonitorMcpTasksForAgent(...)
如果 MONITOR_TOOL feature 开着,finally 还会杀掉这个 agent 关联的 monitor MCP tasks。
这和 shell task cleanup 一样,都是:
- 子任务不是自然跟着 agent 自动收束
- 必须额外按 agent scope 回收
说明 Claude Code 的 agent 结束,不只是“主循环结束”,而是:
还要主动收 agent fan-out 出去的附属任务。
11. 为什么这页反过来证明 foreground→background 必须 return()
这页最关键的意义,其实不是列清单,而是反向证明前面的一个判断:
foreground→background 时,旧 iterator 不能直接丢着不管。
因为一旦不让它 finally 跑:
- MCP cleanup 可能不跑
- session hooks 可能不清
- prompt cache tracking 可能残留
- readFileState cache 可能不 clear
- inherited messages 可能继续被数组引用
- perfetto registry / transcript mapping 可能残留
- todos key 可能泄漏
- background shell / monitor tasks 可能继续挂着
所以 agentIterator.return() 真不是礼貌性收尾,而是:
旧 agent-scope runtime state 的释放入口。
12. 当前可以更准确地描述 runAgent()
之前如果说:
runAgent()是本地 agent 执行引擎
现在可以补一句:
runAgent()同时也是一个 agent-scope resource container,它在 finally 里统一释放自己在执行过程中注册的 MCP、hooks、cache、transcript mapping、todo state、shell/monitor 子任务等附属资源。
这比单说“runAgent 会 finally 清理资源”准确得多。
13. 还值得继续追的两个点
A. mcpCleanup() 的真实边界
finally 里虽然 await 了它,但它具体清的是:
- 连接?
- server process?
- tool registration?
- 认证态?
这还值得单追。
B. killShellTasksForAgent() / killMonitorMcpTasksForAgent() 的粒度
现在知道 finally 会杀,但:
- 靠什么索引到 agentId
- 杀的是 task state 还是 OS process
- 是否存在 race / best-effort
还没完全拆开。
当前结论
runAgent.ts finally 已经把“前面注释里说的那些清理”落实成了具体代码,而且范围比预想还大。
最重要的不是某一条清理,而是整体结构:
- 连接态:MCP、hooks
- 缓存态:prompt cache tracking、readFileState
- 上下文态:initialMessages、transcript subdir
- 观测态:perfetto registry
- 应用态:todos key
- 子任务态:background shell tasks、monitor MCP tasks
这意味着 foreground→background / abort / terminal transition 里对 finally 的尊重,都是有真实资源语义的,不是形式主义。