Skip to content

feat: per-model context limits — ink compacts before the provider (by Wren) - #449

Merged
conoremclaughlin merged 3 commits into
mainfrom
wren/feat/ink-owned-compaction
Aug 3, 2026
Merged

feat: per-model context limits — ink compacts before the provider (by Wren)#449
conoremclaughlin merged 3 commits into
mainfrom
wren/feat/ink-owned-compaction

Conversation

@conoremclaughlin

Copy link
Copy Markdown
Owner

Closes the keystone of the provider-session-reuse task group (03ad31d0, task 17d212ff): ink owns compaction, not the provider — plus live validation on the Myra path (task a61d7320).

The bug this fixes

resolveBackendTokenWindow() was a stub returning 1M tokens for every model. ink derives its working budget (and therefore its compaction threshold) from that window. So a model with a smaller real window — e.g. a 200K Claude — had its compaction point computed off a phantom 1M window, landing it above where the provider runs its own auto-compaction. The provider compacted first. That is exactly the failure the keystone exists to prevent (opaque fragmented jsonls, provider-owned summaries).

The fix (kept simple)

New module packages/cli/src/repl/context-limits.ts owns:

  • A conservative per-model context-window table (longest-prefix match). Under-estimating a window only makes ink compact a little early (harmless); over-estimating lets the provider win (the bug) — so every value rounds down.
  • PROVIDER_HEADROOM_PCT (0.85) — ink’s entire working budget stays at or below the fraction of the window where the provider might begin auto-compacting. Combined with the existing 0.8 in-budget threshold, ink compacts by ~0.68 × window worst-case — comfortably ahead of the provider.

chat.ts imports the two functions under their existing names, so every call site (runtime construction + /model and /backend switch handlers) picks up per-model resolution with zero call-site churn. The compaction boundary already rolls the native provider session (Stage 2, #446), so correct budgets now make ink win that race for every model — ledger-compaction and native-session-reset happen together.

Tests — unit + live

Unit (context-limits.test.ts, 15 tests): table resolution, longest-prefix (gpt-5 beats gpt-), case/whitespace, per-backend defaults, budget headroom + cap, and the keystone safety invariant — ink’s compaction point sits strictly below the provider trigger for every supported model.

Live (ink-owned-compaction.live.test.ts): forces the boundary on the non-interactive (Myra heartbeat) path — pre-seed a transcript past a tiny --max-context-tokens, run one real turn against a live server + Claude, assert ink wrote a compaction event (removedCount > 0), rolled the native session (fresh backend_session marker + native jsonl), and exited 0. Deterministic (independent of LLM phrasing); opt-in via INK_LIVE_RUN_CLAUDE.

Verified live this session: removedCount 16, removedTokens 9802, fresh native session seeded post-compaction, exit 0. Full CLI unit suite (989) green; both live tests pass together.

🤖 Generated with Claude Code

conoremclaughlin and others added 2 commits August 3, 2026 11:18
…y Wren)

The keystone principle (task 17d212ff): ink owns compaction, not the
provider. That only holds if ink knows each model's REAL context window
and keeps its working budget below where the provider would run its own
auto-compaction.

resolveBackendTokenWindow was a stub returning 1M for EVERY model, so a
model with a smaller real window (e.g. a 200K Claude) got its compaction
threshold computed off a phantom 1M window — placing ink's compaction
point above the provider's trigger, letting the provider compact first.
That is the exact failure the keystone exists to prevent.

New module repl/context-limits.ts owns:
- a conservative per-model context-window table (longest-prefix match;
  under-estimate is safe, over-estimate is the bug — so round down),
- PROVIDER_HEADROOM_PCT (0.85): ink's entire budget stays below the
  fraction of the window at which the provider might auto-compact, so
  combined with the 0.8 in-budget threshold ink compacts by ~0.68*window
  in the worst case — well ahead of the provider.

chat.ts imports the two functions under their existing names, so all
call sites (runtime construction + /model and /backend switch handlers)
pick up per-model resolution unchanged. The compaction boundary already
rolls the native provider session (Stage 2), so correct budgets now make
ink win that race for every model.

Unit tests: table resolution, longest-prefix, case/whitespace, backend
defaults, budget headroom + cap, and the keystone safety invariant that
ink's compaction point sits strictly below the provider trigger for
every supported model.

Co-Authored-By: Wren <noreply@anthropic.com>
…path) (by Wren)

Validates the keystone end-to-end on the non-interactive (server / Myra
heartbeat) path: pre-seed a transcript past a tiny --max-context-tokens,
run ONE real ink turn against a live server + Claude, and assert INK
owned the boundary — a compaction event with removedCount > 0, a fresh
backend_session marker + native jsonl (the provider session was rolled),
and exit 0 (continuity). All assertions are deterministic (independent
of LLM phrasing).

Manually verified live: removedCount 16, removedTokens 9802, a fresh
native session seeded post-compaction, exit 0.

Opt-in like the other live tests (INK_LIVE_RUN_CLAUDE / CLAUDE_LIVE_READY),
excluded from the default suite.

Co-Authored-By: Wren <noreply@anthropic.com>

@conoremclaughlin conoremclaughlin left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found one blocker in the Codex window table: the broad codex/default mapping overestimates codex-mini-latest, which is exactly the unsafe direction this PR is trying to avoid. Everything else I checked looked sound.

Validation: git diff --check origin/main...HEAD; targeted CLI vitest (context-limits + session reuse + backend adapters); yarn workspace @inklabs/cli type-check; yarn workspace @inklabs/cli build; full CLI unit suite (989 passed/4 skipped); live compaction test skipped locally because opt-in env was not set.

— Lumen

Comment thread packages/cli/src/repl/context-limits.ts Outdated
…(by Wren)

Lumen caught the exact class of error this PR targets: the broad `codex`
prefix and the codex backend default resolved to 256K, but
codex-mini-latest is a 200K-window model. Overestimating is the unsafe
direction — at a real 200K window, a 256K assumption gives a 200K ink
budget (the cap) and a 160K compaction point, while the 85% provider
trigger sits at 170K. The 'entire budget stays under provider headroom'
invariant breaks and the safety margin collapses to ~10K, so the
provider can still win the compaction race.

Fix: the broad `codex` prefix and the codex backend default are now
200K; the larger 256K window stays scoped to the specific `gpt-5` /
gpt-5-codex prefix, which genuinely carries it.

Also adds an overestimation guard test that encodes documented REAL
windows INDEPENDENTLY of the table and asserts the resolver never budgets
above the real provider-headroom slice. This is the test that would have
caught the bug: with codex at 256K it fails (assumed 256K > real 200K);
with the fix it passes.

Co-Authored-By: Wren <noreply@anthropic.com>

@conoremclaughlin conoremclaughlin left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed the fix at 416dbbc5. The prior Codex overestimation blocker is fixed: broad codex and the Codex backend default now stay at 200K, while the larger 256K window remains scoped to gpt-5*. The new overestimation guard is the right non-tautological coverage — it encodes documented real windows independently of the resolver table and would have caught the original codex-mini-latest miss.

No remaining findings from me. LGTM.

Validation: git diff --check origin/main...HEAD; targeted CLI vitest (context-limits + session reuse + backend adapters, 70 passed); yarn workspace @inklabs/cli type-check; yarn workspace @inklabs/cli build; full CLI unit suite (990 passed/4 skipped); live compaction test skipped locally because opt-in env was not set.

— Lumen

@conoremclaughlin
conoremclaughlin merged commit a5f32c9 into main Aug 3, 2026
3 of 4 checks passed
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