Skip to content

Commit d217434

Browse files
authored
fix: Fix model selection lost during task creation (#1437)
1 parent b8d8008 commit d217434

6 files changed

Lines changed: 90 additions & 75 deletions

File tree

apps/code/src/main/services/agent/schemas.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export const startSessionInput = z.object({
4949
additionalDirectories: z.array(z.string()).optional(),
5050
customInstructions: z.string().max(2000).optional(),
5151
effort: effortLevelSchema.optional(),
52+
model: z.string().optional(),
5253
});
5354

5455
export type StartSessionInput = z.infer<typeof startSessionInput>;

apps/code/src/main/services/agent/service.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,8 @@ interface SessionConfig {
189189
customInstructions?: string;
190190
/** Effort level for Claude sessions */
191191
effort?: EffortLevel;
192+
/** Model to use for the session (e.g. "claude-sonnet-4-6") */
193+
model?: string;
192194
}
193195

194196
interface ManagedSession {
@@ -465,6 +467,7 @@ export class AgentService extends TypedEventEmitter<AgentServiceEvents> {
465467
permissionMode,
466468
customInstructions,
467469
effort,
470+
model,
468471
} = config;
469472

470473
// Preview sessions don't need a real repo — use a temp directory
@@ -638,6 +641,7 @@ export class AgentService extends TypedEventEmitter<AgentServiceEvents> {
638641
sessionId: existingSessionId,
639642
systemPrompt,
640643
...(permissionMode && { permissionMode }),
644+
...(model != null && { model }),
641645
claudeCode: {
642646
options: {
643647
...(additionalDirectories?.length && {
@@ -669,6 +673,7 @@ export class AgentService extends TypedEventEmitter<AgentServiceEvents> {
669673
taskRunId,
670674
systemPrompt,
671675
...(permissionMode && { permissionMode }),
676+
...(model != null && { model }),
672677
claudeCode: {
673678
options: {
674679
...(additionalDirectories?.length && { additionalDirectories }),
@@ -1362,6 +1367,7 @@ For git operations while detached:
13621367
customInstructions:
13631368
"customInstructions" in params ? params.customInstructions : undefined,
13641369
effort: "effort" in params ? params.effort : undefined,
1370+
model: "model" in params ? params.model : undefined,
13651371
};
13661372
}
13671373

apps/code/src/renderer/features/sessions/hooks/useSessionConnection.ts

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -86,28 +86,24 @@ export function useSessionConnection({
8686
return;
8787
}
8888

89-
connectingTasks.add(taskId);
90-
91-
const isNewSession = !task.latest_run?.id;
92-
const hasInitialPrompt = isNewSession && task.description;
89+
// New sessions (no latest_run) are handled by the task creation saga,
90+
// which passes model/adapter/executionMode. Only reconnect existing ones here.
91+
if (!task.latest_run?.id) return;
9392

94-
if (hasInitialPrompt) {
95-
markActivity(task.id);
96-
}
93+
connectingTasks.add(taskId);
9794

98-
log.info("Connecting to task session", {
95+
log.info("Reconnecting to existing task session", {
9996
taskId: task.id,
10097
hasLatestRun: !!task.latest_run,
10198
sessionStatus: session?.status ?? "none",
10299
});
103100

101+
markActivity(task.id);
102+
104103
getSessionService()
105104
.connectToTask({
106105
task,
107106
repoPath,
108-
initialPrompt: hasInitialPrompt
109-
? [{ type: "text", text: task.description }]
110-
: undefined,
111107
})
112108
.finally(() => {
113109
connectingTasks.delete(taskId);

apps/code/src/renderer/features/sessions/service/service.ts

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,7 @@ export class SessionService {
536536

537537
const { customInstructions: startCustomInstructions } =
538538
useSettingsStore.getState();
539+
const preferredModel = model ?? DEFAULT_GATEWAY_MODEL;
539540
const result = await trpcClient.agent.start.mutate({
540541
taskId,
541542
taskRunId: taskRun.id,
@@ -548,6 +549,7 @@ export class SessionService {
548549
effort: effortLevelSchema.safeParse(reasoningLevel).success
549550
? (reasoningLevel as EffortLevel)
550551
: undefined,
552+
model: preferredModel,
551553
});
552554

553555
const session = this.createBaseSession(taskRun.id, taskId, taskTitle);
@@ -586,32 +588,6 @@ export class SessionService {
586588
adapter,
587589
});
588590

589-
const preferredModel = model ?? DEFAULT_GATEWAY_MODEL;
590-
const configPromises: Promise<void>[] = [];
591-
if (preferredModel) {
592-
configPromises.push(
593-
this.setSessionConfigOptionByCategory(
594-
taskId,
595-
"model",
596-
preferredModel,
597-
).catch((err) => log.warn("Failed to set model", { taskId, err })),
598-
);
599-
}
600-
if (reasoningLevel) {
601-
configPromises.push(
602-
this.setSessionConfigOptionByCategory(
603-
taskId,
604-
"thought_level",
605-
reasoningLevel,
606-
).catch((err) =>
607-
log.warn("Failed to set reasoning level", { taskId, err }),
608-
),
609-
);
610-
}
611-
if (configPromises.length > 0) {
612-
await Promise.all(configPromises);
613-
}
614-
615591
if (initialPrompt?.length) {
616592
await this.sendPrompt(taskId, initialPrompt);
617593
}

packages/agent/src/adapters/claude/claude-agent.ts

Lines changed: 72 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -876,58 +876,92 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
876876
{ sessionId, taskId, taskRunId: meta?.taskRunId },
877877
);
878878

879-
try {
880-
const result = await withTimeout(
881-
q.initializationResult(),
882-
SESSION_VALIDATION_TIMEOUT_MS,
883-
);
884-
if (result.result === "timeout") {
885-
throw new Error(
886-
`Session ${isResume ? (forkSession ? "fork" : "resumption") : "initialization"} timed out for sessionId=${sessionId}`,
879+
if (isResume) {
880+
// Resume must block on initialization to validate the session is still alive.
881+
// For stale sessions this throws (e.g. "No conversation found").
882+
try {
883+
const result = await withTimeout(
884+
q.initializationResult(),
885+
SESSION_VALIDATION_TIMEOUT_MS,
887886
);
887+
if (result.result === "timeout") {
888+
throw new Error(
889+
`Session ${forkSession ? "fork" : "resumption"} timed out for sessionId=${sessionId}`,
890+
);
891+
}
892+
} catch (err) {
893+
settingsManager.dispose();
894+
if (
895+
err instanceof Error &&
896+
err.message === "Query closed before response received"
897+
) {
898+
throw RequestError.resourceNotFound(sessionId);
899+
}
900+
this.logger.error(
901+
forkSession ? "Session fork failed" : "Session resumption failed",
902+
{
903+
sessionId,
904+
taskId,
905+
taskRunId: meta?.taskRunId,
906+
error: err instanceof Error ? err.message : String(err),
907+
},
908+
);
909+
throw err;
888910
}
889-
} catch (err) {
890-
settingsManager.dispose();
891-
if (
892-
isResume &&
893-
err instanceof Error &&
894-
err.message === "Query closed before response received"
895-
) {
896-
throw RequestError.resourceNotFound(sessionId);
897-
}
898-
this.logger.error(
899-
isResume
900-
? forkSession
901-
? "Session fork failed"
902-
: "Session resumption failed"
903-
: "Session initialization failed",
904-
{
911+
}
912+
913+
// Kick off SDK initialization for new sessions so it runs concurrently
914+
// with the model config fetch below (the gateway REST call is independent).
915+
const initPromise = !isResume
916+
? withTimeout(q.initializationResult(), SESSION_VALIDATION_TIMEOUT_MS)
917+
: undefined;
918+
919+
const [modelOptions] = await Promise.all([
920+
this.getModelConfigOptions(
921+
settingsManager.getSettings().model || meta?.model || undefined,
922+
),
923+
...(meta?.taskRunId
924+
? [
925+
this.client.extNotification("_posthog/sdk_session", {
926+
taskRunId: meta.taskRunId,
927+
sessionId,
928+
adapter: "claude",
929+
}),
930+
]
931+
: []),
932+
]);
933+
934+
if (initPromise) {
935+
try {
936+
const initResult = await initPromise;
937+
if (initResult.result === "timeout") {
938+
settingsManager.dispose();
939+
throw new Error(
940+
`Session initialization timed out for sessionId=${sessionId}`,
941+
);
942+
}
943+
} catch (err) {
944+
settingsManager.dispose();
945+
this.logger.error("Session initialization failed", {
905946
sessionId,
906947
taskId,
907948
taskRunId: meta?.taskRunId,
908949
error: err instanceof Error ? err.message : String(err),
909-
},
910-
);
911-
throw err;
912-
}
913-
914-
if (meta?.taskRunId) {
915-
await this.client.extNotification("_posthog/sdk_session", {
916-
taskRunId: meta.taskRunId,
917-
sessionId,
918-
adapter: "claude",
919-
});
950+
});
951+
throw err;
952+
}
920953
}
921954

922-
// Resolve model: settings model takes priority, then gateway
923955
const settingsModel = settingsManager.getSettings().model;
924-
const modelOptions = await this.getModelConfigOptions();
925-
const resolvedModelId = settingsModel || modelOptions.currentModelId;
956+
const metaModel = meta?.model;
957+
const resolvedModelId =
958+
settingsModel || metaModel || modelOptions.currentModelId;
926959
session.modelId = resolvedModelId;
927960
session.lastContextWindowSize =
928961
this.getContextWindowForModel(resolvedModelId);
929962

930963
const resolvedSdkModel = toSdkModelId(resolvedModelId);
964+
931965
if (!isResume && resolvedSdkModel !== DEFAULT_MODEL) {
932966
await this.session.query.setModel(resolvedSdkModel);
933967
}

packages/agent/src/adapters/claude/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ export type NewSessionMeta = {
108108
persistence?: { taskId?: string; runId?: string; logUrl?: string };
109109
additionalRoots?: string[];
110110
allowedDomains?: string[];
111+
/** Model ID to use for this session (e.g. "claude-sonnet-4-6") */
112+
model?: string;
111113
claudeCode?: {
112114
options?: Options;
113115
};

0 commit comments

Comments
 (0)