Skip to content

Kiro/Claude→OpenAI-Responses stream: message item closed after following function_call, hangs Codex #134

Description

@tonybase

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions