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.
- package/CHANGELOG.md +68 -0
- package/LICENSE +21 -0
- package/README.md +464 -2
- package/core/pricing/prices.json +348 -0
- package/core/schema/001_ingestion.sql +57 -0
- package/core/schema/002_line_count.sql +7 -0
- package/core/schema/003_derived.sql +70 -0
- package/core/schema/004_requests_dedup.sql +50 -0
- package/core/schema/005_repositories.sql +25 -0
- package/core/schema/006_status_readings.sql +33 -0
- package/core/schema/007_pricing.sql +95 -0
- package/core/schema/008_interruptions.sql +263 -0
- package/core/schema/009_headroom.sql +84 -0
- package/core/schema/010_attribution.sql +106 -0
- package/core/schema/011_projections.sql +83 -0
- package/core/schema/012_unsplit_cache_writes.sql +49 -0
- package/core/schema/013_repository_revalidation.sql +104 -0
- package/core/schema/014_observed_readings.sql +204 -0
- package/core/schema/015_response_observations.sql +127 -0
- package/core/schema/016_windows_repository_paths.sql +41 -0
- package/core/schema/017_unattributed_usage_once.sql +47 -0
- package/core/schema/018_request_time_index.sql +21 -0
- package/core/schema/019_dedup_earliest_of_equals.sql +61 -0
- package/core/schema/020_limit_quota_fields.sql +131 -0
- package/core/schema/021_plan_periods.sql +121 -0
- package/core/schema/022_attachment_walk.sql +223 -0
- package/core/schema/023_auto_continuations.sql +129 -0
- package/core/schema/024_counted_limit_groups.sql +29 -0
- package/core/schema/025_model_fallback.sql +77 -0
- package/core/schema/026_changed_readings.sql +106 -0
- package/core/schema/027_weekly_crossings.sql +32 -0
- package/core/schema/028_stale_readings.sql +196 -0
- package/core/schema/029_lockout_sessions.sql +68 -0
- package/core/schema/030_repository_aliases.sql +81 -0
- package/core/schema/031_cte_cursor_budget.sql +153 -0
- package/core/schema/032_stored_views.sql +233 -0
- package/dist/cli/main.cli.js +39 -0
- package/dist/cli/program.js +881 -0
- package/dist/core/db/database.js +182 -0
- package/dist/core/db/fingerprint.js +71 -0
- package/dist/core/ingest/attribution.js +131 -0
- package/dist/core/ingest/classify.js +382 -0
- package/dist/core/ingest/command.js +95 -0
- package/dist/core/ingest/derive.js +219 -0
- package/dist/core/ingest/discover.js +126 -0
- package/dist/core/ingest/ingest.js +160 -0
- package/dist/core/ingest/reader.js +89 -0
- package/dist/core/ingest/reset-time.js +97 -0
- package/dist/core/ingest/results.js +201 -0
- package/dist/core/ingest/spool.js +165 -0
- package/dist/core/ingest/store.js +99 -0
- package/dist/core/ingest/stored-views.js +65 -0
- package/dist/core/install/install.js +185 -0
- package/dist/core/install/locations.js +89 -0
- package/dist/core/install/private-files.js +259 -0
- package/dist/core/install/record.js +103 -0
- package/dist/core/plans/plan-prices.js +128 -0
- package/dist/core/plans/repository-aliases.js +136 -0
- package/dist/core/pricing/prices.js +147 -0
- package/dist/core/settings/settings-file.js +215 -0
- package/dist/core/settings/statusline.js +240 -0
- package/dist/core/util/commands.js +91 -0
- package/dist/core/util/node-version.js +58 -0
- package/dist/core/util/open-page.js +69 -0
- package/dist/core/util/paths.js +36 -0
- package/dist/core/verify/compare.js +77 -0
- package/dist/core/verify/verify.js +220 -0
- package/dist/core/view/server.js +142 -0
- package/dist/viewer/explain.js +574 -0
- package/dist/viewer/format.js +150 -0
- package/dist/viewer/html-charts.js +1292 -0
- package/dist/viewer/html-contract.js +2 -0
- package/dist/viewer/html-data.js +718 -0
- package/dist/viewer/html.js +1717 -0
- package/dist/viewer/queries.js +128 -0
- package/dist/viewer/render.js +602 -0
- package/dist/viewer/report.js +120 -0
- package/dist/viewer/save.js +110 -0
- package/docs/requirements.md +113 -0
- package/hooks/statusline.sh +104 -0
- package/package.json +61 -4
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
-- 025_model_fallback: a request refused by one model and retried by another is its own event (D-080, step G2.8).
|
|
2
|
+
--
|
|
3
|
+
-- Claude Code writes a system line with subtype model_refusal_fallback when a model refuses a
|
|
4
|
+
-- request and another model retries it, naming both models, the requestId, and a refusal
|
|
5
|
+
-- category (observed 2026-10-03). The refused request writes no request line, so no token count
|
|
6
|
+
-- exists for it. Until now the line was ignored_type; it's the only place the refusal is recorded.
|
|
7
|
+
--
|
|
8
|
+
-- parsed_lines and events are rebuilt because SQLite can't change a CHECK constraint in place.
|
|
9
|
+
-- Both are derived: the parser version bump that ships with this migration re-derives them from
|
|
10
|
+
-- raw_lines, so the rows copied here only keep the database consistent until then.
|
|
11
|
+
|
|
12
|
+
-- Dozens of views read parsed_lines and events by name. A modern ALTER TABLE ... RENAME re-checks
|
|
13
|
+
-- every view, and fails while the old table is dropped and the new one not yet renamed. Legacy
|
|
14
|
+
-- rename leaves views untouched; they resolve to the rebuilt tables by name once this finishes.
|
|
15
|
+
PRAGMA legacy_alter_table = ON;
|
|
16
|
+
|
|
17
|
+
CREATE TABLE parsed_lines_025 (
|
|
18
|
+
raw_line_id INTEGER PRIMARY KEY REFERENCES raw_lines (id), -- derived: the line these fields came from
|
|
19
|
+
class TEXT NOT NULL, -- derived: classification, first matching rule wins
|
|
20
|
+
session_id TEXT, -- source: sessionId ('<missing>' if absent or not a string); NULL when malformed
|
|
21
|
+
type TEXT, -- source: type ('<missing>' if absent or not a string); NULL when malformed
|
|
22
|
+
subtype TEXT, -- source: subtype when it's a string, else NULL
|
|
23
|
+
timestamp_raw TEXT, -- source: timestamp as JSON text of its value; NULL when absent or malformed
|
|
24
|
+
timestamp_utc TEXT, -- derived: timestamp normalized to UTC with milliseconds; NULL when it doesn't parse
|
|
25
|
+
cwd TEXT, -- source: cwd when it's a string, else NULL
|
|
26
|
+
git_branch TEXT, -- source: gitBranch when it's a string, else NULL
|
|
27
|
+
cc_version TEXT, -- source: version (Claude Code version) when it's a string, else NULL
|
|
28
|
+
is_sidechain INTEGER NOT NULL, -- source: isSidechain === true (1) or anything else (0)
|
|
29
|
+
uuid TEXT, -- source: uuid when it's a non-empty string
|
|
30
|
+
parent_uuid TEXT, -- source: parentUuid when it's a non-empty string (D-021)
|
|
31
|
+
origin_kind TEXT, -- source: origin.kind when it's a string (D-022)
|
|
32
|
+
user_content TEXT, -- derived: user lines only; 'tool_result' when message.content holds a tool_result block, else 'prompt'
|
|
33
|
+
is_meta INTEGER NOT NULL DEFAULT 0, -- source: isMeta === true (1) or anything else (0)
|
|
34
|
+
CHECK (class IN ('malformed', 'limit_hit', 'api_error', 'synthetic_other', 'request', 'retry_notice', 'model_fallback', 'ignored_type')),
|
|
35
|
+
CHECK (is_sidechain IN (0, 1)),
|
|
36
|
+
CHECK (user_content IN ('tool_result', 'prompt')),
|
|
37
|
+
CHECK (is_meta IN (0, 1))
|
|
38
|
+
) STRICT;
|
|
39
|
+
INSERT INTO parsed_lines_025 (raw_line_id, class, session_id, type, subtype, timestamp_raw, timestamp_utc, cwd,
|
|
40
|
+
git_branch, cc_version, is_sidechain, uuid, parent_uuid, origin_kind, user_content, is_meta)
|
|
41
|
+
SELECT raw_line_id, class, session_id, type, subtype, timestamp_raw, timestamp_utc, cwd,
|
|
42
|
+
git_branch, cc_version, is_sidechain, uuid, parent_uuid, origin_kind, user_content, is_meta
|
|
43
|
+
FROM parsed_lines;
|
|
44
|
+
DROP TABLE parsed_lines;
|
|
45
|
+
ALTER TABLE parsed_lines_025 RENAME TO parsed_lines;
|
|
46
|
+
CREATE INDEX parsed_lines_session_uuid ON parsed_lines (session_id, uuid);
|
|
47
|
+
CREATE INDEX parsed_lines_session_time ON parsed_lines (session_id, timestamp_utc);
|
|
48
|
+
CREATE INDEX parsed_lines_request_time ON parsed_lines (timestamp_utc) WHERE class = 'request';
|
|
49
|
+
|
|
50
|
+
CREATE TABLE events_025 (
|
|
51
|
+
raw_line_id INTEGER PRIMARY KEY REFERENCES raw_lines (id), -- derived: the event line
|
|
52
|
+
class TEXT NOT NULL, -- derived: 'limit_hit', 'api_error', 'synthetic_other', 'retry_notice', or 'model_fallback'
|
|
53
|
+
error TEXT, -- source: error when it's a string
|
|
54
|
+
api_error_status NUMERIC, -- source: apiErrorStatus, or error.status for retry notices
|
|
55
|
+
window TEXT, -- derived: 'five_hour' or 'seven_day' parsed from limit-hit text (D-004)
|
|
56
|
+
reset_text TEXT, -- derived: text after 'resets ' in limit-hit text (D-004)
|
|
57
|
+
unknown_error_key TEXT, -- derived: report label for an api_error's unrecognized or missing error
|
|
58
|
+
retry_rate_limits_present INTEGER NOT NULL, -- derived: 1 when a retry notice's error.rateLimits is present and not null (D-019)
|
|
59
|
+
reset_at_utc TEXT, -- derived: limit hits only; reset_text resolved against the hit's timestamp (D-023)
|
|
60
|
+
quota_window TEXT, -- source: quotaLimits.rateLimitType when it's 'five_hour' or 'seven_day' (limit hits only, D-067)
|
|
61
|
+
quota_resets_at_utc TEXT, -- source: quotaLimits.resetsAt (Unix seconds, whole, 1..253402300799) as ISO-8601 UTC (limit hits only, D-067)
|
|
62
|
+
fallback_original_model TEXT, -- source: originalModel when it's a string (model fallbacks only, D-080)
|
|
63
|
+
fallback_model TEXT, -- source: fallbackModel when it's a string (model fallbacks only, D-080)
|
|
64
|
+
fallback_request_id TEXT, -- source: requestId when it's a string (model fallbacks only, D-080)
|
|
65
|
+
refusal_category TEXT, -- source: apiRefusalCategory when it's a string (model fallbacks only, D-080); stored, never shown
|
|
66
|
+
CHECK (class IN ('limit_hit', 'api_error', 'synthetic_other', 'retry_notice', 'model_fallback')),
|
|
67
|
+
CHECK (quota_window IN ('five_hour', 'seven_day'))
|
|
68
|
+
);
|
|
69
|
+
INSERT INTO events_025 (raw_line_id, class, error, api_error_status, window, reset_text, unknown_error_key,
|
|
70
|
+
retry_rate_limits_present, reset_at_utc, quota_window, quota_resets_at_utc)
|
|
71
|
+
SELECT raw_line_id, class, error, api_error_status, window, reset_text, unknown_error_key,
|
|
72
|
+
retry_rate_limits_present, reset_at_utc, quota_window, quota_resets_at_utc
|
|
73
|
+
FROM events;
|
|
74
|
+
DROP TABLE events;
|
|
75
|
+
ALTER TABLE events_025 RENAME TO events;
|
|
76
|
+
|
|
77
|
+
PRAGMA legacy_alter_table = OFF;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
-- 026_changed_readings: a value that changed without a new response is its own observation (D-086, step G3.1).
|
|
2
|
+
--
|
|
3
|
+
-- D-044 dates a reading by its session's last API response, on the premise that an idle session
|
|
4
|
+
-- repeats that response's numbers. Since Claude Code 2.1.281 an idle session sometimes carries the
|
|
5
|
+
-- account's later total instead (found 2026-10-19, weekly pass 3). Those captures were dated back to
|
|
6
|
+
-- the response, and keeping the latest capture per response replaced the response's own value.
|
|
7
|
+
-- Here the captures of one response are split into runs of the same value: the first run is dated
|
|
8
|
+
-- by the response, as before; each later run, by its first capture. Columns are unchanged.
|
|
9
|
+
|
|
10
|
+
DROP VIEW window_readings;
|
|
11
|
+
|
|
12
|
+
-- Observed: one row per observation of a plan window. Captures of one API response's numbers are dated by that response's first line (D-044, D-045); a value that changed after it, by its first capture (D-086).
|
|
13
|
+
CREATE VIEW window_readings AS
|
|
14
|
+
WITH readings AS (
|
|
15
|
+
SELECT
|
|
16
|
+
w.raw_line_id, w.window, w.resets_at, w.used_percentage, s.session_id, s.captured_at_s,
|
|
17
|
+
-- The response behind the latest request line in this session, inside this window, at or before
|
|
18
|
+
-- the capture (+1 s for whole-second captures). An unkeyed line is its own response.
|
|
19
|
+
(SELECT COALESCE(rq.dedup_key, 'line:' || rq.raw_line_id)
|
|
20
|
+
FROM parsed_lines p
|
|
21
|
+
JOIN requests rq ON rq.raw_line_id = p.raw_line_id
|
|
22
|
+
WHERE p.session_id = s.session_id AND p.class = 'request'
|
|
23
|
+
AND p.timestamp_utc > strftime('%Y-%m-%dT%H:%M:%fZ',
|
|
24
|
+
w.resets_at - CASE w.window WHEN 'five_hour' THEN 5 * 3600 ELSE 7 * 86400 END, 'unixepoch')
|
|
25
|
+
AND p.timestamp_utc < strftime('%Y-%m-%dT%H:%M:%fZ', s.captured_at_s + 1, 'unixepoch')
|
|
26
|
+
ORDER BY p.timestamp_utc DESC, p.raw_line_id DESC
|
|
27
|
+
LIMIT 1) AS response_key
|
|
28
|
+
FROM rate_limit_windows w
|
|
29
|
+
JOIN status_readings s ON s.raw_line_id = w.raw_line_id
|
|
30
|
+
WHERE s.status = 'ok' AND w.validity = 'valid' AND w.window IN ('five_hour', 'seven_day')
|
|
31
|
+
),
|
|
32
|
+
dated AS (
|
|
33
|
+
SELECT
|
|
34
|
+
r.*,
|
|
35
|
+
-- The response's first line: when its numbers first existed.
|
|
36
|
+
(SELECT MIN(p.timestamp_utc)
|
|
37
|
+
FROM requests rq
|
|
38
|
+
JOIN parsed_lines p ON p.raw_line_id = rq.raw_line_id
|
|
39
|
+
WHERE (r.response_key LIKE 'line:%' AND rq.raw_line_id = CAST(substr(r.response_key, 6) AS INTEGER))
|
|
40
|
+
OR rq.dedup_key = r.response_key) AS response_first_utc,
|
|
41
|
+
-- The previous capture of the same response, in capture order.
|
|
42
|
+
LAG(r.used_percentage) OVER captures AS previous_used_percentage
|
|
43
|
+
FROM readings r
|
|
44
|
+
-- A capture-time fallback is its own group: NULL response keys must not merge (D-044).
|
|
45
|
+
WINDOW captures AS (
|
|
46
|
+
PARTITION BY r.window, r.resets_at, r.session_id, r.response_key,
|
|
47
|
+
CASE WHEN r.response_key IS NULL THEN r.raw_line_id END
|
|
48
|
+
ORDER BY r.captured_at_s, r.raw_line_id
|
|
49
|
+
)
|
|
50
|
+
),
|
|
51
|
+
runs AS (
|
|
52
|
+
SELECT
|
|
53
|
+
d.*,
|
|
54
|
+
-- A new run starts at the first capture and at every change of more than 0.01 points: payloads
|
|
55
|
+
-- carry computed values such as 7.000000000000001, which are the same number (D-044, D-086).
|
|
56
|
+
SUM(CASE WHEN d.previous_used_percentage IS NULL
|
|
57
|
+
OR abs(d.used_percentage - d.previous_used_percentage) > 0.01 THEN 1 ELSE 0 END)
|
|
58
|
+
OVER (
|
|
59
|
+
PARTITION BY d.window, d.resets_at, d.session_id, d.response_key,
|
|
60
|
+
CASE WHEN d.response_key IS NULL THEN d.raw_line_id END
|
|
61
|
+
ORDER BY d.captured_at_s, d.raw_line_id
|
|
62
|
+
ROWS UNBOUNDED PRECEDING
|
|
63
|
+
) AS run
|
|
64
|
+
FROM dated d
|
|
65
|
+
),
|
|
66
|
+
observed AS (
|
|
67
|
+
SELECT
|
|
68
|
+
u.*,
|
|
69
|
+
-- A later run's value didn't come from the response, so it's dated when it was first seen.
|
|
70
|
+
CASE
|
|
71
|
+
WHEN u.response_first_utc IS NULL THEN strftime('%Y-%m-%dT%H:%M:%fZ', u.captured_at_s, 'unixepoch')
|
|
72
|
+
WHEN u.run = 1 THEN u.response_first_utc
|
|
73
|
+
ELSE strftime('%Y-%m-%dT%H:%M:%fZ', MIN(u.captured_at_s) OVER run_captures, 'unixepoch')
|
|
74
|
+
END AS observed_at_utc,
|
|
75
|
+
ROW_NUMBER() OVER run_captures_latest AS copy_rank
|
|
76
|
+
FROM runs u
|
|
77
|
+
WINDOW
|
|
78
|
+
run_captures AS (
|
|
79
|
+
PARTITION BY u.window, u.resets_at, u.session_id, u.response_key,
|
|
80
|
+
CASE WHEN u.response_key IS NULL THEN u.raw_line_id END, u.run
|
|
81
|
+
),
|
|
82
|
+
run_captures_latest AS (
|
|
83
|
+
PARTITION BY u.window, u.resets_at, u.session_id, u.response_key,
|
|
84
|
+
CASE WHEN u.response_key IS NULL THEN u.raw_line_id END, u.run
|
|
85
|
+
ORDER BY u.captured_at_s DESC, u.raw_line_id DESC
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
SELECT
|
|
89
|
+
raw_line_id,
|
|
90
|
+
window,
|
|
91
|
+
resets_at,
|
|
92
|
+
strftime('%Y-%m-%dT%H:%M:%fZ', resets_at, 'unixepoch') AS reset_at_utc,
|
|
93
|
+
used_percentage,
|
|
94
|
+
session_id,
|
|
95
|
+
captured_at_s,
|
|
96
|
+
strftime('%Y-%m-%dT%H:%M:%fZ', captured_at_s, 'unixepoch') AS captured_at_utc,
|
|
97
|
+
observed_at_utc,
|
|
98
|
+
unixepoch(observed_at_utc, 'subsec') AS observed_at_s,
|
|
99
|
+
CASE
|
|
100
|
+
WHEN response_key IS NULL THEN 'capture'
|
|
101
|
+
WHEN run = 1 THEN 'last_request'
|
|
102
|
+
ELSE 'changed_capture'
|
|
103
|
+
END AS observed_via,
|
|
104
|
+
unixepoch(observed_at_utc, 'subsec') > resets_at AS after_reset
|
|
105
|
+
FROM observed
|
|
106
|
+
WHERE copy_rank = 1;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
-- 027_weekly_crossings: when each weekly window was first read at or above 75% and 90% (D-087, step G3.2).
|
|
2
|
+
--
|
|
3
|
+
-- Interruptions count only limits that were hit. Weekly pass 3 found a week that reached 99% with
|
|
4
|
+
-- no hit: a weekly window at 91% with four and a half days left, and the work around it. This view
|
|
5
|
+
-- states the observed part, the first reading at or above each level and the time left before the
|
|
6
|
+
-- reset, without inferring why usage changed afterwards (principle 3).
|
|
7
|
+
|
|
8
|
+
-- Observed: per weekly window and level (75, 90), the first counted reading at or above it and the seconds from it to the window's reset, from init onward (D-087).
|
|
9
|
+
CREATE VIEW obs_weekly_crossings AS
|
|
10
|
+
WITH levels (threshold) AS (VALUES (75), (90)),
|
|
11
|
+
at_level AS (
|
|
12
|
+
SELECT
|
|
13
|
+
l.threshold, r.window, r.resets_at, r.reset_at_utc, r.raw_line_id, r.used_percentage,
|
|
14
|
+
r.observed_at_utc, r.observed_at_s,
|
|
15
|
+
ROW_NUMBER() OVER (
|
|
16
|
+
PARTITION BY l.threshold, r.window, r.resets_at ORDER BY r.observed_at_s, r.raw_line_id
|
|
17
|
+
) AS reading_rank
|
|
18
|
+
FROM window_readings r
|
|
19
|
+
JOIN levels l ON r.used_percentage >= l.threshold
|
|
20
|
+
-- Counted readings only: one after its reset belongs to no window (D-024).
|
|
21
|
+
WHERE r.window = 'seven_day' AND r.after_reset = 0
|
|
22
|
+
)
|
|
23
|
+
SELECT
|
|
24
|
+
window,
|
|
25
|
+
reset_at_utc,
|
|
26
|
+
threshold,
|
|
27
|
+
observed_at_utc AS first_at_utc,
|
|
28
|
+
used_percentage,
|
|
29
|
+
resets_at - observed_at_s AS seconds_before_reset,
|
|
30
|
+
raw_line_id
|
|
31
|
+
FROM at_level
|
|
32
|
+
WHERE reading_rank = 1;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
-- 028_stale_readings: a reading below an earlier one in its window is stale, kept and not used (D-089, step G4.1).
|
|
2
|
+
--
|
|
3
|
+
-- Within one window usage only rises, but about 1% of real readings came in lower than an earlier
|
|
4
|
+
-- reading in the same window, in every Claude Code version and model (found in the 2026-10-22
|
|
5
|
+
-- click-through: a 5-hour window locked at 100% from 01:17 showed a "last observed" 73% at 01:59).
|
|
6
|
+
-- Such a reading is marked stale. It stays a row, counted and listed in explain (principle 1), but
|
|
7
|
+
-- isn't the window's last observed value, isn't part of a reading pair, and isn't drawn on the line.
|
|
8
|
+
-- Peaks and crossings are unaffected: a stale reading is below an earlier one by definition.
|
|
9
|
+
|
|
10
|
+
DROP VIEW window_reading_pairs;
|
|
11
|
+
DROP VIEW obs_window_headroom;
|
|
12
|
+
DROP VIEW window_instances;
|
|
13
|
+
DROP VIEW window_readings;
|
|
14
|
+
|
|
15
|
+
-- Observed: one row per observation of a plan window, as in 026 (D-044, D-045, D-086), with stale = 1 when a counted reading is more than 1 point below an earlier counted one in its window (D-089).
|
|
16
|
+
CREATE VIEW window_readings AS
|
|
17
|
+
WITH readings AS (
|
|
18
|
+
SELECT
|
|
19
|
+
w.raw_line_id, w.window, w.resets_at, w.used_percentage, s.session_id, s.captured_at_s,
|
|
20
|
+
-- The response behind the latest request line in this session, inside this window, at or before
|
|
21
|
+
-- the capture (+1 s for whole-second captures). An unkeyed line is its own response.
|
|
22
|
+
(SELECT COALESCE(rq.dedup_key, 'line:' || rq.raw_line_id)
|
|
23
|
+
FROM parsed_lines p
|
|
24
|
+
JOIN requests rq ON rq.raw_line_id = p.raw_line_id
|
|
25
|
+
WHERE p.session_id = s.session_id AND p.class = 'request'
|
|
26
|
+
AND p.timestamp_utc > strftime('%Y-%m-%dT%H:%M:%fZ',
|
|
27
|
+
w.resets_at - CASE w.window WHEN 'five_hour' THEN 5 * 3600 ELSE 7 * 86400 END, 'unixepoch')
|
|
28
|
+
AND p.timestamp_utc < strftime('%Y-%m-%dT%H:%M:%fZ', s.captured_at_s + 1, 'unixepoch')
|
|
29
|
+
ORDER BY p.timestamp_utc DESC, p.raw_line_id DESC
|
|
30
|
+
LIMIT 1) AS response_key
|
|
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
|
+
dated AS (
|
|
36
|
+
SELECT
|
|
37
|
+
r.*,
|
|
38
|
+
-- The response's first line: when its numbers first existed.
|
|
39
|
+
(SELECT MIN(p.timestamp_utc)
|
|
40
|
+
FROM requests rq
|
|
41
|
+
JOIN parsed_lines p ON p.raw_line_id = rq.raw_line_id
|
|
42
|
+
WHERE (r.response_key LIKE 'line:%' AND rq.raw_line_id = CAST(substr(r.response_key, 6) AS INTEGER))
|
|
43
|
+
OR rq.dedup_key = r.response_key) AS response_first_utc,
|
|
44
|
+
-- The previous capture of the same response, in capture order.
|
|
45
|
+
LAG(r.used_percentage) OVER captures AS previous_used_percentage
|
|
46
|
+
FROM readings r
|
|
47
|
+
-- A capture-time fallback is its own group: NULL response keys must not merge (D-044).
|
|
48
|
+
WINDOW captures AS (
|
|
49
|
+
PARTITION BY r.window, r.resets_at, r.session_id, r.response_key,
|
|
50
|
+
CASE WHEN r.response_key IS NULL THEN r.raw_line_id END
|
|
51
|
+
ORDER BY r.captured_at_s, r.raw_line_id
|
|
52
|
+
)
|
|
53
|
+
),
|
|
54
|
+
runs AS (
|
|
55
|
+
SELECT
|
|
56
|
+
d.*,
|
|
57
|
+
-- A new run starts at the first capture and at every change of more than 0.01 points: payloads
|
|
58
|
+
-- carry computed values such as 7.000000000000001, which are the same number (D-044, D-086).
|
|
59
|
+
SUM(CASE WHEN d.previous_used_percentage IS NULL
|
|
60
|
+
OR abs(d.used_percentage - d.previous_used_percentage) > 0.01 THEN 1 ELSE 0 END)
|
|
61
|
+
OVER (
|
|
62
|
+
PARTITION BY d.window, d.resets_at, d.session_id, d.response_key,
|
|
63
|
+
CASE WHEN d.response_key IS NULL THEN d.raw_line_id END
|
|
64
|
+
ORDER BY d.captured_at_s, d.raw_line_id
|
|
65
|
+
ROWS UNBOUNDED PRECEDING
|
|
66
|
+
) AS run
|
|
67
|
+
FROM dated d
|
|
68
|
+
),
|
|
69
|
+
observed AS (
|
|
70
|
+
SELECT
|
|
71
|
+
u.*,
|
|
72
|
+
-- A later run's value didn't come from the response, so it's dated when it was first seen.
|
|
73
|
+
CASE
|
|
74
|
+
WHEN u.response_first_utc IS NULL THEN strftime('%Y-%m-%dT%H:%M:%fZ', u.captured_at_s, 'unixepoch')
|
|
75
|
+
WHEN u.run = 1 THEN u.response_first_utc
|
|
76
|
+
ELSE strftime('%Y-%m-%dT%H:%M:%fZ', MIN(u.captured_at_s) OVER run_captures, 'unixepoch')
|
|
77
|
+
END AS observed_at_utc,
|
|
78
|
+
ROW_NUMBER() OVER run_captures_latest AS copy_rank
|
|
79
|
+
FROM runs u
|
|
80
|
+
WINDOW
|
|
81
|
+
run_captures AS (
|
|
82
|
+
PARTITION BY u.window, u.resets_at, u.session_id, u.response_key,
|
|
83
|
+
CASE WHEN u.response_key IS NULL THEN u.raw_line_id END, u.run
|
|
84
|
+
),
|
|
85
|
+
run_captures_latest AS (
|
|
86
|
+
PARTITION BY u.window, u.resets_at, u.session_id, u.response_key,
|
|
87
|
+
CASE WHEN u.response_key IS NULL THEN u.raw_line_id END, u.run
|
|
88
|
+
ORDER BY u.captured_at_s DESC, u.raw_line_id DESC
|
|
89
|
+
)
|
|
90
|
+
),
|
|
91
|
+
counted AS (
|
|
92
|
+
SELECT
|
|
93
|
+
raw_line_id,
|
|
94
|
+
window,
|
|
95
|
+
resets_at,
|
|
96
|
+
strftime('%Y-%m-%dT%H:%M:%fZ', resets_at, 'unixepoch') AS reset_at_utc,
|
|
97
|
+
used_percentage,
|
|
98
|
+
session_id,
|
|
99
|
+
captured_at_s,
|
|
100
|
+
strftime('%Y-%m-%dT%H:%M:%fZ', captured_at_s, 'unixepoch') AS captured_at_utc,
|
|
101
|
+
observed_at_utc,
|
|
102
|
+
unixepoch(observed_at_utc, 'subsec') AS observed_at_s,
|
|
103
|
+
CASE
|
|
104
|
+
WHEN response_key IS NULL THEN 'capture'
|
|
105
|
+
WHEN run = 1 THEN 'last_request'
|
|
106
|
+
ELSE 'changed_capture'
|
|
107
|
+
END AS observed_via,
|
|
108
|
+
unixepoch(observed_at_utc, 'subsec') > resets_at AS after_reset
|
|
109
|
+
FROM observed
|
|
110
|
+
WHERE copy_rank = 1
|
|
111
|
+
)
|
|
112
|
+
SELECT
|
|
113
|
+
c.*,
|
|
114
|
+
-- Usage within one window only rises, so a counted reading more than 1 point below an earlier one
|
|
115
|
+
-- in the same window, from any session, carries a number from before that earlier reading (D-089).
|
|
116
|
+
CASE
|
|
117
|
+
WHEN c.after_reset = 0 AND c.used_percentage < MAX(c.used_percentage) OVER (
|
|
118
|
+
PARTITION BY c.window, c.resets_at ORDER BY c.observed_at_s, c.raw_line_id
|
|
119
|
+
ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING
|
|
120
|
+
) - 1 THEN 1
|
|
121
|
+
ELSE 0
|
|
122
|
+
END AS stale
|
|
123
|
+
FROM counted c;
|
|
124
|
+
|
|
125
|
+
-- Observed: per window instance, observation counts (stale ones also on their own), whether it is still open, and the status line coverage.
|
|
126
|
+
CREATE VIEW window_instances AS
|
|
127
|
+
SELECT
|
|
128
|
+
window,
|
|
129
|
+
resets_at,
|
|
130
|
+
reset_at_utc,
|
|
131
|
+
COUNT(*) FILTER (WHERE after_reset = 0) AS readings,
|
|
132
|
+
COUNT(*) FILTER (WHERE after_reset = 1) AS readings_after_reset,
|
|
133
|
+
MIN(observed_at_utc) FILTER (WHERE after_reset = 0) AS first_reading_at_utc,
|
|
134
|
+
-- Open: no status line reading at all was captured at or after this reset, so later usage may still come.
|
|
135
|
+
resets_at > (SELECT MAX(captured_at_s) FROM status_readings WHERE status = 'ok') AS window_open,
|
|
136
|
+
(SELECT covers_from FROM source_coverage WHERE source = 'status_line') AS covers_from,
|
|
137
|
+
(SELECT covers_to FROM source_coverage WHERE source = 'status_line') AS covers_to,
|
|
138
|
+
-- Counted readings marked stale: in readings above, and used for no value (D-089).
|
|
139
|
+
COUNT(*) FILTER (WHERE after_reset = 0 AND stale = 1) AS readings_stale
|
|
140
|
+
FROM window_readings
|
|
141
|
+
GROUP BY window, resets_at;
|
|
142
|
+
|
|
143
|
+
-- Observed: last observed usage before each window's reset that isn't stale, with when it was observed (a lower bound on final usage; D-024, D-044, D-089).
|
|
144
|
+
CREATE VIEW obs_window_headroom AS
|
|
145
|
+
WITH ranked AS (
|
|
146
|
+
SELECT
|
|
147
|
+
r.*,
|
|
148
|
+
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
|
|
149
|
+
FROM window_readings r
|
|
150
|
+
WHERE r.after_reset = 0 AND r.stale = 0
|
|
151
|
+
)
|
|
152
|
+
SELECT
|
|
153
|
+
i.window,
|
|
154
|
+
i.reset_at_utc,
|
|
155
|
+
r.used_percentage AS last_used_percentage,
|
|
156
|
+
r.observed_at_utc AS last_reading_at_utc,
|
|
157
|
+
r.raw_line_id AS last_reading_raw_line_id,
|
|
158
|
+
i.readings,
|
|
159
|
+
i.readings_after_reset,
|
|
160
|
+
i.first_reading_at_utc,
|
|
161
|
+
i.window_open,
|
|
162
|
+
i.covers_from,
|
|
163
|
+
i.covers_to,
|
|
164
|
+
i.readings_stale
|
|
165
|
+
FROM window_instances i
|
|
166
|
+
LEFT JOIN ranked r ON r.window = i.window AND r.resets_at = i.resets_at AND r.latest_rank = 1;
|
|
167
|
+
|
|
168
|
+
-- Observed: consecutive non-stale observations of one window, the change between them, and Claude Code responses in between (D-025, D-045, D-089).
|
|
169
|
+
CREATE VIEW window_reading_pairs AS
|
|
170
|
+
WITH ordered AS (
|
|
171
|
+
SELECT
|
|
172
|
+
r.*,
|
|
173
|
+
LAG(r.raw_line_id) OVER w AS previous_raw_line_id,
|
|
174
|
+
LAG(r.used_percentage) OVER w AS previous_used_percentage,
|
|
175
|
+
LAG(r.observed_at_utc) OVER w AS previous_observed_at_utc
|
|
176
|
+
FROM window_readings r
|
|
177
|
+
WHERE r.after_reset = 0 AND r.stale = 0
|
|
178
|
+
WINDOW w AS (PARTITION BY r.window, r.resets_at ORDER BY r.observed_at_s, r.raw_line_id)
|
|
179
|
+
)
|
|
180
|
+
SELECT
|
|
181
|
+
o.window,
|
|
182
|
+
o.reset_at_utc,
|
|
183
|
+
o.previous_raw_line_id,
|
|
184
|
+
o.raw_line_id,
|
|
185
|
+
o.previous_observed_at_utc,
|
|
186
|
+
o.observed_at_utc,
|
|
187
|
+
o.used_percentage - o.previous_used_percentage AS change_percentage_points,
|
|
188
|
+
-- Distinct responses with any line from the previous observation up to one second past this one.
|
|
189
|
+
(SELECT COUNT(DISTINCT COALESCE(rq.dedup_key, 'line:' || rq.raw_line_id))
|
|
190
|
+
FROM parsed_lines p
|
|
191
|
+
JOIN requests rq ON rq.raw_line_id = p.raw_line_id
|
|
192
|
+
WHERE p.class = 'request'
|
|
193
|
+
AND p.timestamp_utc >= o.previous_observed_at_utc
|
|
194
|
+
AND p.timestamp_utc < strftime('%Y-%m-%dT%H:%M:%fZ', o.observed_at_s + 1, 'unixepoch')) AS requests_between
|
|
195
|
+
FROM ordered o
|
|
196
|
+
WHERE o.previous_raw_line_id IS NOT NULL;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
-- 029_lockout_sessions: how many sessions each lockout refused requests from (D-090, step G4.2).
|
|
2
|
+
--
|
|
3
|
+
-- One lockout can refuse many requests: on 2026-09-24, 13 within 3 minutes across 3 sessions, 6 of
|
|
4
|
+
-- them in subagents. Shown as "13 hits", that read as 13 separate limits. The report now leads with
|
|
5
|
+
-- lockouts and calls the hit lines refused requests, naming the sessions they came from.
|
|
6
|
+
|
|
7
|
+
DROP VIEW obs_lockout_intervals;
|
|
8
|
+
|
|
9
|
+
-- Observed: elapsed lockout intervals (limit hit to reset, overlapping hits merged), with the next request after each reset, logged auto-continuations, and the sessions and refused requests in each (D-023, D-077, D-090).
|
|
10
|
+
CREATE VIEW obs_lockout_intervals AS
|
|
11
|
+
WITH interval_hits AS MATERIALIZED (
|
|
12
|
+
-- Computed once for everything below: interval membership is the costly part.
|
|
13
|
+
SELECT raw_line_id, start_utc, end_utc, interval_number FROM obs_lockout_interval_hits
|
|
14
|
+
),
|
|
15
|
+
grouped AS (
|
|
16
|
+
SELECT
|
|
17
|
+
interval_number,
|
|
18
|
+
MIN(start_utc) AS locked_from_utc,
|
|
19
|
+
MAX(end_utc) AS locked_until_utc,
|
|
20
|
+
COUNT(*) AS hits
|
|
21
|
+
FROM interval_hits
|
|
22
|
+
GROUP BY interval_number
|
|
23
|
+
),
|
|
24
|
+
with_next AS (
|
|
25
|
+
SELECT
|
|
26
|
+
g.*,
|
|
27
|
+
(SELECT MIN(d.timestamp_utc) FROM requests_dedup d WHERE d.timestamp_utc >= g.locked_until_utc) AS next_request_at_utc
|
|
28
|
+
FROM grouped g
|
|
29
|
+
),
|
|
30
|
+
sessions AS (
|
|
31
|
+
SELECT
|
|
32
|
+
h.interval_number,
|
|
33
|
+
-- A logged hit names its session; a status-line-only hit is a spool reading, which names one too.
|
|
34
|
+
COUNT(DISTINCT COALESCE(p.session_id, s.session_id)) AS sessions,
|
|
35
|
+
-- Hits from the session logs: each is a request the limit refused.
|
|
36
|
+
COUNT(p.raw_line_id) AS refused_requests
|
|
37
|
+
FROM interval_hits h
|
|
38
|
+
LEFT JOIN parsed_lines p ON p.raw_line_id = h.raw_line_id
|
|
39
|
+
LEFT JOIN status_readings s ON s.raw_line_id = h.raw_line_id
|
|
40
|
+
GROUP BY h.interval_number
|
|
41
|
+
),
|
|
42
|
+
resumed AS (
|
|
43
|
+
SELECT
|
|
44
|
+
h.interval_number,
|
|
45
|
+
COUNT(*) AS auto_continuations,
|
|
46
|
+
MIN((round(unixepoch(m.continued_at_utc, 'subsec') * 1000) - round(unixepoch(g.locked_until_utc, 'subsec') * 1000)) / 1000.0)
|
|
47
|
+
AS first_seconds
|
|
48
|
+
FROM auto_continuation_marks m
|
|
49
|
+
JOIN interval_hits h ON h.raw_line_id = m.hit_raw_line_id
|
|
50
|
+
JOIN grouped g ON g.interval_number = h.interval_number
|
|
51
|
+
GROUP BY h.interval_number
|
|
52
|
+
)
|
|
53
|
+
SELECT
|
|
54
|
+
w.*,
|
|
55
|
+
(round(unixepoch(w.locked_until_utc, 'subsec') * 1000) - round(unixepoch(w.locked_from_utc, 'subsec') * 1000)) / 1000.0 AS lockout_seconds,
|
|
56
|
+
(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,
|
|
57
|
+
w.locked_until_utc > (SELECT covers_to FROM source_coverage WHERE source = 'session_logs') AS reset_after_coverage,
|
|
58
|
+
-- How many sessions Claude Code resumed by itself after this reset, as logged; 0 when none.
|
|
59
|
+
COALESCE(r.auto_continuations, 0) AS auto_continuations,
|
|
60
|
+
-- The earliest of those, seconds after the reset; NULL when none.
|
|
61
|
+
r.first_seconds AS first_auto_continuation_seconds,
|
|
62
|
+
-- Distinct sessions with a hit in this lockout (D-090).
|
|
63
|
+
COALESCE(n.sessions, 0) AS sessions,
|
|
64
|
+
-- Its logged hits, each a refused request; the rest of its hits are status line readings at 100% (D-023, D-090).
|
|
65
|
+
COALESCE(n.refused_requests, 0) AS refused_requests
|
|
66
|
+
FROM with_next w
|
|
67
|
+
LEFT JOIN resumed r ON r.interval_number = w.interval_number
|
|
68
|
+
LEFT JOIN sessions n ON n.interval_number = w.interval_number;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
-- 030_repository_aliases: a renamed or moved project's old path counts under its new one (D-092, step G4.5).
|
|
2
|
+
--
|
|
3
|
+
-- Nothing in the logs links a repository's old path to its new one (D-028), so a renamed or moved
|
|
4
|
+
-- project shows twice. The first real case: this project under its old name on the old Mac, a
|
|
5
|
+
-- directory that no longer exists, beside its current checkout. The user enters the link with
|
|
6
|
+
-- `nilometer alias set`; it's user data, like plan prices, and rebuilds never touch it. Each
|
|
7
|
+
-- request keeps the path it was attributed to before the alias, so every number still traces.
|
|
8
|
+
|
|
9
|
+
-- User data: one row per old path; entering the same old path again replaces its target.
|
|
10
|
+
CREATE TABLE repository_aliases (
|
|
11
|
+
from_repository TEXT PRIMARY KEY, -- source: user entry, a repository path exactly as attributed (the old name or place)
|
|
12
|
+
to_repository TEXT NOT NULL, -- source: user entry, the repository path its requests count under
|
|
13
|
+
entered_at TEXT NOT NULL, -- derived: ISO-8601 UTC time the row was written
|
|
14
|
+
CHECK (from_repository <> to_repository)
|
|
15
|
+
) STRICT;
|
|
16
|
+
|
|
17
|
+
DROP VIEW request_repositories;
|
|
18
|
+
|
|
19
|
+
-- Observed: each deduplicated request's repository (worktrees and subfolders count as their repository, D-028, D-049), after any alias the user entered (D-092), with the path it had before.
|
|
20
|
+
CREATE VIEW request_repositories AS
|
|
21
|
+
WITH attributed AS (
|
|
22
|
+
SELECT
|
|
23
|
+
d.raw_line_id,
|
|
24
|
+
d.cwd,
|
|
25
|
+
CASE
|
|
26
|
+
WHEN r.cwd IS NULL THEN 'unresolved'
|
|
27
|
+
WHEN r.repo_root IS NOT NULL AND r.root_exists = 1 THEN 'repo'
|
|
28
|
+
WHEN r.repo_root IS NOT NULL THEN 'missing'
|
|
29
|
+
ELSE r.kind
|
|
30
|
+
END AS repo_kind,
|
|
31
|
+
COALESCE(r.repo_root, d.cwd) AS repository,
|
|
32
|
+
r.resolved_via
|
|
33
|
+
FROM requests_dedup d
|
|
34
|
+
LEFT JOIN repositories r ON r.cwd = d.cwd
|
|
35
|
+
),
|
|
36
|
+
normalized AS MATERIALIZED (
|
|
37
|
+
SELECT
|
|
38
|
+
raw_line_id,
|
|
39
|
+
cwd,
|
|
40
|
+
repo_kind,
|
|
41
|
+
CASE
|
|
42
|
+
WHEN repository GLOB '[A-Za-z]:[\/]*'
|
|
43
|
+
THEN upper(substr(repository, 1, 1)) || replace(substr(repository, 2), char(92), '/')
|
|
44
|
+
ELSE repository
|
|
45
|
+
END AS repository,
|
|
46
|
+
resolved_via
|
|
47
|
+
FROM attributed
|
|
48
|
+
),
|
|
49
|
+
targets AS (
|
|
50
|
+
-- An aliased request takes its target's kind, so the two count as one row: a repository when any
|
|
51
|
+
-- request under the target is one, else whatever the target has, else its own.
|
|
52
|
+
SELECT
|
|
53
|
+
a.from_repository,
|
|
54
|
+
a.to_repository,
|
|
55
|
+
(SELECT m.repo_kind FROM normalized m WHERE m.repository = a.to_repository
|
|
56
|
+
ORDER BY m.repo_kind = 'repo' DESC, m.repo_kind LIMIT 1) AS to_kind
|
|
57
|
+
FROM repository_aliases a
|
|
58
|
+
)
|
|
59
|
+
SELECT
|
|
60
|
+
n.raw_line_id,
|
|
61
|
+
n.cwd,
|
|
62
|
+
CASE WHEN t.from_repository IS NULL THEN n.repo_kind ELSE COALESCE(t.to_kind, n.repo_kind) END AS repo_kind,
|
|
63
|
+
COALESCE(t.to_repository, n.repository) AS repository,
|
|
64
|
+
n.resolved_via,
|
|
65
|
+
-- The repository the request had before an alias moved it; NULL when no alias applies.
|
|
66
|
+
CASE WHEN t.from_repository IS NULL THEN NULL ELSE n.repository END AS aliased_from
|
|
67
|
+
FROM normalized n
|
|
68
|
+
LEFT JOIN targets t ON t.from_repository = n.repository;
|
|
69
|
+
|
|
70
|
+
DROP VIEW obs_usage_events;
|
|
71
|
+
|
|
72
|
+
-- Observed: every deduplicated request with its repository, as obs_usage_by_repo sums it (D-028), and the path an alias moved it from (D-092).
|
|
73
|
+
CREATE VIEW obs_usage_events AS
|
|
74
|
+
SELECT
|
|
75
|
+
d.raw_line_id, d.dedup_key, d.session_id, d.timestamp_utc, d.model,
|
|
76
|
+
r.repository, r.repo_kind,
|
|
77
|
+
d.input_tokens, d.output_tokens, d.cache_read_tokens,
|
|
78
|
+
d.cache_write_5m_tokens, d.cache_write_1h_tokens, d.cache_write_unsplit_tokens,
|
|
79
|
+
r.aliased_from
|
|
80
|
+
FROM requests_dedup d
|
|
81
|
+
JOIN request_repositories r ON r.raw_line_id = d.raw_line_id;
|