Skip to content

fix(adapters): convert Codex agent_message for every routed Responses destination - #3917

Closed
mashfromband wants to merge 1 commit into
lidge-jun:devfrom
mashfromband:fix/routed-agent-message-normalize
Closed

fix(adapters): convert Codex agent_message for every routed Responses destination#3917
mashfromband wants to merge 1 commit into
lidge-jun:devfrom
mashfromband:fix/routed-agent-message-normalize

Conversation

@mashfromband

@mashfromband mashfromband commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Fixes #3911.

Summary

A Codex thread that has used sub-agents cannot be routed. Codex writes every sub-agent
reply into the rollout as an agent_message item, so it is replayed in the input of
every later turn of that thread, and agent_message is private to the ChatGPT Codex
backend's schema. A routed Responses destination answers the whole request with

422 {"error":"Failed to deserialize the JSON body into the target type: input[31]: unknown item type \"agent_message\"; expected one of: message, reasoning, function_call, ... , compaction"}

422 is a client error, so nothing fails over (attempts has one entry) and the status
reaches the client verbatim. In the report behind #3911 the same conversation failed 12
times in 15 minutes on xai/grok-4.6 picked by a combo route.

The plaintext conversion for this already exists; it was scoped to the OpenCode Go
destination. Nothing about the rejection is destination-specific, so this widens the
conversion to every destination with authMode other than "forward":

  • forward destinations are unchanged. That is the only schema that understands the
    item, and the existing forward tests still assert it arrives untouched.
  • Encrypted and unknown part types are unchanged. They keep the current fail-closed
    path; the encrypted v2 task surface still owns them through
    unreadable_encrypted_agent_task and the opt-in recovery route. This PR does not touch
    that policy.
  • isOpenCodeGo existed only to scope this one call, so it is removed with it, and the
    helper plus its test file move to destination-neutral names.

One behavioral consequence worth naming: opaque-blob recovery repairs an undecryptable
part into [encrypted content omitted], which leaves the item entirely plaintext. On a
routed retry that item is now converted too — which is what lets the retry be accepted at
all, since re-sending the repaired-but-still-private item would hit the same 422. The four
assertions in tests/responses/responses-opaque-blob-recovery.test.ts that pinned the old
shape are updated to the converted one (their fixture provider is authMode: "key", i.e.
routed); the authMode: "forward" case in the same file is untouched and still passes.

docs-site said the conversion "is scoped to that destination ... other Responses
destinations keep their input unchanged", so both reference pages are updated.

Verification

Rebased onto dev at 8bc9e4e; the tested tree is 21eaaf9.

  • bun run typecheck — passed.
  • bun run privacy:scan — passed (Privacy scan passed).
  • git diff --check — clean.
  • bun test tests/adapters/routed-agent-messages.test.ts tests/responses/responses-opaque-blob-recovery.test.ts tests/test-layout.test.ts tests/test-layout-tooling.test.ts82 pass, 0 fail, 917 expect() calls. That is the moved/renamed suite, the file whose expectations this PR changes, and both layout guards.
  • Reverting only the openai-responses.ts call-site line reproduces the old behavior in the new an arbitrary routed destination converts too test.
  • Live reproduction against a real provider, through a running ocx, with only input[1] differing between requests: message → 200, agent_message (plaintext) → 422, agent_message with encrypted_content422. After this change the same routed request returns 200, and the authMode: "forward" destination still receives the item unchanged.

bun run test (full suite) reached this repository's own 900-second runner limit and
exited 124 on my machine, which was not idle — the runner's own message names that cause.
The failures it did print before the limit were 5000 ms timeouts of server/API tests plus
one EICACLS Windows ACL error, and each one I re-ran in isolation passed. I am not
reporting the full local suite as a pass
; the cross-platform CI on this PR is the signal
I am relying on for it.

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed.
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults.

Security note: this sends strictly less to a routed destination than before in the
encrypted case (nothing changes — the item is still not converted) and the same bytes in
the plaintext case, re-labelled. author/recipient were already on the wire inside the
item and remain readable text. No decryption is attempted anywhere in this change.

Review readiness checklist

  • All CI tests are green on my local testing.
  • I pushed my PR to the latest dev commit.
  • I resolved all correct Codex and CodeRabbit findings.
  • My PR is ready for review.

