cctally 1.72.0 → 1.74.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 +22 -0
- package/bin/_cctally_cache.py +271 -55
- package/bin/_cctally_core.py +20 -40
- package/bin/_cctally_dashboard.py +35 -1
- package/bin/_cctally_dashboard_conversation.py +731 -94
- package/bin/_cctally_dashboard_share.py +38 -3
- package/bin/_cctally_dashboard_sources.py +812 -74
- package/bin/_cctally_db.py +104 -1
- package/bin/_cctally_doctor.py +86 -4
- package/bin/_cctally_parser.py +17 -4
- package/bin/_cctally_record.py +162 -33
- package/bin/_cctally_refresh.py +58 -36
- package/bin/_cctally_source_analytics.py +288 -3
- package/bin/_cctally_statusline.py +811 -82
- package/bin/_cctally_transcript.py +223 -7
- package/bin/_cctally_tui.py +17 -14
- package/bin/_lib_aggregators.py +48 -34
- package/bin/_lib_codex_conversation_export.py +124 -0
- package/bin/_lib_codex_conversation_query.py +500 -28
- package/bin/_lib_codex_conversation_watch.py +323 -0
- package/bin/_lib_conversation_dispatch.py +269 -7
- package/bin/_lib_conversation_query.py +88 -0
- package/bin/_lib_dashboard_sources.py +20 -7
- package/bin/_lib_doctor.py +106 -0
- package/bin/_lib_jsonl.py +11 -0
- package/bin/_lib_pricing.py +7 -4
- package/bin/_lib_pricing_check.py +7 -1
- package/bin/_lib_quota.py +12 -11
- package/bin/_lib_statusline_candidates.py +734 -0
- package/bin/_lib_view_models.py +50 -14
- package/bin/cctally +31 -2
- package/dashboard/static/assets/index-DeQjEMO6.js +80 -0
- package/dashboard/static/assets/index-ZiPO8veo.css +1 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +4 -1
- package/dashboard/static/assets/index-BWadAHxC.js +0 -80
- package/dashboard/static/assets/index-D2nwo_ln.css +0 -1
|
@@ -4,10 +4,11 @@ from __future__ import annotations
|
|
|
4
4
|
import datetime as dt
|
|
5
5
|
import hashlib
|
|
6
6
|
import json
|
|
7
|
+
import pathlib
|
|
7
8
|
import sqlite3
|
|
8
9
|
import sys
|
|
9
10
|
from collections.abc import Mapping
|
|
10
|
-
from dataclasses import dataclass
|
|
11
|
+
from dataclasses import dataclass, replace
|
|
11
12
|
from types import MappingProxyType
|
|
12
13
|
from typing import Any, Iterable
|
|
13
14
|
from zoneinfo import ZoneInfo
|
|
@@ -18,11 +19,19 @@ from _cctally_quota import (
|
|
|
18
19
|
load_codex_quota_observations,
|
|
19
20
|
load_codex_quota_projection_certificate,
|
|
20
21
|
)
|
|
21
|
-
from _cctally_source_analytics import
|
|
22
|
+
from _cctally_source_analytics import (
|
|
23
|
+
QualifiedMetadataUnavailable,
|
|
24
|
+
has_cached_codex_accounting_entries,
|
|
25
|
+
load_cached_rooted_codex_accounting_entries,
|
|
26
|
+
load_codex_project_metadata_health,
|
|
27
|
+
load_qualified_codex_entries,
|
|
28
|
+
)
|
|
29
|
+
import _lib_log
|
|
22
30
|
from _lib_dashboard_sources import (
|
|
23
31
|
CapabilityRecord,
|
|
24
32
|
ProjectionCoherence,
|
|
25
33
|
SourceDashboardState,
|
|
34
|
+
SourceDashboardWarning,
|
|
26
35
|
assess_codex_projection_coherence,
|
|
27
36
|
dashboard_resource_key,
|
|
28
37
|
)
|
|
@@ -35,12 +44,16 @@ from _lib_quota import (
|
|
|
35
44
|
select_baseline,
|
|
36
45
|
)
|
|
37
46
|
from _lib_jsonl import CodexEntry
|
|
47
|
+
from _lib_fmt import stable_sum
|
|
48
|
+
from _lib_aggregators import _aggregate_codex_buckets
|
|
49
|
+
from _lib_five_hour import _FIVE_HOUR_JITTER_FLOOR_SECONDS
|
|
38
50
|
from _lib_source_analytics import build_codex_project_result
|
|
39
51
|
from _lib_view_models import (
|
|
52
|
+
CodexWeeklyView,
|
|
40
53
|
build_codex_daily_view,
|
|
41
54
|
build_codex_monthly_view,
|
|
55
|
+
build_rooted_codex_session_view,
|
|
42
56
|
build_codex_session_view,
|
|
43
|
-
build_codex_weekly_view,
|
|
44
57
|
)
|
|
45
58
|
|
|
46
59
|
|
|
@@ -50,12 +63,12 @@ DASHBOARD_QUOTA_OBSERVATION_LIMIT = 1000
|
|
|
50
63
|
DASHBOARD_QUOTA_RECENT_DAYS = 35
|
|
51
64
|
|
|
52
65
|
|
|
53
|
-
class
|
|
54
|
-
"""
|
|
66
|
+
class CodexCycleUnavailable(RuntimeError):
|
|
67
|
+
"""No single active native seven-day boundary can bound hero accounting."""
|
|
55
68
|
|
|
56
|
-
def __init__(self, reason: str
|
|
69
|
+
def __init__(self, reason: str) -> None:
|
|
57
70
|
self.reason = reason
|
|
58
|
-
super().__init__(
|
|
71
|
+
super().__init__(reason)
|
|
59
72
|
|
|
60
73
|
|
|
61
74
|
class SourceCapabilityUnavailable(ValueError):
|
|
@@ -66,6 +79,157 @@ class SourceResourceNotFound(LookupError):
|
|
|
66
79
|
"""A valid opaque resource key has no row in its provider state."""
|
|
67
80
|
|
|
68
81
|
|
|
82
|
+
@dataclass(frozen=True)
|
|
83
|
+
class CodexCycleBoundary:
|
|
84
|
+
"""The one active native subscription cycle usable for hero accounting."""
|
|
85
|
+
|
|
86
|
+
window_minutes: int
|
|
87
|
+
start_at: dt.datetime
|
|
88
|
+
resets_at: dt.datetime
|
|
89
|
+
# Root provenance is server-only accounting input, never public wire data.
|
|
90
|
+
source_root_keys: tuple[str, ...]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass(frozen=True)
|
|
94
|
+
class CodexWeeklyPeriod:
|
|
95
|
+
"""One non-overlapping observed native seven-day quota cycle segment."""
|
|
96
|
+
|
|
97
|
+
start_at: dt.datetime
|
|
98
|
+
end_at: dt.datetime
|
|
99
|
+
source_root_keys: tuple[str, ...]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _resolve_codex_weekly_cycle(
|
|
103
|
+
observations: Iterable[object],
|
|
104
|
+
now_utc: dt.datetime,
|
|
105
|
+
) -> CodexCycleBoundary:
|
|
106
|
+
"""Select exactly one active account-level 10,080-minute native cycle."""
|
|
107
|
+
boundaries: dict[tuple[int, dt.datetime], set[str]] = {}
|
|
108
|
+
stale_weekly_evidence = False
|
|
109
|
+
for history in build_history(tuple(observations)):
|
|
110
|
+
if history.identity.window_minutes != 10_080:
|
|
111
|
+
continue
|
|
112
|
+
baseline = select_baseline(history.observations, now_utc)
|
|
113
|
+
if baseline is None or baseline.resets_at <= now_utc:
|
|
114
|
+
continue
|
|
115
|
+
if quota_freshness(history.physical_observations, now_utc).state != "fresh":
|
|
116
|
+
stale_weekly_evidence = True
|
|
117
|
+
continue
|
|
118
|
+
boundary = (history.identity.window_minutes, baseline.resets_at)
|
|
119
|
+
boundaries.setdefault(boundary, set()).add(history.identity.source_root_key)
|
|
120
|
+
if len(boundaries) != 1:
|
|
121
|
+
if not boundaries:
|
|
122
|
+
raise CodexCycleUnavailable("stale" if stale_weekly_evidence else "missing")
|
|
123
|
+
raise CodexCycleUnavailable("conflicting")
|
|
124
|
+
(window_minutes, resets_at), root_keys = next(iter(boundaries.items()))
|
|
125
|
+
return CodexCycleBoundary(
|
|
126
|
+
window_minutes=window_minutes,
|
|
127
|
+
start_at=resets_at - dt.timedelta(minutes=window_minutes),
|
|
128
|
+
resets_at=resets_at,
|
|
129
|
+
source_root_keys=tuple(sorted(root_keys)),
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _codex_weekly_periods(
|
|
134
|
+
stats_conn: sqlite3.Connection,
|
|
135
|
+
*,
|
|
136
|
+
source_root_keys: Iterable[str],
|
|
137
|
+
active_cycle: CodexCycleBoundary | None,
|
|
138
|
+
) -> tuple[CodexWeeklyPeriod, ...]:
|
|
139
|
+
"""Read durable 10,080-minute boundaries and clip early re-anchors.
|
|
140
|
+
|
|
141
|
+
A provider-granted reset changes the native window's nominal start before
|
|
142
|
+
the prior seven-day deadline. Sorting those nominal starts and ending the
|
|
143
|
+
prior segment at the next start preserves the actual quota-cycle boundary
|
|
144
|
+
without double-counting the overlapping nominal windows.
|
|
145
|
+
"""
|
|
146
|
+
roots = tuple(sorted({
|
|
147
|
+
root for root in source_root_keys if isinstance(root, str) and root
|
|
148
|
+
}))
|
|
149
|
+
if not roots:
|
|
150
|
+
return ()
|
|
151
|
+
placeholders = ",".join("?" for _ in roots)
|
|
152
|
+
try:
|
|
153
|
+
rows = stats_conn.execute(
|
|
154
|
+
"SELECT source_root_key, resets_at_utc, nominal_start_at_utc "
|
|
155
|
+
"FROM quota_window_blocks "
|
|
156
|
+
"WHERE source='codex' AND window_minutes=10080 "
|
|
157
|
+
f"AND source_root_key IN ({placeholders}) AND orphaned_at IS NULL "
|
|
158
|
+
"ORDER BY nominal_start_at_utc DESC, resets_at_utc DESC, source_root_key "
|
|
159
|
+
"LIMIT ?",
|
|
160
|
+
(*roots, SOURCE_HISTORY_LIMIT),
|
|
161
|
+
).fetchall()
|
|
162
|
+
except sqlite3.Error:
|
|
163
|
+
rows = ()
|
|
164
|
+
|
|
165
|
+
raw_boundaries: list[tuple[dt.datetime, dt.datetime, set[str]]] = []
|
|
166
|
+
|
|
167
|
+
for root_key, resets_at_raw, start_at_raw in rows:
|
|
168
|
+
try:
|
|
169
|
+
start_at = dt.datetime.fromisoformat(str(start_at_raw).replace("Z", "+00:00"))
|
|
170
|
+
resets_at = dt.datetime.fromisoformat(str(resets_at_raw).replace("Z", "+00:00"))
|
|
171
|
+
except (TypeError, ValueError):
|
|
172
|
+
continue
|
|
173
|
+
if start_at.tzinfo is None or resets_at.tzinfo is None:
|
|
174
|
+
continue
|
|
175
|
+
start_at = start_at.astimezone(UTC)
|
|
176
|
+
resets_at = resets_at.astimezone(UTC)
|
|
177
|
+
if resets_at <= start_at:
|
|
178
|
+
continue
|
|
179
|
+
raw_boundaries.append((start_at, resets_at, {str(root_key)}))
|
|
180
|
+
|
|
181
|
+
if active_cycle is not None:
|
|
182
|
+
raw_boundaries.append((
|
|
183
|
+
active_cycle.start_at.astimezone(UTC),
|
|
184
|
+
active_cycle.resets_at.astimezone(UTC),
|
|
185
|
+
set(active_cycle.source_root_keys),
|
|
186
|
+
))
|
|
187
|
+
|
|
188
|
+
ordered: list[tuple[dt.datetime, dt.datetime, set[str]]] = []
|
|
189
|
+
for start_at, resets_at, period_roots in sorted(
|
|
190
|
+
raw_boundaries, key=lambda item: (item[0], item[1]),
|
|
191
|
+
):
|
|
192
|
+
if (
|
|
193
|
+
ordered
|
|
194
|
+
and (start_at - ordered[-1][0]).total_seconds()
|
|
195
|
+
< _FIVE_HOUR_JITTER_FLOOR_SECONDS
|
|
196
|
+
):
|
|
197
|
+
first_start, latest_reset, existing_roots = ordered[-1]
|
|
198
|
+
existing_roots.update(period_roots)
|
|
199
|
+
ordered[-1] = (first_start, max(latest_reset, resets_at), existing_roots)
|
|
200
|
+
else:
|
|
201
|
+
ordered.append((start_at, resets_at, set(period_roots)))
|
|
202
|
+
periods: list[CodexWeeklyPeriod] = []
|
|
203
|
+
for index, (start_at, resets_at, period_roots) in enumerate(ordered):
|
|
204
|
+
next_start = ordered[index + 1][0] if index + 1 < len(ordered) else None
|
|
205
|
+
end_at = min(resets_at, next_start) if next_start is not None else resets_at
|
|
206
|
+
if end_at <= start_at:
|
|
207
|
+
continue
|
|
208
|
+
periods.append(CodexWeeklyPeriod(
|
|
209
|
+
start_at=start_at,
|
|
210
|
+
end_at=end_at,
|
|
211
|
+
source_root_keys=tuple(sorted(period_roots)),
|
|
212
|
+
))
|
|
213
|
+
return tuple(periods)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _native_limit_label(limit_name: object, window_minutes: object) -> str:
|
|
217
|
+
"""Prefer provider label text, deriving duration copy only when absent."""
|
|
218
|
+
if isinstance(limit_name, str) and limit_name.strip():
|
|
219
|
+
return limit_name.strip()
|
|
220
|
+
if window_minutes == 300:
|
|
221
|
+
return "5-hour limit"
|
|
222
|
+
if window_minutes == 10_080:
|
|
223
|
+
return "7-day limit"
|
|
224
|
+
if not isinstance(window_minutes, int) or isinstance(window_minutes, bool) or window_minutes <= 0:
|
|
225
|
+
return "Codex quota"
|
|
226
|
+
if window_minutes % 1_440 == 0:
|
|
227
|
+
return f"{window_minutes // 1_440}-day limit"
|
|
228
|
+
if window_minutes % 60 == 0:
|
|
229
|
+
return f"{window_minutes // 60}-hour limit"
|
|
230
|
+
return f"{window_minutes}-minute limit"
|
|
231
|
+
|
|
232
|
+
|
|
69
233
|
@dataclass(frozen=True)
|
|
70
234
|
class DashboardSourceSemantics:
|
|
71
235
|
"""One canonical CLI configuration resolution for a dashboard read.
|
|
@@ -298,6 +462,7 @@ def _bucket_wire(bucket: Any) -> dict[str, object]:
|
|
|
298
462
|
"reasoning_output_tokens": bucket.reasoning_output_tokens,
|
|
299
463
|
"total_tokens": bucket.total_tokens,
|
|
300
464
|
"models": tuple(bucket.models),
|
|
465
|
+
"model_breakdowns": tuple(dict(row) for row in bucket.model_breakdowns),
|
|
301
466
|
}
|
|
302
467
|
|
|
303
468
|
|
|
@@ -310,20 +475,179 @@ def _period_wire(view: Any) -> dict[str, object]:
|
|
|
310
475
|
}
|
|
311
476
|
|
|
312
477
|
|
|
313
|
-
def
|
|
478
|
+
def _codex_conversation_metadata(
|
|
479
|
+
cache_conn: sqlite3.Connection,
|
|
480
|
+
) -> dict[tuple[str, str], dict[str, object]]:
|
|
481
|
+
"""Read task short names and cached project metadata by rooted rollout.
|
|
482
|
+
|
|
483
|
+
``state_5.sqlite.threads.title`` is Codex's persisted user-facing task name.
|
|
484
|
+
Conversation rollup titles are derived from prompt text and therefore must
|
|
485
|
+
never be substituted for that name on the dashboard. Accounting remains
|
|
486
|
+
authoritative for totals; conversation rollups only decorate projects.
|
|
487
|
+
"""
|
|
488
|
+
metadata: dict[tuple[str, str], dict[str, object]] = {}
|
|
489
|
+
try:
|
|
490
|
+
rows = tuple(cache_conn.execute(
|
|
491
|
+
"SELECT t.source_root_key, t.source_path, t.native_thread_id, "
|
|
492
|
+
"(SELECT e.session_id FROM codex_session_entries AS e "
|
|
493
|
+
" WHERE e.source_root_key=t.source_root_key AND e.source_path=t.source_path "
|
|
494
|
+
" ORDER BY e.id LIMIT 1) AS accounting_session_id, "
|
|
495
|
+
"r.project_key, r.project_label, r.started_utc "
|
|
496
|
+
"FROM codex_conversation_threads AS t "
|
|
497
|
+
"LEFT JOIN codex_conversation_rollups AS r "
|
|
498
|
+
"ON r.conversation_key=t.conversation_key "
|
|
499
|
+
"ORDER BY r.last_activity_utc DESC, t.conversation_key DESC"
|
|
500
|
+
))
|
|
501
|
+
file_aliases = tuple(cache_conn.execute(
|
|
502
|
+
"SELECT source_root_key, path, last_native_thread_id, last_session_id "
|
|
503
|
+
"FROM codex_session_files "
|
|
504
|
+
"WHERE last_native_thread_id IS NOT NULL AND last_native_thread_id != '' "
|
|
505
|
+
"ORDER BY last_ingested_at DESC, path DESC"
|
|
506
|
+
))
|
|
507
|
+
native_ids = tuple(sorted({
|
|
508
|
+
str(native_thread_id) for _, _, native_thread_id, *_ in rows
|
|
509
|
+
if isinstance(native_thread_id, str) and native_thread_id
|
|
510
|
+
} | {
|
|
511
|
+
str(native_thread_id) for _, _, native_thread_id, _ in file_aliases
|
|
512
|
+
if isinstance(native_thread_id, str) and native_thread_id
|
|
513
|
+
}))
|
|
514
|
+
provider_roots = {
|
|
515
|
+
str(root_key): pathlib.Path(root_path)
|
|
516
|
+
for root_key, root_path in cache_conn.execute(
|
|
517
|
+
"SELECT source_root_key, canonical_root_path FROM codex_source_roots "
|
|
518
|
+
"ORDER BY source_root_key"
|
|
519
|
+
)
|
|
520
|
+
if isinstance(root_key, str) and root_key
|
|
521
|
+
and isinstance(root_path, str) and root_path
|
|
522
|
+
}
|
|
523
|
+
short_names: dict[str, str] = {}
|
|
524
|
+
for provider_root in provider_roots.values():
|
|
525
|
+
state_path = provider_root / "state_5.sqlite"
|
|
526
|
+
if not state_path.is_file():
|
|
527
|
+
continue
|
|
528
|
+
state_conn: sqlite3.Connection | None = None
|
|
529
|
+
try:
|
|
530
|
+
state_conn = sqlite3.connect(
|
|
531
|
+
f"{state_path.resolve().as_uri()}?mode=ro",
|
|
532
|
+
uri=True,
|
|
533
|
+
timeout=0.05,
|
|
534
|
+
)
|
|
535
|
+
for offset in range(0, len(native_ids), 500):
|
|
536
|
+
batch = native_ids[offset:offset + 500]
|
|
537
|
+
if not batch:
|
|
538
|
+
continue
|
|
539
|
+
placeholders = ",".join("?" for _ in batch)
|
|
540
|
+
for thread_id, title in state_conn.execute(
|
|
541
|
+
f"SELECT id, title FROM threads WHERE id IN ({placeholders})",
|
|
542
|
+
batch,
|
|
543
|
+
):
|
|
544
|
+
clean_title = " ".join(str(title or "").split())
|
|
545
|
+
if clean_title:
|
|
546
|
+
short_names[str(thread_id)] = clean_title
|
|
547
|
+
except (OSError, sqlite3.Error):
|
|
548
|
+
continue
|
|
549
|
+
finally:
|
|
550
|
+
if state_conn is not None:
|
|
551
|
+
state_conn.close()
|
|
552
|
+
|
|
553
|
+
metadata_by_native: dict[tuple[str, str], dict[str, object]] = {}
|
|
554
|
+
for (
|
|
555
|
+
root_key, source_path, native_thread_id, accounting_session_id,
|
|
556
|
+
project_key, project_label, started_at,
|
|
557
|
+
) in rows:
|
|
558
|
+
identity = (str(root_key or ""), str(source_path or ""))
|
|
559
|
+
if not all(identity) or identity in metadata:
|
|
560
|
+
continue
|
|
561
|
+
item = {
|
|
562
|
+
"title": short_names.get(str(native_thread_id or "")),
|
|
563
|
+
"native_thread_id": native_thread_id,
|
|
564
|
+
"accounting_session_id": accounting_session_id,
|
|
565
|
+
"root_path": str(provider_roots.get(identity[0]) or ""),
|
|
566
|
+
"project_key": project_key,
|
|
567
|
+
"project_label": project_label,
|
|
568
|
+
"started_at": started_at,
|
|
569
|
+
}
|
|
570
|
+
metadata[identity] = item
|
|
571
|
+
native_identity = (identity[0], str(native_thread_id or ""))
|
|
572
|
+
existing = metadata_by_native.get(native_identity)
|
|
573
|
+
if existing is None or (not existing.get("project_key") and project_key):
|
|
574
|
+
metadata_by_native[native_identity] = item
|
|
575
|
+
|
|
576
|
+
# A child rollout can be accounting-complete while its own historical
|
|
577
|
+
# conversation-thread row is absent (for example, a file first cached
|
|
578
|
+
# before conversation normalization was introduced). The cursor still
|
|
579
|
+
# persists the rooted native thread id. Inherit only presentation
|
|
580
|
+
# metadata from that rooted task; the child's accounting path and
|
|
581
|
+
# session id remain its own identity and totals are never merged.
|
|
582
|
+
for root_key, source_path, native_thread_id, accounting_session_id in file_aliases:
|
|
583
|
+
identity = (str(root_key or ""), str(source_path or ""))
|
|
584
|
+
if not all(identity) or identity in metadata:
|
|
585
|
+
continue
|
|
586
|
+
native_identity = (identity[0], str(native_thread_id or ""))
|
|
587
|
+
inherited = metadata_by_native.get(native_identity)
|
|
588
|
+
metadata[identity] = {
|
|
589
|
+
"title": short_names.get(native_identity[1]) or (inherited or {}).get("title"),
|
|
590
|
+
"native_thread_id": native_thread_id,
|
|
591
|
+
"accounting_session_id": accounting_session_id,
|
|
592
|
+
"root_path": str(provider_roots.get(identity[0]) or ""),
|
|
593
|
+
"project_key": (inherited or {}).get("project_key"),
|
|
594
|
+
"project_label": (inherited or {}).get("project_label"),
|
|
595
|
+
"started_at": (inherited or {}).get("started_at"),
|
|
596
|
+
}
|
|
597
|
+
except sqlite3.Error:
|
|
598
|
+
return {}
|
|
599
|
+
return metadata
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def _session_wire(
|
|
603
|
+
view: Any,
|
|
604
|
+
*,
|
|
605
|
+
metadata: Mapping[tuple[str, str], Mapping[str, object]] | None = None,
|
|
606
|
+
) -> dict[str, object]:
|
|
314
607
|
rows = []
|
|
315
|
-
for
|
|
608
|
+
for row in view.rows:
|
|
316
609
|
# The Codex session aggregator intentionally splits equal relative
|
|
317
610
|
# session paths from distinct $CODEX_HOME roots. The opaque detail
|
|
318
611
|
# key must use that same grouping identity or two visible rows route
|
|
319
612
|
# to one another's detail payload.
|
|
320
613
|
root_identity = row.codex_root or "single-root"
|
|
614
|
+
row_metadata = (metadata or {}).get((str(row.codex_root or ""), str(row.session_id_path)))
|
|
615
|
+
if row_metadata is None and metadata is not None:
|
|
616
|
+
row_metadata = next((
|
|
617
|
+
value for (root_key, source_path), value in metadata.items()
|
|
618
|
+
if (
|
|
619
|
+
str(value.get("native_thread_id") or "") == str(row.session_id or "")
|
|
620
|
+
or str(value.get("accounting_session_id") or "") == str(row.session_id or "")
|
|
621
|
+
or source_path == str(row.session_id_path)
|
|
622
|
+
)
|
|
623
|
+
and (
|
|
624
|
+
not row.codex_root
|
|
625
|
+
or str(row.codex_root) in (
|
|
626
|
+
root_key,
|
|
627
|
+
str(value.get("root_path") or ""),
|
|
628
|
+
)
|
|
629
|
+
)
|
|
630
|
+
), None)
|
|
631
|
+
title = str(row_metadata.get("title") or "").strip() if row_metadata else ""
|
|
632
|
+
project = str(row_metadata.get("project_label") or "").strip() if row_metadata else ""
|
|
633
|
+
started_at = row_metadata.get("started_at") if row_metadata else None
|
|
634
|
+
duration_min = None
|
|
635
|
+
if isinstance(started_at, str):
|
|
636
|
+
try:
|
|
637
|
+
started_dt = dt.datetime.fromisoformat(started_at.replace("Z", "+00:00"))
|
|
638
|
+
duration_min = max(0, round((row.last_activity.astimezone(UTC) - started_dt.astimezone(UTC)).total_seconds() / 60))
|
|
639
|
+
except (TypeError, ValueError):
|
|
640
|
+
started_at = None
|
|
321
641
|
rows.append({
|
|
322
642
|
"key": dashboard_resource_key(
|
|
323
643
|
"session", "codex", root_identity, row.session_id_path,
|
|
324
644
|
),
|
|
325
645
|
"source": "codex",
|
|
326
|
-
"label":
|
|
646
|
+
"label": title or None,
|
|
647
|
+
"project": project or None,
|
|
648
|
+
"project_key": row_metadata.get("project_key") if row_metadata else None,
|
|
649
|
+
"started_at": started_at,
|
|
650
|
+
"duration_min": duration_min,
|
|
327
651
|
"last_activity": row.last_activity.astimezone(UTC).isoformat(),
|
|
328
652
|
"cost_usd": row.cost_usd,
|
|
329
653
|
"input_tokens": row.input_tokens,
|
|
@@ -341,31 +665,111 @@ def _session_wire(view: Any) -> dict[str, object]:
|
|
|
341
665
|
}
|
|
342
666
|
|
|
343
667
|
|
|
344
|
-
def _quota_wire(
|
|
668
|
+
def _quota_wire(
|
|
669
|
+
stats_conn: sqlite3.Connection,
|
|
670
|
+
*,
|
|
671
|
+
accounting_entries: Iterable[object] = (),
|
|
672
|
+
cycle: CodexCycleBoundary | None = None,
|
|
673
|
+
now_utc: dt.datetime | None = None,
|
|
674
|
+
display_tz_name: str | None = None,
|
|
675
|
+
) -> tuple[dict[str, object], ...]:
|
|
676
|
+
"""Build current-cycle Codex 5-hour activity rows from durable windows.
|
|
677
|
+
|
|
678
|
+
The durable projection supplies the truthful native block boundaries. Cost,
|
|
679
|
+
tokens, and model splits come from root-qualified accounting inside each
|
|
680
|
+
half-open 300-minute interval. Weekly quota summaries are deliberately not
|
|
681
|
+
activity blocks and never enter this wire.
|
|
682
|
+
"""
|
|
683
|
+
if cycle is None or now_utc is None:
|
|
684
|
+
return ()
|
|
345
685
|
try:
|
|
346
686
|
rows = stats_conn.execute(
|
|
347
687
|
"SELECT source_root_key, logical_limit_key, observed_slot, window_minutes, "
|
|
348
|
-
"limit_name, resets_at_utc, current_percent, orphaned_at "
|
|
349
|
-
"FROM quota_window_blocks WHERE source='codex' "
|
|
688
|
+
"limit_name, resets_at_utc, nominal_start_at_utc, current_percent, orphaned_at "
|
|
689
|
+
"FROM quota_window_blocks WHERE source='codex' AND window_minutes=300 "
|
|
350
690
|
"ORDER BY resets_at_utc DESC, source_root_key, logical_limit_key, observed_slot "
|
|
351
691
|
"LIMIT ?",
|
|
352
692
|
(SOURCE_HISTORY_LIMIT,),
|
|
353
693
|
).fetchall()
|
|
354
694
|
except sqlite3.Error:
|
|
355
695
|
return ()
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
"resets_at": resets_at,
|
|
363
|
-
"current_percent": current_percent,
|
|
364
|
-
"orphaned": orphaned_at is not None,
|
|
365
|
-
} for (
|
|
696
|
+
entries = tuple(accounting_entries)
|
|
697
|
+
display_tz = ZoneInfo(display_tz_name) if display_tz_name else None
|
|
698
|
+
c = sys.modules["cctally"]
|
|
699
|
+
wired: list[dict[str, object]] = []
|
|
700
|
+
seen_windows: set[tuple[str, dt.datetime, dt.datetime]] = set()
|
|
701
|
+
for (
|
|
366
702
|
root_key, logical_limit_key, observed_slot, window_minutes,
|
|
367
|
-
|
|
368
|
-
) in rows
|
|
703
|
+
_limit_name, resets_at_raw, nominal_start_raw, current_percent, orphaned_at,
|
|
704
|
+
) in rows:
|
|
705
|
+
if orphaned_at is not None or str(root_key) not in cycle.source_root_keys:
|
|
706
|
+
continue
|
|
707
|
+
try:
|
|
708
|
+
start_at = dt.datetime.fromisoformat(str(nominal_start_raw).replace("Z", "+00:00"))
|
|
709
|
+
resets_at = dt.datetime.fromisoformat(str(resets_at_raw).replace("Z", "+00:00"))
|
|
710
|
+
except (TypeError, ValueError):
|
|
711
|
+
continue
|
|
712
|
+
if start_at.tzinfo is None or resets_at.tzinfo is None:
|
|
713
|
+
continue
|
|
714
|
+
start_at = start_at.astimezone(UTC)
|
|
715
|
+
resets_at = resets_at.astimezone(UTC)
|
|
716
|
+
if resets_at <= cycle.start_at or start_at >= cycle.resets_at:
|
|
717
|
+
continue
|
|
718
|
+
physical_key = (str(root_key), start_at, resets_at)
|
|
719
|
+
if physical_key in seen_windows:
|
|
720
|
+
continue
|
|
721
|
+
seen_windows.add(physical_key)
|
|
722
|
+
block_entries = tuple(
|
|
723
|
+
entry for entry in entries
|
|
724
|
+
if str(getattr(entry, "source_root_key", "")) == str(root_key)
|
|
725
|
+
and start_at <= getattr(entry, "timestamp").astimezone(UTC) < resets_at
|
|
726
|
+
)
|
|
727
|
+
if not block_entries:
|
|
728
|
+
continue
|
|
729
|
+
by_model: dict[str, dict[str, object]] = {}
|
|
730
|
+
for entry in block_entries:
|
|
731
|
+
model = str(getattr(entry, "model", "") or "unknown")
|
|
732
|
+
aggregate = by_model.setdefault(model, {
|
|
733
|
+
"modelName": model,
|
|
734
|
+
"inputTokens": 0,
|
|
735
|
+
"cachedInputTokens": 0,
|
|
736
|
+
"outputTokens": 0,
|
|
737
|
+
"reasoningOutputTokens": 0,
|
|
738
|
+
"totalTokens": 0,
|
|
739
|
+
"costParts": [],
|
|
740
|
+
})
|
|
741
|
+
aggregate["inputTokens"] += int(getattr(entry, "input_tokens", 0))
|
|
742
|
+
aggregate["cachedInputTokens"] += int(getattr(entry, "cached_input_tokens", 0))
|
|
743
|
+
aggregate["outputTokens"] += int(getattr(entry, "output_tokens", 0))
|
|
744
|
+
aggregate["reasoningOutputTokens"] += int(getattr(entry, "reasoning_output_tokens", 0))
|
|
745
|
+
aggregate["totalTokens"] += int(getattr(entry, "total_tokens", 0))
|
|
746
|
+
aggregate["costParts"].append(float(getattr(entry, "cost_usd", 0.0)))
|
|
747
|
+
breakdowns: list[dict[str, object]] = []
|
|
748
|
+
for aggregate in by_model.values():
|
|
749
|
+
cost = stable_sum(aggregate.pop("costParts"))
|
|
750
|
+
breakdowns.append({**aggregate, "cost": cost})
|
|
751
|
+
breakdowns.sort(key=lambda row: (-float(row["cost"]), str(row["modelName"])))
|
|
752
|
+
cost_usd = stable_sum(float(row["cost"]) for row in breakdowns)
|
|
753
|
+
wired.append({
|
|
754
|
+
"key": dashboard_resource_key(
|
|
755
|
+
"block", "codex", root_key, logical_limit_key,
|
|
756
|
+
observed_slot, window_minutes, resets_at_raw,
|
|
757
|
+
),
|
|
758
|
+
"source": "codex",
|
|
759
|
+
"label": c.format_display_dt(
|
|
760
|
+
start_at, display_tz, fmt="%H:%M %b %d", suffix=True,
|
|
761
|
+
),
|
|
762
|
+
"window_minutes": window_minutes,
|
|
763
|
+
"start_at": start_at.isoformat(),
|
|
764
|
+
"end_at": resets_at.isoformat(),
|
|
765
|
+
"resets_at": resets_at_raw,
|
|
766
|
+
"current_percent": current_percent,
|
|
767
|
+
"orphaned": False,
|
|
768
|
+
"is_active": start_at <= now_utc < resets_at,
|
|
769
|
+
"cost_usd": cost_usd,
|
|
770
|
+
"model_breakdowns": tuple(breakdowns),
|
|
771
|
+
})
|
|
772
|
+
return tuple(wired)
|
|
369
773
|
|
|
370
774
|
|
|
371
775
|
def _budget_wire(stats_conn: sqlite3.Connection) -> tuple[dict[str, object], ...]:
|
|
@@ -511,7 +915,7 @@ def _quota_read_model(
|
|
|
511
915
|
history_rows.append({
|
|
512
916
|
"key": dashboard_resource_key("quota", "codex", *key_parts),
|
|
513
917
|
"source": "codex",
|
|
514
|
-
"label": identity.limit_name
|
|
918
|
+
"label": _native_limit_label(identity.limit_name, identity.window_minutes),
|
|
515
919
|
"observed_slot": identity.observed_slot,
|
|
516
920
|
"window_minutes": identity.window_minutes,
|
|
517
921
|
"current_percent": baseline.used_percent if baseline is not None else None,
|
|
@@ -607,6 +1011,41 @@ def _clock_freshness(
|
|
|
607
1011
|
return "stale" if age_seconds > stale_after else "fresh"
|
|
608
1012
|
|
|
609
1013
|
|
|
1014
|
+
def _clock_cycle_validity(
|
|
1015
|
+
histories: Iterable[object],
|
|
1016
|
+
now_utc: dt.datetime,
|
|
1017
|
+
) -> tuple[bool, str]:
|
|
1018
|
+
"""Re-evaluate frozen weekly evidence without touching cache or rollouts."""
|
|
1019
|
+
boundaries: set[dt.datetime] = set()
|
|
1020
|
+
stale_weekly_evidence = False
|
|
1021
|
+
for raw_history in histories:
|
|
1022
|
+
if not isinstance(raw_history, Mapping):
|
|
1023
|
+
continue
|
|
1024
|
+
if raw_history.get("window_minutes") != 10_080:
|
|
1025
|
+
continue
|
|
1026
|
+
current = raw_history.get("current_percent")
|
|
1027
|
+
forecast = raw_history.get("forecast")
|
|
1028
|
+
if current is None or not isinstance(forecast, Mapping):
|
|
1029
|
+
continue
|
|
1030
|
+
try:
|
|
1031
|
+
resets_at = dt.datetime.fromisoformat(
|
|
1032
|
+
str(forecast.get("resets_at")).replace("Z", "+00:00")
|
|
1033
|
+
).astimezone(UTC)
|
|
1034
|
+
except (TypeError, ValueError):
|
|
1035
|
+
continue
|
|
1036
|
+
if resets_at <= now_utc:
|
|
1037
|
+
continue
|
|
1038
|
+
if raw_history.get("freshness") != "fresh":
|
|
1039
|
+
stale_weekly_evidence = True
|
|
1040
|
+
continue
|
|
1041
|
+
boundaries.add(resets_at)
|
|
1042
|
+
if len(boundaries) == 1:
|
|
1043
|
+
return True, "ok"
|
|
1044
|
+
if not boundaries and stale_weekly_evidence:
|
|
1045
|
+
return False, "stale"
|
|
1046
|
+
return False, "missing" if not boundaries else "conflicting"
|
|
1047
|
+
|
|
1048
|
+
|
|
610
1049
|
def _refresh_budget_status_clock(
|
|
611
1050
|
status: Mapping[str, object] | None,
|
|
612
1051
|
now_utc: dt.datetime,
|
|
@@ -676,6 +1115,11 @@ def refresh_codex_source_clock(
|
|
|
676
1115
|
data = dict(state.data)
|
|
677
1116
|
quota = data.get("quota")
|
|
678
1117
|
quota_changed = False
|
|
1118
|
+
cycle_changed = False
|
|
1119
|
+
capabilities = state.capabilities
|
|
1120
|
+
warnings = state.warnings
|
|
1121
|
+
availability = state.availability
|
|
1122
|
+
freshness = state.freshness
|
|
679
1123
|
if isinstance(quota, Mapping):
|
|
680
1124
|
quota = dict(quota)
|
|
681
1125
|
refreshed_histories: list[dict[str, object]] = []
|
|
@@ -757,6 +1201,40 @@ def refresh_codex_source_clock(
|
|
|
757
1201
|
quota["summary"] = summary
|
|
758
1202
|
data["quota"] = quota
|
|
759
1203
|
quota_changed = bool(refreshed_histories)
|
|
1204
|
+
hero = data.get("hero")
|
|
1205
|
+
hero_capability = state.capabilities.get("hero")
|
|
1206
|
+
if (
|
|
1207
|
+
isinstance(hero, Mapping)
|
|
1208
|
+
and isinstance(hero.get("cycle"), Mapping)
|
|
1209
|
+
and hero_capability is not None
|
|
1210
|
+
and hero_capability.status == "supported"
|
|
1211
|
+
):
|
|
1212
|
+
cycle_valid, cycle_reason = _clock_cycle_validity(refreshed_histories, now_utc)
|
|
1213
|
+
if not cycle_valid:
|
|
1214
|
+
hero = dict(hero)
|
|
1215
|
+
for field in (
|
|
1216
|
+
"cost_usd", "input_tokens", "cached_input_tokens", "output_tokens",
|
|
1217
|
+
"reasoning_output_tokens", "total_tokens", "cycle",
|
|
1218
|
+
):
|
|
1219
|
+
hero[field] = None
|
|
1220
|
+
data["hero"] = hero
|
|
1221
|
+
refreshed_capabilities = dict(state.capabilities)
|
|
1222
|
+
refreshed_capabilities["hero"] = CapabilityRecord(
|
|
1223
|
+
"unavailable", "missing-or-conflicting-native-cycle",
|
|
1224
|
+
)
|
|
1225
|
+
capabilities = refreshed_capabilities
|
|
1226
|
+
warnings = tuple(
|
|
1227
|
+
warning for warning in state.warnings
|
|
1228
|
+
if warning.code != "codex_cycle_unavailable"
|
|
1229
|
+
) + (SourceDashboardWarning(
|
|
1230
|
+
"codex_cycle_unavailable",
|
|
1231
|
+
"Codex native reset cycle is unavailable.",
|
|
1232
|
+
"hero",
|
|
1233
|
+
),)
|
|
1234
|
+
availability = "partial"
|
|
1235
|
+
if cycle_reason == "stale":
|
|
1236
|
+
freshness = "stale"
|
|
1237
|
+
cycle_changed = True
|
|
760
1238
|
budget_domain = data.get("budget")
|
|
761
1239
|
budget_changed = False
|
|
762
1240
|
if isinstance(budget_domain, Mapping):
|
|
@@ -778,16 +1256,16 @@ def refresh_codex_source_clock(
|
|
|
778
1256
|
hero["budget"] = refreshed_budget
|
|
779
1257
|
data["hero"] = hero
|
|
780
1258
|
budget_changed = True
|
|
781
|
-
if not (quota_changed or budget_changed):
|
|
1259
|
+
if not (quota_changed or budget_changed or cycle_changed):
|
|
782
1260
|
return state
|
|
783
1261
|
refreshed_state = SourceDashboardState(
|
|
784
1262
|
source=state.source,
|
|
785
|
-
availability=
|
|
786
|
-
freshness=
|
|
787
|
-
warnings=
|
|
1263
|
+
availability=availability,
|
|
1264
|
+
freshness=freshness,
|
|
1265
|
+
warnings=warnings,
|
|
788
1266
|
data_version=state.data_version,
|
|
789
1267
|
last_success_at=state.last_success_at,
|
|
790
|
-
capabilities=
|
|
1268
|
+
capabilities=capabilities,
|
|
791
1269
|
data=data,
|
|
792
1270
|
clock_data=state.clock_data,
|
|
793
1271
|
)
|
|
@@ -888,14 +1366,83 @@ def _projects_wire(
|
|
|
888
1366
|
}
|
|
889
1367
|
|
|
890
1368
|
|
|
891
|
-
def
|
|
892
|
-
|
|
1369
|
+
def _partial_projects_wire(
|
|
1370
|
+
entries: Iterable[object],
|
|
1371
|
+
metadata: Mapping[tuple[str, str], Mapping[str, object]],
|
|
1372
|
+
) -> dict[str, object]:
|
|
1373
|
+
"""Aggregate the qualified subset when older accounting metadata is mixed.
|
|
1374
|
+
|
|
1375
|
+
Rows without a cached conversation/project identity are omitted and remain
|
|
1376
|
+
covered by the Projects-domain warning. Valid projects stay visible; their
|
|
1377
|
+
totals never include an unqualified accounting row.
|
|
1378
|
+
"""
|
|
1379
|
+
groups: dict[tuple[str, str], dict[str, object]] = {}
|
|
1380
|
+
for entry in entries:
|
|
1381
|
+
identity = (
|
|
1382
|
+
str(getattr(entry, "source_root_key", "") or ""),
|
|
1383
|
+
str(getattr(entry, "source_path", "") or ""),
|
|
1384
|
+
)
|
|
1385
|
+
row_metadata = metadata.get(identity)
|
|
1386
|
+
project_key = str(row_metadata.get("project_key") or "").strip() if row_metadata else ""
|
|
1387
|
+
project_label = str(row_metadata.get("project_label") or "").strip() if row_metadata else ""
|
|
1388
|
+
if not project_key or not project_label:
|
|
1389
|
+
continue
|
|
1390
|
+
group_key = (identity[0], project_key)
|
|
1391
|
+
group = groups.setdefault(group_key, {
|
|
1392
|
+
"project_key": project_key,
|
|
1393
|
+
"label": project_label,
|
|
1394
|
+
"sessions": set(),
|
|
1395
|
+
"first_seen": getattr(entry, "timestamp"),
|
|
1396
|
+
"last_seen": getattr(entry, "timestamp"),
|
|
1397
|
+
"cost_usd": 0.0,
|
|
1398
|
+
"input_tokens": 0,
|
|
1399
|
+
"cached_input_tokens": 0,
|
|
1400
|
+
"output_tokens": 0,
|
|
1401
|
+
"reasoning_output_tokens": 0,
|
|
1402
|
+
"total_tokens": 0,
|
|
1403
|
+
})
|
|
1404
|
+
timestamp = getattr(entry, "timestamp")
|
|
1405
|
+
group["first_seen"] = min(group["first_seen"], timestamp)
|
|
1406
|
+
group["last_seen"] = max(group["last_seen"], timestamp)
|
|
1407
|
+
group["sessions"].add(identity)
|
|
1408
|
+
for field in (
|
|
1409
|
+
"cost_usd", "input_tokens", "cached_input_tokens", "output_tokens",
|
|
1410
|
+
"reasoning_output_tokens", "total_tokens",
|
|
1411
|
+
):
|
|
1412
|
+
group[field] += getattr(entry, field)
|
|
1413
|
+
|
|
1414
|
+
rows = []
|
|
1415
|
+
for (root_key, _project_key), group in groups.items():
|
|
1416
|
+
rows.append({
|
|
1417
|
+
"key": dashboard_resource_key("project", "codex", root_key, group["project_key"]),
|
|
1418
|
+
"source": "codex",
|
|
1419
|
+
"label": group["label"],
|
|
1420
|
+
"session_count": len(group["sessions"]),
|
|
1421
|
+
"first_seen": group["first_seen"].astimezone(UTC).isoformat(),
|
|
1422
|
+
"last_seen": group["last_seen"].astimezone(UTC).isoformat(),
|
|
1423
|
+
"cost_usd": group["cost_usd"],
|
|
1424
|
+
"input_tokens": group["input_tokens"],
|
|
1425
|
+
"cached_input_tokens": group["cached_input_tokens"],
|
|
1426
|
+
"output_tokens": group["output_tokens"],
|
|
1427
|
+
"reasoning_output_tokens": group["reasoning_output_tokens"],
|
|
1428
|
+
"total_tokens": group["total_tokens"],
|
|
1429
|
+
})
|
|
1430
|
+
rows.sort(key=lambda row: (-float(row["cost_usd"]), str(row["label"]), str(row["key"])))
|
|
1431
|
+
return {
|
|
1432
|
+
"rows": tuple(rows),
|
|
1433
|
+
"total_cost_usd": stable_sum(float(row["cost_usd"]) for row in rows),
|
|
1434
|
+
"total_tokens": sum(int(row["total_tokens"]) for row in rows),
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
|
|
1438
|
+
def _codex_entries_from_accounting(entries: Iterable[object]) -> list[CodexEntry]:
|
|
1439
|
+
"""Adapt coordinated accounting rows for the shipped non-project kernels."""
|
|
893
1440
|
converted: list[CodexEntry] = []
|
|
894
1441
|
for entry in entries:
|
|
895
1442
|
source_path = str(getattr(entry, "source_path", "") or "")
|
|
896
1443
|
session_id = str(getattr(entry, "session_id", "") or "")
|
|
897
1444
|
if not source_path or not session_id:
|
|
898
|
-
raise SourceCapabilityUnavailable("
|
|
1445
|
+
raise SourceCapabilityUnavailable("Codex accounting lacks session identity")
|
|
899
1446
|
converted.append(CodexEntry(
|
|
900
1447
|
timestamp=getattr(entry, "timestamp"),
|
|
901
1448
|
session_id=session_id,
|
|
@@ -910,6 +1457,67 @@ def _codex_entries_from_qualified(entries: Iterable[object]) -> list[CodexEntry]
|
|
|
910
1457
|
return converted
|
|
911
1458
|
|
|
912
1459
|
|
|
1460
|
+
def _codex_entries_from_qualified(entries: Iterable[object]) -> list[CodexEntry]:
|
|
1461
|
+
"""Compatibility name retained for the source-detail reader."""
|
|
1462
|
+
return _codex_entries_from_accounting(entries)
|
|
1463
|
+
|
|
1464
|
+
|
|
1465
|
+
def _build_codex_native_weekly_view(
|
|
1466
|
+
stats_conn: sqlite3.Connection,
|
|
1467
|
+
entries: Iterable[object],
|
|
1468
|
+
*,
|
|
1469
|
+
source_root_keys: Iterable[str],
|
|
1470
|
+
active_cycle: CodexCycleBoundary | None,
|
|
1471
|
+
now_utc: dt.datetime,
|
|
1472
|
+
display_tz_name: str | None,
|
|
1473
|
+
speed: str,
|
|
1474
|
+
) -> CodexWeeklyView:
|
|
1475
|
+
"""Aggregate Codex cost into observed native quota-cycle segments."""
|
|
1476
|
+
periods = _codex_weekly_periods(
|
|
1477
|
+
stats_conn,
|
|
1478
|
+
source_root_keys=source_root_keys,
|
|
1479
|
+
active_cycle=active_cycle,
|
|
1480
|
+
)
|
|
1481
|
+
converted: list[CodexEntry] = []
|
|
1482
|
+
bucket_by_entry: dict[int, str] = {}
|
|
1483
|
+
display_tz = ZoneInfo(display_tz_name) if display_tz_name else None
|
|
1484
|
+
labels: dict[str, str] = {}
|
|
1485
|
+
for entry in entries:
|
|
1486
|
+
timestamp = getattr(entry, "timestamp").astimezone(UTC)
|
|
1487
|
+
root_key = str(getattr(entry, "source_root_key", "") or "")
|
|
1488
|
+
period = next((
|
|
1489
|
+
candidate for candidate in periods
|
|
1490
|
+
if root_key in candidate.source_root_keys
|
|
1491
|
+
and candidate.start_at <= timestamp < candidate.end_at
|
|
1492
|
+
), None)
|
|
1493
|
+
if period is None:
|
|
1494
|
+
continue
|
|
1495
|
+
converted_entry = _codex_entries_from_accounting((entry,))[0]
|
|
1496
|
+
bucket = period.start_at.isoformat()
|
|
1497
|
+
converted.append(converted_entry)
|
|
1498
|
+
bucket_by_entry[id(converted_entry)] = bucket
|
|
1499
|
+
local_start = (
|
|
1500
|
+
period.start_at.astimezone(display_tz)
|
|
1501
|
+
if display_tz is not None else period.start_at.astimezone()
|
|
1502
|
+
)
|
|
1503
|
+
labels[bucket] = local_start.strftime("%m-%d %H:%M")
|
|
1504
|
+
|
|
1505
|
+
rows = _aggregate_codex_buckets(
|
|
1506
|
+
converted,
|
|
1507
|
+
key_fn=lambda entry: bucket_by_entry[id(entry)],
|
|
1508
|
+
speed=speed,
|
|
1509
|
+
)
|
|
1510
|
+
display_rows = tuple(replace(row, bucket=labels[row.bucket]) for row in rows)
|
|
1511
|
+
return CodexWeeklyView(
|
|
1512
|
+
rows=display_rows,
|
|
1513
|
+
total_cost_usd=stable_sum(row.cost_usd for row in display_rows),
|
|
1514
|
+
total_tokens=sum(row.total_tokens for row in display_rows),
|
|
1515
|
+
period_start=(periods[0].start_at if periods else None),
|
|
1516
|
+
period_end=now_utc,
|
|
1517
|
+
display_tz_label=display_tz_name or str(dt.datetime.now().astimezone().tzinfo),
|
|
1518
|
+
)
|
|
1519
|
+
|
|
1520
|
+
|
|
913
1521
|
def build_codex_source_state(
|
|
914
1522
|
context: DashboardReadContext,
|
|
915
1523
|
*,
|
|
@@ -938,8 +1546,7 @@ def build_codex_source_state(
|
|
|
938
1546
|
coherence = codex_projection_coherence(
|
|
939
1547
|
context,
|
|
940
1548
|
)
|
|
941
|
-
|
|
942
|
-
raise CodexProjectionIncoherent(coherence.reason)
|
|
1549
|
+
projection_incoherent = not coherence.coherent
|
|
943
1550
|
# The cache reader's established report surface treats the ``now`` instant
|
|
944
1551
|
# as inclusive. The qualified adapter is half-open, so extend only its
|
|
945
1552
|
# query/result boundary by one microsecond and keep all live budget sums
|
|
@@ -949,37 +1556,119 @@ def build_codex_source_state(
|
|
|
949
1556
|
if context.codex_budget is not None:
|
|
950
1557
|
_period, budget_start, _budget_end = _configured_codex_budget_window(context)
|
|
951
1558
|
accounting_start = min(accounting_start, budget_start)
|
|
952
|
-
|
|
953
|
-
accounting_start,
|
|
954
|
-
accounting_end,
|
|
955
|
-
speed=context.speed,
|
|
956
|
-
sync=False,
|
|
1559
|
+
health = load_codex_project_metadata_health(
|
|
957
1560
|
cache_conn=context.cache_conn,
|
|
1561
|
+
start=accounting_start,
|
|
1562
|
+
end=accounting_end,
|
|
958
1563
|
)
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1564
|
+
metadata_incomplete = health.incomplete_rows > 0
|
|
1565
|
+
metadata_warning_message = (
|
|
1566
|
+
f"{health.incomplete_rows} Codex accounting row(s) lack project metadata; "
|
|
1567
|
+
"run `cctally cache-sync --source codex --rebuild`."
|
|
1568
|
+
if metadata_incomplete
|
|
1569
|
+
else "Codex project metadata could not be read; "
|
|
1570
|
+
"run `cctally cache-sync --source codex --rebuild`."
|
|
1571
|
+
)
|
|
1572
|
+
qualified_entries: tuple[object, ...] = ()
|
|
1573
|
+
if not metadata_incomplete:
|
|
1574
|
+
try:
|
|
1575
|
+
qualified_entries = load_qualified_codex_entries(
|
|
1576
|
+
accounting_start,
|
|
1577
|
+
accounting_end,
|
|
1578
|
+
speed=context.speed,
|
|
1579
|
+
sync=False,
|
|
1580
|
+
cache_conn=context.cache_conn,
|
|
1581
|
+
)
|
|
1582
|
+
accounting_entries: tuple[object, ...] = qualified_entries
|
|
1583
|
+
except QualifiedMetadataUnavailable:
|
|
1584
|
+
# A cached read must be internally coherent, but retain accounting
|
|
1585
|
+
# once if a defensive race or malformed row violates that premise.
|
|
1586
|
+
_lib_log.get_logger("dashboard").warning(
|
|
1587
|
+
"Codex qualified metadata read became unavailable; using cache-only accounting fallback"
|
|
1588
|
+
)
|
|
1589
|
+
metadata_incomplete = True
|
|
1590
|
+
accounting_entries = load_cached_rooted_codex_accounting_entries(
|
|
1591
|
+
accounting_start,
|
|
1592
|
+
accounting_end,
|
|
1593
|
+
speed=context.speed,
|
|
1594
|
+
cache_conn=context.cache_conn,
|
|
1595
|
+
)
|
|
1596
|
+
else:
|
|
1597
|
+
accounting_entries = load_cached_rooted_codex_accounting_entries(
|
|
1598
|
+
accounting_start,
|
|
1599
|
+
accounting_end,
|
|
1600
|
+
speed=context.speed,
|
|
1601
|
+
cache_conn=context.cache_conn,
|
|
1602
|
+
)
|
|
1603
|
+
budget_entries = _codex_entries_from_accounting(accounting_entries)
|
|
1604
|
+
cycle_reason: str | None = None
|
|
1605
|
+
try:
|
|
1606
|
+
cycle = _resolve_codex_weekly_cycle(quota_observations, context.now_utc)
|
|
1607
|
+
except CodexCycleUnavailable as exc:
|
|
1608
|
+
cycle = None
|
|
1609
|
+
cycle_reason = exc.reason
|
|
1610
|
+
cycle_failure = cycle is None and has_cached_codex_accounting_entries(
|
|
1611
|
+
cache_conn=context.cache_conn,
|
|
1612
|
+
)
|
|
1613
|
+
hero_failure = projection_incoherent or cycle_failure
|
|
1614
|
+
if cycle is None or hero_failure:
|
|
1615
|
+
cycle_entries: list[CodexEntry] = []
|
|
1616
|
+
cycle_cost_usd: float | None = None if hero_failure else 0.0
|
|
1617
|
+
else:
|
|
1618
|
+
cycle_end = min(accounting_end, cycle.resets_at)
|
|
1619
|
+
cycle_rows = load_cached_rooted_codex_accounting_entries(
|
|
1620
|
+
cycle.start_at,
|
|
1621
|
+
cycle_end,
|
|
1622
|
+
speed=context.speed,
|
|
1623
|
+
cache_conn=context.cache_conn,
|
|
1624
|
+
source_root_keys=cycle.source_root_keys,
|
|
1625
|
+
)
|
|
1626
|
+
cycle_entries = _codex_entries_from_accounting(cycle_rows)
|
|
1627
|
+
cycle_cost_usd = build_codex_daily_view(
|
|
1628
|
+
cycle_entries,
|
|
1629
|
+
now_utc=context.now_utc,
|
|
1630
|
+
tz_name=context.display_tz_name,
|
|
1631
|
+
speed=context.speed,
|
|
1632
|
+
).total_cost_usd
|
|
1633
|
+
visible_accounting_entries = tuple(
|
|
1634
|
+
entry for entry in accounting_entries
|
|
962
1635
|
if context.range_start <= getattr(entry, "timestamp").astimezone(UTC) < accounting_end
|
|
963
1636
|
)
|
|
964
|
-
entries =
|
|
1637
|
+
entries = _codex_entries_from_accounting(visible_accounting_entries)
|
|
965
1638
|
daily = build_codex_daily_view(
|
|
966
1639
|
entries, now_utc=context.now_utc, tz_name=context.display_tz_name, speed=context.speed,
|
|
967
1640
|
)
|
|
968
1641
|
monthly = build_codex_monthly_view(
|
|
969
1642
|
entries, now_utc=context.now_utc, tz_name=context.display_tz_name, speed=context.speed,
|
|
970
1643
|
)
|
|
971
|
-
weekly =
|
|
972
|
-
|
|
1644
|
+
weekly = _build_codex_native_weekly_view(
|
|
1645
|
+
context.stats_conn,
|
|
1646
|
+
visible_accounting_entries,
|
|
1647
|
+
source_root_keys=active_roots,
|
|
1648
|
+
active_cycle=cycle,
|
|
973
1649
|
now_utc=context.now_utc,
|
|
974
|
-
|
|
975
|
-
week_start_idx=context.week_start_idx,
|
|
1650
|
+
display_tz_name=context.display_tz_name,
|
|
976
1651
|
speed=context.speed,
|
|
977
1652
|
)
|
|
978
|
-
sessions =
|
|
979
|
-
|
|
1653
|
+
sessions = (
|
|
1654
|
+
build_rooted_codex_session_view(
|
|
1655
|
+
visible_accounting_entries,
|
|
1656
|
+
now_utc=context.now_utc,
|
|
1657
|
+
tz_name=context.display_tz_name,
|
|
1658
|
+
speed=context.speed,
|
|
1659
|
+
)
|
|
1660
|
+
if metadata_incomplete else build_codex_session_view(
|
|
1661
|
+
entries, now_utc=context.now_utc, tz_name=context.display_tz_name, speed=context.speed,
|
|
1662
|
+
)
|
|
980
1663
|
)
|
|
981
1664
|
quota = _quota_read_model(context, quota_observations)
|
|
982
|
-
quota_blocks = _quota_wire(
|
|
1665
|
+
quota_blocks = _quota_wire(
|
|
1666
|
+
context.stats_conn,
|
|
1667
|
+
accounting_entries=visible_accounting_entries,
|
|
1668
|
+
cycle=cycle,
|
|
1669
|
+
now_utc=context.now_utc,
|
|
1670
|
+
display_tz_name=context.display_tz_name,
|
|
1671
|
+
)
|
|
983
1672
|
quota = {**quota, "blocks": quota_blocks}
|
|
984
1673
|
budget_rows = _budget_wire(context.stats_conn)
|
|
985
1674
|
projected_budget_rows = _projected_budget_wire(context.stats_conn)
|
|
@@ -987,45 +1676,94 @@ def build_codex_source_state(
|
|
|
987
1676
|
configured_budget = _configured_codex_budget_status(
|
|
988
1677
|
context, budget_entries, cost_events=budget_cost_events,
|
|
989
1678
|
)
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
1679
|
+
conversation_metadata = _codex_conversation_metadata(context.cache_conn)
|
|
1680
|
+
projects = (
|
|
1681
|
+
_partial_projects_wire(visible_accounting_entries, conversation_metadata)
|
|
1682
|
+
if metadata_incomplete else _projects_wire(
|
|
1683
|
+
context,
|
|
1684
|
+
quota_observations,
|
|
1685
|
+
visible_accounting_entries,
|
|
1686
|
+
accounting_end=accounting_end,
|
|
1687
|
+
)
|
|
995
1688
|
)
|
|
996
1689
|
alerts = _alerts_wire(context.stats_conn)
|
|
997
|
-
availability =
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1690
|
+
availability = (
|
|
1691
|
+
"partial" if metadata_incomplete or hero_failure
|
|
1692
|
+
else ("ok" if (entries or quota_blocks or budget_rows) else "empty")
|
|
1693
|
+
)
|
|
1694
|
+
hero_input = None if hero_failure else sum(entry.input_tokens for entry in cycle_entries)
|
|
1695
|
+
hero_cached = None if hero_failure else sum(entry.cached_input_tokens for entry in cycle_entries)
|
|
1696
|
+
hero_output = None if hero_failure else sum(entry.output_tokens for entry in cycle_entries)
|
|
1697
|
+
hero_reasoning = None if hero_failure else sum(entry.reasoning_output_tokens for entry in cycle_entries)
|
|
1698
|
+
hero_total = None if hero_failure else sum(entry.total_tokens for entry in cycle_entries)
|
|
1699
|
+
warnings: list[SourceDashboardWarning] = []
|
|
1700
|
+
if metadata_incomplete:
|
|
1701
|
+
warnings.append(SourceDashboardWarning(
|
|
1702
|
+
"codex_metadata_incomplete",
|
|
1703
|
+
metadata_warning_message,
|
|
1704
|
+
"projects",
|
|
1705
|
+
))
|
|
1706
|
+
if projection_incoherent:
|
|
1707
|
+
warnings.append(SourceDashboardWarning(
|
|
1708
|
+
"codex_projection_incoherent",
|
|
1709
|
+
"Codex quota projection is unavailable.",
|
|
1710
|
+
"hero",
|
|
1711
|
+
))
|
|
1712
|
+
if cycle_failure:
|
|
1713
|
+
warnings.append(SourceDashboardWarning(
|
|
1714
|
+
"codex_cycle_unavailable",
|
|
1715
|
+
"Codex native reset cycle is unavailable.",
|
|
1716
|
+
"hero",
|
|
1717
|
+
))
|
|
1002
1718
|
return SourceDashboardState(
|
|
1003
1719
|
source="codex",
|
|
1004
1720
|
availability=availability,
|
|
1005
|
-
freshness="fresh",
|
|
1006
|
-
warnings=(),
|
|
1721
|
+
freshness=("stale" if cycle_reason == "stale" else "fresh"),
|
|
1722
|
+
warnings=tuple(warnings),
|
|
1007
1723
|
data_version=data_version,
|
|
1008
1724
|
last_success_at=context.now_utc,
|
|
1009
1725
|
capabilities={
|
|
1010
|
-
"hero":
|
|
1726
|
+
"hero": (
|
|
1727
|
+
CapabilityRecord(
|
|
1728
|
+
"unavailable",
|
|
1729
|
+
(
|
|
1730
|
+
"projection-incoherent" if projection_incoherent
|
|
1731
|
+
else "missing-or-conflicting-native-cycle"
|
|
1732
|
+
),
|
|
1733
|
+
)
|
|
1734
|
+
if hero_failure
|
|
1735
|
+
else CapabilityRecord("supported", "native-reset-cycle")
|
|
1736
|
+
),
|
|
1011
1737
|
"daily": CapabilityRecord("supported", "calendar-day"),
|
|
1012
1738
|
"monthly": CapabilityRecord("supported", "calendar-month"),
|
|
1013
|
-
"weekly": CapabilityRecord("
|
|
1739
|
+
"weekly": CapabilityRecord("derived", "native-reset-cycles"),
|
|
1014
1740
|
"sessions": CapabilityRecord("supported", "inclusive-input-tokens"),
|
|
1015
1741
|
"forensics": CapabilityRecord("supported", "inclusive-input-token-reuse"),
|
|
1016
1742
|
"quota": CapabilityRecord("derived", "native-windows"),
|
|
1017
1743
|
"budget": CapabilityRecord("supported", "calendar-period"),
|
|
1018
|
-
"projects":
|
|
1744
|
+
"projects": (
|
|
1745
|
+
CapabilityRecord("supported", "conversation-metadata-partial")
|
|
1746
|
+
if metadata_incomplete
|
|
1747
|
+
else CapabilityRecord("supported", "qualified-attribution")
|
|
1748
|
+
),
|
|
1019
1749
|
"alerts": CapabilityRecord("supported", "provider-native"),
|
|
1020
1750
|
},
|
|
1021
1751
|
data={
|
|
1022
1752
|
"hero": {
|
|
1023
|
-
"cost_usd":
|
|
1024
|
-
"input_tokens":
|
|
1025
|
-
"cached_input_tokens":
|
|
1026
|
-
"output_tokens":
|
|
1027
|
-
"reasoning_output_tokens":
|
|
1028
|
-
"total_tokens":
|
|
1753
|
+
"cost_usd": cycle_cost_usd,
|
|
1754
|
+
"input_tokens": hero_input,
|
|
1755
|
+
"cached_input_tokens": hero_cached,
|
|
1756
|
+
"output_tokens": hero_output,
|
|
1757
|
+
"reasoning_output_tokens": hero_reasoning,
|
|
1758
|
+
"total_tokens": hero_total,
|
|
1759
|
+
"cycle": (
|
|
1760
|
+
{
|
|
1761
|
+
"window_minutes": cycle.window_minutes,
|
|
1762
|
+
"start_at": cycle.start_at.astimezone(UTC).isoformat(),
|
|
1763
|
+
"resets_at": cycle.resets_at.astimezone(UTC).isoformat(),
|
|
1764
|
+
}
|
|
1765
|
+
if cycle is not None and not hero_failure else None
|
|
1766
|
+
),
|
|
1029
1767
|
"quota": quota["summary"],
|
|
1030
1768
|
"budget": configured_budget,
|
|
1031
1769
|
"alerts": {"count": len(alerts)},
|
|
@@ -1035,7 +1773,7 @@ def build_codex_source_state(
|
|
|
1035
1773
|
"monthly": _period_wire(monthly),
|
|
1036
1774
|
"weekly": _period_wire(weekly),
|
|
1037
1775
|
},
|
|
1038
|
-
"sessions": _session_wire(sessions),
|
|
1776
|
+
"sessions": _session_wire(sessions, metadata=conversation_metadata),
|
|
1039
1777
|
"quota": quota,
|
|
1040
1778
|
"budget": {
|
|
1041
1779
|
"status": configured_budget,
|