cctally 1.75.0 → 1.75.1
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 +6 -0
- package/bin/_cctally_db.py +41 -12
- package/bin/_lib_conversation_retention.py +26 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,12 @@ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [1.75.1] - 2026-07-20
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- Database repair now rejects an SQLite shell without functional `.recover` support before creating a forensic copy, explains the required `SQLITE_ENABLE_DBPAGE_VTAB` capability, and runs its public Linux recovery tests against a checksum-pinned official SQLite shell instead of Ubuntu's incompatible distro build.
|
|
12
|
+
- Automatic transcript-space reclamation now drives zero-column `incremental_vacuum` pragmas through SQLite's completion path in bounded, progress-checked chunks, fixing the Python 3.11/Linux case that could leave freed pages on the freelist after retention pruning.
|
|
13
|
+
|
|
8
14
|
## [1.75.0] - 2026-07-20
|
|
9
15
|
|
|
10
16
|
### Added
|
package/bin/_cctally_db.py
CHANGED
|
@@ -6411,6 +6411,25 @@ def _run_sqlite_recover(
|
|
|
6411
6411
|
return True, ""
|
|
6412
6412
|
|
|
6413
6413
|
|
|
6414
|
+
def _probe_sqlite_recover(sqlite_binary: str) -> "tuple[bool, str]":
|
|
6415
|
+
"""Prove the selected shell has a functional ``.recover`` command."""
|
|
6416
|
+
try:
|
|
6417
|
+
probe = subprocess.run(
|
|
6418
|
+
[sqlite_binary, ":memory:", ".recover"],
|
|
6419
|
+
stdout=subprocess.PIPE,
|
|
6420
|
+
stderr=subprocess.PIPE,
|
|
6421
|
+
check=False,
|
|
6422
|
+
)
|
|
6423
|
+
except OSError as exc:
|
|
6424
|
+
return False, str(exc)
|
|
6425
|
+
stderr = probe.stderr.decode("utf-8", "replace").strip()
|
|
6426
|
+
if probe.returncode != 0:
|
|
6427
|
+
return False, stderr or f"sqlite3 .recover probe exited {probe.returncode}"
|
|
6428
|
+
if b"BEGIN;" not in probe.stdout or b"COMMIT;" not in probe.stdout:
|
|
6429
|
+
return False, stderr or "sqlite3 .recover probe produced incomplete SQL"
|
|
6430
|
+
return True, ""
|
|
6431
|
+
|
|
6432
|
+
|
|
6414
6433
|
def _repair_preflight_and_copy(
|
|
6415
6434
|
path: pathlib.Path,
|
|
6416
6435
|
backup: pathlib.Path,
|
|
@@ -6549,6 +6568,28 @@ def _cmd_db_repair_claimed(args: argparse.Namespace, path: pathlib.Path) -> int:
|
|
|
6549
6568
|
"""Repair body; caller owns the marker and has proved no old handles."""
|
|
6550
6569
|
|
|
6551
6570
|
timeout_ms = int(getattr(args, "busy_timeout_ms", 250) or 250)
|
|
6571
|
+
sqlite_binary = (
|
|
6572
|
+
getattr(args, "sqlite3_binary", None) or shutil.which("sqlite3")
|
|
6573
|
+
)
|
|
6574
|
+
if not sqlite_binary:
|
|
6575
|
+
eprint(
|
|
6576
|
+
"cctally: db repair requires the sqlite3 command-line tool for "
|
|
6577
|
+
'its corruption-tolerant ".recover" operation. The live DB is '
|
|
6578
|
+
"untouched and no corrupt backup was created."
|
|
6579
|
+
)
|
|
6580
|
+
return 3
|
|
6581
|
+
recover_supported, recover_reason = _probe_sqlite_recover(
|
|
6582
|
+
str(sqlite_binary)
|
|
6583
|
+
)
|
|
6584
|
+
if not recover_supported:
|
|
6585
|
+
eprint(
|
|
6586
|
+
"cctally: the selected sqlite3 build does not support SQLite "
|
|
6587
|
+
f".recover ({recover_reason}). Install the official sqlite.org "
|
|
6588
|
+
"CLI built with SQLITE_ENABLE_DBPAGE_VTAB. The live DB is "
|
|
6589
|
+
"untouched and no corrupt backup was created."
|
|
6590
|
+
)
|
|
6591
|
+
return 3
|
|
6592
|
+
|
|
6552
6593
|
stamp = _db_backup_timestamp()
|
|
6553
6594
|
backup = _unique_sibling_path(
|
|
6554
6595
|
path.with_name(f"{path.name}.bak-corrupt-malformed-{stamp}")
|
|
@@ -6623,18 +6664,6 @@ def _cmd_db_repair_claimed(args: argparse.Namespace, path: pathlib.Path) -> int:
|
|
|
6623
6664
|
close_guard()
|
|
6624
6665
|
return 3
|
|
6625
6666
|
|
|
6626
|
-
sqlite_binary = (
|
|
6627
|
-
getattr(args, "sqlite3_binary", None) or shutil.which("sqlite3")
|
|
6628
|
-
)
|
|
6629
|
-
if not sqlite_binary:
|
|
6630
|
-
eprint(
|
|
6631
|
-
"cctally: db repair requires the sqlite3 command-line tool for "
|
|
6632
|
-
'its corruption-tolerant ".recover" operation. The live DB is '
|
|
6633
|
-
f"untouched and the corrupt backup is {backup}."
|
|
6634
|
-
)
|
|
6635
|
-
close_guard()
|
|
6636
|
-
return 3
|
|
6637
|
-
|
|
6638
6667
|
ok, reason = _run_sqlite_recover(
|
|
6639
6668
|
str(sqlite_binary), snapshot, recovered_path, scratch
|
|
6640
6669
|
)
|
|
@@ -197,6 +197,27 @@ def _stamp_retention_prune(conn: sqlite3.Connection, now_utc: dt.datetime) -> No
|
|
|
197
197
|
)
|
|
198
198
|
|
|
199
199
|
|
|
200
|
+
def _reclaim_incremental_vacuum(conn: sqlite3.Connection) -> None:
|
|
201
|
+
"""Drive zero-column incremental-vacuum rows to completion portably."""
|
|
202
|
+
remaining = int(conn.execute("PRAGMA freelist_count").fetchone()[0])
|
|
203
|
+
if remaining <= 0:
|
|
204
|
+
return
|
|
205
|
+
chunk_pages = 4096
|
|
206
|
+
max_passes = (remaining + chunk_pages - 1) // chunk_pages + 1
|
|
207
|
+
for _ in range(max_passes):
|
|
208
|
+
requested = min(remaining, chunk_pages)
|
|
209
|
+
# executescript() routes through sqlite3_exec(), which steps zero-column
|
|
210
|
+
# pragma rows through SQLITE_DONE on Python/SQLite combinations where a
|
|
211
|
+
# Cursor.fetchall() can stop after the first row (public Linux 3.11).
|
|
212
|
+
conn.executescript(f"PRAGMA incremental_vacuum({requested});")
|
|
213
|
+
after = int(conn.execute("PRAGMA freelist_count").fetchone()[0])
|
|
214
|
+
if after <= 0:
|
|
215
|
+
return
|
|
216
|
+
if after >= remaining:
|
|
217
|
+
return
|
|
218
|
+
remaining = after
|
|
219
|
+
|
|
220
|
+
|
|
200
221
|
def _maybe_prune_conversation_retention(
|
|
201
222
|
conn: sqlite3.Connection,
|
|
202
223
|
*,
|
|
@@ -271,12 +292,11 @@ def _maybe_prune_conversation_retention(
|
|
|
271
292
|
# already-durable prune.
|
|
272
293
|
if stats.total_rows > 0:
|
|
273
294
|
try:
|
|
274
|
-
#
|
|
275
|
-
#
|
|
276
|
-
#
|
|
277
|
-
#
|
|
278
|
-
|
|
279
|
-
conn.execute("PRAGMA incremental_vacuum").fetchall()
|
|
295
|
+
# Use the sqlite3_exec path and verify progress between
|
|
296
|
+
# bounded chunks. This clears the freelist and drops
|
|
297
|
+
# page_count; the physical file shrinks on the next
|
|
298
|
+
# `wal_checkpoint(TRUNCATE)` the sync loop forces (#297).
|
|
299
|
+
_reclaim_incremental_vacuum(conn)
|
|
280
300
|
except sqlite3.Error:
|
|
281
301
|
pass
|
|
282
302
|
return stats
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cctally",
|
|
3
|
-
"version": "1.75.
|
|
3
|
+
"version": "1.75.1",
|
|
4
4
|
"description": "Claude Code usage tracker and local dashboard for Pro/Max subscription limits - weekly cost-per-percent trend, quota forecasts, threshold alerts. ccusage-compatible.",
|
|
5
5
|
"homepage": "https://github.com/omrikais/cctally",
|
|
6
6
|
"repository": {
|