cctally 1.90.1 → 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.
@@ -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
- SOURCE_SCHEMA_VERSION = 3
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
- # #350 spec §3.5: a stale-cycle hero is NOT combinable. Retaining it would
396
- # let a sum over stale evidence be published while `compose_all_state` marks
397
- # the result fresh — and the All hero carries no Snapshot row or staleness
398
- # marker at all, so the staleness would be silently undisclosed. The
399
- # combined NUMBER therefore behaves exactly as it does today.
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
- # #350 spec §3.5: "All behaves exactly as today" is true only of the combined
466
- # NUMBER. Under §3.4 both providers stay coherent, so All now publishes
467
- # partial/fresh with no provider warning to explain it — the status chip
468
- # would fall through to a generic `degraded` and the All hero fallback would
469
- # claim a provider is degraded while both provider envelopes say otherwise.
470
- # This All-LOCAL warning states the real reason without touching either
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
- all_local_warnings = (SourceDashboardWarning(
478
- "combined_totals_withheld",
479
- f"{' and '.join(stale_cycle_providers)} quota evidence is stale, "
480
- "so combined totals are withheld.",
481
- "hero",
482
- ),)
483
- availability: Availability = (
484
- "partial"
485
- if combined is None or "partial" in (claude.availability, codex.availability)
486
- else (
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 real reason combined totals are withheld.
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,