cctally 1.103.0 → 1.105.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 +94 -0
- package/README.md +6 -6
- package/bin/_cctally_alerts.py +104 -10
- package/bin/_cctally_cache.py +193 -71
- package/bin/_cctally_config.py +62 -2
- package/bin/_cctally_core.py +62 -1
- package/bin/_cctally_dashboard.py +694 -70
- package/bin/_cctally_dashboard_conversation.py +400 -6
- package/bin/_cctally_dashboard_envelope.py +336 -14
- package/bin/_cctally_dashboard_perf.py +93 -0
- package/bin/_cctally_dashboard_share.py +56 -25
- package/bin/_cctally_dashboard_sources.py +988 -94
- package/bin/_cctally_db.py +30 -3
- package/bin/_cctally_diagnosis_sources.py +699 -186
- package/bin/_cctally_doctor.py +77 -0
- package/bin/_cctally_forecast.py +932 -51
- package/bin/_cctally_journal.py +430 -24
- package/bin/_cctally_parser.py +66 -0
- package/bin/_cctally_project.py +535 -10
- package/bin/_cctally_quota.py +28 -0
- package/bin/_cctally_quota_calibration.py +146 -0
- package/bin/_cctally_quota_model.py +2142 -0
- package/bin/_cctally_record.py +204 -30
- package/bin/_cctally_share.py +16 -8
- package/bin/_cctally_statusline.py +34 -0
- package/bin/_cctally_tui.py +531 -115
- package/bin/_lib_codex_conversation_query.py +258 -12
- package/bin/_lib_codex_hooks.py +26 -0
- package/bin/_lib_conversation_query.py +145 -15
- package/bin/_lib_dashboard_json.py +105 -0
- package/bin/_lib_dashboard_settings_contract.py +2 -0
- package/bin/_lib_diagnosis.py +21 -2
- package/bin/_lib_doctor.py +215 -1
- package/bin/_lib_forecast.py +337 -43
- package/bin/_lib_ingest_frontier.py +889 -0
- package/bin/_lib_meter_rate_change.py +360 -0
- package/bin/_lib_perf.py +22 -0
- package/bin/_lib_pricing.py +30 -3
- package/bin/_lib_quota_calibration.py +311 -0
- package/bin/_lib_quota_copy.py +157 -0
- package/bin/_lib_quota_model.py +2520 -0
- package/bin/_lib_rate_change_delivery.py +119 -0
- package/bin/_lib_record.py +50 -0
- package/bin/_lib_rederive.py +10 -0
- package/bin/_lib_render.py +6 -0
- package/bin/_lib_retained_size.py +187 -0
- package/bin/_lib_share_templates.py +37 -5
- package/bin/_lib_snapshot_cache.py +504 -17
- package/bin/_lib_statusline.py +200 -2
- package/bin/_lib_tick_stats.py +28 -4
- package/bin/_lib_view_models.py +30 -12
- package/bin/cctally +32 -0
- package/dashboard/static/assets/ConversationsView-DA4j7Yso.js +72 -0
- package/dashboard/static/assets/DoctorModal-D4PbVnbE.js +1 -0
- package/dashboard/static/assets/ModalRoot-D4oSPxPp.js +1 -0
- package/dashboard/static/assets/ProjectsDrillPanel-DkPlB9JH.js +1 -0
- package/dashboard/static/assets/SourceDetailModal-BE7FONsS.js +1 -0
- package/dashboard/static/assets/UpdateModal-BJuI8glf.js +7 -0
- package/dashboard/static/assets/index-klO46NcU.css +1 -0
- package/dashboard/static/assets/index-w_bINkJ8.js +13 -0
- package/dashboard/static/assets/outlineNavigation-zDVm6Hdd.js +9 -0
- package/dashboard/static/assets/useKeymap-CJ-Pi17D.js +1 -0
- package/dashboard/static/dashboard.html +3 -2
- package/package.json +10 -1
- package/dashboard/static/assets/index-Di2hljvB.css +0 -1
- package/dashboard/static/assets/index-XYCIWjVG.js +0 -97
|
@@ -824,6 +824,68 @@ def _source_bundle_to_envelope(bundle: object | None) -> dict:
|
|
|
824
824
|
}
|
|
825
825
|
|
|
826
826
|
|
|
827
|
+
#: How many rate-change events the envelope publishes. A metering-rate
|
|
828
|
+
#: transition is rare — the maintainer's store holds one — so this is a
|
|
829
|
+
#: bound rather than a page size, and a store holding more than a handful
|
|
830
|
+
#: describes a detector that is misfiring rather than a busy account.
|
|
831
|
+
METER_RATE_CHANGE_LIMIT = 20
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
def _build_meter_rate_change_array(
|
|
835
|
+
conn: sqlite3.Connection, limit: int = METER_RATE_CHANGE_LIMIT,
|
|
836
|
+
) -> list:
|
|
837
|
+
"""The #661 S2 §6.1 non-threshold event family's wire rows.
|
|
838
|
+
|
|
839
|
+
A SEPARATE array from `alerts`, and deliberately so. Every member of
|
|
840
|
+
`AXIS_REGISTRY` is a numeric threshold axis: `AlertEntry` requires a
|
|
841
|
+
numeric `threshold`, its `id` is threshold-shaped, and `alert_row_owner`
|
|
842
|
+
raises on a seventh axis so that adding one without deciding its
|
|
843
|
+
ownership fails a test rather than shipping an invisible row. A rate
|
|
844
|
+
transition has neither a threshold nor a threshold-derived severity, so
|
|
845
|
+
it gets its own discriminated variant instead of being forced into that
|
|
846
|
+
union.
|
|
847
|
+
|
|
848
|
+
Each row carries the explicit severity, the old and new rates, the
|
|
849
|
+
effective instant, its OWNERSHIP (which provider tab renders it) and its
|
|
850
|
+
SCOPE (the account, decorated under R8 exactly as the alert rows are).
|
|
851
|
+
"""
|
|
852
|
+
account_fields = _alert_account_resolver(conn)
|
|
853
|
+
try:
|
|
854
|
+
rows = conn.execute(
|
|
855
|
+
"SELECT provider, account_key, effective_from,"
|
|
856
|
+
" previous_units_per_point, new_units_per_point, severity,"
|
|
857
|
+
" detected_at_utc, created_at_utc"
|
|
858
|
+
" FROM meter_rate_change_events"
|
|
859
|
+
" ORDER BY unixepoch(effective_from) DESC, id DESC"
|
|
860
|
+
" LIMIT ?", (int(limit),)).fetchall()
|
|
861
|
+
except sqlite3.Error:
|
|
862
|
+
# A store predating epoch 1011 has no such table, and the rebuild
|
|
863
|
+
# that creates it is deferred to a background worker. An empty array
|
|
864
|
+
# renders as "no change recorded", which is the truth on that store.
|
|
865
|
+
return []
|
|
866
|
+
out: list[dict] = []
|
|
867
|
+
for (provider, account_key, effective_from, previous, new, severity,
|
|
868
|
+
detected_at, created_at) in rows:
|
|
869
|
+
entry = {
|
|
870
|
+
# Opaque React key. It is never parsed — the same contract the
|
|
871
|
+
# alert `id` carries.
|
|
872
|
+
"id": f"meter_rate_change:{provider}:{account_key}:{effective_from}",
|
|
873
|
+
"family": "meter_rate_change",
|
|
874
|
+
"provider": str(provider),
|
|
875
|
+
"owner": str(provider),
|
|
876
|
+
"severity": str(severity or "info"),
|
|
877
|
+
"effective_from": str(effective_from),
|
|
878
|
+
"detected_at": str(detected_at or created_at or ""),
|
|
879
|
+
"recorded_at": str(created_at or ""),
|
|
880
|
+
"previous_units_per_point": (
|
|
881
|
+
None if previous is None else float(previous)),
|
|
882
|
+
"new_units_per_point": None if new is None else float(new),
|
|
883
|
+
}
|
|
884
|
+
entry.update(account_fields(str(provider), account_key))
|
|
885
|
+
out.append(entry)
|
|
886
|
+
return out
|
|
887
|
+
|
|
888
|
+
|
|
827
889
|
def _build_alerts_envelope_array(
|
|
828
890
|
conn: sqlite3.Connection,
|
|
829
891
|
limit: int = 100,
|
|
@@ -1103,6 +1165,239 @@ def _sync_activity_envelope(activity: "dict | None") -> dict:
|
|
|
1103
1165
|
}
|
|
1104
1166
|
|
|
1105
1167
|
|
|
1168
|
+
#: How far two subscription-week anchors may drift and still count as the
|
|
1169
|
+
#: same reset domain. The week anchor is normalized to the hour to absorb
|
|
1170
|
+
#: Anthropic's reset jitter, so two hours is comfortably wider than the
|
|
1171
|
+
#: jitter and far narrower than the shortest interval a synthesized
|
|
1172
|
+
#: mid-week row could produce.
|
|
1173
|
+
_RESET_DOMAIN_TOLERANCE_SECONDS = 2 * 3600
|
|
1174
|
+
_SUBSCRIPTION_WEEK_SECONDS = 7 * 24 * 3600
|
|
1175
|
+
|
|
1176
|
+
|
|
1177
|
+
def _same_reset_domain(current_start, prior_start) -> bool:
|
|
1178
|
+
"""Whether two week anchors belong to the same reset domain.
|
|
1179
|
+
|
|
1180
|
+
Spec §10.1 requires the prior operand to share the current row's reset
|
|
1181
|
+
domain. The rows carry no domain field, so it is derived from the one
|
|
1182
|
+
thing they always carry: two anchors are in the same domain when they
|
|
1183
|
+
are a whole number of subscription weeks apart. A row synthesized by
|
|
1184
|
+
reset or credit handling sits at a fractional offset and is skipped
|
|
1185
|
+
rather than silently used, which is exactly what that clause is for.
|
|
1186
|
+
"""
|
|
1187
|
+
if current_start is None or prior_start is None:
|
|
1188
|
+
return False
|
|
1189
|
+
delta = (current_start - prior_start).total_seconds()
|
|
1190
|
+
if delta <= 0:
|
|
1191
|
+
return False
|
|
1192
|
+
offset = delta % _SUBSCRIPTION_WEEK_SECONDS
|
|
1193
|
+
return min(offset, _SUBSCRIPTION_WEEK_SECONDS - offset) <= \
|
|
1194
|
+
_RESET_DOMAIN_TOLERANCE_SECONDS
|
|
1195
|
+
|
|
1196
|
+
|
|
1197
|
+
def _cause_presentation(code):
|
|
1198
|
+
"""The §8 presentation fields for one cause, or None for no cause.
|
|
1199
|
+
|
|
1200
|
+
ADDITIVE beside the machine `code`, which is unchanged and stays the
|
|
1201
|
+
stable value a client keys on. A client that receives no presentation
|
|
1202
|
+
fields — a new tab against an older server, which `execvp` makes
|
|
1203
|
+
ordinary — derives the short token from the code itself through the same
|
|
1204
|
+
named function this uses.
|
|
1205
|
+
"""
|
|
1206
|
+
if not code:
|
|
1207
|
+
return None
|
|
1208
|
+
try:
|
|
1209
|
+
copy = sys.modules["cctally"]._load_sibling("_lib_quota_copy")
|
|
1210
|
+
except Exception: # noqa: BLE001
|
|
1211
|
+
return None
|
|
1212
|
+
out = copy.presentation(code)
|
|
1213
|
+
return out or None
|
|
1214
|
+
|
|
1215
|
+
|
|
1216
|
+
def _basis_presentation(basis):
|
|
1217
|
+
"""The shared two-register projection-basis copy, or None.
|
|
1218
|
+
|
|
1219
|
+
Separate from `_cause_presentation`: a basis describes a present figure's
|
|
1220
|
+
provenance and never joins the withholding-cause vocabulary.
|
|
1221
|
+
"""
|
|
1222
|
+
if not basis:
|
|
1223
|
+
return None
|
|
1224
|
+
try:
|
|
1225
|
+
copy = sys.modules["cctally"]._load_sibling("_lib_quota_copy")
|
|
1226
|
+
except Exception: # noqa: BLE001
|
|
1227
|
+
return None
|
|
1228
|
+
out = copy.basis_presentation(basis)
|
|
1229
|
+
return out or None
|
|
1230
|
+
|
|
1231
|
+
|
|
1232
|
+
def _forecast_quota_envelope(fc, cw, rate_change):
|
|
1233
|
+
"""Spec §10's optional typed `forecast.quota` object.
|
|
1234
|
+
|
|
1235
|
+
Additive, and deliberately does NOT bump `source_schema_version`: that
|
|
1236
|
+
field describes the provider-source bundles, not this.
|
|
1237
|
+
|
|
1238
|
+
Every quantity here was already computed. The basis, the codes, the
|
|
1239
|
+
corrected interval and the calibrated triple come off the forecast the
|
|
1240
|
+
envelope is already serializing, and `rate_change` is the caller's cheap
|
|
1241
|
+
calibration-file read. Nothing here opens a store.
|
|
1242
|
+
|
|
1243
|
+
NO PRESENTATION LATCH: this is rebuilt from the current forecast on
|
|
1244
|
+
every refresh, so the surface switches with the shared selector rather
|
|
1245
|
+
than presenting a model whose evidence has since been withheld.
|
|
1246
|
+
"""
|
|
1247
|
+
if fc is None:
|
|
1248
|
+
return None
|
|
1249
|
+
inputs = getattr(fc, "inputs", None)
|
|
1250
|
+
interval = getattr(inputs, "p_now_interval", None)
|
|
1251
|
+
code = getattr(fc, "projection_code", None)
|
|
1252
|
+
calibration_code = getattr(inputs, "calibrated_withheld_code", None)
|
|
1253
|
+
consumption = getattr(inputs, "calibrated_consumption_pct", None)
|
|
1254
|
+
consumption_interval = getattr(
|
|
1255
|
+
inputs, "calibrated_consumption_interval", None)
|
|
1256
|
+
observed = getattr(cw, "used_pct", None) if cw is not None else None
|
|
1257
|
+
basis = getattr(fc, "projection_basis", None)
|
|
1258
|
+
projection = getattr(fc, "week_avg_projection_pct", None)
|
|
1259
|
+
headroom = getattr(inputs, "calibrated_headroom_pct", None)
|
|
1260
|
+
# The calibrated view is published as a UNIT (#661 S2 review, D2).
|
|
1261
|
+
#
|
|
1262
|
+
# `select_projection_basis` reads ONLY `calibrated_projection_pct`, so a
|
|
1263
|
+
# non-null projection beside a null modelled consumption would publish
|
|
1264
|
+
# `basis: "calibrated"` over a number the model does not state — a modal
|
|
1265
|
+
# reading "basis: model" above "modelled consumption: —". The invariant
|
|
1266
|
+
# held until now only because `_calibrated_week_detail` sets the whole
|
|
1267
|
+
# triple or none of it, which is the producer's discipline rather than a
|
|
1268
|
+
# property of this serializer.
|
|
1269
|
+
#
|
|
1270
|
+
# WITHHELD rather than demoted. Demoting to `corrected-meter` would
|
|
1271
|
+
# relabel a number the calibrated model produced, and the meter's own
|
|
1272
|
+
# projection is not on this forecast to substitute, so there is nothing
|
|
1273
|
+
# honest to publish. `withheld` is an already-rendered basis on the panel
|
|
1274
|
+
# and in the modal, and it carries a cause.
|
|
1275
|
+
# `projection is None` is included for completeness rather than because
|
|
1276
|
+
# it is reachable: `select_projection_basis` reads `calibrated_projection_pct`,
|
|
1277
|
+
# so a null projection does not yield this basis in the first place, and
|
|
1278
|
+
# `bin/_lib_forecast.py` nulls the projection only on its WITHHELD branch.
|
|
1279
|
+
# The argument that motivates the other two operands -- that the whole
|
|
1280
|
+
# triple is the producer's discipline and not a property of this
|
|
1281
|
+
# serializer -- applies to the projection identically, so leaving it out
|
|
1282
|
+
# would make the guard state a narrower invariant than the one it means.
|
|
1283
|
+
if basis == "calibrated" and (projection is None
|
|
1284
|
+
or consumption is None
|
|
1285
|
+
or headroom is None):
|
|
1286
|
+
basis = "withheld"
|
|
1287
|
+
projection = None
|
|
1288
|
+
consumption = None
|
|
1289
|
+
consumption_interval = None
|
|
1290
|
+
headroom = None
|
|
1291
|
+
code = code or "unavailable"
|
|
1292
|
+
calibration_code = calibration_code or "unavailable"
|
|
1293
|
+
return {
|
|
1294
|
+
"basis": basis,
|
|
1295
|
+
"basis_presentation": _basis_presentation(basis),
|
|
1296
|
+
"projection_pct": projection,
|
|
1297
|
+
"right_censored": bool(getattr(fc, "right_censored", False)),
|
|
1298
|
+
# The cause of a WITHHELD projection, and separately why the
|
|
1299
|
+
# CALIBRATED basis was not reached. They are different questions and
|
|
1300
|
+
# a surface that collapses them says nothing when it falls back.
|
|
1301
|
+
"code": code,
|
|
1302
|
+
"code_presentation": _cause_presentation(code),
|
|
1303
|
+
"calibration_code": calibration_code,
|
|
1304
|
+
"calibration_code_presentation":
|
|
1305
|
+
_cause_presentation(calibration_code),
|
|
1306
|
+
# The corrected-meter interval a displayed reading denotes. `hi` is
|
|
1307
|
+
# null at a right-censored 100, which is unbounded above.
|
|
1308
|
+
"corrected_interval":
|
|
1309
|
+
None if interval is None
|
|
1310
|
+
else {"lo": interval[0], "hi": interval[1]},
|
|
1311
|
+
"calibrated_consumption_pct": consumption,
|
|
1312
|
+
"calibrated_consumption_interval":
|
|
1313
|
+
None if not consumption_interval
|
|
1314
|
+
else {"lo": consumption_interval[0],
|
|
1315
|
+
"hi": consumption_interval[1]},
|
|
1316
|
+
"calibrated_headroom_pct": headroom,
|
|
1317
|
+
"rate_change": rate_change,
|
|
1318
|
+
# Spec §10: the observed meter minus the modelled local quota,
|
|
1319
|
+
# labelled as exactly that. It is a DIFFERENCE between two
|
|
1320
|
+
# quantities and does not identify or estimate usage from another
|
|
1321
|
+
# machine; the modal states that sentence, and it is null unless
|
|
1322
|
+
# both sides exist.
|
|
1323
|
+
"observed_minus_modelled_pct":
|
|
1324
|
+
None if (observed is None or consumption is None)
|
|
1325
|
+
else observed - consumption,
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
|
|
1329
|
+
def _header_rate_and_delta(current_rate, current_week_start, trend):
|
|
1330
|
+
"""`(dollar_per_pct, vs_last_week_delta)` from ONE current operand.
|
|
1331
|
+
|
|
1332
|
+
Spec §10.1, and the defect it closes: `header.dollar_per_pct` came from
|
|
1333
|
+
`_tui_build_current_week` while `header.vs_last_week_delta` came from the
|
|
1334
|
+
operand `build_trend_view` returned. Two computations, no guard, so the
|
|
1335
|
+
hero could publish a week-over-week comparison on a screen stating no
|
|
1336
|
+
`$ / 1%` at all.
|
|
1337
|
+
|
|
1338
|
+
Both values are computed here, from the header's own current operand,
|
|
1339
|
+
before serialization — so the client never reconciles two differently
|
|
1340
|
+
sourced "current" values. When the current rate is absent BOTH are null,
|
|
1341
|
+
because a delta measured from an absent operand is not a delta.
|
|
1342
|
+
|
|
1343
|
+
The prior operand is the NEAREST COMPARABLE completed row, and
|
|
1344
|
+
comparable is the exact list §10.1 gives. Provider and account scope are
|
|
1345
|
+
properties of the whole snapshot, so every row in `trend` shares them by
|
|
1346
|
+
construction; what has to be tested per row is the remaining three:
|
|
1347
|
+
|
|
1348
|
+
* a completed rather than current interval (`is_current` is False);
|
|
1349
|
+
* a non-null rate on that row; and
|
|
1350
|
+
* the same reset domain as the current week.
|
|
1351
|
+
|
|
1352
|
+
A row failing any of them is SKIPPED and the walk continues to the next
|
|
1353
|
+
older row, rather than the delta being abandoned or the row being used.
|
|
1354
|
+
|
|
1355
|
+
NEAREST IS BY ANCHOR, not by list position. The walk used to be
|
|
1356
|
+
`reversed(list(trend))`, which makes "nearest" mean "last in the list" and
|
|
1357
|
+
rests the whole clause on `snap.trend` arriving ascending. That holds for
|
|
1358
|
+
`build_trend_view` and for every committed fixture and was stated nowhere,
|
|
1359
|
+
and `_same_reset_domain` accepts any positive whole-week multiple, so a
|
|
1360
|
+
row twenty weeks back was as comparable as one week back. The candidates
|
|
1361
|
+
are therefore ordered here, newest anchor first. A row carrying no anchor
|
|
1362
|
+
sorts last and is skipped by the reset-domain test anyway.
|
|
1363
|
+
"""
|
|
1364
|
+
if current_rate is None:
|
|
1365
|
+
return None, None
|
|
1366
|
+
# Only rows whose anchor is a `datetime` are sorted; anything else keeps
|
|
1367
|
+
# list order after them. `sorted` compares the keys, so one naive anchor
|
|
1368
|
+
# beside an aware one -- or a `str` anchor, which the sibling row type at
|
|
1369
|
+
# `bin/_lib_view_models.py:174` carries -- would raise `TypeError` inside
|
|
1370
|
+
# the dashboard's serialization path and surface as a 500 rather than a
|
|
1371
|
+
# withheld figure. `build_trend_view` emits aware datetimes today, so the
|
|
1372
|
+
# partition is a guard against a future producer, not a live defect.
|
|
1373
|
+
# Ties keep list order, because `sorted` is stable: two rows sharing an
|
|
1374
|
+
# anchor resolve to the earlier one in `trend`.
|
|
1375
|
+
rows = list(trend or ())
|
|
1376
|
+
anchored = [r for r in rows
|
|
1377
|
+
if isinstance(getattr(r, "week_start_at", None), dt.datetime)]
|
|
1378
|
+
unanchored = [r for r in rows
|
|
1379
|
+
if not isinstance(getattr(r, "week_start_at", None),
|
|
1380
|
+
dt.datetime)]
|
|
1381
|
+
aware = [r for r in anchored if r.week_start_at.tzinfo is not None]
|
|
1382
|
+
naive = [r for r in anchored if r.week_start_at.tzinfo is None]
|
|
1383
|
+
candidates = (
|
|
1384
|
+
sorted(aware, key=lambda row: row.week_start_at, reverse=True)
|
|
1385
|
+
+ sorted(naive, key=lambda row: row.week_start_at, reverse=True)
|
|
1386
|
+
+ unanchored
|
|
1387
|
+
)
|
|
1388
|
+
for row in candidates:
|
|
1389
|
+
if getattr(row, "is_current", False):
|
|
1390
|
+
continue
|
|
1391
|
+
prior_rate = getattr(row, "dollars_per_percent", None)
|
|
1392
|
+
if prior_rate is None:
|
|
1393
|
+
continue
|
|
1394
|
+
if not _same_reset_domain(current_week_start,
|
|
1395
|
+
getattr(row, "week_start_at", None)):
|
|
1396
|
+
continue
|
|
1397
|
+
return current_rate, current_rate - prior_rate
|
|
1398
|
+
return current_rate, None
|
|
1399
|
+
|
|
1400
|
+
|
|
1106
1401
|
def snapshot_to_envelope(snap: "DataSnapshot", *,
|
|
1107
1402
|
now_utc: "dt.datetime",
|
|
1108
1403
|
monotonic_now: "float | None" = None,
|
|
@@ -1256,17 +1551,13 @@ def snapshot_to_envelope(snap: "DataSnapshot", *,
|
|
|
1256
1551
|
inputs = getattr(fc, "inputs", None)
|
|
1257
1552
|
if inputs is not None:
|
|
1258
1553
|
confidence = getattr(inputs, "confidence", None)
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
and r_recent is not None):
|
|
1267
|
-
p_final_recent = p_now + r_recent * rem_hrs
|
|
1268
|
-
if fcast_pct is None or p_final_recent != fcast_pct:
|
|
1269
|
-
recent_24h_pct = p_final_recent
|
|
1554
|
+
# The ONE decomposition (#661 S2 spec section 3.3), so this legacy
|
|
1555
|
+
# branch cannot answer the projection question differently from the
|
|
1556
|
+
# View path above it — which is exactly what the `over` golden
|
|
1557
|
+
# recorded before this call replaced the inline arithmetic.
|
|
1558
|
+
from _lib_view_models import _forecast_projection_pcts
|
|
1559
|
+
|
|
1560
|
+
fcast_pct, recent_24h_pct = _forecast_projection_pcts(fc)
|
|
1270
1561
|
if getattr(fc, "already_capped", False):
|
|
1271
1562
|
verdict = "capped"
|
|
1272
1563
|
elif getattr(fc, "projected_cap", False):
|
|
@@ -1474,6 +1765,11 @@ def snapshot_to_envelope(snap: "DataSnapshot", *,
|
|
|
1474
1765
|
# the entire snapshot — fall back to safe defaults and rely on
|
|
1475
1766
|
# `_warn_alerts_bad_config_once` for the user-visible signal.
|
|
1476
1767
|
alerts_array = list(getattr(snap, "alerts", []) or [])
|
|
1768
|
+
# #661 S2 §6.1. Precomputed at sync time beside `alerts`, so
|
|
1769
|
+
# `snapshot_to_envelope` stays a pure renderer with no DB I/O on the
|
|
1770
|
+
# dashboard hot path.
|
|
1771
|
+
meter_rate_changes_array = list(
|
|
1772
|
+
getattr(snap, "meter_rate_changes", []) or [])
|
|
1477
1773
|
# #268 M4: reuse the precompute's config (or the fallback load_config()
|
|
1478
1774
|
# resolved above) — the envelope used to call load_config() a SECOND time
|
|
1479
1775
|
# here. Within one tick both reads returned the same file, so this is
|
|
@@ -1551,6 +1847,11 @@ def snapshot_to_envelope(snap: "DataSnapshot", *,
|
|
|
1551
1847
|
# template itself).
|
|
1552
1848
|
"notifier": _alerts_cfg.get("notifier", "auto"),
|
|
1553
1849
|
"command_configured": _alerts_cfg.get("command_template") is not None,
|
|
1850
|
+
# #661 S2 §6.2: the metering-rate-change family's PUSH toggle. The
|
|
1851
|
+
# events themselves are recorded whatever this says, so the client
|
|
1852
|
+
# renders the history with no configuration; this mirror only tells
|
|
1853
|
+
# Settings whether the OS popup is armed.
|
|
1854
|
+
"rate_change_enabled": bool(_alerts_cfg.get("rate_change_enabled")),
|
|
1554
1855
|
}
|
|
1555
1856
|
# Dashboard render-prefs mirror (cache-failure-markers opt-out, spec §5).
|
|
1556
1857
|
# Reuses the single `_cfg_for_alerts = load_config()` read above (no extra
|
|
@@ -1666,6 +1967,15 @@ def snapshot_to_envelope(snap: "DataSnapshot", *,
|
|
|
1666
1967
|
(r for r in reversed(snap.trend) if r.is_current), None
|
|
1667
1968
|
) if snap.trend else None
|
|
1668
1969
|
|
|
1970
|
+
# #661 S2 §10.1. BOTH header operands are decided here, together, from
|
|
1971
|
+
# the header's own current rate. `_current_trend` is retained above
|
|
1972
|
+
# because other blocks read it, but its `delta_dpp` is no longer what
|
|
1973
|
+
# the header publishes: that field is computed against whichever row
|
|
1974
|
+
# happened to precede the current one in the trend series, which is a
|
|
1975
|
+
# different question from "the nearest comparable prior operand".
|
|
1976
|
+
dollar_pp, header_delta = _header_rate_and_delta(
|
|
1977
|
+
dollar_pp, week_start_at_utc, snap.trend)
|
|
1978
|
+
|
|
1669
1979
|
envelope = {
|
|
1670
1980
|
"envelope_version": 2,
|
|
1671
1981
|
# #278 Theme A: single additive first-paint hydration latch. True only
|
|
@@ -1718,9 +2028,7 @@ def snapshot_to_envelope(snap: "DataSnapshot", *,
|
|
|
1718
2028
|
"dollar_per_pct": dollar_pp,
|
|
1719
2029
|
"forecast_pct": header_fcast_pct,
|
|
1720
2030
|
"forecast_verdict": verdict,
|
|
1721
|
-
"vs_last_week_delta":
|
|
1722
|
-
_current_trend.delta_dpp if _current_trend is not None else None
|
|
1723
|
-
),
|
|
2031
|
+
"vs_last_week_delta": header_delta,
|
|
1724
2032
|
},
|
|
1725
2033
|
|
|
1726
2034
|
"current_week":
|
|
@@ -1804,6 +2112,12 @@ def snapshot_to_envelope(snap: "DataSnapshot", *,
|
|
|
1804
2112
|
else 2 if confidence == "low"
|
|
1805
2113
|
else 0),
|
|
1806
2114
|
"explain": sys.modules["cctally"]._build_forecast_json_payload(fc),
|
|
2115
|
+
# #661 S2 §10. Optional, typed, and ADDITIVE: it sits under
|
|
2116
|
+
# `forecast` because `ForecastEnvelope` is its semantic
|
|
2117
|
+
# boundary, and it does not bump `source_schema_version`,
|
|
2118
|
+
# which describes the provider-source bundles.
|
|
2119
|
+
"quota": _forecast_quota_envelope(
|
|
2120
|
+
fc, cw, getattr(snap, "quota_rate_change", None)),
|
|
1807
2121
|
},
|
|
1808
2122
|
|
|
1809
2123
|
"trend":
|
|
@@ -1921,6 +2235,14 @@ def snapshot_to_envelope(snap: "DataSnapshot", *,
|
|
|
1921
2235
|
"alerts": alerts_array,
|
|
1922
2236
|
"alerts_settings": alerts_settings,
|
|
1923
2237
|
|
|
2238
|
+
# #661 S2 §6.1: the non-threshold event family, in its OWN array
|
|
2239
|
+
# rather than inside `alerts`. `AlertEntry` requires a numeric
|
|
2240
|
+
# threshold and a rate transition has none, so mixing the two would
|
|
2241
|
+
# either weaken that contract or ship a row every consumer skips.
|
|
2242
|
+
# Additive optional — `envelope_version` stays at 2, and an older
|
|
2243
|
+
# client simply does not read it.
|
|
2244
|
+
"meter_rate_changes": meter_rate_changes_array,
|
|
2245
|
+
|
|
1924
2246
|
# Dashboard render-prefs mirror (cache-failure-markers opt-out, spec
|
|
1925
2247
|
# §5). Additive optional, like alerts_settings — envelope_version
|
|
1926
2248
|
# stays at 2. The client derives `markersEnabled` from this, defaulting
|
|
@@ -47,6 +47,7 @@ _TRACE_PATH = "/api/debug/backend/trace"
|
|
|
47
47
|
_REPORTED_REGIMES = ("active", "idle")
|
|
48
48
|
|
|
49
49
|
_REGIME_LABELS = {"active": "Codex-active", "idle": "Codex-idle"}
|
|
50
|
+
COMBINED_BACKGROUND_CPU_CEILING = 0.75
|
|
50
51
|
|
|
51
52
|
|
|
52
53
|
def _cctally():
|
|
@@ -202,6 +203,36 @@ def _mean_or_none(values):
|
|
|
202
203
|
return int(sum(values) / len(values)) if values else None
|
|
203
204
|
|
|
204
205
|
|
|
206
|
+
def _cpu_duty(rows):
|
|
207
|
+
paired = [
|
|
208
|
+
(int(row.get("cpu_ns") or 0), int(row["period_ns"]))
|
|
209
|
+
for row in rows or ()
|
|
210
|
+
if row.get("cpu_ns") is not None
|
|
211
|
+
and row.get("period_ns") is not None
|
|
212
|
+
and int(row["period_ns"]) > 0
|
|
213
|
+
]
|
|
214
|
+
if not paired:
|
|
215
|
+
return None
|
|
216
|
+
return sum(cpu for cpu, _period in paired) / sum(
|
|
217
|
+
period for _cpu, period in paired)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def summarise_combined_background_duty(tick: dict) -> dict:
|
|
221
|
+
"""Conservative sum of each loop's same-span one-core duty."""
|
|
222
|
+
main = _cpu_duty(tick.get("records") or ())
|
|
223
|
+
conversations = _cpu_duty(tick.get("conversation_sync") or ())
|
|
224
|
+
combined = None if main is None or conversations is None else (
|
|
225
|
+
main + conversations)
|
|
226
|
+
return {
|
|
227
|
+
"main": main,
|
|
228
|
+
"conversation": conversations,
|
|
229
|
+
"combined": combined,
|
|
230
|
+
"ceiling": COMBINED_BACKGROUND_CPU_CEILING,
|
|
231
|
+
"withinCeiling": None if combined is None else (
|
|
232
|
+
combined <= COMBINED_BACKGROUND_CPU_CEILING),
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
|
|
205
236
|
def _median(values):
|
|
206
237
|
import statistics
|
|
207
238
|
|
|
@@ -265,6 +296,17 @@ def _render_conversation_sync(tick: dict) -> list:
|
|
|
265
296
|
statuses[key] = statuses.get(key, 0) + 1
|
|
266
297
|
detail = " · ".join(f"{k} {v}" for k, v in sorted(statuses.items()))
|
|
267
298
|
lines.append(f" {'status':<14} {detail}")
|
|
299
|
+
for provider in ("claude", "codex"):
|
|
300
|
+
modes: dict = {}
|
|
301
|
+
files = 0
|
|
302
|
+
for record in rows:
|
|
303
|
+
mode = record.get(f"{provider}_mode") or "not_observed"
|
|
304
|
+
modes[mode] = modes.get(mode, 0) + 1
|
|
305
|
+
files += max(0, int(record.get(f"{provider}_files") or 0))
|
|
306
|
+
mode_detail = " · ".join(
|
|
307
|
+
f"{mode} {count}" for mode, count in sorted(modes.items()))
|
|
308
|
+
lines.append(
|
|
309
|
+
f" {provider:<14} {mode_detail} · files {files}")
|
|
268
310
|
return lines
|
|
269
311
|
|
|
270
312
|
|
|
@@ -316,6 +358,23 @@ def render_dashboard_perf(payload: dict) -> str:
|
|
|
316
358
|
f"over {len(builder)} tick(s)")
|
|
317
359
|
lines.append(
|
|
318
360
|
f" whole tick mean {_millis(_mean_or_none(duration))}")
|
|
361
|
+
cpu = [int(r.get("cpu_ns") or 0) for r in records]
|
|
362
|
+
lines.append(
|
|
363
|
+
f" thread cpu mean {_millis(_mean_or_none(cpu))}")
|
|
364
|
+
paired_cpu = [
|
|
365
|
+
(int(r.get("cpu_ns") or 0), int(r["period_ns"]))
|
|
366
|
+
for r in records
|
|
367
|
+
if r.get("cpu_ns") is not None
|
|
368
|
+
and r.get("period_ns") is not None
|
|
369
|
+
and int(r["period_ns"]) > 0
|
|
370
|
+
]
|
|
371
|
+
if paired_cpu:
|
|
372
|
+
duty = sum(value for value, _period in paired_cpu) / sum(
|
|
373
|
+
period for _value, period in paired_cpu
|
|
374
|
+
)
|
|
375
|
+
lines.append(f" cpu duty {duty * 100:.1f}% of one core")
|
|
376
|
+
else:
|
|
377
|
+
lines.append(" cpu duty no samples yet")
|
|
319
378
|
# #583 S5 §2.4: the cache.db read pin, measured at its own BEGIN and
|
|
320
379
|
# ROLLBACK boundaries. Reported separately from `builder` because it
|
|
321
380
|
# is a SUBSET of builder time rather than a third exclusive half, and
|
|
@@ -331,6 +390,7 @@ def render_dashboard_perf(payload: dict) -> str:
|
|
|
331
390
|
f"{'/cold' if newest.get('cold') else '/warm'} "
|
|
332
391
|
f"ingest {_millis(newest.get('ingest_ns'))} "
|
|
333
392
|
f"builder {_millis(newest.get('builder_ns'))} "
|
|
393
|
+
f"cpu {_millis(newest.get('cpu_ns'))} "
|
|
334
394
|
f"pin {_millis(newest.get('cache_pin_ns', 0) or 0)} "
|
|
335
395
|
f"total {_millis(newest.get('duration_ns'))}")
|
|
336
396
|
else:
|
|
@@ -342,6 +402,15 @@ def render_dashboard_perf(payload: dict) -> str:
|
|
|
342
402
|
f"total {_millis(standalone.get('duration_ns'))} "
|
|
343
403
|
f"(the last build made outside a refresh tick)")
|
|
344
404
|
lines.extend(_render_conversation_sync(tick))
|
|
405
|
+
combined = summarise_combined_background_duty(tick)
|
|
406
|
+
lines.extend(["", "Combined background work"])
|
|
407
|
+
if combined["combined"] is None:
|
|
408
|
+
lines.append(" duty not jointly sampled")
|
|
409
|
+
else:
|
|
410
|
+
verdict = "within" if combined["withinCeiling"] else "over"
|
|
411
|
+
lines.append(
|
|
412
|
+
f" duty {combined['combined'] * 100:.1f}% of one core · "
|
|
413
|
+
f"ceiling {combined['ceiling'] * 100:.1f}% · {verdict}")
|
|
345
414
|
lines.append("")
|
|
346
415
|
|
|
347
416
|
counts = tick.get("dispatch_counts") or {}
|
|
@@ -359,6 +428,24 @@ def render_dashboard_perf(payload: dict) -> str:
|
|
|
359
428
|
else:
|
|
360
429
|
lines.append("Group A cache-open failures none")
|
|
361
430
|
|
|
431
|
+
memory = payload.get("memory") or {}
|
|
432
|
+
if memory:
|
|
433
|
+
lines.append("")
|
|
434
|
+
lines.append("Retained process memory")
|
|
435
|
+
lines.append(
|
|
436
|
+
" owned "
|
|
437
|
+
f"{int(memory.get('ownerEstimatedBytes', 0)) / 1048576:.1f} MiB "
|
|
438
|
+
f"of {int(memory.get('ownerCeilingBytes', 0)) / 1048576:.1f} MiB")
|
|
439
|
+
lines.append(
|
|
440
|
+
" process cap "
|
|
441
|
+
f"{int(memory.get('processCeilingBytes', 0)) / 1048576:.0f} MiB")
|
|
442
|
+
for name, row in sorted((memory.get("owners") or {}).items()):
|
|
443
|
+
lines.append(
|
|
444
|
+
f" {name:<22} "
|
|
445
|
+
f"{int(row.get('estimatedBytes', 0)) / 1048576:.1f}/"
|
|
446
|
+
f"{int(row.get('maxBytes', 0)) / 1048576:.1f} MiB · "
|
|
447
|
+
f"entries {int(row.get('entryCount', 0))}")
|
|
448
|
+
|
|
362
449
|
applied = tracing.get("applied")
|
|
363
450
|
requested = tracing.get("requested")
|
|
364
451
|
applies_at = tracing.get("applies_at", "none")
|
|
@@ -374,6 +461,12 @@ def render_dashboard_perf(payload: dict) -> str:
|
|
|
374
461
|
else:
|
|
375
462
|
lines.append(" no phase tree stored — arm one with "
|
|
376
463
|
"`cctally dashboard-perf --trace on`")
|
|
464
|
+
if payload.get("ingest_phases") is not None:
|
|
465
|
+
ingest = payload["ingest_phases"]
|
|
466
|
+
lines.append(
|
|
467
|
+
f" ingest tree {ingest.get('elapsed_ms', 0):.1f}ms, generated at "
|
|
468
|
+
f"{payload.get('ingest_generated_at')}"
|
|
469
|
+
)
|
|
377
470
|
return "\n".join(lines) + "\n"
|
|
378
471
|
|
|
379
472
|
|