From 1974ea94303bd14c144f83c50d17c35a0d9861cf Mon Sep 17 00:00:00 2001 From: luvs01 Date: Mon, 31 Aug 2026 10:44:53 +0900 Subject: [PATCH] fix(kiro): persist local terminal continuations --- src/server/responses/core.ts | 22 +++++++--- tests/server-kiro-completion-e2e.test.ts | 56 ++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 5 deletions(-) diff --git a/src/server/responses/core.ts b/src/server/responses/core.ts index 1f56ed979a..158165ad17 100644 --- a/src/server/responses/core.ts +++ b/src/server/responses/core.ts @@ -5480,6 +5480,14 @@ async function handleResponsesInner( translatorBudget, ...(options.forceEmptyResponseId ? { responseId: "" } : {}), ...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}), + onCompletedResponse: (response: Record) => { + rememberResponseState( + parsed._rawBody, + response, + continuationStateForResponse(), + responseStateOptions(adapterNeedsForcedContinuation(activeAdapter.name)), + ); + }, }, ); // Same lifetime tracking as every other streaming return in this function: the turn @@ -5498,12 +5506,16 @@ async function handleResponsesInner( }, ); } - return new Response( - JSON.stringify(buildResponseJSON(terminalEvents, parsed._responseModelId ?? parsed.modelId, { - translatorBudget, - })), - { headers: { "Content-Type": "application/json" } }, + const json = buildResponseJSON(terminalEvents, parsed._responseModelId ?? parsed.modelId, { + translatorBudget, + }); + rememberResponseState( + parsed._rawBody, + json, + continuationStateForResponse(), + responseStateOptions(adapterNeedsForcedContinuation(activeAdapter.name)), ); + return new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json" } }); } // One request-scoped transient-retry budget owner, declared here so BOTH the initial send // and the later recovery refetches (429, key/account rotation, OAuth replay) share it. A diff --git a/tests/server-kiro-completion-e2e.test.ts b/tests/server-kiro-completion-e2e.test.ts index 61b6181643..03b306074a 100644 --- a/tests/server-kiro-completion-e2e.test.ts +++ b/tests/server-kiro-completion-e2e.test.ts @@ -637,3 +637,59 @@ describe("Kiro local terminal accounting", () => { }); } }); + +describe("Kiro local terminal continuation", () => { + for (const stream of [true, false]) { + test(`a local terminal remains available through previous_response_id (stream=${stream})`, async () => { + const upstream = scriptedKiroUpstream([completionFrames("Yes — one JavaScript cell, many tool calls.")]); + saveConfig(kiroConfig(upstream.server.url.toString())); + const proxy = startServer(0); + try { + const terminal = await originalFetch(new URL("/v1/responses", proxy.url), { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + model: "kiro-test/gpt-5.6-sol", + stream, + store: false, + input: [ + { type: "message", role: "user", content: [{ type: "input_text", text: "what is code mode" }] }, + { + type: "message", + role: "assistant", + phase: "final_answer", + content: [{ type: "output_text", text: "Code mode runs JavaScript that calls tools." }], + }, + ], + }), + }); + expect(terminal.status).toBe(200); + const terminalBody = await terminal.text(); + const responseId = stream + ? responseEvents(terminalBody).find(event => event.name === "response.completed")?.data.response.id + : (JSON.parse(terminalBody) as { id: string }).id; + expect(responseId).toMatch(/^resp_/); + expect(upstream.requests).toHaveLength(0); + + const followUp = await originalFetch(new URL("/v1/responses", proxy.url), { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + model: "kiro-test/gpt-5.6-sol", + stream: false, + store: false, + previous_response_id: responseId, + input: "so it batches calls?", + }), + }); + + expect(followUp.status).toBe(200); + await followUp.text(); + expect(upstream.requests).toHaveLength(1); + } finally { + await proxy.stop(true); + upstream.server.stop(true); + } + }); + } +});