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.
- package/CHANGELOG.md +38 -0
- package/README.md +6 -6
- package/bin/_cctally_alerts.py +46 -13
- package/bin/_cctally_cache.py +193 -71
- package/bin/_cctally_dashboard.py +683 -70
- package/bin/_cctally_dashboard_conversation.py +400 -6
- package/bin/_cctally_dashboard_envelope.py +17 -0
- package/bin/_cctally_dashboard_perf.py +93 -0
- package/bin/_cctally_dashboard_sources.py +988 -94
- package/bin/_cctally_db.py +30 -3
- package/bin/_cctally_diagnosis_sources.py +699 -186
- package/bin/_cctally_doctor.py +22 -8
- package/bin/_cctally_forecast.py +17 -9
- package/bin/_cctally_journal.py +298 -40
- package/bin/_cctally_quota.py +15 -0
- package/bin/_cctally_quota_model.py +584 -58
- package/bin/_cctally_record.py +133 -67
- package/bin/_cctally_tui.py +317 -70
- package/bin/_lib_codex_conversation_query.py +258 -12
- package/bin/_lib_codex_hooks.py +26 -0
- package/bin/_lib_conversation_query.py +145 -15
- package/bin/_lib_dashboard_json.py +105 -0
- package/bin/_lib_diagnosis.py +21 -2
- package/bin/_lib_doctor.py +64 -8
- package/bin/_lib_ingest_frontier.py +889 -0
- package/bin/_lib_meter_rate_change.py +95 -29
- package/bin/_lib_perf.py +22 -0
- package/bin/_lib_pricing.py +30 -3
- package/bin/_lib_quota_copy.py +30 -4
- package/bin/_lib_quota_model.py +215 -28
- package/bin/_lib_rate_change_delivery.py +119 -0
- package/bin/_lib_record.py +50 -0
- package/bin/_lib_retained_size.py +187 -0
- package/bin/_lib_snapshot_cache.py +504 -17
- package/bin/_lib_statusline.py +3 -29
- package/bin/_lib_tick_stats.py +28 -4
- package/dashboard/static/assets/ConversationsView-DA4j7Yso.js +72 -0
- package/dashboard/static/assets/DoctorModal-D4PbVnbE.js +1 -0
- package/dashboard/static/assets/ModalRoot-D4oSPxPp.js +1 -0
- package/dashboard/static/assets/ProjectsDrillPanel-DkPlB9JH.js +1 -0
- package/dashboard/static/assets/SourceDetailModal-BE7FONsS.js +1 -0
- package/dashboard/static/assets/UpdateModal-BJuI8glf.js +7 -0
- package/dashboard/static/assets/index-w_bINkJ8.js +13 -0
- package/dashboard/static/assets/outlineNavigation-zDVm6Hdd.js +9 -0
- package/dashboard/static/assets/useKeymap-CJ-Pi17D.js +1 -0
- package/dashboard/static/dashboard.html +2 -1
- package/package.json +4 -1
- 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
|
-
|
|
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
|
-
|
|
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(
|
|
336
|
+
digest.update(current_digest.encode())
|
|
310
337
|
digest.update(b"\x1dbaseline=")
|
|
311
|
-
digest.update(
|
|
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
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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
|
-
|
|
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
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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[
|
|
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 =
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
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
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
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
|
|
1157
|
+
session_key=conversation_key or source_path or "(unknown)",
|
|
948
1158
|
session_label=conversation_key or "(unknown)",
|
|
949
|
-
root_key=
|
|
950
|
-
pool=
|
|
1159
|
+
root_key=root_key,
|
|
1160
|
+
pool=pools[model],
|
|
951
1161
|
cost_usd=cost,
|
|
952
|
-
input_tokens=
|
|
953
|
-
output_tokens=
|
|
954
|
-
cache_read_tokens=
|
|
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=
|
|
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 =
|
|
1271
|
+
entries, digest_rows, thread_rows = (
|
|
1272
|
+
_read_codex_entries(scope, conn, bundle.plan)
|
|
1273
|
+
)
|
|
1063
1274
|
retention_sql = _CODEX_RETENTION_SQL
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2089
|
-
|
|
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(
|
|
2340
|
+
allocation = allocate_scan_budget(all_sessions,
|
|
2096
2341
|
kernel.DIAGNOSIS_SEED_SCAN_BUDGET_ROWS)
|
|
2097
|
-
|
|
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(
|
|
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
|
-
|
|
2235
|
-
|
|
2236
|
-
evaluated =
|
|
2237
|
-
|
|
2238
|
-
|
|
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 =
|
|
2241
|
-
|
|
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=
|
|
2261
|
-
evaluated=
|
|
2262
|
-
candidate_count=
|
|
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 =
|
|
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
|
-
|
|
2460
|
-
|
|
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
|
-
|
|
2833
|
+
targets_by_path: dict[str, list[int]] = {}
|
|
2495
2834
|
for entry in facts.entries:
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
turn_maps, capacities, starved = _codex_turn_attribution(
|
|
2500
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
for
|
|
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] =
|
|
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 =
|
|
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
|
-
|
|
2782
|
-
|
|
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 =
|
|
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
|
|
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.
|
|
3156
|
+
ambiguous.extend(session_entries)
|
|
2814
3157
|
continue
|
|
2815
3158
|
if key in unresolvable:
|
|
2816
|
-
unallocated.
|
|
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
|
-
|
|
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.
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
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=
|
|
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
|
-
|
|
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
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
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(
|
|
3178
|
-
observed_end=_iso_z(
|
|
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] =
|
|
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
|
-
|
|
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(
|
|
3245
|
-
observed_end=_iso_z(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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=
|
|
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=
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
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
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|