cctally 1.113.0 → 1.114.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +4 -6
  3. package/bin/_cctally_cache.py +1683 -485
  4. package/bin/_cctally_core.py +223 -1
  5. package/bin/_cctally_dashboard.py +96 -18
  6. package/bin/_cctally_dashboard_perf.py +128 -9
  7. package/bin/_cctally_dashboard_sources.py +267 -204
  8. package/bin/_cctally_db.py +861 -18
  9. package/bin/_cctally_diagnosis_sources.py +22 -4
  10. package/bin/_cctally_doctor.py +136 -18
  11. package/bin/_cctally_five_hour.py +2 -2
  12. package/bin/_cctally_forecast.py +27 -6
  13. package/bin/_cctally_journal.py +67 -5
  14. package/bin/_cctally_milestone_history.py +82 -9
  15. package/bin/_cctally_percent_breakdown.py +3 -2
  16. package/bin/_cctally_quota.py +201 -33
  17. package/bin/_cctally_record.py +14 -0
  18. package/bin/_cctally_rederive.py +23 -9
  19. package/bin/_cctally_source_analytics.py +60 -15
  20. package/bin/_cctally_statusline.py +17 -0
  21. package/bin/_cctally_store.py +80 -5
  22. package/bin/_cctally_tui.py +4 -3
  23. package/bin/_cctally_weekrefs.py +128 -21
  24. package/bin/_lib_cache_report.py +35 -6
  25. package/bin/_lib_codex_conversation.py +32 -0
  26. package/bin/_lib_codex_conversation_query.py +164 -30
  27. package/bin/_lib_conversation_query.py +95 -56
  28. package/bin/_lib_conversation_retention.py +1605 -640
  29. package/bin/_lib_cost_provenance.py +53 -2
  30. package/bin/_lib_doctor.py +263 -69
  31. package/bin/_lib_fmt.py +11 -0
  32. package/bin/_lib_merge_gate.py +469 -51
  33. package/bin/_lib_perf.py +27 -1
  34. package/bin/_lib_pricing.py +193 -8
  35. package/bin/_lib_pricing_check.py +20 -4
  36. package/bin/_lib_reclaim_planner.py +792 -0
  37. package/bin/_lib_rederive.py +225 -77
  38. package/bin/_lib_snapshot_cache.py +21 -0
  39. package/bin/_lib_sqlite_close.py +81 -0
  40. package/bin/_lib_subscription_weeks.py +100 -17
  41. package/bin/_lib_tick_stats.py +217 -17
  42. package/bin/_lib_wal_checkpoint.py +1077 -0
  43. package/bin/_lib_write_budget.py +483 -0
  44. package/bin/_lib_write_io.py +703 -0
  45. package/bin/cctally +8 -0
  46. package/dashboard/static/assets/{ConversationsView-CEZ-BUyi.js → ConversationsView-Bc8LCOAZ.js} +1 -1
  47. package/dashboard/static/assets/{DoctorModal-CE1oH_Zy.js → DoctorModal-D--qVBwe.js} +1 -1
  48. package/dashboard/static/assets/ModalRoot-DLPgyAM_.js +1 -0
  49. package/dashboard/static/assets/{ProjectsDrillPanel-B3DZoCeD.js → ProjectsDrillPanel-z53a4v4M.js} +1 -1
  50. package/dashboard/static/assets/{SourceDetailModal-CHp9EgLP.js → SourceDetailModal-Bw0T8W-W.js} +1 -1
  51. package/dashboard/static/assets/{UpdateModal-5iKST6jo.js → UpdateModal-D5AKx4Yw.js} +1 -1
  52. package/dashboard/static/assets/index-BKITk1CT.css +1 -0
  53. package/dashboard/static/assets/{index-Cm0VTxJC.js → index-BXDOzSng.js} +3 -3
  54. package/dashboard/static/assets/{outlineNavigation-DkXGL8J6.js → outlineNavigation-JObY7cUY.js} +1 -1
  55. package/dashboard/static/dashboard.html +2 -2
  56. package/package.json +6 -1
  57. package/dashboard/static/assets/ModalRoot-D5Y8IFUG.js +0 -1
  58. package/dashboard/static/assets/index-Dt4tqf43.css +0 -1
@@ -2865,23 +2865,57 @@ def _codex_cache_report_wire(
2865
2865
  )
2866
2866
 
2867
2867
 
2868
- #: Per-file terminal thread aliases, joined to their first accounting entry.
2869
- #: The ``(source_root_key, source_path)`` join predicate needs the composite
2870
- #: ``idx_codex_entries_root_path`` to stay linear: a single-column root index
2871
- #: cannot discriminate when every rollout resolves to one provider root, so the
2872
- #: join degenerates to files x entries. Module-level so the query-plan
2873
- #: regression asserts THIS text rather than a copy that can drift from it.
2868
+ #: Per-file terminal thread aliases, each with its file's first accounting
2869
+ #: instant.
2870
+ #:
2871
+ #: #901 W2 (spec §5.2, T2). The pre-#901 form LEFT JOINed every accounting row
2872
+ #: and GROUPed the join back to one row per file, which needed two temp b-trees
2873
+ #: over the joined history (168 MB of temp-file writes per Codex-active build on
2874
+ #: the measured store). ``codex_session_files.path`` is the primary key, so the
2875
+ #: grouping only ever collapsed one file's joined rows; a correlated MIN per
2876
+ #: file answers the same question with one covering seek of
2877
+ #: ``idx_codex_entries_root_path_time``, and ``idx_codex_files_alias_recent``
2878
+ #: (partial on the same filter, in the final order) removes the sorter. A file
2879
+ #: with no accounting row still yields NULL, and the root qualification is
2880
+ #: unchanged. Module-level so the plan tests assert THIS text rather than a
2881
+ #: copy that can drift from it.
2874
2882
  _CODEX_FILE_ALIAS_SQL = (
2875
2883
  "SELECT f.source_root_key, f.path, f.last_native_thread_id, "
2876
- "f.last_session_id, MIN(e.timestamp_utc) "
2884
+ "f.last_session_id, "
2885
+ "(SELECT MIN(e.timestamp_utc) FROM codex_session_entries AS e "
2886
+ "WHERE e.source_root_key=f.source_root_key AND e.source_path=f.path) "
2877
2887
  "FROM codex_session_files AS f "
2878
- "LEFT JOIN codex_session_entries AS e "
2879
- "ON e.source_root_key=f.source_root_key AND e.source_path=f.path "
2880
2888
  "WHERE f.last_native_thread_id IS NOT NULL AND f.last_native_thread_id != '' "
2881
- "GROUP BY f.source_root_key, f.path, f.last_native_thread_id, f.last_session_id "
2882
2889
  "ORDER BY f.last_ingested_at DESC, f.path DESC"
2883
2890
  )
2884
2891
 
2892
+ #: Conversation threads with their accounting session and first accounting
2893
+ #: instant (#901 W2, spec §5.2, T2).
2894
+ #:
2895
+ #: Two INDEPENDENT correlated lookups replace the pre-#901 ``WITH accounting``
2896
+ #: CTE, which grouped every accounting row (materialized, then probed through
2897
+ #: an automatic index, then sorted). The accounting session is the row with the
2898
+ #: minimum ``id`` (``idx_codex_entries_root_path``, whose trailing rowid makes it
2899
+ #: a seek) and ``started_at`` is the minimum ``timestamp_utc``
2900
+ #: (``idx_codex_entries_root_path_time``); they need not be the same row, which
2901
+ #: is exactly what the CTE's two independent minima computed.
2902
+ #: ``idx_codex_threads_recent`` serves the final order. A thread with no
2903
+ #: accounting rows still yields NULL for both.
2904
+ _CODEX_CONVERSATION_METADATA_CORE_SQL = (
2905
+ "SELECT t.source_root_key, t.source_path, t.native_thread_id, "
2906
+ "(SELECT e.session_id FROM codex_session_entries AS e WHERE e.id = ("
2907
+ "SELECT MIN(f.id) FROM codex_session_entries AS f "
2908
+ "WHERE f.source_root_key=t.source_root_key AND f.source_path=t.source_path"
2909
+ ")) AS accounting_session_id, "
2910
+ "CAST(t.cwd AS BLOB) AS cwd_blob, "
2911
+ "CAST(t.git_json AS BLOB) AS git_json_blob, "
2912
+ "(SELECT MIN(s.timestamp_utc) FROM codex_session_entries AS s "
2913
+ "WHERE s.source_root_key=t.source_root_key AND s.source_path=t.source_path"
2914
+ ") AS started_at, t.last_seen_utc "
2915
+ "FROM codex_conversation_threads AS t "
2916
+ "ORDER BY t.last_seen_utc DESC, t.conversation_key DESC"
2917
+ )
2918
+
2885
2919
 
2886
2920
  @dataclass(frozen=True)
2887
2921
  class _CodexConversationMetadataRead:
@@ -3015,23 +3049,7 @@ def _codex_conversation_metadata(
3015
3049
  metadata: dict[tuple[str, str], dict[str, object]] = {}
3016
3050
  try:
3017
3051
  core_rows = tuple(cache_conn.execute(
3018
- "WITH accounting AS ("
3019
- " SELECT source_root_key, source_path, MIN(id) AS first_id,"
3020
- " MIN(timestamp_utc) AS started_at"
3021
- " FROM codex_session_entries"
3022
- " GROUP BY source_root_key, source_path"
3023
- ") "
3024
- "SELECT t.source_root_key, t.source_path, t.native_thread_id, "
3025
- "e.session_id AS accounting_session_id, "
3026
- "CAST(t.cwd AS BLOB) AS cwd_blob, "
3027
- "CAST(t.git_json AS BLOB) AS git_json_blob, "
3028
- "a.started_at, t.last_seen_utc "
3029
- "FROM codex_conversation_threads AS t "
3030
- "LEFT JOIN accounting AS a "
3031
- "ON a.source_root_key=t.source_root_key AND a.source_path=t.source_path "
3032
- "LEFT JOIN codex_session_entries AS e ON e.id=a.first_id "
3033
- "ORDER BY t.last_seen_utc DESC, t.conversation_key DESC"
3034
- ))
3052
+ _CODEX_CONVERSATION_METADATA_CORE_SQL))
3035
3053
  from _cctally_cache import _codex_conversation_project_attribution
3036
3054
 
3037
3055
  def _attribution(root_key, cwd_blob, git_json_blob):
@@ -4535,6 +4553,28 @@ _CODEX_PROJECT_LABEL_CACHE: dict[object, dict[str, object]] = _ObservedDict()
4535
4553
  _CODEX_PROJECT_LABEL_ALGORITHM_VERSION = "collision-safe-labels-v1"
4536
4554
 
4537
4555
 
4556
+ def _codex_visible_population_name(
4557
+ signature: tuple | None,
4558
+ token: int | None,
4559
+ visible_start: dt.datetime,
4560
+ ) -> tuple | None:
4561
+ """#872: the exact in-process name of one VISIBLE Codex population.
4562
+
4563
+ The carrier signature alone repeats across a cleared state (its
4564
+ generation leg restarts at one, #857), and it names the CARRIER window,
4565
+ which a configured budget widens below the visible lower bound. The
4566
+ never-reused provenance token closes the first gap and the visible lower
4567
+ bound the second. `None` — no signature (no ledger) or no token (metadata
4568
+ incomplete) — names nothing, so it can be neither a hit nor a base.
4569
+ """
4570
+ if signature is None or token is None:
4571
+ return None
4572
+ return (
4573
+ *signature, ("provenance", int(token)),
4574
+ ("visible-start", visible_start),
4575
+ )
4576
+
4577
+
4538
4578
  def _codex_project_label_signature(base: tuple | None) -> tuple | None:
4539
4579
  """Extend one accounting population name with the labelling algorithm's.
4540
4580
 
@@ -4554,6 +4594,9 @@ def _codex_project_label_signature(base: tuple | None) -> tuple | None:
4554
4594
  def _codex_project_population_signature() -> tuple | None:
4555
4595
  """The identity the project and label caches compare instead of scanning.
4556
4596
 
4597
+ Test-facing since #872: a build names its population from its OWN capture
4598
+ (`_codex_visible_population_name`), never from this process-wide reading.
4599
+
4557
4600
  `None` means the accounting carrier could not establish an identity, and
4558
4601
  every consumer must then prove its hit the old way rather than assume one.
4559
4602
  """
@@ -4722,124 +4765,41 @@ def _cached_projects_wire(
4722
4765
  semantic_signature: object,
4723
4766
  population_signature: tuple | None = None,
4724
4767
  ) -> dict[str, object]:
4725
- """Rebuild only project groups touched by accounting changes.
4726
-
4727
- The population half of the key is the accounting carrier's own signature
4728
- when it can name one, and the per-entry `(project_key, project_label)`
4729
- multiset only when it cannot. The multiset answered the same question by
4730
- visiting every row, which made the nominal reuse path proportional to the
4731
- whole population on every tick.
4768
+ """Reuse the project wire only under a name that identifies the population.
4769
+
4770
+ The name is `(semantic_signature, population_signature)`, where the
4771
+ population half is the visible population's #872 name (provenance token
4772
+ and visible start included). An equal name therefore means an unchanged
4773
+ population, so a hit returns the retained value as it stands. A miss
4774
+ rebuilds through `_projects_wire`, whose label cache advances by the delta
4775
+ when G1 has kept the layer coherent. `None` names nothing: no reuse.
4776
+
4777
+ The former group splice applied a delta onto retained groups under a
4778
+ matching key and read labels from the label cache's state. With a sound
4779
+ name it was reachable only through a reused name or the signature-less
4780
+ pair-set key, and there it raised or double-counted (#872 D3).
4732
4781
  """
4733
4782
  values = tuple(entries)
4734
- changed_old = tuple(changed_old)
4735
- changed_new = tuple(changed_new)
4736
- if population_signature is not None:
4737
- population_key: object = population_signature
4738
- else:
4739
- population_key = frozenset(
4740
- (str(entry.project_key), str(entry.project_label))
4741
- for entry in values
4742
- )
4743
- # `entries` is already the complete half-open population for the advancing
4744
- # upper bound. A moving wall clock is therefore not an aggregation
4745
- # semantic: `build_cached_codex_accounting` emits newly-visible rows in the
4746
- # delta when its upper bound advances. Keeping `accounting_end` here made
4747
- # every live dirty tick discard every project group before that delta could
4748
- # be spliced.
4749
- signature = (semantic_signature, population_key, context.range_start)
4750
- state = _CODEX_PROJECT_WIRE_CACHE.get(cache_key)
4751
- if state is None or state[0] != signature:
4752
- value = _projects_wire(
4783
+ if population_signature is None:
4784
+ _CODEX_PROJECT_WIRE_CACHE.pop(cache_key, None)
4785
+ return _projects_wire(
4753
4786
  context, quota_observations, values,
4754
4787
  accounting_end=accounting_end, cache_key=cache_key,
4755
- population_signature=population_signature,
4788
+ population_signature=None,
4756
4789
  changed_old=changed_old, changed_new=changed_new,
4757
4790
  )
4758
- groups: dict[tuple[str, str], list[object]] = {}
4759
- for entry in values:
4760
- groups.setdefault(
4761
- (str(entry.source_root_key), str(entry.project_key)), [],
4762
- ).append(entry)
4763
- _CODEX_PROJECT_WIRE_CACHE[cache_key] = (
4764
- signature, value,
4765
- {key: tuple(group) for key, group in groups.items()},
4766
- )
4767
- return value
4768
-
4769
- _CODEX_PROJECT_WIRE_CACHE.touch(cache_key)
4770
- affected = {
4771
- (str(entry.source_root_key), str(entry.project_key))
4772
- for entry in (*changed_old, *changed_new)
4773
- }
4774
- if not affected:
4791
+ signature = (semantic_signature, population_signature)
4792
+ state = _CODEX_PROJECT_WIRE_CACHE.get(cache_key)
4793
+ if state is not None and state[0] == signature:
4794
+ _CODEX_PROJECT_WIRE_CACHE.touch(cache_key)
4775
4795
  return state[1]
4776
- label_state = _CODEX_PROJECT_LABEL_CACHE.get(cache_key) or {}
4777
- labels = label_state.get("labels") or {}
4778
- old_ids = {
4779
- int(getattr(entry, "cache_entry_id", 0) or 0)
4780
- for entry in changed_old
4781
- }
4782
- groups = dict(state[2])
4783
- for key in affected:
4784
- groups[key] = tuple(
4785
- entry for entry in groups.get(key, ())
4786
- if int(getattr(entry, "cache_entry_id", 0) or 0) not in old_ids
4787
- )
4788
- for entry in changed_new:
4789
- key = (str(entry.source_root_key), str(entry.project_key))
4790
- groups[key] = (*groups.get(key, ()), entry)
4791
- for key in affected:
4792
- if groups.get(key):
4793
- groups[key] = tuple(sorted(
4794
- groups[key], key=_codex_incremental_entry_order,
4795
- ))
4796
- else:
4797
- groups.pop(key, None)
4798
- partial_entries = tuple(
4799
- replace(entry, display_label=labels[str(entry.project_key)])
4800
- for key in sorted(affected) for entry in groups.get(key, ())
4801
- )
4802
- result = build_codex_project_result(
4803
- partial_entries,
4804
- range_start=context.range_start,
4805
- range_end=accounting_end,
4806
- as_of=context.now_utc,
4796
+ value = _projects_wire(
4797
+ context, quota_observations, values,
4798
+ accounting_end=accounting_end, cache_key=cache_key,
4799
+ population_signature=population_signature,
4800
+ changed_old=changed_old, changed_new=changed_new,
4807
4801
  )
4808
- partial_rows = () if result.data is None else tuple({
4809
- "key": dashboard_resource_key("project", "codex", row.project_key),
4810
- "source": "codex",
4811
- "label": row.display_label,
4812
- "session_count": row.session_count,
4813
- "first_seen": row.first_seen.astimezone(UTC).isoformat(),
4814
- "last_seen": row.last_seen.astimezone(UTC).isoformat(),
4815
- "cost_usd": row.totals.cost_usd,
4816
- "input_tokens": row.totals.input_tokens,
4817
- "cached_input_tokens": row.totals.cached_input_tokens,
4818
- "output_tokens": row.totals.output_tokens,
4819
- "reasoning_output_tokens": row.totals.reasoning_output_tokens,
4820
- "total_tokens": row.totals.total_tokens,
4821
- } for row in result.data.projects)
4822
- affected_keys = {
4823
- dashboard_resource_key("project", "codex", project_key)
4824
- for _root_key, project_key in affected
4825
- }
4826
- rows = tuple(sorted(
4827
- (
4828
- *(row for row in state[1]["rows"] if row["key"] not in affected_keys),
4829
- *partial_rows,
4830
- ),
4831
- key=lambda row: (
4832
- float(row["cost_usd"]), str(row["label"]), str(row["key"]),
4833
- ),
4834
- reverse=True,
4835
- ))
4836
- value = {
4837
- "rows": rows,
4838
- "total_cost_usd": stable_sum(
4839
- float(row["cost_usd"]) for row in rows),
4840
- "total_tokens": sum(int(row["total_tokens"]) for row in rows),
4841
- }
4842
- _CODEX_PROJECT_WIRE_CACHE[cache_key] = (signature, value, groups)
4802
+ _CODEX_PROJECT_WIRE_CACHE[cache_key] = (signature, value)
4843
4803
  return value
4844
4804
 
4845
4805
 
@@ -5648,6 +5608,12 @@ def _codex_fold_visible_rows(
5648
5608
 
5649
5609
 
5650
5610
  _CODEX_VISIBLE_POPULATION_MAX_BYTES = 128 * 1024 * 1024
5611
+ #: #872 (review F7): every per-account visible version comes from this
5612
+ #: allocator, which nothing rewinds. `_CODEX_ACCOUNT_CARD_TOTALS_CACHE` keys
5613
+ #: card totals on these versions, and the two caches are evicted
5614
+ #: independently; a reconstruction that restarted at one would re-issue a
5615
+ #: version a surviving card-total entry still holds and serve its old spend.
5616
+ _CODEX_VISIBLE_ACCOUNT_VERSIONS = itertools.count(1)
5651
5617
  _CODEX_VISIBLE_POPULATION_CACHE: dict[str, object] = _ObservedDict()
5652
5618
  _CODEX_ACCOUNT_CARD_TOTALS_MAX_BYTES = 256 * 1024
5653
5619
  _CODEX_ACCOUNT_CARD_TOTAL_ESTIMATED_BYTES = 1024
@@ -5780,7 +5746,9 @@ def _cached_codex_visible_rows(
5780
5746
  "entries_by_id": entry_by_id,
5781
5747
  "rows_by_account": dict(rows_by_account),
5782
5748
  "entries_by_account": dict(entries_by_account),
5783
- "account_versions": {key: 1 for key in rows_by_account},
5749
+ "account_versions": {
5750
+ key: next(_CODEX_VISIBLE_ACCOUNT_VERSIONS)
5751
+ for key in rows_by_account},
5784
5752
  "estimated_bytes": estimated_bytes,
5785
5753
  "entry_count": len(rows),
5786
5754
  "account_count": len(rows_by_account),
@@ -5848,7 +5816,7 @@ def _cached_codex_visible_rows(
5848
5816
  else:
5849
5817
  rows_by_account.pop(key, None)
5850
5818
  entries_by_account.pop(key, None)
5851
- account_versions[key] = int(account_versions.get(key, 0)) + 1
5819
+ account_versions[key] = next(_CODEX_VISIBLE_ACCOUNT_VERSIONS)
5852
5820
 
5853
5821
  try:
5854
5822
  entries = tuple(
@@ -5895,13 +5863,59 @@ _CODEX_ACCOUNT_SCOPE_CACHE: dict[
5895
5863
  ] = _ObservedDict()
5896
5864
 
5897
5865
 
5898
- def reset_codex_account_scope_cache() -> None:
5899
- """Test/process reset for #582's immutable finalized account scopes."""
5866
+ @dataclass(frozen=True)
5867
+ class _CodexDerivedCoherence:
5868
+ """#872: the population every delta-advanced derived Codex cache reflects.
5869
+
5870
+ `name` is the visible-population name the whole derived layer was last
5871
+ advanced to, or `None` when that cannot be stated; `account_keys` is
5872
+ every account whose per-account derived state was built since the last
5873
+ discard. ONE immutable value under ONE key, so the two are written,
5874
+ journaled, charged and evicted together and can never disagree.
5875
+ """
5876
+
5877
+ name: tuple | None
5878
+ account_keys: frozenset
5879
+
5880
+
5881
+ _CODEX_DERIVED_COHERENCE_KEY = "state"
5882
+ _CODEX_DERIVED_COHERENCE: dict[str, _CodexDerivedCoherence] = _ObservedDict()
5883
+
5884
+
5885
+ def _codex_derived_coherence() -> _CodexDerivedCoherence:
5886
+ """The current record; an absent or evicted slot names nothing."""
5887
+ record = _CODEX_DERIVED_COHERENCE.get(_CODEX_DERIVED_COHERENCE_KEY)
5888
+ if isinstance(record, _CodexDerivedCoherence):
5889
+ return record
5890
+ return _CodexDerivedCoherence(None, frozenset())
5891
+
5892
+
5893
+ def reset_codex_account_scope_cache(*, keep_counters: bool = False) -> None:
5894
+ """Discard every delta-advanced derived Codex cache and the #872 record.
5895
+
5896
+ The complete derived reset: a test/process reset, the metadata-incomplete
5897
+ fold's reset, and — with `keep_counters=True` — #872's G1 discard, which
5898
+ runs in production on every base mismatch. G1 must not zero the
5899
+ cumulative fallback counters `bin/cctally-snapshot-measure` reads (the
5900
+ card-totals fallbacks and the visible population's `fallback_count`), so
5901
+ it keeps them; the caches themselves are discarded either way.
5902
+ """
5900
5903
  global _CODEX_ACCOUNT_CARD_TOTALS_FALLBACKS
5904
+ visible_fallbacks = int(
5905
+ _CODEX_VISIBLE_POPULATION_CACHE.get("fallback_count", 0) or 0)
5901
5906
  _CODEX_ACCOUNT_SCOPE_CACHE.clear()
5902
5907
  _CODEX_VISIBLE_POPULATION_CACHE.clear()
5908
+ if keep_counters and visible_fallbacks:
5909
+ # The over-size shape `_cached_codex_visible_rows` already writes:
5910
+ # counters only, nothing reusable.
5911
+ _CODEX_VISIBLE_POPULATION_CACHE.update({
5912
+ "fallback_count": visible_fallbacks,
5913
+ "estimated_bytes": 0,
5914
+ "entry_count": 0,
5915
+ })
5903
5916
  _CODEX_ACCOUNT_CARD_TOTALS_CACHE.clear()
5904
- _CODEX_ACCOUNT_CARD_TOTALS_FALLBACKS = 0
5917
+ if not keep_counters:
5918
+ _CODEX_ACCOUNT_CARD_TOTALS_FALLBACKS = 0
5905
5919
  _CODEX_ENTRY_ADAPTER_CACHE.clear()
5906
5920
  _CODEX_PROJECT_LABEL_CACHE.clear()
5907
5921
  _CODEX_PERIOD_VIEW_CACHE.clear()
@@ -5909,6 +5923,7 @@ def reset_codex_account_scope_cache() -> None:
5909
5923
  _CODEX_CACHE_REPORT_ROWS.clear()
5910
5924
  _CODEX_SESSION_VIEW_CACHE.clear()
5911
5925
  _CODEX_PROJECT_WIRE_CACHE.clear()
5926
+ _CODEX_DERIVED_COHERENCE.clear()
5912
5927
 
5913
5928
 
5914
5929
  def _codex_account_scopes_wire(
@@ -6494,6 +6509,7 @@ def _codex_source_caches() -> tuple[dict, ...]:
6494
6509
  _CODEX_ACCOUNT_CARD_TOTALS_CACHE,
6495
6510
  _CODEX_WEEKLY_VIEW_CACHE,
6496
6511
  _CODEX_ACCOUNT_SCOPE_CACHE,
6512
+ _CODEX_DERIVED_COHERENCE,
6497
6513
  )
6498
6514
 
6499
6515
 
@@ -6525,7 +6541,7 @@ def _codex_source_caches() -> tuple[dict, ...]:
6525
6541
  # incremental byte estimate of its own, and a second charge computed here would
6526
6542
  # be the same number derived twice. Those readers are named beside the owner.
6527
6543
  #
6528
- # WHAT THE TOTAL IS, AND WHAT IT IS NOT. It is the sum of eleven DECLARED
6544
+ # WHAT THE TOTAL IS, AND WHAT IT IS NOT. It is the sum of twelve DECLARED
6529
6545
  # CHARGES. It is not a measurement of process memory, and
6530
6546
  # `_CODEX_SOURCE_ACCELERATOR_MAX_BYTES` is not a cap on process memory: it is a
6531
6547
  # budget over these charges, and the charges depart from the retained graph in
@@ -6553,8 +6569,8 @@ def _codex_source_caches() -> tuple[dict, ...]:
6553
6569
  # `tests/test_source_cache_ownership.py` holds the WALKED owners to a two-sided
6554
6570
  # band against one walk of their own cache — the upper half is what caught the
6555
6571
  # adapter walking its row's strings once per row — while asserting only the
6556
- # upper bound for the four closed-form owners and asserting that the
6557
- # closed-form set is exactly the four named here.
6572
+ # upper bound for the three closed-form owners and asserting that the
6573
+ # closed-form set is exactly the three named here.
6558
6574
 
6559
6575
 
6560
6576
  def _is_codex_accounting_row(obj) -> bool:
@@ -6626,23 +6642,13 @@ def _charge_project_label_entry(_key, value) -> int:
6626
6642
 
6627
6643
 
6628
6644
  def _charge_project_wire_entry(_key, value) -> int:
6629
- """`(signature, wire, groups)` — the wire is owned, the groups are shells.
6630
-
6631
- `groups` maps a project identity to a tuple of BORROWED accounting rows.
6632
- Its own charge is the mapping plus one tuple shell per project, which is
6633
- constant in the number of projects; descending into the tuples to ask about
6634
- each row would make an ordinary tick proportional to the whole population
6635
- again, which is the cost this tranche exists to remove.
6636
- """
6637
- signature, wire, groups = value[0], value[1], value[2]
6638
- total = sys.getsizeof(value)
6639
- total += retained_size_bytes(
6640
- signature, borrowed=_is_borrowed_codex_payload)
6641
- total += retained_size_bytes(wire, borrowed=_is_borrowed_codex_payload)
6642
- total += sys.getsizeof(groups)
6643
- for key, group in groups.items():
6644
- total += sys.getsizeof(key) + sys.getsizeof(group)
6645
- return total
6645
+ """`(signature, wire)` — both owned; borrowed payloads are excluded."""
6646
+ signature, wire = value[0], value[1]
6647
+ return (
6648
+ sys.getsizeof(value)
6649
+ + retained_size_bytes(signature, borrowed=_is_borrowed_codex_payload)
6650
+ + retained_size_bytes(wire, borrowed=_is_borrowed_codex_payload)
6651
+ )
6646
6652
 
6647
6653
 
6648
6654
  #: How faithfully each owner's declared charge tracks what one walk of that
@@ -6652,9 +6658,9 @@ def _charge_project_wire_entry(_key, value) -> int:
6652
6658
  #: formula that DELIBERATELY UNDERSTATES, with no stated bound on the gap; the
6653
6659
  #: value beside the name is why that trade was taken.
6654
6660
  #:
6655
- #: The budget is compared against a sum that includes the four closed-form
6661
+ #: The budget is compared against a sum that includes the three closed-form
6656
6662
  #: owners, so the budget is a heuristic over declared charges rather than a cap
6657
- #: on memory. Adding a fifth closed-form owner without adding it here is a
6663
+ #: on memory. Adding a fourth closed-form owner without adding it here is a
6658
6664
  #: test failure, which is the only reason this constant exists.
6659
6665
  CODEX_SOURCE_CHARGE_FIDELITY = MappingProxyType({
6660
6666
  "quota_observations": "walked",
@@ -6666,10 +6672,7 @@ CODEX_SOURCE_CHARGE_FIDELITY = MappingProxyType({
6666
6672
  "string each, because a labelled copy is `replace(row, "
6667
6673
  "display_label=...)` and every other field is the borrowed row's own "
6668
6674
  "object"),
6669
- "project_wire": (
6670
- "closed-form: the group containers, never the borrowed rows inside "
6671
- "them, because descending into them makes an ordinary tick "
6672
- "proportional to the whole population"),
6675
+ "project_wire": "walked",
6673
6676
  "entry_adapters": (
6674
6677
  "closed-form: two object shells and one float, because the adapted "
6675
6678
  "entry shares every string with the row beside it and walking it "
@@ -6681,6 +6684,7 @@ CODEX_SOURCE_CHARGE_FIDELITY = MappingProxyType({
6681
6684
  "account_card_totals": "walked",
6682
6685
  "weekly_views": "walked",
6683
6686
  "account_scopes": "walked",
6687
+ "derived_coherence": "walked",
6684
6688
  })
6685
6689
 
6686
6690
 
@@ -6834,8 +6838,9 @@ def _declare_codex_source_cache_owners() -> tuple[_CodexCacheOwner, ...]:
6834
6838
  _CodexCacheOwner(
6835
6839
  "project_wire", _CODEX_PROJECT_WIRE_CACHE,
6836
6840
  _charge_project_wire_entry, None,
6837
- owns="the rendered project wire and the group container shells",
6838
- borrows="the accounting rows inside those groups",
6841
+ owns="the rendered project wire and the population name it was "
6842
+ "built for (#872 removed the retained group splice)",
6843
+ borrows="nothing; borrowed payloads are excluded from its charge",
6839
6844
  ),
6840
6845
  _CodexCacheOwner(
6841
6846
  "entry_adapters", _CODEX_ENTRY_ADAPTER_CACHE,
@@ -6872,6 +6877,13 @@ def _declare_codex_source_cache_owners() -> tuple[_CodexCacheOwner, ...]:
6872
6877
  owns="the finalized per-account scope mappings",
6873
6878
  borrows="accounting rows and adapted entries",
6874
6879
  ),
6880
+ _CodexCacheOwner(
6881
+ "derived_coherence", _CODEX_DERIVED_COHERENCE,
6882
+ _charge_borrowing_payloads, None,
6883
+ owns="the one immutable record naming the population every "
6884
+ "delta-advanced derived cache reflects",
6885
+ borrows="nothing",
6886
+ ),
6875
6887
  )
6876
6888
  caches = _codex_source_caches()
6877
6889
  if tuple(owner.cache for owner in owners) != caches:
@@ -6888,10 +6900,10 @@ _CODEX_SOURCE_CACHE_OWNERS = _declare_codex_source_cache_owners()
6888
6900
 
6889
6901
 
6890
6902
  def codex_source_retained_bytes() -> int:
6891
- """The eleven Codex source caches' DECLARED CHARGES, summed.
6903
+ """The twelve Codex source caches' DECLARED CHARGES, summed.
6892
6904
 
6893
6905
  Read it as "what these caches say they cost", not as "how much memory
6894
- these caches hold". FOUR of the eleven are charged in closed form and
6906
+ these caches hold". THREE of the twelve are charged in closed form and
6895
6907
  understate without a stated bound — see `CODEX_SOURCE_CHARGE_FIDELITY` —
6896
6908
  so this figure is the input to a heuristic budget rather than a
6897
6909
  measurement of the retained graph.
@@ -6907,8 +6919,8 @@ def codex_source_retained_bytes() -> int:
6907
6919
 
6908
6920
 
6909
6921
  #: The budget the declared charges are held to. NOT a bound on process memory:
6910
- #: it is compared against `codex_source_retained_bytes()`, four of whose
6911
- #: eleven terms are closed-form under-charges with no stated bound on the gap.
6922
+ #: it is compared against `codex_source_retained_bytes()`, three of whose
6923
+ #: twelve terms are closed-form under-charges with no stated bound on the gap.
6912
6924
  #: See `CODEX_SOURCE_CHARGE_FIDELITY`.
6913
6925
  _CODEX_SOURCE_ACCELERATOR_MAX_BYTES = 768 * 1024 * 1024
6914
6926
  _CODEX_SOURCE_ACCELERATOR_MAX_ENTRIES = 500_000
@@ -6995,15 +7007,16 @@ def _evict_codex_source_buckets(caches) -> tuple[int, bool]:
6995
7007
 
6996
7008
  Returns `(evicted_entries, saw_an_indivisible_oversized_bucket)`.
6997
7009
 
6998
- Ten of the eleven caches are already keyed by stable semantic units —
7010
+ Eleven of the twelve caches are keyed by stable semantic units —
6999
7011
  `("account", key)`, `("parent",)`, a project identity, a session identity,
7000
- a physical row id — so those keys ARE the buckets and no new partition is
7001
- needed. The eleventh is declared indivisible and is evicted whole; see
7012
+ a physical row id, the #872 coherence record's single key — so those keys
7013
+ ARE the buckets and no new partition is needed. The visible population is
7014
+ declared indivisible and is evicted whole; see
7002
7015
  `CODEX_VISIBLE_POPULATION_INDIVISIBLE_REASON`.
7003
7016
 
7004
7017
  Cold buckets go first and everything else stays resident, which is what
7005
7018
  makes an above-cap steady state rebuild only what it lost. Clearing all
7006
- eleven caches, which is what this replaced, made a population permanently
7019
+ the caches, which is what this replaced, made a population permanently
7007
7020
  above a cap rebuild and discard on every generation: measured on the sealed
7008
7021
  #786 v3 `current` corpus, one run cleared 222,932 entries at a stroke.
7009
7022
 
@@ -7022,7 +7035,7 @@ def _evict_codex_source_buckets(caches) -> tuple[int, bool]:
7022
7035
  # budget leaves the next build one entry over it again, and every tick then
7023
7036
  # pays the candidate build below over every resident key — 222,932 of them
7024
7037
  # on the measurement corpus. Freeing headroom instead lets the early return
7025
- # above answer the next several ticks for eleven integer reads.
7038
+ # above answer the next several ticks for twelve integer reads.
7026
7039
  if count >= int(_CODEX_SOURCE_ACCELERATOR_LOW_WATER_MIN_ENTRIES):
7027
7040
  low_water = min(
7028
7041
  budget, int(budget * _CODEX_SOURCE_ACCELERATOR_LOW_WATER))
@@ -7058,7 +7071,7 @@ def _evict_codex_source_buckets(caches) -> tuple[int, bool]:
7058
7071
  # proportional to the whole population. Together they mean the
7059
7072
  # owner with the largest under-charge is also the last one this
7060
7073
  # pass will ever reach, so freeing memory by lowering the budget
7061
- # acts on the other ten owners first.
7074
+ # acts on the other eleven owners first.
7062
7075
  candidates.append((
7063
7076
  max(recency for recency, _key, _bytes in entries),
7064
7077
  index, position, _CACHE_MISSING, owner.retained_bytes(),
@@ -7102,7 +7115,7 @@ def _evict_codex_source_buckets(caches) -> tuple[int, bool]:
7102
7115
  continue
7103
7116
  evicted += removed
7104
7117
  count -= removed
7105
- # Re-read ONE owner rather than summing all eleven after every single
7118
+ # Re-read ONE owner rather than summing all twelve after every single
7106
7119
  # eviction. A charged owner's total moves by exactly this bucket, and a
7107
7120
  # self-reported one answers for itself; either way the arithmetic below
7108
7121
  # is checked against a full re-read once, after the loop.
@@ -7127,17 +7140,17 @@ def _evict_codex_source_buckets(caches) -> tuple[int, bool]:
7127
7140
  def enforce_codex_source_accelerator_bounds() -> None:
7128
7141
  """Apply byte and entry admission from the incrementally maintained total.
7129
7142
 
7130
- The byte total is a sum of eleven integers each owner keeps current, so
7143
+ The byte total is a sum of twelve integers each owner keeps current, so
7131
7144
  admission is a read. It used to be a background traversal of two full
7132
7145
  container copies of the whole cache set, once per generation, in a worker
7133
7146
  limited to a quarter of one core — which on the measurement corpus never
7134
7147
  finished a generation at all, so the caches were admitted against a byte
7135
7148
  figure that stayed at zero until the walk finally returned the over-budget
7136
- sentinel and cleared all eleven at once.
7149
+ sentinel and cleared every cache at once.
7137
7150
 
7138
7151
  Publication is unconditional now rather than gated on the dirty flag,
7139
7152
  because there is nothing left to schedule: an unchanged generation costs
7140
- the same eleven reads and publishes the same number.
7153
+ the same twelve reads and publishes the same number.
7141
7154
  """
7142
7155
  caches = _codex_source_caches()
7143
7156
  global _CODEX_SOURCE_ACCELERATOR_ESTIMATED_BYTES
@@ -7271,7 +7284,7 @@ _CODEX_SOURCE_PUBLISH_LOCK = threading.Lock()
7271
7284
 
7272
7285
 
7273
7286
  class _CodexSourceBuildOverlay:
7274
- """One build's tentative generation over the eleven source caches.
7287
+ """One build's tentative generation over the twelve source caches.
7275
7288
 
7276
7289
  THE JOURNAL IS THE OVERLAY. Rather than hold changed keys in a separate
7277
7290
  mapping every read would have to consult, each mutation records the key's
@@ -7463,6 +7476,11 @@ class _CodexAccountingCapture:
7463
7476
  #: qualified accounting read: such a generation names no population, and
7464
7477
  #: borrowing the process's current token would name one it never read.
7465
7478
  provenance_token: int | None = None
7479
+ #: #872: the name of the population this capture's delta is relative to,
7480
+ #: from the accounting result (`None` on a cold result and whenever the
7481
+ #: capture bypassed the qualified read, exactly like the fields above).
7482
+ base_population_signature: tuple | None = None
7483
+ base_provenance_token: int | None = None
7466
7484
 
7467
7485
 
7468
7486
  #: The detail routes' horizon: one year ending at the generation instant. The
@@ -7556,6 +7574,9 @@ def _capture_codex_accounting(
7556
7574
  population_signature: tuple | None = None
7557
7575
  # #857: set only by a qualified read that returned; every bypass keeps it.
7558
7576
  provenance_token: int | None = None
7577
+ # #872: the delta's base, under the same rule.
7578
+ base_population_signature: tuple | None = None
7579
+ base_provenance_token: int | None = None
7559
7580
 
7560
7581
  def fallback_entries() -> tuple[object, ...]:
7561
7582
  nonlocal metadata_read_failed, failed_legs
@@ -7625,6 +7646,9 @@ def _capture_codex_accounting(
7625
7646
  changed_new = cached_accounting.changed_new
7626
7647
  population_signature = cached_accounting.population_signature
7627
7648
  provenance_token = cached_accounting.provenance_token
7649
+ base_population_signature = (
7650
+ cached_accounting.base_population_signature)
7651
+ base_provenance_token = cached_accounting.base_provenance_token
7628
7652
  entries: tuple[object, ...] = qualified_entries
7629
7653
  except QualifiedMetadataUnavailable as exc:
7630
7654
  _lib_log.get_logger("dashboard").warning(
@@ -7671,6 +7695,10 @@ def _capture_codex_accounting(
7671
7695
  None if metadata_incomplete else population_signature),
7672
7696
  provenance_token=(
7673
7697
  None if metadata_incomplete else provenance_token),
7698
+ base_population_signature=(
7699
+ None if metadata_incomplete else base_population_signature),
7700
+ base_provenance_token=(
7701
+ None if metadata_incomplete else base_provenance_token),
7674
7702
  )
7675
7703
 
7676
7704
 
@@ -7797,7 +7825,7 @@ def _drop_codex_source_generation(
7797
7825
  builds and replaying it would overwrite whatever the other build
7798
7826
  legitimately published.
7799
7827
 
7800
- ALL ELEVEN CACHES GO, not only the ones this build wrote to, and that is a
7828
+ ALL TWELVE CACHES GO, not only the ones this build wrote to, and that is a
7801
7829
  correctness requirement rather than caution. Ten of them are fed by the
7802
7830
  accounting carrier's DELTA lists, not by its population: a build hands
7803
7831
  `_cached_codex_period_view` and its siblings `changed_old` and
@@ -8320,6 +8348,23 @@ def _build_codex_source_state(
8320
8348
  accounting_changed_new = accounting_capture.changed_new
8321
8349
  accounting_entries = accounting_capture.entries
8322
8350
  metadata_incomplete = accounting_capture.metadata_incomplete
8351
+ # #872 G1: a delta reaches the derived layer only while the layer's
8352
+ # coherence record names the delta's BASE. A cold carrier result has no
8353
+ # base, and a record that names anything else — an overflow-cleared or
8354
+ # reset carrier, a moved visible start, a skipped generation, a vanished
8355
+ # account — means the retained state is not what the delta is relative
8356
+ # to. Every delta-advanced derived cache is then discarded, and each
8357
+ # consumer derives from the full population through its own cold path.
8358
+ derived_base_name = _codex_visible_population_name(
8359
+ accounting_capture.base_population_signature,
8360
+ accounting_capture.base_provenance_token, context.range_start)
8361
+ derived_current_name = _codex_visible_population_name(
8362
+ accounting_capture.population_signature,
8363
+ accounting_capture.provenance_token, context.range_start)
8364
+ prior_coherence = _codex_derived_coherence()
8365
+ if derived_base_name is None or prior_coherence.name != derived_base_name:
8366
+ reset_codex_account_scope_cache(keep_counters=True)
8367
+ prior_coherence = _CodexDerivedCoherence(None, frozenset())
8323
8368
 
8324
8369
  conversation_metadata_read = (
8325
8370
  capture.conversation_metadata
@@ -8557,24 +8602,13 @@ def _build_codex_source_state(
8557
8602
  period_signature = _codex_accounting_period_signature(
8558
8603
  context.cache_conn, context.range_start, metadata_incomplete,
8559
8604
  )
8560
- # The accounting carrier's own exact name for the population these domains
8561
- # are built from. The project and label caches compare it instead of
8562
- # scanning every row to prove a hit; `None` means it could not establish
8563
- # one, and they then prove the hit the old way.
8564
- # Taken from THIS build's own accounting capture where there is one, and
8565
- # only re-read from the process-wide module global on the uncaptured path.
8566
- # See `_CodexAccountingCapture.population_signature`.
8567
- if capture is not None:
8568
- project_population_signature = (
8569
- None if metadata_incomplete
8570
- else _codex_project_label_signature(
8571
- capture.accounting.population_signature)
8572
- )
8573
- else:
8574
- project_population_signature = (
8575
- None if metadata_incomplete
8576
- else _codex_project_population_signature()
8577
- )
8605
+ # The project caches' name for this build's population: the visible
8606
+ # population name G1 computed from THIS build's own capture (sound across
8607
+ # a cleared carrier and a moved visible start, #872), plus the label
8608
+ # algorithm version. `None` when the population cannot be named, and then
8609
+ # nothing is reused. See `_CodexAccountingCapture.population_signature`.
8610
+ project_population_signature = _codex_project_label_signature(
8611
+ derived_current_name)
8578
8612
  daily = _cached_codex_period_view(
8579
8613
  entries, changed_old=changed_old_entries,
8580
8614
  changed_new=changed_new_entries, kind="daily", cache_key=("parent",),
@@ -8796,6 +8830,8 @@ def _build_codex_source_state(
8796
8830
  hero_cycles_wire: list[dict[str, object]] = []
8797
8831
  combined_accounting: dict[str, object] | None = None
8798
8832
  account_scopes: dict[str, dict[str, object]] = {}
8833
+ # #872 G2 input: whether the per-account scopes were built this build.
8834
+ account_scope_status = "not-built"
8799
8835
  # #556 S5 §3.8: bound OUTSIDE the try, because the degrade path below has to
8800
8836
  # be able to clear it, and the retained `clock_data` reads it either way.
8801
8837
  budget_events_by_account: dict[str, tuple[tuple[dt.datetime, float], ...]] = {}
@@ -8941,6 +8977,7 @@ def _build_codex_source_state(
8941
8977
  scope_signature=period_signature,
8942
8978
  project_population_signature=project_population_signature,
8943
8979
  )
8980
+ account_scope_status = "built"
8944
8981
  # #416 QA P1-A — the "All accounts" Blocks panel is the UNION of
8945
8982
  # every account's 5-hour blocks. `_quota_wire` filters
8946
8983
  # `str(root_key) not in cycle.source_root_keys` against a single
@@ -8997,6 +9034,7 @@ def _build_codex_source_state(
8997
9034
  ))
8998
9035
  quota = {**quota, "blocks": merged_blocks}
8999
9036
  except (sqlite3.Error, QualifiedMetadataUnavailable):
9037
+ account_scope_status = "degraded"
9000
9038
  # A per-account wire failure must never fail the whole source build;
9001
9039
  # degrade to the byte-stable undecorated shape.
9002
9040
  accounts_wire = []
@@ -9129,6 +9167,31 @@ def _build_codex_source_state(
9129
9167
  {name: operands[name] for name in _HERO_CARD_OPERANDS
9130
9168
  if name in operands}
9131
9169
  )
9170
+ # #872 G2: the record advances to this build's population only when the
9171
+ # build advanced EVERY delta-advanced consumer over the capture's whole
9172
+ # delta. Completeness is stated positively: metadata complete and not
9173
+ # transient (a transient generation skips both project legs), scopes not
9174
+ # degraded, and every account that holds per-account state still live
9175
+ # (an account that left the live set kept a stale state the delta never
9176
+ # reached). Anything else records `None`, so the next build's G1 discards.
9177
+ scope_keys = frozenset(str(key) for key in account_scopes)
9178
+ coherent = (
9179
+ derived_current_name is not None
9180
+ and not metadata_incomplete
9181
+ and not metadata_transient
9182
+ and account_scope_status != "degraded"
9183
+ and prior_coherence.account_keys <= scope_keys
9184
+ )
9185
+ record = _CodexDerivedCoherence(
9186
+ name=derived_current_name if coherent else None,
9187
+ account_keys=prior_coherence.account_keys | scope_keys,
9188
+ )
9189
+ if _CODEX_DERIVED_COHERENCE.get(_CODEX_DERIVED_COHERENCE_KEY) == record:
9190
+ # Unchanged (a warm-clean tick on the same ledger head): say it was
9191
+ # used without rewriting it, so an idle tick dirties nothing.
9192
+ _CODEX_DERIVED_COHERENCE.touch(_CODEX_DERIVED_COHERENCE_KEY)
9193
+ else:
9194
+ _CODEX_DERIVED_COHERENCE[_CODEX_DERIVED_COHERENCE_KEY] = record
9132
9195
  return SourceDashboardState(
9133
9196
  source="codex",
9134
9197
  availability=availability,