token_dashboard: ingester core + detail tables (calls, marks, turn_detail, ab_rows) for the control-panel dashboard
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (mypy, mypy) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (pylint, pylint) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (rest, ) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (shellcheck, shellcheck) (push) Waiting to run
Python tests / test (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / lib-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JThjboXTeZ3yJPBzxMx5wS
This commit is contained in:
Krzysztof Rudnicki 2026-10-18 14:45:06 +02:00
parent 6e63b66ff4
commit 1ba202c567
54 changed files with 5829 additions and 15 deletions

View File

@ -0,0 +1,15 @@
{
"title": "token_dashboard: durable token store + ingester (dashboard core)",
"objective": "Ingest Claude Code transcripts and aux sources into a durable, migrated SQLite store that the control-panel /tokens page reads, with an incremental pass under 60 s and 0 LLM tokens.",
"acceptance_criteria": [
"python_pkg/token_dashboard and python_pkg/token_audit stay at 100% line and branch coverage; every file <= 250 lines",
"python3 -m python_pkg.token_dashboard.ingest --budget 40 finishes in under 60 s and is resumable",
"schema.connect migrates a v1/v2 store forward without deleting rows",
"calls/marks/turn_detail rows are replaced, never duplicated, when a transcript is re-walked"
],
"out_of_scope": [
"Remaining /tokens panel groups A-Q",
"Deleting or rebuilding ~/data/token-dashboard/tokens.sqlite"
],
"verifier": "pytest --cov=python_pkg/token_dashboard --cov=python_pkg/token_audit --cov-branch; the ingest command; pre-commit hooks on the staged files"
}

View File

@ -0,0 +1,40 @@
{
"intent": "A durable SQLite token store (~/data/token-dashboard/tokens.sqlite) that outlives Claude Code's transcript cleanup and feeds the control-panel /tokens cost dashboard, with 0 LLM tokens spent.",
"scope": [
"python_pkg/token_dashboard/ (new): walk, rows, store, schema v3 with ordered additive migrations, finalize, carry, efficiency, ab, levers, context_files, aux, commits, survival, blocks, ingest; detail layer: classify, detail, detail_rows, detail_store, marks, hook_join (per-call, per-mark and per-turn rows with creation + carry cost); ab_rows (per-assignment A/B outcomes)",
"python_pkg/token_audit: persist structured lever candidates (Lever.metric join key) and expose the record primitives the walker shares",
"Non-goal: the remaining dashboard panel groups A-Q (tracked in ~/src/control-panel/TODO-token-dashboard-panels.md)"
],
"changes": [
"Resumable incremental ingest: walks changed transcripts until its --budget is spent, commits, exits pending if any remain",
"Schema migrations never drop the store; rows of vanished transcripts are kept",
"token_audit --recommend stores the lever candidates as data in the snapshot (snap['levers'])",
"New raw tables calls / marks / turn_detail (no schema version bump: CREATE IF NOT EXISTS) with key, cwd, model, ts, day, is_sub so the dashboard's filter clause applies; carry.Tracker settles creation (next call's cache-write multiplier x input price) and carry_calls too",
"ab.rebuild also stores per-assignment rows in ab_rows so the A/B panel can recount arms per cwd/model/scope"
],
"verification": [
{
"command": "python3 -m pytest -q python_pkg/token_dashboard python_pkg/token_audit --cov=python_pkg/token_dashboard --cov=python_pkg/token_audit --cov-branch",
"result": "pass",
"evidence": "token_dashboard + token_audit: 100.00% line+branch coverage (pytest --cov, --cov-branch)"
},
{
"command": "python3 -m python_pkg.token_dashboard.ingest --budget 40",
"result": "pass",
"evidence": "token_dashboard: parsed 7 files, refreshed 2 sessions in 0.7s; exit 0"
},
{
"command": "sqlite3 tokens.sqlite 'DELETE FROM files' && python3 -m python_pkg.token_dashboard.ingest --budget 40",
"result": "pass",
"evidence": "full re-walk of 1616 transcripts in one 32.5 s pass; 114,599 calls, 103,713 turn_detail rows; store kept (backup tokens.v3-2026-10-18.sqlite)"
}
],
"risks": [
"A schema change without a MIGRATIONS entry would leave old stores without the new column",
"Transcript format drift in Claude Code could make the walker skip new record types"
],
"rollback": [
"git revert this commit; the store file is left untouched and stays readable by the previous code",
"Validate: control-panel /api/tokens/overview still answers from the existing store"
]
}

View File

@ -9,6 +9,7 @@ is worse than no number, because it gets acted on.
from __future__ import annotations
import argparse
from dataclasses import asdict
import json
from pathlib import Path
import sys
@ -141,13 +142,18 @@ def main(argv: list[str] | None = None) -> int:
totals, axes, priced.days
)
previous = history.last_snapshot(args.out)
found = review.candidates(snap, previous) if args.recommend else None
if found is not None:
# Persisted so the dashboard reads proposed levers as data instead of
# re-parsing this run's markdown table.
snap["levers"] = [asdict(lever) for lever in found]
markdown = report.render(totals, axes, window, sessions, snap, None)
markdown += "\n".join(
[
"",
*breakdown.render(priced),
"",
*review.render(snap, previous, levers=args.recommend),
*review.render(snap, previous, levers=args.recommend, found=found),
"",
],
)

View File

@ -203,3 +203,10 @@ def load_session(path: Path, window: records.Window | None = None) -> Session:
elif isinstance(event, ToolCall):
session.tools.append(event)
return session
# Public names for the record primitives, shared with token_dashboard's walker
# (which needs prompt boundaries and timestamps that iter_events drops).
load_line = _load_line
content_blocks = _blocks
pair_result = _pair_result

View File

@ -9,7 +9,7 @@ an ``if`` statement and lives here.
from __future__ import annotations
from dataclasses import dataclass
from dataclasses import dataclass, replace
from typing import Any
# Rule thresholds. Each is the point past which the lever is worth a look,
@ -33,6 +33,22 @@ class Lever:
evidence: str
action: str
est_usd_per_day: float | None = None
# The snapshot metric the lever is meant to move: the join key to ledger
# lines, whose free-text titles never match these.
metric: str = ""
# Snapshot metric each rule's lever targets, by title (titles are fixed).
TITLE_METRIC: dict[str, str] = {
"Opus runs most subagent work": "subagent_opus_usd_share",
"Calls over the context budget": "over_budget_usd_share",
"Sessions started in ~": "home_cwd_usd_share",
"Independent tool calls not batched": "batching_calls_per_msg",
"Large Bash results": "bash_big_result_share",
"Always-loaded context grew": "standing_tokens_est",
"Images held in context": "image_usd_share",
"Unused MCP servers / skills": "standing_tokens_est",
}
def _num(metrics: dict[str, Any], key: str) -> float:
@ -173,7 +189,8 @@ def candidates(
+ _mechanics(metrics, previous)
+ _dead(dead_mcp, dead_skills)
)
return sorted(levers, key=lambda lv: -(lv.est_usd_per_day or 0.0))
tagged = [replace(lv, metric=TITLE_METRIC.get(lv.title, "")) for lv in levers]
return sorted(tagged, key=lambda lv: -(lv.est_usd_per_day or 0.0))
def render(levers: list[Lever]) -> list[str]:

View File

@ -101,25 +101,37 @@ def _ledger(snap: dict[str, Any]) -> list[str]:
return out
def candidates(
snap: dict[str, Any], prev: dict[str, Any] | None
) -> list[recommend.Lever]:
"""Every rule-based lever that fires for this snapshot."""
dead_mcp = [u.name for u in unused.mcp_usage() if u.dead]
dead_skills = [u.name for u in unused.skill_usage() if u.dead]
prev_metrics = (
prev.get("metrics") if prev and prev.get("v", 1) >= SNAPSHOT_VERSION else None
)
return recommend.candidates(snap["metrics"], prev_metrics, dead_mcp, dead_skills)
def render(
snap: dict[str, Any], prev: dict[str, Any] | None, *, levers: bool
snap: dict[str, Any],
prev: dict[str, Any] | None,
*,
levers: bool,
found: list[recommend.Lever] | None = None,
) -> list[str]:
"""Markdown for comparison, ledger and (optionally) candidate levers."""
"""Markdown for comparison, ledger and (optionally) candidate levers.
``found`` is the already-computed candidate list when the caller also
persists it in the snapshot, so the unused-surface scan runs once.
"""
lines = ["## Since the previous analysis", ""]
lines += _comparison(snap["metrics"], prev)
lines += ["", "## What helped (ledger)", ""]
lines += _ledger(snap)
if levers:
dead_mcp = [u.name for u in unused.mcp_usage() if u.dead]
dead_skills = [u.name for u in unused.skill_usage() if u.dead]
prev_metrics = (
prev.get("metrics")
if prev and prev.get("v", 1) >= SNAPSHOT_VERSION
else None
)
found = recommend.candidates(
snap["metrics"], prev_metrics, dead_mcp, dead_skills
)
if found is None:
found = candidates(snap, prev)
lines += ["", "## Candidate levers (rule-based)", ""]
lines += recommend.render(found)
return lines

View File

@ -205,3 +205,28 @@ def test_rerun_starts_where_previous_window_ended(tmp_path: Path) -> None:
assert cli.main(args) == 0
# The first run's window end is now the default start: nothing newer.
assert cli.main(args) == cli.EXIT_NO_DATA
def test_recommend_persists_levers_as_structured_snapshot_data(
tmp_path: Path,
) -> None:
root = tmp_path / "projects"
root.mkdir()
_transcript(root, turns=3)
out = tmp_path / "out"
assert cli.main(["--root", str(root), "--out", str(out), "--recommend"]) == 0
(snap_path,) = (out / "history").glob("*.json")
snap = json.loads(snap_path.read_text(encoding="utf-8"))
assert isinstance(snap["levers"], list)
keys = {"title", "metric", "evidence", "action"}
assert all(keys <= set(lever) for lever in snap["levers"])
def test_without_recommend_no_levers_key(tmp_path: Path) -> None:
root = tmp_path / "projects"
root.mkdir()
_transcript(root, turns=3)
out = tmp_path / "out"
assert cli.main(["--root", str(root), "--out", str(out)]) == 0
(snap_path,) = (out / "history").glob("*.json")
assert "levers" not in json.loads(snap_path.read_text(encoding="utf-8"))

View File

@ -88,3 +88,21 @@ def test_render_table_formats_estimates() -> None:
assert lines[0].startswith("| category")
assert "| $1.50 |" in lines[2]
assert "| - |" in lines[3]
def test_every_rule_title_maps_to_a_snapshot_metric() -> None:
metrics = {
"subagent_usd_share": 0.9,
"subagent_opus_usd_share": 0.9,
"over_budget_usd_share": 0.9,
"home_cwd_usd_share": 0.9,
"batching_calls_per_msg": 1.0,
"bash_big_result_share": 0.9,
"image_usd_share": 0.9,
"standing_tokens_est": 200,
"usd_total": 10,
"usd_per_day": 1,
}
found = recommend.candidates(metrics, {"standing_tokens_est": 100}, ["m"], [])
assert len(found) == len(recommend.TITLE_METRIC)
assert all(lv.metric == recommend.TITLE_METRIC[lv.title] for lv in found)

View File

@ -0,0 +1,5 @@
"""Durable, zero-token store of Claude Code spend for the control-panel /tokens page.
``ingest`` walks transcripts (reusing token_audit's parser, dedup and prices)
into SQLite under ``~/data/token-dashboard/``; the dashboard only reads it.
"""

View File

@ -0,0 +1,243 @@
"""A/B test of judgment subagents: Sonnet arm vs Opus arm, 0 tokens.
Assignments come from ``~/.claude/logs/agent-ab.jsonl`` (one line per
dispatched review/debug/design/plan agent: ``tool_use_id``, ``arm``, ...).
Each is joined on ``tool_use_id`` to the ``Agent`` call the ingester stored,
and judged on outcomes visible in the transcript after the agent returned:
* **redo** -- the parent dispatched another Agent in the same session whose
description shares >= :data:`REDO_JACCARD` of its word set;
* **follow-up** -- the parent sent that agent a ``SendMessage``;
* **error** -- the call errored or the agent was aborted;
* **correction** (noisy) -- the next user prompt opens with a correction
phrase (:data:`CORRECTION`);
* the parent session's end tag.
Rates get 95% Wilson intervals and per-arm differences Newcombe's hybrid
interval. The banner turns RED only when, with >= :data:`MIN_N` per arm, the
Sonnet arm's redo or correction rate is higher and the two Wilson intervals do
not overlap.
"""
from __future__ import annotations
from dataclasses import dataclass
import json
import math
from pathlib import Path
import re
from typing import TYPE_CHECKING, Any
from python_pkg.token_dashboard import ab_rows
if TYPE_CHECKING:
import sqlite3
LOG = Path.home() / ".claude" / "logs" / "agent-ab.jsonl"
Z = 1.96
MIN_N = 20
REDO_JACCARD = 0.6
ERRORS = frozenset({"error", "aborted", "killed", "failed", "cancelled"})
CORRECTION = re.compile(
r"^\W*no\b|\bwrong\b|\bthat'?s not\b|\bthat is not\b|\bincorrect\b"
r"|\brevert\b|\byou missed\b",
re.IGNORECASE,
)
WORD = re.compile(r"[a-z0-9]+")
RATES = ("redo", "followup", "error", "correction")
def wilson(k: int, n: int, z: float = Z) -> tuple[float, float]:
"""95% Wilson score interval for ``k`` successes in ``n`` trials."""
if n == 0:
return 0.0, 1.0
p = k / n
centre = p + z * z / (2 * n)
spread = z * math.sqrt(p * (1 - p) / n + z * z / (4 * n * n))
denom = 1 + z * z / n
return max(0.0, (centre - spread) / denom), min(1.0, (centre + spread) / denom)
def diff_ci(k1: int, n1: int, k2: int, n2: int) -> tuple[float, float, float]:
"""``p1 - p2`` with Newcombe's hybrid-score 95% interval."""
p1, p2 = (k1 / n1 if n1 else 0.0), (k2 / n2 if n2 else 0.0)
l1, u1 = wilson(k1, n1)
l2, u2 = wilson(k2, n2)
diff = p1 - p2
low = diff - math.sqrt((p1 - l1) ** 2 + (u2 - p2) ** 2)
high = diff + math.sqrt((u1 - p1) ** 2 + (p2 - l2) ** 2)
return diff, low, high
def jaccard(a: str, b: str) -> float:
"""Word-set Jaccard similarity of two descriptions."""
left, right = set(WORD.findall(a.lower())), set(WORD.findall(b.lower()))
union = left | right
return len(left & right) / len(union) if union else 0.0
def is_correction(text: str) -> bool:
"""Whether a user prompt reads as correcting the previous answer."""
return CORRECTION.search(text) is not None
@dataclass(frozen=True)
class ArmCounts:
"""What the banner needs from one arm."""
n: int
redo: int
correction: int
def banner(sonnet: ArmCounts, opus: ArmCounts) -> dict[str, str]:
"""RED / GREEN / GREY verdict for the top of every /tokens view."""
n = min(sonnet.n, opus.n)
if n < MIN_N:
return {"level": "grey", "text": f"A/B judgment agents: collecting (n={n})"}
for name in ("redo", "correction"):
ks, ko = getattr(sonnet, name), getattr(opus, name)
low_s, _ = wilson(ks, sonnet.n)
_, high_o = wilson(ko, opus.n)
if ks / sonnet.n > ko / opus.n and low_s > high_o:
return {
"level": "red",
"text": f"QUALITY DROP: Sonnet {name} rate {ks}/{sonnet.n} vs Opus "
f"{ko}/{opus.n} (95% CIs do not overlap)",
}
return {
"level": "green",
"text": f"A/B judgment agents: no detectable drop (n={n})",
}
def _assignments(log: Path) -> list[dict[str, Any]]:
"""Well-formed assignment lines."""
if not log.is_file():
return []
rows = []
for line in log.read_text(encoding="utf-8", errors="replace").splitlines():
try:
row = json.loads(line)
except ValueError:
continue
if isinstance(row, dict) and row.get("tool_use_id") and row.get("arm"):
rows.append(row)
return rows
def _outcome(conn: sqlite3.Connection, tool_use_id: str) -> dict[str, Any] | None:
"""The joined call: cost, duration and the four flags, or ``None``."""
call = conn.execute(
"SELECT key, agent_id, ts, description, result_ts, status FROM agent_calls"
" WHERE tool_use_id = ?",
(tool_use_id,),
).fetchone()
if call is None:
return None
key, agent_id, ts, desc, result_ts, status = call
main = key.split("/", 1)[0]
sub = conn.execute(
"SELECT usd, wall_s, last_ts FROM subagent_link WHERE sub_key = ?",
(f"{main}/agent-{agent_id}",),
).fetchone() or (0.0, (result_ts or ts) - ts, result_ts or ts)
returned = max(result_ts or ts, sub[2] or 0)
later = conn.execute(
"SELECT description FROM agent_calls WHERE key = ? AND ts > ?",
(key, returned),
).fetchall()
followup = conn.execute(
"SELECT COUNT(*) FROM sent_messages WHERE key = ? AND to_agent = ? AND ts > ?",
(key, agent_id, returned),
).fetchone()[0]
nxt = conn.execute(
"SELECT text FROM prompts WHERE key = ? AND ts > ? ORDER BY ts LIMIT 1",
(main, returned),
).fetchone()
tag = conn.execute(
"SELECT final_tag FROM session_summary WHERE key = ?", (main,)
).fetchone()
return {
"key": key,
"ts": ts,
"usd": sub[0] or 0.0,
"duration_s": sub[1] or 0.0,
"redo": any(jaccard(desc, d) >= REDO_JACCARD for (d,) in later),
"followup": followup > 0,
"error": (status or "").lower() in ERRORS,
"correction": nxt is not None and is_correction(nxt[0]),
"tag": (tag[0] if tag else None) or "untagged",
}
def _mean_ci(values: list[float]) -> dict[str, float]:
"""Mean with a normal-approximation 95% interval."""
n = len(values)
if n == 0:
return {"mean": 0.0, "low": 0.0, "high": 0.0}
mean = sum(values) / n
var = sum((v - mean) ** 2 for v in values) / (n - 1) if n > 1 else 0.0
half = Z * math.sqrt(var / n)
return {"mean": mean, "low": mean - half, "high": mean + half}
def _arm(rows: list[dict[str, Any]]) -> dict[str, Any]:
"""Per-arm summary."""
n = len(rows)
out: dict[str, Any] = {"n": n}
for name in RATES:
k = sum(1 for r in rows if r[name])
low, high = wilson(k, n)
out[name] = {"k": k, "rate": k / n if n else None, "low": low, "high": high}
out["usd"] = _mean_ci([r["usd"] for r in rows])
out["duration_s"] = _mean_ci([r["duration_s"] for r in rows])
tags: dict[str, int] = {}
for r in rows:
tags[r["tag"]] = tags.get(r["tag"], 0) + 1
out["tags"] = tags
return out
def rebuild(conn: sqlite3.Connection, log: Path | None = None) -> dict[str, Any]:
"""Join every assignment, summarise both arms, store in ``meta['ab']``."""
joined: dict[str, list[dict[str, Any]]] = {"sonnet": [], "opus": []}
kept: list[tuple[str, str, dict[str, Any]]] = []
unjoined = 0
for row in _assignments(log or LOG):
found = _outcome(conn, str(row["tool_use_id"]))
if found is None or row["arm"] not in joined:
unjoined += 1
continue
joined[row["arm"]].append(found)
kept.append((row["arm"], str(row["tool_use_id"]), found))
ab_rows.store(conn, kept)
arms = {name: _arm(rows) for name, rows in joined.items()}
diffs = {
name: dict(
zip(
("diff", "low", "high"),
diff_ci(
arms["sonnet"][name]["k"],
arms["sonnet"]["n"],
arms["opus"][name]["k"],
arms["opus"]["n"],
),
strict=True,
)
)
for name in RATES
}
counts = {
name: ArmCounts(
arms[name]["n"], arms[name]["redo"]["k"], arms[name]["correction"]["k"]
)
for name in joined
}
payload = {
"arms": arms,
"diffs": diffs,
"unjoined": unjoined,
"banner": banner(counts["sonnet"], counts["opus"]),
}
conn.execute("INSERT OR REPLACE INTO meta VALUES ('ab', ?)", (json.dumps(payload),))
return payload

View File

@ -0,0 +1,56 @@
"""Per-assignment A/B outcomes, so the dashboard can filter the A/B panel.
:func:`ab.rebuild` summarises all assignments into ``meta['ab']`` (the
all-time verdict). This module also keeps one row per joined assignment, with
the parent call's session, cwd, model and scope, so a cwd / model / scope
filter can recount the arms. Derived: replaced wholesale on every rebuild.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
import sqlite3
SCHEMA = """
CREATE TABLE IF NOT EXISTS ab_rows(
tool_use_id TEXT PRIMARY KEY, arm TEXT, key TEXT, main_key TEXT, is_sub INT,
cwd TEXT, model TEXT, ts REAL, usd REAL, duration_s REAL, redo INT,
followup INT, error INT, correction INT, tag TEXT);
"""
def store(
conn: sqlite3.Connection, joined: list[tuple[str, str, dict[str, Any]]]
) -> None:
"""Replace every row with ``(arm, tool_use_id, outcome)`` triples."""
conn.executescript(SCHEMA)
conn.execute("DELETE FROM ab_rows")
for arm, tool_use_id, found in joined:
key = str(found["key"])
where = conn.execute(
"SELECT cwd, model FROM turns WHERE key = ? AND ts <= ?"
" ORDER BY ts DESC LIMIT 1",
(key, found["ts"]),
).fetchone() or (None, None)
conn.execute(
"INSERT OR REPLACE INTO ab_rows VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
(
tool_use_id,
arm,
key,
key.split("/", 1)[0],
int("/" in key),
where[0],
where[1],
found["ts"],
found["usd"],
found["duration_s"],
int(found["redo"]),
int(found["followup"]),
int(found["error"]),
int(found["correction"]),
found["tag"],
),
)

View File

@ -0,0 +1,56 @@
"""Active time: the sum of gaps between consecutive records, ignoring idle gaps.
A gap longer than :data:`IDLE_GAP` contributes **zero** (not the cap): it means
nobody and nothing was working. Storing each file as maximal runs of records
spaced at most ``IDLE_GAP`` apart is lossless for that rule -- the active time
of several files' merged timeline equals the measure of the union of their runs
with gaps of up to ``IDLE_GAP`` between runs bridged. That is what lets a main
session and the subagents it spawned, ingested in different runs, combine
exactly afterwards.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Iterable
IDLE_GAP = 300.0
Span = tuple[float, float]
def intervals(stamps: Iterable[float], gap: float = IDLE_GAP) -> list[Span]:
"""Collapse sorted-or-not timestamps into maximal runs."""
out: list[Span] = []
for stamp in sorted(stamps):
if out and stamp - out[-1][1] <= gap:
out[-1] = (out[-1][0], stamp)
else:
out.append((stamp, stamp))
return out
def measure(
spans: Iterable[Span],
low: float = float("-inf"),
high: float = float("inf"),
gap: float = IDLE_GAP,
) -> float:
"""Active seconds of the union of runs, clipped to ``[low, high)``."""
merged = intervals_union(spans, gap)
total = 0.0
for start, end in merged:
total += max(0.0, min(end, high) - max(start, low))
return total
def intervals_union(spans: Iterable[Span], gap: float = IDLE_GAP) -> list[Span]:
"""Union of runs, bridging gaps no longer than ``gap``."""
out: list[Span] = []
for start, end in sorted(spans):
if out and start - out[-1][1] <= gap:
out[-1] = (out[-1][0], max(out[-1][1], end))
else:
out.append((start, end))
return out

View File

