# MyBurn Metrics Catalog — Claude Code JSONL Extraction Specification

> **Version**: 1.0
> **Date**: 2026-02-24
> **Based on**: [JSONL Spike Report](jsonl-spike-report.md) (461 files, 91,306 entries)
> **Purpose**: Formal specification of every metric MyBurn can extract from Claude Code JSONL logs. This document serves as the implementation contract for the native Python parser.

---

## Extraction Principle

We extract **EVENTS** and **METADATA**, never **CONTENT**.

Claude Code JSONL logs contain everything: user prompts, assistant responses, code diffs, file contents, bash output. MyBurn reads the structural envelope of each entry — timestamps, token counts, model names, tool names, durations, error codes — and ignores everything inside the content payload.

The analogy: we read the postage stamp, not the letter.

---

## 1. Safety Boundary

### SAFE to extract (pure metadata)

These fields contain no user content, code, or intellectual property:

| Field | Location | Type | Example |
|-------|----------|------|---------|
| `type` | top-level | string | `"assistant"`, `"user"`, `"system"` |
| `subtype` | top-level (system entries) | string | `"api_error"`, `"turn_duration"` |
| `uuid` | top-level | string | `"abc-123-..."` |
| `parentUuid` | top-level | string/null | `"def-456-..."` |
| `sessionId` | top-level | string | `"session-uuid"` |
| `timestamp` | top-level | string (ISO 8601) | `"2026-02-18T02:41:20.435Z"` |
| `version` | top-level | string | `"2.1.50"` |
| `gitBranch` | top-level | string | `"master"`, `"feat/auth"` |
| `isSidechain` | top-level | boolean | `true` |
| `requestId` | top-level (assistant) | string | `"req_011CYEmB..."` |
| `slug` | top-level | string | `"peppy-seeking-brooks"` |
| `agentId` | top-level (subagents) | string | `"a4682d9"` |
| `permissionMode` | top-level (user entries) | string | `"bypassPermissions"` |
| `durationMs` | top-level (turn_duration) | integer | `12450` |
| `isApiErrorMessage` | top-level | boolean | `true` |
| `isCompactSummary` | top-level | boolean | `true` |
| `isMeta` | top-level | boolean | `true` |
| `message.role` | message object | string | `"assistant"` |
| `message.type` | message object | string | `"message"` |
| `message.id` | message object | string | `"msg_01J31r7..."` |
| `message.model` | message object | string | `"claude-opus-4-6"` |
| `message.stop_reason` | message object | string/null | `"tool_use"`, `"stop_sequence"` |
| `message.usage.*` | message object | object | All token counts, cache stats |
| `message.content[].type` | content blocks | string | `"text"`, `"thinking"`, `"tool_use"`, `"tool_result"` |
| `message.content[].name` | tool_use blocks | string | `"Bash"`, `"Read"`, `"Edit"` |
| `message.content[].id` | tool_use blocks | string | `"toolu_01VF..."` |
| `message.content[].is_error` | tool_result blocks | boolean | `true` |
| `message.content[].tool_use_id` | tool_result blocks | string | `"toolu_01VF..."` |
| `compactMetadata.trigger` | compact_boundary | string | `"auto"` |
| `compactMetadata.preTokens` | compact_boundary | integer | `167088` |
| `retryAttempt` | api_error | integer | `1` |
| `maxRetries` | api_error | integer | `10` |
| `retryInMs` | api_error | float | `507.11` |
| `error.cause.code` | api_error | string | `"ConnectionRefused"` |
| `data.type` | progress entries | string | `"bash_progress"`, `"agent_progress"` |
| `operation` | queue-operation | string | `"enqueue"`, `"dequeue"` |
| `toolUseResult` (structural fields only) | user entries | object | See Grey Zone below |

### NEVER extract (user content / intellectual property)

These fields contain actual code, prompts, file contents, or reasoning:

| Field | Why it's dangerous |
|-------|-------------------|
| `message.content[type=text].text` | Contains user prompts OR assistant prose — actual conversation |
| `message.content[type=thinking].thinking` | Contains model's reasoning chain — may reference code |
| `message.content[type=thinking].signature` | Cryptographic signature — no value to extract |
| `message.content[type=tool_use].input` | Contains tool parameters: code to write, commands to run, file paths with content |
| `message.content[type=tool_result].content` | Contains tool output: bash stdout, file contents, grep results |
| `toolUseResult.stdout` | Bash command output |
| `toolUseResult.stderr` | Bash error output |
| `toolUseResult.content` (string body) | File contents from Read, grep matches, search results body |
| `toolUseResult.oldString` / `toolUseResult.newString` | Edit content (code diffs) |
| `toolUseResult.originalFile` | Original file content before edit |
| `toolUseResult.plan` | Plan mode content (user's thinking) |
| `toolUseResult.prompt` | Subagent prompt text |
| `toolUseResult.answers` | User's answers to AskUserQuestion |
| `toolUseResult.questions` | Questions shown to user |
| `content` (on queue-operation entries) | Queued message text |
| `content` (on system entries) | Human-readable system messages (generally safe but not needed) |
| `planContent` | Plan mode content |
| `snapshot.trackedFileBackups` | Actual file contents for undo |

### GREY ZONE (extract structural metadata only)

`toolUseResult` fields — we extract the structural envelope, never the content body:

| Extractable | Field | Example |
|-------------|-------|---------|
| YES | `toolUseResult.numFiles` | `5` |
| YES | `toolUseResult.numLines` | `230` |
| YES | `toolUseResult.numMatches` | `12` |
| YES | `toolUseResult.mode` | `"file_list"` |
| YES | `toolUseResult.appliedLimit` | `true` |
| YES | `toolUseResult.truncated` | `false` |
| YES | `toolUseResult.durationMs` | `340` |
| YES | `toolUseResult.durationSeconds` | `1.2` |
| YES | `toolUseResult.interrupted` | `false` |
| YES | `toolUseResult.isImage` | `false` |
| YES | `toolUseResult.noOutputExpected` | `true` |
| YES | `toolUseResult.returnCodeInterpretation` | `"No matches found"` |
| YES | `toolUseResult.is_error` (from content) | `true` |
| YES | `toolUseResult.filenames` (count only, not values) | length = 3 |
| YES | `toolUseResult.success` | `true` |
| YES | `toolUseResult.statusChange` | `"pending" -> "done"` |
| YES | `toolUseResult.commandName` (Skill) | `"commit"` |
| YES | `toolUseResult.agentId` | `"a4682d9"` |
| YES | `toolUseResult.totalDurationMs` | `45000` |
| YES | `toolUseResult.isAsync` | `true` |
| YES | `toolUseResult.status` | `"completed"` |
| NO | `toolUseResult.content` (text body) | -- |
| NO | `toolUseResult.stdout` / `toolUseResult.stderr` | -- |
| NO | `toolUseResult.query` (search query text) | -- |
| NO | `toolUseResult.results` (search result bodies) | -- |
| NO | `toolUseResult.filenames` (actual paths) | -- |
| NO | `toolUseResult.filePath` / `toolUseResult.file` | -- |

**Rule of thumb**: If a field tells you *how much* or *what kind*, extract it. If it tells you *what exactly*, skip it.

---

## 2. Metrics by Category

### Legend

Each metric uses these fields:

- **Name**: Short identifier (used in code as variable/column name)
- **Description**: What it measures in plain English
- **Source**: Exact JSONL field path(s)
- **Calculation**: How to compute the value
- **Granularity**: `turn` | `session` | `day` | `project` | `all-time` (may support multiple)
- **Confidence**: `HIGH` (direct field read) | `MEDIUM` (derived/heuristic) | `LOW` (unreliable/incomplete)
- **Privacy**: `SAFE` (pure metadata) | `CAREFUL` (structural only) | `SKIP` (touches content)
- **Dashboard potential**: How it could be displayed
- **Priority**: `P0` | `P1` | `P2`

---

### 2.1 Cost & Tokens

These are the core billing metrics. Most already exist in the current dashboard.

#### `input_tokens`
- **Description**: Raw input tokens sent to the API (excludes cached tokens)
- **Source**: `message.usage.input_tokens`
- **Calculation**: Sum across deduplicated assistant messages (group by `message.id`)
- **Granularity**: turn, session, day, project, all-time
- **Confidence**: HIGH
- **Privacy**: SAFE
- **Dashboard potential**: Number in scorecard, line chart over time
- **Priority**: P0

#### `output_tokens`
- **Description**: Tokens generated by the model in its response
- **Source**: `message.usage.output_tokens`
- **Calculation**: Sum across deduplicated assistant messages
- **Granularity**: turn, session, day, project, all-time
- **Confidence**: HIGH
- **Privacy**: SAFE
- **Dashboard potential**: Number in scorecard, stacked bar chart
- **Priority**: P0

#### `cache_creation_tokens`
- **Description**: Tokens written to cache (billed at creation rate)
- **Source**: `message.usage.cache_creation_input_tokens`
- **Calculation**: Sum across deduplicated assistant messages
- **Granularity**: turn, session, day, project, all-time
- **Confidence**: HIGH
- **Privacy**: SAFE
- **Dashboard potential**: Number, cache efficiency chart
- **Priority**: P0

#### `cache_read_tokens`
- **Description**: Tokens served from cache (billed at discounted read rate)
- **Source**: `message.usage.cache_read_input_tokens`
- **Calculation**: Sum across deduplicated assistant messages
- **Granularity**: turn, session, day, project, all-time
- **Confidence**: HIGH
- **Privacy**: SAFE
- **Dashboard potential**: Number, cache efficiency chart
- **Priority**: P0

#### `total_tokens`
- **Description**: All tokens involved in a request (input + output + cache creation + cache read)
- **Source**: Derived from usage fields
- **Calculation**: `input_tokens + output_tokens + cache_creation_input_tokens + cache_read_input_tokens`
- **Granularity**: turn, session, day, project, all-time
- **Confidence**: HIGH
- **Privacy**: SAFE
- **Dashboard potential**: Hero number in scorecard, main KPI
- **Priority**: P0

#### `cost_usd`
- **Description**: Estimated USD cost based on token counts and model pricing
- **Source**: Derived from usage fields + model name + pricing table
- **Calculation**: Per model:
  - `input_tokens * price_per_input_token`
  - `+ output_tokens * price_per_output_token`
  - `+ cache_creation_input_tokens * price_per_cache_creation_token`
  - `+ cache_read_input_tokens * price_per_cache_read_token`
  - Pricing table is hardcoded with config override
- **Granularity**: turn, session, day, project, all-time
- **Confidence**: MEDIUM (pricing may be outdated; depends on hardcoded table accuracy)
- **Privacy**: SAFE
- **Dashboard potential**: Hero dollar amount, cost trend chart, "You spent $X this month"
- **Priority**: P0

#### `model_name`
- **Description**: Which Claude model processed the request
- **Source**: `message.model`
- **Calculation**: Direct read. Filter out `"<synthetic>"` entries.
- **Granularity**: per-turn (tagged), aggregated at session/day/project
- **Confidence**: HIGH
- **Privacy**: SAFE
- **Dashboard potential**: Model distribution pie chart, "Opus: 72%, Sonnet: 25%, Haiku: 3%"
- **Priority**: P0

#### `cost_per_model`
- **Description**: Cost breakdown by model within any time period
- **Source**: Derived from `cost_usd` grouped by `message.model`
- **Calculation**: Group deduplicated messages by model, sum cost per group
- **Granularity**: session, day, project, all-time
- **Confidence**: MEDIUM
- **Privacy**: SAFE
- **Dashboard potential**: Stacked bar chart, model comparison table
- **Priority**: P0

---

### 2.2 YOLO Mode

#### `permission_mode`
- **Description**: What permission level the user was running in when they sent a message
- **Source**: `permissionMode` (top-level field on `type: "user"` entries)
- **Calculation**: Direct read. Values: `"default"`, `"acceptEdits"`, `"bypassPermissions"`, `"plan"`, `"dontAsk"`
- **Granularity**: per user-entry, aggregated to session/day/project
- **Confidence**: HIGH
- **Privacy**: SAFE
- **Dashboard potential**: Badge ("YOLO MODE"), percentage chart
- **Priority**: P0

#### `yolo_session_pct`
- **Description**: Percentage of user messages sent in YOLO mode (`bypassPermissions`) within a session
- **Source**: Derived from `permissionMode` on user entries
- **Calculation**: `count(permissionMode == "bypassPermissions") / count(all user entries with permissionMode) * 100`
- **Granularity**: session, day, project, all-time
- **Confidence**: HIGH
- **Privacy**: SAFE
- **Dashboard potential**: "32% of your sessions are in YOLO mode", badge, wrapped slide
- **Priority**: P0

#### `yolo_sessions`
- **Description**: Count of sessions where any user entry has `permissionMode: "bypassPermissions"`
- **Source**: Derived from `permissionMode`
- **Calculation**: Count distinct `sessionId` values where at least one user entry has `bypassPermissions`
- **Granularity**: day, project, all-time
- **Confidence**: HIGH
- **Privacy**: SAFE
- **Dashboard potential**: "23 YOLO sessions this month", leaderboard stat
- **Priority**: P0

#### `yolo_cost`
- **Description**: Total cost incurred during YOLO mode sessions
- **Source**: Derived by correlating `permissionMode` with session cost
- **Calculation**: Sum `cost_usd` for all sessions flagged as YOLO. Note: a session is YOLO if ANY user entry has `bypassPermissions` — this is a session-level flag, not per-turn.
- **Granularity**: session, day, project, all-time
- **Confidence**: MEDIUM (session-level attribution, not turn-level)
- **Privacy**: SAFE
- **Dashboard potential**: "$450 spent in YOLO mode", "YOLO cost: 67% of total"
- **Priority**: P1

---

### 2.3 Tool Usage

#### `tool_calls`
- **Description**: Count of tool invocations by tool name
- **Source**: `message.content[]` blocks where `type == "tool_use"` — extract `name` field
- **Calculation**: After deduplication (group by `message.id`, merge content blocks), count `tool_use` blocks grouped by `name`. Tool names: `Bash`, `Read`, `Write`, `Edit`, `Glob`, `Grep`, `WebSearch`, `WebFetch`, `TaskCreate`, `TaskUpdate`, `TaskList`, `TaskOutput`, `TaskStop`, `Task`, `TodoWrite`, `Skill`, `EnterPlanMode`, `ExitPlanMode`, `AskUserQuestion`.
- **Granularity**: turn, session, day, project, all-time
- **Confidence**: HIGH
- **Privacy**: SAFE (we only read the `name` field, never `input`)
- **Dashboard potential**: "Claude used 847 tools", bar chart by tool type, "Most used: Bash (312)"
- **Priority**: P0

#### `tool_error_rate`
- **Description**: Percentage of tool calls that resulted in errors
- **Source**: `message.content[type=tool_result].is_error` on user entries
- **Calculation**: `count(is_error == true) / count(all tool_results) * 100`. Match tool_result back to tool_use via `tool_use_id` to get tool name.
- **Granularity**: session, day, project, all-time
- **Confidence**: HIGH
- **Privacy**: SAFE (only read `is_error` boolean, not error content)
- **Dashboard potential**: "Tool error rate: 4.2%", per-tool error rates
- **Priority**: P1

#### `files_touched`
- **Description**: Number of files involved in file I/O operations
- **Source**: `toolUseResult.numFiles` (on user entries that are tool results for Read/Write/Edit/Glob/Grep)
- **Calculation**: Sum `numFiles` across relevant tool results. Note: this counts operations, not unique files (same file read twice = 2).
- **Granularity**: session, day, project, all-time
- **Confidence**: MEDIUM (counts operations, not unique files; only available when `toolUseResult` is present)
- **Privacy**: CAREFUL (we read `numFiles` count only, never filenames)
- **Dashboard potential**: "Claude touched 234 files", wrapped slide
- **Priority**: P1

#### `lines_read`
- **Description**: Total lines read across Read tool operations
- **Source**: `toolUseResult.numLines` (on Read tool results)
- **Calculation**: Sum `numLines` across Read tool results
- **Granularity**: session, day, project, all-time
- **Confidence**: MEDIUM (only present when `toolUseResult` has the field)
- **Privacy**: CAREFUL (line count only)
- **Dashboard potential**: "Claude read 45,000 lines of code"
- **Priority**: P1

#### `grep_matches`
- **Description**: Total search matches found across Grep operations
- **Source**: `toolUseResult.numMatches` (on Grep tool results)
- **Calculation**: Sum `numMatches` across Grep tool results
- **Granularity**: session, day, project, all-time
- **Confidence**: MEDIUM
- **Privacy**: CAREFUL (match count only, no content)
- **Dashboard potential**: "Found 1,247 matches across searches"
- **Priority**: P2

#### `bash_runs`
- **Description**: Count of Bash tool executions specifically
- **Source**: `tool_use` blocks with `name == "Bash"`
- **Calculation**: Count tool_use blocks named "Bash" after deduplication
- **Granularity**: session, day, project, all-time
- **Confidence**: HIGH
- **Privacy**: SAFE
- **Dashboard potential**: "Ran 312 commands", danger badge for high counts in YOLO mode
- **Priority**: P0

#### `edit_operations`
- **Description**: Count of Edit tool invocations (code modification operations)
- **Source**: `tool_use` blocks with `name == "Edit"`
- **Calculation**: Count after dedup
- **Granularity**: session, day, project, all-time
- **Confidence**: HIGH
- **Privacy**: SAFE
- **Dashboard potential**: "Made 156 code edits"
- **Priority**: P0

#### `write_operations`
- **Description**: Count of Write tool invocations (full file writes)
- **Source**: `tool_use` blocks with `name == "Write"`
- **Calculation**: Count after dedup
- **Granularity**: session, day, project, all-time
- **Confidence**: HIGH
- **Privacy**: SAFE
- **Dashboard potential**: "Wrote 42 files from scratch"
- **Priority**: P0

#### `skill_invocations`
- **Description**: Count and names of Skill tool invocations
- **Source**: `tool_use` blocks with `name == "Skill"`, plus `toolUseResult.commandName` for the skill name
- **Calculation**: Count tool_use blocks named "Skill". For skill breakdown, extract `commandName` from the matching `toolUseResult`.
- **Granularity**: session, day, project, all-time
- **Confidence**: HIGH (count), MEDIUM (skill name requires matching toolUseResult)
- **Privacy**: CAREFUL (commandName is a skill identifier like "commit", not content)
- **Dashboard potential**: "Used /commit 23 times, /review-pr 8 times"
- **Priority**: P2

---

### 2.4 Turn Economics

#### `turn_duration_ms`
- **Description**: Wall-clock time for a single assistant turn (from user message to final response)
- **Source**: `system:turn_duration` entries — `durationMs` field
- **Calculation**: Direct read from `durationMs`. Each `system:turn_duration` entry corresponds to one complete assistant turn.
- **Granularity**: per-turn, aggregated as avg/median/p95 at session/day/project
- **Confidence**: HIGH
- **Privacy**: SAFE
- **Dashboard potential**: "Avg turn: 12.4s", "Longest turn: 3m 42s", histogram
- **Priority**: P0

#### `turn_cost`
- **Description**: Cost of a single assistant turn
- **Source**: Derived from `message.usage` of the deduplicated assistant message in that turn
- **Calculation**: Apply pricing to the usage of the single deduplicated assistant message. A "turn" is one user message followed by one assistant response (which may include multiple tool calls and continuations until the next user message or stop).
- **Granularity**: per-turn, aggregated as avg/max at session/day/project
- **Confidence**: MEDIUM (turn boundary detection is heuristic — based on `parentUuid` chain)
- **Privacy**: SAFE
- **Dashboard potential**: "Most expensive turn: $0.42", "Avg cost per turn: $0.08"
- **Priority**: P1

#### `turn_token_breakdown`
- **Description**: Token breakdown for a single turn (input, output, cache creation, cache read)
- **Source**: `message.usage.*` for the deduplicated assistant message
- **Calculation**: Direct read per turn
- **Granularity**: per-turn
- **Confidence**: HIGH
- **Privacy**: SAFE
- **Dashboard potential**: Stacked bar showing token composition per turn
- **Priority**: P1

#### `tokens_per_second`
- **Description**: Effective output generation speed
- **Source**: Derived from `output_tokens` / `turn_duration_ms`
- **Calculation**: `output_tokens / (turn_duration_ms / 1000)`. Requires matching turn_duration entries to their corresponding assistant messages (via UUID chain or timestamp proximity).
- **Granularity**: per-turn, averaged at session/day
- **Confidence**: LOW (turn_duration includes tool execution time, not just generation; matching is heuristic)
- **Privacy**: SAFE
- **Dashboard potential**: "Avg speed: 42 tok/s" (use with large caveat)
- **Priority**: P2

#### `thinking_presence`
- **Description**: Whether the assistant used extended thinking in a turn
- **Source**: `message.content[]` blocks where `type == "thinking"`
- **Calculation**: Boolean: at least one `thinking` block exists in the deduplicated message content
- **Granularity**: per-turn, counted at session/day/project
- **Confidence**: HIGH
- **Privacy**: SAFE (we detect presence of `thinking` type, never read `thinking` text)
- **Dashboard potential**: "Used extended thinking in 78% of turns", thinking vs non-thinking cost comparison
- **Priority**: P1

---

### 2.5 Session Intelligence

#### `session_duration`
- **Description**: Wall-clock duration of a coding session
- **Source**: `timestamp` field — first entry to last entry per `sessionId`
- **Calculation**: `max(timestamp) - min(timestamp)` for all entries sharing the same `sessionId` (main session file only, excluding subagent files)
- **Granularity**: per-session, averaged at day/project/all-time
- **Confidence**: MEDIUM (includes idle time; user may leave session open)
- **Privacy**: SAFE
- **Dashboard potential**: "Avg session: 47 min", "Longest session: 4h 12m", "Total coding time: 127h"
- **Priority**: P0

#### `session_turn_count`
- **Description**: Number of human turns in a session
- **Source**: Count `type: "user"` entries where `message.content` is a string (human message, not tool_result)
- **Calculation**: For each `sessionId`, count user entries where `message.content` is a plain string (not an array of tool_results). Alternatively, count user entries that do NOT have `sourceToolAssistantUUID` (which marks automated tool responses).
- **Granularity**: per-session, averaged at day/project
- **Confidence**: MEDIUM (heuristic to distinguish human messages from tool_result entries)
- **Privacy**: SAFE
- **Dashboard potential**: "Avg turns per session: 12", histogram of session sizes
- **Priority**: P0

#### `session_cost`
- **Description**: Total cost of a session
- **Source**: Sum of `cost_usd` across all deduplicated assistant messages in the session
- **Calculation**: Group by `sessionId`, sum costs. Include subagent costs (from subagent JSONL files that share the same `sessionId`).
- **Granularity**: per-session
- **Confidence**: MEDIUM
- **Privacy**: SAFE
- **Dashboard potential**: "Most expensive session: $12.40", session cost histogram
- **Priority**: P0

#### `compaction_count`
- **Description**: How many times the conversation was compacted in a session
- **Source**: `type: "system"`, `subtype: "compact_boundary"` entries
- **Calculation**: Count `compact_boundary` entries per session
- **Granularity**: per-session, summed at day/project/all-time
- **Confidence**: HIGH
- **Privacy**: SAFE
- **Dashboard potential**: "Compacted 23 times", "Average context before compaction: 145K tokens"
- **Priority**: P1

#### `pre_compaction_tokens`
- **Description**: Token count just before each compaction event
- **Source**: `compactMetadata.preTokens` on `compact_boundary` entries
- **Calculation**: Direct read. Average/max across compaction events.
- **Granularity**: per-event, aggregated at session/all-time
- **Confidence**: HIGH
- **Privacy**: SAFE
- **Dashboard potential**: "Avg context at compaction: 167K tokens", "You maxed out context 5 times"
- **Priority**: P1

#### `compaction_trigger`
- **Description**: Whether compaction was automatic or manual
- **Source**: `compactMetadata.trigger` — `"auto"` or `"manual"`
- **Calculation**: Direct read, count by trigger type
- **Granularity**: per-event, aggregated
- **Confidence**: HIGH
- **Privacy**: SAFE
- **Dashboard potential**: "Auto-compacted 18 times, manually compacted 5 times"
- **Priority**: P2

#### `session_error_rate`
- **Description**: API errors per session
- **Source**: `type: "system"`, `subtype: "api_error"` entries
- **Calculation**: Count `api_error` entries per session
- **Granularity**: per-session, averaged across sessions
- **Confidence**: HIGH
- **Privacy**: SAFE
- **Dashboard potential**: "Error-free sessions: 87%", "Worst session had 12 API errors"
- **Priority**: P1

#### `interruption_count`
- **Description**: How many times the user interrupted Claude mid-response
- **Source**: User entries where `message.content` contains a text block with `"[Request interrupted by user]"`
- **Calculation**: Count user entries matching the interruption pattern. Note: we check for the specific string `"[Request interrupted by user]"` in the `type` field structure, not in content text — this is a system-generated marker.
- **Granularity**: per-session, summed at day/project
- **Confidence**: HIGH
- **Privacy**: SAFE (this is a system-generated marker, not user content)
- **Dashboard potential**: "You interrupted Claude 47 times", "Patience score: 3/10"
- **Priority**: P1

#### `session_slug`
- **Description**: Human-readable session name assigned by Claude Code
- **Source**: `slug` field (e.g., `"peppy-seeking-brooks"`)
- **Calculation**: Direct read, take from any entry in the session (appears after first few entries)
- **Granularity**: per-session
- **Confidence**: HIGH
- **Privacy**: SAFE (auto-generated name, not user content)
- **Dashboard potential**: Session labels in lists/tables
- **Priority**: P1

---

### 2.6 Subagent Analytics

#### `subagent_count`
- **Description**: Number of subagents spawned in a session
- **Source**: Filesystem: count files in `{sessionId}/subagents/agent-*.jsonl` (excluding `agent-acompact-*`)
- **Calculation**: Count matching files per session directory. Alternatively, count distinct `agentId` values across entries with `isSidechain: true`.
- **Granularity**: per-session, summed at day/project/all-time
- **Confidence**: HIGH
- **Privacy**: SAFE
- **Dashboard potential**: "Claude spawned 142 sub-agents", "Avg 3.2 subagents per session"
- **Priority**: P0

#### `subagent_compact_count`
- **Description**: Number of subagents that were compacted (long-running enough to need summarization)
- **Source**: Filesystem: count `agent-acompact-*.jsonl` files per session
- **Calculation**: Count matching files
- **Granularity**: per-session, summed at day/project
- **Confidence**: HIGH
- **Privacy**: SAFE
- **Dashboard potential**: "23 subagents ran long enough to compact"
- **Priority**: P2

#### `subagent_total_cost`
- **Description**: Total cost incurred by subagents within a session
- **Source**: Parse subagent JSONL files, sum `cost_usd` from deduplicated assistant messages
- **Calculation**: Same as main session cost calculation, applied to each subagent file. Sum across all subagents for a session.
- **Granularity**: per-session, summed at day/project
- **Confidence**: MEDIUM (requires parsing additional files)
- **Privacy**: SAFE
- **Dashboard potential**: "Subagents cost $34 (28% of total)", cost delegation pie chart
- **Priority**: P1

#### `subagent_duration`
- **Description**: Duration of a subagent's execution
- **Source**: `toolUseResult.totalDurationMs` on the user entry that receives the subagent completion, OR timestamp span within subagent JSONL file
- **Calculation**: Prefer `totalDurationMs` if available. Fallback: `max(timestamp) - min(timestamp)` in the subagent file.
- **Granularity**: per-subagent, aggregated at session
- **Confidence**: MEDIUM
- **Privacy**: SAFE
- **Dashboard potential**: "Longest subagent ran for 8m 23s"
- **Priority**: P2

#### `subagent_cost_ratio`
- **Description**: Percentage of session cost delegated to subagents
- **Source**: Derived from `subagent_total_cost` / `session_cost`
- **Calculation**: `subagent_total_cost / session_cost * 100`
- **Granularity**: per-session, averaged
- **Confidence**: MEDIUM
- **Privacy**: SAFE
- **Dashboard potential**: "Subagent delegation: 28%"
- **Priority**: P2

---

### 2.7 Cache Performance

#### `cache_hit_rate`
- **Description**: Ratio of cache reads to total input tokens (how much context was served from cache)
- **Source**: Derived from `message.usage`
- **Calculation**: `cache_read_input_tokens / (input_tokens + cache_creation_input_tokens + cache_read_input_tokens) * 100`. Computed per deduplicated message, then averaged or computed at aggregate level.
- **Granularity**: per-turn, averaged at session/day/project
- **Confidence**: HIGH
- **Privacy**: SAFE
- **Dashboard potential**: "Cache hit rate: 87%", trend line over time
- **Priority**: P1

#### `cache_creation_ratio`
- **Description**: Ratio of cache creation tokens to total cached tokens
- **Source**: Derived from `message.usage`
- **Calculation**: `cache_creation_input_tokens / (cache_creation_input_tokens + cache_read_input_tokens) * 100`
- **Granularity**: per-turn, averaged at session/day
- **Confidence**: HIGH
- **Privacy**: SAFE
- **Dashboard potential**: "Cache efficiency: 12% creation, 88% reads" — lower creation = better reuse
- **Priority**: P2

#### `cache_savings_usd`
- **Description**: Estimated cost saved by cache reads vs full input pricing
- **Source**: Derived from `cache_read_input_tokens` * (full input price - cache read price)
- **Calculation**: `cache_read_input_tokens * (price_per_input_token - price_per_cache_read_token)` per model
- **Granularity**: session, day, project, all-time
- **Confidence**: MEDIUM (depends on pricing table accuracy)
- **Privacy**: SAFE
- **Dashboard potential**: "Cache saved you $127 this month", "You'd have spent $890 without caching"
- **Priority**: P1

#### `ephemeral_cache_5m_tokens`
- **Description**: Tokens written to 5-minute ephemeral cache
- **Source**: `message.usage.cache_creation.ephemeral_5m_input_tokens`
- **Calculation**: Sum across deduplicated messages
- **Granularity**: turn, session, day
- **Confidence**: HIGH
- **Privacy**: SAFE
- **Dashboard potential**: Cache tier breakdown chart
- **Priority**: P2

#### `ephemeral_cache_1h_tokens`
- **Description**: Tokens written to 1-hour ephemeral cache
- **Source**: `message.usage.cache_creation.ephemeral_1h_input_tokens`
- **Calculation**: Sum across deduplicated messages
- **Granularity**: turn, session, day
- **Confidence**: HIGH
- **Privacy**: SAFE
- **Dashboard potential**: Cache tier breakdown chart
- **Priority**: P2

---

### 2.8 Model Switching

#### `models_per_session`
- **Description**: How many distinct models were used in a single session
- **Source**: `message.model` across deduplicated assistant entries per session
- **Calculation**: `count(distinct message.model)` per session, filtering out `"<synthetic>"`
- **Granularity**: per-session, averaged
- **Confidence**: HIGH
- **Privacy**: SAFE
- **Dashboard potential**: "Used 3 models in this session", model mix badge
- **Priority**: P1

#### `model_switches`
- **Description**: Number of times the model changed between consecutive turns within a session
- **Source**: Ordered sequence of `message.model` per session (by timestamp)
- **Calculation**: Count transitions where `model[i] != model[i-1]` in the ordered sequence of deduplicated assistant messages. Filter out `"<synthetic>"`.
- **Granularity**: per-session, summed/averaged
- **Confidence**: HIGH
- **Privacy**: SAFE
- **Dashboard potential**: "Switched models 7 times this session", model timeline visualization
- **Priority**: P2

#### `model_distribution`
- **Description**: Percentage of tokens/cost per model
- **Source**: Group `total_tokens` and `cost_usd` by `message.model`
- **Calculation**: Group and compute percentages
- **Granularity**: session, day, project, all-time
- **Confidence**: HIGH
- **Privacy**: SAFE
- **Dashboard potential**: Pie chart, "Opus: 72% of spend, Sonnet: 25%, Haiku: 3%"
- **Priority**: P0

---

### 2.9 Temporal Patterns

#### `hour_of_day`
- **Description**: Distribution of activity by hour of day (local time)
- **Source**: `timestamp` field on user entries (representing when the developer was active)
- **Calculation**: Parse ISO 8601 timestamp, convert to local timezone, bucket by hour (0-23). Count entries per hour.
- **Granularity**: all-time distribution
- **Confidence**: HIGH
- **Privacy**: SAFE
- **Dashboard potential**: Heatmap, "Peak coding hours: 2-4 AM", "Night owl" badge
- **Priority**: P0

#### `day_of_week`
- **Description**: Distribution of activity by day of week
- **Source**: `timestamp` field on user entries
- **Calculation**: Parse timestamp, determine day of week, count entries per day
- **Granularity**: all-time distribution
- **Confidence**: HIGH
- **Privacy**: SAFE
- **Dashboard potential**: Weekly heatmap, "Busiest day: Tuesday", "Weekend warrior" badge
- **Priority**: P0

#### `daily_cost`
- **Description**: Cost per calendar day
- **Source**: Derived from `cost_usd` grouped by date
- **Calculation**: Group deduplicated assistant messages by date (from timestamp), sum cost
- **Granularity**: per-day
- **Confidence**: MEDIUM
- **Privacy**: SAFE
- **Dashboard potential**: Daily cost chart, streak display, "Most expensive day: $87"
- **Priority**: P0

#### `coding_streak`
- **Description**: Consecutive days with at least one session
- **Source**: Derived from `timestamp` on user entries
- **Calculation**: Extract unique dates, find longest consecutive sequence. Current streak = consecutive days ending today/yesterday.
- **Granularity**: all-time
- **Confidence**: HIGH
- **Privacy**: SAFE
- **Dashboard potential**: "Current streak: 12 days", "Longest streak: 34 days", fire badge
- **Priority**: P0

#### `sessions_per_day`
- **Description**: Number of distinct sessions per calendar day
- **Source**: Count distinct `sessionId` values per date
- **Calculation**: Group by date, count unique session IDs
- **Granularity**: per-day, averaged
- **Confidence**: HIGH
- **Privacy**: SAFE
- **Dashboard potential**: "Avg 4.2 sessions/day", "Most active day: 11 sessions"
- **Priority**: P1

#### `active_hours_per_day`
- **Description**: Estimated coding hours per day (based on session activity spans)
- **Source**: Derived from session start/end timestamps per day
- **Calculation**: Sum session durations per day. Cap individual sessions at reasonable maximum (e.g., 4 hours) to handle sessions left open overnight.
- **Granularity**: per-day, averaged
- **Confidence**: LOW (sessions left open skew this; no heartbeat signal)
- **Privacy**: SAFE
- **Dashboard potential**: "Coded 6.3 hours today", weekly hours chart
- **Priority**: P2

---

### 2.10 Developer Identity

#### `git_branch`
- **Description**: Git branch the developer was working on
- **Source**: `gitBranch` top-level field
- **Calculation**: Direct read. Group sessions/cost by branch name.
- **Granularity**: per-entry, aggregated at session/project
- **Confidence**: HIGH
- **Privacy**: CAREFUL (branch names may reveal project features — but standard in git workflows, not content)
- **Dashboard potential**: "Most expensive branch: feat/auth — $234", branch cost breakdown, "Worked on 12 branches"
- **Priority**: P1

#### `cost_per_branch`
- **Description**: Total cost grouped by git branch
- **Source**: Derived from `gitBranch` + `cost_usd`
- **Calculation**: Group deduplicated assistant messages by `gitBranch`, sum cost
- **Granularity**: project, all-time
- **Confidence**: MEDIUM (branch may change mid-session; we attribute to branch at time of entry)
- **Privacy**: CAREFUL
- **Dashboard potential**: Branch cost leaderboard, "feat/auth cost you $234"
- **Priority**: P1

#### `cli_version`
- **Description**: Claude Code CLI version being used
- **Source**: `version` top-level field (e.g., `"2.1.50"`)
- **Calculation**: Direct read. Track version over time, detect upgrades.
- **Granularity**: per-entry, aggregated at session/day
- **Confidence**: HIGH
- **Privacy**: SAFE
- **Dashboard potential**: "Currently on v2.1.50", version timeline, "Upgraded 3 times this month"
- **Priority**: P2

#### `plan_mode_usage`
- **Description**: How often the developer uses plan mode
- **Source**: `tool_use` blocks with `name == "EnterPlanMode"` and `name == "ExitPlanMode"`
- **Calculation**: Count `EnterPlanMode` invocations. Duration = timestamp delta between Enter and Exit pairs (if matchable).
- **Granularity**: per-session, summed at day/project
- **Confidence**: HIGH (count), LOW (duration pairing)
- **Privacy**: SAFE (we count invocations, never read plan content)
- **Dashboard potential**: "Used plan mode 12 times", "Plans before codes" badge
- **Priority**: P1

#### `working_directory`
- **Description**: Project working directory (used for project grouping)
- **Source**: `cwd` top-level field
- **Calculation**: Direct read. Group sessions by `cwd` for project-level aggregation.
- **Granularity**: per-entry
- **Confidence**: HIGH
- **Privacy**: CAREFUL (file paths reveal project structure — but needed for project grouping; never exposed to other users in social layer)
- **Dashboard potential**: Used internally for project grouping, never displayed raw
- **Priority**: P0

---

### 2.11 Web Usage

#### `web_search_count`
- **Description**: Number of web search requests made by the model (server-side)
- **Source**: `message.usage.server_tool_use.web_search_requests`
- **Calculation**: Sum across deduplicated assistant messages. Note: this counts server-side searches (the API-level count), not the WebSearch tool invocations.
- **Granularity**: turn, session, day, project, all-time
- **Confidence**: HIGH
- **Privacy**: SAFE (count only, no query content)
- **Dashboard potential**: "Claude searched the web 89 times for you"
- **Priority**: P1

#### `web_fetch_count`
- **Description**: Number of web fetch requests made by the model (server-side)
- **Source**: `message.usage.server_tool_use.web_fetch_requests`
- **Calculation**: Sum across deduplicated assistant messages
- **Granularity**: turn, session, day, project, all-time
- **Confidence**: HIGH
- **Privacy**: SAFE
- **Dashboard potential**: "Fetched 34 web pages"
- **Priority**: P1

#### `web_tool_calls`
- **Description**: Count of client-side WebSearch and WebFetch tool invocations
- **Source**: `tool_use` blocks with `name == "WebSearch"` or `name == "WebFetch"`
- **Calculation**: Count after dedup, grouped by tool name
- **Granularity**: session, day, project, all-time
- **Confidence**: HIGH
- **Privacy**: SAFE
- **Dashboard potential**: Comparison with server-side counts, "Research-heavy sessions"
- **Priority**: P1

#### `web_search_duration`
- **Description**: Duration of web search tool executions
- **Source**: `toolUseResult.durationSeconds` on WebSearch tool results
- **Calculation**: Sum or average `durationSeconds`
- **Granularity**: per-event, aggregated
- **Confidence**: MEDIUM (only when toolUseResult is present)
- **Privacy**: SAFE
- **Dashboard potential**: "Spent 45s searching the web"
- **Priority**: P2

---

### 2.12 Error & Retry

#### `api_error_count`
- **Description**: Total API errors encountered
- **Source**: `type: "system"`, `subtype: "api_error"` entries
- **Calculation**: Count per session/day. Also count entries with `isApiErrorMessage: true`.
- **Granularity**: per-session, summed at day/project/all-time
- **Confidence**: HIGH
- **Privacy**: SAFE
- **Dashboard potential**: "12 API errors this month", error rate trend
- **Priority**: P1

#### `api_error_types`
- **Description**: Breakdown of API errors by error code
- **Source**: `error.cause.code` on `api_error` entries (e.g., `"ConnectionRefused"`)
- **Calculation**: Group by `error.cause.code`, count per type
- **Granularity**: all-time distribution
- **Confidence**: HIGH
- **Privacy**: SAFE (error codes are generic, not content)
- **Dashboard potential**: "Top error: ConnectionRefused (8), RateLimit (3)"
- **Priority**: P2

#### `retry_count`
- **Description**: Total retry attempts across all API errors
- **Source**: `retryAttempt` field on `api_error` entries
- **Calculation**: Sum `retryAttempt` values, or count entries where `retryAttempt > 0`
- **Granularity**: per-session, summed
- **Confidence**: HIGH
- **Privacy**: SAFE
- **Dashboard potential**: "Retried 47 times", retry success rate
- **Priority**: P2

#### `max_retry_depth`
- **Description**: Deepest retry attempt reached in a single error sequence
- **Source**: `retryAttempt` and `maxRetries` on `api_error` entries
- **Calculation**: `max(retryAttempt)` across all api_error entries. Track if it ever reached `maxRetries`.
- **Granularity**: per-session, max across all
- **Confidence**: HIGH
- **Privacy**: SAFE
- **Dashboard potential**: "Deepest retry: 6/10", "Hit max retries: 2 times"
- **Priority**: P2

#### `retry_backoff_total_ms`
- **Description**: Total time spent waiting for retries
- **Source**: `retryInMs` on `api_error` entries
- **Calculation**: Sum `retryInMs` across all retry entries. Shows exponential backoff pattern.
- **Granularity**: per-session, summed
- **Confidence**: HIGH
- **Privacy**: SAFE
- **Dashboard potential**: "Spent 47s waiting for retries"
- **Priority**: P2

#### `tool_error_count`
- **Description**: Count of tool executions that returned errors
- **Source**: `message.content[type=tool_result].is_error == true` on user entries
- **Calculation**: Count tool_result blocks where `is_error` is `true`
- **Granularity**: per-session, summed at day/project
- **Confidence**: HIGH
- **Privacy**: SAFE (only read `is_error` flag)
- **Dashboard potential**: "42 tool errors", "Error rate: 3.1%"
- **Priority**: P1

#### `error_free_sessions_pct`
- **Description**: Percentage of sessions with zero API errors
- **Source**: Derived from `api_error_count`
- **Calculation**: `count(sessions with 0 api_errors) / count(all sessions) * 100`
- **Granularity**: all-time, per-project
- **Confidence**: HIGH
- **Privacy**: SAFE
- **Dashboard potential**: "87% error-free sessions", reliability badge
- **Priority**: P1

---

## 3. Implementation Priority

### P0 — Ship First

Easy to extract (direct field reads), high viral/social potential. These form the MVP metric set.

| Metric | Category | Why P0 |
|--------|----------|--------|
| `input_tokens` | Cost & Tokens | Core billing, already implemented |
| `output_tokens` | Cost & Tokens | Core billing, already implemented |
| `cache_creation_tokens` | Cost & Tokens | Core billing, already implemented |
| `cache_read_tokens` | Cost & Tokens | Core billing, already implemented |
| `total_tokens` | Cost & Tokens | Hero number, already implemented |
| `cost_usd` | Cost & Tokens | Main KPI, already implemented |
| `model_name` | Cost & Tokens | Model distribution, already implemented |
| `cost_per_model` | Cost & Tokens | Model breakdown, already implemented |
| `permission_mode` | YOLO Mode | Direct field, massive viral potential ("32% YOLO") |
| `yolo_session_pct` | YOLO Mode | Simple derivation, great scorecard stat |
| `yolo_sessions` | YOLO Mode | Count, leaderboard material |
| `tool_calls` | Tool Usage | Direct from content blocks, "Claude used 847 tools" |
| `bash_runs` | Tool Usage | Subset of above, "Ran 312 commands" |
| `edit_operations` | Tool Usage | Subset of above, "Made 156 edits" |
| `write_operations` | Tool Usage | Subset of above, "Wrote 42 files" |
| `turn_duration_ms` | Turn Economics | Direct field, "Avg turn: 12.4s" |
| `session_duration` | Session Intelligence | Timestamp math, "Longest session: 4h" |
| `session_turn_count` | Session Intelligence | Count, "12 turns per session" |
| `session_cost` | Session Intelligence | Sum, "Most expensive session: $12" |
| `subagent_count` | Subagent Analytics | File count, "Spawned 142 sub-agents" |
| `model_distribution` | Model Switching | Already partially implemented |
| `hour_of_day` | Temporal Patterns | Timestamp parse, "Night owl" badge |
| `day_of_week` | Temporal Patterns | Timestamp parse, weekly heatmap |
| `daily_cost` | Temporal Patterns | Already implemented |
| `coding_streak` | Temporal Patterns | Date math, "12-day streak" |
| `working_directory` | Developer Identity | Already used for project grouping |

### P1 — Ship Second

Moderate extraction complexity, solid dashboard value. Build after P0 is stable.

| Metric | Category | Extraction Notes |
|--------|----------|-----------------|
| `yolo_cost` | YOLO Mode | Requires session-level correlation |
| `tool_error_rate` | Tool Usage | Match tool_result.is_error back to tool_use |
| `files_touched` | Tool Usage | Requires toolUseResult parsing |
| `lines_read` | Tool Usage | Requires toolUseResult parsing |
| `turn_cost` | Turn Economics | Requires turn boundary detection |
| `turn_token_breakdown` | Turn Economics | Per-turn grouping |
| `thinking_presence` | Turn Economics | Content block type scan |
| `compaction_count` | Session Intelligence | Count system entries by subtype |
| `pre_compaction_tokens` | Session Intelligence | Read compactMetadata |
| `session_error_rate` | Session Intelligence | Count api_error entries |
| `interruption_count` | Session Intelligence | Pattern match on user content type |
| `session_slug` | Session Intelligence | Direct field |
| `subagent_total_cost` | Subagent Analytics | Parse subagent files |
| `cache_hit_rate` | Cache Performance | Derived from usage fields |
| `cache_savings_usd` | Cache Performance | Requires pricing math |
| `models_per_session` | Model Switching | Distinct count |
| `sessions_per_day` | Temporal Patterns | Group by date |
| `git_branch` | Developer Identity | Direct field |
| `cost_per_branch` | Developer Identity | Group by branch |
| `plan_mode_usage` | Developer Identity | Tool name count |
| `web_search_count` | Web Usage | Direct from usage.server_tool_use |
| `web_fetch_count` | Web Usage | Direct from usage.server_tool_use |
| `web_tool_calls` | Web Usage | Tool name count |
| `api_error_count` | Error & Retry | Count system entries |
| `tool_error_count` | Error & Retry | Count is_error flags |
| `error_free_sessions_pct` | Error & Retry | Derived percentage |

### P2 — Ship Later

Complex extraction, nice-to-have, or niche appeal.

| Metric | Category | Why P2 |
|--------|----------|--------|
| `grep_matches` | Tool Usage | Niche, requires toolUseResult parsing |
| `skill_invocations` | Tool Usage | Low volume, matching complexity |
| `tokens_per_second` | Turn Economics | Unreliable (includes tool time) |
| `compaction_trigger` | Session Intelligence | Minor detail |
| `subagent_compact_count` | Subagent Analytics | Niche |
| `subagent_duration` | Subagent Analytics | Matching complexity |
| `subagent_cost_ratio` | Subagent Analytics | Derived from P1 metrics |
| `cache_creation_ratio` | Cache Performance | Niche optimization stat |
| `ephemeral_cache_5m_tokens` | Cache Performance | Deep cache analysis |
| `ephemeral_cache_1h_tokens` | Cache Performance | Deep cache analysis |
| `model_switches` | Model Switching | Requires ordered sequence analysis |
| `active_hours_per_day` | Temporal Patterns | Unreliable without heartbeat |
| `cli_version` | Developer Identity | Low viral potential |
| `web_search_duration` | Web Usage | Niche |
| `api_error_types` | Error & Retry | Niche |
| `retry_count` | Error & Retry | Niche |
| `max_retry_depth` | Error & Retry | Niche |
| `retry_backoff_total_ms` | Error & Retry | Niche |

---

## 4. Gotchas

Critical implementation pitfalls learned from the spike analysis. Every one of these has burned hours before.

### 4.1 Streaming Deduplication

**Problem**: A single API response produces N JSONL entries (observed up to 20) sharing the same `message.id`. Each streaming entry carries the **full `usage` object duplicated**. Without dedup, token counts inflate 2-3x.

**Solution**: Group assistant entries by `message.id` (or `requestId` as fallback). Take `usage` from any one entry (they are identical). Concatenate content blocks across all entries (they are incremental, not cumulative).

**Key**: The dedup key is `message.id:requestId`. Entries with `null` for both always pass through.

### 4.2 permissionMode Lives on User Entries

**Problem**: `permissionMode` is a top-level field on `type: "user"` entries, NOT on `type: "assistant"` entries. If you only parse assistant entries for metrics, you'll miss YOLO mode entirely.

**Solution**: Parse user entries for `permissionMode`. Associate with the session via `sessionId`. A session is "YOLO" if ANY user entry has `permissionMode: "bypassPermissions"`.

### 4.3 stop_reason is Null on Streaming Entries

**Problem**: `message.stop_reason` is `null` on ALL streaming entries, even the final one. The spike found `end_turn` was never observed — only `tool_use` and `stop_sequence` appear, and only on non-streamed or final-state entries.

**Solution**: Do not rely on `stop_reason` to detect the "final" streaming entry. Instead, use `message.id` grouping. To detect tool_use vs text completion, check if the merged content blocks contain a `tool_use` type block.

### 4.4 `<synthetic>` Model Must Be Filtered

**Problem**: Compact summary entries use `message.model: "<synthetic>"` with zero or meaningless usage values. Including these inflates message counts and corrupts model distribution stats.

**Solution**: Filter out all entries where `message.model == "<synthetic>"`. Also filter entries with `isCompactSummary: true`.

### 4.5 Subagent Files Have Different First-Entry Pattern

**Problem**: Main session files start with a `file-history-snapshot` entry. Subagent files start with a `user` entry (tool_result or compact prompt). If your parser assumes a specific first-entry structure, it will break on subagent files.

**Solution**: Check `isSidechain` and `agentId` fields. Don't assume entry order. Subagent files share the parent's `sessionId` — so group them with the parent session for cost aggregation.

### 4.6 tool_use Blocks Span Multiple Streaming Entries

**Problem**: A single tool_use invocation may appear as a content block in one streaming entry, while the preceding text block was in an earlier entry. If you process entries independently, you'll get partial tool usage counts.

**Solution**: Always merge content blocks across all streaming entries of the same `message.id` before counting tools. The merged content array represents the complete response.

### 4.7 turn_duration is a Separate System Entry

**Problem**: Turn duration is NOT a field on the assistant message entry. It's a completely separate `type: "system"`, `subtype: "turn_duration"` entry with its own `uuid`. It appears after the assistant turn completes.

**Solution**: Match `turn_duration` entries to their corresponding turn by position in the `parentUuid` chain or by timestamp proximity. The `turn_duration` entry's `parentUuid` may point to the last entry of the completed turn.

### 4.8 Content Blocks Are Incremental

**Problem**: Across streaming entries of the same `message.id`, content blocks are INCREMENTAL (each entry adds new blocks), NOT cumulative (each entry does not contain all previous blocks). This is different from how many streaming APIs work.

**Solution**: When merging: concatenate all content arrays. Do not deduplicate content blocks within a merged message — each one is unique.

### 4.9 Usage Values Are Duplicated Across Streaming Entries

**Problem**: Unlike content blocks (which are incremental), `usage` values are IDENTICAL across all streaming entries of the same message. This is the opposite of what you'd expect.

**Solution**: Take `usage` from ANY single entry in the streaming group. Do NOT sum usage across streaming entries of the same message.

### 4.10 User Entries Serve Double Duty

**Problem**: `type: "user"` entries include both human messages (the developer typing a prompt) AND tool results (automated responses from tool executions). They look the same at the type level.

**Solution**: Distinguish by content structure:
- Human message: `message.content` is a plain string
- Tool result: `message.content` is an array containing `tool_result` blocks
- Also: tool result entries have `sourceToolAssistantUUID` linking them to the assistant that requested the tool

### 4.11 Compact Subagent Files Are Separate

**Problem**: A compacted subagent generates BOTH a regular `agent-a<hash>.jsonl` file AND a compact `agent-acompact-<hash>.jsonl` file. The compact file contains the summary conversation, not a replacement.

**Solution**: Parse both files. The regular file contains the actual work (with real usage data). The compact file contains the summarization overhead. For cost accounting, both contribute to the session's total.

### 4.12 Timezone Handling

**Problem**: All timestamps in JSONL are UTC (ISO 8601 with `Z` suffix). For temporal patterns (hour-of-day, day-of-week), you need the developer's local timezone.

**Solution**: For the local dashboard (Layer 1), use the system's local timezone for display. For the social layer (Layer 2), the push payload should include the timezone offset or let the server normalize. Never store local times in the database — always store UTC and convert at display time.

### 4.13 Sessions with Zero Assistant Entries

**Problem**: Some sessions crash immediately: they contain only a `file-history-snapshot`, a user message, and `api_error` entries. These have zero cost but are valid sessions.

**Solution**: Don't skip sessions with zero assistant entries. Count them for session stats (session count, error rate) but exclude from cost/token averages to avoid pulling down means.

### 4.14 Queue Operations Contain User Content

**Problem**: `queue-operation` entries have a `content` field that contains the queued message text — this is actual user content.

**Solution**: Never extract `content` from `queue-operation` entries. Only extract `operation` type (`"enqueue"`, `"dequeue"`, etc.) for queue activity metrics if needed.

### 4.15 File History Snapshots Contain File Contents

**Problem**: `file-history-snapshot` entries with `isSnapshotUpdate: true` contain actual file contents in `snapshot.trackedFileBackups`.

**Solution**: Never parse `snapshot.trackedFileBackups` values. Only count snapshot entries if tracking undo frequency.

### 4.16 Multiple permissionMode Values Exist

**Problem**: `permissionMode` isn't binary (YOLO vs not). Observed values include: `"default"`, `"acceptEdits"`, `"bypassPermissions"`, `"plan"`, `"dontAsk"`. Each has different permission semantics.

**Solution**: Map values to a permission spectrum for display:
- `"default"` — Normal (asks for all permissions)
- `"acceptEdits"` — Auto-accept file edits
- `"bypassPermissions"` — YOLO (no permission prompts)
- `"plan"` — Plan mode (read-only)
- `"dontAsk"` — Similar to bypass, auto-accept variant

For "YOLO mode" metrics, count only `"bypassPermissions"`. For a full permission breakdown, track all values.

---

## 5. Composite / Derived Metrics (Social Layer)

These metrics combine multiple base metrics for the social profile and leaderboard system. They are not directly extracted from JSONL but computed from the base metrics above.

| Metric | Formula | Social Use |
|--------|---------|------------|
| **Efficiency Score** | `output_tokens / cost_usd` | "Tokens per dollar" leaderboard |
| **YOLO Index** | `yolo_session_pct * yolo_cost / total_cost` | Risk-taker ranking |
| **Delegation Rate** | `subagent_cost / session_cost` | "Subagent delegator" badge |
| **Cache Master Score** | `cache_hit_rate * cache_savings_usd` | Cache optimization ranking |
| **Night Owl Score** | Weighted activity in 00:00-05:00 hours | Time-based badges |
| **Streak Rank** | `current_streak / max(all_users_streaks)` | Consistency leaderboard |
| **Tool Diversity** | Count of distinct tools used / total tools available | "Swiss army knife" badge |
| **Error Resilience** | `error_free_sessions_pct` | "99.9% uptime" badge |
| **Session Intensity** | `session_cost / session_duration_hours` | "Burn rate" — $/hour |
| **Research Ratio** | `(web_search_count + web_fetch_count) / total_turns` | "Researcher" badge |

These composite metrics are computed server-side for the social layer (Layer 2) and are the basis for rankings, badges, and the RPG system.

---

## 6. Push Payload Mapping

When a user publishes stats via `myburn push`, only aggregated numbers are sent. This table maps each metric to its push payload field:

| Metric | Push Payload Field | Aggregation |
|--------|-------------------|-------------|
| `total_tokens` | `tokens.total` | Sum for period |
| `input_tokens` | `tokens.input` | Sum |
| `output_tokens` | `tokens.output` | Sum |
| `cache_creation_tokens` | `tokens.cache_creation` | Sum |
| `cache_read_tokens` | `tokens.cache_read` | Sum |
| `cost_usd` | `cost.total` | Sum |
| `cost_per_model` | `cost.by_model` | Dict of model -> cost |
| `model_distribution` | `models.distribution` | Dict of model -> pct |
| `session_count` | `sessions.count` | Count |
| `session_duration` (avg) | `sessions.avg_duration_s` | Average in seconds |
| `turn_count` (total) | `turns.total` | Sum |
| `turn_duration_ms` (avg) | `turns.avg_duration_ms` | Average |
| `tool_calls` | `tools.by_name` | Dict of tool -> count |
| `yolo_session_pct` | `yolo.session_pct` | Percentage |
| `subagent_count` | `subagents.total` | Sum |
| `coding_streak` | `streaks.current` | Current |
| `coding_streak` (max) | `streaks.longest` | All-time max |
| `hour_of_day` | `temporal.hours` | Array of 24 counts |
| `day_of_week` | `temporal.days` | Array of 7 counts |
| `compaction_count` | `compactions.total` | Sum |
| `api_error_count` | `errors.api_total` | Sum |
| `web_search_count` | `web.searches` | Sum |
| `cache_hit_rate` | `cache.hit_rate_pct` | Average |

No content, file paths, branch names, prompts, or code are ever included in the push payload.

---

## 7. Quick Reference — Field to Metric Map

For parser implementation, this reverse index maps JSONL fields to the metrics that consume them:

| JSONL Field | Metrics |
|-------------|---------|
| `type` | All (entry filtering) |
| `subtype` | `compaction_count`, `api_error_count`, `turn_duration_ms` |
| `timestamp` | `session_duration`, `hour_of_day`, `day_of_week`, `daily_cost`, `coding_streak` |
| `sessionId` | All session-level metrics |
| `message.id` | Deduplication key |
| `message.model` | `model_name`, `model_distribution`, `cost_per_model`, `models_per_session`, `model_switches` |
| `message.usage.*` | All token and cost metrics, `cache_hit_rate`, `cache_savings_usd` |
| `message.usage.server_tool_use.*` | `web_search_count`, `web_fetch_count` |
| `message.content[].type` | `tool_calls`, `thinking_presence`, content block routing |
| `message.content[].name` | `tool_calls`, `bash_runs`, `edit_operations`, `write_operations`, `plan_mode_usage`, `web_tool_calls`, `skill_invocations` |
| `message.content[].is_error` | `tool_error_count`, `tool_error_rate` |
| `requestId` | Deduplication fallback key |
| `permissionMode` | `permission_mode`, `yolo_session_pct`, `yolo_sessions`, `yolo_cost` |
| `durationMs` | `turn_duration_ms` |
| `isSidechain` | `subagent_count`, subagent filtering |
| `agentId` | `subagent_count`, subagent grouping |
| `gitBranch` | `git_branch`, `cost_per_branch` |
| `version` | `cli_version` |
| `slug` | `session_slug` |
| `compactMetadata.*` | `compaction_count`, `pre_compaction_tokens`, `compaction_trigger` |
| `retryAttempt` | `retry_count`, `max_retry_depth` |
| `retryInMs` | `retry_backoff_total_ms` |
| `error.cause.code` | `api_error_types` |
| `toolUseResult.numFiles` | `files_touched` |
| `toolUseResult.numLines` | `lines_read` |
| `toolUseResult.numMatches` | `grep_matches` |
| `toolUseResult.totalDurationMs` | `subagent_duration` |
| `toolUseResult.durationSeconds` | `web_search_duration` |
| `toolUseResult.commandName` | `skill_invocations` |
| `cwd` | `working_directory` |
