Skip to content

Tool-heavy Codex App sessions can enter an unbounded continuation loop with WebSocket forwarding #326

Closed

Description

@kdnsna

Client or integration

Codex App (also observed from a Codex task using the local proxy)

Area

Proxy and routing / Responses WebSocket continuation

Summary

A tool-heavy Codex task routed through OpenCodex can keep issuing new continuation requests after each tool result, repeatedly emitting near-identical progress messages and re-selecting the same browser action instead of reaching a terminal response.

I expected the Responses bridge to preserve one coherent tool-turn continuation and to surface a terminal completion (or a bounded, actionable error) once the task is no longer making progress.

This does not look like a raw HTTP retry of one request: the observed request IDs were distinct and most responses were HTTP 200. Stopping/bypassing OpenCodex immediately stopped the behavior in the same Codex environment.

Reproduction

This is currently intermittent rather than a short deterministic reproduction.

  1. Use Codex App with its OpenAI endpoint pointed at a local OpenCodex proxy.
  2. Enable WebSocket support and route the native OpenAI/Codex Responses traffic through the OpenAI Responses forward route.
  3. Start a long, tool-heavy task (browser/computer-use actions; the original task also used /goal auto-continuation).
  4. Put the task into a non-progressing end state, such as a browser workflow that cannot finish the final external action.
  5. Observe that after tool outputs the client keeps producing new continuation turns, near-identical status text, and repeated selection of the same tool target.

A second short diagnostic task showed the same pattern without /goal or subagents: 13 similar commentary updates over about three minutes, backed by 13 distinct Responses requests, all HTTP 200. This makes /goal an amplifier rather than the only cause.

Version

  • Incident observed with @bitkyc08/opencodex 2.7.33
  • Current local installation is now 2.7.34; I have not deliberately rerun the long scenario yet because it can loop for a long time.
  • Codex CLI: 0.144.1
  • Codex App build: 26.715.21425

Operating system

macOS 26.5.2 (build 25F84), Apple Silicon

Provider and model

Native OpenAI/Codex login forward route with the openai-responses adapter. I am deliberately omitting account/model identifiers because the behavior appears to be at the bridge/continuation level.

Logs or error output

Sanitized observations from one incident:

duration: about 77 minutes
assistant messages: 293
tool calls: 463
unique proxy request IDs: 75
HTTP results: 73 x 200, then 2 x 502 during interruption

separate diagnostic:
near-identical commentary updates: 13
distinct Responses requests: 13
HTTP results: 13 x 200

I inspected the local Responses state after the diagnostic. It contained distinct resp_* states with usable cursors and accumulated tool output; I did not find reuse of one request ID or verbatim replay of the same assistant commentary as developer input.

Supporting context

This may be in the same compatibility family as openai/codex#25327 (custom proxy + WebSocket + tool calls, requests returning 200 while continuation handling fails), but the symptom here is repeated continuation/tool selection rather than a disconnected client.

I also reviewed OpenCodex #215 and #272:

Could maintainers advise whether the bridge guarantees exactly one terminal event for each tool-output continuation, and which redacted bridge events/state fields would be most useful to capture next? I can provide a minimal sanitized trace privately in the issue if needed.

Redacted configuration

{
  "local_proxy": "http://127.0.0.1:10100/v1",
  "websockets": true,
  "provider": "openai",
  "adapter": "openai-responses",
  "upstream": "native OpenAI/Codex forward route"
}

Checks

  • I searched existing issues and documentation.
  • I removed secrets, tokens, account details, request credentials, task content, and personal data.

