From 97a783d00bb484d7bea2fa80dc8a77fa8bb3f5e8 Mon Sep 17 00:00:00 2001 From: Ammar Date: Mon, 24 Nov 2025 11:04:54 -0600 Subject: [PATCH] =?UTF-8?q?=F0=9F=A4=96=20fix:=20use=20correct=20model=20f?= =?UTF-8?q?or=20post-compaction=20continue=20messages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduced in a31a2e0, the auto-continue message queueing logic was inheriting the compaction model (e.g. haiku) for the follow-up task. This fix: 1. Preserves the original workspace model as `resumeModel` in compaction metadata 2. Sanitizes the queued continue message options to strip compaction-specific flags 3. Restores the correct model for the follow-up turn --- mobile/src/utils/slashCommandHelpers.test.ts | 1 + mobile/src/utils/slashCommandHelpers.ts | 1 + src/browser/utils/chatCommands.test.ts | 39 ++++++++++++++++++- src/browser/utils/chatCommands.ts | 1 + src/common/types/message.ts | 2 + src/node/services/agentSession.ts | 14 ++++++- .../compactionContinueOptions.test.ts | 37 ++++++++++++++++++ .../services/compactionContinueOptions.ts | 27 +++++++++++++ 8 files changed, 119 insertions(+), 3 deletions(-) create mode 100644 src/node/services/compactionContinueOptions.test.ts create mode 100644 src/node/services/compactionContinueOptions.ts diff --git a/mobile/src/utils/slashCommandHelpers.test.ts b/mobile/src/utils/slashCommandHelpers.test.ts index cfade10b48..02a456682b 100644 --- a/mobile/src/utils/slashCommandHelpers.test.ts +++ b/mobile/src/utils/slashCommandHelpers.test.ts @@ -56,6 +56,7 @@ describe("buildMobileCompactionPayload", () => { model: "anthropic:claude-opus-4-1", maxOutputTokens: 800, continueMessage: parsed.continueMessage, + resumeModel: baseOptions.model, }); expect(payload.sendOptions.model).toBe("anthropic:claude-opus-4-1"); expect(payload.sendOptions.mode).toBe("compact"); diff --git a/mobile/src/utils/slashCommandHelpers.ts b/mobile/src/utils/slashCommandHelpers.ts index 0282ce8595..e930480d9b 100644 --- a/mobile/src/utils/slashCommandHelpers.ts +++ b/mobile/src/utils/slashCommandHelpers.ts @@ -57,6 +57,7 @@ export function buildMobileCompactionPayload( model: parsed.model, maxOutputTokens: parsed.maxOutputTokens, continueMessage: parsed.continueMessage, + resumeModel: baseOptions.model, }, }; diff --git a/src/browser/utils/chatCommands.test.ts b/src/browser/utils/chatCommands.test.ts index 57385f0af8..cb42943d8b 100644 --- a/src/browser/utils/chatCommands.test.ts +++ b/src/browser/utils/chatCommands.test.ts @@ -1,4 +1,14 @@ -import { parseRuntimeString } from "./chatCommands"; +import { describe, expect, test, beforeEach } from "bun:test"; +import type { SendMessageOptions } from "@/common/types/ipc"; +import { parseRuntimeString, prepareCompactionMessage } from "./chatCommands"; + +// Simple mock for localStorage to satisfy resolveCompactionModel +beforeEach(() => { + globalThis.localStorage = { + getItem: () => null, + setItem: () => undefined, + } as unknown as Storage; +}); describe("parseRuntimeString", () => { const workspaceName = "test-workspace"; @@ -84,3 +94,30 @@ describe("parseRuntimeString", () => { ); }); }); + +describe("prepareCompactionMessage", () => { + const createBaseOptions = (): SendMessageOptions => ({ + model: "anthropic:claude-3-5-sonnet", + thinkingLevel: "medium", + toolPolicy: [], + mode: "exec", + }); + + test("embeds resumeModel from base send options", () => { + const sendMessageOptions = createBaseOptions(); + const { metadata } = prepareCompactionMessage({ + workspaceId: "ws-1", + maxOutputTokens: 4096, + continueMessage: "Keep building", + model: "anthropic:claude-3-5-haiku", + sendMessageOptions, + }); + + expect(metadata.type).toBe("compaction-request"); + if (metadata.type !== "compaction-request") { + throw new Error("Expected compaction metadata"); + } + + expect(metadata.parsed.resumeModel).toBe(sendMessageOptions.model); + }); +}); diff --git a/src/browser/utils/chatCommands.ts b/src/browser/utils/chatCommands.ts index 39f63800b2..bd7f064fa6 100644 --- a/src/browser/utils/chatCommands.ts +++ b/src/browser/utils/chatCommands.ts @@ -214,6 +214,7 @@ export function prepareCompactionMessage(options: CompactionOptions): { model: effectiveModel, maxOutputTokens: options.maxOutputTokens, continueMessage: options.continueMessage, + resumeModel: options.sendMessageOptions.model, }; const metadata: MuxFrontendMetadata = { diff --git a/src/common/types/message.ts b/src/common/types/message.ts index 0d88b52d45..90a6190d6f 100644 --- a/src/common/types/message.ts +++ b/src/common/types/message.ts @@ -10,6 +10,7 @@ export interface CompactionRequestData { model?: string; // Custom model override for compaction maxOutputTokens?: number; continueMessage?: string; + resumeModel?: string; // Original workspace model to use after compaction continues } // Frontend-specific metadata stored in muxMetadata field @@ -103,6 +104,7 @@ export type DisplayedMessage = parsed: { maxOutputTokens?: number; continueMessage?: string; + resumeModel?: string; }; }; } diff --git a/src/node/services/agentSession.ts b/src/node/services/agentSession.ts index df98d9b477..07012203eb 100644 --- a/src/node/services/agentSession.ts +++ b/src/node/services/agentSession.ts @@ -23,6 +23,7 @@ import { Ok, Err } from "@/common/types/result"; import { enforceThinkingPolicy } from "@/browser/utils/thinking/policy"; import { createRuntime } from "@/node/runtime/runtimeFactory"; import { MessageQueue } from "./messageQueue"; +import { buildContinueMessageOptions } from "./compactionContinueOptions"; import type { StreamEndEvent, StreamAbortEvent } from "@/common/types/stream"; import { CompactionHandler } from "./compactionHandler"; @@ -336,8 +337,17 @@ export class AgentSession { const muxMeta = options?.muxMetadata; if (muxMeta?.type === "compaction-request" && muxMeta.parsed.continueMessage && options) { // Strip out edit-specific and compaction-specific fields so the queued message is a fresh user message - const { muxMetadata, mode, editMessageId, ...continueOptions } = options; - this.messageQueue.add(muxMeta.parsed.continueMessage, continueOptions); + const { muxMetadata, mode, editMessageId, imageParts, ...rest } = options; + const baseContinueOptions: SendMessageOptions = { ...rest }; + const sanitizedOptions = buildContinueMessageOptions( + baseContinueOptions, + muxMeta.parsed.resumeModel + ); + const continuePayload = + imageParts && imageParts.length > 0 + ? { ...sanitizedOptions, imageParts } + : sanitizedOptions; + this.messageQueue.add(muxMeta.parsed.continueMessage, continuePayload); this.emitQueuedMessageChanged(); } diff --git a/src/node/services/compactionContinueOptions.test.ts b/src/node/services/compactionContinueOptions.test.ts new file mode 100644 index 0000000000..3593a0b751 --- /dev/null +++ b/src/node/services/compactionContinueOptions.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from "bun:test"; +import type { SendMessageOptions } from "@/common/types/ipc"; +import { buildContinueMessageOptions } from "./compactionContinueOptions"; + +const baseOptions = (): SendMessageOptions => ({ + model: "anthropic:claude-3-5-sonnet", + thinkingLevel: "medium", + toolPolicy: [], + additionalSystemInstructions: "be helpful", + mode: "compact", + maxOutputTokens: 2048, +}); + +describe("buildContinueMessageOptions", () => { + it("uses resumeModel when provided and drops compact overrides", () => { + const options = baseOptions(); + const result = buildContinueMessageOptions(options, "anthropic:claude-3-5-haiku"); + + expect(result).not.toBe(options); + expect(result.model).toBe("anthropic:claude-3-5-haiku"); + expect(result.mode).toBeUndefined(); + expect(result.maxOutputTokens).toBeUndefined(); + expect(result.thinkingLevel).toBe("medium"); + expect(result.toolPolicy).toEqual([]); + // Ensure original options untouched + expect(options.model).toBe("anthropic:claude-3-5-sonnet"); + expect(options.mode).toBe("compact"); + expect(options.maxOutputTokens).toBe(2048); + }); + + it("falls back to compaction model when resumeModel is missing", () => { + const options = baseOptions(); + const result = buildContinueMessageOptions(options); + + expect(result.model).toBe(options.model); + }); +}); diff --git a/src/node/services/compactionContinueOptions.ts b/src/node/services/compactionContinueOptions.ts new file mode 100644 index 0000000000..5c38fa694c --- /dev/null +++ b/src/node/services/compactionContinueOptions.ts @@ -0,0 +1,27 @@ +import type { SendMessageOptions } from "@/common/types/ipc"; + +/** + * Build sanitized SendMessageOptions for auto-continue messages after compaction. + * + * - Drops compaction-specific overrides (mode="compact", maxOutputTokens) + * - Removes frontend metadata (muxMetadata) + * - Restores the original workspace model when provided + */ +export function buildContinueMessageOptions( + options: SendMessageOptions, + resumeModel?: string +): SendMessageOptions { + const { + muxMetadata: _ignoredMetadata, + maxOutputTokens: _ignoredMaxOutputTokens, + mode: _ignoredMode, + ...rest + } = options; + + const nextModel = resumeModel ?? options.model; + + return { + ...rest, + model: nextModel, + }; +}