cctally 1.80.4 → 1.81.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 +24 -0
- package/bin/_cctally_account.py +397 -0
- package/bin/_cctally_alerts.py +58 -2
- package/bin/_cctally_cache.py +696 -166
- package/bin/_cctally_cache_report.py +21 -3
- package/bin/_cctally_config.py +119 -8
- package/bin/_cctally_core.py +343 -48
- package/bin/_cctally_dashboard.py +32 -7
- package/bin/_cctally_dashboard_conversation.py +6 -2
- package/bin/_cctally_dashboard_envelope.py +49 -0
- package/bin/_cctally_dashboard_share.py +78 -1
- package/bin/_cctally_dashboard_sources.py +434 -48
- package/bin/_cctally_db.py +659 -152
- package/bin/_cctally_diff.py +9 -0
- package/bin/_cctally_doctor.py +201 -26
- package/bin/_cctally_five_hour.py +110 -69
- package/bin/_cctally_forecast.py +112 -43
- package/bin/_cctally_journal.py +591 -80
- package/bin/_cctally_milestones.py +180 -67
- package/bin/_cctally_parser.py +87 -0
- package/bin/_cctally_percent_breakdown.py +24 -15
- package/bin/_cctally_project.py +12 -1
- package/bin/_cctally_quota.py +150 -39
- package/bin/_cctally_record.py +491 -141
- package/bin/_cctally_reporting.py +66 -14
- package/bin/_cctally_setup.py +9 -1
- package/bin/_cctally_source_analytics.py +56 -7
- package/bin/_cctally_statusline.py +55 -8
- package/bin/_cctally_store.py +20 -8
- package/bin/_cctally_sync_week.py +30 -10
- package/bin/_cctally_tui.py +99 -18
- package/bin/_cctally_weekrefs.py +38 -15
- package/bin/_lib_accounts.py +325 -0
- package/bin/_lib_alerts_payload.py +25 -4
- package/bin/_lib_cache_writer_lock.py +77 -0
- package/bin/_lib_codex_hooks.py +40 -1
- package/bin/_lib_diff_kernel.py +28 -14
- package/bin/_lib_doctor.py +160 -0
- package/bin/_lib_journal.py +65 -2
- package/bin/_lib_quota.py +81 -4
- package/bin/_lib_render.py +19 -5
- package/bin/_lib_share.py +25 -0
- package/bin/_lib_snapshot_cache.py +8 -0
- package/bin/_lib_subscription_weeks.py +16 -2
- package/bin/_lib_view_models.py +18 -9
- package/bin/cctally +43 -4
- package/dashboard/static/assets/index-DJP4gEB7.js +92 -0
- package/dashboard/static/assets/index-Dk1nplOz.css +1 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +4 -1
- package/dashboard/static/assets/index-BzWujBS3.js +0 -92
- package/dashboard/static/assets/index-Cc2ykqF_.css +0 -1
package/bin/_cctally_record.py
CHANGED
|
@@ -201,6 +201,7 @@ from _cctally_core import (
|
|
|
201
201
|
_command_as_of,
|
|
202
202
|
_as_of_or_command,
|
|
203
203
|
)
|
|
204
|
+
import _lib_accounts # pure stdlib kernel; UNATTRIBUTED sentinel default (#341)
|
|
204
205
|
from _lib_five_hour import _canonical_5h_window_key, five_hour_milestone_range
|
|
205
206
|
from _lib_pricing import _calculate_entry_cost
|
|
206
207
|
from _lib_codex_hooks import (
|
|
@@ -446,6 +447,26 @@ def _warn_budget_bad_config_once(*args, **kwargs):
|
|
|
446
447
|
return sys.modules["cctally"]._warn_budget_bad_config_once(*args, **kwargs)
|
|
447
448
|
|
|
448
449
|
|
|
450
|
+
_BUDGET_ANCHOR_WARNED = False
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def _warn_budget_active_anchor_unavailable_once():
|
|
454
|
+
"""One-shot stderr WARN (#341 Step 4-eval, spec §6 `*`-anchor): the Claude
|
|
455
|
+
vendor-wide subscription-week budget ladder was SKIPPED this tick because the
|
|
456
|
+
active-account anchor is genuinely unavailable (a torn `~/.claude.json`
|
|
457
|
+
read). Throttled to once per process so a persistent torn read never spams
|
|
458
|
+
the hot path; the persistent health signal is the doctor `accounts.identity`
|
|
459
|
+
WARN leg."""
|
|
460
|
+
global _BUDGET_ANCHOR_WARNED
|
|
461
|
+
if _BUDGET_ANCHOR_WARNED:
|
|
462
|
+
return
|
|
463
|
+
_BUDGET_ANCHOR_WARNED = True
|
|
464
|
+
eprint(
|
|
465
|
+
"warning: skipping vendor-wide Claude budget alert — the active account "
|
|
466
|
+
"anchor is unavailable (torn ~/.claude.json read); see `cctally doctor`"
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
|
|
449
470
|
def _hook_tick_oauth_refresh(*args, **kwargs):
|
|
450
471
|
"""Shim for ``_hook_tick_oauth_refresh``.
|
|
451
472
|
|
|
@@ -538,9 +559,13 @@ _logged_window_key_coerce_failure = False
|
|
|
538
559
|
def _resolve_active_five_hour_reset_event_id(
|
|
539
560
|
conn: "sqlite3.Connection",
|
|
540
561
|
five_hour_window_key: int,
|
|
562
|
+
*,
|
|
563
|
+
account_key: str = _lib_accounts.UNATTRIBUTED,
|
|
541
564
|
) -> int:
|
|
542
565
|
"""Return ``id`` of the most-recent ``five_hour_reset_events`` row for
|
|
543
|
-
``five_hour_window_key``, else 0 (pre-credit / no-event
|
|
566
|
+
``(account_key, five_hour_window_key)``, else 0 (pre-credit / no-event
|
|
567
|
+
sentinel). ``account_key`` (#341) scopes the active-segment resolution to
|
|
568
|
+
the account so a shared physical window keeps per-account segments.
|
|
544
569
|
|
|
545
570
|
Mirrors the weekly active-segment resolution pattern used by
|
|
546
571
|
``maybe_record_milestone`` for ``percent_milestones.reset_event_id``.
|
|
@@ -565,9 +590,9 @@ def _resolve_active_five_hour_reset_event_id(
|
|
|
565
590
|
try:
|
|
566
591
|
row = conn.execute(
|
|
567
592
|
"SELECT id FROM five_hour_reset_events "
|
|
568
|
-
"WHERE five_hour_window_key = ? "
|
|
593
|
+
"WHERE five_hour_window_key = ? AND account_key = ? "
|
|
569
594
|
"ORDER BY id DESC LIMIT 1",
|
|
570
|
-
(int(five_hour_window_key),),
|
|
595
|
+
(int(five_hour_window_key), account_key),
|
|
571
596
|
).fetchone()
|
|
572
597
|
except sqlite3.DatabaseError:
|
|
573
598
|
return 0
|
|
@@ -583,10 +608,17 @@ def maybe_record_milestone(
|
|
|
583
608
|
as_of: "str | None" = None,
|
|
584
609
|
alert_sink: "list | None" = None,
|
|
585
610
|
journal: "tuple | None" = None,
|
|
611
|
+
account_key: str = _lib_accounts.UNATTRIBUTED,
|
|
586
612
|
) -> None:
|
|
587
613
|
"""Check if a new integer percent threshold was crossed, and if so,
|
|
588
614
|
fetch cost and record the milestone. Errors are logged, not raised.
|
|
589
615
|
|
|
616
|
+
``account_key`` (#341): the account the crossing belongs to. Scopes the
|
|
617
|
+
per-account milestone ledger (max/marginal reads, INSERT, alerted_at
|
|
618
|
+
UPDATE) so two accounts crossing the SAME threshold in the SAME week each
|
|
619
|
+
record independently. Default ``"unattributed"`` is the rev-4.1 defensive
|
|
620
|
+
backstop; the ingest pipeline hook passes the resolved account explicitly.
|
|
621
|
+
|
|
590
622
|
Transaction-neutral / capture-time-pure seam (DB journal redesign §5.2.3):
|
|
591
623
|
when ``conn`` is passed the crossing folds into the caller's transaction —
|
|
592
624
|
no internal ``open_db()``/``commit()``/``close()``, and alert dispatch is
|
|
@@ -646,16 +678,18 @@ def maybe_record_milestone(
|
|
|
646
678
|
"""
|
|
647
679
|
SELECT id FROM week_reset_events
|
|
648
680
|
WHERE new_week_end_at = ?
|
|
681
|
+
AND account_key = ?
|
|
649
682
|
AND unixepoch(effective_reset_at_utc) <= unixepoch(?)
|
|
650
683
|
ORDER BY id DESC LIMIT 1
|
|
651
684
|
""",
|
|
652
|
-
(week_end_at, captured_at_iso),
|
|
685
|
+
(week_end_at, account_key, captured_at_iso),
|
|
653
686
|
).fetchone()
|
|
654
687
|
if seg_row is not None:
|
|
655
688
|
reset_event_id = int(seg_row["id"])
|
|
656
689
|
|
|
657
690
|
max_existing = get_max_milestone_for_week(
|
|
658
691
|
conn, week_start_date, reset_event_id=reset_event_id,
|
|
692
|
+
account_key=account_key,
|
|
659
693
|
)
|
|
660
694
|
if max_existing is not None and current_floor <= max_existing:
|
|
661
695
|
return
|
|
@@ -684,6 +718,9 @@ def maybe_record_milestone(
|
|
|
684
718
|
conn=(None if own_conn else conn),
|
|
685
719
|
as_of=as_of,
|
|
686
720
|
journal=(None if own_conn else journal),
|
|
721
|
+
# Materialize the cost snapshot under the crossing's account
|
|
722
|
+
# (#341 P2-1) so the account-scoped read below finds it.
|
|
723
|
+
account_key=account_key,
|
|
687
724
|
)
|
|
688
725
|
except Exception as exc:
|
|
689
726
|
eprint(f"[milestone] cost sync failed, using latest available: {exc}")
|
|
@@ -715,7 +752,13 @@ def maybe_record_milestone(
|
|
|
715
752
|
cumulative_cost = live_cost
|
|
716
753
|
cost_snapshot_id = 0 # no snapshot row to anchor against
|
|
717
754
|
else:
|
|
718
|
-
|
|
755
|
+
# Account-scoped read (#341 P2-1): the cost snapshot was just
|
|
756
|
+
# materialized under `account_key`, so scope the read to it — the
|
|
757
|
+
# merged (account-blind) read would return another account's row on
|
|
758
|
+
# a multi-account install. Byte-stable at single-account (the sole
|
|
759
|
+
# row is the crossing account's).
|
|
760
|
+
latest_cost = get_latest_cost_for_week(
|
|
761
|
+
conn, week_ref, account_key=account_key)
|
|
719
762
|
if latest_cost is None:
|
|
720
763
|
eprint("[milestone] no cost snapshot yet for this week, skipping")
|
|
721
764
|
return
|
|
@@ -754,6 +797,7 @@ def maybe_record_milestone(
|
|
|
754
797
|
prev_cost = get_milestone_cost_for_week(
|
|
755
798
|
conn, week_start_date, max_existing,
|
|
756
799
|
reset_event_id=reset_event_id,
|
|
800
|
+
account_key=account_key,
|
|
757
801
|
)
|
|
758
802
|
marginal = (cumulative_cost - prev_cost) if prev_cost is not None else None
|
|
759
803
|
else:
|
|
@@ -773,6 +817,7 @@ def maybe_record_milestone(
|
|
|
773
817
|
commit=False,
|
|
774
818
|
reset_event_id=reset_event_id,
|
|
775
819
|
as_of=as_of,
|
|
820
|
+
account_key=account_key,
|
|
776
821
|
)
|
|
777
822
|
# ── Threshold-actions dispatch (set-then-dispatch, spec §3.2) ──
|
|
778
823
|
# Only the genuine-new-crossing winner (rowcount==1) reaches this
|
|
@@ -797,9 +842,10 @@ def maybe_record_milestone(
|
|
|
797
842
|
conn.execute(
|
|
798
843
|
"UPDATE percent_milestones SET alerted_at = ? "
|
|
799
844
|
"WHERE week_start_date = ? AND percent_threshold = ? "
|
|
800
|
-
" AND reset_event_id = ? "
|
|
845
|
+
" AND reset_event_id = ? AND account_key = ? "
|
|
801
846
|
" AND alerted_at IS NULL",
|
|
802
|
-
(crossed_at, week_start_date, pct, reset_event_id
|
|
847
|
+
(crossed_at, week_start_date, pct, reset_event_id,
|
|
848
|
+
account_key),
|
|
803
849
|
)
|
|
804
850
|
# Cheap re-read for payload context (cumulative_cost_usd
|
|
805
851
|
# reflects the value persisted on insert, immune to any
|
|
@@ -811,8 +857,8 @@ def maybe_record_milestone(
|
|
|
811
857
|
row = conn.execute(
|
|
812
858
|
"SELECT cumulative_cost_usd FROM percent_milestones "
|
|
813
859
|
"WHERE week_start_date = ? AND percent_threshold = ? "
|
|
814
|
-
" AND reset_event_id = ?",
|
|
815
|
-
(week_start_date, pct, reset_event_id),
|
|
860
|
+
" AND reset_event_id = ? AND account_key = ?",
|
|
861
|
+
(week_start_date, pct, reset_event_id, account_key),
|
|
816
862
|
).fetchone()
|
|
817
863
|
if row is not None:
|
|
818
864
|
cum = float(row["cumulative_cost_usd"])
|
|
@@ -824,6 +870,7 @@ def maybe_record_milestone(
|
|
|
824
870
|
week_start_date=week_start_date,
|
|
825
871
|
cumulative_cost_usd=cum,
|
|
826
872
|
dollars_per_percent=dpp,
|
|
873
|
+
account_key=account_key,
|
|
827
874
|
)
|
|
828
875
|
pending_alerts.append(payload)
|
|
829
876
|
# Single commit after the loop durably writes every milestone row
|
|
@@ -864,6 +911,7 @@ def maybe_record_milestone(
|
|
|
864
911
|
def _record_budget_milestone_for_vendor(
|
|
865
912
|
*, vendor, target, thresholds, period, config, tz, build_payload,
|
|
866
913
|
raise_errors: bool = False, conn=None, as_of=None, alert_sink=None,
|
|
914
|
+
account_key: str = "*", window_account_key=None,
|
|
867
915
|
) -> int:
|
|
868
916
|
"""Shared budget-milestone firing core for both vendors (#143).
|
|
869
917
|
|
|
@@ -894,6 +942,7 @@ def _record_budget_milestone_for_vendor(
|
|
|
894
942
|
``threshold`` / ``crossed_at_utc`` / ``period_key`` / ``period`` /
|
|
895
943
|
``budget_usd`` / ``spent_usd`` / ``consumption_pct`` keyword args.
|
|
896
944
|
"""
|
|
945
|
+
import _cctally_cache # for the fail-closed AccountAttributionUnavailable skip (#341)
|
|
897
946
|
now_utc = _as_of_or_command(as_of)
|
|
898
947
|
pending_alerts: list[dict[str, Any]] = []
|
|
899
948
|
own_conn = conn is None
|
|
@@ -902,7 +951,7 @@ def _record_budget_milestone_for_vendor(
|
|
|
902
951
|
try:
|
|
903
952
|
start_at = _resolve_budget_window(
|
|
904
953
|
conn, vendor=vendor, now_utc=now_utc, period=period,
|
|
905
|
-
config=config, tz=tz,
|
|
954
|
+
config=config, tz=tz, window_account_key=window_account_key,
|
|
906
955
|
)
|
|
907
956
|
if start_at is None:
|
|
908
957
|
return 0 # no resolvable window yet (claude subscription-week pre-snapshot)
|
|
@@ -911,9 +960,9 @@ def _record_budget_milestone_for_vendor(
|
|
|
911
960
|
present = {
|
|
912
961
|
int(r[0]) for r in conn.execute(
|
|
913
962
|
"SELECT threshold FROM budget_milestones "
|
|
914
|
-
"WHERE vendor = ? AND period_start_at = ? "
|
|
963
|
+
"WHERE vendor = ? AND account_key = ? AND period_start_at = ? "
|
|
915
964
|
" AND (period = ? OR period IS NULL)",
|
|
916
|
-
(vendor, period_key, period),
|
|
965
|
+
(vendor, account_key, period_key, period),
|
|
917
966
|
)
|
|
918
967
|
}
|
|
919
968
|
pending = [t for t in sorted(thresholds) if t not in present]
|
|
@@ -921,7 +970,8 @@ def _record_budget_milestone_for_vendor(
|
|
|
921
970
|
return 0 # nothing left this window → skip the cost SUM
|
|
922
971
|
|
|
923
972
|
spent = _budget_spend_for_vendor(
|
|
924
|
-
conn, vendor=vendor, start_at=start_at, now_utc=now_utc
|
|
973
|
+
conn, vendor=vendor, start_at=start_at, now_utc=now_utc,
|
|
974
|
+
account_key=account_key,
|
|
925
975
|
)
|
|
926
976
|
# Shared INSERT-and-arm core (set-then-dispatch, fire-once via rowcount);
|
|
927
977
|
# commit=False inside, so this conn owns the single durable commit below.
|
|
@@ -935,6 +985,7 @@ def _record_budget_milestone_for_vendor(
|
|
|
935
985
|
spent=spent,
|
|
936
986
|
now_utc=now_utc,
|
|
937
987
|
as_of=as_of,
|
|
988
|
+
account_key=account_key,
|
|
938
989
|
):
|
|
939
990
|
pending_alerts.append(build_payload(
|
|
940
991
|
threshold=t,
|
|
@@ -944,12 +995,26 @@ def _record_budget_milestone_for_vendor(
|
|
|
944
995
|
budget_usd=tg,
|
|
945
996
|
spent_usd=sp,
|
|
946
997
|
consumption_pct=pct,
|
|
998
|
+
account_key=account_key,
|
|
947
999
|
))
|
|
948
1000
|
# Single commit: every INSERT + its alerted_at marker durable together.
|
|
949
1001
|
# On the passed-conn (ingest) path the caller owns the commit + the
|
|
950
1002
|
# post-commit dispatch (the ingester's ALERT_DISPATCHER).
|
|
951
1003
|
if own_conn:
|
|
952
1004
|
conn.commit()
|
|
1005
|
+
except _cctally_cache.AccountAttributionUnavailable as exc:
|
|
1006
|
+
# #341 (Task 4): a per-account ladder whose account-scoped spend read
|
|
1007
|
+
# hit the fail-closed cache guard is SKIPPED this tick — never fired on
|
|
1008
|
+
# wrong/merged data (the mislabel the guard prevents) and, unlike a
|
|
1009
|
+
# generic failure, NEVER re-raised (a transient attribution gap must not
|
|
1010
|
+
# abort the whole ingest cycle). Fires next healthy tick. spec §6
|
|
1011
|
+
# "unresolvable keys ... skipped ... never a crash". `pending_alerts`
|
|
1012
|
+
# is empty at this point (the spend leg precedes any crossing), so no
|
|
1013
|
+
# partial arm leaks. Vendor-wide (`*`) reads pass `account_key=None`
|
|
1014
|
+
# downstream and never trip the guard, so this only guards real ladders.
|
|
1015
|
+
eprint(f"[budget-milestone:{vendor}] account attribution unavailable; "
|
|
1016
|
+
f"skipping the {account_key} ladder this tick ({exc})")
|
|
1017
|
+
return 0
|
|
953
1018
|
except Exception as exc:
|
|
954
1019
|
# Exception discipline (6c-gate P1): passed-conn (ingest) path re-raises
|
|
955
1020
|
# so a failure ABORTS the cycle (invariant ii); this covers both budget
|
|
@@ -1010,31 +1075,32 @@ def maybe_record_budget_milestone(
|
|
|
1010
1075
|
except _BudgetConfigError as exc:
|
|
1011
1076
|
_warn_budget_bad_config_once(exc)
|
|
1012
1077
|
return
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
thresholds = budget_cfg["alert_thresholds"]
|
|
1016
|
-
if not thresholds:
|
|
1078
|
+
thresholds = budget_cfg.get("alert_thresholds")
|
|
1079
|
+
if not thresholds or not budget_cfg.get("alerts_enabled"):
|
|
1017
1080
|
return
|
|
1081
|
+
weekly_usd = budget_cfg.get("weekly_usd")
|
|
1082
|
+
# Per-account budget ladders (#341 Step 4-eval, spec §6): `budget.accounts` is
|
|
1083
|
+
# a {account_key: usd} map (refs normalised to immutable keys at write time).
|
|
1084
|
+
# Each real account fires its OWN ladder over its OWN stamped spend. A Claude
|
|
1085
|
+
# budget can be per-account-ONLY (no vendor-wide `weekly_usd`), so the gate
|
|
1086
|
+
# below no longer requires `weekly_usd`.
|
|
1087
|
+
accounts = budget_cfg.get("accounts") or {}
|
|
1088
|
+
if weekly_usd is None and not accounts:
|
|
1089
|
+
return # neither a vendor-wide nor a per-account budget → nothing to do
|
|
1018
1090
|
# Period generalization (spec §6): subscription-week resolves the snapshot-
|
|
1019
1091
|
# anchored window; a calendar period (calendar-week / calendar-month)
|
|
1020
1092
|
# resolves the window purely from `now` + the period. config/tz are
|
|
1021
1093
|
# resolved once for the calendar branch.
|
|
1022
1094
|
period = budget_cfg.get("period", "subscription-week")
|
|
1023
1095
|
tz = resolve_display_tz(argparse.Namespace(tz=None), config)
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
target=budget_cfg["weekly_usd"],
|
|
1027
|
-
thresholds=thresholds,
|
|
1028
|
-
period=period,
|
|
1029
|
-
config=config,
|
|
1030
|
-
tz=tz,
|
|
1031
|
-
conn=conn,
|
|
1032
|
-
as_of=as_of,
|
|
1033
|
-
alert_sink=alert_sink,
|
|
1096
|
+
|
|
1097
|
+
def _claude_budget_payload(**kw):
|
|
1034
1098
|
# The Claude payload builder takes the legacy `week_start_at=` kwarg
|
|
1035
1099
|
# (its value is the resolved period-start instant, == period_key), so
|
|
1036
1100
|
# the at-fire dispatch id stays byte-stable `budget:<period_start_at>:<t>`.
|
|
1037
|
-
|
|
1101
|
+
# `account_key` (#341) rides through to the alert's [label] prefix + the
|
|
1102
|
+
# `alerts.log` 8th field; `*` for the vendor-wide ladder.
|
|
1103
|
+
return _build_alert_payload_budget(
|
|
1038
1104
|
threshold=kw["threshold"],
|
|
1039
1105
|
crossed_at_utc=kw["crossed_at_utc"],
|
|
1040
1106
|
week_start_at=kw["period_key"],
|
|
@@ -1042,8 +1108,50 @@ def maybe_record_budget_milestone(
|
|
|
1042
1108
|
spent_usd=kw["spent_usd"],
|
|
1043
1109
|
consumption_pct=kw["consumption_pct"],
|
|
1044
1110
|
period=kw["period"],
|
|
1045
|
-
|
|
1046
|
-
|
|
1111
|
+
account_key=kw["account_key"],
|
|
1112
|
+
)
|
|
1113
|
+
|
|
1114
|
+
# Vendor-wide (`*`) ladder — today's semantics (sum across ALL accounts incl.
|
|
1115
|
+
# unattributed, the guaranteed-complete vendor total). Rev-3 `*`-anchor: a
|
|
1116
|
+
# subscription-week vendor budget anchors on the ACTIVE account's week; when
|
|
1117
|
+
# the active identity is genuinely UNAVAILABLE (a TORN `~/.claude.json` read —
|
|
1118
|
+
# NOT a resolved `unattributed` absence) we skip the eval + WARN rather than
|
|
1119
|
+
# guess a wrong anchor (the doctor `accounts.identity` leg surfaces the torn
|
|
1120
|
+
# read persistently). A <=1-real-account install resolves `unattributed`
|
|
1121
|
+
# (stably-absent) and behaves exactly as today (byte-stable).
|
|
1122
|
+
if weekly_usd is not None:
|
|
1123
|
+
skip_vendor_wide = False
|
|
1124
|
+
vendor_window_key = None
|
|
1125
|
+
if period == "subscription-week":
|
|
1126
|
+
ident = _cctally_core._resolve_active_claude_identity()
|
|
1127
|
+
if ident.get("status") == "torn":
|
|
1128
|
+
_warn_budget_active_anchor_unavailable_once()
|
|
1129
|
+
skip_vendor_wide = True
|
|
1130
|
+
else:
|
|
1131
|
+
# #341 (spec §6 rev-4): the vendor-wide (`*`) subscription-week
|
|
1132
|
+
# ladder anchors its PERIOD on the ACTIVE account's week (its
|
|
1133
|
+
# SPEND still sums every account). A <=1-real-account install
|
|
1134
|
+
# resolves `unattributed` → the same window as today (byte-stable).
|
|
1135
|
+
vendor_window_key = ident.get("account_key")
|
|
1136
|
+
if not skip_vendor_wide:
|
|
1137
|
+
_record_budget_milestone_for_vendor(
|
|
1138
|
+
vendor="claude", target=weekly_usd, thresholds=thresholds,
|
|
1139
|
+
period=period, config=config, tz=tz, conn=conn, as_of=as_of,
|
|
1140
|
+
alert_sink=alert_sink, account_key="*",
|
|
1141
|
+
window_account_key=vendor_window_key,
|
|
1142
|
+
build_payload=_claude_budget_payload,
|
|
1143
|
+
)
|
|
1144
|
+
|
|
1145
|
+
# Per-account ladders — one per real account in `budget.accounts`. Each
|
|
1146
|
+
# anchors its period on its OWN account's subscription week (spec §6).
|
|
1147
|
+
for acct_key, acct_usd in accounts.items():
|
|
1148
|
+
_record_budget_milestone_for_vendor(
|
|
1149
|
+
vendor="claude", target=acct_usd, thresholds=thresholds,
|
|
1150
|
+
period=period, config=config, tz=tz, conn=conn, as_of=as_of,
|
|
1151
|
+
alert_sink=alert_sink, account_key=acct_key,
|
|
1152
|
+
window_account_key=acct_key,
|
|
1153
|
+
build_payload=_claude_budget_payload,
|
|
1154
|
+
)
|
|
1047
1155
|
|
|
1048
1156
|
|
|
1049
1157
|
def maybe_record_project_budget_milestone(
|
|
@@ -1093,7 +1201,20 @@ def maybe_record_project_budget_milestone(
|
|
|
1093
1201
|
if own_conn:
|
|
1094
1202
|
conn = open_db()
|
|
1095
1203
|
try:
|
|
1096
|
-
|
|
1204
|
+
# #341 (spec §6 rev-4): project_budget_milestones is a `*`-scoped
|
|
1205
|
+
# subscription-week writer, so it anchors "the current week" on the
|
|
1206
|
+
# ACTIVE account's week under the same skip+WARN rule as the vendor
|
|
1207
|
+
# budget ladder. TORN identity → skip + one-shot WARN (never guess an
|
|
1208
|
+
# anchor); identified/stably-absent → scope the window to that account
|
|
1209
|
+
# (<=1-real-account resolves `unattributed` = byte-stable).
|
|
1210
|
+
pb_window_key = None
|
|
1211
|
+
ident = _cctally_core._resolve_active_claude_identity()
|
|
1212
|
+
if ident.get("status") == "torn":
|
|
1213
|
+
_warn_budget_active_anchor_unavailable_once()
|
|
1214
|
+
return
|
|
1215
|
+
pb_window_key = ident.get("account_key")
|
|
1216
|
+
window = _resolve_current_budget_window(
|
|
1217
|
+
conn, now_utc, account_key=pb_window_key)
|
|
1097
1218
|
if window is None:
|
|
1098
1219
|
return # no resolvable week window yet
|
|
1099
1220
|
week_start_at, _week_end_at = window
|
|
@@ -1272,20 +1393,22 @@ def maybe_record_codex_budget_milestone(
|
|
|
1272
1393
|
return 0
|
|
1273
1394
|
target = codex_cfg.get("amount_usd")
|
|
1274
1395
|
thresholds = codex_cfg.get("alert_thresholds") or []
|
|
1275
|
-
|
|
1396
|
+
# Per-account Codex ladders (#341 Step 4-eval, spec §6): `budget.codex.accounts`
|
|
1397
|
+
# is a {account_key: usd} map. A Codex budget can be per-account-ONLY (no
|
|
1398
|
+
# vendor-wide `amount_usd`), so the gate accepts either a vendor-wide amount OR
|
|
1399
|
+
# a non-empty per-account map. Codex is calendar-anchored (no subscription
|
|
1400
|
+
# week -> no `*`-anchor rule).
|
|
1401
|
+
accounts = codex_cfg.get("accounts") or {}
|
|
1402
|
+
if not thresholds or (target is None and not accounts):
|
|
1276
1403
|
return 0
|
|
1277
1404
|
tz = resolve_display_tz(argparse.Namespace(tz=None), config)
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
target=target,
|
|
1281
|
-
thresholds=thresholds,
|
|
1282
|
-
period=codex_cfg["period"],
|
|
1283
|
-
config=config,
|
|
1284
|
-
tz=tz,
|
|
1405
|
+
|
|
1406
|
+
def _codex_budget_payload(**kw):
|
|
1285
1407
|
# The Codex payload builder takes `period_start_at=` directly (== the
|
|
1286
1408
|
# resolved period-start instant, == period_key), so the at-fire dispatch
|
|
1287
1409
|
# id stays byte-stable `codex_budget:<period_start_at>:<threshold>`.
|
|
1288
|
-
|
|
1410
|
+
# `account_key` (#341) rides through to the [label] prefix + log field.
|
|
1411
|
+
return _build_alert_payload_codex_budget(
|
|
1289
1412
|
threshold=kw["threshold"],
|
|
1290
1413
|
crossed_at_utc=kw["crossed_at_utc"],
|
|
1291
1414
|
period_start_at=kw["period_key"],
|
|
@@ -1293,12 +1416,27 @@ def maybe_record_codex_budget_milestone(
|
|
|
1293
1416
|
budget_usd=kw["budget_usd"],
|
|
1294
1417
|
spent_usd=kw["spent_usd"],
|
|
1295
1418
|
consumption_pct=kw["consumption_pct"],
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1419
|
+
account_key=kw["account_key"],
|
|
1420
|
+
)
|
|
1421
|
+
|
|
1422
|
+
fired = 0
|
|
1423
|
+
if target is not None:
|
|
1424
|
+
fired += _record_budget_milestone_for_vendor(
|
|
1425
|
+
vendor="codex", target=target, thresholds=thresholds,
|
|
1426
|
+
period=codex_cfg["period"], config=config, tz=tz,
|
|
1427
|
+
build_payload=_codex_budget_payload, account_key="*",
|
|
1428
|
+
raise_errors=raise_errors, conn=conn, as_of=as_of,
|
|
1429
|
+
alert_sink=alert_sink,
|
|
1430
|
+
) or 0
|
|
1431
|
+
for acct_key, acct_usd in accounts.items():
|
|
1432
|
+
fired += _record_budget_milestone_for_vendor(
|
|
1433
|
+
vendor="codex", target=acct_usd, thresholds=thresholds,
|
|
1434
|
+
period=codex_cfg["period"], config=config, tz=tz,
|
|
1435
|
+
build_payload=_codex_budget_payload, account_key=acct_key,
|
|
1436
|
+
raise_errors=raise_errors, conn=conn, as_of=as_of,
|
|
1437
|
+
alert_sink=alert_sink,
|
|
1438
|
+
) or 0
|
|
1439
|
+
return fired
|
|
1302
1440
|
|
|
1303
1441
|
|
|
1304
1442
|
def _weekly_pct_week_avg_projection(conn, now_utc):
|
|
@@ -1510,13 +1648,29 @@ def maybe_record_projected_alert(
|
|
|
1510
1648
|
sorted(set(int(t) for t in budget_cfg["alert_thresholds"]))
|
|
1511
1649
|
)
|
|
1512
1650
|
claude_period = budget_cfg.get("period", "subscription-week")
|
|
1651
|
+
# #341 (spec §6 rev-4): the vendor-budget projected metric is a
|
|
1652
|
+
# `*`-scoped subscription-week writer, so it anchors on the ACTIVE
|
|
1653
|
+
# account's week under the same skip+WARN rule. TORN identity → skip
|
|
1654
|
+
# ONLY this leg (the weekly_pct + codex legs still run); identified /
|
|
1655
|
+
# stably-absent → scope the window (byte-stable at <=1 real account).
|
|
1656
|
+
# Calendar periods ignore the key (pure calendar).
|
|
1657
|
+
bu_window_key = None
|
|
1658
|
+
bu_leg_ok = True
|
|
1659
|
+
if claude_period == "subscription-week":
|
|
1660
|
+
_bu_ident = _cctally_core._resolve_active_claude_identity()
|
|
1661
|
+
if _bu_ident.get("status") == "torn":
|
|
1662
|
+
_warn_budget_active_anchor_unavailable_once()
|
|
1663
|
+
bu_leg_ok = False
|
|
1664
|
+
else:
|
|
1665
|
+
bu_window_key = _bu_ident.get("account_key")
|
|
1513
1666
|
# Resolve the window key CHEAPLY first (SUM-free, same resolver the
|
|
1514
1667
|
# actual-budget axis uses) so the pre-probe can short-circuit BEFORE
|
|
1515
1668
|
# _build_vendor_budget_inputs runs any cost SUM / cache sync — the
|
|
1516
1669
|
# pre-probe-runs-first contract (spec §3.4; mirrors the actual axis).
|
|
1517
1670
|
window = _resolve_claude_budget_window(
|
|
1518
|
-
conn, now_utc, period=claude_period, config=cfg, tz=config_tz
|
|
1519
|
-
|
|
1671
|
+
conn, now_utc, period=claude_period, config=cfg, tz=config_tz,
|
|
1672
|
+
window_account_key=bu_window_key,
|
|
1673
|
+
) if bu_leg_ok else None
|
|
1520
1674
|
if window is not None and thresholds:
|
|
1521
1675
|
b_ws_at, _b_we_at = window
|
|
1522
1676
|
b_week_key = b_ws_at.isoformat(timespec="seconds")
|
|
@@ -1530,6 +1684,7 @@ def maybe_record_projected_alert(
|
|
|
1530
1684
|
vendor="claude", period=claude_period, target_usd=target,
|
|
1531
1685
|
alert_thresholds=thresholds, now_utc=now_utc, config=cfg,
|
|
1532
1686
|
tz=config_tz, skip_sync=True,
|
|
1687
|
+
window_account_key=bu_window_key,
|
|
1533
1688
|
)
|
|
1534
1689
|
if inputs is not None:
|
|
1535
1690
|
status = compute_budget_status(inputs)
|
|
@@ -1758,12 +1913,19 @@ def _compute_block_totals(
|
|
|
1758
1913
|
|
|
1759
1914
|
def maybe_update_five_hour_block(
|
|
1760
1915
|
saved: dict[str, Any], *, conn=None, as_of: "str | None" = None,
|
|
1761
|
-
alert_sink: "list | None" = None,
|
|
1916
|
+
alert_sink: "list | None" = None, account_key: str = _lib_accounts.UNATTRIBUTED,
|
|
1762
1917
|
) -> None:
|
|
1763
1918
|
"""Upsert the current 5h block in five_hour_blocks; close strictly
|
|
1764
1919
|
older open blocks; sweep naturally-expired blocks; flag blocks
|
|
1765
1920
|
spanning a recorded mid-week 7d-reset.
|
|
1766
1921
|
|
|
1922
|
+
``account_key`` (#341): the account the 5h block belongs to. Every
|
|
1923
|
+
block/child/milestone query below is scoped to it (composite
|
|
1924
|
+
``(account_key, five_hour_window_key)`` identity — spec review finding 3) so
|
|
1925
|
+
two accounts observing the SAME physical 5h window each own their own block
|
|
1926
|
+
and children. Default ``"unattributed"`` is the rev-4.1 defensive backstop;
|
|
1927
|
+
the ingest pipeline hook passes the resolved account explicitly.
|
|
1928
|
+
|
|
1767
1929
|
Errors are logged and swallowed — record-usage must not regress
|
|
1768
1930
|
because of this helper, same posture as maybe_record_milestone.
|
|
1769
1931
|
|
|
@@ -1810,8 +1972,9 @@ def maybe_update_five_hour_block(
|
|
|
1810
1972
|
block_start_at AS block_start_at
|
|
1811
1973
|
FROM five_hour_blocks
|
|
1812
1974
|
WHERE five_hour_window_key = ?
|
|
1975
|
+
AND account_key = ?
|
|
1813
1976
|
""",
|
|
1814
|
-
(int(five_hour_window_key),),
|
|
1977
|
+
(int(five_hour_window_key), account_key),
|
|
1815
1978
|
).fetchone()
|
|
1816
1979
|
|
|
1817
1980
|
# Whole-table emptiness BEFORE this tick's upsert — the exact trigger of
|
|
@@ -1822,7 +1985,8 @@ def maybe_update_five_hour_block(
|
|
|
1822
1985
|
# block. Gating on this (not just `resets < now`) keeps a strictly-newer
|
|
1823
1986
|
# historical block open — the backfill never touched a non-first insert.
|
|
1824
1987
|
blocks_were_empty = conn.execute(
|
|
1825
|
-
"SELECT COUNT(*) FROM five_hour_blocks"
|
|
1988
|
+
"SELECT COUNT(*) FROM five_hour_blocks WHERE account_key = ?",
|
|
1989
|
+
(account_key,),
|
|
1826
1990
|
).fetchone()[0] == 0
|
|
1827
1991
|
|
|
1828
1992
|
if prior is None:
|
|
@@ -1903,9 +2067,10 @@ def maybe_update_five_hour_block(
|
|
|
1903
2067
|
UPDATE five_hour_blocks
|
|
1904
2068
|
SET is_closed = 1, last_updated_at_utc = ?
|
|
1905
2069
|
WHERE is_closed = 0
|
|
2070
|
+
AND account_key = ?
|
|
1906
2071
|
AND five_hour_window_key < ?
|
|
1907
2072
|
""",
|
|
1908
|
-
(now_iso, int(five_hour_window_key)),
|
|
2073
|
+
(now_iso, account_key, int(five_hour_window_key)),
|
|
1909
2074
|
)
|
|
1910
2075
|
|
|
1911
2076
|
# Step 5b: natural-expiration sweep. The close-older predicate
|
|
@@ -1920,9 +2085,10 @@ def maybe_update_five_hour_block(
|
|
|
1920
2085
|
UPDATE five_hour_blocks
|
|
1921
2086
|
SET is_closed = 1, last_updated_at_utc = ?
|
|
1922
2087
|
WHERE is_closed = 0
|
|
2088
|
+
AND account_key = ?
|
|
1923
2089
|
AND five_hour_resets_at < ?
|
|
1924
2090
|
""",
|
|
1925
|
-
(now_iso, now_iso),
|
|
2091
|
+
(now_iso, account_key, now_iso),
|
|
1926
2092
|
)
|
|
1927
2093
|
|
|
1928
2094
|
# Step 7: atomic upsert. Single statement collapses the
|
|
@@ -1956,10 +2122,11 @@ def maybe_update_five_hour_block(
|
|
|
1956
2122
|
total_cost_usd,
|
|
1957
2123
|
is_closed,
|
|
1958
2124
|
created_at_utc,
|
|
1959
|
-
last_updated_at_utc
|
|
2125
|
+
last_updated_at_utc,
|
|
2126
|
+
account_key
|
|
1960
2127
|
)
|
|
1961
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?, 0, ?, ?)
|
|
1962
|
-
ON CONFLICT(five_hour_window_key) DO UPDATE SET
|
|
2128
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?, 0, ?, ?, ?)
|
|
2129
|
+
ON CONFLICT(account_key, five_hour_window_key) DO UPDATE SET
|
|
1963
2130
|
last_observed_at_utc = excluded.last_observed_at_utc,
|
|
1964
2131
|
final_five_hour_percent = excluded.final_five_hour_percent,
|
|
1965
2132
|
seven_day_pct_at_block_end = excluded.seven_day_pct_at_block_end,
|
|
@@ -1986,16 +2153,19 @@ def maybe_update_five_hour_block(
|
|
|
1986
2153
|
totals["cost_usd"],
|
|
1987
2154
|
now_iso,
|
|
1988
2155
|
now_iso,
|
|
2156
|
+
account_key,
|
|
1989
2157
|
),
|
|
1990
2158
|
)
|
|
1991
2159
|
|
|
1992
2160
|
# ── Resolve current block_id once for reuse by the per-(block, model)
|
|
1993
2161
|
# / per-(block, project) child writes below AND the existing milestone
|
|
1994
2162
|
# detection (which previously did its own SELECT — drop that SELECT in
|
|
1995
|
-
# favor of this variable).
|
|
2163
|
+
# favor of this variable). Composite (account_key, five_hour_window_key)
|
|
2164
|
+
# so a shared physical window resolves THIS account's block (#341).
|
|
1996
2165
|
block_id_row = conn.execute(
|
|
1997
|
-
"SELECT id FROM five_hour_blocks
|
|
1998
|
-
|
|
2166
|
+
"SELECT id FROM five_hour_blocks "
|
|
2167
|
+
"WHERE five_hour_window_key = ? AND account_key = ?",
|
|
2168
|
+
(int(five_hour_window_key), account_key),
|
|
1999
2169
|
).fetchone()
|
|
2000
2170
|
block_id = int(block_id_row["id"])
|
|
2001
2171
|
|
|
@@ -2010,9 +2180,9 @@ def maybe_update_five_hour_block(
|
|
|
2010
2180
|
if blocks_were_empty:
|
|
2011
2181
|
conn.execute(
|
|
2012
2182
|
"UPDATE five_hour_blocks SET is_closed = 1, last_updated_at_utc = ? "
|
|
2013
|
-
"WHERE five_hour_window_key = ? AND
|
|
2014
|
-
" AND five_hour_resets_at < ?",
|
|
2015
|
-
(now_iso, int(five_hour_window_key), now_iso),
|
|
2183
|
+
"WHERE five_hour_window_key = ? AND account_key = ? "
|
|
2184
|
+
" AND is_closed = 0 AND five_hour_resets_at < ?",
|
|
2185
|
+
(now_iso, int(five_hour_window_key), account_key, now_iso),
|
|
2016
2186
|
)
|
|
2017
2187
|
|
|
2018
2188
|
# ── Replace-all per-tick: per-(block, model) and per-(block, project_path)
|
|
@@ -2021,8 +2191,9 @@ def maybe_update_five_hour_block(
|
|
|
2021
2191
|
# Same transaction as the parent upsert; if these raise, the whole tick
|
|
2022
2192
|
# rolls back and the next tick recomputes from scratch.
|
|
2023
2193
|
conn.execute(
|
|
2024
|
-
"DELETE FROM five_hour_block_models
|
|
2025
|
-
|
|
2194
|
+
"DELETE FROM five_hour_block_models "
|
|
2195
|
+
"WHERE five_hour_window_key = ? AND account_key = ?",
|
|
2196
|
+
(int(five_hour_window_key), account_key),
|
|
2026
2197
|
)
|
|
2027
2198
|
if totals.get("by_model"):
|
|
2028
2199
|
conn.executemany(
|
|
@@ -2031,9 +2202,9 @@ def maybe_update_five_hour_block(
|
|
|
2031
2202
|
block_id, five_hour_window_key, model,
|
|
2032
2203
|
input_tokens, output_tokens,
|
|
2033
2204
|
cache_create_tokens, cache_read_tokens,
|
|
2034
|
-
cost_usd, entry_count
|
|
2205
|
+
cost_usd, entry_count, account_key
|
|
2035
2206
|
)
|
|
2036
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2207
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2037
2208
|
""",
|
|
2038
2209
|
[
|
|
2039
2210
|
(
|
|
@@ -2046,14 +2217,16 @@ def maybe_update_five_hour_block(
|
|
|
2046
2217
|
b["cache_read_tokens"],
|
|
2047
2218
|
b["cost_usd"],
|
|
2048
2219
|
b["entry_count"],
|
|
2220
|
+
account_key,
|
|
2049
2221
|
)
|
|
2050
2222
|
for model, b in totals["by_model"].items()
|
|
2051
2223
|
],
|
|
2052
2224
|
)
|
|
2053
2225
|
|
|
2054
2226
|
conn.execute(
|
|
2055
|
-
"DELETE FROM five_hour_block_projects
|
|
2056
|
-
|
|
2227
|
+
"DELETE FROM five_hour_block_projects "
|
|
2228
|
+
"WHERE five_hour_window_key = ? AND account_key = ?",
|
|
2229
|
+
(int(five_hour_window_key), account_key),
|
|
2057
2230
|
)
|
|
2058
2231
|
if totals.get("by_project"):
|
|
2059
2232
|
conn.executemany(
|
|
@@ -2062,9 +2235,9 @@ def maybe_update_five_hour_block(
|
|
|
2062
2235
|
block_id, five_hour_window_key, project_path,
|
|
2063
2236
|
input_tokens, output_tokens,
|
|
2064
2237
|
cache_create_tokens, cache_read_tokens,
|
|
2065
|
-
cost_usd, entry_count
|
|
2238
|
+
cost_usd, entry_count, account_key
|
|
2066
2239
|
)
|
|
2067
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2240
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2068
2241
|
""",
|
|
2069
2242
|
[
|
|
2070
2243
|
(
|
|
@@ -2077,6 +2250,7 @@ def maybe_update_five_hour_block(
|
|
|
2077
2250
|
b["cache_read_tokens"],
|
|
2078
2251
|
b["cost_usd"],
|
|
2079
2252
|
b["entry_count"],
|
|
2253
|
+
account_key,
|
|
2080
2254
|
)
|
|
2081
2255
|
for project_path, b in totals["by_project"].items()
|
|
2082
2256
|
],
|
|
@@ -2094,7 +2268,7 @@ def maybe_update_five_hour_block(
|
|
|
2094
2268
|
# the active segment is the latest five_hour_reset_events row
|
|
2095
2269
|
# for this window_key, else sentinel 0 (pre-credit).
|
|
2096
2270
|
active_reset_event_id = _resolve_active_five_hour_reset_event_id(
|
|
2097
|
-
conn, int(five_hour_window_key)
|
|
2271
|
+
conn, int(five_hour_window_key), account_key=account_key
|
|
2098
2272
|
)
|
|
2099
2273
|
|
|
2100
2274
|
if current_floor >= 1:
|
|
@@ -2110,8 +2284,9 @@ def maybe_update_five_hour_block(
|
|
|
2110
2284
|
# maybe_record_milestone's max_existing path.
|
|
2111
2285
|
row = conn.execute(
|
|
2112
2286
|
"SELECT MAX(percent_threshold) AS m FROM five_hour_milestones "
|
|
2113
|
-
"WHERE five_hour_window_key = ? AND reset_event_id = ?"
|
|
2114
|
-
|
|
2287
|
+
"WHERE five_hour_window_key = ? AND reset_event_id = ? "
|
|
2288
|
+
" AND account_key = ?",
|
|
2289
|
+
(int(five_hour_window_key), active_reset_event_id, account_key),
|
|
2115
2290
|
).fetchone()
|
|
2116
2291
|
max_existing = row["m"] if row and row["m"] is not None else None
|
|
2117
2292
|
|
|
@@ -2141,9 +2316,10 @@ def maybe_update_five_hour_block(
|
|
|
2141
2316
|
"SELECT block_cost_usd FROM five_hour_milestones "
|
|
2142
2317
|
"WHERE five_hour_window_key = ? "
|
|
2143
2318
|
" AND percent_threshold = ? "
|
|
2144
|
-
" AND reset_event_id = ?"
|
|
2319
|
+
" AND reset_event_id = ? "
|
|
2320
|
+
" AND account_key = ?",
|
|
2145
2321
|
(int(five_hour_window_key), int(max_existing),
|
|
2146
|
-
active_reset_event_id),
|
|
2322
|
+
active_reset_event_id, account_key),
|
|
2147
2323
|
).fetchone()
|
|
2148
2324
|
if prev_row is not None:
|
|
2149
2325
|
prior_cost = float(prev_row["block_cost_usd"])
|
|
@@ -2174,9 +2350,10 @@ def maybe_update_five_hour_block(
|
|
|
2174
2350
|
block_cost_usd,
|
|
2175
2351
|
marginal_cost_usd,
|
|
2176
2352
|
seven_day_pct_at_crossing,
|
|
2177
|
-
reset_event_id
|
|
2353
|
+
reset_event_id,
|
|
2354
|
+
account_key
|
|
2178
2355
|
)
|
|
2179
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2356
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2180
2357
|
""",
|
|
2181
2358
|
(
|
|
2182
2359
|
block_id,
|
|
@@ -2192,6 +2369,7 @@ def maybe_update_five_hour_block(
|
|
|
2192
2369
|
marginal,
|
|
2193
2370
|
weekly_percent,
|
|
2194
2371
|
active_reset_event_id,
|
|
2372
|
+
account_key,
|
|
2195
2373
|
),
|
|
2196
2374
|
)
|
|
2197
2375
|
# ── Threshold-actions dispatch (set-then-dispatch, spec §3.2) ──
|
|
@@ -2230,9 +2408,10 @@ def maybe_update_five_hour_block(
|
|
|
2230
2408
|
"WHERE five_hour_window_key = ? "
|
|
2231
2409
|
" AND percent_threshold = ? "
|
|
2232
2410
|
" AND reset_event_id = ? "
|
|
2411
|
+
" AND account_key = ? "
|
|
2233
2412
|
" AND alerted_at IS NULL",
|
|
2234
2413
|
(crossed_at, int(five_hour_window_key),
|
|
2235
|
-
int(pct), active_reset_event_id),
|
|
2414
|
+
int(pct), active_reset_event_id, account_key),
|
|
2236
2415
|
)
|
|
2237
2416
|
# Cheap re-reads inside BEGIN are SELECT-only and
|
|
2238
2417
|
# safe; values reflect post-INSERT state. We
|
|
@@ -2248,17 +2427,19 @@ def maybe_update_five_hour_block(
|
|
|
2248
2427
|
"SELECT block_cost_usd FROM five_hour_milestones "
|
|
2249
2428
|
"WHERE five_hour_window_key = ? "
|
|
2250
2429
|
" AND percent_threshold = ? "
|
|
2251
|
-
" AND reset_event_id = ?"
|
|
2430
|
+
" AND reset_event_id = ? "
|
|
2431
|
+
" AND account_key = ?",
|
|
2252
2432
|
(int(five_hour_window_key), int(pct),
|
|
2253
|
-
active_reset_event_id),
|
|
2433
|
+
active_reset_event_id, account_key),
|
|
2254
2434
|
).fetchone()
|
|
2255
2435
|
block_row = conn.execute(
|
|
2256
2436
|
"SELECT block_start_at FROM five_hour_blocks "
|
|
2257
|
-
"WHERE five_hour_window_key = ?",
|
|
2258
|
-
(int(five_hour_window_key),),
|
|
2437
|
+
"WHERE five_hour_window_key = ? AND account_key = ?",
|
|
2438
|
+
(int(five_hour_window_key), account_key),
|
|
2259
2439
|
).fetchone()
|
|
2260
2440
|
primary_model = _resolve_primary_model_for_block(
|
|
2261
|
-
conn, int(five_hour_window_key)
|
|
2441
|
+
conn, int(five_hour_window_key),
|
|
2442
|
+
account_key=account_key,
|
|
2262
2443
|
)
|
|
2263
2444
|
payload = _build_alert_payload_five_hour(
|
|
2264
2445
|
threshold=int(pct),
|
|
@@ -2273,6 +2454,7 @@ def maybe_update_five_hour_block(
|
|
|
2273
2454
|
else 0.0
|
|
2274
2455
|
),
|
|
2275
2456
|
primary_model=primary_model,
|
|
2457
|
+
account_key=account_key,
|
|
2276
2458
|
)
|
|
2277
2459
|
pending_alerts.append(payload)
|
|
2278
2460
|
|
|
@@ -2320,16 +2502,19 @@ def maybe_update_five_hour_block(
|
|
|
2320
2502
|
UPDATE five_hour_blocks
|
|
2321
2503
|
SET crossed_seven_day_reset = 1
|
|
2322
2504
|
WHERE crossed_seven_day_reset = 0
|
|
2505
|
+
AND account_key = ?
|
|
2323
2506
|
AND (
|
|
2324
2507
|
EXISTS (
|
|
2325
2508
|
SELECT 1 FROM week_reset_events e
|
|
2326
|
-
WHERE
|
|
2509
|
+
WHERE e.account_key = five_hour_blocks.account_key
|
|
2510
|
+
AND unixepoch(e.effective_reset_at_utc)
|
|
2327
2511
|
BETWEEN unixepoch(five_hour_blocks.block_start_at)
|
|
2328
2512
|
AND unixepoch(five_hour_blocks.last_observed_at_utc)
|
|
2329
2513
|
)
|
|
2330
2514
|
OR EXISTS (
|
|
2331
2515
|
SELECT 1 FROM weekly_usage_snapshots ws
|
|
2332
2516
|
WHERE ws.week_start_at IS NOT NULL
|
|
2517
|
+
AND ws.account_key = five_hour_blocks.account_key
|
|
2333
2518
|
AND unixepoch(ws.week_start_at)
|
|
2334
2519
|
> unixepoch(five_hour_blocks.block_start_at)
|
|
2335
2520
|
AND unixepoch(ws.week_start_at)
|
|
@@ -2337,6 +2522,7 @@ def maybe_update_five_hour_block(
|
|
|
2337
2522
|
)
|
|
2338
2523
|
)
|
|
2339
2524
|
""",
|
|
2525
|
+
(account_key,),
|
|
2340
2526
|
)
|
|
2341
2527
|
|
|
2342
2528
|
if own_conn:
|
|
@@ -2458,11 +2644,15 @@ def _read_reset_zero_marker():
|
|
|
2458
2644
|
|
|
2459
2645
|
def _fire_in_place_credit(conn, week_start_date, cur_end_canon, weekly_percent,
|
|
2460
2646
|
*, observed_pre_credit_pct, effective_dt,
|
|
2461
|
-
as_of=None, commit=True, ctx=None
|
|
2647
|
+
as_of=None, commit=True, ctx=None,
|
|
2648
|
+
account_key=_lib_accounts.UNATTRIBUTED):
|
|
2462
2649
|
"""Emit/refresh the in-place weekly-credit artifacts (issue #19 + #128).
|
|
2463
2650
|
Shared by the immediate >=25pp path and the debounced reset-to-zero
|
|
2464
2651
|
confirmation path.
|
|
2465
2652
|
|
|
2653
|
+
``account_key`` (#341): stamps the ``week_reset_events`` row and scopes the
|
|
2654
|
+
stale-replica capture/DELETE to the account.
|
|
2655
|
+
|
|
2466
2656
|
Transaction-neutral / capture-time-pure seam (DB journal redesign §5.2.3):
|
|
2467
2657
|
``commit=False`` folds the event-row INSERT + stale-replica DELETE into the
|
|
2468
2658
|
caller's transaction (the ingester's cycle) instead of committing inline;
|
|
@@ -2494,8 +2684,9 @@ def _fire_in_place_credit(conn, week_start_date, cur_end_canon, weekly_percent,
|
|
|
2494
2684
|
# ticks. UNIQUE(old, new) also dedups, but the pre-check avoids a useless
|
|
2495
2685
|
# write attempt and keeps logs clean.
|
|
2496
2686
|
already = conn.execute(
|
|
2497
|
-
"SELECT 1 FROM week_reset_events
|
|
2498
|
-
|
|
2687
|
+
"SELECT 1 FROM week_reset_events "
|
|
2688
|
+
"WHERE new_week_end_at = ? AND account_key = ? LIMIT 1",
|
|
2689
|
+
(cur_end_canon, account_key),
|
|
2499
2690
|
).fetchone()
|
|
2500
2691
|
if already is None:
|
|
2501
2692
|
# Row shape: old=effective_iso, new=cur_end_canon (DISTINCT) so only
|
|
@@ -2505,10 +2696,10 @@ def _fire_in_place_credit(conn, week_start_date, cur_end_canon, weekly_percent,
|
|
|
2505
2696
|
ins_wr = conn.execute(
|
|
2506
2697
|
"INSERT OR IGNORE INTO week_reset_events "
|
|
2507
2698
|
"(detected_at_utc, old_week_end_at, new_week_end_at, "
|
|
2508
|
-
" effective_reset_at_utc, observed_pre_credit_pct) "
|
|
2509
|
-
"VALUES (?, ?, ?, ?, ?)",
|
|
2699
|
+
" effective_reset_at_utc, observed_pre_credit_pct, account_key) "
|
|
2700
|
+
"VALUES (?, ?, ?, ?, ?, ?)",
|
|
2510
2701
|
(as_of or now_utc_iso(), effective_iso, cur_end_canon,
|
|
2511
|
-
effective_iso, float(observed_pre_credit_pct)),
|
|
2702
|
+
effective_iso, float(observed_pre_credit_pct), account_key),
|
|
2512
2703
|
)
|
|
2513
2704
|
# Design B (§5.3 event+effects): on the ingest path, capture the doomed
|
|
2514
2705
|
# stale-replica snapshots' journal_ids BEFORE the pivot-2 DELETE (SAME
|
|
@@ -2519,12 +2710,16 @@ def _fire_in_place_credit(conn, week_start_date, cur_end_canon, weekly_percent,
|
|
|
2519
2710
|
if ctx is not None and ins_wr.rowcount == 1:
|
|
2520
2711
|
doomed = conn.execute(
|
|
2521
2712
|
"SELECT journal_id FROM weekly_usage_snapshots "
|
|
2522
|
-
"WHERE week_start_date = ? "
|
|
2713
|
+
"WHERE week_start_date = ? AND account_key = ? "
|
|
2523
2714
|
" AND unixepoch(captured_at_utc) >= unixepoch(?) "
|
|
2524
2715
|
" AND ABS(weekly_percent - ?) < 1.0",
|
|
2525
|
-
(week_start_date,
|
|
2716
|
+
(week_start_date, account_key, effective_iso,
|
|
2717
|
+
float(observed_pre_credit_pct)),
|
|
2526
2718
|
).fetchall()
|
|
2527
|
-
|
|
2719
|
+
# #341: the wr harvest id_parts now lead with account_key, so the
|
|
2720
|
+
# suppression_map key (which _build_harvest_evt derives from id_parts)
|
|
2721
|
+
# must match: (account_key, old_week_end_at, new_week_end_at).
|
|
2722
|
+
ctx.suppression_map[(account_key, effective_iso, cur_end_canon)] = [
|
|
2528
2723
|
r[0] for r in doomed
|
|
2529
2724
|
]
|
|
2530
2725
|
if commit:
|
|
@@ -2546,10 +2741,11 @@ def _fire_in_place_credit(conn, week_start_date, cur_end_canon, weekly_percent,
|
|
|
2546
2741
|
try:
|
|
2547
2742
|
conn.execute(
|
|
2548
2743
|
"DELETE FROM weekly_usage_snapshots "
|
|
2549
|
-
"WHERE week_start_date = ? "
|
|
2744
|
+
"WHERE week_start_date = ? AND account_key = ? "
|
|
2550
2745
|
" AND unixepoch(captured_at_utc) >= unixepoch(?) "
|
|
2551
2746
|
" AND ABS(weekly_percent - ?) < 1.0",
|
|
2552
|
-
(week_start_date,
|
|
2747
|
+
(week_start_date, account_key, effective_iso,
|
|
2748
|
+
float(observed_pre_credit_pct)),
|
|
2553
2749
|
)
|
|
2554
2750
|
if commit:
|
|
2555
2751
|
conn.commit()
|
|
@@ -2560,11 +2756,17 @@ def _fire_in_place_credit(conn, week_start_date, cur_end_canon, weekly_percent,
|
|
|
2560
2756
|
def detect_reset_and_credit(conn, *, week_start_date, week_end_at,
|
|
2561
2757
|
weekly_percent, five_hour_window_key,
|
|
2562
2758
|
five_hour_percent, as_of=None, commit=True,
|
|
2563
|
-
ctx=None):
|
|
2759
|
+
ctx=None, account_key=_lib_accounts.UNATTRIBUTED):
|
|
2564
2760
|
"""Detect + record weekly and 5h reset/credit artifacts for one usage
|
|
2565
2761
|
observation (extracted from ``cmd_record_usage``; DB journal redesign
|
|
2566
2762
|
§5.2.3).
|
|
2567
2763
|
|
|
2764
|
+
``account_key`` (#341): the account the observation belongs to. Every
|
|
2765
|
+
reset/credit row written here (``week_reset_events``,
|
|
2766
|
+
``five_hour_reset_events``, ``weekly_credit_floors``, synthetic snapshots)
|
|
2767
|
+
is stamped with it so the reset-aware clamp legs stay account-scoped.
|
|
2768
|
+
Default ``"unattributed"`` is the rev-4.1 defensive backstop.
|
|
2769
|
+
|
|
2568
2770
|
Runs the mid-week ``week_reset_events`` detection, the reset-to-zero
|
|
2569
2771
|
debounce + >=25pp in-place-credit fire, and the parallel 5h in-place-
|
|
2570
2772
|
credit detection, all on the caller's ``conn``.
|
|
@@ -2644,9 +2846,10 @@ def detect_reset_and_credit(conn, *, week_start_date, week_end_at,
|
|
|
2644
2846
|
conn.execute(
|
|
2645
2847
|
"INSERT OR IGNORE INTO week_reset_events "
|
|
2646
2848
|
"(detected_at_utc, old_week_end_at, new_week_end_at, "
|
|
2647
|
-
" effective_reset_at_utc)
|
|
2849
|
+
" effective_reset_at_utc, account_key) "
|
|
2850
|
+
"VALUES (?, ?, ?, ?, ?)",
|
|
2648
2851
|
((as_of or now_utc_iso()), prior_end_canon, cur_end_canon,
|
|
2649
|
-
effective_iso),
|
|
2852
|
+
effective_iso, account_key),
|
|
2650
2853
|
)
|
|
2651
2854
|
# (inline commit removed — the function commits once at the
|
|
2652
2855
|
# end on the legacy path; the ingest cycle owns the commit.)
|
|
@@ -2692,6 +2895,7 @@ def detect_reset_and_credit(conn, *, week_start_date, week_end_at,
|
|
|
2692
2895
|
observed_pre_credit_pct=float(prior_pct),
|
|
2693
2896
|
effective_dt=_floor_to_hour(now_utc),
|
|
2694
2897
|
as_of=as_of, commit=commit, ctx=ctx,
|
|
2898
|
+
account_key=account_key,
|
|
2695
2899
|
)
|
|
2696
2900
|
elif decision == CONFIRM_RESET:
|
|
2697
2901
|
# Second reading stayed low → confirm. Anchor the
|
|
@@ -2706,6 +2910,7 @@ def detect_reset_and_credit(conn, *, week_start_date, week_end_at,
|
|
|
2706
2910
|
observed_pre_credit_pct=marker[2],
|
|
2707
2911
|
effective_dt=_floor_to_hour(first_zero_dt),
|
|
2708
2912
|
as_of=as_of, commit=commit, ctx=ctx,
|
|
2913
|
+
account_key=account_key,
|
|
2709
2914
|
)
|
|
2710
2915
|
# Clear ONLY after the fire completes (P2a): a
|
|
2711
2916
|
# mid-fire crash leaves the marker armed so the next
|
|
@@ -2811,8 +3016,9 @@ def detect_reset_and_credit(conn, *, week_start_date, week_end_at,
|
|
|
2811
3016
|
"SELECT prior_percent, post_percent "
|
|
2812
3017
|
" FROM five_hour_reset_events "
|
|
2813
3018
|
" WHERE five_hour_window_key = ? "
|
|
3019
|
+
" AND account_key = ? "
|
|
2814
3020
|
" ORDER BY id DESC LIMIT 1",
|
|
2815
|
-
(int(five_hour_window_key),),
|
|
3021
|
+
(int(five_hour_window_key), account_key),
|
|
2816
3022
|
).fetchone()
|
|
2817
3023
|
is_dup = (
|
|
2818
3024
|
most_recent is not None
|
|
@@ -2846,14 +3052,15 @@ def detect_reset_and_credit(conn, *, week_start_date, week_end_at,
|
|
|
2846
3052
|
"INSERT OR IGNORE INTO five_hour_reset_events "
|
|
2847
3053
|
"(detected_at_utc, five_hour_window_key, "
|
|
2848
3054
|
" prior_percent, post_percent, "
|
|
2849
|
-
" effective_reset_at_utc) "
|
|
2850
|
-
"VALUES (?, ?, ?, ?, ?)",
|
|
3055
|
+
" effective_reset_at_utc, account_key) "
|
|
3056
|
+
"VALUES (?, ?, ?, ?, ?, ?)",
|
|
2851
3057
|
(
|
|
2852
3058
|
(as_of or now_utc_iso()),
|
|
2853
3059
|
int(five_hour_window_key),
|
|
2854
3060
|
prior_5h_pct,
|
|
2855
3061
|
float(five_hour_percent),
|
|
2856
3062
|
effective_iso,
|
|
3063
|
+
account_key,
|
|
2857
3064
|
),
|
|
2858
3065
|
)
|
|
2859
3066
|
# Design B (§5.3 event+effects): on the ingest path,
|
|
@@ -2873,18 +3080,23 @@ def detect_reset_and_credit(conn, *, week_start_date, week_end_at,
|
|
|
2873
3080
|
"SELECT journal_id "
|
|
2874
3081
|
"FROM weekly_usage_snapshots "
|
|
2875
3082
|
" WHERE five_hour_window_key = ? "
|
|
3083
|
+
" AND account_key = ? "
|
|
2876
3084
|
" AND unixepoch(captured_at_utc) "
|
|
2877
3085
|
" >= unixepoch(?) "
|
|
2878
3086
|
" AND ABS(five_hour_percent - ?) "
|
|
2879
3087
|
" < 1.0",
|
|
2880
3088
|
(
|
|
2881
3089
|
int(five_hour_window_key),
|
|
3090
|
+
account_key,
|
|
2882
3091
|
effective_iso,
|
|
2883
3092
|
prior_5h_pct,
|
|
2884
3093
|
),
|
|
2885
3094
|
).fetchall()
|
|
3095
|
+
# #341: fhc harvest id_parts lead with
|
|
3096
|
+
# account_key -> match the suppression_map key.
|
|
2886
3097
|
ctx.suppression_map[
|
|
2887
|
-
(int(five_hour_window_key),
|
|
3098
|
+
(account_key, int(five_hour_window_key),
|
|
3099
|
+
effective_iso)
|
|
2888
3100
|
] = [r[0] for r in doomed]
|
|
2889
3101
|
# (inline commit removed — one end-of-function commit
|
|
2890
3102
|
# on legacy; the ingest cycle owns the commit.)
|
|
@@ -2960,12 +3172,14 @@ def detect_reset_and_credit(conn, *, week_start_date, week_end_at,
|
|
|
2960
3172
|
conn.execute(
|
|
2961
3173
|
"DELETE FROM weekly_usage_snapshots "
|
|
2962
3174
|
" WHERE five_hour_window_key = ? "
|
|
3175
|
+
" AND account_key = ? "
|
|
2963
3176
|
" AND unixepoch(captured_at_utc) "
|
|
2964
3177
|
" >= unixepoch(?) "
|
|
2965
3178
|
" AND ABS(five_hour_percent - ?) "
|
|
2966
3179
|
" < 1.0",
|
|
2967
3180
|
(
|
|
2968
3181
|
int(five_hour_window_key),
|
|
3182
|
+
account_key,
|
|
2969
3183
|
effective_iso,
|
|
2970
3184
|
prior_5h_pct,
|
|
2971
3185
|
),
|
|
@@ -2999,36 +3213,49 @@ def detect_reset_and_credit(conn, *, week_start_date, week_end_at,
|
|
|
2999
3213
|
conn.commit()
|
|
3000
3214
|
|
|
3001
3215
|
|
|
3002
|
-
def _resolve_reset_aware_hwm(conn, week_start_date, week_start_at, week_end_at
|
|
3216
|
+
def _resolve_reset_aware_hwm(conn, week_start_date, week_start_at, week_end_at,
|
|
3217
|
+
*, account_key):
|
|
3003
3218
|
"""The floored MAX(weekly_percent) the statusline _hwm_clamp computes:
|
|
3004
3219
|
MAX over snapshots captured at/after the latest in-week clamp floor. The
|
|
3005
3220
|
floor is the latest effective across BOTH `week_reset_events` and
|
|
3006
3221
|
`weekly_credit_floors` (`_reset_aware_floor`) so a manual partial credit
|
|
3007
3222
|
(record-credit M2, #209) lowers the resolved HWM without re-anchoring the
|
|
3008
3223
|
week — used both as the `--from` default and as the assertion source of
|
|
3009
|
-
truth in the record-credit tests.
|
|
3010
|
-
|
|
3224
|
+
truth in the record-credit tests.
|
|
3225
|
+
|
|
3226
|
+
``account_key`` (#341): MANDATORY account context (no silent global
|
|
3227
|
+
fallback). A real key scopes the HWM to one account; ``None`` is the
|
|
3228
|
+
explicit merged read (byte-identical to today on a single-account install)."""
|
|
3229
|
+
floor_iso = _reset_aware_floor(conn, week_start_date, week_start_at,
|
|
3230
|
+
week_end_at, account_key=account_key)
|
|
3231
|
+
acct_pred = "" if account_key is None else " AND account_key = ?"
|
|
3232
|
+
acct_param: tuple = () if account_key is None else (account_key,)
|
|
3011
3233
|
if floor_iso is not None:
|
|
3012
3234
|
row = conn.execute(
|
|
3013
3235
|
"SELECT MAX(weekly_percent) FROM weekly_usage_snapshots "
|
|
3014
|
-
" WHERE week_start_date = ?
|
|
3015
|
-
(
|
|
3236
|
+
f" WHERE week_start_date = ?{acct_pred} "
|
|
3237
|
+
" AND unixepoch(captured_at_utc) >= unixepoch(?)",
|
|
3238
|
+
(week_start_date, *acct_param, floor_iso),
|
|
3016
3239
|
).fetchone()
|
|
3017
3240
|
else:
|
|
3018
3241
|
row = conn.execute(
|
|
3019
3242
|
"SELECT MAX(weekly_percent) FROM weekly_usage_snapshots "
|
|
3020
|
-
" WHERE week_start_date = ?",
|
|
3021
|
-
(week_start_date,),
|
|
3243
|
+
f" WHERE week_start_date = ?{acct_pred}",
|
|
3244
|
+
(week_start_date, *acct_param),
|
|
3022
3245
|
).fetchone()
|
|
3023
3246
|
return None if not row or row[0] is None else float(row[0])
|
|
3024
3247
|
|
|
3025
3248
|
|
|
3026
3249
|
def _insert_credit_snapshot(conn, plan, *, five_hour=(None, None, None),
|
|
3027
|
-
commit=True, journal=None
|
|
3250
|
+
commit=True, journal=None,
|
|
3251
|
+
account_key=_lib_accounts.UNATTRIBUTED):
|
|
3028
3252
|
"""Insert the post-credit synthetic snapshot at plan.to_pct, tagged
|
|
3029
3253
|
source='record-credit'. The SOLE synthetic-snapshot writer on BOTH paths
|
|
3030
3254
|
(legacy inline + ingest), so the non-vacuity stub stays load-bearing.
|
|
3031
3255
|
|
|
3256
|
+
``account_key`` (#341) stamps the synthetic row so it joins the same
|
|
3257
|
+
account's clamp/HWM ledger.
|
|
3258
|
+
|
|
3032
3259
|
``commit=False`` (DB journal redesign §5.2.3) folds the synthetic-snapshot
|
|
3033
3260
|
INSERT into the caller's transaction instead of committing inline; the
|
|
3034
3261
|
default keeps the legacy inline-commit behavior. Capture time comes from
|
|
@@ -3065,6 +3292,7 @@ def _insert_credit_snapshot(conn, plan, *, five_hour=(None, None, None),
|
|
|
3065
3292
|
"five_hour_percent": fhp,
|
|
3066
3293
|
"five_hour_resets_at": fhr,
|
|
3067
3294
|
"five_hour_window_key": fhk,
|
|
3295
|
+
"account_key": account_key,
|
|
3068
3296
|
}
|
|
3069
3297
|
if journal is not None:
|
|
3070
3298
|
ctx, evt_id = journal
|
|
@@ -3105,12 +3333,16 @@ def _resolve_prior_5h(conn, at_dt):
|
|
|
3105
3333
|
|
|
3106
3334
|
|
|
3107
3335
|
def _apply_credit(conn, plan, *, five_hour=(None, None, None), as_of=None,
|
|
3108
|
-
commit=True, ctx=None, id_base=None, forced=False
|
|
3336
|
+
commit=True, ctx=None, id_base=None, forced=False,
|
|
3337
|
+
account_key=_lib_accounts.UNATTRIBUTED):
|
|
3109
3338
|
"""Apply the M2 same-window partial-credit artifacts (record-credit, #209,
|
|
3110
3339
|
spec §4). Unlike the >=25pp auto-credit path (`_fire_in_place_credit`), this
|
|
3111
3340
|
writes NO `week_reset_events` row — the window-resolution code never sees a
|
|
3112
3341
|
credit, so the week is NOT re-anchored. It only lowers the clamp floor.
|
|
3113
3342
|
|
|
3343
|
+
``account_key`` (#341): stamps the ``weekly_credit_floors`` row + the
|
|
3344
|
+
synthetic snapshot and scopes the stale-replay DELETE to the account.
|
|
3345
|
+
|
|
3114
3346
|
Transaction-neutral / capture-time-pure seam (DB journal redesign §5.2.3):
|
|
3115
3347
|
``commit=False`` folds the floor INSERT + stale-replay DELETE + synthetic
|
|
3116
3348
|
snapshot into the caller's transaction (the ingester's cycle) instead of
|
|
@@ -3175,9 +3407,11 @@ def _apply_credit(conn, plan, *, five_hour=(None, None, None), as_of=None,
|
|
|
3175
3407
|
if ctx is None:
|
|
3176
3408
|
conn.execute(
|
|
3177
3409
|
"INSERT OR IGNORE INTO weekly_credit_floors "
|
|
3178
|
-
"(week_start_date, effective_at_utc, observed_pre_credit_pct,
|
|
3179
|
-
"
|
|
3180
|
-
(
|
|
3410
|
+
"(week_start_date, effective_at_utc, observed_pre_credit_pct, "
|
|
3411
|
+
" applied_at_utc, account_key) "
|
|
3412
|
+
"VALUES (?, ?, ?, ?, ?)",
|
|
3413
|
+
(plan.week_start_date, effective_iso, pre_credit,
|
|
3414
|
+
as_of or now_utc_iso(), account_key),
|
|
3181
3415
|
)
|
|
3182
3416
|
if commit:
|
|
3183
3417
|
conn.commit()
|
|
@@ -3201,10 +3435,10 @@ def _apply_credit(conn, plan, *, five_hour=(None, None, None), as_of=None,
|
|
|
3201
3435
|
try:
|
|
3202
3436
|
conn.execute(
|
|
3203
3437
|
"DELETE FROM weekly_usage_snapshots "
|
|
3204
|
-
"WHERE week_start_date = ? "
|
|
3438
|
+
"WHERE week_start_date = ? AND account_key = ? "
|
|
3205
3439
|
" AND unixepoch(captured_at_utc) >= unixepoch(?) "
|
|
3206
3440
|
" AND ABS(weekly_percent - ?) < 1.0",
|
|
3207
|
-
(plan.week_start_date, effective_iso, pre_credit),
|
|
3441
|
+
(plan.week_start_date, account_key, effective_iso, pre_credit),
|
|
3208
3442
|
)
|
|
3209
3443
|
if commit:
|
|
3210
3444
|
conn.commit()
|
|
@@ -3212,7 +3446,8 @@ def _apply_credit(conn, plan, *, five_hour=(None, None, None), as_of=None,
|
|
|
3212
3446
|
eprint(f"[record-credit] post-credit cleanup failed: {exc}")
|
|
3213
3447
|
|
|
3214
3448
|
# 4d. INSERT the synthetic post-credit snapshot at plan.to_pct.
|
|
3215
|
-
c._insert_credit_snapshot(conn, plan, five_hour=five_hour, commit=commit
|
|
3449
|
+
c._insert_credit_snapshot(conn, plan, five_hour=five_hour, commit=commit,
|
|
3450
|
+
account_key=account_key)
|
|
3216
3451
|
else:
|
|
3217
3452
|
# ── Ingest path (Design B, §5.3 event+effects) ─────────────────────
|
|
3218
3453
|
# Journal the destructive effects + synthetic instead of running them
|
|
@@ -3235,12 +3470,13 @@ def _apply_credit(conn, plan, *, five_hour=(None, None, None), as_of=None,
|
|
|
3235
3470
|
# (`id_base` is a content digest `o:<hex>`, no LIKE metacharacters.)
|
|
3236
3471
|
doomed = conn.execute(
|
|
3237
3472
|
"SELECT journal_id FROM weekly_usage_snapshots "
|
|
3238
|
-
"WHERE week_start_date = ? "
|
|
3473
|
+
"WHERE week_start_date = ? AND account_key = ? "
|
|
3239
3474
|
" AND unixepoch(captured_at_utc) >= unixepoch(?) "
|
|
3240
3475
|
" AND ABS(weekly_percent - ?) < 1.0 "
|
|
3241
3476
|
" AND journal_id IS NOT NULL "
|
|
3242
3477
|
" AND journal_id NOT LIKE 'sa:' || ? || ':syn:%'",
|
|
3243
|
-
(plan.week_start_date, effective_iso, pre_credit,
|
|
3478
|
+
(plan.week_start_date, account_key, effective_iso, pre_credit,
|
|
3479
|
+
id_base),
|
|
3244
3480
|
).fetchall()
|
|
3245
3481
|
supp = [r[0] for r in doomed]
|
|
3246
3482
|
floor_supp: list = []
|
|
@@ -3267,17 +3503,18 @@ def _apply_credit(conn, plan, *, five_hour=(None, None, None), as_of=None,
|
|
|
3267
3503
|
# content digest `o:<hex>`, no LIKE metacharacters.)
|
|
3268
3504
|
old_syn = conn.execute(
|
|
3269
3505
|
"SELECT journal_id FROM weekly_usage_snapshots "
|
|
3270
|
-
"WHERE week_start_date = ? AND
|
|
3506
|
+
"WHERE week_start_date = ? AND account_key = ? "
|
|
3507
|
+
" AND source = 'record-credit' "
|
|
3271
3508
|
" AND journal_id IS NOT NULL "
|
|
3272
3509
|
" AND journal_id NOT LIKE 'sa:' || ? || ':syn:%'",
|
|
3273
|
-
(plan.week_start_date, id_base),
|
|
3510
|
+
(plan.week_start_date, account_key, id_base),
|
|
3274
3511
|
).fetchall()
|
|
3275
3512
|
supp.extend(r[0] for r in old_syn if r[0] not in supp)
|
|
3276
3513
|
old_floors = conn.execute(
|
|
3277
3514
|
"SELECT journal_id FROM weekly_credit_floors "
|
|
3278
|
-
"WHERE week_start_date = ? AND
|
|
3279
|
-
" AND journal_id != ?",
|
|
3280
|
-
(plan.week_start_date, id_base),
|
|
3515
|
+
"WHERE week_start_date = ? AND account_key = ? "
|
|
3516
|
+
" AND journal_id IS NOT NULL AND journal_id != ?",
|
|
3517
|
+
(plan.week_start_date, account_key, id_base),
|
|
3281
3518
|
).fetchall()
|
|
3282
3519
|
floor_supp = [r[0] for r in old_floors]
|
|
3283
3520
|
# wce evt (effects-only, table=None): snapshot suppression list + floor
|
|
@@ -3307,7 +3544,7 @@ def _apply_credit(conn, plan, *, five_hour=(None, None, None), as_of=None,
|
|
|
3307
3544
|
# load-bearing (stubbing it disables the synthetic on the ingest path too).
|
|
3308
3545
|
c._insert_credit_snapshot(
|
|
3309
3546
|
conn, plan, five_hour=five_hour, commit=False,
|
|
3310
|
-
journal=(ctx, f"sa:{id_base}:syn:0"))
|
|
3547
|
+
journal=(ctx, f"sa:{id_base}:syn:0"), account_key=account_key)
|
|
3311
3548
|
|
|
3312
3549
|
# 4e. Clear a stale same-week reset-zero marker so the next record-usage
|
|
3313
3550
|
# tick can't confirm a phantom reset-to-zero off it.
|
|
@@ -3426,7 +3663,7 @@ def _revalidate_credit_plan(conn, args, *, now, at_dt, expected_plan):
|
|
|
3426
3663
|
elif existing is not None and existing[2] is not None:
|
|
3427
3664
|
from_pct, from_source = float(existing[2]), "prior_credit"
|
|
3428
3665
|
else:
|
|
3429
|
-
from_pct = _resolve_reset_aware_hwm(conn, week_start_date, ws_at, we_at)
|
|
3666
|
+
from_pct = _resolve_reset_aware_hwm(conn, week_start_date, ws_at, we_at, account_key=None)
|
|
3430
3667
|
if from_pct is None:
|
|
3431
3668
|
return None
|
|
3432
3669
|
from_source = "hwm"
|
|
@@ -3514,7 +3751,7 @@ def cmd_record_credit(args) -> int:
|
|
|
3514
3751
|
# is 'prior_credit' (spec §5).
|
|
3515
3752
|
from_pct, from_source = float(existing[2]), "prior_credit"
|
|
3516
3753
|
else:
|
|
3517
|
-
hwm = _resolve_reset_aware_hwm(conn, week_start_date, ws_at, we_at)
|
|
3754
|
+
hwm = _resolve_reset_aware_hwm(conn, week_start_date, ws_at, we_at, account_key=None)
|
|
3518
3755
|
if hwm is None:
|
|
3519
3756
|
eprint("record-credit: no usage history for the week; pass --from")
|
|
3520
3757
|
return 2
|
|
@@ -3562,7 +3799,7 @@ def cmd_record_credit(args) -> int:
|
|
|
3562
3799
|
is_dry = getattr(args, "dry_run", False)
|
|
3563
3800
|
is_yes = getattr(args, "yes", False)
|
|
3564
3801
|
stale_replays = _count_stale_replays(conn, plan)
|
|
3565
|
-
hwm_before = _resolve_reset_aware_hwm(conn, week_start_date, ws_at, we_at)
|
|
3802
|
+
hwm_before = _resolve_reset_aware_hwm(conn, week_start_date, ws_at, we_at, account_key=None)
|
|
3566
3803
|
if hwm_before is None:
|
|
3567
3804
|
hwm_before = plan.from_pct
|
|
3568
3805
|
|
|
@@ -3638,7 +3875,8 @@ def cmd_record_credit(args) -> int:
|
|
|
3638
3875
|
plan, existing, is_completion = revalidated
|
|
3639
3876
|
stale_replays = _count_stale_replays(conn, plan)
|
|
3640
3877
|
hwm_before = _resolve_reset_aware_hwm(
|
|
3641
|
-
conn, plan.week_start_date, plan.week_start_at, plan.week_end_at
|
|
3878
|
+
conn, plan.week_start_date, plan.week_start_at, plan.week_end_at,
|
|
3879
|
+
account_key=None
|
|
3642
3880
|
)
|
|
3643
3881
|
if hwm_before is None:
|
|
3644
3882
|
hwm_before = plan.from_pct
|
|
@@ -3670,6 +3908,22 @@ def cmd_record_credit(args) -> int:
|
|
|
3670
3908
|
effective_utc = parse_iso_datetime(
|
|
3671
3909
|
plan.effective_iso, "op.effective"
|
|
3672
3910
|
).astimezone(dt.timezone.utc).isoformat(timespec="seconds")
|
|
3911
|
+
# Active-account gate (#341 P2-1, spec §3): record-credit is
|
|
3912
|
+
# active-account-only. Resolve the active Claude account and stamp
|
|
3913
|
+
# the credit op so the floor lands under the SAME account post-Step-9
|
|
3914
|
+
# usage carries — else the account-scoped `_reset_aware_floor` clamp
|
|
3915
|
+
# would never see a real credit floor. A TORN read (transient
|
|
3916
|
+
# mid-write ~/.claude.json) means the active identity is genuinely
|
|
3917
|
+
# unavailable -> exit 2 (retry). A stably-absent read (no
|
|
3918
|
+
# ~/.claude.json / api-key mode) is a RESOLVED `unattributed` outcome
|
|
3919
|
+
# (single-account / legacy install), NOT unavailable -> proceed
|
|
3920
|
+
# byte-identically to pre-#341.
|
|
3921
|
+
identity = _cctally_core._resolve_active_claude_identity()
|
|
3922
|
+
if identity.get("status") == "torn":
|
|
3923
|
+
eprint("record-credit: active Claude account is unavailable "
|
|
3924
|
+
"(torn read of ~/.claude.json); retry once it settles")
|
|
3925
|
+
return 2
|
|
3926
|
+
credit_account_key = identity["account_key"]
|
|
3673
3927
|
import _cctally_journal as _jr
|
|
3674
3928
|
import _lib_journal as _lj
|
|
3675
3929
|
op = _lj.make_op(
|
|
@@ -3684,6 +3938,9 @@ def cmd_record_credit(args) -> int:
|
|
|
3684
3938
|
"plan": dict(vars(plan)),
|
|
3685
3939
|
"five_hour": list(five_hour),
|
|
3686
3940
|
"forced": forced,
|
|
3941
|
+
# Two-shaped stamp (#341): evt/op carry account_key in the
|
|
3942
|
+
# payload; the op fold + `_apply_credit` both read it.
|
|
3943
|
+
"account_key": credit_account_key,
|
|
3687
3944
|
},
|
|
3688
3945
|
)
|
|
3689
3946
|
# Close the command's read connection BEFORE the ingester (the sole
|
|
@@ -3857,14 +4114,63 @@ def cmd_record_usage(
|
|
|
3857
4114
|
|
|
3858
4115
|
import _cctally_journal as _jr
|
|
3859
4116
|
import _lib_journal as _lj
|
|
4117
|
+
import _lib_accounts
|
|
4118
|
+
# Observe-and-stamp (#341, spec §1): resolve the active Claude account from
|
|
4119
|
+
# ~/.claude.json (stable-read, mtime-cached) and stamp the usage obs with it.
|
|
4120
|
+
# Byte-safe: a real account is stamped explicitly; the reserved sentinel
|
|
4121
|
+
# (no ~/.claude.json / api-key / torn) OMITS the field, so a single-account /
|
|
4122
|
+
# legacy install's journal obs stays byte-identical to today (the pipeline's
|
|
4123
|
+
# `rec.get("account") or UNATTRIBUTED` fallback treats both the same).
|
|
4124
|
+
obs_at = now_utc_iso(now_dt)
|
|
4125
|
+
identity = _cctally_core._resolve_active_claude_identity()
|
|
4126
|
+
account_key = identity["account_key"]
|
|
4127
|
+
obs_account = None if account_key == _lib_accounts.UNATTRIBUTED else account_key
|
|
4128
|
+
if obs_account is not None:
|
|
4129
|
+
# Journal the account_observe DURABLY BEFORE the account-stamped usage obs
|
|
4130
|
+
# (spec §1: replay can never see a stamped row whose account was never
|
|
4131
|
+
# observed). First-sight/identity-change only — marker-deduped so it is
|
|
4132
|
+
# not appended every tick.
|
|
4133
|
+
_maybe_append_account_observe(identity, at=obs_at)
|
|
3860
4134
|
_jr.append_record(_lj.make_obs(
|
|
3861
|
-
at=
|
|
4135
|
+
at=obs_at, src=writer, provider="claude", payload=raw,
|
|
4136
|
+
account=obs_account))
|
|
3862
4137
|
# authoritative observes its own write synchronously; opportunistic skips a
|
|
3863
4138
|
# busy ingest lock and lets the current holder consume the appended line.
|
|
3864
4139
|
_jr.run_stats_ingest(mode=ingest_mode)
|
|
3865
4140
|
return 0
|
|
3866
4141
|
|
|
3867
4142
|
|
|
4143
|
+
_OBSERVED_ACCOUNT_MARKER = "observed-claude-account"
|
|
4144
|
+
|
|
4145
|
+
|
|
4146
|
+
def _maybe_append_account_observe(identity: dict, *, at: str) -> None:
|
|
4147
|
+
"""Append an ``account_observe`` op on first sight of a Claude account or an
|
|
4148
|
+
identity change (#341, spec §1) — NOT every tick. Deduped by a marker file in
|
|
4149
|
+
APP_DIR so a hot status-line loop journals at most one observe per account.
|
|
4150
|
+
Best-effort: a marker/journal hiccup never breaks record-usage."""
|
|
4151
|
+
import _lib_accounts
|
|
4152
|
+
account_key = identity.get("account_key")
|
|
4153
|
+
if not account_key or account_key == _lib_accounts.UNATTRIBUTED:
|
|
4154
|
+
return
|
|
4155
|
+
marker = _cctally_core.APP_DIR / _OBSERVED_ACCOUNT_MARKER
|
|
4156
|
+
try:
|
|
4157
|
+
last = marker.read_text().strip()
|
|
4158
|
+
except OSError:
|
|
4159
|
+
last = None
|
|
4160
|
+
if last == account_key:
|
|
4161
|
+
return
|
|
4162
|
+
try:
|
|
4163
|
+
import _cctally_journal as _jr
|
|
4164
|
+
import _lib_journal as _lj
|
|
4165
|
+
_jr.append_record(_lj.make_account_observe(
|
|
4166
|
+
at=at, account_key=account_key, provider="claude",
|
|
4167
|
+
natural_id=identity.get("natural_id"), email=identity.get("email"),
|
|
4168
|
+
plan_type=identity.get("plan_type"), label_source="auto"))
|
|
4169
|
+
marker.write_text(account_key + "\n")
|
|
4170
|
+
except OSError:
|
|
4171
|
+
pass
|
|
4172
|
+
|
|
4173
|
+
|
|
3868
4174
|
def _hook_tick_log_line(line: str) -> None:
|
|
3869
4175
|
"""Append one line to hook-tick.log; create dir if missing.
|
|
3870
4176
|
|
|
@@ -4227,7 +4533,13 @@ def _cmd_hook_tick_codex(args: argparse.Namespace, *, event: str = "unknown") ->
|
|
|
4227
4533
|
contextlib.redirect_stdout(quiet), contextlib.redirect_stderr(quiet):
|
|
4228
4534
|
cache = c.open_cache_db()
|
|
4229
4535
|
try:
|
|
4230
|
-
|
|
4536
|
+
cache_mod = c._load_sibling("_cctally_cache")
|
|
4537
|
+
stats, cache = cache_mod._run_cache_operation_with_recovery(
|
|
4538
|
+
cache,
|
|
4539
|
+
lambda active_conn: c.sync_codex_cache(
|
|
4540
|
+
active_conn, lock_timeout=0
|
|
4541
|
+
),
|
|
4542
|
+
)
|
|
4231
4543
|
finally:
|
|
4232
4544
|
cache.close()
|
|
4233
4545
|
if stats.lock_contended:
|
|
@@ -4400,7 +4712,13 @@ def cmd_hook_tick(args: argparse.Namespace) -> int:
|
|
|
4400
4712
|
try:
|
|
4401
4713
|
cache_conn = open_cache_db()
|
|
4402
4714
|
try:
|
|
4403
|
-
|
|
4715
|
+
cache_mod = _cctally()._load_sibling("_cctally_cache")
|
|
4716
|
+
stats, cache_conn = (
|
|
4717
|
+
cache_mod._run_cache_operation_with_recovery(
|
|
4718
|
+
cache_conn,
|
|
4719
|
+
lambda active_conn: sync_cache(active_conn),
|
|
4720
|
+
)
|
|
4721
|
+
)
|
|
4404
4722
|
ingested = int(stats.rows_changed)
|
|
4405
4723
|
parse_malformed = int(stats.lines_malformed)
|
|
4406
4724
|
parse_skipped = int(stats.assistant_lines_skipped)
|
|
@@ -4610,6 +4928,7 @@ _USAGE_SNAPSHOT_COLUMNS = (
|
|
|
4610
4928
|
"captured_at_utc", "week_start_date", "week_end_date", "week_start_at",
|
|
4611
4929
|
"week_end_at", "weekly_percent", "page_url", "source", "payload_json",
|
|
4612
4930
|
"five_hour_percent", "five_hour_resets_at", "five_hour_window_key",
|
|
4931
|
+
"account_key",
|
|
4613
4932
|
)
|
|
4614
4933
|
|
|
4615
4934
|
|
|
@@ -4685,6 +5004,10 @@ def _usage_snapshot_columns(conn, payload, week_start_name):
|
|
|
4685
5004
|
"five_hour_percent": five_hour_percent,
|
|
4686
5005
|
"five_hour_resets_at": five_hour_resets_at,
|
|
4687
5006
|
"five_hour_window_key": five_hour_window_key,
|
|
5007
|
+
# Account dimension (#341): carried from the payload when present (the
|
|
5008
|
+
# snapshot_accept emit path threads the resolved account through here);
|
|
5009
|
+
# defaults to the reserved sentinel for the bare test-only insert path.
|
|
5010
|
+
"account_key": payload.get("account_key") or "unattributed",
|
|
4688
5011
|
}
|
|
4689
5012
|
|
|
4690
5013
|
saved = {
|
|
@@ -4727,9 +5050,10 @@ def insert_usage_snapshot(payload: dict[str, Any], week_start_name: str) -> dict
|
|
|
4727
5050
|
payload_json,
|
|
4728
5051
|
five_hour_percent,
|
|
4729
5052
|
five_hour_resets_at,
|
|
4730
|
-
five_hour_window_key
|
|
5053
|
+
five_hour_window_key,
|
|
5054
|
+
account_key
|
|
4731
5055
|
)
|
|
4732
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
5056
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
4733
5057
|
""",
|
|
4734
5058
|
tuple(cols[k] for k in _USAGE_SNAPSHOT_COLUMNS),
|
|
4735
5059
|
)
|
|
@@ -4970,6 +5294,12 @@ def _pipeline_claude_usage(ctx, rec):
|
|
|
4970
5294
|
weekly_percent = float(payload["weekly_percent"])
|
|
4971
5295
|
resets_at = int(payload["resets_at"])
|
|
4972
5296
|
source = payload.get("source", "statusline")
|
|
5297
|
+
# Account dimension (#341): the obs carries the account stamp (record-usage /
|
|
5298
|
+
# statusline resolve it once per read, Step 9). Absent (pre-multi-account /
|
|
5299
|
+
# legacy obs) -> the reserved sentinel, which keeps every scoped query
|
|
5300
|
+
# byte-identical on a single-account install.
|
|
5301
|
+
import _lib_accounts
|
|
5302
|
+
account_key = rec.get("account") or _lib_accounts.UNATTRIBUTED
|
|
4973
5303
|
|
|
4974
5304
|
# Week boundaries from resets_at (cmd_record_usage canonicalization).
|
|
4975
5305
|
week_end_at_dt = dt.datetime.fromtimestamp(resets_at, tz=dt.timezone.utc)
|
|
@@ -5004,6 +5334,7 @@ def _pipeline_claude_usage(ctx, rec):
|
|
|
5004
5334
|
as_of=as_of,
|
|
5005
5335
|
commit=False,
|
|
5006
5336
|
ctx=ctx,
|
|
5337
|
+
account_key=account_key,
|
|
5007
5338
|
)
|
|
5008
5339
|
|
|
5009
5340
|
# 2. Accept/skip DECISION (clamp + dedup), made ONCE and journaled via the
|
|
@@ -5016,6 +5347,7 @@ def _pipeline_claude_usage(ctx, rec):
|
|
|
5016
5347
|
"weekly_percent": weekly_percent,
|
|
5017
5348
|
"five_hour_percent": five_hour_percent,
|
|
5018
5349
|
"five_hour_window_key": five_hour_window_key,
|
|
5350
|
+
"account_key": account_key,
|
|
5019
5351
|
})
|
|
5020
5352
|
five_hour_percent = adjusted_5h
|
|
5021
5353
|
|
|
@@ -5046,6 +5378,9 @@ def _pipeline_claude_usage(ctx, rec):
|
|
|
5046
5378
|
|
|
5047
5379
|
week_start_name = get_week_start_name(ctx.config or {}, None)
|
|
5048
5380
|
cols, saved = _usage_snapshot_columns(conn, out_payload, week_start_name)
|
|
5381
|
+
# Stamp the account onto the snapshot_accept evt columns so the journaled
|
|
5382
|
+
# row (and every replay/rebuild fold of it) carries the account (#341).
|
|
5383
|
+
cols["account_key"] = account_key
|
|
5049
5384
|
rowid = jr.emit_model_a(
|
|
5050
5385
|
ctx,
|
|
5051
5386
|
kind="snapshot_accept",
|
|
@@ -5058,8 +5393,9 @@ def _pipeline_claude_usage(ctx, rec):
|
|
|
5058
5393
|
else:
|
|
5059
5394
|
latest = conn.execute(
|
|
5060
5395
|
"SELECT * FROM weekly_usage_snapshots WHERE week_start_date = ? "
|
|
5396
|
+
" AND account_key = ? "
|
|
5061
5397
|
"ORDER BY captured_at_utc DESC, id DESC LIMIT 1",
|
|
5062
|
-
(week_start_date,),
|
|
5398
|
+
(week_start_date, account_key),
|
|
5063
5399
|
).fetchone()
|
|
5064
5400
|
if latest is None:
|
|
5065
5401
|
return # nothing recorded yet -> nothing to derive against
|
|
@@ -5073,9 +5409,10 @@ def _pipeline_claude_usage(ctx, rec):
|
|
|
5073
5409
|
# crosses a $ threshold still derives it.
|
|
5074
5410
|
c.maybe_record_milestone(
|
|
5075
5411
|
saved, conn=conn, as_of=capture_at, alert_sink=ctx.pending_alerts,
|
|
5076
|
-
journal=(ctx, rec["id"]))
|
|
5412
|
+
journal=(ctx, rec["id"]), account_key=account_key)
|
|
5077
5413
|
c.maybe_update_five_hour_block(
|
|
5078
|
-
saved, conn=conn, as_of=capture_at, alert_sink=ctx.pending_alerts
|
|
5414
|
+
saved, conn=conn, as_of=capture_at, alert_sink=ctx.pending_alerts,
|
|
5415
|
+
account_key=account_key)
|
|
5079
5416
|
# The dollar-decoupled axes resolve a CURRENT budget/period WINDOW from
|
|
5080
5417
|
# "now" and must see the record's DETECTION clock (`as_of` = rec["at"] =
|
|
5081
5418
|
# `_command_as_of()`), NOT the wall-clock capture stamp: legacy
|
|
@@ -5107,7 +5444,8 @@ def _pipeline_claude_usage(ctx, rec):
|
|
|
5107
5444
|
if latest_wk is None or int(latest_wk) != int(five_hour_window_key):
|
|
5108
5445
|
if conn.execute(
|
|
5109
5446
|
"SELECT 1 FROM five_hour_blocks WHERE five_hour_window_key = ? "
|
|
5110
|
-
"LIMIT 1",
|
|
5447
|
+
" AND account_key = ? LIMIT 1",
|
|
5448
|
+
(int(five_hour_window_key), account_key),
|
|
5111
5449
|
).fetchone() is None:
|
|
5112
5450
|
c.maybe_update_five_hour_block(
|
|
5113
5451
|
{
|
|
@@ -5118,7 +5456,8 @@ def _pipeline_claude_usage(ctx, rec):
|
|
|
5118
5456
|
"fiveHourResetsAt": five_hour_resets_at_str,
|
|
5119
5457
|
"fiveHourWindowKey": int(five_hour_window_key),
|
|
5120
5458
|
},
|
|
5121
|
-
conn=conn, as_of=capture_at, alert_sink=ctx.pending_alerts
|
|
5459
|
+
conn=conn, as_of=capture_at, alert_sink=ctx.pending_alerts,
|
|
5460
|
+
account_key=account_key)
|
|
5122
5461
|
|
|
5123
5462
|
# 5. hwm-7d / hwm-5h projection files — ACCEPT path only (the monotonic
|
|
5124
5463
|
# writer; a dedup tick's percent is already <= the stored HWM). In-place
|
|
@@ -5156,10 +5495,15 @@ def _pipeline_record_credit(ctx, rec):
|
|
|
5156
5495
|
c = _cctally()
|
|
5157
5496
|
plan = argparse.Namespace(**plan_data)
|
|
5158
5497
|
five_hour = tuple(payload.get("five_hour") or (None, None, None))
|
|
5498
|
+
# Two-shaped stamp (#341): the record-credit op carries account_key in its
|
|
5499
|
+
# payload (Step 9 / Task 3 resolves the active account before appending it);
|
|
5500
|
+
# default to the sentinel for a legacy op written pre-#341.
|
|
5501
|
+
import _lib_accounts
|
|
5159
5502
|
c._apply_credit(
|
|
5160
5503
|
ctx.conn, plan, five_hour=five_hour, as_of=rec["at"],
|
|
5161
5504
|
commit=False, ctx=ctx, id_base=rec["id"],
|
|
5162
|
-
forced=bool(payload.get("forced"))
|
|
5505
|
+
forced=bool(payload.get("forced")),
|
|
5506
|
+
account_key=(payload.get("account_key") or _lib_accounts.UNATTRIBUTED))
|
|
5163
5507
|
|
|
5164
5508
|
|
|
5165
5509
|
def _pipeline_sync_week(ctx, rec):
|
|
@@ -5184,8 +5528,14 @@ def _pipeline_sync_week(ctx, rec):
|
|
|
5184
5528
|
json=False,
|
|
5185
5529
|
quiet=True,
|
|
5186
5530
|
)
|
|
5531
|
+
# Two-shaped stamp (#341 P2-1): the sync_week op carries the active account
|
|
5532
|
+
# in its payload; the fold stamps the cost snapshot under it (rebuild-
|
|
5533
|
+
# deterministic). Absent -> the sentinel (single-account / legacy op).
|
|
5534
|
+
import _lib_accounts
|
|
5187
5535
|
c.cmd_sync_week(args, conn=ctx.conn, as_of=rec["at"],
|
|
5188
|
-
journal=(ctx, rec["id"])
|
|
5536
|
+
journal=(ctx, rec["id"]),
|
|
5537
|
+
account_key=(payload.get("account_key")
|
|
5538
|
+
or _lib_accounts.UNATTRIBUTED))
|
|
5189
5539
|
|
|
5190
5540
|
|
|
5191
5541
|
def _register_ingest_pipeline_hooks():
|