cctally 1.91.0 → 1.92.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.
Files changed (37) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/bin/_cctally_cache.py +863 -74
  3. package/bin/_cctally_config.py +57 -0
  4. package/bin/_cctally_core.py +39 -8
  5. package/bin/_cctally_dashboard.py +146 -5
  6. package/bin/_cctally_dashboard_conversation.py +164 -18
  7. package/bin/_cctally_dashboard_envelope.py +2 -0
  8. package/bin/_cctally_db.py +372 -10
  9. package/bin/_cctally_doctor.py +18 -1
  10. package/bin/_cctally_journal.py +535 -13
  11. package/bin/_cctally_journal_repair.py +6 -0
  12. package/bin/_cctally_parser.py +6 -0
  13. package/bin/_cctally_quota.py +171 -55
  14. package/bin/_cctally_record.py +13 -1
  15. package/bin/_cctally_rederive.py +4 -0
  16. package/bin/_cctally_store.py +311 -6
  17. package/bin/_cctally_transcript.py +32 -2
  18. package/bin/_lib_cache_report.py +8 -3
  19. package/bin/_lib_codex_conversation.py +851 -81
  20. package/bin/_lib_codex_conversation_query.py +2005 -95
  21. package/bin/_lib_codex_find_projection.py +370 -0
  22. package/bin/_lib_codex_harness_preamble.py +176 -0
  23. package/bin/_lib_codex_hooks.py +5 -3
  24. package/bin/_lib_codex_js_scan.py +254 -0
  25. package/bin/_lib_codex_landmarks.py +309 -0
  26. package/bin/_lib_codex_title_clean.py +116 -0
  27. package/bin/_lib_conversation_dispatch.py +153 -21
  28. package/bin/_lib_conversation_watch.py +4 -2
  29. package/bin/_lib_doctor.py +64 -0
  30. package/bin/_lib_quota_alert_axes.py +31 -34
  31. package/bin/_lib_stats_damage.py +523 -0
  32. package/bin/cctally +5 -0
  33. package/dashboard/static/assets/index-BEzzJtUd.js +97 -0
  34. package/dashboard/static/assets/{index-Dwirao3Y.css → index-DnWdv8um.css} +1 -1
  35. package/dashboard/static/dashboard.html +2 -2
  36. package/package.json +7 -1
  37. package/dashboard/static/assets/index-CILAoEja.js +0 -90
@@ -290,7 +290,11 @@ def would_block_prod_stats_cutover(path) -> bool:
290
290
  # verification adds `quota_projection_ledger_state.last_full_pass_at`, and a
291
291
  # legacy index that already ran the fixups at version 2 would skip the schema
292
292
  # apply that adds it.
293
- _STATS_OPEN_FIXUPS_VERSION = 3
293
+ #
294
+ # 3 -> 4 (#460): scheduled boundary ownership adds
295
+ # `quota_projection_ledger_state.next_evaluation_by_root_json` through the same
296
+ # in-place legacy cutover seam.
297
+ _STATS_OPEN_FIXUPS_VERSION = 4
294
298
 
295
299
 
296
300
  def stats_open_fixups_current(conn: sqlite3.Connection) -> bool:
@@ -784,6 +788,208 @@ def _stats_repair_marker(db_path) -> pathlib.Path:
784
788
  return pathlib.Path(db_path).with_name("stats.db.repairing")
785
789
 
786
790
 
791
+ def _stats_publication_marker(db_path) -> pathlib.Path:
792
+ """The durable publication marker for ``db_path`` (#496 S1 F1)."""
793
+ return pathlib.Path(str(db_path) + ".publication")
794
+
795
+
796
+ def _remove_stats_publication_marker(db_path) -> None:
797
+ import _cctally_journal
798
+
799
+ try:
800
+ _stats_publication_marker(db_path).unlink()
801
+ except FileNotFoundError:
802
+ pass
803
+ _cctally_journal._fsync_dir(pathlib.Path(db_path).parent)
804
+
805
+
806
+ def _read_stats_publication_marker(db_path) -> "dict | None":
807
+ """The marker's state as a MAPPING, or None when no marker exists.
808
+
809
+ A marker that is present but unreadable, or whose bytes are valid JSON that
810
+ is not an object (`null`, `[]`), reads as an empty mapping: it exists, and
811
+ it records nothing. `json.loads` returns whatever the bytes decode to, so
812
+ calling `.get(...)` on the raw result raises `AttributeError` on those
813
+ shapes — which, from `_raise_settled_publication_failure`, escapes the heal
814
+ hook's `except Exception` and surfaces as a raw traceback from `open_db`.
815
+ """
816
+ try:
817
+ state = json.loads(_stats_publication_marker(db_path).read_text())
818
+ except FileNotFoundError:
819
+ return None
820
+ except (OSError, ValueError):
821
+ return {}
822
+ return state if isinstance(state, dict) else {}
823
+
824
+
825
+ def _discard_pending_stats_publication_marker(db_path) -> None:
826
+ """Drop a PENDING marker whose own publication never replaced anything.
827
+
828
+ Such a marker's pinned high-water describes an index that was never
829
+ published, so validating the live destination against it would condemn a
830
+ healthy index (measured: destination cursor 303 against a pinned
831
+ high-water of 2852).
832
+
833
+ Callers must establish that fact first — see
834
+ `_pending_stats_publication_never_replaced`. **The maintenance lock does
835
+ NOT establish it.** A rebuild that dies releases its flock, so a scratch on
836
+ disk can belong to a strictly later run than the marker beside it, and the
837
+ two are then unrelated.
838
+
839
+ A `priorFailure` block is restored rather than dropped. It records a verdict
840
+ a PREVIOUS publication owed on bytes that are still live, carried forward by
841
+ the run whose marker this is; because that run never replaced the
842
+ destination, those bytes are exactly what a later opener would connect to.
843
+
844
+ A `failed` marker is a settled verdict about the CURRENT destination and is
845
+ never discarded here.
846
+ """
847
+ state = _read_stats_publication_marker(db_path)
848
+ if state is None or str(state.get("status") or "") != "pending":
849
+ return
850
+ prior = state.get("priorFailure")
851
+ if isinstance(prior, dict) and prior:
852
+ import _cctally_journal
853
+
854
+ _cctally_db._atomic_write_private_json(
855
+ _stats_publication_marker(db_path), prior
856
+ )
857
+ _cctally_journal._fsync_dir(pathlib.Path(db_path).parent)
858
+ return
859
+ _remove_stats_publication_marker(db_path)
860
+
861
+
862
+ def _pending_stats_publication_never_replaced(db_path) -> bool:
863
+ """Whether a PENDING marker's own scratch index is still on disk.
864
+
865
+ `os.replace` is the only thing that consumes ANOTHER run's scratch, so a
866
+ marker still naming a live scratch pathname proves that run never reached
867
+ publication: the live destination is the untouched predecessor and the
868
+ marker is stale. When the scratch is gone the replacement DID happen and
869
+ the verdict on the published bytes is still owed, so the marker must be
870
+ resolved rather than discarded.
871
+
872
+ The stronger form of that claim — that `os.replace` is the only consumer of
873
+ any scratch — is false, and the difference is confined to the run's OWN
874
+ process. `_cctally_journal._cleanup_new_correction_scratches` removes the
875
+ scratch this run just created when `_recover_completed_correction`'s
876
+ rebuild raises, so a failing `os.replace` in that path leaves the marker
877
+ naming a scratch that its own process then deleted, and this predicate
878
+ reads that as "replaced". The proxy is used only across processes, where
879
+ that cleanup cannot reach, so the weaker property is the one it needs.
880
+
881
+ Must be consulted BEFORE stale-artifact cleanup removes the scratch.
882
+
883
+ A marker carrying no `scratchPath` cannot prove it published, so it is
884
+ treated as never-replaced. No released binary has ever written one — the
885
+ marker and this field ship together — so the branch exists only to keep an
886
+ unreadable marker from wedging every open.
887
+ """
888
+ state = _read_stats_publication_marker(db_path)
889
+ if not state:
890
+ return True
891
+ if str(state.get("status") or "") != "pending":
892
+ return False
893
+ scratch = state.get("scratchPath")
894
+ if not isinstance(scratch, str) or not scratch:
895
+ return True
896
+ return pathlib.Path(scratch).exists()
897
+
898
+
899
+ def _stats_publication_failed_error(
900
+ db_path, record_path,
901
+ ) -> _cctally_db.StatsPublicationFailedError:
902
+ return _cctally_db.StatsPublicationFailedError(
903
+ _cctally_core.STATS_PUBLICATION_FAILED_MSG.format(
904
+ path=db_path, record=record_path or "<unrecorded>",
905
+ )
906
+ )
907
+
908
+
909
+ def _raise_settled_publication_failure(db_path) -> None:
910
+ """Re-raise a `failed` publication verdict the caller's `except` swallowed.
911
+
912
+ Only a marker already written as `failed` reaches this; every other state
913
+ returns and leaves the caller's behaviour unchanged.
914
+ """
915
+ state = _read_stats_publication_marker(db_path)
916
+ if not state:
917
+ return
918
+ if str(state.get("status") or "") == "failed":
919
+ raise _stats_publication_failed_error(db_path, state.get("recordPath"))
920
+
921
+
922
+ def _resolve_stats_publication_marker(db_path: pathlib.Path) -> None:
923
+ """Honour a durable publication marker (#496 S1 F1).
924
+
925
+ Caller holds maintenance EXCLUSIVE, which is what makes the pending case
926
+ safe: a rebuild that is still in flight owns the lock, so reaching here
927
+ proves its outcome is settled.
928
+
929
+ Order of precedence is established by the caller: a `.rebuilding-*` scratch
930
+ artifact is classified FIRST, so the existing interrupted-rebuild recovery
931
+ keeps taking precedence and clears any stale marker when it republishes.
932
+
933
+ This function refuses and reports. It never decides to rebuild — choosing
934
+ when to rebuild is firing policy.
935
+ """
936
+ import _cctally_journal
937
+
938
+ marker = _stats_publication_marker(db_path)
939
+ state = _read_stats_publication_marker(db_path)
940
+ if state is None:
941
+ return
942
+ status = str(state.get("status") or "")
943
+ record_path = state.get("recordPath")
944
+
945
+ if status == "failed":
946
+ raise _stats_publication_failed_error(db_path, record_path)
947
+ if status != "pending":
948
+ _remove_stats_publication_marker(db_path)
949
+ return
950
+
951
+ record = None
952
+ if isinstance(record_path, str):
953
+ try:
954
+ record = json.loads(pathlib.Path(record_path).read_text())
955
+ except (OSError, ValueError):
956
+ record = None
957
+ if not isinstance(record, dict) or "highWater" not in record:
958
+ # Without its record the marker cannot be judged, and validating
959
+ # against a guessed high-water would condemn a healthy index. This
960
+ # marker is diagnostic scaffolding; it must not wedge every open.
961
+ print(
962
+ "[stats] discarding an unresolvable stats.db publication marker "
963
+ f"(rebuild record: {record_path!r})",
964
+ file=sys.stderr,
965
+ )
966
+ _remove_stats_publication_marker(db_path)
967
+ return
968
+
969
+ raw = record.get("highWater")
970
+ high_water = (
971
+ (str(raw[0]), int(raw[1]))
972
+ if isinstance(raw, (list, tuple)) and len(raw) == 2
973
+ else None
974
+ )
975
+ error = _cctally_journal.validate_published_stats_index(db_path, high_water)
976
+ if error is None:
977
+ _remove_stats_publication_marker(db_path)
978
+ return
979
+
980
+ state.update({"status": "failed", "error": error})
981
+ try:
982
+ _cctally_db._atomic_write_private_json(marker, state)
983
+ record["status"] = "failed"
984
+ record["postPublicationValidation"] = {"ok": False, "error": error}
985
+ _cctally_db._atomic_write_private_json(
986
+ pathlib.Path(record_path), record
987
+ )
988
+ except OSError:
989
+ pass
990
+ raise _stats_publication_failed_error(db_path, record_path)
991
+
992
+
787
993
  def _resume_pending_quarantine(db_path: pathlib.Path) -> None:
