-
Notifications
You must be signed in to change notification settings - Fork 0
fix(server): report idle sessions as not-running on reconnect #402
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1114,6 +1114,104 @@ describe('handleReconnect reconnected summary (P1)', () => { | |
| expect(reattachChat).not.toHaveBeenCalled(); | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔵 missing_tests: No test verifies the behavioral side-effect: that idle sessions (lastSpeaker=assistant) skip the reattach logic on reconnect. The current tests only check the summary payload. A test should verify that |
||
| }); | ||
|
|
||
| it('reports running=false when session is active but idle (lastSpeaker=assistant)', () => { | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔵 missing_tests: The new idle-session tests don't verify that reattachChat is NOT called when running is downgraded to false by the lastSpeaker check. The zombie test (line 1117) explicitly asserts
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔵 missing_tests: No test covers the case where storeMeta exists, isActive is true, but lastSpeaker is null (brand-new session that hasn't had any turns yet). The implementation handles it correctly (null !== 'assistant' → running stays true), but an explicit test would document this edge case and guard against future regressions. |
||
| const sessionReg = mockSessionRegistry(); | ||
| sessionReg.findBySessionId.mockReturnValue({ clientId: 'driver-1' }); | ||
| sessionReg.isActive.mockReturnValue(true); | ||
| sessionReg.isAttached.mockReturnValue(false); | ||
|
|
||
| const eventStore = mockEventStore(); | ||
| eventStore.getSession.mockReturnValue({ isActive: true, lastSpeaker: 'assistant' }); | ||
|
|
||
| const ctx = createContext({ | ||
| sessionRegistry: sessionReg as unknown as V2HandlerContext['sessionRegistry'], | ||
| eventStore: eventStore as unknown as V2HandlerContext['eventStore'], | ||
| }); | ||
| const transport = mockTransport(); | ||
| ctx.connRegistry.register('c1', transport); | ||
|
|
||
| handleReconnect( | ||
| 'c1', | ||
| { type: 'reconnect', sessions: [{ sessionId: 'sess-idle', lastSeq: 0 }] }, | ||
| ctx, | ||
| ); | ||
|
|
||
| const summary = transport.sent.find((m) => m.type === 'reconnected') as { | ||
| sessions: Array<{ sessionId: string; running: boolean }>; | ||
| }; | ||
| expect(summary.sessions[0].running).toBe(false); | ||
| }); | ||
|
|
||
| it('reports running=true when session is active and mid-turn (lastSpeaker=user)', () => { | ||
| const sessionReg = mockSessionRegistry(); | ||
| sessionReg.findBySessionId.mockReturnValue({ clientId: 'driver-1' }); | ||
| sessionReg.isActive.mockReturnValue(true); | ||
| sessionReg.isAttached.mockReturnValue(false); | ||
|
|
||
| const eventStore = mockEventStore(); | ||
| eventStore.getSession.mockReturnValue({ isActive: true, lastSpeaker: 'user' }); | ||
|
|
||
| const ctx = createContext({ | ||
| sessionRegistry: sessionReg as unknown as V2HandlerContext['sessionRegistry'], | ||
| eventStore: eventStore as unknown as V2HandlerContext['eventStore'], | ||
| }); | ||
| const transport = mockTransport(); | ||
| ctx.connRegistry.register('c1', transport); | ||
|
|
||
| handleReconnect( | ||
| 'c1', | ||
| { type: 'reconnect', sessions: [{ sessionId: 'sess-busy', lastSeq: 0 }] }, | ||
| ctx, | ||
| ); | ||
|
|
||
| const summary = transport.sent.find((m) => m.type === 'reconnected') as { | ||
| sessions: Array<{ sessionId: string; running: boolean }>; | ||
| }; | ||
| expect(summary.sessions[0].running).toBe(true); | ||
| }); | ||
|
|
||
| it('handles mixed running states across multiple sessions', () => { | ||
| const sessionReg = mockSessionRegistry(); | ||
| sessionReg.findBySessionId | ||
| .mockReturnValueOnce({ clientId: 'driver-1' }) | ||
| .mockReturnValueOnce(null) | ||
| .mockReturnValueOnce({ clientId: 'driver-3' }); | ||
| sessionReg.isActive.mockReturnValueOnce(true).mockReturnValueOnce(false); | ||
|
|
||
| const ctx = createContext({ | ||
| sessionRegistry: sessionReg as unknown as V2HandlerContext['sessionRegistry'], | ||
| }); | ||
| const transport = mockTransport(); | ||
| ctx.connRegistry.register('c1', transport); | ||
|
|
||
| handleReconnect( | ||
| 'c1', | ||
| { | ||
| type: 'reconnect', | ||
| sessions: [ | ||
| { sessionId: 'sess-1', lastSeq: 0 }, | ||
| { sessionId: 'sess-2', lastSeq: 0 }, | ||
| { sessionId: 'sess-3', lastSeq: 0 }, | ||
| ], | ||
| }, | ||
| ctx, | ||
| ); | ||
|
|
||
| const summary = transport.sent.find((m) => m.type === 'reconnected') as { | ||
| sessions: Array<{ sessionId: string; running: boolean }>; | ||
| }; | ||
| expect(summary.sessions).toHaveLength(3); | ||
| expect(summary.sessions[0]).toEqual( | ||
| expect.objectContaining({ sessionId: 'sess-1', running: true }), | ||
| ); | ||
| expect(summary.sessions[1]).toEqual( | ||
| expect.objectContaining({ sessionId: 'sess-2', running: false }), | ||
| ); | ||
| expect(summary.sessions[2]).toEqual( | ||
| expect.objectContaining({ sessionId: 'sess-3', running: false }), | ||
| ); | ||
| }); | ||
|
|
||
| it('replays multiple events in sequence order', () => { | ||
| const eventStore = mockEventStore(); | ||
| eventStore.getEventsAfter.mockReturnValue([ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -267,6 +267,22 @@ export function handleReconnect( | |
| }); | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 bugs: The
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 bugs: Even if |
||
| ctx.sessionRegistry.remove(found!.clientId); | ||
| } | ||
| // Distinguish "query loop alive but idle" from "agent actively | ||
| // processing a turn". lastSpeaker === 'assistant' means the agent | ||
| // completed its last turn and is waiting for input — report as | ||
| // not-running so the client sends messages immediately instead of | ||
| // queuing them behind a 5-second fallback timer. | ||
| if (running) { | ||
| const storeMeta = ctx.eventStore.getSession(entry.sessionId); | ||
| if (storeMeta?.lastSpeaker === 'assistant') { | ||
| running = false; | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 regressions: Setting |
||
| log.info('session alive but idle (last speaker: assistant)', { | ||
| connectionId, | ||
| sessionId: entry.sessionId, | ||
| clientId: found!.clientId, | ||
| }); | ||
| } | ||
| } | ||
| if (found && running && !ctx.sessionRegistry.isAttached(found.clientId)) { | ||
| const ownerConnection = getOwnerConnection(found.clientId); | ||
| const ownerGone = !ctx.connRegistry.get(ownerConnection); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔴 regressions: The existing test at line 1064 (
'reconnected summary does not include running field') explicitly assertsexpect(summary.sessions[0]).not.toHaveProperty('running'). The three new tests directly contradict this by assertingrunningIS present with specific boolean values. One of these test groups must be updated — either the existing test is removed (ifrunningis being added) or the new tests are wrong.[fixable]