fix(sessions): the gate is the universal turn heartbeat (v0.326.0) - #596
Merged
Conversation
A session that was visibly generating could read `ready` — the inverse of the v0.324.0 bug, and the half that had no backstop. isWorking self-healed against a STALE flag, but nothing could set the flag back. Hook settings are written into the agent folder at LAUNCH, so a session launched before UserPromptSubmit was wired never fires it: once its busy_since was cleared, it could never regain it. Observed live on ses_c63f1492dc12ce06 — pane showing "Germinating… (1m 2s)", gate events every few seconds, busy_since NULL, console reading `ready`. The heartbeat now lives on the gate hook, which is wired into every session that exists and is the one thing that cannot be missing — it IS the invariant. A tool call is proof a turn is running, so it stamps busy_since too: - with `answered: false`, so a tool call never retires a "waiting on you" card (only a human submitting a prompt does). A session blocked on an approval reads `needs you` regardless, since that outranks `working`. - re-stamping a flag older than MID_TURN_MAX_MS, which turns that ceiling from a dumb timer into an honest ACTIVITY test: a long turn still calling tools keeps its spinner, a wedged one emits nothing and ages out. Without this arm a real 2h+ turn would silently read `ready`. - throttled to one write per 30s per session (in-memory, dropped by clearTurnBusy so the first tool call of the next turn is never swallowed). The gate is a hot path and node:sqlite is synchronous — a lock-taking write there is the event-loop blocking that has made a busy box feel unresponsive before, and the statement is a no-op mid-turn anyway. 9 new assertions in scripts/turn-lifecycle-test.cjs (49 total). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ZUfffxY4hKv7M6wMCcaTz
vikasprogrammer
added a commit
that referenced
this pull request
Aug 11, 2026
) A session that was visibly generating could read `ready` — the inverse of the v0.324.0 bug, and the half that had no backstop. isWorking self-healed against a STALE flag, but nothing could set the flag back. Hook settings are written into the agent folder at LAUNCH, so a session launched before UserPromptSubmit was wired never fires it: once its busy_since was cleared, it could never regain it. Observed live on ses_c63f1492dc12ce06 — pane showing "Germinating… (1m 2s)", gate events every few seconds, busy_since NULL, console reading `ready`. The heartbeat now lives on the gate hook, which is wired into every session that exists and is the one thing that cannot be missing — it IS the invariant. A tool call is proof a turn is running, so it stamps busy_since too: - with `answered: false`, so a tool call never retires a "waiting on you" card (only a human submitting a prompt does). A session blocked on an approval reads `needs you` regardless, since that outranks `working`. - re-stamping a flag older than MID_TURN_MAX_MS, which turns that ceiling from a dumb timer into an honest ACTIVITY test: a long turn still calling tools keeps its spinner, a wedged one emits nothing and ages out. Without this arm a real 2h+ turn would silently read `ready`. - throttled to one write per 30s per session (in-memory, dropped by clearTurnBusy so the first tool call of the next turn is never swallowed). The gate is a hot path and node:sqlite is synchronous — a lock-taking write there is the event-loop blocking that has made a busy box feel unresponsive before, and the statement is a no-op mid-turn anyway. 9 new assertions in scripts/turn-lifecycle-test.cjs (49 total). Claude-Session: https://claude.ai/code/session_015ZUfffxY4hKv7M6wMCcaTz Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Reported: a session actively doing work shows
ready.Yes — because it was launched before the lifecycle hook shipped. But it's a real hole either way: #593 made
isWorkingself-heal against a stale flag, and left the opposite direction with no backstop at all — nothing could set the flag back.Hook settings are written into the agent folder at launch, so a session launched before
UserPromptSubmitwas wired never fires it. Once itsbusy_sincewas cleared it could never regain it.Confirmed on the reported session
ses_c63f1492dc12ce06:Fix — put the heartbeat on the one hook that cannot be missing
The gate hook is wired into every session that exists; it is the invariant. A tool call is proof a turn is running, so it stamps
busy_sincetoo.Three properties that make it safe:
answered: false— a tool call is not a human answering, so it never retires a "waiting on you" card. (A session blocked on an approval readsneeds youregardless — that outranksworking.)MID_TURN_MAX_MS, turning that ceiling from a dumb timer into an honest activity test: a long turn still calling tools keeps its spinner, a wedged one emits nothing and ages out. Without this arm a real 2h+ turn would silently readready— the same class of bug in a new place.clearTurnBusyso the first tool call of the next turn is never swallowed). The gate is a hot path andnode:sqliteis synchronous — a lock-taking write there is the event-loop blocking that has made a busy box feel unresponsive before, and the statement is a no-op mid-turn anyway.Verification
9 new assertions in
scripts/turn-lifecycle-test.cjs(49 total): an old session with noUserPromptSubmitgoes working on a tool call; a tool call doesn't clear a waiting card; the throttle never swallows the first call of a turn; a 3h-old stamp is wedged until a tool call revives it. Full suite green.Deploy note: server restart required. Works on already-running sessions — that's the point.
🤖 Generated with Claude Code
https://claude.ai/code/session_015ZUfffxY4hKv7M6wMCcaTz