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 src/browser/hooks/useResumeManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ export function useResumeManager() {
text: lastUserMsg.compactionRequest.parsed.continueMessage?.text ?? "",
imageParts: lastUserMsg.compactionRequest.parsed.continueMessage?.imageParts,
model: lastUserMsg.compactionRequest.parsed.continueMessage?.model ?? options.model,
mode: lastUserMsg.compactionRequest.parsed.continueMessage?.mode ?? "exec",
},
});
}
Expand Down
4 changes: 4 additions & 0 deletions src/browser/utils/chatCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,9 @@ export function prepareCompactionMessage(options: CompactionOptions): {
// Create compaction metadata (will be stored in user message)
// Only include continueMessage if there's text, images, or reviews to queue after compaction
const hasText = continueText;
// Determine mode for continue message - use mode from sendMessageOptions if it's exec/plan, otherwise default to exec
const continueMode = options.sendMessageOptions.mode === "plan" ? "plan" : "exec";

const compactData: CompactionRequestData = {
model: effectiveModel,
maxOutputTokens: options.maxOutputTokens,
Expand All @@ -664,6 +667,7 @@ export function prepareCompactionMessage(options: CompactionOptions): {
text: options.continueMessage?.text ?? "",
imageParts: options.continueMessage?.imageParts,
model: options.continueMessage?.model ?? options.sendMessageOptions.model,
mode: continueMode,
reviews: options.continueMessage?.reviews,
}
: undefined,
Expand Down
4 changes: 3 additions & 1 deletion src/common/types/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@ export interface UserMessageContent {

/**
* Message to continue with after compaction.
* Extends UserMessageContent with model preference.
* Extends UserMessageContent with model and mode preferences.
*/
export interface ContinueMessage extends UserMessageContent {
model?: string;
/** Mode for the continue message (determines tool policy). Defaults to 'exec'. */
mode?: "exec" | "plan";
}

// Parsed compaction request data (shared type for consistency)
Expand Down
76 changes: 76 additions & 0 deletions src/node/services/agentSession.continueMessageToolPolicy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { describe, expect, test } from "bun:test";
import { modeToToolPolicy } from "@/common/utils/ui/modeUtils";

/**
* Regression test for continue message tool policy bug.
*
* Bug: When a /compact command includes a continue message, the continue message
* was being queued with the compaction mode's tool policy (all tools disabled)
* instead of the intended execution mode's tool policy.
*
* Fix: Continue message now carries its own mode field, and the backend uses
* modeToToolPolicy(continueMessage.mode) instead of copying options.toolPolicy.
*
* This test verifies that the mode-to-policy transformation produces the expected
* tool policies for continue messages. The actual integration of this logic into
* agentSession.ts is verified by the type system and manual testing.
*/
describe("Continue message tool policy derivation", () => {
test("exec mode enables tools (not disabled-all like compaction)", () => {
const execPolicy = modeToToolPolicy("exec");
const compactionPolicy = [{ regex_match: ".*", action: "disable" }];

// Exec mode should NOT disable all tools like compaction does
expect(execPolicy).not.toEqual(compactionPolicy);

// Exec mode specifically disables propose_plan (the plan mode tool)
expect(execPolicy).toEqual([{ regex_match: "propose_plan", action: "disable" }]);
});

test("plan mode has different policy than compaction", () => {
const planPolicy = modeToToolPolicy("plan");
const compactionPolicy = [{ regex_match: ".*", action: "disable" }];

// Plan mode should NOT disable all tools like compaction does
expect(planPolicy).not.toEqual(compactionPolicy);

// Plan mode enables propose_plan
expect(planPolicy).toEqual([{ regex_match: "propose_plan", action: "enable" }]);
});

test("verifies fix: mode field determines policy, not inherited compaction policy", () => {
// This test documents the fix behavior:
// Before fix: continueMessage used options.toolPolicy (compaction's disabled-all)
// After fix: continueMessage.mode determines the policy via modeToToolPolicy()

// Simulating the fixed logic from agentSession.ts:
// const continueMode = continueMessage.mode ?? "exec";
// toolPolicy: modeToToolPolicy(continueMode)

const simulateContinueMessagePolicy = (mode?: "exec" | "plan") => {
const continueMode = mode ?? "exec"; // Default to exec as in the fix
return modeToToolPolicy(continueMode);
};

// Explicit exec mode
expect(simulateContinueMessagePolicy("exec")).toEqual([
{ regex_match: "propose_plan", action: "disable" },
]);

// Explicit plan mode
expect(simulateContinueMessagePolicy("plan")).toEqual([
{ regex_match: "propose_plan", action: "enable" },
]);

// No mode specified (defaults to exec)
expect(simulateContinueMessagePolicy(undefined)).toEqual([
{ regex_match: "propose_plan", action: "disable" },
]);

// None of these should be the compaction policy
const compactionPolicy = [{ regex_match: ".*", action: "disable" }];
expect(simulateContinueMessagePolicy("exec")).not.toEqual(compactionPolicy);
expect(simulateContinueMessagePolicy("plan")).not.toEqual(compactionPolicy);
expect(simulateContinueMessagePolicy(undefined)).not.toEqual(compactionPolicy);
});
});
5 changes: 4 additions & 1 deletion src/node/services/agentSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import type { PostCompactionAttachment, PostCompactionExclusions } from "@/commo
import { TURNS_BETWEEN_ATTACHMENTS } from "@/common/constants/attachments";
import { extractEditedFileDiffs } from "@/common/utils/messages/extractEditedFiles";
import { isValidModelFormat } from "@/common/utils/ai/models";
import { modeToToolPolicy } from "@/common/utils/ui/modeUtils";

/**
* Tracked file state for detecting external edits.
Expand Down Expand Up @@ -525,13 +526,15 @@ export class AgentSession {
const { finalText, metadata } = prepareUserMessageForSend(continueMessage);

// Build options for the queued message (strip compaction-specific fields)
// Use the mode from continueMessage to derive correct tool policy, not the compaction mode's disabled-all policy
const continueMode = continueMessage.mode ?? "exec";
const sanitizedOptions: Omit<
SendMessageOptions,
"muxMetadata" | "mode" | "editMessageId" | "imageParts" | "maxOutputTokens"
> & { imageParts?: typeof continueMessage.imageParts; muxMetadata?: typeof metadata } = {
model: continueMessage.model ?? options.model,
thinkingLevel: options.thinkingLevel,
toolPolicy: options.toolPolicy,
toolPolicy: modeToToolPolicy(continueMode),
additionalSystemInstructions: options.additionalSystemInstructions,
providerOptions: options.providerOptions,
experiments: options.experiments,
Expand Down