fix(hooks): find subagents in both layouts, tolerate non-string timestamps - #247
Merged
Conversation
…tamps The subagent directory was derived by stripping the transcript's extension, which only locates it in the flat layout. Under the nested layout the path resolves to a directory that does not exist, the isdir guard returns an empty list without logging, and every subagent token is dropped. It is now looked up in both positions, and the listing recurses. Ordering an entry against the turn's prompt compared a raw timestamp with a string. A numeric one raises TypeError past the JSONDecodeError handler, reaching the bare except and discarding usage already read for the turn. Comparisons go through a helper that orders strings only. Where an entry cannot be ordered, the two paths differ on purpose: the codex usage aggregation counts it toward the turn, because the alternative is losing the whole turn to estimation, while the scoping filters drop it, because including it would re-fold it on every later Stop. Coercing with str() was rejected: a numeric timestamp compared lexically against an ISO string mis-scopes the turn silently, which is worse than the crash it would replace.
vigneshsubbiah16
left a comment
Collaborator
There was a problem hiding this comment.
Failed to authenticate: OAuth session expired and could not be refreshed
🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head fe0ffef6 · 2026-08-20T07:00Z
gowshik450526511
approved these changes
Aug 20, 2026
Routing comparisons through a string-only helper changed what happens to a cumulative snapshot with no timestamp. It had been sorted into the baseline by the empty-string default; it began landing in the turn instead, so an earlier session's running total was billed as this turn's spend. Timestamps now normalize before comparison: numeric epoch seconds and milliseconds resolve to the same ISO form the string ones use, so a numeric transcript is ordered rather than discarded. What remains unplaceable is sorted into the baseline, as it was before, because a cumulative snapshot that cannot be placed is far likelier to predate the turn than to be its spend, and guessing the other way over-bills.
vigneshsubbiah16
left a comment
Collaborator
There was a problem hiding this comment.
Failed to authenticate: OAuth session expired and could not be refreshed
🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head ec8292b4 · 2026-08-20T07:41Z
pugazhendhi-m
added a commit
that referenced
this pull request
Aug 20, 2026
staging carries #247 as a squash, so its commits are not ancestors here and git saw two lineages over the same region of the codex hook. The file is resolved to this branch's version, which already contains that work and builds the subagent fold on top; staging's copy of it is byte-identical, so nothing from #247 is lost.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two format-change exposures in the device hooks. Both are correct on today's data and both fail silently when the upstream format shifts. Found by the scheduled ccusage parity audit.
Linear: WEB-5508
1. Nested session-directory layout dropped every subagent token
_fold_subagent_usagederived the subagent directory by stripping the transcript's extension. That locates it in the flat layout (projects/{proj}/{session}.jsonl→{session}/subagents) but not the nested one (projects/{proj}/{session}/chat.jsonl→chat/subagents, which never exists). Theos.path.isdirguard then returned an empty list without logging, so every subagent token was dropped and the turn under-billed by the whole Task.New
_subagent_dir()checks both positions and returns whichever exists. The listing is now anos.walkinstead of a single-levellistdir, matching ccusage, which recurses without a depth limit (rust/adapters/claude/src/paths.rs:130; its README covers "both nested session directories and legacy flat JSONL files").2. A non-string timestamp discarded the turn's usage
entry.get('timestamp', '') < user_prompt_timestampraisesTypeErroron an integer. The inner handler only catchesjson.JSONDecodeError, so it unwound to the bareexcept Exception: return None— losing everytoken_countline already read for that turn, not one line, and dropping the turn to the tiktoken estimate.All comparisons now route through a
_ts_lt()helper that orders strings only. Applied at all five sites, including two the audit didn't name (codex parse_transcript_file:445,parse_codex_transcript_for_tools:1370).str()coercion was rejected. A numeric timestamp compared lexically against an ISO string mis-scopes the turn silently, which is worse than the crash it replaces.The two directions are deliberate. When an entry can't be ordered:
Rule: never raise, and when ordering is impossible prefer the choice that cannot double-count. Both helper docstrings carry that reasoning.
Verification
Seven synthetic cases:
NoneSuites: codex 33, augment 133, copilot 25, claude-code 220 pass. Two failures are pre-existing on
stagingin files this branch doesn't touch (TestMatcherParityAcrossTreescomparessetup.pymatchers; the cursor identity test is unrelated — cursor has no changes here).No new imports, so
binary/unbound-hook.specHIDDENneeds no update. No version bump — that belongs to the release commit.Not in this PR
Codex subagent token collection (WEB-5507) — separate change, needs a sessions-tree scan and replay-prefix handling.
🤖 Generated with Claude Code
https://claude.ai/code/session_01JVhNY6fkBwnqhsZkQfJnoT
Note
Medium Risk
Changes directly affect per-turn token aggregation and billing; incorrect timestamp or subagent path handling could still over- or under-count usage, though the diff targets known parity gaps.
Overview
Fixes silent under-billing when Claude subagent transcripts live outside the flat
{session}.jsonllayout and turn loss / mis-scoping when transcript timestamps are numeric instead of ISO strings.Claude Code:
_fold_subagent_usagenow resolvessubagents/via_subagent_dir()(flat{session}/subagentsand nested{session}/subagentsbeside the transcript) and recursively collects all.jsonlfiles under that tree, matching ccusage-style discovery.Claude + Codex: Adds
_ts_key()/_ts_lt()so comparisons never raise—numeric epochs (seconds or ms) normalize to ISOZstrings before ordering. Turn scoping (main transcript, subagent fold, Codex tool parsing) skips entries unless_ts_lt(user_prompt_timestamp, entry_ts)proves they are after the prompt. Codextoken_countcumulative usage uses the same helpers with an explicit rule: unorderable timestamps count as pre-prompt baseline so sticky cumulative snapshots are not attributed to the current turn.Reviewed by Cursor Bugbot for commit ec8292b. Bugbot is set up for automated code reviews on this repo. Configure here.
Greptile Summary
The PR makes Claude Code and Codex transcript parsing tolerant of numeric timestamps and restores Claude subagent usage discovery across flat and nested session layouts.
Confidence Score: 5/5
The PR appears safe to merge.
No blocking failure remains.
Important Files Changed
Reviews (2): Last reviewed commit: "fix(hooks): treat an unplaceable timesta..." | Re-trigger Greptile
Context used: