Build your own Claude Code
Context management & compaction·28 min

Compact the history

0 / 1
# grade this stage (from your course directory)
npx theinference test --stage compaction --watch

Last step you learned the policy by heart: keep intent, edits, errors, and open tasks; drop verbatim output, intermediate reasoning, and skill descriptions; re-inject startup. Now you encode it. Compaction is graded as a pure classifier — given a list of items by kind, you say which survive and which go. No model judges the summary's prose; the grader checks two deterministic sets. You also build the trip wire that decides when to fire it.

The classification, by item kind
startup            → keep      (system prompt, tool defs, CLAUDE.md, memory)
user-intent        → keep      (the original goal)
edit               → keep      (diffs already applied)
error              → keep      (failures seen — don't repeat them)
task               → keep      (open to-do list)

tool-output        → drop      (verbatim file dumps / bash stdout)
reasoning          → drop      (intermediate thinking)
skill-description  → drop      (reloads on demand when invoked)

What to build

Your task

Implement compact and needsCompaction

Implement two functions in src/context.ts:

  • compact(items) — take a list of item kinds (strings) and partition them into { kept, dropped }. Keep startup, user-intent, edit, error, and task; drop tool-output, reasoning, and skill-description. Preserve input order within each list, and don't dedupe — classify each item independently.
  • needsCompaction(used, limit, threshold = 75) — return true once pressure reaches the threshold: (used / limit) * 100 >= threshold. Default the threshold to 75 (percent).

The program.ts compact subcommand reads fixtures/case.json ({ items }), calls compact, and prints { kept, dropped } as one JSON line.

tests/adapters.ts
const KEEP = new Set(['startup', 'user-intent', 'edit', 'error', 'task'])

export function compact(items: string[]): { kept: string[]; dropped: string[] } {
  // partition items by KEEP membership, preserving order in each list
}

export function needsCompaction(used: number, limit: number, threshold = 75): boolean {
  // true once (used / limit) * 100 >= threshold
}
Classify by kind, not by guessing

The grader compares your kept and dropped sets against the reference exactly, so an item in the wrong bucket fails the step. The two that trip people: skill-description is dropped (it reloads when the skill is invoked, so the body is never load-bearing in history), and task is kept (open work the agent must still finish). startup is kept here because the classifier sees it as a surviving item — re-injection of startup content is the loop's job, not the classifier's.

Use percent, and use >=

needsCompaction works in percent: (used / limit) * 100, compared with the threshold default of 75. Use >=, not > — at exactly the threshold you compact. used = 150, limit = 200 is 75% → true; used = 149 is 74.5% → false. Compacting at the line, not past it, is what keeps the next request from overflowing.

How it's graded

Process mode. The harness runs npx tsx program.ts compact fixtures/case.json, which calls compact(case.items) and prints JSON.stringify({ kept, dropped }). The grader compares that line to fixtures/expected.json byte-for-byte — so for items [startup, user-intent, tool-output, reasoning, edit, error, skill-description, task] it must print {"kept":["startup","user-intent","edit","error","task"],"dropped":["tool-output","reasoning","skill-description"]}. Order within each list comes straight from the input. This is a structural check on the policy — no model judges a summary.