From bcb42f1b71c08503d59f9e00da1ecca7890f452f Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Thu, 22 Jan 2026 23:09:39 -0500 Subject: [PATCH 1/5] feat: default to empty protectedTools for deduplication and purgeErrors strategies Deduplication and purge-errors are hygiene strategies that are always beneficial for context management - they don't lose information, they just clean up redundancy and stale error inputs. By defaulting protectedTools to empty arrays, these strategies now run on all tools out of the box. Users can still configure protectedTools if they want to protect specific tools from these strategies. --- lib/config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/config.ts b/lib/config.ts index beabaa3f..f24e9680 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -450,7 +450,7 @@ const defaultConfig: PluginConfig = { strategies: { deduplication: { enabled: true, - protectedTools: [...DEFAULT_PROTECTED_TOOLS], + protectedTools: [], }, supersedeWrites: { enabled: false, @@ -458,7 +458,7 @@ const defaultConfig: PluginConfig = { purgeErrors: { enabled: true, turns: 4, - protectedTools: [...DEFAULT_PROTECTED_TOOLS], + protectedTools: [], }, }, } From cf779a745b56857e49e5d890b1436269577b9525 Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Thu, 22 Jan 2026 23:09:52 -0500 Subject: [PATCH 2/5] chore: remove redundant JSDoc comments --- lib/ui/utils.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lib/ui/utils.ts b/lib/ui/utils.ts index b1e00ed9..9134a5cf 100644 --- a/lib/ui/utils.ts +++ b/lib/ui/utils.ts @@ -60,9 +60,6 @@ function shortenSinglePath(path: string, workingDirectory?: string): string { return path } -/** - * Formats a list of pruned items in the style: "→ tool: parameter" - */ export function formatPrunedItemsList( pruneToolIds: string[], toolMetadata: Map, @@ -95,9 +92,6 @@ export function formatPrunedItemsList( return lines } -/** - * Formats a PruningResult into a human-readable string for the prune tool output. - */ export function formatPruningResultForTool( prunedIds: string[], toolMetadata: Map, From 2db74746b980c19b04100d216a2be86d8449e0b1 Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Wed, 28 Jan 2026 15:41:04 -0500 Subject: [PATCH 3/5] docs: add Ko-fi sponsorship link --- .github/FUNDING.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..e4ff0536 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +ko_fi: dansmolsky From 31ecb6fb3505e67cefc45594f36af948aabf932c Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Thu, 29 Jan 2026 19:35:19 -0500 Subject: [PATCH 4/5] refactor: hybrid injection strategy for DeepSeek/Kimi models Apply injection system from beta to dev branch: - Change createSyntheticAssistantMessage to use text parts (was tool parts) - Add isDeepSeekOrKimi detection function - Add createSyntheticToolPart for DeepSeek/Kimi special handling - Update insertPruneToolContext with hybrid injection strategy: * DeepSeek/Kimi: append tool part to last assistant message * Other models: create new assistant message with text part - Replace findLast with reverse for loop (ES2022 compatibility) --- lib/messages/inject.ts | 29 ++++++++++---- lib/messages/utils.ts | 89 ++++++++++++++++++++++++------------------ 2 files changed, 72 insertions(+), 46 deletions(-) diff --git a/lib/messages/inject.ts b/lib/messages/inject.ts index 5920566a..e189513d 100644 --- a/lib/messages/inject.ts +++ b/lib/messages/inject.ts @@ -8,6 +8,8 @@ import { buildToolIdList, createSyntheticAssistantMessage, createSyntheticUserMessage, + createSyntheticToolPart, + isDeepSeekOrKimi, isIgnoredUserMessage, } from "./utils" import { getFilePathFromParameters, isProtectedFilePath } from "../protected-file-patterns" @@ -142,15 +144,28 @@ export const insertPruneToolContext = ( const userInfo = lastUserMessage.info as UserMessage const variant = state.variant ?? userInfo.variant - const lastMessage = messages[messages.length - 1] - const isLastMessageUser = - lastMessage?.info?.role === "user" && !isIgnoredUserMessage(lastMessage) + let lastNonIgnoredMessage: WithParts | undefined + for (let i = messages.length - 1; i >= 0; i--) { + const msg = messages[i] + if (!(msg.info.role === "user" && isIgnoredUserMessage(msg))) { + lastNonIgnoredMessage = msg + break + } + } - if (isLastMessageUser) { + if (!lastNonIgnoredMessage || lastNonIgnoredMessage.info.role === "user") { messages.push(createSyntheticUserMessage(lastUserMessage, prunableToolsContent, variant)) } else { - messages.push( - createSyntheticAssistantMessage(lastUserMessage, prunableToolsContent, variant), - ) + const providerID = userInfo.model?.providerID || "" + const modelID = userInfo.model?.modelID || "" + + if (isDeepSeekOrKimi(providerID, modelID)) { + const toolPart = createSyntheticToolPart(lastNonIgnoredMessage, prunableToolsContent) + lastNonIgnoredMessage.parts.push(toolPart) + } else { + messages.push( + createSyntheticAssistantMessage(lastUserMessage, prunableToolsContent, variant) + ) + } } } diff --git a/lib/messages/utils.ts b/lib/messages/utils.ts index 48ae0e6c..406b6f42 100644 --- a/lib/messages/utils.ts +++ b/lib/messages/utils.ts @@ -7,9 +7,15 @@ const SYNTHETIC_MESSAGE_ID = "msg_01234567890123456789012345" const SYNTHETIC_PART_ID = "prt_01234567890123456789012345" const SYNTHETIC_CALL_ID = "call_01234567890123456789012345" -const isGeminiModel = (modelID: string): boolean => { +export const isDeepSeekOrKimi = (providerID: string, modelID: string): boolean => { + const lowerProviderID = providerID.toLowerCase() const lowerModelID = modelID.toLowerCase() - return lowerModelID.includes("gemini") + return ( + lowerProviderID.includes("deepseek") || + lowerProviderID.includes("kimi") || + lowerModelID.includes("deepseek") || + lowerModelID.includes("kimi") + ) } export const createSyntheticUserMessage = ( @@ -50,54 +56,59 @@ export const createSyntheticAssistantMessage = ( const userInfo = baseMessage.info as UserMessage const now = Date.now() - const baseInfo = { - id: SYNTHETIC_MESSAGE_ID, - sessionID: userInfo.sessionID, - role: "assistant" as const, - agent: userInfo.agent || "code", - parentID: userInfo.id, - modelID: userInfo.model.modelID, - providerID: userInfo.model.providerID, - mode: "default", - path: { - cwd: "/", - root: "/", - }, - time: { created: now, completed: now }, - cost: 0, - tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, - ...(variant !== undefined && { variant }), - } - - // For Gemini models, add thoughtSignature bypass to avoid validation errors - const toolPartMetadata = isGeminiModel(userInfo.model.modelID) - ? { google: { thoughtSignature: "skip_thought_signature_validator" } } - : undefined - return { - info: baseInfo, + info: { + id: SYNTHETIC_MESSAGE_ID, + sessionID: userInfo.sessionID, + role: "assistant" as const, + agent: userInfo.agent || "code", + parentID: userInfo.id, + modelID: userInfo.model.modelID, + providerID: userInfo.model.providerID, + mode: "default", + path: { + cwd: "/", + root: "/", + }, + time: { created: now, completed: now }, + cost: 0, + tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + ...(variant !== undefined && { variant }), + }, parts: [ { id: SYNTHETIC_PART_ID, sessionID: userInfo.sessionID, messageID: SYNTHETIC_MESSAGE_ID, - type: "tool", - callID: SYNTHETIC_CALL_ID, - tool: "context_info", - state: { - status: "completed", - input: {}, - output: content, - title: "Context Info", - metadata: {}, - time: { start: now, end: now }, - }, - ...(toolPartMetadata && { metadata: toolPartMetadata }), + type: "text", + text: content, }, ], } } +export const createSyntheticToolPart = (baseMessage: WithParts, content: string) => { + const userInfo = baseMessage.info as UserMessage + const now = Date.now() + + return { + id: SYNTHETIC_PART_ID, + sessionID: userInfo.sessionID, + messageID: baseMessage.info.id, + type: "tool" as const, + callID: SYNTHETIC_CALL_ID, + tool: "context_info", + state: { + status: "completed" as const, + input: {}, + output: content, + title: "Context Info", + metadata: {}, + time: { start: now, end: now }, + }, + } +} + /** * Extracts a human-readable key from tool metadata for display purposes. */ From 005730a7c470a57afac5db620b38ac08f1d120bc Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Thu, 29 Jan 2026 19:45:46 -0500 Subject: [PATCH 5/5] format --- lib/messages/inject.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/messages/inject.ts b/lib/messages/inject.ts index e189513d..491ecd6c 100644 --- a/lib/messages/inject.ts +++ b/lib/messages/inject.ts @@ -164,7 +164,7 @@ export const insertPruneToolContext = ( lastNonIgnoredMessage.parts.push(toolPart) } else { messages.push( - createSyntheticAssistantMessage(lastUserMessage, prunableToolsContent, variant) + createSyntheticAssistantMessage(lastUserMessage, prunableToolsContent, variant), ) } }