Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit b042866

Browse files
fix: auto-migrate v1 condensing prompt and handle invalid providers on import (#10931)
1 parent 4e67357 commit b042866

4 files changed

Lines changed: 819 additions & 16 deletions

File tree

src/core/config/ContextProxy.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ export class ContextProxy {
9696
// Migration: Move legacy customCondensingPrompt to customSupportPrompts
9797
await this.migrateLegacyCondensingPrompt()
9898

99+
// Migration: Clear old default condensing prompt so users get the improved v2 default
100+
await this.migrateOldDefaultCondensingPrompt()
101+
99102
this._isInitialized = true
100103
}
101104

@@ -138,6 +141,87 @@ export class ContextProxy {
138141
}
139142
}
140143

144+
/**
145+
* Clears the old v1 default condensing prompt from customSupportPrompts.CONDENSE if present.
146+
*
147+
* Before PR #10873 "Intelligent Context Condensation v2", the default condensing prompt was
148+
* a simpler 6-section format. Users who had this old default saved in their settings would
149+
* be stuck with it instead of getting the improved v2 default (which includes analysis tags,
150+
* error tracking, all user messages, and better task continuity).
151+
*
152+
* This migration uses fingerprinting to detect the old v1 default - checking for key
153+
* identifying phrases unique to v1 and absence of v2-specific features. This is more
154+
* lenient than exact matching and handles whitespace variations.
155+
*/
156+
private async migrateOldDefaultCondensingPrompt() {
157+
try {
158+
const currentSupportPrompts =
159+
this.originalContext.globalState.get<Record<string, string>>("customSupportPrompts") || {}
160+
161+
const savedCondensePrompt = currentSupportPrompts.CONDENSE
162+
163+
if (savedCondensePrompt && this.isOldV1DefaultCondensePrompt(savedCondensePrompt)) {
164+
logger.info(
165+
"Clearing old v1 default condensing prompt from customSupportPrompts.CONDENSE - user will now get the improved v2 default",
166+
)
167+
168+
// Remove the CONDENSE key from customSupportPrompts
169+
const { CONDENSE: _, ...remainingPrompts } = currentSupportPrompts
170+
const updatedPrompts = Object.keys(remainingPrompts).length > 0 ? remainingPrompts : undefined
171+
172+
await this.originalContext.globalState.update("customSupportPrompts", updatedPrompts)
173+
this.stateCache.customSupportPrompts = updatedPrompts
174+
}
175+
} catch (error) {
176+
logger.error(
177+
`Error during old default condensing prompt migration: ${error instanceof Error ? error.message : String(error)}`,
178+
)
179+
}
180+
}
181+
182+
/**
183+
* Detects if a prompt is the old v1 default condensing prompt using fingerprinting.
184+
* This is more lenient than exact matching - it checks for key identifying phrases
185+
* unique to v1 and absence of v2-specific features.
186+
*
187+
* V1 characteristics:
188+
* - Exactly 6 numbered sections (1-6)
189+
* - Contains specific section headers like "Previous Conversation", "Current Work", etc.
190+
* - Does NOT contain v2-specific features like "<analysis>", "SYSTEM OPERATION", etc.
191+
*/
192+
private isOldV1DefaultCondensePrompt(prompt: string): boolean {
193+
// Key phrases unique to the v1 default (must ALL be present)
194+
const v1RequiredPhrases = [
195+
"Your task is to create a detailed summary of the conversation so far",
196+
"1. Previous Conversation:",
197+
"2. Current Work:",
198+
"3. Key Technical Concepts:",
199+
"4. Relevant Files and Code:",
200+
"5. Problem Solving:",
201+
"6. Pending Tasks and Next Steps:",
202+
"Output only the summary of the conversation so far",
203+
]
204+
205+
// V2-specific features (if ANY are present, this is NOT v1 default)
206+
const v2Features = [
207+
"<analysis>",
208+
"SYSTEM OPERATION",
209+
"Errors and fixes",
210+
"All user messages",
211+
"7.", // v2 has more than 6 sections
212+
"8.",
213+
"9.",
214+
]
215+
216+
// Check that all v1 required phrases are present
217+
const hasAllV1Phrases = v1RequiredPhrases.every((phrase) => prompt.toLowerCase().includes(phrase.toLowerCase()))
218+
219+
// Check that no v2 features are present
220+
const hasNoV2Features = v2Features.every((feature) => !prompt.toLowerCase().includes(feature.toLowerCase()))
221+
222+
return hasAllV1Phrases && hasNoV2Features
223+
}
224+
141225
/**
142226
* Migrates invalid/removed apiProvider values by clearing them from storage.
143227
* This handles cases where a user had a provider selected that was later removed

src/core/config/__tests__/ContextProxy.spec.ts

Lines changed: 125 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,16 +70,18 @@ describe("ContextProxy", () => {
7070

7171
describe("constructor", () => {
7272
it("should initialize state cache with all global state keys", () => {
73-
// +2 for the migration checks:
73+
// +3 for the migration checks:
7474
// 1. openRouterImageGenerationSettings
7575
// 2. customCondensingPrompt
76-
expect(mockGlobalState.get).toHaveBeenCalledTimes(GLOBAL_STATE_KEYS.length + 2)
76+
// 3. customSupportPrompts (for migrateOldDefaultCondensingPrompt)
77+
expect(mockGlobalState.get).toHaveBeenCalledTimes(GLOBAL_STATE_KEYS.length + 3)
7778
for (const key of GLOBAL_STATE_KEYS) {
7879
expect(mockGlobalState.get).toHaveBeenCalledWith(key)
7980
}
8081
// Also check for migration calls
8182
expect(mockGlobalState.get).toHaveBeenCalledWith("openRouterImageGenerationSettings")
8283
expect(mockGlobalState.get).toHaveBeenCalledWith("customCondensingPrompt")
84+
expect(mockGlobalState.get).toHaveBeenCalledWith("customSupportPrompts")
8385
})
8486

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

105-
// Original context should be called once during updateGlobalState (+2 for migration checks)
106-
expect(mockGlobalState.get).toHaveBeenCalledTimes(GLOBAL_STATE_KEYS.length + 2) // From initialization + migration checks
107+
// Original context should be called once during updateGlobalState (+3 for migration checks)
108+
expect(mockGlobalState.get).toHaveBeenCalledTimes(GLOBAL_STATE_KEYS.length + 3) // From initialization + migration checks
107109
})
108110

109111
it("should handle default values correctly", async () => {
@@ -506,4 +508,123 @@ describe("ContextProxy", () => {
506508
expect(settings.apiProvider).toBeUndefined()
507509
})
508510
})
511+
512+
describe("old default condensing prompt migration", () => {
513+
// The old v1 default condensing prompt from before PR #10873
514+
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.
515+
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.
516+
517+
Your summary should be structured as follows:
518+
Context: The context to continue the conversation with. If applicable based on the current task, this should include:
519+
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.
520+
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.
521+
3. Key Technical Concepts: List all important technical concepts, technologies, coding conventions, and frameworks discussed, which might be relevant for continuing with this work.
522+
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.
523+
5. Problem Solving: Document problems solved thus far and any ongoing troubleshooting efforts.
524+
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.
525+
526+
Example summary structure:
527+
1. Previous Conversation:
528+
[Detailed description]
529+
2. Current Work:
530+
[Detailed description]
531+
3. Key Technical Concepts:
532+
- [Concept 1]
533+
- [Concept 2]
534+
- [...]
535+
4. Relevant Files and Code:
536+
- [File Name 1]
537+
- [Summary of why this file is important]
538+
- [Summary of the changes made to this file, if any]
539+
- [Important Code Snippet]
540+
- [File Name 2]
541+
- [Important Code Snippet]
542+
- [...]
543+
5. Problem Solving:
544+
[Detailed description]
545+
6. Pending Tasks and Next Steps:
546+
- [Task 1 details & next steps]
547+
- [Task 2 details & next steps]
548+
- [...]
549+
550+
Output only the summary of the conversation so far, without any additional commentary or explanation.`
551+
552+
it("should clear old v1 default condensing prompt from customSupportPrompts during initialization", async () => {
553+
// Reset and create a new proxy with old v1 default prompt in customSupportPrompts
554+
vi.clearAllMocks()
555+
mockGlobalState.get.mockImplementation((key: string) => {
556+
if (key === "customSupportPrompts") {
557+
return { CONDENSE: OLD_V1_DEFAULT_CONDENSE_PROMPT }
558+
}
559+
return undefined
560+
})
561+
562+
const proxyWithOldDefault = new ContextProxy(mockContext)
563+
await proxyWithOldDefault.initialize()
564+
565+
// Should have cleared the old default by updating customSupportPrompts to undefined
566+
// (since CONDENSE was the only key)
567+
expect(mockGlobalState.update).toHaveBeenCalledWith("customSupportPrompts", undefined)
568+
})
569+
570+
it("should preserve other custom prompts when clearing old v1 default", async () => {
571+
// Reset and create a new proxy with old v1 default plus other custom prompts
572+
vi.clearAllMocks()
573+
mockGlobalState.get.mockImplementation((key: string) => {
574+
if (key === "customSupportPrompts") {
575+
return {
576+
CONDENSE: OLD_V1_DEFAULT_CONDENSE_PROMPT,
577+
EXPLAIN: "Custom explain prompt",
578+
}
579+
}
580+
return undefined
581+
})
582+
583+
const proxyWithOldDefault = new ContextProxy(mockContext)
584+
await proxyWithOldDefault.initialize()
585+
586+
// Should have updated customSupportPrompts to keep EXPLAIN but remove CONDENSE
587+
expect(mockGlobalState.update).toHaveBeenCalledWith("customSupportPrompts", {
588+
EXPLAIN: "Custom explain prompt",
589+
})
590+
})
591+
592+
it("should not clear truly customized condensing prompts", async () => {
593+
// Reset and create a new proxy with a truly customized condensing prompt
594+
vi.clearAllMocks()
595+
const customPrompt = "My custom condensing instructions"
596+
mockGlobalState.get.mockImplementation((key: string) => {
597+
if (key === "customSupportPrompts") {
598+
return { CONDENSE: customPrompt }
599+
}
600+
return undefined
601+
})
602+
603+
const proxyWithCustomPrompt = new ContextProxy(mockContext)
604+
await proxyWithCustomPrompt.initialize()
605+
606+
// Should NOT have called update for customSupportPrompts (custom prompt should be preserved)
607+
const updateCalls = mockGlobalState.update.mock.calls
608+
const customSupportPromptsUpdateCalls = updateCalls.filter(
609+
(call: any[]) => call[0] === "customSupportPrompts",
610+
)
611+
expect(customSupportPromptsUpdateCalls.length).toBe(0)
612+
})
613+
614+
it("should not fail when customSupportPrompts is undefined", async () => {
615+
// Reset and create a new proxy with no customSupportPrompts
616+
vi.clearAllMocks()
617+
mockGlobalState.get.mockReturnValue(undefined)
618+
619+
const proxyWithNoPrompts = new ContextProxy(mockContext)
620+
await proxyWithNoPrompts.initialize()
621+
622+
// Should not have called update for customSupportPrompts
623+
const updateCalls = mockGlobalState.update.mock.calls
624+
const customSupportPromptsUpdateCalls = updateCalls.filter(
625+
(call: any[]) => call[0] === "customSupportPrompts",
626+
)
627+
expect(customSupportPromptsUpdateCalls.length).toBe(0)
628+
})
629+
})
509630
})

0 commit comments

Comments
 (0)