cctally 1.117.2 → 1.118.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 +30 -0
- package/README.md +7 -3
- package/bin/_cctally_cache.py +2783 -175
- package/bin/_cctally_completion.py +106 -33
- package/bin/_cctally_core.py +69 -4
- package/bin/_cctally_dashboard.py +188 -13
- package/bin/_cctally_dashboard_conversation.py +5 -1
- package/bin/_cctally_dashboard_envelope.py +50 -19
- package/bin/_cctally_dashboard_sources.py +23 -1
- package/bin/_cctally_db.py +193 -153
- package/bin/_cctally_diagnosis_sources.py +63 -7
- package/bin/_cctally_doctor.py +865 -93
- package/bin/_cctally_five_hour.py +67 -50
- package/bin/_cctally_forecast.py +35 -13
- package/bin/_cctally_journal.py +186 -18
- package/bin/_cctally_milestone_history.py +859 -62
- package/bin/_cctally_oauth_samples.py +286 -15
- package/bin/_cctally_parser.py +2 -2
- package/bin/_cctally_quota.py +104 -12
- package/bin/_cctally_record.py +153 -29
- package/bin/_cctally_rederive.py +608 -68
- package/bin/_cctally_statusline.py +108 -81
- package/bin/_cctally_store.py +179 -38
- package/bin/_cctally_tui.py +892 -181
- package/bin/_cctally_update.py +32 -13
- package/bin/_lib_codex_conversation_query.py +52 -14
- package/bin/_lib_codex_drain_admission.py +224 -6
- package/bin/_lib_codex_metadata.py +39 -0
- package/bin/_lib_conversation_anon.py +15 -8
- package/bin/_lib_conversation_dispatch.py +7 -0
- package/bin/_lib_conversation_query.py +289 -70
- package/bin/_lib_dashboard_sources.py +37 -1
- package/bin/_lib_detector_access_registry.py +497 -26
- package/bin/_lib_doctor.py +85 -20
- package/bin/_lib_doctor_scan_index.py +321 -0
- package/bin/_lib_ingest_frontier.py +20 -1
- package/bin/_lib_oauth_samples.py +23 -7
- package/bin/_lib_pricing.py +146 -16
- package/bin/_lib_quota.py +19 -0
- package/bin/_lib_rederive.py +11 -6
- package/bin/_lib_retained_records.py +516 -0
- package/bin/_lib_snapshot_cache.py +10 -0
- package/bin/_lib_stale_proof.py +272 -43
- package/bin/_lib_stats_content_revision.py +831 -0
- package/bin/_lib_write_io.py +11 -4
- package/bin/cctally +4 -1
- package/dashboard/static/assets/ConversationsView-D4Iv4ku0.js +72 -0
- package/dashboard/static/assets/{DoctorModal-B0DnU9d9.js → DoctorModal-D4QSq6uz.js} +1 -1
- package/dashboard/static/assets/ModalRoot-C9F5dhZi.js +1 -0
- package/dashboard/static/assets/{ProjectsDrillPanel-DSxXQSa-.js → ProjectsDrillPanel-WxAdRDMt.js} +1 -1
- package/dashboard/static/assets/{SourceDetailModal-BleDpQqG.js → SourceDetailModal-NyyEfXmc.js} +1 -1
- package/dashboard/static/assets/{UpdateModal-CMqaAkSK.js → UpdateModal-CFLq9pT1.js} +1 -1
- package/dashboard/static/assets/{index-BjQV-0-D.js → index-BKTWH9JE.js} +2 -2
- package/dashboard/static/assets/{outlineNavigation-CO-VWBm7.js → outlineNavigation-BnGUwCS3.js} +3 -3
- package/dashboard/static/dashboard.html +1 -1
- package/package.json +4 -1
- package/dashboard/static/assets/ConversationsView-oqNsWzWV.js +0 -72
- package/dashboard/static/assets/ModalRoot-CYua2wP_.js +0 -1
package/bin/_lib_doctor.py
CHANGED
|
@@ -281,7 +281,7 @@ class DoctorState:
|
|
|
281
281
|
conversations_db_quick_check: Optional[str] = None
|
|
282
282
|
# #279 S2 (F5c): non-blocking flock probes on the two sync lock files
|
|
283
283
|
# (name -> True held / False free / None unreadable). Probe never
|
|
284
|
-
# creates files (doctor
|
|
284
|
+
# creates files (doctor never changes your data). None = probe errored.
|
|
285
285
|
locks_held: Optional[dict] = None
|
|
286
286
|
# #297: size in bytes of cache.db-wal, gathered read-only in
|
|
287
287
|
# doctor_gather_state (getsize, OSError/absent -> None). Warns above
|
|
@@ -485,6 +485,14 @@ class DoctorState:
|
|
|
485
485
|
# launch ledger, process identities probed before a re-read, pending
|
|
486
486
|
# marker). None = not gathered; the check then reads both as never run.
|
|
487
487
|
rebuild_workers: Optional[dict] = None
|
|
488
|
+
# #1018: the newest admitted Claude weekly reading (the observation
|
|
489
|
+
# index), no later than `now_utc`. `latest_snapshot_at` keeps its
|
|
490
|
+
# held-inclusive materialized meaning; `data.latest_snapshot_age` takes
|
|
491
|
+
# the newer of the two.
|
|
492
|
+
latest_reading_at: Optional[dt.datetime] = None
|
|
493
|
+
# #1018 C6: why freshness evidence could not be read — None,
|
|
494
|
+
# "stats_maintenance" or "stats_unreadable".
|
|
495
|
+
freshness_unavailable_reason: Optional[str] = None
|
|
488
496
|
|
|
489
497
|
|
|
490
498
|
@dataclasses.dataclass(frozen=True)
|
|
@@ -1059,7 +1067,7 @@ def _check_db_version_ahead(s: DoctorState) -> CheckResult:
|
|
|
1059
1067
|
# current index as a mismatch; keep this in lockstep with core
|
|
1060
1068
|
# (#496 S5b §6.1). It stays a literal because this kernel is pure and
|
|
1061
1069
|
# must not import `_cctally_core`.
|
|
1062
|
-
epoch =
|
|
1070
|
+
epoch = 1024
|
|
1063
1071
|
mismatch = uv > legacy_head and uv != epoch
|
|
1064
1072
|
return {"user_version": uv, "legacy_head": legacy_head, "epoch": epoch,
|
|
1065
1073
|
"mismatch": mismatch}
|
|
@@ -1376,29 +1384,86 @@ def rebuild_worker_check(rebuild_workers, now_utc) -> CheckResult:
|
|
|
1376
1384
|
)
|
|
1377
1385
|
|
|
1378
1386
|
|
|
1387
|
+
_FRESHNESS_UNAVAILABLE = {
|
|
1388
|
+
"stats_maintenance": (
|
|
1389
|
+
"unavailable (stats.db maintenance in progress)",
|
|
1390
|
+
"Retry after the stats.db maintenance finishes.",
|
|
1391
|
+
),
|
|
1392
|
+
"stats_unreadable": (
|
|
1393
|
+
"unavailable (stats.db could not be read)",
|
|
1394
|
+
"See the stats.db check.",
|
|
1395
|
+
),
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
|
|
1379
1399
|
def _check_data_latest_snapshot_age(s: DoctorState) -> CheckResult:
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1400
|
+
"""Is the usage write path alive? (#1018, spec 5.4.)
|
|
1401
|
+
|
|
1402
|
+
The evidence is the newer of the held-inclusive materialized snapshot and
|
|
1403
|
+
the newest admitted reading, each counted only when no later than now.
|
|
1404
|
+
Fresh positive evidence decides even when a source failed; stale or absent
|
|
1405
|
+
evidence with a failed source is "unavailable", never silence.
|
|
1406
|
+
"""
|
|
1407
|
+
now = s.now_utc
|
|
1408
|
+
|
|
1409
|
+
def _valid(moment):
|
|
1410
|
+
return moment if moment is not None and moment <= now else None
|
|
1411
|
+
|
|
1412
|
+
def _age(moment):
|
|
1413
|
+
return int((now - moment).total_seconds())
|
|
1414
|
+
|
|
1415
|
+
snapshot = _valid(s.latest_snapshot_at)
|
|
1416
|
+
reading = _valid(s.latest_reading_at)
|
|
1417
|
+
valid = [moment for moment in (snapshot, reading) if moment is not None]
|
|
1418
|
+
candidate = max(valid) if valid else None
|
|
1419
|
+
cand_age = _age(candidate) if candidate is not None else None
|
|
1420
|
+
reason = s.freshness_unavailable_reason
|
|
1421
|
+
|
|
1422
|
+
effective = candidate
|
|
1423
|
+
if candidate is not None and cand_age <= 300:
|
|
1424
|
+
sev, summary, rem = "ok", f"{cand_age}s ago", None
|
|
1425
|
+
elif reason:
|
|
1391
1426
|
sev = "warn"
|
|
1427
|
+
summary, rem = _FRESHNESS_UNAVAILABLE.get(
|
|
1428
|
+
reason, _FRESHNESS_UNAVAILABLE["stats_unreadable"])
|
|
1429
|
+
effective = None
|
|
1430
|
+
elif candidate is not None and cand_age <= 3600:
|
|
1431
|
+
sev, summary = "warn", f"{cand_age}s ago"
|
|
1392
1432
|
rem = "Recent but not current. Check Claude Code session is active."
|
|
1393
|
-
|
|
1394
|
-
sev = "fail"
|
|
1433
|
+
elif candidate is not None:
|
|
1434
|
+
sev, summary = "fail", f"{cand_age}s ago"
|
|
1395
1435
|
rem = "No snapshot in >1h. Hooks may be broken — check `cctally setup --status`."
|
|
1436
|
+
else:
|
|
1437
|
+
sev, summary = "fail", "never"
|
|
1438
|
+
rem = "Check hooks are installed and Claude Code is running"
|
|
1439
|
+
|
|
1440
|
+
details: dict = {
|
|
1441
|
+
"latest_snapshot_at": (
|
|
1442
|
+
s.latest_snapshot_at.isoformat()
|
|
1443
|
+
if s.latest_snapshot_at is not None else None
|
|
1444
|
+
),
|
|
1445
|
+
}
|
|
1446
|
+
if s.latest_snapshot_at is not None:
|
|
1447
|
+
details["latest_snapshot_age_s"] = (
|
|
1448
|
+
_age(snapshot) if snapshot is not None else None
|
|
1449
|
+
)
|
|
1450
|
+
details.update({
|
|
1451
|
+
"latest_reading_at": (
|
|
1452
|
+
s.latest_reading_at.isoformat()
|
|
1453
|
+
if s.latest_reading_at is not None else None
|
|
1454
|
+
),
|
|
1455
|
+
"latest_reading_age_s": _age(reading) if reading is not None else None,
|
|
1456
|
+
"effective_freshness_at": (
|
|
1457
|
+
effective.isoformat() if effective is not None else None
|
|
1458
|
+
),
|
|
1459
|
+
"effective_freshness_age_s": (
|
|
1460
|
+
_age(effective) if effective is not None else None
|
|
1461
|
+
),
|
|
1462
|
+
"freshness_unavailable_reason": reason,
|
|
1463
|
+
})
|
|
1396
1464
|
return CheckResult(
|
|
1397
1465
|
id="data.latest_snapshot_age", title="Latest snapshot",
|
|
1398
|
-
severity=sev, summary=
|
|
1399
|
-
remediation=rem,
|
|
1400
|
-
details={"latest_snapshot_at": s.latest_snapshot_at.isoformat(),
|
|
1401
|
-
"latest_snapshot_age_s": age_s},
|
|
1466
|
+
severity=sev, summary=summary, remediation=rem, details=details,
|
|
1402
1467
|
)
|
|
1403
1468
|
|
|
1404
1469
|
|
|
@@ -2386,7 +2451,7 @@ def _check_data_conversation_sessions_rollup(s: DoctorState) -> CheckResult:
|
|
|
2386
2451
|
``doctor_gather_state`` from a NON-BLOCKING ``conversations.db.lock`` flock probe
|
|
2387
2452
|
(lock held ⇒ a writer is mid-flight) AND the presence of any pending
|
|
2388
2453
|
``cache_meta`` reingest/split/backfill flag; if either says in-progress, this
|
|
2389
|
-
stays OK. Doctor
|
|
2454
|
+
stays OK. Doctor never changes your data and never blocks on the lock.
|
|
2390
2455
|
|
|
2391
2456
|
Informational only (no remediation): the next conversation sync re-derives
|
|
2392
2457
|
the rollup via its incremental DELETE+INSERT.
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"""The deep doctor journal scan's checkpoint — the pure kernel (#1054).
|
|
2
|
+
|
|
3
|
+
`cctally doctor` decodes every journal line on every deep run to compute its
|
|
4
|
+
journal legs: the malformed and torn counts, the same-revision conflicts, the
|
|
5
|
+
structural protocol violations and acknowledgements, the cutover value and the
|
|
6
|
+
excluded population. On a production-sized journal that is about 2.6 million
|
|
7
|
+
decodes, of which roughly 5 % are records the scan keeps; the rest are
|
|
8
|
+
observations that only consume a selector position.
|
|
9
|
+
|
|
10
|
+
Doctor therefore keeps one disposable file beside the segments,
|
|
11
|
+
``JOURNAL_DIR/.doctor-scan-index``. Per segment it records a SHA-256 of the
|
|
12
|
+
complete-line prefix it scanned, the number of decoded records and malformed
|
|
13
|
+
lines in that prefix, and the line offset and decoded ordinal of every record
|
|
14
|
+
that matters to the scan (a "hit"). A later run proves the prefix against the
|
|
15
|
+
current bytes, decodes only the hits, fills every other position with the
|
|
16
|
+
``None`` placeholder the selector expects, and decodes the fresh suffix. The
|
|
17
|
+
excluded population is cached the same way, under its own digest of the range
|
|
18
|
+
it classified and an explicit fingerprint of the admission boundary.
|
|
19
|
+
|
|
20
|
+
**Nothing here is trusted without a content proof.** There is no stat-identity
|
|
21
|
+
shortcut: every reuse hashes the bytes it skips. A document whose version or
|
|
22
|
+
checksum does not match is ignored as a whole, and an entry that fails the
|
|
23
|
+
structural rules below is dropped while the others are kept. Torn status is
|
|
24
|
+
never stored, because a torn tail can be repaired and re-torn at the same size.
|
|
25
|
+
|
|
26
|
+
**The version is derived, not stamped.** `index_version` hashes `SHAPE_VERSION`
|
|
27
|
+
with the descriptors the glue supplies (the retained record types, the special
|
|
28
|
+
ids, the protocol-resolution kinds, every constant the admission predicate
|
|
29
|
+
reads and the stats epoch), so a change to any of those invalidates every
|
|
30
|
+
checkpoint. `SHAPE_VERSION` is bumped by hand for any change those inputs do
|
|
31
|
+
not capture: the meaning of a stored field, the decoder, or the scan's
|
|
32
|
+
per-record steps.
|
|
33
|
+
|
|
34
|
+
This module performs no I/O and imports nothing from the journal modules, the
|
|
35
|
+
same rule `bin/_lib_segment_summary.py` and `bin/_lib_journal_router.py` follow,
|
|
36
|
+
so every rule here is unit-testable without a journal on disk. The glue — the
|
|
37
|
+
read, the per-record step, the exclusion read and the atomic publication —
|
|
38
|
+
lives in `bin/_cctally_doctor.py`.
|
|
39
|
+
"""
|
|
40
|
+
from __future__ import annotations
|
|
41
|
+
|
|
42
|
+
import dataclasses
|
|
43
|
+
import hashlib
|
|
44
|
+
import json
|
|
45
|
+
import re
|
|
46
|
+
from typing import Iterable
|
|
47
|
+
|
|
48
|
+
#: Bumped by hand whenever a stored field's meaning, the decoder semantics or
|
|
49
|
+
#: the scan's per-record steps change in a way the derived descriptors do not
|
|
50
|
+
#: capture (see `docs/journal-gotchas.md`).
|
|
51
|
+
SHAPE_VERSION = 1
|
|
52
|
+
|
|
53
|
+
_HEX_DIGEST = re.compile(r"\A[0-9a-f]{64}\Z")
|
|
54
|
+
_ENTRY_KEYS = frozenset(
|
|
55
|
+
("name", "covered", "sha256", "decoded", "malformed", "hits", "exclusion"))
|
|
56
|
+
_EXCLUSION_KEYS = frozenset(("boundary", "lo", "covered", "sha256", "excluded"))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclasses.dataclass(frozen=True)
|
|
60
|
+
class Exclusion:
|
|
61
|
+
"""One segment's cached excluded population over ``[lo, covered)``.
|
|
62
|
+
|
|
63
|
+
``boundary`` is the `boundary_fingerprint` it was computed under and
|
|
64
|
+
``sha256`` the hex digest of exactly those bytes; ``excluded`` lists each
|
|
65
|
+
excluded record's line-start offset and admission family, in offset
|
|
66
|
+
order."""
|
|
67
|
+
|
|
68
|
+
boundary: str
|
|
69
|
+
lo: int
|
|
70
|
+
covered: int
|
|
71
|
+
sha256: str
|
|
72
|
+
excluded: tuple
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclasses.dataclass(frozen=True)
|
|
76
|
+
class Entry:
|
|
77
|
+
"""One segment's main-scan checkpoint over ``[0, covered)``.
|
|
78
|
+
|
|
79
|
+
``covered`` is a complete-line end or 0; ``sha256`` the hex digest of
|
|
80
|
+
those bytes; ``decoded`` and ``malformed`` the decoded records and the
|
|
81
|
+
malformed non-blank complete lines in that prefix; ``hits`` the
|
|
82
|
+
``(line-start offset, 0-based decoded ordinal)`` of every record that
|
|
83
|
+
matters to the scan, in offset order."""
|
|
84
|
+
|
|
85
|
+
name: str
|
|
86
|
+
covered: int
|
|
87
|
+
sha256: str
|
|
88
|
+
decoded: int
|
|
89
|
+
malformed: int
|
|
90
|
+
hits: tuple
|
|
91
|
+
exclusion: "Exclusion | None"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _canonical(obj) -> bytes:
|
|
95
|
+
return json.dumps(obj, sort_keys=True, separators=(",", ":"),
|
|
96
|
+
allow_nan=False).encode("utf-8")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _digest(obj) -> str:
|
|
100
|
+
return "sha256:" + hashlib.sha256(_canonical(obj)).hexdigest()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def index_version(descriptors: dict) -> str:
|
|
104
|
+
"""The checkpoint version: `SHAPE_VERSION` plus every derived input."""
|
|
105
|
+
return _digest({"shape": SHAPE_VERSION, "descriptors": descriptors})
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def boundary_fingerprint(fields: dict) -> str:
|
|
109
|
+
"""The digest of the admission-boundary fields the glue serializes."""
|
|
110
|
+
return _digest(fields)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _exclusion_document(exclusion: "Exclusion | None"):
|
|
114
|
+
if exclusion is None:
|
|
115
|
+
return None
|
|
116
|
+
return {"boundary": exclusion.boundary, "lo": exclusion.lo,
|
|
117
|
+
"covered": exclusion.covered, "sha256": exclusion.sha256,
|
|
118
|
+
"excluded": [[offset, family]
|
|
119
|
+
for offset, family in exclusion.excluded]}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _entry_document(entry: Entry) -> dict:
|
|
123
|
+
return {"name": entry.name, "covered": entry.covered,
|
|
124
|
+
"sha256": entry.sha256, "decoded": entry.decoded,
|
|
125
|
+
"malformed": entry.malformed,
|
|
126
|
+
"hits": [[offset, ordinal] for offset, ordinal in entry.hits],
|
|
127
|
+
"exclusion": _exclusion_document(entry.exclusion)}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def encode(version: str, entries: Iterable[Entry]) -> bytes:
|
|
131
|
+
"""The canonical checkpoint document: segments in the given order."""
|
|
132
|
+
segments = [_entry_document(entry) for entry in entries]
|
|
133
|
+
return _canonical({"version": version, "checksum": _digest(segments),
|
|
134
|
+
"segments": segments})
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _count(value) -> bool:
|
|
138
|
+
return type(value) is int and value >= 0
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _hex(value) -> bool:
|
|
142
|
+
return isinstance(value, str) and _HEX_DIGEST.match(value) is not None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _pairs(value, second_ok, lo: int, hi: int, second_hi=None):
|
|
146
|
+
"""``value`` as a tuple of ``(offset, second)`` pairs with offsets
|
|
147
|
+
strictly increasing in ``[lo, hi)`` — and, when ``second_hi`` is given,
|
|
148
|
+
integer seconds strictly increasing in ``[0, second_hi)`` — else None."""
|
|
149
|
+
if not isinstance(value, list):
|
|
150
|
+
return None
|
|
151
|
+
out = []
|
|
152
|
+
last_offset = lo - 1
|
|
153
|
+
last_second = -1
|
|
154
|
+
for item in value:
|
|
155
|
+
if not isinstance(item, list) or len(item) != 2:
|
|
156
|
+
return None
|
|
157
|
+
offset, second = item
|
|
158
|
+
if not _count(offset) or not (last_offset < offset < hi):
|
|
159
|
+
return None
|
|
160
|
+
if not second_ok(second):
|
|
161
|
+
return None
|
|
162
|
+
if second_hi is not None:
|
|
163
|
+
if not (last_second < second < second_hi):
|
|
164
|
+
return None
|
|
165
|
+
last_second = second
|
|
166
|
+
last_offset = offset
|
|
167
|
+
out.append((offset, second))
|
|
168
|
+
return tuple(out)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _exclusion_from_document(doc):
|
|
172
|
+
"""``(valid, Exclusion | None)``."""
|
|
173
|
+
if doc is None:
|
|
174
|
+
return True, None
|
|
175
|
+
if not isinstance(doc, dict) or set(doc) != _EXCLUSION_KEYS:
|
|
176
|
+
return False, None
|
|
177
|
+
boundary, lo, covered = doc["boundary"], doc["lo"], doc["covered"]
|
|
178
|
+
if not isinstance(boundary, str) or not boundary:
|
|
179
|
+
return False, None
|
|
180
|
+
if not (_count(lo) and _count(covered) and lo <= covered):
|
|
181
|
+
return False, None
|
|
182
|
+
if not _hex(doc["sha256"]):
|
|
183
|
+
return False, None
|
|
184
|
+
excluded = _pairs(doc["excluded"], lambda family: isinstance(family, str),
|
|
185
|
+
lo, covered)
|
|
186
|
+
if excluded is None:
|
|
187
|
+
return False, None
|
|
188
|
+
return True, Exclusion(boundary, lo, covered, doc["sha256"], excluded)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _entry_from_document(doc) -> "Entry | None":
|
|
192
|
+
if not isinstance(doc, dict) or set(doc) != _ENTRY_KEYS:
|
|
193
|
+
return None
|
|
194
|
+
name, covered = doc["name"], doc["covered"]
|
|
195
|
+
decoded, malformed = doc["decoded"], doc["malformed"]
|
|
196
|
+
if not isinstance(name, str) or not name:
|
|
197
|
+
return None
|
|
198
|
+
if not (_count(covered) and _count(decoded) and _count(malformed)):
|
|
199
|
+
return None
|
|
200
|
+
# Each counted line occupies at least one byte of `[0, covered)`: its
|
|
201
|
+
# newline.
|
|
202
|
+
if malformed + decoded > covered:
|
|
203
|
+
return None
|
|
204
|
+
if not _hex(doc["sha256"]):
|
|
205
|
+
return None
|
|
206
|
+
hits = _pairs(doc["hits"], _count, 0, covered, second_hi=decoded)
|
|
207
|
+
if hits is None:
|
|
208
|
+
return None
|
|
209
|
+
valid, exclusion = _exclusion_from_document(doc["exclusion"])
|
|
210
|
+
if not valid:
|
|
211
|
+
return None
|
|
212
|
+
return Entry(name, covered, doc["sha256"], decoded, malformed, hits,
|
|
213
|
+
exclusion)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def decode(raw: bytes, version: str) -> "dict[str, Entry] | None":
|
|
217
|
+
"""The valid entries of a checkpoint document, keyed by segment name.
|
|
218
|
+
|
|
219
|
+
None when the document does not parse, names another version or fails
|
|
220
|
+
its checksum. A structurally invalid entry is dropped and the others are
|
|
221
|
+
kept; a segment named twice keeps neither entry."""
|
|
222
|
+
return decode_document(raw, version)[0]
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def decode_document(raw: bytes, version: str):
|
|
226
|
+
"""``(decode(raw, version), clean)``: ``clean`` is True only when the
|
|
227
|
+
document is valid and every one of its entries was kept, so a writer
|
|
228
|
+
holding the same entries has nothing to rewrite."""
|
|
229
|
+
try:
|
|
230
|
+
doc = json.loads(raw)
|
|
231
|
+
except (ValueError, TypeError, RecursionError):
|
|
232
|
+
return None, False
|
|
233
|
+
if not isinstance(doc, dict) or doc.get("version") != version:
|
|
234
|
+
return None, False
|
|
235
|
+
segments = doc.get("segments")
|
|
236
|
+
if not isinstance(segments, list):
|
|
237
|
+
return None, False
|
|
238
|
+
try:
|
|
239
|
+
if doc.get("checksum") != _digest(segments):
|
|
240
|
+
return None, False
|
|
241
|
+
except (ValueError, TypeError, RecursionError):
|
|
242
|
+
return None, False
|
|
243
|
+
out: dict = {}
|
|
244
|
+
duplicated = set()
|
|
245
|
+
for item in segments:
|
|
246
|
+
entry = _entry_from_document(item)
|
|
247
|
+
if entry is None:
|
|
248
|
+
continue
|
|
249
|
+
if entry.name in out:
|
|
250
|
+
duplicated.add(entry.name)
|
|
251
|
+
out[entry.name] = entry
|
|
252
|
+
for name in duplicated:
|
|
253
|
+
del out[name]
|
|
254
|
+
return out, len(out) == len(segments)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def verify_prefix(entry_sha: str, covered: int, data: bytes):
|
|
258
|
+
"""The running SHA-256 after ``data[:covered]`` when that prefix is a
|
|
259
|
+
complete-line prefix of ``data`` whose digest is ``entry_sha``, else None.
|
|
260
|
+
|
|
261
|
+
The caller continues the returned object over the fresh suffix, so every
|
|
262
|
+
byte is hashed once."""
|
|
263
|
+
if len(data) < covered:
|
|
264
|
+
return None
|
|
265
|
+
if covered and data[covered - 1] != 0x0A:
|
|
266
|
+
return None
|
|
267
|
+
hasher = hashlib.sha256()
|
|
268
|
+
hasher.update(memoryview(data)[:covered])
|
|
269
|
+
if hasher.hexdigest() != entry_sha:
|
|
270
|
+
return None
|
|
271
|
+
return hasher
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class RangeHasher:
|
|
275
|
+
"""An ``on_bytes`` callback hashing every chunk of one ``[lo, hi)`` read.
|
|
276
|
+
|
|
277
|
+
With ``covered`` set it also proves the cached prefix ``[lo, covered)``:
|
|
278
|
+
the chunk that crosses ``covered`` is split, the prefix digest compared
|
|
279
|
+
with ``expected_prefix``, and ``prefix_state`` settled before the call
|
|
280
|
+
returns — so before the reader yields any line starting at or after
|
|
281
|
+
``covered``. With ``covered == lo`` the empty prefix is compared at
|
|
282
|
+
construction. ``prefix_state`` is ``"none"`` without ``covered``,
|
|
283
|
+
otherwise ``"pending"`` until settled as ``"match"`` or ``"mismatch"``;
|
|
284
|
+
a read that ends while it is still pending never reached ``covered``."""
|
|
285
|
+
|
|
286
|
+
def __init__(self, lo: int, covered: "int | None",
|
|
287
|
+
expected_prefix: "str | None"):
|
|
288
|
+
if covered is not None and covered < lo:
|
|
289
|
+
raise ValueError("covered precedes lo")
|
|
290
|
+
self._lo = lo
|
|
291
|
+
self._covered = covered
|
|
292
|
+
self._expected = expected_prefix
|
|
293
|
+
self._hash = hashlib.sha256()
|
|
294
|
+
self.consumed = 0
|
|
295
|
+
if covered is None:
|
|
296
|
+
self.prefix_state = "none"
|
|
297
|
+
elif covered == lo:
|
|
298
|
+
self.prefix_state = self._settle()
|
|
299
|
+
else:
|
|
300
|
+
self.prefix_state = "pending"
|
|
301
|
+
|
|
302
|
+
def _settle(self) -> str:
|
|
303
|
+
return ("match" if self._hash.hexdigest() == self._expected
|
|
304
|
+
else "mismatch")
|
|
305
|
+
|
|
306
|
+
def __call__(self, chunk: bytes) -> None:
|
|
307
|
+
if self.prefix_state == "pending":
|
|
308
|
+
need = self._covered - self._lo - self.consumed
|
|
309
|
+
if len(chunk) >= need:
|
|
310
|
+
view = memoryview(chunk)
|
|
311
|
+
self._hash.update(view[:need])
|
|
312
|
+
self.prefix_state = self._settle()
|
|
313
|
+
self._hash.update(view[need:])
|
|
314
|
+
self.consumed += len(chunk)
|
|
315
|
+
return
|
|
316
|
+
self._hash.update(chunk)
|
|
317
|
+
self.consumed += len(chunk)
|
|
318
|
+
|
|
319
|
+
def digest(self) -> str:
|
|
320
|
+
"""The hex digest of every byte consumed so far."""
|
|
321
|
+
return self._hash.hexdigest()
|
|
@@ -79,6 +79,13 @@ _COMPLETE_KEYS = {
|
|
|
79
79
|
#: clean except for pending prune debt writes a fresh one; any other
|
|
80
80
|
#: unbudgeted or certifying full walk deletes it.
|
|
81
81
|
CODEX_FULL_WALK_DEBT_ONLY_KEY = "dashboard_codex_full_walk_debt_only"
|
|
82
|
+
#: #916. cache.db's residual-only token: the most recent exhaustive Claude
|
|
83
|
+
#: walk visited every on-disk source cleanly and its only defect was tracked
|
|
84
|
+
#: rows whose transcripts no longer exist (orphans the prune gates refused).
|
|
85
|
+
#: Mutually exclusive with ``claude_ingest_walk_complete``, which keeps its
|
|
86
|
+
#: D5a meaning for the upgrade gate; only this frontier reads the token, as a
|
|
87
|
+
#: second way for the Claude store to be complete. Its JSON value is debug-only.
|
|
88
|
+
CLAUDE_WALK_ORPHAN_RESIDUAL_KEY = "claude_ingest_walk_orphan_residual"
|
|
82
89
|
CODEX_CONVERSATION_WALK_DEBT_ONLY_KEY = "dashboard_codex_conversation_walk_debt_only"
|
|
83
90
|
#: How long a frontier holds a debt-only store before walking it again: the
|
|
84
91
|
#: certificate's own staleness bound, so a store whose certificate is
|
|
@@ -1872,6 +1879,15 @@ def _pending_identity(conn) -> tuple[str, ...]:
|
|
|
1872
1879
|
|
|
1873
1880
|
|
|
1874
1881
|
def _provider_complete(conn, provider: str) -> bool:
|
|
1882
|
+
if provider == "claude":
|
|
1883
|
+
# #916: a residual-only walk certifies every on-disk source as fully
|
|
1884
|
+
# as a clean one; the orphan rows it leaves are never read or written
|
|
1885
|
+
# by a walk, so they cannot hide pending work.
|
|
1886
|
+
row = conn.execute(
|
|
1887
|
+
"SELECT 1 FROM cache_meta WHERE key IN (?, ?) LIMIT 1",
|
|
1888
|
+
(_COMPLETE_KEYS[provider], CLAUDE_WALK_ORPHAN_RESIDUAL_KEY),
|
|
1889
|
+
).fetchone()
|
|
1890
|
+
return row is not None
|
|
1875
1891
|
row = conn.execute(
|
|
1876
1892
|
"SELECT 1 FROM cache_meta WHERE key=? LIMIT 1",
|
|
1877
1893
|
(_COMPLETE_KEYS[provider],),
|
|
@@ -2228,7 +2244,10 @@ def provider_sync_certifiable(mode: str, stats: object) -> bool:
|
|
|
2228
2244
|
if mode == "targeted":
|
|
2229
2245
|
return bool(getattr(stats, "targeted_clean", True))
|
|
2230
2246
|
if mode == "full":
|
|
2231
|
-
|
|
2247
|
+
# #916: ``full_walk_residual_only`` exists only on the Claude
|
|
2248
|
+
# ``IngestStats``; Codex stats and ``_DebtCleared`` read False.
|
|
2249
|
+
return bool(getattr(stats, "full_walk_complete", False)
|
|
2250
|
+
or getattr(stats, "full_walk_residual_only", False))
|
|
2232
2251
|
return False
|
|
2233
2252
|
|
|
2234
2253
|
|
|
@@ -164,6 +164,28 @@ def _percent(value) -> "float | None":
|
|
|
164
164
|
return value if math.isfinite(value) else None
|
|
165
165
|
|
|
166
166
|
|
|
167
|
+
def finite_percent(value) -> "float | None":
|
|
168
|
+
"""A raw payload percent as a finite float, or None (a bool, a string, a
|
|
169
|
+
missing or non-finite value). The one parse both the samples projection
|
|
170
|
+
and the weekly observation index (#1018 twin spec 5.1) apply."""
|
|
171
|
+
return _percent(value)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def weekly_reset_identity(resets_at) -> "str | None":
|
|
175
|
+
"""The normalized weekly window identity of a raw payload ``resets_at``
|
|
176
|
+
(:func:`normalize_weekly_reset`), or None when it is missing, a bool or
|
|
177
|
+
unparsable. `claude_oauth_samples.weekly_resets_at_utc` and
|
|
178
|
+
`claude_weekly_observation_index.weekly_resets_at_utc` (#1018 twin spec
|
|
179
|
+
5.1) are both spelled by this one helper, so window equality holds
|
|
180
|
+
between them."""
|
|
181
|
+
if resets_at is None or isinstance(resets_at, bool):
|
|
182
|
+
return None
|
|
183
|
+
try:
|
|
184
|
+
return normalize_weekly_reset(int(resets_at))
|
|
185
|
+
except (TypeError, ValueError, OverflowError, OSError):
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
|
|
167
189
|
def _bound_account(payload: dict) -> "str | None":
|
|
168
190
|
binding = payload.get("account_binding")
|
|
169
191
|
if not isinstance(binding, dict):
|
|
@@ -200,13 +222,7 @@ def sample_from_observation(payload: dict, *, account_key: str,
|
|
|
200
222
|
return None
|
|
201
223
|
|
|
202
224
|
weekly_percent = _percent(payload.get("weekly_percent"))
|
|
203
|
-
weekly_reset =
|
|
204
|
-
resets_at = payload.get("resets_at")
|
|
205
|
-
if not isinstance(resets_at, bool):
|
|
206
|
-
try:
|
|
207
|
-
weekly_reset = normalize_weekly_reset(int(resets_at))
|
|
208
|
-
except (TypeError, ValueError, OverflowError, OSError):
|
|
209
|
-
weekly_reset = None
|
|
225
|
+
weekly_reset = weekly_reset_identity(payload.get("resets_at"))
|
|
210
226
|
if weekly_reset is None:
|
|
211
227
|
weekly_percent = None
|
|
212
228
|
|