diff --git a/CHANGELOG.md b/CHANGELOG.md index 6181d59a7b9..2d1bd8165e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -99,7 +99,7 @@ Docs: https://docs.openclaw.ai - Models/fallback: resolve bare fallback model provider ids before model switching, so configured fallback chains keep working when a fallback is named without an explicit provider prefix. Thanks @steipete. - Voice-call/Telnyx: preserve inbound/outbound callback metadata and read transcription text from Telnyx's current `transcription_data` payload. Thanks @steipete. - Providers/DeepSeek: wire V4 thinking controls and OpenAI-compatible replay policy so follow-up turns preserve DeepSeek `reasoning_content`, while the None/off thinking path strips replayed reasoning fields. Fixes #70931. Thanks @lsdsjy. -- Providers/GitHub Copilot: align Copilot request headers across Anthropic and Responses transports, including tool-result and image follow-up turns, without enabling unverified Responses continuation. Thanks @steipete. +- Providers/GitHub Copilot: align Copilot request headers across Anthropic, Responses, and built-in compaction summarization paths, including tool-result and image follow-up turns, without enabling unverified Responses continuation. Thanks @steipete. - Codex harness: send verbose tool progress to chat channels for native app-server runs, matching the Pi harness `/verbose on` and `/verbose full` behavior. (#70966) Thanks @jalehman. - Codex models: fetch paginated Codex app-server model catalogs, mark truncated `/codex models` output, and keep ChatGPT OAuth defaults on the `openai-codex/gpt-5.5` route instead of the OpenAI API-key route. Thanks @steipete. - Codex status: report Codex CLI OAuth as `oauth (codex-cli)` for native `codex/*` sessions instead of showing unknown auth. Fixes #70688. Thanks @jb510. diff --git a/docs/providers/github-copilot.md b/docs/providers/github-copilot.md index 33f95c3b57b..67c46df4ff9 100644 --- a/docs/providers/github-copilot.md +++ b/docs/providers/github-copilot.md @@ -92,9 +92,9 @@ openclaw models auth login --provider github-copilot --method device --set-defau OpenClaw sends Copilot IDE-style request headers on Copilot transports, - including tool-result and image follow-up turns. It does not enable - provider-level Responses continuation for Copilot unless that behavior has - been verified against Copilot's API. + including built-in compaction, tool-result, and image follow-up turns. It + does not enable provider-level Responses continuation for Copilot unless + that behavior has been verified against Copilot's API. diff --git a/src/agents/pi-hooks/compaction-safeguard.test.ts b/src/agents/pi-hooks/compaction-safeguard.test.ts index b1f4af27d8a..1f65bd3ca07 100644 --- a/src/agents/pi-hooks/compaction-safeguard.test.ts +++ b/src/agents/pi-hooks/compaction-safeguard.test.ts @@ -1286,6 +1286,52 @@ describe("compaction-safeguard recent-turn preservation", () => { expect(droppedCall?.customInstructions).toContain("Keep security caveats."); }); + it("adds Copilot IDE headers to built-in compaction summarization", async () => { + mockSummarizeInStages.mockReset(); + mockSummarizeInStages.mockResolvedValue("mock summary"); + + const sessionManager = stubSessionManager(); + const model = createAnthropicModelFixture({ + id: "gpt-5.4", + name: "gpt-5.4", + provider: "github-copilot", + api: "openai-responses" as const, + baseUrl: "https://api.githubcopilot.com", + }); + setCompactionSafeguardRuntime(sessionManager, { model, recentTurnsPreserve: 0 }); + + const getApiKeyAndHeadersMock = vi.fn().mockResolvedValue({ + ok: true, + apiKey: "github-token", + headers: { "X-Test": "1" }, + }); + const mockContext = createCompactionContext({ + sessionManager, + getApiKeyAndHeadersMock, + }); + const compactionHandler = createCompactionHandler(); + const event = createCompactionEvent({ + messageText: "summarize me", + tokensBefore: 1000, + }); + (event.preparation as { settings?: { reserveTokens: number } }).settings = { + reserveTokens: 4000, + }; + + const result = (await compactionHandler(event, mockContext)) as { cancel?: boolean }; + + expect(result.cancel).not.toBe(true); + const summaryCall = mockSummarizeInStages.mock.calls.at(-1)?.[0]; + expect(summaryCall?.headers).toMatchObject({ + "Copilot-Integration-Id": "vscode-chat", + "Editor-Plugin-Version": "copilot-chat/0.35.0", + "Openai-Organization": "github-copilot", + "User-Agent": "GitHubCopilotChat/0.26.7", + "X-Test": "1", + "x-initiator": "user", + }); + }); + it("does not retry summaries unless quality guard is explicitly enabled", async () => { mockSummarizeInStages.mockReset(); mockSummarizeInStages.mockResolvedValue("summary missing headings"); diff --git a/src/agents/pi-hooks/compaction-safeguard.ts b/src/agents/pi-hooks/compaction-safeguard.ts index 2e3287ecea8..bbeac04f8e8 100644 --- a/src/agents/pi-hooks/compaction-safeguard.ts +++ b/src/agents/pi-hooks/compaction-safeguard.ts @@ -28,6 +28,7 @@ import { summarizeInStages, } from "../compaction.js"; import { collectTextContentBlocks } from "../content-blocks.js"; +import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "../copilot-dynamic-headers.js"; import { isTimeoutError } from "../failover-error.js"; import { repairToolUseResultPairing } from "../session-transcript-repair.js"; import { extractToolCallsFromAssistant, extractToolResultId } from "../tool-call-id.js"; @@ -236,6 +237,26 @@ async function resolveModelAuth( return { ok: true, apiKey: requestAuth.apiKey, headers: requestAuth.headers }; } +function buildCompactionSummaryHeaders(params: { + model: NonNullable; + messages: AgentMessage[]; + headers?: Record; +}): Record | undefined { + if (params.model.provider !== "github-copilot") { + return params.headers; + } + const messages = params.messages as unknown as Parameters< + typeof buildCopilotDynamicHeaders + >[0]["messages"]; + return { + ...buildCopilotDynamicHeaders({ + messages, + hasImages: hasCopilotVisionInput(messages), + }), + ...params.headers, + }; +} + function clampNonNegativeInt(value: unknown, fallback: number): number { const normalized = typeof value === "number" && Number.isFinite(value) ? value : fallback; return Math.max(0, Math.floor(normalized)); @@ -880,12 +901,17 @@ export default function compactionSafeguardExtension(api: ExtensionAPI): void { return { cancel: true }; } const apiKey = authResult.apiKey ?? ""; - const headers = authResult.headers; + const authHeaders = authResult.headers; try { const modelContextWindow = resolveContextWindowTokens(model); const contextWindowTokens = runtime?.contextWindowTokens ?? modelContextWindow; let messagesToSummarize = preparation.messagesToSummarize; + const headers = buildCompactionSummaryHeaders({ + model, + messages: messagesToSummarize, + headers: authHeaders, + }); const qualityGuardEnabled = runtime?.qualityGuardEnabled ?? false; const qualityGuardMaxRetries = resolveQualityGuardMaxRetries(runtime?.qualityGuardMaxRetries);