cctally 1.109.0 → 1.111.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 +52 -0
- package/README.md +6 -6
- package/bin/_cctally_cache.py +62 -13
- package/bin/_cctally_core.py +8 -1
- package/bin/_cctally_dashboard.py +422 -195
- package/bin/_cctally_dashboard_conversation.py +231 -40
- package/bin/_cctally_dashboard_envelope.py +48 -32
- package/bin/_cctally_dashboard_sources.py +503 -109
- package/bin/_cctally_db.py +44 -0
- package/bin/_cctally_diagnosis_sources.py +181 -15
- package/bin/_cctally_doctor.py +15 -3
- package/bin/_cctally_forecast.py +25 -12
- package/bin/_cctally_journal.py +59 -32
- package/bin/_cctally_milestone_history.py +65 -9
- package/bin/_cctally_parser.py +5 -0
- package/bin/_cctally_record.py +50 -12
- package/bin/_cctally_rederive.py +899 -30
- package/bin/_cctally_source_analytics.py +567 -133
- package/bin/_cctally_transcript.py +27 -2
- package/bin/_cctally_tui.py +39 -6
- package/bin/_cctally_update.py +420 -14
- package/bin/_lib_cache_report.py +7 -10
- package/bin/_lib_codex_conversation_query.py +286 -63
- package/bin/_lib_codex_metadata.py +207 -0
- package/bin/_lib_conversation_query.py +254 -37
- package/bin/_lib_doctor.py +16 -2
- package/bin/_lib_pricing.py +80 -276
- package/bin/_lib_rederive.py +903 -2
- package/bin/_lib_view_models.py +5 -0
- package/bin/cctally +18 -5
- package/dashboard/static/assets/ConversationsView-BB9vUiL6.js +72 -0
- package/dashboard/static/assets/{DoctorModal-BZGXOlc2.js → DoctorModal-DFY2iPDf.js} +1 -1
- package/dashboard/static/assets/ModalRoot-DvRKHbQE.js +1 -0
- package/dashboard/static/assets/{ProjectsDrillPanel-KbO_KWa7.js → ProjectsDrillPanel-DAv39uSo.js} +1 -1
- package/dashboard/static/assets/{SourceDetailModal-BrOPLVRL.js → SourceDetailModal-DSAWo2Fp.js} +1 -1
- package/dashboard/static/assets/{UpdateModal-CYgEFAi4.js → UpdateModal-R_9dOVnh.js} +3 -3
- package/dashboard/static/assets/index-COhxe3vU.css +1 -0
- package/dashboard/static/assets/index-DdU62sov.js +13 -0
- package/dashboard/static/assets/outlineNavigation-BOCLcRa6.js +9 -0
- package/dashboard/static/assets/{useKeymap-DQx_A1MB.js → useKeymap-CW7Up0Ay.js} +1 -1
- package/dashboard/static/dashboard.html +3 -3
- package/package.json +2 -1
- package/dashboard/static/assets/ConversationsView-TO5bpIyc.js +0 -72
- package/dashboard/static/assets/ModalRoot-C_fvugTl.js +0 -1
- package/dashboard/static/assets/index-Cj_l7T7u.css +0 -1
- package/dashboard/static/assets/index-DLVwACUM.js +0 -13
- package/dashboard/static/assets/outlineNavigation-DXX-RjKl.js +0 -9
|
@@ -21,6 +21,12 @@ from _cctally_core import (
|
|
|
21
21
|
)
|
|
22
22
|
from _cctally_cache import _codex_provider_roots
|
|
23
23
|
import _lib_accounts
|
|
24
|
+
from _lib_codex_metadata import (
|
|
25
|
+
DecodedCodexProjectMetadata,
|
|
26
|
+
codex_metadata_is_malformed,
|
|
27
|
+
decode_codex_project_metadata,
|
|
28
|
+
resolve_codex_threads_table,
|
|
29
|
+
)
|
|
24
30
|
from _lib_quota import build_blocks
|
|
25
31
|
from _lib_pricing import _calculate_codex_entry_cost
|
|
26
32
|
from _cctally_quota import load_codex_quota_observations
|
|
@@ -76,10 +82,21 @@ class CodexProjectMetadataHealth:
|
|
|
76
82
|
qualified_rows: int
|
|
77
83
|
missing_conversation_key_rows: int
|
|
78
84
|
missing_thread_join_rows: int
|
|
85
|
+
#: #845 §4.3. Entries whose project metadata is present but undecodable,
|
|
86
|
+
#: counted over THIS population and net of whatever this population's
|
|
87
|
+
#: aggregate already counted for another reason.
|
|
88
|
+
undecodable_metadata_rows: int = 0
|
|
89
|
+
#: Rows with a refused timestamp or token operand, net of key/join reasons.
|
|
90
|
+
malformed_accounting_rows: int = 0
|
|
79
91
|
|
|
80
92
|
@property
|
|
81
93
|
def incomplete_rows(self) -> int:
|
|
82
|
-
return
|
|
94
|
+
return (
|
|
95
|
+
self.missing_conversation_key_rows
|
|
96
|
+
+ self.missing_thread_join_rows
|
|
97
|
+
+ self.undecodable_metadata_rows
|
|
98
|
+
+ self.malformed_accounting_rows
|
|
99
|
+
)
|
|
83
100
|
|
|
84
101
|
|
|
85
102
|
@dataclasses.dataclass(frozen=True)
|
|
@@ -101,6 +118,11 @@ class RootedCodexAccountingEntry:
|
|
|
101
118
|
# reserved ``unattributed`` sentinel). Carried so the per-account hero cards
|
|
102
119
|
# can scope spend without a second query shape.
|
|
103
120
|
account_key: str = _lib_accounts.UNATTRIBUTED
|
|
121
|
+
# #845 §4.4: the per-conversation identity the accounting-identity map is
|
|
122
|
+
# keyed by. A path-keyed map cannot carry a per-conversation decision,
|
|
123
|
+
# because one rollout path may host a healthy and a malformed conversation
|
|
124
|
+
# at once and one decision would be wrong for one of them.
|
|
125
|
+
conversation_key: str = ""
|
|
104
126
|
|
|
105
127
|
|
|
106
128
|
_OPAQUE_PROJECT_KEY_RE = re.compile(r"^project:[0-9a-f]{24}$")
|
|
@@ -112,7 +134,9 @@ _QUALIFIED_CODEX_ENTRIES_SQL = """
|
|
|
112
134
|
entries.conversation_key, entries.model, entries.account_key,
|
|
113
135
|
entries.input_tokens, entries.cached_input_tokens,
|
|
114
136
|
entries.output_tokens, entries.reasoning_output_tokens,
|
|
115
|
-
entries.total_tokens,
|
|
137
|
+
entries.total_tokens,
|
|
138
|
+
CAST(threads.cwd AS BLOB) AS cwd_blob,
|
|
139
|
+
CAST(threads.git_json AS BLOB) AS git_json_blob,
|
|
116
140
|
threads.conversation_key AS joined_conversation_key,
|
|
117
141
|
threads.source_root_key AS joined_source_root_key,
|
|
118
142
|
entries.id AS cache_entry_id
|
|
@@ -143,33 +167,166 @@ def _qualified_codex_path_entries_sql(identity_count: int) -> str:
|
|
|
143
167
|
)
|
|
144
168
|
|
|
145
169
|
|
|
146
|
-
|
|
147
|
-
|
|
170
|
+
#: The thread leg of the decode inventory (§4.3 item 1). ``{threads}`` is the
|
|
171
|
+
#: raw table reference ``resolve_codex_threads_table`` supplies at call time, so
|
|
172
|
+
#: a conversations connection reads the attached cache rather than failing, and
|
|
173
|
+
#: an account-scoped TEMP view over the unqualified name cannot hide the rows.
|
|
174
|
+
#: Measured on the production store: 2,646 rows, 2.2 ms.
|
|
175
|
+
_CODEX_THREAD_INVENTORY_SQL = """
|
|
176
|
+
SELECT conversation_key, source_root_key, native_thread_id, last_seen_utc,
|
|
177
|
+
CAST(cwd AS BLOB) AS cwd_blob,
|
|
178
|
+
CAST(git_json AS BLOB) AS git_json_blob
|
|
179
|
+
FROM {threads}
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
#: The alias leg (§4.3 item 2). Issued only when this cache generation carries
|
|
183
|
+
#: ``last_native_thread_id``. Measured: 2,986 rows, millisecond-scale.
|
|
184
|
+
_CODEX_FILE_ALIAS_INVENTORY_SQL = """
|
|
185
|
+
SELECT files.source_root_key, files.path, files.last_native_thread_id
|
|
148
186
|
FROM codex_session_files AS files
|
|
149
|
-
JOIN codex_conversation_threads AS inherited
|
|
150
|
-
ON inherited.source_root_key = files.source_root_key
|
|
151
|
-
AND inherited.native_thread_id = files.last_native_thread_id
|
|
152
187
|
WHERE files.last_native_thread_id IS NOT NULL
|
|
153
188
|
AND files.last_native_thread_id != ''
|
|
154
|
-
ORDER BY inherited.last_seen_utc DESC, inherited.conversation_key DESC
|
|
155
189
|
"""
|
|
156
190
|
|
|
157
191
|
|
|
158
|
-
def
|
|
159
|
-
"""Constrain
|
|
192
|
+
def _codex_alias_inventory_sql(identity_count: int) -> str:
|
|
193
|
+
"""Constrain the alias leg to dirty physical paths."""
|
|
160
194
|
if identity_count < 1:
|
|
161
195
|
raise ValueError("identity_count must be positive")
|
|
162
196
|
predicates = " OR ".join(
|
|
163
197
|
"(files.source_root_key = ? AND files.path = ?)"
|
|
164
198
|
for _ in range(identity_count)
|
|
165
199
|
)
|
|
166
|
-
return
|
|
200
|
+
return _CODEX_FILE_ALIAS_INVENTORY_SQL.replace(
|
|
167
201
|
" WHERE files.last_native_thread_id IS NOT NULL",
|
|
168
202
|
f" WHERE ({predicates})\n"
|
|
169
203
|
" AND files.last_native_thread_id IS NOT NULL",
|
|
170
204
|
)
|
|
171
205
|
|
|
172
206
|
|
|
207
|
+
def _alias_winner_sort_key(last_seen_utc: object, conversation_key: object) -> tuple:
|
|
208
|
+
"""Reproduce ``ORDER BY last_seen_utc DESC, conversation_key DESC``.
|
|
209
|
+
|
|
210
|
+
The retired statement ordered its joined rows that way and the qualifier
|
|
211
|
+
kept the FIRST row per ``(source_root_key, path)``, so the winner is the
|
|
212
|
+
maximum under this key. SQLite sorts NULL FIRST ascending, hence LAST
|
|
213
|
+
descending, so a NULL ``last_seen_utc`` loses to every stored value; the
|
|
214
|
+
leading flag is what reproduces that, and ``max`` is what reproduces
|
|
215
|
+
"first row wins".
|
|
216
|
+
"""
|
|
217
|
+
has_last_seen = 0 if last_seen_utc is None else 1
|
|
218
|
+
return (
|
|
219
|
+
has_last_seen,
|
|
220
|
+
"" if last_seen_utc is None else str(last_seen_utc),
|
|
221
|
+
str(conversation_key or ""),
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@dataclasses.dataclass(frozen=True)
|
|
226
|
+
class CodexThreadMetadataInventory:
|
|
227
|
+
"""One linear pass over the thread and alias tables (§4.3).
|
|
228
|
+
|
|
229
|
+
This replaces the SQL join in the retired
|
|
230
|
+
``_INHERITED_CODEX_PROJECT_METADATA_SQL``, whose plan on the current schema
|
|
231
|
+
is ``SCAN files`` then ``SEARCH inherited USING INDEX
|
|
232
|
+
idx_codex_threads_source_root``: on a one-root machine every file row
|
|
233
|
+
rescans all 2,646 threads and the statement costs 1,362 ms. No index is
|
|
234
|
+
added, because a new index needs a real cache migration, which M4 excludes.
|
|
235
|
+
|
|
236
|
+
``undecodable_direct`` and ``undecodable_inherited`` are what make the
|
|
237
|
+
counting of §4.3 zero-cost on a clean store: both are empty, so no
|
|
238
|
+
per-identity query runs at all.
|
|
239
|
+
"""
|
|
240
|
+
|
|
241
|
+
direct_by_conversation: "dict[tuple[str, str], DecodedCodexProjectMetadata]"
|
|
242
|
+
inherited_by_path: "dict[tuple[str, str], DecodedCodexProjectMetadata]"
|
|
243
|
+
inherited_winner_by_path: "dict[tuple[str, str], tuple[str, str]]"
|
|
244
|
+
undecodable_direct: "frozenset[tuple[str, str]]"
|
|
245
|
+
undecodable_inherited: "frozenset[tuple[str, str]]"
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def load_codex_thread_metadata_inventory(
|
|
249
|
+
conn, *, paths: "Iterable[tuple[str, str]] | None" = None,
|
|
250
|
+
) -> CodexThreadMetadataInventory:
|
|
251
|
+
"""Read the thread and alias tables once each and decode every row.
|
|
252
|
+
|
|
253
|
+
``paths`` bounds only the ALIAS leg, to the ``(source_root_key, path)``
|
|
254
|
+
identities a delta-resumed read actually asked for. The thread leg stays
|
|
255
|
+
whole-table because the alias winner for any path can be any thread sharing
|
|
256
|
+
its ``last_native_thread_id``.
|
|
257
|
+
"""
|
|
258
|
+
threads_table = resolve_codex_threads_table(conn)
|
|
259
|
+
direct_by_conversation: "dict[tuple[str, str], DecodedCodexProjectMetadata]" = {}
|
|
260
|
+
candidates_by_native: "dict[tuple[str, str], tuple]" = {}
|
|
261
|
+
undecodable_direct: "set[tuple[str, str]]" = set()
|
|
262
|
+
for row in conn.execute(
|
|
263
|
+
_CODEX_THREAD_INVENTORY_SQL.format(threads=threads_table)
|
|
264
|
+
):
|
|
265
|
+
(
|
|
266
|
+
conversation_key, root_key, native_thread_id, last_seen_utc,
|
|
267
|
+
cwd_blob, git_json_blob,
|
|
268
|
+
) = row
|
|
269
|
+
identity = (str(root_key or ""), str(conversation_key or ""))
|
|
270
|
+
decoded = decode_codex_project_metadata(cwd_blob, git_json_blob)
|
|
271
|
+
if all(identity):
|
|
272
|
+
direct_by_conversation[identity] = decoded
|
|
273
|
+
if decoded.undecodable_fields:
|
|
274
|
+
undecodable_direct.add(identity)
|
|
275
|
+
native_identity = (identity[0], str(native_thread_id or ""))
|
|
276
|
+
if not all(native_identity):
|
|
277
|
+
continue
|
|
278
|
+
sort_key = _alias_winner_sort_key(last_seen_utc, conversation_key)
|
|
279
|
+
current = candidates_by_native.get(native_identity)
|
|
280
|
+
if current is None or sort_key > current[0]:
|
|
281
|
+
candidates_by_native[native_identity] = (
|
|
282
|
+
sort_key, str(conversation_key or ""), decoded,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
inherited_by_path: "dict[tuple[str, str], DecodedCodexProjectMetadata]" = {}
|
|
286
|
+
inherited_winner_by_path: "dict[tuple[str, str], tuple[str, str]]" = {}
|
|
287
|
+
undecodable_inherited: "set[tuple[str, str]]" = set()
|
|
288
|
+
if _supports_native_file_aliases(conn):
|
|
289
|
+
identities = (
|
|
290
|
+
None if paths is None
|
|
291
|
+
else tuple(sorted({
|
|
292
|
+
(str(root), str(path)) for root, path in paths
|
|
293
|
+
if str(root) and str(path)
|
|
294
|
+
}))
|
|
295
|
+
)
|
|
296
|
+
if identities is not None and not identities:
|
|
297
|
+
alias_rows: tuple = ()
|
|
298
|
+
else:
|
|
299
|
+
alias_rows = tuple(conn.execute(
|
|
300
|
+
_CODEX_FILE_ALIAS_INVENTORY_SQL if identities is None
|
|
301
|
+
else _codex_alias_inventory_sql(len(identities)),
|
|
302
|
+
tuple(
|
|
303
|
+
value for identity in (identities or ()) for value in identity
|
|
304
|
+
),
|
|
305
|
+
))
|
|
306
|
+
for root_key, path, native_thread_id in alias_rows:
|
|
307
|
+
identity = (str(root_key or ""), str(path or ""))
|
|
308
|
+
if not all(identity):
|
|
309
|
+
continue
|
|
310
|
+
winner = candidates_by_native.get(
|
|
311
|
+
(identity[0], str(native_thread_id or "")))
|
|
312
|
+
if winner is None:
|
|
313
|
+
continue
|
|
314
|
+
_sort_key, winning_conversation, decoded = winner
|
|
315
|
+
inherited_by_path[identity] = decoded
|
|
316
|
+
inherited_winner_by_path[identity] = (
|
|
317
|
+
identity[0], winning_conversation)
|
|
318
|
+
if decoded.undecodable_fields:
|
|
319
|
+
undecodable_inherited.add(identity)
|
|
320
|
+
|
|
321
|
+
return CodexThreadMetadataInventory(
|
|
322
|
+
direct_by_conversation=direct_by_conversation,
|
|
323
|
+
inherited_by_path=inherited_by_path,
|
|
324
|
+
inherited_winner_by_path=inherited_winner_by_path,
|
|
325
|
+
undecodable_direct=frozenset(undecodable_direct),
|
|
326
|
+
undecodable_inherited=frozenset(undecodable_inherited),
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
|
|
173
330
|
_CODEX_ACCOUNTING_ENTRIES_SQL = """
|
|
174
331
|
SELECT timestamp_utc, source_root_key, conversation_key, model,
|
|
175
332
|
input_tokens, cached_input_tokens, output_tokens,
|
|
@@ -193,7 +350,7 @@ _CODEX_ACCOUNTING_ENTRIES_SQL = """
|
|
|
193
350
|
_ROOTED_CODEX_ACCOUNTING_ENTRIES_SQL = """
|
|
194
351
|
SELECT timestamp_utc, session_id, source_path, source_root_key, model,
|
|
195
352
|
input_tokens, cached_input_tokens, output_tokens,
|
|
196
|
-
reasoning_output_tokens, total_tokens, account_key
|
|
353
|
+
reasoning_output_tokens, total_tokens, account_key, conversation_key
|
|
197
354
|
FROM codex_session_entries INDEXED BY idx_codex_entries_ts_root_conversation
|
|
198
355
|
WHERE timestamp_utc >= ?
|
|
199
356
|
AND timestamp_utc < ?
|
|
@@ -221,7 +378,26 @@ _CODEX_PROJECT_METADATA_HEALTH_SQL = """
|
|
|
221
378
|
WHERE files.path = entries.source_path
|
|
222
379
|
AND files.source_root_key = entries.source_root_key
|
|
223
380
|
)
|
|
224
|
-
THEN 1 ELSE 0 END), 0) AS missing_thread_join_rows
|
|
381
|
+
THEN 1 ELSE 0 END), 0) AS missing_thread_join_rows,
|
|
382
|
+
COALESCE(SUM(CASE
|
|
383
|
+
WHEN entries.conversation_key IS NOT NULL
|
|
384
|
+
AND entries.conversation_key != ''
|
|
385
|
+
AND NOT (
|
|
386
|
+
threads.conversation_key IS NULL
|
|
387
|
+
AND NOT EXISTS (
|
|
388
|
+
SELECT 1 FROM codex_session_files AS files
|
|
389
|
+
JOIN codex_conversation_threads AS inherited
|
|
390
|
+
ON inherited.source_root_key = entries.source_root_key
|
|
391
|
+
AND inherited.native_thread_id = files.last_native_thread_id
|
|
392
|
+
WHERE files.path = entries.source_path
|
|
393
|
+
AND files.source_root_key = entries.source_root_key
|
|
394
|
+
)
|
|
395
|
+
)
|
|
396
|
+
AND codex_accounting_row_usable(
|
|
397
|
+
entries.timestamp_utc, entries.input_tokens,
|
|
398
|
+
entries.cached_input_tokens, entries.output_tokens,
|
|
399
|
+
entries.reasoning_output_tokens, entries.total_tokens) = 0
|
|
400
|
+
THEN 1 ELSE 0 END), 0) AS malformed_accounting_rows
|
|
225
401
|
FROM codex_session_entries AS entries
|
|
226
402
|
LEFT JOIN codex_conversation_threads AS threads
|
|
227
403
|
ON threads.conversation_key = entries.conversation_key
|
|
@@ -232,6 +408,37 @@ _CODEX_PROJECT_METADATA_HEALTH_SQL = """
|
|
|
232
408
|
|
|
233
409
|
|
|
234
410
|
_CODEX_PROJECT_METADATA_HEALTH_LEGACY_SQL = """
|
|
411
|
+
SELECT
|
|
412
|
+
COUNT(*) AS total_rows,
|
|
413
|
+
COALESCE(SUM(CASE
|
|
414
|
+
WHEN entries.conversation_key IS NULL OR entries.conversation_key = ''
|
|
415
|
+
THEN 1 ELSE 0 END), 0) AS missing_conversation_key_rows,
|
|
416
|
+
COALESCE(SUM(CASE
|
|
417
|
+
WHEN entries.conversation_key IS NOT NULL
|
|
418
|
+
AND entries.conversation_key != ''
|
|
419
|
+
AND threads.conversation_key IS NULL
|
|
420
|
+
THEN 1 ELSE 0 END), 0) AS missing_thread_join_rows,
|
|
421
|
+
COALESCE(SUM(CASE
|
|
422
|
+
WHEN entries.conversation_key IS NOT NULL
|
|
423
|
+
AND entries.conversation_key != ''
|
|
424
|
+
AND threads.conversation_key IS NOT NULL
|
|
425
|
+
AND codex_accounting_row_usable(
|
|
426
|
+
entries.timestamp_utc, entries.input_tokens,
|
|
427
|
+
entries.cached_input_tokens, entries.output_tokens,
|
|
428
|
+
entries.reasoning_output_tokens, entries.total_tokens) = 0
|
|
429
|
+
THEN 1 ELSE 0 END), 0) AS malformed_accounting_rows
|
|
430
|
+
FROM codex_session_entries AS entries
|
|
431
|
+
LEFT JOIN codex_conversation_threads AS threads
|
|
432
|
+
ON threads.conversation_key = entries.conversation_key
|
|
433
|
+
AND threads.source_root_key = entries.source_root_key
|
|
434
|
+
WHERE (? IS NULL OR entries.timestamp_utc >= ?)
|
|
435
|
+
AND (? IS NULL OR entries.timestamp_utc < ?)
|
|
436
|
+
"""
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
# A pre-native-alias cache may also predate the accounting operand columns.
|
|
440
|
+
# Doctor must still report its metadata partition instead of failing the read.
|
|
441
|
+
_CODEX_PROJECT_METADATA_HEALTH_PRE_OPERAND_SQL = """
|
|
235
442
|
SELECT
|
|
236
443
|
COUNT(*) AS total_rows,
|
|
237
444
|
COALESCE(SUM(CASE
|
|
@@ -253,63 +460,52 @@ _CODEX_PROJECT_METADATA_HEALTH_LEGACY_SQL = """
|
|
|
253
460
|
|
|
254
461
|
# === #834 S2 (#828) — the bounded detail-horizon metadata probe ============
|
|
255
462
|
#
|
|
256
|
-
# `_CODEX_PROJECT_METADATA_HEALTH_SQL` above counts
|
|
257
|
-
#
|
|
258
|
-
# `_parse_timestamp` and the integer token operands inside
|
|
259
|
-
# `_calculate_codex_entry_cost`, raising `QualifiedMetadataUnavailable` on
|
|
260
|
-
# either. A row with intact joins and a malformed timestamp was therefore
|
|
261
|
-
# counted healthy by that SQL and refused by the reader.
|
|
463
|
+
# `_CODEX_PROJECT_METADATA_HEALTH_SQL` above counts key, join and accounting
|
|
464
|
+
# operand failures. The detail probe answers the larger read horizon.
|
|
262
465
|
#
|
|
263
466
|
# The horizon is the other half. The generation's accounting window is roughly
|
|
264
467
|
# thirty calendar days while both detail routes read a YEAR, so a malformed row
|
|
265
468
|
# aged thirty-one to three hundred and sixty-five days set no flag at all and
|
|
266
469
|
# the page rendered in full with that row's cost silently absent.
|
|
267
470
|
#
|
|
268
|
-
#
|
|
471
|
+
# ONE LIMIT, stated rather than hidden. This probe and the qualified
|
|
269
472
|
# reader share the same TEXT range bounds, so a timestamp malformed enough to
|
|
270
473
|
# sort outside the window is invisible to BOTH — such a row also contributes to
|
|
271
474
|
# no figure and reaches no route, so the two agree about it by construction.
|
|
272
|
-
#
|
|
273
|
-
#
|
|
274
|
-
#
|
|
275
|
-
# shape, and both errors are in the direction of counting fewer rows, never of
|
|
276
|
-
# degrading a page that would have rendered.
|
|
475
|
+
# The operand predicate below calls the reader's own timestamp and integer
|
|
476
|
+
# parsers, so SQLite's looser datetime and signed-digit grammars cannot turn a
|
|
477
|
+
# refused row into a healthy probe result.
|
|
277
478
|
|
|
278
479
|
#: The five integer operands the qualified reader parses. `total_tokens` is
|
|
279
|
-
#: included
|
|
280
|
-
#: malformed value there escapes as a bare `ValueError` rather than as
|
|
281
|
-
#: `QualifiedMetadataUnavailable`. It is still a row the reader cannot turn
|
|
282
|
-
#: into an entry, which is what this probe counts.
|
|
480
|
+
#: included in the same translated conversion as the other four operands.
|
|
283
481
|
_CODEX_TOKEN_OPERANDS = (
|
|
284
482
|
"input_tokens", "cached_input_tokens", "output_tokens",
|
|
285
483
|
"reasoning_output_tokens", "total_tokens",
|
|
286
484
|
)
|
|
287
485
|
|
|
288
486
|
|
|
289
|
-
def
|
|
290
|
-
"""
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
487
|
+
def _codex_accounting_row_usable(*values: object) -> int:
|
|
488
|
+
"""Use the reader's exact parsers for the six stored accounting values."""
|
|
489
|
+
try:
|
|
490
|
+
_parse_timestamp(values[0])
|
|
491
|
+
for value in values[1:]:
|
|
492
|
+
int(value)
|
|
493
|
+
except (QualifiedMetadataUnavailable, TypeError, ValueError, OverflowError):
|
|
494
|
+
return 0
|
|
495
|
+
return 1
|
|
298
496
|
|
|
299
497
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
)
|
|
312
|
-
)"""
|
|
498
|
+
_CODEX_ACCOUNTING_ROW_USABLE_SQL = (
|
|
499
|
+
"codex_accounting_row_usable(entries.timestamp_utc, "
|
|
500
|
+
+ ", ".join(f"entries.{column}" for column in _CODEX_TOKEN_OPERANDS)
|
|
501
|
+
+ ")"
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def _register_codex_accounting_probe(cache_conn: sqlite3.Connection) -> None:
|
|
506
|
+
cache_conn.create_function(
|
|
507
|
+
"codex_accounting_row_usable", 6, _codex_accounting_row_usable,
|
|
508
|
+
)
|
|
313
509
|
|
|
314
510
|
_CODEX_DETAIL_PROBE_JOIN_SQL = """
|
|
315
511
|
entries.conversation_key IS NOT NULL
|
|
@@ -332,13 +528,11 @@ _CODEX_DETAIL_PROBE_JOIN_LEGACY_SQL = """
|
|
|
332
528
|
|
|
333
529
|
def _codex_detail_metadata_probe_sql(cache_conn: sqlite3.Connection) -> str:
|
|
334
530
|
"""Compose the probe over whichever thread-alias shape this cache carries."""
|
|
531
|
+
_register_codex_accounting_probe(cache_conn)
|
|
335
532
|
join_predicate = (
|
|
336
533
|
_CODEX_DETAIL_PROBE_JOIN_SQL if _supports_native_file_aliases(cache_conn)
|
|
337
534
|
else _CODEX_DETAIL_PROBE_JOIN_LEGACY_SQL
|
|
338
535
|
)
|
|
339
|
-
token_predicate = "\n OR ".join(
|
|
340
|
-
_unusable_integer_predicate(column) for column in _CODEX_TOKEN_OPERANDS
|
|
341
|
-
)
|
|
342
536
|
# Direct non-null bounds, never the nullable `(? IS NULL OR ...)` form the
|
|
343
537
|
# unbounded doctor helper needs: that form is opaque to the query planner,
|
|
344
538
|
# so it cannot use the timestamp-leading index and reads every row.
|
|
@@ -349,8 +543,7 @@ def _codex_detail_metadata_probe_sql(cache_conn: sqlite3.Connection) -> str:
|
|
|
349
543
|
entries.conversation_key IS NULL OR entries.conversation_key = ''
|
|
350
544
|
OR ({join_predicate}
|
|
351
545
|
)
|
|
352
|
-
OR
|
|
353
|
-
OR {token_predicate}
|
|
546
|
+
OR {_CODEX_ACCOUNTING_ROW_USABLE_SQL} = 0
|
|
354
547
|
THEN 1 ELSE 0 END), 0) AS unqualifiable_rows
|
|
355
548
|
FROM codex_session_entries AS entries
|
|
356
549
|
INDEXED BY idx_codex_entries_ts_root_conversation
|
|
@@ -362,6 +555,186 @@ def _codex_detail_metadata_probe_sql(cache_conn: sqlite3.Connection) -> str:
|
|
|
362
555
|
"""
|
|
363
556
|
|
|
364
557
|
|
|
558
|
+
#: How many affected ids one overlap statement carries. The subtraction runs
|
|
559
|
+
#: over the ids the per-identity queries already returned, so the chunk bounds
|
|
560
|
+
#: the SQL text rather than the work.
|
|
561
|
+
_OVERLAP_ID_CHUNK = 400
|
|
562
|
+
|
|
563
|
+
#: One query per undecodable identity, never a disjunction over all of them: a
|
|
564
|
+
#: multi-identity `OR` predicate makes SQLite abandon the index and scan the
|
|
565
|
+
#: table, which is why `load_codex_quota_observations` shards too.
|
|
566
|
+
_UNDECODABLE_DIRECT_ENTRIES_SQL = """
|
|
567
|
+
SELECT entries.id, entries.source_path, entries.conversation_key
|
|
568
|
+
FROM codex_session_entries AS entries
|
|
569
|
+
INDEXED BY idx_codex_entries_conversation
|
|
570
|
+
WHERE entries.source_root_key = ?
|
|
571
|
+
AND entries.conversation_key = ?
|
|
572
|
+
"""
|
|
573
|
+
|
|
574
|
+
_UNDECODABLE_INHERITED_ENTRIES_SQL = """
|
|
575
|
+
SELECT entries.id, entries.source_path, entries.conversation_key
|
|
576
|
+
FROM codex_session_entries AS entries
|
|
577
|
+
INDEXED BY idx_codex_entries_root_path
|
|
578
|
+
WHERE entries.source_root_key = ?
|
|
579
|
+
AND entries.source_path = ?
|
|
580
|
+
"""
|
|
581
|
+
|
|
582
|
+
_BOUNDED_ENTRIES_PREDICATE = """
|
|
583
|
+
AND entries.timestamp_utc >= ?
|
|
584
|
+
AND entries.timestamp_utc < ?
|
|
585
|
+
"""
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def _undecodable_entries_sql(base: str, *, bounded: bool) -> str:
|
|
589
|
+
return base + (_BOUNDED_ENTRIES_PREDICATE if bounded else "")
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def _codex_population_reason_sql(
|
|
593
|
+
cache_conn: sqlite3.Connection, *, include_probe_reasons: bool,
|
|
594
|
+
) -> str:
|
|
595
|
+
"""This population's OWN reason predicate, reproduced not re-implemented.
|
|
596
|
+
|
|
597
|
+
The aggregates count a missing conversation key, a missing thread join,
|
|
598
|
+
and a refused accounting operand. A row with undecodable metadata and a
|
|
599
|
+
bad timestamp is already counted by the operand branch, so it is
|
|
600
|
+
subtracted from the undecodable count for each population.
|
|
601
|
+
|
|
602
|
+
The reason semantics — the shared accounting parser, the alias
|
|
603
|
+
``NOT EXISTS`` — are never re-implemented in Python: the predicate text
|
|
604
|
+
here is the same text the aggregates evaluate.
|
|
605
|
+
"""
|
|
606
|
+
join_predicate = (
|
|
607
|
+
_CODEX_DETAIL_PROBE_JOIN_SQL if _supports_native_file_aliases(cache_conn)
|
|
608
|
+
else _CODEX_DETAIL_PROBE_JOIN_LEGACY_SQL
|
|
609
|
+
)
|
|
610
|
+
reasons = [
|
|
611
|
+
"(entries.conversation_key IS NULL OR entries.conversation_key = '')",
|
|
612
|
+
f"({join_predicate})",
|
|
613
|
+
]
|
|
614
|
+
if include_probe_reasons:
|
|
615
|
+
_register_codex_accounting_probe(cache_conn)
|
|
616
|
+
reasons.append(f"{_CODEX_ACCOUNTING_ROW_USABLE_SQL} = 0")
|
|
617
|
+
return "\n OR ".join(reasons)
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def _codex_overlap_sql(
|
|
621
|
+
cache_conn: sqlite3.Connection, *, include_probe_reasons: bool,
|
|
622
|
+
bounded: bool, chunk_size: int,
|
|
623
|
+
) -> str:
|
|
624
|
+
"""The population's complete caller shape, over one chunk of ids.
|
|
625
|
+
|
|
626
|
+
The callers' reason predicates are not standalone expressions: they
|
|
627
|
+
reference the ``threads`` join alias and the inherited-alias ``NOT
|
|
628
|
+
EXISTS``, so this statement reproduces the aggregate's own ``LEFT JOIN``
|
|
629
|
+
and its own time bounds rather than evaluating the predicate in isolation.
|
|
630
|
+
"""
|
|
631
|
+
placeholders = ",".join("?" for _ in range(chunk_size))
|
|
632
|
+
bounds = _BOUNDED_ENTRIES_PREDICATE if bounded else ""
|
|
633
|
+
return f"""
|
|
634
|
+
SELECT entries.id
|
|
635
|
+
FROM codex_session_entries AS entries
|
|
636
|
+
LEFT JOIN codex_conversation_threads AS threads
|
|
637
|
+
ON threads.conversation_key = entries.conversation_key
|
|
638
|
+
AND threads.source_root_key = entries.source_root_key
|
|
639
|
+
WHERE entries.id IN ({placeholders}){bounds}
|
|
640
|
+
AND ({_codex_population_reason_sql(
|
|
641
|
+
cache_conn, include_probe_reasons=include_probe_reasons)})
|
|
642
|
+
"""
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def count_undecodable_codex_metadata_rows(
|
|
646
|
+
cache_conn: sqlite3.Connection,
|
|
647
|
+
*,
|
|
648
|
+
bound_start: "str | None",
|
|
649
|
+
bound_end: "str | None",
|
|
650
|
+
include_probe_reasons: bool,
|
|
651
|
+
inventory: "CodexThreadMetadataInventory | None" = None,
|
|
652
|
+
) -> int:
|
|
653
|
+
"""Entries this population must count for undecodable project metadata.
|
|
654
|
+
|
|
655
|
+
Zero-cost on a clean store: both undecodable sets are empty, so no
|
|
656
|
+
per-identity query runs at all and this returns before touching the
|
|
657
|
+
entries table.
|
|
658
|
+
|
|
659
|
+
The affected ids are unioned across the direct and inherited identities,
|
|
660
|
+
and a second bounded query per population returns the subset that
|
|
661
|
+
population's aggregate already counted for another reason; the decode count
|
|
662
|
+
is the union minus that subset, so no entry is counted twice.
|
|
663
|
+
"""
|
|
664
|
+
if inventory is None:
|
|
665
|
+
inventory = load_codex_thread_metadata_inventory(cache_conn)
|
|
666
|
+
if not inventory.undecodable_direct and not inventory.undecodable_inherited:
|
|
667
|
+
return 0
|
|
668
|
+
bounded = bound_start is not None and bound_end is not None
|
|
669
|
+
bounds: tuple = (bound_start, bound_end) if bounded else ()
|
|
670
|
+
|
|
671
|
+
affected: set[int] = set()
|
|
672
|
+
seeks = (
|
|
673
|
+
(_UNDECODABLE_DIRECT_ENTRIES_SQL, inventory.undecodable_direct),
|
|
674
|
+
(_UNDECODABLE_INHERITED_ENTRIES_SQL, inventory.undecodable_inherited),
|
|
675
|
+
)
|
|
676
|
+
for base_sql, identities in seeks:
|
|
677
|
+
if not identities:
|
|
678
|
+
continue
|
|
679
|
+
sql = _undecodable_entries_sql(base_sql, bounded=bounded)
|
|
680
|
+
for root_key, second in sorted(identities):
|
|
681
|
+
for entry_id, source_path, conversation_key in cache_conn.execute(
|
|
682
|
+
sql, (root_key, second, *bounds),
|
|
683
|
+
):
|
|
684
|
+
direct = inventory.direct_by_conversation.get(
|
|
685
|
+
(str(root_key), str(conversation_key or "")))
|
|
686
|
+
inherited = inventory.inherited_by_path.get(
|
|
687
|
+
(str(root_key), str(source_path or "")))
|
|
688
|
+
if codex_metadata_is_malformed(direct, inherited):
|
|
689
|
+
affected.add(int(entry_id))
|
|
690
|
+
if not affected:
|
|
691
|
+
return 0
|
|
692
|
+
|
|
693
|
+
already_counted: set[int] = set()
|
|
694
|
+
ordered = sorted(affected)
|
|
695
|
+
for offset in range(0, len(ordered), _OVERLAP_ID_CHUNK):
|
|
696
|
+
chunk = ordered[offset:offset + _OVERLAP_ID_CHUNK]
|
|
697
|
+
sql = _codex_overlap_sql(
|
|
698
|
+
cache_conn, include_probe_reasons=include_probe_reasons,
|
|
699
|
+
bounded=bounded, chunk_size=len(chunk),
|
|
700
|
+
)
|
|
701
|
+
already_counted.update(
|
|
702
|
+
int(row[0]) for row in cache_conn.execute(sql, (*chunk, *bounds))
|
|
703
|
+
)
|
|
704
|
+
return len(affected - already_counted)
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def explain_undecodable_codex_overlap(
|
|
708
|
+
cache_conn: sqlite3.Connection,
|
|
709
|
+
*,
|
|
710
|
+
include_probe_reasons: bool,
|
|
711
|
+
bounded: bool,
|
|
712
|
+
chunk_size: int = 1,
|
|
713
|
+
) -> "tuple[str, ...]":
|
|
714
|
+
"""The overlap statement's query plan, so a test can prove it seeks."""
|
|
715
|
+
sql = _codex_overlap_sql(
|
|
716
|
+
cache_conn, include_probe_reasons=include_probe_reasons,
|
|
717
|
+
bounded=bounded, chunk_size=chunk_size,
|
|
718
|
+
)
|
|
719
|
+
params = tuple(range(chunk_size)) + ((("", "")) if bounded else ())
|
|
720
|
+
rows = cache_conn.execute("EXPLAIN QUERY PLAN " + sql, params).fetchall()
|
|
721
|
+
return tuple(str(row[-1]) for row in rows)
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
def explain_undecodable_codex_identity_seek(
|
|
725
|
+
cache_conn: sqlite3.Connection, *, inherited: bool, bounded: bool,
|
|
726
|
+
) -> "tuple[str, ...]":
|
|
727
|
+
"""One per-identity counting query's plan, so a test can prove it seeks."""
|
|
728
|
+
sql = _undecodable_entries_sql(
|
|
729
|
+
_UNDECODABLE_INHERITED_ENTRIES_SQL if inherited
|
|
730
|
+
else _UNDECODABLE_DIRECT_ENTRIES_SQL,
|
|
731
|
+
bounded=bounded,
|
|
732
|
+
)
|
|
733
|
+
params = ("", "") + (("", "") if bounded else ())
|
|
734
|
+
rows = cache_conn.execute("EXPLAIN QUERY PLAN " + sql, params).fetchall()
|
|
735
|
+
return tuple(str(row[-1]) for row in rows)
|
|
736
|
+
|
|
737
|
+
|
|
365
738
|
def _probe_bounds(start: dt.datetime, end: dt.datetime) -> tuple[str, str]:
|
|
366
739
|
bound_start = _metadata_health_bound(start, name="start")
|
|
367
740
|
bound_end = _metadata_health_bound(end, name="end")
|
|
@@ -377,6 +750,7 @@ def probe_codex_detail_metadata_health(
|
|
|
377
750
|
cache_conn: sqlite3.Connection,
|
|
378
751
|
start: dt.datetime,
|
|
379
752
|
end: dt.datetime,
|
|
753
|
+
inventory: "CodexThreadMetadataInventory | None" = None,
|
|
380
754
|
) -> CodexProjectMetadataHealth:
|
|
381
755
|
"""Count rows in ``[start, end)`` the qualified reader would refuse.
|
|
382
756
|
|
|
@@ -393,6 +767,7 @@ def probe_codex_detail_metadata_health(
|
|
|
393
767
|
caller reads.
|
|
394
768
|
"""
|
|
395
769
|
bound_start, bound_end = _probe_bounds(start, end)
|
|
770
|
+
_register_codex_accounting_probe(cache_conn)
|
|
396
771
|
row = cache_conn.execute(
|
|
397
772
|
_codex_detail_metadata_probe_sql(cache_conn), (bound_start, bound_end),
|
|
398
773
|
).fetchone()
|
|
@@ -404,11 +779,21 @@ def probe_codex_detail_metadata_health(
|
|
|
404
779
|
raise RuntimeError("Codex detail metadata probe returned invalid counts") from exc
|
|
405
780
|
if total_rows < 0 or unqualifiable_rows < 0 or unqualifiable_rows > total_rows:
|
|
406
781
|
raise RuntimeError("Codex detail metadata probe partition is invalid")
|
|
782
|
+
undecodable_rows = count_undecodable_codex_metadata_rows(
|
|
783
|
+
cache_conn,
|
|
784
|
+
bound_start=bound_start,
|
|
785
|
+
bound_end=bound_end,
|
|
786
|
+
include_probe_reasons=True,
|
|
787
|
+
inventory=inventory,
|
|
788
|
+
)
|
|
789
|
+
if unqualifiable_rows + undecodable_rows > total_rows:
|
|
790
|
+
raise RuntimeError("Codex detail metadata probe partition is invalid")
|
|
407
791
|
return CodexProjectMetadataHealth(
|
|
408
792
|
total_rows=total_rows,
|
|
409
|
-
qualified_rows=total_rows - unqualifiable_rows,
|
|
793
|
+
qualified_rows=total_rows - unqualifiable_rows - undecodable_rows,
|
|
410
794
|
missing_conversation_key_rows=unqualifiable_rows,
|
|
411
795
|
missing_thread_join_rows=0,
|
|
796
|
+
undecodable_metadata_rows=undecodable_rows,
|
|
412
797
|
)
|
|
413
798
|
|
|
414
799
|
|
|
@@ -428,11 +813,18 @@ def explain_codex_detail_metadata_probe(
|
|
|
428
813
|
|
|
429
814
|
|
|
430
815
|
def _supports_native_file_aliases(cache_conn: sqlite3.Connection) -> bool:
|
|
431
|
-
"""Return whether this cache generation can link child files to a root task.
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
816
|
+
"""Return whether this cache generation can link child files to a root task.
|
|
817
|
+
|
|
818
|
+
#846 §4.6, the second taxonomy hole. This used to answer ``False`` both
|
|
819
|
+
when the PRAGMA succeeded and the column was absent, and when the PRAGMA
|
|
820
|
+
itself failed. The two are different facts: the first is an old schema and
|
|
821
|
+
the second is a failed read, and answering "old schema" for a failed read
|
|
822
|
+
silently dropped the inherited alias population from every caller. A
|
|
823
|
+
``sqlite3.Error`` now propagates, and the combined verdict of §4.6 turns it
|
|
824
|
+
into a transient generation rather than a quietly narrower one.
|
|
825
|
+
"""
|
|
826
|
+
columns = cache_conn.execute(
|
|
827
|
+
"PRAGMA table_info(codex_session_files)").fetchall()
|
|
436
828
|
return any(len(row) > 1 and row[1] == "last_native_thread_id" for row in columns)
|
|
437
829
|
|
|
438
830
|
|
|
@@ -471,6 +863,7 @@ def load_codex_project_metadata_health(
|
|
|
471
863
|
cache_conn: sqlite3.Connection,
|
|
472
864
|
start: dt.datetime | None = None,
|
|
473
865
|
end: dt.datetime | None = None,
|
|
866
|
+
inventory: "CodexThreadMetadataInventory | None" = None,
|
|
474
867
|
) -> CodexProjectMetadataHealth:
|
|
475
868
|
"""Classify retained Codex accounting in one root-qualified SQL read.
|
|
476
869
|
|
|
@@ -485,19 +878,48 @@ def load_codex_project_metadata_health(
|
|
|
485
878
|
if start is not None and end is not None and start > end:
|
|
486
879
|
raise ValueError("start must not be after end")
|
|
487
880
|
|
|
881
|
+
entry_columns = {
|
|
882
|
+
str(row[1]) for row in cache_conn.execute(
|
|
883
|
+
"PRAGMA table_info(codex_session_entries)")
|
|
884
|
+
}
|
|
885
|
+
has_operands = set(_CODEX_TOKEN_OPERANDS).issubset(entry_columns)
|
|
886
|
+
if has_operands:
|
|
887
|
+
_register_codex_accounting_probe(cache_conn)
|
|
888
|
+
native_aliases = _supports_native_file_aliases(cache_conn)
|
|
889
|
+
if has_operands:
|
|
890
|
+
sql = (_CODEX_PROJECT_METADATA_HEALTH_SQL if native_aliases
|
|
891
|
+
else _CODEX_PROJECT_METADATA_HEALTH_LEGACY_SQL)
|
|
892
|
+
else:
|
|
893
|
+
sql = _CODEX_PROJECT_METADATA_HEALTH_PRE_OPERAND_SQL
|
|
488
894
|
row = cache_conn.execute(
|
|
489
|
-
|
|
490
|
-
else _CODEX_PROJECT_METADATA_HEALTH_LEGACY_SQL),
|
|
895
|
+
sql,
|
|
491
896
|
(bound_start, bound_start, bound_end, bound_end),
|
|
492
897
|
).fetchone()
|
|
493
|
-
if row is None or len(row) != 3:
|
|
898
|
+
if row is None or len(row) != (4 if has_operands else 3):
|
|
494
899
|
raise RuntimeError("Codex project metadata health query returned no partition")
|
|
495
900
|
try:
|
|
496
|
-
|
|
901
|
+
counts = tuple(int(value) for value in row)
|
|
902
|
+
total_rows, missing_key_rows, missing_join_rows = counts[:3]
|
|
903
|
+
malformed_rows = counts[3] if has_operands else 0
|
|
497
904
|
except (TypeError, ValueError) as exc:
|
|
498
905
|
raise RuntimeError("Codex project metadata health query returned invalid counts") from exc
|
|
499
|
-
|
|
500
|
-
|
|
906
|
+
# #845 §4.3. The same partition over the same population, with a third
|
|
907
|
+
# reason. Doctor's all-history call omits both timestamp predicates, which
|
|
908
|
+
# is exactly the nullable-bound shape this function already answers for.
|
|
909
|
+
undecodable_rows = count_undecodable_codex_metadata_rows(
|
|
910
|
+
cache_conn,
|
|
911
|
+
bound_start=bound_start,
|
|
912
|
+
bound_end=bound_end,
|
|
913
|
+
include_probe_reasons=has_operands,
|
|
914
|
+
inventory=inventory,
|
|
915
|
+
)
|
|
916
|
+
qualified_rows = (
|
|
917
|
+
total_rows - missing_key_rows - missing_join_rows
|
|
918
|
+
- malformed_rows - undecodable_rows)
|
|
919
|
+
values = (
|
|
920
|
+
total_rows, qualified_rows, missing_key_rows, missing_join_rows,
|
|
921
|
+
undecodable_rows, malformed_rows,
|
|
922
|
+
)
|
|
501
923
|
if any(value < 0 for value in values) or total_rows != sum(values[1:]):
|
|
502
924
|
raise RuntimeError("Codex project metadata health partition is invalid")
|
|
503
925
|
return CodexProjectMetadataHealth(
|
|
@@ -505,6 +927,8 @@ def load_codex_project_metadata_health(
|
|
|
505
927
|
qualified_rows=qualified_rows,
|
|
506
928
|
missing_conversation_key_rows=missing_key_rows,
|
|
507
929
|
missing_thread_join_rows=missing_join_rows,
|
|
930
|
+
undecodable_metadata_rows=undecodable_rows,
|
|
931
|
+
malformed_accounting_rows=malformed_rows,
|
|
508
932
|
)
|
|
509
933
|
|
|
510
934
|
|
|
@@ -569,11 +993,17 @@ def load_cached_rooted_codex_accounting_entries(
|
|
|
569
993
|
|
|
570
994
|
result: list[RootedCodexAccountingEntry] = []
|
|
571
995
|
for row in rows:
|
|
996
|
+
# A corrupt operand cannot contribute cost or tokens. The generation's
|
|
997
|
+
# bounded probes count this physical row once; keep every other row.
|
|
998
|
+
if not _codex_accounting_row_usable(
|
|
999
|
+
row[0], row[5], row[6], row[7], row[8], row[9],
|
|
1000
|
+
):
|
|
1001
|
+
continue
|
|
572
1002
|
try:
|
|
573
1003
|
(
|
|
574
1004
|
timestamp_raw, session_id_raw, source_path_raw, source_root_key_raw,
|
|
575
1005
|
model_raw, input_raw, cached_input_raw, output_raw, reasoning_raw,
|
|
576
|
-
total_raw, account_key_raw,
|
|
1006
|
+
total_raw, account_key_raw, conversation_key_raw,
|
|
577
1007
|
) = row
|
|
578
1008
|
source_path = source_path_raw if isinstance(source_path_raw, str) else ""
|
|
579
1009
|
source_root_key = source_root_key_raw if isinstance(source_root_key_raw, str) else ""
|
|
@@ -612,6 +1042,10 @@ def load_cached_rooted_codex_accounting_entries(
|
|
|
612
1042
|
total_tokens=total_tokens,
|
|
613
1043
|
cost_usd=cost_usd,
|
|
614
1044
|
account_key=entry_account_key,
|
|
1045
|
+
conversation_key=(
|
|
1046
|
+
conversation_key_raw
|
|
1047
|
+
if isinstance(conversation_key_raw, str) else ""
|
|
1048
|
+
),
|
|
615
1049
|
))
|
|
616
1050
|
return tuple(result)
|
|
617
1051
|
|
|
@@ -747,6 +1181,7 @@ def load_qualified_codex_entries(
|
|
|
747
1181
|
group: str = "git-root",
|
|
748
1182
|
cache_conn: sqlite3.Connection | None = None,
|
|
749
1183
|
source_identities: Iterable[tuple[str, str]] | None = None,
|
|
1184
|
+
inventory: "CodexThreadMetadataInventory | None" = None,
|
|
750
1185
|
) -> tuple[QualifiedCodexEntry, ...]:
|
|
751
1186
|
"""Load exactly one bounded, root-qualified Codex accounting read.
|
|
752
1187
|
|
|
@@ -803,19 +1238,15 @@ def load_qualified_codex_entries(
|
|
|
803
1238
|
*(value for identity in (identities or ()) for value in identity),
|
|
804
1239
|
)
|
|
805
1240
|
rows = tuple(conn.execute(sql, params))
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
for inherited in conn.execute(inherited_sql, inherited_params):
|
|
816
|
-
identity = (str(inherited["source_root_key"] or ""), str(inherited["path"] or ""))
|
|
817
|
-
if all(identity):
|
|
818
|
-
inherited_metadata.setdefault(identity, inherited)
|
|
1241
|
+
# §4.3: one inventory read per build. The caller supplies the build's
|
|
1242
|
+
# own inventory when it has one; its alias leg is unbounded, which is a
|
|
1243
|
+
# SUPERSET of the `paths=identities` leg this read would have loaded,
|
|
1244
|
+
# and every lookup below is by identity, so the wider map answers each
|
|
1245
|
+
# row identically.
|
|
1246
|
+
inherited_metadata = (
|
|
1247
|
+
load_codex_thread_metadata_inventory(conn, paths=identities)
|
|
1248
|
+
if inventory is None else inventory
|
|
1249
|
+
).inherited_by_path
|
|
819
1250
|
except sqlite3.Error as exc:
|
|
820
1251
|
raise QualifiedMetadataUnavailable("Codex qualified project metadata is unavailable") from exc
|
|
821
1252
|
finally:
|
|
@@ -855,15 +1286,34 @@ def _qualify_codex_rows(
|
|
|
855
1286
|
or not isinstance(conversation_key, str) or not conversation_key
|
|
856
1287
|
):
|
|
857
1288
|
raise
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
1289
|
+
# #845 §4.1/§4.2. The two thread columns arrive as BLOBs and are
|
|
1290
|
+
# decoded field by field, so one undecodable byte no longer fails the
|
|
1291
|
+
# statement that selected it. An entry whose resolver path REACHES an
|
|
1292
|
+
# undecodable field cannot be attributed, and this reader has no
|
|
1293
|
+
# partial-result carrier, so it refuses the whole read — the same
|
|
1294
|
+
# deterministic refusal `_parse_timestamp` and the cost operands
|
|
1295
|
+
# already use. It does not skip the row.
|
|
1296
|
+
direct = decode_codex_project_metadata(
|
|
1297
|
+
row["cwd_blob"], row["git_json_blob"])
|
|
1298
|
+
if codex_metadata_is_malformed(direct, inherited):
|
|
1299
|
+
raise QualifiedMetadataUnavailable(
|
|
1300
|
+
"Codex qualified project metadata is unavailable",
|
|
1301
|
+
transient=False)
|
|
1302
|
+
cwd = direct.cwd or (inherited.cwd if inherited is not None else None)
|
|
1303
|
+
git_json = direct.git_json or (
|
|
1304
|
+
inherited.git_json if inherited is not None else None)
|
|
861
1305
|
resolved_key, project_label = projects.resolve(cwd, git_json)
|
|
862
1306
|
try:
|
|
1307
|
+
timestamp = _parse_timestamp(row["timestamp_utc"])
|
|
1308
|
+
input_tokens = int(row["input_tokens"])
|
|
1309
|
+
cached_input_tokens = int(row["cached_input_tokens"])
|
|
1310
|
+
output_tokens = int(row["output_tokens"])
|
|
1311
|
+
reasoning_output_tokens = int(row["reasoning_output_tokens"])
|
|
1312
|
+
total_tokens = int(row["total_tokens"])
|
|
863
1313
|
cost_usd = c._calculate_codex_entry_cost(
|
|
864
|
-
str(row["model"]),
|
|
865
|
-
|
|
866
|
-
|
|
1314
|
+
str(row["model"]), input_tokens,
|
|
1315
|
+
cached_input_tokens, output_tokens,
|
|
1316
|
+
reasoning_output_tokens, speed=speed,
|
|
867
1317
|
)
|
|
868
1318
|
except (TypeError, ValueError, OverflowError) as exc:
|
|
869
1319
|
# #834 S2 (#829): deterministic. A stored token operand `int()`
|
|
@@ -871,7 +1321,7 @@ def _qualify_codex_rows(
|
|
|
871
1321
|
raise QualifiedMetadataUnavailable(
|
|
872
1322
|
"Codex qualified accounting is unavailable", transient=False) from exc
|
|
873
1323
|
result.append(QualifiedCodexEntry(
|
|
874
|
-
timestamp=
|
|
1324
|
+
timestamp=timestamp,
|
|
875
1325
|
session_id=str(row["session_id"] or ""),
|
|
876
1326
|
source_path=str(row["source_path"] or ""),
|
|
877
1327
|
source_root_key=root_key,
|
|
@@ -879,11 +1329,11 @@ def _qualify_codex_rows(
|
|
|
879
1329
|
project_key=opaque_project_key("codex", root_key, resolved_key),
|
|
880
1330
|
project_label=project_label,
|
|
881
1331
|
model=str(row["model"]),
|
|
882
|
-
input_tokens=
|
|
883
|
-
cached_input_tokens=
|
|
884
|
-
output_tokens=
|
|
885
|
-
reasoning_output_tokens=
|
|
886
|
-
total_tokens=
|
|
1332
|
+
input_tokens=input_tokens,
|
|
1333
|
+
cached_input_tokens=cached_input_tokens,
|
|
1334
|
+
output_tokens=output_tokens,
|
|
1335
|
+
reasoning_output_tokens=reasoning_output_tokens,
|
|
1336
|
+
total_tokens=total_tokens,
|
|
887
1337
|
cost_usd=cost_usd,
|
|
888
1338
|
# NULL ≡ unattributed — the cache-read rule (#416 §5.2). Carried,
|
|
889
1339
|
# never grouped; see the field's comment on `QualifiedCodexEntry`.
|
|
@@ -893,12 +1343,6 @@ def _qualify_codex_rows(
|
|
|
893
1343
|
return tuple(result)
|
|
894
1344
|
|
|
895
1345
|
|
|
896
|
-
_CODEX_PROJECT_THREAD_METADATA_SQL = """
|
|
897
|
-
SELECT conversation_key, source_root_key, cwd, git_json
|
|
898
|
-
FROM codex_conversation_threads
|
|
899
|
-
"""
|
|
900
|
-
|
|
901
|
-
|
|
902
1346
|
def _require_sql_replace(sql: str, marker: str, replacement: str) -> str:
|
|
903
1347
|
"""Rewrite ``marker`` in ``sql``, refusing when it is not there.
|
|
904
1348
|
|
|
@@ -974,25 +1418,6 @@ _ROOT_PATH_SCOPED_CODEX_ENTRIES_SQL = _require_sql_replace(
|
|
|
974
1418
|
)
|
|
975
1419
|
|
|
976
1420
|
|
|
977
|
-
def _load_inherited_codex_metadata(conn) -> "dict[tuple[str, str], sqlite3.Row]":
|
|
978
|
-
"""The file-alias metadata map, keyed by ``(source_root_key, path)``.
|
|
979
|
-
|
|
980
|
-
``setdefault`` and not assignment: the statement orders by
|
|
981
|
-
``inherited.last_seen_utc DESC, inherited.conversation_key DESC`` and the
|
|
982
|
-
qualifier keeps the FIRST row per identity, so a second writer for the same
|
|
983
|
-
path must not win.
|
|
984
|
-
"""
|
|
985
|
-
inherited_metadata: "dict[tuple[str, str], sqlite3.Row]" = {}
|
|
986
|
-
if not _supports_native_file_aliases(conn):
|
|
987
|
-
return inherited_metadata
|
|
988
|
-
for inherited in conn.execute(_INHERITED_CODEX_PROJECT_METADATA_SQL):
|
|
989
|
-
identity = (
|
|
990
|
-
str(inherited["source_root_key"] or ""), str(inherited["path"] or ""))
|
|
991
|
-
if all(identity):
|
|
992
|
-
inherited_metadata.setdefault(identity, inherited)
|
|
993
|
-
return inherited_metadata
|
|
994
|
-
|
|
995
|
-
|
|
996
1421
|
def resolve_codex_project_scope(
|
|
997
1422
|
conn, matches_project_key, *, group: str = "git-root",
|
|
998
1423
|
) -> "tuple[tuple[tuple[str, str], ...], tuple[tuple[str, str], ...], dict]":
|
|
@@ -1042,26 +1467,35 @@ def resolve_codex_project_scope(
|
|
|
1042
1467
|
# Set here rather than assumed, so this resolver is usable on a plain
|
|
1043
1468
|
# caller-owned connection as well as inside the scoped read.
|
|
1044
1469
|
conn.row_factory = sqlite3.Row
|
|
1045
|
-
|
|
1046
|
-
for row in conn.execute(_CODEX_PROJECT_THREAD_METADATA_SQL):
|
|
1047
|
-
root_key = str(row["source_root_key"] or "")
|
|
1048
|
-
conversation_key = str(row["conversation_key"] or "")
|
|
1049
|
-
if not root_key or not conversation_key:
|
|
1050
|
-
continue
|
|
1051
|
-
if matches_project_key(
|
|
1052
|
-
projects.project_key(root_key, row["cwd"], row["git_json"])
|
|
1053
|
-
):
|
|
1054
|
-
conversations.append((root_key, conversation_key))
|
|
1055
|
-
inherited_metadata = _load_inherited_codex_metadata(conn)
|
|
1470
|
+
inventory = load_codex_thread_metadata_inventory(conn)
|
|
1056
1471
|
finally:
|
|
1057
1472
|
conn.row_factory = previous_row_factory
|
|
1473
|
+
conversations: list[tuple[str, str]] = []
|
|
1474
|
+
for (root_key, conversation_key), direct in (
|
|
1475
|
+
inventory.direct_by_conversation.items()
|
|
1476
|
+
):
|
|
1477
|
+
# #845 §4.1. A thread whose own resolver path reaches an undecodable
|
|
1478
|
+
# field resolves to nothing, so it can carry no request. Handing its
|
|
1479
|
+
# decoded `None` fields to the resolver would answer `(unassigned)`
|
|
1480
|
+
# and select the conversation for an unrelated request.
|
|
1481
|
+
if codex_metadata_is_malformed(direct, None):
|
|
1482
|
+
continue
|
|
1483
|
+
if matches_project_key(
|
|
1484
|
+
projects.project_key(root_key, direct.cwd, direct.git_json)
|
|
1485
|
+
):
|
|
1486
|
+
conversations.append((root_key, conversation_key))
|
|
1058
1487
|
paths: list[tuple[str, str]] = []
|
|
1059
|
-
for (root_key, path), inherited in
|
|
1488
|
+
for (root_key, path), inherited in inventory.inherited_by_path.items():
|
|
1489
|
+
if codex_metadata_is_malformed(None, inherited):
|
|
1490
|
+
continue
|
|
1060
1491
|
if matches_project_key(projects.project_key(
|
|
1061
|
-
root_key, inherited
|
|
1492
|
+
root_key, inherited.cwd, inherited.git_json,
|
|
1062
1493
|
)):
|
|
1063
1494
|
paths.append((root_key, path))
|
|
1064
|
-
return
|
|
1495
|
+
return (
|
|
1496
|
+
tuple(sorted(set(conversations))), tuple(sorted(set(paths))),
|
|
1497
|
+
inventory.inherited_by_path,
|
|
1498
|
+
)
|
|
1065
1499
|
|
|
1066
1500
|
|
|
1067
1501
|
def load_codex_project_scoped_entries(
|