Repository navigation
fix: keep Responses assistant history valid after compression (#564) - #565
Conversation
Folding removes the tool calls/results that separated two assistant turns, and stripKernelSummaries can then delete the acp_summary that was the last separator - merging two turns into one run that Responses rejects with invalid_assistant_history. Add repairResponsesAssistantOrdering(), applied AFTER summary dedup at all three Responses wire-build sites (initial prepare, compact forge, compress re-request). It rebuilds turn boundaries from the ORIGINAL client history: drop a reasoning whose turn body was wholly folded away (keep originally-reasoning-only turns), otherwise separate merged runs with a neutral user marker. Signed reasoning items are preserved verbatim (encrypted_content untouched). Adds tests/issue564-responses-ordering.test.ts: an end-to-end case capturing the actual re-request body after a live compress, plus unit cases for orphaned-reasoning drop, reasoning-only-turn retention, and summary-carrier preservation.
📦 Built Package ArtifactBranch: Option A — Install from npm PR tag (recommended)npm install -g billion-context@pr-565Each push to this PR publishes a new version under the Option B — Download artifact
tar xzf billion-context-pr565.tgz
npm install -g packageThis comment is automatically updated on each push. |
[bot] 🏷 Review of PR #565 (fixes #564) Reviewed the diff ( Duplicate screen: no other open/closed issue or PR targets this (searched by symptom + fix). Confirmed distinct from #539 as you framed it — #539 dropped the Root cause — verified. Responses enforces per-run order Fix — sound.
Signed reasoning is preserved verbatim (
I hand-traced the drop/separator logic against the orphaned-reasoning, reasoning-only-turn, and surviving-carrier cases — all consistent. Importantly, a separator is only ever inserted directly after assistant content (the phase/seenReasoning flags reset on every non-assistant item), so it cannot create two consecutive same-role items. Both compression modes: the repair is carrier-independent (pure CoreMessage transform) and covers both plugin-mode replay and proxy-mode re-request. ✓ Pre-flight (reproduced independently on
Tests: strong. The e2e case drives the real Non-blocking observation (optional hardening):
My lean is option 1: it matches the actual bug scope and avoids introducing untested behavior against real codex (the E2E suite couldn't run here). Not a blocker either way. E2E note: the real-codex-through-bili suite needs a local Responses-compatible upstream + the codex binary and could not be executed in this sandbox (loopback upstreams blocked); the fetch-probe regression pins the final ordering instead, which is adequate. Recommend running the manual-dispatch Per repo policy I won't merge (human-only). Happy to implement the responses-gating follow-up if you'd like it. |
What
Fixes #564: after a successful compression, bili could send an invalid Responses assistant history upstream, producing repeated
400 invalid_assistant_history:Root cause
Responses item order equals CoreMessage order (
coreToResponsesmaps each core message to an item in sequence). Two original assistant turns were being merged into one run by two steps:stripKernelSummaries()then deletes theacp_summary_<block>that was the last remaining separator (the block's compress call is present in history, so the summary is redundant and gets stripped).Result:
[reasoningA][messageA][reasoningB][messageB]— a single run violating the Responses invariant (within a run the order must bereasoning* -> message* -> function_call*, with at most one reasoning item). This is Responses-specific: OpenAI chat merges consecutive assistant messages into one object, so it never surfaces there.Distinct from #539 (which dropped the
reasoning_contentfield on re-request): here the existing reasoning items survive, but their order relative to assistant text/tool-calls is corrupted.Fix
New
repairResponsesAssistantOrdering(folded, original)insrc/server.ts, applied after summary dedup at all three Responses wire-build sites:prepareResponses(initial request / plugin-mode replay)prepareResponsesCompact(forged compacted history handed back to the client)refreshFolded(post-compress internal re-request)It rebuilds turn boundaries from the original client history:
Signed reasoning items are preserved verbatim (
encrypted_contentuntouched); the original history and compression blocks are never mutated.Tests
tests/issue564-responses-ordering.test.ts:runCompressLoop+ responses adapter through a live compress and asserts on the actual re-request body (captured via a fetch probe) — not just the pre-dedup fold output;acp_summary-carrier preservation (no spurious marker injected).Pre-flight
npm run typecheck— cleannpm test— 1051 pass / 1 fail. The single failure (launcher.test.ts:resolveClientCommand: codex/claude resolve to themselves) is pre-existing and environmental: this checkout has/usr/bin/codexinstalled, so resolution returns the full path where the test expects the bare name. Reproduced identically on pristine master; unrelated to this change.npm run build— cleanNote: the real-codex-through-bili E2E suite (
tests/e2e/e2e-codex.test.ts) requires a local Responses-compatible upstream plus the codex binary and could not be executed in this sandbox (loopback upstreams are blocked by network policy); the new regression pins the final wire ordering directly instead.