$ cat blog/subagent-alarm-noise

子线程结束,不该吵醒你

Turn alarm 只有在"确实需要人"的时候才有意义。Orchestrator 的 fan-out 差点毁掉这个承诺——而最直觉的修法根本不管用。

Agent Island 的全部工作就是一个承诺:当 Claude Code 或 Codex 的线程跑完、真的轮到你了,它把你叫回来——全屏弹窗、提示音、一个能跳回线程的按钮。而这个承诺死掉的方式只有一种:为没人等的回合弹窗。

编排式任务恰恰就是这样。给 Codex 一个大任务,它会派生出一堆子线程——执行器、review、compact。每个子线程都往 ~/.codex/sessions 写一份和真实会话一模一样的 rollout 文件,而且每隔几分钟就"完成一轮"。扫描器天真一点,结果就是弹窗风暴:一小时几十个弹窗,没有一个需要你。用户很合理地把它理解成"报警坏了"。

不管用的那次修复

每个 Codex rollout 的第一行都是 session_meta,里面有个 originator 字段。自动化流量在那里很好认:codex exec、probe、bridge。1.4.1 就按这些子串过滤,也确实消掉了一部分噪音。

然后收到反馈:在 Codex 应用里跑编排,子线程照样弹。把真实 session 文件 dump 出来,一行就说明了一切:

"originator": "Codex Desktop"   // 你亲手打开的交互会话
"originator": "Codex Desktop"   // 你从没碰过的派生子线程

派生出来的子线程,originator 和你手开的会话一字不差。只看这个字段,两者根本无法区分。过滤器盯错了信号。

真正的标记在哪

分隔符其实就在 session_meta 自己身上。机器驱动的线程带着交互会话永远不会有的 spawn 标记:

  • thread_source: "subagent" 和非空的 parent_thread_id——orchestrator 的子线程;
  • source: {"subagent": …} 对象(spawned/review/compact),{"internal": …}(probe)——而直接会话的 source 是 "cli" 这样的纯字符串;
  • "exec" / "mcp" 字符串——无头自动化。

在踩到这个 bug 的机器上,对 77 个真实 rollout 跑了一遍:按这些标记过滤,61 个机器线程被滤掉,16 个交互会话全部保留。originator 检查只作为旧式自动化的兜底留下。

两级过滤,一个开关

机器线程也分两种待遇:

  • 自动化——exec、MCP 流量、内部 probe。永不弹窗。没有人会守着批处理跑。
  • 子线程——orchestrator 的孩子们。默认静音,但设置里有开关(「子线程结束也弹窗」),留给真的盯着 fan-out、希望每个子线程结束都被打断的人。

这个开关比看上去重要:三种 subagent 标记会同时出现在同一个 rollout 上,所以每一种都必须尊重开关。只要有一种标记被无条件过滤,开关就悄悄失效——这个 bug 在 review 里被抓住了,没上线。

一道关于大小写的伤疤

更早的版本用过白名单——"originator 以 codex 开头的留下"。Codex 应用上报的是 Codex Desktop,大写 C。前缀检查没匹配上,所有桌面会话被当成自动化,最需要报警的人反而全静音了。留下两条教训:匹配一律大小写不敏感;宁可对已知的机器标记做黑名单,也不要对"看起来像人"做白名单——新的合法 originator 会不断出现,白名单恰好在错误的那一侧失效。


这套过滤随 Agent Island 1.5.1 在 macOS 和 Windows 同时发布,并带着风暴折叠:就算你打开了子线程弹窗,一波密集完成也只会弹最新的那一个。