diff --git a/src/utils/attachments.ts b/src/utils/attachments.ts index 4085c42b9..7eb449474 100644 --- a/src/utils/attachments.ts +++ b/src/utils/attachments.ts @@ -819,6 +819,10 @@ export async function getAttachments( !options?.skipSkillDiscovery ? [ maybe('skill_discovery', async () => { + if (suppressNextDiscovery) { + suppressNextDiscovery = false + return [] + } const result = await skillSearchModules.prefetch.getTurnZeroSkillDiscovery( input, messages ?? [], @@ -2638,6 +2642,7 @@ const sentSkillNames = new Map>() export function resetSentSkillNames(): void { sentSkillNames.clear() suppressNext = false + suppressNextDiscovery = false } /** @@ -2661,6 +2666,18 @@ export function suppressNextSkillListing(): void { } let suppressNext = false +/** + * Suppress the next skill-discovery injection on resume. Same rationale as + * suppressNextSkillListing: skill_discovery attachments are not persisted to + * transcript for non-ant users, so the prior process's discovery result is + * already in the conversation history the model sees. Re-generating it would + * inject duplicate content and bust the prompt cache prefix. + */ +export function suppressNextSkillDiscovery(): void { + suppressNextDiscovery = true +} +let suppressNextDiscovery = false + // When skill-search is enabled and the filtered (bundled + MCP) listing exceeds // this count, fall back to bundled-only. Protects MCP-heavy users (100+ servers) // from truncation while keeping the turn-0 guarantee for typical setups. diff --git a/src/utils/conversationRecovery.ts b/src/utils/conversationRecovery.ts index 51c36520c..3379e9dd0 100644 --- a/src/utils/conversationRecovery.ts +++ b/src/utils/conversationRecovery.ts @@ -18,7 +18,7 @@ import type { NormalizedUserMessage, } from '../types/message.js' import { PERMISSION_MODES } from '../types/permissions.js' -import { suppressNextSkillListing } from './attachments.js' +import { suppressNextSkillDiscovery, suppressNextSkillListing } from './attachments.js' import { copyFileHistoryForResume, type FileHistorySnapshot, @@ -403,6 +403,16 @@ export function restoreSkillStateFromMessages(messages: Message[]): void { suppressNextSkillListing() } } + + // Unconditionally suppress skill_listing and skill_discovery on resume. + // Attachments are not persisted to transcript for non-ant users + // (isLoggableMessage filters them out), so the per-type checks above may + // never find them even though the prior process already injected the content + // into the conversation via blocks. Without this, every + // resume re-injects ~1K tokens of duplicate content and busts the Anthropic + // prompt cache prefix (which requires 100% byte-identical segments). + suppressNextSkillListing() + suppressNextSkillDiscovery() } /**