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

The JSONL transcript

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

Compaction drops things from the window — but not from the record. The transcript is the durable log of everything that happened in a session, written to disk as it goes. It's what lets --resume reopen a session and reconstruct its state, and it's where the dropped tool output still lives if you ever need to audit what the agent actually saw. The format is deliberately boring: JSONL — one JSON object per line, one line per event, appended as the session runs.

One event, one line

JSONL (JSON Lines) is append-only by design: to record an event you serialize it and write a single line — no rewriting the file, no closing a top-level array. Each event keeps its type (message, tool_use, tool_result, or compact) and the fields that type carries. A resume reads the file back line by line, parses each, and replays them in order to rebuild the conversation — so the transcript is the session, the same way the messages array was the context.

A session transcript — one object per line
{"type":"message","role":"user","text":"hi"}
{"type":"tool_use","name":"Bash"}
{"type":"tool_result","name":"Bash","content":"..."}
{"type":"compact","kept":["user-intent","edit"],"dropped":["tool-output"]}

# append-only: each event is JSON.stringify(event) on its own line
# resume: read lines → JSON.parse each → replay in order

What to build

Your task

Implement transcript and the pressure check

In src/context.ts, turn a session into its transcript and report window pressure:

  • transcript(events) — map each event object to its JSON line and return the array of lines. One JSON.stringify per event, in order; the array is the JSONL file (the loop joins it with \n when writing to disk).
  • Reuse needsCompaction(used, limit, threshold) from the previous step for the pressure flag.

The program.ts transcript subcommand reads fixtures/case.json ({ events, used, limit, threshold }) and prints { jsonl, needsCompaction } as one JSON line — jsonl is the array of per-event lines, needsCompaction is the pressure check on (used, limit, threshold).

tests/adapters.ts
export function transcript(events: object[]): string[] {
  // one JSON.stringify per event, in order — the JSONL lines
}

// reuse needsCompaction(used, limit, threshold) from the compaction step
// program.ts transcript prints: { jsonl: transcript(events),
//                                 needsCompaction: needsCompaction(used, limit, threshold) }
One object per line — no array, no pretty-printing

JSONL is not a JSON array. Don't wrap the events in [...] and don't pass a spacing argument to JSON.stringify — a pretty-printed object spans multiple lines and breaks the one-event-per-line contract, so a resume can't split on \n. Each element of the returned array must be exactly JSON.stringify(event) with no embedded newlines.

Key order is insertion order

JSON.stringify emits keys in the object's insertion order, and the grader byte-compares, so build each event with its keys in the same order the fixtures use (type first, then the rest). You don't reorder or normalize keys — you serialize the event objects exactly as they arrive.

How it's graded

Process mode. The harness runs npx tsx program.ts transcript fixtures/case.json and compares the printed line to fixtures/expected.json byte-for-byte. For events [{type:"message",role:"user",text:"hi"}, {type:"tool_use",name:"Bash"}] with used:160, limit:200, threshold:75, it must print {"jsonl":["{\"type\":\"message\",\"role\":\"user\",\"text\":\"hi\"}","{\"type\":\"tool_use\",\"name\":\"Bash\"}"],"needsCompaction":true}. 160/200 is 80% ≥ 75, so the flag is true; the jsonl strings are the inner objects serialized one per line.