cctally 1.82.0 → 1.83.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 (52) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/README.md +52 -74
  3. package/bin/_cctally_alerts.py +8 -1
  4. package/bin/_cctally_cache.py +963 -149
  5. package/bin/_cctally_config.py +43 -4
  6. package/bin/_cctally_core.py +933 -759
  7. package/bin/_cctally_dashboard.py +157 -47
  8. package/bin/_cctally_dashboard_cache_report.py +13 -6
  9. package/bin/_cctally_dashboard_conversation.py +1 -0
  10. package/bin/_cctally_dashboard_envelope.py +186 -8
  11. package/bin/_cctally_dashboard_share.py +60 -20
  12. package/bin/_cctally_dashboard_sources.py +427 -128
  13. package/bin/_cctally_db.py +605 -128
  14. package/bin/_cctally_doctor.py +413 -28
  15. package/bin/_cctally_five_hour.py +12 -5
  16. package/bin/_cctally_journal.py +2050 -156
  17. package/bin/_cctally_journal_repair.py +519 -0
  18. package/bin/_cctally_milestone_history.py +142 -56
  19. package/bin/_cctally_milestones.py +179 -111
  20. package/bin/_cctally_parser.py +42 -0
  21. package/bin/_cctally_project.py +24 -18
  22. package/bin/_cctally_quota.py +139 -25
  23. package/bin/_cctally_record.py +279 -108
  24. package/bin/_cctally_rederive.py +1052 -0
  25. package/bin/_cctally_reporting.py +58 -53
  26. package/bin/_cctally_setup.py +1 -0
  27. package/bin/_cctally_source_analytics.py +4 -1
  28. package/bin/_cctally_statusline.py +11 -11
  29. package/bin/_cctally_store.py +1039 -31
  30. package/bin/_cctally_sync_week.py +17 -8
  31. package/bin/_cctally_tui.py +421 -54
  32. package/bin/_cctally_update.py +133 -8
  33. package/bin/_cctally_weekrefs.py +14 -0
  34. package/bin/_lib_aggregators.py +10 -6
  35. package/bin/_lib_cache_report.py +101 -9
  36. package/bin/_lib_codex_pools.py +82 -0
  37. package/bin/_lib_conversation_query.py +126 -33
  38. package/bin/_lib_dashboard_sources.py +126 -1
  39. package/bin/_lib_diff_kernel.py +28 -15
  40. package/bin/_lib_doctor.py +342 -4
  41. package/bin/_lib_journal.py +924 -2
  42. package/bin/_lib_jsonl.py +43 -14
  43. package/bin/_lib_pricing.py +140 -21
  44. package/bin/_lib_readme_refresh.py +401 -0
  45. package/bin/_lib_rederive.py +395 -0
  46. package/bin/_lib_share.py +58 -2
  47. package/bin/cctally +56 -8
  48. package/dashboard/static/assets/{index-DJP4gEB7.js → index-3bgCMVHb.js} +52 -52
  49. package/dashboard/static/assets/index-D27EIHEI.css +1 -0
  50. package/dashboard/static/dashboard.html +2 -2
  51. package/package.json +6 -1
  52. package/dashboard/static/assets/index-Dk1nplOz.css +0 -1
@@ -434,8 +434,10 @@ def _config_codex_leaf_value(config: dict, key: str) -> object:
434
434
  return list(value) if isinstance(value, list) else value
435
435
 
436
436
 
437
- def _resolve_one_account_budget_ref(conn, ref: object, provider: str,
438
- label: str) -> str:
437
+ def _resolve_one_account_budget_ref(
438
+ conn, ref: object, provider: str, label: str,
439
+ *, stats_maintenance_unavailable: bool = False,
440
+ ) -> str:
439
441
  """Resolve one per-account-budget ref to an immutable account_key at WRITE
440
442
  time (#341, spec §6 — a later label rename must never retarget the budget).
441
443
 
@@ -456,6 +458,12 @@ def _resolve_one_account_budget_ref(conn, ref: object, provider: str,
456
458
  if _re.fullmatch(r"[0-9a-f]{32}", ref):
457
459
  return ref # already an immutable account_key
458
460
  if conn is None:
461
+ if stats_maintenance_unavailable:
462
+ raise _BudgetConfigError(
463
+ f"{label}: cannot resolve account ref {ref!r} while stats.db "
464
+ "maintenance is in progress; retry after it completes or use "
465
+ "the 32-hex account key"
466
+ )
459
467
  raise _BudgetConfigError(
460
468
  f"{label}: cannot resolve account ref {ref!r} — no accounts observed "
461
469
  "yet; use the 32-hex account key"
@@ -475,13 +483,44 @@ def _normalize_account_budget_refs(raw_obj: dict, provider: str,
475
483
  (validated numerically by ``_validate_account_budget_map`` under the lock)."""
476
484
  import sqlite3
477
485
  conn = None
486
+ stats_maintenance_unavailable = False
478
487
  try:
479
488
  db_path = _cctally_core.DB_PATH
480
489
  if db_path.exists():
481
- conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
490
+ # #386: `mode=ro` is not exempt from the opener protocol — measured,
491
+ # it CREATES `-shm`/`-wal` when they are absent.
492
+ #
493
+ # LOCK ORDER, deliberately noted: this runs under
494
+ # `config_writer_lock`, so it takes `stats.db.maintenance.lock`
495
+ # SHARED while holding it. That edge is safe because it is one-way —
496
+ # no stats maintenance path writes config.json, so no holder of the
497
+ # stats maintenance lock ever waits on `config_writer_lock`. Adding
498
+ # such a writer would create a cycle; don't.
499
+ #
500
+ # A database read failure degrades to `conn = None`, which preserves
501
+ # raw 32-hex account-key writes but cannot resolve label/email/prefix
502
+ # refs. A bounded maintenance-lock timeout is distinguished below
503
+ # so the user is told to retry after maintenance instead of being
504
+ # told that no accounts have been observed.
505
+ import _cctally_store
506
+ import _cctally_db
507
+ try:
508
+ conn = _cctally_store.stats_open_guarded(
509
+ db_path,
510
+ connect=lambda p: sqlite3.connect(
511
+ f"file:{p}?mode=ro", uri=True),
512
+ )
513
+ except _cctally_db.StatsDbMaintenanceError:
514
+ stats_maintenance_unavailable = True
515
+ conn = None
516
+ except sqlite3.DatabaseError:
517
+ conn = None
482
518
  out: dict = {}
483
519
  for ref, val in raw_obj.items():
484
- key = _resolve_one_account_budget_ref(conn, ref, provider, label)
520
+ key = _resolve_one_account_budget_ref(
521
+ conn, ref, provider, label,
522
+ stats_maintenance_unavailable=stats_maintenance_unavailable,
523
+ )
485
524
  out[key] = val
486
525
  return out
487
526
  finally: