cctally 1.113.0 → 1.115.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 (97) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +4 -6
  3. package/bin/_cctally_account.py +32 -8
  4. package/bin/_cctally_cache.py +6035 -722
  5. package/bin/_cctally_completion.py +1287 -0
  6. package/bin/_cctally_config.py +37 -0
  7. package/bin/_cctally_core.py +400 -2
  8. package/bin/_cctally_dashboard.py +98 -20
  9. package/bin/_cctally_dashboard_conversation.py +292 -23
  10. package/bin/_cctally_dashboard_envelope.py +16 -3
  11. package/bin/_cctally_dashboard_perf.py +128 -9
  12. package/bin/_cctally_dashboard_sources.py +285 -213
  13. package/bin/_cctally_db.py +1177 -23
  14. package/bin/_cctally_diagnosis_sources.py +22 -4
  15. package/bin/_cctally_doctor.py +572 -45
  16. package/bin/_cctally_five_hour.py +84 -33
  17. package/bin/_cctally_forecast.py +108 -15
  18. package/bin/_cctally_journal.py +3950 -116
  19. package/bin/_cctally_journal_repair.py +101 -12
  20. package/bin/_cctally_milestone_history.py +82 -9
  21. package/bin/_cctally_parser.py +42 -0
  22. package/bin/_cctally_percent_breakdown.py +3 -2
  23. package/bin/_cctally_project.py +8 -2
  24. package/bin/_cctally_quarantine.py +652 -0
  25. package/bin/_cctally_quota.py +227 -37
  26. package/bin/_cctally_quota_model.py +360 -83
  27. package/bin/_cctally_record.py +691 -149
  28. package/bin/_cctally_rederive.py +320 -41
  29. package/bin/_cctally_refresh.py +84 -13
  30. package/bin/_cctally_source_analytics.py +83 -17
  31. package/bin/_cctally_statusline.py +6154 -517
  32. package/bin/_cctally_store.py +367 -53
  33. package/bin/_cctally_transcript.py +8 -7
  34. package/bin/_cctally_tui.py +61 -56
  35. package/bin/_cctally_weekrefs.py +128 -21
  36. package/bin/_lib_account_binding.py +152 -0
  37. package/bin/_lib_account_protocol.py +386 -0
  38. package/bin/_lib_aggregators.py +4 -0
  39. package/bin/_lib_cache_coverage.py +13 -1
  40. package/bin/_lib_cache_report.py +35 -6
  41. package/bin/_lib_cache_writer_lock.py +19 -3
  42. package/bin/_lib_clone_copy.py +142 -0
  43. package/bin/_lib_codex_conversation.py +32 -0
  44. package/bin/_lib_codex_conversation_query.py +297 -49
  45. package/bin/_lib_codex_conversation_watch.py +59 -14
  46. package/bin/_lib_codex_drain_admission.py +214 -0
  47. package/bin/_lib_codex_metadata.py +18 -1
  48. package/bin/_lib_codex_publication_barrier.py +277 -0
  49. package/bin/_lib_codex_rollout_container.py +502 -0
  50. package/bin/_lib_conversation_dispatch.py +3 -0
  51. package/bin/_lib_conversation_query.py +106 -59
  52. package/bin/_lib_conversation_retention.py +1605 -640
  53. package/bin/_lib_cost_provenance.py +53 -2
  54. package/bin/_lib_detector_access_registry.py +3817 -0
  55. package/bin/_lib_doctor.py +714 -69
  56. package/bin/_lib_five_hour.py +126 -0
  57. package/bin/_lib_fmt.py +11 -0
  58. package/bin/_lib_ingest_frontier.py +414 -20
  59. package/bin/_lib_journal.py +141 -10
  60. package/bin/_lib_jsonl.py +5 -4
  61. package/bin/_lib_legacy_spool.py +126 -0
  62. package/bin/_lib_legacy_writer_evidence.py +136 -0
  63. package/bin/_lib_merge_gate.py +231 -86
  64. package/bin/_lib_meter_rate_change.py +10 -0
  65. package/bin/_lib_outcome_quarantine.py +690 -0
  66. package/bin/_lib_perf.py +27 -1
  67. package/bin/_lib_pricing.py +199 -9
  68. package/bin/_lib_pricing_check.py +20 -4
  69. package/bin/_lib_quarantine.py +154 -0
  70. package/bin/_lib_quota_calibration.py +53 -8
  71. package/bin/_lib_quota_copy.py +2 -1
  72. package/bin/_lib_quota_model.py +288 -46
  73. package/bin/_lib_reclaim_planner.py +792 -0
  74. package/bin/_lib_rederive.py +319 -82
  75. package/bin/_lib_segment_summary.py +35 -2
  76. package/bin/_lib_snapshot_cache.py +21 -0
  77. package/bin/_lib_sqlite_close.py +81 -0
  78. package/bin/_lib_statusline_candidates.py +292 -23
  79. package/bin/_lib_subscription_weeks.py +100 -17
  80. package/bin/_lib_tick_stats.py +217 -17
  81. package/bin/_lib_wal_checkpoint.py +1077 -0
  82. package/bin/_lib_write_budget.py +483 -0
  83. package/bin/_lib_write_io.py +703 -0
  84. package/bin/cctally +31 -1
  85. package/dashboard/static/assets/{ConversationsView-CEZ-BUyi.js → ConversationsView-Bc8LCOAZ.js} +1 -1
  86. package/dashboard/static/assets/{DoctorModal-CE1oH_Zy.js → DoctorModal-D--qVBwe.js} +1 -1
  87. package/dashboard/static/assets/ModalRoot-DLPgyAM_.js +1 -0
  88. package/dashboard/static/assets/{ProjectsDrillPanel-B3DZoCeD.js → ProjectsDrillPanel-z53a4v4M.js} +1 -1
  89. package/dashboard/static/assets/{SourceDetailModal-CHp9EgLP.js → SourceDetailModal-Bw0T8W-W.js} +1 -1
  90. package/dashboard/static/assets/{UpdateModal-5iKST6jo.js → UpdateModal-D5AKx4Yw.js} +1 -1
  91. package/dashboard/static/assets/index-BKITk1CT.css +1 -0
  92. package/dashboard/static/assets/{index-Cm0VTxJC.js → index-BXDOzSng.js} +3 -3
  93. package/dashboard/static/assets/{outlineNavigation-DkXGL8J6.js → outlineNavigation-JObY7cUY.js} +1 -1
  94. package/dashboard/static/dashboard.html +2 -2
  95. package/package.json +19 -1
  96. package/dashboard/static/assets/ModalRoot-D5Y8IFUG.js +0 -1
  97. package/dashboard/static/assets/index-Dt4tqf43.css +0 -1
@@ -204,8 +204,13 @@ from _cctally_core import (
204
204
  _as_of_or_command,
205
205
  )
206
206
  import _lib_accounts # pure stdlib kernel; UNATTRIBUTED sentinel default (#341)
207
+ import _lib_account_protocol # pure #825/#832 writer-generation kernel
207
208
  import _lib_blocks # pure stdlib kernel; the #751a five-hour ownership rule
208
- from _lib_five_hour import _canonical_5h_window_key, five_hour_milestone_range
209
+ from _lib_five_hour import (
210
+ _canonical_5h_window_key,
211
+ five_hour_milestone_range,
212
+ latest_five_hour_reading,
213
+ )
209
214
  from _lib_pricing import _calculate_entry_cost, claude_usage_dict
