Summary
When a Kiro/Claude→OpenAI-Responses streaming response contains text followed by a tool call, the emitted Responses SSE events are ordered incorrectly: the assistant message item (output_index 0) is never closed at its content_block_stop; its output_text.done / content_part.done / output_item.done are deferred to message_stop and therefore emitted after the function_call item (output_index 1) has already been fully opened and closed.
This violates the Responses protocol invariant that each output item is fully closed before the next output_item.added. Codex (codex-tui, observed 0.144.5) hitting /v1/responses gets stuck in a permanent "loading" state: its streaming state machine sees output_item.added for index 1 while index 0 is still open, and waits forever for the missing close of index 0. The associated exec_command shows as a hung "background terminal" that never resolves, even though the underlying command is not actually running.
Pure-text responses are unaffected (finalize at message_stop yields correct order). The bug only manifests when text is followed by tool_use/function_call in the same turn.
Observed event order (buggy)
seq 3 output_item.added index 0 (message)
seq 4 content_part.added index 0
seq 5-31 output_text.delta index 0
seq 32 output_item.added index 1 (function_call) <-- message idx0 not yet closed
seq 33 function_call_arguments.delta index 1
seq 34 function_call_arguments.done index 1
seq 35 output_item.done index 1 (function_call)
seq 36 output_text.done index 0 <-- message close arrives late
seq 37 content_part.done index 0
seq 38 output_item.done index 0 (message)
seq 39 response.completed
Root cause
File: internal/translator/claude/openai/responses/claude_openai-responses_response.go
In the streaming converter ConvertClaudeResponseToOpenAIResponses, the content_block_stop case for a text block only clears st.InTextBlock and emits nothing (lines ~349-350). The message-closing trio is emitted by finalizeAssistantMessage, which is only called at message_stop (line ~406). When a tool_use block starts after the text block, its events are emitted before the message is finalized.
ConvertKiroStreamToOpenAIResponses (internal/translator/kiro/responses/kiro_responses.go) delegates directly to this converter, so Kiro inherits the bug.
The non-stream aggregator path is not affected (it orders items by index).
Proposed fix
Close the assistant message in place when the text block ends, instead of deferring to message_stop.
In the content_block_stop case, change the text-block branch to finalize the message immediately:
case "content_block_stop":
idx := int(root.Get("index").Int())
if st.InTextBlock {
st.InTextBlock = false
// Close the message item now so it is fully terminated before any
// subsequent function_call/tool_use item opens. Deferring this to
// message_stop interleaves the message-close events after a following
// function_call, which breaks Responses-protocol clients (e.g. Codex
// hangs waiting for the message item to close).
out = append(out, st.finalizeAssistantMessage(nextSeq)...)
} else if st.InFuncBlock {
// ... unchanged ...
finalizeAssistantMessage already guards on !st.MessageOpen (returns nil) and resets MessageOpen/ContentPartOpen/CurrentTextBuf, so the existing message_stop call becomes a no-op for text that was already closed and remains correct for the pure-text case. Note finalizeAssistantMessage uses st.TextBuf (full-message buffer) for the finalized text; this preserves current output text content.
Resulting order:
message (idx0): added -> deltas -> text.done -> part.done -> item.done
function_call(idx1): added -> args.delta -> args.done -> item.done
response.completed
Tests
Add a streaming test in internal/translator/claude/openai/responses/claude_openai-responses_response_test.go (and/or internal/translator/kiro/responses/kiro_responses_test.go) covering text-then-tool_use, asserting that response.output_item.done for the message (output_index 0) is emitted before response.output_item.added for the function_call (output_index 1).
Repro / evidence
- Proxy log
codex-proxy-logs/20260716T175157-034-v1_responses.log: last Codex turn, event seq 32-39 show the interleave above; stream ends with a valid response.completed, yet Codex stayed in loading. ps confirmed no npm/node process was actually running for the reported "background terminal"; running the same npm install manually did not hang, isolating the issue to event ordering.
Summary
When a Kiro/Claude→OpenAI-Responses streaming response contains text followed by a tool call, the emitted Responses SSE events are ordered incorrectly: the assistant
messageitem (output_index 0) is never closed at itscontent_block_stop; itsoutput_text.done/content_part.done/output_item.doneare deferred tomessage_stopand therefore emitted after thefunction_callitem (output_index 1) has already been fully opened and closed.This violates the Responses protocol invariant that each output item is fully closed before the next
output_item.added. Codex (codex-tui, observed 0.144.5) hitting/v1/responsesgets stuck in a permanent "loading" state: its streaming state machine seesoutput_item.addedfor index 1 while index 0 is still open, and waits forever for the missing close of index 0. The associatedexec_commandshows as a hung "background terminal" that never resolves, even though the underlying command is not actually running.Pure-text responses are unaffected (finalize at
message_stopyields correct order). The bug only manifests when text is followed bytool_use/function_callin the same turn.Observed event order (buggy)
Root cause
File:
internal/translator/claude/openai/responses/claude_openai-responses_response.goIn the streaming converter
ConvertClaudeResponseToOpenAIResponses, thecontent_block_stopcase for a text block only clearsst.InTextBlockand emits nothing (lines ~349-350). The message-closing trio is emitted byfinalizeAssistantMessage, which is only called atmessage_stop(line ~406). When a tool_use block starts after the text block, its events are emitted before the message is finalized.ConvertKiroStreamToOpenAIResponses(internal/translator/kiro/responses/kiro_responses.go) delegates directly to this converter, so Kiro inherits the bug.The non-stream aggregator path is not affected (it orders items by index).
Proposed fix
Close the assistant message in place when the text block ends, instead of deferring to
message_stop.In the
content_block_stopcase, change the text-block branch to finalize the message immediately:finalizeAssistantMessagealready guards on!st.MessageOpen(returns nil) and resetsMessageOpen/ContentPartOpen/CurrentTextBuf, so the existingmessage_stopcall becomes a no-op for text that was already closed and remains correct for the pure-text case. NotefinalizeAssistantMessageusesst.TextBuf(full-message buffer) for the finalized text; this preserves current output text content.Resulting order:
Tests
Add a streaming test in
internal/translator/claude/openai/responses/claude_openai-responses_response_test.go(and/orinternal/translator/kiro/responses/kiro_responses_test.go) covering text-then-tool_use, asserting thatresponse.output_item.donefor the message (output_index 0) is emitted beforeresponse.output_item.addedfor the function_call (output_index 1).Repro / evidence
codex-proxy-logs/20260716T175157-034-v1_responses.log: last Codex turn, event seq 32-39 show the interleave above; stream ends with a validresponse.completed, yet Codex stayed in loading.psconfirmed no npm/node process was actually running for the reported "background terminal"; running the samenpm installmanually did not hang, isolating the issue to event ordering.