From 7d45e9906edb29546335e8af5fbb669b280c1cd9 Mon Sep 17 00:00:00 2001 From: Bhavya U Date: Sat, 23 May 2026 18:45:49 -0700 Subject: [PATCH] Don't emit empty on terminal steering turns (#318128) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Don't emit empty on steering turns Fixes #317763 Terminal steering requests sent from runInTerminalTool.ts don't forward an instructionContext, so chatServiceImpl short-circuits collectInstructions() to [] and the vscode.customizations.index variable is absent for those turns. The old freeze/drift consumer in agentPrompt.tsx treated that absence as 'all customizations removed' via 'effectiveCurrent = currentValue ?? ''', emitting an empty block whose own text says it 'supersedes the system prompt' — falsely telling the model that all skills/instructions/agents had been wiped mid-task and churning the cache tail. Gate drift emission on 'currentValue !== undefined' so an absent variable preserves the frozen system-prompt listing instead of falsely signalling removal. Also graduates the experimental 'github.copilot.chat.freezeCustomizationsIndex' setting (it was already default true on onExp). --- extensions/copilot/package.json | 10 ---- extensions/copilot/package.nls.json | 1 - .../prompts/node/agent/agentPrompt.tsx | 51 +++++++++---------- .../common/configurationService.ts | 3 -- 4 files changed, 24 insertions(+), 41 deletions(-) diff --git a/extensions/copilot/package.json b/extensions/copilot/package.json index 7603e9a2292..ce8e78a38f1 100644 --- a/extensions/copilot/package.json +++ b/extensions/copilot/package.json @@ -4294,16 +4294,6 @@ "advanced" ] }, - "github.copilot.chat.freezeCustomizationsIndex": { - "type": "boolean", - "default": true, - "tags": [ - "advanced", - "experimental", - "onExp" - ], - "description": "%github.copilot.config.freezeCustomizationsIndex%" - }, "github.copilot.chat.installExtensionSkill.enabled": { "type": "boolean", "default": false, diff --git a/extensions/copilot/package.nls.json b/extensions/copilot/package.nls.json index 8a7b535dd01..1fab6adc3af 100644 --- a/extensions/copilot/package.nls.json +++ b/extensions/copilot/package.nls.json @@ -340,7 +340,6 @@ "github.copilot.config.anthropic.promptCaching.extendedTtl": "Use the extended (1 hour) prompt cache TTL on tools and system blocks for the Anthropic Messages API. Applied to Claude Opus 4.5/4.6/4.7 and Sonnet 4.5/4.6 variants; other models keep the default 5 minute TTL even when this setting is enabled.\n\n**Note**: This is an experimental feature. Only the main agent conversation is eligible — inline chat, terminal chat, notebook chat, and subagent requests are excluded.", "github.copilot.config.anthropic.promptCaching.extendedTtlMessages": "Also extend the 1 hour prompt cache TTL to message-level breakpoints. Requires `chat.anthropic.promptCaching.extendedTtl` to be enabled; has no effect on its own.\n\n**Note**: This is an experimental feature.", "github.copilot.config.modelCapabilityOverrides": "Per-model capability overrides keyed by model id, intended for evaluating preview and tenanted models against an existing model's capability profile. For each model id, declare an aliased `family`. Setting `family` to a known production family (e.g. `\"claude-opus-4.7\"`) makes the model receive that family's full capability profile — Anthropic family detection, latest Opus prompt, multi-replace tools, tool search, context editing, extended cache TTL — without a code change.\n\n**Note**: This is an advanced setting for evaluation use; it is not intended for regular end-user configuration.", - "github.copilot.config.freezeCustomizationsIndex": "Freeze the bundled ``, ``, and `` listing in the system prompt at the first turn of a conversation and reuse it on every subsequent turn. Prevents per-turn churn (e.g. the active mode swapping which subagent entry is listed, or async experimentation flipping a skill in or out) from invalidating the prompt cache. When the listing changes mid-conversation, the updated set is appended to the latest user message so the model still sees instructions, skills, or agents that became available or were removed.\n\n**Note**: This is an experimental feature.", "github.copilot.config.useResponsesApi": "Use the Responses API instead of the Chat Completions API when supported. Enables reasoning and reasoning summaries.\n\n**Note**: This is an experimental feature that is not yet activated for all users.\n\n**Important**: For Custom Endpoint models, the API type is independent of this setting and is determined per-model via the `apiType` property, or inferred from the `url` path when omitted.", "github.copilot.config.responsesApiReasoningSummary": "Sets the reasoning summary style used for the Responses API. Requires `#github.copilot.chat.useResponsesApi#`.", "github.copilot.config.responsesApiContextManagement.enabled": "Enables context management for the Responses API. Requires `#github.copilot.chat.useResponsesApi#`.", diff --git a/extensions/copilot/src/extension/prompts/node/agent/agentPrompt.tsx b/extensions/copilot/src/extension/prompts/node/agent/agentPrompt.tsx index 00019fc3a2e..2175eac81e7 100644 --- a/extensions/copilot/src/extension/prompts/node/agent/agentPrompt.tsx +++ b/extensions/copilot/src/extension/prompts/node/agent/agentPrompt.tsx @@ -230,8 +230,7 @@ export class AgentPrompt extends PromptElement { } /** - * When the experimental `FreezeCustomizationsIndex` setting is enabled, - * snapshot the customizations-index variable on the first turn of the + * Snapshot the customizations-index variable on the first turn of the * conversation and reuse it for every subsequent turn. Stops per-turn * churn in the bundled ``/``/`` text (e.g. * the active mode swapping which subagent entry is listed in ``) @@ -239,27 +238,23 @@ export class AgentPrompt extends PromptElement { * * Returns: * - `frozen`: the value (and matching tool-reference offsets) to substitute - * in the system prompt. Always present when the setting is enabled and a - * variable is available. + * in the system prompt. Always present once a variable is available. * - `drift`: the live current-turn value (and offsets) when it differs from * `frozen`. Rendered in the latest user message so the model sees the - * up-to-date listing without busting the system prompt cache. Also - * emitted as an empty value when the live variable disappears, so the - * model gets a signal that previously listed entries are no longer - * available. + * up-to-date listing without busting the system prompt cache. Only + * surfaced when the variable was present this turn; an absent variable + * (e.g. on a request path without `instructionContext` such as terminal + * steering, or when collection ran but produced no listings) leaves the + * frozen system-prompt listing standing rather than emitting an empty + * update that would falsely signal removal. * - * Returns `undefined` overall if no override should apply (setting off, - * no first turn available, or no snapshot yet and the variable is absent - * on this turn). + * Returns `undefined` overall if no override should apply (no first turn + * available, or no snapshot yet and the variable is absent on this turn). */ private getOrFreezeCustomizationsIndex(): { frozen: { value: string; toolReferences: readonly ChatLanguageModelToolReference[] | undefined }; drift?: { value: string; toolReferences: readonly ChatLanguageModelToolReference[] | undefined }; } | undefined { - const enabled = this.configurationService.getExperimentBasedConfig(ConfigKey.Advanced.FreezeCustomizationsIndex, this.experimentationService); - if (!enabled) { - return undefined; - } const firstTurn = this.props.promptContext.conversation?.turns.at(0); if (!firstTurn) { return undefined; @@ -272,14 +267,17 @@ export class AgentPrompt extends PromptElement { const existing = firstTurn.getMetadata(CustomizationsIndexMetadata); if (existing && existing.cacheKey === currentCacheKey) { const frozen = { value: existing.value, toolReferences: existing.toolReferences }; - // Surface drift in either direction: a different live value, or the - // live variable disappearing entirely (treated as an empty listing). - // Without the second case the model is left looking at the stale - // frozen ``/``/`` block with no signal - // that entries have been removed. - const effectiveCurrent = currentValue ?? ''; - if (effectiveCurrent !== existing.value) { - return { frozen, drift: { value: effectiveCurrent, toolReferences: currentToolReferences } }; + // Only surface drift when the variable was present this turn AND + // differs from the snapshot. An absent variable can mean either + // "collection didn't run" (e.g. terminal steering requests omit + // `instructionContext`) or "collection ran but produced no listings". + // In either case we keep the frozen system-prompt listing standing + // rather than emit an empty `` block, which + // would falsely tell the model that all customizations were removed + // (its rendered text says it "supersedes" the system prompt) and + // would needlessly churn the cache tail. + if (currentValue !== undefined && currentValue !== existing.value) { + return { frozen, drift: { value: currentValue, toolReferences: currentToolReferences } }; } return { frozen }; } @@ -404,8 +402,7 @@ export interface AgentUserMessageProps extends BasePromptElementProps, AgentUser * store just the drift block and historical replays on later turns would * lose the actual user query, busting cross-turn cache continuity. * - * Only set when the experimental `FreezeCustomizationsIndex` setting is - * enabled and the current value differs from the snapshot captured on + * Only set when the current value differs from the snapshot captured on * the first turn. */ readonly customizationsIndexUpdate?: { value: string; toolReferences: readonly ChatLanguageModelToolReference[] | undefined }; @@ -638,8 +635,8 @@ interface CustomizationsIndexUpdateProps extends BasePromptElementProps { * the drift block and historical replays on later turns would lose the * actual user query. * - * Used only when `FreezeCustomizationsIndex` is on and the live index - * differs from the snapshot captured on the first turn. + * Used only when the live index differs from the snapshot captured on the + * first turn. */ class CustomizationsIndexUpdate extends PromptElement { constructor( diff --git a/extensions/copilot/src/platform/configuration/common/configurationService.ts b/extensions/copilot/src/platform/configuration/common/configurationService.ts index bc538e2cf2b..5d291df50d2 100644 --- a/extensions/copilot/src/platform/configuration/common/configurationService.ts +++ b/extensions/copilot/src/platform/configuration/common/configurationService.ts @@ -742,9 +742,6 @@ export namespace ConfigKey { /** Per-model capability overrides. Keys are model ids, values declare an aliased `family`. Lets evals route an unknown preview model id to a known production family (e.g. `"claude-opus-4.7"`) so the Anthropic prompt resolver, multi-replace tools, tool search, context editing, etc. all activate without a code change. */ export const ModelCapabilityOverrides = defineSetting>('chat.modelCapabilityOverrides', ConfigType.Simple, {}); - /** Freeze the customizations-index variable (the ``/``/`` block) at the first turn of a conversation and reuse it on subsequent turns. Prevents the system prompt cache from being invalidated by per-turn churn — e.g. the active mode swapping which subagent entry appears in ``, or async experimentation flipping a `when`-gated skill. */ - export const FreezeCustomizationsIndex = defineSetting('chat.freezeCustomizationsIndex', ConfigType.ExperimentBased, true); - export const InlineEditsXtabProviderModelConfiguration = (() => { const oldKey = 'chat.advanced.inlineEdits.xtabProvider.modelConfiguration'; const newKey = 'chat.inlineEdits.xtabProvider.modelConfiguration';