Skip to content

fix(responses): bundle lane E — combo resend safety, WebSocket replacement, goal helpers, Devin retry delays - #5675

Merged
lidge-jun merged 8 commits into
devfrom
codex/260923-bundle-e-responses-combo
Sep 23, 2026
Merged

lidge-jun merged 8 commits into
devfrom
codex/260923-bundle-e-responses-combo

Conversation

@lidge-jun

@lidge-jun lidge-jun commented Sep 23, 2026 •

Copy link
Copy Markdown
Owner

Summary

Bundle lane E (responses and combo resilience) from the 260923 PR consolidation round. One branch, one commit per carried PR, every carried commit credited with Co-authored-by.

Commit Carries What changes
fix(devin): accept generated approximate retry delays #5629 (@luvs01) The shared retry-delay parser accepts the generated retry after ~180s marker after Retry-After. Review fixes: a repeated marker (~1 minute ~30 seconds) now rejects the hint instead of shortening it, and a stale comment is corrected.
fix(responses): restore code-mode goal helpers #5659 (@Ingwannu) Routed create_goal / get_goal / update_goal (bare or default.-prefixed) compile to the matching nested helper inside a declared code-mode exec. The helper list is closed; a declared bare goal tool keeps its identity; no exec still fails closed. Review fixes: guard asserted on the raw wire name, unlisted-name and bare-precedence cases, authorization comments, guide sentence.
fix(responses): stop combo failover once a request has spent its ambiguous replacement #5646 (@FredAmartey) Once the retryOnReset replacement is spent, a zero-output failure of the replacement's 200 stops the combo (and the direct opaque-blob rebuild) instead of sending the turn a third time. Carried first of the pair.
fix(responses): let retryOnReset replace a Codex WebSocket send that died unanswered #5633 (@FredAmartey) For opted-in providers, a Codex WebSocket that dies under its create frame before any Responses event may use the request's single replacement. Carried after #5646 so the new row inherits the spent-grant stop. Review fixes: duplicate import from the pair merge removed; the failover doc states the 2xx contract once as settled.
fix(combo): fail over undeclared zero-output tools #5489 (@AaronZ345), net diff A runTurn adapter's undeclared first tool call in a combo attempt becomes a pre-commit 502 so the combo hops with the same catalog. Review fix: the non-streaming path now respects a replay-unsafe heartbeat like the streaming preflight (test fails without it). Combos guide row added in all 8 locales.
fix(combo): keep failures after a replay-unsafe side effect on their child lane fix From the adversarial review: an adapter error or empty end after a replay-unsafe heartbeat is now marked non-replayable, so a combo no longer repeats a side effect Cursor already ran (pre-existing on dev); and a scope bridged onto a budget without ambiguousResendSpent now reports a grant it claimed, closing a third-send path.

Closes #5495.

Supersedes #5629.
Supersedes #5659.
Supersedes #5646.
Supersedes #5633.
Supersedes #5489.

Related to #5407: this covers its Responses path only; the reported Claude Code / Anthropic Messages path is unchanged, so the issue stays open.

Not included, with reasons (details in devlog/_plan/260923_bundle_lane_e/010_decisions.md):

Co-authored-by: Epinephrine 27862058+luvs01@users.noreply.github.com
Co-authored-by: ingwannu 186453546+Ingwannu@users.noreply.github.com
Co-authored-by: Fred Amartey 43480311+FredAmartey@users.noreply.github.com
Co-authored-by: Yu Zhang 34849476+AaronZ345@users.noreply.github.com

Verification

  • Focused suite over every touched area and its consumers: bun test on 33 files (retry delay, Devin stated reset, code-mode goal helpers and undeclared-tool guard, WebSocket ambiguous resend, replay refusal parity, ambiguous-resend composition, execution-budget permits, routing policy fallback, upstream retry, reset replay, opaque-blob recovery, combo failover e2e, combo zero-output failover, run-turn queue, bridge, combo stream preflight, test layout, file-size ratchet, structure SSOT) — 1003 pass, 0 fail, on the head rebased onto 7dd1db22ac.
  • New regression cases fail without their fix: non-streaming undeclared tool after a replay-unsafe heartbeat; streaming and non-streaming error after a replay-unsafe heartbeat; bridged parent without a spent flag.
  • bun x tsc --noEmit (exit 0), bun run structure:check, bun run privacy:scan, git diff --check.
  • Full local suite not run by design: the round's owner runs bun run test once after every lane has landed. Exact-head hosted CI on this PR is the merge evidence.
  • Reviews: per-PR soundness reviews and two adversarial reviews of the final diff by gpt-6-sol; one adversarial FAIL (two blockers) was fixed in the last commit and re-checked PASS.

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.

Summary by CodeRabbit

  • New Features

    • Undeclared first tool calls in eligible Responses turns can now trigger failover to another target before output or replay-unsafe side effects occur. After either boundary, the refusal remains final.
    • Eligible ChatGPT WebSocket failures before the first Responses event can receive a single HTTP replacement request when provider retry settings allow it.
    • Routed code-mode calls for goal helpers are now handled through exec when the request declares exec.
    • Devin adapter retries now recognize approximate reset delays and honor the stated wait when it fits within the configured allowance.
  • Documentation

    • Updated provider retry and combo failover guidance across supported languages.

@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner September 23, 2026 10:22
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-23T10:29:22.586306Z d166470 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

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

Copy link
Copy Markdown
Contributor

✅ Deterministic PR hygiene checks passed.

@coderabbitai

coderabbitai Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: Repository: lidge-jun/opencodex/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 2743acdb-e099-4659-a851-574a21e23197

📥 Commits

Reviewing files that changed from the base of the PR and between a1dba2c and 056f9cd.

📒 Files selected for processing (55)
  • devlog/_plan/260923_bundle_lane_e/000_overview.md
  • devlog/_plan/260923_bundle_lane_e/010_decisions.md
  • devlog/_plan/260923_bundle_lane_e/020_carry.md
  • devlog/_plan/260923_bundle_lane_e/030_verify.md
  • docs-site/src/content/docs/fr/guides/combos.md
  • docs-site/src/content/docs/fr/reference/configuration/providers.md
  • docs-site/src/content/docs/guides/codex-integration.md
  • docs-site/src/content/docs/guides/combos.md
  • docs-site/src/content/docs/ja/guides/combos.md
  • docs-site/src/content/docs/ja/reference/configuration/providers.md
  • docs-site/src/content/docs/ko/guides/combos.md
  • docs-site/src/content/docs/ko/reference/configuration/providers.md
  • docs-site/src/content/docs/reference/adapters.md
  • docs-site/src/content/docs/reference/configuration/providers.md
  • docs-site/src/content/docs/reference/configuration/server.md
  • docs-site/src/content/docs/ru/guides/combos.md
  • docs-site/src/content/docs/ru/reference/configuration/providers.md
  • docs-site/src/content/docs/tr/guides/combos.md
  • docs-site/src/content/docs/tr/reference/configuration/providers.md
  • docs-site/src/content/docs/zh-cn/guides/combos.md
  • docs-site/src/content/docs/zh-cn/reference/configuration/providers.md
  • docs-site/src/content/docs/zh-tw/guides/combos.md
  • docs-site/src/content/docs/zh-tw/reference/configuration/providers.md
  • scripts/test-layout/layout.json
  • src/adapters/devin/cloud-direct/chat.ts
  • src/adapters/run-turn-queue.ts
  • src/lib/request-execution-budget.ts
  • src/lib/request-resend-gate.ts
  • src/lib/retry-delay.ts
  • src/lib/upstream-retry.ts
  • src/responses/code-mode-helper-compat.ts
  • src/server/responses/codex-ws-exchange.ts
  • src/server/responses/codex-ws-wire.ts
  • src/server/responses/core-combo.ts
  • src/server/responses/core-opaque-recovery.ts
  • src/server/responses/passthrough-dispatch.ts
  • src/server/responses/policy-fallback.ts
  • src/server/responses/request-send-budget.ts
  • src/server/responses/run-turn-execution.ts
  • src/types/tools.ts
  • structure/adapters/registry.md
  • structure/runtime.md
  • structure/transports/responses-failover.md
  • structure/transports/responses-wire-shapes.md
  • tests/adapters/run-turn-queue.test.ts
  • tests/fixtures/test-layout-expected.json
  • tests/lib/ambiguous-resend-composition.test.ts
  • tests/providers/devin-stated-reset-retry.test.ts
  • tests/responses/responses-code-mode-goal-helpers.test.ts
  • tests/responses/ws-ambiguous-resend.test.ts
  • tests/routing/routing-policy-fallback.test.ts
  • tests/server/replay-refusal-parity.test.ts
  • tests/server/retry-after-429.test.ts
  • tests/server/retry-delay-hardening.test.ts
  • tests/server/server-combo-zero-output-failover.test.ts
 ____________________________________________________________________________________________________________________________________________________
< Critically analyze what you read and hear. Don't be swayed by vendors, media hype, or dogma. Analyze information in terms of you and your project. >
 ----------------------------------------------------------------------------------------------------------------------------------------------------
  \
   \   \
        \ /\
        ( )
      .( o ).
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d166470b9c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/server/responses/passthrough-dispatch.ts Outdated
lidge-jun added a commit that referenced this pull request Sep 23, 2026
…grant

Review finding on #5675. With retryOnReset.replacements set to 2, a dead
Codex WebSocket spends the first grant on its HTTP replacement; if that
replacement resets before its head, the WebSocket row settled it as the
replay refusal at once, so the configured second replacement was never
reachable. The reset is the pre-header row again, so the row now asks the
same gate (and the send budget) once more and resends only when a grant
remains; with the default of one it still settles as the refusal. The loop is
bounded by the request's finite allowance.
lidge-jun and others added 8 commits September 23, 2026 19:33
Carries #5629. The shared retry-delay parser accepts the generated
"retry after ~180s" approximation marker after Retry-After, and the bounded
Devin replay re-evaluates the delay on every attempt within the existing
cumulative ceilings.

Folded review fixes: a repeated approximation marker ("~1 minute ~30
seconds") now rejects the hint instead of silently shortening it to the first
component, and the cloud-direct comment no longer claims the marker blocks
re-parsing.

Supersedes #5629.

Co-authored-by: Epinephrine <27862058+luvs01@users.noreply.github.com>
Carries #5659. Routed create_goal, get_goal and update_goal calls (bare or
with a provider-invented default. prefix) are accepted as nested helpers of a
genuinely declared code-mode exec and compiled to the matching
tools.<helper>(...) call instead of falling through to exec_command. A
genuinely declared bare goal tool keeps its identity, and a catalog that
declares neither the tool nor exec still fails closed.

Folded review fixes: the guard is asserted on the original unrestored wire
name, an unlisted helper-like name is proven not admitted, bare-goal
precedence is covered through full restoration, the authorization comments in
src/types/tools.ts name the goal helpers, and the codex integration guide
describes the repair.

Closes #5495. Supersedes #5659.

Co-authored-by: ingwannu <186453546+Ingwannu@users.noreply.github.com>
…guous replacement

Carries #5646. Once a request has spent its retryOnReset replacement, the
first send may already have run the turn, so a replacement that answers 200
and then fails with zero output must not be sent again. RequestExecutionBudget
now reports ambiguousResendSpent from the one shared grant; combo failover
stops when it is spent and settles the answer with the shared
settleOperatorReplacement rule (a resendable status becomes the replay
refusal, anything else keeps its status with the non-replayable marker). On
the direct path the streamed opaque-blob rebuild is skipped once the grant is
spent. Carried before #5633 so its WebSocket replacement row is never exposed
to the third-send gap.

Supersedes #5646.

Co-authored-by: Fred Amartey <43480311+FredAmartey@users.noreply.github.com>
…died unanswered

Carries #5633. A Codex WebSocket that opens and then closes or errors under
its create frame before any Responses event is the same unknown state as an
HTTP connection that resets before its head. For a provider that opted into
retryOnReset, the request-resend gate may now spend the request's single
replacement on it (one replacement per logical request, self-contained body
only). The exchange records the stage it reached; silence keeps its 504 and a
drop after a relayed event keeps its errored 200. Providers that have not
opted in are unchanged.

Carried after #5646, so the WebSocket replacement row inherits the spent-grant
stop. Folded review fixes: the duplicate settleOperatorReplacement import the
pair merge produced is removed, and responses-failover.md states the 2xx
replacement contract once as settled for all three replacement rows.

Supersedes #5633.

Co-authored-by: Fred Amartey <43480311+FredAmartey@users.noreply.github.com>
Carries #5489 (net diff; its upstream/dev merge commit is dropped). When a
runTurn adapter's first meaningful event in a combo attempt is a tool call the
current request did not declare, the existing fail-closed refusal is projected
as a pre-commit 502 so the combo can hop to the next target with the same tool
catalog. Chat Completions and Anthropic Messages inbound requests keep their
existing behaviour.

Folded review fix: the non-streaming path now applies the same boundary as the
streaming preflight. An undeclared tool call after a replay-unsafe heartbeat
(an adapter-side effect already ran) keeps the refusal on that child instead of
sending the turn to another target. New streaming and non-streaming cases prove
exactly one dispatch; the non-streaming one fails without the gate. The combos
guide gains the hop row in every locale and responses-failover.md records the
runTurn boundary.

Related to #5407 (covers its Responses path only; the reported Claude Code
Anthropic Messages path is unchanged). Supersedes #5489.

Co-authored-by: Yu Zhang <34849476+AaronZ345@users.noreply.github.com>
…child

Found by the lane's adversarial review of the carried #5646/#5489 changes.

- A runTurn adapter that emits a replay-unsafe heartbeat (it already ran a
  local side effect, as Cursor does) and then errors or ends empty before any
  output returned a plain 502, so a combo sent the turn to the next target and
  could repeat the side effect. Streaming and non-streaming paths now mark that
  502 non-replayable, and the combo stops on the child. This predates the
  carried commits; it sits on the same boundary structure/runtime.md states.
- A scope derived from a shape-compatible budget that implements
  claimAmbiguousResend but not ambiguousResendSpent reported "not spent" after
  it claimed the grant, which would let a combo hop on a zero-output 200 from
  the replacement (a third send). Grants claimed through the bridge are now
  latched per bridged parent and visible to every sibling scope.

Both are covered by new tests that fail without the fix.
…grant

Review finding on #5675. With retryOnReset.replacements set to 2, a dead
Codex WebSocket spends the first grant on its HTTP replacement; if that
replacement resets before its head, the WebSocket row settled it as the
replay refusal at once, so the configured second replacement was never
reachable. The reset is the pre-header row again, so the row now asks the
same gate (and the send budget) once more and resends only when a grant
remains; with the default of one it still settles as the refusal. The loop is
bounded by the request's finite allowance.
@lidge-jun

Copy link
Copy Markdown
Owner Author

리뷰 · 우선순위 56 / 80

이 PR은 같은 요청을 한 번 더 보내지 않게 막는 수정과, 코드 모드 목표 도구, Devin 재시도 시간을 dev 위에 한 브랜치로 모은 것입니다. 헤드 056f9cd8은 이미 dev에 머지되어 있습니다.

Devin이 retry after ~180s처럼 물결표로 기다리는 시간을 말하면 그 시간을 읽습니다. 물결표는 Retry-After 뒤에 한 번만 됩니다. ~1 minute ~30 seconds처럼 두 번 나오면 그 힌트는 버립니다.

코드 모드에서 exec만 선언된 요청이 create_goal, get_goal, update_goal을 부르면, 그 이름을 exec 안의 tools.그이름(...)으로 바꿉니다. default.가 붙어 있어도 같습니다. 요청이 그 도구를 직접 선언했으면 이름 그대로 둡니다. exec도 없고 그 도구도 없으면 거절합니다.

retryOnReset으로 대체 요청을 한 번 쓴 뒤, 그 대체가 글을 하나도 못 내고 끝나면 콤보는 다음 대상으로 넘기지 않습니다. 직접 경로의 opaque-blob 재구성도 하지 않습니다. 클라이언트가 다시 보낼 상태 코드는 재시도 거절로 바꿉니다.

Codex 웹소켓이 응답 이벤트를 받기 전에 끊기면, retryOnReset을 켠 제공자는 HTTP로 한 번 다시 보냅니다. 그 HTTP가 헤더 전에 끊기고 replacements가 2이면 한 번 더 보냅니다. 기본값 1이면 거절로 끝냅니다.

콤보의 runTurn이 요청에 없는 도구를 처음 호출하면, 글을 내기 전의 502로 바꿔 다음 대상으로 넘깁니다. 그 전에 다시 실행하면 안 되는 심장박동(Cursor가 이미 일을 한 표시)이 있었으면 그 대상에 남습니다. 에러나 빈 종료도 그 심장박동 뒤라면 같은 대상에 남습니다.

src/types/tools.ts:129 NAMESPACED_BARE_ALIAS_EXCLUDED_NAMES - 목표 도구 세 이름이 코드 모드 도우미 목록(src/types/tools.ts:82)에 들어가면서, 네임스페이스 도구의 짧은 이름을 만들지 않는 목록에도 같이 들어갔습니다. 네임스페이스만 있는 create_goal은 짧은 이름으로 더 이상 호출되지 않습니다. 같은 요청에 exec도 있으면 src/types/tools.ts:195가 그 짧은 이름을 exec로 바꿉니다. src/server/responses-undeclared-tool-guard.ts:122가 이 목록을 읽습니다.

src/lib/request-execution-budget.ts:250 ambiguousResendSpent - 허용 횟수를 다 썼는지는 보지 않고, 한 번이라도 쓰면 참입니다. replacements가 2여도 콤보(src/server/responses/core-combo.ts:684)는 첫 대체 뒤에 멈춥니다. 두 번째 전송은 웹소켓의 HTTP 대체가 헤더 전에 연결 리셋으로 예외를 던질 때만 src/server/responses/passthrough-dispatch.ts의 루프가 씁니다. 200 뒤에 출력이 없거나 502 응답이 돌아오면 남은 횟수는 쓰지 않습니다.

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

목표 도구 세 이름을 네임스페이스 도구의 짧은 이름에서 뺄지입니다. 코드 모드 복구에 필요한 것은 도우미 목록입니다. 짧은 이름 금지는 그 목록과 한 배열을 공유해서 따라왔습니다.

컨트롤 프레임만 온 웹소켓도 HTTP 대체에 넣을지입니다. 프레임이 하나도 없는 끊김과 같은 허용을 탑니다.

replacements: 2를 콤보의 두 번째 대상으로 볼지, 지금처럼 같은 대상의 헤더 전 리셋에만 쓸지입니다.

너의 추천

짧은 이름 금지 목록에는 create_goal, get_goal, update_goal을 넣지 마세요. 도우미 목록에는 두세요. 그러면 exec만 있는 요청은 지금처럼 복구되고, 네임스페이스 도구의 짧은 이름은 남습니다. replacements가 2일 때 두 번째 전송은 헤더 전 리셋에만 있다는 문장을 서버 설정 문서에 한 줄 두세요. 이 PR이 대신한 #5629, #5659, #5646, #5633, #5489와 이슈 #5495는 이미 닫혀 있습니다. types.ts와 config.ts를 나누는 변경은 이 PR에 없습니다. #5407의 Claude Code 경로는 이 PR에 없습니다.

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant