Skip to content

feat(traces): record model, reasoning effort, and token usage per session event#320

Merged
efenocchi merged 33 commits into
mainfrom
feat/trace-model-tokens
Jul 21, 2026
Merged

feat(traces): record model, reasoning effort, and token usage per session event#320
efenocchi merged 33 commits into
mainfrom
feat/trace-model-tokens

Conversation

@efenocchi

@efenocchi efenocchi commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

What

Records model, reasoning effort, and token consumption on captured session traces, so per-model token usage is a simple query over the sessions table. Covers every code assistant the plugin captures.

Neither Claude Code nor Codex passes this in the hook payload, but both write it to the on-disk transcript the capture hooks already receive a path to. Pi and OpenClaw expose model+usage on the in-process SDK message. A shared module reads/normalizes it and the capture paths enrich the JSONB message column — no schema migration.

Coverage (verified end-to-end against real transcripts)

Agent model reasoning effort token usage source
Claude Code n/a (no per-message field) ✅ input/output/cache transcript last assistant turn
Codex ✅ input/output/cache/reasoning + cumulative rollout turn_context + token_count
Pi n/a ✅ input/output/cache SDK message_end
OpenClaw n/a ✅ input/output/cache SDK agent_end
Cursor ✅ (already) n/a none exists in its transcript payload
Hermes none in payload

Stored shape (JSONB message)

{ "model": "claude-opus-4-8", "reasoning_effort": null,
  "token_usage": { "input_tokens": 131, "output_tokens": 403,
                   "cache_read_tokens": 357059, "cache_creation_tokens": 2102 } }

Codex rows additionally carry reasoning_effort and token_usage_total (whole-session cumulative).

How

  • New src/notifications/model-usage.ts: parseClaudeTurnMeta, parseCodexTurnMeta, and normalizeSdkUsage/sdkTurnMeta (Pi/OpenClaw). Normalized keys across agents; absent fields stay absent; non-negative-safe-integer filtering; best-effort (null on any failure).
  • Wiring: src/hooks/capture.ts (Claude), src/hooks/codex/capture.ts (Codex), harnesses/openclaw/src/index.ts (import), harnesses/pi/extension-source/hivemind.ts (inlined — pi ships raw .ts).
  • src/hooks/shared/redact.ts: guard so the entropy backstop doesn't shred provider model ids now stored in every row (would break per-model rollups) — narrow enough that a secret wearing a model prefix is still masked.

Testing

  • scripts/trace-model-usage-e2e.mjs drives the real capture hooks (Claude/Codex/Cursor/Hermes) across every model in the local transcripts, writes to a throwaway table, reads back, and asserts each agent's rows landed with token_usage for the token-bearing agents. Pi/OpenClaw capture in-process, so it proves their enriched entry from a real transcript message. Passes.
    • Verified models incl. claude-opus-4-8 / sonnet-4-6 / haiku-4-5, gpt-5.6-sol / gpt-5.5 (+ reasoning + cumulative), pi gpt-5.5, openclaw anthropic/claude-sonnet-4.
    • The sessions table drops/lags rapid same-table bursts (200 OK, rows=0); the harness paces + retries and reports invoked-vs-visible honestly. This is a backend characteristic, not a feature defect.
  • Unit tests: tests/shared/notifications-model-usage.test.ts, redaction guard tests in tests/shared/redact.test.ts. tsc clean; full suite green except one pre-existing, unrelated cli-bundle-runtime tree-sitter test (environmental; my diff touches no graph/CLI code).

Review

codex review run twice (initial + expanded scope); all findings fixed — SubagentStop transcript, Codex turn-boundary scoping, cumulative-vs-per-model semantics, non-object JSONL guard, redaction guard tightening, and e2e capture-verification. CodeRabbit findings (invalid token counts, per-turn effort reset) also fixed.

Summary by CodeRabbit

  • New Features
    • Captured assistant messages and related rows are now enriched with per-turn model identifiers, reasoning effort, token usage, and Claude/Codex/Pi/OpenClaw/Cursor-derived cost details when available, including stop reason and quota-style extras.
    • Added an end-to-end trace script to validate model/token enrichment from local transcripts and capture outputs.
  • Bug Fixes
    • Provider-style model identifier slugs are no longer incorrectly treated as secrets.
    • Metadata enrichment is best-effort and avoids failures on missing, malformed, or incomplete transcripts.
  • Tests
    • Expanded unit tests for parsing/normalization behavior and improved secret redaction and end-to-end coverage.

New shared module src/notifications/model-usage.ts with two best-effort
parsers that read the on-disk transcript the capture hooks already receive
a path to:

- parseClaudeTurnMeta: last assistant turn's model + usage from a Claude
  Code transcript. Reasoning effort left null (no per-message field).
- parseCodexTurnMeta: model + reasoning_effort (turn_context) + per-turn
  and cumulative token usage (token_count) from a Codex rollout.

Both normalize onto a shared NormalizedUsage shape (cache_read_tokens,
cache_creation_tokens, reasoning_output_tokens, total_tokens) so a
per-model rollup is a single GROUP BY over the sessions table. Absent
fields stay absent; any read/parse failure returns null.

Covered by tests/shared/notifications-model-usage.test.ts (real transcript
shapes: reverse-scan, omitted fields, malformed lines, model fallback,
last-vs-total usage).
Enrich the assistant_message trace row (Stop event) with model,
reasoning_effort (null for Claude) and per-turn token_usage, read from the
transcript's last assistant turn via parseClaudeTurnMeta. Best-effort: the
row is written unchanged when the transcript is unreadable.
Enrich the Codex trace meta with reasoning_effort, per-turn token_usage
and cumulative token_usage_total from the rollout (parseCodexTurnMeta).
Codex emits no assistant event, so the enrichment rides on every
user/tool row; the running total rolls up per model with MAX, not SUM.
The turn_context model refines the payload model when present.
Four correctness fixes from the codex review of this branch:

- SubagentStop: parse agent_transcript_path when present. transcript_path
  points at the parent session, so the subagent's assistant event was
  recording the parent model/usage (capture.ts).
- Codex per-turn usage: clear it on each turn_context so a new turn's model
  never inherits the previous turn's tokens. total stays cumulative.
- token_usage_total: documented as whole-session cumulative (across all
  models), not per-model; per-model totals sum per-turn usage deduped by
  turn_id. Fixed the misleading MAX-per-model note.
- Best-effort contract: skip non-object JSONL roots (a bare `null` line)
  before dereferencing, in both parsers.

Tests extended: turn-boundary model change omits stale usage; bare-null
line survives in both parsers.
@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds best-effort model, token-usage, billing, quota, and stop-reason extraction for Claude, Codex, Pi, and OpenClaw captures. Adds unit and end-to-end validation, and prevents recognized provider model identifiers from being redacted as secrets.

Changes

Model usage capture and preservation

Layer / File(s) Summary
Metadata contracts and parsing helpers
src/notifications/model-usage.ts
Defines normalized usage contracts and parses Claude and Codex JSONL transcripts with validation, fallback, billing, quota, and turn-boundary handling.
Capture hook metadata integration
src/hooks/capture.ts, src/hooks/codex/capture.ts
Merges parsed metadata into Claude assistant and Codex user-message/tool-call entries.
Pi and OpenClaw entry enrichment
harnesses/pi/extension-source/hivemind.ts, harnesses/openclaw/src/index.ts
Adds normalized model, stop-reason, token, and cost metadata to assistant rows captured from SDK payloads.
Parser and normalization validation
tests/shared/notifications-model-usage.test.ts
Tests transcript parsing, normalization, turn boundaries, malformed input, billing details, quota details, and missing metadata.
End-to-end usage verification
scripts/trace-model-usage-e2e.mjs
Discovers real transcripts, invokes compiled hooks, polls stored rows, checks metadata invariants, and cleans up test data.
Provider model identifier preservation
src/hooks/shared/redact.ts, tests/shared/redact.test.ts
Exempts recognized provider model slugs from entropy masking while continuing to redact high-entropy secrets with similar prefixes.
Capture wiring validation
tests/claude-code/skillify-session-start-injection.test.ts
Validates the source-order relationship between the OpenClaw agent_end hook, the auto-capture log, and worker startup.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant TraceScript
  participant TranscriptFiles
  participant CaptureHooks
  participant ModelUsageParsers
  participant SessionsTable
  TraceScript->>TranscriptFiles: Discover representative model transcripts
  TraceScript->>CaptureHooks: Invoke compiled hooks
  CaptureHooks->>ModelUsageParsers: Parse transcript or SDK usage metadata
  ModelUsageParsers-->>CaptureHooks: Return normalized model metadata
  CaptureHooks->>SessionsTable: Insert enriched capture rows
  TraceScript->>SessionsTable: Poll and validate stored metadata
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: recording model, reasoning effort, and token usage in session events.
Description check ✅ Passed The description is mostly complete and covers the change, implementation, and testing, though it doesn't follow the template's exact section headings.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/trace-model-tokens

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Coverage Report

