cctally 1.91.0 → 1.92.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 +51 -0
- package/README.md +4 -2
- package/bin/_cctally_cache.py +903 -74
- package/bin/_cctally_config.py +57 -0
- package/bin/_cctally_core.py +94 -14
- package/bin/_cctally_dashboard.py +217 -19
- package/bin/_cctally_dashboard_conversation.py +170 -20
- package/bin/_cctally_dashboard_envelope.py +2 -0
- package/bin/_cctally_db.py +481 -19
- package/bin/_cctally_doctor.py +18 -1
- package/bin/_cctally_journal.py +1156 -21
- package/bin/_cctally_journal_repair.py +6 -0
- package/bin/_cctally_parser.py +26 -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_statusline.py +6 -6
- package/bin/_cctally_store.py +1061 -40
- package/bin/_cctally_transcript.py +32 -2
- package/bin/_cctally_tui.py +54 -6
- package/bin/_lib_cache_report.py +8 -3
- package/bin/_lib_codex_conversation.py +851 -81
- package/bin/_lib_codex_conversation_query.py +2031 -96
- package/bin/_lib_codex_find_projection.py +517 -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 +168 -22
- package/bin/_lib_conversation_query.py +62 -2
- 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/_lib_stats_publish.py +243 -0
- package/bin/cctally +17 -3
- package/dashboard/static/assets/index-Dat-mza6.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 +8 -1
- package/dashboard/static/assets/index-CILAoEja.js +0 -90
|
@@ -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/_cctally_tui.py
CHANGED
|
@@ -2966,7 +2966,22 @@ def _tui_build_snapshot(
|
|
|
2966
2966
|
# ``_tui_build_snapshot_once`` closes its live stats connection before
|
|
2967
2967
|
# this boundary. The replacement-capable hook can therefore satisfy
|
|
2968
2968
|
# the whole-family drain gate, and the retry opens the published family.
|
|
2969
|
-
|
|
2969
|
+
#
|
|
2970
|
+
# #496 S3 §6: the heal now DEFERS, and its signal is a
|
|
2971
|
+
# `BaseException`. This call sits before the retry's own `except`, so
|
|
2972
|
+
# the shared deferral base is caught AT the heal-call boundary and the
|
|
2973
|
+
# corruption-typed degraded frame is built directly. The retry must not
|
|
2974
|
+
# spin: after deferral the index is still corrupt, so a second attempt
|
|
2975
|
+
# would only fail again, and the frame converges on a later tick.
|
|
2976
|
+
try:
|
|
2977
|
+
_tui_heal_post_query_stats(fault.cause)
|
|
2978
|
+
except _cctally().StatsRebuildDeferred as deferred:
|
|
2979
|
+
return _tui_stats_retry_degraded_snapshot(
|
|
2980
|
+
now_utc=now_utc or dt.datetime.now(dt.timezone.utc),
|
|
2981
|
+
exc=deferred,
|
|
2982
|
+
precompute_envelope=precompute_envelope,
|
|
2983
|
+
runtime_bind=runtime_bind,
|
|
2984
|
+
)
|
|
2970
2985
|
try:
|
|
2971
2986
|
return _tui_build_snapshot_once(
|
|
2972
2987
|
now_utc=now_utc,
|
|
@@ -3279,6 +3294,8 @@ def _tui_build_snapshot_once(
|
|
|
3279
3294
|
runtime_bind=runtime_bind, raw_config=raw_config,
|
|
3280
3295
|
display_tz_pref_override=display_tz_pref_override,
|
|
3281
3296
|
source_stats_conn=conn,
|
|
3297
|
+
failures=sync_failures,
|
|
3298
|
+
stats_heal_attempted=stats_heal_attempted,
|
|
3282
3299
|
source_display_tz_name=(
|
|
3283
3300
|
getattr(_build_display_tz, "key", None)
|
|
3284
3301
|
if _build_display_tz is not None else None
|
|
@@ -4068,7 +4085,9 @@ def _tui_build_idle_snapshot(prior, *, now_utc, precompute_envelope,
|
|
|
4068
4085
|
codex_ingest_contended=False,
|
|
4069
4086
|
codex_ingest_failed=False,
|
|
4070
4087
|
claude_ingest_contended=False,
|
|
4071
|
-
claude_ingest_failed=False
|
|
4088
|
+
claude_ingest_failed=False,
|
|
4089
|
+
failures=None,
|
|
4090
|
+
stats_heal_attempted=False):
|
|
4072
4091
|
"""Fresh snapshot reusing ``prior``'s heavy rows, re-patching only the
|
|
4073
4092
|
time-derived fields + the doctor payload / envelope precompute on each idle
|
|
4074
4093
|
tick (spec §3 idle path).
|
|
@@ -4108,6 +4127,7 @@ def _tui_build_idle_snapshot(prior, *, now_utc, precompute_envelope,
|
|
|
4108
4127
|
envelope_precompute = _tui_precompute_envelope_config(raw_config)
|
|
4109
4128
|
except Exception as exc: # noqa: BLE001 — never crash the rebuild
|
|
4110
4129
|
errors.append(f"envelope-precompute: {exc}")
|
|
4130
|
+
idle_failures = failures if failures is not None else []
|
|
4111
4131
|
source_bundle = prior.source_bundle
|
|
4112
4132
|
if source_bundle is not None:
|
|
4113
4133
|
try:
|
|
@@ -4180,15 +4200,36 @@ def _tui_build_idle_snapshot(prior, *, now_utc, precompute_envelope,
|
|
|
4180
4200
|
prior_bundle=source_bundle,
|
|
4181
4201
|
raw_config=raw_config,
|
|
4182
4202
|
)
|
|
4203
|
+
except _StatsSnapshotCorruption:
|
|
4204
|
+
raise
|
|
4183
4205
|
except Exception as exc: # noqa: BLE001 — retain prior complete bundle
|
|
4184
|
-
|
|
4206
|
+
# #496 S3 §8 (F16). This branch read stats through
|
|
4207
|
+
# `source_stats_conn` and swallowed the failure into a plain
|
|
4208
|
+
# string, so it built no `SyncFailureAttribution`, never raised
|
|
4209
|
+
# `_StatsSnapshotCorruption`, and never reached the heal — and
|
|
4210
|
+
# `_sync_failure_envelope`, finding no typed attribution, fell to
|
|
4211
|
+
# its raw-text matcher and told the user to run
|
|
4212
|
+
# `cctally cache-sync --rebuild` for a STATS fault. Classify it at
|
|
4213
|
+
# the catch site instead, against the connection that faulted.
|
|
4214
|
+
if source_stats_conn is not None:
|
|
4215
|
+
_tui_capture_sync_failure(
|
|
4216
|
+
source_stats_conn,
|
|
4217
|
+
errors,
|
|
4218
|
+
idle_failures,
|
|
4219
|
+
leg="source-clock-refresh",
|
|
4220
|
+
database="stats_or_cache",
|
|
4221
|
+
exc=exc,
|
|
4222
|
+
stats_heal_attempted=stats_heal_attempted,
|
|
4223
|
+
)
|
|
4224
|
+
else:
|
|
4225
|
+
errors.append(f"source-clock-refresh: {exc}")
|
|
4185
4226
|
source_bundle = prior.source_bundle
|
|
4186
4227
|
return dataclasses.replace(
|
|
4187
4228
|
prior,
|
|
4188
4229
|
generated_at=now_utc,
|
|
4189
4230
|
last_sync_at=time.monotonic(),
|
|
4190
4231
|
last_sync_error=("; ".join(errors) if errors else None),
|
|
4191
|
-
sync_failures=(),
|
|
4232
|
+
sync_failures=tuple(idle_failures),
|
|
4192
4233
|
doctor_payload=doctor_payload,
|
|
4193
4234
|
envelope_precompute=envelope_precompute,
|
|
4194
4235
|
source_bundle=source_bundle,
|
|
@@ -6611,12 +6652,17 @@ def _make_run_sync_now_locked(*, ref, hub, pinned_now, display_tz_pref_override,
|
|
|
6611
6652
|
snap = dataclasses.replace(snap, last_sync_at=None, hydrating=False)
|
|
6612
6653
|
ref.set(snap)
|
|
6613
6654
|
hub.publish(snap)
|
|
6614
|
-
except _cctally().
|
|
6655
|
+
except _cctally().StatsRebuildDeferred as exc:
|
|
6615
6656
|
# #453: the first periodic tick runs before HTTP bind. Preserve the
|
|
6616
6657
|
# initial hydrating/degraded frame while the dedicated replay owns
|
|
6617
6658
|
# stats maintenance; a generic crash frame would clear the latch
|
|
6618
6659
|
# before any client could observe it. The loop retries normally on
|
|
6619
6660
|
# its next cadence and publishes a full frame after convergence.
|
|
6661
|
+
#
|
|
6662
|
+
# #496 S3: the two deferral classes must NOT flatten here. A wrong
|
|
6663
|
+
# EPOCH is a readable index (`corruption=False`); a deferred heal is
|
|
6664
|
+
# an index that could not be read, and reporting that as
|
|
6665
|
+
# non-corruption makes the envelope name the wrong fault.
|
|
6620
6666
|
prev = ref.get()
|
|
6621
6667
|
pending = dataclasses.replace(
|
|
6622
6668
|
prev,
|
|
@@ -6625,7 +6671,9 @@ def _make_run_sync_now_locked(*, ref, hub, pinned_now, display_tz_pref_override,
|
|
|
6625
6671
|
SyncFailureAttribution(
|
|
6626
6672
|
leg="stats-open",
|
|
6627
6673
|
database="stats",
|
|
6628
|
-
corruption=
|
|
6674
|
+
corruption=isinstance(
|
|
6675
|
+
exc, _cctally().StatsHealDeferred
|
|
6676
|
+
),
|
|
6629
6677
|
),
|
|
6630
6678
|
),
|
|
6631
6679
|
generated_at=dt.datetime.now(dt.timezone.utc),
|
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")
|