fix: handle SDK keep_alive messages to prevent idle timeout during long operations (#163)
The stream idle timer (330s) fires during long-running Claude Code tool executions because the SDK's keep_alive heartbeat messages are silently dropped. This causes a false "Stream idle timeout" error even though Claude is still actively working. Root cause: claude-client.ts only handles 6 SDK message types in its switch statement and has no default case, so keep_alive messages from the SDK are never forwarded to the frontend. The idle timer in stream-session-manager.ts never gets reset by markActive(). Fix: thread keep_alive through the existing 3-layer event pipeline: - claude-client.ts: forward keep_alive as SSE event (via default case) - useSSEStream.ts: add onKeepAlive callback to SSECallbacks interface - stream-session-manager.ts: call markActive() on keep_alive to reset the idle timer No visible UI change — keep_alive only resets the timer silently. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
315aac0f04
commit
a906172807
4 files changed
+18
No files matched your search
@@ -24,6 +24,7 @@ export interface SSECallbacks {
|
||||
onToolTimeout: (toolName: string, elapsedSeconds: number) => void;
|
||||
onModeChanged: (mode: string) => void;
|
||||
onTaskUpdate: (sessionId: string) => void;
|
||||
onKeepAlive: () => void;
|
||||
onError: (accumulated: string) => void;
|
||||
}
|
||||
|
||||
@@ -146,6 +147,11 @@ function handleSSEEvent(
|
||||
return accumulated;
|
||||
}
|
||||
|
||||
case 'keep_alive': {
|
||||
callbacks.onKeepAlive();
|
||||
return accumulated;
|
||||
}
|
||||
|
||||
case 'error': {
|
||||
const next = accumulated + '\n\n**Error:** ' + event.data;
|
||||
callbacks.onError(next);
|
||||
@@ -232,6 +238,7 @@ export function useSSEStream() {
|
||||
onToolTimeout: (n, s) => callbacksRef.current?.onToolTimeout(n, s),
|
||||
onModeChanged: (m) => callbacksRef.current?.onModeChanged(m),
|
||||
onTaskUpdate: (s) => callbacksRef.current?.onTaskUpdate(s),
|
||||
onKeepAlive: () => callbacksRef.current?.onKeepAlive(),
|
||||
onError: (a) => callbacksRef.current?.onError(a),
|
||||
};
|
||||
|
||||
|
||||
@@ -873,6 +873,13 @@ export function streamClaude(options: ClaudeStreamOptions): ReadableStream<strin
|
||||
}));
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
if ((message as { type: string }).type === 'keep_alive') {
|
||||
controller.enqueue(formatSSE({ type: 'keep_alive', data: '' }));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -311,6 +311,9 @@ async function runStream(stream: ActiveStream, params: StartStreamParams): Promi
|
||||
markActive();
|
||||
window.dispatchEvent(new CustomEvent('tasks-updated'));
|
||||
},
|
||||
onKeepAlive: () => {
|
||||
markActive();
|
||||
},
|
||||
onError: (acc) => {
|
||||
markActive();
|
||||
stream.accumulatedText = acc;
|
||||
|
||||
@@ -356,6 +356,7 @@ export type SSEEventType =
|
||||
| 'permission_request' // permission approval needed
|
||||
| 'mode_changed' // SDK permission mode changed (e.g. plan → code)
|
||||
| 'task_update' // SDK TodoWrite task sync
|
||||
| 'keep_alive' // SDK keep-alive heartbeat (resets idle timer)
|
||||
| 'done'; // stream complete
|
||||
|
||||
export interface SSEEvent {
|
||||
|
||||
Reference in new issue
Block a user