fix: make initial prompt preservation conditional on power-steering feature

- Add powerSteeringEnabled parameter to summarizeConversation function
- Check power steering experiment flag before preserving first message
- Update all callers to pass power steering state
- Update tests to handle new parameter

This prevents tasks from being restarted unnecessarily after context
condensing when power steering is disabled.

Fixes #8093
This commit is contained in:
Roo Code 2025-09-17 22:22:41 +00:00
parent b1c104e20f
commit 334d310c64
6 changed files with 164 additions and 21 deletions

View file

@ -72,7 +72,17 @@ describe("Condense", () => {
{ role: "user", content: "Ninth message" },
]
const result = await summarizeConversation(messages, mockApiHandler, "System prompt", taskId, 5000, false)
const result = await summarizeConversation(
messages,
mockApiHandler,
"System prompt",
taskId,
5000,
false,
undefined,
undefined,
true,
) // powerSteeringEnabled = true to preserve first message
// Verify the first message is preserved
expect(result.messages[0]).toEqual(messages[0])
@ -106,7 +116,17 @@ describe("Condense", () => {
{ role: "user", content: "Thanks!" },
]
const result = await summarizeConversation(messages, mockApiHandler, "System prompt", taskId, 5000, false)
const result = await summarizeConversation(
messages,
mockApiHandler,
"System prompt",
taskId,
5000,
false,
undefined,
undefined,
true,
) // powerSteeringEnabled = true to preserve first message
// The first message with slash command should be intact
expect(result.messages[0].content).toBe(slashCommandContent)
@ -131,7 +151,17 @@ describe("Condense", () => {
{ role: "user", content: "Perfect!" },
]
const result = await summarizeConversation(messages, mockApiHandler, "System prompt", taskId, 5000, false)
const result = await summarizeConversation(
messages,
mockApiHandler,
"System prompt",
taskId,
5000,
false,
undefined,
undefined,
true,
) // powerSteeringEnabled = true to preserve first message
// The first message with complex content should be preserved
expect(result.messages[0].content).toEqual(complexContent)
@ -146,7 +176,17 @@ describe("Condense", () => {
{ role: "assistant", content: "Fourth message" },
]
const result = await summarizeConversation(messages, mockApiHandler, "System prompt", taskId, 5000, false)
const result = await summarizeConversation(
messages,
mockApiHandler,
"System prompt",
taskId,
5000,
false,
undefined,
undefined,
false,
)
// Should return an error since we have only 4 messages (first + 3 to keep)
expect(result.error).toBeDefined()
@ -165,7 +205,17 @@ describe("Condense", () => {
{ role: "user", content: "Final message" },
]
const result = await summarizeConversation(messages, mockApiHandler, "System prompt", taskId, 5000, false)
const result = await summarizeConversation(
messages,
mockApiHandler,
"System prompt",
taskId,
5000,
false,
undefined,
undefined,
false,
)
// Should return an error due to recent summary in last N messages
expect(result.error).toBeDefined()
@ -198,7 +248,17 @@ describe("Condense", () => {
{ role: "user", content: "Seventh" },
]
const result = await summarizeConversation(messages, emptyHandler, "System prompt", taskId, 5000, false)
const result = await summarizeConversation(
messages,
emptyHandler,
"System prompt",
taskId,
5000,
false,
undefined,
undefined,
false,
)
expect(result.error).toBeDefined()
expect(result.messages).toEqual(messages)

View file

@ -129,6 +129,10 @@ describe("summarizeConversation", () => {
defaultSystemPrompt,
taskId,
DEFAULT_PREV_CONTEXT_TOKENS,
false,
undefined,
undefined,
false, // powerSteeringEnabled
)
expect(result.messages).toEqual(messages)
expect(result.cost).toBe(0)
@ -155,6 +159,10 @@ describe("summarizeConversation", () => {
defaultSystemPrompt,
taskId,
DEFAULT_PREV_CONTEXT_TOKENS,
false,
undefined,
undefined,
false, // powerSteeringEnabled
)
expect(result.messages).toEqual(messages)
expect(result.cost).toBe(0)
@ -181,6 +189,10 @@ describe("summarizeConversation", () => {
defaultSystemPrompt,
taskId,
DEFAULT_PREV_CONTEXT_TOKENS,
false,
undefined,
undefined,
false, // powerSteeringEnabled
)
// Check that the API was called correctly
@ -188,14 +200,11 @@ describe("summarizeConversation", () => {
expect(maybeRemoveImageBlocks).toHaveBeenCalled()
// Verify the structure of the result
// The result should be: first message + summary + last N messages
expect(result.messages.length).toBe(1 + 1 + N_MESSAGES_TO_KEEP) // First + summary + last N
// Check that the first message is preserved
expect(result.messages[0]).toEqual(messages[0])
// When powerSteeringEnabled is false, the result should be: summary + last N messages
expect(result.messages.length).toBe(1 + N_MESSAGES_TO_KEEP) // summary + last N
// Check that the summary message was inserted correctly
const summaryMessage = result.messages[1]
const summaryMessage = result.messages[0]
expect(summaryMessage.role).toBe("assistant")
expect(summaryMessage.content).toBe("This is a summary")
expect(summaryMessage.isSummary).toBe(true)
@ -244,6 +253,10 @@ describe("summarizeConversation", () => {
defaultSystemPrompt,
taskId,
DEFAULT_PREV_CONTEXT_TOKENS,
false,
undefined,
undefined,
false, // powerSteeringEnabled
)
// Should return original messages when summary is empty
@ -265,7 +278,17 @@ describe("summarizeConversation", () => {
{ role: "user", content: "Tell me more", ts: 7 },
]
await summarizeConversation(messages, mockApiHandler, defaultSystemPrompt, taskId, DEFAULT_PREV_CONTEXT_TOKENS)
await summarizeConversation(
messages,
mockApiHandler,
defaultSystemPrompt,
taskId,
DEFAULT_PREV_CONTEXT_TOKENS,
false,
undefined,
undefined,
false,
)
// Verify the final request message
const expectedFinalMessage = {
@ -312,6 +335,10 @@ describe("summarizeConversation", () => {
systemPrompt,
taskId,
DEFAULT_PREV_CONTEXT_TOKENS,
false,
undefined,
undefined,
false, // powerSteeringEnabled
)
// Verify that countTokens was called with the correct messages including system prompt
@ -355,6 +382,10 @@ describe("summarizeConversation", () => {
defaultSystemPrompt,
taskId,
prevContextTokens,
false,
undefined,
undefined,
false, // powerSteeringEnabled
)
// Should return original messages when context would grow
@ -395,11 +426,15 @@ describe("summarizeConversation", () => {
defaultSystemPrompt,
taskId,
prevContextTokens,
false,
undefined,
undefined,
false, // powerSteeringEnabled
)
// Should successfully summarize
// Result should be: first message + summary + last N messages
expect(result.messages.length).toBe(1 + 1 + N_MESSAGES_TO_KEEP) // First + summary + last N
// When powerSteeringEnabled is false, the result should be: summary + last N messages
expect(result.messages.length).toBe(1 + N_MESSAGES_TO_KEEP) // summary + last N
expect(result.cost).toBe(0.03)
expect(result.summary).toBe("Concise summary")
expect(result.error).toBeUndefined()
@ -416,6 +451,10 @@ describe("summarizeConversation", () => {
defaultSystemPrompt,
taskId,
DEFAULT_PREV_CONTEXT_TOKENS,
false,
undefined,
undefined,
false, // powerSteeringEnabled
)
// Should return original messages when not enough to summarize
@ -444,6 +483,10 @@ describe("summarizeConversation", () => {
defaultSystemPrompt,
taskId,
DEFAULT_PREV_CONTEXT_TOKENS,
false,
undefined,
undefined,
false, // powerSteeringEnabled
)
// Should return original messages when recent summary exists
@ -493,6 +536,7 @@ describe("summarizeConversation", () => {
false,
undefined,
invalidCondensingHandler,
false, // powerSteeringEnabled
)
// Should return original messages when both handlers are invalid
@ -601,6 +645,8 @@ describe("summarizeConversation with custom settings", () => {
DEFAULT_PREV_CONTEXT_TOKENS,
false,
customPrompt,
undefined,
false, // powerSteeringEnabled
)
// Verify the custom prompt was used
@ -622,6 +668,8 @@ describe("summarizeConversation with custom settings", () => {
DEFAULT_PREV_CONTEXT_TOKENS,
false,
" ", // Empty custom prompt
undefined,
false, // powerSteeringEnabled
)
// Verify the default prompt was used
@ -639,6 +687,8 @@ describe("summarizeConversation with custom settings", () => {
DEFAULT_PREV_CONTEXT_TOKENS,
false,
undefined, // No custom prompt
undefined,
false, // powerSteeringEnabled
)
// Verify the default prompt was used again
@ -660,6 +710,7 @@ describe("summarizeConversation with custom settings", () => {
false,
undefined,
mockCondensingApiHandler,
false, // powerSteeringEnabled
)
// Verify the condensing handler was used
@ -680,6 +731,7 @@ describe("summarizeConversation with custom settings", () => {
false,
undefined,
undefined,
false, // powerSteeringEnabled
)
// Verify the main handler was used
@ -711,6 +763,7 @@ describe("summarizeConversation with custom settings", () => {
false,
undefined,
invalidHandler,
false, // powerSteeringEnabled
)
// Verify the main handler was used as fallback
@ -737,6 +790,8 @@ describe("summarizeConversation with custom settings", () => {
DEFAULT_PREV_CONTEXT_TOKENS,
false,
"Custom prompt",
undefined,
false, // powerSteeringEnabled
)
// Verify telemetry was called with custom prompt flag
@ -761,6 +816,7 @@ describe("summarizeConversation with custom settings", () => {
false,
undefined,
mockCondensingApiHandler,
false, // powerSteeringEnabled
)
// Verify telemetry was called with custom API handler flag
@ -785,6 +841,7 @@ describe("summarizeConversation with custom settings", () => {
true, // isAutomaticTrigger
"Custom prompt",
mockCondensingApiHandler,
false, // powerSteeringEnabled
)
// Verify telemetry was called with both flags

View file

@ -80,6 +80,7 @@ export type SummarizeResponse = {
* @param {boolean} isAutomaticTrigger - Whether the summarization is triggered automatically
* @param {string} customCondensingPrompt - Optional custom prompt to use for condensing
* @param {ApiHandler} condensingApiHandler - Optional specific API handler to use for condensing
* @param {boolean} powerSteeringEnabled - Whether power steering is enabled (controls initial prompt preservation)
* @returns {SummarizeResponse} - The result of the summarization operation (see above)
*/
export async function summarizeConversation(
@ -91,6 +92,7 @@ export async function summarizeConversation(
isAutomaticTrigger?: boolean,
customCondensingPrompt?: string,
condensingApiHandler?: ApiHandler,
powerSteeringEnabled?: boolean,
): Promise<SummarizeResponse> {
TelemetryService.instance.captureContextCondensed(
taskId,
@ -101,10 +103,14 @@ export async function summarizeConversation(
const response: SummarizeResponse = { messages, cost: 0, summary: "" }
// Always preserve the first message (which may contain slash command content)
const firstMessage = messages[0]
// Get messages to summarize, excluding the first message and last N messages
const messagesToSummarize = getMessagesSinceLastSummary(messages.slice(1, -N_MESSAGES_TO_KEEP))
// Conditionally preserve the first message based on power steering setting
// When power steering is enabled, preserve the initial prompt to maintain task context
// When disabled, don't preserve it to prevent task restarts after condensing
const firstMessage = powerSteeringEnabled ? messages[0] : undefined
const messagesToKeepFrom = powerSteeringEnabled ? 1 : 0
// Get messages to summarize, excluding the conditionally preserved first message and last N messages
const messagesToSummarize = getMessagesSinceLastSummary(messages.slice(messagesToKeepFrom, -N_MESSAGES_TO_KEEP))
if (messagesToSummarize.length <= 1) {
const error =
@ -188,8 +194,10 @@ export async function summarizeConversation(
isSummary: true,
}
// Reconstruct messages: [first message, summary, last N messages]
const newMessages = [firstMessage, summaryMessage, ...keepMessages]
// Reconstruct messages: [first message (if power steering enabled), summary, last N messages]
const newMessages = firstMessage
? [firstMessage, summaryMessage, ...keepMessages]
: [summaryMessage, ...keepMessages]
// Count the tokens in the context for the next API request
// We only estimate the tokens in summaryMesage if outputTokens is 0, otherwise we use outputTokens

View file

@ -594,6 +594,7 @@ describe("Sliding Window", () => {
true,
undefined, // customCondensingPrompt
undefined, // condensingApiHandler
undefined, // powerSteeringEnabled
)
// Verify the result contains the summary information
@ -765,6 +766,7 @@ describe("Sliding Window", () => {
true,
undefined, // customCondensingPrompt
undefined, // condensingApiHandler
undefined, // powerSteeringEnabled
)
// Verify the result contains the summary information

View file

@ -6,6 +6,7 @@ import { ApiHandler } from "../../api"
import { MAX_CONDENSE_THRESHOLD, MIN_CONDENSE_THRESHOLD, summarizeConversation, SummarizeResponse } from "../condense"
import { ApiMessage } from "../task-persistence/apiMessages"
import { ANTHROPIC_DEFAULT_MAX_TOKENS } from "@roo-code/types"
import { experiments, EXPERIMENT_IDS } from "../../shared/experiments"
/**
* Default percentage of the context window to use as a buffer when deciding when to truncate
@ -77,6 +78,7 @@ type TruncateOptions = {
condensingApiHandler?: ApiHandler
profileThresholds: Record<string, number>
currentProfileId: string
powerSteeringEnabled?: boolean
}
type TruncateResponse = SummarizeResponse & { prevContextTokens: number }
@ -102,6 +104,7 @@ export async function truncateConversationIfNeeded({
condensingApiHandler,
profileThresholds,
currentProfileId,
powerSteeringEnabled,
}: TruncateOptions): Promise<TruncateResponse> {
let error: string | undefined
let cost = 0
@ -155,6 +158,7 @@ export async function truncateConversationIfNeeded({
true, // automatic trigger
customCondensingPrompt,
condensingApiHandler,
powerSteeringEnabled,
)
if (result.error) {
error = result.error

View file

@ -1006,6 +1006,9 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
const { contextTokens: prevContextTokens } = this.getTokenUsage()
// Check if power steering is enabled
const powerSteeringEnabled = experiments.isEnabled(state?.experiments ?? {}, EXPERIMENT_IDS.POWER_STEERING)
const {
messages,
summary,
@ -1021,6 +1024,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
false, // manual trigger
customCondensingPrompt, // User's custom prompt
condensingApiHandler, // Specific handler for condensing
powerSteeringEnabled, // Pass power steering state
)
if (error) {
this.say(
@ -2461,6 +2465,9 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
`Forcing truncation to ${FORCED_CONTEXT_REDUCTION_PERCENT}% of current context.`,
)
// Check if power steering is enabled
const powerSteeringEnabled = experiments.isEnabled(state?.experiments ?? {}, EXPERIMENT_IDS.POWER_STEERING)
// Force aggressive truncation by keeping only 75% of the conversation history
const truncateResult = await truncateConversationIfNeeded({
messages: this.apiConversationHistory,
@ -2474,6 +2481,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
taskId: this.taskId,
profileThresholds,
currentProfileId,
powerSteeringEnabled,
})
if (truncateResult.messages !== this.apiConversationHistory) {
@ -2577,6 +2585,9 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// Get the current profile ID using the helper method
const currentProfileId = this.getCurrentProfileId(state)
// Check if power steering is enabled
const powerSteeringEnabled = experiments.isEnabled(state?.experiments ?? {}, EXPERIMENT_IDS.POWER_STEERING)
const truncateResult = await truncateConversationIfNeeded({
messages: this.apiConversationHistory,
totalTokens: contextTokens,
@ -2591,6 +2602,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
condensingApiHandler,
profileThresholds,
currentProfileId,
powerSteeringEnabled,
})
if (truncateResult.messages !== this.apiConversationHistory) {
await this.overwriteApiConversationHistory(truncateResult.messages)