The ⌘L log showed Codex '0 turns' on a session that had clearly been working
Codex panes opened the ⌘L conversation log as 'Codex · 0 turns / No conversation yet', and Claude turns were truncated mid-sentence. Root cause: an exactly-matched rollout (the open file descriptor of the pane's Codex process) was then re-filtered by launch time, which subtracted every message to zero; and a human-facing viewer reused an 8k LLM-payload char cap. Fixed by not double-filtering, a non-destructive filter, and a generous cap.
The founder, mid-session: "코덱스가 특히 병신이야" — a screenshot of the ⌘L conversation log
reading Codex · 0 turns and "No conversation yet — give this session a turn." on a pane
that had visibly been working. Sometimes it showed a previous session's chat instead. Claude's
log worked but cut long turns with … (truncated — N chars).
The parser was innocent
First instinct: the Codex rollout format changed and the parser produces nothing. So I replayed a
real rollout (~/.codex/sessions/.../rollout-*.jsonl) through the parser's logic. It extracted
646 user + 6337 agent messages — Korean content and all. The parser was fine. So the emptiness
came from upstream.
Two filters fighting over the same fact
The pane→rollout match is good: pane_codex_rollout_path finds the rollout file descriptor the
pane's Codex process has open (via lsof). That's an exact identity — this pane, this file.
But the panel then also passed sinceMs = agentStartedAt (when the pane launched Codex), and the
parser did messages.retain(|m| m.ts >= since). When the launch timestamp sat a hair after the
rollout's own timestamps — clock skew, or a codex resumed session whose turns predate the
relaunch — the filter dropped every message. An exactly-matched file, filtered to zero. That's
the "0 turns" on a busy session.
The truncation was simpler: both read_transcript (Claude) and parse_codex_transcript (Codex)
capped each merged turn at MSG_CAP = 8000 chars — a cap that makes sense for an LLM payload, not
for a human reading their own conversation history.
The fix
- Don't double-filter an exact match. When the rollout came from the open fd, set
sinceMs = undefined. The file is already pinpointed to this pane; a second fuzzy launch-time filter can only subtract. - Make the filter non-destructive. If the
sincefilter would empty the list, keep the full session — a wrong-time view beats a blank one. Test-locked. - Raise the cap to 60k chars (both providers). The ⌘L panel already lazy-mounts only N turns, so a generous cap costs nothing at render time.
cargo test 6/6, tsc clean.
The lesson
Don't filter the same identity signal twice. Once a record is matched exactly — here, an open
file descriptor — a second fuzzy filter (a launch-time window) can only remove things, and in the
worst case removes all of them. Match exactly, or filter fuzzily; never both on the same record.
And any destructive retain behind a view should refuse to render nothing when it had something.