fix(server): bound late permission waits (#1019)

This commit is contained in:
程序员阿江(Relakkes) 2026-07-17 22:57:13 +08:00
parent 948eb36b67
commit 1d8597fb7b
2 changed files with 44 additions and 0 deletions

View File

@ -430,6 +430,39 @@ describe('WebSocket handler session isolation', () => {
expect(stopSession).toHaveBeenCalledWith(sessionId) expect(stopSession).toHaveBeenCalledWith(sessionId)
}) })
it('starts the permission cleanup bound when the prompt arrives after disconnect', () => {
const sessionId = `late-permission-disconnect-${crypto.randomUUID()}`
const ws = makeClientSocket(sessionId)
const setTimeoutSpy = spyOn(globalThis, 'setTimeout').mockImplementation(() => 0 as any)
const pendingRequests = spyOn(conversationService, 'getPendingPermissionRequests')
.mockReturnValue([])
let turnOutputCallback: ((cliMsg: any) => void) | null = null
spyOn(conversationService, 'onOutput').mockImplementation((_sid, callback) => {
turnOutputCallback = callback
})
spyOn(conversationService, 'removeOutputCallback').mockImplementation(() => {})
handleWebSocket.open(ws)
__markActiveTurnForTests(sessionId)
setTimeoutSpy.mockClear()
handleWebSocket.close(ws, 1006, 'renderer closed before permission prompt')
expect(setTimeoutSpy).not.toHaveBeenCalled()
pendingRequests.mockReturnValue([{
requestId: 'late-request-1',
toolName: 'Bash',
input: { command: 'echo later' },
}])
;(turnOutputCallback as ((cliMsg: any) => void) | null)?.({
type: 'control_request',
request_id: 'late-request-1',
request: { subtype: 'can_use_tool', tool_name: 'Bash' },
})
expect(setTimeoutSpy).toHaveBeenCalledTimes(1)
expect(setTimeoutSpy.mock.calls[0]?.[1]).toBe(30 * 60_000)
})
it('does not forward prewarm startup status to a reconnecting client', async () => { it('does not forward prewarm startup status to a reconnecting client', async () => {
const sessionId = `prewarm-reconnect-${crypto.randomUUID()}` const sessionId = `prewarm-reconnect-${crypto.randomUUID()}`
const second = makeClientSocket(sessionId) const second = makeClientSocket(sessionId)

View File

@ -2298,6 +2298,17 @@ function watchTurnCompletionForCleanup(sessionId: string): void {
cancelSessionDisconnectWatcher(sessionId) cancelSessionDisconnectWatcher(sessionId)
const onComplete = (cliMsg: any) => { const onComplete = (cliMsg: any) => {
if (
cliMsg?.type === 'control_request' &&
cliMsg.request?.subtype === 'can_use_tool' &&
!hasActiveClients(sessionId)
) {
// The permission request may arrive after the renderer disconnected.
// ConversationService records it before notifying this callback, so the
// cleanup delay resolves to the bounded pending-permission window.
scheduleDisconnectCleanup(sessionId)
return
}
if (cliMsg?.type !== 'result') return if (cliMsg?.type !== 'result') return
cancelSessionDisconnectWatcher(sessionId) cancelSessionDisconnectWatcher(sessionId)
// The turn finished while still unobserved — fall back to the idle timer. // The turn finished while still unobserved — fall back to the idle timer.