cctally 1.115.0 → 1.116.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 +16 -0
- package/bin/_cctally_cache.py +85 -4
- package/bin/_cctally_completion.py +80 -12
- package/bin/_cctally_core.py +106 -9
- package/bin/_cctally_dashboard.py +6 -9
- package/bin/_cctally_dashboard_envelope.py +26 -32
- package/bin/_cctally_db.py +85 -4
- package/bin/_cctally_doctor.py +38 -5
- package/bin/_cctally_five_hour.py +83 -1017
- package/bin/_cctally_journal.py +1307 -180
- package/bin/_cctally_journal_repair.py +9 -7
- package/bin/_cctally_oauth_samples.py +280 -0
- package/bin/_cctally_parser.py +5 -0
- package/bin/_cctally_quarantine.py +14 -4
- package/bin/_cctally_quota.py +70 -8
- package/bin/_cctally_quota_model.py +35 -4
- package/bin/_cctally_record.py +414 -123
- package/bin/_cctally_rederive.py +632 -235
- package/bin/_cctally_refresh.py +29 -0
- package/bin/_cctally_statusline.py +984 -47
- package/bin/_cctally_store.py +972 -58
- package/bin/_cctally_tui.py +62 -7
- package/bin/_cctally_update.py +25 -0
- package/bin/_lib_account_protocol.py +201 -18
- package/bin/_lib_clone_copy.py +46 -11
- package/bin/_lib_dashboard_sources.py +57 -15
- package/bin/_lib_detector_access_registry.py +273 -52
- package/bin/_lib_doctor.py +302 -1
- package/bin/_lib_journal.py +145 -25
- package/bin/_lib_journal_router.py +166 -48
- package/bin/_lib_oauth_samples.py +238 -0
- package/bin/_lib_pricing_check.py +0 -12
- package/bin/_lib_provider_activity.py +770 -0
- package/bin/_lib_quarantine.py +3 -2
- package/bin/_lib_quota_model.py +96 -6
- package/bin/_lib_rebuild_progress.py +146 -0
- package/bin/_lib_record.py +22 -0
- package/bin/_lib_rederive.py +299 -3
- package/bin/_lib_snapshot_cache.py +43 -1
- package/bin/_lib_stale_proof.py +708 -0
- package/bin/_lib_statusline_candidates.py +65 -5
- package/bin/_lib_worker_launches.py +315 -0
- package/bin/cctally +6 -11
- package/dashboard/static/assets/{ConversationsView-Bc8LCOAZ.js → ConversationsView-Dflb_myA.js} +1 -1
- package/dashboard/static/assets/DoctorModal-DFX5CfIs.js +1 -0
- package/dashboard/static/assets/{ModalRoot-DLPgyAM_.js → ModalRoot-HlxoagIa.js} +1 -1
- package/dashboard/static/assets/{ProjectsDrillPanel-z53a4v4M.js → ProjectsDrillPanel-C3PJy1xx.js} +1 -1
- package/dashboard/static/assets/{SourceDetailModal-Bw0T8W-W.js → SourceDetailModal-BczsWKhD.js} +1 -1
- package/dashboard/static/assets/{UpdateModal-D5AKx4Yw.js → UpdateModal-Os-Lo5cs.js} +1 -1
- package/dashboard/static/assets/{index-BXDOzSng.js → index-CYZ3NLqF.js} +3 -3
- package/dashboard/static/assets/{outlineNavigation-JObY7cUY.js → outlineNavigation-k-zIif6W.js} +1 -1
- package/dashboard/static/dashboard.html +1 -1
- package/package.json +7 -2
- package/bin/_lib_merge_gate.py +0 -452
- package/dashboard/static/assets/DoctorModal-D--qVBwe.js +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,22 @@ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [1.116.0] - 2026-10-28
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `cctally doctor`, `cctally db status` and the dashboard's Doctor chip now say when a stats.db upgrade rebuild or auto-heal is running, has stalled, failed or died, or used more memory than its bound.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- The stats.db upgrade rebuild that runs after an update, `cctally db rebuild` and `cctally db rederive` use far less memory and disk, and the upgrade rebuild finishes faster.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- After a usage credit, `cctally statusline` refuses an idle session's cached usage once it is proven stale, and five-hour views stop hiding genuine weekly values. `cctally db rederive` adds `retract`; stats.db rebuilds once after upgrading.
|
|
18
|
+
- Under Python 3.11, `cctally quota --json` no longer reports a mix-support radius that differs in its last digits from one run to the next.
|
|
19
|
+
- `cctally quota --json` reports the same `eligibilityFence` on Linux as on macOS; on Linux it could differ in its last digits.
|
|
20
|
+
- A sync with nothing new to read no longer rewrites the compressed Codex rollout status in `cache.db` and `conversations.db`, so an idle `cctally dashboard` writes less to disk.
|
|
21
|
+
- `cctally cache-sync --rebuild` rebuilds transcripts again on a filesystem without copy-on-write clones, such as ext4 on Linux, instead of failing every time.
|
|
22
|
+
- On Python 3.11, the upgrade that moves transcripts out of `cache.db` now shrinks the file as intended. If you already upgraded on Python 3.11, run `cctally db vacuum --db cache` to reclaim the space.
|
|
23
|
+
|
|
8
24
|
## [1.115.0] - 2026-10-21
|
|
9
25
|
|
|
10
26
|
### Added
|
package/bin/_cctally_cache.py
CHANGED
|
@@ -102,6 +102,7 @@ import bisect
|
|
|
102
102
|
import collections
|
|
103
103
|
import contextlib
|
|
104
104
|
import datetime as dt
|
|
105
|
+
import errno
|
|
105
106
|
import fcntl
|
|
106
107
|
import json
|
|
107
108
|
import os
|
|
@@ -9268,6 +9269,18 @@ def _record_codex_container_status(conn, stats, *, census_complete: bool,
|
|
|
9268
9269
|
if cursor_table == "codex_session_files"
|
|
9269
9270
|
else prior.get("barrier", {})),
|
|
9270
9271
|
}
|
|
9272
|
+
# #901 W8: a pass that observes the status already recorded writes
|
|
9273
|
+
# nothing. Every caller runs this at finalization, the idle tick and the
|
|
9274
|
+
# idle conversation pass included, and stamping `observedAt` alone wrote a
|
|
9275
|
+
# WAL frame in each store on every pass more than a second after the last.
|
|
9276
|
+
# `observedAt` therefore keeps the time this status was first recorded;
|
|
9277
|
+
# the other timestamps already change only with the state they date.
|
|
9278
|
+
def _content(status):
|
|
9279
|
+
return json.dumps({k: v for k, v in status.items() if k != "observedAt"},
|
|
9280
|
+
sort_keys=True)
|
|
9281
|
+
|
|
9282
|
+
if prior and "observedAt" in prior and _content(prior) == _content(record):
|
|
9283
|
+
return
|
|
9271
9284
|
_set_cache_meta(conn, CODEX_CONTAINER_STATUS_KEY,
|
|
9272
9285
|
json.dumps(record, sort_keys=True))
|
|
9273
9286
|
|
|
@@ -15496,6 +15509,54 @@ def _conversation_probe_snapshot(path: pathlib.Path):
|
|
|
15496
15509
|
yield snapshot
|
|
15497
15510
|
|
|
15498
15511
|
|
|
15512
|
+
class ConversationProbeCloneUnavailable(OSError):
|
|
15513
|
+
"""The filesystem or platform cannot clone the transcript family.
|
|
15514
|
+
|
|
15515
|
+
The clone helper raises it when `cp` is missing, the platform has no
|
|
15516
|
+
clone command, or `cp` reports that the filesystem refused the clone (see
|
|
15517
|
+
:func:`_cp_refused_clone`). The pre-rebuild quick-check catches it, skips
|
|
15518
|
+
and lets the rebuild continue; recovery still declines without a snapshot.
|
|
15519
|
+
Every other clone failure stays a plain OSError and fails the rebuild.
|
|
15520
|
+
"""
|
|
15521
|
+
|
|
15522
|
+
|
|
15523
|
+
# errno values that mean "this filesystem cannot clone these files", as
|
|
15524
|
+
# opposed to a failure while cloning them. Linux ioctl_ficlone(2) documents
|
|
15525
|
+
# EOPNOTSUPP (the filesystem cannot reflink, e.g. ext4), EXDEV (the paths are
|
|
15526
|
+
# on different mounts, e.g. a bind mount) and EINVAL (the filesystem cannot
|
|
15527
|
+
# reflink these files); ioctl(2) returns ENOTTY where the kernel or filesystem
|
|
15528
|
+
# has no FICLONE at all. macOS clonefile(2) documents ENOTSUP and EXDEV. Each
|
|
15529
|
+
# is a permanent property of where the store lives. Anything else (ENOSPC,
|
|
15530
|
+
# EDQUOT, EACCES, EPERM, ENOENT, EIO, or a failure to open, stat or create
|
|
15531
|
+
# before the clone) is a real failure and must fail the rebuild: an I/O error
|
|
15532
|
+
# reading a damaged store is the very signal the probe exists to catch.
|
|
15533
|
+
_CLONE_REFUSAL_ERRNOS = frozenset(
|
|
15534
|
+
getattr(errno, name)
|
|
15535
|
+
for name in ("EOPNOTSUPP", "ENOTSUP", "EXDEV", "EINVAL", "ENOTTY")
|
|
15536
|
+
if hasattr(errno, name)
|
|
15537
|
+
)
|
|
15538
|
+
|
|
15539
|
+
|
|
15540
|
+
def _cp_refused_clone(stderr: str) -> bool:
|
|
15541
|
+
"""Whether `cp` reported a clone refused for one of those errno values.
|
|
15542
|
+
|
|
15543
|
+
`cp` runs under ``LC_ALL=C``, so its strerror text is the C locale's, the
|
|
15544
|
+
same text :func:`os.strerror` returns. GNU `cp --reflink=always` reports a
|
|
15545
|
+
failed FICLONE as ``failed to clone DST from SRC: <strerror>``. macOS
|
|
15546
|
+
`cp -c` falls back to copyfile(2), a full copy, when the target cannot
|
|
15547
|
+
clone, so it rarely reports a refusal at all; when clonefile(2) does fail
|
|
15548
|
+
it warns ``DST: clonefile failed: <strerror>``.
|
|
15549
|
+
"""
|
|
15550
|
+
reasons = tuple(f": {os.strerror(code)}" for code in _CLONE_REFUSAL_ERRNOS)
|
|
15551
|
+
for line in stderr.splitlines():
|
|
15552
|
+
line = line.strip()
|
|
15553
|
+
if "failed to clone " not in line and ": clonefile failed: " not in line:
|
|
15554
|
+
continue
|
|
15555
|
+
if line.endswith(reasons):
|
|
15556
|
+
return True
|
|
15557
|
+
return False
|
|
15558
|
+
|
|
15559
|
+
|
|
15499
15560
|
def _clone_conversation_probe_member(
|
|
15500
15561
|
source: pathlib.Path,
|
|
15501
15562
|
destination: pathlib.Path,
|
|
@@ -15513,7 +15574,7 @@ def _clone_conversation_probe_member(
|
|
|
15513
15574
|
return
|
|
15514
15575
|
cp = shutil.which("cp")
|
|
15515
15576
|
if cp is None:
|
|
15516
|
-
raise
|
|
15577
|
+
raise ConversationProbeCloneUnavailable(
|
|
15517
15578
|
"copy-on-write transcript probe unavailable: `cp` was not found"
|
|
15518
15579
|
)
|
|
15519
15580
|
if sys.platform == "darwin":
|
|
@@ -15523,7 +15584,7 @@ def _clone_conversation_probe_member(
|
|
|
15523
15584
|
cp, "--reflink=always", "--", str(source), str(destination),
|
|
15524
15585
|
]
|
|
15525
15586
|
else:
|
|
15526
|
-
raise
|
|
15587
|
+
raise ConversationProbeCloneUnavailable(
|
|
15527
15588
|
"copy-on-write transcript probe is unsupported on "
|
|
15528
15589
|
f"{sys.platform}"
|
|
15529
15590
|
)
|
|
@@ -15535,6 +15596,7 @@ def _clone_conversation_probe_member(
|
|
|
15535
15596
|
text=True,
|
|
15536
15597
|
timeout=_CONVERSATION_PROBE_CLONE_TIMEOUT_SECONDS,
|
|
15537
15598
|
check=False,
|
|
15599
|
+
env={**os.environ, "LC_ALL": "C"},
|
|
15538
15600
|
)
|
|
15539
15601
|
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
15540
15602
|
raise OSError(
|
|
@@ -15544,9 +15606,14 @@ def _clone_conversation_probe_member(
|
|
|
15544
15606
|
reason = (result.stderr or "").strip() or (
|
|
15545
15607
|
f"cp exited {result.returncode}"
|
|
15546
15608
|
)
|
|
15609
|
+
if _cp_refused_clone(result.stderr or ""):
|
|
15610
|
+
raise ConversationProbeCloneUnavailable(
|
|
15611
|
+
"copy-on-write transcript probe unavailable for "
|
|
15612
|
+
f"{source.name}: {reason}"
|
|
15613
|
+
)
|
|
15547
15614
|
raise OSError(
|
|
15548
|
-
"copy-on-write transcript probe
|
|
15549
|
-
f"{
|
|
15615
|
+
f"copy-on-write transcript probe failed for {source.name}: "
|
|
15616
|
+
f"{reason}"
|
|
15550
15617
|
)
|
|
15551
15618
|
|
|
15552
15619
|
|
|
@@ -15622,6 +15689,20 @@ def _probe_conversation_rebuild(
|
|
|
15622
15689
|
probe.close()
|
|
15623
15690
|
except sqlite3.DatabaseError as exc:
|
|
15624
15691
|
return exc
|
|
15692
|
+
except ConversationProbeCloneUnavailable as exc:
|
|
15693
|
+
# The clone helper raised this because the filesystem cannot
|
|
15694
|
+
# clone, which never changes: failing here failed every explicit
|
|
15695
|
+
# rebuild over an existing store on such a filesystem. Skip the
|
|
15696
|
+
# check (never a byte copy) and rebuild. Corruption quick_check
|
|
15697
|
+
# would have found may then go unnoticed, or surface later in a
|
|
15698
|
+
# rebuild worker; recovery declines without a snapshot either
|
|
15699
|
+
# way, so nothing is quarantined.
|
|
15700
|
+
eprint(
|
|
15701
|
+
"[cache-sync] conversations.db integrity check skipped "
|
|
15702
|
+
f"before the transcript rebuild ({exc}); continuing without "
|
|
15703
|
+
"automatic corruption recovery"
|
|
15704
|
+
)
|
|
15705
|
+
return None
|
|
15625
15706
|
finally:
|
|
15626
15707
|
keeper_yield.close()
|
|
15627
15708
|
if provider_locks is not None:
|
|
@@ -114,6 +114,7 @@ import _lib_account_protocol
|
|
|
114
114
|
import _lib_accounts
|
|
115
115
|
import _lib_journal
|
|
116
116
|
import _lib_journal_router
|
|
117
|
+
import _lib_rebuild_progress
|
|
117
118
|
import _lib_rederive
|
|
118
119
|
|
|
119
120
|
#: The correction-batch family this step (and the live collision check)
|
|
@@ -152,7 +153,7 @@ def is_deriving_input(record) -> bool:
|
|
|
152
153
|
return False
|
|
153
154
|
if record.get("t") == "obs":
|
|
154
155
|
import _cctally_rederive
|
|
155
|
-
return
|
|
156
|
+
return _cctally_rederive._is_rederivable_raw(record)
|
|
156
157
|
if record.get("t") == "op":
|
|
157
158
|
return (record.get("payload") or {}).get("kind") in _DERIVING_OP_KINDS
|
|
158
159
|
return False
|
|
@@ -800,6 +801,9 @@ def _complete(context, trusted, target, boundary):
|
|
|
800
801
|
with tempfile.TemporaryDirectory(
|
|
801
802
|
prefix="cctally-completion-", dir=str(_cctally_core.APP_DIR)) as tmp:
|
|
802
803
|
base = pathlib.Path(tmp) / "predecessor.db"
|
|
804
|
+
# #997 D9: progress for a detached worker's state file (a no-op
|
|
805
|
+
# unless one installed a sink).
|
|
806
|
+
_lib_rebuild_progress.phase("completion:predecessor")
|
|
803
807
|
_journal.rebuild_stats_index(
|
|
804
808
|
context=_journal.RebuildContext(trigger=context.trigger),
|
|
805
809
|
target_path=str(base), high_water=start,
|
|
@@ -847,6 +851,7 @@ def _apply_owed_outputs(base, start, output_end, boundary) -> frozenset:
|
|
|
847
851
|
revision through that output end, in the rebuild's fold order (structural
|
|
848
852
|
folds, the open 5h block projection, then milestones). Returns the ids
|
|
849
853
|
applied, which the predecessor then holds."""
|
|
854
|
+
_lib_rebuild_progress.phase("completion:owed-outputs")
|
|
850
855
|
prefix = _read_prefix(start, output_end, boundary)
|
|
851
856
|
if prefix.cutover_claude is _journal._CUTOVER_UNSEEN:
|
|
852
857
|
prefix.cutover_claude = _journal._resolve_cutover_for_rebuild(
|
|
@@ -873,6 +878,9 @@ def _apply_owed_outputs(base, start, output_end, boundary) -> frozenset:
|
|
|
873
878
|
_journal._fold_order(item[1]), item[0]))
|
|
874
879
|
conn = _cctally_core.open_db(_target_path=str(base))
|
|
875
880
|
try:
|
|
881
|
+
# #997 (spec D1): the predecessor is a disposable scratch too.
|
|
882
|
+
import _cctally_store
|
|
883
|
+
_cctally_store.apply_disposable_scratch_policy(conn)
|
|
876
884
|
for phase in ("structural", "tail"):
|
|
877
885
|
if phase == "tail":
|
|
878
886
|
import sys
|
|
@@ -917,6 +925,10 @@ class _Prefix:
|
|
|
917
925
|
self.excluded_revisions: dict = {}
|
|
918
926
|
self.inert_revisions: dict = {}
|
|
919
927
|
self.begin_at: dict = {}
|
|
928
|
+
#: #797/#823/#840 spec 4.6 B1: the begin markers per batch id, so a
|
|
929
|
+
#: retry of a batch an earlier writer began without the revision-2
|
|
930
|
+
#: stamp is authored under a derived id instead of restamped.
|
|
931
|
+
self.begins: dict = {}
|
|
920
932
|
self.batch_family: dict = {}
|
|
921
933
|
self.cutover_claude = _journal._CUTOVER_UNSEEN
|
|
922
934
|
# Admitted decision ids before the window (the predecessor holds
|
|
@@ -933,25 +945,57 @@ def _bump(table: dict, record) -> None:
|
|
|
933
945
|
table[event_id] = max(rev, table.get(event_id, rev))
|
|
934
946
|
|
|
935
947
|
|
|
948
|
+
def _window_omits(record) -> bool:
|
|
949
|
+
"""#997 spec D3: whether an ADMITTED record is left out of the window —
|
|
950
|
+
an observation the replay provably ignores: not a rederivable raw input
|
|
951
|
+
and not a Claude reading, so it yields no candidate and the quarantine
|
|
952
|
+
fold returns before touching state (`reading_from_record` requires
|
|
953
|
+
`is_claude_reading`). Codex quota observations, 85 % of a production
|
|
954
|
+
journal, are the population. Its line still feeds the prefix hasher and
|
|
955
|
+
its `None` slot, and `window_has_derivable_inputs` still scans it; an
|
|
956
|
+
EXCLUDED observation keeps its placeholder, which the fold's exclusion
|
|
957
|
+
trigger needs. Every window consumer reads the same compacted window
|
|
958
|
+
within one `_complete_window` call, so indexing stays consistent."""
|
|
959
|
+
if record.get("t") != "obs":
|
|
960
|
+
return False
|
|
961
|
+
import _cctally_quarantine
|
|
962
|
+
import _cctally_rederive
|
|
963
|
+
return (not _cctally_rederive._is_rederivable_raw(record)
|
|
964
|
+
and not _cctally_quarantine.is_claude_reading(record))
|
|
965
|
+
|
|
966
|
+
|
|
936
967
|
def _read_prefix(start, target, boundary) -> _Prefix:
|
|
937
968
|
out = _Prefix()
|
|
938
969
|
segments = _journal.list_segments()
|
|
939
|
-
hasher =
|
|
970
|
+
hasher = _journal._new_prefix_hasher()
|
|
940
971
|
prior = {"hw": None}
|
|
941
972
|
placeholder = _lib_account_protocol.excluded_placeholder()
|
|
942
973
|
start_key = _order(start)
|
|
974
|
+
lines = 0
|
|
943
975
|
for segment, offset, raw in _journal._iter_range_with_segments(
|
|
944
976
|
None, target, segments,
|
|
945
|
-
on_segment=lambda name: hasher.begin_segment(
|
|
977
|
+
on_segment=lambda name, size: hasher.begin_segment(
|
|
978
|
+
name, prior["hw"], size=size),
|
|
946
979
|
on_bytes=hasher.extend,
|
|
947
980
|
):
|
|
981
|
+
lines += 1
|
|
982
|
+
if not lines & 4095:
|
|
983
|
+
_lib_rebuild_progress.tick(lines=lines)
|
|
948
984
|
end = (segment, offset + len(raw) + 1)
|
|
949
985
|
record = _lib_journal.decode_line(raw)
|
|
950
986
|
if record is None:
|
|
951
987
|
prior["hw"] = end
|
|
952
988
|
continue
|
|
953
989
|
in_window = _order((segment, offset)) >= start_key
|
|
954
|
-
|
|
990
|
+
admitted = _journal._is_admitted(record, segment, offset, boundary)
|
|
991
|
+
t = record.get("t")
|
|
992
|
+
omitted = admitted and in_window and _window_omits(record)
|
|
993
|
+
if (in_window and not omitted) or (
|
|
994
|
+
admitted and t in _lib_journal_router.RETAINED_RECORD_TYPES):
|
|
995
|
+
# #997 D15: a record the window or the selector slots keep is
|
|
996
|
+
# compacted before anything holds a reference to it.
|
|
997
|
+
record = _lib_journal.compact_retained(record)
|
|
998
|
+
if not admitted:
|
|
955
999
|
out.slots.append(None)
|
|
956
1000
|
if record.get("t") in ("evt", "correction"):
|
|
957
1001
|
_bump(out.excluded_revisions, record)
|
|
@@ -961,7 +1005,6 @@ def _read_prefix(start, target, boundary) -> _Prefix:
|
|
|
961
1005
|
out.coordinates.append((segment, offset))
|
|
962
1006
|
prior["hw"] = end
|
|
963
1007
|
continue
|
|
964
|
-
t = record.get("t")
|
|
965
1008
|
if t == "op":
|
|
966
1009
|
_journal._capture_protocol_prefix_evidence(
|
|
967
1010
|
record, prior["hw"], out.evidence, hasher=hasher)
|
|
@@ -976,10 +1019,11 @@ def _read_prefix(start, target, boundary) -> _Prefix:
|
|
|
976
1019
|
out.legacy_origin_ids.add(record["id"])
|
|
977
1020
|
if t == "correction_batch" and record.get("phase") == "begin":
|
|
978
1021
|
out.begin_at.setdefault(record.get("id"), record.get("at"))
|
|
1022
|
+
out.begins.setdefault(record.get("id"), []).append(record)
|
|
979
1023
|
out.batch_family.setdefault(record.get("id"), record.get("family"))
|
|
980
1024
|
out.slots.append(
|
|
981
1025
|
record if t in _lib_journal_router.RETAINED_RECORD_TYPES else None)
|
|
982
|
-
if in_window:
|
|
1026
|
+
if in_window and not omitted:
|
|
983
1027
|
out.window.append(record)
|
|
984
1028
|
out.coordinates.append((segment, offset))
|
|
985
1029
|
prior["hw"] = end
|
|
@@ -1055,6 +1099,17 @@ def _no_cache_read_adapters():
|
|
|
1055
1099
|
setattr(cctally, name, value)
|
|
1056
1100
|
|
|
1057
1101
|
|
|
1102
|
+
def _normalized_copy(record, cutover_claude):
|
|
1103
|
+
"""One window record as a replay sees it: a private deep copy, its legacy
|
|
1104
|
+
account stamp normalized under ``cutover_claude`` (an excluded placeholder
|
|
1105
|
+
is only copied). Reads only this record and the journal-wide cutover
|
|
1106
|
+
value (#997 spec D5)."""
|
|
1107
|
+
out = copy.deepcopy(record)
|
|
1108
|
+
if out.get("t") != _lib_account_protocol.EXCLUDED_PLACEHOLDER_TYPE:
|
|
1109
|
+
_journal._normalize_legacy_account_stamp(out, cutover_claude)
|
|
1110
|
+
return out
|
|
1111
|
+
|
|
1112
|
+
|
|
1058
1113
|
def _replay(base, tmp, name, prefix, held_ids, cache_conn, *, retention,
|
|
1059
1114
|
skip=frozenset()):
|
|
1060
1115
|
"""One scratch replay of the window from the predecessor state, the
|
|
@@ -1065,17 +1120,19 @@ def _replay(base, tmp, name, prefix, held_ids, cache_conn, *, retention,
|
|
|
1065
1120
|
import _cctally_rederive
|
|
1066
1121
|
copy_path = tmp / name
|
|
1067
1122
|
shutil.copyfile(base, copy_path)
|
|
1068
|
-
|
|
1069
|
-
for record in records:
|
|
1070
|
-
if record.get("t") != _lib_account_protocol.EXCLUDED_PLACEHOLDER_TYPE:
|
|
1071
|
-
_journal._normalize_legacy_account_stamp(
|
|
1072
|
-
record, prefix.cutover_claude)
|
|
1123
|
+
cutover_claude = prefix.cutover_claude
|
|
1073
1124
|
by_input: dict = {}
|
|
1074
1125
|
conn = _cctally_core.open_db(_target_path=str(copy_path))
|
|
1075
1126
|
try:
|
|
1127
|
+
# #997 (spec D1): a disposable replay copy, never published.
|
|
1128
|
+
import _cctally_store
|
|
1129
|
+
_cctally_store.apply_disposable_scratch_policy(conn)
|
|
1076
1130
|
fold = _cctally_quarantine.QuarantineFold.load(conn, held_ids=held_ids)
|
|
1131
|
+
# #997 spec D5: each record is copied and normalized as it is
|
|
1132
|
+
# offered, never the whole window up front.
|
|
1077
1133
|
events = _cctally_rederive.replay_scratch_records(
|
|
1078
|
-
conn,
|
|
1134
|
+
conn, prefix.window, cache_conn, fold=fold, held_ids=held_ids,
|
|
1135
|
+
prepare=lambda record: _normalized_copy(record, cutover_claude),
|
|
1079
1136
|
coordinates=prefix.coordinates, skip_indexes=skip,
|
|
1080
1137
|
on_input=lambda index, derived: by_input.__setitem__(
|
|
1081
1138
|
index, list(derived)),
|
|
@@ -1177,6 +1234,7 @@ def _complete_window(base, tmp, start, target, boundary, round_index, *,
|
|
|
1177
1234
|
holds the fresh candidate only where it differs from the reference, so a
|
|
1178
1235
|
gap's effect reaches its descendants while every unaffected decision
|
|
1179
1236
|
stays the retained one. Their difference is the causal suffix."""
|
|
1237
|
+
_lib_rebuild_progress.phase("completion:read", round=round_index)
|
|
1180
1238
|
prefix = _read_prefix(start, target, boundary)
|
|
1181
1239
|
if prefix.cutover_claude is _journal._CUTOVER_UNSEEN:
|
|
1182
1240
|
prefix.cutover_claude = _journal._resolve_cutover_for_rebuild(
|
|
@@ -1203,6 +1261,7 @@ def _complete_window(base, tmp, start, target, boundary, round_index, *,
|
|
|
1203
1261
|
legacy = _legacy_decided_inputs(prefix)
|
|
1204
1262
|
|
|
1205
1263
|
with _cache_read_view() as cache_conn:
|
|
1264
|
+
_lib_rebuild_progress.phase("completion:replay-all", round=round_index)
|
|
1206
1265
|
_events, by_input = _replay(
|
|
1207
1266
|
base, tmp, f"all-{round_index}.db", prefix, held_ids, cache_conn,
|
|
1208
1267
|
retention=_retention(effective, prefix, owed, legacy=legacy))
|
|
@@ -1210,6 +1269,8 @@ def _complete_window(base, tmp, start, target, boundary, round_index, *,
|
|
|
1210
1269
|
gap = _gap_inputs(by_input, selection, legacy)
|
|
1211
1270
|
if not gap:
|
|
1212
1271
|
return False
|
|
1272
|
+
_lib_rebuild_progress.phase(
|
|
1273
|
+
"completion:replay-without", round=round_index)
|
|
1213
1274
|
events_a, _ = _replay(
|
|
1214
1275
|
base, tmp, f"without-{round_index}.db", prefix, held_ids,
|
|
1215
1276
|
cache_conn, skip=gap,
|
|
@@ -1218,6 +1279,8 @@ def _complete_window(base, tmp, start, target, boundary, round_index, *,
|
|
|
1218
1279
|
for event in events_a:
|
|
1219
1280
|
reference.setdefault(
|
|
1220
1281
|
event["id"], _lib_journal.event_content_hash(event))
|
|
1282
|
+
_lib_rebuild_progress.phase(
|
|
1283
|
+
"completion:replay-with", round=round_index)
|
|
1221
1284
|
events_b, _ = _replay(
|
|
1222
1285
|
base, tmp, f"with-{round_index}.db", prefix, held_ids, cache_conn,
|
|
1223
1286
|
retention=_retention(effective, prefix, owed, legacy=legacy,
|
|
@@ -1271,6 +1334,9 @@ def _complete_window(base, tmp, start, target, boundary, round_index, *,
|
|
|
1271
1334
|
actions, sort_keys=True, separators=(",", ":"),
|
|
1272
1335
|
ensure_ascii=False).encode("utf-8")).hexdigest()
|
|
1273
1336
|
batch_id = f"{FAMILY}:{digest}"
|
|
1337
|
+
if batch_id not in selection.completed_batches:
|
|
1338
|
+
batch_id = _lib_journal.revision_batch_id(
|
|
1339
|
+
batch_id, prefix.begins.get(batch_id, ()))
|
|
1274
1340
|
if batch_id not in selection.completed_batches:
|
|
1275
1341
|
records.extend(_lib_journal.make_correction_batch(
|
|
1276
1342
|
batch_id=batch_id, family=FAMILY,
|
|
@@ -1283,5 +1349,7 @@ def _complete_window(base, tmp, start, target, boundary, round_index, *,
|
|
|
1283
1349
|
records.extend(plain)
|
|
1284
1350
|
if not records:
|
|
1285
1351
|
return False
|
|
1352
|
+
_lib_rebuild_progress.phase(
|
|
1353
|
+
"completion:append", round=round_index, records=len(records))
|
|
1286
1354
|
_journal.append_records(records)
|
|
1287
1355
|
return True
|
package/bin/_cctally_core.py
CHANGED
|
@@ -78,6 +78,7 @@ def _init_paths_from_env() -> None:
|
|
|
78
78
|
global STATUSLINE_AUTHORITATIVE_7D_PATH, STATUSLINE_AUTHORITATIVE_5H_PATH
|
|
79
79
|
global STATUSLINE_ACCOUNT_BOUND_DIR, LEGACY_SPOOL_BASELINE_PATH
|
|
80
80
|
global ACCOUNT_BOUND_CUTOVER_CACHE_PATH, LEGACY_WRITER_EVIDENCE_PATH
|
|
81
|
+
global STATUSLINE_ACCOUNT_BOUND_V1_DIR, ACCOUNT_BOUND_REVISION_CACHE_PATH
|
|
81
82
|
global OAUTH_BACKOFF_MARKER_PATH, OAUTH_BACKOFF_COUNT_PATH
|
|
82
83
|
global UPDATE_STATE_PATH, UPDATE_SUPPRESS_PATH
|
|
83
84
|
global UPDATE_LOCK_PATH, UPDATE_LOG_PATH, UPDATE_LOG_ROTATED_PATH
|
|
@@ -192,11 +193,25 @@ def _init_paths_from_env() -> None:
|
|
|
192
193
|
# the journal-anchored legacy spool baseline. The coordinate cache only
|
|
193
194
|
# accelerates locating the cutover op and never confers authority; the
|
|
194
195
|
# legacy-writer evidence sidecar is diagnostic only (spec section 8).
|
|
195
|
-
|
|
196
|
+
#
|
|
197
|
+
# #797/#823/#840 (spec 4.6 B4): revision 2 of the fence moves every
|
|
198
|
+
# current coordination file — candidates, intake, evaluations, control,
|
|
199
|
+
# pending and retry state, acknowledgements, tombstones, frozen plans,
|
|
200
|
+
# locks, transition and recovery metadata, and the protocol files below
|
|
201
|
+
# — to the v2 root. No v1 state is imported. The v1 root is retired: its
|
|
202
|
+
# `accounts/` tree and `transport.last` join the legacy spool inventory
|
|
203
|
+
# (`_cctally_journal._legacy_spool_paths`), and this binary never writes,
|
|
204
|
+
# prunes or deletes anything under it.
|
|
205
|
+
STATUSLINE_ACCOUNT_BOUND_V1_DIR = APP_DIR / "statusline-account-bound-v1"
|
|
206
|
+
STATUSLINE_ACCOUNT_BOUND_DIR = APP_DIR / "statusline-account-bound-v2"
|
|
196
207
|
LEGACY_SPOOL_BASELINE_PATH = (
|
|
197
208
|
STATUSLINE_ACCOUNT_BOUND_DIR / "legacy-spool-baseline.json")
|
|
198
209
|
ACCOUNT_BOUND_CUTOVER_CACHE_PATH = (
|
|
199
210
|
STATUSLINE_ACCOUNT_BOUND_DIR / "cutover-coordinate.json")
|
|
211
|
+
#: The revision boundary's coordinate cache; like the cutover's, it only
|
|
212
|
+
#: accelerates the lookup and never confers authority.
|
|
213
|
+
ACCOUNT_BOUND_REVISION_CACHE_PATH = (
|
|
214
|
+
STATUSLINE_ACCOUNT_BOUND_DIR / "revision-coordinate.json")
|
|
200
215
|
LEGACY_WRITER_EVIDENCE_PATH = (
|
|
201
216
|
STATUSLINE_ACCOUNT_BOUND_DIR / "legacy-writer-evidence.json")
|
|
202
217
|
OAUTH_BACKOFF_MARKER_PATH = APP_DIR / "oauth-backoff.until"
|
|
@@ -523,7 +538,23 @@ _init_paths_from_env()
|
|
|
523
538
|
# `_cctally_store.validated_read_snapshot` checks inside each decision
|
|
524
539
|
# scope's own snapshot. An epoch transition, not a numbered migration: the
|
|
525
540
|
# registry stays frozen at 13.
|
|
526
|
-
|
|
541
|
+
# 1018 -> 1019 (#797/#823/#840 spec 4.6 B6): revision 2 of the fence changes
|
|
542
|
+
# the authoritative interpretation of retained journal records again — after
|
|
543
|
+
# the `account_bound_revision` boundary a Claude-scoped record without
|
|
544
|
+
# `wpr == 2` is excluded — so an index, a derivation-completeness proof or a
|
|
545
|
+
# selector continuation produced under revision 1 is not current. The #825
|
|
546
|
+
# section 6 precedent; still no numbered migration and no accounting column.
|
|
547
|
+
# The same epoch carries the provider-sample index state of stage 1 (#797
|
|
548
|
+
# Task 2, spec 4.3 D8 and D11), so 1019 is introduced once: the
|
|
549
|
+
# `claude_oauth_samples` projection of raw api observations carrying
|
|
550
|
+
# `oauth_request_started_at`, the `claude_weekly_observation_decisions`
|
|
551
|
+
# projection of reviewed weekly decision ops that resolves a sample's weekly
|
|
552
|
+
# eligibility at read time, and four NULLABLE identity columns on
|
|
553
|
+
# `five_hour_credit_confirmation_state` (`baseline_observation_id`,
|
|
554
|
+
# `baseline_generation`, `pending_generation`, `pending_position`) that the
|
|
555
|
+
# five-hour credit generation (Task 5) writes. Both projections are rebuilt
|
|
556
|
+
# from the journal; the confirmation state stays disposable.
|
|
557
|
+
STATS_INDEX_EPOCH = 1019
|
|
527
558
|
LEGACY_STATS_HEAD = 13
|
|
528
559
|
|
|
529
560
|
|
|
@@ -2691,20 +2722,86 @@ def open_db(*, _target_path=None) -> sqlite3.Connection:
|
|
|
2691
2722
|
# above: a rebuild legitimately loses it, and the cold-start
|
|
2692
2723
|
# consequence is stated at `_read_five_hour_source_state` in
|
|
2693
2724
|
# `bin/_cctally_record.py`.
|
|
2725
|
+
#
|
|
2726
|
+
# #797/#823/#840 epoch 1019 (spec 4.9 D11): the four identity columns
|
|
2727
|
+
# the five-hour credit generation writes, all NULL until it does.
|
|
2728
|
+
# `baseline_observation_id` names the observation that set the
|
|
2729
|
+
# baseline; `baseline_generation` / `pending_generation` are the
|
|
2730
|
+
# effective five-hour credit generation the baseline and the armed
|
|
2731
|
+
# descent were observed under, encoded `"<event_id>@<revision>"` with
|
|
2732
|
+
# the no-credit sentinel `"none"`; `pending_position` is the arming
|
|
2733
|
+
# observation's logical position. Columns of the CREATE, not an
|
|
2734
|
+
# `add_column_if_missing`: an epoch-current open returns before any
|
|
2735
|
+
# schema work, so only the epoch's fresh CREATE can carry them.
|
|
2694
2736
|
conn.execute(
|
|
2695
2737
|
"""
|
|
2696
2738
|
CREATE TABLE IF NOT EXISTS five_hour_credit_confirmation_state (
|
|
2697
|
-
account_key
|
|
2698
|
-
five_hour_window_key
|
|
2699
|
-
source
|
|
2700
|
-
baseline_pct
|
|
2701
|
-
pending_low_pct
|
|
2702
|
-
pending_at_utc
|
|
2703
|
-
pending_observation_id
|
|
2739
|
+
account_key TEXT NOT NULL,
|
|
2740
|
+
five_hour_window_key INTEGER NOT NULL,
|
|
2741
|
+
source TEXT NOT NULL,
|
|
2742
|
+
baseline_pct REAL NOT NULL,
|
|
2743
|
+
pending_low_pct REAL,
|
|
2744
|
+
pending_at_utc TEXT,
|
|
2745
|
+
pending_observation_id TEXT,
|
|
2746
|
+
baseline_observation_id TEXT,
|
|
2747
|
+
baseline_generation TEXT,
|
|
2748
|
+
pending_generation TEXT,
|
|
2749
|
+
pending_position TEXT,
|
|
2704
2750
|
PRIMARY KEY (account_key, five_hour_window_key, source)
|
|
2705
2751
|
)
|
|
2706
2752
|
"""
|
|
2707
2753
|
)
|
|
2754
|
+
# #797/#823/#840 epoch 1019 (spec 4.3 D8, D9): provider samples. One
|
|
2755
|
+
# row per ADMITTED Claude api observation carrying a valid
|
|
2756
|
+
# `oauth_request_started_at` (`requested_at_utc`, canonical
|
|
2757
|
+
# full-precision UTC, so text order is time order), projected from
|
|
2758
|
+
# the RAW record by `_cctally_oauth_samples.project_sample` on every
|
|
2759
|
+
# derivation path, independently of the snapshot fold, the five-hour
|
|
2760
|
+
# adjustment and credit suppression or retirement. Values are the raw
|
|
2761
|
+
# readings, never the MAX-clamped stored ones; `weekly_resets_at_utc`
|
|
2762
|
+
# is nearest-hour normalized and `five_hour_window_key` is the
|
|
2763
|
+
# authoritative canonical key. Journal truth (rebuilt, never pruned),
|
|
2764
|
+
# indexed by account, axis window and `q` for the exact-window read.
|
|
2765
|
+
conn.execute(
|
|
2766
|
+
"""
|
|
2767
|
+
CREATE TABLE IF NOT EXISTS claude_oauth_samples (
|
|
2768
|
+
account_key TEXT NOT NULL,
|
|
2769
|
+
observation_id TEXT NOT NULL,
|
|
2770
|
+
requested_at_utc TEXT NOT NULL,
|
|
2771
|
+
captured_at_utc TEXT NOT NULL,
|
|
2772
|
+
weekly_percent REAL,
|
|
2773
|
+
weekly_resets_at_utc TEXT,
|
|
2774
|
+
five_hour_percent REAL,
|
|
2775
|
+
five_hour_resets_at_utc TEXT,
|
|
2776
|
+
five_hour_window_key INTEGER,
|
|
2777
|
+
PRIMARY KEY (account_key, observation_id)
|
|
2778
|
+
)
|
|
2779
|
+
"""
|
|
2780
|
+
)
|
|
2781
|
+
conn.execute(
|
|
2782
|
+
"CREATE INDEX IF NOT EXISTS idx_claude_oauth_samples_weekly "
|
|
2783
|
+
"ON claude_oauth_samples "
|
|
2784
|
+
"(account_key, weekly_resets_at_utc, requested_at_utc)"
|
|
2785
|
+
)
|
|
2786
|
+
conn.execute(
|
|
2787
|
+
"CREATE INDEX IF NOT EXISTS idx_claude_oauth_samples_five_hour "
|
|
2788
|
+
"ON claude_oauth_samples "
|
|
2789
|
+
"(account_key, five_hour_window_key, requested_at_utc)"
|
|
2790
|
+
)
|
|
2791
|
+
# The effective reviewed weekly decision per observation (latest
|
|
2792
|
+
# `claude_weekly_observation_decision[_v2]` op wins, journal order),
|
|
2793
|
+
# projected by that op's fold applier so a sample's weekly
|
|
2794
|
+
# eligibility is resolved at read time: `hold` excludes the weekly
|
|
2795
|
+
# sample only, a later `accept` restores it.
|
|
2796
|
+
conn.execute(
|
|
2797
|
+
"""
|
|
2798
|
+
CREATE TABLE IF NOT EXISTS claude_weekly_observation_decisions (
|
|
2799
|
+
observation_id TEXT PRIMARY KEY,
|
|
2800
|
+
disposition TEXT NOT NULL,
|
|
2801
|
+
decision_op_id TEXT NOT NULL
|
|
2802
|
+
)
|
|
2803
|
+
"""
|
|
2804
|
+
)
|
|
2708
2805
|
_backfill_week_reset_events(conn)
|
|
2709
2806
|
|
|
2710
2807
|
# ── five_hour_reset_events (Anthropic-issued in-place 5h credits) ──
|
|
@@ -11999,15 +11999,12 @@ def _dashboard_stats_deferred_snapshot(args, *, pinned_now, exc):
|
|
|
11999
11999
|
# worker owns maintenance-exclusive, doing that before HTTP bind reintroduces
|
|
12000
12000
|
# the exact multi-second startup stall this path exists to remove. Supply a
|
|
12001
12001
|
# schema-valid, privacy-safe warning block; the first successful background
|
|
12002
|
-
# snapshot replaces it with a real doctor payload.
|
|
12003
|
-
|
|
12004
|
-
|
|
12005
|
-
|
|
12006
|
-
|
|
12007
|
-
|
|
12008
|
-
),
|
|
12009
|
-
"fingerprint": "sha1:" + ("0" * 40),
|
|
12010
|
-
}
|
|
12002
|
+
# snapshot replaces it with a real doctor payload. #997 D10: the block still
|
|
12003
|
+
# carries the rebuild-worker aggregate, from the worker snapshots alone
|
|
12004
|
+
# (lock-free, never opens stats), so the chip names the running or dead
|
|
12005
|
+
# rebuild from the first frame.
|
|
12006
|
+
doctor_payload = tui._deferred_doctor_placeholder(now_utc)
|
|
12007
|
+
doctor_payload["rebuild_worker"] = tui._tui_rebuild_worker_value(now_utc)
|
|
12011
12008
|
replacements = {
|
|
12012
12009
|
# #583 S2 §6.1: this frame is degraded by construction — it carries a
|
|
12013
12010
|
# `stats-open` error and no successful build ran, so it must not stamp
|