cctally 1.111.0 → 1.113.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 +22 -0
- package/README.md +6 -6
- package/bin/_cctally_cache.py +14 -7
- package/bin/_cctally_core.py +9 -1
- package/bin/_cctally_dashboard_envelope.py +7 -6
- package/bin/_cctally_dashboard_sources.py +90 -55
- package/bin/_cctally_doctor.py +73 -27
- package/bin/_cctally_forecast.py +2 -1
- package/bin/_cctally_journal.py +147 -6
- package/bin/_cctally_milestones.py +29 -3
- package/bin/_cctally_quota.py +10 -1
- package/bin/_cctally_record.py +136 -4
- package/bin/_cctally_rederive.py +311 -8
- package/bin/_cctally_source_analytics.py +20 -8
- package/bin/_cctally_tui.py +546 -45
- package/bin/_lib_codex_conversation_query.py +40 -4
- package/bin/_lib_codex_metadata.py +57 -11
- package/bin/_lib_cost_provenance.py +303 -0
- package/bin/_lib_dashboard_sources.py +263 -28
- package/bin/_lib_doctor.py +2 -2
- package/bin/_lib_ingest_frontier.py +137 -38
- package/bin/_lib_pricing.py +29 -1
- package/bin/_lib_rederive.py +704 -36
- package/bin/_lib_snapshot_cache.py +332 -9
- package/dashboard/static/assets/{ConversationsView-BB9vUiL6.js → ConversationsView-CEZ-BUyi.js} +1 -1
- package/dashboard/static/assets/{DoctorModal-DFY2iPDf.js → DoctorModal-CE1oH_Zy.js} +1 -1
- package/dashboard/static/assets/ModalRoot-D5Y8IFUG.js +1 -0
- package/dashboard/static/assets/{ProjectsDrillPanel-DAv39uSo.js → ProjectsDrillPanel-B3DZoCeD.js} +1 -1
- package/dashboard/static/assets/{SourceDetailModal-DSAWo2Fp.js → SourceDetailModal-CHp9EgLP.js} +1 -1
- package/dashboard/static/assets/{UpdateModal-R_9dOVnh.js → UpdateModal-5iKST6jo.js} +1 -1
- package/dashboard/static/assets/{index-DdU62sov.js → index-Cm0VTxJC.js} +2 -2
- package/dashboard/static/assets/index-Dt4tqf43.css +1 -0
- package/dashboard/static/assets/{outlineNavigation-BOCLcRa6.js → outlineNavigation-DkXGL8J6.js} +1 -1
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +2 -1
- package/dashboard/static/assets/ModalRoot-DvRKHbQE.js +0 -1
- package/dashboard/static/assets/index-COhxe3vU.css +0 -1
|
@@ -6,8 +6,10 @@ import datetime as dt
|
|
|
6
6
|
import hashlib
|
|
7
7
|
import json
|
|
8
8
|
import math
|
|
9
|
+
import os
|
|
9
10
|
import re
|
|
10
11
|
import sqlite3
|
|
12
|
+
import time
|
|
11
13
|
from dataclasses import dataclass
|
|
12
14
|
from types import MappingProxyType
|
|
13
15
|
from typing import Literal, Mapping
|
|
@@ -717,6 +719,54 @@ def reuse_coherent_source_state(
|
|
|
717
719
|
return prior if _reusable_provider(prior) and prior.data_version == data_version else None
|
|
718
720
|
|
|
719
721
|
|
|
722
|
+
#: The complete warning-code set of a settled cycle-unavailable generation.
|
|
723
|
+
SETTLED_CODEX_CYCLE_WARNING_CODES = frozenset(("codex_cycle_unavailable",))
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
def settled_codex_cycle_unavailable(state: object) -> bool:
|
|
727
|
+
"""Whether a Codex generation is the SETTLED cycle-unavailable state (#857).
|
|
728
|
+
|
|
729
|
+
The one Codex-only predicate both TUI retention legs consult. It names the
|
|
730
|
+
everyday state of an install that has not used Codex since its last weekly
|
|
731
|
+
reset: the build SUCCEEDED, every domain it publishes is current, and the
|
|
732
|
+
only thing withheld is the hero, because no native weekly boundary contains
|
|
733
|
+
``now``. That is ``partial`` and ``fresh`` with data present and exactly one
|
|
734
|
+
warning code, ``codex_cycle_unavailable``.
|
|
735
|
+
|
|
736
|
+
It is deliberately narrower than "partial for a known cause". Every
|
|
737
|
+
neighbour still rebuilds: a composite with an incoherent projection (the
|
|
738
|
+
certificate-recovery case), an unresolved account scope (#819), incomplete
|
|
739
|
+
metadata (the retry kernel's own business), an unknown code, stale or
|
|
740
|
+
missing data, a failed aggregate fold, and a RETRYABLE metadata carrier,
|
|
741
|
+
which no reuse or retention gate may admit (#846). The cause is NOT added to
|
|
742
|
+
``_lib_source_retry.RETAINABLE_PARTIAL_CAUSES``: that allowlist also decides
|
|
743
|
+
composite keys, and this state is not a throttled retry but a generation
|
|
744
|
+
with nothing left to retry.
|
|
745
|
+
|
|
746
|
+
This predicate states only the SHAPE. Whether the shape may be republished
|
|
747
|
+
this tick is the caller's question — its captured input identity must still
|
|
748
|
+
be current and its decision deadline must not have passed — because only
|
|
749
|
+
the caller holds the current identity and the clock.
|
|
750
|
+
"""
|
|
751
|
+
if not isinstance(state, SourceDashboardState) or state.source != "codex":
|
|
752
|
+
return False
|
|
753
|
+
if (
|
|
754
|
+
state.availability != "partial"
|
|
755
|
+
or state.freshness != "fresh"
|
|
756
|
+
or state.data is None
|
|
757
|
+
or not state.data_version
|
|
758
|
+
):
|
|
759
|
+
return False
|
|
760
|
+
if frozenset(
|
|
761
|
+
warning.code for warning in state.warnings
|
|
762
|
+
) != SETTLED_CODEX_CYCLE_WARNING_CODES:
|
|
763
|
+
return False
|
|
764
|
+
health = state.metadata_health
|
|
765
|
+
if health is not None and bool(health.get("retryable")):
|
|
766
|
+
return False
|
|
767
|
+
return not aggregate_scope_failed(state)
|
|
768
|
+
|
|
769
|
+
|
|
720
770
|
# === #556 S1 — the typed combined outcome (spec §3.5, §3.7) =================
|
|
721
771
|
#
|
|
722
772
|
# `combined` is the sum, over both providers, of that provider's accounting
|
|
@@ -987,15 +1037,7 @@ def _decorated_combined_leg(
|
|
|
987
1037
|
}, ()
|
|
988
1038
|
|
|
989
1039
|
|
|
990
|
-
def
|
|
991
|
-
"""Normalize an aware ISO-8601 firing instant to one UTC ``Z`` spelling.
|
|
992
|
-
|
|
993
|
-
#556 S3 §2.2. The union sorted on a field one writer never wrote, so a
|
|
994
|
-
missing or malformed value must raise here rather than degrade to a
|
|
995
|
-
sentinel that sorts silently. Sub-second precision is truncated, so two
|
|
996
|
-
alerts firing in the same second compare equal and fall back to source
|
|
997
|
-
order; no writer emits it today.
|
|
998
|
-
"""
|
|
1040
|
+
def _parse_alerted_at(value: object) -> dt.datetime:
|
|
999
1041
|
if not isinstance(value, str) or not value:
|
|
1000
1042
|
raise ValueError(f"alerted_at must be a non-empty ISO-8601 string, got {value!r}")
|
|
1001
1043
|
try:
|
|
@@ -1004,25 +1046,96 @@ def canonical_alerted_at(value: object) -> str:
|
|
|
1004
1046
|
raise ValueError(f"unparseable alerted_at {value!r}") from exc
|
|
1005
1047
|
if parsed.tzinfo is None:
|
|
1006
1048
|
raise ValueError(f"naive alerted_at {value!r}; an aware instant is required")
|
|
1007
|
-
return parsed.astimezone(dt.timezone.utc)
|
|
1049
|
+
return parsed.astimezone(dt.timezone.utc)
|
|
1050
|
+
|
|
1051
|
+
|
|
1052
|
+
def canonical_alerted_at(value: object) -> str:
|
|
1053
|
+
"""Normalize an aware ISO-8601 firing instant to one UTC ``Z`` spelling.
|
|
1054
|
+
|
|
1055
|
+
#556 S3 §2.2. The union sorted on a field one writer never wrote, so a
|
|
1056
|
+
missing or malformed value must raise here rather than degrade to a
|
|
1057
|
+
sentinel that sorts silently. A whole-second instant keeps its historical
|
|
1058
|
+
``…SSZ`` spelling. #869 F14: a Codex budget crossing records the exact
|
|
1059
|
+
microsecond cutoff its spend scan used, so a sub-second instant keeps its
|
|
1060
|
+
six-digit fraction instead of being truncated into a tie with a Claude
|
|
1061
|
+
alert from the same second. This value is for publication; ordering uses
|
|
1062
|
+
:func:`alerted_at_order_key`, because two widths do not sort as text.
|
|
1063
|
+
"""
|
|
1064
|
+
parsed = _parse_alerted_at(value)
|
|
1065
|
+
if parsed.microsecond:
|
|
1066
|
+
return parsed.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
|
|
1067
|
+
return parsed.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
1068
|
+
|
|
1069
|
+
|
|
1070
|
+
def alerted_at_order_key(value: object) -> str:
|
|
1071
|
+
"""The full firing instant as fixed-width UTC text: ``…SS.ffffffZ``.
|
|
1072
|
+
|
|
1073
|
+
#869 F14. Every truncation and union across vendors compares firing
|
|
1074
|
+
instants, and a whole-second Claude alert must sort before a Codex alert
|
|
1075
|
+
fired later in the same second. Fixed width makes text order equal instant
|
|
1076
|
+
order, and :func:`alerted_at_order_key_sql` produces the same string in
|
|
1077
|
+
SQLite. Raises exactly as :func:`canonical_alerted_at` does.
|
|
1078
|
+
"""
|
|
1079
|
+
return _parse_alerted_at(value).strftime("%Y-%m-%dT%H:%M:%S.%fZ")
|
|
1080
|
+
|
|
1081
|
+
|
|
1082
|
+
def _alerted_at_fraction_sql(column: str) -> str:
|
|
1083
|
+
"""The six-digit sub-second fraction of an ISO-8601 value, in SQLite.
|
|
1084
|
+
|
|
1085
|
+
SQLite's date functions keep only milliseconds, so the fraction is read
|
|
1086
|
+
from the text itself. A UTC offset is a whole number of minutes, so the
|
|
1087
|
+
source fraction is also the UTC instant's fraction.
|
|
1088
|
+
"""
|
|
1089
|
+
tail = f"substr({column}, 21)"
|
|
1090
|
+
digits = (
|
|
1091
|
+
f"substr({tail}, 1, CASE "
|
|
1092
|
+
f"WHEN instr({tail}, 'Z') > 0 THEN instr({tail}, 'Z') - 1 "
|
|
1093
|
+
f"WHEN instr({tail}, '+') > 0 THEN instr({tail}, '+') - 1 "
|
|
1094
|
+
f"WHEN instr({tail}, '-') > 0 THEN instr({tail}, '-') - 1 "
|
|
1095
|
+
f"ELSE length({tail}) END)"
|
|
1096
|
+
)
|
|
1097
|
+
return (
|
|
1098
|
+
f"(CASE WHEN substr({column}, 20, 1) = '.' "
|
|
1099
|
+
f"THEN substr({digits} || '000000', 1, 6) ELSE '000000' END)"
|
|
1100
|
+
)
|
|
1101
|
+
|
|
1102
|
+
|
|
1103
|
+
def alerted_at_order_key_sql(column: str = "alerted_at") -> str:
|
|
1104
|
+
"""The SQL twin of :func:`alerted_at_order_key`, for ``ORDER BY … LIMIT``.
|
|
1105
|
+
|
|
1106
|
+
#556 S3 §2.3 / #869 F14. Every per-axis ``LIMIT`` decides MEMBERSHIP, not
|
|
1107
|
+
merely order, so it must compare the full instant: two rows fired in the
|
|
1108
|
+
same second are otherwise tie-broken by an unrelated column and the newer
|
|
1109
|
+
one can be dropped before any later projection sees it. SQLite parses the
|
|
1110
|
+
offset for the seconds part and the text supplies the fraction, so for
|
|
1111
|
+
every aware spelling this returns the Python helper's string —
|
|
1112
|
+
``tests/test_556_s3_alert_ordering.py`` pins that agreement. A naive value
|
|
1113
|
+
is read as UTC here where the Python helper raises; an unparseable value
|
|
1114
|
+
yields ``NULL``, which sorts last under ``DESC``. The expression names
|
|
1115
|
+
``column`` several times, so a bound parameter must be numbered (``?1``).
|
|
1116
|
+
"""
|
|
1117
|
+
return (
|
|
1118
|
+
f"(strftime('%Y-%m-%dT%H:%M:%S', {column}) || '.' || "
|
|
1119
|
+
f"{_alerted_at_fraction_sql(column)} || 'Z')"
|
|
1120
|
+
)
|
|
1008
1121
|
|
|
1009
1122
|
|
|
1010
1123
|
def canonical_alerted_at_sql(column: str = "alerted_at") -> str:
|
|
1011
|
-
"""The SQL twin of :func:`canonical_alerted_at`, for
|
|
1012
|
-
|
|
1013
|
-
#556 S3 §2.3.
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
``tests/test_556_s3_alert_ordering.py`` pins that agreement over the
|
|
1019
|
-
committed estate. The twins diverge on exactly one input: SQLite reads a
|
|
1020
|
-
naive value as UTC where the Python helper raises, so such a row is ordered
|
|
1021
|
-
here and rejected later. An unparseable value yields SQL ``NULL``, which
|
|
1022
|
-
sorts last under ``DESC``, so the ``LIMIT`` usually drops that row before
|
|
1023
|
-
composition can reject it — corruption is truncated away, not surfaced.
|
|
1124
|
+
"""The SQL twin of :func:`canonical_alerted_at`, for the same spelling.
|
|
1125
|
+
|
|
1126
|
+
#556 S3 §2.3 / #869 F14. It agrees with the Python helper for every aware
|
|
1127
|
+
spelling, including a sub-second one, and is kept so a writer emitting a
|
|
1128
|
+
spelling only one side understands surfaces in the agreement test. It is
|
|
1129
|
+
variable-width, so it never orders a ``LIMIT``; that is
|
|
1130
|
+
:func:`alerted_at_order_key_sql`'s job.
|
|
1024
1131
|
"""
|
|
1025
|
-
|
|
1132
|
+
fraction = _alerted_at_fraction_sql(column)
|
|
1133
|
+
return (
|
|
1134
|
+
f"(CASE WHEN {fraction} = '000000' "
|
|
1135
|
+
f"THEN strftime('%Y-%m-%dT%H:%M:%SZ', {column}) "
|
|
1136
|
+
f"ELSE strftime('%Y-%m-%dT%H:%M:%S', {column}) || '.' || {fraction} "
|
|
1137
|
+
f"|| 'Z' END)"
|
|
1138
|
+
)
|
|
1026
1139
|
|
|
1027
1140
|
|
|
1028
1141
|
def _leg_period(
|
|
@@ -1525,7 +1638,7 @@ def _combined_alert_rows(
|
|
|
1525
1638
|
|
|
1526
1639
|
def _instant(row: Mapping[str, object]) -> str:
|
|
1527
1640
|
try:
|
|
1528
|
-
return
|
|
1641
|
+
return alerted_at_order_key(row.get("alerted_at"))
|
|
1529
1642
|
except ValueError as exc:
|
|
1530
1643
|
identity = row.get("id") if row.get("id") is not None else row.get("key")
|
|
1531
1644
|
raise ValueError(
|
|
@@ -1827,10 +1940,116 @@ _CLAUDE_STATS_DIGEST_RELATIONS: tuple[tuple[str, str], ...] = (
|
|
|
1827
1940
|
)
|
|
1828
1941
|
|
|
1829
1942
|
|
|
1943
|
+
_STATS_RELATIONS_DIGEST_MEMO: "dict[tuple, str]" = {}
|
|
1944
|
+
|
|
1945
|
+
# A file timestamp is only as fine as its filesystem: nanoseconds on APFS, a
|
|
1946
|
+
# kernel tick on ext4, one second on HFS+, two on FAT. Two commits inside one
|
|
1947
|
+
# tick can leave identical timestamps, so no digest is memoized until the
|
|
1948
|
+
# file's newest timestamp is at least this old. Any later commit then carries a
|
|
1949
|
+
# strictly newer timestamp, whatever the granularity up to this bound.
|
|
1950
|
+
_STATS_DIGEST_MEMO_SETTLE_SECONDS = 2.0
|
|
1951
|
+
|
|
1952
|
+
|
|
1953
|
+
def _stats_memo_now() -> float:
|
|
1954
|
+
"""The wall clock the settle window is measured on (a test seam)."""
|
|
1955
|
+
return time.time()
|
|
1956
|
+
|
|
1957
|
+
|
|
1958
|
+
def _stats_relations_digest_signal(
|
|
1959
|
+
stats_conn: sqlite3.Connection,
|
|
1960
|
+
) -> "tuple | None":
|
|
1961
|
+
"""Return the stats file's write identity, or no cache key.
|
|
1962
|
+
|
|
1963
|
+
The journal cursor cannot serve as this identity. ``_run_cycle`` runs the
|
|
1964
|
+
Codex projection leg and the budget-config reconcile on a cycle that
|
|
1965
|
+
consumed no journal bytes and rewrites the cursor to the same high-water
|
|
1966
|
+
mark, and the own-connection reconcile branches, the incomplete-projection
|
|
1967
|
+
reconciliation and every administrative writer never touch it at all.
|
|
1968
|
+
|
|
1969
|
+
Every committed SQLite write in rollback-journal mode rewrites pages of the
|
|
1970
|
+
main file, which moves its mtime and ctime, so the file's ``stat`` covers
|
|
1971
|
+
all of those writers, in every process, without enumerating them. SQLite
|
|
1972
|
+
skips an overwrite whose bytes are unchanged, so an idle cycle that
|
|
1973
|
+
rewrites the cursor and selector rows to identical values leaves the file,
|
|
1974
|
+
and this key, untouched. ``(st_dev, st_ino)`` covers a rebuilt or replaced
|
|
1975
|
+
index, and ``st_size`` rides along for free.
|
|
1976
|
+
|
|
1977
|
+
The file is only ever ``stat``-ed, never opened: closing a second
|
|
1978
|
+
descriptor on a database file releases every POSIX lock this process's
|
|
1979
|
+
SQLite connections hold on it.
|
|
1980
|
+
|
|
1981
|
+
A connection inside a transaction may be reading its own uncommitted
|
|
1982
|
+
writes, which the file does not show yet, so it stays on the cold path, as
|
|
1983
|
+
does an in-memory or temporary store.
|
|
1984
|
+
"""
|
|
1985
|
+
if stats_conn.in_transaction:
|
|
1986
|
+
return None
|
|
1987
|
+
try:
|
|
1988
|
+
rows = stats_conn.execute("PRAGMA database_list").fetchall()
|
|
1989
|
+
path = next(
|
|
1990
|
+
str(row[2]) for row in rows
|
|
1991
|
+
if len(row) > 2 and str(row[1]) == "main" and row[2]
|
|
1992
|
+
)
|
|
1993
|
+
stat = os.stat(path)
|
|
1994
|
+
except (OSError, StopIteration, sqlite3.Error, TypeError, ValueError):
|
|
1995
|
+
return None
|
|
1996
|
+
return (
|
|
1997
|
+
path, stat.st_dev, stat.st_ino, stat.st_size,
|
|
1998
|
+
stat.st_mtime_ns, stat.st_ctime_ns,
|
|
1999
|
+
)
|
|
2000
|
+
|
|
2001
|
+
|
|
2002
|
+
def _stats_relations_digest_memoizable(
|
|
2003
|
+
stats_conn: sqlite3.Connection,
|
|
2004
|
+
signal: tuple,
|
|
2005
|
+
observed_at: float,
|
|
2006
|
+
) -> bool:
|
|
2007
|
+
"""May a digest computed under ``signal`` be served to a later read?
|
|
2008
|
+
|
|
2009
|
+
Only when the file did not move while the relations were read, the file had
|
|
2010
|
+
settled before they were, and the store is not in WAL mode. A WAL commit
|
|
2011
|
+
lands in the ``-wal`` file and leaves the main file's timestamps alone, so
|
|
2012
|
+
the key could not see it. The mode is read here, after the relation reads,
|
|
2013
|
+
because a connection reports ``wal`` only once it has read the file. A later
|
|
2014
|
+
switch into WAL is itself a write to the main file, so a stored entry never
|
|
2015
|
+
has to be re-checked.
|
|
2016
|
+
"""
|
|
2017
|
+
newest = max(signal[4], signal[5]) / 1e9
|
|
2018
|
+
if observed_at - newest < _STATS_DIGEST_MEMO_SETTLE_SECONDS:
|
|
2019
|
+
return False
|
|
2020
|
+
try:
|
|
2021
|
+
if stats_conn.in_transaction:
|
|
2022
|
+
return False
|
|
2023
|
+
mode = stats_conn.execute("PRAGMA journal_mode").fetchone()
|
|
2024
|
+
if mode is None or str(mode[0]).lower() == "wal":
|
|
2025
|
+
return False
|
|
2026
|
+
stat = os.stat(signal[0])
|
|
2027
|
+
except (OSError, sqlite3.Error, TypeError, ValueError):
|
|
2028
|
+
return False
|
|
2029
|
+
return signal[1:] == (
|
|
2030
|
+
stat.st_dev, stat.st_ino, stat.st_size,
|
|
2031
|
+
stat.st_mtime_ns, stat.st_ctime_ns,
|
|
2032
|
+
)
|
|
2033
|
+
|
|
2034
|
+
|
|
1830
2035
|
def _stats_relations_digest(
|
|
1831
2036
|
stats_conn: sqlite3.Connection,
|
|
1832
2037
|
relations: tuple[tuple[str, str], ...],
|
|
2038
|
+
*,
|
|
2039
|
+
memo_namespace: "str | None" = None,
|
|
1833
2040
|
) -> str:
|
|
2041
|
+
signal = None
|
|
2042
|
+
observed_at = 0.0
|
|
2043
|
+
if memo_namespace is not None:
|
|
2044
|
+
# The clock is read BEFORE the file, so the settle test compares the
|
|
2045
|
+
# file against an instant no later than the relation reads.
|
|
2046
|
+
observed_at = _stats_memo_now()
|
|
2047
|
+
signal = _stats_relations_digest_signal(stats_conn)
|
|
2048
|
+
key = (memo_namespace, signal) if signal is not None else None
|
|
2049
|
+
if key is not None:
|
|
2050
|
+
cached = _STATS_RELATIONS_DIGEST_MEMO.get(key)
|
|
2051
|
+
if cached is not None:
|
|
2052
|
+
return cached
|
|
1834
2053
|
relation_rows: list[list[list[object]]] = []
|
|
1835
2054
|
for _name, query in relations:
|
|
1836
2055
|
try:
|
|
@@ -1846,7 +2065,15 @@ def _stats_relations_digest(
|
|
|
1846
2065
|
ensure_ascii=False,
|
|
1847
2066
|
separators=(",", ":"),
|
|
1848
2067
|
).encode("utf-8")
|
|
1849
|
-
|
|
2068
|
+
digest = hashlib.sha256(canonical).hexdigest()
|
|
2069
|
+
if key is not None and _stats_relations_digest_memoizable(
|
|
2070
|
+
stats_conn, signal, observed_at,
|
|
2071
|
+
):
|
|
2072
|
+
for prior in tuple(_STATS_RELATIONS_DIGEST_MEMO):
|
|
2073
|
+
if prior[0] == memo_namespace:
|
|
2074
|
+
_STATS_RELATIONS_DIGEST_MEMO.pop(prior, None)
|
|
2075
|
+
_STATS_RELATIONS_DIGEST_MEMO[key] = digest
|
|
2076
|
+
return digest
|
|
1850
2077
|
|
|
1851
2078
|
|
|
1852
2079
|
def claude_stats_digest(stats_conn: sqlite3.Connection) -> str:
|
|
@@ -1855,7 +2082,11 @@ def claude_stats_digest(stats_conn: sqlite3.Connection) -> str:
|
|
|
1855
2082
|
A missing table is an empty relation, so an older or fresh stats database
|
|
1856
2083
|
still has a stable digest — the same posture ``codex_stats_digest`` takes.
|
|
1857
2084
|
"""
|
|
1858
|
-
return _stats_relations_digest(
|
|
2085
|
+
return _stats_relations_digest(
|
|
2086
|
+
stats_conn,
|
|
2087
|
+
_CLAUDE_STATS_DIGEST_RELATIONS,
|
|
2088
|
+
memo_namespace="claude",
|
|
2089
|
+
)
|
|
1859
2090
|
|
|
1860
2091
|
|
|
1861
2092
|
def codex_stats_digest(stats_conn: sqlite3.Connection) -> str:
|
|
@@ -1866,7 +2097,11 @@ def codex_stats_digest(stats_conn: sqlite3.Connection) -> str:
|
|
|
1866
2097
|
then follows the source all-or-prior failure matrix instead of publishing a
|
|
1867
2098
|
guessed identity.
|
|
1868
2099
|
"""
|
|
1869
|
-
return _stats_relations_digest(
|
|
2100
|
+
return _stats_relations_digest(
|
|
2101
|
+
stats_conn,
|
|
2102
|
+
_CODEX_STATS_DIGEST_RELATIONS,
|
|
2103
|
+
memo_namespace="codex",
|
|
2104
|
+
)
|
|
1870
2105
|
|
|
1871
2106
|
|
|
1872
2107
|
def assess_codex_projection_coherence(
|
package/bin/_lib_doctor.py
CHANGED
|
@@ -1011,7 +1011,7 @@ def _check_db_version_ahead(s: DoctorState) -> CheckResult:
|
|
|
1011
1011
|
# current index as a mismatch; keep this in lockstep with core
|
|
1012
1012
|
# (#496 S5b §6.1). It stays a literal because this kernel is pure and
|
|
1013
1013
|
# must not import `_cctally_core`.
|
|
1014
|
-
epoch =
|
|
1014
|
+
epoch = 1016
|
|
1015
1015
|
mismatch = uv > legacy_head and uv != epoch
|
|
1016
1016
|
return {"user_version": uv, "legacy_head": legacy_head, "epoch": epoch,
|
|
1017
1017
|
"mismatch": mismatch}
|
|
@@ -3528,7 +3528,7 @@ def _check_journal_writer_guard(s: DoctorState) -> CheckResult:
|
|
|
3528
3528
|
# not promise one it cannot deliver.
|
|
3529
3529
|
_REDERIVABLE_CONFLICT_PREFIXES = (
|
|
3530
3530
|
"sa:", "wcs:", "wce:", "wr:", "fhc:", "fhbc:", "pm:", "fhm:",
|
|
3531
|
-
"bm:", "pjm:", "pbm:",
|
|
3531
|
+
"bm:", "bm2:", "pjm:", "pjm2:", "pbm:", "pbm2:",
|
|
3532
3532
|
)
|
|
3533
3533
|
|
|
3534
3534
|
|
|
@@ -1456,33 +1456,125 @@ def _provider_complete(conn, provider: str) -> bool:
|
|
|
1456
1456
|
return row is not None
|
|
1457
1457
|
|
|
1458
1458
|
|
|
1459
|
-
def _source_paths(conn, provider: str) -> tuple[
|
|
1459
|
+
def _source_paths(conn, provider: str) -> tuple[str, ...]:
|
|
1460
|
+
"""Every absolute accounting source path, as the stored string.
|
|
1461
|
+
|
|
1462
|
+
Strings rather than ``pathlib.Path`` objects: ``_source_directory_closure``
|
|
1463
|
+
resolves each distinct parent directory once, so building a path object
|
|
1464
|
+
per source row here was pure overhead on a large store (#880).
|
|
1465
|
+
"""
|
|
1460
1466
|
table = _ingest_source_table(provider)
|
|
1461
1467
|
return tuple(
|
|
1462
|
-
|
|
1468
|
+
str(row[0])
|
|
1463
1469
|
for row in conn.execute(f"SELECT path FROM {table}")
|
|
1464
1470
|
if row[0] and os.path.isabs(str(row[0]))
|
|
1465
1471
|
)
|
|
1466
1472
|
|
|
1467
1473
|
|
|
1468
|
-
def
|
|
1474
|
+
def _is_canonical_absolute(raw: str) -> bool:
|
|
1475
|
+
"""Whether ``str(pathlib.Path(raw)) == raw`` for an absolute ``raw``.
|
|
1476
|
+
|
|
1477
|
+
POSIX pathlib drops empty and ``.`` segments and a trailing separator, and
|
|
1478
|
+
keeps a leading ``//`` as a distinct anchor. A string with none of those
|
|
1479
|
+
spellings is already in pathlib's form, so its parent is the text before
|
|
1480
|
+
its last separator. ``/`` itself is excluded so that every string this
|
|
1481
|
+
accepts has a non-empty final segment.
|
|
1482
|
+
"""
|
|
1483
|
+
return (
|
|
1484
|
+
raw.startswith("/")
|
|
1485
|
+
and "//" not in raw
|
|
1486
|
+
and "/./" not in raw
|
|
1487
|
+
and not raw.endswith("/")
|
|
1488
|
+
and not raw.endswith("/.")
|
|
1489
|
+
)
|
|
1490
|
+
|
|
1491
|
+
|
|
1492
|
+
def _add_source_chain_pathlib(raw: str, normalized_roots, directories) -> None:
|
|
1493
|
+
"""The original per-source pathlib derivation, for a non-canonical string.
|
|
1494
|
+
|
|
1495
|
+
Adds the parent of ``raw`` and every directory above it, up to and
|
|
1496
|
+
including the FIRST root in ``normalized_roots`` order that lexically
|
|
1497
|
+
contains it. A parent outside every root contributes nothing.
|
|
1498
|
+
"""
|
|
1499
|
+
parent = pathlib.Path(raw).parent
|
|
1500
|
+
for root in normalized_roots:
|
|
1501
|
+
try:
|
|
1502
|
+
parent.relative_to(root)
|
|
1503
|
+
except ValueError:
|
|
1504
|
+
continue
|
|
1505
|
+
cur = parent
|
|
1506
|
+
while True:
|
|
1507
|
+
directories.add(str(cur))
|
|
1508
|
+
if cur == root:
|
|
1509
|
+
break
|
|
1510
|
+
cur = cur.parent
|
|
1511
|
+
break
|
|
1512
|
+
|
|
1513
|
+
|
|
1514
|
+
def _source_directory_closure(read_sources, roots) -> tuple[pathlib.Path, ...]:
|
|
1515
|
+
"""The directory set a walk certifies: the roots, plus each source's chain.
|
|
1516
|
+
|
|
1517
|
+
A source contributes its parent and every directory above it up to the
|
|
1518
|
+
FIRST configured root, in the caller's order, that lexically contains the
|
|
1519
|
+
parent; a source outside every root contributes nothing. That is exactly
|
|
1520
|
+
the per-source ``pathlib`` derivation this replaced, and it returns the
|
|
1521
|
+
same sorted tuple of ``pathlib.Path`` objects.
|
|
1522
|
+
|
|
1523
|
+
What changed is the cost (#880). Thousands of source rows share a few
|
|
1524
|
+
thousand parent directories, and the old loop built a path object per row,
|
|
1525
|
+
tested it against each root with ``relative_to``, and re-walked an
|
|
1526
|
+
ancestor chain its siblings had already collected. Here a canonical string
|
|
1527
|
+
is handled as text: each distinct parent is resolved once, and a chain
|
|
1528
|
+
walk stops at the first directory whose chain up to the SAME root is
|
|
1529
|
+
already collected. The memo is keyed by root, not shared, because with
|
|
1530
|
+
nested roots a directory can be complete up to an inner root while the
|
|
1531
|
+
directories between that inner root and an outer one are not — stopping
|
|
1532
|
+
at "already in the set" would lose them whenever the outer root comes
|
|
1533
|
+
first. A non-canonical string (``//``, ``/./``, a trailing separator)
|
|
1534
|
+
falls back to the original pathlib computation for that one row.
|
|
1535
|
+
|
|
1536
|
+
``read_sources`` is called only after the roots are resolved, the order
|
|
1537
|
+
the pathlib derivation used, so an unresolvable root fails as the OSError
|
|
1538
|
+
the seed path treats as a non-certifiable walk rather than behind a store
|
|
1539
|
+
error.
|
|
1540
|
+
"""
|
|
1469
1541
|
normalized_roots = tuple(pathlib.Path(root).absolute() for root in roots)
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1542
|
+
root_strings = tuple(str(root) for root in normalized_roots)
|
|
1543
|
+
# A canonical parent strictly below root `r` starts with `r + "/"`; the
|
|
1544
|
+
# filesystem root is already its own prefix. A root anchored at `//` can
|
|
1545
|
+
# contain no canonical parent, and none of its prefixes match one.
|
|
1546
|
+
root_prefixes = tuple(
|
|
1547
|
+
root if root.endswith("/") else root + "/" for root in root_strings)
|
|
1548
|
+
directories = set(root_strings)
|
|
1549
|
+
# complete[i]: directories whose whole chain up to root i is collected.
|
|
1550
|
+
complete = tuple({root} for root in root_strings)
|
|
1551
|
+
resolved_parents = set()
|
|
1552
|
+
for raw in read_sources():
|
|
1553
|
+
if not _is_canonical_absolute(raw):
|
|
1554
|
+
_add_source_chain_pathlib(raw, normalized_roots, directories)
|
|
1555
|
+
continue
|
|
1556
|
+
parent = raw[:raw.rfind("/")] or "/"
|
|
1557
|
+
if parent in resolved_parents:
|
|
1558
|
+
continue
|
|
1559
|
+
resolved_parents.add(parent)
|
|
1560
|
+
for index, prefix in enumerate(root_prefixes):
|
|
1561
|
+
if parent != root_strings[index] and not parent.startswith(prefix):
|
|
1477
1562
|
continue
|
|
1563
|
+
chain = complete[index]
|
|
1478
1564
|
cur = parent
|
|
1479
|
-
|
|
1565
|
+
# Terminates: `cur` lies strictly below root i until it becomes
|
|
1566
|
+
# root i, which is in `chain` from the start.
|
|
1567
|
+
while cur not in chain:
|
|
1568
|
+
chain.add(cur)
|
|
1480
1569
|
directories.add(cur)
|
|
1481
|
-
|
|
1482
|
-
break
|
|
1483
|
-
cur = cur.parent
|
|
1570
|
+
cur = cur[:cur.rfind("/")] or "/"
|
|
1484
1571
|
break
|
|
1485
|
-
return tuple(sorted(directories
|
|
1572
|
+
return tuple(pathlib.Path(directory) for directory in sorted(directories))
|
|
1573
|
+
|
|
1574
|
+
|
|
1575
|
+
def _directory_paths(conn, provider: str, roots) -> tuple[pathlib.Path, ...]:
|
|
1576
|
+
return _source_directory_closure(
|
|
1577
|
+
lambda: _source_paths(conn, provider), roots)
|
|
1486
1578
|
|
|
1487
1579
|
|
|
1488
1580
|
def _directory_identity(conn, provider: str, roots):
|
|
@@ -1493,10 +1585,28 @@ def _directory_identity(conn, provider: str, roots):
|
|
|
1493
1585
|
|
|
1494
1586
|
|
|
1495
1587
|
def _restat_directory_identity(saved):
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1588
|
+
"""Restat every saved directory key; ``(0, 0, 0, 0)`` when it is gone.
|
|
1589
|
+
|
|
1590
|
+
Both frontiers run this on every idle tick over thousands of keys. The
|
|
1591
|
+
keys are ``str(path)`` of the paths the walk stated, already in pathlib's
|
|
1592
|
+
normalized form, so ``os.stat`` on the key stats exactly what
|
|
1593
|
+
``pathlib.Path(key).stat()`` would — without building a path object per
|
|
1594
|
+
key per tick, which cost about three times the stat itself (#880).
|
|
1595
|
+
|
|
1596
|
+
That equivalence holds for exactly those keys: non-empty ``str`` values
|
|
1597
|
+
with no trailing separator. An empty key, a trailing slash, bytes or an
|
|
1598
|
+
integer would behave differently, and the walk never records one.
|
|
1599
|
+
"""
|
|
1600
|
+
identity = {}
|
|
1601
|
+
for raw in saved:
|
|
1602
|
+
try:
|
|
1603
|
+
st = os.stat(raw)
|
|
1604
|
+
except OSError:
|
|
1605
|
+
identity[raw] = (0, 0, 0, 0)
|
|
1606
|
+
else:
|
|
1607
|
+
identity[raw] = (
|
|
1608
|
+
st.st_dev, st.st_ino, st.st_mtime_ns, st.st_ctime_ns)
|
|
1609
|
+
return identity
|
|
1500
1610
|
|
|
1501
1611
|
|
|
1502
1612
|
def _guard_identity(paths):
|
|
@@ -1935,33 +2045,22 @@ def _conversation_source_table(provider: str) -> str:
|
|
|
1935
2045
|
raise ValueError("unknown provider")
|
|
1936
2046
|
|
|
1937
2047
|
|
|
1938
|
-
def _conversation_source_paths(conn, provider: str) -> tuple[
|
|
2048
|
+
def _conversation_source_paths(conn, provider: str) -> tuple[str, ...]:
|
|
2049
|
+
"""Every absolute transcript source path, as the stored string.
|
|
2050
|
+
|
|
2051
|
+
The transcript twin of ``_source_paths``, and a string for the same reason.
|
|
2052
|
+
"""
|
|
1939
2053
|
table = _conversation_source_table(provider)
|
|
1940
2054
|
return tuple(
|
|
1941
|
-
|
|
2055
|
+
str(row[0])
|
|
1942
2056
|
for row in conn.execute(f"SELECT path FROM {table}")
|
|
1943
2057
|
if row[0] and os.path.isabs(str(row[0]))
|
|
1944
2058
|
)
|
|
1945
2059
|
|
|
1946
2060
|
|
|
1947
2061
|
def _conversation_directory_paths(conn, provider: str, roots):
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
for source in _conversation_source_paths(conn, provider):
|
|
1951
|
-
parent = source.parent
|
|
1952
|
-
for root in normalized_roots:
|
|
1953
|
-
try:
|
|
1954
|
-
parent.relative_to(root)
|
|
1955
|
-
except ValueError:
|
|
1956
|
-
continue
|
|
1957
|
-
cur = parent
|
|
1958
|
-
while True:
|
|
1959
|
-
directories.add(cur)
|
|
1960
|
-
if cur == root:
|
|
1961
|
-
break
|
|
1962
|
-
cur = cur.parent
|
|
1963
|
-
break
|
|
1964
|
-
return tuple(sorted(directories, key=str))
|
|
2062
|
+
return _source_directory_closure(
|
|
2063
|
+
lambda: _conversation_source_paths(conn, provider), roots)
|
|
1965
2064
|
|
|
1966
2065
|
|
|
1967
2066
|
def _conversation_directory_identity(conn, provider: str, roots):
|
package/bin/_lib_pricing.py
CHANGED
|
@@ -63,7 +63,7 @@ def _chip_for_model(name: str) -> str:
|
|
|
63
63
|
# fingerprint a store recorded and refuses a write from an older process. That
|
|
64
64
|
# comparison is day-granular by construction, so two revisions sharing a date
|
|
65
65
|
# compare equal and the older process is authorized to write.
|
|
66
|
-
PRICING_SNAPSHOT_DATE = "2026-09-
|
|
66
|
+
PRICING_SNAPSHOT_DATE = "2026-09-30"
|
|
67
67
|
PRICING_STALENESS_DAYS = 60 # release pre-flight WARNs past this age
|
|
68
68
|
|
|
69
69
|
|
|
@@ -400,6 +400,11 @@ PRICING_DRIFT_ALLOWLIST: list[dict] = [
|
|
|
400
400
|
# MTok, $5 five-minute cache write, $0.20 cache read, and 2x fast rate.
|
|
401
401
|
# Its 1M context uses standard rates throughout. The pricing fingerprint
|
|
402
402
|
# advances to 2026-09-23 because another revision already used 2026-09-22.
|
|
403
|
+
# 2026-09-29: added claude-sonnet-5-5 at Anthropic's published $2/$10 per
|
|
404
|
+
# MTok, $2.50 five-minute cache write and $0.20 cache read, the same card
|
|
405
|
+
# as Sonnet 5 and the standard 0.1x read multiplier. 1M context at standard
|
|
406
|
+
# pricing, so NO above-200k tier, and no fast mode, so no fast multiplier.
|
|
407
|
+
# Verified against LiteLLM's anthropic-provider row on 2026-09-29.
|
|
403
408
|
# Anthropic prices a cache WRITE by TTL: 1.25x base input for a 5-minute write,
|
|
404
409
|
# 2x for a 1-hour write. Both WRITE multipliers are documented as applying
|
|
405
410
|
# consistently across all supported models, so the 1h rate is DERIVED from
|
|
@@ -646,6 +651,13 @@ CLAUDE_MODEL_PRICING: dict[str, dict[str, Any]] = {
|
|
|
646
651
|
"cache_creation_input_token_cost": 2.5e-06,
|
|
647
652
|
"cache_read_input_token_cost": 2e-07,
|
|
648
653
|
},
|
|
654
|
+
"claude-sonnet-5-5": {
|
|
655
|
+
# Source: https://platform.claude.com/docs/en/models/sonnet-5-5/overview
|
|
656
|
+
"input_cost_per_token": 2e-06,
|
|
657
|
+
"output_cost_per_token": 1e-05,
|
|
658
|
+
"cache_creation_input_token_cost": 2.5e-06,
|
|
659
|
+
"cache_read_input_token_cost": 2e-07,
|
|
660
|
+
},
|
|
649
661
|
}
|
|
650
662
|
|
|
651
663
|
# Anthropic fast-mode pricing is genuinely model-specific. Unsupported models
|
|
@@ -750,6 +762,11 @@ _unknown_model_warnings: set[str] = set()
|
|
|
750
762
|
# card at $12.50 input / $1.25 cached input / $75.00 output per MTok.
|
|
751
763
|
# The current OpenAI table lists its 5.6 successor instead. LiteLLM does
|
|
752
764
|
# not publish an above-272K rate for 5.5 Cyber, so do not infer that tier.
|
|
765
|
+
# 2026-09-29: added gpt-6.1-sol from OpenAI's model and pricing pages:
|
|
766
|
+
# Standard $2.00 input / $0.10 cached input / $10.00 output per MTok;
|
|
767
|
+
# above 272K, $4.00 / $0.20 / $15.00. Fast mode is 2x. LiteLLM does not
|
|
768
|
+
# yet carry this identifier. The snapshot date advances to 2026-09-30
|
|
769
|
+
# because the Sonnet 5.5 sync already used 2026-09-29 (#705).
|
|
753
770
|
#
|
|
754
771
|
# Billing rules:
|
|
755
772
|
# - reasoning_output_tokens is billed at the *output* rate (matches
|
|
@@ -915,6 +932,17 @@ CODEX_MODEL_PRICING: dict[str, dict[str, Any]] = {
|
|
|
915
932
|
"cache_read_input_token_cost_above_272k_tokens": 4e-07,
|
|
916
933
|
"output_cost_per_token_above_272k_tokens": 1.5e-05,
|
|
917
934
|
},
|
|
935
|
+
"gpt-6.1-sol": {
|
|
936
|
+
# Source: https://developers.openai.com/api/docs/models/gpt-6.1-sol
|
|
937
|
+
# Standard $2.00/M input, $0.10/M cached input, $10.00/M output;
|
|
938
|
+
# above 272K input, rates are 2x input/cache and 1.5x output.
|
|
939
|
+
"input_cost_per_token": 2e-06,
|
|
940
|
+
"cache_read_input_token_cost": 1e-07,
|
|
941
|
+
"output_cost_per_token": 1e-05,
|
|
942
|
+
"input_cost_per_token_above_272k_tokens": 4e-06,
|
|
943
|
+
"cache_read_input_token_cost_above_272k_tokens": 2e-07,
|
|
944
|
+
"output_cost_per_token_above_272k_tokens": 1.5e-05,
|
|
945
|
+
},
|
|
918
946
|
"gpt-6-luna": {
|
|
919
947
|
# Source: https://developers.openai.com/api/docs/models/gpt-6-luna
|
|
920
948
|
"input_cost_per_token": 1e-07,
|