cctally 1.64.0 → 1.65.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 +17 -0
- package/bin/_cctally_cache.py +60 -19
- package/bin/_cctally_dashboard.py +364 -38
- package/bin/_cctally_tui.py +445 -236
- package/bin/_cctally_update.py +5 -0
- package/bin/_lib_conversation_query.py +349 -36
- package/bin/_lib_perf.py +180 -0
- package/bin/_lib_transcript_access.py +23 -0
- package/dashboard/static/assets/index-CJTUCpKt.js +80 -0
- package/dashboard/static/assets/index-DQWNrIqu.css +1 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +2 -1
- package/dashboard/static/assets/index-0jzYm75p.css +0 -1
- package/dashboard/static/assets/index-D_Ylyqsf.js +0 -80
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,23 @@ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [1.65.0] - 2026-07-09
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Opt-in backend performance instrumentation: set `CCTALLY_PERF_TRACE=1` to print a phase-timing trace to stderr on `cache-sync` (and the hidden `tui --render-once` build), and read live backend timings + cache state from the dashboard's loopback-only `/api/debug/backend` endpoint. The trace is off by default and never changes command output — it goes to stderr only, so stdout stays byte-identical. New [docs/backend-performance.md](docs/backend-performance.md) documents the read model, hot paths, and invariants.
|
|
12
|
+
- Reproducible backend benchmark suite (`bin/cctally-bench` + `bin/build-bench-fixtures.py`, a dev/maintainer tool — never shipped): in-process timings for the dashboard snapshot (cold/warm/idle), cache ingest (no-op/delta), the conversation rail/search/find/payload/outline, and the warm reconcile paths, measured against a deterministic synthetic fixture, with a committed advisory baseline and an opt-in `--compare`/`--gate` (never a CI gate) that guard the recent backend performance wins from silent regression. See [bench/README.md](bench/README.md).
|
|
13
|
+
- The opt-in `CCTALLY_PERF_TRACE` backend trace now breaks whole-session conversation assembly into named sub-phases (read, dedup, build, correlate, fold, classify, cost, finalize) under the reader's detail/paginate phases, and the loopback `/api/debug/backend` diagnostic can now surface a conversation-open trace (list/search/outline/find/export/prompts/detail) as well as the data-snapshot build — so a slow transcript open is attributable to a specific stage on a live dashboard; off by default, no command output changes.
|
|
14
|
+
- `cctally-bench --assembly-scan` (dev/maintainer tool): sweeps the cost of assembling a whole conversation across a size ladder against its own isolated fixture and baseline (`bench/baselines/assembly.json`), the measurement behind the recorded decision to defer materializing pre-rendered conversation turns — assembly stays well under a 100 ms visibility budget for all but pathologically large (multi-thousand-turn) sessions, and the reader already caps payload bytes. See [docs/backend-performance.md](docs/backend-performance.md) §5.
|
|
15
|
+
- The conversation browse rail and cross-session search can now be filtered by **model family**: a new "Model" section in the filter popover lists the Opus / Sonnet / Haiku / Fable families that actually appear (each with a session count), and selecting one or more scopes both browsing and search to sessions that used that model — anywhere in the session, including its subagents — with removable chips and "Clear all" just like the other filters (#278).
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- Conversation reader no longer re-processes an open conversation on every background refresh — it now recomputes only when the conversation actually grows, cutting idle CPU/battery use for anyone leaving a transcript open (#278).
|
|
19
|
+
- The dashboard now opens instantly on a heavy-history instance: it binds and paints the current-week and forecast panels almost immediately instead of waiting for the full ~2s data aggregation, and the heavier panels (sessions, projects, weekly, monthly, blocks, daily, trend, cache report) hydrate over the live stream showing a brief loading skeleton (respecting reduced-motion) rather than flashing a misleading "no data" / "restart the dashboard" empty state that then fills in (#278).
|
|
20
|
+
- A first-run or long-gap dashboard now fills in progressively as session history is ingested, instead of sitting blank and then snapping to fully-loaded in one jump; a warm returning session is unaffected, and a slow or backgrounded browser tab always converges to the latest data instead of getting stuck on a stale frame (#278).
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- The conversation filter popover now retries once if its filter options (the Project and Model lists) fail to load on a transient hiccup, instead of coming up empty and staying that way until it is reopened (#278).
|
|
24
|
+
|
|
8
25
|
## [1.64.0] - 2026-07-07
|
|
9
26
|
|
|
10
27
|
### Added
|
package/bin/_cctally_cache.py
CHANGED
|
@@ -176,6 +176,12 @@ _should_replace = _lib_jsonl._should_replace
|
|
|
176
176
|
_lib_conversation = _load_lib("_lib_conversation")
|
|
177
177
|
_iter_message_rows = _lib_conversation.iter_message_rows
|
|
178
178
|
|
|
179
|
+
# Opt-in backend phase-instrumentation collector (issue #276, Session A). Pure
|
|
180
|
+
# stdlib leaf; near-noop when CCTALLY_PERF_TRACE is unset (phase() returns a
|
|
181
|
+
# shared no-op singleton), so the sync_cache seam wraps below cost nothing on
|
|
182
|
+
# the default path.
|
|
183
|
+
_perf = _load_lib("_lib_perf")
|
|
184
|
+
|
|
179
185
|
# Shared by the fused per-file walk AND backfill_conversation_messages so the
|
|
180
186
|
# column list, placeholders, and tuple order live in ONE place — a column
|
|
181
187
|
# add/reorder can't silently desync the two ingest paths (which would land
|
|
@@ -857,7 +863,10 @@ def sync_cache(
|
|
|
857
863
|
|
|
858
864
|
lock_fh = open(_cctally_core.CACHE_LOCK_PATH, "w")
|
|
859
865
|
try:
|
|
860
|
-
|
|
866
|
+
with _perf.phase("flock") as _p_flock:
|
|
867
|
+
_acquired = _acquire_cache_flock(lock_fh, timeout=lock_timeout)
|
|
868
|
+
_p_flock.set_meta(contended=not _acquired)
|
|
869
|
+
if not _acquired:
|
|
861
870
|
eprint("[cache] sync already in progress; using existing cache")
|
|
862
871
|
stats.lock_contended = True
|
|
863
872
|
return stats
|
|
@@ -1044,6 +1053,12 @@ def sync_cache(
|
|
|
1044
1053
|
# path, which already cleared the flag and repopulates via the normal
|
|
1045
1054
|
# walk). A path-less/:memory: conn has no cache_meta only if the schema
|
|
1046
1055
|
# was never applied; the try/except tolerates that.
|
|
1056
|
+
# #276 perf: bracket the (rare, upgrade-only) backfill/reingest region
|
|
1057
|
+
# as one coarse "backfills" phase. Opened via the context-manager
|
|
1058
|
+
# protocol rather than a ``with`` block so the ~150-line body below is
|
|
1059
|
+
# not reindented. Near-noop when tracing is off (_NULL_PHASE).
|
|
1060
|
+
_p_backfills = _perf.phase("backfills")
|
|
1061
|
+
_p_backfills.__enter__()
|
|
1047
1062
|
if not rebuild and not targeted:
|
|
1048
1063
|
try:
|
|
1049
1064
|
_pending = conn.execute(
|
|
@@ -1191,12 +1206,15 @@ def sync_cache(
|
|
|
1191
1206
|
# IGNORE). Touches ONLY conversation_file_touches, never
|
|
1192
1207
|
# conversation_messages (P1-2).
|
|
1193
1208
|
_consume_file_touches(conn)
|
|
1209
|
+
_p_backfills.__exit__(None, None, None)
|
|
1194
1210
|
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1211
|
+
with _perf.phase("discover") as _p_disc:
|
|
1212
|
+
if targeted:
|
|
1213
|
+
paths = [pathlib.Path(p) for p in only_paths if pathlib.Path(p).is_file()]
|
|
1214
|
+
else:
|
|
1215
|
+
paths = list(_iter_claude_jsonl_files())
|
|
1216
|
+
stats.files_total = len(paths)
|
|
1217
|
+
_p_disc.set_count(len(paths))
|
|
1200
1218
|
|
|
1201
1219
|
# This SELECT does NOT open an implicit transaction (Python's
|
|
1202
1220
|
# sqlite3 module only BEGINs on DML). Do NOT add any INSERT/
|
|
@@ -1406,6 +1424,12 @@ def sync_cache(
|
|
|
1406
1424
|
# zero-write-lock read/parse region, so it adds no DML there.
|
|
1407
1425
|
touched_sessions: set = set()
|
|
1408
1426
|
|
|
1427
|
+
# #276 perf: bracket the fused per-file ingest loop as ONE coarse
|
|
1428
|
+
# "walk" phase (never per-row — Section 2 rule: volume is a count, not
|
|
1429
|
+
# N timed phases). Opened via the context-manager protocol so the hot
|
|
1430
|
+
# loop body below is not reindented; counts recorded after the loop.
|
|
1431
|
+
_p_walk = _perf.phase("walk")
|
|
1432
|
+
_p_walk.__enter__()
|
|
1409
1433
|
for jp in paths:
|
|
1410
1434
|
path_str = str(jp)
|
|
1411
1435
|
# Backfill session_id/project_path for A2 `session` subcommand.
|
|
@@ -1683,6 +1707,13 @@ def sync_cache(
|
|
|
1683
1707
|
|
|
1684
1708
|
if progress is not None:
|
|
1685
1709
|
progress(stats)
|
|
1710
|
+
_p_walk.__exit__(None, None, None)
|
|
1711
|
+
_p_walk.set_count(stats.files_processed)
|
|
1712
|
+
_p_walk.set_meta(
|
|
1713
|
+
skipped=stats.files_skipped_unchanged,
|
|
1714
|
+
failed=stats.files_failed,
|
|
1715
|
+
rows=stats.rows_changed,
|
|
1716
|
+
)
|
|
1686
1717
|
|
|
1687
1718
|
# Browse-rail rollup maintenance (single post-walk recompute, under the
|
|
1688
1719
|
# still-held flock, after every per-file commit and before the
|
|
@@ -1698,16 +1729,17 @@ def sync_cache(
|
|
|
1698
1729
|
# ~1 session/tick). Both recomputes derive COUNT/MIN/MAX from the same
|
|
1699
1730
|
# rows the rail's old live aggregate read, so the rollup stays
|
|
1700
1731
|
# byte-identical to that aggregate.
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1732
|
+
with _perf.phase("recompute.conversation_sessions"):
|
|
1733
|
+
if _conversation_sessions_backfill_pending(conn):
|
|
1734
|
+
_recompute_conversation_sessions(conn)
|
|
1735
|
+
conn.execute(
|
|
1736
|
+
"DELETE FROM cache_meta "
|
|
1737
|
+
"WHERE key='conversation_sessions_backfill_pending'"
|
|
1738
|
+
)
|
|
1739
|
+
conn.commit()
|
|
1740
|
+
elif touched_sessions:
|
|
1741
|
+
_recompute_conversation_sessions(conn, touched_sessions)
|
|
1742
|
+
conn.commit()
|
|
1711
1743
|
|
|
1712
1744
|
# Walk-complete sentinel write (cctally-dev#93, D5a). Still inside the
|
|
1713
1745
|
# held fcntl lock, before the finally-unlock. Only when the entire walk
|
|
@@ -3370,6 +3402,10 @@ def cmd_cache_sync(args: argparse.Namespace) -> int:
|
|
|
3370
3402
|
default is 'all'.
|
|
3371
3403
|
"""
|
|
3372
3404
|
source = getattr(args, "source", "all")
|
|
3405
|
+
# #276 perf: clear any prior tree on this thread so a leaked root can't be
|
|
3406
|
+
# flushed, then (below) time the Claude sync_cache call as the "sync_cache"
|
|
3407
|
+
# root phase and flush the tree to stderr when CCTALLY_PERF_TRACE is set.
|
|
3408
|
+
_perf.reset_thread()
|
|
3373
3409
|
conn = open_cache_db()
|
|
3374
3410
|
|
|
3375
3411
|
# --prune-orphans: fast, targeted cleanup of cache rows whose source
|
|
@@ -3419,9 +3455,10 @@ def cmd_cache_sync(args: argparse.Namespace) -> int:
|
|
|
3419
3455
|
contended = False
|
|
3420
3456
|
|
|
3421
3457
|
if source in ("claude", "all"):
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3458
|
+
with _perf.phase("sync_cache"):
|
|
3459
|
+
stats = sync_cache(
|
|
3460
|
+
conn, progress=_progress_stderr, rebuild=args.rebuild, lock_timeout=lt
|
|
3461
|
+
)
|
|
3425
3462
|
_progress_stderr(stats, force=True)
|
|
3426
3463
|
if stats.lock_contended and args.rebuild:
|
|
3427
3464
|
eprint(
|
|
@@ -3456,4 +3493,8 @@ def cmd_cache_sync(args: argparse.Namespace) -> int:
|
|
|
3456
3493
|
f"{stats.rows_changed} rows changed"
|
|
3457
3494
|
)
|
|
3458
3495
|
|
|
3496
|
+
# #276 perf: when tracing is enabled, flush the completed "sync_cache"
|
|
3497
|
+
# phase tree to stderr (stdout stays byte-identical). No-op when off.
|
|
3498
|
+
if _perf.enabled():
|
|
3499
|
+
_perf.flush_stderr(_perf.current_root())
|
|
3459
3500
|
return 1 if contended else 0
|