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.
- package/CHANGELOG.md +39 -0
- package/bin/_cctally_cache.py +863 -74
- package/bin/_cctally_config.py +57 -0
- package/bin/_cctally_core.py +39 -8
- package/bin/_cctally_dashboard.py +146 -5
- package/bin/_cctally_dashboard_conversation.py +164 -18
- package/bin/_cctally_dashboard_envelope.py +2 -0
- package/bin/_cctally_db.py +372 -10
- package/bin/_cctally_doctor.py +18 -1
- package/bin/_cctally_journal.py +535 -13
- package/bin/_cctally_journal_repair.py +6 -0
- package/bin/_cctally_parser.py +6 -0
- package/bin/_cctally_quota.py +171 -55
- package/bin/_cctally_record.py +13 -1
- package/bin/_cctally_rederive.py +4 -0
- package/bin/_cctally_store.py +311 -6
- package/bin/_cctally_transcript.py +32 -2
- package/bin/_lib_cache_report.py +8 -3
- package/bin/_lib_codex_conversation.py +851 -81
- package/bin/_lib_codex_conversation_query.py +2005 -95
- package/bin/_lib_codex_find_projection.py +370 -0
- package/bin/_lib_codex_harness_preamble.py +176 -0
- package/bin/_lib_codex_hooks.py +5 -3
- package/bin/_lib_codex_js_scan.py +254 -0
- package/bin/_lib_codex_landmarks.py +309 -0
- package/bin/_lib_codex_title_clean.py +116 -0
- package/bin/_lib_conversation_dispatch.py +153 -21
- package/bin/_lib_conversation_watch.py +4 -2
- package/bin/_lib_doctor.py +64 -0
- package/bin/_lib_quota_alert_axes.py +31 -34
- package/bin/_lib_stats_damage.py +523 -0
- package/bin/cctally +5 -0
- package/dashboard/static/assets/index-BEzzJtUd.js +97 -0
- package/dashboard/static/assets/{index-Dwirao3Y.css → index-DnWdv8um.css} +1 -1
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +7 -1
- package/dashboard/static/assets/index-CILAoEja.js +0 -90
package/bin/_cctally_store.py
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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
|
-
|
|
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"),
|
package/bin/_lib_cache_report.py
CHANGED
|
@@ -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 <
|
|
861
|
-
|
|
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 <
|
|
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")
|