유대선
프로젝트로
·트러블슈팅·2

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

  1. 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.
  2. Make the filter non-destructive. If the since filter would empty the list, keep the full session — a wrong-time view beats a blank one. Test-locked.
  3. 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.