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
84 changes: 84 additions & 0 deletions src/core/config/ContextProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ export class ContextProxy {
// Migration: Move legacy customCondensingPrompt to customSupportPrompts
await this.migrateLegacyCondensingPrompt()

// Migration: Clear old default condensing prompt so users get the improved v2 default
await this.migrateOldDefaultCondensingPrompt()

this._isInitialized = true
}

Expand Down Expand Up @@ -138,6 +141,87 @@ export class ContextProxy {
}
}

/**
* Clears the old v1 default condensing prompt from customSupportPrompts.CONDENSE if present.
*
* Before PR #10873 "Intelligent Context Condensation v2", the default condensing prompt was
* a simpler 6-section format. Users who had this old default saved in their settings would
* be stuck with it instead of getting the improved v2 default (which includes analysis tags,
* error tracking, all user messages, and better task continuity).
*
* This migration uses fingerprinting to detect the old v1 default - checking for key
* identifying phrases unique to v1 and absence of v2-specific features. This is more
* lenient than exact matching and handles whitespace variations.
*/
private async migrateOldDefaultCondensingPrompt() {
try {
const currentSupportPrompts =
this.originalContext.globalState.get<Record<string, string>>("customSupportPrompts") || {}

const savedCondensePrompt = currentSupportPrompts.CONDENSE

if (savedCondensePrompt && this.isOldV1DefaultCondensePrompt(savedCondensePrompt)) {
logger.info(
"Clearing old v1 default condensing prompt from customSupportPrompts.CONDENSE - user will now get the improved v2 default",
)

// Remove the CONDENSE key from customSupportPrompts
const { CONDENSE: _, ...remainingPrompts } = currentSupportPrompts
const updatedPrompts = Object.keys(remainingPrompts).length > 0 ? remainingPrompts : undefined

await this.originalContext.globalState.update("customSupportPrompts", updatedPrompts)
this.stateCache.customSupportPrompts = updatedPrompts
}
} catch (error) {
logger.error(
`Error during old default condensing prompt migration: ${error instanceof Error ? error.message : String(error)}`,
)
}
}

/**
* Detects if a prompt is the old v1 default condensing prompt using fingerprinting.
* This is more lenient than exact matching - it checks for key identifying phrases
* unique to v1 and absence of v2-specific features.
*
* V1 characteristics:
* - Exactly 6 numbered sections (1-6)
* - Contains specific section headers like "Previous Conversation", "Current Work", etc.
* - Does NOT contain v2-specific features like "<analysis>", "SYSTEM OPERATION", etc.
*/
private isOldV1DefaultCondensePrompt(prompt: string): boolean {
// Key phrases unique to the v1 default (must ALL be present)
const v1RequiredPhrases = [
"Your task is to create a detailed summary of the conversation so far",
"1. Previous Conversation:",
"2. Current Work:",
"3. Key Technical Concepts:",
"4. Relevant Files and Code:",
"5. Problem Solving:",
"6. Pending Tasks and Next Steps:",
"Output only the summary of the conversation so far",
]

// V2-specific features (if ANY are present, this is NOT v1 default)
const v2Features = [
"<analysis>",
"SYSTEM OPERATION",
"Errors and fixes",
"All user messages",
"7.", // v2 has more than 6 sections
"8.",
"9.",
]

// Check that all v1 required phrases are present
const hasAllV1Phrases = v1RequiredPhrases.every((phrase) => prompt.toLowerCase().includes(phrase.toLowerCase()))

// Check that no v2 features are present
const hasNoV2Features = v2Features.every((feature) => !prompt.toLowerCase().includes(feature.toLowerCase()))

return hasAllV1Phrases && hasNoV2Features
}

