cctally 1.104.0 → 1.105.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +6 -6
  3. package/bin/_cctally_alerts.py +46 -13
  4. package/bin/_cctally_cache.py +193 -71
  5. package/bin/_cctally_dashboard.py +683 -70
  6. package/bin/_cctally_dashboard_conversation.py +400 -6
  7. package/bin/_cctally_dashboard_envelope.py +17 -0
  8. package/bin/_cctally_dashboard_perf.py +93 -0
  9. package/bin/_cctally_dashboard_sources.py +988 -94
  10. package/bin/_cctally_db.py +30 -3
  11. package/bin/_cctally_diagnosis_sources.py +699 -186
  12. package/bin/_cctally_doctor.py +22 -8
  13. package/bin/_cctally_forecast.py +17 -9
  14. package/bin/_cctally_journal.py +298 -40
  15. package/bin/_cctally_quota.py +15 -0
  16. package/bin/_cctally_quota_model.py +584 -58
  17. package/bin/_cctally_record.py +133 -67
  18. package/bin/_cctally_tui.py +317 -70
  19. package/bin/_lib_codex_conversation_query.py +258 -12
  20. package/bin/_lib_codex_hooks.py +26 -0
  21. package/bin/_lib_conversation_query.py +145 -15
  22. package/bin/_lib_dashboard_json.py +105 -0
  23. package/bin/_lib_diagnosis.py +21 -2
  24. package/bin/_lib_doctor.py +64 -8
  25. package/bin/_lib_ingest_frontier.py +889 -0
  26. package/bin/_lib_meter_rate_change.py +95 -29
  27. package/bin/_lib_perf.py +22 -0
  28. package/bin/_lib_pricing.py +30 -3
  29. package/bin/_lib_quota_copy.py +30 -4
  30. package/bin/_lib_quota_model.py +215 -28
  31. package/bin/_lib_rate_change_delivery.py +119 -0
  32. package/bin/_lib_record.py +50 -0
  33. package/bin/_lib_retained_size.py +187 -0
  34. package/bin/_lib_snapshot_cache.py +504 -17
  35. package/bin/_lib_statusline.py +3 -29
  36. package/bin/_lib_tick_stats.py +28 -4
  37. package/dashboard/static/assets/ConversationsView-DA4j7Yso.js +72 -0
  38. package/dashboard/static/assets/DoctorModal-D4PbVnbE.js +1 -0
  39. package/dashboard/static/assets/ModalRoot-D4oSPxPp.js +1 -0
  40. package/dashboard/static/assets/ProjectsDrillPanel-DkPlB9JH.js +1 -0
  41. package/dashboard/static/assets/SourceDetailModal-BE7FONsS.js +1 -0
  42. package/dashboard/static/assets/UpdateModal-BJuI8glf.js +7 -0
  43. package/dashboard/static/assets/index-w_bINkJ8.js +13 -0
  44. package/dashboard/static/assets/outlineNavigation-zDVm6Hdd.js +9 -0
  45. package/dashboard/static/assets/useKeymap-CJ-Pi17D.js +1 -0
  46. package/dashboard/static/dashboard.html +2 -1
  47. package/package.json +4 -1
  48. package/dashboard/static/assets/index-D19TO7Mg.js +0 -97
@@ -37,18 +37,24 @@ from __future__ import annotations
37
37
  import datetime as dt
38
38
  import hashlib
39
39
  import json
40
+ import os
41
+ import pickle
40
42
  import re
41
43
  import shlex
42
44
  import sqlite3
43
45
  import sys
46
+ import threading
47
+ from collections import defaultdict
44
48
  from dataclasses import dataclass, field
49
+ from functools import cached_property
45
50
  from types import MappingProxyType
46
- from typing import Any, Collection, Iterable, Mapping, Sequence
51
+ from typing import Any, Collection, Iterable, Mapping, NamedTuple, Sequence
47
52
 
48
53
  import _cctally_core
49
54
  import _lib_accounts
50
55
  import _lib_codex_pools
51
56
  import _lib_diagnosis as kernel
57
+ import _lib_pricing
52
58
  from _lib_diagnosis import (
53
59
  ClassResult, ContributorSpec, Denominator, DiagnosisWindow,
54
60
  EstablishmentError, EstablishmentFailure, PopulationCoverage,
@@ -181,11 +187,18 @@ def _parse_ts(raw: object) -> dt.datetime | None:
181
187
  if not isinstance(raw, str) or not raw:
182
188
  return None
183
189
  try:
184
- parsed = dt.datetime.fromisoformat(raw.replace("Z", "+00:00"))
190
+ if raw.endswith("Z"):
191
+ # Codex's canonical store spelling. Avoid allocating a replaced
192
+ # string and normalizing an offset that is already known to be
193
+ # UTC on every accounting row in the hot scan.
194
+ return dt.datetime.fromisoformat(raw[:-1]).replace(tzinfo=UTC)
195
+ parsed = dt.datetime.fromisoformat(raw)
185
196
  except ValueError:
186
197
  return None
187
198
  if parsed.tzinfo is None:
188
- parsed = parsed.replace(tzinfo=UTC)
199
+ return parsed.replace(tzinfo=UTC)
200
+ if parsed.tzinfo is UTC:
201
+ return parsed
189
202
  return parsed.astimezone(UTC)
190
203
 
191
204
 
@@ -304,11 +317,25 @@ def _digest_component_pair(current_rows: Iterable[Sequence[Any]],
304
317
  current-window and its baseline-window row streams, and the two are
305
318
  domain-separated so a row moving between them is visible.
306
319
  """
320
+ return _digest_component_pair_from_digests(
321
+ _digest_rows(current_rows), _digest_rows(baseline_rows or ()))
322
+
323
+
324
+ def _digest_component_pair_from_digests(
325
+ current_digest: str, baseline_digest: str | None) -> str:
326
+ """Bind two already-established component digests without rereading.
327
+
328
+ `_establish` computes each digest while its component is protected by the
329
+ before/after probe pair. Re-serializing the full accounting population at
330
+ publication time added a second 150K-row hash pass without observing any
331
+ new fact. Domain-separating those same subdigests is byte-identical to the
332
+ original row-stream helper above.
333
+ """
307
334
  digest = hashlib.sha256()
308
335
  digest.update(b"current=")
309
- digest.update(_digest_rows(current_rows).encode())
336
+ digest.update(current_digest.encode())
310
337
  digest.update(b"\x1dbaseline=")
311
- digest.update(_digest_rows(baseline_rows or ()).encode())
338
+ digest.update((baseline_digest or _digest_rows(())).encode())
312
339
  return digest.hexdigest()
313
340
 
314
341
 
@@ -443,18 +470,28 @@ def _digest_configuration(scope: DiagnosisScope) -> tuple[list, Any]:
443
470
 
444
471
 
445
472
  def _digest_rows(rows: Iterable[Sequence[Any]]) -> str:
473
+ """Digest a component's already-materialized primitive row stream.
474
+
475
+ Protocol 5 is deterministic for these tuples of SQLite/JSON primitives
476
+ and frames the values without delimiter collisions. Feeding the pickler
477
+ directly into SHA avoids both the per-cell Python string loop and a second
478
+ serialized copy proportional to the accounting population.
479
+ """
446
480
  digest = hashlib.sha256()
447
- for row in rows:
448
- digest.update(b"\x1e")
449
- digest.update("\x1f".join("" if v is None else str(v)
450
- for v in row).encode())
481
+
482
+ class _DigestWriter:
483
+ def write(self, data: bytes) -> int:
484
+ digest.update(data)
485
+ return len(data)
486
+
487
+ payload = rows if isinstance(rows, (list, tuple)) else tuple(rows)
488
+ pickle.Pickler(_DigestWriter(), protocol=5).dump(payload)
451
489
  return digest.hexdigest()
452
490
 
453
491
 
454
492
  # --- raw accounting facts ----------------------------------------------
455
493
 
456
- @dataclass(frozen=True)
457
- class AccountingEntry:
494
+ class AccountingEntry(NamedTuple):
458
495
  """One priced accounting row, provider-neutral for the fold."""
459
496
 
460
497
  timestamp: dt.datetime
@@ -531,6 +568,16 @@ def _native_block_key(root_key: str, pool: str | None,
531
568
  return f"{root_key}|{pool or 'standard'}|{_iso_z(start_at)}"
532
569
 
533
570
 
571
+ @dataclass(frozen=True)
572
+ class _PopulationIndex:
573
+ total_usd: float
574
+ observed_range: tuple[dt.datetime | None, dt.datetime | None]
575
+ coverage_counts: Mapping[str, int]
576
+ grouped_entries: Mapping[
577
+ str, tuple[dict[str, list[AccountingEntry]], dict[str, str]]]
578
+ population_digest: str
579
+
580
+
534
581
  @dataclass(frozen=True)
535
582
  class RawFacts:
536
583
  entries: tuple[AccountingEntry, ...] = ()
@@ -542,12 +589,89 @@ class RawFacts:
542
589
  # past the window start from an install that simply did not exist then.
543
590
  store_horizon: dt.datetime | None = None
544
591
  unavailable_cause: str | None = None
592
+ population_digest_override: str | None = field(
593
+ default=None, repr=False, compare=False)
594
+
595
+ @cached_property
596
+ def _index(self) -> _PopulationIndex:
597
+ """Derive every provider-wide fold in one population traversal."""
598
+ grouped = {kind: defaultdict(list)
599
+ for kind in ("model", "project", "session")}
600
+ labels = {kind: {} for kind in grouped}
601
+ model_groups = grouped["model"]
602
+ project_groups = grouped["project"]
603
+ session_groups = grouped["session"]
604
+ model_labels = labels["model"]
605
+ project_labels = labels["project"]
606
+ session_labels = labels["session"]
607
+ costs: list[float] = []
608
+ digest = (None if self.population_digest_override is not None
609
+ else hashlib.sha256())
610
+ low: dt.datetime | None = None
611
+ high: dt.datetime | None = None
612
+ model_count = project_count = session_count = priced_count = 0
613
+ for entry in self.entries:
614
+ costs.append(entry.cost_usd)
615
+ if low is None or entry.timestamp < low:
616
+ low = entry.timestamp
617
+ if high is None or entry.timestamp > high:
618
+ high = entry.timestamp
619
+ model_count += bool(entry.model)
620
+ project_count += bool(entry.project_identity_resolved)
621
+ session_count += bool(entry.session_identity_resolved)
622
+ priced_count += not entry.is_fallback_pricing
623
+ if digest is not None:
624
+ digest.update((f"{_iso_z(entry.timestamp)}|{entry.model}|"
625
+ f"{entry.session_key}|"
626
+ f"{entry.cost_usd!r}").encode())
627
+ model_key = entry.model or "(unknown)"
628
+ if model_key not in model_groups:
629
+ model_labels[model_key] = model_key
630
+ model_groups[model_key].append(entry)
631
+ if entry.project_key not in project_groups:
632
+ project_labels[entry.project_key] = entry.project_label
633
+ project_groups[entry.project_key].append(entry)
634
+ if entry.session_key not in session_groups:
635
+ session_labels[entry.session_key] = entry.session_label
636
+ session_groups[entry.session_key].append(entry)
637
+ return _PopulationIndex(
638
+ total_usd=stable_sum(costs),
639
+ observed_range=(low, high),
640
+ coverage_counts=MappingProxyType({
641
+ "model": model_count,
642
+ "project": project_count,
643
+ "session": session_count,
644
+ "block": len(costs),
645
+ "priced": priced_count,
646
+ }),
647
+ grouped_entries=MappingProxyType({
648
+ kind: (grouped[kind], labels[kind]) for kind in grouped
649
+ }),
650
+ population_digest=(self.population_digest_override
651
+ if self.population_digest_override is not None
652
+ else digest.hexdigest()[:32]),
653
+ )
545
654
 
546
655
  @property
547
656
  def total_usd(self) -> float:
548
- # `stable_sum` at every fold layer: `golden-json.txt` prints full float
549
- # repr values, so a summation difference is a golden difference.
550
- return stable_sum(e.cost_usd for e in self.entries)
657
+ return self._index.total_usd
658
+
659
+ @property
660
+ def observed_range(self) -> tuple[dt.datetime | None, dt.datetime | None]:
661
+ return self._index.observed_range
662
+
663
+ @property
664
+ def coverage_counts(self) -> Mapping[str, int]:
665
+ return self._index.coverage_counts
666
+
667
+ @property
668
+ def grouped_entries(self) -> Mapping[
669
+ str, tuple[dict[str, list[AccountingEntry]], dict[str, str]]]:
670
+ return self._index.grouped_entries
671
+
672
+ @property
673
+ def population_digest(self) -> str:
674
+ return self._index.population_digest
551
675
 
552
676
 
553
677
  # The "not yet asked" sentinel for the memoized schema-gap answer, because
@@ -587,6 +711,7 @@ class StoreBundle:
587
711
  # during establishment when the plan opened the conversations store,
588
712
  # so the digest and the rows describe one evaluation rather than two.
589
713
  self.s3_evaluations: dict | None = None
714
+ self.claude_window_sessions: tuple[str, ...] | None = None
590
715
  # Each S3 class's aggregate qualifying-cost share over the immediately
591
716
  # preceding equal-duration window. Absent means the baseline could not
592
717
  # be established, which is `baseline_insufficient`.
@@ -857,16 +982,12 @@ def _read_claude_blocks(scope: DiagnosisScope,
857
982
  # --- Codex accounting ---------------------------------------------------
858
983
 
859
984
  _CODEX_ENTRIES_SQL = """
860
- SELECT entries.timestamp_utc, entries.session_id, entries.source_path,
985
+ SELECT entries.timestamp_utc, entries.source_path,
861
986
  entries.source_root_key, entries.conversation_key, entries.model,
862
987
  entries.account_key, entries.input_tokens,
863
988
  entries.cached_input_tokens, entries.output_tokens,
864
- entries.reasoning_output_tokens, entries.total_tokens,
865
- threads.cwd, threads.git_json
989
+ entries.reasoning_output_tokens
866
990
  FROM codex_session_entries AS entries
867
- LEFT JOIN codex_conversation_threads AS threads
868
- ON threads.conversation_key = entries.conversation_key
869
- AND threads.source_root_key = entries.source_root_key
870
991
  WHERE entries.timestamp_utc >= ? AND entries.timestamp_utc < ?
871
992
  """
872
993
 
@@ -878,16 +999,12 @@ _CODEX_ENTRIES_SQL = """
878
999
  # per-turn capacity. `source_root_key` and `conversation_key` — the fan-out
879
1000
  # join keys — are already in the frozen projection.
880
1001
  _CODEX_ENTRIES_S3_SQL = """
881
- SELECT entries.timestamp_utc, entries.session_id, entries.source_path,
1002
+ SELECT entries.timestamp_utc, entries.source_path,
882
1003
  entries.source_root_key, entries.conversation_key, entries.model,
883
1004
  entries.account_key, entries.input_tokens,
884
1005
  entries.cached_input_tokens, entries.output_tokens,
885
- entries.reasoning_output_tokens, entries.total_tokens,
886
- threads.cwd, threads.git_json, entries.line_offset
1006
+ entries.reasoning_output_tokens, entries.line_offset
887
1007
  FROM codex_session_entries AS entries
888
- LEFT JOIN codex_conversation_threads AS threads
889
- ON threads.conversation_key = entries.conversation_key
890
- AND threads.source_root_key = entries.source_root_key
891
1008
  WHERE entries.timestamp_utc >= ? AND entries.timestamp_utc < ?
892
1009
  """
893
1010
 
@@ -897,9 +1014,71 @@ _CODEX_RETENTION_SQL = """
897
1014
  """
898
1015
 
899
1016
 
1017
+ def _compiled_codex_pricer(model: str, speed: str):
1018
+ """Resolve one model's canonical pricing once for the hot range scan.
1019
+
1020
+ The returned function preserves `_calculate_codex_entry_cost`'s exact
1021
+ arithmetic and warning contract. It only removes repeated model aliases,
1022
+ dictionary lookups, rate extraction and fast-tier resolution from every
1023
+ token event in the same diagnosis window.
1024
+ """
1025
+ pricing, is_fallback = _resolve_codex_pricing(model)
1026
+ if pricing is None:
1027
+ _lib_pricing._warn_unknown_codex_model(model)
1028
+ return (lambda *_tokens: 0.0), is_fallback
1029
+ if is_fallback:
1030
+ _lib_pricing._warn_unknown_codex_model(model)
1031
+
1032
+ threshold = _lib_pricing.CODEX_TIERED_THRESHOLD
1033
+ input_rate = pricing.get("input_cost_per_token", 0.0)
1034
+ input_tier = pricing.get("input_cost_per_token_above_272k_tokens")
1035
+ cache_rate = pricing.get("cache_read_input_token_cost", 0.0)
1036
+ cache_tier = pricing.get(
1037
+ "cache_read_input_token_cost_above_272k_tokens")
1038
+ output_rate = pricing.get("output_cost_per_token", 0.0)
1039
+ output_tier = pricing.get("output_cost_per_token_above_272k_tokens")
1040
+ multiplier = (_lib_pricing._codex_fast_multiplier(model)
1041
+ if speed == "fast" else 1.0)
1042
+
1043
+ def _price(input_tokens: int, cached_input_tokens: int,
1044
+ output_tokens: int,
1045
+ reasoning_output_tokens: int) -> float:
1046
+ del reasoning_output_tokens
1047
+ non_cached_input = max(0, input_tokens - cached_input_tokens)
1048
+ if non_cached_input <= 0 or not input_rate:
1049
+ input_cost = 0.0
1050
+ elif non_cached_input > threshold and input_tier is not None:
1051
+ input_cost = (threshold * input_rate
1052
+ + (non_cached_input - threshold) * input_tier)
1053
+ else:
1054
+ input_cost = non_cached_input * input_rate
1055
+ if cached_input_tokens <= 0 or not cache_rate:
1056
+ cached_input_cost = 0.0
1057
+ elif cached_input_tokens > threshold and cache_tier is not None:
1058
+ cached_input_cost = (
1059
+ threshold * cache_rate
1060
+ + (cached_input_tokens - threshold) * cache_tier)
1061
+ else:
1062
+ cached_input_cost = cached_input_tokens * cache_rate
1063
+ if output_tokens <= 0 or not output_rate:
1064
+ output_cost = 0.0
1065
+ elif output_tokens > threshold and output_tier is not None:
1066
+ output_cost = (threshold * output_rate
1067
+ + (output_tokens - threshold) * output_tier)
1068
+ else:
1069
+ output_cost = output_tokens * output_rate
1070
+ base = input_cost + cached_input_cost + output_cost
1071
+ if speed == "fast":
1072
+ base *= multiplier
1073
+ return base
1074
+
1075
+ return _price, is_fallback
1076
+
1077
+
900
1078
  def _read_codex_entries(scope: DiagnosisScope, conn: sqlite3.Connection,
901
1079
  plan: "kernel.ExecutionPlan"
902
- ) -> tuple[list[AccountingEntry], list[tuple]]:
1080
+ ) -> tuple[
1081
+ list[AccountingEntry], list[tuple], list]:
903
1082
  params: list[Any] = [_iso_sql(scope.window_start), _iso_sql(scope.window_end)]
904
1083
  expanded = plan.requires_s3_projection()
905
1084
  # Every Codex query carries `account_key` (#341 / #373). A Codex read that
@@ -910,55 +1089,86 @@ def _read_codex_entries(scope: DiagnosisScope, conn: sqlite3.Connection,
910
1089
  " entries.conversation_key ASC, entries.id ASC")
911
1090
  rows = _execute(conn, sql, params)
912
1091
 
1092
+ # Thread metadata is conversation-shaped, not entry-shaped. Joining it
1093
+ # onto the hot accounting range repeats the same cwd/git/topology strings
1094
+ # once per token event (150,000 times in the acceptance corpus) before
1095
+ # Python immediately deduplicates them again. Read the scoped thread set
1096
+ # once under this component's same probe pair and reuse it for project
1097
+ # attribution, the per-entry digest, and the fan-out evaluator.
1098
+ scoped = sorted({str(row["conversation_key"]) for row in rows
1099
+ if row["conversation_key"]})
1100
+ thread_rows = _read_codex_threads(conn, scoped)
1101
+ threads = {str(row["conversation_key"]): row for row in thread_rows}
1102
+
913
1103
  c = _cctally()
914
1104
  speed = scope.effective_speed or "standard"
1105
+ pricers: dict[str, tuple[Any, bool]] = {}
915
1106
  resolver_cache: dict[str, Any] = {}
1107
+ projects: dict[str, tuple[str, str, bool]] = {}
1108
+ for conversation_key, thread in threads.items():
1109
+ cwd = thread["cwd"]
1110
+ if isinstance(cwd, str) and cwd:
1111
+ project = c._resolve_project_key(cwd, "git-root", resolver_cache)
1112
+ projects[conversation_key] = (
1113
+ project.bucket_path, project.display_key,
1114
+ not project.is_unknown,
1115
+ )
1116
+ else:
1117
+ projects[conversation_key] = ("(unassigned)", "(unassigned)",
1118
+ False)
1119
+ pools: dict[str, str | None] = {}
916
1120
  entries: list[AccountingEntry] = []
917
1121
  digest_rows: list[tuple] = []
918
1122
  for row in rows:
919
1123
  timestamp = _parse_ts(row["timestamp_utc"])
920
1124
  if timestamp is None:
921
1125
  continue
922
- model = str(row["model"] or "")
923
- cost = c._calculate_codex_entry_cost(
924
- model, int(row["input_tokens"] or 0),
925
- int(row["cached_input_tokens"] or 0),
926
- int(row["output_tokens"] or 0),
927
- int(row["reasoning_output_tokens"] or 0),
928
- speed=speed,
1126
+ model = row["model"] or ""
1127
+ input_tokens = int(row["input_tokens"] or 0)
1128
+ cached_input_tokens = int(row["cached_input_tokens"] or 0)
1129
+ output_tokens = int(row["output_tokens"] or 0)
1130
+ reasoning_output_tokens = int(row["reasoning_output_tokens"] or 0)
1131
+ compiled = pricers.get(model)
1132
+ if compiled is None:
1133
+ compiled = _compiled_codex_pricer(model, speed)
1134
+ pricers[model] = compiled
1135
+ pricer, is_fallback = compiled
1136
+ cost = pricer(
1137
+ input_tokens, cached_input_tokens, output_tokens,
1138
+ reasoning_output_tokens,
929
1139
  )
930
- _pricing, is_fallback = _resolve_codex_pricing(model)
931
- cwd = row["cwd"]
932
- if isinstance(cwd, str) and cwd:
933
- project = c._resolve_project_key(cwd, "git-root", resolver_cache)
934
- project_key, project_label = project.bucket_path, project.display_key
935
- project_resolved = not project.is_unknown
936
- else:
937
- # Absent Codex project metadata reduces identityCoverage
938
- # explicitly and never basename-merges distinct git roots.
939
- project_key = project_label = "(unassigned)"
940
- project_resolved = False
941
- conversation_key = str(row["conversation_key"] or "")
1140
+ conversation_key = row["conversation_key"] or ""
1141
+ source_path = row["source_path"] or ""
1142
+ root_key = row["source_root_key"] or ""
1143
+ thread = threads.get(conversation_key)
1144
+ cwd = thread["cwd"] if thread is not None else None
1145
+ # Project identity is conversation metadata, so resolve it once per
1146
+ # thread rather than once per token event. Absent metadata still
1147
+ # reduces identityCoverage and never basename-merges git roots.
1148
+ project_key, project_label, project_resolved = projects.get(
1149
+ conversation_key, ("(unassigned)", "(unassigned)", False))
1150
+ if model not in pools:
1151
+ pools[model] = _lib_codex_pools.codex_model_scoped_quota_pool(model)
942
1152
  entries.append(AccountingEntry(
943
1153
  timestamp=timestamp,
944
1154
  model=model,
945
1155
  project_key=project_key,
946
1156
  project_label=project_label,
947
- session_key=conversation_key or str(row["source_path"] or "(unknown)"),
1157
+ session_key=conversation_key or source_path or "(unknown)",
948
1158
  session_label=conversation_key or "(unknown)",
949
- root_key=str(row["source_root_key"] or ""),
950
- pool=_lib_codex_pools.codex_model_scoped_quota_pool(model),
1159
+ root_key=root_key,
1160
+ pool=pools[model],
951
1161
  cost_usd=cost,
952
- input_tokens=int(row["input_tokens"] or 0),
953
- output_tokens=int(row["output_tokens"] or 0),
954
- cache_read_tokens=int(row["cached_input_tokens"] or 0),
1162
+ input_tokens=input_tokens,
1163
+ output_tokens=output_tokens,
1164
+ cache_read_tokens=cached_input_tokens,
955
1165
  is_fallback_pricing=bool(is_fallback),
956
1166
  project_identity_resolved=project_resolved,
957
1167
  session_identity_resolved=bool(conversation_key),
958
1168
  # The Codex legacy fallback prices the entry, so the model is
959
1169
  # qualified rather than unpriceable.
960
1170
  pricing_resolved=True,
961
- source_path=str(row["source_path"] or ""),
1171
+ source_path=source_path,
962
1172
  line_offset=(row["line_offset"] if expanded else None),
963
1173
  ))
964
1174
  digest_row = (
@@ -966,12 +1176,11 @@ def _read_codex_entries(scope: DiagnosisScope, conn: sqlite3.Connection,
966
1176
  row["conversation_key"], model, row["account_key"],
967
1177
  row["input_tokens"], row["cached_input_tokens"],
968
1178
  row["output_tokens"], row["reasoning_output_tokens"],
969
- row["cwd"], row["git_json"],
970
1179
  )
971
1180
  if expanded:
972
1181
  digest_row = digest_row + (row["source_path"], row["line_offset"])
973
1182
  digest_rows.append(digest_row)
974
- return entries, digest_rows
1183
+ return entries, digest_rows, thread_rows
975
1184
 
976
1185
 
977
1186
  _CODEX_BLOCKS_SQL = """
@@ -1059,18 +1268,16 @@ def _read_cache_component(scope: DiagnosisScope,
1059
1268
  thread_rows: list = []
1060
1269
  try:
1061
1270
  if scope.source == "codex":
1062
- entries, digest_rows = _read_codex_entries(scope, conn, bundle.plan)
1271
+ entries, digest_rows, thread_rows = (
1272
+ _read_codex_entries(scope, conn, bundle.plan)
1273
+ )
1063
1274
  retention_sql = _CODEX_RETENTION_SQL
1064
- if bundle.plan.requires_s3_projection():
1065
- # The Codex fan-out signal lives in THIS store, so its rows
1066
- # belong to THIS component's digest. Read once here and handed
1067
- # to the evaluator: read only in the evaluator they would move
1068
- # a published verdict while the identifier stood still, and
1069
- # read in both places the digest could describe a different
1070
- # state from the answer.
1071
- thread_rows = _read_codex_threads(conn, entries)
1072
- digest_rows = list(digest_rows) + _codex_thread_digest_rows(
1073
- thread_rows)
1275
+ # Thread metadata drives project attribution on every plan and
1276
+ # fan-out on the expanded plan. Digest each conversation-shaped
1277
+ # fact once here instead of repeating cwd/git strings for every
1278
+ # token event in that conversation.
1279
+ digest_rows = list(digest_rows) + _codex_thread_digest_rows(
1280
+ thread_rows)
1074
1281
  else:
1075
1282
  entries, digest_rows = _read_claude_entries(scope, conn, bundle.plan)
1076
1283
  retention_sql = _CLAUDE_RETENTION_SQL
@@ -1106,7 +1313,7 @@ def _read_cache_component(scope: DiagnosisScope,
1106
1313
 
1107
1314
 
1108
1315
  def _read_codex_threads(conn: sqlite3.Connection,
1109
- entries: Sequence[AccountingEntry]) -> list:
1316
+ scoped: Sequence[str]) -> list:
1110
1317
  """The thread rows the Codex fan-out predicate resolves over.
1111
1318
 
1112
1319
  Seeded from the scoped in-window accounting keys, never by walking the
@@ -1119,8 +1326,6 @@ def _read_codex_threads(conn: sqlite3.Connection,
1119
1326
  row handed in twice presents itself as two matches under the complete
1120
1327
  identity, which is exactly the condition that makes a child unallocated.
1121
1328
  """
1122
- scoped = sorted({entry.session_key for entry in entries
1123
- if entry.session_key})
1124
1329
  if not scoped:
1125
1330
  return []
1126
1331
  rows: dict[str, Any] = {}
@@ -1147,7 +1352,7 @@ def _codex_thread_digest_rows(thread_rows: Sequence[Any]) -> list[tuple]:
1147
1352
  (str(row["conversation_key"]), str(row["source_root_key"] or ""),
1148
1353
  str(row["native_thread_id"] or ""),
1149
1354
  str(row["root_thread_id"] or ""),
1150
- str(row["parent_thread_id"] or ""),
1355
+ str(row["parent_thread_id"] or ""), row["cwd"], row["git_json"],
1151
1356
  row["context_window"])
1152
1357
  for row in thread_rows
1153
1358
  ]
@@ -1714,35 +1919,58 @@ _CODEX_EVENT_COLUMNS = (
1714
1919
  # entry. The budget is allocated per source file so no file starves another,
1715
1920
  # and a file that exhausts its share yields NO turn map at all — a truncated
1716
1921
  # read would produce a wrong map rather than a missing one.
1717
- _CODEX_EVENTS_SQL = """
1718
- SELECT source_path, line_offset, source_root_key, conversation_key,
1719
- native_thread_id, root_thread_id, parent_thread_id, timestamp_utc,
1720
- record_type, event_type, turn_id, call_id, payload_json, rank FROM (
1721
- SELECT source_path, line_offset, source_root_key, conversation_key,
1722
- native_thread_id, root_thread_id, parent_thread_id,
1723
- timestamp_utc, record_type, event_type, turn_id, call_id,
1724
- payload_json,
1725
- ROW_NUMBER() OVER (PARTITION BY source_path
1726
- ORDER BY line_offset ASC) AS rank
1727
- FROM codex_conversation_events
1728
- WHERE source_path IN ({placeholders})
1922
+ _CODEX_EVENT_FILTER = """
1729
1923
  AND (record_type IN ('session_meta', 'turn_context')
1730
1924
  OR turn_id IS NOT NULL
1731
1925
  OR event_type = 'token_count')
1732
- ) WHERE rank <= ?
1926
+ """
1927
+
1928
+ # Read at most ONE row beyond each file's allocated share. A grouped COUNT
1929
+ # scans every token event even though the only question is whether the file
1930
+ # exceeds its cap. Repeating this term under one `UNION ALL` keeps the probe
1931
+ # set-wise while letting the source-path/offset index stop each arm at its
1932
+ # first conclusive row. The decision remains exact: `share + 1` rows means the
1933
+ # whole file is starved, while at most `share` means every relevant row fit.
1934
+ _CODEX_EVENT_BUDGET_TERM_SQL = """
1935
+ SELECT ? AS source_path, COUNT(*) AS rows_read
1936
+ FROM (
1937
+ SELECT 1
1938
+ FROM codex_conversation_events
1939
+ WHERE source_path = ?
1940
+ """ + _CODEX_EVENT_FILTER + """
1941
+ ORDER BY line_offset ASC
1942
+ LIMIT ?
1943
+ )
1944
+ """
1945
+
1946
+ # Three bindings per compound arm stay below SQLite's historical 999-variable
1947
+ # ceiling, and below its 500-term compound-select ceiling too.
1948
+ _CODEX_EVENT_BUDGET_CHUNK = 300
1949
+
1950
+ # Only lifecycle anchors can change the inferred turn. Accounting offsets are
1951
+ # merged against these rows in Python, so token_count payloads never cross the
1952
+ # SQLite boundary merely to inherit the current turn.
1953
+ _CODEX_EVENT_ANCHORS_SQL = """
1954
+ SELECT source_path, line_offset, source_root_key, conversation_key,
1955
+ native_thread_id, root_thread_id, parent_thread_id, timestamp_utc,
1956
+ record_type, event_type, turn_id, call_id, payload_json
1957
+ FROM codex_conversation_events
1958
+ WHERE source_path IN ({placeholders})
1959
+ AND (record_type IN ('session_meta', 'turn_context')
1960
+ OR turn_id IS NOT NULL)
1733
1961
  ORDER BY source_path ASC, line_offset ASC
1734
1962
  """
1735
1963
 
1736
1964
  _CODEX_THREADS_BY_KEY_SQL = """
1737
1965
  SELECT conversation_key, source_root_key, native_thread_id,
1738
- root_thread_id, parent_thread_id, context_window
1966
+ root_thread_id, parent_thread_id, cwd, git_json, context_window
1739
1967
  FROM codex_conversation_threads
1740
1968
  WHERE conversation_key IN ({placeholders})
1741
1969
  """
1742
1970
 
1743
1971
  _CODEX_THREADS_BY_NATIVE_SQL = """
1744
1972
  SELECT conversation_key, source_root_key, native_thread_id,
1745
- root_thread_id, parent_thread_id, context_window
1973
+ root_thread_id, parent_thread_id, cwd, git_json, context_window
1746
1974
  FROM codex_conversation_threads
1747
1975
  WHERE native_thread_id IN ({placeholders})
1748
1976
  """
@@ -1784,7 +2012,8 @@ _CONVERSATIONS_STATEMENTS: dict[str, tuple[str, ...]] = {
1784
2012
  ),
1785
2013
  "codex": (
1786
2014
  _CODEX_PROMPT_CANDIDATE_SQL,
1787
- _CODEX_EVENTS_SQL,
2015
+ _CODEX_EVENT_BUDGET_TERM_SQL,
2016
+ _CODEX_EVENT_ANCHORS_SQL,
1788
2017
  ),
1789
2018
  }
1790
2019
 
@@ -2070,6 +2299,25 @@ def _fanout_gaps(unallocated: Sequence[Any],
2070
2299
 
2071
2300
  # --- 2.2 prompt-cache churn, Claude only --------------------------------
2072
2301
 
2302
+ def _claude_window_session_ids(scope: DiagnosisScope, bundle: StoreBundle,
2303
+ conversations: sqlite3.Connection
2304
+ ) -> tuple[str, ...]:
2305
+ """The shared conversation population for this provider/window bundle."""
2306
+ if bundle.claude_window_sessions is None:
2307
+ # Seed and normalization budgets are divided over the COMPLETE
2308
+ # transcript population, not only sessions carrying priced entries.
2309
+ # A transcript-only session can therefore change another session's
2310
+ # share even though it can never contribute a ranked dollar. Replacing
2311
+ # this set with the accounting population changes typed withholding,
2312
+ # so the complete range scan is part of the evidence contract.
2313
+ bounds = [_iso_sql(scope.window_start), _iso_sql(scope.window_end)]
2314
+ bundle.claude_window_sessions = tuple(sorted(
2315
+ str(row[0]) for row in _execute(
2316
+ conversations, _CLAUDE_WINDOW_SESSIONS_SQL, bounds)
2317
+ ))
2318
+ return bundle.claude_window_sessions
2319
+
2320
+
2073
2321
  def _evaluate_cache_churn(scope: DiagnosisScope, bundle: StoreBundle,
2074
2322
  conversations: sqlite3.Connection) -> _S3Evaluation:
2075
2323
  """Flag every in-window turn that re-created the bulk of its cached prefix.
@@ -2085,16 +2333,25 @@ def _evaluate_cache_churn(scope: DiagnosisScope, bundle: StoreBundle,
2085
2333
  query = _conversation_query()
2086
2334
  facts = bundle.facts
2087
2335
  bounds = [_iso_sql(scope.window_start), _iso_sql(scope.window_end)]
2088
- sessions = sorted(
2089
- str(row[0]) for row in
2090
- _execute(conversations, _CLAUDE_WINDOW_SESSIONS_SQL, bounds)
2091
- )
2092
- if not sessions:
2336
+ all_sessions = _claude_window_session_ids(scope, bundle, conversations)
2337
+ if not all_sessions:
2093
2338
  return _unestablished()
2094
2339
 
2095
- allocation = allocate_scan_budget(sessions,
2340
+ allocation = allocate_scan_budget(all_sessions,
2096
2341
  kernel.DIAGNOSIS_SEED_SCAN_BUDGET_ROWS)
2097
- largest = max(allocation.values(), default=0)
2342
+ # Preserve the complete-population allocation above, then avoid loading
2343
+ # seed/window bodies for transcript-only sessions when accounting already
2344
+ # resolved the exact sessions that can contribute a ranked dollar. The
2345
+ # empty-accounting fallback keeps the prior typed outcome for older or
2346
+ # partially populated stores.
2347
+ spending_sessions = {
2348
+ entry.session_key for entry in facts.entries
2349
+ if entry.session_identity_resolved and entry.session_key
2350
+ }
2351
+ sessions = tuple(
2352
+ session for session in all_sessions if session in spending_sessions
2353
+ ) if spending_sessions else all_sessions
2354
+ largest = max((allocation[session] for session in sessions), default=0)
2098
2355
  prefix: dict[str, list] = {session: [] for session in sessions}
2099
2356
  overflowed: set[str] = set()
2100
2357
  for chunk in _chunks(sessions):
@@ -2174,7 +2431,7 @@ def _evaluate_cache_churn(scope: DiagnosisScope, bundle: StoreBundle,
2174
2431
  wasted.append(query._cache_failure_wasted_usd(
2175
2432
  model, lost, speed=speed))
2176
2433
 
2177
- known = set(sessions)
2434
+ known = set(all_sessions)
2178
2435
  seeded = set(streams)
2179
2436
  candidates = [entry for entry in facts.entries
2180
2437
  if entry.session_key in known]
@@ -2231,14 +2488,20 @@ def _short_context_evaluation(facts: RawFacts, *, known: Sequence[str],
2231
2488
  """Shape one provider's decided conversations into the class result."""
2232
2489
  known_set = set(known)
2233
2490
  qualifying_set = set(qualifying)
2234
- candidates = [entry for entry in facts.entries
2235
- if entry.session_key in known_set]
2236
- evaluated = [entry for entry in candidates
2237
- if entry.session_key not in unevaluable]
2238
- if _nothing_evaluable(candidates, evaluated):
2491
+ by_session, _labels = facts.grouped_entries["session"]
2492
+ candidate_count = sum(len(by_session.get(key, ())) for key in known_set)
2493
+ evaluated = tuple(
2494
+ entry
2495
+ for key in known_set if key not in unevaluable
2496
+ for entry in by_session.get(key, ())
2497
+ )
2498
+ if candidate_count and not evaluated:
2239
2499
  return _unestablished(sorted(set(unevaluable.values())))
2240
- matched = [entry for entry in evaluated
2241
- if entry.session_key in qualifying_set]
2500
+ matched = tuple(
2501
+ entry
2502
+ for key in qualifying_set if key not in unevaluable
2503
+ for entry in by_session.get(key, ())
2504
+ )
2242
2505
  member_turns = [turn_counts[key] for key in qualifying_set
2243
2506
  if key in turn_counts]
2244
2507
  members = sorted((fractions[key], key) for key in qualifying_set
@@ -2257,9 +2520,9 @@ def _short_context_evaluation(facts: RawFacts, *, known: Sequence[str],
2257
2520
  qualifications=marks)
2258
2521
  return _S3Evaluation(
2259
2522
  established=True,
2260
- qualifying=tuple(matched),
2261
- evaluated=tuple(evaluated),
2262
- candidate_count=len(candidates),
2523
+ qualifying=matched,
2524
+ evaluated=evaluated,
2525
+ candidate_count=candidate_count,
2263
2526
  gap_codes=tuple(sorted(set(unevaluable.values()))),
2264
2527
  evidence={
2265
2528
  "conversationCount": _EvidenceValue(value=len(qualifying_set)),
@@ -2298,10 +2561,7 @@ def _evaluate_claude_short_high_context(scope: DiagnosisScope,
2298
2561
  """
2299
2562
  facts = bundle.facts
2300
2563
  bounds = [_iso_sql(scope.window_start), _iso_sql(scope.window_end)]
2301
- sessions = sorted(
2302
- str(row[0]) for row in
2303
- _execute(conversations, _CLAUDE_WINDOW_SESSIONS_SQL, bounds)
2304
- )
2564
+ sessions = _claude_window_session_ids(scope, bundle, conversations)
2305
2565
  if not sessions:
2306
2566
  return _unestablished()
2307
2567
 
@@ -2433,6 +2693,85 @@ def _codex_turn_capacities(events: Sequence[Any]) -> dict[str, int]:
2433
2693
  return capacities
2434
2694
 
2435
2695
 
2696
+ def _fold_codex_target_turns(events: Sequence[Any],
2697
+ target_offsets: Sequence[int]) -> dict[int, str | None]:
2698
+ """Infer turns only at selected physical offsets.
2699
+
2700
+ Rows without a native turn anchor never change the lifecycle state. The
2701
+ old diagnosis nevertheless loaded every token-count payload, constructed
2702
+ a ``CodexPhysicalEvent`` for it, parsed its JSON, and then threw every map
2703
+ entry away except the accounting offsets. This merge walks the retained
2704
+ lifecycle anchors and the already-known accounting offsets directly.
2705
+
2706
+ The pending list preserves the canonical late-anchor rule: a later native
2707
+ proof backfills only the unanchored prefix since the latest
2708
+ ``session_meta``. A ``task_started`` anchor establishes the turn forward
2709
+ but does not backfill that prefix.
2710
+ """
2711
+ ordered_events = sorted(events, key=lambda event: int(event.line_offset))
2712
+ targets = sorted({int(offset) for offset in target_offsets})
2713
+ result: dict[int, str | None] = {}
2714
+ pending: list[int] = []
2715
+ current: str | None = None
2716
+ target_index = 0
2717
+
2718
+ def _record_until(limit: int, *, inclusive: bool = False) -> None:
2719
+ nonlocal target_index
2720
+ while target_index < len(targets):
2721
+ offset = targets[target_index]
2722
+ if offset > limit or (offset == limit and not inclusive):
2723
+ break
2724
+ if current is None:
2725
+ pending.append(offset)
2726
+ else:
2727
+ result[offset] = current
2728
+ target_index += 1
2729
+
2730
+ for event in ordered_events:
2731
+ offset = int(event.line_offset)
2732
+ _record_until(offset)
2733
+ record_type = getattr(event, "record_type", None)
2734
+ event_type = getattr(event, "event_type", None)
2735
+ explicit = getattr(event, "turn_id", None)
2736
+ if explicit is None and record_type == "turn_context":
2737
+ try:
2738
+ obj = json.loads(getattr(event, "payload_json", "") or "{}")
2739
+ except (ValueError, TypeError):
2740
+ obj = {}
2741
+ payload = obj.get("payload") if isinstance(obj, Mapping) else None
2742
+ candidate = payload.get("turn_id") if isinstance(payload, Mapping) else None
2743
+ explicit = candidate if isinstance(candidate, str) and candidate else None
2744
+ if record_type == "session_meta":
2745
+ # A later anchor cannot cross this segment boundary. Pending
2746
+ # targets from the preceding segment stay unresolved.
2747
+ for target in pending:
2748
+ result[target] = None
2749
+ pending.clear()
2750
+ current = None
2751
+ elif explicit is not None:
2752
+ is_late = (record_type != "turn_context"
2753
+ and event_type != "task_started")
2754
+ if current is None and is_late:
2755
+ for target in pending:
2756
+ result[target] = str(explicit)
2757
+ else:
2758
+ for target in pending:
2759
+ result[target] = None
2760
+ pending.clear()
2761
+ current = str(explicit)
2762
+ if (record_type == "session_meta" and target_index < len(targets)
2763
+ and targets[target_index] == offset):
2764
+ result[offset] = None
2765
+ target_index += 1
2766
+ else:
2767
+ _record_until(offset, inclusive=True)
2768
+
2769
+ _record_until(targets[-1] if targets else -1, inclusive=True)
2770
+ for target in pending:
2771
+ result[target] = None
2772
+ return {offset: result.get(offset) for offset in targets}
2773
+
2774
+
2436
2775
  def _codex_threads_for(cache: sqlite3.Connection, sql: str,
2437
2776
  values: Sequence[str]) -> list:
2438
2777
  rows: list = []
@@ -2456,8 +2795,8 @@ def _evaluate_codex_short_high_context(scope: DiagnosisScope,
2456
2795
  Codex, not only the fan-out class.
2457
2796
  """
2458
2797
  facts = bundle.facts
2459
- scoped = sorted({entry.session_key for entry in facts.entries
2460
- if entry.session_key})
2798
+ by_conversation, _labels = facts.grouped_entries["session"]
2799
+ scoped = sorted(key for key in by_conversation if key)
2461
2800
  if not scoped:
2462
2801
  return _unestablished()
2463
2802
  threads = {str(row["conversation_key"]): row
@@ -2491,13 +2830,13 @@ def _evaluate_codex_short_high_context(scope: DiagnosisScope,
2491
2830
  candidate_rows.get(key, []))
2492
2831
  return prompt_counts[key]
2493
2832
 
2494
- by_conversation: dict[str, list] = {}
2833
+ targets_by_path: dict[str, list[int]] = {}
2495
2834
  for entry in facts.entries:
2496
- by_conversation.setdefault(entry.session_key, []).append(entry)
2497
- paths = sorted({entry.source_path for entry in facts.entries
2498
- if entry.source_path})
2499
- turn_maps, capacities, starved = _codex_turn_attribution(conversations,
2500
- paths)
2835
+ if entry.source_path and entry.line_offset is not None:
2836
+ targets_by_path.setdefault(entry.source_path, []).append(
2837
+ int(entry.line_offset))
2838
+ turn_maps, capacities, starved = _codex_turn_attribution(
2839
+ conversations, targets_by_path)
2501
2840
 
2502
2841
  maximum = kernel.DIAGNOSIS_SHORT_CONVERSATION_MAX_HUMAN_TURNS
2503
2842
  unevaluable: dict[str, str] = {}
@@ -2570,7 +2909,7 @@ def _evaluate_codex_short_high_context(scope: DiagnosisScope,
2570
2909
 
2571
2910
 
2572
2911
  def _codex_turn_attribution(conversations: sqlite3.Connection,
2573
- paths: Sequence[str]):
2912
+ targets_by_path: Mapping[str, Sequence[int]]):
2574
2913
  """`(turn_maps, capacities, starved)` over the budgeted event inference.
2575
2914
 
2576
2915
  A file that exhausts its share yields NO turn map: a truncated read would
@@ -2580,29 +2919,37 @@ def _codex_turn_attribution(conversations: sqlite3.Connection,
2580
2919
  turn_maps: dict[str, dict] = {}
2581
2920
  capacities: dict[str, int] = {}
2582
2921
  starved: set[str] = set()
2922
+ paths = sorted(targets_by_path)
2583
2923
  if not paths:
2584
2924
  return turn_maps, capacities, starved
2585
- codex_kernel = _cctally()._load_sibling("_lib_codex_conversation")
2586
2925
  physical = _cctally()._load_sibling("_lib_jsonl").CodexPhysicalEvent
2587
2926
  allocation = allocate_scan_budget(
2588
2927
  list(paths), kernel.DIAGNOSIS_CODEX_EVENT_SCAN_BUDGET_ROWS)
2589
2928
  per_file = kernel.DIAGNOSIS_CODEX_EVENT_SCAN_PER_FILE_ROWS
2590
2929
  allocation = {path: min(share, per_file)
2591
2930
  for path, share in allocation.items()}
2592
- largest = max(allocation.values(), default=0)
2593
- grouped: dict[str, list] = {path: [] for path in paths}
2594
- for chunk in _chunks(list(paths)):
2595
- sql = _CODEX_EVENTS_SQL.format(placeholders=_placeholders(len(chunk)))
2596
- for row in _execute(conversations, sql, list(chunk) + [largest + 1]):
2931
+ for chunk in _chunks(paths, _CODEX_EVENT_BUDGET_CHUNK):
2932
+ sql = "\nUNION ALL\n".join(
2933
+ _CODEX_EVENT_BUDGET_TERM_SQL for _path in chunk)
2934
+ params: list[Any] = []
2935
+ for path in chunk:
2936
+ params.extend((path, path, allocation.get(path, 0) + 1))
2937
+ for row in _execute(conversations, sql, params):
2938
+ path = str(row["source_path"])
2939
+ if int(row["rows_read"]) > allocation.get(path, 0):
2940
+ starved.add(path)
2941
+ grouped: dict[str, list] = {path: [] for path in paths if path not in starved}
2942
+ eligible = sorted(grouped)
2943
+ for chunk in _chunks(eligible):
2944
+ sql = _CODEX_EVENT_ANCHORS_SQL.format(
2945
+ placeholders=_placeholders(len(chunk)))
2946
+ for row in _execute(conversations, sql, list(chunk)):
2597
2947
  grouped.setdefault(str(row["source_path"]), []).append(row)
2598
2948
  for path, rows in grouped.items():
2599
- share = allocation.get(path, 0)
2600
- if any(int(row["rank"]) > share for row in rows):
2601
- starved.add(path)
2602
- continue
2603
2949
  events = [physical(*[row[column] for column in _CODEX_EVENT_COLUMNS])
2604
2950
  for row in rows]
2605
- turn_maps[path] = codex_kernel.fold_codex_event_turns(events)
2951
+ turn_maps[path] = _fold_codex_target_turns(
2952
+ events, targets_by_path.get(path, ()))
2606
2953
  capacities.update(_codex_turn_capacities(events))
2607
2954
  return turn_maps, capacities, starved
2608
2955
 
@@ -2695,10 +3042,7 @@ def _evaluate_claude_fanout(scope: DiagnosisScope, bundle: StoreBundle,
2695
3042
  query = _conversation_query()
2696
3043
  facts = bundle.facts
2697
3044
  bounds = [_iso_sql(scope.window_start), _iso_sql(scope.window_end)]
2698
- sessions = sorted(
2699
- str(row[0]) for row in
2700
- _execute(conversations, _CLAUDE_WINDOW_SESSIONS_SQL, bounds)
2701
- )
3045
+ sessions = _claude_window_session_ids(scope, bundle, conversations)
2702
3046
  if not sessions:
2703
3047
  return _unestablished()
2704
3048
  assistant_rows: dict[str, list] = {}
@@ -2778,8 +3122,8 @@ def _evaluate_codex_fanout(scope: DiagnosisScope,
2778
3122
  weaker pair.
2779
3123
  """
2780
3124
  facts = bundle.facts
2781
- scoped = sorted({entry.session_key for entry in facts.entries
2782
- if entry.session_key})
3125
+ by_conversation, _labels = facts.grouped_entries["session"]
3126
+ scoped = sorted(key for key in by_conversation if key)
2783
3127
  if not scoped:
2784
3128
  return _unestablished()
2785
3129
  # The thread rows come from the `cache` component that read and DIGESTED
@@ -2800,31 +3144,33 @@ def _evaluate_codex_fanout(scope: DiagnosisScope,
2800
3144
  unresolvable = set(fanout.unallocated)
2801
3145
  unreadable = set(fanout.ambiguous)
2802
3146
 
2803
- candidates = list(facts.entries)
3147
+ candidates = facts.entries
2804
3148
  allocated: list = []
2805
3149
  unallocated: list = []
2806
3150
  ambiguous: list = []
2807
3151
  bucket_rows: dict[str, list] = {}
2808
- for entry in candidates:
2809
- key = entry.session_key
3152
+ for key, session_entries in by_conversation.items():
2810
3153
  if key in unreadable:
2811
3154
  # The origin category could not be read at all, so this is not
2812
3155
  # known subagent spend and its dollars are published nowhere.
2813
- ambiguous.append(entry)
3156
+ ambiguous.extend(session_entries)
2814
3157
  continue
2815
3158
  if key in unresolvable:
2816
- unallocated.append(entry)
3159
+ unallocated.extend(session_entries)
2817
3160
  continue
2818
3161
  group = fanout.parents.get(key)
2819
3162
  if group is None or group not in qualifying_groups:
2820
3163
  continue
2821
- if entry.root_key != group[0]:
3164
+ matching = [entry for entry in session_entries
3165
+ if entry.root_key == group[0]]
3166
+ if len(matching) != len(session_entries):
2822
3167
  # The join is `(source_root_key, conversation_key)`, and a key
2823
3168
  # that reached a different root is not this child's spend.
2824
- unallocated.append(entry)
2825
- continue
2826
- allocated.append(entry)
2827
- bucket_rows.setdefault(key, []).append(entry)
3169
+ unallocated.extend(entry for entry in session_entries
3170
+ if entry.root_key != group[0])
3171
+ if matching:
3172
+ allocated.extend(matching)
3173
+ bucket_rows[key] = matching
2828
3174
  bucket_usd = {key: stable_sum(entry.cost_usd for entry in rows)
2829
3175
  for key, rows in bucket_rows.items()}
2830
3176
  return _fanout_evidence(
@@ -3066,6 +3412,15 @@ def _establish(scope: DiagnosisScope, bundle: StoreBundle) -> StoreBundle:
3066
3412
  _read_with_probe(component)
3067
3413
  cache_payload = payloads.get("cache") or {}
3068
3414
  stats_payload = payloads.get("stats") or {}
3415
+ # The cache component already hashes every accounting row read for this
3416
+ # exact provider/window under its probe pair. Re-hashing a second,
3417
+ # lossy projection of the same 150K-entry population for the denominator
3418
+ # identifier added CPU and no independent coherence. Domain-separate the
3419
+ # established component digest so the public population identifier stays
3420
+ # opaque and remains distinct from the generation component value.
3421
+ cache_digest = _digest_rows(rows["cache"])
3422
+ population_digest = hashlib.sha256(
3423
+ b"diagnosis-population\x00" + cache_digest.encode()).hexdigest()[:32]
3069
3424
  # The accounting facts are assembled BEFORE the conversations component,
3070
3425
  # because that component digests the facts the three conversation-derived
3071
3426
  # evaluators publish and every one of them divides transcript structure
@@ -3078,6 +3433,7 @@ def _establish(scope: DiagnosisScope, bundle: StoreBundle) -> StoreBundle:
3078
3433
  retained_end=cache_payload.get("retained_end"),
3079
3434
  store_horizon=stats_payload.get("store_horizon"),
3080
3435
  unavailable_cause=cache_payload.get("unavailable_cause"),
3436
+ population_digest_override=population_digest,
3081
3437
  )
3082
3438
  # A class denied in plan stage 1 is SETTLED, so the store it would have
3083
3439
  # needed is never opened, probed or digested. The conversations component
@@ -3117,7 +3473,7 @@ def _establish(scope: DiagnosisScope, bundle: StoreBundle) -> StoreBundle:
3117
3473
  bundle.component_rows = rows
3118
3474
  bundle.vector = GenerationVector(
3119
3475
  stats=_digest_rows(rows["stats"]),
3120
- cache=_digest_rows(rows["cache"]),
3476
+ cache=cache_digest,
3121
3477
  configuration=_digest_rows(rows["configuration"]),
3122
3478
  conversations=(_digest_rows(rows["conversations"])
3123
3479
  if "conversations" in rows else None),
@@ -3163,19 +3519,25 @@ def _coverage_for(scope: DiagnosisScope, facts: RawFacts,
3163
3519
  """
3164
3520
  total_usd = facts.total_usd
3165
3521
  total_count = len(facts.entries)
3166
- observed = [e.timestamp for e in facts.entries]
3167
- attributed_usd = stable_sum(e.cost_usd for e in attributed)
3522
+ observed_start, observed_end = facts.observed_range
3168
3523
  attributed_count = len(attributed)
3169
- identity_resolved = sum(1 for e in attributed
3170
- if _identity_resolved(e, identity_kind))
3171
- priced_without_fallback = sum(
3172
- 1 for e in attributed if not e.is_fallback_pricing
3173
- )
3524
+ if attributed is facts.entries:
3525
+ attributed_usd = total_usd
3526
+ identity_resolved = facts.coverage_counts.get(identity_kind,
3527
+ attributed_count)
3528
+ priced_without_fallback = facts.coverage_counts["priced"]
3529
+ else:
3530
+ attributed_usd = stable_sum(e.cost_usd for e in attributed)
3531
+ identity_resolved = sum(1 for e in attributed
3532
+ if _identity_resolved(e, identity_kind))
3533
+ priced_without_fallback = sum(
3534
+ 1 for e in attributed if not e.is_fallback_pricing
3535
+ )
3174
3536
  return PopulationCoverage(
3175
3537
  requested_start=scope.window_start_iso,
3176
3538
  requested_end=scope.window_end_iso,
3177
- observed_start=_iso_z(min(observed)) if observed else None,
3178
- observed_end=_iso_z(max(observed)) if observed else None,
3539
+ observed_start=_iso_z(observed_start) if observed_start else None,
3540
+ observed_end=_iso_z(observed_end) if observed_end else None,
3179
3541
  count_coverage=(attributed_count / total_count) if total_count else None,
3180
3542
  usd_coverage=(attributed_usd / total_usd) if total_usd > 0 else None,
3181
3543
  identity_coverage=((identity_resolved / attributed_count)
@@ -3191,7 +3553,7 @@ def _coverage_for(scope: DiagnosisScope, facts: RawFacts,
3191
3553
  # Handed out BY REFERENCE to every S3 coverage object, so it is wrapped rather
3192
3554
  # than copied: a plain dict would let one caller mutate the map every other
3193
3555
  # coverage object is publishing.
3194
- _S3_DIMENSIONS: Mapping[str, str] = MappingProxyType({
3556
+ _S3_DIMENSIONS: Mapping[str, str] = kernel.FrozenDict({
3195
3557
  "countCoverage": "evaluated entries / all in-window provider entries",
3196
3558
  "usdCoverage": "evaluated USD / all in-window provider USD",
3197
3559
  "identityCoverage": "evaluated entries whose subject identity resolved "
@@ -3230,7 +3592,7 @@ def s3_coverage(scope: DiagnosisScope, facts: RawFacts, *,
3230
3592
  """
3231
3593
  total_usd = facts.total_usd
3232
3594
  total_count = len(facts.entries)
3233
- observed = [e.timestamp for e in facts.entries]
3595
+ observed_start, observed_end = facts.observed_range
3234
3596
  evaluated_usd = stable_sum(e.cost_usd for e in evaluated)
3235
3597
  evaluated_count = len(evaluated)
3236
3598
  identity_resolved = sum(1 for e in evaluated
@@ -3241,8 +3603,8 @@ def s3_coverage(scope: DiagnosisScope, facts: RawFacts, *,
3241
3603
  return PopulationCoverage(
3242
3604
  requested_start=scope.window_start_iso,
3243
3605
  requested_end=scope.window_end_iso,
3244
- observed_start=_iso_z(min(observed)) if observed else None,
3245
- observed_end=_iso_z(max(observed)) if observed else None,
3606
+ observed_start=_iso_z(observed_start) if observed_start else None,
3607
+ observed_end=_iso_z(observed_end) if observed_end else None,
3246
3608
  count_coverage=(evaluated_count / total_count) if total_count else None,
3247
3609
  usd_coverage=(evaluated_usd / total_usd) if total_usd > 0 else None,
3248
3610
  identity_coverage=((identity_resolved / evaluated_count)
@@ -3513,7 +3875,7 @@ def _baseline_lookup(bundle: StoreBundle, scope: DiagnosisScope,
3513
3875
  return _block_lookup, None
3514
3876
 
3515
3877
  if spec.kind == "session_concentration":
3516
- grouped, _labels = _group_entries(baseline.entries, "session")
3878
+ grouped, _labels = baseline.grouped_entries["session"]
3517
3879
  top = max(
3518
3880
  (stable_sum(e.cost_usd for e in rows) / total
3519
3881
  for rows in grouped.values()),
@@ -3526,7 +3888,7 @@ def _baseline_lookup(bundle: StoreBundle, scope: DiagnosisScope,
3526
3888
  return _session_lookup, None
3527
3889
 
3528
3890
  kind = "model" if spec.kind == "model_mix" else "project"
3529
- grouped, _labels = _group_entries(baseline.entries, kind)
3891
+ grouped, _labels = baseline.grouped_entries[kind]
3530
3892
  shares = {key: stable_sum(e.cost_usd for e in rows) / total
3531
3893
  for key, rows in grouped.items()}
3532
3894
 
@@ -3765,13 +4127,13 @@ def _load_class_facts_inner(bundle: StoreBundle, scope: DiagnosisScope,
3765
4127
  return ClassFacts(spec.kind, subjects, population, facts.total_usd)
3766
4128
 
3767
4129
  kind = _identity_kind_for(spec)
3768
- grouped, labels = _group_entries(facts.entries, kind)
4130
+ grouped, labels = facts.grouped_entries[kind]
3769
4131
  subjects = tuple(
3770
4132
  _subject_from_group(key, labels[key], rows,
3771
4133
  _baseline_for(key, None), baseline_code)
3772
4134
  for key, rows in grouped.items()
3773
4135
  )
3774
- attributed = list(facts.entries)
4136
+ attributed = facts.entries
3775
4137
 
3776
4138
  gap_codes: tuple[str, ...] = ()
3777
4139
  preempting: str | None = None
@@ -3797,11 +4159,7 @@ def _load_class_facts_inner(bundle: StoreBundle, scope: DiagnosisScope,
3797
4159
  # --- the assembled diagnosis -------------------------------------------
3798
4160
 
3799
4161
  def _population_digest(facts: RawFacts) -> str:
3800
- digest = hashlib.sha256()
3801
- for entry in facts.entries:
3802
- digest.update(f"{_iso_z(entry.timestamp)}|{entry.model}|"
3803
- f"{entry.session_key}|{entry.cost_usd!r}".encode())
3804
- return digest.hexdigest()[:32]
4162
+ return facts.population_digest
3805
4163
 
3806
4164
 
3807
4165
  def _next_step_context(scope: DiagnosisScope) -> dict[str, str]:
@@ -3836,13 +4194,13 @@ def build_provider_diagnosis(scope: DiagnosisScope, *,
3836
4194
  transcripts_visible=transcripts_visible)
3837
4195
  with StoreBundle(scope, policy) as bundle:
3838
4196
  _establish(scope, bundle)
3839
- baseline_rows: dict[str, list] = {}
4197
+ baseline_vector: GenerationVector | None = None
3840
4198
  preceding = scope.preceding()
3841
4199
  try:
3842
4200
  with StoreBundle(preceding, policy) as baseline_bundle:
3843
4201
  _establish(preceding, baseline_bundle)
3844
4202
  bundle.baseline = baseline_bundle.facts
3845
- baseline_rows = dict(baseline_bundle.component_rows)
4203
+ baseline_vector = baseline_bundle.vector
3846
4204
  # The S3 comparator is the preceding window's aggregate
3847
4205
  # qualifying-cost share, so it is computed while that bundle
3848
4206
  # is still open — its stores close with the block.
@@ -3870,7 +4228,7 @@ def build_provider_diagnosis(scope: DiagnosisScope, *,
3870
4228
  )
3871
4229
  else:
3872
4230
  coverage = _coverage_for(
3873
- scope, bundle.facts, attributed=list(bundle.facts.entries),
4231
+ scope, bundle.facts, attributed=bundle.facts.entries,
3874
4232
  identity_kind="model",
3875
4233
  )
3876
4234
  denominator_usd = (
@@ -3913,17 +4271,21 @@ def build_provider_diagnosis(scope: DiagnosisScope, *,
3913
4271
  # Each component digest binds BOTH windows, so a baseline that moved
3914
4272
  # while the current window stood still moves the identifier too.
3915
4273
  published = GenerationVector(
3916
- stats=_digest_component_pair(bundle.component_rows.get("stats", []),
3917
- baseline_rows.get("stats")),
3918
- cache=_digest_component_pair(bundle.component_rows.get("cache", []),
3919
- baseline_rows.get("cache")),
3920
- configuration=_digest_component_pair(
3921
- bundle.component_rows.get("configuration", []),
3922
- baseline_rows.get("configuration")),
4274
+ stats=_digest_component_pair_from_digests(
4275
+ bundle.vector.stats,
4276
+ baseline_vector.stats if baseline_vector else None),
4277
+ cache=_digest_component_pair_from_digests(
4278
+ bundle.vector.cache,
4279
+ baseline_vector.cache if baseline_vector else None),
4280
+ configuration=_digest_component_pair_from_digests(
4281
+ bundle.vector.configuration,
4282
+ baseline_vector.configuration if baseline_vector else None),
3923
4283
  conversations=(
3924
- _digest_component_pair(bundle.component_rows["conversations"],
3925
- baseline_rows.get("conversations"))
3926
- if "conversations" in bundle.component_rows else None),
4284
+ _digest_component_pair_from_digests(
4285
+ bundle.vector.conversations,
4286
+ (baseline_vector.conversations
4287
+ if baseline_vector else None))
4288
+ if bundle.vector.conversations is not None else None),
3927
4289
  )
3928
4290
  return kernel.build_provider_result(
3929
4291
  source=scope.source,
@@ -3937,6 +4299,101 @@ def build_provider_diagnosis(scope: DiagnosisScope, *,
3937
4299
  )
3938
4300
 
3939
4301
 
4302
+ def _build_provider_task(args) -> kernel.ProviderResult:
4303
+ """Pickle-safe provider build for the independent `all` branches."""
4304
+ scope, transcripts_visible = args
4305
+ try:
4306
+ result = build_provider_diagnosis(
4307
+ scope, transcripts_visible=transcripts_visible)
4308
+ except EstablishmentFailure as exc:
4309
+ if exc.code != EstablishmentError.STORE_UNAVAILABLE.value:
4310
+ raise
4311
+ result = _unavailable_provider_result(
4312
+ scope, detail=exc.message,
4313
+ transcripts_visible=transcripts_visible)
4314
+ return result
4315
+
4316
+
4317
+ def _start_forked_provider(args) -> tuple[int, int]:
4318
+ """Fork before any provider store opens; return `(pid, read_fd)`."""
4319
+ read_fd, write_fd = os.pipe()
4320
+ pid = os.fork()
4321
+ if pid == 0:
4322
+ os.close(read_fd)
4323
+ try:
4324
+ payload = (True, _build_provider_task(args))
4325
+ except EstablishmentFailure as exc:
4326
+ payload = (False, "establishment", exc.code, exc.message)
4327
+ except BaseException as exc:
4328
+ payload = (False, "unexpected", type(exc).__name__, str(exc))
4329
+ try:
4330
+ with os.fdopen(write_fd, "wb") as stream:
4331
+ pickle.dump(payload, stream, protocol=5)
4332
+ finally:
4333
+ os._exit(0)
4334
+ os.close(write_fd)
4335
+ return pid, read_fd
4336
+
4337
+
4338
+ def _finish_forked_provider(worker: tuple[int, int]):
4339
+ pid, read_fd = worker
4340
+ try:
4341
+ with os.fdopen(read_fd, "rb") as stream:
4342
+ payload = pickle.load(stream)
4343
+ finally:
4344
+ _waited_pid, status = os.waitpid(pid, 0)
4345
+ if not payload[0]:
4346
+ if payload[1] == "establishment":
4347
+ raise EstablishmentFailure(payload[2], payload[3])
4348
+ raise RuntimeError(f"{payload[2]}: {payload[3]}")
4349
+ if status != 0:
4350
+ raise RuntimeError(f"diagnosis provider worker exited {status}")
4351
+ return payload[1]
4352
+
4353
+
4354
+ def _spawn_worker_init(cctally_path: str,
4355
+ store_paths: Mapping[str, str]) -> None:
4356
+ """Initialize a safe spawned/forkserver dashboard worker."""
4357
+ import importlib.machinery
4358
+ import importlib.util
4359
+ import pathlib
4360
+
4361
+ # The canonical entry point is deliberately extensionless (`bin/cctally`),
4362
+ # so `spec_from_file_location` cannot infer a loader from its suffix.
4363
+ # Name the source loader explicitly; this executes the same file the
4364
+ # parent loaded instead of copying another import surface into the worker.
4365
+ loader = importlib.machinery.SourceFileLoader("cctally", cctally_path)
4366
+ spec = importlib.util.spec_from_loader("cctally", loader)
4367
+ if spec is None or spec.loader is None:
4368
+ raise RuntimeError(f"cannot load cctally worker from {cctally_path}")
4369
+ module = importlib.util.module_from_spec(spec)
4370
+ sys.modules["cctally"] = module
4371
+ spec.loader.exec_module(module)
4372
+ for name, raw in store_paths.items():
4373
+ setattr(_cctally_core, name, pathlib.Path(raw))
4374
+
4375
+
4376
+ def _build_isolated_provider(args):
4377
+ """Run one provider in a portable, one-shot safe process worker."""
4378
+ import concurrent.futures
4379
+ import multiprocessing
4380
+
4381
+ c = _cctally()
4382
+ paths = {
4383
+ name: str(getattr(_cctally_core, name))
4384
+ for name in ("CACHE_DB_PATH", "DB_PATH",
4385
+ "CONVERSATIONS_DB_PATH", "CONFIG_PATH")
4386
+ }
4387
+ methods = multiprocessing.get_all_start_methods()
4388
+ method = "forkserver" if "forkserver" in methods else "spawn"
4389
+ with concurrent.futures.ProcessPoolExecutor(
4390
+ max_workers=1,
4391
+ mp_context=multiprocessing.get_context(method),
4392
+ initializer=_spawn_worker_init,
4393
+ initargs=(str(c.__file__), paths)) as executor:
4394
+ return executor.submit(_build_provider_task, args).result()
4395
+
4396
+
3940
4397
  def build_diagnosis(scope: DiagnosisScope,
3941
4398
  *, measured_at: dt.datetime | None = None,
3942
4399
  transcripts_visible: bool) -> kernel.DiagnosisReport:
@@ -3953,8 +4410,7 @@ def build_diagnosis(scope: DiagnosisScope,
3953
4410
  now = measured_at or dt.datetime.now(UTC)
3954
4411
  sources = (("claude", "codex") if scope.source == "all"
3955
4412
  else (scope.source,))
3956
- results = []
3957
- for source in sources:
4413
+ def _build_one(source: str) -> kernel.ProviderResult:
3958
4414
  provider_scope = DiagnosisScope(
3959
4415
  source=source,
3960
4416
  account_key=scope.account_key,
@@ -3965,8 +4421,8 @@ def build_diagnosis(scope: DiagnosisScope,
3965
4421
  label=scope.label,
3966
4422
  )
3967
4423
  try:
3968
- results.append(build_provider_diagnosis(
3969
- provider_scope, transcripts_visible=transcripts_visible))
4424
+ return build_provider_diagnosis(
4425
+ provider_scope, transcripts_visible=transcripts_visible)
3970
4426
  except EstablishmentFailure as exc:
3971
4427
  # A store that cannot be OPENED withholds its provider rather than
3972
4428
  # ending the request, whether or not a second provider was
@@ -3977,9 +4433,66 @@ def build_diagnosis(scope: DiagnosisScope,
3977
4433
  # two-provider user a report and exit 0, from one condition.
3978
4434
  if exc.code != EstablishmentError.STORE_UNAVAILABLE.value:
3979
4435
  raise
3980
- results.append(_unavailable_provider_result(
4436
+ return _unavailable_provider_result(
3981
4437
  provider_scope, detail=exc.message,
3982
- transcripts_visible=transcripts_visible))
4438
+ transcripts_visible=transcripts_visible)
4439
+
4440
+ if len(sources) == 1:
4441
+ results = [_build_one(sources[0])]
4442
+ else:
4443
+ # Provider stores, denominators and generation vectors are deliberately
4444
+ # independent under `all`. Reading them in series made the combined
4445
+ # latency the SUM of the Claude and Codex paths while adding no
4446
+ # consistency guarantee. `map` preserves the canonical Claude/Codex
4447
+ # result order even when Codex finishes first.
4448
+ tasks = [(
4449
+ DiagnosisScope(
4450
+ source=source,
4451
+ account_key=scope.account_key,
4452
+ window_start=scope.window_start,
4453
+ window_end=scope.window_end,
4454
+ effective_speed=(scope.effective_speed
4455
+ if source == "codex" else None),
4456
+ display_tz=scope.display_tz,
4457
+ label=scope.label,
4458
+ ), transcripts_visible,
4459
+ ) for source in sources]
4460
+ # Keep Claude in the already-loaded parent and isolate only the much
4461
+ # larger Codex fold. This retains true CPU overlap without paying to
4462
+ # fork and pickle both branches; canonical order remains Claude then
4463
+ # Codex regardless of which finishes first.
4464
+ # Fork before opening either provider's stores. The child owns Codex;
4465
+ # the parent builds Claude while it runs, then reads one compact result.
4466
+ if (threading.current_thread() is threading.main_thread()
4467
+ and hasattr(os, "fork")):
4468
+ codex_worker = _start_forked_provider(tasks[1])
4469
+ try:
4470
+ claude_result = _build_one("claude")
4471
+ except BaseException:
4472
+ # Drain the pipe and reap the already-running child before
4473
+ # preserving the parent failure. Without this path an early
4474
+ # Claude establishment error leaves either a zombie or a
4475
+ # child blocked while writing its result into a full pipe.
4476
+ try:
4477
+ _finish_forked_provider(codex_worker)
4478
+ except BaseException:
4479
+ pass
4480
+ raise
4481
+ codex_result = _finish_forked_provider(codex_worker)
4482
+ else:
4483
+ # `os.fork()` from ThreadingHTTPServer's request thread is not a
4484
+ # safe operation. A one-shot forkserver worker gives the route the
4485
+ # same CPU isolation without retaining a second full diagnosis
4486
+ # heap between requests or adding any periodic work.
4487
+ import concurrent.futures
4488
+
4489
+ with concurrent.futures.ThreadPoolExecutor(
4490
+ max_workers=1) as launcher:
4491
+ codex_future = launcher.submit(
4492
+ _build_isolated_provider, tasks[1])
4493
+ claude_result = _build_one("claude")
4494
+ codex_result = codex_future.result()
4495
+ results = [claude_result, codex_result]
3983
4496
  return kernel.build_report(_iso_z(now), scope.window(), results)
3984
4497
 
3985
4498