cctally 1.96.2 → 1.97.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.
@@ -36,7 +36,16 @@ CapabilityStatus = Literal[
36
36
  # 3 -> 4 (#465): the Codex cache report retired its transitional
37
37
  # `cache_hit_percent` alias and changed structurally inapplicable figures from
38
38
  # numeric placeholders to null.
39
- SOURCE_SCHEMA_VERSION = 4
39
+ # 4 -> 5 (#556 S1): Claude's `hero.cost_usd` / `hero.total_tokens` changed
40
+ # MEANING — they were a thirty-day accounting rollup and are now current-cycle
41
+ # actuals, matching what the same-named Codex fields have always meant. The All
42
+ # source's `data.combined` also gained a required `legs` object and the optional
43
+ # `qualifications` / `combined_unavailable` companions, which supersedes normal-
44
+ # payload byte identity for this version (spec §3.5). No client branches on this
45
+ # number; after an in-place `execvp` update a still-loaded old client renders the
46
+ # new figure under old copy until it reloads, and that one-reconnect transient is
47
+ # accepted, consistent with the 2 -> 3 precedent above.
48
+ SOURCE_SCHEMA_VERSION = 5
40
49
  DEFAULT_SOURCE = "claude"
41
50
  SOURCE_ORDER = ("claude", "codex", "all")
42
51
  SOURCE_FRESHNESS_DOMAINS = ("hero", "quota", "sessions")
@@ -133,6 +142,20 @@ class SourceDashboardState:
133
142
  # They are deliberately separate from ``data`` so no internal accounting
134
143
  # evidence becomes part of the public source-envelope contract.
135
144
  clock_data: Mapping[str, object] | None = None
145
+ # #556 S1 §3.8 — the provider's authoritative REAL account count, resolved
146
+ # by the builder and carried here so `compose_all_state` can apply the
147
+ # single-account gate. Server-only, in the same class as `clock_data`:
148
+ # deliberately outside `data`, so no account cardinality enters the public
149
+ # source envelope. Shape: ``{"real_account_count": int}``.
150
+ #
151
+ # ``None`` means UNRESOLVED and must fail closed (withhold the combined
152
+ # figure), never "undecorated". Inferring decoration from the published
153
+ # `data.accounts` is forbidden for exactly this reason: both physical
154
+ # builders swallow a decoration-read failure and fall back to the
155
+ # undecorated shape, so a two-account install whose account read failed
156
+ # would present as single-account and publish the one number §3.2 forbids,
157
+ # on precisely the install where it is wrong.
158
+ account_scope: Mapping[str, object] | None = None
136
159
  # Request-gated transcript content. This mapping is frozen with the source
137
160
  # generation but is deliberately outside ``data``: source serialization
138
161
  # publishes only ``data``, then the HTTP/SSE envelope layer injects a label
@@ -181,6 +204,8 @@ class SourceDashboardState:
181
204
  object.__setattr__(self, "data", _freeze(self.data))
182
205
  if self.clock_data is not None:
183
206
  object.__setattr__(self, "clock_data", _freeze(self.clock_data))
207
+ if self.account_scope is not None:
208
+ object.__setattr__(self, "account_scope", _freeze(self.account_scope))
184
209
  if self.private_session_labels is not None:
185
210
  private_session_labels = {
186
211
  _nonempty_string(key, "private session label key"):
@@ -295,6 +320,10 @@ def degrade_source_state(
295
320
  domain: "stale" for domain in SOURCE_FRESHNESS_DOMAINS
296
321
  },
297
322
  clock_data=prior.clock_data,
323
+ # #556 S1 §3.8: a degraded generation must not LOSE the count. Dropping
324
+ # it here would turn a transient provider failure into
325
+ # `account_scope_unresolved` on an install whose count read fine.
326
+ account_scope=prior.account_scope,
298
327
  private_session_labels=prior.private_session_labels,
299
328
  )
300
329
 
@@ -365,69 +394,334 @@ def reuse_coherent_source_state(
365
394
  return prior if _coherent_provider(prior) and prior.data_version == data_version else None
366
395
 
367
396
 
368
- def _hero_cycle_is_stale(state: SourceDashboardState) -> bool:
369
- """Whether a provider's hero is bounded by STALE quota evidence (#350 §3.4).
397
+ # === #556 S1 — the typed combined outcome (spec §3.5, §3.7) =================
398
+ #
399
+ # `combined` is the sum, over both providers, of that provider's accounting
400
+ # actuals within its OWN current cycle: Claude's subscription week and Codex's
401
+ # native 7-day cycle. The two legs are deliberately not one shared range — the
402
+ # property bought is that each leg reconciles with its provider tab, and each
403
+ # leg therefore names the cycle it covers.
370
404
 
371
- ``hero.cycle_freshness`` is additive and OMITTED while the cycle is fresh, so
372
- this is false for every provider and every generation that predates #350.
405
+ _PROVIDER_LABELS: Mapping[str, str] = MappingProxyType(
406
+ {"claude": "Claude", "codex": "Codex"},
407
+ )
408
+ _LEG_PERIOD_KINDS: Mapping[str, tuple[str, str, str, str]] = MappingProxyType({
409
+ # provider -> (kind, label, hero container key, (start key, end key))
410
+ "claude": ("subscription_week", "Claude subscription week",
411
+ "current_week", "week_start_at|reset_at_utc"),
412
+ "codex": ("native_7_day_cycle", "Codex native 7-day cycle",
413
+ "cycle", "start_at|resets_at"),
414
+ })
415
+
416
+
417
+ @dataclass(frozen=True)
418
+ class _CombinedCause:
419
+ """One reason the combined figure is withheld, with its precedence rank."""
420
+
421
+ precedence: int
422
+ provider: PhysicalSource
423
+ code: str
424
+ detail: Mapping[str, object] | None = None
425
+
426
+
427
+ def _cause_message(cause: _CombinedCause) -> str:
428
+ """Public-safe prose for one cause. Never echoes a rejected value."""
429
+ provider = _PROVIDER_LABELS.get(cause.provider, cause.provider)
430
+ detail = cause.detail or {}
431
+ if cause.code == "provider_incoherent":
432
+ return (
433
+ f"{provider} data is not current, so a combined total is withheld."
434
+ )
435
+ if cause.code == "account_scope_unresolved":
436
+ return (
437
+ f"{provider}'s account count could not be read, so a combined "
438
+ "total is withheld."
439
+ )
440
+ if cause.code == "multi_account_unsupported":
441
+ count = detail.get("account_count")
442
+ return (
443
+ f"{provider} has {count} accounts on separate cycles, so a "
444
+ "combined total is not published; see the per-account cards."
445
+ )
446
+ if cause.code == "claude_cycle_unresolved":
447
+ return "Claude's current subscription week could not be resolved."
448
+ if cause.code == "codex_projection_incoherent":
449
+ return "Codex quota projection is unavailable."
450
+ if cause.code == "codex_cycle_unavailable":
451
+ return "Codex native reset cycle is unavailable."
452
+ if cause.code == "invalid_counter":
453
+ return (
454
+ f"{provider} reported an unusable {detail.get('field')} counter "
455
+ f"({detail.get('reason')})."
456
+ )
457
+ return f"{provider} data cannot contribute to a combined total."
458
+
459
+
460
+ def _combined_hero(state: SourceDashboardState) -> Mapping[str, object] | None:
461
+ data = state.data
462
+ if not isinstance(data, Mapping):
463
+ return None
464
+ hero = data.get("hero")
465
+ return hero if isinstance(hero, Mapping) else None
466
+
467
+
468
+ def _real_account_count(state: SourceDashboardState) -> int | None:
469
+ """The provider's authoritative REAL account count, or ``None``.
470
+
471
+ ``None`` is UNRESOLVED and fails closed (§3.8). Decoration is never
472
+ inferred from published data, because both physical builders swallow a
473
+ decoration-read failure and fall back to the undecorated shape.
373
474
  """
374
- if source_domain_freshness(state, "hero") == "stale":
375
- return True
376
- if not isinstance(state.data, Mapping):
377
- return False
378
- hero = state.data.get("hero")
379
- return isinstance(hero, Mapping) and hero.get("cycle_freshness") == "stale"
475
+ scope = getattr(state, "account_scope", None)
476
+ if not isinstance(scope, Mapping):
477
+ return None
478
+ count = scope.get("real_account_count")
479
+ if isinstance(count, bool) or not isinstance(count, int) or count < 0:
480
+ return None
481
+ return count
380
482
 
381
483
 
382
- def _stale_cycle_providers(
383
- claude: SourceDashboardState,
384
- codex: SourceDashboardState,
385
- ) -> tuple[str, ...]:
386
- return tuple(
387
- label for label, state in (("Claude", claude), ("Codex", codex))
388
- if _hero_cycle_is_stale(state)
389
- )
484
+ def _counter_reason(value: object, *, integral: bool) -> str | None:
485
+ """Return why ``value`` is unusable as a counter, or ``None`` if it is fine.
486
+
487
+ Booleans are rejected: ``True`` is an ``int`` in Python, and a flag that
488
+ leaked into a counter slot would otherwise be summed as 1.
489
+ """
490
+ if value is None:
491
+ return "missing"
492
+ if isinstance(value, bool):
493
+ return "non_integer"
494
+ if integral:
495
+ if not isinstance(value, int):
496
+ return "non_integer"
497
+ else:
498
+ if not isinstance(value, (int, float)):
499
+ return "non_integer"
500
+ if not math.isfinite(value):
501
+ return "non_finite"
502
+ return "negative" if value < 0 else None
390
503
 
391
504
 
392
- def _combined_metrics(
393
- claude: SourceDashboardState,
394
- codex: SourceDashboardState,
395
- ) -> Mapping[str, object] | None:
396
- if not (_coherent_provider(claude) and _coherent_provider(codex)):
505
+ def _period_instant(value: object) -> str | None:
506
+ """One canonical UTC spelling for a published period bound.
507
+
508
+ The two providers reach this with different spellings of the same
509
+ convention Claude's bounds arrive from the legacy envelope's `_iso_z`
510
+ (`...Z`) and Codex's from `datetime.isoformat()` (`...+00:00`). Publishing
511
+ both would make every client parse two forms of one field for no reason.
512
+ """
513
+ if not isinstance(value, str) or not value:
397
514
  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.
403
- for state in (claude, codex):
404
- hero_capability = state.capabilities.get("hero")
405
- if hero_capability is None or hero_capability.status not in {"supported", "derived"}:
406
- return None
407
515
  try:
408
- claude_hero = claude.data["hero"]
409
- codex_hero = codex.data["hero"]
410
- if not isinstance(claude_hero, Mapping) or not isinstance(codex_hero, Mapping):
411
- return None
412
- claude_cost = claude_hero["cost_usd"]
413
- codex_cost = codex_hero["cost_usd"]
414
- claude_tokens = claude_hero["total_tokens"]
415
- codex_tokens = codex_hero["total_tokens"]
416
- if (
417
- isinstance(claude_cost, bool) or not isinstance(claude_cost, (int, float))
418
- or isinstance(codex_cost, bool) or not isinstance(codex_cost, (int, float))
419
- or isinstance(claude_tokens, bool) or not isinstance(claude_tokens, int)
420
- or isinstance(codex_tokens, bool) or not isinstance(codex_tokens, int)
421
- ):
422
- return None
423
- except (KeyError, TypeError):
516
+ parsed = dt.datetime.fromisoformat(value.replace("Z", "+00:00"))
517
+ except ValueError:
518
+ return None
519
+ if parsed.tzinfo is None:
520
+ parsed = parsed.replace(tzinfo=dt.timezone.utc)
521
+ return parsed.astimezone(dt.timezone.utc).isoformat().replace("+00:00", "Z")
522
+
523
+
524
+ def _leg_period(
525
+ provider: PhysicalSource, hero: Mapping[str, object] | None,
526
+ ) -> Mapping[str, object] | None:
527
+ """The named cycle a `current` leg covers, or ``None`` when unresolvable."""
528
+ kind, label, container_key, bound_keys = _LEG_PERIOD_KINDS[provider]
529
+ container = hero.get(container_key) if isinstance(hero, Mapping) else None
530
+ if not isinstance(container, Mapping):
531
+ return None
532
+ start_key, end_key = bound_keys.split("|")
533
+ start_at = _period_instant(container.get(start_key))
534
+ end_at = _period_instant(container.get(end_key))
535
+ if start_at is None or end_at is None:
424
536
  return None
425
537
  return {
426
- "cost_usd": float(claude_cost) + float(codex_cost),
427
- "total_tokens": claude_tokens + codex_tokens,
538
+ "kind": kind, "label": label, "start_at": start_at, "end_at": end_at,
428
539
  }
429
540
 
430
541
 
542
+ _CODEX_HERO_FAILURE_ORDER: tuple[str, ...] = (
543
+ "codex_projection_incoherent", "codex_cycle_unavailable",
544
+ )
545
+
546
+
547
+ def _codex_hero_failure_codes(state: SourceDashboardState) -> tuple[str, ...]:
548
+ """EVERY Codex hero failure present, in the order §3.7 fixes.
549
+
550
+ Codex computes projection coherence and cycle resolution independently and
551
+ emits BOTH warnings, so §3.5 lists both as causes and §3.7 fixes their
552
+ order: an incoherent projection certificate invalidates the evidence the
553
+ cycle resolution rests on, and is therefore the earlier cause. The first
554
+ element is the winner, so the top-level `code` is unchanged by listing the
555
+ rest.
556
+ """
557
+ codes = {warning.code for warning in state.warnings}
558
+ found = tuple(code for code in _CODEX_HERO_FAILURE_ORDER if code in codes)
559
+ if found:
560
+ return found
561
+ # No warning names the failure — fall back to the hero capability's own
562
+ # semantics, which can only describe one of the two.
563
+ capability = state.capabilities.get("hero")
564
+ semantics = capability.semantics if capability is not None else None
565
+ if semantics == "projection-incoherent":
566
+ return ("codex_projection_incoherent",)
567
+ return ("codex_cycle_unavailable",)
568
+
569
+
570
+ def _combined_leg(
571
+ state: SourceDashboardState, provider: PhysicalSource,
572
+ ) -> tuple[Mapping[str, object] | None, tuple[_CombinedCause, ...]]:
573
+ """Build one leg, or the causes that stop it contributing.
574
+
575
+ A leg is `empty` when the provider reports no accounting AND no cycle:
576
+ `availability == "empty"` and no period resolves. Both halves are needed.
577
+ `availability` alone is not the fact, because it is computed over the
578
+ dashboard's VISIBLE range while Codex's hero is a separate cycle-bounded
579
+ read — a Codex provider can be `empty` in the visible range while its
580
+ current cycle holds real spend. An unresolved period alone is not the fact
581
+ either, because for Claude that is a FAILURE when accounting exists
582
+ (§3.7, "empty versus unresolved").
583
+ """
584
+ hero = _combined_hero(state)
585
+ capability = state.capabilities.get("hero")
586
+ if capability is None or capability.status not in {"supported", "derived"}:
587
+ codes = (
588
+ _codex_hero_failure_codes(state) if provider == "codex"
589
+ else ("claude_cycle_unresolved",)
590
+ )
591
+ return None, tuple(
592
+ _CombinedCause(4, provider, code) for code in codes
593
+ )
594
+ cost = hero.get("cost_usd") if hero is not None else None
595
+ tokens = hero.get("total_tokens") if hero is not None else None
596
+ period = _leg_period(provider, hero)
597
+ if period is None and state.availability == "empty":
598
+ # Numeric zeros and no period, so nothing presents `$0` as observed
599
+ # spend inside a named cycle.
600
+ return {"state": "empty", "cost_usd": 0.0, "total_tokens": 0}, ()
601
+ if provider == "claude" and cost is None and tokens is None:
602
+ # Accounting exists but no subscription week resolved. Claude's hero
603
+ # capability stays `supported` in that state, so this is its own
604
+ # detection rather than the capability branch above.
605
+ return None, (_CombinedCause(4, provider, "claude_cycle_unresolved"),)
606
+ causes = tuple(
607
+ _CombinedCause(
608
+ 5, provider, "invalid_counter", {"field": field, "reason": reason},
609
+ )
610
+ for field, reason in (
611
+ ("cost_usd", _counter_reason(cost, integral=False)),
612
+ ("total_tokens", _counter_reason(tokens, integral=True)),
613
+ )
614
+ if reason is not None
615
+ )
616
+ if causes:
617
+ return None, causes
618
+ return {
619
+ "state": "current",
620
+ "cost_usd": float(cost),
621
+ "total_tokens": int(tokens),
622
+ **({"period": period} if period is not None else {}),
623
+ }, ()
624
+
625
+
626
+ def _combined_qualifications(
627
+ legs: Mapping[str, Mapping[str, object]],
628
+ claude: SourceDashboardState,
629
+ codex: SourceDashboardState,
630
+ ) -> tuple[Mapping[str, object], ...]:
631
+ """Notes that qualify a PUBLISHED figure (§4.3). Empty means omit the key."""
632
+ qualifications: list[Mapping[str, object]] = []
633
+ for provider in ("claude", "codex"):
634
+ if legs[provider].get("state") == "empty":
635
+ label = _PROVIDER_LABELS[provider]
636
+ qualifications.append({
637
+ "code": "provider_empty",
638
+ "message": f"{label} has no accounting in its current cycle.",
639
+ "provider": provider,
640
+ })
641
+ # public #5: the Codex ingest backlog is LIFTED here rather than read from
642
+ # the provider field by the All surfaces, so the figure and its disclosure
643
+ # cannot disagree. The provider field stays published for the Codex tab.
644
+ data = codex.data
645
+ backlog = data.get("ingest_backlog") if isinstance(data, Mapping) else None
646
+ if isinstance(backlog, Mapping) and backlog:
647
+ qualifications.append({
648
+ "code": "codex_ingest_backlog",
649
+ "message": (
650
+ "Codex has pending accounting to ingest, so its cycle total "
651
+ "may be incomplete."
652
+ ),
653
+ "provider": "codex",
654
+ })
655
+ return tuple(qualifications)
656
+
657
+
658
+ def _combined_outcome(
659
+ claude: SourceDashboardState,
660
+ codex: SourceDashboardState,
661
+ ) -> tuple[Mapping[str, object] | None, Mapping[str, object] | None]:
662
+ """Return ``(combined, combined_unavailable)`` — exactly one is not None.
663
+
664
+ Cause precedence (§3.7), first match wins, Claude before Codex at equal
665
+ precedence: provider incoherence, unresolved account scope, decoration, a
666
+ hero capability outside {supported, derived} reported as the provider's own
667
+ reason, then an invalid counter. Every cause found is listed, ordered so
668
+ that `causes[0]` is always the winner.
669
+ """
670
+ pairs: tuple[tuple[PhysicalSource, SourceDashboardState], ...] = (
671
+ ("claude", claude), ("codex", codex),
672
+ )
673
+ causes: list[_CombinedCause] = []
674
+ for provider, state in pairs:
675
+ if not _coherent_provider(state):
676
+ causes.append(_CombinedCause(1, provider, "provider_incoherent"))
677
+ for provider, state in pairs:
678
+ count = _real_account_count(state)
679
+ if count is None:
680
+ causes.append(
681
+ _CombinedCause(2, provider, "account_scope_unresolved"))
682
+ elif count > 1:
683
+ causes.append(_CombinedCause(
684
+ 3, provider, "multi_account_unsupported",
685
+ {"account_count": count},
686
+ ))
687
+ legs: dict[str, Mapping[str, object]] = {}
688
+ for provider, state in pairs:
689
+ if not _coherent_provider(state):
690
+ # An incoherent generation's data cannot be trusted to yield a leg
691
+ # OR a leg-level cause; precedence 1 already withholds the figure.
692
+ continue
693
+ leg, leg_causes = _combined_leg(state, provider)
694
+ causes.extend(leg_causes)
695
+ if leg is not None:
696
+ legs[provider] = leg
697
+ if causes:
698
+ ordered = sorted(
699
+ causes,
700
+ key=lambda cause: (
701
+ cause.precedence, 0 if cause.provider == "claude" else 1,
702
+ ),
703
+ )
704
+ return None, {
705
+ "code": ordered[0].code,
706
+ "message": _cause_message(ordered[0]),
707
+ "causes": tuple(
708
+ {
709
+ "provider": cause.provider,
710
+ "code": cause.code,
711
+ **({"detail": cause.detail} if cause.detail else {}),
712
+ }
713
+ for cause in ordered
714
+ ),
715
+ }
716
+ qualifications = _combined_qualifications(legs, claude, codex)
717
+ return {
718
+ "cost_usd": float(legs["claude"]["cost_usd"]) + float(legs["codex"]["cost_usd"]),
719
+ "total_tokens": int(legs["claude"]["total_tokens"]) + int(legs["codex"]["total_tokens"]),
720
+ "legs": legs,
721
+ **({"qualifications": qualifications} if qualifications else {}),
722
+ }, None
723
+
724
+
431
725
  def _combined_alert_rows(
432
726
  claude: SourceDashboardState,
433
727
  codex: SourceDashboardState,
@@ -461,35 +755,18 @@ def compose_all_state(
461
755
  """Compose provider-labeled sections without inventing blended semantics."""
462
756
  if claude.source != "claude" or codex.source != "codex":
463
757
  raise ValueError("all composition requires Claude and Codex provider states")
464
- combined = _combined_metrics(claude, codex)
758
+ combined, combined_unavailable = _combined_outcome(claude, codex)
465
759
  providers_coherent = _coherent_provider(claude) and _coherent_provider(codex)
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.
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
- ),)
480
760
  if providers_coherent:
481
- if stale_cycle_providers:
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
- )
761
+ availability: Availability = (
762
+ "partial"
763
+ if combined is None or "partial" in (claude.availability, codex.availability)
764
+ else (
765
+ "empty"
766
+ if claude.availability == "empty" and codex.availability == "empty"
767
+ else "ok"
492
768
  )
769
+ )
493
770
  freshness: Freshness = "fresh"
494
771
  else:
495
772
  availability = "partial"
@@ -506,22 +783,33 @@ def compose_all_state(
506
783
  source_domain_freshness(codex, domain)
507
784
  for domain in SOURCE_FRESHNESS_DOMAINS
508
785
  ],
509
- combined is not None,
786
+ # #556 S1: the WHOLE outcome, not merely whether one exists. The
787
+ # legs, their periods, the qualifications and the withheld cause
788
+ # are all published, and the account scope and hero counters that
789
+ # produce them are not otherwise in this material — so hashing only
790
+ # `combined is not None` would leave materially different All
791
+ # states sharing one `data_version` (invariant 6).
792
+ combined, combined_unavailable,
510
793
  ],
511
794
  separators=(",", ":"),
795
+ sort_keys=True,
512
796
  ).encode("utf-8")
513
797
  data_version = "all:" + hashlib.sha256(version_material).hexdigest()[:24]
514
- successes = (item for item in (claude.last_success_at, codex.last_success_at) if item is not None)
515
- last_success_at = min(successes, default=None)
798
+ successes = (claude.last_success_at, codex.last_success_at)
799
+ # §4.6: `None` unless BOTH providers have one, otherwise the older.
800
+ # Filtering `None` before `min` let one provider's success masquerade as
801
+ # All's while the client keys "no successful snapshot yet" on null.
802
+ last_success_at = None if None in successes else min(successes)
516
803
  return SourceDashboardState(
517
804
  source="all",
518
805
  availability=availability,
519
806
  freshness=freshness,
520
- # All-LOCAL warnings lead: `warningForSource` on the client falls back to
521
- # the FIRST warning, so a merely-partial provider warning (e.g.
522
- # `codex_metadata_incomplete`) would otherwise pre-empt the chip label
523
- # and hide the stale qualification on the combined actual.
524
- warnings=tuple((*all_local_warnings, *claude.warnings, *codex.warnings)),
807
+ # §4.2: the All-local `combined_totals_stale` warning is retired. The
808
+ # combined figure's own disclosure now travels in `data.combined`
809
+ # (`qualifications`) and `data.combined_unavailable`, which is typed and
810
+ # has provenance All flattens both providers' warnings into one tuple
811
+ # with no provenance field, so warning order could never carry it.
812
+ warnings=tuple((*claude.warnings, *codex.warnings)),
525
813
  data_version=data_version,
526
814
  last_success_at=last_success_at,
527
815
  capabilities={
@@ -532,6 +820,9 @@ def compose_all_state(
532
820
  },
533
821
  data={
534
822
  "combined": combined,
823
+ # Emitted iff the figure is withheld; omitted-when-inapplicable.
824
+ **({"combined_unavailable": combined_unavailable}
825
+ if combined is None else {}),
535
826
  "alerts": {"rows": _combined_alert_rows(claude, codex)},
536
827
  "providers": {
537
828
  "claude": claude.data,
@@ -27,6 +27,20 @@ import hashlib
27
27
  RETAINED_RECORD_TYPES = frozenset({"evt", "correction", "correction_batch", "op"})
28
28
 
29
29
 
30
+ def selector_slot(record):
31
+ """Return one position-preserving input slot for the shared selector.
32
+
33
+ Every successfully decoded journal line consumes one sequence number in
34
+ ``resolve_effective_events``. Decision records therefore stay decoded,
35
+ while observations and other irrelevant records become ``None`` rather
36
+ than being dropped (which renumbers durable protocol fingerprints) or kept
37
+ as dictionaries (which makes memory follow observation volume).
38
+ """
39
+ if record.get("t") in RETAINED_RECORD_TYPES:
40
+ return record
41
+ return None
42
+
43
+
30
44
  class LastSeenAccumulator:
31
45
  """Reproduce `_derive_account_last_seen`'s contribution set from a stream.
32
46