cctally 1.112.0 → 1.114.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 (58) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +4 -5
  3. package/bin/_cctally_cache.py +1683 -485
  4. package/bin/_cctally_core.py +223 -1
  5. package/bin/_cctally_dashboard.py +96 -18
  6. package/bin/_cctally_dashboard_perf.py +128 -9
  7. package/bin/_cctally_dashboard_sources.py +267 -204
  8. package/bin/_cctally_db.py +861 -18
  9. package/bin/_cctally_diagnosis_sources.py +22 -4
  10. package/bin/_cctally_doctor.py +136 -18
  11. package/bin/_cctally_five_hour.py +2 -2
  12. package/bin/_cctally_forecast.py +27 -6
  13. package/bin/_cctally_journal.py +67 -5
  14. package/bin/_cctally_milestone_history.py +82 -9
  15. package/bin/_cctally_percent_breakdown.py +3 -2
  16. package/bin/_cctally_quota.py +201 -33
  17. package/bin/_cctally_record.py +14 -0
  18. package/bin/_cctally_rederive.py +23 -9
  19. package/bin/_cctally_source_analytics.py +60 -15
  20. package/bin/_cctally_statusline.py +17 -0
  21. package/bin/_cctally_store.py +80 -5
  22. package/bin/_cctally_tui.py +4 -3
  23. package/bin/_cctally_weekrefs.py +128 -21
  24. package/bin/_lib_cache_report.py +35 -6
  25. package/bin/_lib_codex_conversation.py +32 -0
  26. package/bin/_lib_codex_conversation_query.py +164 -30
  27. package/bin/_lib_conversation_query.py +95 -56
  28. package/bin/_lib_conversation_retention.py +1605 -640
  29. package/bin/_lib_cost_provenance.py +53 -2
  30. package/bin/_lib_doctor.py +263 -69
  31. package/bin/_lib_fmt.py +11 -0
  32. package/bin/_lib_merge_gate.py +469 -51
  33. package/bin/_lib_perf.py +27 -1
  34. package/bin/_lib_pricing.py +209 -8
  35. package/bin/_lib_pricing_check.py +20 -4
  36. package/bin/_lib_reclaim_planner.py +792 -0
  37. package/bin/_lib_rederive.py +225 -77
  38. package/bin/_lib_snapshot_cache.py +21 -0
  39. package/bin/_lib_sqlite_close.py +81 -0
  40. package/bin/_lib_subscription_weeks.py +100 -17
  41. package/bin/_lib_tick_stats.py +217 -17
  42. package/bin/_lib_wal_checkpoint.py +1077 -0
  43. package/bin/_lib_write_budget.py +483 -0
  44. package/bin/_lib_write_io.py +703 -0
  45. package/bin/cctally +8 -0
  46. package/dashboard/static/assets/{ConversationsView-CEZ-BUyi.js → ConversationsView-Bc8LCOAZ.js} +1 -1
  47. package/dashboard/static/assets/{DoctorModal-CE1oH_Zy.js → DoctorModal-D--qVBwe.js} +1 -1
  48. package/dashboard/static/assets/ModalRoot-DLPgyAM_.js +1 -0
  49. package/dashboard/static/assets/{ProjectsDrillPanel-B3DZoCeD.js → ProjectsDrillPanel-z53a4v4M.js} +1 -1
  50. package/dashboard/static/assets/{SourceDetailModal-CHp9EgLP.js → SourceDetailModal-Bw0T8W-W.js} +1 -1
  51. package/dashboard/static/assets/{UpdateModal-5iKST6jo.js → UpdateModal-D5AKx4Yw.js} +1 -1
  52. package/dashboard/static/assets/index-BKITk1CT.css +1 -0
  53. package/dashboard/static/assets/{index-Cm0VTxJC.js → index-BXDOzSng.js} +3 -3
  54. package/dashboard/static/assets/{outlineNavigation-DkXGL8J6.js → outlineNavigation-JObY7cUY.js} +1 -1
  55. package/dashboard/static/dashboard.html +2 -2
  56. package/package.json +6 -1
  57. package/dashboard/static/assets/ModalRoot-D5Y8IFUG.js +0 -1
  58. package/dashboard/static/assets/index-Dt4tqf43.css +0 -1
