cctally 1.116.1 → 1.117.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -0
- package/README.md +4 -6
- package/bin/_cctally_account.py +57 -3
- package/bin/_cctally_cache.py +5720 -885
- package/bin/_cctally_claude_attribution.py +997 -0
- package/bin/_cctally_completion.py +101 -28
- package/bin/_cctally_core.py +235 -12
- package/bin/_cctally_dashboard.py +263 -17
- package/bin/_cctally_dashboard_conversation.py +107 -21
- package/bin/_cctally_dashboard_envelope.py +44 -0
- package/bin/_cctally_db.py +928 -80
- package/bin/_cctally_diagnosis_sources.py +78 -13
- package/bin/_cctally_doctor.py +211 -86
- package/bin/_cctally_five_hour.py +446 -22
- package/bin/_cctally_forecast.py +302 -6
- package/bin/_cctally_journal.py +2418 -158
- package/bin/_cctally_milestone_history.py +379 -91
- package/bin/_cctally_oauth_samples.py +185 -7
- package/bin/_cctally_parser.py +38 -0
- package/bin/_cctally_project.py +34 -8
- package/bin/_cctally_quarantine.py +821 -343
- package/bin/_cctally_quota.py +69 -4
- package/bin/_cctally_quota_calibration.py +677 -3
- package/bin/_cctally_quota_model.py +927 -127
- package/bin/_cctally_record.py +922 -445
- package/bin/_cctally_rederive.py +119 -15
- package/bin/_cctally_statusline.py +1048 -176
- package/bin/_cctally_store.py +544 -110
- package/bin/_cctally_tui.py +455 -42
- package/bin/_cctally_update.py +77 -14
- package/bin/_lib_account_protocol.py +227 -19
- package/bin/_lib_claude_attribution.py +421 -0
- package/bin/_lib_clone_copy.py +30 -0
- package/bin/_lib_codex_conversation_query.py +45 -9
- package/bin/_lib_codex_conversation_watch.py +72 -13
- package/bin/_lib_codex_rollout_container.py +234 -19
- package/bin/_lib_codex_worker_lifecycle.py +374 -0
- package/bin/_lib_credit.py +22 -13
- package/bin/_lib_dashboard_sources.py +257 -18
- package/bin/_lib_detector_access_registry.py +1335 -166
- package/bin/_lib_doctor.py +276 -92
- package/bin/_lib_ingest_frontier.py +269 -36
- package/bin/_lib_journal.py +273 -22
- package/bin/_lib_jsonl.py +12 -0
- package/bin/_lib_meter_rate_change.py +72 -0
- package/bin/_lib_outcome_quarantine.py +917 -554
- package/bin/_lib_provider_activity.py +24 -6
- package/bin/_lib_quota_calibration.py +57 -3
- package/bin/_lib_quota_copy.py +7 -2
- package/bin/_lib_quota_lineage.py +613 -0
- package/bin/_lib_quota_model.py +464 -33
- package/bin/_lib_rate_change_delivery.py +16 -0
- package/bin/_lib_reclaim_planner.py +6 -4
- package/bin/_lib_rederive.py +2046 -21
- package/bin/_lib_selector_state.py +131 -3
- package/bin/_lib_snapshot_cache.py +8 -0
- package/bin/_lib_stale_proof.py +113 -4
- package/bin/_lib_statusline.py +72 -3
- package/bin/_lib_statusline_candidates.py +246 -42
- package/bin/_lib_tick_stats.py +10 -5
- package/bin/_lib_write_io.py +419 -6
- package/bin/cctally +28 -11
- package/dashboard/static/assets/{ConversationsView-Dflb_myA.js → ConversationsView-oqNsWzWV.js} +1 -1
- package/dashboard/static/assets/{DoctorModal-DFX5CfIs.js → DoctorModal-B0DnU9d9.js} +1 -1
- package/dashboard/static/assets/ModalRoot-CYua2wP_.js +1 -0
- package/dashboard/static/assets/{ProjectsDrillPanel-C3PJy1xx.js → ProjectsDrillPanel-DSxXQSa-.js} +1 -1
- package/dashboard/static/assets/{SourceDetailModal-BczsWKhD.js → SourceDetailModal-BleDpQqG.js} +1 -1
- package/dashboard/static/assets/{UpdateModal-Os-Lo5cs.js → UpdateModal-CMqaAkSK.js} +1 -1
- package/dashboard/static/assets/{index-CYZ3NLqF.js → index-BjQV-0-D.js} +3 -3
- package/dashboard/static/assets/index-D34eYhAc.css +1 -0
- package/dashboard/static/assets/{outlineNavigation-k-zIif6W.js → outlineNavigation-CO-VWBm7.js} +1 -1
- package/dashboard/static/assets/{useKeymap-CW7Up0Ay.js → useKeymap-CUgEnnua.js} +1 -1
- package/dashboard/static/dashboard.html +3 -3
- package/package.json +5 -1
- package/dashboard/static/assets/ModalRoot-HlxoagIa.js +0 -1
- package/dashboard/static/assets/index-BKITk1CT.css +0 -1
|
@@ -42,7 +42,9 @@ import datetime as dt
|
|
|
42
42
|
import json
|
|
43
43
|
import sqlite3
|
|
44
44
|
import sys
|
|
45
|
+
from dataclasses import dataclass
|
|
45
46
|
|
|
47
|
+
import _cctally_core # `weekly_held_exclusion`: the held-column guard (#836)
|
|
46
48
|
import _lib_accounts # pure stdlib kernel; UNATTRIBUTED sentinel (#341)
|
|
47
49
|
import _lib_rebuild_progress # #997 D9 progress ticks (stdlib only)
|
|
48
50
|
from _lib_five_hour import latest_five_hour_reading
|
|
@@ -1085,13 +1087,57 @@ def _credit_aware_block_weekly_axes(conn, block, *, now_utc, cache=None):
|
|
|
1085
1087
|
block.get("seven_day_pct_at_block_end"))
|
|
1086
1088
|
|
|
1087
1089
|
|
|
1090
|
+
def _weekly_retired_sql(conn, alias: str) -> "str | None":
|
|
1091
|
+
"""The suppression-retired predicate over ``alias``, or None on a store
|
|
1092
|
+
without the held column (#836, spec 5.4).
|
|
1093
|
+
|
|
1094
|
+
A held WRITE stores ``"weeklyObservationHeld": true`` in ``payload_json``
|
|
1095
|
+
beside its effective basis value (`_cctally_record`, the held branch of
|
|
1096
|
+
the snapshot write). A weekly credit's cleanup instead turns an UNHELD row,
|
|
1097
|
+
whose payload never carried that marker, into a held one and leaves its
|
|
1098
|
+
stale `weekly_percent` in place (`_delete_doomed_snapshot_rows`, replay
|
|
1099
|
+
`_cctally_journal._retire_weekly_effects`). The marker therefore separates
|
|
1100
|
+
the two exactly. A store without the column cannot hold a held row, the
|
|
1101
|
+
reason `_cctally_core.weekly_held_exclusion` omits its own predicate there.
|
|
1102
|
+
"""
|
|
1103
|
+
if not _cctally_core.weekly_held_exclusion(conn):
|
|
1104
|
+
return None
|
|
1105
|
+
return (f"({alias}.weekly_observation_held = 1 AND "
|
|
1106
|
+
f"json_extract({alias}.payload_json, '$.weeklyObservationHeld') "
|
|
1107
|
+
f"IS NOT 1)")
|
|
1108
|
+
|
|
1109
|
+
|
|
1110
|
+
def effective_weekly_value_sql(conn, alias: str = "s") -> str:
|
|
1111
|
+
"""The joined snapshot's EFFECTIVE weekly value (#836, spec 5.4): NULL for
|
|
1112
|
+
a suppression-retired row, its stored `weekly_percent` otherwise. A
|
|
1113
|
+
clamped held write and a genuine climb-back keep their values; no
|
|
1114
|
+
percentage band is involved."""
|
|
1115
|
+
retired = _weekly_retired_sql(conn, alias)
|
|
1116
|
+
if retired is None:
|
|
1117
|
+
return f"{alias}.weekly_percent"
|
|
1118
|
+
return f"CASE WHEN {retired} THEN NULL ELSE {alias}.weekly_percent END"
|
|
1119
|
+
|
|
1120
|
+
|
|
1121
|
+
def weekly_join_unavailable_sql(conn, alias: str = "s") -> str:
|
|
1122
|
+
"""A five-hour milestone's effective weekly join is UNAVAILABLE (#836,
|
|
1123
|
+
spec 5.4): the joined snapshot is absent — deleted, or the ``0`` sentinel
|
|
1124
|
+
`trg_usage_snapshot_suppression` writes — or suppression-retired. The
|
|
1125
|
+
shared predicate of the detail stamp and the dashboard's retired-join
|
|
1126
|
+
signature leg, over a ``LEFT JOIN`` aliased ``alias``."""
|
|
1127
|
+
retired = _weekly_retired_sql(conn, alias)
|
|
1128
|
+
if retired is None:
|
|
1129
|
+
return f"({alias}.id IS NULL)"
|
|
1130
|
+
return f"({alias}.id IS NULL OR {retired})"
|
|
1131
|
+
|
|
1132
|
+
|
|
1088
1133
|
#: The six `five_hour_milestones` columns every surface already published, plus
|
|
1089
|
-
#: the joined EFFECTIVE weekly value
|
|
1090
|
-
#:
|
|
1134
|
+
#: the joined EFFECTIVE weekly value (`effective_weekly_value_sql`, filled in
|
|
1135
|
+
#: as ``{effective}``). Ordering is `captured_at_utc, id` — spec §5.2's
|
|
1136
|
+
#: merged-stream order, which post-credit threshold repeats depend on.
|
|
1091
1137
|
_FIVE_HOUR_MILESTONE_SQL = """
|
|
1092
1138
|
SELECT m.percent_threshold, m.captured_at_utc, m.block_cost_usd,
|
|
1093
1139
|
m.marginal_cost_usd, m.seven_day_pct_at_crossing, m.reset_event_id,
|
|
1094
|
-
|
|
1140
|
+
{effective} AS effective_seven_day_pct_at_crossing
|
|
1095
1141
|
FROM five_hour_milestones m
|
|
1096
1142
|
LEFT JOIN weekly_usage_snapshots s ON s.id = m.usage_snapshot_id
|
|
1097
1143
|
WHERE {selector}
|
|
@@ -1138,6 +1184,12 @@ def _load_five_hour_milestones(
|
|
|
1138
1184
|
store is corrected by `db rederive`'s reviewed `retract`. A missing joined
|
|
1139
1185
|
snapshot still publishes None and never falls back to the raw value.
|
|
1140
1186
|
|
|
1187
|
+
**A suppression-retired snapshot publishes None** (#836, spec 5.4). The
|
|
1188
|
+
fold rejected its weekly value, and the cleanup kept the row only for its
|
|
1189
|
+
five-hour evidence; its provenance is exact (no held-write marker on a
|
|
1190
|
+
held row), so this is not the withdrawn band. See
|
|
1191
|
+
`effective_weekly_value_sql`.
|
|
1192
|
+
|
|
1141
1193
|
SELECTOR. Exactly one of `block_id` or `five_hour_window_key` must be given.
|
|
1142
1194
|
`block_id` is the precise form — `five_hour_milestones.block_id` names one
|
|
1143
1195
|
block outright. The pair form filters on the window key AND the account,
|
|
@@ -1165,7 +1217,9 @@ def _load_five_hour_milestones(
|
|
|
1165
1217
|
selector += " AND m.account_key = ?"
|
|
1166
1218
|
params += (account_key,)
|
|
1167
1219
|
rows = conn.execute(
|
|
1168
|
-
_FIVE_HOUR_MILESTONE_SQL.format(
|
|
1220
|
+
_FIVE_HOUR_MILESTONE_SQL.format(
|
|
1221
|
+
selector=selector,
|
|
1222
|
+
effective=effective_weekly_value_sql(conn, "s")), params,
|
|
1169
1223
|
).fetchall()
|
|
1170
1224
|
out: list[dict] = []
|
|
1171
1225
|
for r in rows:
|
|
@@ -1779,9 +1833,359 @@ def cmd_five_hour_breakdown(args: argparse.Namespace) -> int:
|
|
|
1779
1833
|
conn.close()
|
|
1780
1834
|
|
|
1781
1835
|
|
|
1836
|
+
@dataclass(frozen=True)
|
|
1837
|
+
class OpenBlockEnd:
|
|
1838
|
+
"""The reading an open five-hour block ends at (#956 class 1, spec
|
|
1839
|
+
4.5.1): its capture, five-hour percent and weekly percent, as stored and
|
|
1840
|
+
fold-effective. ``origin`` names where it came from: ``"snapshot"`` is the
|
|
1841
|
+
window's latest surviving five-hour reading, ``"opening"`` the opening
|
|
1842
|
+
reading of the window's retained opening fact (`five_hour_block_openings`,
|
|
1843
|
+
#956 class 2, spec 4.5.4) when no reading survives."""
|
|
1844
|
+
|
|
1845
|
+
captured_at_utc: str
|
|
1846
|
+
five_hour_percent: float
|
|
1847
|
+
seven_day_percent: "float | None"
|
|
1848
|
+
origin: str
|
|
1849
|
+
|
|
1850
|
+
|
|
1851
|
+
def select_open_block_end(
|
|
1852
|
+
conn: sqlite3.Connection, *, account_key: str, five_hour_window_key: int,
|
|
1853
|
+
) -> "OpenBlockEnd | None":
|
|
1854
|
+
"""The end reading of one open block, read only (#956 class 1): the
|
|
1855
|
+
window's latest surviving five-hour reading in CAPTURE order, held rows
|
|
1856
|
+
included, ties broken by logical identity (`journal_id`, a NULL one
|
|
1857
|
+
lowest), never by journal order or rowid.
|
|
1858
|
+
|
|
1859
|
+
With no surviving reading (a correction removed every one, or `record-
|
|
1860
|
+
credit --force` removed the synthetics that were the window's only
|
|
1861
|
+
readings) it is the window's opening fact's OPENING reading (#956 spec
|
|
1862
|
+
4.5.4): its capture, five-hour percent and weekly start value, retained
|
|
1863
|
+
and immutable, so live, every rebuild and rederive agree. ``None`` only
|
|
1864
|
+
for a legacy window with neither (residual R4); the caller then leaves
|
|
1865
|
+
the end columns as they are.
|
|
1866
|
+
|
|
1867
|
+
Live ingest, derivation completion and the rebuild's Phase 2a all read
|
|
1868
|
+
the end here, so an earlier capture appended later, a credit that
|
|
1869
|
+
suppresses the selected reading and a `record-credit` synthetic move
|
|
1870
|
+
every path identically. `latest_five_hour_reading` (the week-scoped
|
|
1871
|
+
selection forecast, statusline and the dashboard read) is deliberately
|
|
1872
|
+
a different function and is not changed."""
|
|
1873
|
+
# The order `unixepoch(captured_at_utc) DESC, journal_id DESC` (NULLs
|
|
1874
|
+
# lowest on both keys, as SQLite sorts them under DESC) is applied here
|
|
1875
|
+
# over the window's rows rather than by an ORDER BY: this runs on every
|
|
1876
|
+
# live tick, and the window-key index cannot serve an expression order,
|
|
1877
|
+
# so SQL would sort through a temp B-tree each time (#902's recurring-
|
|
1878
|
+
# plan guard). One five-hour window holds a bounded number of readings.
|
|
1879
|
+
rows = conn.execute(
|
|
1880
|
+
"""
|
|
1881
|
+
SELECT unixepoch(captured_at_utc) AS captured_epoch, journal_id,
|
|
1882
|
+
captured_at_utc, five_hour_percent, weekly_percent
|
|
1883
|
+
FROM weekly_usage_snapshots
|
|
1884
|
+
WHERE account_key = ? AND five_hour_window_key = ?
|
|
1885
|
+
AND five_hour_percent IS NOT NULL
|
|
1886
|
+
""",
|
|
1887
|
+
(account_key, int(five_hour_window_key)),
|
|
1888
|
+
).fetchall()
|
|
1889
|
+
if not rows:
|
|
1890
|
+
seed = conn.execute(
|
|
1891
|
+
"SELECT first_observed_at_utc, five_hour_percent, "
|
|
1892
|
+
" seven_day_pct_at_block_start "
|
|
1893
|
+
" FROM five_hour_block_openings "
|
|
1894
|
+
" WHERE account_key = ? AND five_hour_window_key = ?",
|
|
1895
|
+
(account_key, int(five_hour_window_key)),
|
|
1896
|
+
).fetchone()
|
|
1897
|
+
if seed is None:
|
|
1898
|
+
return None
|
|
1899
|
+
return OpenBlockEnd(
|
|
1900
|
+
captured_at_utc=seed[0],
|
|
1901
|
+
five_hour_percent=float(seed[1]),
|
|
1902
|
+
seven_day_percent=None if seed[2] is None else float(seed[2]),
|
|
1903
|
+
origin="opening",
|
|
1904
|
+
)
|
|
1905
|
+
row = max(rows, key=lambda r: (
|
|
1906
|
+
r["captured_epoch"] is not None, r["captured_epoch"] or 0,
|
|
1907
|
+
r["journal_id"] is not None, r["journal_id"] or "",
|
|
1908
|
+
))
|
|
1909
|
+
return OpenBlockEnd(
|
|
1910
|
+
captured_at_utc=row["captured_at_utc"],
|
|
1911
|
+
five_hour_percent=float(row["five_hour_percent"]),
|
|
1912
|
+
seven_day_percent=(
|
|
1913
|
+
None if row["weekly_percent"] is None
|
|
1914
|
+
else float(row["weekly_percent"])
|
|
1915
|
+
),
|
|
1916
|
+
origin="snapshot",
|
|
1917
|
+
)
|
|
1918
|
+
|
|
1919
|
+
|
|
1920
|
+
def project_open_block_end(
|
|
1921
|
+
conn: sqlite3.Connection, block, end: "OpenBlockEnd | None", *,
|
|
1922
|
+
ownership, journal_ctx, as_of: str,
|
|
1923
|
+
) -> None:
|
|
1924
|
+
"""Project one OPEN block's end columns and accounting from ``end``
|
|
1925
|
+
(#956 class 1, spec 4.5.1), within the caller's transaction.
|
|
1926
|
+
|
|
1927
|
+
It sets `last_observed_at_utc`, `final_five_hour_percent` and
|
|
1928
|
+
`seven_day_pct_at_block_end` from ``end`` (left as they are when ``end``
|
|
1929
|
+
is None), and recomputes the four token totals, `total_cost_usd`, both
|
|
1930
|
+
child sets, `pricing_provenance_json` and `crossed_seven_day_reset` to
|
|
1931
|
+
the end's capture, the INCLUSIVE observation cutoff. Ownership is
|
|
1932
|
+
`_lib_blocks.resolve_owning_window` over ``ownership`` (OwnedWindow
|
|
1933
|
+
values of the account's block windows, the block's own included), with
|
|
1934
|
+
the half-open ``[start, reset)`` interval. With rederive's
|
|
1935
|
+
`block_source_evidence` sink present it records the owned entries behind
|
|
1936
|
+
these totals. It never touches start columns, never refreshes a closed
|
|
1937
|
+
block (a journaled fact), and stamps `last_updated_at_utc = as_of`.
|
|
1938
|
+
|
|
1939
|
+
Cache inputs are read with ``skip_sync=True``: this runs inside a stats
|
|
1940
|
+
transaction (and inside `open_db()` on the rebuild path), and the live
|
|
1941
|
+
tick's own totals computation already synced the cache."""
|
|
1942
|
+
_c = _cctally()
|
|
1943
|
+
current = conn.execute(
|
|
1944
|
+
"SELECT * FROM five_hour_blocks WHERE id = ?", (int(block["id"]),),
|
|
1945
|
+
).fetchone()
|
|
1946
|
+
if current is None or int(current["is_closed"]) != 0:
|
|
1947
|
+
return
|
|
1948
|
+
account_key = current["account_key"]
|
|
1949
|
+
window_key = int(current["five_hour_window_key"])
|
|
1950
|
+
block_start_dt = parse_iso_datetime(
|
|
1951
|
+
current["block_start_at"], "five_hour_blocks.block_start_at",
|
|
1952
|
+
)
|
|
1953
|
+
if end is None:
|
|
1954
|
+
last_observed = current["last_observed_at_utc"]
|
|
1955
|
+
final_five_hour = current["final_five_hour_percent"]
|
|
1956
|
+
weekly_end = current["seven_day_pct_at_block_end"]
|
|
1957
|
+
else:
|
|
1958
|
+
last_observed = end.captured_at_utc
|
|
1959
|
+
final_five_hour = float(end.five_hour_percent)
|
|
1960
|
+
weekly_end = end.seven_day_percent
|
|
1961
|
+
cutoff_dt = parse_iso_datetime(
|
|
1962
|
+
last_observed, "five_hour_blocks.last_observed_at_utc",
|
|
1963
|
+
)
|
|
1964
|
+
windows = list(ownership)
|
|
1965
|
+
if not any(w.key == window_key for w in windows):
|
|
1966
|
+
windows.append(_c.OwnedWindow(
|
|
1967
|
+
key=window_key,
|
|
1968
|
+
start=block_start_dt.astimezone(dt.timezone.utc),
|
|
1969
|
+
reset=parse_iso_datetime(
|
|
1970
|
+
current["five_hour_resets_at"],
|
|
1971
|
+
"five_hour_blocks.five_hour_resets_at",
|
|
1972
|
+
).astimezone(dt.timezone.utc),
|
|
1973
|
+
))
|
|
1974
|
+
pricing_evidence: dict = {}
|
|
1975
|
+
source_sink = getattr(journal_ctx, "block_source_evidence", None)
|
|
1976
|
+
source_key = (account_key, window_key)
|
|
1977
|
+
owned_entries = (
|
|
1978
|
+
[] if source_sink is not None and source_key in source_sink else None
|
|
1979
|
+
)
|
|
1980
|
+
totals = _c._compute_block_totals(
|
|
1981
|
+
block_start_dt, cutoff_dt, skip_sync=True,
|
|
1982
|
+
owner_key=window_key, windows=windows,
|
|
1983
|
+
pricing_evidence=pricing_evidence, owned_entries=owned_entries,
|
|
1984
|
+
)
|
|
1985
|
+
if owned_entries is not None:
|
|
1986
|
+
source_sink[source_key] = tuple(owned_entries)
|
|
1987
|
+
# The reset-crossing flag over [block_start_at, end], from the same two
|
|
1988
|
+
# sources and predicates as the live sweep
|
|
1989
|
+
# (`_flag_blocks_spanning_week_resets`), computed exactly so an end that
|
|
1990
|
+
# moves backward clears it too.
|
|
1991
|
+
crossed = conn.execute(
|
|
1992
|
+
"""
|
|
1993
|
+
SELECT
|
|
1994
|
+
EXISTS (
|
|
1995
|
+
SELECT 1 FROM week_reset_events
|
|
1996
|
+
WHERE account_key = ?
|
|
1997
|
+
AND unixepoch(effective_reset_at_utc)
|
|
1998
|
+
BETWEEN unixepoch(?) AND unixepoch(?)
|
|
1999
|
+
)
|
|
2000
|
+
OR EXISTS (
|
|
2001
|
+
SELECT 1 FROM weekly_usage_snapshots
|
|
2002
|
+
WHERE week_start_at IS NOT NULL
|
|
2003
|
+
AND account_key = ?
|
|
2004
|
+
AND unixepoch(week_start_at) > unixepoch(?)
|
|
2005
|
+
AND unixepoch(week_start_at) <= unixepoch(?)
|
|
2006
|
+
)
|
|
2007
|
+
""",
|
|
2008
|
+
(account_key, current["block_start_at"], last_observed,
|
|
2009
|
+
account_key, current["block_start_at"], last_observed),
|
|
2010
|
+
).fetchone()[0]
|
|
2011
|
+
conn.execute(
|
|
2012
|
+
"""
|
|
2013
|
+
UPDATE five_hour_blocks
|
|
2014
|
+
SET last_observed_at_utc = ?,
|
|
2015
|
+
final_five_hour_percent = ?,
|
|
2016
|
+
seven_day_pct_at_block_end = ?,
|
|
2017
|
+
total_input_tokens = ?,
|
|
2018
|
+
total_output_tokens = ?,
|
|
2019
|
+
total_cache_create_tokens = ?,
|
|
2020
|
+
total_cache_read_tokens = ?,
|
|
2021
|
+
total_cost_usd = ?,
|
|
2022
|
+
pricing_provenance_json = ?,
|
|
2023
|
+
crossed_seven_day_reset = ?,
|
|
2024
|
+
last_updated_at_utc = ?
|
|
2025
|
+
WHERE id = ? AND is_closed = 0
|
|
2026
|
+
""",
|
|
2027
|
+
(
|
|
2028
|
+
last_observed, final_five_hour, weekly_end,
|
|
2029
|
+
totals["input_tokens"], totals["output_tokens"],
|
|
2030
|
+
totals["cache_create_tokens"], totals["cache_read_tokens"],
|
|
2031
|
+
totals["cost_usd"],
|
|
2032
|
+
_c._block_pricing_provenance_json(pricing_evidence),
|
|
2033
|
+
1 if crossed else 0, as_of, int(current["id"]),
|
|
2034
|
+
),
|
|
2035
|
+
)
|
|
2036
|
+
# Replace-all children, keyed on the window (not block_id) so an orphan
|
|
2037
|
+
# child of an earlier parent rebuild is cleaned up as well.
|
|
2038
|
+
block_id = int(current["id"])
|
|
2039
|
+
conn.execute(
|
|
2040
|
+
"DELETE FROM five_hour_block_models "
|
|
2041
|
+
"WHERE five_hour_window_key = ? AND account_key = ?",
|
|
2042
|
+
(window_key, account_key),
|
|
2043
|
+
)
|
|
2044
|
+
if totals.get("by_model"):
|
|
2045
|
+
conn.executemany(
|
|
2046
|
+
"""
|
|
2047
|
+
INSERT INTO five_hour_block_models (
|
|
2048
|
+
block_id, five_hour_window_key, model,
|
|
2049
|
+
input_tokens, output_tokens,
|
|
2050
|
+
cache_create_tokens, cache_read_tokens,
|
|
2051
|
+
cost_usd, entry_count, account_key
|
|
2052
|
+
)
|
|
2053
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2054
|
+
""",
|
|
2055
|
+
[
|
|
2056
|
+
(
|
|
2057
|
+
block_id, window_key, model,
|
|
2058
|
+
b["input_tokens"], b["output_tokens"],
|
|
2059
|
+
b["cache_create_tokens"], b["cache_read_tokens"],
|
|
2060
|
+
b["cost_usd"], b["entry_count"], account_key,
|
|
2061
|
+
)
|
|
2062
|
+
for model, b in totals["by_model"].items()
|
|
2063
|
+
],
|
|
2064
|
+
)
|
|
2065
|
+
conn.execute(
|
|
2066
|
+
"DELETE FROM five_hour_block_projects "
|
|
2067
|
+
"WHERE five_hour_window_key = ? AND account_key = ?",
|
|
2068
|
+
(window_key, account_key),
|
|
2069
|
+
)
|
|
2070
|
+
if totals.get("by_project"):
|
|
2071
|
+
conn.executemany(
|
|
2072
|
+
"""
|
|
2073
|
+
INSERT INTO five_hour_block_projects (
|
|
2074
|
+
block_id, five_hour_window_key, project_path,
|
|
2075
|
+
input_tokens, output_tokens,
|
|
2076
|
+
cache_create_tokens, cache_read_tokens,
|
|
2077
|
+
cost_usd, entry_count, account_key
|
|
2078
|
+
)
|
|
2079
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2080
|
+
""",
|
|
2081
|
+
[
|
|
2082
|
+
(
|
|
2083
|
+
block_id, window_key, project_path,
|
|
2084
|
+
b["input_tokens"], b["output_tokens"],
|
|
2085
|
+
b["cache_create_tokens"], b["cache_read_tokens"],
|
|
2086
|
+
b["cost_usd"], b["entry_count"], account_key,
|
|
2087
|
+
)
|
|
2088
|
+
for project_path, b in totals["by_project"].items()
|
|
2089
|
+
],
|
|
2090
|
+
)
|
|
2091
|
+
|
|
2092
|
+
|
|
2093
|
+
def _materialize_open_candidate(conn, key, acct, start_row) -> int:
|
|
2094
|
+
"""Insert one rebuild open-block candidate with its start columns from
|
|
2095
|
+
``start_row`` (the first writer, else the earliest survivor) and its end
|
|
2096
|
+
from `select_open_block_end`; zero totals and NULL provenance until
|
|
2097
|
+
`_refresh_every_open_block` projects it in the same transaction. Returns
|
|
2098
|
+
the inserted row count."""
|
|
2099
|
+
end = select_open_block_end(
|
|
2100
|
+
conn, account_key=acct, five_hour_window_key=key)
|
|
2101
|
+
if start_row is None or end is None:
|
|
2102
|
+
return 0 # defensive — should be unreachable per the keys query
|
|
2103
|
+
resets_at = start_row["five_hour_resets_at"]
|
|
2104
|
+
block_start_at = (
|
|
2105
|
+
parse_iso_datetime(resets_at, "resets_at backfill")
|
|
2106
|
+
- dt.timedelta(hours=5)
|
|
2107
|
+
).isoformat(timespec="seconds")
|
|
2108
|
+
# A rebuild's only-missing row is the trailing unjournaled projection,
|
|
2109
|
+
# never a close decision. Wall time may be past its reset, but only a
|
|
2110
|
+
# later retained observation may close and clock it (#399).
|
|
2111
|
+
cur = conn.execute(
|
|
2112
|
+
"""
|
|
2113
|
+
INSERT OR IGNORE INTO five_hour_blocks (
|
|
2114
|
+
five_hour_window_key, five_hour_resets_at, block_start_at,
|
|
2115
|
+
first_observed_at_utc, last_observed_at_utc,
|
|
2116
|
+
final_five_hour_percent,
|
|
2117
|
+
seven_day_pct_at_block_start, seven_day_pct_at_block_end,
|
|
2118
|
+
crossed_seven_day_reset,
|
|
2119
|
+
total_input_tokens, total_output_tokens,
|
|
2120
|
+
total_cache_create_tokens, total_cache_read_tokens,
|
|
2121
|
+
total_cost_usd, pricing_provenance_json, is_closed,
|
|
2122
|
+
created_at_utc, last_updated_at_utc, account_key
|
|
2123
|
+
)
|
|
2124
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, 0, 0, 0, 0, 0, NULL, 0, ?, ?, ?)
|
|
2125
|
+
""",
|
|
2126
|
+
(
|
|
2127
|
+
key, resets_at, block_start_at,
|
|
2128
|
+
start_row["captured_at_utc"], end.captured_at_utc,
|
|
2129
|
+
end.five_hour_percent,
|
|
2130
|
+
start_row["weekly_percent"], end.seven_day_percent,
|
|
2131
|
+
start_row["captured_at_utc"], end.captured_at_utc, acct,
|
|
2132
|
+
),
|
|
2133
|
+
)
|
|
2134
|
+
return cur.rowcount or 0
|
|
2135
|
+
|
|
2136
|
+
|
|
2137
|
+
def _refresh_every_open_block(conn) -> int:
|
|
2138
|
+
"""Project every open block through `select_open_block_end` /
|
|
2139
|
+
`project_open_block_end` (#956 class 1, spec 4.5.3), with each account's
|
|
2140
|
+
ownership context built from every `five_hour_blocks` row as it stands
|
|
2141
|
+
now. Runs inside the caller's transaction. Returns the blocks refreshed.
|
|
2142
|
+
A block whose window has no surviving reading keeps its end columns, and
|
|
2143
|
+
its accounting is recomputed to its stored `last_observed_at_utc`."""
|
|
2144
|
+
_c = _cctally()
|
|
2145
|
+
ownership: dict = {}
|
|
2146
|
+
for row in conn.execute(
|
|
2147
|
+
"SELECT five_hour_window_key, block_start_at, "
|
|
2148
|
+
" five_hour_resets_at, account_key FROM five_hour_blocks"
|
|
2149
|
+
).fetchall():
|
|
2150
|
+
try:
|
|
2151
|
+
w_start = parse_iso_datetime(
|
|
2152
|
+
row["block_start_at"], "five_hour_blocks.block_start_at",
|
|
2153
|
+
).astimezone(dt.timezone.utc)
|
|
2154
|
+
w_reset = parse_iso_datetime(
|
|
2155
|
+
row["five_hour_resets_at"],
|
|
2156
|
+
"five_hour_blocks.five_hour_resets_at",
|
|
2157
|
+
).astimezone(dt.timezone.utc)
|
|
2158
|
+
except ValueError:
|
|
2159
|
+
continue
|
|
2160
|
+
ownership.setdefault(row["account_key"], []).append(_c.OwnedWindow(
|
|
2161
|
+
key=int(row["five_hour_window_key"]), start=w_start,
|
|
2162
|
+
reset=w_reset,
|
|
2163
|
+
))
|
|
2164
|
+
refreshed = 0
|
|
2165
|
+
for index, block in enumerate(conn.execute(
|
|
2166
|
+
"SELECT id, account_key, five_hour_window_key, last_updated_at_utc "
|
|
2167
|
+
" FROM five_hour_blocks WHERE is_closed = 0"
|
|
2168
|
+
).fetchall(), 1):
|
|
2169
|
+
if not index & 1023:
|
|
2170
|
+
_lib_rebuild_progress.tick(blocks=index)
|
|
2171
|
+
end = select_open_block_end(
|
|
2172
|
+
conn, account_key=block["account_key"],
|
|
2173
|
+
five_hour_window_key=int(block["five_hour_window_key"]))
|
|
2174
|
+
project_open_block_end(
|
|
2175
|
+
conn, block, end,
|
|
2176
|
+
ownership=ownership.get(block["account_key"], []),
|
|
2177
|
+
journal_ctx=None,
|
|
2178
|
+
as_of=(end.captured_at_utc if end is not None
|
|
2179
|
+
else block["last_updated_at_utc"]),
|
|
2180
|
+
)
|
|
2181
|
+
refreshed += 1
|
|
2182
|
+
return refreshed
|
|
2183
|
+
|
|
2184
|
+
|
|
1782
2185
|
def _backfill_five_hour_blocks(
|
|
1783
2186
|
conn: sqlite3.Connection, *, only_missing: bool = False,
|
|
1784
2187
|
journaled_first: "dict | None" = None,
|
|
2188
|
+
retired_openings: frozenset = frozenset(),
|
|
1785
2189
|
) -> int:
|
|
1786
2190
|
"""One-shot historical backfill of five_hour_blocks from existing
|
|
1787
2191
|
weekly_usage_snapshots data. Idempotent via UNIQUE(five_hour_window_key)
|
|
@@ -1834,6 +2238,26 @@ def _backfill_five_hour_blocks(
|
|
|
1834
2238
|
ordinary (legacy, pre-journal) backfill keeps NULL: its blocks predate
|
|
1835
2239
|
provenance, which `_lib_cost_provenance.historical_close_marker` reads
|
|
1836
2240
|
as unproven.
|
|
2241
|
+
|
|
2242
|
+
#956 class 1 (spec 4.5.3): with ``only_missing`` the end columns and the
|
|
2243
|
+
accounting come from the one capture-order projection live ingest uses
|
|
2244
|
+
(`select_open_block_end` / `project_open_block_end`), not from a
|
|
2245
|
+
backfill-local end row. The pass materializes the missing candidates
|
|
2246
|
+
(start columns only, as above), then builds each account's ownership
|
|
2247
|
+
context from EVERY block row after that materialization, and refreshes
|
|
2248
|
+
EVERY open block, not only those it created: a block another path
|
|
2249
|
+
materialized is refreshed against the same window set. The candidate
|
|
2250
|
+
ranking breaks equal-second ties by `journal_id` (logical identity), never
|
|
2251
|
+
by rowid, which a journal correction can reorder.
|
|
2252
|
+
|
|
2253
|
+
#956 class 2 (spec 4.6.3): a window created by a current binary has a
|
|
2254
|
+
retained opening fact whose fold already materialized its block, so it
|
|
2255
|
+
is never a candidate here and its start columns are the seed's. The
|
|
2256
|
+
candidates are the LEGACY open blocks (created before opening facts, R4):
|
|
2257
|
+
each account's latest-captured window with no block row, except a window
|
|
2258
|
+
in ``retired_openings`` — the `(account_key, five_hour_window_key)` pairs
|
|
2259
|
+
whose opening fact a correction tombstoned (`_cctally_journal.
|
|
2260
|
+
retired_block_openings`): its block was removed, never "never seeded".
|
|
1837
2261
|
"""
|
|
1838
2262
|
_c = _cctally()
|
|
1839
2263
|
inserted = 0
|
|
@@ -1884,7 +2308,7 @@ def _backfill_five_hour_blocks(
|
|
|
1884
2308
|
" SELECT id, ROW_NUMBER() OVER ("
|
|
1885
2309
|
" PARTITION BY account_key"
|
|
1886
2310
|
" ORDER BY unixepoch(captured_at_utc) DESC,"
|
|
1887
|
-
"
|
|
2311
|
+
" journal_id DESC"
|
|
1888
2312
|
" ) AS rn"
|
|
1889
2313
|
" FROM weekly_usage_snapshots"
|
|
1890
2314
|
" WHERE five_hour_window_key IS NOT NULL"
|
|
@@ -1899,6 +2323,9 @@ def _backfill_five_hour_blocks(
|
|
|
1899
2323
|
" )"
|
|
1900
2324
|
)
|
|
1901
2325
|
keys = [(int(r[0]), r[1]) for r in conn.execute(keys_sql).fetchall()]
|
|
2326
|
+
if only_missing and retired_openings:
|
|
2327
|
+
keys = [(key, acct) for key, acct in keys
|
|
2328
|
+
if (acct, key) not in retired_openings]
|
|
1902
2329
|
|
|
1903
2330
|
now_iso = now_utc_iso()
|
|
1904
2331
|
now_dt = parse_iso_datetime(now_iso, "now")
|
|
@@ -1911,10 +2338,10 @@ def _backfill_five_hour_blocks(
|
|
|
1911
2338
|
# pair re-prices its overlap twice.
|
|
1912
2339
|
ownership: dict[Any, list] = {}
|
|
1913
2340
|
seen_keys: set = set()
|
|
1914
|
-
for row in conn.execute(
|
|
2341
|
+
for row in ([] if only_missing else conn.execute(
|
|
1915
2342
|
"SELECT five_hour_window_key, block_start_at, "
|
|
1916
2343
|
" five_hour_resets_at, account_key FROM five_hour_blocks"
|
|
1917
|
-
).fetchall():
|
|
2344
|
+
).fetchall()):
|
|
1918
2345
|
try:
|
|
1919
2346
|
w_start = parse_iso_datetime(
|
|
1920
2347
|
row["block_start_at"], "five_hour_blocks.block_start_at",
|
|
@@ -1930,7 +2357,7 @@ def _backfill_five_hour_blocks(
|
|
|
1930
2357
|
ownership.setdefault(row["account_key"], []).append(
|
|
1931
2358
|
_c.OwnedWindow(key=w_key, start=w_start, reset=w_reset)
|
|
1932
2359
|
)
|
|
1933
|
-
for key, acct in keys:
|
|
2360
|
+
for key, acct in ([] if only_missing else keys):
|
|
1934
2361
|
if (acct, key) in seen_keys:
|
|
1935
2362
|
continue
|
|
1936
2363
|
# Same start-row pick the insert loop makes below, so the
|
|
@@ -1973,6 +2400,10 @@ def _backfill_five_hour_blocks(
|
|
|
1973
2400
|
# (window_key, account_key) so a shared window's per-account
|
|
1974
2401
|
# blocks each read their own account's boundary rows (#341).
|
|
1975
2402
|
min_row = _first_row(key, acct)
|
|
2403
|
+
if only_missing:
|
|
2404
|
+
inserted += _materialize_open_candidate(
|
|
2405
|
+
conn, key, acct, min_row)
|
|
2406
|
+
continue
|
|
1976
2407
|
max_row = conn.execute(
|
|
1977
2408
|
"""
|
|
1978
2409
|
SELECT captured_at_utc, weekly_percent, five_hour_percent
|
|
@@ -2038,26 +2469,16 @@ def _backfill_five_hour_blocks(
|
|
|
2038
2469
|
# reset, but only a later retained observation may close and
|
|
2039
2470
|
# clock it (#399). The ordinary legacy backfill keeps its
|
|
2040
2471
|
# historical wall-time classification.
|
|
2041
|
-
is_closed =
|
|
2042
|
-
0 if only_missing else (1 if resets_dt < now_dt else 0)
|
|
2043
|
-
)
|
|
2044
|
-
projection_created_at = first_obs if only_missing else now_iso
|
|
2045
|
-
projection_updated_at = last_obs if only_missing else now_iso
|
|
2472
|
+
is_closed = 1 if resets_dt < now_dt else 0
|
|
2046
2473
|
|
|
2047
2474
|
# Token + cost totals — recomputed via the shared helper,
|
|
2048
2475
|
# which routes through get_entries() and falls back to
|
|
2049
2476
|
# JSONL if cache.db is unreadable.
|
|
2050
2477
|
# skip_sync=True: backfill runs inside open_db(); a sync_cache
|
|
2051
2478
|
# cascade here would reopen cache.db recursively.
|
|
2052
|
-
pricing_evidence = {} if only_missing else None
|
|
2053
2479
|
totals = _c._compute_block_totals(
|
|
2054
2480
|
block_start_dt, last_obs_dt, skip_sync=True,
|
|
2055
2481
|
owner_key=key, windows=ownership.get(acct, []),
|
|
2056
|
-
pricing_evidence=pricing_evidence,
|
|
2057
|
-
)
|
|
2058
|
-
pricing_provenance_json = (
|
|
2059
|
-
_c._block_pricing_provenance_json(pricing_evidence)
|
|
2060
|
-
if pricing_evidence is not None else None
|
|
2061
2482
|
)
|
|
2062
2483
|
|
|
2063
2484
|
cur = conn.execute(
|
|
@@ -2101,10 +2522,10 @@ def _backfill_five_hour_blocks(
|
|
|
2101
2522
|
totals["cache_create_tokens"],
|
|
2102
2523
|
totals["cache_read_tokens"],
|
|
2103
2524
|
totals["cost_usd"],
|
|
2104
|
-
|
|
2525
|
+
None,
|
|
2105
2526
|
is_closed,
|
|
2106
|
-
|
|
2107
|
-
|
|
2527
|
+
now_iso,
|
|
2528
|
+
now_iso,
|
|
2108
2529
|
acct,
|
|
2109
2530
|
),
|
|
2110
2531
|
)
|
|
@@ -2176,6 +2597,9 @@ def _backfill_five_hour_blocks(
|
|
|
2176
2597
|
],
|
|
2177
2598
|
)
|
|
2178
2599
|
|
|
2600
|
+
if only_missing:
|
|
2601
|
+
_refresh_every_open_block(conn)
|
|
2602
|
+
|
|
2179
2603
|
# Mark child-table migrations done so the upgrade-user gates in
|
|
2180
2604
|
# open_db() don't re-fire on next open. Inside the same
|
|
2181
2605
|
# transaction as the per-window inserts so partial-fail leaves
|