cctally 1.113.0 → 1.115.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 +47 -0
- package/README.md +4 -6
- package/bin/_cctally_account.py +32 -8
- package/bin/_cctally_cache.py +6035 -722
- package/bin/_cctally_completion.py +1287 -0
- package/bin/_cctally_config.py +37 -0
- package/bin/_cctally_core.py +400 -2
- package/bin/_cctally_dashboard.py +98 -20
- package/bin/_cctally_dashboard_conversation.py +292 -23
- package/bin/_cctally_dashboard_envelope.py +16 -3
- package/bin/_cctally_dashboard_perf.py +128 -9
- package/bin/_cctally_dashboard_sources.py +285 -213
- package/bin/_cctally_db.py +1177 -23
- package/bin/_cctally_diagnosis_sources.py +22 -4
- package/bin/_cctally_doctor.py +572 -45
- package/bin/_cctally_five_hour.py +84 -33
- package/bin/_cctally_forecast.py +108 -15
- package/bin/_cctally_journal.py +3950 -116
- package/bin/_cctally_journal_repair.py +101 -12
- package/bin/_cctally_milestone_history.py +82 -9
- package/bin/_cctally_parser.py +42 -0
- package/bin/_cctally_percent_breakdown.py +3 -2
- package/bin/_cctally_project.py +8 -2
- package/bin/_cctally_quarantine.py +652 -0
- package/bin/_cctally_quota.py +227 -37
- package/bin/_cctally_quota_model.py +360 -83
- package/bin/_cctally_record.py +691 -149
- package/bin/_cctally_rederive.py +320 -41
- package/bin/_cctally_refresh.py +84 -13
- package/bin/_cctally_source_analytics.py +83 -17
- package/bin/_cctally_statusline.py +6154 -517
- package/bin/_cctally_store.py +367 -53
- package/bin/_cctally_transcript.py +8 -7
- package/bin/_cctally_tui.py +61 -56
- package/bin/_cctally_weekrefs.py +128 -21
- package/bin/_lib_account_binding.py +152 -0
- package/bin/_lib_account_protocol.py +386 -0
- package/bin/_lib_aggregators.py +4 -0
- package/bin/_lib_cache_coverage.py +13 -1
- package/bin/_lib_cache_report.py +35 -6
- package/bin/_lib_cache_writer_lock.py +19 -3
- package/bin/_lib_clone_copy.py +142 -0
- package/bin/_lib_codex_conversation.py +32 -0
- package/bin/_lib_codex_conversation_query.py +297 -49
- package/bin/_lib_codex_conversation_watch.py +59 -14
- package/bin/_lib_codex_drain_admission.py +214 -0
- package/bin/_lib_codex_metadata.py +18 -1
- package/bin/_lib_codex_publication_barrier.py +277 -0
- package/bin/_lib_codex_rollout_container.py +502 -0
- package/bin/_lib_conversation_dispatch.py +3 -0
- package/bin/_lib_conversation_query.py +106 -59
- package/bin/_lib_conversation_retention.py +1605 -640
- package/bin/_lib_cost_provenance.py +53 -2
- package/bin/_lib_detector_access_registry.py +3817 -0
- package/bin/_lib_doctor.py +714 -69
- package/bin/_lib_five_hour.py +126 -0
- package/bin/_lib_fmt.py +11 -0
- package/bin/_lib_ingest_frontier.py +414 -20
- package/bin/_lib_journal.py +141 -10
- package/bin/_lib_jsonl.py +5 -4
- package/bin/_lib_legacy_spool.py +126 -0
- package/bin/_lib_legacy_writer_evidence.py +136 -0
- package/bin/_lib_merge_gate.py +231 -86
- package/bin/_lib_meter_rate_change.py +10 -0
- package/bin/_lib_outcome_quarantine.py +690 -0
- package/bin/_lib_perf.py +27 -1
- package/bin/_lib_pricing.py +199 -9
- package/bin/_lib_pricing_check.py +20 -4
- package/bin/_lib_quarantine.py +154 -0
- package/bin/_lib_quota_calibration.py +53 -8
- package/bin/_lib_quota_copy.py +2 -1
- package/bin/_lib_quota_model.py +288 -46
- package/bin/_lib_reclaim_planner.py +792 -0
- package/bin/_lib_rederive.py +319 -82
- package/bin/_lib_segment_summary.py +35 -2
- package/bin/_lib_snapshot_cache.py +21 -0
- package/bin/_lib_sqlite_close.py +81 -0
- package/bin/_lib_statusline_candidates.py +292 -23
- package/bin/_lib_subscription_weeks.py +100 -17
- package/bin/_lib_tick_stats.py +217 -17
- package/bin/_lib_wal_checkpoint.py +1077 -0
- package/bin/_lib_write_budget.py +483 -0
- package/bin/_lib_write_io.py +703 -0
- package/bin/cctally +31 -1
- package/dashboard/static/assets/{ConversationsView-CEZ-BUyi.js → ConversationsView-Bc8LCOAZ.js} +1 -1
- package/dashboard/static/assets/{DoctorModal-CE1oH_Zy.js → DoctorModal-D--qVBwe.js} +1 -1
- package/dashboard/static/assets/ModalRoot-DLPgyAM_.js +1 -0
- package/dashboard/static/assets/{ProjectsDrillPanel-B3DZoCeD.js → ProjectsDrillPanel-z53a4v4M.js} +1 -1
- package/dashboard/static/assets/{SourceDetailModal-CHp9EgLP.js → SourceDetailModal-Bw0T8W-W.js} +1 -1
- package/dashboard/static/assets/{UpdateModal-5iKST6jo.js → UpdateModal-D5AKx4Yw.js} +1 -1
- package/dashboard/static/assets/index-BKITk1CT.css +1 -0
- package/dashboard/static/assets/{index-Cm0VTxJC.js → index-BXDOzSng.js} +3 -3
- package/dashboard/static/assets/{outlineNavigation-DkXGL8J6.js → outlineNavigation-JObY7cUY.js} +1 -1
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +19 -1
- package/dashboard/static/assets/ModalRoot-D5Y8IFUG.js +0 -1
- package/dashboard/static/assets/index-Dt4tqf43.css +0 -1
package/bin/_cctally_refresh.py
CHANGED
|
@@ -609,19 +609,25 @@ def _refresh_usage_inproc(timeout_seconds: float = 5.0) -> _RefreshUsageResult:
|
|
|
609
609
|
reason="test stub",
|
|
610
610
|
)
|
|
611
611
|
|
|
612
|
-
token = c._resolve_oauth_token()
|
|
613
|
-
if not token:
|
|
614
|
-
return _RefreshUsageResult(status="no_oauth_token", reason="no token")
|
|
615
|
-
|
|
616
612
|
# Force-refresh deliberately bypasses the pre-fetch backoff gate, but its
|
|
617
613
|
# resulting 429/success transition must be ordered with the automatic
|
|
618
614
|
# refresh writer. Hold the canonical selected-state lock from the fetch
|
|
619
615
|
# through authoritative publication and the matching backoff update.
|
|
620
616
|
try:
|
|
621
617
|
with c._selected_state_lock():
|
|
618
|
+
# #825/#832 spec 4.6: the selected credential and its binding are
|
|
619
|
+
# captured TOGETHER, before the fetch.
|
|
620
|
+
binding, token = _capture_selected_credential(c)
|
|
621
|
+
if binding is None:
|
|
622
|
+
return _RefreshUsageResult(
|
|
623
|
+
status="record_failed",
|
|
624
|
+
reason="the active Claude identity is unreadable or changing")
|
|
625
|
+
if not token:
|
|
626
|
+
return _RefreshUsageResult(status="no_oauth_token", reason="no token")
|
|
622
627
|
return _refresh_usage_inproc_locked(
|
|
623
628
|
c,
|
|
624
629
|
token=token,
|
|
630
|
+
binding=binding,
|
|
625
631
|
timeout_seconds=timeout_seconds,
|
|
626
632
|
)
|
|
627
633
|
except OSError as exc:
|
|
@@ -632,12 +638,20 @@ def _refresh_usage_inproc_locked(
|
|
|
632
638
|
c,
|
|
633
639
|
*,
|
|
634
640
|
token: str,
|
|
641
|
+
binding,
|
|
635
642
|
timeout_seconds: float,
|
|
636
643
|
) -> _RefreshUsageResult:
|
|
637
|
-
"""Force-refresh with the canonical selected-state lock already held.
|
|
644
|
+
"""Force-refresh with the canonical selected-state lock already held.
|
|
645
|
+
|
|
646
|
+
``binding`` was captured together with ``token`` before the fetch. The
|
|
647
|
+
response is revalidated against it before ANY mutation (spec 4.6)."""
|
|
638
648
|
try:
|
|
639
649
|
api = c._fetch_oauth_usage(token=token, timeout_seconds=timeout_seconds)
|
|
640
650
|
except RefreshUsageRateLimitError as exc:
|
|
651
|
+
if not _revalidate_selected_credential(c, binding):
|
|
652
|
+
return _RefreshUsageResult(
|
|
653
|
+
status="record_failed",
|
|
654
|
+
reason="the selected identity changed during the fetch")
|
|
641
655
|
# Force-refresh bypasses the backfill/backoff GATE (user asked for
|
|
642
656
|
# it), but a resulting 429 MUST advance the shared backoff deadline
|
|
643
657
|
# so automatic polling still honors it (spec §4).
|
|
@@ -698,6 +712,14 @@ def _refresh_usage_inproc_locked(
|
|
|
698
712
|
f"ignoring unparseable five_hour fields: {exc}"
|
|
699
713
|
)
|
|
700
714
|
|
|
715
|
+
# #825/#832 spec 4.6: publication proceeds only when the second stable
|
|
716
|
+
# binding equals the captured one AND the selected credential is still
|
|
717
|
+
# the one the response was fetched with. A->B, a torn read or a changed
|
|
718
|
+
# credential discards the response before any mutation.
|
|
719
|
+
if not _revalidate_selected_credential(c, binding):
|
|
720
|
+
return _RefreshUsageResult(
|
|
721
|
+
status="record_failed",
|
|
722
|
+
reason="the selected identity or credential changed during the fetch")
|
|
701
723
|
record_args = argparse.Namespace(
|
|
702
724
|
percent=seven_pct,
|
|
703
725
|
resets_at=str(seven_resets_epoch),
|
|
@@ -716,6 +738,7 @@ def _refresh_usage_inproc_locked(
|
|
|
716
738
|
*({"fiveHour"} if five_pct is not None else set()),
|
|
717
739
|
},
|
|
718
740
|
lock_held=True,
|
|
741
|
+
binding=binding,
|
|
719
742
|
# #583 S2 §5.3: the dashboard calls this INSIDE its own `sync_lock`
|
|
720
743
|
# while servicing a `refresh=1`, so an inner nudge would enqueue a
|
|
721
744
|
# second rebuild for work already underway. `cmd_refresh_usage`
|
|
@@ -918,11 +941,46 @@ def cmd_refresh_usage(args: argparse.Namespace) -> int:
|
|
|
918
941
|
# Hook-tick OAuth refresh path
|
|
919
942
|
# =========================================================================
|
|
920
943
|
|
|
921
|
-
def
|
|
922
|
-
"""
|
|
944
|
+
def _capture_selected_credential(c):
|
|
945
|
+
"""``(binding, token)`` captured together under the selected-state lock
|
|
946
|
+
(#825/#832 spec 4.6), or ``(None, None)`` when the identity is torn or
|
|
947
|
+
changes across the credential read. The identity is read on both sides of
|
|
948
|
+
the credential read so a credential is never paired with an identity it
|
|
949
|
+
was not selected under; the binding then carries the credential's salted
|
|
950
|
+
digest (never the credential). ``(binding, None)``: stable, no token."""
|
|
951
|
+
import _cctally_statusline as _sl
|
|
952
|
+
|
|
953
|
+
first = _sl._capture_binding()
|
|
954
|
+
if first is None:
|
|
955
|
+
return None, None
|
|
956
|
+
token = c._resolve_oauth_token()
|
|
957
|
+
second = _sl._capture_binding()
|
|
958
|
+
if second is None or second != first:
|
|
959
|
+
return None, None
|
|
960
|
+
if not token:
|
|
961
|
+
return first, None
|
|
962
|
+
bound = _sl._capture_binding(credential=token)
|
|
963
|
+
if bound is None or bound.without_credential() != first:
|
|
964
|
+
return None, None
|
|
965
|
+
return bound, token
|
|
966
|
+
|
|
967
|
+
|
|
968
|
+
def _revalidate_selected_credential(c, binding) -> bool:
|
|
969
|
+
"""The second stable read after the fetch: the identity AND the selected
|
|
970
|
+
credential must still be exactly the captured ones. A->B->A is accepted
|
|
971
|
+
only when the credential is unchanged (the response was fetched with A's
|
|
972
|
+
captured credential)."""
|
|
973
|
+
current, token = _capture_selected_credential(c)
|
|
974
|
+
return current is not None and bool(token) and current == binding
|
|
975
|
+
|
|
976
|
+
|
|
977
|
+
def _hook_tick_oauth_skip_status(c, *, throttle_seconds: float, binding) -> str | None:
|
|
978
|
+
"""Return the automatic-refresh suppression reason under selected lock.
|
|
979
|
+
Repair need and selected freshness are the captured account's own."""
|
|
923
980
|
now_epoch = int(time.time())
|
|
924
|
-
needs_repair = c._authoritative_repair_required(
|
|
925
|
-
|
|
981
|
+
needs_repair = c._authoritative_repair_required(
|
|
982
|
+
now_epoch=now_epoch, binding=binding)
|
|
983
|
+
obs_age = c._statusline_observe_age_seconds(binding=binding)
|
|
926
984
|
if (not needs_repair
|
|
927
985
|
and obs_age < float(_cctally_core.OAUTH_BACKFILL_STALE_SECONDS)):
|
|
928
986
|
return f"skipped(statusline-fresh:{int(obs_age)}s)"
|
|
@@ -995,9 +1053,6 @@ def _hook_tick_oauth_refresh(
|
|
|
995
1053
|
`five_hour`) on success, or None on any non-ok branch.
|
|
996
1054
|
"""
|
|
997
1055
|
c = _cctally()
|
|
998
|
-
token = c._resolve_oauth_token()
|
|
999
|
-
if not token:
|
|
1000
|
-
return "skipped-no-token", None
|
|
1001
1056
|
if throttle_seconds is None:
|
|
1002
1057
|
try:
|
|
1003
1058
|
throttle_seconds = float(_get_oauth_usage_config(c.load_config())["throttle_seconds"])
|
|
@@ -1010,8 +1065,15 @@ def _hook_tick_oauth_refresh(
|
|
|
1010
1065
|
# during network I/O.
|
|
1011
1066
|
try:
|
|
1012
1067
|
with c._selected_state_lock():
|
|
1068
|
+
# #825/#832 spec 4.6: capture the selected credential and its
|
|
1069
|
+
# binding together before the fetch.
|
|
1070
|
+
binding, token = _capture_selected_credential(c)
|
|
1071
|
+
if binding is None:
|
|
1072
|
+
return "err(identity-unstable)", None
|
|
1073
|
+
if not token:
|
|
1074
|
+
return "skipped-no-token", None
|
|
1013
1075
|
skipped = _hook_tick_oauth_skip_status(
|
|
1014
|
-
c, throttle_seconds=throttle_seconds,
|
|
1076
|
+
c, throttle_seconds=throttle_seconds, binding=binding,
|
|
1015
1077
|
)
|
|
1016
1078
|
except OSError:
|
|
1017
1079
|
return "err(record-usage=exc)", None
|
|
@@ -1023,6 +1085,8 @@ def _hook_tick_oauth_refresh(
|
|
|
1023
1085
|
except RefreshUsageRateLimitError as exc:
|
|
1024
1086
|
try:
|
|
1025
1087
|
with c._selected_state_lock():
|
|
1088
|
+
if not _revalidate_selected_credential(c, binding):
|
|
1089
|
+
return "err(identity-changed)", None
|
|
1026
1090
|
c._oauth_backoff_register_429(
|
|
1027
1091
|
retry_after_deadline=getattr(
|
|
1028
1092
|
exc, "retry_after_deadline", None,
|
|
@@ -1048,9 +1112,15 @@ def _hook_tick_oauth_refresh(
|
|
|
1048
1112
|
deferred = []
|
|
1049
1113
|
try:
|
|
1050
1114
|
with c._selected_state_lock():
|
|
1115
|
+
# Revalidate under the reacquired lock BEFORE any mutation: the
|
|
1116
|
+
# response is published only for the identity AND credential it
|
|
1117
|
+
# was fetched with (spec 4.6).
|
|
1118
|
+
if not _revalidate_selected_credential(c, binding):
|
|
1119
|
+
return "err(identity-changed)", None
|
|
1051
1120
|
skipped = _hook_tick_oauth_skip_status(
|
|
1052
1121
|
c,
|
|
1053
1122
|
throttle_seconds=throttle_seconds,
|
|
1123
|
+
binding=binding,
|
|
1054
1124
|
)
|
|
1055
1125
|
if skipped is not None:
|
|
1056
1126
|
return skipped, None
|
|
@@ -1058,6 +1128,7 @@ def _hook_tick_oauth_refresh(
|
|
|
1058
1128
|
record_args,
|
|
1059
1129
|
axes,
|
|
1060
1130
|
lock_held=True,
|
|
1131
|
+
binding=binding,
|
|
1061
1132
|
nudge_sink=lambda: deferred.append(1),
|
|
1062
1133
|
)
|
|
1063
1134
|
if authoritative.status != "ok":
|
|
@@ -11,7 +11,7 @@ import sqlite3
|
|
|
11
11
|
import sys
|
|
12
12
|
from copy import copy
|
|
13
13
|
from collections import defaultdict
|
|
14
|
-
from typing import Iterable
|
|
14
|
+
from typing import Callable, Iterable
|
|
15
15
|
|
|
16
16
|
from _cctally_core import (
|
|
17
17
|
_command_as_of,
|
|
@@ -19,7 +19,7 @@ from _cctally_core import (
|
|
|
19
19
|
get_week_start_name,
|
|
20
20
|
parse_iso_datetime,
|
|
21
21
|
)
|
|
22
|
-
from _cctally_cache import _codex_provider_roots
|
|
22
|
+
from _cctally_cache import CodexHistoryUnavailable, _codex_provider_roots
|
|
23
23
|
import _lib_accounts
|
|
24
24
|
from _lib_codex_metadata import (
|
|
25
25
|
DecodedCodexProjectMetadata,
|
|
@@ -153,18 +153,45 @@ _QUALIFIED_CODEX_ENTRIES_SQL = """
|
|
|
153
153
|
|
|
154
154
|
|
|
155
155
|
def _qualified_codex_path_entries_sql(identity_count: int) -> str:
|
|
156
|
-
"""Return the qualified read constrained to physical cache identities.
|
|
156
|
+
"""Return the qualified read constrained to physical cache identities.
|
|
157
|
+
|
|
158
|
+
Unordered: ``_load_qualified_codex_rows`` orders its rows (#901
|
|
159
|
+
Amendment 19 T1). Seeking ``idx_codex_entries_root_path`` cannot also
|
|
160
|
+
deliver the window read's order, so the old ``ORDER BY`` sorted every
|
|
161
|
+
entry of the dirty rollouts through a temp b-tree, and one rollout file is
|
|
162
|
+
not a bound.
|
|
163
|
+
"""
|
|
157
164
|
predicates = " OR ".join(
|
|
158
165
|
"(entries.source_root_key = ? AND entries.source_path = ?)"
|
|
159
166
|
for _ in range(identity_count)
|
|
160
167
|
)
|
|
161
|
-
|
|
168
|
+
head, order_by, _tail = _QUALIFIED_CODEX_ENTRIES_SQL.partition(
|
|
169
|
+
" ORDER BY entries.timestamp_utc ASC")
|
|
170
|
+
if not order_by:
|
|
171
|
+
raise RuntimeError(
|
|
172
|
+
"_QUALIFIED_CODEX_ENTRIES_SQL no longer carries its ORDER BY")
|
|
173
|
+
return head.replace(
|
|
162
174
|
"INDEXED BY idx_codex_entries_ts_root_conversation",
|
|
163
175
|
"INDEXED BY idx_codex_entries_root_path",
|
|
164
|
-
)
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
176
|
+
) + f" AND ({predicates})\n"
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _qualified_codex_row_order(row) -> tuple:
|
|
180
|
+
"""The window read's ``ORDER BY entries.timestamp_utc,
|
|
181
|
+
entries.source_root_key, entries.conversation_key, entries.id`` as a Python
|
|
182
|
+
key: NULL first, text by UTF-8 bytes (SQLite's BINARY collation). ``id``
|
|
183
|
+
is unique, so the order is total."""
|
|
184
|
+
def collate(value):
|
|
185
|
+
if value is None:
|
|
186
|
+
return (0, b"")
|
|
187
|
+
if isinstance(value, str):
|
|
188
|
+
return (2, value.encode("utf-8"))
|
|
189
|
+
if isinstance(value, (bytes, bytearray, memoryview)):
|
|
190
|
+
return (3, bytes(value))
|
|
191
|
+
return (1, value)
|
|
192
|
+
|
|
193
|
+
return (collate(row[0]), collate(row[3]), collate(row[4]),
|
|
194
|
+
collate(row[16]))
|
|
168
195
|
|
|
169
196
|
|
|
170
197
|
#: The thread leg of the decode inventory (§4.3 item 1). ``{threads}`` is the
|
|
@@ -1184,6 +1211,32 @@ def _require_joined_metadata(row: sqlite3.Row) -> tuple[str, str]:
|
|
|
1184
1211
|
return root_key, conversation_key
|
|
1185
1212
|
|
|
1186
1213
|
|
|
1214
|
+
def _load_qualified_codex_rows(
|
|
1215
|
+
conn: sqlite3.Connection, start: dt.datetime, end: dt.datetime,
|
|
1216
|
+
identities: "tuple[tuple[str, str], ...] | None",
|
|
1217
|
+
) -> tuple:
|
|
1218
|
+
"""The qualified accounting rows of ``[start, end)``, in
|
|
1219
|
+
``(timestamp_utc, source_root_key, conversation_key, id)`` order: the whole
|
|
1220
|
+
window, or only the given dirty ``(root, path)`` identities.
|
|
1221
|
+
|
|
1222
|
+
The window read streams in that order from
|
|
1223
|
+
``idx_codex_entries_ts_root_conversation``. The dirty-path read seeks
|
|
1224
|
+
``idx_codex_entries_root_path`` instead and is ordered here, in Python, over
|
|
1225
|
+
the rows it returns whole anyway (#901 Amendment 19 T1); the rows keep the
|
|
1226
|
+
connection's row factory.
|
|
1227
|
+
"""
|
|
1228
|
+
params: tuple[object, ...] = (
|
|
1229
|
+
start.astimezone(UTC).isoformat(), end.astimezone(UTC).isoformat(),
|
|
1230
|
+
*(value for identity in (identities or ()) for value in identity),
|
|
1231
|
+
)
|
|
1232
|
+
if identities is None:
|
|
1233
|
+
return tuple(conn.execute(_QUALIFIED_CODEX_ENTRIES_SQL, params))
|
|
1234
|
+
rows = conn.execute(
|
|
1235
|
+
_qualified_codex_path_entries_sql(len(identities)), params).fetchall()
|
|
1236
|
+
rows.sort(key=_qualified_codex_row_order)
|
|
1237
|
+
return tuple(rows)
|
|
1238
|
+
|
|
1239
|
+
|
|
1187
1240
|
def load_qualified_codex_entries(
|
|
1188
1241
|
start: dt.datetime,
|
|
1189
1242
|
end: dt.datetime,
|
|
@@ -1194,6 +1247,7 @@ def load_qualified_codex_entries(
|
|
|
1194
1247
|
cache_conn: sqlite3.Connection | None = None,
|
|
1195
1248
|
source_identities: Iterable[tuple[str, str]] | None = None,
|
|
1196
1249
|
inventory: "CodexThreadMetadataInventory | None" = None,
|
|
1250
|
+
_on_sync: "Callable[[object], object] | None" = None,
|
|
1197
1251
|
) -> tuple[QualifiedCodexEntry, ...]:
|
|
1198
1252
|
"""Load exactly one bounded, root-qualified Codex accounting read.
|
|
1199
1253
|
|
|
@@ -1240,16 +1294,10 @@ def load_qualified_codex_entries(
|
|
|
1240
1294
|
)
|
|
1241
1295
|
if stats.lock_contended:
|
|
1242
1296
|
raise QualifiedMetadataUnavailable("Codex qualified project metadata is unavailable")
|
|
1297
|
+
if _on_sync is not None:
|
|
1298
|
+
_on_sync(stats)
|
|
1243
1299
|
conn.row_factory = sqlite3.Row
|
|
1244
|
-
|
|
1245
|
-
_QUALIFIED_CODEX_ENTRIES_SQL if identities is None
|
|
1246
|
-
else _qualified_codex_path_entries_sql(len(identities))
|
|
1247
|
-
)
|
|
1248
|
-
params: tuple[object, ...] = (
|
|
1249
|
-
start.astimezone(UTC).isoformat(), end.astimezone(UTC).isoformat(),
|
|
1250
|
-
*(value for identity in (identities or ()) for value in identity),
|
|
1251
|
-
)
|
|
1252
|
-
rows = tuple(conn.execute(sql, params))
|
|
1300
|
+
rows = _load_qualified_codex_rows(conn, start, end, identities)
|
|
1253
1301
|
# §4.3: one inventory read per build. The caller supplies the build's
|
|
1254
1302
|
# own inventory when it has one; its alias leg is unbounded, which is a
|
|
1255
1303
|
# SUPERSET of the `paths=identities` leg this read would have loaded,
|
|
@@ -1903,6 +1951,8 @@ def _source_entries(
|
|
|
1903
1951
|
if qualified:
|
|
1904
1952
|
values = load_qualified_codex_entries(
|
|
1905
1953
|
start, query_end, speed=speed, sync=sync, group=group,
|
|
1954
|
+
_on_sync=_cctally()._load_sibling(
|
|
1955
|
+
"_cctally_cache")._warn_compressed_unreadable_once,
|
|
1906
1956
|
)
|
|
1907
1957
|
return assign_collision_safe_project_labels(values)
|
|
1908
1958
|
return load_codex_accounting_entries(
|
|
@@ -3228,6 +3278,8 @@ def cmd_source_diff(args: object) -> int:
|
|
|
3228
3278
|
except ValueError as exc:
|
|
3229
3279
|
print(f"diff: {exc}", file=sys.stderr)
|
|
3230
3280
|
return 2
|
|
3281
|
+
except CodexHistoryUnavailable:
|
|
3282
|
+
raise
|
|
3231
3283
|
except RuntimeError as exc:
|
|
3232
3284
|
print(str(exc), file=sys.stderr)
|
|
3233
3285
|
return 1
|
|
@@ -3261,12 +3313,16 @@ def cmd_source_diff(args: object) -> int:
|
|
|
3261
3313
|
except QualifiedMetadataUnavailable:
|
|
3262
3314
|
try:
|
|
3263
3315
|
entries = _source_entries(entry_args, start, end, qualified=False)
|
|
3316
|
+
except CodexHistoryUnavailable:
|
|
3317
|
+
raise
|
|
3264
3318
|
except RuntimeError:
|
|
3265
3319
|
return _emit_source_result(
|
|
3266
3320
|
args, SourceResult("codex", "unavailable", None, (QUALIFIED_METADATA_WARNING,)),
|
|
3267
3321
|
diff=True, claude=claude,
|
|
3268
3322
|
)
|
|
3269
3323
|
metadata_available = False
|
|
3324
|
+
except CodexHistoryUnavailable:
|
|
3325
|
+
raise
|
|
3270
3326
|
except RuntimeError:
|
|
3271
3327
|
return _emit_source_result(
|
|
3272
3328
|
args, SourceResult("codex", "unavailable", None, (QUALIFIED_METADATA_WARNING,)),
|
|
@@ -3316,6 +3372,8 @@ def cmd_source_range_cost(args: object) -> int:
|
|
|
3316
3372
|
args, start, end, qualified=bool(getattr(args, "project", None)),
|
|
3317
3373
|
inclusive_end=True,
|
|
3318
3374
|
)
|
|
3375
|
+
except CodexHistoryUnavailable:
|
|
3376
|
+
raise
|
|
3319
3377
|
except (QualifiedMetadataUnavailable, RuntimeError):
|
|
3320
3378
|
return _emit_source_result(
|
|
3321
3379
|
args, SourceResult("codex", "unavailable", None, (QUALIFIED_METADATA_WARNING,)),
|
|
@@ -3372,6 +3430,8 @@ def cmd_source_cache_report(args: object) -> int:
|
|
|
3372
3430
|
)
|
|
3373
3431
|
try:
|
|
3374
3432
|
entries = _source_entries(entry_args, start, end, qualified=False)
|
|
3433
|
+
except CodexHistoryUnavailable:
|
|
3434
|
+
raise
|
|
3375
3435
|
except RuntimeError:
|
|
3376
3436
|
return _emit_source_result(
|
|
3377
3437
|
args, SourceResult("codex", "unavailable", None, (QUALIFIED_METADATA_WARNING,)),
|
|
@@ -3432,6 +3492,10 @@ def cmd_source_report(args: object) -> int:
|
|
|
3432
3492
|
c.reconcile_codex_quota_projection(now=as_of)
|
|
3433
3493
|
force_direct_accounting = bool(getattr(sync_stats, "lock_contended", False))
|
|
3434
3494
|
already_synced = not force_direct_accounting
|
|
3495
|
+
if already_synced:
|
|
3496
|
+
cache_mod._warn_compressed_unreadable_once(sync_stats)
|
|
3497
|
+
except CodexHistoryUnavailable:
|
|
3498
|
+
raise
|
|
3435
3499
|
except (OSError, sqlite3.Error, RuntimeError):
|
|
3436
3500
|
return _emit_source_result(
|
|
3437
3501
|
args, build_codex_report_result((), (), as_of=as_of, quota_available=False),
|
|
@@ -3458,6 +3522,8 @@ def cmd_source_report(args: object) -> int:
|
|
|
3458
3522
|
args, start, end, qualified=False, sync=not already_synced,
|
|
3459
3523
|
force_direct=force_direct_accounting,
|
|
3460
3524
|
)
|
|
3525
|
+
except CodexHistoryUnavailable:
|
|
3526
|
+
raise
|
|
3461
3527
|
except RuntimeError:
|
|
3462
3528
|
return _emit_source_result(
|
|
3463
3529
|
args, build_codex_report_result((), (), as_of=as_of, quota_available=False),
|