chat: persistent Agent SDK session per chat — decouple worker from turn (Phase 1)
Root fix for orphaned background tasks: the platform drove Claude Code as a
one-shot `claude -p` per turn (stdin ignored, process exits at turn end), so
run_in_background/Monitor work — and its task_notification — had no live harness
to return to. Now each chat session runs ONE long-lived Agent SDK query() with
streaming input; turns are user messages pushed onto it, and the session stays
warm between turns.
- claude-manager: persistent `query({ prompt: AsyncIterable, options })` per
sessionKey (bypassPermissions, --resume, mcp via extraArgs, CLAUDECODE stripped).
Single consumer loop maps every SDK message → ChatEvent, incl. post-turn
task_started / task_notification. interrupt() = stop-turn; abort() = kill-session;
30-min idle GC.
- stream-parser: processMessage() (object-level, reused by the SDK loop) + task
message handling. ChatEvent/ServerMessage gain task:started / task:notification.
- API: the sidecar event subscription is now SESSION-scoped (no longer unsubscribes
on 'result'), so background events after turn-end still reach the client. First
turn opens the session; later turns push onto it. handleStop → interrupt (keeps
session warm); disconnect/deleteSession → kill.
- protocol/sidecar-registry/user-instance: claude:interrupt command + interruptClaude.
- client: render task:started / task:notification in the transcript.
Verified end-to-end through the real chat WS: a run_in_background task's completion
arrives ~6s AFTER the turn's result; multi-turn on one warm session works.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -50,16 +50,11 @@ export async function sendClaudeCodeStreaming(params: ClaudeCodeStreamingParams)
|
||||
|
||||
logger.info('Claude Code streaming exec (via sidecar)', { sessionKey: params.sessionKey });
|
||||
|
||||
// Subscribe to events for this session
|
||||
// Session-scoped subscription. The persistent session outlives the turn, so background task events
|
||||
// (task:notification) arrive AFTER 'result' — do NOT unsubscribe on a terminal turn event; only on
|
||||
// an explicit kill/teardown (the returned handle, called from deleteSession/disconnect).
|
||||
const unsub = sidecar.onClaudeEvent((sessionKey, event) => {
|
||||
if (sessionKey === params.sessionKey) {
|
||||
onEvent(event);
|
||||
|
||||
// Unsubscribe when we get a terminal event
|
||||
if (event.type === 'result' || event.type === 'error') {
|
||||
unsub();
|
||||
}
|
||||
}
|
||||
if (sessionKey === params.sessionKey) onEvent(event);
|
||||
});
|
||||
|
||||
await sidecar.spawnClaudeStreaming(spawnParams);
|
||||
|
||||
Reference in New Issue
Block a user