cctally 1.117.2 → 1.118.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 +30 -0
- package/README.md +7 -3
- package/bin/_cctally_cache.py +2783 -175
- package/bin/_cctally_completion.py +106 -33
- package/bin/_cctally_core.py +69 -4
- package/bin/_cctally_dashboard.py +188 -13
- package/bin/_cctally_dashboard_conversation.py +5 -1
- package/bin/_cctally_dashboard_envelope.py +50 -19
- package/bin/_cctally_dashboard_sources.py +23 -1
- package/bin/_cctally_db.py +193 -153
- package/bin/_cctally_diagnosis_sources.py +63 -7
- package/bin/_cctally_doctor.py +865 -93
- package/bin/_cctally_five_hour.py +67 -50
- package/bin/_cctally_forecast.py +35 -13
- package/bin/_cctally_journal.py +186 -18
- package/bin/_cctally_milestone_history.py +859 -62
- package/bin/_cctally_oauth_samples.py +286 -15
- package/bin/_cctally_parser.py +2 -2
- package/bin/_cctally_quota.py +104 -12
- package/bin/_cctally_record.py +153 -29
- package/bin/_cctally_rederive.py +608 -68
- package/bin/_cctally_statusline.py +108 -81
- package/bin/_cctally_store.py +179 -38
- package/bin/_cctally_tui.py +892 -181
- package/bin/_cctally_update.py +32 -13
- package/bin/_lib_codex_conversation_query.py +52 -14
- package/bin/_lib_codex_drain_admission.py +224 -6
- package/bin/_lib_codex_metadata.py +39 -0
- package/bin/_lib_conversation_anon.py +15 -8
- package/bin/_lib_conversation_dispatch.py +7 -0
- package/bin/_lib_conversation_query.py +289 -70
- package/bin/_lib_dashboard_sources.py +37 -1
- package/bin/_lib_detector_access_registry.py +497 -26
- package/bin/_lib_doctor.py +85 -20
- package/bin/_lib_doctor_scan_index.py +321 -0
- package/bin/_lib_ingest_frontier.py +20 -1
- package/bin/_lib_oauth_samples.py +23 -7
- package/bin/_lib_pricing.py +146 -16
- package/bin/_lib_quota.py +19 -0
- package/bin/_lib_rederive.py +11 -6
- package/bin/_lib_retained_records.py +516 -0
- package/bin/_lib_snapshot_cache.py +10 -0
- package/bin/_lib_stale_proof.py +272 -43
- package/bin/_lib_stats_content_revision.py +831 -0
- package/bin/_lib_write_io.py +11 -4
- package/bin/cctally +4 -1
- package/dashboard/static/assets/ConversationsView-D4Iv4ku0.js +72 -0
- package/dashboard/static/assets/{DoctorModal-B0DnU9d9.js → DoctorModal-D4QSq6uz.js} +1 -1
- package/dashboard/static/assets/ModalRoot-C9F5dhZi.js +1 -0
- package/dashboard/static/assets/{ProjectsDrillPanel-DSxXQSa-.js → ProjectsDrillPanel-WxAdRDMt.js} +1 -1
- package/dashboard/static/assets/{SourceDetailModal-BleDpQqG.js → SourceDetailModal-NyyEfXmc.js} +1 -1
- package/dashboard/static/assets/{UpdateModal-CMqaAkSK.js → UpdateModal-CFLq9pT1.js} +1 -1
- package/dashboard/static/assets/{index-BjQV-0-D.js → index-BKTWH9JE.js} +2 -2
- package/dashboard/static/assets/{outlineNavigation-CO-VWBm7.js → outlineNavigation-BnGUwCS3.js} +3 -3
- package/dashboard/static/dashboard.html +1 -1
- package/package.json +4 -1
- package/dashboard/static/assets/ConversationsView-oqNsWzWV.js +0 -72
- package/dashboard/static/assets/ModalRoot-CYua2wP_.js +0 -1
|
@@ -47,6 +47,7 @@ from dataclasses import dataclass
|
|
|
47
47
|
import _cctally_core # `weekly_held_exclusion`: the held-column guard (#836)
|
|
48
48
|
import _lib_accounts # pure stdlib kernel; UNATTRIBUTED sentinel (#341)
|
|
49
49
|
import _lib_rebuild_progress # #997 D9 progress ticks (stdlib only)
|
|
50
|
+
import _lib_stats_content_revision # #1050 identical child-set check
|
|
50
51
|
from _lib_five_hour import latest_five_hour_reading
|
|
51
52
|
from _cctally_core import _command_as_of, eprint, now_utc_iso, open_db, parse_iso_datetime
|
|
52
53
|
|
|
@@ -2034,60 +2035,76 @@ def project_open_block_end(
|
|
|
2034
2035
|
),
|
|
2035
2036
|
)
|
|
2036
2037
|
# Replace-all children, keyed on the window (not block_id) so an orphan
|
|
2037
|
-
# child of an earlier parent rebuild is cleaned up as well.
|
|
2038
|
+
# child of an earlier parent rebuild is cleaned up as well. An identical
|
|
2039
|
+
# stored set is left in place (#1050): a DELETE and re-INSERT of the same
|
|
2040
|
+
# rows would advance the content revision with nothing changed.
|
|
2038
2041
|
block_id = int(current["id"])
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
""
|
|
2047
|
-
|
|
2048
|
-
block_id, five_hour_window_key, model,
|
|
2049
|
-
input_tokens, output_tokens,
|
|
2050
|
-
cache_create_tokens, cache_read_tokens,
|
|
2051
|
-
cost_usd, entry_count, account_key
|
|
2052
|
-
)
|
|
2053
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2054
|
-
""",
|
|
2055
|
-
[
|
|
2056
|
-
(
|
|
2057
|
-
block_id, window_key, model,
|
|
2058
|
-
b["input_tokens"], b["output_tokens"],
|
|
2059
|
-
b["cache_create_tokens"], b["cache_read_tokens"],
|
|
2060
|
-
b["cost_usd"], b["entry_count"], account_key,
|
|
2061
|
-
)
|
|
2062
|
-
for model, b in totals["by_model"].items()
|
|
2063
|
-
],
|
|
2042
|
+
model_rows = _open_block_child_rows(
|
|
2043
|
+
block_id, window_key, account_key, "model", totals.get("by_model"))
|
|
2044
|
+
if not _lib_stats_content_revision.child_set_unchanged(
|
|
2045
|
+
conn, "five_hour_block_models", five_hour_window_key=window_key,
|
|
2046
|
+
account_key=account_key, replacement=model_rows):
|
|
2047
|
+
conn.execute(
|
|
2048
|
+
"DELETE FROM five_hour_block_models "
|
|
2049
|
+
"WHERE five_hour_window_key = ? AND account_key = ?",
|
|
2050
|
+
(window_key, account_key),
|
|
2064
2051
|
)
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
INSERT INTO five_hour_block_projects (
|
|
2074
|
-
block_id, five_hour_window_key, project_path,
|
|
2075
|
-
input_tokens, output_tokens,
|
|
2076
|
-
cache_create_tokens, cache_read_tokens,
|
|
2077
|
-
cost_usd, entry_count, account_key
|
|
2078
|
-
)
|
|
2079
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2080
|
-
""",
|
|
2081
|
-
[
|
|
2082
|
-
(
|
|
2083
|
-
block_id, window_key, project_path,
|
|
2084
|
-
b["input_tokens"], b["output_tokens"],
|
|
2085
|
-
b["cache_create_tokens"], b["cache_read_tokens"],
|
|
2086
|
-
b["cost_usd"], b["entry_count"], account_key,
|
|
2052
|
+
if model_rows:
|
|
2053
|
+
conn.executemany(
|
|
2054
|
+
"""
|
|
2055
|
+
INSERT INTO five_hour_block_models (
|
|
2056
|
+
block_id, five_hour_window_key, model,
|
|
2057
|
+
input_tokens, output_tokens,
|
|
2058
|
+
cache_create_tokens, cache_read_tokens,
|
|
2059
|
+
cost_usd, entry_count, account_key
|
|
2087
2060
|
)
|
|
2088
|
-
|
|
2089
|
-
|
|
2061
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2062
|
+
""",
|
|
2063
|
+
[tuple(row.values()) for row in model_rows],
|
|
2064
|
+
)
|
|
2065
|
+
project_rows = _open_block_child_rows(
|
|
2066
|
+
block_id, window_key, account_key, "project_path",
|
|
2067
|
+
totals.get("by_project"))
|
|
2068
|
+
if not _lib_stats_content_revision.child_set_unchanged(
|
|
2069
|
+
conn, "five_hour_block_projects", five_hour_window_key=window_key,
|
|
2070
|
+
account_key=account_key, replacement=project_rows):
|
|
2071
|
+
conn.execute(
|
|
2072
|
+
"DELETE FROM five_hour_block_projects "
|
|
2073
|
+
"WHERE five_hour_window_key = ? AND account_key = ?",
|
|
2074
|
+
(window_key, account_key),
|
|
2090
2075
|
)
|
|
2076
|
+
if project_rows:
|
|
2077
|
+
conn.executemany(
|
|
2078
|
+
"""
|
|
2079
|
+
INSERT INTO five_hour_block_projects (
|
|
2080
|
+
block_id, five_hour_window_key, project_path,
|
|
2081
|
+
input_tokens, output_tokens,
|
|
2082
|
+
cache_create_tokens, cache_read_tokens,
|
|
2083
|
+
cost_usd, entry_count, account_key
|
|
2084
|
+
)
|
|
2085
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2086
|
+
""",
|
|
2087
|
+
[tuple(row.values()) for row in project_rows],
|
|
2088
|
+
)
|
|
2089
|
+
|
|
2090
|
+
|
|
2091
|
+
def _open_block_child_rows(block_id, window_key, account_key, label,
|
|
2092
|
+
breakdown) -> "list[dict]":
|
|
2093
|
+
"""One open block's child rows from a `_compute_block_totals` breakdown,
|
|
2094
|
+
as column → value mappings in the INSERT's column order."""
|
|
2095
|
+
return [
|
|
2096
|
+
{
|
|
2097
|
+
"block_id": block_id, "five_hour_window_key": window_key,
|
|
2098
|
+
label: name,
|
|
2099
|
+
"input_tokens": b["input_tokens"],
|
|
2100
|
+
"output_tokens": b["output_tokens"],
|
|
2101
|
+
"cache_create_tokens": b["cache_create_tokens"],
|
|
2102
|
+
"cache_read_tokens": b["cache_read_tokens"],
|
|
2103
|
+
"cost_usd": b["cost_usd"], "entry_count": b["entry_count"],
|
|
2104
|
+
"account_key": account_key,
|
|
2105
|
+
}
|
|
2106
|
+
for name, b in (breakdown or {}).items()
|
|
2107
|
+
]
|
|
2091
2108
|
|
|
2092
2109
|
|
|
2093
2110
|
def _materialize_open_candidate(conn, key, acct, start_row) -> int:
|
package/bin/_cctally_forecast.py
CHANGED
|
@@ -135,15 +135,19 @@ def _resolve_forecast_now(as_of: str | None) -> dt.datetime:
|
|
|
135
135
|
|
|
136
136
|
|
|
137
137
|
def _shape_week_samples(rows, *, include_held: bool,
|
|
138
|
-
include_account: bool = False
|
|
138
|
+
include_account: bool = False,
|
|
139
|
+
include_journal_id: bool = False) -> list:
|
|
139
140
|
"""Turn snapshot rows into the sample tuples `_fetch_current_week_snapshots`
|
|
140
141
|
returns. Both of that function's row-fetch legs go through here so the two
|
|
141
142
|
cannot drift on the tuple width.
|
|
142
143
|
|
|
143
144
|
`rows` are `(captured_at_utc, weekly_percent, five_hour_percent,
|
|
144
|
-
weekly_observation_held[, account_key])`. The held flag is
|
|
145
|
-
for a held-inclusive read,
|
|
146
|
-
read
|
|
145
|
+
weekly_observation_held[, account_key][, journal_id])`. The held flag is
|
|
146
|
+
appended only for a held-inclusive read, the account only for an explicit
|
|
147
|
+
identity read, and the row's `journal_id` (#1018 twin spec 5.3: the
|
|
148
|
+
displayed row's originating observation) only when asked for, after
|
|
149
|
+
every other field. Existing consumers keep the historical three- or
|
|
150
|
+
four-field shape.
|
|
147
151
|
"""
|
|
148
152
|
out = []
|
|
149
153
|
for row in rows:
|
|
@@ -156,6 +160,8 @@ def _shape_week_samples(rows, *, include_held: bool,
|
|
|
156
160
|
sample = sample + (int(row[3] or 0),)
|
|
157
161
|
if include_account:
|
|
158
162
|
sample = sample + (row[4],)
|
|
163
|
+
if include_journal_id:
|
|
164
|
+
sample = sample + (row[5 if include_account else 4],)
|
|
159
165
|
out.append(sample)
|
|
160
166
|
return out
|
|
161
167
|
|
|
@@ -183,7 +189,8 @@ def _fetch_current_week_snapshots(conn: sqlite3.Connection, now_utc: dt.datetime
|
|
|
183
189
|
*, account_key: "str | None" = None,
|
|
184
190
|
include_held: bool = False,
|
|
185
191
|
include_account: bool = False,
|
|
186
|
-
include_scope: bool = False
|
|
192
|
+
include_scope: bool = False,
|
|
193
|
+
include_journal_id: bool = False):
|
|
187
194
|
"""Return (week_start_at, week_end_at, list[(captured_at, percent, five_hr)])
|
|
188
195
|
for the subscription week containing `now_utc`, or None if no snapshot
|
|
189
196
|
exists for the current week.
|
|
@@ -241,6 +248,12 @@ def _fetch_current_week_snapshots(conn: sqlite3.Connection, now_utc: dt.datetime
|
|
|
241
248
|
reading the five-hour axis selects its reading under exactly this scope
|
|
242
249
|
rather than from the newest sample, which a retired replica or a
|
|
243
250
|
weekly-only reading leaves without a five-hour value.
|
|
251
|
+
|
|
252
|
+
``include_journal_id`` (#1018 twin spec 5.3) appends each row's
|
|
253
|
+
`journal_id` as the LAST sample field, after the held flag and the
|
|
254
|
+
account, so the Current Week card can resolve its displayed row's
|
|
255
|
+
originating observation. Every other caller keeps its tuple width, and
|
|
256
|
+
the trailing SQL row id ordering is unchanged.
|
|
244
257
|
"""
|
|
245
258
|
# #341: optional account scoping — same predicate on every leg so the whole
|
|
246
259
|
# window resolution stays consistent (a real key or the `unattributed`
|
|
@@ -254,6 +267,8 @@ def _fetch_current_week_snapshots(conn: sqlite3.Connection, now_utc: dt.datetime
|
|
|
254
267
|
# The TUI's multi-account card needs the physical owner of the sample it
|
|
255
268
|
# actually displays. Keep the historical sample shape for every other read.
|
|
256
269
|
_owner_column = ", account_key" if include_account else ""
|
|
270
|
+
if include_journal_id:
|
|
271
|
+
_owner_column += ", journal_id"
|
|
257
272
|
_sample_order = " ASC, id ASC" if include_account else " ASC"
|
|
258
273
|
candidates = conn.execute(
|
|
259
274
|
"SELECT week_start_at, week_end_at, week_start_date, MAX(captured_at_utc) AS latest_cap "
|
|
@@ -310,7 +325,8 @@ def _fetch_current_week_snapshots(conn: sqlite3.Connection, now_utc: dt.datetime
|
|
|
310
325
|
(drow[0],) + _acct_p,
|
|
311
326
|
).fetchall()
|
|
312
327
|
samples = _shape_week_samples(rows, include_held=include_held,
|
|
313
|
-
include_account=include_account
|
|
328
|
+
include_account=include_account,
|
|
329
|
+
include_journal_id=include_journal_id)
|
|
314
330
|
samples = [s for s in samples if s[0] <= now_utc]
|
|
315
331
|
if include_scope:
|
|
316
332
|
return week_start_at, week_end_at, samples, (drow[0], None)
|
|
@@ -343,7 +359,8 @@ def _fetch_current_week_snapshots(conn: sqlite3.Connection, now_utc: dt.datetime
|
|
|
343
359
|
tuple(matching_texts) + (chosen_date,) + _acct_p,
|
|
344
360
|
), by_id=include_account)
|
|
345
361
|
samples = _shape_week_samples(rows, include_held=include_held,
|
|
346
|
-
include_account=include_account
|
|
362
|
+
include_account=include_account,
|
|
363
|
+
include_journal_id=include_journal_id)
|
|
347
364
|
samples = [s for s in samples if s[0] <= now_utc]
|
|
348
365
|
if include_scope:
|
|
349
366
|
return (week_start_at, week_end_at, samples,
|
|
@@ -579,7 +596,8 @@ def _claude_current_cycle_scope(conn, now_utc: dt.datetime, *,
|
|
|
579
596
|
|
|
580
597
|
|
|
581
598
|
def _claude_cycle_scope_samples(conn, scope: ClaudeCycleScope,
|
|
582
|
-
now_utc: dt.datetime
|
|
599
|
+
now_utc: dt.datetime, *,
|
|
600
|
+
include_journal_id: bool = False):
|
|
583
601
|
"""The decorated card's held-inclusive samples for ``scope`` (#836).
|
|
584
602
|
|
|
585
603
|
The shape of ``_fetch_current_week_snapshots(include_held=True,
|
|
@@ -590,8 +608,10 @@ def _claude_cycle_scope_samples(conn, scope: ClaudeCycleScope,
|
|
|
590
608
|
``MAX(captured_at_utc)``. ``week_start_at`` is the NOMINAL start; the
|
|
591
609
|
caller filters from ``scope.effective_start_at``. Samples are filtered to
|
|
592
610
|
``captured_at <= now_utc``. Returns ``None`` when the window cannot be
|
|
593
|
-
resolved.
|
|
611
|
+
resolved. ``include_journal_id`` appends each row's `journal_id` as the
|
|
612
|
+
last sample field (#1018 twin spec 5.3).
|
|
594
613
|
"""
|
|
614
|
+
_jid = ", journal_id" if include_journal_id else ""
|
|
595
615
|
floor_date = (now_utc.astimezone(dt.timezone.utc).date()
|
|
596
616
|
- dt.timedelta(days=_CLAUDE_CYCLE_CANDIDATE_DAYS)).isoformat()
|
|
597
617
|
if scope.week_start_at is None:
|
|
@@ -615,14 +635,15 @@ def _claude_cycle_scope_samples(conn, scope: ClaudeCycleScope,
|
|
|
615
635
|
local_tz).astimezone(dt.timezone.utc)
|
|
616
636
|
rows = conn.execute(
|
|
617
637
|
"SELECT captured_at_utc, weekly_percent, five_hour_percent, "
|
|
618
|
-
" weekly_observation_held, account_key "
|
|
638
|
+
" weekly_observation_held, account_key" + _jid + " "
|
|
619
639
|
"FROM weekly_usage_snapshots "
|
|
620
640
|
"WHERE week_start_date = ? AND account_key = ? "
|
|
621
641
|
"ORDER BY captured_at_utc ASC, id ASC",
|
|
622
642
|
(scope.week_start_date, scope.account_key),
|
|
623
643
|
).fetchall()
|
|
624
644
|
samples = _shape_week_samples(rows, include_held=True,
|
|
625
|
-
include_account=True
|
|
645
|
+
include_account=True,
|
|
646
|
+
include_journal_id=include_journal_id)
|
|
626
647
|
samples = [s for s in samples if s[0] <= now_utc]
|
|
627
648
|
return (week_start_at, week_end_at, samples,
|
|
628
649
|
(scope.week_start_date, None))
|
|
@@ -647,7 +668,7 @@ def _claude_cycle_scope_samples(conn, scope: ClaudeCycleScope,
|
|
|
647
668
|
placeholders = ",".join("?" * len(matching_texts))
|
|
648
669
|
rows = _week_rows_by_capture(conn.execute(
|
|
649
670
|
f"SELECT captured_at_utc, weekly_percent, five_hour_percent, "
|
|
650
|
-
f" weekly_observation_held, account_key, id "
|
|
671
|
+
f" weekly_observation_held, account_key{_jid}, id "
|
|
651
672
|
f"FROM weekly_usage_snapshots "
|
|
652
673
|
f"WHERE (week_start_at IN ({placeholders}) "
|
|
653
674
|
f" OR (week_start_at IS NULL AND week_start_date = ?)) "
|
|
@@ -655,7 +676,8 @@ def _claude_cycle_scope_samples(conn, scope: ClaudeCycleScope,
|
|
|
655
676
|
tuple(matching_texts) + (scope.week_start_date, scope.account_key),
|
|
656
677
|
), by_id=True)
|
|
657
678
|
samples = _shape_week_samples(rows, include_held=True,
|
|
658
|
-
include_account=True
|
|
679
|
+
include_account=True,
|
|
680
|
+
include_journal_id=include_journal_id)
|
|
659
681
|
samples = [s for s in samples if s[0] <= now_utc]
|
|
660
682
|
return (week_start_at, week_end_at, samples,
|
|
661
683
|
(scope.week_start_date, tuple(matching_texts)))
|
package/bin/_cctally_journal.py
CHANGED
|
@@ -55,6 +55,7 @@ import _lib_rebuild_progress
|
|
|
55
55
|
import _lib_record
|
|
56
56
|
import _lib_segment_summary
|
|
57
57
|
import _lib_selector_state
|
|
58
|
+
import _lib_stats_content_revision
|
|
58
59
|
|
|
59
60
|
|
|
60
61
|
# Torn-tail scan window (spec §4.3). A single line must fit inside it — a
|
|
@@ -1974,6 +1975,67 @@ def _read_framed_line(fh, segment, offset, length) -> bytes:
|
|
|
1974
1975
|
return data[lead:lead + length]
|
|
1975
1976
|
|
|
1976
1977
|
|
|
1978
|
+
class _RetainedLineReader:
|
|
1979
|
+
"""`_lib_retained_records.LineReader` for the final rebuild's and
|
|
1980
|
+
derivation completion's streamed records (#1004 D4): the framed read
|
|
1981
|
+
`QuotaLineSource` uses, through the one physical segment read boundary,
|
|
1982
|
+
with one open handle (the current segment's), so an in-order pass opens
|
|
1983
|
+
each segment once. A line that is gone or no longer framed raises
|
|
1984
|
+
`JournalError`, the error those readers already raise for a journal
|
|
1985
|
+
that changed under them."""
|
|
1986
|
+
|
|
1987
|
+
__slots__ = ("_segment", "_fh", "__weakref__")
|
|
1988
|
+
|
|
1989
|
+
def __init__(self) -> None:
|
|
1990
|
+
self._segment = None
|
|
1991
|
+
self._fh = None
|
|
1992
|
+
|
|
1993
|
+
def read(self, segment: str, start: int, length: int) -> bytes:
|
|
1994
|
+
if segment != self._segment:
|
|
1995
|
+
self.close()
|
|
1996
|
+
try:
|
|
1997
|
+
self._fh = _open_segment_for_read(
|
|
1998
|
+
_cctally_core.JOURNAL_DIR / segment)
|
|
1999
|
+
except OSError as exc:
|
|
2000
|
+
raise _retained_line_changed(
|
|
2001
|
+
f"{segment}: {type(exc).__name__}: {exc}") from exc
|
|
2002
|
+
self._segment = segment
|
|
2003
|
+
try:
|
|
2004
|
+
return _read_framed_line(self._fh, segment, start, length)
|
|
2005
|
+
except (QuotaLineSourceError, OSError) as exc:
|
|
2006
|
+
raise _retained_line_changed(str(exc)) from exc
|
|
2007
|
+
|
|
2008
|
+
def close(self) -> None:
|
|
2009
|
+
fh, self._fh = self._fh, None
|
|
2010
|
+
self._segment = None
|
|
2011
|
+
if fh is not None:
|
|
2012
|
+
fh.close()
|
|
2013
|
+
|
|
2014
|
+
|
|
2015
|
+
def _retained_line_changed(message: str) -> Exception:
|
|
2016
|
+
"""The integrity error of a streamed record whose line changed after
|
|
2017
|
+
the read pass (framing or digest): a `JournalError`."""
|
|
2018
|
+
return JournalError(f"journal changed since it was read: {message}")
|
|
2019
|
+
|
|
2020
|
+
|
|
2021
|
+
def _decode_retained_line(line: bytes):
|
|
2022
|
+
"""#1004 D4: a streamed slot's record exactly as the rebuild and
|
|
2023
|
+
completion retained it before: the strict decode, then #997 D15's
|
|
2024
|
+
compaction."""
|
|
2025
|
+
record = _lib_journal.decode_line(line)
|
|
2026
|
+
if record is None:
|
|
2027
|
+
return None
|
|
2028
|
+
return _lib_journal.compact_retained(record)
|
|
2029
|
+
|
|
2030
|
+
|
|
2031
|
+
def _new_retained_records():
|
|
2032
|
+
"""An empty coordinate-backed `RetainedRecords` over this journal."""
|
|
2033
|
+
import _lib_retained_records
|
|
2034
|
+
return _lib_retained_records.RetainedRecords(
|
|
2035
|
+
_lib_retained_records.LineCoordinates(), _RetainedLineReader,
|
|
2036
|
+
_decode_retained_line, error=_retained_line_changed)
|
|
2037
|
+
|
|
2038
|
+
|
|
1977
2039
|
def _quota_line_sizes(quota_raw):
|
|
1978
2040
|
"""Encoded size per observation (line plus newline), for chunk planning."""
|
|
1979
2041
|
if isinstance(quota_raw, QuotaLineSource):
|
|
@@ -4303,7 +4365,7 @@ def _resolve_obs_anchor(resolver, rec: dict) -> "str | None":
|
|
|
4303
4365
|
|
|
4304
4366
|
|
|
4305
4367
|
def _apply_quota_records(
|
|
4306
|
-
cache, records, *, reported_conflicts=None, quiet=False,
|
|
4368
|
+
cache, records, *, reported_conflicts=None, quiet=False, anchors=None,
|
|
4307
4369
|
) -> None:
|
|
4308
4370
|
"""Materialize Codex quota obs into an OPEN cache.db transaction, applying
|
|
4309
4371
|
the §3.5 precedence rule. Callers must apply the batch's file-account
|
|
@@ -4322,7 +4384,13 @@ def _apply_quota_records(
|
|
|
4322
4384
|
reachable from the open-time reconciliation, so `cache-sync` and the
|
|
4323
4385
|
dashboard would emit a line they never emitted before, and acceptance
|
|
4324
4386
|
criterion 10 requires no new stderr on an ordinary command. Its sibling was
|
|
4325
|
-
guarded and this one was not.
|
|
4387
|
+
guarded and this one was not.
|
|
4388
|
+
|
|
4389
|
+
``anchors`` (#975, spec 4.7) replaces the per-call reset-anchor resolver
|
|
4390
|
+
when given. The history provenance drain passes one built from its
|
|
4391
|
+
committed-evidence seed, so a pass seeds each anchor group once instead of
|
|
4392
|
+
once per identity; every other caller omits it and gets a new resolver per
|
|
4393
|
+
call, as before."""
|
|
4326
4394
|
oracle = _CodexAttributionOracle(cache)
|
|
4327
4395
|
# One line per FILE PER BATCH, not per record. A mid-file account switch
|
|
4328
4396
|
# legitimately produces a run of observations whose first-stamp-wins account
|
|
@@ -4342,7 +4410,10 @@ def _apply_quota_records(
|
|
|
4342
4410
|
# NULL anchor, a later walk's `INSERT OR IGNORE` cannot correct it, and that
|
|
4343
4411
|
# window stays fragmented forever.
|
|
4344
4412
|
has_anchor = _cache_has_anchor_column(cache)
|
|
4345
|
-
|
|
4413
|
+
if not has_anchor:
|
|
4414
|
+
anchors = None
|
|
4415
|
+
elif anchors is None:
|
|
4416
|
+
anchors = _codex_anchor_resolver(cache)
|
|
4346
4417
|
insert_sql = _QUOTA_SNAPSHOT_INSERT if has_anchor else _QUOTA_SNAPSHOT_INSERT_LEGACY
|
|
4347
4418
|
upsert_sql = _QUOTA_SNAPSHOT_UPSERT if has_anchor else _QUOTA_SNAPSHOT_UPSERT_LEGACY
|
|
4348
4419
|
# #980 D3: a span per root covering every quota record this call wrote
|
|
@@ -6325,6 +6396,10 @@ def _replace_block_children(
|
|
|
6325
6396
|
convergence replace both sets rather than relying on natural-key
|
|
6326
6397
|
``INSERT OR IGNORE``. This removes stale children and restores missing
|
|
6327
6398
|
children while preserving the parent rowid used by milestone FKs.
|
|
6399
|
+
|
|
6400
|
+
A stored set that already equals the replacement is left in place
|
|
6401
|
+
(#1050): a DELETE and re-INSERT of identical rows would advance the
|
|
6402
|
+
content revision on every repeated fold or convergence.
|
|
6328
6403
|
"""
|
|
6329
6404
|
for payload_key, child_table in _BLOCK_CHILDREN:
|
|
6330
6405
|
if parent_account is None:
|
|
@@ -6333,14 +6408,21 @@ def _replace_block_children(
|
|
|
6333
6408
|
else:
|
|
6334
6409
|
predicate = "account_key = ? AND five_hour_window_key = ?"
|
|
6335
6410
|
params = (parent_account, int(parent_window))
|
|
6336
|
-
|
|
6337
|
-
f"DELETE FROM {child_table} WHERE {predicate}", params
|
|
6338
|
-
)
|
|
6411
|
+
rows = []
|
|
6339
6412
|
for child in children.get(payload_key, []):
|
|
6340
6413
|
cols = dict(child)
|
|
6341
6414
|
cols["block_id"] = int(block_id)
|
|
6342
6415
|
if parent_account is not None:
|
|
6343
6416
|
cols["account_key"] = parent_account
|
|
6417
|
+
rows.append(cols)
|
|
6418
|
+
if _lib_stats_content_revision.child_set_unchanged(
|
|
6419
|
+
conn, child_table, five_hour_window_key=int(parent_window),
|
|
6420
|
+
account_key=parent_account, replacement=rows):
|
|
6421
|
+
continue
|
|
6422
|
+
conn.execute(
|
|
6423
|
+
f"DELETE FROM {child_table} WHERE {predicate}", params
|
|
6424
|
+
)
|
|
6425
|
+
for cols in rows:
|
|
6344
6426
|
_insert_or_ignore(conn, child_table, cols, strict=True)
|
|
6345
6427
|
|
|
6346
6428
|
|
|
@@ -11706,6 +11788,10 @@ _REBUILD_REQUIRED_TABLES = frozenset(
|
|
|
11706
11788
|
"schema_migrations",
|
|
11707
11789
|
"schema_migrations_skipped",
|
|
11708
11790
|
"stats_open_fixups",
|
|
11791
|
+
# #1050 §4.3: the content-revision singleton and its scope table
|
|
11792
|
+
# (`_lib_stats_content_revision.KERNEL_TABLES`).
|
|
11793
|
+
"stats_content_revision",
|
|
11794
|
+
"stats_content_revision_scopes",
|
|
11709
11795
|
"stats_publication_stamp",
|
|
11710
11796
|
"stats_quota_projection_state",
|
|
11711
11797
|
"usage_snapshot_suppressions",
|
|
@@ -11716,11 +11802,54 @@ _REBUILD_REQUIRED_TABLES = frozenset(
|
|
|
11716
11802
|
"weekly_usage_snapshots",
|
|
11717
11803
|
}
|
|
11718
11804
|
)
|
|
11719
|
-
#: #825/#832 Task 4.0. The
|
|
11805
|
+
#: #825/#832 Task 4.0. The suppression trigger. A scratch index without it would
|
|
11720
11806
|
#: publish a store whose live stale-replica removals leave dangling milestone
|
|
11721
11807
|
#: references a rebuild cannot reproduce, so its absence fails validation like a
|
|
11722
|
-
#: missing table does.
|
|
11723
|
-
|
|
11808
|
+
#: missing table does. #1050 §4.3 adds the content-revision kernel's triggers:
|
|
11809
|
+
#: a generation published without them would leave every in-place rewrite
|
|
11810
|
+
#: invisible to the dashboard until the next open reinstalled them.
|
|
11811
|
+
_REBUILD_REQUIRED_TRIGGERS = frozenset(
|
|
11812
|
+
{
|
|
11813
|
+
# `_lib_stats_content_revision.KERNEL_TRIGGERS`
|
|
11814
|
+
"trg_content_revision_budget_milestones_delete",
|
|
11815
|
+
"trg_content_revision_budget_milestones_update",
|
|
11816
|
+
"trg_content_revision_claude_weekly_observation_index_delete",
|
|
11817
|
+
"trg_content_revision_claude_weekly_observation_index_update",
|
|
11818
|
+
"trg_content_revision_five_hour_block_models_delete",
|
|
11819
|
+
"trg_content_revision_five_hour_block_models_insert",
|
|
11820
|
+
"trg_content_revision_five_hour_block_models_update",
|
|
11821
|
+
"trg_content_revision_five_hour_block_projects_delete",
|
|
11822
|
+
"trg_content_revision_five_hour_block_projects_insert",
|
|
11823
|
+
"trg_content_revision_five_hour_block_projects_update",
|
|
11824
|
+
"trg_content_revision_five_hour_blocks_delete",
|
|
11825
|
+
"trg_content_revision_five_hour_blocks_insert",
|
|
11826
|
+
"trg_content_revision_five_hour_blocks_update",
|
|
11827
|
+
"trg_content_revision_five_hour_milestones_delete",
|
|
11828
|
+
"trg_content_revision_five_hour_milestones_insert",
|
|
11829
|
+
"trg_content_revision_five_hour_milestones_update",
|
|
11830
|
+
"trg_content_revision_five_hour_reset_events_delete",
|
|
11831
|
+
"trg_content_revision_five_hour_reset_events_insert",
|
|
11832
|
+
"trg_content_revision_five_hour_reset_events_update",
|
|
11833
|
+
"trg_content_revision_percent_milestones_delete",
|
|
11834
|
+
"trg_content_revision_percent_milestones_insert",
|
|
11835
|
+
"trg_content_revision_percent_milestones_update",
|
|
11836
|
+
"trg_content_revision_project_budget_milestones_delete",
|
|
11837
|
+
"trg_content_revision_project_budget_milestones_update",
|
|
11838
|
+
"trg_content_revision_projected_milestones_delete",
|
|
11839
|
+
"trg_content_revision_projected_milestones_update",
|
|
11840
|
+
"trg_content_revision_week_reset_events_delete",
|
|
11841
|
+
"trg_content_revision_week_reset_events_update",
|
|
11842
|
+
"trg_content_revision_weekly_cost_snapshots_delete",
|
|
11843
|
+
"trg_content_revision_weekly_cost_snapshots_insert",
|
|
11844
|
+
"trg_content_revision_weekly_cost_snapshots_update",
|
|
11845
|
+
"trg_content_revision_weekly_credit_floors_delete",
|
|
11846
|
+
"trg_content_revision_weekly_credit_floors_update",
|
|
11847
|
+
"trg_content_revision_weekly_usage_snapshots_delete",
|
|
11848
|
+
"trg_content_revision_weekly_usage_snapshots_insert",
|
|
11849
|
+
"trg_content_revision_weekly_usage_snapshots_update",
|
|
11850
|
+
"trg_usage_snapshot_suppression",
|
|
11851
|
+
}
|
|
11852
|
+
)
|
|
11724
11853
|
_REBUILD_REQUIRED_INDEXES = frozenset(
|
|
11725
11854
|
{
|
|
11726
11855
|
"idx_budget_milestones_alert_digest",
|
|
@@ -11729,6 +11858,7 @@ _REBUILD_REQUIRED_INDEXES = frozenset(
|
|
|
11729
11858
|
"idx_budget_milestones_journal_id_null",
|
|
11730
11859
|
"idx_claude_oauth_samples_five_hour",
|
|
11731
11860
|
"idx_claude_oauth_samples_weekly",
|
|
11861
|
+
"idx_claude_weekly_observation_index_freshness",
|
|
11732
11862
|
"idx_claude_weekly_observation_index_window",
|
|
11733
11863
|
"idx_cost_week_start_at_time",
|
|
11734
11864
|
"idx_cost_week_time",
|
|
@@ -11747,6 +11877,9 @@ _REBUILD_REQUIRED_INDEXES = frozenset(
|
|
|
11747
11877
|
"idx_five_hour_milestones_block",
|
|
11748
11878
|
"idx_five_hour_milestones_journal_id",
|
|
11749
11879
|
"idx_five_hour_milestones_journal_id_null",
|
|
11880
|
+
# #1050 §4.2: the snapshot trigger's block lookup
|
|
11881
|
+
# (`_lib_stats_content_revision.USAGE_SNAPSHOT_INDEX`).
|
|
11882
|
+
"idx_five_hour_milestones_usage_snapshot_id",
|
|
11750
11883
|
"idx_five_hour_reset_events_journal_id",
|
|
11751
11884
|
"idx_five_hour_reset_events_journal_id_null",
|
|
11752
11885
|
"idx_journal_protocol_violations_batch",
|
|
@@ -11807,8 +11940,12 @@ _REBUILD_REQUIRED_INDEXES = frozenset(
|
|
|
11807
11940
|
# #948 epoch 1023: `outcome_quarantine_envelope` and
|
|
11808
11941
|
# `outcome_quarantine_certificates` dropped; `outcome_quarantine_episodes`,
|
|
11809
11942
|
# `outcome_quarantine_bounds` and `outcome_quarantine_axis_state` added.
|
|
11943
|
+
# #1018 twin epoch 1024: the four enriched columns of
|
|
11944
|
+
# `claude_weekly_observation_index` and its partial freshness index.
|
|
11945
|
+
# #1050 (same epoch, no bump): the content-revision kernel's two tables, its
|
|
11946
|
+
# triggers and `idx_five_hour_milestones_usage_snapshot_id`.
|
|
11810
11947
|
_REBUILD_SCHEMA_FINGERPRINT = (
|
|
11811
|
-
"
|
|
11948
|
+
"1cc565ce0908c80a72884d5e76d815776f57a297fc4d928d0735ba179da4a6f8"
|
|
11812
11949
|
)
|
|
11813
11950
|
|
|
11814
11951
|
|
|
@@ -15038,7 +15175,17 @@ def _rebuild_stats_index_locked(
|
|
|
15038
15175
|
# dropped as soon as it has contributed (#496 S4 §4). #997 D7: the
|
|
15039
15176
|
# quota observations are now retained as COORDINATES (16 bytes each)
|
|
15040
15177
|
# and the cache leg re-reads them one chunk at a time.
|
|
15041
|
-
|
|
15178
|
+
#
|
|
15179
|
+
# #1004 D4: `decoded` is a `RetainedRecords`. A retained record is a
|
|
15180
|
+
# streamed slot (its coordinates and the digest of its line), re-read,
|
|
15181
|
+
# digest-checked and decoded on each access, so every pass sees a
|
|
15182
|
+
# fresh record equal to the one retained before; every other position
|
|
15183
|
+
# keeps its `None` slot. With `MATERIALIZE_FOR_TESTS` set the pass
|
|
15184
|
+
# retains the decoded records and hands on today's list (a test
|
|
15185
|
+
# switch).
|
|
15186
|
+
import _lib_retained_records
|
|
15187
|
+
materialize = _lib_retained_records.MATERIALIZE_FOR_TESTS
|
|
15188
|
+
decoded = _new_retained_records()
|
|
15042
15189
|
quota_raw = QuotaLineSource()
|
|
15043
15190
|
protocol_evidence = []
|
|
15044
15191
|
prior_high_water = None
|
|
@@ -15100,7 +15247,10 @@ def _rebuild_stats_index_locked(
|
|
|
15100
15247
|
# fingerprint that `journal_protocol_resolution` ops reference by
|
|
15101
15248
|
# name, so contributing the wrong count makes an acknowledged
|
|
15102
15249
|
# violation unresolvable (#496 S5b §5.4).
|
|
15103
|
-
|
|
15250
|
+
for _ in range(int(summary.decoded_entry_count)):
|
|
15251
|
+
# An elided line is never read back; its coordinate is a
|
|
15252
|
+
# zero-length stand-in in its segment.
|
|
15253
|
+
decoded.append_retained(None, name, 0, 0)
|
|
15104
15254
|
last_seen.merge(
|
|
15105
15255
|
summary.last_seen_stamped,
|
|
15106
15256
|
summary.last_seen_legacy_claude_at,
|
|
@@ -15162,7 +15312,7 @@ def _rebuild_stats_index_locked(
|
|
|
15162
15312
|
{"t": rec.get("t"), "id": rec.get("id"),
|
|
15163
15313
|
"rev": rec.get("rev", 0)})
|
|
15164
15314
|
summaries.decoded(False)
|
|
15165
|
-
decoded.
|
|
15315
|
+
decoded.append_retained(None, segment, offset, len(raw))
|
|
15166
15316
|
prior_high_water = (segment, offset + len(raw) + 1)
|
|
15167
15317
|
continue
|
|
15168
15318
|
rebuild_quarantine.on_record(
|
|
@@ -15231,7 +15381,12 @@ def _rebuild_stats_index_locked(
|
|
|
15231
15381
|
rec.get("t") in _lib_journal_router.RETAINED_RECORD_TYPES)
|
|
15232
15382
|
summaries.decoded(retained, _is_codex_quota_obs(rec))
|
|
15233
15383
|
if retained:
|
|
15234
|
-
|
|
15384
|
+
if materialize:
|
|
15385
|
+
decoded.append_retained(
|
|
15386
|
+
rec, segment, offset, len(raw))
|
|
15387
|
+
else:
|
|
15388
|
+
decoded.append_streamed(
|
|
15389
|
+
raw, segment, offset, len(raw))
|
|
15235
15390
|
else:
|
|
15236
15391
|
if update_quota_cache and _is_codex_quota_obs(rec):
|
|
15237
15392
|
quota_raw.append(segment, offset, len(raw))
|
|
@@ -15247,7 +15402,7 @@ def _rebuild_stats_index_locked(
|
|
|
15247
15402
|
# and raise on every later rebuild. The selector skips a non-dict
|
|
15248
15403
|
# element, so this costs one pointer and keeps every sequence
|
|
15249
15404
|
# identical to the pre-change numbering (#496 S4; corrects §4.7).
|
|
15250
|
-
decoded.
|
|
15405
|
+
decoded.append_retained(None, segment, offset, len(raw))
|
|
15251
15406
|
prior_high_water = (
|
|
15252
15407
|
segment,
|
|
15253
15408
|
offset + len(raw) + 1,
|
|
@@ -15295,9 +15450,22 @@ def _rebuild_stats_index_locked(
|
|
|
15295
15450
|
# is dropped before the inventory check and publication.
|
|
15296
15451
|
_cctally_oauth_samples.attribute_staged_observations(
|
|
15297
15452
|
conn, cutover_claude)
|
|
15298
|
-
|
|
15299
|
-
|
|
15300
|
-
|
|
15453
|
+
if materialize:
|
|
15454
|
+
# Every slot is a retained object here, so this decodes nothing.
|
|
15455
|
+
listed = list(decoded)
|
|
15456
|
+
decoded.close()
|
|
15457
|
+
decoded = listed
|
|
15458
|
+
for rec in decoded:
|
|
15459
|
+
if rec is not None:
|
|
15460
|
+
_normalize_legacy_account_stamp(rec, cutover_claude)
|
|
15461
|
+
else:
|
|
15462
|
+
# #1004 D4: normalization moves to decode time. The hook is
|
|
15463
|
+
# installed before the first consumer reads a record (the staged
|
|
15464
|
+
# attribution above reads only projected tables), so every pass
|
|
15465
|
+
# below sees each record normalized, as the in-place loop left it.
|
|
15466
|
+
decoded.set_normalizer(
|
|
15467
|
+
lambda rec: _normalize_legacy_account_stamp(
|
|
15468
|
+
rec, cutover_claude))
|
|
15301
15469
|
|
|
15302
15470
|
# Resolve corrections BEFORE either disposable index is mutated. A
|
|
15303
15471
|
# malformed revision, divergent same-revision candidate, or invalid
|