nilometer 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/LICENSE +21 -0
  3. package/README.md +464 -2
  4. package/core/pricing/prices.json +348 -0
  5. package/core/schema/001_ingestion.sql +57 -0
  6. package/core/schema/002_line_count.sql +7 -0
  7. package/core/schema/003_derived.sql +70 -0
  8. package/core/schema/004_requests_dedup.sql +50 -0
  9. package/core/schema/005_repositories.sql +25 -0
  10. package/core/schema/006_status_readings.sql +33 -0
  11. package/core/schema/007_pricing.sql +95 -0
  12. package/core/schema/008_interruptions.sql +263 -0
  13. package/core/schema/009_headroom.sql +84 -0
  14. package/core/schema/010_attribution.sql +106 -0
  15. package/core/schema/011_projections.sql +83 -0
  16. package/core/schema/012_unsplit_cache_writes.sql +49 -0
  17. package/core/schema/013_repository_revalidation.sql +104 -0
  18. package/core/schema/014_observed_readings.sql +204 -0
  19. package/core/schema/015_response_observations.sql +127 -0
  20. package/core/schema/016_windows_repository_paths.sql +41 -0
  21. package/core/schema/017_unattributed_usage_once.sql +47 -0
  22. package/core/schema/018_request_time_index.sql +21 -0
  23. package/core/schema/019_dedup_earliest_of_equals.sql +61 -0
  24. package/core/schema/020_limit_quota_fields.sql +131 -0
  25. package/core/schema/021_plan_periods.sql +121 -0
  26. package/core/schema/022_attachment_walk.sql +223 -0
  27. package/core/schema/023_auto_continuations.sql +129 -0
  28. package/core/schema/024_counted_limit_groups.sql +29 -0
  29. package/core/schema/025_model_fallback.sql +77 -0
  30. package/core/schema/026_changed_readings.sql +106 -0
  31. package/core/schema/027_weekly_crossings.sql +32 -0
  32. package/core/schema/028_stale_readings.sql +196 -0
  33. package/core/schema/029_lockout_sessions.sql +68 -0
  34. package/core/schema/030_repository_aliases.sql +81 -0
  35. package/core/schema/031_cte_cursor_budget.sql +153 -0
  36. package/core/schema/032_stored_views.sql +233 -0
  37. package/dist/cli/main.cli.js +39 -0
  38. package/dist/cli/program.js +881 -0
  39. package/dist/core/db/database.js +182 -0
  40. package/dist/core/db/fingerprint.js +71 -0
  41. package/dist/core/ingest/attribution.js +131 -0
  42. package/dist/core/ingest/classify.js +382 -0
  43. package/dist/core/ingest/command.js +95 -0
  44. package/dist/core/ingest/derive.js +219 -0
  45. package/dist/core/ingest/discover.js +126 -0
  46. package/dist/core/ingest/ingest.js +160 -0
  47. package/dist/core/ingest/reader.js +89 -0
  48. package/dist/core/ingest/reset-time.js +97 -0
  49. package/dist/core/ingest/results.js +201 -0
  50. package/dist/core/ingest/spool.js +165 -0
  51. package/dist/core/ingest/store.js +99 -0
  52. package/dist/core/ingest/stored-views.js +65 -0
  53. package/dist/core/install/install.js +185 -0
  54. package/dist/core/install/locations.js +89 -0
  55. package/dist/core/install/private-files.js +259 -0
  56. package/dist/core/install/record.js +103 -0
  57. package/dist/core/plans/plan-prices.js +128 -0
  58. package/dist/core/plans/repository-aliases.js +136 -0
  59. package/dist/core/pricing/prices.js +147 -0
  60. package/dist/core/settings/settings-file.js +215 -0
  61. package/dist/core/settings/statusline.js +240 -0
  62. package/dist/core/util/commands.js +91 -0
  63. package/dist/core/util/node-version.js +58 -0
  64. package/dist/core/util/open-page.js +69 -0
  65. package/dist/core/util/paths.js +36 -0
  66. package/dist/core/verify/compare.js +77 -0
  67. package/dist/core/verify/verify.js +220 -0
  68. package/dist/core/view/server.js +142 -0
  69. package/dist/viewer/explain.js +574 -0
  70. package/dist/viewer/format.js +150 -0
  71. package/dist/viewer/html-charts.js +1292 -0
  72. package/dist/viewer/html-contract.js +2 -0
  73. package/dist/viewer/html-data.js +718 -0
  74. package/dist/viewer/html.js +1717 -0
  75. package/dist/viewer/queries.js +128 -0
  76. package/dist/viewer/render.js +602 -0
  77. package/dist/viewer/report.js +120 -0
  78. package/dist/viewer/save.js +110 -0
  79. package/docs/requirements.md +113 -0
  80. package/hooks/statusline.sh +104 -0
  81. package/package.json +61 -4
@@ -0,0 +1,49 @@
1
+ -- 012_unsplit_cache_writes: unsplit cache writes sum to 0, not NULL, when every request has the split.
2
+ --
3
+ -- In 010, SUM over cache_write_unsplit_tokens returned NULL for a model or repository whose requests
4
+ -- all recorded the 5m/1h split, and the viewer showed "unknown" for tokens that were known to be 0.
5
+ -- A NULL in the 5m and 1h columns still means unknown: those tokens exist, but their duration wasn't
6
+ -- recorded. Views are recreated rather than editing 010, which existing databases have applied.
7
+
8
+ DROP VIEW obs_usage_by_model;
9
+ DROP VIEW obs_usage_by_repo;
10
+
11
+ -- Observed: tokens per model per token type, with request counts and unknowns.
12
+ CREATE VIEW obs_usage_by_model AS
13
+ SELECT
14
+ model,
15
+ COUNT(*) AS requests,
16
+ SUM(input_tokens) AS input_tokens,
17
+ SUM(output_tokens) AS output_tokens,
18
+ SUM(cache_read_tokens) AS cache_read_tokens,
19
+ SUM(cache_write_5m_tokens) AS cache_write_5m_tokens,
20
+ SUM(cache_write_1h_tokens) AS cache_write_1h_tokens,
21
+ -- NULL here means every request recorded the 5m/1h split, so there were no unsplit writes: 0, not unknown.
22
+ COALESCE(SUM(cache_write_unsplit_tokens), 0) AS cache_write_unsplit_tokens,
23
+ COUNT(*) FILTER (WHERE dedup_key IS NULL) AS unkeyed_requests,
24
+ COUNT(*) FILTER (WHERE timestamp_utc IS NULL) AS requests_without_timestamp,
25
+ MIN(timestamp_utc) AS first_request_at_utc,
26
+ MAX(timestamp_utc) AS last_request_at_utc,
27
+ (SELECT covers_from FROM source_coverage WHERE source = 'session_logs') AS covers_from,
28
+ (SELECT covers_to FROM source_coverage WHERE source = 'session_logs') AS covers_to
29
+ FROM obs_usage_events
30
+ GROUP BY model;
31
+
32
+ -- Observed: tokens per repository (git root, or the working directory outside git; NULL when no cwd) per token type.
33
+ CREATE VIEW obs_usage_by_repo AS
34
+ SELECT
35
+ repository,
36
+ repo_kind,
37
+ COUNT(*) AS requests,
38
+ SUM(input_tokens) AS input_tokens,
39
+ SUM(output_tokens) AS output_tokens,
40
+ SUM(cache_read_tokens) AS cache_read_tokens,
41
+ SUM(cache_write_5m_tokens) AS cache_write_5m_tokens,
42
+ SUM(cache_write_1h_tokens) AS cache_write_1h_tokens,
43
+ -- NULL here means every request recorded the 5m/1h split, so there were no unsplit writes: 0, not unknown.
44
+ COALESCE(SUM(cache_write_unsplit_tokens), 0) AS cache_write_unsplit_tokens,
45
+ COUNT(*) FILTER (WHERE timestamp_utc IS NULL) AS requests_without_timestamp,
46
+ (SELECT covers_from FROM source_coverage WHERE source = 'session_logs') AS covers_from,
47
+ (SELECT covers_to FROM source_coverage WHERE source = 'session_logs') AS covers_to
48
+ FROM obs_usage_events
49
+ GROUP BY repository, repo_kind;
@@ -0,0 +1,104 @@
1
+ -- 013_repository_revalidation: repositories remembers how each working directory resolved and whether its root exists (D-028).
2
+ --
3
+ -- 005 cached one resolution per working directory forever, so a renamed or deleted folder kept its
4
+ -- "git repository" label, and a subfolder deleted before its first ingest couldn't be placed in its
5
+ -- repository. The table is rebuilt because SQLite can't change CHECK constraints in place, and the
6
+ -- views that read it are recreated around the rebuild. Only 'repo' and 'not_git' rows are carried
7
+ -- over: 'missing' rows are dropped so the next ingest resolves them again with the parent-folder
8
+ -- lookup. Every carried row is re-checked against the filesystem on that ingest too.
9
+
10
+ DROP VIEW obs_usage_by_model;
11
+ DROP VIEW obs_usage_by_repo;
12
+ DROP VIEW obs_usage_events;
13
+ DROP VIEW request_repositories;
14
+
15
+ CREATE TABLE repositories_013 (
16
+ cwd TEXT PRIMARY KEY, -- source: cwd from log lines
17
+ kind TEXT NOT NULL, -- derived: the working directory at the last check: 'repo', 'not_git', or 'missing'
18
+ repo_root TEXT, -- derived: git's root for 'repo'; for 'missing', the last-known or nearest existing parent's root; else NULL
19
+ root_exists INTEGER, -- derived: 1 if repo_root existed at the last check, 0 if not; NULL without repo_root
20
+ resolved_via TEXT NOT NULL, -- derived: 'git' (run in the directory), 'last_known', 'parent', or 'none'
21
+ resolved_at TEXT NOT NULL, -- derived: ISO-8601 UTC time of the last resolution
22
+ CHECK (kind IN ('repo', 'not_git', 'missing')),
23
+ CHECK (resolved_via IN ('git', 'last_known', 'parent', 'none')),
24
+ CHECK (root_exists IN (0, 1)),
25
+ CHECK ((repo_root IS NULL) = (root_exists IS NULL)),
26
+ CHECK (kind <> 'repo' OR (repo_root IS NOT NULL AND resolved_via = 'git')),
27
+ CHECK (kind <> 'not_git' OR (repo_root IS NULL AND resolved_via = 'git')),
28
+ CHECK (kind <> 'missing' OR (resolved_via = 'none') = (repo_root IS NULL)),
29
+ CHECK (kind = 'missing' OR resolved_via = 'git')
30
+ ) STRICT;
31
+
32
+ INSERT INTO repositories_013 (cwd, kind, repo_root, root_exists, resolved_via, resolved_at)
33
+ SELECT cwd, kind, repo_root, CASE WHEN repo_root IS NULL THEN NULL ELSE 1 END, 'git', resolved_at
34
+ FROM repositories
35
+ WHERE kind IN ('repo', 'not_git');
36
+
37
+ DROP TABLE repositories;
38
+ ALTER TABLE repositories_013 RENAME TO repositories;
39
+
40
+ -- Observed: each request's repository and how it was attributed (D-010, D-028); a missing folder whose repository still exists counts as that repository.
41
+ CREATE VIEW request_repositories AS
42
+ SELECT
43
+ d.raw_line_id,
44
+ d.cwd,
45
+ CASE
46
+ WHEN r.cwd IS NULL THEN 'unresolved'
47
+ WHEN r.repo_root IS NOT NULL AND r.root_exists = 1 THEN 'repo'
48
+ WHEN r.repo_root IS NOT NULL THEN 'missing'
49
+ ELSE r.kind
50
+ END AS repo_kind,
51
+ COALESCE(r.repo_root, d.cwd) AS repository,
52
+ r.resolved_via
53
+ FROM requests_dedup d
54
+ LEFT JOIN repositories r ON r.cwd = d.cwd;
55
+
56
+ -- Observed: every deduplicated request with the model and repository it's attributed to.
57
+ CREATE VIEW obs_usage_events AS
58
+ SELECT
59
+ d.raw_line_id, d.dedup_key, d.session_id, d.timestamp_utc, d.model,
60
+ r.repository, r.repo_kind,
61
+ d.input_tokens, d.output_tokens, d.cache_read_tokens,
62
+ d.cache_write_5m_tokens, d.cache_write_1h_tokens, d.cache_write_unsplit_tokens
63
+ FROM requests_dedup d
64
+ JOIN request_repositories r ON r.raw_line_id = d.raw_line_id;
65
+
66
+ -- Observed: tokens per model per token type, with request counts and unknowns.
67
+ CREATE VIEW obs_usage_by_model AS
68
+ SELECT
69
+ model,
70
+ COUNT(*) AS requests,
71
+ SUM(input_tokens) AS input_tokens,
72
+ SUM(output_tokens) AS output_tokens,
73
+ SUM(cache_read_tokens) AS cache_read_tokens,
74
+ SUM(cache_write_5m_tokens) AS cache_write_5m_tokens,
75
+ SUM(cache_write_1h_tokens) AS cache_write_1h_tokens,
76
+ -- NULL here means every request recorded the 5m/1h split, so there were no unsplit writes: 0, not unknown.
77
+ COALESCE(SUM(cache_write_unsplit_tokens), 0) AS cache_write_unsplit_tokens,
78
+ COUNT(*) FILTER (WHERE dedup_key IS NULL) AS unkeyed_requests,
79
+ COUNT(*) FILTER (WHERE timestamp_utc IS NULL) AS requests_without_timestamp,
80
+ MIN(timestamp_utc) AS first_request_at_utc,
81
+ MAX(timestamp_utc) AS last_request_at_utc,
82
+ (SELECT covers_from FROM source_coverage WHERE source = 'session_logs') AS covers_from,
83
+ (SELECT covers_to FROM source_coverage WHERE source = 'session_logs') AS covers_to
84
+ FROM obs_usage_events
85
+ GROUP BY model;
86
+
87
+ -- Observed: tokens per repository (git root, or the working directory outside git; NULL when no cwd) per token type.
88
+ CREATE VIEW obs_usage_by_repo AS
89
+ SELECT
90
+ repository,
91
+ repo_kind,
92
+ COUNT(*) AS requests,
93
+ SUM(input_tokens) AS input_tokens,
94
+ SUM(output_tokens) AS output_tokens,
95
+ SUM(cache_read_tokens) AS cache_read_tokens,
96
+ SUM(cache_write_5m_tokens) AS cache_write_5m_tokens,
97
+ SUM(cache_write_1h_tokens) AS cache_write_1h_tokens,
98
+ -- NULL here means every request recorded the 5m/1h split, so there were no unsplit writes: 0, not unknown.
99
+ COALESCE(SUM(cache_write_unsplit_tokens), 0) AS cache_write_unsplit_tokens,
100
+ COUNT(*) FILTER (WHERE timestamp_utc IS NULL) AS requests_without_timestamp,
101
+ (SELECT covers_from FROM source_coverage WHERE source = 'session_logs') AS covers_from,
102
+ (SELECT covers_to FROM source_coverage WHERE source = 'session_logs') AS covers_to
103
+ FROM obs_usage_events
104
+ GROUP BY repository, repo_kind;
@@ -0,0 +1,204 @@
1
+ -- 014_observed_readings: date status line readings by the API response they came from, not by capture (D-044).
2
+ --
3
+ -- A payload's rate_limits are the numbers from the latest API response in that session; re-running
4
+ -- the status line (a refresh timer, a window reset, /compact) repeats them. So a reading is observed
5
+ -- at the latest request line in its session at or before the capture (+1 s for whole-second
6
+ -- captures), within the window it reports, and readings of the same response collapse to one
7
+ -- observation. Every view that ordered, dated, counted, or excluded readings by capture time is
8
+ -- recreated here to use the observed time. Dependent views keep their column names and need no change.
9
+
10
+ DROP VIEW obs_unattributed_usage;
11
+ DROP VIEW obs_unattributed_usage_events;
12
+ DROP VIEW window_reading_pairs;
13
+ DROP VIEW obs_window_peak;
14
+ DROP VIEW obs_window_headroom;
15
+ DROP VIEW window_instances;
16
+ DROP VIEW status_limit_groups;
17
+ DROP VIEW source_coverage;
18
+ DROP VIEW window_readings;
19
+
20
+ -- Observed: one row per observation of a plan window: the latest capture of each API response's numbers, with when they were observed (D-024, D-044).
21
+ CREATE VIEW window_readings AS
22
+ WITH readings AS (
23
+ SELECT
24
+ w.raw_line_id, w.window, w.resets_at, w.used_percentage, s.session_id, s.captured_at_s,
25
+ -- Only a request inside this window can have produced numbers for it; +1 s covers whole-second captures.
26
+ (SELECT MAX(p.timestamp_utc) FROM parsed_lines p
27
+ WHERE p.session_id = s.session_id AND p.class = 'request'
28
+ AND p.timestamp_utc > strftime('%Y-%m-%dT%H:%M:%fZ',
29
+ w.resets_at - CASE w.window WHEN 'five_hour' THEN 5 * 3600 ELSE 7 * 86400 END, 'unixepoch')
30
+ AND p.timestamp_utc < strftime('%Y-%m-%dT%H:%M:%fZ', s.captured_at_s + 1, 'unixepoch')) AS last_request_utc
31
+ FROM rate_limit_windows w
32
+ JOIN status_readings s ON s.raw_line_id = w.raw_line_id
33
+ WHERE s.status = 'ok' AND w.validity = 'valid' AND w.window IN ('five_hour', 'seven_day')
34
+ ),
35
+ observed AS (
36
+ SELECT
37
+ r.*,
38
+ COALESCE(r.last_request_utc, strftime('%Y-%m-%dT%H:%M:%fZ', r.captured_at_s, 'unixepoch')) AS observed_at_utc,
39
+ ROW_NUMBER() OVER (
40
+ -- A capture-time fallback is its own observation: NULL request times must not merge.
41
+ PARTITION BY r.window, r.resets_at, r.session_id, r.last_request_utc,
42
+ CASE WHEN r.last_request_utc IS NULL THEN r.raw_line_id END
43
+ ORDER BY r.captured_at_s DESC, r.raw_line_id DESC
44
+ ) AS copy_rank
45
+ FROM readings r
46
+ )
47
+ SELECT
48
+ raw_line_id,
49
+ window,
50
+ resets_at,
51
+ strftime('%Y-%m-%dT%H:%M:%fZ', resets_at, 'unixepoch') AS reset_at_utc,
52
+ used_percentage,
53
+ session_id,
54
+ captured_at_s,
55
+ strftime('%Y-%m-%dT%H:%M:%fZ', captured_at_s, 'unixepoch') AS captured_at_utc,
56
+ observed_at_utc,
57
+ unixepoch(observed_at_utc, 'subsec') AS observed_at_s,
58
+ CASE WHEN last_request_utc IS NULL THEN 'capture' ELSE 'last_request' END AS observed_via,
59
+ unixepoch(observed_at_utc, 'subsec') > resets_at AS after_reset
60
+ FROM observed
61
+ WHERE copy_rank = 1;
62
+
63
+ -- Observed: the time span each source's data covers, from the data present (README principle 5); status line spans observations (D-044).
64
+ CREATE VIEW source_coverage AS
65
+ SELECT 'session_logs' AS source, MIN(timestamp_utc) AS covers_from, MAX(timestamp_utc) AS covers_to
66
+ FROM parsed_lines
67
+ WHERE timestamp_utc IS NOT NULL
68
+ UNION ALL
69
+ SELECT 'status_line', MIN(observed_at_utc), MAX(observed_at_utc)
70
+ FROM window_readings;
71
+
72
+ -- Observed: status line windows observed at 100%, one row per (window, resets_at), from init onward (D-023, D-044).
73
+ CREATE VIEW status_limit_groups AS
74
+ WITH at_limit AS (
75
+ SELECT
76
+ r.raw_line_id, r.window, r.resets_at, r.session_id, r.observed_at_utc,
77
+ ROW_NUMBER() OVER (PARTITION BY r.window, r.resets_at ORDER BY r.observed_at_s, r.raw_line_id) AS reading_rank,
78
+ COUNT(*) OVER (PARTITION BY r.window, r.resets_at) AS readings_at_limit
79
+ FROM window_readings r
80
+ WHERE r.used_percentage >= 100
81
+ )
82
+ SELECT
83
+ raw_line_id, window, resets_at, session_id, readings_at_limit,
84
+ observed_at_utc AS first_at_utc,
85
+ strftime('%Y-%m-%dT%H:%M:%fZ', resets_at, 'unixepoch') AS reset_at_utc,
86
+ CASE window WHEN 'five_hour' THEN 5 * 3600 ELSE 7 * 86400 END AS window_seconds
87
+ FROM at_limit
88
+ WHERE reading_rank = 1;
89
+
90
+ -- Observed: per window instance, observation counts, whether it is still open, and the status line coverage.
91
+ CREATE VIEW window_instances AS
92
+ SELECT
93
+ window,
94
+ resets_at,
95
+ reset_at_utc,
96
+ COUNT(*) FILTER (WHERE after_reset = 0) AS readings,
97
+ COUNT(*) FILTER (WHERE after_reset = 1) AS readings_after_reset,
98
+ MIN(observed_at_utc) FILTER (WHERE after_reset = 0) AS first_reading_at_utc,
99
+ -- Open: no status line reading at all was captured at or after this reset, so later usage may still come.
100
+ resets_at > (SELECT MAX(captured_at_s) FROM status_readings WHERE status = 'ok') AS window_open,
101
+ (SELECT covers_from FROM source_coverage WHERE source = 'status_line') AS covers_from,
102
+ (SELECT covers_to FROM source_coverage WHERE source = 'status_line') AS covers_to
103
+ FROM window_readings
104
+ GROUP BY window, resets_at;
105
+
106
+ -- Observed: last observed usage before each window's reset, with when it was observed (a lower bound on final usage; D-024, D-044).
107
+ CREATE VIEW obs_window_headroom AS
108
+ WITH ranked AS (
109
+ SELECT
110
+ r.*,
111
+ ROW_NUMBER() OVER (PARTITION BY r.window, r.resets_at ORDER BY r.observed_at_s DESC, r.raw_line_id DESC) AS latest_rank
112
+ FROM window_readings r
113
+ WHERE r.after_reset = 0
114
+ )
115
+ SELECT
116
+ i.window,
117
+ i.reset_at_utc,
118
+ r.used_percentage AS last_used_percentage,
119
+ r.observed_at_utc AS last_reading_at_utc,
120
+ r.raw_line_id AS last_reading_raw_line_id,
121
+ i.readings,
122
+ i.readings_after_reset,
123
+ i.first_reading_at_utc,
124
+ i.window_open,
125
+ i.covers_from,
126
+ i.covers_to
127
+ FROM window_instances i
128
+ LEFT JOIN ranked r ON r.window = i.window AND r.resets_at = i.resets_at AND r.latest_rank = 1;
129
+
130
+ -- Observed: highest observed usage in each window, with the observation it came from (D-044).
131
+ CREATE VIEW obs_window_peak AS
132
+ WITH ranked AS (
133
+ SELECT
134
+ r.*,
135
+ ROW_NUMBER() OVER (PARTITION BY r.window, r.resets_at ORDER BY r.used_percentage DESC, r.observed_at_s, r.raw_line_id) AS peak_rank
136
+ FROM window_readings r
137
+ WHERE r.after_reset = 0
138
+ )
139
+ SELECT
140
+ i.window,
141
+ i.reset_at_utc,
142
+ r.used_percentage AS peak_used_percentage,
143
+ r.observed_at_utc AS peak_reading_at_utc,
144
+ r.raw_line_id AS peak_reading_raw_line_id,
145
+ i.readings,
146
+ i.readings_after_reset,
147
+ i.window_open,
148
+ i.covers_from,
149
+ i.covers_to
150
+ FROM window_instances i
151
+ LEFT JOIN ranked r ON r.window = i.window AND r.resets_at = i.resets_at AND r.peak_rank = 1;
152
+
153
+ -- Observed: consecutive observations of one window, the change between them, and Claude Code requests in between (D-025, D-044).
154
+ CREATE VIEW window_reading_pairs AS
155
+ WITH ordered AS (
156
+ SELECT
157
+ r.*,
158
+ LAG(r.raw_line_id) OVER w AS previous_raw_line_id,
159
+ LAG(r.used_percentage) OVER w AS previous_used_percentage,
160
+ LAG(r.observed_at_utc) OVER w AS previous_observed_at_utc
161
+ FROM window_readings r
162
+ WHERE r.after_reset = 0
163
+ WINDOW w AS (PARTITION BY r.window, r.resets_at ORDER BY r.observed_at_s, r.raw_line_id)
164
+ )
165
+ SELECT
166
+ o.window,
167
+ o.reset_at_utc,
168
+ o.previous_raw_line_id,
169
+ o.raw_line_id,
170
+ o.previous_observed_at_utc,
171
+ o.observed_at_utc,
172
+ o.used_percentage - o.previous_used_percentage AS change_percentage_points,
173
+ -- Include the previous observation's instant, and one second past this one (whole-second captures).
174
+ (SELECT COUNT(*) FROM requests_dedup d
175
+ WHERE d.timestamp_utc >= o.previous_observed_at_utc
176
+ AND d.timestamp_utc < strftime('%Y-%m-%dT%H:%M:%fZ', o.observed_at_s + 1, 'unixepoch')) AS requests_between
177
+ FROM ordered o
178
+ WHERE o.previous_raw_line_id IS NOT NULL;
179
+
180
+ -- Observed: observation pairs whose usage rose with no Claude Code request in between (D-025; see D-044 on why real data rarely has any).
181
+ CREATE VIEW obs_unattributed_usage_events AS
182
+ SELECT * FROM window_reading_pairs
183
+ WHERE requests_between = 0 AND change_percentage_points > 0;
184
+
185
+ -- Observed: per window, percentage points consumed with no Claude Code request in between (a lower bound), beside pair counts.
186
+ CREATE VIEW obs_unattributed_usage AS
187
+ SELECT
188
+ i.window,
189
+ i.reset_at_utc,
190
+ i.readings,
191
+ (SELECT COUNT(*) FROM window_reading_pairs p WHERE p.window = i.window AND p.reset_at_utc = i.reset_at_utc) AS pairs,
192
+ (SELECT COUNT(*) FROM window_reading_pairs p
193
+ WHERE p.window = i.window AND p.reset_at_utc = i.reset_at_utc AND p.requests_between = 0) AS pairs_without_requests,
194
+ (SELECT COUNT(*) FROM window_reading_pairs p
195
+ WHERE p.window = i.window AND p.reset_at_utc = i.reset_at_utc AND p.change_percentage_points < 0) AS decreasing_pairs,
196
+ -- Fewer than two observations means no pair was observed: unknown, not 0.
197
+ CASE WHEN i.readings >= 2 THEN
198
+ (SELECT TOTAL(e.change_percentage_points) FROM obs_unattributed_usage_events e
199
+ WHERE e.window = i.window AND e.reset_at_utc = i.reset_at_utc)
200
+ END AS unattributed_percentage_points,
201
+ i.window_open,
202
+ i.covers_from,
203
+ i.covers_to
204
+ FROM window_instances i;
@@ -0,0 +1,127 @@
1
+ -- 015_response_observations: one status line observation per API response, dated by its first log line (D-045).
2
+ --
3
+ -- 014 keyed observations by the latest request line, but one response is several streaming snapshot
4
+ -- lines with their own timestamps (D-001), so readings taken mid-stream became separate observations
5
+ -- of one response. Here the key is the response (its D-001 dedup key, or the line itself when
6
+ -- unkeyed), dated by the response's first line, and reading pairs count distinct responses with any
7
+ -- line in the interval. Dependent views keep their column names.
8
+
9
+ DROP VIEW obs_unattributed_usage;
10
+ DROP VIEW obs_unattributed_usage_events;
11
+ DROP VIEW window_reading_pairs;
12
+ DROP VIEW window_readings;
13
+
14
+ -- Observed: one row per observation of a plan window: the latest capture of each API response's numbers, dated by that response's first line (D-044, D-045).
15
+ CREATE VIEW window_readings AS
16
+ WITH readings AS (
17
+ SELECT
18
+ w.raw_line_id, w.window, w.resets_at, w.used_percentage, s.session_id, s.captured_at_s,
19
+ -- The response behind the latest request line in this session, inside this window, at or before
20
+ -- the capture (+1 s for whole-second captures). An unkeyed line is its own response.
21
+ (SELECT COALESCE(rq.dedup_key, 'line:' || rq.raw_line_id)
22
+ FROM parsed_lines p
23
+ JOIN requests rq ON rq.raw_line_id = p.raw_line_id
24
+ WHERE p.session_id = s.session_id AND p.class = 'request'
25
+ AND p.timestamp_utc > strftime('%Y-%m-%dT%H:%M:%fZ',
26
+ w.resets_at - CASE w.window WHEN 'five_hour' THEN 5 * 3600 ELSE 7 * 86400 END, 'unixepoch')
27
+ AND p.timestamp_utc < strftime('%Y-%m-%dT%H:%M:%fZ', s.captured_at_s + 1, 'unixepoch')
28
+ ORDER BY p.timestamp_utc DESC, p.raw_line_id DESC
29
+ LIMIT 1) AS response_key
30
+ FROM rate_limit_windows w
31
+ JOIN status_readings s ON s.raw_line_id = w.raw_line_id
32
+ WHERE s.status = 'ok' AND w.validity = 'valid' AND w.window IN ('five_hour', 'seven_day')
33
+ ),
34
+ dated AS (
35
+ SELECT
36
+ r.*,
37
+ -- The response's first line: when its numbers first existed.
38
+ (SELECT MIN(p.timestamp_utc)
39
+ FROM requests rq
40
+ JOIN parsed_lines p ON p.raw_line_id = rq.raw_line_id
41
+ WHERE (r.response_key LIKE 'line:%' AND rq.raw_line_id = CAST(substr(r.response_key, 6) AS INTEGER))
42
+ OR rq.dedup_key = r.response_key) AS response_first_utc
43
+ FROM readings r
44
+ ),
45
+ observed AS (
46
+ SELECT
47
+ d.*,
48
+ COALESCE(d.response_first_utc, strftime('%Y-%m-%dT%H:%M:%fZ', d.captured_at_s, 'unixepoch')) AS observed_at_utc,
49
+ ROW_NUMBER() OVER (
50
+ -- A capture-time fallback is its own observation: NULL response keys must not merge.
51
+ PARTITION BY d.window, d.resets_at, d.session_id, d.response_key,
52
+ CASE WHEN d.response_key IS NULL THEN d.raw_line_id END
53
+ ORDER BY d.captured_at_s DESC, d.raw_line_id DESC
54
+ ) AS copy_rank
55
+ FROM dated d
56
+ )
57
+ SELECT
58
+ raw_line_id,
59
+ window,
60
+ resets_at,
61
+ strftime('%Y-%m-%dT%H:%M:%fZ', resets_at, 'unixepoch') AS reset_at_utc,
62
+ used_percentage,
63
+ session_id,
64
+ captured_at_s,
65
+ strftime('%Y-%m-%dT%H:%M:%fZ', captured_at_s, 'unixepoch') AS captured_at_utc,
66
+ observed_at_utc,
67
+ unixepoch(observed_at_utc, 'subsec') AS observed_at_s,
68
+ CASE WHEN response_key IS NULL THEN 'capture' ELSE 'last_request' END AS observed_via,
69
+ unixepoch(observed_at_utc, 'subsec') > resets_at AS after_reset
70
+ FROM observed
71
+ WHERE copy_rank = 1;
72
+
73
+ -- Observed: consecutive observations of one window, the change between them, and Claude Code responses in between (D-025, D-045).
74
+ CREATE VIEW window_reading_pairs AS
75
+ WITH ordered AS (
76
+ SELECT
77
+ r.*,
78
+ LAG(r.raw_line_id) OVER w AS previous_raw_line_id,
79
+ LAG(r.used_percentage) OVER w AS previous_used_percentage,
80
+ LAG(r.observed_at_utc) OVER w AS previous_observed_at_utc
81
+ FROM window_readings r
82
+ WHERE r.after_reset = 0
83
+ WINDOW w AS (PARTITION BY r.window, r.resets_at ORDER BY r.observed_at_s, r.raw_line_id)
84
+ )
85
+ SELECT
86
+ o.window,
87
+ o.reset_at_utc,
88
+ o.previous_raw_line_id,
89
+ o.raw_line_id,
90
+ o.previous_observed_at_utc,
91
+ o.observed_at_utc,
92
+ o.used_percentage - o.previous_used_percentage AS change_percentage_points,
93
+ -- Distinct responses with any line from the previous observation up to one second past this one.
94
+ (SELECT COUNT(DISTINCT COALESCE(rq.dedup_key, 'line:' || rq.raw_line_id))
95
+ FROM parsed_lines p
96
+ JOIN requests rq ON rq.raw_line_id = p.raw_line_id
97
+ WHERE p.class = 'request'
98
+ AND p.timestamp_utc >= o.previous_observed_at_utc
99
+ AND p.timestamp_utc < strftime('%Y-%m-%dT%H:%M:%fZ', o.observed_at_s + 1, 'unixepoch')) AS requests_between
100
+ FROM ordered o
101
+ WHERE o.previous_raw_line_id IS NOT NULL;
102
+
103
+ -- Observed: observation pairs whose usage rose with no Claude Code request in between (D-025; see D-044 on why real data rarely has any).
104
+ CREATE VIEW obs_unattributed_usage_events AS
105
+ SELECT * FROM window_reading_pairs
106
+ WHERE requests_between = 0 AND change_percentage_points > 0;
107
+
108
+ -- Observed: per window, percentage points consumed with no Claude Code request in between (a lower bound), beside pair counts.
109
+ CREATE VIEW obs_unattributed_usage AS
110
+ SELECT
111
+ i.window,
112
+ i.reset_at_utc,
113
+ i.readings,
114
+ (SELECT COUNT(*) FROM window_reading_pairs p WHERE p.window = i.window AND p.reset_at_utc = i.reset_at_utc) AS pairs,
115
+ (SELECT COUNT(*) FROM window_reading_pairs p
116
+ WHERE p.window = i.window AND p.reset_at_utc = i.reset_at_utc AND p.requests_between = 0) AS pairs_without_requests,
117
+ (SELECT COUNT(*) FROM window_reading_pairs p
118
+ WHERE p.window = i.window AND p.reset_at_utc = i.reset_at_utc AND p.change_percentage_points < 0) AS decreasing_pairs,
119
+ -- Fewer than two observations means no pair was observed: unknown, not 0.
120
+ CASE WHEN i.readings >= 2 THEN
121
+ (SELECT TOTAL(e.change_percentage_points) FROM obs_unattributed_usage_events e
122
+ WHERE e.window = i.window AND e.reset_at_utc = i.reset_at_utc)
123
+ END AS unattributed_percentage_points,
124
+ i.window_open,
125
+ i.covers_from,
126
+ i.covers_to
127
+ FROM window_instances i;
@@ -0,0 +1,41 @@
1
+ -- 016_windows_repository_paths: a Windows repository path is written one way, whatever its source (D-049).
2
+ --
3
+ -- Observed on a Windows PC (2026-09-17, counts and structure only): Claude Code logged working
4
+ -- directories with `\` separators and with the drive letter both as `C:` and `c:`, while git returns
5
+ -- repository roots with `/` separators. For a folder that still exists, git resolved both spellings to
6
+ -- one root. A deleted folder with no known root is its own repository, so one such folder appeared as
7
+ -- two rows. Here a repository that starts with a drive letter gets an uppercase letter and `/`
8
+ -- separators. Windows paths are case-insensitive for the drive, and a POSIX repository is an absolute
9
+ -- path starting with `/`, so no POSIX value changes. The stored cwd and repo_root keep their source
10
+ -- spelling; only the attributed repository is normalized.
11
+
12
+ DROP VIEW request_repositories;
13
+
14
+ -- Observed: each request's repository and how it was attributed (D-010, D-028); a missing folder whose repository still exists counts as that repository; a Windows path is written with an uppercase drive letter and `/` separators (D-049).
15
+ CREATE VIEW request_repositories AS
16
+ WITH attributed AS (
17
+ SELECT
18
+ d.raw_line_id,
19
+ d.cwd,
20
+ CASE
21
+ WHEN r.cwd IS NULL THEN 'unresolved'
22
+ WHEN r.repo_root IS NOT NULL AND r.root_exists = 1 THEN 'repo'
23
+ WHEN r.repo_root IS NOT NULL THEN 'missing'
24
+ ELSE r.kind
25
+ END AS repo_kind,
26
+ COALESCE(r.repo_root, d.cwd) AS repository,
27
+ r.resolved_via
28
+ FROM requests_dedup d
29
+ LEFT JOIN repositories r ON r.cwd = d.cwd
30
+ )
31
+ SELECT
32
+ raw_line_id,
33
+ cwd,
34
+ repo_kind,
35
+ CASE
36
+ WHEN repository GLOB '[A-Za-z]:[\/]*'
37
+ THEN upper(substr(repository, 1, 1)) || replace(substr(repository, 2), char(92), '/')
38
+ ELSE repository
39
+ END AS repository,
40
+ resolved_via
41
+ FROM attributed;
@@ -0,0 +1,47 @@
1
+ -- 017_unattributed_usage_once: compute the reading pairs once per window instead of four times.
2
+ --
3
+ -- obs_unattributed_usage asked window_reading_pairs four separate correlated questions per window
4
+ -- instance, and window_reading_pairs answers each one by running its own correlated subquery over
5
+ -- parsed_lines for every pair. Reading the view took about 8.9 seconds on a real database, 55% of
6
+ -- the whole report, to return one row per window. The numbers were right; they were just computed
7
+ -- many times over.
8
+ --
9
+ -- The rewrite aggregates the pairs once, grouped by window instance, and joins that to the
10
+ -- instances. No column, value, or row changes: a window with no pairs reported zeros through the
11
+ -- subqueries and reports them through COALESCE here, and TOTAL() over no rows is 0.0, which the
12
+ -- COALESCE reproduces. Source: no API field; this view is built from status line readings (D-025,
13
+ -- D-044).
14
+ DROP VIEW obs_unattributed_usage;
15
+
16
+ -- Observed: per window, percentage points consumed with no Claude Code request in between (a lower bound), beside pair counts.
17
+ CREATE VIEW obs_unattributed_usage AS
18
+ WITH pair_totals AS (
19
+ SELECT
20
+ p.window,
21
+ p.reset_at_utc,
22
+ COUNT(*) AS pairs,
23
+ COUNT(*) FILTER (WHERE p.requests_between = 0) AS pairs_without_requests,
24
+ COUNT(*) FILTER (WHERE p.change_percentage_points < 0) AS decreasing_pairs,
25
+ -- The same rows obs_unattributed_usage_events selects: rose, with no request in between.
26
+ TOTAL(
27
+ CASE WHEN p.requests_between = 0 AND p.change_percentage_points > 0
28
+ THEN p.change_percentage_points END
29
+ ) AS unattributed_percentage_points
30
+ FROM window_reading_pairs p
31
+ GROUP BY p.window, p.reset_at_utc
32
+ )
33
+ SELECT
34
+ i.window,
35
+ i.reset_at_utc,
36
+ i.readings,
37
+ COALESCE(t.pairs, 0) AS pairs,
38
+ COALESCE(t.pairs_without_requests, 0) AS pairs_without_requests,
39
+ COALESCE(t.decreasing_pairs, 0) AS decreasing_pairs,
40
+ -- Fewer than two observations means no pair was observed: unknown, not 0.
41
+ CASE WHEN i.readings >= 2 THEN COALESCE(t.unattributed_percentage_points, 0.0) END
42
+ AS unattributed_percentage_points,
43
+ i.window_open,
44
+ i.covers_from,
45
+ i.covers_to
46
+ FROM window_instances i
47
+ LEFT JOIN pair_totals t ON t.window = i.window AND t.reset_at_utc = i.reset_at_utc;
@@ -0,0 +1,21 @@
1
+ -- 018_request_time_index: index request lines by time, so counting the responses between two status
2
+ -- line readings is a range scan rather than a table scan.
3
+ --
4
+ -- window_reading_pairs counts distinct responses in an interval, once per pair. That subquery
5
+ -- filters parsed_lines by class and a timestamp range with no session, and every existing index
6
+ -- leads with session_id, so each of the hundreds of pairs scanned the whole table.
7
+ --
8
+ -- The index is PARTIAL on purpose. A plain index on (class, timestamp_utc) fixed this query but
9
+ -- tempted the planner into using it for the window views, which filter by session and time. Measured
10
+ -- on a real database, as medians of three runs:
11
+ -- view no index full index partial index
12
+ -- obs_unattributed_usage 9347 1794 1236
13
+ -- obs_lockout_time 2296 3391 2313
14
+ -- obs_sessions_not_resumed 1157 1740 1167
15
+ -- every report view (total) 17281 13633 9285
16
+ -- A partial index can only be used by a query carrying the same WHERE clause, so it serves this
17
+ -- subquery and is invisible to the rest. Every shape returned identical rows; only the time changed.
18
+ --
19
+ -- The cost is one small index and slightly slower inserts during ingest, which writes each line once
20
+ -- and reads these views on every report.
21
+ CREATE INDEX parsed_lines_request_time ON parsed_lines (timestamp_utc) WHERE class = 'request';
@@ -0,0 +1,61 @@
1
+ -- 019_dedup_earliest_of_equals: when the dedup rule cannot choose, take the earliest line.
2
+ --
3
+ -- One API response is written as several lines and the largest output_tokens wins (D-001). When
4
+ -- every line carries the SAME counts the rule selects nothing, and the tie-breaks that followed it
5
+ -- ended on `line_number DESC` — the last line — which decided nothing except, at a day boundary,
6
+ -- which day the response landed on. Observed on a real log set: one response written three times
7
+ -- across 23:59:57 to 00:00:01, counted on the later day here and the earlier one by ccusage.
8
+ --
9
+ -- The rule this project already states for the same problem is D-045: collapse duplicates of one
10
+ -- response and date it by its first line, "when its numbers first existed". That is applied here.
11
+ -- Nothing changes where output_tokens grow, because the largest still wins before this is reached.
12
+ -- Source: no API field; this is a choice among lines the API produced (D-001, D-045, D-065).
13
+ DROP VIEW requests_dedup;
14
+
15
+ -- Observed: one row per distinct API response (winners) plus every unkeyed request line.
16
+ CREATE VIEW requests_dedup AS
17
+ WITH ranked AS (
18
+ SELECT
19
+ r.raw_line_id,
20
+ r.dedup_key,
21
+ r.message_id,
22
+ r.request_id,
23
+ r.model,
24
+ r.input_tokens,
25
+ r.output_tokens,
26
+ r.cache_read_tokens,
27
+ r.cache_write_5m_tokens,
28
+ r.cache_write_1h_tokens,
29
+ r.cache_write_unsplit_tokens,
30
+ r.speed,
31
+ p.session_id,
32
+ p.timestamp_raw,
33
+ p.timestamp_utc,
34
+ p.cwd,
35
+ p.is_sidechain,
36
+ f.root,
37
+ f.relative_path,
38
+ l.first_run_id,
39
+ l.line_number,
40
+ ROW_NUMBER() OVER (
41
+ PARTITION BY r.dedup_key
42
+ ORDER BY r.output_tokens DESC, p.is_sidechain ASC,
43
+ -- Among lines tied on output, the earliest: when those numbers first existed
44
+ -- (D-065, the same rule D-045 applies to readings). NULLs last, so an unparsed
45
+ -- timestamp never wins.
46
+ (p.timestamp_utc IS NULL) ASC, p.timestamp_utc ASC,
47
+ f.root DESC, f.relative_path DESC, l.first_run_id DESC, l.line_number DESC
48
+ ) AS candidate_rank
49
+ FROM requests r
50
+ JOIN parsed_lines p ON p.raw_line_id = r.raw_line_id
51
+ JOIN raw_lines l ON l.id = r.raw_line_id
52
+ JOIN source_files f ON f.id = l.source_file_id
53
+ )
54
+ SELECT
55
+ raw_line_id, dedup_key, message_id, request_id, model,
56
+ input_tokens, output_tokens, cache_read_tokens,
57
+ cache_write_5m_tokens, cache_write_1h_tokens, cache_write_unsplit_tokens, speed,
58
+ session_id, timestamp_raw, timestamp_utc, cwd, is_sidechain,
59
+ root, relative_path, first_run_id, line_number
60
+ FROM ranked
61
+ WHERE dedup_key IS NULL OR candidate_rank = 1;