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
|
@@ -57,6 +57,8 @@ def compute_week_cost(
|
|
|
57
57
|
project: str | None,
|
|
58
58
|
start_iso_override: str | None = None,
|
|
59
59
|
end_iso_override: str | None = None,
|
|
60
|
+
*,
|
|
61
|
+
account_key: "str | None" = None,
|
|
60
62
|
) -> WeekCostResult:
|
|
61
63
|
# internal fallback: host-local intentional
|
|
62
64
|
now_local = dt.datetime.now().astimezone()
|
|
@@ -101,7 +103,11 @@ def compute_week_cost(
|
|
|
101
103
|
end_dt = parse_iso_datetime(end_iso, "end")
|
|
102
104
|
|
|
103
105
|
c = _cctally()
|
|
104
|
-
cost
|
|
106
|
+
# #341 P2-CQ2: scope the cost sum to the account the snapshot is stamped for
|
|
107
|
+
# (None = merged / byte-identical) so per-account weekly_cost_snapshots + the
|
|
108
|
+
# milestone $/1% cumulative cost carry genuinely per-account cost.
|
|
109
|
+
cost = c._sum_cost_for_range(
|
|
110
|
+
start_dt, end_dt, mode=mode, project=project, account_key=account_key)
|
|
105
111
|
|
|
106
112
|
return WeekCostResult(
|
|
107
113
|
week_start=week_start,
|
|
@@ -112,8 +118,12 @@ def compute_week_cost(
|
|
|
112
118
|
)
|
|
113
119
|
|
|
114
120
|
|
|
115
|
-
def get_latest_cost_for_week(
|
|
116
|
-
|
|
121
|
+
def get_latest_cost_for_week(
|
|
122
|
+
conn: sqlite3.Connection, week_ref: WeekRef, *,
|
|
123
|
+
account_key: "str | None" = None,
|
|
124
|
+
) -> sqlite3.Row | None:
|
|
125
|
+
return _get_latest_row_for_week(
|
|
126
|
+
conn, "weekly_cost_snapshots", week_ref, account_key=account_key)
|
|
117
127
|
|
|
118
128
|
|
|
119
129
|
def insert_cost_snapshot(
|
|
@@ -131,9 +141,15 @@ def insert_cost_snapshot(
|
|
|
131
141
|
commit: bool = True,
|
|
132
142
|
as_of: "str | None" = None,
|
|
133
143
|
journal: "tuple | None" = None,
|
|
144
|
+
account_key: str = "unattributed",
|
|
134
145
|
) -> int:
|
|
135
146
|
"""Insert a ``weekly_cost_snapshots`` row and return its rowid.
|
|
136
147
|
|
|
148
|
+
``account_key`` (#341): the account dimension of the write. Default
|
|
149
|
+
``"unattributed"`` mirrors the schema DEFAULT (rev 4.1 defensive backstop);
|
|
150
|
+
per-account ``sync-week`` materialization (Step 9 / Task 3) passes the
|
|
151
|
+
resolved account explicitly.
|
|
152
|
+
|
|
137
153
|
Transaction-neutral / capture-time-pure seam (DB journal redesign §5.2.3):
|
|
138
154
|
``commit=False`` skips the inner ``conn.commit()`` so the ingester can bundle
|
|
139
155
|
this insert into its single cycle transaction; ``as_of`` (ISO-Z) overrides
|
|
@@ -174,6 +190,7 @@ def insert_cost_snapshot(
|
|
|
174
190
|
"cost_usd": cost_usd,
|
|
175
191
|
"mode": mode,
|
|
176
192
|
"project": project,
|
|
193
|
+
"account_key": account_key,
|
|
177
194
|
},
|
|
178
195
|
at=captured,
|
|
179
196
|
)
|
|
@@ -190,9 +207,10 @@ def insert_cost_snapshot(
|
|
|
190
207
|
range_end_iso,
|
|
191
208
|
cost_usd,
|
|
192
209
|
mode,
|
|
193
|
-
project
|
|
210
|
+
project,
|
|
211
|
+
account_key
|
|
194
212
|
)
|
|
195
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
213
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
196
214
|
""",
|
|
197
215
|
(
|
|
198
216
|
captured,
|
|
@@ -205,6 +223,7 @@ def insert_cost_snapshot(
|
|
|
205
223
|
cost_usd,
|
|
206
224
|
mode,
|
|
207
225
|
project,
|
|
226
|
+
account_key,
|
|
208
227
|
),
|
|
209
228
|
)
|
|
210
229
|
if commit:
|
|
@@ -217,6 +236,7 @@ def get_max_milestone_for_week(
|
|
|
217
236
|
week_start_date: str,
|
|
218
237
|
*,
|
|
219
238
|
reset_event_id: int = 0,
|
|
239
|
+
account_key: str,
|
|
220
240
|
) -> int | None:
|
|
221
241
|
"""Return the highest percent_threshold recorded for a week's segment,
|
|
222
242
|
or None.
|
|
@@ -227,6 +247,11 @@ def get_max_milestone_for_week(
|
|
|
227
247
|
segment id so the segment's threshold ledger is independent of the
|
|
228
248
|
pre-credit one — the post-credit 1% / 2% / 3% milestones fire even
|
|
229
249
|
if the pre-credit segment already crossed those thresholds.
|
|
250
|
+
|
|
251
|
+
``account_key`` (#341, review finding 11): MANDATORY — the max is scoped
|
|
252
|
+
to one account's ledger so a second account crossing the SAME threshold in
|
|
253
|
+
the SAME week is not silently deduped against the first. No silent global
|
|
254
|
+
fallback: the caller resolves the account being processed.
|
|
230
255
|
"""
|
|
231
256
|
row = conn.execute(
|
|
232
257
|
"""
|
|
@@ -234,8 +259,9 @@ def get_max_milestone_for_week(
|
|
|
234
259
|
FROM percent_milestones
|
|
235
260
|
WHERE week_start_date = ?
|
|
236
261
|
AND reset_event_id = ?
|
|
262
|
+
AND account_key = ?
|
|
237
263
|
""",
|
|
238
|
-
(week_start_date, reset_event_id),
|
|
264
|
+
(week_start_date, reset_event_id, account_key),
|
|
239
265
|
).fetchone()
|
|
240
266
|
if row and row["max_pct"] is not None:
|
|
241
267
|
return int(row["max_pct"])
|
|
@@ -248,6 +274,7 @@ def get_milestone_cost_for_week(
|
|
|
248
274
|
percent_threshold: int,
|
|
249
275
|
*,
|
|
250
276
|
reset_event_id: int = 0,
|
|
277
|
+
account_key: str,
|
|
251
278
|
) -> float | None:
|
|
252
279
|
"""Return the cumulative_cost_usd for a specific (week, threshold,
|
|
253
280
|
segment), or None.
|
|
@@ -257,6 +284,9 @@ def get_milestone_cost_for_week(
|
|
|
257
284
|
marginal cost between consecutive thresholds inside the SAME segment
|
|
258
285
|
— without the filter, the post-credit threshold-3 row would compute
|
|
259
286
|
its marginal against the pre-credit threshold-2 cost (wrong segment).
|
|
287
|
+
|
|
288
|
+
``account_key`` (#341): MANDATORY — scoped to one account's ledger (a
|
|
289
|
+
marginal must be computed against the SAME account's prior threshold row).
|
|
260
290
|
"""
|
|
261
291
|
row = conn.execute(
|
|
262
292
|
"""
|
|
@@ -265,8 +295,9 @@ def get_milestone_cost_for_week(
|
|
|
265
295
|
WHERE week_start_date = ?
|
|
266
296
|
AND percent_threshold = ?
|
|
267
297
|
AND reset_event_id = ?
|
|
298
|
+
AND account_key = ?
|
|
268
299
|
""",
|
|
269
|
-
(week_start_date, percent_threshold, reset_event_id),
|
|
300
|
+
(week_start_date, percent_threshold, reset_event_id, account_key),
|
|
270
301
|
).fetchone()
|
|
271
302
|
if row:
|
|
272
303
|
return float(row["cumulative_cost_usd"])
|
|
@@ -276,16 +307,25 @@ def get_milestone_cost_for_week(
|
|
|
276
307
|
def get_milestones_for_week(
|
|
277
308
|
conn: sqlite3.Connection,
|
|
278
309
|
week_start_date: str,
|
|
310
|
+
*,
|
|
311
|
+
account_key: "str | None" = None,
|
|
279
312
|
) -> list[sqlite3.Row]:
|
|
280
|
-
"""Return all milestones for a week, ordered by threshold ascending.
|
|
313
|
+
"""Return all milestones for a week, ordered by threshold ascending.
|
|
314
|
+
|
|
315
|
+
``account_key`` (#341, spec §3): ``None`` = the account-blind merged read
|
|
316
|
+
(today's byte-identical behavior); a real key / ``unattributed`` scopes the
|
|
317
|
+
``percent_milestones`` read to that account (``percent-breakdown --account``).
|
|
318
|
+
"""
|
|
319
|
+
acct_pred = "" if account_key is None else " AND account_key = ?"
|
|
320
|
+
acct_p: tuple = () if account_key is None else (account_key,)
|
|
281
321
|
return conn.execute(
|
|
282
|
-
"""
|
|
322
|
+
f"""
|
|
283
323
|
SELECT *
|
|
284
324
|
FROM percent_milestones
|
|
285
|
-
WHERE week_start_date = ?
|
|
325
|
+
WHERE week_start_date = ?{acct_pred}
|
|
286
326
|
ORDER BY percent_threshold ASC
|
|
287
327
|
""",
|
|
288
|
-
(week_start_date,),
|
|
328
|
+
(week_start_date,) + acct_p,
|
|
289
329
|
).fetchall()
|
|
290
330
|
|
|
291
331
|
|
|
@@ -305,9 +345,18 @@ def insert_percent_milestone(
|
|
|
305
345
|
commit: bool = True,
|
|
306
346
|
reset_event_id: int = 0,
|
|
307
347
|
as_of: "str | None" = None,
|
|
348
|
+
account_key: str = "unattributed",
|
|
308
349
|
) -> int:
|
|
309
350
|
"""Insert a percent_milestones row idempotently.
|
|
310
351
|
|
|
352
|
+
``account_key`` (#341): the account dimension of the write. Default
|
|
353
|
+
``"unattributed"`` mirrors the schema DEFAULT as a defensive backstop
|
|
354
|
+
(rev 4.1); the production writer (``maybe_record_milestone``) passes the
|
|
355
|
+
resolved account explicitly, enforced by the structural writer-audit test.
|
|
356
|
+
Participates in the ``UNIQUE(account_key, week_start_date, percent_threshold,
|
|
357
|
+
reset_event_id)`` dedup key so two accounts crossing the same threshold in
|
|
358
|
+
the same week each get their own row.
|
|
359
|
+
|
|
311
360
|
``as_of`` (DB journal redesign §5.2.3): overrides the ``captured_at_utc``
|
|
312
361
|
wall-clock stamp with the record's capture time when the ingester drives
|
|
313
362
|
this at fold time; ``None`` keeps the legacy ``now_utc_iso()`` behavior.
|
|
@@ -356,9 +405,10 @@ def insert_percent_milestone(
|
|
|
356
405
|
usage_snapshot_id,
|
|
357
406
|
cost_snapshot_id,
|
|
358
407
|
five_hour_percent_at_crossing,
|
|
359
|
-
reset_event_id
|
|
408
|
+
reset_event_id,
|
|
409
|
+
account_key
|
|
360
410
|
)
|
|
361
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
411
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
362
412
|
""",
|
|
363
413
|
(
|
|
364
414
|
as_of or now_utc_iso(),
|
|
@@ -373,6 +423,7 @@ def insert_percent_milestone(
|
|
|
373
423
|
cost_snapshot_id,
|
|
374
424
|
five_hour_percent_at_crossing,
|
|
375
425
|
reset_event_id,
|
|
426
|
+
account_key,
|
|
376
427
|
),
|
|
377
428
|
)
|
|
378
429
|
if commit:
|
|
@@ -392,6 +443,7 @@ def insert_budget_milestone(
|
|
|
392
443
|
consumption_pct: float,
|
|
393
444
|
commit: bool = True,
|
|
394
445
|
as_of: "str | None" = None,
|
|
446
|
+
account_key: str = "*",
|
|
395
447
|
) -> int:
|
|
396
448
|
"""INSERT OR IGNORE a budget threshold crossing into the unified vendor-tagged
|
|
397
449
|
table (#143). Returns ``cur.rowcount`` (1 = genuinely new crossing, 0 =
|
|
@@ -407,7 +459,11 @@ def insert_budget_milestone(
|
|
|
407
459
|
``period`` (#137) is the configured period noun at crossing
|
|
408
460
|
('calendar-week'|'calendar-month'|'subscription-week'); it discriminates the
|
|
409
461
|
UNIQUE key so calendar-week and calendar-month windows that share a start
|
|
410
|
-
instant don't collide.
|
|
462
|
+
instant don't collide. ``account_key`` (#341 Step 4-eval) discriminates the
|
|
463
|
+
per-account ladder from the vendor-wide ladder: ``"*"`` (the schema DEFAULT +
|
|
464
|
+
this arg's default) is the vendor-wide row; a real account key is that
|
|
465
|
+
account's own ladder — ``UNIQUE(vendor, account_key, period_start_at, period,
|
|
466
|
+
threshold)``. A NULL ``period`` is the pre-011 "unknown" sentinel
|
|
411
467
|
(only seeded migration rows carry it). ``alerted_at`` is left NULL — the
|
|
412
468
|
caller stamps it in the SAME transaction BEFORE dispatching (set-then-dispatch
|
|
413
469
|
invariant, CLAUDE.md Alerts gotcha). ``commit=False`` lets the caller bundle
|
|
@@ -416,11 +472,12 @@ def insert_budget_milestone(
|
|
|
416
472
|
"""
|
|
417
473
|
cur = conn.execute(
|
|
418
474
|
"INSERT OR IGNORE INTO budget_milestones "
|
|
419
|
-
"(vendor, period_start_at, period, threshold, budget_usd,
|
|
420
|
-
" consumption_pct, crossed_at_utc) "
|
|
421
|
-
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
|
475
|
+
"(vendor, account_key, period_start_at, period, threshold, budget_usd, "
|
|
476
|
+
" spent_usd, consumption_pct, crossed_at_utc) "
|
|
477
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
422
478
|
(
|
|
423
479
|
str(vendor),
|
|
480
|
+
str(account_key),
|
|
424
481
|
period_start_at,
|
|
425
482
|
period,
|
|
426
483
|
int(threshold),
|
|
@@ -562,7 +619,8 @@ def _projected_levels_already_latched(
|
|
|
562
619
|
return all(int(level) in have for level in levels)
|
|
563
620
|
|
|
564
621
|
|
|
565
|
-
def _resolve_claude_budget_window(conn, now_utc, *, period, config, tz
|
|
622
|
+
def _resolve_claude_budget_window(conn, now_utc, *, period, config, tz,
|
|
623
|
+
window_account_key=None):
|
|
566
624
|
"""Resolve the Claude budget's ``(start_utc, end_utc)`` window for the
|
|
567
625
|
configured ``period`` (calendar-period-codex-budgets generalization, spec
|
|
568
626
|
§6). Subscription-week → the existing ``_resolve_current_budget_window``
|
|
@@ -570,14 +628,21 @@ def _resolve_claude_budget_window(conn, now_utc, *, period, config, tz):
|
|
|
570
628
|
yet). Calendar period → the pure ``_resolve_calendar_window`` (derived purely
|
|
571
629
|
from ``now`` + the period; NEVER ``None``). The dedup key column is now
|
|
572
630
|
``period_start_at`` (#143) — it carries the resolved PERIOD-start instant
|
|
573
|
-
(subscription-week OR calendar period-start).
|
|
631
|
+
(subscription-week OR calendar period-start).
|
|
632
|
+
|
|
633
|
+
``window_account_key`` (#341, spec §6 `*`-anchor): scopes the
|
|
634
|
+
subscription-week snapshot window to one account (the active account for a
|
|
635
|
+
`*` ladder, the ladder's own account for a per-account ladder). ``None`` =
|
|
636
|
+
merged (byte-identical); calendar periods ignore it (pure calendar)."""
|
|
574
637
|
c = _cctally()
|
|
575
638
|
if period == "subscription-week":
|
|
576
|
-
return c._resolve_current_budget_window(
|
|
639
|
+
return c._resolve_current_budget_window(
|
|
640
|
+
conn, now_utc, account_key=window_account_key)
|
|
577
641
|
return c._resolve_calendar_window(period, now_utc, config, tz)
|
|
578
642
|
|
|
579
643
|
|
|
580
|
-
def _resolve_budget_window(conn, *, vendor, now_utc, period, config, tz
|
|
644
|
+
def _resolve_budget_window(conn, *, vendor, now_utc, period, config, tz,
|
|
645
|
+
window_account_key=None):
|
|
581
646
|
"""Resolve the budget period-start instant for ``vendor`` (#143). CHEAP — does
|
|
582
647
|
NO cost SUM, preserving the pre-probe-before-spend hot path (spec §4.2): the
|
|
583
648
|
firing paths resolve this cheap window, pre-probe which thresholds are already
|
|
@@ -594,7 +659,8 @@ def _resolve_budget_window(conn, *, vendor, now_utc, period, config, tz):
|
|
|
594
659
|
pre-snapshot)."""
|
|
595
660
|
if vendor == "claude":
|
|
596
661
|
window = _resolve_claude_budget_window(
|
|
597
|
-
conn, now_utc, period=period, config=config, tz=tz
|
|
662
|
+
conn, now_utc, period=period, config=config, tz=tz,
|
|
663
|
+
window_account_key=window_account_key,
|
|
598
664
|
)
|
|
599
665
|
else:
|
|
600
666
|
window = _resolve_codex_budget_period_window(period, now_utc, config, tz)
|
|
@@ -604,20 +670,34 @@ def _resolve_budget_window(conn, *, vendor, now_utc, period, config, tz):
|
|
|
604
670
|
return start_at
|
|
605
671
|
|
|
606
672
|
|
|
607
|
-
def _budget_spend_for_vendor(conn, *, vendor, start_at, now_utc
|
|
673
|
+
def _budget_spend_for_vendor(conn, *, vendor, start_at, now_utc,
|
|
674
|
+
account_key: str = "*") -> float:
|
|
608
675
|
"""Spend over ``[start_at, now]`` for ``vendor`` (#143) — the COSTLY leg,
|
|
609
676
|
called only after the pre-probe finds pending thresholds (spec §4.2). claude
|
|
610
677
|
routes through the Claude cost SUM (``mode="auto"``); codex through the Codex
|
|
611
|
-
cost SUM.
|
|
678
|
+
cost SUM.
|
|
679
|
+
|
|
680
|
+
``account_key`` (#341 Step 4-eval, spec §6): the vendor-wide sentinel ``"*"``
|
|
681
|
+
sums EVERY account's spend (including ``unattributed`` — the guaranteed-
|
|
682
|
+
complete vendor total), so it reads unscoped (``account_key=None`` on the
|
|
683
|
+
cost SUM). A REAL account scopes the sum to that account's stamped entries so
|
|
684
|
+
a per-account ladder counts only its own spend (unattributed spend can never
|
|
685
|
+
trip a per-account alert)."""
|
|
612
686
|
c = _cctally()
|
|
687
|
+
scope = None if account_key == "*" else account_key
|
|
688
|
+
# Byte-stability: the vendor-wide (`*`) path calls with the EXACT legacy
|
|
689
|
+
# signature (no `account_key` kwarg), so existing cost-sum test doubles +
|
|
690
|
+
# every non-account install are untouched. Only a per-account ladder passes
|
|
691
|
+
# the kwarg (its doubles accept it).
|
|
692
|
+
extra = {} if scope is None else {"account_key": scope}
|
|
613
693
|
if vendor == "claude":
|
|
614
|
-
return c._sum_cost_for_range(start_at, now_utc, mode="auto")
|
|
615
|
-
return c._sum_codex_cost_for_range(start_at, now_utc)
|
|
694
|
+
return c._sum_cost_for_range(start_at, now_utc, mode="auto", **extra)
|
|
695
|
+
return c._sum_codex_cost_for_range(start_at, now_utc, **extra)
|
|
616
696
|
|
|
617
697
|
|
|
618
698
|
def _reconcile_budget_milestones_on_set(
|
|
619
699
|
conn, *, vendor, target, thresholds, now_utc, period, config=None, tz=None,
|
|
620
|
-
as_of=None, commit=True,
|
|
700
|
+
as_of=None, commit=True, account_key: str = "*",
|
|
621
701
|
):
|
|
622
702
|
"""Forward-only-from-set reconcile for the budget axis (both vendors, #143):
|
|
623
703
|
on `budget set`, every threshold ALREADY crossed for the current
|
|
@@ -644,7 +724,8 @@ def _reconcile_budget_milestones_on_set(
|
|
|
644
724
|
return
|
|
645
725
|
period_key = start_at.isoformat(timespec="seconds")
|
|
646
726
|
spent = _budget_spend_for_vendor(
|
|
647
|
-
conn, vendor=vendor, start_at=start_at, now_utc=now_utc
|
|
727
|
+
conn, vendor=vendor, start_at=start_at, now_utc=now_utc,
|
|
728
|
+
account_key=account_key,
|
|
648
729
|
)
|
|
649
730
|
# target > 0 guaranteed by the caller (validated weekly_usd / amount_usd);
|
|
650
731
|
# the else is belt-and-suspenders.
|
|
@@ -662,15 +743,17 @@ def _reconcile_budget_milestones_on_set(
|
|
|
662
743
|
consumption_pct=consumption_pct,
|
|
663
744
|
commit=False,
|
|
664
745
|
as_of=as_of,
|
|
746
|
+
account_key=account_key,
|
|
665
747
|
)
|
|
666
|
-
# alerted_at UPDATE keys on the CONCRETE (vendor, period)
|
|
667
|
-
# wildcard): only the row we just inserted is stamped, never
|
|
668
|
-
# pre-011 NULL-period sibling (#137)
|
|
748
|
+
# alerted_at UPDATE keys on the CONCRETE (vendor, account_key, period)
|
|
749
|
+
# (not the wildcard): only the row we just inserted is stamped, never
|
|
750
|
+
# a pre-011 NULL-period sibling (#137), another vendor's row (#143),
|
|
751
|
+
# or a different account's ladder (#341).
|
|
669
752
|
conn.execute(
|
|
670
753
|
"UPDATE budget_milestones SET alerted_at = ? "
|
|
671
|
-
"WHERE vendor = ? AND
|
|
672
|
-
" AND threshold = ? AND alerted_at IS NULL",
|
|
673
|
-
(as_of or now_utc_iso(), vendor, period_key, period, t),
|
|
754
|
+
"WHERE vendor = ? AND account_key = ? AND period_start_at = ? "
|
|
755
|
+
" AND period = ? AND threshold = ? AND alerted_at IS NULL",
|
|
756
|
+
(as_of or now_utc_iso(), vendor, account_key, period_key, period, t),
|
|
674
757
|
)
|
|
675
758
|
if commit:
|
|
676
759
|
conn.commit()
|
|
@@ -690,7 +773,7 @@ def _resolve_codex_budget_period_window(period, now_utc, config, tz):
|
|
|
690
773
|
|
|
691
774
|
def _budget_crossings(
|
|
692
775
|
conn, *, vendor, period_key, period=None, thresholds, target, spent, now_utc,
|
|
693
|
-
as_of=None,
|
|
776
|
+
as_of=None, account_key: str = "*",
|
|
694
777
|
):
|
|
695
778
|
"""Shared INSERT-and-arm core for the budget axis (both vendors, #143): for
|
|
696
779
|
every STILL-pending threshold that's been crossed at ``spent``, ``INSERT OR
|
|
@@ -727,17 +810,19 @@ def _budget_crossings(
|
|
|
727
810
|
consumption_pct=consumption_pct,
|
|
728
811
|
commit=False,
|
|
729
812
|
as_of=as_of,
|
|
813
|
+
account_key=account_key,
|
|
730
814
|
)
|
|
731
815
|
if inserted == 1:
|
|
732
816
|
crossed_at = as_of or now_utc_iso()
|
|
733
|
-
# alerted_at UPDATE keys on the CONCRETE (vendor,
|
|
734
|
-
# #143): only the row just inserted is
|
|
735
|
-
# NULL-period sibling
|
|
817
|
+
# alerted_at UPDATE keys on the CONCRETE (vendor, account_key,
|
|
818
|
+
# period) (#137 / #143 / #341): only the row just inserted is
|
|
819
|
+
# stamped, never a pre-011 NULL-period sibling, another vendor's
|
|
820
|
+
# row, or a different account's ladder.
|
|
736
821
|
conn.execute(
|
|
737
822
|
"UPDATE budget_milestones SET alerted_at = ? "
|
|
738
|
-
"WHERE vendor = ? AND
|
|
739
|
-
" AND threshold = ? AND alerted_at IS NULL",
|
|
740
|
-
(crossed_at, vendor, period_key, period, t),
|
|
823
|
+
"WHERE vendor = ? AND account_key = ? AND period_start_at = ? "
|
|
824
|
+
" AND period = ? AND threshold = ? AND alerted_at IS NULL",
|
|
825
|
+
(crossed_at, vendor, account_key, period_key, period, t),
|
|
741
826
|
)
|
|
742
827
|
fired.append((t, crossed_at, spent, target, consumption_pct))
|
|
743
828
|
return fired
|
|
@@ -759,7 +844,11 @@ def _reconcile_codex_budget_on_config_write(validated_budget, *, conn=None, as_o
|
|
|
759
844
|
if not codex:
|
|
760
845
|
return
|
|
761
846
|
thresholds = codex.get("alert_thresholds") or []
|
|
762
|
-
|
|
847
|
+
amount_usd = codex.get("amount_usd")
|
|
848
|
+
# Per-account Codex ladders (#341 Step 4-eval): reconcile each account too.
|
|
849
|
+
accounts = codex.get("accounts") or {}
|
|
850
|
+
if not (codex.get("alerts_enabled") and thresholds
|
|
851
|
+
and (amount_usd is not None or accounts)):
|
|
763
852
|
return
|
|
764
853
|
c = _cctally()
|
|
765
854
|
own_conn = conn is None
|
|
@@ -767,21 +856,30 @@ def _reconcile_codex_budget_on_config_write(validated_budget, *, conn=None, as_o
|
|
|
767
856
|
import argparse
|
|
768
857
|
config = c.load_config()
|
|
769
858
|
tz = c.resolve_display_tz(argparse.Namespace(tz=None), config)
|
|
859
|
+
now_utc = _as_of_or_command(as_of)
|
|
770
860
|
if own_conn:
|
|
771
861
|
conn = open_db()
|
|
772
862
|
try:
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
863
|
+
ladders = []
|
|
864
|
+
if amount_usd is not None:
|
|
865
|
+
ladders.append(("*", amount_usd))
|
|
866
|
+
ladders.extend((k, v) for k, v in accounts.items())
|
|
867
|
+
for acct_key, acct_usd in ladders:
|
|
868
|
+
_reconcile_budget_milestones_on_set(
|
|
869
|
+
conn,
|
|
870
|
+
vendor="codex",
|
|
871
|
+
target=acct_usd,
|
|
872
|
+
thresholds=thresholds,
|
|
873
|
+
now_utc=now_utc,
|
|
874
|
+
period=codex["period"],
|
|
875
|
+
config=config,
|
|
876
|
+
tz=tz,
|
|
877
|
+
as_of=as_of,
|
|
878
|
+
commit=False,
|
|
879
|
+
account_key=acct_key,
|
|
880
|
+
)
|
|
881
|
+
if own_conn:
|
|
882
|
+
conn.commit()
|
|
785
883
|
finally:
|
|
786
884
|
if own_conn:
|
|
787
885
|
conn.close()
|
|
@@ -801,7 +899,13 @@ def _reconcile_budget_on_config_write(validated_budget, *, conn=None, as_of=None
|
|
|
801
899
|
open/commit/close); ``as_of`` (ISO-Z) injects the capture time. Both
|
|
802
900
|
defaults keep the legacy own-connection, wall-clock behavior."""
|
|
803
901
|
thresholds = validated_budget.get("alert_thresholds") or []
|
|
804
|
-
|
|
902
|
+
alerts_enabled = bool(validated_budget.get("alerts_enabled"))
|
|
903
|
+
weekly_usd = validated_budget.get("weekly_usd")
|
|
904
|
+
# Per-account ladders (#341 Step 4-eval): reconcile each real account in
|
|
905
|
+
# `budget.accounts` too, so setting a per-account budget mid-week (already
|
|
906
|
+
# over) records the crossed thresholds as already-alerted WITHOUT dispatch.
|
|
907
|
+
accounts = validated_budget.get("accounts") or {}
|
|
908
|
+
if not thresholds or not alerts_enabled or (weekly_usd is None and not accounts):
|
|
805
909
|
return
|
|
806
910
|
c = _cctally()
|
|
807
911
|
period = validated_budget.get("period", "subscription-week")
|
|
@@ -810,21 +914,30 @@ def _reconcile_budget_on_config_write(validated_budget, *, conn=None, as_of=None
|
|
|
810
914
|
import argparse
|
|
811
915
|
config = c.load_config()
|
|
812
916
|
tz = c.resolve_display_tz(argparse.Namespace(tz=None), config)
|
|
917
|
+
now_utc = _as_of_or_command(as_of)
|
|
813
918
|
if own_conn:
|
|
814
919
|
conn = open_db()
|
|
815
920
|
try:
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
921
|
+
ladders = []
|
|
922
|
+
if weekly_usd is not None:
|
|
923
|
+
ladders.append(("*", weekly_usd))
|
|
924
|
+
ladders.extend((k, v) for k, v in accounts.items())
|
|
925
|
+
for acct_key, acct_usd in ladders:
|
|
926
|
+
_reconcile_budget_milestones_on_set(
|
|
927
|
+
conn,
|
|
928
|
+
vendor="claude",
|
|
929
|
+
target=acct_usd,
|
|
930
|
+
thresholds=thresholds,
|
|
931
|
+
now_utc=now_utc,
|
|
932
|
+
period=period,
|
|
933
|
+
config=config,
|
|
934
|
+
tz=tz,
|
|
935
|
+
as_of=as_of,
|
|
936
|
+
commit=False,
|
|
937
|
+
account_key=acct_key,
|
|
938
|
+
)
|
|
939
|
+
if own_conn:
|
|
940
|
+
conn.commit()
|
|
828
941
|
finally:
|
|
829
942
|
if own_conn:
|
|
830
943
|
conn.close()
|