@@ -1314,6 +1314,21 @@ def _preliminary_decision() -> "_candidates.ReductionDecision | None":
1314
1314
  )
1315
1315
 
1316
1316
 
1317
+ def _finalize_wal_checkpoints() -> None:
1318
+ """The W9 finalizer before a forked child's ``os._exit`` (#901 Q14).
1319
+
1320
+ `os._exit` skips `atexit`. The persist child records usage and the OAuth
1321
+ child may too, and either can reach a cache sync that arms a store; the
1322
+ finalizer then attempts one PASSIVE per armed store and closes the
1323
+ keepers. With nothing armed it does nothing."""
1324
+ try:
1325
+ import _lib_wal_checkpoint
1326
+
1327
+ _lib_wal_checkpoint.finalize()
1328
+ except BaseException: # noqa: BLE001 — the child exits regardless
1329
+ pass
1330
+
1331
+
1317
1332
  def _fork_persist(parent_lock_fd: int) -> None:
1318
1333
  """Run the revalidated spool reducer in a detached serialized child."""
1319
1334
  try:
@@ -1375,6 +1390,7 @@ def _fork_persist(parent_lock_fd: int) -> None:
1375
1390
  except BaseException:
1376
1391
  pass
1377
1392
  finally:
1393
+ _finalize_wal_checkpoints()
1378
1394
  os._exit(0)
1379
1395
 
1380
1396
 
@@ -1487,6 +1503,7 @@ def _fork_statusline_oauth_refresh(lock_fd: int) -> bool:
1487
1503
  pass
1488
1504
  finally:
1489
1505
  _release_statusline_oauth_lock(lock_fd)
1506
+ _finalize_wal_checkpoints()
1490
1507
  os._exit(0)
1491
1508
 
1492
1509
 
@@ -168,25 +168,100 @@ def open_index(store: str) -> sqlite3.Connection:
168
168
  return conn
169
169
 
170
170
 
171
- def apply_policy(conn: sqlite3.Connection, store: str) -> None:
171
+ #: `PRAGMA auto_vacuum`'s numeric report for each policy value.
172
+ _AUTO_VACUUM_MODES = {"NONE": 0, "FULL": 1, "INCREMENTAL": 2}
173
+
174
+
175
+ def apply_policy(
176
+ conn: sqlite3.Connection, store: str, *, memory_temp_store: bool = True,
177
+ ) -> None:
172
178
  """Apply the §6.1 PRAGMA policy for ``store`` to an open connection.
173
179
 
174
180
  ``auto_vacuum`` (when set) is emitted first: it only takes effect before the
175
181
  first page is written, so it must precede ``journal_mode`` / any DDL.
182
+ ``memory_temp_store`` is forwarded to ``apply_connection_policy``.
183
+
184
+ #901 W9 (Q14): the pragma is emitted only when the database does not
185
+ already report the policy's mode. Setting INCREMENTAL or FULL rewrites
186
+ page 1 (a write transaction) even when the value is unchanged, so a
187
+ current-schema conversations.db open wrote a WAL frame on every pass.
188
+ Creation (an empty file reports 0) and a database in another mode still
189
+ receive it, the latter so a later ``VACUUM`` on that connection converts.
176
190
  """
177
191
  policy = STORE_POLICY[store]
178
192
  if policy.auto_vacuum is not None:
179
- conn.execute(f"PRAGMA auto_vacuum={policy.auto_vacuum}")
193
+ wanted = _AUTO_VACUUM_MODES[policy.auto_vacuum.upper()]
194
+ current = conn.execute("PRAGMA auto_vacuum").fetchone()[0]
195
+ if int(current) != wanted:
196
+ conn.execute(f"PRAGMA auto_vacuum={policy.auto_vacuum}")
180
197
  conn.execute(f"PRAGMA journal_mode={policy.journal_mode}")
