cctally 1.87.1 → 1.88.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 +11 -0
- package/README.md +2 -2
- package/bin/_cctally_cache.py +134 -2
- package/bin/_cctally_dashboard_envelope.py +3 -2
- package/bin/_cctally_dashboard_sources.py +306 -180
- package/bin/_cctally_db.py +122 -0
- package/bin/_cctally_doctor.py +25 -1
- package/bin/_cctally_transcript.py +7 -2
- package/bin/_cctally_tui.py +20 -7
- package/bin/_lib_codex_conversation.py +17 -0
- package/bin/_lib_codex_conversation_query.py +16 -2
- package/bin/_lib_dashboard_sources.py +8 -1
- package/bin/_lib_doctor.py +57 -0
- package/bin/_lib_jsonl.py +35 -2
- package/package.json +1 -1
|
@@ -183,6 +183,64 @@ def _codex_history_row_is_model_scoped(row: object) -> bool:
|
|
|
183
183
|
return bool(isinstance(row, Mapping) and row.get("model_scoped"))
|
|
184
184
|
|
|
185
185
|
|
|
186
|
+
def _active_row_from_history(
|
|
187
|
+
history_row: Mapping[str, object], *, now_utc: dt.datetime,
|
|
188
|
+
) -> dict[str, object] | None:
|
|
189
|
+
"""Project a serialized quota history row onto its ``summary.active[]`` row.
|
|
190
|
+
|
|
191
|
+
#429 §4.1. The ONE home for the active-window predicate and the active-row
|
|
192
|
+
shape, so the initial build and ``refresh_codex_source_clock`` cannot
|
|
193
|
+
disagree about what ``captured_at`` means — the defect this fixes. Callers
|
|
194
|
+
must keep no separate copy of any part of the predicate, including the #373
|
|
195
|
+
model-pool exclusion: a live Spark/foreign-pool window must never reach an
|
|
196
|
+
account-level aggregate.
|
|
197
|
+
|
|
198
|
+
Both call sites see the same serialized shape. The clock's forecast refresh
|
|
199
|
+
rewrites ``status``, ``remaining_seconds`` and ``projected_percent`` but
|
|
200
|
+
never ``current_percent`` or ``resets_at``, so the fields read here are
|
|
201
|
+
identical at build time and at every tick.
|
|
202
|
+
|
|
203
|
+
#428: the liveness predicate and the emitted ``resets_at`` are the SAME
|
|
204
|
+
``forecast.resets_at``, which is ``baseline.canonical_resets_at``
|
|
205
|
+
(``_lib_quota.forecast_quota``). The client compares ``active[].resets_at``
|
|
206
|
+
against ``hero.cycle.resets_at`` (``activeWeeklyKeys``) to decide which
|
|
207
|
+
weekly history is the live one, so both must carry that one anchor.
|
|
208
|
+
"""
|
|
209
|
+
if _codex_history_row_is_model_scoped(history_row):
|
|
210
|
+
return None
|
|
211
|
+
forecast = history_row.get("forecast")
|
|
212
|
+
if not isinstance(forecast, Mapping):
|
|
213
|
+
return None
|
|
214
|
+
current = forecast.get("current_percent")
|
|
215
|
+
if not isinstance(current, (int, float)) or isinstance(current, bool):
|
|
216
|
+
return None
|
|
217
|
+
resets_at = forecast.get("resets_at")
|
|
218
|
+
try:
|
|
219
|
+
reset = dt.datetime.fromisoformat(
|
|
220
|
+
str(resets_at).replace("Z", "+00:00")
|
|
221
|
+
).astimezone(UTC)
|
|
222
|
+
except (TypeError, ValueError):
|
|
223
|
+
return None
|
|
224
|
+
if reset <= now_utc:
|
|
225
|
+
return None
|
|
226
|
+
row: dict[str, object] = {
|
|
227
|
+
"key": history_row.get("key"),
|
|
228
|
+
"current_percent": current,
|
|
229
|
+
# #429 §3: evidence recency — the newest PHYSICAL observation, the same
|
|
230
|
+
# one that produced this row's `freshness` and `stale_after_seconds`.
|
|
231
|
+
# Not the interpreted baseline, which is a value axis and belongs to
|
|
232
|
+
# `current_percent` alone.
|
|
233
|
+
"captured_at": history_row.get("captured_at"),
|
|
234
|
+
"resets_at": resets_at,
|
|
235
|
+
"freshness": history_row.get("freshness"),
|
|
236
|
+
"stale_after_seconds": history_row.get("stale_after_seconds"),
|
|
237
|
+
}
|
|
238
|
+
account_key = history_row.get("account_key")
|
|
239
|
+
if account_key:
|
|
240
|
+
row["account_key"] = account_key
|
|
241
|
+
return row
|
|
242
|
+
|
|
243
|
+
|
|
186
244
|
def _resolve_codex_weekly_cycle(
|
|
187
245
|
observations: Iterable[object],
|
|
188
246
|
now_utc: dt.datetime,
|
|
@@ -1693,6 +1751,7 @@ def _quota_read_model(
|
|
|
1693
1751
|
*,
|
|
1694
1752
|
accounting_entries: Iterable[object] = (),
|
|
1695
1753
|
account_key: str | None = None,
|
|
1754
|
+
decorated: bool,
|
|
1696
1755
|
) -> dict[str, object]:
|
|
1697
1756
|
"""Use S2's pure history/block/forecast kernels over cache evidence.
|
|
1698
1757
|
|
|
@@ -1714,9 +1773,11 @@ def _quota_read_model(
|
|
|
1714
1773
|
cost_entries = tuple(accounting_entries)
|
|
1715
1774
|
histories = build_history(quota_observations)
|
|
1716
1775
|
blocks = build_blocks(quota_observations)
|
|
1717
|
-
history_rows: list[dict[str, object]] = []
|
|
1718
1776
|
milestone_rows: list[dict[str, object]] = []
|
|
1719
|
-
|
|
1777
|
+
# #429 §4.2: one candidate unit per identity — (ordinal, history row, active
|
|
1778
|
+
# projection or None) — so the cap retains PAIRS instead of capping the two
|
|
1779
|
+
# lists independently and in different orders.
|
|
1780
|
+
candidates: list[tuple[int, dict[str, object], dict[str, object] | None]] = []
|
|
1720
1781
|
# R8 (#341 Task 4): the per-account `account_key` is serialized onto each
|
|
1721
1782
|
# history row ONLY when the Codex provider has >1 REAL account, so the
|
|
1722
1783
|
# dashboard client can scope per-account quota rows instead of merging them.
|
|
@@ -1726,13 +1787,12 @@ def _quota_read_model(
|
|
|
1726
1787
|
# public history view is LOSSY — capped at `SOURCE_HISTORY_LIMIT` and without
|
|
1727
1788
|
# `logical_limit_key` — so it cannot resolve the cycle authoritatively. Build
|
|
1728
1789
|
# time owns resolution; a `clock_data` decision deadline forces the rebuild.
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
_codex_decorated = False
|
|
1790
|
+
#
|
|
1791
|
+
# #429 §4.4: the caller owns this gate. Re-querying here, per parent AND per
|
|
1792
|
+
# child, let a transient failure emit decorated scopes whose quota rows were
|
|
1793
|
+
# silently unstamped — and the active-row helper cannot project a field the
|
|
1794
|
+
# history row never carried.
|
|
1795
|
+
_codex_decorated = decorated
|
|
1736
1796
|
for history in histories:
|
|
1737
1797
|
identity = history.identity
|
|
1738
1798
|
key_parts = (
|
|
@@ -1777,21 +1837,11 @@ def _quota_read_model(
|
|
|
1777
1837
|
"confidence": forecast.confidence,
|
|
1778
1838
|
},
|
|
1779
1839
|
}
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
# history is the live one, so both must carry the SAME anchor.
|
|
1786
|
-
if baseline is not None and baseline.canonical_resets_at > context.now_utc:
|
|
1787
|
-
active_rows.append({
|
|
1788
|
-
"key": dashboard_resource_key("quota", "codex", *key_parts),
|
|
1789
|
-
"current_percent": baseline.used_percent,
|
|
1790
|
-
"captured_at": baseline.captured_at.astimezone(UTC).isoformat(),
|
|
1791
|
-
"resets_at": baseline.canonical_resets_at.astimezone(UTC).isoformat(),
|
|
1792
|
-
"freshness": freshness.state,
|
|
1793
|
-
"stale_after_seconds": freshness.stale_after_seconds,
|
|
1794
|
-
})
|
|
1840
|
+
candidates.append((
|
|
1841
|
+
len(candidates),
|
|
1842
|
+
row,
|
|
1843
|
+
_active_row_from_history(row, now_utc=context.now_utc),
|
|
1844
|
+
))
|
|
1795
1845
|
for block in blocks:
|
|
1796
1846
|
identity = block.identity
|
|
1797
1847
|
block_parts = (
|
|
@@ -1899,20 +1949,21 @@ def _quota_read_model(
|
|
|
1899
1949
|
"marginal_usd": max(0.0, cumulative_usd - previous_cumulative),
|
|
1900
1950
|
})
|
|
1901
1951
|
previous_cumulative = cumulative_usd
|
|
1902
|
-
latest_percent = max(
|
|
1903
|
-
(float(row["current_percent"]) for row in active_rows), default=None,
|
|
1904
|
-
)
|
|
1905
|
-
active_freshness = (
|
|
1906
|
-
"fresh" if active_rows and all(row["freshness"] == "fresh" for row in active_rows)
|
|
1907
|
-
else ("unavailable" if not active_rows else "stale")
|
|
1908
|
-
)
|
|
1909
1952
|
# Active account identities are presentation-critical. Independent
|
|
1910
1953
|
# model-scoped pools are also legitimate provider facts, so reserve the
|
|
1911
1954
|
# remaining cap space for their newest captures before inactive account
|
|
1912
1955
|
# history. Opaque resource-key order is only a stable tie-breaker.
|
|
1913
|
-
|
|
1956
|
+
#
|
|
1957
|
+
# #429 §4.2 — retention decides ONCE per (history, active) unit, then each
|
|
1958
|
+
# list is emitted in its own established order: histories in retention
|
|
1959
|
+
# order, actives in identity order. Emitting both in a single order would
|
|
1960
|
+
# reorder active rows below the cap and move bytes for every install.
|
|
1961
|
+
active_keys = {
|
|
1962
|
+
str(active["key"]) for _, _, active in candidates if active is not None
|
|
1963
|
+
}
|
|
1914
1964
|
|
|
1915
|
-
def _history_retention_key(
|
|
1965
|
+
def _history_retention_key(unit):
|
|
1966
|
+
_, row, _ = unit
|
|
1916
1967
|
key = str(row["key"])
|
|
1917
1968
|
if key in active_keys:
|
|
1918
1969
|
return (0, 0.0, key)
|
|
@@ -1927,11 +1978,22 @@ def _quota_read_model(
|
|
|
1927
1978
|
return (1, -captured_epoch, key)
|
|
1928
1979
|
return (2, 0.0, key)
|
|
1929
1980
|
|
|
1930
|
-
|
|
1931
|
-
history_rows =
|
|
1981
|
+
retained = sorted(candidates, key=_history_retention_key)[:SOURCE_HISTORY_LIMIT]
|
|
1982
|
+
history_rows = [row for _, row, _ in retained]
|
|
1983
|
+
active_rows = [
|
|
1984
|
+
active
|
|
1985
|
+
for _, _, active in sorted(retained, key=lambda unit: unit[0])
|
|
1986
|
+
if active is not None
|
|
1987
|
+
]
|
|
1988
|
+
latest_percent = max(
|
|
1989
|
+
(float(row["current_percent"]) for row in active_rows), default=None,
|
|
1990
|
+
)
|
|
1991
|
+
active_freshness = (
|
|
1992
|
+
"fresh" if active_rows and all(row["freshness"] == "fresh" for row in active_rows)
|
|
1993
|
+
else ("unavailable" if not active_rows else "stale")
|
|
1994
|
+
)
|
|
1932
1995
|
milestone_rows.sort(key=lambda row: str(row["captured_at"]), reverse=True)
|
|
1933
1996
|
milestone_rows = milestone_rows[:SOURCE_HISTORY_LIMIT]
|
|
1934
|
-
active_rows = active_rows[:SOURCE_HISTORY_LIMIT]
|
|
1935
1997
|
return {
|
|
1936
1998
|
"summary": {
|
|
1937
1999
|
"window_count": len(blocks),
|
|
@@ -2035,6 +2097,117 @@ def _refresh_budget_status_clock(
|
|
|
2035
2097
|
}
|
|
2036
2098
|
|
|
2037
2099
|
|
|
2100
|
+
def _scoped_quota_identity(row: Mapping[str, object]) -> tuple[str, str]:
|
|
2101
|
+
"""#429 §3.1. `dashboard_resource_key` carries no account, and two accounts
|
|
2102
|
+
sharing one $CODEX_HOME root emit the same key, so bare key is not an
|
|
2103
|
+
identity under decoration. `"unattributed"` is a legitimate account here."""
|
|
2104
|
+
return (str(row.get("account_key") or ""), str(row.get("key")))
|
|
2105
|
+
|
|
2106
|
+
|
|
2107
|
+
def _reclock_quota_domain(
|
|
2108
|
+
quota: Mapping[str, object], *, now_utc: dt.datetime,
|
|
2109
|
+
) -> dict[str, object]:
|
|
2110
|
+
"""Re-evaluate a quota domain's row freshness and summary against ``now``.
|
|
2111
|
+
|
|
2112
|
+
#429 §4.3. Replaces ONLY `histories` and `summary`; `blocks`, `milestones`
|
|
2113
|
+
and `cycle_index` are carried through untouched, because the per-account
|
|
2114
|
+
scopes carry them and a scope that lost them would render empty.
|
|
2115
|
+
|
|
2116
|
+
Emits TUPLES, matching what `_quota_read_model` publishes. Publication
|
|
2117
|
+
freezes lists into tuples anyway, so this is byte-identical on the wire —
|
|
2118
|
+
but it is what lets the caller detect an unchanged domain by comparing the
|
|
2119
|
+
result against the frozen original. A list would never compare equal to the
|
|
2120
|
+
tuple it was frozen from (``[] != ()``), the caller would report a change on
|
|
2121
|
+
every tick, and the retain/degrade paths that assert the EXACT prior ``data``
|
|
2122
|
+
object is handed back would break.
|
|
2123
|
+
"""
|
|
2124
|
+
refreshed = dict(quota)
|
|
2125
|
+
refreshed_histories: list[dict[str, object]] = []
|
|
2126
|
+
active_rows: list[dict[str, object]] = []
|
|
2127
|
+
for raw_history in quota.get("histories", ()):
|
|
2128
|
+
if not isinstance(raw_history, Mapping):
|
|
2129
|
+
continue
|
|
2130
|
+
history = dict(raw_history)
|
|
2131
|
+
# #350 spec §3.9: this is a PER-ROW value and must never shadow the
|
|
2132
|
+
# envelope-level `freshness`. It used to, so after the loop the
|
|
2133
|
+
# envelope held the LAST retained history row's freshness — often an
|
|
2134
|
+
# inactive row, and with a single weekly history the active weekly
|
|
2135
|
+
# one, which silently marked the whole provider stale on an idle
|
|
2136
|
+
# stale crossing and tripped idle eligibility on its own.
|
|
2137
|
+
row_freshness = _clock_freshness(
|
|
2138
|
+
history.get("captured_at"), history.get("stale_after_seconds"), now_utc,
|
|
2139
|
+
)
|
|
2140
|
+
history["freshness"] = row_freshness
|
|
2141
|
+
forecast = history.get("forecast")
|
|
2142
|
+
if isinstance(forecast, Mapping):
|
|
2143
|
+
forecast = dict(forecast)
|
|
2144
|
+
resets_at = forecast.get("resets_at")
|
|
2145
|
+
try:
|
|
2146
|
+
reset = dt.datetime.fromisoformat(
|
|
2147
|
+
str(resets_at).replace("Z", "+00:00")
|
|
2148
|
+
).astimezone(UTC)
|
|
2149
|
+
except (TypeError, ValueError):
|
|
2150
|
+
reset = None
|
|
2151
|
+
remaining = max(0, int((reset - now_utc).total_seconds())) if reset else None
|
|
2152
|
+
forecast["remaining_seconds"] = remaining
|
|
2153
|
+
sample_count = int(forecast.get("sample_count") or 0)
|
|
2154
|
+
if row_freshness == "future":
|
|
2155
|
+
forecast["status"] = "future"
|
|
2156
|
+
elif row_freshness == "stale":
|
|
2157
|
+
forecast["status"] = "stale"
|
|
2158
|
+
elif sample_count == 0:
|
|
2159
|
+
forecast["status"] = "insufficient-history"
|
|
2160
|
+
else:
|
|
2161
|
+
forecast["status"] = "ok"
|
|
2162
|
+
rate = forecast.get("rate_percent_per_hour")
|
|
2163
|
+
current = forecast.get("current_percent")
|
|
2164
|
+
if (
|
|
2165
|
+
isinstance(rate, (int, float)) and not isinstance(rate, bool)
|
|
2166
|
+
and isinstance(current, (int, float)) and not isinstance(current, bool)
|
|
2167
|
+
and remaining is not None
|
|
2168
|
+
):
|
|
2169
|
+
forecast["projected_percent"] = min(
|
|
2170
|
+
100.0, max(float(current), float(current) + float(rate) * remaining / 3600),
|
|
2171
|
+
)
|
|
2172
|
+
history["forecast"] = forecast
|
|
2173
|
+
# Called unconditionally, exactly as the build calls it. The helper
|
|
2174
|
+
# already returns None for a row without a usable forecast, and keeping
|
|
2175
|
+
# a caller-side `isinstance(forecast, Mapping)` guard here would put a
|
|
2176
|
+
# fragment of the predicate back on the caller — the split #429 exists
|
|
2177
|
+
# to remove.
|
|
2178
|
+
active = _active_row_from_history(history, now_utc=now_utc)
|
|
2179
|
+
if active is not None:
|
|
2180
|
+
active_rows.append(active)
|
|
2181
|
+
refreshed_histories.append(history)
|
|
2182
|
+
summary = dict(quota.get("summary") or {})
|
|
2183
|
+
prior_active = summary.get("active")
|
|
2184
|
+
if isinstance(prior_active, (tuple, list)):
|
|
2185
|
+
# #429 §3.1: scoped identity, not bare key — two decorated rows can
|
|
2186
|
+
# share one key and would collapse into a single map entry.
|
|
2187
|
+
active_order = {
|
|
2188
|
+
_scoped_quota_identity(row): index
|
|
2189
|
+
for index, row in enumerate(prior_active)
|
|
2190
|
+
if isinstance(row, Mapping)
|
|
2191
|
+
}
|
|
2192
|
+
active_rows.sort(
|
|
2193
|
+
key=lambda row: active_order.get(
|
|
2194
|
+
_scoped_quota_identity(row), len(active_order)),
|
|
2195
|
+
)
|
|
2196
|
+
summary.update({
|
|
2197
|
+
"active_window_count": len(active_rows),
|
|
2198
|
+
"latest_percent": max(
|
|
2199
|
+
(float(row["current_percent"]) for row in active_rows), default=None),
|
|
2200
|
+
"freshness": (
|
|
2201
|
+
"fresh" if active_rows and all(row["freshness"] == "fresh" for row in active_rows)
|
|
2202
|
+
else ("unavailable" if not active_rows else "stale")
|
|
2203
|
+
),
|
|
2204
|
+
"active": tuple(active_rows),
|
|
2205
|
+
})
|
|
2206
|
+
refreshed["histories"] = tuple(refreshed_histories)
|
|
2207
|
+
refreshed["summary"] = summary
|
|
2208
|
+
return refreshed
|
|
2209
|
+
|
|
2210
|
+
|
|
2038
2211
|
def refresh_codex_source_clock(
|
|
2039
2212
|
state: SourceDashboardState,
|
|
2040
2213
|
*,
|
|
@@ -2059,148 +2232,48 @@ def refresh_codex_source_clock(
|
|
|
2059
2232
|
freshness = state.freshness
|
|
2060
2233
|
domain_freshness = dict(state.domain_freshness or {})
|
|
2061
2234
|
if isinstance(quota, Mapping):
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
# one, which silently marked the whole provider stale on an idle
|
|
2074
|
-
# stale crossing and tripped idle eligibility on its own.
|
|
2075
|
-
row_freshness = _clock_freshness(
|
|
2076
|
-
history.get("captured_at"), history.get("stale_after_seconds"), now_utc,
|
|
2077
|
-
)
|
|
2078
|
-
history["freshness"] = row_freshness
|
|
2079
|
-
forecast = history.get("forecast")
|
|
2080
|
-
if isinstance(forecast, Mapping):
|
|
2081
|
-
forecast = dict(forecast)
|
|
2082
|
-
resets_at = forecast.get("resets_at")
|
|
2083
|
-
try:
|
|
2084
|
-
reset = dt.datetime.fromisoformat(
|
|
2085
|
-
str(resets_at).replace("Z", "+00:00")
|
|
2086
|
-
).astimezone(UTC)
|
|
2087
|
-
except (TypeError, ValueError):
|
|
2088
|
-
reset = None
|
|
2089
|
-
remaining = max(0, int((reset - now_utc).total_seconds())) if reset else None
|
|
2090
|
-
forecast["remaining_seconds"] = remaining
|
|
2091
|
-
sample_count = int(forecast.get("sample_count") or 0)
|
|
2092
|
-
if row_freshness == "future":
|
|
2093
|
-
forecast["status"] = "future"
|
|
2094
|
-
elif row_freshness == "stale":
|
|
2095
|
-
forecast["status"] = "stale"
|
|
2096
|
-
elif sample_count == 0:
|
|
2097
|
-
forecast["status"] = "insufficient-history"
|
|
2098
|
-
else:
|
|
2099
|
-
forecast["status"] = "ok"
|
|
2100
|
-
rate = forecast.get("rate_percent_per_hour")
|
|
2101
|
-
current = forecast.get("current_percent")
|
|
2102
|
-
if (
|
|
2103
|
-
isinstance(rate, (int, float)) and not isinstance(rate, bool)
|
|
2104
|
-
and isinstance(current, (int, float)) and not isinstance(current, bool)
|
|
2105
|
-
and remaining is not None
|
|
2106
|
-
):
|
|
2107
|
-
forecast["projected_percent"] = min(
|
|
2108
|
-
100.0, max(float(current), float(current) + float(rate) * remaining / 3600),
|
|
2109
|
-
)
|
|
2110
|
-
history["forecast"] = forecast
|
|
2111
|
-
# #373: same rule as the initial build, through the same
|
|
2112
|
-
# predicate, so the two paths cannot drift.
|
|
2113
|
-
if (
|
|
2114
|
-
not _codex_history_row_is_model_scoped(history)
|
|
2115
|
-
and reset is not None and reset > now_utc and current is not None
|
|
2116
|
-
):
|
|
2117
|
-
active_rows.append({
|
|
2118
|
-
"key": history.get("key"),
|
|
2119
|
-
"current_percent": current,
|
|
2120
|
-
"captured_at": history.get("captured_at"),
|
|
2121
|
-
"resets_at": resets_at,
|
|
2122
|
-
"freshness": row_freshness,
|
|
2123
|
-
"stale_after_seconds": history.get("stale_after_seconds"),
|
|
2124
|
-
})
|
|
2125
|
-
refreshed_histories.append(history)
|
|
2126
|
-
quota["histories"] = refreshed_histories
|
|
2127
|
-
latest_percent = max(
|
|
2128
|
-
(float(row["current_percent"]) for row in active_rows), default=None,
|
|
2129
|
-
)
|
|
2130
|
-
summary = dict(quota.get("summary") or {})
|
|
2131
|
-
prior_active = summary.get("active")
|
|
2132
|
-
if isinstance(prior_active, (tuple, list)):
|
|
2133
|
-
active_order = {
|
|
2134
|
-
str(row.get("key")): index
|
|
2135
|
-
for index, row in enumerate(prior_active)
|
|
2136
|
-
if isinstance(row, Mapping)
|
|
2137
|
-
}
|
|
2138
|
-
active_rows.sort(
|
|
2139
|
-
key=lambda row: active_order.get(str(row.get("key")), len(active_order)),
|
|
2140
|
-
)
|
|
2141
|
-
summary.update({
|
|
2142
|
-
"active_window_count": len(active_rows),
|
|
2143
|
-
"latest_percent": latest_percent,
|
|
2144
|
-
"freshness": (
|
|
2145
|
-
"fresh" if active_rows and all(row["freshness"] == "fresh" for row in active_rows)
|
|
2146
|
-
else ("unavailable" if not active_rows else "stale")
|
|
2147
|
-
),
|
|
2148
|
-
"active": active_rows,
|
|
2149
|
-
})
|
|
2150
|
-
# Only account-level active histories reach ``active_rows``; the shared
|
|
2151
|
-
# model-scoped predicate above excludes foreign pools. An unavailable
|
|
2152
|
-
# active set is a capability/data-availability fact, not invented
|
|
2153
|
-
# staleness, so only the exact stale verdict moves this axis.
|
|
2235
|
+
reclocked = _reclock_quota_domain(quota, now_utc=now_utc)
|
|
2236
|
+
quota_changed = reclocked != quota
|
|
2237
|
+
quota = reclocked
|
|
2238
|
+
data["quota"] = quota
|
|
2239
|
+
# Only account-level active histories reach the summary; the shared
|
|
2240
|
+
# model-scoped predicate excludes foreign pools. An unavailable active
|
|
2241
|
+
# set is a capability/data-availability fact, not invented staleness,
|
|
2242
|
+
# so only the exact stale verdict moves this axis. #429 §4.3 keeps this
|
|
2243
|
+
# deriving from the TOP-LEVEL summary only — the #350 §3.9 rule that a
|
|
2244
|
+
# row-level freshness value never touches `state.freshness` extends to
|
|
2245
|
+
# the per-account scopes clocked below.
|
|
2154
2246
|
domain_freshness["quota"] = (
|
|
2155
|
-
"stale" if summary["freshness"] == "stale" else "fresh"
|
|
2247
|
+
"stale" if quota["summary"]["freshness"] == "stale" else "fresh"
|
|
2156
2248
|
)
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
"cost_usd", "input_tokens", "cached_input_tokens", "output_tokens",
|
|
2183
|
-
"reasoning_output_tokens", "total_tokens", "cycle",
|
|
2184
|
-
):
|
|
2185
|
-
hero[field] = None
|
|
2186
|
-
data["hero"] = hero
|
|
2187
|
-
refreshed_capabilities = dict(state.capabilities)
|
|
2188
|
-
refreshed_capabilities["hero"] = CapabilityRecord(
|
|
2189
|
-
"unavailable", "missing-or-conflicting-native-cycle",
|
|
2190
|
-
)
|
|
2191
|
-
capabilities = refreshed_capabilities
|
|
2192
|
-
warnings = tuple(
|
|
2193
|
-
warning for warning in state.warnings
|
|
2194
|
-
if warning.code != "codex_cycle_unavailable"
|
|
2195
|
-
) + (SourceDashboardWarning(
|
|
2196
|
-
"codex_cycle_unavailable",
|
|
2197
|
-
"Codex native reset cycle is unavailable.",
|
|
2198
|
-
"hero",
|
|
2199
|
-
),)
|
|
2200
|
-
availability = "partial"
|
|
2201
|
-
cycle_changed = True
|
|
2249
|
+
# #429 §4.3: the per-account scopes are independent evidence domains and
|
|
2250
|
+
# were never clocked at all, so a focused account read frozen freshness
|
|
2251
|
+
# forever. The published state is recursively frozen (`MappingProxyType`),
|
|
2252
|
+
# so nothing may be mutated in place — copy outward: the scopes mapping,
|
|
2253
|
+
# then the scope, then only that scope's `quota`.
|
|
2254
|
+
scopes = data.get("account_scopes")
|
|
2255
|
+
scopes_changed = False
|
|
2256
|
+
if isinstance(scopes, Mapping):
|
|
2257
|
+
rebuilt_scopes = dict(scopes)
|
|
2258
|
+
for scope_key, scope in scopes.items():
|
|
2259
|
+
if not isinstance(scope, Mapping):
|
|
2260
|
+
continue
|
|
2261
|
+
scope_quota = scope.get("quota")
|
|
2262
|
+
if not isinstance(scope_quota, Mapping):
|
|
2263
|
+
continue
|
|
2264
|
+
reclocked_scope_quota = _reclock_quota_domain(
|
|
2265
|
+
scope_quota, now_utc=now_utc)
|
|
2266
|
+
if reclocked_scope_quota == scope_quota:
|
|
2267
|
+
continue
|
|
2268
|
+
rebuilt_scope = dict(scope)
|
|
2269
|
+
rebuilt_scope["quota"] = reclocked_scope_quota
|
|
2270
|
+
rebuilt_scopes[scope_key] = rebuilt_scope
|
|
2271
|
+
scopes_changed = True
|
|
2272
|
+
if scopes_changed:
|
|
2273
|
+
data["account_scopes"] = rebuilt_scopes
|
|
2202
2274
|
budget_domain = data.get("budget")
|
|
2203
2275
|
budget_changed = False
|
|
2276
|
+
refreshed_budget = None
|
|
2204
2277
|
if isinstance(budget_domain, Mapping):
|
|
2205
2278
|
budget_domain = dict(budget_domain)
|
|
2206
2279
|
refreshed_budget = _refresh_budget_status_clock(
|
|
@@ -2214,13 +2287,62 @@ def refresh_codex_source_clock(
|
|
|
2214
2287
|
if refreshed_budget is not None:
|
|
2215
2288
|
budget_domain["status"] = refreshed_budget
|
|
2216
2289
|
data["budget"] = budget_domain
|
|
2217
|
-
hero = data.get("hero")
|
|
2218
|
-
if isinstance(hero, Mapping):
|
|
2219
|
-
hero = dict(hero)
|
|
2220
|
-
hero["budget"] = refreshed_budget
|
|
2221
|
-
data["hero"] = hero
|
|
2222
2290
|
budget_changed = True
|
|
2223
|
-
|
|
2291
|
+
# #429 §4.5: all three hero mutations compose on ONE copy, in a stated
|
|
2292
|
+
# order. Three independent `dict(hero)` copies let the last write win, which
|
|
2293
|
+
# is how `hero["quota"]` stayed frozen at its build-time value while
|
|
2294
|
+
# `quota["summary"]` advanced.
|
|
2295
|
+
hero = data.get("hero")
|
|
2296
|
+
if isinstance(hero, Mapping):
|
|
2297
|
+
hero = dict(hero)
|
|
2298
|
+
# 1. quota first — `_clock_cycle_expired` reads `hero["cycle"]`, never
|
|
2299
|
+
# `hero["quota"]`, so replacing quota cannot affect the predicate.
|
|
2300
|
+
if isinstance(quota, Mapping):
|
|
2301
|
+
hero["quota"] = quota["summary"]
|
|
2302
|
+
# 2. cycle expiry, with its capability/warning consequences.
|
|
2303
|
+
# #350 spec §3.3: the clock no longer RE-DERIVES cycle validity. Its
|
|
2304
|
+
# public-history view is lossy (capped, no `logical_limit_key`, no
|
|
2305
|
+
# `quota_identity`), so it cannot resolve the cycle correctly — and
|
|
2306
|
+
# per §2.2 it cannot simply trust the old verdict forever either,
|
|
2307
|
+
# because resolution is time-dependent on frozen evidence. Build time
|
|
2308
|
+
# owns resolution and records a decision deadline in `clock_data`; the
|
|
2309
|
+
# tick rebuilds authoritatively at the crossing. All the clock keeps
|
|
2310
|
+
# is this cheap invariant guard: a cycle that has already RESET cannot
|
|
2311
|
+
# bound current accounting, so it degrades exactly as before. Expiry
|
|
2312
|
+
# is also deadline candidate #1, so the two paths are disjoint
|
|
2313
|
+
# belt-and-suspenders rather than a single mechanism.
|
|
2314
|
+
hero_capability = state.capabilities.get("hero")
|
|
2315
|
+
if (
|
|
2316
|
+
isinstance(hero.get("cycle"), Mapping)
|
|
2317
|
+
and hero_capability is not None
|
|
2318
|
+
and hero_capability.status == "supported"
|
|
2319
|
+
and _clock_cycle_expired(hero.get("cycle"), now_utc)
|
|
2320
|
+
):
|
|
2321
|
+
for field in (
|
|
2322
|
+
"cost_usd", "input_tokens", "cached_input_tokens", "output_tokens",
|
|
2323
|
+
"reasoning_output_tokens", "total_tokens", "cycle",
|
|
2324
|
+
):
|
|
2325
|
+
hero[field] = None
|
|
2326
|
+
refreshed_capabilities = dict(state.capabilities)
|
|
2327
|
+
refreshed_capabilities["hero"] = CapabilityRecord(
|
|
2328
|
+
"unavailable", "missing-or-conflicting-native-cycle",
|
|
2329
|
+
)
|
|
2330
|
+
capabilities = refreshed_capabilities
|
|
2331
|
+
warnings = tuple(
|
|
2332
|
+
warning for warning in state.warnings
|
|
2333
|
+
if warning.code != "codex_cycle_unavailable"
|
|
2334
|
+
) + (SourceDashboardWarning(
|
|
2335
|
+
"codex_cycle_unavailable",
|
|
2336
|
+
"Codex native reset cycle is unavailable.",
|
|
2337
|
+
"hero",
|
|
2338
|
+
),)
|
|
2339
|
+
availability = "partial"
|
|
2340
|
+
cycle_changed = True
|
|
2341
|
+
# 3. budget last
|
|
2342
|
+
if refreshed_budget is not None:
|
|
2343
|
+
hero["budget"] = refreshed_budget
|
|
2344
|
+
data["hero"] = hero
|
|
2345
|
+
if not (quota_changed or budget_changed or cycle_changed or scopes_changed):
|
|
2224
2346
|
return state
|
|
2225
2347
|
refreshed_state = SourceDashboardState(
|
|
2226
2348
|
source=state.source,
|
|
@@ -2832,6 +2954,9 @@ def _codex_account_scopes_wire(
|
|
|
2832
2954
|
quota = _quota_read_model(
|
|
2833
2955
|
context, account_observations, accounting_entries=rows,
|
|
2834
2956
|
account_key=key,
|
|
2957
|
+
# #429 §4.4: this wire is only ever reached under decoration — the
|
|
2958
|
+
# caller gates the whole `account_scopes` surface on it.
|
|
2959
|
+
decorated=True,
|
|
2835
2960
|
)
|
|
2836
2961
|
# Each decorated child owns the only honest cycle index for that
|
|
2837
2962
|
# account. Reusing a merged parent index here would render account A's
|
|
@@ -3228,6 +3353,7 @@ def build_codex_source_state(
|
|
|
3228
3353
|
context,
|
|
3229
3354
|
quota_observations,
|
|
3230
3355
|
accounting_entries=visible_accounting_entries,
|
|
3356
|
+
decorated=_codex_decorated,
|
|
3231
3357
|
)
|
|
3232
3358
|
# R8 gate, resolved ONCE and threaded (#341 Task 4 / #416 §5.8). Every
|
|
3233
3359
|
# per-account decoration below — block/alert/budget `account_key`, the
|