Summary by CodeRabbit

  • New Features

    • Routed Responses destinations now support Codex agent messages across all non-forward providers, not only a specific destination.
    • Agent messages are presented as user messages with author and recipient context when applicable.
    • Opaque or undecryptable message content continues to recover with an omission marker.
  • Documentation

    • Updated adapter and provider guidance to describe routed agent-message handling, supported provider scope, and related recovery behavior.
    • Clarified that forward-authenticated providers preserve agent messages unchanged.

@coderabbitai

coderabbitai Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Team

Run ID: 1a6db42c-60f5-44e8-8d7a-e829d1b69375

📥 Commits

Reviewing files that changed from the base of the PR and between 273a3ab and 21eaaf9.

📒 Files selected for processing (8)
  • docs-site/src/content/docs/reference/adapters.md
  • docs-site/src/content/docs/reference/configuration/providers.md
  • scripts/test-layout/layout.json
  • src/adapters/openai-responses.ts
  • src/adapters/routed-agent-messages.ts
  • tests/adapters/routed-agent-messages.test.ts
  • tests/fixtures/test-layout-expected.json
  • tests/responses/responses-opaque-blob-recovery.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.


📝 Walkthrough

Walkthrough

The OpenAI Responses adapter now converts plaintext agent_message items for every non-forward provider. The URL-specific OpenCode Go helper was removed. Tests, layout mappings, and documentation now use the generalized routed-agent message behavior.

Changes

Routed agent message handling

Layer / File(s) Summary
Generalized agent message normalization
src/adapters/routed-agent-messages.ts
Renames the helper and applies it to routed destinations. Plaintext agent_message items become message items with role user; ciphertext and unknown parts retain fail-closed behavior.
Adapter wiring and behavior validation
src/adapters/openai-responses.ts, tests/adapters/routed-agent-messages.test.ts, tests/responses/responses-opaque-blob-recovery.test.ts, scripts/test-layout/layout.json, tests/fixtures/test-layout-expected.json
buildRequest normalizes all non-forward providers. Tests cover arbitrary routed URLs, unchanged bodies, Go-like URLs, and opaque-blob recovery. Test layout entries use the routed-agent message name and adapter domain.
Documentation of routed scope
docs-site/src/content/docs/reference/adapters.md, docs-site/src/content/docs/reference/configuration/providers.md
Documents the generalized non-forward scope and the repeated 422 unknown item type "agent_message" failure for routed destinations.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to 21eaa

Plaintext sub-agent messages are converted for routed Responses destinations, preventing repeated schema-rejection failures while preserving forward-provider and fail-closed behavior. No actionable current-head merge risk remains.

Sequence Diagram(s)

sequenceDiagram
  participant Codex
  participant buildRequest
  participant normalizeRoutedAgentMessages
  participant RoutedResponsesDestination
  Codex->>buildRequest: send request with agent_message history
  buildRequest->>normalizeRoutedAgentMessages: normalize when authMode is not forward
  normalizeRoutedAgentMessages->>buildRequest: return message/user items
  buildRequest->>RoutedResponsesDestination: send normalized request
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 5 functions across 4 files. (4 skipped: 4… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The implementation satisfies issue #3911. It normalizes plaintext agent_message items for all non-forward Responses destinations, preserves forward behavior, retains fail-closed handling for encrypted…
Out of Scope Changes check ✅ Passed The changes remain within scope for issue #3911. The adapter implementation, generalized helper, regression tests, layout fixtures, opaque-blob recovery expectations, and documentation all support the…
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: converting Codex agent_message items for all routed Responses destinations.
Full details: Docstring Coverage

Explanation

Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 5 functions across 4 files. (4 skipped: 4 unsupported.)

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

@github-actions github-actions Bot added the bug Something isn't working label Sep 7, 2026
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

⏳ DRAFT

  • review readiness checklist open (1/4 boxes ticked).

What to do

  • Tick all four boxes in the PR description once you're done (currently 1/4).

Review readiness checklist

  • ⬜ All CI tests are green on my local testing.
  • ✅ I pushed my PR to the latest dev commit.
  • ⬜ I resolved all correct Codex and CodeRabbit findings.
  • ⬜ My PR is ready for review.

1/4 boxes ticked.

This PR stays in draft until every box above is ticked.

Hygiene

Deterministic PR hygiene checks passed.

@lidge-jun

Copy link
Copy Markdown
Owner

리뷰 · 우선순위 73 / 80

이 PR는 Codex가 서브에이전트를 쓴 뒤 롤아웃에 남기는 비공개 입력 타입 agent_message 때문에, 같은 스레드를 콤보/라우티드 Responses 목적지로 보내면 매 턴 422로 죽는 구멍을 막는 수정이다. 지금 dev HEAD(273a3ab86, 2.47.0, #3913 릴리즈 트레인 기록 직후)에서는 src/adapters/openai-responses.tsauthMode !== "forward"이어도 isOpenCodeGo(provider.baseUrl)일 때만 normalizeOpenCodeGoAgentMessages를 돌린다. 그래서 OpenCode Go가 아닌 xAI/Grok·기타 키 모드 목적지는 그대로 비공개 아이템을 받고 unknown item type "agent_message"로 본문 전체를 거절한다. 422는 클라이언트 오류라 페일오버도 안 타고, Codex가 서브에이전트 히스토리를 매 턴 다시 넣으니 스레드가 끝날 때까지 반복된다. 이 브랜치는 변환을 목적지 이름과 무관하게 !forward 전부로 넓히고, 헬퍼를 opencode-go.ts에서 routed-agent-messages.ts로 이름만 바꿔 옮긴다. forward는 손대지 않고, 암호문/알 수 없는 part는 기존처럼 변환하지 않아 unreadable_encrypted_agent_task 복구 경로와 경계를 지킨다. opaque-blob 복구가 생략 마커만 남긴 뒤 라우티드 재시도에서 공개 message로 바뀌는 쪽도 테스트 기대를 맞춰 두었다. 문서 두 장도 “Go만 변환” 서술을 “라우티드 전부”로 고쳤다.

증상 자체는 릴리즈 트레인 스냅샷이 말하는 콤보/라우팅 안정성 방향과 정면으로 맞고, 고치는 코드 양도 작다. 다만 PR이 아직 draft이고 본문이 로컬 풀 스위트 900초 타임아웃을 솔직히 적었다. 가벼운 hygiene/enforce-target만 초록인 상태라, Ready로 올린 뒤 크로스 플랫폼 CI가 한 바퀴 도는 게 머지 게이트다. 연결된 #3911은 템플릿 없이 자동으로 not_planned 닫힌 이슈라, 머지 전에 이슈를 다시 열거나 Fixes 링크를 정리할지가 남는다. 비슷한 표면의 열린 이슈 #3907(V2 child agent_message → 부모 422)과도 겹치는지 한 줄만 확인하면 좋다.

src/adapters/openai-responses.ts 호출부 - if (!forward) outBody = normalizeRoutedAgentMessages(outBody) 로 바뀌어 Go URL 게이트가 사라진다. 프로덕션에서 isOpenCodeGo를 쓰던 곳은 이 한 줄뿐이라 삭제해도 다른 세션 헤더 로직은 안 깨진다.
src/adapters/routed-agent-messages.ts - 변환 본문은 HEAD의 Go 헬퍼와 같고, 평문 input_text/image/file만 공개 user message로 바꾼다. 암호문 혼합은 그대로 둔다.
tests/adapters/routed-agent-messages.test.ts - 임의 라우티드 URL도 변환된다는 회귀와, 위조 Go-like URL이 세션 아이덴티티를 받지 않는다는 점이 잠겨 있다.
tests/responses/responses-opaque-blob-recovery.test.ts - 복구 후 재시도 body가 변환된 message를 기대하도록 바뀌었다. authMode: "forward" 케이스는 그대로라 의도된 동작 변경이다.
PR 상태 draft - CodeRabbit도 draft라 스킵했다. Ready + 풀 CI 전에는 머지하지 않는 편이 맞다.
#3911 - Fixes로 연결되어 있지만 이슈는 템플릿 봇이 not_planned로 닫았다. 머지 시 이슈 재오픈/재기록 여부가 필요하다.

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

  • draft를 Ready로 올린 뒤 풀 CI(특히 adapters/responses 관련)를 머지 게이트로 삼을지
  • 자동 닫힌 #3911을 다시 열어 Fixes를 살릴지, 새 이슈로 바꿀지
  • 열린 [xAI/Grok Responses] V2 child result agent_message causes parent HTTP 422 #3907(V2 child agent_message 부모 422)이 이 PR로 같이 줄어드는지, 별 패치가 남는지
  • opaque-blob 복구 후 라우티드 재시도가 공개 message로 바뀌는 문서/릴리즈 노트 한 줄을 남길지

너의 추천
초안을 Ready로 바꾸고 풀 CI가 초록이면 머지한다. 변환 범위가 정확하고 forward/암호문 경계를 지키며, 콤보로 서브에이전트 스레드를 돌리는 실제 422 루프를 끊는다. 머지 전에 #3911 상태와 #3907 겹침만 한 번 확인하면 된다.

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

@mashfromband
mashfromband marked this pull request as ready for review September 7, 2026 15:20
@github-actions
github-actions Bot marked this pull request as draft September 7, 2026 15:20
@mashfromband
mashfromband marked this pull request as ready for review September 7, 2026 15:21
@github-actions
github-actions Bot marked this pull request as draft September 7, 2026 15:22
… destination

`agent_message` is Codex's private multi-agent input item and exists only in the
ChatGPT Codex backend's schema. Codex writes every sub-agent reply into the
rollout, so it is replayed in the `input` of every later turn of that thread. A
routed Responses destination answers the whole body with
`422 unknown item type "agent_message"`, and 422 is a client error nothing fails
over, so the thread stays broken until the history is dropped.

The plaintext conversion already existed but was scoped to the OpenCode Go
destination. Nothing about the rejection is destination-specific, so the
conversion now applies to every destination with `authMode` other than
"forward". Forward destinations keep the item unchanged, and genuine ciphertext
and unknown part types keep their existing fail-closed path; the encrypted v2
task surface still owns those through `unreadable_encrypted_agent_task` and the
opt-in recovery route.

`isOpenCodeGo` existed only to scope this call and is removed with it. The
helper and its tests move to destination-neutral names.

Opaque-blob recovery repairs an undecryptable part into an omission marker,
which leaves the item entirely plaintext; on a routed retry it is now converted
too, which is what lets that retry be accepted at all.

Fixes lidge-jun#3911
@mashfromband
mashfromband force-pushed the fix/routed-agent-message-normalize branch from 21eaaf9 to 2430724 Compare September 7, 2026 15:22
lidge-jun added a commit that referenced this pull request Sep 7, 2026
…3942)

* docs(devlog): plan the workstream-A Responses compatibility stack

Roadmap for landing four Responses-compatibility changes on dev as one
dependent branch chain whose tip carries all of them, so a single CI run
certifies the set: PR #3906 (Muse Spark Free web_search strip), PR #3886
(Spark Responses Lite header), issue #3922 (Claude tool strict default,
new work), and PR #3917 (routed agent_message conversion).

