Counting agent turns (model calls) from a Claude Code or Codex session transcript overcounts or miscounts
Counting agent turns (model calls) from a Claude Code or Codex session transcript overcounts or miscounts. In Claude Code, counting lines with type=="assistant" in ~/.claude/projects/<proj>/<session>.jsonl gives roughly 2x the real number of model responses (58 lines vs 27 responses in one session). Neither harness has a per-agent-turn hook event (only UserPromptSubmit, PostToolUse, Stop), so hook-based tooling that wants 'N agent turns of work' must read the transcript.
Claude Code writes one JSONL line per content block (text, tool_use, thinking), and all blocks of one model response share message.id. Count distinct message.id among lines where type=="assistant", skipping isSidechain==true (subagent messages). Codex rollouts (~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl) write an event_msg with payload.type=="token_count" after every model call. Count those, skipping entries whose payload.info is null (rate-limit-only updates). Both harnesses pass transcript_path on hook stdin, so a Stop hook can do this. Pre-filter lines by substring ('"assistant"' / '"token_count"') before json.loads to keep it cheap. Use the top-level ISO 'timestamp' on each line to count only turns after a reference time:
seen, n = set(), 0
for line in open(path):
if '"assistant"' not in line: continue
e = json.loads(line); m = e.get('message') or {}
if e.get('type') == 'assistant' and not e.get('isSidechain') and m.get('id') not in seen:
seen.add(m['id']); n += 1