- Published on
Why I Built a Compaction System Instead of Handing Off to New Agents
- Authors

- Name
- Duncan Leung
- @leungd
In my previous post, I described how /compact is lossy — it drops external state mutations, abandoned approaches, and behavioral constraints — and how a precompact/postcompact checkpoint system makes compaction safe. That system worked. I used it for months.
But it left a harder question unanswered: should you compact at all, or should you hand off to a fresh agent?
I tried both, and handoff consistently underperformed.
The Case for Handoff
The argument for handoff is clean. When an orchestrator session fills its context window, you stop it, write a brief, and start a fresh session. The new agent reads the brief and picks up where the old one left off. No degraded reasoning from a bloated context. No lossy compression. A clean slate.
In a multi-agent topology like OpenRig — where an orchestrator routes work to implementation and review seats — the handoff model looks like this:
- The orchestrator detects it is approaching context limits.
- It writes a handoff brief: current state, pending work, decisions made, external mutations.
- A new orchestrator session starts, reads the brief, and resumes.
This is the approach I recommended in the previous post for cases where reasoning had degraded. "A fresh session via handoff is the right call," I wrote. "No amount of checkpointing saves a session that has lost the ability to reason about its own output."
I was right about the reasoning part. I was wrong about how often reasoning degradation is the actual problem.
Why Handoff Failed in Practice
Over several weeks of running OpenRig's factory loop — a pipeline that takes tickets from a backlog, dispatches implementation to a dev seat, routes PRs to a review seat, and manages merge gates — I accumulated enough handoff experience to see the pattern clearly.
1. Startup Overhead Is Not Free
A fresh orchestrator session does not start working immediately. It must:
- Run
rig whoami --jsonto establish identity - Read the handoff brief
- Read the factory configuration (
.factory.yml) - Read the project CLAUDE.md and global instructions
- Load the factory-loop skill (~2000 lines)
- Run restart reconciliation against the queue, GitHub, and git worktrees
- Verify peer seat states
This takes 60–90 seconds and consumes 30–50K tokens before the new session does any actual work. For context: the orchestrator's entire budget is ~140K tokens. A handoff burns 25–35% of the new session's capacity on orientation alone.
Compaction, by contrast, preserves the skill loading, the CLAUDE.md instructions, and the system prompt. The post-compact session starts with those already in context. The restore overhead is the restore map (a few KB) plus the ranked reading list — typically 8–12K tokens of targeted reads versus 30–50K of cold-start discovery.
2. Continuity Is the Product
An orchestrator's value is not the code it writes — that happens in the dev seat. The orchestrator's value is the decisions it has made, the state it tracks, and the relationships it maintains with peer seats.
A handoff brief captures the facts. It does not capture the judgment. When a human says "improve the existing pipeline rather than expanding it," that directive colors every subsequent prioritization decision. A compacted session retains that directive in its summary. A handoff brief records it as a bullet point that the new session may or may not weight correctly.
After three handoffs in one day, I observed the new orchestrator re-proposing work the human had already rejected, dispatching tickets in the wrong priority order, and losing track of which advisor corrections had been applied. The facts were in the brief. The judgment was not.
3. Developer Experience Matters
This one is pragmatic. The human operator works in the orchestrator's terminal. They see the conversation history, the decision points, the approvals they gave. A handoff resets that history. The operator loses their place.
With compaction, the conversation continues. The operator sees the compaction marker, then the restore, then the next piece of work — all in the same terminal, the same session, the same thread. They can scroll up and see the approval they gave two hours ago. With handoff, that approval is in a different session's transcript, and the operator must trust that the new agent internalized it correctly.
For a system where the human is the decision-maker and the agents are the executors, maintaining the human's continuity with the conversation is not a convenience — it is a design requirement.
The Compaction System I Built
Given that compaction is the right choice, the question becomes: how do you make compaction safe, automatic, and useful? The system has four components: a daemon enforcer that decides when to compact and drives the lifecycle, a restore map that captures ground truth before compaction, a four-stage restore protocol that the daemon executes after compaction, and a wiki distill that turns every compaction into a knowledge artifact.
Here is the full lifecycle, from rig start to restored seat:
RIG SPEC (YAML)
|
+----v----+
|Material-| Runs once at rig start.
| izer | Reads compaction_strategy per seat.
+----+----+ Registers watchdog jobs.
|
+----------v----------+
| Context Monitor | Polls transcript file sizes
| (every ~30s) | every ~30 seconds.
+----------+----------+
|
+--------------+--------------+
v v
160K threshold 300K threshold
(watchdog fires) (enforcer fires)
| |
v v
+---------------+ +----------------+
| PREPARATION | | COMPACTION |
| | | |
| Daemon sends | | Daemon sends |
| prep prompt | | /compact |
| | | | (idle-gated) |
| v | +-------+---------+
| Seat writes | |
| restore map | v
| to disk | +----------------------+
+---------------+ | FOUR-STAGE RESTORE |
| (each idle-gated) |
| |
| 1. Turn boundary |
| 2. Restore prompt |
| 3. Compliance audit |
| 4. Cleanup + cooldown|
+----------+-----------+
|
+----------v-----------+
| WIKI DISTILL |
| (SessionStart hook) |
| |
| Restore map -> Haiku|
| -> wiki/sessions/ |
| (~7 seconds) |
+----------------------+
The daemon owns every step. The seat participates (writing the restore map, reading it back, running the constraint audit) but never decides when. The operator never types a command. The sections below describe each component in detail.
The Daemon Enforcer
The daemon is the sole compaction authority. Seats never monitor their own token count, never ask the operator to type /compact, and never invoke the precompact skill on their own initiative.
This was not the original design. Early versions had three competing systems: the daemon's compaction enforcer (percentage-based threshold), the daemon's materializer (per-seat watchdog jobs at absolute token thresholds), and the seat itself (global CLAUDE.md instructions told every seat to monitor its own context usage and request manual compaction at 120K-160K tokens).
The seat self-compaction was the problem. On a 1M context window, 120K tokens is 12% utilization. The seat would announce "Type /compact now" while 88% of the window was still usable. The daemon's enforcer never fired because the seat interrupted the workflow first.
I removed all self-compaction logic from the seats. The rig spec's compaction_strategy field opts each seat into daemon-managed compaction:
seats:
orch-lead:
compaction_strategy: managed-compaction
model: claude-opus-4-6[1m]
When a seat has managed-compaction, the daemon monitors its transcript file size, converts bytes to an estimated token count, and triggers compaction when usage crosses the configured threshold. The seat participates in the lifecycle (writing restore maps when asked) but never initiates it.
The daemon's enforcer uses model-specific absolute token thresholds keyed by context window size, consistent with the absolute-not-percentage principle from my dumb zone post. The Chroma "Context Rot" study tested 1M-token models specifically and found that observable degradation starts at 300K-400K tokens, not the 200K I originally assumed for smaller windows. The general thresholds are conservative for 1M models because those models are trained on longer sequences and their attention decay slopes are gentler.
The enforcer config maps each context window to an absolute token threshold:
- 1M models (Opus 4.6, Sonnet 4.6/5.5, Haiku 5.5): compact at 300K tokens (DUMB +100K, the Chroma onset boundary)
- 200K models: compact at 150K tokens (WARN +50K, the highest usable threshold before the window limit)
A fallback percentage covers unknown future window sizes.
The materializer registers preparation watchdog jobs at 160K tokens, well before the enforcer's 300K compact trigger for 1M models. When a preparation watchdog fires, the daemon sends a prompt asking the seat to write its restore map. The seat writes the map and returns to idle. The daemon then waits for usage to cross the enforcer threshold before sending /compact.
| Component | Threshold | Research basis |
|---|---|---|
| Enforcer (1M models) | 300K tokens | Chroma: degradation onset at 300K-400K |
| Enforcer (200K models) | 150K tokens | Highest usable before window limit |
| Preparation nudge | 160K tokens | WARN zone, before DUMB onset |
Two components, two thresholds
The daemon's compaction system is split into two components with separate responsibilities.
The materializer runs once at rig instantiation. It reads each seat's compaction_strategy from the rig spec and registers watchdog jobs in the daemon's watchdog repository. These jobs tell the context monitor what to watch and at what thresholds. The materializer is the bridge between declarative configuration (the YAML spec) and runtime monitoring (live watchdog jobs). It also handles non-compaction strategies: for apprentice-handover, it registers jobs that trigger a seat handover instead of compaction.
The enforcer runs on every poll tick (~30 seconds). It checks whether a seat's context usage crossed a threshold, manages preparation state, sends /compact, and drives the four-stage post-compact restore lifecycle. It is the runtime decision-maker.
These two components fire at different thresholds for different purposes:
Context usage (1M model)
|
| 0K 160K 300K 1M
| +----------+------ ----------+----- ---------------+
| | | | |
| | NORMAL | PREPARATION | COMPACTION FIRES |
| | WORK | RUNWAY | (/compact sent) |
| | | | |
| | v v |
| | Materializer's Enforcer's |
| | watchdog fires threshold fires |
| | (seat writes (daemon sends |
| | restore map) /compact) |
| | | | |
| | +--- 140K gap ---+ |
| | | (preparation | |
| | | runway) | |
| +----------+-----------------+---------------------+
At 160K tokens, the materializer's watchdog fires. The enforcer enters preparation mode and sends a prompt asking the seat to write its restore map. The seat writes the map while continuing normal work, with 840K tokens of context budget still available.
At 300K tokens, the enforcer's threshold fires. If the restore map exists on disk, the enforcer sends /compact. If the map is not ready, the enforcer waits.
The 140K-token gap between the two thresholds is the preparation runway. It gives the seat time to write the restore map (30-60 seconds) without racing against the compact trigger. Without the materializer's early nudge, the enforcer would fire at 300K with no restore map on disk, and the seat would need to prepare and compact in the same stretch, risking degradation before the map is finished.
Layer 1: The Restore Map
Before compaction, the seat writes a restore map: a structured document that captures everything the post-compact session needs to resume. The daemon's preparation nudge triggers this write. At 160K tokens, the daemon sends a prompt asking the seat to load the restore skill and produce the map. The seat does not decide when to prepare; the daemon tells it when.
Unlike the earlier checkpoint format (a flat list of fields), the restore map is organized around what the session will do next:
## Summary (10 lines)
I am orch-lead@openrig, factory-loop orchestrator.
This session merged 21 PRs (#39–#58)...
## Active Work
### RIG-9 — Downvote filtering
- State: planning — dispatched to dev-impl
- Queue item: qitem-20261018181522-6a53706a4844edf0
- What to do next:
1. Wait for TICKET_LOOP_RESULT from dev-impl
2. If pr-created: transition to reviewing
3. If escalate: present to human
## Session Totals
- PRs merged: 21 (#39–#58)
- Tickets created: RIG-98/99/100/101
## Ranked Reading List
| # | Path | ~tokens | Why |
|---|----------------|---------|------------------------|
| 1 | world pack | ~2000 | Re-enter the world |
| 2 | restore map | ~4500 | Primary restore source |
| 3 | .factory.yml | ~400 | Config |
| — | Tier 1 line | | ~8900 tokens |
| 8 | factory-loop | ~12000 | Pipeline reference |
| — | Tier 2 line | | ~28200 tokens |
The ranked reading list is the key innovation over the flat checkpoint. Instead of "here are all the things you should know," it says "here are the things you should read, in priority order, with a running token count." The orchestrator reads to its tier line (Tier 2 for a high-context seat, ~28K tokens) and stops. This turns restoration from an unbounded context-filling exercise into a budgeted one.
Layer 2: The Four-Stage Restore Protocol
After the daemon sends /compact and the context resets, it drives a four-stage restore protocol. Each stage is idle-gated: the daemon waits for the seat's pane to show an idle prompt before sending the next message. If the pane is busy, the stage does not advance, and the daemon retries on its next poll tick (~30 seconds). A busy seat never receives a restore prompt mid-thought.
Stage 1: Turn boundary. The daemon sends a turn-boundary handshake to verify the session is alive and responsive after compaction. This is a lightweight probe, not a restore instruction.
Stage 2: Restore prompt. The daemon sends the restore prompt, which tells the seat to:
- Read the restore map (the primary source of truth, not the compacted summary).
- Follow the ranked reading list up to the tier line.
- Run a read-depth audit: for every required item, mark it FULL, PARTIAL, or NOT_READ.
- Write a RESTORED note with what was recovered and what the next step is.
The restore prompt can include per-seat extra files for project-specific recovery instructions. It can also include an overflow compact instruction for cases where restoration itself pushes the session close to the threshold again.
Stage 3: Compliance prompt. The daemon sends the constraint audit prompt, which verifies that behavioral constraints survived compaction. The post-compact session reads the restore map's constraint section, compares it against its active instructions, and reports any constraint that was dropped. This is the daemon-driven version of the constraint audit described in the Hardening section below.
Stage 4: Cleanup. The daemon records a width receipt (post-restore usage metrics), sets a cooldown timer to prevent immediate re-triggering, and resets its internal state for the seat. The seat is now fully restored and working.
The trust model is explicit: the restore map is ground truth. The compacted summary is a fallback. When they disagree, the map wins. This matches the insight from the earlier checkpoint system, but formalized into a daemon-driven protocol that every seat follows without human intervention.
The old claude -p pattern, which used a separate API-billed Claude process to type /compact into the main session, is now obsolete. The daemon does the same thing: it sends /compact to the seat externally when the seat is idle, then drives the restore, then the audit. The seat never blocks and the operator never types a command.
Where the fresh tokens go
After compaction, the fresh context window is the seat's most valuable resource. Here is how it breaks down:
After /compact (1M model, compacted at 300K):
+----------------------------------------------------+
| Fresh context window (~300K usable before next compact) |
| |
| +---------+ Compact summary ~5-10K |
| +---------+ System prompt + CLAUDE.md ~8-12K |
| +---------+ <- Already consumed (not optional) |
| | | |
| | | Restore map read ~4-5K |
| | | Ranked reading list files ~4-23K |
| | | Read-depth audit (fork) ~0K * |
| | | Constraint audit ~1-2K |
| +---------+ <- Restoration overhead: 9-30K |
| | | |
| | | |
| | | ACTUAL WORK |
| | | (~260-280K available) |
| | | |
| +---------+ |
+----------------------------------------------------+
Restoration uses 9-30K tokens of the fresh window, depending on which tier line the seat reads to. That is 3-10% of the 300K budget before the next compaction fires. The read-depth audit runs in a background fork and consumes zero tokens from the main session. The remaining 260-280K tokens are available for actual work.
This overhead is not waste that a separate process could handle instead. The seat reads those files because it needs them in its own context to resume work. A separate process could read the files, but the main seat would still not know what is in them. The reading IS the restoration. At 3-10% of the fresh window, the cost is well within budget.
Layer 3: The Wiki Distill
This is the newest piece, and the one that made compaction not just safe but productive. After a successful /compact, a SessionStart hook fires and:
- Finds the newest restore map for the seat (must be less than 15 minutes old).
- Redacts it — strips API keys, tokens, secrets, and long base64 strings.
- Sends the redacted map to Claude Haiku via a direct API call.
- Redacts the response a second time.
- Writes a wiki page to
wiki/sessions/with structured headings: Accomplished, Decisions, Open Items.
The hook runs as a detached child process. It returns immediately — the session startup is never delayed. The wiki page appears a few seconds later.
2026-10-18T18:21:07Z outcome=dispatched
2026-10-18T18:21:14Z outcome=written page=2026-10-18-orch-lead@openrig-5243fdcd.md
Seven seconds, end to end. The session was already restored and working by the time the wiki page landed.
Why a direct API call instead of using the Claude Code subscription? The hook runs outside Claude Code's conversation loop. It is a Node.js script triggered by a hook event. It has no access to the subscription's message stream. A direct fetch() to api.anthropic.com with an ANTHROPIC_API_KEY is the only way to call a model from a hook. The cost is negligible — one Haiku call on a 64KB input with a 2000-token output cap runs about $0.01–0.02 per compaction.
Why Haiku? The distill does not need Opus-level reasoning. It summarizes a structured document into three sections. Haiku does this well, fast, and cheap. The confidence: low frontmatter on every page tells readers (including other agents) to verify before acting.
Why redact twice? The restore map may contain secrets the orchestrator handled during its session. Redacting before the API call prevents secrets from reaching the model. Redacting after the API call catches cases where the model hallucinates a secret-shaped string that happens to match a real one. Belt and suspenders.
The Result: 21 PRs in One Session
The compaction system has run through three compaction cycles in one orchestrator session. That single session — the same conversation, the same terminal, the same human continuity — processed 21 PRs across 20+ tickets, including:
- Implementation dispatches to a dev seat
- Code review dispatches to a quality reviewer seat
- ESCALATE gates with human plan approval
- A comprehensive backlog audit with six parallel research forks
- Advisor validation of the audit results
- Ticket creation, cancellation, and priority adjustments
Each compaction cycle took about 30 seconds for the restore and 7 seconds for the wiki distill. The session resumed at the exact point it left off, with verified state and a wiki page that other agents (and the human) can reference.
A handoff-based system would have required three fresh orchestrator sessions, each burning 30–50K tokens on cold start, each losing the accumulated judgment of the prior session. The human would have switched terminals three times. The conversation history would be spread across three transcripts.
Compaction kept it as one conversation.
What the Research Says
The academic literature does not declare a winner. Two recent papers study the failure modes of each approach in isolation — no published work compares them head-to-head as alternative strategies.
Compaction silently drops behavioral constraints
Wang et al.1 measured constraint retention across six compaction methods and three scenarios. The results are bad: only 17% of injected behavioral constraints survive compaction on average, and compliance with session constraints drops from 99.7% to 31.8% after compaction. In a controlled inbox experiment, 30 of 100 sessions saw unauthorized email deletions after compaction versus zero before.
This is the failure mode my restore map was built to address. The map writes constraints to disk before compaction and restores them afterward — functionally equivalent to the paper's recommended mitigation, a separate constraint extractor that maintains a registry apart from the task summary. Their extractor (a 9B model) achieves over 90% retention and raises compliance from ~54% to ~95%. My restore map serves the same purpose: the constraints survive because they are on disk, not in the compressed summary.
The 17% average includes non-LLM compactors (truncation, LLMLingua-2) that retain zero constraints. LLM-based compactors like GPT-5.4-mini retain 88–98% in two of three scenarios. The headline number is real but overstates the problem for well-designed compaction systems.
Handoff carries a "handoff tax"
Ganz et al.2 studied what happens when one model inherits another's trajectory (the handoff scenario). On SWE-bench Verified across 58,000 agent runs, full-trajectory handoff recovers less than half the quality gap between cheap and strong models: 47% for Claude, 36% for GPT.
The finding that matters most for my use case: compaction outperforms raw handoff for escalation (weak-to-strong). Claude with compacted trajectory hits 71.8% at 1.61. Compaction is both cheaper and more effective when a stronger model picks up from a weaker one.
But the paper also found a directional asymmetry I did not account for: for strong-to-weak handoff, removing the predecessor's trajectory hurts quality (-4.7pp for Claude, -5.6pp for GPT, both statistically significant). A blanket recommendation of either compaction or trajectory removal is wrong — the right strategy depends on whether the receiving model is stronger or weaker than the one that produced the context.
In my topology, the orchestrator (Opus) compacts its own context and restores within the same model family. This is neither escalation nor downshift — it is same-model continuation, which neither paper directly studies. My empirical result (21 PRs across 3 compaction cycles without regression) is evidence for the specific case, not a general claim.
The gap in the literature
No paper directly compares compaction against handoff as alternative context-management strategies for the same task. The compaction papers analyze information loss. The handoff papers analyze trajectory inheritance. Each body of research measures one approach in isolation. This post's conclusion — that compaction is better for long-running orchestrators — is an empirical observation from one system, not a finding supported by controlled comparison.
When Handoff Is Still Right
Compaction is not universally better. Handoff wins when:
- Reasoning has genuinely degraded. If the orchestrator is producing circular logic or ignoring instructions, no amount of context management helps. Kill it and start fresh.
- The session crosses a phase boundary. If Phase A is planning and Phase B is execution, a handoff between them is natural — the new session needs Phase B context, not Phase A's exploratory dead ends.
- The receiving model is weaker. The Handoff Tax paper shows that strong-to-weak handoff benefits from keeping the full trajectory. If you are downshifting to a cheaper model for cost reasons, compaction (which removes context) hurts. Keep the trajectory intact.
- The agent is not an orchestrator. An implementation seat that finishes a PR does not need to compact — its job is done. The orchestrator tracks the pipeline state; the implementation seat is disposable.
The compaction system is built for long-running orchestrators whose value accumulates over time. For short-lived workers, handoff (or simply stopping) remains the right call.
Hardening: What Broke at Fleet Scale
The system described above is the design. Running it across a 21-rig fleet with 100+ seats exposed three failure modes the design did not anticipate. Each required a fork-specific feature that upstream OpenRig does not yet include.
Constraint audit on restore (RIG-103)
Wang et al. found that only 17% of behavioral constraints survive compaction on average. The restore map addresses this by writing constraints to disk. The constraint audit verifies they made it back.
After each restore, the daemon's compliance prompt (Stage 3 of the restore protocol) runs a structured check: it reads the restore map's constraint section, compares it against the restored session's active instructions, and reports any constraint that did not survive. In practice, the audit catches constraints that the restore map captured but the post-compact session silently deprioritized during its orientation phase. The difference between "written to disk" and "loaded into working memory" is where constraints get lost, and the audit closes that gap.
This is the fork's implementation of what Wang et al. recommended: a separate constraint extractor that maintains a registry apart from the task summary. The audit report was initially verbose (a full table of every constraint). RIG-117 cut it to exceptions only, so a clean restore produces no output and a dropped constraint is immediately visible.
Lifecycle watchdog for stuck compactions (RIG-118)
A compaction attempt has multiple stages: preparation, restore-map write, /compact delivery, the four-stage restore protocol, and the RESTORED acknowledgment. Each stage can fail independently, and a failed stage produces a session that is neither compacted nor restored, only stuck.
At fleet scale, stuck compactions accumulate. A rig restructure orphans compaction attempts for seats that no longer exist. A daemon restart can lose in-flight restore requests. A session that crashes mid-compact leaves a preparation with no completion.
The lifecycle watchdog detects attempts that have been in one stage longer than their deadline and logs them. Without it, stuck attempts are invisible until the daemon log fills with repeated retry messages. In one incident after a fleet-wide update, 14 stuck attempts from dead sessions produced over 6,000 log lines before the watchdog's output made the pattern visible.
Escalation path for failed restores
When a compaction attempt gets stuck, someone needs to know. The daemon's compaction enforcer can detect the failure, but it needs a destination for the alert.
The workspace.operator_seat_name configuration points the enforcer at the kernel's operator seat. When a restore fails or times out, the enforcer creates an advisory for the operator. Without this configuration (stored in the daemon's settings database, not in a config file), stuck compactions retry indefinitely with no alert.
This configuration does not survive a daemon database reset. A version update that rebuilds the database clears it. The post-update verification checklist now includes a check for this value, because the compaction system appears healthy (the context monitor polls, the threshold is configured, the enforcer is active) even when the escalation path is broken.
The Design Principles
Four principles shaped this system, and they generalize beyond compaction:
1. Write ground truth to disk before you lose it. The context window is volatile. Disk is durable. Anything that must survive a context reset — state, decisions, external mutations — must be on disk before the reset happens. This applies to compaction, crashes, and session endings alike.
2. Budget your restoration. A post-compact session that reads everything it can find fills its context with orientation material and has no room for actual work. The ranked reading list with tier lines forces a budget: read the most important things first, stop at the budget, and trust that the map captured what matters.
3. Make compaction produce something. The wiki distill turns a maintenance operation (freeing context) into a knowledge-creation operation (producing a session summary). Every compaction leaves behind a searchable, browsable record of what the session accomplished. Over time, these pages become the project's institutional memory — written by the agents, for the agents and the human.
4. Centralize the compaction authority. When multiple systems can trigger the same operation, they compete instead of coordinating. The seat, the materializer, and the enforcer each had a compaction threshold. The lowest one always won, regardless of whether it was the right one. A single authority (the daemon enforcer) that owns the full lifecycle eliminates the race and lets you set thresholds based on research rather than on which system fires first.
Compaction is not a limitation to work around. With the right system, it is a feature.