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
1 change: 1 addition & 0 deletions mobile/src/utils/slashCommandHelpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
1 change: 1 addition & 0 deletions mobile/src/utils/slashCommandHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export function buildMobileCompactionPayload(
model: parsed.model,
maxOutputTokens: parsed.maxOutputTokens,
continueMessage: parsed.continueMessage,
resumeModel: baseOptions.model,
},
};

Expand Down
39 changes: 38 additions & 1 deletion src/browser/utils/chatCommands.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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);
});
});
1 change: 1 addition & 0 deletions src/browser/utils/chatCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ export function prepareCompactionMessage(options: CompactionOptions): {
model: effectiveModel,
maxOutputTokens: options.maxOutputTokens,
continueMessage: options.continueMessage,
resumeModel: options.sendMessageOptions.model,
};

const metadata: MuxFrontendMetadata = {
Expand Down
2 changes: 2 additions & 0 deletions src/common/types/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -103,6 +104,7 @@ export type DisplayedMessage =
parsed: {
maxOutputTokens?: number;
continueMessage?: string;
resumeModel?: string;
};
};
}
Expand Down
14 changes: 12 additions & 2 deletions src/node/services/agentSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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();
}

Expand Down
37 changes: 37 additions & 0 deletions src/node/services/compactionContinueOptions.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
27 changes: 27 additions & 0 deletions src/node/services/compactionContinueOptions.ts
Original file line number Diff line number Diff line change
@@ -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,
};
}