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 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
@@ -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
- # `.fetchall()` drives the pragma to completion a bare
275
- # execute() steps it once and reclaims a single page. This
276
- # clears the freelist and drops page_count; the physical
277
- # file shrinks on the next `wal_checkpoint(TRUNCATE)` the
278
- # sync loop already forces (#297).
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.0",
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": {