cctally 1.105.0 → 1.106.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/bin/_cctally_core.py +151 -20
- package/bin/_cctally_dashboard.py +20 -227
- package/bin/_cctally_dashboard_conversation.py +38 -0
- package/bin/_cctally_dashboard_envelope.py +79 -10
- package/bin/_cctally_dashboard_share.py +7 -1
- package/bin/_cctally_db.py +267 -5
- package/bin/_cctally_doctor.py +40 -15
- package/bin/_cctally_five_hour.py +53 -6
- package/bin/_cctally_forecast.py +197 -42
- package/bin/_cctally_journal.py +217 -27
- package/bin/_cctally_milestone_history.py +141 -58
- package/bin/_cctally_milestones.py +72 -0
- package/bin/_cctally_parser.py +10 -6
- package/bin/_cctally_percent_breakdown.py +26 -20
- package/bin/_cctally_project.py +5 -3
- package/bin/_cctally_quota.py +12 -2
- package/bin/_cctally_quota_model.py +12 -4
- package/bin/_cctally_record.py +1254 -300
- package/bin/_cctally_reporting.py +19 -0
- package/bin/_cctally_share.py +21 -3
- package/bin/_cctally_statusline.py +15 -11
- package/bin/_cctally_tui.py +192 -42
- package/bin/_cctally_weekrefs.py +322 -161
- package/bin/_lib_credit.py +9 -2
- package/bin/_lib_credit_identity.py +159 -0
- package/bin/_lib_credit_selection.py +342 -0
- package/bin/_lib_dashboard_sources.py +16 -4
- package/bin/_lib_doctor.py +1 -1
- package/bin/_lib_ingest_frontier.py +46 -0
- package/bin/_lib_quota_model.py +34 -28
- package/bin/_lib_record.py +41 -9
- package/bin/_lib_rederive.py +8 -0
- package/bin/_lib_render.py +72 -4
- package/bin/_lib_snapshot_cache.py +13 -10
- package/bin/_lib_subscription_weeks.py +141 -54
- package/bin/_lib_view_models.py +128 -22
- package/bin/cctally +7 -0
- package/dashboard/static/assets/{ConversationsView-DA4j7Yso.js → ConversationsView-Dst33C0n.js} +13 -13
- package/dashboard/static/assets/{DoctorModal-D4PbVnbE.js → DoctorModal-5rvkyksy.js} +1 -1
- package/dashboard/static/assets/ModalRoot-BJH02VzE.js +1 -0
- package/dashboard/static/assets/{ProjectsDrillPanel-DkPlB9JH.js → ProjectsDrillPanel-CsR1bjJu.js} +1 -1
- package/dashboard/static/assets/{SourceDetailModal-BE7FONsS.js → SourceDetailModal-BnTIwt-R.js} +1 -1
- package/dashboard/static/assets/{UpdateModal-BJuI8glf.js → UpdateModal-D_RfssnW.js} +3 -3
- package/dashboard/static/assets/index-rzXw99Hy.js +13 -0
- package/dashboard/static/assets/index-u3wLfjt2.css +1 -0
- package/dashboard/static/assets/{outlineNavigation-zDVm6Hdd.js → outlineNavigation-C7J9lhGB.js} +1 -1
- package/dashboard/static/assets/useKeymap-B3KCc_c7.js +1 -0
- package/dashboard/static/dashboard.html +3 -3
- package/package.json +3 -1
- package/dashboard/static/assets/ModalRoot-D4oSPxPp.js +0 -1
- package/dashboard/static/assets/index-klO46NcU.css +0 -1
- package/dashboard/static/assets/index-w_bINkJ8.js +0 -13
- package/dashboard/static/assets/useKeymap-CJ-Pi17D.js +0 -1
|
@@ -1326,6 +1326,30 @@ def _forecast_quota_envelope(fc, cw, rate_change):
|
|
|
1326
1326
|
}
|
|
1327
1327
|
|
|
1328
1328
|
|
|
1329
|
+
def _trend_withheld_key(row) -> dict:
|
|
1330
|
+
"""The `dollar_per_pct_withheld` key for one trend row, or no key at all.
|
|
1331
|
+
|
|
1332
|
+
#703 + #707 §6.3. `build_trend_view` resolves the cause per week, and both
|
|
1333
|
+
trend arrays feed a `$/1%` column — ``weeks`` the panel's, ``history`` the
|
|
1334
|
+
modal's. Without the cause the column renders the em-dash that reads as "no
|
|
1335
|
+
usage recorded", which is a different and wrong statement about a week that
|
|
1336
|
+
holds a credit.
|
|
1337
|
+
|
|
1338
|
+
The key is OMITTED rather than published as null when nothing is withheld,
|
|
1339
|
+
the way the session rows' ``title`` key is. The client's
|
|
1340
|
+
``withheldDollarPerPctLabel`` treats an absent key and a null one
|
|
1341
|
+
identically, a week that withholds nothing is the overwhelming majority,
|
|
1342
|
+
and publishing the null on all twenty rows of the two arrays would move
|
|
1343
|
+
every committed dashboard golden and the corpus-wide envelope oracle to
|
|
1344
|
+
carry a value no consumer can distinguish from its absence.
|
|
1345
|
+
|
|
1346
|
+
``getattr`` tolerates the minimal trend shapes — SimpleNamespace test rows,
|
|
1347
|
+
older fixtures — that the sibling ``cost_usd`` key already tolerates.
|
|
1348
|
+
"""
|
|
1349
|
+
cause = getattr(row, "dpp_withheld_cause", None)
|
|
1350
|
+
return {} if cause is None else {"dollar_per_pct_withheld": cause}
|
|
1351
|
+
|
|
1352
|
+
|
|
1329
1353
|
def _header_rate_and_delta(current_rate, current_week_start, trend):
|
|
1330
1354
|
"""`(dollar_per_pct, vs_last_week_delta)` from ONE current operand.
|
|
1331
1355
|
|
|
@@ -1595,22 +1619,39 @@ def snapshot_to_envelope(snap: "DataSnapshot", *,
|
|
|
1595
1619
|
week_lbl: "str | None" = None
|
|
1596
1620
|
reset_at_utc: "dt.datetime | None" = None
|
|
1597
1621
|
week_start_at_utc: "dt.datetime | None" = None
|
|
1622
|
+
nominal_week_start_at_utc: "dt.datetime | None" = None
|
|
1598
1623
|
if cw is not None:
|
|
1599
1624
|
ws = getattr(cw, "week_start_at", None)
|
|
1600
1625
|
we = getattr(cw, "week_end_at", None)
|
|
1626
|
+
# #703 + #707 §6.1/§6.4 — the LABEL names the week, not the credit.
|
|
1627
|
+
# `week_start_at` is the rate anchor and moves to the credit instant on
|
|
1628
|
+
# a credited week, which is intended for the `$/1%` numerator and wrong
|
|
1629
|
+
# for the window a person is shown: a credit moves no boundary.
|
|
1630
|
+
# Labelling from it made the hero read `WEEK USAGE · Apr 16–Apr 20` over
|
|
1631
|
+
# a week running Apr 13–Apr 20, over milestone rows dated before Apr 16,
|
|
1632
|
+
# and against the credit tooltip's own promise that the week keeps its
|
|
1633
|
+
# own boundaries.
|
|
1634
|
+
#
|
|
1635
|
+
# `displayed_week_start_at` falls back to the rate anchor, so legacy and
|
|
1636
|
+
# fixture snapshots that construct `TuiCurrentWeek` without the field
|
|
1637
|
+
# render exactly what they rendered before.
|
|
1638
|
+
from _lib_view_models import displayed_week_start_at
|
|
1639
|
+
ws_label = displayed_week_start_at(cw)
|
|
1601
1640
|
# Full window "Apr 13–20" so the dashboard matches the TUI/report
|
|
1602
1641
|
# headers and doesn't hide month crossings (e.g. "Apr 27–May 03").
|
|
1603
1642
|
# En-dash U+2013 matches the TUI format at cmd_tui.
|
|
1604
|
-
if
|
|
1643
|
+
if ws_label is not None and we is not None:
|
|
1605
1644
|
week_lbl = (
|
|
1606
|
-
f"{format_display_dt(
|
|
1645
|
+
f"{format_display_dt(ws_label, resolved_tz_obj, fmt='%b %d', suffix=False)}"
|
|
1607
1646
|
f"–"
|
|
1608
1647
|
f"{format_display_dt(we, resolved_tz_obj, fmt='%b %d', suffix=False)}"
|
|
1609
1648
|
)
|
|
1610
|
-
elif
|
|
1611
|
-
week_lbl = format_display_dt(
|
|
1649
|
+
elif ws_label is not None:
|
|
1650
|
+
week_lbl = format_display_dt(
|
|
1651
|
+
ws_label, resolved_tz_obj, fmt='%b %d', suffix=False)
|
|
1612
1652
|
reset_at_utc = we
|
|
1613
1653
|
week_start_at_utc = ws
|
|
1654
|
+
nominal_week_start_at_utc = ws_label
|
|
1614
1655
|
|
|
1615
1656
|
# Header forecast_pct should match the projection that drove the
|
|
1616
1657
|
# verdict pill next to it. The View (issue #57) carries the
|
|
@@ -1648,6 +1689,13 @@ def snapshot_to_envelope(snap: "DataSnapshot", *,
|
|
|
1648
1689
|
"models": list(r.models),
|
|
1649
1690
|
"week_start_at": r.week_start_at,
|
|
1650
1691
|
"week_end_at": r.week_end_at,
|
|
1692
|
+
# #703 + #707 §6.4/§6.6: additive, so no `schemaVersion` bump. The
|
|
1693
|
+
# week no longer splits, so `credited` is what explains a low
|
|
1694
|
+
# percent late in a heavy week; the withheld cause distinguishes
|
|
1695
|
+
# "the epoch supports no divisor" from "no usage recorded", which a
|
|
1696
|
+
# bare null cannot.
|
|
1697
|
+
"credited": r.credited,
|
|
1698
|
+
"dollar_per_pct_withheld": r.dollar_per_pct_withheld_cause,
|
|
1651
1699
|
}
|
|
1652
1700
|
|
|
1653
1701
|
def _monthly_row_to_dict(r: "MonthlyPeriodRow") -> dict:
|
|
@@ -2045,13 +2093,32 @@ def snapshot_to_envelope(snap: "DataSnapshot", *,
|
|
|
2045
2093
|
# serializing, the same way `five_hour_block` does.
|
|
2046
2094
|
"total_tokens": getattr(cw, "total_tokens", 0),
|
|
2047
2095
|
"dollar_per_pct": cw.dollars_per_percent,
|
|
2048
|
-
# #
|
|
2049
|
-
# the
|
|
2050
|
-
#
|
|
2051
|
-
#
|
|
2052
|
-
#
|
|
2053
|
-
#
|
|
2096
|
+
# #703 + #707 §6.3 — why the ratio above is null on a credited
|
|
2097
|
+
# week. Additive and optional, the same shape the weekly rows
|
|
2098
|
+
# publish, so the two surfaces on one screen state one cause.
|
|
2099
|
+
# Without it the hero rendered `$0.000` beside `spent $0.00`,
|
|
2100
|
+
# which is the misleading figure the withholding rule exists to
|
|
2101
|
+
# replace. `getattr` keeps legacy fixture modules that construct
|
|
2102
|
+
# `TuiCurrentWeek` without the field serializing.
|
|
2103
|
+
"dollar_per_pct_withheld": getattr(
|
|
2104
|
+
cw, "dpp_withheld_cause", None),
|
|
2105
|
+
# #556 S1 §3.5/§3.6 — the RATE cycle start, the companion of
|
|
2106
|
+
# the already-published `reset_at_utc` end.
|
|
2107
|
+
# `_tui_build_current_week` stores this AFTER
|
|
2108
|
+
# `_apply_midweek_reset_override`, so on a credited week it is
|
|
2109
|
+
# the credit instant. The source version detects a nominal
|
|
2110
|
+
# rollover from it (§3.6), and the Projects panel and its drill
|
|
2111
|
+
# anchor their grid on it (#620).
|
|
2054
2112
|
"week_start_at": _iso_z(week_start_at_utc),
|
|
2113
|
+
# #703 + #707 §6.2 — the week's OWN start, which is the range
|
|
2114
|
+
# `spent_usd` above covers. Published because composition labels
|
|
2115
|
+
# the Claude leg's period over exactly the spend that leg
|
|
2116
|
+
# reports, and `week_start_at` stopped being that bound when
|
|
2117
|
+
# spend moved back to the full week: a leg built from it named a
|
|
2118
|
+
# four-day window `Claude subscription week` and reported a full
|
|
2119
|
+
# week's spend inside it. Additive and optional, so an older
|
|
2120
|
+
# client and an older reader both keep working.
|
|
2121
|
+
"nominal_week_start_at": _iso_z(nominal_week_start_at_utc),
|
|
2055
2122
|
"reset_at_utc": _iso_z(reset_at_utc),
|
|
2056
2123
|
"reset_in_sec":
|
|
2057
2124
|
None if reset_at_utc is None
|
|
@@ -2137,6 +2204,7 @@ def snapshot_to_envelope(snap: "DataSnapshot", *,
|
|
|
2137
2204
|
# that predate this nullable field. ``None`` when the
|
|
2138
2205
|
# week has no cost snapshot.
|
|
2139
2206
|
"cost_usd": round(_wc, 4) if (_wc := getattr(w, "weekly_cost_usd", None)) is not None else None,
|
|
2207
|
+
**_trend_withheld_key(w),
|
|
2140
2208
|
}
|
|
2141
2209
|
for w in snap.trend
|
|
2142
2210
|
],
|
|
@@ -2151,6 +2219,7 @@ def snapshot_to_envelope(snap: "DataSnapshot", *,
|
|
|
2151
2219
|
# #264 S3: additive weekly cost (see weeks[] above; same
|
|
2152
2220
|
# getattr tolerance for minimal/older trend shapes).
|
|
2153
2221
|
"cost_usd": round(_wc, 4) if (_wc := getattr(w, "weekly_cost_usd", None)) is not None else None,
|
|
2222
|
+
**_trend_withheld_key(w),
|
|
2154
2223
|
}
|
|
2155
2224
|
for w in (snap.weekly_history or [])
|
|
2156
2225
|
],
|
|
@@ -720,7 +720,13 @@ def _build_current_week_share_panel_data(options: dict,
|
|
|
720
720
|
"week_start_date": _share_now_utc().strftime("%Y-%m-%d"),
|
|
721
721
|
"display_tz": options.get("display_tz", "Etc/UTC"),
|
|
722
722
|
}
|
|
723
|
-
|
|
723
|
+
# #703 + #707 §6.2 — the week's OWN start, not the rate anchor. It dates
|
|
724
|
+
# this card, clips `daily_progression`, and bounds `top_projects`, and all
|
|
725
|
+
# three describe `kpi_cost_usd`, which is the whole week's spend. Anchoring
|
|
726
|
+
# them at a credit instant would date the card at the credit while reporting
|
|
727
|
+
# the week's money, and would decompose that money over part of its range.
|
|
728
|
+
from _lib_view_models import displayed_week_start_at
|
|
729
|
+
week_start = displayed_week_start_at(cw)
|
|
724
730
|
week_end = getattr(cw, "week_end_at", None)
|
|
725
731
|
week_start_date = (
|
|
726
732
|
week_start.strftime("%Y-%m-%d") if isinstance(week_start, dt.datetime)
|
package/bin/_cctally_db.py
CHANGED
|
@@ -190,6 +190,132 @@ def add_column_if_missing(
|
|
|
190
190
|
return True
|
|
191
191
|
|
|
192
192
|
|
|
193
|
+
# --------------------------------------------------------------------------
|
|
194
|
+
# week_reset_events reshape for the unified credit record (#703 + #707)
|
|
195
|
+
# --------------------------------------------------------------------------
|
|
196
|
+
#
|
|
197
|
+
# The unified credit record changed `week_reset_events` in three ways that
|
|
198
|
+
# `CREATE TABLE IF NOT EXISTS` cannot deliver to a store whose table already
|
|
199
|
+
# exists: six fact columns were added, both boundary columns became nullable,
|
|
200
|
+
# and the row constraint moved from the boundary pair to
|
|
201
|
+
# `(account_key, credit_key)`. Only a rename-recreate-copy applies all three.
|
|
202
|
+
#
|
|
203
|
+
# The epoch bump alone covers ONE of the two upgrade paths. A store above
|
|
204
|
+
# `LEGACY_STATS_HEAD` defers to a rebuild that materializes a fresh scratch
|
|
205
|
+
# index through the current DDL, so it arrives correct. A PRE-CUTOVER store at
|
|
206
|
+
# `user_version <= 13` takes the other path: the schema apply is a no-op
|
|
207
|
+
# against the table that is already there, the cutover stamps the new epoch in
|
|
208
|
+
# place, and every later open fast-returns at the epoch gate before any schema
|
|
209
|
+
# work runs. Without this reshape such a store REPORTS the current epoch while
|
|
210
|
+
# missing every fact column and keeping both NOT NULL boundaries — a state with
|
|
211
|
+
# no path back, in which the manual credit fold, the automatic fold and every
|
|
212
|
+
# fact read raise.
|
|
213
|
+
#
|
|
214
|
+
# `add_column_if_missing` is not sufficient on its own, because it cannot relax
|
|
215
|
+
# a NOT NULL constraint or replace a UNIQUE, and the manual fold inserts NULL
|
|
216
|
+
# into both boundary columns.
|
|
217
|
+
#
|
|
218
|
+
# Split in two so the canonical DDL has exactly one copy — the one in
|
|
219
|
+
# `_cctally_core.open_db`. `begin_week_reset_events_reshape` renames the legacy
|
|
220
|
+
# table out of the way; the caller's own `CREATE TABLE IF NOT EXISTS` then
|
|
221
|
+
# builds the current shape; `finish_week_reset_events_reshape` copies the rows
|
|
222
|
+
# across and drops the legacy table. `sqlite_master.sql` for the recreated
|
|
223
|
+
# table is therefore byte-identical to a freshly created one, which the rebuild
|
|
224
|
+
# validator's schema fingerprint requires.
|
|
225
|
+
|
|
226
|
+
#: Where the pre-reshape table is parked between the two halves. A leftover
|
|
227
|
+
#: under this name means a previous attempt crashed after the rename; the next
|
|
228
|
+
#: open resumes from it rather than starting over.
|
|
229
|
+
WEEK_RESET_EVENTS_RESHAPE_OLD = "week_reset_events_pre_1012"
|
|
230
|
+
|
|
231
|
+
#: Presence of these decides whether a reshape is needed. They arrived together
|
|
232
|
+
#: with the nullability and constraint changes, so any store missing them is a
|
|
233
|
+
#: store that predates all three.
|
|
234
|
+
_WEEK_RESET_EVENTS_FACT_COLUMNS = frozenset({
|
|
235
|
+
"week_start_date", "observed_at_utc", "confirming_capture_at_utc",
|
|
236
|
+
"observed_post_credit_pct", "credit_key", "credit_order",
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _table_exists(conn: sqlite3.Connection, table: str) -> bool:
|
|
241
|
+
return conn.execute(
|
|
242
|
+
"SELECT 1 FROM sqlite_master WHERE type='table' AND name=?", (table,)
|
|
243
|
+
).fetchone() is not None
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def begin_week_reset_events_reshape(conn: sqlite3.Connection) -> bool:
|
|
247
|
+
"""Park a pre-#703 `week_reset_events` aside; return whether one is parked.
|
|
248
|
+
|
|
249
|
+
Returns True when the caller must call `finish_week_reset_events_reshape`
|
|
250
|
+
after creating the current-shape table — either because this call renamed a
|
|
251
|
+
legacy table, or because a previous attempt already did and crashed before
|
|
252
|
+
copying.
|
|
253
|
+
"""
|
|
254
|
+
if _table_exists(conn, WEEK_RESET_EVENTS_RESHAPE_OLD):
|
|
255
|
+
return True
|
|
256
|
+
if not _table_exists(conn, "week_reset_events"):
|
|
257
|
+
return False
|
|
258
|
+
cols = {
|
|
259
|
+
str(r[1])
|
|
260
|
+
for r in conn.execute("PRAGMA table_info(week_reset_events)").fetchall()
|
|
261
|
+
}
|
|
262
|
+
if _WEEK_RESET_EVENTS_FACT_COLUMNS <= cols:
|
|
263
|
+
return False
|
|
264
|
+
conn.execute(
|
|
265
|
+
f"ALTER TABLE week_reset_events "
|
|
266
|
+
f"RENAME TO {WEEK_RESET_EVENTS_RESHAPE_OLD}"
|
|
267
|
+
)
|
|
268
|
+
return True
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def finish_week_reset_events_reshape(conn: sqlite3.Connection) -> None:
|
|
272
|
+
"""Copy the parked rows into the current-shape table and drop the parked one.
|
|
273
|
+
|
|
274
|
+
`id` is copied explicitly. `percent_milestones.reset_event_id` names a
|
|
275
|
+
credit by that identifier and the foreign keys in this codebase are
|
|
276
|
+
documentation-only, so letting AUTOINCREMENT renumber the rows would
|
|
277
|
+
silently re-point every milestone at a different credit with nothing
|
|
278
|
+
raising.
|
|
279
|
+
|
|
280
|
+
Columns present only on the legacy table are dropped and columns present
|
|
281
|
+
only on the new one arrive NULL, which is the honest value: NULL on a fact
|
|
282
|
+
column means the row predates the change, exactly as it does for a legacy
|
|
283
|
+
`wr:` event replayed on the rebuild path.
|
|
284
|
+
|
|
285
|
+
The parked table's indexes followed it through the rename and still hold
|
|
286
|
+
the `idx_week_reset_events_*` names, so `DROP TABLE` here is what frees
|
|
287
|
+
those names for the caller's own `CREATE INDEX IF NOT EXISTS` to rebuild
|
|
288
|
+
them against the new table. Dropping them any later would leave the new
|
|
289
|
+
table unindexed while the statements that would have built them no-op.
|
|
290
|
+
"""
|
|
291
|
+
if not _table_exists(conn, WEEK_RESET_EVENTS_RESHAPE_OLD):
|
|
292
|
+
return
|
|
293
|
+
old_cols = [
|
|
294
|
+
str(r[1])
|
|
295
|
+
for r in conn.execute(
|
|
296
|
+
f"PRAGMA table_info({WEEK_RESET_EVENTS_RESHAPE_OLD})").fetchall()
|
|
297
|
+
]
|
|
298
|
+
new_cols = {
|
|
299
|
+
str(r[1])
|
|
300
|
+
for r in conn.execute("PRAGMA table_info(week_reset_events)").fetchall()
|
|
301
|
+
}
|
|
302
|
+
shared = [name for name in old_cols if name in new_cols]
|
|
303
|
+
projection = ", ".join(f'"{name}"' for name in shared)
|
|
304
|
+
if conn.in_transaction:
|
|
305
|
+
conn.commit()
|
|
306
|
+
conn.execute("BEGIN IMMEDIATE")
|
|
307
|
+
try:
|
|
308
|
+
conn.execute(
|
|
309
|
+
f"INSERT INTO week_reset_events ({projection}) "
|
|
310
|
+
f"SELECT {projection} FROM {WEEK_RESET_EVENTS_RESHAPE_OLD}"
|
|
311
|
+
)
|
|
312
|
+
conn.execute(f"DROP TABLE {WEEK_RESET_EVENTS_RESHAPE_OLD}")
|
|
313
|
+
conn.commit()
|
|
314
|
+
except Exception:
|
|
315
|
+
conn.rollback()
|
|
316
|
+
raise
|
|
317
|
+
|
|
318
|
+
|
|
193
319
|
# === Region 2: Migration framework + dispatcher (was bin/cctally:10952-11229) ===
|
|
194
320
|
|
|
195
321
|
_MIGRATION_NAME_RE = re.compile(r"^\d{3}_[a-z0-9_]+$")
|
|
@@ -3381,6 +3507,13 @@ def _migration_observed_pre_credit_pct(conn: sqlite3.Connection) -> None:
|
|
|
3381
3507
|
switches from ``round(weekly_percent,1) = round(?,1)`` to
|
|
3382
3508
|
``ABS(weekly_percent - ?) < 1.0``.
|
|
3383
3509
|
|
|
3510
|
+
That companion predicate is HISTORY, not current behaviour: #703 + #707
|
|
3511
|
+
replaced the tolerance band with the evidence rules in
|
|
3512
|
+
``bin/_lib_credit_selection.py``, because the band compared the stored
|
|
3513
|
+
percent against a remembered level rather than against evidence. The column
|
|
3514
|
+
this migration adds is still written and still read; only the predicate that
|
|
3515
|
+
consumed it changed.
|
|
3516
|
+
|
|
3384
3517
|
Idempotent: a second invocation finds the column already present
|
|
3385
3518
|
and returns. Empty-column fast path: when the live CREATE TABLE
|
|
3386
3519
|
already carries the column (fresh install), return without an ALTER
|
|
@@ -4206,10 +4339,18 @@ def _apply_codex_accounting_change_ledger(conn: sqlite3.Connection) -> None:
|
|
|
4206
4339
|
class SchemaDeliveryObject(NamedTuple):
|
|
4207
4340
|
"""One re-derivable schema object declared by a version-gated apply.
|
|
4208
4341
|
|
|
4209
|
-
``kind`` is one of ``table``, ``view``, ``trigger
|
|
4210
|
-
store registries and their structural tests cover
|
|
4211
|
-
those kinds, including virtual tables while
|
|
4212
|
-
shadow objects.
|
|
4342
|
+
``kind`` is one of ``table``, ``view``, ``trigger``, ``index`` or
|
|
4343
|
+
``column``. The two store registries and their structural tests cover
|
|
4344
|
+
every explicit object of those kinds, including virtual tables while
|
|
4345
|
+
excluding SQLite-created FTS shadow objects.
|
|
4346
|
+
|
|
4347
|
+
A ``column`` record is named ``<table>.<column>`` and covers exactly the
|
|
4348
|
+
columns a schema apply declares with ``add_column_if_missing``. Those
|
|
4349
|
+
are the columns whose delivery depends on the version gate, because a
|
|
4350
|
+
store created from scratch receives them from the CREATE TABLE body
|
|
4351
|
+
instead. #682 added two such declarations with no companion migration,
|
|
4352
|
+
the head never moved, and every already-current store kept the old table
|
|
4353
|
+
shape while the shipped code assumed the new one.
|
|
4213
4354
|
|
|
4214
4355
|
``ensure_helper`` names the shared module-level function that owns the DDL,
|
|
4215
4356
|
or is ``None`` when the statement is written inline in the schema body.
|
|
@@ -4243,6 +4384,42 @@ class SchemaDeliveryObject(NamedTuple):
|
|
|
4243
4384
|
#: identity so even a same-kind remove-one/add-one substitution fails.
|
|
4244
4385
|
CACHE_REDERIVABLE_OBJECTS: "tuple[SchemaDeliveryObject, ...]" = (
|
|
4245
4386
|
# ── frozen baseline: audited objects without handler-owned delivery DDL ──
|
|
4387
|
+
# Columns predating this registry's column kind. Every one was verified
|
|
4388
|
+
# present in a real production cache.db stamped at head 44, so the
|
|
4389
|
+
# baseline is an observation rather than an assumption. Several arrived
|
|
4390
|
+
# alongside a migration that backfilled their VALUES, but no handler owns
|
|
4391
|
+
# the ALTER itself, so naming one here would fail the delivery assertion.
|
|
4392
|
+
SchemaDeliveryObject("column", "codex_session_entries.account_key", None, None),
|
|
4393
|
+
SchemaDeliveryObject("column", "codex_session_entries.conversation_key", None, None),
|
|
4394
|
+
SchemaDeliveryObject("column", "codex_session_entries.source_root_key", None, None),
|
|
4395
|
+
SchemaDeliveryObject("column", "codex_session_files.account_key", None, None),
|
|
4396
|
+
SchemaDeliveryObject("column", "codex_session_files.last_conversation_key", None, None),
|
|
4397
|
+
SchemaDeliveryObject("column", "codex_session_files.last_native_thread_id", None, None),
|
|
4398
|
+
SchemaDeliveryObject("column", "codex_session_files.last_parent_thread_id", None, None),
|
|
4399
|
+
SchemaDeliveryObject("column", "codex_session_files.last_root_thread_id", None, None),
|
|
4400
|
+
SchemaDeliveryObject("column", "codex_session_files.last_turn_id", None, None),
|
|
4401
|
+
SchemaDeliveryObject("column", "codex_session_files.source_root_key", None, None),
|
|
4402
|
+
SchemaDeliveryObject("column", "conversation_messages.attribution_plugin", None, None),
|
|
4403
|
+
SchemaDeliveryObject("column", "conversation_messages.attribution_skill", None, None),
|
|
4404
|
+
SchemaDeliveryObject("column", "conversation_messages.search_thinking", None, None),
|
|
4405
|
+
SchemaDeliveryObject("column", "conversation_messages.search_tool", None, None),
|
|
4406
|
+
SchemaDeliveryObject("column", "conversation_messages.source_tool_use_id", None, None),
|
|
4407
|
+
SchemaDeliveryObject("column", "conversation_messages.stop_reason", None, None),
|
|
4408
|
+
SchemaDeliveryObject("column", "conversation_sessions.git_branch", None, None),
|
|
4409
|
+
SchemaDeliveryObject("column", "conversation_sessions.models_json", None, None),
|
|
4410
|
+
SchemaDeliveryObject("column", "conversation_sessions.title", None, None),
|
|
4411
|
+
SchemaDeliveryObject("column", "quota_window_snapshots.account_key", None, None),
|
|
4412
|
+
SchemaDeliveryObject("column", "quota_window_snapshots.canonical_resets_at_utc", None, None),
|
|
4413
|
+
SchemaDeliveryObject("column", "quota_window_snapshots.observed_model", None, None),
|
|
4414
|
+
SchemaDeliveryObject("column", "session_entries.account_key", None, None),
|
|
4415
|
+
SchemaDeliveryObject("column", "session_entries.cache_create_1h_tokens", None, None),
|
|
4416
|
+
SchemaDeliveryObject("column", "session_entries.cache_create_5m_tokens", None, None),
|
|
4417
|
+
SchemaDeliveryObject("column", "session_entries.mutation_min_ts", None, None),
|
|
4418
|
+
SchemaDeliveryObject("column", "session_entries.mutation_seq", None, None),
|
|
4419
|
+
SchemaDeliveryObject("column", "session_entries.speed", None, None),
|
|
4420
|
+
SchemaDeliveryObject("column", "session_files.account_key", None, None),
|
|
4421
|
+
SchemaDeliveryObject("column", "session_files.project_path", None, None),
|
|
4422
|
+
SchemaDeliveryObject("column", "session_files.session_id", None, None),
|
|
4246
4423
|
SchemaDeliveryObject("index", "idx_codex_conv_msgs_conversation", None, None),
|
|
4247
4424
|
SchemaDeliveryObject("index", "idx_codex_conv_msgs_source", None, None),
|
|
4248
4425
|
SchemaDeliveryObject("index", "idx_codex_conv_rollups_recent", None, None),
|
|
@@ -4301,6 +4478,15 @@ CACHE_REDERIVABLE_OBJECTS: "tuple[SchemaDeliveryObject, ...]" = (
|
|
|
4301
4478
|
SchemaDeliveryObject("trigger", "conv_title_fts_ai", None, None),
|
|
4302
4479
|
SchemaDeliveryObject("trigger", "conv_title_fts_au", None, None),
|
|
4303
4480
|
# ── post-baseline: each migration owns the object's delivery DDL ──
|
|
4481
|
+
SchemaDeliveryObject(
|
|
4482
|
+
"column", "codex_session_files.ingest_complete", None,
|
|
4483
|
+
"038_codex_session_files_ingest_complete"),
|
|
4484
|
+
SchemaDeliveryObject(
|
|
4485
|
+
"column", "codex_conversation_rollups.render_revision", None,
|
|
4486
|
+
"045_conversation_render_revision_columns"),
|
|
4487
|
+
SchemaDeliveryObject(
|
|
4488
|
+
"column", "conversation_sessions.render_revision", None,
|
|
4489
|
+
"045_conversation_render_revision_columns"),
|
|
4304
4490
|
SchemaDeliveryObject(
|
|
4305
4491
|
"index", "idx_entries_physical", None,
|
|
4306
4492
|
"020_session_entries_physical_unique"),
|
|
@@ -5368,6 +5554,22 @@ def _apply_codex_find_projection_schema(conn: sqlite3.Connection) -> None:
|
|
|
5368
5554
|
#: projection is the one audited family with handler-owned delivery DDL.
|
|
5369
5555
|
CONVERSATIONS_REDERIVABLE_OBJECTS: "tuple[SchemaDeliveryObject, ...]" = (
|
|
5370
5556
|
# ── frozen baseline: audited objects without handler-owned delivery DDL ──
|
|
5557
|
+
# Same column-kind baseline rule as the cache registry above. This store is
|
|
5558
|
+
# projected from the cache schema, so most of these are the same physical
|
|
5559
|
+
# declarations scoped to the tables conversations.db actually carries; all
|
|
5560
|
+
# twelve were verified present in a real production conversations.db.
|
|
5561
|
+
SchemaDeliveryObject("column", "codex_conversation_events.account_key", None, None),
|
|
5562
|
+
SchemaDeliveryObject("column", "codex_conversation_messages.account_key", None, None),
|
|
5563
|
+
SchemaDeliveryObject("column", "conversation_messages.account_key", None, None),
|
|
5564
|
+
SchemaDeliveryObject("column", "conversation_messages.attribution_plugin", None, None),
|
|
5565
|
+
SchemaDeliveryObject("column", "conversation_messages.attribution_skill", None, None),
|
|
5566
|
+
SchemaDeliveryObject("column", "conversation_messages.search_thinking", None, None),
|
|
5567
|
+
SchemaDeliveryObject("column", "conversation_messages.search_tool", None, None),
|
|
5568
|
+
SchemaDeliveryObject("column", "conversation_messages.source_tool_use_id", None, None),
|
|
5569
|
+
SchemaDeliveryObject("column", "conversation_messages.stop_reason", None, None),
|
|
5570
|
+
SchemaDeliveryObject("column", "conversation_sessions.git_branch", None, None),
|
|
5571
|
+
SchemaDeliveryObject("column", "conversation_sessions.models_json", None, None),
|
|
5572
|
+
SchemaDeliveryObject("column", "conversation_sessions.title", None, None),
|
|
5371
5573
|
SchemaDeliveryObject(
|
|
5372
5574
|
"index", "idx_codex_conv_messages_account_conversation", None, None),
|
|
5373
5575
|
SchemaDeliveryObject("index", "idx_codex_conv_msgs_conversation", None, None),
|
|
@@ -5411,7 +5613,13 @@ CONVERSATIONS_REDERIVABLE_OBJECTS: "tuple[SchemaDeliveryObject, ...]" = (
|
|
|
5411
5613
|
SchemaDeliveryObject("trigger", "conv_title_fts_ad", None, None),
|
|
5412
5614
|
SchemaDeliveryObject("trigger", "conv_title_fts_ai", None, None),
|
|
5413
5615
|
SchemaDeliveryObject("trigger", "conv_title_fts_au", None, None),
|
|
5414
|
-
# ── post-baseline: migration
|
|
5616
|
+
# ── post-baseline: each migration owns the object's delivery DDL ──
|
|
5617
|
+
SchemaDeliveryObject(
|
|
5618
|
+
"column", "codex_conversation_rollups.render_revision", None,
|
|
5619
|
+
"008_conversation_render_revision"),
|
|
5620
|
+
SchemaDeliveryObject(
|
|
5621
|
+
"column", "conversation_sessions.render_revision", None,
|
|
5622
|
+
"008_conversation_render_revision"),
|
|
5415
5623
|
SchemaDeliveryObject(
|
|
5416
5624
|
"index", "idx_codex_find_projection_conversation_order",
|
|
5417
5625
|
"_apply_codex_find_projection_schema", "004_codex_find_projection"),
|
|
@@ -8485,6 +8693,60 @@ def _044_codex_accounting_change_ledger(conn: sqlite3.Connection) -> None:
|
|
|
8485
8693
|
conn.commit()
|
|
8486
8694
|
|
|
8487
8695
|
|
|
8696
|
+
@cache_migration("045_conversation_render_revision_columns")
|
|
8697
|
+
def _045_conversation_render_revision_columns(conn: sqlite3.Connection) -> None:
|
|
8698
|
+
"""Deliver #682's two ``render_revision`` columns to an existing store.
|
|
8699
|
+
|
|
8700
|
+
``_apply_cache_schema`` already declares both columns with
|
|
8701
|
+
``add_column_if_missing``, so a store created from scratch has them. That
|
|
8702
|
+
schema pass is VERSION-GATED though: ``open_cache_db`` runs it only when
|
|
8703
|
+
``PRAGMA user_version`` differs from ``len(_CACHE_MIGRATIONS)``. #682
|
|
8704
|
+
shipped the two declarations with no companion migration, so the head never
|
|
8705
|
+
moved, an already-current store never re-ran the apply, and the columns
|
|
8706
|
+
reached new installs only. Registering here bumps the head, which is the
|
|
8707
|
+
same mechanism migrations 029, 031 and 044 rely on and is stated there in
|
|
8708
|
+
the same words.
|
|
8709
|
+
|
|
8710
|
+
The consequence on an unmigrated store was total rather than partial:
|
|
8711
|
+
``_recompute_conversation_sessions`` runs
|
|
8712
|
+
``UPDATE conversation_sessions SET render_revision=?`` on every sync, that
|
|
8713
|
+
statement cannot be prepared against the old shape, and so every
|
|
8714
|
+
``sync_cache`` failed with ``no such column: render_revision`` while the
|
|
8715
|
+
dashboard showed a permanent ``server sync error``.
|
|
8716
|
+
|
|
8717
|
+
No backfill is needed. ``render_revision`` is a monotonic invalidation
|
|
8718
|
+
frontier whose whole purpose is to force re-assembly, and the DEFAULT 0 the
|
|
8719
|
+
column arrives with is already the value that marks every existing row as
|
|
8720
|
+
needing one. The rollup recompute chokepoints advance it from there.
|
|
8721
|
+
|
|
8722
|
+
Both tables are checked for existence first, because their absence is a
|
|
8723
|
+
reachable state rather than a corrupt one: cache migration 028 removes
|
|
8724
|
+
the legacy transcript objects, and ``open_cache_db`` recreates them only
|
|
8725
|
+
afterwards. ``add_column_if_missing`` raises on a missing table, so an
|
|
8726
|
+
unguarded ALTER here would turn that ordinary state into a migration
|
|
8727
|
+
failure with a rendered error banner. Skipping is correct rather than
|
|
8728
|
+
merely safe: a table created later is created by the schema apply, whose
|
|
8729
|
+
CREATE TABLE body already carries the column.
|
|
8730
|
+
|
|
8731
|
+
The ``add_column_if_missing`` pair below is a defensive re-assert, not the
|
|
8732
|
+
primary delivery path, exactly as migration 031 re-asserts its two tables:
|
|
8733
|
+
it keeps the handler self-contained if the schema apply's ordering ever
|
|
8734
|
+
drifts, it is what the per-migration golden exercises, and it is what
|
|
8735
|
+
``tests/test_cache_schema_delivery.py`` matches against this migration's
|
|
8736
|
+
two registry records. Re-running is a no-op. NO self-stamp — the dispatcher
|
|
8737
|
+
central-stamps on a clean return (#140).
|
|
8738
|
+
"""
|
|
8739
|
+
if _table_exists(conn, "conversation_sessions"):
|
|
8740
|
+
add_column_if_missing(
|
|
8741
|
+
conn, "conversation_sessions", "render_revision",
|
|
8742
|
+
"INTEGER NOT NULL DEFAULT 0")
|
|
8743
|
+
if _table_exists(conn, "codex_conversation_rollups"):
|
|
8744
|
+
add_column_if_missing(
|
|
8745
|
+
conn, "codex_conversation_rollups", "render_revision",
|
|
8746
|
+
"INTEGER NOT NULL DEFAULT 0")
|
|
8747
|
+
conn.commit()
|
|
8748
|
+
|
|
8749
|
+
|
|
8488
8750
|
# === Region 7d: Stats migration 008_recompute_weekly_cost_snapshots_dedup_fix ===
|
|
8489
8751
|
|
|
8490
8752
|
@stats_migration("008_recompute_weekly_cost_snapshots_dedup_fix")
|
package/bin/_cctally_doctor.py
CHANGED
|
@@ -1399,31 +1399,56 @@ def _doctor_gather_state_impl(
|
|
|
1399
1399
|
# post_credit_milestone_count == 0.
|
|
1400
1400
|
# unixepoch() normalizes the cross-offset comparison.
|
|
1401
1401
|
try:
|
|
1402
|
+
# #703 + #707 §6.2: select by the canonical WEEK and the
|
|
1403
|
+
# ACCOUNT, not by `new_week_end_at`. A manual credit
|
|
1404
|
+
# leaves both boundary columns NULL, so the boundary
|
|
1405
|
+
# join found no snapshot and every `record-credit` week
|
|
1406
|
+
# was silently skipped by this check. The instant is the
|
|
1407
|
+
# accounting one, matching every other accounting read.
|
|
1402
1408
|
credit_rows = conn.execute(
|
|
1403
1409
|
"""
|
|
1404
1410
|
SELECT wre.id AS event_id,
|
|
1405
|
-
wre.
|
|
1406
|
-
wre.
|
|
1411
|
+
wre.week_start_date AS week_start_date,
|
|
1412
|
+
wre.account_key AS account_key,
|
|
1413
|
+
wre.new_week_end_at AS end_at
|
|
1407
1414
|
FROM week_reset_events wre
|
|
1408
|
-
WHERE unixepoch(wre.
|
|
1415
|
+
WHERE unixepoch(COALESCE(wre.observed_at_utc,
|
|
1416
|
+
wre.effective_reset_at_utc))
|
|
1409
1417
|
<= unixepoch(?)
|
|
1410
1418
|
""",
|
|
1411
1419
|
(now_utc_iso(),),
|
|
1412
1420
|
).fetchall()
|
|
1413
1421
|
credited_weeks = []
|
|
1414
1422
|
for cr in credit_rows:
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1423
|
+
evt_id = cr["event_id"]
|
|
1424
|
+
# A row that predates `week_start_date` carries only
|
|
1425
|
+
# the boundary, so that join is the fallback for
|
|
1426
|
+
# exactly those rows and nothing else.
|
|
1427
|
+
if cr["week_start_date"]:
|
|
1428
|
+
latest = conn.execute(
|
|
1429
|
+
"""
|
|
1430
|
+
SELECT week_start_date, weekly_percent
|
|
1431
|
+
FROM weekly_usage_snapshots
|
|
1432
|
+
WHERE week_start_date = ?
|
|
1433
|
+
AND account_key = ?
|
|
1434
|
+
ORDER BY captured_at_utc DESC, id DESC
|
|
1435
|
+
LIMIT 1
|
|
1436
|
+
""",
|
|
1437
|
+
(cr["week_start_date"], cr["account_key"]),
|
|
1438
|
+
).fetchone()
|
|
1439
|
+
elif cr["end_at"]:
|
|
1440
|
+
latest = conn.execute(
|
|
1441
|
+
"""
|
|
1442
|
+
SELECT week_start_date, weekly_percent
|
|
1443
|
+
FROM weekly_usage_snapshots
|
|
1444
|
+
WHERE week_end_at = ?
|
|
1445
|
+
ORDER BY captured_at_utc DESC, id DESC
|
|
1446
|
+
LIMIT 1
|
|
1447
|
+
""",
|
|
1448
|
+
(cr["end_at"],),
|
|
1449
|
+
).fetchone()
|
|
1450
|
+
else:
|
|
1451
|
+
latest = None
|
|
1427
1452
|
if latest is None or latest[0] is None:
|
|
1428
1453
|
continue
|
|
1429
1454
|
ws = latest[0]
|
|
@@ -351,13 +351,52 @@ def _load_recorded_five_hour_windows(
|
|
|
351
351
|
# rendered as a phantom heuristic "~" row).
|
|
352
352
|
credit_moments: list[dt.datetime] = []
|
|
353
353
|
try:
|
|
354
|
+
# #703 + #707 §6.2: EVERY credit contributes a discontinuity
|
|
355
|
+
# moment, so the `old_week_end_at = effective_reset_at_utc`
|
|
356
|
+
# filter is gone. It selected only the automatic in-place shape,
|
|
357
|
+
# and a MANUAL credit leaves both boundary columns NULL — so a
|
|
358
|
+
# `record-credit` week produced no moment here at all and its
|
|
359
|
+
# overlapping canonical blocks still resolved by dropping one,
|
|
360
|
+
# leaving the phantom heuristic row this code exists to remove.
|
|
361
|
+
# The instant is the ACCOUNTING one, matching every other
|
|
362
|
+
# accounting read.
|
|
363
|
+
#
|
|
364
|
+
# This scan is account-BLIND, and deliberately stays so — but
|
|
365
|
+
# not for the reason first recorded here. That reason said no
|
|
366
|
+
# caller holds an account key, and it is false: the statusline
|
|
367
|
+
# resolves `_statusline_active_account()` and scopes its 7d
|
|
368
|
+
# clamp legs to it in the same render, then calls this function
|
|
369
|
+
# without one. The decision stands on the other two grounds. The
|
|
370
|
+
# canonical-block query above is account-blind, and so are the
|
|
371
|
+
# session entries these moments disambiguate, because `blocks`
|
|
372
|
+
# is a ccusage drop-in with no account axis at all. Scoping only
|
|
373
|
+
# this leg would make the truncation moments disagree with the
|
|
374
|
+
# anchors and the entries they are truncating; scoping the
|
|
375
|
+
# subsystem is a change to `blocks` itself.
|
|
376
|
+
#
|
|
377
|
+
# Two consequences are stated rather than left implicit. The
|
|
378
|
+
# widened selection admits manual credits and other accounts'
|
|
379
|
+
# credits as block-truncation points. And a moment can only be
|
|
380
|
+
# used when it falls in `[next_bs, rs]` for a canonical pair
|
|
381
|
+
# inside `[range_start, range_end]`; since a stored block start
|
|
382
|
+
# is at most one block duration before its own reset, every
|
|
383
|
+
# usable moment lies in `[range_start - BLOCK_DURATION,
|
|
384
|
+
# range_end]`, so the scan is bounded to exactly that and stops
|
|
385
|
+
# loading every credit ever recorded.
|
|
386
|
+
credit_scan_start = (
|
|
387
|
+
range_start - _c.BLOCK_DURATION).isoformat()
|
|
388
|
+
credit_scan_end = range_end.isoformat()
|
|
354
389
|
credit_rows = conn.execute(
|
|
355
|
-
"SELECT effective_reset_at_utc "
|
|
356
|
-
"
|
|
357
|
-
"
|
|
390
|
+
"SELECT COALESCE(observed_at_utc, effective_reset_at_utc) "
|
|
391
|
+
" AS accounting_at "
|
|
392
|
+
" FROM week_reset_events "
|
|
393
|
+
" WHERE accounting_at IS NOT NULL "
|
|
394
|
+
" AND unixepoch(accounting_at) >= unixepoch(?) "
|
|
395
|
+
" AND unixepoch(accounting_at) <= unixepoch(?)",
|
|
396
|
+
(credit_scan_start, credit_scan_end),
|
|
358
397
|
).fetchall()
|
|
359
398
|
for c in credit_rows:
|
|
360
|
-
raw = c["
|
|
399
|
+
raw = c["accounting_at"]
|
|
361
400
|
try:
|
|
362
401
|
d = dt.datetime.fromisoformat(str(raw))
|
|
363
402
|
except ValueError:
|
|
@@ -1647,12 +1686,20 @@ def _backfill_five_hour_blocks(
|
|
|
1647
1686
|
# suffixes (block_start_at is host-local; week_start_at /
|
|
1648
1687
|
# effective_reset_at_utc are ``+00:00``); see the live-path
|
|
1649
1688
|
# comment for rationale.
|
|
1689
|
+
# #703 + #707: the ACCOUNTING instant, so a block is flagged as
|
|
1690
|
+
# crossing the moment the counter actually moved rather than the
|
|
1691
|
+
# hour it is displayed at — the floored instant can fall in the
|
|
1692
|
+
# previous block.
|
|
1650
1693
|
cross_row = conn.execute(
|
|
1651
1694
|
"""
|
|
1652
1695
|
SELECT 1 FROM week_reset_events
|
|
1653
1696
|
WHERE account_key = ?
|
|
1654
|
-
AND unixepoch(
|
|
1655
|
-
|
|
1697
|
+
AND unixepoch(COALESCE(observed_at_utc,
|
|
1698
|
+
effective_reset_at_utc))
|
|
1699
|
+
>= unixepoch(?)
|
|
1700
|
+
AND unixepoch(COALESCE(observed_at_utc,
|
|
1701
|
+
effective_reset_at_utc))
|
|
1702
|
+
<= unixepoch(?)
|
|
1656
1703
|
LIMIT 1
|
|
1657
1704
|
""",
|
|
1658
1705
|
(acct, block_start_at, last_obs),
|