cctally 1.111.0 → 1.112.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 +17 -0
- package/README.md +5 -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 +13 -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
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,23 @@ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [1.112.0] - 2026-09-29
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Claude cost reports price `claude-sonnet-5-5` at its published input, output and cache rates instead of showing $0 for new usage.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- `cctally dashboard` uses less CPU while idle on a large history.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- The Conversation Viewer keeps project-filtered browse working with any number of unreadable Codex project paths, avoids repeated metadata schema probes, and preserves SQLite alias ordering.
|
|
18
|
+
- The TUI and dashboard clear a Codex metadata retry as soon as its transient partial generation is rejected, so the first healthy refresh recovers immediately instead of waiting through a stale retry delay.
|
|
19
|
+
- `cctally db rederive` corrects costs recorded while a model had no rate card, including provably unpriced $0 five-hour blocks. A Codex budget alert that fallback pricing overstated is retired and can fire again when real spend crosses.
|
|
20
|
+
- After `cctally db rederive --yes`, a still-crossed Claude budget, projected or project-budget alert no longer breaks every later stats ingest until the period ends, and fired five-hour alerts stay in Recent Alerts.
|
|
21
|
+
- On phones, the dashboard's `Current Usage` dialog shows its full title and hides the onboarding hint while the dialog is open.
|
|
22
|
+
- An idle `cctally dashboard` no longer rebuilds its Codex view on every refresh after a Codex weekly reset with no newer Codex activity, which cost steady CPU while nothing changed.
|
|
23
|
+
- `cctally db rederive` no longer reprices the unchanged five-hour milestones of a closed block whose cost is frozen when a rate card changes: each keeps its recorded cumulative cost; its marginal changes only to follow the prior milestone.
|
|
24
|
+
|
|
8
25
|
## [1.111.0] - 2026-09-24
|
|
9
26
|
|
|
10
27
|
### Fixed
|
package/README.md
CHANGED
|
@@ -30,15 +30,14 @@ Your Claude Code plan meters you with a percentage that creeps up all week. ccta
|
|
|
30
30
|
</p>
|
|
31
31
|
|
|
32
32
|
<!-- cctally:latest-stable:begin -->
|
|
33
|
-
**Latest stable: v1.
|
|
33
|
+
**Latest stable: v1.111.0** (2026-09-24)
|
|
34
34
|
|
|
35
|
-
Highlights from the `v1.
|
|
35
|
+
Highlights from the `v1.109.0` to `v1.111.0` stable upgrade:
|
|
36
36
|
|
|
37
|
-
-
|
|
38
|
-
-
|
|
39
|
-
- The dashboard's Projects panel gives every billing cycle of a credited week its own percentage. Both cycles shared one calendar date, and only the later reading survived, so the second cycle could never carry one.
|
|
37
|
+
- After `cctally update` or a Homebrew upgrade installs a newer version, a running `dashboard` restarts itself so new prices and code apply. Restart it yourself once after installing this release, and after any manual `npm install -g`.
|
|
38
|
+
- A `dashboard` restart after an update no longer leaves an extra Node process running on npm installs.
|
|
40
39
|
|
|
41
|
-
[See every change in this stable upgrade](https://github.com/omrikais/cctally/releases/tag/v1.
|
|
40
|
+
[See every change in this stable upgrade](https://github.com/omrikais/cctally/releases/tag/v1.111.0)
|
|
42
41
|
<!-- cctally:latest-stable:end -->
|
|
43
42
|
|
|
44
43
|
## Quick start
|
package/bin/_cctally_cache.py
CHANGED
|
@@ -9478,6 +9478,7 @@ def _sum_codex_cost_for_range(
|
|
|
9478
9478
|
speed: str = "auto",
|
|
9479
9479
|
skip_sync: bool = False,
|
|
9480
9480
|
account_key: "str | None" = None,
|
|
9481
|
+
pricing_provenance: "dict | None" = None,
|
|
9481
9482
|
) -> float:
|
|
9482
9483
|
"""Sum USD Codex cost of all `codex_session_entries` in ``[start, end)``.
|
|
9483
9484
|
|
|
@@ -9511,17 +9512,23 @@ def _sum_codex_cost_for_range(
|
|
|
9511
9512
|
"""
|
|
9512
9513
|
c = _cctally()
|
|
9513
9514
|
eff_speed = c._resolve_codex_speed(speed)
|
|
9515
|
+
entries = c.get_codex_entries(
|
|
9516
|
+
start, end, skip_sync=skip_sync, account_key=account_key,
|
|
9517
|
+
)
|
|
9518
|
+
if pricing_provenance is not None:
|
|
9519
|
+
import _lib_cost_provenance
|
|
9520
|
+
total, evidence = _lib_cost_provenance.codex_cost_with_provenance(
|
|
9521
|
+
entries, end=end, speed=eff_speed,
|
|
9522
|
+
)
|
|
9523
|
+
pricing_provenance.update(evidence)
|
|
9524
|
+
return total
|
|
9514
9525
|
total = 0.0
|
|
9515
|
-
for entry in
|
|
9516
|
-
account_key=account_key):
|
|
9526
|
+
for entry in entries:
|
|
9517
9527
|
if entry.timestamp >= end:
|
|
9518
9528
|
continue
|
|
9519
9529
|
total += c._calculate_codex_entry_cost(
|
|
9520
|
-
entry.model,
|
|
9521
|
-
entry.
|
|
9522
|
-
entry.cached_input_tokens,
|
|
9523
|
-
entry.output_tokens,
|
|
9524
|
-
entry.reasoning_output_tokens,
|
|
9530
|
+
entry.model, entry.input_tokens, entry.cached_input_tokens,
|
|
9531
|
+
entry.output_tokens, entry.reasoning_output_tokens,
|
|
9525
9532
|
speed=eff_speed,
|
|
9526
9533
|
)
|
|
9527
9534
|
return total
|
package/bin/_cctally_core.py
CHANGED
|
@@ -468,7 +468,12 @@ _init_paths_from_env()
|
|
|
468
468
|
# a migration is unchanged: the registry is frozen at 13, and an epoch-current
|
|
469
469
|
# open returns before any schema work, so a handler or an
|
|
470
470
|
# `add_column_if_missing` would never run on an upgraded install.
|
|
471
|
-
|
|
471
|
+
# 1015 -> 1016 (#869): an open five-hour block records the pricing evidence
|
|
472
|
+
# used to compute its current cost. Closing a different block on a successor
|
|
473
|
+
# tick must carry that earlier evidence into the journal, even after this
|
|
474
|
+
# binary learns a new model card. A pre-existing open projection that was
|
|
475
|
+
# rebuilt without the marker remains unproven until a new tick recomputes it.
|
|
476
|
+
STATS_INDEX_EPOCH = 1016
|
|
472
477
|
LEGACY_STATS_HEAD = 13
|
|
473
478
|
|
|
474
479
|
#: #496 S1 F1. A NEW branch, for a state that cannot occur before the
|
|
@@ -2506,6 +2511,7 @@ def open_db(*, _target_path=None) -> sqlite3.Connection:
|
|
|
2506
2511
|
total_cache_create_tokens INTEGER NOT NULL DEFAULT 0,
|
|
2507
2512
|
total_cache_read_tokens INTEGER NOT NULL DEFAULT 0,
|
|
2508
2513
|
total_cost_usd REAL NOT NULL DEFAULT 0,
|
|
2514
|
+
pricing_provenance_json TEXT,
|
|
2509
2515
|
is_closed INTEGER NOT NULL DEFAULT 0,
|
|
2510
2516
|
created_at_utc TEXT NOT NULL,
|
|
2511
2517
|
last_updated_at_utc TEXT NOT NULL,
|
|
@@ -2517,6 +2523,8 @@ def open_db(*, _target_path=None) -> sqlite3.Connection:
|
|
|
2517
2523
|
add_column_if_missing(
|
|
2518
2524
|
conn, "five_hour_blocks", "account_key",
|
|
2519
2525
|
"TEXT NOT NULL DEFAULT 'unattributed'")
|
|
2526
|
+
add_column_if_missing(
|
|
2527
|
+
conn, "five_hour_blocks", "pricing_provenance_json", "TEXT")
|
|
2520
2528
|
conn.execute(
|
|
2521
2529
|
"""
|
|
2522
2530
|
CREATE INDEX IF NOT EXISTS idx_five_hour_blocks_block_start
|
|
@@ -56,8 +56,8 @@ from _cctally_core import (
|
|
|
56
56
|
)
|
|
57
57
|
from _lib_dashboard_sources import (
|
|
58
58
|
SOURCE_SCHEMA_VERSION,
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
alerted_at_order_key as _alerted_at_order_key,
|
|
60
|
+
alerted_at_order_key_sql as _alerted_at_order_key_sql,
|
|
61
61
|
dashboard_resource_key as _dashboard_resource_key,
|
|
62
62
|
)
|
|
63
63
|
from _lib_display_tz import _compute_display_block, format_display_dt
|
|
@@ -475,9 +475,10 @@ def _alert_account_resolver(conn: sqlite3.Connection):
|
|
|
475
475
|
# and a row excluded there is unrecoverable. Two spellings of one instant
|
|
476
476
|
# compare equal here; a nonzero offset — which the ``all-combined`` fixture
|
|
477
477
|
# stores deliberately — orders by its true instant rather than by its
|
|
478
|
-
# local-time text.
|
|
479
|
-
|
|
480
|
-
|
|
478
|
+
# local-time text. #869 F14: the key carries the full sub-second instant, so a
|
|
479
|
+
# Codex crossing fired later in the same second as a Claude alert sorts newer.
|
|
480
|
+
_CANON_ALERTED_AT = _alerted_at_order_key_sql()
|
|
481
|
+
_CANON_ALERTED_AT_M = _alerted_at_order_key_sql("m.alerted_at")
|
|
481
482
|
|
|
482
483
|
|
|
483
484
|
def _envelope_rows_weekly(
|
|
@@ -1169,7 +1170,7 @@ def _build_alerts_envelope_array(
|
|
|
1169
1170
|
# `_combined_alert_rows` does — otherwise the operator sees an empty
|
|
1170
1171
|
# panel and a message that identifies nothing.
|
|
1171
1172
|
try:
|
|
1172
|
-
return
|
|
1173
|
+
return _alerted_at_order_key(alert["alerted_at"])
|
|
1173
1174
|
except (KeyError, ValueError) as exc:
|
|
1174
1175
|
identity = alert.get("key") or alert.get("id")
|
|
1175
1176
|
raise ValueError(
|
|
@@ -55,15 +55,17 @@ from _lib_dashboard_sources import (
|
|
|
55
55
|
SourceDashboardState,
|
|
56
56
|
SourceDashboardWarning,
|
|
57
57
|
assess_codex_projection_coherence,
|
|
58
|
+
alerted_at_order_key,
|
|
59
|
+
alerted_at_order_key_sql,
|
|
58
60
|
build_metadata_health,
|
|
59
61
|
canonical_alerted_at,
|
|
60
|
-
canonical_alerted_at_sql,
|
|
61
62
|
claude_stats_digest,
|
|
62
63
|
combined_accounting_version,
|
|
63
64
|
codex_stats_digest,
|
|
64
65
|
dashboard_resource_key,
|
|
65
66
|
)
|
|
66
67
|
from _lib_quota import (
|
|
68
|
+
FUTURE_CLOCK_SKEW_SECONDS,
|
|
67
69
|
QuotaWindowIdentity,
|
|
68
70
|
build_blocks,
|
|
69
71
|
build_history,
|
|
@@ -627,26 +629,12 @@ def _codex_quota_reuse_identity(
|
|
|
627
629
|
"stats_identity must be the three-digest tuple or None")
|
|
628
630
|
elif not isinstance(stats_conn, sqlite3.Connection):
|
|
629
631
|
return None
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
"SELECT 1 FROM sqlite_master WHERE type='table' "
|
|
637
|
-
"AND name='quota_window_change_log'"
|
|
638
|
-
).fetchone()
|
|
639
|
-
revision_row = cache_conn.execute(
|
|
640
|
-
"SELECT value FROM cache_meta "
|
|
641
|
-
"WHERE key='codex_window_attribution_revision'"
|
|
642
|
-
).fetchone()
|
|
643
|
-
db_path = next(
|
|
644
|
-
str(row[2]) for row in cache_conn.execute("PRAGMA database_list")
|
|
645
|
-
if str(row[1]) == "main"
|
|
646
|
-
)
|
|
647
|
-
except (sqlite3.Error, StopIteration):
|
|
648
|
-
return None
|
|
649
|
-
if table is None:
|
|
632
|
+
# #857: the cache.db legs are read through the snapshot-cache kernel, which
|
|
633
|
+
# states each leg's query once and shares it with the doctor's quota probe
|
|
634
|
+
# and the dashboard's quota-dependency identity.
|
|
635
|
+
db_path = _lib_snapshot_cache.codex_main_database_path(cache_conn)
|
|
636
|
+
legs = _lib_snapshot_cache.codex_quota_observation_legs(cache_conn)
|
|
637
|
+
if db_path is None or legs is None:
|
|
650
638
|
return None
|
|
651
639
|
if stats_identity is None:
|
|
652
640
|
try:
|
|
@@ -660,12 +648,7 @@ def _codex_quota_reuse_identity(
|
|
|
660
648
|
return None
|
|
661
649
|
except sqlite3.Error:
|
|
662
650
|
return None
|
|
663
|
-
return (
|
|
664
|
-
db_path,
|
|
665
|
-
0 if seq_row is None else int(seq_row[0]),
|
|
666
|
-
"" if revision_row is None else str(revision_row[0]),
|
|
667
|
-
*stats_identity,
|
|
668
|
-
)
|
|
651
|
+
return (db_path, *legs, *stats_identity)
|
|
669
652
|
|
|
670
653
|
|
|
671
654
|
def _cached_codex_quota_observations(**kwargs) -> tuple[object, ...]:
|
|
@@ -1190,51 +1173,82 @@ def _codex_next_decision_at(
|
|
|
1190
1173
|
crossing. That is one rebuild per deadline (a handful per weekly cycle), not
|
|
1191
1174
|
one per tick.
|
|
1192
1175
|
|
|
1193
|
-
The deadline is the ``min`` of
|
|
1194
|
-
at or before ``now_utc
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1176
|
+
The deadline is the ``min`` of every candidate below, dropping any
|
|
1177
|
+
candidate at or before ``now_utc``. #857 made the set COMPLETE, because a
|
|
1178
|
+
settled cycle-unavailable generation now idles until this instant: a
|
|
1179
|
+
transition with no candidate is never picked up at all, and one whose
|
|
1180
|
+
candidate fires early is lost too — the rebuild at the early instant drops
|
|
1181
|
+
the candidate (it is no longer after ``now``) while the transition it stood
|
|
1182
|
+
for is still ahead. Each candidate is therefore the FIRST instant its
|
|
1183
|
+
transition is observable, under the resolver's own comparison:
|
|
1184
|
+
|
|
1185
|
+
1. every weekly baseline's ``canonical_resets_at`` — selected or not, since
|
|
1186
|
+
an UNSELECTED boundary expiring is what resolves a ``conflicting``
|
|
1187
|
+
account (the resolver drops ``resets_at <= now``, so the reset instant
|
|
1188
|
+
itself is the first expired one). Every selected cycle's ``resets_at`` is
|
|
1189
|
+
one of these; they stay listed explicitly for the #341 multi-account case
|
|
1190
|
+
where account A expires while B stays live;
|
|
1191
|
+
2. every weekly baseline's nominal start plus one microsecond: the resolver
|
|
1192
|
+
rejects ``start >= now`` (#599), so a window captured ahead of its start
|
|
1193
|
+
becomes eligible strictly after it, on frozen evidence;
|
|
1194
|
+
3. for every weekly history with a baseline that has not reset: the
|
|
1195
|
+
fresh -> stale flip of its latest physical capture, which
|
|
1196
|
+
``quota_freshness`` takes when the whole-second age EXCEEDS
|
|
1197
|
+
``stale_after_seconds`` — ``capture + stale_after + 1s``, not
|
|
1198
|
+
``capture + stale_after`` — and the future -> fresh flip, when the
|
|
1199
|
+
truncated age stops being below ``-FUTURE_CLOCK_SKEW_SECONDS`` —
|
|
1200
|
+
strictly after ``capture - (skew + 1)s``. This is a deliberate SUPERSET
|
|
1201
|
+
of the §3.2 ranking participants, because an extra candidate can only
|
|
1202
|
+
pull the deadline EARLIER and costs one rebuild at an instant where the
|
|
1203
|
+
verdict could have changed;
|
|
1204
|
+
4. the ``captured_at`` of any future-dated weekly observation (baseline
|
|
1205
|
+
eligibility, ``captured_at <= now``). Recorded BEFORE the model-scoped
|
|
1206
|
+
skip: the pool label is read from the baseline and ``limit_name`` is not
|
|
1207
|
+
part of history identity, so a future capture can move a history out of
|
|
1208
|
+
a model-scoped pool.
|
|
1206
1209
|
|
|
1207
1210
|
Returns ``None`` when nothing can flip. Server-only: it rides ``clock_data``
|
|
1208
1211
|
and never reaches the public source envelope.
|
|
1209
1212
|
"""
|
|
1213
|
+
one_microsecond = dt.timedelta(microseconds=1)
|
|
1210
1214
|
candidates: list[dt.datetime] = []
|
|
1211
1215
|
for cycle in cycles:
|
|
1212
1216
|
candidates.append(cycle.resets_at.astimezone(UTC))
|
|
1213
1217
|
for history in build_history(tuple(observations)):
|
|
1214
1218
|
if history.identity.window_minutes != 10_080:
|
|
1215
1219
|
continue
|
|
1220
|
+
# 4. A capture ahead of ``now`` is not baseline-eligible yet, so this
|
|
1221
|
+
# runs even for a history with no live baseline at all.
|
|
1222
|
+
for observation in (
|
|
1223
|
+
*history.observations, *history.physical_observations,
|
|
1224
|
+
):
|
|
1225
|
+
captured_at = observation.captured_at.astimezone(UTC)
|
|
1226
|
+
if captured_at > now_utc:
|
|
1227
|
+
candidates.append(captured_at)
|
|
1216
1228
|
baseline = select_baseline(history.observations, now_utc)
|
|
1217
1229
|
if codex_history_is_model_scoped(history, baseline=baseline):
|
|
1218
1230
|
continue
|
|
1231
|
+
if baseline is None:
|
|
1232
|
+
continue
|
|
1219
1233
|
# #428: the anchor, so the decision deadline describes the same window
|
|
1220
1234
|
# the hero publishes rather than a jitter sibling up to 600s away.
|
|
1221
|
-
|
|
1235
|
+
resets_at = baseline.canonical_resets_at.astimezone(UTC)
|
|
1236
|
+
window = dt.timedelta(minutes=history.identity.window_minutes)
|
|
1237
|
+
candidates.append(resets_at) # 1.
|
|
1238
|
+
candidates.append(resets_at - window + one_microsecond) # 2.
|
|
1239
|
+
if resets_at > now_utc: # 3.
|
|
1222
1240
|
latest = latest_physical_observation(history.physical_observations)
|
|
1223
1241
|
if latest is not None:
|
|
1242
|
+
latest_at = latest.captured_at.astimezone(UTC)
|
|
1243
|
+
candidates.append(latest_at + dt.timedelta(
|
|
1244
|
+
seconds=stale_after_seconds(
|
|
1245
|
+
history.identity.window_minutes) + 1,
|
|
1246
|
+
))
|
|
1224
1247
|
candidates.append(
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
)
|
|
1248
|
+
latest_at
|
|
1249
|
+
- dt.timedelta(seconds=FUTURE_CLOCK_SKEW_SECONDS + 1)
|
|
1250
|
+
+ one_microsecond
|
|
1229
1251
|
)
|
|
1230
|
-
# A capture ahead of ``now`` is not baseline-eligible yet, so this runs
|
|
1231
|
-
# even for a history with no live baseline at all.
|
|
1232
|
-
for observation in (
|
|
1233
|
-
*history.observations, *history.physical_observations,
|
|
1234
|
-
):
|
|
1235
|
-
captured_at = observation.captured_at.astimezone(UTC)
|
|
1236
|
-
if captured_at > now_utc:
|
|
1237
|
-
candidates.append(captured_at)
|
|
1238
1252
|
live = [candidate for candidate in candidates if candidate > now_utc]
|
|
1239
1253
|
return min(live) if live else None
|
|
1240
1254
|
|
|
@@ -4447,7 +4461,8 @@ def _alerts_wire(
|
|
|
4447
4461
|
# row that fired most recently but crossed longest ago was dropped at the
|
|
4448
4462
|
# LIMIT and never reached the panel at all. `created_at` stays as an
|
|
4449
4463
|
# equal-valued compatibility alias for a client reading a pre-v7 envelope.
|
|
4450
|
-
|
|
4464
|
+
# #869 F14: the full instant, so a same-second pair keeps its newer row.
|
|
4465
|
+
canon = alerted_at_order_key_sql()
|
|
4451
4466
|
|
|
4452
4467
|
def _instants(raw: object) -> dict[str, str]:
|
|
4453
4468
|
value = canonical_alerted_at(raw)
|
|
@@ -4506,7 +4521,7 @@ def _alerts_wire(
|
|
|
4506
4521
|
return ()
|
|
4507
4522
|
return tuple(sorted(
|
|
4508
4523
|
rows,
|
|
4509
|
-
key=lambda item:
|
|
4524
|
+
key=lambda item: alerted_at_order_key(item["alerted_at"]),
|
|
4510
4525
|
reverse=True,
|
|
4511
4526
|
)[:SOURCE_HISTORY_LIMIT])
|
|
4512
4527
|
|
|
@@ -7441,6 +7456,13 @@ class _CodexAccountingCapture:
|
|
|
7441
7456
|
#: safety came from the compare-and-swap rather than from the signature,
|
|
7442
7457
|
#: and carrying the value makes the signature answer for itself.
|
|
7443
7458
|
population_signature: tuple | None = None
|
|
7459
|
+
#: #857: the accounting provenance token THIS capture's accounting result
|
|
7460
|
+
#: returned — carried for the same reason as the signature above, because
|
|
7461
|
+
#: overlay publication or a concurrent capture can move the module state
|
|
7462
|
+
#: before the fold stamps it. ``None`` when the capture bypassed the
|
|
7463
|
+
#: qualified accounting read: such a generation names no population, and
|
|
7464
|
+
#: borrowing the process's current token would name one it never read.
|
|
7465
|
+
provenance_token: int | None = None
|
|
7444
7466
|
|
|
7445
7467
|
|
|
7446
7468
|
#: The detail routes' horizon: one year ending at the generation instant. The
|
|
@@ -7532,6 +7554,8 @@ def _capture_codex_accounting(
|
|
|
7532
7554
|
changed_old: tuple[object, ...] = ()
|
|
7533
7555
|
changed_new: tuple[object, ...] = ()
|
|
7534
7556
|
population_signature: tuple | None = None
|
|
7557
|
+
# #857: set only by a qualified read that returned; every bypass keeps it.
|
|
7558
|
+
provenance_token: int | None = None
|
|
7535
7559
|
|
|
7536
7560
|
def fallback_entries() -> tuple[object, ...]:
|
|
7537
7561
|
nonlocal metadata_read_failed, failed_legs
|
|
@@ -7600,6 +7624,7 @@ def _capture_codex_accounting(
|
|
|
7600
7624
|
changed_old = cached_accounting.changed_old
|
|
7601
7625
|
changed_new = cached_accounting.changed_new
|
|
7602
7626
|
population_signature = cached_accounting.population_signature
|
|
7627
|
+
provenance_token = cached_accounting.provenance_token
|
|
7603
7628
|
entries: tuple[object, ...] = qualified_entries
|
|
7604
7629
|
except QualifiedMetadataUnavailable as exc:
|
|
7605
7630
|
_lib_log.get_logger("dashboard").warning(
|
|
@@ -7644,6 +7669,8 @@ def _capture_codex_accounting(
|
|
|
7644
7669
|
failed_legs=failed_legs,
|
|
7645
7670
|
population_signature=(
|
|
7646
7671
|
None if metadata_incomplete else population_signature),
|
|
7672
|
+
provenance_token=(
|
|
7673
|
+
None if metadata_incomplete else provenance_token),
|
|
7647
7674
|
)
|
|
7648
7675
|
|
|
7649
7676
|
|
|
@@ -9253,6 +9280,14 @@ def _build_codex_source_state(
|
|
|
9253
9280
|
"codex_next_decision_at": _codex_next_decision_at(
|
|
9254
9281
|
quota_observations, cycles_all, context.now_utc,
|
|
9255
9282
|
),
|
|
9283
|
+
# #857: the accounting provenance token of the population THIS
|
|
9284
|
+
# build's capture read (`None` when it bypassed the qualified
|
|
9285
|
+
# read). A retained generation is admitted only while this equals
|
|
9286
|
+
# the token of the population the process last consumed, so a
|
|
9287
|
+
# build that consumed accounting and then failed to publish leaves
|
|
9288
|
+
# every older generation refused, whatever the failure site.
|
|
9289
|
+
_lib_snapshot_cache.CODEX_ACCOUNTING_PROVENANCE_KEY: (
|
|
9290
|
+
accounting_capture.provenance_token),
|
|
9256
9291
|
},
|
|
9257
9292
|
private_session_labels=private_session_labels,
|
|
9258
9293
|
# #819: the SAME reading that gated decoration above. Attaching it here
|
package/bin/_cctally_doctor.py
CHANGED
|
@@ -1032,31 +1032,17 @@ def _codex_quota_observation_signal() -> "tuple | None":
|
|
|
1032
1032
|
except sqlite3.Error:
|
|
1033
1033
|
return None
|
|
1034
1034
|
try:
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
).
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
seq_row = conn.execute(
|
|
1042
|
-
"SELECT seq FROM sqlite_sequence "
|
|
1043
|
-
"WHERE name='quota_window_change_log'"
|
|
1044
|
-
).fetchone()
|
|
1045
|
-
revision_row = conn.execute(
|
|
1046
|
-
"SELECT value FROM cache_meta "
|
|
1047
|
-
"WHERE key='codex_window_attribution_revision'"
|
|
1048
|
-
).fetchone()
|
|
1049
|
-
except sqlite3.Error:
|
|
1050
|
-
return None
|
|
1035
|
+
# #857: the two ledger legs are stated once, in the snapshot-cache
|
|
1036
|
+
# kernel, and shared with the dashboard's Codex quota-dependency
|
|
1037
|
+
# identity rather than restated here.
|
|
1038
|
+
legs = _cctally()._load_sibling(
|
|
1039
|
+
"_lib_snapshot_cache"
|
|
1040
|
+
).codex_quota_observation_legs(conn)
|
|
1051
1041
|
finally:
|
|
1052
1042
|
conn.close()
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
stat.st_ino,
|
|
1057
|
-
0 if seq_row is None else int(seq_row[0]),
|
|
1058
|
-
"" if revision_row is None else str(revision_row[0]),
|
|
1059
|
-
)
|
|
1043
|
+
if legs is None:
|
|
1044
|
+
return None
|
|
1045
|
+
return (str(path), stat.st_dev, stat.st_ino, *legs)
|
|
1060
1046
|
|
|
1061
1047
|
|
|
1062
1048
|
def _load_codex_quota_observations_for_doctor(*, force_cold: bool = False):
|
|
@@ -1079,6 +1065,67 @@ def _load_codex_quota_observations_for_doctor(*, force_cold: bool = False):
|
|
|
1079
1065
|
return loaded
|
|
1080
1066
|
|
|
1081
1067
|
|
|
1068
|
+
# The all-history project-metadata partition is the other large immutable input
|
|
1069
|
+
# in a warm dashboard Doctor gather. On the production-shaped #857 copy it
|
|
1070
|
+
# cost 422-432 ms per call while returning the same six counts every 30-second
|
|
1071
|
+
# Doctor refresh. Retain only that raw partition, never a Doctor verdict or a
|
|
1072
|
+
# clock-derived field, under the cache identity plus the existing physical
|
|
1073
|
+
# mutation sequence. Codex ingest advances that sequence in the same
|
|
1074
|
+
# transaction as token, thread, root, cursor, and metadata writes, which are
|
|
1075
|
+
# exactly the three-table inputs of ``load_codex_project_metadata_health``.
|
|
1076
|
+
# Replacement is covered separately by device/inode, and an absent or malformed
|
|
1077
|
+
# signal always takes the cold path. The path is the one the passed connection
|
|
1078
|
+
# opened, never the global ``CACHE_DB_PATH``: the sequence is read from that
|
|
1079
|
+
# connection, so the file identity must describe the same file (#857).
|
|
1080
|
+
_CODEX_PROJECT_METADATA_MEMO: "dict[tuple, object]" = {}
|
|
1081
|
+
|
|
1082
|
+
|
|
1083
|
+
def _codex_project_metadata_signal(
|
|
1084
|
+
cache_conn: sqlite3.Connection,
|
|
1085
|
+
) -> "tuple | None":
|
|
1086
|
+
try:
|
|
1087
|
+
path = next(
|
|
1088
|
+
str(row[2]) for row in cache_conn.execute("PRAGMA database_list")
|
|
1089
|
+
if len(row) > 2 and str(row[1]) == "main" and row[2]
|
|
1090
|
+
)
|
|
1091
|
+
stat = os.stat(path)
|
|
1092
|
+
row = cache_conn.execute(
|
|
1093
|
+
"SELECT value FROM cache_meta "
|
|
1094
|
+
"WHERE key='codex_physical_mutation_seq'"
|
|
1095
|
+
).fetchone()
|
|
1096
|
+
if row is None:
|
|
1097
|
+
return None
|
|
1098
|
+
sequence = int(row[0])
|
|
1099
|
+
if sequence < 0:
|
|
1100
|
+
return None
|
|
1101
|
+
except (OSError, StopIteration, sqlite3.Error, TypeError, ValueError):
|
|
1102
|
+
return None
|
|
1103
|
+
return (path, stat.st_dev, stat.st_ino, sequence)
|
|
1104
|
+
|
|
1105
|
+
|
|
1106
|
+
def _load_codex_project_metadata_health_for_doctor(
|
|
1107
|
+
cache_conn: sqlite3.Connection,
|
|
1108
|
+
*,
|
|
1109
|
+
force_cold: bool = False,
|
|
1110
|
+
):
|
|
1111
|
+
signal = (
|
|
1112
|
+
None if force_cold
|
|
1113
|
+
else _codex_project_metadata_signal(cache_conn)
|
|
1114
|
+
)
|
|
1115
|
+
if signal is not None:
|
|
1116
|
+
cached = _CODEX_PROJECT_METADATA_MEMO.get(signal)
|
|
1117
|
+
if cached is not None:
|
|
1118
|
+
return cached
|
|
1119
|
+
analytics = _cctally()._load_sibling("_cctally_source_analytics")
|
|
1120
|
+
loaded = analytics.load_codex_project_metadata_health(
|
|
1121
|
+
cache_conn=cache_conn,
|
|
1122
|
+
)
|
|
1123
|
+
if signal is not None:
|
|
1124
|
+
_CODEX_PROJECT_METADATA_MEMO.clear()
|
|
1125
|
+
_CODEX_PROJECT_METADATA_MEMO[signal] = loaded
|
|
1126
|
+
return loaded
|
|
1127
|
+
|
|
1128
|
+
|
|
1082
1129
|
_QUOTA_NO_CHANGE: dict = {
|
|
1083
1130
|
"active": False, "assessed": True, "effective_from": None,
|
|
1084
1131
|
"previous_units_per_point": None, "new_units_per_point": None,
|
|
@@ -1835,10 +1882,9 @@ def _doctor_gather_state_impl(
|
|
|
1835
1882
|
# connection. A failed probe is health evidence, not an
|
|
1836
1883
|
# empty corpus: the kernel renders it as a distinct FAIL.
|
|
1837
1884
|
try:
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
cache_conn=conn,
|
|
1885
|
+
health = _load_codex_project_metadata_health_for_doctor(
|
|
1886
|
+
conn,
|
|
1887
|
+
force_cold=_force_cold_inputs,
|
|
1842
1888
|
)
|
|
1843
1889
|
codex_project_metadata_health = {
|
|
1844
1890
|
"total_rows": health.total_rows,
|
package/bin/_cctally_forecast.py
CHANGED
|
@@ -3173,7 +3173,8 @@ def cmd_budget(args: argparse.Namespace) -> int:
|
|
|
3173
3173
|
|
|
3174
3174
|
def _codex_backstop_leg(ictx):
|
|
3175
3175
|
c.maybe_record_codex_budget_milestone(
|
|
3176
|
-
{}, conn=ictx.conn, alert_sink=ictx.pending_alerts
|
|
3176
|
+
{}, conn=ictx.conn, alert_sink=ictx.pending_alerts,
|
|
3177
|
+
journal_ctx=ictx)
|
|
3177
3178
|
if projected_on:
|
|
3178
3179
|
c.maybe_record_projected_alert(
|
|
3179
3180
|
{}, only_metrics={"codex_budget_usd"},
|