Session JSONL bloat: repeated identical AGENTS-derived user_instructions per turn (turn_context)

Resolved 💬 8 comments Opened Feb 2, 2026 by baba20o Closed Feb 2, 2026
💡 Likely answer: A maintainer (etraut-openai, contributor) responded on this thread — see the highlighted reply below.

Summary

Codex CLI persists turn_context.payload.user_instructions (derived from AGENTS.md) verbatim on every turn even when unchanged. In long-lived sessions (Discord-first adapters, “keep the same session open all day”), this creates significant JSONL session bloat and makes token/cost analysis confusing (Codex also emits many duplicated token snapshots).

This is not about whether the model needs instructions each request (it does). It’s about logging/persisting identical instruction blobs thousands of times.

Environment

  • codex-cli 0.92.0
  • OS: WSL2 Ubuntu 22.04.5 LTS (Linux 5.15.* microsoft-standard-WSL2)
  • Repo root: /mnt/c/Projects/Co-deworker
  • Root AGENTS.md size: 6,328 bytes

Observed impact (real session)

Session file:

  • ~/.codex/sessions/2026/01/29/rollout-2026-01-29T16-17-00-019c0b9d-dc7a-7863-a19f-80d81bbdf62e.jsonl
  • Size: 37,210,490 bytes (~35.5MB)

Measured bloat:

  • turn_context.payload.user_instructions
  • Entries: 1,759
  • Unique blobs: 1 (identical hash)
  • Total bytes (sum across entries): 16,001,623 bytes (~16MB)
  • Single blob size: ~9,047 chars
  • “AGENTS.md instructions…” user messages (the wrapped # AGENTS.md instructions for … payload)
  • User messages total: 384 (~919,792 bytes)
  • Messages starting with # AGENTS.md instructions for: 96 (~881,856 bytes; 95.9% of user-message bytes)
  • Unique blobs: 1

(We also saw many token snapshot events with identical values repeated, which can cause naive log tools to massively overcount if they sum snapshots.)

Repro (minimal)

  1. Create a repo with a root AGENTS.md.
  2. Run codex in that repo and do multiple turns.
  3. Inspect ~/.codex/sessions/<yyyy>/<mm>/<dd>/rollout-*.jsonl.
  4. Observe that turn_context.payload.user_instructions repeats verbatim even when unchanged.

A quick uniqueness check for turn_context.user_instructions:

python3 - <<'PY'
import json, hashlib
from pathlib import Path
p = Path.home()/'.codex/sessions/2026/01/29/rollout-2026-01-29T16-17-00-019c0b9d-dc7a-7863-a19f-80d81bbdf62e.jsonl'
counts={}
entries=0
bytes_total=0
with p.open('r',encoding='utf-8') as f:
  for line in f:
    try: e=json.loads(line)
    except: continue
    if e.get('type')=='turn_context':
      ui=(e.get('payload') or {}).get('user_instructions')
      if isinstance(ui,str) and ui:
        h=hashlib.sha1(ui.encode('utf-8','replace')).hexdigest()
        counts[h]=counts.get(h,0)+1
        entries+=1
        bytes_total += len(ui.encode('utf-8','replace'))
print('entries', entries)
print('unique', len(counts))
print('top_count', max(counts.values()) if counts else 0)
print('total_bytes', bytes_total)
PY

Expected behavior

  • It’s fine for Codex to use the instructions every request.
  • But session logging should not persist the identical user_instructions blob on every turn.

Prefer one of:

  • Log full instructions once per session (session header/meta) and reference by hash/id.
  • Only log user_instructions when changed.
  • Only log at compaction boundaries.
  • Or store hash+len on each turn_context and the full blob in a separate content-addressed record.

Actual behavior

  • turn_context.payload.user_instructions repeats verbatim across turns even when unchanged.

Relevant code pointers

Codex builds the “AGENTS.md instructions…” user-instructions message here:

  • codex-rs/core/src/instructions/user_instructions.rs (USER_INSTRUCTIONS_PREFIX = "# AGENTS.md instructions for ")

User instructions appear loaded at session spawn:

  • codex-rs/core/src/codex.rs (calls get_user_instructions(...) during spawn)

Suggested fix

Add session-writer deduplication for turn_context.user_instructions:

  • Track last_user_instructions_hash in memory; only include user_instructions in the JSONL when it changes.
  • Alternatively, promote it to a single session-meta record and reference it thereafter.

Optionally add a config knob:

  • log_user_instructions = "always|on_change|never" (default on_change)

Why this matters

  • Long-lived sessions (Discord/Slack adapters) quickly generate large JSONLs dominated by repeated instructions.
  • Makes session analysis slow and can mislead cost dashboards/tools.

View original on GitHub ↗

8 Comments

etraut-openai contributor · 5 months ago

Can you describe your use case in more detail? Are you reading the on-disk JSONL rollout files for analysis? That's not really what these were designed for. There are other mechanisms that you may find more appropriate for this — for example, using the app server event stream or Otel event stream.

baba20o · 5 months ago

Session rollout bloat: repeated TurnContext.user_instructions

We saw very large Codex session JSONL growth driven by the same turn_context.user_instructions being written on every RolloutItem::TurnContext line.

Observed on one real rollout JSONL:

  • turn_context.user_instructions: 1,759 entries, 1 unique, ~16.0MB total
  • Per-blob size ~9KB (dominated by repeated AGENTS / user-instructions content)

This makes rollouts noisy and (when people post/log/ingest them) can lead to misleading input-token accounting.

Proposed fix (low-risk): dedupe repeated user_instructions on write

In the rollout recorder/writer, keep a last_user_instructions (string or hash). When persisting a RolloutItem::TurnContext, only write user_instructions when it changes. Otherwise set it to None so it is omitted from JSON (skip_serializing_if = Option::is_none).

Sketch (Rust):

// in rollout_writer(...)
let mut last_user_instructions: Option<String> = None;

...
if let RolloutItem::TurnContext(turn) = &mut item {
    if turn.user_instructions.is_some() {
        if last_user_instructions.as_ref() == turn.user_instructions.as_ref() {
            turn.user_instructions = None;
        } else {
            last_user_instructions = turn.user_instructions.clone();
        }
    }
}

Why it should be safe

  • TurnContextItem.user_instructions is already optional and skipped when None
  • reconstruct_history_from_rollout(...) does not replay RolloutItem::TurnContext lines (only response items / compacted / events), so removing redundant fields from those lines should not affect replay

Optional: config knob

log_user_instructions = "always|on_change|never" (default on_change).

If helpful, I can provide a clean patch against current main once invited (per CONTRIBUTING).

baba20o · 5 months ago
Can you describe your use case in more detail? Are you reading the on-disk JSONL rollout files for analysis? That's not really what these were designed for. There are other mechanisms that you may find more appropriate for this — for example, using the app server event stream or Otel event stream.

Thanks — totally fair question.

Our use case (why we read the rollout JSONL):

  • We run Codex CLI headless as part of a Discord-first orchestrator (multiple backtest hosts, long sessions).
  • After a run, we want an offline, local postmortem artifact we can analyze without needing the live app-server stream / OTEL collector.
  • We do not feed the raw JSONL back into the model; we parse it locally to generate a compact “run report” (counts, tool output sizes, errors, cost/tokens sanity checks, etc.).
  • The JSONL is currently the only durable per-session artifact we can reliably grab from ephemeral machines and archive.

Why the TurnContext.user_instructions repetition matters for us anyway:

  • In one real rollout, turn_context.user_instructions was written 1,759 times with 1 unique value (~16MB). That’s unnecessary disk growth and makes local inspection (jq, our own tool) noisy/slow.
  • It also leads to misleading accounting if someone naively sums “input tokens” from repeated snapshots (we hit this internally).

We’re open to switching mechanisms
If there’s a supported/recommended way to export a compact, canonical session history from the new sqlite/state DB or via an event stream after the fact, we’d happily adopt that. But until then, the rollout JSONL is what we have for postmortems.

Ask: would you consider either:
1) Dedupe TurnContext.user_instructions in the rollout writer (only emit on change), or
2) Move static-ish instructions into SessionMeta (written once) and omit from later turn contexts?

Both keep the rollout useful for debugging while preventing ballooning on long sessions.

etraut-openai contributor · 5 months ago

Thanks for the additional details. Like I mentioned, you're using the JSONLs in a manner that isn't intended, so you're likely to see various impedance mismatches. We're unlikely to make changes specifically to accommodate your use case. The JSONL files are more of an internal implementation detail. It's quite likely that they will change in the future, so I don't recommend building features that depend on their format or structure.

baba20o · 5 months ago

Totally understood re: the rollout JSONL being an internal detail — we’ll treat it as best-effort and won’t build hard deps on the schema.

One clarification though: our primary pain isn’t only disk bloat from the JSONL, it’s API-side input token bloat that appears to correlate with the same “project instructions / AGENTS.md” blob being included repeatedly in the model prompt.

In our sessions, that blob is large and very stable, and we see it echoed as turn_context.user_instructions over and over. If that same content is being resent to the model each turn, it’s a big driver of billed input tokens.

Two questions so we can align with the intended surface:
1) Is Codex expected to resend the full project/user instructions on every model request (stateless prompt), or is there a supported way to avoid re-sending / reference it once per session?
2) If it must be resent, does Codex support OpenAI prompt-caching / “cached input tokens” so repeated stable prefixes (AGENTS + base instructions) don’t repeatedly incur full input cost? If yes, where is that configured / enabled?

If the answer is essentially “keep AGENTS small”, that’s fine — we’ll shrink it. But right now the spike we’re investigating looks like the repeated instruction blob is dominating prompt size.

Separately, if there’s a supported way to export a compact postmortem artifact from the app-server/OTel/sqlite store after the fact, we’d love pointers; that would let us stop touching the JSONL entirely.

etraut-openai contributor · 5 months ago

We recently posted a blog post that should answer your question.

If you'd like to see new functionality, feel free to open a feature request. It's most useful if you can explain the problem in a general sense. It's also fine to propose solutions, but a problem statement will give us the freedom to consider alternative approaches. We generally prioritize feature requests based on community upvotes.

baba20o · 5 months ago

Thanks — read the post and it helps (stateless agent loop + preserving prefix to maximize prompt caching).

Two practical questions so we can align with the intended surfaces:
1) Is there a supported way to observe caching effectiveness (e.g. cached vs uncached input tokens, cache hit rate) from Codex CLI logs / events / OTEL?
2) For offline postmortems on ephemeral hosts, what’s the recommended way to persist the app-server event stream or OTEL stream to disk (so we’re not touching internal JSONLs)? Any flags / config pointers would be hugely appreciated.

We’re going to open a feature request with a general problem statement (offline export + cache visibility) so others can upvote it.

etraut-openai contributor · 5 months ago

Many enterprises use network-based LLM proxies like LiteLLM for the sort of monitoring and analysis that you're describing here.

OTEL support does include token stats as part of the response.completed event. Fields include input_token_count and cached_token_count`. Refer to this documentation for some details. If you have more detailed questions, you can clone the sources and ask Codex questions about its implementation.