Skip to content

feat: context x-ray panel with compaction controls - #10462

Open
filipkujawa wants to merge 6 commits into
mainfrom
feat/context-power-tool
Open

feat: context x-ray panel with compaction controls#10462
filipkujawa wants to merge 6 commits into
mainfrom
feat/context-power-tool

Conversation

@filipkujawa

@filipkujawa filipkujawa commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

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_MODEL setting, which falls back to GOOSE_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

Screenshot 2026-07-28 at 2 55 35 PM

@filipkujawa
filipkujawa marked this pull request as draft July 15, 2026 05:17
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.
@filipkujawa
filipkujawa force-pushed the feat/context-power-tool branch from 7b7002e to 5fa24d9 Compare July 27, 2026 23:18
@github-actions

Copy link
Copy Markdown
Contributor

Documentation preview deployed: https://pr-10462.goose-pr-previews-poc.pages.dev

@filipkujawa
filipkujawa marked this pull request as ready for review July 28, 2026 22:13

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

SELECT model_id, name, family, context_limit, reasoning, recommended

P1 Badge Add a migration for provider inventory recommendations

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".

@michaelneale
michaelneale removed their request for review August 10, 2026 04:17
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.

2 participants