feat: context x-ray panel with compaction controls - #10462
Conversation
Adds a typed ACP method (_goose/unstable/context/report) reporting per-segment token accounting for the next request: system prompt, extension instructions, hint files with provenance, turn context, per-extension tool definitions, and messages. Segments sum exactly to the wire total via marginal tool diffing plus a residual overhead segment. The desktop context window indicator now opens an x-ray sheet with a capacity meter and category drill-down, replacing the bottom-menu alert popover. The sheet includes compaction controls: auto-compact threshold, tool-pair summarization with keep-last-N, and a compaction model picker backed by GOOSE_COMPACTION_MODEL (resolution falls back to GOOSE_FAST_MODEL, then the provider fast model; the main model remains a runtime fallback and compaction pre-sizes against the compaction model's context window). Localized in all 16 locales. Covered by an ACP E2E test and unit tests for the accounting invariants.
…egory Structured compaction (#10471) turned the post-compaction summary into a template-rendered artifact with named sections, but the x-ray still reported it as one anonymous message inside "Conversation" - so the largest single block of retained context was the least legible thing in the panel. Compaction now marks its summary message, and the report gives it a `compaction_summary` category whose parts are the rendered `##` sections, each with its own token and character count. Section boundaries come from the rendered markdown rather than the parsed StructuredSummary, so the breakdown reports what the context window actually holds even when compaction_summary.md has been customized to drop or rename sections. Fenced blocks are skipped over because key_code can legally contain heading lines. Summaries that fell back to raw model output are labelled as such and keep a whole-text preview instead of invented sections, since splitting arbitrary prose on `##` would fabricate structure that isn't there. Segment totals are unchanged: the summary segment carries the same count_message_tokens value it contributed as a message part before.
Review of the panel turned up several places where the numbers it shows were quietly wrong. Token accounting. count_message_tokens is now the single per-message counter; three call sites went through count_chat_tokens with an empty system prompt and no tools, which adds the reply primer and so inflated every message by 3 tokens. UI-only content blocks (tool confirmation requests, action-required, system notifications) no longer count at all, since every format converter drops them before the request goes out. RedactedThinking is deliberately still counted - anthropic.rs does put it on the wire. Prompt reconstruction. build_segments rendered the template a second time with instructions blanked, which silently ate literal scaffolding such as the "### Instructions" headings. It now renders once with per-extension markers and strips those, so the segments reconstruct the built prompt byte for byte - asserted by a new test. That also collapses three renders per report down to one. Parts summing to their segment. Hint parts now marginal-diff the combined block, so the scope headers are charged to the file that introduces them. Toolshim parts were measuring pretty JSON while their parent measured the rendered appendix, roughly double; both now measure the appendix, with the fixed calling-convention preamble carried as an explicit part rather than hidden. The e2e test now asserts the invariant for every segment. Hints were also loaded twice per report - once for the reply prompt and once for the breakdown, so the panel could disagree with the prompt it was describing. prepare_tools_and_prompt does the single read and passes the sources through ReplyContext; SystemPromptBuilder::with_hints is deleted so no second path exists. Likewise the session was loaded twice per request; build_context_report takes working_dir from the row it already reads. COMPACTION_PROMPT_FILL replaces a hard-coded 0.8 that was being confused with GOOSE_AUTO_COMPACT_THRESHOLD. They are different questions: a user who compacts at 50% still wants the whole compaction window used for the summarization prompt, and a disabled threshold would otherwise yield a budget of zero.
…ion model The report types are consumed by the TypeScript SDK, so the things a client cannot infer from a field name are now written down: that estimatedTotalTokens is the segment sum and never below wireTotalTokens (the synthetic "Prompt overhead" segment absorbs the difference), that wireTotalTokens is a local-tokenizer estimate of the next request while liveTotalTokens is the provider's own count for the last turn only and therefore describes a past request, and that `source` is free-form display provenance that must not be parsed. Documenting the ContextCategory variants makes schemars emit oneOf/const instead of a flat enum, so the generated zod goes from z.enum to a union of literals. Same emitted TypeScript type, and it matches the existing SessionSystemPromptMode precedent. Also restores three doc comments dropped earlier in this branch and documents GOOSE_COMPACTION_MODEL, which was added without an entry in the environment variable guide.
The panel could not express the one value that actually disables auto-compaction. The engine treats a threshold outside (0, 1) as off and the ACP validator accepts exactly 1.0, but the control capped at 99%, so a config already holding 1.0 was displayed as "99%" - an outright lie. 100% is now an accepted input that persists 1.0, and the load path shows a disabled config truthfully instead of clamping it. Out-of-range input reverts to the last saved value rather than being silently substituted; the keep-last field previously clamped its upper bound, so typing 900 quietly became 500. Writes are now guarded by a per-field monotonic id, so a slow rejected write can no longer roll the control back over a newer value the user has since set. The error state only cleared on the fetch entry point, so a panel that had failed once stayed in an error state through later successful background refreshes; it now clears on any success. The panel did not refresh after compaction at all - the promise from onCompact is dropped by BaseChat's void-typed submit wrapper, so it instead refetches on the agent's busy-to-idle transition, and "Compact now" is disabled while a turn is in flight. Two token formatters disagreed at the boundary, one rendering 999.6k while the meter beside it read 1M / 100%. formatTokenCount is now the single implementation with one boundary and one rounding rule. The tokenizer-overhead legend row and meter band were dead: the Rust builder already folds any residual into an explicit "Prompt overhead" segment, so wireTotalTokens - segmentTotal is never positive. Removed, along with the now-unreferenced message and an unused event constant. Adds the first tests for this panel: 17 for the compaction controls, plus coverage of the sheet's fetch state machine, the formatters, and the indicator's accessible name, which previously replaced the token counts with a bare label.
…st rows format.ts re-exported formatTokenCount purely so the three x-ray components could import it and formatPercentOf from one place. The indirection bought nothing and needed a test asserting it was still the same function object; the components now import the canonical formatter directly and both the re-export and its guard test are gone. Also trims table rows that exercise a branch already covered: three percent cases that all hit the total <= 0 guard or the sub-1% floor, two precise-mode values above the single 999_500 rollover boundary, one out-of-range threshold variant, and a concurrency test that repeated the threshold scenario on the cutoff field without crossing fields, which is the only part the per-field write guard adds.
7b7002e to
5fa24d9
Compare
|
Documentation preview deployed: https://pr-10462.goose-pr-previews-poc.pages.dev |
There was a problem hiding this comment.
💡 Codex Review
For users with an existing goose data DB created before this change, provider_inventory_models already exists without the new recommended column, and CREATE TABLE IF NOT EXISTS will not add it. Any cached inventory read through _goose/unstable/providers/list now executes this SELECT and fails with no such column: recommended (refresh writes will fail similarly), breaking provider/model loading until the DB is recreated; please add a schema migration/ALTER for the column before reading it.
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
Summary
The context window is a single number in the bottom bar today.
This adds a way to see what is actually in it, and to control how compaction trims it.
A typed ACP method,
_goose/unstable/context/report, that breaks the next request into per-segment token counts: system prompt, extension instructions, hint files with provenance, turn context, per-extension tool definitions, the compaction summary (by rendered##section), and messages.Segments sum exactly to the wire total.
An x-ray sheet in the desktop app, opened from the context window indicator: capacity meter plus category drill-down.
Replaces the bottom-menu alert popover.
Compaction controls in the same sheet: auto-compact threshold, tool-pair summarization with keep-last-N, and a compaction model picker backed by a new
GOOSE_COMPACTION_MODELsetting, which falls back toGOOSE_FAST_MODEL, then the provider's fast model, then the main model.Building the panel surfaced a few accounting bugs, fixed here: per-message counts included the reply primer, UI-only content blocks were billed as context that never reaches the wire, and the threshold input could not express the one value that disables auto-compaction.
Most of the diff is generated: 16 i18n catalogs, the ACP schema, and the SDK types.
Testing
An ACP E2E test, unit tests for the accounting invariants, and UI tests for the sheet and the compaction controls.
Manually verified in the desktop app against live sessions, including a real compaction.
Localized across all 16 locales.
Related Issues
Builds on structured compaction (#10471), which is what gives the compaction summary named sections to break down.
Screenshots/Demos