cctally 1.105.0 → 1.106.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 +32 -0
- package/bin/_cctally_core.py +151 -20
- package/bin/_cctally_dashboard.py +20 -227
- package/bin/_cctally_dashboard_conversation.py +38 -0
- package/bin/_cctally_dashboard_envelope.py +79 -10
- package/bin/_cctally_dashboard_share.py +7 -1
- package/bin/_cctally_db.py +267 -5
- package/bin/_cctally_doctor.py +40 -15
- package/bin/_cctally_five_hour.py +53 -6
- package/bin/_cctally_forecast.py +197 -42
- package/bin/_cctally_journal.py +217 -27
- package/bin/_cctally_milestone_history.py +141 -58
- package/bin/_cctally_milestones.py +72 -0
- package/bin/_cctally_parser.py +10 -6
- package/bin/_cctally_percent_breakdown.py +26 -20
- package/bin/_cctally_project.py +5 -3
- package/bin/_cctally_quota.py +12 -2
- package/bin/_cctally_quota_model.py +12 -4
- package/bin/_cctally_record.py +1254 -300
- package/bin/_cctally_reporting.py +19 -0
- package/bin/_cctally_share.py +21 -3
- package/bin/_cctally_statusline.py +15 -11
- package/bin/_cctally_tui.py +192 -42
- package/bin/_cctally_weekrefs.py +322 -161
- package/bin/_lib_credit.py +9 -2
- package/bin/_lib_credit_identity.py +159 -0
- package/bin/_lib_credit_selection.py +342 -0
- package/bin/_lib_dashboard_sources.py +16 -4
- package/bin/_lib_doctor.py +1 -1
- package/bin/_lib_ingest_frontier.py +46 -0
- package/bin/_lib_quota_model.py +34 -28
- package/bin/_lib_record.py +41 -9
- package/bin/_lib_rederive.py +8 -0
- package/bin/_lib_render.py +72 -4
- package/bin/_lib_snapshot_cache.py +13 -10
- package/bin/_lib_subscription_weeks.py +141 -54
- package/bin/_lib_view_models.py +128 -22
- package/bin/cctally +7 -0
- package/dashboard/static/assets/{ConversationsView-DA4j7Yso.js → ConversationsView-Dst33C0n.js} +13 -13
- package/dashboard/static/assets/{DoctorModal-D4PbVnbE.js → DoctorModal-5rvkyksy.js} +1 -1
- package/dashboard/static/assets/ModalRoot-BJH02VzE.js +1 -0
- package/dashboard/static/assets/{ProjectsDrillPanel-DkPlB9JH.js → ProjectsDrillPanel-CsR1bjJu.js} +1 -1
- package/dashboard/static/assets/{SourceDetailModal-BE7FONsS.js → SourceDetailModal-BnTIwt-R.js} +1 -1
- package/dashboard/static/assets/{UpdateModal-BJuI8glf.js → UpdateModal-D_RfssnW.js} +3 -3
- package/dashboard/static/assets/index-rzXw99Hy.js +13 -0
- package/dashboard/static/assets/index-u3wLfjt2.css +1 -0
- package/dashboard/static/assets/{outlineNavigation-zDVm6Hdd.js → outlineNavigation-C7J9lhGB.js} +1 -1
- package/dashboard/static/assets/useKeymap-B3KCc_c7.js +1 -0
- package/dashboard/static/dashboard.html +3 -3
- package/package.json +3 -1
- package/dashboard/static/assets/ModalRoot-D4oSPxPp.js +0 -1
- package/dashboard/static/assets/index-klO46NcU.css +0 -1
- package/dashboard/static/assets/index-w_bINkJ8.js +0 -13
- package/dashboard/static/assets/useKeymap-CJ-Pi17D.js +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,38 @@ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [1.106.0] - 2026-09-03
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- `cctally record-credit --force` replaces the single credit `--at` names instead of clearing the whole week, and refuses when `--at` names none. Recording a second credit in one week needs no `--force`: rerun with a different `--at`.
|
|
12
|
+
- A week Anthropic credited in place renders as one row on its own boundaries, instead of two rows split at the credit. A credit that also moved the week's declared end is marked, but still renders on the windows the API stated.
|
|
13
|
+
- The dashboard's Current Week header and modal name the week's own boundaries on a credited week, instead of the window that starts at the credit.
|
|
14
|
+
- A credited week's row is marked `+` in `cctally report` and `cctally weekly`, so a low `Used %` beside a week's worth of spend has a visible explanation.
|
|
15
|
+
- The dashboard's Weekly panel and its week detail mark a credited week, and the week detail, the Current Week card and its modal name why `$/1%` is withheld instead of the em-dash or `$0.000` you cannot tell from a week with no usage.
|
|
16
|
+
- The dashboard's `$/1%` tables — the Weekly modal's week list, the `$/1% Trend` panel and its modal — say `No climb` for a credited week whose ratio is withheld, instead of the em-dash you cannot tell from a week with no usage.
|
|
17
|
+
- `$/1%` for a credited week is measured from the credit forward, over the climb since it. With no climb yet the figure is withheld and names why. Uncredited weeks are unchanged.
|
|
18
|
+
- The dashboard's Current Week card, the TUI header and a shared Current Week recap report a credited week's whole spend, matching the week they name and the Weekly card beside them. Only `$/1%` is measured from the credit forward.
|
|
19
|
+
- `cctally milestone` history lists a credited week once instead of once per credit, and its detail separates the week's percent ladders with a row naming each credit and the level it dropped from.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- A conversation open in the viewer updates its own cost as its turns stream, for both Claude and Codex. The live tail advanced the transcript but not the accounting the cost is read from.
|
|
23
|
+
- A Codex conversation's cost no longer reads `$0.00` long after its turns finish. The dashboard now re-scans each provider at least every two minutes, so accounting stays current even when that provider's activity hooks never fire.
|
|
24
|
+
- Upgrading an existing installation to v1.105.0 left every background sync failing and the dashboard stuck on `server sync error`. The two columns that release added now reach an existing database, not only a freshly created one.
|
|
25
|
+
- After Anthropic zeroes your weekly counter, every 7d surface lowers on the next status-line tick. A reading captured in the same hour no longer holds the week at its pre-credit percentage, and no manual command is needed.
|
|
26
|
+
- A goodwill credit to a non-zero level records that level's own percent milestone. Before, the ladder for the new period opened one percent late, and `cctally percent-breakdown` was missing its first row.
|
|
27
|
+
- `cctally percent-breakdown`, the TUI's milestone panel and the milestone writer agree on which credit a percent belongs to, including after a database rebuild and for a credit recorded with `cctally record-credit`.
|
|
28
|
+
- A percent milestone crossed after a credit reports the cost spent since that credit. After a credit recorded with `cctally record-credit` it reported the whole week's spend, which made the first post-credit `$/1%` figure far too high.
|
|
29
|
+
- `cctally record-credit` no longer deletes genuine usage recorded between the moment you assert with `--at` and the moment you run the command.
|
|
30
|
+
- On an install with more than one Claude account, a reset detected for one account no longer records a credit for another, and no longer discards the first account's pending detection.
|
|
31
|
+
- `cctally forecast` measures its rate from the latest credit rather than across it, including one you recorded yourself. `cctally budget` keeps summing the whole week, because the money was spent inside one unchanged window.
|
|
32
|
+
- `cctally doctor`'s post-credit milestone check now sees a week credited with `cctally record-credit`, which it silently skipped before.
|
|
33
|
+
- `cctally record-credit` accepts a second credit inside the same hour as the first. Only a credit at the identical instant is still refused, because that is indistinguishable from running the command twice.
|
|
34
|
+
- An install upgrading from a version that predates the append-only journal now gets the current weekly-credit table shape, instead of reporting the new database version while missing the columns a credit needs.
|
|
35
|
+
- `cctally record-credit --force` no longer leaves a replaced credit's percent milestones behind after a database rebuild, and `cctally record-usage` no longer leaves a removed stale reading's five-hour milestones pointing at it.
|
|
36
|
+
- `cctally` no longer records a second, automatically-shaped credit for a large credit you already recorded with `cctally record-credit`.
|
|
37
|
+
- A stale pre-credit reading replayed by the status line after a credit is removed on the next tick that contradicts it, instead of holding every 7d surface at the old percentage and provoking a second, phantom credit.
|
|
38
|
+
- On an install with more than one Claude account, `cctally record-credit` no longer names another account's credit when refusing, and no longer finishes another account's interrupted credit as though it were yours.
|
|
39
|
+
|
|
8
40
|
## [1.105.0] - 2026-09-02
|
|
9
41
|
|
|
10
42
|
### Added
|
package/bin/_cctally_core.py
CHANGED
|
@@ -410,7 +410,21 @@ _init_paths_from_env()
|
|
|
410
410
|
# install and the table would simply never appear. `quota_alert_arming` is the
|
|
411
411
|
# precedent the family follows — a journaled STATE record whose boundary
|
|
412
412
|
# survives a rebuild, so history cannot re-fire.
|
|
413
|
-
|
|
413
|
+
# 1011 -> 1012 (#703 + #707): the unified Anthropic credit record. Adds the six
|
|
414
|
+
# `week_reset_events` fact columns (`week_start_date`, `observed_at_utc`,
|
|
415
|
+
# `confirming_capture_at_utc`, `observed_post_credit_pct`, `credit_key`,
|
|
416
|
+
# `credit_order`) and relaxes `old_week_end_at` / `new_week_end_at` to NULL. On
|
|
417
|
+
# a store ABOVE `LEGACY_STATS_HEAD` the bump alone delivers all of that, because
|
|
418
|
+
# an epoch mismatch resolves by rebuilding the disposable index from the journal
|
|
419
|
+
# against the new DDL. A PRE-CUTOVER store at `user_version <= 13` takes the
|
|
420
|
+
# in-place cutover path instead and reaches this epoch with its existing table
|
|
421
|
+
# untouched, so that path additionally runs the rename-recreate-copy in
|
|
422
|
+
# `begin_week_reset_events_reshape` / `finish_week_reset_events_reshape`.
|
|
423
|
+
# It is an epoch bump and NOT a stats migration for the two reasons every
|
|
424
|
+
# bump since 1005 carries: the 13-entry registry is frozen, AND an epoch-current
|
|
425
|
+
# open returns before any schema work, so a `@stats_migration` handler would
|
|
426
|
+
# never run on an upgraded install and the columns would simply never appear.
|
|
427
|
+
STATS_INDEX_EPOCH = 1012
|
|
414
428
|
LEGACY_STATS_HEAD = 13
|
|
415
429
|
|
|
416
430
|
#: #496 S1 F1. A NEW branch, for a state that cannot occur before the
|
|
@@ -1783,6 +1797,8 @@ def open_db(*, _target_path=None) -> sqlite3.Connection:
|
|
|
1783
1797
|
# Enforced by tests/test_kernel_extraction_invariants.py
|
|
1784
1798
|
# test_core_accessor_use_is_bounded (lands in I2).
|
|
1785
1799
|
add_column_if_missing = c.add_column_if_missing
|
|
1800
|
+
_begin_week_reset_events_reshape = c.begin_week_reset_events_reshape
|
|
1801
|
+
_finish_week_reset_events_reshape = c.finish_week_reset_events_reshape
|
|
1786
1802
|
_canonical_5h_window_key = c._canonical_5h_window_key
|
|
1787
1803
|
_backfill_week_reset_events = c._backfill_week_reset_events
|
|
1788
1804
|
_backfill_five_hour_blocks = c._backfill_five_hour_blocks
|
|
@@ -2217,23 +2233,111 @@ def open_db(*, _target_path=None) -> sqlite3.Connection:
|
|
|
2217
2233
|
# start — preventing the API's -7d-derived new week from overlapping
|
|
2218
2234
|
# the old week. Inserted by cmd_record_usage on detection; read by
|
|
2219
2235
|
# _apply_reset_events_to_weekrefs and the cost live-recompute path.
|
|
2236
|
+
# #703 + #707: this table is now the SINGLE durable representation of an
|
|
2237
|
+
# Anthropic weekly credit, and it defines an accounting epoch only —
|
|
2238
|
+
# never a display boundary. The six fact columns below are declared HERE
|
|
2239
|
+
# rather than added by `add_column_if_missing`, because this is an epoch
|
|
2240
|
+
# bump: an epoch-current open returns before any schema work, so a
|
|
2241
|
+
# conditional column addition would never run on an upgraded install.
|
|
2242
|
+
#
|
|
2243
|
+
# The epoch bump alone covers only ONE of the two upgrade paths. A store
|
|
2244
|
+
# ABOVE `LEGACY_STATS_HEAD` defers to a rebuild that materializes a fresh
|
|
2245
|
+
# scratch index through this DDL, so it arrives correct. A PRE-CUTOVER
|
|
2246
|
+
# store at `user_version <= 13` reaches this line with the table already
|
|
2247
|
+
# present, where `CREATE TABLE IF NOT EXISTS` is a no-op; the cutover
|
|
2248
|
+
# below then stamps the new epoch in place and every later open
|
|
2249
|
+
# fast-returns before any schema work. `begin_week_reset_events_reshape`
|
|
2250
|
+
# parks such a table aside so this CREATE builds the current shape, and
|
|
2251
|
+
# `finish_week_reset_events_reshape` (right after the `account_key`
|
|
2252
|
+
# guard) copies the rows back and drops the parked table. Without it the
|
|
2253
|
+
# store reports the current epoch while missing every fact column and
|
|
2254
|
+
# keeping both NOT NULL boundaries, which no later open can repair.
|
|
2255
|
+
#
|
|
2256
|
+
# week_start_date the week the credit belongs to, so the
|
|
2257
|
+
# unified table is queryable by week without
|
|
2258
|
+
# going through a boundary column.
|
|
2259
|
+
# observed_at_utc the exact instant the post-credit state was
|
|
2260
|
+
# first observed, in the SAME clock domain as
|
|
2261
|
+
# `weekly_usage_snapshots.captured_at_utc`
|
|
2262
|
+
# (the column it filters). NOT the detection
|
|
2263
|
+
# clock — the ingest path separates the two,
|
|
2264
|
+
# and comparing across the domains back-dates
|
|
2265
|
+
# the epoch (the #703 incident).
|
|
2266
|
+
# confirming_capture_at_utc the capture instant of the observation that
|
|
2267
|
+
# confirmed the credit; closes the upper end
|
|
2268
|
+
# of the automatic stale-replica bracket.
|
|
2269
|
+
# observed_post_credit_pct where the counter landed.
|
|
2270
|
+
# credit_key the row's durable identity, derived from the
|
|
2271
|
+
# SOURCE journal record and never from the
|
|
2272
|
+
# boundaries. Several credits per week are
|
|
2273
|
+
# representable because of it.
|
|
2274
|
+
# credit_order that same source record's own journal
|
|
2275
|
+
# instant, in Unix epoch seconds. NOT the
|
|
2276
|
+
# absolute journal sequence position: that
|
|
2277
|
+
# numbering exists but returns None on many
|
|
2278
|
+
# ordinary ticks, and a durable column cannot
|
|
2279
|
+
# be populated by something sometimes
|
|
2280
|
+
# unavailable. NOT the fold position of the
|
|
2281
|
+
# derived row either — a fold order expresses
|
|
2282
|
+
# dependency, not occurrence. The instant is
|
|
2283
|
+
# a fact OF THE SOURCE RECORD, so a rebuild
|
|
2284
|
+
# reproduces it byte-identically and it orders
|
|
2285
|
+
# by occurrence. Two known losses, both
|
|
2286
|
+
# deliberate: two source instants inside one
|
|
2287
|
+
# second tie and fall through to
|
|
2288
|
+
# `credit_key DESC`, which is content-hash
|
|
2289
|
+
# rather than chronological order; and under a
|
|
2290
|
+
# backward clock step instant order and append
|
|
2291
|
+
# order disagree.
|
|
2292
|
+
#
|
|
2293
|
+
# `old_week_end_at` / `new_week_end_at` are legacy provenance now and are
|
|
2294
|
+
# NULLABLE: a cutover-exported `weekly_credit_floor` op cannot supply them
|
|
2295
|
+
# (`weekly_credit_floors` retains no week-end timestamp), and NULL is the
|
|
2296
|
+
# honest value for a row that never had them. NULL on any new column means
|
|
2297
|
+
# the row predates this change or its source genuinely lacked the fact;
|
|
2298
|
+
# every accounting read falls back through
|
|
2299
|
+
# COALESCE(observed_at_utc, effective_reset_at_utc).
|
|
2300
|
+
_wre_reshaping = _begin_week_reset_events_reshape(conn)
|
|
2220
2301
|
conn.execute(
|
|
2221
2302
|
"""
|
|
2222
2303
|
CREATE TABLE IF NOT EXISTS week_reset_events (
|
|
2223
2304
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2224
2305
|
detected_at_utc TEXT NOT NULL,
|
|
2225
|
-
old_week_end_at TEXT
|
|
2226
|
-
new_week_end_at TEXT
|
|
2306
|
+
old_week_end_at TEXT,
|
|
2307
|
+
new_week_end_at TEXT,
|
|
2227
2308
|
effective_reset_at_utc TEXT NOT NULL,
|
|
2228
2309
|
observed_pre_credit_pct REAL,
|
|
2229
2310
|
account_key TEXT NOT NULL DEFAULT 'unattributed',
|
|
2230
|
-
|
|
2311
|
+
week_start_date TEXT,
|
|
2312
|
+
observed_at_utc TEXT,
|
|
2313
|
+
confirming_capture_at_utc TEXT,
|
|
2314
|
+
observed_post_credit_pct REAL,
|
|
2315
|
+
credit_key TEXT,
|
|
2316
|
+
credit_order INTEGER,
|
|
2317
|
+
-- Identity is the SOURCE record, not the boundaries. Keyed on
|
|
2318
|
+
-- the boundary pair, a second credit in one week had nowhere to
|
|
2319
|
+
-- live: two credits inside one hour collide outright, because
|
|
2320
|
+
-- `effective_reset_at_utc` is hour-floored. `credit_key` is NULL
|
|
2321
|
+
-- for a row that predates this change, and SQLite treats NULLs
|
|
2322
|
+
-- as distinct under UNIQUE, so legacy rows neither collide with
|
|
2323
|
+
-- each other nor with a keyed one. Replay idempotence does not
|
|
2324
|
+
-- rest on this constraint either way: every fold inserts with
|
|
2325
|
+
-- `journal_id` set, and that column carries its own partial
|
|
2326
|
+
-- UNIQUE index.
|
|
2327
|
+
UNIQUE(account_key, credit_key)
|
|
2231
2328
|
)
|
|
2232
2329
|
"""
|
|
2233
2330
|
)
|
|
2234
2331
|
add_column_if_missing(
|
|
2235
2332
|
conn, "week_reset_events", "account_key",
|
|
2236
2333
|
"TEXT NOT NULL DEFAULT 'unattributed'")
|
|
2334
|
+
if _wre_reshaping:
|
|
2335
|
+
# `journal_id` is added here rather than by the loop further down so
|
|
2336
|
+
# the copy can carry it: a parked row's logical identity must reach
|
|
2337
|
+
# the new table, or harvest would re-emit every already-journaled
|
|
2338
|
+
# credit as a fresh `wr:` event. The loop below then no-ops on it.
|
|
2339
|
+
add_column_if_missing(conn, "week_reset_events", "journal_id", "TEXT")
|
|
2340
|
+
_finish_week_reset_events_reshape(conn)
|
|
2237
2341
|
_backfill_week_reset_events(conn)
|
|
2238
2342
|
|
|
2239
2343
|
# ── five_hour_reset_events (Anthropic-issued in-place 5h credits) ──
|
|
@@ -3064,9 +3168,8 @@ def _reset_aware_floor(
|
|
|
3064
3168
|
*,
|
|
3065
3169
|
account_key: str | None,
|
|
3066
3170
|
) -> str | None:
|
|
3067
|
-
"""Return the latest in-week
|
|
3068
|
-
|
|
3069
|
-
row for this week.
|
|
3171
|
+
"""Return the latest in-week accounting floor (an ISO timestamp), or None
|
|
3172
|
+
when this week has no credit.
|
|
3070
3173
|
|
|
3071
3174
|
``account_key`` (#341, review finding 11): MANDATORY account context — no
|
|
3072
3175
|
silent global fallback. A real key scopes both legs to that account's
|
|
@@ -3075,40 +3178,68 @@ def _reset_aware_floor(
|
|
|
3075
3178
|
analytics floor path, byte-identical to today on a single-account install.
|
|
3076
3179
|
|
|
3077
3180
|
This is the single chokepoint the four MAX-clamp sites consult to floor the
|
|
3078
|
-
current 7d % to the most-recent in-place credit / reset
|
|
3181
|
+
current 7d % to the most-recent in-place credit / reset moment
|
|
3079
3182
|
(record-credit M2, issue #209, spec §4a):
|
|
3080
3183
|
- statusline `_hwm_clamp` 7d (bin/_cctally_statusline.py)
|
|
3081
3184
|
- the record-usage write-site monotonic clamp (bin/_cctally_record.py)
|
|
3082
3185
|
- `_resolve_reset_aware_hwm` (the --from default helper)
|
|
3083
3186
|
- `project`'s `_load_week_snapshots` per-week MAX (bin/_cctally_project.py)
|
|
3084
3187
|
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3188
|
+
#703 + #707 §5.3: the floor is ``COALESCE(observed_at_utc,
|
|
3189
|
+
effective_reset_at_utc)``, NOT the effective instant. The effective instant
|
|
3190
|
+
is hour-floored and display-only, and that rounding is what back-dated the
|
|
3191
|
+
2026-09-01 incident's epoch: the credit was first observed at 17:59:41, the
|
|
3192
|
+
row recorded 17:00:00, and a genuine 13.0 snapshot captured at 17:33:38 fell
|
|
3193
|
+
inside the accounting window, held the maximum, and made every later genuine
|
|
3194
|
+
reading look like a regression. NULL on the exact instant means the row
|
|
3195
|
+
predates this change, and such a row keeps the only instant it has.
|
|
3196
|
+
|
|
3197
|
+
A row counts iff it belongs to this WEEK. That is `week_start_date` for a
|
|
3198
|
+
row that records it, and the `[week_start_at, week_end_at)` range only for a
|
|
3199
|
+
row that predates the column. The range alone is a narrowing this helper
|
|
3200
|
+
must not inherit from the reset-event leg it replaces: the display layer
|
|
3201
|
+
overrides `week_start_at` to a credit's own effective instant, so an EARLIER
|
|
3202
|
+
credit in the same week falls outside the range the caller passes. The
|
|
3203
|
+
`weekly_credit_floors` leg this collapses had no time bound at all and found
|
|
3204
|
+
it; keying on the week's identity keeps that.
|
|
3205
|
+
|
|
3206
|
+
The `weekly_credit_floors` leg is retained for exactly one state and is
|
|
3207
|
+
otherwise dead. The in-place cutover EXPORTS that table to a bootstrap
|
|
3208
|
+
segment and stamps `journal_id` on its rows, but it does not re-fold them,
|
|
3209
|
+
so between a legacy store's cutover and its first rebuild the credit exists
|
|
3210
|
+
only there. Nothing writes the table any more — the `weekly_credit_floor` op
|
|
3211
|
+
folds into `week_reset_events` — and a rebuilt index does not carry the rows
|
|
3212
|
+
at all, so on every other store this leg matches nothing. It cannot lower a
|
|
3213
|
+
floor either: for one credit present in both tables the unified row's exact
|
|
3214
|
+
instant is at or after the floored one, and the latest wins.
|
|
3089
3215
|
|
|
3090
3216
|
The latest floor wins via `ORDER BY unixepoch(floor_at) DESC LIMIT 1` —
|
|
3091
|
-
`unixepoch()`, NOT a textual `MAX(...)`: the
|
|
3217
|
+
`unixepoch()`, NOT a textual `MAX(...)`: the legs carry mixed offset
|
|
3092
3218
|
spellings (`Z` / `+00:00`), and a lexical MAX would silently mis-order them
|
|
3093
3219
|
on a non-UTC host (the same gotcha as the statusline clamp / 5h-block
|
|
3094
3220
|
cross-reset flag; see the comment at bin/_cctally_statusline.py)."""
|
|
3095
3221
|
acct_pred = "" if account_key is None else " AND account_key = ?"
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3222
|
+
acct_param: tuple = () if account_key is None else (account_key,)
|
|
3223
|
+
reset_params: tuple = (
|
|
3224
|
+
(week_start_date, week_start_at, week_end_at) + acct_param)
|
|
3225
|
+
floor_params: tuple = (week_start_date,) + acct_param
|
|
3100
3226
|
row = conn.execute(
|
|
3101
3227
|
f"""
|
|
3102
3228
|
SELECT floor_at FROM (
|
|
3103
|
-
SELECT effective_reset_at_utc AS floor_at
|
|
3229
|
+
SELECT COALESCE(observed_at_utc, effective_reset_at_utc) AS floor_at
|
|
3104
3230
|
FROM week_reset_events
|
|
3105
|
-
WHERE
|
|
3106
|
-
|
|
3231
|
+
WHERE (
|
|
3232
|
+
week_start_date = ?
|
|
3233
|
+
OR (week_start_date IS NULL
|
|
3234
|
+
AND unixepoch(effective_reset_at_utc) >= unixepoch(?)
|
|
3235
|
+
AND unixepoch(effective_reset_at_utc) < unixepoch(?))
|
|
3236
|
+
){acct_pred}
|
|
3107
3237
|
UNION ALL
|
|
3108
3238
|
SELECT effective_at_utc AS floor_at
|
|
3109
3239
|
FROM weekly_credit_floors
|
|
3110
3240
|
WHERE week_start_date = ?{acct_pred}
|
|
3111
3241
|
)
|
|
3242
|
+
WHERE floor_at IS NOT NULL
|
|
3112
3243
|
ORDER BY unixepoch(floor_at) DESC
|
|
3113
3244
|
LIMIT 1
|
|
3114
3245
|
""",
|
|
@@ -510,15 +510,12 @@ from _cctally_cache import (
|
|
|
510
510
|
from _lib_snapshot_cache import (
|
|
511
511
|
build_cached_group_a,
|
|
512
512
|
bump_generation,
|
|
513
|
-
cached_bugk_segment,
|
|
514
513
|
reset_bugk_segment_state,
|
|
515
514
|
reset_cache_report_state,
|
|
516
515
|
reset_group_a_state,
|
|
517
516
|
reset_projects_env_state,
|
|
518
517
|
reset_session_cache_state,
|
|
519
518
|
reset_weekref_cost_state,
|
|
520
|
-
BugKSegment,
|
|
521
|
-
_bugk_key,
|
|
522
519
|
_max_id as _snapshot_max_id,
|
|
523
520
|
_reset_sig as _snapshot_reset_sig,
|
|
524
521
|
)
|
|
@@ -3623,19 +3620,20 @@ def _dashboard_build_weekly_periods(conn: "sqlite3.Connection",
|
|
|
3623
3620
|
now_utc: "dt.datetime",
|
|
3624
3621
|
*, n: int = 12,
|
|
3625
3622
|
skip_sync: bool = False,
|
|
3626
|
-
use_group_a_cache: bool = False
|
|
3627
|
-
use_bugk_segment_cache: bool = False) -> "list[WeeklyPeriodRow]":
|
|
3623
|
+
use_group_a_cache: bool = False) -> "list[WeeklyPeriodRow]":
|
|
3628
3624
|
"""Latest n subscription weeks as WeeklyPeriodRow, newest-first.
|
|
3629
3625
|
|
|
3630
|
-
Thin builder-using prelude
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3626
|
+
Thin builder-using prelude on top of ``view.rows``. ``build_weekly_view``
|
|
3627
|
+
(in ``bin/_lib_view_models.py``) owns the bucket+overlay walk — this
|
|
3628
|
+
function calls it, swaps two presentation fields (``label`` ←
|
|
3629
|
+
``display_start_date`` for post-early-reset weeks; ``is_current`` ←
|
|
3630
|
+
SubWeek-containing-now_utc with snapshot fallback so the "Now" pill tracks
|
|
3631
|
+
wall time), then computes ``delta_cost_pct`` newest-first.
|
|
3632
|
+
|
|
3633
|
+
It used to layer a synthesized pre-credit row over the natural rows, because
|
|
3634
|
+
the credited week's own row covered only the post-credit interval. #703 +
|
|
3635
|
+
#707 §6.1 removed both halves of that: an Anthropic reset never changes the
|
|
3636
|
+
week's boundaries, so the natural row already covers the whole week.
|
|
3639
3637
|
|
|
3640
3638
|
Note: weekly bucketing intentionally does NOT take ``display_tz`` —
|
|
3641
3639
|
SubWeek bucket keys come from server-anchored stored anchors and the
|
|
@@ -3749,219 +3747,14 @@ def _dashboard_build_weekly_periods(conn: "sqlite3.Connection",
|
|
|
3749
3747
|
r.delta_cost_pct = None
|
|
3750
3748
|
rows_oldest_first.append(r)
|
|
3751
3749
|
|
|
3752
|
-
#
|
|
3753
|
-
#
|
|
3754
|
-
#
|
|
3755
|
-
# the week's
|
|
3756
|
-
#
|
|
3757
|
-
#
|
|
3758
|
-
#
|
|
3759
|
-
#
|
|
3760
|
-
# bucket for that SubWeek already covers ONLY the post-credit
|
|
3761
|
-
# interval. We rebuild the pre-credit bucket here by fetching the
|
|
3762
|
-
# ``[original_start, effective)`` window directly (#268: no longer a
|
|
3763
|
-
# wide ``entries`` list on the cached path) and re-aggregating cost /
|
|
3764
|
-
# tokens / per-model.
|
|
3765
|
-
#
|
|
3766
|
-
# The pre-credit row's ``used_pct`` comes from the
|
|
3767
|
-
# weekly_usage_snapshots row captured at-or-before the credit
|
|
3768
|
-
# moment (the pre-credit peak the user reached); fall back to None
|
|
3769
|
-
# if no snapshot was recorded before the credit fired.
|
|
3770
|
-
in_place_credits = conn.execute(
|
|
3771
|
-
"SELECT new_week_end_at, effective_reset_at_utc "
|
|
3772
|
-
"FROM week_reset_events "
|
|
3773
|
-
"WHERE old_week_end_at = effective_reset_at_utc"
|
|
3774
|
-
).fetchall()
|
|
3775
|
-
if in_place_credits:
|
|
3776
|
-
_lib_pricing = sys.modules.get("_lib_pricing")
|
|
3777
|
-
if _lib_pricing is None:
|
|
3778
|
-
import importlib.util as _ilu, pathlib as _pl
|
|
3779
|
-
_p = _pl.Path(__file__).resolve().parent / "_lib_pricing.py"
|
|
3780
|
-
_spec = _ilu.spec_from_file_location("_lib_pricing", _p)
|
|
3781
|
-
_lib_pricing = _ilu.module_from_spec(_spec)
|
|
3782
|
-
sys.modules["_lib_pricing"] = _lib_pricing
|
|
3783
|
-
_spec.loader.exec_module(_lib_pricing)
|
|
3784
|
-
_calc = _lib_pricing._calculate_entry_cost
|
|
3785
|
-
|
|
3786
|
-
insertions: list[tuple[int, WeeklyPeriodRow]] = []
|
|
3787
|
-
for ev in in_place_credits:
|
|
3788
|
-
try:
|
|
3789
|
-
eff_dt = parse_iso_datetime(
|
|
3790
|
-
ev["effective_reset_at_utc"], "credit.eff"
|
|
3791
|
-
)
|
|
3792
|
-
new_end_dt = parse_iso_datetime(
|
|
3793
|
-
ev["new_week_end_at"], "credit.new_end"
|
|
3794
|
-
)
|
|
3795
|
-
except ValueError:
|
|
3796
|
-
continue
|
|
3797
|
-
# Find the SubWeek whose end_ts equals new_week_end_at (the
|
|
3798
|
-
# post-credit segment); its start_ts has already been
|
|
3799
|
-
# shifted to ``effective`` by _apply_reset_events_to_subweeks.
|
|
3800
|
-
post_sw = None
|
|
3801
|
-
for w in weeks:
|
|
3802
|
-
try:
|
|
3803
|
-
w_end = parse_iso_datetime(w.end_ts, "sw.end")
|
|
3804
|
-
except ValueError:
|
|
3805
|
-
continue
|
|
3806
|
-
if w_end == new_end_dt:
|
|
3807
|
-
post_sw = w
|
|
3808
|
-
break
|
|
3809
|
-
if post_sw is None:
|
|
3810
|
-
continue
|
|
3811
|
-
|
|
3812
|
-
# Original start instant: take the EARLIEST recorded
|
|
3813
|
-
# week_start_at for this week_start_date. The post-credit
|
|
3814
|
-
# SubWeek's start_ts is the shifted value (= effective); the
|
|
3815
|
-
# MIN over weekly_usage_snapshots gives us the original
|
|
3816
|
-
# API-derived start before the override fired.
|
|
3817
|
-
orig_row = conn.execute(
|
|
3818
|
-
"SELECT MIN(week_start_at) AS ws "
|
|
3819
|
-
"FROM weekly_usage_snapshots "
|
|
3820
|
-
"WHERE week_start_date = ? AND week_start_at IS NOT NULL",
|
|
3821
|
-
(post_sw.start_date.isoformat(),),
|
|
3822
|
-
).fetchone()
|
|
3823
|
-
if orig_row is None or orig_row["ws"] is None:
|
|
3824
|
-
continue
|
|
3825
|
-
try:
|
|
3826
|
-
original_start_iso = str(orig_row["ws"])
|
|
3827
|
-
original_start_dt = parse_iso_datetime(
|
|
3828
|
-
original_start_iso, "credit.original_start"
|
|
3829
|
-
)
|
|
3830
|
-
except ValueError:
|
|
3831
|
-
continue
|
|
3832
|
-
if original_start_dt >= eff_dt:
|
|
3833
|
-
# No pre-credit interval to aggregate.
|
|
3834
|
-
continue
|
|
3835
|
-
|
|
3836
|
-
# Aggregate entries in [original_start, effective). Fetch this
|
|
3837
|
-
# window directly (skip_sync=True — the rebuild already ingested,
|
|
3838
|
-
# or the fallback's wide fetch above did) rather than filtering a
|
|
3839
|
-
# wide `entries` list, so Bug-K works identically on the cached
|
|
3840
|
-
# path (where no wide entry list exists) and the fallback path.
|
|
3841
|
-
#
|
|
3842
|
-
# #271 §18: the [original_start, effective) window is a CLOSED past
|
|
3843
|
-
# interval (effective is a historical credit moment), so this folded
|
|
3844
|
-
# aggregate is IMMUTABLE — cache it byte-identically (the same
|
|
3845
|
-
# "re-aggregate immutable history every tick" pattern the #269
|
|
3846
|
-
# weekref cost cache fixed). `_compute_pre_segment` is the exact
|
|
3847
|
-
# from-scratch fetch+fold closure; on the sync thread
|
|
3848
|
-
# (`use_bugk_segment_cache=True`) it routes through
|
|
3849
|
-
# `cached_bugk_segment` (get-or-compute over the canonical window
|
|
3850
|
-
# key), on EVERY other caller (CLI / share / tests / reconcile-
|
|
3851
|
-
# failed) it computes directly — byte-unchanged. The `used_pct`
|
|
3852
|
-
# snapshot query + WeeklyPeriodRow build BELOW always rerun fresh
|
|
3853
|
-
# from the segment (do NOT cache the row — Codex-BK-5).
|
|
3854
|
-
def _compute_pre_segment(_orig=original_start_dt, _eff=eff_dt):
|
|
3855
|
-
pi = po = pcc = pcr = 0
|
|
3856
|
-
pcost = 0.0
|
|
3857
|
-
pmodels: dict[str, float] = {}
|
|
3858
|
-
pcount = 0
|
|
3859
|
-
for e in get_entries(_orig, _eff, skip_sync=True):
|
|
3860
|
-
if _orig <= e.timestamp < _eff:
|
|
3861
|
-
usage = e.usage
|
|
3862
|
-
pi += usage.get("input_tokens", 0)
|
|
3863
|
-
po += usage.get("output_tokens", 0)
|
|
3864
|
-
pcc += usage.get("cache_creation_input_tokens", 0)
|
|
3865
|
-
pcr += usage.get("cache_read_input_tokens", 0)
|
|
3866
|
-
c = _calc(
|
|
3867
|
-
e.model, usage, mode="auto", cost_usd=e.cost_usd,
|
|
3868
|
-
)
|
|
3869
|
-
pcost += c
|
|
3870
|
-
pmodels[e.model] = pmodels.get(e.model, 0.0) + c
|
|
3871
|
-
pcount += 1
|
|
3872
|
-
# Codex-BK-4: freeze `models` as a tuple of (model, cost) in
|
|
3873
|
-
# first-seen (dict-insertion) order so the row's stable cost-desc
|
|
3874
|
-
# sort tie-order can't be mutated after caching.
|
|
3875
|
-
return BugKSegment(
|
|
3876
|
-
input=pi, output=po, cache_create=pcc, cache_read=pcr,
|
|
3877
|
-
cost=pcost, models=tuple(pmodels.items()), entry_count=pcount,
|
|
3878
|
-
)
|
|
3879
|
-
|
|
3880
|
-
if use_bugk_segment_cache:
|
|
3881
|
-
seg = cached_bugk_segment(
|
|
3882
|
-
key=_bugk_key(original_start_dt, eff_dt),
|
|
3883
|
-
compute=_compute_pre_segment,
|
|
3884
|
-
)
|
|
3885
|
-
else:
|
|
3886
|
-
seg = _compute_pre_segment()
|
|
3887
|
-
pre_input = seg.input
|
|
3888
|
-
pre_output = seg.output
|
|
3889
|
-
pre_cc = seg.cache_create
|
|
3890
|
-
pre_cr = seg.cache_read
|
|
3891
|
-
pre_cost = seg.cost
|
|
3892
|
-
pre_models = seg.models # ((model, cost), ...) in first-seen order
|
|
3893
|
-
pre_entry_count = seg.entry_count
|
|
3894
|
-
if pre_entry_count == 0 and pre_cost <= 0:
|
|
3895
|
-
# No measurable pre-credit activity — skip insertion.
|
|
3896
|
-
continue
|
|
3897
|
-
|
|
3898
|
-
# Pre-credit used_pct: latest snapshot at-or-before the
|
|
3899
|
-
# credit moment for this week_start_date.
|
|
3900
|
-
pre_usage = conn.execute(
|
|
3901
|
-
"SELECT weekly_percent FROM weekly_usage_snapshots "
|
|
3902
|
-
"WHERE week_start_date = ? "
|
|
3903
|
-
" AND unixepoch(captured_at_utc) <= unixepoch(?) "
|
|
3904
|
-
"ORDER BY captured_at_utc DESC, id DESC LIMIT 1",
|
|
3905
|
-
(post_sw.start_date.isoformat(), ev["effective_reset_at_utc"]),
|
|
3906
|
-
).fetchone()
|
|
3907
|
-
pre_used_pct: float | None = None
|
|
3908
|
-
if pre_usage is not None and pre_usage["weekly_percent"] is not None:
|
|
3909
|
-
pre_used_pct = float(pre_usage["weekly_percent"])
|
|
3910
|
-
pre_dpp = (
|
|
3911
|
-
pre_cost / pre_used_pct
|
|
3912
|
-
if pre_used_pct and pre_used_pct > 0 else None
|
|
3913
|
-
)
|
|
3914
|
-
|
|
3915
|
-
pre_total = pre_input + pre_output + pre_cc + pre_cr
|
|
3916
|
-
# `pre_models` is a first-seen-order tuple of (model, cost); the
|
|
3917
|
-
# stable cost-desc sort preserves that tie-order byte-for-byte,
|
|
3918
|
-
# identical to the pre-#271 `sorted(dict.items(), ...)`.
|
|
3919
|
-
pre_model_breakdowns = [
|
|
3920
|
-
{"modelName": m, "cost": c}
|
|
3921
|
-
for m, c in sorted(pre_models, key=lambda kv: -kv[1])
|
|
3922
|
-
]
|
|
3923
|
-
pre_label = original_start_dt.strftime("%m-%d")
|
|
3924
|
-
pre_row = WeeklyPeriodRow(
|
|
3925
|
-
label=pre_label,
|
|
3926
|
-
cost_usd=pre_cost,
|
|
3927
|
-
total_tokens=pre_total,
|
|
3928
|
-
input_tokens=pre_input,
|
|
3929
|
-
output_tokens=pre_output,
|
|
3930
|
-
cache_creation_tokens=pre_cc,
|
|
3931
|
-
cache_read_tokens=pre_cr,
|
|
3932
|
-
used_pct=pre_used_pct,
|
|
3933
|
-
dollar_per_pct=pre_dpp,
|
|
3934
|
-
delta_cost_pct=None,
|
|
3935
|
-
# Pre-credit segment is historical even though it
|
|
3936
|
-
# shares the bucket date with the live week.
|
|
3937
|
-
is_current=False,
|
|
3938
|
-
models=_model_breakdowns_to_models(
|
|
3939
|
-
pre_model_breakdowns, pre_cost
|
|
3940
|
-
),
|
|
3941
|
-
week_start_at=original_start_iso,
|
|
3942
|
-
week_end_at=ev["effective_reset_at_utc"],
|
|
3943
|
-
)
|
|
3944
|
-
|
|
3945
|
-
# Find post-credit row's index and insert pre-credit BEFORE
|
|
3946
|
-
# it (chronological order: pre then post in oldest-first).
|
|
3947
|
-
post_idx = None
|
|
3948
|
-
for i, r in enumerate(rows_oldest_first):
|
|
3949
|
-
if r.week_start_at == post_sw.start_ts and r.week_end_at == post_sw.end_ts:
|
|
3950
|
-
post_idx = i
|
|
3951
|
-
break
|
|
3952
|
-
if post_idx is None:
|
|
3953
|
-
# The post-credit row may have been dropped by
|
|
3954
|
-
# _aggregate_weekly (no entries in the post-credit
|
|
3955
|
-
# interval) — append at the most-recent slot so the
|
|
3956
|
-
# pre-credit segment still surfaces.
|
|
3957
|
-
insertions.append((len(rows_oldest_first), pre_row))
|
|
3958
|
-
else:
|
|
3959
|
-
insertions.append((post_idx, pre_row))
|
|
3960
|
-
|
|
3961
|
-
# Apply insertions in REVERSE index order so prior insertions
|
|
3962
|
-
# don't shift the indices of later ones.
|
|
3963
|
-
for idx, pre_row in sorted(insertions, key=lambda t: -t[0]):
|
|
3964
|
-
rows_oldest_first.insert(idx, pre_row)
|
|
3750
|
+
# #703 + #707 §6.1: the pre-credit segment row is gone. It existed because
|
|
3751
|
+
# `_apply_reset_events_to_subweeks` shifted the credited week's `start_ts`
|
|
3752
|
+
# to the credit moment, so the panel's bucket covered only the post-credit
|
|
3753
|
+
# interval and the bulk of the week's spend was invisible. That shift is
|
|
3754
|
+
# gone — an Anthropic reset never changes the week's boundaries — so the
|
|
3755
|
+
# single row already covers the whole week and a second one would double it.
|
|
3756
|
+
# The credit still shows: the row carries the marker §6.4 describes, and the
|
|
3757
|
+
# accounting segments remain in the milestone and drill-down data.
|
|
3965
3758
|
|
|
3966
3759
|
# Reverse so caller gets newest-first; compute delta_cost_pct vs the
|
|
3967
3760
|
# immediately older row in that orientation.
|
|
@@ -53,6 +53,7 @@ import time
|
|
|
53
53
|
from _cctally_cache import (
|
|
54
54
|
open_cache_db,
|
|
55
55
|
scope_conversations_db_to_account,
|
|
56
|
+
sync_cache,
|
|
56
57
|
sync_codex_cache,
|
|
57
58
|
sync_claude_conversations,
|
|
58
59
|
sync_codex_conversations,
|
|
@@ -64,6 +65,38 @@ from _lib_dashboard_json import (
|
|
|
64
65
|
)
|
|
65
66
|
from _lib_retained_size import retained_size_bytes
|
|
66
67
|
|
|
68
|
+
|
|
69
|
+
def _advance_live_tail_accounting(handler, changed, sync, label):
|
|
70
|
+
"""Advance the accounting cache for paths a live tail just saw grow.
|
|
71
|
+
|
|
72
|
+
A conversation's rendered cost comes from the accounting store -- Claude
|
|
73
|
+
turns from ``session_entries``, Codex from ``codex_session_entries`` -- and
|
|
74
|
+
the periodic dashboard tick owns it. A tail that advanced only the
|
|
75
|
+
transcript therefore streamed new turns whose cost stayed frozen at the last
|
|
76
|
+
ordinary tick's value, which reads as ``$0.00`` for a whole session whenever
|
|
77
|
+
that tick was certified caught up. The ingest is targeted at exactly the
|
|
78
|
+
paths the tail resolved as changed, so this costs one file, never a walk.
|
|
79
|
+
|
|
80
|
+
Cost is an enhancement to the stream and never a precondition for it: a
|
|
81
|
+
contended or failed accounting write is logged and the transcript still
|
|
82
|
+
advances, because stalling the turns a reader is watching is strictly worse
|
|
83
|
+
than showing them beside a stale figure. Callers that genuinely depend on
|
|
84
|
+
the accounting result -- the account-scoped Codex tail, which needs the
|
|
85
|
+
physical account-range decision committed before transcript ingest -- must
|
|
86
|
+
do their own ordered sync instead of calling this.
|
|
87
|
+
"""
|
|
88
|
+
try:
|
|
89
|
+
accounting = open_cache_db()
|
|
90
|
+
try:
|
|
91
|
+
sync(accounting, only_paths=set(changed))
|
|
92
|
+
finally:
|
|
93
|
+
accounting.close()
|
|
94
|
+
except Exception as exc: # noqa: BLE001
|
|
95
|
+
handler.log_error(
|
|
96
|
+
"api/conversation/events %s accounting sync failed: %r",
|
|
97
|
+
label, exc,
|
|
98
|
+
)
|
|
99
|
+
|
|
67
100
|
# Live-tail watch-loop tuning — used ONLY by _handle_get_conversation_events_impl
|
|
68
101
|
# below, so moved here with the events handler (spec §4.1 / §6).
|
|
69
102
|
_LIVE_TAIL_POLL_INTERVAL = 1.0 # seconds between stat polls of the open file(s)
|
|
@@ -1104,6 +1137,7 @@ def _bare_conversation_events(
|
|
|
1104
1137
|
return cq.session_source_paths(conn, session_id) if conn else []
|
|
1105
1138
|
|
|
1106
1139
|
def _ingest(changed):
|
|
1140
|
+
_advance_live_tail_accounting(handler, changed, sync_cache, "claude")
|
|
1107
1141
|
if account_key is None:
|
|
1108
1142
|
return sync_claude_conversations(conn, only_paths=set(changed))
|
|
1109
1143
|
before = conn.execute(
|
|
@@ -1274,6 +1308,8 @@ def _qualified_conversation_events(
|
|
|
1274
1308
|
|
|
1275
1309
|
def _ingest(changed):
|
|
1276
1310
|
if account_key is None:
|
|
1311
|
+
_advance_live_tail_accounting(
|
|
1312
|
+
handler, changed, sync_codex_cache, "codex")
|
|
1277
1313
|
return sync_codex_conversations(conn, only_paths=set(changed))
|
|
1278
1314
|
before = conn.execute(
|
|
1279
1315
|
"SELECT COUNT(*),MAX(id) FROM codex_conversation_messages "
|
|
@@ -1317,6 +1353,8 @@ def _qualified_conversation_events(
|
|
|
1317
1353
|
return cq.session_source_paths(conn, native)
|
|
1318
1354
|
|
|
1319
1355
|
def _ingest(changed):
|
|
1356
|
+
_advance_live_tail_accounting(
|
|
1357
|
+
handler, changed, sync_cache, "claude")
|
|
1320
1358
|
if account_key is None:
|
|
1321
1359
|
return sync_claude_conversations(conn, only_paths=set(changed))
|
|
1322
1360
|
before = conn.execute(
|