DeepSeek Harness · Context Engineering

Compaction Dual Paths & replaceGeneration

When context is nearly full, pack proactively; when it really blows up, pack first then retry. Before retry, check the generation number — if packing didn’t work, no retry.

Course goalAfter this lesson you can explain two ideas: why DSH splits auto-compaction into proactive and passive triggers that each own a segment and don’t overlap; and why the credential for overflow retry is the monotonically increasing replaceGeneration number — and why a plugin’s own return value doesn’t count.
Interactive demo · Suitcase packing simulator

Think of the context window as a suitcase: each message is a piece of clothing; the dashed line is the 80%-full warning. The pack count at bottom-left is replaceGeneration (generation number) — it only goes up. Three scenarios show three fates of compaction, with captions at every step.

80% line (threshold 0.8)
Request rejected · CONTEXT_WINDOW_EXCEEDED
Pack count replaceGeneration3
Pick a scenario and hit Play, or scroll here to auto-play Scenario A.
Logic trail (line numbers map to compaction-basic/src/index.ts; the line lights up as the animation reaches that step)
PRESSURE path
on('agent/pre-step')L147
measure().totalTokens ≥ thresholdTokens ?L304
Prune → summarize → surface replaceL308-323
CONTEXT-OVERFLOW path
on('agent/request-error')L179
code ≠ CONTEXT_WINDOW_EXCEEDED → next()L183
generation = surface.replaceGenerationL191
compactIfNeeded('context-overflow')L194
replaceGeneration > generation ?L218-219
Yes → return { kind: 'retry' }L222
No → return next(), keep original errorL219
This demo is a teaching simulation: capacity bar, clothing blocks, and generation number are course abstractions. Scenario C’s lying compaction backend is a teaching assumption — real compaction-basic doesn’t lie; but compaction is an open seam, so once a third-party backend plugs in, anything can happen, and generation reconciliation is what catches them. Source:packages/compaction/compaction-basic/src/index.ts lines 147–223.
Design idea 1 · Packing needs two triggers

What problem it solves

Suppose you only have the proactive threshold path: measure before each request, pack when past 80%. Sounds enough — it isn’t. Token counts are estimates; estimates and the provider’s real count always diverge. A huge tool result can land suddenly — measurement hasn’t hit the threshold yet, but the request already exceeds the limit and the provider rejects it. With no recovery logic to take over, the turn dies with an error, and the user sees a baffling failure.

One miscount and you slam into the wall with no second line of defense. That’s the single-trigger problem.

What the idea is

DSH splits this into two independent triggers. Pack proactively when nearly full; recover passively after a wall hit. The two paths hang on different events, use different conditions, and have different failure semantics.

The pressure path hangs before each Step (one model request) starts. Measure total tokens; if past 0.8 of capacity, pack — leaving a 16% raw tail of recent dialogue. Failure semantics are loose: if packing errors mid-way, log a line and keep going; a failed early pack isn’t the end of the world.

The context-overflow path hangs after a request error, and only accepts the adapter-normalized code CONTEXT_WINDOW_EXCEEDED — other errors pass through. It ignores the threshold, zeros the reserve budget, and forces a real shrink. Failure semantics are strict: you must decide — retry, or keep and report the original error. Retry cap defaults to 1; each successful model reply resets the counter, so healthy sessions don’t get stuck.

Same job, two triggers, each owns a segment Nearly full (measure before request) Total tokens over 80% of capacity Proactive packing Keep 16% recent dialogue raw Journey continues On failure, just log a line Hit the wall (request rejected) CONTEXT_WINDOW_EXCEEDED Forced packing Ignore threshold; pack everything possible Decide after reconciliation Retry, or report original error
Top row is prevention; bottom is fallback. The two paths are independent — if one fails, the other still works.

Source:pressure listener at packages/compaction/compaction-basic/src/index.ts lines 147–165; overflow listener at lines 179–223; defaults 0.8 and 0.16 in the same package’s config.ts lines 20 and 23; retry cap default 1 at line 93 — all overridable per provider+model combo.

Why it lasts

Separating prevention from fallback is a reliability-engineering rule. Backup and restore are two systems; rate limiting and circuit breakers are two gates — same idea: the prevention path wants cheap, frequent, and failure-tolerant; the fallback path wants reliable, rare, and accountable on failure. Stuff both into one logic and they’ll compromise each other. So even if you rewrite the whole harness in another language, as long as models have context caps and tokens are estimated, you still need both triggers.

Design idea 2 · Retries must show evidence

What problem it solves

After hitting the wall you pack once, then resend. The question: how do you know packing actually worked? Compaction is an open seam — third parties can plug in custom backends. Suppose a backend always reports success but never changes model-visible content: if you retry on return value alone, the request still overflows, errors again, compacts again, retries again — each lap burns API money for nothing until dawn.

What the idea is

DSH’s answer is a generation number. Background: surface is the live projection of model-visible events in the session log — the dialogue as the model sees it. replaceGeneration is a read-only counter on it for how many times that dialogue was replaced. Only one place in the whole codebase increments it: the moment old messages are truly replaced by a summary and truly flushed to disk. No API can shrink or reset it. So a forward generation number is mathematically equivalent to at least one real, persisted replacement.

Overflow recovery is three steps: snapshot the current value before packing; pack; compare the new value to the snapshot. Strictly greater → allow retry; otherwise pass through and report the original error as-is. What the plugin says doesn’t matter — whether the ledger number moved does.

Before retry, check whether the pack count moved Snapshot before acting Pack count = 3 Pack once Hand off to compaction backend Check the count again What is it now? Now 4 — the suitcase really moved Allow retry Still 3 — packing didn’t work Refuse retry; report original error
The counter only goes up; only the one place where a replacement truly persists increments it — so a larger number is hard evidence.

There’s a reverse detail too. Even if packing throws mid-way, as long as free pruning already persisted and the generation advanced, that progress still qualifies a retry. Allow on evidence, refuse on evidence — same standard both ways.

If the generation didn’t advance, not a single retry is allowed.

Source:Snapshot and compare at packages/compaction/compaction-basic/src/index.ts line 191 and lines 218–222; retry on already-persisted progress after exception at lines 195–208; replaceGeneration defined in packages/core/session/src/surface.ts lines 136–142; the sole increment in the codebase at lines 361–371. The project’s Agent Note (.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.zh.md) explicitly rejected return-value-only checks, because a custom backend may report success without changing model-visible state.

Why it lasts

Proving state really changed with a monotonically increasing version number is a pattern databases have used for decades as optimistic locking; Git commit chains and distributed-system epochs are variants. The win: turn a trust problem into arithmetic — the executor can lie, the ledger can’t. As long as untrusted extension points exist, checking an immutable counter before retry is always the cheapest defense.

Side-by-side · How three systems stop money-burning
Grok Build

The proactive threshold path is isomorphic to DSH’s pressure: default 85% trigger, plus an off-by-default two-pass pre-summary — details in Compaction: 85% threshold & optional two-pass. Passive recovery after request error plus generation reconciliation — no equivalent in the Grok Build materials we’ve checked. Based on public evidence; unknowns reserved.

Claude Code

The proactive side is thickest: before each API call it runs trim, micro-compact, fold, and full summary. The anti-burn answer is a counted circuit breaker: stop after 3 consecutive auto-compaction failures. That 3 comes from a real incident — source comments record 1279 sessions failing 50+ times in a row, wasting ~250k API calls/day globally. Source: claude-code-sourcemap-main/study/chapters/03-context-management.md lines 78–81, 121–124.

One comparison focus: does compaction failure loop and burn money? Claude Code counts failures and trips a circuit breaker at 3 — a loss limit calibrated from incident data: allow the problem a few times, then cap it. DSH doesn’t count; every retry must show generation advance — zero invalid retries allowed. That’s structural proof: invalid retries can’t leave the mechanism. Their 3 is fed by incidents; DSH’s 0 is deduced. Add orthogonal dual paths, and these two are DSH-unique among the three.

Classroom Exercise
01

Hand-walk a lying-success backend

Set retry cap to 1; install a custom compaction backend that always reports packing success but never really replaces model-visible content. Now the first overflow error happens.

Q1: Will DSH start a second compaction attempt? Hint: after reconciliation fails, the original error is reported and the turn ends — the retry counter never gets a chance to increment.

Q2: If the criterion becomes return-value-only, and each lap takes 5 seconds in the same scenario, how many doomed requests fire in the first minute? And after which request does Claude Code’s 3-failure circuit breaker stop the bleeding?

Takeaway:Pack when nearly full; recover when you hit the wall — prevention and fallback each have their own trigger. The credential for overflow retry is monotone advance of replaceGeneration; a plugin’s return value doesn’t count. Demand evidence — don’t trust verbal reports.