cctally 1.113.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 (97) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +4 -6
  3. package/bin/_cctally_account.py +32 -8
  4. package/bin/_cctally_cache.py +6035 -722
  5. package/bin/_cctally_completion.py +1287 -0
  6. package/bin/_cctally_config.py +37 -0
  7. package/bin/_cctally_core.py +400 -2
  8. package/bin/_cctally_dashboard.py +98 -20
  9. package/bin/_cctally_dashboard_conversation.py +292 -23
  10. package/bin/_cctally_dashboard_envelope.py +16 -3
  11. package/bin/_cctally_dashboard_perf.py +128 -9
  12. package/bin/_cctally_dashboard_sources.py +285 -213
  13. package/bin/_cctally_db.py +1177 -23
  14. package/bin/_cctally_diagnosis_sources.py +22 -4
  15. package/bin/_cctally_doctor.py +572 -45
  16. package/bin/_cctally_five_hour.py +84 -33
  17. package/bin/_cctally_forecast.py +108 -15
  18. package/bin/_cctally_journal.py +3950 -116
  19. package/bin/_cctally_journal_repair.py +101 -12
  20. package/bin/_cctally_milestone_history.py +82 -9
  21. package/bin/_cctally_parser.py +42 -0
  22. package/bin/_cctally_percent_breakdown.py +3 -2
  23. package/bin/_cctally_project.py +8 -2
  24. package/bin/_cctally_quarantine.py +652 -0
  25. package/bin/_cctally_quota.py +227 -37
  26. package/bin/_cctally_quota_model.py +360 -83
  27. package/bin/_cctally_record.py +691 -149
  28. package/bin/_cctally_rederive.py +320 -41
  29. package/bin/_cctally_refresh.py +84 -13
  30. package/bin/_cctally_source_analytics.py +83 -17
  31. package/bin/_cctally_statusline.py +6154 -517
  32. package/bin/_cctally_store.py +367 -53
  33. package/bin/_cctally_transcript.py +8 -7
  34. package/bin/_cctally_tui.py +61 -56
  35. package/bin/_cctally_weekrefs.py +128 -21
  36. package/bin/_lib_account_binding.py +152 -0
  37. package/bin/_lib_account_protocol.py +386 -0
  38. package/bin/_lib_aggregators.py +4 -0
  39. package/bin/_lib_cache_coverage.py +13 -1
  40. package/bin/_lib_cache_report.py +35 -6
  41. package/bin/_lib_cache_writer_lock.py +19 -3
  42. package/bin/_lib_clone_copy.py +142 -0
  43. package/bin/_lib_codex_conversation.py +32 -0
  44. package/bin/_lib_codex_conversation_query.py +297 -49
  45. package/bin/_lib_codex_conversation_watch.py +59 -14
  46. package/bin/_lib_codex_drain_admission.py +214 -0
  47. package/bin/_lib_codex_metadata.py +18 -1
  48. package/bin/_lib_codex_publication_barrier.py +277 -0
  49. package/bin/_lib_codex_rollout_container.py +502 -0
  50. package/bin/_lib_conversation_dispatch.py +3 -0
  51. package/bin/_lib_conversation_query.py +106 -59
  52. package/bin/_lib_conversation_retention.py +1605 -640
  53. package/bin/_lib_cost_provenance.py +53 -2
  54. package/bin/_lib_detector_access_registry.py +3817 -0
  55. package/bin/_lib_doctor.py +714 -69
  56. package/bin/_lib_five_hour.py +126 -0
  57. package/bin/_lib_fmt.py +11 -0
  58. package/bin/_lib_ingest_frontier.py +414 -20
  59. package/bin/_lib_journal.py +141 -10
  60. package/bin/_lib_jsonl.py +5 -4
  61. package/bin/_lib_legacy_spool.py +126 -0
  62. package/bin/_lib_legacy_writer_evidence.py +136 -0
  63. package/bin/_lib_merge_gate.py +231 -86
  64. package/bin/_lib_meter_rate_change.py +10 -0
  65. package/bin/_lib_outcome_quarantine.py +690 -0
  66. package/bin/_lib_perf.py +27 -1
  67. package/bin/_lib_pricing.py +199 -9
  68. package/bin/_lib_pricing_check.py +20 -4
  69. package/bin/_lib_quarantine.py +154 -0
  70. package/bin/_lib_quota_calibration.py +53 -8
  71. package/bin/_lib_quota_copy.py +2 -1
  72. package/bin/_lib_quota_model.py +288 -46
  73. package/bin/_lib_reclaim_planner.py +792 -0
  74. package/bin/_lib_rederive.py +319 -82
  75. package/bin/_lib_segment_summary.py +35 -2
  76. package/bin/_lib_snapshot_cache.py +21 -0
  77. package/bin/_lib_sqlite_close.py +81 -0
  78. package/bin/_lib_statusline_candidates.py +292 -23
  79. package/bin/_lib_subscription_weeks.py +100 -17
  80. package/bin/_lib_tick_stats.py +217 -17
  81. package/bin/_lib_wal_checkpoint.py +1077 -0
  82. package/bin/_lib_write_budget.py +483 -0
  83. package/bin/_lib_write_io.py +703 -0
  84. package/bin/cctally +31 -1
  85. package/dashboard/static/assets/{ConversationsView-CEZ-BUyi.js → ConversationsView-Bc8LCOAZ.js} +1 -1
  86. package/dashboard/static/assets/{DoctorModal-CE1oH_Zy.js → DoctorModal-D--qVBwe.js} +1 -1
  87. package/dashboard/static/assets/ModalRoot-DLPgyAM_.js +1 -0
  88. package/dashboard/static/assets/{ProjectsDrillPanel-B3DZoCeD.js → ProjectsDrillPanel-z53a4v4M.js} +1 -1
  89. package/dashboard/static/assets/{SourceDetailModal-CHp9EgLP.js → SourceDetailModal-Bw0T8W-W.js} +1 -1
  90. package/dashboard/static/assets/{UpdateModal-5iKST6jo.js → UpdateModal-D5AKx4Yw.js} +1 -1
  91. package/dashboard/static/assets/index-BKITk1CT.css +1 -0
  92. package/dashboard/static/assets/{index-Cm0VTxJC.js → index-BXDOzSng.js} +3 -3
  93. package/dashboard/static/assets/{outlineNavigation-DkXGL8J6.js → outlineNavigation-JObY7cUY.js} +1 -1
  94. package/dashboard/static/dashboard.html +2 -2
  95. package/package.json +19 -1
  96. package/dashboard/static/assets/ModalRoot-D5Y8IFUG.js +0 -1
  97. package/dashboard/static/assets/index-Dt4tqf43.css +0 -1
@@ -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,
@@ -2865,23 +2868,57 @@ def _codex_cache_report_wire(
2865
2868
  )
2866
2869
 
2867
2870
 
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.
2871
+ #: Per-file terminal thread aliases, each with its file's first accounting
2872
+ #: instant.
2873
+ #:
2874
+ #: #901 W2 (spec §5.2, T2). The pre-#901 form LEFT JOINed every accounting row
2875
+ #: and GROUPed the join back to one row per file, which needed two temp b-trees
2876
+ #: over the joined history (168 MB of temp-file writes per Codex-active build on
2877
+ #: the measured store). ``codex_session_files.path`` is the primary key, so the
2878
+ #: grouping only ever collapsed one file's joined rows; a correlated MIN per
2879
+ #: file answers the same question with one covering seek of
2880
+ #: ``idx_codex_entries_root_path_time``, and ``idx_codex_files_alias_recent``
2881
+ #: (partial on the same filter, in the final order) removes the sorter. A file
2882
+ #: with no accounting row still yields NULL, and the root qualification is
2883
+ #: unchanged. Module-level so the plan tests assert THIS text rather than a
2884
+ #: copy that can drift from it.
2874
2885
  _CODEX_FILE_ALIAS_SQL = (
2875
2886
  "SELECT f.source_root_key, f.path, f.last_native_thread_id, "
2876
- "f.last_session_id, MIN(e.timestamp_utc) "
2887
+ "f.last_session_id, "
2888
+ "(SELECT MIN(e.timestamp_utc) FROM codex_session_entries AS e "
2889
+ "WHERE e.source_root_key=f.source_root_key AND e.source_path=f.path) "
2877
2890
  "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
2891
  "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
2892
  "ORDER BY f.last_ingested_at DESC, f.path DESC"
2883
2893
  )
2884
2894
 
2895
+ #: Conversation threads with their accounting session and first accounting
2896
+ #: instant (#901 W2, spec §5.2, T2).
2897
+ #:
2898
+ #: Two INDEPENDENT correlated lookups replace the pre-#901 ``WITH accounting``
2899
+ #: CTE, which grouped every accounting row (materialized, then probed through
2900
+ #: an automatic index, then sorted). The accounting session is the row with the
2901
+ #: minimum ``id`` (``idx_codex_entries_root_path``, whose trailing rowid makes it
2902
+ #: a seek) and ``started_at`` is the minimum ``timestamp_utc``
2903
+ #: (``idx_codex_entries_root_path_time``); they need not be the same row, which
2904
+ #: is exactly what the CTE's two independent minima computed.
2905
+ #: ``idx_codex_threads_recent`` serves the final order. A thread with no
2906
+ #: accounting rows still yields NULL for both.
2907
+ _CODEX_CONVERSATION_METADATA_CORE_SQL = (
2908
+ "SELECT t.source_root_key, t.source_path, t.native_thread_id, "
2909
+ "(SELECT e.session_id FROM codex_session_entries AS e WHERE e.id = ("
2910
+ "SELECT MIN(f.id) FROM codex_session_entries AS f "
2911
+ "WHERE f.source_root_key=t.source_root_key AND f.source_path=t.source_path"
2912
+ ")) AS accounting_session_id, "
2913
+ "CAST(t.cwd AS BLOB) AS cwd_blob, "
2914
+ "CAST(t.git_json AS BLOB) AS git_json_blob, "
2915
+ "(SELECT MIN(s.timestamp_utc) FROM codex_session_entries AS s "
2916
+ "WHERE s.source_root_key=t.source_root_key AND s.source_path=t.source_path"
2917
+ ") AS started_at, t.last_seen_utc "
2918
+ "FROM codex_conversation_threads AS t "
2919
+ "ORDER BY t.last_seen_utc DESC, t.conversation_key DESC"
2920
+ )
2921
+
2885
2922
 
2886
2923
  @dataclass(frozen=True)
2887
2924
  class _CodexConversationMetadataRead:
@@ -3015,23 +3052,7 @@ def _codex_conversation_metadata(
3015
3052
  metadata: dict[tuple[str, str], dict[str, object]] = {}
3016
3053
  try:
3017
3054
  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
- ))
3055
+ _CODEX_CONVERSATION_METADATA_CORE_SQL))
3035
3056
  from _cctally_cache import _codex_conversation_project_attribution
3036
3057
 
3037
3058
  def _attribution(root_key, cwd_blob, git_json_blob):
@@ -4535,6 +4556,28 @@ _CODEX_PROJECT_LABEL_CACHE: dict[object, dict[str, object]] = _ObservedDict()
4535
4556
  _CODEX_PROJECT_LABEL_ALGORITHM_VERSION = "collision-safe-labels-v1"
4536
4557
 
4537
4558
 
4559
+ def _codex_visible_population_name(
4560
+ signature: tuple | None,
4561
+ token: int | None,
4562
+ visible_start: dt.datetime,
4563
+ ) -> tuple | None:
4564
+ """#872: the exact in-process name of one VISIBLE Codex population.
4565
+
4566
+ The carrier signature alone repeats across a cleared state (its
4567
+ generation leg restarts at one, #857), and it names the CARRIER window,
4568
+ which a configured budget widens below the visible lower bound. The
4569
+ never-reused provenance token closes the first gap and the visible lower
4570
+ bound the second. `None` — no signature (no ledger) or no token (metadata
4571
+ incomplete) — names nothing, so it can be neither a hit nor a base.
4572
+ """
4573
+ if signature is None or token is None:
4574
+ return None
4575
+ return (
4576
+ *signature, ("provenance", int(token)),
4577
+ ("visible-start", visible_start),
4578
+ )
4579
+
4580
+
4538
4581
  def _codex_project_label_signature(base: tuple | None) -> tuple | None:
4539
4582
  """Extend one accounting population name with the labelling algorithm's.
4540
4583
 
@@ -4554,6 +4597,9 @@ def _codex_project_label_signature(base: tuple | None) -> tuple | None:
4554
4597
  def _codex_project_population_signature() -> tuple | None:
4555
4598
  """The identity the project and label caches compare instead of scanning.
4556
4599
 
4600
+ Test-facing since #872: a build names its population from its OWN capture
4601
+ (`_codex_visible_population_name`), never from this process-wide reading.
4602
+
4557
4603
  `None` means the accounting carrier could not establish an identity, and
4558
4604
  every consumer must then prove its hit the old way rather than assume one.
4559
4605
  """
@@ -4722,124 +4768,41 @@ def _cached_projects_wire(
4722
4768
  semantic_signature: object,
4723
4769
  population_signature: tuple | None = None,
4724
4770
  ) -> 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.
4771
+ """Reuse the project wire only under a name that identifies the population.
4772
+
4773
+ The name is `(semantic_signature, population_signature)`, where the
4774
+ population half is the visible population's #872 name (provenance token
4775
+ and visible start included). An equal name therefore means an unchanged
4776
+ population, so a hit returns the retained value as it stands. A miss
4777
+ rebuilds through `_projects_wire`, whose label cache advances by the delta
4778
+ when G1 has kept the layer coherent. `None` names nothing: no reuse.
4779
+
4780
+ The former group splice applied a delta onto retained groups under a
4781
+ matching key and read labels from the label cache's state. With a sound
4782
+ name it was reachable only through a reused name or the signature-less
4783
+ pair-set key, and there it raised or double-counted (#872 D3).
4732
4784
  """
4733
4785
  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(
4786
+ if population_signature is None:
4787
+ _CODEX_PROJECT_WIRE_CACHE.pop(cache_key, None)
4788
+ return _projects_wire(
4753
4789
  context, quota_observations, values,
4754
4790
  accounting_end=accounting_end, cache_key=cache_key,
4755
- population_signature=population_signature,
4791
+ population_signature=None,
4756
4792
  changed_old=changed_old, changed_new=changed_new,
4757
4793
  )
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:
4794
+ signature = (semantic_signature, population_signature)
4795
+ state = _CODEX_PROJECT_WIRE_CACHE.get(cache_key)
4796
+ if state is not None and state[0] == signature:
4797
+ _CODEX_PROJECT_WIRE_CACHE.touch(cache_key)
4775
4798
  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,
4799
+ value = _projects_wire(
4800
+ context, quota_observations, values,
4801
+ accounting_end=accounting_end, cache_key=cache_key,
4802
+ population_signature=population_signature,
4803
+ changed_old=changed_old, changed_new=changed_new,
4807
4804
  )
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)
4805
+ _CODEX_PROJECT_WIRE_CACHE[cache_key] = (signature, value)
4843
4806
  return value
4844
4807
 
4845
4808
 
@@ -5648,6 +5611,12 @@ def _codex_fold_visible_rows(
5648
5611
 
5649
5612
 
5650
5613
  _CODEX_VISIBLE_POPULATION_MAX_BYTES = 128 * 1024 * 1024
5614
+ #: #872 (review F7): every per-account visible version comes from this
5615
+ #: allocator, which nothing rewinds. `_CODEX_ACCOUNT_CARD_TOTALS_CACHE` keys
5616
+ #: card totals on these versions, and the two caches are evicted
5617
+ #: independently; a reconstruction that restarted at one would re-issue a
5618
+ #: version a surviving card-total entry still holds and serve its old spend.
5619
+ _CODEX_VISIBLE_ACCOUNT_VERSIONS = itertools.count(1)
5651
5620
  _CODEX_VISIBLE_POPULATION_CACHE: dict[str, object] = _ObservedDict()
5652
5621
  _CODEX_ACCOUNT_CARD_TOTALS_MAX_BYTES = 256 * 1024
5653
5622
  _CODEX_ACCOUNT_CARD_TOTAL_ESTIMATED_BYTES = 1024
@@ -5780,7 +5749,9 @@ def _cached_codex_visible_rows(
5780
5749
  "entries_by_id": entry_by_id,
5781
5750
  "rows_by_account": dict(rows_by_account),
5782
5751
  "entries_by_account": dict(entries_by_account),
5783
- "account_versions": {key: 1 for key in rows_by_account},
5752
+ "account_versions": {
5753
+ key: next(_CODEX_VISIBLE_ACCOUNT_VERSIONS)
5754
+ for key in rows_by_account},
5784
5755
  "estimated_bytes": estimated_bytes,
5785
5756
  "entry_count": len(rows),
5786
5757
  "account_count": len(rows_by_account),
@@ -5848,7 +5819,7 @@ def _cached_codex_visible_rows(
5848
5819
  else:
5849
5820
  rows_by_account.pop(key, None)
5850
5821
  entries_by_account.pop(key, None)
5851
- account_versions[key] = int(account_versions.get(key, 0)) + 1
5822
+ account_versions[key] = next(_CODEX_VISIBLE_ACCOUNT_VERSIONS)
5852
5823
 
5853
5824
  try:
5854
5825
  entries = tuple(
@@ -5895,13 +5866,59 @@ _CODEX_ACCOUNT_SCOPE_CACHE: dict[
5895
5866
  ] = _ObservedDict()
5896
5867
 
5897
5868
 
5898
- def reset_codex_account_scope_cache() -> None:
5899
- """Test/process reset for #582's immutable finalized account scopes."""
5869
+ @dataclass(frozen=True)
5870
+ class _CodexDerivedCoherence:
5871
+ """#872: the population every delta-advanced derived Codex cache reflects.
5872
+
5873
+ `name` is the visible-population name the whole derived layer was last
5874
+ advanced to, or `None` when that cannot be stated; `account_keys` is
5875
+ every account whose per-account derived state was built since the last
5876
+ discard. ONE immutable value under ONE key, so the two are written,
5877
+ journaled, charged and evicted together and can never disagree.
5878
+ """
5879
+
5880
+ name: tuple | None
5881
+ account_keys: frozenset
5882
+
5883
+
5884
+ _CODEX_DERIVED_COHERENCE_KEY = "state"
5885
+ _CODEX_DERIVED_COHERENCE: dict[str, _CodexDerivedCoherence] = _ObservedDict()
5886
+
5887
+
5888
+ def _codex_derived_coherence() -> _CodexDerivedCoherence:
5889
+ """The current record; an absent or evicted slot names nothing."""
5890
+ record = _CODEX_DERIVED_COHERENCE.get(_CODEX_DERIVED_COHERENCE_KEY)
5891
+ if isinstance(record, _CodexDerivedCoherence):
5892
+ return record
5893
+ return _CodexDerivedCoherence(None, frozenset())
5894
+
5895
+
5896
+ def reset_codex_account_scope_cache(*, keep_counters: bool = False) -> None:
5897
+ """Discard every delta-advanced derived Codex cache and the #872 record.
5898
+
5899
+ The complete derived reset: a test/process reset, the metadata-incomplete
5900
+ fold's reset, and — with `keep_counters=True` — #872's G1 discard, which
5901
+ runs in production on every base mismatch. G1 must not zero the
5902
+ cumulative fallback counters `bin/cctally-snapshot-measure` reads (the
5903
+ card-totals fallbacks and the visible population's `fallback_count`), so
5904
+ it keeps them; the caches themselves are discarded either way.
5905
+ """
5900
5906
  global _CODEX_ACCOUNT_CARD_TOTALS_FALLBACKS
5907
+ visible_fallbacks = int(
5908
+ _CODEX_VISIBLE_POPULATION_CACHE.get("fallback_count", 0) or 0)
5901
5909
  _CODEX_ACCOUNT_SCOPE_CACHE.clear()
5902
5910
  _CODEX_VISIBLE_POPULATION_CACHE.clear()
5911
+ if keep_counters and visible_fallbacks:
5912
+ # The over-size shape `_cached_codex_visible_rows` already writes:
5913
+ # counters only, nothing reusable.
5914
+ _CODEX_VISIBLE_POPULATION_CACHE.update({
5915
+ "fallback_count": visible_fallbacks,
5916
+ "estimated_bytes": 0,
5917
+ "entry_count": 0,
5918
+ })
5903
5919
  _CODEX_ACCOUNT_CARD_TOTALS_CACHE.clear()
5904
- _CODEX_ACCOUNT_CARD_TOTALS_FALLBACKS = 0
5920
+ if not keep_counters:
5921
+ _CODEX_ACCOUNT_CARD_TOTALS_FALLBACKS = 0
5905
5922
  _CODEX_ENTRY_ADAPTER_CACHE.clear()
5906
5923
  _CODEX_PROJECT_LABEL_CACHE.clear()
5907
5924
  _CODEX_PERIOD_VIEW_CACHE.clear()
@@ -5909,6 +5926,7 @@ def reset_codex_account_scope_cache() -> None:
5909
5926
  _CODEX_CACHE_REPORT_ROWS.clear()
5910
5927
  _CODEX_SESSION_VIEW_CACHE.clear()
5911
5928
  _CODEX_PROJECT_WIRE_CACHE.clear()
5929
+ _CODEX_DERIVED_COHERENCE.clear()
5912
5930
 
5913
5931
 
5914
5932
  def _codex_account_scopes_wire(
@@ -6313,6 +6331,11 @@ def _claude_accounts_wire(
6313
6331
  # load-bearing, which is the same reason every other weekly consumer
6314
6332
  # excludes held rows rather than trusting it.
6315
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
+ #
6316
6339
  # None is still returned only when the account retained NO row at all,
6317
6340
  # because the caller uses that to decide whether the unattributed
6318
6341
  # bucket appears.
@@ -6323,17 +6346,18 @@ def _claude_accounts_wire(
6323
6346
  "ORDER BY captured_at_utc DESC LIMIT 1",
6324
6347
  (key,),
6325
6348
  ).fetchone()
6326
- five_hour = stats_conn.execute(
6327
- "SELECT five_hour_percent FROM weekly_usage_snapshots "
6328
- "WHERE account_key=? "
6329
- "ORDER BY captured_at_utc DESC LIMIT 1",
6330
- (key,),
6331
- ).fetchone()
6349
+ five_hour = latest_five_hour_reading(stats_conn, account_key=key)
6332
6350
  if weekly is None and five_hour is None:
6333
- 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
6334
6358
  return (
6335
6359
  None if weekly is None else weekly[0],
6336
- None if five_hour is None else five_hour[0],
6360
+ None if five_hour is None else five_hour.percent,
6337
6361
  None if weekly is None else weekly[1],
6338
6362
  )
6339
6363
 
@@ -6494,6 +6518,7 @@ def _codex_source_caches() -> tuple[dict, ...]:
6494
6518
  _CODEX_ACCOUNT_CARD_TOTALS_CACHE,
6495
6519
  _CODEX_WEEKLY_VIEW_CACHE,
6496
6520
  _CODEX_ACCOUNT_SCOPE_CACHE,
6521
+ _CODEX_DERIVED_COHERENCE,
6497
6522
  )
6498
6523
 
6499
6524
 
@@ -6525,7 +6550,7 @@ def _codex_source_caches() -> tuple[dict, ...]:
6525
6550
  # incremental byte estimate of its own, and a second charge computed here would
6526
6551
  # be the same number derived twice. Those readers are named beside the owner.
6527
6552
  #
6528
- # WHAT THE TOTAL IS, AND WHAT IT IS NOT. It is the sum of eleven DECLARED
6553
+ # WHAT THE TOTAL IS, AND WHAT IT IS NOT. It is the sum of twelve DECLARED
6529
6554
  # CHARGES. It is not a measurement of process memory, and
6530
6555
  # `_CODEX_SOURCE_ACCELERATOR_MAX_BYTES` is not a cap on process memory: it is a
6531
6556
  # budget over these charges, and the charges depart from the retained graph in
@@ -6553,8 +6578,8 @@ def _codex_source_caches() -> tuple[dict, ...]:
6553
6578
  # `tests/test_source_cache_ownership.py` holds the WALKED owners to a two-sided
6554
6579
  # band against one walk of their own cache — the upper half is what caught the
6555
6580
  # 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.
6581
+ # upper bound for the three closed-form owners and asserting that the
6582
+ # closed-form set is exactly the three named here.
6558
6583
 
6559
6584
 
6560
6585
  def _is_codex_accounting_row(obj) -> bool:
@@ -6626,23 +6651,13 @@ def _charge_project_label_entry(_key, value) -> int:
6626
6651
 
6627
6652
 
6628
6653
  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
6654
+ """`(signature, wire)` — both owned; borrowed payloads are excluded."""
6655
+ signature, wire = value[0], value[1]
6656
+ return (
6657
+ sys.getsizeof(value)
6658
+ + retained_size_bytes(signature, borrowed=_is_borrowed_codex_payload)
6659
+ + retained_size_bytes(wire, borrowed=_is_borrowed_codex_payload)
6660
+ )
6646
6661
 
6647
6662
 
6648
6663
  #: How faithfully each owner's declared charge tracks what one walk of that
@@ -6652,9 +6667,9 @@ def _charge_project_wire_entry(_key, value) -> int:
6652
6667
  #: formula that DELIBERATELY UNDERSTATES, with no stated bound on the gap; the
6653
6668
  #: value beside the name is why that trade was taken.
6654
6669
  #:
6655
- #: The budget is compared against a sum that includes the four closed-form
6670
+ #: The budget is compared against a sum that includes the three closed-form
6656
6671
  #: 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
6672
+ #: on memory. Adding a fourth closed-form owner without adding it here is a
6658
6673
  #: test failure, which is the only reason this constant exists.
6659
6674
  CODEX_SOURCE_CHARGE_FIDELITY = MappingProxyType({
6660
6675
  "quota_observations": "walked",
@@ -6666,10 +6681,7 @@ CODEX_SOURCE_CHARGE_FIDELITY = MappingProxyType({
6666
6681
  "string each, because a labelled copy is `replace(row, "
6667
6682
  "display_label=...)` and every other field is the borrowed row's own "
6668
6683
  "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"),
6684
+ "project_wire": "walked",
6673
6685
  "entry_adapters": (
6674
6686
  "closed-form: two object shells and one float, because the adapted "
6675
6687
  "entry shares every string with the row beside it and walking it "
@@ -6681,6 +6693,7 @@ CODEX_SOURCE_CHARGE_FIDELITY = MappingProxyType({
6681
6693
  "account_card_totals": "walked",
6682
6694
  "weekly_views": "walked",
6683
6695
  "account_scopes": "walked",
6696
+ "derived_coherence": "walked",
6684
6697
  })
6685
6698
 
6686
6699
 
@@ -6834,8 +6847,9 @@ def _declare_codex_source_cache_owners() -> tuple[_CodexCacheOwner, ...]:
6834
6847
  _CodexCacheOwner(
6835
6848
  "project_wire", _CODEX_PROJECT_WIRE_CACHE,
6836
6849
  _charge_project_wire_entry, None,
6837
- owns="the rendered project wire and the group container shells",
6838
- borrows="the accounting rows inside those groups",
6850
+ owns="the rendered project wire and the population name it was "
6851
+ "built for (#872 removed the retained group splice)",
6852
+ borrows="nothing; borrowed payloads are excluded from its charge",
6839
6853
  ),
6840
6854
  _CodexCacheOwner(
6841
6855
  "entry_adapters", _CODEX_ENTRY_ADAPTER_CACHE,
@@ -6872,6 +6886,13 @@ def _declare_codex_source_cache_owners() -> tuple[_CodexCacheOwner, ...]:
6872
6886
  owns="the finalized per-account scope mappings",
6873
6887
  borrows="accounting rows and adapted entries",
6874
6888
  ),
6889
+ _CodexCacheOwner(
6890
+ "derived_coherence", _CODEX_DERIVED_COHERENCE,
6891
+ _charge_borrowing_payloads, None,
6892
+ owns="the one immutable record naming the population every "
6893
+ "delta-advanced derived cache reflects",
6894
+ borrows="nothing",
6895
+ ),
6875
6896
  )
6876
6897
  caches = _codex_source_caches()
6877
6898
  if tuple(owner.cache for owner in owners) != caches:
@@ -6888,10 +6909,10 @@ _CODEX_SOURCE_CACHE_OWNERS = _declare_codex_source_cache_owners()
6888
6909
 
6889
6910
 
6890
6911
  def codex_source_retained_bytes() -> int:
6891
- """The eleven Codex source caches' DECLARED CHARGES, summed.
6912
+ """The twelve Codex source caches' DECLARED CHARGES, summed.
6892
6913
 
6893
6914
  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
6915
+ these caches hold". THREE of the twelve are charged in closed form and
6895
6916
  understate without a stated bound — see `CODEX_SOURCE_CHARGE_FIDELITY` —
6896
6917
  so this figure is the input to a heuristic budget rather than a
6897
6918
  measurement of the retained graph.
@@ -6907,8 +6928,8 @@ def codex_source_retained_bytes() -> int:
6907
6928
 
6908
6929
 
6909
6930
  #: 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.
6931
+ #: it is compared against `codex_source_retained_bytes()`, three of whose
6932
+ #: twelve terms are closed-form under-charges with no stated bound on the gap.
6912
6933
  #: See `CODEX_SOURCE_CHARGE_FIDELITY`.
6913
6934
  _CODEX_SOURCE_ACCELERATOR_MAX_BYTES = 768 * 1024 * 1024
6914
6935
  _CODEX_SOURCE_ACCELERATOR_MAX_ENTRIES = 500_000
@@ -6995,15 +7016,16 @@ def _evict_codex_source_buckets(caches) -> tuple[int, bool]:
6995
7016
 
6996
7017
  Returns `(evicted_entries, saw_an_indivisible_oversized_bucket)`.
6997
7018
 
6998
- Ten of the eleven caches are already keyed by stable semantic units —
7019
+ Eleven of the twelve caches are keyed by stable semantic units —
6999
7020
  `("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
7021
+ a physical row id, the #872 coherence record's single key — so those keys
7022
+ ARE the buckets and no new partition is needed. The visible population is
7023
+ declared indivisible and is evicted whole; see
7002
7024
  `CODEX_VISIBLE_POPULATION_INDIVISIBLE_REASON`.
7003
7025
 
7004
7026
  Cold buckets go first and everything else stays resident, which is what
7005
7027
  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
7028
+ the caches, which is what this replaced, made a population permanently
7007
7029
  above a cap rebuild and discard on every generation: measured on the sealed
7008
7030
  #786 v3 `current` corpus, one run cleared 222,932 entries at a stroke.
7009
7031
 
@@ -7022,7 +7044,7 @@ def _evict_codex_source_buckets(caches) -> tuple[int, bool]:
7022
7044
  # budget leaves the next build one entry over it again, and every tick then
7023
7045
  # pays the candidate build below over every resident key — 222,932 of them
7024
7046
  # on the measurement corpus. Freeing headroom instead lets the early return
7025
- # above answer the next several ticks for eleven integer reads.
7047
+ # above answer the next several ticks for twelve integer reads.
7026
7048
  if count >= int(_CODEX_SOURCE_ACCELERATOR_LOW_WATER_MIN_ENTRIES):
7027
7049
  low_water = min(
7028
7050
  budget, int(budget * _CODEX_SOURCE_ACCELERATOR_LOW_WATER))
@@ -7058,7 +7080,7 @@ def _evict_codex_source_buckets(caches) -> tuple[int, bool]:
7058
7080
  # proportional to the whole population. Together they mean the
7059
7081
  # owner with the largest under-charge is also the last one this
7060
7082
  # pass will ever reach, so freeing memory by lowering the budget
7061
- # acts on the other ten owners first.
7083
+ # acts on the other eleven owners first.
7062
7084
  candidates.append((
7063
7085
  max(recency for recency, _key, _bytes in entries),
7064
7086
  index, position, _CACHE_MISSING, owner.retained_bytes(),
@@ -7102,7 +7124,7 @@ def _evict_codex_source_buckets(caches) -> tuple[int, bool]:
7102
7124
  continue
7103
7125
  evicted += removed
7104
7126
  count -= removed
7105
- # Re-read ONE owner rather than summing all eleven after every single
7127
+ # Re-read ONE owner rather than summing all twelve after every single
7106
7128
  # eviction. A charged owner's total moves by exactly this bucket, and a
7107
7129
  # self-reported one answers for itself; either way the arithmetic below
7108
7130
  # is checked against a full re-read once, after the loop.
@@ -7127,17 +7149,17 @@ def _evict_codex_source_buckets(caches) -> tuple[int, bool]:
7127
7149
  def enforce_codex_source_accelerator_bounds() -> None:
7128
7150
  """Apply byte and entry admission from the incrementally maintained total.
7129
7151
 
7130
- The byte total is a sum of eleven integers each owner keeps current, so
7152
+ The byte total is a sum of twelve integers each owner keeps current, so
7131
7153
  admission is a read. It used to be a background traversal of two full
7132
7154
  container copies of the whole cache set, once per generation, in a worker
7133
7155
  limited to a quarter of one core — which on the measurement corpus never
7134
7156
  finished a generation at all, so the caches were admitted against a byte
7135
7157
  figure that stayed at zero until the walk finally returned the over-budget
7136
- sentinel and cleared all eleven at once.
7158
+ sentinel and cleared every cache at once.
7137
7159
 
7138
7160
  Publication is unconditional now rather than gated on the dirty flag,
7139
7161
  because there is nothing left to schedule: an unchanged generation costs
7140
- the same eleven reads and publishes the same number.
7162
+ the same twelve reads and publishes the same number.
7141
7163
  """
7142
7164
  caches = _codex_source_caches()
7143
7165
  global _CODEX_SOURCE_ACCELERATOR_ESTIMATED_BYTES
@@ -7271,7 +7293,7 @@ _CODEX_SOURCE_PUBLISH_LOCK = threading.Lock()
7271
7293
 
7272
7294
 
7273
7295
  class _CodexSourceBuildOverlay:
7274
- """One build's tentative generation over the eleven source caches.
7296
+ """One build's tentative generation over the twelve source caches.
7275
7297
 
7276
7298
  THE JOURNAL IS THE OVERLAY. Rather than hold changed keys in a separate
7277
7299
  mapping every read would have to consult, each mutation records the key's
@@ -7463,6 +7485,11 @@ class _CodexAccountingCapture:
7463
7485
  #: qualified accounting read: such a generation names no population, and
7464
7486
  #: borrowing the process's current token would name one it never read.
7465
7487
  provenance_token: int | None = None
7488
+ #: #872: the name of the population this capture's delta is relative to,
7489
+ #: from the accounting result (`None` on a cold result and whenever the
7490
+ #: capture bypassed the qualified read, exactly like the fields above).
7491
+ base_population_signature: tuple | None = None
7492
+ base_provenance_token: int | None = None
7466
7493
 
7467
7494
 
7468
7495
  #: The detail routes' horizon: one year ending at the generation instant. The
@@ -7556,6 +7583,9 @@ def _capture_codex_accounting(
7556
7583
  population_signature: tuple | None = None
7557
7584
  # #857: set only by a qualified read that returned; every bypass keeps it.
7558
7585
  provenance_token: int | None = None
7586
+ # #872: the delta's base, under the same rule.
7587
+ base_population_signature: tuple | None = None
7588
+ base_provenance_token: int | None = None
7559
7589
 
7560
7590
  def fallback_entries() -> tuple[object, ...]:
7561
7591
  nonlocal metadata_read_failed, failed_legs
@@ -7625,6 +7655,9 @@ def _capture_codex_accounting(
7625
7655
  changed_new = cached_accounting.changed_new
7626
7656
  population_signature = cached_accounting.population_signature
7627
7657
  provenance_token = cached_accounting.provenance_token
7658
+ base_population_signature = (
7659
+ cached_accounting.base_population_signature)
7660
+ base_provenance_token = cached_accounting.base_provenance_token
7628
7661
  entries: tuple[object, ...] = qualified_entries
7629
7662
  except QualifiedMetadataUnavailable as exc:
7630
7663
  _lib_log.get_logger("dashboard").warning(
@@ -7671,6 +7704,10 @@ def _capture_codex_accounting(
7671
7704
  None if metadata_incomplete else population_signature),
7672
7705
  provenance_token=(
7673
7706
  None if metadata_incomplete else provenance_token),
7707
+ base_population_signature=(
7708
+ None if metadata_incomplete else base_population_signature),
7709
+ base_provenance_token=(
7710
+ None if metadata_incomplete else base_provenance_token),
7674
7711
  )
7675
7712
 
7676
7713
 
@@ -7797,7 +7834,7 @@ def _drop_codex_source_generation(
7797
7834
  builds and replaying it would overwrite whatever the other build
7798
7835
  legitimately published.
7799
7836
 
7800
- ALL ELEVEN CACHES GO, not only the ones this build wrote to, and that is a
7837
+ ALL TWELVE CACHES GO, not only the ones this build wrote to, and that is a
7801
7838
  correctness requirement rather than caution. Ten of them are fed by the
7802
7839
  accounting carrier's DELTA lists, not by its population: a build hands
7803
7840
  `_cached_codex_period_view` and its siblings `changed_old` and
@@ -8320,6 +8357,23 @@ def _build_codex_source_state(
8320
8357
  accounting_changed_new = accounting_capture.changed_new
8321
8358
  accounting_entries = accounting_capture.entries
8322
8359
  metadata_incomplete = accounting_capture.metadata_incomplete
8360
+ # #872 G1: a delta reaches the derived layer only while the layer's
8361
+ # coherence record names the delta's BASE. A cold carrier result has no
8362
+ # base, and a record that names anything else — an overflow-cleared or
8363
+ # reset carrier, a moved visible start, a skipped generation, a vanished
8364
+ # account — means the retained state is not what the delta is relative
8365
+ # to. Every delta-advanced derived cache is then discarded, and each
8366
+ # consumer derives from the full population through its own cold path.
8367
+ derived_base_name = _codex_visible_population_name(
8368
+ accounting_capture.base_population_signature,
8369
+ accounting_capture.base_provenance_token, context.range_start)
8370
+ derived_current_name = _codex_visible_population_name(
8371
+ accounting_capture.population_signature,
8372
+ accounting_capture.provenance_token, context.range_start)
8373
+ prior_coherence = _codex_derived_coherence()
8374
+ if derived_base_name is None or prior_coherence.name != derived_base_name:
8375
+ reset_codex_account_scope_cache(keep_counters=True)
8376
+ prior_coherence = _CodexDerivedCoherence(None, frozenset())
8323
8377
 
8324
8378
  conversation_metadata_read = (
8325
8379
  capture.conversation_metadata
@@ -8557,24 +8611,13 @@ def _build_codex_source_state(
8557
8611
  period_signature = _codex_accounting_period_signature(
8558
8612
  context.cache_conn, context.range_start, metadata_incomplete,
8559
8613
  )
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
- )
8614
+ # The project caches' name for this build's population: the visible
8615
+ # population name G1 computed from THIS build's own capture (sound across
8616
+ # a cleared carrier and a moved visible start, #872), plus the label
8617
+ # algorithm version. `None` when the population cannot be named, and then
8618
+ # nothing is reused. See `_CodexAccountingCapture.population_signature`.
8619
+ project_population_signature = _codex_project_label_signature(
8620
+ derived_current_name)
8578
8621
  daily = _cached_codex_period_view(
8579
8622
  entries, changed_old=changed_old_entries,
8580
8623
  changed_new=changed_new_entries, kind="daily", cache_key=("parent",),
@@ -8796,6 +8839,8 @@ def _build_codex_source_state(
8796
8839
  hero_cycles_wire: list[dict[str, object]] = []
8797
8840
  combined_accounting: dict[str, object] | None = None
8798
8841
  account_scopes: dict[str, dict[str, object]] = {}
8842
+ # #872 G2 input: whether the per-account scopes were built this build.
8843
+ account_scope_status = "not-built"
8799
8844
  # #556 S5 §3.8: bound OUTSIDE the try, because the degrade path below has to
8800
8845
  # be able to clear it, and the retained `clock_data` reads it either way.
8801
8846
  budget_events_by_account: dict[str, tuple[tuple[dt.datetime, float], ...]] = {}
@@ -8941,6 +8986,7 @@ def _build_codex_source_state(
8941
8986
  scope_signature=period_signature,
8942
8987
  project_population_signature=project_population_signature,
8943
8988
  )
8989
+ account_scope_status = "built"
8944
8990
  # #416 QA P1-A — the "All accounts" Blocks panel is the UNION of
8945
8991
  # every account's 5-hour blocks. `_quota_wire` filters
8946
8992
  # `str(root_key) not in cycle.source_root_keys` against a single
@@ -8997,6 +9043,7 @@ def _build_codex_source_state(
8997
9043
  ))
8998
9044
  quota = {**quota, "blocks": merged_blocks}
8999
9045
  except (sqlite3.Error, QualifiedMetadataUnavailable):
9046
+ account_scope_status = "degraded"
9000
9047
  # A per-account wire failure must never fail the whole source build;
9001
9048
  # degrade to the byte-stable undecorated shape.
9002
9049
  accounts_wire = []
@@ -9129,6 +9176,31 @@ def _build_codex_source_state(
9129
9176
  {name: operands[name] for name in _HERO_CARD_OPERANDS
9130
9177
  if name in operands}
9131
9178
  )
9179
+ # #872 G2: the record advances to this build's population only when the
9180
+ # build advanced EVERY delta-advanced consumer over the capture's whole
9181
+ # delta. Completeness is stated positively: metadata complete and not
9182
+ # transient (a transient generation skips both project legs), scopes not
9183
+ # degraded, and every account that holds per-account state still live
9184
+ # (an account that left the live set kept a stale state the delta never
9185
+ # reached). Anything else records `None`, so the next build's G1 discards.
9186
+ scope_keys = frozenset(str(key) for key in account_scopes)
9187
+ coherent = (
9188
+ derived_current_name is not None
9189
+ and not metadata_incomplete
9190
+ and not metadata_transient
9191
+ and account_scope_status != "degraded"
9192
+ and prior_coherence.account_keys <= scope_keys
9193
+ )
9194
+ record = _CodexDerivedCoherence(
9195
+ name=derived_current_name if coherent else None,
9196
+ account_keys=prior_coherence.account_keys | scope_keys,
9197
+ )
9198
+ if _CODEX_DERIVED_COHERENCE.get(_CODEX_DERIVED_COHERENCE_KEY) == record:
9199
+ # Unchanged (a warm-clean tick on the same ledger head): say it was
9200
+ # used without rewriting it, so an idle tick dirties nothing.
9201
+ _CODEX_DERIVED_COHERENCE.touch(_CODEX_DERIVED_COHERENCE_KEY)
9202
+ else:
9203
+ _CODEX_DERIVED_COHERENCE[_CODEX_DERIVED_COHERENCE_KEY] = record
9132
9204
  return SourceDashboardState(
9133
9205
  source="codex",
9134
9206
  availability=availability,