Who Owns Compaction on an Unattended Lane: Notice, Warn, Force, and a Note-to-Self

Agent context compaction on unattended lanes: who fires it, token thresholds under price lines, what must survive the cut, and whether the summary is readable.

Agent context compaction on an unattended lane: a context gauge with notice, warn and force lines, and a note-to-self carried across the compaction boundary
Three lines on the gauge and one note that crosses the boundary. The contract says who draws the lines and who holds the button.

Every agent harness ships a compaction default, and on an unattended lane that default is the entire policy. Claude Code on a native 1M-window model on the Anthropic API compacts at about 967K tokens. Codex on GPT-6 Astra compacts at 244,800. Pi compacts when the context crosses the window minus a 16,384-token reserve.

Nobody picked those numbers for the lane that ran all night, and agent context compaction as most playbooks teach it assumes a person at the milestone who types /compact.

This runbook replaces the default with a contract. By Tuesday each unattended or swarm lane carries one page that answers four questions: who pulls the trigger, at which token counts, what must survive the cut, and whether the summary prompt can be replaced and the summary read. The answers differ by harness, and the most interesting new pattern, an agent that compacts itself, works well in one harness and strands a lane in another.

Chatbots suggest; agents act. An agent that acts for hours will compact along the way, and every compaction decides what the lane forgets.

IndyDevDan’s self-compacting Pi agent, and what the other harnesses let you copy

On Sep 21, 2026, IndyDevDan published Self-Compact Pi Agent, a half-hour devlog, with the code in disler/self-compact-pi-agent, an MIT-licensed Pi extension created the day before. It hands the model the gauge and the button. The agent gets two tools: self_compact(note_to_self) and a read-only view_context() that reports usage and thresholds as JSON.

Three lines sit on the window: a notice at 10%, a warning at 20% where the agent is told to write its handoff note, and a hard cutoff at the warning plus a 10% buffer, capped at 90% of the window. The README converts those defaults to roughly 105K, 210K and 315K tokens on a 1M-token model.

At the hard cutoff the extension blocks every tool except self_compact and, per the source file, view_context; the README’s phase table names only the first. After compaction the note returns byte for byte as the message that starts the next turn. The summary prompt is replaceable by flag or by a prompt file, and the extension cancels every Pi compaction that was not started manually, so Pi’s own near-the-wall pass is cancelled whenever it fires.

README of disler/self-compact-pi-agent on GitHub: the paragraph on reactive compaction and the How a cycle works diagram running notice, warning, forced, self_compact, note returned Screenshot: GitHub, “GitHub - disler/self-compact-pi-agent” (Sep 20, 2026), captured Sep 21, 2026.

The video description makes the sharpest claim about prompts: “Pi lets you replace it. Claude Code does not.” It also reports a build-off in which a GLM run hit 98% of its context and never finished, while Claude Code on Fable 5.1 and Codex on GPT-6 Astra shipped. That is one run per harness, reported by the author; read it as an anecdote.

The prompt claim holds up against the docs, with one correction. Claude Code lets you steer the summary with /compact <instructions> or a Compact instructions section in CLAUDE.md, and move the threshold with /autocompact, the --autocompact flag, the autoCompactWindow setting or CLAUDE_CODE_AUTO_COMPACT_WINDOW (model configuration). Nothing documents replacing the prompt, and the model can’t invoke /compact. The correction: a PreCompact hook can veto a proactive auto-compaction (hooks reference).

Codex’s configuration reference exposes compact_prompt, but on the OpenAI, Azure and Bedrock providers compaction runs remotely and never reads it (Codex source, main, Sep 21). Pi’s session_before_compact event can cancel a compaction or supply the whole summary (Pi extensions docs). And Claude’s on-demand compaction beta, released Sep 14, returns a signed block that your code swaps in when it chooses.

Agent context compaction needs an owner when nobody is at the milestone

The live context engineering playbook teaches compaction the attended way: a person reaches a milestone, checks the state and compacts. Keep that for sessions you watch. On a swarm lane the milestone arrives at 3 a.m., and whatever the harness defaults to is what happens.

Two failures follow. A lane that compacts late pays for stale context on every turn and works through the decay the README calls context rot. A lane that compacts with a generic summary loses what it was about to do; a summary written by someone who wasn’t doing the work sends the agent back over finished work. The contract has one clause for each thing the default decides without you.

Clause 1: Decide who pulls the trigger, and never port the force step blindly

The first clause is the only one that is not a number. The agent should own the trigger where the lane allows it: Pi, which exposes ctx.compact() to extensions, or your own Claude API loop, where an on-demand compaction happens when your code sends the request and at no other time. Everywhere else the harness owns it, and your job shrinks to the window, the veto and what gets carried across.

Lane Who can fire compaction The lane’s veto Force step (lock tools until compaction)
Pi + self-compact extension The agent, via self_compact; Pi’s own automatic passes are cancelled The extension decides Safe: the agent can always press the button
Your Claude API loop Your code, with the on-demand beta; or the API itself with threshold compaction at your trigger (default 150K, minimum 50K) Your code Safe if you give the model a compaction tool your loop honors
Claude Code The harness, at its window; a person with /compact PreCompact exit 2 skips a proactive pass; DISABLE_AUTO_COMPACT=1 turns automatic passes off Strands the lane: the model can’t call /compact
Codex (OpenAI, Azure or Bedrock provider) The harness, at 90% of the model’s window; a person with a manual compaction continue: false on PreCompact or PostCompact halts the lane Don’t: the docs do not give the model a compaction call

The last column is where the self-compact pattern breaks when you copy it. In Pi, locking every tool except self_compact is safe because the agent can always call it. In Claude Code the skills docs list /compact among the built-in commands the Skill tool can’t reach, so a hook that denies every tool past a hard line leaves a model that can’t compact, can’t work, and waits for a harness trigger you may have just moved. Port the thresholds and the note; leave the lock behind.

Claude Code docs page Extend Claude with skills, showing the passage that says only a few built-in commands such as /init and /security-review are available through the Skill tool and /compact is not Screenshot: Claude Code Docs, “Extend Claude with skills” (undated), captured Sep 21, 2026.

Diagram of who holds the compaction button: on agent-owned lanes the agent writes its note, calls compaction and gets the note back; on harness-owned lanes the lane keeps a handoff file, the harness fires, and a SessionStart compact hook re-injects the file; porting the force step to Claude Code strands the lane Two ways to own the button. The note crosses the boundary in both; the tool lock belongs only where the agent can reach the button.

The Claude Code veto is a guardrail with sharp edges, so write its failure behaviour into the contract. A PreCompact hook receives only the trigger (manual or auto) and any custom instructions, so nothing in its documented input tells a proactive pass from a recovery pass. Blocking a proactive pass leaves the conversation uncompacted; blocking a pass that is recovering from a context-limit error fails the request.

A hook that crashes or exits 1 is a non-blocking error, and compaction proceeds. So veto only to defer, only when the handoff file is stale, at most once per run, and leave automatic compaction switched on behind the hook as the wall. DISABLE_COMPACT=1 turns off both automatic and manual compaction, and it has no place on an unattended lane.

Codex hooks halt rather than defer: continue: false on PreCompact stops the lane before compacting, and on PostCompact stops it after (Codex hooks). Treat that as a quarantine, useful when the runner wants a human or a lint pass between the cut and the next action.

Pi needs the same honesty. The README warns that usage is Pi’s own estimate, so one large tool result can jump past a threshold before the lock engages; if compaction fails, the note and the lock survive and the extension retries. The runner is the wall behind it: a lane stuck in the forced phase for more than a few minutes gets /self-compact-now or a kill, not patience.

Clause 2: Set thresholds in tokens, under the price line your billing path actually has

Percentages travel badly. Ten percent of a 272K window and ten percent of a 1M window describe different lanes, and a model swap changes the window under an unchanged percentage. Write every threshold in tokens and derive it in four steps.

  1. Effective window. Pi fires at contextWindow - reserveTokens, with a 16,384-token reserve by default (Pi compaction docs). Claude Code caps any window you set at the model’s context window, compacts Opus 5 at the 200K boundary where it runs with a 200K window (Amazon Bedrock, Google Cloud, Microsoft Foundry), and loses room before auto-compaction when you raise CLAUDE_CODE_MAX_OUTPUT_TOKENS. Codex fires at 90% of the model’s context_window, which for GPT-6 Astra in the bundled catalog is 272,000.
  2. Reserve. Leave room for the largest tool result the lane routinely pulls, plus the summary request itself, which carries the context it is summarizing.
  3. Quality. Put the hard line where the lane’s work still holds up in your own run logs, not where the window ends.
  4. Price line, where one exists. The OpenAI API bills a request over 272K input tokens at twice the input and cache rates and 1.5 times output, for the whole request (GPT-6 Astra model page); the three meters piece and the GPT-6 Astra host inventory already do that arithmetic. OpenAI’s Enterprise rate card says GPT-6 Astra in Codex carries no long-context multiplier, so confirm which billing path the lane uses before the line sets your threshold. Anthropic bills the full 1M window at standard rates on Claude 4.6 and later (Claude pricing). Gemini 3.1 Pro Preview’s line is 200K, where input moves from $2 to $4 and output from $12 to $18 per million tokens (Gemini API pricing).

Now hold those lines against what the defaults already do. The self-compact percentages on a 1M window put the warning near 210K, under OpenAI’s line, and the hard cutoff near 315K, over it. The video’s description talks about compacting before the 270K mark, but the shipped percentages don’t enforce that.

Codex on Astra already compacts at 244,800. Claude Code on a native-1M model waits for about 967K.

Horizontal bar chart of agent context compaction trigger points in tokens: self-compact notice about 105K, warning about 210K, Codex on GPT-6 Astra 244,800, self-compact hard cutoff about 315K, Claude Code native-1M default about 967K and Pi built-in trigger 983,616 on a 1M window, with dashed price lines at 200K for Gemini 3.1 Pro and 272K for the OpenAI API Where compaction fires by default. The self-compact hard cutoff lands above the OpenAI API’s 272K line; Codex on Astra fires just under it; Claude Code and Pi wait for the wall.

Here is how the clause lands as configuration. The key names and the rules in the comments come from each harness’s docs or source; every value is illustrative.

# self-compact extension flags (names from the repo README); tokens, not percentages
--compact-soft-at 140k    # notice
--compact-at 210k         # warning: the agent writes its note
--compact-buffer 40k      # hard cutoff = 210k + 40k = 250k, under the 272K line
# Claude Code: plain integer only. A "k" suffix is misread and clamps to the 100K minimum.
export CLAUDE_CODE_AUTO_COMPACT_WINDOW=400000
# Codex config.toml: this key can only LOWER the default trigger (90% of context_window)
model_auto_compact_token_limit = 200000
# raising the trigger means raising model_context_window instead (Astra's max is 872,000),
# which on an API-billed lane walks straight past the 272K line

Three more rules come straight from the sources. Keep the notice line well above Pi’s keepRecentTokens (20K by default), or a compaction has nothing to cut. If you run several models through Pi, compaction.modelOverrides (added in 0.86.0 on Sep 19) sets reserveTokens and keepRecentTokens per exact provider and model ID (Pi settings). On Claude Code, CLAUDE_AUTOCOMPACT_PCT_OVERRIDE can only lower the trigger, and CLAUDE_CODE_AUTO_COMPACT_WINDOW outranks the flag, the command and the setting while it is set (environment variables).

Clause 3: Name what must survive, and the hook that carries it across

In Pi with the extension, the note-to-self is the survival mechanism, and the extension’s guidance messages are deliberately transient: they reach the next model call and are dropped, so the only persisted content is user prompts and the note. On the other harnesses you build the note yourself.

Claude Code re-reads a fixed set from disk after compacting, per its context window docs: project-root CLAUDE.md, auto memory, up to five recently modified files, and invoked skill bodies within a cap of 5,000 tokens per skill and 25,000 in total. Context that hooks added earlier is summarized along with everything else, so a mid-session injection does not survive verbatim. The re-injection point is a SessionStart hook with the matcher compact: it runs after compaction and its output joins the compacted context. Codex offers the same seam, a SessionStart hook matching source compact that runs before the next model request, even when automatic compaction lands mid-turn.

So on harness-owned lanes the note-to-self becomes a handoff file the lane updates after every completed step, because you can’t count on the model seeing a warning in time. A SessionStart hook with matcher compact prints it back after each cut. Use CLAUDE.md’s Compact instructions section to tell the summarizer to keep file paths, commit SHAs and test names, knowing that it steers the summary rather than replacing the prompt.

Handoff field What it prevents after the cut
Goal and acceptance test The successor optimizing for a paraphrased goal
Done, with evidence (commit SHAs, paths, test names) Finished work being redone
In flight Losing what the agent was about to do
Next action, one line A cold restart that re-plans from scratch
Dead ends Retrying approaches that already failed
Open questions for a human Guesses that should have been escalations

Keep the file short (a page is plenty) so re-injection doesn’t eat the room compaction just freed. What a lane loads at start also depends on state you may not have declared, such as synced skills or memory files; proving headless runs ignore undeclared state is its own drill.

Clause 4: Record whether the prompt is replaceable and the summary readable

Lane Replace the prompt? Steer it? Summary readable? Where it persists, how you capture it
Pi (+ extension) Yes: extension flag or prompt file; session_before_compact can return the whole summary /compact [instructions] Yes A compaction entry in the session (summary, tokensBefore, details)
Claude Code No /compact <instructions>; CLAUDE.md Compact instructions Yes, to a PostCompact hook as compact_summary, with no decision control Your hook writes it into the run’s artifacts
Codex, OpenAI/Azure/Bedrock provider No: the remote path never reads compact_prompt Not documented OpenAI provider: no, an encrypted_content item; Azure and Bedrock: remote path, form not documented Keep the transcript and retained messages outside the harness
Codex, any other provider Yes: compact_prompt is read on the local path Through the prompt The docs do not say Test it on the lane
Claude API loop Yes: instructions up to 16,384 characters replaces the default prompt Through instructions Yes: readable text plus a signature Your code stores the block and sends it back exactly as returned

Readable summaries are the only ones you can check before the lane acts on them; linting the compaction summary takes over from here. Where the summary is opaque, keep the transcript and the retained messages outside the harness. Two Claude API lines also belong in the contract: on-demand compaction is not available on Amazon Bedrock or Google Cloud, where threshold compaction is the option, and an altered block fails with a 400.

Clause 5: Write the contract down, pin the harness, re-test on every upgrade

Put the four clauses in one file per lane, next to the lane’s other policy, where the runner reads it before launch.

# compaction-contract.yaml (illustrative shape): one per lane, read by your runner, not by the harness
lane: nightly-migration-07
harness: claude-code
harness_version: 2.1.278             # the version this contract was last tested on
billing_path: anthropic-api          # decides which price line applies
price_line_tokens: none              # openai-api: 272000; gemini-3.1-pro: 200000
trigger:
  owner: harness                     # agent | your-code | harness
  window_tokens: 400000              # CLAUDE_CODE_AUTO_COMPACT_WINDOW, plain integer
  veto: precompact-when-handoff-stale   # proactive passes only; at most once per run
  force_step: forbidden              # the model cannot call /compact
  backstop: auto-compaction-on       # never DISABLE_COMPACT on this lane
survives:
  handoff_file: .lane/handoff.md     # printed by a SessionStart hook, matcher compact
  harness_reinjects: [project CLAUDE.md, auto memory, recent files, skills]
summary:
  prompt_replaceable: false          # steer with CLAUDE.md Compact instructions
  readable: true                     # PostCompact compact_summary
  capture: artifacts/{run_id}/compaction-{n}.md
retest: every-cli-upgrade            # replay the canary session, diff trigger point and survival
---
lane: swarm-worker-pi-12
harness: pi
harness_version: 0.85.1              # what the self-compact extension was built against
billing_path: openai-api
price_line_tokens: 272000
trigger:
  owner: agent                       # self_compact(note_to_self)
  notice_tokens: 140000
  warning_tokens: 210000
  hard_tokens: 250000                # warning + 40k buffer, under the price line
  force_step: allowed                # the agent can always call self_compact
  forced_phase_timeout_min: 5        # runner sends /self-compact-now, then kills
survives:
  note_to_self: verbatim             # returned byte for byte as the next turn's message
summary:
  prompt_replaceable: true
  readable: true
  capture: session compaction entry, copied to artifacts/{run_id}/
retest: every-cli-upgrade

The version pin matters because harnesses move under extensions. Pi 0.87.0 shipped on Sep 21 with breaking changes for extension authors (Pi changelog), while the self-compact README pins Pi v0.85.1; the pattern you are copying was tested two minor releases back. Canary every CLI upgrade replays a fixed long session after each bump and diffs the compaction trigger point, whether the note survived, and whether the summary is still readable. A model swap moves the window too, and swaps a provider makes under an unchanged ID are covered in forced model routing continuity.

Where an agent context compaction contract fails, and the signal for each

The stranded lane. A tool lock copied from Pi onto a Claude Code lane. Signal: a run of denied tool calls with no compaction event after them and no progress in the transcript. Fix: remove the force step wherever the harness owns the trigger.

The suffix trap. CLAUDE_CODE_AUTO_COMPACT_WINDOW set with a k, which reads as a tiny number and clamps to 100K. Signal: every compaction lands at or below 100,000 tokens. Read the compaction log rather than the status line, because once the variable is set the status line’s percentage no longer tells you when compaction will run.

The inert key. Codex’s model_auto_compact_token_limit set above 90% of the window, or compact_prompt edited on an OpenAI-provider lane. Signal: the trigger point in the logs never moves and the behaviour after compaction never changes. Fix: lower the limit or change the window, and record the prompt as not replaceable.

The jump past the line. One oversized tool result carries a Pi lane past the hard cutoff before the lock engages. Signal: tokensBefore on the compaction entry sits well above the contract’s hard line, or a request crosses a price line. Fix: cap tool output and widen the buffer.

The recovery block. A PreCompact veto that fired while Claude Code was recovering from a context-limit error. Signal: a failed request right after the hook blocked. Fix: veto only on a stale handoff file, once per run, with automatic compaction left on.

The bystander summary. The successor redoes work. Signal: the same file edited or the same test run twice across a compaction boundary. Fix: the handoff file’s done-with-evidence field, then the lint in the sibling piece.

Compaction policy belongs to the layer that runs the fleet

None of the five clauses is a prompt. A model told to compact wisely does so right up to the turn where finishing the task feels more urgent, and on a lane nobody watches, that turn always comes. The contract lives beside the lane: read by the runner that launches it, enforced by the hooks the harness calls, backed by the harness’s own automatic compaction as the wall, and re-tested whenever the harness moves. A multi-agent command center earns the name here: an inventory of lanes, which harness and version each one runs, and one page per lane that says who owns compaction.

The self-compact extension is a good idea with a narrow passport. Copy the thresholds and the note everywhere; copy the button only where the agent can reach it.

FAQ

Can Claude Code compact itself when the agent decides it should?

No. Claude Code’s docs list /compact among the built-in commands the Skill tool can’t reach, so the model cannot trigger it. The harness fires automatic compaction at its window, a person can run /compact, and a PreCompact hook can skip a proactive pass. Put the agent’s note-to-self in a handoff file instead.

Why does CLAUDE_CODE_AUTO_COMPACT_WINDOW=500k compact so early?

The environment variable takes a plain integer only. Claude Code reads 500k as 500 and clamps it to the 100K minimum, so the lane’s auto-compact window shrinks to 100,000 tokens. Write 500000. Only the /autocompact command and the --autocompact flag accept k and M suffixes, and the variable overrides both while it is set.

Sources