Skip to content
Merged
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
63 changes: 29 additions & 34 deletions src/node/services/workspaceService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
// 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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down
52 changes: 52 additions & 0 deletions tests/ipc/workspaceAISettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});