feat: per-model context limits — ink compacts before the provider (by Wren) - #449
Conversation
…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
left a comment
There was a problem hiding this comment.
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
…(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
left a comment
There was a problem hiding this comment.
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
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 (taska61d7320).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.tsowns: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 existing0.8in-budget threshold, ink compacts by ~0.68 × window worst-case — comfortably ahead of the provider.chat.tsimports the two functions under their existing names, so every call site (runtime construction +/modeland/backendswitch 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-5beatsgpt-), 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 acompactionevent (removedCount > 0), rolled the native session (freshbackend_sessionmarker + native jsonl), and exited 0. Deterministic (independent of LLM phrasing); opt-in viaINK_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