Skip to content

state: let work clear the idle hint, so a background job is not drawn as idle - #8

Merged
simoncoombes merged 1 commit into
mainfrom
idle-hint-expires
Sep 15, 2026
Merged

simoncoombes merged 1 commit into
mainfrom
idle-hint-expires

Conversation

@simoncoombes

Copy link
Copy Markdown
Owner

A pane attached to a background job session drew the idle screen, "watching for activity", over a main agent that was starting a Bash call every few seconds.

Not a frozen pane. The world was changing the whole time and the state file was being rewritten in step with the transcript; none of it reached the screen because renderFrame sends an idle session to renderIdleFrame before anything else gets a say.

Why it latched

SessionIdle is a hint. The transcript raises it off {"type":"system","subtype":"turn_duration"}, whose meaning is "that turn ended", and parse.go has always said so in a comment. The machine treated it as a state, and the only events that cleared it were SessionStart, UserPromptSubmitted and PostCompact.

An interactive tab gets one of those with every prompt, so the hint expires on its own and nobody noticed it could not expire by itself. A background job session never types a prompt. Its work arrives as a job, so after the first turn ended it latched idle for good.

Captured on a live session, tracing main's events through the machine:

19:51:13 SessionIdle  idle
19:52:19 ToolStart    active   <- with this change; previously idle from here on
19:52:28 ToolStart    active
…                              (eight calls, pane on the idle screen)
19:53:37 SessionIdle  idle

What clears it

A tool call, a spawn or a permission prompt. Those are work by definition. Tokens and thinking are not, because they also arrive while a turn is winding down, and the difference between "still finishing" and "started again" is the whole question here.

The next turn_duration idles the session again, so this expires a stale hint rather than pinning the session active. SessionEnd stays terminal and a straggler call cannot resurrect it.

The timestamp guard

The transcript runs seconds behind and reads its files one after another, so an agent file catching up delivers tool events stamped minutes before a turn end that has already been applied. Those are the tail of a turn that is over, not evidence of a new one, and acting on them would flip the pane out of the idle screen every time a slow file landed.

Tests

Four cases in internal/state/machine_test.go: work after a turn end leaves idle and the next turn end re-idles; a late event predating the turn end does not; tokens and thinking alone do not; an ended session stays ended.

Independent of #7, which is about which session a pane attaches to. This one is about what it draws once attached. The three codexsrc TestRealCodexSession* failures predate both and fail on main.

… as idle

A pane attached to a background job session drew the idle screen, "watching
for activity", over a main agent that was starting a Bash call every few
seconds. Not a frozen pane: the world was changing the whole time, the state
file was being rewritten in step with the transcript, and none of it reached
the screen because renderFrame sends an idle session to renderIdleFrame
before anything else gets a say.

SessionIdle is a hint. The transcript raises it off
{"type":"system","subtype":"turn_duration"}, whose meaning is "that turn
ended", and parse.go has always said as much in a comment. The machine
treated it as a state, and the only events that cleared it were SessionStart,
UserPromptSubmitted and PostCompact. An interactive tab gets one of those
with every prompt, so the hint expires on its own and nobody noticed it could
not expire by itself. A background job session never types a prompt. Its work
arrives as a job, so after the first turn ended it latched idle for good.

Captured on a live session: SessionIdle at 19:51:13, then eight tool calls
between 19:52:19 and 19:53:24 with the pane still on the idle screen, then
the next turn_duration at 19:53:37.

So a tool call, a spawn or a permission prompt now clears it. Those are work
by definition; tokens and thinking are not, because they also arrive while a
turn is winding down and the difference between "still finishing" and
"started again" is the whole question here. The next turn_duration idles the
session again, so this expires a stale hint rather than pinning the session
active. SessionEnd stays terminal and a straggler call cannot resurrect it.

The timestamp guard is what keeps this from fighting a real idle. The
transcript runs seconds behind and reads its files one after another, so an
agent file catching up delivers tool events stamped minutes before the turn
end that has already been applied. Those are the tail of a turn that is over,
not evidence of a new one, and acting on them would flip the pane out of the
idle screen every time a slow file landed.
@simoncoombes
simoncoombes merged commit 764bf3b into main Sep 15, 2026
3 checks passed
@simoncoombes
simoncoombes deleted the idle-hint-expires branch September 23, 2026 14:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant