Skip to content

fix(deepseek): preserve parallel reasoning replay - #1479

Draft
Ingwannu wants to merge 1 commit into
devfrom
agent/fix-1477-deepseek-parallel-replay
Draft

fix(deepseek): preserve parallel reasoning replay#1479
Ingwannu wants to merge 1 commit into
devfrom
agent/fix-1477-deepseek-parallel-replay

Conversation

@Ingwannu

@Ingwannu Ingwannu commented Aug 11, 2026

Copy link
Copy Markdown
Owner

Summary

Root cause

The provider-scoped #1292 normalizer repaired every call/result pair independently. A valid parallel history such as reasoning, call A, call B, output A, output B therefore became reasoning, call A, output A, call B, output B. DeepSeek merges adjacent reasoning and function calls into one assistant message and always enables parallel tool calling, so the second call lost the reasoning block that belonged to its original turn and the continuation failed with reasoning_text missing.

The fix groups calls that occur before the first matched output, emits all calls followed by their outputs in call order, and moves intervening non-tool context after the complete batch. Duplicate, backwards, or otherwise ambiguous call/result histories return unchanged rather than being guessed.

Fixes #1477.

Verification

  • bun test tests/deepseek-inbound-wire.test.ts tests/openai-responses-passthrough.test.ts tests/deepseek-reasoning-replay.test.ts tests/deepseek-reasoning-replay-gaps.test.ts tests/config.test.ts tests/provider-registry-parity.test.ts tests/config-save-boundary.test.ts — 296 passed, 0 failed
  • bun run typecheck — passed
  • bun run privacy:scan — passed
  • cd docs-site && bun install --frozen-lockfile && bun run build — 265 pages built
  • git diff --check — passed
  • full suite — 11,049 passed, 11 skipped, 2 failures outside this diff:
    • the bridge stall timing test passed when rerun in isolation
    • the Unix shim token test fails identically on clean origin/dev@849ab5e35 because this host has a live service-token environment; no shim files are changed here

All local builds and tests ran with taskset -c 0-1 nice -n 10.

Decision Log

  • 목적과 의도: preserve DeepSeek plaintext reasoning replay across same-turn parallel tool continuations without regressing the confirmed single-call hook-context repair
  • 기존 구현 및 제약 조건: DeepSeek's Responses route is stateless, always enables parallel tool calling, and merges adjacent reasoning/function calls into one assistant message
  • 검토한 주요 대안: disable parallel calls, duplicate reasoning per call, remove the [Provider compatibility] DeepSeek V4 Flash returns 400 when developer message is interleaved between function_call and function_call_output #1292 normalization, or normalize one unambiguous call/output batch
  • 선택한 방식: keep calls before the first output in one batch, follow them with matching outputs in call order, and retain non-tool context after the batch
  • 다른 대안 대신 이 방식을 선택한 이유: it preserves the real assistant-turn boundary without fabricating reasoning or forwarding the known hook-interleaving failure
  • 장점, 단점 및 영향: sequential and parallel DeepSeek continuations keep their reasoning contract; tolerant providers are unchanged and ambiguous histories remain fail-closed upstream

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs and the applicable structure decision log are updated.
  • No credential, destination, workflow, dependency, GUI, or release boundary changes.

Summary by CodeRabbit

  • New Features

    • Improved DeepSeek Responses compatibility for parallel tool-call and result batches.
    • Preserved reasoning context and injected messages in the correct order during tool interactions.
    • Maintained separate sequential reasoning rounds.
  • Bug Fixes

    • Avoided reordering ambiguous, duplicate, missing, or out-of-order tool results.
  • Documentation

    • Updated provider and adapter guidance for tool-call batching and ordering behavior.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

DeepSeek Responses normalization now handles unambiguous parallel tool-call batches. It preserves reasoning context, moves injected messages after complete batches, and leaves ambiguous or invalid histories unchanged. Documentation and inbound-wire tests describe and validate the behavior.

Changes

DeepSeek Responses normalization

Layer / File(s) Summary
Batch normalization
src/adapters/openai-responses.ts lines 543–551, 579–624
The adapter validates unique, forward-ordered call/result pairs, emits calls before matched results, and preserves other items after the batch.
Provider contracts and documentation
src/providers/registry.ts, src/types.ts, docs-site/src/content/docs/reference/adapters.md, structure/04_transports-and-sidecars.md
Provider metadata and documentation describe contiguous DeepSeek tool batches, reasoning-bearing parallel calls, injected-context relocation, and unchanged ambiguous histories.
Behavior validation
tests/deepseek-inbound-wire.test.ts lines 102–105, 826–908
Tests cover parallel batches, injected context, separate reasoning rounds, and duplicate call IDs.

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

Possibly related PRs

  • lidge-jun/opencodex#1334: The normalization logic extends this PR’s handling to parallel batches, reasoning context, and ambiguity cases.

Suggested reviewers: lidge-jun, baileyh8

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. 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 changes address issue #1477 by preserving reasoning-bearing turns and replaying unambiguous parallel tool-call and result batches for DeepSeek.
Out of Scope Changes check ✅ Passed The code, tests, and documentation changes directly support the DeepSeek reasoning replay fix and contain no unrelated scope.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: preserving parallel reasoning replay for DeepSeek.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/fix-1477-deepseek-parallel-replay

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

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

@Ingwannu

Copy link
Copy Markdown
Owner Author

Exact-head CI note for 4602b1df5:

  • all Linux shards, gates, API usage, storage policy, keyring jobs, npm-global jobs, React Doctor, hygiene, and target enforcement passed;
  • the macOS full-suite job ended with Bun 1.3.14 SIGTRAP/segmentation fault after its preceding tests passed; there is no failing product assertion in that job log;
  • GitHub currently reports every job/check-run as completed but leaves workflow run 31503526156 in in_progress, so both the failed-run and single-job rerun endpoints return 403 workflow already running.

No code or workflow change is justified by this runtime crash. I will keep the PR draft and rerun only the failed macOS job once GitHub finalizes the parent run.

@Ingwannu

Copy link
Copy Markdown
Owner Author

Exact-head CI update for ec5b995bec83c3e1ca23cdafcd739554eafde2e5 (the second commit is empty and exists only to replace GitHub run 31503526156, whose run object was stuck in_progress after all jobs had completed and returned 500 to both normal and force-cancel APIs).

Run 31506198558 produced consistent product evidence:

  • all four Linux test shards passed;
  • gates passed, including typecheck, GUI tests, privacy scan, release-helper syntax, and CLI smoke;
  • API usage and storage-policy jobs passed;
  • keyring jobs passed on Ubuntu, Windows, and macOS;
  • npm-global jobs passed on Ubuntu, Windows, and macOS.

The only red job is the macOS full-suite job, across three infrastructure/flaky outcomes on the same code:

  1. attempt 1 completed 11,053 tests and failed only the unrelated existing native main profile transactions > automatic recovery reports an externally refreshed target timing test (5s limit, 8.16s runtime);
  2. attempt 2 ended in Bun 1.3.14 macOS Silicon SIGSEGV / exit 133 after 402s, with Bun reporting This indicates a bug in Bun, not your code;
  3. attempt 3 reproduced the same Bun 1.3.14 crash signature and exit 133 after 283s.

Neither failing path is in this PR diff, and the DeepSeek focused coverage plus every Linux shard passed. I am stopping repeated macOS reruns after the same runtime crash reproduced twice. The PR remains draft and unmerged pending independent human review; this comment records CI evidence rather than treating infrastructure failure as approval.

@Wibias Wibias left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed the DeepSeek batch normalizer. I did not find an additional blocking defect in this head. The change preserves same-turn parallel calls as one call batch followed by results, retains the existing single-call repair, and returns ambiguous duplicate/backwards histories unchanged rather than guessing.

I am not approving this head because it is currently not mergeable against the latest dev. Please rebase/resolve conflicts and request a short re-review of the final normalizer diff.

@Wibias
Wibias force-pushed the agent/fix-1477-deepseek-parallel-replay branch from ec5b995 to daea201 Compare August 11, 2026 19:37
@Wibias
Wibias marked this pull request as ready for review August 11, 2026 19:40
@Wibias
Wibias marked this pull request as draft August 11, 2026 19:41
@Wibias
Wibias marked this pull request as ready for review August 11, 2026 19:41

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs-site/src/content/docs/reference/adapters.md`:
- Around line 61-64: Update docs-site/src/content/docs/reference/adapters.md
lines 61-64 to state that the openai-responses adapter preserves original order
for duplicate, missing, and out-of-order call IDs. Update
structure/04_transports-and-sidecars.md lines 359-364 to include missing
call/result pairs in the fail-closed history cases alongside duplicate and
backward/out-of-order pairs.

In `@src/adapters/openai-responses.ts`:
- Around line 580-588: Update the call/output pairing logic around the calls
iteration to fail closed whenever any collected tool call lacks exactly one
matching result, rather than skipping unmatched calls. Validate that every call
has one later result and that no result is backward; return the original body
unchanged for duplicate, missing, or out-of-order IDs, and add regression
coverage for a missing earlier call result and a backward result.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: fa2807df-f832-403b-86da-4831a2108a63

📥 Commits

Reviewing files that changed from the base of the PR and between d91ca60 and daea201.

📒 Files selected for processing (6)
  • docs-site/src/content/docs/reference/adapters.md
  • src/adapters/openai-responses.ts
  • src/providers/registry.ts
  • src/types.ts
  • structure/04_transports-and-sidecars.md
  • tests/deepseek-inbound-wire.test.ts

Comment on lines +61 to +64
- DeepSeek's stateless Responses parser receives provider-scoped history normalization: hook-injected
context moves after an unambiguous tool-call/result batch. Parallel calls remain grouped before
their matching outputs so every call stays in the reasoning-bearing assistant turn. Tolerant
providers and ambiguous duplicate call IDs keep their original input order.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Document all fail-closed history cases.

The documentation must state that missing and out-of-order call/result histories retain their original order. The public adapter reference currently names only duplicate IDs. The design document names duplicate and backward pairs but omits missing pairs.

  • docs-site/src/content/docs/reference/adapters.md#L61-L64: State that duplicate, missing, and out-of-order call IDs remain unchanged.
  • structure/04_transports-and-sidecars.md#L359-L364: Add missing call/result pairs to the fail-closed list.

As per path instructions, the openai-responses reference must document duplicate, missing, and out-of-order IDs as unchanged.

📍 Affects 2 files
  • docs-site/src/content/docs/reference/adapters.md#L61-L64 (this comment)
  • structure/04_transports-and-sidecars.md#L359-L364
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs-site/src/content/docs/reference/adapters.md` around lines 61 - 64,
Update docs-site/src/content/docs/reference/adapters.md lines 61-64 to state
that the openai-responses adapter preserves original order for duplicate,
missing, and out-of-order call IDs. Update
structure/04_transports-and-sidecars.md lines 359-364 to include missing
call/result pairs in the fail-closed history cases alongside duplicate and
backward/out-of-order pairs.

Source: Path instructions

Comment on lines 580 to 588
for (const [key, callIndices] of calls) {
const outputIndices = outputs.get(key);
if (callIndices.length !== 1 || outputIndices?.length !== 1) continue;
if (!outputIndices) continue;
if (callIndices.length !== 1 || outputIndices.length !== 1) return body;
const callIndex = callIndices[0]!;
const outputIndex = outputIndices[0]!;
if (outputIndex === callIndex + 1) continue;
movedOutputIndices.add(outputIndex);
outputAfterCall.set(callIndex, input[outputIndex]);
if (outputIndex <= callIndex) return body;
pairs.push({ callIndex, outputIndex });
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject a history when any tool call has no matching result.

Line 582 skips a call with no result. The function can then reorder a later pair in the same ambiguous history.

For example, [callA, callB, injected, outputB] becomes [callA, callB, outputB, injected], even though callA has no result. Return body when any collected call lacks exactly one later matching result. Add a regression test for this case and for a backward result.

Proposed fix
   for (const [key, callIndices] of calls) {
     const outputIndices = outputs.get(key);
-    if (!outputIndices) continue;
+    if (!outputIndices) return body;
     if (callIndices.length !== 1 || outputIndices.length !== 1) return body;

As per path instructions, normalization must leave “duplicate, missing, or out-of-order call IDs unchanged (fail closed).”

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (const [key, callIndices] of calls) {
const outputIndices = outputs.get(key);
if (callIndices.length !== 1 || outputIndices?.length !== 1) continue;
if (!outputIndices) continue;
if (callIndices.length !== 1 || outputIndices.length !== 1) return body;
const callIndex = callIndices[0]!;
const outputIndex = outputIndices[0]!;
if (outputIndex === callIndex + 1) continue;
movedOutputIndices.add(outputIndex);
outputAfterCall.set(callIndex, input[outputIndex]);
if (outputIndex <= callIndex) return body;
pairs.push({ callIndex, outputIndex });
}
for (const [key, callIndices] of calls) {
const outputIndices = outputs.get(key);
if (!outputIndices) return body;
if (callIndices.length !== 1 || outputIndices.length !== 1) return body;
const callIndex = callIndices[0]!;
const outputIndex = outputIndices[0]!;
if (outputIndex <= callIndex) return body;
pairs.push({ callIndex, outputIndex });
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/adapters/openai-responses.ts` around lines 580 - 588, Update the
call/output pairing logic around the calls iteration to fail closed whenever any
collected tool call lacks exactly one matching result, rather than skipping
unmatched calls. Validate that every call has one later result and that no
result is backward; return the original body unchanged for duplicate, missing,
or out-of-order IDs, and add regression coverage for a missing earlier call
result and a backward result.

Source: Path instructions

@Wibias Wibias left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-review against current dev@e8db4e03: one current blocking correctness issue remains. normalizeResponsesToolResultAdjacency() skips calls with no matching output, so a partially matched history can still be reordered. For example [callA, callB, injected, outputB] can move outputB ahead of injected even though callA is unresolved. This violates the documented fail-closed boundary for missing/duplicate/out-of-order call-result histories. Please return the original body when any collected call lacks exactly one later matching output, add missing/backward regression coverage, and update the public/design docs to name missing histories explicitly. The branch is also 9 commits behind current dev and currently not mergeable, so please rebase after the fix.

@Wibias
Wibias marked this pull request as draft August 11, 2026 19:57
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.

2 participants