runAgent finally 实际清理项

这页回答什么

之前在 AgentTool 分析里只从注释知道 foregroundbackground 会触发 runAgent() 的 finally,从而释放 MCP / session hooks / prompt cache tracking。那 runAgent.ts 里 finally 到底真实清了什么?

关键文件:

  • src/tools/AgentTool/runAgent.ts

一句话结论

runAgent() 的 finally 不是一句空泛的“做些清理”,而是确实在收一串 agent 级 side state:

  1. agent-specific MCP servers
  2. agent session hooks
  3. prompt cache break detection tracking
  4. cloned read-file state cache
  5. cloned fork context messages
  6. perfetto agent registry entry
  7. transcript subdir mapping
  8. AppState.todos 里的 agent entry
  9. 该 agent 启动的 background shell tasks
  10. 该 agent 启动的 monitor MCP tasks(feature gate 开启时)

所以前面说 foregroundbackground 要先 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 都要跑

这也解释了为什么 foregroundbackground 时必须尽量让旧 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. 为什么这页反过来证明 foregroundbackground 必须 return()

这页最关键的意义,其实不是列清单,而是反向证明前面的一个判断:

foregroundbackground 时,旧 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

这意味着 foregroundbackground / abort / terminal transition 里对 finally 的尊重,都是有真实资源语义的,不是形式主义。