Scope: files changed in this PR. Enforced threshold: 90% per metric (per file via vitest.config.ts).

Status Category Percentage Covered / Total
🟢 Lines 97.51% (🎯 90%) 430 / 441
🟢 Statements 94.52% (🎯 90%) 517 / 547
🟢 Functions 100.00% (🎯 90%) 47 / 47
🔴 Branches 86.08% (🎯 90%) 340 / 395
File Coverage — 5 files changed
File Stmts Branches Functions Lines
src/hooks/capture.ts 🟢 95.3% 🔴 83.9% 🟢 100.0% 🟢 100.0%
src/hooks/codex/capture.ts 🟢 97.2% 🔴 82.5% 🟢 100.0% 🟢 100.0%
src/hooks/hermes/capture.ts 🟢 91.7% 🔴 88.0% 🟢 100.0% 🟢 95.0%
src/hooks/shared/redact.ts 🟢 97.2% 🟢 92.8% 🟢 100.0% 🟢 100.0%
src/notifications/model-usage.ts 🟢 94.2% 🔴 85.8% 🟢 100.0% 🟢 96.4%

Generated for commit d73ed74.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/notifications/model-usage.ts`:
- Around line 72-78: Update readLines to avoid synchronously rereading and
splitting the entire transcript on every event. Maintain a per-transcript
cursor/cache or otherwise read only bytes appended since the previous call,
while preserving the missing-transcript handling and returning the newly
available lines needed by callers.
- Around line 62-64: Update toNum in model-usage.ts to return a value only when
v is a finite, non-negative safe integer; return undefined for negative,
fractional, unsafe, or otherwise invalid inputs before they reach usage
aggregation persistence.
- Around line 204-211: Update the turn_context handling in the model-usage
parser to reset model and reasoningEffort for every new turn, using the current
hook model only as the fallback when the new context’s model is missing or
malformed and clearing invalid or absent effort instead of retaining the
previous value. Preserve last/total behavior, and add a regression test covering
a valid first context followed by a context with missing or malformed
model/effort.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 7de3d604-2726-4324-b4f3-4cebcf231d7a

📥 Commits

Reviewing files that changed from the base of the PR and between c4f1ef6 and b9fc044.

📒 Files selected for processing (4)
  • src/hooks/capture.ts
  • src/hooks/codex/capture.ts
  • src/notifications/model-usage.ts
  • tests/shared/notifications-model-usage.test.ts

Comment thread src/notifications/model-usage.ts
Comment thread src/notifications/model-usage.ts Outdated
Comment thread src/notifications/model-usage.ts
…effort per turn

- toNum: accept only non-negative safe integers, so negative / fractional /
  unsafe values never enter token aggregates.
- turn_context: reset both model and reasoning effort per turn (hook model as
  fallback) so a later turn that omits effort no longer inherits the previous
  turn's effort — same scoping fix already applied to per-turn usage.

Tests: per-turn model/effort reset with hook-model fallback; negative and
fractional token counts dropped.
@efenocchi

Copy link
Copy Markdown
Collaborator Author

CodeRabbit triage:

  • toNum (invalid token counts) — fixed in 52910dc: only non-negative safe integers pass.
  • turn_context effort scoping — fixed in 52910dc: model and effort reset per turn (hook model as fallback), matching the per-turn usage scoping.
  • readLines O(n²) full re-read — acknowledged, deferring to a follow-up. Rationale: the capture hooks run async:true, each read is a few ms on transcripts of realistic size, and this mirrors the existing transcript-parser.ts which also reads the whole transcript. A per-transcript cursor/cache (handling truncation/rotation) is a self-contained optimization better done separately rather than bundled into this behavioral change. Tracking it as a follow-up.

normalizeSdkUsage maps the pi/openclaw usage shape
{input, output, cacheRead, cacheWrite, totalTokens} onto the shared
NormalizedUsage keys; sdkTurnMeta builds the {model, token_usage} enrichment
from an in-process SDK message (these runtimes expose no reasoning effort).
Same non-negative-safe-integer filtering as the transcript parsers.

Tests cover the real Pi and OpenClaw usage shapes (incl. cacheWrite ->
cache_creation_tokens) and the model-only / empty cases.
Storing the model on every trace row exposed a latent false positive: the
high-entropy backstop masked long dated slugs like
`claude-haiku-4-5-20251001` -> `********`, which would silently break
per-model token rollups. Add a model-identifier guard to looksLikeSecret
(claude/gpt/o*/gemini/... optionally provider-prefixed) so these survive
redaction. Regression test asserts common ids are preserved.
The agent_end auto-capture loop now enriches assistant rows with model and
normalized token_usage from the SDK message (via the shared sdkTurnMeta).
User rows are unchanged.
message_end now enriches the assistant row with model and normalized
token_usage from the SDK message. Pi ships as raw .ts with no shared-module
imports, so the normalizer is inlined in lockstep with
src/notifications/model-usage.ts (same pattern as the inlined JWT helpers).
scripts/trace-model-usage-e2e.mjs drives the real capture hooks (Claude
Code, Codex, Cursor, Hermes) against every model found in the local
transcripts, writing to a throwaway table and reading the rows back to show
what landed in the JSONB message column. Pi and OpenClaw capture in-process
(not via stdin hooks), so it proves their enriched entry from a real
transcript message via the shared sdkTurnMeta. Suppresses side effects
(HIVEMIND_WIKI_WORKER=1), disables embeddings, and cleans up its rows.
Address codex review [P1]: the earlier exemption used a case-insensitive
prefix match with an open charset, so a random key wearing a model prefix
(`gpt-AbCd…30chars`) would be exempted and stored unmasked. Narrow it to
lowercase-only ids with each dot/dash segment capped at 12 chars, so no 24+
high-entropy run (mixed-case or long) can satisfy it. Added a negative test
asserting prefixed high-entropy secrets are still masked.
… agent

Address codex review [P2] plus the sessions-table write behaviour observed
while running it:
- The hooks swallow insert errors and exit 0, so exit code is not proof.
  Read the rows back and assert each agent's capture path landed rows, with
  token_usage present for the token-bearing agents (claude_code, codex).
- The table drops rapid same-table bursts (200 OK, rows=0, row never lands)
  and is read-after-write lagged. Pace inserts (SLEEP_MS), poll the read-back,
  and retry stragglers; report invoked-vs-visible per agent instead of
  silently treating a hidden tail as success.
- Derive the agent from the session_id prefix (the SQL `agent` column reads
  back unreliably here).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/trace-model-usage-e2e.mjs`:
- Around line 290-301: Strengthen scripts/trace-model-usage-e2e.mjs at lines
290-301 by retaining expected parser metadata per session and comparing every
stored model, reasoning-effort, usage, and cumulative-usage field for each
capture path, rather than checking only row presence; update lines 312-318 to
exercise Pi’s inline normalizer and both integrations’ real entry/write paths
with assertions, ensuring logging or missing transcripts cannot count as
success.
- Around line 297-301: Update the validation loops in the trace-model usage
check to require rows only for agents recorded in invokedByAgent. Gate both
modelsByAgent and tokenRowsByAgent checks on whether each agent was actually
invoked, while preserving the existing missing-row problems for invoked agents.

In `@src/hooks/shared/redact.ts`:
- Around line 70-74: Update the model-identifier whitelist regex in the
redaction logic to recognize un-hyphenated version digits for the affected
families, such as optional single digits after llama and qwen, while preserving
existing matching behavior. Extend the accepted prefix alternatives to include
common Bedrock/provider prefixes—anthropic, amazon, cohere, meta, us, and eu—so
provider-qualified identifiers remain unredacted after colon splitting.

In `@tests/shared/redact.test.ts`:
- Around line 223-251: Replace the generic toContain assertions in the
provider-model and prefixed-secret tests with exact toBe assertions against the
complete expected redacted or unchanged payload. Keep redactSecrets and MASK
behavior unchanged, and ensure each assertion verifies the entire JSON or token
string rather than only a substring.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 0aba9947-0d09-4f5b-a85f-cf481a1a56e1

📥 Commits

Reviewing files that changed from the base of the PR and between 52910dc and 603bd04.

📒 Files selected for processing (7)
  • harnesses/openclaw/src/index.ts
  • harnesses/pi/extension-source/hivemind.ts
  • scripts/trace-model-usage-e2e.mjs
  • src/hooks/shared/redact.ts
  • src/notifications/model-usage.ts
  • tests/shared/notifications-model-usage.test.ts
  • tests/shared/redact.test.ts

Comment thread scripts/trace-model-usage-e2e.mjs Outdated
Comment thread scripts/trace-model-usage-e2e.mjs Outdated
Comment thread src/hooks/shared/redact.ts
Comment thread tests/shared/redact.test.ts
- redact: broaden the model-id guard to cover cloud/region-prefixed slugs
  (us.anthropic.claude-3-5-sonnet-20241022-v2) and digit-fused families
  (qwen2.5-coder-32b-instruct, llama3.1-405b-instruct) so they aren't
  over-redacted, while keeping the lowercase + <=12-char-segment invariants
  that stop a secret from slipping through. Tests use exact assertions.
- e2e: require only agents that were actually invoked (a CI box may lack a
  given agent's transcripts); assert token-bearing rows carry positive
  integer input+output tokens (correctness, not mere presence); and assert
  the Pi/OpenClaw sdkTurnMeta proof yields valid positive token_usage.
@efenocchi

Copy link
Copy Markdown
Collaborator Author

CodeRabbit round-2 triage (all in 551912ae):

  • redact — Bedrock / un-hyphenated families ✅ Broadened the guard to allow cloud/region prefixes (us.anthropic.claude-3-5-sonnet-20241022-v2) and a digit fused to the family (qwen2.5-coder-32b-instruct, llama3.1-405b-instruct), while keeping the two safety invariants (lowercase-only + every segment ≤12 chars) so a high-entropy secret still cannot match.
  • e2e — only require invoked agents ✅ Required agents are now derived from the jobs actually created, not hardcoded — a box without a given agent's transcripts no longer fails spuriously.
  • e2e — validate actual metadata ✅ Token-bearing rows must now carry positive integer input+output tokens (not mere presence), and the Pi/OpenClaw sdkTurnMeta proof asserts valid positive token_usage.
  • tests — exact assertions ✅ Redaction tests now assert exact input/output.
  • toNum / reset model+effort at turn boundary — already in the current code (the reset landed in 52910dcd/b9fc044c; your proposed diff matches it verbatim). No change needed.
  • readLines re-reads the whole transcript (O(n²)) — deferring. The capture hooks run async:true, each read is a few ms on realistic transcripts, and this mirrors the existing transcript-parser.ts which also reads the whole file. A per-transcript cursor/cache (handling truncation/rotation) is a self-contained optimization better landed separately than bundled into this behavioral change. Tracking as a follow-up.

Expand the trace schema beyond raw token counts to everything each agent's
transcript actually exposes:

- NormalizedUsage gains `cost` (fractional dollars; Pi/OpenClaw report it).
- TraceModelMeta gains `stop_reason` (Claude stop_reason / SDK stopReason)
  and `usage_extra`, a per-harness bag for fields that don't generalize:
    * Claude: service_tier, speed, inference_geo, cache-creation TTL split
      (ephemeral 1h vs 5m — priced differently), server_tool_use
      (web_search/web_fetch requests).
    * Codex: model_context_window + rate_limits (primary/secondary windows).
- parseClaudeTurnMeta / parseCodexTurnMeta / normalizeSdkUsage / sdkTurnMeta
  extract all of the above; costs use a finite-non-negative filter (integers
  would drop them), token counts stay non-negative safe integers.

Verified against real transcripts for every agent (Claude billing detail,
Codex quota, Pi/OpenClaw dollar cost). Tests cover each new field.
Extend the inlined piModelMeta to pull message.stopReason and the usage
`cost` object (input/output/cacheRead/cacheWrite/total), normalized in
lockstep with src/notifications/model-usage.ts. Verified live: a real
`pi --print` turn wrote token_usage.cost (dollars) + stop_reason to the
sessions table.
…hared normalizer)

Pass msg.stopReason into sdkTurnMeta so OpenClaw rows record why the turn
ended; cost flows through the shared normalizer. Bump the skillify-wiring
proximity assertion 4000→4400 to accommodate the added tagging block between
`agent_end` and `Auto-captured` (same pattern as the prior 3500→4000 bump).
Codex quota (model_context_window + rate_limits) was retained across a new
turn_context and a later token_count without extras couldn't clear it, so a
new model's row could be tagged with the previous turn's quota. Reset extra
on turn_context and assign unconditionally on each token_count (latest wins,
may clear) — matching the existing per-turn reset for last_token_usage.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
tests/shared/notifications-model-usage.test.ts (1)

322-369: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding a negative-case test for invalid quota values.

These tests cover valid model_context_window/rate_limits extraction and the turn-boundary reset, but don't exercise codexUsageExtra's numeric guards (e.g., negative used_percent, fractional window_minutes/resets_at). A quick case confirming invalid fields are dropped rather than silently included would directly validate that defensive path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/shared/notifications-model-usage.test.ts` around lines 322 - 369, Add a
negative-case test in the parseCodexTurnMeta usage_extra suite that supplies
invalid codexUsageExtra quota fields, including negative used_percent and
fractional window_minutes or resets_at. Assert the resulting usage_extra omits
those invalid fields while retaining any valid quota fields, directly exercising
the numeric guards.
src/notifications/model-usage.ts (1)

304-328: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Simplify rate_limits construction to avoid repeated casts.

Building extra.rate_limits = {} then repeatedly casting it back to Record<string, unknown> to assign primary/secondary works but is clunkier than building the object first.

♻️ Suggested simplification
-  const primary = pickWin(rateLimits?.primary);
-  const secondary = pickWin(rateLimits?.secondary);
-  if (primary || secondary) {
-    extra.rate_limits = {};
-    if (primary) (extra.rate_limits as Record<string, unknown>).primary = primary;
-    if (secondary) (extra.rate_limits as Record<string, unknown>).secondary = secondary;
-  }
+  const primary = pickWin(rateLimits?.primary);
+  const secondary = pickWin(rateLimits?.secondary);
+  if (primary || secondary) {
+    const rl: Record<string, unknown> = {};
+    if (primary) rl.primary = primary;
+    if (secondary) rl.secondary = secondary;
+    extra.rate_limits = rl;
+  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/notifications/model-usage.ts` around lines 304 - 328, In codexUsageExtra,
simplify rate_limits construction by assembling a local object from the
available primary and secondary windows, then assign it to extra.rate_limits
once. Remove the repeated Record<string, unknown> casts while preserving
omission of undefined windows and the existing empty-result behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/claude-code/skillify-session-start-injection.test.ts`:
- Around line 297-299: Replace the character-count-based source-order regex in
the skillify session-start test with structural anchors for the actual agent_end
hook, Auto-captured log, and spawnOpenclawSkillifyWorker({ ... }) call. Preserve
the required ordering while removing the permissive {0,4400} and {0,1500} gaps.

---

Nitpick comments:
In `@src/notifications/model-usage.ts`:
- Around line 304-328: In codexUsageExtra, simplify rate_limits construction by
assembling a local object from the available primary and secondary windows, then
assign it to extra.rate_limits once. Remove the repeated Record<string, unknown>
casts while preserving omission of undefined windows and the existing
empty-result behavior.

In `@tests/shared/notifications-model-usage.test.ts`:
- Around line 322-369: Add a negative-case test in the parseCodexTurnMeta
usage_extra suite that supplies invalid codexUsageExtra quota fields, including
negative used_percent and fractional window_minutes or resets_at. Assert the
resulting usage_extra omits those invalid fields while retaining any valid quota
fields, directly exercising the numeric guards.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ede1af65-1eab-4632-b7b5-9e1ba5638797

📥 Commits

Reviewing files that changed from the base of the PR and between 603bd04 and 65738d1.

📒 Files selected for processing (8)
  • harnesses/openclaw/src/index.ts
  • harnesses/pi/extension-source/hivemind.ts
  • scripts/trace-model-usage-e2e.mjs
  • src/hooks/shared/redact.ts
  • src/notifications/model-usage.ts
  • tests/claude-code/skillify-session-start-injection.test.ts
  • tests/shared/notifications-model-usage.test.ts
  • tests/shared/redact.test.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • tests/shared/redact.test.ts
  • src/hooks/shared/redact.ts
  • harnesses/openclaw/src/index.ts
  • harnesses/pi/extension-source/hivemind.ts
  • scripts/trace-model-usage-e2e.mjs

Comment thread tests/claude-code/skillify-session-start-injection.test.ts Outdated
…t char gap

Address CodeRabbit: replace the brittle `agent_end{0,4400}Auto-captured...`
magic-gap regex with source-order index assertions on the real
`hook("agent_end")`, `Auto-captured`, and `spawnOpenclawSkillifyWorker(`
constructs. Robust to unrelated code landing between them (no future bumps).
…ition

The order assertion matched the earlier function definition instead of the
call after `Auto-captured`, failing the check. Search for the call from the
Auto-captured index. Verified: suite passes.
Claude's effort (low/medium/high, ultrathink=high) is a user setting, not a
per-message transcript field — so parseClaudeTurnMeta now reads `effortLevel`
from settings (project settings.local.json > project settings.json > user
~/.claude/settings.json), falling back to null. Corrects the earlier claim
that Claude has no capturable reasoning effort. Verified: a real transcript
now yields reasoning_effort="medium" from settings. Tests cover precedence,
missing, and malformed settings.
Hermes' pre_llm_call / post_llm_call hooks DO send `model` (and `platform`),
nested in `extra` — everything that isn't tool_name/args/session_id lands
there (NousResearch/hermes-agent agent/shell_hooks.py). Read extra.model
onto the trace row and extra.platform into usage_extra. Token usage / cost
are genuinely absent from the Hermes hook payload. Verified end-to-end: the
real hermes capture hook wrote model + platform to the table.
Settings files are untrusted input — restrict effortLevel to Claude's
supported levels (low/medium/high/xhigh, case-insensitive) so a malformed or
oversized value can't be uploaded as reasoning_effort. Falls through
otherwise. Tests cover rejection of a bogus value and case normalization.
…r rate_limits

- Add a negative-case test: negative used_percent, fractional window_minutes /
  model_context_window are dropped by codexUsageExtra's numeric guards while
  valid fields are retained.
- Simplify rate_limits assembly into a single local object (drop the repeated
  inline Record casts).
Resolve the last open CodeRabbit finding (O(n^2) re-read). Model / usage /
stop_reason all live in the most recent turn at the END of the transcript, so
parseClaudeTurnMeta / parseCodexTurnMeta now read only the tail (default
1 MiB) via a positioned read, dropping the partial leading line so callers
only see whole lines. This bounds each capture event to a constant instead of
O(filesize) — and O(n^2) across a Codex session that parses on every
user/tool row. Falls back to null on any IO error (unchanged contract).

Verified: a real 4.28 MB Claude transcript still extracts model/effort/tokens
correctly from the 1 MiB tail. Tests cover the window boundary (partial first
line dropped, newest line always present) and end-of-file extraction.
Three fixes on the tail reader:
- Whole-file fallback: if a single record exceeds the tail window (giant Claude
  assistant line, or a Codex turn with >window bytes after its turn_context /
  token_count), the parsers re-scan the whole file so usage/effort/quota aren't
  silently lost. scanCodexLines reports foundData so a fallback-model-only
  result still triggers the widen.
- Exact line boundary: read one extra leading byte and cut at the first newline,
  so a window that starts exactly on a line boundary keeps that complete line
  instead of dropping it.
- Short read: Buffer.alloc (zeroed) + decode only the bytes actually read, so a
  truncated/racing read can't decode uninitialized memory.

Tests: boundary-aligned window keeps the whole line; a >1 MiB assistant record
is recovered via the whole-file fallback.
Address codex re-review: turn_context (model/effort) and token_count
(usage/total/quota) carry different halves of a turn's metadata, so trusting
the tail on "either seen" could drop half. scanCodexLines now reports
sawTurnContext + sawTokenCount separately, and parseCodexTurnMeta only trusts
the tail when BOTH are present — otherwise it re-scans the whole file (whose
result is authoritative). Test: a >1 MiB record between turn_context and
token_count still recovers model + effort via the whole-file fallback.
Two more edges from codex re-review:
- Trust the tail only when it also carries the cumulative total_token_usage
  (whole-file scan preserves it across turns; an in-tail token_count may omit
  it if an earlier one set it). Otherwise widen.
- If the whole-file re-read fails (file removed/truncated between reads), keep
  the tail's already-extracted usage instead of clobbering it with a
  model-only fallback.

Test: a total set before the window, followed by a >1 MiB record and a
token_count without total, is still recovered via the whole-file scan.
Close the file-mutation race class: if the whole-file reread comes back empty
or event-less (file truncated/removed between reads), keep the tail's
already-extracted usage instead of overwriting it with a model-only fallback.
Trust the reread only when it actually saw a turn_context or token_count.
…n-race class)

Replace the two independent readTailLines() opens with one openTranscript()
that opens the file once, captures size once, and serves both the tail and the
fallback whole-file read from the SAME fd/snapshot. This structurally closes
the reopen/truncate-between-reads race the review kept surfacing: the fallback
can no longer see a different (older/truncated) file than the tail. readTailLines
stays as a thin wrapper for callers/tests. No behavior change on the happy path;
verified on real Claude + Codex transcripts.
Comment thread src/notifications/model-usage.ts Fixed
Comment thread src/notifications/model-usage.ts Fixed
Root-level reference so agents/contributors can discover what each trace row
records (model, reasoning effort, stop reason, token usage incl. cache tokens
and cost, per-harness usage_extra), the source of each field per harness, the
capture matrix, aggregation queries, and the design notes (best-effort, tail
read, redaction guard). Linked from README Architecture.
…race)

CodeQL flagged the existsSync()->openSync()/readFileSync() pre-check as a
file-system race (2 high alerts on model-usage.ts). Open/read directly and
handle the error (incl. ENOENT) instead of checking first — same behavior,
no TOCTOU. Tail-read/O(1) design unchanged.
Comment thread src/notifications/model-usage.ts Dismissed
Lives alongside the per-agent integrations it documents. Fixed the internal
link (../src/...) and the README pointer.
Update the README pointer to harnesses/TRACE_MODEL_USAGE.md and the doc's
internal link to ../src/notifications/model-usage.ts.
@efenocchi
efenocchi merged commit 782cd68 into main Jul 21, 2026
9 checks passed
@efenocchi
efenocchi deleted the feat/trace-model-tokens branch July 21, 2026 00:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants