cctally 1.112.0 → 1.114.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 +27 -0
- package/README.md +4 -5
- package/bin/_cctally_cache.py +1683 -485
- package/bin/_cctally_core.py +223 -1
- package/bin/_cctally_dashboard.py +96 -18
- package/bin/_cctally_dashboard_perf.py +128 -9
- package/bin/_cctally_dashboard_sources.py +267 -204
- package/bin/_cctally_db.py +861 -18
- package/bin/_cctally_diagnosis_sources.py +22 -4
- package/bin/_cctally_doctor.py +136 -18
- package/bin/_cctally_five_hour.py +2 -2
- package/bin/_cctally_forecast.py +27 -6
- package/bin/_cctally_journal.py +67 -5
- package/bin/_cctally_milestone_history.py +82 -9
- package/bin/_cctally_percent_breakdown.py +3 -2
- package/bin/_cctally_quota.py +201 -33
- package/bin/_cctally_record.py +14 -0
- package/bin/_cctally_rederive.py +23 -9
- package/bin/_cctally_source_analytics.py +60 -15
- package/bin/_cctally_statusline.py +17 -0
- package/bin/_cctally_store.py +80 -5
- package/bin/_cctally_tui.py +4 -3
- package/bin/_cctally_weekrefs.py +128 -21
- package/bin/_lib_cache_report.py +35 -6
- package/bin/_lib_codex_conversation.py +32 -0
- package/bin/_lib_codex_conversation_query.py +164 -30
- package/bin/_lib_conversation_query.py +95 -56
- package/bin/_lib_conversation_retention.py +1605 -640
- package/bin/_lib_cost_provenance.py +53 -2
- package/bin/_lib_doctor.py +263 -69
- package/bin/_lib_fmt.py +11 -0
- package/bin/_lib_merge_gate.py +469 -51
- package/bin/_lib_perf.py +27 -1
- package/bin/_lib_pricing.py +209 -8
- package/bin/_lib_pricing_check.py +20 -4
- package/bin/_lib_reclaim_planner.py +792 -0
- package/bin/_lib_rederive.py +225 -77
- package/bin/_lib_snapshot_cache.py +21 -0
- package/bin/_lib_sqlite_close.py +81 -0
- package/bin/_lib_subscription_weeks.py +100 -17
- package/bin/_lib_tick_stats.py +217 -17
- package/bin/_lib_wal_checkpoint.py +1077 -0
- package/bin/_lib_write_budget.py +483 -0
- package/bin/_lib_write_io.py +703 -0
- package/bin/cctally +8 -0
- package/dashboard/static/assets/{ConversationsView-CEZ-BUyi.js → ConversationsView-Bc8LCOAZ.js} +1 -1
- package/dashboard/static/assets/{DoctorModal-CE1oH_Zy.js → DoctorModal-D--qVBwe.js} +1 -1
- package/dashboard/static/assets/ModalRoot-DLPgyAM_.js +1 -0
- package/dashboard/static/assets/{ProjectsDrillPanel-B3DZoCeD.js → ProjectsDrillPanel-z53a4v4M.js} +1 -1
- package/dashboard/static/assets/{SourceDetailModal-CHp9EgLP.js → SourceDetailModal-Bw0T8W-W.js} +1 -1
- package/dashboard/static/assets/{UpdateModal-5iKST6jo.js → UpdateModal-D5AKx4Yw.js} +1 -1
- package/dashboard/static/assets/index-BKITk1CT.css +1 -0
- package/dashboard/static/assets/{index-Cm0VTxJC.js → index-BXDOzSng.js} +3 -3
- package/dashboard/static/assets/{outlineNavigation-DkXGL8J6.js → outlineNavigation-JObY7cUY.js} +1 -1
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +6 -1
- package/dashboard/static/assets/ModalRoot-D5Y8IFUG.js +0 -1
- package/dashboard/static/assets/index-Dt4tqf43.css +0 -1
|
@@ -817,22 +817,75 @@ def _load_codex_cycles(stats_conn, root_keys, *, include_orphaned=False,
|
|
|
817
817
|
if not roots:
|
|
818
818
|
return []
|
|
819
819
|
placeholders = ",".join("?" for _ in roots)
|
|
820
|
-
orphan_clause = "" if include_orphaned else "AND orphaned_at IS NULL "
|
|
821
|
-
# `quota_window_blocks.account_key` is NOT NULL DEFAULT 'unattributed'.
|
|
822
|
-
account_clause = "" if account_key is None else "AND account_key=? "
|
|
823
|
-
account_params = () if account_key is None else (account_key,)
|
|
824
820
|
# BEFORE the SQL, per #496 S5b section 4.7.
|
|
825
821
|
assert_projection_readable(stats_conn)
|
|
822
|
+
# #901 (I1/G1, Amendment 1 item 2, 1b and 1c): stream every weekly block
|
|
823
|
+
# in reset order through an epoch-1017 partial expression index. Unpinned,
|
|
824
|
+
# the planner seeks a root-leading index and sorts the whole weekly
|
|
825
|
+
# history through a temp b-tree (measured for every root/account/orphan
|
|
826
|
+
# variant).
|
|
827
|
+
#
|
|
828
|
+
# The ORDER BY names the tie order explicitly: `_canonicalize_codex_
|
|
829
|
+
# cluster` keeps the first maximal member, so rows tied on one reset
|
|
830
|
+
# second must arrive in the order the pre-#901 sorter received them, and
|
|
831
|
+
# that order came from the legacy plan, which differs by root count
|
|
832
|
+
# (Amendment 1c, measured on SQLite 3.53.4). With two or more roots the
|
|
833
|
+
# legacy plan scanned `idx_quota_blocks_active`, whose order includes
|
|
834
|
+
# orphaned_at; with exactly one it seeked the UNIQUE autoindex, which has
|
|
835
|
+
# no orphaned_at (its one exception, an account-scoped live-only read, has
|
|
836
|
+
# a constant orphaned_at, so the two orders agree there). The branch
|
|
837
|
+
# counts the NORMALIZED roots above, so duplicate or empty arguments take
|
|
838
|
+
# the single-root order, exactly as they took the single-root plan.
|
|
839
|
+
#
|
|
840
|
+
# Each branch pins its own index only when that index exists; on an
|
|
841
|
+
# older index the statement stays unpinned with ordinary predicates and
|
|
842
|
+
# the same explicit order. Under the pin, the root, account and orphan
|
|
843
|
+
# predicates carry a unary `+`: SQLite treats a column that a WHERE term
|
|
844
|
+
# fixes but the scan does not seek as satisfying its ORDER BY term and
|
|
845
|
+
# then cannot match that index column, so the later ORDER BY terms fell
|
|
846
|
+
# back to a tie-run sorter ("USE TEMP B-TREE FOR LAST n TERMS OF ORDER
|
|
847
|
+
# BY", measured). Limitation: a unary `+` keeps the operand's value but
|
|
848
|
+
# removes its column affinity. That is equivalent here ONLY because these
|
|
849
|
+
# comparisons are TEXT against TEXT: the root keys are filtered to `str`
|
|
850
|
+
# above, `account_key` is a TEXT key bound as a string, and `IS NULL`
|
|
851
|
+
# involves no affinity. It is not an equivalence for numeric parameters or
|
|
852
|
+
# any other affinity-dependent comparison; a caller binding a non-string
|
|
853
|
+
# account key would need the unpinned predicate.
|
|
854
|
+
if len(roots) == 1:
|
|
855
|
+
order_index = "idx_quota_blocks_weekly_single_root_order"
|
|
856
|
+
order_by = (
|
|
857
|
+
"ORDER BY unixepoch(resets_at_utc) DESC, source_root_key ASC, "
|
|
858
|
+
"account_key ASC, logical_limit_key ASC, observed_slot ASC, "
|
|
859
|
+
"resets_at_utc ASC")
|
|
860
|
+
else:
|
|
861
|
+
order_index = "idx_quota_blocks_weekly_reset_order"
|
|
862
|
+
order_by = (
|
|
863
|
+
"ORDER BY unixepoch(resets_at_utc) DESC, source_root_key ASC, "
|
|
864
|
+
"account_key ASC, orphaned_at ASC, logical_limit_key ASC, "
|
|
865
|
+
"observed_slot ASC, resets_at_utc ASC")
|
|
866
|
+
pinned = stats_conn.execute(
|
|
867
|
+
"SELECT 1 FROM sqlite_master WHERE type='index' AND name=?",
|
|
868
|
+
(order_index,),
|
|
869
|
+
).fetchone() is not None
|
|
870
|
+
order_hint = f"INDEXED BY {order_index} " if pinned else ""
|
|
871
|
+
plus = "+" if pinned else ""
|
|
872
|
+
orphan_clause = (
|
|
873
|
+
"" if include_orphaned else f"AND {plus}orphaned_at IS NULL ")
|
|
874
|
+
# `quota_window_blocks.account_key` is NOT NULL DEFAULT 'unattributed'.
|
|
875
|
+
account_clause = (
|
|
876
|
+
"" if account_key is None else f"AND {plus}account_key=? ")
|
|
877
|
+
account_params = () if account_key is None else (account_key,)
|
|
826
878
|
rows = stats_conn.execute(
|
|
827
879
|
"SELECT source_root_key, logical_limit_key, observed_slot, "
|
|
828
880
|
" window_minutes, limit_id, limit_name, account_key, "
|
|
829
881
|
" resets_at_utc, nominal_start_at_utc, current_percent "
|
|
830
882
|
"FROM quota_window_blocks "
|
|
883
|
+
f"{order_hint}"
|
|
831
884
|
"WHERE source='codex' AND window_minutes=10080 "
|
|
832
885
|
f"{orphan_clause}"
|
|
833
886
|
f"{account_clause}"
|
|
834
|
-
f"AND source_root_key IN ({placeholders}) "
|
|
835
|
-
"
|
|
887
|
+
f"AND {plus}source_root_key IN ({placeholders}) "
|
|
888
|
+
f"{order_by}",
|
|
836
889
|
(*account_params, *roots),
|
|
837
890
|
).fetchall()
|
|
838
891
|
|
|
@@ -952,10 +1005,20 @@ def _codex_five_hour_rows(stats_conn, cyc, *, include_orphaned=False) -> list:
|
|
|
952
1005
|
The widening is one-directional on purpose: an unattributed CYCLE never
|
|
953
1006
|
picks up another account's identified 5h rows, which would violate #341's
|
|
954
1007
|
never-combine rule.
|
|
1008
|
+
|
|
1009
|
+
#901 Amendment 19 T1 (spec I1): the rows are returned whole, so they are
|
|
1010
|
+
ordered here, where ``ORDER BY unixepoch(nominal_start_at_utc)`` sorted
|
|
1011
|
+
them through a temp b-tree. Each physical 5h block surfaces as many
|
|
1012
|
+
jittered rows, so one weekly cycle is not a bound. The key is SQLite's own
|
|
1013
|
+
``unixepoch()`` of each distinct start spelling (a table-free scalar
|
|
1014
|
+
query, memoized), so it is the old key exactly, and the rows keep the
|
|
1015
|
+
caller's row factory. ``list.sort`` is stable over the same scan SQLite's
|
|
1016
|
+
in-memory sorter consumed, so rows tied on one start second keep their
|
|
1017
|
+
order. The WHERE clause admits no NULL start.
|
|
955
1018
|
"""
|
|
956
1019
|
orphan_clause = "" if include_orphaned else "AND orphaned_at IS NULL "
|
|
957
1020
|
assert_projection_readable(stats_conn)
|
|
958
|
-
|
|
1021
|
+
rows = stats_conn.execute(
|
|
959
1022
|
"SELECT source_root_key, logical_limit_key, observed_slot, "
|
|
960
1023
|
" window_minutes, limit_id, limit_name, account_key, "
|
|
961
1024
|
" resets_at_utc, nominal_start_at_utc, current_percent "
|
|
@@ -964,12 +1027,22 @@ def _codex_five_hour_rows(stats_conn, cyc, *, include_orphaned=False) -> list:
|
|
|
964
1027
|
f"{orphan_clause}"
|
|
965
1028
|
"AND source_root_key=? AND account_key IN (?,?) "
|
|
966
1029
|
"AND unixepoch(nominal_start_at_utc) < unixepoch(?) "
|
|
967
|
-
"AND unixepoch(resets_at_utc) > unixepoch(?)
|
|
968
|
-
"ORDER BY unixepoch(nominal_start_at_utc) ASC",
|
|
1030
|
+
"AND unixepoch(resets_at_utc) > unixepoch(?)",
|
|
969
1031
|
(cyc.root, cyc.account_key, UNATTRIBUTED,
|
|
970
1032
|
cyc.end.astimezone(UTC).isoformat(),
|
|
971
1033
|
cyc.start.astimezone(UTC).isoformat()),
|
|
972
1034
|
).fetchall()
|
|
1035
|
+
epochs: dict = {}
|
|
1036
|
+
|
|
1037
|
+
def start_epoch(row):
|
|
1038
|
+
text = row[8]
|
|
1039
|
+
if text not in epochs:
|
|
1040
|
+
epochs[text] = stats_conn.execute(
|
|
1041
|
+
"SELECT unixepoch(?)", (text,)).fetchone()[0]
|
|
1042
|
+
return epochs[text]
|
|
1043
|
+
|
|
1044
|
+
rows.sort(key=start_epoch)
|
|
1045
|
+
return rows
|
|
973
1046
|
|
|
974
1047
|
|
|
975
1048
|
def _codex_five_hour_clusters(stats_conn, cyc, *, include_orphaned=False) -> list:
|
|
@@ -262,7 +262,8 @@ def observation_gap_marginal_cell(marginal_value: Any, *, withheld: bool) -> str
|
|
|
262
262
|
table already says what the code means.
|
|
263
263
|
"""
|
|
264
264
|
if marginal_value is not None:
|
|
265
|
-
|
|
265
|
+
c = _cctally()
|
|
266
|
+
return c._fmt_usd_accounting(float(marginal_value), 6)
|
|
266
267
|
return OBSERVATION_GAP_CAUSE if withheld else "n/a"
|
|
267
268
|
|
|
268
269
|
|
|
@@ -315,7 +316,7 @@ def _render_percent_breakdown_terminal(
|
|
|
315
316
|
rows.append([
|
|
316
317
|
str(idx),
|
|
317
318
|
f"{percent}%",
|
|
318
|
-
|
|
319
|
+
c._fmt_usd_accounting(cumulative, 6),
|
|
319
320
|
c.observation_gap_marginal_cell(
|
|
320
321
|
marginal_value, withheld=(idx - 1) in withheld),
|
|
321
322
|
f"{float(five_hour_value):.0f}%" if five_hour_value is not None else "n/a",
|
package/bin/_cctally_quota.py
CHANGED
|
@@ -378,7 +378,14 @@ PROJECTION_DYNAMIC_READ_SITES: "dict[str, int]" = {
|
|
|
378
378
|
# so no `PROJECTION_DYNAMIC_READ_ACTIONS` classification applies.
|
|
379
379
|
"_lib_codex_conversation_query.py": 1,
|
|
380
380
|
"_lib_conversation_query.py": 1,
|
|
381
|
-
|
|
381
|
+
# Four, not two, since #901's paced retention (`f2077468a`, spec §5.4):
|
|
382
|
+
# `group_row_count` counts the rows a deletion's reservation charges with
|
|
383
|
+
# `SELECT COUNT(*) FROM {table}` in its two shapes (by identity key, and by
|
|
384
|
+
# source path for a NULL-identity group). `{table}` comes from
|
|
385
|
+
# `_GROUP_TABLES`, a hardcoded pair (`conversation_messages`,
|
|
386
|
+
# `codex_conversation_events`); neither is a quota projection table, so no
|
|
387
|
+
# `PROJECTION_DYNAMIC_READ_ACTIONS` classification applies.
|
|
388
|
+
"_lib_conversation_retention.py": 4,
|
|
382
389
|
# Five: `_target_has_cursor_gap` selects one of the two accounting source
|
|
383
390
|
# tables, `_conversation_source_paths` and `_conversation_target_risk`
|
|
384
391
|
# select one of the two transcript source tables, and #769 S6's
|
|
@@ -1307,6 +1314,107 @@ def _codex_quota_required_text_present(values: Iterable[object]) -> bool:
|
|
|
1307
1314
|
value is None or not str(value).strip() for value in values)
|
|
1308
1315
|
|
|
1309
1316
|
|
|
1317
|
+
# ── #901 W1: reading the maintained raw-partition maxima (spec §5.1, T1) ─────
|
|
1318
|
+
|
|
1319
|
+
#: The objects the latest-only read and the general loader's plan pins depend
|
|
1320
|
+
#: on, probed together once per load.
|
|
1321
|
+
_CODEX_QUOTA_READ_OBJECTS = (
|
|
1322
|
+
"codex_quota_partition_latest", "idx_qws_partition_capture",
|
|
1323
|
+
"trg_qws_latest_ins", "trg_qws_latest_del", "trg_qws_latest_upd",
|
|
1324
|
+
"idx_qws_codex_load_order", "idx_qws_physical_group",
|
|
1325
|
+
)
|
|
1326
|
+
_CODEX_QUOTA_SUMMARY_OBJECTS = frozenset(_CODEX_QUOTA_READ_OBJECTS[:5])
|
|
1327
|
+
|
|
1328
|
+
#: The latest path's SELECT list over the summary join, aliased so every name
|
|
1329
|
+
#: the interpretation loop reads resolves exactly as it does on the window path.
|
|
1330
|
+
_CODEX_QUOTA_LATEST_SELECT = ", ".join(
|
|
1331
|
+
f"q.{column} AS {column}" for column in (
|
|
1332
|
+
"source", "source_root_key", "source_path", "line_offset",
|
|
1333
|
+
"captured_at_utc", "observed_slot", "logical_limit_key", "limit_id",
|
|
1334
|
+
"limit_name", "window_minutes", "used_percent", "resets_at_utc",
|
|
1335
|
+
"plan_type", "individual_limit_json", "reached_type",
|
|
1336
|
+
"observed_model", "account_key", "canonical_resets_at_utc",
|
|
1337
|
+
))
|
|
1338
|
+
|
|
1339
|
+
|
|
1340
|
+
def _codex_quota_read_objects(conn: sqlite3.Connection) -> "frozenset[str]":
|
|
1341
|
+
"""Which of the read-path objects this store carries (one probe)."""
|
|
1342
|
+
placeholders = ",".join("?" for _ in _CODEX_QUOTA_READ_OBJECTS)
|
|
1343
|
+
return frozenset(
|
|
1344
|
+
str(row[0]) for row in conn.execute(
|
|
1345
|
+
f"SELECT name FROM sqlite_master WHERE name IN ({placeholders})",
|
|
1346
|
+
_CODEX_QUOTA_READ_OBJECTS)
|
|
1347
|
+
)
|
|
1348
|
+
|
|
1349
|
+
|
|
1350
|
+
def _codex_quota_latest_summary_ready(read_objects: "frozenset[str]") -> bool:
|
|
1351
|
+
"""True when the summary and everything that maintains it are present.
|
|
1352
|
+
|
|
1353
|
+
The five objects are created and bootstrapped in one transaction
|
|
1354
|
+
(``_cctally_db._apply_codex_quota_latest_summary``), and SQLite drops a
|
|
1355
|
+
table's triggers and indexes only together with the table, so their joint
|
|
1356
|
+
presence means the summary has been maintained since its bootstrap.
|
|
1357
|
+
Anything less — a read-only open of a store migration 047 has not reached,
|
|
1358
|
+
or a legacy shape — keeps the window query, which selects the same rows.
|
|
1359
|
+
"""
|
|
1360
|
+
return _CODEX_QUOTA_SUMMARY_OBJECTS <= read_objects
|
|
1361
|
+
|
|
1362
|
+
|
|
1363
|
+
def _sqlite_order_value(value) -> tuple:
|
|
1364
|
+
"""Rank a stored value the way SQLite's BINARY ORDER BY ranks it.
|
|
1365
|
+
|
|
1366
|
+
NULL first, then numbers (integers and reals compared numerically), then
|
|
1367
|
+
text in code-point order (which is UTF-8 byte order), then blobs.
|
|
1368
|
+
"""
|
|
1369
|
+
if value is None:
|
|
1370
|
+
return (0, 0)
|
|
1371
|
+
if isinstance(value, (int, float)):
|
|
1372
|
+
return (1, value)
|
|
1373
|
+
if isinstance(value, str):
|
|
1374
|
+
return (2, value)
|
|
1375
|
+
return (3, bytes(value))
|
|
1376
|
+
|
|
1377
|
+
|
|
1378
|
+
def _codex_quota_latest_rows(
|
|
1379
|
+
conn: sqlite3.Connection, requested: "set[str] | None",
|
|
1380
|
+
) -> "list[sqlite3.Row]":
|
|
1381
|
+
"""Every valid physical row tied at its raw partition's maximum second.
|
|
1382
|
+
|
|
1383
|
+
One scan of the summary (one row per retained raw partition) and one
|
|
1384
|
+
``idx_qws_partition_capture`` seek per partition. ``CROSS JOIN`` fixes the
|
|
1385
|
+
summary as the outer loop and ``INDEXED BY`` the inner seek, so the plan
|
|
1386
|
+
cannot drift into walking the snapshots. The window path's ORDER BY
|
|
1387
|
+
(``source_root_key, captured_at_utc, resets_at_utc, source_path,
|
|
1388
|
+
line_offset``) is reproduced here in Python over this bounded set, so the
|
|
1389
|
+
rows reach the interpretation loop in exactly the order they did before.
|
|
1390
|
+
"""
|
|
1391
|
+
import _cctally_db
|
|
1392
|
+
|
|
1393
|
+
on = " AND ".join(
|
|
1394
|
+
f"{expr} = s.{key}" for key, expr in zip(
|
|
1395
|
+
_cctally_db._CODEX_QUOTA_LATEST_KEY_COLUMNS,
|
|
1396
|
+
_cctally_db._codex_quota_latest_key_exprs("q.")))
|
|
1397
|
+
sql = (
|
|
1398
|
+
f"SELECT {_CODEX_QUOTA_LATEST_SELECT}"
|
|
1399
|
+
" FROM codex_quota_partition_latest AS s"
|
|
1400
|
+
" CROSS JOIN quota_window_snapshots AS q"
|
|
1401
|
+
" INDEXED BY idx_qws_partition_capture"
|
|
1402
|
+
f" ON {on} AND unixepoch(q.captured_at_utc) = s.latest_capture_epoch"
|
|
1403
|
+
f" WHERE {_cctally_db._codex_quota_latest_validity_sql('q.')}"
|
|
1404
|
+
)
|
|
1405
|
+
params: "list[object]" = []
|
|
1406
|
+
if requested is not None:
|
|
1407
|
+
sql += (" AND s.k_source_root_key IN ("
|
|
1408
|
+
+ ",".join("quote(?)" for _ in requested) + ")")
|
|
1409
|
+
params.extend(sorted(requested))
|
|
1410
|
+
rows = conn.execute(sql, params).fetchall()
|
|
1411
|
+
rows.sort(key=lambda row: tuple(
|
|
1412
|
+
_sqlite_order_value(row[name]) for name in (
|
|
1413
|
+
"source_root_key", "captured_at_utc", "resets_at_utc",
|
|
1414
|
+
"source_path", "line_offset")))
|
|
1415
|
+
return rows
|
|
1416
|
+
|
|
1417
|
+
|
|
1310
1418
|
# --------------------------------------------------------------------------
|
|
1311
1419
|
# #500 §6.4 — the fold-time operator attribution overlay
|
|
1312
1420
|
# --------------------------------------------------------------------------
|
|
@@ -1449,6 +1557,56 @@ def _codex_attribution_stored_limit_keys(logical_limit_key: str) -> "list[str]":
|
|
|
1449
1557
|
)
|
|
1450
1558
|
|
|
1451
1559
|
|
|
1560
|
+
def _codex_shard_axis_filter(axes: tuple) -> "tuple[str, tuple]":
|
|
1561
|
+
"""The WHERE clause and parameters selecting one attribution axis shard.
|
|
1562
|
+
|
|
1563
|
+
Rows are STORED under whichever length and key spelling the provider sent,
|
|
1564
|
+
so the filter enumerates every equivalent rather than snapping in SQL —
|
|
1565
|
+
which it could not do and still seek the index.
|
|
1566
|
+
"""
|
|
1567
|
+
root_key, limit_key, slot, window_minutes = axes
|
|
1568
|
+
limit_keys = _codex_attribution_stored_limit_keys(limit_key)
|
|
1569
|
+
minutes = sorted({
|
|
1570
|
+
int(value)
|
|
1571
|
+
for value in codex_snap_equivalent_window_minutes(window_minutes)
|
|
1572
|
+
})
|
|
1573
|
+
axis_clause = (
|
|
1574
|
+
" WHERE source='codex' AND source_root_key = ?"
|
|
1575
|
+
" AND logical_limit_key IN ("
|
|
1576
|
+
+ ",".join("?" * len(limit_keys)) + ")"
|
|
1577
|
+
" AND observed_slot = ?"
|
|
1578
|
+
" AND window_minutes IN (" + ",".join("?" * len(minutes)) + ")"
|
|
1579
|
+
)
|
|
1580
|
+
return axis_clause, (root_key, *limit_keys, slot, *minutes)
|
|
1581
|
+
|
|
1582
|
+
|
|
1583
|
+
def _codex_witness_anchors(
|
|
1584
|
+
conn: sqlite3.Connection, anchor_expr: str, axis_clause: str,
|
|
1585
|
+
axis_params: tuple, witnesses: "Sequence[str]",
|
|
1586
|
+
) -> "set[str]":
|
|
1587
|
+
"""Pass 1: the canonical anchors of the groups a witness reset reaches.
|
|
1588
|
+
|
|
1589
|
+
#901 Amendment 19 T1 (spec I1): the set is built in Python from the
|
|
1590
|
+
streamed rows. ``SELECT DISTINCT`` sorted the anchors of every capture
|
|
1591
|
+
carrying a witness reset through a temp b-tree — a window group's whole
|
|
1592
|
+
activity — and one attribution axis shard is not a bound. The result is
|
|
1593
|
+
the same set; the Python set holds one entry per distinct anchor.
|
|
1594
|
+
"""
|
|
1595
|
+
pass_one = (
|
|
1596
|
+
f"SELECT {anchor_expr} AS anchor"
|
|
1597
|
+
" FROM quota_window_snapshots"
|
|
1598
|
+
+ axis_clause
|
|
1599
|
+
+ " AND unixepoch(resets_at_utc) IN ("
|
|
1600
|
+
+ ",".join("unixepoch(?)" for _ in witnesses) + ")"
|
|
1601
|
+
)
|
|
1602
|
+
anchors: "set[str]" = set()
|
|
1603
|
+
for row in conn.execute(pass_one, (*axis_params, *witnesses)):
|
|
1604
|
+
if row[0] is None or not str(row[0]).strip():
|
|
1605
|
+
continue
|
|
1606
|
+
anchors.add(str(row[0]))
|
|
1607
|
+
return anchors
|
|
1608
|
+
|
|
1609
|
+
|
|
1452
1610
|
def _load_codex_window_group_evidence(
|
|
1453
1611
|
conn: sqlite3.Connection, assertions: "Sequence[Mapping[str, object]]",
|
|
1454
1612
|
) -> "tuple[_wa.WindowGroup, ...]":
|
|
@@ -1557,36 +1715,10 @@ def _load_codex_window_group_evidence(
|
|
|
1557
1715
|
witness_texts: "dict[str, str]" = {}
|
|
1558
1716
|
model_scoped: "dict[tuple, bool]" = {}
|
|
1559
1717
|
for axes, shard_witnesses in sorted(shards.items()):
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
limit_keys = _codex_attribution_stored_limit_keys(limit_key)
|
|
1565
|
-
minutes = sorted({
|
|
1566
|
-
int(value)
|
|
1567
|
-
for value in codex_snap_equivalent_window_minutes(window_minutes)
|
|
1568
|
-
})
|
|
1569
|
-
witnesses = sorted(shard_witnesses)
|
|
1570
|
-
axis_clause = (
|
|
1571
|
-
" WHERE source='codex' AND source_root_key = ?"
|
|
1572
|
-
" AND logical_limit_key IN ("
|
|
1573
|
-
+ ",".join("?" * len(limit_keys)) + ")"
|
|
1574
|
-
" AND observed_slot = ?"
|
|
1575
|
-
" AND window_minutes IN (" + ",".join("?" * len(minutes)) + ")"
|
|
1576
|
-
)
|
|
1577
|
-
axis_params = (root_key, *limit_keys, slot, *minutes)
|
|
1578
|
-
anchors: "set[str]" = set()
|
|
1579
|
-
pass_one = (
|
|
1580
|
-
f"SELECT DISTINCT {anchor_expr} AS anchor"
|
|
1581
|
-
" FROM quota_window_snapshots"
|
|
1582
|
-
+ axis_clause
|
|
1583
|
-
+ " AND unixepoch(resets_at_utc) IN ("
|
|
1584
|
-
+ ",".join("unixepoch(?)" for _ in witnesses) + ")"
|
|
1585
|
-
)
|
|
1586
|
-
for row in conn.execute(pass_one, (*axis_params, *witnesses)):
|
|
1587
|
-
if row[0] is None or not str(row[0]).strip():
|
|
1588
|
-
continue
|
|
1589
|
-
anchors.add(str(row[0]))
|
|
1718
|
+
axis_clause, axis_params = _codex_shard_axis_filter(axes)
|
|
1719
|
+
anchors = _codex_witness_anchors(
|
|
1720
|
+
conn, anchor_expr, axis_clause, axis_params,
|
|
1721
|
+
sorted(shard_witnesses))
|
|
1590
1722
|
if not anchors:
|
|
1591
1723
|
continue
|
|
1592
1724
|
# ONE SHARD PER GROUP, for the reason the loader states at its own group
|
|
@@ -1983,6 +2115,16 @@ def load_codex_quota_observations(
|
|
|
1983
2115
|
and therefore the same verdict as the full load, on a store where the full
|
|
1984
2116
|
load meant interpreting 266,337 rows to answer a question about 608 of them.
|
|
1985
2117
|
|
|
2118
|
+
#901 W1 (spec §5.1, T1): on a store carrying cache migration 047's objects
|
|
2119
|
+
the candidate rows come from ``codex_quota_partition_latest``, a
|
|
2120
|
+
trigger-maintained table holding every raw partition's maximum whole-second
|
|
2121
|
+
capture, joined back to every physical row tied at that second through
|
|
2122
|
+
``idx_qws_partition_capture``. The window query described below stays as the
|
|
2123
|
+
fallback for a store without those objects (a read-only open before the
|
|
2124
|
+
migration ran, or a legacy shape); both select exactly the same rows, and
|
|
2125
|
+
tests/test_901_partition_latest_maintenance.py holds the summary to the
|
|
2126
|
+
frozen pre-#901 window query after every writer class.
|
|
2127
|
+
|
|
1986
2128
|
SQL narrows to the rows that can possibly win, and Python still decides. The
|
|
1987
2129
|
partition is the RAW spelling of every column the interpretation reads
|
|
1988
2130
|
(root, limit key, slot, window minutes, limit id and name, observed model,
|
|
@@ -2106,6 +2248,8 @@ def load_codex_quota_observations(
|
|
|
2106
2248
|
"canonical_resets_at_utc" if has_anchor
|
|
2107
2249
|
else "NULL AS canonical_resets_at_utc"
|
|
2108
2250
|
)
|
|
2251
|
+
# #901: one probe for the summary and the two plan-pin indexes.
|
|
2252
|
+
read_objects = _codex_quota_read_objects(conn)
|
|
2109
2253
|
# Public #5: the model is read from THIS table alone. There used to be a
|
|
2110
2254
|
# COALESCE onto the nearest preceding `codex_session_entries.model` at
|
|
2111
2255
|
# or before the snapshot's byte offset, for rows written before the
|
|
@@ -2129,9 +2273,26 @@ def load_codex_quota_observations(
|
|
|
2129
2273
|
model_expr=model_expr, account_expr=account_expr,
|
|
2130
2274
|
anchor_expr=anchor_expr,
|
|
2131
2275
|
)
|
|
2276
|
+
# #901 W3 (spec §5.1, T1/T3): pin the plan. Unpinned, the planner sorts
|
|
2277
|
+
# an unbounded or multi-root load through a temp b-tree via the
|
|
2278
|
+
# `source` autoindex, and the load-order index hijacks the projector's
|
|
2279
|
+
# per-group shards into a whole-root walk (both measured). A bounded
|
|
2280
|
+
# `max_rows` read keeps its plan: its sorter holds at most `max_rows`
|
|
2281
|
+
# rows. Each pin is applied only when its index exists, so a store
|
|
2282
|
+
# without them keeps today's SQL.
|
|
2283
|
+
if group_filter is not None:
|
|
2284
|
+
table_hint = (
|
|
2285
|
+
" INDEXED BY idx_qws_physical_group"
|
|
2286
|
+
if "idx_qws_physical_group" in read_objects else "")
|
|
2287
|
+
elif physical_signatures is None and max_rows is not None:
|
|
2288
|
+
table_hint = ""
|
|
2289
|
+
else:
|
|
2290
|
+
table_hint = (
|
|
2291
|
+
" INDEXED BY idx_qws_codex_load_order"
|
|
2292
|
+
if "idx_qws_codex_load_order" in read_objects else "")
|
|
2132
2293
|
sql = f"""
|
|
2133
2294
|
SELECT {select_list}
|
|
2134
|
-
FROM quota_window_snapshots
|
|
2295
|
+
FROM quota_window_snapshots{table_hint}
|
|
2135
2296
|
WHERE source='codex' AND source_root_key IS NOT NULL
|
|
2136
2297
|
"""
|
|
2137
2298
|
params: list[object] = []
|
|
@@ -2271,9 +2432,16 @@ def load_codex_quota_observations(
|
|
|
2271
2432
|
shard_sql = f"{head}{clause}{order_by}"
|
|
2272
2433
|
for group in group_filter:
|
|
2273
2434
|
shards.append((shard_sql, (*params, *group)))
|
|
2435
|
+
if latest_per_identity and _codex_quota_latest_summary_ready(
|
|
2436
|
+
read_objects):
|
|
2437
|
+
# #901 W1 (spec §5.1, T1): the maintained summary yields exactly
|
|
2438
|
+
# the rows the window query above selects, without sorting history.
|
|
2439
|
+
row_source = _codex_quota_latest_rows(conn, requested)
|
|
2440
|
+
else:
|
|
2441
|
+
row_source = _iter_shard_rows(conn, shards)
|
|
2274
2442
|
result: list[QuotaObservation] = []
|
|
2275
2443
|
signature_tuples: dict[str, dict[str, list[tuple[object, ...]]]] = {}
|
|
2276
|
-
for row in
|
|
2444
|
+
for row in row_source:
|
|
2277
2445
|
if not _codex_quota_required_text_present(
|
|
2278
2446
|
row[name] for name in _CODEX_QUOTA_REQUIRED_TEXT
|
|
2279
2447
|
):
|
package/bin/_cctally_record.py
CHANGED
|
@@ -6596,6 +6596,16 @@ def _record_dashboard_activity(provider: str, transcript_path: str) -> None:
|
|
|
6596
6596
|
pass
|
|
6597
6597
|
|
|
6598
6598
|
|
|
6599
|
+
def _finalize_wal_checkpoints() -> None:
|
|
6600
|
+
"""The W9 finalizer before a detached worker's ``os._exit`` (#901 Q14)."""
|
|
6601
|
+
try:
|
|
6602
|
+
import _lib_wal_checkpoint
|
|
6603
|
+
|
|
6604
|
+
_lib_wal_checkpoint.finalize()
|
|
6605
|
+
except Exception: # noqa: BLE001 — a hook must exit 0 whatever happens
|
|
6606
|
+
pass
|
|
6607
|
+
|
|
6608
|
+
|
|
6599
6609
|
def cmd_hook_tick(args: argparse.Namespace) -> int:
|
|
6600
6610
|
"""Per-fire hook runtime (Section 3 of onboarding spec).
|
|
6601
6611
|
|
|
@@ -6831,6 +6841,10 @@ def cmd_hook_tick(args: argparse.Namespace) -> int:
|
|
|
6831
6841
|
# Forked child: skip Python's atexit / argparse / cleanup paths
|
|
6832
6842
|
# (they may try to flush already-redirected stdio handles).
|
|
6833
6843
|
if forked:
|
|
6844
|
+
# #901 W9 (Q14): `os._exit` skips `atexit`, so the checkpoint
|
|
6845
|
+
# policy's finalizer runs here: one PASSIVE per armed store, then
|
|
6846
|
+
# the keepers close (the last close drains the WAL).
|
|
6847
|
+
_finalize_wal_checkpoints()
|
|
6834
6848
|
os._exit(0)
|
|
6835
6849
|
return 0
|
|
6836
6850
|
# --explain mapping (Section 3 of spec)
|
package/bin/_cctally_rederive.py
CHANGED
|
@@ -90,6 +90,13 @@ def _fingerprint(value) -> str:
|
|
|
90
90
|
return "sha256:" + hashlib.sha256(_canonical_bytes(value)).hexdigest()
|
|
91
91
|
|
|
92
92
|
|
|
93
|
+
#: #874: the only tables the cache contract names. Inspecting any other table
|
|
94
|
+
#: is not the planner's business, and on a virtual table whose module this
|
|
95
|
+
#: SQLite build lacks (a legacy FTS5 index on a build without FTS5) even
|
|
96
|
+
#: `PRAGMA table_info` raises before the contract could type the gap.
|
|
97
|
+
_CONTRACT_TABLES = ("session_entries", "session_files", "codex_session_entries")
|
|
98
|
+
|
|
99
|
+
|
|
93
100
|
def _cache_contract(cache_conn: sqlite3.Connection) -> dict[str, set[str]]:
|
|
94
101
|
names = {
|
|
95
102
|
row[0] for row in cache_conn.execute(
|
|
@@ -99,12 +106,18 @@ def _cache_contract(cache_conn: sqlite3.Connection) -> dict[str, set[str]]:
|
|
|
99
106
|
name: {
|
|
100
107
|
row[1] for row in cache_conn.execute(f"PRAGMA table_info({name})")
|
|
101
108
|
}
|
|
102
|
-
for name in
|
|
109
|
+
for name in _CONTRACT_TABLES
|
|
110
|
+
if name in names
|
|
103
111
|
}
|
|
104
112
|
|
|
105
113
|
|
|
106
114
|
def _cache_fingerprint(cache_conn: sqlite3.Connection, *, include_codex=False) -> str:
|
|
107
|
-
"""Hash every cost-bearing Claude cache row and its project metadata.
|
|
115
|
+
"""Hash every cost-bearing Claude cache row and its project metadata.
|
|
116
|
+
|
|
117
|
+
The caller must have validated the cache contract first, including
|
|
118
|
+
``validate_codex_cache_contract`` when ``include_codex`` is set (#874):
|
|
119
|
+
a missing column here is a raw sqlite error, not a typed data gap.
|
|
120
|
+
"""
|
|
108
121
|
entry_columns = (
|
|
109
122
|
"source_path", "line_offset", "timestamp_utc", "model", "input_tokens",
|
|
110
123
|
"output_tokens", "cache_create_tokens", "cache_read_tokens",
|
|
@@ -123,10 +136,6 @@ def _cache_fingerprint(cache_conn: sqlite3.Connection, *, include_codex=False) -
|
|
|
123
136
|
]
|
|
124
137
|
basis = {"sessionEntries": entries, "sessionFiles": files}
|
|
125
138
|
if include_codex:
|
|
126
|
-
if "codex_session_entries" not in _cache_contract(cache_conn):
|
|
127
|
-
raise _lib_rederive.RederiveDataGap(
|
|
128
|
-
"missing cache.db table codex_session_entries"
|
|
129
|
-
)
|
|
130
139
|
basis["codexEntries"] = [
|
|
131
140
|
list(row) for row in cache_conn.execute(
|
|
132
141
|
"SELECT source_path,line_offset,timestamp_utc,session_id,model,"
|
|
@@ -1088,15 +1097,20 @@ def plan_claude_usage(
|
|
|
1088
1097
|
+ ",".join(report.unclassified_evt_kinds)
|
|
1089
1098
|
+ " op=" + ",".join(report.unclassified_op_kinds)
|
|
1090
1099
|
)
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
_validate_cache_rows(cache_conn, raw_records)
|
|
1100
|
+
tables = _cache_contract(cache_conn)
|
|
1101
|
+
_lib_rederive.validate_claude_cache_contract(tables)
|
|
1094
1102
|
codex_pricing_records = any(
|
|
1095
1103
|
(record.get("payload") or {}).get("vendor") == "codex"
|
|
1096
1104
|
and (record.get("payload") or {}).get("kind") == "budget"
|
|
1097
1105
|
and (record.get("payload") or {}).get("_pricing")
|
|
1098
1106
|
for record in records
|
|
1099
1107
|
)
|
|
1108
|
+
if codex_pricing_records:
|
|
1109
|
+
# #874: the Codex fingerprint and re-pricing read the Codex cache only
|
|
1110
|
+
# here, so its contract is required only here, and before any reader.
|
|
1111
|
+
_lib_rederive.validate_codex_cache_contract(tables)
|
|
1112
|
+
raw_records = _rederivable_raw_records(records)
|
|
1113
|
+
_validate_cache_rows(cache_conn, raw_records)
|
|
1100
1114
|
cache_fingerprint = _cache_fingerprint(
|
|
1101
1115
|
cache_conn, include_codex=codex_pricing_records,
|
|
1102
1116
|
)
|
|
@@ -153,18 +153,45 @@ _QUALIFIED_CODEX_ENTRIES_SQL = """
|
|
|
153
153
|
|
|
154
154
|
|
|
155
155
|
def _qualified_codex_path_entries_sql(identity_count: int) -> str:
|
|
156
|
-
"""Return the qualified read constrained to physical cache identities.
|
|
156
|
+
"""Return the qualified read constrained to physical cache identities.
|
|
157
|
+
|
|
158
|
+
Unordered: ``_load_qualified_codex_rows`` orders its rows (#901
|
|
159
|
+
Amendment 19 T1). Seeking ``idx_codex_entries_root_path`` cannot also
|
|
160
|
+
deliver the window read's order, so the old ``ORDER BY`` sorted every
|
|
161
|
+
entry of the dirty rollouts through a temp b-tree, and one rollout file is
|
|
162
|
+
not a bound.
|
|
163
|
+
"""
|
|
157
164
|
predicates = " OR ".join(
|
|
158
165
|
"(entries.source_root_key = ? AND entries.source_path = ?)"
|
|
159
166
|
for _ in range(identity_count)
|
|
160
167
|
)
|
|
161
|
-
|
|
168
|
+
head, order_by, _tail = _QUALIFIED_CODEX_ENTRIES_SQL.partition(
|
|
169
|
+
" ORDER BY entries.timestamp_utc ASC")
|
|
170
|
+
if not order_by:
|
|
171
|
+
raise RuntimeError(
|
|
172
|
+
"_QUALIFIED_CODEX_ENTRIES_SQL no longer carries its ORDER BY")
|
|
173
|
+
return head.replace(
|
|
162
174
|
"INDEXED BY idx_codex_entries_ts_root_conversation",
|
|
163
175
|
"INDEXED BY idx_codex_entries_root_path",
|
|
164
|
-
)
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
176
|
+
) + f" AND ({predicates})\n"
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _qualified_codex_row_order(row) -> tuple:
|
|
180
|
+
"""The window read's ``ORDER BY entries.timestamp_utc,
|
|
181
|
+
entries.source_root_key, entries.conversation_key, entries.id`` as a Python
|
|
182
|
+
key: NULL first, text by UTF-8 bytes (SQLite's BINARY collation). ``id``
|
|
183
|
+
is unique, so the order is total."""
|
|
184
|
+
def collate(value):
|
|
185
|
+
if value is None:
|
|
186
|
+
return (0, b"")
|
|
187
|
+
if isinstance(value, str):
|
|
188
|
+
return (2, value.encode("utf-8"))
|
|
189
|
+
if isinstance(value, (bytes, bytearray, memoryview)):
|
|
190
|
+
return (3, bytes(value))
|
|
191
|
+
return (1, value)
|
|
192
|
+
|
|
193
|
+
return (collate(row[0]), collate(row[3]), collate(row[4]),
|
|
194
|
+
collate(row[16]))
|
|
168
195
|
|
|
169
196
|
|
|
170
197
|
#: The thread leg of the decode inventory (§4.3 item 1). ``{threads}`` is the
|
|
@@ -1184,6 +1211,32 @@ def _require_joined_metadata(row: sqlite3.Row) -> tuple[str, str]:
|
|
|
1184
1211
|
return root_key, conversation_key
|
|
1185
1212
|
|
|
1186
1213
|
|
|
1214
|
+
def _load_qualified_codex_rows(
|
|
1215
|
+
conn: sqlite3.Connection, start: dt.datetime, end: dt.datetime,
|
|
1216
|
+
identities: "tuple[tuple[str, str], ...] | None",
|
|
1217
|
+
) -> tuple:
|
|
1218
|
+
"""The qualified accounting rows of ``[start, end)``, in
|
|
1219
|
+
``(timestamp_utc, source_root_key, conversation_key, id)`` order: the whole
|
|
1220
|
+
window, or only the given dirty ``(root, path)`` identities.
|
|
1221
|
+
|
|
1222
|
+
The window read streams in that order from
|
|
1223
|
+
``idx_codex_entries_ts_root_conversation``. The dirty-path read seeks
|
|
1224
|
+
``idx_codex_entries_root_path`` instead and is ordered here, in Python, over
|
|
1225
|
+
the rows it returns whole anyway (#901 Amendment 19 T1); the rows keep the
|
|
1226
|
+
connection's row factory.
|
|
1227
|
+
"""
|
|
1228
|
+
params: tuple[object, ...] = (
|
|
1229
|
+
start.astimezone(UTC).isoformat(), end.astimezone(UTC).isoformat(),
|
|
1230
|
+
*(value for identity in (identities or ()) for value in identity),
|
|
1231
|
+
)
|
|
1232
|
+
if identities is None:
|
|
1233
|
+
return tuple(conn.execute(_QUALIFIED_CODEX_ENTRIES_SQL, params))
|
|
1234
|
+
rows = conn.execute(
|
|
1235
|
+
_qualified_codex_path_entries_sql(len(identities)), params).fetchall()
|
|
1236
|
+
rows.sort(key=_qualified_codex_row_order)
|
|
1237
|
+
return tuple(rows)
|
|
1238
|
+
|
|
1239
|
+
|
|
1187
1240
|
def load_qualified_codex_entries(
|
|
1188
1241
|
start: dt.datetime,
|
|
1189
1242
|
end: dt.datetime,
|
|
@@ -1241,15 +1294,7 @@ def load_qualified_codex_entries(
|
|
|
1241
1294
|
if stats.lock_contended:
|
|
1242
1295
|
raise QualifiedMetadataUnavailable("Codex qualified project metadata is unavailable")
|
|
1243
1296
|
conn.row_factory = sqlite3.Row
|
|
1244
|
-
|
|
1245
|
-
_QUALIFIED_CODEX_ENTRIES_SQL if identities is None
|
|
1246
|
-
else _qualified_codex_path_entries_sql(len(identities))
|
|
1247
|
-
)
|
|
1248
|
-
params: tuple[object, ...] = (
|
|
1249
|
-
start.astimezone(UTC).isoformat(), end.astimezone(UTC).isoformat(),
|
|
1250
|
-
*(value for identity in (identities or ()) for value in identity),
|
|
1251
|
-
)
|
|
1252
|
-
rows = tuple(conn.execute(sql, params))
|
|
1297
|
+
rows = _load_qualified_codex_rows(conn, start, end, identities)
|
|
1253
1298
|
# §4.3: one inventory read per build. The caller supplies the build's
|
|
1254
1299
|
# own inventory when it has one; its alias leg is unbounded, which is a
|
|
1255
1300
|
# SUPERSET of the `paths=identities` leg this read would have loaded,
|