Book a call
LESSON20mVERIFIED 2026-08-02 · CLAUDE CODE 2.1.220 · CODEX CLI 0.146.0 · GITHUB COPILOT CLI 1.0.77 · ANTIGRAVITY CLI 1.1.9

Reading a session like a profiler: where the context went

Where a session's context actually went: the startup block you never see, the tool result that blew it up, and the command in your harness that shows you.

"It got worse halfway through" is a measurement

We could say that sessions are where we all still struggle with — sometimes they work well, sometimes not, then we assume it's the model's bad day.

But that number is available, if you were to ask six harnesses what's in the window, five will tell you, one of them with no models being involved, the sixth won't say anything (which is good to know so you don't waste time looking for it). Because usually it's a single command away, which nobody told you.

This is just about examining the profile, we cover other remedies (compaction, subagents, retrieval) in separate chapters; let's find it first.

Limits of the profile

The profile doesn't tell you why, it tells you where — it shows you the biggest item, which might be a bug or the intended workload.

If the profile points to an enormous line, what's up to you is whether it deserves this size. Usually becomes clear once visible, so have a look.

These are often mixed:

  • Resident set — what's in the window now, if you asked the model "how long do you remember I told you about 40 minutes ago?"

  • Cumulative — how much has been billed so far across all turns of the session; for example, have a look at the real 20-turn session we did the math on — 20 turns, uncached input: 35,324, cache read: 1,300,798, cache write: 246,237, output: 9,016.

TEXT
turns            20
input        35,324      (fresh, uncached)
cache_read    1,300,798
cache_write     246,237
output           9,016

Cumulative input is therefore around 1,580,000 tokens, and the window at the last turn: 106,051. So same session is either gigantic or half-empty depending on what you ask, the muddled advice you sometimes see in this area comes from answering one question with figure from another.

Measuring the startup floor

To measure your startup floor we did a dry run of a trivial test across four harnesses: in an empty scratch dir, no git repo, no files, single prompt asking for exactly "ok", nothing requiring a tool; left them on their default models except Claude Code, where we asked for Haiku to keep it cheap; three answered.

Results:

  • Claude Code — 25,686 in / 39 out

  • GitHub Copilot CLI — ~26,500 in / 4 out

  • Antigravity CLI — 10,049 in / 23 out

What's important is that they didn't read anything and didn't search for anything, Claude Code sent 25,686 tokens to the model and received 39 back, its envelope cost "ok" 0.0188 dollars.

If you want to weigh what the model can see without paying, use Codex's debug command — it prints the model-visible prompt list as JSON and exits.

In the same empty dir we had five messages with eight text blocks, 20K characters of scaffolding, five characters of which are ours.

TEXT
developer    6,302 chars   <skills_instructions>…
developer    7,957 chars   <permissions instructions>…
developer      646 chars   <apps_instructions>…
developer    2,183 chars   You are `/root`, the primary agent in a team of agents…
developer      271 chars   <multi_agent_mode>…
user         2,274 chars   <recommended_plugins>…
user           460 chars   <environment_context>…
user             5 chars   hello

Same for Kimi, it persistently stores the context on disk during the run, so you can examine the same view later — our 3-char message above 21,027-char system prompt with the first usage line reporting 12,771 tokens. And cursor-agent refused to start in an empty temp dir — Workspace Trust Required, exit code 1, which we'll get back to in the Cursor section.

Again, it's not a criticism — these are things that make it an agent instead of a chatbot; this is your floor, specific to your machine and the extras you have installed. Until you measure it once you think about the window minus its heaviest element.

Four components of every profile

The four components of every profile are:

Fixed prefix — system prompt, tool definitions, memory files, skill descriptions, environment info; always there, not a one-time start-up charge. For example, this is what Anthropic themselves say about Claude Code in their own walkthrough (which is a self-declared representative, not the actual measurement) — 4,200 for the system prompt, 680 for the auto memory, 280 for environment info, 120 for MCP tool names, 450 for skill descriptions, 320 for the global CLAUDE.md and 1,800 for the one in the project itself — totals to 7,850 tokens before you even type a 45-token prompt.

Tool definitions — about installation, not about usage; Claude Code now keeps full MCP schemas deferred and loads them on demand, so same setups can weigh much less than they did last month with no user changes. Codex embeds a line in its own binary that says it about as bluntly as a vendor can: "The context window is a public good. Skills share the context window with everything else Codex needs: system prompt, conversation history, other Skills' metadata, and the actual user request."

Tool results — these are what we usually find heaviest in profiles, the ones users don't configure but the tool produces. One file read, one test log, one grep that matched six hundred times.

Conversation — the cheapest component of every profile, what the user says and the model responds; and also the first thing people cut.

Cache reads

When you see a large cache_read figure, it's neither free nor alarming. It's accounting — Copilot's model picker, for instance, prices input, cached input and output separately. It's purely a difference in billing, the tokens are still in the window.

Cheap and small are two different things; what the window cares about is size. We did a Claude Code run in an empty dir, it reported 17,536 cache-read tokens on the first turn — one turn, fresh dir, already 17K back from the cache.

To understand what the model can see, include cache reads. The resident number at the end of that 20-turn session above is 92,957 cache-read plus 13,092 cache-write plus 2 fresh tokens, and all three occupy space.

When the in-session view is not enough

There's always a file; all six write sessions to disk, and where there's a file with the chat it contains the conversation — what was actually transmitted, not a best-of view. Which is useful, because this is what survives once the session ends, which is when you usually want it.

Two questions, one shell command each:

  • What's the size of the window at the end? — sum last turn's input figures to get the resident set at the end

  • How much did it cost? — sum every turn to get the cumulative figure

Here's how to get the path for all six and a command for five of them, if there's only one thing to take away from this chapter, let it be where that file is — what separates "session went badly" from "went badly at turn nine when I've run a command that filled the window with its output and pushed everything else out".

Three types of profile shapes

There are three kinds of profiles (from our perspective, not documented anywhere).

Shape 1 — high and flat. This is about the floor. If it's too high, there must be something in the fixed prefix that's too big — memory file that has been growing for six months, or four MCP servers you've installed for an afternoon, or a skill catalogue you haven't pruned. In this case the in-session activity can't help because it predates the session, so fix the setup.

Shape 2 — step. Single turn, single jump; tool result that you can find in the log within seconds. What's left is how justified the cost was, usually easy to answer once you see it — was it a file or an entire directory?

Shape 3 — ramp. Gradual growth over a long session, with no single item responsible. The most common scenario, what compaction addresses, and the only one where the length of the session is the explanation.

We name them because the treatment varies per shape while the symptom is the same — all three end with the model losing the plot.

When to skip it

Usually, if the session was healthy you don't need any of this — profiling a fast endpoint doesn't make sense either; but:

  • Once when setting up a machine or a repo, so you know what your floor is and can notice if it doubles

  • Whenever you experience a session deteriorating as described at the beginning, alternative is guessing which doesn't work well here

IN YOUR HARNESS

Claude Code

In-session: /context (docs describe coloured grid with current usage info and optimisation tips for context-heavy tools, memory bloat, capacity warnings; from v2.1.216 also reports how much beyond the limit a too long conversation is and which command reclaims space); /cost, listed in command reference as an alias of /usage.

After session: the log is at ~/.claude/projects//.jsonl, one JSON object per line, where is your working directory with all awkward characters replaced by dashes. These are two example commands — one for the window contents at the final turn, another summing the session per-turn into input/cached/written/output; headless JSON output returns an envelope with usage, modelUsage, num_turns, duration_ms, total_cost_usd — a profile of a CI run and the cheapest place we know for a budget assertion.

BASH
# what the window held at the end
jq -c 'select(.type=="assistant") | .message.usage' session.jsonl | tail -1

# what the whole session sent
jq -s '[.[] | select(.type=="assistant") | .message.usage]
       | {turns: length,
          input:  (map(.input_tokens)            | add),
          cached: (map(.cache_read_input_tokens  // 0) | add),
          written:(map(.cache_creation_input_tokens // 0) | add),
          output: (map(.output_tokens)           | add)}' session.jsonl
  • Gotcha 1: Anthropic's page about context-window breakdown uses representative numbers, it tells you to run /context to see actual ones

  • Gotcha 2: compaction preserves instructions unequally — CLAUDE.md at the root of the project and auto memory are injected from disk back into the model, but rules carrying paths: frontmatter, and nested CLAUDE.md files, are lost until a matching file is read again; if you want a rule to survive compaction don't path-scope it

Codex CLI

The only harness that weighs the window before spending anything; codex debug prompt-input prints model-visible prompt list as JSON and exits — no call, no cost.

BASH
codex debug prompt-input "hello" \
  | jq -r '.[] | .role as $r | .content[] | "\($r)\t\(.text | length)"'

But: each message can have multiple content blocks, reading only the first one significantly undercounts. Run in your actual repo to see what your skills and settings do for you, then rerun with CODEX_HOME pointing at an empty scratch dir; difference is yours. On our machine it's 6,800 characters across three blocks — a recommended_plugins list and an apps_instructions block present only in the real home, plus a permissions_instructions block twice as big.

But: the first line of that plugins list says these are available but not installed.

In-session: /status (help text says it shows current session configuration and token usage) — model, approvals, token usage in one panel; /usage accepts daily, weekly, cumulative — status line can display permanently showing what percent of the window is used, what percent is left, window size in tokens, session's input/output totals.

After session: rollouts at ~/.codex/sessions/YYYY/MM/DD/rollout--.jsonl with a dedicated accounting event, not usage scattered through message records; it returns total_token_usage (running total), last_token_usage (the latest turn), model_context_window (ceiling) — three numbers kept separate, more than most formats provide.

BASH
jq -c 'select(.payload.type == "token_count") | .payload.info' \
  ~/.codex/sessions/2026/08/02/rollout-*.jsonl | tail -1

Gotcha: no /context in 0.146.0, use /status and the debug prompt-input command instead.

GitHub Copilot CLI

Has the most explicit breakdown of the six; its own help text says: "Shows context-window token usage, broken down by the largest consumers (system prompt, tools, and messages). Finer per-source attribution (skills, subagents, MCP servers, plugins) appears in experimental mode."

That means that per-source view is what you need to find the culprit, and enabling /experimental is required; other commands: /usage for AI credit usage with input/output/cached tokens breakdown, /env lists what has actually loaded (instructions, MCP servers, skills, agents, hooks, plugins, LSPs, extensions), /diagnose points a model at current session's log.

Non-interactive -p run prints an unrequested footer — changes, AI credits and time in seconds, input/output/cached/written tokens up/down, and a resume command.

TEXT
Changes    +0 -0
AI Credits 2.68 (3s)
Tokens     ↑ 26.5k (17.2k cached, 9.3k written) • ↓ 4
Resume     copilot --resume=19352792-9de5-413d-a539-c89644999f68

This is from the table above — 2.68 AI credits in 3s, ↑26.5k (17.2k cached, 9.3k written), ↓4; -s suppresses it, if a script shows nothing that's why.

After session: per-session state under ~/.copilot/session-state//, and SQLite store at ~/.copilot/session-store.db with a real per-turn profiling table you can query for recent rows with turn index, model, input/output/cache-read/cache-write tokens, credits, duration, time to first token.

BASH
sqlite3 -header ~/.copilot/session-store.db \
  "select turn_index, model, input_tokens, output_tokens, cache_read_tokens,
          cache_write_tokens, total_nano_aiu, duration_ms, time_to_first_token_ms
     from assistant_usage_events order by id desc limit 10;"

But: total_nano_aiu is 2679800000 in the run above — AI credits in nano units; divide by a billion to recover the 2.68 from the footer.

Gotcha: --max-ai-credits described as a soft cap, usage known only after response so the run can pass or hit it before the CLI realises and the following call is blocked rather than the one that hit the limit.

Cursor

The CLI doesn't do this — no /context, no /usage, no token panel; /summarize (alias /compress) shrinks the context, /about shows version and account info, neither shows what's in the window.

Headless: output-format reference lists result-event's fields — duration_ms, duration_api_ms, is_error, result, session_id, request_id — but explicitly says it doesn't return token usage or cost.

On disk: sessions at ~/.cursor/chats///, meta.json has schemaVersion, createdAtMs, updatedAtMs, hasConversation, cwd — enough to find a session and its working directory, not accounting.

Substitute 1: run /summarize before a long stretch instead of after the window is already tight; substitute 2: treat auto-loaded rules as an invisible fixed prefix — .mdc files in .cursor/rules and AGENTS.md in project root and subdirectories, docs say rule contents are included at the start of the model context.

But: a plain .md file dropped into .cursor/rules is ignored by the rules system, because it has no frontmatter. A rule you think is loaded and isn't is the opposite of context bloat, and just as hard to notice.

Gotcha: cursor-agent won't run in an untrusted directory on our machine, did exit 1 with Workspace Trust Required in an empty temp folder.

But: prompt offers two paths — deciding with the agent, or a flag; read flags first — --trust means trust current workspace without prompting, -f/--force means force allowing commands unless explicitly denied, --yolo is its alias; only --trust is about the directory.

Gotcha consequence: using --yolo in a CI job to dismiss a trust prompt changes what the agent is allowed to do — a much bigger change than the one asked.

Antigravity CLI

In-session: /context opens a context-usage visualisation panel, /usage (alias /quota) shows model quota usage, /credits shows remaining G1 credits and where to buy; separate commands for quota and window, more clear than in most.

Headless: agy -p with JSON output returns accounting inline, most straightforward of the six to assert on in a script.

JSON
{"conversation_id":"a0336593-…","status":"SUCCESS","response":"ok\n",
 "duration_seconds":1.110007,"num_turns":1,
 "usage":{"input_tokens":10049,"output_tokens":23,"thinking_tokens":17,
          "cache_read_tokens":8141,"total_tokens":10072}}

Returns conversation id, status, response text, duration in seconds, num_turns and usage with input, output, thinking, cache-read and total tokens (10,049 input / 23 output / 17 thinking / 8,141 cache read / 10,072 total).

But: thinking_tokens separated from output_tokens so the reasoning is its own line rather than included in the reply; useful when comparing against others.

After session: conversations under ~/.gemini/antigravity-cli/conversations/ with SQLite index at ~/.gemini/antigravity-cli/conversation_summaries.db.

But: index is for locating a conversation, not profiling — has columns like conversation_id, title, preview, step_count, last_modified_time, workspace_uris; step count, not token count.

Gotcha (us): very large window changes the failure mode rather than eliminating it. A small window is when the bloat is loud because of hitting the ceiling, on a big one you might never hit it so a doubled memory file and an unnecessary tool result just sit there diluting everything, and the session degrades with no error to blame; profile precisely because nothing broke.

Kimi Code CLI

In-session: /debug (source says it's for debugging the context) prints four counters — total messages, token count, checkpoints, and trajectory file behind the session — then full message history in a pager, q to exit.

Most direct of the six — not a rendering of the window, the window itself.

Other commands: /usage (alias /status) shows quota with progress bars and percentages on the Kimi Code platform, /compact shrinks the context and accepts instructions (e.g. preserve database-related discussions), /export writes the entire session to markdown with session id, working directory, message and token counts in header.

After session: ~/.kimi/sessions/// contains context.jsonl and wire.jsonl, context.jsonl is what matters — the window written out in sequence rather than summarised.

BASH
jq -c 'if .content then {role, chars: (.content | tostring | length)} else . end' \
  ~/.kimi/sessions/*/<session-id>/context.jsonl

One example — listing every entry's role and character count; result for a read-back session, the whole thing in seven lines — 21,027-char system prompt, checkpoint, 3-char user message, checkpoint, usage 12,771 tokens, 57-char assistant reply, usage 12,798.

TEXT
{"role":"_system_prompt","chars":21027}
{"role":"_checkpoint","id":0}
{"role":"user","chars":3}
{"role":"_checkpoint","id":1}
{"role":"_usage","token_count":12771}
{"role":"assistant","chars":57}
{"role":"_usage","token_count":12798}

No other tool exposes the floor as plainly as this.

Gotcha: which Kimi do you have? The legacy Python kimi-cli and the rebuilt Kimi Code CLI are two separate binaries both called kimi; on our machine the rebuilt binary crashed at startup while the legacy one worked.

Gotcha consequence: the on-disk layout and the /debug counters above were read off that legacy build and the files it wrote, while the slash commands come from the Kimi Code CLI docs, version stamped on the lesson is of the legacy one (which we ran); run kimi --version to see which is first in PATH.

What changes after you've done it once

The payoff is not a figure, it's that "the model got worse" stops being a self-explanation. Because you can have the profile, getting it takes a second and it's almost never about the model — an outgrown memory file, a forgotten server or a command that dumps its output into the window and pushes everything else to its edge.

j / k to move between lessons