Certified Agent Harness Engineer
Course sections

M4.3

Detect pressure and choose a response: the escalation ladder

1,038 words · about 6 min

This is the core mental model of the module.

The concept

Concept

When the window fills, four responses are available, and they form a strict ladder ordered by cost and by risk of information loss:

  1. Prune — shrink individual tool results in place, keeping their head and tail. Cheap, local, no model call, and the shape of the conversation is unchanged.
  2. Compact — replace a span of history with a model-generated summary. Expensive (it costs a model call), lossy (the summary is not the original), and structurally disruptive.
  3. Spill — move a large artefact to disk and leave a reference. Prevents pressure rather than relieving it; it must happen at write time, not at pressure time.
  4. Fail — stop and tell the truth.

The ladder's ordering is the lesson. You climb it only as far as you must, and you always try the lower rung first.

Why does pruning come before compaction? Consider what each destroys. Pruning a 200KB grep output down to its first 4,096 and last 1,024 characters destroys the middle of a mechanical listing — content the model has almost certainly already extracted what it needed from, and which it can regenerate by re-running the tool. Compaction destroys the conversation: the reasoning, the decisions, the false starts that explain why the current approach was chosen. It replaces them with a summary that is a lossy, model-generated approximation whose fidelity you cannot verify. Pruning is reversible in practice (re-run the tool); compaction is not (the original reasoning is gone from the model's view).

The cost asymmetry points the same way. Pruning is a synchronous string operation. Compaction requires a model call with the entire region as input — a call that can fail, can be slow, and can itself be expensive. And it can fail in the middle, which is why M4.4 spends most of its length on transaction structure.

There is a second-order effect that makes the ordering even more valuable: pruning often makes compaction unnecessary. In a coding agent, tool results routinely dominate history — file reads, search output, build logs. Reclaiming their middles can drop occupancy below the threshold on its own, and then no summary is ever generated and no reasoning is lost.

The mechanism

Mechanism

Detection needs a trigger and a measurement. dsh uses two triggers, typed as CompactionTrigger = 'pressure' | 'context-overflow':

  • pressure is proactive. It runs at agent/pre-step, before the request is derived, and compares the measured total against the threshold.
  • context-overflow is reactive. It fires from agent/request-error when the provider has already rejected the request with a context-length error.

Both exist because neither suffices alone. Proactive detection depends on an estimate that can be wrong; reactive detection is the safety net for when it is. Reactive detection alone would mean every overflow costs a wasted round trip, so proactive detection is the common path and reactive is the backstop.

The pressure path in packages/compaction/compaction-basic/src/index.ts implements the ladder literally, and the sequence is worth reading closely:

  1. Resolve the model's context window; throw a config error if the adapter declares none.
  2. Assert no compaction is already active (a re-check after the await — the window between measuring and acting is real).
  3. Compute thresholdTokens from the ratio.
  4. First gate: if measurement.totalTokens < spec.thresholdTokens, return. No pressure, no work.
  5. Prune: if the optional toolResultPruner service is present, call pruneSession(session), then remeasure.
  6. Second gate: if the remeasured total is now below threshold, return — without summarising anything.
  7. Only now: select a compactable region and summarise, retrying up to compactionRetries + 1 times.

Step 6 is the ladder made executable. Pruning gets a full opportunity to resolve the pressure before a single summarisation token is spent.

The overflow path is more aggressive, as it must be — the request has already been rejected, so there is no threshold left to respect. It prunes, remeasures, and then selects a region with retainTokens of 0, bypassing both the threshold and the retained tail. It also validates progress structurally rather than numerically: it snapshots session.surface.replaceGeneration before acting and only retries the request if the generation advanced. A retry that changed nothing would loop forever, and comparing token estimates would make loop termination depend on a heuristic. Comparing generations makes it depend on a fact.

Failure is handled by not handling it. If pressure compaction throws, the loop logs a warning and continues the turn:

step compaction failed: ${message}; continuing the turn

A failed context-management attempt must not abort work that might still succeed. If the request then overflows, the reactive path takes over; if that also fails, the provider's error reaches the user unmodified. Telling the truth is the fourth rung, and it is a legitimate destination.

Tradeoffs

The ladder assumes tool results are the cheapest thing to lose. That is true for coding agents and false for agents whose tool results are the deliverable — a research agent whose entire value is the fetched documents should not treat them as the first thing to shred. The ordering is a domain judgement dressed as a general principle, and you should re-derive it for your domain rather than inherit it.

Threshold choice is a real dilemma with no clean answer. Too low and you compact conversations that would have finished fine, paying summarisation cost and losing fidelity for nothing. Too high and you have insufficient room to compact within — because compaction itself needs a model call whose input is the region being summarised. dsh's 0.8 is a defensible default, not a derived optimum, and the repo does not claim otherwise.

How others differ

Claude Code performs auto-compaction at a similar occupancy and surfaces it to the user as a visible event, plus a manual /compact. Codex CLI has favoured shorter sessions and explicit resets over sophisticated in-session reclamation — a genuinely different bet: instead of engineering the escalation ladder, make sessions short enough that pressure is rare. That trades away long-running autonomous work, which is precisely the use case the ladder exists to support. Neither field practice is settled; there is no published consensus on where the threshold belongs or on whether summarisation beats simply dropping the oldest turns.


Check yourself

4 questions on M4.3

Start drill