Each phase doc carries exact path:line anchors and before/after diffs.
Three rounds of independent audit corrected the Layer 2 HTTP/WebSocket
coverage boundary, the Claude compatibility semantics, the Layer 3 test
that an added strict field breaks, the converted authMode set, and the
landing proof for each GitHub merge method.

* fix(responses): strip web_search fields for Muse Spark Contributor Free tiers

The -free tiers ride the same Zen Responses wire with the same gateway
contract, so a Codex web_search carrying search_content_types /
indexed_web_access 400s for them exactly like the paid tiers.

(cherry picked from commit 11c498b)

Co-authored-by: MohamadSabree8 <mohamadsabree8@users.noreply.github.com>

* test(responses): cover nested and preview cases for Muse Spark Free tiers

The carried fix covered a top-level web_search tool for the two Contributor
Free ids. The sanitizer also walks input[].additional_tools.tools, and it
must leave web_search_preview alone, so pin both for the free ids the way
the paid ids are already pinned.

Co-authored-by: MohamadSabree8 <mohamadsabree8@users.noreply.github.com>

* fix(responses): disable Lite transport for Spark

The canonical backend starts a Spark SSE response with the Responses Lite header but closes it before a terminal event, which the adapter correctly surfaces as adapter_eof. The identical request completes without that header.

Select the compatibility exception from the final wire model and remove both caller-provided and statically configured Lite headers only for gpt-5.3-codex-spark. Other canonical models retain the existing metadata path.

Regression: cover Spark suppression and unaffected Sol forwarding at the adapter boundary.

(cherry picked from commit 83c1d9b)

Co-authored-by: R <53855466+cb8010d6@users.noreply.github.com>

* docs(devlog): note the layer-1 line drift in the layer-2 anchors

Layer 1 inserts two lines above the canonical-forward block, so the phase
doc now states both the pinned-base line numbers and where the same code
sits on this branch.

* fix(claude): carry the source strict intent into translated Responses tools

Anthropic enables strict tool use by setting strict: true, while the
Responses API reads an omitted strict as permission to normalize the schema
into strict mode. Translating a Claude Code tool without the field therefore
made every optional input_schema parameter behave as required upstream, so a
tool call that omitted one failed even though the client never asked for
strict mode.

Emit the field from the source tool: an explicit true or false is preserved,
an omitted one becomes an explicit false, and a non-boolean value cannot opt
the tool into strict mode. The input_schema is forwarded unchanged, hosted
web_search leaves the translator before this branch, and native Anthropic
passthrough never reaches it.

The existing exact expectation on the translated Read tool gains the field.
The new regression asserts the three cases on the serialized outbound body
built by a real Responses adapter, because parsed._rawBody is the
translator's own object and reading it back would prove nothing about the
wire.

Closes #3922.

* fix(adapters): convert Codex agent_message for every routed Responses destination

`agent_message` is Codex's private multi-agent input item and exists only in the
ChatGPT Codex backend's schema. Codex writes every sub-agent reply into the
rollout, so it is replayed in the `input` of every later turn of that thread. A
routed Responses destination answers the whole body with
`422 unknown item type "agent_message"`, and 422 is a client error nothing fails
over, so the thread stays broken until the history is dropped.

The plaintext conversion already existed but was scoped to the OpenCode Go
destination. Nothing about the rejection is destination-specific, so the
conversion now applies to every destination with `authMode` other than
"forward". Forward destinations keep the item unchanged, and genuine ciphertext
and unknown part types keep their existing fail-closed path; the encrypted v2
task surface still owns those through `unreadable_encrypted_agent_task` and the
opt-in recovery route.

`isOpenCodeGo` existed only to scope this call and is removed with it. The
helper and its tests move to destination-neutral names.

Opaque-blob recovery repairs an undecryptable part into an omission marker,
which leaves the item entirely plaintext; on a routed retry it is now converted
too, which is what lets that retry be accepted at all.

Fixes #3911

(cherry picked from commit 2430724)

Co-authored-by: mashfromband <matsumoto.yukuhashi@gmail.com>

* test(adapters): pin an OAuth destination and narrow the routed-422 wording

The carried conversion is gated on authMode rather than on the destination
URL, but its regression only exercised key and forward. The reported
xAI/Grok failure is an OAuth pool destination, so pin one: a future
narrowing of the gate back toward key-only would otherwise pass unnoticed.

Also narrow the two reference pages. The conversion is justified by the
destinations that actually reported the 422; authMode is an authentication
setting, so it cannot establish what every custom upstream accepts.

Co-authored-by: mashfromband <matsumoto.yukuhashi@gmail.com>

* docs(devlog): drop a trailing blank line in the layer-1 phase doc

---------

Co-authored-by: Codex <a@b.com>
Co-authored-by: MohamadSabree8 <mohamadsabree8@users.noreply.github.com>
Co-authored-by: R <53855466+cb8010d6@users.noreply.github.com>
Co-authored-by: mashfromband <matsumoto.yukuhashi@gmail.com>
@lidge-jun

Copy link
Copy Markdown
Owner

The work from this pull request has landed on dev through the workstream-A compatibility stack in #3942, squash-merged as a13041740.

Your commit was carried onto the stack with its original authorship intact: the carried commit kept you as its author and its cherry picked from provenance line, and a Co-authored-by trailer naming you is present on the squash commit that landed, so the contribution is attributed to you on the merged record.

Two additions on top of your change: an OAuth-destination regression, since the gate reads authMode and the reported xAI case is an OAuth pool destination rather than a key-auth one, and a narrowing of the two reference pages from "any routed destination" to the destinations actually reported — authMode is an authentication setting and cannot establish what every custom upstream accepts.

Verified on a fetched origin/dev: git diff between the stack tip and dev is empty across every path the stack touched. Closing this as landed rather than superseded. Thank you for the fix.

@lidge-jun lidge-jun closed this Sep 7, 2026
@lidge-jun lidge-jun added the landed-via-maintainer Original PR closed after landing via a maintainer merge train label Sep 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working landed-via-maintainer Original PR closed after landing via a maintainer merge train

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants