Skip to content

feat(agent): pin, unpin and drop turns, and keep pins out of compression - #307

Merged
yusiwen merged 1 commit into
masterfrom
feat/context-pin-drop
Oct 28, 2026
Merged

yusiwen merged 1 commit into
masterfrom
feat/context-pin-drop

Conversation

@yusiwen

@yusiwen yusiwen commented Oct 28, 2026

Copy link
Copy Markdown
Owner

What changed

  • types.Message.Pinned — a pin rides on the message, so the compressor can see it.
  • agent/context.go: PinLast(n) / UnpinLast(n) / DropLast(n) / PinnedTurns() over turns (a
    user message plus everything up to the next — the grouping compressHistory already works in).
    DropLast copies the retained prefix rather than truncating in place, because history slices are
    shared with goroutines reading them.
  • agent/compression.go: a pinned group is never fed to the summarizer and is re-inserted verbatim after
    the summary; a middle that is entirely pinned returns the history unchanged with no provider call.
  • TUI: /pin [n], /unpin [n], /drop [n], palette entries, /help lines, and the same
    "a run is in progress" refusal /compress has.
  • Docs: CODEBASE.md (compression step 5, new context.go section), roadmap B5 → In progress — [agent] Give context management turn-level control: pin, unpin, drop #306
    (S1 landed, S2/S3 open), palette goldens, test count 1237 → 1249.

Why

The compressor had two settings — summarize the middle, or raise the threshold until nothing is
compressed — and no per-turn control. /drop is context shaping, not deletion: the session file is what
--resume reads, so the command says it leaves the record alone. Pins are the other half: a turn a user
marked is kept verbatim, and the summarizer never sees it.

Verification

  • go test ./... -count=1 → 16 packages ok; go vet and gofmt -l clean.
  • Mutation check: pin handling disabled (if false around groupPinned) →
    TestCompressionKeepsPinnedGroupsVerbatim fails with the pinned turn was fed to the summarizer;
    restored, it passes.
  • The mutation check found a real hole in the test first: the original version pinned turn 3 of 6, which
    sits in the head — the region kept regardless — so it passed against the mutation. It now uses
    eight turns and pins a middle turn, so the assertion is about the path it claims to test.
  • New tests: 5 in agent/context_test.go (pin/unpin/drop counts and effects, the pinned group kept
    verbatim and absent from the summarizer, the fully pinned middle costing nothing) and 7 in
    tui/context_command_test.go (each command, counts, empty history, the run guard, the palette).
  • git log --format='%h %G? %s' → G.

Honest scope

  • A pin lasts for the session: persistSession rebuilds the stored messages from m.messages, so the
    flag has to travel through the UI transcript to survive a resume. That is S2 of [agent] Give context management turn-level control: pin, unpin, drop #306, together with
    the run-level compaction D7 depends on.
  • compressHistory still runs only at the start of a run. A long round grows until the provider
    refuses — the gap S2 exists for.
  • No file-level unload: file contents are tool results inside a run, not history turns; S2 is where that
    becomes meaningful.
  • No compression preview (S3).

Follow-ups

Refs #306, #168

B5's first slice. Context management had one strategy: keep the first two user
groups, keep the last two, summarize everything between, with no way to say
"keep this" or "drop that".

- `types.Message.Pinned` marks a message; `Agent.PinLast` / `UnpinLast` /
  `DropLast` work in *turns* — a user message plus everything up to the next,
  the same grouping the compressor uses — and report what they touched.
- `compressHistory` never feeds a pinned group to the summarizer and re-inserts
  it verbatim after the summary. A middle that is entirely pinned costs no
  provider call and leaves the history alone.
- `/pin [n]`, `/unpin [n]`, `/drop [n]` in the TUI, refused while a run is
  active for the same reason `/compress` is: they replace Agent.History while
  the run goroutine may append to it.
- `/drop` shapes the live context only. The session file is the record
  `--resume` reads, so the command says so rather than quietly deleting from it.

A pin is honoured for the life of the session; carrying it through the UI
transcript into the session file is S2 of #306, with the run-level compaction D7
depends on.

Two things the tests taught me, both worth keeping:

- the first version of the compression test pinned turn 3 of 6, which lands in
  the *head* — the region that is kept regardless. It passed with pin handling
  disabled. The test now uses eight turns and pins a real middle turn, and the
  mutation (pinning switched off) fails it with "the pinned turn was fed to the
  summarizer".
- `DropLast` copies the retained prefix instead of truncating in place: history
  slices are shared with any goroutine reading them.

Verified: go test ./... -count=1, 16 packages ok; go vet and gofmt clean; the
pin path mutation-checked as above.

Refs #306, #168
@yusiwen yusiwen added the feature A new capability rather than an improvement to existing behaviour label Oct 28, 2026
@yusiwen
yusiwen merged commit 396e77f into master Oct 28, 2026
9 checks passed
@yusiwen
yusiwen deleted the feat/context-pin-drop branch October 28, 2026 04:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature A new capability rather than an improvement to existing behaviour

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant