cctally 1.117.2 → 1.118.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 (58) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +7 -3
  3. package/bin/_cctally_cache.py +2783 -175
  4. package/bin/_cctally_completion.py +106 -33
  5. package/bin/_cctally_core.py +69 -4
  6. package/bin/_cctally_dashboard.py +188 -13
  7. package/bin/_cctally_dashboard_conversation.py +5 -1
  8. package/bin/_cctally_dashboard_envelope.py +50 -19
  9. package/bin/_cctally_dashboard_sources.py +23 -1
  10. package/bin/_cctally_db.py +193 -153
  11. package/bin/_cctally_diagnosis_sources.py +63 -7
  12. package/bin/_cctally_doctor.py +865 -93
  13. package/bin/_cctally_five_hour.py +67 -50
  14. package/bin/_cctally_forecast.py +35 -13
  15. package/bin/_cctally_journal.py +186 -18
  16. package/bin/_cctally_milestone_history.py +859 -62
  17. package/bin/_cctally_oauth_samples.py +286 -15
  18. package/bin/_cctally_parser.py +2 -2
  19. package/bin/_cctally_quota.py +104 -12
  20. package/bin/_cctally_record.py +153 -29
  21. package/bin/_cctally_rederive.py +608 -68
  22. package/bin/_cctally_statusline.py +108 -81
  23. package/bin/_cctally_store.py +179 -38
  24. package/bin/_cctally_tui.py +892 -181
  25. package/bin/_cctally_update.py +32 -13
  26. package/bin/_lib_codex_conversation_query.py +52 -14
  27. package/bin/_lib_codex_drain_admission.py +224 -6
  28. package/bin/_lib_codex_metadata.py +39 -0
  29. package/bin/_lib_conversation_anon.py +15 -8
  30. package/bin/_lib_conversation_dispatch.py +7 -0
  31. package/bin/_lib_conversation_query.py +289 -70
  32. package/bin/_lib_dashboard_sources.py +37 -1
  33. package/bin/_lib_detector_access_registry.py +497 -26
  34. package/bin/_lib_doctor.py +85 -20
  35. package/bin/_lib_doctor_scan_index.py +321 -0
  36. package/bin/_lib_ingest_frontier.py +20 -1
  37. package/bin/_lib_oauth_samples.py +23 -7
  38. package/bin/_lib_pricing.py +146 -16
  39. package/bin/_lib_quota.py +19 -0
  40. package/bin/_lib_rederive.py +11 -6
  41. package/bin/_lib_retained_records.py +516 -0
  42. package/bin/_lib_snapshot_cache.py +10 -0
  43. package/bin/_lib_stale_proof.py +272 -43
  44. package/bin/_lib_stats_content_revision.py +831 -0
  45. package/bin/_lib_write_io.py +11 -4
  46. package/bin/cctally +4 -1
  47. package/dashboard/static/assets/ConversationsView-D4Iv4ku0.js +72 -0
  48. package/dashboard/static/assets/{DoctorModal-B0DnU9d9.js → DoctorModal-D4QSq6uz.js} +1 -1
  49. package/dashboard/static/assets/ModalRoot-C9F5dhZi.js +1 -0
  50. package/dashboard/static/assets/{ProjectsDrillPanel-DSxXQSa-.js → ProjectsDrillPanel-WxAdRDMt.js} +1 -1
  51. package/dashboard/static/assets/{SourceDetailModal-BleDpQqG.js → SourceDetailModal-NyyEfXmc.js} +1 -1
  52. package/dashboard/static/assets/{UpdateModal-CMqaAkSK.js → UpdateModal-CFLq9pT1.js} +1 -1
  53. package/dashboard/static/assets/{index-BjQV-0-D.js → index-BKTWH9JE.js} +2 -2
  54. package/dashboard/static/assets/{outlineNavigation-CO-VWBm7.js → outlineNavigation-BnGUwCS3.js} +3 -3
  55. package/dashboard/static/dashboard.html +1 -1
  56. package/package.json +4 -1
  57. package/dashboard/static/assets/ConversationsView-oqNsWzWV.js +0 -72
  58. package/dashboard/static/assets/ModalRoot-CYua2wP_.js +0 -1
@@ -2345,6 +2345,42 @@ class BlockTotals:
2345
2345
  by_model: dict[str, BlockBucket] = field(default_factory=dict)
2346
2346
  by_project: dict[str, BlockBucket] = field(default_factory=dict)
2347
2347
 
2348
+ def add_priced(
2349
+ self,
2350
+ model: str,
2351
+ project_path: str | None,
2352
+ input_tokens: int,
2353
+ output_tokens: int,
2354
+ cache_creation_tokens: int,
2355
+ cache_read_tokens: int,
2356
+ cost_usd: float,
2357
+ ) -> None:
2358
+ """Fold one priced entry: THE per-entry step of `fold_block_totals`.
2359
+
2360
+ #1003 D1: the scratch replays' incremental engine
2361
+ (`_cctally_rederive.BlockTotalsEngine`) folds through this same
2362
+ method, so both paths perform the identical float additions in the
2363
+ identical order and create buckets in the identical order.
2364
+ """
2365
+ self.input_tokens += input_tokens
2366
+ self.output_tokens += output_tokens
2367
+ self.cache_create_tokens += cache_creation_tokens
2368
+ self.cache_read_tokens += cache_read_tokens
2369
+ self.cost_usd += cost_usd
2370
+ self.entry_count += 1
2371
+
2372
+ for key, bucket_dict in (
2373
+ (model, self.by_model),
2374
+ (project_path or "(unknown)", self.by_project),
2375
+ ):
2376
+ b = bucket_dict.setdefault(key, BlockBucket())
2377
+ b.input_tokens += input_tokens
2378
+ b.output_tokens += output_tokens
2379
+ b.cache_create_tokens += cache_creation_tokens
2380
+ b.cache_read_tokens += cache_read_tokens
2381
+ b.cost_usd += cost_usd
2382
+ b.entry_count += 1
2383
+
2348
2384
  def as_legacy_dict(self) -> dict[str, Any]:
2349
2385
  """The exact dict `_compute_block_totals` has always returned.
2350
2386
 
@@ -2379,24 +2415,11 @@ def fold_block_totals(entries: "Iterable[PricedEntry]") -> BlockTotals:
2379
2415
  """
2380
2416
  totals = BlockTotals()
2381
2417
  for entry in entries:
2382
- totals.input_tokens += entry.input_tokens
2383
- totals.output_tokens += entry.output_tokens
2384
- totals.cache_create_tokens += entry.cache_creation_tokens
2385
- totals.cache_read_tokens += entry.cache_read_tokens
2386
- totals.cost_usd += entry.cost_usd
2387
- totals.entry_count += 1
2388
-
2389
- for key, bucket_dict in (
2390
- (entry.model, totals.by_model),
2391
- (entry.project_path or "(unknown)", totals.by_project),
2392
- ):
2393
- b = bucket_dict.setdefault(key, BlockBucket())
2394
- b.input_tokens += entry.input_tokens
2395
- b.output_tokens += entry.output_tokens
2396
- b.cache_create_tokens += entry.cache_creation_tokens
2397
- b.cache_read_tokens += entry.cache_read_tokens
2398
- b.cost_usd += entry.cost_usd
2399
- b.entry_count += 1
2418
+ totals.add_priced(
2419
+ entry.model, entry.project_path, entry.input_tokens,
2420
+ entry.output_tokens, entry.cache_creation_tokens,
2421
+ entry.cache_read_tokens, entry.cost_usd,
2422
+ )
2400
2423
  return totals
2401
2424
 
2402
2425
 
@@ -2464,6 +2487,23 @@ def _five_hour_ownership_windows(
2464
2487
  return windows
2465
2488
 
2466
2489
 
2490
+ #: #1003 D1: the block-totals engine `_compute_block_totals` defers to, or
2491
+ #: None. Only `_cctally_rederive._scratch_read_adapters` installs one, for the
2492
+ #: duration of a scratch replay over a stable cache snapshot; every live path
2493
+ #: (record-usage, hook-tick, the rebuild's open-block projection, the
2494
+ #: backfills) runs with the slot empty.
2495
+ _BLOCK_TOTALS_ENGINE = None
2496
+
2497
+
2498
+ def swap_block_totals_engine(engine):
2499
+ """Install ``engine`` (or None) in the block-totals slot and return the
2500
+ engine it replaces, so the caller can restore it."""
2501
+ global _BLOCK_TOTALS_ENGINE
2502
+ previous = _BLOCK_TOTALS_ENGINE
2503
+ _BLOCK_TOTALS_ENGINE = engine
2504
+ return previous
2505
+
2506
+
2467
2507
  def _compute_block_totals(
2468
2508
  block_start_at: dt.datetime,
2469
2509
  range_end: dt.datetime,
@@ -2515,7 +2555,36 @@ def _compute_block_totals(
2515
2555
  f"_compute_block_totals: owner_key {owner_key!r} is absent from "
2516
2556
  f"the competing-window context"
2517
2557
  )
2558
+ if _BLOCK_TOTALS_ENGINE is not None:
2559
+ # #1003 D1: inside a scratch replay the engine answers exactly as the
2560
+ # body below would. It ignores ``skip_sync``, as the replay's
2561
+ # `_joined_entries` adapter does.
2562
+ return _BLOCK_TOTALS_ENGINE.totals(
2563
+ block_start_at, range_end, owner_key=owner_key,
2564
+ windows=ownership_windows, pricing_evidence=pricing_evidence,
2565
+ owned_entries=owned_entries,
2566
+ )
2567
+ return _block_totals_reprice(
2568
+ block_start_at, range_end, owner_key=owner_key,
2569
+ ownership_windows=ownership_windows, skip_sync=skip_sync,
2570
+ pricing_evidence=pricing_evidence, owned_entries=owned_entries,
2571
+ )
2572
+
2518
2573
 
2574
+ def _block_totals_reprice(
2575
+ block_start_at: dt.datetime,
2576
+ range_end: dt.datetime,
2577
+ *,
2578
+ owner_key: Any,
2579
+ ownership_windows: "list[Any]",
2580
+ skip_sync: bool = False,
2581
+ pricing_evidence: "dict | None" = None,
2582
+ owned_entries: "list | None" = None,
2583
+ ) -> dict[str, Any]:
2584
+ """`_compute_block_totals`' full re-price: load every entry of the range,
2585
+ partition by owner, price and fold each owned one. The engine-free path,
2586
+ and the engine's own answer for a range too large to cache and its
2587
+ shadow check (#1003 D1)."""
2519
2588
  missing_without_raw = set()
2520
2589
  observed_raw = set()
2521
2590
 
@@ -6664,8 +6733,8 @@ def _record_usage_bound_locked(
6664
6733
  section stalls every other process contending on the same lock. There are
6665
6734
  two such locks and both supply a sink: ``_selected_state_lock`` (the OAuth
6666
6735
  refresh and the authoritative statusline publication) and the statusline
6667
- persist flock (``_statusline_reduce_and_publish``, reached from the forked
6668
- persist child and from the ``sync_for_test`` foreground path)."""
6736
+ persist flock (``_statusline_reduce_and_publish``, reached from the exec'd
6737
+ continuation worker and from the ``sync_for_test`` foreground path)."""
6669
6738
 
6670
6739
  # ULP-noise sanitization is applied at the cmd_record_usage ingress
6671
6740
  # boundary so every downstream consumer (HWM files, DB rows,
@@ -7539,6 +7608,16 @@ def _cmd_hook_tick_codex(
7539
7608
  )
7540
7609
  finally:
7541
7610
  cache.close()
7611
+ if stats.lock_contended and getattr(stats, "replay_refill", False):
7612
+ # #982 spec 4.4: a replay refill holds the locks. Every
7613
+ # budgeted tick declines until it ends, so a waiting
7614
+ # request would only make the refill units idle for its
7615
+ # 60 s lifetime. Request the worker instead, as the
7616
+ # declining tick does, and publish nothing.
7617
+ cache_mod._defer_codex_replay_drain()
7618
+ log_outcome(sync="contended", result="noop",
7619
+ reason="replay_pending")
7620
+ return 0
7542
7621
  if stats.lock_contended:
7543
7622
  # #968 spec 5.7: publish this hook's own waiting request so
7544
7623
  # drain workers yield once their admitted work commits. The
@@ -7551,6 +7630,20 @@ def _cmd_hook_tick_codex(
7551
7630
  drain_request="published" if request_id else "failed")
7552
7631
  return 0
7553
7632
  walk_deferral = getattr(stats, "deferred_reason", None)
7633
+ if (walk_deferral == "replay_pending"
7634
+ and getattr(stats, "replay_refill", False)):
7635
+ # #982 spec 4.4: a byte-zero replay's store is cleared and
7636
+ # only partly refilled by the heal executor, so neither the
7637
+ # quota projection nor the budget leg may read it. The tick
7638
+ # declines like today's replay-pending tick (the sync
7639
+ # recorded the deferral) and requests the worker, which
7640
+ # resumes the executor from the replay record.
7641
+ cache_mod._defer_codex_replay_drain()
7642
+ log_outcome(
7643
+ sync="deferred", result="noop",
7644
+ reason="replay_pending",
7645
+ backlog=int(getattr(stats, "backlog_files", 0) or 0))
7646
+ return 0
7554
7647
  if walk_deferral in ("quota_index_pending", "inventory_pending"):
7555
7648
  # #977 C5 / #927 B4: the walk stopped before ingesting
7556
7649
  # (the quota natural-key index or the retained-identity
@@ -7858,11 +7951,25 @@ def cmd_hook_tick(args: argparse.Namespace) -> int:
7858
7951
  cache_conn = open_cache_db()
7859
7952
  try:
7860
7953
  cache_mod = _cctally()._load_sibling("_cctally_cache")
7954
+ # #1055 spec 4.5: at most one drain wait per hook process.
7955
+ # Only the operation's first invocation may wait behind an
7956
+ # admitted Codex drain unit; a rerun after cache recovery
7957
+ # makes today's single attempt.
7958
+ claude_sync_calls = {"n": 0}
7959
+
7960
+ def _claude_sync(active_conn):
7961
+ claude_sync_calls["n"] += 1
7962
+ if claude_sync_calls["n"] > 1:
7963
+ return sync_cache(active_conn)
7964
+ import _lib_codex_drain_admission as _drain_adm
7965
+ return sync_cache(
7966
+ active_conn,
7967
+ drain_wait_seconds=(
7968
+ _drain_adm.CLAUDE_HOOK_DRAIN_WAIT_SECONDS))
7969
+
7861
7970
  stats, cache_conn = (
7862
7971
  cache_mod._run_cache_operation_with_recovery(
7863
- cache_conn,
7864
- lambda active_conn: sync_cache(active_conn),
7865
- origin="hook.claude.sync",
7972
+ cache_conn, _claude_sync, origin="hook.claude.sync",
7866
7973
  )
7867
7974
  )
7868
7975
  ingested = int(stats.rows_changed)
@@ -7893,11 +8000,27 @@ def cmd_hook_tick(args: argparse.Namespace) -> int:
7893
8000
  )
7894
8001
  except OSError:
7895
8002
  lock_fd = -1
8003
+ contended = False
7896
8004
  try:
7897
8005
  if lock_fd >= 0:
7898
- fcntl.flock(lock_fd, fcntl.LOCK_EX)
8006
+ # #1064 spec 5.4: admission is non-blocking. Another
8007
+ # usage refresh holding the lock (a hook-tick or the
8008
+ # statusline's OAuth child) is already fetching, or waits
8009
+ # for a reducer pass on the selected state, so this one
8010
+ # skips the OAuth leg rather than queue behind it:
8011
+ # no fetch, no throttle-marker touch, no retry. Any other
8012
+ # OSError keeps the internal-error handling below.
8013
+ try:
8014
+ fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
8015
+ except BlockingIOError:
8016
+ contended = True
8017
+ # The marker's age needs no lock; read on contention too so
8018
+ # --explain prints it.
7899
8019
  pre_age = _hook_tick_throttle_age_seconds()
7900
- if pre_age >= throttle_seconds:
8020
+ if contended:
8021
+ decision = "throttled(inflight)"
8022
+ oauth_status = "throttled(inflight)"
8023
+ elif pre_age >= throttle_seconds:
7901
8024
  decision = "fetch"
7902
8025
  oauth_status, _ = _hook_tick_oauth_refresh(throttle_seconds=throttle_seconds)
7903
8026
  if oauth_status.startswith("ok"):
@@ -7906,10 +8029,11 @@ def cmd_hook_tick(args: argparse.Namespace) -> int:
7906
8029
  oauth_status = f"throttled(age={int(pre_age)}s)"
7907
8030
  finally:
7908
8031
  if lock_fd >= 0:
7909
- try:
7910
- fcntl.flock(lock_fd, fcntl.LOCK_UN)
7911
- except OSError:
7912
- pass
8032
+ if not contended:
8033
+ try:
8034
+ fcntl.flock(lock_fd, fcntl.LOCK_UN)
8035
+ except OSError:
8036
+ pass
7913
8037
  try:
7914
8038
  os.close(lock_fd)
7915
8039
  except OSError: