cctally 1.88.2 → 1.89.1
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_cache.py +827 -37
- package/bin/_cctally_config.py +125 -0
- package/bin/_cctally_core.py +86 -2
- package/bin/_cctally_dashboard_cache_report.py +31 -0
- package/bin/_cctally_dashboard_conversation.py +26 -7
- package/bin/_cctally_dashboard_sources.py +51 -0
- package/bin/_cctally_db.py +626 -0
- package/bin/_cctally_doctor.py +84 -1
- package/bin/_cctally_journal.py +2 -1
- package/bin/_cctally_parser.py +42 -0
- package/bin/_cctally_quota.py +1358 -112
- package/bin/_cctally_record.py +249 -9
- package/bin/_cctally_setup.py +14 -5
- package/bin/_cctally_store.py +16 -1
- package/bin/_cctally_tui.py +16 -2
- package/bin/_cctally_update.py +9 -2
- package/bin/_lib_background_mcp.py +168 -0
- package/bin/_lib_cache_report.py +19 -2
- package/bin/_lib_codex_conversation.py +8 -0
- package/bin/_lib_codex_conversation_query.py +8 -7
- package/bin/_lib_conversation.py +105 -5
- package/bin/_lib_conversation_dispatch.py +15 -4
- package/bin/_lib_conversation_query.py +294 -2
- package/bin/_lib_dashboard_sources.py +5 -1
- package/bin/_lib_doctor.py +202 -1
- package/bin/_lib_jsonl.py +12 -0
- package/bin/_lib_quota_alert_axes.py +188 -0
- package/bin/_lib_quota_ledger.py +274 -0
- package/bin/_lib_snapshot_cache.py +36 -0
- package/bin/cctally +6 -3
- package/dashboard/static/assets/index-BgoYXdus.js +92 -0
- package/dashboard/static/assets/index-Ub8vwz1M.css +1 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +4 -1
- package/dashboard/static/assets/index-B0ZCsoxI.css +0 -1
- package/dashboard/static/assets/index-Bvp8mxtz.js +0 -92
package/bin/_cctally_db.py
CHANGED
|
@@ -3100,6 +3100,259 @@ def _is_transient_sqlite_error(exc: sqlite3.OperationalError) -> bool:
|
|
|
3100
3100
|
return False
|
|
3101
3101
|
|
|
3102
3102
|
|
|
3103
|
+
# Public #5: every column of ``quota_window_snapshots`` that feeds INTERPRETATION.
|
|
3104
|
+
# The UPDATE trigger fires ``AFTER UPDATE OF`` exactly this list, and the list is
|
|
3105
|
+
# the mechanism's one silent-skip hazard: a column omitted here lets its mutation
|
|
3106
|
+
# commit while the ledger stays quiet, so the projector never learns the window
|
|
3107
|
+
# is dirty and the stale block survives indefinitely.
|
|
3108
|
+
#
|
|
3109
|
+
# ``source`` is included because it is the trigger's own scope predicate — a row
|
|
3110
|
+
# flipped from 'claude' to 'codex' mints a Codex observation, and without this
|
|
3111
|
+
# entry no trigger would fire for it.
|
|
3112
|
+
#
|
|
3113
|
+
# ``source_path`` / ``line_offset`` are included even though no writer SETs them
|
|
3114
|
+
# today (ingest INSERTs OR IGNOREs; requalification DELETEs and re-INSERTs, both
|
|
3115
|
+
# ledgered; the migrations that UPDATE this table touch only ``observed_model``
|
|
3116
|
+
# and ``canonical_resets_at_utc``). They feed
|
|
3117
|
+
# ``quota_window_blocks.last_source_path``/``last_line_offset``, milestone
|
|
3118
|
+
# provenance and the per-group digest, so a future in-place rewrite of either
|
|
3119
|
+
# would be a silent skip. ``AFTER UPDATE OF`` fires on the statement's SET list,
|
|
3120
|
+
# so listing a column nobody sets costs exactly zero ledger rows — this buys the
|
|
3121
|
+
# removal of a whole hazard class for nothing.
|
|
3122
|
+
#
|
|
3123
|
+
# The one column NOT here is ``id``: the AUTOINCREMENT surrogate key, which no
|
|
3124
|
+
# interpretation reads.
|
|
3125
|
+
_QUOTA_WINDOW_SEMANTIC_COLUMNS = (
|
|
3126
|
+
"source",
|
|
3127
|
+
"source_root_key",
|
|
3128
|
+
"source_path",
|
|
3129
|
+
"line_offset",
|
|
3130
|
+
"logical_limit_key",
|
|
3131
|
+
"observed_slot",
|
|
3132
|
+
"window_minutes",
|
|
3133
|
+
"resets_at_utc",
|
|
3134
|
+
"canonical_resets_at_utc",
|
|
3135
|
+
"captured_at_utc",
|
|
3136
|
+
"used_percent",
|
|
3137
|
+
"limit_id",
|
|
3138
|
+
"limit_name",
|
|
3139
|
+
"plan_type",
|
|
3140
|
+
"individual_limit_json",
|
|
3141
|
+
"reached_type",
|
|
3142
|
+
"observed_model",
|
|
3143
|
+
"account_key",
|
|
3144
|
+
)
|
|
3145
|
+
|
|
3146
|
+
# The physical group coordinates a ledger entry records, in row-image order.
|
|
3147
|
+
_QUOTA_WINDOW_GROUP_COLUMNS = (
|
|
3148
|
+
"source_root_key",
|
|
3149
|
+
"logical_limit_key",
|
|
3150
|
+
"observed_slot",
|
|
3151
|
+
"window_minutes",
|
|
3152
|
+
"resets_at_utc",
|
|
3153
|
+
"canonical_resets_at_utc",
|
|
3154
|
+
)
|
|
3155
|
+
|
|
3156
|
+
|
|
3157
|
+
def _codex_quota_ledger_ddl() -> tuple[str, ...]:
|
|
3158
|
+
"""DDL for the Codex quota change ledger and its three triggers.
|
|
3159
|
+
|
|
3160
|
+
Public #5 spec §1. The ledger records, per mutation, the RAW physical group
|
|
3161
|
+
coordinates of the affected rows — for the old row image, the new one, or
|
|
3162
|
+
both. The projector expands those coordinates to the group's complete
|
|
3163
|
+
current membership and re-interprets it through the existing Python read
|
|
3164
|
+
path.
|
|
3165
|
+
|
|
3166
|
+
The triggers must NEVER compute an interpreted key. Interpretation snaps a
|
|
3167
|
+
jittered ``window_minutes``, rewrites ``logical_limit_key`` from
|
|
3168
|
+
``observed_model``, and folds the account over the window's whole
|
|
3169
|
+
population — the last of which is population-dependent and cannot be
|
|
3170
|
+
expressed per-row in SQL at all. Keeping exactly one implementation of the
|
|
3171
|
+
interpretation rules, in Python, is the point of recording raw scope.
|
|
3172
|
+
|
|
3173
|
+
Triggers rather than writer discipline is also the point: the entry commits
|
|
3174
|
+
in the same transaction as the mutation, so ordinary migration DML and
|
|
3175
|
+
manual repair are captured automatically, with no rule for a future author
|
|
3176
|
+
to remember.
|
|
3177
|
+
|
|
3178
|
+
``seq`` is AUTOINCREMENT, not a bare rowid alias: the projector's watermark
|
|
3179
|
+
needs a value that is never reused, or a pruned ledger could reissue a seq
|
|
3180
|
+
at or below the watermark and that entry would be skipped forever.
|
|
3181
|
+
|
|
3182
|
+
The three triggers are DROPped before they are created, while the table
|
|
3183
|
+
keeps ``IF NOT EXISTS``. A trigger is stateless, so re-creating it costs
|
|
3184
|
+
nothing and loses nothing; ``CREATE TRIGGER IF NOT EXISTS`` alone would
|
|
3185
|
+
silently keep an older body after ``_QUOTA_WINDOW_SEMANTIC_COLUMNS`` grows,
|
|
3186
|
+
and a stale ``UPDATE OF`` list is exactly the silent-skip this mechanism
|
|
3187
|
+
exists to remove. The ledger ROWS are never dropped.
|
|
3188
|
+
"""
|
|
3189
|
+
old_cols = ", ".join(f"old_{name}" for name in _QUOTA_WINDOW_GROUP_COLUMNS)
|
|
3190
|
+
new_cols = ", ".join(f"new_{name}" for name in _QUOTA_WINDOW_GROUP_COLUMNS)
|
|
3191
|
+
old_vals = ", ".join(f"OLD.{name}" for name in _QUOTA_WINDOW_GROUP_COLUMNS)
|
|
3192
|
+
new_vals = ", ".join(f"NEW.{name}" for name in _QUOTA_WINDOW_GROUP_COLUMNS)
|
|
3193
|
+
update_of = ", ".join(_QUOTA_WINDOW_SEMANTIC_COLUMNS)
|
|
3194
|
+
return (
|
|
3195
|
+
"""
|
|
3196
|
+
CREATE TABLE IF NOT EXISTS quota_window_change_log (
|
|
3197
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
3198
|
+
op TEXT NOT NULL
|
|
3199
|
+
CHECK(op IN ('insert','delete','update')),
|
|
3200
|
+
old_source_root_key TEXT,
|
|
3201
|
+
old_logical_limit_key TEXT,
|
|
3202
|
+
old_observed_slot TEXT,
|
|
3203
|
+
old_window_minutes INTEGER,
|
|
3204
|
+
old_resets_at_utc TEXT,
|
|
3205
|
+
old_canonical_resets_at_utc TEXT,
|
|
3206
|
+
new_source_root_key TEXT,
|
|
3207
|
+
new_logical_limit_key TEXT,
|
|
3208
|
+
new_observed_slot TEXT,
|
|
3209
|
+
new_window_minutes INTEGER,
|
|
3210
|
+
new_resets_at_utc TEXT,
|
|
3211
|
+
new_canonical_resets_at_utc TEXT
|
|
3212
|
+
)
|
|
3213
|
+
""",
|
|
3214
|
+
"DROP TRIGGER IF EXISTS trg_qws_ledger_ins",
|
|
3215
|
+
f"""
|
|
3216
|
+
CREATE TRIGGER trg_qws_ledger_ins
|
|
3217
|
+
AFTER INSERT ON quota_window_snapshots
|
|
3218
|
+
WHEN NEW.source = 'codex'
|
|
3219
|
+
BEGIN
|
|
3220
|
+
INSERT INTO quota_window_change_log (op, {new_cols})
|
|
3221
|
+
VALUES ('insert', {new_vals});
|
|
3222
|
+
END
|
|
3223
|
+
""",
|
|
3224
|
+
"DROP TRIGGER IF EXISTS trg_qws_ledger_del",
|
|
3225
|
+
f"""
|
|
3226
|
+
CREATE TRIGGER trg_qws_ledger_del
|
|
3227
|
+
AFTER DELETE ON quota_window_snapshots
|
|
3228
|
+
WHEN OLD.source = 'codex'
|
|
3229
|
+
BEGIN
|
|
3230
|
+
INSERT INTO quota_window_change_log (op, {old_cols})
|
|
3231
|
+
VALUES ('delete', {old_vals});
|
|
3232
|
+
END
|
|
3233
|
+
""",
|
|
3234
|
+
"DROP TRIGGER IF EXISTS trg_qws_ledger_upd",
|
|
3235
|
+
f"""
|
|
3236
|
+
CREATE TRIGGER trg_qws_ledger_upd
|
|
3237
|
+
AFTER UPDATE OF {update_of} ON quota_window_snapshots
|
|
3238
|
+
WHEN OLD.source = 'codex' OR NEW.source = 'codex'
|
|
3239
|
+
BEGIN
|
|
3240
|
+
INSERT INTO quota_window_change_log (op, {old_cols}, {new_cols})
|
|
3241
|
+
VALUES ('update', {old_vals}, {new_vals});
|
|
3242
|
+
END
|
|
3243
|
+
""",
|
|
3244
|
+
)
|
|
3245
|
+
|
|
3246
|
+
|
|
3247
|
+
#: The physical group's five members, in seek order, plus the expression the
|
|
3248
|
+
#: reader actually matches on. ``unixepoch(COALESCE(...))`` is indexed VERBATIM
|
|
3249
|
+
#: because SQLite only uses an expression index when the query's expression
|
|
3250
|
+
#: matches the indexed one — indexing the bare ``COALESCE`` while the reader
|
|
3251
|
+
#: wraps it in ``unixepoch`` silently buys nothing.
|
|
3252
|
+
_QUOTA_GROUP_INDEX_DDL = (
|
|
3253
|
+
"CREATE INDEX IF NOT EXISTS idx_qws_physical_group "
|
|
3254
|
+
"ON quota_window_snapshots("
|
|
3255
|
+
" source_root_key, logical_limit_key, observed_slot, window_minutes,"
|
|
3256
|
+
" unixepoch(COALESCE(canonical_resets_at_utc, resets_at_utc)))"
|
|
3257
|
+
" WHERE source='codex'"
|
|
3258
|
+
)
|
|
3259
|
+
|
|
3260
|
+
|
|
3261
|
+
def _apply_codex_quota_group_index(conn: sqlite3.Connection) -> None:
|
|
3262
|
+
"""Create the physical-group seek index, idempotently.
|
|
3263
|
+
|
|
3264
|
+
Public #5 Task 5. This REPLACES ``idx_qws_window_ident``, which Task 1 built
|
|
3265
|
+
to the plan's prescribed column list and which measurement then disqualified
|
|
3266
|
+
on both counts it was meant to serve. Against a 211K-row / 608-group store:
|
|
3267
|
+
|
|
3268
|
+
* The group filter's reset member is ``unixepoch(COALESCE(
|
|
3269
|
+
canonical_resets_at_utc, resets_at_utc))``, and a b-tree over the two raw
|
|
3270
|
+
reset columns cannot seek an equality on that expression. Only the
|
|
3271
|
+
five-column prefix was usable, so one group's query still walked every
|
|
3272
|
+
reset under its limit key — 19.7ms. With this expression index the same
|
|
3273
|
+
query seeks all five members: 0.60ms, a 33x difference that is the
|
|
3274
|
+
difference between "proportional to the change" and "proportional to
|
|
3275
|
+
history".
|
|
3276
|
+
* The full-sweep load reads nine columns the identity index does not carry
|
|
3277
|
+
(``source_path``, ``line_offset``, ``limit_id``, ``limit_name``,
|
|
3278
|
+
``plan_type``, ``individual_limit_json``, ``reached_type``,
|
|
3279
|
+
``observed_model``, ``account_key``), so it was never covering and the
|
|
3280
|
+
planner scanned anyway — 451ms with the index present, 451ms without it.
|
|
3281
|
+
It also did not change the plan for ``_first_block_physical_tuple``
|
|
3282
|
+
(28.2ms scanning either way). It earned nothing and charged ten columns of
|
|
3283
|
+
write cost on every ingested observation.
|
|
3284
|
+
|
|
3285
|
+
The partial ``WHERE source='codex'`` keeps Claude quota rows out of it, and
|
|
3286
|
+
makes the four leading members sufficient on their own for a Codex-scoped
|
|
3287
|
+
identity read.
|
|
3288
|
+
|
|
3289
|
+
Guarded on ``canonical_resets_at_utc`` for the same reason the ledger is: a
|
|
3290
|
+
legacy-shape cache whose schema apply took the FTS early-return before that
|
|
3291
|
+
column add would raise ``no such column`` here.
|
|
3292
|
+
"""
|
|
3293
|
+
cols = {
|
|
3294
|
+
str(row[1]) for row in conn.execute(
|
|
3295
|
+
"PRAGMA table_info(quota_window_snapshots)")
|
|
3296
|
+
}
|
|
3297
|
+
if not cols or "canonical_resets_at_utc" not in cols:
|
|
3298
|
+
return
|
|
3299
|
+
conn.execute(_QUOTA_GROUP_INDEX_DDL)
|
|
3300
|
+
conn.execute("DROP INDEX IF EXISTS idx_qws_window_ident")
|
|
3301
|
+
|
|
3302
|
+
|
|
3303
|
+
#: Partial index over the rows the standing `observed_model` resolution can
|
|
3304
|
+
#: still change. Public #5 Task 10a Step 14: the resolution runs at the tail of
|
|
3305
|
+
#: EVERY Codex sync (that is what closes the #373 Spark-pool hole on a
|
|
3306
|
+
#: `db skip 039` install and on a cache repopulated from the journal), and its
|
|
3307
|
+
#: `WHERE source='codex' AND observed_model IS NULL` had only the
|
|
3308
|
+
#: `source`-leading unique index to work with — measured at 39.8ms per tick on a
|
|
3309
|
+
#: 212K-row store that has NOTHING left to resolve, against a 264ms steady-state
|
|
3310
|
+
#: tick. The index body holds only unresolved rows (6 on that store), so it
|
|
3311
|
+
#: costs essentially nothing to maintain and turns the recurring scan into a
|
|
3312
|
+
#: seek. Gating the call behind a completion marker was the alternative and is
|
|
3313
|
+
#: strictly worse: a marker cannot see a raw `observed_model` rewrite, which is
|
|
3314
|
+
#: exactly the case migration 028 performs.
|
|
3315
|
+
_QUOTA_UNRESOLVED_MODEL_INDEX_DDL = (
|
|
3316
|
+
"CREATE INDEX IF NOT EXISTS idx_qws_unresolved_model"
|
|
3317
|
+
" ON quota_window_snapshots(source)"
|
|
3318
|
+
" WHERE observed_model IS NULL"
|
|
3319
|
+
)
|
|
3320
|
+
|
|
3321
|
+
|
|
3322
|
+
def _apply_codex_quota_unresolved_model_index(conn: sqlite3.Connection) -> None:
|
|
3323
|
+
"""Create the unresolved-model partial index, idempotently.
|
|
3324
|
+
|
|
3325
|
+
Guarded on the column for the same reason the ledger and the group index
|
|
3326
|
+
are: a legacy-shape cache whose schema apply took the FTS early-return
|
|
3327
|
+
before `observed_model` existed would raise `no such column` here.
|
|
3328
|
+
"""
|
|
3329
|
+
cols = {
|
|
3330
|
+
str(row[1]) for row in conn.execute(
|
|
3331
|
+
"PRAGMA table_info(quota_window_snapshots)")
|
|
3332
|
+
}
|
|
3333
|
+
if not cols or "observed_model" not in cols:
|
|
3334
|
+
return
|
|
3335
|
+
conn.execute(_QUOTA_UNRESOLVED_MODEL_INDEX_DDL)
|
|
3336
|
+
|
|
3337
|
+
|
|
3338
|
+
def _apply_codex_quota_change_ledger(conn: sqlite3.Connection) -> None:
|
|
3339
|
+
"""Create the Codex quota change ledger + triggers, idempotently.
|
|
3340
|
+
|
|
3341
|
+
Guarded on ``canonical_resets_at_utc``: a legacy-shape cache whose schema
|
|
3342
|
+
apply took the FTS early-return before that column add would make every
|
|
3343
|
+
trigger body fail to resolve. Such a cache keeps today's whole-history
|
|
3344
|
+
sweep, which is correct, just not incremental.
|
|
3345
|
+
"""
|
|
3346
|
+
cols = {
|
|
3347
|
+
str(row[1]) for row in conn.execute(
|
|
3348
|
+
"PRAGMA table_info(quota_window_snapshots)")
|
|
3349
|
+
}
|
|
3350
|
+
if not cols or "canonical_resets_at_utc" not in cols:
|
|
3351
|
+
return
|
|
3352
|
+
for statement in _codex_quota_ledger_ddl():
|
|
3353
|
+
conn.execute(statement)
|
|
3354
|
+
|
|
3355
|
+
|
|
3103
3356
|
# === Region 7b2: Eager cache-migration trigger (V4 — same-invocation 008 apply) ===
|
|
3104
3357
|
|
|
3105
3358
|
|
|
@@ -3573,6 +3826,29 @@ def _apply_cache_schema(conn: sqlite3.Connection) -> None:
|
|
|
3573
3826
|
add_column_if_missing(conn, "session_files", "account_key", "TEXT")
|
|
3574
3827
|
add_column_if_missing(conn, "codex_session_entries", "account_key", "TEXT")
|
|
3575
3828
|
add_column_if_missing(conn, "codex_session_files", "account_key", "TEXT")
|
|
3829
|
+
# Public #5: whether ingestion actually reached the stored scan target.
|
|
3830
|
+
#
|
|
3831
|
+
# `_write_codex_file_batch` persists the file's full observed `st_size`
|
|
3832
|
+
# alongside whatever `final_offset` ingestion reached, and the delta
|
|
3833
|
+
# detector skips on `size == prev_size` without consulting the offset. A
|
|
3834
|
+
# hook-budgeted mid-file stop under that representation would make the
|
|
3835
|
+
# unread suffix permanently invisible on any rollout that never grows
|
|
3836
|
+
# again. This flag is what lets detection consult completeness first.
|
|
3837
|
+
#
|
|
3838
|
+
# DEFAULT 1 so every pre-existing row reads as complete, preserving today's
|
|
3839
|
+
# behaviour exactly — only a budgeted partial stop writes 0. NOT NULL with a
|
|
3840
|
+
# non-null default is a metadata-only ALTER in SQLite, so this does not
|
|
3841
|
+
# rewrite a large codex_session_files.
|
|
3842
|
+
#
|
|
3843
|
+
# It sits HERE, immediately after the last previously-added
|
|
3844
|
+
# codex_session_files column, and NOT beside last_turn_id where it reads
|
|
3845
|
+
# more naturally: ALTER TABLE ADD COLUMN appends, so an existing cache would
|
|
3846
|
+
# receive it after account_key while a fresh one built it before, and the
|
|
3847
|
+
# two would disagree on ordinal (the #195 hazard, stated there in the same
|
|
3848
|
+
# words).
|
|
3849
|
+
add_column_if_missing(
|
|
3850
|
+
conn, "codex_session_files", "ingest_complete",
|
|
3851
|
+
"INTEGER NOT NULL DEFAULT 1")
|
|
3576
3852
|
add_column_if_missing(conn, "quota_window_snapshots", "account_key", "TEXT")
|
|
3577
3853
|
# #195: the cache-write TTL split. NULLable with NO DEFAULT — NULL is the
|
|
3578
3854
|
# "split unknown" sentinel a pre-#195 row produces for free, and a real
|
|
@@ -3692,6 +3968,15 @@ def _apply_cache_schema(conn: sqlite3.Connection) -> None:
|
|
|
3692
3968
|
# backfill over existing history.
|
|
3693
3969
|
add_column_if_missing(
|
|
3694
3970
|
conn, "quota_window_snapshots", "canonical_resets_at_utc", "TEXT")
|
|
3971
|
+
# Public #5: the index the exact physical-group filter seeks. Kept OUT of
|
|
3972
|
+
# the top executescript for the same reason as idx_entries_mutation_seq — on
|
|
3973
|
+
# an existing DB the CREATE TABLE there is a no-op, so observed_model /
|
|
3974
|
+
# canonical_resets_at_utc do not exist yet and an index over them would
|
|
3975
|
+
# raise; it must follow the add_column_if_missing calls above. Still BEFORE
|
|
3976
|
+
# the legacy-FTS early-return, so an old-shape cache.db receives it.
|
|
3977
|
+
_apply_codex_quota_group_index(conn)
|
|
3978
|
+
_apply_codex_quota_unresolved_model_index(conn)
|
|
3979
|
+
_apply_codex_quota_change_ledger(conn)
|
|
3695
3980
|
conn.execute(
|
|
3696
3981
|
"CREATE INDEX IF NOT EXISTS idx_session_files_session_id "
|
|
3697
3982
|
"ON session_files(session_id)"
|
|
@@ -3844,6 +4129,12 @@ def _apply_conversations_schema(conn: sqlite3.Connection) -> None:
|
|
|
3844
4129
|
DROP TABLE IF EXISTS codex_session_entries;
|
|
3845
4130
|
DROP TABLE IF EXISTS codex_session_files;
|
|
3846
4131
|
DROP TABLE IF EXISTS quota_window_snapshots;
|
|
4132
|
+
-- Public #5: the quota change ledger is an accounting concern that
|
|
4133
|
+
-- rides in via _apply_cache_schema above. Dropping the snapshots table
|
|
4134
|
+
-- already removed its triggers (SQLite drops a table's triggers with
|
|
4135
|
+
-- it), so this only clears the now-orphan ledger and keeps
|
|
4136
|
+
-- conversations.db transcripts-only.
|
|
4137
|
+
DROP TABLE IF EXISTS quota_window_change_log;
|
|
3847
4138
|
DROP TABLE IF EXISTS codex_conversation_threads;
|
|
3848
4139
|
DROP TABLE IF EXISTS codex_source_roots;
|
|
3849
4140
|
-- #416: the Codex attribution map is a cache.db accounting concern; it
|
|
@@ -3973,6 +4264,51 @@ def _conv_002_codex_thread_source_inference_replay(
|
|
|
3973
4264
|
_release_cache_db_writer_flocks(held)
|
|
3974
4265
|
|
|
3975
4266
|
|
|
4267
|
+
@conversations_migration("003_background_mcp_result_replay")
|
|
4268
|
+
def _conv_003_background_mcp_result_replay(conn: sqlite3.Connection) -> None:
|
|
4269
|
+
"""Arm the byte-zero replay that recovers backgrounded-MCP results.
|
|
4270
|
+
|
|
4271
|
+
Spec:
|
|
4272
|
+
``docs/superpowers/specs/2026-07-31-background-mcp-result-recovery-design.md``
|
|
4273
|
+
§4.
|
|
4274
|
+
|
|
4275
|
+
Claude Code moved MCP dispatch to the background after 120s, and the parser
|
|
4276
|
+
dropped the resulting attachment record — so every such response is missing
|
|
4277
|
+
from already-ingested history. The parser now promotes it, but only for
|
|
4278
|
+
bytes read AFTER the fix; this marker makes the next non-targeted
|
|
4279
|
+
``sync_claude_conversations`` re-walk the Claude JSONL corpus from offset
|
|
4280
|
+
zero so existing sessions recover too.
|
|
4281
|
+
|
|
4282
|
+
This belongs in conversations.db, NOT cache.db: transcript rows and their
|
|
4283
|
+
replay markers live here, and the Claude conversation synchronizer never
|
|
4284
|
+
writes cache.db — a cache migration would arm a flag the conversation walker
|
|
4285
|
+
never reads.
|
|
4286
|
+
|
|
4287
|
+
Writes a marker ONLY; it clears no table. The key is DISTINCT from every
|
|
4288
|
+
existing reingest flag so a partially-completed replay stays recoverable —
|
|
4289
|
+
reusing one would conflate two enrichments.
|
|
4290
|
+
|
|
4291
|
+
Takes the Claude provider flock first and DEFERS on contention, the way
|
|
4292
|
+
conversations 002 does for Codex. Idempotency comes from physical uniqueness
|
|
4293
|
+
on ``(source_path, byte_offset)`` plus the replay's delete-then-reinsert per
|
|
4294
|
+
source file; re-running this handler rewrites the same marker. NO self-stamp
|
|
4295
|
+
— the dispatcher central-stamps on a clean return (#140).
|
|
4296
|
+
|
|
4297
|
+
A ``--no-sync`` reader stays pre-backfill until the next full conversation
|
|
4298
|
+
sync; the cost is one full re-walk of the Claude JSONL corpus.
|
|
4299
|
+
"""
|
|
4300
|
+
import _cctally_cache
|
|
4301
|
+
|
|
4302
|
+
held = _acquire_conversations_db_claude_provider_flock(
|
|
4303
|
+
conn, migration="conversations 003 background_mcp replay")
|
|
4304
|
+
try:
|
|
4305
|
+
_set_cache_meta(
|
|
4306
|
+
conn, _cctally_cache.CONVERSATION_BACKGROUND_MCP_REINGEST_KEY, "1")
|
|
4307
|
+
conn.commit()
|
|
4308
|
+
finally:
|
|
4309
|
+
_release_cache_db_writer_flocks(held)
|
|
4310
|
+
|
|
4311
|
+
|
|
3976
4312
|
# #177 S6: the consolidated multi-column external-content FTS5 table that
|
|
3977
4313
|
# replaces the old conversation_fts(text) + conversation_fts_aux(search_aux)
|
|
3978
4314
|
# pair. The three column names MUST match the conversation_messages columns BY
|
|
@@ -4769,6 +5105,47 @@ def _acquire_conversations_db_codex_provider_flock(
|
|
|
4769
5105
|
return held
|
|
4770
5106
|
|
|
4771
5107
|
|
|
5108
|
+
def _acquire_conversations_db_claude_provider_flock(
|
|
5109
|
+
conn: sqlite3.Connection,
|
|
5110
|
+
*,
|
|
5111
|
+
migration: str,
|
|
5112
|
+
) -> list[int]:
|
|
5113
|
+
"""Take the ``<conversations.db>.lock`` sibling, or DEFER.
|
|
5114
|
+
|
|
5115
|
+
The CLAUDE analogue of ``_acquire_conversations_db_codex_provider_flock``.
|
|
5116
|
+
It derives ``<main-db-file>.lock`` (via ``_cache_db_lock_path_for_conn``,
|
|
5117
|
+
which is store-agnostic) rather than the ``.codex.lock`` sibling — for a
|
|
5118
|
+
conversations connection that resolves to exactly the path
|
|
5119
|
+
``sync_claude_conversations`` serializes on
|
|
5120
|
+
(``_cctally_core.CONVERSATIONS_LOCK_PATH``).
|
|
5121
|
+
|
|
5122
|
+
The conversations dispatcher runs inside ``_conversations_open_guarded``,
|
|
5123
|
+
which holds ``CONVERSATIONS_LOCK_MAINTENANCE_PATH`` only SHARED, so without
|
|
5124
|
+
this a marker can be armed in the middle of a walk that already read it as
|
|
5125
|
+
absent — and that walk's completion clears EVERY reingest flag, consuming a
|
|
5126
|
+
backfill it never performed. Deferring leaves the migration pending, so it
|
|
5127
|
+
arms cleanly at the next open.
|
|
5128
|
+
"""
|
|
5129
|
+
provider_path = _cache_db_lock_path_for_conn(conn)
|
|
5130
|
+
if provider_path is None:
|
|
5131
|
+
return []
|
|
5132
|
+
|
|
5133
|
+
from _lib_cache_writer_lock import acquire_ordered_flocks
|
|
5134
|
+
|
|
5135
|
+
try:
|
|
5136
|
+
held = acquire_ordered_flocks([(provider_path, fcntl.LOCK_EX)])
|
|
5137
|
+
except OSError as exc:
|
|
5138
|
+
raise MigrationGateNotMet(
|
|
5139
|
+
f"conversations.db Claude lock unavailable; deferring {migration}"
|
|
5140
|
+
) from exc
|
|
5141
|
+
if held is None:
|
|
5142
|
+
raise MigrationGateNotMet(
|
|
5143
|
+
f"conversations.db Claude lock held by a concurrent Claude "
|
|
5144
|
+
f"conversation sync; deferring {migration}"
|
|
5145
|
+
)
|
|
5146
|
+
return held
|
|
5147
|
+
|
|
5148
|
+
|
|
4772
5149
|
def _release_cache_db_writer_flocks(held: list[int]) -> None:
|
|
4773
5150
|
from _lib_cache_writer_lock import release_cache_writer_flocks
|
|
4774
5151
|
|
|
@@ -6257,6 +6634,255 @@ def _035_codex_thread_source_inference_replay(conn: sqlite3.Connection) -> None:
|
|
|
6257
6634
|
conn.commit()
|
|
6258
6635
|
|
|
6259
6636
|
|
|
6637
|
+
@cache_migration("036_codex_quota_window_identity_index")
|
|
6638
|
+
def _036_codex_quota_window_identity_index(conn: sqlite3.Connection) -> None:
|
|
6639
|
+
"""Public #5: land the Codex quota window-identity covering index.
|
|
6640
|
+
|
|
6641
|
+
The index itself is created by ``_apply_cache_schema`` (the repo's
|
|
6642
|
+
index-addition rule). This migration exists because that schema apply is
|
|
6643
|
+
VERSION-GATED: a steady-state open compares ``PRAGMA user_version`` against
|
|
6644
|
+
``len(_CACHE_MIGRATIONS)`` and skips the whole DDL pass when they match
|
|
6645
|
+
(``_cctally_store.schema_current``). Registering here bumps the head, so an
|
|
6646
|
+
already-current install re-runs the schema apply and gains the index. Same
|
|
6647
|
+
mechanism migrations 029/031/032 rely on.
|
|
6648
|
+
|
|
6649
|
+
The idempotent DDL below is the handler's self-contained copy, and it is
|
|
6650
|
+
what the per-migration golden exercises (the golden's ``pre.sqlite`` is a
|
|
6651
|
+
genuine 035-head install that predates the index).
|
|
6652
|
+
|
|
6653
|
+
The two columns the index reaches beyond the original table DDL —
|
|
6654
|
+
``observed_model`` and ``canonical_resets_at_utc`` — are guarded rather than
|
|
6655
|
+
assumed: a legacy-shape cache whose FTS early-return fired before those
|
|
6656
|
+
``add_column_if_missing`` calls would otherwise raise ``no such column``
|
|
6657
|
+
here and fail the migration. Only ``canonical_resets_at_utc`` is in the
|
|
6658
|
+
index, so that is the one probed.
|
|
6659
|
+
|
|
6660
|
+
SUPERSEDED by 040. Measurement against a real-scale store disqualified this
|
|
6661
|
+
index on both counts it was meant to serve — it cannot seek the group
|
|
6662
|
+
filter's ``unixepoch(COALESCE(...))`` reset member, and it is not covering
|
|
6663
|
+
for the full-sweep load either — so 040 replaces it with an expression index
|
|
6664
|
+
and drops it. The handler is kept verbatim because an install that has not
|
|
6665
|
+
yet reached 040 still runs it, and its golden pins that behaviour; the end
|
|
6666
|
+
state after 040 is what matters.
|
|
6667
|
+
|
|
6668
|
+
Re-running is a no-op. NO self-stamp — the dispatcher central-stamps on a
|
|
6669
|
+
clean return (#140).
|
|
6670
|
+
"""
|
|
6671
|
+
cols = {
|
|
6672
|
+
str(row[1]) for row in conn.execute(
|
|
6673
|
+
"PRAGMA table_info(quota_window_snapshots)")
|
|
6674
|
+
}
|
|
6675
|
+
if "canonical_resets_at_utc" not in cols:
|
|
6676
|
+
return
|
|
6677
|
+
conn.execute(
|
|
6678
|
+
"CREATE INDEX IF NOT EXISTS idx_qws_window_ident "
|
|
6679
|
+
"ON quota_window_snapshots("
|
|
6680
|
+
" source, source_root_key, logical_limit_key, observed_slot,"
|
|
6681
|
+
" window_minutes, resets_at_utc, canonical_resets_at_utc,"
|
|
6682
|
+
" captured_at_utc, used_percent, id)"
|
|
6683
|
+
)
|
|
6684
|
+
conn.commit()
|
|
6685
|
+
|
|
6686
|
+
|
|
6687
|
+
@cache_migration("037_codex_quota_change_ledger")
|
|
6688
|
+
def _037_codex_quota_change_ledger(conn: sqlite3.Connection) -> None:
|
|
6689
|
+
"""Public #5: land the Codex quota change ledger and its triggers.
|
|
6690
|
+
|
|
6691
|
+
Same version-gate reason as 036: ``_apply_cache_schema`` creates both, but a
|
|
6692
|
+
steady-state open at the registry head skips that whole DDL pass, so an
|
|
6693
|
+
install that never re-runs it would silently keep mutating
|
|
6694
|
+
``quota_window_snapshots`` with no ledger behind it — and the projector
|
|
6695
|
+
would then believe nothing had changed. Registering here bumps the head and
|
|
6696
|
+
forces the apply.
|
|
6697
|
+
|
|
6698
|
+
Delegates to the same ``_apply_codex_quota_change_ledger`` production uses;
|
|
6699
|
+
re-implementing the DDL here would let the migration and the schema drift,
|
|
6700
|
+
and a drifted trigger is a silent-skip, not an error.
|
|
6701
|
+
|
|
6702
|
+
The ledger deliberately starts EMPTY. It is a change log, not a snapshot of
|
|
6703
|
+
existing state — an install whose watermark has never been set has no
|
|
6704
|
+
consumed range, and the projector's first pass on a fresh watermark is a
|
|
6705
|
+
full sweep anyway.
|
|
6706
|
+
|
|
6707
|
+
Re-running is a no-op (every statement is IF NOT EXISTS). NO self-stamp —
|
|
6708
|
+
the dispatcher central-stamps on a clean return (#140).
|
|
6709
|
+
"""
|
|
6710
|
+
_apply_codex_quota_change_ledger(conn)
|
|
6711
|
+
conn.commit()
|
|
6712
|
+
|
|
6713
|
+
|
|
6714
|
+
@cache_migration("038_codex_session_files_ingest_complete")
|
|
6715
|
+
def _038_codex_session_files_ingest_complete(conn: sqlite3.Connection) -> None:
|
|
6716
|
+
"""Public #5: land ``codex_session_files.ingest_complete``.
|
|
6717
|
+
|
|
6718
|
+
The column itself is a plain addition, so it lands through
|
|
6719
|
+
``add_column_if_missing`` in ``_apply_cache_schema`` (the idempotent guard
|
|
6720
|
+
pattern — no marker, no version). This migration exists to bump the registry
|
|
6721
|
+
head so a steady-state install re-runs the version-gated schema apply and
|
|
6722
|
+
actually gains it, the same mechanism 029/031/032/036 rely on.
|
|
6723
|
+
|
|
6724
|
+
There is deliberately NO backfill. ``DEFAULT 1`` already reads every
|
|
6725
|
+
pre-existing row as complete, which is exactly today's behaviour: before
|
|
6726
|
+
the budgeted ingest exists, every committed file WAS scanned to its stored
|
|
6727
|
+
target. Writing 0 anywhere here would strand real history behind a resume
|
|
6728
|
+
that never happens.
|
|
6729
|
+
|
|
6730
|
+
Re-running is a no-op. NO self-stamp — the dispatcher central-stamps on a
|
|
6731
|
+
clean return (#140).
|
|
6732
|
+
"""
|
|
6733
|
+
add_column_if_missing(
|
|
6734
|
+
conn, "codex_session_files", "ingest_complete",
|
|
6735
|
+
"INTEGER NOT NULL DEFAULT 1")
|
|
6736
|
+
conn.commit()
|
|
6737
|
+
|
|
6738
|
+
|
|
6739
|
+
# The read-time fallback ``load_codex_quota_observations`` used before public #5:
|
|
6740
|
+
# the nearest preceding accounting model at or before the snapshot's byte offset,
|
|
6741
|
+
# within the SAME rollout. Reproduced here VERBATIM — the backfill is only
|
|
6742
|
+
# provably equivalent to the behaviour it replaces if it computes the identical
|
|
6743
|
+
# expression, so this string is the contract, not a paraphrase of it.
|
|
6744
|
+
_QUOTA_OBSERVED_MODEL_LOOKUP_SQL = """
|
|
6745
|
+
(SELECT entries.model FROM codex_session_entries AS entries
|
|
6746
|
+
WHERE entries.source_path = quota_window_snapshots.source_path
|
|
6747
|
+
AND entries.line_offset <= quota_window_snapshots.line_offset
|
|
6748
|
+
ORDER BY entries.line_offset DESC LIMIT 1)
|
|
6749
|
+
"""
|
|
6750
|
+
|
|
6751
|
+
# The ``IS NOT NULL`` guard repeats the same expression rather than widening the
|
|
6752
|
+
# UPDATE to every unstamped row. Without it a row the lookup cannot resolve is
|
|
6753
|
+
# "updated" from NULL to NULL: the result is identical, but the UPDATE trigger
|
|
6754
|
+
# fires on it, so the handler dirties a window it did not change AND stops being
|
|
6755
|
+
# a true no-op on re-run — it would append fresh ledger entries on every
|
|
6756
|
+
# markerless retry. Equivalent by construction: a row skipped by the guard is
|
|
6757
|
+
# exactly a row the SET would have written NULL to.
|
|
6758
|
+
_QUOTA_OBSERVED_MODEL_BACKFILL_SQL = f"""
|
|
6759
|
+
UPDATE quota_window_snapshots
|
|
6760
|
+
SET observed_model = {_QUOTA_OBSERVED_MODEL_LOOKUP_SQL}
|
|
6761
|
+
WHERE source = 'codex' AND observed_model IS NULL
|
|
6762
|
+
AND {_QUOTA_OBSERVED_MODEL_LOOKUP_SQL} IS NOT NULL
|
|
6763
|
+
"""
|
|
6764
|
+
|
|
6765
|
+
|
|
6766
|
+
def backfill_codex_quota_observed_model(conn: sqlite3.Connection) -> int:
|
|
6767
|
+
"""Resolve unstamped ``quota_window_snapshots.observed_model`` in place.
|
|
6768
|
+
|
|
6769
|
+
Returns the number of rows the resolution actually changed, so a caller can
|
|
6770
|
+
decide whether the Codex physical mutation sequence has to advance.
|
|
6771
|
+
|
|
6772
|
+
Shared by cache migration 039 and by the tail of ``sync_codex_cache``. The
|
|
6773
|
+
migration alone is not sufficient, because the #373 Spark-pool guarantee
|
|
6774
|
+
then holds only where 039 ran and two supported paths skip it:
|
|
6775
|
+
``cctally db skip 039_…``, and a fresh cache repopulated from the journal
|
|
6776
|
+
(a fresh install fast-stamps every migration handler without invoking it,
|
|
6777
|
+
and the journal cache leg re-materializes rows carrying whatever
|
|
6778
|
+
``observed_model`` was journaled — NULL for anything captured before the
|
|
6779
|
+
column existed). Running it at the end of every Codex sync closes both:
|
|
6780
|
+
the statement only considers rows that are still NULL AND whose lookup
|
|
6781
|
+
resolves, so on a healthy store it is a near-no-op.
|
|
6782
|
+
|
|
6783
|
+
Does NOT commit — the caller owns the transaction boundary.
|
|
6784
|
+
"""
|
|
6785
|
+
quota_cols = {
|
|
6786
|
+
str(row[1]) for row in conn.execute(
|
|
6787
|
+
"PRAGMA table_info(quota_window_snapshots)")
|
|
6788
|
+
}
|
|
6789
|
+
if "observed_model" not in quota_cols:
|
|
6790
|
+
# A legacy-shape cache whose schema apply never reached the column.
|
|
6791
|
+
# Nothing to backfill; the reader's unscoped default is unchanged.
|
|
6792
|
+
return 0
|
|
6793
|
+
entry_cols = {
|
|
6794
|
+
str(row[1]) for row in conn.execute(
|
|
6795
|
+
"PRAGMA table_info(codex_session_entries)")
|
|
6796
|
+
}
|
|
6797
|
+
if not {"source_path", "line_offset", "model"} <= entry_cols:
|
|
6798
|
+
# No accounting corpus to resolve against — the fallback could not have
|
|
6799
|
+
# resolved anything either, so leaving every row NULL IS the equivalent
|
|
6800
|
+
# result.
|
|
6801
|
+
return 0
|
|
6802
|
+
return int(conn.execute(_QUOTA_OBSERVED_MODEL_BACKFILL_SQL).rowcount)
|
|
6803
|
+
|
|
6804
|
+
|
|
6805
|
+
@cache_migration("039_codex_quota_observed_model_backfill")
|
|
6806
|
+
def _039_codex_quota_observed_model_backfill(conn: sqlite3.Connection) -> None:
|
|
6807
|
+
"""Public #5: make ``quota_window_snapshots`` the complete dependency set.
|
|
6808
|
+
|
|
6809
|
+
The change ledger records mutations of ``quota_window_snapshots``, which is
|
|
6810
|
+
sufficient only if nothing outside that table can change how a window is
|
|
6811
|
+
interpreted. Until now something could: with a NULL ``observed_model`` the
|
|
6812
|
+
loader fell back to the nearest preceding ``codex_session_entries.model`` at
|
|
6813
|
+
or before the snapshot's ``line_offset``, so an accounting row arriving
|
|
6814
|
+
later could move a window into a different model pool with no quota-row
|
|
6815
|
+
mutation to observe.
|
|
6816
|
+
|
|
6817
|
+
The dependency is ELIMINATED rather than ledgered — extending the triggers
|
|
6818
|
+
to a second table would grow the mechanism's surface for one legacy case.
|
|
6819
|
+
This backfill materializes exactly what the fallback resolved, ingest
|
|
6820
|
+
already stamps the same sticky model forward onto every quota row it emits,
|
|
6821
|
+
and the read-time COALESCE is removed in the same change.
|
|
6822
|
+
|
|
6823
|
+
A row with no determinable model stays NULL and reads as unscoped — which
|
|
6824
|
+
is today's behaviour when both sources are NULL. Nothing is fabricated.
|
|
6825
|
+
|
|
6826
|
+
Idempotent by construction: only rows whose ``observed_model`` is still NULL
|
|
6827
|
+
AND whose lookup actually resolves are considered, so a re-run over its own
|
|
6828
|
+
output writes nothing at all — not even a NULL-to-NULL update, which would
|
|
6829
|
+
fire the ledger trigger and dirty a window that never changed. Its real DML
|
|
6830
|
+
IS ledgered, which is the mechanism working as designed: a migration that
|
|
6831
|
+
rewrites this column no longer has to remember to announce it.
|
|
6832
|
+
|
|
6833
|
+
The migration is the ONE-TIME leg. It is not the whole guarantee: `db skip`
|
|
6834
|
+
and a fresh journal-repopulated cache both bypass it, so ``sync_codex_cache``
|
|
6835
|
+
runs the same helper on every Codex sync.
|
|
6836
|
+
|
|
6837
|
+
NO self-stamp — the dispatcher central-stamps on a clean return (#140).
|
|
6838
|
+
"""
|
|
6839
|
+
backfill_codex_quota_observed_model(conn)
|
|
6840
|
+
conn.commit()
|
|
6841
|
+
|
|
6842
|
+
|
|
6843
|
+
@cache_migration("040_codex_quota_physical_group_index")
|
|
6844
|
+
def _040_codex_quota_physical_group_index(conn: sqlite3.Connection) -> None:
|
|
6845
|
+
"""Public #5: seek the physical group, and stop paying for what does not.
|
|
6846
|
+
|
|
6847
|
+
Creates ``idx_qws_physical_group`` and drops 036's
|
|
6848
|
+
``idx_qws_window_ident``. The reasoning and the numbers live on
|
|
6849
|
+
``_apply_codex_quota_group_index``, which this delegates to so the migration
|
|
6850
|
+
and the schema apply cannot drift; in short, an expression index over
|
|
6851
|
+
``unixepoch(COALESCE(canonical_resets_at_utc, resets_at_utc))`` takes one
|
|
6852
|
+
group's load from 19.7ms to 0.60ms on a 211K-row store, and the index it
|
|
6853
|
+
replaces changed no query plan at all while charging ten columns of write
|
|
6854
|
+
cost per ingested observation.
|
|
6855
|
+
|
|
6856
|
+
Same version-gate reason as 036/037/038: the schema apply that creates the
|
|
6857
|
+
index is skipped outright by a steady-state open, so registering here is
|
|
6858
|
+
what makes an already-current install pick it up.
|
|
6859
|
+
|
|
6860
|
+
Re-running is a no-op (``IF NOT EXISTS`` / ``IF EXISTS``). NO self-stamp —
|
|
6861
|
+
the dispatcher central-stamps on a clean return (#140).
|
|
6862
|
+
"""
|
|
6863
|
+
_apply_codex_quota_group_index(conn)
|
|
6864
|
+
conn.commit()
|
|
6865
|
+
|
|
6866
|
+
|
|
6867
|
+
@cache_migration("041_codex_quota_unresolved_model_index")
|
|
6868
|
+
def _041_codex_quota_unresolved_model_index(conn: sqlite3.Connection) -> None:
|
|
6869
|
+
"""Public #5: stop rescanning 212K rows for six unresolved ones.
|
|
6870
|
+
|
|
6871
|
+
Creates ``idx_qws_unresolved_model``. The reasoning and the numbers live on
|
|
6872
|
+
``_apply_codex_quota_unresolved_model_index``, which this delegates to so
|
|
6873
|
+
the migration and the schema apply cannot drift.
|
|
6874
|
+
|
|
6875
|
+
Same version-gate reason as 036/037/038/040: the schema apply that creates
|
|
6876
|
+
the index is skipped outright by a steady-state open, so registering here is
|
|
6877
|
+
what makes an already-current install pick it up.
|
|
6878
|
+
|
|
6879
|
+
Re-running is a no-op (``IF NOT EXISTS``). NO self-stamp — the dispatcher
|
|
6880
|
+
central-stamps on a clean return (#140).
|
|
6881
|
+
"""
|
|
6882
|
+
_apply_codex_quota_unresolved_model_index(conn)
|
|
6883
|
+
conn.commit()
|
|
6884
|
+
|
|
6885
|
+
|
|
6260
6886
|
# === Region 7d: Stats migration 008_recompute_weekly_cost_snapshots_dedup_fix ===
|
|
6261
6887
|
|
|
6262
6888
|
@stats_migration("008_recompute_weekly_cost_snapshots_dedup_fix")
|