cctally 1.108.0 → 1.109.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 +101 -0
- package/bin/_cctally_account.py +4 -0
- package/bin/_cctally_alerts.py +6 -2
- package/bin/_cctally_cache.py +1590 -153
- package/bin/_cctally_config.py +3 -2
- package/bin/_cctally_core.py +589 -32
- package/bin/_cctally_dashboard.py +1223 -160
- package/bin/_cctally_dashboard_conversation.py +263 -8
- package/bin/_cctally_dashboard_envelope.py +144 -4
- package/bin/_cctally_dashboard_share.py +154 -36
- package/bin/_cctally_dashboard_sources.py +2146 -347
- package/bin/_cctally_db.py +634 -5
- package/bin/_cctally_diagnosis_sources.py +57 -11
- package/bin/_cctally_doctor.py +84 -15
- package/bin/_cctally_five_hour.py +1295 -40
- package/bin/_cctally_forecast.py +109 -26
- package/bin/_cctally_journal.py +380 -37
- package/bin/_cctally_milestone_history.py +86 -6
- package/bin/_cctally_percent_breakdown.py +6 -1
- package/bin/_cctally_project.py +48 -76
- package/bin/_cctally_quota.py +39 -7
- package/bin/_cctally_quota_model.py +17 -2
- package/bin/_cctally_record.py +1603 -437
- package/bin/_cctally_refresh.py +2 -1
- package/bin/_cctally_share.py +24 -5
- package/bin/_cctally_source_analytics.py +553 -30
- package/bin/_cctally_statusline.py +83 -19
- package/bin/_cctally_store.py +61 -4
- package/bin/_cctally_tui.py +229 -74
- package/bin/_cctally_weekrefs.py +28 -5
- package/bin/_lib_blocks.py +127 -20
- package/bin/_lib_cache_report.py +20 -5
- package/bin/_lib_codex_conversation_query.py +11 -1
- package/bin/_lib_codex_hooks.py +102 -0
- package/bin/_lib_conversation.py +119 -0
- package/bin/_lib_conversation_dispatch.py +31 -1
- package/bin/_lib_conversation_query.py +61 -14
- package/bin/_lib_conversation_retention.py +413 -25
- package/bin/_lib_dashboard_sources.py +267 -7
- package/bin/_lib_diagnosis.py +10 -0
- package/bin/_lib_diff_kernel.py +12 -1
- package/bin/_lib_doctor.py +214 -6
- package/bin/_lib_ingest_frontier.py +1366 -49
- package/bin/_lib_journal.py +19 -2
- package/bin/_lib_merge_gate.py +307 -0
- package/bin/_lib_pricing.py +522 -18
- package/bin/_lib_record.py +130 -12
- package/bin/_lib_render.py +9 -3
- package/bin/_lib_retained_size.py +27 -2
- package/bin/_lib_segment_summary.py +66 -8
- package/bin/_lib_share_templates.py +28 -18
- package/bin/_lib_snapshot_cache.py +115 -1
- package/bin/_lib_source_retry.py +320 -0
- package/bin/_lib_statusline_candidates.py +400 -36
- package/bin/_lib_tick_stats.py +119 -0
- package/bin/_lib_view_models.py +251 -76
- package/bin/cctally +69 -0
- package/dashboard/static/assets/ConversationsView-TO5bpIyc.js +72 -0
- package/dashboard/static/assets/{DoctorModal-CWn3U4Wl.js → DoctorModal-BZGXOlc2.js} +1 -1
- package/dashboard/static/assets/ModalRoot-C_fvugTl.js +1 -0
- package/dashboard/static/assets/{ProjectsDrillPanel-QLI9i5mZ.js → ProjectsDrillPanel-KbO_KWa7.js} +1 -1
- package/dashboard/static/assets/SourceDetailModal-BrOPLVRL.js +1 -0
- package/dashboard/static/assets/{UpdateModal-D3m8GG6V.js → UpdateModal-CYgEFAi4.js} +2 -2
- package/dashboard/static/assets/dashboardStream.shared-worker-DbG6Ef0T.js +1 -0
- package/dashboard/static/assets/index-Cj_l7T7u.css +1 -0
- package/dashboard/static/assets/index-DLVwACUM.js +13 -0
- package/dashboard/static/assets/outlineNavigation-DXX-RjKl.js +9 -0
- package/dashboard/static/assets/useKeymap-DQx_A1MB.js +1 -0
- package/dashboard/static/dashboard.html +3 -3
- package/package.json +3 -1
- package/dashboard/static/assets/ConversationsView-BOSaBRtu.js +0 -72
- package/dashboard/static/assets/ModalRoot-SR070V6c.js +0 -1
- package/dashboard/static/assets/SourceDetailModal-pv0wlFxl.js +0 -1
- package/dashboard/static/assets/dashboardStream.shared-worker-1XTMV3nr.js +0 -1
- package/dashboard/static/assets/index-BHu4mxd8.css +0 -1
- package/dashboard/static/assets/index-CIWsbux3.js +0 -13
- package/dashboard/static/assets/outlineNavigation-CJvKqmLV.js +0 -9
- package/dashboard/static/assets/useKeymap-ffqsS5G0.js +0 -1
|
@@ -49,7 +49,6 @@ from _cctally_core import open_db, parse_iso_datetime
|
|
|
49
49
|
from _cctally_config import save_config, _load_config_unlocked
|
|
50
50
|
from _lib_fmt import stable_sum
|
|
51
51
|
from _lib_pricing import _calculate_entry_cost, claude_usage_dict
|
|
52
|
-
from _lib_five_hour import _canonical_5h_window_key
|
|
53
52
|
from _lib_display_tz import _resolve_tz, resolve_display_tz_name
|
|
54
53
|
|
|
55
54
|
|
|
@@ -525,48 +524,154 @@ def _share_per_day_per_project_for_range(
|
|
|
525
524
|
return out
|
|
526
525
|
|
|
527
526
|
|
|
527
|
+
def _share_block_ownership_context(range_start, range_end):
|
|
528
|
+
"""The canonical five-hour windows competing over ``[range_start,
|
|
529
|
+
range_end]``, as ``(windows, owner_key_by_block_start_epoch)``.
|
|
530
|
+
|
|
531
|
+
#751a: a block cannot decide what it owns without knowing what its
|
|
532
|
+
neighbours own, so the caller widens this range past what it displays
|
|
533
|
+
purely in order to exclude a predecessor's entries.
|
|
534
|
+
"""
|
|
535
|
+
import _lib_blocks
|
|
536
|
+
|
|
537
|
+
_selected, _overrides, canonical_intervals = (
|
|
538
|
+
sys.modules["cctally"]._load_recorded_five_hour_windows(
|
|
539
|
+
range_start, range_end,
|
|
540
|
+
)
|
|
541
|
+
)
|
|
542
|
+
windows = []
|
|
543
|
+
owner_key_by_start: dict[int, object] = {}
|
|
544
|
+
for anchor, (block_start, reset) in canonical_intervals.items():
|
|
545
|
+
windows.append(_lib_blocks.OwnedWindow(
|
|
546
|
+
key=anchor, start=block_start, reset=reset,
|
|
547
|
+
))
|
|
548
|
+
owner_key_by_start[int(block_start.timestamp())] = anchor
|
|
549
|
+
return windows, owner_key_by_start
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def _share_entry_cost(entry) -> float:
|
|
553
|
+
usage = claude_usage_dict( # #195 chokepoint
|
|
554
|
+
input_tokens=entry.input_tokens,
|
|
555
|
+
output_tokens=entry.output_tokens,
|
|
556
|
+
cache_creation_tokens=entry.cache_creation_tokens,
|
|
557
|
+
cache_read_tokens=entry.cache_read_tokens,
|
|
558
|
+
cache_1h_tokens=getattr(entry, "cache_1h_tokens", None),
|
|
559
|
+
speed=getattr(entry, "speed", None),
|
|
560
|
+
)
|
|
561
|
+
return _calculate_entry_cost(
|
|
562
|
+
entry.model, usage, mode="auto", cost_usd=entry.cost_usd,
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def _share_per_block_per_project_by_ownership(starts, iso_by_start_epoch):
|
|
567
|
+
"""Per-block per-project costs over the entries each block OWNS.
|
|
568
|
+
|
|
569
|
+
The rollup-table path above is the canonical source; this runs only
|
|
570
|
+
when that table is empty or unreadable. It used to sweep each block's
|
|
571
|
+
raw ``[start, start + 5h]`` interval through
|
|
572
|
+
``_share_all_projects_for_range``, which is an independent
|
|
573
|
+
reselection: two windows overlapping after a reset shift each claimed
|
|
574
|
+
the overlap entries, and the artifact's per-block per-project
|
|
575
|
+
breakdown double-counted them (#751a review P2-1).
|
|
576
|
+
|
|
577
|
+
Ownership is resolved once, by the one rule in
|
|
578
|
+
``_lib_blocks.partition_entries_by_owner``. A block with no canonical
|
|
579
|
+
row is heuristic, so it reads the leftovers — the entries no exact
|
|
580
|
+
window contains — inside its own interval, which is the grouping
|
|
581
|
+
pass's own treatment of them.
|
|
582
|
+
"""
|
|
583
|
+
import _lib_blocks
|
|
584
|
+
|
|
585
|
+
five_hours = dt.timedelta(hours=5)
|
|
586
|
+
earliest, latest = min(starts), max(starts)
|
|
587
|
+
out: dict[str, dict[str, float]] = {
|
|
588
|
+
iso: {} for iso in iso_by_start_epoch.values()
|
|
589
|
+
}
|
|
590
|
+
try:
|
|
591
|
+
windows, owner_key_by_start = _share_block_ownership_context(
|
|
592
|
+
earliest - five_hours, latest + 2 * five_hours,
|
|
593
|
+
)
|
|
594
|
+
# late-binding: reach the dashboard module object so both the ns-patch
|
|
595
|
+
# (share tests) and a dashboard-object patch (rebuild-parity) are
|
|
596
|
+
# honored (#279 S5 F1 / spec §3).
|
|
597
|
+
entries = list(
|
|
598
|
+
sys.modules["_cctally_dashboard"].get_claude_session_entries(
|
|
599
|
+
earliest, latest + 2 * five_hours, skip_sync=True,
|
|
600
|
+
)
|
|
601
|
+
)
|
|
602
|
+
except Exception:
|
|
603
|
+
return out
|
|
604
|
+
owned, leftovers = _lib_blocks.partition_entries_by_owner(entries, windows)
|
|
605
|
+
for start in starts:
|
|
606
|
+
block_iso = iso_by_start_epoch[int(start.timestamp())]
|
|
607
|
+
owner_key = owner_key_by_start.get(int(start.timestamp()))
|
|
608
|
+
if owner_key is None:
|
|
609
|
+
mine = [
|
|
610
|
+
e for e in leftovers
|
|
611
|
+
if start <= e.timestamp < start + five_hours
|
|
612
|
+
]
|
|
613
|
+
else:
|
|
614
|
+
mine = owned.get(owner_key, [])
|
|
615
|
+
bucket = out[block_iso]
|
|
616
|
+
for entry in mine:
|
|
617
|
+
key = entry.project_path or "(unknown)"
|
|
618
|
+
bucket[key] = bucket.get(key, 0.0) + _share_entry_cost(entry)
|
|
619
|
+
return out
|
|
620
|
+
|
|
621
|
+
|
|
528
622
|
def _share_per_block_per_project(
|
|
529
623
|
recent_blocks: list[dict],
|
|
530
624
|
) -> dict[str, dict[str, float]]:
|
|
531
625
|
"""Aggregate per-block per-project costs from `five_hour_block_projects`.
|
|
532
626
|
|
|
533
627
|
Returns {block_start_at_iso: {project_path_or_'(unknown)': cost_usd}}.
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
628
|
+
|
|
629
|
+
The rollup rows are reached through their parent's `block_start_at`
|
|
630
|
+
rather than through a window key derived here. `five_hour_block_projects.
|
|
631
|
+
five_hour_window_key` is canonicalized from the window's RESET, so a key
|
|
632
|
+
derived from the block's START missed every row by the block duration and
|
|
633
|
+
the lookup never resolved — which made the fallback the effective path
|
|
634
|
+
(#751a review P2-1). Resolving the parent by its own recorded start is a
|
|
635
|
+
lookup rather than a second derivation, so no third key shape is created
|
|
636
|
+
(CLAUDE.md "5-hour windows").
|
|
637
|
+
|
|
638
|
+
Fallback (rollup empty/unreadable): `_share_per_block_per_project_by_
|
|
639
|
+
ownership`, which prices the entries each block owns. Fires only during
|
|
640
|
+
the first tick after fresh install or before stats-migration
|
|
641
|
+
`002_five_hour_block_projects_backfill_v1` completes. Issue #33.
|
|
543
642
|
"""
|
|
544
643
|
if not recent_blocks:
|
|
545
644
|
return {}
|
|
546
645
|
out: dict[str, dict[str, float]] = {}
|
|
547
|
-
|
|
548
|
-
|
|
646
|
+
starts: list[dt.datetime] = []
|
|
647
|
+
iso_by_start_epoch: dict[int, str] = {}
|
|
549
648
|
for b in recent_blocks:
|
|
550
649
|
try:
|
|
551
650
|
ts = parse_iso_datetime(b["start_at"], "share.block.start_at")
|
|
552
651
|
except (ValueError, KeyError):
|
|
553
652
|
continue
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
if not
|
|
653
|
+
ts = ts.astimezone(dt.timezone.utc)
|
|
654
|
+
starts.append(ts)
|
|
655
|
+
iso_by_start_epoch[int(ts.timestamp())] = b["start_at"]
|
|
656
|
+
if not starts:
|
|
558
657
|
return out
|
|
658
|
+
epochs = list(iso_by_start_epoch)
|
|
559
659
|
try:
|
|
560
660
|
conn = open_db()
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
661
|
+
try:
|
|
662
|
+
placeholders = ",".join("?" for _ in epochs)
|
|
663
|
+
rows = conn.execute(
|
|
664
|
+
f"SELECT unixepoch(b.block_start_at) AS start_epoch, "
|
|
665
|
+
f" p.project_path, p.cost_usd "
|
|
666
|
+
f" FROM five_hour_block_projects p "
|
|
667
|
+
f" JOIN five_hour_blocks b ON b.id = p.block_id "
|
|
668
|
+
f" WHERE unixepoch(b.block_start_at) IN ({placeholders})",
|
|
669
|
+
epochs,
|
|
670
|
+
).fetchall()
|
|
671
|
+
finally:
|
|
672
|
+
conn.close()
|
|
673
|
+
for start_epoch, project_path, cost in rows:
|
|
674
|
+
block_iso = iso_by_start_epoch.get(int(start_epoch))
|
|
570
675
|
if block_iso is None:
|
|
571
676
|
continue
|
|
572
677
|
proj = project_path or "(unknown)"
|
|
@@ -576,15 +681,7 @@ def _share_per_block_per_project(
|
|
|
576
681
|
return out
|
|
577
682
|
except (sqlite3.DatabaseError, OSError):
|
|
578
683
|
pass
|
|
579
|
-
|
|
580
|
-
for b in recent_blocks:
|
|
581
|
-
try:
|
|
582
|
-
ts = parse_iso_datetime(b["start_at"], "share.block.start_at")
|
|
583
|
-
except (ValueError, KeyError):
|
|
584
|
-
continue
|
|
585
|
-
end = ts + dt.timedelta(hours=5)
|
|
586
|
-
out[b["start_at"]] = sys.modules["cctally"]._share_all_projects_for_range(ts, end)
|
|
587
|
-
return out
|
|
684
|
+
return _share_per_block_per_project_by_ownership(starts, iso_by_start_epoch)
|
|
588
685
|
|
|
589
686
|
|
|
590
687
|
def _build_share_panel_data(panel: str, options: dict,
|
|
@@ -1327,7 +1424,28 @@ def _build_projects_share_panel_data(options: dict,
|
|
|
1327
1424
|
week_end = cw_start + dt.timedelta(days=7)
|
|
1328
1425
|
now = _share_now_utc()
|
|
1329
1426
|
period_end = week_end if week_end <= now else now
|
|
1330
|
-
|
|
1427
|
+
# #750 S4 §4.7: the period start comes from the SELECTED INTERVAL
|
|
1428
|
+
# IDENTITIES, not from `cw_start - 7 * (effective_weeks - 1)`. A credited
|
|
1429
|
+
# week adds a billing cycle without adding seven calendar days, so the
|
|
1430
|
+
# seven-day arithmetic advertises a period WIDER than the data the
|
|
1431
|
+
# artifact aggregated the moment one is inside the window. The instants
|
|
1432
|
+
# come from `projects.trend.weeks[].week_start_at` (§2.2). The old
|
|
1433
|
+
# arithmetic remains the fallback for an envelope that predates the
|
|
1434
|
+
# field, where it is still the best available answer.
|
|
1435
|
+
_selected_starts = [
|
|
1436
|
+
w.get("week_start_at") for w in (trend.get("weeks") or [])
|
|
1437
|
+
if isinstance(w, dict) and w.get("week_start_at")
|
|
1438
|
+
][-effective_weeks:]
|
|
1439
|
+
period_start = None
|
|
1440
|
+
if _selected_starts:
|
|
1441
|
+
try:
|
|
1442
|
+
period_start = parse_iso_datetime(
|
|
1443
|
+
_selected_starts[0], "projects.trend.week_start_at",
|
|
1444
|
+
)
|
|
1445
|
+
except (TypeError, ValueError):
|
|
1446
|
+
period_start = None
|
|
1447
|
+
if period_start is None:
|
|
1448
|
+
period_start = cw_start - dt.timedelta(days=7 * (effective_weeks - 1))
|
|
1331
1449
|
|
|
1332
1450
|
return {
|
|
1333
1451
|
"rows": rows,
|
|
@@ -1533,7 +1651,7 @@ def _share_codex_state_for_period(data_snap, *, panel: str, options: dict):
|
|
|
1533
1651
|
),
|
|
1534
1652
|
data_version=(
|
|
1535
1653
|
f"share:codex:{panel}:{range_start.isoformat()}:"
|
|
1536
|
-
f"{now_override.isoformat()}:{semantics.
|
|
1654
|
+
f"{now_override.isoformat()}:{semantics.codex_identity}"
|
|
1537
1655
|
),
|
|
1538
1656
|
)
|
|
1539
1657
|
finally:
|