diff --git a/src/server/responses/core.ts b/src/server/responses/core.ts index 1f56ed979a..61a632bc54 100644 --- a/src/server/responses/core.ts +++ b/src/server/responses/core.ts @@ -273,6 +273,7 @@ import { conversationIdFromResponsesRequest, normalizeLogConversationId, reasoningReplayConversationIdFromResponsesRequest, + sessionLaneIdFromRequest, sessionIdHeaderFromRequest, } from "../request-log-conversation"; import type { AttemptRecoveryKind } from "../../usage/log"; @@ -2716,7 +2717,6 @@ async function handleResponsesInner( threadIdHeader: req.headers.get("thread-id"), cursorConversationId: parsed._cursorConversationId, }); - bindTurnTerminationScope(parsed, resolvedConversationId); const rememberKiroDeliveredFinalAnswer = (adapterName: string, response: unknown): void => { if (adapterName === "kiro") rememberDeliveredFinalAnswer(parsed, response); }; @@ -3352,6 +3352,26 @@ async function handleResponsesInner( delete logCtx.accountLogLabel; } const adapter = resolveAdapter(adapterProvider, config.cacheRetention); + if (adapter.name === "kiro") { + // A log conversation deliberately coalesces a parent's parallel subagents, but a delivered + // final answer may suppress work only for the exact child and serving identity that emitted it. + // Hash the composite before binding so no caller, account, or route identifier is retained. + const exactConversation = sessionLaneIdFromRequest(req.headers) + ?? normalizeLogConversationId(parsed._cursorConversationId); + const admissionIdentity = options.admission?.kind === "configured" + ? `configured:${options.admission.keyId}` + : options.admission?.kind; + const servingAccount = replayOAuthCredentialSnapshot?.accountId ?? codexLogAccountId(authCtx); + bindTurnTerminationScope(parsed, exactConversation + ? normalizeLogConversationId(JSON.stringify([ + exactConversation, + admissionIdentity, + route.providerName, + route.modelId, + servingAccount, + ])) + : undefined); + } bindRouteReasoningReplayScope({ parsed, providerName: route.providerName, diff --git a/tests/server-kiro-completion-e2e.test.ts b/tests/server-kiro-completion-e2e.test.ts index 61b6181643..7cbb95f54f 100644 --- a/tests/server-kiro-completion-e2e.test.ts +++ b/tests/server-kiro-completion-e2e.test.ts @@ -439,6 +439,51 @@ describe("Kiro completion through public server endpoints", () => { } }); + test("a proxy-recorded final answer does not suppress a sibling conversation", async () => { + const deliveredAnswer = "Code mode runs JavaScript that calls tools."; + const upstream = scriptedKiroUpstream([ + completionFrames(deliveredAnswer, "completion-a"), + completionFrames("Sibling work completed.", "completion-b"), + ]); + saveConfig(kiroConfig(upstream.server.url.toString())); + const proxy = startServer(0); + const headers = { + "content-type": "application/json", + "x-codex-parent-thread-id": "shared-parent", + }; + try { + const first = await originalFetch(new URL("/v1/responses", proxy.url), { + method: "POST", + headers: { ...headers, session_id: "child-a" }, + body: JSON.stringify({ model: "kiro-test/gpt-5.6-sol", stream: false, input: "first task" }), + }); + expect(first.status).toBe(200); + await first.text(); + + const sibling = await originalFetch(new URL("/v1/responses", proxy.url), { + method: "POST", + headers: { ...headers, session_id: "child-b" }, + body: JSON.stringify({ + model: "kiro-test/gpt-5.6-sol", + stream: false, + input: [ + { type: "message", role: "user", content: [{ type: "input_text", text: "different task" }] }, + { type: "message", role: "assistant", content: [{ type: "output_text", text: deliveredAnswer }] }, + ], + }), + }); + + expect(sibling.status).toBe(200); + await sibling.text(); + // The shared parent is only a correlation qualifier. A cache hit here would return before + // build/send and leave this at one request, silently terminating the sibling's work. + expect(upstream.requests).toHaveLength(2); + } finally { + await proxy.stop(true); + upstream.server.stop(true); + } + }); + test("a new user request after a proxy-recorded final answer is not suppressed", async () => { const deliveredAnswer = "Code mode runs JavaScript that calls tools."; const upstream = scriptedKiroUpstream([