Codex ≥0.148 rollouts parse as responses: 0 — every Codex session since 2026-08-21 is silently dropped from usage/dashboard
Summary
Codex CLI changed its rollout transcript schema: the per-turn event_msg payloads
(agent_message, user_message, mcp_tool_call_end, patch_apply_end, web_search_end)
were consolidated into a single item_completed envelope carrying a typed item.
parseCodexRollout only recognises the old payload names, so every new rollout yields
responses: 0 and prompts: 0. aggregate() then discards the session wholesale:
// src/lib/usage-index.mjs
if (!rec || !rec.responses) continue; // no assistant turn → not a session
The result is that whole days of Codex work disappear from ak dashboard, with no error
and no health warning.
Impact
On this machine, 2026-08-22 vanished from /api/usage byDay entirely — every session
that day was Codex, and all 12 were dropped. 2026-08-23 shows sessions: 0 with only a
Claude Code session's tokens surviving.
item_completed does not appear anywhere in the 4.0.0-alpha.42 source tree, so no
version currently handles this.
The regression is dated and accelerating
Codex rollout-*.jsonl entries with responses == 0, bucketed by session end-day:
| Day |
parsed OK |
dropped |
| Aug 08 – Aug 20 |
all |
0 |
| 2026-08-21 |
8 |
5 |
| 2026-08-22 |
3 |
20 |
| 2026-08-23 |
5 |
51 |
Clean cutover starting Aug 21, consistent with a Codex CLI auto-update rolling out.
Schema diff
Same histogram of event_msg → payload.type, from a pre-cutover rollout vs a current one:
payload.type |
before |
after |
agent_message |
24 |
0 |
user_message |
4 |
0 |
mcp_tool_call_end |
4 |
0 |
patch_apply_end |
7 |
0 |
web_search_end |
3 |
0 |
item_completed |
0 |
1961 |
token_count |
139 |
937 |
task_started / task_complete |
4 / 3 |
4 / 3 |
Note token_count is unchanged — token accounting still parses fine. Only the turn
counting broke, which is why the failure is silent rather than obvious.
New envelope
item.type values observed in one session (7,267 items):
Reasoning 3615 CollabAgentToolCall 87
CommandExecution 2358 ContextCompaction 27
FileChange 799 Extension 13
AgentMessage 216 UserMessage 5
SubAgentActivity 145 DynamicToolCall 2
The two that matter
⚠️ The content-block discriminator is "Text" for AgentMessage but "text" for
UserMessage. Easy to miss when writing the extractor.
Reproduction
Against any rollout under ~/.codex/sessions/2026/08/2[1-3]/:
const fs = require("fs");
let agent = 0, user = 0, item = 0;
for (const l of fs.readFileSync(process.argv[1], "utf8").split("\n")) {
if (!l) continue;
let o; try { o = JSON.parse(l) } catch { continue }
if (o.type !== "event_msg") continue;
const p = o.payload || {};
if (p.type === "agent_message") agent++;
if (p.type === "user_message") user++;
if (p.type === "item_completed") item++;
}
console.log({ agent, user, item });
Output on a current rollout:
{ agent: 0, user: 0, item: 7267 }
=> ak computes responses=0, prompts=0 -> session DROPPED
Suggested fix
In the event_msg loop in parseCodexRollout (src/lib/usage-index.mjs, around the
existing agent_message / user_message branches), add an item_completed branch that
maps the new item types onto the existing counters:
item.type |
maps to existing handling |
AgentMessage |
agent_message (rec.responses++, punchcard, assistant turn) |
UserMessage |
user_message (rec.prompts++, prompt turn) |
CommandExecution |
patch_apply_end / tool-call family |
FileChange |
patch_apply_end |
DynamicToolCall, Extension, CollabAgentToolCall |
mcp_tool_call_end |
Reasoning, SubAgentActivity, ContextCompaction |
no counter (skip) |
Keeping both code paths would let one parser handle pre- and post-cutover rollouts, which
matters because existing usage-index.json caches hold a mix. This likely also needs a
CACHE_VERSION bump so already-cached (wrongly zeroed) Codex records get re-parsed rather
than served from cache.
Secondary: sourceHealth reports ok while dropping everything
/api/usage returns:
"sourceHealth": { "codex": { "status": "ok", "reason": null } }
…while discarding 51 of 56 Codex files that day. The check appears to verify the source is
readable, not that parsing produced anything. A parse-yield signal (files scanned vs
sessions produced, or a warning when a source yields 0 sessions from N non-empty files)
would have surfaced this immediately instead of looking like "I just didn't work much."
Possibly separate: sessions counted only on their first token-day
Independent of the above, and arguably by design — flagging in case it's worth splitting out.
// src/lib/usage-index.mjs
_day: firstDay, // "The day this session's tokens FIRST landed on"
…
if (s._day && byDay[s._day]) byDay[s._day].sessions++;
A session spanning several days is counted once, on its first day. A 3-day session shows up
as byDay["<day 3>"] = { sessions: 0, tokens: 14574533 } — nonzero tokens, zero sessions.
The comment explains the intent (sum(byDay.sessions) === totals.sessions), but the effect
is that long-running sessions are invisible on every day after their first, which compounds
the symptom above. A separate sessionsActive counter alongside sessions would preserve
the invariant while making recent days readable.
Environment
ak 4.0.0-alpha.42 (newest published — dist-tags: next=4.0.0-alpha.42, latest=4.0.0-alpha.0)
codex-cli 0.148.0
node v26.7.0
macOS 26.6.2 (arm64)
cache ~/.config/agentic-kit/usage-index.json — schemaVersion 9, 274 entries
Cache staleness ruled out: only 4 of 274 entries drift from their on-disk (mtime, size),
all live files mid-write. The index refreshes correctly; the records it writes are wrong.
🤖 Investigated and drafted with Claude Code
Codex ≥0.148 rollouts parse as
responses: 0— every Codex session since 2026-08-21 is silently dropped from usage/dashboardSummary
Codex CLI changed its rollout transcript schema: the per-turn
event_msgpayloads(
agent_message,user_message,mcp_tool_call_end,patch_apply_end,web_search_end)were consolidated into a single
item_completedenvelope carrying a typeditem.parseCodexRolloutonly recognises the old payload names, so every new rollout yieldsresponses: 0andprompts: 0.aggregate()then discards the session wholesale:The result is that whole days of Codex work disappear from
ak dashboard, with no errorand no health warning.
Impact
On this machine, 2026-08-22 vanished from
/api/usagebyDayentirely — every sessionthat day was Codex, and all 12 were dropped. 2026-08-23 shows
sessions: 0with only aClaude Code session's tokens surviving.
item_completeddoes not appear anywhere in the4.0.0-alpha.42source tree, so noversion currently handles this.
The regression is dated and accelerating
Codex
rollout-*.jsonlentries withresponses == 0, bucketed by session end-day:Clean cutover starting Aug 21, consistent with a Codex CLI auto-update rolling out.
Schema diff
Same histogram of
event_msg→payload.type, from a pre-cutover rollout vs a current one:payload.typeagent_messageuser_messagemcp_tool_call_endpatch_apply_endweb_search_enditem_completedtoken_counttask_started/task_completeNote
token_countis unchanged — token accounting still parses fine. Only the turncounting broke, which is why the failure is silent rather than obvious.
New envelope
{ "timestamp": "...", "ordinal": 1234, // new top-level field "type": "event_msg", "payload": { "type": "item_completed", "item": { "type": "AgentMessage", ... } // PascalCase, NOT item_type } }item.typevalues observed in one session (7,267 items):The two that matter
"Text"for AgentMessage but"text"forUserMessage. Easy to miss when writing the extractor.
Reproduction
Against any rollout under
~/.codex/sessions/2026/08/2[1-3]/:Output on a current rollout:
Suggested fix
In the
event_msgloop inparseCodexRollout(src/lib/usage-index.mjs, around theexisting
agent_message/user_messagebranches), add anitem_completedbranch thatmaps the new item types onto the existing counters:
item.typeAgentMessageagent_message(rec.responses++, punchcard, assistant turn)UserMessageuser_message(rec.prompts++, prompt turn)CommandExecutionpatch_apply_end/ tool-call familyFileChangepatch_apply_endDynamicToolCall,Extension,CollabAgentToolCallmcp_tool_call_endReasoning,SubAgentActivity,ContextCompactionKeeping both code paths would let one parser handle pre- and post-cutover rollouts, which
matters because existing
usage-index.jsoncaches hold a mix. This likely also needs aCACHE_VERSIONbump so already-cached (wrongly zeroed) Codex records get re-parsed ratherthan served from cache.
Secondary:
sourceHealthreportsokwhile dropping everything/api/usagereturns:…while discarding 51 of 56 Codex files that day. The check appears to verify the source is
readable, not that parsing produced anything. A parse-yield signal (files scanned vs
sessions produced, or a warning when a source yields 0 sessions from N non-empty files)
would have surfaced this immediately instead of looking like "I just didn't work much."
Possibly separate: sessions counted only on their first token-day
Independent of the above, and arguably by design — flagging in case it's worth splitting out.
A session spanning several days is counted once, on its first day. A 3-day session shows up
as
byDay["<day 3>"] = { sessions: 0, tokens: 14574533 }— nonzero tokens, zero sessions.The comment explains the intent (
sum(byDay.sessions) === totals.sessions), but the effectis that long-running sessions are invisible on every day after their first, which compounds
the symptom above. A separate
sessionsActivecounter alongsidesessionswould preservethe invariant while making recent days readable.
Environment
Cache staleness ruled out: only 4 of 274 entries drift from their on-disk
(mtime, size),all live files mid-write. The index refreshes correctly; the records it writes are wrong.
🤖 Investigated and drafted with Claude Code