Repository navigation
Description
Context
Roadmap B5 (anchor #168): context management has one strategy. compressHistory
(agent/compression.go) protects the first two user groups, keeps the last two, and summarizes
everything between them, automatically at compression_threshold or by hand with /compress. There is
no way to say "keep this", no way to say "drop that", and no run-level compaction at all — the loop's own
messages are never compressed, only a.History at the start of a run.
Two consequences are worth naming:
- No control. A user who pasted a large document, or who wants an early instruction kept, has the
same two options for everything: let it be summarized, or set the threshold so high that nothing is. - The D7 gap. A continuation round is one
Run, and compaction runs only at its start, so a long
round grows until the provider refuses — the failure mode D7's rounds make routine.
Where the increments stand
| Increment | State | Where |
|---|---|---|
| S1 turn-level control | Landed 2026-10-28 | #307, 396e77f |
| S2 run-level compaction + pin persistence | Landed 2026-10-28 | #311, 65d35d4; path-targeted unload moved to S4 |
| S3 compression preview | Landed 2026-10-31 | #312, 8d1ccb8 |
| S4 path-targeted unload | Landed 2026-10-31 | #314, 7887f09 |
A fresh session picking this up needs only this section: S1's sections are kept for provenance, and the
two open increments each name the failure they remove, the code they start from, and what "done" means.
S1 — turn-level control (landed)
types.Message.Pinned(json:"pinned,omitempty");Agent.PinLast(n)/UnpinLast(n)/
DropLast(n)work in turns — a user message plus everything up to the next one, the grouping
compressHistoryalready uses.compressHistorykeeps a pinned group verbatim, never feeds it to the summarizer, and returns the
history unchanged (no provider call) when the whole middle is pinned.- TUI:
/pin [n],/unpin [n],/drop [n], refused while a run is active;/dropshapes the live
context only and says the session file still holds the record.
Acceptance as delivered: PinLast(2) marks exactly the last two turns, UnpinLast(1) clears the newest,
DropLast(2) removes exactly two; a pinned middle group is verbatim and absent from the summarizer's
input; a fully pinned middle costs no call; the commands are refused during a run; /drop leaves the
session store untouched.
S2 — compaction inside a run, and pins that survive a resume (landed)
Why. A continuation round is one Run. compressHistory runs only at its start
(agent/agent.go:447, and again at :941 for the manual path) and only on a.History. The loop's own
messages (agent/agent.go:441) is a local slice that grows with every step and every tool result, and
it is never compressed — so a long round grows until the provider refuses.
What to build.
- Compact
messagesat a step boundary onceEstimateMessagesTokens(messages)crosses
CompressionThreshold: after a step's tool results are appended, before the next provider call. The cut
must snap to a group boundary (groupEnd,agent/compression.go), or an assistanttool_calls
message is separated from its tool results and the next call is rejected by the provider. - Account the summarizer call. It is an extra provider call inside a run, so it must go through
recordCalllike the loop's own calls, otherwiseUsageTotal, the token budgets and the status bar
under-report. Today the compression summarizer (agent/compression.go:195) is invisible to all three —
a pre-existing gap this increment should not copy. - Pin persistence.
persistSession(tui/update.go:977) rebuilds the stored messages from
m.messages, andchatMessage(tui/messages.go:75) has noPinned, so a pin dies at the first save.
Carry it through:chatMessage.Pinned→persistSession→historyFromSession(tui/model.go:284). - File-level unload → S4. A file's contents are a tool result inside
messages, so this is where
"unload a file" would mean something — but/drop <path>cannot reach those messages: they live inside
Run, and the TUI refuses commands while a run is active. The acceptance below no longer assumes a
command channel that does not exist; the work is S4.
Acceptance (S2).
- A run whose
messagescross the threshold compacts at a step boundary, the run completes, and no
provider request carries atool_callsmessage without its tool results. - The summary call's tokens and cost appear in
UsageTotal/CostTotaland in the status bar. - A pin set before a session is saved is still there after
--resume. go test ./... -count=1,-race,go vet,gofmt,staticcheckclean.
As delivered: the cut snaps forward to an assistant message (a tool_calls message never loses its
results), the head and the last eight messages are kept, the summarizer's call is counted through
recordCall, and a pin travels chatMessage.Pinned → persistSession → historyFromSession. The TUI
mirrors the agent's pins into the transcript by role and content, newest first, because the two lists are
not index-aligned.
S3 — a compression preview (landed)
What was built. The partition became a value: planCompression computes head, middle, pinned groups,
tail, the token estimate and the threshold; compressHistory executes that plan instead of re-deriving
it. /compress preview reads the same plan and prints each kept region (role plus first line, capped),
the middle's token count, and whether automatic compression would fire. No provider call; refused while a
run is active, because reading a slice the run appends to is a data race even when nothing is written.
Acceptance (S3).
- The preview names each kept group (role plus the first line) and reports the middle's token count.
- No provider call is made — assertable with a recording provider.
- Pinned groups are listed as kept, and the preview and the real compression agree on the partition: the
same input yields the same head/middle/tail/pinned split.
S4 — a path-targeted unload (landed)
Why it moved here. S2's acceptance assumed /drop <path> could remove the tool results that read a
file. It cannot: those results are in the loop's messages, which exist only inside Run, and the TUI
refuses commands while a run is active — deliberately, because they would mutate a slice the run is
appending to. A command is the wrong channel.
Two shapes worth trying, in order.
- A pre-run rule. An
agent-level list of paths whose reads must survive compaction (set before the
run, e.g. from config or a command while idle).compactRunMessageswould keep any assistant
tool_calls+ tool-result group whose arguments name one of those paths, instead of folding it into
the summary. No new channel, and it composes with the compaction that now exists. - A tool the model calls. The model pins a read as it happens. Cheaper to reach mid-run, but it puts
the decision in the model's hands, which is the opposite of the control B5 is about.
Acceptance (S4).
- With a path pinned before the run, a compaction that crosses that read keeps the result verbatim in the
messages, and the summary does not absorb it. - A read of a path that is not pinned is summarized away as it is today.
- The rule survives
--resume(it is part of the session or the config, and the acceptance says which). go test ./... -count=1,-race,go vet,gofmt,staticcheckclean.
Out of scope
Dropping by message index; rewriting the session file. Both are deliberate: /drop shapes the live
context, and the record on disk stays what --resume reads.
Activity
feat(agent): pin, unpin and drop turns, and keep pins out of compression
yusiwen commentedon Oct 28, 2026
S1 landed as #307, merged as
396e77f.types.Message.Pinned;Agent.PinLast/UnpinLast/DropLastover turns;compressHistorykeeps a pinned group verbatim, never shows it to the summarizer, and returns thehistory unchanged (no provider call) when the whole middle is pinned;
/pin [n],/unpin [n],/drop [n], refused while a run is active;/dropshapes the live contextand says the session file still holds the record.
The mutation check found a hole in my own test before it found anything in the code: the first version
pinned turn 3 of 6, which is inside the head — kept regardless — so it passed with pin handling
disabled. The test now uses eight turns and pins a middle turn, and disabling
groupPinnedfails it with"the pinned turn was fed to the summarizer".
Still open here: S2 (compaction of the run's own messages, where tool results and file contents
live — the part D7 depends on — plus pin persistence through the UI transcript, since
persistSessionrebuilds the stored messages from
m.messages) and S3 (a preview that calls no model). B5 is markedIn progress in the roadmap until then.
feat(agent): compact the run's own messages, and count what it costs
yusiwen commentedon Oct 28, 2026
S2 landed as #311, merged as
65d35d4.compactRunMessagesfires at a step boundary once the runcrosses the threshold; head and last eight messages stay verbatim, the rest becomes one
[COMPACTED RUN — REFERENCE ONLY]message. The cut snaps to an assistant message, so atool_callsmessage never loses its results — asserted by walking the result and checking every tool message
follows its call.
summarizeTextis the one place compression calls a providerand it goes through
recordCall; the pre-existing gap (summary tokens invisible toUsageTotal, thebudgets and the status bar) is closed for
/compresstoo.chatMessage.Pinned→persistSession→historyFromSession, with the TUImirroring the agent's pins into the transcript by role and content, newest first.
The integration test drives a real
Runfor ten tool steps and asserts a later provider request carriesthe compaction marker; disabling the loop hook fails it with "no provider request carried a compacted run
across 12 calls".
S4 was created by this increment. S2's acceptance asked for
/drop <path>to remove the tool resultsthat read a file. It cannot: those results live in the loop's
messagesinsideRun, and the TUI refusescommands while a run is active — deliberately, since they would mutate a slice the run is appending to.
The body is corrected and S4 records the two shapes worth trying (a pre-run rule the compactor honours, or
a tool the model calls) with its own acceptance.
Open here: S3 (a preview that calls no model) and S4.
feat(agent): preview what a compression would keep, without calling a…
yusiwen commentedon Oct 31, 2026
S3 landed as #312, merged as
8d1ccb8.compressionPlan+planCompression: the partition (head, middle, pinned, tail) with its token estimateand threshold, computed without a model call.
compressHistorynow executes the plan rather thanre-deriving the boundaries, which is what lets the preview agree with the compression message for
message — asserted by
TestCompressionPreviewMatchesTheCompression./compress previewprints: whether automatic compression would fire, each kept region with its firstlines and size, and the middle by token count. No provider call, and refused during a run (reading a
slice being appended to is still a race).
TestCompressionPreviewCallsNoModel; swapping pinned and tail in the compression failsTestCompressionPreviewMatchesTheCompression.Only S4 (path-targeted unload) is left in B5.
feat(agent): keep a file's reads out of compaction (roadmap B5, S4)
yusiwen commentedon Oct 31, 2026
S4 landed as #314, merged as
7887f09(with08a078ffor the palette scenario the new entries grew out of an 80x24 screen).
What S4 built, given that the command channel it originally assumed does not exist:
Agent.KeptPaths+/keep <path>,/keep(list),/unkeep <path>— the rule is set before a run,which is the only place it can be set safely;
compactRunMessageskeeps any round whosetool_callsarguments name a kept path verbatim (after themarker, before the tail) and summarizes the rest; a run whose every round is kept costs no summary call;
session.Session.KeptPathscarries the list throughpersistSession/adoptSession, so--resumekeeps the rule.
Both load-bearing paths were mutation-checked: disabling the kept-round check fails "the kept read was
summarized away", and dropping the persist line fails "the session kept []".
B5 is complete:
docs/roadmap.mdmarks it Delivered — #306 (17 delivered, 1 in progress, 21 notstarted, 40 total). The one thing this increment deliberately does not build is a mid-run unload
command — a run owns its messages and the TUI refuses commands while it runs, so a path rule is the
channel that exists.