cctally 1.82.1 → 1.83.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 (51) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +12 -5
  3. package/bin/_cctally_alerts.py +8 -1
  4. package/bin/_cctally_cache.py +912 -149
  5. package/bin/_cctally_config.py +43 -4
  6. package/bin/_cctally_core.py +933 -759
  7. package/bin/_cctally_dashboard.py +157 -47
  8. package/bin/_cctally_dashboard_cache_report.py +13 -6
  9. package/bin/_cctally_dashboard_conversation.py +1 -0
  10. package/bin/_cctally_dashboard_envelope.py +116 -8
  11. package/bin/_cctally_dashboard_share.py +50 -19
  12. package/bin/_cctally_dashboard_sources.py +223 -48
  13. package/bin/_cctally_db.py +605 -128
  14. package/bin/_cctally_doctor.py +413 -28
  15. package/bin/_cctally_five_hour.py +12 -5
  16. package/bin/_cctally_journal.py +2050 -156
  17. package/bin/_cctally_journal_repair.py +519 -0
  18. package/bin/_cctally_milestone_history.py +142 -56
  19. package/bin/_cctally_milestones.py +179 -111
  20. package/bin/_cctally_parser.py +42 -0
  21. package/bin/_cctally_project.py +24 -18
  22. package/bin/_cctally_quota.py +139 -25
  23. package/bin/_cctally_record.py +279 -108
  24. package/bin/_cctally_rederive.py +1052 -0
  25. package/bin/_cctally_reporting.py +58 -53
  26. package/bin/_cctally_setup.py +1 -0
  27. package/bin/_cctally_source_analytics.py +4 -1
  28. package/bin/_cctally_statusline.py +11 -11
  29. package/bin/_cctally_store.py +1039 -31
  30. package/bin/_cctally_sync_week.py +17 -8
  31. package/bin/_cctally_tui.py +350 -44
  32. package/bin/_cctally_update.py +133 -8
  33. package/bin/_cctally_weekrefs.py +14 -0
  34. package/bin/_lib_aggregators.py +10 -6
  35. package/bin/_lib_cache_report.py +101 -9
  36. package/bin/_lib_codex_pools.py +82 -0
  37. package/bin/_lib_conversation_query.py +81 -33
  38. package/bin/_lib_dashboard_sources.py +75 -0
  39. package/bin/_lib_diff_kernel.py +28 -15
  40. package/bin/_lib_doctor.py +342 -4
  41. package/bin/_lib_journal.py +924 -2
  42. package/bin/_lib_jsonl.py +43 -14
  43. package/bin/_lib_pricing.py +140 -21
  44. package/bin/_lib_rederive.py +395 -0
  45. package/bin/_lib_share.py +58 -2
  46. package/bin/cctally +56 -8
  47. package/dashboard/static/assets/{index-BKM43pxK.js → index-3bgCMVHb.js} +52 -52
  48. package/dashboard/static/assets/index-D27EIHEI.css +1 -0
  49. package/dashboard/static/dashboard.html +2 -2
  50. package/package.json +5 -1
  51. package/dashboard/static/assets/index-Dk1nplOz.css +0 -1
@@ -203,7 +203,7 @@ from _cctally_core import (
203
203
  )
204
204
  import _lib_accounts # pure stdlib kernel; UNATTRIBUTED sentinel default (#341)
205
205
  from _lib_five_hour import _canonical_5h_window_key, five_hour_milestone_range
