cctally 1.46.0 → 1.47.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 CHANGED
@@ -5,6 +5,11 @@ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [1.47.0] - 2026-06-16
9
+
10
+ ### Added
11
+ - **Dashboard:** the Recent Sessions modal now shows cache-rebuild events for the selected conversation — rebuild count, wasted $, tokens re-created, a session cache-value-saved figure, and a worst-first list of jump-links that open the conversation viewer at the message that triggered each rebuild (respects `dashboard.cache_failure_markers`).
12
+
8
13
  ## [1.46.0] - 2026-06-15
9
14
 
10
15
  ### Added
@@ -313,6 +313,18 @@ def _cache_failure_wasted_usd(model, lost):
313
313
  return write - read
314
314
 
315
315
 
316
+ def _cache_read_saved_usd(model, cache_read):
317
+ """Marginal USD the cache SAVED this turn: the `cache_read` prefix priced at
318
+ the full input rate minus its actual cache-READ rate. Display-only (same
319
+ caveat as `_cache_failure_wasted_usd`): NEVER summed into a cost-snapshot /
320
+ budget / reconciled figure. `input_tokens` and `cache_read_input_tokens` are
321
+ independent keys in the Claude `_calculate_entry_cost` (no subset
322
+ subtraction), so passing each alone yields the two rates cleanly."""
323
+ full = _calculate_entry_cost(model or "", {"input_tokens": cache_read})
324
+ read = _calculate_entry_cost(model or "", {"cache_read_input_tokens": cache_read})
325
+ return full - read
326
+
327
+
316
328
  def _stamp_cache_failures(items):
317
329
  """Stamp ``item["cache_failure"]`` on each assistant turn that re-creates the
318
330
  bulk of its cached prefix instead of reading it (spec §1). Mutates `items` in
@@ -1120,6 +1132,8 @@ def get_conversation_outline(conn, session_id):
1120
1132
  cf_count = 0
1121
1133
  cf_tokens = 0
1122
1134
  cf_wasted = 0.0
1135
+ cf_rebuilds = [] # per-rebuild list (worst-first), spec §1
1136
+ cache_saved = 0.0 # session cache-value-saved, spec §1
1123
1137
  for it in items:
1124
1138
  kind = it["kind"]
1125
1139
  turn_counts["total"] += 1
@@ -1161,6 +1175,9 @@ def get_conversation_outline(conn, session_id):
1161
1175
  t["tokens"] = tok
1162
1176
  for k in tokens:
1163
1177
  tokens[k] += tok.get(k, 0)
1178
+ cr_tokens = tok.get("cache_read", 0) or 0
1179
+ if cr_tokens > 0:
1180
+ cache_saved += _cache_read_saved_usd(it.get("model"), cr_tokens)
1164
1181
  # Copy the cache-failure marker onto the OutlineTurn exactly where
1165
1182
  # tokens is copied (assistant-only, rides the same source row) and
1166
1183
  # accumulate the session-level aggregate (spec §2).
@@ -1170,6 +1187,13 @@ def get_conversation_outline(conn, session_id):
1170
1187
  cf_count += 1
1171
1188
  cf_tokens += cf.get("tokens_recreated", 0)
1172
1189
  cf_wasted += cf.get("est_wasted_usd", 0.0)
1190
+ cf_rebuilds.append({
1191
+ "uuid": t["uuid"],
1192
+ "subagent_key": t["subagent_key"],
1193
+ "ts": t["ts"],
1194
+ "tokens_recreated": cf.get("tokens_recreated", 0),
1195
+ "est_wasted_usd": cf.get("est_wasted_usd", 0.0),
1196
+ })
1173
1197
  if kind == "meta":
1174
1198
  t["meta_kind"] = it.get("meta_kind")
1175
1199
  t["skill_name"] = it.get("skill_name")
@@ -1185,9 +1209,16 @@ def get_conversation_outline(conn, session_id):
1185
1209
  "duration_seconds": duration, "tokens": tokens,
1186
1210
  "cost_usd": asm["header_cost"]}
1187
1211
  if cf_count:
1188
- stats["cache_failures"] = {"count": cf_count,
1189
- "tokens_recreated": cf_tokens,
1190
- "est_wasted_usd": cf_wasted}
1212
+ stats["cache_failures"] = {
1213
+ "count": cf_count,
1214
+ "tokens_recreated": cf_tokens,
1215
+ "est_wasted_usd": cf_wasted,
1216
+ "rebuilds": sorted(
1217
+ cf_rebuilds,
1218
+ key=lambda r: (-r["est_wasted_usd"], r["ts"] is None, r["ts"] or ""),
1219
+ ),
1220
+ }
1221
+ stats["cache_saved_usd"] = cache_saved
1191
1222
  return {"session_id": session_id,
1192
1223
  "subagent_meta": asm["subagent_meta"],
1193
1224
  "stats": stats,