feat(desktop): show context usage beside the model controls - #4576
Draft
Joob1n wants to merge 8 commits into
Draft
feat(desktop): show context usage beside the model controls#4576Joob1n wants to merge 8 commits into
Joob1n wants to merge 8 commits into
Conversation
6 tasks
Joob1n
force-pushed
the
feat/context-usage-indicator
branch
2 times, most recently
from
September 2, 2026 16:06
bb02be2 to
07eb5d4
Compare
The runtime no longer estimates whether a request fits a context window.
Every "does it fit" question is answered by a provider: the conversation
model's own context-length rejection is recovered by one compact-and-retry,
and the summarizer's provider answers for compaction input (input_too_large
retreats the fold by half). The chars/4 payload ruler, the signed delta
estimate, the 32,000-token fallback history budget, the quarter-window
reserve, the replacement-not-smaller and prefix-over-budget replay gates,
and the final-request rescue re-entry are removed.
Proactive compaction keeps one trigger: the previous accepted request's
real input plus output tokens, as the provider counted them, compared with
the context window the user declared for the model (a model-facts pin or a
relay profile). A provider's /models report and generated metadata are no
longer a threshold on their own. With no declaration there is no proactive
fold; the provider decides. A reply the provider cut at its output limit
(finishReason length) folds once before the next request.
The persisted last-request anchor becomes { inputTokens, outputTokens };
the retired payloadChars key still decodes so 0.2.0 sessions keep loading.
Summaries are capped at 8,000 output tokens with one shorter retry, and the
too-small-for-fold floor reads the summarizer call's real usage instead of
a chars/4 estimate. Two user-visible notes explain provider-side context
changes: context_provider_dropping (an append-only step whose usage did not
grow) and context_window_suggestion (a rejection at a proven-fit total,
with the number the user can declare).
Closes apache#4559
Refs apache#4458, apache#4486
Generated-by: Claude Code
Claude-Session: https://claude.ai/code/session_014ajaRxC4jydavY9nYUFj5J
Usage is the only signal the runtime's context handling now reads: the proactive compaction baseline, the eviction check, the window suggestion and the usage indicator all start from the provider's own token counts. A Chat Completions server returns none unless the request carries `stream_options.include_usage`, and the factory only sent that for the two providers whose registry entries opted in. A local Ollama and every plain OpenAI-compatible relay — the connection type behind apache#4458 — therefore produced no usage at all, and everything downstream stayed dark. Live probe against Ollama (qwen2.5:0.5b): before, the finish chunk carried an empty usage object and no token_usage event was emitted; after, every turn reports its input and output and persists the anchor. The default is now on for the OpenAI-compatible Chat adapter; a registry entry can still opt out with `includeUsage: false`. Refs apache#4559 Generated-by: Claude Code Claude-Session: https://claude.ai/code/session_014ajaRxC4jydavY9nYUFj5J
… can answer The folded span handed to the summarizer usually ends on an assistant message. A chat-template model given a conversation that already ends with its own turn emits an end-of-sequence token and nothing else — observed live on Ollama with qwen2.5:0.5b and qwen2.5:1.5b: finish `stop`, one output token, empty text, on every compaction attempt. The write gate then fails the fold open, so history compaction never succeeded against a local model. The request now closes with a user instruction to write the structured summary; on the first attempt and on the repair. Live against Ollama (qwen2.5:1.5b, declared 1,500-token window): the fold that previously produced an empty summary now writes a 1,737-character checkpoint after one repair, and the next request's provider-counted input drops from 2,954 to 1,222 tokens. Refs apache#4559 Generated-by: Claude Code Claude-Session: https://claude.ai/code/session_014ajaRxC4jydavY9nYUFj5J
The two user-visible compaction notes keyed on the `priorReplay` stage only. Since apache#4486 every new fold happens in the request-projection hook (`activeStep`), so the turn that was actually compacted showed nothing and the note arrived one turn later, when the checkpoint was replayed; a fold that failed open in the hook was never surfaced at all. Live against Ollama a fold succeeded and a fold failed in consecutive turns with no note either time. Both predicates now accept a history-compaction decision from either stage; the once-per-send flags in the backend are unchanged. Refs apache#4559 Generated-by: Claude Code Claude-Session: https://claude.ai/code/session_014ajaRxC4jydavY9nYUFj5J
Review findings from apache#4574, each with a test that fails on the previous commit: - The proactive trigger now reserves the model's declared output limit (`connection.models[].maxOutputTokens` or generated metadata, a provider fact; 0 when none is declared) and fires on `>=`: with the window declared at the provider's real size, an accepted request alone can never exceed it, and the reply the next request must leave room for is what tips it. The window itself stays the user's declaration. - The summarizer's provider is a decider again: a context-length rejection from the summary call (or the Codex compaction call) surfaces as `input_too_large`, the reason the planner retreats on. Removing the estimated pre-fit had left that path without a producer. - The eviction check compares input against input: the previous reply's reasoning is not resent on every wire, so input + output is not a floor. - The window suggestion is written after recovery, only when the turn surfaces the rejection, and names the last accepted total, which a fold does not clear. It is no longer a number on the `>` boundary. - The size floor judges an initial fold only; on a roll-forward the summarizer's input is the increment, not the covered span. - `CheckpointSummaryFoldContext` carries usage only; the builder and copy seams validate structure and truncation, which is all they can. - The CLI transcript renders the two new note kinds. - Restored coverage: a synthetic /compact usage row does not shadow the real anchor; the run-header check is exercised with an anchor above the window. - CHANGELOG carries the downgrade and epoch sentences again. The Host's reported `contextWindow` stays the model's window: it is a protocol fact clients already display, and the declared window has its own surface in the composer indicator. Refs apache#4559 Generated-by: Claude Code Claude-Session: https://claude.ai/code/session_014ajaRxC4jydavY9nYUFj5J
…audit `npm audit --omit=dev` reports two advisories against production dependencies: qs 6.15.3 (moderate: array-limit bypass via bracket-key comma parsing; DoS via attacker-controlled isBuffer), reached through @larksuiteoapi/node-sdk in @maka/runtime, and fast-uri 3.1.x (high: host confusion and SSRF via IDN, IPv6 and percent-decoding normalization). The "Build CLI release candidate" CI step and the Dependency audit workflow fail on any such advisory, so every PR on main is red until the lockfile moves. qs resolves to 6.16.0 and fast-uri to 3.1.7; the production dependency notices are regenerated for the new versions. Generated-by: Claude Code Claude-Session: https://claude.ai/code/session_014ajaRxC4jydavY9nYUFj5J
Two new system_note kinds and the reshaped lastRequestAnchor land in @maka/core's closed allowlists. A merge-base client handshakes on strict epoch equality and would then fail to decode the first transcript carrying them; the epoch moves so the pair refuses each other at the handshake. Refs apache#4559 Generated-by: Claude Code Claude-Session: https://claude.ai/code/session_014ajaRxC4jydavY9nYUFj5J
A read-only indicator in the composer's model controls shows the latest request as the provider counted it: input plus output tokens of the last accepted request, read from the session's newest token_usage record. With a user-declared Maka window it shows the percentage (over 100% is shown as such, never clamped); without one it shows the absolute count and names the model's reported window in a tooltip; without usage it shows a dash and says the provider reported none. Chat model choices carry the reported and the declared window separately so the two are never confused (apache#4559). Refs apache#4559 Generated-by: Claude Code Claude-Session: https://claude.ai/code/session_014ajaRxC4jydavY9nYUFj5J
Joob1n
force-pushed
the
feat/context-usage-indicator
branch
from
September 2, 2026 18:12
07eb5d4 to
a4df755
Compare
Contributor
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Stacked on #4574 (the first commit here is that PR; review only the last one).
A read-only context-usage indicator in the composer's model controls, to the right of the thinking-level control. It shows the latest request as the provider counted it — input plus output tokens of the last accepted request, read from the session's newest
token_usagerecord (lastRequestAnchor, the shape #4574 introduces) — and never a local estimate.buildChatModelChoicesnow carries the reported window (contextWindow) and the declared window (declaredContextWindow) as separate fields, resolved by core's single owner of the declaration rule, so the two are never confused downstream.Refs #4559
Verification
npm --workspace @maka/{core,ui} run build,tsc --noEmitfor core and ui,npm --workspace @maka/desktop run typecheck,npm run check:renderer-architecture -- --base origin/main,npm run check:app-shell-hooks,npm run astryx:surface-inventory,npm run lint,npm run format:check— clean.llm-connections.test.js13/13. Not run locally: the desktop main suite (chat-composer-region-draft-handoff.test.tsgains the new requiredmessagesprop) and Desktop e2e — relying on CI.Review focus
app-shell.tsxcarries two small unrelated rewrites (newChatProviderType, theactiveSessionForViewannotation) that pay for the newmessagesprop under the renderer ratchet: the file's import declarations and specifiers go down by one each, so the debt counts do not increase. Say if you would rather see the ratchet entry adjusted instead.chat-composer-region.tsxscans the message list backwards on each render to find the newesttoken_usage. It is a legacy-ratchet file, so auseMemowould count as a new hook call; the scan stops at the first match, which is normally the last message.AI use
Select exactly one:
Tool(s) and scope: Claude Code — implementation; reviewed and verified by the author.
Checklist
Does this PR entail a change in behavior?
https://claude.ai/code/session_014ajaRxC4jydavY9nYUFj5J