cctally 1.111.0 → 1.112.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 (37) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +5 -6
  3. package/bin/_cctally_cache.py +14 -7
  4. package/bin/_cctally_core.py +9 -1
  5. package/bin/_cctally_dashboard_envelope.py +7 -6
  6. package/bin/_cctally_dashboard_sources.py +90 -55
  7. package/bin/_cctally_doctor.py +73 -27
  8. package/bin/_cctally_forecast.py +2 -1
  9. package/bin/_cctally_journal.py +147 -6
  10. package/bin/_cctally_milestones.py +29 -3
  11. package/bin/_cctally_quota.py +10 -1
  12. package/bin/_cctally_record.py +136 -4
  13. package/bin/_cctally_rederive.py +311 -8
  14. package/bin/_cctally_source_analytics.py +20 -8
  15. package/bin/_cctally_tui.py +546 -45
  16. package/bin/_lib_codex_conversation_query.py +40 -4
  17. package/bin/_lib_codex_metadata.py +57 -11
  18. package/bin/_lib_cost_provenance.py +303 -0
  19. package/bin/_lib_dashboard_sources.py +263 -28
  20. package/bin/_lib_doctor.py +2 -2
  21. package/bin/_lib_ingest_frontier.py +137 -38
  22. package/bin/_lib_pricing.py +13 -1
  23. package/bin/_lib_rederive.py +704 -36
  24. package/bin/_lib_snapshot_cache.py +332 -9
  25. package/dashboard/static/assets/{ConversationsView-BB9vUiL6.js → ConversationsView-CEZ-BUyi.js} +1 -1
  26. package/dashboard/static/assets/{DoctorModal-DFY2iPDf.js → DoctorModal-CE1oH_Zy.js} +1 -1
  27. package/dashboard/static/assets/ModalRoot-D5Y8IFUG.js +1 -0
  28. package/dashboard/static/assets/{ProjectsDrillPanel-DAv39uSo.js → ProjectsDrillPanel-B3DZoCeD.js} +1 -1
  29. package/dashboard/static/assets/{SourceDetailModal-DSAWo2Fp.js → SourceDetailModal-CHp9EgLP.js} +1 -1
  30. package/dashboard/static/assets/{UpdateModal-R_9dOVnh.js → UpdateModal-5iKST6jo.js} +1 -1
  31. package/dashboard/static/assets/{index-DdU62sov.js → index-Cm0VTxJC.js} +2 -2
  32. package/dashboard/static/assets/index-Dt4tqf43.css +1 -0
  33. package/dashboard/static/assets/{outlineNavigation-BOCLcRa6.js → outlineNavigation-DkXGL8J6.js} +1 -1
  34. package/dashboard/static/dashboard.html +2 -2
  35. package/package.json +2 -1
  36. package/dashboard/static/assets/ModalRoot-DvRKHbQE.js +0 -1
  37. package/dashboard/static/assets/index-COhxe3vU.css +0 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,23 @@ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [1.112.0] - 2026-09-29
9
+
10
+ ### Added
11
+ - Claude cost reports price `claude-sonnet-5-5` at its published input, output and cache rates instead of showing $0 for new usage.
12
+
13
+ ### Changed
14
+ - `cctally dashboard` uses less CPU while idle on a large history.
15
+
16
+ ### Fixed
17
+ - The Conversation Viewer keeps project-filtered browse working with any number of unreadable Codex project paths, avoids repeated metadata schema probes, and preserves SQLite alias ordering.
18
+ - The TUI and dashboard clear a Codex metadata retry as soon as its transient partial generation is rejected, so the first healthy refresh recovers immediately instead of waiting through a stale retry delay.
19
+ - `cctally db rederive` corrects costs recorded while a model had no rate card, including provably unpriced $0 five-hour blocks. A Codex budget alert that fallback pricing overstated is retired and can fire again when real spend crosses.
20
+ - After `cctally db rederive --yes`, a still-crossed Claude budget, projected or project-budget alert no longer breaks every later stats ingest until the period ends, and fired five-hour alerts stay in Recent Alerts.
21
+ - On phones, the dashboard's `Current Usage` dialog shows its full title and hides the onboarding hint while the dialog is open.
22
+ - An idle `cctally dashboard` no longer rebuilds its Codex view on every refresh after a Codex weekly reset with no newer Codex activity, which cost steady CPU while nothing changed.
23
+ - `cctally db rederive` no longer reprices the unchanged five-hour milestones of a closed block whose cost is frozen when a rate card changes: each keeps its recorded cumulative cost; its marginal changes only to follow the prior milestone.
24
+
8
25
  ## [1.111.0] - 2026-09-24
9
26
 
10
27
  ### Fixed
package/README.md CHANGED
@@ -30,15 +30,14 @@ Your Claude Code plan meters you with a percentage that creeps up all week. ccta
30
30
  </p>
31
31
 
32
32
  <!-- cctally:latest-stable:begin -->
33
- **Latest stable: v1.109.0** (2026-09-15)
33
+ **Latest stable: v1.111.0** (2026-09-24)
34
34
 
35
- Highlights from the `v1.104.0` to `v1.109.0` stable upgrade:
35
+ Highlights from the `v1.109.0` to `v1.111.0` stable upgrade:
36
36
 
37
- - The dashboard picks up a new pricing revision without a restart. It re-reads the deployed pricing file every 60 seconds, adopts a complete newer revision, and keeps serving; no connected client is dropped.
38
- - The conversation filter popover says `Projects appear once indexing finishes.` in place of `No projects.` while the transcript index is being rebuilt. Model counts stay live on the Claude tab.
39
- - The dashboard's Projects panel gives every billing cycle of a credited week its own percentage. Both cycles shared one calendar date, and only the later reading survived, so the second cycle could never carry one.
37
+ - After `cctally update` or a Homebrew upgrade installs a newer version, a running `dashboard` restarts itself so new prices and code apply. Restart it yourself once after installing this release, and after any manual `npm install -g`.
38
+ - A `dashboard` restart after an update no longer leaves an extra Node process running on npm installs.
40
39
 
41
- [See every change in this stable upgrade](https://github.com/omrikais/cctally/releases/tag/v1.109.0)
40
+ [See every change in this stable upgrade](https://github.com/omrikais/cctally/releases/tag/v1.111.0)
42
41
  <!-- cctally:latest-stable:end -->
43
42
 
44
43
  ## Quick start
@@ -9478,6 +9478,7 @@ def _sum_codex_cost_for_range(
9478
9478
  speed: str = "auto",
9479
9479
  skip_sync: bool = False,
9480
9480
  account_key: "str | None" = None,
9481
+ pricing_provenance: "dict | None" = None,
9481
9482
  ) -> float:
9482
9483
  """Sum USD Codex cost of all `codex_session_entries` in ``[start, end)``.
9483
9484
 
@@ -9511,17 +9512,23 @@ def _sum_codex_cost_for_range(
9511
9512
  """
9512
9513
  c = _cctally()
9513
9514
  eff_speed = c._resolve_codex_speed(speed)
9515
+ entries = c.get_codex_entries(
9516
+ start, end, skip_sync=skip_sync, account_key=account_key,
9517
+ )
9518
+ if pricing_provenance is not None:
9519
+ import _lib_cost_provenance
9520
+ total, evidence = _lib_cost_provenance.codex_cost_with_provenance(
9521
+ entries, end=end, speed=eff_speed,
9522
+ )
9523
+ pricing_provenance.update(evidence)
9524
+ return total
9514
9525
  total = 0.0
9515
- for entry in c.get_codex_entries(start, end, skip_sync=skip_sync,
9516
- account_key=account_key):
9526
+ for entry in entries:
9517
9527
  if entry.timestamp >= end:
9518
9528
  continue
9519
9529
  total += c._calculate_codex_entry_cost(
9520
- entry.model,
9521
- entry.input_tokens,
9522
- entry.cached_input_tokens,
9523
- entry.output_tokens,
9524
- entry.reasoning_output_tokens,
9530
+ entry.model, entry.input_tokens, entry.cached_input_tokens,
9531
+ entry.output_tokens, entry.reasoning_output_tokens,
9525
9532
  speed=eff_speed,
9526
9533
  )
9527
9534
  return total
@@ -468,7 +468,12 @@ _init_paths_from_env()
468
468
  # a migration is unchanged: the registry is frozen at 13, and an epoch-current
469
469
  # open returns before any schema work, so a handler or an
470
470
  # `add_column_if_missing` would never run on an upgraded install.
471
- STATS_INDEX_EPOCH = 1015
471
+ # 1015 -> 1016 (#869): an open five-hour block records the pricing evidence
472
+ # used to compute its current cost. Closing a different block on a successor
473
+ # tick must carry that earlier evidence into the journal, even after this
474
+ # binary learns a new model card. A pre-existing open projection that was
475
+ # rebuilt without the marker remains unproven until a new tick recomputes it.
476
+ STATS_INDEX_EPOCH = 1016
472
477
  LEGACY_STATS_HEAD = 13
473
478
 
474
479
  #: #496 S1 F1. A NEW branch, for a state that cannot occur before the
@@ -2506,6 +2511,7 @@ def open_db(*, _target_path=None) -> sqlite3.Connection:
2506
2511
  total_cache_create_tokens INTEGER NOT NULL DEFAULT 0,
2507
2512
  total_cache_read_tokens INTEGER NOT NULL DEFAULT 0,
2508
2513
  total_cost_usd REAL NOT NULL DEFAULT 0,
2514
+ pricing_provenance_json TEXT,
2509
2515
  is_closed INTEGER NOT NULL DEFAULT 0,
2510
2516
  created_at_utc TEXT NOT NULL,
2511
2517
  last_updated_at_utc TEXT NOT NULL,
@@ -2517,6 +2523,8 @@ def open_db(*, _target_path=None) -> sqlite3.Connection:
2517
2523
  add_column_if_missing(
2518
2524
  conn, "five_hour_blocks", "account_key",
2519
2525
  "TEXT NOT NULL DEFAULT 'unattributed'")
2526
+ add_column_if_missing(
2527
+ conn, "five_hour_blocks", "pricing_provenance_json", "TEXT")
2520
2528
  conn.execute(
2521
2529
  """
2522
2530
  CREATE INDEX IF NOT EXISTS idx_five_hour_blocks_block_start
@@ -56,8 +56,8 @@ from _cctally_core import (
56
56
  )
57
57
  from _lib_dashboard_sources import (
58
58
  SOURCE_SCHEMA_VERSION,
59
- canonical_alerted_at as _canonical_alerted_at,
60
- canonical_alerted_at_sql as _canonical_alerted_at_sql,
59
+ alerted_at_order_key as _alerted_at_order_key,
60
+ alerted_at_order_key_sql as _alerted_at_order_key_sql,
61
61
  dashboard_resource_key as _dashboard_resource_key,
62
62
  )
63
63
  from _lib_display_tz import _compute_display_block, format_display_dt
@@ -475,9 +475,10 @@ def _alert_account_resolver(conn: sqlite3.Connection):
475
475
  # and a row excluded there is unrecoverable. Two spellings of one instant
476
476
  # compare equal here; a nonzero offset — which the ``all-combined`` fixture
477
477
  # stores deliberately — orders by its true instant rather than by its
478
- # local-time text.
479
- _CANON_ALERTED_AT = _canonical_alerted_at_sql()
480
- _CANON_ALERTED_AT_M = _canonical_alerted_at_sql("m.alerted_at")
478
+ # local-time text. #869 F14: the key carries the full sub-second instant, so a
479
+ # Codex crossing fired later in the same second as a Claude alert sorts newer.
480
+ _CANON_ALERTED_AT = _alerted_at_order_key_sql()
481
+ _CANON_ALERTED_AT_M = _alerted_at_order_key_sql("m.alerted_at")
481
482
 
482
483
 
483
484
  def _envelope_rows_weekly(
@@ -1169,7 +1170,7 @@ def _build_alerts_envelope_array(
1169
1170
  # `_combined_alert_rows` does — otherwise the operator sees an empty
1170
1171
  # panel and a message that identifies nothing.
1171
1172
  try:
1172
- return _canonical_alerted_at(alert["alerted_at"])
1173
+ return _alerted_at_order_key(alert["alerted_at"])
1173
1174
  except (KeyError, ValueError) as exc:
1174
1175
  identity = alert.get("key") or alert.get("id")
1175
1176
  raise ValueError(
@@ -55,15 +55,17 @@ from _lib_dashboard_sources import (
55
55
  SourceDashboardState,
56
56
  SourceDashboardWarning,
57
57
  assess_codex_projection_coherence,
58
+ alerted_at_order_key,
59
+ alerted_at_order_key_sql,
58
60
  build_metadata_health,
59
61
  canonical_alerted_at,
60
- canonical_alerted_at_sql,
61
62
  claude_stats_digest,
62
63
  combined_accounting_version,
63
64
  codex_stats_digest,
64
65
  dashboard_resource_key,
65
66
  )
66
67
  from _lib_quota import (
68
+ FUTURE_CLOCK_SKEW_SECONDS,
67
69
  QuotaWindowIdentity,
68
70
  build_blocks,
69
71
  build_history,
@@ -627,26 +629,12 @@ def _codex_quota_reuse_identity(
627
629
  "stats_identity must be the three-digest tuple or None")
628
630
  elif not isinstance(stats_conn, sqlite3.Connection):
629
631
  return None
630
- try:
631
- seq_row = cache_conn.execute(
632
- "SELECT seq FROM sqlite_sequence "
633
- "WHERE name='quota_window_change_log'"
634
- ).fetchone()
635
- table = cache_conn.execute(
636
- "SELECT 1 FROM sqlite_master WHERE type='table' "
637
- "AND name='quota_window_change_log'"
638
- ).fetchone()
639
- revision_row = cache_conn.execute(
640
- "SELECT value FROM cache_meta "
641
- "WHERE key='codex_window_attribution_revision'"
642
- ).fetchone()
643
- db_path = next(
644
- str(row[2]) for row in cache_conn.execute("PRAGMA database_list")
645
- if str(row[1]) == "main"
646
- )
647
- except (sqlite3.Error, StopIteration):
648
- return None
649
- if table is None:
632
+ # #857: the cache.db legs are read through the snapshot-cache kernel, which
633
+ # states each leg's query once and shares it with the doctor's quota probe
634
+ # and the dashboard's quota-dependency identity.
635
+ db_path = _lib_snapshot_cache.codex_main_database_path(cache_conn)
636
+ legs = _lib_snapshot_cache.codex_quota_observation_legs(cache_conn)
637
+ if db_path is None or legs is None:
650
638
  return None
651
639
  if stats_identity is None:
652
640
  try:
@@ -660,12 +648,7 @@ def _codex_quota_reuse_identity(
660
648
  return None
661
649
  except sqlite3.Error:
662
650
  return None
663
- return (
664
- db_path,
665
- 0 if seq_row is None else int(seq_row[0]),
666
- "" if revision_row is None else str(revision_row[0]),
667
- *stats_identity,
668
- )
651
+ return (db_path, *legs, *stats_identity)
669
652
 
670
653
 
671
654
  def _cached_codex_quota_observations(**kwargs) -> tuple[object, ...]:
@@ -1190,51 +1173,82 @@ def _codex_next_decision_at(
1190
1173
  crossing. That is one rebuild per deadline (a handful per weekly cycle), not
1191
1174
  one per tick.
1192
1175
 
1193
- The deadline is the ``min`` of three candidate kinds, dropping any candidate
1194
- at or before ``now_utc``:
1195
-
1196
- 1. every selected cycle's ``resets_at`` (expiry — including the
1197
- #341 multi-account case where account A expires while B stays live);
1198
- 2. ``latest_physical_capture + stale_after_seconds(window)`` for every weekly
1199
- history with a live baseline (fresh -> stale). This is a deliberate
1200
- SUPERSET of the §3.2 ranking participants — it also covers histories whose
1201
- freshness is ``"future"``/``"unavailable"`` and so never enter the ranking
1202
- — because an extra candidate can only pull the deadline EARLIER, and an
1203
- earlier deadline is always the conservative direction;
1204
- 3. the ``captured_at`` of any future-dated weekly observation
1205
- (future -> fresh / baseline eligibility).
1176
+ The deadline is the ``min`` of every candidate below, dropping any
1177
+ candidate at or before ``now_utc``. #857 made the set COMPLETE, because a
1178
+ settled cycle-unavailable generation now idles until this instant: a
1179
+ transition with no candidate is never picked up at all, and one whose
1180
+ candidate fires early is lost too — the rebuild at the early instant drops
1181
+ the candidate (it is no longer after ``now``) while the transition it stood
1182
+ for is still ahead. Each candidate is therefore the FIRST instant its
1183
+ transition is observable, under the resolver's own comparison:
1184
+
1185
+ 1. every weekly baseline's ``canonical_resets_at`` — selected or not, since
1186
+ an UNSELECTED boundary expiring is what resolves a ``conflicting``
1187
+ account (the resolver drops ``resets_at <= now``, so the reset instant
1188
+ itself is the first expired one). Every selected cycle's ``resets_at`` is
1189
+ one of these; they stay listed explicitly for the #341 multi-account case
1190
+ where account A expires while B stays live;
1191
+ 2. every weekly baseline's nominal start plus one microsecond: the resolver
1192
+ rejects ``start >= now`` (#599), so a window captured ahead of its start
1193
+ becomes eligible strictly after it, on frozen evidence;
1194
+ 3. for every weekly history with a baseline that has not reset: the
1195
+ fresh -> stale flip of its latest physical capture, which
1196
+ ``quota_freshness`` takes when the whole-second age EXCEEDS
1197
+ ``stale_after_seconds`` — ``capture + stale_after + 1s``, not
1198
+ ``capture + stale_after`` — and the future -> fresh flip, when the
1199
+ truncated age stops being below ``-FUTURE_CLOCK_SKEW_SECONDS`` —
1200
+ strictly after ``capture - (skew + 1)s``. This is a deliberate SUPERSET
1201
+ of the §3.2 ranking participants, because an extra candidate can only
1202
+ pull the deadline EARLIER and costs one rebuild at an instant where the
1203
+ verdict could have changed;
1204
+ 4. the ``captured_at`` of any future-dated weekly observation (baseline
1205
+ eligibility, ``captured_at <= now``). Recorded BEFORE the model-scoped
1206
+ skip: the pool label is read from the baseline and ``limit_name`` is not
1207
+ part of history identity, so a future capture can move a history out of
1208
+ a model-scoped pool.
1206
1209
 
1207
1210
  Returns ``None`` when nothing can flip. Server-only: it rides ``clock_data``
1208
1211
  and never reaches the public source envelope.
1209
1212
  """
1213
+ one_microsecond = dt.timedelta(microseconds=1)
1210
1214
  candidates: list[dt.datetime] = []
1211
1215
  for cycle in cycles:
1212
1216
  candidates.append(cycle.resets_at.astimezone(UTC))
1213
1217
  for history in build_history(tuple(observations)):
1214
1218
  if history.identity.window_minutes != 10_080:
1215
1219
  continue
1220
+ # 4. A capture ahead of ``now`` is not baseline-eligible yet, so this
1221
+ # runs even for a history with no live baseline at all.
1222
+ for observation in (
1223
+ *history.observations, *history.physical_observations,
1224
+ ):
1225
+ captured_at = observation.captured_at.astimezone(UTC)
1226
+ if captured_at > now_utc:
1227
+ candidates.append(captured_at)
1216
1228
  baseline = select_baseline(history.observations, now_utc)
1217
1229
  if codex_history_is_model_scoped(history, baseline=baseline):
1218
1230
  continue
1231
+ if baseline is None:
1232
+ continue
1219
1233
  # #428: the anchor, so the decision deadline describes the same window
1220
1234
  # the hero publishes rather than a jitter sibling up to 600s away.
1221
- if baseline is not None and baseline.canonical_resets_at > now_utc:
1235
+ resets_at = baseline.canonical_resets_at.astimezone(UTC)
1236
+ window = dt.timedelta(minutes=history.identity.window_minutes)
1237
+ candidates.append(resets_at) # 1.
1238
+ candidates.append(resets_at - window + one_microsecond) # 2.
1239
+ if resets_at > now_utc: # 3.
1222
1240
  latest = latest_physical_observation(history.physical_observations)
1223
1241
  if latest is not None:
1242
+ latest_at = latest.captured_at.astimezone(UTC)
1243
+ candidates.append(latest_at + dt.timedelta(
1244
+ seconds=stale_after_seconds(
1245
+ history.identity.window_minutes) + 1,
1246
+ ))
1224
1247
  candidates.append(
1225
- latest.captured_at.astimezone(UTC)
1226
- + dt.timedelta(
1227
- seconds=stale_after_seconds(history.identity.window_minutes),
1228
- )
1248
+ latest_at
1249
+ - dt.timedelta(seconds=FUTURE_CLOCK_SKEW_SECONDS + 1)
1250
+ + one_microsecond
1229
1251
  )
1230
- # A capture ahead of ``now`` is not baseline-eligible yet, so this runs
1231
- # even for a history with no live baseline at all.
1232
- for observation in (
1233
- *history.observations, *history.physical_observations,
1234
- ):
1235
- captured_at = observation.captured_at.astimezone(UTC)
1236
- if captured_at > now_utc:
1237
- candidates.append(captured_at)
1238
1252
  live = [candidate for candidate in candidates if candidate > now_utc]
1239
1253
  return min(live) if live else None
1240
1254
 
@@ -4447,7 +4461,8 @@ def _alerts_wire(
4447
4461
  # row that fired most recently but crossed longest ago was dropped at the
4448
4462
  # LIMIT and never reached the panel at all. `created_at` stays as an
4449
4463
  # equal-valued compatibility alias for a client reading a pre-v7 envelope.
4450
- canon = canonical_alerted_at_sql()
4464
+ # #869 F14: the full instant, so a same-second pair keeps its newer row.
4465
+ canon = alerted_at_order_key_sql()
4451
4466
 
4452
4467
  def _instants(raw: object) -> dict[str, str]:
4453
4468
  value = canonical_alerted_at(raw)
@@ -4506,7 +4521,7 @@ def _alerts_wire(
4506
4521
  return ()
4507
4522
  return tuple(sorted(
4508
4523
  rows,
4509
- key=lambda item: canonical_alerted_at(item["alerted_at"]),
4524
+ key=lambda item: alerted_at_order_key(item["alerted_at"]),
4510
4525
  reverse=True,
4511
4526
  )[:SOURCE_HISTORY_LIMIT])
4512
4527
 
@@ -7441,6 +7456,13 @@ class _CodexAccountingCapture:
7441
7456
  #: safety came from the compare-and-swap rather than from the signature,
7442
7457
  #: and carrying the value makes the signature answer for itself.
7443
7458
  population_signature: tuple | None = None
7459
+ #: #857: the accounting provenance token THIS capture's accounting result
7460
+ #: returned — carried for the same reason as the signature above, because
7461
+ #: overlay publication or a concurrent capture can move the module state
7462
+ #: before the fold stamps it. ``None`` when the capture bypassed the
7463
+ #: qualified accounting read: such a generation names no population, and
7464
+ #: borrowing the process's current token would name one it never read.
7465
+ provenance_token: int | None = None
7444
7466
 
7445
7467
 
7446
7468
  #: The detail routes' horizon: one year ending at the generation instant. The
@@ -7532,6 +7554,8 @@ def _capture_codex_accounting(
7532
7554
  changed_old: tuple[object, ...] = ()
7533
7555
  changed_new: tuple[object, ...] = ()
7534
7556
  population_signature: tuple | None = None
7557
+ # #857: set only by a qualified read that returned; every bypass keeps it.
7558
+ provenance_token: int | None = None
7535
7559
 
7536
7560
  def fallback_entries() -> tuple[object, ...]:
7537
7561
  nonlocal metadata_read_failed, failed_legs
@@ -7600,6 +7624,7 @@ def _capture_codex_accounting(
7600
7624
  changed_old = cached_accounting.changed_old
7601
7625
  changed_new = cached_accounting.changed_new
7602
7626
  population_signature = cached_accounting.population_signature
7627
+ provenance_token = cached_accounting.provenance_token
7603
7628
  entries: tuple[object, ...] = qualified_entries
7604
7629
  except QualifiedMetadataUnavailable as exc:
7605
7630
  _lib_log.get_logger("dashboard").warning(
@@ -7644,6 +7669,8 @@ def _capture_codex_accounting(
7644
7669
  failed_legs=failed_legs,
7645
7670
  population_signature=(
7646
7671
  None if metadata_incomplete else population_signature),
7672
+ provenance_token=(
7673
+ None if metadata_incomplete else provenance_token),
7647
7674
  )
7648
7675
 
7649
7676
 
@@ -9253,6 +9280,14 @@ def _build_codex_source_state(
9253
9280
  "codex_next_decision_at": _codex_next_decision_at(
9254
9281
  quota_observations, cycles_all, context.now_utc,
9255
9282
  ),
9283
+ # #857: the accounting provenance token of the population THIS
9284
+ # build's capture read (`None` when it bypassed the qualified
9285
+ # read). A retained generation is admitted only while this equals
9286
+ # the token of the population the process last consumed, so a
9287
+ # build that consumed accounting and then failed to publish leaves
9288
+ # every older generation refused, whatever the failure site.
9289
+ _lib_snapshot_cache.CODEX_ACCOUNTING_PROVENANCE_KEY: (
9290
+ accounting_capture.provenance_token),
9256
9291
  },
9257
9292
  private_session_labels=private_session_labels,
9258
9293
  # #819: the SAME reading that gated decoration above. Attaching it here
@@ -1032,31 +1032,17 @@ def _codex_quota_observation_signal() -> "tuple | None":
1032
1032
  except sqlite3.Error:
1033
1033
  return None
1034
1034
  try:
1035
- table = conn.execute(
1036
- "SELECT 1 FROM sqlite_master WHERE type='table' "
1037
- "AND name='quota_window_change_log'"
1038
- ).fetchone()
1039
- if table is None:
1040
- return None
1041
- seq_row = conn.execute(
1042
- "SELECT seq FROM sqlite_sequence "
1043
- "WHERE name='quota_window_change_log'"
1044
- ).fetchone()
1045
- revision_row = conn.execute(
1046
- "SELECT value FROM cache_meta "
1047
- "WHERE key='codex_window_attribution_revision'"
1048
- ).fetchone()
1049
- except sqlite3.Error:
1050
- return None
1035
+ # #857: the two ledger legs are stated once, in the snapshot-cache
1036
+ # kernel, and shared with the dashboard's Codex quota-dependency
1037
+ # identity rather than restated here.
1038
+ legs = _cctally()._load_sibling(
1039
+ "_lib_snapshot_cache"
1040
+ ).codex_quota_observation_legs(conn)
1051
1041
  finally:
1052
1042
  conn.close()
1053
- return (
1054
- str(path),
1055
- stat.st_dev,
1056
- stat.st_ino,
1057
- 0 if seq_row is None else int(seq_row[0]),
1058
- "" if revision_row is None else str(revision_row[0]),
1059
- )
1043
+ if legs is None:
1044
+ return None
1045
+ return (str(path), stat.st_dev, stat.st_ino, *legs)
1060
1046
 
1061
1047
 
1062
1048
  def _load_codex_quota_observations_for_doctor(*, force_cold: bool = False):
@@ -1079,6 +1065,67 @@ def _load_codex_quota_observations_for_doctor(*, force_cold: bool = False):
1079
1065
  return loaded
1080
1066
 
1081
1067
 
1068
+ # The all-history project-metadata partition is the other large immutable input
1069
+ # in a warm dashboard Doctor gather. On the production-shaped #857 copy it
1070
+ # cost 422-432 ms per call while returning the same six counts every 30-second
1071
+ # Doctor refresh. Retain only that raw partition, never a Doctor verdict or a
1072
+ # clock-derived field, under the cache identity plus the existing physical
1073
+ # mutation sequence. Codex ingest advances that sequence in the same
1074
+ # transaction as token, thread, root, cursor, and metadata writes, which are
1075
+ # exactly the three-table inputs of ``load_codex_project_metadata_health``.
1076
+ # Replacement is covered separately by device/inode, and an absent or malformed
1077
+ # signal always takes the cold path. The path is the one the passed connection
1078
+ # opened, never the global ``CACHE_DB_PATH``: the sequence is read from that
1079
+ # connection, so the file identity must describe the same file (#857).
1080
+ _CODEX_PROJECT_METADATA_MEMO: "dict[tuple, object]" = {}
1081
+
1082
+
1083
+ def _codex_project_metadata_signal(
1084
+ cache_conn: sqlite3.Connection,
1085
+ ) -> "tuple | None":
1086
+ try:
1087
+ path = next(
1088
+ str(row[2]) for row in cache_conn.execute("PRAGMA database_list")
1089
+ if len(row) > 2 and str(row[1]) == "main" and row[2]
1090
+ )
1091
+ stat = os.stat(path)
1092
+ row = cache_conn.execute(
1093
+ "SELECT value FROM cache_meta "
1094
+ "WHERE key='codex_physical_mutation_seq'"
1095
+ ).fetchone()
1096
+ if row is None:
1097
+ return None
1098
+ sequence = int(row[0])
1099
+ if sequence < 0:
1100
+ return None
1101
+ except (OSError, StopIteration, sqlite3.Error, TypeError, ValueError):
1102
+ return None
1103
+ return (path, stat.st_dev, stat.st_ino, sequence)
1104
+
1105
+
1106
+ def _load_codex_project_metadata_health_for_doctor(
1107
+ cache_conn: sqlite3.Connection,
1108
+ *,
1109
+ force_cold: bool = False,
1110
+ ):
1111
+ signal = (
1112
+ None if force_cold
1113
+ else _codex_project_metadata_signal(cache_conn)
1114
+ )
1115
+ if signal is not None:
1116
+ cached = _CODEX_PROJECT_METADATA_MEMO.get(signal)
1117
+ if cached is not None:
1118
+ return cached
1119
+ analytics = _cctally()._load_sibling("_cctally_source_analytics")
1120
+ loaded = analytics.load_codex_project_metadata_health(
1121
+ cache_conn=cache_conn,
1122
+ )
1123
+ if signal is not None:
1124
+ _CODEX_PROJECT_METADATA_MEMO.clear()
1125
+ _CODEX_PROJECT_METADATA_MEMO[signal] = loaded
1126
+ return loaded
1127
+
1128
+
1082
1129
  _QUOTA_NO_CHANGE: dict = {
1083
1130
  "active": False, "assessed": True, "effective_from": None,
1084
1131
  "previous_units_per_point": None, "new_units_per_point": None,
@@ -1835,10 +1882,9 @@ def _doctor_gather_state_impl(
1835
1882
  # connection. A failed probe is health evidence, not an
1836
1883
  # empty corpus: the kernel renders it as a distinct FAIL.
1837
1884
  try:
1838
- import _cctally_source_analytics
1839
-
1840
- health = _cctally_source_analytics.load_codex_project_metadata_health(
1841
- cache_conn=conn,
1885
+ health = _load_codex_project_metadata_health_for_doctor(
1886
+ conn,
1887
+ force_cold=_force_cold_inputs,
1842
1888
  )
1843
1889
  codex_project_metadata_health = {
1844
1890
  "total_rows": health.total_rows,
@@ -3173,7 +3173,8 @@ def cmd_budget(args: argparse.Namespace) -> int:
3173
3173
 
3174
3174
  def _codex_backstop_leg(ictx):
3175
3175
  c.maybe_record_codex_budget_milestone(
3176
- {}, conn=ictx.conn, alert_sink=ictx.pending_alerts)
3176
+ {}, conn=ictx.conn, alert_sink=ictx.pending_alerts,
3177
+ journal_ctx=ictx)
3177
3178
  if projected_on:
3178
3179
  c.maybe_record_projected_alert(
3179
3180
  {}, only_metrics={"codex_budget_usd"},