Skip to content

Range segmentation assumes refNum order == message array order — surface-replace hosts get end<start ranges + spurious fragmentation #207

Closed

Description

@ranxianglei

来源: Tyan66666/billion-context-dsh#38 Tyan66666/billion-context-dsh#38 分析 acp-kernel ref-map drift 时发现

Problem (observed live)

billion-context-dsh — a DeepSeek Harness adapter that compresses by durably REPLACING its message surface — observes after a handful of compressions:

  • the nudge range table contains ranges whose endRef is numerically SMALLER than startRef (real example: m00110295..m00106762 — nonsense as a numeric interval);
  • large tool results drop out of the recommended ranges;
  • at ~20 % context occupancy the recommended ranges are worth ~28 tokens, so the benefit floor suppresses every nudge while the session keeps growing.

Root cause

assignRefs (src/refs.ts) assigns refs as an append-only first-seen counter and never prunes them. buildCompressibleRanges (src/recommend.ts) segmented ranges with hasGap = info.refNum > prevRefNum + 1 — correct only when refNum order equals message-array order.

A surface-replacing host breaks that assumption two ways:

  1. compressed messages physically leave the next turn's message array while their refs stay assigned → holes in the ref map → hasGap fires spuriously → every range fragments;
  2. the model's summary node is inserted at the replaced span's position (MID-ARRAY) and assignRefs gives it a fresh HIGH ref → the previous group flushes and a descending pair (startRef > endRef) is emitted.

Append-only hosts (pai-acp / billion-context-pi, opencode-acp) never see this: they pass the full append-only history every turn, so array order == first-seen order == ref order.

Relation to the #176 revert

#176 tried to repair the map by recycling dead slots; #191 reverted it with the invariant "ref numbers are never reused or duplicated within a session — ever". This proposal recycles nothing. assignRefs stays append-only and untouched; instead the RECOMMENDATION stops inferring continuity from ref arithmetic. The apply side is already order-agnostic — boundaries.ts resolves range anchors by message index (indexByMessageId), so even the descending pair applies to the correct span today. Only the recommendation segmentation is wrong, which is why the fix is confined to src/recommend.ts.

Proposed fix (PR incoming)

Segment by ARRAY ADJACENCY: a range splits only when a numbered-ref message is SKIPPED between two list entries (protected tool, synthetic/covered, protected zone) or at user-turn boundaries. Entries carry a gapBefore boolean instead of refNum.

Behavior audit: on append-only (dense) hosts a numbered message is skipped between two entries iff a refNum gap exists — ranges are identical (the existing suite stays green with zero test edits). On replace-style hosts holes are no longer interruptions and mid-array summary nodes extend the range instead of flushing it.

Acceptance: regression tests for both shapes (holes, mid-array summary node) + the full existing suite green; no ref ever reused, rewritten, or pruned.

The downstream side (billion-context-dsh) has a labeled local workaround (buildCompressibleSeqRanges) that it will delete the moment this ships — its removal gate is exactly this issue.

Activity

  1. ranxianglei commented on Sep 6, 2026

    @ranxianglei
    OwnerAuthor

    🤖 ework agent · qwen3.8-27b

    [bot] 🏷 Picked up. Triage so far:

    Implementing array-adjacency segmentation (gapBefore per entry) + regression tests for both shapes now. PR link to follow.

  2. ranxianglei commented on Sep 6, 2026

    @ranxianglei
    OwnerAuthor

    🤖 ework agent · qwen3.8-27b

    [bot] 🏷 Fix implemented — PR: #209

    Triage verdict (audit-before-implementation): both mechanisms verified against source before writing any code —

    1. Holes: allocateFreeRef (src/refs.ts:86) skips used slots and never prunes, so when compressed messages leave the next turn's array the ref map has holes; old segmentation hasGap = info.refNum > prevRefNum + 1 (src/recommend.ts:215) then fires spuriously on every adjacent pair → full fragmentation.
    2. Mid-array summary node: the summary is inserted at the replaced span's position and gets a fresh HIGH ref (m00110295-style), which flushes the previous group and emits the descending startRef > endRef pair.

    Layer judgment: this is a recommendation-layer bug only. The apply side was already order-agnostic (boundaries.ts resolves anchors by message index via indexByMessageId), so even descending pairs applied to the correct span — matching your observation that only the nudge table looked broken. Root cause is inferring continuity from ref arithmetic; the symptom (fragmented ranges, benefit-floor suppression) lives entirely in buildCompressibleRanges. That's why the fix is confined to src/recommend.ts.

    What changed (src/recommend.ts only):

    Tests: 4 new regression tests in tests/recommend.test.ts covering exactly your acceptance shapes — holes don't fragment ([a,e] after b..d removed → one range, count 2); mid-array non-synthetic summary node extends the range instead of flushing ([a, m00006-summary, d, e] → one range, count 4); synthetic [Compressed conversation section] nodes still split (legacy behavior preserved); protected groups split on interleaved numbered messages but survive bare holes.

    Verification: npm run typecheck ✓ · npm test 584/584 pass ✓ · npm run build ✓

    Downstream gate: once #209 is merged and released, billion-context-dsh can delete its local buildCompressibleSeqRanges workaround. Will note it here when the release lands.

  3. added a commit that references this issue on Sep 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions