cctally 1.117.2 → 1.118.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +7 -3
  3. package/bin/_cctally_cache.py +2783 -175
  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
@@ -8,6 +8,8 @@ projection files, provider state, and alert dispatcher are never mutated.
8
8
 
9
9
  from __future__ import annotations
10
10
 
11
+ import bisect
12
+ import collections.abc
11
13
  import contextlib
12
14
  import copy
13
15
  import dataclasses
@@ -22,8 +24,10 @@ import signal
22
24
  import sqlite3
23
25
  import sys
24
26
  import tempfile
27
+ from array import array
25
28
  from dataclasses import dataclass
26
29
  from pathlib import Path
30
+ from typing import ClassVar
27
31
 
28
32
  import _cctally_core
29
33
  import _cctally_journal as _journal
@@ -34,6 +38,7 @@ import _lib_journal
34
38
  import _lib_json_envelope
35
39
  import _lib_rebuild_progress
36
40
  import _lib_rederive
41
+ import _lib_retained_records
37
42
  import _cctally_store
38
43
  from _cctally_store import StatsIndexNotCurrent
39
44
 
@@ -64,9 +69,9 @@ class RederiveObligationNotDurable(RederiveApplyError):
64
69
  @dataclass(frozen=True)
65
70
  class RederivePreview:
66
71
  plan: _lib_rederive.RederivePlan
67
- records: tuple[dict, ...]
72
+ records: "Sequence[dict]" # #1004: a RetainedRecords, or a tuple
68
73
  journal_high_water: "tuple[str, int] | None"
69
- record_ends: tuple[tuple[str, int], ...]
74
+ record_ends: "Sequence[tuple[str, int]]" # its ends view, or a tuple
70
75
  generated_at: str
71
76
  batch_id: "str | None"
72
77
  incomplete_batch: bool
