diff --git a/src/node/services/workspaceService.ts b/src/node/services/workspaceService.ts index a26e835a1..48d0739f6 100644 --- a/src/node/services/workspaceService.ts +++ b/src/node/services/workspaceService.ts @@ -959,6 +959,33 @@ export class WorkspaceService extends EventEmitter { return { model, thinkingLevel }; } + /** + * Best-effort persist AI settings from send/resume options. + * Skips compaction requests which use a different model intentionally. + */ + private async maybePersistAISettingsFromOptions( + workspaceId: string, + options: SendMessageOptions | undefined, + context: "send" | "resume" + ): Promise { + // Skip for compaction - it may use a different model and shouldn't override user preference + const isCompaction = options?.mode === "compact"; + if (isCompaction) return; + + const extractedSettings = this.extractWorkspaceAISettingsFromSendOptions(options); + if (!extractedSettings) return; + + const persistResult = await this.persistWorkspaceAISettings(workspaceId, extractedSettings, { + emitMetadata: false, + }); + if (!persistResult.success) { + log.debug(`Failed to persist workspace AI settings from ${context} options`, { + workspaceId, + error: persistResult.error, + }); + } + } + private async persistWorkspaceAISettings( workspaceId: string, aiSettings: WorkspaceAISettings, @@ -1275,23 +1302,7 @@ export class WorkspaceService extends EventEmitter { }; // Persist last-used model + thinking level for cross-device consistency. - // Best-effort: failures should not block sending. - const extractedSettings = this.extractWorkspaceAISettingsFromSendOptions(resolvedOptions); - if (extractedSettings) { - const persistResult = await this.persistWorkspaceAISettings( - workspaceId, - extractedSettings, - { - emitMetadata: false, - } - ); - if (!persistResult.success) { - log.debug("Failed to persist workspace AI settings from send options", { - workspaceId, - error: persistResult.error, - }); - } - } + await this.maybePersistAISettingsFromOptions(workspaceId, resolvedOptions, "send"); if (this.aiService.isStreaming(workspaceId) && !resolvedOptions?.editMessageId) { const pendingAskUserQuestion = askUserQuestionManager.getLatestPending(workspaceId); @@ -1405,23 +1416,7 @@ export class WorkspaceService extends EventEmitter { const session = this.getOrCreateSession(workspaceId); // Persist last-used model + thinking level for cross-device consistency. - // Best-effort: failures should not block resuming. - const extractedSettings = this.extractWorkspaceAISettingsFromSendOptions(options); - if (extractedSettings) { - const persistResult = await this.persistWorkspaceAISettings( - workspaceId, - extractedSettings, - { - emitMetadata: false, - } - ); - if (!persistResult.success) { - log.debug("Failed to persist workspace AI settings from resume options", { - workspaceId, - error: persistResult.error, - }); - } - } + await this.maybePersistAISettingsFromOptions(workspaceId, options, "resume"); const result = await session.resumeStream(options); if (!result.success) { diff --git a/tests/ipc/workspaceAISettings.test.ts b/tests/ipc/workspaceAISettings.test.ts index 00d71b6f3..5eeab1087 100644 --- a/tests/ipc/workspaceAISettings.test.ts +++ b/tests/ipc/workspaceAISettings.test.ts @@ -48,4 +48,56 @@ describe("workspace.updateAISettings", () => { await cleanupTempGitRepo(tempGitRepo); } }, 60000); + + test("compaction requests do not override workspace aiSettings", async () => { + const env: TestEnvironment = await createTestEnvironment(); + const tempGitRepo = await createTempGitRepo(); + + try { + const branchName = generateBranchName("ai-settings-compact"); + const createResult = await createWorkspace(env, tempGitRepo, branchName); + if (!createResult.success) { + throw new Error(`Workspace creation failed: ${createResult.error}`); + } + + const workspaceId = createResult.metadata.id; + expect(workspaceId).toBeTruthy(); + + const client = resolveOrpcClient(env); + + // Set initial workspace AI settings + const updateResult = await client.workspace.updateAISettings({ + workspaceId: workspaceId!, + aiSettings: { model: "anthropic:claude-sonnet-4-20250514", thinkingLevel: "medium" }, + }); + expect(updateResult.success).toBe(true); + + // Send a compaction request with a different model + // The muxMetadata type: "compaction-request" should prevent AI settings from being persisted + await client.workspace.sendMessage({ + workspaceId: workspaceId!, + message: "Summarize the conversation", + options: { + model: "openai:gpt-4.1-mini", // Different model for compaction + thinkingLevel: "off", + mode: "compact", + muxMetadata: { + type: "compaction-request", + rawCommand: "/compact", + parsed: {}, + }, + }, + }); + + // Verify the original workspace AI settings were NOT overwritten + const info = await client.workspace.getInfo({ workspaceId: workspaceId! }); + expect(info?.aiSettings).toEqual({ + model: "anthropic:claude-sonnet-4-20250514", + thinkingLevel: "medium", + }); + } finally { + await cleanupTestEnvironment(env); + await cleanupTempGitRepo(tempGitRepo); + } + }, 60000); });