From 0602e6fc665596ef99ee01d51c0a1248a37fa823 Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 4 Aug 2026 11:04:54 +0200 Subject: [PATCH 1/4] fix(providers): un-obfuscate GitHub Copilot streamed tool-call arguments --- src/server/responses/core.ts | 9 ++++- src/server/sse-payload-rewrite.ts | 53 +++++++++++++++++++++++++++ tests/sse-payload-rewrite.test.ts | 61 +++++++++++++++++++++++++++++++ 3 files changed, 122 insertions(+), 1 deletion(-) diff --git a/src/server/responses/core.ts b/src/server/responses/core.ts index e714b3d813..cdc713f49b 100644 --- a/src/server/responses/core.ts +++ b/src/server/responses/core.ts @@ -155,7 +155,11 @@ import { imageGenToolCallAliases, restoreImageGenCallsInJson, } from "../responses-image-gen-repair"; -import { composeSsePayloadRewrites, relaySseWithPayloadRewrite } from "../sse-payload-rewrite"; +import { + composeSsePayloadRewrites, + createGithubCopilotObfuscationRewrite, + relaySseWithPayloadRewrite, +} from "../sse-payload-rewrite"; import type { EffectiveSubagentRoster, SpawnAgentSurface } from "../../codex/catalog"; import { buildToolBridgeMaps, collabSurface, injectDeveloperMessage, multiAgentGuidanceText } from "./collaboration"; @@ -1765,6 +1769,9 @@ async function handleResponsesInner( hasResponsesItemIdRepair(repairConfig) ? createResponsesItemIdPayloadRewrite(repairConfig!, translatorBudget) : undefined, + route.providerName === "github-copilot" + ? createGithubCopilotObfuscationRewrite() + : undefined, ].filter((rewrite): rewrite is NonNullable => rewrite !== undefined); // #864: win32 rewrite traffic must never enter the tee()+JS-pull chain // (Bun#32111 JS-sink segfault — text frames pass, the terminal block is diff --git a/src/server/sse-payload-rewrite.ts b/src/server/sse-payload-rewrite.ts index 712853cee1..5d80775071 100644 --- a/src/server/sse-payload-rewrite.ts +++ b/src/server/sse-payload-rewrite.ts @@ -9,6 +9,10 @@ import type { TranslatorBudget } from "../lib/translator-budget"; export type SsePayloadRewrite = (payload: string) => string; +function isPayloadRecord(v: unknown): v is Record { + return typeof v === "object" && v !== null && !Array.isArray(v); +} + /** Split one complete SSE event block while retaining its original blank-line delimiter. */ export function nextSseBlock(buffer: string): { block: string; delimiter: string; rest: string } | null { const match = buffer.match(/\r?\n\r?\n/); @@ -61,6 +65,55 @@ export function composeSsePayloadRewrites(...rewrites: SsePayloadRewrite[]): Sse }; } +/** + * GitHub Copilot's `/responses` wire obfuscates streamed function-call argument + * deltas (ciphertext `delta` plus an `obfuscation` field) for the `vscode-chat` + * integration. Responses clients (codex-rs) cannot de-obfuscate them, so an + * agentic turn stalls on the first tool call. The terminal + * `response.function_call_arguments.done` event carries the plaintext + * `arguments`, so this rewrite drops the ciphertext deltas and re-emits the + * full plaintext arguments as a single delta immediately before `done`, keeping + * the stream protocol-conformant (deltas still concatenate to the final + * arguments). The `obfuscation` metadata is stripped from every event. + */ +export function createGithubCopilotObfuscationRewrite(): SsePayloadRewrite { + const obfuscatedCallIds = new Set(); + return (payload: string): string => { + let parsed: unknown; + try { + parsed = JSON.parse(payload); + } catch { + return payload; + } + if (!isPayloadRecord(parsed)) return payload; + const hadObfuscation = typeof parsed.obfuscation === "string"; + if (hadObfuscation) delete parsed.obfuscation; + const type = parsed.type; + if (type === "response.function_call_arguments.delta" && hadObfuscation) { + if (typeof parsed.item_id === "string") obfuscatedCallIds.add(parsed.item_id); + // Ciphertext chunk: emit an empty delta so the client sees a valid event + // without any unusable bytes; the plaintext arrives with `.done`. + parsed.delta = ""; + return JSON.stringify(parsed); + } + if (type === "response.function_call_arguments.done" && typeof parsed.item_id === "string") { + if (obfuscatedCallIds.delete(parsed.item_id)) { + const deltaEvent = { + type: "response.function_call_arguments.delta", + item_id: parsed.item_id, + output_index: parsed.output_index, + sequence_number: parsed.sequence_number, + delta: typeof parsed.arguments === "string" ? parsed.arguments : "", + }; + // The relay writes `data: ` then the block delimiter, so this + // payload becomes two consecutive valid SSE events (delta, then done). + return `${JSON.stringify(deltaEvent)}\n\ndata: ${JSON.stringify(parsed)}`; + } + } + return hadObfuscation ? JSON.stringify(parsed) : payload; + }; +} + /** * Relay an SSE body through a single JS pull wrapper, rewriting each event's data payload in place. * Non-data fields and framing are preserved; invalid JSON payloads are left to the rewrite callback. diff --git a/tests/sse-payload-rewrite.test.ts b/tests/sse-payload-rewrite.test.ts index c6c72f615b..fa30ae5743 100644 --- a/tests/sse-payload-rewrite.test.ts +++ b/tests/sse-payload-rewrite.test.ts @@ -6,6 +6,7 @@ import { createImageGenCallRestoreRewrite } from "../src/server/responses-image- import { createResponsesItemIdPayloadRewrite } from "../src/server/responses-item-id-repair"; import { composeSsePayloadRewrites, + createGithubCopilotObfuscationRewrite, relaySseWithPayloadRewrite, } from "../src/server/sse-payload-rewrite"; import { createTestTranslatorBudget } from "./helpers/translator-budget"; @@ -39,6 +40,66 @@ async function readAll(stream: ReadableStream): Promise { } describe("SSE payload rewrite composition", () => { + test("GitHub Copilot obfuscation rewrite turns ciphertext deltas into plaintext", async () => { + const rewrite = createGithubCopilotObfuscationRewrite(); + const itemId = "cipher-item-id"; + const upstream = [ + 'data: {"type":"response.function_call_arguments.delta","item_id":"' + itemId + '","output_index":0,"sequence_number":3,"delta":"ciphertext-chunk-1","obfuscation":"abc123"}\n\n', + 'data: {"type":"response.function_call_arguments.delta","item_id":"' + itemId + '","output_index":0,"sequence_number":4,"delta":"ciphertext-chunk-2","obfuscation":"abc123"}\n\n', + 'data: {"type":"response.function_call_arguments.done","item_id":"' + itemId + '","output_index":0,"sequence_number":5,"arguments":"{\\"a\\":2,\\"b\\":2}"}\n\n', + ].join(""); + + const rewritten = await readAll( + relaySseWithPayloadRewrite( + streamFromText(upstream), + rewrite, + createTestTranslatorBudget(), + ), + ); + + const events = rewritten + .split("\n\n") + .map((block) => block.replace(/^data: /, "")) + .filter((block) => block.length > 0) + .map((block) => JSON.parse(block)); + + expect(events.map((event) => event.type)).toEqual([ + "response.function_call_arguments.delta", + "response.function_call_arguments.delta", + "response.function_call_arguments.delta", + "response.function_call_arguments.done", + ]); + expect(events[0].delta).toBe(""); + expect(events[1].delta).toBe(""); + expect(events[2].delta).toBe('{"a":2,"b":2}'); + expect(events[3].arguments).toBe('{"a":2,"b":2}'); + expect(JSON.stringify(rewritten)).not.toContain("obfuscation"); + expect(JSON.stringify(rewritten)).not.toContain("ciphertext-chunk"); + }); + + test("GitHub Copilot obfuscation rewrite strips obfuscation from text deltas", async () => { + const rewrite = createGithubCopilotObfuscationRewrite(); + const upstream = + 'data: {"type":"response.output_text.delta","item_id":"msg_0","output_index":0,"sequence_number":1,"delta":"bonjour","obfuscation":"abc123"}\n\n'; + + const rewritten = await readAll( + relaySseWithPayloadRewrite( + streamFromText(upstream), + rewrite, + createTestTranslatorBudget(), + ), + ); + + expect(rewritten).toContain('"delta":"bonjour"'); + expect(rewritten).not.toContain("obfuscation"); + }); + + test("GitHub Copilot obfuscation rewrite passes clean events through unchanged", () => { + const rewrite = createGithubCopilotObfuscationRewrite(); + const payload = '{"type":"response.output_text.delta","delta":"hello"}'; + expect(rewrite(payload)).toBe(payload); + }); + test("applies image-gen restore and item-id repair in one relay pass", async () => { const upstream = [ 'event: response.output_item.added\ndata: {"type":"response.output_item.added","output_index":0,"item":{"type":"message","id":"msg_0","role":"assistant"}}\n\n', From 6af96da51a22e92dd96c6a4f8dac6ac1b5ea18ec Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 4 Aug 2026 11:43:14 +0200 Subject: [PATCH 2/4] fix(providers): track obfuscated Copilot calls by output_index GitHub re-encrypts item_id per SSE event, so delta and done never share an id; tracking by output_index keeps the plaintext delta injection working. --- src/server/sse-payload-rewrite.ts | 10 ++++++---- tests/sse-payload-rewrite.test.ts | 11 +++++++---- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/server/sse-payload-rewrite.ts b/src/server/sse-payload-rewrite.ts index 5d80775071..bbe05fa30f 100644 --- a/src/server/sse-payload-rewrite.ts +++ b/src/server/sse-payload-rewrite.ts @@ -77,7 +77,9 @@ export function composeSsePayloadRewrites(...rewrites: SsePayloadRewrite[]): Sse * arguments). The `obfuscation` metadata is stripped from every event. */ export function createGithubCopilotObfuscationRewrite(): SsePayloadRewrite { - const obfuscatedCallIds = new Set(); + // GitHub re-encrypts `item_id` per event (delta and done carry different ids for the + // same logical call), so calls are tracked by the stable `output_index` instead. + const obfuscatedCalls = new Set(); return (payload: string): string => { let parsed: unknown; try { @@ -90,14 +92,14 @@ export function createGithubCopilotObfuscationRewrite(): SsePayloadRewrite { if (hadObfuscation) delete parsed.obfuscation; const type = parsed.type; if (type === "response.function_call_arguments.delta" && hadObfuscation) { - if (typeof parsed.item_id === "string") obfuscatedCallIds.add(parsed.item_id); + if (typeof parsed.output_index === "number") obfuscatedCalls.add(parsed.output_index); // Ciphertext chunk: emit an empty delta so the client sees a valid event // without any unusable bytes; the plaintext arrives with `.done`. parsed.delta = ""; return JSON.stringify(parsed); } - if (type === "response.function_call_arguments.done" && typeof parsed.item_id === "string") { - if (obfuscatedCallIds.delete(parsed.item_id)) { + if (type === "response.function_call_arguments.done" && typeof parsed.output_index === "number") { + if (obfuscatedCalls.delete(parsed.output_index)) { const deltaEvent = { type: "response.function_call_arguments.delta", item_id: parsed.item_id, diff --git a/tests/sse-payload-rewrite.test.ts b/tests/sse-payload-rewrite.test.ts index fa30ae5743..a05ad733cc 100644 --- a/tests/sse-payload-rewrite.test.ts +++ b/tests/sse-payload-rewrite.test.ts @@ -42,11 +42,13 @@ async function readAll(stream: ReadableStream): Promise { describe("SSE payload rewrite composition", () => { test("GitHub Copilot obfuscation rewrite turns ciphertext deltas into plaintext", async () => { const rewrite = createGithubCopilotObfuscationRewrite(); - const itemId = "cipher-item-id"; + // GitHub re-encrypts item ids per event; deltas and done never share an id. + const deltaItemId = "cipher-delta-id"; + const doneItemId = "cipher-done-id"; const upstream = [ - 'data: {"type":"response.function_call_arguments.delta","item_id":"' + itemId + '","output_index":0,"sequence_number":3,"delta":"ciphertext-chunk-1","obfuscation":"abc123"}\n\n', - 'data: {"type":"response.function_call_arguments.delta","item_id":"' + itemId + '","output_index":0,"sequence_number":4,"delta":"ciphertext-chunk-2","obfuscation":"abc123"}\n\n', - 'data: {"type":"response.function_call_arguments.done","item_id":"' + itemId + '","output_index":0,"sequence_number":5,"arguments":"{\\"a\\":2,\\"b\\":2}"}\n\n', + 'data: {"type":"response.function_call_arguments.delta","item_id":"' + deltaItemId + '","output_index":0,"sequence_number":3,"delta":"ciphertext-chunk-1","obfuscation":"abc123"}\n\n', + 'data: {"type":"response.function_call_arguments.delta","item_id":"' + deltaItemId + '","output_index":0,"sequence_number":4,"delta":"ciphertext-chunk-2","obfuscation":"abc123"}\n\n', + 'data: {"type":"response.function_call_arguments.done","item_id":"' + doneItemId + '","output_index":0,"sequence_number":5,"arguments":"{\\"a\\":2,\\"b\\":2}"}\n\n', ].join(""); const rewritten = await readAll( @@ -72,6 +74,7 @@ describe("SSE payload rewrite composition", () => { expect(events[0].delta).toBe(""); expect(events[1].delta).toBe(""); expect(events[2].delta).toBe('{"a":2,"b":2}'); + expect(events[2].item_id).toBe(doneItemId); expect(events[3].arguments).toBe('{"a":2,"b":2}'); expect(JSON.stringify(rewritten)).not.toContain("obfuscation"); expect(JSON.stringify(rewritten)).not.toContain("ciphertext-chunk"); From c5d5b25fd93ba577083141b8c15ed04cc46d65f0 Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 4 Aug 2026 11:47:46 +0200 Subject: [PATCH 3/4] fix(providers): neutralize GitHub Copilot encrypted reasoning items GitHub encrypts reasoning summaries with its own scheme (re-encrypted per event); Responses clients cannot decrypt them and stay stuck on thinking. Replace the ciphertext with a canonical empty plaintext summary in output_item events and the terminal completed payload. --- src/server/sse-payload-rewrite.ts | 43 ++++++++++++++++++++++++++++--- tests/sse-payload-rewrite.test.ts | 18 +++++++++++++ 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/src/server/sse-payload-rewrite.ts b/src/server/sse-payload-rewrite.ts index bbe05fa30f..4dbbdce1ae 100644 --- a/src/server/sse-payload-rewrite.ts +++ b/src/server/sse-payload-rewrite.ts @@ -13,6 +13,22 @@ function isPayloadRecord(v: unknown): v is Record { return typeof v === "object" && v !== null && !Array.isArray(v); } +/** + * GitHub Copilot encrypts reasoning summaries (and re-encrypts them per event) with its own + * scheme, which Responses clients cannot decrypt; the app then stays stuck on "thinking". + * Replace the ciphertext with a canonical empty plaintext summary so the item completes + * immediately and the turn moves on to the actual output. + */ +function sanitizeGithubCopilotReasoningItem(item: Record): boolean { + if (item.type !== "reasoning" || typeof item.encrypted_content !== "string") return false; + delete item.encrypted_content; + const summary = Array.isArray(item.summary) ? item.summary : []; + summary.push({ type: "summary_text", text: "" }); + item.summary = summary; + if (!Array.isArray(item.content)) item.content = []; + return true; +} + /** Split one complete SSE event block while retaining its original blank-line delimiter. */ export function nextSseBlock(buffer: string): { block: string; delimiter: string; rest: string } | null { const match = buffer.match(/\r?\n\r?\n/); @@ -88,10 +104,13 @@ export function createGithubCopilotObfuscationRewrite(): SsePayloadRewrite { return payload; } if (!isPayloadRecord(parsed)) return payload; - const hadObfuscation = typeof parsed.obfuscation === "string"; - if (hadObfuscation) delete parsed.obfuscation; + let changed = false; + if (typeof parsed.obfuscation === "string") { + delete parsed.obfuscation; + changed = true; + } const type = parsed.type; - if (type === "response.function_call_arguments.delta" && hadObfuscation) { + if (type === "response.function_call_arguments.delta" && changed) { if (typeof parsed.output_index === "number") obfuscatedCalls.add(parsed.output_index); // Ciphertext chunk: emit an empty delta so the client sees a valid event // without any unusable bytes; the plaintext arrives with `.done`. @@ -112,7 +131,23 @@ export function createGithubCopilotObfuscationRewrite(): SsePayloadRewrite { return `${JSON.stringify(deltaEvent)}\n\ndata: ${JSON.stringify(parsed)}`; } } - return hadObfuscation ? JSON.stringify(parsed) : payload; + if ( + (type === "response.output_item.added" || type === "response.output_item.done") + && isPayloadRecord(parsed.item) + && sanitizeGithubCopilotReasoningItem(parsed.item) + ) { + changed = true; + } + if ( + type === "response.completed" + && isPayloadRecord(parsed.response) + && Array.isArray(parsed.response.output) + ) { + for (const item of parsed.response.output) { + if (isPayloadRecord(item) && sanitizeGithubCopilotReasoningItem(item)) changed = true; + } + } + return changed ? JSON.stringify(parsed) : payload; }; } diff --git a/tests/sse-payload-rewrite.test.ts b/tests/sse-payload-rewrite.test.ts index a05ad733cc..e8a3fb43d6 100644 --- a/tests/sse-payload-rewrite.test.ts +++ b/tests/sse-payload-rewrite.test.ts @@ -103,6 +103,24 @@ describe("SSE payload rewrite composition", () => { expect(rewrite(payload)).toBe(payload); }); + test("GitHub Copilot obfuscation rewrite neutralizes encrypted reasoning items", () => { + const rewrite = createGithubCopilotObfuscationRewrite(); + const added = '{"type":"response.output_item.added","output_index":0,"item":{"type":"reasoning","id":"cipher-id","content":[],"encrypted_content":"ciphertext"}}'; + const done = '{"type":"response.output_item.done","output_index":0,"item":{"type":"reasoning","id":"cipher-id","content":[],"encrypted_content":"ciphertext"}}'; + const completed = '{"type":"response.completed","response":{"id":"resp_1","status":"completed","output":[{"type":"reasoning","id":"cipher-id","content":[],"encrypted_content":"ciphertext"},{"type":"function_call","id":"fc_1","name":"shell","arguments":"{}"}]}}'; + + for (const payload of [added, done, completed]) { + const out = JSON.parse(rewrite(payload)!) as { response?: { output?: unknown[] }; item?: Record }; + const items = out.response?.output ?? (out.item ? [out.item] : []); + for (const item of items as Record[]) { + if (item.type === "reasoning") { + expect(item.encrypted_content).toBeUndefined(); + expect(item.summary).toEqual([{ type: "summary_text", text: "" }]); + } + } + } + }); + test("applies image-gen restore and item-id repair in one relay pass", async () => { const upstream = [ 'event: response.output_item.added\ndata: {"type":"response.output_item.added","output_index":0,"item":{"type":"message","id":"msg_0","role":"assistant"}}\n\n', From 9542d692039b37e256e336549dc242e2102969ef Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 4 Aug 2026 11:53:07 +0200 Subject: [PATCH 4/4] fix(providers): pin GitHub Copilot response and item ids to first-seen values GitHub re-encrypts the response id and every item id per SSE event, so the terminal completed payload disagrees with the streamed events; clients reconcile by id and fail to finalize the turn. Pin each entity to its first-seen id across created, output_item, and completed events. --- src/server/sse-payload-rewrite.ts | 58 ++++++++++++++++++++++++------- tests/sse-payload-rewrite.test.ts | 22 ++++++++++++ 2 files changed, 67 insertions(+), 13 deletions(-) diff --git a/src/server/sse-payload-rewrite.ts b/src/server/sse-payload-rewrite.ts index 4dbbdce1ae..664801f89f 100644 --- a/src/server/sse-payload-rewrite.ts +++ b/src/server/sse-payload-rewrite.ts @@ -96,6 +96,11 @@ export function createGithubCopilotObfuscationRewrite(): SsePayloadRewrite { // GitHub re-encrypts `item_id` per event (delta and done carry different ids for the // same logical call), so calls are tracked by the stable `output_index` instead. const obfuscatedCalls = new Set(); + // GitHub also re-encrypts the response id and every item id per event (`created`/`done` + // and the terminal `completed` payload disagree). Clients reconcile streamed items with + // the completed response by id, so pin every entity to its first-seen id. + let responseId: string | null = null; + const itemIds = new Map(); return (payload: string): string => { let parsed: unknown; try { @@ -131,20 +136,47 @@ export function createGithubCopilotObfuscationRewrite(): SsePayloadRewrite { return `${JSON.stringify(deltaEvent)}\n\ndata: ${JSON.stringify(parsed)}`; } } - if ( - (type === "response.output_item.added" || type === "response.output_item.done") - && isPayloadRecord(parsed.item) - && sanitizeGithubCopilotReasoningItem(parsed.item) - ) { - changed = true; + if (type === "response.created" && isPayloadRecord(parsed.response)) { + const id = parsed.response.id; + if (typeof id === "string") { + if (responseId === null) responseId = id; + else if (id !== responseId) { + parsed.response.id = responseId; + changed = true; + } + } } - if ( - type === "response.completed" - && isPayloadRecord(parsed.response) - && Array.isArray(parsed.response.output) - ) { - for (const item of parsed.response.output) { - if (isPayloadRecord(item) && sanitizeGithubCopilotReasoningItem(item)) changed = true; + if (type === "response.output_item.added" || type === "response.output_item.done") { + const outputIndex = parsed.output_index; + const item = parsed.item; + if (typeof outputIndex === "number" && isPayloadRecord(item) && typeof item.id === "string") { + const existing = itemIds.get(outputIndex); + if (existing === undefined) itemIds.set(outputIndex, item.id); + else if (item.id !== existing) { + item.id = existing; + changed = true; + } + } + if (isPayloadRecord(item) && sanitizeGithubCopilotReasoningItem(item)) changed = true; + } + if (type === "response.completed" && isPayloadRecord(parsed.response)) { + if (typeof parsed.response.id === "string" && responseId !== null && parsed.response.id !== responseId) { + parsed.response.id = responseId; + changed = true; + } + if (Array.isArray(parsed.response.output)) { + parsed.response.output.forEach((item, outputIndex) => { + if (!isPayloadRecord(item)) return; + if (typeof item.id === "string") { + const existing = itemIds.get(outputIndex); + if (existing === undefined) itemIds.set(outputIndex, item.id); + else if (item.id !== existing) { + item.id = existing; + changed = true; + } + } + if (sanitizeGithubCopilotReasoningItem(item)) changed = true; + }); } } return changed ? JSON.stringify(parsed) : payload; diff --git a/tests/sse-payload-rewrite.test.ts b/tests/sse-payload-rewrite.test.ts index e8a3fb43d6..fec094fa2d 100644 --- a/tests/sse-payload-rewrite.test.ts +++ b/tests/sse-payload-rewrite.test.ts @@ -121,6 +121,28 @@ describe("SSE payload rewrite composition", () => { } }); + test("GitHub Copilot obfuscation rewrite pins response and item ids to first-seen values", () => { + const rewrite = createGithubCopilotObfuscationRewrite(); + const created = '{"type":"response.created","response":{"id":"resp-A","status":"in_progress"}}'; + const addedReasoning = '{"type":"response.output_item.added","output_index":0,"item":{"type":"reasoning","id":"rs-stream","content":[],"encrypted_content":"ciphertext"}}'; + const addedCall = '{"type":"response.output_item.added","output_index":1,"item":{"type":"function_call","id":"fc-stream","call_id":"call_1","name":"shell","arguments":""}}'; + const doneCall = '{"type":"response.output_item.done","output_index":1,"item":{"type":"function_call","id":"fc-done","call_id":"call_1","name":"shell","arguments":"{\\"command\\":\\"ls\\"}"}}'; + const completed = '{"type":"response.completed","response":{"id":"resp-B","status":"completed","output":[{"type":"reasoning","id":"rs-final","content":[],"encrypted_content":"ciphertext"},{"type":"function_call","id":"fc-final","call_id":"call_1","name":"shell","arguments":"{\\"command\\":\\"ls\\"}"}]}}'; + + const createdOut = JSON.parse(rewrite(created)!) as { response: { id: string } }; + rewrite(addedReasoning); + rewrite(addedCall); + const doneOut = JSON.parse(rewrite(doneCall)!) as { item: { id: string } }; + const completedOut = JSON.parse(rewrite(completed)!) as { response: { id: string; output: { id: string; encrypted_content?: string }[] } }; + + expect(createdOut.response.id).toBe("resp-A"); + expect(doneOut.item.id).toBe("fc-stream"); + expect(completedOut.response.id).toBe("resp-A"); + expect(completedOut.response.output[0]!.id).toBe("rs-stream"); + expect(completedOut.response.output[1]!.id).toBe("fc-stream"); + expect(completedOut.response.output[0]!.encrypted_content).toBeUndefined(); + }); + test("applies image-gen restore and item-id repair in one relay pass", async () => { const upstream = [ 'event: response.output_item.added\ndata: {"type":"response.output_item.added","output_index":0,"item":{"type":"message","id":"msg_0","role":"assistant"}}\n\n',