From 5a1fe23ddb8d1c058ed4055d94cadefed07b1a6f Mon Sep 17 00:00:00 2001 From: SuperComboGamer Date: Sun, 14 Dec 2025 11:02:42 -0500 Subject: [PATCH] feat: implement SDK session ID handling for conversation continuity - Added support for resuming conversations using the Claude SDK session ID. - Updated the ClaudeProvider to conditionally resume sessions based on the presence of a session ID and conversation history. - Enhanced the AgentService to capture and store the SDK session ID from incoming messages, ensuring continuity in conversations. --- apps/server/src/providers/claude-provider.ts | 75 +++----------------- apps/server/src/providers/types.ts | 1 + apps/server/src/services/agent-service.ts | 10 +++ 3 files changed, 21 insertions(+), 65 deletions(-) diff --git a/apps/server/src/providers/claude-provider.ts b/apps/server/src/providers/claude-provider.ts index cb44811d..b1fe281a 100644 --- a/apps/server/src/providers/claude-provider.ts +++ b/apps/server/src/providers/claude-provider.ts @@ -7,10 +7,6 @@ import { query, type Options } from "@anthropic-ai/claude-agent-sdk"; import { BaseProvider } from "./base-provider.js"; -import { - convertHistoryToMessages, - normalizeContentBlocks, -} from "../lib/conversation-utils.js"; import type { ExecuteOptions, ProviderMessage, @@ -38,6 +34,7 @@ export class ClaudeProvider extends BaseProvider { allowedTools, abortController, conversationHistory, + sdkSessionId, } = options; // Build Claude SDK options @@ -65,69 +62,17 @@ export class ClaudeProvider extends BaseProvider { autoAllowBashIfSandboxed: true, }, abortController, + // Resume existing SDK session if we have a session ID + ...(sdkSessionId && conversationHistory && conversationHistory.length > 0 + ? { resume: sdkSessionId } + : {}), }; - // Build prompt payload with conversation history - let promptPayload: string | AsyncGenerator | Array; + // Build prompt payload + let promptPayload: string | AsyncIterable; - if (conversationHistory && conversationHistory.length > 0) { - // Multi-turn conversation with history - // Convert history to SDK message format - // Note: When using async generator, SDK only accepts SDKUserMessage (type: 'user') - // So we filter to only include user messages to avoid SDK errors - const historyMessages = convertHistoryToMessages(conversationHistory); - const hasAssistantMessages = historyMessages.some( - (msg) => msg.type === "assistant" - ); - - if (hasAssistantMessages) { - // If we have assistant messages, use async generator but filter to only user messages - // This maintains conversation flow while respecting SDK type constraints - promptPayload = (async function* () { - // Filter to only user messages - SDK async generator only accepts SDKUserMessage - const userHistoryMessages = historyMessages.filter( - (msg) => msg.type === "user" - ); - for (const msg of userHistoryMessages) { - yield msg; - } - - // Yield current prompt - const normalizedPrompt = normalizeContentBlocks(prompt); - const currentPrompt = { - type: "user" as const, - session_id: "", - message: { - role: "user" as const, - content: normalizedPrompt, - }, - parent_tool_use_id: null, - }; - yield currentPrompt; - })(); - } else { - // Only user messages in history - can use async generator normally - promptPayload = (async function* () { - for (const msg of historyMessages) { - yield msg; - } - - // Yield current prompt - const normalizedPrompt = normalizeContentBlocks(prompt); - const currentPrompt = { - type: "user" as const, - session_id: "", - message: { - role: "user" as const, - content: normalizedPrompt, - }, - parent_tool_use_id: null, - }; - yield currentPrompt; - })(); - } - } else if (Array.isArray(prompt)) { - // Multi-part prompt (with images) - no history + if (Array.isArray(prompt)) { + // Multi-part prompt (with images) promptPayload = (async function* () { const multiPartPrompt = { type: "user" as const, @@ -141,7 +86,7 @@ export class ClaudeProvider extends BaseProvider { yield multiPartPrompt; })(); } else { - // Simple text prompt - no history + // Simple text prompt promptPayload = prompt; } diff --git a/apps/server/src/providers/types.ts b/apps/server/src/providers/types.ts index 24bd41a8..6a05b6df 100644 --- a/apps/server/src/providers/types.ts +++ b/apps/server/src/providers/types.ts @@ -32,6 +32,7 @@ export interface ExecuteOptions { mcpServers?: Record; abortController?: AbortController; conversationHistory?: ConversationMessage[]; // Previous messages for context + sdkSessionId?: string; // Claude SDK session ID for resuming conversations } /** diff --git a/apps/server/src/services/agent-service.ts b/apps/server/src/services/agent-service.ts index 9a58940a..1117f89c 100644 --- a/apps/server/src/services/agent-service.ts +++ b/apps/server/src/services/agent-service.ts @@ -33,6 +33,7 @@ interface Session { abortController: AbortController | null; workingDirectory: string; model?: string; + sdkSessionId?: string; // Claude SDK session ID for conversation continuity } interface SessionMetadata { @@ -200,6 +201,7 @@ export class AgentService { abortController: session.abortController!, conversationHistory: conversationHistory.length > 0 ? conversationHistory : undefined, + sdkSessionId: session.sdkSessionId, // Pass SDK session ID for resuming }; // Build prompt content with images @@ -221,6 +223,14 @@ export class AgentService { const toolUses: Array<{ name: string; input: unknown }> = []; for await (const msg of stream) { + // Capture SDK session ID from any message + if (msg.session_id && !session.sdkSessionId) { + session.sdkSessionId = msg.session_id; + console.log( + `[AgentService] Captured SDK session ID: ${msg.session_id}` + ); + } + if (msg.type === "assistant") { if (msg.message?.content) { for (const block of msg.message.content) {