你的告警代码不该直接解析 Claude 和 Codex 的日志
如果每个消费方都自己去解析原始的 Agent 日志,补全通知就会变得不可靠。Claude Code 和 Codex 都会以 JSONL 格式追加记录,但两者使用的事件结构并不相同。Claude 的补全信息出现在 message.stop_reason 里;Codex 则使用 task_complete、turn/completed 这类事件名。当菜单栏、通知模块和会话列表各自去解析这些细节时,迟早会互相不一致。
解决办法是:把供应商的具体信息封装成三个字段。应用的其他部分只需要一个很小的结果:
isDone—— 最新的相关事件是否已经交回控制权?key—— 这条结果描述的是哪一轮对话?
activityDate:语义事件何时发生?
Swift 实现中把这个概念称为 SessionTurnStatus,而 Windows 移植版则用一个 C# 的 record struct 承载相同字段。这条边界之上的代码,无需关心具体是哪个提供者产生的证据,就能判断一个对话轮次(turn)是否已完成。
从最新证据开始反向扫描
一旦新的用户消息开启另一个轮次,旧的补全记录就不再是当前状态。因此,分类器会反向扫描一个有界对话记录尾部,并在识别到第一个事件时停止。新用户事件返回“工作中”状态,提供者特定的补全事件返回“完成”。未知记录则跳过,不视为空闲。这条排序规则比具体的 JSON 属性名更重要。一个正向扫描(记住最后一次补全)可能会在后续工作已经恢复后,仍报告过时的手动交接(handoff)。
剔除含义正确的无效记录
一条 JSON 记录即使格式正确,也可能不是正确的补全信号。例如,Claude Code 可能针对 API 或速率限制错误,发出一个 stop_sequence 的助手信封记录,且同时携带 isApiErrorMessage: true。如果只检查停止原因,就会触发错误的“轮到你了”(your-turn)警报。子代理(subagent)事件也需要类似防护。isSidechain 补全属于后台链,而非父对话,如果让它补全前台会话,就会把有效证据和相关信息混淆。这些排除规则同时存在于两个平台的分类器以及测试用例集中。如果移植规则时没有附带上失败的例子,那么跨平台一致性就无法持久。
保持轮次标识的唯一性
文件系统事件和轮询可能会对同一个轮次进行多次分类,因此结果需要一个稳定键值,以便警报代码去重。分类器优先使用记录中已有的标识符,包括 uuid、id、turn_id、item_id 和 call_id。仅当提供者没有给出可用标识符时,才会派生一个有限的备用标识符。通知代码应当消费这个键值。如果通过文件路径或扫描时间戳重新构建标识,就会产生第二个不兼容的轮次定义。
用测试追踪代替孤立的辅助函数
有用的测试夹具应当模拟真实转录记录中的失败场景:例如一个旧的完成事件后紧跟一个新用户事件;一个父事件后的侧链完成事件;一个带有类似完成停止原因的API错误信封;以及新旧Codex完成事件名称。对这些语义案例,要同时用Swift和C#适配器运行测试。当某个提供方更改其事件格式时,先更新共享的真值表,再逐个更新解析器。
边界是有意缩窄的。isDone 并不证明任务已成功或生成的代码正确,它只表示最新识别到的转录证据已将控制权交还给了用户。
我参与运营 Agent Island。这里描述的状态合约和平台特定分类器,已经包含在公开发布的 v1.7.1 版本 macOS 和 Windows 构建中。如需了解完整的状态合约指南,请阅读相关文档。