- Observed: bmad-loop 0.11.1 (installed via uv tool; source clone identical on the cited files), Windows 11, psmux backend, Codex CLI 0.153.4, profile
codex (usage_parser = "codex-rollout", hooks dialect codex-hooks-json, .codex/hooks.json relaying SessionStart + Stop to bmad_loop_hook.py). Run 20260909-144751-8e8b, review stage on [adapter.review] name = "codex", model gpt-6-astra, 38-minute session that completed normally via the Stop hook. Journal: {"kind": "session-end", "task_id": "TASK-2831-review-1", "status": "completed", "tokens": null, "tokens_weighted": null} — while the dev stage (claude) on the same run reported tokens: 19777795, tokens_weighted: 2460930. The task dir tasks/TASK-2831-review-1/ holds only heartbeat.json + prompt.txt (no messages.json).
- The rollout files for that session exist and DO carry usage:
~/.codex/sessions/2026/09/09/rollout-2026-09-09T15-01-52-01a08530-0657-7ba1-91dd-8dd32958cdec.jsonl (39 token_count events) and rollout-2026-09-09T15-02-26-01a08530-8bb7-7910-afd1-5cab56cd8bef.jsonl (18; the session spawned a subagent) — payload.type == "token_count" with payload.info.total_token_usage.*, exactly what tally_codex_rollout reads.
- Code path:
src/bmad_loop/adapters/generic.py:1252-1254 — read_usage() does if not result.transcript_path: return None before tally_usage(self.profile.usage_parser, path) (:1262). src/bmad_loop/signals.py:113 fills transcript_path=data.get("transcript_path") from the hook payload. Codex's Stop hook payload carries no transcript_path (it is a Claude Code field), so for the codex profile the declared usage_parser is unreachable in a real run — every codex session logs tokens: null, and the [limits] token budget / cost ledgers see 0 for Codex.
- Why
bmad-loop probe-adapter codex --probe reports usage fine: probe mode discovers the rollout itself (probe.py ~line 790, glob ~/.codex/sessions/*/*/*/rollout-*.jsonl → infer_token_schema()), bypassing read_usage(); the same-day probe on this machine returned input_tokens: 2942, cache_read_tokens: 12928, output_tokens: 5 from rollout-2026-09-09T14-46-06-01a08521-99b8-72a0-b98b-156a48d74ca2.jsonl. The probe's captured Stop payload has no transcript_path key either — the JSON report keys are binary, binary_found, captured_events, cli, declared_events, dialect, help, hooks_registered, known_profile, mode, next_steps, schema_version, tokens, transcript, usage_parser, version, warnings, with transcript.location coming from the glob.
- Proposed fix (either): (a) when the profile's usage parser is
codex-rollout and the payload has no transcript_path, resolve the rollout from the Stop payload's session/thread id — rollout filenames end with the session UUID (rollout-<timestamp>-<session_id>.jsonl), so ~/.codex/sessions/**/rollout-*-<session_id>.jsonl is deterministic; sum usage across the subagent rollouts spawned by that session if they can be attributed; or (b) reuse the probe's discovery (newest rollout whose payload.cwd == the unit's cwd and whose first timestamp ≥ session start) as a fallback inside read_usage(), behind the profile (transcript_discovery = "codex-sessions-glob"). Add a zero-token test with a fixture rollout + a Stop payload lacking transcript_path that asserts usage is populated (ablation: remove the fallback → the test must fail).
- Also worth a line in
docs/adapter-authoring-guide.md: usage_parser on a profile is only reached when the CLI's Stop payload supplies transcript_path.
Source
found-in: bmad-loop 0.11.1 (uv tool) + source clone E:\vibe-coding\bmad-loop (main), Windows 11 · session: Claude Code 4d0f4496 (2026-09-09) · while: first color-dev-loop run with the review stage on Codex (customthing/ai-pod-auto PR #766) · evidence: journal session-end tokens null vs dev stage populated; rollout files with 39/18 token_count events; generic.py:1252-1254, signals.py:113, probe.py glob discovery
codex(usage_parser = "codex-rollout", hooks dialectcodex-hooks-json,.codex/hooks.jsonrelaying SessionStart + Stop tobmad_loop_hook.py). Run20260909-144751-8e8b, review stage on[adapter.review] name = "codex", model gpt-6-astra, 38-minute session that completed normally via the Stop hook. Journal:{"kind": "session-end", "task_id": "TASK-2831-review-1", "status": "completed", "tokens": null, "tokens_weighted": null}— while the dev stage (claude) on the same run reportedtokens: 19777795, tokens_weighted: 2460930. The task dirtasks/TASK-2831-review-1/holds onlyheartbeat.json+prompt.txt(nomessages.json).~/.codex/sessions/2026/09/09/rollout-2026-09-09T15-01-52-01a08530-0657-7ba1-91dd-8dd32958cdec.jsonl(39token_countevents) androllout-2026-09-09T15-02-26-01a08530-8bb7-7910-afd1-5cab56cd8bef.jsonl(18; the session spawned a subagent) —payload.type == "token_count"withpayload.info.total_token_usage.*, exactly whattally_codex_rolloutreads.src/bmad_loop/adapters/generic.py:1252-1254—read_usage()doesif not result.transcript_path: return Nonebeforetally_usage(self.profile.usage_parser, path)(:1262).src/bmad_loop/signals.py:113fillstranscript_path=data.get("transcript_path")from the hook payload. Codex's Stop hook payload carries notranscript_path(it is a Claude Code field), so for the codex profile the declaredusage_parseris unreachable in a real run — every codex session logstokens: null, and the[limits]token budget / cost ledgers see 0 for Codex.bmad-loop probe-adapter codex --probereports usage fine: probe mode discovers the rollout itself (probe.py~line 790, glob~/.codex/sessions/*/*/*/rollout-*.jsonl→infer_token_schema()), bypassingread_usage(); the same-day probe on this machine returnedinput_tokens: 2942, cache_read_tokens: 12928, output_tokens: 5fromrollout-2026-09-09T14-46-06-01a08521-99b8-72a0-b98b-156a48d74ca2.jsonl. The probe's captured Stop payload has notranscript_pathkey either — the JSON report keys arebinary, binary_found, captured_events, cli, declared_events, dialect, help, hooks_registered, known_profile, mode, next_steps, schema_version, tokens, transcript, usage_parser, version, warnings, withtranscript.locationcoming from the glob.codex-rolloutand the payload has notranscript_path, resolve the rollout from the Stop payload's session/thread id — rollout filenames end with the session UUID (rollout-<timestamp>-<session_id>.jsonl), so~/.codex/sessions/**/rollout-*-<session_id>.jsonlis deterministic; sum usage across the subagent rollouts spawned by that session if they can be attributed; or (b) reuse the probe's discovery (newest rollout whosepayload.cwd== the unit's cwd and whose first timestamp ≥ session start) as a fallback insideread_usage(), behind the profile (transcript_discovery = "codex-sessions-glob"). Add a zero-token test with a fixture rollout + a Stop payload lackingtranscript_paththat asserts usage is populated (ablation: remove the fallback → the test must fail).docs/adapter-authoring-guide.md:usage_parseron a profile is only reached when the CLI's Stop payload suppliestranscript_path.Source
found-in: bmad-loop 0.11.1 (uv tool) + source clone E:\vibe-coding\bmad-loop (main), Windows 11 · session: Claude Code 4d0f4496 (2026-09-09) · while: first color-dev-loop run with the review stage on Codex (customthing/ai-pod-auto PR #766) · evidence: journal session-end tokens null vs dev stage populated; rollout files with 39/18 token_count events; generic.py:1252-1254, signals.py:113, probe.py glob discovery