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.
Files changed (47) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +6 -6
  3. package/bin/_cctally_cache.py +62 -13
  4. package/bin/_cctally_core.py +8 -1
  5. package/bin/_cctally_dashboard.py +422 -195
  6. package/bin/_cctally_dashboard_conversation.py +231 -40
  7. package/bin/_cctally_dashboard_envelope.py +48 -32
  8. package/bin/_cctally_dashboard_sources.py +503 -109
  9. package/bin/_cctally_db.py +44 -0
  10. package/bin/_cctally_diagnosis_sources.py +181 -15
  11. package/bin/_cctally_doctor.py +15 -3
  12. package/bin/_cctally_forecast.py +25 -12
  13. package/bin/_cctally_journal.py +59 -32
  14. package/bin/_cctally_milestone_history.py +65 -9
  15. package/bin/_cctally_parser.py +5 -0
  16. package/bin/_cctally_record.py +50 -12
  17. package/bin/_cctally_rederive.py +899 -30
  18. package/bin/_cctally_source_analytics.py +567 -133
  19. package/bin/_cctally_transcript.py +27 -2
  20. package/bin/_cctally_tui.py +39 -6
  21. package/bin/_cctally_update.py +420 -14
  22. package/bin/_lib_cache_report.py +7 -10
  23. package/bin/_lib_codex_conversation_query.py +286 -63
  24. package/bin/_lib_codex_metadata.py +207 -0
  25. package/bin/_lib_conversation_query.py +254 -37
  26. package/bin/_lib_doctor.py +16 -2
  27. package/bin/_lib_pricing.py +80 -276
  28. package/bin/_lib_rederive.py +903 -2
  29. package/bin/_lib_view_models.py +5 -0
  30. package/bin/cctally +18 -5
  31. package/dashboard/static/assets/ConversationsView-BB9vUiL6.js +72 -0
  32. package/dashboard/static/assets/{DoctorModal-BZGXOlc2.js → DoctorModal-DFY2iPDf.js} +1 -1
  33. package/dashboard/static/assets/ModalRoot-DvRKHbQE.js +1 -0
  34. package/dashboard/static/assets/{ProjectsDrillPanel-KbO_KWa7.js → ProjectsDrillPanel-DAv39uSo.js} +1 -1
  35. package/dashboard/static/assets/{SourceDetailModal-BrOPLVRL.js → SourceDetailModal-DSAWo2Fp.js} +1 -1
  36. package/dashboard/static/assets/{UpdateModal-CYgEFAi4.js → UpdateModal-R_9dOVnh.js} +3 -3
  37. package/dashboard/static/assets/index-COhxe3vU.css +1 -0
  38. package/dashboard/static/assets/index-DdU62sov.js +13 -0
  39. package/dashboard/static/assets/outlineNavigation-BOCLcRa6.js +9 -0
  40. package/dashboard/static/assets/{useKeymap-DQx_A1MB.js → useKeymap-CW7Up0Ay.js} +1 -1
  41. package/dashboard/static/dashboard.html +3 -3
  42. package/package.json +2 -1
  43. package/dashboard/static/assets/ConversationsView-TO5bpIyc.js +0 -72
  44. package/dashboard/static/assets/ModalRoot-C_fvugTl.js +0 -1
  45. package/dashboard/static/assets/index-Cj_l7T7u.css +0 -1
  46. package/dashboard/static/assets/index-DLVwACUM.js +0 -13
  47. 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 self.missing_conversation_key_rows + self.missing_thread_join_rows
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, threads.cwd, threads.git_json,
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
- _INHERITED_CODEX_PROJECT_METADATA_SQL = """
147
- SELECT files.source_root_key, files.path, inherited.cwd, inherited.git_json
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 _inherited_codex_path_metadata_sql(identity_count: int) -> str:
159
- """Constrain inherited file-alias metadata to dirty physical paths."""
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 _INHERITED_CODEX_PROJECT_METADATA_SQL.replace(
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 two qualification failures
257
- # and the qualified reader rejects more: it parses the timestamp at
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
- # TWO LIMITS, stated rather than hidden. First, this probe and the qualified
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
- # Second, the text-integer test below strips the signed-digit character set
273
- # from the left, which admits a pathological interior form such as `1-2` that
274
- # `int()` would refuse. Neither writer in this repository produces either
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 even though its `int()` sits OUTSIDE the reader's `try`, so a
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 _unusable_integer_predicate(column: str) -> str:
290
- """SQL for one token operand that ``int()`` would refuse."""
291
- qualified = f"entries.{column}"
292
- return (
293
- f"(typeof({qualified}) IN ('null', 'blob')"
294
- f" OR (typeof({qualified}) = 'text'"
295
- f" AND ({qualified} = ''"
296
- f" OR ltrim({qualified}, '+-0123456789') != '')))"
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
- #: A timestamp the qualified reader can turn into an aware UTC instant.
301
- #: `datetime()` returns NULL for anything SQLite cannot parse, and the offset
302
- #: test is what rejects a naive value — `dt.datetime.fromisoformat` yields a
303
- #: naive object for those and `_parse_timestamp` raises on it.
304
- _CODEX_AWARE_TIMESTAMP_SQL = """(
305
- entries.timestamp_utc IS NOT NULL
306
- AND datetime(entries.timestamp_utc) IS NOT NULL
307
- AND (
308
- entries.timestamp_utc LIKE '%Z'
309
- OR entries.timestamp_utc GLOB '*[+-][0-9][0-9]:[0-9][0-9]'
310
- OR entries.timestamp_utc GLOB '*[+-][0-9][0-9][0-9][0-9]'
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 NOT {_CODEX_AWARE_TIMESTAMP_SQL}
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
- try:
433
- columns = cache_conn.execute("PRAGMA table_info(codex_session_files)").fetchall()
434
- except sqlite3.Error:
435
- return False
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
- (_CODEX_PROJECT_METADATA_HEALTH_SQL if _supports_native_file_aliases(cache_conn)
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
- total_rows, missing_key_rows, missing_join_rows = (int(value) for value in row)
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
- qualified_rows = total_rows - missing_key_rows - missing_join_rows
500
- values = (total_rows, qualified_rows, missing_key_rows, missing_join_rows)
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
- inherited_metadata: dict[tuple[str, str], sqlite3.Row] = {}
807
- if _supports_native_file_aliases(conn):
808
- inherited_sql = (
809
- _INHERITED_CODEX_PROJECT_METADATA_SQL if identities is None
810
- else _inherited_codex_path_metadata_sql(len(identities))
811
- )
812
- inherited_params = tuple(
813
- value for identity in (identities or ()) for value in identity
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
- cwd = row["cwd"] or (inherited["cwd"] if inherited is not None else None)
859
- git_json = row["git_json"] or (
860
- inherited["git_json"] if inherited is not None else None)
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"]), int(row["input_tokens"]),
865
- int(row["cached_input_tokens"]), int(row["output_tokens"]),
866
- int(row["reasoning_output_tokens"]), speed=speed,
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=_parse_timestamp(row["timestamp_utc"]),
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=int(row["input_tokens"]),
883
- cached_input_tokens=int(row["cached_input_tokens"]),
884
- output_tokens=int(row["output_tokens"]),
885
- reasoning_output_tokens=int(row["reasoning_output_tokens"]),
886
- total_tokens=int(row["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
- conversations: list[tuple[str, str]] = []
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 inherited_metadata.items():
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["cwd"], inherited["git_json"],
1492
+ root_key, inherited.cwd, inherited.git_json,
1062
1493
  )):
1063
1494
  paths.append((root_key, path))
1064
- return tuple(sorted(set(conversations))), tuple(sorted(set(paths))), inherited_metadata
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(