181
- apply_connection_policy(conn, store)
198
+ apply_connection_policy(conn, store, memory_temp_store=memory_temp_store)
182
199
 
183
200
 
184
- def apply_connection_policy(conn: sqlite3.Connection, store: str) -> None:
185
- """Apply non-schema connection settings without changing journal mode."""
201
+ def apply_connection_policy(
202
+ conn: sqlite3.Connection, store: str, *, memory_temp_store: bool = True,
203
+ ) -> None:
204
+ """Apply non-schema connection settings without changing journal mode.
205
+
206
+ Every ordinary writable opener calls this once at open, before any
207
+ transaction, so it also carries the #901 writer temp-store policy
208
+ (``apply_writer_temp_store``). ``memory_temp_store=False`` is for a
209
+ writer §4.7 does not enumerate, which keeps SQLite's default.
210
+ """
186
211
  policy = STORE_POLICY[store]
187
212
  conn.execute(f"PRAGMA synchronous={policy.synchronous}")
188
213
  conn.execute(f"PRAGMA busy_timeout={policy.busy_timeout}")
189
214
  conn.execute(f"PRAGMA journal_size_limit={policy.journal_size_limit}")
215
+ if memory_temp_store:
216
+ apply_writer_temp_store(conn)
217
+
218
+
219
+ # #901 §5.3a (Q11, design answer dc7 D1): the sync and ingest writers keep
220
+ # their statement journals in memory. A statement that dirties more than 64 KiB
221
+ # of pages that existed when it started (an FTS5 segment write behind the
222
+ # conversation-message trigger, a quota insert maintaining eight indexes and
223
+ # two trigger tables) otherwise spills its journal to an `etilqs_*` file, which
224
+ # SQLite unlinks at creation, so nothing in the data directory shows it.
225
+ # Measured on the revision-5 candidate: 3.7-10 MB of such files per dashboard
226
+ # window and per Codex hook.
227
+ #
228
+ # Set once per connection at open, never toggled per pass. The bound is per
229
+ # statement (the original images of the pages that one statement dirties), not
230
+ # a fixed cap; the cache size is unchanged. It is NOT a licence for sorts:
231
+ # G1 (tests/test_901_sql_plan_guard.py) judges every recurring plan regardless
232
+ # of this setting, so moving a history-sized sort into memory never satisfies
233
+ # I1. Openers outside spec §4.7 keep SQLite's default: read-only, state,
234
+ # backup, checkpoint and vacuum connections (VACUUM builds its copy in the temp
235
+ # store, so MEMORY there would hold the whole database in memory), and the
236
+ # stats publication connection. The deletion connection keeps its own Q5
237
+ # MEMORY/FILE selection and reclaim its own settings
238
+ # (`_lib_conversation_retention`).
239
+ #
240
+ #: The temp store every ordinary writable opener selects. A test sets "FILE"
241
+ #: or "DEFAULT" for the file-temp control (tests/test_901_connection_policy.py).
242
+ WRITER_TEMP_STORE = "MEMORY"
243
+
244
+
245
+ def apply_writer_temp_store(conn: sqlite3.Connection) -> None:
246
+ """Select ``WRITER_TEMP_STORE`` on a writable connection, outside any
247
+ transaction (SQLite ignores the change inside one)."""
248
+ conn.execute(f"PRAGMA temp_store = {WRITER_TEMP_STORE}")
249
+
250
+
251
+ #: PR-3: the temp store an opener selects for the duration of PENDING schema
252
+ #: and migration work (a first open, an upgrade, a cutover), then restores
253
+ #: ``WRITER_TEMP_STORE``. A one-time index build or a migration over a
254
+ #: history-sized table sorts and journals in the temp store, which under MEMORY
255
+ #: is a RAM spike as large as that table; before #901 no temp store was
256
+ #: selected, so this is the base behaviour for that work. A current-schema open
257
+ #: never selects it, so the steady state is unchanged.
258
+ SCHEMA_WORK_TEMP_STORE = "FILE"
259
+
260
+
261
+ def apply_schema_work_temp_store(conn: sqlite3.Connection) -> None:
262
+ """Select ``SCHEMA_WORK_TEMP_STORE`` before pending schema work, outside
263
+ any transaction; the opener selects the writer temp store again after."""
264
+ conn.execute(f"PRAGMA temp_store = {SCHEMA_WORK_TEMP_STORE}")
190
265
 
