cctally 1.99.0 → 1.100.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.
@@ -11084,7 +11084,7 @@ def cmd_db_backup(args: argparse.Namespace) -> int:
11084
11084
 
11085
11085
 
11086
11086
  def cmd_db_checkpoint(args: argparse.Namespace) -> int:
11087
- """Fast, non-destructive WAL drain (TRUNCATE checkpoint) for cache.db.
11087
+ """Fast, non-destructive WAL drain (TRUNCATE checkpoint) for a WAL store.
11088
11088
 
11089
11089
  Opens a RAW existing-file-only connection — NOT open_cache_db()/open_db(),
11090
11090
  which apply schema, run the migration dispatcher, can DELETE Codex rows on a
@@ -11095,14 +11095,16 @@ def cmd_db_checkpoint(args: argparse.Namespace) -> int:
11095
11095
 
11096
11096
  A cache checkpoint takes the same global writer flock as both provider
11097
11097
  syncs, using the command's timeout as one bounded wait, so it cannot overlap
11098
- a cache write or another cache checkpoint. Exit 0 when drained /
11099
- already-small / the DB is absent; 3 (staged) if the target stayed busy or
11100
- was not fully truncated through the timeout — an actionable "something is
11101
- still holding it" signal.
11098
+ a cache write or another cache checkpoint. A conversations checkpoint takes
11099
+ a THREE-lock plan instead, because that store has two independently writable
11100
+ provider domains rather than cache.db's single global writer flock (#583
11101
+ S4). Exit 0 when drained / already-small / the DB is absent; 3 (staged) if
11102
+ the target stayed busy or was not fully truncated through the timeout — an
11103
+ actionable "something is still holding it" signal.
11102
11104
  """
11103
11105
  from _lib_json_envelope import stamp_schema_version
11104
11106
 
11105
- which = args.db # "cache" | "stats"
11107
+ which = args.db # "cache" | "conversations" | "stats"
11106
11108
  as_json = bool(getattr(args, "json", False))
11107
11109
  if which == "stats":
11108
11110
  reason = (
@@ -11120,7 +11122,26 @@ def cmd_db_checkpoint(args: argparse.Namespace) -> int:
11120
11122
 
11121
11123
  import _cctally_cache
11122
11124
 
11123
- path, label = _cctally_core.CACHE_DB_PATH, "cache.db"
11125
+ if which == "conversations":
11126
+ path, label = _cctally_core.CONVERSATIONS_DB_PATH, "conversations.db"
11127
+ # conversations.db has TWO independently writable provider domains,
11128
+ # unlike cache.db's single global writer flock, so a checkpoint must
11129
+ # exclude both. The order is the documented lock-order law: maintenance
11130
+ # flocks before the conversation provider flocks (Claude, then Codex),
11131
+ # and checkpoint work takes maintenance SHARED — only mutating
11132
+ # maintenance takes it exclusive, and retention's exclusive hold is an
11133
+ # admission gate it downgrades to shared for the mutating pass itself.
11134
+ lock_plan = [
11135
+ (_cctally_core.CONVERSATIONS_LOCK_MAINTENANCE_PATH, fcntl.LOCK_SH),
11136
+ (_cctally_core.CONVERSATIONS_LOCK_PATH, fcntl.LOCK_EX),
11137
+ (_cctally_core.CONVERSATIONS_LOCK_CODEX_PATH, fcntl.LOCK_EX),
11138
+ ]
11139
+ else:
11140
+ path, label = _cctally_core.CACHE_DB_PATH, "cache.db"
11141
+ lock_plan = [
11142
+ (_cctally_core.CACHE_LOCK_MAINTENANCE_PATH, fcntl.LOCK_SH),
11143
+ (_cctally_core.CACHE_LOCK_PATH, fcntl.LOCK_EX),
11144
+ ]
11124
11145
  timeout = int(getattr(args, "busy_timeout_ms", None)
11125
11146
  or _cctally_cache.CHECKPOINT_CMD_BUSY_TIMEOUT_MS)
11126
11147
 
@@ -11137,16 +11158,11 @@ def cmd_db_checkpoint(args: argparse.Namespace) -> int:
11137
11158
  print(f"cctally: no {label} database file present; nothing to drain.")
11138
11159
  return 0
11139
11160
 
11140
- from _lib_cache_writer_lock import acquire_ordered_flocks
11141
-
11142
- lock_plan = [
11143
- (_cctally_core.CACHE_LOCK_MAINTENANCE_PATH, fcntl.LOCK_SH),
11144
- (_cctally_core.CACHE_LOCK_PATH, fcntl.LOCK_EX),
11145
- ]
11161
+ import _lib_cache_writer_lock
11146
11162
 
11147
11163
  held: list[int] = []
11148
11164
  try:
11149
- acquired = acquire_ordered_flocks(
11165
+ acquired = _lib_cache_writer_lock.acquire_ordered_flocks(
11150
11166
  lock_plan, timeout=max(timeout, 0) / 1000
11151
11167
  )
11152
11168
  if acquired is None: