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.
- package/CHANGELOG.md +27 -0
- package/README.md +4 -5
- package/bin/_cctally_cache.py +1683 -485
- package/bin/_cctally_core.py +223 -1
- package/bin/_cctally_dashboard.py +96 -18
- package/bin/_cctally_dashboard_perf.py +128 -9
- package/bin/_cctally_dashboard_sources.py +267 -204
- package/bin/_cctally_db.py +861 -18
- package/bin/_cctally_diagnosis_sources.py +22 -4
- package/bin/_cctally_doctor.py +136 -18
- package/bin/_cctally_five_hour.py +2 -2
- package/bin/_cctally_forecast.py +27 -6
- package/bin/_cctally_journal.py +67 -5
- package/bin/_cctally_milestone_history.py +82 -9
- package/bin/_cctally_percent_breakdown.py +3 -2
- package/bin/_cctally_quota.py +201 -33
- package/bin/_cctally_record.py +14 -0
- package/bin/_cctally_rederive.py +23 -9
- package/bin/_cctally_source_analytics.py +60 -15
- package/bin/_cctally_statusline.py +17 -0
- package/bin/_cctally_store.py +80 -5
- package/bin/_cctally_tui.py +4 -3
- package/bin/_cctally_weekrefs.py +128 -21
- package/bin/_lib_cache_report.py +35 -6
- package/bin/_lib_codex_conversation.py +32 -0
- package/bin/_lib_codex_conversation_query.py +164 -30
- package/bin/_lib_conversation_query.py +95 -56
- package/bin/_lib_conversation_retention.py +1605 -640
- package/bin/_lib_cost_provenance.py +53 -2
- package/bin/_lib_doctor.py +263 -69
- package/bin/_lib_fmt.py +11 -0
- package/bin/_lib_merge_gate.py +469 -51
- package/bin/_lib_perf.py +27 -1
- package/bin/_lib_pricing.py +209 -8
- package/bin/_lib_pricing_check.py +20 -4
- package/bin/_lib_reclaim_planner.py +792 -0
- package/bin/_lib_rederive.py +225 -77
- package/bin/_lib_snapshot_cache.py +21 -0
- package/bin/_lib_sqlite_close.py +81 -0
- package/bin/_lib_subscription_weeks.py +100 -17
- package/bin/_lib_tick_stats.py +217 -17
- package/bin/_lib_wal_checkpoint.py +1077 -0
- package/bin/_lib_write_budget.py +483 -0
- package/bin/_lib_write_io.py +703 -0
- package/bin/cctally +8 -0
- package/dashboard/static/assets/{ConversationsView-CEZ-BUyi.js → ConversationsView-Bc8LCOAZ.js} +1 -1
- package/dashboard/static/assets/{DoctorModal-CE1oH_Zy.js → DoctorModal-D--qVBwe.js} +1 -1
- package/dashboard/static/assets/ModalRoot-DLPgyAM_.js +1 -0
- package/dashboard/static/assets/{ProjectsDrillPanel-B3DZoCeD.js → ProjectsDrillPanel-z53a4v4M.js} +1 -1
- package/dashboard/static/assets/{SourceDetailModal-CHp9EgLP.js → SourceDetailModal-Bw0T8W-W.js} +1 -1
- package/dashboard/static/assets/{UpdateModal-5iKST6jo.js → UpdateModal-D5AKx4Yw.js} +1 -1
- package/dashboard/static/assets/index-BKITk1CT.css +1 -0
- package/dashboard/static/assets/{index-Cm0VTxJC.js → index-BXDOzSng.js} +3 -3
- package/dashboard/static/assets/{outlineNavigation-DkXGL8J6.js → outlineNavigation-JObY7cUY.js} +1 -1
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +6 -1
- package/dashboard/static/assets/ModalRoot-D5Y8IFUG.js +0 -1
- 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
|
|
package/bin/_cctally_store.py
CHANGED
|
@@ -168,25 +168,100 @@ def open_index(store: str) -> sqlite3.Connection:
|
|
|
168
168
|
return conn
|
|
169
169
|
|
|
170
170
|
|
|
171
|
-
|
|
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
|
-
|
|
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(
|
|
185
|
-
|
|
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
|
# --------------------------------------------------------------------------
|
package/bin/_cctally_tui.py
CHANGED
|
@@ -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 =
|
|
8405
|
-
marg_str = (
|
|
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)}%"
|
package/bin/_cctally_weekrefs.py
CHANGED
|
@@ -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
|
|
123
|
-
|
|
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=
|
|
236
|
+
week_end_date=week_end_date,
|
|
130
237
|
week_start_at=canon_start,
|
|
131
238
|
week_end_at=canon_end,
|
|
132
239
|
)
|
package/bin/_lib_cache_report.py
CHANGED
|
@@ -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
|
|
63
|
-
``CACHE_WRITE_1H_MULTIPLIER`` (the derived 1-hour cache-write
|
|
64
|
-
``
|
|
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
|
|
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,
|
|
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,
|
|
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:
|