206
- from _lib_pricing import _calculate_entry_cost
206
+ from _lib_pricing import _calculate_entry_cost, claude_usage_dict
207
207
  from _lib_codex_hooks import (
208
208
  CODEX_HOOK_THROTTLE_SECONDS,
209
209
  acquire_due_lifecycle_locks,
@@ -609,6 +609,7 @@ def maybe_record_milestone(
609
609
  alert_sink: "list | None" = None,
610
610
  journal: "tuple | None" = None,
611
611
  account_key: str = _lib_accounts.UNATTRIBUTED,
612
+ retained_selection=None,
612
613
  ) -> None:
613
614
  """Check if a new integer percent threshold was crossed, and if so,
614
615
  fetch cost and record the milestone. Errors are logged, not raised.
@@ -634,6 +635,11 @@ def maybe_record_milestone(
634
635
  rebuild. ``None`` keeps today's compute-and-drop behavior on the passed-conn
635
636
  path.
636
637
 
638
+ ``retained_selection`` (#410 Task A) carries the triggering raw
639
+ observation's canonical subscription window. It is deliberately separate
640
+ from ``saved``: a dedup/self-heal fold may reuse a later physical usage row,
641
+ but that must never retarget the triggering observation's ``wcs:`` event.
642
+
637
643
  ``journal`` (DB journal redesign Task 6, Design A): the ``(ctx, id_base)``
638
644
  tuple threaded into the milestone's pre-record cost sync so the
639
645
  ``weekly_cost_snapshots`` row it inserts rides a Model-A
@@ -697,6 +703,13 @@ def maybe_record_milestone(
697
703
  # Threshold crossed — sync cost before recording so the milestone
698
704
  # captures up-to-date cumulative cost, not a stale snapshot.
699
705
  try:
706
+ if retained_selection is None:
707
+ retained_selection = _cctally().WeekSelection(
708
+ week_start=dt.date.fromisoformat(week_start_date),
709
+ week_end=dt.date.fromisoformat(week_end_date),
710
+ start_iso_override=week_start_at,
711
+ end_iso_override=week_end_at,
712
+ )
700
713
  sync_ns = argparse.Namespace(
701
714
  week_start=None,
702
715
  week_end=None,
@@ -721,6 +734,10 @@ def maybe_record_milestone(
721
734
  # Materialize the cost snapshot under the crossing's account
722
735
  # (#341 P2-1) so the account-scoped read below finds it.
723
736
  account_key=account_key,
737
+ # #410 Task A: the implicit milestone sync is a derivation of
738
+ # THIS retained observation. Never ask `pick_week_selection`
739
+ # for the latest stats row, which may carry a later anchor.
740
+ retained_selection=retained_selection,
724
741
  )
725
742
  except Exception as exc:
726
743
  eprint(f"[milestone] cost sync failed, using latest available: {exc}")
@@ -745,7 +762,11 @@ def maybe_record_milestone(
745
762
  effective_ref = adjusted[0]
746
763
 
747
764
  if _week_ref_has_reset_event(conn, effective_ref):
748
- live_cost = _compute_cost_for_weekref(effective_ref)
765
+ live_cost = _compute_cost_for_weekref(
766
+ effective_ref,
767
+ account_key=account_key,
768
+ as_of=as_of,
769
+ )
749
770
  if live_cost is None:
750
771
  eprint("[milestone] could not compute effective-range cost, skipping")
751
772
  return
@@ -1865,12 +1886,14 @@ def _compute_block_totals(
1865
1886
  for entry in get_claude_session_entries(
1866
1887
  block_start_at, range_end, skip_sync=skip_sync,
1867
1888
  ):
1868
- usage = {
1869
- "input_tokens": entry.input_tokens,
1870
- "output_tokens": entry.output_tokens,
1871
- "cache_creation_input_tokens": entry.cache_creation_tokens,
1872
- "cache_read_input_tokens": entry.cache_read_tokens,
1873
- }
1889
+ usage = claude_usage_dict( # #195 chokepoint
1890
+ input_tokens=entry.input_tokens,
1891
+ output_tokens=entry.output_tokens,
1892
+ cache_creation_tokens=entry.cache_creation_tokens,
1893
+ cache_read_tokens=entry.cache_read_tokens,
1894
+ cache_1h_tokens=getattr(entry, "cache_1h_tokens", None),
1895
+ speed=getattr(entry, "speed", None),
1896
+ )
1874
1897
  cost = _calculate_entry_cost(
1875
1898
  entry.model, usage, mode="auto", cost_usd=entry.cost_usd,
1876
1899
  )
@@ -1914,6 +1937,7 @@ def _compute_block_totals(
1914
1937
  def maybe_update_five_hour_block(
1915
1938
  saved: dict[str, Any], *, conn=None, as_of: "str | None" = None,
1916
1939
  alert_sink: "list | None" = None, account_key: str = _lib_accounts.UNATTRIBUTED,
1940
+ journal_ctx=None,
1917
1941
  ) -> None:
1918
1942
  """Upsert the current 5h block in five_hour_blocks; close strictly
1919
1943
  older open blocks; sweep naturally-expired blocks; flag blocks
@@ -1935,7 +1959,12 @@ def maybe_update_five_hour_block(
1935
1959
  ``BEGIN IMMEDIATE``/``commit()``/``close()``, and alert dispatch is left to
1936
1960
  the caller (the ingester's ALERT_DISPATCHER). ``as_of`` (ISO-Z) is stamped
1937
1961
  as ``last_updated_at_utc`` / ``alerted_at`` in place of wall clock. Both
1938
- defaults keep the legacy own-connection, own-transaction behavior."""
1962
+ defaults keep the legacy own-connection, own-transaction behavior.
1963
+
1964
+ ``journal_ctx`` makes a close durable at the transition itself. The complete
1965
+ parent + model/project child sets are frozen before the transaction can
1966
+ commit; an already-stamped closed block is immutable to later observations
1967
+ and mutable cache growth."""
1939
1968
  five_hour_percent = saved.get("fiveHourPercent")
1940
1969
  five_hour_resets_at = saved.get("fiveHourResetsAt")
1941
1970
  five_hour_window_key = saved.get("fiveHourWindowKey")
@@ -1969,7 +1998,10 @@ def maybe_update_five_hour_block(
1969
1998
  prior = conn.execute(
1970
1999
  """
1971
2000
  SELECT id AS prior_block_id,
1972
- block_start_at AS block_start_at
2001
+ block_start_at AS block_start_at,
2002
+ is_closed AS is_closed,
2003
+ journal_id AS journal_id,
2004
+ last_updated_at_utc AS last_updated_at_utc
1973
2005
  FROM five_hour_blocks
1974
2006
  WHERE five_hour_window_key = ?
1975
2007
  AND account_key = ?
@@ -2006,6 +2038,11 @@ def maybe_update_five_hour_block(
2006
2038
  block_start_dt = parse_iso_datetime(
2007
2039
  block_start_at, "five_hour_blocks.block_start_at",
2008
2040
  )
2041
+ current_is_frozen = (
2042
+ prior is not None
2043
+ and int(prior["is_closed"]) == 1
2044
+ and prior["journal_id"] is not None
2045
+ )
2009
2046
 
2010
2047
  # Step 6 (totals) — done outside the transaction so the
2011
2048
  # cache.db read doesn't hold the stats.db write lock open.
@@ -2056,6 +2093,44 @@ def maybe_update_five_hour_block(
2056
2093
  if own_conn:
2057
2094
  conn.execute("BEGIN IMMEDIATE")
2058
2095
  try:
2096
+ # Capture the exact transition set before mutating it. A close is
2097
+ # triggered either by a retained successor-window observation or
2098
+ # by the first retained observation captured after natural expiry.
2099
+ # Both rules use ``now_iso`` (the observation's capture clock),
2100
+ # never ingest/retry wall time.
2101
+ closing_ids = {
2102
+ int(row["id"])
2103
+ for row in conn.execute(
2104
+ "SELECT id FROM five_hour_blocks "
2105
+ "WHERE is_closed = 0 AND account_key = ? "
2106
+ "AND (five_hour_window_key < ? "
2107
+ " OR unixepoch(five_hour_resets_at) < unixepoch(?)) "
2108
+ "ORDER BY id",
2109
+ (account_key, int(five_hour_window_key), now_iso),
2110
+ ).fetchall()
2111
+ }
2112
+ # A lost-commit retry replays the frozen event before reprocessing
2113
+ # its trigger observation. Re-emit that exact duplicate only when
2114
+ # this retained trigger clock matches the frozen closure clock;
2115
+ # later ordinary observations do not churn duplicate lines.
2116
+ retry_close_ids = {
2117
+ int(row["id"])
2118
+ for row in conn.execute(
2119
+ "SELECT id FROM five_hour_blocks "
2120
+ "WHERE is_closed = 1 AND journal_id IS NOT NULL "
2121
+ "AND account_key = ? AND last_updated_at_utc = ? "
2122
+ "AND (five_hour_window_key < ? "
2123
+ " OR unixepoch(five_hour_resets_at) < unixepoch(?)) "
2124
+ "ORDER BY id",
2125
+ (
2126
+ account_key,
2127
+ now_iso,
2128
+ int(five_hour_window_key),
2129
+ now_iso,
2130
+ ),
2131
+ ).fetchall()
2132
+ }
2133
+
2059
2134
  # Step 5: close any STRICTLY OLDER open block. `<` not `!=`
2060
2135
  # — record-usage runs in parallel via background hook-tick &
2061
2136
  # detach + status-line ticks; an older invocation completing
@@ -2078,15 +2153,15 @@ def maybe_update_five_hour_block(
2078
2153
  # who lets a block expire without a successor (idle / shut down
2079
2154
  # past the 5h reset) would otherwise leave the row at
2080
2155
  # is_closed = 0 forever. Idempotent (only flips 0 → 1); safe to
2081
- # re-run every tick. ISO-string compare is monotonic so it
2082
- # works directly on five_hour_resets_at.
2156
+ # re-run every tick. Normalize through unixepoch() because retained
2157
+ # reset/capture stamps may use different equivalent UTC suffixes.
2083
2158
  conn.execute(
2084
2159
  """
2085
2160
  UPDATE five_hour_blocks
2086
2161
  SET is_closed = 1, last_updated_at_utc = ?
2087
2162
  WHERE is_closed = 0
2088
2163
  AND account_key = ?
2089
- AND five_hour_resets_at < ?
2164
+ AND unixepoch(five_hour_resets_at) < unixepoch(?)
2090
2165
  """,
2091
2166
  (now_iso, account_key, now_iso),
2092
2167
  )
@@ -2136,6 +2211,8 @@ def maybe_update_five_hour_block(
2136
2211
  total_cache_read_tokens = excluded.total_cache_read_tokens,
2137
2212
  total_cost_usd = excluded.total_cost_usd,
2138
2213
  last_updated_at_utc = excluded.last_updated_at_utc
2214
+ WHERE five_hour_blocks.is_closed = 0
2215
+ OR five_hour_blocks.journal_id IS NULL
2139
2216
  """,
2140
2217
  (
2141
2218
  int(five_hour_window_key),
@@ -2176,12 +2253,13 @@ def maybe_update_five_hour_block(
2176
2253
  # a strictly-newer historical block stays open (scenario D), and on
2177
2254
  # `is_closed = 0` so it is idempotent + a no-op for a legacy own-conn
2178
2255
  # caller whose backfill already closed the historical block. `<`
2179
- # ISO-string-compares resets_at against now_iso, same as the sweep.
2256
+ # instant-compares resets_at against now_iso, same as the sweep.
2180
2257
  if blocks_were_empty:
2181
2258
  conn.execute(
2182
2259
  "UPDATE five_hour_blocks SET is_closed = 1, last_updated_at_utc = ? "
2183
2260
  "WHERE five_hour_window_key = ? AND account_key = ? "
2184
- " AND is_closed = 0 AND five_hour_resets_at < ?",
2261
+ " AND is_closed = 0 "
2262
+ " AND unixepoch(five_hour_resets_at) < unixepoch(?)",
2185
2263
  (now_iso, int(five_hour_window_key), account_key, now_iso),
2186
2264
  )
2187
2265
 
@@ -2190,71 +2268,83 @@ def maybe_update_five_hour_block(
2190
2268
  # orphan child rows from a prior parent rebuild are cleaned up automatically.
2191
2269
  # Same transaction as the parent upsert; if these raise, the whole tick
2192
2270
  # rolls back and the next tick recomputes from scratch.
2193
- conn.execute(
2194
- "DELETE FROM five_hour_block_models "
2195
- "WHERE five_hour_window_key = ? AND account_key = ?",
2196
- (int(five_hour_window_key), account_key),
2197
- )
2198
- if totals.get("by_model"):
2199
- conn.executemany(
2200
- """
2201
- INSERT INTO five_hour_block_models (
2202
- block_id, five_hour_window_key, model,
2203
- input_tokens, output_tokens,
2204
- cache_create_tokens, cache_read_tokens,
2205
- cost_usd, entry_count, account_key
2206
- )
2207
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2208
- """,
2209
- [
2210
- (
2211
- block_id,
2212
- int(five_hour_window_key),
2213
- model,
2214
- b["input_tokens"],
2215
- b["output_tokens"],
2216
- b["cache_create_tokens"],
2217
- b["cache_read_tokens"],
2218
- b["cost_usd"],
2219
- b["entry_count"],
2220
- account_key,
2221
- )
2222
- for model, b in totals["by_model"].items()
2223
- ],
2271
+ if not current_is_frozen:
2272
+ conn.execute(
2273
+ "DELETE FROM five_hour_block_models "
2274
+ "WHERE five_hour_window_key = ? AND account_key = ?",
2275
+ (int(five_hour_window_key), account_key),
2224
2276
  )
2225
-
2226
- conn.execute(
2227
- "DELETE FROM five_hour_block_projects "
2228
- "WHERE five_hour_window_key = ? AND account_key = ?",
2229
- (int(five_hour_window_key), account_key),
2230
- )
2231
- if totals.get("by_project"):
2232
- conn.executemany(
2233
- """
2234
- INSERT INTO five_hour_block_projects (
2235
- block_id, five_hour_window_key, project_path,
2236
- input_tokens, output_tokens,
2237
- cache_create_tokens, cache_read_tokens,
2238
- cost_usd, entry_count, account_key
2239
- )
2240
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2241
- """,
2242
- [
2243
- (
2244
- block_id,
2245
- int(five_hour_window_key),
2246
- project_path,
2247
- b["input_tokens"],
2248
- b["output_tokens"],
2249
- b["cache_create_tokens"],
2250
- b["cache_read_tokens"],
2251
- b["cost_usd"],
2252
- b["entry_count"],
2253
- account_key,
2277
+ if totals.get("by_model"):
2278
+ conn.executemany(
2279
+ """
2280
+ INSERT INTO five_hour_block_models (
2281
+ block_id, five_hour_window_key, model,
2282
+ input_tokens, output_tokens,
2283
+ cache_create_tokens, cache_read_tokens,
2284
+ cost_usd, entry_count, account_key
2254
2285
  )
2255
- for project_path, b in totals["by_project"].items()
2256
- ],
2286
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2287
+ """,
2288
+ [
2289
+ (
2290
+ block_id,
2291
+ int(five_hour_window_key),
2292
+ model,
2293
+ b["input_tokens"],
2294
+ b["output_tokens"],
2295
+ b["cache_create_tokens"],
2296
+ b["cache_read_tokens"],
2297
+ b["cost_usd"],
2298
+ b["entry_count"],
2299
+ account_key,
2300
+ )
2301
+ for model, b in totals["by_model"].items()
2302
+ ],
2303
+ )
2304
+
2305
+ conn.execute(
2306
+ "DELETE FROM five_hour_block_projects "
2307
+ "WHERE five_hour_window_key = ? AND account_key = ?",
2308
+ (int(five_hour_window_key), account_key),
2257
2309
  )
2310
+ if totals.get("by_project"):
2311
+ conn.executemany(
2312
+ """
2313
+ INSERT INTO five_hour_block_projects (
2314
+ block_id, five_hour_window_key, project_path,
2315
+ input_tokens, output_tokens,
2316
+ cache_create_tokens, cache_read_tokens,
2317
+ cost_usd, entry_count, account_key
2318
+ )
2319
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2320
+ """,
2321
+ [
2322
+ (
2323
+ block_id,
2324
+ int(five_hour_window_key),
2325
+ project_path,
2326
+ b["input_tokens"],
2327
+ b["output_tokens"],
2328
+ b["cache_create_tokens"],
2329
+ b["cache_read_tokens"],
2330
+ b["cost_usd"],
2331
+ b["entry_count"],
2332
+ account_key,
2333
+ )
2334
+ for project_path, b in totals["by_project"].items()
2335
+ ],
2336
+ )
2337
+
2338
+ # The first historical block is inserted after the sweep above, so
2339
+ # include it explicitly when this observation closes it.
2340
+ if blocks_were_empty:
2341
+ closed_now = conn.execute(
2342
+ "SELECT id FROM five_hour_blocks "
2343
+ "WHERE id = ? AND is_closed = 1 AND journal_id IS NULL",
2344
+ (block_id,),
2345
+ ).fetchone()
2346
+ if closed_now is not None:
2347
+ closing_ids.add(int(closed_now["id"]))
2258
2348
 
2259
2349
  # ── 5h-% milestone detection (mirrors maybe_record_milestone) ──
2260
2350
  # Snap-up-by-1e-9 per the gotcha: 0.50 * 100 == 49.99...9 in
@@ -2503,6 +2593,7 @@ def maybe_update_five_hour_block(
2503
2593
  SET crossed_seven_day_reset = 1
2504
2594
  WHERE crossed_seven_day_reset = 0
2505
2595
  AND account_key = ?
2596
+ AND (is_closed = 0 OR journal_id IS NULL)
2506
2597
  AND (
2507
2598
  EXISTS (
2508
2599
  SELECT 1 FROM week_reset_events e
@@ -2525,6 +2616,19 @@ def maybe_update_five_hour_block(
2525
2616
  (account_key,),
2526
2617
  )
2527
2618
 
2619
+ # Freeze only after every parent field and both rollup child sets
2620
+ # have reached their final close-state values for this observation.
2621
+ # The cross-reset sweep above is part of the parent fact, so moving
2622
+ # this earlier would journal ``crossed_seven_day_reset = 0`` and
2623
+ # then mutate the live closed row to 1.
2624
+ if journal_ctx is not None:
2625
+ import _cctally_journal as _jr
2626
+
2627
+ for closing_id in sorted(closing_ids):
2628
+ _jr.freeze_five_hour_block_close(journal_ctx, closing_id)
2629
+ for retry_id in sorted(retry_close_ids - closing_ids):
2630
+ _jr.freeze_five_hour_block_close(journal_ctx, retry_id)
2631
+
2528
2632
  if own_conn:
2529
2633
  conn.commit()
2530
2634
  except Exception:
@@ -2636,6 +2740,38 @@ def _read_reset_zero_marker():
2636
2740
  return (week_start_date, cur_end_canon, baseline_pct, first_zero_iso)
2637
2741
 
2638
2742
 
2743
+ def _projection_read_reset_zero_marker(ctx):
2744
+ if ctx is not None and not ctx.projection_writes:
2745
+ return ctx.projection_state.get("reset_zero_marker")
2746
+ return _read_reset_zero_marker()
2747
+
2748
+
2749
+ def _projection_clear_reset_zero_marker(ctx):
2750
+ if ctx is not None and not ctx.projection_writes:
2751
+ ctx.projection_state.pop("reset_zero_marker", None)
2752
+ return
2753
+ _clear_reset_zero_marker()
2754
+
2755
+
2756
+ def _projection_arm_reset_zero_marker(
2757
+ ctx, week_start_date, cur_end_canon, *, baseline_pct, first_zero_iso,
2758
+ ):
2759
+ if ctx is not None and not ctx.projection_writes:
2760
+ ctx.projection_state["reset_zero_marker"] = (
2761
+ week_start_date,
2762
+ cur_end_canon,
2763
+ float(baseline_pct),
2764
+ first_zero_iso,
2765
+ )
2766
+ return
2767
+ _arm_reset_zero_marker(
2768
+ week_start_date,
2769
+ cur_end_canon,
2770
+ baseline_pct=baseline_pct,
2771
+ first_zero_iso=first_zero_iso,
2772
+ )
2773
+
2774
+
2639
2775
  # ``CreditPlan`` / ``_parse_credit_at`` / ``_build_credit_plan`` now live in
2640
2776
  # ``bin/_lib_credit.py`` (#279 S4 F1); re-imported at module top so the
2641
2777
  # ``bin/cctally`` re-exports and this module's own callers
@@ -2727,12 +2863,13 @@ def _fire_in_place_credit(conn, week_start_date, cur_end_canon, weekly_percent,
2727
2863
  # Unconditional pivot 1: force-write hwm-7d so the next status-line render
2728
2864
  # reflects the post-credit value (the monotonic guard at the normal write
2729
2865
  # site would refuse to decrease the file).
2730
- try:
2731
- (_cctally_core.APP_DIR / "hwm-7d").write_text(
2732
- f"{week_start_date} {weekly_percent}\n"
2733
- )
2734
- except OSError:
2735
- pass
2866
+ if ctx is None or ctx.projection_writes:
2867
+ try:
2868
+ (_cctally_core.APP_DIR / "hwm-7d").write_text(
2869
+ f"{week_start_date} {weekly_percent}\n"
2870
+ )
2871
+ except OSError:
2872
+ pass
2736
2873
  # Unconditional pivot 2: race-defensive cleanup of stale pre-credit replays
2737
2874
  # (external claude-statusline can replay pre-credit --percent values that
2738
2875
  # land captured_at >= effective with pct ~= baseline and dominate the
@@ -2812,8 +2949,9 @@ def detect_reset_and_credit(conn, *, week_start_date, week_end_at,
2812
2949
  cur_end_canon = _canonicalize_optional_iso(week_end_at, "record.cur")
2813
2950
  prior = conn.execute(
2814
2951
  "SELECT week_end_at, weekly_percent FROM weekly_usage_snapshots "
2815
- "WHERE week_end_at IS NOT NULL "
2816
- "ORDER BY captured_at_utc DESC, id DESC LIMIT 1"
2952
+ "WHERE week_end_at IS NOT NULL AND account_key = ? "
2953
+ "ORDER BY captured_at_utc DESC, id DESC LIMIT 1",
2954
+ (account_key,),
2817
2955
  ).fetchone()
2818
2956
  if prior and prior["week_end_at"] and cur_end_canon:
2819
2957
  prior_end_canon = _canonicalize_optional_iso(
@@ -2872,7 +3010,7 @@ def detect_reset_and_credit(conn, *, week_start_date, week_end_at,
2872
3010
  # then the glue below executes the decided I/O. The 5
2873
3011
  # branch outcomes (fire-immediate / confirm / clear /
2874
3012
  # arm / none) map 1:1 to the pre-extraction structure.
2875
- marker = _read_reset_zero_marker()
3013
+ marker = _projection_read_reset_zero_marker(ctx)
2876
3014
  armed = (
2877
3015
  marker is not None
2878
3016
  and marker[0] == week_start_date
@@ -2889,7 +3027,7 @@ def detect_reset_and_credit(conn, *, week_start_date, week_end_at,
2889
3027
  if decision == FIRE_IMMEDIATE:
2890
3028
  # >=25pp goodwill credit — fire immediately, never
2891
3029
  # debounced. Clear any pending arm (now moot).
2892
- _clear_reset_zero_marker()
3030
+ _projection_clear_reset_zero_marker(ctx)
2893
3031
  _fire_in_place_credit(
2894
3032
  conn, week_start_date, cur_end_canon, weekly_percent,
2895
3033
  observed_pre_credit_pct=float(prior_pct),
@@ -2915,11 +3053,11 @@ def detect_reset_and_credit(conn, *, week_start_date, week_end_at,
2915
3053
  # Clear ONLY after the fire completes (P2a): a
2916
3054
  # mid-fire crash leaves the marker armed so the next
2917
3055
  # zero re-confirms + re-runs the idempotent pivots.
2918
- _clear_reset_zero_marker()
3056
+ _projection_clear_reset_zero_marker(ctx)
2919
3057
  elif decision == CLEAR_MARKER:
2920
3058
  # Recovered toward baseline → transient zero, not a
2921
3059
  # reset. Clear, do not fire.
2922
- _clear_reset_zero_marker()
3060
+ _projection_clear_reset_zero_marker(ctx)
2923
3061
  elif decision == ARM_MARKER:
2924
3062
  # First ~0 → arm; do NOT fire. The write clamp
2925
3063
  # suppresses this 0 (no event row yet), so the prior
@@ -2927,7 +3065,8 @@ def detect_reset_and_credit(conn, *, week_start_date, week_end_at,
2927
3065
  # re-evaluates next tick. first_zero_iso is the
2928
3066
  # _command_as_of() value (now_utc), NOT wall-clock —
2929
3067
  # it becomes the effective anchor.
2930
- _arm_reset_zero_marker(
3068
+ _projection_arm_reset_zero_marker(
3069
+ ctx,
2931
3070
  week_start_date, cur_end_canon,
2932
3071
  baseline_pct=float(prior_pct),
2933
3072
  first_zero_iso=now_utc.isoformat(timespec="seconds"),
@@ -2973,7 +3112,9 @@ def detect_reset_and_credit(conn, *, week_start_date, week_end_at,
2973
3112
  " FROM weekly_usage_snapshots "
2974
3113
  " WHERE five_hour_window_key IS NOT NULL "
2975
3114
  " AND five_hour_percent IS NOT NULL "
2976
- " ORDER BY captured_at_utc DESC, id DESC LIMIT 1"
3115
+ " AND account_key = ? "
3116
+ " ORDER BY captured_at_utc DESC, id DESC LIMIT 1",
3117
+ (account_key,),
2977
3118
  ).fetchone()
2978
3119
  if (
2979
3120
  prior_5h_row is not None
@@ -3136,13 +3277,14 @@ def detect_reset_and_credit(conn, *, week_start_date, week_end_at,
3136
3277
  # event row is durable. File format
3137
3278
  # matches the canonical writer:
3138
3279
  # ``<key> <percent>\n``.
3139
- try:
3140
- (_cctally_core.APP_DIR / "hwm-5h").write_text(
3141
- f"{int(five_hour_window_key)} "
3142
- f"{float(five_hour_percent)}\n"
3143
- )
3144
- except OSError:
3145
- pass
3280
+ if ctx is None or ctx.projection_writes:
3281
+ try:
3282
+ (_cctally_core.APP_DIR / "hwm-5h").write_text(
3283
+ f"{int(five_hour_window_key)} "
3284
+ f"{float(five_hour_percent)}\n"
3285
+ )
3286
+ except OSError:
3287
+ pass
3146
3288
  # Stale-replica DELETE (spec §4.3).
3147
3289
  # Defends against claude-statusline
3148
3290
  # replaying the pre-credit
@@ -3548,7 +3690,7 @@ def _apply_credit(conn, plan, *, five_hour=(None, None, None), as_of=None,
3548
3690
 
3549
3691
  # 4e. Clear a stale same-week reset-zero marker so the next record-usage
3550
3692
  # tick can't confirm a phantom reset-to-zero off it.
3551
- _clear_reset_zero_marker()
3693
+ _projection_clear_reset_zero_marker(ctx)
3552
3694
 
3553
3695
 
3554
3696
  def _count_stale_replays(conn, plan):
@@ -4094,6 +4236,11 @@ def cmd_record_usage(
4094
4236
  # Build the RAW observation (spec 4.2 / 5.3 -- raw capture only, NO derived
4095
4237
  # week columns; `_pipeline_claude_usage` canonicalizes the week boundaries +
4096
4238
  # the 5h window key at ingest). Append it, then run the single-flight cycle.
4239
+ # The capture clock intentionally stays separate from CCTALLY_AS_OF in
4240
+ # production. The harness-only pin lets deterministic CLI scenarios model a
4241
+ # genuinely open historical block without pretending a years-old reset is
4242
+ # current wall time.
4243
+ capture_now = now_dt if os.environ.get("CCTALLY_TEST_PIN_CAPTURE") else None
4097
4244
  raw: dict[str, Any] = {
4098
4245
  "weekly_percent": weekly_percent,
4099
4246
  "resets_at": resets_at,
@@ -4105,7 +4252,7 @@ def cmd_record_usage(
4105
4252
  # carries `_command_as_of()` (below) so the hook preserves that split.
4106
4253
  # Captured ONCE here (append time) and journaled, so a delayed ingest is
4107
4254
  # deterministic (never the ingest wall clock).
4108
- "captured_at": now_utc_iso(),
4255
+ "captured_at": now_utc_iso(capture_now),
4109
4256
  }
4110
4257
  if five_hour_percent is not None:
4111
4258
  raw["five_hour_percent"] = five_hour_percent
@@ -4539,6 +4686,7 @@ def _cmd_hook_tick_codex(args: argparse.Namespace, *, event: str = "unknown") ->
4539
4686
  lambda active_conn: c.sync_codex_cache(
4540
4687
  active_conn, lock_timeout=0
4541
4688
  ),
4689
+ origin="hook.codex_quota.sync",
4542
4690
  )
4543
4691
  finally:
4544
4692
  cache.close()
@@ -4717,6 +4865,7 @@ def cmd_hook_tick(args: argparse.Namespace) -> int:
4717
4865
  cache_mod._run_cache_operation_with_recovery(
4718
4866
  cache_conn,
4719
4867
  lambda active_conn: sync_cache(active_conn),
4868
+ origin="hook.claude.sync",
4720
4869
  )
4721
4870
  )
4722
4871
  ingested = int(stats.rows_changed)
@@ -5196,13 +5345,15 @@ def _derive_5h_window_key(conn, five_hour_resets_at_epoch):
5196
5345
  )
5197
5346
 
5198
5347
 
5199
- def _run_dollar_axes(saved, *, conn, as_of, alert_sink):
5348
+ def _run_dollar_axes(saved, *, conn, as_of, alert_sink, enabled=True):
5200
5349
  """The four dollar-decoupled alert axes in cmd_record_usage's legacy order
5201
5350
  (budget → project-budget → codex-budget → projected). Runs on BOTH the accept
5202
5351
  path AND every dedup-skip tick (spec §4.5: USD spend can cross a $ threshold
5203
5352
  while the weekly/5h percent is flat). Passed-conn → the chokepoints fold into
5204
5353
  the cycle txn and re-raise on failure (invariant ii); their crossings' alert
5205
5354
  payloads land in `alert_sink` for post-commit dispatch."""
5355
+ if not enabled:
5356
+ return
5206
5357
  c = _cctally()
5207
5358
  c.maybe_record_budget_milestone(
5208
5359
  saved, conn=conn, as_of=as_of, alert_sink=alert_sink)
@@ -5302,7 +5453,13 @@ def _pipeline_claude_usage(ctx, rec):
5302
5453
  account_key = rec.get("account") or _lib_accounts.UNATTRIBUTED
5303
5454
 
5304
5455
  # Week boundaries from resets_at (cmd_record_usage canonicalization).
5305
- week_end_at_dt = dt.datetime.fromtimestamp(resets_at, tz=dt.timezone.utc)
5456
+ # Normalize before deriving the date keys as well as the ISO overrides:
5457
+ # rounding a 23:30–23:59 boundary crosses midnight, and letting
5458
+ # `_usage_snapshot_columns` normalize only the saved row would fork the
5459
+ # milestone's retained cost window onto the prior calendar date.
5460
+ week_end_at_dt = _cctally_core._normalize_week_boundary_dt(
5461
+ dt.datetime.fromtimestamp(resets_at, tz=dt.timezone.utc)
5462
+ )
5306
5463
  week_start_at_dt = week_end_at_dt - dt.timedelta(days=7)
5307
5464
  week_start_date = week_start_at_dt.date().isoformat()
5308
5465
  week_end_date = week_end_at_dt.date().isoformat()
@@ -5409,10 +5566,16 @@ def _pipeline_claude_usage(ctx, rec):
5409
5566
  # crosses a $ threshold still derives it.
5410
5567
  c.maybe_record_milestone(
5411
5568
  saved, conn=conn, as_of=capture_at, alert_sink=ctx.pending_alerts,
5412
- journal=(ctx, rec["id"]), account_key=account_key)
5569
+ journal=(ctx, rec["id"]), account_key=account_key,
5570
+ retained_selection=c.WeekSelection(
5571
+ week_start=dt.date.fromisoformat(week_start_date),
5572
+ week_end=dt.date.fromisoformat(week_end_date),
5573
+ start_iso_override=week_start_at,
5574
+ end_iso_override=week_end_at,
5575
+ ))
5413
5576
  c.maybe_update_five_hour_block(
5414
5577
  saved, conn=conn, as_of=capture_at, alert_sink=ctx.pending_alerts,
5415
- account_key=account_key)
5578
+ account_key=account_key, journal_ctx=ctx)
5416
5579
  # The dollar-decoupled axes resolve a CURRENT budget/period WINDOW from
5417
5580
  # "now" and must see the record's DETECTION clock (`as_of` = rec["at"] =
5418
5581
  # `_command_as_of()`), NOT the wall-clock capture stamp: legacy
@@ -5421,8 +5584,16 @@ def _pipeline_claude_usage(ctx, rec):
5421
5584
  # week's window (capture_at is the raw wall clock, which lands outside a
5422
5585
  # pinned fixture window and silently drops the crossing). Both collapse to
5423
5586
  # real-now in production; the split only matters under a pinned as_of.
5424
- _run_dollar_axes(saved, conn=conn, as_of=as_of,
5425
- alert_sink=ctx.pending_alerts)
5587
+ _run_dollar_axes(
5588
+ saved,
5589
+ conn=conn,
5590
+ as_of=as_of,
5591
+ alert_sink=ctx.pending_alerts,
5592
+ # Budget/projected rows depend on historical config that was not retained
5593
+ # in the journal. Task B classifies them as re-materialized projections:
5594
+ # the planner retires stale latches and never fabricates old config.
5595
+ enabled=(ctx.event_sink is None),
5596
+ )
5426
5597
 
5427
5598
  # 4'. Window-rollover 5h-block heal (SKIP path only). A dedup skip swallows
5428
5599
  # the snapshot insert, so `saved` (the dedup target — the LATEST stored
@@ -5457,12 +5628,12 @@ def _pipeline_claude_usage(ctx, rec):
5457
5628
  "fiveHourWindowKey": int(five_hour_window_key),
5458
5629
  },
5459
5630
  conn=conn, as_of=capture_at, alert_sink=ctx.pending_alerts,
5460
- account_key=account_key)
5631
+ account_key=account_key, journal_ctx=ctx)
5461
5632
 
5462
5633
  # 5. hwm-7d / hwm-5h projection files — ACCEPT path only (the monotonic
5463
5634
  # writer; a dedup tick's percent is already <= the stored HWM). In-place
5464
5635
  # credit force-writes live in detect_reset_and_credit.
5465
- if not skip:
5636
+ if not skip and ctx.projection_writes:
5466
5637
  _write_hwm_files(week_start_date, weekly_percent,
5467
5638
  five_hour_window_key, five_hour_percent)
5468
5639