cctally 1.90.1 → 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 +74 -0
- package/README.md +2 -2
- package/bin/_cctally_cache.py +863 -74
- package/bin/_cctally_config.py +57 -0
- package/bin/_cctally_core.py +53 -8
- package/bin/_cctally_dashboard.py +146 -5
- package/bin/_cctally_dashboard_conversation.py +164 -18
- package/bin/_cctally_dashboard_envelope.py +69 -12
- package/bin/_cctally_dashboard_sources.py +27 -1
- 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_cache_report_wire.py +8 -20
- package/bin/_lib_codex_conversation.py +959 -81
- package/bin/_lib_codex_conversation_query.py +2792 -167
- 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_reasoning_headings.py +73 -0
- package/bin/_lib_codex_segments.py +259 -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_dashboard_sources.py +33 -32
- 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-DnWdv8um.css +1 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +9 -1
- package/dashboard/static/assets/index-Bar8-S1i.css +0 -1
- package/dashboard/static/assets/index-CRogVlEC.js +0 -92
|
@@ -33,7 +33,10 @@ CapabilityStatus = Literal[
|
|
|
33
33
|
# field. Additive and omitted-when-zero, so the normal payload is byte-identical
|
|
34
34
|
# — but the same `execvp` transition applies, so the bump ships as the signal it
|
|
35
35
|
# has always been rather than as a mechanism the client branches on.
|
|
36
|
-
|
|
36
|
+
# 3 -> 4 (#465): the Codex cache report retired its transitional
|
|
37
|
+
# `cache_hit_percent` alias and changed structurally inapplicable figures from
|
|
38
|
+
# numeric placeholders to null.
|
|
39
|
+
SOURCE_SCHEMA_VERSION = 4
|
|
37
40
|
DEFAULT_SOURCE = "claude"
|
|
38
41
|
SOURCE_ORDER = ("claude", "codex", "all")
|
|
39
42
|
SOURCE_FRESHNESS_DOMAINS = ("hero", "quota", "sessions")
|
|
@@ -392,13 +395,11 @@ def _combined_metrics(
|
|
|
392
395
|
) -> Mapping[str, object] | None:
|
|
393
396
|
if not (_coherent_provider(claude) and _coherent_provider(codex)):
|
|
394
397
|
return None
|
|
395
|
-
# #
|
|
396
|
-
#
|
|
397
|
-
# the
|
|
398
|
-
#
|
|
399
|
-
#
|
|
400
|
-
if _stale_cycle_providers(claude, codex):
|
|
401
|
-
return None
|
|
398
|
+
# #359: the hero counters are backward-looking accounting actuals. A stale
|
|
399
|
+
# but still-live quota boundary pauses projections; it does not invalidate
|
|
400
|
+
# the retained cost/token sums that each provider already keeps visible.
|
|
401
|
+
# Composition therefore retains the compatible number and discloses the
|
|
402
|
+
# stale boundary through All's hero-domain freshness + local warning.
|
|
402
403
|
for state in (claude, codex):
|
|
403
404
|
hero_capability = state.capabilities.get("hero")
|
|
404
405
|
if hero_capability is None or hero_capability.status not in {"supported", "derived"}:
|
|
@@ -462,33 +463,33 @@ def compose_all_state(
|
|
|
462
463
|
raise ValueError("all composition requires Claude and Codex provider states")
|
|
463
464
|
combined = _combined_metrics(claude, codex)
|
|
464
465
|
providers_coherent = _coherent_provider(claude) and _coherent_provider(codex)
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
#
|
|
469
|
-
#
|
|
470
|
-
#
|
|
471
|
-
# provider envelope. It is emitted only when the providers are otherwise
|
|
472
|
-
# coherent; an incoherent provider already publishes its own reason.
|
|
466
|
+
stale_cycle_providers = (
|
|
467
|
+
_stale_cycle_providers(claude, codex) if providers_coherent else ()
|
|
468
|
+
)
|
|
469
|
+
# #359: the warning qualifies a retained combined actual. It stays
|
|
470
|
+
# All-local and keeps the composed source partial so the header status also
|
|
471
|
+
# names the caveat; provider envelopes remain independently coherent.
|
|
473
472
|
all_local_warnings: tuple[SourceDashboardWarning, ...] = ()
|
|
473
|
+
if stale_cycle_providers:
|
|
474
|
+
all_local_warnings = (SourceDashboardWarning(
|
|
475
|
+
"combined_totals_stale",
|
|
476
|
+
f"{' and '.join(stale_cycle_providers)} quota evidence is stale; "
|
|
477
|
+
"combined totals use retained actuals.",
|
|
478
|
+
"hero",
|
|
479
|
+
),)
|
|
474
480
|
if providers_coherent:
|
|
475
|
-
stale_cycle_providers = _stale_cycle_providers(claude, codex)
|
|
476
481
|
if stale_cycle_providers:
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
"
|
|
481
|
-
"
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
"empty"
|
|
488
|
-
if claude.availability == "empty" and codex.availability == "empty"
|
|
489
|
-
else "ok"
|
|
482
|
+
availability: Availability = "partial"
|
|
483
|
+
else:
|
|
484
|
+
availability = (
|
|
485
|
+
"partial"
|
|
486
|
+
if combined is None or "partial" in (claude.availability, codex.availability)
|
|
487
|
+
else (
|
|
488
|
+
"empty"
|
|
489
|
+
if claude.availability == "empty" and codex.availability == "empty"
|
|
490
|
+
else "ok"
|
|
491
|
+
)
|
|
490
492
|
)
|
|
491
|
-
)
|
|
492
493
|
freshness: Freshness = "fresh"
|
|
493
494
|
else:
|
|
494
495
|
availability = "partial"
|
|
@@ -519,7 +520,7 @@ def compose_all_state(
|
|
|
519
520
|
# All-LOCAL warnings lead: `warningForSource` on the client falls back to
|
|
520
521
|
# the FIRST warning, so a merely-partial provider warning (e.g.
|
|
521
522
|
# `codex_metadata_incomplete`) would otherwise pre-empt the chip label
|
|
522
|
-
# and hide the
|
|
523
|
+
# and hide the stale qualification on the combined actual.
|
|
523
524
|
warnings=tuple((*all_local_warnings, *claude.warnings, *codex.warnings)),
|
|
524
525
|
data_version=data_version,
|
|
525
526
|
last_success_at=last_success_at,
|
package/bin/_lib_doctor.py
CHANGED
|
@@ -210,6 +210,11 @@ class DoctorState:
|
|
|
210
210
|
# walk, so `files_failed`/`files_deferred_torn` are both zero, no `blocked`
|
|
211
211
|
# record is ever written, and the ingest-backlog leg reads a drained store.
|
|
212
212
|
codex_replay_deferred: Optional[dict] = None
|
|
213
|
+
# #485: privacy-safe durable records written when cache.db and/or
|
|
214
|
+
# conversations.db refused to treat an invalid Codex root as deletion
|
|
215
|
+
# evidence. None is the normal state; each record contains counts/reasons
|
|
216
|
+
# but never configured paths or provider identifiers.
|
|
217
|
+
codex_prune_refusals: Optional[list[dict]] = None
|
|
213
218
|
# #279 S2 (F5b): PRAGMA quick_check(1) results, gathered ONLY under
|
|
214
219
|
# doctor_gather_state(deep=True) (CLI cmd_doctor) — the dashboard
|
|
215
220
|
# rebuild loop calls the gather every rebuild and quick_check on a
|
|
@@ -1236,6 +1241,64 @@ def _check_data_codex_replay(s: DoctorState) -> CheckResult:
|
|
|
1236
1241
|
)
|
|
1237
1242
|
|
|
1238
1243
|
|
|
1244
|
+
def _check_data_codex_prune_safety(s: DoctorState) -> CheckResult:
|
|
1245
|
+
"""Surface a fail-closed orphan-prune decision instead of silent loss."""
|
|
1246
|
+
records = [
|
|
1247
|
+
record for record in (s.codex_prune_refusals or [])
|
|
1248
|
+
if isinstance(record, dict)
|
|
1249
|
+
]
|
|
1250
|
+
if not records:
|
|
1251
|
+
return CheckResult(
|
|
1252
|
+
id="data.codex_prune_safety",
|
|
1253
|
+
title="Codex orphan pruning",
|
|
1254
|
+
severity="ok",
|
|
1255
|
+
summary="no refused prune",
|
|
1256
|
+
remediation=None,
|
|
1257
|
+
details={"refusalCount": 0, "stores": []},
|
|
1258
|
+
)
|
|
1259
|
+
stores = sorted({
|
|
1260
|
+
str(record.get("store"))
|
|
1261
|
+
for record in records
|
|
1262
|
+
if record.get("store") in {"cache", "conversations"}
|
|
1263
|
+
})
|
|
1264
|
+
preserved_files = max(
|
|
1265
|
+
(int(record.get("preservedFileCount") or 0) for record in records),
|
|
1266
|
+
default=0,
|
|
1267
|
+
)
|
|
1268
|
+
since_values = sorted({
|
|
1269
|
+
str(record["since"])
|
|
1270
|
+
for record in records
|
|
1271
|
+
if isinstance(record.get("since"), str) and record["since"]
|
|
1272
|
+
})
|
|
1273
|
+
reasons = sorted({
|
|
1274
|
+
str(reason)
|
|
1275
|
+
for record in records
|
|
1276
|
+
for reason in (
|
|
1277
|
+
record.get("reasons") if isinstance(record.get("reasons"), list) else []
|
|
1278
|
+
)
|
|
1279
|
+
if isinstance(reason, str)
|
|
1280
|
+
})
|
|
1281
|
+
return CheckResult(
|
|
1282
|
+
id="data.codex_prune_safety",
|
|
1283
|
+
title="Codex orphan pruning",
|
|
1284
|
+
severity="warn",
|
|
1285
|
+
summary=(
|
|
1286
|
+
f"refused unsafe prune; preserved {preserved_files} tracked file(s)"
|
|
1287
|
+
),
|
|
1288
|
+
remediation=(
|
|
1289
|
+
"Check that every $CODEX_HOME root is mounted and contains Codex "
|
|
1290
|
+
"rollout JSONL, then run `cctally cache-sync --source codex`"
|
|
1291
|
+
),
|
|
1292
|
+
details={
|
|
1293
|
+
"refusalCount": len(records),
|
|
1294
|
+
"stores": stores,
|
|
1295
|
+
"since": since_values[0] if since_values else None,
|
|
1296
|
+
"reasons": reasons,
|
|
1297
|
+
"preservedFileCount": preserved_files,
|
|
1298
|
+
},
|
|
1299
|
+
)
|
|
1300
|
+
|
|
1301
|
+
|
|
1239
1302
|
def _check_data_codex_quota_verification(s: DoctorState) -> CheckResult:
|
|
1240
1303
|
"""WARN when the detached Codex quota verification is not landing.
|
|
1241
1304
|
|
|
@@ -2935,6 +2998,7 @@ _CATEGORY_DEFINITIONS: tuple[tuple[str, str, tuple[tuple[str, str], ...]], ...]
|
|
|
2935
2998
|
# tests/test_doctor_codex_project_metadata.py), so the replay leg goes
|
|
2936
2999
|
# after the pair rather than between them.
|
|
2937
3000
|
("data.codex_project_metadata", "_check_data_codex_project_metadata"),
|
|
3001
|
+
("data.codex_prune_safety", "_check_data_codex_prune_safety"),
|
|
2938
3002
|
("data.codex_replay", "_check_data_codex_replay"),
|
|
2939
3003
|
("data.codex_ingest_backlog", "_check_data_codex_ingest_backlog"),
|
|
2940
3004
|
("data.codex_quota", "_check_data_codex_quota"),
|
|
@@ -23,9 +23,10 @@ in ``quota_window_snapshots`` moving at all:
|
|
|
23
23
|
and becomes eligible when wall time passes it with no mutation to observe.
|
|
24
24
|
Persisting that boundary and treating ``now >= boundary`` as dirty is what
|
|
25
25
|
closes it. Unlike axes 2 and 3 this one fires on WALL CLOCK rather than on a
|
|
26
|
-
configuration change
|
|
27
|
-
|
|
28
|
-
a blocking
|
|
26
|
+
configuration change. An ownership schedule lets the hook evaluate only the
|
|
27
|
+
complete roots whose deadlines matured; scalar-only legacy state still
|
|
28
|
+
defers rather than paying an unannounced whole-history pass on a blocking
|
|
29
|
+
tick.
|
|
29
30
|
5. **Durable lifecycle state** — the existing arming rows and terminal events,
|
|
30
31
|
unchanged. Represented here only as the fingerprints axis 2 compares.
|
|
31
32
|
|
|
@@ -80,6 +81,7 @@ def alert_dirty_scope(
|
|
|
80
81
|
gate_after: bool,
|
|
81
82
|
now: dt.datetime,
|
|
82
83
|
next_evaluation_at: "dt.datetime | None",
|
|
84
|
+
scheduled_roots: "Iterable[str] | None" = None,
|
|
83
85
|
defer_scheduled: bool = False,
|
|
84
86
|
) -> AlertDirtyScope:
|
|
85
87
|
"""Resolve the five axes into one decision.
|
|
@@ -89,14 +91,11 @@ def alert_dirty_scope(
|
|
|
89
91
|
observed_slot, window_minutes)``; the ROOT is element 1, which is what an
|
|
90
92
|
exact-rule change is scoped to.
|
|
91
93
|
|
|
92
|
-
``
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
``REASON_SCHEDULED_DEFERRED`` and does NOT strengthen the scope — and the
|
|
98
|
-
caller owes the stored boundary a carry-through, because a deferral that
|
|
99
|
-
lets the boundary be recomputed is a silent drop.
|
|
94
|
+
``scheduled_roots`` is the validated ownership retained with axis 4. When
|
|
95
|
+
present, a matured instant scopes to those roots even on the hook path.
|
|
96
|
+
``None`` is the legacy/unavailable-ownership shape; only that shape needs
|
|
97
|
+
``defer_scheduled`` to avoid an unannounced whole-history hook pass, and the
|
|
98
|
+
caller then owes the scalar boundary a carry-through.
|
|
100
99
|
"""
|
|
101
100
|
reasons: list[str] = []
|
|
102
101
|
if not gate_after:
|
|
@@ -132,11 +131,19 @@ def alert_dirty_scope(
|
|
|
132
131
|
reasons.append("rule_changed")
|
|
133
132
|
|
|
134
133
|
if next_evaluation_at is not None and now >= next_evaluation_at:
|
|
135
|
-
#
|
|
136
|
-
#
|
|
137
|
-
#
|
|
138
|
-
#
|
|
139
|
-
|
|
134
|
+
# Epoch 1007 records the roots owning each scheduled instant. A complete
|
|
135
|
+
# semantic pass over those roots is bounded enough for the hook path and
|
|
136
|
+
# is all axis 4 needs. ``None`` means legacy/unavailable ownership, where
|
|
137
|
+
# the only honest scope remains everything (and therefore deferral on a
|
|
138
|
+
# hook tick). An empty known set means the owning roots are not lifecycle
|
|
139
|
+
# eligible on this tick; the stored axis remains due for a later tick.
|
|
140
|
+
if scheduled_roots is not None:
|
|
141
|
+
due_roots = {str(root) for root in scheduled_roots if str(root)}
|
|
142
|
+
if due_roots:
|
|
143
|
+
scope = _strongest(scope, SCOPE_ROOTS)
|
|
144
|
+
roots |= due_roots
|
|
145
|
+
reasons.append("scheduled")
|
|
146
|
+
elif defer_scheduled:
|
|
140
147
|
reasons.append(REASON_SCHEDULED_DEFERRED)
|
|
141
148
|
else:
|
|
142
149
|
scope = _strongest(scope, SCOPE_ALL)
|
|
@@ -154,12 +161,12 @@ def next_evaluation_boundary(
|
|
|
154
161
|
) -> "dt.datetime | None":
|
|
155
162
|
"""The earliest still-future capture the projector must come back for.
|
|
156
163
|
|
|
157
|
-
A bounded pass only sees
|
|
164
|
+
This is the legacy scalar helper. A bounded pass only sees dirty windows, so
|
|
165
|
+
the STORED boundary is
|
|
158
166
|
retained whenever it is still in the future: dropping it would forget a
|
|
159
167
|
future-clocked observation sitting in a window this pass never loaded. Once
|
|
160
|
-
wall time passes it the axis fires
|
|
161
|
-
|
|
162
|
-
ever cost one extra pass, never a missed one.
|
|
168
|
+
wall time passes it the axis fires and the caller decides whether it has
|
|
169
|
+
enough ownership to scope the pass.
|
|
163
170
|
|
|
164
171
|
``retain_due`` keeps a boundary that is ALREADY due, which is the case where
|
|
165
172
|
"recomputed from complete evidence" is a lie: a reporting-only pass never
|
|
@@ -167,20 +174,10 @@ def next_evaluation_boundary(
|
|
|
167
174
|
the widening deliberately did not look. Either would otherwise retire the
|
|
168
175
|
axis on behalf of an evaluation nobody performed.
|
|
169
176
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
hook is the only production caller that carries it.
|
|
175
|
-
|
|
176
|
-
It does NOT stay "until a pass that can act on it does", and that gap is
|
|
177
|
-
open rather than closed: on a hook-only install with a steady enabled gate,
|
|
178
|
-
unchanged rules and a quiet ledger, no qualifying pass ever runs and the
|
|
179
|
-
instant is retained indefinitely. The cost is bounded — the tick stays
|
|
180
|
-
bounded and fast, and the window is re-evaluated as soon as it goes
|
|
181
|
-
ledger-dirty again, which for a live window is continuous — so the exposure
|
|
182
|
-
is a future-clocked capture in a window that then goes permanently quiet
|
|
183
|
-
never qualifying a threshold. Under-alerting, never a stall or a burst.
|
|
177
|
+
Epoch 1007's per-root map is maintained by the projector rather than this
|
|
178
|
+
helper. It closes the quiet-window gap by letting a hook tick replace only
|
|
179
|
+
the roots it evaluated; scalar-only legacy state still uses ``retain_due``
|
|
180
|
+
and the conservative full/deferred path.
|
|
184
181
|
"""
|
|
185
182
|
candidates = [value for value in capture_times if value > now]
|
|
186
183
|
if stored is not None and (retain_due or stored > now):
|