Skip to content

fix: detect codex compaction requests before preflight; verbatim passthrough when not intercepted (#332) - #335

Merged
ranxianglei merged 2 commits into
masterfrom
2026-08-29_compaction-preflight-order
Sep 3, 2026
Merged

ranxianglei merged 2 commits into
masterfrom
2026-08-29_compaction-preflight-order

Conversation

@ranxianglei

@ranxianglei ranxianglei commented Aug 29, 2026 •

Copy link
Copy Markdown
Owner

What

Fixes #333. codex's native remote-compaction request (compaction_trigger form) used to flow through prepareResponses (kernel transform + payload rebuild) and the preflight overflow-compress before the compact detection/gate ran. Even when the result was a passthrough, the upstream received a mutated compaction request — diverging from codex's local history, and 400-looping on non-OpenAI backends that reject the compaction_trigger input item (real-device evidence in the issue: 44354 tok > 32000 window → preflight compressed + rebuilt → relay 400 ×3).

How

Detection is moved to the top of the per-session pipeline, before runPrepare()/preflight, with a three-way dispatch exactly as proposed in the issue:

  1. E2 on + gate preconditions pass → prepareResponses runs (transform + existing forge logic) → forged 2-frame SSE served locally, upstream not contacted.
  2. E2 off (pass mode) or gate preconditions fail → verbatim passthrough: the raw request bytes are forwarded (prepared = null path, same as the fix: stitched-stream usage double-count after compress re-request (adapter-anthropic >0 guard) #300 hop-marker passthrough). No preflight, no rebuild, no window clamp, no session-state folding as a side effect.
  3. Non-codex client / non-compact request → normal pipeline, unchanged.

Changes:

  • src/codex-compact.ts — split the safety valve: codexCompactGatePre(session, limit) (limit > 0, steady-state < 90% of window, ≥1 active block — everything knowable before the transform) and codexCompactGate = transformOk && gatePre (identical semantics to before).
  • src/server.ts (pipeline) — detect isCodexClient && last input item === compaction_trigger on the raw parsed body; dispatch as above. When not intercepted, logs the reason (BILI_CODEX_COMPACT=pass / gate preconditions not met / transform/forge failed).
  • src/server.ts (prepareResponsesCompact, endpoint form) — gate preconditions checked before processTurn, so a compact that will not be intercepted no longer mutates session state as a side effect (wire behavior unchanged: raw body passthrough).

Notes / scope:

Tests

4 new e2e regression tests in tests/codex-compact-e2e.test.ts using an oversized payload (14 msgs ≈ 15.4k tok > 10k window — the condition that made the old ordering fire preflight):

  • pass mode + oversized compact → forwarded byte-identical to the raw request, session state untouched
  • intercept + gate preconditions fail (≥90%) + oversized compact → forwarded byte-identical (the issue's exact log scenario)
  • intercept + healthy ACP + oversized compact → forged, upstream untouched (preflight no longer interferes with the forge)
  • endpoint form + gate preconditions fail → raw body forwarded, processTurn skipped (no state mutation)

All 4 fail on the pre-fix code (verified by stashing src/server.ts and re-running) and pass with the fix.

Pre-flight

  • npm run typecheck — clean
  • npm test — 761/761 pass
  • npm run build — clean

@github-actions

github-actions Bot commented Aug 29, 2026 •

Copy link
Copy Markdown
Contributor

📦 Built Package Artifact

Branch: 2026-08-29_compaction-preflight-order (2eb7759)

Option A — Install from npm PR tag (recommended)

npm install -g billion-context@pr-335

Each push to this PR publishes a new version under the pr-335 npm tag.

Option B — Download artifact

  1. Download the artifact from the Actions run
  2. Extract the tarball and install:
tar xzf billion-context-pr335.tgz
npm install -g package

This comment is automatically updated on each push.

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 ework agent · vllm-qwen/qwen3.8-27b

[bot] On it — reviewing PR #332 (branch 2026-08-29_compaction-preflight-order, stacked on 2026-08-28_codex-compact-intercept / PR #325). I'll verify the claims: check out the branch, run typecheck/tests/build, review the diff (gate split in src/codex-compact.ts, pipeline dispatch in src/server.ts, the 4 new e2e regression tests), and confirm the tests fail on pre-fix code. Full review reply to follow shortly.

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 ework agent · vllm-qwen/qwen3.8-27b

[bot] Review of PR #332 complete — approved, all claims verified. Details below.

Verification (all run on 2026-08-29_compaction-preflight-order @ 3b34dde)

Check Result
npm run typecheck ✅ clean
npm test ✅ 761/761 pass (matches the claim)
npm run build ✅ clean (tsup, dist/index.js 2.53 MB)
4 new tests fail pre-fix ✅ Verified: checked out 3b34dde^:src/server.ts and re-ran tests/codex-compact-e2e.test.ts → exactly the 4 #332 tests fail, the 5 pre-existing E2 tests still pass. Restored afterwards.
Merge vs master ✅ git merge-tree against origin/master (caca703, PR #329) → no conflicts (disjoint files: branch touches src/codex-compact.ts, src/server.ts, tests/codex-compact*.test.ts; master added tests/e2e/, AGENTS.md, ci-e2e.yml, test:e2e script)

Code review

The implementation matches the description exactly:

  • src/codex-compact.ts — codexCompactGatePre(session, effectiveLimit) = limit > 0 && lastInputTokens < 0.9·limit && ≥1 active block; codexCompactGate = transformOk && gatePre. Semantics identical to the old combined gate.
  • src/server.ts:1064-1082 — trigger detection on the raw parsed body inside withSessionLock, before runPrepare()/preflight. Three-way dispatch as proposed: intercept+gatePre → runPrepare() → forge served locally via forward()'s codexForge branch (upstream untouched, rememberPluginMessages called); otherwise raw bodyBuffer forwarded with prepared = null (so no #300 chain-marker stamp — correct). The why log (BILI_CODEX_COMPACT=pass / gate preconditions not met / transform/forge failed) is accurate for each branch.
  • src/server.ts:1665-1671 (prepareResponsesCompact) — gatePre checked before processTurn, so a non-intercepted endpoint compact returns base (raw body) with no state mutation.
  • Consistency: the dispatch and prepareResponses both use the same reqConfig.modelContextLimit (the dispatch's gatePre at src/server.ts:1071 and the forge gate at src/server.ts:1531 see the identical limit), so there's no window where the dispatch says "intercept" but prepareResponses' full gate disagrees except via transformOk — which is exactly the "transform/forge failed" branch.
  • No regression to the E2 e2e suite: the forge log codex compact intercepted (trigger) (src/server.ts:1539) that tests/e2e/e2e-codex.test.ts asserts on is unchanged.
  • The two flagged out-of-scope notes check out: x-codex-beta-features stripping in buildForwardTarget is untouched, and the triggerFinal nudge-skip (src/server.ts:1499-1502) is now dead for the trigger form but correctly left for the feat: intercept + forge codex native compaction when ACP is healthy (#321 PR-E2) #325 change set.

Minor observations (non-blocking)

  1. "transform/forge failed" branch still touches a little state: when mode=intercept + gatePre passes but the kernel transform throws, runPrepare() has already run ++session.stats.requests, reconcileNativeCompactionBoundary, and stripBiliCompactionItems before the failure is discovered. processTurn/session.state is not mutated on the throw path (state is only assigned after success), so this is the same pre-existing behavior as any transform-failure request — not a regression from this PR, just noting it for completeness.
  2. Redundant (harmless) check: src/server.ts:1684 re-checks the full codexCompactGate after processTurn in the endpoint form, but at that point gatePre already passed and transformOk === true, so it can never fail. Fine as defense-in-depth.
  3. Test quality is good: the new tests assert byte-identical forwarding (h.bodies[last] === rawBody), session-state immutability via JSON.stringify snapshot, and upstream non-contact on the forge path — with the oversized 14-msg payload that specifically reproduces the old preflight-firing condition.

Merge ordering

⚠️ One dependency to keep in mind (already noted in the PR body): PR #325 (2026-08-28_codex-compact-intercept) is not in master yet — none of its commits (008bb95, c507e0c, c959942) are ancestors of origin/master. This branch is stacked on it, so #325 must merge first (or both land together). No conflicts either way.

LGTM — ready to merge once #325 lands.

@ranxianglei
ranxianglei force-pushed the 2026-08-29_compaction-preflight-order branch from 3b34dde to 174006e Compare September 3, 2026 09:05
@ranxianglei
ranxianglei merged commit 4f5fe7e into master Sep 3, 2026
6 checks passed
antclt pushed a commit to antclt/billion-context that referenced this pull request Sep 22, 2026
Kernel 0.0.82 (acp-kernel#339) ships the host checkpoint-carrier fix (ranxianglei#335):
plain message-ref ranges skip live carriers marked via CoreMessage.summaryOfBlockId.
Billion-context does not consume the marker yet (host-side adoption is dsh's),
but the pin keeps the bundled kernel current.
ranxianglei pushed a commit that referenced this pull request Oct 11, 2026
… array class (#2663)

The range selector (buildCompressibleRanges) advertised spans the apply side
structurally rejects: live checkpoint carriers (#335) survive pruning but are
carved out at apply time, so a span bridging carriers over covered raws
resolved to zero foldable messages and threw every turn (#2638). The two
screens also judged two different array classes for integrity withdrawal.

- state.ts: shared isLiveCheckpointCarrier predicate — single source of truth
  for selector and apply side
- recommend.ts: live carriers screen as gaps so no advertised group bridges
  across them; new optional integritySource param lets the withdrawal screening
  run on the same array class the apply gate judges (default: passed array)
- pipeline.ts/compress.ts: prune node stashes the pre-prune resent array in
  effects.originalMessages; recommend feeds it as the withdrawal source
- report.ts: acp_status UNCOMPRESSED rows open a gap at live carriers so no
  reported range bridges across them
- tests: carrier-advertise-validate.test.ts — #2638 production geometry
  end-to-end, carrier boundary (live/stale), withdrawal array-class contract,
  status-row consistency, plus both directions of the #2607 media split
  (archivable folds like text, unrecoverable stays pinned)

Rebased after #2607 landed on master: the originally drafted blanket media
carve was dropped — filterProtectedToolMessages already enforces the #1188
never-fold exemption for unrecoverable payloads, and a blanket carve would
have re-pinned archivable media against #2607's fold-like-text direction.
Media handling itself is untouched by this PR.

kernel-change: advertise/validate disagreement lives inside the kernel
(recommend.ts vs compress.ts); host side cannot express carrier-aware screening
or the pruned-vs-original view split. Closes #2663
ranxianglei pushed a commit that referenced this pull request Oct 11, 2026
… array class (#2663)

The range selector (buildCompressibleRanges) advertised spans the apply side
structurally rejects: live checkpoint carriers (#335) survive pruning but are
carved out at apply time, so a span bridging carriers over covered raws
resolved to zero foldable messages and threw every turn (#2638). The two
screens also judged two different array classes for integrity withdrawal.

- state.ts: shared isLiveCheckpointCarrier predicate — single source of truth
  for selector and apply side
- recommend.ts: live carriers screen as gaps so no advertised group bridges
  across them; new optional integritySource param lets the withdrawal screening
  run on the same array class the apply gate judges (default: passed array)
- pipeline.ts/compress.ts: prune node stashes the pre-prune resent array in
  effects.originalMessages; recommend feeds it as the withdrawal source
- compress.ts: media payloads in an applied span are carved out of the block
  instead of silently folded (bytes unrecoverable once folded, #1188); the
  existing warning now tells the truth
- report.ts: acp_status UNCOMPRESSED rows open a gap at live carriers so no
  reported range bridges across them

kernel-change: advertise/validate disagreement lives inside the kernel
(recommend.ts vs compress.ts); host side cannot express carrier-aware screening
or the pruned-vs-original view split. Closes #2663
ranxianglei pushed a commit that referenced this pull request Oct 11, 2026
… array class (#2663)

The range selector (buildCompressibleRanges) advertised spans the apply side
structurally rejects: live checkpoint carriers (#335) survive pruning but are
carved out at apply time, so a span bridging carriers over covered raws
resolved to zero foldable messages and threw every turn (#2638). The two
screens also judged two different array classes for integrity withdrawal.

- state.ts: shared isLiveCheckpointCarrier predicate — single source of truth
  for selector and apply side
- recommend.ts: live carriers screen as gaps so no advertised group bridges
  across them; new optional integritySource param lets the withdrawal screening
  run on the same array class the apply gate judges (default: passed array)
- pipeline.ts/compress.ts: prune node stashes the pre-prune resent array in
  effects.originalMessages; recommend feeds it as the withdrawal source
- report.ts: acp_status UNCOMPRESSED rows open a gap at live carriers so no
  reported range bridges across them
- tests: carrier-advertise-validate.test.ts — #2638 production geometry
  end-to-end, carrier boundary (live/stale), withdrawal array-class contract,
  status-row consistency, plus both directions of the #2607 media split
  (archivable folds like text, unrecoverable stays pinned)

Rebased after #2607 landed on master: the originally drafted blanket media
carve was dropped — filterProtectedToolMessages already enforces the #1188
never-fold exemption for unrecoverable payloads, and a blanket carve would
have re-pinned archivable media against #2607's fold-like-text direction.
Media handling itself is untouched by this PR.

kernel-change: advertise/validate disagreement lives inside the kernel
(recommend.ts vs compress.ts); host side cannot express carrier-aware screening
or the pruned-vs-original view split. Closes #2663
@ranxianglei
ranxianglei deleted the 2026-08-29_compaction-preflight-order branch October 15, 2026 05:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

compaction_trigger requests pass through preflight/clamp before compact detection — mutated payload, upstream 400 loop

1 participant