cctally 1.82.1 → 1.83.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 +58 -0
- package/README.md +12 -5
- package/bin/_cctally_alerts.py +8 -1
- package/bin/_cctally_cache.py +912 -149
- package/bin/_cctally_config.py +43 -4
- package/bin/_cctally_core.py +933 -759
- package/bin/_cctally_dashboard.py +157 -47
- package/bin/_cctally_dashboard_cache_report.py +13 -6
- package/bin/_cctally_dashboard_conversation.py +1 -0
- package/bin/_cctally_dashboard_envelope.py +116 -8
- package/bin/_cctally_dashboard_share.py +50 -19
- package/bin/_cctally_dashboard_sources.py +223 -48
- package/bin/_cctally_db.py +605 -128
- package/bin/_cctally_doctor.py +413 -28
- package/bin/_cctally_five_hour.py +12 -5
- package/bin/_cctally_journal.py +2050 -156
- package/bin/_cctally_journal_repair.py +519 -0
- package/bin/_cctally_milestone_history.py +142 -56
- package/bin/_cctally_milestones.py +179 -111
- package/bin/_cctally_parser.py +42 -0
- package/bin/_cctally_project.py +24 -18
- package/bin/_cctally_quota.py +139 -25
- package/bin/_cctally_record.py +279 -108
- package/bin/_cctally_rederive.py +1052 -0
- package/bin/_cctally_reporting.py +58 -53
- package/bin/_cctally_setup.py +1 -0
- package/bin/_cctally_source_analytics.py +4 -1
- package/bin/_cctally_statusline.py +11 -11
- package/bin/_cctally_store.py +1039 -31
- package/bin/_cctally_sync_week.py +17 -8
- package/bin/_cctally_tui.py +350 -44
- package/bin/_cctally_update.py +133 -8
- package/bin/_cctally_weekrefs.py +14 -0
- package/bin/_lib_aggregators.py +10 -6
- package/bin/_lib_cache_report.py +101 -9
- package/bin/_lib_codex_pools.py +82 -0
- package/bin/_lib_conversation_query.py +81 -33
- package/bin/_lib_dashboard_sources.py +75 -0
- package/bin/_lib_diff_kernel.py +28 -15
- package/bin/_lib_doctor.py +342 -4
- package/bin/_lib_journal.py +924 -2
- package/bin/_lib_jsonl.py +43 -14
- package/bin/_lib_pricing.py +140 -21
- package/bin/_lib_rederive.py +395 -0
- package/bin/_lib_share.py +58 -2
- package/bin/cctally +56 -8
- package/dashboard/static/assets/{index-BKM43pxK.js → index-3bgCMVHb.js} +52 -52
- package/dashboard/static/assets/index-D27EIHEI.css +1 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +5 -1
- package/dashboard/static/assets/index-Dk1nplOz.css +0 -1
package/bin/_cctally_record.py
CHANGED
|
@@ -203,7 +203,7 @@ from _cctally_core import (
|
|
|
203
203
|
)
|
|
204
204
|
import _lib_accounts # pure stdlib kernel; UNATTRIBUTED sentinel default (#341)
|
|
205
205
|
from _lib_five_hour import _canonical_5h_window_key, five_hour_milestone_range
|
|
206
|
-
from _lib_pricing import _calculate_entry_cost
|
|
206
|
+
from _lib_pricing import _calculate_entry_cost, claude_usage_dict
|
|
207
207
|
from _lib_codex_hooks import (
|
|
208
208
|
CODEX_HOOK_THROTTLE_SECONDS,
|
|
209
209
|
acquire_due_lifecycle_locks,
|
|
@@ -609,6 +609,7 @@ def maybe_record_milestone(
|
|
|
609
609
|
alert_sink: "list | None" = None,
|
|
610
610
|
journal: "tuple | None" = None,
|
|
611
611
|
account_key: str = _lib_accounts.UNATTRIBUTED,
|
|
612
|
+
retained_selection=None,
|
|
612
613
|
) -> None:
|
|
613
614
|
"""Check if a new integer percent threshold was crossed, and if so,
|
|
614
615
|
fetch cost and record the milestone. Errors are logged, not raised.
|
|
@@ -634,6 +635,11 @@ def maybe_record_milestone(
|
|
|
634
635
|
rebuild. ``None`` keeps today's compute-and-drop behavior on the passed-conn
|
|
635
636
|
path.
|
|
636
637
|
|
|
638
|
+
``retained_selection`` (#410 Task A) carries the triggering raw
|
|
639
|
+
observation's canonical subscription window. It is deliberately separate
|
|
640
|
+
from ``saved``: a dedup/self-heal fold may reuse a later physical usage row,
|
|
641
|
+
but that must never retarget the triggering observation's ``wcs:`` event.
|
|
642
|
+
|
|
637
643
|
``journal`` (DB journal redesign Task 6, Design A): the ``(ctx, id_base)``
|
|
638
644
|
tuple threaded into the milestone's pre-record cost sync so the
|
|
639
645
|
``weekly_cost_snapshots`` row it inserts rides a Model-A
|
|
@@ -697,6 +703,13 @@ def maybe_record_milestone(
|
|
|
697
703
|
# Threshold crossed — sync cost before recording so the milestone
|
|
698
704
|
# captures up-to-date cumulative cost, not a stale snapshot.
|
|
699
705
|
try:
|
|
706
|
+
if retained_selection is None:
|
|
707
|
+
retained_selection = _cctally().WeekSelection(
|
|
708
|
+
week_start=dt.date.fromisoformat(week_start_date),
|
|
709
|
+
week_end=dt.date.fromisoformat(week_end_date),
|
|
710
|
+
start_iso_override=week_start_at,
|
|
711
|
+
end_iso_override=week_end_at,
|
|
712
|
+
)
|
|
700
713
|
sync_ns = argparse.Namespace(
|
|
701
714
|
week_start=None,
|
|
702
715
|
week_end=None,
|
|
@@ -721,6 +734,10 @@ def maybe_record_milestone(
|
|
|
721
734
|
# Materialize the cost snapshot under the crossing's account
|
|
722
735
|
# (#341 P2-1) so the account-scoped read below finds it.
|
|
723
736
|
account_key=account_key,
|
|
737
|
+
# #410 Task A: the implicit milestone sync is a derivation of
|
|
738
|
+
# THIS retained observation. Never ask `pick_week_selection`
|
|
739
|
+
# for the latest stats row, which may carry a later anchor.
|
|
740
|
+
retained_selection=retained_selection,
|
|
724
741
|
)
|
|
725
742
|
except Exception as exc:
|
|
726
743
|
eprint(f"[milestone] cost sync failed, using latest available: {exc}")
|
|
@@ -745,7 +762,11 @@ def maybe_record_milestone(
|
|
|
745
762
|
effective_ref = adjusted[0]
|
|
746
763
|
|
|
747
764
|
if _week_ref_has_reset_event(conn, effective_ref):
|
|
748
|
-
live_cost = _compute_cost_for_weekref(
|
|
765
|
+
live_cost = _compute_cost_for_weekref(
|
|
766
|
+
effective_ref,
|
|
767
|
+
account_key=account_key,
|
|
768
|
+
as_of=as_of,
|
|
769
|
+
)
|
|
749
770
|
if live_cost is None:
|
|
750
771
|
eprint("[milestone] could not compute effective-range cost, skipping")
|
|
751
772
|
return
|
|
@@ -1865,12 +1886,14 @@ def _compute_block_totals(
|
|
|
1865
1886
|
for entry in get_claude_session_entries(
|
|
1866
1887
|
block_start_at, range_end, skip_sync=skip_sync,
|
|
1867
1888
|
):
|
|
1868
|
-
usage =
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1889
|
+
usage = claude_usage_dict( # #195 chokepoint
|
|
1890
|
+
input_tokens=entry.input_tokens,
|
|
1891
|
+
output_tokens=entry.output_tokens,
|
|
1892
|
+
cache_creation_tokens=entry.cache_creation_tokens,
|
|
1893
|
+
cache_read_tokens=entry.cache_read_tokens,
|
|
1894
|
+
cache_1h_tokens=getattr(entry, "cache_1h_tokens", None),
|
|
1895
|
+
speed=getattr(entry, "speed", None),
|
|
1896
|
+
)
|
|
1874
1897
|
cost = _calculate_entry_cost(
|
|
1875
1898
|
entry.model, usage, mode="auto", cost_usd=entry.cost_usd,
|
|
1876
1899
|
)
|
|
@@ -1914,6 +1937,7 @@ def _compute_block_totals(
|
|
|
1914
1937
|
def maybe_update_five_hour_block(
|
|
1915
1938
|
saved: dict[str, Any], *, conn=None, as_of: "str | None" = None,
|
|
1916
1939
|
alert_sink: "list | None" = None, account_key: str = _lib_accounts.UNATTRIBUTED,
|
|
1940
|
+
journal_ctx=None,
|
|
1917
1941
|
) -> None:
|
|
1918
1942
|
"""Upsert the current 5h block in five_hour_blocks; close strictly
|
|
1919
1943
|
older open blocks; sweep naturally-expired blocks; flag blocks
|
|
@@ -1935,7 +1959,12 @@ def maybe_update_five_hour_block(
|
|
|
1935
1959
|
``BEGIN IMMEDIATE``/``commit()``/``close()``, and alert dispatch is left to
|
|
1936
1960
|
the caller (the ingester's ALERT_DISPATCHER). ``as_of`` (ISO-Z) is stamped
|
|
1937
1961
|
as ``last_updated_at_utc`` / ``alerted_at`` in place of wall clock. Both
|
|
1938
|
-
defaults keep the legacy own-connection, own-transaction behavior.
|
|
1962
|
+
defaults keep the legacy own-connection, own-transaction behavior.
|
|
1963
|
+
|
|
1964
|
+
``journal_ctx`` makes a close durable at the transition itself. The complete
|
|
1965
|
+
parent + model/project child sets are frozen before the transaction can
|
|
1966
|
+
commit; an already-stamped closed block is immutable to later observations
|
|
1967
|
+
and mutable cache growth."""
|
|
1939
1968
|
five_hour_percent = saved.get("fiveHourPercent")
|
|
1940
1969
|
five_hour_resets_at = saved.get("fiveHourResetsAt")
|
|
1941
1970
|
five_hour_window_key = saved.get("fiveHourWindowKey")
|
|
@@ -1969,7 +1998,10 @@ def maybe_update_five_hour_block(
|
|
|
1969
1998
|
prior = conn.execute(
|
|
1970
1999
|
"""
|
|
1971
2000
|
SELECT id AS prior_block_id,
|
|
1972
|
-
block_start_at AS block_start_at
|
|
2001
|
+
block_start_at AS block_start_at,
|
|
2002
|
+
is_closed AS is_closed,
|
|
2003
|
+
journal_id AS journal_id,
|
|
2004
|
+
last_updated_at_utc AS last_updated_at_utc
|
|
1973
2005
|
FROM five_hour_blocks
|
|
1974
2006
|
WHERE five_hour_window_key = ?
|
|
1975
2007
|
AND account_key = ?
|
|
@@ -2006,6 +2038,11 @@ def maybe_update_five_hour_block(
|
|
|
2006
2038
|
block_start_dt = parse_iso_datetime(
|
|
2007
2039
|
block_start_at, "five_hour_blocks.block_start_at",
|
|
2008
2040
|
)
|
|
2041
|
+
current_is_frozen = (
|
|
2042
|
+
prior is not None
|
|
2043
|
+
and int(prior["is_closed"]) == 1
|
|
2044
|
+
and prior["journal_id"] is not None
|
|
2045
|
+
)
|
|
2009
2046
|
|
|
2010
2047
|
# Step 6 (totals) — done outside the transaction so the
|
|
2011
2048
|
# cache.db read doesn't hold the stats.db write lock open.
|
|
@@ -2056,6 +2093,44 @@ def maybe_update_five_hour_block(
|
|
|
2056
2093
|
if own_conn:
|
|
2057
2094
|
conn.execute("BEGIN IMMEDIATE")
|
|
2058
2095
|
try:
|
|
2096
|
+
# Capture the exact transition set before mutating it. A close is
|
|
2097
|
+
# triggered either by a retained successor-window observation or
|
|
2098
|
+
# by the first retained observation captured after natural expiry.
|
|
2099
|
+
# Both rules use ``now_iso`` (the observation's capture clock),
|
|
2100
|
+
# never ingest/retry wall time.
|
|
2101
|
+
closing_ids = {
|
|
2102
|
+
int(row["id"])
|
|
2103
|
+
for row in conn.execute(
|
|
2104
|
+
"SELECT id FROM five_hour_blocks "
|
|
2105
|
+
"WHERE is_closed = 0 AND account_key = ? "
|
|
2106
|
+
"AND (five_hour_window_key < ? "
|
|
2107
|
+
" OR unixepoch(five_hour_resets_at) < unixepoch(?)) "
|
|
2108
|
+
"ORDER BY id",
|
|
2109
|
+
(account_key, int(five_hour_window_key), now_iso),
|
|
2110
|
+
).fetchall()
|
|
2111
|
+
}
|
|
2112
|
+
# A lost-commit retry replays the frozen event before reprocessing
|
|
2113
|
+
# its trigger observation. Re-emit that exact duplicate only when
|
|
2114
|
+
# this retained trigger clock matches the frozen closure clock;
|
|
2115
|
+
# later ordinary observations do not churn duplicate lines.
|
|
2116
|
+
retry_close_ids = {
|
|
2117
|
+
int(row["id"])
|
|
2118
|
+
for row in conn.execute(
|
|
2119
|
+
"SELECT id FROM five_hour_blocks "
|
|
2120
|
+
"WHERE is_closed = 1 AND journal_id IS NOT NULL "
|
|
2121
|
+
"AND account_key = ? AND last_updated_at_utc = ? "
|
|
2122
|
+
"AND (five_hour_window_key < ? "
|
|
2123
|
+
" OR unixepoch(five_hour_resets_at) < unixepoch(?)) "
|
|
2124
|
+
"ORDER BY id",
|
|
2125
|
+
(
|
|
2126
|
+
account_key,
|
|
2127
|
+
now_iso,
|
|
2128
|
+
int(five_hour_window_key),
|
|
2129
|
+
now_iso,
|
|
2130
|
+
),
|
|
2131
|
+
).fetchall()
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2059
2134
|
# Step 5: close any STRICTLY OLDER open block. `<` not `!=`
|
|
2060
2135
|
# — record-usage runs in parallel via background hook-tick &
|
|
2061
2136
|
# detach + status-line ticks; an older invocation completing
|
|
@@ -2078,15 +2153,15 @@ def maybe_update_five_hour_block(
|
|
|
2078
2153
|
# who lets a block expire without a successor (idle / shut down
|
|
2079
2154
|
# past the 5h reset) would otherwise leave the row at
|
|
2080
2155
|
# is_closed = 0 forever. Idempotent (only flips 0 → 1); safe to
|
|
2081
|
-
# re-run every tick.
|
|
2082
|
-
#
|
|
2156
|
+
# re-run every tick. Normalize through unixepoch() because retained
|
|
2157
|
+
# reset/capture stamps may use different equivalent UTC suffixes.
|
|
2083
2158
|
conn.execute(
|
|
2084
2159
|
"""
|
|
2085
2160
|
UPDATE five_hour_blocks
|
|
2086
2161
|
SET is_closed = 1, last_updated_at_utc = ?
|
|
2087
2162
|
WHERE is_closed = 0
|
|
2088
2163
|
AND account_key = ?
|
|
2089
|
-
AND five_hour_resets_at < ?
|
|
2164
|
+
AND unixepoch(five_hour_resets_at) < unixepoch(?)
|
|
2090
2165
|
""",
|
|
2091
2166
|
(now_iso, account_key, now_iso),
|
|
2092
2167
|
)
|
|
@@ -2136,6 +2211,8 @@ def maybe_update_five_hour_block(
|
|
|
2136
2211
|
total_cache_read_tokens = excluded.total_cache_read_tokens,
|
|
2137
2212
|
total_cost_usd = excluded.total_cost_usd,
|
|
2138
2213
|
last_updated_at_utc = excluded.last_updated_at_utc
|
|
2214
|
+
WHERE five_hour_blocks.is_closed = 0
|
|
2215
|
+
OR five_hour_blocks.journal_id IS NULL
|
|
2139
2216
|
""",
|
|
2140
2217
|
(
|
|
2141
2218
|
int(five_hour_window_key),
|
|
@@ -2176,12 +2253,13 @@ def maybe_update_five_hour_block(
|
|
|
2176
2253
|
# a strictly-newer historical block stays open (scenario D), and on
|
|
2177
2254
|
# `is_closed = 0` so it is idempotent + a no-op for a legacy own-conn
|
|
2178
2255
|
# caller whose backfill already closed the historical block. `<`
|
|
2179
|
-
#
|
|
2256
|
+
# instant-compares resets_at against now_iso, same as the sweep.
|
|
2180
2257
|
if blocks_were_empty:
|
|
2181
2258
|
conn.execute(
|
|
2182
2259
|
"UPDATE five_hour_blocks SET is_closed = 1, last_updated_at_utc = ? "
|
|
2183
2260
|
"WHERE five_hour_window_key = ? AND account_key = ? "
|
|
2184
|
-
" AND is_closed = 0
|
|
2261
|
+
" AND is_closed = 0 "
|
|
2262
|
+
" AND unixepoch(five_hour_resets_at) < unixepoch(?)",
|
|
2185
2263
|
(now_iso, int(five_hour_window_key), account_key, now_iso),
|
|
2186
2264
|
)
|
|
2187
2265
|
|
|
@@ -2190,71 +2268,83 @@ def maybe_update_five_hour_block(
|
|
|
2190
2268
|
# orphan child rows from a prior parent rebuild are cleaned up automatically.
|
|
2191
2269
|
# Same transaction as the parent upsert; if these raise, the whole tick
|
|
2192
2270
|
# rolls back and the next tick recomputes from scratch.
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
if totals.get("by_model"):
|
|
2199
|
-
conn.executemany(
|
|
2200
|
-
"""
|
|
2201
|
-
INSERT INTO five_hour_block_models (
|
|
2202
|
-
block_id, five_hour_window_key, model,
|
|
2203
|
-
input_tokens, output_tokens,
|
|
2204
|
-
cache_create_tokens, cache_read_tokens,
|
|
2205
|
-
cost_usd, entry_count, account_key
|
|
2206
|
-
)
|
|
2207
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2208
|
-
""",
|
|
2209
|
-
[
|
|
2210
|
-
(
|
|
2211
|
-
block_id,
|
|
2212
|
-
int(five_hour_window_key),
|
|
2213
|
-
model,
|
|
2214
|
-
b["input_tokens"],
|
|
2215
|
-
b["output_tokens"],
|
|
2216
|
-
b["cache_create_tokens"],
|
|
2217
|
-
b["cache_read_tokens"],
|
|
2218
|
-
b["cost_usd"],
|
|
2219
|
-
b["entry_count"],
|
|
2220
|
-
account_key,
|
|
2221
|
-
)
|
|
2222
|
-
for model, b in totals["by_model"].items()
|
|
2223
|
-
],
|
|
2271
|
+
if not current_is_frozen:
|
|
2272
|
+
conn.execute(
|
|
2273
|
+
"DELETE FROM five_hour_block_models "
|
|
2274
|
+
"WHERE five_hour_window_key = ? AND account_key = ?",
|
|
2275
|
+
(int(five_hour_window_key), account_key),
|
|
2224
2276
|
)
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
"""
|
|
2234
|
-
INSERT INTO five_hour_block_projects (
|
|
2235
|
-
block_id, five_hour_window_key, project_path,
|
|
2236
|
-
input_tokens, output_tokens,
|
|
2237
|
-
cache_create_tokens, cache_read_tokens,
|
|
2238
|
-
cost_usd, entry_count, account_key
|
|
2239
|
-
)
|
|
2240
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2241
|
-
""",
|
|
2242
|
-
[
|
|
2243
|
-
(
|
|
2244
|
-
block_id,
|
|
2245
|
-
int(five_hour_window_key),
|
|
2246
|
-
project_path,
|
|
2247
|
-
b["input_tokens"],
|
|
2248
|
-
b["output_tokens"],
|
|
2249
|
-
b["cache_create_tokens"],
|
|
2250
|
-
b["cache_read_tokens"],
|
|
2251
|
-
b["cost_usd"],
|
|
2252
|
-
b["entry_count"],
|
|
2253
|
-
account_key,
|
|
2277
|
+
if totals.get("by_model"):
|
|
2278
|
+
conn.executemany(
|
|
2279
|
+
"""
|
|
2280
|
+
INSERT INTO five_hour_block_models (
|
|
2281
|
+
block_id, five_hour_window_key, model,
|
|
2282
|
+
input_tokens, output_tokens,
|
|
2283
|
+
cache_create_tokens, cache_read_tokens,
|
|
2284
|
+
cost_usd, entry_count, account_key
|
|
2254
2285
|
)
|
|
2255
|
-
|
|
2256
|
-
|
|
2286
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2287
|
+
""",
|
|
2288
|
+
[
|
|
2289
|
+
(
|
|
2290
|
+
block_id,
|
|
2291
|
+
int(five_hour_window_key),
|
|
2292
|
+
model,
|
|
2293
|
+
b["input_tokens"],
|
|
2294
|
+
b["output_tokens"],
|
|
2295
|
+
b["cache_create_tokens"],
|
|
2296
|
+
b["cache_read_tokens"],
|
|
2297
|
+
b["cost_usd"],
|
|
2298
|
+
b["entry_count"],
|
|
2299
|
+
account_key,
|
|
2300
|
+
)
|
|
2301
|
+
for model, b in totals["by_model"].items()
|
|
2302
|
+
],
|
|
2303
|
+
)
|
|
2304
|
+
|
|
2305
|
+
conn.execute(
|
|
2306
|
+
"DELETE FROM five_hour_block_projects "
|
|
2307
|
+
"WHERE five_hour_window_key = ? AND account_key = ?",
|
|
2308
|
+
(int(five_hour_window_key), account_key),
|
|
2257
2309
|
)
|
|
2310
|
+
if totals.get("by_project"):
|
|
2311
|
+
conn.executemany(
|
|
2312
|
+
"""
|
|
2313
|
+
INSERT INTO five_hour_block_projects (
|
|
2314
|
+
block_id, five_hour_window_key, project_path,
|
|
2315
|
+
input_tokens, output_tokens,
|
|
2316
|
+
cache_create_tokens, cache_read_tokens,
|
|
2317
|
+
cost_usd, entry_count, account_key
|
|
2318
|
+
)
|
|
2319
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2320
|
+
""",
|
|
2321
|
+
[
|
|
2322
|
+
(
|
|
2323
|
+
block_id,
|
|
2324
|
+
int(five_hour_window_key),
|
|
2325
|
+
project_path,
|
|
2326
|
+
b["input_tokens"],
|
|
2327
|
+
b["output_tokens"],
|
|
2328
|
+
b["cache_create_tokens"],
|
|
2329
|
+
b["cache_read_tokens"],
|
|
2330
|
+
b["cost_usd"],
|
|
2331
|
+
b["entry_count"],
|
|
2332
|
+
account_key,
|
|
2333
|
+
)
|
|
2334
|
+
for project_path, b in totals["by_project"].items()
|
|
2335
|
+
],
|
|
2336
|
+
)
|
|
2337
|
+
|
|
2338
|
+
# The first historical block is inserted after the sweep above, so
|
|
2339
|
+
# include it explicitly when this observation closes it.
|
|
2340
|
+
if blocks_were_empty:
|
|
2341
|
+
closed_now = conn.execute(
|
|
2342
|
+
"SELECT id FROM five_hour_blocks "
|
|
2343
|
+
"WHERE id = ? AND is_closed = 1 AND journal_id IS NULL",
|
|
2344
|
+
(block_id,),
|
|
2345
|
+
).fetchone()
|
|
2346
|
+
if closed_now is not None:
|
|
2347
|
+
closing_ids.add(int(closed_now["id"]))
|
|
2258
2348
|
|
|
2259
2349
|
# ── 5h-% milestone detection (mirrors maybe_record_milestone) ──
|
|
2260
2350
|
# Snap-up-by-1e-9 per the gotcha: 0.50 * 100 == 49.99...9 in
|
|
@@ -2503,6 +2593,7 @@ def maybe_update_five_hour_block(
|
|
|
2503
2593
|
SET crossed_seven_day_reset = 1
|
|
2504
2594
|
WHERE crossed_seven_day_reset = 0
|
|
2505
2595
|
AND account_key = ?
|
|
2596
|
+
AND (is_closed = 0 OR journal_id IS NULL)
|
|
2506
2597
|
AND (
|
|
2507
2598
|
EXISTS (
|
|
2508
2599
|
SELECT 1 FROM week_reset_events e
|
|
@@ -2525,6 +2616,19 @@ def maybe_update_five_hour_block(
|
|
|
2525
2616
|
(account_key,),
|
|
2526
2617
|
)
|
|
2527
2618
|
|
|
2619
|
+
# Freeze only after every parent field and both rollup child sets
|
|
2620
|
+
# have reached their final close-state values for this observation.
|
|
2621
|
+
# The cross-reset sweep above is part of the parent fact, so moving
|
|
2622
|
+
# this earlier would journal ``crossed_seven_day_reset = 0`` and
|
|
2623
|
+
# then mutate the live closed row to 1.
|
|
2624
|
+
if journal_ctx is not None:
|
|
2625
|
+
import _cctally_journal as _jr
|
|
2626
|
+
|
|
2627
|
+
for closing_id in sorted(closing_ids):
|
|
2628
|
+
_jr.freeze_five_hour_block_close(journal_ctx, closing_id)
|
|
2629
|
+
for retry_id in sorted(retry_close_ids - closing_ids):
|
|
2630
|
+
_jr.freeze_five_hour_block_close(journal_ctx, retry_id)
|
|
2631
|
+
|
|
2528
2632
|
if own_conn:
|
|
2529
2633
|
conn.commit()
|
|
2530
2634
|
except Exception:
|
|
@@ -2636,6 +2740,38 @@ def _read_reset_zero_marker():
|
|
|
2636
2740
|
return (week_start_date, cur_end_canon, baseline_pct, first_zero_iso)
|
|
2637
2741
|
|
|
2638
2742
|
|
|
2743
|
+
def _projection_read_reset_zero_marker(ctx):
|
|
2744
|
+
if ctx is not None and not ctx.projection_writes:
|
|
2745
|
+
return ctx.projection_state.get("reset_zero_marker")
|
|
2746
|
+
return _read_reset_zero_marker()
|
|
2747
|
+
|
|
2748
|
+
|
|
2749
|
+
def _projection_clear_reset_zero_marker(ctx):
|
|
2750
|
+
if ctx is not None and not ctx.projection_writes:
|
|
2751
|
+
ctx.projection_state.pop("reset_zero_marker", None)
|
|
2752
|
+
return
|
|
2753
|
+
_clear_reset_zero_marker()
|
|
2754
|
+
|
|
2755
|
+
|
|
2756
|
+
def _projection_arm_reset_zero_marker(
|
|
2757
|
+
ctx, week_start_date, cur_end_canon, *, baseline_pct, first_zero_iso,
|
|
2758
|
+
):
|
|
2759
|
+
if ctx is not None and not ctx.projection_writes:
|
|
2760
|
+
ctx.projection_state["reset_zero_marker"] = (
|
|
2761
|
+
week_start_date,
|
|
2762
|
+
cur_end_canon,
|
|
2763
|
+
float(baseline_pct),
|
|
2764
|
+
first_zero_iso,
|
|
2765
|
+
)
|
|
2766
|
+
return
|
|
2767
|
+
_arm_reset_zero_marker(
|
|
2768
|
+
week_start_date,
|
|
2769
|
+
cur_end_canon,
|
|
2770
|
+
baseline_pct=baseline_pct,
|
|
2771
|
+
first_zero_iso=first_zero_iso,
|
|
2772
|
+
)
|
|
2773
|
+
|
|
2774
|
+
|
|
2639
2775
|
# ``CreditPlan`` / ``_parse_credit_at`` / ``_build_credit_plan`` now live in
|
|
2640
2776
|
# ``bin/_lib_credit.py`` (#279 S4 F1); re-imported at module top so the
|
|
2641
2777
|
# ``bin/cctally`` re-exports and this module's own callers
|
|
@@ -2727,12 +2863,13 @@ def _fire_in_place_credit(conn, week_start_date, cur_end_canon, weekly_percent,
|
|
|
2727
2863
|
# Unconditional pivot 1: force-write hwm-7d so the next status-line render
|
|
2728
2864
|
# reflects the post-credit value (the monotonic guard at the normal write
|
|
2729
2865
|
# site would refuse to decrease the file).
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2866
|
+
if ctx is None or ctx.projection_writes:
|
|
2867
|
+
try:
|
|
2868
|
+
(_cctally_core.APP_DIR / "hwm-7d").write_text(
|
|
2869
|
+
f"{week_start_date} {weekly_percent}\n"
|
|
2870
|
+
)
|
|
2871
|
+
except OSError:
|
|
2872
|
+
pass
|
|
2736
2873
|
# Unconditional pivot 2: race-defensive cleanup of stale pre-credit replays
|
|
2737
2874
|
# (external claude-statusline can replay pre-credit --percent values that
|
|
2738
2875
|
# land captured_at >= effective with pct ~= baseline and dominate the
|
|
@@ -2812,8 +2949,9 @@ def detect_reset_and_credit(conn, *, week_start_date, week_end_at,
|
|
|
2812
2949
|
cur_end_canon = _canonicalize_optional_iso(week_end_at, "record.cur")
|
|
2813
2950
|
prior = conn.execute(
|
|
2814
2951
|
"SELECT week_end_at, weekly_percent FROM weekly_usage_snapshots "
|
|
2815
|
-
"WHERE week_end_at IS NOT NULL "
|
|
2816
|
-
"ORDER BY captured_at_utc DESC, id DESC LIMIT 1"
|
|
2952
|
+
"WHERE week_end_at IS NOT NULL AND account_key = ? "
|
|
2953
|
+
"ORDER BY captured_at_utc DESC, id DESC LIMIT 1",
|
|
2954
|
+
(account_key,),
|
|
2817
2955
|
).fetchone()
|
|
2818
2956
|
if prior and prior["week_end_at"] and cur_end_canon:
|
|
2819
2957
|
prior_end_canon = _canonicalize_optional_iso(
|
|
@@ -2872,7 +3010,7 @@ def detect_reset_and_credit(conn, *, week_start_date, week_end_at,
|
|
|
2872
3010
|
# then the glue below executes the decided I/O. The 5
|
|
2873
3011
|
# branch outcomes (fire-immediate / confirm / clear /
|
|
2874
3012
|
# arm / none) map 1:1 to the pre-extraction structure.
|
|
2875
|
-
marker =
|
|
3013
|
+
marker = _projection_read_reset_zero_marker(ctx)
|
|
2876
3014
|
armed = (
|
|
2877
3015
|
marker is not None
|
|
2878
3016
|
and marker[0] == week_start_date
|
|
@@ -2889,7 +3027,7 @@ def detect_reset_and_credit(conn, *, week_start_date, week_end_at,
|
|
|
2889
3027
|
if decision == FIRE_IMMEDIATE:
|
|
2890
3028
|
# >=25pp goodwill credit — fire immediately, never
|
|
2891
3029
|
# debounced. Clear any pending arm (now moot).
|
|
2892
|
-
|
|
3030
|
+
_projection_clear_reset_zero_marker(ctx)
|
|
2893
3031
|
_fire_in_place_credit(
|
|
2894
3032
|
conn, week_start_date, cur_end_canon, weekly_percent,
|
|
2895
3033
|
observed_pre_credit_pct=float(prior_pct),
|
|
@@ -2915,11 +3053,11 @@ def detect_reset_and_credit(conn, *, week_start_date, week_end_at,
|
|
|
2915
3053
|
# Clear ONLY after the fire completes (P2a): a
|
|
2916
3054
|
# mid-fire crash leaves the marker armed so the next
|
|
2917
3055
|
# zero re-confirms + re-runs the idempotent pivots.
|
|
2918
|
-
|
|
3056
|
+
_projection_clear_reset_zero_marker(ctx)
|
|
2919
3057
|
elif decision == CLEAR_MARKER:
|
|
2920
3058
|
# Recovered toward baseline → transient zero, not a
|
|
2921
3059
|
# reset. Clear, do not fire.
|
|
2922
|
-
|
|
3060
|
+
_projection_clear_reset_zero_marker(ctx)
|
|
2923
3061
|
elif decision == ARM_MARKER:
|
|
2924
3062
|
# First ~0 → arm; do NOT fire. The write clamp
|
|
2925
3063
|
# suppresses this 0 (no event row yet), so the prior
|
|
@@ -2927,7 +3065,8 @@ def detect_reset_and_credit(conn, *, week_start_date, week_end_at,
|
|
|
2927
3065
|
# re-evaluates next tick. first_zero_iso is the
|
|
2928
3066
|
# _command_as_of() value (now_utc), NOT wall-clock —
|
|
2929
3067
|
# it becomes the effective anchor.
|
|
2930
|
-
|
|
3068
|
+
_projection_arm_reset_zero_marker(
|
|
3069
|
+
ctx,
|
|
2931
3070
|
week_start_date, cur_end_canon,
|
|
2932
3071
|
baseline_pct=float(prior_pct),
|
|
2933
3072
|
first_zero_iso=now_utc.isoformat(timespec="seconds"),
|
|
@@ -2973,7 +3112,9 @@ def detect_reset_and_credit(conn, *, week_start_date, week_end_at,
|
|
|
2973
3112
|
" FROM weekly_usage_snapshots "
|
|
2974
3113
|
" WHERE five_hour_window_key IS NOT NULL "
|
|
2975
3114
|
" AND five_hour_percent IS NOT NULL "
|
|
2976
|
-
"
|
|
3115
|
+
" AND account_key = ? "
|
|
3116
|
+
" ORDER BY captured_at_utc DESC, id DESC LIMIT 1",
|
|
3117
|
+
(account_key,),
|
|
2977
3118
|
).fetchone()
|
|
2978
3119
|
if (
|
|
2979
3120
|
prior_5h_row is not None
|
|
@@ -3136,13 +3277,14 @@ def detect_reset_and_credit(conn, *, week_start_date, week_end_at,
|
|
|
3136
3277
|
# event row is durable. File format
|
|
3137
3278
|
# matches the canonical writer:
|
|
3138
3279
|
# ``<key> <percent>\n``.
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3280
|
+
if ctx is None or ctx.projection_writes:
|
|
3281
|
+
try:
|
|
3282
|
+
(_cctally_core.APP_DIR / "hwm-5h").write_text(
|
|
3283
|
+
f"{int(five_hour_window_key)} "
|
|
3284
|
+
f"{float(five_hour_percent)}\n"
|
|
3285
|
+
)
|
|
3286
|
+
except OSError:
|
|
3287
|
+
pass
|
|
3146
3288
|
# Stale-replica DELETE (spec §4.3).
|
|
3147
3289
|
# Defends against claude-statusline
|
|
3148
3290
|
# replaying the pre-credit
|
|
@@ -3548,7 +3690,7 @@ def _apply_credit(conn, plan, *, five_hour=(None, None, None), as_of=None,
|
|
|
3548
3690
|
|
|
3549
3691
|
# 4e. Clear a stale same-week reset-zero marker so the next record-usage
|
|
3550
3692
|
# tick can't confirm a phantom reset-to-zero off it.
|
|
3551
|
-
|
|
3693
|
+
_projection_clear_reset_zero_marker(ctx)
|
|
3552
3694
|
|
|
3553
3695
|
|
|
3554
3696
|
def _count_stale_replays(conn, plan):
|
|
@@ -4094,6 +4236,11 @@ def cmd_record_usage(
|
|
|
4094
4236
|
# Build the RAW observation (spec 4.2 / 5.3 -- raw capture only, NO derived
|
|
4095
4237
|
# week columns; `_pipeline_claude_usage` canonicalizes the week boundaries +
|
|
4096
4238
|
# the 5h window key at ingest). Append it, then run the single-flight cycle.
|
|
4239
|
+
# The capture clock intentionally stays separate from CCTALLY_AS_OF in
|
|
4240
|
+
# production. The harness-only pin lets deterministic CLI scenarios model a
|
|
4241
|
+
# genuinely open historical block without pretending a years-old reset is
|
|
4242
|
+
# current wall time.
|
|
4243
|
+
capture_now = now_dt if os.environ.get("CCTALLY_TEST_PIN_CAPTURE") else None
|
|
4097
4244
|
raw: dict[str, Any] = {
|
|
4098
4245
|
"weekly_percent": weekly_percent,
|
|
4099
4246
|
"resets_at": resets_at,
|
|
@@ -4105,7 +4252,7 @@ def cmd_record_usage(
|
|
|
4105
4252
|
# carries `_command_as_of()` (below) so the hook preserves that split.
|
|
4106
4253
|
# Captured ONCE here (append time) and journaled, so a delayed ingest is
|
|
4107
4254
|
# deterministic (never the ingest wall clock).
|
|
4108
|
-
"captured_at": now_utc_iso(),
|
|
4255
|
+
"captured_at": now_utc_iso(capture_now),
|
|
4109
4256
|
}
|
|
4110
4257
|
if five_hour_percent is not None:
|
|
4111
4258
|
raw["five_hour_percent"] = five_hour_percent
|
|
@@ -4539,6 +4686,7 @@ def _cmd_hook_tick_codex(args: argparse.Namespace, *, event: str = "unknown") ->
|
|
|
4539
4686
|
lambda active_conn: c.sync_codex_cache(
|
|
4540
4687
|
active_conn, lock_timeout=0
|
|
4541
4688
|
),
|
|
4689
|
+
origin="hook.codex_quota.sync",
|
|
4542
4690
|
)
|
|
4543
4691
|
finally:
|
|
4544
4692
|
cache.close()
|
|
@@ -4717,6 +4865,7 @@ def cmd_hook_tick(args: argparse.Namespace) -> int:
|
|
|
4717
4865
|
cache_mod._run_cache_operation_with_recovery(
|
|
4718
4866
|
cache_conn,
|
|
4719
4867
|
lambda active_conn: sync_cache(active_conn),
|
|
4868
|
+
origin="hook.claude.sync",
|
|
4720
4869
|
)
|
|
4721
4870
|
)
|
|
4722
4871
|
ingested = int(stats.rows_changed)
|
|
@@ -5196,13 +5345,15 @@ def _derive_5h_window_key(conn, five_hour_resets_at_epoch):
|
|
|
5196
5345
|
)
|
|
5197
5346
|
|
|
5198
5347
|
|
|
5199
|
-
def _run_dollar_axes(saved, *, conn, as_of, alert_sink):
|
|
5348
|
+
def _run_dollar_axes(saved, *, conn, as_of, alert_sink, enabled=True):
|
|
5200
5349
|
"""The four dollar-decoupled alert axes in cmd_record_usage's legacy order
|
|
5201
5350
|
(budget → project-budget → codex-budget → projected). Runs on BOTH the accept
|
|
5202
5351
|
path AND every dedup-skip tick (spec §4.5: USD spend can cross a $ threshold
|
|
5203
5352
|
while the weekly/5h percent is flat). Passed-conn → the chokepoints fold into
|
|
5204
5353
|
the cycle txn and re-raise on failure (invariant ii); their crossings' alert
|
|
5205
5354
|
payloads land in `alert_sink` for post-commit dispatch."""
|
|
5355
|
+
if not enabled:
|
|
5356
|
+
return
|
|
5206
5357
|
c = _cctally()
|
|
5207
5358
|
c.maybe_record_budget_milestone(
|
|
5208
5359
|
saved, conn=conn, as_of=as_of, alert_sink=alert_sink)
|
|
@@ -5302,7 +5453,13 @@ def _pipeline_claude_usage(ctx, rec):
|
|
|
5302
5453
|
account_key = rec.get("account") or _lib_accounts.UNATTRIBUTED
|
|
5303
5454
|
|
|
5304
5455
|
# Week boundaries from resets_at (cmd_record_usage canonicalization).
|
|
5305
|
-
|
|
5456
|
+
# Normalize before deriving the date keys as well as the ISO overrides:
|
|
5457
|
+
# rounding a 23:30–23:59 boundary crosses midnight, and letting
|
|
5458
|
+
# `_usage_snapshot_columns` normalize only the saved row would fork the
|
|
5459
|
+
# milestone's retained cost window onto the prior calendar date.
|
|
5460
|
+
week_end_at_dt = _cctally_core._normalize_week_boundary_dt(
|
|
5461
|
+
dt.datetime.fromtimestamp(resets_at, tz=dt.timezone.utc)
|
|
5462
|
+
)
|
|
5306
5463
|
week_start_at_dt = week_end_at_dt - dt.timedelta(days=7)
|
|
5307
5464
|
week_start_date = week_start_at_dt.date().isoformat()
|
|
5308
5465
|
week_end_date = week_end_at_dt.date().isoformat()
|
|
@@ -5409,10 +5566,16 @@ def _pipeline_claude_usage(ctx, rec):
|
|
|
5409
5566
|
# crosses a $ threshold still derives it.
|
|
5410
5567
|
c.maybe_record_milestone(
|
|
5411
5568
|
saved, conn=conn, as_of=capture_at, alert_sink=ctx.pending_alerts,
|
|
5412
|
-
journal=(ctx, rec["id"]), account_key=account_key
|
|
5569
|
+
journal=(ctx, rec["id"]), account_key=account_key,
|
|
5570
|
+
retained_selection=c.WeekSelection(
|
|
5571
|
+
week_start=dt.date.fromisoformat(week_start_date),
|
|
5572
|
+
week_end=dt.date.fromisoformat(week_end_date),
|
|
5573
|
+
start_iso_override=week_start_at,
|
|
5574
|
+
end_iso_override=week_end_at,
|
|
5575
|
+
))
|
|
5413
5576
|
c.maybe_update_five_hour_block(
|
|
5414
5577
|
saved, conn=conn, as_of=capture_at, alert_sink=ctx.pending_alerts,
|
|
5415
|
-
account_key=account_key)
|
|
5578
|
+
account_key=account_key, journal_ctx=ctx)
|
|
5416
5579
|
# The dollar-decoupled axes resolve a CURRENT budget/period WINDOW from
|
|
5417
5580
|
# "now" and must see the record's DETECTION clock (`as_of` = rec["at"] =
|
|
5418
5581
|
# `_command_as_of()`), NOT the wall-clock capture stamp: legacy
|
|
@@ -5421,8 +5584,16 @@ def _pipeline_claude_usage(ctx, rec):
|
|
|
5421
5584
|
# week's window (capture_at is the raw wall clock, which lands outside a
|
|
5422
5585
|
# pinned fixture window and silently drops the crossing). Both collapse to
|
|
5423
5586
|
# real-now in production; the split only matters under a pinned as_of.
|
|
5424
|
-
_run_dollar_axes(
|
|
5425
|
-
|
|
5587
|
+
_run_dollar_axes(
|
|
5588
|
+
saved,
|
|
5589
|
+
conn=conn,
|
|
5590
|
+
as_of=as_of,
|
|
5591
|
+
alert_sink=ctx.pending_alerts,
|
|
5592
|
+
# Budget/projected rows depend on historical config that was not retained
|
|
5593
|
+
# in the journal. Task B classifies them as re-materialized projections:
|
|
5594
|
+
# the planner retires stale latches and never fabricates old config.
|
|
5595
|
+
enabled=(ctx.event_sink is None),
|
|
5596
|
+
)
|
|
5426
5597
|
|
|
5427
5598
|
# 4'. Window-rollover 5h-block heal (SKIP path only). A dedup skip swallows
|
|
5428
5599
|
# the snapshot insert, so `saved` (the dedup target — the LATEST stored
|
|
@@ -5457,12 +5628,12 @@ def _pipeline_claude_usage(ctx, rec):
|
|
|
5457
5628
|
"fiveHourWindowKey": int(five_hour_window_key),
|
|
5458
5629
|
},
|
|
5459
5630
|
conn=conn, as_of=capture_at, alert_sink=ctx.pending_alerts,
|
|
5460
|
-
account_key=account_key)
|
|
5631
|
+
account_key=account_key, journal_ctx=ctx)
|
|
5461
5632
|
|
|
5462
5633
|
# 5. hwm-7d / hwm-5h projection files — ACCEPT path only (the monotonic
|
|
5463
5634
|
# writer; a dedup tick's percent is already <= the stored HWM). In-place
|
|
5464
5635
|
# credit force-writes live in detect_reset_and_credit.
|
|
5465
|
-
if not skip:
|
|
5636
|
+
if not skip and ctx.projection_writes:
|
|
5466
5637
|
_write_hwm_files(week_start_date, weekly_percent,
|
|
5467
5638
|
five_hour_window_key, five_hour_percent)
|
|
5468
5639
|
|