抑制编码 Agent 的提醒,但不漏掉那次交接

一条编码 Agent 的通知可以是正确的,同时时机很糟。如果 Claude Code 在它的终端处于最前面时结束,一个完整提醒会盖住用户正在读的那段回复。而把来自终端或 VS Code 的提醒一律压掉也不是答案:另一个会话可能正在别的窗口里等着

条件必须更窄:此刻处于最前面的,是不是正好承载这个会话的那个应用?Agent Island v1.7.1 在 macOS 与 Windows 上用的就是这个检查:答案已经露在眼前时按住提醒,焦点变化后如果这个回合仍然需要处理,再把它放出去

从会话身份出发,而不是应用白名单

「终端在前台时不提醒」这类白名单会丢掉会话身份。一个用户可能同时有好几个终端、编辑器内置的 CLI 和后台会话。只有当前台应用拥有那个 CLI 进程、且它的工作目录与在等的线程匹配时,前台应用才是相关的

已发布的实现走的是这条链:

在等的线程
  -> 记录下来的工作目录
  -> 匹配的 claude 或 codex 进程
  -> 父进程链
  -> 前台应用进程

macOS 读取进程路径、工作目录和父 PID。Windows 取一次进程快照,匹配 claudecodexnodebun,读取进程工作目录,再沿父链走到前台窗口所属的进程。两边的父链回溯都限制在 24 跳以内

是按住,不是确认

被抑制的提醒不是已投递的提醒。它被停放在一个以服务商、会话身份和回合身份为键的表里。这个区分挡住了两个常见 bug:

  • 把一个回合标成已通知,而用户其实从没看到过提醒
  • 在用户已经回复之后,才弹出一条过期提醒

焦点变化时,系统会重新检查这个回合。只有当提醒仍然启用、这个回合仍在「需要你」集合里、并且它的投递键既未被确认也未被投递时,才会真的投递。如果一次新鲜的记录扫描显示用户已经回复,被按住的那一条就会被丢掉

if exact_session_is_frontmost:
    held[delivery_key] = waiting_turn
else:
    deliver(waiting_turn)

on_focus_change:
    for each held turn:
        if no_longer_waiting: discard
        else if exact_session_is_still_frontmost: keep holding
        else: deliver once

两端都用事件驱动的时序

Agent Island 在投递前本来就有一秒的确认缓冲。这段窗口里,一次记录写入可以触发更新鲜的扫描,取消一个用户已经回答过的回合。前台按住又加了一道事件边界:macOS 上的应用激活,Windows 上的 EVENT_SYSTEM_FOREGROUND。没有任何前台轮询循环

于是有三次机会在不丢掉交接的前提下压掉噪音:在确认缓冲期内取消、在正好那个宿主可见时按住,以及在下一次状态扫描发现回合已不在等待时丢弃

进程归属不清楚时要 fail open

进程祖先链并不总能还原。tmux 会话可能被重新挂接,容器可能藏住相关进程,权限也可能挡住工作目录的读取。这些情况下解析器返回否,提醒照常走。少压掉一次,比悄悄丢掉一次真实交接损害小得多

路径归一化同样要紧。macOS 上 /tmp/private/tmp 可能指向同一个位置;Windows 上大小写与分隔符的差异不能让匹配失败。Windows 还会在跟随父链之前先检查进程创建时间,否则被回收的 PID 可能把你带进一个毫不相干的进程

给一条懂焦点的提醒写的测试

  1. 正好这个会话在最前面:按住提醒
  2. 同一个应用里的另一个会话在最前面:投递
  3. 回合还开着,用户切走了:投递一次
  4. 用户在切走之前已经回复:丢弃被按住的提醒
  5. 宿主无法解析:fail open,照常投递
  6. 好几个子 Agent 同时结束:先收敛突发,再套用焦点逻辑
  7. App 启动时存在旧的等待回合:把历史当基线,而不是重放它

这件事证明不了什么

前台匹配证明不了用户读了那段回复,它只证明承载那个 CLI 的应用是可见的。这个设计是在一个很窄、且可撤销的条件下抑制一次打断,它不声称能追踪注意力

这里描述的行为已随 Agent Island v1.7.1 发布。macOS 与 Windows 的实现都在公开仓库里,可供查看