Zurück zu allen Artikeln

Notizen aus der Praxis

Compact or hand over: how my agent harness manages context

Pi compacts on overflow. My harness now decides earlier: same-session compaction at safe boundaries, or a handover into a fresh session with an archive it can look back into.

Compact or hand over: how my agent harness manages context

Every agent that works long enough in one session eventually fills its context window. Pi handles that on its own: when the window overflows, it compacts. That behavior is still there in my harness, but only as the fallback. Overflow is the worst moment to decide what an agent should remember, because the decision happens mid-task, under pressure, and nobody chose the boundary. So the harness now owns that decision, and it has two different answers to it.

Two notebooks on a dark desk, one closed, one open on a blank page

Yesterday I wrote about the VanillaCore Agent Harness and why I built it on Pi. Since then, context maintenance has landed on main: compaction at safe boundaries, a handover into a fresh session, a scoped archive the new session can look back into, and completion notices that go through the same durable outbox as normal replies. This post is about that lifecycle. Pi still does the summarizing; the harness decides when it happens, what it is asked to keep, and what survives around it.

Two different operations

Compaction and handover sound similar, and they are not. Compaction keeps the agent in the same model session. The older part of the conversation is replaced by a summary, and the agent continues with more room. The session ID stays the same, and the agent works on with a shorter memory of what came before.

A handover ends the session. The harness asks for a summary, saves it as a prepared transition, opens a fresh runtime session and seeds it with a checkpoint. From the outside, the conversation stays the same: channel, thread, queue, goals and review budgets stay with the same harness session key. The model, however, starts clean, with a short forward checkpoint instead of a long compressed past.

Diagram: compaction versus handover. Left, compaction: session A keeps its ID; older turns are replaced by a summary and recent turns remain, giving a shorter memory and more room. Right, handover: session A ends, an on-disk archive stores the retrospective, the checkpoint and copied text entries, and a new session B starts clean with only the checkpoint and a new session ID; the archive stays readable. In both cases the harness session key, channel, thread, queue, goals and review budgets stay the same. Both paths summarize and both can lose details.

Neither operation is free. The harness documentation says plainly that compaction may lose details, and that is true for both paths. A summary is a summary.

The defaults

Context maintenance is on by default, with these settings:

contextMaintenance:
  enabled: true
  assessPercent: 60
  compactPercent: 75
  checkEveryTurns: 5
  cooldownTurns: 5
  autoHandover: false

Compaction and handover run at the boundary after a working turn and its review, never while a tool is running. Before that, there is only advice: from 60% context usage on, every fifth turn carries a short instruction for the agent: context is at this level, consider whether compaction is useful at this task boundary, and if so, request it with what to preserve and how to resume. That instruction is appended to the turn and costs no extra model call. If the agent decides that continuity matters more, it simply continues.

From 75% on, the harness does not wait for the agent. It compacts at the next safe boundary, using Pi's native compaction under the default native strategy (a plugin can replace the strategy), with instructions to keep operator intent and corrections, decisions and rejected alternatives, verified results versus assumptions, Git, PR and deployment state, open work and next steps, and never credentials. Those are instructions to a model. They improve the odds; they do not guarantee the result.

After a maintenance attempt, a cooldown of five turns applies. The cooldown is persisted in a small state file per harness session, so it survives a restart, and it is reserved before the summary runs, which means a failed attempt also counts. Without that, a broken summary call could retry every turn and burn tokens in a loop. When the runtime cannot report context usage, nothing is compacted automatically, because guessing is worse than waiting for Pi's overflow fallback.

Diagram: default context-maintenance thresholds by context usage. Below 60% nothing happens automatically, but the agent can still request maintenance. From 60% a short advisory hint is appended every fifth turn without an extra model call. From 75% the harness compacts at the next safe boundary without waiting for the agent. From 90% it still compacts, or hands over only if autoHandover is on, which is off by default. The safe boundary comes after the working turn, where tools run, and after review, before the next turn. A 5-turn cooldown is persisted per harness session, across handovers, and reserved before the summary runs, so failed attempts count. When usage is unknown there is no automatic compaction and Pi's own overflow compaction remains the fallback.

The admin command /context-policy switches maintenance and automatic handover on or off through the local settings overlay. Changing the thresholds currently means editing the file and restarting.

Asking for it

The agent can also request maintenance itself. The bundled tool plugin_context_maintenance takes an action, compact or handover, plus two pieces of text: what to preserve and how to resume. The tool does not compact anything. It persists the request, tells the agent to finish the current turn at a coherent checkpoint, and the harness executes it after the working turn and after review. An agent that compacts itself in the middle of its own tool call would cut the branch it is sitting on, so it does not get to do that.

A request made during the cooldown stays pending until the cooldown has passed.

