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,7 +50,9 @@ export type ServerMessage =
|
||||
| { type: 'sync:messages'; sessionId: string; messages: Message[]; isGenerating: boolean; streamingText: string }
|
||||
| { type: 'error'; message: string; errorCode?: string }
|
||||
| { type: 'stopped' }
|
||||
| { type: 'disconnected' };
|
||||
| { type: 'disconnected' }
|
||||
| { type: 'task:started'; taskId: string; description: string; taskType?: string }
|
||||
| { type: 'task:notification'; taskId: string; status: 'completed' | 'failed' | 'stopped'; summary: string };
|
||||
|
||||
export type Message = {
|
||||
id: string;
|
||||
|
||||
@@ -209,6 +209,17 @@ export function useChat(initialSessionId?: string, initialModel?: string | null,
|
||||
commitStreaming();
|
||||
setIsGenerating(false);
|
||||
break;
|
||||
|
||||
case 'task:started':
|
||||
setMessages((prev) => [...prev, { role: 'assistant', id: crypto.randomUUID(), text: `⏳ Background task started — ${msg.description}` }]);
|
||||
break;
|
||||
|
||||
case 'task:notification': {
|
||||
// The fix in action: a background task's completion arriving after the turn ended.
|
||||
const icon = msg.status === 'completed' ? '✅' : msg.status === 'failed' ? '❌' : '⏹️';
|
||||
setMessages((prev) => [...prev, { role: 'assistant', id: crypto.randomUUID(), text: `${icon} Background task ${msg.status} — ${msg.summary}` }]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user