cctally 1.113.0 → 1.115.0

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