The handover

An explicit handover request works even when automatic handover is disabled. Before anything is compressed, the harness copies the text entries of the current session branch. Pi's native compaction then writes a backward-looking retrospective: decisions with reasons, rejected alternatives, completed actions with evidence, open uncertainties, and a section labeled as current state and next steps. That runs as a tool-free summary operation on the same session, not as an extra working turn and not through a separate summary model. As a side effect, the old session is compacted as well.

The harness stores the retrospective, the forward checkpoint and the copied entries as an archive under session-archives/ in the harness home, with source session ID, target session ID and entry IDs. The forward seed for the new session contains the checkpoint section, the last three operator messages (each cut at 2,000 characters) and the archive ID. If the model does not produce the labeled section, the checkpoint falls back to the tail of the retrospective. The documentation is honest about why: model compliance with section instructions is not guaranteed.

Two details in the seed matter to me. The recent operator messages are explicitly marked as historical context, not as fresh authorization, so an old "go ahead and deploy" does not become a new approval in a new session. And the seed tells the agent not to repeat completed side effects blindly and to verify files and Git state before acting.

The new session is seeded without executing tools. If there is a queued operator request, it reads the seed as context and continues. If there is none, the handover itself adds no turn; the harness does not invent an autonomous continuation just to look busy. Goals, loops or heartbeats that are already configured keep working as before. The old native session history stays on disk.

Looking back

The second tool, plugin_session_history, gives the new session access to the archive. It reads the retrospective with section: summary, or it searches and pages through the original text entries by archive ID, query, offset and character offset. Access is scoped to the current harness conversation key, archive IDs are validated against a fixed format, and no model input becomes a file path. Reads are bounded: summaries return at most 20,000 characters, entry pages at most 30 entries, each entry in slices of 1,500 characters. The full text stays on disk.

Diagram: what a new session can reach after a handover. One, always present: the seed with the checkpoint of current state and next steps, the last three operator messages up to 2,000 characters each marked as history and not fresh authorization, and the archive ID. Two, on request through plugin_session_history, which the harness does not check was read, and scoped to this harness conversation key: the archive with the retrospective and checkpoint, one summary read capped at 20,000 characters, copied text entries searchable in pages of up to 30 and slices of 1,500 characters, and source and target session IDs. Three, outside the tools: the old native session, still on disk; images and other non-text blocks are not copied into the archive and live only there; there is no pruning and no retention or deletion policy.

The seed asks the agent to read the retrospective before continuing. That is guidance. The harness does not check whether the agent actually read it, and the short forward state is in the seed anyway. Images and other non-text blocks are not copied into the archive either; for those, the original native session is the only place to look.

Automatic handover

Handover can also happen without a request. With autoHandover enabled, a maintenance boundary at 90% or more usage hands over instead of compacting, as long as the cooldown permits it. This is off by default, and I would keep it off until you have watched a few handovers on your own agents. /context-policy handover-on switches it on.

Telling the room

Maintenance is invisible by default, and invisible state changes in a long-running agent are a debugging nightmare. So after a successful compaction or handover, and only after the state is saved, the harness queues a short completion notice to the conversation the turn came from. Failed attempts get a notice that maintenance has not fully completed, without leaking exception details. No-op checks and cooldowns stay silent.

When the conversation has a destination, these notices go through the durable outbox, the same machinery as normal replies. The notice and the completion record are persisted together with a stable ID, startup and periodic ticks move pending notices into the outbox, and the notice is only acknowledged after the outbox write has passed its persistence barrier. A crash before that retries the same ID. This is at-least-once delivery: a channel without delivery-ID deduplication can still show a duplicate after a crash. A handover that is resumed after a restart can also seed the same checkpoint twice. That duplicates context, and it does not intentionally repeat tool actions, but it is recovery, not an exactly-once guarantee for model calls or side effects.

What it does not do

The limitations are documented, so I will repeat them here. Every handover writes an archive, but nothing prunes the native session history, and there is no retention or deletion policy for the archives; a failed handover can leave an archive behind, and the harness keeps it rather than deleting possible recovery data. Archives contain the same sensitive task data as the session itself, so the harness home needs protecting. There is no isolated summary model. There is no automatic continuation without queued input, and not every maintenance operation shows up in a UI yet. Custom session providers have to honor the binding contract for rotation to work. And a build or a merge does not activate any of this in an instance that is already running.

Why this belongs in the harness

None of this makes the model remember better. It makes forgetting a decision with a time, a reason and a record. Compaction happens at a boundary I can name, handover leaves an archive I can open, and completed or failed maintenance reports itself through the same outbox that already carries the agent's replies. A persistent agent will lose detail somewhere; I want to know where it happened, and I want the harness to admit that it happened.

Sources used