Expand description
What a workspace’s agent forgets, and when.
mentra shrinks an agent’s history in two unrelated ways, and only one of them is what the word compaction suggests.
Micro-compaction runs on every provider request
(Agent::micro_compacted_history, reached from mentra’s turn runner before
each call). It walks the tool results in the transcript and replaces the
content of every one past the most recent keep_recent_tool_results with
[Previous: used <tool>], for any result over 100 bytes. There is no token
budget in that decision: it happens on the fourth tool call of a conversation
as readily as on the four-hundredth, on a 1M-token model as readily as on a
small one. Mentra 0.23 makes the request-only rewrite observable through
Event::RequestToolResultsElided,
without changing the canonical transcript. mentra’s own default is
usize::MAX — keep everything — and basis agrees, so this is one knob
basis states only to make the same default explicit: elision is opt-in a
host asks for by number
(with_keep_recent_tool_results),
which is exactly how mentra models the off switch it already has:
keep_recent == usize::MAX returns the history untouched.
Real compaction is the summarizing pass: the transcript is written to a
snapshot file, an older prefix of it is replaced by a model-written summary,
and the recent tail is preserved. It fires three ways, and it announces
itself the same way every time
(Event::CompactionStarted /
CompactionCompleted):
-
The estimated request size crosses
auto_threshold_tokens— an absolute token count, for a model whose context window basis does not know. -
Or, when the model’s window is known (
ModelInfo::context_window— populated from Gemini’s model listing,Nonefor Anthropic and the Responses transport), it crossesauto_threshold_percentof that window instead, which wins over the absolute number whenever both apply. This is the knob that did not exist until mentra could ask the model itself how big it is: 50,000 tokens is most of a small model’s window and a rounding error in a 1M-token one, so no single constant was ever going to be right for both.PreparedRun::context_windowandestimated_context_tokensare how a host reads the same two figures for itself, ahead of whichever trigger fires first.The two are one setting resolved together, not two triggers that fire independently — but which of them is armed is something a host states rather than something it encodes by leaving a number in. mentra 0.24 splits the switch out of the number (
CompactionConfig::auto_compact_trigger, anAutoCompactTrigger), and basis’s two knobs are read onto it, three states for three spellings:auto_threshold_tokensauto_threshold_percentwhat mentra is told what fires set either Thresholdsthe percentage of a known window, else the absolute number cleared set WindowShareOnlythe percentage of a known window, and nothing when the window is unknown cleared cleared Offnothing, at any window The middle row is the one that had no spelling before 0.24, and it is what
ARCHITECTURE.md’s compaction row has always promised: a trigger that is a share of the model’s window when the provider reports one. Until then a clearedauto_threshold_tokenswas mentra’s off switch for the whole feature, read before the percentage was consulted at all, so a host that wanted the window share had to leave behind an absolute number it did not believe in — and that invented number went live on exactly the models whose window nobody reports, which is where a wrong guess does the damage. basis documented that faithfully while it was true; the tests named for it in this module’stestsare replaced by the three rows above. -
Or the provider refuses a request as too long (
ProviderError::ContextLengthExceeded), regardless of either threshold — including with automatic summarizing off. mentra compacts once and retries the same request; a second overflow after that is not retried again. So anOffposture means basis never compacts ahead of running out of room, not that a conversation which does is guaranteed to fail — the provider’s own refusal gets the one attempt basis’s own trigger would have spent earlier.
§What a failed automatic pass looks like
Like nothing. Both events above are built from a success: mentra
synthesizes CompactionStarted and CompactionCompleted together from the
one ContextCompacted a finished pass emits, so there is no “started” line
to leave dangling — and no line of any kind when the pass does not finish.
An automatic pass that fails is retried up to three times and then dropped
(Agent::auto_compact_if_needed, mentra 0.24.0 src/agent/compact.rs),
which is a reasonable posture — the run continues on an unshortened
history rather than dying over a summary — but it is taken silently: no
event, no hook, nothing a sink can see. The retries in between surface as
Event::Retry, indistinguishable from a model
request’s own.
With one exception, and it is the one that matters for control: since
mentra 0.24 an automatic pass inherits the bounds of the turn it happens
inside, and a cancellation or a deadline reached during it ends the run
instead of being degraded past. Silently continuing after a caller asked
the run to stop would have made the stop button a suggestion. basis names
that ending exactly as it names the same bound reached anywhere else in the
turn — Bound::Deadline for the deadline, and
for a cancel the run’s own
RunFailure::Cancelled with no Bound,
because a stop somebody asked for is not an allowance the run outgrew. No
basis-side mapping was needed for that; basis’s tests/compact.rs pins
both so a change to either is noticed.
basis does not paper over that. Inferring a failure from an estimate that
crossed a threshold with no compaction after it would be a guess dressed as
an event, and the fix belongs where the failure is (ADR-0005). What a host
can rely on is the pass it asks for itself:
PreparedRun::compact reports its failure on
the stream as well as to the caller.
Neither kind of pass is counted in RunUsage — see
there for why.
§What is not here
mentra’s CompactionConfig has twelve fields. This exposes four: the three
triggers above and how much recent user text a summarizing pass must leave
alone — and derives a fifth, auto_compact_trigger, from two of them
rather than offering it, because the numbers already say which posture a
host means and a switch beside them could disagree with them (the private
Compaction::trigger is where the three rows above are read off — it stays
private for that reason, so there is nothing to link to). The mutually
exclusive projected-byte budget is pinned off: exposing it needs a
Basis-owned policy shape rather than two knobs that can disagree.
The summary’s input and output caps, local versus remote summarization, and
how many snapshots are kept remain defaults nobody has had a reason to move.
A knob basis offers is a knob basis has to keep meaning; they arrive when a
host asks, with the case that asked.
transcript_dir is deliberately not a knob at all: where a workspace’s
files go is settled by
RuntimeBuilder::with_store_dir,
and a second answer here could disagree with it — so the conversion to
mentra’s config takes the directory as a parameter rather than reading a
field.
Structs§
- Compaction
- How much of a conversation reaches the model, for every run a workspace mints.