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,95 @@
1
+ -- 007_pricing: dated API list prices and the per-request cost view (D-005, D-006, D-020).
2
+ --
3
+ -- prices is replaced from core/pricing/prices.json on every ingest. request_costs prices each
4
+ -- deduplicated request at the row in effect on its UTC day, in SQL, from unrounded values. The
5
+ -- displayed label for these numbers is "observed tokens at API list price" (README principle 3):
6
+ -- a projection, never "what you would have spent".
7
+
8
+ CREATE TABLE prices (
9
+ model_id TEXT NOT NULL, -- source: prices.json model_id (exact message.model string)
10
+ effective_from TEXT NOT NULL, -- source: prices.json effective_from, YYYY-MM-DD; 0000-01-01 = since release (D-020)
11
+ input_per_mtok REAL NOT NULL, -- source: pricing page, base input USD per million tokens
12
+ output_per_mtok REAL NOT NULL, -- source: pricing page, output
13
+ cache_write_5m_per_mtok REAL NOT NULL, -- source: pricing page, 5-minute cache writes
14
+ cache_write_1h_per_mtok REAL NOT NULL, -- source: pricing page, 1-hour cache writes
15
+ cache_read_per_mtok REAL NOT NULL, -- source: pricing page, cache hits and refreshes
16
+ fast_input_per_mtok REAL, -- source: pricing page, fast mode input; NULL when not published
17
+ fast_output_per_mtok REAL, -- source: pricing page, fast mode output
18
+ fast_cache_write_5m_per_mtok REAL, -- source: pricing page, caching multipliers on fast input
19
+ fast_cache_write_1h_per_mtok REAL, -- source: pricing page, caching multipliers on fast input
20
+ fast_cache_read_per_mtok REAL, -- source: pricing page, caching multipliers on fast input
21
+ standard_rate_max_input_tokens INTEGER, -- source: prices.json; NULL when the full context window is standard-priced
22
+ source_url TEXT NOT NULL, -- source: page the rates were read from
23
+ verified_on TEXT NOT NULL, -- source: day the page was read, YYYY-MM-DD
24
+ PRIMARY KEY (model_id, effective_from)
25
+ ) STRICT;
26
+
27
+ ALTER TABLE requests ADD COLUMN service_tier TEXT; -- source: message.usage.service_tier when it's a string
28
+ ALTER TABLE requests ADD COLUMN inference_geo TEXT; -- source: message.usage.inference_geo when it's a string
29
+
30
+ -- Projected: each deduplicated request at API list price, or NULL cost with the reason it can't be priced.
31
+ CREATE VIEW request_costs AS
32
+ WITH matched AS (
33
+ SELECT
34
+ d.*,
35
+ r.service_tier,
36
+ r.inference_geo,
37
+ substr(d.timestamp_utc, 1, 10) AS day_utc,
38
+ d.input_tokens + d.cache_read_tokens + COALESCE(d.cache_write_5m_tokens, 0)
39
+ + COALESCE(d.cache_write_1h_tokens, 0) + COALESCE(d.cache_write_unsplit_tokens, 0) AS total_input_tokens,
40
+ (SELECT p.effective_from FROM prices p
41
+ WHERE p.model_id = d.model AND p.effective_from <= substr(d.timestamp_utc, 1, 10)
42
+ ORDER BY p.effective_from DESC LIMIT 1) AS price_effective_from
43
+ FROM requests_dedup d
44
+ JOIN requests r ON r.raw_line_id = d.raw_line_id
45
+ ),
46
+ rated AS (
47
+ SELECT
48
+ m.*,
49
+ p.verified_on,
50
+ p.standard_rate_max_input_tokens,
51
+ CASE
52
+ WHEN m.timestamp_utc IS NULL THEN 'unparsed_timestamp'
53
+ WHEN p.model_id IS NULL THEN 'no_price_row'
54
+ WHEN m.speed = 'fast' AND p.fast_input_per_mtok IS NULL THEN 'fast_rate_unknown'
55
+ WHEN m.service_tier IS NOT NULL AND m.service_tier <> 'standard' THEN 'service_tier_not_standard'
56
+ WHEN p.standard_rate_max_input_tokens IS NOT NULL
57
+ AND m.total_input_tokens > p.standard_rate_max_input_tokens THEN 'long_context_rate_unverified'
58
+ ELSE NULL
59
+ END AS unpriced_reason,
60
+ CASE WHEN m.speed = 'fast' THEN p.fast_input_per_mtok ELSE p.input_per_mtok END AS rate_input,
61
+ CASE WHEN m.speed = 'fast' THEN p.fast_output_per_mtok ELSE p.output_per_mtok END AS rate_output,
62
+ CASE WHEN m.speed = 'fast' THEN p.fast_cache_write_5m_per_mtok ELSE p.cache_write_5m_per_mtok END AS rate_write_5m,
63
+ CASE WHEN m.speed = 'fast' THEN p.fast_cache_write_1h_per_mtok ELSE p.cache_write_1h_per_mtok END AS rate_write_1h,
64
+ CASE WHEN m.speed = 'fast' THEN p.fast_cache_read_per_mtok ELSE p.cache_read_per_mtok END AS rate_read,
65
+ CASE WHEN m.inference_geo = 'us' THEN 1.1 ELSE 1.0 END AS geo_multiplier
66
+ FROM matched m
67
+ LEFT JOIN prices p ON p.model_id = m.model AND p.effective_from = m.price_effective_from
68
+ )
69
+ SELECT
70
+ raw_line_id, dedup_key, model, day_utc, timestamp_utc, speed, service_tier, inference_geo,
71
+ input_tokens, output_tokens, cache_read_tokens,
72
+ cache_write_5m_tokens, cache_write_1h_tokens, cache_write_unsplit_tokens,
73
+ unpriced_reason,
74
+ price_effective_from,
75
+ verified_on,
76
+ -- 1 when unsplit cache writes were priced at the 5-minute rate, so the cost is a lower bound.
77
+ CASE WHEN unpriced_reason IS NULL AND cache_write_unsplit_tokens > 0 THEN 1 ELSE 0 END AS is_lower_bound,
78
+ CASE WHEN unpriced_reason IS NULL THEN input_tokens * rate_input * geo_multiplier / 1e6 END AS input_usd,
79
+ CASE WHEN unpriced_reason IS NULL THEN output_tokens * rate_output * geo_multiplier / 1e6 END AS output_usd,
80
+ CASE WHEN unpriced_reason IS NULL THEN cache_read_tokens * rate_read * geo_multiplier / 1e6 END AS cache_read_usd,
81
+ CASE WHEN unpriced_reason IS NULL
82
+ THEN (COALESCE(cache_write_5m_tokens, 0) + COALESCE(cache_write_unsplit_tokens, 0)) * rate_write_5m * geo_multiplier / 1e6 END AS cache_write_5m_usd,
83
+ CASE WHEN unpriced_reason IS NULL THEN COALESCE(cache_write_1h_tokens, 0) * rate_write_1h * geo_multiplier / 1e6 END AS cache_write_1h_usd,
84
+ CASE WHEN unpriced_reason IS NULL THEN
85
+ (input_tokens * rate_input + output_tokens * rate_output + cache_read_tokens * rate_read
86
+ + (COALESCE(cache_write_5m_tokens, 0) + COALESCE(cache_write_unsplit_tokens, 0)) * rate_write_5m
87
+ + COALESCE(cache_write_1h_tokens, 0) * rate_write_1h) * geo_multiplier / 1e6
88
+ END AS total_usd
89
+ FROM rated;
90
+
91
+ -- Observed: requests that couldn't be priced, and why (D-005: never $0, never a family fallback).
92
+ CREATE VIEW unpriced_requests AS
93
+ SELECT raw_line_id, dedup_key, model, day_utc, speed, service_tier, unpriced_reason
94
+ FROM request_costs
95
+ WHERE unpriced_reason IS NOT NULL;
@@ -0,0 +1,263 @@
1
+ -- 008_interruptions: conversation links, resolved reset times, and the interruption metrics (P6.1).
2
+ --
3
+ -- Rules: D-004 (limit hits), D-021 (mid-task), D-022 (resumption), D-023 (lockout, merging hits).
4
+ -- Every metric has a summary view (obs_<metric>) returning covers_from/covers_to, and an events
5
+ -- view listing the rows behind its number, so each number can be re-summed from its events
6
+ -- (README principles 4 and 5). Timestamps are UTC text, `YYYY-MM-DDTHH:MM:SS.sssZ`, which sorts
7
+ -- chronologically. Spans subtract whole milliseconds, which is the timestamps' precision, so
8
+ -- they carry no floating-point noise.
9
+
10
+ ALTER TABLE parsed_lines ADD COLUMN uuid TEXT; -- source: uuid when it's a non-empty string
11
+ ALTER TABLE parsed_lines ADD COLUMN parent_uuid TEXT; -- source: parentUuid when it's a non-empty string (D-021)
12
+ ALTER TABLE parsed_lines ADD COLUMN origin_kind TEXT; -- source: origin.kind when it's a string (D-022)
13
+ ALTER TABLE parsed_lines ADD COLUMN user_content TEXT CHECK (user_content IN ('tool_result', 'prompt')); -- derived: user lines only; 'tool_result' when message.content holds a tool_result block
14
+ ALTER TABLE parsed_lines ADD COLUMN is_meta INTEGER NOT NULL DEFAULT 0 CHECK (is_meta IN (0, 1)); -- source: isMeta === true (1) or anything else (0)
15
+ ALTER TABLE events ADD COLUMN reset_at_utc TEXT; -- derived: limit hits only; reset_text resolved against the hit's timestamp (D-023)
16
+
17
+ -- Parent lookups and "next line in this session" lookups.
18
+ CREATE INDEX parsed_lines_session_uuid ON parsed_lines (session_id, uuid);
19
+ CREATE INDEX parsed_lines_session_time ON parsed_lines (session_id, timestamp_utc);
20
+
21
+ -- Observed: the time span each source's data covers, from the data present (README principle 5).
22
+ CREATE VIEW source_coverage AS
23
+ SELECT 'session_logs' AS source, MIN(timestamp_utc) AS covers_from, MAX(timestamp_utc) AS covers_to
24
+ FROM parsed_lines
25
+ WHERE timestamp_utc IS NOT NULL
26
+ UNION ALL
27
+ SELECT
28
+ 'status_line',
29
+ strftime('%Y-%m-%dT%H:%M:%fZ', MIN(captured_at_s), 'unixepoch'),
30
+ strftime('%Y-%m-%dT%H:%M:%fZ', MAX(captured_at_s), 'unixepoch')
31
+ FROM status_readings
32
+ WHERE status = 'ok';
33
+
34
+ -- Observed: logged limit hits, one per distinct line (copies of a uuid in one session count once), with the D-021 position.
35
+ CREATE VIEW logged_limit_hits AS
36
+ WITH copies AS (
37
+ SELECT
38
+ p.raw_line_id, p.session_id, p.uuid, p.parent_uuid, p.timestamp_utc, p.is_sidechain,
39
+ e.window, e.reset_text, e.reset_at_utc,
40
+ ROW_NUMBER() OVER (
41
+ -- A line without a uuid is its own group: NULL uuids must not merge with each other.
42
+ PARTITION BY p.session_id, p.uuid, CASE WHEN p.uuid IS NULL THEN p.raw_line_id END
43
+ ORDER BY f.root, f.relative_path, l.first_run_id, l.line_number
44
+ ) AS copy_rank
45
+ FROM events e
46
+ JOIN parsed_lines p ON p.raw_line_id = e.raw_line_id
47
+ JOIN raw_lines l ON l.id = e.raw_line_id
48
+ JOIN source_files f ON f.id = l.source_file_id
49
+ WHERE e.class = 'limit_hit'
50
+ )
51
+ SELECT
52
+ c.raw_line_id, c.session_id, c.uuid, c.parent_uuid, c.timestamp_utc, c.is_sidechain,
53
+ c.window, c.reset_text, c.reset_at_utc,
54
+ COALESCE((
55
+ SELECT CASE
56
+ WHEN par.user_content = 'tool_result' THEN 'mid_task'
57
+ WHEN par.user_content = 'prompt' AND par.is_meta = 0 THEN 'turn_start'
58
+ ELSE 'unknown'
59
+ END
60
+ FROM parsed_lines par
61
+ JOIN raw_lines pl ON pl.id = par.raw_line_id
62
+ JOIN source_files pf ON pf.id = pl.source_file_id
63
+ WHERE par.session_id = c.session_id AND par.uuid = c.parent_uuid
64
+ ORDER BY pf.root, pf.relative_path, pl.first_run_id, pl.line_number
65
+ LIMIT 1
66
+ ), 'unknown') AS position
67
+ FROM copies c
68
+ WHERE c.copy_rank = 1;
69
+
70
+ -- Observed: status line windows that reached 100%, one row per (window, resets_at), from init onward (D-023).
71
+ CREATE VIEW status_limit_groups AS
72
+ WITH at_limit AS (
73
+ SELECT
74
+ w.raw_line_id, w.window, w.resets_at, s.session_id, s.captured_at_s,
75
+ ROW_NUMBER() OVER (PARTITION BY w.window, w.resets_at ORDER BY s.captured_at_s, w.raw_line_id) AS reading_rank,
76
+ COUNT(*) OVER (PARTITION BY w.window, w.resets_at) AS readings_at_limit
77
+ FROM rate_limit_windows w
78
+ JOIN status_readings s ON s.raw_line_id = w.raw_line_id
79
+ WHERE s.status = 'ok' AND w.validity = 'valid'
80
+ AND w.window IN ('five_hour', 'seven_day') AND w.used_percentage >= 100
81
+ )
82
+ SELECT
83
+ raw_line_id, window, resets_at, session_id, readings_at_limit,
84
+ strftime('%Y-%m-%dT%H:%M:%fZ', captured_at_s, 'unixepoch') 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: every rate-limit interruption, logged hits merged with status line groups without double-counting (D-004, D-023).
91
+ CREATE VIEW obs_limit_hits_events AS
92
+ WITH logged AS (
93
+ SELECT
94
+ h.*,
95
+ (SELECT g.raw_line_id FROM status_limit_groups g
96
+ WHERE (h.window IS NULL OR h.window = g.window)
97
+ AND unixepoch(h.timestamp_utc, 'subsec') > g.resets_at - g.window_seconds
98
+ AND unixepoch(h.timestamp_utc, 'subsec') <= g.resets_at
99
+ ORDER BY g.resets_at, g.window
100
+ LIMIT 1) AS status_raw_line_id
101
+ FROM logged_limit_hits h
102
+ ),
103
+ merged AS (
104
+ SELECT
105
+ 'session_log' AS source,
106
+ l.raw_line_id,
107
+ l.status_raw_line_id,
108
+ l.session_id,
109
+ l.timestamp_utc AS hit_at_utc,
110
+ COALESCE(l.window, g.window) AS window,
111
+ -- The payload's resets_at is exact; reset text has minute precision.
112
+ COALESCE(g.reset_at_utc, l.reset_at_utc) AS reset_at_utc,
113
+ CASE WHEN g.raw_line_id IS NOT NULL THEN 'status_line' WHEN l.reset_at_utc IS NOT NULL THEN 'log_text' END AS reset_source,
114
+ l.reset_text,
115
+ l.position,
116
+ l.is_sidechain
117
+ FROM logged l
118
+ LEFT JOIN status_limit_groups g ON g.raw_line_id = l.status_raw_line_id
119
+ UNION ALL
120
+ SELECT 'status_line', g.raw_line_id, g.raw_line_id, g.session_id, g.first_at_utc, g.window,
121
+ g.reset_at_utc, 'status_line', NULL, 'unknown', 0
122
+ FROM status_limit_groups g
123
+ WHERE NOT EXISTS (SELECT 1 FROM logged l WHERE l.status_raw_line_id = g.raw_line_id)
124
+ )
125
+ SELECT
126
+ m.*,
127
+ (round(unixepoch(m.reset_at_utc, 'subsec') * 1000) - round(unixepoch(m.hit_at_utc, 'subsec') * 1000)) / 1000.0 AS hit_to_reset_seconds,
128
+ (SELECT MIN(d.timestamp_utc) FROM requests_dedup d
129
+ WHERE d.session_id = m.session_id AND d.timestamp_utc > m.hit_at_utc) AS next_session_request_at_utc,
130
+ (SELECT p.timestamp_utc FROM parsed_lines p
131
+ WHERE p.session_id = m.session_id AND p.user_content = 'prompt' AND p.is_meta = 0
132
+ AND p.timestamp_utc > m.hit_at_utc
133
+ ORDER BY p.timestamp_utc, p.raw_line_id LIMIT 1) AS next_session_prompt_at_utc,
134
+ -- origin.kind as written (D-022): never relabeled as automatic or manual.
135
+ (SELECT p.origin_kind FROM parsed_lines p
136
+ WHERE p.session_id = m.session_id AND p.user_content = 'prompt' AND p.is_meta = 0
137
+ AND p.timestamp_utc > m.hit_at_utc
138
+ ORDER BY p.timestamp_utc, p.raw_line_id LIMIT 1) AS next_session_prompt_origin_kind,
139
+ m.reset_at_utc > (SELECT covers_to FROM source_coverage WHERE source = 'session_logs') AS reset_after_coverage
140
+ FROM merged m;
141
+
142
+ -- Observed: count of rate-limit interruptions, by source and window.
143
+ CREATE VIEW obs_limit_hits AS
144
+ SELECT
145
+ COUNT(e.raw_line_id) AS hits,
146
+ COUNT(e.raw_line_id) FILTER (WHERE e.source = 'session_log') AS logged_hits,
147
+ COUNT(e.raw_line_id) FILTER (WHERE e.source = 'status_line') AS status_line_only_hits,
148
+ COUNT(e.raw_line_id) FILTER (WHERE e.window = 'five_hour') AS five_hour_hits,
149
+ COUNT(e.raw_line_id) FILTER (WHERE e.window = 'seven_day') AS seven_day_hits,
150
+ COUNT(e.raw_line_id) FILTER (WHERE e.window IS NULL) AS unknown_window_hits,
151
+ (SELECT covers_from FROM source_coverage WHERE source = 'session_logs') AS covers_from,
152
+ (SELECT covers_to FROM source_coverage WHERE source = 'session_logs') AS covers_to,
153
+ (SELECT covers_from FROM source_coverage WHERE source = 'status_line') AS status_line_covers_from,
154
+ (SELECT covers_to FROM source_coverage WHERE source = 'status_line') AS status_line_covers_to
155
+ FROM obs_limit_hits_events e;
156
+
157
+ -- Observed: the interruptions that stopped the model while it was continuing its own tool loop (D-021).
158
+ CREATE VIEW obs_mid_task_interruptions_events AS
159
+ SELECT * FROM obs_limit_hits_events WHERE position = 'mid_task';
160
+
161
+ -- Observed: mid-task interruptions, beside turn-start and unknown-position counts (D-021).
162
+ CREATE VIEW obs_mid_task_interruptions AS
163
+ SELECT
164
+ COUNT(e.raw_line_id) FILTER (WHERE e.position = 'mid_task') AS mid_task,
165
+ COUNT(e.raw_line_id) FILTER (WHERE e.position = 'turn_start') AS turn_start,
166
+ COUNT(e.raw_line_id) FILTER (WHERE e.position = 'unknown') AS position_unknown,
167
+ (SELECT covers_from FROM source_coverage WHERE source = 'session_logs') AS covers_from,
168
+ (SELECT covers_to FROM source_coverage WHERE source = 'session_logs') AS covers_to
169
+ FROM obs_limit_hits_events e;
170
+
171
+ -- Observed: which merged lockout interval each hit with a known reset belongs to (D-023).
172
+ CREATE VIEW obs_lockout_interval_hits AS
173
+ WITH spans AS (
174
+ SELECT
175
+ raw_line_id,
176
+ hit_at_utc AS start_utc,
177
+ -- A reset in the hit's own minute can precede the hit by seconds; the span is then empty.
178
+ MAX(reset_at_utc, hit_at_utc) AS end_utc
179
+ FROM obs_limit_hits_events
180
+ WHERE hit_at_utc IS NOT NULL AND reset_at_utc IS NOT NULL
181
+ ),
182
+ ordered AS (
183
+ SELECT
184
+ s.*,
185
+ MAX(end_utc) OVER (ORDER BY start_utc, end_utc, raw_line_id
186
+ ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING) AS previous_end_utc
187
+ FROM spans s
188
+ )
189
+ SELECT
190
+ raw_line_id, start_utc, end_utc,
191
+ -- A span starting after every earlier span has ended opens a new interval; touching spans merge.
192
+ SUM(CASE WHEN previous_end_utc IS NULL OR start_utc > previous_end_utc THEN 1 ELSE 0 END)
193
+ OVER (ORDER BY start_utc, end_utc, raw_line_id) AS interval_number
194
+ FROM ordered;
195
+
196
+ -- Observed: merged lockout intervals, hit → reset, with the first Claude Code request at or after each reset (D-023).
197
+ CREATE VIEW obs_lockout_intervals AS
198
+ WITH grouped AS (
199
+ SELECT
200
+ interval_number,
201
+ MIN(start_utc) AS locked_from_utc,
202
+ MAX(end_utc) AS locked_until_utc,
203
+ COUNT(*) AS hits
204
+ FROM obs_lockout_interval_hits
205
+ GROUP BY interval_number
206
+ ),
207
+ with_next AS (
208
+ SELECT
209
+ g.*,
210
+ (SELECT MIN(d.timestamp_utc) FROM requests_dedup d WHERE d.timestamp_utc >= g.locked_until_utc) AS next_request_at_utc
211
+ FROM grouped g
212
+ )
213
+ SELECT
214
+ w.*,
215
+ (round(unixepoch(w.locked_until_utc, 'subsec') * 1000) - round(unixepoch(w.locked_from_utc, 'subsec') * 1000)) / 1000.0 AS lockout_seconds,
216
+ (round(unixepoch(w.next_request_at_utc, 'subsec') * 1000) - round(unixepoch(w.locked_until_utc, 'subsec') * 1000)) / 1000.0 AS reset_to_next_request_seconds,
217
+ w.locked_until_utc > (SELECT covers_to FROM source_coverage WHERE source = 'session_logs') AS reset_after_coverage
218
+ FROM with_next w;
219
+
220
+ -- Observed: total elapsed lockout time (hit → reset, overlaps counted once), beside hits whose reset is unknown.
221
+ CREATE VIEW obs_lockout_time AS
222
+ SELECT
223
+ (SELECT COUNT(*) FROM obs_lockout_intervals) AS intervals,
224
+ (SELECT TOTAL(lockout_seconds) FROM obs_lockout_intervals) AS lockout_seconds,
225
+ (SELECT COUNT(*) FROM obs_lockout_interval_hits) AS hits_with_known_reset,
226
+ (SELECT COUNT(*) FROM obs_limit_hits_events WHERE reset_at_utc IS NULL OR hit_at_utc IS NULL) AS hits_with_unknown_reset,
227
+ (SELECT covers_from FROM source_coverage WHERE source = 'session_logs') AS covers_from,
228
+ (SELECT covers_to FROM source_coverage WHERE source = 'session_logs') AS covers_to;
229
+
230
+ -- Observed: sessions whose last limit hit has no later request in that session (D-022).
231
+ CREATE VIEW obs_sessions_not_resumed_events AS
232
+ WITH ranked AS (
233
+ SELECT
234
+ e.session_id, e.raw_line_id, e.hit_at_utc, e.reset_at_utc, e.reset_after_coverage,
235
+ ROW_NUMBER() OVER (PARTITION BY e.session_id ORDER BY e.hit_at_utc DESC, e.raw_line_id DESC) AS hit_rank,
236
+ COUNT(*) OVER (PARTITION BY e.session_id) AS hits
237
+ FROM obs_limit_hits_events e
238
+ WHERE e.session_id IS NOT NULL AND e.session_id <> '<missing>' AND e.hit_at_utc IS NOT NULL
239
+ )
240
+ SELECT
241
+ session_id,
242
+ raw_line_id AS last_hit_raw_line_id,
243
+ hit_at_utc AS last_hit_at_utc,
244
+ reset_at_utc,
245
+ reset_after_coverage,
246
+ hits
247
+ FROM ranked r
248
+ WHERE hit_rank = 1
249
+ AND NOT EXISTS (
250
+ SELECT 1 FROM requests_dedup d WHERE d.session_id = r.session_id AND d.timestamp_utc > r.hit_at_utc
251
+ );
252
+
253
+ -- Observed: sessions not resumed after a limit hit, with those whose reset was after log coverage counted separately.
254
+ CREATE VIEW obs_sessions_not_resumed AS
255
+ SELECT
256
+ (SELECT COUNT(DISTINCT session_id) FROM obs_limit_hits_events
257
+ WHERE session_id IS NOT NULL AND session_id <> '<missing>' AND hit_at_utc IS NOT NULL) AS sessions_with_hits,
258
+ COUNT(n.session_id) AS sessions_not_resumed,
259
+ COUNT(n.session_id) FILTER (WHERE n.reset_after_coverage = 1) AS reset_after_coverage,
260
+ COUNT(n.session_id) FILTER (WHERE n.reset_at_utc IS NULL) AS reset_unknown,
261
+ (SELECT covers_from FROM source_coverage WHERE source = 'session_logs') AS covers_from,
262
+ (SELECT covers_to FROM source_coverage WHERE source = 'session_logs') AS covers_to
263
+ FROM obs_sessions_not_resumed_events n;
@@ -0,0 +1,84 @@
1
+ -- 009_headroom: last observed usage and peak usage per rate-limit window, from init onward (P6.2, D-024).
2
+ --
3
+ -- A window instance is (window, resets_at). Readings come only from the status line, so these
4
+ -- metrics cover the status line's span, not the logs' (README principle 5). The last reading is a
5
+ -- lower bound on the window's final usage: budget spent after the last Claude Code turn isn't seen.
6
+
7
+ -- Observed: valid plan-window readings, each with its window instance and whether it was captured after that reset.
8
+ CREATE VIEW window_readings AS
9
+ SELECT
10
+ w.raw_line_id,
11
+ w.window,
12
+ w.resets_at,
13
+ strftime('%Y-%m-%dT%H:%M:%fZ', w.resets_at, 'unixepoch') AS reset_at_utc,
14
+ w.used_percentage,
15
+ s.session_id,
16
+ s.captured_at_s,
17
+ strftime('%Y-%m-%dT%H:%M:%fZ', s.captured_at_s, 'unixepoch') AS captured_at_utc,
18
+ s.captured_at_s > w.resets_at AS captured_after_reset
19
+ FROM rate_limit_windows w
20
+ JOIN status_readings s ON s.raw_line_id = w.raw_line_id
21
+ WHERE s.status = 'ok' AND w.validity = 'valid' AND w.window IN ('five_hour', 'seven_day');
22
+
23
+ -- Observed: per window instance, reading counts, whether it is still open, and the status line coverage.
24
+ CREATE VIEW window_instances AS
25
+ SELECT
26
+ window,
27
+ resets_at,
28
+ reset_at_utc,
29
+ COUNT(*) FILTER (WHERE captured_after_reset = 0) AS readings,
30
+ COUNT(*) FILTER (WHERE captured_after_reset = 1) AS readings_after_reset,
31
+ MIN(captured_at_utc) FILTER (WHERE captured_after_reset = 0) AS first_reading_at_utc,
32
+ -- Open: no status line reading at all was captured at or after this reset, so later usage may still come.
33
+ resets_at > (SELECT MAX(captured_at_s) FROM status_readings WHERE status = 'ok') AS window_open,
34
+ (SELECT covers_from FROM source_coverage WHERE source = 'status_line') AS covers_from,
35
+ (SELECT covers_to FROM source_coverage WHERE source = 'status_line') AS covers_to
36
+ FROM window_readings
37
+ GROUP BY window, resets_at;
38
+
39
+ -- Observed: last observed usage before each window's reset, with that reading's time (a lower bound on final usage).
40
+ CREATE VIEW obs_window_headroom AS
41
+ WITH ranked AS (
42
+ SELECT
43
+ r.*,
44
+ ROW_NUMBER() OVER (PARTITION BY r.window, r.resets_at ORDER BY r.captured_at_s DESC, r.raw_line_id DESC) AS latest_rank
45
+ FROM window_readings r
46
+ WHERE r.captured_after_reset = 0
47
+ )
48
+ SELECT
49
+ i.window,
50
+ i.reset_at_utc,
51
+ r.used_percentage AS last_used_percentage,
52
+ r.captured_at_utc AS last_reading_at_utc,
53
+ r.raw_line_id AS last_reading_raw_line_id,
54
+ i.readings,
55
+ i.readings_after_reset,
56
+ i.first_reading_at_utc,
57
+ i.window_open,
58
+ i.covers_from,
59
+ i.covers_to
60
+ FROM window_instances i
61
+ LEFT JOIN ranked r ON r.window = i.window AND r.resets_at = i.resets_at AND r.latest_rank = 1;
62
+
63
+ -- Observed: highest observed usage in each window, with the reading it came from.
64
+ CREATE VIEW obs_window_peak AS
65
+ WITH ranked AS (
66
+ SELECT
67
+ r.*,
68
+ ROW_NUMBER() OVER (PARTITION BY r.window, r.resets_at ORDER BY r.used_percentage DESC, r.captured_at_s, r.raw_line_id) AS peak_rank
69
+ FROM window_readings r
70
+ WHERE r.captured_after_reset = 0
71
+ )
72
+ SELECT
73
+ i.window,
74
+ i.reset_at_utc,
75
+ r.used_percentage AS peak_used_percentage,
76
+ r.captured_at_utc AS peak_reading_at_utc,
77
+ r.raw_line_id AS peak_reading_raw_line_id,
78
+ i.readings,
79
+ i.readings_after_reset,
80
+ i.window_open,
81
+ i.covers_from,
82
+ i.covers_to
83
+ FROM window_instances i
84
+ LEFT JOIN ranked r ON r.window = i.window AND r.resets_at = i.resets_at AND r.peak_rank = 1;
@@ -0,0 +1,106 @@
1
+ -- 010_attribution: where observed usage goes, by model and repository, and budget used outside Claude Code (P6.3).
2
+ --
3
+ -- Model and repository metrics are log-derived and cover the logs' span; unattributed usage is
4
+ -- status-line-derived and covers the status line's span (README principle 5). Token sums are
5
+ -- per request after dedup (D-001, D-007). A token type with no requests reporting it sums to NULL.
6
+
7
+ -- Observed: every deduplicated request with the model and repository it's attributed to.
8
+ CREATE VIEW obs_usage_events AS
9
+ SELECT
10
+ d.raw_line_id, d.dedup_key, d.session_id, d.timestamp_utc, d.model,
11
+ r.repository, r.repo_kind,
12
+ d.input_tokens, d.output_tokens, d.cache_read_tokens,
13
+ d.cache_write_5m_tokens, d.cache_write_1h_tokens, d.cache_write_unsplit_tokens
14
+ FROM requests_dedup d
15
+ JOIN request_repositories r ON r.raw_line_id = d.raw_line_id;
16
+
17
+ -- Observed: tokens per model per token type, with request counts and unknowns.
18
+ CREATE VIEW obs_usage_by_model AS
19
+ SELECT
20
+ model,
21
+ COUNT(*) AS requests,
22
+ SUM(input_tokens) AS input_tokens,
23
+ SUM(output_tokens) AS output_tokens,
24
+ SUM(cache_read_tokens) AS cache_read_tokens,
25
+ SUM(cache_write_5m_tokens) AS cache_write_5m_tokens,
26
+ SUM(cache_write_1h_tokens) AS cache_write_1h_tokens,
27
+ SUM(cache_write_unsplit_tokens) AS cache_write_unsplit_tokens,
28
+ COUNT(*) FILTER (WHERE dedup_key IS NULL) AS unkeyed_requests,
29
+ COUNT(*) FILTER (WHERE timestamp_utc IS NULL) AS requests_without_timestamp,
30
+ MIN(timestamp_utc) AS first_request_at_utc,
31
+ MAX(timestamp_utc) AS last_request_at_utc,
32
+ (SELECT covers_from FROM source_coverage WHERE source = 'session_logs') AS covers_from,
33
+ (SELECT covers_to FROM source_coverage WHERE source = 'session_logs') AS covers_to
34
+ FROM obs_usage_events
35
+ GROUP BY model;
36
+
37
+ -- Observed: tokens per repository (git root, or the working directory outside git; NULL when no cwd) per token type.
38
+ CREATE VIEW obs_usage_by_repo AS
39
+ SELECT
40
+ repository,
41
+ repo_kind,
42
+ COUNT(*) AS requests,
43
+ SUM(input_tokens) AS input_tokens,
44
+ SUM(output_tokens) AS output_tokens,
45
+ SUM(cache_read_tokens) AS cache_read_tokens,
46
+ SUM(cache_write_5m_tokens) AS cache_write_5m_tokens,
47
+ SUM(cache_write_1h_tokens) AS cache_write_1h_tokens,
48
+ SUM(cache_write_unsplit_tokens) AS cache_write_unsplit_tokens,
49
+ COUNT(*) FILTER (WHERE timestamp_utc IS NULL) AS requests_without_timestamp,
50
+ (SELECT covers_from FROM source_coverage WHERE source = 'session_logs') AS covers_from,
51
+ (SELECT covers_to FROM source_coverage WHERE source = 'session_logs') AS covers_to
52
+ FROM obs_usage_events
53
+ GROUP BY repository, repo_kind;
54
+
55
+ -- Observed: consecutive readings of one window, the change between them, and Claude Code requests in between (D-025).
56
+ CREATE VIEW window_reading_pairs AS
57
+ WITH ordered AS (
58
+ SELECT
59
+ r.*,
60
+ LAG(r.raw_line_id) OVER w AS previous_raw_line_id,
61
+ LAG(r.used_percentage) OVER w AS previous_used_percentage,
62
+ LAG(r.captured_at_s) OVER w AS previous_captured_at_s
63
+ FROM window_readings r
64
+ WHERE r.captured_after_reset = 0
65
+ WINDOW w AS (PARTITION BY r.window, r.resets_at ORDER BY r.captured_at_s, r.raw_line_id)
66
+ )
67
+ SELECT
68
+ o.window,
69
+ o.reset_at_utc,
70
+ o.previous_raw_line_id,
71
+ o.raw_line_id,
72
+ strftime('%Y-%m-%dT%H:%M:%fZ', o.previous_captured_at_s, 'unixepoch') AS previous_captured_at_utc,
73
+ o.captured_at_utc,
74
+ o.used_percentage - o.previous_used_percentage AS change_percentage_points,
75
+ -- Captures are whole seconds: include the previous capture's second, and one second past this capture.
76
+ (SELECT COUNT(*) FROM requests_dedup d
77
+ WHERE d.timestamp_utc >= strftime('%Y-%m-%dT%H:%M:%fZ', o.previous_captured_at_s, 'unixepoch')
78
+ AND d.timestamp_utc < strftime('%Y-%m-%dT%H:%M:%fZ', o.captured_at_s + 1, 'unixepoch')) AS requests_between
79
+ FROM ordered o
80
+ WHERE o.previous_raw_line_id IS NOT NULL;
81
+
82
+ -- Observed: reading pairs whose usage rose with no Claude Code request in between (D-025).
83
+ CREATE VIEW obs_unattributed_usage_events AS
84
+ SELECT * FROM window_reading_pairs
85
+ WHERE requests_between = 0 AND change_percentage_points > 0;
86
+
87
+ -- Observed: per window, percentage points consumed with no Claude Code request in between (a lower bound), beside pair counts.
88
+ CREATE VIEW obs_unattributed_usage AS
89
+ SELECT
90
+ i.window,
91
+ i.reset_at_utc,
92
+ i.readings,
93
+ (SELECT COUNT(*) FROM window_reading_pairs p WHERE p.window = i.window AND p.reset_at_utc = i.reset_at_utc) AS pairs,
94
+ (SELECT COUNT(*) FROM window_reading_pairs p
95
+ WHERE p.window = i.window AND p.reset_at_utc = i.reset_at_utc AND p.requests_between = 0) AS pairs_without_requests,
96
+ (SELECT COUNT(*) FROM window_reading_pairs p
97
+ WHERE p.window = i.window AND p.reset_at_utc = i.reset_at_utc AND p.change_percentage_points < 0) AS decreasing_pairs,
98
+ -- Fewer than two readings means no pair was observed: unknown, not 0.
99
+ CASE WHEN i.readings >= 2 THEN
100
+ (SELECT TOTAL(e.change_percentage_points) FROM obs_unattributed_usage_events e
101
+ WHERE e.window = i.window AND e.reset_at_utc = i.reset_at_utc)
102
+ END AS unattributed_percentage_points,
103
+ i.window_open,
104
+ i.covers_from,
105
+ i.covers_to
106
+ FROM window_instances i;
@@ -0,0 +1,83 @@
1
+ -- 011_projections: projections derived from observations, kept apart from observed views (P6.4, D-026, D-027).
2
+ --
3
+ -- Every view here is a projection: it carries the proj_ prefix, and no obs_ view may read from it
4
+ -- (README principle 2; enforced by tests/unit/core/metrics/structure.test.ts). plan_prices is user
5
+ -- data entered with the plan-price command: not derived, never rebuilt, never committed.
6
+
7
+ CREATE TABLE plan_prices (
8
+ effective_month TEXT PRIMARY KEY, -- source: user entry, YYYY-MM (local calendar month from which the price applies)
9
+ plan_name TEXT NOT NULL, -- source: user entry, the plan's name as the user writes it
10
+ usd_per_month REAL NOT NULL, -- source: user entry, the plan's USD list price per month (README, Currency)
11
+ entered_at TEXT NOT NULL, -- derived: ISO-8601 UTC time the row was written
12
+ CHECK (effective_month GLOB '[0-9][0-9][0-9][0-9]-[0-1][0-9]'),
13
+ CHECK (usd_per_month >= 0)
14
+ ) STRICT;
15
+
16
+ -- Projected: when each window's limit would be reached at its average rate since the window began (D-026).
17
+ CREATE VIEW proj_burn_rate AS
18
+ WITH timed AS (
19
+ SELECT
20
+ h.*,
21
+ unixepoch(h.reset_at_utc) - CASE h.window WHEN 'five_hour' THEN 5 * 3600 ELSE 7 * 86400 END AS window_start_s,
22
+ unixepoch(h.last_reading_at_utc, 'subsec') AS last_reading_s
23
+ FROM obs_window_headroom h
24
+ )
25
+ SELECT
26
+ window,
27
+ reset_at_utc,
28
+ strftime('%Y-%m-%dT%H:%M:%fZ', window_start_s, 'unixepoch') AS window_start_utc,
29
+ last_used_percentage,
30
+ last_reading_at_utc,
31
+ window_open,
32
+ last_used_percentage >= 100 AS limit_reached,
33
+ -- Unknown without a reading after the window start: the premise (0% at the start) can't apply.
34
+ CASE WHEN last_reading_s > window_start_s
35
+ THEN last_used_percentage * 3600.0 / (last_reading_s - window_start_s)
36
+ END AS projected_percentage_points_per_hour,
37
+ CASE WHEN last_reading_s > window_start_s AND last_used_percentage > 0 AND last_used_percentage < 100
38
+ THEN strftime('%Y-%m-%dT%H:%M:%fZ',
39
+ window_start_s + (last_reading_s - window_start_s) * 100.0 / last_used_percentage,
40
+ 'unixepoch')
41
+ END AS projected_limit_at_utc,
42
+ CASE WHEN last_reading_s > window_start_s AND last_used_percentage > 0 AND last_used_percentage < 100
43
+ THEN window_start_s + (last_reading_s - window_start_s) * 100.0 / last_used_percentage < unixepoch(reset_at_utc)
44
+ END AS projected_limit_before_reset,
45
+ covers_from,
46
+ covers_to
47
+ FROM timed;
48
+
49
+ -- Projected: observed tokens at API list price per local calendar month, beside the plan price entered for that month (D-027).
50
+ CREATE VIEW proj_api_list_price AS
51
+ WITH months AS (
52
+ SELECT
53
+ strftime('%Y-%m', timestamp_utc, 'localtime') AS month,
54
+ COUNT(*) AS requests,
55
+ COUNT(*) FILTER (WHERE unpriced_reason IS NULL) AS priced_requests,
56
+ COUNT(*) FILTER (WHERE unpriced_reason IS NOT NULL) AS unpriced_requests,
57
+ COUNT(*) FILTER (WHERE is_lower_bound = 1) AS lower_bound_requests,
58
+ COUNT(*) FILTER (WHERE unpriced_reason IS NULL AND day_utc < verified_on) AS priced_before_verified_requests,
59
+ MAX(verified_on) AS verified_on,
60
+ TOTAL(total_usd) AS priced_usd,
61
+ MIN(timestamp_utc) AS covers_from,
62
+ MAX(timestamp_utc) AS covers_to
63
+ FROM request_costs
64
+ WHERE timestamp_utc IS NOT NULL
65
+ GROUP BY month
66
+ )
67
+ SELECT
68
+ m.month,
69
+ m.requests,
70
+ m.priced_requests,
71
+ m.unpriced_requests,
72
+ m.lower_bound_requests,
73
+ m.priced_before_verified_requests,
74
+ m.verified_on,
75
+ -- No priced request in the month: the cost is unknown, not $0.
76
+ CASE WHEN m.priced_requests > 0 THEN m.priced_usd END AS api_list_price_usd,
77
+ (SELECT p.plan_name FROM plan_prices p WHERE p.effective_month <= m.month
78
+ ORDER BY p.effective_month DESC LIMIT 1) AS plan_name,
79
+ (SELECT p.usd_per_month FROM plan_prices p WHERE p.effective_month <= m.month
80
+ ORDER BY p.effective_month DESC LIMIT 1) AS plan_usd_per_month,
81
+ m.covers_from,
82
+ m.covers_to
83
+ FROM months m;