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: 21 additions & 1 deletion src/server/responses/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ import {
conversationIdFromResponsesRequest,
normalizeLogConversationId,
reasoningReplayConversationIdFromResponsesRequest,
sessionLaneIdFromRequest,
sessionIdHeaderFromRequest,
} from "../request-log-conversation";
import type { AttemptRecoveryKind } from "../../usage/log";
Expand Down Expand Up @@ -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);
};
Expand Down Expand Up @@ -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
Comment on lines +3364 to +3365

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Rebind termination scope after OAuth account failover

With multiple Kiro OAuth accounts, a preflight 429 can switch the request to snapshot.accountId in rotateRunTurnAdapterOnPreflight429, but this scope remains bound to the account selected before that retry. A final response served by account B is therefore cached under account A; a replay served by B is sent upstream again, while a later request on A can be incorrectly locally terminated. Rebind the termination scope whenever generic OAuth failover succeeds, using the rotated snapshot's account ID.

Useful? React with 👍 / 👎.

? normalizeLogConversationId(JSON.stringify([
exactConversation,
admissionIdentity,
route.providerName,
route.modelId,
servingAccount,
]))
: undefined);
}
bindRouteReasoningReplayScope({
parsed,
providerName: route.providerName,
Expand Down
45 changes: 45 additions & 0 deletions tests/server-kiro-completion-e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand Down
Loading