191
266
 
192
267
  # --------------------------------------------------------------------------
@@ -223,7 +223,7 @@ from _lib_display_tz import (
223
223
  _compute_display_block,
224
224
  )
225
225
  from _lib_aggregators import _aggregate_monthly, codex_path_scope
226
- from _lib_fmt import stable_sum
226
+ from _lib_fmt import _fmt_usd_accounting, stable_sum
227
227
  # Opt-in backend phase-instrumentation collector (issue #276, Session A). Pure
228
228
  # stdlib leaf; near-noop when CCTALLY_PERF_TRACE is unset (phase() returns a
229
229
  # shared no-op singleton), so the _tui_build_snapshot seam wraps below cost
@@ -8401,8 +8401,9 @@ def _tui_modal_current_week(snap, runtime, width):
8401
8401
  ms.crossed_at, runtime.display_tz,
8402
8402
  fmt="%b %d %H:%M:%S", suffix=True,
8403
8403
  )
8404
- cumul_str = f"${ms.cumulative_cost_usd:.2f}".ljust(8)
8405
- marg_str = (f"${ms.marginal_cost_usd:.2f}" if ms.marginal_cost_usd is not None else "—").ljust(10)
8404
+ cumul_str = _fmt_usd_accounting(ms.cumulative_cost_usd, 2).ljust(8)
8405
+ marg_str = (_fmt_usd_accounting(ms.marginal_cost_usd, 2)
8406
+ if ms.marginal_cost_usd is not None else "—").ljust(10)
8406
8407
  line = f" {{b}}{ms.percent:>3}{{/}} {{bright}}{ts_str:<22}{{/}} {{b}}{cumul_str}{{/}} {{b}}{marg_str}{{/}}"
8407
8408
  if show_5h:
