feat(desktop): show the context ring what a compaction just freed - #700
zhangqingkun976 wants to merge 1 commit into
Conversation
|
#688 / #700 / #705 are all included in #721 These three are the same work split small. I have assembled them (plus the Nothing in them changed; #721 is strictly a superset. Review whichever shape you |
…r-model context settings # Context and compaction, as one set This assembles the context/compaction work into a single reviewable change: the model can read its own history back, compaction stops being a one-way door, and the two thresholds that decide *when* it happens are configurable per model. It subsumes my three open PRs (vastsa#688, vastsa#700, vastsa#705) — those are the same code, split differently; review whichever shape you prefer. ## 1. The model can read its own history (`recall`, `recall_project`) **Host.** `recall_transcript` searches a session's complete transcript and ranks candidates by match strength (how many query words, how often), and `read_message_text` pages one message back in bounded character windows — which is how a tool result (not in the word index) is read. `search_project_messages` + `read_project_messages` do the same across one project's sessions, and both take a project id the host resolves from a path; a session bound to another project reads as *not found*, so session existence never leaks across projects. `SleepRecord` + `append_sleep` add a `sleep` line kind to the transcript: the layout scan ignores it and it never counts as a message. Five RPC methods carry this: `session.recall`, `session.readMessage`, `search.query`, `session.readProject`, `session.appendSleep`. **Model side.** `packages/agent-runtime/src/recall-tools.ts` holds both tools and takes a two-method host adapter (so both are unit-testable without a session, a provider or a process). Both are registered in the **core** tool set: a capability the model has to go looking for is one it will not reach for while it is trying to recover what a summary left out. The query rule is the host's, not a preference, so the description states it (split into words, every word must appear, unspaced Chinese matches literally) and tells the model to write the query in the words of whoever wrote the message. An empty answer says what to try instead; a bare "no matches" teaches nothing. ## 2. Compaction stops being a one-way door - **The projected summary carries a recall pointer**: pre-boundary messages are not deleted, and `recall` reads them back verbatim. The pointer is a promise, so it is only appended because the tool exists. - **A session ledger** is built mechanically when a checkpoint is installed (files read/modified, commands, message and tool-call counts, plus the goal and the unresolved items) and stored on the checkpoint's opaque `details`. The projection renders it as a bounded block after the summary, so the next window keeps an account of what the boundary covered — text that is one `recall` call away. A checkpoint written before the ledger existed projects exactly the pointer and nothing else. - **The wording now matches the mechanism.** The second budget reminder (Codex's `AutoCompactFallbackPrompt`: "unsummarized detail will not be available afterwards") is deleted rather than rephrased — that sentence is false here. The rollover text says where the messages went and how to read them again instead of telling the model the transcript is available "to the user". ## 3. Two thresholds, per model | Setting | Default | What it does | | --- | --- | --- | | `dynamicContext.thresholdPercent` | window-aware (`100 − max(100k, 15%×window)/window`, clamped 40–95) | At this share of the hard limit, old tool results are shortened to a head plus a recovery pointer. Off means nothing is narrowed. | | `earlyCompaction` `{thresholdPercent, delaySeconds, silent}` | 75 / 120 s / silent | After a run settles and the session stays idle that long, the pass compacts **in the background**. Any new prompt stands it down before it spends its summary request. | | `sleepTime` `{enabled, maxRunsPerHour}` | off / 2 | On the idle arm, write a deterministic digest of where the work stands to the transcript (no model call), so a summary request that never returns still leaves the next window with the goal and the open items. | The **coupling** is the point: the narrowing gate is what the outgoing request, the hard-boundary check and the early pass all read, so what narrowing saves is visible to compaction instead of compaction firing while real room remained. The early pass is the one place that spends a summary request off the critical path, and it is silent **only when it succeeded** — a degraded pass warns exactly like the inline path, and the hard boundary always warns. `compaction_end` reports `idle` and `silent`; the transcript row, the context inspector and `recall` record the checkpoint either way, and the renderer skips only the routine toast. ## Verification (Windows 11) cargo test -p host-core --bin pi-desktop-host-core 555 passed, 4 failed cargo fmt / clippy clean (1 pre-existing warning) pnpm --filter @pi-desktop/shared typecheck|build exit 0 pnpm --filter @pi-desktop/agent-runtime typecheck exit 0 pnpm --filter @pi-desktop/agent-runtime test 45/46 files pnpm --filter @pi-desktop/desktop typecheck exit 0 - The 4 Rust failures are pre-existing Windows path-separator assertions in `mcp_servers`, `user_skills`, `scheduled_rpc` and `scheduled_tools`; two of those files are untouched by this change (hash-verified). - The 1 agent-runtime failure is the pre-existing `src/native-pi-session.test.ts` case ("never deletes a foreign publication…"), which fails identically without this change. - New tests: 6 Rust (recall matching/ranking, non-ASCII folding, character paging, sleep append + layout, project search scoping, project read isolation), 10 for the recall tools (both tools, paging, the empty-answer advice, the output cap, project scoping, a host failure surfacing as text), 2 for the summary projection (pointer + ledger; a pre-ledger checkpoint projects the pointer alone). - Four existing tests were updated to the new contract, each strengthened rather than relaxed: the core tool list now includes both recall tools; the budget reminder is asserted to have **one** tier (and the sharper second one to be absent); the projected summary is asserted to carry the recall pointer; the dormant second-tier flag is gone. ## What is deliberately not here - **The settings pane UI.** The three settings are typed, clamped, persisted with the model binding and carried on the launch payload, but the pane's controls and their translations are not in this change — so today they are set through the binding rather than the UI. That is the next piece. - **Estimate calibration (vastsa#683).** The reviewer asked for a one-way (never lower) correction and a ratio-based unanchored term before it lands; that rework is in progress and will come as its own PR. - **The semantic/embeddings recall channel.** Measured and frozen in my build: it needs a model asset and a host channel for a gain the lexical channel already gets on the queries that matter here.
2dbefa0 to
857a22d
Compare
|
Rebased onto Local before pushing: This PR is also part of #721, if you would rather review the feature as a whole. |
5df43d5 to
f78b337
Compare
|
Rebased onto The only conflicts were append-vs-append — the E2E scenario catalog and the decisions log, where main and this branch each added an entry — and both sides are kept. The identifier bands are still free: upstream's newest ADR is 0299 and its newest decisions-log entry is D607 (the plugin crash report, which is this repository's own fix for issue #747 built on my #756). |
f78b337 to
c7ce845
Compare
|
Rebased onto Local gates on the rebased head:
The description now carries the base/head line and the E2E status in full. |
c7ce845 to
e98d0db
Compare
|
Rebased onto It applied cleanly, including the Local gates on the rebased head: One thing to know when reading the JS job: |
Rebased onto main (79cd7ae); the rebase applied cleanly, no conflicts. A compaction leaves the message transcript untouched by design, so the ring kept showing the pre-compaction request until the next provider response landed. `CompactionRecord.tokensAfter` (optional; a checkpoint written before the field existed reads as absent) carries the post-compaction estimate, the mark and the store carry it, and the inspector leads with it until a newer real request reports usage. C:\Users\10470\.pi-desktop\scratch\cb9e2d41-8a55-4a18-899d-92ca2791ab51\commit-700-r2.txt
e98d0db to
ce620f0
Compare
|
Correction to my previous comment on this PR. I wrote that pristine What actually happened: my local After rebuilding
The two real baseline failures are:
So This head Apologies for the noise — and for stating a baseline I had not verified. The CI panel on this head is the authoritative record, and it is green. |
When a compaction lands, the context ring keeps showing the pre-compaction
request: the checkpoint changes what the next request will carry, but the ring
only updates when a provider response reports usage, so until then the UI
quietly understates the available room by the whole compacted range.
Nothing on disk records what the window shrank to. The visible transcript is
untouched by a compaction — that is its point — so the only place the new
occupancy can live is the checkpoint itself.
This stamps it there, and lets the ring lead with it:
CompactionRecord.tokensAfter(Rust + shared type): optional, so atranscript written before the field existed loads with
Noneand an olderreader ignores the extra key on a newer line. A test writes a legacy
checkpoint line by hand and reads it back.
persistCheckpointcomputes it: the estimate of the compacted projection,plus the gap between
tokensBefore(a measured request size) and thepre-compaction estimate — that gap is the system prompt and tool schemas the
next request pays again, and without adding it back the number would not be
comparable with the request-based occupancy the ring shows elsewhere.
contextCompactionMarkcarries it onto the mark, so the livecompaction_endevent and the store both have it without extra payload.latestTurnContextInspectorsurfacesestimatedOccupancyTokensonlywhile the newest checkpoint is newer than the latest usage-bearing message —
the next real request replaces it, and the popover's provider rows keep the
last real usage either way.
ContextUsageInspectorleads the ring and heading with the estimate andprefixes it with
≈, because it is an estimate, not a measurement.Verification (Windows 11; re-run on the rebased head
ce620f07, with@pi-desktop/sharedrebuilt first):Three new tests cover the inspector's rule — leads with the estimate after
compaction, drops it once newer usage arrives, ignores marks without one — and
one shared test covers the record → mark hop. Mutation-checked: removing the
"newer usage wins" condition turns the second test red and restoring it turns
the suite green again.
One existing runtime test changed: the strict
markassertion in "compacts atthe turn boundary" now also requires
tokensAfter: expect.any(Number), whichis the new contract, not a relaxation.
Note on CI: this branch used to inherit the
Typecheck Desktopfailure fromuser-login-path.ts; that one-line fix landed inmainas #692, so therebased head does not carry it.
Base and head
base 206085c07· headce620f07· 10 files, +221 / −7. Rebased onto currentmain(82 commitsfurther on); it applied cleanly except for one same-point insertion in
ModelSelectionPanes.tsxon the branches that touch it, where both sides werekept. The published tree is verified to be the same object the gates ran on: