cctally 1.111.0 → 1.112.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 (37) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +5 -6
  3. package/bin/_cctally_cache.py +14 -7
  4. package/bin/_cctally_core.py +9 -1
  5. package/bin/_cctally_dashboard_envelope.py +7 -6
  6. package/bin/_cctally_dashboard_sources.py +90 -55
  7. package/bin/_cctally_doctor.py +73 -27
  8. package/bin/_cctally_forecast.py +2 -1
  9. package/bin/_cctally_journal.py +147 -6
  10. package/bin/_cctally_milestones.py +29 -3
  11. package/bin/_cctally_quota.py +10 -1
  12. package/bin/_cctally_record.py +136 -4
  13. package/bin/_cctally_rederive.py +311 -8
  14. package/bin/_cctally_source_analytics.py +20 -8
  15. package/bin/_cctally_tui.py +546 -45
  16. package/bin/_lib_codex_conversation_query.py +40 -4
  17. package/bin/_lib_codex_metadata.py +57 -11
  18. package/bin/_lib_cost_provenance.py +303 -0
  19. package/bin/_lib_dashboard_sources.py +263 -28
  20. package/bin/_lib_doctor.py +2 -2
  21. package/bin/_lib_ingest_frontier.py +137 -38
  22. package/bin/_lib_pricing.py +13 -1
  23. package/bin/_lib_rederive.py +704 -36
  24. package/bin/_lib_snapshot_cache.py +332 -9
  25. package/dashboard/static/assets/{ConversationsView-BB9vUiL6.js → ConversationsView-CEZ-BUyi.js} +1 -1
  26. package/dashboard/static/assets/{DoctorModal-DFY2iPDf.js → DoctorModal-CE1oH_Zy.js} +1 -1
  27. package/dashboard/static/assets/ModalRoot-D5Y8IFUG.js +1 -0
  28. package/dashboard/static/assets/{ProjectsDrillPanel-DAv39uSo.js → ProjectsDrillPanel-B3DZoCeD.js} +1 -1
  29. package/dashboard/static/assets/{SourceDetailModal-DSAWo2Fp.js → SourceDetailModal-CHp9EgLP.js} +1 -1
  30. package/dashboard/static/assets/{UpdateModal-R_9dOVnh.js → UpdateModal-5iKST6jo.js} +1 -1
  31. package/dashboard/static/assets/{index-DdU62sov.js → index-Cm0VTxJC.js} +2 -2
  32. package/dashboard/static/assets/index-Dt4tqf43.css +1 -0
  33. package/dashboard/static/assets/{outlineNavigation-BOCLcRa6.js → outlineNavigation-DkXGL8J6.js} +1 -1
  34. package/dashboard/static/dashboard.html +2 -2
  35. package/package.json +2 -1
  36. package/dashboard/static/assets/ModalRoot-DvRKHbQE.js +0 -1
  37. package/dashboard/static/assets/index-COhxe3vU.css +0 -1
@@ -3314,11 +3314,8 @@ def _stored_filter_sql(
3314
3314
  clauses.append(f"{alias}.project_key = ?")
3315
3315
  params.append(project_key)
3316
3316
  if undecodable_keys:
3317
- ordered = sorted(undecodable_keys)
3318
- placeholders = ",".join("?" for _ in ordered)
3319
3317
  clauses.append(
3320
- f"{alias}.conversation_key NOT IN ({placeholders})")
3321
- params.extend(ordered)
3318
+ f"cctally_codex_undecodable({alias}.conversation_key) = 0")
3322
3319
  if model is not None:
3323
3320
  # models_json is the writer's canonical JSON array. Searching for the
3324
3321
  # complete JSON string literal is exact and does not require JSON1.
@@ -3347,10 +3344,49 @@ def _page_costs(
3347
3344
  return totals
3348
3345
 
3349
3346
 
3347
+ @contextlib.contextmanager
3348
+ def _stored_undecodable_membership(
3349
+ conn: sqlite3.Connection, undecodable_keys: "frozenset[str]",
3350
+ ):
3351
+ """Install the stored page's request-bounded membership predicate.
3352
+
3353
+ Only one membership bit crosses the SQL boundary; the raw unscoped read's
3354
+ attribution values never do. Key cardinality consumes no host parameters,
3355
+ registration executes no SQL, and cleanup prevents a later request from
3356
+ seeing this request's frozen set.
3357
+ """
3358
+ if not undecodable_keys:
3359
+ yield
3360
+ return
3361
+ frozen = frozenset(undecodable_keys)
3362
+ conn.create_function(
3363
+ "cctally_codex_undecodable", 1,
3364
+ lambda conversation_key: int(conversation_key in frozen),
3365
+ deterministic=True,
3366
+ )
3367
+ try:
3368
+ yield
3369
+ finally:
3370
+ conn.create_function("cctally_codex_undecodable", 1, None)
3371
+
3372
+
3350
3373
  def _stored_browse_page(
3351
3374
  conn: sqlite3.Connection, *, effective_speed: str,
3352
3375
  project_key: str | None, model: str | None, limit: int,
3353
3376
  cursor: str | None, undecodable_keys: "frozenset[str]" = frozenset(),
3377
+ ):
3378
+ membership = undecodable_keys if project_key is not None else frozenset()
3379
+ with _stored_undecodable_membership(conn, membership):
3380
+ return _stored_browse_page_queries(
3381
+ conn, effective_speed=effective_speed, project_key=project_key,
3382
+ model=model, limit=limit, cursor=cursor,
3383
+ undecodable_keys=undecodable_keys)
3384
+
3385
+
3386
+ def _stored_browse_page_queries(
3387
+ conn: sqlite3.Connection, *, effective_speed: str,
3388
+ project_key: str | None, model: str | None, limit: int,
3389
+ cursor: str | None, undecodable_keys: "frozenset[str]" = frozenset(),
3354
3390
  ):
3355
3391
  where_sql, filter_params = _stored_filter_sql(
3356
3392
  "r", project_key, model, undecodable_keys)
@@ -23,6 +23,8 @@ from __future__ import annotations
23
23
 
24
24
  import dataclasses
25
25
  import sqlite3
26
+ import threading
27
+ from collections import OrderedDict
26
28
 
27
29
 
28
30
  #: The physical name of the raw threads table. ``resolve_codex_threads_table``
@@ -30,6 +32,16 @@ import sqlite3
30
32
  CODEX_THREADS_TABLE = "codex_conversation_threads"
31
33
 
32
34
 
35
+ # ``sqlite3.Connection`` has no writable attribute dictionary and cannot be
36
+ # weak-referenced. A small strong-key LRU carries each positive resolution on
37
+ # the connection object itself. Holding the object prevents an ``id`` reuse
38
+ # from inheriting another connection's answer; the bound prevents closed
39
+ # connection objects from accumulating without limit.
40
+ _THREADS_TABLE_RESOLUTION_CACHE_MAX = 64
41
+ _threads_table_resolution_cache: "OrderedDict[object, str]" = OrderedDict()
42
+ _threads_table_resolution_lock = threading.Lock()
43
+
44
+
33
45
  @dataclasses.dataclass(frozen=True)
34
46
  class DecodedCodexProjectMetadata:
35
47
  """One thread row's two project-metadata fields, decoded field by field.
@@ -171,7 +183,7 @@ def _schema_holds_threads_table(conn: sqlite3.Connection, schema: str) -> bool:
171
183
 
172
184
 
173
185
  def resolve_codex_threads_table(conn: sqlite3.Connection) -> str:
174
- """The RAW threads table reference for this connection, resolved at call time.
186
+ """The RAW threads table reference for this connection.
175
187
 
176
188
  A bare cache connection (``open_cache_db()``) holds the table in its own
177
189
  ``main`` schema; a conversations connection
@@ -187,6 +199,16 @@ def resolve_codex_threads_table(conn: sqlite3.Connection) -> str:
187
199
  with ``no such table`` on the bare cache connection, which is why the
188
200
  reference is resolved rather than written out.
189
201
 
202
+ A positive resolution is retained for this connection object. Production
203
+ connections establish their fixed ``main``/``cache_db`` topology before
204
+ their first read, so steady-state requests perform no repeated
205
+ ``sqlite_master`` or ``database_list`` probes. A caller that changes that
206
+ topology with ATTACH, DETACH, or table DDL must call
207
+ :func:`invalidate_codex_threads_table_resolution` before the next read.
208
+ Closing the connection ends the useful lifetime of its entry, and the
209
+ bounded LRU eventually releases the closed object. Absent-table results
210
+ are never cached.
211
+
190
212
  ``RuntimeError`` means exactly one thing: the table is absent from every
191
213
  schema this connection can see. A ``sqlite3.Error`` raised while
192
214
  resolving is a read the store could not perform and propagates unchanged,
@@ -194,14 +216,38 @@ def resolve_codex_threads_table(conn: sqlite3.Connection) -> str:
194
216
  (zero undecodable rows, the empty key set) never gives that answer for a
195
217
  locked or malformed store.
196
218
  """
219
+ with _threads_table_resolution_lock:
220
+ cached = _threads_table_resolution_cache.get(conn)
221
+ if cached is not None:
222
+ _threads_table_resolution_cache.move_to_end(conn)
223
+ return cached
224
+
225
+ resolved = None
197
226
  if _schema_holds_threads_table(conn, "main"):
198
- return f"main.{CODEX_THREADS_TABLE}"
199
- attached = tuple(conn.execute("PRAGMA database_list"))
200
- for row in attached:
201
- name = str(row[1]) if len(row) > 1 else ""
202
- if name == "cache_db" and _schema_holds_threads_table(conn, name):
203
- return f"cache_db.{CODEX_THREADS_TABLE}"
204
- raise RuntimeError(
205
- "the Codex threads table is not present in this connection's main "
206
- "schema and no cache database is attached as cache_db"
207
- )
227
+ resolved = f"main.{CODEX_THREADS_TABLE}"
228
+ else:
229
+ attached = tuple(conn.execute("PRAGMA database_list"))
230
+ for row in attached:
231
+ name = str(row[1]) if len(row) > 1 else ""
232
+ if name == "cache_db" and _schema_holds_threads_table(conn, name):
233
+ resolved = f"cache_db.{CODEX_THREADS_TABLE}"
234
+ break
235
+ if resolved is None:
236
+ raise RuntimeError(
237
+ "the Codex threads table is not present in this connection's main "
238
+ "schema and no cache database is attached as cache_db"
239
+ )
240
+
241
+ with _threads_table_resolution_lock:
242
+ _threads_table_resolution_cache[conn] = resolved
243
+ _threads_table_resolution_cache.move_to_end(conn)
244
+ while len(_threads_table_resolution_cache) > (
245
+ _THREADS_TABLE_RESOLUTION_CACHE_MAX):
246
+ _threads_table_resolution_cache.popitem(last=False)
247
+ return resolved
248
+
249
+
250
+ def invalidate_codex_threads_table_resolution(conn: sqlite3.Connection) -> None:
251
+ """Forget a positive raw-table answer after connection schema changes."""
252
+ with _threads_table_resolution_lock:
253
+ _threads_table_resolution_cache.pop(conn, None)
@@ -0,0 +1,303 @@
1
+ """Durable evidence for cost facts whose model lacked a direct rate card."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import datetime as dt
6
+ import hashlib
7
+ import json
8
+ import math
9
+
10
+
11
+ def _pricing():
12
+ """The pricing kernel the cost engine prices with, resolved per call.
13
+
14
+ A module-top binding pins whichever `_lib_pricing` object existed when this
15
+ module first loaded. A test loader that replaces that entry would then
16
+ leave this evidence describing a different rate table from the one the
17
+ cost it annotates was computed with.
18
+ """
19
+ import _lib_pricing
20
+ return _lib_pricing
21
+
22
+
23
+ def codex_cost_with_provenance(entries, *, end, speed):
24
+ """Price one retained Codex population and fingerprint its exact inputs."""
25
+ _lib_pricing = _pricing()
26
+ total = 0.0
27
+ fallback_cost = 0.0
28
+ model_costs = {}
29
+ source = []
30
+ fallback = set()
31
+ for entry in entries:
32
+ if entry.timestamp >= end: # budgets use a half-open period
33
+ continue
34
+ item = [
35
+ entry.timestamp.isoformat(), entry.source_path, entry.session_id,
36
+ entry.model, entry.input_tokens, entry.cached_input_tokens,
37
+ entry.output_tokens, entry.reasoning_output_tokens,
38
+ ]
39
+ source.append(item)
40
+ used_fallback = (
41
+ entry.input_tokens or entry.output_tokens
42
+ ) and _lib_pricing._is_codex_fallback(entry.model)
43
+ if used_fallback:
44
+ fallback.add(entry.model)
45
+ cost = _lib_pricing._calculate_codex_entry_cost(
46
+ entry.model, entry.input_tokens, entry.cached_input_tokens,
47
+ entry.output_tokens, entry.reasoning_output_tokens, speed=speed,
48
+ )
49
+ total += cost
50
+ model_costs[entry.model] = model_costs.get(entry.model, 0.0) + cost
51
+ if used_fallback:
52
+ fallback_cost += cost
53
+ encoded = json.dumps(
54
+ sorted(source), sort_keys=True, separators=(",", ":"),
55
+ ensure_ascii=False,
56
+ ).encode("utf-8")
57
+ evidence = {
58
+ "version": 1,
59
+ "pricingDate": _lib_pricing.current_pricing_snapshot().snapshot_date,
60
+ "fallbackModels": sorted(fallback),
61
+ "sourceHash": "sha256:" + hashlib.sha256(encoded).hexdigest(),
62
+ "entryCount": len(source),
63
+ "speed": speed,
64
+ "directCostUsd": total - fallback_cost,
65
+ "modelCostsUsd": model_costs,
66
+ }
67
+ return total, evidence
68
+
69
+
70
+ # ---------------------------------------------------------------------------
71
+ # Historical Claude five-hour closes (#869)
72
+ # ---------------------------------------------------------------------------
73
+
74
+ #: The ``basis`` of a close whose missing card was proven AFTER it closed, from
75
+ #: its retained entries, rather than recorded while its cost was computed. The
76
+ #: marker freezes the corrected close against every later card revision.
77
+ HISTORICAL_CLOSE_BASIS = "historical-zero-cost-inference"
78
+
79
+ #: The ``basis`` of a close corrected from the missing-card state its own cost
80
+ #: computation recorded. Without it the corrected marker is indistinguishable
81
+ #: from a close that was priced all along, and the plan could not freeze the
82
+ #: corrected close's five-hour milestones with it.
83
+ RECORDED_CLOSE_CORRECTION_BASIS = "computation-time-missing-card"
84
+
85
+ _BLOCK_TOKEN_KEYS = (
86
+ "input_tokens", "output_tokens", "cache_create_tokens", "cache_read_tokens",
87
+ )
88
+ _CLAUDE_BASE_RATES = (
89
+ "input_cost_per_token",
90
+ "output_cost_per_token",
91
+ "cache_creation_input_token_cost",
92
+ "cache_read_input_token_cost",
93
+ )
94
+ _CLAUDE_TIER_RATES = (
95
+ "input_cost_per_token_above_200k_tokens",
96
+ "output_cost_per_token_above_200k_tokens",
97
+ "cache_creation_input_token_cost_above_200k_tokens",
98
+ "cache_read_input_token_cost_above_200k_tokens",
99
+ )
100
+
101
+
102
+ def _positive_rate(value) -> bool:
103
+ return (
104
+ isinstance(value, (int, float))
105
+ and not isinstance(value, bool)
106
+ and math.isfinite(value)
107
+ and value > 0
108
+ )
109
+
110
+
111
+ def _zero_amount(value) -> bool:
112
+ return (
113
+ isinstance(value, (int, float))
114
+ and not isinstance(value, bool)
115
+ and value == 0
116
+ )
117
+
118
+
119
+ def claude_card_rate_violations(snapshot) -> list:
120
+ """Every ``(model, rate)`` a zero-cost inference would silently rely on.
121
+
122
+ A closed amount of exactly $0 proves a missing card only while no Claude
123
+ card prices a used token class at zero. Base rates, any present long-
124
+ context tier, the derived 1-hour cache-write rate and any Fast multiplier
125
+ must all be positive; an empty list is that invariant.
126
+ """
127
+ _lib_pricing = _pricing()
128
+ out = []
129
+ multiplier = snapshot.cache_write_1h_multiplier
130
+ fast = snapshot.fast_multipliers.get("claude") or {}
131
+ for model, card in sorted(snapshot.claude_pricing.items()):
132
+ for field in _CLAUDE_BASE_RATES:
133
+ if not _positive_rate(card.get(field)):
134
+ out.append((model, field))
135
+ for field in _CLAUDE_TIER_RATES:
136
+ if field in card and not _positive_rate(card[field]):
137
+ out.append((model, field))
138
+ if not (_positive_rate(multiplier)
139
+ and _positive_rate(card.get("input_cost_per_token"))):
140
+ out.append((model, "cache_write_1h"))
141
+ tier_input = card.get("input_cost_per_token_above_200k_tokens")
142
+ if tier_input is not None and not (
143
+ _positive_rate(multiplier) and _positive_rate(tier_input)
144
+ ):
145
+ out.append((model, "cache_write_1h_above_200k_tokens"))
146
+ stripped = _lib_pricing._strip_anthropic_model_prefix(model)
147
+ if stripped in fast and not _positive_rate(fast[stripped]):
148
+ out.append((model, "fast_multiplier"))
149
+ return out
150
+
151
+
152
+ def claude_entry_rates_positive(card, entry, snapshot) -> bool:
153
+ """True iff every rate that prices one of ``entry``'s tokens is positive.
154
+
155
+ Mirrors `_lib_pricing._calculate_entry_cost` term by term: each token class
156
+ is priced below the long-context threshold at its base rate and above it at
157
+ a present tier rate, and a known 1-hour cache-write split prices its share
158
+ at the derived input-times-multiplier rate.
159
+ """
160
+ if card is None:
161
+ return False
162
+ _lib_pricing = _pricing()
163
+ threshold = snapshot.tier_thresholds["claude"]
164
+
165
+ def tiered(tokens, base_key, tier_key):
166
+ if tokens <= 0:
167
+ return True
168
+ if not _positive_rate(card.get(base_key)):
169
+ return False
170
+ tier = card.get(tier_key)
171
+ if tokens > threshold and tier is not None:
172
+ return _positive_rate(tier)
173
+ return True
174
+
175
+ if not (
176
+ tiered(int(entry.input_tokens or 0), "input_cost_per_token",
177
+ "input_cost_per_token_above_200k_tokens")
178
+ and tiered(int(entry.output_tokens or 0), "output_cost_per_token",
179
+ "output_cost_per_token_above_200k_tokens")
180
+ and tiered(int(entry.cache_read_tokens or 0),
181
+ "cache_read_input_token_cost",
182
+ "cache_read_input_token_cost_above_200k_tokens")
183
+ ):
184
+ return False
185
+ flat = int(entry.cache_creation_tokens or 0)
186
+ if flat > 0:
187
+ raw_1h = getattr(entry, "cache_1h_tokens", None)
188
+ hour = 0 if raw_1h is None else max(0, min(int(raw_1h), flat))
189
+ if hour == 0:
190
+ if not tiered(flat, "cache_creation_input_token_cost",
191
+ "cache_creation_input_token_cost_above_200k_tokens"):
192
+ return False
193
+ else:
194
+ multiplier = snapshot.cache_write_1h_multiplier
195
+ base = card.get("input_cost_per_token", 0.0)
196
+ rate_5m = card.get("cache_creation_input_token_cost", 0.0)
197
+ below = min(flat, threshold)
198
+ above = max(0, flat - threshold)
199
+ terms = (
200
+ (below and hour, _positive_rate(multiplier)
201
+ and _positive_rate(base)),
202
+ (above and hour, _positive_rate(multiplier) and _positive_rate(
203
+ card.get("input_cost_per_token_above_200k_tokens", base))),
204
+ (below and flat - hour, _positive_rate(rate_5m)),
205
+ (above and flat - hour, _positive_rate(card.get(
206
+ "cache_creation_input_token_cost_above_200k_tokens",
207
+ rate_5m))),
208
+ )
209
+ if any(quantity and not positive for quantity, positive in terms):
210
+ return False
211
+ if getattr(entry, "speed", None) == "fast":
212
+ fast = (snapshot.fast_multipliers.get("claude") or {}).get(
213
+ _lib_pricing._strip_anthropic_model_prefix(entry.model), 1.0)
214
+ if not _positive_rate(fast):
215
+ return False
216
+ return True
217
+
218
+
219
+ def historical_close_marker(close_payload, owned_entries, *, unattributed):
220
+ """The correction marker for an unmarked zero-cost close, or None.
221
+
222
+ The close must carry no computation-time marker, total exactly $0, and have
223
+ every positive-token model child at exactly $0; one priced model makes the
224
+ whole close unprovable, because its project children have no model axis.
225
+ ``owned_entries`` are the retained entries the current ownership rule
226
+ assigns to this close. Each one must belong to the close's account, a
227
+ positive-token model's entries must carry no provider cost, and today's
228
+ card for that model must price every token class the entry used at a
229
+ positive rate. The caller still compares the replayed child populations.
230
+ """
231
+ if (
232
+ close_payload.get("_pricing")
233
+ or close_payload.get("pricing_provenance_json") is not None
234
+ or close_payload.get("is_closed") != 1
235
+ or not _zero_amount(close_payload.get("total_cost_usd"))
236
+ or owned_entries is None
237
+ ):
238
+ return None
239
+ inferred = set()
240
+ for child in close_payload.get("_models") or []:
241
+ if any(int(child.get(key) or 0) > 0 for key in _BLOCK_TOKEN_KEYS):
242
+ model = child.get("model")
243
+ if not isinstance(model, str) or not model:
244
+ return None
245
+ if not _zero_amount(child.get("cost_usd")):
246
+ return None
247
+ inferred.add(model)
248
+ if not inferred or any(
249
+ not _zero_amount(child.get("cost_usd"))
250
+ for child in close_payload.get("_projects") or []
251
+ ):
252
+ return None
253
+ account = close_payload.get("account_key") or unattributed
254
+ _lib_pricing = _pricing()
255
+ snapshot = _lib_pricing.current_pricing_snapshot()
256
+ source = []
257
+ priced_models = set()
258
+ for entry in owned_entries:
259
+ offset = getattr(entry, "source_line_offset", None)
260
+ if (getattr(entry, "source_account_key", None) or unattributed) != account:
261
+ return None
262
+ if offset is None:
263
+ return None
264
+ positive = any((
265
+ entry.input_tokens, entry.output_tokens,
266
+ entry.cache_creation_tokens, entry.cache_read_tokens,
267
+ ))
268
+ if entry.model in inferred:
269
+ if entry.cost_usd is not None:
270
+ return None
271
+ if positive and not claude_entry_rates_positive(
272
+ _lib_pricing._resolve_model_pricing(entry.model, warn=False),
273
+ entry, snapshot,
274
+ ):
275
+ return None
276
+ if positive:
277
+ priced_models.add(entry.model)
278
+ elif positive:
279
+ return None
280
+ source.append([
281
+ entry.timestamp.astimezone(dt.timezone.utc).isoformat(),
282
+ entry.source_path, int(offset), entry.model,
283
+ int(entry.input_tokens or 0), int(entry.output_tokens or 0),
284
+ int(entry.cache_creation_tokens or 0),
285
+ getattr(entry, "cache_1h_tokens", None),
286
+ int(entry.cache_read_tokens or 0),
287
+ getattr(entry, "speed", None),
288
+ ])
289
+ if priced_models != inferred:
290
+ return None
291
+ encoded = json.dumps(
292
+ sorted(source, key=lambda item: json.dumps(item)),
293
+ sort_keys=True, separators=(",", ":"), ensure_ascii=False,
294
+ ).encode("utf-8")
295
+ return {
296
+ "version": 1,
297
+ "basis": HISTORICAL_CLOSE_BASIS,
298
+ "inferredModels": sorted(inferred),
299
+ "pricingDate": snapshot.snapshot_date,
300
+ "sourceHash": "sha256:" + hashlib.sha256(encoded).hexdigest(),
301
+ "entryCount": len(source),
302
+ "unpricedModels": [],
303
+ }