cctally 1.90.0 → 1.91.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 +40 -0
- package/README.md +2 -2
- package/bin/_cctally_core.py +14 -0
- package/bin/_cctally_dashboard_envelope.py +67 -12
- package/bin/_cctally_dashboard_sources.py +27 -1
- package/bin/_lib_cache_report_wire.py +8 -20
- package/bin/_lib_codex_conversation.py +108 -0
- package/bin/_lib_codex_conversation_query.py +832 -117
- package/bin/_lib_codex_reasoning_headings.py +73 -0
- package/bin/_lib_codex_segments.py +259 -0
- package/bin/_lib_dashboard_sources.py +33 -32
- package/dashboard/static/assets/index-CILAoEja.js +90 -0
- package/dashboard/static/assets/index-Dwirao3Y.css +1 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +3 -1
- package/dashboard/static/assets/index-Bar8-S1i.css +0 -1
- package/dashboard/static/assets/index-CRogVlEC.js +0 -92
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Pure decomposition of a Codex reasoning aggregate into its authored headings.
|
|
2
|
+
|
|
3
|
+
#463 S2 §2.4. Codex writes reasoning as short bold headings. When an aggregate
|
|
4
|
+
holds several of them joined by newlines, ``_REASONING_TITLE_RE`` cannot
|
|
5
|
+
fullmatch the whole summary and the blob becomes one ``summary``, which the
|
|
6
|
+
reader renders as one clipped line. Measured over a real store: 5,081 of the
|
|
7
|
+
5,234 summary-only reasoning blocks are entirely bold-heading lines, holding
|
|
8
|
+
12,323 individual headings between them, and every one of those headings is
|
|
9
|
+
today invisible past the first thirty characters of the first one.
|
|
10
|
+
|
|
11
|
+
This module recovers the individual headings WITHOUT touching the stored
|
|
12
|
+
projection. That seam is load-bearing and is the trap most likely to be walked
|
|
13
|
+
into again: ``_row_is_reasoning_title`` reads the stored reasoning projection to
|
|
14
|
+
decide whether a row is a title boundary, and that is one of segmentation's two
|
|
15
|
+
semantic boundaries. Decomposing inside ``_reasoning_projection`` would change
|
|
16
|
+
which rows are boundaries and therefore move segment boundaries, which #463 S1's
|
|
17
|
+
contract forbids. Decomposition therefore runs at READ time, over the retained
|
|
18
|
+
payload's ``summary`` entries, and the stored ``title``, ``summary`` and ``body``
|
|
19
|
+
keep exactly today's values.
|
|
20
|
+
|
|
21
|
+
No I/O, no database, no imports beyond ``re``.
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import re
|
|
26
|
+
from collections.abc import Iterable
|
|
27
|
+
|
|
28
|
+
# The same shape ``_lib_codex_conversation._REASONING_TITLE_RE`` recognises,
|
|
29
|
+
# applied per LINE rather than to the whole summary. Deliberately NOT carrying
|
|
30
|
+
# the stored projection's additional guard (which rejects a title containing
|
|
31
|
+
# ``**``): the projection is a frozen segmentation input, this is a read-time
|
|
32
|
+
# rendering concern, and §2.4 states the per-line rule without that guard.
|
|
33
|
+
_HEADING_LINE_RE = re.compile(r"\A\*\*([^\n]+)\*\*\Z")
|
|
34
|
+
|
|
35
|
+
# Codex interleaves this literal between headings in a minority of aggregates
|
|
36
|
+
# (123 of 5,234 measured). It is layout, not content, so a decomposed entry
|
|
37
|
+
# discards it.
|
|
38
|
+
_SEPARATOR_LINE = "<!-- -->"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def decompose_reasoning_headings(entries: Iterable[object]) -> list[str]:
|
|
42
|
+
"""Heading texts for one reasoning aggregate, in entry order.
|
|
43
|
+
|
|
44
|
+
``entries`` is the ordered ``text`` values of the retained payload's
|
|
45
|
+
``summary`` entries. The unit of decision is the ENTRY and the decision is
|
|
46
|
+
all-or-nothing: an entry yields several headings only when, after discarding
|
|
47
|
+
separator lines, every remaining line is a heading line and at least one
|
|
48
|
+
remains. Otherwise it yields exactly one heading whose text is the entry
|
|
49
|
+
VERBATIM, separator lines included — a fallback that partially cleaned its
|
|
50
|
+
input would be a second transformation nobody reviewed.
|
|
51
|
+
|
|
52
|
+
Coverage invariant (§2.4, and the test): every line of the source aggregate
|
|
53
|
+
is either a discarded separator in a decomposed entry, or appears in exactly
|
|
54
|
+
one heading, exactly once. No line is dropped, none is duplicated, and no
|
|
55
|
+
path both extracts headings and re-renders the original text.
|
|
56
|
+
|
|
57
|
+
Total over its input: a non-string entry is skipped rather than raised on, so
|
|
58
|
+
no provider shape can fail detail assembly. The all-or-nothing rule for a
|
|
59
|
+
malformed payload is enforced at the call site, which validates the whole
|
|
60
|
+
entry list before calling.
|
|
61
|
+
"""
|
|
62
|
+
out: list[str] = []
|
|
63
|
+
for entry in entries or ():
|
|
64
|
+
if not isinstance(entry, str) or not entry.strip():
|
|
65
|
+
continue
|
|
66
|
+
lines = [line.strip() for line in entry.split("\n") if line.strip()]
|
|
67
|
+
kept = [line for line in lines if line != _SEPARATOR_LINE]
|
|
68
|
+
matches = [_HEADING_LINE_RE.fullmatch(line) for line in kept]
|
|
69
|
+
if kept and all(match is not None for match in matches):
|
|
70
|
+
out.extend(match.group(1) for match in matches)
|
|
71
|
+
else:
|
|
72
|
+
out.append(entry)
|
|
73
|
+
return out
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"""#463 S1 — the pure segmentation kernel for Codex conversation turns.
|
|
2
|
+
|
|
3
|
+
A **segment** is a run of consecutive fold groups inside one ``klass ==
|
|
4
|
+
"response"`` canonical item, taken greedily from the start of the turn, closed
|
|
5
|
+
when the block budget is reached, and closed earlier when a semantic boundary
|
|
6
|
+
falls inside the budget window. Items whose class is not ``response`` already
|
|
7
|
+
contain a single row; each is exactly one segment and its key does not change.
|
|
8
|
+
|
|
9
|
+
**The unit is a fold group, not a row, and that is not a detail.**
|
|
10
|
+
``_item_blocks_with_rows`` folds a ``tool_output`` into a preceding ``tool_call``
|
|
11
|
+
whenever the call identifier is non-empty, owned by exactly one call in the item,
|
|
12
|
+
and already seen — with **no adjacency requirement**. Patch, web-search and MCP
|
|
13
|
+
completion events fold the same way. A boundary drawn between a call and its
|
|
14
|
+
folded output would make the page-local builder emit a different block structure
|
|
15
|
+
than the whole-turn builder does, so a fold group is atomic here. Because folds
|
|
16
|
+
are non-adjacent a group can span intervening blocks, so a group that exceeds the
|
|
17
|
+
budget becomes its own segment: the budget is a target with fold-group atomicity
|
|
18
|
+
as a hard floor, and the ceiling is the budget plus at most one maximal group.
|
|
19
|
+
|
|
20
|
+
This module is deliberately pure — no SQLite, no I/O, and no import from the
|
|
21
|
+
query layer. The caller derives fold groups and the turn-scoped
|
|
22
|
+
``call_owner_count`` and passes them in.
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import dataclasses
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
# Per-segment block budget (spec section 2). At the measured 27.5 DOM nodes per
|
|
30
|
+
# block that is roughly 1,100 nodes, close to the 776 nodes per mounted row the
|
|
31
|
+
# Claude control paints in 449 ms.
|
|
32
|
+
SEGMENT_BLOCK_BUDGET = 40
|
|
33
|
+
|
|
34
|
+
# Per-page block budget, applied alongside ``limit`` (spec section 2). Roughly
|
|
35
|
+
# fifty full segments, in the same range as the Claude control's 2.58 MB page.
|
|
36
|
+
# The per-page bound is not optional: the profiled response was
|
|
37
|
+
# ``total: 78, returned: 78, has_after: false`` — 13.3 MB in one page, because
|
|
38
|
+
# 78 is fewer than the requested 500 — so a change that capped items alone would
|
|
39
|
+
# not bound that conversation at all.
|
|
40
|
+
PAGE_BLOCK_BUDGET = 2000
|
|
41
|
+
|
|
42
|
+
# Per-page SOURCE-byte budget, applied alongside ``limit`` and
|
|
43
|
+
# ``PAGE_BLOCK_BUDGET``; the first bound reached closes the page. It bounds
|
|
44
|
+
# TRANSFER and PARSE cost, which is a byte cost the block budget does not
|
|
45
|
+
# express, because a Codex block is far heavier than a Claude block.
|
|
46
|
+
#
|
|
47
|
+
# It is not redundant with PAGE_BLOCK_BUDGET. After segmentation the profiled
|
|
48
|
+
# conversation is 128 segments carrying 1,906 blocks, so a whole-conversation
|
|
49
|
+
# page holds 1,713 blocks — BELOW the 2,000-block budget. The block bound never
|
|
50
|
+
# fires on it, and without this one the response is still 13.24 MB in one page.
|
|
51
|
+
#
|
|
52
|
+
# CALIBRATED BY MEASUREMENT, not by arithmetic, on 2026-08-02 against a
|
|
53
|
+
# read-only copy of the production store. At 3,000,000 source bytes the
|
|
54
|
+
# profiled conversation serves 16 of its 128 segments, 274 blocks and 2.54 MB
|
|
55
|
+
# on the wire — the 2 to 3 MB target, and effectively the Claude control's
|
|
56
|
+
# 2.58 MB page. Across the six heaviest conversations the served page at this
|
|
57
|
+
# budget ranges from 0.75 MB to 2.54 MB of wire. At 4,000,000 the maximum rises
|
|
58
|
+
# to 3.16 MB; at 2,000,000 the profiled conversation falls to 1.45 MB.
|
|
59
|
+
#
|
|
60
|
+
# Do NOT re-derive this figure by dividing a wire target by the
|
|
61
|
+
# whole-conversation source-to-wire ratio. That ratio is not uniform and not
|
|
62
|
+
# even close: the profiled conversation is 6.91x source-to-wire taken whole
|
|
63
|
+
# (91.52 MB source, 13.24 MB wire), but only 1.11x over the segments a 3 MB page
|
|
64
|
+
# actually serves (2.83 MB source, 2.54 MB wire), because its heaviest rows sit
|
|
65
|
+
# in the tail and are clipped hardest. Re-calibrate by measuring served pages.
|
|
66
|
+
PAGE_SOURCE_BYTE_BUDGET = 3_000_000
|
|
67
|
+
|
|
68
|
+
# Fraction of the budget the boundary snap may give up. Bounding it at a quarter
|
|
69
|
+
# guarantees a segment is never smaller than 75 percent of the budget, so the
|
|
70
|
+
# rule cannot produce a run of very small segments, and it preserves the ceiling
|
|
71
|
+
# because it only ever closes a segment EARLIER than the budget would.
|
|
72
|
+
LOOKBACK_FRACTION = 0.25
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# Both dataclasses below are ``frozen=True`` with ``eq=True``, which makes
|
|
76
|
+
# Python synthesize a ``__hash__`` — and that synthesized hash raises TypeError
|
|
77
|
+
# here, because every instance carries a ``list`` field. Nothing hashes a
|
|
78
|
+
# FoldGroup or a Segment today and nothing should: they are records passed
|
|
79
|
+
# between two functions in one call, never dict keys or set members, and their
|
|
80
|
+
# identity is positional rather than structural. ``unsafe_hash`` is deliberately
|
|
81
|
+
# NOT set, and ``__hash__`` is set to None so the failure is an explicit
|
|
82
|
+
# "unhashable type" at the call site rather than a TypeError from inside a
|
|
83
|
+
# generated method.
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclasses.dataclass(frozen=True)
|
|
87
|
+
class FoldGroup:
|
|
88
|
+
"""A ``tool_call`` together with every row that folds into it, or a single
|
|
89
|
+
non-folding row. Never divided across segments.
|
|
90
|
+
|
|
91
|
+
``is_title_boundary`` is true when the group's first row is a reasoning row
|
|
92
|
+
whose stored projection produces a ``title``. ``is_tool_transition`` is true
|
|
93
|
+
when it is the first ``tool_call`` following a run of assistant or reasoning
|
|
94
|
+
rows. Those are the two semantic boundaries, in that priority order.
|
|
95
|
+
|
|
96
|
+
``first_pos`` and ``last_pos`` are the group's physical row positions inside
|
|
97
|
+
its item. Because folds are non-adjacent, ``last_pos`` can be far past
|
|
98
|
+
``first_pos`` and can bracket a LATER group entirely, which is what
|
|
99
|
+
``plan_segments`` uses to keep a segment physically contiguous. ``None``
|
|
100
|
+
disables that extension, for callers that have no positional information.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
rows: list
|
|
104
|
+
block_count: int
|
|
105
|
+
source_bytes: int
|
|
106
|
+
is_title_boundary: bool = False
|
|
107
|
+
is_tool_transition: bool = False
|
|
108
|
+
first_pos: int | None = None
|
|
109
|
+
last_pos: int | None = None
|
|
110
|
+
|
|
111
|
+
__hash__ = None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclasses.dataclass(frozen=True)
|
|
115
|
+
class Segment:
|
|
116
|
+
"""One bounded run of fold groups inside a turn."""
|
|
117
|
+
|
|
118
|
+
ordinal: int
|
|
119
|
+
groups: list
|
|
120
|
+
block_count: int
|
|
121
|
+
source_bytes: int
|
|
122
|
+
anchor_row: Any
|
|
123
|
+
|
|
124
|
+
__hash__ = None
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _boundary_rank(group: FoldGroup) -> int:
|
|
128
|
+
"""Priority of the boundary a cut before this group would land on.
|
|
129
|
+
|
|
130
|
+
Lower is better. 0 = a reasoning title, 1 = a tool transition, 2 = not a
|
|
131
|
+
boundary at all.
|
|
132
|
+
"""
|
|
133
|
+
if group.is_title_boundary:
|
|
134
|
+
return 0
|
|
135
|
+
if group.is_tool_transition:
|
|
136
|
+
return 1
|
|
137
|
+
return 2
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _extend_to_contiguous(groups: list, start: int, end: int, total: int) -> int:
|
|
141
|
+
"""Grow ``end`` until the segment covers a CONTIGUOUS physical row range.
|
|
142
|
+
|
|
143
|
+
Fold-group atomicity alone does not give this (spec section 1). Because
|
|
144
|
+
folds are non-adjacent, a group's rows can bracket a later group's rows —
|
|
145
|
+
a native patch completion event sits between its call and that call's
|
|
146
|
+
output, for instance. Cutting between the two groups would then produce
|
|
147
|
+
segments whose physical ranges overlap: the earlier segment would render
|
|
148
|
+
rows out of physical order, and the later segment's rows would fall inside
|
|
149
|
+
the earlier one's time span.
|
|
150
|
+
|
|
151
|
+
Groups are created in the physical order of their FIRST row, so ``first_pos``
|
|
152
|
+
increases across the list. A cut before group ``end`` is therefore legal
|
|
153
|
+
exactly when every chosen group ends before ``groups[end]`` begins; if it
|
|
154
|
+
does not, that group is absorbed and the test repeats.
|
|
155
|
+
|
|
156
|
+
The extension only ever GROWS a segment, so the 75 percent lookback floor is
|
|
157
|
+
preserved. The ceiling becomes the budget plus the physical span of one
|
|
158
|
+
maximal fold group, which is what spec section 2 states.
|
|
159
|
+
"""
|
|
160
|
+
ends = [groups[i].last_pos for i in range(start, end)
|
|
161
|
+
if groups[i].last_pos is not None]
|
|
162
|
+
if not ends:
|
|
163
|
+
return end
|
|
164
|
+
max_last = max(ends)
|
|
165
|
+
while end < total:
|
|
166
|
+
nxt = groups[end].first_pos
|
|
167
|
+
if nxt is None or nxt > max_last:
|
|
168
|
+
break
|
|
169
|
+
if groups[end].last_pos is not None:
|
|
170
|
+
max_last = max(max_last, groups[end].last_pos)
|
|
171
|
+
end += 1
|
|
172
|
+
return end
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def plan_segments(
|
|
176
|
+
fold_groups: list,
|
|
177
|
+
*,
|
|
178
|
+
block_budget: int | None = None,
|
|
179
|
+
lookback_fraction: float | None = None,
|
|
180
|
+
) -> list[Segment]:
|
|
181
|
+
"""Divide a turn's fold groups into ordered segments.
|
|
182
|
+
|
|
183
|
+
Fills greedily from index 0. A segment closes at the last group that fits
|
|
184
|
+
inside ``block_budget``, or earlier at the highest-priority semantic
|
|
185
|
+
boundary whose cut point falls inside the lookback window — the range from
|
|
186
|
+
``(1 - lookback_fraction) * block_budget`` blocks up to the budget. A group
|
|
187
|
+
that does not fit even into an empty segment becomes its own segment.
|
|
188
|
+
|
|
189
|
+
**Greedy-from-start is the mechanism, not a convenience.** Every segment
|
|
190
|
+
depends only on the groups before it, so appending groups to a growing turn
|
|
191
|
+
leaves earlier segments — and therefore earlier segment keys — untouched.
|
|
192
|
+
Computing boundaries from the end would renumber a conversation's history on
|
|
193
|
+
every append. Segment keys 1..N remain durable only under that tail append;
|
|
194
|
+
inserting or deleting a row before a boundary shifts every later boundary in
|
|
195
|
+
the turn, and a former anchor becomes an interior row.
|
|
196
|
+
|
|
197
|
+
``block_budget`` and ``lookback_fraction`` resolve to the module constants
|
|
198
|
+
at CALL time when omitted. They are deliberately not default ARGUMENT values:
|
|
199
|
+
a default argument binds once at import, so a test that lowers or raises
|
|
200
|
+
``SEGMENT_BLOCK_BUDGET`` would silently keep the imported figure and pass
|
|
201
|
+
vacuously.
|
|
202
|
+
"""
|
|
203
|
+
if block_budget is None:
|
|
204
|
+
block_budget = SEGMENT_BLOCK_BUDGET
|
|
205
|
+
if lookback_fraction is None:
|
|
206
|
+
lookback_fraction = LOOKBACK_FRACTION
|
|
207
|
+
if block_budget <= 0:
|
|
208
|
+
raise ValueError("block_budget must be positive")
|
|
209
|
+
if not 0.0 <= lookback_fraction < 1.0:
|
|
210
|
+
raise ValueError("lookback_fraction must be in [0.0, 1.0)")
|
|
211
|
+
|
|
212
|
+
groups = list(fold_groups)
|
|
213
|
+
total = len(groups)
|
|
214
|
+
floor_blocks = block_budget - int(block_budget * lookback_fraction)
|
|
215
|
+
|
|
216
|
+
segments: list[Segment] = []
|
|
217
|
+
start = 0
|
|
218
|
+
while start < total:
|
|
219
|
+
# How far the budget alone reaches. At least one group always fits, so a
|
|
220
|
+
# single oversized group becomes its own segment rather than stalling.
|
|
221
|
+
end = start
|
|
222
|
+
blocks = 0
|
|
223
|
+
while end < total:
|
|
224
|
+
candidate = blocks + groups[end].block_count
|
|
225
|
+
if end > start and candidate > block_budget:
|
|
226
|
+
break
|
|
227
|
+
blocks = candidate
|
|
228
|
+
end += 1
|
|
229
|
+
|
|
230
|
+
# Look for a boundary inside the window. A cut happens BEFORE group
|
|
231
|
+
# ``cut``, so that group must exist and must itself be a boundary, and
|
|
232
|
+
# the blocks kept must already clear the lookback floor.
|
|
233
|
+
best_cut = None
|
|
234
|
+
best_rank = 2
|
|
235
|
+
kept = 0
|
|
236
|
+
for cut in range(start, end):
|
|
237
|
+
if cut > start and kept >= floor_blocks and cut < total:
|
|
238
|
+
rank = _boundary_rank(groups[cut])
|
|
239
|
+
if rank < best_rank:
|
|
240
|
+
best_rank = rank
|
|
241
|
+
best_cut = cut
|
|
242
|
+
if rank == 0:
|
|
243
|
+
break
|
|
244
|
+
kept += groups[cut].block_count
|
|
245
|
+
if best_cut is not None:
|
|
246
|
+
end = best_cut
|
|
247
|
+
|
|
248
|
+
end = _extend_to_contiguous(groups, start, end, total)
|
|
249
|
+
|
|
250
|
+
chosen = groups[start:end]
|
|
251
|
+
segments.append(Segment(
|
|
252
|
+
ordinal=len(segments),
|
|
253
|
+
groups=chosen,
|
|
254
|
+
block_count=sum(group.block_count for group in chosen),
|
|
255
|
+
source_bytes=sum(group.source_bytes for group in chosen),
|
|
256
|
+
anchor_row=chosen[0].rows[0] if chosen and chosen[0].rows else None,
|
|
257
|
+
))
|
|
258
|
+
start = end
|
|
259
|
+
return segments
|
|
@@ -33,7 +33,10 @@ CapabilityStatus = Literal[
|
|
|
33
33
|
# field. Additive and omitted-when-zero, so the normal payload is byte-identical
|
|
34
34
|
# — but the same `execvp` transition applies, so the bump ships as the signal it
|
|
35
35
|
# has always been rather than as a mechanism the client branches on.
|
|
36
|
-
|
|
36
|
+
# 3 -> 4 (#465): the Codex cache report retired its transitional
|
|
37
|
+
# `cache_hit_percent` alias and changed structurally inapplicable figures from
|
|
38
|
+
# numeric placeholders to null.
|
|
39
|
+
SOURCE_SCHEMA_VERSION = 4
|
|
37
40
|
DEFAULT_SOURCE = "claude"
|
|
38
41
|
SOURCE_ORDER = ("claude", "codex", "all")
|
|
39
42
|
SOURCE_FRESHNESS_DOMAINS = ("hero", "quota", "sessions")
|
|
@@ -392,13 +395,11 @@ def _combined_metrics(
|
|
|
392
395
|
) -> Mapping[str, object] | None:
|
|
393
396
|
if not (_coherent_provider(claude) and _coherent_provider(codex)):
|
|
394
397
|
return None
|
|
395
|
-
# #
|
|
396
|
-
#
|
|
397
|
-
# the
|
|
398
|
-
#
|
|
399
|
-
#
|
|
400
|
-
if _stale_cycle_providers(claude, codex):
|
|
401
|
-
return None
|
|
398
|
+
# #359: the hero counters are backward-looking accounting actuals. A stale
|
|
399
|
+
# but still-live quota boundary pauses projections; it does not invalidate
|
|
400
|
+
# the retained cost/token sums that each provider already keeps visible.
|
|
401
|
+
# Composition therefore retains the compatible number and discloses the
|
|
402
|
+
# stale boundary through All's hero-domain freshness + local warning.
|
|
402
403
|
for state in (claude, codex):
|
|
403
404
|
hero_capability = state.capabilities.get("hero")
|
|
404
405
|
if hero_capability is None or hero_capability.status not in {"supported", "derived"}:
|
|
@@ -462,33 +463,33 @@ def compose_all_state(
|
|
|
462
463
|
raise ValueError("all composition requires Claude and Codex provider states")
|
|
463
464
|
combined = _combined_metrics(claude, codex)
|
|
464
465
|
providers_coherent = _coherent_provider(claude) and _coherent_provider(codex)
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
#
|
|
469
|
-
#
|
|
470
|
-
#
|
|
471
|
-
# provider envelope. It is emitted only when the providers are otherwise
|
|
472
|
-
# coherent; an incoherent provider already publishes its own reason.
|
|
466
|
+
stale_cycle_providers = (
|
|
467
|
+
_stale_cycle_providers(claude, codex) if providers_coherent else ()
|
|
468
|
+
)
|
|
469
|
+
# #359: the warning qualifies a retained combined actual. It stays
|
|
470
|
+
# All-local and keeps the composed source partial so the header status also
|
|
471
|
+
# names the caveat; provider envelopes remain independently coherent.
|
|
473
472
|
all_local_warnings: tuple[SourceDashboardWarning, ...] = ()
|
|
473
|
+
if stale_cycle_providers:
|
|
474
|
+
all_local_warnings = (SourceDashboardWarning(
|
|
475
|
+
"combined_totals_stale",
|
|
476
|
+
f"{' and '.join(stale_cycle_providers)} quota evidence is stale; "
|
|
477
|
+
"combined totals use retained actuals.",
|
|
478
|
+
"hero",
|
|
479
|
+
),)
|
|
474
480
|
if providers_coherent:
|
|
475
|
-
stale_cycle_providers = _stale_cycle_providers(claude, codex)
|
|
476
481
|
if stale_cycle_providers:
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
"
|
|
481
|
-
"
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
"empty"
|
|
488
|
-
if claude.availability == "empty" and codex.availability == "empty"
|
|
489
|
-
else "ok"
|
|
482
|
+
availability: Availability = "partial"
|
|
483
|
+
else:
|
|
484
|
+
availability = (
|
|
485
|
+
"partial"
|
|
486
|
+
if combined is None or "partial" in (claude.availability, codex.availability)
|
|
487
|
+
else (
|
|
488
|
+
"empty"
|
|
489
|
+
if claude.availability == "empty" and codex.availability == "empty"
|
|
490
|
+
else "ok"
|
|
491
|
+
)
|
|
490
492
|
)
|
|
491
|
-
)
|
|
492
493
|
freshness: Freshness = "fresh"
|
|
493
494
|
else:
|
|
494
495
|
availability = "partial"
|
|
@@ -519,7 +520,7 @@ def compose_all_state(
|
|
|
519
520
|
# All-LOCAL warnings lead: `warningForSource` on the client falls back to
|
|
520
521
|
# the FIRST warning, so a merely-partial provider warning (e.g.
|
|
521
522
|
# `codex_metadata_incomplete`) would otherwise pre-empt the chip label
|
|
522
|
-
# and hide the
|
|
523
|
+
# and hide the stale qualification on the combined actual.
|
|
523
524
|
warnings=tuple((*all_local_warnings, *claude.warnings, *codex.warnings)),
|
|
524
525
|
data_version=data_version,
|
|
525
526
|
last_success_at=last_success_at,
|