/**
* Migrates invalid/removed apiProvider values by clearing them from storage.
* This handles cases where a user had a provider selected that was later removed
Expand Down
129 changes: 125 additions & 4 deletions src/core/config/__tests__/ContextProxy.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,16 +70,18 @@ describe("ContextProxy", () => {

describe("constructor", () => {
it("should initialize state cache with all global state keys", () => {
// +2 for the migration checks:
// +3 for the migration checks:
// 1. openRouterImageGenerationSettings
// 2. customCondensingPrompt
expect(mockGlobalState.get).toHaveBeenCalledTimes(GLOBAL_STATE_KEYS.length + 2)
// 3. customSupportPrompts (for migrateOldDefaultCondensingPrompt)
expect(mockGlobalState.get).toHaveBeenCalledTimes(GLOBAL_STATE_KEYS.length + 3)
for (const key of GLOBAL_STATE_KEYS) {
expect(mockGlobalState.get).toHaveBeenCalledWith(key)
}
// Also check for migration calls
expect(mockGlobalState.get).toHaveBeenCalledWith("openRouterImageGenerationSettings")
expect(mockGlobalState.get).toHaveBeenCalledWith("customCondensingPrompt")
expect(mockGlobalState.get).toHaveBeenCalledWith("customSupportPrompts")
})

it("should initialize secret cache with all secret keys", () => {
Expand All @@ -102,8 +104,8 @@ describe("ContextProxy", () => {
const result = proxy.getGlobalState("apiProvider")
expect(result).toBe("deepseek")

// Original context should be called once during updateGlobalState (+2 for migration checks)
expect(mockGlobalState.get).toHaveBeenCalledTimes(GLOBAL_STATE_KEYS.length + 2) // From initialization + migration checks
// Original context should be called once during updateGlobalState (+3 for migration checks)
expect(mockGlobalState.get).toHaveBeenCalledTimes(GLOBAL_STATE_KEYS.length + 3) // From initialization + migration checks
})

it("should handle default values correctly", async () => {
Expand Down Expand Up @@ -506,4 +508,123 @@ describe("ContextProxy", () => {
expect(settings.apiProvider).toBeUndefined()
})
})

describe("old default condensing prompt migration", () => {
// The old v1 default condensing prompt from before PR #10873
const OLD_V1_DEFAULT_CONDENSE_PROMPT = `Your task is to create a detailed summary of the conversation so far, paying close attention to the user's explicit requests and your previous actions.
This summary should be thorough in capturing technical details, code patterns, and architectural decisions that would be essential for continuing with the conversation and supporting any continuing tasks.

Your summary should be structured as follows:
Context: The context to continue the conversation with. If applicable based on the current task, this should include:
1. Previous Conversation: High level details about what was discussed throughout the entire conversation with the user. This should be written to allow someone to be able to follow the general overarching conversation flow.
2. Current Work: Describe in detail what was being worked on prior to this request to summarize the conversation. Pay special attention to the more recent messages in the conversation.
3. Key Technical Concepts: List all important technical concepts, technologies, coding conventions, and frameworks discussed, which might be relevant for continuing with this work.
4. Relevant Files and Code: If applicable, enumerate specific files and code sections examined, modified, or created for the task continuation. Pay special attention to the most recent messages and changes.
5. Problem Solving: Document problems solved thus far and any ongoing troubleshooting efforts.
6. Pending Tasks and Next Steps: Outline all pending tasks that you have explicitly been asked to work on, as well as list the next steps you will take for all outstanding work, if applicable. Include code snippets where they add clarity. For any next steps, include direct quotes from the most recent conversation showing exactly what task you were working on and where you left off. This should be verbatim to ensure there's no information loss in context between tasks.

Example summary structure:
1. Previous Conversation:
[Detailed description]
2. Current Work:
[Detailed description]
3. Key Technical Concepts:
- [Concept 1]
- [Concept 2]
- [...]
4. Relevant Files and Code:
- [File Name 1]
- [Summary of why this file is important]
- [Summary of the changes made to this file, if any]
- [Important Code Snippet]
- [File Name 2]
- [Important Code Snippet]
- [...]
5. Problem Solving:
[Detailed description]
6. Pending Tasks and Next Steps:
- [Task 1 details & next steps]
- [Task 2 details & next steps]
- [...]

Output only the summary of the conversation so far, without any additional commentary or explanation.`

it("should clear old v1 default condensing prompt from customSupportPrompts during initialization", async () => {
// Reset and create a new proxy with old v1 default prompt in customSupportPrompts
vi.clearAllMocks()
mockGlobalState.get.mockImplementation((key: string) => {
if (key === "customSupportPrompts") {
return { CONDENSE: OLD_V1_DEFAULT_CONDENSE_PROMPT }
}
return undefined
})

const proxyWithOldDefault = new ContextProxy(mockContext)
await proxyWithOldDefault.initialize()

// Should have cleared the old default by updating customSupportPrompts to undefined
// (since CONDENSE was the only key)
expect(mockGlobalState.update).toHaveBeenCalledWith("customSupportPrompts", undefined)
})

it("should preserve other custom prompts when clearing old v1 default", async () => {
// Reset and create a new proxy with old v1 default plus other custom prompts
vi.clearAllMocks()
mockGlobalState.get.mockImplementation((key: string) => {
if (key === "customSupportPrompts") {
return {
CONDENSE: OLD_V1_DEFAULT_CONDENSE_PROMPT,
EXPLAIN: "Custom explain prompt",
}
}
return undefined
})

const proxyWithOldDefault = new ContextProxy(mockContext)
await proxyWithOldDefault.initialize()

// Should have updated customSupportPrompts to keep EXPLAIN but remove CONDENSE
expect(mockGlobalState.update).toHaveBeenCalledWith("customSupportPrompts", {
EXPLAIN: "Custom explain prompt",
})
})

it("should not clear truly customized condensing prompts", async () => {
// Reset and create a new proxy with a truly customized condensing prompt
vi.clearAllMocks()
const customPrompt = "My custom condensing instructions"
mockGlobalState.get.mockImplementation((key: string) => {
if (key === "customSupportPrompts") {
return { CONDENSE: customPrompt }
}
return undefined
})

const proxyWithCustomPrompt = new ContextProxy(mockContext)
await proxyWithCustomPrompt.initialize()

// Should NOT have called update for customSupportPrompts (custom prompt should be preserved)
const updateCalls = mockGlobalState.update.mock.calls
const customSupportPromptsUpdateCalls = updateCalls.filter(
(call: any[]) => call[0] === "customSupportPrompts",
)
expect(customSupportPromptsUpdateCalls.length).toBe(0)
})

it("should not fail when customSupportPrompts is undefined", async () => {
// Reset and create a new proxy with no customSupportPrompts
vi.clearAllMocks()
mockGlobalState.get.mockReturnValue(undefined)

const proxyWithNoPrompts = new ContextProxy(mockContext)
await proxyWithNoPrompts.initialize()

// Should not have called update for customSupportPrompts
const updateCalls = mockGlobalState.update.mock.calls
const customSupportPromptsUpdateCalls = updateCalls.filter(
(call: any[]) => call[0] === "customSupportPrompts",
)
expect(customSupportPromptsUpdateCalls.length).toBe(0)
})
})
})
Loading
Loading