cctally 1.88.2 → 1.89.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/bin/_cctally_cache.py +827 -37
  3. package/bin/_cctally_config.py +125 -0
  4. package/bin/_cctally_core.py +86 -2
  5. package/bin/_cctally_dashboard_cache_report.py +31 -0
  6. package/bin/_cctally_dashboard_conversation.py +26 -7
  7. package/bin/_cctally_dashboard_sources.py +51 -0
  8. package/bin/_cctally_db.py +626 -0
  9. package/bin/_cctally_doctor.py +84 -1
  10. package/bin/_cctally_journal.py +2 -1
  11. package/bin/_cctally_parser.py +42 -0
  12. package/bin/_cctally_quota.py +1358 -112
  13. package/bin/_cctally_record.py +249 -9
  14. package/bin/_cctally_setup.py +14 -5
  15. package/bin/_cctally_store.py +16 -1
  16. package/bin/_cctally_tui.py +16 -2
  17. package/bin/_cctally_update.py +9 -2
  18. package/bin/_lib_background_mcp.py +168 -0
  19. package/bin/_lib_cache_report.py +19 -2
  20. package/bin/_lib_codex_conversation.py +8 -0
  21. package/bin/_lib_codex_conversation_query.py +8 -7
  22. package/bin/_lib_conversation.py +105 -5
  23. package/bin/_lib_conversation_dispatch.py +15 -4
  24. package/bin/_lib_conversation_query.py +294 -2
  25. package/bin/_lib_dashboard_sources.py +5 -1
  26. package/bin/_lib_doctor.py +202 -1
  27. package/bin/_lib_jsonl.py +12 -0
  28. package/bin/_lib_quota_alert_axes.py +188 -0
  29. package/bin/_lib_quota_ledger.py +274 -0
  30. package/bin/_lib_snapshot_cache.py +36 -0
  31. package/bin/cctally +6 -3
  32. package/dashboard/static/assets/index-BgoYXdus.js +92 -0
  33. package/dashboard/static/assets/index-Ub8vwz1M.css +1 -0
  34. package/dashboard/static/dashboard.html +2 -2
  35. package/package.json +4 -1
  36. package/dashboard/static/assets/index-B0ZCsoxI.css +0 -1
  37. package/dashboard/static/assets/index-Bvp8mxtz.js +0 -92
@@ -414,6 +414,65 @@ def _codex_lifecycle_activity_24h(
414
414
  return records
415
415
 
416
416
 
417
+ def _codex_quota_verify_activity_24h(*, now_utc: "dt.datetime") -> dict:
418
+ """Aggregate the detached `_codex-quota-verify` worker's 24h outcomes.
419
+
420
+ The worker's three streams are `/dev/null` and its exit code is observed by
421
+ nobody, so `hook-tick.log` is the only place its outcome can land — which is
422
+ why it writes there. `_codex_lifecycle_activity_24h` above cannot supply
423
+ this: worker lines carry no `source_root_key`, so its root filter drops
424
+ every one of them.
425
+
426
+ Same bounded-read contract as its sibling — timestamped records, aggregate
427
+ counters only, never session/prompt/response content.
428
+ """
429
+ cutoff = now_utc - dt.timedelta(hours=24)
430
+ counts: dict = {
431
+ "success_count_24h": 0,
432
+ "error_count_24h": 0,
433
+ "spawn_failure_count_24h": 0,
434
+ "last_success_at": None,
435
+ }
436
+ for path in (
437
+ _cctally_core.HOOK_TICK_LOG_ROTATED_PATH,
438
+ _cctally_core.HOOK_TICK_LOG_PATH,
439
+ ):
440
+ try:
441
+ lines = path.read_text(encoding="utf-8", errors="replace").splitlines()
442
+ except OSError:
443
+ continue
444
+ for line in lines:
445
+ tokens = line.split()
446
+ if not tokens:
447
+ continue
448
+ try:
449
+ captured_at = parse_iso_datetime(
450
+ tokens[0], "codex quota verify log timestamp")
451
+ captured_at = captured_at.astimezone(dt.timezone.utc)
452
+ except (IndexError, ValueError, TypeError):
453
+ continue
454
+ if captured_at > now_utc or captured_at < cutoff:
455
+ continue
456
+ fields = {
457
+ token.split("=", 1)[0]: token.split("=", 1)[1]
458
+ for token in tokens[1:] if "=" in token
459
+ }
460
+ if fields.get("provider") != "codex":
461
+ continue
462
+ op = fields.get("op")
463
+ outcome = fields.get("result")
464
+ if op == "quota-verify" and outcome == "success":
465
+ counts["success_count_24h"] += 1
466
+ prior = counts["last_success_at"]
467
+ if prior is None or captured_at > prior:
468
+ counts["last_success_at"] = captured_at
469
+ elif op == "quota-verify" and outcome == "error":
470
+ counts["error_count_24h"] += 1
471
+ elif op == "quota-verify-spawn" and outcome == "failed":
472
+ counts["spawn_failure_count_24h"] += 1
473
+ return counts
474
+
475
+
417
476
  def _gather_accounts_state(now_utc: "dt.datetime") -> dict:
418
477
  """Best-effort account-attribution state for the doctor `accounts.*` legs
419
478
  (#341). Never raises: identity + registry reads are read-only and each guard
@@ -1120,6 +1179,12 @@ def _doctor_gather_state_impl(
1120
1179
  except Exception:
1121
1180
  codex_lifecycle_activity_24h = {}
1122
1181
 
1182
+ try:
1183
+ codex_quota_verify_activity = _codex_quota_verify_activity_24h(
1184
+ now_utc=now_utc)
1185
+ except Exception:
1186
+ codex_quota_verify_activity = None
1187
+
1123
1188
  # ── Parse health (#279 S2 F5a) ───────────────────────────────────
1124
1189
  parse_health_claude = parse_health_codex = None
1125
1190
  # #416 review B4: the durable record that a torn Codex `auth.json` halted
@@ -1131,19 +1196,30 @@ def _doctor_gather_state_impl(
1131
1196
  # from the kernel constants, never inline literals.
1132
1197
  codex_replay_pending = None
1133
1198
  codex_replay_blocked = None
1199
+ # public #5: the budgeted-decline record. Same JSON-dict contract as the
1200
+ # blocked one, and the only signal a hook-only install produces when its
1201
+ # Codex ingest is frozen behind an un-runnable replay.
1202
+ codex_replay_deferred = None
1203
+ # public #5 spec §5: the hook's budgeted-ingest backlog record. Absent means
1204
+ # a zero backlog — a drained walk DELETES the row rather than zeroing it, so
1205
+ # None and "nothing owed" are the same state by construction.
1206
+ codex_ingest_backlog = None
1134
1207
  try:
1135
1208
  import _lib_codex_conversation as _codex_kern
1136
1209
  _blocked_key = _codex_kern.CODEX_REPLAY_BLOCKED_KEY
1137
1210
  _pending_key = _codex_kern.CODEX_REPLAY_FROM_ZERO_KEY
1211
+ _deferred_key = _codex_kern.CODEX_REPLAY_DEFERRED_KEY
1138
1212
  except Exception:
1139
1213
  _blocked_key = "codex_replay_from_zero_blocked"
1140
1214
  _pending_key = "codex_replay_from_zero_pending"
1215
+ _deferred_key = "codex_replay_from_zero_deferred"
1141
1216
  try:
1142
1217
  if _cache_probe_allowed and _cctally_core.CACHE_DB_PATH.exists():
1143
1218
  conn = sqlite3.connect(str(_cctally_core.CACHE_DB_PATH))
1144
1219
  try:
1145
1220
  for _key in ("parse_health_claude", "parse_health_codex",
1146
- "codex_torn_auth_deferred", _blocked_key):
1221
+ "codex_torn_auth_deferred", _blocked_key,
1222
+ _deferred_key, "codex_ingest_backlog"):
1147
1223
  try:
1148
1224
  row = conn.execute(
1149
1225
  "SELECT value FROM cache_meta WHERE key = ?",
@@ -1158,6 +1234,10 @@ def _doctor_gather_state_impl(
1158
1234
  parse_health_codex = _parsed
1159
1235
  elif _key == _blocked_key:
1160
1236
  codex_replay_blocked = _parsed
1237
+ elif _key == _deferred_key:
1238
+ codex_replay_deferred = _parsed
1239
+ elif _key == "codex_ingest_backlog":
1240
+ codex_ingest_backlog = _parsed
1161
1241
  else:
1162
1242
  codex_torn_deferred = _parsed
1163
1243
  except (sqlite3.OperationalError, ValueError):
@@ -1744,8 +1824,10 @@ def _doctor_gather_state_impl(
1744
1824
  parse_health_claude=parse_health_claude,
1745
1825
  parse_health_codex=parse_health_codex,
1746
1826
  codex_torn_deferred=codex_torn_deferred,
1827
+ codex_ingest_backlog=codex_ingest_backlog,
1747
1828
  codex_replay_pending=codex_replay_pending,
1748
1829
  codex_replay_blocked=codex_replay_blocked,
1830
+ codex_replay_deferred=codex_replay_deferred,
1749
1831
  stats_db_quick_check=stats_db_quick_check,
1750
1832
  cache_db_quick_check=cache_db_quick_check,
1751
1833
  conversations_db_quick_check=conversations_db_quick_check,
@@ -1765,6 +1847,7 @@ def _doctor_gather_state_impl(
1765
1847
  codex_quota_windows=codex_quota_windows,
1766
1848
  codex_hook_roots=codex_hook_roots,
1767
1849
  codex_lifecycle_activity_24h=codex_lifecycle_activity_24h,
1850
+ codex_quota_verify_activity=codex_quota_verify_activity,
1768
1851
  # #311: precomputed statusLine.refreshInterval classification.
1769
1852
  statusline_refresh_state=statusline_refresh_state,
1770
1853
  statusline_pipeline=statusline_pipeline,
@@ -4244,6 +4244,7 @@ _REBUILD_REQUIRED_TABLES = frozenset(
4244
4244
  "projected_milestones",
4245
4245
  "quota_alert_arming",
4246
4246
  "quota_percent_milestones",
4247
+ "quota_projection_ledger_state",
4247
4248
  "quota_projection_state",
4248
4249
  "quota_threshold_events",
4249
4250
  "quota_window_blocks",
@@ -4298,7 +4299,7 @@ _REBUILD_REQUIRED_INDEXES = frozenset(
4298
4299
  # omitted column, constraint, partial predicate, or index definition. An epoch
4299
4300
  # schema change must update this contract alongside STATS_INDEX_EPOCH.
4300
4301
  _REBUILD_SCHEMA_FINGERPRINT = (
4301
- "3e0ec46a965c9fa10ac827cfd1656c66ecae50ed289e3aa1c34d2cf6a3e5c4a3"
4302
+ "1e0a8cc22b3dc754cb8a6074ff9d2ef28df77b656dfc65347e4afbcb6edfdfae"
4302
4303
  )
4303
4304
 
4304
4305
 
@@ -3337,6 +3337,46 @@ def _build_telemetry_beat_parser(subparsers, name, *, help_text, xref=None):
3337
3337
  )
3338
3338
  tb.set_defaults(func=c.cmd_telemetry_beat_internal)
3339
3339
 
3340
+ def _build_codex_quota_verify_parser(subparsers, name, *, help_text, xref=None):
3341
+ """Build the `_codex-quota-verify` parser (public #5 spec §2)."""
3342
+ c = _cctally()
3343
+ qv = subparsers.add_parser(
3344
+ name,
3345
+ help=help_text,
3346
+ formatter_class=CLIHelpFormatter,
3347
+ description=textwrap.dedent(
3348
+ """\
3349
+ Internal subcommand: detached worker that runs the Codex
3350
+ quota projection's periodic whole-history verification
3351
+ pass. Spawned by the Codex hook tick when the pass comes
3352
+ due, so the one unbounded operation in the incremental
3353
+ design never runs on the blocking hook path. Always returns
3354
+ 0; the deadline is stamped only if the pass completes.
3355
+ """
3356
+ ),
3357
+ )
3358
+ qv.set_defaults(func=c.cmd_codex_quota_verify_internal)
3359
+
3360
+ def _build_codex_replay_drain_parser(subparsers, name, *, help_text, xref=None):
3361
+ """Build the `_codex-replay-drain` parser (public #5 §4)."""
3362
+ c = _cctally()
3363
+ rd = subparsers.add_parser(
3364
+ name,
3365
+ help=help_text,
3366
+ formatter_class=CLIHelpFormatter,
3367
+ description=textwrap.dedent(
3368
+ """\
3369
+ Internal subcommand: detached worker that performs the
3370
+ byte-zero Codex replay a budgeted hook tick declined. The
3371
+ replay is not sliceable, so the hook never attempts one and
3372
+ a hook-only install would otherwise freeze all Codex ingest
3373
+ permanently. Always returns 0; failures are written to
3374
+ hook-tick.log.
3375
+ """
3376
+ ),
3377
+ )
3378
+ rd.set_defaults(func=c.cmd_codex_replay_drain_internal)
3379
+
3340
3380
  def _build_repair_symlinks_parser(subparsers, name, *, help_text, xref=None):
3341
3381
  """Build the `repair-symlinks` parser (registered via _REGISTRATION; #279 S6 W3).
3342
3382
 
@@ -3417,6 +3457,8 @@ _REGISTRATION = (
3417
3457
  _Reg('update', _build_update_parser, "Update cctally to the latest version", None, None),
3418
3458
  _Reg('_update-check', _build_update_check_parser, argparse.SUPPRESS, None, None),
3419
3459
  _Reg('_telemetry-beat', _build_telemetry_beat_parser, argparse.SUPPRESS, None, None),
3460
+ _Reg('_codex-quota-verify', _build_codex_quota_verify_parser, argparse.SUPPRESS, None, None),
3461
+ _Reg('_codex-replay-drain', _build_codex_replay_drain_parser, argparse.SUPPRESS, None, None),
3420
3462
  _Reg('repair-symlinks', _build_repair_symlinks_parser, argparse.SUPPRESS, None, None),
3421
3463
  )
3422
3464