cctally 1.99.1 → 1.101.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 (49) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/bin/_cctally_alerts.py +13 -2
  3. package/bin/_cctally_cache.py +3 -1
  4. package/bin/_cctally_cache_report.py +103 -6
  5. package/bin/_cctally_dashboard.py +2083 -356
  6. package/bin/_cctally_dashboard_envelope.py +115 -31
  7. package/bin/_cctally_dashboard_perf.py +433 -0
  8. package/bin/_cctally_dashboard_share.py +101 -29
  9. package/bin/_cctally_dashboard_sources.py +1072 -214
  10. package/bin/_cctally_db.py +30 -14
  11. package/bin/_cctally_diff.py +20 -0
  12. package/bin/_cctally_doctor.py +1331 -1148
  13. package/bin/_cctally_forecast.py +304 -96
  14. package/bin/_cctally_milestone_history.py +10 -2
  15. package/bin/_cctally_parser.py +70 -1
  16. package/bin/_cctally_project.py +155 -47
  17. package/bin/_cctally_quota.py +40 -21
  18. package/bin/_cctally_record.py +44 -3
  19. package/bin/_cctally_refresh.py +60 -10
  20. package/bin/_cctally_share.py +9 -2
  21. package/bin/_cctally_source_analytics.py +40 -4
  22. package/bin/_cctally_statusline.py +53 -7
  23. package/bin/_cctally_tui.py +723 -232
  24. package/bin/_cctally_update.py +28 -22
  25. package/bin/_lib_alert_scope.py +685 -0
  26. package/bin/_lib_alerts_payload.py +112 -7
  27. package/bin/_lib_cache_report.py +110 -1
  28. package/bin/_lib_codex_pools.py +20 -8
  29. package/bin/_lib_dashboard_sources.py +237 -30
  30. package/bin/_lib_doctor.py +37 -0
  31. package/bin/_lib_forecast.py +12 -4
  32. package/bin/_lib_jsonl.py +4 -2
  33. package/bin/_lib_perf.py +132 -3
  34. package/bin/_lib_pricing.py +8 -7
  35. package/bin/_lib_render.py +31 -3
  36. package/bin/_lib_share_templates.py +150 -55
  37. package/bin/_lib_snapshot_cache.py +71 -13
  38. package/bin/_lib_source_analytics.py +2 -2
  39. package/bin/_lib_source_identity.py +50 -2
  40. package/bin/_lib_subscription_weeks.py +65 -0
  41. package/bin/_lib_tick_stats.py +538 -0
  42. package/bin/cctally +29 -7
  43. package/dashboard/static/assets/dashboardStream.shared-worker-1XTMV3nr.js +1 -0
  44. package/dashboard/static/assets/index-D6Eb9KDn.js +97 -0
  45. package/dashboard/static/assets/index-i3g7g8zo.css +1 -0
  46. package/dashboard/static/dashboard.html +2 -2
  47. package/package.json +4 -1
  48. package/dashboard/static/assets/index-C5NBB2w9.js +0 -97
  49. package/dashboard/static/assets/index-hJP4wlIO.css +0 -1
@@ -11084,7 +11084,7 @@ def cmd_db_backup(args: argparse.Namespace) -> int:
11084
11084
 
11085
11085
 
11086
11086
  def cmd_db_checkpoint(args: argparse.Namespace) -> int:
11087
- """Fast, non-destructive WAL drain (TRUNCATE checkpoint) for cache.db.
11087
+ """Fast, non-destructive WAL drain (TRUNCATE checkpoint) for a WAL store.
11088
11088
 
11089
11089
  Opens a RAW existing-file-only connection — NOT open_cache_db()/open_db(),
11090
11090
  which apply schema, run the migration dispatcher, can DELETE Codex rows on a
@@ -11095,14 +11095,16 @@ def cmd_db_checkpoint(args: argparse.Namespace) -> int:
11095
11095
 
11096
11096
  A cache checkpoint takes the same global writer flock as both provider
11097
11097
  syncs, using the command's timeout as one bounded wait, so it cannot overlap
11098
- a cache write or another cache checkpoint. Exit 0 when drained /
11099
- already-small / the DB is absent; 3 (staged) if the target stayed busy or
11100
- was not fully truncated through the timeout — an actionable "something is
11101
- still holding it" signal.
11098
+ a cache write or another cache checkpoint. A conversations checkpoint takes
11099
+ a THREE-lock plan instead, because that store has two independently writable
11100
+ provider domains rather than cache.db's single global writer flock (#583
11101
+ S4). Exit 0 when drained / already-small / the DB is absent; 3 (staged) if
11102
+ the target stayed busy or was not fully truncated through the timeout — an
11103
+ actionable "something is still holding it" signal.
11102
11104
  """
11103
11105
  from _lib_json_envelope import stamp_schema_version
11104
11106
 
11105
- which = args.db # "cache" | "stats"
11107
+ which = args.db # "cache" | "conversations" | "stats"
11106
11108
  as_json = bool(getattr(args, "json", False))
11107
11109
  if which == "stats":
11108
11110
  reason = (
@@ -11120,7 +11122,26 @@ def cmd_db_checkpoint(args: argparse.Namespace) -> int:
11120
11122
 
11121
11123
  import _cctally_cache
11122
11124
 
11123
- path, label = _cctally_core.CACHE_DB_PATH, "cache.db"
11125
+ if which == "conversations":
11126
+ path, label = _cctally_core.CONVERSATIONS_DB_PATH, "conversations.db"
11127
+ # conversations.db has TWO independently writable provider domains,
11128
+ # unlike cache.db's single global writer flock, so a checkpoint must
11129
+ # exclude both. The order is the documented lock-order law: maintenance
11130
+ # flocks before the conversation provider flocks (Claude, then Codex),
11131
+ # and checkpoint work takes maintenance SHARED — only mutating
11132
+ # maintenance takes it exclusive, and retention's exclusive hold is an
11133
+ # admission gate it downgrades to shared for the mutating pass itself.
11134
+ lock_plan = [
11135
+ (_cctally_core.CONVERSATIONS_LOCK_MAINTENANCE_PATH, fcntl.LOCK_SH),
11136
+ (_cctally_core.CONVERSATIONS_LOCK_PATH, fcntl.LOCK_EX),
11137
+ (_cctally_core.CONVERSATIONS_LOCK_CODEX_PATH, fcntl.LOCK_EX),
11138
+ ]
11139
+ else:
11140
+ path, label = _cctally_core.CACHE_DB_PATH, "cache.db"
11141
+ lock_plan = [
11142
+ (_cctally_core.CACHE_LOCK_MAINTENANCE_PATH, fcntl.LOCK_SH),
11143
+ (_cctally_core.CACHE_LOCK_PATH, fcntl.LOCK_EX),
11144
+ ]
11124
11145
  timeout = int(getattr(args, "busy_timeout_ms", None)
11125
11146
  or _cctally_cache.CHECKPOINT_CMD_BUSY_TIMEOUT_MS)
11126
11147
 
@@ -11137,16 +11158,11 @@ def cmd_db_checkpoint(args: argparse.Namespace) -> int:
11137
11158
  print(f"cctally: no {label} database file present; nothing to drain.")
11138
11159
  return 0
11139
11160
 
11140
- from _lib_cache_writer_lock import acquire_ordered_flocks
11141
-
11142
- lock_plan = [
11143
- (_cctally_core.CACHE_LOCK_MAINTENANCE_PATH, fcntl.LOCK_SH),
11144
- (_cctally_core.CACHE_LOCK_PATH, fcntl.LOCK_EX),
11145
- ]
11161
+ import _lib_cache_writer_lock
11146
11162
 
11147
11163
  held: list[int] = []
11148
11164
  try:
11149
- acquired = acquire_ordered_flocks(
11165
+ acquired = _lib_cache_writer_lock.acquire_ordered_flocks(
11150
11166
  lock_plan, timeout=max(timeout, 0) / 1000
11151
11167
  )
11152
11168
  if acquired is None:
@@ -21,6 +21,8 @@ import sqlite3
21
21
  import sys
22
22
 
23
23
  import _lib_diff_kernel as dk
24
+ # #620 S1 D11: the one affordance shape every warning state renders.
25
+ import _lib_alert_scope
24
26
 
25
27
  from _cctally_core import _command_as_of, eprint
26
28
 
@@ -279,4 +281,22 @@ def cmd_diff(args: argparse.Namespace) -> int:
279
281
  result, color=color, width=width, raw_aggregates=result.raw_totals,
280
282
  tz=tz_obj, compact=args.compact,
281
283
  ))
284
+ # #620 S1 D11: when the two windows are of different lengths the absolute
285
+ # dollars on screen are per-day normalizations, so route to the command
286
+ # that reports window A's real, un-normalized total.
287
+ if result.mismatched_length and not result.auto_normalized:
288
+ source = getattr(args, "source", None) or "claude"
289
+ a = result.window_a
290
+ # Full UTC instants, not bare dates: `range-cost -s/-e` are parsed by
291
+ # `parse_iso_datetime`, which reads a naive value as host-local and
292
+ # never extends a date to end-of-day, so a date-only selector would
293
+ # drop every hour after midnight from the window the line states.
294
+ print(_lib_alert_scope.next_step_line(
295
+ f"cctally range-cost -s {a.start_utc:%Y-%m-%dT%H:%M:%SZ} "
296
+ f"-e {a.end_utc:%Y-%m-%dT%H:%M:%SZ} -b --source {source}",
297
+ provider=source,
298
+ window_start=a.start_utc,
299
+ window_end=a.end_utc,
300
+ tz=tz_obj,
301
+ ))
282
302
  return 0