cctally 1.114.0 → 1.116.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 (86) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/bin/_cctally_account.py +32 -8
  3. package/bin/_cctally_cache.py +4491 -295
  4. package/bin/_cctally_completion.py +1355 -0
  5. package/bin/_cctally_config.py +37 -0
  6. package/bin/_cctally_core.py +282 -9
  7. package/bin/_cctally_dashboard.py +8 -11
  8. package/bin/_cctally_dashboard_conversation.py +292 -23
  9. package/bin/_cctally_dashboard_envelope.py +42 -35
  10. package/bin/_cctally_dashboard_sources.py +18 -9
  11. package/bin/_cctally_db.py +401 -9
  12. package/bin/_cctally_doctor.py +469 -27
  13. package/bin/_cctally_five_hour.py +166 -1049
  14. package/bin/_cctally_forecast.py +81 -9
  15. package/bin/_cctally_journal.py +5076 -177
  16. package/bin/_cctally_journal_repair.py +110 -19
  17. package/bin/_cctally_oauth_samples.py +280 -0
  18. package/bin/_cctally_parser.py +47 -0
  19. package/bin/_cctally_project.py +8 -2
  20. package/bin/_cctally_quarantine.py +662 -0
  21. package/bin/_cctally_quota.py +96 -12
  22. package/bin/_cctally_quota_model.py +391 -83
  23. package/bin/_cctally_record.py +1088 -269
  24. package/bin/_cctally_rederive.py +854 -192
  25. package/bin/_cctally_refresh.py +113 -13
  26. package/bin/_cctally_source_analytics.py +23 -2
  27. package/bin/_cctally_statusline.py +7062 -505
  28. package/bin/_cctally_store.py +1244 -91
  29. package/bin/_cctally_transcript.py +8 -7
  30. package/bin/_cctally_tui.py +119 -60
  31. package/bin/_cctally_update.py +25 -0
  32. package/bin/_lib_account_binding.py +152 -0
  33. package/bin/_lib_account_protocol.py +569 -0
  34. package/bin/_lib_aggregators.py +4 -0
  35. package/bin/_lib_cache_coverage.py +13 -1
  36. package/bin/_lib_cache_writer_lock.py +19 -3
  37. package/bin/_lib_clone_copy.py +177 -0
  38. package/bin/_lib_codex_conversation_query.py +133 -19
  39. package/bin/_lib_codex_conversation_watch.py +59 -14
  40. package/bin/_lib_codex_drain_admission.py +214 -0
  41. package/bin/_lib_codex_metadata.py +18 -1
  42. package/bin/_lib_codex_publication_barrier.py +277 -0
  43. package/bin/_lib_codex_rollout_container.py +502 -0
  44. package/bin/_lib_conversation_dispatch.py +3 -0
  45. package/bin/_lib_conversation_query.py +11 -3
  46. package/bin/_lib_dashboard_sources.py +57 -15
  47. package/bin/_lib_detector_access_registry.py +4038 -0
  48. package/bin/_lib_doctor.py +753 -1
  49. package/bin/_lib_five_hour.py +126 -0
  50. package/bin/_lib_ingest_frontier.py +414 -20
  51. package/bin/_lib_journal.py +261 -10
  52. package/bin/_lib_journal_router.py +166 -48
  53. package/bin/_lib_jsonl.py +5 -4
  54. package/bin/_lib_legacy_spool.py +126 -0
  55. package/bin/_lib_legacy_writer_evidence.py +136 -0
  56. package/bin/_lib_meter_rate_change.py +10 -0
  57. package/bin/_lib_oauth_samples.py +238 -0
  58. package/bin/_lib_outcome_quarantine.py +690 -0
  59. package/bin/_lib_pricing.py +7 -2
  60. package/bin/_lib_pricing_check.py +0 -12
  61. package/bin/_lib_provider_activity.py +770 -0
  62. package/bin/_lib_quarantine.py +155 -0
  63. package/bin/_lib_quota_calibration.py +53 -8
  64. package/bin/_lib_quota_copy.py +2 -1
  65. package/bin/_lib_quota_model.py +384 -52
  66. package/bin/_lib_rebuild_progress.py +146 -0
  67. package/bin/_lib_record.py +22 -0
  68. package/bin/_lib_rederive.py +393 -8
  69. package/bin/_lib_segment_summary.py +35 -2
  70. package/bin/_lib_snapshot_cache.py +43 -1
  71. package/bin/_lib_stale_proof.py +708 -0
  72. package/bin/_lib_statusline_candidates.py +356 -27
  73. package/bin/_lib_worker_launches.py +315 -0
  74. package/bin/cctally +29 -12
  75. package/dashboard/static/assets/{ConversationsView-Bc8LCOAZ.js → ConversationsView-Dflb_myA.js} +1 -1
  76. package/dashboard/static/assets/DoctorModal-DFX5CfIs.js +1 -0
  77. package/dashboard/static/assets/{ModalRoot-DLPgyAM_.js → ModalRoot-HlxoagIa.js} +1 -1
  78. package/dashboard/static/assets/{ProjectsDrillPanel-z53a4v4M.js → ProjectsDrillPanel-C3PJy1xx.js} +1 -1
  79. package/dashboard/static/assets/{SourceDetailModal-Bw0T8W-W.js → SourceDetailModal-BczsWKhD.js} +1 -1
  80. package/dashboard/static/assets/{UpdateModal-D5AKx4Yw.js → UpdateModal-Os-Lo5cs.js} +1 -1
  81. package/dashboard/static/assets/{index-BXDOzSng.js → index-CYZ3NLqF.js} +3 -3
  82. package/dashboard/static/assets/{outlineNavigation-JObY7cUY.js → outlineNavigation-k-zIif6W.js} +1 -1
  83. package/dashboard/static/dashboard.html +1 -1
  84. package/package.json +20 -2
  85. package/bin/_lib_merge_gate.py +0 -725
  86. package/dashboard/static/assets/DoctorModal-D--qVBwe.js +0 -1
@@ -0,0 +1,1355 @@
1
+ """The shared, durable derivation-completion step (#825/#832 fix round A,
2
+ Codex fix-round-1 Q1/Q2).
3
+
4
+ A stats rebuild is apply-only: it replays journaled decisions and never
5
+ derives a raw observation. Whenever an admitted current input reached the
6
+ journal without a current binary deriving it, an apply-only rebuild consumes
7
+ it without materializing it — permanently, because no decision exists for any
8
+ later rebuild to replay. Three ways in:
9
+
10
+ * the epoch transition: the tick whose ingest found another epoch appended its
11
+ reading and deferred (spec C3/F1);
12
+ * a previous-release process whose ingest, at its own epoch, derived current
13
+ readings UNSTAMPED: admission excludes those decisions (review P1-1);
14
+ * the corrupt-index heal and any interrupted recovery, whose rebuild consumed
15
+ a reading a deferred ingest never reached.
16
+
17
+ `complete_and_rebuild` runs before every production rebuild of the live index
18
+ (the caller holds stats maintenance EXCLUSIVE and `journal.ingest.lock`, so no
19
+ current decision can be appended concurrently). It
20
+
21
+ 1. starts from a PROVEN complete predecessor state — the derivation frontier
22
+ of a trusted current index (`journal_derivation_frontier`) — or, absent that
23
+ proof, from the ORIGIN of the retained journal, bootstrap facts included
24
+ (fix round E, spec 4.7 revision 1, Codex fix-round-2 Q1): the cutover is no
25
+ proof, because an admitted decision for a pre-cutover input lands after
26
+ it. The replaced index's applied cursor
27
+ is never proof: an old process advances it past inputs it derived
28
+ unstamped, and a rebuild advances it past inputs it never derived. The
29
+ frontier counts consumed inputs; the cycle that consumed them journaled
30
+ its decisions beyond it, through the proof's output end, so the
31
+ predecessor is the rebuild through the frontier PLUS those owed decisions
32
+ (fix round C, P1-1) — without them a window input is derived against a
33
+ state uninterrupted ingest never had;
34
+ 2. builds a scratch index at that point and replays the admitted inputs of the
35
+ window in journal order through the ONE shared scratch derivation
36
+ (`_cctally_rederive.replay_scratch_records`: the live pipeline hooks,
37
+ harvest included), evaluating the outcome quarantine at each input's
38
+ original position, while a `_cctally_journal.ScratchRetention` keeps the
39
+ projection on the retained decisions (historical facts, corrections,
40
+ reviewed holds and tombstones), never on fresh candidates in their place;
41
+ 3. finds the GAP inputs (an input with a derived decision no admitted
42
+ decision answers; an input a previous release decided is history, never a
43
+ gap), replays the window without them as the reference, and once more
44
+ with them, holding a fresh candidate only where it differs from the
45
+ reference, so that the difference between the two is exactly the gap's
46
+ causal effect and its descendants — never an unrelated repricing of
47
+ decisions already made;
48
+ 4. journals that difference by the binding append policy: no physical event
49
+ under the id → an ordinary current-stamped revision-0 `evt`; an admitted
50
+ identical decision → nothing; only an excluded legacy decision, or an
51
+ admitted decision the gap changes → one completed correction batch whose
52
+ action is above every retained revision of the id; an admitted decision the
53
+ gap removes → a tombstone in that batch;
54
+ 5. rebuilds the live index over the result. The consumed-input frontier stays
55
+ distinct from output positions: inputs another process appended between
56
+ the window and the appended outputs are completed in a further round, and
57
+ an index published with inputs still unproven records the frontier where
58
+ proof stops, so the next completion picks them up. The published proof's
59
+ output end is the journal end, sound only because nothing between the two
60
+ derives from a later input (`_certify`); when that cannot be shown, the
61
+ proof completion started from stands.
62
+
63
+ 6. owes ONE post-unlock ingest when the published prefix stops before an
64
+ admitted derivable input (fix round J4, Codex cycle 5 P1-1). The
65
+ recorder that appended such an input waited a bounded time for the
66
+ locks this step holds and may have exited, and a statusline wake finds
67
+ no intake for it, so nothing else is bound to derive it. A durable
68
+ obligation file of its own (`residual_dir`, named by a nonce) records
69
+ the obligation before the locks are released; every publisher's
70
+ completion wake then dispatches the statusline continuation worker,
71
+ which reads every obligation and runs exactly one bounded authoritative
72
+ ingest (`run_owed_residual_derivation`) holding no accounting or
73
+ selected-state lock. Each obligation names the journal end it was
74
+ written at, and the attempt discharges it only when the cursor its
75
+ ingest left applied covers that residual — or nothing derivable remains
76
+ before it (fix round K5, Codex cycle 6 P1-1) — by removing exactly that
77
+ file, so a completion that records its obligation meanwhile (another
78
+ file) is never discharged by an attempt that predates it (fix round K6,
79
+ Codex cycle 6 verification P1-1). A returned lock-contention stop (a
80
+ busy maintenance, ingest or cache writer lock) keeps the obligation and
81
+ hands the lock that refused it to the statusline's one-shot release
82
+ waiter, whose wake runs the next attempt; any other failure keeps it
83
+ too, recording the failure and a backoff in it, so the continuation
84
+ never retries because of its own failure and a persistently failing
85
+ ingest costs one attempt per backoff interval. A worker killed inside
86
+ the ingest leaves the obligation for the next activating event (review
87
+ cycle 6).
88
+
89
+ Maintenance never replays a notification: the scratch replay has no alert
90
+ configuration and the live index is only ever folded, never derived. A retry
91
+ recomputes the same plan from journal truth, reuses an interrupted batch's
92
+ `at`, and so re-emits byte-identical lines; a completed batch makes the gap
93
+ disappear, so a further run appends nothing.
94
+ """
95
+ from __future__ import annotations
96
+
97
+ import contextlib
98
+ import copy
99
+ import dataclasses
100
+ import datetime as dt
101
+ import errno
102
+ import hashlib
103
+ import json
104
+ import os
105
+ import pathlib
106
+ import shutil
107
+ import sqlite3
108
+ import tempfile
109
+ import uuid
110
+
111
+ import _cctally_core
112
+ import _cctally_journal as _journal
113
+ import _lib_account_protocol
114
+ import _lib_accounts
115
+ import _lib_journal
116
+ import _lib_journal_router
117
+ import _lib_rebuild_progress
118
+ import _lib_rederive
119
+
120
+ #: The correction-batch family this step (and the live collision check)
121
+ #: journals under.
122
+ FAMILY = _journal.DERIVATION_COMPLETION_FAMILY
123
+
124
+ #: Completion rounds per rebuild. A round only repeats when another process
125
+ #: appended a derivable input while the previous round was appending its own
126
+ #: outputs; past this bound the index is published with the frontier where
127
+ #: proof stops and the next completion continues from there.
128
+ MAX_ROUNDS = 3
129
+
130
+ #: Op kinds the pipeline derives decisions from (manual credits, cost syncs
131
+ #: and reviewed weekly decisions). Account-registry ops only fold, which an
132
+ #: apply-only rebuild already does, so they alone never open a window.
133
+ _DERIVING_OP_KINDS = frozenset({
134
+ "weekly_credit_floor",
135
+ "sync_week",
136
+ "claude_weekly_observation_decision",
137
+ _lib_account_protocol.WEEKLY_OBSERVATION_DECISION_V2_KIND,
138
+ })
139
+
140
+
141
+ def _order(coordinate) -> tuple:
142
+ return _journal.journal_coordinate_key(coordinate)
143
+
144
+
145
+ def _iso_now() -> str:
146
+ return (dt.datetime.now(dt.timezone.utc).replace(microsecond=0)
147
+ .isoformat().replace("+00:00", "Z"))
148
+
149
+
150
+ def is_deriving_input(record) -> bool:
151
+ """Whether the live pipeline derives decisions from ``record``."""
152
+ if not isinstance(record, dict):
153
+ return False
154
+ if record.get("t") == "obs":
155
+ import _cctally_rederive
156
+ return _cctally_rederive._is_rederivable_raw(record)
157
+ if record.get("t") == "op":
158
+ return (record.get("payload") or {}).get("kind") in _DERIVING_OP_KINDS
159
+ return False
160
+
161
+
162
+ def window_has_derivable_inputs(lo, hi, boundary) -> bool:
163
+ """Whether the journal holds an admitted deriving input after ``lo`` and
164
+ at or before ``hi`` (both line-boundary coordinates)."""
165
+ if lo is None or hi is None or _order(hi) <= _order(lo):
166
+ return False
167
+ for segment, offset, raw in _journal.iter_range(lo, hi):
168
+ record = _lib_journal.decode_line(raw)
169
+ if (record is not None and is_deriving_input(record)
170
+ and _journal._is_admitted(record, segment, offset, boundary)):
171
+ return True
172
+ return False
173
+
174
+
175
+ # --------------------------------------------------------------------------
176
+ # entry point
177
+ # --------------------------------------------------------------------------
178
+
179
+ def complete_and_rebuild(*, context, high_water=None, update_quota_cache=True,
180
+ before_swap=None):
181
+ """Complete derivation through the rebuild's prefix, then rebuild the
182
+ live index over it (see the module docstring). Returns the final
183
+ rebuild's `RebuildResult`."""
184
+ import _cctally_retention
185
+
186
+ dest = pathlib.Path(_cctally_core.DB_PATH)
187
+ boundary = _journal._whole_prefix_admission_boundary()
188
+ # #946 (spec 4.7 revision 2): the SAFE endpoint of the prefix, so the
189
+ # proof this step publishes never names a torn suffix as consumed.
190
+ target = (_journal._safe_pinned_high_water(high_water)
191
+ if high_water is not None
192
+ else _journal.journal_safe_high_water())
193
+ if boundary is None or target is None:
194
+ # No account-bound cutover yet: nothing is fenced and there is no
195
+ # baseline to replay from, so the rebuild stays apply-only and
196
+ # carries whatever frontier the replaced index proves.
197
+ with _cctally_retention.retention_shared(label="stats rebuild"):
198
+ return _journal._rebuild_stats_index_locked(
199
+ context=context, high_water=high_water,
200
+ update_quota_cache=update_quota_cache,
201
+ before_swap=before_swap)
202
+ trusted = _journal.read_trusted_derivation_state(dest)
203
+ baseline = (boundary.segment, boundary.end_offset)
204
+ if trusted is not None and _order(trusted.frontier) < _order(baseline):
205
+ trusted = None
206
+ frontier, target = _complete(context, trusted, target, boundary)
207
+ with _cctally_retention.retention_shared(label="stats rebuild"):
208
+ result = _journal._rebuild_stats_index_locked(
209
+ context=context, high_water=target,
210
+ update_quota_cache=update_quota_cache, before_swap=before_swap,
211
+ derivation_frontier=frontier,
212
+ whole_prefix=high_water is None)
213
+ # Step 6 (fix round J4): read the journal end AFTER the publication, so
214
+ # an input appended while the final rebuild ran is owed too. Still under
215
+ # the caller's locks, so no ingest can consume it in between.
216
+ end = _journal.journal_safe_high_water()
217
+ if window_has_derivable_inputs(target, end, boundary):
218
+ try:
219
+ owe_residual_derivation(target, residual_through=end)
220
+ except ResidualObligationNotDurable as exc:
221
+ # Fix round K11 (K9K10-R7): the index IS published. The caller
222
+ # still posts the publication wake once its locks are released,
223
+ # and reports the obligation failure rather than a failed
224
+ # rebuild; ``result`` is the published index.
225
+ exc.result = result
226
+ raise
227
+ return result
228
+
229
+
230
+ def post_publication_wake() -> None:
231
+ """#825/#832 spec 4.3 revision 2: the completion wake a live index
232
+ publication posts once its locks are released. Fix round K11
233
+ (K9K10-R7): a completion whose residual obligation could not be made
234
+ durable has still published, so it posts this wake before reporting
235
+ that failure. Never raises, never waits for the work. File-only unless
236
+ every retained head waits: then the event compares their dependency
237
+ tokens without waiting (fix round K13, 825-832-CD7-V02), which a
238
+ publishing process may read.
239
+
240
+ Fix round K12 (K11R-11): the one helper every live publication uses.
241
+ It also swallows `StatsRebuildDeferred`, a `BaseException` a wake's own
242
+ reservation can meet, which no publication may surface."""
243
+ import _cctally_db
244
+ try:
245
+ import _cctally_statusline
246
+ _cctally_statusline.post_wake("index-publication")
247
+ except (Exception, _cctally_db.StatsRebuildDeferred): # noqa: BLE001
248
+ pass
249
+
250
+
251
+ # --------------------------------------------------------------------------
252
+ # the residual-derivation continuation (fix round J4, Codex cycle 5 P1-1)
253
+ # --------------------------------------------------------------------------
254
+
255
+ #: The backoff after a failed residual attempt that no lock caused (fix
256
+ #: round K6, Codex cycle 6 verification P1-1): the first retry waits this
257
+ #: long, each further failure doubles it, up to `_RESIDUAL_RETRY_MAX_S`.
258
+ _RESIDUAL_RETRY_BASE_S = 60
259
+ _RESIDUAL_RETRY_MAX_S = 3600
260
+
261
+ #: A marker document larger than this is not one this module wrote.
262
+ _RESIDUAL_MARKER_MAX_BYTES = 64 * 1024
263
+
264
+
265
+ def _now() -> float:
266
+ """The wall clock the residual backoff is judged against (a test seam)."""
267
+ import time
268
+ return time.time()
269
+
270
+
271
+ def residual_dir() -> pathlib.Path:
272
+ """The durable obligations: one file per completion that owes ONE
273
+ post-unlock ingest, named by its nonce (fix round K6). A completion only
274
+ ever creates its own file, and a discharge only ever removes a file it
275
+ read and proved covered, so no replacement can race a removal."""
276
+ return pathlib.Path(_cctally_core.APP_DIR) / "derivation-residual"
277
+
278
+
279
+ def _obligation_paths() -> "list[pathlib.Path]":
280
+ """One listing; a missing directory is no obligation. A dot-file is an
281
+ unpublished temporary and never an obligation.
282
+
283
+ Fix round K11 (K9K10-R8): only a MISSING directory is empty. Any other
284
+ listing failure raises `OSError`, because a directory that cannot be
285
+ listed can hold owed work: the callers count it as owed
286
+ (`residual_derivation_owed`, `residual_derivation_due`) and answer it
287
+ with a bounded attempt (`run_owed_residual_derivation`)."""
288
+ try:
289
+ with os.scandir(residual_dir()) as entries:
290
+ names = [entry.name for entry in entries]
291
+ except FileNotFoundError:
292
+ return []
293
+ return sorted(residual_dir() / name for name in names
294
+ if name.endswith(".json") and not name.startswith("."))
295
+
296
+
297
+ @dataclasses.dataclass(frozen=True)
298
+ class _Obligation:
299
+ path: pathlib.Path
300
+ document: dict
301
+
302
+
303
+ def _read_obligation(path: pathlib.Path) -> "_Obligation | None":
304
+ """The obligation recorded at ``path``, or None once it is gone. A file
305
+ that cannot be parsed still owes its ingest: it reads as an obligation
306
+ whose residual end is unknown (only a clean cycle covers it) and that
307
+ has never failed."""
308
+ try:
309
+ with open(path, "rb") as handle:
310
+ raw = handle.read(_RESIDUAL_MARKER_MAX_BYTES + 1)
311
+ except FileNotFoundError:
312
+ return None
313
+ except OSError:
314
+ raw = b""
315
+ try:
316
+ document = json.loads(raw.decode("utf-8"))
317
+ if not isinstance(document, dict):
318
+ document = {}
319
+ except (ValueError, UnicodeDecodeError):
320
+ document = {}
321
+ return _Obligation(path, document)
322
+
323
+
324
+ def _read_obligations() -> "list[_Obligation]":
325
+ found = []
326
+ for path in _obligation_paths():
327
+ obligation = _read_obligation(path)
328
+ if obligation is not None:
329
+ found.append(obligation)
330
+ return found
331
+
332
+
333
+ def residual_obligations() -> "list[dict]":
334
+ """Every recorded obligation's document, oldest file name first
335
+ (diagnostics and tests)."""
336
+ return [obligation.document for obligation in _read_obligations()]
337
+
338
+
339
+ def residual_derivation_owed() -> bool:
340
+ """File-only and lock-free (one listing): does any completion owe a
341
+ residual ingest, whatever its backoff? An unreadable obligation
342
+ directory is owed work, never none (fix round K11, K9K10-R8)."""
343
+ try:
344
+ return bool(_obligation_paths())
345
+ except OSError:
346
+ return True
347
+
348
+
349
+ def _unreadable_backoff_path() -> pathlib.Path:
350
+ """Fix round K11 (K9K10-R8): the backoff of the attempts made while the
351
+ obligation directory cannot be listed. It lives beside that directory,
352
+ never in it, so it stays writable when only the directory is not."""
353
+ return (pathlib.Path(_cctally_core.APP_DIR)
354
+ / "residual-obligations-unlistable.json")
355
+
356
+
357
+ def _read_unreadable_backoff() -> dict:
358
+ try:
359
+ with open(_unreadable_backoff_path(), "rb") as handle:
360
+ document = json.loads(
361
+ handle.read(_RESIDUAL_MARKER_MAX_BYTES + 1).decode("utf-8"))
362
+ except (OSError, ValueError, UnicodeDecodeError):
363
+ return {}
364
+ return document if isinstance(document, dict) else {}
365
+
366
+
367
+ def _unreadable_due(now: float) -> bool:
368
+ """Whether an attempt over an unlistable obligation directory is owed
369
+ now: never attempted, or its non-sliding backoff ended (spec 4.3
370
+ *Waiting heads*: 60 s doubling to 3,600 s)."""
371
+ return _due(_Obligation(_unreadable_backoff_path(),
372
+ _read_unreadable_backoff()), now)
373
+
374
+
375
+ def _clear_unreadable_backoff() -> None:
376
+ """End the unlistable directory's backoff once it lists again. A stat
377
+ in the steady state: there is nothing to remove."""
378
+ path = _unreadable_backoff_path()
379
+ if not os.path.lexists(path):
380
+ return
381
+ try:
382
+ path.unlink()
383
+ except OSError:
384
+ pass
385
+
386
+
387
+ def _retry_at(document: dict):
388
+ value = document.get("retryAt")
389
+ return value if isinstance(value, (int, float)) and not isinstance(
390
+ value, bool) else None
391
+
392
+
393
+ def _due(obligation: _Obligation, now: float) -> bool:
394
+ """Whether an attempt is owed now. A retry instant further away than the
395
+ longest backoff was recorded before the clock stepped back, and is due
396
+ rather than stalling the obligation for as long as the step."""
397
+ retry_at = _retry_at(obligation.document)
398
+ return (retry_at is None or retry_at <= now
399
+ or retry_at - now > _RESIDUAL_RETRY_MAX_S)
400
+
401
+
402
+ def residual_derivation_due(now: "float | None" = None) -> bool:
403
+ """File-only and lock-free: is an attempt owed NOW — an obligation exists
404
+ that is not waiting out the backoff of its own failed attempt (fix round
405
+ K6)? The statusline counts only this as pending work, so an obligation
406
+ in backoff costs no dispatch until an activating event after it ends.
407
+ One listing when nothing is owed."""
408
+ now = _now() if now is None else now
409
+ try:
410
+ paths = _obligation_paths()
411
+ except OSError:
412
+ # Fix round K11 (K9K10-R8): unknown obligations are owed, bounded by
413
+ # the unreadable directory's own backoff.
414
+ return _unreadable_due(now)
415
+ if not paths:
416
+ return False
417
+ for path in paths:
418
+ obligation = _read_obligation(path)
419
+ if obligation is not None and _due(obligation, now):
420
+ return True
421
+ return False
422
+
423
+
424
+ def _point(coordinate):
425
+ return (None if coordinate is None
426
+ else [str(coordinate[0]), int(coordinate[1])])
427
+
428
+
429
+ class ResidualObligationNotDurable(OSError):
430
+ """A completion published its capped index but could not durably record
431
+ the residual-derivation obligation it owes (review cycle 7 P1-1, spec
432
+ 4.7). The completion must not report success: nothing would resume its
433
+ residual automatically. The residual input itself stays in the journal,
434
+ past the published cursor, for the next ingest."""
435
+
436
+
437
+ def _fsync_path(path) -> None:
438
+ fd = os.open(path, os.O_RDONLY)
439
+ try:
440
+ os.fsync(fd)
441
+ finally:
442
+ os.close(fd)
443
+
444
+
445
+ def _write_obligation(path: pathlib.Path, document: dict) -> None:
446
+ """Durably publish ``document`` at ``path`` (temporary, fsync, atomic
447
+ rename, directory fsync), or raise `OSError`. Only a completion creating
448
+ its own nonce file, and the one reservation-holding attempt recording its
449
+ failure in a file it read, ever write one, so the rename never replaces
450
+ another writer's obligation.
451
+
452
+ Review cycle 7 P1-1: EVERY writer fsyncs the residual directory's parent
453
+ after ensuring the directory exists, not only the writer that created
454
+ it. A creator that died between its `mkdir` and that barrier left a
455
+ directory a host crash can still lose — with every obligation a later
456
+ writer put in it."""
457
+ directory = path.parent
458
+ temp = directory / f".{path.name}.{os.getpid()}.tmp"
459
+ try:
460
+ directory.mkdir(parents=True, exist_ok=True)
461
+ _fsync_path(directory.parent)
462
+ fd = os.open(temp, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
463
+ with os.fdopen(fd, "w", encoding="utf-8") as handle:
464
+ json.dump(document, handle, separators=(",", ":"))
465
+ handle.flush()
466
+ os.fsync(handle.fileno())
467
+ os.replace(temp, path)
468
+ _fsync_path(directory)
469
+ except OSError:
470
+ try:
471
+ temp.unlink()
472
+ except OSError:
473
+ pass
474
+ raise
475
+
476
+
477
+ def owe_residual_derivation(published, *, residual_through=None) -> None:
478
+ """Record, durably, that the index this completion published stops at
479
+ ``published`` before an admitted derivable input, and that the inputs
480
+ owed end at ``residual_through`` (the journal end the completion read;
481
+ read here when not given). Every completion records its OWN obligation
482
+ file, named by a fresh nonce: one attempt answers all of them (one
483
+ ingest consumes every input past the cursor) and removes only the files
484
+ it read and proved covered.
485
+
486
+ Fail-closed (review cycle 7 P1-1, spec 4.7): when the obligation cannot
487
+ be made durable this raises `ResidualObligationNotDurable`, so the
488
+ completion — which has already published its capped index — reports
489
+ failure instead of a success whose wake finds nothing to resume. The
490
+ caller's own failure handling (an exit 3, a retained heal marker, a
491
+ rederive apply error) then stands, and the residual input stays in the
492
+ journal for the next ingest."""
493
+ if residual_through is None:
494
+ try:
495
+ residual_through = _journal.journal_safe_high_water()
496
+ except Exception: # noqa: BLE001 — best effort, like the write
497
+ residual_through = None
498
+ nonce = uuid.uuid4().hex
499
+ path = residual_dir() / f"{nonce}.json"
500
+ try:
501
+ _write_obligation(path, {
502
+ "schemaVersion": 3,
503
+ "publishedThrough": _point(published),
504
+ "residualThrough": _point(residual_through),
505
+ "recordedAt": _iso_now(), "nonce": nonce})
506
+ except OSError as exc:
507
+ raise ResidualObligationNotDurable(
508
+ getattr(exc, "errno", None) or errno.EIO,
509
+ "the derivation completion published a capped index but could "
510
+ f"not durably record its residual obligation ({exc})",
511
+ str(path)) from exc
512
+ # Fix round K7 (Codex round 14 P1-1): the new obligation advances
513
+ # the statusline's pending generation, as a render capture does, so
514
+ # a continuation pass that scanned the disk before this file existed
515
+ # cannot clear pending past it and lose the wake that follows.
516
+ # Fix round K13 (review cycle 7 P1-1): a failed registration is not
517
+ # absorbed — `register_durable_work` falls back to the durable dirty
518
+ # notification, and an older pass keeps pending for the publication
519
+ # wake this completion posts after it releases its locks.
520
+ try:
521
+ import _cctally_statusline
522
+ _cctally_statusline.register_durable_work()
523
+ except Exception: # noqa: BLE001 — the obligation itself is durable
524
+ pass
525
+
526
+
527
+ @dataclasses.dataclass(frozen=True)
528
+ class ResidualAttempt:
529
+ """What one `run_owed_residual_derivation` call did (fix rounds K5, K6).
530
+
531
+ ``disposition`` is ``none`` (nothing was owed), ``discharged`` (the
532
+ applied cursor covers every obligation it read), ``blocked`` (a
533
+ lock-contention stop: the uncovered obligations stay and ``blocked_by``
534
+ names the lock whose release resumes them), ``failed`` (any other
535
+ failure: the uncovered obligations stay, each recording the failure and
536
+ its backoff, and ``reason`` says why) or ``backoff`` (every obligation is
537
+ still waiting out a failed attempt's backoff: no ingest ran)."""
538
+
539
+ owed: bool
540
+ disposition: str
541
+ blocked_by: object = None
542
+ reason: str = ""
543
+
544
+
545
+ def _residual_through(document: dict):
546
+ """The journal end an obligation's residual inputs reach, or None when
547
+ it does not say (unreadable, or written without one)."""
548
+ through = document.get("residualThrough")
549
+ if (isinstance(through, list) and len(through) == 2
550
+ and isinstance(through[0], str) and type(through[1]) is int
551
+ and through[1] >= 0):
552
+ return (through[0], through[1])
553
+ return None
554
+
555
+
556
+ def _residual_covered(result, through) -> bool:
557
+ """Whether a returned ingest consumed the residual inputs a marker owes:
558
+ its applied cursor reaches ``through``, or no admitted derivable input
559
+ lies between the two. A marker that names no end is covered only by a
560
+ clean cycle, which consumed through a high water it read after the
561
+ marker was written."""
562
+ if not getattr(result, "ran", False) or getattr(result, "error", None):
563
+ return False
564
+ applied = getattr(result, "applied_through", None)
565
+ if through is None:
566
+ return getattr(result, "blocked_by", None) is None
567
+ if applied is not None and _order(applied) >= _order(through):
568
+ return True
569
+ start = applied if applied is not None else journal_origin()
570
+ return not window_has_derivable_inputs(
571
+ start, through, _journal._whole_prefix_admission_boundary())
572
+
573
+
574
+ def _discharge(obligation: _Obligation) -> None:
575
+ """Remove exactly the obligation file this attempt read and proved
576
+ covered. No other writer replaces a file under its name, so a newer
577
+ completion's obligation — another file — can never be removed here."""
578
+ try:
579
+ obligation.path.unlink()
580
+ except OSError:
581
+ pass
582
+
583
+
584
+ def _record_failure(obligation: _Obligation, reason: str, now: float) -> float:
585
+ """Record one failed attempt in the obligation it was made for: the
586
+ count, when and why, and the backoff before the next attempt. Returns
587
+ the retry instant."""
588
+ document = dict(obligation.document)
589
+ failures = document.get("failures")
590
+ failures = (failures if type(failures) is int and failures >= 0 else 0) + 1
591
+ interval = min(_RESIDUAL_RETRY_MAX_S,
592
+ _RESIDUAL_RETRY_BASE_S * 2 ** min(failures - 1, 16))
593
+ document.update({
594
+ "failures": failures,
595
+ "lastFailure": reason,
596
+ "lastFailureAt": _iso_now(),
597
+ "retryAt": now + interval,
598
+ })
599
+ try:
600
+ _write_obligation(obligation.path, document)
601
+ except OSError:
602
+ # The obligation itself is already durable and unchanged; only its
603
+ # backoff is lost, so the next activating event retries sooner.
604
+ pass
605
+ return now + interval
606
+
607
+
608
+ def run_owed_residual_derivation() -> ResidualAttempt:
609
+ """Answer the recorded obligations with exactly ONE bounded
610
+ authoritative ingest attempt. Called by the statusline continuation
611
+ worker, which holds no accounting or selected-state lock, so the
612
+ ingest's bounded waits respect the lock-order law. The attempt reads
613
+ every obligation file first; one written after that (a completion that
614
+ published another capped prefix meanwhile) is not answered by this
615
+ attempt and stays owed. Once the ingest RETURNS (fix round K5, Codex
616
+ cycle 6 P1-1; fix round K6, its verification P1-1):
617
+
618
+ * each obligation its applied cursor covers is discharged — its own
619
+ file removed, nothing else;
620
+ * it lost a lock another holder held (a contended ``blocked_by``: the
621
+ maintenance or ingest lock refused it, or the cache leg prefix-stopped
622
+ on a cache writer flock) -> the uncovered obligations stay, and the
623
+ caller registers that lock with the one-shot release waiter, whose
624
+ wake runs the next attempt; nothing here retries;
625
+ * anything else (a raised error, an I/O failure on a lock, a cycle that
626
+ returned short of the residual with no lock to wait for) -> the
627
+ uncovered obligations stay, durable, each recording the failure and a
628
+ backoff. The attempt never re-arms itself: until the backoff ends no
629
+ attempt runs an ingest and the statusline counts none of them as
630
+ pending work, so a persistently failing ingest costs at most one
631
+ attempt per backoff interval, run by an activating event after it.
632
+
633
+ When every obligation is still in its backoff, nothing runs. A worker
634
+ killed inside the ingest leaves every obligation for the next
635
+ activating event (review cycle 6)."""
636
+ try:
637
+ obligations = _read_obligations()
638
+ except OSError as exc:
639
+ return _attempt_unreadable_obligations(exc)
640
+ _clear_unreadable_backoff()
641
+ if not obligations:
642
+ return ResidualAttempt(False, "none")
643
+ now = _now()
644
+ if not any(_due(obligation, now) for obligation in obligations):
645
+ return ResidualAttempt(True, "backoff",
646
+ reason="every obligation is in its backoff")
647
+ import _cctally_db
648
+ result = None
649
+ failure = None
650
+ try:
651
+ result = _journal.run_stats_ingest(mode="authoritative")
652
+ except (Exception, _cctally_db.StatsRebuildDeferred) as exc:
653
+ failure = exc
654
+ block = getattr(result, "blocked_by", None)
655
+ uncovered = []
656
+ for obligation in obligations:
657
+ if failure is None and _residual_covered(
658
+ result, _residual_through(obligation.document)):
659
+ _discharge(obligation)
660
+ else:
661
+ uncovered.append(obligation)
662
+ if not uncovered:
663
+ return ResidualAttempt(True, "discharged")
664
+ if failure is None and block is not None and getattr(
665
+ block, "contended", False):
666
+ return ResidualAttempt(
667
+ True, "blocked", blocked_by=block,
668
+ reason=f"{block.lock} lock busy ({block.mode})")
669
+ if failure is not None:
670
+ reason = f"ingest raised {type(failure).__name__}: {failure}"
671
+ elif result is None or not isinstance(result, _journal.IngestResult):
672
+ reason = "ingest returned no result"
673
+ elif getattr(result, "error", None) is not None:
674
+ error = result.error
675
+ reason = f"ingest error {type(error).__name__}: {error}"
676
+ elif block is not None:
677
+ reason = f"{block.lock} lock failed with an I/O error"
678
+ else:
679
+ reason = "the cycle returned short of the residual inputs"
680
+ retry_at = max(_record_failure(obligation, reason, now)
681
+ for obligation in uncovered)
682
+ retry = (dt.datetime.fromtimestamp(retry_at, tz=dt.timezone.utc)
683
+ .replace(microsecond=0).isoformat().replace("+00:00", "Z"))
684
+ _cctally_core.eprint(
685
+ f"[stats] residual derivation attempt failed: {reason}; "
686
+ f"{len(uncovered)} obligation(s) kept in {residual_dir()}, "
687
+ f"retried by the first activating event after {retry}")
688
+ return ResidualAttempt(True, "failed", reason=reason)
689
+
690
+
691
+ def _attempt_unreadable_obligations(listing_error) -> ResidualAttempt:
692
+ """Fix round K11 (K9K10-R8): the obligation directory cannot be listed,
693
+ so whatever it holds is owed and unknown. When the directory's own
694
+ backoff allows, run the ONE ingest every obligation is answered by —
695
+ nothing can be discharged, because nothing was read. The attempt is
696
+ recorded (with the next, doubled, backoff) BEFORE the ingest runs, so an
697
+ attempt the ingest's own wake starts meanwhile waits it out instead of
698
+ ingesting again."""
699
+ now = _now()
700
+ reason = f"the obligation directory cannot be listed ({listing_error})"
701
+ if not _unreadable_due(now):
702
+ return ResidualAttempt(True, "backoff", reason=reason)
703
+ retry_at = _record_failure(
704
+ _Obligation(_unreadable_backoff_path(), _read_unreadable_backoff()),
705
+ reason, now)
706
+ import _cctally_db
707
+ result = None
708
+ try:
709
+ result = _journal.run_stats_ingest(mode="authoritative")
710
+ except (Exception, _cctally_db.StatsRebuildDeferred):
711
+ result = None
712
+ block = getattr(result, "blocked_by", None)
713
+ if block is not None and getattr(block, "contended", False):
714
+ return ResidualAttempt(
715
+ True, "blocked", blocked_by=block,
716
+ reason=f"{block.lock} lock busy ({block.mode})")
717
+ retry = (dt.datetime.fromtimestamp(retry_at, tz=dt.timezone.utc)
718
+ .replace(microsecond=0).isoformat().replace("+00:00", "Z"))
719
+ _cctally_core.eprint(
720
+ f"[stats] residual derivation obligations unknown: {reason}; "
721
+ f"retried by the first activating event after {retry}")
722
+ return ResidualAttempt(True, "failed", reason=reason)
723
+
724
+
725
+ def _certify(proven, entry, boundary, fallback):
726
+ """The derivation proof to publish once every admitted derivable input
727
+ through ``proven`` has its decisions journaled (fix round C, P1-1).
728
+
729
+ The output end is the journal end now. That is sound only when no
730
+ admitted decision in between derives from an input after ``proven``: the
731
+ caller holds `journal.ingest.lock`, so nothing appended since ``entry``
732
+ (the journal end when completion started) is a live decision — only this
733
+ step's own outputs, for inputs at or before ``proven``, and recorders'
734
+ inputs. Before ``entry`` a live cycle may have derived a later input, so
735
+ that stretch must hold no derivable input — or no admitted decision at
736
+ all: a decision derived from an input is journaled after it, so a stretch
737
+ holding inputs but no decision (a recorder appended after the caller's
738
+ pinned prefix, its ingest waiting on the lock this step holds) holds
739
+ nothing derived from a later input either. Withholding the proof there
740
+ left it behind the published cursor, where live ingest can never extend
741
+ it, so drop classification (which requires the proof at the cursor,
742
+ review cycle 3 P1-1) stayed unknown until some later rebuild. Otherwise
743
+ ``fallback`` (the proof completion started from, still a journal fact, or
744
+ None) stands."""
745
+ if proven is None:
746
+ return fallback
747
+ if (entry is None or _order(proven) >= _order(entry)
748
+ or not window_has_derivable_inputs(proven, entry, boundary)
749
+ or not _window_has_admitted_decisions(proven, entry, boundary)):
750
+ # The output end is the verified complete-line endpoint (revision
751
+ # 2), never the raw size: a torn suffix past it holds no output.
752
+ return _journal.DerivationFrontier(
753
+ proven, _journal.journal_safe_high_water())
754
+ return fallback
755
+
756
+
757
+ def _window_has_admitted_decisions(lo, hi, boundary) -> bool:
758
+ """Whether the journal holds an admitted decision record (an event, a
759
+ correction or a correction-batch marker) after ``lo`` and at or before
760
+ ``hi``. An excluded record is never part of the admitted state a proof
761
+ describes."""
762
+ if lo is None or hi is None or _order(hi) <= _order(lo):
763
+ return False
764
+ for segment, offset, raw in _journal.iter_range(lo, hi):
765
+ record = _lib_journal.decode_line(raw)
766
+ if (isinstance(record, dict)
767
+ and record.get("t") in ("evt", "correction", "correction_batch")
768
+ and _journal._is_admitted(record, segment, offset, boundary)):
769
+ return True
770
+ return False
771
+
772
+
773
+ def journal_origin():
774
+ """The coordinate before the first retained journal line: where
775
+ completion reconstructs from without a trusted frontier (fix round E)."""
776
+ segments = _journal.list_segments()
777
+ return (segments[0], 0) if segments else None
778
+
779
+
780
+ def _complete(context, trusted, target, boundary):
781
+ """Returns ``(proof, target)``: the derivation proof to publish (a
782
+ `DerivationFrontier` or None), and the prefix the live index must cover
783
+ (at least the original target, plus any outputs this step appended).
784
+
785
+ Without a trusted frontier the predecessor is the empty index at the
786
+ journal's origin and the window is the whole retained journal, bootstrap
787
+ facts included (fix round E, Codex fix-round-2 Q1): the cutover is no
788
+ proof that the decisions it precedes are all a later input needs — an
789
+ admitted decision for a pre-cutover input lands after it."""
790
+ entry = _journal.journal_safe_high_water()
791
+ start = trusted.frontier if trusted is not None else journal_origin()
792
+ if start is None:
793
+ return _certify(target, entry, boundary, None), target
794
+ if _order(start) >= _order(target):
795
+ # The proof already reaches past this prefix: a journal fact, kept.
796
+ return (trusted if trusted is not None
797
+ else _certify(target, entry, boundary, None)), target
798
+ if not window_has_derivable_inputs(start, target, boundary):
799
+ return _certify(target, entry, boundary, trusted), target
800
+ _cctally_core.APP_DIR.mkdir(parents=True, exist_ok=True)
801
+ with tempfile.TemporaryDirectory(
802
+ prefix="cctally-completion-", dir=str(_cctally_core.APP_DIR)) as tmp:
803
+ base = pathlib.Path(tmp) / "predecessor.db"
804
+ # #997 D9: progress for a detached worker's state file (a no-op
805
+ # unless one installed a sink).
806
+ _lib_rebuild_progress.phase("completion:predecessor")
807
+ _journal.rebuild_stats_index(
808
+ context=_journal.RebuildContext(trigger=context.trigger),
809
+ target_path=str(base), high_water=start,
810
+ update_quota_cache=False)
811
+ owed = frozenset()
812
+ if trusted is not None and trusted.output_end is not None:
813
+ owed = _apply_owed_outputs(
814
+ base, start, trusted.output_end, boundary)
815
+ for round_index in range(MAX_ROUNDS):
816
+ appended = _complete_window(
817
+ base, pathlib.Path(tmp), start, target, boundary, round_index,
818
+ owed=owed)
819
+ if not appended:
820
+ return _certify(target, entry, boundary, trusted), target
821
+ after = _journal.journal_safe_high_water()
822
+ if not window_has_derivable_inputs(target, after, boundary):
823
+ return _certify(after, entry, boundary, trusted), after
824
+ if round_index + 1 < MAX_ROUNDS:
825
+ target = after
826
+ # Review cycle 3 P1-1 (Codex fix-round-4 Q1): the rounds ran out
827
+ # while another process kept appending derivable inputs. Publish
828
+ # ONLY through the proven input endpoint: the residual inputs past it
829
+ # stay outside both applied-cursor fields and the admission and
830
+ # selector coverage, so ordinary ingest resumes there. The proof's
831
+ # output end (the journal end now) is kept separate, so the decisions
832
+ # this last round journaled beyond the residual inputs are owed, not
833
+ # discarded: ingest applies them before it derives those inputs, and
834
+ # advances the frontier with the cursor in one transaction. Advancing
835
+ # the published prefix to the journal end instead consumed the
836
+ # residual inputs without deriving them, permanently.
837
+ return _certify(target, entry, boundary, trusted), target
838
+
839
+
840
+ def _apply_owed_outputs(base, start, output_end, boundary) -> frozenset:
841
+ """Complete the predecessor at the consumed-input frontier ``start``
842
+ (fix round C, P1-1): apply the admitted decisions journaled after it, up
843
+ to its output end, which were derived from inputs at or before it.
844
+
845
+ A rebuild through the frontier alone lacks them, because the cycle that
846
+ consumed those inputs journaled its decisions beyond its captured high
847
+ water, and the window replay derives only inputs — so a later window
848
+ input would be derived against a state uninterrupted ingest never had.
849
+ The proof guarantees nothing in that stretch derives from a later input,
850
+ so these are exactly the outputs owed. Each is applied as its effective
851
+ revision through that output end, in the rebuild's fold order (structural
852
+ folds, the open 5h block projection, then milestones). Returns the ids
853
+ applied, which the predecessor then holds."""
854
+ _lib_rebuild_progress.phase("completion:owed-outputs")
855
+ prefix = _read_prefix(start, output_end, boundary)
856
+ if prefix.cutover_claude is _journal._CUTOVER_UNSEEN:
857
+ prefix.cutover_claude = _journal._resolve_cutover_for_rebuild(
858
+ _journal._CUTOVER_UNSEEN, output_end, _journal.list_segments(),
859
+ {"lines": 0, "bytes": 0, "decodes": 0}, boundary=boundary)
860
+ elif prefix.cutover_claude is None:
861
+ prefix.cutover_claude = _lib_accounts.UNATTRIBUTED
862
+ for record in prefix.slots:
863
+ if record is not None:
864
+ _journal._normalize_legacy_account_stamp(
865
+ record, prefix.cutover_claude)
866
+ selection = _lib_journal.resolve_effective_events(
867
+ prefix.slots, protocol_prefix_evidence=tuple(prefix.evidence))
868
+ owed: dict = {}
869
+ for position, record in enumerate(prefix.window):
870
+ if record.get("t") not in ("evt", "correction"):
871
+ continue
872
+ selected = selection.by_id.get(record.get("id"))
873
+ if selected is not None and selected.status == "active":
874
+ owed.setdefault(record["id"], (position, selected.record))
875
+ if not owed:
876
+ return frozenset()
877
+ stream = sorted(owed.values(), key=lambda item: (
878
+ _journal._fold_order(item[1]), item[0]))
879
+ conn = _cctally_core.open_db(_target_path=str(base))
880
+ try:
881
+ # #997 (spec D1): the predecessor is a disposable scratch too.
882
+ import _cctally_store
883
+ _cctally_store.apply_disposable_scratch_policy(conn)
884
+ for phase in ("structural", "tail"):
885
+ if phase == "tail":
886
+ import sys
887
+ backfill = getattr(sys.modules.get("cctally"),
888
+ "_backfill_five_hour_blocks", None)
889
+ if backfill is not None:
890
+ # Fix round J5: the first writer is read over the WHOLE
891
+ # prefix in journal order, never over the owed decisions
892
+ # alone, which may start after a window's first decision.
893
+ backfill(conn, only_missing=True,
894
+ journaled_first=(
895
+ _journal.journaled_block_first_observations(
896
+ prefix.slots, selection.by_id)))
897
+ conn.execute("BEGIN IMMEDIATE")
898
+ try:
899
+ for _position, record in stream:
900
+ structural = (_journal._fold_order(record)
901
+ < _journal._REBUILD_MILESTONE_ORDER)
902
+ if structural == (phase == "structural"):
903
+ _journal._apply_evt(conn, record)
904
+ conn.commit()
905
+ except BaseException:
906
+ conn.rollback()
907
+ raise
908
+ finally:
909
+ conn.close()
910
+ return frozenset(owed)
911
+
912
+
913
+ # --------------------------------------------------------------------------
914
+ # one round
915
+ # --------------------------------------------------------------------------
916
+
917
+ class _Prefix:
918
+ """One pass over the journal through the round's target."""
919
+
920
+ def __init__(self):
921
+ self.slots: list = [] # selector slots, one per decoded record
922
+ self.evidence: list = [] # protocol-resolution prefix evidence
923
+ self.window: list = [] # window records (placeholders kept)
924
+ self.coordinates: list = [] # parallel to `window`
925
+ self.excluded_revisions: dict = {}
926
+ self.inert_revisions: dict = {}
927
+ self.begin_at: dict = {}
928
+ #: #797/#823/#840 spec 4.6 B1: the begin markers per batch id, so a
929
+ #: retry of a batch an earlier writer began without the revision-2
930
+ #: stamp is authored under a derived id instead of restamped.
931
+ self.begins: dict = {}
932
+ self.batch_family: dict = {}
933
+ self.cutover_claude = _journal._CUTOVER_UNSEEN
934
+ # Admitted decision ids before the window (the predecessor holds
935
+ # them), and ids whose admitted revision-0 event a previous release
936
+ # wrote (fix round E).
937
+ self.before_ids: set = set()
938
+ self.legacy_origin_ids: set = set()
939
+
940
+
941
+ def _bump(table: dict, record) -> None:
942
+ event_id = record.get("id")
943
+ rev = record.get("rev", 0)
944
+ if isinstance(event_id, str) and type(rev) is int and rev >= 0:
945
+ table[event_id] = max(rev, table.get(event_id, rev))
946
+
947
+
948
+ def _window_omits(record) -> bool:
949
+ """#997 spec D3: whether an ADMITTED record is left out of the window —
950
+ an observation the replay provably ignores: not a rederivable raw input
951
+ and not a Claude reading, so it yields no candidate and the quarantine
952
+ fold returns before touching state (`reading_from_record` requires
953
+ `is_claude_reading`). Codex quota observations, 85 % of a production
954
+ journal, are the population. Its line still feeds the prefix hasher and
955
+ its `None` slot, and `window_has_derivable_inputs` still scans it; an
956
+ EXCLUDED observation keeps its placeholder, which the fold's exclusion
957
+ trigger needs. Every window consumer reads the same compacted window
958
+ within one `_complete_window` call, so indexing stays consistent."""
959
+ if record.get("t") != "obs":
960
+ return False
961
+ import _cctally_quarantine
962
+ import _cctally_rederive
963
+ return (not _cctally_rederive._is_rederivable_raw(record)
964
+ and not _cctally_quarantine.is_claude_reading(record))
965
+
966
+
967
+ def _read_prefix(start, target, boundary) -> _Prefix:
968
+ out = _Prefix()
969
+ segments = _journal.list_segments()
970
+ hasher = _journal._new_prefix_hasher()
971
+ prior = {"hw": None}
972
+ placeholder = _lib_account_protocol.excluded_placeholder()
973
+ start_key = _order(start)
974
+ lines = 0
975
+ for segment, offset, raw in _journal._iter_range_with_segments(
976
+ None, target, segments,
977
+ on_segment=lambda name, size: hasher.begin_segment(
978
+ name, prior["hw"], size=size),
979
+ on_bytes=hasher.extend,
980
+ ):
981
+ lines += 1
982
+ if not lines & 4095:
983
+ _lib_rebuild_progress.tick(lines=lines)
984
+ end = (segment, offset + len(raw) + 1)
985
+ record = _lib_journal.decode_line(raw)
986
+ if record is None:
987
+ prior["hw"] = end
988
+ continue
989
+ in_window = _order((segment, offset)) >= start_key
990
+ admitted = _journal._is_admitted(record, segment, offset, boundary)
991
+ t = record.get("t")
992
+ omitted = admitted and in_window and _window_omits(record)
993
+ if (in_window and not omitted) or (
994
+ admitted and t in _lib_journal_router.RETAINED_RECORD_TYPES):
995
+ # #997 D15: a record the window or the selector slots keep is
996
+ # compacted before anything holds a reference to it.
997
+ record = _lib_journal.compact_retained(record)
998
+ if not admitted:
999
+ out.slots.append(None)
1000
+ if record.get("t") in ("evt", "correction"):
1001
+ _bump(out.excluded_revisions, record)
1002
+ if in_window:
1003
+ out.window.append({**placeholder, "quarantine": {
1004
+ "record": record, "segment": segment, "offset": offset}})
1005
+ out.coordinates.append((segment, offset))
1006
+ prior["hw"] = end
1007
+ continue
1008
+ if t == "op":
1009
+ _journal._capture_protocol_prefix_evidence(
1010
+ record, prior["hw"], out.evidence, hasher=hasher)
1011
+ if (out.cutover_claude is _journal._CUTOVER_UNSEEN
1012
+ and record.get("id") == _journal.CUTOVER_OP_ID):
1013
+ out.cutover_claude = _journal._cutover_value_of(record)
1014
+ if t in ("evt", "correction") and isinstance(record.get("id"), str):
1015
+ if not in_window:
1016
+ out.before_ids.add(record["id"])
1017
+ if (t == "evt" and record.get("rev", 0) == 0
1018
+ and not _lib_account_protocol.is_stamped(record)):
1019
+ out.legacy_origin_ids.add(record["id"])
1020
+ if t == "correction_batch" and record.get("phase") == "begin":
1021
+ out.begin_at.setdefault(record.get("id"), record.get("at"))
1022
+ out.begins.setdefault(record.get("id"), []).append(record)
1023
+ out.batch_family.setdefault(record.get("id"), record.get("family"))
1024
+ out.slots.append(
1025
+ record if t in _lib_journal_router.RETAINED_RECORD_TYPES else None)
1026
+ if in_window and not omitted:
1027
+ out.window.append(record)
1028
+ out.coordinates.append((segment, offset))
1029
+ prior["hw"] = end
1030
+ return out
1031
+
1032
+
1033
+ def _held_observation_ids(slots) -> frozenset:
1034
+ """The observations a retained reviewed weekly decision holds (latest
1035
+ decision per observation wins, as the rederive planner reads them)."""
1036
+ import _cctally_rederive
1037
+ effective: dict = {}
1038
+ for record in slots:
1039
+ if (not isinstance(record, dict) or record.get("t") != "op"
1040
+ or (record.get("payload") or {}).get("kind")
1041
+ not in _cctally_rederive._REVIEWED_WEEKLY_KINDS):
1042
+ continue
1043
+ payload = record["payload"]
1044
+ decisions = (payload.get("decisions")
1045
+ if payload.get("schema_version") == 1
1046
+ else payload.get("weekly_axis_decisions")) or []
1047
+ for item in decisions:
1048
+ if isinstance(item, dict) and isinstance(
1049
+ item.get("observationId"), str):
1050
+ effective[item["observationId"]] = item.get("disposition")
1051
+ return frozenset(obs for obs, disposition in effective.items()
1052
+ if disposition == "hold")
1053
+
1054
+
1055
+ @contextlib.contextmanager
1056
+ def _cache_read_view():
1057
+ """A stable read-only view of cache.db for the replay's cost reads, or
1058
+ None when there is no readable cache. Takes no cache flock (the caller
1059
+ holds the stats locks); a read transaction pins one snapshot for both
1060
+ replays, so the causal difference never reflects a cache change."""
1061
+ conn = None
1062
+ path = _cctally_core.CACHE_DB_PATH
1063
+ if pathlib.Path(path).exists():
1064
+ try:
1065
+ conn = sqlite3.connect(f"file:{path}?mode=ro", uri=True,
1066
+ timeout=5.0)
1067
+ conn.execute("BEGIN")
1068
+ conn.execute("SELECT 1 FROM session_entries LIMIT 1").fetchall()
1069
+ except sqlite3.Error:
1070
+ if conn is not None:
1071
+ conn.close()
1072
+ conn = None
1073
+ try:
1074
+ yield conn
1075
+ finally:
1076
+ if conn is not None:
1077
+ conn.close()
1078
+
1079
+
1080
+ @contextlib.contextmanager
1081
+ def _no_cache_read_adapters():
1082
+ """The replay's readers with no cache: no retained usage entries and no
1083
+ alert configuration — the same for both replays, so the causal
1084
+ difference is unaffected; only cost facts go unpriced."""
1085
+ import sys
1086
+ cctally = sys.modules["cctally"]
1087
+ replacements = {
1088
+ "get_entries": lambda *args, **kwargs: [],
1089
+ "get_claude_session_entries": lambda *args, **kwargs: [],
1090
+ "load_config": lambda *args, **kwargs: {},
1091
+ }
1092
+ prior = {name: getattr(cctally, name) for name in replacements}
1093
+ try:
1094
+ for name, value in replacements.items():
1095
+ setattr(cctally, name, value)
1096
+ yield
1097
+ finally:
1098
+ for name, value in prior.items():
1099
+ setattr(cctally, name, value)
1100
+
1101
+
1102
+ def _normalized_copy(record, cutover_claude):
1103
+ """One window record as a replay sees it: a private deep copy, its legacy
1104
+ account stamp normalized under ``cutover_claude`` (an excluded placeholder
1105
+ is only copied). Reads only this record and the journal-wide cutover
1106
+ value (#997 spec D5)."""
1107
+ out = copy.deepcopy(record)
1108
+ if out.get("t") != _lib_account_protocol.EXCLUDED_PLACEHOLDER_TYPE:
1109
+ _journal._normalize_legacy_account_stamp(out, cutover_claude)
1110
+ return out
1111
+
1112
+
1113
+ def _replay(base, tmp, name, prefix, held_ids, cache_conn, *, retention,
1114
+ skip=frozenset()):
1115
+ """One scratch replay of the window from the predecessor state, the
1116
+ projection kept on the retained decisions by ``retention``. Returns
1117
+ ``(events, by_input)``: every derived candidate, and per raw input index
1118
+ its own candidates."""
1119
+ import _cctally_quarantine
1120
+ import _cctally_rederive
1121
+ copy_path = tmp / name
1122
+ shutil.copyfile(base, copy_path)
1123
+ cutover_claude = prefix.cutover_claude
1124
+ by_input: dict = {}
1125
+ conn = _cctally_core.open_db(_target_path=str(copy_path))
1126
+ try:
1127
+ # #997 (spec D1): a disposable replay copy, never published.
1128
+ import _cctally_store
1129
+ _cctally_store.apply_disposable_scratch_policy(conn)
1130
+ fold = _cctally_quarantine.QuarantineFold.load(conn, held_ids=held_ids)
1131
+ # #997 spec D5: each record is copied and normalized as it is
1132
+ # offered, never the whole window up front.
1133
+ events = _cctally_rederive.replay_scratch_records(
1134
+ conn, prefix.window, cache_conn, fold=fold, held_ids=held_ids,
1135
+ prepare=lambda record: _normalized_copy(record, cutover_claude),
1136
+ coordinates=prefix.coordinates, skip_indexes=skip,
1137
+ on_input=lambda index, derived: by_input.__setitem__(
1138
+ index, list(derived)),
1139
+ read_adapters=(None if cache_conn is not None
1140
+ else _no_cache_read_adapters()),
1141
+ retention=retention)
1142
+ finally:
1143
+ conn.close()
1144
+ return events, by_input
1145
+
1146
+
1147
+ def _owned_by_id(events) -> dict:
1148
+ """First candidate per owned id, in derivation order (a later divergent
1149
+ re-derivation of one id is what live ingest withholds as a conflict)."""
1150
+ out: dict = {}
1151
+ for event in events:
1152
+ if _lib_rederive._is_owned_event(event):
1153
+ out.setdefault(event["id"], event)
1154
+ return out
1155
+
1156
+
1157
+ def _effective_records(selection) -> dict:
1158
+ """Every selected id's effective record (None for a tombstone)."""
1159
+ return {event_id: (item.record if item.status == "active" else None)
1160
+ for event_id, item in selection.by_id.items()
1161
+ if item.status in ("active", "tombstone")}
1162
+
1163
+
1164
+ def _retention(effective, prefix, owed, *, legacy, reference=None):
1165
+ retention = _journal.ScratchRetention(
1166
+ effective, reference=reference, retained_only=legacy)
1167
+ retention.seed(prefix.before_ids)
1168
+ retention.seed(owed)
1169
+ return retention
1170
+
1171
+
1172
+ #: The snapshot decision id the live pipeline mints for a Claude usage
1173
+ #: observation (`_cctally_record._pipeline_claude_usage`).
1174
+ _SNAPSHOT_ID_PREFIX = "sa:"
1175
+
1176
+
1177
+ def _is_legacy_input(record) -> bool:
1178
+ """Whether a window input was itself written by a previous release:
1179
+ unstamped, or stamped by a writer generation other than this one."""
1180
+ return (isinstance(record, dict) and record.get("t") == "obs"
1181
+ and not _lib_account_protocol.is_stamped(record))
1182
+
1183
+
1184
+ def _legacy_decided_inputs(prefix, by_input=None) -> frozenset:
1185
+ """The window inputs a previous release decided (fix round E): a LEGACY
1186
+ input (`_is_legacy_input`) one of whose decisions is an admitted
1187
+ UNSTAMPED revision-0 event. Its history is exactly what that release
1188
+ journaled, which the current pipeline may spell or extend differently,
1189
+ so it is never a gap and its extra fresh candidates are never
1190
+ materialized. Before a replay only the snapshot linkage is known;
1191
+ ``by_input`` adds every derived id.
1192
+
1193
+ Both conditions are required (revision 2, Codex design-fix-round-3 Q6,
1194
+ P1-1). Legacy provenance alone never exempts a missing decision, and a
1195
+ CURRENT-stamped input never becomes history because one of its derived
1196
+ candidates collides with a retained legacy event identity: a same-second
1197
+ current reading re-emits the retained closure of a five-hour block a
1198
+ previous-release reading closed at that second, and treating that retry
1199
+ as the reading's own history left the reading's decision unjournaled."""
1200
+ legacy = prefix.legacy_origin_ids
1201
+ out = {index for index, record in enumerate(prefix.window)
1202
+ if _is_legacy_input(record)
1203
+ and _SNAPSHOT_ID_PREFIX + str(record.get("id")) in legacy}
1204
+ for index, derived in (by_input or {}).items():
1205
+ if not (0 <= index < len(prefix.window)
1206
+ and _is_legacy_input(prefix.window[index])):
1207
+ continue
1208
+ if any(event["id"] in legacy for event in derived
1209
+ if _lib_rederive._is_owned_event(event)):
1210
+ out.add(index)
1211
+ return frozenset(out)
1212
+
1213
+
1214
+ def _gap_inputs(by_input, selection, legacy) -> frozenset:
1215
+ """The inputs with a derived decision no admitted decision answers,
1216
+ previous-release inputs (``legacy``) excepted. Only the with-gap /
1217
+ without-gap comparison, never an absent id alone, says what a gap
1218
+ owes."""
1219
+ return frozenset(
1220
+ index for index, derived in by_input.items()
1221
+ if index not in legacy
1222
+ and any(_lib_rederive._is_owned_event(event)
1223
+ and event["id"] not in selection.by_id
1224
+ for event in derived))
1225
+
1226
+
1227
+ def _complete_window(base, tmp, start, target, boundary, round_index, *,
1228
+ owed=frozenset()) -> bool:
1229
+ """Plan and journal one round. Returns whether anything was appended.
1230
+
1231
+ Three replays from the predecessor, each keeping the projection on the
1232
+ retained decisions (`_journal.ScratchRetention`): one over every input
1233
+ finds the gap; one without the gap is the reference; one with the gap
1234
+ holds the fresh candidate only where it differs from the reference, so a
1235
+ gap's effect reaches its descendants while every unaffected decision
1236
+ stays the retained one. Their difference is the causal suffix."""
1237
+ _lib_rebuild_progress.phase("completion:read", round=round_index)
1238
+ prefix = _read_prefix(start, target, boundary)
1239
+ if prefix.cutover_claude is _journal._CUTOVER_UNSEEN:
1240
+ prefix.cutover_claude = _journal._resolve_cutover_for_rebuild(
1241
+ _journal._CUTOVER_UNSEEN, target, _journal.list_segments(),
1242
+ {"lines": 0, "bytes": 0, "decodes": 0}, boundary=boundary)
1243
+ elif prefix.cutover_claude is None:
1244
+ prefix.cutover_claude = _lib_accounts.UNATTRIBUTED
1245
+ for record in prefix.slots:
1246
+ if record is not None:
1247
+ _journal._normalize_legacy_account_stamp(
1248
+ record, prefix.cutover_claude)
1249
+ selection = _lib_journal.resolve_effective_events(
1250
+ prefix.slots, protocol_prefix_evidence=tuple(prefix.evidence))
1251
+ # Inert (incomplete or tainted) batch actions of OTHER families still
1252
+ # occupy their revision: a later retry of that batch must not collide.
1253
+ for record in prefix.slots:
1254
+ if (isinstance(record, dict) and record.get("t") == "correction"
1255
+ and record.get("batch") not in selection.completed_batches
1256
+ and prefix.batch_family.get(record.get("batch")) != FAMILY):
1257
+ _bump(prefix.inert_revisions, record)
1258
+ held_ids = _held_observation_ids(prefix.slots)
1259
+ effective = _effective_records(selection)
1260
+
1261
+ legacy = _legacy_decided_inputs(prefix)
1262
+
1263
+ with _cache_read_view() as cache_conn:
1264
+ _lib_rebuild_progress.phase("completion:replay-all", round=round_index)
1265
+ _events, by_input = _replay(
1266
+ base, tmp, f"all-{round_index}.db", prefix, held_ids, cache_conn,
1267
+ retention=_retention(effective, prefix, owed, legacy=legacy))
1268
+ legacy = _legacy_decided_inputs(prefix, by_input)
1269
+ gap = _gap_inputs(by_input, selection, legacy)
1270
+ if not gap:
1271
+ return False
1272
+ _lib_rebuild_progress.phase(
1273
+ "completion:replay-without", round=round_index)
1274
+ events_a, _ = _replay(
1275
+ base, tmp, f"without-{round_index}.db", prefix, held_ids,
1276
+ cache_conn, skip=gap,
1277
+ retention=_retention(effective, prefix, owed, legacy=legacy))
1278
+ reference: dict = {}
1279
+ for event in events_a:
1280
+ reference.setdefault(
1281
+ event["id"], _lib_journal.event_content_hash(event))
1282
+ _lib_rebuild_progress.phase(
1283
+ "completion:replay-with", round=round_index)
1284
+ events_b, _ = _replay(
1285
+ base, tmp, f"with-{round_index}.db", prefix, held_ids, cache_conn,
1286
+ retention=_retention(effective, prefix, owed, legacy=legacy,
1287
+ reference=reference))
1288
+ with_gap = _owned_by_id(events_b)
1289
+ without_gap = _owned_by_id(events_a)
1290
+
1291
+ plain: list = []
1292
+ actions: list = []
1293
+ for event_id in sorted(set(with_gap) | set(without_gap)):
1294
+ desired = with_gap.get(event_id)
1295
+ reference = without_gap.get(event_id)
1296
+ if (desired is not None and reference is not None
1297
+ and _lib_rederive._semantic_event(desired)
1298
+ == _lib_rederive._semantic_event(reference)):
1299
+ # Not caused by the gap: an unrelated difference against the
1300
+ # journal (a repricing, a previous release's input the current
1301
+ # pipeline extends) is never acted on.
1302
+ continue
1303
+ selected = selection.by_id.get(event_id)
1304
+ current = (selected.record if selected is not None
1305
+ and selected.status == "active" else None)
1306
+ physical = [rev for rev in (
1307
+ prefix.excluded_revisions.get(event_id),
1308
+ prefix.inert_revisions.get(event_id),
1309
+ None if selected is None else selected.rev,
1310
+ ) if rev is not None]
1311
+ if desired is None:
1312
+ if current is not None:
1313
+ actions.append({"action": "tombstone", "id": event_id,
1314
+ "rev": max(physical) + 1,
1315
+ "at": str(current["at"]), "payload": None})
1316
+ continue
1317
+ if current is not None:
1318
+ desired = _lib_rederive._preserve_non_derivable_state(
1319
+ current, desired)
1320
+ if (_lib_rederive._semantic_event(current)
1321
+ == _lib_rederive._semantic_event(desired)):
1322
+ continue # an admitted identical decision: nothing
1323
+ if not physical:
1324
+ plain.append(desired)
1325
+ continue
1326
+ actions.append({"action": "replace", "id": event_id,
1327
+ "rev": max(physical) + 1,
1328
+ "at": str(desired["at"]),
1329
+ "payload": dict(desired.get("payload") or {})})
1330
+
1331
+ records: list = []
1332
+ if actions:
1333
+ digest = hashlib.sha256(json.dumps(
1334
+ actions, sort_keys=True, separators=(",", ":"),
1335
+ ensure_ascii=False).encode("utf-8")).hexdigest()
1336
+ batch_id = f"{FAMILY}:{digest}"
1337
+ if batch_id not in selection.completed_batches:
1338
+ batch_id = _lib_journal.revision_batch_id(
1339
+ batch_id, prefix.begins.get(batch_id, ()))
1340
+ if batch_id not in selection.completed_batches:
1341
+ records.extend(_lib_journal.make_correction_batch(
1342
+ batch_id=batch_id, family=FAMILY,
1343
+ # An interrupted earlier attempt's `at`, so the retry's lines
1344
+ # are byte-identical and the selector keeps each once.
1345
+ at=str(prefix.begin_at.get(batch_id) or _iso_now()),
1346
+ actions=actions))
1347
+ order = {id(event): index for index, event in enumerate(events_b)}
1348
+ plain.sort(key=lambda event: order.get(id(event), 0))
1349
+ records.extend(plain)
1350
+ if not records:
1351
+ return False
1352
+ _lib_rebuild_progress.phase(
1353
+ "completion:append", round=round_index, records=len(records))
1354
+ _journal.append_records(records)
1355
+ return True