A completion alert becomes unreliable when every consumer interprets raw agent logs for itself.
Claude Code and Codex both append JSONL records, but they do not use the same event envelope. Claude completion can appear in message.stop_reason. Codex uses event names such as task_complete and turn/completed. If the menu bar, notification code, and session list each parse those details, they will eventually disagree.
Put provider details behind three fields
The rest of the application only needs a small result:
isDone did the newest relevant event hand control back?
key which turn does this result describe?
activityDate when did the semantic event happen?
The Swift implementation calls this SessionTurnStatus. The Windows port uses the same fields in a C# record struct. Code above that boundary can reason about a completed turn without knowing which provider produced the evidence.
Read from the newest evidence backward
An older completion record stops being current as soon as a new user message begins another turn.
The classifier therefore scans a bounded transcript tail in reverse and stops at the first recognized event. A new user event returns a working state. A provider-specific completion event returns done. Unknown records are skipped instead of being treated as idle.
This ordering rule matters more than the exact JSON property names. A forward scan that remembers the last completion can report a stale handoff after work has already resumed.
Exclude valid records with the wrong meaning
A JSON record can be well formed and still be the wrong completion signal.
Claude Code can emit an assistant envelope with stop_sequence for an API or rate-limit error. The record also carries isApiErrorMessage: true. Checking the stop reason alone would fire a false your-turn alert.
Subagent events need a similar guard. An isSidechain completion belongs to a background chain, not the parent conversation. Letting it complete the foreground session confuses valid evidence with relevant evidence.
These exclusions live in both platform classifiers and in the fixture set. Porting the rule without the failure examples would make parity temporary.
Keep one turn identity
Filesystem events and polling can classify the same turn more than once. The result needs a stable key so alert code can deduplicate it.
The classifier prefers identifiers already present in the record, including uuid, id, turn_id, item_id, and call_id. It derives a bounded fallback only when the provider gives no usable identifier.
Notification code should consume that key. Rebuilding identity from a file path or scan timestamp creates a second, incompatible definition of a turn.
Test traces instead of isolated helpers
The useful fixtures resemble failures from real transcripts:
- an old completion followed by a new user event;
- a sidechain completion after a parent event;
- an API-error envelope with a completion-like stop reason;
- old and current Codex completion event names.
Run the same semantic cases against the Swift and C# adapters. When a provider changes its event format, update the shared truth table first, then each parser.
The boundary is deliberately narrow. isDone does not prove that the task succeeded or that generated code is correct. It only says that the newest recognized transcript evidence handed control back to the user.
I help run Agent Island. The state contract and the platform-specific classifiers described here are in the public v1.7.1 macOS and Windows builds.
Top comments (0)