Repository navigation
fix(kernel): fold summaries may never precede the pinned first user message (#2733) - #2755
Conversation
…essage (#2733) Production logs (issue #2733, codex/gpt-6-astra wire mode) show the request after a fold matching only the ~11K immutable instructions+tools head while the kernel expected the pre-foldPoint prefix (anchor) to stay cached: shrink 11% foldPoint=m01360 ... anchor≈34427 tok → next request cached=11125 (3%) instead of ≥ anchor Root cause: when a summary block's anchor ids resolve to index 0 — a range rooted at the pinned first user message (session 01a1210e: foldPoint=m00001), or a chain re-fold whose effective ids are all pruned — rebuildMessages splices the summary AHEAD of the pinned first user message. The pinned message is the provider prefix-cache head; rewriting that slot collapses the cached prefix on every fold, and the next request re-warms from the constant head only. That double prefix-cache penalty per fold is 56% of all uncached tokens in the #2733 log. Fix (kernel/src/prune.ts), four parts: - collectSummaryAnchors now prefers block.directMessageIds over effective (merged) ids and falls back to messages.length (tail) instead of 0, so a block whose ids were all consumed parks its summary after the surviving material, never at the head. - rebuildMessages drops rendered summary copies of ANY known block (knownSummaryIds from state.blocks, including consumed ones) so stale copies of merged blocks cannot leak into loop rebuild views. - after resolving anchors, insertAt is clamped to >= firstUserIndex + 1: a summary may never precede the pinned first user message. tests/responses-refold-head-stability.test.ts pins the invariant across a fold + two chain re-folds rooted at the pin: every summary-carrying view must keep input[1] byte-identical to the pinned m00001 message. Red on the old kernel (view 12 head = CHAIN2 summary), green with the fix.
📦 Built Package ArtifactBranch: Option A — Install this exact build (recommended)npm install -g billion-context@0.0.3022-pr.2755.964227fThis exact version is immutable and reproducible. The rolling Option B — Download artifact
tar xzf billion-context-pr2755.tgz
npm install -g packageInstall instructions are refreshed on each push. |
… pin-safe The #2733 clamp landed as unconditional, which broke two kernel tests: renderHandoff re-derives the model view through prune(), and handoff docs WANT the summary of older absent material at the head (no provider prefix cache at stake) — 're-pruning the same snapshot without folded resurrects the summary at index 0'. PruneOptions now carries headSummaryAnchors (default false = the pin-safe request path: tail parking + the after-pin clamp); renderHandoff passes true. The promote-after-prune regression's placement assertion moves to the new invariant (summary after the pinned first user message) — its actual subject, consumption of a pin-covering block via the surviving raw, is unchanged. kernel suite 1271/1271; repo handoff/kernel/loop/responses/compress/ fold/anthropic/prune/absorb suites green; tsc + unused-exports clean.
aaf6568 to
7adc878
Compare
…2733) Review finding on the head-stability fix: knownSummaryIds was built from state.blocks unconditionally, so with injectSummaries:false — a path with no fresh render at all — rebuildMessages would DROP already-rendered summary copies instead of keeping them, silently inverting that opt-out's semantics (pre-change: copies stayed). No caller uses injectSummaries:false today (kernel handoff/compress, host persist/plugin all use the default), but the opt-out is part of the prune() contract, so the drop set is now scoped to inject=true and a state-prune.test.ts case pins the retention behavior.
tests/responses-refold-cache-seam.test.ts shipped with soft diagnostics only
('pin after confirmation') and its PR-body label described a detector this
lane does not exercise. It now pins the post-fix invariants hard, on the
production consumption geometry (a fold whose range nests over a prior
fold's covered region, rooted at the pin):
(1) the pinned first user message stays the first history item across the
consumption re-fold;
(2) the rewrite starts exactly at item[2] — right after the pin;
(3) exactly one summary survives in both the re-request and the next
steady view (stale consumed-block copies dropped);
(4) re-request and steady turn share the merged summary byte-for-byte at
the same position (no one-build drift).
Red on the old kernel (head slot = merged F2A summary), green with the fix.
7adc878 to
08dbc45
Compare
[bot] 🏷 ① 事故披露(必须放在最前):冲突唤醒后我基于旧分支头做了 rebase + force-push,随后发现远端分支上已有 5 个新提交(版本号修复、merge master、
② 结构化八问
③ 合规与描述修复(kernel-guard 红灯根因 + body 勘误)
④ 验证证据:kernel suite 1272/1272(含分支上的 2 处测试更新 + 我新增 1 例);bili 定向 superset(fold/refold/prune/absorb/responses/anthropic/compress,84 文件)651/651;tsc + unused-exports clean;两条新 lane 均做红-绿双向验证。全量回归按本地测试纪律交给 CI——当前 head ⑤ 判定:机制级根因修复、方向正确、测试有牙、合规项已补齐。这是 kernel 变更,按 Kernel Boundary 属 human-gated,不适用 auto-merge。建议:等本轮 CI 全绿后你终审合并;若对 |
📦 Merged Build PublishedThis merge to master was published as npm install -g billion-context@0.0.271-master.1acb789 # this exact build
npm install -g billion-context@master # always the newest merged build
|
…, not the stale corpus manifest (#2671) After the 0222ef2 master merge the shadow e2e went red against the corpus manifest digests. Root cause is NOT parity breakage: master's #2755 legitimately evolves folded-view serialization (fold summaries now park after the first user message; newer builds materialize stats counters the old pins predate), so a fresh save of an old-world corpus case can no longer digest to the pinned value. The shadow layer's own reconciliation (shadowDigest(loadLegacyView(rows)) vs shadowDigest (legacyLoad(record)) per write) stays clean throughout. The e2e now pins the contracts that actually matter, version-stably: - record parity: unified rows must digest identically to the record the legacy file holds after the SAME write (recordDigest maps the envelope payload onto the view digest shape) - fixed point: reload + re-save must produce a byte-equal digest — an unstable round-trip would reshuffle the tail on every save The corpus manifest stays the oracle for raw ingest in storage-corpus.test.ts (old corpus still digests to its pins). Also pins the mode-label matrix against the developer machine's config file (BILI_CONFIG_FILE → missing file), which otherwise leaks a storage.unified=full file tier into the legacy expectation.
…, not the stale corpus manifest (#2671) After the 0222ef2 master merge the shadow e2e went red against the corpus manifest digests. Root cause is NOT parity breakage: master's #2755 legitimately evolves folded-view serialization (fold summaries now park after the first user message; newer builds materialize stats counters the old pins predate), so a fresh save of an old-world corpus case can no longer digest to the pinned value. The shadow layer's own reconciliation (shadowDigest(loadLegacyView(rows)) vs shadowDigest (legacyLoad(record)) per write) stays clean throughout. The e2e now pins the contracts that actually matter, version-stably: - record parity: unified rows must digest identically to the record the legacy file holds after the SAME write (recordDigest maps the envelope payload onto the view digest shape) - fixed point: reload + re-save must produce a byte-equal digest — an unstable round-trip would reshuffle the tail on every save The corpus manifest stays the oracle for raw ingest in storage-corpus.test.ts (old corpus still digests to its pins). Also pins the mode-label matrix against the developer machine's config file (BILI_CONFIG_FILE → missing file), which otherwise leaks a storage.unified=full file tier into the legacy expectation.
v0.1.192 was a carve-out release (78df4ea, ranxianglei#2664 only) and its version bump never flowed back to master, so master still declares 0.1.191 while npm latest is 0.1.192. That trips the Release one-click Drift guard: ::error::master package.json (0.1.191) != npm latest (0.1.192) ranxianglei#2664's changes themselves are already in master (f0b3f5d), so this is metadata-only: bump package.json + package-lock.json (root and packages."" entries) to 0.1.192 so the next one-click release (0.1.193, carrying the ranxianglei#2755 fold-summary head-stability fix) can proceed.
…ane (ranxianglei#2997) ranxianglei#2755/ranxianglei#2741 and their regression tests cover only the Responses-wire / proxy-mode lane. DSH-native is plugin mode over /v1/chat/completions (folds arrive via /__bili/plugin/tool) and had no post-fold cache-seam coverage. Add a hermetic backstop asserting that across a multi-disjoint-range fold the pinned first user message never moves, no summary precedes it, and consecutive steady-state builds are byte-identical except at the tail — the invariant whose violation produces ranxianglei#2997's early-prefix cache miss. Carrier-agnostic (holds under either the acp_summary or the client tool-pair carrier).
…3194 CI) Two defects surfaced by the storage-ext / sampled-verification CI legs: 1. dropResidentMetadata crashed on hand-built sessions with no meta (TypeError reading 'protocol') — guard with optional chaining, the lazy loader degrades to the id-only lookup. 2. After ②a's foldResidentTail folds an idle session's tail, subsequent saves still presented the folded re-render as the ⑤ rawTail override. The re-render reorders fold summaries (#2755: the summary yields to the leading user message on first re-render), but the equal-length watermark delta trusted the claimed rawness and skipped the tail-pointer rebuild — the unified table kept the pre-fold ordering while the record moved on, and the sampled digest correctly flagged drift on every 21st save. The override now requires lastMessagesFolded !== true: folded saves present the envelope render (no override), which planIngestDelta already routes to the full ingest, rebuilding tailSeqs in the render's order. Raw rows interned by earlier raw saves stay put (append-only). storage-unified oracle updated: parity compares against the override input when a live raw tail exists, else against the written record. Tested: storage-ext 5/5, storage-save-path 3/3, storage-unified 13/13, memory families + persist-tail + corpus + issue404 42/42.
Part of #2733 · Closes #2741
kernel-change: fold-summary anchor/insertAt fix in kernel/src/prune.ts (rebuildMessages) for the #2733 per-fold prefix-cache collapse; adds PruneOptions.headSummaryAnchors so handoff/restore docs keep chronological head placement while every request path stays pin-safe; acp-kernel 0.0.116 → 0.0.117 (master took 0.0.116 via #2669).
Summary
Fixes the per-fold double prefix-cache penalty behind the low cache hit rate in #2733 (89.8% weighted, 79.4% per-request; 56% of all uncached tokens = fold request + next request both matching only the ~11K immutable instructions+tools head).
Root cause
When a fold block's anchor ids resolve to index 0,
rebuildMessagessplices the rendered summary ahead of the pinned first user message:01a1210ein the 192版本缓存命中率低codex #2733 log hasfoldPoint=m00001folds; orinsertAt: earliest ?? 0fallback.The pinned first user message is the provider prefix-cache head. Rewriting the slot that holds it collapses the cached prefix at every fold; the next request can then only match the constant head. This is exactly what the kernel's own compression telemetry predicted vs. what the provider reported:
Fix (
kernel/src/prune.ts)collectSummaryAnchorsprefersblock.directMessageIdsover effective (merged) ids, and falls back tomessages.length(tail) instead of0.rebuildMessagesreceivesknownSummaryIdsbuilt from allstate.blocks(including consumed ones) and drops rendered copies of any of them — stale copies of merged blocks can no longer leak into loop rebuild views.insertAtis clamped to>= firstUserIndex + 1: a summary may never precede the pinned first user message.knownSummaryIds) is scoped toinjectSummaries: true— with injection off there is no fresh render to substitute, so already-rendered copies are kept (the opt-out path keeps its pre-change semantics).PruneOptions.headSummaryAnchors(default false): session handoff/restore docs have no provider prefix cache at stake, sorenderHandoffopts into chronological head anchoring; all request-path callers keep the pin-safe placement.Test
tests/responses-refold-head-stability.test.ts: 20-turn harness, a fold plus two chain re-folds rooted at the pin ([uniq[0], uniq[4]], the production geometry). Asserts every summary-carrying upstream view keepsinput[1]byte-identical to the pinnedm00001message. Red on the old kernel (view 12's head slot =[Compressed conversation section] — CHAIN2…), green with the fix.Also adds
tests/responses-refold-cache-seam.test.ts: wire-level regression for the mid-history seam the[cache-seam]detector (src/cache-ledger.ts) flagged in the #2733 log, driven on the production consumption geometry (a fold whose range nests over a prior fold's covered region, rooted at the pin). Hard assertions: the pinned first user message stays the first history item across the consumption re-fold; the rewrite starts exactly at item[2] (right after the pin); exactly one summary survives in both the re-request and the next steady view (stale consumed-block copies dropped); and the two builds share the merged summary byte-for-byte at the same position (no one-build drift). Red on the old kernel, green with the fix.Kernel-layer tests moved with the behavior:
kernel/tests/regression-promote-after-prune.test.tsnow asserts the summary sits AFTER the pinned first user message (its subject — consumption of a pin-covering block via the surviving raw — is unchanged), andkernel/tests/state-prune.test.tspins the injectSummaries:false retention semantics above.Behavior change (wire layout after folds)
headSummaryAnchors(their placement is unchanged from pre-change).Verification
tsc --noEmitclean,scripts/check-unused-exports.mjscleanFold double-penalty component of #2733; its remaining facets are tracked separately: Class A preflight wire shape → #2735, Class B early native compaction and the D1 nudge window → the #2733 thread.