@ -0,0 +1,200 @@
"""Non-transcript sources: snapshots, levers, autopsy aggregates, activity logs.
Each loader is idempotent (it replaces its own rows) and cheap, so every
complete ingest pass reruns all of them.
The session-autopsy cache (``~/.claude/autopsy/sessions.jsonl``) is the only
source of per-session numbers for transcripts already deleted. It is
**aggregate only**: tokens are summed per transcript record, not per API
message (about 2x), there is no model split and no subagents. It is therefore
scaled by ``api_turns / assistant_msgs``, priced as Opus 5.5, and then
calibrated against sessions present in both sources (``meta`` key
``autopsy_calibration``). It is never mixed into the per-session tables.
"""
from __future__ import annotations
from collections import Counter
from datetime import datetime
import json
from pathlib import Path
from typing import TYPE_CHECKING, Any
from python_pkg.token_audit import pricing
from python_pkg.token_dashboard import levers, store
if TYPE_CHECKING:
from collections.abc import Iterator
import sqlite3
CLAUDE = Path.home() / ".claude"
AUTOPSY = CLAUDE / "autopsy" / "sessions.jsonl"
PROMPT_HISTORY = CLAUDE / "history.jsonl"
TIME_CAP = CLAUDE / "logs" / "time-cap.jsonl"
_MS = 1000.0
_MS_EPOCH = 1e11 # anything larger is milliseconds, not seconds
def _jsonl(path: Path) -> Iterator[dict[str, Any]]:
"""Well-formed object lines of a JSONL file (none if it is missing)."""
if not path.is_file():
return
with path.open(encoding="utf-8", errors="replace") as handle:
for line in handle:
try:
row = json.loads(line)
except ValueError:
continue
if isinstance(row, dict):
yield row
def _stamp(raw: object) -> float | None:
"""ISO string or epoch (s or ms) to unix seconds."""
if isinstance(raw, (int, float)):
return raw / _MS if raw > _MS_EPOCH else float(raw)
if isinstance(raw, str):
try:
return datetime.fromisoformat(raw).timestamp()
except ValueError:
return None
return None
def snapshots(conn: sqlite3.Connection, report: Path) -> None:
"""Copy every history snapshot's window and metrics."""
for path in sorted((report / "history").glob("*.json")):
try:
snap = json.loads(path.read_text(encoding="utf-8"))
except ValueError:
continue
window = snap.get("window", {})
conn.execute(
"INSERT OR REPLACE INTO snapshots VALUES (?,?,?,?,?)",
(
path.stem,
snap.get("generated"),
window.get("since"),
window.get("until"),
json.dumps(snap.get("metrics", {})),
),
)
def _nearest(generated: list[tuple[float, str]], stamp: float) -> str:
"""The snapshot day generated closest to ``stamp``."""
return min(generated, key=lambda g: abs(g[0] - stamp))[1]
def lever_rows(conn: sqlite3.Connection, report: Path) -> None:
"""Rebuild the ``levers`` table from every source."""
generated = [
(_stamp(g) or 0.0, name[:10])
for name, g in conn.execute("SELECT name, generated FROM snapshots")
]
tables: dict[str, str] = {}
found = conn.execute("SELECT ts, markdown FROM lever_tables ORDER BY ts")
weekly = report / "WEEKLY.md"
extra = []
if weekly.is_file():
extra = [(weekly.stat().st_mtime, weekly.read_text(encoding="utf-8"))]
for ts, markdown in [*found, *extra]:
# A table belongs to the snapshot generated closest to it.
if generated:
tables.setdefault(_nearest(generated, ts), markdown)
conn.execute("DELETE FROM levers")
conn.executemany(
"INSERT INTO levers VALUES (?,?,?,?,?,?,?,?,?,?,?)",
[tuple(row.values()) for row in levers.build(report, tables)],
)
def autopsy(conn: sqlite3.Connection) -> None:
"""Load autopsy aggregates and calibrate their dollar estimate."""
price = pricing.load().for_model("claude-opus-5-5")
for row in _jsonl(AUTOPSY):
meta, tok, turns = (
row.get("meta", {}),
row.get("tokens", {}),
row.get("turns", {}),
)
started = _stamp(meta.get("started_at"))
records = row.get("counts", {}).get("assistant_msgs") or 0
api = turns.get("api_turns") or records
if started is None or not records:
continue
scale = api / records
raw = (
tok.get("input", 0) * price.input
+ tok.get("cache_write", 0) * price.input * pricing.WRITE_1H
+ tok.get("cache_read", 0) * price.cache_read
+ tok.get("output", 0) * price.output
) / pricing.PER_MILLION
conn.execute(
"INSERT OR REPLACE INTO aggregate_sessions VALUES"
" (?,?,?,?,?,?,?,?,?,?,?,?,?)",
(
row.get("session_id"),
"autopsy",
store.day_of(started),
started,
meta.get("cwd"),
meta.get("title"),
tok.get("input", 0),
tok.get("cache_write", 0),
tok.get("cache_read", 0),
tok.get("output", 0),
api,
records,
raw * scale,
),
)
est, real = conn.execute(
"SELECT SUM(a.usd_est), SUM(s.usd_main) FROM aggregate_sessions a"
" JOIN session_summary s ON s.key = a.session_id"
).fetchone()
factor = (real / est) if est and real else 1.0
conn.execute(
"INSERT OR REPLACE INTO meta VALUES ('autopsy_calibration', ?)", (str(factor),)
)
def activity_logs(conn: sqlite3.Connection, report: Path) -> None:
"""Prompt counts per day, time-cap events, task-size misses and estimates."""
days: Counter[str] = Counter()
for row in _jsonl(PROMPT_HISTORY):
stamp = _stamp(row.get("timestamp"))
if stamp is not None:
days[store.day_of(stamp)] += 1
# Upserts, never a wipe: if history.jsonl is trimmed, the days it lost stay.
conn.executemany(
"INSERT INTO prompt_activity VALUES (?,?) ON CONFLICT(day) DO UPDATE"
" SET prompts = MAX(prompts, excluded.prompts)",
days.items(),
)
sources = (
("time-cap", TIME_CAP, "event"),
("task-size-miss", report / "task-size" / "misses.jsonl", "kind"),
)
for name, path, kind_key in sources:
conn.executemany(
"INSERT OR IGNORE INTO events VALUES (?,?,?,?)",
[
(
name,
_stamp(r.get("ts")),
str(r.get(kind_key)),
str(r.get("command") or r.get("first_prompt") or "")[:200],
)
for r in _jsonl(path)
],
)
for row in _jsonl(report / "task-size" / "verdicts.jsonl"):
verdict = row.get("verdict")
calls = verdict.get("est_calls") if isinstance(verdict, dict) else None
if row.get("status") == "ok" and isinstance(calls, int):
# The first sized prompt of a session is its pre-work estimate.
conn.execute(
"INSERT OR IGNORE INTO estimates VALUES (?,?)",
(row.get("session"), calls),
)

View File

@ -0,0 +1,104 @@
"""Pure readers for individual transcript records and content blocks.
Split from :mod:`walk` so the record-level rules (what counts as a typed
prompt, how a status tag or a commit hash is recognised) are testable without a
file and the walker stays under the file-length cap.
"""
from __future__ import annotations
import os
from pathlib import Path
import re
from typing import Any
from python_pkg.token_audit.discover import WRAPPER
PROMPT_CHARS = 120
LEVER_HEADING = "Candidate levers (rule-based)"
# kuhy's convention: every final reply ends on one of these status tags.
TAG = re.compile(r"^\W*(DONE|RUNNING|QUESTION|NEEDS ACTION)\W*:", re.MULTILINE)
COMMAND = re.compile(r"<command-name>\s*(\S+?)\s*</command-name>")
# `git commit` prints `[branch sha] subject`, also through finish_auto.sh.
COMMIT_LINE = re.compile(
r"^\[[^\]\s]+(?: \(root-commit\))? ([0-9a-f]{7,40})\]", re.MULTILINE
)
REPO_HINT = re.compile(r"(?:git\s+-C|\bcd)\s+(\"[^\"]+\"|'[^']+'|[^\s;&|]+)")
_SKIP_PREFIXES = ("[Request interrupted", "Caveat:")
def text_of(content: object) -> str:
"""Flatten a message/tool-result ``content`` into plain text."""
if isinstance(content, str):
return content
if not isinstance(content, list):
return ""
parts = [
str(block.get("text") or "")
for block in content
if isinstance(block, dict) and block.get("type") == "text"
]
return "\n".join(parts)
def prompt_text(record: dict[str, Any]) -> str | None:
"""The typed text of a real user prompt, or ``None`` for anything else.
Excludes tool results, ``isMeta`` / compact-summary records, interrupts,
and harness-injected ``<tag>`` blocks. A slash command keeps its name.
"""
if record.get("type") != "user" or record.get("isMeta"):
return None
if record.get("isCompactSummary") or record.get("isVisibleInTranscriptOnly"):
return None
message = record.get("message")
content = message.get("content") if isinstance(message, dict) else None
if isinstance(content, list) and any(
isinstance(b, dict) and b.get("type") == "tool_result" for b in content
):
return None
raw = text_of(content)
command = COMMAND.search(raw)
text = WRAPPER.sub("", raw).strip()
if command is not None and not text:
text = command.group(1)
if not text or text.startswith(_SKIP_PREFIXES):
return None
return " ".join(text.split())[:PROMPT_CHARS]
def final_tag(text: str) -> str | None:
"""The last status tag in a reply, or ``None`` when it carries none."""
found = TAG.findall(text)
return found[-1] if found else None
def commit_shas(text: str) -> list[str]:
"""Commit hashes printed by ``git commit`` in a tool result."""
return COMMIT_LINE.findall(text)
def repo_hint(command: str, cwd: str | None) -> str:
"""The repository a commit command ran in: ``-C``/``cd`` target, else cwd."""
match = REPO_HINT.search(command)
target = match.group(1).strip("\"'") if match else (cwd or "")
if not target:
return ""
path = Path(target).expanduser()
if cwd and not path.is_absolute():
path = Path(cwd) / path
return os.path.normpath(path)
def lever_table(text: str) -> str | None:
"""The markdown table following the candidate-levers heading, if any."""
at = text.find(LEVER_HEADING)
if at < 0:
return None
rows: list[str] = []
for line in text[at:].splitlines()[1:]:
if line.startswith("|"):
rows.append(line)
elif rows:
break
return "\n".join(rows) if rows else None

View File

@ -0,0 +1,79 @@
"""Carry cost: what a tool result costs by staying in context afterwards.
A result of N tokens is cache-read again on every later API call of the same
file until a compaction drops it (or the session ends), so its real price is
N x (cache-read price of each later call) -- usually far more than the call
that fetched it. A compaction shows up as the context shrinking below
``COMPACTION_DROP_RATIO`` of the previous call's, the same rule token_audit's
image-cost model uses.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Protocol
from python_pkg.token_audit.model import COMPACTION_DROP_RATIO
from python_pkg.token_audit.pricing import PER_MILLION, WRITE_5M
if TYPE_CHECKING:
from python_pkg.token_audit.pricing import PriceBook
class _Row(Protocol):
"""Anything with creation / carry accumulators."""
create_usd: float
carry_usd: float
carry_calls: int
class Tracker:
"""Collects calls and results in order, settles carry at the end."""
def __init__(self) -> None:
"""Start with no calls and no results."""
self.calls: list[tuple[str, int]] = []
self.writes: list[float] = []
self.results: list[tuple[int, int, _Row]] = []
def turn(self, model: str, context: int, write: float = WRITE_5M) -> None:
"""Note one API call, in order, with its cache-write price multiplier."""
self.calls.append((model, context))
self.writes.append(write)
def result(self, row: _Row, tokens: int) -> None:
"""Note a result arriving before call number ``len(self.calls)``."""
self.results.append((len(self.calls), tokens, row))
def spans(self) -> list[tuple[int, int, int, _Row]]:
"""``(first, end, tokens, row)``: the calls ``first..end-1`` carry it."""
# cut[i]: index of the first call at or after i that starts a new
# context (after a compaction), else len(calls).
size = len(self.calls)
cut = [size] * (size + 1)
for index in range(size - 1, -1, -1):
dropped = index > 0 and self.calls[index][1] < (
COMPACTION_DROP_RATIO * self.calls[index - 1][1]
)
cut[index] = index if dropped else cut[index + 1]
return [
(start, cut[start + 1], tokens, row)
for start, tokens, row in self.results
if start < size
]
def settle(self, book: PriceBook) -> None:
"""Add each result's creation and carry to its row.
Creation: the first call after the result writes it to cache, at that
call's write multiplier of input price. Carry: every call from there
to the next compaction re-reads it at the cache-read price.
"""
prefix = [0.0]
for model, _ in self.calls:
prefix.append(prefix[-1] + book.for_model(model).cache_read)
for start, end, tokens, row in self.spans():
price = book.for_model(self.calls[start][0]).input
row.create_usd += tokens * self.writes[start] * price / PER_MILLION
row.carry_usd += tokens * (prefix[end] - prefix[start]) / PER_MILLION
row.carry_calls += end - start

View File

@ -0,0 +1,78 @@
"""Reconcile the store against a token_audit snapshot: within 1% or exit 1.
The snapshot's ``usd_per_day`` is ``usd_total`` over the span between the
earliest and latest priced record inside its window (not the window length),
so the same definition is applied to the store's turns here.
python3 -m python_pkg.token_dashboard.check_window # newest
python3 -m python_pkg.token_dashboard.check_window --snapshot 2026-10-17
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
import sys
from typing import TYPE_CHECKING
from python_pkg.token_audit import history
from python_pkg.token_dashboard import schema
if TYPE_CHECKING:
import sqlite3
TOLERANCE = 0.01
DAY_S = 86400.0
MIN_SPAN_S = 60.0
def measure(conn: sqlite3.Connection, since: float, until: float) -> dict[str, float]:
"""``usd_total``, ``usd_per_day`` and ``calls`` from the store's turns."""
usd, calls, first, last = conn.execute(
"SELECT SUM(usd), COUNT(*), MIN(ts), MAX(ts) FROM turns"
" WHERE ts BETWEEN ? AND ?",
(since, until),
).fetchone()
usd = usd or 0.0
days = max((last or 0) - (first or 0), MIN_SPAN_S) / DAY_S
return {"usd_total": usd, "usd_per_day": usd / days, "calls": float(calls)}
def main(argv: list[str] | None = None) -> int:
"""Compare and print one line per metric; 0 when every one is within 1%."""
parser = argparse.ArgumentParser(prog="token_dashboard.check_window")
parser.add_argument("--db", type=Path, default=schema.DEFAULT_DB)
parser.add_argument("--report-dir", type=Path, default=history.REPORT_DIR)
parser.add_argument("--snapshot", default=None, help="history file stem")
args = parser.parse_args(argv)
folder = args.report_dir / history.HISTORY_NAME
path = (
folder / f"{args.snapshot}.json"
if args.snapshot
else max(folder.glob("*.json"), default=None)
)
if path is None or not path.is_file() or not args.db.is_file():
sys.stderr.write(f"missing snapshot or store ({path}, {args.db})\n")
return 1
snap = json.loads(path.read_text(encoding="utf-8"))
window = snap["window"]
conn = schema.connect(args.db, readonly=True)
try:
got = measure(conn, float(window["since"]), float(window["until"]))
finally:
conn.close()
ok = True
for key, value in got.items():
want = float(snap["metrics"][key])
drift = abs(value - want) / want if want else 0.0
ok &= drift <= TOLERANCE
sys.stdout.write(
f"{key}: store {value:,.2f} vs snapshot {want:,.2f}"
f" ({drift:.2%}) {'OK' if drift <= TOLERANCE else 'FAIL'}\n"
)
return 0 if ok else 1
if __name__ == "__main__":
raise SystemExit(main())

View File

@ -0,0 +1,216 @@
"""Name what a tool call was, for the carry-cost and tool panels.
``family`` is the bucket a panel groups by (``git status``, ``mcp:<server>``,
``Read:memory``); ``target`` is the one thing it touched (the command, path,
host or query), capped so the store stays small; ``signature`` identifies
identical repeated calls (loops) without storing the full input.
"""
from __future__ import annotations
import hashlib
import json
import re
from urllib.parse import urlsplit
from python_pkg.token_audit.model import IMAGE_SUFFIXES
TARGET_CHARS = 160
SIG_CHARS = 16
EMPTY = "(empty)"
# Words that only wrap the real command (and the flags/numbers they take).
_WRAPPERS = frozenset(
{
"sudo",
"env",
"nice",
"ionice",
"timeout",
"time",
"nohup",
"exec",
"command",
"capped.sh",
"stdbuf",
"xargs",
}
)
# Tools whose first non-flag argument is what the call really did.
_SUBCOMMANDS = frozenset(
{
"git",
"pnpm",
"npm",
"npx",
"flutter",
"dart",
"cargo",
"go",
"docker",
"systemctl",
"gh",
"pre-commit",
"uv",
"pip",
"yay",
"pacman",
"adb",
"make",
"fvm",
"journalctl",
}
)
_TAKES_VALUE = frozenset({"-C", "-c", "-n", "--dir", "--prefix"})
_SHELLS = frozenset({"bash", "sh", "zsh"})
_ASSIGN = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*=")
_SEGMENTS = re.compile(r"\s*(?:&&|\|\||;|\||\n)\s*")
_NUMBERISH = re.compile(r"^\d+[smhd]?$")
_ERRORS = (
("Exit code", "exit_code"),
("<tool_use_error>", "tool_error"),
("doesn't want to proceed", "rejected"),
("Permission for this action was denied", "denied"),
("hook error", "hook"),
("TIME CAP", "time_cap"),
("[Request interrupted", "interrupted"),
)
def _base(word: str) -> str:
"""Last path component of a command word."""
return word.rsplit("/", 1)[-1]
def _command_words(words: list[str]) -> list[str]:
"""Drop leading assignments, wrappers and the flags/values they take."""
index = 0
while index < len(words):
word = words[index]
if not (
_ASSIGN.match(word)
or _base(word) in _WRAPPERS
or word.startswith("-")
or _NUMBERISH.match(word)
):
break
index += 1
return words[index:]
def _first_argument(words: list[str]) -> str:
"""The first non-flag word, skipping flags that take a value."""
skip = False
for word in words:
if skip:
skip = False
elif word in _TAKES_VALUE:
skip = True
elif not word.startswith(("-", "<", ">", "2>")):
return word
return ""
def _name_command(words: list[str]) -> str:
"""``git status``, ``python3 -m pytest``, ``bash x.sh`` or the bare name."""
head, rest = _base(words[0]), words[1:]
if head.startswith("python"):
if "-m" in rest:
after = rest[rest.index("-m") + 1 :]
return f"{head} -m {after[0]}" if after else f"{head} -m"
script = _first_argument(rest)
return f"{head} {_base(script)}" if script else head
if head in _SHELLS and "-c" in rest:
return f"{head} -c"
if head in _SHELLS or head in _SUBCOMMANDS:
arg = _first_argument(rest)
return f"{head} {_base(arg) if head in _SHELLS else arg}".strip()
return head
def bash_family(command: str) -> str:
"""The command a Bash call really ran, ignoring wrappers and ``cd``."""
last = ""
for segment in _SEGMENTS.split(command.strip()):
words = _command_words(segment.split())
if not words:
continue
last = _name_command(words)
if last != "cd":
return last
return last or EMPTY
_PATH_KINDS = (
("memory", ("/.claude/", "/memory/"), ("/.claude/", "/memories/")),
("rules", ("/.claude/rules",), ("rules-ondemand",)),
("skill", ("/skills/",)),
("claude", ("/.claude/",)),
)
def _path_kind(path: str) -> str:
"""What a file path is, as far as standing context goes."""
name = _base(path)
if name.startswith("CLAUDE") and name.endswith(".md"):
return "claude-md"
for kind, *needle_sets in _PATH_KINDS:
if any(all(n in path for n in needles) for needles in needle_sets):
return kind
lowered = path.lower()
return "image" if lowered.endswith(tuple(IMAGE_SUFFIXES)) else "file"
def _text(tool_input: dict[str, object], key: str) -> str:
"""One string input field, empty when absent."""
value = tool_input.get(key)
return value if isinstance(value, str) else ""
def family(name: str, tool_input: dict[str, object]) -> str:
"""The bucket a call is grouped under."""
if name == "Bash":
return bash_family(_text(tool_input, "command"))
if name.startswith("mcp__"):
return f"mcp:{name.split('__')[1]}"
if name == "Skill":
return "skill"
if name in {"Read", "Write", "Edit", "MultiEdit", "NotebookEdit"}:
return f"{name}:{_path_kind(_text(tool_input, 'file_path'))}"
return name
def target(name: str, tool_input: dict[str, object]) -> str:
"""The one thing a call touched, whitespace-collapsed and capped."""
if name == "Bash":
found = _text(tool_input, "command")
elif name.startswith("mcp__"):
found = name.split("__", 2)[-1]
elif name == "Skill":
found = _text(tool_input, "skill")
elif name == "WebFetch":
found = urlsplit(_text(tool_input, "url")).netloc
elif name in {"WebSearch", "Grep", "Glob"}:
found = _text(tool_input, "query") or _text(tool_input, "pattern")
elif name in {"Agent", "Task"}:
kind = _text(tool_input, "subagent_type") or "general-purpose"
worktree = _text(tool_input, "isolation") == "worktree"
found = f"{kind} [worktree]" if worktree else kind
else:
found = _text(tool_input, "file_path")
return " ".join(found.split())[:TARGET_CHARS]
def signature(name: str, tool_input: dict[str, object]) -> str:
"""A short stable hash of the full call, for spotting identical repeats."""
raw = json.dumps([name, tool_input], sort_keys=True, default=str)
return hashlib.sha1(raw.encode(), usedforsecurity=False).hexdigest()[:SIG_CHARS]
def error_kind(text: str) -> str:
"""Why a tool call failed, from its result text."""
head = text[:400]
for needle, kind in _ERRORS:
if needle in head:
return kind
return "error"

View File

@ -0,0 +1,105 @@
"""Find the commits each session made, from git rather than from tool output.
Measured 2026-10-17: of 601 recent commit commands only 35 printed a parseable
``[branch sha]`` line -- most run through ``finish_auto.sh`` with a tailed or
backgrounded output. So every commit command is stored with its time window
and repository, and the hashes come from ``git log`` instead:
* a commit whose hash a command printed belongs to that command's session;
* otherwise a commit carrying a ``Co-Authored-By: Claude`` trailer belongs to
the commit command in the same repository whose window covers its commit
time (from :data:`LEAD_S` before the call to :data:`TAIL_S` after its
result -- a backgrounded finish_auto.sh commits after the call returns),
the nearest one when several do.
Rows are only ever added, so a repository deleted later keeps its history.
"""
from __future__ import annotations
from collections import defaultdict
import re
from typing import TYPE_CHECKING
from python_pkg.token_dashboard import gitcmd
if TYPE_CHECKING:
import sqlite3
LEAD_S = 60.0
TAIL_S = 1800.0
TRAILER = re.compile(r"^Co-Authored-By:\s*Claude", re.IGNORECASE | re.MULTILINE)
_FIELD, _RECORD = "\x1f", "\x1e"
_LOG_FIELDS = 3
def _git(repo: str, *args: str) -> str | None:
"""Run git in ``repo``; ``None`` on any failure."""
return gitcmd.run(repo, *args)
def _toplevel(hint: str, cache: dict[str, str | None]) -> str | None:
"""The work tree containing ``hint``, cached per hint."""
if hint not in cache:
out = _git(hint, "rev-parse", "--show-toplevel") if hint else None
cache[hint] = out.strip() if out else None
return cache[hint]
def _log(repo: str, since: float) -> list[tuple[str, float, bool]]:
"""``(sha, commit time, has Claude trailer)`` on HEAD since ``since``."""
out = _git(
repo,
"log",
"HEAD",
f"--since=@{int(since)}",
f"--format=%H{_FIELD}%ct{_FIELD}%B{_RECORD}",
)
rows = []
for record in (out or "").split(_RECORD):
parts = record.strip("\n").split(_FIELD, 2)
if len(parts) == _LOG_FIELDS:
rows.append((parts[0], float(parts[1]), bool(TRAILER.search(parts[2]))))
return rows
Command = tuple[str, int, float, float, str] # key, prompt, start, end, sha
def _match(
sha: str, ts: float, cmds: list[Command], *, trailer: bool
) -> Command | None:
"""The command that made this commit, if any."""
for cmd in cmds:
if cmd[4] and sha.startswith(cmd[4]):
return cmd
if not trailer:
return None
covering = [c for c in cmds if c[2] - LEAD_S <= ts <= max(c[2], c[3]) + TAIL_S]
if not covering:
return None
return min(covering, key=lambda c: abs(ts - c[2]))
def discover(conn: sqlite3.Connection) -> int:
"""Attribute git commits to sessions; returns how many rows were added."""
cache: dict[str, str | None] = {}
by_repo: dict[str, list[Command]] = defaultdict(list)
for key, prompt, start, end, sha, hint in conn.execute(
"SELECT key, prompt_idx, ts, end_ts, sha, repo FROM commits"
):
top = _toplevel(hint, cache)
if top is not None:
by_repo[top].append((key, prompt, start, end, sha))
added = 0
for repo, cmds in by_repo.items():
since = min(c[2] for c in cmds) - LEAD_S
for sha, ts, trailer in _log(repo, since):
cmd = _match(sha, ts, cmds, trailer=trailer)
if cmd is None:
continue
added += conn.execute(
"INSERT OR IGNORE INTO session_commits VALUES (?,?,?,?,?)",
(sha, repo, ts, cmd[0], cmd[1]),
).rowcount
return added

View File

@ -0,0 +1,162 @@
"""Which files enter Claude's context, and how big each has been over time.
:func:`find` lists them per file (token_audit's :mod:`surfaces` sums them per
surface, which cannot say which rule grew). Only the list and the size history
are stored -- the dashboard reads each file live when asked, so it never shows
a stale copy.
Size history comes from two places, both cheap: ``~/.claude``'s own git
history (one ``git log --raw`` plus one ``git cat-file --batch-check`` for
every blob, incremental from the last commit seen) and the weekly prune's
``prune/<date>/sizes*.json`` snapshots.
"""
from __future__ import annotations
from dataclasses import dataclass
import json
from pathlib import Path
from typing import TYPE_CHECKING
from python_pkg.token_audit import pricing
from python_pkg.token_audit.surfaces import CLAUDE_DIR
from python_pkg.token_dashboard import gitcmd
if TYPE_CHECKING:
import sqlite3
SRC_DIR = Path.home() / "src"
MEMORY_LIMIT = 24_400 # MEMORY.md is only read up to ~24.4 KB
CLAUDE_MD_LIMIT = 40_000 # Claude Code warns about a CLAUDE.md over 40k chars
DESCRIPTION_LIMIT = 1024 # a skill description's maximum length
_BATCH_FIELDS = 3 # cat-file --batch-check: <sha> <type> <size>
@dataclass(frozen=True)
class ContextFile:
"""One file and how it reaches the context."""
path: str
kind: str
standing: str # always | in repo | description
limit: int | None
def find(claude: Path | None = None, src: Path | None = None) -> list[ContextFile]:
"""Every context file on disk (on-demand backups excluded)."""
claude = claude or CLAUDE_DIR
src = src or SRC_DIR
out = [
ContextFile(
str(claude / "CLAUDE.md"), "global CLAUDE.md", "always", CLAUDE_MD_LIMIT
)
]
groups = (
("memories", "*.md", "memories (@-imported)", "always", None),
("rules", "**/*.md", "rules (loaded recursively)", "always", None),
("skills", "*/SKILL.md", "skill", "description", DESCRIPTION_LIMIT),
("agents", "*.md", "agent definition", "description", DESCRIPTION_LIMIT),
("output-styles", "*.md", "output style", "always", None),
)
for folder, pattern, kind, standing, limit in groups:
out += [
ContextFile(str(p), kind, standing, limit)
for p in sorted((claude / folder).glob(pattern))
if p.is_file()
]
memory = claude / "projects" / "-home-kuhy" / "memory" / "MEMORY.md"
out.append(ContextFile(str(memory), "auto-memory index", "always", MEMORY_LIMIT))
for name in ("CLAUDE.md", "AGENTS.md"):
out += [
ContextFile(str(p), f"repo {name}", "in repo", CLAUDE_MD_LIMIT)
for p in sorted(src.glob(f"*/{name}"))
]
return [f for f in out if Path(f.path).is_file()]
def _git(repo: Path, *args: str, stdin: str | None = None) -> str:
"""Run git, returning stdout ('' on failure)."""
return gitcmd.run(repo, *args, stdin=stdin) or ""
def _git_sizes(claude: Path, since: str) -> tuple[list[tuple[str, float, str]], str]:
"""``(relpath, commit time, blob)`` for commits after ``since``; new head."""
head = _git(claude, "rev-parse", "HEAD").strip()
if not head or head == since:
return [], since
span = f"{since}..HEAD" if since else "HEAD"
out = _git(claude, "log", "--raw", "--no-abbrev", "--format=@%ct", span)
rows: list[tuple[str, float, str]] = []
stamp = 0.0
for line in out.splitlines():
if line.startswith("@"):
stamp = float(line[1:])
elif line.startswith(":") and line.endswith(".md"):
meta, _, rel = line.partition("\t")
blob = meta.split()[3]
rows.append((rel, stamp, blob))
return rows, head
def refresh(
conn: sqlite3.Connection, claude: Path | None = None, src: Path | None = None
) -> None:
"""Re-list context files and append any new size-history points."""
claude = claude or CLAUDE_DIR
files = find(claude, src)
conn.execute("DELETE FROM context_files")
conn.executemany(
"INSERT INTO context_files VALUES (?,?,?,?)",
[(f.path, f.kind, f.standing, f.limit) for f in files],
)
row = conn.execute("SELECT value FROM meta WHERE key='context_git_head'").fetchone()
rows, head = _git_sizes(claude, row[0] if row else "")
zero = "0" * 40
blobs = sorted({b for _, _, b in rows if b != zero})
sizes: dict[str, int] = {}
batch = _git(claude, "cat-file", "--batch-check", stdin="\n".join(blobs) + "\n")
for line in batch.splitlines():
parts = line.split()
if len(parts) == _BATCH_FIELDS and parts[2].isdigit():
sizes[parts[0]] = int(parts[2])
conn.executemany(
"INSERT OR IGNORE INTO context_sizes VALUES (?,?,?,?)",
[(str(claude / rel), ts, sizes.get(b, 0), "git") for rel, ts, b in rows],
)
conn.execute("INSERT OR REPLACE INTO meta VALUES ('context_git_head', ?)", (head,))
for path in sorted((claude / "token-report" / "prune").glob("*/sizes*.json")):
try:
data = json.loads(path.read_text(encoding="utf-8")).get("file_bytes", {})
except ValueError, OSError, AttributeError:
continue
stamp = path.stat().st_mtime
conn.executemany(
"INSERT OR IGNORE INTO context_sizes VALUES (?,?,?,?)",
[
(str(claude / rel), stamp, size, f"prune {path.parent.name}")
for rel, size in data.items()
if isinstance(size, int)
],
)
def price_mix(conn: sqlite3.Connection, now: float, days: float = 7.0) -> float:
"""Call-weighted cache-read USD/MTok over the last ``days``, into ``meta``.
A standing file costs its tokens x calls/day x this price: every call
re-reads the whole prefix from cache.
"""
book = pricing.load()
rows = conn.execute(
"SELECT model, COUNT(*) FROM turns WHERE ts > ? GROUP BY model",
(now - days * 86400,),
).fetchall()
calls = sum(n for _, n in rows)
price = (
sum(book.for_model(m).cache_read * n for m, n in rows) / calls if calls else 0.0
)
conn.execute(
"INSERT OR REPLACE INTO meta VALUES ('cache_read_usd_per_mtok', ?)",
(str(price),),
)
return price

View File

@ -0,0 +1,207 @@
"""Collect the detail rows (calls, marks, turn facts) during a file walk.
The walker feeds every record, tool call, tool result and priced turn here;
:meth:`Detail.finish` settles creation and carry cost on each row. Marks come
from what Claude Code writes next to the conversation (see :mod:`marks`).
"""
from __future__ import annotations
import json
from typing import TYPE_CHECKING, Any
from python_pkg.token_audit import parse, records
from python_pkg.token_audit.pricing import WRITE_1H, WRITE_5M
from python_pkg.token_dashboard import blocks, carry, classify, hook_join, marks
from python_pkg.token_dashboard.detail_rows import (
CallRow,
DetailRows,
MarkRow,
TurnDetail,
Where,
)
if TYPE_CHECKING:
from python_pkg.token_audit.model import Turn
from python_pkg.token_audit.pricing import PriceBook
CHARS = parse.CHARS_PER_TOKEN
def write_multiplier(usage: dict[str, int]) -> float:
"""Average cache-write price of one call, as a multiple of input."""
total = usage.get("cache_creation_input_tokens", 0)
if not total:
return WRITE_5M
one_hour = min(usage.get(records.CACHE_1H, 0), total)
return (one_hour * WRITE_1H + (total - one_hour) * WRITE_5M) / total
def _message(rec: dict[str, Any]) -> dict[str, Any]:
"""A record's ``message`` dict, empty when absent."""
message = rec.get("message")
return message if isinstance(message, dict) else {}
class _Output:
"""Visible output of one API message, accumulated over its records."""
def __init__(self, latency: float) -> None:
"""Start empty; ``latency`` is the gap before the first record."""
self.latency = latency
self.text = 0
self.tool = 0
self.thinking = False
def add(self, block: dict[str, Any]) -> None:
"""Count one content block."""
kind = block.get("type")
if kind == "text":
self.text += len(str(block.get("text") or ""))
elif kind == "tool_use":
self.tool += len(json.dumps(block.get("input") or {}))
elif kind in {"thinking", "redacted_thinking"}:
self.thinking = True
def split(self, output: int) -> tuple[int, int, int]:
"""``(text, tool, thinking)`` tokens summing to ``output``.
Visible blocks are estimated by length; billed output left over is
thinking (whose text Claude Code does not keep). With no thinking
block, or more visible text than output, the visible estimates are
scaled to the billed total instead.
"""
text, tool = self.text // CHARS, self.tool // CHARS
visible = text + tool
if not visible:
return (0, 0, output) if self.thinking else (output, 0, 0)
if visible > output or not self.thinking:
text = round(output * text / visible)
return text, output - text, 0
return text, tool, output - visible
class Detail:
"""Detail state for one file."""
def __init__(self) -> None:
"""Nothing seen yet."""
self.rows = DetailRows()
self.tracker = carry.Tracker()
self.calls: dict[str, CallRow] = {}
self.outputs: dict[str, _Output] = {}
self.last_ts = 0.0
self.model = ""
self.hooks = hook_join.Hooks()
def mark(self, found: marks.Found, ts: float, prompt: int) -> MarkRow:
"""Turn a found mark into a row; carry it if it sits in context."""
row = MarkRow(
Where(prompt, ts, self.model),
found.kind,
found.name,
detail=found.detail,
count=found.count,
tokens=found.tokens,
extra=found.extra,
duration_ms=found.duration_ms,
)
self.rows.marks.append(row)
if row.tokens and found.kind not in marks.NOT_IN_CONTEXT:
self.tracker.result(row.cost, row.tokens)
self.hooks.track(row, found)
return row
def record(self, rec: dict[str, Any], ts: float, prompt: int) -> None:
"""Account one record (after the walker opened any new prompt)."""
kind = rec.get("type")
if kind == "assistant":
self.assistant(rec, ts, prompt)
elif kind == "user" and blocks.prompt_text(rec) is not None:
self.hooks.prompt_opened()
self.last_ts = ts
for found in marks.of_record(rec):
self.mark(found, ts, prompt)
def assistant(self, rec: dict[str, Any], ts: float, prompt: int) -> None:
"""Error placeholders become marks; real messages add visible output."""
message = _message(rec)
if rec.get("isApiErrorMessage"):
text = blocks.text_of(message.get("content"))
detail = str(rec.get("apiError") or text)[: classify.TARGET_CHARS]
name = str(rec.get("error") or "unknown")
self.mark(marks.Found("api_error", name, detail=detail), ts, prompt)
return
mid = str(message.get("id") or "")
if mid not in self.outputs:
self.outputs[mid] = _Output(max(0.0, ts - (self.last_ts or ts)))
for block in parse.content_blocks(rec):
self.outputs[mid].add(block)
def use(
self, rec: dict[str, Any], block: dict[str, Any], ts: float, prompt: int
) -> None:
"""Open a call row for a ``tool_use`` block."""
message = _message(rec)
raw = block.get("input")
tool_input: dict[str, object] = raw if isinstance(raw, dict) else {}
name = str(block.get("name") or "unknown")
row = CallRow(
Where(prompt, ts, str(message.get("model") or self.model)),
name=name,
family=classify.family(name, tool_input),
target=classify.target(name, tool_input),
sig=classify.signature(name, tool_input),
message_id=str(message.get("id") or ""),
)
self.calls[str(block.get("id") or "")] = row
self.rows.calls.append(row)
def result(self, block: dict[str, Any], ts: float) -> None:
"""Close the call a ``tool_result`` answers."""
row = self.calls.pop(str(block.get("tool_use_id") or ""), None)
if row is None:
return
content = block.get("content")
images, text = marks.split_images(content)
row.end_ts = ts
row.images = images
row.result_tokens = images * parse.IMAGE_TOKENS + len(text) // CHARS
if block.get("is_error"):
row.error_kind = classify.error_kind(blocks.text_of(content))
self.tracker.result(row.cost, row.result_tokens)
def turn(self, turn: Turn, cost: float, prompt: int) -> None:
"""Note a priced API call."""
usage = turn.usage
self.model = turn.model
self.tracker.turn(turn.model, turn.context, write_multiplier(usage))
self.hooks.turn(cost, turn.timestamp)
output = self.outputs.pop(turn.message_id, None) or _Output(0.0)
text, tool, think = output.split(usage.get("output_tokens", 0))
one_hour = usage.get(records.CACHE_1H, 0)
self.rows.turns.append(
TurnDetail(
Where(prompt, turn.timestamp, turn.model),
latency_s=output.latency,
cw_1h=one_hour,
cw_5m=usage.get("cache_creation_input_tokens", 0) - one_hour,
out_text=text,
out_tool=tool,
out_think=think,
tool_calls=turn.tool_calls,
)
)
def finish(self, book: PriceBook) -> DetailRows:
"""Settle costs, fill batch sizes and models, merge repeated marks."""
self.tracker.settle(book)
per_message: dict[str, int] = {}
for call in self.rows.calls:
per_message[call.message_id] = per_message.get(call.message_id, 0) + 1
for call in self.rows.calls:
call.batch = per_message[call.message_id]
first_model = self.rows.turns[0].where.model if self.rows.turns else ""
self.hooks.resolve()
self.rows.marks = hook_join.merge(self.rows.marks, first_model)
return self.rows

View File

@ -0,0 +1,91 @@
"""Row types for the detail tables: one tool call, one context mark, one turn.
Each call and mark carries a :class:`Cost` (see :mod:`carry`): what putting
the chunk into the prompt cache cost, and what re-reading it on every later
call of the same file cost until a compaction dropped it.
"""
from __future__ import annotations
from dataclasses import dataclass, field
@dataclass
class Cost:
"""Creation + carry of one chunk of context, and the calls that carried it."""
create_usd: float = 0.0
carry_usd: float = 0.0
carry_calls: int = 0
@dataclass(frozen=True)
class Where:
"""When something happened: prompt index, timestamp, serving model."""
prompt_idx: int
ts: float
model: str
@dataclass
class CallRow:
"""One tool call, paired with its result (``error_kind`` empty when ok)."""
where: Where
name: str
family: str
target: str
sig: str
message_id: str
end_ts: float = 0.0
batch: int = 1
result_tokens: int = 0
images: int = 0
error_kind: str = ""
cost: Cost = field(default_factory=Cost)
@dataclass
class MarkRow:
"""Something that entered (or touched) context without being a tool call.
``kind`` is one of ``hook_run``, ``hook_context``, ``hook_block``,
``hook_error``, ``hook_msg``, ``attach``, ``compaction``, ``api_error``,
``interrupt``, ``image``. Rows with the same prompt, kind, name, detail
and model are summed into one, with ``count`` occurrences.
"""
where: Where
kind: str
name: str
detail: str = ""
count: int = 1
tokens: int = 0
extra: int = 0
duration_ms: int = 0
retry_usd: float = 0.0
cost: Cost = field(default_factory=Cost)
@dataclass(frozen=True)
class TurnDetail:
"""Per-API-call facts the ``turns`` table does not hold."""
where: Where
latency_s: float
cw_1h: int
cw_5m: int
out_text: int
out_tool: int
out_think: int
tool_calls: int
@dataclass
class DetailRows:
"""Everything one file contributes to the detail tables."""
calls: list[CallRow] = field(default_factory=list)
marks: list[MarkRow] = field(default_factory=list)
turns: list[TurnDetail] = field(default_factory=list)

View File

@ -0,0 +1,110 @@
"""Write one file's detail rows (calls, marks, turn facts) into the store.
Every row repeats the owning file's key, main session key, subagent flag and
cwd, plus its own model, timestamp and local day, so the dashboard's shared
filter clause (date range, cwd, model, scope) applies to these tables as-is.
"""
from __future__ import annotations
from datetime import datetime
from typing import TYPE_CHECKING
if TYPE_CHECKING:
import sqlite3
from python_pkg.token_dashboard.detail_rows import Cost, Where
from python_pkg.token_dashboard.rows import FileFacts
CLEAR = (
"DELETE FROM calls WHERE key = ?",
"DELETE FROM marks WHERE key = ?",
"DELETE FROM turn_detail WHERE key = ?",
)
Owner = tuple[str, str, int, str | None]
def day_of(stamp: float) -> str:
"""Local calendar day of a unix timestamp."""
return datetime.fromtimestamp(stamp).astimezone().strftime("%Y-%m-%d")
def _where(where: Where) -> tuple[str, float, str, int]:
"""``model, ts, day, prompt_idx`` columns."""
return (where.model, where.ts, day_of(where.ts), where.prompt_idx)
def _cost(cost: Cost) -> tuple[float, float, int]:
"""``create_usd, carry_usd, carry_calls`` columns."""
return (cost.create_usd, cost.carry_usd, cost.carry_calls)
def write(conn: sqlite3.Connection, facts: FileFacts, owner: Owner) -> None:
"""Insert the detail rows; :func:`store.write` already cleared old ones."""
detail = facts.rows.detail
conn.executemany(
"INSERT INTO calls (key, main_key, is_sub, cwd, model, ts, day,"
" prompt_idx, end_ts, name, family, target, sig, batch, result_tokens,"
" images, is_error, error_kind, create_usd, carry_usd, carry_calls)"
" VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
[
(
*owner,
*_where(c.where),
c.end_ts,
c.name,
c.family,
c.target,
c.sig,
c.batch,
c.result_tokens,
c.images,
int(bool(c.error_kind)),
c.error_kind,
*_cost(c.cost),
)
for c in detail.calls
],
)
conn.executemany(
"INSERT INTO marks (key, main_key, is_sub, cwd, model, ts, day,"
" prompt_idx, kind, name, detail, count, tokens, extra, duration_ms,"
" retry_usd, create_usd, carry_usd, carry_calls)"
" VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
[
(
*owner,
*_where(m.where),
m.kind,
m.name,
m.detail,
m.count,
m.tokens,
m.extra,
m.duration_ms,
m.retry_usd,
*_cost(m.cost),
)
for m in detail.marks
],
)
conn.executemany(
"INSERT INTO turn_detail (key, main_key, is_sub, cwd, model, ts, day,"
" prompt_idx, latency_s, cw_1h, cw_5m, out_text, out_tool, out_think,"
" tool_calls) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
[
(
*owner,
*_where(t.where),
t.latency_s,
t.cw_1h,
t.cw_5m,
t.out_text,
t.out_tool,
t.out_think,
t.tool_calls,
)
for t in detail.turns
],
)

View File

@ -0,0 +1,153 @@
"""Three 0-token efficiency trends, weekly.
1. **USD per DONE session** -- all spend of status-tagged sessions in a week
divided by how many of them ended on ``DONE:``. Failed and parked sessions
count against the done ones, which is the point. Sessions with no tag at all
(before the convention) are counted but kept out of the ratio.
2. **USD per surviving commit** -- the week's total spend divided by commits
made that week that survived :mod:`survival`'s 14-day rule.
3. **USD per estimated size unit** -- spend of sessions the task-size estimator
sized, divided by the sum of its ``est_calls`` for them.
"""
from __future__ import annotations
from collections import defaultdict
from dataclasses import dataclass
from datetime import datetime, timedelta
import json
from typing import TYPE_CHECKING, Any
from python_pkg.token_dashboard import survival
if TYPE_CHECKING:
from collections.abc import Iterable
import sqlite3
Week = dict[str, Any]
@dataclass(frozen=True)
class SessionCost:
"""What one main session cost and how it ended."""
key: str
first_ts: float
usd: float
tag: str | None
def week_of(stamp: float) -> str:
"""Monday (local time) of the week containing ``stamp``."""
day = datetime.fromtimestamp(stamp).astimezone().date()
return (day - timedelta(days=day.weekday())).isoformat()
def _ratio(part: float, whole: float) -> float | None:
"""``part / whole``, ``None`` when ``whole`` is zero."""
return part / whole if whole else None
def done_weekly(rows: Iterable[SessionCost]) -> list[Week]:
"""USD per DONE session and the DONE share, per week."""
weeks: dict[str, Week] = defaultdict(
lambda: {"done": 0, "other": 0, "untagged": 0, "usd_tagged": 0.0}
)
for row in rows:
week = weeks[week_of(row.first_ts)]
if row.tag is None:
week["untagged"] += 1
continue
week["usd_tagged"] += row.usd
week["done" if row.tag == "DONE" else "other"] += 1
out = []
for name in sorted(weeks):
week = weeks[name]
tagged = week["done"] + week["other"]
week |= {
"week": name,
"usd_per_done": _ratio(week["usd_tagged"], week["done"]),
"done_share": _ratio(week["done"], tagged),
}
out.append(week)
return out
def commits_weekly(
verdicts: Iterable[tuple[float, str]], week_usd: dict[str, float]
) -> list[Week]:
"""USD per surviving commit, with lost / pending / unknown counts."""
weeks: dict[str, Week] = defaultdict(
lambda: {"survived": 0, "lost": 0, "pending": 0, "unknown": 0}
)
for stamp, verdict in verdicts:
week = weeks[week_of(stamp)]
if verdict == "survived":
week["survived"] += 1
elif verdict in survival.FINAL:
week["lost"] += 1
elif verdict == "pending":
week["pending"] += 1
else:
week["unknown"] += 1
out = []
for name in sorted(weeks):
usd = week_usd.get(name, 0.0)
week = weeks[name]
week |= {
"week": name,
"usd": usd,
"usd_per_survivor": _ratio(usd, week["survived"]),
}
out.append(week)
return out
def estimate_weekly(rows: Iterable[SessionCost], est: dict[str, int]) -> list[Week]:
"""USD per estimated call, over sessions the estimator sized."""
weeks: dict[str, Week] = defaultdict(lambda: {"usd": 0.0, "est_calls": 0, "n": 0})
for row in rows:
if row.key not in est:
continue
week = weeks[week_of(row.first_ts)]
week["usd"] += row.usd
week["est_calls"] += est[row.key]
week["n"] += 1
return [
weeks[name]
| {
"week": name,
"usd_per_unit": _ratio(weeks[name]["usd"], weeks[name]["est_calls"]),
}
for name in sorted(weeks)
]
def rebuild(conn: sqlite3.Connection, now: float) -> None:
"""Recompute the three weekly series into ``meta['efficiency']``."""
rows = [
SessionCost(key, first, usd, tag)
for key, first, usd, tag in conn.execute(
"SELECT key, first_ts, usd, final_tag FROM session_summary"
)
]
week_usd: dict[str, float] = defaultdict(float)
for row in rows:
week_usd[week_of(row.first_ts)] += row.usd
judged = conn.execute(
"SELECT c.ts, v.verdict FROM session_commits c"
" LEFT JOIN commit_verdicts v ON v.sha = c.sha"
).fetchall()
verdicts = [
(ts, verdict or ("pending" if now - ts < survival.WINDOW_S else "unchecked"))
for ts, verdict in judged
]
est = dict(conn.execute("SELECT session_id, est_calls FROM estimates"))
payload = {
"done": done_weekly(rows),
"commits": commits_weekly(verdicts, week_usd),
"estimate": estimate_weekly(rows, est),
}
conn.execute(
"INSERT OR REPLACE INTO meta VALUES ('efficiency', ?)", (json.dumps(payload),)
)

View File

@ -0,0 +1,217 @@
"""Derive per-session, per-prompt and per-subagent summaries from raw rows.
Run for every main session touched by an ingest pass. A subagent file may be
ingested in a different pass than its parent, so everything here is computed
from the store, never from the in-memory walk.
Linking a subagent to the prompt that spawned it: the ``toolUseId`` in its
``.meta.json`` (or the ``agentId`` the ``Agent`` result reported) names an
``Agent`` call; that call's prompt is the answer. A depth-2 agent's call sits
in its parent agent's file, so the chain is followed upward. An agent with no
resolvable spawn -- one that outlived ``/clear`` -- stays ``prompt_idx NULL``
("unattributed") rather than being dropped, so session cost always reconciles.
"""
from __future__ import annotations
from collections import defaultdict
import json
from typing import TYPE_CHECKING, Any
from python_pkg.token_dashboard import active
if TYPE_CHECKING:
import sqlite3
_MAX_DEPTH = 8
def _rows(conn: sqlite3.Connection, sql: str, *args: object) -> list[Any]:
"""Run a query and return all rows."""
return conn.execute(sql, args).fetchall()
class _Session:
"""Everything stored for one main session and its subagents."""
def __init__(self, conn: sqlite3.Connection, main: str) -> None:
self.main = main
self.sessions = {
r[0]: r
for r in _rows(
conn,
"SELECT key, cwd, agent_type, description, spawn_tool_use_id,"
" agent_model, first_ts, last_ts, final_tag"
" FROM sessions WHERE main_key = ?",
main,
)
}
self.turns = _rows(
conn,
"SELECT key, prompt_idx, usd, model FROM turns WHERE main_key = ?",
main,
)
keys = json.dumps(list(self.sessions))
self.calls = _rows(
conn,
"SELECT key, tool_use_id, agent_id, prompt_idx, description,"
" subagent_type FROM agent_calls WHERE key IN"
" (SELECT value FROM json_each(?))",
keys,
)
self.spans_by_key: dict[str, list[active.Span]] = defaultdict(list)
for key, start, end in _rows(
conn,
"SELECT key, start, end FROM activity WHERE key IN"
" (SELECT value FROM json_each(?))",
keys,
):
self.spans_by_key[key].append((start, end))
self.spans = [s for spans in self.spans_by_key.values() for s in spans]
self.prompts = _rows(
conn,
"SELECT idx, ts, text FROM prompts WHERE key = ? ORDER BY idx",
main,
)
self.usd: dict[str, float] = defaultdict(float)
self.ncalls: dict[str, int] = defaultdict(int)
self.models: dict[str, dict[str, float]] = defaultdict(
lambda: defaultdict(float)
)
for key, _, usd, model in self.turns:
self.usd[key] += usd
self.ncalls[key] += 1
self.models[key][model] += usd
def spawn(self, sub: str) -> tuple[Any, ...] | None:
"""The ``Agent`` call that spawned ``sub``, if any is stored."""
tool_use_id = self.sessions[sub][4]
agent_id = sub.rsplit("/agent-", 1)[-1]
for call in self.calls:
if (tool_use_id and call[1] == tool_use_id) or call[2] == agent_id:
return tuple(call)
return None
def resolve(self, sub: str) -> tuple[int | None, int, tuple[Any, ...] | None]:
"""``(prompt_idx, depth, spawning call)`` for a subagent."""
first = self.spawn(sub)
call, depth = first, 1
while call is not None and call[0] != self.main and depth < _MAX_DEPTH:
call = self.spawn(call[0]) if call[0] in self.sessions else None
depth += 1
return (call[3] if call is not None else None), depth, first
def _sub_rows(state: _Session) -> list[tuple[Any, ...]]:
"""One ``subagent_link`` row per subagent of the session."""
out = []
for key, row in state.sessions.items():
if key == state.main:
continue
prompt_idx, depth, call = state.resolve(key)
models = state.models.get(key) or {}
model = max(models, key=models.__getitem__) if models else row[5]
spans = state.spans_by_key.get(key, [])
out.append(
(
key,
state.main,
prompt_idx,
depth,
row[3] or (call[4] if call else "") or key,
row[2] or (call[5] if call else "") or "unknown",
model,
state.usd.get(key, 0.0),
state.ncalls.get(key, 0),
row[6],
row[7],
row[7] - row[6],
active.measure(spans),
)
)
return out
def refresh(conn: sqlite3.Connection, main: str) -> None:
"""Rebuild every summary row for one main session."""
state = _Session(conn, main)
conn.execute("DELETE FROM subagent_link WHERE main_key = ?", (main,))
conn.execute("DELETE FROM prompt_summary WHERE key = ?", (main,))
conn.execute("DELETE FROM session_summary WHERE key = ?", (main,))
if not state.sessions:
return
subs = _sub_rows(state)
conn.executemany(
"INSERT INTO subagent_link VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)", subs
)
first = min((r[6] for r in state.sessions.values() if r[6]), default=0.0)
last = max(r[7] for r in state.sessions.values())
_prompt_rows(conn, state, subs, last)
own = state.sessions.get(main)
usd_main = state.usd.get(main, 0.0)
usd_sub = sum(r[7] for r in subs)
merged: dict[str, float] = defaultdict(float)
for models in state.models.values():
for model, usd in models.items():
merged[model] += usd
conn.execute(
"INSERT INTO session_summary VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
(
main,
own[1] if own else next(iter(state.sessions.values()))[1],
state.prompts[0][2] if state.prompts else (subs[0][4] if subs else ""),
first,
last,
last - first,
active.measure(state.spans),
usd_main + usd_sub,
usd_main,
usd_sub,
sum(state.ncalls.values()),
len(state.prompts),
len(subs),
json.dumps(dict(merged)),
own[8] if own else None,
),
)
def _prompt_rows(
conn: sqlite3.Connection,
state: _Session,
subs: list[tuple[Any, ...]],
last: float,
) -> None:
"""One ``prompt_summary`` row per prompt, plus one for pre-prompt turns."""
own: dict[int, float] = defaultdict(float)
calls: dict[int, int] = defaultdict(int)
for key, idx, usd, _ in state.turns:
if key == state.main:
own[idx] += usd
calls[idx] += 1
sub_usd: dict[int, float] = defaultdict(float)
for row in subs:
if row[2] is not None:
sub_usd[row[2]] += row[7]
prompts = list(state.prompts)
if own.get(-1) or sub_usd.get(-1):
start = min((s for s, _ in state.spans), default=0.0)
prompts.insert(0, (-1, start, "(before the first prompt)"))
bounds = [ts for _, ts, _ in prompts[1:]] + ([last + 1] if prompts else [])
conn.executemany(
"INSERT INTO prompt_summary VALUES (?,?,?,?,?,?,?,?,?)",
[
(
state.main,
idx,
ts,
text,
own.get(idx, 0.0),
sub_usd.get(idx, 0.0),
active.measure(state.spans, ts, end),
min(end, last) - ts,
calls.get(idx, 0),
)
for (idx, ts, text), end in zip(prompts, bounds, strict=True)
],
)

