Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 17 additions & 5 deletions src/server/responses/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5480,6 +5480,14 @@ async function handleResponsesInner(
translatorBudget,
...(options.forceEmptyResponseId ? { responseId: "" } : {}),
...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
onCompletedResponse: (response: Record<string, unknown>) => {
rememberResponseState(
parsed._rawBody,
response,
continuationStateForResponse(),
responseStateOptions(adapterNeedsForcedContinuation(activeAdapter.name)),
);
},
},
);
// Same lifetime tracking as every other streaming return in this function: the turn
Expand All @@ -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(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve a replay anchor for empty local terminals

When a client sends previous_response_id together with the full history it already carries, this new snapshot cannot trigger the overlap skip: a local terminal has output: [], so rememberResponseState sets providerOutputStart equal to the end of the stored items, while expandPreviousResponseInput requires a matched provider-issued item after that boundary. It consequently prepends the stored history to the identical client history, and subsequent continuations can repeatedly inflate or corrupt the Kiro context. Preserve an authoritative replay anchor/terminal marker for this empty response, and cover a follow-up whose input includes the prior messages rather than only the delta.

Useful? React with 👍 / 👎.

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
Expand Down
56 changes: 56 additions & 0 deletions tests/server-kiro-completion-e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
});
}
});
Loading