<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://jazzyalex.github.io/agent-sessions/feed.xml" rel="self" type="application/atom+xml" /><link href="https://jazzyalex.github.io/agent-sessions/" rel="alternate" type="text/html" /><updated>2026-07-16T18:50:11-07:00</updated><id>https://jazzyalex.github.io/agent-sessions/feed.xml</id><title type="html">Agent Sessions</title><subtitle>Local history, search, and usage tracking for AI coding agents on macOS — Codex, Claude Code, Cursor, OpenCode, Copilot, and more.</subtitle><entry><title type="html">How coding agents remember: a field study of six session-history formats</title><link href="https://jazzyalex.github.io/agent-sessions/blog/how-coding-agents-remember/" rel="alternate" type="text/html" title="How coding agents remember: a field study of six session-history formats" /><published>2026-07-14T00:00:00-07:00</published><updated>2026-07-14T00:00:00-07:00</updated><id>https://jazzyalex.github.io/agent-sessions/blog/how-coding-agents-remember</id><content type="html" xml:base="https://jazzyalex.github.io/agent-sessions/blog/how-coding-agents-remember/"><![CDATA[<p>Over the last eleven months, the coding agents on one of our Macs wrote 3,096
session transcripts totaling 3.8 GB — roughly a million logged events across
Claude Code, Codex, Cursor, GitHub Copilot CLI, OpenCode, and Hermes. Every one
of those files is the complete record of a working session: what was asked,
what was tried, what broke, what shipped.</p>

<p>There is a useful way to think about these files. The model has no memory of
its own; when the session ends, everything it will ever be able to recall
about that refactor is what it managed to write down at the time. The session
file is the agent’s dream journal, kept while the dream is happening. Six
agents keep six very different journals, and the differences are not
cosmetic. They decide what the agent can resume, what a tool like a history
browser can show you, what you can audit six months later, and what is
silently lost.</p>

<p>A <a href="/blog/where-agents-store-history/">companion post</a>
covers where these files live on disk. This one is about what is inside them.
We measured it.</p>

<h2 id="method">Method</h2>

<p>We ran a read-only measurement harness over every session store on one
machine: 184 Claude Code sessions, 2,706 Codex rollouts, 14 Cursor Agent
transcripts, 20 Copilot CLI sessions, 61 OpenCode sessions, and 111 Hermes
sessions, spanning August 2025 to July 2026. The harness parses every event,
classifies it (user message, assistant message, tool activity, metadata), and
records aggregates only: counts, byte sizes, ratios, field coverage, timings.
No transcript content leaves the analysis, and the numbers below are corpus
totals, not excerpts. The script and the raw aggregates are <a href="https://github.com/jazzyalex/agent-sessions/tree/main/docs/superpowers/specs/data">in the Agent
Sessions repo</a>.</p>

<p>Honest caveats, stated up front. This is one machine and one user, so the
usage mix differs per agent: Codex did most of the daily work here, Cursor
saw fourteen sessions. Cross-agent comparisons are indicative, not a
controlled experiment. Where a number depends on usage rather than format, we
say so.</p>

<h2 id="finding-1-the-journal-is-almost-all-margin-notes">Finding 1: the journal is almost all margin notes</h2>

<p>The first thing the data shows is how expensive remembering is. Divide each
store’s total size by the number of user-visible messages in it, and you get
the cost of remembering one human sentence:</p>

