一条编码 Agent 的通知可以是正确的,同时时机很糟。如果 Claude Code 在它的终端处于最前面时结束,一个完整提醒会盖住用户正在读的那段回复。而把来自终端或 VS Code 的提醒一律压掉也不是答案:另一个会话可能正在别的窗口里等着
条件必须更窄:此刻处于最前面的,是不是正好承载这个会话的那个应用?Agent Island v1.7.1 在 macOS 与 Windows 上用的就是这个检查:答案已经露在眼前时按住提醒,焦点变化后如果这个回合仍然需要处理,再把它放出去
从会话身份出发,而不是应用白名单
「终端在前台时不提醒」这类白名单会丢掉会话身份。一个用户可能同时有好几个终端、编辑器内置的 CLI 和后台会话。只有当前台应用拥有那个 CLI 进程、且它的工作目录与在等的线程匹配时,前台应用才是相关的
已发布的实现走的是这条链:
在等的线程
-> 记录下来的工作目录
-> 匹配的 claude 或 codex 进程
-> 父进程链
-> 前台应用进程
macOS 读取进程路径、工作目录和父 PID。Windows 取一次进程快照,匹配 claude、codex、node 或 bun,读取进程工作目录,再沿父链走到前台窗口所属的进程。两边的父链回溯都限制在 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 可能把你带进一个毫不相干的进程
给一条懂焦点的提醒写的测试
- 正好这个会话在最前面:按住提醒
- 同一个应用里的另一个会话在最前面:投递
- 回合还开着,用户切走了:投递一次
- 用户在切走之前已经回复:丢弃被按住的提醒
- 宿主无法解析:fail open,照常投递
- 好几个子 Agent 同时结束:先收敛突发,再套用焦点逻辑
- App 启动时存在旧的等待回合:把历史当基线,而不是重放它
这件事证明不了什么
前台匹配证明不了用户读了那段回复,它只证明承载那个 CLI 的应用是可见的。这个设计是在一个很窄、且可撤销的条件下抑制一次打断,它不声称能追踪注意力
这里描述的行为已随 Agent Island v1.7.1 发布。macOS 与 Windows 的实现都在公开仓库里,可供查看

