Skip to content

fix(nudge): merge small ranges + effective T1 pending + tier-aware emergency arbitration - #57

Merged
ranxianglei merged 4 commits into
masterfrom
2026-08-11_nudge-merge-effective-arbitration
Aug 15, 2026
Merged

ranxianglei merged 4 commits into
masterfrom
2026-08-11_nudge-merge-effective-arbitration

Conversation

@ranxianglei

Copy link
Copy Markdown
Owner

Problem

Diagnosed from the ACP endurance report (dog/billion-context-pi#23): under a
24.5h isolated session, compress failure rate hit 60.5% (825/1363), and the
dominant cause was "too small" rejections — 707 cases, 86% of all failures,
476 of those single-range.

Root cause is a granularity mismatch between nudge arbitration and the
ranges the nudge offers the model:

  • pendingByTier computed T1 pending as the aggregate sum of all
    compressible ranges (inflated by fragmentation).
  • That aggregate let T1 win the arbitration loop, so the nudge fired at T1.
  • But the nudge then offered per-turn ranges (each often < 5000 chars after
    buildCompressibleRanges splits at user boundaries), and the model picked
    only 1–2 of them → `minCompressRange` rejection.
  • Emergency was worse: it skipped the tier loop entirely and always pointed at
    T1-only ranges, never at tier distillation — so T1 blocks piled up unconsolidated.

Fix

1. `mergeRangesToThreshold` (recommend.ts) — two-pass grouping. A greedy
accumulator merges consecutive compressible ranges until the batch reaches
`tokens*4 >= compress.minCompressRange`, then closes. Merges across ref gaps;
count-weighted `toolPct`; `dangerous` propagates. `recommendedRanges` now
returns these merged ranges, so the nudge offers batch-sized ranges the model
can compress in one call without tripping `minCompressRange`.

2. Effective T1 pending (compress.ts) — `pendingByTier` now counts only
effective merged ranges (`tokens*4 >= minCompressRange`) for T1, killing
the inflation that starved tier distillation.

3. Tier-aware arbitration (compress.ts `decideNudge`) —

  • Non-emergency: T1 is default when `t1Eff >= nudgeGrowthTokens`; T2
    overrides only when `t2Pen >= nudgeGrowthTokens × 1.5` AND `t2Pen > t1Eff`.
    New config `tier2GrowthMultiplier: 1.5`.
  • Emergency: ignores tier priority, picks the tier with the max pending
    (T1 effective vs T2/T3 summary totals) — so emergency now CAN route to
    distillation when that reclaims the most tokens.

4. `renderNudgeText` (nudge-text.ts) — tier≥2 path now also serves the
emergency case: emergency voice + `[EMERGENCY — TIER N DISTILLATION]` header,
still with the full distillation block list + rules.

Tests

+16 (239 total, all pass; typecheck clean; build 103.72 KB).

  • `recommend-merge.test.ts` (7): empty / disabled / single≥ / two-merge /
    three-split / dangerous propagation / count-weighted toolPct.
  • `nudge.test.ts` (5): non-emergency T1; non-emergency T2 @1.5× && >T1;
    T2 large but T1 wins; emergency T2>T1 → tier 2 + override; emergency T1 max → tier 1.
  • `nudge-text.test.ts` (1): emergency + tier 2 → emergency voice + distill text.
  • `validation.test.ts` (1 bonus): batch aggregate of small ranges passes when
    total ≥ `minCompressRange` (documents the batching the report questioned).

Compatibility

No adapter API change. `tier2GrowthMultiplier` defaults to 1.5; existing
config-overrides continue to work (it is additive). Once merged and published,
billion-context-pi will bundle it on its next release bump.

@ranxianglei ranxianglei mentioned this pull request Aug 11, 2026
@ranxianglei

Copy link
Copy Markdown
Owner Author

Cross-link: complementary with the adapter-side filter

pi-side adapter filtering (ranxianglei/billion-context-pi#148 + the extracted billion-context-kit viableRanges, ≥200 tok) attacks the same fragmentation root from the presentation layer. With this PR merged, that filter degrades gracefully into a safety net (tail leftovers + older-kernel repos, e.g. opencode still pinning 0.0.19). No conflict — merged ranges (≥ minCompressRange ≈ 1250 tok) pass the adapter floor trivially; the kit filter only trims the unmergeable tail groups.

Review: two regressions found

1. The maxContextLimitPct band was deleted → dead config (high).

Master had three arbitration bands: growth-gated normal, overLimit (usage ≥ maxContextLimitPct, default 0.75) bypassing the growth gate with any tier pending >= minCompressRange, and emergency (≥ emergencyThresholdPct, 0.80). This PR keeps only growthReady + emergencyOverride — grep overLimit|maxContextLimitPct in the new compress.ts: 0 hits. Consequences:

  • usage in the 75–80% band with growth < floor no longer nudges at all (master fired immediately, bypassing growth);
  • maxContextLimitPct survives in config.ts/types.ts with no consumer — dead config inviting misconfiguration;
  • the shouldInject = … || (overLimit && recommendedRanges.length > 0) last-resort fallback is gone.

2. Emergency with nothing effective now injects every turn (medium).

Master: emergency + no eligible tier → shouldInject = false (the "(no compressible content)" reason was computed but never sent). New: shouldInject = emergencyOverride || injectedTier !== null → at ≥80% with zero effective pending, an EMERGENCY nudge fires every turn; worse, if recommendedRanges still holds sub-threshold tail groups, the nudge offers ranges that will be atomically rejected — the exact failure mode this PR fixes, resurfacing under pressure. Master's overLimit gate (pending >= minCompressRange) existed precisely to prevent offering sub-threshold ranges.

Suggested fix: restore a mid band — treat usage >= maxContextLimitPct like emergency but gated on effective pending (any tier > 0), and only inject emergency when some tier has effective pending or ranges are nonempty; keep a distinct reason suffix for the no-content case so the 75–80% early pressure isn't silent.

Checked clean

  • mergeRangesToThreshold merging across gaps is safe at compress time: soft-protection excludes + warns (compress.ts:583-619), covered messages drop out of directMessageIds (their original blocks keep them), protected tool pairs excluded per the Bug 39 handling. Only cosmetic nit: merged count sums compressible parts but the span visually includes gap messages.
  • T1 cadence-check removal is fine — the growth gate (growthReference = lastNudgeShownTokens) is the same condition in disguise.
  • tier2GrowthMultiplier is additive; adapters spread-merge user config, no downstream break. NudgeConfig addition is non-breaking until adapters bump their kernel pin.
  • Idle reason format changed (ready/blocked hints) — adapters log it verbatim, no parser depends on it; only e2e tests asserting exact idle strings would notice.

239 tests pass because nothing covers the 75–80% band or the emergency-with-nothing case — both worth a test before merge.

@ranxianglei

Copy link
Copy Markdown
Owner Author

Fixes for both regressions are up in #70 (contains this PR's commits via merge + current master, which also resolves the stale emergencyThresholdPct: 0.80 against master's 0.95 truncate tuning). Two new regression tests are red on this PR as-is and green on #70. Suggest closing this in favor of #70 after CI.

@ranxianglei
ranxianglei merged commit 9d0b068 into master Aug 15, 2026
3 checks passed
Tyan66666 pushed a commit to Tyan66666/acp-kernel that referenced this pull request Aug 18, 2026
…nudges on effective pending

Review fixes for ranxianglei#57 (two regressions):

1. The 75-95% over-limit band was deleted — maxContextLimitPct had no
   consumer. Restored as the pressure band: bypasses growth gate +
   cadence, picks the tier with the MAX pending (keeping ranxianglei#57's tier-aware
   argmax and effective-pending semantics), labeled OVER-LIMIT vs
   EMERGENCY by which threshold fired.

2. Emergency with zero effective pending injected every turn — and could
   offer sub-minCompressRange tail ranges that would be atomically
   rejected (the exact failure mode ranxianglei#57 fixes, resurfacing under
   pressure). Pressure nudges are now gated on effective pending > 0;
   the suppressed case reports why instead of spamming.

Also merges master (truncate machinery, emergencyThresholdPct 0.95) into
the branch: pr-57's arbitration is preserved, config keeps master's
tuning + the new tier2GrowthMultiplier.

295 tests (+2 regression tests that are red on unpatched ranxianglei#57).
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.

1 participant