@@ -518,29 +523,349 @@ def _joined_entries(cache_conn, range_start, range_end, *,
518
523
  sql += " AND se.account_key = ?"
519
524
  params.append(account_key)
520
525
  sql += " ORDER BY se.timestamp_utc ASC, se.id ASC"
521
- out = []
522
- for row in cache_conn.execute(sql, params):
523
- entry = cache._JoinedClaudeEntry(
524
- timestamp=dt.datetime.fromisoformat(row[0]),
525
- model=row[1],
526
- input_tokens=int(row[2] or 0),
527
- output_tokens=int(row[3] or 0),
528
- cache_creation_tokens=int(row[4] or 0),
529
- cache_read_tokens=int(row[5] or 0),
530
- source_path=row[6],
531
- session_id=row[7],
532
- project_path=row[8],
533
- cost_usd=row[9],
534
- usage_extra=({"speed": row[10]} if row[10] else None),
535
- cache_1h_tokens=(None if row[11] is None else int(row[11])),
526
+ return [_joined_entry_from_row(cache, row)
527
+ for row in cache_conn.execute(sql, params)]
528
+
529
+
530
+ #: The columns `_joined_entry_from_row` reads, in its row order.
531
+ _JOINED_ENTRY_COLUMNS = (
532
+ "se.timestamp_utc, se.model, se.input_tokens, se.output_tokens, "
533
+ "se.cache_create_tokens, se.cache_read_tokens, se.source_path, "
534
+ "sf.session_id, sf.project_path, se.cost_usd_raw, se.speed, "
535
+ "se.cache_create_1h_tokens, se.line_offset, se.account_key"
536
+ )
537
+
538
+
539
+ def _joined_entry_from_row(cache, row):
540
+ """One `_JoinedClaudeEntry` from a `_JOINED_ENTRY_COLUMNS` row: the one
541
+ row-to-entry construction `_joined_entries` and `BlockTotalsEngine`
542
+ share, so an engine entry equals a fresh query's field for field."""
543
+ entry = cache._JoinedClaudeEntry(
544
+ timestamp=dt.datetime.fromisoformat(row[0]),
545
+ model=row[1],
546
+ input_tokens=int(row[2] or 0),
547
+ output_tokens=int(row[3] or 0),
548
+ cache_creation_tokens=int(row[4] or 0),
549
+ cache_read_tokens=int(row[5] or 0),
550
+ source_path=row[6],
551
+ session_id=row[7],
552
+ project_path=row[8],
553
+ cost_usd=row[9],
554
+ usage_extra=({"speed": row[10]} if row[10] else None),
555
+ cache_1h_tokens=(None if row[11] is None else int(row[11])),
556
+ )
557
+ # #869: the retained source identity and account of the entry. The
558
+ # cost path never reads these; the historical-close proof fingerprints
559
+ # the exact population and requires every entry to be the close's.
560
+ entry.source_line_offset = row[12]
561
+ entry.source_account_key = row[13]
562
+ return entry
563
+
564
+
565
+ #: The engine's memo marker for an entry it has not priced yet.
566
+ _UNPRICED = object()
567
+
568
+
569
+ class _EntryRun:
570
+ """The cached entries of one load range start (spec D1 *Entry cache*):
571
+ the entries in SQL order, their raw `timestamp_utc` texts, each entry's
572
+ memoized cost, and ``through``, the largest end text fetched."""
573
+
574
+ __slots__ = ("entries", "texts", "costs", "through", "state_keys")
575
+
576
+ def __init__(self, through: str) -> None:
577
+ self.entries: list = []
578
+ self.texts: list = []
579
+ self.costs: list = []
580
+ self.through = through
581
+ self.state_keys: set = set()
582
+
583
+
584
+ class _FoldState:
585
+ """One fold over a run's prefix for one owner and window set (spec D1
586
+ *Fold states*): the prefix length consumed, the totals, the evidence
587
+ sets and the run indexes of the owned entries, in order."""
588
+
589
+ __slots__ = ("consumed", "totals", "missing_without_raw", "observed_raw",
590
+ "owned")
591
+
592
+ def __init__(self, totals) -> None:
593
+ self.consumed = 0
594
+ self.totals = totals
595
+ self.missing_without_raw: set = set()
596
+ self.observed_raw: set = set()
597
+ self.owned: list = []
598
+
599
+
600
+ def _typed(value):
601
+ """A hashable spelling of one key or window field that keeps values
602
+ equal under ``==`` but different in type or offset apart (``1`` and
603
+ ``True``; two instants spelled with different offsets)."""
604
+ if isinstance(value, dt.datetime):
605
+ return ("datetime", value.isoformat())
606
+ return (type(value).__qualname__, value)
607
+
608
+
609
+ class BlockTotalsEngine:
610
+ """#1003 D1: exact incremental `_compute_block_totals` for one scratch
611
+ replay over a stable cache snapshot.
612
+
613
+ The full re-price re-reads, re-partitions and re-prices every entry an
614
+ open block owns on every tick, so a replay's cost grows with each block's
615
+ age. The engine caches each load range's entries and extends them with a
616
+ delta fetch, keeps one fold per (range start, owner, window set) that it
617
+ extends by the new entries only, and prices each cached entry once. Its
618
+ answer equals the full re-price's: the same entries in the same order
619
+ (text comparisons and the delta predicate reproduce SQLite's byte order),
620
+ the same pure ownership rule, and the same `BlockTotals.add_priced`
621
+ additions in the same order.
622
+
623
+ Only `_scratch_read_adapters` installs one, so the cache connection holds
624
+ one read snapshot for the engine's whole life.
625
+ """
626
+
627
+ #: Shadow verification: every call also runs the full re-price and
628
+ #: raises on any difference. Set only by tests and the acceptance driver.
629
+ verify: ClassVar[bool] = False
630
+ #: Spec D1 *Bounds*: cached entries across all runs, runs, fold states.
631
+ ENTRY_BUDGET = 65536
632
+ MAX_RUNS = 64
633
+ MAX_STATES = 64
634
+
635
+ def __init__(self, cache_conn: sqlite3.Connection) -> None:
636
+ import collections
637
+ self._conn = cache_conn
638
+ self._cache = sys.modules["_cctally_cache"]
639
+ self._runs: "collections.OrderedDict[str, _EntryRun]" = (
640
+ collections.OrderedDict())
641
+ self._states: "collections.OrderedDict[tuple, _FoldState]" = (
642
+ collections.OrderedDict())
643
+ self._cached_entries = 0
644
+ self._pricing_missing: dict = {}
645
+ self.counts = collections.Counter()
646
+
647
+ # -- entry cache ---------------------------------------------------------
648
+
649
+ def _fetch(self, start_iso: str, end_iso: str, through: "str | None"):
650
+ sql = (
651
+ f"SELECT {_JOINED_ENTRY_COLUMNS} FROM session_entries se "
652
+ "LEFT JOIN session_files sf ON sf.path = se.source_path "
653
+ "WHERE se.timestamp_utc >= ?"
536
654
  )
537
- # #869: the retained source identity and account of the entry. The
538
- # cost path never reads these; the historical-close proof fingerprints
539
- # the exact population and requires every entry to be the close's.
540
- entry.source_line_offset = row[12]
541
- entry.source_account_key = row[13]
542
- out.append(entry)
543
- return out
655
+ params = [start_iso]
656
+ if through is not None:
657
+ sql += " AND se.timestamp_utc > ?"
658
+ params.append(through)
659
+ sql += (" AND se.timestamp_utc <= ?"
660
+ " ORDER BY se.timestamp_utc ASC, se.id ASC")
661
+ params.append(end_iso)
662
+ return self._conn.execute(sql, params).fetchall()
663
+
664
+ def _drop_run(self, start_iso: str) -> None:
665
+ run = self._runs.pop(start_iso)
666
+ self._cached_entries -= len(run.entries)
667
+ for key in run.state_keys:
668
+ self._states.pop(key, None)
669
+
670
+ def _run_for(self, start_iso: str, end_iso: str) -> "_EntryRun | None":
671
+ """The run holding every entry of ``[start_iso, end_iso]`` (by text),
672
+ or None when that run alone would exceed the entry budget."""
673
+ run = self._runs.get(start_iso)
674
+ if run is not None:
675
+ self._runs.move_to_end(start_iso)
676
+ if end_iso <= run.through:
677
+ return run
678
+ rows = self._fetch(start_iso, end_iso, run.through)
679
+ self.counts["delta_fetches"] += 1
680
+ else:
681
+ rows = self._fetch(start_iso, end_iso, None)
682
+ self.counts["fetches"] += 1
683
+ held = 0 if run is None else len(run.entries)
684
+ if held + len(rows) > self.ENTRY_BUDGET:
685
+ if run is not None:
686
+ self._drop_run(start_iso)
687
+ return None
688
+ if run is None:
689
+ run = _EntryRun(end_iso)
690
+ self._runs[start_iso] = run
691
+ for row in rows:
692
+ run.entries.append(_joined_entry_from_row(self._cache, row))
693
+ run.texts.append(row[0])
694
+ run.costs.append(_UNPRICED)
695
+ run.through = end_iso
696
+ self._cached_entries += len(rows)
697
+ while (self._cached_entries > self.ENTRY_BUDGET
698
+ or len(self._runs) > self.MAX_RUNS):
699
+ oldest = next(iter(self._runs))
700
+ if oldest == start_iso:
701
+ break
702
+ self._drop_run(oldest)
703
+ self.counts["runs_evicted"] += 1
704
+ return run
705
+
706
+ # -- fold states ---------------------------------------------------------
707
+
708
+ def _missing_pricing(self, model) -> bool:
709
+ missing = self._pricing_missing.get(model)
710
+ if missing is None:
711
+ import _lib_pricing
712
+ missing = _lib_pricing._resolve_model_pricing(
713
+ model, warn=False) is None
714
+ self._pricing_missing[model] = missing
715
+ return missing
716
+
717
+ def _cost(self, run: _EntryRun, index: int) -> float:
718
+ cost = run.costs[index]
719
+ if cost is _UNPRICED:
720
+ entry = run.entries[index]
721
+ usage = _record.claude_usage_dict( # #195 chokepoint
722
+ input_tokens=entry.input_tokens,
723
+ output_tokens=entry.output_tokens,
724
+ cache_creation_tokens=entry.cache_creation_tokens,
725
+ cache_read_tokens=entry.cache_read_tokens,
726
+ cache_1h_tokens=getattr(entry, "cache_1h_tokens", None),
727
+ speed=getattr(entry, "speed", None),
728
+ )
729
+ cost = _record._calculate_entry_cost(
730
+ entry.model, usage, mode="auto", cost_usd=entry.cost_usd,
731
+ )
732
+ run.costs[index] = cost
733
+ return cost
734
+
735
+ def _extend(self, state: _FoldState, run: _EntryRun, n: int, owner_key,
736
+ windows, owned_entries) -> None:
737
+ """Fold ``run.entries[state.consumed:n]``: each owned entry, in order,
738
+ is appended to ``owned_entries``, records its evidence, is priced
739
+ (once per run) and is folded, exactly as the full re-price does."""
740
+ import _lib_blocks
741
+ start = state.consumed
742
+ new = run.entries[start:n]
743
+ position = {id(entry): start + i for i, entry in enumerate(new)}
744
+ owned, _unowned = _lib_blocks.partition_entries_by_owner(new, windows)
745
+ for entry in owned[owner_key]:
746
+ index = position[id(entry)]
747
+ if owned_entries is not None:
748
+ owned_entries.append(entry)
749
+ if (entry.input_tokens or entry.output_tokens
750
+ or entry.cache_creation_tokens or entry.cache_read_tokens):
751
+ if entry.cost_usd is not None:
752
+ state.observed_raw.add(entry.model)
753
+ elif self._missing_pricing(entry.model):
754
+ state.missing_without_raw.add(entry.model)
755
+ state.totals.add_priced(
756
+ entry.model, entry.project_path, entry.input_tokens,
757
+ entry.output_tokens, entry.cache_creation_tokens,
758
+ entry.cache_read_tokens, self._cost(run, index),
759
+ )
760
+ state.owned.append(index)
761
+ state.consumed = n
762
+
763
+ def _state_key(self, start_iso: str, owner_key, windows) -> "tuple | None":
764
+ signature = []
765
+ for window in windows:
766
+ if not dataclasses.is_dataclass(window):
767
+ return None
768
+ signature.append(tuple(
769
+ _typed(getattr(window, field.name))
770
+ for field in dataclasses.fields(window)))
771
+ return (start_iso, _typed(owner_key), tuple(signature))
772
+
773
+ # -- the answer ----------------------------------------------------------
774
+
775
+ def totals(self, block_start_at, range_end, *, owner_key, windows: list,
776
+ pricing_evidence: "dict | None",
777
+ owned_entries: "list | None") -> dict:
778
+ """`_compute_block_totals`' answer for this call, with the same
779
+ side effects on ``pricing_evidence`` and ``owned_entries``."""
780
+ if not self.verify:
781
+ return self._totals(block_start_at, range_end, owner_key,
782
+ windows, pricing_evidence, owned_entries)
783
+ evidence: dict = {}
784
+ owned: list = []
785
+ result = self._totals(block_start_at, range_end, owner_key, windows,
786
+ evidence, owned)
787
+ shadow_evidence: dict = {}
788
+ shadow_owned: list = []
789
+ shadow = _record._block_totals_reprice(
790
+ block_start_at, range_end, owner_key=owner_key,
791
+ ownership_windows=windows, pricing_evidence=shadow_evidence,
792
+ owned_entries=shadow_owned)
793
+ if (_float_exact(result) != _float_exact(shadow)
794
+ or evidence != shadow_evidence
795
+ or [vars(e) for e in owned]
796
+ != [vars(e) for e in shadow_owned]):
797
+ raise AssertionError(
798
+ "BlockTotalsEngine differs from the full re-price for "
799
+ f"{block_start_at!r}..{range_end!r} owner {owner_key!r}")
800
+ if pricing_evidence is not None:
801
+ pricing_evidence.update(evidence)
802
+ if owned_entries is not None:
803
+ owned_entries.extend(owned)
804
+ return result
805
+
806
+ def _totals(self, block_start_at, range_end, owner_key, windows,
807
+ pricing_evidence, owned_entries) -> dict:
808
+ start_iso = block_start_at.astimezone(dt.timezone.utc).isoformat()
809
+ end_iso = range_end.astimezone(dt.timezone.utc).isoformat()
810
+ key = self._state_key(start_iso, owner_key, windows)
811
+ run = None if key is None else self._run_for(start_iso, end_iso)
812
+ if run is None:
813
+ self.counts["uncached"] += 1
814
+ return _record._block_totals_reprice(
815
+ block_start_at, range_end, owner_key=owner_key,
816
+ ownership_windows=windows, pricing_evidence=pricing_evidence,
817
+ owned_entries=owned_entries)
818
+ n = bisect.bisect_right(run.texts, end_iso)
819
+ state = self._states.get(key)
820
+ if state is not None and n < state.consumed:
821
+ self._states.pop(key)
822
+ state = None
823
+ self.counts["rebuilt"] += 1
824
+ if state is None:
825
+ state = _FoldState(_record.BlockTotals())
826
+ self._states[key] = state
827
+ run.state_keys.add(key)
828
+ while len(self._states) > self.MAX_STATES:
829
+ old_key, _old = self._states.popitem(last=False)
830
+ run_of_old = self._runs.get(old_key[0])
831
+ if run_of_old is not None:
832
+ run_of_old.state_keys.discard(old_key)
833
+ self.counts["states_evicted"] += 1
834
+ else:
835
+ self._states.move_to_end(key)
836
+ if owned_entries is not None:
837
+ owned_entries.extend(run.entries[i] for i in state.owned)
838
+ if n > state.consumed:
839
+ try:
840
+ self._extend(state, run, n, owner_key, windows, owned_entries)
841
+ except BaseException:
842
+ # A half-folded state must never answer a later call.
843
+ self._states.pop(key, None)
844
+ run.state_keys.discard(key)
845
+ raise
846
+ self.counts["extended"] += 1
847
+ else:
848
+ self.counts["cached"] += 1
849
+ result = state.totals.as_legacy_dict()
850
+ if pricing_evidence is not None:
851
+ pricing_evidence["unpricedModels"] = sorted(
852
+ state.missing_without_raw - state.observed_raw)
853
+ ambiguous = sorted(state.missing_without_raw & state.observed_raw)
854
+ if ambiguous:
855
+ pricing_evidence["uncertainModels"] = ambiguous
856
+ return result
857
+
858
+
859
+ def _float_exact(value):
860
+ """``value`` with every float spelled by its exact bits and every dict
861
+ by its items in order, for the engine's shadow comparison."""
862
+ if isinstance(value, float):
863
+ return ("float", value.hex())
864
+ if isinstance(value, dict):
865
+ return ("dict", tuple((k, _float_exact(v)) for k, v in value.items()))
866
+ if isinstance(value, (list, tuple)):
867
+ return ("seq", tuple(_float_exact(v) for v in value))
868
+ return value
544
869
 
545
870
 
546
871
  @contextlib.contextmanager
@@ -562,6 +887,10 @@ def _scratch_read_adapters(cache_conn: sqlite3.Connection):
562
887
  "load_config": lambda *args, **kwargs: {},
563
888
  }
564
889
  prior = {name: getattr(cctally, name) for name in replacements}
890
+ # #1003 D1: the replay's block totals come from an incremental engine over
891
+ # this context's stable cache snapshot, one fresh engine per context.
892
+ prior_engine = _record.swap_block_totals_engine(
893
+ BlockTotalsEngine(cache_conn))
565
894
  try:
566
895
  for name, value in replacements.items():
567
896
  setattr(cctally, name, value)
@@ -569,6 +898,7 @@ def _scratch_read_adapters(cache_conn: sqlite3.Connection):
569
898
  finally:
570
899
  for name, value in prior.items():
571
900
  setattr(cctally, name, value)
901
+ _record.swap_block_totals_engine(prior_engine)
572
902
 
573
903
 
574
904
  #: Op kinds the scratch replay runs through the pipeline hooks.
@@ -593,8 +923,24 @@ def _is_rederivable_raw(record: dict) -> bool:
593
923
  return kind in _REDERIVABLE_OP_KINDS or kind in _REVIEWED_WEEKLY_KINDS
594
924
 
595
925
 
596
- def _rederivable_raw_records(records: list[dict]) -> list[dict]:
597
- return [record for record in records if _is_rederivable_raw(record)]
926
+ def _rederivable_raw_records(records):
927
+ """The rederivable raw inputs of ``records``, in journal order: a
928
+ position-subset view of a coordinate-backed sequence (#1004 D4), else a
929
+ list."""
930
+ subset = getattr(records, "subset", None)
931
+ if subset is None:
932
+ return [record for record in records if _is_rederivable_raw(record)]
933
+ return subset(_positions_where(records, _is_rederivable_raw))
934
+
935
+
936
+ def _positions_where(records, predicate) -> "array":
937
+ """The positions of ``records`` whose record satisfies ``predicate``,
938
+ as an unsigned array (8 bytes a position, no record kept)."""
939
+ positions = array("Q")
940
+ for index, record in enumerate(records):
941
+ if predicate(record):
942
+ positions.append(index)
943
+ return positions
598
944
 
599
945
 
600
946
  #: #825/#832 spec 5.1: the previous release validates a reviewed weekly
@@ -1058,7 +1404,95 @@ def _normalize_legacy_accounts(records: list[dict]) -> None:
1058
1404
  _journal._normalize_legacy_account_stamp(record, cutover_claude)
1059
1405
 
1060
1406
 
1061
- def _normalized_planning_view(records, cutover_claude=None) -> list:
1407
+ class _PlanningView(collections.abc.Sequence):
1408
+ """#1004 spec D4: the planner's normalized view of a coordinate-backed
1409
+ sequence (a `RetainedRecords` or one of its views).
1410
+
1411
+ A streamed position's decode is private, so it is normalized in place and
1412
+ returned; it never enters the `id()` memo, whose key a freed decode's
1413
+ address could later repeat for another position. A retained object (a D4
1414
+ stub, a placeholder, an appended reviewed op) takes `_normalized_planning_
1415
+ view`'s copy-on-write path, memoized by identity: those objects live as
1416
+ long as the source, so their ids cannot be reused while the view is.
1417
+ Views derived from this one (slices, subsets) share the memo, so a shared
1418
+ stub normalizes once for the whole planning call."""
1419
+
1420
+ __slots__ = ("_source", "_cutover", "_changed", "_shared")
1421
+
1422
+ def __init__(self, source, cutover_claude, *, changed=None, shared=None):
1423
+ self._source = source
1424
+ self._cutover = cutover_claude
1425
+ #: id(retained source object) -> its normalized copy.
1426
+ self._changed = {} if changed is None else changed
1427
+ #: ids of every retained object this view returned (shared with the
1428
+ #: caller or memoized), which a replay must copy before mutating.
1429
+ self._shared = set() if shared is None else shared
1430
+
1431
+ def _derived(self, source):
1432
+ return _PlanningView(source, self._cutover, changed=self._changed,
1433
+ shared=self._shared)
1434
+
1435
+ def _fresh(self, record):
1436
+ _journal._normalize_legacy_account_stamp(record, self._cutover)
1437
+ return record
1438
+
1439
+ def _retained(self, record):
1440
+ if not isinstance(record, dict):
1441
+ return record
1442
+ hit = self._changed.get(id(record))
1443
+ if hit is not None:
1444
+ return hit
1445
+ probe = dict(record)
1446
+ if isinstance(record.get("payload"), dict):
1447
+ probe["payload"] = dict(record["payload"])
1448
+ _journal._normalize_legacy_account_stamp(probe, self._cutover)
1449
+ if probe == record:
1450
+ self._shared.add(id(record))
1451
+ return record
1452
+ normalized = copy.deepcopy(record)
1453
+ _journal._normalize_legacy_account_stamp(normalized, self._cutover)
1454
+ self._changed[id(record)] = normalized
1455
+ self._shared.add(id(normalized))
1456
+ return normalized
1457
+
1458
+ def __len__(self) -> int:
1459
+ return len(self._source)
1460
+
1461
+ def __getitem__(self, index):
1462
+ if isinstance(index, slice):
1463
+ part = self._source[index]
1464
+ if isinstance(part, list):
1465
+ return [self[i] for i in range(len(self))[index]]
1466
+ return self._derived(part)
1467
+ record = self._source[index]
1468
+ if self._source.is_streamed(index):
1469
+ return self._fresh(record)
1470
+ return self._retained(record)
1471
+
1472
+ def __iter__(self):
1473
+ for fresh, record in self._source.iter_marked():
1474
+ yield self._fresh(record) if fresh else self._retained(record)
1475
+
1476
+ def iter_marked(self):
1477
+ for fresh, record in self._source.iter_marked():
1478
+ yield ((True, self._fresh(record)) if fresh
1479
+ else (False, self._retained(record)))
1480
+
1481
+ def is_streamed(self, index) -> bool:
1482
+ return self._source.is_streamed(index)
1483
+
1484
+ def subset(self, positions):
1485
+ return self._derived(self._source.subset(positions))
1486
+
1487
+ def prepare_for_replay(self, record):
1488
+ """The replay's private copy of a record this view returned (#997
1489
+ D5): a fresh decode is already private; a retained one is copied."""
1490
+ if id(record) in self._shared:
1491
+ return copy.deepcopy(record)
1492
+ return record
1493
+
1494
+
1495
+ def _normalized_planning_view(records, cutover_claude=None):
1062
1496
  """#997 spec D5: the planner's normalized records, copy-on-write.
1063
1497
 
1064
1498
  The same global pass as `_normalize_legacy_accounts` (every cutover value
@@ -1071,6 +1505,8 @@ def _normalized_planning_view(records, cutover_claude=None) -> list:
1071
1505
  the up-front deep copy of every record normalization leaves alone."""
1072
1506
  if cutover_claude is None:
1073
1507
  cutover_claude = _planner_cutover_claude(records)
1508
+ if hasattr(records, "iter_marked"):
1509
+ return _PlanningView(records, cutover_claude)
1074
1510
  changed: dict = {}
1075
1511
  view = []
1076
1512
  for record in records:
@@ -1121,15 +1557,17 @@ def _derive_desired_events_into(
1121
1557
  held_weekly_observation_ids=frozenset(),
1122
1558
  block_sources: "dict | None" = None,
1123
1559
  ) -> list[dict]:
1124
- raw_records = _rederivable_raw_records(records)
1125
1560
  held_ids = frozenset(held_weekly_observation_ids)
1126
- valid_ids = {
1127
- record.get("id") for record in raw_records
1561
+ # #1004 D4: only the held ids found, not one id per raw observation,
1562
+ # which the frame would otherwise keep through the whole replay.
1563
+ valid_held_ids = {
1564
+ record.get("id") for record in _rederivable_raw_records(records)
1128
1565
  if record.get("t") == "obs"
1566
+ and record.get("id") in held_ids
1129
1567
  and "weekly_percent" in (record.get("payload") or {})
1130
1568
  and "resets_at" in (record.get("payload") or {})
1131
1569
  }
1132
- unknown_ids = held_ids - valid_ids
1570
+ unknown_ids = held_ids - valid_held_ids
1133
1571
  if unknown_ids:
1134
1572
  raise _lib_rederive.RederiveConflict(
1135
1573
  "reviewed weekly hold names no Claude raw observation: "
@@ -1147,9 +1585,12 @@ def _derive_desired_events_into(
1147
1585
  try:
1148
1586
  # #997 spec D5: the planner's view shares unchanged records with the
1149
1587
  # caller, so the replay prepares a private copy of each record.
1588
+ # #1004 D4: a streamed record's decode is already private, so the
1589
+ # coordinate-backed view copies only the retained ones.
1150
1590
  return replay_scratch_records(
1151
1591
  conn, records, cache_conn, fold=fold, held_ids=held_ids,
1152
- block_sources=block_sources, prepare=copy.deepcopy)
1592
+ block_sources=block_sources,
1593
+ prepare=getattr(records, "prepare_for_replay", copy.deepcopy))
1153
1594
  finally:
1154
1595
  conn.close()
1155
1596
 
@@ -1335,8 +1776,10 @@ def plan_claude_usage(
1335
1776
  replay and comparison contract.
1336
1777
  """
1337
1778
  # #997 spec D5: a copy-on-write normalized view, not a whole-list deep
1338
- # copy. Every planner consumer below reads this view.
1339
- records = _normalized_planning_view(list(records))
1779
+ # copy. Every planner consumer below reads this view. #1004 D4: a
1780
+ # coordinate-backed sequence is viewed as it is, never listed.
1781
+ records = _normalized_planning_view(
1782
+ records if hasattr(records, "iter_marked") else list(records))
1340
1783
  report = _lib_rederive.validate_family_registry(
1341
1784
  evt_kinds=set(_journal._EVT_SPECS),
1342
1785
  op_kinds=_planner_op_kinds(),
@@ -1575,14 +2018,21 @@ def read_rederive_journal_prefix(
1575
2018
  keeps only the decision records and substitutes `None` placeholders for
1576
2019
  everything else; the planner here reads every observation for cache
1577
2020
  validation, desired-event derivation and preservation decisions, and walks
1578
- `records` in parallel with `record_ends`. Both lists stay complete and
1579
- aligned, so the win in this file is the removed double materialization and
1580
- the removed hash traversals, not reduced retention.
2021
+ `records` in parallel with `record_ends`. Both stay complete and aligned.
1581
2022
 
1582
2023
  #997 spec D4: positions are kept, but an admitted Codex quota observation
1583
2024
  is retained as an interned read-only stub (`_inert_codex_stub`) rather
1584
2025
  than decoded in full; no consumer on this path reads its quota payload,
1585
2026
  `id` or `at`.
2027
+
2028
+ #1004 spec D3/D4: `records` is a `RetainedRecords` and `record_ends` its
2029
+ `LineCoordinates.ends()` view. Every other record (each one retained
2030
+ compacted before) is a streamed slot: its coordinates and the digest of
2031
+ its line, re-read, digest-checked and decoded (then compacted) on each
2032
+ access, so a pass sees a fresh record equal to the one retained before.
2033
+ The D4 stubs and the admission placeholders stay retained objects. With
2034
+ `_lib_retained_records.MATERIALIZE_FOR_TESTS` set, the reader returns
2035
+ today's two lists instead (a test switch).
1586
2036
  """
1587
2037
  if high_water is None:
1588
2038
  high_water = _journal.journal_high_water()
@@ -1593,8 +2043,11 @@ def read_rederive_journal_prefix(
1593
2043
  raise OSError(
1594
2044
  f"journal high-water segment is unavailable: {high_water[0]}"
1595
2045
  )
1596
- records: list[dict] = []
1597
- record_ends: list[tuple[str, int]] = []
2046
+ materialize = _lib_retained_records.MATERIALIZE_FOR_TESTS
2047
+ coordinates = _lib_retained_records.LineCoordinates()
2048
+ records = _lib_retained_records.RetainedRecords(
2049
+ coordinates, _RederiveLineReader, _decode_retained_line,
2050
+ error=_journal_changed_error)
1598
2051
  evidence: list = []
1599
2052
  evidence_owners: list = []
1600
2053
  malformed = 0
@@ -1605,7 +2058,6 @@ def read_rederive_journal_prefix(
1605
2058
  # at its own position, so the digest is taken inline and the validation
1606
2059
  # runs after the pass, for admitted decisions only.
1607
2060
  pass_boundary = _journal._PassBoundary()
1608
- coordinates: list = []
1609
2061
  pending_reviewed: list = []
1610
2062
  inert_stubs: dict = {}
1611
2063
  lines = 0
@@ -1651,17 +2103,20 @@ def read_rederive_journal_prefix(
1651
2103
  evidence_owners.append(len(records))
1652
2104
  elif inert:
1653
2105
  record = _inert_codex_stub(record, inert_stubs)
1654
- records.append(record)
1655
- record_ends.append(record_end)
1656
- coordinates.append((segment, offset))
2106
+ if inert or materialize:
2107
+ records.append_retained(record, segment, offset, len(raw))
2108
+ else:
2109
+ records.append_streamed(raw, segment, offset, len(raw))
1657
2110
  prior_high_water = record_end
2111
+ starts = coordinates.starts()
1658
2112
  # An inert placeholder keeps every position and sequence number; an
1659
2113
  # excluded correction, reviewed decision or Claude record is never planning
1660
- # input (case F7).
1661
- originals = list(records)
2114
+ # input (case F7). The snapshot shares the coordinates and digests, so an
2115
+ # excluded position still decodes its original below.
2116
+ originals = records.slot_snapshot()
1662
2117
  placeholder = _lib_account_protocol.excluded_placeholder()
1663
2118
  _journal._apply_pass_admission(
1664
- records, coordinates, pass_boundary.resolve(high_water),
2119
+ records, starts, pass_boundary.resolve(high_water),
1665
2120
  placeholder=placeholder)
1666
2121
  # #825/#832 fix round C (CD2-005): an excluded acknowledgement takes its
1667
2122
  # raw-prefix evidence with it, so selection reads it as absent.
@@ -1671,18 +2126,24 @@ def read_rederive_journal_prefix(
1671
2126
  # coordinate for the outcome-quarantine fold of the scratch replay (a
1672
2127
  # trigger whose readings widen the every-account envelope). Nothing else
1673
2128
  # reads a placeholder's fields.
1674
- for index, slot in enumerate(records):
1675
- if slot is placeholder:
2129
+ for index in range(len(records)):
2130
+ if records.slot(index) is placeholder:
2131
+ segment, offset = starts[index]
1676
2132
  records[index] = {
1677
2133
  **placeholder,
1678
2134
  "quarantine": {
1679
2135
  "record": originals[index],
1680
- "segment": coordinates[index][0],
1681
- "offset": coordinates[index][1],
2136
+ "segment": segment,
2137
+ "offset": offset,
1682
2138
  },
1683
2139
  }
2140
+ originals.close()
2141
+ originals = None
1684
2142
  for index, record, prior, digest in pending_reviewed:
1685
- if records[index] is record:
2143
+ # #1004 D4: a position check. The slot still holds the op (a
2144
+ # retained form) or is still streamed, so admission kept it.
2145
+ slot = records.slot(index)
2146
+ if slot is record or slot is _lib_retained_records.STREAMED:
1686
2147
  _validate_retained_reviewed_op(
1687
2148
  record, prior, _PinnedPrefixDigest(prior, digest),
1688
2149
  )
@@ -1697,7 +2158,62 @@ def read_rederive_journal_prefix(
1697
2158
  raise _lib_rederive.RederiveConflict(
1698
2159
  f"journal prefix contains {malformed} malformed line(s)"
1699
2160
  )
1700
- return records, high_water, record_ends, tuple(evidence)
2161
+ if materialize:
2162
+ # Every slot is a retained object here, so this decodes nothing.
2163
+ listed = list(records)
2164
+ records.close()
2165
+ return listed, high_water, list(coordinates.ends()), tuple(evidence)
2166
+ return records, high_water, coordinates.ends(), tuple(evidence)
2167
+
2168
+
2169
+ def _decode_retained_line(line: bytes):
2170
+ """#1004 D4: a streamed slot's record, exactly as the reader retained
2171
+ it before: the strict decode, then #997 D15's compaction."""
2172
+ record = _lib_journal.decode_line(line)
2173
+ if record is None:
2174
+ return None
2175
+ return _lib_journal.compact_retained(record)
2176
+
2177
+
2178
+ def _journal_changed_error(message: str) -> Exception:
2179
+ """The reader's integrity error for a line that changed under it: the
2180
+ `RederiveConflict` a malformed prefix raises."""
2181
+ return _lib_rederive.RederiveConflict(
2182
+ f"journal prefix changed since it was read: {message}")
2183
+
2184
+
2185
+ class _RederiveLineReader:
2186
+ """`LineReader` for the rederive reader's streamed records: the framed
2187
+ read `QuotaLineSource` uses (#997 D7), through the one physical segment
2188
+ read boundary, with one open handle (the current segment's), so an
2189
+ in-order pass opens each segment once."""
2190
+
2191
+ __slots__ = ("_segment", "_fh", "__weakref__")
2192
+
2193
+ def __init__(self) -> None:
2194
+ self._segment = None
2195
+ self._fh = None
2196
+
2197
+ def read(self, segment: str, start: int, length: int) -> bytes:
2198
+ if segment != self._segment:
2199
+ self.close()
2200
+ try:
2201
+ self._fh = _journal._open_segment_for_read(
2202
+ _cctally_core.JOURNAL_DIR / segment)
2203
+ except OSError as exc:
2204
+ raise _journal_changed_error(
2205
+ f"{segment}: {type(exc).__name__}: {exc}") from exc
2206
+ self._segment = segment
2207
+ try:
2208
+ return _journal._read_framed_line(self._fh, segment, start, length)
2209
+ except (_journal.QuotaLineSourceError, OSError) as exc:
2210
+ raise _journal_changed_error(str(exc)) from exc
2211
+
2212
+ def close(self) -> None:
2213
+ fh, self._fh = self._fh, None
2214
+ self._segment = None
2215
+ if fh is not None:
2216
+ fh.close()
1701
2217
 
1702
2218
 
1703
2219
  def _read_only_journal_high_water() -> "tuple[str, int] | None":
@@ -1937,16 +2453,34 @@ def _fresh_manifest_baseline(records, completed_batches, family, *,
1937
2453
  idempotence stated on `_decision_batch_completed`: the op's actions
1938
2454
  re-enter the delta and apply idempotently over anything the torn batch
1939
2455
  already wrote."""
1940
- return [
1941
- record for index, record in enumerate(records)
2456
+ kept = [] if getattr(records, "subset", None) is None else None
2457
+ positions = array("Q")
2458
+ for index, record in enumerate(records):
1942
2459
  if index != exclude_index and not (
1943
2460
  record.get("t") == "op"
1944
2461
  and (record.get("payload") or {}).get("kind")
1945
2462
  in _REVIEWED_WEEKLY_KINDS
1946
2463
  and not _decision_batch_completed(
1947
2464
  records, index, completed_batches, family)
1948
- )
1949
- ]
2465
+ ):
2466
+ if kept is None:
2467
+ positions.append(index)
2468
+ else:
2469
+ kept.append(record)
2470
+ # #1004 D4: a position-subset view of a coordinate-backed sequence.
2471
+ return kept if kept is not None else records.subset(positions)
2472
+
2473
+
2474
+ def correction_only_suffix(records, start) -> bool:
2475
+ """Whether every non-review-neutral record from ``start`` on is a
2476
+ correction or a correction-batch marker (#1004 D4: a streaming predicate
2477
+ that keeps no record)."""
2478
+ for record in records[start:]:
2479
+ if _is_review_neutral(record):
2480
+ continue
2481
+ if record.get("t") not in {"correction", "correction_batch"}:
2482
+ return False
2483
+ return True
1950
2484
 
1951
2485
 
1952
2486
  def _preview_from_snapshot(
@@ -1987,7 +2521,10 @@ def _preview_from_snapshot(
1987
2521
  raise _lib_rederive.RederiveConflict(
1988
2522
  "reviewed weekly decision journalPrefixHash drifted"
1989
2523
  )
1990
- planning_records = [*records, reviewed_op]
2524
+ with_extra = getattr(records, "with_extra", None)
2525
+ planning_records = (
2526
+ [*records, reviewed_op] if with_extra is None
2527
+ else with_extra(reviewed_op))
1991
2528
  baseline_records = None # #1015 R8: resolved after the selection
1992
2529
  else:
1993
2530
  decision_indexes = [
@@ -2008,18 +2545,10 @@ def _preview_from_snapshot(
2008
2545
  baseline_records = _fresh_manifest_baseline(
2009
2546
  records, selection.completed_batches, family)
2010
2547
  if retained_decision is not None:
2011
- suffix = [
2012
- record for record in records[retained_decision_index + 1:]
2013
- if not _is_review_neutral(record)
2014
- ]
2015
2548
  completed_decision_batch = _decision_batch_completed(
2016
2549
  records, retained_decision_index, selection.completed_batches,
2017
2550
  family)
2018
- correction_only_suffix = all(
2019
- record.get("t") in {"correction", "correction_batch"}
2020
- for record in suffix
2021
- )
2022
- if correction_only_suffix:
2551
+ if correction_only_suffix(records, retained_decision_index + 1):
2023
2552
  decision_op = retained_decision
2024
2553
  decision_from_journal = not completed_decision_batch
2025
2554
  decision_batch_completed = completed_decision_batch
@@ -2134,9 +2663,11 @@ def _preview_from_snapshot(
2134
2663
  preview = RederivePreview(
2135
2664
  journal_conflicts=journal_conflicts,
2136
2665
  plan=plan,
2137
- records=tuple(records),
2666
+ # #1004 D3/D4: a coordinate-backed sequence and its ends view are
2667
+ # stored as they are; `tuple(...)` would decode every record back.
2668
+ records=_preview_sequence(records),
2138
2669
  journal_high_water=high_water,
2139
- record_ends=tuple(record_ends),
2670
+ record_ends=_preview_sequence(record_ends),
2140
2671
  generated_at=generated_at,
2141
2672
  batch_id=batch_id,
2142
2673
  incomplete_batch=incomplete,
@@ -2533,9 +3064,18 @@ def _compact_preview(preview: RederivePreview) -> RederivePreview:
2533
3064
  renderers and every error read what they read before. The one release
2534
3065
  seam: T18 disables the release by patching it to the identity.
2535
3066
  """
3067
+ close = getattr(preview.records, "close", None)
3068
+ if close is not None:
3069
+ close()
2536
3070
  return dataclasses.replace(preview, records=(), record_ends=())
2537
3071
 
2538
3072
 
3073
+ def _preview_sequence(sequence):
3074
+ """A preview's `records`/`record_ends`: today's tuple for a list, the
3075
+ coordinate-backed sequence or view itself otherwise (#1004 D4)."""
3076
+ return tuple(sequence) if isinstance(sequence, list) else sequence
3077
+
3078
+
2539
3079
  def _release_preview_records(*previews):
2540
3080
  """#997 D18: compact copies of `previews`, in order, for the apply to
2541
3081
  rebind immediately before a rebuild, so no full preview stays reachable