Skip to content

[Provider compatibility] Codex App delegated tasks fail on ollama-cloud with orphan tool result <missing-id> #3259

Description

@gongyu0918-debug

Client or integration

Codex App

Provider or upstream service

Ollama Cloud (ollama-cloud, ollama-native adapter)

OpenCodex version

2.39.0

Endpoint or capability

POST /v1/responses — delegated task startup and history conversion

Current behaviour

An Ollama Cloud model fails before producing its first response when it is assigned to a delegated child task in Codex App.

The same model works when a task is created normally and begins with a regular user message. The provider connection is healthy, model discovery succeeds, and a normal minimal request returns HTTP 200.

The failure was reproduced with:

  • ollama-cloud/deepseek-v4-flash:0731
  • ollama-cloud/glm-5.3-flash

Minimal redacted request or reproduction

  1. Connect Codex App to a healthy OpenCodex instance.
  2. Confirm that Ollama Cloud model discovery and a normal task both work.
  3. From an existing Codex App task, start a delegated child task and assign it an Ollama Cloud model.
  4. Let the child task begin.
  5. The child fails before any model output is produced.
  6. Start a separate task normally with the same model. It reaches Ollama Cloud successfully.

Actual response or error

Codex App reports:

unexpected status 502 Bad Gateway: Provider unreachable:
ollama-native orphan tool result <missing-id>

The delegated task history begins with a result-like item that has no call_id and no preceding matching call. During local history conversion, ollama-native classifies it as an orphan result and aborts the request. Diagnostics show that the failing request is rejected before an upstream request is sent.

There is also an error-mapping difference:

  • Without web search, the same history condition returns HTTP 400 invalid_request_error.
  • With web search enabled, it returns HTTP 502 Provider unreachable.

Expected behaviour

Delegated child tasks should start normally on Ollama Cloud models. The client bootstrap history should be normalized before native Ollama tool-history validation.

A local history-validation failure should not be reported as an unreachable upstream provider.

Additional context and attachments

Codex CLI: 0.144.6
OS: Windows 11, build 26200

Control results:

Ollama Cloud provider test: connected, 19 models discovered
Normal Ollama Cloud request: HTTP 200, upstream request sent
Delegated-task history failure: rejected locally, no upstream request sent

Upstream documentation

Activity

  1. added
    providerProvider adapters, OpenAI-compat presets, upstream API quirks
    toolstool_calls, MCP, web-search / sidecar tools
    on Sep 2, 2026
  2. github-actions commented on Sep 2, 2026

    @github-actions
    Contributor

    Issue reopened

    The report now contains the information required by the automated check. Thanks for updating it.

  3. lidge-jun commented on Sep 2, 2026

    @lidge-jun
    Owner

    리뷰 · 우선순위 72 / 80

    이 이슈는 Codex App에서 위임(delegated) 자식 작업을 만들 때, 그 자식에 Ollama Cloud 모델을 붙이면 첫 응답도 나오기 전에 실패한다는 보고입니다. 같은 모델로 일반 작업을 새로 시작하면 정상입니다. 지금 dev(HEAD a6ee24f5b, 패키지 2.40.0)에도 보고하신 문구가 그대로 남아 있습니다. src/adapters/ollama-native.ts의 buildNativeMessages는 히스토리를 네이티브 /api/chat 메시지로 바꿀 때, 앞에 짝이 되는 assistant tool-call 배치(pending)가 없는데 toolResult가 먼저 오면 ollama-native orphan tool result <missing-id>를 던지고 요청을 막습니다. 업스트림으로 한 번도 나가지 않은 채 로컬에서 거절되는 경로라, 재현 설명과 코드가 맞습니다.

    위임 자식 작업의 부트스트랩 히스토리가 문제의 시작으로 보입니다. Codex App이 자식 턴을 열 때 결과처럼 생긴 아이템을 앞에 넣는데, call_id도 없고 짝이 되는 호출도 없습니다. 일반 작업은 사용자 메시지로 시작하니 이 검증을 안 타고, 그래서 같은 모델·같은 연결인데도 위임만 깨집니다. deepseek-v4-flash:0731과 glm-5.3-flash 둘 다에서 났다는 점도, 특정 모델 버그라기보다 히스토리 모양과 어댑터의 엄격한 짝 검증이 부딪힌 쪽에 가깝습니다. 보고 버전은 OpenCodex 2.39.0이지만, HEAD의 같은 함수도 여전히 고아 결과를 합성·완화하지 않고 바로 throw 합니다.

    다른 어댑터와 비교하면 차이가 큽니다. openai-chat은 고아 tool result 앞에 합성 assistant tool_call을 넣고, id가 비면 call_orphan_ 접두로 채웁니다. anthropic/google/command-code도 고아를 텍스트 캐리어나 합성 쌍으로 넘기는 쪽입니다. 반면 ollama-native(그리고 비슷한 kiro)는 네이티브 와이어가 assistant tool_calls 묶음 뒤에 tool 결과가 와야 한다고 못 박고, 짝이 안 맞으면 요청 자체를 거절합니다. #2863으로 들어온 네이티브 경로의 의도(불완전한 리플레이를 업스트림에 보내지 않기)는 타당하지만, Codex App 위임 부트스트랩처럼 클라이언트가 넣은 빈 껍데기 결과까지 같은 엄격함으로 치면 실사용 위임이 막힙니다.

    상태 코드 매핑도 이슈 본문과 맞습니다. 웹 검색이 꺼진 경로에서는 어댑터 throw가 invalid_request_error/HTTP 400 쪽으로 분류되기 쉽고, 웹 검색 루프를 타면 src/web-search/loop.ts가 일반 Error를 잡아 LoopError(502, Provider unreachable: …)로 감쌉니다. 로컬 히스토리 검증 실패인데도 사용자에게는 업스트림이 죽은 것처럼 보입니다. 기대한 동작(위임도 시작되고, 로컬 거절은 unreachable이 아니어야 함)과 지금 HEAD 동작이 어긋난 상태입니다.

    types.ts/config.ts 대분할 캠페인과는 무관합니다. 손댈 곳은 어댑터 히스토리 변환과 에러 분류·웹검색 루프 매핑입니다. 설정 스키마 분할로 무효화될 PR/이슈가 아니라 close-don't-rebase 대상도 아닙니다. 중복 이슈로 바로 묶을 만한 열린 티켓도 지금 스캔에서는 보이지 않습니다. 라벨은 provider-compatibility/provider/tools로 이미 맞아 있어 이 댓글에서 바꾸지 않습니다.

    src/adapters/ollama-native.ts 라인 332-334 - toolResult인데 pending이 없으면 즉시 orphan tool result <missing-id>를 throw. 위임 부트스트랩의 call_id 없는 결과 아이템과 정확히 일치합니다.
    src/adapters/ollama-native.ts 라인 336-338 - call_id는 있으나 pending 배치에 없으면 다른 문구로 throw. 이번 재현은 id 자체가 비어 <missing-id> 분기로 간 것으로 보입니다.
    src/adapters/ollama-native.ts 라인 350-352 - 대화 메시지가 오면 pending을 flush하고, 미해결 호출은 만들지 않습니다. 고아를 합성하지 않는 설계가 여기서 고정됩니다.
    src/adapters/openai-chat.ts (고아 합성 경로) - 같은 OCX 히스토리라도 chat 어댑터는 합성 쌍으로 통과시킵니다. ollama-native만 위임 부트스트랩에 취약합니다.
    src/web-search/loop.ts 라인 567 - 일반 Error를 502 Provider unreachable로 감싸, 로컬 검증 실패가 업스트림 장애처럼 보입니다.
    재현 대조 - 일반 작업 HTTP 200·업스트림 송신 vs 위임은 로컬 거절·업스트림 미송신. 제공자/키가 아니라 히스토리 모양 문제로 좁혀집니다.

    메인테이너의 판단이 필요한 지점

    • 고아 tool result를 ollama-native에서도 openai-chat처럼 합성 assistant 쌍으로 고칠지, 아니면 Codex App 위임 부트스트랩만 서버/파서 단에서 정규화할지.
    • id가 비어 있는 결과(<missing-id>)는 합성 대상인지, 아니면 드롭/텍스트 캐리어로만 넘길지.
    • 웹 검색 루프에서 어댑터 로컬 검증 Error를 502 unreachable로 감싸지 말고 400 invalid_request로 유지할지(관측/UX 수정).
    • 수정 범위를 2.40.x 핫픽스로 볼지, 위임·서브에이전트 히스토리 정규화 캠페인에 묶을지.

    너의 추천

    • 이슈는 유효합니다. HEAD에서도 같은 throw가 살아 있으니 닫지 말고 help wanted 또는 버그 트랙으로 유지하세요.
    • 1순위: 위임 자식의 부트스트랩 입력을 로그/픽스처로 고정한 뒤, buildNativeMessages에 고아(특히 missing id) 회귀 테스트를 추가하세요.
    • 수정 방향 후보: (A) 위임 시작 히스토리에서 call_id 없는 result-like 아이템을 제거·정규화, (B) ollama-native가 고아를 합성/텍스트 캐리어로 완화, (C) 웹검색 루프의 로컬 검증 Error → 400 매핑. A+C가 증상·관측을 빨리 줄이고, B는 다른 클라이언트에도 이득입니다.
    • types/config 분할과 무관하니 close-don't-rebase 대상이 아닙니다. 패치 PR이 나오면 위 픽스처와 400/502 매핑 회귀를 게이트로 두면 됩니다.

    이 댓글은 grok-bot이 작성했습니다

  4. isarhoo commented on Sep 3, 2026

    @isarhoo

    Additional reproduction: the same delegated-task bootstrap/history issue also affects the Kiro adapter in OpenCodex 2.40.0.

    Environment:

    • Client: Codex App on Windows
    • OpenCodex: 2.40.0
    • Model: kiro/gpt-5.6-terra
    • Reasoning effort: high

    Observed behavior:

    1. A normal task started directly with the same Kiro model succeeds.
    2. A native Codex worker subagent configured with the same model also succeeds (HTTP 200, OpenCodex log sendCount: 1).
    3. A delegated B task created from another Codex App task fails before its first model response.

    The delegated task reports:

    unexpected status 502 Bad Gateway: Provider unreachable:
    undefined is not an object (evaluating 'id.replace')
    

    OpenCodex usage logs record the failed requests with:

    provider: kiro
    adapter: kiro
    status: 502
    sendCount: 0
    upstreamError: Provider unreachable: undefined is not an object (evaluating 'id.replace')
    

    sendCount: 0 confirms that these requests fail during local history conversion before Kiro receives an upstream request.

    The likely Kiro-specific crash path is:

    • responses/parser.ts treats call.call_id as a string and assigns it to OcxToolCall.id without a runtime check.
    • adapters/kiro.ts later calls normalizeToolId(tc.id).
    • adapters/kiro-wire.ts executes id.replace(...).

    If the delegated bootstrap item has no call_id, undefined reaches normalizeToolId, which matches the observed error. This looks like the same missing-ID delegated history shape described here, but exposed as a Kiro adapter crash instead of an Ollama orphan-result error.

    It would be useful to normalize or reject missing tool-call IDs before provider-specific history conversion, and to report this as a local invalid-history error rather than Provider unreachable.

  5. Ingwannu commented on Sep 3, 2026

    @Ingwannu
    Owner

    @lidge-jun The new Kiro reproduction is a meaningful second failure path and should keep this issue open. sendCount: 0 plus the exact id.replace exception does place the failure before upstream transport: src/adapters/kiro.ts calls normalizeToolId(tc.id) / normalizeToolId(tr.toolCallId), and src/adapters/kiro-wire.ts currently assumes a string and calls .replace() immediately.

    One part of the proposed root cause is not yet proven on current dev, however. The current Responses input schema requires a non-empty string call_id for function_call, function_call_output, custom_tool_call, and custom_tool_call_output. A raw missing call_id should therefore be rejected before the parser constructs an OcxToolCall. That means the undefined id may be entering through a different replay/continuation conversion path, or the 2.40.0 path differs from current dev.

    Please provide one redacted failing input item shape only: type, role, whether id and call_id are present and their types, and the surrounding item-type order. Do not include prompt text, tokens, arguments, or output. A current-dev reproduction would also distinguish an already-fixed schema boundary from a still-live path.

    The eventual fix should not merely change this TypeError into another 502. It needs to define the delegated-bootstrap policy consistently for both Kiro and Ollama: either safely drop a proven empty bootstrap shell, synthesize a deterministic pair where the wire permits it, or reject it at the common boundary with a stable 400. The regression must cover the real delegated history shape and prove no upstream send occurs for malformed data while valid delegated turns still send once.

  6. isarhoo commented on Sep 3, 2026

    @isarhoo

    Tested this against current dev at b15cbb2c3dbbb747b103f0d01b501c67e09a1f7c (2.41.0) with an isolated server.

    The redacted item shape from the original failed Codex task rollout is:

    1. type: message
       role: developer
       id: present, string
       call_id: absent
    
    2. type: message
       role: user
       id: present, string
       call_id: absent
    
    3. type: function_call_output
       role: absent
       id: present, string
       call_id: absent
    

    No prompt text, arguments, output, tokens, or credentials are included above.

    I then reproduced the same shape through POST /v1/responses on current dev using the Kiro adapter:

    HTTP status: 400
    error: undefined is not an object (evaluating 'id.replace')
    sendCount: 0
    

    A direct parser/builder check shows the intermediate state:

    parseRequest: succeeded
    tool result toolCallId type: undefined
    Kiro buildRequest: failed in normalizeToolId with id.replace
    

    A control request using a valid delegated-history pair (function_call and function_call_output sharing a non-empty string call_id) reached transport exactly once:

    sendCount: 1
    

    The control used a synthetic invalid bearer token, so the upstream returned the expected 403 invalid_api_key; this was only to verify that the valid shape crosses the outbound transport boundary.

    The current-dev schema boundary appears bypassable because inputItemSchema ends with the loose fallback z.object({ type: z.string() }).loose(). A malformed item with the recognized type function_call_output fails the specific schema but matches that fallback. parseRequest subsequently branches on effectiveType === "function_call_output", constructs a tool result with an undefined toolCallId, and the Kiro adapter passes it to normalizeToolId().

    So this path is still live on current dev: malformed delegated history does not get rejected at the schema boundary, and it produces the exact pre-transport id.replace failure.

  7. isarhoo commented on Sep 4, 2026

    @isarhoo

    Follow-up with an actual Codex Desktop delegated task (not a synthetic /v1/responses fixture).

    Environment:

    OpenCodex dev: 8b60e4c447191b71b32e4ad5dcb52407cc5f1d0d (2.42.0)
    Codex CLI: 0.153.0
    originator: Codex Desktop
    source: vscode
    thread_source: agent_created_thread
    requested model: kiro/gpt-5.6-terra
    reasoning effort: high
    

    I added temporary, opt-in structural logging around the Responses request path. It records field presence and types only; prompt text, tool output, tokens, and credentials are excluded.

    For the real delegated task, the raw request reaching OpenCodex contained:

    {
      "stage": "raw",
      "previous_response_id_present": false,
      "headers": {
        "x-codex-parent-thread-id": false,
        "thread-id": true,
        "session-id": true
      },
      "items": [
        { "type": "message", "role": "developer", "id_present": true },
        { "type": "message", "role": "user", "id_present": true },
        {
          "type": "function_call_output",
          "id_present": true,
          "call_id_present": false,
          "name": "create_thread",
          "namespace": "codex_app",
          "output_type": "string",
          "output_length": 459
        }
      ]
    }

    Immediately after expandPreviousResponseInput, the third item was unchanged:

    {
      "stage": "expanded",
      "previous_response_id_present": false,
      "type": "function_call_output",
      "id_present": true,
      "call_id_present": false
    }

    After parseRequest, it became:

    {
      "stage": "parsed",
      "role": "toolResult",
      "toolCallId_present": false,
      "toolCallId_type": "undefined",
      "toolName": ""
    }

    The capture produced 19 complete raw -> expanded -> parsed sets. Eighteen had this same delegated-task shape during the failed turn.

    This confirms that the malformed function_call_output already arrives in the raw request emitted for an agent_created_thread; OpenCodex's previous-response expansion does not introduce it. The parser then converts it to a tool result with an undefined toolCallId.

    The isolated task's first provider turn ultimately returned 401 OAuth authentication failed, so this run did not independently reach the later Kiro id.replace crash. However, the earlier current-dev fixture already demonstrated the next steps in the same chain:

    parser.ts:738       toolCallId: output.call_id
    kiro.ts:661         normalizeToolId(tr.toolCallId)
    kiro-wire.ts:33     id.replace(...)
    

    One fix caveat: using only call_id ?? id avoids the immediate undefined value, but the delegated bootstrap history still has no matching function_call. A complete fix likely needs an explicit policy for unpaired bootstrap tool results, plus a regression test using this agent_created_thread shape.

  8. lidge-jun commented on Sep 4, 2026

    @lidge-jun
    Owner

    Landed via #3471 at 4968d0f.

    dev now rejects unpaired tool results (missing/empty call_id) on the translating Responses path so kiro/ollama/anthropic no longer see undefined. Passthrough and routed compaction stay on their own degrade path.

    이 댓글은 grok-bot이 작성했습니다

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

    providerProvider adapters, OpenAI-compat presets, upstream API quirksprovider-compatibilityProvider compatibility reportstoolstool_calls, MCP, web-search / sidecar tools

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions