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.
- package/CHANGELOG.md +30 -0
- package/README.md +7 -3
- package/bin/_cctally_cache.py +2783 -175
- package/bin/_cctally_completion.py +106 -33
- package/bin/_cctally_core.py +69 -4
- package/bin/_cctally_dashboard.py +188 -13
- package/bin/_cctally_dashboard_conversation.py +5 -1
- package/bin/_cctally_dashboard_envelope.py +50 -19
- package/bin/_cctally_dashboard_sources.py +23 -1
- package/bin/_cctally_db.py +193 -153
- package/bin/_cctally_diagnosis_sources.py +63 -7
- package/bin/_cctally_doctor.py +865 -93
- package/bin/_cctally_five_hour.py +67 -50
- package/bin/_cctally_forecast.py +35 -13
- package/bin/_cctally_journal.py +186 -18
- package/bin/_cctally_milestone_history.py +859 -62
- package/bin/_cctally_oauth_samples.py +286 -15
- package/bin/_cctally_parser.py +2 -2
- package/bin/_cctally_quota.py +104 -12
- package/bin/_cctally_record.py +153 -29
- package/bin/_cctally_rederive.py +608 -68
- package/bin/_cctally_statusline.py +108 -81
- package/bin/_cctally_store.py +179 -38
- package/bin/_cctally_tui.py +892 -181
- package/bin/_cctally_update.py +32 -13
- package/bin/_lib_codex_conversation_query.py +52 -14
- package/bin/_lib_codex_drain_admission.py +224 -6
- package/bin/_lib_codex_metadata.py +39 -0
- package/bin/_lib_conversation_anon.py +15 -8
- package/bin/_lib_conversation_dispatch.py +7 -0
- package/bin/_lib_conversation_query.py +289 -70
- package/bin/_lib_dashboard_sources.py +37 -1
- package/bin/_lib_detector_access_registry.py +497 -26
- package/bin/_lib_doctor.py +85 -20
- package/bin/_lib_doctor_scan_index.py +321 -0
- package/bin/_lib_ingest_frontier.py +20 -1
- package/bin/_lib_oauth_samples.py +23 -7
- package/bin/_lib_pricing.py +146 -16
- package/bin/_lib_quota.py +19 -0
- package/bin/_lib_rederive.py +11 -6
- package/bin/_lib_retained_records.py +516 -0
- package/bin/_lib_snapshot_cache.py +10 -0
- package/bin/_lib_stale_proof.py +272 -43
- package/bin/_lib_stats_content_revision.py +831 -0
- package/bin/_lib_write_io.py +11 -4
- package/bin/cctally +4 -1
- package/dashboard/static/assets/ConversationsView-D4Iv4ku0.js +72 -0
- package/dashboard/static/assets/{DoctorModal-B0DnU9d9.js → DoctorModal-D4QSq6uz.js} +1 -1
- package/dashboard/static/assets/ModalRoot-C9F5dhZi.js +1 -0
- package/dashboard/static/assets/{ProjectsDrillPanel-DSxXQSa-.js → ProjectsDrillPanel-WxAdRDMt.js} +1 -1
- package/dashboard/static/assets/{SourceDetailModal-BleDpQqG.js → SourceDetailModal-NyyEfXmc.js} +1 -1
- package/dashboard/static/assets/{UpdateModal-CMqaAkSK.js → UpdateModal-CFLq9pT1.js} +1 -1
- package/dashboard/static/assets/{index-BjQV-0-D.js → index-BKTWH9JE.js} +2 -2
- package/dashboard/static/assets/{outlineNavigation-CO-VWBm7.js → outlineNavigation-BnGUwCS3.js} +3 -3
- package/dashboard/static/dashboard.html +1 -1
- package/package.json +4 -1
- package/dashboard/static/assets/ConversationsView-oqNsWzWV.js +0 -72
- package/dashboard/static/assets/ModalRoot-CYua2wP_.js +0 -1
package/bin/_cctally_record.py
CHANGED
|
@@ -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.
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
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
|
|
6668
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
7910
|
-
|
|
7911
|
-
|
|
7912
|
-
|
|
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:
|