cctally 1.117.2 → 1.118.1

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 +37 -0
  2. package/README.md +7 -3
  3. package/bin/_cctally_cache.py +2873 -193
  4. package/bin/_cctally_completion.py +106 -33
  5. package/bin/_cctally_core.py +69 -4
  6. package/bin/_cctally_dashboard.py +188 -13
  7. package/bin/_cctally_dashboard_conversation.py +5 -1
  8. package/bin/_cctally_dashboard_envelope.py +50 -19
  9. package/bin/_cctally_dashboard_sources.py +23 -1
  10. package/bin/_cctally_db.py +193 -153
  11. package/bin/_cctally_diagnosis_sources.py +63 -7
  12. package/bin/_cctally_doctor.py +865 -93
  13. package/bin/_cctally_five_hour.py +67 -50
  14. package/bin/_cctally_forecast.py +35 -13
  15. package/bin/_cctally_journal.py +186 -18
  16. package/bin/_cctally_milestone_history.py +859 -62
  17. package/bin/_cctally_oauth_samples.py +286 -15
  18. package/bin/_cctally_parser.py +2 -2
  19. package/bin/_cctally_quota.py +104 -12
  20. package/bin/_cctally_record.py +153 -29
  21. package/bin/_cctally_rederive.py +608 -68
  22. package/bin/_cctally_statusline.py +108 -81
  23. package/bin/_cctally_store.py +179 -38
  24. package/bin/_cctally_tui.py +892 -181
  25. package/bin/_cctally_update.py +32 -13
  26. package/bin/_lib_codex_conversation_query.py +52 -14
  27. package/bin/_lib_codex_drain_admission.py +224 -6
  28. package/bin/_lib_codex_metadata.py +39 -0
  29. package/bin/_lib_conversation_anon.py +15 -8
  30. package/bin/_lib_conversation_dispatch.py +7 -0
  31. package/bin/_lib_conversation_query.py +289 -70
  32. package/bin/_lib_dashboard_sources.py +37 -1
  33. package/bin/_lib_detector_access_registry.py +497 -26
  34. package/bin/_lib_doctor.py +85 -20
  35. package/bin/_lib_doctor_scan_index.py +321 -0
  36. package/bin/_lib_ingest_frontier.py +20 -1
  37. package/bin/_lib_oauth_samples.py +23 -7
  38. package/bin/_lib_pricing.py +146 -16
  39. package/bin/_lib_quota.py +19 -0
  40. package/bin/_lib_rederive.py +11 -6
  41. package/bin/_lib_retained_records.py +516 -0
  42. package/bin/_lib_snapshot_cache.py +10 -0
  43. package/bin/_lib_stale_proof.py +272 -43
  44. package/bin/_lib_stats_content_revision.py +831 -0
  45. package/bin/_lib_write_io.py +11 -4
  46. package/bin/cctally +4 -1
  47. package/dashboard/static/assets/ConversationsView-D4Iv4ku0.js +72 -0
  48. package/dashboard/static/assets/{DoctorModal-B0DnU9d9.js → DoctorModal-D4QSq6uz.js} +1 -1
  49. package/dashboard/static/assets/ModalRoot-C9F5dhZi.js +1 -0
  50. package/dashboard/static/assets/{ProjectsDrillPanel-DSxXQSa-.js → ProjectsDrillPanel-WxAdRDMt.js} +1 -1
  51. package/dashboard/static/assets/{SourceDetailModal-BleDpQqG.js → SourceDetailModal-NyyEfXmc.js} +1 -1
  52. package/dashboard/static/assets/{UpdateModal-CMqaAkSK.js → UpdateModal-CFLq9pT1.js} +1 -1
  53. package/dashboard/static/assets/{index-BjQV-0-D.js → index-BKTWH9JE.js} +2 -2
  54. package/dashboard/static/assets/{outlineNavigation-CO-VWBm7.js → outlineNavigation-BnGUwCS3.js} +3 -3
  55. package/dashboard/static/dashboard.html +1 -1
  56. package/package.json +4 -1
  57. package/dashboard/static/assets/ConversationsView-oqNsWzWV.js +0 -72
  58. package/dashboard/static/assets/ModalRoot-CYua2wP_.js +0 -1
@@ -47,6 +47,7 @@ from dataclasses import dataclass
47
47
  import _cctally_core # `weekly_held_exclusion`: the held-column guard (#836)
48
48
  import _lib_accounts # pure stdlib kernel; UNATTRIBUTED sentinel (#341)
49
49
  import _lib_rebuild_progress # #997 D9 progress ticks (stdlib only)
50
+ import _lib_stats_content_revision # #1050 identical child-set check
50
51
  from _lib_five_hour import latest_five_hour_reading
51
52
  from _cctally_core import _command_as_of, eprint, now_utc_iso, open_db, parse_iso_datetime
52
53
 
@@ -2034,60 +2035,76 @@ def project_open_block_end(
2034
2035
  ),
2035
2036
  )
2036
2037
  # Replace-all children, keyed on the window (not block_id) so an orphan
2037
- # child of an earlier parent rebuild is cleaned up as well.
2038
+ # child of an earlier parent rebuild is cleaned up as well. An identical
2039
+ # stored set is left in place (#1050): a DELETE and re-INSERT of the same
2040
+ # rows would advance the content revision with nothing changed.
2038
2041
  block_id = int(current["id"])
2039
- conn.execute(
2040
- "DELETE FROM five_hour_block_models "
2041
- "WHERE five_hour_window_key = ? AND account_key = ?",
2042
- (window_key, account_key),
2043
- )
2044
- if totals.get("by_model"):
2045
- conn.executemany(
2046
- """
2047
- INSERT INTO five_hour_block_models (
2048
- block_id, five_hour_window_key, model,
2049
- input_tokens, output_tokens,
2050
- cache_create_tokens, cache_read_tokens,
2051
- cost_usd, entry_count, account_key
2052
- )
2053
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2054
- """,
2055
- [
2056
- (
2057
- block_id, window_key, model,
2058
- b["input_tokens"], b["output_tokens"],
2059
- b["cache_create_tokens"], b["cache_read_tokens"],
2060
- b["cost_usd"], b["entry_count"], account_key,
2061
- )
2062
- for model, b in totals["by_model"].items()
2063
- ],
2042
+ model_rows = _open_block_child_rows(
2043
+ block_id, window_key, account_key, "model", totals.get("by_model"))
2044
+ if not _lib_stats_content_revision.child_set_unchanged(
2045
+ conn, "five_hour_block_models", five_hour_window_key=window_key,
2046
+ account_key=account_key, replacement=model_rows):
2047
+ conn.execute(
2048
+ "DELETE FROM five_hour_block_models "
2049
+ "WHERE five_hour_window_key = ? AND account_key = ?",
2050
+ (window_key, account_key),
2064
2051
  )
2065
- conn.execute(
2066
- "DELETE FROM five_hour_block_projects "
2067
- "WHERE five_hour_window_key = ? AND account_key = ?",
2068
- (window_key, account_key),
2069
- )
2070
- if totals.get("by_project"):
2071
- conn.executemany(
2072
- """
2073
- INSERT INTO five_hour_block_projects (
2074
- block_id, five_hour_window_key, project_path,
2075
- input_tokens, output_tokens,
2076
- cache_create_tokens, cache_read_tokens,
2077
- cost_usd, entry_count, account_key
2078
- )
2079
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2080
- """,
2081
- [
2082
- (
2083
- block_id, window_key, project_path,
2084
- b["input_tokens"], b["output_tokens"],
2085
- b["cache_create_tokens"], b["cache_read_tokens"],
2086
- b["cost_usd"], b["entry_count"], account_key,
2052
+ if model_rows:
2053
+ conn.executemany(
2054
+ """
2055
+ INSERT INTO five_hour_block_models (
2056
+ block_id, five_hour_window_key, model,
2057
+ input_tokens, output_tokens,
2058
+ cache_create_tokens, cache_read_tokens,
2059
+ cost_usd, entry_count, account_key
2087
2060
  )
2088
- for project_path, b in totals["by_project"].items()
2089
- ],
2061
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2062
+ """,
2063
+ [tuple(row.values()) for row in model_rows],
2064
+ )
2065
+ project_rows = _open_block_child_rows(
2066
+ block_id, window_key, account_key, "project_path",
2067
+ totals.get("by_project"))
2068
+ if not _lib_stats_content_revision.child_set_unchanged(
2069
+ conn, "five_hour_block_projects", five_hour_window_key=window_key,
2070
+ account_key=account_key, replacement=project_rows):
2071
+ conn.execute(
2072
+ "DELETE FROM five_hour_block_projects "
2073
+ "WHERE five_hour_window_key = ? AND account_key = ?",
2074
+ (window_key, account_key),
2090
2075
  )
2076
+ if project_rows:
2077
+ conn.executemany(
2078
+ """
2079
+ INSERT INTO five_hour_block_projects (
2080
+ block_id, five_hour_window_key, project_path,
2081
+ input_tokens, output_tokens,
2082
+ cache_create_tokens, cache_read_tokens,
2083
+ cost_usd, entry_count, account_key
2084
+ )
2085
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2086
+ """,
2087
+ [tuple(row.values()) for row in project_rows],
2088
+ )
2089
+
2090
+
2091
+ def _open_block_child_rows(block_id, window_key, account_key, label,
2092
+ breakdown) -> "list[dict]":
2093
+ """One open block's child rows from a `_compute_block_totals` breakdown,
2094
+ as column → value mappings in the INSERT's column order."""
2095
+ return [
2096
+ {
2097
+ "block_id": block_id, "five_hour_window_key": window_key,
2098
+ label: name,
2099
+ "input_tokens": b["input_tokens"],
2100
+ "output_tokens": b["output_tokens"],
2101
+ "cache_create_tokens": b["cache_create_tokens"],
2102
+ "cache_read_tokens": b["cache_read_tokens"],
2103
+ "cost_usd": b["cost_usd"], "entry_count": b["entry_count"],
2104
+ "account_key": account_key,
2105
+ }
2106
+ for name, b in (breakdown or {}).items()
2107
+ ]
2091
2108
 
2092
2109
 
2093
2110
  def _materialize_open_candidate(conn, key, acct, start_row) -> int:
@@ -135,15 +135,19 @@ def _resolve_forecast_now(as_of: str | None) -> dt.datetime:
135
135
 
136
136
 
137
137
  def _shape_week_samples(rows, *, include_held: bool,
138
- include_account: bool = False) -> list:
138
+ include_account: bool = False,
139
+ include_journal_id: bool = False) -> list:
139
140
  """Turn snapshot rows into the sample tuples `_fetch_current_week_snapshots`
140
141
  returns. Both of that function's row-fetch legs go through here so the two
141
142
  cannot drift on the tuple width.
142
143
 
143
144
  `rows` are `(captured_at_utc, weekly_percent, five_hour_percent,
144
- weekly_observation_held[, account_key])`. The held flag is appended only
145
- for a held-inclusive read, and the account only for an explicit identity
146
- read. Existing consumers keep the historical three- or four-field shape.
145
+ weekly_observation_held[, account_key][, journal_id])`. The held flag is
146
+ appended only for a held-inclusive read, the account only for an explicit
147
+ identity read, and the row's `journal_id` (#1018 twin spec 5.3: the
148
+ displayed row's originating observation) only when asked for, after
149
+ every other field. Existing consumers keep the historical three- or
150
+ four-field shape.
147
151
  """
148
152
  out = []
149
153
  for row in rows:
@@ -156,6 +160,8 @@ def _shape_week_samples(rows, *, include_held: bool,
156
160
  sample = sample + (int(row[3] or 0),)
157
161
  if include_account:
158
162
  sample = sample + (row[4],)
163
+ if include_journal_id:
164
+ sample = sample + (row[5 if include_account else 4],)
159
165
  out.append(sample)
160
166
  return out
161
167
 
@@ -183,7 +189,8 @@ def _fetch_current_week_snapshots(conn: sqlite3.Connection, now_utc: dt.datetime
183
189
  *, account_key: "str | None" = None,
184
190
  include_held: bool = False,
185
191
  include_account: bool = False,
186
- include_scope: bool = False):
192
+ include_scope: bool = False,
193
+ include_journal_id: bool = False):
187
194
  """Return (week_start_at, week_end_at, list[(captured_at, percent, five_hr)])
188
195
  for the subscription week containing `now_utc`, or None if no snapshot
189
196
  exists for the current week.
@@ -241,6 +248,12 @@ def _fetch_current_week_snapshots(conn: sqlite3.Connection, now_utc: dt.datetime
241
248
  reading the five-hour axis selects its reading under exactly this scope
242
249
  rather than from the newest sample, which a retired replica or a
243
250
  weekly-only reading leaves without a five-hour value.
251
+
252
+ ``include_journal_id`` (#1018 twin spec 5.3) appends each row's
253
+ `journal_id` as the LAST sample field, after the held flag and the
254
+ account, so the Current Week card can resolve its displayed row's
255
+ originating observation. Every other caller keeps its tuple width, and
256
+ the trailing SQL row id ordering is unchanged.
244
257
  """
245
258
  # #341: optional account scoping — same predicate on every leg so the whole
246
259
  # window resolution stays consistent (a real key or the `unattributed`
@@ -254,6 +267,8 @@ def _fetch_current_week_snapshots(conn: sqlite3.Connection, now_utc: dt.datetime
254
267
  # The TUI's multi-account card needs the physical owner of the sample it
255
268
  # actually displays. Keep the historical sample shape for every other read.
256
269
  _owner_column = ", account_key" if include_account else ""
270
+ if include_journal_id:
271
+ _owner_column += ", journal_id"
257
272
  _sample_order = " ASC, id ASC" if include_account else " ASC"
258
273
  candidates = conn.execute(
259
274
  "SELECT week_start_at, week_end_at, week_start_date, MAX(captured_at_utc) AS latest_cap "
@@ -310,7 +325,8 @@ def _fetch_current_week_snapshots(conn: sqlite3.Connection, now_utc: dt.datetime
310
325
  (drow[0],) + _acct_p,
311
326
  ).fetchall()
312
327
  samples = _shape_week_samples(rows, include_held=include_held,
313
- include_account=include_account)
328
+ include_account=include_account,
329
+ include_journal_id=include_journal_id)
314
330
  samples = [s for s in samples if s[0] <= now_utc]
315
331
  if include_scope:
316
332
  return week_start_at, week_end_at, samples, (drow[0], None)
@@ -343,7 +359,8 @@ def _fetch_current_week_snapshots(conn: sqlite3.Connection, now_utc: dt.datetime
343
359
  tuple(matching_texts) + (chosen_date,) + _acct_p,
344
360
  ), by_id=include_account)
345
361
  samples = _shape_week_samples(rows, include_held=include_held,
346
- include_account=include_account)
362
+ include_account=include_account,
363
+ include_journal_id=include_journal_id)
347
364
  samples = [s for s in samples if s[0] <= now_utc]
348
365
  if include_scope:
349
366
  return (week_start_at, week_end_at, samples,
@@ -579,7 +596,8 @@ def _claude_current_cycle_scope(conn, now_utc: dt.datetime, *,
579
596
 
580
597
 
581
598
  def _claude_cycle_scope_samples(conn, scope: ClaudeCycleScope,
582
- now_utc: dt.datetime):
599
+ now_utc: dt.datetime, *,
600
+ include_journal_id: bool = False):
583
601
  """The decorated card's held-inclusive samples for ``scope`` (#836).
584
602
 
585
603
  The shape of ``_fetch_current_week_snapshots(include_held=True,
@@ -590,8 +608,10 @@ def _claude_cycle_scope_samples(conn, scope: ClaudeCycleScope,
590
608
  ``MAX(captured_at_utc)``. ``week_start_at`` is the NOMINAL start; the
591
609
  caller filters from ``scope.effective_start_at``. Samples are filtered to
592
610
  ``captured_at <= now_utc``. Returns ``None`` when the window cannot be
593
- resolved.
611
+ resolved. ``include_journal_id`` appends each row's `journal_id` as the
612
+ last sample field (#1018 twin spec 5.3).
594
613
  """
614
+ _jid = ", journal_id" if include_journal_id else ""
595
615
  floor_date = (now_utc.astimezone(dt.timezone.utc).date()
596
616
  - dt.timedelta(days=_CLAUDE_CYCLE_CANDIDATE_DAYS)).isoformat()
597
617
  if scope.week_start_at is None:
@@ -615,14 +635,15 @@ def _claude_cycle_scope_samples(conn, scope: ClaudeCycleScope,
615
635
  local_tz).astimezone(dt.timezone.utc)
616
636
  rows = conn.execute(
617
637
  "SELECT captured_at_utc, weekly_percent, five_hour_percent, "
618
- " weekly_observation_held, account_key "
638
+ " weekly_observation_held, account_key" + _jid + " "
619
639
  "FROM weekly_usage_snapshots "
620
640
  "WHERE week_start_date = ? AND account_key = ? "
621
641
  "ORDER BY captured_at_utc ASC, id ASC",
622
642
  (scope.week_start_date, scope.account_key),
623
643
  ).fetchall()
624
644
  samples = _shape_week_samples(rows, include_held=True,
625
- include_account=True)
645
+ include_account=True,
646
+ include_journal_id=include_journal_id)
626
647
  samples = [s for s in samples if s[0] <= now_utc]
627
648
  return (week_start_at, week_end_at, samples,
628
649
  (scope.week_start_date, None))
@@ -647,7 +668,7 @@ def _claude_cycle_scope_samples(conn, scope: ClaudeCycleScope,
647
668
  placeholders = ",".join("?" * len(matching_texts))
648
669
  rows = _week_rows_by_capture(conn.execute(
649
670
  f"SELECT captured_at_utc, weekly_percent, five_hour_percent, "
650
- f" weekly_observation_held, account_key, id "
671
+ f" weekly_observation_held, account_key{_jid}, id "
651
672
  f"FROM weekly_usage_snapshots "
652
673
  f"WHERE (week_start_at IN ({placeholders}) "
653
674
  f" OR (week_start_at IS NULL AND week_start_date = ?)) "
@@ -655,7 +676,8 @@ def _claude_cycle_scope_samples(conn, scope: ClaudeCycleScope,
655
676
  tuple(matching_texts) + (scope.week_start_date, scope.account_key),
656
677
  ), by_id=True)
657
678
  samples = _shape_week_samples(rows, include_held=True,
658
- include_account=True)
679
+ include_account=True,
680
+ include_journal_id=include_journal_id)
659
681
  samples = [s for s in samples if s[0] <= now_utc]
660
682
  return (week_start_at, week_end_at, samples,
661
683
  (scope.week_start_date, tuple(matching_texts)))
@@ -55,6 +55,7 @@ import _lib_rebuild_progress
55
55
  import _lib_record
56
56
  import _lib_segment_summary
57
57
  import _lib_selector_state
58
+ import _lib_stats_content_revision
58
59
 
59
60
 
60
61
  # Torn-tail scan window (spec §4.3). A single line must fit inside it — a
@@ -1974,6 +1975,67 @@ def _read_framed_line(fh, segment, offset, length) -> bytes:
1974
1975
  return data[lead:lead + length]
1975
1976
 
1976
1977
 
1978
+ class _RetainedLineReader:
1979
+ """`_lib_retained_records.LineReader` for the final rebuild's and
1980
+ derivation completion's streamed records (#1004 D4): the framed read
1981
+ `QuotaLineSource` uses, through the one physical segment read boundary,
1982
+ with one open handle (the current segment's), so an in-order pass opens
1983
+ each segment once. A line that is gone or no longer framed raises
1984
+ `JournalError`, the error those readers already raise for a journal
1985
+ that changed under them."""
1986
+
1987
+ __slots__ = ("_segment", "_fh", "__weakref__")
1988
+
1989
+ def __init__(self) -> None:
1990
+ self._segment = None
1991
+ self._fh = None
1992
+
1993
+ def read(self, segment: str, start: int, length: int) -> bytes:
1994
+ if segment != self._segment:
1995
+ self.close()
1996
+ try:
1997
+ self._fh = _open_segment_for_read(
1998
+ _cctally_core.JOURNAL_DIR / segment)
1999
+ except OSError as exc:
2000
+ raise _retained_line_changed(
2001
+ f"{segment}: {type(exc).__name__}: {exc}") from exc
2002
+ self._segment = segment
2003
+ try:
2004
+ return _read_framed_line(self._fh, segment, start, length)
2005
+ except (QuotaLineSourceError, OSError) as exc:
2006
+ raise _retained_line_changed(str(exc)) from exc
2007
+
2008
+ def close(self) -> None:
2009
+ fh, self._fh = self._fh, None
2010
+ self._segment = None
2011
+ if fh is not None:
2012
+ fh.close()
2013
+
2014
+
2015
+ def _retained_line_changed(message: str) -> Exception:
2016
+ """The integrity error of a streamed record whose line changed after
2017
+ the read pass (framing or digest): a `JournalError`."""
2018
+ return JournalError(f"journal changed since it was read: {message}")
2019
+
2020
+
2021
+ def _decode_retained_line(line: bytes):
2022
+ """#1004 D4: a streamed slot's record exactly as the rebuild and
2023
+ completion retained it before: the strict decode, then #997 D15's
2024
+ compaction."""
2025
+ record = _lib_journal.decode_line(line)
2026
+ if record is None:
2027
+ return None
2028
+ return _lib_journal.compact_retained(record)
2029
+
2030
+
2031
+ def _new_retained_records():
2032
+ """An empty coordinate-backed `RetainedRecords` over this journal."""
2033
+ import _lib_retained_records
2034
+ return _lib_retained_records.RetainedRecords(
2035
+ _lib_retained_records.LineCoordinates(), _RetainedLineReader,
2036
+ _decode_retained_line, error=_retained_line_changed)
2037
+
2038
+
1977
2039
  def _quota_line_sizes(quota_raw):
1978
2040
  """Encoded size per observation (line plus newline), for chunk planning."""
1979
2041
  if isinstance(quota_raw, QuotaLineSource):
@@ -4303,7 +4365,7 @@ def _resolve_obs_anchor(resolver, rec: dict) -> "str | None":
4303
4365
 
4304
4366
 
4305
4367
  def _apply_quota_records(
4306
- cache, records, *, reported_conflicts=None, quiet=False,
4368
+ cache, records, *, reported_conflicts=None, quiet=False, anchors=None,
4307
4369
  ) -> None:
4308
4370
  """Materialize Codex quota obs into an OPEN cache.db transaction, applying
4309
4371
  the §3.5 precedence rule. Callers must apply the batch's file-account
@@ -4322,7 +4384,13 @@ def _apply_quota_records(
4322
4384
  reachable from the open-time reconciliation, so `cache-sync` and the
4323
4385
  dashboard would emit a line they never emitted before, and acceptance
4324
4386
  criterion 10 requires no new stderr on an ordinary command. Its sibling was
4325
- guarded and this one was not."""
4387
+ guarded and this one was not.
4388
+
4389
+ ``anchors`` (#975, spec 4.7) replaces the per-call reset-anchor resolver
4390
+ when given. The history provenance drain passes one built from its
4391
+ committed-evidence seed, so a pass seeds each anchor group once instead of
4392
+ once per identity; every other caller omits it and gets a new resolver per
4393
+ call, as before."""
4326
4394
  oracle = _CodexAttributionOracle(cache)
4327
4395
  # One line per FILE PER BATCH, not per record. A mid-file account switch
4328
4396
  # legitimately produces a run of observations whose first-stamp-wins account
@@ -4342,7 +4410,10 @@ def _apply_quota_records(
4342
4410
  # NULL anchor, a later walk's `INSERT OR IGNORE` cannot correct it, and that
4343
4411
  # window stays fragmented forever.
4344
4412
  has_anchor = _cache_has_anchor_column(cache)
4345
- anchors = _codex_anchor_resolver(cache) if has_anchor else None
4413
+ if not has_anchor:
4414
+ anchors = None
4415
+ elif anchors is None:
4416
+ anchors = _codex_anchor_resolver(cache)
4346
4417
  insert_sql = _QUOTA_SNAPSHOT_INSERT if has_anchor else _QUOTA_SNAPSHOT_INSERT_LEGACY
4347
4418
  upsert_sql = _QUOTA_SNAPSHOT_UPSERT if has_anchor else _QUOTA_SNAPSHOT_UPSERT_LEGACY
4348
4419
  # #980 D3: a span per root covering every quota record this call wrote
@@ -6325,6 +6396,10 @@ def _replace_block_children(
6325
6396
  convergence replace both sets rather than relying on natural-key
6326
6397
  ``INSERT OR IGNORE``. This removes stale children and restores missing
6327
6398
  children while preserving the parent rowid used by milestone FKs.
6399
+
6400
+ A stored set that already equals the replacement is left in place
6401
+ (#1050): a DELETE and re-INSERT of identical rows would advance the
6402
+ content revision on every repeated fold or convergence.
6328
6403
  """
6329
6404
  for payload_key, child_table in _BLOCK_CHILDREN:
6330
6405
  if parent_account is None:
@@ -6333,14 +6408,21 @@ def _replace_block_children(
6333
6408
  else:
6334
6409
  predicate = "account_key = ? AND five_hour_window_key = ?"
6335
6410
  params = (parent_account, int(parent_window))
6336
- conn.execute(
6337
- f"DELETE FROM {child_table} WHERE {predicate}", params
6338
- )
6411
+ rows = []
6339
6412
  for child in children.get(payload_key, []):
6340
6413
  cols = dict(child)
6341
6414
  cols["block_id"] = int(block_id)
6342
6415
  if parent_account is not None:
6343
6416
  cols["account_key"] = parent_account
6417
+ rows.append(cols)
6418
+ if _lib_stats_content_revision.child_set_unchanged(
6419
+ conn, child_table, five_hour_window_key=int(parent_window),
6420
+ account_key=parent_account, replacement=rows):
6421
+ continue
6422
+ conn.execute(
6423
+ f"DELETE FROM {child_table} WHERE {predicate}", params
6424
+ )
6425
+ for cols in rows:
6344
6426
  _insert_or_ignore(conn, child_table, cols, strict=True)
6345
6427
 
6346
6428
 
@@ -11706,6 +11788,10 @@ _REBUILD_REQUIRED_TABLES = frozenset(
11706
11788
  "schema_migrations",
11707
11789
  "schema_migrations_skipped",
11708
11790
  "stats_open_fixups",
11791
+ # #1050 §4.3: the content-revision singleton and its scope table
11792
+ # (`_lib_stats_content_revision.KERNEL_TABLES`).
11793
+ "stats_content_revision",
11794
+ "stats_content_revision_scopes",
11709
11795
  "stats_publication_stamp",
11710
11796
  "stats_quota_projection_state",
11711
11797
  "usage_snapshot_suppressions",
@@ -11716,11 +11802,54 @@ _REBUILD_REQUIRED_TABLES = frozenset(
11716
11802
  "weekly_usage_snapshots",
11717
11803
  }
11718
11804
  )
11719
- #: #825/#832 Task 4.0. The one stats trigger. A scratch index without it would
11805
+ #: #825/#832 Task 4.0. The suppression trigger. A scratch index without it would
11720
11806
  #: publish a store whose live stale-replica removals leave dangling milestone
11721
11807
  #: references a rebuild cannot reproduce, so its absence fails validation like a
11722
- #: missing table does.
11723
- _REBUILD_REQUIRED_TRIGGERS = frozenset({"trg_usage_snapshot_suppression"})
11808
+ #: missing table does. #1050 §4.3 adds the content-revision kernel's triggers:
11809
+ #: a generation published without them would leave every in-place rewrite
11810
+ #: invisible to the dashboard until the next open reinstalled them.
11811
+ _REBUILD_REQUIRED_TRIGGERS = frozenset(
11812
+ {
11813
+ # `_lib_stats_content_revision.KERNEL_TRIGGERS`
11814
+ "trg_content_revision_budget_milestones_delete",
11815
+ "trg_content_revision_budget_milestones_update",
11816
+ "trg_content_revision_claude_weekly_observation_index_delete",
11817
+ "trg_content_revision_claude_weekly_observation_index_update",
11818
+ "trg_content_revision_five_hour_block_models_delete",
11819
+ "trg_content_revision_five_hour_block_models_insert",
11820
+ "trg_content_revision_five_hour_block_models_update",
11821
+ "trg_content_revision_five_hour_block_projects_delete",
11822
+ "trg_content_revision_five_hour_block_projects_insert",
11823
+ "trg_content_revision_five_hour_block_projects_update",
11824
+ "trg_content_revision_five_hour_blocks_delete",
11825
+ "trg_content_revision_five_hour_blocks_insert",
11826
+ "trg_content_revision_five_hour_blocks_update",
11827
+ "trg_content_revision_five_hour_milestones_delete",
11828
+ "trg_content_revision_five_hour_milestones_insert",
11829
+ "trg_content_revision_five_hour_milestones_update",
11830
+ "trg_content_revision_five_hour_reset_events_delete",
11831
+ "trg_content_revision_five_hour_reset_events_insert",
11832
+ "trg_content_revision_five_hour_reset_events_update",
11833
+ "trg_content_revision_percent_milestones_delete",
11834
+ "trg_content_revision_percent_milestones_insert",
11835
+ "trg_content_revision_percent_milestones_update",
11836
+ "trg_content_revision_project_budget_milestones_delete",
11837
+ "trg_content_revision_project_budget_milestones_update",
11838
+ "trg_content_revision_projected_milestones_delete",
11839
+ "trg_content_revision_projected_milestones_update",
11840
+ "trg_content_revision_week_reset_events_delete",
11841
+ "trg_content_revision_week_reset_events_update",
11842
+ "trg_content_revision_weekly_cost_snapshots_delete",
11843
+ "trg_content_revision_weekly_cost_snapshots_insert",
11844
+ "trg_content_revision_weekly_cost_snapshots_update",
11845
+ "trg_content_revision_weekly_credit_floors_delete",
11846
+ "trg_content_revision_weekly_credit_floors_update",
11847
+ "trg_content_revision_weekly_usage_snapshots_delete",
11848
+ "trg_content_revision_weekly_usage_snapshots_insert",
11849
+ "trg_content_revision_weekly_usage_snapshots_update",
11850
+ "trg_usage_snapshot_suppression",
11851
+ }
11852
+ )
11724
11853
  _REBUILD_REQUIRED_INDEXES = frozenset(
11725
11854
  {
11726
11855
  "idx_budget_milestones_alert_digest",
@@ -11729,6 +11858,7 @@ _REBUILD_REQUIRED_INDEXES = frozenset(
11729
11858
  "idx_budget_milestones_journal_id_null",
11730
11859
  "idx_claude_oauth_samples_five_hour",
11731
11860
  "idx_claude_oauth_samples_weekly",
11861
+ "idx_claude_weekly_observation_index_freshness",
11732
11862
  "idx_claude_weekly_observation_index_window",
11733
11863
  "idx_cost_week_start_at_time",
11734
11864
  "idx_cost_week_time",
@@ -11747,6 +11877,9 @@ _REBUILD_REQUIRED_INDEXES = frozenset(
11747
11877
  "idx_five_hour_milestones_block",
11748
11878
  "idx_five_hour_milestones_journal_id",
11749
11879
  "idx_five_hour_milestones_journal_id_null",
11880
+ # #1050 §4.2: the snapshot trigger's block lookup
11881
+ # (`_lib_stats_content_revision.USAGE_SNAPSHOT_INDEX`).
11882
+ "idx_five_hour_milestones_usage_snapshot_id",
11750
11883
  "idx_five_hour_reset_events_journal_id",
11751
11884
  "idx_five_hour_reset_events_journal_id_null",
11752
11885
  "idx_journal_protocol_violations_batch",
@@ -11807,8 +11940,12 @@ _REBUILD_REQUIRED_INDEXES = frozenset(
11807
11940
  # #948 epoch 1023: `outcome_quarantine_envelope` and
11808
11941
  # `outcome_quarantine_certificates` dropped; `outcome_quarantine_episodes`,
11809
11942
  # `outcome_quarantine_bounds` and `outcome_quarantine_axis_state` added.
11943
+ # #1018 twin epoch 1024: the four enriched columns of
11944
+ # `claude_weekly_observation_index` and its partial freshness index.
11945
+ # #1050 (same epoch, no bump): the content-revision kernel's two tables, its
11946
+ # triggers and `idx_five_hour_milestones_usage_snapshot_id`.
11810
11947
  _REBUILD_SCHEMA_FINGERPRINT = (
11811
- "b7fab856ea97a64c43b3d24669212925d21348c92948f57fba50787aa966995a"
11948
+ "1cc565ce0908c80a72884d5e76d815776f57a297fc4d928d0735ba179da4a6f8"
11812
11949
  )
11813
11950
 
11814
11951
 
@@ -15038,7 +15175,17 @@ def _rebuild_stats_index_locked(
15038
15175
  # dropped as soon as it has contributed (#496 S4 §4). #997 D7: the
15039
15176
  # quota observations are now retained as COORDINATES (16 bytes each)
15040
15177
  # and the cache leg re-reads them one chunk at a time.
15041
- decoded: list = []
15178
+ #
15179
+ # #1004 D4: `decoded` is a `RetainedRecords`. A retained record is a
15180
+ # streamed slot (its coordinates and the digest of its line), re-read,
15181
+ # digest-checked and decoded on each access, so every pass sees a
15182
+ # fresh record equal to the one retained before; every other position
15183
+ # keeps its `None` slot. With `MATERIALIZE_FOR_TESTS` set the pass
15184
+ # retains the decoded records and hands on today's list (a test
15185
+ # switch).
15186
+ import _lib_retained_records
15187
+ materialize = _lib_retained_records.MATERIALIZE_FOR_TESTS
15188
+ decoded = _new_retained_records()
15042
15189
  quota_raw = QuotaLineSource()
15043
15190
  protocol_evidence = []
15044
15191
  prior_high_water = None
@@ -15100,7 +15247,10 @@ def _rebuild_stats_index_locked(
15100
15247
  # fingerprint that `journal_protocol_resolution` ops reference by
15101
15248
  # name, so contributing the wrong count makes an acknowledged
15102
15249
  # violation unresolvable (#496 S5b §5.4).
15103
- decoded.extend([None] * int(summary.decoded_entry_count))
15250
+ for _ in range(int(summary.decoded_entry_count)):
15251
+ # An elided line is never read back; its coordinate is a
15252
+ # zero-length stand-in in its segment.
15253
+ decoded.append_retained(None, name, 0, 0)
15104
15254
  last_seen.merge(
15105
15255
  summary.last_seen_stamped,
15106
15256
  summary.last_seen_legacy_claude_at,
@@ -15162,7 +15312,7 @@ def _rebuild_stats_index_locked(
15162
15312
  {"t": rec.get("t"), "id": rec.get("id"),
15163
15313
  "rev": rec.get("rev", 0)})
15164
15314
  summaries.decoded(False)
15165
- decoded.append(None)
15315
+ decoded.append_retained(None, segment, offset, len(raw))
15166
15316
  prior_high_water = (segment, offset + len(raw) + 1)
15167
15317
  continue
15168
15318
  rebuild_quarantine.on_record(
@@ -15231,7 +15381,12 @@ def _rebuild_stats_index_locked(
15231
15381
  rec.get("t") in _lib_journal_router.RETAINED_RECORD_TYPES)
15232
15382
  summaries.decoded(retained, _is_codex_quota_obs(rec))
15233
15383
  if retained:
15234
- decoded.append(rec)
15384
+ if materialize:
15385
+ decoded.append_retained(
15386
+ rec, segment, offset, len(raw))
15387
+ else:
15388
+ decoded.append_streamed(
15389
+ raw, segment, offset, len(raw))
15235
15390
  else:
15236
15391
  if update_quota_cache and _is_codex_quota_obs(rec):
15237
15392
  quota_raw.append(segment, offset, len(raw))
@@ -15247,7 +15402,7 @@ def _rebuild_stats_index_locked(
15247
15402
  # and raise on every later rebuild. The selector skips a non-dict
15248
15403
  # element, so this costs one pointer and keeps every sequence
15249
15404
  # identical to the pre-change numbering (#496 S4; corrects §4.7).
15250
- decoded.append(None)
15405
+ decoded.append_retained(None, segment, offset, len(raw))
15251
15406
  prior_high_water = (
15252
15407
  segment,
15253
15408
  offset + len(raw) + 1,
@@ -15295,9 +15450,22 @@ def _rebuild_stats_index_locked(
15295
15450
  # is dropped before the inventory check and publication.
15296
15451
  _cctally_oauth_samples.attribute_staged_observations(
15297
15452
  conn, cutover_claude)
15298
- for rec in decoded:
15299
- if rec is not None:
15300
- _normalize_legacy_account_stamp(rec, cutover_claude)
15453
+ if materialize:
15454
+ # Every slot is a retained object here, so this decodes nothing.
15455
+ listed = list(decoded)
15456
+ decoded.close()
15457
+ decoded = listed
15458
+ for rec in decoded:
15459
+ if rec is not None:
15460
+ _normalize_legacy_account_stamp(rec, cutover_claude)
15461
+ else:
15462
+ # #1004 D4: normalization moves to decode time. The hook is
15463
+ # installed before the first consumer reads a record (the staged
15464
+ # attribution above reads only projected tables), so every pass
15465
+ # below sees each record normalized, as the in-place loop left it.
15466
+ decoded.set_normalizer(
15467
+ lambda rec: _normalize_legacy_account_stamp(
15468
+ rec, cutover_claude))
15301
15469
 
15302
15470
  # Resolve corrections BEFORE either disposable index is mutated. A
15303
15471
  # malformed revision, divergent same-revision candidate, or invalid