cctally 1.114.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 (64) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/bin/_cctally_account.py +32 -8
  3. package/bin/_cctally_cache.py +4394 -279
  4. package/bin/_cctally_completion.py +1287 -0
  5. package/bin/_cctally_config.py +37 -0
  6. package/bin/_cctally_core.py +178 -2
  7. package/bin/_cctally_dashboard.py +2 -2
  8. package/bin/_cctally_dashboard_conversation.py +292 -23
  9. package/bin/_cctally_dashboard_envelope.py +16 -3
  10. package/bin/_cctally_dashboard_sources.py +18 -9
  11. package/bin/_cctally_db.py +316 -5
  12. package/bin/_cctally_doctor.py +436 -27
  13. package/bin/_cctally_five_hour.py +82 -31
  14. package/bin/_cctally_forecast.py +81 -9
  15. package/bin/_cctally_journal.py +3884 -112
  16. package/bin/_cctally_journal_repair.py +101 -12
  17. package/bin/_cctally_parser.py +42 -0
  18. package/bin/_cctally_project.py +8 -2
  19. package/bin/_cctally_quarantine.py +652 -0
  20. package/bin/_cctally_quota.py +26 -4
  21. package/bin/_cctally_quota_model.py +360 -83
  22. package/bin/_cctally_record.py +677 -149
  23. package/bin/_cctally_rederive.py +297 -32
  24. package/bin/_cctally_refresh.py +84 -13
  25. package/bin/_cctally_source_analytics.py +23 -2
  26. package/bin/_cctally_statusline.py +6137 -517
  27. package/bin/_cctally_store.py +287 -48
  28. package/bin/_cctally_transcript.py +8 -7
  29. package/bin/_cctally_tui.py +57 -53
  30. package/bin/_lib_account_binding.py +152 -0
  31. package/bin/_lib_account_protocol.py +386 -0
  32. package/bin/_lib_aggregators.py +4 -0
  33. package/bin/_lib_cache_coverage.py +13 -1
  34. package/bin/_lib_cache_writer_lock.py +19 -3
  35. package/bin/_lib_clone_copy.py +142 -0
  36. package/bin/_lib_codex_conversation_query.py +133 -19
  37. package/bin/_lib_codex_conversation_watch.py +59 -14
  38. package/bin/_lib_codex_drain_admission.py +214 -0
  39. package/bin/_lib_codex_metadata.py +18 -1
  40. package/bin/_lib_codex_publication_barrier.py +277 -0
  41. package/bin/_lib_codex_rollout_container.py +502 -0
  42. package/bin/_lib_conversation_dispatch.py +3 -0
  43. package/bin/_lib_conversation_query.py +11 -3
  44. package/bin/_lib_detector_access_registry.py +3817 -0
  45. package/bin/_lib_doctor.py +452 -1
  46. package/bin/_lib_five_hour.py +126 -0
  47. package/bin/_lib_ingest_frontier.py +414 -20
  48. package/bin/_lib_journal.py +141 -10
  49. package/bin/_lib_jsonl.py +5 -4
  50. package/bin/_lib_legacy_spool.py +126 -0
  51. package/bin/_lib_legacy_writer_evidence.py +136 -0
  52. package/bin/_lib_merge_gate.py +29 -302
  53. package/bin/_lib_meter_rate_change.py +10 -0
  54. package/bin/_lib_outcome_quarantine.py +690 -0
  55. package/bin/_lib_pricing.py +7 -2
  56. package/bin/_lib_quarantine.py +154 -0
  57. package/bin/_lib_quota_calibration.py +53 -8
  58. package/bin/_lib_quota_copy.py +2 -1
  59. package/bin/_lib_quota_model.py +288 -46
  60. package/bin/_lib_rederive.py +94 -5
  61. package/bin/_lib_segment_summary.py +35 -2
  62. package/bin/_lib_statusline_candidates.py +292 -23
  63. package/bin/cctally +23 -1
  64. package/package.json +14 -1
@@ -24,6 +24,9 @@ CONTROL_SCHEMA_VERSION = 2
24
24
  #: Control documents this binary can read. Version 1 predates the pending
25
25
  #: drop's deadline and retained evidence; see `_pending_drop`.
26
26
  CONTROL_SCHEMA_VERSIONS_READ = (1, 2)
27
+ #: #825/#832: the account-bound protocol generation. Every current document,
28
+ #: retry namespace and plan names it; the legacy spool never carries it.
29
+ PROTOCOL_GENERATION = "account-bound-v1"
27
30
  #: D5: a non-extendable wall clock measured from the first pending drop.
28
31
  PENDING_DROP_DEADLINE_SECONDS = 180
29
32
  CANDIDATE_DOCUMENT_MAX_BYTES = 4 * 1024
@@ -53,6 +56,9 @@ class Candidate:
53
56
  received_at: int = 0
54
57
  five_hour: AxisValue | None = None
55
58
  seven_day: AxisValue | None = None
59
+ #: The immutable `AccountBinding` captured with this candidate (#825/#832
60
+ #: spec 4.2). The reducer partitions by its account; it is never inferred.
61
+ binding: object | None = None
56
62
 
57
63
 
58
64
  @dataclasses.dataclass(frozen=True)
