一个编码 Agent 可以写出一个完成事件,同时仍然让监控给出错误答案
这个事件说的是「某一个回合结束了」。它没说用户是不是已经回来了、之后有没有新的回合开始、这条记录属不属于前台会话,也没说这个事件是不是已经旧到不配触发提醒。有用的监控要回答的是一个更窄的问题:这个新鲜的会话现在还需要用户吗
我们是在给 Agent Island 做 Claude Code 与 Codex 的本地状态检测时撞上这件事的。产品读取本地会话记录,显示一小组状态之一,任何会话内容都不必离开这台机器。难的不是找到一个 end_turn 或 task_complete 标记,难的是判断这个标记此刻意味着什么
先解析含义,再去看时间
Claude 和 Codex 写的记录格式并不相同。对 Claude 来说,一条助手记录可能带 end_turn、stop_sequence 或 stop 这样的终止原因;Codex 则发出 task_complete 和 turn/completed 这类事件记录
这些字符串只是第一道闸门。Claude 可能用一个长得像完成的助手信封来装一个 API 错误。把那条记录当成成功结束,恰恰会在会话撞到限额的时候产生一次假的「轮到你了」。所以解析器会先看错误标记,再决定要不要接受这个终止原因
解析器返回的是三个值,而不是一个布尔:
isDone 最近这个相关回合有没有结束
turnKey 这一回合的稳定身份
activityDate 这条记录携带的语义时间戳
回合键之所以重要,是因为同一条完成记录可能被观察到不止一次。监听器可能在写入后触发,兜底扫描也可能再看到同一个文件。只靠文件修改时间去重是脆弱的,因为无关的记账动作也可能碰过这个文件
文件时间是兜底
下一步是把回合时间戳和其它本地活动合起来看。比如 Claude Desktop 可能有它自己的会话活动时间戳。监控采用它已知的最新语义时间,只有当记录没有可用时间戳时,才退回到会话记录文件的修改时间
这个次序避免了一个常见错误:让文件系统的元数据来定义产品状态。修改时间只告诉我们字节变了,它不告诉我们为什么变
完成回合之后还有一层褶皱。桌面端的记账写入可能正好落在助手最后一条记录之后。如果每一个更新的桌面时间戳都立刻取消这次完成,监控就会藏掉一条有效提醒。Agent Island 给这些紧贴回合的写入留了一小段记账宽限期,宽限期之后很久才出现的活动,则按「用户回来了」处理
要紧的是这个比较关系,而不是那个具体时长:
语义活动时间 = max(回合时间戳, 外部会话活动)
如果一个已完成的回合仍然新鲜,且之后没有用户活动:
状态 = 需要你
否则如果近期活动仍在继续:
状态 = 工作中
否则如果一个此前在工作的会话安静了下来:
状态 = 卡住
否则:
状态 = 空闲
这就是为什么一次完成提醒更像一份租约,而不是一条永久事实。它在一段有界的时间里有效,并且可以被更新的证据撤销
单调推理仍然有极限
墙钟时间戳来自文件和事件负载,所以监控必须容忍缺失或不完美的日期。它还需要一个有界的注意力窗口。没有这个窗口,一台放了一个周末之后打开的笔记本,可能会把一次旧的完成报告得像刚刚发生
实现会把最近的「工作中」观测保留在内存里。如果一个会话此前在工作、随后停止产出,它可以在一段有限的时间里进入卡住状态。这个状态刻意与「需要你」分开:安静证明不了完成
这个模型是有代价的。更长的新鲜度窗口能照顾到走开的用户,但也提高了旧回合仍然可见的概率;更短的窗口减少过期提醒,却可能藏掉有用的完成。文件监听降低延迟,但周期扫描仍然必要,因为事件会被丢掉。上游记录格式变化时,解析器也得跟着演进
我们验证什么
状态解析器的测试覆盖了能区分用户消息、助手终止、API 错误信封、Codex 开始事件与 Codex 完成事件的记录。外层扫描器则检查新鲜度、之后的活动、卡住会话,以及最终把旧会话送回空闲的那个上限
公开的 v1.6.1 版本同时包含 macOS 与 Windows 产物。对本文而言,这只说明该版本可获取,并不能替代按平台做的行为测试,也不足以支撑关于额度、成本或报告等无关功能的声称
最后得到的规则简单到一句话就能说:对一个新鲜的、相关的、且未被取代的已完成回合发出提醒。但要走到这一步,需要身份、语义时间戳、后续活动检查和一条过期策略。完成事件是证据,状态是由多份证据共同做出的判断
