cctally 1.114.0 → 1.115.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/bin/_cctally_account.py +32 -8
  3. package/bin/_cctally_cache.py +4394 -279
  4. package/bin/_cctally_completion.py +1287 -0
  5. package/bin/_cctally_config.py +37 -0
  6. package/bin/_cctally_core.py +178 -2
  7. package/bin/_cctally_dashboard.py +2 -2
  8. package/bin/_cctally_dashboard_conversation.py +292 -23
  9. package/bin/_cctally_dashboard_envelope.py +16 -3
  10. package/bin/_cctally_dashboard_sources.py +18 -9
  11. package/bin/_cctally_db.py +316 -5
  12. package/bin/_cctally_doctor.py +436 -27
  13. package/bin/_cctally_five_hour.py +82 -31
  14. package/bin/_cctally_forecast.py +81 -9
  15. package/bin/_cctally_journal.py +3884 -112
  16. package/bin/_cctally_journal_repair.py +101 -12
  17. package/bin/_cctally_parser.py +42 -0
  18. package/bin/_cctally_project.py +8 -2
  19. package/bin/_cctally_quarantine.py +652 -0
  20. package/bin/_cctally_quota.py +26 -4
  21. package/bin/_cctally_quota_model.py +360 -83
  22. package/bin/_cctally_record.py +677 -149
  23. package/bin/_cctally_rederive.py +297 -32
  24. package/bin/_cctally_refresh.py +84 -13
  25. package/bin/_cctally_source_analytics.py +23 -2
  26. package/bin/_cctally_statusline.py +6137 -517
  27. package/bin/_cctally_store.py +287 -48
  28. package/bin/_cctally_transcript.py +8 -7
  29. package/bin/_cctally_tui.py +57 -53
  30. package/bin/_lib_account_binding.py +152 -0
  31. package/bin/_lib_account_protocol.py +386 -0
  32. package/bin/_lib_aggregators.py +4 -0
  33. package/bin/_lib_cache_coverage.py +13 -1
  34. package/bin/_lib_cache_writer_lock.py +19 -3
  35. package/bin/_lib_clone_copy.py +142 -0
  36. package/bin/_lib_codex_conversation_query.py +133 -19
  37. package/bin/_lib_codex_conversation_watch.py +59 -14
  38. package/bin/_lib_codex_drain_admission.py +214 -0
  39. package/bin/_lib_codex_metadata.py +18 -1
  40. package/bin/_lib_codex_publication_barrier.py +277 -0
  41. package/bin/_lib_codex_rollout_container.py +502 -0
  42. package/bin/_lib_conversation_dispatch.py +3 -0
  43. package/bin/_lib_conversation_query.py +11 -3
  44. package/bin/_lib_detector_access_registry.py +3817 -0
  45. package/bin/_lib_doctor.py +452 -1
  46. package/bin/_lib_five_hour.py +126 -0
  47. package/bin/_lib_ingest_frontier.py +414 -20
  48. package/bin/_lib_journal.py +141 -10
  49. package/bin/_lib_jsonl.py +5 -4
  50. package/bin/_lib_legacy_spool.py +126 -0
  51. package/bin/_lib_legacy_writer_evidence.py +136 -0
  52. package/bin/_lib_merge_gate.py +29 -302
  53. package/bin/_lib_meter_rate_change.py +10 -0
  54. package/bin/_lib_outcome_quarantine.py +690 -0
  55. package/bin/_lib_pricing.py +7 -2
  56. package/bin/_lib_quarantine.py +154 -0
  57. package/bin/_lib_quota_calibration.py +53 -8
  58. package/bin/_lib_quota_copy.py +2 -1
  59. package/bin/_lib_quota_model.py +288 -46
  60. package/bin/_lib_rederive.py +94 -5
  61. package/bin/_lib_segment_summary.py +35 -2
  62. package/bin/_lib_statusline_candidates.py +292 -23
  63. package/bin/cctally +23 -1
  64. package/package.json +14 -1
@@ -65,6 +65,7 @@ from _lib_dashboard_json import (
65
65
  encode_dashboard_json_bytes_capped,
66
66
  )
67
67
  from _lib_retained_size import retained_size_bytes
68
+ import _lib_codex_rollout_container as _rc
68
69
 
69
70
 
70
71
  def _advance_live_tail_accounting(handler, changed, sync, label):
@@ -443,40 +444,280 @@ def _cached_file_sigs(conn, paths):
443
444
  return out
444
445
 
445
446
 
447
+ #: The cursor columns a Codex live-tail signature is computed from (#968 5.9).
448
+ _CODEX_WATCH_SIG_COLUMNS = (
449
+ "path, size_bytes, container_kind, container_size_bytes, mtime_ns, "
450
+ "device_id, inode, pending_container_fingerprint")
451
+
452
+
453
+ def _codex_watch_kernel():
454
+ return sys.modules["cctally"]._load_sibling("_lib_codex_conversation_watch")
455
+
456
+
457
+ def _codex_acknowledged_sig(row):
458
+ """The live-tail signature one ``codex_conversation_source_files`` row
459
+ acknowledges (#968 spec 5.9).
460
+
461
+ A container already recorded as pending (or unverified) work is
462
+ acknowledged at its recorded fingerprint: the verification executor owns
463
+ it, and rescheduling it on every poll is the non-convergence I9 forbids.
464
+ Otherwise the accepted container's signature: ``("plain", size)`` for
465
+ plain, which is today's committed size, or the container identity for a
466
+ zstd container.
467
+ """
468
+ _path, size, kind, csize, mtime_ns, device, inode, pending = row
469
+ if pending:
470
+ fingerprint = _rc.ContainerFingerprint.from_json(pending)
471
+ if fingerprint is not None:
472
+ return fingerprint.watch_sig()
473
+ return _codex_watch_kernel().committed_watch_sig(
474
+ kind, size, csize, mtime_ns, device, inode)
475
+
476
+
446
477
  def _codex_cached_file_sigs(conn, paths):
447
- """{path: size_bytes} from ``codex_conversation_source_files`` for the given paths — the
448
- Codex analogue of ``_cached_file_sigs`` (spec §5.2). Size-only, matching the
449
- watch kernel's size-only signature and ``sync_codex_cache``'s size-only
450
- delta. Paths with no row are absent → treated as changed. Baselines the
451
- Codex live-tail watch against the cache's own committed cursor so growth
452
- during an ingest is re-detected next cycle (the per-path committed-cursor
453
- invariant — the S6 physical mutation sequence is at most an extra
454
- certificate, never this cursor's replacement)."""
478
+ """{path: acknowledged signature} from ``codex_conversation_source_files``
479
+ for the given paths — the Codex analogue of ``_cached_file_sigs`` (spec
480
+ §5.2), in the container-aware shape the Codex observation seam returns
481
+ (#968 spec 5.9). Paths with no row are absent → treated as changed.
482
+ Baselines the Codex live-tail watch against the cache's own committed
483
+ cursor so growth during an ingest is re-detected next cycle (the per-path
484
+ committed-cursor invariant — the S6 physical mutation sequence is at most
485
+ an extra certificate, never this cursor's replacement)."""
455
486
  out = {}
456
487
  if not paths:
457
488
  return out
458
489
  placeholders = ",".join("?" for _ in paths)
459
490
  try:
460
491
  rows = conn.execute(
461
- f"SELECT path, size_bytes FROM codex_conversation_source_files "
492
+ f"SELECT {_CODEX_WATCH_SIG_COLUMNS} "
493
+ f"FROM codex_conversation_source_files "
462
494
  f"WHERE path IN ({placeholders})", list(paths)).fetchall()
463
495
  except sqlite3.OperationalError:
464
496
  return out
465
- for p, size in rows:
466
- out[p] = size
497
+ for row in rows:
498
+ out[row[0]] = _codex_acknowledged_sig(row)
467
499
  return out
468
500
 
469
501
 
470
502
  def _codex_all_committed_sizes(conn):
471
- """{path: size_bytes} for EVERY tracked Codex file — fed to the frontier as
472
- both the ``known_paths`` diff set and the ``committed_sizes`` growth
473
- baseline. A plain SELECT (no lock), so it never widens the SSE lock scope."""
503
+ """{path: acknowledged signature} for EVERY tracked Codex file — fed to the
504
+ frontier as both the ``known_paths`` diff set and the ``committed_sizes``
505
+ growth baseline, which the kernel compares with ``codex_sig_advanced``
506
+ (#968 spec 5.9). A plain SELECT (no lock), so it never widens the SSE lock
507
+ scope."""
474
508
  try:
475
509
  rows = conn.execute(
476
- "SELECT path, size_bytes FROM codex_conversation_source_files").fetchall()
510
+ f"SELECT {_CODEX_WATCH_SIG_COLUMNS} "
511
+ "FROM codex_conversation_source_files").fetchall()
477
512
  except sqlite3.OperationalError:
478
513
  return {}
479
- return {p: size for (p, size) in rows}
514
+ return {row[0]: _codex_acknowledged_sig(row) for row in rows}
515
+
516
+
517
+ def _codex_seal_token(conn):
518
+ """The publication barrier's seal token (Task 11's ``SEAL_SEQ_KEY``), read
519
+ through the attached cache; ``None`` when unreadable, which never re-arms
520
+ anything."""
521
+ barrier = sys.modules["cctally"]._load_sibling(
522
+ "_lib_codex_publication_barrier")
523
+ try:
524
+ row = conn.execute(
525
+ "SELECT value FROM cache_db.cache_meta WHERE key=?",
526
+ (barrier.SEAL_SEQ_KEY,)).fetchone()
527
+ except sqlite3.Error:
528
+ return None
529
+ return None if row is None else row[0]
530
+
531
+
532
+ @dataclasses.dataclass
533
+ class _CodexWatchIngest:
534
+ """Stream-local observations, separate from the durable ingest result.
535
+
536
+ ``stats.targeted_clean`` remains authoritative for ingest completion. A
537
+ held container can acknowledge its observed signature without certifying
538
+ any accounting or transcript work as complete (T12-INTEG01).
539
+ """
540
+
541
+ stats: object
542
+ accounting_blocked: bool = False
543
+ acknowledged: dict = dataclasses.field(default_factory=dict)
544
+ observation_visible: bool = False
545
+
546
+ def __getattr__(self, name):
547
+ return getattr(self.stats, name)
548
+
549
+
550
+ def _codex_observation_hold_only(stats, *, executor_owned=False):
551
+ """Recognize container holds without overlooking an actual ingest loss.
552
+
553
+ This is only admission to a stream-local observation acknowledgment,
554
+ never a replacement for ``targeted_clean`` or a frontier certificate.
555
+ Unknown skipped reasons and every failed/contended ingest stay dirty.
556
+ """
557
+ if any(getattr(stats, name, False) for name in (
558
+ "lock_contended", "deferred_reason", "files_failed",
559
+ "files_deferred_torn", "files_deferred_reset", "maintenance_failed",
560
+ "budget_exhausted", "drain_yielded", "rebuild_refused")):
561
+ return False
562
+ if getattr(stats, "barrier_retry", False) and not executor_owned:
563
+ return False
564
+ skipped = getattr(stats, "container_skipped", {})
565
+ return (bool(getattr(stats, "container_pending", 0) or any(skipped.values()))
566
+ and set(skipped) <= {
567
+ "decoder-unavailable", "decode-error", "read-error",
568
+ "unstable-stat", "verification-pending", "verification-unverified"})
569
+
570
+
571
+ def _codex_watch_probes(conn):
572
+ """Container observations and committed cursors for both Codex polls.
573
+
574
+ Pending fingerprints acknowledge executor-owned work. A container held
575
+ without a recorded fingerprint acknowledges the pre-ingest observation,
576
+ so replacement during ingest is still detected. These are observation
577
+ acknowledgments only: durable pending work remains dirty.
578
+
579
+ A scoped accounting hold cannot run transcript ingest. It acknowledges
580
+ the observation quietly and retries once accounting accepts that exact
581
+ container. Failed or contended accounting remains unacknowledged. Plain
582
+ barrier-deferred files wait at their pre-ingest observation until a real
583
+ file change or a readable new seal token re-arms them once.
584
+ """
585
+ observed: dict = {}
586
+ deferred: dict = {} # path -> (pre-ingest signature, seal token)
587
+ accounting_wait: dict = {} # path -> observed container awaiting accounting
588
+ rearmed: set = set()
589
+
590
+ def accounting_accepted(path, sig):
591
+ try:
592
+ row = conn.execute(
593
+ f"SELECT {_CODEX_WATCH_SIG_COLUMNS} "
594
+ "FROM cache_db.codex_session_files "
595
+ "WHERE path=? AND container_state='accepted'", (path,)).fetchone()
596
+ except sqlite3.Error:
597
+ return False
598
+ return row is not None and _codex_acknowledged_sig(row) == sig
599
+
600
+ def executor_owns_observations(paths):
601
+ # Publication-unit planning can encounter the same pending container
602
+ # as phase one and set barrier_retry. Acknowledge only when EVERY
603
+ # changed path has durable executor ownership of the exact observation;
604
+ # a failed plain/accepted contributor must keep the whole poll retrying.
605
+ if not paths:
606
+ return False
607
+ marks = ",".join("?" for _ in paths)
608
+ try:
609
+ rows = conn.execute(
610
+ "SELECT path,pending_container_fingerprint "
611
+ "FROM codex_conversation_source_files "
612
+ f"WHERE path IN ({marks}) AND container_state='pending'",
613
+ list(paths)).fetchall()
614
+ except sqlite3.Error:
615
+ return False
616
+ if len(rows) != len(paths):
617
+ return False
618
+ for path, pending in rows:
619
+ fingerprint = _rc.ContainerFingerprint.from_json(pending)
620
+ before = observed.get(path)
621
+ if (fingerprint is None or before is None
622
+ or before[0] != _rc.ZSTD
623
+ or fingerprint.watch_sig() != before):
624
+ return False
625
+ return True
626
+
627
+ def stat_fn(path):
628
+ sig = _rc.observe_watch_sig(path)
629
+ observed[path] = sig
630
+ waiting = accounting_wait.get(path)
631
+ if waiting is not None:
632
+ if sig != waiting:
633
+ del accounting_wait[path]
634
+ elif accounting_accepted(path, sig):
635
+ del accounting_wait[path]
636
+ rearmed.add(path)
637
+ held = deferred.get(path)
638
+ if held is not None:
639
+ token = _codex_seal_token(conn)
640
+ if sig != held[0] or (token is not None and token != held[1]):
641
+ del deferred[path]
642
+ if sig == held[0]:
643
+ rearmed.add(path)
644
+ if path in rearmed and sig is not None:
645
+ return ("rearm",) + tuple(sig)
646
+ return sig
647
+
648
+ def committed_sig(path):
649
+ rearmed.discard(path) # reached only after a clean ingest
650
+ held = deferred.get(path)
651
+ if held is not None:
652
+ return held[0]
653
+ acknowledged = _codex_cached_file_sigs(conn, [path]).get(path)
654
+ before = observed.get(path)
655
+ if (before is not None and before[0] == _rc.ZSTD
656
+ and acknowledged != before):
657
+ return before
658
+ return acknowledged
659
+
660
+ def settle_ingest(ingest):
661
+ def settled(changed):
662
+ token = _codex_seal_token(conn)
663
+ result = ingest(changed)
664
+ if isinstance(result, _CodexWatchIngest):
665
+ stats = result.stats
666
+ else:
667
+ stats = result
668
+ result = _CodexWatchIngest(stats)
669
+ clean = getattr(stats, "targeted_clean", False)
670
+ executor_owned = (
671
+ getattr(stats, "barrier_retry", False)
672
+ and not result.accounting_blocked
673
+ and not getattr(stats, "barrier_files_deferred", 0)
674
+ and executor_owns_observations(changed))
675
+ held_only = _codex_observation_hold_only(
676
+ stats, executor_owned=executor_owned)
677
+ if not clean and not held_only:
678
+ return result
679
+ acknowledged = _codex_cached_file_sigs(conn, list(changed))
680
+ plain_deferred = 0
681
+ for path in changed:
682
+ before = observed.get(path)
683
+ if before is None:
684
+ continue
685
+ if before[0] == _rc.ZSTD and held_only:
686
+ # The pass actually observed this container, even if the
687
+ # verification executor still owes its durable work.
688
+ result.acknowledged[path] = before
689
+ rearmed.discard(path)
690
+ if result.accounting_blocked:
691
+ accounting_wait[path] = before
692
+ elif (not result.accounting_blocked
693
+ and getattr(stats, "barrier_files_deferred", 0)
694
+ and before[0] != _rc.ZSTD
695
+ and _codex_watch_kernel().codex_sig_advanced(
696
+ before, acknowledged.get(path))):
697
+ # Phase-one deferrals can publish in phase two. Only a
698
+ # committed cursor behind the probe still owes this work;
699
+ # ingestion may already have consumed a later append.
700
+ deferred[path] = (before, token)
701
+ result.acknowledged[path] = before
702
+ plain_deferred += 1
703
+ elif not result.accounting_blocked and not clean:
704
+ # A mixed pass may have committed a plain contributor
705
+ # while holding a container. Keep its committed cursor.
706
+ sig = acknowledged.get(path)
707
+ if sig is not None:
708
+ result.acknowledged[path] = sig
709
+ if (plain_deferred == len(changed)
710
+ and not getattr(stats, "targeted_visible", False)):
711
+ stats.targeted_visible = False
712
+ result.observation_visible = (
713
+ not clean and bool(result.acknowledged)
714
+ and not result.accounting_blocked
715
+ and not getattr(stats, "barrier_retry", False)
716
+ and bool(getattr(stats, "targeted_visible", True)))
717
+ return result
718
+ return settled
719
+
720
+ return stat_fn, committed_sig, settle_ingest
480
721
 
481
722
 
482
723
  def _codex_classified_paths(conn, paths):
@@ -1220,7 +1461,7 @@ def _send_sse_headers(handler) -> None:
1220
1461
 
1221
1462
  def _run_conversation_events_stream(
1222
1463
  handler, conn, *, passive, tail_data, resolve, ingest, cached_sigs,
1223
- discovery_step=None,
1464
+ discovery_step=None, stat_fn=None, committed_sig=None,
1224
1465
  ) -> None:
1225
1466
  """Shared per-conversation live-tail SSE loop (spec §5.2), driving both the
1226
1467
  bare (``sessionId``) and qualified (``conversationKey``) streams from ONE
@@ -1236,9 +1477,18 @@ def _run_conversation_events_stream(
1236
1477
  ~10-cycle Codex child-discovery frontier (``None`` for Claude — a bare or
1237
1478
  qualified-Claude stream never runs it). The cache lock is held ONLY inside
1238
1479
  ``ingest`` / ``discovery_step``, never across a sleep (the #297 WAL
1239
- discipline)."""
1480
+ discipline).
1481
+
1482
+ ``stat_fn`` / ``committed_sig`` are the provider's observation seam
1483
+ (#968 spec 5.9): Codex passes ``_codex_watch_probes`` so both poll sites
1484
+ compare one container-aware signature shape; ``None`` keeps the kernel's
1485
+ literal-path size probe and the committed-cursor lookup through
1486
+ ``cached_sigs`` (Claude, unchanged).
1487
+ """
1240
1488
  import time as _time
1241
1489
  watch = sys.modules["cctally"]._load_sibling("_lib_conversation_watch")
1490
+ probe = stat_fn or watch.file_sig
1491
+ committed = committed_sig or (lambda p: cached_sigs([p]).get(p))
1242
1492
  tail_frame = (
1243
1493
  "event: tail\ndata: " + encode_dashboard_json(tail_data) + "\n\n"
1244
1494
  ).encode("utf-8")
@@ -1286,12 +1536,26 @@ def _run_conversation_events_stream(
1286
1536
  if _request_peer_gone(handler, timeout=_LIVE_TAIL_POLL_INTERVAL):
1287
1537
  return
1288
1538
  cycles += 1
1289
- changed = watch.changed_paths(files, seen)
1539
+ changed = watch.changed_paths(files, seen, probe)
1290
1540
  if changed:
1291
1541
  _time.sleep(_LIVE_TAIL_DEBOUNCE)
1542
+ observation = None
1543
+
1544
+ def poll_ingest(changed):
1545
+ nonlocal observation
1546
+ observation = ingest(changed)
1547
+ return observation
1548
+
1292
1549
  new_seen, emitted = watch.watch_step(
1293
- files, seen, ingest_fn=ingest,
1294
- committed_sig_fn=lambda p: cached_sigs([p]).get(p))
1550
+ files, seen, stat_fn=probe, ingest_fn=poll_ingest,
1551
+ committed_sig_fn=committed)
1552
+ if isinstance(observation, _CodexWatchIngest):
1553
+ # A held container was observed, not successfully ingested.
1554
+ # Keep that distinction out of the generic watch kernel and
1555
+ # out of every durable cursor/certification decision.
1556
+ new_seen = dict(new_seen)
1557
+ new_seen.update(observation.acknowledged)
1558
+ emitted = emitted or observation.observation_visible
1295
1559
  seen = new_seen
1296
1560
  if emitted:
1297
1561
  handler.wfile.write(tail_frame)
@@ -1643,7 +1907,8 @@ def _qualified_conversation_events(
1643
1907
  finally:
1644
1908
  accounting.close()
1645
1909
  if not accounting_stats.targeted_clean:
1646
- return accounting_stats
1910
+ return _CodexWatchIngest(
1911
+ accounting_stats, accounting_blocked=True)
1647
1912
  writer = sys.modules["_cctally_dashboard"].open_conversations_db()
1648
1913
  try:
1649
1914
  stats = sync_codex_conversations(writer, only_paths=set(changed))
@@ -1658,6 +1923,8 @@ def _qualified_conversation_events(
1658
1923
  return stats
1659
1924
 
1660
1925
  cached = lambda paths: _codex_cached_file_sigs(conn, paths)
1926
+ stat_fn, committed_sig, settle_ingest = _codex_watch_probes(conn)
1927
+ _ingest = settle_ingest(_ingest)
1661
1928
  discovery = (
1662
1929
  None if account_key is not None
1663
1930
  else _make_codex_discovery_step(handler, conn, key, cq_codex)
@@ -1694,6 +1961,7 @@ def _qualified_conversation_events(
1694
1961
  return stats
1695
1962
 
1696
1963
  cached = lambda paths: _cached_file_sigs(conn, paths)
1964
+ stat_fn = committed_sig = None
1697
1965
  discovery = None
1698
1966
 
1699
1967
  try:
@@ -1701,7 +1969,8 @@ def _qualified_conversation_events(
1701
1969
  handler, conn, passive=passive,
1702
1970
  tail_data={"conversationKey": key},
1703
1971
  resolve=_resolve, ingest=_ingest, cached_sigs=cached,
1704
- discovery_step=discovery)
1972
+ discovery_step=discovery, stat_fn=stat_fn,
1973
+ committed_sig=committed_sig)
1705
1974
  except (BrokenPipeError, ConnectionResetError,
1706
1975
  ConnectionAbortedError, socket.timeout):
1707
1976
  pass
@@ -96,6 +96,7 @@ from _cctally_dashboard_cache_report import _cache_report_snapshot_to_dict
96
96
  # copy was already exactly this — collapse to the single definition).
97
97
  _ensure_sibling_loaded("_lib_json_envelope")
98
98
  from _lib_json_envelope import _iso_z
99
+ from _lib_five_hour import latest_five_hour_reading
99
100
 
100
101
 
101
102
  def _model_breakdowns_to_models(model_breakdowns: list[dict[str, Any]],
@@ -235,9 +236,17 @@ def _select_current_block_for_envelope(
235
236
  # statement of that key. Its `week_start_at` is a byte copy of the
236
237
  # non-held row it was derived from, so scoping the post-reset lookup
237
238
  # below by it is unaffected.
239
+ #
240
+ # #825/#832 spec 4.7 revision 7: the current window is the newest five-hour
241
+ # READING's, through the one shared selection under the same as-of and
242
+ # account restrictions. A retired replica (its five-hour fields cleared in
243
+ # place by a five-hour credit) or a weekly-only reading can be the newest
244
+ # row and names no window. The newest row still supplies `week_start_at`
245
+ # for the post-reset lookup below: that is weekly evidence, selected
246
+ # independently.
238
247
  snap = conn.execute(
239
248
  """
240
- SELECT five_hour_window_key, week_start_at
249
+ SELECT week_start_at
241
250
  FROM weekly_usage_snapshots
242
251
  WHERE captured_at_utc <= ?
243
252
  AND (? IS NULL OR account_key = ?)
@@ -246,7 +255,11 @@ def _select_current_block_for_envelope(
246
255
  """,
247
256
  (_iso_z(now_utc), account_key, account_key),
248
257
  ).fetchone()
249
- if snap is None or snap["five_hour_window_key"] is None:
258
+ if snap is None:
259
+ return None
260
+ reading = latest_five_hour_reading(
261
+ conn, account_key=account_key, as_of_utc=_iso_z(now_utc))
262
+ if reading is None:
250
263
  return None
251
264
 
252
265
  # The account supplied by `_tui_build_current_week` is the owner of the
@@ -298,7 +311,7 @@ def _select_current_block_for_envelope(
298
311
  ORDER BY id ASC
299
312
  LIMIT 1
300
313
  """,
301
- (snap["five_hour_window_key"], account_key, account_key,
314
+ (reading.window_key, account_key, account_key,
302
315
  _iso_z(now_utc)),
303
316
  ).fetchone()
304
317
  if block is None:
@@ -86,7 +86,10 @@ from _lib_codex_pools import (
86
86
  from _lib_codex_conversation import _display_title as _codex_display_title
87
87
  from _lib_fmt import stable_sum
88
88
  from _lib_aggregators import _aggregate_codex_buckets, codex_path_scope
89
- from _lib_five_hour import _FIVE_HOUR_JITTER_FLOOR_SECONDS
89
+ from _lib_five_hour import (
90
+ _FIVE_HOUR_JITTER_FLOOR_SECONDS,
91
+ latest_five_hour_reading,
92
+ )
90
93
  from _lib_source_analytics import (
91
94
  QualifiedCodexEntry,
92
95
  assign_collision_safe_project_labels,
@@ -6328,6 +6331,11 @@ def _claude_accounts_wire(
6328
6331
  # load-bearing, which is the same reason every other weekly consumer
6329
6332
  # excludes held rows rather than trusting it.
6330
6333
  #
6334
+ # The five-hour figure is the account's newest five-hour READING
6335
+ # (#825/#832 spec 4.7 revision 7, the one shared selection): a retired
6336
+ # replica or a weekly-only reading can be the newest row and carries
6337
+ # no five-hour value, so "the latest row" is not the latest reading.
6338
+ #
6331
6339
  # None is still returned only when the account retained NO row at all,
6332
6340
  # because the caller uses that to decide whether the unattributed
6333
6341
  # bucket appears.
@@ -6338,17 +6346,18 @@ def _claude_accounts_wire(
6338
6346
  "ORDER BY captured_at_utc DESC LIMIT 1",
6339
6347
  (key,),
6340
6348
  ).fetchone()
6341
- five_hour = stats_conn.execute(
6342
- "SELECT five_hour_percent FROM weekly_usage_snapshots "
6343
- "WHERE account_key=? "
6344
- "ORDER BY captured_at_utc DESC LIMIT 1",
6345
- (key,),
6346
- ).fetchone()
6349
+ five_hour = latest_five_hour_reading(stats_conn, account_key=key)
6347
6350
  if weekly is None and five_hour is None:
6348
- return None
6351
+ retained = stats_conn.execute(
6352
+ "SELECT 1 FROM weekly_usage_snapshots WHERE account_key=? "
6353
+ "LIMIT 1",
6354
+ (key,),
6355
+ ).fetchone()
6356
+ if retained is None:
6357
+ return None
6349
6358
  return (
6350
6359
  None if weekly is None else weekly[0],
6351
- None if five_hour is None else five_hour[0],
6360
+ None if five_hour is None else five_hour.percent,
6352
6361
  None if weekly is None else weekly[1],
6353
6362
  )
6354
6363