$ cat blog/completion-event-session-state

A Completion Event Is Not a Session State

A completion marker is evidence about one turn. A status monitor still has to decide whether that fresh session needs the user now.

A coding agent can write a completion event and still leave a monitor with the wrong answer.

The event says that one turn ended. It does not say whether the user has already returned, whether a later turn started, whether the record belongs to the foreground session, or whether the event is now too old to deserve an alert. A useful monitor has to answer a narrower question: does this fresh session still need the user?

We ran into this while building Agent Island's local status detector for Claude Code and Codex. The product reads local session records and shows one of a small set of states. No transcript has to leave the machine. The hard part was not finding an end_turn or task_complete marker. The hard part was deciding what that marker meant now.

Parse meaning before looking at time

Claude and Codex do not write the same record format. For Claude, an assistant record can carry a stop reason such as end_turn, stop_sequence, or stop. Codex emits event records such as task_complete and turn/completed.

Those strings are only the first gate. Claude can use a completion-shaped assistant envelope for an API error. Treating that record as a successful finish produces a false your-turn alert exactly when the session has hit a limit. The parser therefore checks the error flag before accepting the stop reason.

The parser returns three values instead of a boolean:

isDone       whether the latest relevant turn ended
turnKey      a stable identity for that turn
activityDate the semantic timestamp carried by the record

The turn key matters because the same completion record can be observed more than once. A watcher can fire after a write, and the fallback scan can see the same file again. Deduplication based only on file modification time is brittle because unrelated bookkeeping may touch the file.

File time is a fallback

The next step combines the turn timestamp with other local activity. Claude Desktop, for example, can have its own session activity timestamp. The monitor uses the newest semantic date it knows and falls back to the transcript's modification time only when the record has no usable timestamp.

That ordering avoids a common mistake: letting filesystem metadata define product state. Modification time tells us that bytes changed. It does not tell us why they changed.

There is another wrinkle after a completed turn. Desktop bookkeeping may land just after the assistant's final record. If every newer desktop timestamp immediately cancels the completion, the monitor hides a valid alert. Agent Island gives those near-turn writes a short bookkeeping grace period. Activity well after that window is treated as the user returning.

The important point is the comparison, not the exact duration:

semantic activity = max(turn timestamp, external session activity)

if a completed turn is still fresh and no later user activity exists:
    state = needsYou
else if recent activity continues:
    state = working
else if a previously working session has gone quiet:
    state = stalled
else:
    state = idle

This is why a completion alert behaves more like a lease than a permanent fact. It is valid for a bounded period and can be revoked by newer evidence.

Monotonic reasoning still has limits

Wall-clock timestamps arrive from files and event payloads, so the monitor has to tolerate missing or imperfect dates. It also needs a bounded attention window. Without one, a laptop opened after a weekend could report an old completion as if it had just happened.

The implementation keeps recent working observations in memory. If a session was working and then stops producing activity, it can enter a stalled state for a limited period. That state is intentionally separate from needs-you. Silence does not prove completion.

This model has costs. A longer freshness window catches users who step away, but increases the chance that an old turn remains visible. A shorter window reduces stale alerts, but can hide useful completions. Filesystem watchers lower latency, but a periodic scan is still needed because events can be dropped. The parser also has to evolve when upstream record formats change.

What we verify

The state parser is covered with records that distinguish user messages, assistant stops, API-error envelopes, Codex start events, and Codex completion events. The surrounding scanner checks recency, later activity, stalled sessions, and the cap that eventually returns old sessions to idle.

The public v1.6.1 release contains both macOS and Windows artifacts. For this article, that only establishes release availability. It does not replace platform-specific behavior testing, and it does not justify claims about unrelated quota, cost, or reporting features.

The resulting rule is simple enough to state: alert on a fresh, relevant completed turn that has not been superseded. Getting there requires identity, semantic timestamps, later-activity checks, and an expiry policy. The completion event is evidence. The state is a decision made from several pieces of evidence.