From 0a34ce366d849586ddd7d1aad44c40919e53a335 Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Wed, 1 Apr 2026 19:02:52 -0700 Subject: [PATCH 1/3] Fix model selection lost during task creation --- apps/code/src/main/services/agent/schemas.ts | 1 + apps/code/src/main/services/agent/service.ts | 6 ++++ .../sessions/hooks/useSessionConnection.ts | 18 +++++------- .../features/sessions/service/service.ts | 28 ++----------------- .../agent/src/adapters/claude/claude-agent.ts | 11 ++++++-- packages/agent/src/adapters/claude/types.ts | 2 ++ 6 files changed, 26 insertions(+), 40 deletions(-) diff --git a/apps/code/src/main/services/agent/schemas.ts b/apps/code/src/main/services/agent/schemas.ts index dafcaf169..97d7648ec 100644 --- a/apps/code/src/main/services/agent/schemas.ts +++ b/apps/code/src/main/services/agent/schemas.ts @@ -49,6 +49,7 @@ export const startSessionInput = z.object({ additionalDirectories: z.array(z.string()).optional(), customInstructions: z.string().max(2000).optional(), effort: effortLevelSchema.optional(), + model: z.string().optional(), }); export type StartSessionInput = z.infer; diff --git a/apps/code/src/main/services/agent/service.ts b/apps/code/src/main/services/agent/service.ts index 9d64c2ac9..c3a2e3f8b 100644 --- a/apps/code/src/main/services/agent/service.ts +++ b/apps/code/src/main/services/agent/service.ts @@ -189,6 +189,8 @@ interface SessionConfig { customInstructions?: string; /** Effort level for Claude sessions */ effort?: EffortLevel; + /** Model to use for the session (e.g. "claude-sonnet-4-6") */ + model?: string; } interface ManagedSession { @@ -465,6 +467,7 @@ export class AgentService extends TypedEventEmitter { permissionMode, customInstructions, effort, + model, } = config; // Preview sessions don't need a real repo — use a temp directory @@ -638,6 +641,7 @@ export class AgentService extends TypedEventEmitter { sessionId: existingSessionId, systemPrompt, ...(permissionMode && { permissionMode }), + ...(model && { model }), claudeCode: { options: { ...(additionalDirectories?.length && { @@ -669,6 +673,7 @@ export class AgentService extends TypedEventEmitter { taskRunId, systemPrompt, ...(permissionMode && { permissionMode }), + ...(model && { model }), claudeCode: { options: { ...(additionalDirectories?.length && { additionalDirectories }), @@ -1362,6 +1367,7 @@ For git operations while detached: customInstructions: "customInstructions" in params ? params.customInstructions : undefined, effort: "effort" in params ? params.effort : undefined, + model: "model" in params ? params.model : undefined, }; } diff --git a/apps/code/src/renderer/features/sessions/hooks/useSessionConnection.ts b/apps/code/src/renderer/features/sessions/hooks/useSessionConnection.ts index 1c8eab208..cb7e20789 100644 --- a/apps/code/src/renderer/features/sessions/hooks/useSessionConnection.ts +++ b/apps/code/src/renderer/features/sessions/hooks/useSessionConnection.ts @@ -86,28 +86,24 @@ export function useSessionConnection({ return; } - connectingTasks.add(taskId); - - const isNewSession = !task.latest_run?.id; - const hasInitialPrompt = isNewSession && task.description; + // New sessions (no latest_run) are handled by the task creation saga, + // which passes model/adapter/executionMode. Only reconnect existing ones here. + if (!task.latest_run?.id) return; - if (hasInitialPrompt) { - markActivity(task.id); - } + connectingTasks.add(taskId); - log.info("Connecting to task session", { + log.info("Reconnecting to existing task session", { taskId: task.id, hasLatestRun: !!task.latest_run, sessionStatus: session?.status ?? "none", }); + markActivity(task.id); + getSessionService() .connectToTask({ task, repoPath, - initialPrompt: hasInitialPrompt - ? [{ type: "text", text: task.description }] - : undefined, }) .finally(() => { connectingTasks.delete(taskId); diff --git a/apps/code/src/renderer/features/sessions/service/service.ts b/apps/code/src/renderer/features/sessions/service/service.ts index 5eb919f47..b4a9c16eb 100644 --- a/apps/code/src/renderer/features/sessions/service/service.ts +++ b/apps/code/src/renderer/features/sessions/service/service.ts @@ -536,6 +536,7 @@ export class SessionService { const { customInstructions: startCustomInstructions } = useSettingsStore.getState(); + const preferredModel = model ?? DEFAULT_GATEWAY_MODEL; const result = await trpcClient.agent.start.mutate({ taskId, taskRunId: taskRun.id, @@ -548,6 +549,7 @@ export class SessionService { effort: effortLevelSchema.safeParse(reasoningLevel).success ? (reasoningLevel as EffortLevel) : undefined, + model: preferredModel || undefined, }); const session = this.createBaseSession(taskRun.id, taskId, taskTitle); @@ -586,32 +588,6 @@ export class SessionService { adapter, }); - const preferredModel = model ?? DEFAULT_GATEWAY_MODEL; - const configPromises: Promise[] = []; - if (preferredModel) { - configPromises.push( - this.setSessionConfigOptionByCategory( - taskId, - "model", - preferredModel, - ).catch((err) => log.warn("Failed to set model", { taskId, err })), - ); - } - if (reasoningLevel) { - configPromises.push( - this.setSessionConfigOptionByCategory( - taskId, - "thought_level", - reasoningLevel, - ).catch((err) => - log.warn("Failed to set reasoning level", { taskId, err }), - ), - ); - } - if (configPromises.length > 0) { - await Promise.all(configPromises); - } - if (initialPrompt?.length) { await this.sendPrompt(taskId, initialPrompt); } diff --git a/packages/agent/src/adapters/claude/claude-agent.ts b/packages/agent/src/adapters/claude/claude-agent.ts index f4a036857..59b832ea7 100644 --- a/packages/agent/src/adapters/claude/claude-agent.ts +++ b/packages/agent/src/adapters/claude/claude-agent.ts @@ -919,15 +919,20 @@ export class ClaudeAcpAgent extends BaseAcpAgent { }); } - // Resolve model: settings model takes priority, then gateway + // Resolve model: settings model takes priority, then meta model, then gateway default const settingsModel = settingsManager.getSettings().model; - const modelOptions = await this.getModelConfigOptions(); - const resolvedModelId = settingsModel || modelOptions.currentModelId; + const metaModel = meta?.model; + const modelOptions = await this.getModelConfigOptions( + settingsModel || metaModel || undefined, + ); + const resolvedModelId = + settingsModel || metaModel || modelOptions.currentModelId; session.modelId = resolvedModelId; session.lastContextWindowSize = this.getContextWindowForModel(resolvedModelId); const resolvedSdkModel = toSdkModelId(resolvedModelId); + if (!isResume && resolvedSdkModel !== DEFAULT_MODEL) { await this.session.query.setModel(resolvedSdkModel); } diff --git a/packages/agent/src/adapters/claude/types.ts b/packages/agent/src/adapters/claude/types.ts index 02ea340d9..8bc8cce35 100644 --- a/packages/agent/src/adapters/claude/types.ts +++ b/packages/agent/src/adapters/claude/types.ts @@ -108,6 +108,8 @@ export type NewSessionMeta = { persistence?: { taskId?: string; runId?: string; logUrl?: string }; additionalRoots?: string[]; allowedDomains?: string[]; + /** Model ID to use for this session (e.g. "claude-sonnet-4-6") */ + model?: string; claudeCode?: { options?: Options; }; From 88fa2ac3f1bcb3b6c857a1905f28a16221b011a3 Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Wed, 1 Apr 2026 19:11:55 -0700 Subject: [PATCH 2/3] Parallelize SDK initialization with model config fetch --- .../agent/src/adapters/claude/claude-agent.ts | 114 +++++++++++------- 1 file changed, 72 insertions(+), 42 deletions(-) diff --git a/packages/agent/src/adapters/claude/claude-agent.ts b/packages/agent/src/adapters/claude/claude-agent.ts index 59b832ea7..0a2527b57 100644 --- a/packages/agent/src/adapters/claude/claude-agent.ts +++ b/packages/agent/src/adapters/claude/claude-agent.ts @@ -876,55 +876,85 @@ export class ClaudeAcpAgent extends BaseAcpAgent { { sessionId, taskId, taskRunId: meta?.taskRunId }, ); - try { - const result = await withTimeout( - q.initializationResult(), - SESSION_VALIDATION_TIMEOUT_MS, - ); - if (result.result === "timeout") { - throw new Error( - `Session ${isResume ? (forkSession ? "fork" : "resumption") : "initialization"} timed out for sessionId=${sessionId}`, + if (isResume) { + // Resume must block on initialization to validate the session is still alive. + // For stale sessions this throws (e.g. "No conversation found"). + try { + const result = await withTimeout( + q.initializationResult(), + SESSION_VALIDATION_TIMEOUT_MS, ); + if (result.result === "timeout") { + throw new Error( + `Session ${forkSession ? "fork" : "resumption"} timed out for sessionId=${sessionId}`, + ); + } + } catch (err) { + settingsManager.dispose(); + if ( + err instanceof Error && + err.message === "Query closed before response received" + ) { + throw RequestError.resourceNotFound(sessionId); + } + this.logger.error( + forkSession ? "Session fork failed" : "Session resumption failed", + { + sessionId, + taskId, + taskRunId: meta?.taskRunId, + error: err instanceof Error ? err.message : String(err), + }, + ); + throw err; } - } catch (err) { - settingsManager.dispose(); - if ( - isResume && - err instanceof Error && - err.message === "Query closed before response received" - ) { - throw RequestError.resourceNotFound(sessionId); - } - this.logger.error( - isResume - ? forkSession - ? "Session fork failed" - : "Session resumption failed" - : "Session initialization failed", - { - sessionId, - taskId, - taskRunId: meta?.taskRunId, - error: err instanceof Error ? err.message : String(err), - }, - ); - throw err; } - if (meta?.taskRunId) { - await this.client.extNotification("_posthog/sdk_session", { - taskRunId: meta.taskRunId, - sessionId, - adapter: "claude", - }); - } + // Fetch model config in parallel with SDK initialization for new sessions. + // The gateway REST call doesn't depend on the SDK being ready. + const [modelOptions] = await Promise.all([ + this.getModelConfigOptions( + settingsManager.getSettings().model || meta?.model || undefined, + ), + // For new sessions, await initialization concurrently with model fetch. + // SDK starts in the background via query() — we just need it ready + // before the first prompt, not before returning configOptions. + ...(!isResume + ? [ + withTimeout(q.initializationResult(), SESSION_VALIDATION_TIMEOUT_MS) + .then((result) => { + if (result.result === "timeout") { + this.logger.error("Session initialization timed out", { + sessionId, + taskId, + taskRunId: meta?.taskRunId, + }); + } + }) + .catch((err) => { + this.logger.error("Session initialization failed", { + sessionId, + taskId, + taskRunId: meta?.taskRunId, + error: err instanceof Error ? err.message : String(err), + }); + }), + ] + : []), + // Fire notification in parallel too + ...(meta?.taskRunId + ? [ + this.client.extNotification("_posthog/sdk_session", { + taskRunId: meta.taskRunId, + sessionId, + adapter: "claude", + }), + ] + : []), + ]); - // Resolve model: settings model takes priority, then meta model, then gateway default const settingsModel = settingsManager.getSettings().model; const metaModel = meta?.model; - const modelOptions = await this.getModelConfigOptions( - settingsModel || metaModel || undefined, - ); const resolvedModelId = settingsModel || metaModel || modelOptions.currentModelId; session.modelId = resolvedModelId; From ff2787dc258ec849ce257fc00cfc0c418f7fcf26 Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Wed, 1 Apr 2026 21:42:31 -0700 Subject: [PATCH 3/3] Throw on new session init failure instead of swallowing --- apps/code/src/main/services/agent/service.ts | 4 +- .../features/sessions/service/service.ts | 2 +- .../agent/src/adapters/claude/claude-agent.ts | 55 +++++++++---------- 3 files changed, 30 insertions(+), 31 deletions(-) diff --git a/apps/code/src/main/services/agent/service.ts b/apps/code/src/main/services/agent/service.ts index c3a2e3f8b..409632d66 100644 --- a/apps/code/src/main/services/agent/service.ts +++ b/apps/code/src/main/services/agent/service.ts @@ -641,7 +641,7 @@ export class AgentService extends TypedEventEmitter { sessionId: existingSessionId, systemPrompt, ...(permissionMode && { permissionMode }), - ...(model && { model }), + ...(model != null && { model }), claudeCode: { options: { ...(additionalDirectories?.length && { @@ -673,7 +673,7 @@ export class AgentService extends TypedEventEmitter { taskRunId, systemPrompt, ...(permissionMode && { permissionMode }), - ...(model && { model }), + ...(model != null && { model }), claudeCode: { options: { ...(additionalDirectories?.length && { additionalDirectories }), diff --git a/apps/code/src/renderer/features/sessions/service/service.ts b/apps/code/src/renderer/features/sessions/service/service.ts index b4a9c16eb..c8843f34f 100644 --- a/apps/code/src/renderer/features/sessions/service/service.ts +++ b/apps/code/src/renderer/features/sessions/service/service.ts @@ -549,7 +549,7 @@ export class SessionService { effort: effortLevelSchema.safeParse(reasoningLevel).success ? (reasoningLevel as EffortLevel) : undefined, - model: preferredModel || undefined, + model: preferredModel, }); const session = this.createBaseSession(taskRun.id, taskId, taskTitle); diff --git a/packages/agent/src/adapters/claude/claude-agent.ts b/packages/agent/src/adapters/claude/claude-agent.ts index 0a2527b57..78719875f 100644 --- a/packages/agent/src/adapters/claude/claude-agent.ts +++ b/packages/agent/src/adapters/claude/claude-agent.ts @@ -910,38 +910,16 @@ export class ClaudeAcpAgent extends BaseAcpAgent { } } - // Fetch model config in parallel with SDK initialization for new sessions. - // The gateway REST call doesn't depend on the SDK being ready. + // Kick off SDK initialization for new sessions so it runs concurrently + // with the model config fetch below (the gateway REST call is independent). + const initPromise = !isResume + ? withTimeout(q.initializationResult(), SESSION_VALIDATION_TIMEOUT_MS) + : undefined; + const [modelOptions] = await Promise.all([ this.getModelConfigOptions( settingsManager.getSettings().model || meta?.model || undefined, ), - // For new sessions, await initialization concurrently with model fetch. - // SDK starts in the background via query() — we just need it ready - // before the first prompt, not before returning configOptions. - ...(!isResume - ? [ - withTimeout(q.initializationResult(), SESSION_VALIDATION_TIMEOUT_MS) - .then((result) => { - if (result.result === "timeout") { - this.logger.error("Session initialization timed out", { - sessionId, - taskId, - taskRunId: meta?.taskRunId, - }); - } - }) - .catch((err) => { - this.logger.error("Session initialization failed", { - sessionId, - taskId, - taskRunId: meta?.taskRunId, - error: err instanceof Error ? err.message : String(err), - }); - }), - ] - : []), - // Fire notification in parallel too ...(meta?.taskRunId ? [ this.client.extNotification("_posthog/sdk_session", { @@ -953,6 +931,27 @@ export class ClaudeAcpAgent extends BaseAcpAgent { : []), ]); + if (initPromise) { + try { + const initResult = await initPromise; + if (initResult.result === "timeout") { + settingsManager.dispose(); + throw new Error( + `Session initialization timed out for sessionId=${sessionId}`, + ); + } + } catch (err) { + settingsManager.dispose(); + this.logger.error("Session initialization failed", { + sessionId, + taskId, + taskRunId: meta?.taskRunId, + error: err instanceof Error ? err.message : String(err), + }); + throw err; + } + } + const settingsModel = settingsManager.getSettings().model; const metaModel = meta?.model; const resolvedModelId =