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..409632d66 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 != null && { model }), claudeCode: { options: { ...(additionalDirectories?.length && { @@ -669,6 +673,7 @@ export class AgentService extends TypedEventEmitter { taskRunId, systemPrompt, ...(permissionMode && { permissionMode }), + ...(model != null && { 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..c8843f34f 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, }); 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..78719875f 100644 --- a/packages/agent/src/adapters/claude/claude-agent.ts +++ b/packages/agent/src/adapters/claude/claude-agent.ts @@ -876,58 +876,92 @@ 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", - { + } + + // 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, + ), + ...(meta?.taskRunId + ? [ + this.client.extNotification("_posthog/sdk_session", { + taskRunId: meta.taskRunId, + sessionId, + adapter: "claude", + }), + ] + : []), + ]); + + 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; - } - - if (meta?.taskRunId) { - await this.client.extNotification("_posthog/sdk_session", { - taskRunId: meta.taskRunId, - sessionId, - adapter: "claude", - }); + }); + throw err; + } } - // Resolve model: settings model takes priority, then gateway const settingsModel = settingsManager.getSettings().model; - const modelOptions = await this.getModelConfigOptions(); - const resolvedModelId = settingsModel || modelOptions.currentModelId; + const metaModel = meta?.model; + 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; };