Activity

  1. lidge-jun commented on Jul 23, 2026

    @lidge-jun
    Owner

    Thanks for the thorough write-up. The symptom you describe — distinct request IDs, HTTP 200 responses, repeated tool selection without terminal completion — is clearly different from the known issues:

    Your second diagnostic (13 requests, 13 identical 200s, no /goal or subagents) is particularly useful because it narrows the surface: the loop is not driven by /goal auto-continuation alone.

    What would help narrow this down:

    To determine whether the bridge is emitting a terminal event that the client ignores, or whether the bridge itself is re-initiating continuations, we'd need a sanitized trace of the WS event sequence. Specifically:

    1. Whether response.completed events appear between continuation turns (does each continuation's response terminate cleanly?).
    2. The tool output submission pattern — after each tool result, does a new resp_* state get created, or is the same response ID reused?
    3. Whether response.output_item.done events fire for each tool call output before the next continuation starts.

    If you can reproduce on v2.7.34, enabling OCX_DEBUG=bridge would capture the relevant event flow (request bodies are not logged; only event types and IDs).

    Alternatively, if you have the local Responses state from the diagnostic session, the cursor chain and resp_* creation timestamps would show whether the bridge or the client is driving the loop.

  2. kdnsna commented on Jul 23, 2026

    @kdnsna
    Author

    I did a deeper state/source correlation before adding more speculation. There is now a concrete deterministic defect in the native passthrough continuation path that matches the same-thread loop.

    Observed chain from the looping turn

    The affected Codex App turn was a normal request to track this issue. Between 09:46:01 and 09:47:44 it emitted six near-equivalent progress/commentary messages. Each was followed by a different successful tool call, so this was not an identical HTTP/SSE replay.

    The six persisted passthrough states were distinct and grew as follows:

    state   total input/state items   tool outputs   identical no-ID developer guidance copies
    1       68                        14             2
    2       70                        15             3
    3       72                        16             4
    4       74                        17             5
    5       76                        18             6
    6       78                        19             7
    

    Every continuation added exactly:

    1. one new custom_tool_call_output, and
    2. one additional developer message with no item ID.

    Those developer messages are byte-identical: 538 characters, same SHA-256 prefix d9d2e296ad4382a9. Without exposing the prompt body, structural checks identify it as OpenCodex's generated multi-agent guidance: it has the <multi_agent_mode> wrapper and mentions spawn_agent, fork_turns, the preferred sub-agent, and the roster.

    The local config has an injectionModel plus subagentModels; multiAgentGuidanceEnabled is unset, which means enabled by default.

    Source-level cause in v2.7.34

    The ordering appears non-idempotent:

    1. expandPreviousResponseInput() restores all previous.items into the new request input.
    2. Later, the guidance shim computes and unconditionally appends the same guidance via injectDeveloperMessage().
    3. The native ChatGPT passthrough then force-records parsed._rawBody; rememberResponseState() stores that already-injected input.
    4. The next previous_response_id expansion replays the old guidance and the shim appends another copy.

    That exactly explains the observed +1 identical developer message per tool continuation. It also explains why stopping/bypassing the proxy immediately cleared the behavior in the same thread.

    I checked the current dev branch as well; it is ahead of v2.7.34, but the same expand-then-unconditional-inject sequence remains.

    Suggested fix / regression test

    Either of these should prevent accumulation:

    • make injectDeveloperMessage idempotent for the exact generated guidance already present in the replayed prefix; or
    • exclude proxy-authored guidance from the input persisted by rememberResponseState, then inject one fresh copy per outbound request.

    A regression test could create a Responses request with collaboration tools and generated guidance, record a completed response, then submit two chained previous_response_id tool-output continuations. Each outbound request and each saved state should contain at most one copy of that exact proxy-generated guidance.

    As a diagnostic A/B, setting multiAgentGuidanceEnabled: false should remove this accumulator while leaving the underlying Codex collaboration surface intact. I have not changed the user's persistent config yet; I can run that bounded A/B if you want confirmation before a patch.

    This finding does not yet rule out a separate terminal-event issue, but it is independently sufficient to explain why a tool-heavy native OpenAI turn becomes increasingly biased toward re-planning/repeating the same workflow.

  3. added 2 commits that reference this issue on Jul 23, 2026
  4. lidge-jun commented on Jul 23, 2026

    @lidge-jun
    Owner

    Fixed on dev by PR #350 (merged 05ee9ec).

    Root cause was exactly as @kdnsna diagnosed: expandPreviousResponseInput() replays persisted input (including the previously injected guidance) and the guidance shim then unconditionally appended another copy, which rememberResponseState persisted — +1 identical developer message per tool continuation.

    The fix takes the idempotent-injection path: the replay-prefix length is now carried from the expansion onto the parsed request, and injectDeveloperMessage scans that replayed prefix for the exact proxy-generated guidance item, skipping re-insertion when present. Near-match items (different text/shape) still inject fresh guidance, and the guidance-before-compaction_trigger ordering invariant is preserved. The suggested two-continuation regression is implemented: two chained previous_response_id continuations now assert exactly one guidance copy in both the outbound request and saved state.

    Ships in the next release; available on dev now. Thank you for the SHA-level evidence trail — it pinned the accumulator precisely.

  5. added 2 commits that reference this issue on Sep 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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