Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/code/src/main/services/agent/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof startSessionInput>;
Expand Down
6 changes: 6 additions & 0 deletions apps/code/src/main/services/agent/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -465,6 +467,7 @@ export class AgentService extends TypedEventEmitter<AgentServiceEvents> {
permissionMode,
customInstructions,
effort,
model,
} = config;

// Preview sessions don't need a real repo — use a temp directory
Expand Down Expand Up @@ -638,6 +641,7 @@ export class AgentService extends TypedEventEmitter<AgentServiceEvents> {
sessionId: existingSessionId,
systemPrompt,
...(permissionMode && { permissionMode }),
...(model != null && { model }),
claudeCode: {
options: {
...(additionalDirectories?.length && {
Expand Down Expand Up @@ -669,6 +673,7 @@ export class AgentService extends TypedEventEmitter<AgentServiceEvents> {
taskRunId,
systemPrompt,
...(permissionMode && { permissionMode }),
...(model != null && { model }),
claudeCode: {
options: {
...(additionalDirectories?.length && { additionalDirectories }),
Expand Down Expand Up @@ -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,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
28 changes: 2 additions & 26 deletions apps/code/src/renderer/features/sessions/service/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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);
Expand Down Expand Up @@ -586,32 +588,6 @@ export class SessionService {
adapter,
});

const preferredModel = model ?? DEFAULT_GATEWAY_MODEL;
const configPromises: Promise<void>[] = [];
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);
}
Expand Down
110 changes: 72 additions & 38 deletions packages/agent/src/adapters/claude/claude-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
2 changes: 2 additions & 0 deletions packages/agent/src/adapters/claude/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand Down
Loading