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.
- package/CHANGELOG.md +47 -0
- package/README.md +4 -6
- package/bin/_cctally_account.py +32 -8
- package/bin/_cctally_cache.py +6035 -722
- package/bin/_cctally_completion.py +1287 -0
- package/bin/_cctally_config.py +37 -0
- package/bin/_cctally_core.py +400 -2
- package/bin/_cctally_dashboard.py +98 -20
- package/bin/_cctally_dashboard_conversation.py +292 -23
- package/bin/_cctally_dashboard_envelope.py +16 -3
- package/bin/_cctally_dashboard_perf.py +128 -9
- package/bin/_cctally_dashboard_sources.py +285 -213
- package/bin/_cctally_db.py +1177 -23
- package/bin/_cctally_diagnosis_sources.py +22 -4
- package/bin/_cctally_doctor.py +572 -45
- package/bin/_cctally_five_hour.py +84 -33
- package/bin/_cctally_forecast.py +108 -15
- package/bin/_cctally_journal.py +3950 -116
- package/bin/_cctally_journal_repair.py +101 -12
- package/bin/_cctally_milestone_history.py +82 -9
- package/bin/_cctally_parser.py +42 -0
- package/bin/_cctally_percent_breakdown.py +3 -2
- package/bin/_cctally_project.py +8 -2
- package/bin/_cctally_quarantine.py +652 -0
- package/bin/_cctally_quota.py +227 -37
- package/bin/_cctally_quota_model.py +360 -83
- package/bin/_cctally_record.py +691 -149
- package/bin/_cctally_rederive.py +320 -41
- package/bin/_cctally_refresh.py +84 -13
- package/bin/_cctally_source_analytics.py +83 -17
- package/bin/_cctally_statusline.py +6154 -517
- package/bin/_cctally_store.py +367 -53
- package/bin/_cctally_transcript.py +8 -7
- package/bin/_cctally_tui.py +61 -56
- package/bin/_cctally_weekrefs.py +128 -21
- package/bin/_lib_account_binding.py +152 -0
- package/bin/_lib_account_protocol.py +386 -0
- package/bin/_lib_aggregators.py +4 -0
- package/bin/_lib_cache_coverage.py +13 -1
- package/bin/_lib_cache_report.py +35 -6
- package/bin/_lib_cache_writer_lock.py +19 -3
- package/bin/_lib_clone_copy.py +142 -0
- package/bin/_lib_codex_conversation.py +32 -0
- package/bin/_lib_codex_conversation_query.py +297 -49
- package/bin/_lib_codex_conversation_watch.py +59 -14
- package/bin/_lib_codex_drain_admission.py +214 -0
- package/bin/_lib_codex_metadata.py +18 -1
- package/bin/_lib_codex_publication_barrier.py +277 -0
- package/bin/_lib_codex_rollout_container.py +502 -0
- package/bin/_lib_conversation_dispatch.py +3 -0
- package/bin/_lib_conversation_query.py +106 -59
- package/bin/_lib_conversation_retention.py +1605 -640
- package/bin/_lib_cost_provenance.py +53 -2
- package/bin/_lib_detector_access_registry.py +3817 -0
- package/bin/_lib_doctor.py +714 -69
- package/bin/_lib_five_hour.py +126 -0
- package/bin/_lib_fmt.py +11 -0
- package/bin/_lib_ingest_frontier.py +414 -20
- package/bin/_lib_journal.py +141 -10
- package/bin/_lib_jsonl.py +5 -4
- package/bin/_lib_legacy_spool.py +126 -0
- package/bin/_lib_legacy_writer_evidence.py +136 -0
- package/bin/_lib_merge_gate.py +231 -86
- package/bin/_lib_meter_rate_change.py +10 -0
- package/bin/_lib_outcome_quarantine.py +690 -0
- package/bin/_lib_perf.py +27 -1
- package/bin/_lib_pricing.py +199 -9
- package/bin/_lib_pricing_check.py +20 -4
- package/bin/_lib_quarantine.py +154 -0
- package/bin/_lib_quota_calibration.py +53 -8
- package/bin/_lib_quota_copy.py +2 -1
- package/bin/_lib_quota_model.py +288 -46
- package/bin/_lib_reclaim_planner.py +792 -0
- package/bin/_lib_rederive.py +319 -82
- package/bin/_lib_segment_summary.py +35 -2
- package/bin/_lib_snapshot_cache.py +21 -0
- package/bin/_lib_sqlite_close.py +81 -0
- package/bin/_lib_statusline_candidates.py +292 -23
- package/bin/_lib_subscription_weeks.py +100 -17
- package/bin/_lib_tick_stats.py +217 -17
- package/bin/_lib_wal_checkpoint.py +1077 -0
- package/bin/_lib_write_budget.py +483 -0
- package/bin/_lib_write_io.py +703 -0
- package/bin/cctally +31 -1
- package/dashboard/static/assets/{ConversationsView-CEZ-BUyi.js → ConversationsView-Bc8LCOAZ.js} +1 -1
- package/dashboard/static/assets/{DoctorModal-CE1oH_Zy.js → DoctorModal-D--qVBwe.js} +1 -1
- package/dashboard/static/assets/ModalRoot-DLPgyAM_.js +1 -0
- package/dashboard/static/assets/{ProjectsDrillPanel-B3DZoCeD.js → ProjectsDrillPanel-z53a4v4M.js} +1 -1
- package/dashboard/static/assets/{SourceDetailModal-CHp9EgLP.js → SourceDetailModal-Bw0T8W-W.js} +1 -1
- package/dashboard/static/assets/{UpdateModal-5iKST6jo.js → UpdateModal-D5AKx4Yw.js} +1 -1
- package/dashboard/static/assets/index-BKITk1CT.css +1 -0
- package/dashboard/static/assets/{index-Cm0VTxJC.js → index-BXDOzSng.js} +3 -3
- package/dashboard/static/assets/{outlineNavigation-DkXGL8J6.js → outlineNavigation-JObY7cUY.js} +1 -1
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +19 -1
- package/dashboard/static/assets/ModalRoot-D5Y8IFUG.js +0 -1
- package/dashboard/static/assets/index-Dt4tqf43.css +0 -1
package/bin/_cctally_record.py
CHANGED
|
@@ -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
|
|
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
|
|
661
|
+
account_key: str,
|
|
575
662
|
) -> int:
|
|
576
|
-
"""
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2575
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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"},
|
|
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,
|
|
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
|
-
"""
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 —
|
|
5873
|
-
#
|
|
5874
|
-
|
|
5875
|
-
|
|
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
|
-
|
|
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(
|
|
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.
|
|
5907
|
-
|
|
5908
|
-
|
|
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 =
|
|
6301
|
+
account_key = binding.account_key
|
|
5911
6302
|
if not account_key or account_key == _lib_accounts.UNATTRIBUTED:
|
|
5912
|
-
return
|
|
5913
|
-
marker =
|
|
6303
|
+
return None
|
|
6304
|
+
marker = _account_observe_marker_path(account_key)
|
|
5914
6305
|
try:
|
|
5915
|
-
last = marker.read_text().
|
|
5916
|
-
except OSError:
|
|
6306
|
+
last = json.loads(marker.read_text()).get("identityEvidence")
|
|
6307
|
+
except (OSError, ValueError, AttributeError):
|
|
5917
6308
|
last = None
|
|
5918
|
-
if last ==
|
|
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
|
-
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
|
|
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
|
-
|
|
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
|
|
6026
|
-
"""
|
|
6027
|
-
|
|
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
|
|
6031
|
-
"""
|
|
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
|
-
|
|
6034
|
-
|
|
6035
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
"
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|