View File

@ -0,0 +1,29 @@
"""One way to run git for the dashboard: absolute binary, bounded, never raises."""
from __future__ import annotations
import shutil
import subprocess
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from pathlib import Path
GIT = shutil.which("git") or "/usr/bin/git"
TIMEOUT_S = 30
def run(repo: str | Path, *args: str, stdin: str | None = None) -> str | None:
"""``git -C repo args``: stdout, or ``None`` on any failure."""
try:
done = subprocess.run(
[GIT, "-C", str(repo), *args],
input=stdin,
capture_output=True,
text=True,
check=False,
timeout=TIMEOUT_S,
)
except OSError, subprocess.TimeoutExpired:
return None
return done.stdout if done.returncode == 0 else None

View File

@ -0,0 +1,91 @@
"""Join hook context to the run that produced it; price forced retries; merge.
``hook_additional_context`` names no command, so it is matched to a
``hook_success`` run of the same tool-use id and hook name whose stdout
contains the context text. A ``hook_blocking_error`` costs the calls it
forces: a Stop bounce every call until the next prompt, any other block the
one call that reacts to it.
"""
from __future__ import annotations
from dataclasses import replace
import json
from typing import TYPE_CHECKING
from python_pkg.token_dashboard.marks import SNIPPET
if TYPE_CHECKING:
from python_pkg.token_dashboard.detail_rows import MarkRow
from python_pkg.token_dashboard.marks import Found
_STOPS = frozenset({"Stop", "SubagentStop"})
class Hooks:
"""Pending joins and open retry windows for one file."""
def __init__(self) -> None:
"""Nothing seen yet."""
self.runs: dict[tuple[str, str], list[tuple[str, str]]] = {}
self.pending: list[tuple[MarkRow, tuple[str, str], str]] = []
self.until_prompt: list[MarkRow] = []
self.next_turn: list[MarkRow] = []
def track(self, row: MarkRow, found: Found) -> None:
"""Remember what later records or turns need to complete ``row``."""
if found.kind == "hook_run":
self.runs.setdefault(found.key, []).append((row.detail, found.text))
elif found.kind == "hook_context":
self.pending.append((row, found.key, found.text))
elif found.kind == "hook_block":
stop = row.name in _STOPS
(self.until_prompt if stop else self.next_turn).append(row)
def prompt_opened(self) -> None:
"""A new prompt ends every Stop-bounce retry."""
self.until_prompt.clear()
def turn(self, cost: float, ts: float) -> None:
"""Charge an API call made after a block to that block's retry.
Turns are released one message late, so a turn released after the
block record may still predate it: the timestamp decides.
"""
for row in [*self.until_prompt, *self.next_turn]:
if ts > row.where.ts:
row.retry_usd += cost
self.next_turn = [row for row in self.next_turn if ts <= row.where.ts]
def resolve(self) -> None:
"""Name the script behind each injected hook context."""
for row, key, text in self.pending:
snippet = text[:SNIPPET]
escaped = json.dumps(snippet)[1:-1]
for detail, stdout in self.runs.get(key, []):
if snippet and (snippet in stdout or escaped in stdout):
row.detail = detail
break
def merge(rows: list[MarkRow], first_model: str) -> list[MarkRow]:
"""Sum marks sharing prompt, kind, name, detail and model; oldest first."""
merged: dict[tuple[int, str, str, str, str], MarkRow] = {}
for row in rows:
row.where = replace(row.where, model=row.where.model or first_model)
where = row.where
key = (where.prompt_idx, row.kind, row.name, row.detail, where.model)
held = merged.get(key)
if held is None:
merged[key] = row
continue
held.where = replace(held.where, ts=min(held.where.ts, where.ts))
held.count += row.count
held.tokens += row.tokens
held.extra += row.extra
held.duration_ms += row.duration_ms
held.retry_usd += row.retry_usd
held.cost.create_usd += row.cost.create_usd
held.cost.carry_usd += row.cost.carry_usd
held.cost.carry_calls += row.cost.carry_calls
return sorted(merged.values(), key=lambda r: r.where.ts)

View File

@ -0,0 +1,137 @@
"""Refresh the token store: changed transcripts first, then every aux source.
Resumable by construction. Each pass walks changed files until its time budget
is spent (always at least one), commits, and exits :data:`EXIT_PENDING` if any
are left -- so a first backfill of months of transcripts is a handful of
sub-minute runs, and the systemd timer simply picks up where the last ended.
python3 -m python_pkg.token_dashboard.ingest # one pass
python3 -m python_pkg.token_dashboard.ingest --budget 40
"""
from __future__ import annotations
import argparse
from dataclasses import dataclass
from pathlib import Path
import sys
import time
from typing import TYPE_CHECKING
from python_pkg.token_audit import discover, history
from python_pkg.token_dashboard import (
ab,
aux,
commits,
context_files,
efficiency,
finalize,
schema,
store,
survival,
walk,
)
if TYPE_CHECKING:
import sqlite3
ROOT = Path.home() / ".claude" / "projects"
DEFAULT_BUDGET = 40.0
EXIT_PENDING = 75 # EX_TEMPFAIL: run again to continue
@dataclass(frozen=True)
class Outcome:
"""What one pass did."""
parsed: int
finalized: int
complete: bool
def _changed(
conn: sqlite3.Connection, root: Path
) -> list[tuple[Path, tuple[int, float]]]:
"""Transcripts whose size or mtime differ from what the store last saw."""
seen = {
path: (size, mtime)
for path, size, mtime in conn.execute("SELECT path, size, mtime FROM files")
}
out = []
for pattern in discover.PATTERNS:
for path in root.glob(pattern):
stat = path.stat()
now = (stat.st_size, stat.st_mtime)
if seen.get(str(path)) != now:
out.append((path, now))
# Oldest first: a backfill lands history in order, and a live session
# (newest mtime) is re-read last, closest to the pass's end.
return sorted(out, key=lambda item: item[1][1])
def run(
root: Path = ROOT,
db_path: Path = schema.DEFAULT_DB,
report_dir: Path = history.REPORT_DIR,
budget_s: float = DEFAULT_BUDGET,
) -> Outcome:
"""One resumable pass."""
started = time.monotonic()
deadline = started + budget_s
conn = schema.connect(db_path)
try:
todo = _changed(conn, root)
mains: set[str] = set()
parsed = 0
for path, stat in todo:
if parsed and time.monotonic() > deadline:
break
facts = walk.walk_file(path, root)
store.write(conn, facts, str(path), stat)
mains.add(facts.parent_session or facts.key)
parsed += 1
for main_key in sorted(mains):
finalize.refresh(conn, main_key)
conn.commit()
complete = parsed == len(todo)
if complete:
aux.snapshots(conn, report_dir)
aux.lever_rows(conn, report_dir)
aux.autopsy(conn)
aux.activity_logs(conn, report_dir)
commits.discover(conn)
now = time.time()
complete = survival.update(conn, now, deadline, time.monotonic)
efficiency.rebuild(conn, now)
ab.rebuild(conn)
context_files.refresh(conn)
context_files.price_mix(conn, now)
conn.execute(
"INSERT OR REPLACE INTO meta VALUES ('refreshed_at', ?)", (str(now),)
)
conn.commit()
finally:
conn.close()
return Outcome(parsed, len(mains), complete)
def main(argv: list[str] | None = None) -> int:
"""CLI: 0 when the store is current, :data:`EXIT_PENDING` when not."""
parser = argparse.ArgumentParser(prog="token_dashboard.ingest")
parser.add_argument("--root", type=Path, default=ROOT)
parser.add_argument("--db", type=Path, default=schema.DEFAULT_DB)
parser.add_argument("--report-dir", type=Path, default=history.REPORT_DIR)
parser.add_argument("--budget", type=float, default=DEFAULT_BUDGET)
args = parser.parse_args(argv)
started = time.monotonic()
outcome = run(args.root, args.db, args.report_dir, args.budget)
sys.stderr.write(
f"token_dashboard: parsed {outcome.parsed} files, refreshed "
f"{outcome.finalized} sessions in {time.monotonic() - started:.1f}s"
f"{'' if outcome.complete else ' (more pending: run again)'}\n"
)
return 0 if outcome.complete else EXIT_PENDING
if __name__ == "__main__":
raise SystemExit(main())

View File

@ -0,0 +1,173 @@
"""Every lever ever applied, proposed or passed over, with its outcome.
Sources, newest form first:
* ``history/*.json`` ``levers`` -- structured candidates (snapshots from
2026-10-18 on; see ``token_audit.review``).
* Markdown "Candidate levers" tables -- for older snapshots. WEEKLY.md is
overwritten every run, so older tables survive only in transcript tool
results; :mod:`walk` keeps those as ``lever_tables`` rows.
* ``ledger.jsonl`` -- applied levers, scored by ``token_audit.history.score``.
* ``prune/<date>/PROPOSAL.md`` -- weekly standing-prefix prune proposals.
Status of a candidate: ``applied`` if a ledger line on the same metric is dated
on or after the snapshot, ``not applied`` if a newer snapshot came and went
without one, else ``proposed``. Titles never match ledger text, so the join is
on the metric key.
"""
from __future__ import annotations
import json
import re
from typing import TYPE_CHECKING, Any
from python_pkg.token_audit import history
from python_pkg.token_audit.recommend import TITLE_METRIC
if TYPE_CHECKING:
from pathlib import Path
Row = dict[str, Any]
DATE = re.compile(r"\d{4}-\d{2}-\d{2}")
_COLUMNS = 5
def parse_candidates(markdown: str) -> list[Row]:
"""Rows of a ``| category | lever | evidence | est $/day | action |`` table."""
rows = []
for line in markdown.splitlines():
cells = [c.strip() for c in line.strip().strip("|").split("|")]
if len(cells) != _COLUMNS or cells[0] in {"category", ""}:
continue
if set(cells[0]) <= {"-"}:
continue
est = cells[3].lstrip("$").replace(",", "")
rows.append(
{
"category": cells[0],
"title": cells[1],
"evidence": cells[2],
"est_usd_per_day": float(est)
if est.replace(".", "").isdigit()
else None,
"action": cells[4],
"metric": TITLE_METRIC.get(cells[1], ""),
}
)
return rows
def ledger_date(entry: Row) -> str | None:
"""A ledger line's date, else one named in its text (prune lines)."""
if isinstance(entry.get("date"), str):
return str(entry["date"])
found = DATE.search(str(entry.get("lever", "")))
return found.group(0) if found else None
def _snapshots(report: Path) -> list[tuple[str, Row]]:
"""``(date, snapshot)`` oldest first."""
out = []
for path in sorted((report / "history").glob("*.json")):
try:
snap = json.loads(path.read_text(encoding="utf-8"))
except ValueError:
continue
if isinstance(snap, dict):
out.append((path.stem[:10], snap))
return out
def _applied(report: Path, latest: Row) -> list[Row]:
"""Ledger lines, scored against the newest snapshot."""
entries = history.read_ledger(report)
rows = []
for entry, verdict in zip(entries, history.score(entries, latest), strict=True):
rows.append(
{
"source": "ledger",
"date": ledger_date(entry),
"lever": verdict.lever,
"metric": verdict.metric,
"baseline": verdict.baseline,
"predicted": verdict.predicted,
"actual": verdict.actual,
"lower_is_better": int(bool(entry.get("lower_is_better", True))),
"status": "applied",
"outcome": verdict.outcome,
"detail": f"{verdict.note}; undo: {entry.get('undo', '-')}",
}
)
return rows
def _candidates(
snaps: list[tuple[str, Row]], tables: dict[str, str], applied: list[Row]
) -> list[Row]:
"""Candidate rows per snapshot, structured first, markdown second."""
rows = []
for position, (day, snap) in enumerate(snaps):
found = snap.get("levers")
if not isinstance(found, list):
found = parse_candidates(tables.get(day, ""))
newer = position + 1 < len(snaps)
for lever in found:
metric = str(lever.get("metric") or "")
hit = any(
a["metric"] == metric and (a["date"] or "") >= day for a in applied
)
status = "applied" if hit else ("not applied" if newer else "proposed")
rows.append(
{
"source": f"snapshot {day}",
"date": day,
"lever": lever.get("title"),
"metric": metric,
"baseline": snap.get("metrics", {}).get(metric),
"predicted": None,
"actual": None,
"lower_is_better": 1,
"status": status,
"outcome": None,
"detail": f"{lever.get('evidence')} -> {lever.get('action')}"
f" (est ${lever.get('est_usd_per_day') or '-'}/day)",
}
)
return rows
def _prunes(report: Path, applied: list[Row]) -> list[Row]:
"""One row per prune proposal."""
paths = sorted((report / "prune").glob("*/PROPOSAL.md"))
rows = []
for position, path in enumerate(paths):
day = path.parent.name
lines = [x for x in path.read_text(encoding="utf-8").splitlines() if x.strip()]
hit = any(f"prune {day}" in str(a["lever"]) for a in applied)
latest = position == len(paths) - 1
status = "applied" if hit else ("proposed" if latest else "not applied")
rows.append(
{
"source": f"prune {day}",
"date": day,
"lever": lines[0].lstrip("# ") if lines else f"prune {day}",
"metric": "standing_tokens_est",
"baseline": None,
"predicted": None,
"actual": None,
"lower_is_better": 1,
"status": status,
"outcome": None,
"detail": lines[1] if len(lines) > 1 else "",
}
)
return rows
def build(report: Path, tables: dict[str, str]) -> list[Row]:
"""Every lever row, applied ones first."""
snaps = _snapshots(report)
latest = snaps[-1][1] if snaps else {}
applied = _applied(report, latest)
return applied + _candidates(snaps, tables, applied) + _prunes(report, applied)

View File

@ -0,0 +1,177 @@
"""Read context marks out of the records that are not the conversation.
Hooks write ``attachment`` records: ``hook_success`` (every run, with its
command and duration; SessionStart / UserPromptSubmit stdout is injected as
context), ``hook_additional_context`` (injected text, without the command --
joined back to its run by :mod:`hook_join`), ``hook_blocking_error`` (a block
or a Stop-hook bounce) and ``hook_system_message`` (shown to the user only).
Other attachment types in :data:`INJECTED` are rendered into the prompt;
the rest (``prompt_snapshot``, ``environment`` ...) only record what was sent.
"""
from __future__ import annotations
from dataclasses import dataclass
import json
from typing import TYPE_CHECKING, Any
from python_pkg.token_audit import parse
from python_pkg.token_dashboard import blocks, classify
if TYPE_CHECKING:
from collections.abc import Iterator
CHARS = parse.CHARS_PER_TOKEN
# Marks that never sit in the model's context, so carry nothing.
NOT_IN_CONTEXT = frozenset({"hook_msg", "compaction", "api_error", "interrupt"})
# Hook events whose plain stdout Claude Code adds to the context.
INJECTING = frozenset({"SessionStart", "UserPromptSubmit"})
INJECTED = frozenset(
{
"nested_memory",
"edited_text_file",
"total_tokens_reminder",
"skill_listing",
"deferred_tools_delta",
"mcp_instructions_delta",
"agent_listing_delta",
"queued_command",
"task_reminder",
"silent_turn_reminder",
"file",
"todo",
"command_permissions",
}
)
_SCRIPTS = (".sh", ".py", ".js", ".ts", ".mjs")
SNIPPET = 40
@dataclass(frozen=True)
class Found:
"""One mark read from a record, before it becomes a row.
``key`` (tool-use id, hook name) and ``text`` only matter to hook runs
and hook context, which :mod:`hook_join` pairs up.
"""
kind: str
name: str
detail: str = ""
count: int = 1
tokens: int = 0
extra: int = 0
duration_ms: int = 0
key: tuple[str, str] = ("", "")
text: str = ""
def short_command(command: str) -> str:
"""A hook command's script name, or its first 40 characters."""
words = command.split()
scripts = [w for w in words if w.endswith(_SCRIPTS)]
if scripts:
return scripts[0].rsplit("/", 1)[-1]
return " ".join(words)[:SNIPPET]
def _int(value: object) -> int:
"""A number that may arrive as a string; 0 when it is neither."""
try:
return int(float(str(value)))
except ValueError:
return 0
def split_images(content: object) -> tuple[int, str]:
"""Image block count and the JSON text of everything else."""
if not isinstance(content, list):
return 0, "" if content is None else json.dumps(content)
images = [b for b in content if isinstance(b, dict) and b.get("type") == "image"]
rest = [b for b in content if b not in images]
return len(images), json.dumps(rest)
def _tokens(text: object) -> int:
"""Estimated tokens of a text value."""
return len(str(text or "")) // CHARS
def _hook(att: dict[str, Any]) -> Iterator[Found]:
"""Marks of one hook attachment."""
kind = att.get("type")
name = str(att.get("hookName") or "")
key = (str(att.get("toolUseID") or ""), name)
content = att.get("content")
if kind == "hook_success":
text = str(content or "")
injected = att.get("hookEvent") in INJECTING
injected = injected and not text.lstrip().startswith("{")
yield Found(
"hook_run",
name,
detail=short_command(str(att.get("command") or "")),
duration_ms=_int(att.get("durationMs")),
tokens=_tokens(text) if injected else 0,
key=key,
text=str(att.get("stdout") or ""),
)
elif kind == "hook_additional_context":
for item in content if isinstance(content, list) else [content]:
text = str(item or "")
yield Found("hook_context", name, tokens=_tokens(text), key=key, text=text)
elif kind == "hook_blocking_error":
raw = att.get("blockingError")
error = raw if isinstance(raw, dict) else {"blockingError": raw}
command = short_command(str(error.get("command") or ""))
yield Found(
"hook_block", name, detail=command, tokens=_tokens(error["blockingError"])
)
elif kind == "hook_non_blocking_error":
yield Found("hook_error", name, tokens=_tokens(content))
elif kind == "hook_system_message":
yield Found("hook_msg", name)
def _attachment(att: dict[str, Any]) -> Iterator[Found]:
"""Marks of one attachment record."""
kind = str(att.get("type") or "")
if kind.startswith("hook_"):
yield from _hook(att)
elif kind in INJECTED:
rest = {k: v for k, v in att.items() if k != "type"}
path = str(att.get("path") or "")[: classify.TARGET_CHARS]
yield Found("attach", kind, detail=path, tokens=_tokens(json.dumps(rest)))
def _user(rec: dict[str, Any]) -> Iterator[Found]:
"""Interruptions and pasted images."""
message = rec.get("message")
content = message.get("content") if isinstance(message, dict) else None
if blocks.text_of(content).startswith("[Request interrupted"):
yield Found("interrupt", "user")
images, _ = split_images(content)
if images:
yield Found("image", "user", count=images, tokens=images * parse.IMAGE_TOKENS)
def of_record(rec: dict[str, Any]) -> list[Found]:
"""Every mark one non-assistant record carries."""
kind = rec.get("type")
att = rec.get("attachment")
if kind == "attachment" and isinstance(att, dict):
return list(_attachment(att))
if kind == "system" and rec.get("subtype") == "compact_boundary":
meta = rec.get("compactMetadata")
meta = meta if isinstance(meta, dict) else {}
found = Found(
"compaction",
str(meta.get("trigger") or "manual"),
tokens=_int(meta.get("preTokens")),
extra=_int(meta.get("postTokens")),
duration_ms=_int(meta.get("durationMs")),
)
return [found]
if kind == "user":
return list(_user(rec))
return []

View File

@ -0,0 +1,104 @@
"""Row types one transcript file contributes to the store (see :mod:`walk`)."""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any
from python_pkg.token_dashboard.detail_rows import DetailRows
if TYPE_CHECKING:
from python_pkg.token_dashboard import active
@dataclass(frozen=True)
class TurnRow:
"""One deduplicated API call, priced."""
ts: float
model: str
usd: float
context: int
prompt_idx: int
usage: dict[str, int]
@dataclass(frozen=True)
class PromptRow:
"""One typed user prompt."""
idx: int
ts: float
prompt_id: str
text: str
@dataclass
class ToolRow:
"""Calls of one tool inside one prompt."""
prompt_idx: int
name: str
calls: int = 0
result_tokens: int = 0
usd: float = 0.0
carry_usd: float = 0.0
# Settled by the carry tracker too, but not stored per tool (the calls
# table holds creation per call).
create_usd: float = 0.0
carry_calls: int = 0
@dataclass(frozen=True)
class AgentCall:
"""An ``Agent`` tool call: the spawn point of a subagent."""
tool_use_id: str
agent_id: str
prompt_idx: int
ts: float
description: str
subagent_type: str
model: str
result_ts: float
status: str
@dataclass(frozen=True)
class Commit:
"""A commit hash printed by a Bash ``git commit`` in this file."""
prompt_idx: int
ts: float
end_ts: float
sha: str
repo: str
@dataclass
class FileRows:
"""The row lists one transcript file contributes."""
turns: list[TurnRow] = field(default_factory=list)
prompts: list[PromptRow] = field(default_factory=list)
tools: list[ToolRow] = field(default_factory=list)
agent_calls: list[AgentCall] = field(default_factory=list)
intervals: list[active.Span] = field(default_factory=list)
lever_tables: list[tuple[float, str]] = field(default_factory=list)
commits: list[Commit] = field(default_factory=list)
messages: list[tuple[float, str]] = field(default_factory=list)
detail: DetailRows = field(default_factory=DetailRows)
@dataclass
class FileFacts:
"""Everything one transcript file contributes to the store."""
key: str
session_id: str
parent_session: str | None
cwd: str | None
meta: dict[str, Any]
rows: FileRows = field(default_factory=FileRows)
final_tag: str | None = None
span: tuple[float, float] = (0.0, 0.0)

View File

@ -0,0 +1,192 @@
"""SQLite layout of the durable token store.
Raw tables (``files`` .. ``commits``) hold what one transcript file said and
are replaced wholesale when that file changes. They are **never** deleted when
the file disappears: Claude Code removes transcripts after
``cleanupPeriodDays``, and outliving that is the store's whole purpose.
Summary tables (``session_summary`` .. ``subagent_link``) are derived from the
raw ones by :mod:`finalize` and may be rebuilt at any time. Aux tables hold the
sources that are not transcripts (snapshots, ledger, levers, autopsy rows).
"""
from __future__ import annotations
from pathlib import Path
import sqlite3
DEFAULT_DB = Path.home() / "data" / "token-dashboard" / "tokens.sqlite"
VERSION = "3"
_RAW = """
CREATE TABLE IF NOT EXISTS meta(key TEXT PRIMARY KEY, value TEXT);
CREATE TABLE IF NOT EXISTS files(
path TEXT PRIMARY KEY, key TEXT, size INT, mtime REAL);
CREATE TABLE IF NOT EXISTS sessions(
key TEXT PRIMARY KEY, session_id TEXT, main_key TEXT, cwd TEXT,
agent_type TEXT, description TEXT, spawn_tool_use_id TEXT,
parent_agent_id TEXT, agent_model TEXT, first_ts REAL, last_ts REAL,
final_tag TEXT);
CREATE TABLE IF NOT EXISTS turns(
key TEXT, main_key TEXT, is_sub INT, cwd TEXT, ts REAL, day TEXT,
model TEXT, usd REAL, context INT, prompt_idx INT, input INT,
cache_write INT, cache_read INT, output INT);
CREATE INDEX IF NOT EXISTS turns_key ON turns(key);
CREATE INDEX IF NOT EXISTS turns_day ON turns(day);
CREATE TABLE IF NOT EXISTS prompts(
key TEXT, idx INT, ts REAL, prompt_id TEXT, text TEXT);
CREATE INDEX IF NOT EXISTS prompts_key ON prompts(key);
CREATE TABLE IF NOT EXISTS tool_use(
key TEXT, prompt_idx INT, name TEXT, calls INT, result_tokens INT,
usd REAL, carry_usd REAL);
CREATE INDEX IF NOT EXISTS tool_use_key ON tool_use(key);
CREATE TABLE IF NOT EXISTS agent_calls(
key TEXT, tool_use_id TEXT, agent_id TEXT, prompt_idx INT, ts REAL,
description TEXT, subagent_type TEXT, model TEXT, result_ts REAL,
status TEXT);
CREATE INDEX IF NOT EXISTS agent_calls_key ON agent_calls(key);
CREATE TABLE IF NOT EXISTS activity(key TEXT, start REAL, end REAL);
CREATE INDEX IF NOT EXISTS activity_key ON activity(key);
CREATE TABLE IF NOT EXISTS sent_messages(key TEXT, ts REAL, to_agent TEXT);
CREATE TABLE IF NOT EXISTS lever_tables(key TEXT, ts REAL, markdown TEXT);
CREATE TABLE IF NOT EXISTS commits(
key TEXT, prompt_idx INT, ts REAL, end_ts REAL, sha TEXT, repo TEXT);
CREATE TABLE IF NOT EXISTS calls(
key TEXT, main_key TEXT, is_sub INT, cwd TEXT, model TEXT, ts REAL,
day TEXT, prompt_idx INT, end_ts REAL, name TEXT, family TEXT,
target TEXT, sig TEXT, batch INT, result_tokens INT, images INT,
is_error INT, error_kind TEXT, create_usd REAL, carry_usd REAL,
carry_calls INT);
CREATE INDEX IF NOT EXISTS calls_key ON calls(key);
CREATE INDEX IF NOT EXISTS calls_ts ON calls(ts);
CREATE TABLE IF NOT EXISTS marks(
key TEXT, main_key TEXT, is_sub INT, cwd TEXT, model TEXT, ts REAL,
day TEXT, prompt_idx INT, kind TEXT, name TEXT, detail TEXT, count INT,
tokens INT, extra INT, duration_ms INT, retry_usd REAL, create_usd REAL,
carry_usd REAL, carry_calls INT);
CREATE INDEX IF NOT EXISTS marks_key ON marks(key);
CREATE INDEX IF NOT EXISTS marks_ts ON marks(ts);
CREATE TABLE IF NOT EXISTS turn_detail(
key TEXT, main_key TEXT, is_sub INT, cwd TEXT, model TEXT, ts REAL,
day TEXT, prompt_idx INT, latency_s REAL, cw_1h INT, cw_5m INT,
out_text INT, out_tool INT, out_think INT, tool_calls INT);
CREATE INDEX IF NOT EXISTS turn_detail_key ON turn_detail(key);
CREATE INDEX IF NOT EXISTS turn_detail_ts ON turn_detail(ts);
CREATE TABLE IF NOT EXISTS session_commits(
sha TEXT PRIMARY KEY, repo TEXT, ts REAL, key TEXT, prompt_idx INT);
"""
_DERIVED = """
CREATE TABLE IF NOT EXISTS session_summary(
key TEXT PRIMARY KEY, cwd TEXT, title TEXT, first_ts REAL, last_ts REAL,
wall_s REAL, active_s REAL, usd REAL, usd_main REAL, usd_sub REAL,
calls INT, prompts INT, subagents INT, models TEXT, final_tag TEXT);
CREATE TABLE IF NOT EXISTS prompt_summary(
key TEXT, idx INT, ts REAL, text TEXT, usd_own REAL, usd_sub REAL,
active_s REAL, wall_s REAL, calls INT);
CREATE INDEX IF NOT EXISTS prompt_summary_key ON prompt_summary(key);
CREATE TABLE IF NOT EXISTS subagent_link(
sub_key TEXT PRIMARY KEY, main_key TEXT, prompt_idx INT, depth INT,
description TEXT, agent_type TEXT, model TEXT, usd REAL, calls INT,
first_ts REAL, last_ts REAL, wall_s REAL, active_s REAL);
CREATE INDEX IF NOT EXISTS subagent_link_main ON subagent_link(main_key);
CREATE TABLE IF NOT EXISTS commit_verdicts(
sha TEXT PRIMARY KEY, repo TEXT, ts REAL, added INT, kept INT,
verdict TEXT);
CREATE VIEW IF NOT EXISTS daily AS
SELECT day, SUM(usd) AS usd, COUNT(*) AS calls,
SUM(CASE WHEN is_sub THEN usd ELSE 0 END) AS usd_sub,
SUM(CASE WHEN model LIKE '%opus%' THEN usd ELSE 0 END) AS usd_opus,
SUM(CASE WHEN context > 300000 THEN usd ELSE 0 END) AS usd_over
FROM turns GROUP BY day;
"""
_AUX = """
CREATE TABLE IF NOT EXISTS snapshots(
name TEXT PRIMARY KEY, generated TEXT, since REAL, until REAL,
metrics TEXT);
CREATE TABLE IF NOT EXISTS levers(
source TEXT, date TEXT, lever TEXT, metric TEXT, baseline REAL,
predicted REAL, actual REAL, lower_is_better INT, status TEXT,
outcome TEXT, detail TEXT);
CREATE TABLE IF NOT EXISTS aggregate_sessions(
session_id TEXT PRIMARY KEY, source TEXT, day TEXT, started REAL,
cwd TEXT, title TEXT, input INT, cache_write INT, cache_read INT,
output INT, api_turns INT, records INT, usd_est REAL);
CREATE TABLE IF NOT EXISTS prompt_activity(day TEXT PRIMARY KEY, prompts INT);
CREATE TABLE IF NOT EXISTS events(source TEXT, ts REAL, kind TEXT, detail TEXT);
CREATE UNIQUE INDEX IF NOT EXISTS events_once ON events(source, ts, kind, detail);
CREATE TABLE IF NOT EXISTS context_files(
path TEXT PRIMARY KEY, kind TEXT, standing TEXT, size_limit INT);
CREATE TABLE IF NOT EXISTS context_sizes(
path TEXT, ts REAL, bytes INT, source TEXT, PRIMARY KEY (path, ts, source));
CREATE TABLE IF NOT EXISTS estimates(session_id TEXT PRIMARY KEY, est_calls INT);
"""
# Ordered, additive migrations: MIGRATIONS[n] takes a store from n-1 to n.
# The store must never be deleted to change its schema -- rows for expired
# transcripts cannot be regenerated -- so every change lands here instead.
# New tables need no entry: CREATE TABLE IF NOT EXISTS adds them.
MIGRATIONS: dict[int, tuple[str, ...]] = {
2: ("ALTER TABLE commits ADD COLUMN end_ts REAL",),
3: (
"ALTER TABLE agent_calls ADD COLUMN result_ts REAL",
"ALTER TABLE agent_calls ADD COLUMN status TEXT",
),
}
class SchemaError(RuntimeError):
"""The store was written by a newer (or unknown) schema."""
def __init__(self, found: object) -> None:
"""Name the version found and the one this code knows."""
super().__init__(f"store schema {found!r}; this code knows {VERSION}")
def _stored_version(conn: sqlite3.Connection) -> int | None:
"""The schema the store was written with, ``None`` for a fresh file."""
has_meta = conn.execute(
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='meta'"
).fetchone()
if has_meta is None:
return None
row = conn.execute("SELECT value FROM meta WHERE key='schema'").fetchone()
if row is None or not str(row[0]).isdigit():
raise SchemaError(row)
return int(row[0])
def _migrate(conn: sqlite3.Connection, stored: int) -> None:
"""Apply every migration newer than ``stored``, in order."""
current = int(VERSION)
if stored > current:
raise SchemaError(stored)
for step in range(stored + 1, current + 1):
for statement in MIGRATIONS.get(step, ()):
table, column = statement.split()[2], statement.split()[5]
present = {r[1] for r in conn.execute(f"PRAGMA table_info({table})")}
if column not in present:
conn.execute(statement)
def connect(path: Path, *, readonly: bool = False) -> sqlite3.Connection:
"""Open the store; create missing tables and migrate when writing."""
if readonly:
return sqlite3.connect(f"file:{path}?mode=ro", uri=True)
path.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(path)
try:
stored = _stored_version(conn)
# Missing tables first, at today's shape; then columns old tables lack.
conn.executescript(_RAW + _DERIVED + _AUX)
if stored is not None:
_migrate(conn, stored)
except SchemaError:
conn.close()
raise
conn.execute("PRAGMA journal_mode=WAL")
conn.execute("INSERT OR REPLACE INTO meta VALUES ('schema', ?)", (VERSION,))
conn.commit()
return conn

View File

@ -0,0 +1,135 @@
"""Write one file's :class:`FileFacts` into the raw tables, replacing old rows.
The unit of change is the whole file: a changed transcript is re-walked and
its rows replaced in one transaction. Resuming from a byte offset would need
the dedup state (:class:`TurnMerger`) persisted across runs, and re-walking
even the largest transcript costs a fraction of a second.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from python_pkg.token_dashboard import detail_store
from python_pkg.token_dashboard.detail_store import day_of
if TYPE_CHECKING:
import sqlite3
from python_pkg.token_dashboard.walk import FileFacts
# One literal statement per raw table: no SQL is assembled from strings.
CLEAR = (
"DELETE FROM turns WHERE key = ?",
"DELETE FROM prompts WHERE key = ?",
"DELETE FROM tool_use WHERE key = ?",
"DELETE FROM agent_calls WHERE key = ?",
"DELETE FROM activity WHERE key = ?",
"DELETE FROM lever_tables WHERE key = ?",
"DELETE FROM commits WHERE key = ?",
"DELETE FROM sent_messages WHERE key = ?",
*detail_store.CLEAR,
)
def write(
conn: sqlite3.Connection, facts: FileFacts, path: str, stat: tuple[int, float]
) -> None:
"""Replace every row ``facts.key`` owns. Caller commits."""
key = facts.key
for statement in CLEAR:
conn.execute(statement, (key,))
main_key = facts.parent_session or key
meta = facts.meta
conn.execute(
"INSERT OR REPLACE INTO sessions VALUES (?,?,?,?,?,?,?,?,?,?,?,?)",
(
key,
facts.session_id,
main_key,
facts.cwd,
meta.get("agentType"),
meta.get("description"),
meta.get("toolUseId"),
meta.get("parentAgentId"),
meta.get("model"),
*facts.span,
facts.final_tag,
),
)
is_sub = int(facts.parent_session is not None)
conn.executemany(
"INSERT INTO turns VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
[
(
key,
main_key,
is_sub,
facts.cwd,
t.ts,
day_of(t.ts),
t.model,
t.usd,
t.context,
t.prompt_idx,
t.usage.get("input_tokens", 0),
t.usage.get("cache_creation_input_tokens", 0),
t.usage.get("cache_read_input_tokens", 0),
t.usage.get("output_tokens", 0),
)
for t in facts.rows.turns
],
)
conn.executemany(
"INSERT INTO prompts VALUES (?,?,?,?,?)",
[(key, p.idx, p.ts, p.prompt_id, p.text) for p in facts.rows.prompts],
)
conn.executemany(
"INSERT INTO tool_use VALUES (?,?,?,?,?,?,?)",
[
(key, t.prompt_idx, t.name, t.calls, t.result_tokens, t.usd, t.carry_usd)
for t in facts.rows.tools
],
)
conn.executemany(
"INSERT INTO agent_calls (key, tool_use_id, agent_id, prompt_idx, ts,"
" description, subagent_type, model, result_ts, status)"
" VALUES (?,?,?,?,?,?,?,?,?,?)",
[
(
key,
a.tool_use_id,
a.agent_id,
a.prompt_idx,
a.ts,
a.description,
a.subagent_type,
a.model,
a.result_ts,
a.status,
)
for a in facts.rows.agent_calls
],
)
conn.executemany(
"INSERT INTO activity VALUES (?,?,?)",
[(key, s, e) for s, e in facts.rows.intervals],
)
conn.executemany(
"INSERT INTO lever_tables VALUES (?,?,?)",
[(key, ts, md) for ts, md in facts.rows.lever_tables],
)
conn.executemany(
"INSERT INTO commits (key, prompt_idx, ts, end_ts, sha, repo)"
" VALUES (?,?,?,?,?,?)",
[
(key, c.prompt_idx, c.ts, c.end_ts, c.sha, c.repo)
for c in facts.rows.commits
],
)
conn.executemany(
"INSERT INTO sent_messages VALUES (?,?,?)",
[(key, ts, to) for ts, to in facts.rows.messages],
)
detail_store.write(conn, facts, (key, main_key, is_sub, facts.cwd))
conn.execute("INSERT OR REPLACE INTO files VALUES (?,?,?,?)", (path, key, *stat))

View File

@ -0,0 +1,126 @@
"""Did a commit made in a session survive? Decided by git, never by a model.
Rule (fixed here, shown in the dashboard caption): a commit is judged once it
is :data:`WINDOW_DAYS` old, at the last commit of its repository's ``HEAD``
history that is no more than that old relative to it.
* ``reverted`` -- a commit inside the window says ``This reverts commit <sha>``.
* ``churned`` -- under :data:`KEEP_SHARE` of the lines it added (counted over
its :data:`MAX_FILES` largest files) still blame to it.
* ``survived`` -- otherwise, including a commit that added no lines.
* ``pending`` -- younger than the window.
* ``unknown`` -- the repo or hash is gone, or the commit is not on ``HEAD``
(rebased, squashed or on another branch): excluded from the ratio.
"""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import TYPE_CHECKING
from python_pkg.token_dashboard import gitcmd
if TYPE_CHECKING:
from collections.abc import Callable
import sqlite3
WINDOW_DAYS = 14
WINDOW_S = WINDOW_DAYS * 86400
KEEP_SHARE = 0.5
MAX_FILES = 10
_NUMSTAT_FIELDS = 3
FINAL = frozenset({"survived", "churned", "reverted"})
@dataclass(frozen=True)
class Verdict:
"""The outcome for one commit."""
verdict: str
sha: str = ""
ts: float = 0.0
added: int = 0
kept: int = 0
def judge(added: int, kept: int, *, reverted: bool) -> str:
"""Apply the survival rule to measured numbers."""
if reverted:
return "reverted"
if added and kept < KEEP_SHARE * added:
return "churned"
return "survived"
def _git(repo: str, *args: str) -> str | None:
"""Run git in ``repo``; ``None`` on any failure."""
return gitcmd.run(repo, *args)
def _added(repo: str, sha: str) -> list[tuple[int, str]]:
"""``(added lines, path)`` for the commit's largest text files."""
out = _git(repo, "show", "--numstat", "--format=", sha) or ""
rows = []
for line in out.splitlines():
parts = line.split("\t", 2)
if len(parts) == _NUMSTAT_FIELDS and parts[0].isdigit() and int(parts[0]):
rows.append((int(parts[0]), parts[2]))
return sorted(rows, reverse=True)[:MAX_FILES]
def _kept(repo: str, rev: str, sha: str, path: str) -> int:
"""Lines of ``path`` at ``rev`` that still blame to ``sha``."""
out = _git(repo, "blame", "--porcelain", rev, "--", path) or ""
prefix = sha + " "
return sum(1 for line in out.splitlines() if line.startswith(prefix))
def evaluate(repo: str, sha: str, now: float) -> Verdict:
"""Judge one commit in one repository."""
if not Path(repo).is_dir():
return Verdict("unknown")
full = (
_git(repo, "rev-parse", "--verify", "-q", f"{sha}^{{commit}}") or ""
).strip()
if not full:
return Verdict("unknown")
ts = float((_git(repo, "show", "-s", "--format=%ct", full) or "0").strip() or 0)
if now - ts < WINDOW_S:
return Verdict("pending", full, ts)
if _git(repo, "merge-base", "--is-ancestor", full, "HEAD") is None:
return Verdict("unknown", full, ts)
until = f"@{int(ts + WINDOW_S)}"
end = (_git(repo, "rev-list", "-1", f"--before={until}", "HEAD") or "").strip()
log = _git(repo, "log", "--format=%B", f"--since=@{int(ts)}", f"--until={until}")
reverted = f"This reverts commit {full}" in (log or "")
files = _added(repo, full)
added = sum(n for n, _ in files)
kept = sum(_kept(repo, end or full, full, path) for _, path in files)
return Verdict(judge(added, kept, reverted=reverted), full, ts, added, kept)
def update(
conn: sqlite3.Connection, now: float, deadline: float, clock: Callable[[], float]
) -> bool:
"""Judge every session commit without a final verdict, within the deadline.
Returns whether the backlog was cleared. A verdict once final is never
recomputed, so the backlog is only commits that turned 14 days old.
"""
rows = conn.execute(
"SELECT c.sha, c.repo, c.ts FROM session_commits c LEFT JOIN"
" commit_verdicts v ON v.sha = c.sha WHERE v.verdict IS NULL"
" OR v.verdict = 'pending'"
).fetchall()
for sha, repo, ts in rows:
if now - ts < WINDOW_S:
continue # cannot be final yet; no git call needed
if clock() > deadline:
return False
found = evaluate(repo, sha, now)
conn.execute(
"INSERT OR REPLACE INTO commit_verdicts VALUES (?,?,?,?,?,?)",
(sha, repo, found.ts or ts, found.added, found.kept, found.verdict),
)
return True

View File

@ -0,0 +1,123 @@
"""Shared dashboard test setup: source isolation and transcript builders.
The autouse fixture keeps every test away from the real ``~/.claude`` state;
the builders below make synthetic transcript records.
"""
from __future__ import annotations
from datetime import UTC, datetime
import json
from typing import TYPE_CHECKING, Any
import pytest
from python_pkg.token_dashboard import ab, aux, context_files, walk
if TYPE_CHECKING:
from pathlib import Path
from python_pkg.token_dashboard.detail_rows import DetailRows
BASE = 1_791_600_000.0 # 2026-10-09, a fixed anchor so day buckets are stable
@pytest.fixture(autouse=True)
def _isolated_sources(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""Point every aux source at an empty tmp directory."""
claude = tmp_path / "claude-home"
monkeypatch.setattr(aux, "AUTOPSY", claude / "autopsy.jsonl")
monkeypatch.setattr(aux, "PROMPT_HISTORY", claude / "history.jsonl")
monkeypatch.setattr(aux, "TIME_CAP", claude / "time-cap.jsonl")
monkeypatch.setattr(ab, "LOG", claude / "agent-ab.jsonl")
monkeypatch.setattr(context_files, "CLAUDE_DIR", claude)
monkeypatch.setattr(context_files, "SRC_DIR", claude / "src")
def iso(offset: float) -> str:
"""ISO timestamp ``offset`` seconds after :data:`BASE`."""
return datetime.fromtimestamp(BASE + offset, tz=UTC).isoformat()
def prompt(text: str, at: float, **extra: object) -> dict[str, Any]:
"""A typed user prompt."""
return {
"type": "user",
"timestamp": iso(at),
"cwd": "/repo",
"message": {"role": "user", "content": text},
**extra,
}
def assistant(
mid: str,
at: float,
*,
output: int = 1000,
model: str = "claude-opus-5-5",
blocks: list[dict[str, Any]] | None = None,
context: int = 0,
**usage: object,
) -> dict[str, Any]:
"""One assistant record; ``context`` becomes cache-read tokens.
``usage`` adds raw usage fields, e.g. a cache-write split.
"""
return {
"type": "assistant",
"timestamp": iso(at),
"message": {
"id": mid,
"model": model,
"usage": {
"output_tokens": output,
"cache_read_input_tokens": context,
**usage,
},
"content": blocks or [{"type": "text", "text": "ok"}],
},
}
def use(tid: str, name: str, **tool_input: object) -> dict[str, Any]:
"""A ``tool_use`` content block."""
return {"type": "tool_use", "id": tid, "name": name, "input": tool_input}
def result(
tid: str, at: float, text: str = "x" * 40, **extra: object
) -> dict[str, Any]:
"""A user record carrying one ``tool_result``."""
return {
"type": "user",
"timestamp": iso(at),
"message": {
"role": "user",
"content": [{"type": "tool_result", "tool_use_id": tid, "content": text}],
},
**extra,
}
def write(path: Path, records: list[dict[str, Any]]) -> Path:
"""Write records as JSONL, creating parent directories."""
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text("\n".join(json.dumps(r) for r in records) + "\n", encoding="utf-8")
return path
def usd_opus_output(tokens: int) -> float:
"""List price of ``tokens`` Opus 5.5 output tokens (USD 20 / MTok)."""
return tokens * 20 / 1_000_000
def attach(at: float, **attachment: object) -> dict[str, Any]:
"""An ``attachment`` record."""
return {"type": "attachment", "timestamp": iso(at), "attachment": attachment}
def walk_detail(tmp_path: Path, records: list[dict[str, Any]]) -> DetailRows:
"""Walk one main-session file and return its detail rows."""
path = write(tmp_path / "proj" / "s.jsonl", records)
return walk.walk_file(path, tmp_path).rows.detail

View File

@ -0,0 +1,110 @@
from __future__ import annotations
import json
from typing import TYPE_CHECKING
import pytest
from python_pkg.token_dashboard import ab, ingest, schema
from python_pkg.token_dashboard.tests.conftest import (
assistant,
prompt,
result,
use,
write,
)
if TYPE_CHECKING:
from pathlib import Path
def test_wilson_matches_reference_values() -> None:
# Reference: Wilson score interval, z = 1.96 (Newcombe 1998, table I).
low, high = ab.wilson(81, 263)
assert (round(low, 4), round(high, 4)) == (0.2553, 0.3662)
assert ab.wilson(0, 10)[0] == 0.0
assert ab.wilson(0, 0) == (0.0, 1.0)
def test_newcombe_difference_contains_zero_for_equal_rates() -> None:
diff, low, high = ab.diff_ci(5, 50, 5, 50)
assert diff == 0.0
assert low < 0 < high
def test_jaccard_and_correction_phrases() -> None:
assert ab.jaccard("Find RTDB rules for tutor", "find rtdb rules tutor log") >= 0.6
assert ab.jaccard("Find RTDB rules", "Write the README") == 0.0
assert ab.is_correction("No, that's wrong")
assert ab.is_correction("you missed the second file")
assert not ab.is_correction("nothing else, thanks")
def _arm(n: int, redo: int, corr: int = 0) -> ab.ArmCounts:
return ab.ArmCounts(n=n, redo=redo, correction=corr)
@pytest.mark.parametrize(
("sonnet", "opus", "level"),
[
(_arm(10, 9), _arm(10, 0), "grey"),
(_arm(40, 20), _arm(40, 2), "red"),
(_arm(40, 2, 20), _arm(40, 2, 1), "red"),
(_arm(40, 6), _arm(40, 4), "green"),
(_arm(40, 0), _arm(40, 20), "green"), # Sonnet better is not a drop
],
)
def test_banner(sonnet: ab.ArmCounts, opus: ab.ArmCounts, level: str) -> None:
found = ab.banner(sonnet, opus)
assert found["level"] == level
assert f"n={min(sonnet.n, opus.n)}" in found["text"] or level == "red"
def test_rebuild_joins_assignments_to_transcripts(tmp_path: Path) -> None:
root = tmp_path / "projects"
first = use("t1", "Agent", description="Review the parser change")
again = use("t2", "Agent", description="Review the parser change again")
msg = use("t3", "SendMessage", to="abc", message="more")
write(
root / "p" / "s1.jsonl",
[
prompt("go", 0),
assistant("m1", 1, blocks=[first]),
result("t1", 50, "done", toolUseResult={"agentId": "abc"}),
assistant("m2", 60, blocks=[msg]),
result("t3", 61),
prompt("no, that's wrong", 100),
assistant("m3", 101, blocks=[again]),
result(
"t2", 150, "err", toolUseResult={"agentId": "def", "status": "error"}
),
],
)
log = tmp_path / "agent-ab.jsonl"
rows = [
{"tool_use_id": "t1", "arm": "sonnet", "class": "review", "session_id": "s1"},
{"tool_use_id": "t2", "arm": "opus", "class": "review", "session_id": "s1"},
]
log.write_text("\n".join(json.dumps(r) for r in rows), encoding="utf-8")
ingest.run(root, tmp_path / "t.sqlite", tmp_path / "report", 30.0)
conn = schema.connect(tmp_path / "t.sqlite")
payload = ab.rebuild(conn, log)
stored = conn.execute(
"SELECT tool_use_id, arm, key, main_key, is_sub, cwd, redo, followup,"
" error, correction, tag FROM ab_rows ORDER BY tool_use_id"
).fetchall()
payload = ab.rebuild(conn, log) # a rebuild replaces, never appends
count = conn.execute("SELECT COUNT(*) FROM ab_rows").fetchone()[0]
conn.close()
assert stored == [
("t1", "sonnet", "s1", "s1", 0, "/repo", 1, 1, 0, 1, "untagged"),
("t2", "opus", "s1", "s1", 0, "/repo", 0, 0, 1, 0, "untagged"),
]
assert count == 2
sonnet, opus = payload["arms"]["sonnet"], payload["arms"]["opus"]
assert (sonnet["n"], opus["n"]) == (1, 1)
assert sonnet["redo"]["k"] == 1
assert sonnet["followup"]["k"] == 1
assert sonnet["correction"]["k"] == 1
assert opus["error"]["k"] == 1
assert payload["banner"]["level"] == "grey"

View File

@ -0,0 +1,31 @@
from __future__ import annotations
from python_pkg.token_dashboard import active
def test_gap_over_limit_contributes_zero_not_the_limit() -> None:
spans = active.intervals([0, 60, 120, 1000, 1030])
assert spans == [(0, 120), (1000, 1030)]
assert active.measure(spans) == 150
def test_union_bridges_short_gaps_between_files() -> None:
main = [(0.0, 100.0)]
sub = [(250.0, 400.0)] # 150 s after main went quiet: still active
far = [(2000.0, 2010.0)]
assert active.measure(main + sub + far) == 410
def test_overlapping_intervals_count_once() -> None:
assert active.measure([(0.0, 100.0), (50.0, 150.0)]) == 150
def test_clip_to_a_prompt_window() -> None:
spans = [(0.0, 100.0), (1000.0, 1100.0)]
assert active.measure(spans, 50.0, 1050.0) == 100
assert active.measure([], 0.0, 10.0) == 0
def test_empty_and_single_stamp() -> None:
assert active.intervals([]) == []
assert active.intervals([5.0]) == [(5.0, 5.0)]

View File

@ -0,0 +1,173 @@
from __future__ import annotations
from datetime import UTC, datetime
import json
from typing import TYPE_CHECKING
import pytest
from python_pkg.token_dashboard import aux, schema
from python_pkg.token_dashboard.tests.test_levers import TABLE
if TYPE_CHECKING:
from collections.abc import Iterator, Sequence
from pathlib import Path
import sqlite3
NOON = datetime(2026, 10, 9, 12, tzinfo=UTC)
SECONDS = NOON.timestamp()
def _lines(path: Path, rows: Sequence[object], *junk: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
body = [json.dumps(r) for r in rows] + list(junk)
path.write_text("\n".join(body) + "\n", encoding="utf-8")
@pytest.fixture
def conn(tmp_path: Path) -> Iterator[sqlite3.Connection]:
opened = schema.connect(tmp_path / "t.sqlite")
yield opened
opened.close()
def test_jsonl_skips_blank_malformed_and_non_object_lines(tmp_path: Path) -> None:
path = tmp_path / "x.jsonl"
_lines(path, [{"a": 1}, [1, 2], 3], "", "{broken")
assert list(aux._jsonl(path)) == [{"a": 1}]
assert list(aux._jsonl(tmp_path / "missing.jsonl")) == []
def test_stamp_reads_iso_seconds_milliseconds_and_rejects_the_rest() -> None:
assert aux._stamp(NOON.isoformat()) == SECONDS
assert aux._stamp(SECONDS) == SECONDS
assert aux._stamp(int(SECONDS * 1000)) == pytest.approx(int(SECONDS))
assert aux._stamp("yesterday") is None
assert aux._stamp(["x"]) is None
assert aux._stamp(None) is None
def test_snapshots_copy_window_and_metrics_and_skip_unreadable(
tmp_path: Path, conn: sqlite3.Connection
) -> None:
history = tmp_path / "report" / "history"
history.mkdir(parents=True)
snap = {
"generated": NOON.isoformat(),
"window": {"since": 1.0, "until": 2.0},
"metrics": {"usd_total": 5},
}
(history / "2026-10-09.json").write_text(json.dumps(snap))
(history / "2026-10-10.json").write_text("{not json")
aux.snapshots(conn, tmp_path / "report")
rows = conn.execute("SELECT name, since, until, metrics FROM snapshots").fetchall()
assert rows == [("2026-10-09", 1.0, 2.0, '{"usd_total": 5}')]
def test_lever_rows_assign_each_table_to_the_nearest_snapshot(
tmp_path: Path, conn: sqlite3.Connection
) -> None:
report = tmp_path / "report"
(report / "history").mkdir(parents=True)
for day in ("2026-10-02", "2026-10-09"):
stamp = datetime.fromisoformat(f"{day}T12:00:00+00:00").isoformat()
(report / "history" / f"{day}.json").write_text(
json.dumps({"generated": stamp})
)
(report / "WEEKLY.md").write_text(TABLE, encoding="utf-8")
aux.snapshots(conn, report)
conn.execute("INSERT INTO lever_tables VALUES ('s', ?, ?)", (SECONDS, TABLE))
aux.lever_rows(conn, report)
sources = {r[0] for r in conn.execute("SELECT source FROM levers")}
assert "snapshot 2026-10-09" in sources
def test_lever_rows_with_no_snapshots_store_nothing(
tmp_path: Path, conn: sqlite3.Connection
) -> None:
conn.execute("INSERT INTO lever_tables VALUES ('s', ?, ?)", (SECONDS, TABLE))
aux.lever_rows(conn, tmp_path / "report")
assert conn.execute("SELECT COUNT(*) FROM levers").fetchone() == (0,)
def _autopsy_row(
sid: str, started: object, msgs: int, api: int | None
) -> dict[str, object]:
return {
"session_id": sid,
"meta": {"started_at": started, "cwd": "/r", "title": sid},
"tokens": {"input": 100, "cache_write": 200, "cache_read": 300, "output": 400},
"turns": {"api_turns": api},
"counts": {"assistant_msgs": msgs},
}
def test_autopsy_scales_by_api_turns_and_calibrates_against_transcripts(
conn: sqlite3.Connection,
) -> None:
rows = [
_autopsy_row("both", NOON.isoformat(), 10, 5),
_autopsy_row("alone", SECONDS, 4, None),
_autopsy_row("nostart", None, 4, 2),
_autopsy_row("nomsgs", SECONDS, 0, 0),
]
_lines(aux.AUTOPSY, rows)
conn.execute("INSERT INTO session_summary(key, usd_main) VALUES ('both', 3.0)")
aux.autopsy(conn)
got = dict(conn.execute("SELECT session_id, usd_est FROM aggregate_sessions"))
assert set(got) == {"both", "alone"}
# Same tokens: 5 API turns over 10 records scales the cost by one half.
assert got["both"] == pytest.approx(got["alone"] / 2)
factor = conn.execute("SELECT value FROM meta WHERE key='autopsy_calibration'")
assert float(factor.fetchone()[0]) == pytest.approx(3.0 / got["both"])
def test_autopsy_without_overlap_keeps_factor_one(conn: sqlite3.Connection) -> None:
aux.autopsy(conn)
factor = conn.execute("SELECT value FROM meta WHERE key='autopsy_calibration'")
assert factor.fetchone() == ("1.0",)
def test_activity_logs_count_prompts_events_and_first_estimate(
tmp_path: Path, conn: sqlite3.Connection
) -> None:
report = tmp_path / "report"
_lines(
aux.PROMPT_HISTORY,
[{"timestamp": int(SECONDS * 1000)}, {"timestamp": SECONDS}, {"x": 1}],
)
_lines(aux.TIME_CAP, [{"ts": SECONDS, "event": "kill", "command": "sleep 99"}])
misses = report / "task-size" / "misses.jsonl"
_lines(misses, [{"ts": NOON.isoformat(), "kind": "under", "first_prompt": "p"}])
verdicts = [
{"status": "ok", "session": "a", "verdict": {"est_calls": 12}},
{"status": "ok", "session": "a", "verdict": {"est_calls": 99}},
{"status": "ok", "session": "b", "verdict": "none"},
{"status": "error", "session": "c", "verdict": {"est_calls": 3}},
]
_lines(report / "task-size" / "verdicts.jsonl", verdicts)
aux.activity_logs(conn, report)
days = conn.execute("SELECT prompts FROM prompt_activity").fetchall()
events = conn.execute("SELECT source, kind, detail FROM events").fetchall()
estimates = conn.execute("SELECT session_id, est_calls FROM estimates").fetchall()
assert days == [(2,)]
assert events == [
("time-cap", "kill", "sleep 99"),
("task-size-miss", "under", "p"),
]
assert estimates == [("a", 12)]
def test_reloading_keeps_rows_a_trimmed_source_no_longer_has(tmp_path: Path) -> None:
conn = schema.connect(tmp_path / "t.sqlite")
_lines(
aux.PROMPT_HISTORY,
[{"timestamp": 1_700_000_000_000}, {"timestamp": 1_800_000_000}],
)
aux.activity_logs(conn, tmp_path / "report")
aux.PROMPT_HISTORY.write_text("", encoding="utf-8") # rotated away
aux.activity_logs(conn, tmp_path / "report")
aux.activity_logs(conn, tmp_path / "report")
days = conn.execute("SELECT COUNT(*), SUM(prompts) FROM prompt_activity").fetchone()
conn.close()
assert days == (2, 2)

View File

@ -0,0 +1,82 @@
from __future__ import annotations
import json
from typing import TYPE_CHECKING
import pytest
from python_pkg.token_dashboard import check_window, schema
if TYPE_CHECKING:
from collections.abc import Mapping
from pathlib import Path
DAY = 86400.0
def _store(tmp_path: Path, stamps: list[float]) -> Path:
db = tmp_path / "t.sqlite"
conn = schema.connect(db)
conn.executemany(
"INSERT INTO turns(key, ts, usd) VALUES ('k', ?, 1.0)", [(s,) for s in stamps]
)
conn.commit()
conn.close()
return db
def _snapshot(report: Path, name: str, metrics: Mapping[str, float]) -> None:
folder = report / "history"
folder.mkdir(parents=True, exist_ok=True)
snap = {"window": {"since": 0, "until": 10 * DAY}, "metrics": metrics}
(folder / f"{name}.json").write_text(json.dumps(snap), encoding="utf-8")
def _run(tmp_path: Path, db: Path, *extra: str) -> int:
return check_window.main(
["--db", str(db), "--report-dir", str(tmp_path / "report"), *extra]
)
def test_matching_snapshot_exits_zero(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
db = _store(tmp_path, [100.0, 100.0 + DAY])
good = {"usd_total": 2.0, "usd_per_day": 2.0, "calls": 2}
_snapshot(tmp_path / "report", "2026-10-16", {**good, "calls": 99})
_snapshot(tmp_path / "report", "2026-10-17", good)
assert _run(tmp_path, db, "--snapshot", "2026-10-17") == 0
out = capsys.readouterr().out
assert out.count("OK") == 3
assert "FAIL" not in out
def test_drift_beyond_one_percent_in_the_newest_snapshot_fails(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
db = _store(tmp_path, [100.0, 100.0 + DAY])
_snapshot(tmp_path / "report", "2026-10-16", {"usd_total": 2.0})
bad = {"usd_total": 2.5, "usd_per_day": 2.0, "calls": 2}
_snapshot(tmp_path / "report", "2026-10-17", bad)
assert _run(tmp_path, db) == 1
assert "usd_total: store 2.00 vs snapshot 2.50 (20.00%) FAIL" in (
capsys.readouterr().out
)
def test_empty_window_against_zero_metrics_is_a_match(tmp_path: Path) -> None:
db = _store(tmp_path, [])
zero = {"usd_total": 0, "usd_per_day": 0, "calls": 0}
_snapshot(tmp_path / "report", "2026-10-17", zero)
assert _run(tmp_path, db) == 0
def test_missing_snapshot_or_store_exits_one(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
db = _store(tmp_path, [1.0])
assert _run(tmp_path, db) == 1 # no snapshot directory at all
_snapshot(tmp_path / "report", "2026-10-17", {"usd_total": 1})
assert _run(tmp_path, db, "--snapshot", "1999-01-01") == 1
assert _run(tmp_path, tmp_path / "gone.sqlite") == 1
assert capsys.readouterr().err.count("missing snapshot or store") == 3

View File

@ -0,0 +1,124 @@
from __future__ import annotations
import pytest
from python_pkg.token_dashboard import classify
@pytest.mark.parametrize(
("command", "family"),
[
("ls -la", "ls"),
("git -C /repo status --short | head", "git status"),
("cd /x && git log --oneline -3", "git log"),
("~/.claude/scripts/capped.sh python3 -m pytest -q", "python3 -m pytest"),
(
"CAP_MEM=4G CAP_CPU_PCT=20 ~/.claude/scripts/capped.sh pnpm -s lint",
"pnpm lint",
),
("timeout 40 nice -n 19 grep -r x .", "grep"),
("python3 scripts/check.py --all", "python3 check.py"),
("bash ~/src/utils/scripts/gate_stamp.sh step x", "bash gate_stamp.sh"),
("/usr/bin/sqlite3 db 'SELECT 1'", "sqlite3"),
("cd /x", "cd"),
("python3 -m", "python3 -m"),
("python3 <<'EOF'\nprint(1)\nEOF", "python3"),
("bash -c 'x' > /dev/null", "bash -c"),
("git", "git"),
("FOO=1", "(empty)"),
("", "(empty)"),
],
)
def test_bash_family(command: str, family: str) -> None:
assert classify.bash_family(command) == family
@pytest.mark.parametrize(
("name", "tool_input", "family", "target"),
[
("Bash", {"command": "git status\n"}, "git status", "git status"),
("Bash", {}, "(empty)", ""),
(
"mcp__claude-in-chrome__navigate",
{"url": "x"},
"mcp:claude-in-chrome",
"navigate",
),
("Skill", {"skill": "dataviz"}, "skill", "dataviz"),
(
"Read",
{"file_path": "/h/.claude/rules/a.md"},
"Read:rules",
"/h/.claude/rules/a.md",
),
(
"Read",
{"file_path": "/h/.claude/projects/p/memory/m.md"},
"Read:memory",
"/h/.claude/projects/p/memory/m.md",
),
("Read", {"file_path": "/r/CLAUDE.md"}, "Read:claude-md", "/r/CLAUDE.md"),
(
"Edit",
{"file_path": "/h/.claude/skills/x/SKILL.md"},
"Edit:skill",
"/h/.claude/skills/x/SKILL.md",
),
(
"Write",
{"file_path": "/h/.claude/settings.json"},
"Write:claude",
"/h/.claude/settings.json",
),
("Read", {"file_path": "/r/shot.PNG"}, "Read:image", "/r/shot.PNG"),
("Read", {"file_path": "/r/a.py"}, "Read:file", "/r/a.py"),
("Read", {}, "Read:file", ""),
("Grep", {"pattern": "def x", "path": "/r"}, "Grep", "def x"),
(
"WebFetch",
{"url": "https://docs.example.com/a?b"},
"WebFetch",
"docs.example.com",
),
("WebSearch", {"query": "sqlite window"}, "WebSearch", "sqlite window"),
("Agent", {"subagent_type": "Explore"}, "Agent", "Explore"),
("Agent", {"isolation": "worktree"}, "Agent", "general-purpose [worktree]"),
("TaskStop", {"id": 1}, "TaskStop", ""),
],
)
def test_family_and_target(
name: str, tool_input: dict[str, object], family: str, target: str
) -> None:
assert classify.family(name, tool_input) == family
assert classify.target(name, tool_input) == target
def test_target_is_capped() -> None:
long = {"command": "echo " + "x" * 1000}
assert len(classify.target("Bash", long)) == classify.TARGET_CHARS
def test_signature_ignores_key_order_but_not_values() -> None:
one = classify.signature("Bash", {"command": "ls", "timeout": 5})
same = classify.signature("Bash", {"timeout": 5, "command": "ls"})
other = classify.signature("Bash", {"command": "ls -a", "timeout": 5})
assert one == same
assert one != other
assert len(one) == classify.SIG_CHARS
@pytest.mark.parametrize(
("text", "kind"),
[
("Exit code 1\nboom", "exit_code"),
("<tool_use_error>File has not been read yet</tool_use_error>", "tool_error"),
("The user doesn't want to proceed with this tool use.", "rejected"),
("Permission for this action was denied by the classifier", "denied"),
("PreToolUse:Bash hook error: blocked by policy", "hook"),
("TIME CAP: this command was killed at 60s", "time_cap"),
("[Request interrupted by user for tool use]", "interrupted"),
("something else", "error"),
],
)
def test_error_kind(text: str, kind: str) -> None:
assert classify.error_kind(text) == kind

View File

@ -0,0 +1,61 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from python_pkg.token_dashboard import commits, schema
from python_pkg.token_dashboard.tests.test_efficiency import MONDAY, _commit, _git
if TYPE_CHECKING:
from pathlib import Path
TRAILER = "\n\nCo-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
def test_git_log_attributes_trailer_commits_to_the_commit_command(
tmp_path: Path,
) -> None:
repo = tmp_path / "r"
repo.mkdir()
_git(repo, "init", "-q", when=MONDAY)
(repo / "a").write_text("1")
_git(repo, "add", "a", when=MONDAY)
_git(repo, "commit", "-qm", "ours" + TRAILER, when=MONDAY + 30)
ours = _git(repo, "rev-parse", "HEAD", when=MONDAY)
_commit(repo, "b", "2", MONDAY + 40) # no trailer: a human commit
_git(
repo,
"commit",
"-q",
"--allow-empty",
"-m",
"late" + TRAILER,
when=MONDAY + 9000,
)
conn = schema.connect(tmp_path / "t.sqlite")
rows = [
("s1", 0, MONDAY, MONDAY + 60, "", str(repo)),
("s2", 3, MONDAY - 5000, MONDAY - 4000, "", str(repo / "sub")),
("s3", 1, MONDAY, MONDAY + 1, "", str(tmp_path / "missing")),
]
conn.executemany("INSERT INTO commits VALUES (?,?,?,?,?,?)", rows)
commits.discover(conn)
found = conn.execute("SELECT sha, key, prompt_idx FROM session_commits").fetchall()
conn.close()
assert found == [(ours, "s1", 0)]
def test_a_printed_hash_is_attributed_even_without_trailer(tmp_path: Path) -> None:
repo = tmp_path / "r"
repo.mkdir()
_git(repo, "init", "-q", when=MONDAY)
sha = _commit(repo, "a", "1", MONDAY + 5)
conn = schema.connect(tmp_path / "t.sqlite")
conn.execute(
"INSERT INTO commits VALUES (?,?,?,?,?,?)",
("s1", 2, MONDAY, MONDAY + 9, sha[:7], str(repo)),
)
commits.discover(conn)
commits.discover(conn) # idempotent
found = conn.execute("SELECT sha, key FROM session_commits").fetchall()
conn.close()
assert found == [(sha, "s1")]

View File

@ -0,0 +1,103 @@
from __future__ import annotations
import json
import os
import subprocess
from typing import TYPE_CHECKING
from python_pkg.token_dashboard import context_files, gitcmd, schema
if TYPE_CHECKING:
from pathlib import Path
def _tree(tmp_path: Path) -> tuple[Path, Path]:
claude, src = tmp_path / ".claude", tmp_path / "src"
for rel in (
"CLAUDE.md",
"rules/a.instructions.md",
"rules/deep/b.md",
"memories/mistakes.md",
"skills/x/SKILL.md",
"agents/reviewer.md",
"projects/-home-kuhy/memory/MEMORY.md",
"backups/rules-ondemand/not-loaded.md",
):
(claude / rel).parent.mkdir(parents=True, exist_ok=True)
(claude / rel).write_text("# x\n" * 10, encoding="utf-8")
(src / "repo").mkdir(parents=True)
(src / "repo" / "CLAUDE.md").write_text("## Commands\n", encoding="utf-8")
(src / "repo" / "AGENTS.md").write_text("agents\n", encoding="utf-8")
return claude, src
def test_enumerates_every_loaded_file_and_nothing_on_demand(tmp_path: Path) -> None:
claude, src = _tree(tmp_path)
found = {
f.path.replace(str(tmp_path), ""): f for f in context_files.find(claude, src)
}
assert set(found) == {
"/.claude/CLAUDE.md",
"/.claude/rules/a.instructions.md",
"/.claude/rules/deep/b.md",
"/.claude/memories/mistakes.md",
"/.claude/skills/x/SKILL.md",
"/.claude/agents/reviewer.md",
"/.claude/projects/-home-kuhy/memory/MEMORY.md",
"/src/repo/CLAUDE.md",
"/src/repo/AGENTS.md",
}
assert found["/.claude/rules/deep/b.md"].standing == "always"
assert found["/src/repo/CLAUDE.md"].standing == "in repo"
assert found["/.claude/skills/x/SKILL.md"].standing == "description"
assert found["/.claude/projects/-home-kuhy/memory/MEMORY.md"].limit == 24_400
def _git(repo: Path, *args: str, when: int) -> None:
env = {**os.environ, "GIT_AUTHOR_DATE": f"@{when} +0000"}
env |= {"GIT_COMMITTER_DATE": f"@{when} +0000", "HOME": str(repo)}
env |= {"GIT_AUTHOR_NAME": "t", "GIT_AUTHOR_EMAIL": "t@t"}
env |= {"GIT_COMMITTER_NAME": "t", "GIT_COMMITTER_EMAIL": "t@t"}
subprocess.run(
[gitcmd.GIT, "-C", str(repo), *args], env=env, check=True, capture_output=True
)
def test_size_history_from_git_and_prune_sizes(tmp_path: Path) -> None:
claude, src = _tree(tmp_path)
_git(claude, "init", "-q", when=1000)
_git(claude, "add", "CLAUDE.md", when=1000)
_git(claude, "commit", "-qm", "a", when=1000)
(claude / "CLAUDE.md").write_text("longer\n" * 50, encoding="utf-8")
_git(claude, "commit", "-qam", "b", when=2000)
prune = claude / "token-report" / "prune" / "2026-10-12"
prune.mkdir(parents=True)
(prune / "sizes.json").write_text(json.dumps({"file_bytes": {"CLAUDE.md": 77}}))
conn = schema.connect(tmp_path / "t.sqlite")
context_files.refresh(conn, claude, src)
rows = conn.execute(
"SELECT ts, bytes, source FROM context_sizes WHERE path LIKE '%/CLAUDE.md'"
" AND path NOT LIKE '%/src/%' ORDER BY ts"
).fetchall()
listed = conn.execute("SELECT COUNT(*) FROM context_files").fetchone()[0]
context_files.refresh(conn, claude, src) # incremental: no duplicates
again = conn.execute("SELECT COUNT(*) FROM context_sizes").fetchone()[0]
conn.close()
assert [(r[1], r[2]) for r in rows[:2]] == [(40, "git"), (350, "git")]
assert (77, "prune 2026-10-12") in [(r[1], r[2]) for r in rows]
assert listed == 9
assert again == len(rows)
def test_price_mix_weights_cache_read_price_by_calls(tmp_path: Path) -> None:
conn = schema.connect(tmp_path / "t.sqlite")
rows = [
("claude-opus-5-5", 100.0),
("claude-opus-5-5", 101.0),
("claude-haiku-4-5", 102.0),
("claude-haiku-4-5", 1.0),
]
conn.executemany("INSERT INTO turns (model, ts) VALUES (?, ?)", rows)
price = context_files.price_mix(conn, now=200.0, days=150 / 86400)
conn.close()
assert round(price, 4) == round((0.2 * 2 + 0.1) / 3, 4)

View File

@ -0,0 +1,164 @@
"""Per-call and per-turn detail rows the walker keeps."""
from __future__ import annotations
from typing import TYPE_CHECKING
import pytest
from python_pkg.token_dashboard import detail
from python_pkg.token_dashboard.tests.conftest import (
assistant,
iso,
prompt,
result,
use,
walk_detail,
)
if TYPE_CHECKING:
from pathlib import Path
ONE_HOUR_WRITE = {
"cache_creation_input_tokens": 1000,
"cache_creation": {"ephemeral_1h_input_tokens": 1000},
}
def test_result_creation_and_carry_are_priced_by_hand(tmp_path: Path) -> None:
rows = walk_detail(
tmp_path,
[
prompt("go", 0),
assistant("m1", 2, blocks=[use("t1", "Bash", command="git status")]),
result("t1", 5, "x" * 398), # json.dumps -> 400 chars -> 100 tokens
assistant("m2", 9, **ONE_HOUR_WRITE),
assistant("m3", 12, context=2000),
],
)
(call,) = rows.calls
assert (call.family, call.target, call.batch) == ("git status", "git status", 1)
assert call.result_tokens == 100
assert call.end_ts - call.where.ts == pytest.approx(3)
# Written once by m2 at the 1h rate (2x input $4/MTok) ...
assert call.cost.create_usd == pytest.approx(100 * 2.0 * 4 / 1e6)
# ... then cache-read by m2 and m3 at $0.20/MTok.
assert call.cost.carry_usd == pytest.approx(100 * 2 * 0.2 / 1e6)
assert call.cost.carry_calls == 2
assert call.where.model == "claude-opus-5-5"
def test_compaction_stops_the_carry(tmp_path: Path) -> None:
boundary = {
"type": "system",
"subtype": "compact_boundary",
"timestamp": iso(4),
"compactMetadata": {
"trigger": "auto",
"preTokens": 100000,
"postTokens": 9000,
"durationMs": 5000,
},
}
read = use("t1", "Read", file_path="/a")
rows = walk_detail(
tmp_path,
[
prompt("go", 0),
assistant("m1", 1, blocks=[read], context=100000),
result("t1", 2, "x" * 398),
assistant("m2", 3, context=100000),
boundary,
assistant("m3", 5, context=9000),
],
)
assert rows.calls[0].cost.carry_calls == 1
(mark,) = [m for m in rows.marks if m.kind == "compaction"]
assert (mark.name, mark.tokens, mark.extra) == ("auto", 100000, 9000)
assert mark.duration_ms == 5000
assert mark.cost.carry_usd == 0 # the summary call is not context
def test_batch_errors_images_and_signatures(tmp_path: Path) -> None:
image = {"type": "image", "source": {"type": "base64", "data": "A" * 9000}}
blocks = [use("a", "Bash", command="ls"), use("b", "mcp__chrome__shot")]
failed = {
"type": "tool_result",
"tool_use_id": "a",
"content": "Exit code 2",
"is_error": True,
}
shot = {
"type": "tool_result",
"tool_use_id": "b",
"content": [image, {"type": "text", "text": "ok"}],
}
rows = walk_detail(
tmp_path,
[
prompt("go", 0),
assistant("m1", 1, blocks=blocks[:1]),
assistant("m1", 1, blocks=blocks[1:]),
{"type": "user", "timestamp": iso(2), "message": {"content": [failed]}},
{"type": "user", "timestamp": iso(3), "message": {"content": [shot]}},
assistant("m2", 4),
],
)
by_name = {c.name: c for c in rows.calls}
assert {c.batch for c in rows.calls} == {2}
assert by_name["Bash"].error_kind == "exit_code"
found = by_name["mcp__chrome__shot"]
assert (found.family, found.target, found.images) == ("mcp:chrome", "shot", 1)
assert found.error_kind == ""
assert found.result_tokens < 3000 # an image is priced by area, not base64
assert by_name["Bash"].sig != found.sig
def test_turn_detail_latency_cache_split_and_output_split(tmp_path: Path) -> None:
text = {"type": "text", "text": "y" * 400} # ~100 tokens of reply
thinking = {"type": "thinking", "thinking": ""}
rows = walk_detail(
tmp_path,
[
prompt("go", 0),
assistant("m1", 7, **ONE_HOUR_WRITE, blocks=[thinking]),
assistant("m1", 8, blocks=[text]),
assistant("m2", 9, output=10, blocks=[text]),
assistant("m3", 10, output=50, blocks=[{"type": "server_tool_use"}]),
assistant("m4", 11, output=40, blocks=[thinking]),
],
)
first, second, third, fourth = rows.turns
assert first.latency_s == pytest.approx(7)
assert (first.cw_1h, first.cw_5m) == (1000, 0)
assert (first.out_text, first.out_tool, first.out_think) == (100, 0, 900)
# More visible text than billed output: scaled down, no thinking left.
assert (second.out_text, second.out_think) == (10, 0)
assert second.latency_s == pytest.approx(1)
# Nothing visible: all of it is text, or thinking when a block says so.
assert (third.out_text, third.out_think) == (50, 0)
assert (fourth.out_text, fourth.out_think) == (0, 40)
def test_text_and_tool_output_without_thinking_split_by_length(
tmp_path: Path,
) -> None:
blocks = [{"type": "text", "text": "y" * 400}, use("t", "Bash", command="z" * 385)]
rows = walk_detail(
tmp_path, [prompt("go", 0), assistant("m1", 1, output=1000, blocks=blocks)]
)
(turn,) = rows.turns
assert (turn.out_text, turn.out_tool, turn.out_think) == (500, 500, 0)
assert turn.tool_calls == 1
def test_write_multiplier_blends_the_split() -> None:
assert detail.write_multiplier({}) == pytest.approx(1.25)
half = {"cache_creation_input_tokens": 100, "cache_creation_1h": 50}
assert detail.write_multiplier(half) == pytest.approx((50 * 2 + 50 * 1.25) / 100)
def test_result_for_an_unknown_call_is_ignored() -> None:
found = detail.Detail()
found.result({"tool_use_id": "nope", "content": "x"}, 1.0)
assert found.rows.calls == []

View File

@ -0,0 +1,198 @@
"""Context marks: hooks, injected attachments, API errors, interruptions."""
from __future__ import annotations
from typing import TYPE_CHECKING
import pytest
from python_pkg.token_dashboard.tests.conftest import (
BASE,
assistant,
attach,
iso,
prompt,
result,
use,
walk_detail,
)
if TYPE_CHECKING:
from pathlib import Path
CTX = "cd chain wrapped in a subshell"
def _hook_records() -> list[dict[str, object]]:
run = {"type": "hook_success", "hookName": "PreToolUse:Bash"}
run |= {"hookEvent": "PreToolUse", "toolUseID": "t1", "content": ""}
stop = {"blockingError": "no status tag", "command": "python3 /h/tag_stop.py"}
return [
attach(
0,
type="hook_success",
hookName="SessionStart:startup",
hookEvent="SessionStart",
toolUseID="s",
content="x" * 400,
stdout="x" * 400,
command="bash /h/.claude/hooks/start.sh",
durationMs=30,
),
prompt("go", 1),
assistant("m1", 2, blocks=[use("t1", "Bash", command="cd /x && ls")]),
attach(
3,
type="hook_additional_context",
hookName="PreToolUse:Bash",
hookEvent="PreToolUse",
toolUseID="t1",
content=[CTX],
),
attach(
3,
**run,
stdout='{"additionalContext":"' + CTX + '"}',
command="bash /h/.claude/hooks/cd_chain.sh",
durationMs=15,
),
attach(3, **run, stdout="{}", command="bash /h/other.sh", durationMs="7"),
result("t1", 4),
assistant("m2", 5),
attach(
6,
type="hook_blocking_error",
hookName="Stop",
toolUseID="z",
blockingError=stop,
),
assistant("m3", 7, output=500),
assistant("m4", 8, output=500),
prompt("next", 20),
assistant("m5", 21),
attach(22, type="hook_system_message", hookName="UserPromptSubmit"),
attach(
22, type="hook_non_blocking_error", hookName="PostToolUse", content="oops"
),
attach(22, type="nested_memory", path="/r/CLAUDE.md", content={"c": "z"}),
attach(22, type="prompt_snapshot", systemPrompt=["huge"]),
]
def test_hooks_are_marked_matched_and_their_retry_priced(tmp_path: Path) -> None:
rows = walk_detail(tmp_path, _hook_records())
marks = {(m.kind, m.detail): m for m in rows.marks}
start = marks[("hook_run", "start.sh")]
assert (start.tokens, start.duration_ms, start.where.prompt_idx) == (100, 30, -1)
assert start.cost.carry_usd > 0
assert start.where.model == "claude-opus-5-5" # filled from the first turn
assert marks[("hook_context", "cd_chain.sh")].tokens == len(CTX) // 4
assert marks[("hook_run", "other.sh")].duration_ms == 7
assert marks[("hook_run", "cd_chain.sh")].tokens == 0 # JSON is not injected
bounce = marks[("hook_block", "tag_stop.py")]
assert bounce.name == "Stop"
assert bounce.retry_usd == pytest.approx(2 * 500 * 20 / 1e6) # m3 + m4 only
assert ("hook_msg", "") in marks
assert marks[("hook_error", "")].tokens == 1
assert marks[("attach", "/r/CLAUDE.md")].name == "nested_memory"
assert not [m for m in rows.marks if m.name == "prompt_snapshot"]
def test_pretool_block_retry_is_only_the_next_call(tmp_path: Path) -> None:
rows = walk_detail(
tmp_path,
[
prompt("go", 0),
assistant("m1", 1),
attach(
2,
type="hook_blocking_error",
hookName="PreToolUse:Bash",
toolUseID="q",
blockingError="denied",
),
assistant("m2", 3, output=100),
assistant("m3", 4, output=100),
],
)
(block,) = rows.marks
assert (block.detail, block.tokens) == ("", 1)
assert block.retry_usd == pytest.approx(100 * 20 / 1e6)
def test_api_errors_interruptions_and_pasted_images(tmp_path: Path) -> None:
pasted = {"type": "image", "source": {"type": "base64", "data": "A"}}
limit = {"isApiErrorMessage": True, "error": "rate_limit"}
limit |= {"apiError": "usage_limit_reached"}
text = [{"type": "text", "text": "Overloaded"}]
rows = walk_detail(
tmp_path,
[
{
"type": "user",
"timestamp": iso(0),
"message": {"content": [{"type": "text", "text": "look"}, pasted]},
},
{"type": "assistant", "timestamp": iso(1), **limit, "message": {}},
{
"type": "assistant",
"timestamp": iso(2),
"isApiErrorMessage": True,
"message": {"content": text},
},
{
"type": "user",
"timestamp": iso(3),
"message": {
"content": [
{"type": "text", "text": "[Request interrupted by user]"}
]
},
},
assistant("m1", 4),
],
)
kinds = sorted((m.kind, m.name, m.detail) for m in rows.marks)
assert kinds == [
("api_error", "rate_limit", "usage_limit_reached"),
("api_error", "unknown", "Overloaded"),
("image", "user", ""),
("interrupt", "user", ""),
]
assert rows.marks[0].where.ts == pytest.approx(BASE)
assert rows.marks[0].tokens > 0
def test_repeats_merge_and_odd_records_are_tolerated(tmp_path: Path) -> None:
reminder = {"type": "total_tokens_reminder", "left": 5}
odd = {"hookName": "PostToolUse", "toolUseID": "u"}
rows = walk_detail(
tmp_path,
[
prompt("go", 0),
attach(1, **reminder),
attach(2, **reminder),
attach(
2,
type="hook_success",
**odd,
content="",
stdout="",
command="",
durationMs="n/a",
),
attach(2, type="hook_additional_context", **odd, content=""),
attach(2, type="hook_additional_context", **odd, content="unmatched"),
attach(2, type="hook_cancelled", **odd),
{"type": "system", "subtype": "turn_duration", "timestamp": iso(2)},
assistant("m1", 3, blocks=[{"type": "server_tool_use", "id": "w"}]),
],
)
(merged,) = [m for m in rows.marks if m.name == "total_tokens_reminder"]
assert merged.count == 2
assert merged.where.ts == pytest.approx(BASE + 1)
assert merged.cost.carry_calls == 2
run = next(m for m in rows.marks if m.kind == "hook_run")
assert (run.duration_ms, run.detail) == (0, "")
contexts = [m for m in rows.marks if m.kind == "hook_context"]
assert {m.detail for m in contexts} == {""}

View File

@ -0,0 +1,79 @@
"""The detail tables are written per file, replaced on re-walk, filterable."""
from __future__ import annotations
from contextlib import closing
import sqlite3
from typing import TYPE_CHECKING
from python_pkg.token_dashboard import ingest
from python_pkg.token_dashboard.tests.conftest import (
assistant,
iso,
prompt,
result,
use,
write,
)
if TYPE_CHECKING:
from pathlib import Path
COUNTS = (
"SELECT (SELECT COUNT(*) FROM calls), (SELECT COUNT(*) FROM marks),"
" (SELECT COUNT(*) FROM turn_detail)"
)
def _records(extra: int) -> list[dict[str, object]]:
hook = {
"type": "attachment",
"timestamp": iso(1),
"attachment": {"type": "hook_system_message", "hookName": "Stop"},
}
return [
prompt("go", 0),
hook,
assistant("m1", 2, blocks=[use("t1", "Bash", command="ls")]),
result("t1", 3),
*[assistant(f"x{i}", 4 + i) for i in range(extra)],
]
def _ingest(tmp_path: Path) -> closing[sqlite3.Connection]:
ingest.run(
root=tmp_path / "projects",
db_path=tmp_path / "t.sqlite",
report_dir=tmp_path / "reports",
budget_s=30.0,
)
return closing(sqlite3.connect(tmp_path / "t.sqlite"))
def test_rewalk_replaces_detail_rows(tmp_path: Path) -> None:
path = tmp_path / "projects" / "proj" / "s1.jsonl"
write(path, _records(1))
with _ingest(tmp_path) as conn:
assert conn.execute(COUNTS).fetchone() == (1, 1, 2)
write(path, _records(2))
with _ingest(tmp_path) as conn:
assert conn.execute(COUNTS).fetchone() == (1, 1, 3)
def test_detail_rows_carry_the_shared_filter_columns(tmp_path: Path) -> None:
write(tmp_path / "projects" / "proj" / "s1.jsonl", _records(1))
with _ingest(tmp_path) as conn:
call = conn.execute(
"SELECT key, main_key, is_sub, cwd, model, day, family, target, batch,"
" carry_calls FROM calls"
).fetchone()
mark = conn.execute(
"SELECT key, main_key, is_sub, cwd, model, kind, name, count FROM marks"
).fetchone()
turn = conn.execute(
"SELECT key, is_sub, cwd, model, latency_s FROM turn_detail ORDER BY ts"
).fetchone()
assert call[:5] == ("s1", "s1", 0, "/repo", "claude-opus-5-5")
assert call[6:] == ("ls", "ls", 1, 1)
assert mark == ("s1", "s1", 0, "/repo", "claude-opus-5-5", "hook_msg", "Stop", 1)
assert turn == ("s1", 0, "/repo", "claude-opus-5-5", 1.0)

View File

@ -0,0 +1,146 @@
from __future__ import annotations
from datetime import UTC, datetime
import os
from pathlib import Path
import subprocess
import pytest
from python_pkg.token_dashboard import efficiency, gitcmd, survival
DAY = 86400.0
MONDAY = datetime(2026, 10, 5, 12, tzinfo=UTC).timestamp()
def _s(ts: float, usd: float, tag: str | None) -> efficiency.SessionCost:
return efficiency.SessionCost(key=f"k{ts}{tag}", first_ts=ts, usd=usd, tag=tag)
def test_cost_per_done_counts_failed_sessions_against_done_ones() -> None:
rows = [
_s(MONDAY, 10.0, "DONE"),
_s(MONDAY + 1, 6.0, "QUESTION"),
_s(MONDAY + 2, 4.0, "DONE"),
_s(MONDAY + 3, 99.0, None), # pre-convention: excluded from the ratio
]
(week,) = efficiency.done_weekly(rows)
assert week["done"] == 2
assert week["other"] == 1
assert week["untagged"] == 1
assert week["usd_per_done"] == pytest.approx(10.0)
assert week["done_share"] == pytest.approx(2 / 3)
def test_week_with_no_done_has_no_ratio() -> None:
(week,) = efficiency.done_weekly([_s(MONDAY, 5.0, "RUNNING")])
assert week["usd_per_done"] is None
def test_cost_per_surviving_commit_and_pending() -> None:
week_usd = {efficiency.week_of(MONDAY): 30.0}
verdicts = [
(MONDAY, "survived"),
(MONDAY, "survived"),
(MONDAY, "churned"),
(MONDAY, "pending"),
]
(week,) = efficiency.commits_weekly(verdicts, week_usd)
assert (week["survived"], week["lost"], week["pending"]) == (2, 1, 1)
assert week["usd_per_survivor"] == pytest.approx(15.0)
def test_cost_per_estimated_unit() -> None:
rows = [_s(MONDAY, 8.0, None), _s(MONDAY + 5, 2.0, "DONE")]
estimates = {rows[0].key: 30, rows[1].key: 10}
(week,) = efficiency.estimate_weekly(rows, estimates)
assert week["est_calls"] == 40
assert week["usd_per_unit"] == pytest.approx(0.25)
@pytest.mark.parametrize(
("added", "kept", "reverted", "expected"),
[
(10, 10, False, "survived"),
(10, 5, False, "survived"),
(10, 4, False, "churned"),
(10, 10, True, "reverted"),
(0, 0, False, "survived"),
],
)
def test_survival_rule(added: int, kept: int, *, reverted: bool, expected: str) -> None:
assert survival.judge(added, kept, reverted=reverted) == expected
def _git(repo: Path, *args: str, when: float) -> str:
stamp = f"@{int(when)} +0000"
env = {**os.environ, "GIT_AUTHOR_DATE": stamp, "GIT_COMMITTER_DATE": stamp}
env |= {"GIT_AUTHOR_NAME": "t", "GIT_AUTHOR_EMAIL": "t@t", "HOME": str(repo)}
env |= {"GIT_COMMITTER_NAME": "t", "GIT_COMMITTER_EMAIL": "t@t"}
done = subprocess.run(
[gitcmd.GIT, "-C", str(repo), *args],
env=env,
check=True,
capture_output=True,
text=True,
)
return done.stdout.strip()
def _commit(repo: Path, name: str, text: str, when: float) -> str:
(repo / name).write_text(text, encoding="utf-8")
_git(repo, "add", name, when=when)
_git(repo, "commit", "-qm", name, when=when)
return _git(repo, "rev-parse", "HEAD", when=when)
def test_git_survival_against_a_real_repo(tmp_path: Path) -> None:
repo = tmp_path / "r"
repo.mkdir()
_git(repo, "init", "-q", when=MONDAY)
t0 = MONDAY - 40 * DAY
kept = _commit(repo, "a.txt", "1\n2\n3\n4\n", t0)
churn = _commit(repo, "b.txt", "1\n2\n3\n4\n", t0 + 1)
_commit(repo, "b.txt", "x\ny\nz\n4\n", t0 + 2 * DAY)
young = _commit(repo, "c.txt", "1\n", MONDAY)
now = MONDAY + 1
assert survival.evaluate(str(repo), kept[:8], now).verdict == "survived"
churned = survival.evaluate(str(repo), churn, now)
assert (churned.verdict, churned.added, churned.kept) == ("churned", 4, 1)
assert survival.evaluate(str(repo), young, now).verdict == "pending"
assert survival.evaluate(str(repo), "deadbeef", now).verdict == "unknown"
assert survival.evaluate(str(tmp_path / "nope"), kept, now).verdict == "unknown"
def test_git_revert_within_window_is_reverted(tmp_path: Path) -> None:
repo = tmp_path / "r"
repo.mkdir()
_git(repo, "init", "-q", when=MONDAY)
t0 = MONDAY - 40 * DAY
_commit(repo, "a.txt", "base\n", t0 - 10)
bad = _commit(repo, "b.txt", "1\n", t0)
_git(repo, "revert", "--no-edit", bad, when=t0 + DAY)
assert survival.evaluate(str(repo), bad, MONDAY).verdict == "reverted"
def test_update_judges_old_commits_and_stops_at_the_deadline(tmp_path: Path) -> None:
from python_pkg.token_dashboard import schema
repo = tmp_path / "r"
repo.mkdir()
_git(repo, "init", "-q", when=MONDAY)
old = _commit(repo, "a.txt", "1\n", MONDAY - 40 * DAY)
conn = schema.connect(tmp_path / "t.sqlite")
rows = [
(old, str(repo), MONDAY - 40 * DAY, "s", 0),
("f" * 40, "x", MONDAY, "s", 0),
]
conn.executemany("INSERT INTO session_commits VALUES (?,?,?,?,?)", rows)
assert not survival.update(conn, MONDAY, deadline=0.0, clock=lambda: 1.0)
assert survival.update(conn, MONDAY, deadline=2.0, clock=lambda: 1.0)
efficiency.rebuild(conn, MONDAY)
verdicts = conn.execute("SELECT verdict FROM commit_verdicts").fetchall()
payload = conn.execute("SELECT value FROM meta WHERE key='efficiency'").fetchone()
conn.close()
assert verdicts == [("survived",)]
assert '"pending": 1' in payload[0]

View File

@ -0,0 +1,54 @@
"""Git-backed edge cases: caching, binary files, commits outside HEAD."""
from __future__ import annotations
from typing import TYPE_CHECKING
from python_pkg.token_dashboard import commits, survival
from python_pkg.token_dashboard.tests.test_efficiency import DAY, MONDAY, _commit, _git
if TYPE_CHECKING:
from pathlib import Path
def _repo(tmp_path: Path) -> Path:
repo = tmp_path / "r"
repo.mkdir()
_git(repo, "init", "-q", when=MONDAY)
return repo
def test_toplevel_is_resolved_once_per_hint(tmp_path: Path) -> None:
repo = _repo(tmp_path)
_commit(repo, "a", "1", MONDAY)
cache: dict[str, str | None] = {}
first = commits._toplevel(str(repo), cache)
assert first == str(repo.resolve())
cache[str(repo)] = "cached"
assert commits._toplevel(str(repo), cache) == "cached"
assert commits._toplevel("", cache) is None
def test_binary_and_empty_files_do_not_count_as_added_lines(tmp_path: Path) -> None:
repo = _repo(tmp_path)
old = MONDAY - 40 * DAY
(repo / "blob.bin").write_bytes(b"\x00\x01\x02")
(repo / "empty.txt").write_text("", encoding="utf-8")
(repo / "t.txt").write_text("1\n2\n", encoding="utf-8")
_git(repo, "add", ".", when=old)
_git(repo, "commit", "-qm", "mixed", when=old)
sha = _git(repo, "rev-parse", "HEAD", when=old)
found = survival.evaluate(str(repo), sha, MONDAY)
assert (found.verdict, found.added, found.kept) == ("survived", 2, 2)
def test_a_commit_that_is_not_an_ancestor_of_head_is_unknown(tmp_path: Path) -> None:
repo = _repo(tmp_path)
old = MONDAY - 40 * DAY
_commit(repo, "a", "1", old)
trunk = _git(repo, "branch", "--show-current", when=old)
_git(repo, "checkout", "-qb", "side", when=old)
side = _commit(repo, "b", "2", old + 1)
_git(repo, "checkout", "-q", trunk, when=old)
found = survival.evaluate(str(repo), side, MONDAY)
assert (found.verdict, found.sha) == ("unknown", side)

View File

@ -0,0 +1,116 @@
"""Edge cases of the walker, the summaries and the aux readers."""
from __future__ import annotations
import json
from typing import TYPE_CHECKING
from python_pkg.token_dashboard import ab, context_files, finalize, ingest, schema, walk
from python_pkg.token_dashboard.tests.conftest import (
assistant,
prompt,
result,
use,
write,
)
if TYPE_CHECKING:
from pathlib import Path
def test_timestampless_orphans_and_garbage_lines_are_tolerated(tmp_path: Path) -> None:
path = tmp_path / "proj" / "s.jsonl"
path.parent.mkdir()
lines = ['{"type": "summary", "summary": "x"}', "{broken", ""]
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
facts = walk.walk_file(path, tmp_path)
assert facts.span == (0.0, 0.0)
assert facts.rows.turns == []
assert facts.final_tag is None
def test_a_result_without_its_call_is_ignored(tmp_path: Path) -> None:
path = write(tmp_path / "proj" / "s.jsonl", [prompt("go", 0), result("nope", 1)])
facts = walk.walk_file(path, tmp_path)
assert facts.rows.tools == []
def test_a_nested_subagent_resolves_to_the_prompt_that_started_the_chain(
tmp_path: Path,
) -> None:
root = tmp_path / "projects"
outer = use("ta", "Agent", description="outer")
inner = use("tb", "Agent", description="inner")
write(
root / "p" / "s1.jsonl",
[
assistant("m0", 0), # a turn before any prompt
prompt("go", 5),
assistant("m1", 10, blocks=[outer]),
result("ta", 11, "ok", toolUseResult={"agentId": "aaa"}),
],
)
subs = root / "p" / "s1" / "subagents"
write(
subs / "agent-aaa.jsonl",
[
assistant("a1", 20, blocks=[inner]),
result("tb", 21, toolUseResult={"agentId": "bbb"}),
],
)
write(subs / "agent-bbb.jsonl", [assistant("b1", 30)])
ingest.run(root, tmp_path / "t.sqlite", tmp_path / "report", 30.0)
conn = schema.connect(tmp_path / "t.sqlite")
links = conn.execute(
"SELECT sub_key, prompt_idx, depth FROM subagent_link ORDER BY depth"
).fetchall()
first = conn.execute(
"SELECT idx, text FROM prompt_summary WHERE key='s1' ORDER BY idx"
).fetchone()
conn.close()
assert links == [("s1/agent-aaa", 0, 1), ("s1/agent-bbb", 0, 2)]
assert first == (-1, "(before the first prompt)")
def test_refreshing_an_unknown_session_changes_nothing(tmp_path: Path) -> None:
conn = schema.connect(tmp_path / "t.sqlite")
finalize.refresh(conn, "ghost")
assert conn.execute("SELECT COUNT(*) FROM session_summary").fetchone() == (0,)
conn.close()
def test_unreadable_prune_sizes_are_skipped(tmp_path: Path) -> None:
claude, src = tmp_path / "claude", tmp_path / "src"
prune = claude / "token-report" / "prune"
for name, body in (
("a", "{broken"),
("b", "[1]"),
("c", '{"file_bytes": {"X.md": 9}}'),
):
(prune / name).mkdir(parents=True)
(prune / name / "sizes.json").write_text(body, encoding="utf-8")
conn = schema.connect(tmp_path / "t.sqlite")
context_files.refresh(conn, claude, src)
sources = conn.execute("SELECT source FROM context_sizes").fetchall()
conn.close()
assert sources == [("prune c",)]
def test_unmatched_and_malformed_assignments_are_counted_unjoined(
tmp_path: Path,
) -> None:
log = tmp_path / "agent-ab.jsonl"
rows = [
{"tool_use_id": "ghost", "arm": "sonnet"},
{"tool_use_id": "t1", "arm": "haiku"},
{"tool_use_id": "t2"},
[1],
]
log.write_text("\n".join(json.dumps(r) for r in rows) + "\n{broken\n")
conn = schema.connect(tmp_path / "t.sqlite")
payload = ab.rebuild(conn, log)
default = ab.rebuild(conn) # the isolated, absent default log
conn.close()
assert payload["unjoined"] == 2
assert payload["arms"]["sonnet"]["n"] == 0
assert default["unjoined"] == 0

View File

@ -0,0 +1,94 @@
"""Edge cases of the small pure helpers: blocks, gitcmd, levers, schema, efficiency."""
from __future__ import annotations
import json
import sqlite3
import subprocess
from typing import TYPE_CHECKING
import pytest
from python_pkg.token_dashboard import blocks, efficiency, gitcmd, levers, schema
if TYPE_CHECKING:
from pathlib import Path
def test_text_of_joins_text_blocks_and_ignores_the_rest() -> None:
content = [
{"type": "text", "text": "one"},
{"type": "image"},
"stray",
{"type": "text", "text": "two"},
]
assert blocks.text_of(content) == "one\ntwo"
assert blocks.text_of("plain") == "plain"
assert blocks.text_of(None) == ""
assert blocks.text_of({"type": "text", "text": "dict"}) == ""
def test_compact_summaries_and_tool_results_are_not_prompts() -> None:
message = {"role": "user", "content": "summary of the chat"}
summary = {"type": "user", "isCompactSummary": True, "message": message}
assert blocks.prompt_text(summary) is None
visible = {"type": "user", "isVisibleInTranscriptOnly": True, "message": message}
assert blocks.prompt_text(visible) is None
tool = [{"type": "tool_result", "tool_use_id": "t", "content": "x"}]
reply = {"type": "user", "message": {"content": tool}}
assert blocks.prompt_text(reply) is None
def test_repo_hint_falls_back_to_cwd_and_resolves_relative_targets() -> None:
assert blocks.repo_hint("git commit -m x", None) == ""
assert blocks.repo_hint("git commit -m x", "/work/r") == "/work/r"
assert blocks.repo_hint("cd sub && git commit", "/work/r") == "/work/r/sub"
assert blocks.repo_hint("git -C /abs/repo commit", "/work/r") == "/abs/repo"
def test_lever_table_needs_the_heading_and_at_least_one_row() -> None:
heading = f"## {blocks.LEVER_HEADING}\n\n"
assert blocks.lever_table("nothing here") is None
assert blocks.lever_table(heading + "just prose") is None
assert (
blocks.lever_table(heading + "| a | b |\n| c | d |") == "| a | b |\n| c | d |"
)
@pytest.mark.parametrize(
"error", [OSError("no git"), subprocess.TimeoutExpired("git", 1)]
)
def test_git_run_never_raises(
error: Exception, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
def boom(*_args: object, **_kwargs: object) -> None:
raise error
monkeypatch.setattr("python_pkg.token_dashboard.gitcmd.subprocess.run", boom)
assert gitcmd.run(tmp_path, "status") is None
def test_unreadable_or_non_object_snapshots_are_skipped(tmp_path: Path) -> None:
history = tmp_path / "history"
history.mkdir()
(history / "2026-10-01.json").write_text("{broken")
(history / "2026-10-02.json").write_text(json.dumps([1, 2]))
(history / "2026-10-03.json").write_text(json.dumps({"metrics": {}}))
assert levers._snapshots(tmp_path) == [("2026-10-03", {"metrics": {}})]
def test_readonly_store_refuses_writes(tmp_path: Path) -> None:
db = tmp_path / "t.sqlite"
schema.connect(db).close()
conn = schema.connect(db, readonly=True)
try:
assert conn.execute("SELECT COUNT(*) FROM turns").fetchone() == (0,)
with pytest.raises(sqlite3.OperationalError, match="readonly"):
conn.execute("INSERT INTO meta VALUES ('k', 'v')")
finally:
conn.close()
def test_commits_with_an_unrecognised_verdict_count_as_unknown() -> None:
(week,) = efficiency.commits_weekly([(1.0, "mystery")], {})
assert (week["unknown"], week["survived"], week["lost"]) == (1, 0, 0)

View File

@ -0,0 +1,181 @@
from __future__ import annotations
import json
import sqlite3
from typing import TYPE_CHECKING
import pytest
from python_pkg.token_dashboard import ingest
from python_pkg.token_dashboard.tests.conftest import (
assistant,
prompt,
result,
usd_opus_output,
use,
write,
)
if TYPE_CHECKING:
from collections.abc import Callable, Iterator
from pathlib import Path
def _tree(root: Path) -> None:
agent = use("ta", "Agent", description="Find x", subagent_type="Explore")
write(
root / "proj" / "s1.jsonl",
[
prompt("first", 0),
assistant("m1", 10, blocks=[agent]),
result("ta", 11, "launched", toolUseResult={"agentId": "abc"}),
assistant("m2", 20),
prompt("second", 1000),
assistant("m3", 1010, blocks=[{"type": "text", "text": "DONE: x"}]),
],
)
subdir = root / "proj" / "s1" / "subagents"
write(subdir / "agent-abc.jsonl", [prompt("task", 12), assistant("a1", 200)])
(subdir / "agent-abc.meta.json").write_text(
json.dumps({"agentType": "Explore", "description": "Find x"}),
encoding="utf-8",
)
# An agent that outlived /clear: nothing in s1 spawned it.
write(subdir / "agent-zzz.jsonl", [prompt("orphan", 30), assistant("z1", 40)])
def _run(tmp_path: Path, budget: float = 30.0) -> ingest.Outcome:
return ingest.run(
root=tmp_path / "projects",
db_path=tmp_path / "t.sqlite",
report_dir=tmp_path / "report",
budget_s=budget,
)
@pytest.fixture
def db(tmp_path: Path) -> Iterator[Callable[[], sqlite3.Connection]]:
opened: list[sqlite3.Connection] = []
def _open() -> sqlite3.Connection:
opened.append(sqlite3.connect(tmp_path / "t.sqlite"))
return opened[-1]
yield _open
for conn in opened:
conn.close()
def test_session_cost_reconciles_to_prompts_plus_unattributed(
tmp_path: Path, db: Callable[[], sqlite3.Connection]
) -> None:
_tree(tmp_path / "projects")
outcome = _run(tmp_path)
assert outcome.complete
conn = db()
usd, active_s, wall_s, tag = conn.execute(
"SELECT usd, active_s, wall_s, final_tag FROM session_summary WHERE key='s1'"
).fetchone()
assert usd == pytest.approx(usd_opus_output(1000) * 5)
prompts = conn.execute(
"SELECT idx, usd_own + usd_sub FROM prompt_summary WHERE key='s1' ORDER BY idx"
).fetchall()
unattributed = conn.execute(
"SELECT SUM(usd) FROM subagent_link WHERE main_key='s1' AND prompt_idx IS NULL"
).fetchone()[0]
assert sum(v for _, v in prompts) + unattributed == pytest.approx(usd)
# Prompt 0 carries its own two turns plus the Explore agent it spawned.
assert prompts[0][1] == pytest.approx(usd_opus_output(1000) * 3)
assert wall_s == 1010
# Union of main + subagent activity: 0..200 bridged, then 1000..1010.
assert active_s == pytest.approx(210)
assert tag == "DONE"
def test_subagent_links_to_its_prompt_via_agent_id(
tmp_path: Path, db: Callable[[], sqlite3.Connection]
) -> None:
_tree(tmp_path / "projects")
_run(tmp_path)
rows = dict(
db().execute("SELECT sub_key, prompt_idx FROM subagent_link").fetchall()
)
assert rows == {"s1/agent-abc": 0, "s1/agent-zzz": None}
def test_rerun_skips_unchanged_and_rows_outlive_deleted_files(
tmp_path: Path, db: Callable[[], sqlite3.Connection]
) -> None:
_tree(tmp_path / "projects")
_run(tmp_path)
assert _run(tmp_path).parsed == 0
(tmp_path / "projects" / "proj" / "s1.jsonl").unlink()
_run(tmp_path)
count = db().execute("SELECT COUNT(*) FROM turns").fetchone()[0]
assert count == 5
def test_changed_file_is_reparsed_without_duplicating_rows(
tmp_path: Path, db: Callable[[], sqlite3.Connection]
) -> None:
_tree(tmp_path / "projects")
_run(tmp_path)
path = tmp_path / "projects" / "proj" / "s1.jsonl"
with path.open("a", encoding="utf-8") as handle:
handle.write(json.dumps(assistant("m4", 1020)) + "\n")
assert _run(tmp_path).parsed == 1
count = db().execute("SELECT COUNT(*) FROM turns").fetchone()[0]
assert count == 6
def test_zero_budget_leaves_work_pending_and_resumes(tmp_path: Path) -> None:
_tree(tmp_path / "projects")
first = _run(tmp_path, budget=0.0)
assert not first.complete
assert first.parsed == 1 # always makes progress
rest = _run(tmp_path)
assert rest.complete
assert first.parsed + rest.parsed == 3
def test_daily_rollup_matches_turns(
tmp_path: Path, db: Callable[[], sqlite3.Connection]
) -> None:
_tree(tmp_path / "projects")
_run(tmp_path)
conn = db()
total, calls, sub = conn.execute(
"SELECT SUM(usd), SUM(calls), SUM(usd_sub) FROM daily"
).fetchone()
assert total == pytest.approx(usd_opus_output(1000) * 5)
assert calls == 5
assert sub == pytest.approx(usd_opus_output(1000) * 2)
def test_cli_exit_codes(tmp_path: Path) -> None:
_tree(tmp_path / "projects")
args = [
"--root",
str(tmp_path / "projects"),
"--db",
str(tmp_path / "t.sqlite"),
"--report-dir",
str(tmp_path / "report"),
]
assert ingest.main([*args, "--budget", "0"]) == ingest.EXIT_PENDING
assert ingest.main(args) == 0
def test_session_with_no_prompts_and_no_main_file(
tmp_path: Path, db: Callable[[], sqlite3.Connection]
) -> None:
sub = tmp_path / "projects" / "proj" / "gone" / "subagents" / "agent-q.jsonl"
write(sub, [assistant("q1", 5), assistant("q2", 6)])
assert _run(tmp_path).complete
usd, prompts = (
db()
.execute("SELECT usd, prompts FROM session_summary WHERE key='gone'")
.fetchone()
)
assert usd == pytest.approx(usd_opus_output(1000) * 2)
assert prompts == 0

View File

@ -0,0 +1,114 @@
from __future__ import annotations
import json
from typing import TYPE_CHECKING
from python_pkg.token_dashboard import levers
if TYPE_CHECKING:
from pathlib import Path
TABLE = """## Candidate levers (rule-based)
| category | lever | evidence | est $/day | action |
|---|---|---|---|---|
| cheaper agents | Opus runs most subagent work | subagents 64% | $14.47 | route |
| agent mechanics | Large Bash results | 40% of Bash | - | read ranges |
## Next
"""
def test_parse_candidate_table() -> None:
rows = levers.parse_candidates(TABLE)
assert [r["title"] for r in rows] == [
"Opus runs most subagent work",
"Large Bash results",
]
assert rows[0]["est_usd_per_day"] == 14.47
assert rows[1]["est_usd_per_day"] is None
assert rows[0]["metric"] == "subagent_opus_usd_share"
def test_ledger_date_falls_back_to_text_then_unknown() -> None:
assert levers.ledger_date({"date": "2026-10-04", "lever": "x"}) == "2026-10-04"
assert levers.ledger_date({"lever": "weekly prefix prune 2026-10-05"}) == (
"2026-10-05"
)
assert levers.ledger_date({"lever": "no date"}) is None
def _report(tmp_path: Path) -> Path:
report = tmp_path / "report"
(report / "history").mkdir(parents=True)
ledger = [
{
"date": "2026-10-04",
"lever": "routing",
"metric": "subagent_opus_usd_share",
"baseline": 0.98,
"predicted": 0.55,
},
{
"lever": "weekly prefix prune 2026-10-05",
"metric": "standing_tokens_est",
"baseline": 22187,
"predicted": 19620,
},
]
(report / "ledger.jsonl").write_text(
"\n".join(json.dumps(e) for e in ledger), encoding="utf-8"
)
for day, value in (("2026-10-10", 0.8), ("2026-10-17", 0.84)):
snap = {
"generated": f"{day}T10:00:00+00:00",
"metrics": {"subagent_opus_usd_share": value, "standing_tokens_est": 23670},
}
(report / "history" / f"{day}.json").write_text(json.dumps(snap))
for day in ("2026-10-05", "2026-10-12"):
(report / "prune" / day).mkdir(parents=True)
(report / "prune" / day / "PROPOSAL.md").write_text(
f"# Prefix prune proposal {day}\n\nModest week. Estimated ~ -0.35k.\n"
)
return report
def test_build_rows_status_and_outcome(tmp_path: Path) -> None:
report = _report(tmp_path)
tables = {"2026-10-10": TABLE}
rows = levers.build(report, tables)
applied = [r for r in rows if r["source"] == "ledger"]
assert [r["lever"] for r in applied] == [
"routing",
"weekly prefix prune 2026-10-05",
]
assert applied[0]["outcome"] == "helped"
assert applied[1]["outcome"] == "regressed"
assert applied[1]["date"] == "2026-10-05"
status = {r["source"]: r["status"] for r in rows if r["source"].startswith("prune")}
assert status == {"prune 2026-10-05": "applied", "prune 2026-10-12": "proposed"}
# The 10-10 Opus candidate was followed by no ledger line on its metric
# and a newer snapshot exists, so it was not applied.
opus = next(r for r in rows if r["lever"] == "Opus runs most subagent work")
assert opus["status"] == "not applied"
def test_structured_levers_in_a_snapshot_win_over_markdown(tmp_path: Path) -> None:
report = _report(tmp_path)
snap_path = report / "history" / "2026-10-17.json"
snap = json.loads(snap_path.read_text())
snap["levers"] = [
{
"category": "c",
"title": "Structured one",
"evidence": "e",
"action": "a",
"est_usd_per_day": 1.5,
"metric": "usd_per_day",
},
]
snap_path.write_text(json.dumps(snap))
rows = levers.build(report, {})
found = next(r for r in rows if r["lever"] == "Structured one")
assert found["status"] == "proposed"
assert found["source"] == "snapshot 2026-10-17"

View File

@ -0,0 +1,83 @@
from __future__ import annotations
import sqlite3
from typing import TYPE_CHECKING
import pytest
from python_pkg.token_dashboard import schema
if TYPE_CHECKING:
from pathlib import Path
def _v1_store(path: Path) -> None:
"""A store as schema v1 wrote it: commits without end_ts, no A/B columns."""
conn = sqlite3.connect(path)
conn.executescript(
"CREATE TABLE meta(key TEXT PRIMARY KEY, value TEXT);"
"INSERT INTO meta VALUES ('schema', '1');"
"CREATE TABLE commits(key TEXT, prompt_idx INT, ts REAL, sha TEXT,"
" repo TEXT);"
"INSERT INTO commits VALUES ('s1', 0, 5.0, 'abc', '/r');"
"CREATE TABLE agent_calls(key TEXT, tool_use_id TEXT, agent_id TEXT,"
" prompt_idx INT, ts REAL, description TEXT, subagent_type TEXT,"
" model TEXT);"
"INSERT INTO agent_calls VALUES ('s1','t','a',0,1.0,'d','x','m');"
)
conn.commit()
conn.close()
def test_old_store_is_migrated_in_place_and_keeps_its_rows(tmp_path: Path) -> None:
path = tmp_path / "t.sqlite"
_v1_store(path)
conn = schema.connect(path)
kept = conn.execute("SELECT key, sha, end_ts FROM commits").fetchall()
calls = conn.execute("SELECT tool_use_id, result_ts, status FROM agent_calls")
version = conn.execute("SELECT value FROM meta WHERE key='schema'").fetchone()
assert kept == [("s1", "abc", None)]
assert calls.fetchall() == [("t", None, None)]
assert version == (schema.VERSION,)
conn.close()
def test_newer_store_is_refused_not_relabelled(tmp_path: Path) -> None:
path = tmp_path / "t.sqlite"
conn = sqlite3.connect(path)
conn.executescript(
"CREATE TABLE meta(key TEXT PRIMARY KEY, value TEXT);"
"INSERT INTO meta VALUES ('schema', '999');"
)
conn.commit()
conn.close()
with pytest.raises(schema.SchemaError):
schema.connect(path)
check = sqlite3.connect(path)
assert check.execute("SELECT value FROM meta").fetchone() == ("999",)
check.close()
def _meta_only(path: Path, version: str | None) -> None:
conn = sqlite3.connect(path)
conn.execute("CREATE TABLE meta(key TEXT PRIMARY KEY, value TEXT)")
if version is not None:
conn.execute("INSERT INTO meta VALUES ('schema', ?)", (version,))
conn.commit()
conn.close()
def test_meta_without_a_version_is_refused(tmp_path: Path) -> None:
_meta_only(tmp_path / "t.sqlite", None)
with pytest.raises(schema.SchemaError):
schema.connect(tmp_path / "t.sqlite")
def test_table_missing_from_an_old_store_is_created_at_todays_shape(
tmp_path: Path,
) -> None:
_meta_only(tmp_path / "t.sqlite", "1")
conn = schema.connect(tmp_path / "t.sqlite")
columns = {r[1] for r in conn.execute("PRAGMA table_info(commits)")}
conn.close()
assert "end_ts" in columns

View File

@ -0,0 +1,166 @@
from __future__ import annotations
from typing import TYPE_CHECKING
import pytest
from python_pkg.token_dashboard import walk
from python_pkg.token_dashboard.tests.conftest import (
assistant,
prompt,
result,
usd_opus_output,
use,
write,
)
if TYPE_CHECKING:
from pathlib import Path
def _main(tmp_path: Path, records: list[dict[str, object]]) -> walk.FileFacts:
path = write(tmp_path / "proj" / "sess-1.jsonl", records)
return walk.walk_file(path, tmp_path)
def test_one_api_message_written_as_three_records_is_priced_once(
tmp_path: Path,
) -> None:
blocks = [use("t1", "Bash", command="ls"), use("t2", "Read", file_path="/a")]
facts = _main(
tmp_path,
[
prompt("do it", 0),
assistant("m1", 1, blocks=[{"type": "thinking", "thinking": ""}]),
assistant("m1", 1, blocks=blocks[:1]),
assistant("m1", 1, blocks=blocks[1:]),
result("t1", 2),
result("t2", 2),
],
)
assert len(facts.rows.turns) == 1
assert facts.rows.turns[0].usd == pytest.approx(usd_opus_output(1000))
# The one turn's dollars split evenly across the two tools it issued.
by_name = {t.name: t for t in facts.rows.tools}
assert by_name["Bash"].usd == pytest.approx(usd_opus_output(500))
assert by_name["Bash"].calls == 1
assert by_name["Read"].calls == 1
assert by_name["Read"].result_tokens > 0
def test_prompts_split_turns_and_text_only_turns_go_to_no_tool(
tmp_path: Path,
) -> None:
facts = _main(
tmp_path,
[
prompt("first " + "a" * 300, 0, promptId="p1"),
assistant("m1", 10),
prompt("second", 100),
assistant("m2", 110),
assistant("m3", 120),
],
)
assert [p.idx for p in facts.rows.prompts] == [0, 1]
assert len(facts.rows.prompts[0].text) <= walk.PROMPT_CHARS
assert facts.rows.prompts[0].prompt_id == "p1"
assert [t.prompt_idx for t in facts.rows.turns] == [0, 1, 1]
assert {t.name for t in facts.rows.tools} == {walk.NO_TOOL}
def test_tool_results_and_meta_records_are_not_prompts(tmp_path: Path) -> None:
facts = _main(
tmp_path,
[
prompt("real", 0),
prompt("<system-reminder>injected</system-reminder>", 1),
prompt("hidden", 2, isMeta=True),
prompt("[Request interrupted by user]", 3),
prompt("<command-name>/clear</command-name>", 4),
assistant("m1", 5, blocks=[use("t1", "Bash", command="ls")]),
result("t1", 6),
],
)
assert [p.text for p in facts.rows.prompts] == ["real", "/clear"]
def test_agent_call_and_commit_and_final_tag_are_captured(tmp_path: Path) -> None:
agent = use("ta", "Agent", description="Find x", subagent_type="Explore")
commit = use("tc", "Bash", command="git -C ~/src/foo commit -m x")
facts = _main(
tmp_path,
[
prompt("go", 0),
assistant("m1", 1, blocks=[agent]),
result("ta", 2, "launched", toolUseResult={"agentId": "abc"}),
assistant("m2", 3, blocks=[commit]),
result("tc", 4, "[main 1a2b3c4d] x\n 1 file changed"),
assistant(
"m3", 5, blocks=[{"type": "text", "text": "x\nDONE: ok\nNOTE: y"}]
),
],
)
call = facts.rows.agent_calls[0]
assert (call.tool_use_id, call.agent_id, call.description) == (
"ta",
"abc",
"Find x",
)
assert call.subagent_type == "Explore"
assert facts.rows.commits[0].sha == "1a2b3c4d"
assert facts.rows.commits[0].repo.endswith("/src/foo")
assert facts.final_tag == "DONE"
@pytest.mark.parametrize(
("text", "tag"),
[
("all good", None),
("**NEEDS ACTION:** run it", "NEEDS ACTION"),
("RUNNING: tests", "RUNNING"),
],
)
def test_final_tag_variants(tmp_path: Path, text: str, tag: str | None) -> None:
blocks = [{"type": "text", "text": text}]
facts = _main(tmp_path, [prompt("go", 0), assistant("m1", 1, blocks=blocks)])
assert facts.final_tag == tag
def test_lever_table_in_a_tool_result_is_kept(tmp_path: Path) -> None:
table = (
"## Candidate levers (rule-based)\n\n| category | lever |\n|---|---|\n"
"| a | b |\n\nafter"
)
facts = _main(
tmp_path,
[
prompt("go", 0),
assistant("m1", 1, blocks=[use("t1", "Bash", command="x")]),
result("t1", 2, table),
],
)
assert facts.rows.lever_tables[0][1].splitlines()[-1] == "| a | b |"
def test_activity_intervals_break_on_gaps_over_five_minutes(tmp_path: Path) -> None:
facts = _main(
tmp_path,
[prompt("a", 0), assistant("m1", 60), prompt("b", 1000), assistant("m2", 1100)],
)
spans = [(round(e - s)) for s, e in facts.rows.intervals]
assert spans == [60, 100]
assert facts.span[1] - facts.span[0] == 1100
def test_subagent_file_reads_meta_and_parent(tmp_path: Path) -> None:
sub = tmp_path / "proj" / "sess-1" / "subagents" / "agent-abc.jsonl"
write(sub, [prompt("task", 0), assistant("m1", 1, model="claude-sonnet-5-5")])
sub.with_suffix(".meta.json").write_text(
'{"agentType": "Explore", "description": "Find", "toolUseId": "ta"}',
encoding="utf-8",
)
facts = walk.walk_file(sub, tmp_path)
assert facts.key == "sess-1/agent-abc"
assert facts.parent_session == "sess-1"
assert facts.meta["toolUseId"] == "ta"
assert facts.rows.prompts == []

View File

@ -0,0 +1,226 @@
"""Walk one transcript file into the rows the store keeps for it.
Reuses token_audit's record primitives -- line loading, usage extraction,
per-API-message dedup (:class:`TurnMerger`) and list-price pricing -- rather
than :func:`parse.iter_events`, because the dashboard also needs what that
stream drops: every record's timestamp (active time), prompt boundaries, the
``Agent`` calls that spawned subagents, and commit hashes.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from python_pkg.token_audit import discover, parse, pricing, records, turns
from python_pkg.token_dashboard import active, blocks, carry, detail
from python_pkg.token_dashboard.blocks import PROMPT_CHARS
from python_pkg.token_dashboard.rows import (
AgentCall,
Commit,
FileFacts,
PromptRow,
ToolRow,
TurnRow,
)
if TYPE_CHECKING:
from pathlib import Path
from python_pkg.token_audit.model import Turn
__all__ = ["NO_TOOL", "PROMPT_CHARS", "FileFacts", "walk_file"]
NO_TOOL = "(no tool)"
AGENT_TOOLS = frozenset({"Agent", "Task"})
_BOOK = pricing.load()
class _Walker:
"""Single pass over a file's records, holding the cross-record state."""
def __init__(self, facts: FileFacts) -> None:
self.facts = facts
self.prompt = -1
self.ts = 0.0
self.stamps: list[float] = []
self.pending: dict[str, dict[str, Any]] = {}
self.names: dict[str, list[str]] = {}
self.use_ts: dict[str, float] = {}
self.tools: dict[tuple[int, str], ToolRow] = {}
self.carry = carry.Tracker()
self.detail = detail.Detail()
self.merger = records.TurnMerger()
self.last_text = ""
def tool(self, name: str) -> ToolRow:
"""The accumulator for ``name`` in the current prompt."""
return self.tools.setdefault((self.prompt, name), ToolRow(self.prompt, name))
def release(self, turn: Turn | None) -> None:
"""Price a completed API call and split it across its tools."""
if turn is None:
return
cost = pricing.usd(turn, _BOOK)
usage = turn.usage
self.facts.rows.turns.append(
TurnRow(
turn.timestamp or self.ts,
turn.model,
cost,
turn.context,
self.prompt,
usage,
)
)
self.carry.turn(turn.model, turn.context)
self.detail.turn(turn, cost, self.prompt)
names = self.names.pop(turn.message_id, []) or [NO_TOOL]
for name in names:
self.tool(name).usd += cost / len(names)
def record(self, rec: dict[str, Any]) -> None:
"""Account one record."""
stamp = records.timestamp(rec)
if stamp is not None:
self.ts = stamp
self.stamps.append(stamp)
if self.facts.cwd is None and isinstance(rec.get("cwd"), str):
self.facts.cwd = rec["cwd"]
if rec.get("type") == "user":
self.release(self.merger.flush())
self.user(rec)
self.detail.record(rec, self.ts, self.prompt)
for block in parse.content_blocks(rec):
self.block(rec, block)
found = turns.usage_of(rec)
if found is not None:
self.release(self.merger.push(turns.build_turn(rec, *found)))
def user(self, rec: dict[str, Any]) -> None:
"""Open a new prompt when this user record is one."""
text = blocks.prompt_text(rec)
if text is None or self.facts.parent_session is not None:
return
self.prompt += 1
prompt_id = rec.get("promptId")
self.facts.rows.prompts.append(
PromptRow(self.prompt, self.ts, str(prompt_id or ""), text)
)
def block(self, rec: dict[str, Any], block: dict[str, Any]) -> None:
"""Remember tool calls, pair results, keep the last reply text."""
kind = block.get("type")
if kind == "tool_use":
self.tool_use(rec, block)
elif kind == "tool_result":
self.tool_result(rec, block)
elif kind == "text" and rec.get("type") == "assistant":
self.last_text = str(block.get("text") or "")
def tool_use(self, rec: dict[str, Any], block: dict[str, Any]) -> None:
"""Hold a call until its result arrives; note which message issued it."""
call_id = str(block.get("id") or "")
self.pending[call_id] = block
self.use_ts[call_id] = self.ts
message = rec.get("message")
mid = message.get("id") if isinstance(message, dict) else None
name = str(block.get("name") or "unknown")
self.names.setdefault(str(mid or ""), []).append(name)
self.detail.use(rec, block, self.ts, self.prompt)
def tool_result(self, rec: dict[str, Any], block: dict[str, Any]) -> None:
"""Pair a result with its call and record what it tells us."""
call_id = str(block.get("tool_use_id") or "")
use = self.pending.get(call_id)
paired = parse.pair_result(block, self.pending)
if use is None or paired is None:
return
self.detail.result(block, self.ts)
row = self.tool(paired.name)
row.calls += 1
row.result_tokens += paired.result_tokens
self.carry.result(row, paired.result_tokens)
text = blocks.text_of(block.get("content"))
raw_input = use.get("input")
tool_input: dict[str, Any] = raw_input if isinstance(raw_input, dict) else {}
if paired.name in AGENT_TOOLS:
failed = bool(block.get("is_error"))
self.agent_call(rec, call_id, tool_input, failed=failed)
elif paired.name == "SendMessage":
self.facts.rows.messages.append((self.ts, str(tool_input.get("to") or "")))
if paired.commit:
# Every commit command is kept, hash or not: most run through
# finish_auto.sh with a tailed or backgrounded output, so the hash
# is usually found later by :mod:`commits` from git log instead.
hint = blocks.repo_hint(str(tool_input.get("command")), self.facts.cwd)
started = self.use_ts.pop(call_id, self.ts)
self.facts.rows.commits += [
Commit(self.prompt, started, self.ts, sha, hint)
for sha in blocks.commit_shas(text) or [""]
]
table = blocks.lever_table(text)
if table is not None:
self.facts.rows.lever_tables.append((self.ts, table))
def agent_call(
self,
rec: dict[str, Any],
call_id: str,
tool_input: dict[str, Any],
*,
failed: bool,
) -> None:
"""Record a subagent spawn with the ``agentId`` its result names."""
result = rec.get("toolUseResult")
result = result if isinstance(result, dict) else {}
agent_id = result.get("agentId")
status = "error" if failed else str(result.get("status") or "")
self.facts.rows.agent_calls.append(
AgentCall(
tool_use_id=call_id,
agent_id=str(agent_id or ""),
prompt_idx=self.prompt,
ts=self.use_ts.get(call_id, self.ts),
description=str(tool_input.get("description") or ""),
subagent_type=str(tool_input.get("subagent_type") or ""),
model=str(tool_input.get("model") or ""),
result_ts=self.ts,
status=status,
)
)
def finish(self) -> FileFacts:
"""Flush state into the facts."""
self.release(self.merger.flush())
facts = self.facts
facts.rows.tools = list(self.tools.values())
self.carry.settle(_BOOK)
facts.rows.detail = self.detail.finish(_BOOK)
facts.rows.intervals = active.intervals(self.stamps)
if self.stamps:
facts.span = (min(self.stamps), max(self.stamps))
facts.final_tag = blocks.final_tag(self.last_text)
return facts
def walk_file(path: Path, root: Path) -> FileFacts:
"""Read one transcript (main or subagent) into :class:`FileFacts`."""
del root # keys are derived from the path's own layout
if path.parent.name == "subagents":
parent = path.parent.parent.name
facts = FileFacts(
key=f"{parent}/{path.stem}",
session_id=parent,
parent_session=parent,
cwd=None,
meta=discover.agent_meta(path),
)
else:
facts = FileFacts(path.stem, path.stem, None, None, {})
walker = _Walker(facts)
with path.open(encoding="utf-8", errors="replace") as handle:
for line in handle:
rec = parse.load_line(line)
if rec is not None:
walker.record(rec)
return walker.finish()