cctally 1.76.0 → 1.77.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 +11 -0
- package/bin/_cctally_cache.py +918 -143
- package/bin/_cctally_core.py +12 -1
- package/bin/_cctally_dashboard.py +49 -13
- package/bin/_cctally_dashboard_conversation.py +40 -17
- package/bin/_cctally_dashboard_sources.py +18 -7
- package/bin/_cctally_db.py +207 -5
- package/bin/_cctally_doctor.py +37 -7
- package/bin/_cctally_parser.py +1 -1
- package/bin/_cctally_quota.py +23 -24
- package/bin/_cctally_transcript.py +4 -4
- package/bin/_cctally_tui.py +6 -34
- package/bin/_lib_codex_conversation_query.py +19 -3
- package/bin/_lib_conversation_query.py +8 -3
- package/bin/_lib_conversation_retention.py +3 -3
- package/bin/_lib_doctor.py +53 -4
- package/bin/cctally +3 -0
- package/dashboard/static/assets/index-CvcCeA4L.css +1 -0
- package/dashboard/static/assets/index-DLNXAevv.js +87 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +1 -1
- package/dashboard/static/assets/index-CsDAxBT5.js +0 -80
- package/dashboard/static/assets/index-yftBNnLR.css +0 -1
package/bin/_cctally_doctor.py
CHANGED
|
@@ -429,16 +429,35 @@ def doctor_gather_state(
|
|
|
429
429
|
# Conversation-sessions rollup consistency (#217 S1 / U9). Two cheap COUNTs
|
|
430
430
|
# (graceful None on a missing table / unreadable DB) + an in-progress signal
|
|
431
431
|
# so a transient mid-sync mismatch never WARNs. The in-progress signal is a
|
|
432
|
-
# NON-BLOCKING
|
|
432
|
+
# NON-BLOCKING conversations flock probe (a writer mid-walk holds it) OR the
|
|
433
433
|
# presence of any pending reingest/split/backfill cache_meta flag — doctor
|
|
434
434
|
# stays read-only and never blocks on the lock.
|
|
435
435
|
conv_sessions_rollup_count = None
|
|
436
436
|
conv_messages_distinct_sessions = None
|
|
437
437
|
conv_rollup_sync_in_progress = False
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
438
|
+
conversations_db_page_count = None
|
|
439
|
+
conversations_db_freelist_count = None
|
|
440
|
+
try:
|
|
441
|
+
if _cctally_core.CONVERSATIONS_DB_PATH.exists():
|
|
442
|
+
# This gather also runs inside dashboard snapshot precompute. A
|
|
443
|
+
# transcript writer may hold an exclusive SQLite lock, so use a
|
|
444
|
+
# read-only zero-timeout probe: conversation health can degrade,
|
|
445
|
+
# but it must never delay core snapshot freshness (#320).
|
|
446
|
+
conv_uri = (
|
|
447
|
+
_cctally_core.CONVERSATIONS_DB_PATH.resolve().as_uri()
|
|
448
|
+
+ "?mode=ro"
|
|
449
|
+
)
|
|
450
|
+
conn = sqlite3.connect(conv_uri, uri=True, timeout=0.0)
|
|
441
451
|
try:
|
|
452
|
+
try:
|
|
453
|
+
row = conn.execute("PRAGMA page_count").fetchone()
|
|
454
|
+
if row and row[0] is not None:
|
|
455
|
+
conversations_db_page_count = int(row[0])
|
|
456
|
+
row = conn.execute("PRAGMA freelist_count").fetchone()
|
|
457
|
+
if row and row[0] is not None:
|
|
458
|
+
conversations_db_freelist_count = int(row[0])
|
|
459
|
+
except sqlite3.Error:
|
|
460
|
+
pass
|
|
442
461
|
try:
|
|
443
462
|
row = conn.execute(
|
|
444
463
|
"SELECT COUNT(*) FROM conversation_sessions"
|
|
@@ -472,12 +491,12 @@ def doctor_gather_state(
|
|
|
472
491
|
pass
|
|
473
492
|
finally:
|
|
474
493
|
conn.close()
|
|
475
|
-
# Non-blocking flock probe: if a writer
|
|
476
|
-
#
|
|
494
|
+
# Non-blocking flock probe: if a transcript writer/reingest holds the
|
|
495
|
+
# conversations.db lock, the rollup may be mid-recompute → in progress. We
|
|
477
496
|
# acquire LOCK_EX|LOCK_NB and immediately release; failure (held) is the
|
|
478
497
|
# signal. Never blocks (LOCK_NB), so doctor stays read-only + prompt.
|
|
479
498
|
if not conv_rollup_sync_in_progress:
|
|
480
|
-
lock_path = _cctally_core.
|
|
499
|
+
lock_path = _cctally_core.CONVERSATIONS_LOCK_PATH
|
|
481
500
|
if lock_path is not None and pathlib.Path(lock_path).exists():
|
|
482
501
|
import fcntl as _fcntl
|
|
483
502
|
lock_fh = open(str(lock_path), "w")
|
|
@@ -669,6 +688,15 @@ def doctor_gather_state(
|
|
|
669
688
|
for _name, _lp in (
|
|
670
689
|
("cache.db.lock", _cctally_core.CACHE_LOCK_PATH),
|
|
671
690
|
("cache.db.codex.lock", _cctally_core.CACHE_LOCK_CODEX_PATH),
|
|
691
|
+
("conversations.db.lock", _cctally_core.CONVERSATIONS_LOCK_PATH),
|
|
692
|
+
(
|
|
693
|
+
"conversations.db.codex.lock",
|
|
694
|
+
_cctally_core.CONVERSATIONS_LOCK_CODEX_PATH,
|
|
695
|
+
),
|
|
696
|
+
(
|
|
697
|
+
"conversations.db.maintenance.lock",
|
|
698
|
+
_cctally_core.CONVERSATIONS_LOCK_MAINTENANCE_PATH,
|
|
699
|
+
),
|
|
672
700
|
):
|
|
673
701
|
if not _lp.exists():
|
|
674
702
|
locks_held[_name] = False
|
|
@@ -877,6 +905,8 @@ def doctor_gather_state(
|
|
|
877
905
|
# #315: read-only cache free-page evidence for the reclaim hint.
|
|
878
906
|
cache_db_page_count=cache_db_page_count,
|
|
879
907
|
cache_db_freelist_count=cache_db_freelist_count,
|
|
908
|
+
conversations_db_page_count=conversations_db_page_count,
|
|
909
|
+
conversations_db_freelist_count=conversations_db_freelist_count,
|
|
880
910
|
codex_quota_windows=codex_quota_windows,
|
|
881
911
|
codex_hook_roots=codex_hook_roots,
|
|
882
912
|
codex_lifecycle_activity_24h=codex_lifecycle_activity_24h,
|
package/bin/_cctally_parser.py
CHANGED
|
@@ -2924,7 +2924,7 @@ def _build_db_parser(subparsers, name, *, help_text, xref=None):
|
|
|
2924
2924
|
)
|
|
2925
2925
|
db_vacuum.add_argument(
|
|
2926
2926
|
"--db",
|
|
2927
|
-
choices=("cache", "stats", "all"),
|
|
2927
|
+
choices=("cache", "conversations", "stats", "all"),
|
|
2928
2928
|
default="cache",
|
|
2929
2929
|
help="Which DB to VACUUM (default: cache)",
|
|
2930
2930
|
)
|
package/bin/_cctally_quota.py
CHANGED
|
@@ -279,33 +279,32 @@ def load_codex_quota_observations(
|
|
|
279
279
|
}
|
|
280
280
|
return required <= columns
|
|
281
281
|
|
|
282
|
-
has_conversation_events = has_columns(
|
|
283
|
-
"codex_conversation_events",
|
|
284
|
-
{"source_path", "line_offset", "record_type", "payload_json"},
|
|
285
|
-
)
|
|
286
282
|
has_session_entries = has_columns(
|
|
287
283
|
"codex_session_entries", {"source_path", "line_offset", "model"},
|
|
288
284
|
)
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
285
|
+
has_observed_model = has_columns(
|
|
286
|
+
"quota_window_snapshots", {"observed_model"},
|
|
287
|
+
)
|
|
288
|
+
entry_lookup = (
|
|
289
|
+
"(SELECT entries.model FROM codex_session_entries AS entries "
|
|
290
|
+
"WHERE entries.source_path=quota_window_snapshots.source_path "
|
|
291
|
+
"AND entries.line_offset<=quota_window_snapshots.line_offset "
|
|
292
|
+
"ORDER BY entries.line_offset DESC LIMIT 1)"
|
|
293
|
+
)
|
|
294
|
+
# A file's terminal model is not evidence for an earlier quota sample:
|
|
295
|
+
# the model may change later in the rollout. Prefer the compact stamp,
|
|
296
|
+
# then only a nearest-prior accounting context for legacy rows. If
|
|
297
|
+
# neither exists, leave the pool unscoped rather than fabricating it.
|
|
298
|
+
fallback_model = entry_lookup if has_session_entries else "NULL"
|
|
299
|
+
selected_model = (
|
|
300
|
+
f"COALESCE(quota_window_snapshots.observed_model,{fallback_model})"
|
|
301
|
+
if has_observed_model and fallback_model != "NULL"
|
|
302
|
+
else (
|
|
303
|
+
"quota_window_snapshots.observed_model"
|
|
304
|
+
if has_observed_model else fallback_model
|
|
305
|
+
)
|
|
306
|
+
)
|
|
307
|
+
model_expr = f"{selected_model} AS observed_model"
|
|
309
308
|
sql = """
|
|
310
309
|
SELECT source, source_root_key, source_path, line_offset,
|
|
311
310
|
captured_at_utc, observed_slot, logical_limit_key, limit_id,
|
|
@@ -86,7 +86,7 @@ def _cmd_transcript_export(args) -> int:
|
|
|
86
86
|
eprint(_SPEED_ONLY_CODEX_MSG)
|
|
87
87
|
return 2
|
|
88
88
|
|
|
89
|
-
conn = c.
|
|
89
|
+
conn = c.open_conversations_db()
|
|
90
90
|
try:
|
|
91
91
|
cq = c._load_sibling("_lib_conversation_query")
|
|
92
92
|
md = cq.get_conversation_export(conn, session_id, scope)
|
|
@@ -118,7 +118,7 @@ def _cmd_transcript_export_qualified(
|
|
|
118
118
|
return 2
|
|
119
119
|
speed = c._resolve_codex_speed(speed_arg or "auto")
|
|
120
120
|
|
|
121
|
-
conn = c.
|
|
121
|
+
conn = c.open_conversations_db()
|
|
122
122
|
try:
|
|
123
123
|
env = disp.neutral_export(
|
|
124
124
|
conn, session_id, scope=scope, effective_speed=speed)
|
|
@@ -189,7 +189,7 @@ def _cmd_transcript_search_claude(args) -> int:
|
|
|
189
189
|
eprint(f"transcript: {exc}")
|
|
190
190
|
return 2
|
|
191
191
|
|
|
192
|
-
conn = c.
|
|
192
|
+
conn = c.open_conversations_db()
|
|
193
193
|
try:
|
|
194
194
|
cq = c._load_sibling("_lib_conversation_query")
|
|
195
195
|
try:
|
|
@@ -264,7 +264,7 @@ def _cmd_transcript_search_codex(args) -> int:
|
|
|
264
264
|
eprint("transcript: invalid --cursor")
|
|
265
265
|
return 2
|
|
266
266
|
|
|
267
|
-
conn = c.
|
|
267
|
+
conn = c.open_conversations_db()
|
|
268
268
|
try:
|
|
269
269
|
result = disp.neutral_search(
|
|
270
270
|
conn, query, source="codex", kind=kind,
|
package/bin/_cctally_tui.py
CHANGED
|
@@ -1772,40 +1772,12 @@ def _tui_build_sessions(
|
|
|
1772
1772
|
(), now_utc=now_utc, limit=limit, display_tz=None,
|
|
1773
1773
|
aggregated_override=aggregated_override,
|
|
1774
1774
|
)
|
|
1775
|
-
|
|
1776
|
-
#
|
|
1777
|
-
#
|
|
1778
|
-
#
|
|
1779
|
-
#
|
|
1780
|
-
|
|
1781
|
-
# bounded indexed query (<= `limit` session ids, rides idx_conv_session_ts)
|
|
1782
|
-
# per snapshot build. Titles are stashed unconditionally on this
|
|
1783
|
-
# server-internal row; the privacy gate is applied later, at envelope
|
|
1784
|
-
# serialization (``snapshot_to_envelope(transcripts_visible=...)``).
|
|
1785
|
-
if rows:
|
|
1786
|
-
session_ids = [r.session_id for r in rows if r.session_id]
|
|
1787
|
-
try:
|
|
1788
|
-
conn = c.open_cache_db()
|
|
1789
|
-
except (sqlite3.DatabaseError, OSError):
|
|
1790
|
-
conn = None
|
|
1791
|
-
if conn is not None:
|
|
1792
|
-
try:
|
|
1793
|
-
titles = c._load_sibling(
|
|
1794
|
-
"_lib_conversation_query"
|
|
1795
|
-
)._session_titles_map(conn, session_ids)
|
|
1796
|
-
rows = [
|
|
1797
|
-
dataclasses.replace(r, title=titles.get(r.session_id))
|
|
1798
|
-
if r.session_id in titles else r
|
|
1799
|
-
for r in rows
|
|
1800
|
-
]
|
|
1801
|
-
except (sqlite3.DatabaseError, OSError):
|
|
1802
|
-
pass # fail-soft: leave titles None
|
|
1803
|
-
finally:
|
|
1804
|
-
try:
|
|
1805
|
-
conn.close()
|
|
1806
|
-
except sqlite3.Error:
|
|
1807
|
-
pass
|
|
1808
|
-
return rows
|
|
1775
|
+
# #320: transcript-derived titles are optional decoration. The core
|
|
1776
|
+
# dashboard/TUI snapshot must never open conversations.db, because even a
|
|
1777
|
+
# fail-soft read pays SQLite's lock timeout before it can fail. Conversation
|
|
1778
|
+
# routes retain title derivation; the accounting Sessions panel renders its
|
|
1779
|
+
# existing em-dash fallback when the independent store is unavailable.
|
|
1780
|
+
return list(view.rows)
|
|
1809
1781
|
|
|
1810
1782
|
|
|
1811
1783
|
def _tui_sessions_cached(
|
|
@@ -71,9 +71,25 @@ _SEARCH_BADGE = {
|
|
|
71
71
|
|
|
72
72
|
|
|
73
73
|
def codex_normalization_authoritative(conn: sqlite3.Connection) -> bool:
|
|
74
|
-
"""True iff
|
|
75
|
-
|
|
76
|
-
|
|
74
|
+
"""True iff the normalized Codex corpus is authoritative (§3.5).
|
|
75
|
+
|
|
76
|
+
Split stores use their provider-local rebuild marker: current schema alone
|
|
77
|
+
is not authority while migration 028's byte-zero replay is pending. Legacy
|
|
78
|
+
monolithic/bare connections retain the migration-025 stamp contract.
|
|
79
|
+
"""
|
|
80
|
+
try:
|
|
81
|
+
split = conn.execute(
|
|
82
|
+
"SELECT 1 FROM cache_meta "
|
|
83
|
+
"WHERE key='conversation_schema_version'"
|
|
84
|
+
).fetchone() is not None
|
|
85
|
+
if split:
|
|
86
|
+
pending = conn.execute(
|
|
87
|
+
"SELECT 1 FROM cache_meta "
|
|
88
|
+
"WHERE key='conversation_rebuild_codex_pending'"
|
|
89
|
+
).fetchone() is not None
|
|
90
|
+
return not pending
|
|
91
|
+
except sqlite3.OperationalError:
|
|
92
|
+
pass
|
|
77
93
|
try:
|
|
78
94
|
row = conn.execute(
|
|
79
95
|
"SELECT 1 FROM schema_migrations WHERE name = ?",
|
|
@@ -2048,7 +2048,9 @@ def _assemble_memo_clear() -> None:
|
|
|
2048
2048
|
def _assemble_memo_key(conn, session_id):
|
|
2049
2049
|
"""Return (session_id, msg_count, last_activity_utc, entry_mutation_seq), or
|
|
2050
2050
|
None to signal 'bypass the memo' — a missing rollup row or an unreadable
|
|
2051
|
-
cache_meta
|
|
2051
|
+
accounting ``cache_meta``. Split-store connections read that watermark
|
|
2052
|
+
from the read-only ``cache_db`` attachment; legacy/bare test connections
|
|
2053
|
+
retain the main-schema fallback. The caller has already confirmed the
|
|
2052
2054
|
rollup is authoritative."""
|
|
2053
2055
|
try:
|
|
2054
2056
|
row = conn.execute(
|
|
@@ -2059,9 +2061,12 @@ def _assemble_memo_key(conn, session_id):
|
|
|
2059
2061
|
if row is None:
|
|
2060
2062
|
return None
|
|
2061
2063
|
try:
|
|
2064
|
+
schemas = {row[1] for row in conn.execute("PRAGMA database_list")}
|
|
2065
|
+
meta_schema = "cache_db" if "cache_db" in schemas else "main"
|
|
2062
2066
|
seq_row = conn.execute(
|
|
2063
|
-
"SELECT value FROM cache_meta "
|
|
2064
|
-
"WHERE key='session_entries_mutation_seq'"
|
|
2067
|
+
f"SELECT value FROM {meta_schema}.cache_meta "
|
|
2068
|
+
"WHERE key='session_entries_mutation_seq'"
|
|
2069
|
+
).fetchone()
|
|
2065
2070
|
except sqlite3.OperationalError:
|
|
2066
2071
|
return None
|
|
2067
2072
|
seq = int(seq_row[0]) if seq_row and seq_row[0] is not None else 0
|
|
@@ -277,7 +277,7 @@ def _maybe_prune_conversation_retention(
|
|
|
277
277
|
core.APP_DIR.mkdir(parents=True, exist_ok=True)
|
|
278
278
|
except OSError:
|
|
279
279
|
return None
|
|
280
|
-
maint_fh = open(core.
|
|
280
|
+
maint_fh = open(core.CONVERSATIONS_LOCK_MAINTENANCE_PATH, "w")
|
|
281
281
|
try:
|
|
282
282
|
try:
|
|
283
283
|
fcntl.flock(maint_fh, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
@@ -285,13 +285,13 @@ def _maybe_prune_conversation_retention(
|
|
|
285
285
|
return None # another prune holds it; skip cleanly, do NOT stamp
|
|
286
286
|
if not force and not _retention_due(conn, now_utc):
|
|
287
287
|
return None
|
|
288
|
-
claude_fh = open(core.
|
|
288
|
+
claude_fh = open(core.CONVERSATIONS_LOCK_PATH, "w")
|
|
289
289
|
try:
|
|
290
290
|
try:
|
|
291
291
|
fcntl.flock(claude_fh, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
292
292
|
except (BlockingIOError, OSError):
|
|
293
293
|
return None # a Claude sync is mid-flight; retry next cycle
|
|
294
|
-
codex_fh = open(core.
|
|
294
|
+
codex_fh = open(core.CONVERSATIONS_LOCK_CODEX_PATH, "w")
|
|
295
295
|
try:
|
|
296
296
|
try:
|
|
297
297
|
fcntl.flock(codex_fh, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
package/bin/_lib_doctor.py
CHANGED
|
@@ -206,6 +206,10 @@ class DoctorState:
|
|
|
206
206
|
# cache was absent or unreadable; the check degrades to OK.
|
|
207
207
|
cache_db_page_count: Optional[int] = None
|
|
208
208
|
cache_db_freelist_count: Optional[int] = None
|
|
209
|
+
# #320: the independently large transcript store needs the same reclaim
|
|
210
|
+
# visibility, with remediation targeted at its own VACUUM surface.
|
|
211
|
+
conversations_db_page_count: Optional[int] = None
|
|
212
|
+
conversations_db_freelist_count: Optional[int] = None
|
|
209
213
|
# #294 S2: root-qualified physical Codex quota freshness, per-root native
|
|
210
214
|
# hook state, and lifecycle activity are gathered by _cctally_doctor.
|
|
211
215
|
codex_quota_windows: Optional[list[dict]] = None
|
|
@@ -1235,21 +1239,22 @@ def _check_data_conversation_sessions_rollup(s: DoctorState) -> CheckResult:
|
|
|
1235
1239
|
row per distinct ``conversation_messages.session_id`` (#217 S1 / U9).
|
|
1236
1240
|
|
|
1237
1241
|
OK when the two counts are equal, when either is ``None`` (pre-rollup or an
|
|
1238
|
-
unreadable
|
|
1242
|
+
unreadable conversations.db — consistent with the kernel's graceful-degrade
|
|
1243
|
+
posture),
|
|
1239
1244
|
OR when a sync/reingest/backfill is in progress. WARN ONLY on a mismatch
|
|
1240
1245
|
observed in a QUIESCENT cache.
|
|
1241
1246
|
|
|
1242
|
-
False-WARN avoidance (Codex P2): ``
|
|
1247
|
+
False-WARN avoidance (Codex P2): ``sync_claude_conversations`` commits
|
|
1243
1248
|
``conversation_messages`` per file *before* the ``conversation_sessions``
|
|
1244
1249
|
recompute, and resumable reingest commits per file before its rollup
|
|
1245
1250
|
completes — so an unsynchronized read can transiently mismatch. The
|
|
1246
1251
|
in-progress signal (``conv_rollup_sync_in_progress``) is set by
|
|
1247
|
-
``doctor_gather_state`` from a NON-BLOCKING ``
|
|
1252
|
+
``doctor_gather_state`` from a NON-BLOCKING ``conversations.db.lock`` flock probe
|
|
1248
1253
|
(lock held ⇒ a writer is mid-flight) AND the presence of any pending
|
|
1249
1254
|
``cache_meta`` reingest/split/backfill flag; if either says in-progress, this
|
|
1250
1255
|
stays OK. Doctor remains read-only and never blocks on the lock.
|
|
1251
1256
|
|
|
1252
|
-
Informational only (no remediation): the next
|
|
1257
|
+
Informational only (no remediation): the next conversation sync re-derives
|
|
1253
1258
|
the rollup via its incremental DELETE+INSERT.
|
|
1254
1259
|
"""
|
|
1255
1260
|
rollup = s.conv_sessions_rollup_count
|
|
@@ -1773,6 +1778,49 @@ def _check_db_reclaimable(s: DoctorState) -> CheckResult:
|
|
|
1773
1778
|
)
|
|
1774
1779
|
|
|
1775
1780
|
|
|
1781
|
+
def _check_db_conversations_reclaimable(s: DoctorState) -> CheckResult:
|
|
1782
|
+
"""Surface transcript-store free pages without mutating the DB (#320)."""
|
|
1783
|
+
page_count = s.conversations_db_page_count
|
|
1784
|
+
freelist_count = s.conversations_db_freelist_count
|
|
1785
|
+
ratio = None
|
|
1786
|
+
if (
|
|
1787
|
+
isinstance(page_count, int)
|
|
1788
|
+
and not isinstance(page_count, bool)
|
|
1789
|
+
and page_count > 0
|
|
1790
|
+
and isinstance(freelist_count, int)
|
|
1791
|
+
and not isinstance(freelist_count, bool)
|
|
1792
|
+
and 0 <= freelist_count <= page_count
|
|
1793
|
+
):
|
|
1794
|
+
ratio = freelist_count / page_count
|
|
1795
|
+
details = {
|
|
1796
|
+
"conversations_db_page_count": page_count,
|
|
1797
|
+
"conversations_db_freelist_count": freelist_count,
|
|
1798
|
+
"conversations_db_free_ratio": ratio,
|
|
1799
|
+
"warn_ratio": DOCTOR_RECLAIMABLE_WARN_RATIO,
|
|
1800
|
+
}
|
|
1801
|
+
if ratio is not None and ratio >= DOCTOR_RECLAIMABLE_WARN_RATIO:
|
|
1802
|
+
return CheckResult(
|
|
1803
|
+
id="db.conversations_reclaimable",
|
|
1804
|
+
title="Reclaimable transcript space",
|
|
1805
|
+
severity="warn",
|
|
1806
|
+
summary=(
|
|
1807
|
+
f"high — {ratio * 100:.1f}% of conversations.db pages are free"
|
|
1808
|
+
),
|
|
1809
|
+
remediation=(
|
|
1810
|
+
"Run `cctally db vacuum --db conversations` to reclaim disk space."
|
|
1811
|
+
),
|
|
1812
|
+
details=details,
|
|
1813
|
+
)
|
|
1814
|
+
return CheckResult(
|
|
1815
|
+
id="db.conversations_reclaimable",
|
|
1816
|
+
title="Reclaimable transcript space",
|
|
1817
|
+
severity="ok",
|
|
1818
|
+
summary="below threshold",
|
|
1819
|
+
remediation=None,
|
|
1820
|
+
details=details,
|
|
1821
|
+
)
|
|
1822
|
+
|
|
1823
|
+
|
|
1776
1824
|
# Each entry is (category_id, category_title, ((check_id, evaluator_fn_name), ...)).
|
|
1777
1825
|
# The dotted check_id is the stable JSON-contract ID (spec §5.2) AND the
|
|
1778
1826
|
# fingerprint identity-slice key (spec §5.5). When an evaluator raises,
|
|
@@ -1808,6 +1856,7 @@ _CATEGORY_DEFINITIONS: tuple[tuple[str, str, tuple[tuple[str, str], ...]], ...]
|
|
|
1808
1856
|
("db.lock_state", "_check_db_lock_state"),
|
|
1809
1857
|
("db.wal_size", "_check_db_wal_size"),
|
|
1810
1858
|
("db.reclaimable", "_check_db_reclaimable"),
|
|
1859
|
+
("db.conversations_reclaimable", "_check_db_conversations_reclaimable"),
|
|
1811
1860
|
)),
|
|
1812
1861
|
("data", "Data", (
|
|
1813
1862
|
("data.latest_snapshot_age", "_check_data_latest_snapshot_age"),
|
package/bin/cctally
CHANGED
|
@@ -1008,6 +1008,9 @@ get_codex_entries = _cctally_cache.get_codex_entries
|
|
|
1008
1008
|
_sum_codex_cost_for_range = _cctally_cache._sum_codex_cost_for_range
|
|
1009
1009
|
get_entries = _cctally_cache.get_entries
|
|
1010
1010
|
open_cache_db = _cctally_cache.open_cache_db
|
|
1011
|
+
open_conversations_db = _cctally_cache.open_conversations_db
|
|
1012
|
+
sync_claude_conversations = _cctally_cache.sync_claude_conversations
|
|
1013
|
+
sync_codex_conversations = _cctally_cache.sync_codex_conversations
|
|
1011
1014
|
_reset_orphan_warning_throttle = _cctally_cache._reset_orphan_warning_throttle
|
|
1012
1015
|
cmd_cache_sync = _cctally_cache.cmd_cache_sync
|
|
1013
1016
|
|