diff --git a/src/ClaudeDo.Worker/Usage/TokenTracker/TokenTrackerModels.cs b/src/ClaudeDo.Worker/Usage/TokenTracker/TokenTrackerModels.cs
new file mode 100644
index 00000000..b5eb6137
--- /dev/null
+++ b/src/ClaudeDo.Worker/Usage/TokenTracker/TokenTrackerModels.cs
@@ -0,0 +1,66 @@
+namespace ClaudeDo.Worker.Usage.TokenTracker;
+
+/// One row of tokentracker sessions --format json. Field names mirror the
+/// export's snake_case keys: cached_input_tokens is the cache *read* count,
+/// cache_creation_input_tokens the write count.
+/// ⚠️ is not unique across rows — TokenTracker splits one
+/// Claude session into several rows (measured: 886 rows over 547 hashes), e.g. on a model switch.
+/// The segments are disjoint, so summing rows that share a hash is correct; counting them is not
+/// the same as counting sessions.
+public sealed record TokenTrackerSession(
+ string SessionHash,
+ string Source,
+ string Model,
+ DateTimeOffset StartedAt,
+ long InputTokens,
+ long OutputTokens,
+ long CacheReadTokens,
+ long CacheCreationTokens,
+ double CostUsd,
+ int Turns,
+ int RetryTurns,
+ bool Productive,
+ bool OneShot);
+
+/// A parsed export plus when we fetched it. is the
+/// version field of the session rows — the only compatibility signal the export gives us.
+public sealed record TokenTrackerExport(
+ int FormatVersion,
+ IReadOnlyList Sessions,
+ DateTime FetchedAtUtc);
+
+public sealed record TokenTrackerProbe(
+ bool Installed,
+ string? Version,
+ bool NodeOk,
+ string? NodeVersion,
+ string? Error)
+{
+ public static TokenTrackerProbe Unknown { get; } = new(false, null, false, null, null);
+}
+
+/// Outcome of one CLI invocation. false always carries an
+/// — callers turn that into state, never into an exception.
+public sealed record TokenTrackerRunResult(bool Ok, string StdOut, string? Error);
+
+/// Aggregated per date/model/scope. Scope is "claudedo" or "other",
+/// decided by whether the session hash belongs to one of our runs. Sessions replaces the
+/// old assistant-message count — it counts export rows (session segments), because the
+/// export has neither message granularity nor one row per session.
+public sealed record TokenTrackerModelRow(
+ DateOnly Date,
+ string Model,
+ string Scope,
+ long InputTokens,
+ long OutputTokens,
+ long CacheReadTokens,
+ long CacheCreationTokens,
+ int Sessions,
+ double CostUsd);
+
+/// What the export adds on top of what task_runs already knows about a task.
+public sealed record TokenTrackerTaskExtras(
+ double CostUsd,
+ int Retries,
+ bool Productive,
+ bool OneShot);
diff --git a/tests/ClaudeDo.Worker.Tests/Usage/TokenTracker/TokenTrackerFixtures.cs b/tests/ClaudeDo.Worker.Tests/Usage/TokenTracker/TokenTrackerFixtures.cs
new file mode 100644
index 00000000..fea5adac
--- /dev/null
+++ b/tests/ClaudeDo.Worker.Tests/Usage/TokenTracker/TokenTrackerFixtures.cs
@@ -0,0 +1,138 @@
+namespace ClaudeDo.Worker.Tests.Usage.TokenTracker;
+
+public static class TokenTrackerFixtures
+{
+ /// Session id whose hash is the fixture's first ClaudeDo session.
+ public const string ClaudeDoSessionIdA = "77470328-94f7-49d3-b378-a562d1501b5f";
+
+ /// Session id whose hash is the fixture's second ClaudeDo session.
+ public const string ClaudeDoSessionIdB = "20ebc1bc-3e6a-4c09-a4b3-19472dffa841";
+
+ /// Shape copied from a real `tokentracker sessions --format json` run
+ /// (v0.88.4, 2026-08-24), trimmed to four rows and stripped of everything we don't read.
+ public const string SessionsJson = """
+ {
+ "available": true,
+ "sessions": [
+ {
+ "version": 11,
+ "session_hash": "b24babcbf2b615730459773f",
+ "source": "claude",
+ "project_key": "ClaudeDo",
+ "model": "claude-opus-5",
+ "started_at": "2026-08-20T10:00:00.000Z",
+ "ended_at": "2026-08-20T10:10:00.000Z",
+ "turns": 4,
+ "edit_turns": 2,
+ "retry_turns": 1,
+ "tokens": {
+ "input_tokens": 100,
+ "cached_input_tokens": 900,
+ "cache_creation_input_tokens": 50,
+ "output_tokens": 400,
+ "reasoning_output_tokens": 0,
+ "total_tokens": 1450
+ },
+ "total_tokens": 1450,
+ "cost_usd": 2.5,
+ "productive": true,
+ "first_pass": true,
+ "one_shot": true
+ },
+ {
+ "version": 11,
+ "session_hash": "3e2a08fe8f2b941817fb554f",
+ "source": "claude",
+ "project_key": "02f6746f-d67b-4217-a581-f2f911f6ff0d",
+ "model": "claude-opus-5",
+ "started_at": "2026-08-20T12:00:00.000Z",
+ "ended_at": "2026-08-20T12:30:00.000Z",
+ "turns": 6,
+ "edit_turns": 0,
+ "retry_turns": 2,
+ "tokens": {
+ "input_tokens": 10,
+ "cached_input_tokens": 90,
+ "cache_creation_input_tokens": 5,
+ "output_tokens": 40,
+ "reasoning_output_tokens": 0,
+ "total_tokens": 145
+ },
+ "total_tokens": 145,
+ "cost_usd": 1.0,
+ "productive": false,
+ "first_pass": false,
+ "one_shot": false
+ },
+ {
+ "version": 11,
+ "session_hash": "ffffffffffffffffffffffff",
+ "source": "claude",
+ "project_key": "SomeOtherRepo",
+ "model": "claude-sonnet-5",
+ "started_at": "2026-08-20T14:00:00.000Z",
+ "ended_at": "2026-08-20T14:05:00.000Z",
+ "turns": 2,
+ "edit_turns": 1,
+ "retry_turns": 0,
+ "tokens": {
+ "input_tokens": 7,
+ "cached_input_tokens": 3,
+ "cache_creation_input_tokens": 1,
+ "output_tokens": 9,
+ "reasoning_output_tokens": 0,
+ "total_tokens": 20
+ },
+ "total_tokens": 20,
+ "cost_usd": 0.25,
+ "productive": true,
+ "first_pass": true,
+ "one_shot": true
+ },
+ {
+ "version": 11,
+ "session_hash": "aaaaaaaaaaaaaaaaaaaaaaaa",
+ "source": "claude",
+ "project_key": "ClaudeDo",
+ "model": "claude-opus-5",
+ "started_at": "2026-08-25T10:00:00.000Z",
+ "ended_at": "2026-08-25T10:20:00.000Z",
+ "turns": 3,
+ "edit_turns": 1,
+ "retry_turns": 0,
+ "tokens": {
+ "input_tokens": 1000,
+ "cached_input_tokens": 2000,
+ "cache_creation_input_tokens": 300,
+ "output_tokens": 4000,
+ "reasoning_output_tokens": 0,
+ "total_tokens": 7300
+ },
+ "total_tokens": 7300,
+ "cost_usd": 99.0,
+ "productive": true,
+ "first_pass": true,
+ "one_shot": true
+ }
+ ]
+ }
+ """;
+
+ /// Same as but with a future format version.
+ public const string UnsupportedVersionJson = """
+ {
+ "available": true,
+ "sessions": [
+ {
+ "version": 99,
+ "session_hash": "b24babcbf2b615730459773f",
+ "source": "claude",
+ "model": "claude-opus-5",
+ "started_at": "2026-08-20T10:00:00.000Z",
+ "tokens": { "input_tokens": 1, "output_tokens": 2 },
+ "cost_usd": 0.1
+ }
+ ]
+ }
+ """;
+}