210
215
  from _lib_codex_hooks import (
211
216
  CODEX_HOOK_THROTTLE_SECONDS,
@@ -567,49 +572,109 @@ _logged_window_key_coerce_failure = False
567
572
  # these call-time constant/helper accessors are untouched.)
568
573
 
569
574
 
575
+ @dataclass(frozen=True)
576
+ class FiveHourResetEvent:
577
+ """The active five-hour reset event of one exact account and window
578
+ (#825/#832 spec 4.5).
579
+
580
+ ``logical_generation`` is replay-stable: it is built from the effective
581
+ reset instant, the detection instant and the event's journal identity (the
582
+ natural-key id ``fhc:<account>:<window>:<effective>`` the harvest stamps),
583
+ never from the SQLite rowid. ``row_id`` is the materialized foreign-key
584
+ target inside ONE materialization (milestone ``reset_event_id``); it is
585
+ excluded from equality and must never enter retry identity, because a
586
+ rebuild renumbers it."""
587
+
588
+ account_key: str
589
+ canonical_window_key: int
590
+ effective_reset_at_utc: str
591
+ floor_epoch: int
592
+ logical_generation: str
593
+ row_id: int = field(default=0, compare=False)
594
+
595
+
596
+ @dataclass(frozen=True)
597
+ class NoFiveHourResetEvent:
598
+ """The exact account/window lookup found no reset event."""
599
+
600
+ account_key: str
601
+ canonical_window_key: int
602
+
603
+
604
+ def resolve_five_hour_reset_event(
605
+ conn: "sqlite3.Connection",
606
+ canonical_window_key: int,
607
+ *,
608
+ account_key: str,
609
+ ) -> "FiveHourResetEvent | NoFiveHourResetEvent":
610
+ """Resolve the active five-hour reset event for EXACTLY
611
+ ``(account_key, canonical_window_key)`` (#825/#832 spec 4.5, #832).
612
+
613
+ There is no default account: a missing argument is a TypeError, never a
614
+ silent query of the ``unattributed`` sentinel. The active event is the
615
+ latest by logical ordering — effective reset instant, then detection
616
+ instant, then the stable journal identity — never by largest rowid, so a
617
+ rebuild that renumbers rows cannot change which event is active. A database
618
+ or query error PROPAGATES: it is not evidence that no reset happened.
619
+ ``NoFiveHourResetEvent`` is returned only when the exact lookup finds no
620
+ row."""
621
+ if not isinstance(account_key, str) or not account_key:
622
+ raise ValueError("five-hour reset resolution requires an exact account")
623
+ import _lib_journal
624
+
625
+ key = int(canonical_window_key)
626
+ rows = conn.execute(
627
+ "SELECT detected_at_utc, effective_reset_at_utc, id "
628
+ "FROM five_hour_reset_events "
629
+ "WHERE five_hour_window_key = ? AND account_key = ?",
630
+ (key, account_key),
631
+ ).fetchall()
632
+ best = None
633
+ for row in rows:
634
+ effective = str(row["effective_reset_at_utc"])
635
+ detected = str(row["detected_at_utc"])
636
+ identity = _lib_journal.evt_id("fhc", account_key, key, effective)
637
+ order = (
638
+ int(parse_iso_datetime(effective, "five_hour_reset_events.effective").timestamp()),
639
+ int(parse_iso_datetime(detected, "five_hour_reset_events.detected").timestamp()),
640
+ identity,
641
+ )
642
+ if best is None or order > best[0]:
643
+ best = (order, row, effective, detected, identity)
644
+ if best is None:
645
+ return NoFiveHourResetEvent(account_key, key)
646
+ order, row, effective, detected, identity = best
647
+ return FiveHourResetEvent(
648
+ account_key=account_key,
649
+ canonical_window_key=key,
650
+ effective_reset_at_utc=effective,
651
+ floor_epoch=order[0],
652
+ logical_generation=f"{effective}|{detected}|{identity}",
653
+ row_id=int(row["id"]),
654
+ )
655
+
656
+
570
657
  def _resolve_active_five_hour_reset_event_id(
571
658
  conn: "sqlite3.Connection",
572
659
  five_hour_window_key: int,
573
660
  *,
574
- account_key: str = _lib_accounts.UNATTRIBUTED,
661
+ account_key: str,
575
662
  ) -> int:
576
- """Return ``id`` of the most-recent ``five_hour_reset_events`` row for
577
- ``(account_key, five_hour_window_key)``, else 0 (pre-credit / no-event
578
- sentinel). ``account_key`` (#341) scopes the active-segment resolution to
579
- the account so a shared physical window keeps per-account segments.
580
-
581
- Mirrors the weekly active-segment resolution pattern used by
582
- ``maybe_record_milestone`` for ``percent_milestones.reset_event_id``.
583
- Called once per ``maybe_update_five_hour_block`` invocation and the
584
- return value is threaded through every read/write site that keys on
585
- ``(five_hour_window_key, percent_threshold)`` so post-credit threshold
586
- crossings land as a distinct row from any pre-credit one at the same
587
- threshold. See spec
588
- docs/superpowers/specs/2026-05-16-5h-in-place-credit-detection.md §3.3.
589
-
590
- Returns ``0`` when:
591
- - The window has no ``five_hour_reset_events`` row (most blocks).
592
- - The table doesn't exist yet (DB predates this feature).
593
-
594
- Returns the largest ``id`` matching the window otherwise; the
595
- ``ORDER BY id DESC LIMIT 1`` clause is what *defines* "active" in
596
- the stacked-credit case (spec §2.3 — multiple events across distinct
597
- 10-min slots): pre-credit milestones key on ``seg=0``, milestones
598
- between credit 1 and credit 2 key on event-1's id, and milestones
599
- after credit 2 key on event-2's id.
600
- """
601
- try:
602
- row = conn.execute(
603
- "SELECT id FROM five_hour_reset_events "
604
- "WHERE five_hour_window_key = ? AND account_key = ? "
605
- "ORDER BY id DESC LIMIT 1",
606
- (int(five_hour_window_key), account_key),
607
- ).fetchone()
608
- except sqlite3.DatabaseError:
609
- return 0
610
- if row is None:
663
+ """The materialized ``id`` of the active event for the exact account and
664
+ window, else the pre-credit sentinel 0. A thin adapter over
665
+ `resolve_five_hour_reset_event` for the one materialization that keys
666
+ milestones on the row (``five_hour_milestones.reset_event_id``); the choice
667
+ of event is the logical one, and ``account_key`` has no default (#832).
668
+
669
+ The returned id is valid only inside the current materialization: pre-credit
670
+ milestones key on ``seg=0`` and post-credit milestones on the active event,
671
+ exactly as before, and replay resolves the logical reference again (spec
672
+ docs/superpowers/specs/2026-05-16-5h-in-place-credit-detection.md §3.3)."""
673
+ event = resolve_five_hour_reset_event(
674
+ conn, five_hour_window_key, account_key=account_key)
675
+ if isinstance(event, NoFiveHourResetEvent):
611
676
  return 0
612
- return int(row["id"])
677
+ return event.row_id
613
678
 
614
679
 
615
680
  def maybe_record_milestone(
@@ -661,6 +726,12 @@ def maybe_record_milestone(
661
726
  weekly_percent = saved.get("weeklyPercent")
662
727
  if weekly_percent is None or weekly_percent < 1:
663
728
  return
729
+ # A held row never establishes a weekly crossing (spec 4.7 revision 7
730
+ # *Held semantics*): the dedup and no-write self-heals pass the latest
731
+ # stored row, which a weekly credit's cleanup may have kept weekly-held
732
+ # with its retired pre-credit value beside its five-hour evidence.
733
+ if saved.get("weeklyObservationHeld"):
734
+ return
664
735
 
665
736
  # Snap near-integer values up before flooring: the status-line API returns
666
737
  # N% as 0.N * 100, which in IEEE 754 can land one ULP below N.0 (e.g.
@@ -2401,6 +2472,71 @@ def _compute_block_totals(
2401
2472
  return result
2402
2473
 
2403
2474
 
2475
+ def _block_pricing_provenance_json(pricing_evidence: dict) -> str:
2476
+ """The computation-time pricing provenance of one block's totals, from
2477
+ the ``pricing_evidence`` `_compute_block_totals` filled while computing
2478
+ them: the ONE spelling the live upsert and the rebuild's open-block
2479
+ projection (`_backfill_five_hour_blocks`, #825/#832 fix round J4) both
2480
+ store in `five_hour_blocks.pricing_provenance_json`.
2481
+
2482
+ It describes the pricing used for THESE computed totals. A later
2483
+ successor closes the block without recomputing it, so looking up cards
2484
+ while harvesting the close would describe the wrong binary."""
2485
+ import _lib_pricing
2486
+ pricing_marker = {
2487
+ "version": 1,
2488
+ "pricingDate": _lib_pricing.current_pricing_snapshot().snapshot_date,
2489
+ "unpricedModels": pricing_evidence["unpricedModels"],
2490
+ }
2491
+ if pricing_evidence.get("uncertainModels"):
2492
+ pricing_marker["uncertainModels"] = pricing_evidence["uncertainModels"]
2493
+ return json.dumps(pricing_marker, sort_keys=True, separators=(",", ":"))
2494
+
2495
+
2496
+ def _flag_blocks_spanning_week_resets(conn, account_key: str) -> None:
2497
+ """The reset-crossing cross-flag sweep (see its call site in
2498
+ `maybe_update_five_hour_block` for the two sources and the
2499
+ `unixepoch()` rationale). Idempotent: it only flips 0 -> 1, and never
2500
+ touches a journaled closed block.
2501
+
2502
+ #825/#832 fix round K16b (spec 4.7 revision 7, *Versioning and
2503
+ replay*): it also runs for an observation with no canonical five-hour
2504
+ anchor. A weekly-only reading can mint the in-place weekly credit whose
2505
+ effective instant lies inside an OPEN block that an earlier-appended,
2506
+ later-captured reading extended, and the rebuild's open-block
2507
+ projection flags that block from durable state. Without the sweep on
2508
+ that tick, live kept the flag at 0 until the next five-hour tick, so
2509
+ live and replay disagreed."""
2510
+ conn.execute(
2511
+ """
2512
+ UPDATE five_hour_blocks
2513
+ SET crossed_seven_day_reset = 1
2514
+ WHERE crossed_seven_day_reset = 0
2515
+ AND account_key = ?
2516
+ AND (is_closed = 0 OR journal_id IS NULL)
2517
+ AND (
2518
+ EXISTS (
2519
+ SELECT 1 FROM week_reset_events e
2520
+ WHERE e.account_key = five_hour_blocks.account_key
2521
+ AND unixepoch(e.effective_reset_at_utc)
2522
+ BETWEEN unixepoch(five_hour_blocks.block_start_at)
2523
+ AND unixepoch(five_hour_blocks.last_observed_at_utc)
2524
+ )
2525
+ OR EXISTS (
2526
+ SELECT 1 FROM weekly_usage_snapshots ws
2527
+ WHERE ws.week_start_at IS NOT NULL
2528
+ AND ws.account_key = five_hour_blocks.account_key
2529
+ AND unixepoch(ws.week_start_at)
2530
+ > unixepoch(five_hour_blocks.block_start_at)
2531
+ AND unixepoch(ws.week_start_at)
2532
+ <= unixepoch(five_hour_blocks.last_observed_at_utc)
2533
+ )
2534
+ )
2535
+ """,
2536
+ (account_key,),
2537
+ )
2538
+
2539
+
2404
2540
  def maybe_update_five_hour_block(
2405
2541
  saved: dict[str, Any], *, conn=None, as_of: "str | None" = None,
2406
2542
  alert_sink: "list | None" = None, account_key: str = _lib_accounts.UNATTRIBUTED,
@@ -2440,7 +2576,12 @@ def maybe_update_five_hour_block(
2440
2576
  or five_hour_resets_at is None
2441
2577
  or five_hour_window_key is None
2442
2578
  ):
2443
- return # no canonical 5h anchor — nothing to record
2579
+ # No canonical 5h anchor — nothing to record. On the ingest path the
2580
+ # cross-flag sweep still runs: this tick may have minted a weekly
2581
+ # reset inside an open block (fix round K16b).
2582
+ if conn is not None:
2583
+ _flag_blocks_spanning_week_resets(conn, account_key)
2584
+ return
2444
2585
 
2445
2586
  captured_at = saved["capturedAt"]
2446
2587
  # #769 S11 (#824). Two concepts, deliberately named apart: the weekly value
@@ -2571,20 +2712,8 @@ def maybe_update_five_hour_block(
2571
2712
  )
2572
2713
  if owned_entries is not None:
2573
2714
  source_sink[source_key] = tuple(owned_entries)
2574
- # This describes the pricing used for THESE computed totals. A later
2575
- # successor closes this block without recomputing it, so looking up
2576
- # cards while harvesting the close would describe the wrong binary.
2577
- import _lib_pricing
2578
- pricing_marker = {
2579
- "version": 1,
2580
- "pricingDate": _lib_pricing.current_pricing_snapshot().snapshot_date,
2581
- "unpricedModels": pricing_evidence["unpricedModels"],
2582
- }
2583
- if pricing_evidence.get("uncertainModels"):
2584
- pricing_marker["uncertainModels"] = pricing_evidence["uncertainModels"]
2585
- pricing_provenance_json = json.dumps(
2586
- pricing_marker, sort_keys=True, separators=(",", ":"),
2587
- )
2715
+ pricing_provenance_json = _block_pricing_provenance_json(
2716
+ pricing_evidence)
2588
2717
 
2589
2718
  # Hoist alerts config above BEGIN (M1 + M2): single read serves
2590
2719
  # all per-pct iterations in the catch-up case, AND keeps the
@@ -2635,14 +2764,15 @@ def maybe_update_five_hour_block(
2635
2764
  # by the first retained observation captured after natural expiry.
2636
2765
  # Both rules use ``now_iso`` (the observation's capture clock),
2637
2766
  # never ingest/retry wall time.
2767
+ # Read into sets without a history-sized SQL sort; each emission
2768
+ # set is sorted below before frozen closure events are emitted.
2638
2769
  closing_ids = {
2639
2770
  int(row["id"])
2640
2771
  for row in conn.execute(
2641
2772
  "SELECT id FROM five_hour_blocks "
2642
2773
  "WHERE is_closed = 0 AND account_key = ? "
2643
2774
  "AND (five_hour_window_key < ? "
2644
- " OR unixepoch(five_hour_resets_at) < unixepoch(?)) "
2645
- "ORDER BY id",
2775
+ " OR unixepoch(five_hour_resets_at) < unixepoch(?))",
2646
2776
  (account_key, int(five_hour_window_key), now_iso),
2647
2777
  ).fetchall()
2648
2778
  }
@@ -2657,8 +2787,7 @@ def maybe_update_five_hour_block(
2657
2787
  "WHERE is_closed = 1 AND journal_id IS NOT NULL "
2658
2788
  "AND account_key = ? AND last_updated_at_utc = ? "
2659
2789
  "AND (five_hour_window_key < ? "
2660
- " OR unixepoch(five_hour_resets_at) < unixepoch(?)) "
2661
- "ORDER BY id",
2790
+ " OR unixepoch(five_hour_resets_at) < unixepoch(?))",
2662
2791
  (
2663
2792
  account_key,
2664
2793
  now_iso,
@@ -3129,34 +3258,7 @@ def maybe_update_five_hour_block(
3129
3258
  # INSERT OR IGNORE was a duplicate and the flag stayed 0
3130
3259
  # forever. The JOIN re-derives from durable state on
3131
3260
  # every tick and self-heals.
3132
- conn.execute(
3133
- """
3134
- UPDATE five_hour_blocks
3135
- SET crossed_seven_day_reset = 1
3136
- WHERE crossed_seven_day_reset = 0
3137
- AND account_key = ?
3138
- AND (is_closed = 0 OR journal_id IS NULL)
3139
- AND (
3140
- EXISTS (
3141
- SELECT 1 FROM week_reset_events e
3142
- WHERE e.account_key = five_hour_blocks.account_key
3143
- AND unixepoch(e.effective_reset_at_utc)
3144
- BETWEEN unixepoch(five_hour_blocks.block_start_at)
3145
- AND unixepoch(five_hour_blocks.last_observed_at_utc)
3146
- )
3147
- OR EXISTS (
3148
- SELECT 1 FROM weekly_usage_snapshots ws
3149
- WHERE ws.week_start_at IS NOT NULL
3150
- AND ws.account_key = five_hour_blocks.account_key
3151
- AND unixepoch(ws.week_start_at)
3152
- > unixepoch(five_hour_blocks.block_start_at)
3153
- AND unixepoch(ws.week_start_at)
3154
- <= unixepoch(five_hour_blocks.last_observed_at_utc)
3155
- )
3156
- )
3157
- """,
3158
- (account_key,),
3159
- )
3261
+ _flag_blocks_spanning_week_resets(conn, account_key)
3160
3262
 
3161
3263
  # Freeze only after every parent field and both rollup child sets
3162
3264
  # have reached their final close-state values for this observation.
@@ -3515,6 +3617,23 @@ _STALE_REPLICA_BAND_TEMPLATE = (
3515
3617
  )
3516
3618
 
3517
3619
 
3620
+ #: #825/#832 spec 4.7 revision 7 (fix round K16b): the band rows a weekly
3621
+ #: credit's stale-replica cleanup KEEPS, weekly-held, because they carry a
3622
+ #: five-hour reading of their own: a five-hour percent and reset pair. A
3623
+ #: `record-credit` synthetic is excluded: its five-hour columns are a copy of
3624
+ #: the crediting account's latest reading (`_resolve_prior_5h`), which its own
3625
+ #: row keeps, so the synthetic carries no independent evidence and is removed
3626
+ #: exactly as before (also by a `--force` re-record's clear, which names it
3627
+ #: in the same suppression list). The journal applier
3628
+ #: (`_cctally_journal._retire_weekly_effects`) states the same rule by
3629
+ #: logical id.
3630
+ _WEEKLY_RETIREMENT_KEEPS_FIVE_HOUR = (
3631
+ " AND five_hour_percent IS NOT NULL "
3632
+ " AND five_hour_resets_at IS NOT NULL "
3633
+ " AND source <> 'record-credit'"
3634
+ )
3635
+
3636
+
3518
3637
  def _stale_replica_band_sql(conn, *, manual):
3519
3638
  """The stale-replica band for ``conn``, as one text.
3520
3639
 
@@ -3652,11 +3771,30 @@ def _delete_doomed_snapshot_rows(conn, *, week_start_date, account_key,
3652
3771
  projection, and the set relation between that projection and the rows this
3653
3772
  removes is now true by construction rather than by mechanism: both apply the
3654
3773
  same band. The relation is still asserted as a test, which is where it
3655
- belongs."""
3774
+ belongs.
3775
+
3776
+ #825/#832 spec 4.7 revision 7 (*Weekly suppression and five-hour evidence*,
3777
+ fix round K16b): the cleanup retires WEEKLY effects only. A band row that
3778
+ carries a five-hour reading of its own (`_WEEKLY_RETIREMENT_KEEPS_FIVE_HOUR`)
3779
+ is kept and becomes weekly-held, with its identity, capture, boundaries and
3780
+ five-hour columns intact; it then leaves the band (which selects unheld rows
3781
+ only), so the DELETE that follows reaches exactly the rows that keep evidence
3782
+ on neither axis. The suppression projection still names every band row, so
3783
+ the journal applier (`_cctally_journal._retire_weekly_effects`) replays the
3784
+ same split by logical id. A store without the held column cannot hold a
3785
+ held row and keeps the plain DELETE (the band's own pre-column tolerance).
3786
+ Returns the number of rows deleted."""
3787
+ params = (week_start_date, account_key, effective_iso, float(pre_credit))
3788
+ band = _stale_replica_band_sql(conn, manual=manual)
3789
+ if _cctally_core.weekly_held_exclusion(conn):
3790
+ conn.execute(
3791
+ "UPDATE weekly_usage_snapshots SET weekly_observation_held = 1 "
3792
+ + band + _WEEKLY_RETIREMENT_KEEPS_FIVE_HOUR,
3793
+ params,
3794
+ )
3656
3795
  return conn.execute(
3657
- "DELETE FROM weekly_usage_snapshots "
3658
- + _stale_replica_band_sql(conn, manual=manual),
3659
- (week_start_date, account_key, effective_iso, float(pre_credit)),
3796
+ "DELETE FROM weekly_usage_snapshots " + band,
3797
+ params,
3660
3798
  ).rowcount
3661
3799
 
3662
3800
 
@@ -4050,7 +4188,8 @@ def detect_reset_and_credit(conn, *, week_start_date, week_end_at,
4050
4188
  five_hour_percent, as_of=None, commit=True,
4051
4189
  ctx=None, account_key=_lib_accounts.UNATTRIBUTED,
4052
4190
  origin_observation_id=None, capture_at=None,
4053
- source=None, hold_weekly_axis=False):
4191
+ source=None, hold_weekly_axis=False,
4192
+ outcomes_allowed=True, fold_policy=0):
4054
4193
  """Detect + record weekly and 5h reset/credit artifacts for one usage
4055
4194
  observation (extracted from ``cmd_record_usage``; DB journal redesign
4056
4195
  §5.2.3).
@@ -4118,6 +4257,26 @@ def detect_reset_and_credit(conn, *, week_start_date, week_end_at,
4118
4257
  reviewed observation hold. It bypasses weekly reset/credit detection
4119
4258
  while still running the independent five-hour detector below. Live
4120
4259
  callers retain the default.
4260
+ - ``outcomes_allowed`` (#825/#832 spec 5.6 item 2, default ``True``): the
4261
+ outcome-quarantine gate, decided by the ingest fold BEFORE this call and
4262
+ before any durable append. ``False`` withholds every automatic outcome —
4263
+ the moved week-end transition, the >=25 pp fire, the reset-to-zero
4264
+ confirmation and the five-hour credit — together with its event, its
4265
+ stale-replica deletions, its floor and HWM changes and its state
4266
+ replacement: the detector takes its no-outcome branch. Detector state
4267
+ that is not an outcome (arming, a cleared transient zero, a five-hour
4268
+ baseline) still advances, and clearing it never establishes recovery.
4269
+ - ``fold_policy`` (#825/#832 spec 4.7 revision 6, default the historical
4270
+ policy 0): the policy the observation was captured under. The
4271
+ detector itself no longer depends on it (fix round K15): for every
4272
+ policy a five-hour credit's stale-replica cleanup retires FIVE-HOUR
4273
+ effects only. A doomed row that carries accepted weekly evidence
4274
+ (``weekly_observation_held = 0``) keeps its weekly value, boundary,
4275
+ capture and held provenance and loses only its five-hour fields; a
4276
+ held row carries no weekly observation and is deleted. The event
4277
+ records every doomed row in its one ``suppression`` list, exactly as
4278
+ the historical policy writes it, so the previous release folds it,
4279
+ and the current applier replays it with the same split.
4121
4280
  """
4122
4281
  c = _cctally()
4123
4282
  now_utc = _as_of_or_command(as_of)
@@ -4176,7 +4335,8 @@ def detect_reset_and_credit(conn, *, week_start_date, week_end_at,
4176
4335
  # or more (filters out API flaps / transient boundary
4177
4336
  # jitter where usage stays roughly the same).
4178
4337
  if (
4179
- prior_end_dt > now_utc
4338
+ outcomes_allowed
4339
+ and prior_end_dt > now_utc
4180
4340
  and prior_pct is not None
4181
4341
  and c._is_reset_drop(prior_pct, weekly_percent)
4182
4342
  ):
@@ -4250,6 +4410,14 @@ def detect_reset_and_credit(conn, *, week_start_date, week_end_at,
4250
4410
  marker_armed=armed,
4251
4411
  marker_baseline=(state[2] if armed else None),
4252
4412
  ).action
4413
+ if (not outcomes_allowed
4414
+ and decision in (FIRE_IMMEDIATE, CONFIRM_RESET)):
4415
+ # Quarantined (spec 5.6 item 2): the no-outcome
4416
+ # branch. The armed state, if any, is left exactly
4417
+ # as it is.
4418
+ decision = NO_ACTION
4419
+ if decision in (FIRE_IMMEDIATE, CONFIRM_RESET):
4420
+ _note_deletion_outcome(ctx, account_key)
4253
4421
  if decision == FIRE_IMMEDIATE:
4254
4422
  # >=25pp goodwill credit — fire immediately, never
4255
4423
  # debounced. Clear any pending arm (now moot).
@@ -4400,7 +4568,13 @@ def detect_reset_and_credit(conn, *, week_start_date, week_end_at,
4400
4568
  window_live=window_live,
4401
4569
  now_iso=now_utc.isoformat(timespec="seconds"),
4402
4570
  )
4403
- if src_decision.action == FIVE_HOUR_CONFIRM:
4571
+ if src_decision.action == FIVE_HOUR_CONFIRM and not outcomes_allowed:
4572
+ # Quarantined (spec 5.6 item 2): no event, no stale-replica
4573
+ # deletion, no HWM force-write and no state replacement.
4574
+ # The source state stays armed, as on a mid-fire raise.
4575
+ pass
4576
+ elif src_decision.action == FIVE_HOUR_CONFIRM:
4577
+ _note_deletion_outcome(ctx, account_key)
4404
4578
  # The two ends of the credit, both taken from the ARMING
4405
4579
  # observation: the pre-drop baseline this source
4406
4580
  # established, and the low that dropped away from it. The
@@ -4658,6 +4832,56 @@ def detect_reset_and_credit(conn, *, week_start_date, week_end_at,
4658
4832
  # event's ``prior_percent``; see the two
4659
4833
  # values' definitions above.
4660
4834
  try:
4835
+ # #825/#832 spec 4.7 revision 6 (fix round K15): the
4836
+ # cleanup retires FIVE-HOUR effects only, for every
4837
+ # fold policy. An unheld stale replica carries
4838
+ # accepted weekly evidence, so it is retired in
4839
+ # place (its five-hour percent, reset and window key
4840
+ # become NULL together) and keeps its weekly value,
4841
+ # boundary, capture and held provenance. The DELETE
4842
+ # below then reaches only the held rows (a retired
4843
+ # row has left the band), whose weekly value is a
4844
+ # copy of an older basis. `_apply_reset_with_
4845
+ # suppression` replays the event's one suppression
4846
+ # list the same way. A store that predates the held
4847
+ # column holds no held row, so there every band row
4848
+ # is retired (two literal statements, so each
4849
+ # keeps its own detector-access contract).
4850
+ retire_params = (
4851
+ int(five_hour_window_key),
4852
+ account_key,
4853
+ effective_iso,
4854
+ stale_replica_pct,
4855
+ )
4856
+ if _cctally_core.weekly_held_exclusion(conn):
4857
+ conn.execute(
4858
+ "UPDATE weekly_usage_snapshots "
4859
+ " SET five_hour_percent = NULL, "
4860
+ " five_hour_resets_at = NULL, "
4861
+ " five_hour_window_key = NULL "
4862
+ " WHERE five_hour_window_key = ? "
4863
+ " AND account_key = ? "
4864
+ " AND unixepoch(captured_at_utc) "
4865
+ " >= unixepoch(?) "
4866
+ " AND ABS(five_hour_percent - ?) "
4867
+ " < 1.0 "
4868
+ " AND weekly_observation_held = 0",
4869
+ retire_params,
4870
+ )
4871
+ else:
4872
+ conn.execute(
4873
+ "UPDATE weekly_usage_snapshots "
4874
+ " SET five_hour_percent = NULL, "
4875
+ " five_hour_resets_at = NULL, "
4876
+ " five_hour_window_key = NULL "
4877
+ " WHERE five_hour_window_key = ? "
4878
+ " AND account_key = ? "
4879
+ " AND unixepoch(captured_at_utc) "
4880
+ " >= unixepoch(?) "
4881
+ " AND ABS(five_hour_percent - ?) "
4882
+ " < 1.0",
4883
+ retire_params,
4884
+ )
4661
4885
  conn.execute(
4662
4886
  "DELETE FROM weekly_usage_snapshots "
4663
4887
  " WHERE five_hour_window_key = ? "
@@ -4714,6 +4938,16 @@ def detect_reset_and_credit(conn, *, week_start_date, week_end_at,
4714
4938
  conn.commit()
4715
4939
 
4716
4940
 
4941
+ def _note_deletion_outcome(ctx, account_key) -> None:
4942
+ """Tell the quarantine fold (when one drives this ingest) that an
4943
+ automatic outcome with stale-replica deletions is about to be minted for
4944
+ ``account_key``. Inert while every registered cross-period read is inert
4945
+ (spec 5.6 item 5)."""
4946
+ fold = getattr(ctx, "quarantine", None) if ctx is not None else None
4947
+ if fold is not None:
4948
+ fold.note_deletion_outcome(account_key)
4949
+
4950
+
4717
4951
  def _resolve_reset_aware_hwm(conn, week_start_date, week_start_at, week_end_at,
4718
4952
  *, account_key):
4719
4953
  """The floored MAX(weekly_percent) the statusline _hwm_clamp computes:
@@ -5554,6 +5788,18 @@ def cmd_record_credit(args) -> int:
5554
5788
  c = _cctally()
5555
5789
  with c._selected_state_lock():
5556
5790
  conn = open_db()
5791
+ # #825/#832 spec 6: the revalidation and the prior five-hour read
5792
+ # that are serialized into the op come from ONE snapshot proven to
5793
+ # be the current interpretation; the read transaction ends when the
5794
+ # connection closes before the append.
5795
+ import _cctally_store
5796
+ conn.execute("BEGIN")
5797
+ try:
5798
+ _cctally_store.require_current_stats_identity(conn)
5799
+ except _cctally_store.StatsIndexNotCurrent as exc:
5800
+ eprint(f"record-credit: {exc}; another cctally process is "
5801
+ "republishing the stats index — restart it and retry")
5802
+ return 2
5557
5803
  revalidated = c._revalidate_credit_plan(
5558
5804
  conn,
5559
5805
  args,
@@ -5611,9 +5857,22 @@ def cmd_record_credit(args) -> int:
5611
5857
  )
5612
5858
  if hwm_before is None:
5613
5859
  hwm_before = plan.from_pct
5860
+ # #825/#832 spec 4.6: the weekly fence and the control
5861
+ # reconciliation are bound to the credit's own account and exact
5862
+ # week, never a global axis-only fence.
5863
+ import _lib_account_binding
5864
+ credit_binding = _lib_account_binding.capture(locked_identity)
5865
+ if credit_binding is None:
5866
+ eprint("record-credit: the active Claude identity is changing; retry")
5867
+ return 2
5868
+ credit_week_end = int(_cctally_core._normalize_week_boundary_dt(
5869
+ parse_iso_datetime(plan.week_end_at, "plan.week_end_at")
5870
+ .astimezone(dt.timezone.utc)).timestamp())
5614
5871
  try:
5615
5872
  handles = c._authoritative_begin(
5616
- {"sevenDay"}, now_epoch=int(time.time())
5873
+ {"sevenDay"}, binding=credit_binding,
5874
+ windows={"sevenDay": credit_week_end},
5875
+ now_epoch=int(time.time()),
5617
5876
  )
5618
5877
  except (OSError, ValueError) as exc:
5619
5878
  eprint(f"record-credit: could not prepare authoritative state: {exc}")
@@ -5674,15 +5933,17 @@ def cmd_record_credit(args) -> int:
5674
5933
  # atomically committed. Leave inflight on any failure so a later
5675
5934
  # authority repair remains fail-closed.
5676
5935
  try:
5677
- projection = c._read_db_projection_stable()
5936
+ projection = c._read_db_projection_stable(
5937
+ account_key=credit_account_key)
5678
5938
  completion_epoch = int(time.time())
5679
5939
  c._authoritative_commit(
5680
5940
  handles, completion_epoch=completion_epoch
5681
5941
  )
5682
5942
  c._reconcile_selected_control(
5683
- projection, now_epoch=completion_epoch, observed_axes={"sevenDay"}
5943
+ projection, binding=credit_binding,
5944
+ now_epoch=completion_epoch, observed_axes={"sevenDay"}
5684
5945
  )
5685
- c._statusline_observe_touch()
5946
+ c._statusline_observe_touch(binding=credit_binding)
5686
5947
  except Exception as exc:
5687
5948
  eprint(f"record-credit: authoritative state incomplete: {exc}")
5688
5949
  return 3
@@ -5723,8 +5984,67 @@ def cmd_record_usage(
5723
5984
  writer: str = "record-usage",
5724
5985
  nudge_dashboard: bool = True,
5725
5986
  nudge_sink=None,
5987
+ binding=None,
5988
+ publication: "dict | None" = None,
5989
+ appended: "list | None" = None,
5990
+ expected_high_water: "tuple | None" = None,
5726
5991
  ) -> int:
5727
- """Record usage from the Claude Code status line rate_limits — DB journal
5992
+ """The CLI adapter over `record_usage_bound` (#825/#832 spec 4.7).
5993
+
5994
+ It captures ONE binding from the stable-read identity resolver when the
5995
+ caller supplies none, and never resolves identity again. A torn or
5996
+ inconsistent identity writes nothing and exits 2 (spec 4.1): the reading
5997
+ is never guessed onto `unattributed`. Every bound producer (the statusline
5998
+ reducer's plan, the OAuth publications) passes its own captured binding."""
5999
+ import _lib_account_binding
6000
+
6001
+ if binding is None:
6002
+ binding = _lib_account_binding.capture(
6003
+ _cctally_core._resolve_active_claude_identity())
6004
+ if binding is None:
6005
+ eprint("[record-usage] the active Claude identity is unreadable or "
6006
+ "changing; no observation written")
6007
+ return 2
6008
+ return record_usage_bound(
6009
+ args, binding=binding, ingest_mode=ingest_mode, writer=writer,
6010
+ nudge_dashboard=nudge_dashboard, nudge_sink=nudge_sink,
6011
+ publication=publication, appended=appended,
6012
+ expected_high_water=expected_high_water)
6013
+
6014
+
6015
+ def record_usage_bound(
6016
+ args: argparse.Namespace, *,
6017
+ binding,
6018
+ ingest_mode: str = "authoritative",
6019
+ writer: str = "record-usage",
6020
+ nudge_dashboard: bool = True,
6021
+ nudge_sink=None,
6022
+ publication: "dict | None" = None,
6023
+ appended: "list | None" = None,
6024
+ expected_high_water: "tuple | None" = None,
6025
+ ) -> int:
6026
+ """The bound recording kernel (#825/#832 spec 4.7).
6027
+
6028
+ ``binding`` is required and is the ONLY source of the observation's
6029
+ account: this kernel never resolves the active identity. The account
6030
+ observation (when one is due) and the usage observation carry the same
6031
+ binding, and the account observation precedes the usage observation.
6032
+
6033
+ ``publication`` is a frozen statusline plan: ``{"publicationId",
6034
+ "capturedAt", "fiveHourWindowKey", "provenance"}``. Its captured instant
6035
+ replaces both clocks, so repeating an uncommitted plan rebuilds the SAME
6036
+ observation (same content id) instead of a new confirmation with a new
6037
+ receipt time; its planned five-hour window key is the account-local anchor
6038
+ the reducer chose (spec 4.4). ``five_hour_window_key`` on ``args`` carries
6039
+ the same for an OAuth publication.
6040
+
6041
+ ``expected_high_water`` (#825/#832 fix-round-1 Q3) binds the append to the
6042
+ journal coordinate a statusline drop gate proved covered: the account
6043
+ observation (when due) and the usage observation are appended as ONE group
6044
+ under the journal leaf lock only if the journal still ends exactly there,
6045
+ else `JournalError` is raised and nothing is written.
6046
+
6047
+ Record usage from the Claude Code status line rate_limits — DB journal
5728
6048
  redesign reroute (Appendix A).
5729
6049
 
5730
6050
  Validates the ingress (weekly plausibility -> exit 2; out-of-band 5h dropped
@@ -5769,7 +6089,15 @@ def cmd_record_usage(
5769
6089
  # OverflowError. Reject path returns exit 2 so
5770
6090
  # _refresh_usage_inproc maps it to status="record_failed" instead
5771
6091
  # of silently reporting success on a dropped payload.
5772
- now_dt = _command_as_of()
6092
+ import _lib_account_binding
6093
+
6094
+ # Re-validate the immutable envelope before any accounting mutation.
6095
+ binding = _lib_account_binding.load(binding.document())
6096
+ if publication is not None:
6097
+ now_dt = dt.datetime.fromtimestamp(
6098
+ int(publication["capturedAt"]), tz=dt.timezone.utc)
6099
+ else:
6100
+ now_dt = _command_as_of()
5773
6101
  now_epoch = int(now_dt.timestamp())
5774
6102
  if not check_resets_at_plausibility(
5775
6103
  resets_at, now_epoch,
@@ -5834,7 +6162,9 @@ def cmd_record_usage(
5834
6162
  # production. The harness-only pin lets deterministic CLI scenarios model a
5835
6163
  # genuinely open historical block without pretending a years-old reset is
5836
6164
  # current wall time.
5837
- capture_now = now_dt if os.environ.get("CCTALLY_TEST_PIN_CAPTURE") else None
6165
+ capture_now = (
6166
+ now_dt if (publication is not None
6167
+ or os.environ.get("CCTALLY_TEST_PIN_CAPTURE")) else None)
5838
6168
  raw: dict[str, Any] = {
5839
6169
  "weekly_percent": weekly_percent,
5840
6170
  "resets_at": resets_at,
@@ -5852,9 +6182,31 @@ def cmd_record_usage(
5852
6182
  raw["five_hour_percent"] = five_hour_percent
5853
6183
  if five_hour_resets_at_str is not None:
5854
6184
  raw["five_hour_resets_at"] = five_hour_resets_at_str
6185
+ # #825/#832 spec 4.7 / 5.1: the binding and the plan's stable publication
6186
+ # identity live in the RAW observation payload (never in a known event
6187
+ # payload). The previous release reads this payload by `.get`, so the extra
6188
+ # keys are inert to it.
6189
+ raw["account_binding"] = binding.document()
6190
+ planned_key = getattr(args, "five_hour_window_key", None)
6191
+ if publication is not None:
6192
+ raw["publication_id"] = publication["publicationId"]
6193
+ raw["publication"] = publication.get("provenance") or {}
6194
+ planned_key = publication.get("fiveHourWindowKey")
6195
+ if planned_key is not None and five_hour_resets_at_str is not None:
6196
+ # The account-local anchor the producer chose (spec 4.4); ingest
6197
+ # validates it against the raw reset before using it.
6198
+ raw["five_hour_window_key"] = int(planned_key)
5855
6199
 
5856
6200
  import _cctally_journal as _jr
5857
6201
  import _lib_journal as _lj
6202
+ # #825/#832 spec 4.7 revision 6: the fold policy this observation is
6203
+ # decided under. A frozen plan keeps the revision it was frozen with, so
6204
+ # a plan frozen before revision 6 publishes an unversioned observation
6205
+ # (the historical policy) and its recovery stays byte-identical.
6206
+ if publication is None:
6207
+ raw[_jr.FOLD_POLICY_KEY] = _jr.CURRENT_FOLD_POLICY
6208
+ elif _jr.PLAN_FOLD_POLICY_KEY in publication:
6209
+ raw[_jr.FOLD_POLICY_KEY] = publication[_jr.PLAN_FOLD_POLICY_KEY]
5858
6210
  import _lib_accounts
5859
6211
  # Observe-and-stamp (#341, spec §1): resolve the active Claude account from
5860
6212
  # ~/.claude.json (stable-read, mtime-cached) and stamp the usage obs with it.
@@ -5863,18 +6215,34 @@ def cmd_record_usage(
5863
6215
  # legacy install's journal obs stays byte-identical to today (the pipeline's
5864
6216
  # `rec.get("account") or UNATTRIBUTED` fallback treats both the same).
5865
6217
  obs_at = now_utc_iso(now_dt)
5866
- identity = _cctally_core._resolve_active_claude_identity()
5867
- account_key = identity["account_key"]
6218
+ account_key = binding.account_key
5868
6219
  obs_account = None if account_key == _lib_accounts.UNATTRIBUTED else account_key
6220
+ observe = None
5869
6221
  if obs_account is not None:
5870
6222
  # Journal the account_observe DURABLY BEFORE the account-stamped usage obs
5871
6223
  # (spec §1: replay can never see a stamped row whose account was never
5872
- # observed). First-sight/identity-change only — marker-deduped so it is
5873
- # not appended every tick.
5874
- _maybe_append_account_observe(identity, at=obs_at)
5875
- _jr.append_record(_lj.make_obs(
6224
+ # observed). First-sight/identity-change only — deduplicated on the
6225
+ # binding's full identity evidence, never on an account-name marker.
6226
+ if expected_high_water is None:
6227
+ _maybe_append_account_observe(binding, at=obs_at)
6228
+ else:
6229
+ observe = _due_account_observe(binding, at=obs_at)
6230
+ obs = _lj.prepare_record(_lj.make_obs(
5876
6231
  at=obs_at, src=writer, provider="claude", payload=raw,
5877
6232
  account=obs_account))
6233
+ if expected_high_water is None:
6234
+ _jr.append_record(obs)
6235
+ else:
6236
+ _jr.append_records(
6237
+ [obs] if observe is None else [observe, obs],
6238
+ expected_high_water=(str(expected_high_water[0]),
6239
+ int(expected_high_water[1])))
6240
+ if observe is not None:
6241
+ _mark_account_observed(binding)
6242
+ if appended is not None:
6243
+ # #825/#832 spec 5.6 item 7: the authoritative OAuth writer records
6244
+ # this observation's identity as its control commit.
6245
+ appended.append(obs["id"])
5878
6246
  # authoritative observes its own write synchronously; opportunistic skips a
5879
6247
  # busy ingest lock and lets the current holder consume the appended line.
5880
6248
  result = _jr.run_stats_ingest(mode=ingest_mode)
@@ -5898,33 +6266,65 @@ def cmd_record_usage(
5898
6266
  return 0
5899
6267
 
5900
6268
 
5901
- _OBSERVED_ACCOUNT_MARKER = "observed-claude-account"
6269
+ def _account_observe_marker_path(account_key: str):
6270
+ """The current-protocol dedup marker of one account (#825/#832 F10). It
6271
+ lives under the account-bound root; the previous release's
6272
+ `observed-claude-account` marker is never read, so a legacy marker can
6273
+ never suppress current account evidence."""
6274
+ return (_cctally_core.STATUSLINE_ACCOUNT_BOUND_DIR / "accounts" / account_key
6275
+ / "account-observed.json")
5902
6276
 
5903
6277
 
5904
- def _maybe_append_account_observe(identity: dict, *, at: str) -> None:
6278
+ def _maybe_append_account_observe(binding, *, at: str) -> None:
5905
6279
  """Append an ``account_observe`` op on first sight of a Claude account or an
5906
- identity change (#341, spec §1) — NOT every tick. Deduped by a marker file in
5907
- APP_DIR so a hot status-line loop journals at most one observe per account.
5908
- Best-effort: a marker/journal hiccup never breaks record-usage."""
6280
+ identity change (#341, spec §1) — NOT every tick.
6281
+
6282
+ Deduplicated by the binding's FULL identity evidence (#825/#832 F10): the
6283
+ same account key with a changed email or plan is a new observation. The
6284
+ marker is written only after the op is durable, so a crash re-appends
6285
+ rather than skips. A marker failure never breaks recording; a journal
6286
+ failure propagates, because the usage observation must not precede its
6287
+ account evidence."""
6288
+ observe = _due_account_observe(binding, at=at)
6289
+ if observe is None:
6290
+ return
6291
+ import _cctally_journal as _jr
6292
+ _jr.append_record(observe)
6293
+ _mark_account_observed(binding)
6294
+
6295
+
6296
+ def _due_account_observe(binding, *, at: str) -> "dict | None":
6297
+ """The ``account_observe`` op ``binding`` is due (first sight or an
6298
+ identity change), or None. Appending it is the caller's job, and so is
6299
+ `_mark_account_observed` once it is durable."""
5909
6300
  import _lib_accounts
5910
- account_key = identity.get("account_key")
6301
+ account_key = binding.account_key
5911
6302
  if not account_key or account_key == _lib_accounts.UNATTRIBUTED:
5912
- return
5913
- marker = _cctally_core.APP_DIR / _OBSERVED_ACCOUNT_MARKER
6303
+ return None
6304
+ marker = _account_observe_marker_path(account_key)
5914
6305
  try:
5915
- last = marker.read_text().strip()
5916
- except OSError:
6306
+ last = json.loads(marker.read_text()).get("identityEvidence")
6307
+ except (OSError, ValueError, AttributeError):
5917
6308
  last = None
5918
- if last == account_key:
5919
- return
6309
+ if last == binding.identity_evidence:
6310
+ return None
6311
+ import _lib_journal as _lj
6312
+ return _lj.make_account_observe(
6313
+ at=at, account_key=account_key, provider="claude",
6314
+ natural_id=binding.natural_id, email=binding.email,
6315
+ plan_type=binding.plan_type, label_source="auto")
6316
+
6317
+
6318
+ def _mark_account_observed(binding) -> None:
6319
+ """Record that ``binding``'s account evidence is durable. A marker failure
6320
+ never breaks recording (the op is re-appended next time)."""
6321
+ marker = _account_observe_marker_path(binding.account_key)
5920
6322
  try:
5921
- import _cctally_journal as _jr
5922
- import _lib_journal as _lj
5923
- _jr.append_record(_lj.make_account_observe(
5924
- at=at, account_key=account_key, provider="claude",
5925
- natural_id=identity.get("natural_id"), email=identity.get("email"),
5926
- plan_type=identity.get("plan_type"), label_source="auto"))
5927
- marker.write_text(account_key + "\n")
6323
+ marker.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
6324
+ tmp = marker.with_name(marker.name + f".{os.getpid()}.tmp")
6325
+ tmp.write_text(json.dumps(
6326
+ {"identityEvidence": binding.identity_evidence}) + "\n")
6327
+ os.replace(tmp, marker)
5928
6328
  except OSError:
5929
6329
  pass
5930
6330
 
@@ -6015,34 +6415,64 @@ def _marker_age(path) -> float:
6015
6415
 
6016
6416
  def _touch_marker(path) -> None:
6017
6417
  try:
6018
- _cctally_core.APP_DIR.mkdir(parents=True, exist_ok=True)
6418
+ path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
6019
6419
  path.touch(exist_ok=True)
6020
6420
  os.utime(path, None)
6021
6421
  except OSError:
6022
6422
  pass
6023
6423
 
6024
6424
 
6025
- def _statusline_observe_age_seconds() -> float:
6026
- """Seconds since selected/authoritative usage changed; +inf if never."""
6027
- return _marker_age(_cctally_core.STATUSLINE_OBSERVE_MARKER_PATH)
6425
+ def _bound_marker_binding(binding):
6426
+ """The binding a freshness marker belongs to: the caller's captured one,
6427
+ else one captured now (a torn identity has no marker)."""
6428
+ if binding is not None:
6429
+ return binding
6430
+ import _lib_account_binding
6431
+ try:
6432
+ return _lib_account_binding.capture(
6433
+ _cctally_core._resolve_active_claude_identity())
6434
+ except Exception:
6435
+ return None
6436
+
6437
+
6438
+ def _statusline_observe_path(binding):
6439
+ """#825/#832 section 7: selected freshness is per exact account, under the
6440
+ account-bound root. The previous release's observe marker is never
6441
+ touched or read."""
6442
+ return (_cctally_core.STATUSLINE_ACCOUNT_BOUND_DIR / "accounts"
6443
+ / binding.account_key / "observe.last")
6028
6444
 
6029
6445
 
6030
- def _statusline_observe_touch() -> None:
6031
- """Mark selected usage freshness after a proven selected transition.
6446
+ def _statusline_observe_age_seconds(*, binding=None) -> float:
6447
+ """Seconds since this account's selected/authoritative usage changed;
6448
+ +inf if never (or when the identity is torn)."""
6449
+ binding = _bound_marker_binding(binding)
6450
+ if binding is None:
6451
+ return float("inf")
6452
+ return _marker_age(_statusline_observe_path(binding))
6032
6453
 
6033
- The historical public helper names remain aliases for selected freshness.
6034
- """
6035
- _touch_marker(_cctally_core.STATUSLINE_OBSERVE_MARKER_PATH)
6454
+
6455
+ def _statusline_observe_touch(*, binding=None) -> None:
6456
+ """Mark the bound account's selected usage freshness after a proven
6457
+ selected transition. Only that account's marker changes."""
6458
+ binding = _bound_marker_binding(binding)
6459
+ if binding is None:
6460
+ return
6461
+ _touch_marker(_statusline_observe_path(binding))
6462
+
6463
+
6464
+ def _statusline_transport_path():
6465
+ return _cctally_core.STATUSLINE_ACCOUNT_BOUND_DIR / "transport.last"
6036
6466
 
6037
6467
 
6038
6468
  def _statusline_transport_age_seconds() -> float:
6039
6469
  """Seconds since an eligible regular-pool candidate reached the spool."""
6040
- return _marker_age(_cctally_core.STATUSLINE_TRANSPORT_MARKER_PATH)
6470
+ return _marker_age(_statusline_transport_path())
6041
6471
 
6042
6472
 
6043
6473
  def _statusline_transport_touch() -> None:
6044
6474
  """Mark regular-pool statusline transport after an atomic candidate write."""
6045
- _touch_marker(_cctally_core.STATUSLINE_TRANSPORT_MARKER_PATH)
6475
+ _touch_marker(_statusline_transport_path())
6046
6476
 
6047
6477
 
6048
6478
  def _oauth_backoff_remaining_seconds() -> float:
@@ -6300,7 +6730,7 @@ def _codex_lifecycle_log_line(
6300
6730
  *, source_root_key: str, event: str, sync: str, result: str,
6301
6731
  blocks: int, milestones: int, alert_eligible_roots: int,
6302
6732
  quota_alerts: int, budget_alerts: int, dur_ms: int, backlog: int = 0,
6303
- error: str = "",
6733
+ error: str = "", drain_request: str = "",
6304
6734
  ) -> str:
6305
6735
  """Render one privacy-safe root-qualified Codex lifecycle outcome.
6306
6736
 
@@ -6328,11 +6758,11 @@ def _codex_lifecycle_log_line(
6328
6758
  safe_event = "".join(
6329
6759
  char for char in str(event)[:40] if char.isalnum() or char in "-_"
6330
6760
  ) or "unknown"
6331
- suffix = ""
6761
+ suffix = f" drain_request={drain_request}" if drain_request else ""
6332
6762
  if error:
6333
6763
  collapsed = _hook_log_safe_free_text(error)
6334
6764
  if collapsed:
6335
- suffix = f" error={collapsed}"
6765
+ suffix += f" error={collapsed}"
6336
6766
  return (
6337
6767
  f"{now_utc_iso()} provider=codex source_root_key={source_root_key} "
6338
6768
  f"event={safe_event} sync={sync} blocks={int(blocks)} "
@@ -6390,7 +6820,7 @@ def _cmd_hook_tick_codex(
6390
6820
 
6391
6821
  def log_outcome(
6392
6822
  *, sync: str, result: str, projection=None, budget_alerts: int = 0,
6393
- backlog: int = 0, error: str = "",
6823
+ backlog: int = 0, error: str = "", drain_request: str = "",
6394
6824
  ) -> None:
6395
6825
  blocks = int(getattr(projection, "blocks_upserted", 0) or 0)
6396
6826
  milestones = int(getattr(projection, "milestones_upserted", 0) or 0)
@@ -6410,6 +6840,7 @@ def _cmd_hook_tick_codex(
6410
6840
  backlog=backlog,
6411
6841
  dur_ms=dur_ms,
6412
6842
  error=error,
6843
+ drain_request=drain_request,
6413
6844
  ))
6414
6845
  _hook_tick_log_rotate_if_needed()
6415
6846
 
@@ -6464,7 +6895,15 @@ def _cmd_hook_tick_codex(
6464
6895
  finally:
6465
6896
  cache.close()
6466
6897
  if stats.lock_contended:
6467
- log_outcome(sync="contended", result="noop")
6898
+ # #968 spec 5.7: publish this hook's own waiting request so
6899
+ # drain workers yield once their admitted work commits. The
6900
+ # hook never waits and never touches the drain gate.
6901
+ import _lib_codex_drain_admission as _drain_adm
6902
+ request_id = _drain_adm.publish_waiting_request(
6903
+ _cctally_core.APP_DIR, root_keys=all_root_keys)
6904
+ log_outcome(
6905
+ sync="contended", result="noop",
6906
+ drain_request="published" if request_id else "failed")
6468
6907
  return 0
6469
6908
  projection = c.reconcile_codex_quota_projection(
6470
6909
  source_root_keys=all_root_keys,
@@ -6596,6 +7035,16 @@ def _record_dashboard_activity(provider: str, transcript_path: str) -> None:
6596
7035
  pass
6597
7036
 
6598
7037
 
7038
+ def _finalize_wal_checkpoints() -> None:
7039
+ """The W9 finalizer before a detached worker's ``os._exit`` (#901 Q14)."""
7040
+ try:
7041
+ import _lib_wal_checkpoint
7042
+
7043
+ _lib_wal_checkpoint.finalize()
7044
+ except Exception: # noqa: BLE001 — a hook must exit 0 whatever happens
7045
+ pass
7046
+
7047
+
6599
7048
  def cmd_hook_tick(args: argparse.Namespace) -> int:
6600
7049
  """Per-fire hook runtime (Section 3 of onboarding spec).
6601
7050
 
@@ -6831,6 +7280,10 @@ def cmd_hook_tick(args: argparse.Namespace) -> int:
6831
7280
  # Forked child: skip Python's atexit / argparse / cleanup paths
6832
7281
  # (they may try to flush already-redirected stdio handles).
6833
7282
  if forked:
7283
+ # #901 W9 (Q14): `os._exit` skips `atexit`, so the checkpoint
7284
+ # policy's finalizer runs here: one PASSIVE per armed store, then
7285
+ # the keepers close (the last close drains the WAL).
7286
+ _finalize_wal_checkpoints()
6834
7287
  os._exit(0)
6835
7288
  return 0
6836
7289
  # --explain mapping (Section 3 of spec)
@@ -7179,6 +7632,14 @@ def _saved_dict_from_usage_row(row: sqlite3.Row) -> dict[str, Any]:
7179
7632
  out["fiveHourResetsAt"] = row["five_hour_resets_at"]
7180
7633
  if row["five_hour_window_key"] is not None:
7181
7634
  out["fiveHourWindowKey"] = int(row["five_hour_window_key"])
7635
+ # Spec 4.7 revision 7 *Held semantics* (#825/#832 fix round K16b): a
7636
+ # held row's weekly value is not an accepted reading. It is either the
7637
+ # basis a held write carried or the retired value a weekly credit's
7638
+ # cleanup kept beside its five-hour evidence, so the self-heal must not
7639
+ # derive a weekly crossing from it (`maybe_record_milestone`). Mirrors
7640
+ # the fresh held write's `out_payload` key.
7641
+ if "weekly_observation_held" in row.keys() and row["weekly_observation_held"]:
7642
+ out["weeklyObservationHeld"] = True
7182
7643
  return out
7183
7644
 
7184
7645
 
@@ -7203,7 +7664,7 @@ _CLAUDE_OBS_SRCS = frozenset(
7203
7664
  )
7204
7665
 
7205
7666
 
7206
- def _derive_5h_window_key(conn, five_hour_resets_at_epoch):
7667
+ def _derive_5h_window_key(conn, five_hour_resets_at_epoch, *, account_key):
7207
7668
  """Resolve the canonical 5h window key for a raw `five_hour_resets_at` epoch,
7208
7669
  on `conn` (extracted from cmd_record_usage's Tier 1/2/3 prior-anchor logic).
7209
7670
 
@@ -7212,7 +7673,11 @@ def _derive_5h_window_key(conn, five_hour_resets_at_epoch):
7212
7673
  600s floor. Passing the prior anchor collapses boundary-straddling
7213
7674
  seconds-jitter to the first-seen key (spec 5h invariant #3). Runs at ingest
7214
7675
  (not capture) now, so the key is derived against the same DB state the fold
7215
- decision + clamp read."""
7676
+ decision + clamp read.
7677
+
7678
+ #825/#832 spec 4.4: the anchor is ACCOUNT-LOCAL. Both tiers read only
7679
+ ``account_key``'s own blocks and samples, so A and B keep independent
7680
+ anchors even when their raw resets overlap."""
7216
7681
  c = _cctally()
7217
7682
  prior_5h_epoch = None
7218
7683
  prior_5h_key = None
@@ -7221,11 +7686,13 @@ def _derive_5h_window_key(conn, five_hour_resets_at_epoch):
7221
7686
  """
7222
7687
  SELECT five_hour_window_key, five_hour_resets_at
7223
7688
  FROM five_hour_blocks
7224
- WHERE abs(? - CAST(strftime('%s', five_hour_resets_at) AS INTEGER)) <= ?
7689
+ WHERE account_key = ?
7690
+ AND abs(? - CAST(strftime('%s', five_hour_resets_at) AS INTEGER)) <= ?
7225
7691
  ORDER BY abs(? - CAST(strftime('%s', five_hour_resets_at) AS INTEGER)) ASC
7226
7692
  LIMIT 1
7227
7693
  """,
7228
7694
  (
7695
+ account_key,
7229
7696
  five_hour_resets_at_epoch,
7230
7697
  c._FIVE_HOUR_JITTER_FLOOR_SECONDS * 3,
7231
7698
  five_hour_resets_at_epoch,
@@ -7246,7 +7713,9 @@ def _derive_5h_window_key(conn, five_hour_resets_at_epoch):
7246
7713
  "FROM weekly_usage_snapshots "
7247
7714
  "WHERE five_hour_resets_at IS NOT NULL "
7248
7715
  " AND five_hour_window_key IS NOT NULL "
7249
- "ORDER BY captured_at_utc DESC, id DESC LIMIT 1"
7716
+ " AND account_key = ? "
7717
+ "ORDER BY captured_at_utc DESC, id DESC LIMIT 1",
7718
+ (account_key,),
7250
7719
  ).fetchone()
7251
7720
  if prior_5h_row is not None:
7252
7721
  prior_5h_epoch = int(parse_iso_datetime(
@@ -7464,11 +7933,29 @@ def _pipeline_claude_usage(ctx, rec):
7464
7933
  try:
7465
7934
  fh_epoch = int(parse_iso_datetime(
7466
7935
  five_hour_resets_at_str, "obs.five_hour_resets_at").timestamp())
7467
- five_hour_window_key = _derive_5h_window_key(conn, fh_epoch)
7936
+ five_hour_window_key = _derive_5h_window_key(
7937
+ conn, fh_epoch, account_key=account_key)
7938
+ planned_key = payload.get("five_hour_window_key")
7939
+ if (planned_key is not None
7940
+ and _lib_account_protocol.is_stamped(rec)):
7941
+ # #825/#832 spec 4.4: a current producer journals the
7942
+ # account-local anchor it chose, so replay, rebuild and
7943
+ # rederive select the same window as the live reducer. It must
7944
+ # be a canonical key within the jitter tolerance of the raw
7945
+ # reset; anything else is a corrupt plan, not a window.
7946
+ if (type(planned_key) is not int
7947
+ or abs(planned_key - fh_epoch)
7948
+ > _cctally()._FIVE_HOUR_JITTER_FLOOR_SECONDS * 3):
7949
+ raise ValueError("planned five-hour window key is invalid")
7950
+ five_hour_window_key = planned_key
7468
7951
  except (ValueError, TypeError) as exc:
7469
7952
  eprint(f"[ingest] 5h window-key derivation failed: {exc}")
7470
7953
 
7471
7954
  import _cctally_journal as jr
7955
+ # #825/#832 spec 4.7 revision 6: the fold policy the RAW observation
7956
+ # recorded. Never re-policied: an unversioned observation keeps the
7957
+ # historical decisions under replay and `db rederive`.
7958
+ fold_policy = jr.fold_policy_of(payload)
7472
7959
  reviewed_weekly_basis = None
7473
7960
  if hold_weekly_axis:
7474
7961
  reviewed_weekly_basis = (
@@ -7483,6 +7970,12 @@ def _pipeline_claude_usage(ctx, rec):
7483
7970
  )
7484
7971
 
7485
7972
  # 1. Reset/credit detection (fold into the cycle txn; Design B suppression).
7973
+ # #825/#832 spec 5.6 item 2: the outcome-quarantine gate the ingest fold
7974
+ # decided for this record BEFORE any hook ran (and so before any durable
7975
+ # append). Withheld outcomes take the detector's no-outcome branch;
7976
+ # ordinary recording below continues (C3).
7977
+ import _cctally_quarantine
7978
+ gate = _cctally_quarantine.quarantine_gate(ctx, account_key)
7486
7979
  c.detect_reset_and_credit(
7487
7980
  conn,
7488
7981
  week_start_date=week_start_date,
@@ -7507,6 +8000,8 @@ def _pipeline_claude_usage(ctx, rec):
7507
8000
  # confirmation. `payload.source`, never `rec["src"]`.
7508
8001
  source=source,
7509
8002
  hold_weekly_axis=hold_weekly_axis,
8003
+ outcomes_allowed=gate.allowed,
8004
+ fold_policy=fold_policy,
7510
8005
  )
7511
8006
 
7512
8007
  # 2. Accept/skip DECISION (clamp + dedup), made ONCE and journaled via the
@@ -7519,6 +8014,12 @@ def _pipeline_claude_usage(ctx, rec):
7519
8014
  "five_hour_percent": five_hour_percent,
7520
8015
  "five_hour_window_key": five_hour_window_key,
7521
8016
  "account_key": account_key,
8017
+ # #825/#832 spec 4.7 revision 6: the policy this observation was
8018
+ # captured under (missing = historical) and its retained capture
8019
+ # instant, which decides whether it can be an axis's first
8020
+ # post-floor reading.
8021
+ jr.FOLD_POLICY_KEY: fold_policy,
8022
+ "captured_at": capture_at,
7522
8023
  }, force_weekly_hold=hold_weekly_axis,
7523
8024
  reviewed_weekly_basis=reviewed_weekly_basis)
7524
8025
  # #769 S11 (#824). `weekly_held` says the weekly axis is held at the
@@ -7610,6 +8111,7 @@ def _pipeline_claude_usage(ctx, rec):
7610
8111
  if rowid is None:
7611
8112
  return
7612
8113
  saved["id"] = rowid
8114
+ weekly_saved = saved
7613
8115
  if ctx.event_sink is not None and not weekly_held:
7614
8116
  # Keep the most recent accepted weekly source across scratch
7615
8117
  # records. A later five-hour credit may suppress its snapshot
@@ -7633,6 +8135,44 @@ def _pipeline_claude_usage(ctx, rec):
7633
8135
  if latest is None:
7634
8136
  return # nothing recorded yet -> nothing to derive against
7635
8137
  saved = _saved_dict_from_usage_row(latest)
8138
+ # #825/#832 spec 4.7 revision 7. The newest row of the week can be a
8139
+ # retired replica (a five-hour credit cleared its five-hour fields in
8140
+ # place) or a weekly-only reading, and neither carries a five-hour
8141
+ # reading. The five-hour derivation below therefore runs against the
8142
+ # newest five-hour READING under the same week and account, through
8143
+ # the one shared selection, with that snapshot's own identity and
8144
+ # capture.
8145
+ #
8146
+ # The weekly milestone is the same problem on the other axis
8147
+ # (825-832-CD7-V03, fix round K17). The newest row can be one a weekly
8148
+ # credit's cleanup kept weekly-held for its five-hour evidence, and a
8149
+ # held row never establishes a weekly crossing, so deriving from it
8150
+ # recovers nothing: a crossing owed by an accepted reading (its cost
8151
+ # sync failed) would stay lost behind the held row for good. The
8152
+ # weekly milestone therefore derives from the newest UNHELD row of the
8153
+ # week and account, the same selection a held row copies its basis
8154
+ # from; with none, there is no accepted weekly evidence to derive.
8155
+ weekly_id = saved["id"]
8156
+ if saved.get("weeklyObservationHeld"):
8157
+ basis = jr._latest_weekly_basis(
8158
+ conn, account_key, week_start_date=saved_week_start_date)
8159
+ weekly_id = None if basis is None else basis.snapshot_id
8160
+ reading = latest_five_hour_reading(
8161
+ conn, account_key=account_key,
8162
+ week_start_date=saved_week_start_date)
8163
+ five_hour_id = (
8164
+ reading.snapshot_id if reading is not None else saved["id"])
8165
+ by_id = {saved["id"]: saved}
8166
+ for snapshot_id in sorted(
8167
+ {weekly_id, five_hour_id} - {None} - set(by_id)):
8168
+ row = conn.execute(
8169
+ "SELECT * FROM weekly_usage_snapshots WHERE id = ?",
8170
+ (snapshot_id,),
8171
+ ).fetchone()
8172
+ if row is not None:
8173
+ by_id[snapshot_id] = _saved_dict_from_usage_row(row)
8174
+ weekly_saved = by_id.get(weekly_id)
8175
+ five_hour_saved = by_id.get(five_hour_id, saved)
7636
8176
 
7637
8177
  # 4. Derivations run for EVERY line, legacy order (milestone → 5h block → $
7638
8178
  # axes). maybe_record_milestone threads journal=(ctx, rec id) for its cost
@@ -7653,10 +8193,12 @@ def _pipeline_claude_usage(ctx, rec):
7653
8193
  # step 4') genuinely need to run, and on a held tick they are the whole
7654
8194
  # point of the row that was written. Note the gate keys on the AXIS, not
7655
8195
  # on whether a row was written — a held tick writes one and must still
7656
- # suppress the weekly milestone.
7657
- if not weekly_held:
8196
+ # suppress the weekly milestone. On a no-write tick the weekly milestone
8197
+ # derives from the newest UNHELD stored row (`weekly_saved`, K17).
8198
+ if not weekly_held and weekly_saved is not None:
7658
8199
  c.maybe_record_milestone(
7659
- saved, conn=conn, as_of=capture_at, alert_sink=ctx.pending_alerts,
8200
+ weekly_saved, conn=conn, as_of=capture_at,
8201
+ alert_sink=ctx.pending_alerts,
7660
8202
  journal=(ctx, rec["id"]), account_key=account_key,
7661
8203
  retained_selection=c.WeekSelection(
7662
8204
  week_start=dt.date.fromisoformat(week_start_date),
@@ -7676,7 +8218,7 @@ def _pipeline_claude_usage(ctx, rec):
7676
8218
  fold, snapshot_id=saved["id"],
7677
8219
  capture_at=saved["capturedAt"],
7678
8220
  five_hour_resets_at=five_hour_resets_at_str,
7679
- ) if wrote_row else saved,
8221
+ ) if wrote_row else five_hour_saved,
7680
8222
  conn=conn, as_of=capture_at, alert_sink=ctx.pending_alerts,
7681
8223
  account_key=account_key, journal_ctx=ctx)
7682
8224
  # The dollar-decoupled axes resolve a CURRENT budget/period WINDOW from