8408
8409
  five_str = (f"{int(ms.five_hour_pct_at_crossing)}%"
@@ -88,6 +88,131 @@ def _get_canonical_boundary_for_date(
88
88
  return None, None
89
89
 
90
90
 
91
+ #: #901 (I1/G1, Amendment 1 item 8 and 1b): one leg per snapshot table of the
92
+ #: recent-week read. Each groups ONE table's weeks newest first, streamed off a
93
+ #: week-date-leading index, so neither leg sorts anything; `_recent_week_rows`
94
+ #: merges the two. The usage leg reads the covering epoch-1017
95
+ #: `idx_usage_week_date_group` (no table lookups), falling back to
96
+ #: `idx_usage_week_time` on an index without it; the cost leg reads
97
+ #: `idx_cost_week_time`.
98
+ _RECENT_USAGE_WEEKS_SQL = (
99
+ "SELECT week_start_date, MAX(week_end_date) AS week_end_date "
100
+ "FROM weekly_usage_snapshots{hint}{account} "
101
+ "GROUP BY week_start_date ORDER BY week_start_date DESC"
102
+ )
103
+ _RECENT_COST_WEEKS_SQL = (
104
+ "SELECT week_start_date, MAX(week_end_date) AS week_end_date "
105
+ "FROM weekly_cost_snapshots{hint}{account} "
106
+ "GROUP BY week_start_date ORDER BY week_start_date DESC"
107
+ )
108
+
109
+
110
+ def _sqlite_sort_key(value) -> tuple:
111
+ """Rank a stored value the way SQLite's BINARY comparison ranks it.
112
+
113
+ NULL first, then numbers (integers and reals compared numerically), then
114
+ text in code-point order (which is UTF-8 byte order), then blobs.
115
+ """
116
+ if value is None:
117
+ return (0, 0)
118
+ if isinstance(value, (int, float)):
119
+ return (1, value)
120
+ if isinstance(value, str):
121
+ return (2, value)
122
+ return (3, bytes(value))
123
+
124
+
125
+ def _sqlite_max(left, right):
126
+ """SQLite's MAX() over two group maxima: NULL never wins."""
127
+ if left is None:
128
+ return right
129
+ if right is None:
130
+ return left
131
+ return left if _sqlite_sort_key(left) >= _sqlite_sort_key(right) else right
132
+
133
+
134
+ def _recent_week_rows(
135
+ conn: sqlite3.Connection, limit: "int | None", *,
136
+ account_key: "str | None" = None,
137
+ ) -> "list[tuple[object, object]]":
138
+ """The newest ``limit`` week dates across both snapshot tables (#901).
139
+
140
+ Returns ``(week_start_date, week_end_date)`` pairs, newest date first, where
141
+ ``week_end_date`` is the latest end date EITHER table records for that
142
+ date: exactly the rows of the pre-#901 statement, which grouped the
143
+ ``UNION ALL`` of both tables through a temp b-tree over every snapshot ever
144
+ recorded and applied ``LIMIT`` only afterwards (Amendment 1 item 8,
145
+ ``c901-design-2``). Each table now streams its own groups in descending
146
+ date order; this merges the two cursors, combining equal dates with MAX,
147
+ holds one pending group per cursor, and stops at ``limit`` merged distinct
148
+ dates. ``None`` or a negative ``limit`` is unbounded and ``0`` returns
149
+ nothing, as SQL ``LIMIT`` did. The limit applies before any boundary
150
+ conversion or invalid-ref filtering, as it did.
151
+
152
+ Both cursors read one snapshot: inside the caller's transaction when there
153
+ is one (left open), otherwise inside a read transaction opened and closed
154
+ here. Each leg is pinned to its index only when the index exists; the
155
+ usage leg prefers the covering ``idx_usage_week_date_group`` (Amendment
156
+ 1b) and keeps ``idx_usage_week_time`` as its compatibility fallback.
157
+ """
158
+ limit_n = -1 if limit is None else int(limit)
159
+ if limit_n == 0:
160
+ return []
161
+ present = {
162
+ str(row[0]) for row in conn.execute(
163
+ "SELECT name FROM sqlite_master WHERE type='index' AND name IN "
164
+ "('idx_usage_week_date_group', 'idx_usage_week_time', "
165
+ "'idx_cost_week_time')")
166
+ }
167
+ account = "" if account_key is None else " WHERE account_key = ?"
168
+ params: tuple = () if account_key is None else (account_key,)
169
+ usage_index = next(
170
+ (name for name in ("idx_usage_week_date_group", "idx_usage_week_time")
171
+ if name in present), None)
172
+ usage_sql = _RECENT_USAGE_WEEKS_SQL.format(
173
+ hint=f" INDEXED BY {usage_index}" if usage_index else "",
174
+ account=account)
175
+ cost_sql = _RECENT_COST_WEEKS_SQL.format(
176
+ hint=(" INDEXED BY idx_cost_week_time"
177
+ if "idx_cost_week_time" in present else ""),
178
+ account=account)
179
+ own_transaction = not conn.in_transaction
180
+ if own_transaction:
181
+ conn.execute("BEGIN")
182
+ try:
183
+ usage = conn.execute(usage_sql, params)
184
+ cost = conn.execute(cost_sql, params)
185
+ try:
186
+ rows: "list[tuple[object, object]]" = []
187
+ pending_usage = usage.fetchone()
188
+ pending_cost = cost.fetchone()
189
+ while (pending_usage is not None or pending_cost is not None) and (
190
+ limit_n < 0 or len(rows) < limit_n):
191
+ if pending_cost is None or (
192
+ pending_usage is not None
193
+ and _sqlite_sort_key(pending_usage[0])
194
+ > _sqlite_sort_key(pending_cost[0])):
195
+ rows.append((pending_usage[0], pending_usage[1]))
196
+ pending_usage = usage.fetchone()
197
+ elif pending_usage is None or (
198
+ _sqlite_sort_key(pending_cost[0])
199
+ > _sqlite_sort_key(pending_usage[0])):
200
+ rows.append((pending_cost[0], pending_cost[1]))
201
+ pending_cost = cost.fetchone()
202
+ else:
203
+ rows.append((pending_usage[0], _sqlite_max(
204
+ pending_usage[1], pending_cost[1])))
205
+ pending_usage = usage.fetchone()
206
+ pending_cost = cost.fetchone()
207
+ finally:
208
+ usage.close()
209
+ cost.close()
210
+ finally:
211
+ if own_transaction:
212
+ conn.execute("COMMIT")
213
+ return rows
214
+
215
+
91
216
  def get_recent_weeks(
92
217
  conn: sqlite3.Connection, limit: "int | None", *,
93
218
  account_key: "str | None" = None,
@@ -100,33 +225,15 @@ def get_recent_weeks(
100
225
  # (today's byte-identical behavior); a real key / ``unattributed`` scopes both
101
226
  # snapshot legs to that account's weeks (the ``--account`` render consumers —
102
227
  # ``report`` — pass it explicitly).
103
- limit_sql = -1 if limit is None else int(limit)
104
- acct_pred = "" if account_key is None else " WHERE account_key = ?"
105
- acct_p: tuple = () if account_key is None else (account_key,)
106
- rows = conn.execute(
107
- f"""
108
- SELECT week_start_date, MAX(week_end_date) AS week_end_date
109
- FROM (
110
- SELECT week_start_date, week_end_date FROM weekly_usage_snapshots{acct_pred}
111
- UNION ALL
112
- SELECT week_start_date, week_end_date FROM weekly_cost_snapshots{acct_pred}
113
- )
114
- GROUP BY week_start_date
115
- ORDER BY week_start_date DESC
116
- LIMIT ?
117
- """,
118
- acct_p + acct_p + (limit_sql,),
119
- ).fetchall()
120
-
121
228
  refs: list[WeekRef] = []
122
- for row in rows:
123
- date_str = row["week_start_date"]
229
+ for date_str, week_end_date in _recent_week_rows(
230
+ conn, limit, account_key=account_key):
124
231
  canon_start, canon_end = _get_canonical_boundary_for_date(
125
232
  conn, date_str, account_key=account_key)
126
233
  try:
127
234
  ref = make_week_ref(
128
235
  week_start_date=date_str,
129
- week_end_date=row["week_end_date"],
236
+ week_end_date=week_end_date,
130
237
  week_start_at=canon_start,
131
238
  week_end_at=canon_end,
132
239
  )
@@ -59,9 +59,13 @@ stable_sum = _import_stable_sum()
59
59
 
60
60
 
61
61
  def _import_pricing_kernel():
62
- """Resolve the ``_lib_pricing`` symbols this kernel needs (#195/#413):
63
- ``CACHE_WRITE_1H_MULTIPLIER`` (the derived 1-hour cache-write rate) and
64
- ``claude_usage_dict`` (the single cost-feeding usage-dict constructor).
62
+ """Resolve the ``_lib_pricing`` symbols this kernel needs, returned in
63
+ this order: ``CACHE_WRITE_1H_MULTIPLIER`` (the derived 1-hour cache-write
64
+ rate, #195), ``_claude_fast_multiplier`` (the Fast-mode multiplier for a
65
+ model), ``claude_usage_dict`` (the single cost-feeding usage-dict
66
+ constructor, #413), ``TIERED_THRESHOLD`` (the legacy 200K marginal-tier
67
+ boundary) and ``_select_claude_request_card`` (the whole-request card
68
+ selector, #929).
65
69
 
66
70
  Same shape and same justification as ``_import_stable_sum`` above:
67
71
  ``_lib_pricing`` is a PURE stdlib leaf with no sibling imports, so binding
@@ -79,6 +83,7 @@ def _import_pricing_kernel():
79
83
  m._claude_fast_multiplier,
80
84
  m.claude_usage_dict,
81
85
  m.TIERED_THRESHOLD,
86
+ m._select_claude_request_card,
82
87
  )
83
88
  from pathlib import Path
84
89
  import importlib.util
@@ -99,11 +104,13 @@ def _import_pricing_kernel():
99
104
  m._claude_fast_multiplier,
100
105
  m.claude_usage_dict,
101
106
  m.TIERED_THRESHOLD,
107
+ m._select_claude_request_card,
102
108
  )
103
109
 
104
110
 
105
111
  (CACHE_WRITE_1H_MULTIPLIER, _claude_fast_multiplier,
106
- claude_usage_dict, DEFAULT_TIERED_THRESHOLD) = _import_pricing_kernel()
112
+ claude_usage_dict, DEFAULT_TIERED_THRESHOLD,
113
+ _select_claude_request_card) = _import_pricing_kernel()
107
114
 
108
115
 
109
116
  # ``DEFAULT_TIERED_THRESHOLD`` above is Anthropic's per-call >200K-tokens tier,
@@ -353,6 +360,7 @@ def _compute_entry_cache_dollars(
353
360
  cache_creation_tokens: int,
354
361
  cache_read_tokens: int,
355
362
  *,
363
+ input_tokens: int,
356
364
  pricing: dict,
357
365
  tiered_threshold: "int | None" = None,
358
366
  cache_1h_tokens: int | None = None,
@@ -384,12 +392,27 @@ def _compute_entry_cache_dollars(
384
392
  than in the signature. A default argument is evaluated once, at
385
393
  function-definition time, so a change to the module global would never
386
394
  reach the calls.
395
+
396
+ ``input_tokens`` (REQUIRED keyword, #929) is the entry's uncached input.
397
+ A whole-request card (Claude Haiku 5.5) selects ONE card per request from
398
+ its full prompt — input + cache creation + cache read — exactly as the
399
+ cost kernel does, so the card is selected once here, before any rate is
400
+ read. Selecting from the cache tokens alone would price a long request at
401
+ the base card. A legacy card is returned unchanged and every expression
402
+ below runs exactly as before.
387
403
  """
388
404
  if tiered_threshold is None:
389
405
  tiered_threshold = DEFAULT_TIERED_THRESHOLD
390
406
  p = _lookup_pricing(model, pricing) or {}
391
407
  if not p:
392
408
  return (0.0, 0.0, 0.0)
409
+ # The selected card carries no `_above_200k_tokens` key, so
410
+ # `_tiered_rate` returns its flat rates, and the wasted expression below
411
+ # is already the direct TTL form on the selected input rate.
412
+ p = _select_claude_request_card(
413
+ p, input_tokens=input_tokens,
414
+ cache_creation_tokens=cache_creation_tokens,
415
+ cache_read_tokens=cache_read_tokens)
393
416
 
394
417
  def _tiered_rate(tokens: int, base_key: str, tiered_key: str) -> float:
395
418
  """Blended $/token rate for a single-call token count under tiered pricing."""
@@ -527,7 +550,9 @@ def _aggregate_cache_by_day(
527
550
  )
528
551
  else:
529
552
  saved, wasted, net = _compute_entry_cache_dollars(
530
- entry.model, create_tok, read_tok, pricing=pricing,
553
+ entry.model, create_tok, read_tok,
554
+ input_tokens=entry.usage.get("input_tokens", 0), # #929
555
+ pricing=pricing,
531
556
  # #195: the normalized flat key the ingest chokepoint writes;
532
557
  # absent on a pre-split row -> None -> unchanged pricing.
533
558
  cache_1h_tokens=entry.usage.get("cache_creation_1h_input_tokens"),
@@ -703,6 +728,7 @@ def _aggregate_cache_by_session(
703
728
  entry.model,
704
729
  entry.cache_creation_tokens,
705
730
  entry.cache_read_tokens,
731
+ input_tokens=entry.input_tokens, # #929
706
732
  pricing=pricing,
707
733
  # #195: getattr, not attribute access. This is a PURE kernel with
708
734
  # a duck-typed entry contract — callers pass `_JoinedClaudeEntry`
@@ -1156,6 +1182,7 @@ def _aggregate_cache_breakdown(
1156
1182
  getattr(e, "model", ""),
1157
1183
  getattr(e, "cache_creation_tokens", 0),
1158
1184
  getattr(e, "cache_read_tokens", 0),
1185
+ input_tokens=getattr(e, "input_tokens", 0), # #929
1159
1186
  pricing=pricing,
1160
1187
  # #195: getattr default None == split unknown, so an entry type
1161
1188
  # that does not carry the split prices exactly as before.
@@ -1270,7 +1297,9 @@ def aggregate_by_day_project(
1270
1297
  create_tok = getattr(e, "cache_creation_tokens", 0)
1271
1298
  read_tok = getattr(e, "cache_read_tokens", 0)
1272
1299
  _s, _w, net = _compute_entry_cache_dollars(
1273
- model, create_tok, read_tok, pricing=pricing,
1300
+ model, create_tok, read_tok,
1301
+ input_tokens=getattr(e, "input_tokens", 0), # #929
1302
+ pricing=pricing,
1274
1303
  cache_1h_tokens=getattr(e, "cache_1h_tokens", None), # #195
1275
1304
  speed=getattr(e, "speed", None),
1276
1305
  )
@@ -2167,6 +2167,38 @@ def _is_late_turn_anchor(
2167
2167
  )
2168
2168
 
2169
2169
 
2170
+ def in_physical_order(rows: Iterable[Any], *, timestamp, position) -> list:
2171
+ """Rows read in ``(timestamp_utc, id)`` index order, returned in physical
2172
+ order: ``(timestamp_utc, source_path, line_offset)``.
2173
+
2174
+ #901 Amendment 19 T1 (spec I1). ``idx_codex_conv_msgs_conversation`` is
2175
+ ``(conversation_key, timestamp_utc, id)``, so a read ordered by the
2176
+ physical position past the timestamp left SQLite a "LAST 2 TERMS" sorter
2177
+ over every run of rows tied on one timestamp, and one conversation's tie
2178
+ run (a whole conversation, when its rows share or lack a timestamp) is not
2179
+ a bound. The caller now reads in index order and this orders each run in
2180
+ Python: the same rows the caller already holds, no temp structure.
2181
+
2182
+ Exact: equal timestamps (NULL included) are contiguous in index order and
2183
+ compare equal here as they do in an ORDER BY, and ``UNIQUE(source_path,
2184
+ line_offset)`` makes ``position`` a total order inside a run.
2185
+ """
2186
+ ordered: list = []
2187
+ run: list = []
2188
+ run_timestamp = None
2189
+ for row in rows:
2190
+ stamp = timestamp(row)
2191
+ if run and stamp != run_timestamp:
2192
+ run.sort(key=position)
2193
+ ordered.extend(run)
2194
+ run = []
2195
+ run_timestamp = stamp
2196
+ run.append(row)
2197
+ run.sort(key=position)
2198
+ ordered.extend(run)
2199
+ return ordered
2200
+
2201
+
2170
2202
  def codex_event_is_late_turn_anchor(event: Any) -> bool:
2171
2203
  """Public predicate shared by delta persistence and turn inference."""
2172
2204
  try: