fix(session-overview): done sessions never transition to idle#384
Conversation
Centaur ReviewFound 4 issue(s) (1 warning).
|
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
Found 3 issue(s).
server/session-overview.ts
Clean, well-tested fix for two real bugs (SSE reconnect missing session reattach, done sessions never transitioning to idle). No correctness issues found — just minor suggestions around multi-session timer edge-case documentation and test coverage.
- 🔵 unsafe_assumptions (L110): scheduleIdleTransition resets to a single timer on every touch, keyed to the most recent touch only. If multiple sessions have staggered touch times (e.g., client-1 touched 4 min ago, client-2 touched just now), the timer fires at DONE_TIMEOUT_MS after client-2's touch (5 more minutes). Client-1's done→idle transition at the 5-minute mark is not proactively broadcast — it relies on compute() being triggered by some other event (e.g., the 60s uncommitted refresh). In practice this means up to 60s of stale 'done' state for sessions that hit the threshold between the last touch and the next refresh. Acceptable latency for a UI badge, but worth documenting.
[fixable]
packages/client/src/__tests__/sse-connection.test.ts
Clean, well-tested fix for two real bugs (SSE reconnect missing session reattach, done sessions never transitioning to idle). No correctness issues found — just minor suggestions around multi-session timer edge-case documentation and test coverage.
- 🔵 missing_tests (L293): No test covers reconnect POST with multiple tracked sessions. The current test tracks only one session. A test with 2+ sessions would verify the serialization of the sessions array and confirm ordering is stable.
[fixable]
packages/client/src/sse-connection.ts
Clean, well-tested fix for two real bugs (SSE reconnect missing session reattach, done sessions never transitioning to idle). No correctness issues found — just minor suggestions around multi-session timer edge-case documentation and test coverage.
- 🔵 style (L218): The 6-line comment block (lines 218-223) inside the welcome handler restates the problem description from the commit message and the comment at lines 198-201. The rationale is already captured once above doConnect(); repeating it in the handler adds noise without new information.
[fixable]
| * timer — only the most recent touch matters since compute() evaluates all | ||
| * sessions at broadcast time. | ||
| */ | ||
| private scheduleIdleTransition(): void { |
There was a problem hiding this comment.
🔵 unsafe_assumptions: scheduleIdleTransition resets to a single timer on every touch, keyed to the most recent touch only. If multiple sessions have staggered touch times (e.g., client-1 touched 4 min ago, client-2 touched just now), the timer fires at DONE_TIMEOUT_MS after client-2's touch (5 more minutes). Client-1's done→idle transition at the 5-minute mark is not proactively broadcast — it relies on compute() being triggered by some other event (e.g., the 60s uncommitted refresh). In practice this means up to 60s of stale 'done' state for sessions that hit the threshold between the last touch and the next refresh. Acceptable latency for a UI badge, but worth documenting. [fixable]
|
|
||
| it('includes sessions in reconnect URL', () => { | ||
| const conn = new SseConnection(createConfig()); | ||
| it('sends reconnect POST on welcome when has tracked sessions', () => { |
There was a problem hiding this comment.
🔵 missing_tests: No test covers reconnect POST with multiple tracked sessions. The current test tracks only one session. A test with 2+ sessions would verify the serialization of the sessions array and confirm ordering is stable. [fixable]
| this._connectionId = msg.connectionId as string; | ||
| this._connected = true; | ||
|
|
||
| // Always send reconnect on welcome if we have tracked sessions. |
There was a problem hiding this comment.
🔵 style: The 6-line comment block (lines 218-223) inside the welcome handler restates the problem description from the commit message and the comment at lines 198-201. The rationale is already captured once above doConnect(); repeating it in the handler adds noise without new information. [fixable]
|
Requesting second Centaur review. Context:
|
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
Found 4 issue(s).
server/session-overview.ts
Clean, well-tested fix. The SSE reconnect change correctly moves session data from URL query params to POST (fixing EventSource auto-reconnect), and the session-overview change properly transitions persistent done sessions to idle. Only minor suggestions around timer precision with multiple staggered sessions.
- 🔵 unsafe_assumptions (L110): scheduleIdleTransition() uses a single timer that resets on every touch(). With multiple sessions touched at different times, earlier sessions don't get a precisely-timed idle broadcast — they rely on the 60-second uncommitted-refresh cycle. For example, session A touched at T=0 and session B touched at T=3min means session A won't get an idle broadcast until T=8min (from B's timer) or T=5min+60s (from the background refresh). Consider scheduling based on the earliest pending transition rather than always resetting to DONE_TIMEOUT_MS from now.
[fixable] - 🔵 style (L107): Comment says 'only the most recent touch matters since compute() evaluates all sessions at broadcast time' — this is true for correctness but misleading about timeliness. Earlier sessions' done→idle transitions are delayed until either this timer fires or the 60s background refresh runs. Consider noting the worst-case delay.
[fixable]
server/__tests__/session-overview.test.ts
Clean, well-tested fix. The SSE reconnect change correctly moves session data from URL query params to POST (fixing EventSource auto-reconnect), and the session-overview change properly transitions persistent done sessions to idle. Only minor suggestions around timer precision with multiple staggered sessions.
- 🔵 missing_tests: No test covers the multi-session stagger scenario: session A touched at T=0, session B touched at T=3min — verify session A transitions to idle at approximately T=5min (not delayed until B's timer fires at T=8min). This would document the current behavior and catch regressions if the timer strategy changes.
[fixable] - 🔵 missing_tests: No test verifies that persistent sessions (from getAttentionSessions) schedule an idle transition timer. Currently persistent sessions only transition to idle when some other event triggers a broadcast (background refresh, another touch). A test exercising this path would make the timing contract explicit.
[fixable]
| * timer — only the most recent touch matters since compute() evaluates all | ||
| * sessions at broadcast time. | ||
| */ | ||
| private scheduleIdleTransition(): void { |
There was a problem hiding this comment.
🔵 unsafe_assumptions: scheduleIdleTransition() uses a single timer that resets on every touch(). With multiple sessions touched at different times, earlier sessions don't get a precisely-timed idle broadcast — they rely on the 60-second uncommitted-refresh cycle. For example, session A touched at T=0 and session B touched at T=3min means session A won't get an idle broadcast until T=8min (from B's timer) or T=5min+60s (from the background refresh). Consider scheduling based on the earliest pending transition rather than always resetting to DONE_TIMEOUT_MS from now. [fixable]
| /** | ||
| * Schedule a broadcast at DONE_TIMEOUT_MS so that sessions entering "done" | ||
| * transition to "idle" even when no other events occur. Resets any existing | ||
| * timer — only the most recent touch matters since compute() evaluates all |
There was a problem hiding this comment.
🔵 style: Comment says 'only the most recent touch matters since compute() evaluates all sessions at broadcast time' — this is true for correctness but misleading about timeliness. Earlier sessions' done→idle transitions are delayed until either this timer fires or the 60s background refresh runs. Consider noting the worst-case delay. [fixable]
Two bugs caused sessions to stay visible in the Active Sessions list indefinitely instead of disappearing after 5 minutes: 1. No proactive idle timer — the done→idle transition was only checked during on-demand state derivation. If no events occurred after a session finished, the server never re-evaluated or broadcast the state change. Fixed by scheduling a broadcast at DONE_TIMEOUT_MS after each touch(). 2. Persistent attention sessions hardcoded to "done" — sessions from EventStore's getAttentionSessions() always returned state:'done' regardless of age. Fixed by applying the same elapsed-time check as live sessions. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Co-Authored-By: Claude Opus 4.6 <[email protected]>
…rsistent tests - scheduleIdleTransition now picks the minimum remaining time across all tracked sessions instead of always resetting to DONE_TIMEOUT_MS, so staggered sessions transition on their own schedule - Re-schedules after each fire if sessions remain - Added test: staggered sessions A (T=0) and B (T=3min) — A transitions idle at T=5min while B remains done - Added test: persistent sessions transition to idle via the timer Co-Authored-By: Claude Opus 4.6 <[email protected]>
ff13d38 to
06477ea
Compare
Summary
touch()now schedules a broadcast atDONE_TIMEOUT_MS(5 min) so the done→idle transition fires proactively, even when no other events occurpersistentToActivity()now applies the same elapsed-time check instead of hardcodingstate: 'done'— old attention sessions transition to idle and disappear from the active listRoot cause
Sessions were stuck as "done" in the Active Sessions list indefinitely because:
getAttentionSessions()(assistant spoke last) were always returned asdoneregardless of ageTest plan
🤖 Generated with Claude Code