cctally 1.117.2 → 1.118.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +7 -3
  3. package/bin/_cctally_cache.py +2873 -193
  4. package/bin/_cctally_completion.py +106 -33
  5. package/bin/_cctally_core.py +69 -4
  6. package/bin/_cctally_dashboard.py +188 -13
  7. package/bin/_cctally_dashboard_conversation.py +5 -1
  8. package/bin/_cctally_dashboard_envelope.py +50 -19
  9. package/bin/_cctally_dashboard_sources.py +23 -1
  10. package/bin/_cctally_db.py +193 -153
  11. package/bin/_cctally_diagnosis_sources.py +63 -7
  12. package/bin/_cctally_doctor.py +865 -93
  13. package/bin/_cctally_five_hour.py +67 -50
  14. package/bin/_cctally_forecast.py +35 -13
  15. package/bin/_cctally_journal.py +186 -18
  16. package/bin/_cctally_milestone_history.py +859 -62
  17. package/bin/_cctally_oauth_samples.py +286 -15
  18. package/bin/_cctally_parser.py +2 -2
  19. package/bin/_cctally_quota.py +104 -12
  20. package/bin/_cctally_record.py +153 -29
  21. package/bin/_cctally_rederive.py +608 -68
  22. package/bin/_cctally_statusline.py +108 -81
  23. package/bin/_cctally_store.py +179 -38
  24. package/bin/_cctally_tui.py +892 -181
  25. package/bin/_cctally_update.py +32 -13
  26. package/bin/_lib_codex_conversation_query.py +52 -14
  27. package/bin/_lib_codex_drain_admission.py +224 -6
  28. package/bin/_lib_codex_metadata.py +39 -0
  29. package/bin/_lib_conversation_anon.py +15 -8
  30. package/bin/_lib_conversation_dispatch.py +7 -0
  31. package/bin/_lib_conversation_query.py +289 -70
  32. package/bin/_lib_dashboard_sources.py +37 -1
  33. package/bin/_lib_detector_access_registry.py +497 -26
  34. package/bin/_lib_doctor.py +85 -20
  35. package/bin/_lib_doctor_scan_index.py +321 -0
  36. package/bin/_lib_ingest_frontier.py +20 -1
  37. package/bin/_lib_oauth_samples.py +23 -7
  38. package/bin/_lib_pricing.py +146 -16
  39. package/bin/_lib_quota.py +19 -0
  40. package/bin/_lib_rederive.py +11 -6
  41. package/bin/_lib_retained_records.py +516 -0
  42. package/bin/_lib_snapshot_cache.py +10 -0
  43. package/bin/_lib_stale_proof.py +272 -43
  44. package/bin/_lib_stats_content_revision.py +831 -0
  45. package/bin/_lib_write_io.py +11 -4
  46. package/bin/cctally +4 -1
  47. package/dashboard/static/assets/ConversationsView-D4Iv4ku0.js +72 -0
  48. package/dashboard/static/assets/{DoctorModal-B0DnU9d9.js → DoctorModal-D4QSq6uz.js} +1 -1
  49. package/dashboard/static/assets/ModalRoot-C9F5dhZi.js +1 -0
  50. package/dashboard/static/assets/{ProjectsDrillPanel-DSxXQSa-.js → ProjectsDrillPanel-WxAdRDMt.js} +1 -1
  51. package/dashboard/static/assets/{SourceDetailModal-BleDpQqG.js → SourceDetailModal-NyyEfXmc.js} +1 -1
  52. package/dashboard/static/assets/{UpdateModal-CMqaAkSK.js → UpdateModal-CFLq9pT1.js} +1 -1
  53. package/dashboard/static/assets/{index-BjQV-0-D.js → index-BKTWH9JE.js} +2 -2
  54. package/dashboard/static/assets/{outlineNavigation-CO-VWBm7.js → outlineNavigation-BnGUwCS3.js} +3 -3
  55. package/dashboard/static/dashboard.html +1 -1
  56. package/package.json +4 -1
  57. package/dashboard/static/assets/ConversationsView-oqNsWzWV.js +0 -72
  58. package/dashboard/static/assets/ModalRoot-CYua2wP_.js +0 -1
@@ -24,14 +24,24 @@ host-local offset while boundaries are canonical UTC).
24
24
  """
25
25
  from __future__ import annotations
26
26
 
27
+ import bisect
27
28
  import datetime as dt
29
+ import hashlib
30
+ import json
31
+ import math
32
+ import os
33
+ import secrets
28
34
  import sqlite3
29
35
  import sys
36
+ import threading
30
37
  from dataclasses import replace
38
+ from typing import NamedTuple
31
39
 
32
40
  from _cctally_core import (
33
41
  _canonicalize_optional_iso, make_week_ref, parse_iso_datetime,
34
42
  )
43
+ import _cctally_quota as _quota
44
+ import _lib_pricing
35
45
  from _cctally_quota import assert_projection_readable, codex_quota_breakdown
36
46
  from _lib_accounts import UNATTRIBUTED
37
47
  from _lib_codex_pools import is_model_scoped_codex_quota
@@ -45,6 +55,7 @@ from _lib_json_envelope import _iso_z
45
55
  from _lib_quota import QuotaWindowIdentity
46
56
 
47
57
  import _lib_milestone_history as _mh
58
+ import _lib_stats_content_revision as _content_revision
48
59
 
49
60
  UTC = dt.timezone.utc
50
61
 
@@ -374,35 +385,9 @@ def _claude_cycle_rows(conn: sqlite3.Connection, ref, *, account_key=None,
374
385
  for its CURRENT cycle, so the index, the detail and the card read one
375
386
  segment (spec 5.2 step 2) rather than re-deriving it from the cut instant.
376
387
  """
377
- acct_s, acct_p = _account_predicate(account_key, "s.account_key")
378
- acct_m, _ = _account_predicate(account_key, "m.account_key")
379
- acct_e, _ = _account_predicate(account_key, "e.account_key")
380
- acct_r, _ = _account_predicate(account_key, "account_key")
388
+ acct_r, acct_p = _account_predicate(account_key, "account_key")
381
389
  if cohort_id is None:
382
- cohort_id = 0
383
- if ref.week_start_at:
384
- row = conn.execute(
385
- "SELECT e.id FROM week_reset_events e "
386
- "WHERE unixepoch(e.effective_reset_at_utc)=unixepoch(?)"
387
- + acct_e + " "
388
- "AND (EXISTS ("
389
- " SELECT 1 FROM weekly_usage_snapshots s "
390
- " WHERE s.week_start_date=?" + acct_s + " AND ("
391
- " unixepoch(s.week_end_at)=unixepoch(e.old_week_end_at) OR "
392
- " unixepoch(s.week_end_at)=unixepoch(e.new_week_end_at)"
393
- " )"
394
- ") OR EXISTS ("
395
- " SELECT 1 FROM percent_milestones m "
396
- " WHERE m.week_start_date=?" + acct_m + " AND ("
397
- " unixepoch(m.week_end_at)=unixepoch(e.old_week_end_at) OR "
398
- " unixepoch(m.week_end_at)=unixepoch(e.new_week_end_at)"
399
- " )"
400
- ")) ORDER BY e.id DESC LIMIT 1",
401
- (ref.week_start_at,) + acct_p + (ref.key,) + acct_p
402
- + (ref.key,) + acct_p,
403
- ).fetchone()
404
- if row is not None:
405
- cohort_id = int(row[0])
390
+ cohort_id = _claude_cycle_cohort_id(conn, ref, account_key=account_key)
406
391
  return conn.execute(
407
392
  "SELECT * FROM percent_milestones WHERE week_start_date=? "
408
393
  "AND reset_event_id=?" + acct_r + " "
@@ -411,6 +396,42 @@ def _claude_cycle_rows(conn: sqlite3.Connection, ref, *, account_key=None,
411
396
  ).fetchall()
412
397
 
413
398
 
399
+ def _claude_cycle_cohort_id(conn: sqlite3.Connection, ref, *,
400
+ account_key=None) -> int:
401
+ """The cycle's weekly milestone cohort (`percent_milestones.
402
+ reset_event_id`) as derived from its effective start: the newest reset
403
+ event effective at that instant that names this storage week, else ``0``.
404
+ Shared by the cohort read and the #1050 stamp, which scopes on it."""
405
+ acct_s, acct_p = _account_predicate(account_key, "s.account_key")
406
+ acct_m, _ = _account_predicate(account_key, "m.account_key")
407
+ acct_e, _ = _account_predicate(account_key, "e.account_key")
408
+ cohort_id = 0
409
+ if ref.week_start_at:
410
+ row = conn.execute(
411
+ "SELECT e.id FROM week_reset_events e "
412
+ "WHERE unixepoch(e.effective_reset_at_utc)=unixepoch(?)"
413
+ + acct_e + " "
414
+ "AND (EXISTS ("
415
+ " SELECT 1 FROM weekly_usage_snapshots s "
416
+ " WHERE s.week_start_date=?" + acct_s + " AND ("
417
+ " unixepoch(s.week_end_at)=unixepoch(e.old_week_end_at) OR "
418
+ " unixepoch(s.week_end_at)=unixepoch(e.new_week_end_at)"
419
+ " )"
420
+ ") OR EXISTS ("
421
+ " SELECT 1 FROM percent_milestones m "
422
+ " WHERE m.week_start_date=?" + acct_m + " AND ("
423
+ " unixepoch(m.week_end_at)=unixepoch(e.old_week_end_at) OR "
424
+ " unixepoch(m.week_end_at)=unixepoch(e.new_week_end_at)"
425
+ " )"
426
+ ")) ORDER BY e.id DESC LIMIT 1",
427
+ (ref.week_start_at,) + acct_p + (ref.key,) + acct_p
428
+ + (ref.key,) + acct_p,
429
+ ).fetchone()
430
+ if row is not None:
431
+ cohort_id = int(row[0])
432
+ return cohort_id
433
+
434
+
414
435
  def _five_hour_detail_revision(conn, start_iso, end_iso, *,
415
436
  account_key=None) -> tuple[int, ...]:
416
437
  """Append-only 5h child revision for one cycle's intersecting blocks.
@@ -511,8 +532,100 @@ def _unavailable_join_component(conn, start_iso, end_iso,
511
532
  return ("u" + _mh.compute_detail_stamp(*ids),)
512
533
 
513
534
 
535
+ class _ClaudeRevisionScopes:
536
+ """The #1050 content-revision material of ONE index build (spec 4.6).
537
+
538
+ Read once per `build_claude_week_index`: the build's `ContentRevision`
539
+ and every scope row, in one read snapshot — the caller's transaction when
540
+ one is open (the detail route's), otherwise a short ``BEGIN`` …
541
+ ``ROLLBACK`` of its own. The scope rows are strict to ``account_key`` when
542
+ the index is scoped and merged across every account (``unattributed``
543
+ included) otherwise, exactly as the index reads its rows.
544
+
545
+ A ``current`` store names the relevant revision of each cycle; any other
546
+ state (absent, partial, unsupported, unreadable) cannot certify a stamp,
547
+ so every entry of the build carries the same fresh nonce instead and the
548
+ client never serves a detail cached under an earlier one.
549
+ """
550
+
551
+ def __init__(self, conn: sqlite3.Connection, *, account_key=None):
552
+ own = not conn.in_transaction
553
+ if own:
554
+ conn.execute("BEGIN")
555
+ try:
556
+ try:
557
+ revision = _content_revision.read_claude_content_revision(conn)
558
+ except _content_revision.StatsContentRevisionUnreadable:
559
+ revision = None
560
+ trusted = (revision is not None
561
+ and revision.state == _content_revision.STATE_CURRENT)
562
+ rows = (_content_revision.read_scope_rows(
563
+ conn, account_key=account_key) if trusted else [])
564
+ finally:
565
+ if own:
566
+ conn.rollback()
567
+ self.trusted = trusted
568
+ self.cycle_basis = revision.cycle_basis if trusted else None
569
+ self.nonce = None if trusted else secrets.token_hex(8)
570
+ self._keyed: dict = {"cohort": {}, "bucket": {}}
571
+ blocks, points = [], []
572
+ for row in rows:
573
+ if row.kind in self._keyed:
574
+ keyed = self._keyed[row.kind]
575
+ keyed[row.scope_key] = max(
576
+ keyed.get(row.scope_key, row.revision), row.revision)
577
+ elif row.start_epoch is None or row.end_epoch is None:
578
+ continue # an unparseable bound never matches, as in SQL
579
+ elif row.kind == "block":
580
+ blocks.append((row.start_epoch, row.end_epoch, row.revision))
581
+ elif row.kind == "reset_point":
582
+ points.append((row.start_epoch, row.revision))
583
+ blocks.sort()
584
+ points.sort()
585
+ self._blocks = blocks
586
+ self._block_starts = [b[0] for b in blocks]
587
+ self._points = points
588
+ self._point_epochs = [p[0] for p in points]
589
+
590
+ def component(self, conn, ref, cohort_id, start_iso, end_iso) -> tuple:
591
+ """The stamp component for one cycle: ``("c" + …,)`` over the MAX
592
+ revision of its cohort, its bucket, every block intersecting
593
+ ``[start, end)`` and every reset point in ``[start, end]``; ``()``
594
+ when no scope is relevant and the store was never published or
595
+ repaired (so such a cycle keeps its bytes); ``("u" + nonce,)`` on an
596
+ untrusted store."""
597
+ if not self.trusted:
598
+ return ("u" + self.nonce,)
599
+ relevant = [
600
+ rev for rev in (
601
+ self._keyed["cohort"].get(f"{ref.key}|{cohort_id}"),
602
+ self._keyed["bucket"].get(ref.key),
603
+ ) if rev is not None
604
+ ]
605
+ if (self._blocks or self._points) and start_iso and end_iso:
606
+ start, end = conn.execute(
607
+ "SELECT unixepoch(?), unixepoch(?)",
608
+ (start_iso, end_iso)).fetchone()
609
+ if start is not None and end is not None:
610
+ # Blocks starting before the cycle's end, then ending after
611
+ # its start; reset points inside the closed interval.
612
+ upto = bisect.bisect_left(self._block_starts, end)
613
+ relevant.extend(rev for _s, b_end, rev in self._blocks[:upto]
614
+ if b_end > start)
615
+ lo = bisect.bisect_left(self._point_epochs, start)
616
+ hi = bisect.bisect_right(self._point_epochs, end)
617
+ relevant.extend(rev for _e, rev in self._points[lo:hi])
618
+ if not relevant and self.cycle_basis is None:
619
+ return ()
620
+ return ("c" + _mh.compute_detail_stamp(
621
+ self.cycle_basis, max(relevant, default=None)),)
622
+
623
+
514
624
  def _index_entry(conn, ref, current_cycle_key, tz, *, unavailable_rows,
515
- decorated=False, account_key=None, cohort_id=None) -> dict:
625
+ revision_scopes, decorated=False, account_key=None,
626
+ cohort_id=None) -> dict:
627
+ if cohort_id is None:
628
+ cohort_id = _claude_cycle_cohort_id(conn, ref, account_key=account_key)
516
629
  rows = _claude_cycle_rows(
517
630
  conn, ref, account_key=account_key, cohort_id=cohort_id)
518
631
  milestone_count = len(rows)
@@ -535,6 +648,10 @@ def _index_entry(conn, ref, current_cycle_key, tz, *, unavailable_rows,
535
648
  unavailable = _unavailable_join_component(
536
649
  conn, start_z, end_z, unavailable_rows)
537
650
  scope_part = () if account_key is None else ("a" + account_key,)
651
+ # #1050 spec 4.6: the cycle's stored-content revision, so an in-place
652
+ # correction of any row this cycle reads re-stamps it (and only it).
653
+ revision_part = revision_scopes.component(
654
+ conn, ref, cohort_id, start_z, end_z)
538
655
 
539
656
  # #750 S4 §5.3. A CURRENT single-segment cycle neither fetches its detail
540
657
  # nor uses one that arrives, so a gap in the LIVE cycle would stay
@@ -563,7 +680,7 @@ def _index_entry(conn, ref, current_cycle_key, tz, *, unavailable_rows,
563
680
  "has_observation_gap": has_gap,
564
681
  "detail_stamp": _mh.compute_detail_stamp(
565
682
  key, milestone_count, block_count, segment_count, max_captured,
566
- *block_revision, *unavailable, *scope_part,
683
+ *block_revision, *revision_part, *unavailable, *scope_part,
567
684
  ),
568
685
  }
569
686
 
@@ -655,9 +772,11 @@ def build_claude_week_index(conn: sqlite3.Connection, *, account_key=None,
655
772
  current_cycle_key = (
656
773
  _claude_cycle_key(current_ref) if current_ref else None)
657
774
  unavailable_rows = _unavailable_join_rows(conn)
775
+ revision_scopes = _ClaudeRevisionScopes(conn)
658
776
  entries = [
659
777
  _index_entry(conn, ref, current_cycle_key, tz,
660
- unavailable_rows=unavailable_rows, decorated=decorated)
778
+ unavailable_rows=unavailable_rows,
779
+ revision_scopes=revision_scopes, decorated=decorated)
661
780
  for ref in refs
662
781
  ]
663
782
  else:
@@ -667,10 +786,12 @@ def build_claude_week_index(conn: sqlite3.Connection, *, account_key=None,
667
786
  _claude_cycle_key(current_ref) if current_ref else None)
668
787
  unavailable_rows = _unavailable_join_rows(
669
788
  conn, account_key=account_key)
789
+ revision_scopes = _ClaudeRevisionScopes(conn, account_key=account_key)
670
790
  entries = [
671
791
  _index_entry(
672
792
  conn, ref, current_cycle_key, tz,
673
- unavailable_rows=unavailable_rows, decorated=decorated,
793
+ unavailable_rows=unavailable_rows,
794
+ revision_scopes=revision_scopes, decorated=decorated,
674
795
  account_key=account_key,
675
796
  cohort_id=(scope.reset_event_id
676
797
  if ref is current_ref else None),
@@ -1260,28 +1381,12 @@ def _select_live_physical_cycle(cycles: list, boundary):
1260
1381
  return min(pool, key=_rank)
1261
1382
 
1262
1383
 
1263
- def _codex_milestone_count(stats_conn, cyc) -> tuple[int, str | None]:
1264
- """Unique threshold count + max captured-at for one selected identity."""
1265
- resets = cyc.member_reset_isos
1266
- placeholders = ",".join("unixepoch(?)" for _ in resets)
1267
- row = stats_conn.execute(
1268
- "SELECT COUNT(DISTINCT percent_threshold), MAX(captured_at_utc) "
1269
- "FROM quota_percent_milestones "
1270
- "WHERE source='codex' AND source_root_key=? AND account_key=? "
1271
- " AND logical_limit_key=? "
1272
- " AND observed_slot=? AND window_minutes=? "
1273
- f" AND unixepoch(resets_at_utc) IN ({placeholders}) AND orphaned_at IS NULL",
1274
- (cyc.root, cyc.account_key, cyc.limit, cyc.slot, cyc.window, *resets),
1275
- ).fetchone()
1276
- return int(row[0] or 0), row[1]
1277
-
1278
-
1279
1384
  def _codex_five_hour_rows(stats_conn, cyc, *, include_orphaned=False) -> list:
1280
1385
  """Every retained 5h block on the selected root intersecting [start, end).
1281
1386
 
1282
1387
  The account filter admits the cycle's own key OR the ``unattributed``
1283
1388
  sentinel, and deliberately NOT strict equality. Unlike
1284
- ``_codex_milestone_count`` — whose block and milestone parameters both come
1389
+ ``_codex_cycle_raw_rows`` — whose block and milestone parameters both come
1285
1390
  from one ``block.identity`` and so cannot disagree — this query spans
1286
1391
  identities, selecting by root plus time range. The 300-minute windows are a
1287
1392
  separate physical-window group from the weekly one and
@@ -1333,7 +1438,8 @@ def _codex_five_hour_rows(stats_conn, cyc, *, include_orphaned=False) -> list:
1333
1438
  return rows
1334
1439
 
1335
1440
 
1336
- def _codex_five_hour_clusters(stats_conn, cyc, *, include_orphaned=False) -> list:
1441
+ def _codex_five_hour_clusters(stats_conn, cyc, *, include_orphaned=False,
1442
+ rows=None) -> list:
1337
1443
  """Retained 5h blocks intersecting the cycle, jitter-canonicalized.
1338
1444
 
1339
1445
  The 300-minute rows carry the same second-level ``resets_at`` jitter as the
@@ -1341,9 +1447,16 @@ def _codex_five_hour_clusters(stats_conn, cyc, *, include_orphaned=False) -> lis
1341
1447
  clustered the same way — one canonical block per physical reset — before
1342
1448
  counting/rendering. Returns cluster-representative ``_CodexCycle`` blocks
1343
1449
  (``.members`` set), ordered by ascending start.
1450
+
1451
+ ``rows`` (#1050) clusters rows the caller already read with
1452
+ ``_codex_five_hour_rows`` — `_codex_cycle_inputs`' block rows — in that
1453
+ function's own order, so the stamp and the detail enumerate one population.
1344
1454
  """
1455
+ if rows is None:
1456
+ rows = _codex_five_hour_rows(
1457
+ stats_conn, cyc, include_orphaned=include_orphaned)
1345
1458
  blocks: list = []
1346
- for row in _codex_five_hour_rows(stats_conn, cyc, include_orphaned=include_orphaned):
1459
+ for row in rows:
1347
1460
  block = _CodexCycle(row)
1348
1461
  if block.start is None or block.reset is None:
1349
1462
  continue
@@ -1359,9 +1472,580 @@ def _codex_five_hour_clusters(stats_conn, cyc, *, include_orphaned=False) -> lis
1359
1472
  return clusters
1360
1473
 
1361
1474
 
1362
- def _codex_cycle_entry(stats_conn, cyc, tz) -> dict:
1363
- milestone_count, max_captured = _codex_milestone_count(stats_conn, cyc)
1364
- block_count = len(_codex_five_hour_clusters(stats_conn, cyc))
1475
+ # ── #1050 spec 4.7: every input of a Codex cycle detail ─────────────────────
1476
+ #
1477
+ # The Codex cycle detail publishes block `current_percent`, the cycle ladder
1478
+ # and every included block's ladder (crossings, high-water percent, physical
1479
+ # boundaries), and per-milestone dollars and tokens priced at read time from
1480
+ # `codex_session_entries` at the resolved speed. Its stamp named none of those
1481
+ # values, so a cached detail survived every in-place rewrite of them. One
1482
+ # helper reads all of them, and both the index entry (the stamp) and the
1483
+ # detail route consume it.
1484
+
1485
+ #: Every `quota_percent_milestones` column a Codex detail reads, its unique key
1486
+ #: (`source, source_root_key, account_key, logical_limit_key, observed_slot,
1487
+ #: window_minutes, resets_at_utc, percent_threshold`) first, so the rows sort
1488
+ #: by that key as plain tuples (every key column is NOT NULL).
1489
+ _CODEX_MILESTONE_INPUT_COLUMNS = (
1490
+ "source, source_root_key, account_key, logical_limit_key, observed_slot, "
1491
+ "window_minutes, resets_at_utc, percent_threshold, captured_at_utc, "
1492
+ "source_path, line_offset, high_water_percent"
1493
+ )
1494
+ _CODEX_MILESTONE_KEY_WIDTH = 8
1495
+
1496
+
1497
+ class CodexCycleInputs(NamedTuple):
1498
+ """Everything one Codex cycle detail's published values depend on.
1499
+
1500
+ * ``cycle_rows`` — the cycle's raw qualifying ``quota_percent_milestones``
1501
+ rows across its jitter-member resets (the stamp's distinct-threshold count
1502
+ and raw maximum capture come from these), ordered by unique key.
1503
+ * ``block_ladder_rows`` — per selection the detail prices (the cycle's own
1504
+ members, then every included five-hour block cluster's members): the
1505
+ ladder rows ``codex_quota_breakdown`` reads for that member, by unique key.
1506
+ * ``block_rows`` — the five-hour block rows ``_codex_five_hour_rows``
1507
+ returns, by unique key.
1508
+ * ``first_boundaries`` — per priced member, the physical
1509
+ ``(captured_at_utc, source_path, line_offset)`` the breakdown opens from,
1510
+ ``"absent"`` when it has none, ``"unread"`` when the member has no ladder.
1511
+ * ``selection_aggregates`` — per priced member with a boundary, the
1512
+ aggregate twin of its accounting read.
1513
+ * ``accounting_seq``, ``speed`` (resolved), ``pricing_identity``.
1514
+ * ``block_clusters`` — derived from the block rows in their read order, the
1515
+ clusters the detail renders; excluded from the content hash.
1516
+ """
1517
+
1518
+ cycle_rows: tuple
1519
+ block_ladder_rows: tuple
1520
+ block_rows: tuple
1521
+ first_boundaries: tuple
1522
+ selection_aggregates: tuple
1523
+ accounting_seq: "int | None"
1524
+ speed: str
1525
+ pricing_identity: str
1526
+ block_clusters: tuple
1527
+
1528
+
1529
+ def _canonical_digest(value) -> str:
1530
+ return hashlib.sha256(json.dumps(
1531
+ value, allow_nan=False, ensure_ascii=False, separators=(",", ":"),
1532
+ ).encode("utf-8")).hexdigest()
1533
+
1534
+
1535
+ def _member_ident(member) -> tuple:
1536
+ return (member.root, member.account_key, member.limit, member.slot,
1537
+ member.window, member.reset_iso)
1538
+
1539
+
1540
+ def _codex_cycle_raw_rows(stats_conn, cyc) -> tuple:
1541
+ """The cycle's raw qualifying milestone rows: the population the stamp's
1542
+ count and maximum capture have always covered, now read whole."""
1543
+ resets = cyc.member_reset_isos
1544
+ placeholders = ",".join("unixepoch(?)" for _ in resets)
1545
+ rows = stats_conn.execute(
1546
+ f"SELECT {_CODEX_MILESTONE_INPUT_COLUMNS} "
1547
+ "FROM quota_percent_milestones "
1548
+ "WHERE source='codex' AND source_root_key=? AND account_key=? "
1549
+ " AND logical_limit_key=? "
1550
+ " AND observed_slot=? AND window_minutes=? "
1551
+ f" AND unixepoch(resets_at_utc) IN ({placeholders}) AND orphaned_at IS NULL",
1552
+ (cyc.root, cyc.account_key, cyc.limit, cyc.slot, cyc.window, *resets),
1553
+ ).fetchall()
1554
+ return tuple(sorted(tuple(r) for r in rows))
1555
+
1556
+
1557
+ def _codex_member_ladder_rows(stats_conn, member) -> tuple:
1558
+ """One member's ladder exactly as ``_cctally_quota._load_active_milestones``
1559
+ selects it (text-equal reset, the member's own account), every column."""
1560
+ rows = stats_conn.execute(
1561
+ f"SELECT {_CODEX_MILESTONE_INPUT_COLUMNS} "
1562
+ "FROM quota_percent_milestones "
1563
+ "WHERE source='codex' AND source_root_key=? AND account_key=? "
1564
+ " AND logical_limit_key=? AND observed_slot=? AND window_minutes=? "
1565
+ " AND resets_at_utc=? AND orphaned_at IS NULL",
1566
+ (member.root, member.account_key, member.limit, member.slot,
1567
+ member.window, member.reset_iso),
1568
+ ).fetchall()
1569
+ return tuple(sorted(tuple(r) for r in rows))
1570
+
1571
+
1572
+ def _codex_block_row_key(row) -> tuple:
1573
+ # (source_root_key, account_key, logical_limit_key, observed_slot,
1574
+ # window_minutes, resets_at_utc): the unique key minus the constant source.
1575
+ return (row[0], row[6], row[1], row[2], row[3], row[7])
1576
+
1577
+
1578
+ def _codex_cache_file_identity(cache_conn) -> "tuple | None":
1579
+ try:
1580
+ rows = cache_conn.execute("PRAGMA database_list").fetchall()
1581
+ path = next(str(r[2]) for r in rows if str(r[1]) == "main" and r[2])
1582
+ stat = os.stat(path)
1583
+ except (OSError, StopIteration, sqlite3.Error, TypeError, ValueError):
1584
+ return None
1585
+ return (stat.st_dev, stat.st_ino)
1586
+
1587
+
1588
+ def _codex_accounting_seq(cache_conn) -> "int | None":
1589
+ try:
1590
+ row = cache_conn.execute(
1591
+ "SELECT value FROM cache_meta "
1592
+ "WHERE key='codex_accounting_mutation_seq'").fetchone()
1593
+ except sqlite3.Error:
1594
+ return None
1595
+ try:
1596
+ return None if row is None else int(row[0])
1597
+ except (TypeError, ValueError):
1598
+ return None
1599
+
1600
+
1601
+ def _codex_quota_log_seq(cache_conn) -> "int | None":
1602
+ try:
1603
+ row = cache_conn.execute(
1604
+ "SELECT seq FROM sqlite_sequence "
1605
+ "WHERE name='quota_window_change_log'").fetchone()
1606
+ except sqlite3.Error:
1607
+ return None
1608
+ return 0 if row is None else int(row[0])
1609
+
1610
+
1611
+ def _codex_snapshot_seq(cache_conn) -> "int | None":
1612
+ try:
1613
+ row = cache_conn.execute(
1614
+ "SELECT seq FROM sqlite_sequence "
1615
+ "WHERE name='quota_window_snapshots'").fetchone()
1616
+ except sqlite3.Error:
1617
+ return None
1618
+ return 0 if row is None else int(row[0])
1619
+
1620
+
1621
+ def _reset_epoch(text) -> "int | None":
1622
+ parsed = _parse_utc(text)
1623
+ return None if parsed is None else math.floor(parsed.timestamp())
1624
+
1625
+
1626
+ def _codex_boundary_order_key(row) -> tuple:
1627
+ """``_first_block_physical_tuple``'s ``ORDER BY unixepoch(captured_at_utc),
1628
+ unixepoch(resets_at_utc), source_path, line_offset`` over a bulk row
1629
+ ``(reset_epoch, captured_epoch, captured_text, path, offset, account)``:
1630
+ SQLite sorts a NULL ``unixepoch`` first."""
1631
+ return (row[1] is not None, row[1] or 0, row[0], row[3], row[4])
1632
+
1633
+
1634
+ def _codex_fold_boundary_row(out: dict, row: tuple) -> None:
1635
+ """Fold one snapshot ``(reset_epoch, captured_epoch, captured_text, path,
1636
+ offset, account)`` into its reset's per-flavour minimum (see
1637
+ `_codex_bulk_first_boundaries`). A minimum over a superset is the minimum
1638
+ of the old minimum and the new rows, so folding is also how appended rows
1639
+ join a memoized identity."""
1640
+ slots = out.get(row[0])
1641
+ if slots is None:
1642
+ slots = out[row[0]] = [None, None, {}]
1643
+ key = _codex_boundary_order_key(row)
1644
+ if slots[0] is None or key < slots[0][0]:
1645
+ slots[0] = (key, row)
1646
+ account = row[5]
1647
+ if account is None or account == UNATTRIBUTED:
1648
+ if slots[1] is None or key < slots[1][0]:
1649
+ slots[1] = (key, row)
1650
+ else:
1651
+ prior = slots[2].get(account)
1652
+ if prior is None or key < prior[0]:
1653
+ slots[2][account] = (key, row)
1654
+
1655
+
1656
+ def _codex_bulk_first_boundaries(cache_conn, identity_parts) -> dict:
1657
+ """Every first boundary of one raw quota identity, in one pass (#1050).
1658
+
1659
+ ``_first_block_physical_tuple`` cannot seek its own predicate:
1660
+ ``unixepoch(resets_at_utc)=unixepoch(?)`` matches no index expression, so
1661
+ each call scans the identity's whole snapshot history (11-70 ms on the
1662
+ #997 store), and the index needs one per priced member. This is that
1663
+ query's exact twin over every reset at once: per reset second, the
1664
+ minimum row in the same order for each account-predicate flavour the
1665
+ boundary read takes (``_codex_cache_account_predicate(...,
1666
+ admit_unattributed=True)``): every row (no scope), the unattributed rows
1667
+ (``NULL`` or the sentinel), and each real account's own rows — a real
1668
+ scope admits its own rows plus the unattributed ones.
1669
+ """
1670
+ root, limit, slot, window = identity_parts
1671
+ out: dict = {}
1672
+ for row in cache_conn.execute(
1673
+ "SELECT unixepoch(resets_at_utc), unixepoch(captured_at_utc), "
1674
+ " captured_at_utc, source_path, line_offset, account_key "
1675
+ "FROM quota_window_snapshots "
1676
+ "WHERE source='codex' AND source_root_key=? "
1677
+ " AND logical_limit_key=? AND observed_slot=? "
1678
+ " AND window_minutes=? AND unixepoch(resets_at_utc) IS NOT NULL",
1679
+ (root, limit, slot, window)):
1680
+ _codex_fold_boundary_row(out, tuple(row))
1681
+ return out
1682
+
1683
+
1684
+ def _codex_boundary_from_bulk(slots, account_scope):
1685
+ """One member's boundary from its reset's bulk slots, parsed exactly as
1686
+ ``_first_block_physical_tuple`` parses its row (a row that does not parse
1687
+ is no boundary)."""
1688
+ if slots is None:
1689
+ return None
1690
+ if account_scope is None:
1691
+ best = slots[0]
1692
+ elif account_scope == UNATTRIBUTED:
1693
+ best = slots[1]
1694
+ else:
1695
+ candidates = [c for c in (slots[2].get(account_scope), slots[1])
1696
+ if c is not None]
1697
+ best = min(candidates, key=lambda c: c[0]) if candidates else None
1698
+ if best is None:
1699
+ return None
1700
+ row = best[1]
1701
+ try:
1702
+ return (_quota._parse_utc(str(row[2]), "captured_at_utc"),
1703
+ str(row[3]), int(row[4]))
1704
+ except (TypeError, ValueError):
1705
+ return None
1706
+
1707
+
1708
+ class _CodexCycleInputsMemo:
1709
+ """Process memo of the Codex cycle-index stamps' expensive inputs (#1050).
1710
+
1711
+ The index stamps every retained cycle, and the pricing inputs are
1712
+ per-member reads over `quota_window_snapshots` and `codex_session_entries`.
1713
+ Read one by one they cost seconds per index build, so the source build
1714
+ hands the index this memo. Only the index uses it: the detail route always
1715
+ reads fresh, through the breakdown's own boundary read, so
1716
+ `test_1050_codex_detail_stamp_equals_index_stamp` checks the memo against
1717
+ a fresh read.
1718
+
1719
+ * **First boundaries** come from one bulk pass per raw identity
1720
+ (`_codex_bulk_first_boundaries`), kept while the cache file is the same
1721
+ and every Codex mutation since is an append. The triggers on
1722
+ `quota_window_snapshots` append one row to the AUTOINCREMENT
1723
+ `quota_window_change_log` for every Codex insert, delete and semantic
1724
+ update, and neither sequence is ever reused, so when the ledger's
1725
+ high-water moved by exactly the number of Codex snapshot rows now present
1726
+ above the snapshot high-water last seen, every change was an insert of
1727
+ one of those rows (a delete or update adds a ledger row without adding
1728
+ such a row) and they fold in (`_codex_fold_boundary_row`). Anything else
1729
+ re-reads the boundaries. The projector prunes the ledger, which leaves
1730
+ its high-water where it is, so this never reads ledger rows.
1731
+ * **Entry aggregates** are kept per selection and invalidated by
1732
+ `codex_accounting_change_log`, as
1733
+ `_lib_snapshot_cache.build_cached_codex_accounting` already trusts it: a
1734
+ selection drops when a changed path's old or new timestamp extent
1735
+ intersects it. A replaced file, a counter that went backwards, a ledger
1736
+ gap, a `full` change or too many dirty paths go cold.
1737
+ * **Per-cycle results** are keyed on the caller's `codex_stats_digest`,
1738
+ which (with the two #1050 relations) covers every stats column the helper
1739
+ reads, plus the cycle's own selection; an entry is reused only while
1740
+ every boundary and aggregate it was built from is unchanged. One stats
1741
+ generation is kept at a time.
1742
+ """
1743
+
1744
+ _MAX_DIRTY_PATHS = 512
1745
+ _MAX_ENTRIES = 8192
1746
+
1747
+ def __init__(self):
1748
+ self.lock = threading.Lock()
1749
+ self.cache_ident = None
1750
+ self.accounting_seq = None
1751
+ self.quota_log_seq = None
1752
+ self.snapshot_seq = None
1753
+ self.path_extents: dict = {}
1754
+ self.bulk_boundaries: dict = {}
1755
+ self.aggregates: dict = {}
1756
+ self.models = None
1757
+ self.stats_key = None
1758
+ self.cycles: dict = {}
1759
+ self.usable = False
1760
+
1761
+ def _cold(self):
1762
+ self.path_extents = {}
1763
+ self.bulk_boundaries = {}
1764
+ self.aggregates = {}
1765
+ self.models = None
1766
+ self.cycles = {}
1767
+
1768
+ def sync(self, cache_conn, stats_key) -> None:
1769
+ """Bring the memo to ``cache_conn``'s state before an index build."""
1770
+ ident = _codex_cache_file_identity(cache_conn)
1771
+ seq = _codex_accounting_seq(cache_conn)
1772
+ log_seq = _codex_quota_log_seq(cache_conn)
1773
+ snapshot_seq = _codex_snapshot_seq(cache_conn)
1774
+ if stats_key != self.stats_key:
1775
+ self.cycles = {}
1776
+ self.stats_key = stats_key
1777
+ if (ident is None or seq is None or log_seq is None
1778
+ or snapshot_seq is None):
1779
+ self.usable = False
1780
+ self.cache_ident = None
1781
+ self._cold()
1782
+ return
1783
+ cold = (ident != self.cache_ident or self.accounting_seq is None
1784
+ or seq < self.accounting_seq)
1785
+ if not cold and seq > self.accounting_seq:
1786
+ cold = not self._apply_accounting_changes(cache_conn, seq)
1787
+ if cold:
1788
+ self._cold()
1789
+ try:
1790
+ self.path_extents = {
1791
+ (str(root or ""), str(path)): (lo, hi)
1792
+ for root, path, lo, hi in cache_conn.execute(
1793
+ "SELECT source_root_key, source_path, "
1794
+ " MIN(timestamp_utc), MAX(timestamp_utc) "
1795
+ "FROM codex_session_entries "
1796
+ "GROUP BY source_root_key, source_path")
1797
+ }
1798
+ except sqlite3.Error:
1799
+ self.usable = False
1800
+ self.cache_ident = None
1801
+ return
1802
+ elif ((log_seq, snapshot_seq)
1803
+ != (self.quota_log_seq, self.snapshot_seq)
1804
+ and not self._apply_quota_appends(
1805
+ cache_conn, log_seq, snapshot_seq)):
1806
+ self.bulk_boundaries = {}
1807
+ self.cache_ident = ident
1808
+ self.accounting_seq = seq
1809
+ self.quota_log_seq = log_seq
1810
+ self.snapshot_seq = snapshot_seq
1811
+ self.usable = True
1812
+ for store in (self.aggregates, self.cycles):
1813
+ if len(store) > self._MAX_ENTRIES:
1814
+ store.clear()
1815
+
1816
+ def _apply_quota_appends(self, cache_conn, log_seq, snapshot_seq) -> bool:
1817
+ """Fold appended snapshots into the memoized boundaries, or ``False``
1818
+ when the change since the last sync is not provably append-only."""
1819
+ if (self.quota_log_seq is None or self.snapshot_seq is None
1820
+ or log_seq < self.quota_log_seq
1821
+ or snapshot_seq < self.snapshot_seq):
1822
+ return False
1823
+ expected = log_seq - self.quota_log_seq
1824
+ if expected > self._MAX_DIRTY_PATHS * 8:
1825
+ return False
1826
+ try:
1827
+ rows = tuple(cache_conn.execute(
1828
+ "SELECT source_root_key, logical_limit_key, observed_slot, "
1829
+ " window_minutes, unixepoch(resets_at_utc), "
1830
+ " unixepoch(captured_at_utc), captured_at_utc, "
1831
+ " source_path, line_offset, account_key "
1832
+ "FROM quota_window_snapshots "
1833
+ "WHERE id > ? AND id <= ? AND source='codex'",
1834
+ (self.snapshot_seq, snapshot_seq)))
1835
+ except sqlite3.Error:
1836
+ return False
1837
+ if len(rows) != expected:
1838
+ return False
1839
+ for row in rows:
1840
+ bulk = self.bulk_boundaries.get(tuple(row[:4]))
1841
+ if bulk is not None and row[4] is not None:
1842
+ _codex_fold_boundary_row(bulk, tuple(row[4:]))
1843
+ return True
1844
+
1845
+ def _apply_accounting_changes(self, cache_conn, seq) -> bool:
1846
+ last = self.accounting_seq
1847
+ try:
1848
+ changes = tuple(cache_conn.execute(
1849
+ "SELECT mutation_seq, change_kind, source_root_key, source_path "
1850
+ "FROM codex_accounting_change_log "
1851
+ "WHERE mutation_seq > ? ORDER BY mutation_seq, seq", (last,)))
1852
+ except sqlite3.Error:
1853
+ return False
1854
+ if (not changes or int(changes[0][0]) > last + 1
1855
+ or int(changes[-1][0]) != seq
1856
+ or any(row[1] == "full" for row in changes)):
1857
+ return False
1858
+ dirty = {(str(row[2] or ""), str(row[3]))
1859
+ for row in changes if row[1] == "path" and row[3]}
1860
+ if len(dirty) > self._MAX_DIRTY_PATHS:
1861
+ return False
1862
+ intervals: dict = {}
1863
+ try:
1864
+ for root, path in dirty:
1865
+ old = self.path_extents.pop((root, path), None)
1866
+ new = cache_conn.execute(
1867
+ "SELECT MIN(timestamp_utc), MAX(timestamp_utc) "
1868
+ "FROM codex_session_entries "
1869
+ "WHERE source_root_key=? AND source_path=?",
1870
+ (root, path)).fetchone()
1871
+ if new is not None and new[0] is not None:
1872
+ self.path_extents[(root, path)] = (new[0], new[1])
1873
+ intervals.setdefault(root, []).append((new[0], new[1]))
1874
+ if old is not None:
1875
+ intervals.setdefault(root, []).append(old)
1876
+ except sqlite3.Error:
1877
+ return False
1878
+ for key in tuple(self.aggregates):
1879
+ root, lo, hi = key[0], key[1], key[2]
1880
+ if any(a <= hi and b >= lo for a, b in intervals.get(root, ())):
1881
+ del self.aggregates[key]
1882
+ self.models = None
1883
+ return True
1884
+
1885
+ def boundary(self, cache_conn, parts, reset_epoch, account_scope):
1886
+ bulk = self.bulk_boundaries.get(parts)
1887
+ if bulk is None:
1888
+ bulk = self.bulk_boundaries[parts] = _codex_bulk_first_boundaries(
1889
+ cache_conn, parts)
1890
+ return _codex_boundary_from_bulk(bulk.get(reset_epoch), account_scope)
1891
+
1892
+ def entry_models(self, cache_conn) -> tuple:
1893
+ if self.models is None:
1894
+ self.models = _quota.codex_entry_models(cache_conn)
1895
+ return self.models
1896
+
1897
+
1898
+ _CODEX_CYCLE_INPUTS_MEMO = _CodexCycleInputsMemo()
1899
+
1900
+
1901
+ def _codex_first_boundary(cache_conn, member, account_scope, memo, used):
1902
+ """The physical tuple ``codex_quota_breakdown`` opens ``member``'s ladder
1903
+ from: the breakdown's own read, or with the index memo its bulk twin."""
1904
+ if memo is not None:
1905
+ key = ((member.root, member.limit, member.slot, member.window),
1906
+ math.floor(member.reset.timestamp()), account_scope)
1907
+ value = memo.boundary(cache_conn, *key)
1908
+ used.append(("b", key, value))
1909
+ return value
1910
+ ident = QuotaWindowIdentity(
1911
+ source="codex", source_root_key=member.root,
1912
+ logical_limit_key=member.limit, observed_slot=member.slot,
1913
+ window_minutes=member.window, account_key=member.account_key,
1914
+ limit_id=member.limit_id, limit_name=member.limit_name,
1915
+ )
1916
+ return _quota._first_block_physical_tuple(
1917
+ ident, member.reset, cache_conn=cache_conn, account_key=account_scope)
1918
+
1919
+
1920
+ def _codex_selection_aggregate(cache_conn, member, query_start, query_end,
1921
+ account_scope, memo, used, models_box):
1922
+ key = (member.root, query_start, query_end, account_scope)
1923
+ if memo is not None and key in memo.aggregates:
1924
+ value = memo.aggregates[key]
1925
+ used.append(("a", key, value))
1926
+ return value
1927
+ if memo is not None:
1928
+ models = memo.entry_models(cache_conn)
1929
+ else:
1930
+ # The model list is read once per helper call, not per selection.
1931
+ if not models_box:
1932
+ models_box.append(_quota.codex_entry_models(cache_conn))
1933
+ models = models_box[0]
1934
+ value = _quota.codex_entry_selection_aggregate(
1935
+ cache_conn, source_root_key=member.root, query_start=query_start,
1936
+ query_end=query_end, account_key=account_scope, models=models)
1937
+ if memo is not None:
1938
+ memo.aggregates[key] = value
1939
+ used.append(("a", key, value))
1940
+ return value
1941
+
1942
+
1943
+ def _codex_cycle_inputs(stats_conn, cache_conn, cyc, *, speed: str,
1944
+ account_scope: "str | None",
1945
+ _memo=None, _used=None) -> CodexCycleInputs:
1946
+ """Read every input of ``cyc``'s Codex detail (#1050 spec 4.7).
1947
+
1948
+ ``account_scope`` is the route's or the index's account focus — the scope
1949
+ the two cache reads use (``_union_cluster_milestones``) — never
1950
+ ``cyc.account_key``. ``speed`` is resolved here exactly as the breakdown
1951
+ resolves it, so the stamp records the tier the dollars were priced at.
1952
+
1953
+ ``cache_conn=None`` opens the breakdown's own read-only cache connection,
1954
+ exactly as ``codex_quota_breakdown`` does for such a caller; a missing
1955
+ cache reads every boundary as absent.
1956
+
1957
+ The reads keep the projection-gate classification of the reads they
1958
+ replace: the block rows still come from the gated
1959
+ ``_codex_five_hour_rows``, and ``quota_percent_milestones`` is not a
1960
+ projection family.
1961
+ """
1962
+ used = _used if _used is not None else []
1963
+ cycle_rows = _codex_cycle_raw_rows(stats_conn, cyc)
1964
+ raw_block_rows = _codex_five_hour_rows(stats_conn, cyc)
1965
+ clusters = tuple(_codex_five_hour_clusters(
1966
+ stats_conn, cyc, rows=raw_block_rows))
1967
+ selections = [(cyc.block_key, cyc)] + [(b.block_key, b) for b in clusters]
1968
+ owns_cache = cache_conn is None
1969
+ cache = cache_conn
1970
+ if owns_cache:
1971
+ try:
1972
+ cache = _quota._cache_connection()
1973
+ except (FileNotFoundError, sqlite3.Error):
1974
+ cache = None
1975
+ ladders: list = []
1976
+ boundaries: list = []
1977
+ aggregates: list = []
1978
+ models_box: list = []
1979
+ try:
1980
+ for selection_key, cluster in selections:
1981
+ for member in cluster.cluster_members:
1982
+ ident = list(_member_ident(member))
1983
+ rows = _codex_member_ladder_rows(stats_conn, member)
1984
+ ladders.append([selection_key, ident, [list(r) for r in rows]])
1985
+ if not rows:
1986
+ boundaries.append([selection_key, ident, "unread"])
1987
+ continue
1988
+ start = (None if cache is None else _codex_first_boundary(
1989
+ cache, member, account_scope, _memo, used))
1990
+ if start is None:
1991
+ boundaries.append([selection_key, ident, "absent"])
1992
+ continue
1993
+ boundaries.append([selection_key, ident, [
1994
+ start[0].astimezone(UTC).isoformat(), start[1], start[2]]])
1995
+ # The breakdown's ladder is ordered by threshold; its last row
1996
+ # bounds the accounting read.
1997
+ last = max(rows, key=lambda r: r[7])
1998
+ query_start, query_end = _quota.codex_entry_selection_bounds(
1999
+ start[0], last[8])
2000
+ count, max_rowid, per_model = _codex_selection_aggregate(
2001
+ cache, member, query_start, query_end, account_scope,
2002
+ _memo, used, models_box)
2003
+ aggregates.append([selection_key, ident, query_start,
2004
+ query_end, count, max_rowid,
2005
+ [list(m) for m in per_model]])
2006
+ accounting_seq = (None if cache is None
2007
+ else _codex_accounting_seq(cache))
2008
+ finally:
2009
+ if owns_cache and cache is not None:
2010
+ cache.close()
2011
+ return CodexCycleInputs(
2012
+ cycle_rows=cycle_rows,
2013
+ block_ladder_rows=tuple(ladders),
2014
+ block_rows=tuple(sorted(
2015
+ (tuple(r) for r in raw_block_rows), key=_codex_block_row_key)),
2016
+ first_boundaries=tuple(boundaries),
2017
+ selection_aggregates=tuple(aggregates),
2018
+ accounting_seq=accounting_seq,
2019
+ speed=_cctally()._resolve_codex_speed(speed),
2020
+ pricing_identity=_lib_pricing.live_codex_pricing_identity(),
2021
+ block_clusters=clusters,
2022
+ )
2023
+
2024
+
2025
+ def _codex_inputs_stats_digest(inputs: CodexCycleInputs) -> str:
2026
+ """The digest of everything but the per-tick scalars, so the index can
2027
+ memoize it per cycle and recombine on an accounting move."""
2028
+ return _canonical_digest([
2029
+ [list(r) for r in inputs.cycle_rows],
2030
+ list(inputs.block_ladder_rows),
2031
+ [list(r) for r in inputs.block_rows],
2032
+ list(inputs.first_boundaries),
2033
+ list(inputs.selection_aggregates),
2034
+ ])
2035
+
2036
+
2037
+ def _codex_content_component(selection_digest: str, accounting_seq, speed,
2038
+ pricing_identity) -> str:
2039
+ """#1050 spec 4.7: the stamp's content component — SHA-256 over the
2040
+ canonical JSON of every stats row and cost input, the rows entering
2041
+ through their own SHA-256 so an accounting move recombines without
2042
+ re-hashing every row."""
2043
+ return "i" + _canonical_digest([
2044
+ selection_digest, accounting_seq, speed, pricing_identity])[:16]
2045
+
2046
+
2047
+ def _codex_entry_dict(cyc, tz, milestone_count, block_count, max_captured,
2048
+ component) -> dict:
1365
2049
  key = cyc.key
1366
2050
  return {
1367
2051
  "key": key,
@@ -1373,11 +2057,70 @@ def _codex_cycle_entry(stats_conn, cyc, tz) -> dict:
1373
2057
  "milestone_count": milestone_count,
1374
2058
  "block_count": block_count,
1375
2059
  "detail_stamp": _mh.compute_detail_stamp(
1376
- key, milestone_count, block_count, max_captured
2060
+ key, milestone_count, block_count, max_captured, component,
1377
2061
  ),
1378
2062
  }
1379
2063
 
1380
2064
 
2065
+ def _codex_stamp_parts(inputs: CodexCycleInputs) -> tuple:
2066
+ """``(milestone_count, block_count, max_captured)``: the distinct
2067
+ thresholds and the raw maximum capture over the cycle's raw rows
2068
+ (independent of the detail's earliest-crossing pick), and the clusters."""
2069
+ milestone_count = len({row[7] for row in inputs.cycle_rows})
2070
+ max_captured = max((row[8] for row in inputs.cycle_rows), default=None)
2071
+ return milestone_count, len(inputs.block_clusters), max_captured
2072
+
2073
+
2074
+ def _codex_cycle_entry(stats_conn, cache_conn, cyc, tz, *, speed,
2075
+ account_scope, inputs=None, memo=None) -> dict:
2076
+ """The index entry for ``cyc``, stamped from `_codex_cycle_inputs`.
2077
+
2078
+ ``inputs`` lets the detail stamp from the read it renders from; ``memo``
2079
+ is the index's process memo (`_CodexCycleInputsMemo`)."""
2080
+ if inputs is not None or memo is None or not memo.usable:
2081
+ if inputs is None:
2082
+ inputs = _codex_cycle_inputs(
2083
+ stats_conn, cache_conn, cyc, speed=speed,
2084
+ account_scope=account_scope)
2085
+ milestone_count, block_count, max_captured = _codex_stamp_parts(inputs)
2086
+ component = _codex_content_component(
2087
+ _codex_inputs_stats_digest(inputs), inputs.accounting_seq,
2088
+ inputs.speed, inputs.pricing_identity)
2089
+ return _codex_entry_dict(cyc, tz, milestone_count, block_count,
2090
+ max_captured, component)
2091
+ cycle_key = (
2092
+ account_scope, cyc.key, _iso_z(cyc.start), _iso_z(cyc.end),
2093
+ bool(cyc.is_current),
2094
+ tuple(_member_ident(m) for m in cyc.cluster_members),
2095
+ )
2096
+ cached = memo.cycles.get(cycle_key)
2097
+ # Both kinds revalidate by VALUE. Presence alone is not enough for an
2098
+ # aggregate: two cycles can share one key (a 5-hour block straddling
2099
+ # their boundary), and after a late entry evicts it the first cycle
2100
+ # recomputes and reinserts it, so the second would find the key present
2101
+ # and keep a digest of the old value (#1050 spec 4.7 stamp equality).
2102
+ if cached is not None and all(
2103
+ (memo.boundary(cache_conn, *key) == value)
2104
+ if kind == "b"
2105
+ else (key in memo.aggregates and memo.aggregates[key] == value)
2106
+ for kind, key, value in cached[4]):
2107
+ milestone_count, block_count, max_captured, selection_digest, _ = cached
2108
+ else:
2109
+ used: list = []
2110
+ inputs = _codex_cycle_inputs(
2111
+ stats_conn, cache_conn, cyc, speed=speed,
2112
+ account_scope=account_scope, _memo=memo, _used=used)
2113
+ milestone_count, block_count, max_captured = _codex_stamp_parts(inputs)
2114
+ selection_digest = _codex_inputs_stats_digest(inputs)
2115
+ memo.cycles[cycle_key] = (milestone_count, block_count, max_captured,
2116
+ selection_digest, tuple(used))
2117
+ component = _codex_content_component(
2118
+ selection_digest, memo.accounting_seq,
2119
+ _cctally()._resolve_codex_speed(speed),
2120
+ _lib_pricing.live_codex_pricing_identity())
2121
+ return _codex_entry_dict(cyc, tz, milestone_count, block_count,
2122
+ max_captured, component)
2123
+
1381
2124
  def _codex_cycle_label(cyc, tz) -> str:
1382
2125
  start_iso = cyc.start.astimezone(UTC).isoformat()
1383
2126
  end_iso = cyc.end.astimezone(UTC).isoformat()
@@ -1389,14 +2132,34 @@ def _codex_cycle_label(cyc, tz) -> str:
1389
2132
 
1390
2133
 
1391
2134
  def build_codex_cycle_index(stats_conn, *, identity, now_utc,
1392
- account_key=None) -> list:
2135
+ account_key=None, cache_conn=None, speed="auto",
2136
+ stats_digest=None) -> list:
1393
2137
  """Newest-first Codex cycle index over the durable projection (spec §1c,
1394
2138
  §3). Enumerates the hero root's reset-defined ledger with no depth cap;
1395
2139
  one selected full native identity per physical reset.
1396
2140
 
1397
2141
  ``account_key`` (#416 Slice 3A review B3) builds the index for ONE account,
1398
2142
  so a focused hero renders its own milestone history instead of the first
1399
- account's. ``None`` is the merged parent index and is byte-stable."""
2143
+ account's. ``None`` is the merged parent index.
2144
+
2145
+ ``cache_conn`` and ``speed`` (#1050 spec 4.7) are the cost inputs every
2146
+ stamp now names, read exactly as the detail route reads them, with
2147
+ ``account_key`` as the account scope; ``cache_conn=None`` opens the
2148
+ breakdown's own read-only connection, as the detail does for such a caller.
2149
+ ``stats_digest`` is the caller's `codex_stats_digest` of the generation it
2150
+ builds from: given together with ``cache_conn`` it admits the process memo
2151
+ (`_CodexCycleInputsMemo`), which the source build relies on to keep the
2152
+ index's cost near what it was before the stamps carried every input.
2153
+ Precondition: the memo keys its per-cycle results on ``stats_digest`` and
2154
+ reuses them without re-reading the stats rows they came from, so a caller
2155
+ supplying it must guarantee that a result built from rows newer than the
2156
+ digest is never published as the digest's. Either ``stats_conn`` reads
2157
+ the same pinned snapshot the digest was computed in, or the call sits
2158
+ inside a before/after validation that re-reads the digest and discards
2159
+ the build on movement. The dashboard source build is the second kind: it
2160
+ leaves stats.db in statement-scoped autocommit (#393) and its
2161
+ reconciliation re-reads ``codex_stats_digest`` after construction
2162
+ (`_cctally_tui._tui_build_source_bundle`, `stats_generation_moved`)."""
1400
2163
  now = now_utc.astimezone(UTC) if now_utc.tzinfo else now_utc.replace(tzinfo=UTC)
1401
2164
  tz = _resolve_display_tz()
1402
2165
  cycles = _load_codex_cycles(
@@ -1404,8 +2167,26 @@ def build_codex_cycle_index(stats_conn, *, identity, now_utc,
1404
2167
  preferred_identity=getattr(identity, "quota_identity", None),
1405
2168
  current_boundary=identity, now_utc=now, account_key=account_key,
1406
2169
  )
1407
- return [_codex_cycle_entry(stats_conn, c, tz) for c in cycles]
2170
+ if cache_conn is None or stats_digest is None:
2171
+ return [
2172
+ _codex_cycle_entry(stats_conn, cache_conn, c, tz, speed=speed,
2173
+ account_scope=account_key)
2174
+ for c in cycles
2175
+ ]
2176
+ memo = _CODEX_CYCLE_INPUTS_MEMO
2177
+ with memo.lock:
2178
+ memo.sync(cache_conn, stats_digest)
2179
+ return [
2180
+ _codex_cycle_entry(stats_conn, cache_conn, c, tz, speed=speed,
2181
+ account_scope=account_key, memo=memo)
2182
+ for c in cycles
2183
+ ]
2184
+
1408
2185
 
2186
+ def reset_codex_cycle_inputs_memo() -> None:
2187
+ """Drop the index's process memo (tests and measurements)."""
2188
+ global _CODEX_CYCLE_INPUTS_MEMO
2189
+ _CODEX_CYCLE_INPUTS_MEMO = _CodexCycleInputsMemo()
1409
2190
 
1410
2191
  def _shape_codex_milestone(row, *, key_parts, block_key) -> dict:
1411
2192
  captured_iso = row.captured_at.astimezone(UTC).isoformat()
@@ -1506,9 +2287,13 @@ def _union_cluster_milestones(cluster, block_key, speed, cache_conn, stats_conn,
1506
2287
 
1507
2288
 
1508
2289
  def _codex_cycle_blocks(stats_conn, cache_conn, cyc, speed,
1509
- account_key=None) -> "list | None":
2290
+ account_key=None, clusters=None) -> "list | None":
2291
+ """``clusters`` (#1050): the block clusters `_codex_cycle_inputs` read, so
2292
+ the detail renders the population its stamp names."""
1510
2293
  out: list = []
1511
- for block in _codex_five_hour_clusters(stats_conn, cyc):
2294
+ if clusters is None:
2295
+ clusters = _codex_five_hour_clusters(stats_conn, cyc)
2296
+ for block in clusters:
1512
2297
  block_key = block.block_key
1513
2298
  milestones = _union_cluster_milestones(
1514
2299
  block, block_key, speed, cache_conn, stats_conn,
@@ -1585,6 +2370,15 @@ def build_codex_cycle_detail(
1585
2370
  return (None, "pruned")
1586
2371
  return (None, "unknown")
1587
2372
 
2373
+ # #1050 spec 4.7: ONE read of every input, stamped and rendered from. The
2374
+ # ladders' dollars still come from `codex_quota_breakdown`, over the same
2375
+ # rows: the route holds one read transaction per database around all of it.
2376
+ try:
2377
+ inputs = _codex_cycle_inputs(
2378
+ stats_conn, cache_conn, match, speed=speed,
2379
+ account_scope=account_key)
2380
+ except sqlite3.Error:
2381
+ return (None, "projection_incoherent")
1588
2382
  milestones = _union_cluster_milestones(
1589
2383
  match, match.block_key, speed, cache_conn, stats_conn,
1590
2384
  account_key=account_key,
@@ -1592,11 +2386,14 @@ def build_codex_cycle_detail(
1592
2386
  if milestones is None:
1593
2387
  return (None, "projection_incoherent")
1594
2388
  blocks = _codex_cycle_blocks(
1595
- stats_conn, cache_conn, match, speed, account_key=account_key)
2389
+ stats_conn, cache_conn, match, speed, account_key=account_key,
2390
+ clusters=inputs.block_clusters)
1596
2391
  if blocks is None:
1597
2392
  return (None, "projection_incoherent")
1598
2393
 
1599
- entry = _codex_cycle_entry(stats_conn, match, tz)
2394
+ entry = _codex_cycle_entry(
2395
+ stats_conn, cache_conn, match, tz, speed=speed,
2396
+ account_scope=account_key, inputs=inputs)
1600
2397
  return {
1601
2398
  "source": "codex",
1602
2399
  "key": entry["key"],