788
994
  """Finish a strict quarantine that a previous owner did not complete.
789
995
 
@@ -993,12 +1199,27 @@ def _recover_or_reclaim_interrupted_stats_rebuild(
993
1199
  # matching legacy prebuild-quarantine incident, exact scratch names are
994
1200
  # unpublished Task A artifacts and are safe to reclaim under the
995
1201
  # caller's maintenance EXCLUSIVE hold.
1202
+ #
1203
+ # The marker beside them is a separate question, decided BEFORE the
1204
+ # cleanup destroys the evidence: a scratch here need not belong to the
1205
+ # marker's run at all, because a crashed rebuild releases its flock and
1206
+ # a later run can leave its own scratch behind.
1207
+ stale_marker = _pending_stats_publication_never_replaced(db_path)
996
1208
  _remove_stale_stats_rebuild_artifacts(artifacts)
1209
+ if stale_marker:
1210
+ _discard_pending_stats_publication_marker(db_path)
1211
+ else:
1212
+ _resolve_stats_publication_marker(db_path)
997
1213
  return True
998
1214
  if db_path.exists() and _cctally_journal.stats_index_matches_journal_prefix(
999
1215
  db_path, high_water
1000
1216
  ):
1001
1217
  _remove_stale_stats_rebuild_artifacts(artifacts)
1218
+ # This branch just PROVED the destination is a fully valid
1219
+ # materialization of the journal prefix, which is strictly stronger
1220
+ # than any publication marker's own check, so the proof supersedes
1221
+ # whatever the marker recorded.
1222
+ _remove_stats_publication_marker(db_path)
1002
1223
  return True
1003
1224
  if high_water is None or high_water[1] == 0:
1004
1225
  return False
@@ -1010,8 +1231,14 @@ def _recover_or_reclaim_interrupted_stats_rebuild(
1010
1231
  )
1011
1232
  try:
1012
1233
  with stats_write_scope("maintenance-interrupted-rebuild"):
1013
- _cctally_journal.rebuild_stats_index(high_water=high_water)
1234
+ _cctally_journal.rebuild_stats_index(
1235
+ context=_cctally_journal.RebuildContext(
1236
+ trigger="interrupted-rebuild-recovery"
1237
+ ),
1238
+ high_water=high_water,
1239
+ )
1014
1240
  _remove_stale_stats_rebuild_artifacts(artifacts)
1241
+ _discard_pending_stats_publication_marker(db_path)
1015
1242
  return True
1016
1243
  finally:
1017
1244
  _cctally_journal._release_ingest_lock(ingest_fd)
@@ -1114,6 +1341,11 @@ def stats_open_guarded(
1114
1341
  except (
1115
1342
  _cctally_db.ProdMigrationRefused,
1116
1343
  _cctally_db.StatsDbMaintenanceError,
1344
+ # Recovery may resolve a publication marker whose run
1345
+ # DID replace the destination; that verdict carries its
1346
+ # own guided wording and must not be reworded into a
1347
+ # maintenance-in-progress error.
1348
+ _cctally_db.StatsPublicationFailedError,
1117
1349
  ):
1118
1350
  raise
1119
1351
  except Exception as exc:
@@ -1138,6 +1370,36 @@ def stats_open_guarded(
1138
1370
  if marker.exists() or pending.exists():
1139
1371
  fcntl.flock(lock_fh, fcntl.LOCK_UN)
1140
1372
  continue
1373
+ # #496 S1 F1: a durable publication marker, honoured AFTER the
1374
+ # scratch-artifact classification above so that path keeps
1375
+ # precedence. Steady state costs one stat() on a file that does not
1376
+ # exist. Suppressed exactly where interrupted recovery is, so
1377
+ # doctor's read-only gather stays read-only.
1378
+ if (
1379
+ recover_interruptions
1380
+ and _INTERRUPTED_RECOVERY_SUPPRESSED.get() == 0
1381
+ and _stats_publication_marker(db_path).exists()
1382
+ ):
1383
+ fcntl.flock(lock_fh, fcntl.LOCK_UN)
1384
+ if not _flock_bounded(
1385
+ lock_fh, fcntl.LOCK_EX, _STATS_OPEN_RESUME_WAIT_S
1386
+ ):
1387
+ raise _cctally_db.StatsDbMaintenanceError(
1388
+ _STATS_OPEN_MAINTENANCE_TIMEOUT_MSG
1389
+ )
1390
+ try:
1391
+ _resolve_stats_publication_marker(db_path)
1392
+ finally:
1393
+ fcntl.flock(lock_fh, fcntl.LOCK_UN)
1394
+ if not _flock_bounded(
1395
+ lock_fh, fcntl.LOCK_SH, _STATS_OPEN_MAINTENANCE_WAIT_S
1396
+ ):
1397
+ raise _cctally_db.StatsDbMaintenanceError(
1398
+ _STATS_OPEN_MAINTENANCE_TIMEOUT_MSG
1399
+ )
1400
+ if marker.exists() or pending.exists():
1401
+ fcntl.flock(lock_fh, fcntl.LOCK_UN)
1402
+ continue
1141
1403
  try:
1142
1404
  conn = _connect(db_path)
1143
1405
  # Re-check inside the same shared hold: cheap, and it closes the
@@ -1370,7 +1632,15 @@ def _stats_heal_hook(
1370
1632
  the maintenance lock; False when it DECLINES — a non-corruption
1371
1633
  ``DatabaseError`` (BUSY / disk-full / permission), the dev-checkout-on-prod
1372
1634
  guard, or re-entrancy. A False return leaves ``open_db`` to raise its guided
1373
- ``StatsDbCorruptError``."""
1635
+ ``StatsDbCorruptError``.
1636
+
1637
+ It can also RAISE ``_cctally_db.StatsPublicationFailedError`` (#496 S1 F1),
1638
+ and both callers depend on that: ``_cctally_tui._tui_heal_post_query_stats``
1639
+ catches it and degrades, while ``_cctally_core.open_db`` deliberately lets
1640
+ it propagate. The heal that replaced the index and then failed to validate
1641
+ it must report that itself, because ``open_db``'s decline branch would tell
1642
+ the user the database was "Not auto-recreated" — false once replacement has
1643
+ occurred."""
1374
1644
  global _HEAL_ACTIVE
1375
1645
  if store != "stats":
1376
1646
  return False
@@ -1402,8 +1672,21 @@ def _stats_heal_hook(
1402
1672
  probe = _probe_stats_integrity_ok if post_query else _probe_stats_ok
1403
1673
  if probe(path):
1404
1674
  return True # a sibling process already healed it — retry the open
1405
- # Forensics FIRST — before anything disturbs the evidence.
1406
- _cctally_db.write_corruption_forensics(path, db_label="stats")
1675
+ # Forensics FIRST — before anything disturbs the evidence. The
1676
+ # trigger pair is what arms the #496 S1 forensics-time WAL capture
1677
+ # and what lets the quarantine incident name the bundle that
1678
+ # preceded it.
1679
+ #
1680
+ # The typed result is CAPTURED but deliberately NOT gated on:
1681
+ # refusing the heal on an UNCONFIRMED disposition is F4 and belongs
1682
+ # to S3. S1 only makes the evidence available at this call site.
1683
+ forensics = _cctally_db.write_corruption_forensics(
1684
+ path,
1685
+ db_label="stats",
1686
+ trigger_origin="corruption-heal",
1687
+ trigger_exception=exc,
1688
+ return_result=True,
1689
+ )
1407
1690
  if holds_ingest_lock():
1408
1691
  ingest_fd = None # this context IS the serialized writer
1409
1692
  else:
@@ -1424,7 +1707,20 @@ def _stats_heal_hook(
1424
1707
  # exclusive, which is exactly what spec §3.1 sanctions.
1425
1708
  with stats_write_scope("maintenance-heal"):
1426
1709
  import _cctally_journal
1427
- _cctally_journal.rebuild_stats_index()
1710
+ _cctally_journal.rebuild_stats_index(
1711
+ context=_cctally_journal.RebuildContext(
1712
+ trigger="corruption-heal",
1713
+ trigger_error=_cctally_db._bounded_forensics_text(
1714
+ exc,
1715
+ _cctally_db._FORENSICS_EXCEPTION_MESSAGE_MAX,
1716
+ ),
1717
+ forensics_path=(
1718
+ str(forensics.path)
1719
+ if forensics.path is not None
1720
+ else None
1721
+ ),
1722
+ )
1723
+ )
1428
1724
  finally:
1429
1725
  if ingest_fd is not None:
1430
1726
  _heal_release_flock(ingest_fd)
@@ -1439,6 +1735,15 @@ def _stats_heal_hook(
1439
1735
  _release_stats_maintenance_reentrant(maint_fd)
1440
1736
  except Exception as heal_exc:
1441
1737
  print(f"[heal] stats.db auto-heal failed: {heal_exc}", file=sys.stderr)
1738
+ # A post-publication validation failure has ALREADY replaced the index.
1739
+ # Declining here sends `open_db` to its pre-existing branch, which tells
1740
+ # the user the database was "Not auto-recreated" and to run
1741
+ # `db repair --db stats --yes` — both false once replacement occurred.
1742
+ # The durable marker makes the NEXT process say the right thing; the
1743
+ # process that caused the failure must say it too (#496 S1 F1).
1744
+ # Narrow by construction: it fires only on a `failed` marker, so every
1745
+ # other heal failure keeps its existing behaviour.
1746
+ _raise_settled_publication_failure(path)
1442
1747
  return False
1443
1748
  finally:
1444
1749
  _HEAL_ACTIVE = False
@@ -81,7 +81,7 @@ def _cmd_transcript_export(args) -> int:
81
81
 
82
82
  if session_id.startswith("v1."):
83
83
  return _cmd_transcript_export_qualified(
84
- c, session_id, scope, raw, output, speed_arg)
84
+ c, args, session_id, scope, raw, output, speed_arg)
85
85
 
86
86
  # Legacy bare Claude path — byte-untouched. --speed is Codex pricing behavior,
87
87
  # so an explicit value on any non-Codex ref is a usage error (resolved-source
@@ -90,8 +90,16 @@ def _cmd_transcript_export(args) -> int:
90
90
  eprint(_SPEED_ONLY_CODEX_MSG)
91
91
  return 2
92
92
 
93
+ account_key, account_error = c.resolve_account_filter(
94
+ args, "claude", needs_cache=True
95
+ )
96
+ if account_error is not None:
97
+ return account_error
98
+
93
99
  conn = c.open_conversations_db()
94
100
  try:
101
+ if account_key is not None:
102
+ c.scope_conversations_db_to_account(conn, account_key)
95
103
  cq = c._load_sibling("_lib_conversation_query")
96
104
  md = cq.get_conversation_export(conn, session_id, scope)
97
105
  if md is None:
@@ -109,7 +117,7 @@ def _cmd_transcript_export(args) -> int:
109
117
 
110
118
 
111
119
  def _cmd_transcript_export_qualified(
112
- c, session_id, scope, raw, output, speed_arg) -> int:
120
+ c, args, session_id, scope, raw, output, speed_arg) -> int:
113
121
  """Qualified (``v1.``) export via the neutral dispatch layer (§4.1). Anonymized
114
122
  by default with the QUALIFIED provider-aware plan (§3.6); ``--raw`` escapes.
115
123
  Byte-matches ``GET /api/conversation/<v1key>/export`` in both modes."""
@@ -121,9 +129,17 @@ def _cmd_transcript_export_qualified(
121
129
  eprint(_SPEED_ONLY_CODEX_MSG)
122
130
  return 2
123
131
  speed = c._resolve_codex_speed(speed_arg or "auto")
132
+ provider = cref.source if cref is not None else "claude"
133
+ account_key, account_error = c.resolve_account_filter(
134
+ args, provider, needs_cache=True
135
+ )
136
+ if account_error is not None:
137
+ return account_error
124
138
 
125
139
  conn = c.open_conversations_db()
126
140
  try:
141
+ if account_key is not None:
142
+ c.scope_conversations_db_to_account(conn, account_key)
127
143
  env = disp.neutral_export(
128
144
  conn, session_id, scope=scope, effective_speed=speed)
129
145
  status = env.get("status")
@@ -176,6 +192,11 @@ def _cmd_transcript_search_claude(args) -> int:
176
192
  cost_min = getattr(args, "cost_min", None)
177
193
  cost_max = getattr(args, "cost_max", None)
178
194
  rebuild_min = getattr(args, "rebuild_min", None)
195
+ account_key, account_error = c.resolve_account_filter(
196
+ args, "claude", needs_cache=True
197
+ )
198
+ if account_error is not None:
199
+ return account_error
179
200
 
180
201
  # Date-only bounds parse through the SAME display-tz-aware helper the HTTP
181
202
  # filter handler uses (no second parser) — reuse, don't reimplement.
@@ -195,6 +216,8 @@ def _cmd_transcript_search_claude(args) -> int:
195
216
 
196
217
  conn = c.open_conversations_db()
197
218
  try:
219
+ if account_key is not None:
220
+ c.scope_conversations_db_to_account(conn, account_key)
198
221
  cq = c._load_sibling("_lib_conversation_query")
199
222
  try:
200
223
  result = cq.search_conversations(
@@ -233,6 +256,11 @@ def _cmd_transcript_search_codex(args) -> int:
233
256
  limit = getattr(args, "limit", 50)
234
257
  cursor = getattr(args, "cursor", None)
235
258
  as_json = bool(getattr(args, "json", False))
259
+ account_key, account_error = c.resolve_account_filter(
260
+ args, "codex", needs_cache=True
261
+ )
262
+ if account_error is not None:
263
+ return account_error
236
264
 
237
265
  # Pagination + filter axes the Codex kernel does not have → exit 2 (silently
238
266
  # ignoring a filter would fabricate results).
@@ -271,6 +299,8 @@ def _cmd_transcript_search_codex(args) -> int:
271
299
 
272
300
  conn = c.open_conversations_db()
273
301
  try:
302
+ if account_key is not None:
303
+ c.scope_conversations_db_to_account(conn, account_key)
274
304
  result = disp.neutral_search(
275
305
  conn, query, source="codex", kind=kind,
276
306
  effective_speed=c._resolve_codex_speed("auto"),
@@ -114,6 +114,11 @@ DEFAULT_TIERED_THRESHOLD = 200_000
114
114
  CACHE_REPORT_MIN_BASELINE_DAYS = 5
115
115
  CACHE_REPORT_MIN_BASELINE_SESSIONS = 10
116
116
 
117
+ # Monetary comparisons elsewhere in cctally reconcile within 1e-9 USD. Keep
118
+ # an exactly-cancelling cache row inside that same tolerance so a value that
119
+ # renders as zero cannot raise a non-actionable ``net_negative`` anomaly.
120
+ CACHE_REPORT_NET_NEGATIVE_TOLERANCE_USD = 1e-9
121
+
117
122
 
118
123
  # Anomaly-threshold bounds and default. #443 S3 F17: four sites used to
119
124
  # decide independently what `cache_report.anomaly_threshold_pp` means —
@@ -857,8 +862,8 @@ def _classify_anomalies(
857
862
  ) -> None:
858
863
  """Mutate each row's ``anomaly_triggered`` / ``anomaly_reasons`` in place.
859
864
 
860
- Trigger 1 (``net_negative``): ``net_usd < 0`` (strict). Skipped when the
861
- row has zero cache activity (no-op session, not a bug).
865
+ Trigger 1 (``net_negative``): ``net_usd < -1e-9``. Skipped when the row
866
+ has zero cache activity (no-op session, not a bug).
862
867
 
863
868
  Trigger 2 (``cache_drop``): ``cache_hit_percent`` is ``>= threshold_pp``
864
869
  below the trailing ``window_days`` median of OTHER rows. Requires
@@ -901,7 +906,7 @@ def _classify_anomalies(
901
906
 
902
907
  # Trigger 1: net_negative (no baseline needed; cache-activity guard).
903
908
  if row.cache_creation_tokens + row.cache_read_tokens > 0:
904
- if row.net_usd < 0:
909
+ if row.net_usd < -CACHE_REPORT_NET_NEGATIVE_TOLERANCE_USD:
905
910
  reasons.append("net_negative")
906
911
  else:
907
912
  unevaluated.append("net_negative")