cctally 1.117.0 → 1.118.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 +46 -0
- package/README.md +7 -3
- package/bin/_cctally_cache.py +2783 -175
- package/bin/_cctally_completion.py +106 -33
- package/bin/_cctally_core.py +69 -4
- package/bin/_cctally_dashboard.py +227 -23
- package/bin/_cctally_dashboard_conversation.py +5 -1
- package/bin/_cctally_dashboard_envelope.py +60 -19
- package/bin/_cctally_dashboard_sources.py +23 -1
- package/bin/_cctally_db.py +193 -153
- package/bin/_cctally_diagnosis_sources.py +63 -7
- package/bin/_cctally_doctor.py +865 -93
- package/bin/_cctally_five_hour.py +124 -54
- package/bin/_cctally_forecast.py +301 -7
- package/bin/_cctally_journal.py +186 -18
- package/bin/_cctally_milestone_history.py +1193 -108
- package/bin/_cctally_oauth_samples.py +286 -15
- package/bin/_cctally_parser.py +2 -2
- package/bin/_cctally_quota.py +104 -12
- package/bin/_cctally_record.py +153 -29
- package/bin/_cctally_rederive.py +608 -68
- package/bin/_cctally_statusline.py +108 -81
- package/bin/_cctally_store.py +179 -38
- package/bin/_cctally_tui.py +1210 -170
- package/bin/_cctally_update.py +32 -13
- package/bin/_lib_codex_conversation_query.py +52 -14
- package/bin/_lib_codex_drain_admission.py +224 -6
- package/bin/_lib_codex_metadata.py +39 -0
- package/bin/_lib_conversation_anon.py +15 -8
- package/bin/_lib_conversation_dispatch.py +7 -0
- package/bin/_lib_conversation_query.py +289 -70
- package/bin/_lib_dashboard_sources.py +131 -9
- package/bin/_lib_detector_access_registry.py +497 -26
- package/bin/_lib_doctor.py +85 -20
- package/bin/_lib_doctor_scan_index.py +321 -0
- package/bin/_lib_ingest_frontier.py +20 -1
- package/bin/_lib_oauth_samples.py +23 -7
- package/bin/_lib_pricing.py +146 -16
- package/bin/_lib_quota.py +19 -0
- package/bin/_lib_quota_model.py +29 -11
- package/bin/_lib_rederive.py +11 -6
- package/bin/_lib_retained_records.py +516 -0
- package/bin/_lib_snapshot_cache.py +18 -0
- package/bin/_lib_stale_proof.py +272 -43
- package/bin/_lib_stats_content_revision.py +831 -0
- package/bin/_lib_tick_stats.py +10 -5
- package/bin/_lib_write_io.py +12 -5
- package/bin/cctally +8 -1
- package/dashboard/static/assets/ConversationsView-D4Iv4ku0.js +72 -0
- package/dashboard/static/assets/{DoctorModal-BqmXbnhn.js → DoctorModal-D4QSq6uz.js} +1 -1
- package/dashboard/static/assets/ModalRoot-C9F5dhZi.js +1 -0
- package/dashboard/static/assets/{ProjectsDrillPanel-B-HZkDnT.js → ProjectsDrillPanel-WxAdRDMt.js} +1 -1
- package/dashboard/static/assets/{SourceDetailModal-QhJkHB7i.js → SourceDetailModal-NyyEfXmc.js} +1 -1
- package/dashboard/static/assets/{UpdateModal-CkXEIQcK.js → UpdateModal-CFLq9pT1.js} +1 -1
- package/dashboard/static/assets/{index-HKS1s56o.js → index-BKTWH9JE.js} +2 -2
- package/dashboard/static/assets/{outlineNavigation-BNGaNPCp.js → outlineNavigation-BnGUwCS3.js} +3 -3
- package/dashboard/static/dashboard.html +1 -1
- package/package.json +4 -1
- package/dashboard/static/assets/ConversationsView-Dcfn8T6f.js +0 -72
- package/dashboard/static/assets/ModalRoot-Ck3WJK2V.js +0 -1
|
@@ -44,8 +44,10 @@ import sqlite3
|
|
|
44
44
|
import sys
|
|
45
45
|
from dataclasses import dataclass
|
|
46
46
|
|
|
47
|
+
import _cctally_core # `weekly_held_exclusion`: the held-column guard (#836)
|
|
47
48
|
import _lib_accounts # pure stdlib kernel; UNATTRIBUTED sentinel (#341)
|
|
48
49
|
import _lib_rebuild_progress # #997 D9 progress ticks (stdlib only)
|
|
50
|
+
import _lib_stats_content_revision # #1050 identical child-set check
|
|
49
51
|
from _lib_five_hour import latest_five_hour_reading
|
|
50
52
|
from _cctally_core import _command_as_of, eprint, now_utc_iso, open_db, parse_iso_datetime
|
|
51
53
|
|
|
@@ -1086,13 +1088,57 @@ def _credit_aware_block_weekly_axes(conn, block, *, now_utc, cache=None):
|
|
|
1086
1088
|
block.get("seven_day_pct_at_block_end"))
|
|
1087
1089
|
|
|
1088
1090
|
|
|
1091
|
+
def _weekly_retired_sql(conn, alias: str) -> "str | None":
|
|
1092
|
+
"""The suppression-retired predicate over ``alias``, or None on a store
|
|
1093
|
+
without the held column (#836, spec 5.4).
|
|
1094
|
+
|
|
1095
|
+
A held WRITE stores ``"weeklyObservationHeld": true`` in ``payload_json``
|
|
1096
|
+
beside its effective basis value (`_cctally_record`, the held branch of
|
|
1097
|
+
the snapshot write). A weekly credit's cleanup instead turns an UNHELD row,
|
|
1098
|
+
whose payload never carried that marker, into a held one and leaves its
|
|
1099
|
+
stale `weekly_percent` in place (`_delete_doomed_snapshot_rows`, replay
|
|
1100
|
+
`_cctally_journal._retire_weekly_effects`). The marker therefore separates
|
|
1101
|
+
the two exactly. A store without the column cannot hold a held row, the
|
|
1102
|
+
reason `_cctally_core.weekly_held_exclusion` omits its own predicate there.
|
|
1103
|
+
"""
|
|
1104
|
+
if not _cctally_core.weekly_held_exclusion(conn):
|
|
1105
|
+
return None
|
|
1106
|
+
return (f"({alias}.weekly_observation_held = 1 AND "
|
|
1107
|
+
f"json_extract({alias}.payload_json, '$.weeklyObservationHeld') "
|
|
1108
|
+
f"IS NOT 1)")
|
|
1109
|
+
|
|
1110
|
+
|
|
1111
|
+
def effective_weekly_value_sql(conn, alias: str = "s") -> str:
|
|
1112
|
+
"""The joined snapshot's EFFECTIVE weekly value (#836, spec 5.4): NULL for
|
|
1113
|
+
a suppression-retired row, its stored `weekly_percent` otherwise. A
|
|
1114
|
+
clamped held write and a genuine climb-back keep their values; no
|
|
1115
|
+
percentage band is involved."""
|
|
1116
|
+
retired = _weekly_retired_sql(conn, alias)
|
|
1117
|
+
if retired is None:
|
|
1118
|
+
return f"{alias}.weekly_percent"
|
|
1119
|
+
return f"CASE WHEN {retired} THEN NULL ELSE {alias}.weekly_percent END"
|
|
1120
|
+
|
|
1121
|
+
|
|
1122
|
+
def weekly_join_unavailable_sql(conn, alias: str = "s") -> str:
|
|
1123
|
+
"""A five-hour milestone's effective weekly join is UNAVAILABLE (#836,
|
|
1124
|
+
spec 5.4): the joined snapshot is absent — deleted, or the ``0`` sentinel
|
|
1125
|
+
`trg_usage_snapshot_suppression` writes — or suppression-retired. The
|
|
1126
|
+
shared predicate of the detail stamp and the dashboard's retired-join
|
|
1127
|
+
signature leg, over a ``LEFT JOIN`` aliased ``alias``."""
|
|
1128
|
+
retired = _weekly_retired_sql(conn, alias)
|
|
1129
|
+
if retired is None:
|
|
1130
|
+
return f"({alias}.id IS NULL)"
|
|
1131
|
+
return f"({alias}.id IS NULL OR {retired})"
|
|
1132
|
+
|
|
1133
|
+
|
|
1089
1134
|
#: The six `five_hour_milestones` columns every surface already published, plus
|
|
1090
|
-
#: the joined EFFECTIVE weekly value
|
|
1091
|
-
#:
|
|
1135
|
+
#: the joined EFFECTIVE weekly value (`effective_weekly_value_sql`, filled in
|
|
1136
|
+
#: as ``{effective}``). Ordering is `captured_at_utc, id` — spec §5.2's
|
|
1137
|
+
#: merged-stream order, which post-credit threshold repeats depend on.
|
|
1092
1138
|
_FIVE_HOUR_MILESTONE_SQL = """
|
|
1093
1139
|
SELECT m.percent_threshold, m.captured_at_utc, m.block_cost_usd,
|
|
1094
1140
|
m.marginal_cost_usd, m.seven_day_pct_at_crossing, m.reset_event_id,
|
|
1095
|
-
|
|
1141
|
+
{effective} AS effective_seven_day_pct_at_crossing
|
|
1096
1142
|
FROM five_hour_milestones m
|
|
1097
1143
|
LEFT JOIN weekly_usage_snapshots s ON s.id = m.usage_snapshot_id
|
|
1098
1144
|
WHERE {selector}
|
|
@@ -1139,6 +1185,12 @@ def _load_five_hour_milestones(
|
|
|
1139
1185
|
store is corrected by `db rederive`'s reviewed `retract`. A missing joined
|
|
1140
1186
|
snapshot still publishes None and never falls back to the raw value.
|
|
1141
1187
|
|
|
1188
|
+
**A suppression-retired snapshot publishes None** (#836, spec 5.4). The
|
|
1189
|
+
fold rejected its weekly value, and the cleanup kept the row only for its
|
|
1190
|
+
five-hour evidence; its provenance is exact (no held-write marker on a
|
|
1191
|
+
held row), so this is not the withdrawn band. See
|
|
1192
|
+
`effective_weekly_value_sql`.
|
|
1193
|
+
|
|
1142
1194
|
SELECTOR. Exactly one of `block_id` or `five_hour_window_key` must be given.
|
|
1143
1195
|
`block_id` is the precise form — `five_hour_milestones.block_id` names one
|
|
1144
1196
|
block outright. The pair form filters on the window key AND the account,
|
|
@@ -1166,7 +1218,9 @@ def _load_five_hour_milestones(
|
|
|
1166
1218
|
selector += " AND m.account_key = ?"
|
|
1167
1219
|
params += (account_key,)
|
|
1168
1220
|
rows = conn.execute(
|
|
1169
|
-
_FIVE_HOUR_MILESTONE_SQL.format(
|
|
1221
|
+
_FIVE_HOUR_MILESTONE_SQL.format(
|
|
1222
|
+
selector=selector,
|
|
1223
|
+
effective=effective_weekly_value_sql(conn, "s")), params,
|
|
1170
1224
|
).fetchall()
|
|
1171
1225
|
out: list[dict] = []
|
|
1172
1226
|
for r in rows:
|
|
@@ -1981,60 +2035,76 @@ def project_open_block_end(
|
|
|
1981
2035
|
),
|
|
1982
2036
|
)
|
|
1983
2037
|
# Replace-all children, keyed on the window (not block_id) so an orphan
|
|
1984
|
-
# child of an earlier parent rebuild is cleaned up as well.
|
|
2038
|
+
# child of an earlier parent rebuild is cleaned up as well. An identical
|
|
2039
|
+
# stored set is left in place (#1050): a DELETE and re-INSERT of the same
|
|
2040
|
+
# rows would advance the content revision with nothing changed.
|
|
1985
2041
|
block_id = int(current["id"])
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
""
|
|
1994
|
-
|
|
1995
|
-
block_id, five_hour_window_key, model,
|
|
1996
|
-
input_tokens, output_tokens,
|
|
1997
|
-
cache_create_tokens, cache_read_tokens,
|
|
1998
|
-
cost_usd, entry_count, account_key
|
|
1999
|
-
)
|
|
2000
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2001
|
-
""",
|
|
2002
|
-
[
|
|
2003
|
-
(
|
|
2004
|
-
block_id, window_key, model,
|
|
2005
|
-
b["input_tokens"], b["output_tokens"],
|
|
2006
|
-
b["cache_create_tokens"], b["cache_read_tokens"],
|
|
2007
|
-
b["cost_usd"], b["entry_count"], account_key,
|
|
2008
|
-
)
|
|
2009
|
-
for model, b in totals["by_model"].items()
|
|
2010
|
-
],
|
|
2042
|
+
model_rows = _open_block_child_rows(
|
|
2043
|
+
block_id, window_key, account_key, "model", totals.get("by_model"))
|
|
2044
|
+
if not _lib_stats_content_revision.child_set_unchanged(
|
|
2045
|
+
conn, "five_hour_block_models", five_hour_window_key=window_key,
|
|
2046
|
+
account_key=account_key, replacement=model_rows):
|
|
2047
|
+
conn.execute(
|
|
2048
|
+
"DELETE FROM five_hour_block_models "
|
|
2049
|
+
"WHERE five_hour_window_key = ? AND account_key = ?",
|
|
2050
|
+
(window_key, account_key),
|
|
2011
2051
|
)
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
INSERT INTO five_hour_block_projects (
|
|
2021
|
-
block_id, five_hour_window_key, project_path,
|
|
2022
|
-
input_tokens, output_tokens,
|
|
2023
|
-
cache_create_tokens, cache_read_tokens,
|
|
2024
|
-
cost_usd, entry_count, account_key
|
|
2025
|
-
)
|
|
2026
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2027
|
-
""",
|
|
2028
|
-
[
|
|
2029
|
-
(
|
|
2030
|
-
block_id, window_key, project_path,
|
|
2031
|
-
b["input_tokens"], b["output_tokens"],
|
|
2032
|
-
b["cache_create_tokens"], b["cache_read_tokens"],
|
|
2033
|
-
b["cost_usd"], b["entry_count"], account_key,
|
|
2052
|
+
if model_rows:
|
|
2053
|
+
conn.executemany(
|
|
2054
|
+
"""
|
|
2055
|
+
INSERT INTO five_hour_block_models (
|
|
2056
|
+
block_id, five_hour_window_key, model,
|
|
2057
|
+
input_tokens, output_tokens,
|
|
2058
|
+
cache_create_tokens, cache_read_tokens,
|
|
2059
|
+
cost_usd, entry_count, account_key
|
|
2034
2060
|
)
|
|
2035
|
-
|
|
2036
|
-
|
|
2061
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2062
|
+
""",
|
|
2063
|
+
[tuple(row.values()) for row in model_rows],
|
|
2064
|
+
)
|
|
2065
|
+
project_rows = _open_block_child_rows(
|
|
2066
|
+
block_id, window_key, account_key, "project_path",
|
|
2067
|
+
totals.get("by_project"))
|
|
2068
|
+
if not _lib_stats_content_revision.child_set_unchanged(
|
|
2069
|
+
conn, "five_hour_block_projects", five_hour_window_key=window_key,
|
|
2070
|
+
account_key=account_key, replacement=project_rows):
|
|
2071
|
+
conn.execute(
|
|
2072
|
+
"DELETE FROM five_hour_block_projects "
|
|
2073
|
+
"WHERE five_hour_window_key = ? AND account_key = ?",
|
|
2074
|
+
(window_key, account_key),
|
|
2037
2075
|
)
|
|
2076
|
+
if project_rows:
|
|
2077
|
+
conn.executemany(
|
|
2078
|
+
"""
|
|
2079
|
+
INSERT INTO five_hour_block_projects (
|
|
2080
|
+
block_id, five_hour_window_key, project_path,
|
|
2081
|
+
input_tokens, output_tokens,
|
|
2082
|
+
cache_create_tokens, cache_read_tokens,
|
|
2083
|
+
cost_usd, entry_count, account_key
|
|
2084
|
+
)
|
|
2085
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2086
|
+
""",
|
|
2087
|
+
[tuple(row.values()) for row in project_rows],
|
|
2088
|
+
)
|
|
2089
|
+
|
|
2090
|
+
|
|
2091
|
+
def _open_block_child_rows(block_id, window_key, account_key, label,
|
|
2092
|
+
breakdown) -> "list[dict]":
|
|
2093
|
+
"""One open block's child rows from a `_compute_block_totals` breakdown,
|
|
2094
|
+
as column → value mappings in the INSERT's column order."""
|
|
2095
|
+
return [
|
|
2096
|
+
{
|
|
2097
|
+
"block_id": block_id, "five_hour_window_key": window_key,
|
|
2098
|
+
label: name,
|
|
2099
|
+
"input_tokens": b["input_tokens"],
|
|
2100
|
+
"output_tokens": b["output_tokens"],
|
|
2101
|
+
"cache_create_tokens": b["cache_create_tokens"],
|
|
2102
|
+
"cache_read_tokens": b["cache_read_tokens"],
|
|
2103
|
+
"cost_usd": b["cost_usd"], "entry_count": b["entry_count"],
|
|
2104
|
+
"account_key": account_key,
|
|
2105
|
+
}
|
|
2106
|
+
for name, b in (breakdown or {}).items()
|
|
2107
|
+
]
|
|
2038
2108
|
|
|
2039
2109
|
|
|
2040
2110
|
def _materialize_open_candidate(conn, key, acct, start_row) -> int:
|
package/bin/_cctally_forecast.py
CHANGED
|
@@ -52,6 +52,12 @@ from _cctally_core import (
|
|
|
52
52
|
_BudgetConfigError, _get_budget_config,
|
|
53
53
|
BUDGET_PERIODS as _CCTALLY_BUDGET_PERIODS,
|
|
54
54
|
)
|
|
55
|
+
# #836: the decorated Current Week's owner and cycle identity read the reset
|
|
56
|
+
# chokepoint and the held-column guard directly; neither is ns-patched.
|
|
57
|
+
from _cctally_core import (
|
|
58
|
+
_canonicalize_optional_iso, _latest_reset_event_for_end,
|
|
59
|
+
weekly_held_exclusion,
|
|
60
|
+
)
|
|
55
61
|
|
|
56
62
|
import importlib.util as _ilu
|
|
57
63
|
|
|
@@ -129,15 +135,19 @@ def _resolve_forecast_now(as_of: str | None) -> dt.datetime:
|
|
|
129
135
|
|
|
130
136
|
|
|
131
137
|
def _shape_week_samples(rows, *, include_held: bool,
|
|
132
|
-
include_account: bool = False
|
|
138
|
+
include_account: bool = False,
|
|
139
|
+
include_journal_id: bool = False) -> list:
|
|
133
140
|
"""Turn snapshot rows into the sample tuples `_fetch_current_week_snapshots`
|
|
134
141
|
returns. Both of that function's row-fetch legs go through here so the two
|
|
135
142
|
cannot drift on the tuple width.
|
|
136
143
|
|
|
137
144
|
`rows` are `(captured_at_utc, weekly_percent, five_hour_percent,
|
|
138
|
-
weekly_observation_held[, account_key])`. The held flag is
|
|
139
|
-
for a held-inclusive read,
|
|
140
|
-
read
|
|
145
|
+
weekly_observation_held[, account_key][, journal_id])`. The held flag is
|
|
146
|
+
appended only for a held-inclusive read, the account only for an explicit
|
|
147
|
+
identity read, and the row's `journal_id` (#1018 twin spec 5.3: the
|
|
148
|
+
displayed row's originating observation) only when asked for, after
|
|
149
|
+
every other field. Existing consumers keep the historical three- or
|
|
150
|
+
four-field shape.
|
|
141
151
|
"""
|
|
142
152
|
out = []
|
|
143
153
|
for row in rows:
|
|
@@ -150,6 +160,8 @@ def _shape_week_samples(rows, *, include_held: bool,
|
|
|
150
160
|
sample = sample + (int(row[3] or 0),)
|
|
151
161
|
if include_account:
|
|
152
162
|
sample = sample + (row[4],)
|
|
163
|
+
if include_journal_id:
|
|
164
|
+
sample = sample + (row[5 if include_account else 4],)
|
|
153
165
|
out.append(sample)
|
|
154
166
|
return out
|
|
155
167
|
|
|
@@ -177,7 +189,8 @@ def _fetch_current_week_snapshots(conn: sqlite3.Connection, now_utc: dt.datetime
|
|
|
177
189
|
*, account_key: "str | None" = None,
|
|
178
190
|
include_held: bool = False,
|
|
179
191
|
include_account: bool = False,
|
|
180
|
-
include_scope: bool = False
|
|
192
|
+
include_scope: bool = False,
|
|
193
|
+
include_journal_id: bool = False):
|
|
181
194
|
"""Return (week_start_at, week_end_at, list[(captured_at, percent, five_hr)])
|
|
182
195
|
for the subscription week containing `now_utc`, or None if no snapshot
|
|
183
196
|
exists for the current week.
|
|
@@ -235,6 +248,12 @@ def _fetch_current_week_snapshots(conn: sqlite3.Connection, now_utc: dt.datetime
|
|
|
235
248
|
reading the five-hour axis selects its reading under exactly this scope
|
|
236
249
|
rather than from the newest sample, which a retired replica or a
|
|
237
250
|
weekly-only reading leaves without a five-hour value.
|
|
251
|
+
|
|
252
|
+
``include_journal_id`` (#1018 twin spec 5.3) appends each row's
|
|
253
|
+
`journal_id` as the LAST sample field, after the held flag and the
|
|
254
|
+
account, so the Current Week card can resolve its displayed row's
|
|
255
|
+
originating observation. Every other caller keeps its tuple width, and
|
|
256
|
+
the trailing SQL row id ordering is unchanged.
|
|
238
257
|
"""
|
|
239
258
|
# #341: optional account scoping — same predicate on every leg so the whole
|
|
240
259
|
# window resolution stays consistent (a real key or the `unattributed`
|
|
@@ -248,6 +267,8 @@ def _fetch_current_week_snapshots(conn: sqlite3.Connection, now_utc: dt.datetime
|
|
|
248
267
|
# The TUI's multi-account card needs the physical owner of the sample it
|
|
249
268
|
# actually displays. Keep the historical sample shape for every other read.
|
|
250
269
|
_owner_column = ", account_key" if include_account else ""
|
|
270
|
+
if include_journal_id:
|
|
271
|
+
_owner_column += ", journal_id"
|
|
251
272
|
_sample_order = " ASC, id ASC" if include_account else " ASC"
|
|
252
273
|
candidates = conn.execute(
|
|
253
274
|
"SELECT week_start_at, week_end_at, week_start_date, MAX(captured_at_utc) AS latest_cap "
|
|
@@ -304,7 +325,8 @@ def _fetch_current_week_snapshots(conn: sqlite3.Connection, now_utc: dt.datetime
|
|
|
304
325
|
(drow[0],) + _acct_p,
|
|
305
326
|
).fetchall()
|
|
306
327
|
samples = _shape_week_samples(rows, include_held=include_held,
|
|
307
|
-
include_account=include_account
|
|
328
|
+
include_account=include_account,
|
|
329
|
+
include_journal_id=include_journal_id)
|
|
308
330
|
samples = [s for s in samples if s[0] <= now_utc]
|
|
309
331
|
if include_scope:
|
|
310
332
|
return week_start_at, week_end_at, samples, (drow[0], None)
|
|
@@ -337,7 +359,8 @@ def _fetch_current_week_snapshots(conn: sqlite3.Connection, now_utc: dt.datetime
|
|
|
337
359
|
tuple(matching_texts) + (chosen_date,) + _acct_p,
|
|
338
360
|
), by_id=include_account)
|
|
339
361
|
samples = _shape_week_samples(rows, include_held=include_held,
|
|
340
|
-
include_account=include_account
|
|
362
|
+
include_account=include_account,
|
|
363
|
+
include_journal_id=include_journal_id)
|
|
341
364
|
samples = [s for s in samples if s[0] <= now_utc]
|
|
342
365
|
if include_scope:
|
|
343
366
|
return (week_start_at, week_end_at, samples,
|
|
@@ -389,6 +412,277 @@ def _apply_midweek_reset_override(
|
|
|
389
412
|
return week_start_at, samples
|
|
390
413
|
|
|
391
414
|
|
|
415
|
+
# === #836: the decorated Current Week — one owner, one cycle identity =======
|
|
416
|
+
#
|
|
417
|
+
# On a store with more than one REAL Claude account (`provider_is_decorated`)
|
|
418
|
+
# the Current Week card, its weekly milestones and the history index's current
|
|
419
|
+
# entry describe ONE account: the owner of the newest non-held weekly sample at
|
|
420
|
+
# the build's as-of instant (#839's contract, spec 5.2). The merged reads above
|
|
421
|
+
# stay unchanged for every undecorated store and every other caller.
|
|
422
|
+
|
|
423
|
+
#: Every subscription window that can contain an instant starts no more than
|
|
424
|
+
#: seven days (plus host-offset slack on `week_start_date`) before it, so this
|
|
425
|
+
#: leading `week_start_date` bound keeps every candidate read an
|
|
426
|
+
#: `idx_usage_week_time` range rather than a scan of the whole history.
|
|
427
|
+
_CLAUDE_CYCLE_CANDIDATE_DAYS = 8
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
@dataclass(frozen=True)
|
|
431
|
+
class ClaudeCycleScope:
|
|
432
|
+
"""The decorated Current Week's cycle identity (#836, spec 5.2 step 2).
|
|
433
|
+
|
|
434
|
+
Resolved once per build by `_claude_current_cycle_scope` and shared by the
|
|
435
|
+
card, the weekly milestones and the scoped history index, so the three
|
|
436
|
+
readers of one cycle cannot choose it differently.
|
|
437
|
+
|
|
438
|
+
``account_key``: the owner. ``as_of_utc``: the build instant (ISO-8601
|
|
439
|
+
``Z``); reset events are admitted only at or before it.
|
|
440
|
+
``week_start_date``: the storage key milestones are recorded under.
|
|
441
|
+
``week_start_at``: the owner's stored boundary text (``None`` on the
|
|
442
|
+
date-only leg). ``week_end_at``: the canonical end
|
|
443
|
+
(`_canonicalize_optional_iso`, the `week_reset_events.new_week_end_at`
|
|
444
|
+
form; ``None`` on the date-only leg). ``effective_start_at``: the week
|
|
445
|
+
start, or the active reset's effective instant when later (ISO-8601 ``Z``;
|
|
446
|
+
``None`` on the date-only leg). ``reset_event_id``: the active reset's id
|
|
447
|
+
(the milestone segment), ``0`` when none.
|
|
448
|
+
"""
|
|
449
|
+
account_key: str
|
|
450
|
+
as_of_utc: str
|
|
451
|
+
week_start_date: str
|
|
452
|
+
week_start_at: "str | None"
|
|
453
|
+
week_end_at: "str | None"
|
|
454
|
+
effective_start_at: "str | None"
|
|
455
|
+
reset_event_id: int
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _claude_newest_weekly_sample(conn, now_utc: dt.datetime, *,
|
|
459
|
+
account_key: "str | None" = None):
|
|
460
|
+
"""The newest non-held weekly sample captured at or before ``now_utc``
|
|
461
|
+
among windows containing it, or ``None`` (#836, spec 5.2 step 1).
|
|
462
|
+
|
|
463
|
+
Only non-held rows take part, so a newer held row of another account (a
|
|
464
|
+
copy of an older weekly value under a newer capture) never chooses the
|
|
465
|
+
window — the defect of the held-inclusive ``MAX(captured_at_utc)`` window
|
|
466
|
+
choice in `_fetch_current_week_snapshots` (spec D-C). Captures after
|
|
467
|
+
``now_utc`` never participate. Boundaryful windows
|
|
468
|
+
(``[week_start_at, week_end_at)`` containing ``now_utc``) are preferred;
|
|
469
|
+
when none qualifies, the date-only leg (``week_start_date`` ..
|
|
470
|
+
``week_end_date`` containing today's host-local date) applies, as in
|
|
471
|
+
`_fetch_current_week_snapshots`. Ties break by ``(captured_at_utc, id)``.
|
|
472
|
+
|
|
473
|
+
Returns a dict of the row's identity and boundary columns plus
|
|
474
|
+
``boundaryful``: whether the CHOSEN row carries both parseable bounds,
|
|
475
|
+
never which pool it came from. On the window's end date, after its reset
|
|
476
|
+
instant and before any next-window sample, a bounded row reaches the
|
|
477
|
+
date-only pool; its scope is still built from its own bounds, so the
|
|
478
|
+
account's own reset stays in effect (#836 review F1).
|
|
479
|
+
"""
|
|
480
|
+
floor_date = (now_utc.astimezone(dt.timezone.utc).date()
|
|
481
|
+
- dt.timedelta(days=_CLAUDE_CYCLE_CANDIDATE_DAYS)).isoformat()
|
|
482
|
+
acct_pred = "" if account_key is None else " AND account_key = ?"
|
|
483
|
+
acct_p: tuple = () if account_key is None else (account_key,)
|
|
484
|
+
rows = conn.execute(
|
|
485
|
+
"SELECT id, account_key, week_start_date, week_end_date, "
|
|
486
|
+
" week_start_at, week_end_at, captured_at_utc "
|
|
487
|
+
"FROM weekly_usage_snapshots "
|
|
488
|
+
"WHERE week_start_date >= ?" + acct_pred
|
|
489
|
+
+ weekly_held_exclusion(conn),
|
|
490
|
+
(floor_date,) + acct_p,
|
|
491
|
+
).fetchall()
|
|
492
|
+
# internal fallback: host-local intentional (the date-only leg's day,
|
|
493
|
+
# matching `_fetch_current_week_snapshots`)
|
|
494
|
+
today_local = now_utc.astimezone().date().isoformat()
|
|
495
|
+
boundaryful = []
|
|
496
|
+
date_only = []
|
|
497
|
+
for r in rows:
|
|
498
|
+
try:
|
|
499
|
+
cap = parse_iso_datetime(r[6], "captured_at_utc")
|
|
500
|
+
except (TypeError, ValueError):
|
|
501
|
+
continue
|
|
502
|
+
if cap > now_utc:
|
|
503
|
+
continue
|
|
504
|
+
rank = (cap, int(r[0]))
|
|
505
|
+
try:
|
|
506
|
+
ws = parse_iso_datetime(r[4], "week_start_at") if r[4] else None
|
|
507
|
+
we = parse_iso_datetime(r[5], "week_end_at") if r[5] else None
|
|
508
|
+
except ValueError:
|
|
509
|
+
ws = we = None
|
|
510
|
+
bounded = ws is not None and we is not None
|
|
511
|
+
if bounded and ws <= now_utc < we:
|
|
512
|
+
boundaryful.append((rank, r, True))
|
|
513
|
+
elif (r[2] is not None and r[3] is not None
|
|
514
|
+
and r[2] <= today_local <= r[3]):
|
|
515
|
+
date_only.append((rank, r, bounded))
|
|
516
|
+
pool = boundaryful or date_only
|
|
517
|
+
if not pool:
|
|
518
|
+
return None
|
|
519
|
+
_rank, r, bounded = max(pool, key=lambda item: item[0])
|
|
520
|
+
return {
|
|
521
|
+
"id": int(r[0]),
|
|
522
|
+
"account_key": r[1],
|
|
523
|
+
"week_start_date": r[2],
|
|
524
|
+
"week_end_date": r[3],
|
|
525
|
+
"week_start_at": r[4],
|
|
526
|
+
"week_end_at": r[5],
|
|
527
|
+
"captured_at_utc": r[6],
|
|
528
|
+
"boundaryful": bounded,
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def _claude_select_current_owner(conn, now_utc: dt.datetime) -> "str | None":
|
|
533
|
+
"""The displayed account: the owner of the newest non-held weekly sample
|
|
534
|
+
at ``now_utc`` (#836, spec 5.2 step 1), or ``None`` when there is none."""
|
|
535
|
+
sample = _claude_newest_weekly_sample(conn, now_utc)
|
|
536
|
+
return None if sample is None else sample["account_key"]
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def _claude_current_cycle_scope(conn, now_utc: dt.datetime, *,
|
|
540
|
+
account_key: str) -> "ClaudeCycleScope | None":
|
|
541
|
+
"""``account_key``'s current cycle identity at ``now_utc`` (#836, spec 5.2
|
|
542
|
+
step 2), or ``None`` when the account has no eligible non-held sample.
|
|
543
|
+
|
|
544
|
+
The window is the one holding the account's newest eligible non-held
|
|
545
|
+
sample. The active reset is `_latest_reset_event_for_end` for the
|
|
546
|
+
canonical end, scoped to the account and admitted only at or before
|
|
547
|
+
``now_utc`` — the #750 S3 chokepoint's effective-instant order — so another
|
|
548
|
+
account's credit never re-anchors this cycle (spec D-A) and a reset that
|
|
549
|
+
takes effect later does not cut it yet. The effective start is that
|
|
550
|
+
reset's instant exactly when the history index cuts the account's storage
|
|
551
|
+
bucket there (`_claude_reset_cuts_cycle`, the one rule both use) — which
|
|
552
|
+
can be EARLIER than the nominal week start after a same-date early
|
|
553
|
+
re-anchor (#836 review F7). The date-only leg has no end text, so it
|
|
554
|
+
carries no reset.
|
|
555
|
+
"""
|
|
556
|
+
sample = _claude_newest_weekly_sample(conn, now_utc, account_key=account_key)
|
|
557
|
+
if sample is None:
|
|
558
|
+
return None
|
|
559
|
+
as_of = _iso_z(now_utc)
|
|
560
|
+
if not sample["boundaryful"]:
|
|
561
|
+
return ClaudeCycleScope(
|
|
562
|
+
account_key=account_key, as_of_utc=as_of,
|
|
563
|
+
week_start_date=sample["week_start_date"],
|
|
564
|
+
week_start_at=None, week_end_at=None, effective_start_at=None,
|
|
565
|
+
reset_event_id=0)
|
|
566
|
+
week_start = parse_iso_datetime(sample["week_start_at"], "week_start_at")
|
|
567
|
+
canon_end = _canonicalize_optional_iso(sample["week_end_at"],
|
|
568
|
+
"claude.cycle.end")
|
|
569
|
+
effective_start = week_start
|
|
570
|
+
reset_event_id = 0
|
|
571
|
+
if canon_end:
|
|
572
|
+
event = _latest_reset_event_for_end(
|
|
573
|
+
conn, canon_end, account_key=account_key, as_of_utc=as_of)
|
|
574
|
+
if event is not None:
|
|
575
|
+
reset_event_id = int(event["id"])
|
|
576
|
+
if event["effective_reset_at_utc"]:
|
|
577
|
+
try:
|
|
578
|
+
reset_dt = parse_iso_datetime(
|
|
579
|
+
event["effective_reset_at_utc"], "reset_event.effective")
|
|
580
|
+
except ValueError:
|
|
581
|
+
reset_dt = None
|
|
582
|
+
if reset_dt is not None:
|
|
583
|
+
import _cctally_milestone_history as _mhist
|
|
584
|
+
outer = _mhist._claude_storage_boundaries(
|
|
585
|
+
conn, sample["week_start_date"],
|
|
586
|
+
account_key=account_key)
|
|
587
|
+
if _mhist._claude_reset_cuts_cycle(outer, reset_dt):
|
|
588
|
+
effective_start = reset_dt
|
|
589
|
+
return ClaudeCycleScope(
|
|
590
|
+
account_key=account_key, as_of_utc=as_of,
|
|
591
|
+
week_start_date=sample["week_start_date"],
|
|
592
|
+
week_start_at=sample["week_start_at"],
|
|
593
|
+
week_end_at=canon_end,
|
|
594
|
+
effective_start_at=_iso_z(effective_start),
|
|
595
|
+
reset_event_id=reset_event_id)
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def _claude_cycle_scope_samples(conn, scope: ClaudeCycleScope,
|
|
599
|
+
now_utc: dt.datetime, *,
|
|
600
|
+
include_journal_id: bool = False):
|
|
601
|
+
"""The decorated card's held-inclusive samples for ``scope`` (#836).
|
|
602
|
+
|
|
603
|
+
The shape of ``_fetch_current_week_snapshots(include_held=True,
|
|
604
|
+
include_account=True, include_scope=True)`` — ``(week_start_at,
|
|
605
|
+
week_end_at, samples, week_scope)`` — restricted to the scope's account and
|
|
606
|
+
window, so the card reads the window its owner was chosen in rather than
|
|
607
|
+
one `_fetch_current_week_snapshots` would choose by its held-inclusive
|
|
608
|
+
``MAX(captured_at_utc)``. ``week_start_at`` is the NOMINAL start; the
|
|
609
|
+
caller filters from ``scope.effective_start_at``. Samples are filtered to
|
|
610
|
+
``captured_at <= now_utc``. Returns ``None`` when the window cannot be
|
|
611
|
+
resolved. ``include_journal_id`` appends each row's `journal_id` as the
|
|
612
|
+
last sample field (#1018 twin spec 5.3).
|
|
613
|
+
"""
|
|
614
|
+
_jid = ", journal_id" if include_journal_id else ""
|
|
615
|
+
floor_date = (now_utc.astimezone(dt.timezone.utc).date()
|
|
616
|
+
- dt.timedelta(days=_CLAUDE_CYCLE_CANDIDATE_DAYS)).isoformat()
|
|
617
|
+
if scope.week_start_at is None:
|
|
618
|
+
drow = conn.execute(
|
|
619
|
+
"SELECT week_end_date FROM weekly_usage_snapshots "
|
|
620
|
+
"WHERE week_start_date = ? AND account_key = ? "
|
|
621
|
+
"AND week_end_date IS NOT NULL "
|
|
622
|
+
"ORDER BY captured_at_utc DESC, id DESC LIMIT 1",
|
|
623
|
+
(scope.week_start_date, scope.account_key),
|
|
624
|
+
).fetchone()
|
|
625
|
+
if drow is None:
|
|
626
|
+
return None
|
|
627
|
+
# internal fallback: host-local intentional (the date-only leg)
|
|
628
|
+
local_tz = dt.datetime.now().astimezone().tzinfo
|
|
629
|
+
ws_date = dt.date.fromisoformat(scope.week_start_date)
|
|
630
|
+
we_date = dt.date.fromisoformat(drow[0])
|
|
631
|
+
week_start_at = dt.datetime.combine(
|
|
632
|
+
ws_date, dt.time(0, 0), local_tz).astimezone(dt.timezone.utc)
|
|
633
|
+
week_end_at = dt.datetime.combine(
|
|
634
|
+
we_date + dt.timedelta(days=1), dt.time(0, 0),
|
|
635
|
+
local_tz).astimezone(dt.timezone.utc)
|
|
636
|
+
rows = conn.execute(
|
|
637
|
+
"SELECT captured_at_utc, weekly_percent, five_hour_percent, "
|
|
638
|
+
" weekly_observation_held, account_key" + _jid + " "
|
|
639
|
+
"FROM weekly_usage_snapshots "
|
|
640
|
+
"WHERE week_start_date = ? AND account_key = ? "
|
|
641
|
+
"ORDER BY captured_at_utc ASC, id ASC",
|
|
642
|
+
(scope.week_start_date, scope.account_key),
|
|
643
|
+
).fetchall()
|
|
644
|
+
samples = _shape_week_samples(rows, include_held=True,
|
|
645
|
+
include_account=True,
|
|
646
|
+
include_journal_id=include_journal_id)
|
|
647
|
+
samples = [s for s in samples if s[0] <= now_utc]
|
|
648
|
+
return (week_start_at, week_end_at, samples,
|
|
649
|
+
(scope.week_start_date, None))
|
|
650
|
+
week_start_at = parse_iso_datetime(scope.week_start_at, "week_start_at")
|
|
651
|
+
week_end_at = parse_iso_datetime(scope.week_end_at, "week_end_at")
|
|
652
|
+
# Every textual spelling of the same start instant, as the merged read
|
|
653
|
+
# gathers them for an upgrade-in-progress window.
|
|
654
|
+
matching_texts: list[str] = []
|
|
655
|
+
for (text,) in conn.execute(
|
|
656
|
+
"SELECT DISTINCT week_start_at FROM weekly_usage_snapshots "
|
|
657
|
+
"WHERE week_start_date >= ? AND account_key = ? "
|
|
658
|
+
"AND week_start_at IS NOT NULL AND week_end_at IS NOT NULL",
|
|
659
|
+
(floor_date, scope.account_key)).fetchall():
|
|
660
|
+
try:
|
|
661
|
+
if parse_iso_datetime(text, "week_start_at") == week_start_at:
|
|
662
|
+
matching_texts.append(text)
|
|
663
|
+
except ValueError:
|
|
664
|
+
continue
|
|
665
|
+
if scope.week_start_at not in matching_texts:
|
|
666
|
+
matching_texts.append(scope.week_start_at)
|
|
667
|
+
matching_texts.sort()
|
|
668
|
+
placeholders = ",".join("?" * len(matching_texts))
|
|
669
|
+
rows = _week_rows_by_capture(conn.execute(
|
|
670
|
+
f"SELECT captured_at_utc, weekly_percent, five_hour_percent, "
|
|
671
|
+
f" weekly_observation_held, account_key{_jid}, id "
|
|
672
|
+
f"FROM weekly_usage_snapshots "
|
|
673
|
+
f"WHERE (week_start_at IN ({placeholders}) "
|
|
674
|
+
f" OR (week_start_at IS NULL AND week_start_date = ?)) "
|
|
675
|
+
f" AND account_key = ?",
|
|
676
|
+
tuple(matching_texts) + (scope.week_start_date, scope.account_key),
|
|
677
|
+
), by_id=True)
|
|
678
|
+
samples = _shape_week_samples(rows, include_held=True,
|
|
679
|
+
include_account=True,
|
|
680
|
+
include_journal_id=include_journal_id)
|
|
681
|
+
samples = [s for s in samples if s[0] <= now_utc]
|
|
682
|
+
return (week_start_at, week_end_at, samples,
|
|
683
|
+
(scope.week_start_date, tuple(matching_texts)))
|
|
684
|
+
|
|
685
|
+
|
|
392
686
|
def _current_week_five_hour_percent(conn, now_utc, week_scope, *,
|
|
393
687
|
account_key, week_start_at,
|
|
394
688
|
week_start_before_override):
|