cctally 1.112.0 → 1.114.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 +27 -0
- package/README.md +4 -5
- package/bin/_cctally_cache.py +1683 -485
- package/bin/_cctally_core.py +223 -1
- package/bin/_cctally_dashboard.py +96 -18
- package/bin/_cctally_dashboard_perf.py +128 -9
- package/bin/_cctally_dashboard_sources.py +267 -204
- package/bin/_cctally_db.py +861 -18
- package/bin/_cctally_diagnosis_sources.py +22 -4
- package/bin/_cctally_doctor.py +136 -18
- package/bin/_cctally_five_hour.py +2 -2
- package/bin/_cctally_forecast.py +27 -6
- package/bin/_cctally_journal.py +67 -5
- package/bin/_cctally_milestone_history.py +82 -9
- package/bin/_cctally_percent_breakdown.py +3 -2
- package/bin/_cctally_quota.py +201 -33
- package/bin/_cctally_record.py +14 -0
- package/bin/_cctally_rederive.py +23 -9
- package/bin/_cctally_source_analytics.py +60 -15
- package/bin/_cctally_statusline.py +17 -0
- package/bin/_cctally_store.py +80 -5
- package/bin/_cctally_tui.py +4 -3
- package/bin/_cctally_weekrefs.py +128 -21
- package/bin/_lib_cache_report.py +35 -6
- package/bin/_lib_codex_conversation.py +32 -0
- package/bin/_lib_codex_conversation_query.py +164 -30
- package/bin/_lib_conversation_query.py +95 -56
- package/bin/_lib_conversation_retention.py +1605 -640
- package/bin/_lib_cost_provenance.py +53 -2
- package/bin/_lib_doctor.py +263 -69
- package/bin/_lib_fmt.py +11 -0
- package/bin/_lib_merge_gate.py +469 -51
- package/bin/_lib_perf.py +27 -1
- package/bin/_lib_pricing.py +209 -8
- package/bin/_lib_pricing_check.py +20 -4
- package/bin/_lib_reclaim_planner.py +792 -0
- package/bin/_lib_rederive.py +225 -77
- package/bin/_lib_snapshot_cache.py +21 -0
- package/bin/_lib_sqlite_close.py +81 -0
- package/bin/_lib_subscription_weeks.py +100 -17
- package/bin/_lib_tick_stats.py +217 -17
- package/bin/_lib_wal_checkpoint.py +1077 -0
- package/bin/_lib_write_budget.py +483 -0
- package/bin/_lib_write_io.py +703 -0
- package/bin/cctally +8 -0
- package/dashboard/static/assets/{ConversationsView-CEZ-BUyi.js → ConversationsView-Bc8LCOAZ.js} +1 -1
- package/dashboard/static/assets/{DoctorModal-CE1oH_Zy.js → DoctorModal-D--qVBwe.js} +1 -1
- package/dashboard/static/assets/ModalRoot-DLPgyAM_.js +1 -0
- package/dashboard/static/assets/{ProjectsDrillPanel-B3DZoCeD.js → ProjectsDrillPanel-z53a4v4M.js} +1 -1
- package/dashboard/static/assets/{SourceDetailModal-CHp9EgLP.js → SourceDetailModal-Bw0T8W-W.js} +1 -1
- package/dashboard/static/assets/{UpdateModal-5iKST6jo.js → UpdateModal-D5AKx4Yw.js} +1 -1
- package/dashboard/static/assets/index-BKITk1CT.css +1 -0
- package/dashboard/static/assets/{index-Cm0VTxJC.js → index-BXDOzSng.js} +3 -3
- package/dashboard/static/assets/{outlineNavigation-DkXGL8J6.js → outlineNavigation-JObY7cUY.js} +1 -1
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +6 -1
- package/dashboard/static/assets/ModalRoot-D5Y8IFUG.js +0 -1
- package/dashboard/static/assets/index-Dt4tqf43.css +0 -1
|
@@ -122,7 +122,8 @@ def claude_card_rate_violations(snapshot) -> list:
|
|
|
122
122
|
A closed amount of exactly $0 proves a missing card only while no Claude
|
|
123
123
|
card prices a used token class at zero. Base rates, any present long-
|
|
124
124
|
context tier, the derived 1-hour cache-write rate and any Fast multiplier
|
|
125
|
-
must all be positive; an empty list is that invariant.
|
|
125
|
+
must all be positive; an empty list is that invariant. A whole-request
|
|
126
|
+
(prompt-length) declaration must also be complete and well formed (#929).
|
|
126
127
|
"""
|
|
127
128
|
_lib_pricing = _pricing()
|
|
128
129
|
out = []
|
|
@@ -143,23 +144,73 @@ def claude_card_rate_violations(snapshot) -> list:
|
|
|
143
144
|
_positive_rate(multiplier) and _positive_rate(tier_input)
|
|
144
145
|
):
|
|
145
146
|
out.append((model, "cache_write_1h_above_200k_tokens"))
|
|
147
|
+
# Not the request hot path, so the unrecognised-field scan runs here
|
|
148
|
+
# too: a card carrying only such a field half-declares the tier (#929).
|
|
149
|
+
if (_lib_pricing._is_whole_request_card(card)
|
|
150
|
+
or _lib_pricing._unrecognised_whole_request_fields(card)):
|
|
151
|
+
out.extend(_whole_request_violations(
|
|
152
|
+
_lib_pricing, model, card, multiplier))
|
|
146
153
|
stripped = _lib_pricing._strip_anthropic_model_prefix(model)
|
|
147
154
|
if stripped in fast and not _positive_rate(fast[stripped]):
|
|
148
155
|
out.append((model, "fast_multiplier"))
|
|
149
156
|
return out
|
|
150
157
|
|
|
151
158
|
|
|
159
|
+
def _whole_request_violations(_lib_pricing, model, card, multiplier) -> list:
|
|
160
|
+
"""The whole-request (prompt-length) tier's share of the invariant (#929).
|
|
161
|
+
|
|
162
|
+
Every higher rate must be present and positive, the threshold marker must
|
|
163
|
+
be the integer the `_above_100k_tokens` fields name, the card must not mix
|
|
164
|
+
in a marginal `_above_200k_tokens` tier, the derived higher 1-hour write
|
|
165
|
+
rate (higher input x multiplier) must be positive, and the card must carry
|
|
166
|
+
no `_above_100k_tokens` field other than the four recognised ones (the
|
|
167
|
+
selector and `check_table_shapes` refuse such a card too).
|
|
168
|
+
"""
|
|
169
|
+
out = []
|
|
170
|
+
for _base, tier in _lib_pricing.CLAUDE_WHOLE_REQUEST_TIER_FIELDS:
|
|
171
|
+
if not _positive_rate(card.get(tier)):
|
|
172
|
+
out.append((model, tier))
|
|
173
|
+
key = _lib_pricing.CLAUDE_WHOLE_REQUEST_THRESHOLD_KEY
|
|
174
|
+
threshold = card.get(key)
|
|
175
|
+
if (key not in card or isinstance(threshold, bool)
|
|
176
|
+
or not isinstance(threshold, int)
|
|
177
|
+
or threshold != _lib_pricing.CLAUDE_WHOLE_REQUEST_THRESHOLD):
|
|
178
|
+
out.append((model, key))
|
|
179
|
+
if any(k.endswith("_above_200k_tokens") for k in card):
|
|
180
|
+
out.append((model, "whole_request_mixed_tiers"))
|
|
181
|
+
if not (_positive_rate(multiplier) and _positive_rate(
|
|
182
|
+
card.get("input_cost_per_token_above_100k_tokens"))):
|
|
183
|
+
out.append((model, "cache_write_1h_above_100k_tokens"))
|
|
184
|
+
for field in _lib_pricing._unrecognised_whole_request_fields(card):
|
|
185
|
+
out.append((model, field))
|
|
186
|
+
return out
|
|
187
|
+
|
|
188
|
+
|
|
152
189
|
def claude_entry_rates_positive(card, entry, snapshot) -> bool:
|
|
153
190
|
"""True iff every rate that prices one of ``entry``'s tokens is positive.
|
|
154
191
|
|
|
155
192
|
Mirrors `_lib_pricing._calculate_entry_cost` term by term: each token class
|
|
156
193
|
is priced below the long-context threshold at its base rate and above it at
|
|
157
194
|
a present tier rate, and a known 1-hour cache-write split prices its share
|
|
158
|
-
at the derived input-times-multiplier rate.
|
|
195
|
+
at the derived input-times-multiplier rate. A whole-request card (#929) is
|
|
196
|
+
first resolved to the card the entry's prompt selects.
|
|
159
197
|
"""
|
|
160
198
|
if card is None:
|
|
161
199
|
return False
|
|
162
200
|
_lib_pricing = _pricing()
|
|
201
|
+
# #929: a whole-request card prices this entry at the card its prompt
|
|
202
|
+
# selects, so judge THAT card; every check below then runs unchanged on
|
|
203
|
+
# it. A malformed declaration prices nothing reliably: refuse it.
|
|
204
|
+
if _lib_pricing._is_whole_request_card(card):
|
|
205
|
+
try:
|
|
206
|
+
card = _lib_pricing._select_claude_request_card(
|
|
207
|
+
card,
|
|
208
|
+
input_tokens=int(entry.input_tokens or 0),
|
|
209
|
+
cache_creation_tokens=int(entry.cache_creation_tokens or 0),
|
|
210
|
+
cache_read_tokens=int(entry.cache_read_tokens or 0),
|
|
211
|
+
)
|
|
212
|
+
except ValueError:
|
|
213
|
+
return False
|
|
163
214
|
threshold = snapshot.tier_thresholds["claude"]
|
|
164
215
|
|
|
165
216
|
def tiered(tokens, base_key, tier_key):
|
package/bin/_lib_doctor.py
CHANGED
|
@@ -441,6 +441,14 @@ class DoctorState:
|
|
|
441
441
|
# known file-level backup/sync root. Only provider/status are retained so
|
|
442
442
|
# reports never expose the machine-specific data path.
|
|
443
443
|
backup_sync_state: Optional[dict] = None
|
|
444
|
+
# #901: transcript-store page size, so `db.conversations_reclaimable`
|
|
445
|
+
# judges the paced-reclaim thresholds on observed bytes.
|
|
446
|
+
conversations_db_page_size: Optional[int] = None
|
|
447
|
+
# #901 §5.6: running dashboards' write telemetry, gathered read-only —
|
|
448
|
+
# the local instance's own snapshot in the dashboard process, or
|
|
449
|
+
# descriptor discovery plus one bounded loopback probe per live
|
|
450
|
+
# instance elsewhere. {"mode", "instances": [...]}; None = not gathered.
|
|
451
|
+
dashboard_disk_writes: Optional[dict] = None
|
|
444
452
|
|
|
445
453
|
|
|
446
454
|
@dataclasses.dataclass(frozen=True)
|
|
@@ -1011,7 +1019,7 @@ def _check_db_version_ahead(s: DoctorState) -> CheckResult:
|
|
|
1011
1019
|
# current index as a mismatch; keep this in lockstep with core
|
|
1012
1020
|
# (#496 S5b §6.1). It stays a literal because this kernel is pure and
|
|
1013
1021
|
# must not import `_cctally_core`.
|
|
1014
|
-
epoch =
|
|
1022
|
+
epoch = 1017
|
|
1015
1023
|
mismatch = uv > legacy_head and uv != epoch
|
|
1016
1024
|
return {"user_version": uv, "legacy_head": legacy_head, "epoch": epoch,
|
|
1017
1025
|
"mismatch": mismatch}
|
|
@@ -2916,92 +2924,119 @@ def _check_db_reclaimable(s: DoctorState) -> CheckResult:
|
|
|
2916
2924
|
|
|
2917
2925
|
|
|
2918
2926
|
def _check_db_conversations_reclaimable(s: DoctorState) -> CheckResult:
|
|
2919
|
-
"""
|
|
2927
|
+
"""Transcript free space against the paced-reclaim policy (#320, #780,
|
|
2928
|
+
#901). Read-only; judged on the OBSERVED freelist, never on a record.
|
|
2929
|
+
|
|
2930
|
+
OK for retained slack below the reclaim start threshold — reusable pages
|
|
2931
|
+
kept on purpose (operator decision Q2). WARN for an eligible backlog being
|
|
2932
|
+
paced, including one whose attempts are failing (`reclaim_failure`); a
|
|
2933
|
+
scheduled wait is not "stuck". FAIL at the hard ceiling, where transcript
|
|
2934
|
+
rebuilds are refused."""
|
|
2935
|
+
import _lib_conversation_retention as _conv_retention
|
|
2936
|
+
|
|
2920
2937
|
page_count = s.conversations_db_page_count
|
|
2921
2938
|
freelist_count = s.conversations_db_freelist_count
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
isinstance(
|
|
2925
|
-
|
|
2926
|
-
and page_count > 0
|
|
2927
|
-
and isinstance(freelist_count, int)
|
|
2928
|
-
and not isinstance(freelist_count, bool)
|
|
2939
|
+
page_size = s.conversations_db_page_size
|
|
2940
|
+
measured = (
|
|
2941
|
+
all(isinstance(v, int) and not isinstance(v, bool)
|
|
2942
|
+
for v in (page_count, freelist_count, page_size))
|
|
2943
|
+
and page_count > 0 and page_size > 0
|
|
2929
2944
|
and 0 <= freelist_count <= page_count
|
|
2930
|
-
)
|
|
2931
|
-
|
|
2945
|
+
)
|
|
2946
|
+
geometry = (
|
|
2947
|
+
_conv_retention.StoreGeometry(page_count, freelist_count, page_size)
|
|
2948
|
+
if measured else None)
|
|
2949
|
+
record = (s.conversations_reclaim_pending
|
|
2950
|
+
if isinstance(s.conversations_reclaim_pending, dict) else None)
|
|
2951
|
+
episode = bool(record and record.get("eligible") is True)
|
|
2952
|
+
failure = record.get("last_failure") if record else None
|
|
2953
|
+
if not isinstance(failure, dict):
|
|
2954
|
+
failure = None
|
|
2955
|
+
free_bytes = geometry.free_bytes if geometry is not None else None
|
|
2932
2956
|
details = {
|
|
2933
2957
|
"conversations_db_page_count": page_count,
|
|
2934
2958
|
"conversations_db_freelist_count": freelist_count,
|
|
2935
|
-
"
|
|
2936
|
-
"
|
|
2959
|
+
"conversations_db_page_size": page_size,
|
|
2960
|
+
"conversations_db_free_ratio": (
|
|
2961
|
+
freelist_count / page_count if geometry is not None else None),
|
|
2962
|
+
"reclaimable_bytes": free_bytes,
|
|
2963
|
+
"reclaim_start_bytes": _conv_retention.RECLAIM_START_BYTES,
|
|
2964
|
+
"reclaim_start_ratio": _conv_retention.RECLAIM_START_RATIO,
|
|
2965
|
+
"reclaim_stop_bytes": _conv_retention.RECLAIM_STOP_BYTES,
|
|
2966
|
+
"reclaim_stop_ratio": _conv_retention.RECLAIM_STOP_RATIO,
|
|
2967
|
+
"reclaim_ceiling_bytes": _conv_retention.RECLAIM_CEILING_BYTES,
|
|
2968
|
+
"reclaim_episode_active": episode if record is not None else None,
|
|
2969
|
+
"reclaim_failure": failure,
|
|
2937
2970
|
}
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
# — on a store with no pending record. That is every store that has never
|
|
2941
|
-
# fallen behind, so an install in the ordinary state renders exactly what
|
|
2942
|
-
# it rendered before.
|
|
2943
|
-
pending = s.conversations_reclaim_pending
|
|
2944
|
-
if pending:
|
|
2945
|
-
import _lib_conversation_retention as _conv_retention
|
|
2946
|
-
|
|
2947
|
-
backlog = _conv_retention.reclaim_backlog_bytes(pending)
|
|
2948
|
-
details["reclaim_pending"] = pending
|
|
2949
|
-
details["reclaim_backlog_bytes"] = backlog
|
|
2950
|
-
details["reclaim_ceiling_bytes"] = _conv_retention.RECLAIM_CEILING_BYTES
|
|
2951
|
-
if _conv_retention.reclaim_backlog_over_ceiling(pending):
|
|
2952
|
-
return CheckResult(
|
|
2953
|
-
id="db.conversations_reclaimable",
|
|
2954
|
-
title="Reclaimable transcript space",
|
|
2955
|
-
severity="fail",
|
|
2956
|
-
summary=(
|
|
2957
|
-
f"reclaim backlog {_gib_text(backlog)} is at or over the "
|
|
2958
|
-
f"{_gib_text(_conv_retention.RECLAIM_CEILING_BYTES)} "
|
|
2959
|
-
"ceiling; transcript rebuilds are refused"
|
|
2960
|
-
),
|
|
2961
|
-
remediation=(
|
|
2962
|
-
"Run `cctally db vacuum --db conversations` to drain the "
|
|
2963
|
-
"backlog, then `cctally db checkpoint --db conversations`."
|
|
2964
|
-
),
|
|
2965
|
-
details=details,
|
|
2966
|
-
)
|
|
2967
|
-
if _conv_retention.reclaim_backlog_escalated(pending):
|
|
2968
|
-
return CheckResult(
|
|
2969
|
-
id="db.conversations_reclaimable",
|
|
2970
|
-
title="Reclaimable transcript space",
|
|
2971
|
-
severity="warn",
|
|
2972
|
-
summary=(
|
|
2973
|
-
f"reclaim backlog {_gib_text(backlog)} is draining across "
|
|
2974
|
-
"continuation passes"
|
|
2975
|
-
),
|
|
2976
|
-
remediation=(
|
|
2977
|
-
"No action is required unless it keeps growing; "
|
|
2978
|
-
"`cctally db vacuum --db conversations` drains it now."
|
|
2979
|
-
),
|
|
2980
|
-
details=details,
|
|
2981
|
-
)
|
|
2982
|
-
if ratio is not None and ratio >= DOCTOR_RECLAIMABLE_WARN_RATIO:
|
|
2971
|
+
title = "Reclaimable transcript space"
|
|
2972
|
+
if _conv_retention.backlog_over_ceiling(free_bytes):
|
|
2983
2973
|
return CheckResult(
|
|
2984
|
-
id="db.conversations_reclaimable",
|
|
2985
|
-
title="Reclaimable transcript space",
|
|
2986
|
-
severity="warn",
|
|
2974
|
+
id="db.conversations_reclaimable", title=title, severity="fail",
|
|
2987
2975
|
summary=(
|
|
2988
|
-
f"
|
|
2976
|
+
f"reclaim backlog {_gib_text(free_bytes)} is at or over the "
|
|
2977
|
+
f"{_gib_text(_conv_retention.RECLAIM_CEILING_BYTES)} "
|
|
2978
|
+
"ceiling; transcript rebuilds are refused"
|
|
2989
2979
|
),
|
|
2990
2980
|
remediation=(
|
|
2991
|
-
"Run `cctally db vacuum --db conversations` to
|
|
2981
|
+
"Run `cctally db vacuum --db conversations` to drain the "
|
|
2982
|
+
"backlog, then `cctally db checkpoint --db conversations`."
|
|
2992
2983
|
),
|
|
2993
2984
|
details=details,
|
|
2994
2985
|
)
|
|
2986
|
+
if _conv_retention.reclaim_episode_active(geometry, episode):
|
|
2987
|
+
# Q9: a paced wait is not a refused attempt. A planner refusal is an
|
|
2988
|
+
# in-memory pass record of the running dashboard (it writes nothing
|
|
2989
|
+
# durable), read from `writeIo.reclaimRefusal`; a connection-settings
|
|
2990
|
+
# or SQLite failure stays the durable `reclaim_failure`, as before.
|
|
2991
|
+
refusal = _latest_reclaim_refusal(s.dashboard_disk_writes)
|
|
2992
|
+
wait = ("refused" if refusal is not None
|
|
2993
|
+
else "failing" if failure is not None else "paced")
|
|
2994
|
+
remediation = (
|
|
2995
|
+
"No action is required unless it keeps growing; "
|
|
2996
|
+
"`cctally db vacuum --db conversations` drains it now.")
|
|
2997
|
+
if refusal is not None:
|
|
2998
|
+
remediation = (
|
|
2999
|
+
"Reclaim attempts are being refused "
|
|
3000
|
+
f"({refusal['reason'].replace('_', ' ')}); "
|
|
3001
|
+
"`cctally db vacuum --db conversations` reclaims the space "
|
|
3002
|
+
"at once.")
|
|
3003
|
+
return CheckResult(
|
|
3004
|
+
id="db.conversations_reclaimable", title=title, severity="warn",
|
|
3005
|
+
summary=(
|
|
3006
|
+
f"Reclaiming {_gib_text(free_bytes)} of free transcript "
|
|
3007
|
+
"space at a paced rate."
|
|
3008
|
+
),
|
|
3009
|
+
remediation=remediation,
|
|
3010
|
+
details={**details, "reclaim_wait": wait,
|
|
3011
|
+
"reclaim_refusal": refusal},
|
|
3012
|
+
)
|
|
2995
3013
|
return CheckResult(
|
|
2996
|
-
id="db.conversations_reclaimable",
|
|
2997
|
-
|
|
2998
|
-
severity="ok",
|
|
2999
|
-
summary="below threshold",
|
|
3000
|
-
remediation=None,
|
|
3001
|
-
details=details,
|
|
3014
|
+
id="db.conversations_reclaimable", title=title, severity="ok",
|
|
3015
|
+
summary="below threshold", remediation=None, details=details,
|
|
3002
3016
|
)
|
|
3003
3017
|
|
|
3004
3018
|
|
|
3019
|
+
def _latest_reclaim_refusal(gathered) -> "dict | None":
|
|
3020
|
+
"""The newest planner refusal any running dashboard reports in its
|
|
3021
|
+
`writeIo.reclaimRefusal`, with that dashboard's port; None when none
|
|
3022
|
+
does (or no dashboard runs)."""
|
|
3023
|
+
latest = None
|
|
3024
|
+
entries = gathered.get("instances") if isinstance(gathered, dict) else None
|
|
3025
|
+
for entry in entries or ():
|
|
3026
|
+
if not isinstance(entry, dict) or entry.get("probe") != "ok":
|
|
3027
|
+
continue
|
|
3028
|
+
refusal = (entry.get("write_io") or {}).get("reclaimRefusal")
|
|
3029
|
+
if (not isinstance(refusal, dict)
|
|
3030
|
+
or not isinstance(refusal.get("reason"), str)
|
|
3031
|
+
or not isinstance(refusal.get("at"), str)):
|
|
3032
|
+
continue
|
|
3033
|
+
candidate = {"reason": refusal["reason"], "at": refusal["at"],
|
|
3034
|
+
"port": entry.get("port")}
|
|
3035
|
+
if latest is None or candidate["at"] > latest["at"]:
|
|
3036
|
+
latest = candidate
|
|
3037
|
+
return latest
|
|
3038
|
+
|
|
3039
|
+
|
|
3005
3040
|
#: A stuck reclaim entry has persisted past this, so no pass will clear it.
|
|
3006
3041
|
_RECLAIM_STUCK_AFTER_SECONDS = 24 * 3600
|
|
3007
3042
|
|
|
@@ -4149,6 +4184,12 @@ _CATEGORY_DEFINITIONS: tuple[tuple[str, str, tuple[tuple[str, str], ...]], ...]
|
|
|
4149
4184
|
("telemetry", "Telemetry", (
|
|
4150
4185
|
("telemetry.state", "_check_telemetry"),
|
|
4151
4186
|
)),
|
|
4187
|
+
# #901 §5.6. Never FAILs, so `doctor`'s exit code is unaffected. Last, so
|
|
4188
|
+
# every existing check keeps its order.
|
|
4189
|
+
("performance", "Performance", (
|
|
4190
|
+
("performance.dashboard_disk_writes",
|
|
4191
|
+
"_check_performance_dashboard_disk_writes"),
|
|
4192
|
+
)),
|
|
4152
4193
|
)
|
|
4153
4194
|
|
|
4154
4195
|
|
|
@@ -4322,6 +4363,159 @@ def _check_quota_meter_drift(s: DoctorState) -> CheckResult:
|
|
|
4322
4363
|
)
|
|
4323
4364
|
|
|
4324
4365
|
|
|
4366
|
+
_DISK_WRITES_ID = "performance.dashboard_disk_writes"
|
|
4367
|
+
_DISK_WRITES_TITLE = "Dashboard disk writes"
|
|
4368
|
+
_DISK_WRITES_REMEDIATION = (
|
|
4369
|
+
"Run `cctally dashboard-perf` for the breakdown, and report it if it "
|
|
4370
|
+
"persists."
|
|
4371
|
+
)
|
|
4372
|
+
|
|
4373
|
+
|
|
4374
|
+
def _words(reason) -> str:
|
|
4375
|
+
return str(reason or "unavailable").replace("_", " ")
|
|
4376
|
+
|
|
4377
|
+
|
|
4378
|
+
#: PR-9: probe failures doctor cannot overcome from here (it sends no token
|
|
4379
|
+
#: and probes over loopback only), each with its sentence fragment.
|
|
4380
|
+
_NOT_MEASURABLE_FROM_HERE = {
|
|
4381
|
+
"authentication_required": "needs its access token",
|
|
4382
|
+
"not_loopback_reachable": "is not reachable over loopback",
|
|
4383
|
+
}
|
|
4384
|
+
|
|
4385
|
+
|
|
4386
|
+
def _check_performance_dashboard_disk_writes(s: DoctorState) -> CheckResult:
|
|
4387
|
+
"""Running dashboards' disk writes against the write budget, and
|
|
4388
|
+
transcript maintenance against its allowance (#901 §5.6, §5.7).
|
|
4389
|
+
|
|
4390
|
+
WARN when a measured running dashboard is over budget, when a live
|
|
4391
|
+
instance cannot supply valid telemetry, or when the durable maintenance
|
|
4392
|
+
ledger is over its allowance (so that leg needs no running dashboard).
|
|
4393
|
+
OK otherwise: none running, within budget, a statistic that has no
|
|
4394
|
+
verdict yet (named by its reason), a dashboard doctor cannot measure from
|
|
4395
|
+
here (a token-mode or non-loopback bind), or a platform with no counter.
|
|
4396
|
+
Never FAIL."""
|
|
4397
|
+
import _lib_write_budget as wb
|
|
4398
|
+
|
|
4399
|
+
gathered = (s.dashboard_disk_writes
|
|
4400
|
+
if isinstance(s.dashboard_disk_writes, dict)
|
|
4401
|
+
else {"mode": "error", "instances": []})
|
|
4402
|
+
record = (s.conversations_reclaim_pending
|
|
4403
|
+
if isinstance(s.conversations_reclaim_pending, dict) else {})
|
|
4404
|
+
maintenance = wb.maintenance_statistic(record.get("ledger"), s.now_utc)
|
|
4405
|
+
rows, over, cannot, within, warming, platform = [], [], [], [], [], []
|
|
4406
|
+
unmeasured = []
|
|
4407
|
+
for entry in gathered.get("instances") or ():
|
|
4408
|
+
if not isinstance(entry, dict):
|
|
4409
|
+
continue
|
|
4410
|
+
port = entry.get("port")
|
|
4411
|
+
write_io = entry.get("write_io") if entry.get("probe") == "ok" else None
|
|
4412
|
+
budget = (write_io or {}).get("budget") or {}
|
|
4413
|
+
if write_io is None:
|
|
4414
|
+
verdict, reason = "unavailable", entry.get("reason") or "unreachable"
|
|
4415
|
+
if reason in _NOT_MEASURABLE_FROM_HERE:
|
|
4416
|
+
# PR-9: doctor sends no token and probes over loopback only,
|
|
4417
|
+
# so these never answer it: a note, not a warning.
|
|
4418
|
+
unmeasured.append((port, reason))
|
|
4419
|
+
else:
|
|
4420
|
+
cannot.append((port, reason))
|
|
4421
|
+
elif write_io.get("status") != "ok":
|
|
4422
|
+
verdict = "unavailable"
|
|
4423
|
+
reason = write_io.get("reason") or "counter_error"
|
|
4424
|
+
if reason == "unsupported_platform":
|
|
4425
|
+
platform.append((port, reason))
|
|
4426
|
+
else:
|
|
4427
|
+
cannot.append((port, reason))
|
|
4428
|
+
else:
|
|
4429
|
+
verdict, reason = budget.get("verdict"), None
|
|
4430
|
+
if verdict == "over":
|
|
4431
|
+
over.append((port, write_io, budget))
|
|
4432
|
+
elif verdict == "ok":
|
|
4433
|
+
within.append((port, write_io))
|
|
4434
|
+
else:
|
|
4435
|
+
verdict = "insufficient"
|
|
4436
|
+
reason = (budget.get("reasons") or ["warming_up"])[0]
|
|
4437
|
+
warming.append((port, reason))
|
|
4438
|
+
rows.append({
|
|
4439
|
+
"instance_id": entry.get("instance_id"),
|
|
4440
|
+
"port": port,
|
|
4441
|
+
"pid": entry.get("pid"),
|
|
4442
|
+
"verdict": verdict,
|
|
4443
|
+
"reason": reason,
|
|
4444
|
+
"bytes_per_minute": (write_io or {}).get("bytesPerMinute"),
|
|
4445
|
+
"mean_bytes_per_publication": (write_io or {}).get(
|
|
4446
|
+
"meanBytesPerTick"),
|
|
4447
|
+
"bytes_per_minute_limit": budget.get("bytesPerMinuteLimit"),
|
|
4448
|
+
"mean_bytes_per_publication_limit": budget.get(
|
|
4449
|
+
"meanBytesPerTickLimit"),
|
|
4450
|
+
})
|
|
4451
|
+
rows.sort(key=lambda row: (row["port"] is None, row["port"] or 0))
|
|
4452
|
+
details = {
|
|
4453
|
+
"policy_version": wb.POLICY_VERSION,
|
|
4454
|
+
"mode": gathered.get("mode"),
|
|
4455
|
+
"instances": rows,
|
|
4456
|
+
"maintenance": maintenance.as_details(),
|
|
4457
|
+
}
|
|
4458
|
+
warn = []
|
|
4459
|
+
for port, write_io, budget in sorted(over, key=lambda item: item[0] or 0):
|
|
4460
|
+
reasons = budget.get("reasons") or []
|
|
4461
|
+
if ("publication_over_limit" in reasons
|
|
4462
|
+
and "rate_over_limit" not in reasons):
|
|
4463
|
+
warn.append(
|
|
4464
|
+
f"The running dashboard (port {port}) is writing "
|
|
4465
|
+
f"{wb.format_mib(write_io.get('meanBytesPerTick') or 0)} per "
|
|
4466
|
+
f"publication, over its "
|
|
4467
|
+
f"{wb.format_mib(budget.get('meanBytesPerTickLimit') or 0)} "
|
|
4468
|
+
f"per publication budget.")
|
|
4469
|
+
else:
|
|
4470
|
+
warn.append(
|
|
4471
|
+
f"The running dashboard (port {port}) is writing "
|
|
4472
|
+
f"{wb.format_mib(write_io.get('bytesPerMinute') or 0)}/min, "
|
|
4473
|
+
f"over its "
|
|
4474
|
+
f"{wb.format_mib(budget.get('bytesPerMinuteLimit') or 0)}/min "
|
|
4475
|
+
f"budget.")
|
|
4476
|
+
for port, reason in sorted(cannot, key=lambda item: item[0] or 0):
|
|
4477
|
+
warn.append(f"The running dashboard (port {port}) cannot report its "
|
|
4478
|
+
f"disk writes ({_words(reason)}).")
|
|
4479
|
+
if maintenance.verdict == "over":
|
|
4480
|
+
warn.append(
|
|
4481
|
+
f"Transcript maintenance was charged "
|
|
4482
|
+
f"{wb.format_size(maintenance.charged_bytes)} over the last "
|
|
4483
|
+
f"{wb.format_window_hm(maintenance.window_minutes)}, over its "
|
|
4484
|
+
f"{wb.format_size(maintenance.allowance_bytes)} allowance.")
|
|
4485
|
+
if warn:
|
|
4486
|
+
return CheckResult(
|
|
4487
|
+
id=_DISK_WRITES_ID, title=_DISK_WRITES_TITLE, severity="warn",
|
|
4488
|
+
summary=" ".join(warn), remediation=_DISK_WRITES_REMEDIATION,
|
|
4489
|
+
details=details)
|
|
4490
|
+
if within:
|
|
4491
|
+
port, write_io = sorted(within, key=lambda item: item[0] or 0)[0]
|
|
4492
|
+
summary = (f"Within budget: "
|
|
4493
|
+
f"{wb.format_mib(write_io.get('bytesPerMinute') or 0)}/min "
|
|
4494
|
+
f"over the last 5 minutes.")
|
|
4495
|
+
elif warming:
|
|
4496
|
+
# PR-8: the first instance's own reason; only `warming_up` is
|
|
4497
|
+
# "no samples yet".
|
|
4498
|
+
_port, reason = sorted(warming, key=lambda item: item[0] or 0)[0]
|
|
4499
|
+
summary = f"Disk writes: {wb.statistic_reason_phrase(reason)}."
|
|
4500
|
+
elif unmeasured:
|
|
4501
|
+
port, reason = sorted(unmeasured, key=lambda item: item[0] or 0)[0]
|
|
4502
|
+
summary = (f"Disk writes: the dashboard on port {port} "
|
|
4503
|
+
f"{_NOT_MEASURABLE_FROM_HERE[reason]}, so it is not "
|
|
4504
|
+
"measured from here.")
|
|
4505
|
+
elif platform:
|
|
4506
|
+
summary = f"Disk writes: unavailable ({_words(platform[0][1])})."
|
|
4507
|
+
elif gathered.get("mode") == "error":
|
|
4508
|
+
summary = "Disk writes: unavailable (gather failed)."
|
|
4509
|
+
elif gathered.get("mode") == "not_probed":
|
|
4510
|
+
summary = ("Disk writes: not measured here; run `cctally doctor` to "
|
|
4511
|
+
"check running dashboards.")
|
|
4512
|
+
else:
|
|
4513
|
+
summary = "No dashboard is running."
|
|
4514
|
+
return CheckResult(
|
|
4515
|
+
id=_DISK_WRITES_ID, title=_DISK_WRITES_TITLE, severity="ok",
|
|
4516
|
+
summary=summary, remediation=None, details=details)
|
|
4517
|
+
|
|
4518
|
+
|
|
4325
4519
|
def _evaluate_one(check_id: str, check_fn_name: str,
|
|
4326
4520
|
state: DoctorState) -> CheckResult:
|
|
4327
4521
|
"""Invoke a single check evaluator by name, catching any exception so
|
package/bin/_lib_fmt.py
CHANGED
|
@@ -292,6 +292,17 @@ def _fmt_num(n: int) -> str:
|
|
|
292
292
|
return f"{n:,}"
|
|
293
293
|
|
|
294
294
|
|
|
295
|
+
def _fmt_usd_accounting(value: float, places: int) -> str:
|
|
296
|
+
"""`-$71.21`: the sign goes before the currency symbol (#886).
|
|
297
|
+
|
|
298
|
+
The CLI convention shared with `_lib_diff_kernel` and `_lib_share`'s
|
|
299
|
+
MoneyCell: an ASCII hyphen, no `+` for a plain amount, and a sign only
|
|
300
|
+
when `value < 0` — so a negative zero prints unsigned.
|
|
301
|
+
"""
|
|
302
|
+
sign = "-" if value < 0 else ""
|
|
303
|
+
return f"{sign}${abs(value):.{places}f}"
|
|
304
|
+
|
|
305
|
+
|
|
295
306
|
def _truncate_num(formatted: str, width: int) -> str:
|
|
296
307
|
"""Truncate a formatted number to fit width, replacing tail with '…'."""
|
|
297
308
|
if len(formatted) <= width:
|