<figure class="post-figure">
<div class="viz-root" style="--viz-ink:#0b0b0b; --viz-ink2:#52514e; --viz-muted:#898781; --viz-grid:#e1e0d9; --viz-axis:#c3c2b7; --viz-bar:#2a78d6;">
<style>
@media (prefers-color-scheme: dark) {
  .viz-root { --viz-ink:#ffffff; --viz-ink2:#c3c2b7; --viz-muted:#898781; --viz-grid:#2c2c2a; --viz-axis:#383835; --viz-bar:#3987e5; }
}
.viz-root svg { max-width: 720px; width: 100%; height: auto; display: block; margin: 0 auto; }
.viz-root text { font-family: system-ui, -apple-system, "Segoe UI", sans-serif; }
</style>
<svg viewBox="0 0 720 296" role="img" aria-label="Bar chart: kilobytes stored per user-visible message, by agent. Codex 126, OpenCode 114, Claude Code 113, Hermes 91, Copilot CLI 25, Cursor Agent 1.8.">
  <text x="12" y="20" font-size="14" font-weight="600" fill="var(--viz-ink)">Kilobytes stored per user-visible message</text>
  <text x="12" y="38" font-size="12" fill="var(--viz-ink2)">Store size ÷ user messages, measured per corpus</text>
  <!-- baseline -->
  <line x1="130" y1="52" x2="130" y2="270" stroke="var(--viz-axis)" stroke-width="1" />
  <!-- bars: x=130, scale 4.3 px/KB, h=22, gap 36 -->
  <g font-size="12.5">
    <text x="122" y="70" text-anchor="end" fill="var(--viz-ink)">Codex</text>
    <rect x="130" y="58" width="542" height="22" rx="4" fill="var(--viz-bar)" />
    <text x="664" y="74" text-anchor="end" font-size="12" font-weight="600" fill="#ffffff">126 KB</text>

    <text x="122" y="106" text-anchor="end" fill="var(--viz-ink)">OpenCode</text>
    <rect x="130" y="94" width="490" height="22" rx="4" fill="var(--viz-bar)" />
    <text x="612" y="110" text-anchor="end" font-size="12" font-weight="600" fill="#ffffff">114 KB</text>

    <text x="122" y="142" text-anchor="end" fill="var(--viz-ink)">Claude Code</text>
    <rect x="130" y="130" width="486" height="22" rx="4" fill="var(--viz-bar)" />
    <text x="608" y="146" text-anchor="end" font-size="12" font-weight="600" fill="#ffffff">113 KB</text>

    <text x="122" y="178" text-anchor="end" fill="var(--viz-ink)">Hermes</text>
    <rect x="130" y="166" width="391" height="22" rx="4" fill="var(--viz-bar)" />
    <text x="513" y="182" text-anchor="end" font-size="12" font-weight="600" fill="#ffffff">91 KB</text>

    <text x="122" y="214" text-anchor="end" fill="var(--viz-ink)">Copilot CLI</text>
    <rect x="130" y="202" width="108" height="22" rx="4" fill="var(--viz-bar)" />
    <text x="246" y="218" font-size="12" font-weight="600" fill="var(--viz-ink)">25 KB</text>

    <text x="122" y="250" text-anchor="end" fill="var(--viz-ink)">Cursor Agent</text>
    <rect x="130" y="238" width="8" height="22" rx="2" fill="var(--viz-bar)" />
    <text x="146" y="254" font-size="12" font-weight="600" fill="var(--viz-ink)">1.8 KB</text>
  </g>
  <text x="130" y="288" font-size="11" fill="var(--viz-muted)">Database stores (OpenCode, Hermes) include their indexes. Cursor's transcript omits tool output entirely.</text>
</svg>
</div>
<figcaption>What it costs each agent to remember that you said one thing. Codex spends 126 KB per user message; Cursor spends 1.8 KB, but only because its transcript leaves the tool activity out.</figcaption>
</figure>

<p>The headline ratio is starker than the bar chart. In the Claude Code corpus,
visible conversation text (what you typed plus what the model said to you) is
3.0% of the bytes. In Codex it is 2.6%. Everything else is tool calls, tool
output, reasoning items, metadata envelopes, and bookkeeping. The journal is
almost entirely margin notes about the dream, not the dream itself.</p>

<p>That is not waste, mostly. Tool output is the evidence of what actually
happened, and it is the part you grep for later. But the volumes are worth
knowing: the largest single Codex session on this machine is a 212 MB JSONL
file, and Claude Code’s largest is 19 MB. Any tool that reads these naively
into memory will eventually meet one of those.</p>

<p>Cursor is the interesting outlier. At 1.8 KB per user message, with half its
bytes being visible text, its transcript is lean because it is incomplete: in
our corpus it records zero tool events and zero per-event timestamps. The
transcript reads well and remembers almost nothing about how the work was
done. It is a diary with the verbs removed.</p>

<h2 id="finding-2-some-agents-know-what-they-cost-and-some-have-no-idea">Finding 2: some agents know what they cost, and some have no idea</h2>

<p>The second axis is self-knowledge: what the format records about its own
execution. The spread here is wide.</p>

<p>Copilot CLI is the quantified dreamer. Every assistant message carries the
model name, output token count, and request IDs; every tool execution carries
the model that requested it; and at shutdown it writes a full accounting
event: total conversation tokens, premium request count, API duration, even
the size of its own event file. Hermes keeps a ledger in the same spirit: its
sessions table has thirty-three columns, including input, output, cache-read,
cache-write, and reasoning token counts, plus <code class="language-plaintext highlighter-rouge">estimated_cost_usd</code> and
<code class="language-plaintext highlighter-rouge">actual_cost_usd</code>. OpenCode records tokens and a <code class="language-plaintext highlighter-rouge">cost</code> figure on each
message row. These three can answer “what did this session cost” from local
data alone.</p>

<p>The bigger names are thriftier. Codex writes turn-level <code class="language-plaintext highlighter-rouge">token_count</code> events
with cumulative usage, which is enough for totals but not for per-message
attribution, and no dollar figure. Claude Code stamps every assistant event
with the model and a usage block, which is genuinely good, and also the
occasion for a correction: our companion post claimed Claude Code writes no
per-event model. Wrong. In this corpus, all 36,376 assistant events carry
<code class="language-plaintext highlighter-rouge">message.model</code>. We measured our own claim and it failed; the fix is in both
posts.</p>

<p>Cursor’s transcript records none of this. The model hint lives in a separate
metadata database, and the events themselves are undated. If you want to know
what a Cursor session cost, the answer is on the vendor’s dashboard, not your
disk.</p>

<h2 id="finding-3-the-dreams-they-are-no-longer-allowed-to-reread">Finding 3: the dreams they are no longer allowed to reread</h2>

<p>Reasoning is where the field is converging, quietly, on the same policy:
sealed.</p>

<p>Codex is explicit about it. In the forty most recent rollouts here, all 7,339
reasoning items are opaque <code class="language-plaintext highlighter-rouge">encrypted_content</code> blobs. They sit on your disk,
in your file, and neither you nor the agent that wrote them can read them
back.</p>

<p>Claude Code turns out to have gone the same way, with less announcement. Its
thinking blocks have a <code class="language-plaintext highlighter-rouge">thinking</code> text field and a cryptographic <code class="language-plaintext highlighter-rouge">signature</code>.
In the thirty most recent sessions on this machine, 2,567 of 2,628 thinking
blocks have a signature and an empty text field. About 2.5% still carry
plaintext. The agent’s private reasoning is now certified rather than
recorded: the file can prove the thinking happened, and cannot say what it
was.</p>

<p>The rest of the field is split. Copilot writes both a <code class="language-plaintext highlighter-rouge">reasoningText</code> and a
<code class="language-plaintext highlighter-rouge">reasoningOpaque</code> field, hedging per message. OpenCode stores reasoning as
first-class part rows, 743 of 1,207 with readable text, the rest empty
depending on the provider. Hermes keeps a plaintext <code class="language-plaintext highlighter-rouge">reasoning_content</code>
column.</p>

<p>There are real reasons for sealing: provider policies, distillation
concerns, resumability across stateless APIs. But it changes what your
archive is. A year of session history used to include why the agent did
things; increasingly it only includes what it did. If the why matters to you,
the readable summary the agent chooses to say out loud is now the only record
of it, which is worth knowing when you decide how much to trust that summary.</p>

<h2 id="finding-4-remembering-is-not-the-same-as-being-able-to-recall">Finding 4: remembering is not the same as being able to recall</h2>

<p>The last measurement is retrieval. A memory you cannot search is barely a
memory, and this is where the two storage philosophies split cleanly.</p>

<p>The JSONL camp (Claude Code, Codex, Copilot, Cursor) has no index of any
kind. Finding a word in this machine’s 3.4 GB of Codex history means reading
3.4 GB: a naive <code class="language-plaintext highlighter-rouge">grep -rl</code> takes 10.8 seconds, and a full structured parse of
the corpus takes 16.6 seconds. Do that on every keystroke of a search box and
you understand why history browsers build their own indexes.</p>

<p>Hermes answers the same class of question in 0.4 milliseconds, four orders
of magnitude faster, because it ships a trigram full-text index inside its
database. It pays for recall honestly: twelve of its eighteen tables are
search infrastructure, and actual message content is about 11% of the
database’s bytes. Hermes spends most of its memory on being able to remember.
OpenCode sits in between: proper relational rows (a query away from anything)
but no full-text index, and reconstructing one message means joining an
average of 3.6 part rows.</p>

<p>Neither philosophy wins outright. JSONL survives crashes by construction (a
truncated last line is the entire failure mode), diffs cleanly, and can be
read by any tool ever written. Databases answer questions. The scorecard
shows how each agent actually trades this off:</p>

<figure class="post-figure">
<div class="viz-root" style="--viz-ink:#0b0b0b; --viz-ink2:#52514e; --viz-muted:#898781; --viz-grid:#e1e0d9;">
<style>
@media (prefers-color-scheme: dark) {
  .viz-root { --viz-ink:#ffffff; --viz-ink2:#c3c2b7; --viz-muted:#898781; --viz-grid:#2c2c2a; }
}
</style>
<svg viewBox="0 0 720 420" role="img" aria-label="Scorecard matrix of nine capabilities across six agents. Full descriptions in the surrounding text.">
  <text x="12" y="20" font-size="14" font-weight="600" fill="var(--viz-ink)">What each session format actually records</text>
  <text x="12" y="38" font-size="12" fill="var(--viz-ink2)">● recorded &nbsp;&nbsp;◐ partial or indirect &nbsp;&nbsp;○ absent — measured on this corpus, July 2026</text>
  <g font-family="system-ui, -apple-system, 'Segoe UI', sans-serif">
    <!-- column headers -->
    <g font-size="11.5" font-weight="600" fill="var(--viz-ink)" text-anchor="middle">
      <text x="278" y="66">Claude</text>
      <text x="358" y="66">Codex</text>
      <text x="438" y="66">Cursor</text>
      <text x="518" y="66">Copilot</text>
      <text x="598" y="66">OpenCode</text>
      <text x="678" y="66">Hermes</text>
    </g>
    <!-- rows: y start 96, step 36 -->
    <g font-size="12.5" fill="var(--viz-ink)">
      <text x="12" y="100">Per-event timestamps</text>
      <text x="12" y="136">Model per message</text>
      <text x="12" y="172">Token counts</text>
      <text x="12" y="208">Cost in dollars</text>
      <text x="12" y="244">Readable reasoning</text>
      <text x="12" y="280">Tool calls + output</text>
      <text x="12" y="316">Thread / tree structure</text>
      <text x="12" y="352">Built-in search index</text>
      <text x="12" y="388">Documented schema</text>
    </g>
    <g font-size="15" fill="var(--viz-ink)" text-anchor="middle">
      <!-- timestamps -->
      <text x="278" y="101">●</text><text x="358" y="101">●</text><text x="438" y="101">○</text><text x="518" y="101">●</text><text x="598" y="101">●</text><text x="678" y="101">●</text>
      <!-- model per message -->
      <text x="278" y="137">●</text><text x="358" y="137">◐</text><text x="438" y="137">◐</text><text x="518" y="137">●</text><text x="598" y="137">●</text><text x="678" y="137">◐</text>
      <!-- tokens -->
      <text x="278" y="173">●</text><text x="358" y="173">●</text><text x="438" y="173">○</text><text x="518" y="173">●</text><text x="598" y="173">●</text><text x="678" y="173">●</text>
      <!-- cost -->
      <text x="278" y="209">○</text><text x="358" y="209">○</text><text x="438" y="209">○</text><text x="518" y="209">◐</text><text x="598" y="209">●</text><text x="678" y="209">●</text>
      <!-- readable reasoning -->
      <text x="278" y="245">○</text><text x="358" y="245">○</text><text x="438" y="245">○</text><text x="518" y="245">◐</text><text x="598" y="245">◐</text><text x="678" y="245">◐</text>
      <!-- tool i/o -->
      <text x="278" y="281">●</text><text x="358" y="281">●</text><text x="438" y="281">○</text><text x="518" y="281">●</text><text x="598" y="281">●</text><text x="678" y="281">●</text>
      <!-- tree -->
      <text x="278" y="317">●</text><text x="358" y="317">○</text><text x="438" y="317">○</text><text x="518" y="317">◐</text><text x="598" y="317">◐</text><text x="678" y="317">◐</text>
      <!-- search index -->
      <text x="278" y="353">○</text><text x="358" y="353">○</text><text x="438" y="353">○</text><text x="518" y="353">○</text><text x="598" y="353">○</text><text x="678" y="353">●</text>
      <!-- docs -->
      <text x="278" y="389">○</text><text x="358" y="389">○</text><text x="438" y="389">○</text><text x="518" y="389">○</text><text x="598" y="389">○</text><text x="678" y="389">○</text>
    </g>
    <!-- hairlines -->
    <g stroke="var(--viz-grid)" stroke-width="1">
      <line x1="12" y1="76" x2="708" y2="76" />
      <line x1="12" y1="112" x2="708" y2="112" />
      <line x1="12" y1="148" x2="708" y2="148" />
      <line x1="12" y1="184" x2="708" y2="184" />
      <line x1="12" y1="220" x2="708" y2="220" />
      <line x1="12" y1="256" x2="708" y2="256" />
      <line x1="12" y1="292" x2="708" y2="292" />
      <line x1="12" y1="328" x2="708" y2="328" />
      <line x1="12" y1="364" x2="708" y2="364" />
      <line x1="12" y1="400" x2="708" y2="400" />
    </g>
  </g>
</svg>
</div>
<figcaption>Nine things a session format can record, measured across six agents. Claude's timestamps cover conversation events (87.7% of all lines); Cursor's model hint and Hermes's model live outside the per-message record; Copilot counts premium requests but not dollars. Note the bottom row: nobody documents their schema.</figcaption>
</figure>

<p>The bottom row deserves its own sentence. None of the six formats has public
schema documentation. Every parser of these files, including ours, is built
by reverse engineering, and every one of them breaks a little when a vendor
renames a field. For files this valuable, that is a strange industry norm.</p>

<h2 id="what-a-session-format-should-be">What a session format should be</h2>

<p>Having read a million events written six different ways, here is the
standard we would hold any of them to, and how the field measures against it.</p>

<p><strong>Write like a log, read like a database.</strong> Append-only JSONL for the write
path, because a crash mid-write should cost one line, not a database
recovery. A sidecar index (SQLite is fine) for the read path, rebuilt from
the log whenever it is stale. Today every agent picks exactly one half:
the JSONL camp cannot search and the database camp cannot <code class="language-plaintext highlighter-rouge">tail -f</code>. Hermes
is closest to the right shape, but the log half of it is inside the database
rather than beside it.</p>

<p><strong>Attribute every message.</strong> Model, timestamps, token usage, and cost belong
on each message, not in a session header, because sessions switch models
mid-flight and post-hoc accounting depends on it. Claude Code and OpenCode
are closest here. Codex should move usage from turn level to message level.
Cursor should start writing timestamps into its own transcript, which in
2026 is a modest ask.</p>

<p><strong>Seal reasoning honestly.</strong> If reasoning must be opaque, say so in the
format: a documented field, a stated policy, and a user-visible switch where
policy allows one. Claude Code’s silent shift from plaintext thinking to
signature-only is the pattern to avoid, less because of the sealing than
because nothing announced it. Users planning to audit their own history a
year out deserve to know the why is no longer being kept.</p>

<p><strong>Publish the schema.</strong> One versioned page per vendor, listing the event
types and stability guarantees. Codex already versions its files internally
and tolerates unknown fields, which is most of the work. The ecosystem that
wants to exist around these files — history browsers, usage analytics, team
knowledge tools — is currently built on guesswork.</p>

<p>The files themselves are the good news. Every agent in this study writes its
journal locally, completely, and by default, and none of it leaves your
machine on its own. The formats are six dialects of the same instinct, and
the instinct is right: the work is worth remembering.</p>

<p><a href="https://github.com/jazzyalex/agent-sessions">Agent Sessions</a> is a free,
local-only macOS browser for all six of these stores; it reads them read-only
and never writes back. The measurement script and raw aggregates behind
every number here are in the repo, and the companion post on the exact disk
locations is <a href="/blog/where-agents-store-history/">here</a>.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[We measured 3,096 real sessions and 3.8 GB of transcripts across Claude Code, Codex, Cursor, Copilot, OpenCode, and Hermes — verbosity, telemetry, sealed reasoning, and search — with a scorecard and recommendations.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://jazzyalex.github.io/agent-sessions/assets/marketing/screenshots/agent-sessions-codex-claude-history.png" /><media:content medium="image" url="https://jazzyalex.github.io/agent-sessions/assets/marketing/screenshots/agent-sessions-codex-claude-history.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Where AI coding agents store your session history: the real paths and formats</title><link href="https://jazzyalex.github.io/agent-sessions/blog/where-agents-store-history/" rel="alternate" type="text/html" title="Where AI coding agents store your session history: the real paths and formats" /><published>2026-07-11T00:00:00-07:00</published><updated>2026-07-11T00:00:00-07:00</updated><id>https://jazzyalex.github.io/agent-sessions/blog/where-agents-store-history</id><content type="html" xml:base="https://jazzyalex.github.io/agent-sessions/blog/where-agents-store-history/"><![CDATA[<p>Every coding agent you run writes a full transcript of the session to a file on
your own disk. The prompts you typed, the tool calls it made, the command
output, the diffs it proposed, the reasoning it showed: all of it lands in a
local file the moment each turn completes. What differs, and differs wildly, is
where that file goes and what shape it takes. Claude Code writes
newline-delimited JSON, one file per session, filed under the project you were
working in. Codex writes a similar format but shards it by date under a
completely different root. OpenCode stopped writing loose files and moved the
whole history into a single SQLite database. Cursor splits one session across
two stores in two different formats. None of them agree, and almost none of it
is documented where you would think to look.</p>

<p>That matters because this data is often the only durable record of how a piece
of work actually happened: the command that failed, the path you settled on, the
reason a function looks the way it does. It is genuinely valuable and genuinely
scattered. Most people assume old agent history is gone, or trapped somewhere
unreachable, when in fact it is sitting in a predictable file a couple of
directories deep. Here is where each major agent actually keeps it, grounded in
how <a href="https://github.com/jazzyalex/agent-sessions">Agent Sessions</a> parses each
one.</p>

<h2 id="claude-code--per-project-jsonl">Claude Code — per-project JSONL</h2>

<p>If you have wondered where Claude Code stores history, the answer is a tree of
JSONL files under your home directory:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~/.claude/projects/&lt;encoded-cwd&gt;/&lt;session-id&gt;.jsonl
</code></pre></div></div>

<p>The clever, occasionally confusing part is the folder name. Claude Code takes
the working directory you launched from and replaces every <code class="language-plaintext highlighter-rouge">/</code> with a <code class="language-plaintext highlighter-rouge">-</code>, so a
session run in <code class="language-plaintext highlighter-rouge">/Users/you/Repository/app</code> lands in a folder named
<code class="language-plaintext highlighter-rouge">-Users-you-Repository-app</code>. One directory per project, one <code class="language-plaintext highlighter-rouge">.jsonl</code> file per
session, named by the session UUID. Agent Sessions also honors the
<code class="language-plaintext highlighter-rouge">CLAUDE_CONFIG_DIR</code> and <code class="language-plaintext highlighter-rouge">CLAUDE_CONFIG_DIRS</code> environment variables and Claude
Desktop’s local-agent-mode roots, because Claude Code will follow those when
they are set.</p>

<p>The format is JSON Lines: one JSON object per line, one line per event. If you
want to read Claude Code JSONL yourself, two details will trip you up. First,
the user’s message text is not at the top level. It is nested inside
<code class="language-plaintext highlighter-rouge">message.content</code>, while Codex and others keep it flatter. Second, there is no
per-event model field. Claude Code records a <code class="language-plaintext highlighter-rouge">version</code> (like <code class="language-plaintext highlighter-rouge">2.0.5</code>), a <code class="language-plaintext highlighter-rouge">cwd</code>,
a <code class="language-plaintext highlighter-rouge">gitBranch</code>, and a <code class="language-plaintext highlighter-rouge">sessionId</code>, but which model answered is simply not written
per turn. Events also thread as a tree through <code class="language-plaintext highlighter-rouge">uuid</code> and <code class="language-plaintext highlighter-rouge">parentUuid</code>, and some
lines are metadata (<code class="language-plaintext highlighter-rouge">summary</code>, <code class="language-plaintext highlighter-rouge">file-history-snapshot</code>, or anything flagged
<code class="language-plaintext highlighter-rouge">isMeta</code>) rather than conversation. Parse for <code class="language-plaintext highlighter-rouge">type == "user"</code> and skip the meta
lines, or your transcript fills with noise.</p>

<h2 id="codex--date-sharded-rollout-files">Codex — date-sharded rollout files</h2>

<p>Codex session files live in a location that is easy to state and easy to get
wrong:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl
</code></pre></div></div>

<p>The gotcha is the environment variable. If <code class="language-plaintext highlighter-rouge">CODEX_HOME</code> is set, that entire tree
moves: the real root becomes <code class="language-plaintext highlighter-rouge">$CODEX_HOME/sessions</code>, and anything hard-coding
<code class="language-plaintext highlighter-rouge">~/.codex/sessions</code> quietly finds nothing. Inside, Codex shards by date into
<code class="language-plaintext highlighter-rouge">YYYY/MM/DD/</code> folders and writes one append-only JSONL file per session, named
<code class="language-plaintext highlighter-rouge">rollout-YYYY-MM-DDThh-mm-ss-&lt;uuid&gt;.jsonl</code>. The timestamp is baked into the
filename, which is why Codex’s own resume picker sorts sessions newest-first by
the name rather than by file mtime.</p>

<p>Two more things worth knowing. Codex is deliberately tolerant of schema drift,
so field names vary between client versions and unknown fields should be
preserved rather than dropped. And when you run with the Responses API under
zero-data-retention or stateless mode, reasoning items come back as opaque
<code class="language-plaintext highlighter-rouge">encrypted_content</code> blobs. They are base64, they are not decryptable locally, and
a viewer should treat them as sensitive and leave them alone. CLI, Desktop, and
VS Code Codex all write into this same rollout corpus, which is convenient: one
location covers three surfaces.</p>

<h2 id="cursor--one-session-two-stores">Cursor — one session, two stores</h2>

<p>Cursor is the odd one out because it splits a single session across two files in
two formats. The readable transcript is JSONL:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~/.cursor/projects/&lt;project&gt;/agent-transcripts/&lt;id&gt;/&lt;id&gt;.jsonl
</code></pre></div></div>

<p>The per-session metadata lives in a small SQLite database next door:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~/.cursor/chats/&lt;md5(project-path)&gt;/&lt;session-id&gt;/store.db
</code></pre></div></div>

<p>That <code class="language-plaintext highlighter-rouge">&lt;md5(project-path)&gt;</code> is exactly what it looks like: the workspace folder is
the MD5 hash of the project path. To assemble complete Cursor Agent history you
read the JSONL for the actual events and the <code class="language-plaintext highlighter-rouge">store.db</code> for the session name,
model hint, timestamps, and workspace context. One without the other gives you
half the picture.</p>

<p>There is an honest boundary here, and it is worth stating plainly. This covers
Cursor Agent transcripts, the ones that produce a JSONL file. Cursor’s older
IDE-chat history that lives only inside the database is stored as protobuf
message blobs, and those are not decoded into transcript events. If a chat never
produced an agent transcript, there is nothing readable to show.</p>

<h2 id="opencode--a-session-database-not-files">OpenCode — a session database, not files</h2>

<p>OpenCode is the cleanest example of the industry drift toward databases. Recent
versions (v1.2 and up) keep the OpenCode session database at a single path:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~/.local/share/opencode/opencode.db
</code></pre></div></div>

<p>Older installs used per-file JSON under
<code class="language-plaintext highlighter-rouge">~/.local/share/opencode/storage/session</code>, and Agent Sessions still falls back to
that when no database is present. But the modern layout is one SQLite file. The
detector prefers it whenever <code class="language-plaintext highlighter-rouge">opencode.db</code> exists and actually contains a
<code class="language-plaintext highlighter-rouge">session</code> table, then reads session metadata, message rows, and — this is the
part people miss — separate <code class="language-plaintext highlighter-rouge">part</code> rows. A single message’s content is spread
across multiple part rows, so reconstructing a turn means joining messages to
their parts and ordering by time. It is a real database schema, not a log you can
<code class="language-plaintext highlighter-rouge">tail</code>.</p>

<h2 id="the-others--copilot-and-hermes">The others — Copilot and Hermes</h2>

<p>Two more worth a mention, because they show the same two patterns.</p>

<p>GitHub Copilot CLI writes JSONL under <code class="language-plaintext highlighter-rouge">~/.copilot/session-state/</code>, and it
recently changed its layout. Legacy installs wrote a flat
<code class="language-plaintext highlighter-rouge">&lt;session-id&gt;.jsonl</code>; current versions (v1.0.11+) write
<code class="language-plaintext highlighter-rouge">&lt;session-id&gt;/events.jsonl</code> inside a per-session directory, with the session name
living in a sibling <code class="language-plaintext highlighter-rouge">workspace.yaml</code>. Same format, moved one level deeper.</p>

<p>Hermes went the OpenCode route. Current versions store everything in
<code class="language-plaintext highlighter-rouge">~/.hermes/state.db</code>, a SQLite database, with a legacy
<code class="language-plaintext highlighter-rouge">~/.hermes/sessions/session_*.json</code> fallback for older installs. Check the
database first; fall back to JSON only when it is absent or empty.</p>

<h2 id="the-whole-map-in-one-table">The whole map, in one table</h2>

<figure class="post-figure">
<style>
.storage-table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; margin: 0 auto; max-width: 720px; }
.storage-table { border-collapse: collapse; width: 100%; font-size: 14px; line-height: 1.45; }
.storage-table th, .storage-table td { text-align: left; vertical-align: top; padding: 8px 12px; border-bottom: 1px solid #d0d7de; }
.storage-table thead th { border-bottom: 2px solid #d0d7de; font-weight: 600; white-space: nowrap; }
.storage-table code { font-size: 12.5px; white-space: nowrap; }
@media (prefers-color-scheme: dark) {
  .storage-table th, .storage-table td { border-bottom-color: #2c2c2e; }
  .storage-table thead th { border-bottom-color: #3a3a3c; }
}
</style>
<div class="storage-table-wrap">
<table class="storage-table">
<thead>
<tr><th>Agent</th><th>Location</th><th>Format</th><th>Notes</th></tr>
</thead>
<tbody>
<tr>
<td>Claude Code</td>
<td><code>~/.claude/projects/&lt;encoded-cwd&gt;/&lt;id&gt;.jsonl</code></td>
<td>JSONL, one event per line</td>
<td>Folder name is the cwd with <code>/</code> turned into <code>-</code>; user text is nested in <code>message.content</code>; no per-event model.</td>
</tr>
<tr>
<td>Codex</td>
<td><code>~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl</code></td>
<td>JSONL rollout, append-only</td>
<td><code>CODEX_HOME</code> relocates the whole tree; timestamp is in the filename; reasoning may be opaque <code>encrypted_content</code>.</td>
</tr>
<tr>
<td>Cursor Agent</td>
<td><code>~/.cursor/projects/.../agent-transcripts/&lt;id&gt;/&lt;id&gt;.jsonl</code> + <code>~/.cursor/chats/&lt;hash&gt;/&lt;id&gt;/store.db</code></td>
<td>JSONL transcript + SQLite metadata</td>
<td>Two stores per session; workspace hash is MD5 of the project path; DB-only IDE chat blobs are protobuf, not decoded.</td>
</tr>
<tr>
<td>OpenCode</td>
<td><code>~/.local/share/opencode/opencode.db</code></td>
<td>SQLite (session / message / part rows)</td>
<td>Moved from per-file JSON to one DB in v1.2; message content is split across <code>part</code> rows.</td>
</tr>
<tr>
<td>Copilot CLI</td>
<td><code>~/.copilot/session-state/&lt;id&gt;/events.jsonl</code></td>
<td>JSONL events</td>
<td>Layout changed from a flat <code>&lt;id&gt;.jsonl</code> to a per-session dir + <code>workspace.yaml</code> in v1.0.11.</td>
</tr>
<tr>
<td>Hermes</td>
<td><code>~/.hermes/state.db</code></td>
<td>SQLite</td>
<td>Current storage is a database; older installs kept <code>sessions/session_*.json</code>.</td>
</tr>
</tbody>
</table>
</div>
<figcaption>The same information every agent writes, filed six different ways. Two patterns dominate: newline-delimited JSON you can read line by line, and SQLite databases you have to query. Cursor manages to use both at once.</figcaption>
</figure>

<h2 id="what-to-take-from-this">What to take from this</h2>

<p>The pattern underneath the mess is a slow migration from flat JSONL to SQLite.
JSONL is trivial to append to and trivial to read one line at a time, which is
why the CLI-first agents started there. Databases give you indexed queries and
atomic writes, which is why OpenCode and Hermes moved. Both are honest choices.
Neither is documented as prominently as it should be, and every one of these
paths has at least one detail that quietly breaks a naive reader:
the encoded folder name, the relocating environment variable, the two-store
split, the message-versus-part rows.</p>

<p>None of this data leaves your machine unless you send it somewhere. It is all
sitting locally, and it is all readable if you know the path and the format.</p>

<p>Agent Sessions reads every one of the locations above into a single searchable
macOS app. It is free, local-only, and has no telemetry; it opens these files
read-only and never writes back into them. If you would rather not memorize six
paths and two schema quirks, <a href="https://github.com/jazzyalex/agent-sessions">the source is on
GitHub</a>, and more posts like this
one live at <a href="/agent-sessions/blog/">/blog/</a>.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[The exact on-disk locations and formats for Claude Code, Codex, Cursor, OpenCode, Copilot, and Hermes session history — real paths, JSONL vs SQLite, and the gotchas.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://jazzyalex.github.io/agent-sessions/assets/marketing/screenshots/agent-sessions-codex-claude-history.png" /><media:content medium="image" url="https://jazzyalex.github.io/agent-sessions/assets/marketing/screenshots/agent-sessions-codex-claude-history.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Projecting the Claude 5-hour limit: burn rate, not percent used</title><link href="https://jazzyalex.github.io/agent-sessions/blog/quota-meter-burn-rate/" rel="alternate" type="text/html" title="Projecting the Claude 5-hour limit: burn rate, not percent used" /><published>2026-07-09T00:00:00-07:00</published><updated>2026-07-09T00:00:00-07:00</updated><id>https://jazzyalex.github.io/agent-sessions/blog/quota-meter-burn-rate</id><content type="html" xml:base="https://jazzyalex.github.io/agent-sessions/blog/quota-meter-burn-rate/"><![CDATA[<p>The Claude 5-hour limit is a rolling window, not a daily allowance. It starts
counting when you start working, resets relative to your own activity rather
than at a fixed hour, and carries weekly caps on top. Codex meters its usage
limit the same way. That mechanism is what makes the standard readout, a static
percentage like “7% used,” such a weak planning tool: whether you reach the
reset before the window fills depends on how fast you’re burning, and a
percentage carries no information about speed. Two accounts can show the
identical number while one of them is twenty minutes from a hard stop.</p>

<p><a href="https://github.com/jazzyalex/agent-sessions">Agent Sessions</a> ships a Quota
Meter with a Session Runway drawer built to answer what the percentage can’t:
how much of the Claude limit is left as a trajectory rather than a level, which
session is spending it, and when the current pace runs the window dry. The math
below is everything that view computes, including the places where the source
data forces some honesty about precision.</p>

<h2 id="what-the-percentage-actually-reports">What the percentage actually reports</h2>

<p>Because both providers meter on rolling windows, the question of when the
Claude limit resets has no fixed daily answer; the reset lands five hours after
the window opened, wherever that happened to fall in your day. A moving target
already argues for tracking a rate. The raw reading argues harder, because it
is coarser than most people assume.</p>

<p>On Claude, Agent Sessions reads usage from the OAuth endpoint, and that payload
reports whole-percent values: <code class="language-plaintext highlighter-rouge">five_hour.utilization</code> and <code class="language-plaintext highlighter-rouge">limits[].percent</code>
move in steps of a full 1%. One percent of a 5-hour window is the finest change
the endpoint will ever show. The app polls every 60 seconds, but the fetch
underneath is cache-first: when the shared usage cache is younger than 180
seconds, the cached payload is served and the live API call is skipped. The
sharing is deliberate, and the second half of this post explains it. The
immediate consequence is that a raw Claude percentage is coarse and slightly
behind reality at the same time.</p>

<p>A perfectly fresh percentage would still be ambiguous, though, because a level
admits opposite explanations. As an illustration: “29% used” at 10:00 and
“29% used” at 10:30 are the same reading, and one can be a quiet morning while
the other is a session that consumed a quarter of the window in ten minutes and
just went idle. Nothing in the number separates those cases, because nothing in
the number involves time.</p>

<figure class="post-figure">
<img src="/agent-sessions/assets/quota-meter-light.png" alt="Agent Sessions Quota Meter showing Codex and Claude 5-hour and weekly percentages, with a Session Runway list below giving each active session's burn rate in quota-minutes per hour" />
<figcaption>The Session Runway, under the raw percentages, lists each active session's burn rate in quota-minutes per hour. That per-session rate is the information the top-line percentage leaves out: it is what says which session is draining the window and how fast.</figcaption>
</figure>

<h2 id="turning-token-logs-into-a-rate">Turning token logs into a rate</h2>

<p>The alternative is arithmetic on data every agent already writes to disk. It
takes three steps.</p>

<p><strong>Per-session tokens per second.</strong> Every agent turn appends its token usage to
the session transcript. Agent Sessions tails those files and computes a recent
tokens-per-second figure for each session. Two adjustments come straight from
how the logs behave:</p>

<ul>
  <li>Cache reads are billed at a steep discount, so the parser weights them at 10%
of their raw count. Without the weighting, a session re-reading a large
context looks like it is torching the quota while it is mostly paying the
cache rate.</li>
  <li>Streaming writes duplicate usage rows that share a message id, so the parser
dedupes on the id. Otherwise every burst would be counted several times.</li>
</ul>

<p>There is also a bootstrap problem, since a measured rate needs two samples
spaced apart. The first completed turn therefore yields a provisional rate from
that turn’s own duration, so a number appears immediately, and a measured burst
rate replaces it once a second sample lands. Provisional rates are additionally
capped at the largest measured rate among peer sessions, because a cache-heavy
first turn can read as tens of thousands of tokens per second and would
otherwise claim nearly the whole attribution split by itself.</p>

<p><strong>From tokens to quota.</strong> Tokens per second is not quota. The account-level
percentage moving over time is the only ground truth for quota burn, so the app
distributes that account-wide rate across active sessions in proportion to
their token rates. Each session ends up with a percent-per-second figure,
displayed as quota minutes per hour. The unit is less exotic than it sounds:
100% of a 5-hour window is 300 quota minutes, so a session showing 40 m/h (an
illustrative number) spends forty of those minutes for every hour it keeps
running.</p>

<p><strong>Projecting run-out.</strong> With a rate in hand, run-out is division: remaining
percent over burn rate. When the projected run-out lands before the reset, the
drawer can warn while there is still time to act. One implementation detail
deserves a warning label, because the obvious version is wrong. When no fresh
account projection exists, the tempting fallback is to assume run-out at the
reset time, which makes the implied rate <code class="language-plaintext highlighter-rouge">remaining / time-to-reset</code>. That
denominator shrinks toward zero as the reset approaches, so the per-session
numbers inflate at exactly the moment a calm reading matters most. Agent
Sessions anchors the fallback to average burn instead: percent used divided by
elapsed window time, with elapsed floored at 10 minutes so a heavy burst right
after a reset cannot inflate the early-window side by the mirror-image
mechanism. The average-burn rate never blows up, stays conservative by
construction, and hands over to measured velocity whenever a fresher projection
arrives.</p>

<h2 id="coarse-by-design-on-claude">Coarse by design on Claude</h2>

<p>The two providers do not give out equal data, and the app does not pretend
they do.</p>

<p>Codex writes fine-grained rate-limit samples into its own session logs and
serves account state over local CLI-RPC, updated frequently. Per-session burn
can be read directly, projections form readily, and a “fresh” indicator on
Codex genuinely means fresh. When the question is how fast you are approaching
the Codex usage limit, the transport supports a live answer.</p>

<p>Claude offers no equivalent. The only account signal is the OAuth usage
endpoint, and Agent Sessions reads it through a cache file shared with the
Claude Code statusline (<code class="language-plaintext highlighter-rouge">/tmp/claude/statusline-usage-cache.json</code>). A running
Claude Code session keeps that file warm, and the app re-serves it rather than
issuing its own request whenever the file is younger than 180 seconds. That is
cooperation rather than laziness: polling the endpoint aggressively earns a 429
with a retry clamp of about five minutes, and independent polling would compete
with the statusline for the same budget. The cost of cooperating is that
Claude’s projection extras, the live ETA badge and the sharpening from average
burn to measured velocity, form far less often than they do for Codex. When the
underlying number has not moved, the app says it is waiting for a usable
sample. Inventing a trajectory from a flat number would produce a reading that
is confident, precise-looking, and made up.</p>

<p>The burn measurement survives all of this on purpose. Per-session burn comes
from token attribution in the transcripts, which the app reads directly and
continuously, so the Session Runway bars stay live on Claude regardless of how
stale the account projection is. An earlier version gated the burn display on a
fresh projection, and that gate was removed for Claude precisely because it
made the bars appear late and flicker. What degrades on Claude is projection
polish; the answer about which session is eating the window, and how fast, does
not degrade at all, and that answer is the reason the drawer exists.</p>

<h2 id="what-the-drawer-shows">What the drawer shows</h2>

<p>A session that is burning but projected to fit inside the window gets a small
smiling face in its run-out column, with a quieter dot available in Preferences
for anyone who wants less personality. A session on track to run past the reset
carries its projected run-out time and rises up the ranking. Freshness is
enforced per row as well: when the latest token sample is older than 30
seconds, the rate falls back to a waiting state rather than showing a stale
figure, so a stopped session clears quickly. By default the drawer appears only
when the runway is actually running low; it can also be pinned always on or
always off.</p>

<p>The practical difference, again with illustrative numbers: instead of
“7% used,” the readout becomes “this session spends 40 quota minutes per hour,
it is the main drain on the window, and at this pace it runs out 25 minutes
before the reset.” That sentence supports a decision. Pause the expensive
session, or let the small ones ride to the reset.</p>

<h2 id="try-it">Try it</h2>

<p>Agent Sessions is a free, local-only macOS app for browsing and searching
Claude Code and Codex sessions; the Quota Meter and Session Runway ship with
it. There is no account and no telemetry, and it reads the same local logs your
agents already write. The code behind everything described above is public:
<a href="https://github.com/jazzyalex/agent-sessions">download it or read the source on GitHub</a>.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[A '% used' number can't say when the Claude 5-hour limit will stop you. The burn-rate math behind projecting run-out for Claude and Codex usage limits.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://jazzyalex.github.io/agent-sessions/assets/marketing/screenshots/agent-sessions-codex-claude-history.png" /><media:content medium="image" url="https://jazzyalex.github.io/agent-sessions/assets/marketing/screenshots/agent-sessions-codex-claude-history.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>