@@ -62,7 +68,11 @@ class AxisProjection:
62
68
  canonical_key: int
63
69
  captured_at: int
64
70
  source: str
65
- reset_generation: int
71
+ #: The reset generation the projected value belongs to. Weekly: the
72
+ #: reset-aware floor epoch (an int). Five-hour (#832): the LOGICAL
73
+ #: generation string of the exact account's active reset event, or "" for
74
+ #: no event — never a SQLite rowid, so a rebuild cannot change it.
75
+ reset_generation: int | str
66
76
 
67
77
 
68
78
  @dataclasses.dataclass(frozen=True)
@@ -73,11 +83,79 @@ class FileFingerprint:
73
83
  mtime_ns: int
74
84
 
75
85
 
86
+ #: The classification evidence of one projected axis (#825/#832 spec 4.3
87
+ #: revision 5, Codex design-fix-round-5 Q1). ``present``: the axis holds a
88
+ #: baseline. ``expired_predecessor``: every admitted five-hour predecessor
89
+ #: left the projection only through expiry at the evaluation's instant (on
90
+ #: the weekly axis: every retained weekly row lies in a week that ends at or
91
+ #: before ``predecessor_key``, fix round K14c). ``proven_empty``: the covered
92
+ #: snapshot proves the account has no admitted baseline on the axis.
93
+ #: ``floor_retired`` (revision 6, Codex design-fix-round-6 Q2): the axis's
94
+ #: group has an effective, admitted floor of the account and no accepted
95
+ #: observation of the axis survives at or after it. ``unknown``: anything
96
+ #: else — never inferred from an absent value.
97
+ EVIDENCE_PRESENT = "present"
98
+ EVIDENCE_EXPIRED_PREDECESSOR = "expired_predecessor"
99
+ EVIDENCE_PROVEN_EMPTY = "proven_empty"
100
+ EVIDENCE_FLOOR_RETIRED = "floor_retired"
101
+ EVIDENCE_UNKNOWN = "unknown"
102
+
103
+
104
+ @dataclasses.dataclass(frozen=True)
105
+ class AxisEvidence:
106
+ """How an axis of a projection was classified, computed inside the same
107
+ validated, prefix-covered snapshot as the projection itself. An
108
+ expired predecessor keeps the greatest expired window's account-canonical
109
+ key and raw reset: the evidence a later reading's window is compared
110
+ with, never a value to display or to fill an axis with."""
111
+
112
+ kind: str
113
+ predecessor_key: int | None = None
114
+ predecessor_raw: int | None = None
115
+ #: Fix round K12 (K11R-10): for an unknown axis that wall time alone can
116
+ #: classify — a row beyond the future band — the earliest instant at
117
+ #: which such a row becomes admissible; else None.
118
+ admissible_at: int | None = None
119
+ #: ``floor_retired`` only (spec 4.3 revision 6): the account, the axis,
120
+ #: the retired group's account-canonical key, the floor instant, the
121
+ #: floor's logical identity and the group's anchor raw reset. Never a
122
+ #: retired percentage.
123
+ account_key: str | None = None
124
+ axis: str | None = None
125
+ group_key: int | None = None
126
+ floor_epoch: int | None = None
127
+ floor_identity: str | None = None
128
+ anchor_raw: int | None = None
129
+
130
+
131
+ EVIDENCE_UNKNOWN_AXIS = AxisEvidence(EVIDENCE_UNKNOWN)
132
+
133
+ #: How `classify_absent_axis` classifies a reading on an axis that holds no
134
+ #: projected baseline. ``publish``: a first reading or a new window, under
135
+ #: either drop gate. ``stale``: an older window, or a reading proven captured
136
+ #: before the floor of a retired group — the older-window disposition, which
137
+ #: publishes nothing on the axis and opens no consensus. ``floor``: a reading
138
+ #: in a floor-retired group, which publishes only from contributors captured
139
+ #: at or after the floor. ``unknown``: unclassifiable; the caller defers the
140
+ #: whole evaluation.
141
+ CLASS_PUBLISH = "publish"
142
+ CLASS_STALE = "stale"
143
+ CLASS_FLOOR = "floor"
144
+ CLASS_UNKNOWN = "unknown"
145
+
146
+
76
147
  @dataclasses.dataclass(frozen=True)
77
148
  class DbProjection:
78
149
  five_hour: AxisProjection | None
79
150
  seven_day: AxisProjection | None
80
151
  db_files: Mapping[str, FileFingerprint | None] | None = None
152
+ #: The exact account this projection was read for (#825/#832 spec 4.5).
153
+ account_key: str | None = None
154
+ #: Per-axis `AxisEvidence` (spec 4.3 revision 5), keyed by axis name. Only
155
+ #: the covered projection read carries it; a projection without it — a
156
+ #: persisted control document, a deferred read — classifies an absent
157
+ #: axis as unknown.
158
+ evidence: Mapping[str, AxisEvidence] | None = None
81
159
 
82
160
 
83
161
  @dataclasses.dataclass(frozen=True)
@@ -92,7 +170,12 @@ class RetrySignature:
92
170
  candidate_percent: float
93
171
  db_key: int | None
94
172
  db_percent: float | None
95
- db_reset_generation: int | None
173
+ db_reset_generation: int | str | None
174
+ #: The retry namespace (#825/#832 spec 4.3): protocol generation, account
175
+ #: and axis. Window, value and logical generation are the fields above.
176
+ protocol_generation: str | None = None
177
+ account_key: str | None = None
178
+ axis: str | None = None
96
179
 
97
180
 
98
181
  @dataclasses.dataclass(frozen=True)
@@ -174,6 +257,12 @@ class ReductionDecision:
174
257
  action: str
175
258
  control: ControlState
176
259
  plan: PublicationPlan | None = None
260
+ #: Fix round K13 (review cycle 7 finding P1-2, spec 4.3 *Classification
261
+ #: evidence*): the axes on which a reading met no projected baseline and
262
+ #: no classification evidence. Neither drop gate publishes such an axis;
263
+ #: the caller defers the whole evaluation instead of reading the NOOP
264
+ #: on it as an unchanged reading.
265
+ unknown_axes: frozenset = frozenset()
177
266
 
178
267
 
179
268
  def _is_int(value: object) -> bool:
@@ -318,18 +407,32 @@ def _axis_projection(value: object) -> AxisProjection | None:
318
407
  for key in ("rawResetsAt", "canonicalKey", "capturedAt"):
319
408
  if not _is_int(doc[key]):
320
409
  raise StateValidationError(f"{key} must be an integer")
321
- if not _is_nonnegative_int(doc["resetGeneration"]):
322
- raise StateValidationError("resetGeneration must be nonnegative")
410
+ generation = _reset_generation(doc["resetGeneration"], "resetGeneration")
323
411
  return AxisProjection(
324
412
  percent=_percent(doc["percent"]),
325
413
  raw_resets_at=doc["rawResetsAt"],
326
414
  canonical_key=doc["canonicalKey"],
327
415
  captured_at=doc["capturedAt"],
328
416
  source=_require_ascii_source(doc["source"]),
329
- reset_generation=doc["resetGeneration"],
417
+ reset_generation=generation,
330
418
  )
331
419
 
332
420
 
421
+ #: A logical five-hour generation is `<effective>|<detected>|<journal id>`.
422
+ _RESET_GENERATION_MAX_CHARS = 512
423
+
424
+
425
+ def _reset_generation(value: object, label: str) -> int | str:
426
+ """A weekly floor epoch (nonnegative int) or a five-hour logical
427
+ generation (bounded ASCII string; "" means no event)."""
428
+ if _is_nonnegative_int(value):
429
+ return value
430
+ if (isinstance(value, str) and len(value) <= _RESET_GENERATION_MAX_CHARS
431
+ and value.isascii()):
432
+ return value
433
+ raise StateValidationError(f"{label} must be a nonnegative int or a logical generation")
434
+
435
+
333
436
  def _fingerprint(value: object) -> FileFingerprint | None:
334
437
  if value is None:
335
438
  return None
@@ -360,21 +463,30 @@ def _retry_signature(value: object) -> RetrySignature | None:
360
463
  if value is None:
361
464
  return None
362
465
  doc = _require_object(value, "retrySignature")
363
- _require_exact_keys(
364
- doc,
365
- {"candidateKey", "candidatePercent", "dbKey", "dbPercent", "dbResetGeneration"},
366
- )
466
+ base = {"candidateKey", "candidatePercent", "dbKey", "dbPercent", "dbResetGeneration"}
467
+ scoped = base | {"protocolGeneration", "accountKey", "axis"}
468
+ if set(doc) not in (base, scoped):
469
+ raise StateValidationError("unexpected retrySignature keys")
367
470
  if not _is_int(doc["candidateKey"]):
368
471
  raise StateValidationError("candidateKey must be an integer")
369
472
  db_generation = doc["dbResetGeneration"]
370
- if db_generation is not None and not _is_nonnegative_int(db_generation):
371
- raise StateValidationError("dbResetGeneration must be nonnegative or null")
473
+ if db_generation is not None:
474
+ db_generation = _reset_generation(db_generation, "dbResetGeneration")
475
+ scope = (None, None, None)
476
+ if set(doc) == scoped:
477
+ scope = (doc["protocolGeneration"], doc["accountKey"], doc["axis"])
478
+ if (not isinstance(scope[0], str) or not isinstance(scope[1], str)
479
+ or not scope[1] or scope[2] not in AXES):
480
+ raise StateValidationError("invalid retry namespace")
372
481
  return RetrySignature(
373
482
  candidate_key=doc["candidateKey"],
374
483
  candidate_percent=_percent(doc["candidatePercent"]),
375
484
  db_key=_nullable_int(doc["dbKey"], "dbKey"),
376
485
  db_percent=_nullable_percent(doc["dbPercent"], "dbPercent"),
377
486
  db_reset_generation=db_generation,
487
+ protocol_generation=scope[0],
488
+ account_key=scope[1],
489
+ axis=scope[2],
378
490
  )
379
491
 
380
492
 
@@ -611,6 +723,9 @@ class _AxisDecision:
611
723
  action: str
612
724
  candidate: AxisValue | None
613
725
  pending: PendingDrop | None
726
+ #: A reading on an axis with no baseline and no classification evidence
727
+ #: (fix round K13): never published, whatever the drop gate.
728
+ unknown: bool = False
614
729
 
615
730
 
616
731
  def _candidate_axis(candidate: Candidate, axis: str) -> AxisValue | None:
@@ -621,6 +736,62 @@ def _projection_axis(projection: DbProjection, axis: str) -> AxisProjection | No
621
736
  return projection.five_hour if axis == "fiveHour" else projection.seven_day
622
737
 
623
738
 
739
+ def axis_evidence(projection: DbProjection, axis: str) -> AxisEvidence:
740
+ """The classification evidence of ``axis``: ``present`` whenever the
741
+ axis holds a baseline, else what the covered read recorded, else
742
+ unknown (an absent value alone proves nothing)."""
743
+ if _projection_axis(projection, axis) is not None:
744
+ return AxisEvidence(EVIDENCE_PRESENT)
745
+ recorded = (projection.evidence or {}).get(axis)
746
+ return recorded if isinstance(recorded, AxisEvidence) else EVIDENCE_UNKNOWN_AXIS
747
+
748
+
749
+ def classify_absent_axis(evidence: AxisEvidence | None, key: int, *,
750
+ axis: str) -> str:
751
+ """How a reading in window ``key`` on ``axis``, which holds no projected
752
+ baseline, is classified (spec 4.3 revisions 5 and 6): one of the
753
+ ``CLASS_*`` answers.
754
+
755
+ - Proven empty, or a predecessor whose window ``key`` strictly follows:
756
+ a first reading or a new window, so it publishes under a closed drop
757
+ gate too.
758
+ - An expired five-hour predecessor whose key ``key`` does not strictly
759
+ exceed (revision 6, *Stale expired window*, Codex design-fix-round-6
760
+ Q3): stale. On the weekly axis the predecessor's key is only a bound,
761
+ so such a reading stays unknown.
762
+ - A floor-retired group (revision 6, *Floor retired*, Q2): a strictly
763
+ newer group takes the new-window rule, an older group is stale, and a
764
+ reading in the group itself depends on its contributors' capture
765
+ instants (``CLASS_FLOOR``).
766
+ - Anything else is unknown classification."""
767
+ if evidence is None:
768
+ return CLASS_UNKNOWN
769
+ if evidence.kind == EVIDENCE_PROVEN_EMPTY:
770
+ return CLASS_PUBLISH
771
+ if (evidence.kind == EVIDENCE_EXPIRED_PREDECESSOR
772
+ and evidence.predecessor_key is not None):
773
+ if int(key) > int(evidence.predecessor_key):
774
+ return CLASS_PUBLISH
775
+ return CLASS_STALE if axis == "fiveHour" else CLASS_UNKNOWN
776
+ if (evidence.kind == EVIDENCE_FLOOR_RETIRED
777
+ and evidence.group_key is not None
778
+ and evidence.floor_epoch is not None):
779
+ if int(key) > int(evidence.group_key):
780
+ return CLASS_PUBLISH
781
+ if int(key) < int(evidence.group_key):
782
+ return CLASS_STALE
783
+ return CLASS_FLOOR
784
+ return CLASS_UNKNOWN
785
+
786
+
787
+ def classifies_first_reading(evidence: AxisEvidence | None, key: int) -> bool:
788
+ """Whether a reading in window ``key`` on an axis with no projected
789
+ baseline is a first reading or a new window whatever its contributors
790
+ (`classify_absent_axis` answers ``publish``). Such a reading is never a
791
+ drop, so it publishes under a closed drop gate too."""
792
+ return classify_absent_axis(evidence, key, axis="sevenDay") == CLASS_PUBLISH
793
+
794
+
624
795
  def _axis_value_key(value: AxisValue) -> int:
625
796
  return value.canonical_key if value.canonical_key is not None else value.raw_resets_at
626
797
 
@@ -793,13 +964,22 @@ def _axis_control_changed(before: PendingDrop | None, after: PendingDrop | None)
793
964
  return before != after
794
965
 
795
966
 
796
- def _build_retry_signature(reduced: AxisValue, db_axis: AxisProjection | None) -> RetrySignature:
967
+ def _build_retry_signature(
968
+ reduced: AxisValue, db_axis: AxisProjection | None, *, scope=None,
969
+ ) -> RetrySignature:
970
+ """``scope`` is ``(protocol_generation, account_key, axis)`` for a bound
971
+ partition (#825/#832 spec 4.3), so two accounts never share a spent
972
+ attempt even with identical values."""
973
+ protocol, account, axis = scope if scope is not None else (None, None, None)
797
974
  return RetrySignature(
798
975
  candidate_key=_axis_value_key(reduced),
799
976
  candidate_percent=reduced.percent,
800
977
  db_key=None if db_axis is None else db_axis.canonical_key,
801
978
  db_percent=None if db_axis is None else db_axis.percent,
802
979
  db_reset_generation=None if db_axis is None else db_axis.reset_generation,
980
+ protocol_generation=protocol,
981
+ account_key=account,
982
+ axis=axis,
803
983
  )
804
984
 
805
985
 
@@ -807,6 +987,8 @@ def _pending_kernel_attempt(
807
987
  pending: PendingDrop,
808
988
  reduced: AxisValue,
809
989
  db_axis: AxisProjection | None,
990
+ *,
991
+ scope=None,
810
992
  ) -> PendingDrop | None:
811
993
  """Return the persisted state for one bounded kernel attempt.
812
994
 
@@ -815,7 +997,7 @@ def _pending_kernel_attempt(
815
997
  plan. A successful post-record re-reduction clears it from DB truth; an
816
998
  unchanged projection retains this bounded retry state for the next tick.
817
999
  """
818
- signature = _build_retry_signature(reduced, db_axis)
1000
+ signature = _build_retry_signature(reduced, db_axis, scope=scope)
819
1001
  if pending.kernel_stage == "suppressed":
820
1002
  if pending.retry_signature == signature:
821
1003
  return None
@@ -888,7 +1070,8 @@ def _supporters_retracted(
888
1070
 
889
1071
 
890
1072
  def _hold_pending(
891
- pending: PendingDrop | None, db_axis: AxisProjection | None, now_epoch: int
1073
+ pending: PendingDrop | None, db_axis: AxisProjection | None, now_epoch: int,
1074
+ *, scope=None, drop_allowed: bool = True,
892
1075
  ) -> _AxisDecision:
893
1076
  """Decide a pending drop with no eligible candidate on its axis.
894
1077
 
@@ -910,10 +1093,13 @@ def _hold_pending(
910
1093
  if db_axis.percent <= pending.retained.percent:
911
1094
  # The database already holds the drop, so the record has done its work.
912
1095
  return _AxisDecision("WRITE_CONTROL", None, None)
913
- if now_epoch < pending.deadline_at:
1096
+ if now_epoch < pending.deadline_at or not drop_allowed:
1097
+ # #825/#832 spec 5.6 item 7: while the account lacks either recovery
1098
+ # certificate a drop below the database baseline is not published at
1099
+ # its deadline either. It stays pending, with no attempt spent.
914
1100
  return _AxisDecision("NOOP", None, pending)
915
1101
  publish = _retained_axis_value(pending.retained, db_axis)
916
- attempted = _pending_kernel_attempt(pending, publish, db_axis)
1102
+ attempted = _pending_kernel_attempt(pending, publish, db_axis, scope=scope)
917
1103
  if attempted is not None:
918
1104
  return _AxisDecision("PUBLISH_DB", publish, attempted)
919
1105
  # The bounded attempts are spent and no contributor remains that could
@@ -928,22 +1114,71 @@ def _reduce_axis(
928
1114
  pending: PendingDrop | None,
929
1115
  tombstone: Tombstone | None,
930
1116
  now_epoch: int,
1117
+ *,
1118
+ scope=None,
1119
+ drop_allowed: bool = True,
1120
+ evidence: AxisEvidence | None = None,
931
1121
  ) -> _AxisDecision:
932
1122
  reduced_data = _reduced_candidate(
933
1123
  candidates, axis=axis, tombstone=tombstone, db_axis=db_axis
934
1124
  )
935
1125
  if reduced_data is None:
936
- return _hold_pending(pending, db_axis, now_epoch)
1126
+ return _hold_pending(pending, db_axis, now_epoch, scope=scope,
1127
+ drop_allowed=drop_allowed)
937
1128
  reduced, in_window = reduced_data
938
1129
  key = _axis_value_key(reduced)
939
1130
 
940
1131
  if db_axis is None:
1132
+ verdict = classify_absent_axis(evidence, key, axis=axis)
1133
+ if verdict == CLASS_FLOOR:
1134
+ # Spec 4.3 revision 6 (*Floor retired*, Codex design-fix-round-6
1135
+ # Q2): the group's admitted floor retired every accepted reading.
1136
+ # Only contributors captured at or after the floor are the
1137
+ # ordinary first post-floor reading; one captured before it is
1138
+ # stale and is never relabelled as post-floor. The reading opens
1139
+ # no consensus and spends no attempt, under either gate.
1140
+ post = tuple((candidate, value) for candidate, value in in_window
1141
+ if candidate.received_at >= int(evidence.floor_epoch))
1142
+ if not post:
1143
+ verdict = CLASS_STALE
1144
+ else:
1145
+ maximum = max(value.percent for _, value in post)
1146
+ reduced = AxisValue(
1147
+ maximum,
1148
+ min(value.raw_resets_at for _, value in post
1149
+ if value.percent == maximum),
1150
+ canonical_key=key)
1151
+ verdict = CLASS_PUBLISH
1152
+ if verdict == CLASS_UNKNOWN:
1153
+ # #825/#832 review finding P1-2 (Codex fix-round-4 Q2): a missing
1154
+ # projection alone proves no empty account — it cannot tell a
1155
+ # first reading from a drop below a baseline it does not show.
1156
+ # Only classification evidence does (spec 4.3 revision 5,
1157
+ # design-fix-round-5 Q1): a proven-empty baseline, or an expired
1158
+ # five-hour predecessor whose window this reading's strictly
1159
+ # follows. Without it the reading is unknown: never published,
1160
+ # and a pending drop's evidence is kept. Fix round K13 (review
1161
+ # cycle 7 P1-2): whatever the drop gate — an open gate is no
1162
+ # classification evidence either.
1163
+ return _AxisDecision("NOOP", None, pending, unknown=True)
1164
+ if verdict == CLASS_STALE:
1165
+ # Spec 4.3 revision 6 (*Stale expired window*, *Floor retired*):
1166
+ # the existing older-window disposition — nothing published on
1167
+ # this axis, no consensus opened, the pending drop kept.
1168
+ return _hold_pending(pending, None, now_epoch, scope=scope,
1169
+ drop_allowed=drop_allowed)
1170
+ if (evidence.kind == EVIDENCE_FLOOR_RETIRED and pending is not None
1171
+ and pending.canonical_key > int(evidence.group_key)):
1172
+ # Pending state retires only where the floor proves it obsolete:
1173
+ # a pending drop of a newer window is not the retired group's.
1174
+ return _AxisDecision("PUBLISH_DB", reduced, pending)
941
1175
  return _AxisDecision("PUBLISH_DB", reduced, None)
942
1176
  if key > db_axis.canonical_key:
943
1177
  return _AxisDecision("PUBLISH_DB", reduced, None)
944
1178
  if key < db_axis.canonical_key:
945
1179
  # A stale older window says nothing about the pending newer one.
946
- return _hold_pending(pending, db_axis, now_epoch)
1180
+ return _hold_pending(pending, db_axis, now_epoch, scope=scope,
1181
+ drop_allowed=drop_allowed)
947
1182
  # `reduced` is the MAXIMUM across active contributors, so it can equal or
948
1183
  # exceed the database value while a lower contributor is reporting a real
949
1184
  # drop. The maximum decides what is displayed; it may not erase the
@@ -1017,8 +1252,13 @@ def _reduce_axis(
1017
1252
  and bool(updated.contributors)
1018
1253
  and all(item.satisfied for item in updated.contributors.values())
1019
1254
  )
1020
- if unanimous:
1021
- attempted = _pending_kernel_attempt(updated, reduced, db_axis)
1255
+ if not drop_allowed:
1256
+ # #825/#832 spec 5.6 item 7: withheld — neither consensus nor the
1257
+ # deadline publishes a drop below the baseline, and no bounded attempt
1258
+ # is spent; the record stays pending with its evidence.
1259
+ pass
1260
+ elif unanimous:
1261
+ attempted = _pending_kernel_attempt(updated, reduced, db_axis, scope=scope)
1022
1262
  if attempted is not None:
1023
1263
  return _AxisDecision("PUBLISH_DB", reduced, attempted)
1024
1264
  elif now_epoch >= updated.deadline_at:
@@ -1027,7 +1267,7 @@ def _reduce_axis(
1027
1267
  # all-contributor maximum, never the old high, and never a fresh
1028
1268
  # generation over the same drop.
1029
1269
  publish = _retained_axis_value(updated.retained, db_axis)
1030
- attempted = _pending_kernel_attempt(updated, publish, db_axis)
1270
+ attempted = _pending_kernel_attempt(updated, publish, db_axis, scope=scope)
1031
1271
  if attempted is not None:
1032
1272
  return _AxisDecision("PUBLISH_DB", publish, attempted)
1033
1273
  # The bounded attempts are spent against an unchanged signature, so this
@@ -1056,11 +1296,29 @@ def reduce_candidates(
1056
1296
  control: ControlState,
1057
1297
  tombstones: Mapping[str, Tombstone | None],
1058
1298
  now_epoch: int,
1299
+ account_key: str | None = None,
1300
+ drop_consensus_allowed: bool = True,
1059
1301
  ) -> ReductionDecision:
1060
1302
  """Reduce active candidates independently for 5h and 7d axes.
1061
1303
 
1062
1304
  The output describes only a pure state transition. The caller owns
1063
1305
  filesystem persistence, DB reconciliation, and invoking the record kernel.
1306
+
1307
+ ``account_key`` names the bound partition (#825/#832 spec 4.3): every
1308
+ retry signature minted here then carries the namespace
1309
+ ``(PROTOCOL_GENERATION, account_key, axis)``. The caller passes only that
1310
+ partition's candidates, control and tombstones.
1311
+
1312
+ ``drop_consensus_allowed`` is the outcome-quarantine control gate
1313
+ (#825/#832 spec 5.6 item 7). False while the account lacks either recovery
1314
+ certificate: a pending drop below the database baseline is then published
1315
+ neither by consensus nor at its deadline, and stays pending; rises and new
1316
+ windows publish as always. An axis with no projected baseline publishes
1317
+ under either gate only when ``db`` carries classification evidence for
1318
+ it (`classify_absent_axis`: a first reading, a new window or the first
1319
+ reading after a retiring floor); a stale reading takes the older-window
1320
+ disposition, and an unclassifiable one is named in ``unknown_axes`` (fix
1321
+ rounds K13 and K14c).
1064
1322
  """
1065
1323
  active = tuple(
1066
1324
  candidate
@@ -1074,6 +1332,9 @@ def reduce_candidates(
1074
1332
  control.pending_drops.get("fiveHour"),
1075
1333
  tombstones.get("fiveHour"),
1076
1334
  now_epoch,
1335
+ scope=None if account_key is None else (PROTOCOL_GENERATION, account_key, "fiveHour"),
1336
+ drop_allowed=drop_consensus_allowed,
1337
+ evidence=axis_evidence(db, "fiveHour"),
1077
1338
  )
1078
1339
  seven = _reduce_axis(
1079
1340
  "sevenDay",
@@ -1082,17 +1343,25 @@ def reduce_candidates(
1082
1343
  control.pending_drops.get("sevenDay"),
1083
1344
  tombstones.get("sevenDay"),
1084
1345
  now_epoch,
1346
+ scope=None if account_key is None else (PROTOCOL_GENERATION, account_key, "sevenDay"),
1347
+ drop_allowed=drop_consensus_allowed,
1348
+ evidence=axis_evidence(db, "sevenDay"),
1085
1349
  )
1086
1350
  next_control = ControlState(
1087
1351
  db_projection=db,
1088
1352
  pending_drops={"fiveHour": five.pending, "sevenDay": seven.pending},
1089
1353
  )
1354
+ unknown = frozenset(
1355
+ axis for axis, decided in (("fiveHour", five), ("sevenDay", seven))
1356
+ if decided.unknown)
1090
1357
  if five.action == "PUBLISH_DB" or seven.action == "PUBLISH_DB":
1091
1358
  return ReductionDecision(
1092
1359
  "PUBLISH_DB",
1093
1360
  next_control,
1094
1361
  PublicationPlan(seven_day=seven.candidate, five_hour=five.candidate),
1362
+ unknown_axes=unknown,
1095
1363
  )
1096
1364
  if five.action == "WRITE_CONTROL" or seven.action == "WRITE_CONTROL":
1097
- return ReductionDecision("WRITE_CONTROL", next_control)
1098
- return ReductionDecision("NOOP", next_control)
1365
+ return ReductionDecision("WRITE_CONTROL", next_control,
1366
+ unknown_axes=unknown)
1367
+ return ReductionDecision("NOOP", next_control, unknown_axes=unknown)
package/bin/cctally CHANGED
@@ -553,6 +553,8 @@ _FIVE_HOUR_JITTER_FLOOR_SECONDS = _lib_five_hour._FIVE_HOUR_JITTER_FLOOR_SECONDS
553
553
  _floor_to_ten_minutes = _lib_five_hour._floor_to_ten_minutes
554
554
  _round_to_ten_minutes = _lib_five_hour._round_to_ten_minutes
555
555
  _canonical_5h_window_key = _lib_five_hour._canonical_5h_window_key
556
+ FiveHourReading = _lib_five_hour.FiveHourReading
557
+ latest_five_hour_reading = _lib_five_hour.latest_five_hour_reading
556
558
 
557
559
  _lib_subscription_weeks = _load_sibling("_lib_subscription_weeks")
558
560
  SubWeek = _lib_subscription_weeks.SubWeek
@@ -1170,6 +1172,7 @@ _collect_codex_entries_direct = _cctally_cache._collect_codex_entries_direct
1170
1172
  get_codex_entries = _cctally_cache.get_codex_entries
1171
1173
  _sum_codex_cost_for_range = _cctally_cache._sum_codex_cost_for_range
1172
1174
  get_entries = _cctally_cache.get_entries
1175
+ CodexHistoryUnavailable = _cctally_cache.CodexHistoryUnavailable
1173
1176
  AccountAttributionUnavailable = _cctally_cache.AccountAttributionUnavailable
1174
1177
  open_cache_db = _cctally_cache.open_cache_db
1175
1178
  open_conversations_db = _cctally_cache.open_conversations_db
@@ -1190,6 +1193,7 @@ sync_codex_conversations = _cctally_cache.sync_codex_conversations
1190
1193
  _reset_orphan_warning_throttle = _cctally_cache._reset_orphan_warning_throttle
1191
1194
  cmd_cache_sync = _cctally_cache.cmd_cache_sync
1192
1195
  cmd_codex_replay_drain_internal = _cctally_cache.cmd_codex_replay_drain_internal
1196
+ cmd_codex_prune_drain_internal = _cctally_cache.cmd_codex_prune_drain_internal
1193
1197
 
1194
1198
 
1195
1199
  # Record-usage / hook-tick hot-path subsystem — the runtime path that
@@ -1220,6 +1224,7 @@ maybe_record_codex_budget_milestone = _cctally_record.maybe_record_codex_budget_
1220
1224
  maybe_record_projected_alert = _cctally_record.maybe_record_projected_alert
1221
1225
  _weekly_pct_week_avg_projection = _cctally_record._weekly_pct_week_avg_projection
1222
1226
  _compute_block_totals = _cctally_record._compute_block_totals
1227
+ _block_pricing_provenance_json = _cctally_record._block_pricing_provenance_json
1223
1228
  # #620 S2 E2: the pure fold lifted out of _compute_block_totals, plus the
1224
1229
  # canonical priced record and bucket shapes it folds over.
1225
1230
  PricedEntry = _cctally_record.PricedEntry
@@ -1228,6 +1233,7 @@ BlockTotals = _cctally_record.BlockTotals
1228
1233
  fold_block_totals = _cctally_record.fold_block_totals
1229
1234
  maybe_update_five_hour_block = _cctally_record.maybe_update_five_hour_block
1230
1235
  cmd_record_usage = _cctally_record.cmd_record_usage
1236
+ record_usage_bound = _cctally_record.record_usage_bound
1231
1237
  CreditPlan = _cctally_record.CreditPlan
1232
1238
  _parse_credit_at = _cctally_record._parse_credit_at
1233
1239
  _build_credit_plan = _cctally_record._build_credit_plan
@@ -1235,6 +1241,9 @@ _resolve_reset_aware_hwm = _cctally_record._resolve_reset_aware_hwm
1235
1241
  _insert_credit_snapshot = _cctally_record._insert_credit_snapshot
1236
1242
  _resolve_prior_5h = _cctally_record._resolve_prior_5h
1237
1243
  _resolve_active_five_hour_reset_event_id = _cctally_record._resolve_active_five_hour_reset_event_id
1244
+ resolve_five_hour_reset_event = _cctally_record.resolve_five_hour_reset_event
1245
+ FiveHourResetEvent = _cctally_record.FiveHourResetEvent
1246
+ NoFiveHourResetEvent = _cctally_record.NoFiveHourResetEvent
1238
1247
  _apply_credit = _cctally_record._apply_credit
1239
1248
  _fire_in_place_credit = _cctally_record._fire_in_place_credit
1240
1249
  detect_reset_and_credit = _cctally_record.detect_reset_and_credit
@@ -1624,6 +1633,11 @@ CLAUDE_MODEL_CONTEXT_WINDOWS = _cctally_statusline.CLAUDE_MODEL_CONTEXT_WINDOWS
1624
1633
  CLAUDE_MODEL_CONTEXT_WINDOW_DEFAULT_FAMILY = _cctally_statusline.CLAUDE_MODEL_CONTEXT_WINDOW_DEFAULT_FAMILY
1625
1634
  _resolve_statusline_tz = _cctally_statusline._resolve_statusline_tz
1626
1635
  cmd_statusline = _cctally_statusline.cmd_statusline
1636
+ # #825/#832 spec 4.3 revision 2: the hidden detached continuation worker and
1637
+ # dependency waiter.
1638
+ cmd_statusline_continue_internal = (
1639
+ _cctally_statusline.cmd_statusline_continue_internal
1640
+ )
1627
1641
  _resolve_context_window = _cctally_statusline._resolve_context_window
1628
1642
  _read_last_assistant_usage = _cctally_statusline._read_last_assistant_usage
1629
1643
  _build_statusline_injections = _cctally_statusline._build_statusline_injections
@@ -1655,6 +1669,8 @@ _authoritative_repair_required = _cctally_statusline._authoritative_repair_requi
1655
1669
  _after_authoritative_record = _cctally_statusline._after_authoritative_record
1656
1670
  _statusline_reduce_and_publish = _cctally_statusline._statusline_reduce_and_publish
1657
1671
  _build_publication_plan = _cctally_statusline._build_publication_plan
1672
+ _statusline_tombstone_states = _cctally_statusline._statusline_tombstone_states
1673
+ _capture_binding = _cctally_statusline._capture_binding
1658
1674
 
1659
1675
 
1660
1676
  # Eager re-export of bin/_cctally_codex.py — the four cmd_codex_* are the
@@ -1751,6 +1767,8 @@ BudgetRow = _cctally_forecast.BudgetRow
1751
1767
  _resolve_forecast_now = _cctally_forecast._resolve_forecast_now
1752
1768
  _fetch_current_week_snapshots = _cctally_forecast._fetch_current_week_snapshots
1753
1769
  _apply_midweek_reset_override = _cctally_forecast._apply_midweek_reset_override
1770
+ _current_week_five_hour_percent = _cctally_forecast._current_week_five_hour_percent
1771
+ _current_week_five_hour_reading = _cctally_forecast._current_week_five_hour_reading
1754
1772
  _resolve_current_budget_window = _cctally_forecast._resolve_current_budget_window
1755
1773
  _build_budget_status_inputs = _cctally_forecast._build_budget_status_inputs
1756
1774
  _build_vendor_budget_inputs = _cctally_forecast._build_vendor_budget_inputs
@@ -3666,6 +3684,9 @@ def main(argv: list[str] | None = None) -> int:
3666
3684
  # not print a partial report".
3667
3685
  eprint(f"cctally: {exc}")
3668
3686
  return 3
3687
+ except CodexHistoryUnavailable as exc:
3688
+ eprint(str(exc))
3689
+ return 3
3669
3690
  except AccountAttributionUnavailable as exc:
3670
3691
  # #341: a `--account`-scoped entry read hit a query-time cache degrade
3671
3692
  # (open failure / concurrent ingest) that would drop the account
@@ -3813,7 +3834,8 @@ def _post_command_update_hooks(command: str | None, args) -> None:
3813
3834
  return
3814
3835
  if command in ("_update-check", "_telemetry-beat", "_codex-quota-verify",
3815
3836
  "_stats-epoch-rebuild", "_stats-corruption-heal",
3816
- "_codex-replay-drain", "_artifact-retention"):
3837
+ "_codex-replay-drain", "_codex-prune-drain", "_artifact-retention",
3838
+ "_statusline-continue"):
3817
3839
  # All six hidden workers are detached and have already done their one job
3818
3840
  # in their own command handler; none must re-enter this hook.
3819
3841
  # Without the guard the ``_update-check`` worker would fall through to