cctally 1.97.0 → 1.99.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 +47 -0
- package/README.md +4 -4
- package/bin/_cctally_account.py +925 -0
- package/bin/_cctally_cache.py +829 -31
- package/bin/_cctally_core.py +52 -0
- package/bin/_cctally_dashboard.py +960 -55
- package/bin/_cctally_dashboard_envelope.py +43 -23
- package/bin/_cctally_dashboard_share.py +50 -1
- package/bin/_cctally_dashboard_sources.py +1580 -131
- package/bin/_cctally_db.py +810 -34
- package/bin/_cctally_doctor.py +184 -1
- package/bin/_cctally_journal.py +732 -76
- package/bin/_cctally_parser.py +65 -0
- package/bin/_cctally_quota.py +896 -17
- package/bin/_cctally_rederive.py +157 -5
- package/bin/_cctally_source_analytics.py +60 -6
- package/bin/_cctally_tui.py +811 -67
- package/bin/_lib_aggregators.py +117 -11
- package/bin/_lib_alert_axes.py +8 -3
- package/bin/_lib_budget.py +60 -0
- package/bin/_lib_codex_window_attribution.py +259 -0
- package/bin/_lib_dashboard_sources.py +488 -19
- package/bin/_lib_doctor.py +71 -0
- package/bin/_lib_journal.py +212 -0
- package/bin/_lib_jsonl.py +6 -0
- package/bin/_lib_rederive.py +8 -0
- package/bin/_lib_snapshot_cache.py +248 -7
- package/bin/_lib_source_analytics.py +20 -2
- package/bin/cctally +25 -0
- package/dashboard/static/assets/index-Bcbm-DNP.js +97 -0
- package/dashboard/static/assets/index-hJP4wlIO.css +1 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +2 -1
- package/dashboard/static/assets/index-BSESoPIK.css +0 -1
- package/dashboard/static/assets/index-DgsMz5hA.js +0 -97
package/bin/_cctally_db.py
CHANGED
|
@@ -76,7 +76,7 @@ import tempfile
|
|
|
76
76
|
import time
|
|
77
77
|
import traceback
|
|
78
78
|
from dataclasses import dataclass
|
|
79
|
-
from typing import Any, Callable
|
|
79
|
+
from typing import Any, Callable, NamedTuple
|
|
80
80
|
|
|
81
81
|
|
|
82
82
|
def _cctally():
|
|
@@ -745,6 +745,42 @@ def _would_block_prod_migration(conn: sqlite3.Connection) -> bool:
|
|
|
745
745
|
return False
|
|
746
746
|
|
|
747
747
|
|
|
748
|
+
def _refuse_prod_migration_before_schema_write(
|
|
749
|
+
conn: sqlite3.Connection,
|
|
750
|
+
registry: "list[Migration]",
|
|
751
|
+
db_label: str,
|
|
752
|
+
) -> None:
|
|
753
|
+
"""Raise ``ProdMigrationRefused`` BEFORE the caller writes policy or DDL.
|
|
754
|
+
|
|
755
|
+
The #142 guard lives inside ``_run_pending_migrations``, which both
|
|
756
|
+
cache-open paths reach only AFTER ``apply_policy``, ``_apply_cache_schema``
|
|
757
|
+
and the ``last_total_tokens`` ALTER-plus-purge. A dev-checkout binary
|
|
758
|
+
pointed at the real prod dir therefore modified the production schema and
|
|
759
|
+
only then refused (#566). This preflight is the same decision, evaluated
|
|
760
|
+
where nothing has been written yet.
|
|
761
|
+
|
|
762
|
+
The conditions are the dispatcher's, unchanged: there must be pending
|
|
763
|
+
migrations that would advance ``user_version``
|
|
764
|
+
(``cur_version < len(registry)``), and ``_would_block_prod_migration`` must
|
|
765
|
+
hold — connection-scoped, password-DB-resolved, suppressor-independent,
|
|
766
|
+
with ``CCTALLY_ALLOW_PROD_MIGRATION`` as the escape.
|
|
767
|
+
|
|
768
|
+
A version-AHEAD store is deliberately NOT refused here. That is the #145
|
|
769
|
+
self-heal case, which the dispatcher owns and which cache.db opts into.
|
|
770
|
+
|
|
771
|
+
Callers must already hold cache maintenance exclusive and the global writer
|
|
772
|
+
flock, so the version this reads is the one the upgrade would act on.
|
|
773
|
+
"""
|
|
774
|
+
cur_version = conn.execute("PRAGMA user_version").fetchone()[0]
|
|
775
|
+
if cur_version >= len(registry):
|
|
776
|
+
return
|
|
777
|
+
if not _would_block_prod_migration(conn):
|
|
778
|
+
return
|
|
779
|
+
raise ProdMigrationRefused(
|
|
780
|
+
db_label, _first_pending_migration_name(conn, registry, cur_version)
|
|
781
|
+
)
|
|
782
|
+
|
|
783
|
+
|
|
748
784
|
def _would_block_prod_stats(path: pathlib.Path) -> bool:
|
|
749
785
|
"""Path-based sibling of ``_would_block_prod_migration`` for the stats.db
|
|
750
786
|
classifier-gated auto-heal + ``db rebuild --db stats`` (spec §6.3, issue
|
|
@@ -3843,6 +3879,25 @@ def _apply_codex_quota_unresolved_model_index(conn: sqlite3.Connection) -> None:
|
|
|
3843
3879
|
conn.execute(_QUOTA_UNRESOLVED_MODEL_INDEX_DDL)
|
|
3844
3880
|
|
|
3845
3881
|
|
|
3882
|
+
def _apply_codex_entries_root_path_index(conn: sqlite3.Connection) -> None:
|
|
3883
|
+
"""Create the per-file alias join index (#566).
|
|
3884
|
+
|
|
3885
|
+
``_codex_conversation_metadata`` matches on (source_root_key, source_path).
|
|
3886
|
+
``idx_codex_entries_source_root`` cannot serve it: a machine normally has
|
|
3887
|
+
ONE provider root, so a root-only search visits every entry row for every
|
|
3888
|
+
file and the join costs files x entries on every dashboard snapshot build.
|
|
3889
|
+
Measured on a real store, the alias query alone went 57.20s -> 0.208s.
|
|
3890
|
+
|
|
3891
|
+
Called from BOTH ``_apply_cache_schema`` and cache migration 042 so the two
|
|
3892
|
+
delivery paths cannot drift. Creating it on a 153K-row store costs 0.23s;
|
|
3893
|
+
re-running is free.
|
|
3894
|
+
"""
|
|
3895
|
+
conn.execute(
|
|
3896
|
+
"CREATE INDEX IF NOT EXISTS idx_codex_entries_root_path "
|
|
3897
|
+
"ON codex_session_entries(source_root_key, source_path)"
|
|
3898
|
+
)
|
|
3899
|
+
|
|
3900
|
+
|
|
3846
3901
|
def _apply_codex_quota_change_ledger(conn: sqlite3.Connection) -> None:
|
|
3847
3902
|
"""Create the Codex quota change ledger + triggers, idempotently.
|
|
3848
3903
|
|
|
@@ -3861,9 +3916,486 @@ def _apply_codex_quota_change_ledger(conn: sqlite3.Connection) -> None:
|
|
|
3861
3916
|
conn.execute(statement)
|
|
3862
3917
|
|
|
3863
3918
|
|
|
3919
|
+
_CODEX_ACCOUNTING_SEMANTIC_COLUMNS = (
|
|
3920
|
+
"source_path",
|
|
3921
|
+
"source_root_key",
|
|
3922
|
+
"timestamp_utc",
|
|
3923
|
+
"session_id",
|
|
3924
|
+
"model",
|
|
3925
|
+
"input_tokens",
|
|
3926
|
+
"cached_input_tokens",
|
|
3927
|
+
"output_tokens",
|
|
3928
|
+
"reasoning_output_tokens",
|
|
3929
|
+
"total_tokens",
|
|
3930
|
+
"account_key",
|
|
3931
|
+
"conversation_key",
|
|
3932
|
+
)
|
|
3933
|
+
|
|
3934
|
+
|
|
3935
|
+
def _codex_accounting_ledger_ddl() -> tuple[str, ...]:
|
|
3936
|
+
"""DDL for #582's path-granular Codex accounting dirty signal.
|
|
3937
|
+
|
|
3938
|
+
The counter is independent of ``codex_physical_mutation_seq``: the latter
|
|
3939
|
+
covers quota, cursors and metadata too, while this sequence advances only
|
|
3940
|
+
when an accounting row's rendered semantics move. UPDATE records both row
|
|
3941
|
+
images under one sequence; the UNIQUE constraint collapses them when the
|
|
3942
|
+
physical path itself did not change (the ordinary account-adoption case).
|
|
3943
|
+
"""
|
|
3944
|
+
update_of = ", ".join(_CODEX_ACCOUNTING_SEMANTIC_COLUMNS)
|
|
3945
|
+
changed = " OR ".join(
|
|
3946
|
+
f"OLD.{name} IS NOT NEW.{name}"
|
|
3947
|
+
for name in _CODEX_ACCOUNTING_SEMANTIC_COLUMNS
|
|
3948
|
+
)
|
|
3949
|
+
# The row is installed before every trigger below. Use a plain UPDATE:
|
|
3950
|
+
# SQLite propagates an outer statement's conflict policy into trigger-body
|
|
3951
|
+
# INSERT/UPSERT statements, so a thread `INSERT ... ON CONFLICT DO UPDATE`
|
|
3952
|
+
# could suppress this bump and make the following ledger insert collide
|
|
3953
|
+
# with the prior sequence. UPDATE has no conflict branch for the outer
|
|
3954
|
+
# policy to override.
|
|
3955
|
+
bump = (
|
|
3956
|
+
"UPDATE cache_meta SET value=CAST(value AS INTEGER) + 1 "
|
|
3957
|
+
"WHERE key='codex_accounting_mutation_seq';"
|
|
3958
|
+
)
|
|
3959
|
+
def thread_paths(image: str) -> str:
|
|
3960
|
+
return f"""
|
|
3961
|
+
SELECT DISTINCT entries.source_root_key, entries.source_path
|
|
3962
|
+
FROM codex_session_entries AS entries
|
|
3963
|
+
LEFT JOIN codex_session_files AS files
|
|
3964
|
+
ON files.source_root_key = entries.source_root_key
|
|
3965
|
+
AND files.path = entries.source_path
|
|
3966
|
+
WHERE (
|
|
3967
|
+
entries.source_root_key = {image}.source_root_key
|
|
3968
|
+
AND entries.conversation_key = {image}.conversation_key
|
|
3969
|
+
) OR (
|
|
3970
|
+
files.source_root_key = {image}.source_root_key
|
|
3971
|
+
AND files.last_native_thread_id = {image}.native_thread_id
|
|
3972
|
+
)
|
|
3973
|
+
"""
|
|
3974
|
+
old_thread_paths = thread_paths("OLD")
|
|
3975
|
+
new_thread_paths = thread_paths("NEW")
|
|
3976
|
+
thread_changed = " OR ".join(
|
|
3977
|
+
f"OLD.{name} IS NOT NEW.{name}"
|
|
3978
|
+
for name in (
|
|
3979
|
+
"conversation_key", "source_root_key", "native_thread_id", "cwd",
|
|
3980
|
+
"git_json", "last_seen_utc",
|
|
3981
|
+
)
|
|
3982
|
+
)
|
|
3983
|
+
def file_has_entries(image: str) -> str:
|
|
3984
|
+
return f"""
|
|
3985
|
+
SELECT 1 FROM codex_session_entries AS entries
|
|
3986
|
+
WHERE entries.source_root_key = {image}.source_root_key
|
|
3987
|
+
AND entries.source_path = {image}.path
|
|
3988
|
+
"""
|
|
3989
|
+
old_file_entries = file_has_entries("OLD")
|
|
3990
|
+
new_file_entries = file_has_entries("NEW")
|
|
3991
|
+
file_changed = " OR ".join(
|
|
3992
|
+
f"OLD.{name} IS NOT NEW.{name}"
|
|
3993
|
+
for name in ("path", "source_root_key", "last_native_thread_id")
|
|
3994
|
+
)
|
|
3995
|
+
return (
|
|
3996
|
+
"""
|
|
3997
|
+
CREATE TABLE IF NOT EXISTS codex_accounting_change_log (
|
|
3998
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
3999
|
+
mutation_seq INTEGER NOT NULL,
|
|
4000
|
+
change_kind TEXT NOT NULL
|
|
4001
|
+
CHECK(change_kind IN ('path','full')),
|
|
4002
|
+
source_root_key TEXT,
|
|
4003
|
+
source_path TEXT,
|
|
4004
|
+
UNIQUE(mutation_seq, change_kind, source_root_key, source_path)
|
|
4005
|
+
)
|
|
4006
|
+
""",
|
|
4007
|
+
"CREATE INDEX IF NOT EXISTS idx_codex_accounting_change_mutation "
|
|
4008
|
+
"ON codex_accounting_change_log(mutation_seq, seq)",
|
|
4009
|
+
"INSERT OR IGNORE INTO cache_meta(key, value) VALUES "
|
|
4010
|
+
"('codex_accounting_mutation_seq', '0')",
|
|
4011
|
+
"DROP TRIGGER IF EXISTS trg_codex_accounting_ins",
|
|
4012
|
+
f"""
|
|
4013
|
+
CREATE TRIGGER trg_codex_accounting_ins
|
|
4014
|
+
AFTER INSERT ON codex_session_entries
|
|
4015
|
+
WHEN NOT EXISTS (
|
|
4016
|
+
SELECT 1 FROM cache_meta
|
|
4017
|
+
WHERE key='codex_accounting_bulk_clear'
|
|
4018
|
+
)
|
|
4019
|
+
BEGIN
|
|
4020
|
+
{bump}
|
|
4021
|
+
INSERT OR IGNORE INTO codex_accounting_change_log
|
|
4022
|
+
(mutation_seq, change_kind, source_root_key, source_path)
|
|
4023
|
+
SELECT CAST(value AS INTEGER), 'path',
|
|
4024
|
+
COALESCE(NEW.source_root_key, ''), NEW.source_path
|
|
4025
|
+
FROM cache_meta
|
|
4026
|
+
WHERE key='codex_accounting_mutation_seq';
|
|
4027
|
+
END
|
|
4028
|
+
""",
|
|
4029
|
+
"DROP TRIGGER IF EXISTS trg_codex_accounting_del",
|
|
4030
|
+
f"""
|
|
4031
|
+
CREATE TRIGGER trg_codex_accounting_del
|
|
4032
|
+
AFTER DELETE ON codex_session_entries
|
|
4033
|
+
WHEN NOT EXISTS (
|
|
4034
|
+
SELECT 1 FROM cache_meta
|
|
4035
|
+
WHERE key='codex_accounting_bulk_clear'
|
|
4036
|
+
)
|
|
4037
|
+
BEGIN
|
|
4038
|
+
{bump}
|
|
4039
|
+
INSERT OR IGNORE INTO codex_accounting_change_log
|
|
4040
|
+
(mutation_seq, change_kind, source_root_key, source_path)
|
|
4041
|
+
SELECT CAST(value AS INTEGER), 'path',
|
|
4042
|
+
COALESCE(OLD.source_root_key, ''), OLD.source_path
|
|
4043
|
+
FROM cache_meta
|
|
4044
|
+
WHERE key='codex_accounting_mutation_seq';
|
|
4045
|
+
END
|
|
4046
|
+
""",
|
|
4047
|
+
"DROP TRIGGER IF EXISTS trg_codex_accounting_upd",
|
|
4048
|
+
f"""
|
|
4049
|
+
CREATE TRIGGER trg_codex_accounting_upd
|
|
4050
|
+
AFTER UPDATE OF {update_of} ON codex_session_entries
|
|
4051
|
+
WHEN ({changed}) AND NOT EXISTS (
|
|
4052
|
+
SELECT 1 FROM cache_meta
|
|
4053
|
+
WHERE key='codex_accounting_bulk_clear'
|
|
4054
|
+
)
|
|
4055
|
+
BEGIN
|
|
4056
|
+
{bump}
|
|
4057
|
+
INSERT OR IGNORE INTO codex_accounting_change_log
|
|
4058
|
+
(mutation_seq, change_kind, source_root_key, source_path)
|
|
4059
|
+
SELECT CAST(value AS INTEGER), 'path',
|
|
4060
|
+
COALESCE(OLD.source_root_key, ''), OLD.source_path
|
|
4061
|
+
FROM cache_meta
|
|
4062
|
+
WHERE key='codex_accounting_mutation_seq'
|
|
4063
|
+
UNION
|
|
4064
|
+
SELECT CAST(value AS INTEGER), 'path',
|
|
4065
|
+
COALESCE(NEW.source_root_key, ''), NEW.source_path
|
|
4066
|
+
FROM cache_meta
|
|
4067
|
+
WHERE key='codex_accounting_mutation_seq';
|
|
4068
|
+
END
|
|
4069
|
+
""",
|
|
4070
|
+
"DROP TRIGGER IF EXISTS trg_codex_accounting_thread_ins",
|
|
4071
|
+
f"""
|
|
4072
|
+
CREATE TRIGGER trg_codex_accounting_thread_ins
|
|
4073
|
+
AFTER INSERT ON codex_conversation_threads
|
|
4074
|
+
WHEN EXISTS ({new_thread_paths})
|
|
4075
|
+
BEGIN
|
|
4076
|
+
{bump}
|
|
4077
|
+
INSERT OR IGNORE INTO codex_accounting_change_log
|
|
4078
|
+
(mutation_seq, change_kind, source_root_key, source_path)
|
|
4079
|
+
SELECT CAST(meta.value AS INTEGER), 'path', paths.source_root_key,
|
|
4080
|
+
paths.source_path
|
|
4081
|
+
FROM ({new_thread_paths}) AS paths
|
|
4082
|
+
JOIN cache_meta AS meta
|
|
4083
|
+
ON meta.key='codex_accounting_mutation_seq';
|
|
4084
|
+
END
|
|
4085
|
+
""",
|
|
4086
|
+
"DROP TRIGGER IF EXISTS trg_codex_accounting_thread_del",
|
|
4087
|
+
f"""
|
|
4088
|
+
CREATE TRIGGER trg_codex_accounting_thread_del
|
|
4089
|
+
AFTER DELETE ON codex_conversation_threads
|
|
4090
|
+
WHEN EXISTS ({old_thread_paths})
|
|
4091
|
+
BEGIN
|
|
4092
|
+
{bump}
|
|
4093
|
+
INSERT OR IGNORE INTO codex_accounting_change_log
|
|
4094
|
+
(mutation_seq, change_kind, source_root_key, source_path)
|
|
4095
|
+
SELECT CAST(meta.value AS INTEGER), 'path', paths.source_root_key,
|
|
4096
|
+
paths.source_path
|
|
4097
|
+
FROM ({old_thread_paths}) AS paths
|
|
4098
|
+
JOIN cache_meta AS meta
|
|
4099
|
+
ON meta.key='codex_accounting_mutation_seq';
|
|
4100
|
+
END
|
|
4101
|
+
""",
|
|
4102
|
+
"DROP TRIGGER IF EXISTS trg_codex_accounting_thread_upd",
|
|
4103
|
+
f"""
|
|
4104
|
+
CREATE TRIGGER trg_codex_accounting_thread_upd
|
|
4105
|
+
AFTER UPDATE OF conversation_key, source_root_key, native_thread_id,
|
|
4106
|
+
cwd, git_json, last_seen_utc
|
|
4107
|
+
ON codex_conversation_threads
|
|
4108
|
+
WHEN ({thread_changed})
|
|
4109
|
+
AND (EXISTS ({old_thread_paths}) OR EXISTS ({new_thread_paths}))
|
|
4110
|
+
BEGIN
|
|
4111
|
+
{bump}
|
|
4112
|
+
INSERT OR IGNORE INTO codex_accounting_change_log
|
|
4113
|
+
(mutation_seq, change_kind, source_root_key, source_path)
|
|
4114
|
+
SELECT CAST(meta.value AS INTEGER), 'path', paths.source_root_key,
|
|
4115
|
+
paths.source_path
|
|
4116
|
+
FROM ({old_thread_paths}) AS paths
|
|
4117
|
+
JOIN cache_meta AS meta
|
|
4118
|
+
ON meta.key='codex_accounting_mutation_seq'
|
|
4119
|
+
UNION
|
|
4120
|
+
SELECT CAST(meta.value AS INTEGER), 'path', paths.source_root_key,
|
|
4121
|
+
paths.source_path
|
|
4122
|
+
FROM ({new_thread_paths}) AS paths
|
|
4123
|
+
JOIN cache_meta AS meta
|
|
4124
|
+
ON meta.key='codex_accounting_mutation_seq';
|
|
4125
|
+
END
|
|
4126
|
+
""",
|
|
4127
|
+
"DROP TRIGGER IF EXISTS trg_codex_accounting_file_ins",
|
|
4128
|
+
f"""
|
|
4129
|
+
CREATE TRIGGER trg_codex_accounting_file_ins
|
|
4130
|
+
AFTER INSERT ON codex_session_files
|
|
4131
|
+
WHEN EXISTS ({new_file_entries})
|
|
4132
|
+
BEGIN
|
|
4133
|
+
{bump}
|
|
4134
|
+
INSERT OR IGNORE INTO codex_accounting_change_log
|
|
4135
|
+
(mutation_seq, change_kind, source_root_key, source_path)
|
|
4136
|
+
SELECT CAST(value AS INTEGER), 'path',
|
|
4137
|
+
COALESCE(NEW.source_root_key, ''), NEW.path
|
|
4138
|
+
FROM cache_meta
|
|
4139
|
+
WHERE key='codex_accounting_mutation_seq';
|
|
4140
|
+
END
|
|
4141
|
+
""",
|
|
4142
|
+
"DROP TRIGGER IF EXISTS trg_codex_accounting_file_del",
|
|
4143
|
+
f"""
|
|
4144
|
+
CREATE TRIGGER trg_codex_accounting_file_del
|
|
4145
|
+
AFTER DELETE ON codex_session_files
|
|
4146
|
+
WHEN EXISTS ({old_file_entries})
|
|
4147
|
+
BEGIN
|
|
4148
|
+
{bump}
|
|
4149
|
+
INSERT OR IGNORE INTO codex_accounting_change_log
|
|
4150
|
+
(mutation_seq, change_kind, source_root_key, source_path)
|
|
4151
|
+
SELECT CAST(value AS INTEGER), 'path',
|
|
4152
|
+
COALESCE(OLD.source_root_key, ''), OLD.path
|
|
4153
|
+
FROM cache_meta
|
|
4154
|
+
WHERE key='codex_accounting_mutation_seq';
|
|
4155
|
+
END
|
|
4156
|
+
""",
|
|
4157
|
+
"DROP TRIGGER IF EXISTS trg_codex_accounting_file_upd",
|
|
4158
|
+
f"""
|
|
4159
|
+
CREATE TRIGGER trg_codex_accounting_file_upd
|
|
4160
|
+
AFTER UPDATE OF path, source_root_key, last_native_thread_id
|
|
4161
|
+
ON codex_session_files
|
|
4162
|
+
WHEN ({file_changed})
|
|
4163
|
+
AND (EXISTS ({old_file_entries}) OR EXISTS ({new_file_entries}))
|
|
4164
|
+
BEGIN
|
|
4165
|
+
{bump}
|
|
4166
|
+
INSERT OR IGNORE INTO codex_accounting_change_log
|
|
4167
|
+
(mutation_seq, change_kind, source_root_key, source_path)
|
|
4168
|
+
SELECT CAST(meta.value AS INTEGER), 'path',
|
|
4169
|
+
COALESCE(OLD.source_root_key, ''), OLD.path
|
|
4170
|
+
FROM cache_meta AS meta
|
|
4171
|
+
WHERE meta.key='codex_accounting_mutation_seq'
|
|
4172
|
+
AND EXISTS ({old_file_entries})
|
|
4173
|
+
UNION
|
|
4174
|
+
SELECT CAST(meta.value AS INTEGER), 'path',
|
|
4175
|
+
COALESCE(NEW.source_root_key, ''), NEW.path
|
|
4176
|
+
FROM cache_meta AS meta
|
|
4177
|
+
WHERE meta.key='codex_accounting_mutation_seq'
|
|
4178
|
+
AND EXISTS ({new_file_entries});
|
|
4179
|
+
END
|
|
4180
|
+
""",
|
|
4181
|
+
)
|
|
4182
|
+
|
|
4183
|
+
|
|
4184
|
+
def _apply_codex_accounting_change_ledger(conn: sqlite3.Connection) -> None:
|
|
4185
|
+
"""Install #582's accounting ledger on every schema delivery path."""
|
|
4186
|
+
entry_cols = {
|
|
4187
|
+
str(row[1]) for row in conn.execute(
|
|
4188
|
+
"PRAGMA table_info(codex_session_entries)")
|
|
4189
|
+
}
|
|
4190
|
+
file_cols = {
|
|
4191
|
+
str(row[1]) for row in conn.execute(
|
|
4192
|
+
"PRAGMA table_info(codex_session_files)")
|
|
4193
|
+
}
|
|
4194
|
+
if (
|
|
4195
|
+
not set(_CODEX_ACCOUNTING_SEMANTIC_COLUMNS) <= entry_cols
|
|
4196
|
+
or not {"path", "source_root_key", "last_native_thread_id"} <= file_cols
|
|
4197
|
+
):
|
|
4198
|
+
return
|
|
4199
|
+
for statement in _codex_accounting_ledger_ddl():
|
|
4200
|
+
conn.execute(statement)
|
|
4201
|
+
|
|
4202
|
+
|
|
3864
4203
|
# === Region 7b2: Eager cache-migration trigger (V4 — same-invocation 008 apply) ===
|
|
3865
4204
|
|
|
3866
4205
|
|
|
4206
|
+
class SchemaDeliveryObject(NamedTuple):
|
|
4207
|
+
"""One re-derivable schema object declared by a version-gated apply.
|
|
4208
|
+
|
|
4209
|
+
``kind`` is one of ``table``, ``view``, ``trigger`` or ``index``. The two
|
|
4210
|
+
store registries and their structural tests cover every explicit object of
|
|
4211
|
+
those kinds, including virtual tables while excluding SQLite-created FTS
|
|
4212
|
+
shadow objects.
|
|
4213
|
+
|
|
4214
|
+
``ensure_helper`` names the shared module-level function that owns the DDL,
|
|
4215
|
+
or is ``None`` when the statement is written inline in the schema body.
|
|
4216
|
+
``introduced_by`` names the store migration whose handler delivers the
|
|
4217
|
+
object to a store already stamped at the previous head, or is ``None`` for
|
|
4218
|
+
the archaeologically audited frozen baseline described below.
|
|
4219
|
+
"""
|
|
4220
|
+
|
|
4221
|
+
kind: str
|
|
4222
|
+
name: str
|
|
4223
|
+
ensure_helper: "str | None"
|
|
4224
|
+
introduced_by: "str | None"
|
|
4225
|
+
|
|
4226
|
+
|
|
4227
|
+
#: Every table, view, trigger and index ``_apply_cache_schema`` declares, and
|
|
4228
|
+
#: how it reaches a store that is NOT newly created (#566, #580).
|
|
4229
|
+
#:
|
|
4230
|
+
#: The schema apply is version-gated: ``open_cache_db`` runs it only when the
|
|
4231
|
+
#: store's ``user_version`` differs from ``len(_CACHE_MIGRATIONS)``. So an object
|
|
4232
|
+
#: added to the schema body alone reaches new stores only — which is exactly
|
|
4233
|
+
#: what happened to ``idx_codex_entries_root_path``, absent from a real install
|
|
4234
|
+
#: running the release that shipped it. ``tests/test_cache_schema_delivery.py``
|
|
4235
|
+
#: compares this registry against the schema body in both directions, so an
|
|
4236
|
+
#: unregistered object fails a test instead of reaching users.
|
|
4237
|
+
#:
|
|
4238
|
+
#: THE BASELINE IS FROZEN. The ``introduced_by=None`` records are the objects
|
|
4239
|
+
#: that predate this registry and have no index-delivery migration; requiring
|
|
4240
|
+
#: provenance for them would mean fabricating history. A NEW object may never
|
|
4241
|
+
#: join them: anything added from now on needs a migration, because that is the
|
|
4242
|
+
#: only way an already-current store picks it up. The test pins every baseline
|
|
4243
|
+
#: identity so even a same-kind remove-one/add-one substitution fails.
|
|
4244
|
+
CACHE_REDERIVABLE_OBJECTS: "tuple[SchemaDeliveryObject, ...]" = (
|
|
4245
|
+
# ── frozen baseline: audited objects without handler-owned delivery DDL ──
|
|
4246
|
+
SchemaDeliveryObject("index", "idx_codex_conv_msgs_conversation", None, None),
|
|
4247
|
+
SchemaDeliveryObject("index", "idx_codex_conv_msgs_source", None, None),
|
|
4248
|
+
SchemaDeliveryObject("index", "idx_codex_conv_rollups_recent", None, None),
|
|
4249
|
+
SchemaDeliveryObject("index", "idx_codex_conv_touches_source", None, None),
|
|
4250
|
+
SchemaDeliveryObject("index", "idx_codex_entries_conversation", None, None),
|
|
4251
|
+
SchemaDeliveryObject("index", "idx_codex_entries_session", None, None),
|
|
4252
|
+
SchemaDeliveryObject("index", "idx_codex_entries_source", None, None),
|
|
4253
|
+
SchemaDeliveryObject("index", "idx_codex_entries_source_root", None, None),
|
|
4254
|
+
SchemaDeliveryObject("index", "idx_codex_entries_timestamp", None, None),
|
|
4255
|
+
SchemaDeliveryObject("index", "idx_codex_entries_ts_root_conversation", None, None),
|
|
4256
|
+
SchemaDeliveryObject("index", "idx_codex_events_conversation", None, None),
|
|
4257
|
+
SchemaDeliveryObject("index", "idx_codex_events_timestamp", None, None),
|
|
4258
|
+
SchemaDeliveryObject("index", "idx_codex_files_conversation", None, None),
|
|
4259
|
+
SchemaDeliveryObject("index", "idx_codex_files_source_root", None, None),
|
|
4260
|
+
SchemaDeliveryObject("index", "idx_codex_threads_source_path", None, None),
|
|
4261
|
+
SchemaDeliveryObject("index", "idx_codex_threads_source_root", None, None),
|
|
4262
|
+
SchemaDeliveryObject("index", "idx_conv_session_ts", None, None),
|
|
4263
|
+
SchemaDeliveryObject("index", "idx_conv_session_uuid", None, None),
|
|
4264
|
+
SchemaDeliveryObject("index", "idx_conv_sessions_recent", None, None),
|
|
4265
|
+
SchemaDeliveryObject("index", "idx_conv_source", None, None),
|
|
4266
|
+
SchemaDeliveryObject("index", "idx_conv_turnkey", None, None),
|
|
4267
|
+
SchemaDeliveryObject("index", "idx_entries_dedup", None, None),
|
|
4268
|
+
SchemaDeliveryObject("index", "idx_entries_mutation_seq", None, None),
|
|
4269
|
+
SchemaDeliveryObject("index", "idx_entries_source", None, None),
|
|
4270
|
+
SchemaDeliveryObject("index", "idx_entries_timestamp", None, None),
|
|
4271
|
+
SchemaDeliveryObject("index", "idx_file_touches_path", None, None),
|
|
4272
|
+
SchemaDeliveryObject("index", "idx_quota_window_captured_at", None, None),
|
|
4273
|
+
SchemaDeliveryObject("index", "idx_quota_window_source_root", None, None),
|
|
4274
|
+
SchemaDeliveryObject("index", "idx_session_files_session_id", None, None),
|
|
4275
|
+
SchemaDeliveryObject("table", "cache_meta", None, None),
|
|
4276
|
+
SchemaDeliveryObject("table", "codex_conversation_events", None, None),
|
|
4277
|
+
SchemaDeliveryObject("table", "codex_conversation_file_touches", None, None),
|
|
4278
|
+
SchemaDeliveryObject("table", "codex_conversation_fts", None, None),
|
|
4279
|
+
SchemaDeliveryObject("table", "codex_conversation_messages", None, None),
|
|
4280
|
+
SchemaDeliveryObject("table", "codex_conversation_rollups", None, None),
|
|
4281
|
+
SchemaDeliveryObject("table", "codex_conversation_threads", None, None),
|
|
4282
|
+
SchemaDeliveryObject("table", "codex_session_entries", None, None),
|
|
4283
|
+
SchemaDeliveryObject("table", "codex_session_files", None, None),
|
|
4284
|
+
SchemaDeliveryObject("table", "codex_source_roots", None, None),
|
|
4285
|
+
SchemaDeliveryObject("table", "conversation_ai_titles", None, None),
|
|
4286
|
+
SchemaDeliveryObject("table", "conversation_file_touches", None, None),
|
|
4287
|
+
SchemaDeliveryObject("table", "conversation_fts", None, None),
|
|
4288
|
+
SchemaDeliveryObject("table", "conversation_messages", None, None),
|
|
4289
|
+
SchemaDeliveryObject("table", "conversation_sessions", None, None),
|
|
4290
|
+
SchemaDeliveryObject("table", "conversation_title_fts", None, None),
|
|
4291
|
+
SchemaDeliveryObject("table", "quota_window_snapshots", None, None),
|
|
4292
|
+
SchemaDeliveryObject("table", "session_entries", None, None),
|
|
4293
|
+
SchemaDeliveryObject("table", "session_files", None, None),
|
|
4294
|
+
SchemaDeliveryObject("trigger", "codex_conv_fts_ad", None, None),
|
|
4295
|
+
SchemaDeliveryObject("trigger", "codex_conv_fts_ai", None, None),
|
|
4296
|
+
SchemaDeliveryObject("trigger", "codex_conv_fts_au", None, None),
|
|
4297
|
+
SchemaDeliveryObject("trigger", "conv_fts_ad", None, None),
|
|
4298
|
+
SchemaDeliveryObject("trigger", "conv_fts_ai", None, None),
|
|
4299
|
+
SchemaDeliveryObject("trigger", "conv_fts_au", None, None),
|
|
4300
|
+
SchemaDeliveryObject("trigger", "conv_title_fts_ad", None, None),
|
|
4301
|
+
SchemaDeliveryObject("trigger", "conv_title_fts_ai", None, None),
|
|
4302
|
+
SchemaDeliveryObject("trigger", "conv_title_fts_au", None, None),
|
|
4303
|
+
# ── post-baseline: each migration owns the object's delivery DDL ──
|
|
4304
|
+
SchemaDeliveryObject(
|
|
4305
|
+
"index", "idx_entries_physical", None,
|
|
4306
|
+
"020_session_entries_physical_unique"),
|
|
4307
|
+
SchemaDeliveryObject(
|
|
4308
|
+
"index", "idx_conversation_messages_cwd", None,
|
|
4309
|
+
"021_index_conversation_messages_cwd"),
|
|
4310
|
+
SchemaDeliveryObject(
|
|
4311
|
+
"index", "idx_conversation_messages_model_session", None,
|
|
4312
|
+
"022_index_conversation_messages_model"),
|
|
4313
|
+
SchemaDeliveryObject(
|
|
4314
|
+
"index", "idx_codex_file_accounts_root", None,
|
|
4315
|
+
"031_codex_file_account_map"),
|
|
4316
|
+
SchemaDeliveryObject(
|
|
4317
|
+
"index", "idx_qws_physical_group", "_apply_codex_quota_group_index",
|
|
4318
|
+
"040_codex_quota_physical_group_index"),
|
|
4319
|
+
SchemaDeliveryObject(
|
|
4320
|
+
"index", "idx_qws_unresolved_model",
|
|
4321
|
+
"_apply_codex_quota_unresolved_model_index",
|
|
4322
|
+
"041_codex_quota_unresolved_model_index"),
|
|
4323
|
+
SchemaDeliveryObject(
|
|
4324
|
+
"index", "idx_codex_entries_root_path",
|
|
4325
|
+
"_apply_codex_entries_root_path_index",
|
|
4326
|
+
"042_codex_entries_root_path_index"),
|
|
4327
|
+
SchemaDeliveryObject(
|
|
4328
|
+
"index", "idx_codex_window_attributions_root", None,
|
|
4329
|
+
"043_codex_window_attributions"),
|
|
4330
|
+
SchemaDeliveryObject(
|
|
4331
|
+
"index", "idx_codex_accounting_change_mutation",
|
|
4332
|
+
"_apply_codex_accounting_change_ledger",
|
|
4333
|
+
"044_codex_accounting_change_ledger"),
|
|
4334
|
+
SchemaDeliveryObject(
|
|
4335
|
+
"table", "codex_file_accounts", None,
|
|
4336
|
+
"031_codex_file_account_map"),
|
|
4337
|
+
SchemaDeliveryObject(
|
|
4338
|
+
"table", "codex_file_incarnations", None,
|
|
4339
|
+
"031_codex_file_account_map"),
|
|
4340
|
+
SchemaDeliveryObject(
|
|
4341
|
+
"table", "quota_window_change_log",
|
|
4342
|
+
"_apply_codex_quota_change_ledger",
|
|
4343
|
+
"037_codex_quota_change_ledger"),
|
|
4344
|
+
SchemaDeliveryObject(
|
|
4345
|
+
"table", "codex_window_attributions", None,
|
|
4346
|
+
"043_codex_window_attributions"),
|
|
4347
|
+
SchemaDeliveryObject(
|
|
4348
|
+
"table", "codex_accounting_change_log",
|
|
4349
|
+
"_apply_codex_accounting_change_ledger",
|
|
4350
|
+
"044_codex_accounting_change_ledger"),
|
|
4351
|
+
SchemaDeliveryObject(
|
|
4352
|
+
"trigger", "trg_qws_ledger_del", "_apply_codex_quota_change_ledger",
|
|
4353
|
+
"037_codex_quota_change_ledger"),
|
|
4354
|
+
SchemaDeliveryObject(
|
|
4355
|
+
"trigger", "trg_qws_ledger_ins", "_apply_codex_quota_change_ledger",
|
|
4356
|
+
"037_codex_quota_change_ledger"),
|
|
4357
|
+
SchemaDeliveryObject(
|
|
4358
|
+
"trigger", "trg_qws_ledger_upd", "_apply_codex_quota_change_ledger",
|
|
4359
|
+
"037_codex_quota_change_ledger"),
|
|
4360
|
+
SchemaDeliveryObject(
|
|
4361
|
+
"trigger", "trg_codex_accounting_del",
|
|
4362
|
+
"_apply_codex_accounting_change_ledger",
|
|
4363
|
+
"044_codex_accounting_change_ledger"),
|
|
4364
|
+
SchemaDeliveryObject(
|
|
4365
|
+
"trigger", "trg_codex_accounting_file_del",
|
|
4366
|
+
"_apply_codex_accounting_change_ledger",
|
|
4367
|
+
"044_codex_accounting_change_ledger"),
|
|
4368
|
+
SchemaDeliveryObject(
|
|
4369
|
+
"trigger", "trg_codex_accounting_file_ins",
|
|
4370
|
+
"_apply_codex_accounting_change_ledger",
|
|
4371
|
+
"044_codex_accounting_change_ledger"),
|
|
4372
|
+
SchemaDeliveryObject(
|
|
4373
|
+
"trigger", "trg_codex_accounting_file_upd",
|
|
4374
|
+
"_apply_codex_accounting_change_ledger",
|
|
4375
|
+
"044_codex_accounting_change_ledger"),
|
|
4376
|
+
SchemaDeliveryObject(
|
|
4377
|
+
"trigger", "trg_codex_accounting_ins",
|
|
4378
|
+
"_apply_codex_accounting_change_ledger",
|
|
4379
|
+
"044_codex_accounting_change_ledger"),
|
|
4380
|
+
SchemaDeliveryObject(
|
|
4381
|
+
"trigger", "trg_codex_accounting_thread_del",
|
|
4382
|
+
"_apply_codex_accounting_change_ledger",
|
|
4383
|
+
"044_codex_accounting_change_ledger"),
|
|
4384
|
+
SchemaDeliveryObject(
|
|
4385
|
+
"trigger", "trg_codex_accounting_thread_ins",
|
|
4386
|
+
"_apply_codex_accounting_change_ledger",
|
|
4387
|
+
"044_codex_accounting_change_ledger"),
|
|
4388
|
+
SchemaDeliveryObject(
|
|
4389
|
+
"trigger", "trg_codex_accounting_thread_upd",
|
|
4390
|
+
"_apply_codex_accounting_change_ledger",
|
|
4391
|
+
"044_codex_accounting_change_ledger"),
|
|
4392
|
+
SchemaDeliveryObject(
|
|
4393
|
+
"trigger", "trg_codex_accounting_upd",
|
|
4394
|
+
"_apply_codex_accounting_change_ledger",
|
|
4395
|
+
"044_codex_accounting_change_ledger"),
|
|
4396
|
+
)
|
|
4397
|
+
|
|
4398
|
+
|
|
3867
4399
|
def _apply_cache_schema(conn: sqlite3.Connection) -> None:
|
|
3868
4400
|
"""Single source of cache.db's schema (cctally-dev#93, spec D4).
|
|
3869
4401
|
|
|
@@ -4027,9 +4559,11 @@ def _apply_cache_schema(conn: sqlite3.Connection) -> None:
|
|
|
4027
4559
|
-- reingest), and migration 019 arms a one-time history backfill.
|
|
4028
4560
|
--
|
|
4029
4561
|
-- CRITICAL: this is a PLAIN table with NO dependency on the FTS shape, and
|
|
4030
|
-
-- it is created HERE — inside the
|
|
4031
|
-
-- FTS5 ``legacy_present`` early-return below — so
|
|
4032
|
-
-- regardless of FTS topology.
|
|
4562
|
+
-- it is created HERE — inside the version-gated schema executescript,
|
|
4563
|
+
-- BEFORE the FTS5 ``legacy_present`` early-return below — so every
|
|
4564
|
+
-- schema application creates it regardless of FTS topology. Migration
|
|
4565
|
+
-- 019 supplies the head bump for existing stores. (The I-2 title-FTS
|
|
4566
|
+
-- bug created its vtable
|
|
4033
4567
|
-- AFTER that early-return, so its consumer crashed on a legacy-shape +
|
|
4034
4568
|
-- both-pending upgrade; the file-touches table must not repeat that class.)
|
|
4035
4569
|
CREATE TABLE IF NOT EXISTS conversation_file_touches (
|
|
@@ -4083,10 +4617,10 @@ def _apply_cache_schema(conn: sqlite3.Connection) -> None:
|
|
|
4083
4617
|
ON codex_session_entries(source_path);
|
|
4084
4618
|
|
|
4085
4619
|
-- #294 S1: physical Codex rollout retention. These tables deliberately
|
|
4086
|
-
-- live in the
|
|
4087
|
-
-- topology checks below:
|
|
4088
|
-
-- still gain the S1 tables
|
|
4089
|
-
-- does above.
|
|
4620
|
+
-- live in the version-gated base-schema script, before the legacy-FTS
|
|
4621
|
+
-- topology checks below: when migration 024 bumps the head, an existing
|
|
4622
|
+
-- cache with an old FTS shape must still gain the S1 tables during that
|
|
4623
|
+
-- schema application, just as conversation_file_touches does above.
|
|
4090
4624
|
CREATE TABLE IF NOT EXISTS codex_source_roots (
|
|
4091
4625
|
source_root_key TEXT NOT NULL PRIMARY KEY,
|
|
4092
4626
|
canonical_root_path TEXT NOT NULL UNIQUE,
|
|
@@ -4108,10 +4642,11 @@ def _apply_cache_schema(conn: sqlite3.Connection) -> None:
|
|
|
4108
4642
|
-- records NO ROW AT ALL — "undecided" and "decided: no account" are
|
|
4109
4643
|
-- distinct states and readers must not collapse them.
|
|
4110
4644
|
--
|
|
4111
|
-
-- Both live in the
|
|
4645
|
+
-- Both live in the top-level executescript of the version-gated apply,
|
|
4646
|
+
-- BEFORE the FTS5
|
|
4112
4647
|
-- `legacy_present` early-return below, because they are plain tables
|
|
4113
|
-
-- with no FTS-shape dependency and the ingest path needs them
|
|
4114
|
-
--
|
|
4648
|
+
-- with no FTS-shape dependency and the ingest path needs them whenever
|
|
4649
|
+
-- that apply runs (the `_apply_cache_schema_legacy_early_return_before_new_table`
|
|
4115
4650
|
-- class). Cache migration 031 exists only to bump the registry head so
|
|
4116
4651
|
-- an existing install re-runs this schema apply.
|
|
4117
4652
|
CREATE TABLE IF NOT EXISTS codex_file_incarnations (
|
|
@@ -4131,6 +4666,43 @@ def _apply_cache_schema(conn: sqlite3.Connection) -> None:
|
|
|
4131
4666
|
CREATE INDEX IF NOT EXISTS idx_codex_file_accounts_root
|
|
4132
4667
|
ON codex_file_accounts(root_scope);
|
|
4133
4668
|
|
|
4669
|
+
-- #500 spec §6.1: the derived index over the operator's durable
|
|
4670
|
+
-- attribution assertions. One row per `codex_window_attribution` op,
|
|
4671
|
+
-- keyed by that op's ordinary content id; a
|
|
4672
|
+
-- `codex_window_attribution_retract` op stamps `retracted_by_op_id` on
|
|
4673
|
+
-- each assertion it names.
|
|
4674
|
+
--
|
|
4675
|
+
-- `account_key` is NOT NULL here, unlike `codex_file_accounts`: this is
|
|
4676
|
+
-- the SUBJECT of an operator assertion, and "the operator asserted no
|
|
4677
|
+
-- account" is not a fact. The builder refuses the `unattributed`
|
|
4678
|
+
-- sentinel outright.
|
|
4679
|
+
--
|
|
4680
|
+
-- `raw_resets_at_utc` stores the witness list as canonical JSON, the
|
|
4681
|
+
-- same spelling the journal payload carries. The group binding is the
|
|
4682
|
+
-- four normalized axes plus an INTERSECTION against those witnesses,
|
|
4683
|
+
-- and `canonical_resets_at_utc` is audit-only — it is
|
|
4684
|
+
-- population-dependent (a later bridging observation can union two
|
|
4685
|
+
-- components and retire the anchor), so it is never matched on.
|
|
4686
|
+
-- Cardinality is dozens of rows, so matching happens in Python.
|
|
4687
|
+
--
|
|
4688
|
+
-- Top-level version-gated executescript for the same reason
|
|
4689
|
+
-- `codex_file_accounts` is: cache migration 043 exists
|
|
4690
|
+
-- to bump the registry head and make an existing install re-run it.
|
|
4691
|
+
CREATE TABLE IF NOT EXISTS codex_window_attributions (
|
|
4692
|
+
op_id TEXT PRIMARY KEY,
|
|
4693
|
+
account_key TEXT NOT NULL,
|
|
4694
|
+
source_root_key TEXT NOT NULL,
|
|
4695
|
+
logical_limit_key TEXT NOT NULL,
|
|
4696
|
+
observed_slot TEXT NOT NULL,
|
|
4697
|
+
window_minutes INTEGER NOT NULL,
|
|
4698
|
+
raw_resets_at_utc TEXT NOT NULL,
|
|
4699
|
+
canonical_resets_at_utc TEXT,
|
|
4700
|
+
asserted_at_utc TEXT NOT NULL,
|
|
4701
|
+
retracted_by_op_id TEXT
|
|
4702
|
+
);
|
|
4703
|
+
CREATE INDEX IF NOT EXISTS idx_codex_window_attributions_root
|
|
4704
|
+
ON codex_window_attributions(source_root_key, window_minutes);
|
|
4705
|
+
|
|
4134
4706
|
CREATE TABLE IF NOT EXISTS codex_conversation_threads (
|
|
4135
4707
|
conversation_key TEXT NOT NULL PRIMARY KEY,
|
|
4136
4708
|
source_root_key TEXT NOT NULL,
|
|
@@ -4202,9 +4774,9 @@ def _apply_cache_schema(conn: sqlite3.Connection) -> None:
|
|
|
4202
4774
|
ON codex_conversation_events(timestamp_utc);
|
|
4203
4775
|
|
|
4204
4776
|
-- #294 S6: normalized Codex conversation storage. Like the S1 tables
|
|
4205
|
-
-- above, these live
|
|
4206
|
-
--
|
|
4207
|
-
--
|
|
4777
|
+
-- above, these live before the legacy-FTS topology checks in the
|
|
4778
|
+
-- version-gated base schema; the S6 migration head bump makes existing
|
|
4779
|
+
-- caches run that apply once. The independent Codex FTS layer
|
|
4208
4780
|
-- (_apply_codex_conversation_fts) stands up codex_conversation_fts + its
|
|
4209
4781
|
-- own triggers separately, BEFORE the Claude legacy-FTS early-return, so
|
|
4210
4782
|
-- a legacy-shape Claude cache still gets the Codex search index.
|
|
@@ -4304,22 +4876,19 @@ def _apply_cache_schema(conn: sqlite3.Connection) -> None:
|
|
|
4304
4876
|
)
|
|
4305
4877
|
# #294 S3: the qualified accounting adapter is bounded by timestamp and
|
|
4306
4878
|
# joins only through the S1 root-qualified conversation identity. Keep
|
|
4307
|
-
# this re-derivable index in the
|
|
4308
|
-
#
|
|
4879
|
+
# this re-derivable index in the version-gated schema path. Migration 024's
|
|
4880
|
+
# head bump delivered it to existing caches; future additions need their own
|
|
4881
|
+
# handler-owned delivery path.
|
|
4309
4882
|
conn.execute(
|
|
4310
4883
|
"CREATE INDEX IF NOT EXISTS idx_codex_entries_ts_root_conversation "
|
|
4311
4884
|
"ON codex_session_entries(timestamp_utc, source_root_key, conversation_key)"
|
|
4312
4885
|
)
|
|
4313
4886
|
# The per-file alias join in `_codex_conversation_metadata` matches on
|
|
4314
|
-
# (source_root_key, source_path).
|
|
4315
|
-
#
|
|
4316
|
-
#
|
|
4317
|
-
#
|
|
4318
|
-
|
|
4319
|
-
conn.execute(
|
|
4320
|
-
"CREATE INDEX IF NOT EXISTS idx_codex_entries_root_path "
|
|
4321
|
-
"ON codex_session_entries(source_root_key, source_path)"
|
|
4322
|
-
)
|
|
4887
|
+
# (source_root_key, source_path). #566: this path is NOT unconditional —
|
|
4888
|
+
# `open_cache_db` runs the schema apply only when `user_version` differs
|
|
4889
|
+
# from the migration count — so the index also needs cache migration 042,
|
|
4890
|
+
# which delegates to the same helper.
|
|
4891
|
+
_apply_codex_entries_root_path_index(conn)
|
|
4323
4892
|
# The per-file terminal thread facts seed a later append without rereading
|
|
4324
4893
|
# the prefix. They are nullable for old cache rows; migration 024 never
|
|
4325
4894
|
# fabricates these source facts and instead clears/rederives them.
|
|
@@ -4419,8 +4988,8 @@ def _apply_cache_schema(conn: sqlite3.Connection) -> None:
|
|
|
4419
4988
|
"ON session_entries(mutation_seq, mutation_min_ts)")
|
|
4420
4989
|
# #279 S3 F3: DB-level idempotency backstop mirroring
|
|
4421
4990
|
# codex_session_entries' UNIQUE(source_path, line_offset). Guarded and
|
|
4422
|
-
# OUTSIDE the top executescript:
|
|
4423
|
-
# migration dispatcher, so a legacy cache.db holding historical
|
|
4991
|
+
# OUTSIDE the top executescript: when the version gate opens, this function
|
|
4992
|
+
# runs BEFORE the migration dispatcher, so a legacy cache.db holding historical
|
|
4424
4993
|
# physical-key duplicates must tolerate the index being ABSENT until cache
|
|
4425
4994
|
# migration 020 dedups it — an unguarded CREATE UNIQUE INDEX here would
|
|
4426
4995
|
# brick every open of such a DB before 020 could ever run. Fresh and clean
|
|
@@ -4496,6 +5065,7 @@ def _apply_cache_schema(conn: sqlite3.Connection) -> None:
|
|
|
4496
5065
|
_apply_codex_quota_group_index(conn)
|
|
4497
5066
|
_apply_codex_quota_unresolved_model_index(conn)
|
|
4498
5067
|
_apply_codex_quota_change_ledger(conn)
|
|
5068
|
+
_apply_codex_accounting_change_ledger(conn)
|
|
4499
5069
|
conn.execute(
|
|
4500
5070
|
"CREATE INDEX IF NOT EXISTS idx_session_files_session_id "
|
|
4501
5071
|
"ON session_files(session_id)"
|
|
@@ -4655,6 +5225,7 @@ def _apply_conversations_schema(conn: sqlite3.Connection) -> None:
|
|
|
4655
5225
|
-- it), so this only clears the now-orphan ledger and keeps
|
|
4656
5226
|
-- conversations.db transcripts-only.
|
|
4657
5227
|
DROP TABLE IF EXISTS quota_window_change_log;
|
|
5228
|
+
DROP TABLE IF EXISTS codex_accounting_change_log;
|
|
4658
5229
|
DROP TABLE IF EXISTS codex_conversation_threads;
|
|
4659
5230
|
DROP TABLE IF EXISTS codex_source_roots;
|
|
4660
5231
|
-- #416: the Codex attribution map is a cache.db accounting concern; it
|
|
@@ -4662,6 +5233,13 @@ def _apply_conversations_schema(conn: sqlite3.Connection) -> None:
|
|
|
4662
5233
|
-- rest of the accounting families so conversations.db stays transcripts-only.
|
|
4663
5234
|
DROP TABLE IF EXISTS codex_file_accounts;
|
|
4664
5235
|
DROP TABLE IF EXISTS codex_file_incarnations;
|
|
5236
|
+
-- #500: the operator's window-attribution index is the same kind of
|
|
5237
|
+
-- cache.db accounting concern and rides in the same way. Dropping the
|
|
5238
|
+
-- table also drops idx_codex_window_attributions_root, because SQLite
|
|
5239
|
+
-- drops a table's indexes with it. Guarded by
|
|
5240
|
+
-- tests/test_conversations_transcripts_only.py, which derives its
|
|
5241
|
+
-- expectation from COVERAGE_CACHE_FAMILIES rather than restating a list.
|
|
5242
|
+
DROP TABLE IF EXISTS codex_window_attributions;
|
|
4665
5243
|
|
|
4666
5244
|
CREATE TABLE IF NOT EXISTS conversation_source_files (
|
|
4667
5245
|
path TEXT PRIMARY KEY,
|
|
@@ -4769,6 +5347,72 @@ def _apply_codex_find_projection_schema(conn: sqlite3.Connection) -> None:
|
|
|
4769
5347
|
_set_cache_meta(conn, "codex_find_projection_generation", "0")
|
|
4770
5348
|
|
|
4771
5349
|
|
|
5350
|
+
#: Every surviving table, view, trigger and index declared by
|
|
5351
|
+
#: ``_apply_conversations_schema``, and how it reaches an already-current
|
|
5352
|
+
#: conversations.db (#580). Most transcript objects predate this registry and
|
|
5353
|
+
#: are therefore frozen baseline records. In particular, the three account
|
|
5354
|
+
#: indexes added by ``b3f859fec`` do NOT claim a migration: their DDL was not
|
|
5355
|
+
#: owned by the migration handler and only reached existing stores because a
|
|
5356
|
+
#: later registry-head bump happened to re-run the schema apply. The Codex find
|
|
5357
|
+
#: projection is the one audited family with handler-owned delivery DDL.
|
|
5358
|
+
CONVERSATIONS_REDERIVABLE_OBJECTS: "tuple[SchemaDeliveryObject, ...]" = (
|
|
5359
|
+
# ── frozen baseline: audited objects without handler-owned delivery DDL ──
|
|
5360
|
+
SchemaDeliveryObject(
|
|
5361
|
+
"index", "idx_codex_conv_messages_account_conversation", None, None),
|
|
5362
|
+
SchemaDeliveryObject("index", "idx_codex_conv_msgs_conversation", None, None),
|
|
5363
|
+
SchemaDeliveryObject("index", "idx_codex_conv_msgs_source", None, None),
|
|
5364
|
+
SchemaDeliveryObject("index", "idx_codex_conv_rollups_recent", None, None),
|
|
5365
|
+
SchemaDeliveryObject("index", "idx_codex_conv_touches_source", None, None),
|
|
5366
|
+
SchemaDeliveryObject(
|
|
5367
|
+
"index", "idx_codex_events_account_conversation", None, None),
|
|
5368
|
+
SchemaDeliveryObject("index", "idx_codex_events_conversation", None, None),
|
|
5369
|
+
SchemaDeliveryObject("index", "idx_codex_events_timestamp", None, None),
|
|
5370
|
+
SchemaDeliveryObject("index", "idx_conv_messages_account_session", None, None),
|
|
5371
|
+
SchemaDeliveryObject("index", "idx_conv_session_ts", None, None),
|
|
5372
|
+
SchemaDeliveryObject("index", "idx_conv_session_uuid", None, None),
|
|
5373
|
+
SchemaDeliveryObject("index", "idx_conv_sessions_recent", None, None),
|
|
5374
|
+
SchemaDeliveryObject("index", "idx_conv_source", None, None),
|
|
5375
|
+
SchemaDeliveryObject("index", "idx_conv_turnkey", None, None),
|
|
5376
|
+
SchemaDeliveryObject("index", "idx_conversation_messages_cwd", None, None),
|
|
5377
|
+
SchemaDeliveryObject(
|
|
5378
|
+
"index", "idx_conversation_messages_model_session", None, None),
|
|
5379
|
+
SchemaDeliveryObject("index", "idx_file_touches_path", None, None),
|
|
5380
|
+
SchemaDeliveryObject("table", "cache_meta", None, None),
|
|
5381
|
+
SchemaDeliveryObject("table", "codex_conversation_events", None, None),
|
|
5382
|
+
SchemaDeliveryObject("table", "codex_conversation_file_touches", None, None),
|
|
5383
|
+
SchemaDeliveryObject("table", "codex_conversation_fts", None, None),
|
|
5384
|
+
SchemaDeliveryObject("table", "codex_conversation_messages", None, None),
|
|
5385
|
+
SchemaDeliveryObject("table", "codex_conversation_rollups", None, None),
|
|
5386
|
+
SchemaDeliveryObject("table", "codex_conversation_source_files", None, None),
|
|
5387
|
+
SchemaDeliveryObject("table", "conversation_ai_titles", None, None),
|
|
5388
|
+
SchemaDeliveryObject("table", "conversation_file_touches", None, None),
|
|
5389
|
+
SchemaDeliveryObject("table", "conversation_fts", None, None),
|
|
5390
|
+
SchemaDeliveryObject("table", "conversation_messages", None, None),
|
|
5391
|
+
SchemaDeliveryObject("table", "conversation_sessions", None, None),
|
|
5392
|
+
SchemaDeliveryObject("table", "conversation_source_files", None, None),
|
|
5393
|
+
SchemaDeliveryObject("table", "conversation_title_fts", None, None),
|
|
5394
|
+
SchemaDeliveryObject("trigger", "codex_conv_fts_ad", None, None),
|
|
5395
|
+
SchemaDeliveryObject("trigger", "codex_conv_fts_ai", None, None),
|
|
5396
|
+
SchemaDeliveryObject("trigger", "codex_conv_fts_au", None, None),
|
|
5397
|
+
SchemaDeliveryObject("trigger", "conv_fts_ad", None, None),
|
|
5398
|
+
SchemaDeliveryObject("trigger", "conv_fts_ai", None, None),
|
|
5399
|
+
SchemaDeliveryObject("trigger", "conv_fts_au", None, None),
|
|
5400
|
+
SchemaDeliveryObject("trigger", "conv_title_fts_ad", None, None),
|
|
5401
|
+
SchemaDeliveryObject("trigger", "conv_title_fts_ai", None, None),
|
|
5402
|
+
SchemaDeliveryObject("trigger", "conv_title_fts_au", None, None),
|
|
5403
|
+
# ── post-baseline: migration 004 owns the shared projection helper ──
|
|
5404
|
+
SchemaDeliveryObject(
|
|
5405
|
+
"index", "idx_codex_find_projection_conversation_order",
|
|
5406
|
+
"_apply_codex_find_projection_schema", "004_codex_find_projection"),
|
|
5407
|
+
SchemaDeliveryObject(
|
|
5408
|
+
"table", "codex_find_projection",
|
|
5409
|
+
"_apply_codex_find_projection_schema", "004_codex_find_projection"),
|
|
5410
|
+
SchemaDeliveryObject(
|
|
5411
|
+
"trigger", "codex_find_projection_message_ad",
|
|
5412
|
+
"_apply_codex_find_projection_schema", "004_codex_find_projection"),
|
|
5413
|
+
)
|
|
5414
|
+
|
|
5415
|
+
|
|
4772
5416
|
def _fts5_available(conn: sqlite3.Connection) -> bool:
|
|
4773
5417
|
"""True if this sqlite build can create an FTS5 table. Cheap probe on a
|
|
4774
5418
|
temp table that is created then dropped. Hidden test seam: tests monkeypatch
|
|
@@ -5644,6 +6288,14 @@ def _eagerly_apply_cache_migrations_under_writer_lock() -> None:
|
|
|
5644
6288
|
pass
|
|
5645
6289
|
conn = sqlite3.connect(cache_db_path)
|
|
5646
6290
|
try:
|
|
6291
|
+
# #566: the refusal must precede the DDL it exists to prevent. Both
|
|
6292
|
+
# cache-open paths used to reach the dispatcher's #142 guard only after
|
|
6293
|
+
# the schema apply had already run, so a dev-checkout binary pointed at
|
|
6294
|
+
# the real prod dir modified the production schema and only then
|
|
6295
|
+
# refused. `journal_mode` is itself a persistent write, so this comes
|
|
6296
|
+
# before it.
|
|
6297
|
+
_refuse_prod_migration_before_schema_write(
|
|
6298
|
+
conn, _CACHE_MIGRATIONS, "cache.db")
|
|
5647
6299
|
conn.execute("PRAGMA journal_mode=WAL")
|
|
5648
6300
|
conn.execute("PRAGMA busy_timeout=5000")
|
|
5649
6301
|
# Apply the shared cache.db schema (cctally-dev#93, D4). This is the
|
|
@@ -6338,8 +6990,9 @@ def _011_conversation_promote_command_args(conn: sqlite3.Connection) -> None:
|
|
|
6338
6990
|
@cache_migration("012_create_conversation_ai_titles")
|
|
6339
6991
|
def _012_create_conversation_ai_titles(conn: sqlite3.Connection) -> None:
|
|
6340
6992
|
"""Flag-only arm for #193. The conversation_ai_titles table itself is created
|
|
6341
|
-
by _apply_cache_schema
|
|
6342
|
-
|
|
6993
|
+
by _apply_cache_schema when the version gate opens; this migration's head
|
|
6994
|
+
bump makes existing installs take that path. This migration sets
|
|
6995
|
+
``ai_titles_backfill_pending`` so sync_cache walks all history
|
|
6343
6996
|
once via backfill_ai_titles under the cache.db.lock flock. No data work here
|
|
6344
6997
|
-> the dispatcher's central stamp (#140) marks a complete handler; a fresh
|
|
6345
6998
|
install stamps WITHOUT a populated history (its incremental walk fills the
|
|
@@ -6351,7 +7004,8 @@ def _012_create_conversation_ai_titles(conn: sqlite3.Connection) -> None:
|
|
|
6351
7004
|
@cache_migration("013_create_conversation_sessions")
|
|
6352
7005
|
def _013_create_conversation_sessions(conn: sqlite3.Connection) -> None:
|
|
6353
7006
|
"""Flag-only arm for the conversation_sessions browse-rail rollup. The table
|
|
6354
|
-
is created by _apply_cache_schema
|
|
7007
|
+
is created by _apply_cache_schema when the version gate opens; this
|
|
7008
|
+
migration's head bump makes existing installs take that path. It sets
|
|
6355
7009
|
conversation_sessions_backfill_pending so sync_cache does the one-time full
|
|
6356
7010
|
GROUP BY recompute under the cache.db.lock flock. No data work here — the
|
|
6357
7011
|
dispatcher's central stamp (#140) marks a complete handler; a fresh install
|
|
@@ -6545,7 +7199,8 @@ def _018_create_conversation_title_fts(conn: sqlite3.Connection) -> None:
|
|
|
6545
7199
|
|
|
6546
7200
|
Flag-only arm. The ``conversation_title_fts`` virtual table + its
|
|
6547
7201
|
conv_title_fts_ai/ad/au sync triggers are created by ``_apply_cache_schema``
|
|
6548
|
-
|
|
7202
|
+
when the version gate opens (this migration bumps the head for existing
|
|
7203
|
+
installs) inside the SAME FTS5-available
|
|
6549
7204
|
envelope as the message FTS (P1-6) — so on a no-FTS5 build the table+triggers
|
|
6550
7205
|
are simply absent and a title upsert never rolls back the ingest. This handler
|
|
6551
7206
|
does NO DDL (mirrors 012's flag-only pattern): it just arms the DISTINCT
|
|
@@ -6576,7 +7231,8 @@ def _019_create_conversation_file_touches(conn: sqlite3.Connection) -> None:
|
|
|
6576
7231
|
|
|
6577
7232
|
Flag-only arm (mirrors 018's pattern). The ``conversation_file_touches`` table
|
|
6578
7233
|
+ its ``COLLATE NOCASE`` path index (``idx_file_touches_path``) are created by
|
|
6579
|
-
``_apply_cache_schema``
|
|
7234
|
+
``_apply_cache_schema`` when the version gate opens (this migration bumps
|
|
7235
|
+
the head for existing installs) — and
|
|
6580
7236
|
CRITICALLY before the FTS5 ``legacy_present`` early-return, since the table is
|
|
6581
7237
|
plain and has NO dependency on the FTS shape (so a legacy-shape upgrade still
|
|
6582
7238
|
gets it). The NOCASE collation is what lets the kind=files PREFIX search ride
|
|
@@ -7169,8 +7825,9 @@ def _031_codex_file_account_map(conn: sqlite3.Connection) -> None:
|
|
|
7169
7825
|
existing install.
|
|
7170
7826
|
|
|
7171
7827
|
The two tables (``codex_file_incarnations``, ``codex_file_accounts``) are
|
|
7172
|
-
created by
|
|
7173
|
-
repo's table-addition rule, and specifically
|
|
7828
|
+
created by the top-level executescript in the version-gated
|
|
7829
|
+
``_apply_cache_schema`` — the repo's table-addition rule, and specifically
|
|
7830
|
+
BEFORE the FTS5
|
|
7174
7831
|
``legacy_present`` early-return so a legacy-shape cache still receives them.
|
|
7175
7832
|
This migration exists because that schema apply is VERSION-GATED: a
|
|
7176
7833
|
steady-state open compares ``PRAGMA user_version`` against
|
|
@@ -7682,6 +8339,125 @@ def _041_codex_quota_unresolved_model_index(conn: sqlite3.Connection) -> None:
|
|
|
7682
8339
|
conn.commit()
|
|
7683
8340
|
|
|
7684
8341
|
|
|
8342
|
+
@cache_migration("042_codex_entries_root_path_index")
|
|
8343
|
+
def _042_codex_entries_root_path_index(conn: sqlite3.Connection) -> None:
|
|
8344
|
+
"""#566: deliver the per-file alias join index to already-current stores.
|
|
8345
|
+
|
|
8346
|
+
``d1f14fad3`` added this index to ``_apply_cache_schema`` believing that
|
|
8347
|
+
path was unconditional. ``open_cache_db`` runs the schema apply only under
|
|
8348
|
+
``if not schema_current:``, and ``schema_current`` is true when
|
|
8349
|
+
``user_version`` equals ``len(_CACHE_MIGRATIONS)``. Every existing install
|
|
8350
|
+
was already at head, so the index reached new stores only, and a real
|
|
8351
|
+
install measured a ~90s snapshot build and a 167-184s publish period while
|
|
8352
|
+
running the release that contained the fix.
|
|
8353
|
+
|
|
8354
|
+
Same version-gate reason as 036/037/038/040/041: registering here is what
|
|
8355
|
+
makes an already-current install pick it up.
|
|
8356
|
+
|
|
8357
|
+
Re-running is a no-op (``IF NOT EXISTS``). NO self-stamp — the dispatcher
|
|
8358
|
+
central-stamps on a clean return (#140).
|
|
8359
|
+
"""
|
|
8360
|
+
_apply_codex_entries_root_path_index(conn)
|
|
8361
|
+
conn.commit()
|
|
8362
|
+
|
|
8363
|
+
|
|
8364
|
+
@cache_migration("043_codex_window_attributions")
|
|
8365
|
+
def _043_codex_window_attributions(conn: sqlite3.Connection) -> None:
|
|
8366
|
+
"""Deliver the #500 operator-attribution index to already-current stores.
|
|
8367
|
+
|
|
8368
|
+
Spec:
|
|
8369
|
+
``docs/superpowers/specs/2026-08-14-500-codex-window-attribution-design.md``
|
|
8370
|
+
§6.1.
|
|
8371
|
+
|
|
8372
|
+
``codex_window_attributions`` lives in the top-level executescript of the
|
|
8373
|
+
version-gated ``_apply_cache_schema``, and ``open_cache_db`` runs that
|
|
8374
|
+
script only when ``schema_current(conn, store)`` is false — i.e. when
|
|
8375
|
+
``user_version`` differs from ``len(_CACHE_MIGRATIONS)``. Every existing
|
|
8376
|
+
install is already at head, so the DDL alone would reach new stores only.
|
|
8377
|
+
Registering here is what bumps the head and makes an existing install
|
|
8378
|
+
re-run the schema apply, exactly as 031/036/037/038/040/041/042 do.
|
|
8379
|
+
|
|
8380
|
+
The handler then materializes the table from the journal, which is its only
|
|
8381
|
+
source. That is a from-zero ADDITIVE replay rather than the authoritative
|
|
8382
|
+
clear-then-replay ``cache-sync --rebuild`` uses: the table has just been
|
|
8383
|
+
created empty, so an additive pass materializes it completely, and a
|
|
8384
|
+
clear here would delete a covered family while a coverage certificate may
|
|
8385
|
+
still stand — the invariant
|
|
8386
|
+
``_cctally_journal._assert_coverage_already_invalidated`` exists to refuse.
|
|
8387
|
+
|
|
8388
|
+
Idempotent by construction: assertions insert on the op-id primary key with
|
|
8389
|
+
``OR IGNORE`` and a retraction stamps only an assertion that is not already
|
|
8390
|
+
retracted, so a re-run over its own output writes nothing but the cursor.
|
|
8391
|
+
NO self-stamp — the dispatcher central-stamps on a clean return (#140).
|
|
8392
|
+
|
|
8393
|
+
Takes the Codex provider flock like handlers 024-027 and 034: this writes a
|
|
8394
|
+
Codex-derived table, so a mid-walk ``sync_codex_cache`` must not interleave.
|
|
8395
|
+
On contention it DEFERS (``MigrationGateNotMet``) before touching any data,
|
|
8396
|
+
which is free here because that same sync rehydrates this table at its own
|
|
8397
|
+
start anyway.
|
|
8398
|
+
"""
|
|
8399
|
+
import _cctally_cache as cache_mod
|
|
8400
|
+
|
|
8401
|
+
# Defensive re-assert, not the primary creation path — the same shape
|
|
8402
|
+
# migration 031 carries and for the same reason: it keeps the handler
|
|
8403
|
+
# self-contained if `_apply_cache_schema`'s ordering ever drifts, and it is
|
|
8404
|
+
# what the per-migration golden exercises, whose `pre.sqlite` is a genuine
|
|
8405
|
+
# 042-head install that predates the table. Re-running is a no-op.
|
|
8406
|
+
conn.executescript(
|
|
8407
|
+
"""
|
|
8408
|
+
CREATE TABLE IF NOT EXISTS codex_window_attributions (
|
|
8409
|
+
op_id TEXT PRIMARY KEY,
|
|
8410
|
+
account_key TEXT NOT NULL,
|
|
8411
|
+
source_root_key TEXT NOT NULL,
|
|
8412
|
+
logical_limit_key TEXT NOT NULL,
|
|
8413
|
+
observed_slot TEXT NOT NULL,
|
|
8414
|
+
window_minutes INTEGER NOT NULL,
|
|
8415
|
+
raw_resets_at_utc TEXT NOT NULL,
|
|
8416
|
+
canonical_resets_at_utc TEXT,
|
|
8417
|
+
asserted_at_utc TEXT NOT NULL,
|
|
8418
|
+
retracted_by_op_id TEXT
|
|
8419
|
+
);
|
|
8420
|
+
CREATE INDEX IF NOT EXISTS idx_codex_window_attributions_root
|
|
8421
|
+
ON codex_window_attributions(source_root_key, window_minutes);
|
|
8422
|
+
"""
|
|
8423
|
+
)
|
|
8424
|
+
held = _acquire_cache_db_codex_provider_flock(
|
|
8425
|
+
conn, migration="043 window attributions")
|
|
8426
|
+
try:
|
|
8427
|
+
conn.execute("BEGIN IMMEDIATE")
|
|
8428
|
+
try:
|
|
8429
|
+
_applied, skipped = cache_mod.rehydrate_codex_window_attributions(
|
|
8430
|
+
conn, authoritative=False)
|
|
8431
|
+
conn.commit()
|
|
8432
|
+
except Exception:
|
|
8433
|
+
conn.rollback()
|
|
8434
|
+
raise
|
|
8435
|
+
# AFTER the commit, the rule `_report_file_account_conflicts` states:
|
|
8436
|
+
# the `except` above rolls back, so a line printed before it would
|
|
8437
|
+
# describe a skip on work that was undone (review finding F2).
|
|
8438
|
+
import _cctally_journal as journal_mod
|
|
8439
|
+
journal_mod._report_window_attribution_skips(skipped)
|
|
8440
|
+
finally:
|
|
8441
|
+
_release_cache_db_writer_flocks(held)
|
|
8442
|
+
|
|
8443
|
+
|
|
8444
|
+
@cache_migration("044_codex_accounting_change_ledger")
|
|
8445
|
+
def _044_codex_accounting_change_ledger(conn: sqlite3.Connection) -> None:
|
|
8446
|
+
"""Deliver #582's dirty-path ledger to already-current cache stores.
|
|
8447
|
+
|
|
8448
|
+
The steady-state schema apply is version-gated, so adding the table and
|
|
8449
|
+
triggers there alone would leave every 043-head installation without the
|
|
8450
|
+
mutation stream the dashboard requires. The shared helper keeps fresh and
|
|
8451
|
+
upgraded stores byte-identical. Existing accounting rows need no backfill:
|
|
8452
|
+
the first dashboard build is deliberately cold and subsequent mutations
|
|
8453
|
+
advance the new sequence.
|
|
8454
|
+
|
|
8455
|
+
The DDL is idempotent and the dispatcher owns the applied marker.
|
|
8456
|
+
"""
|
|
8457
|
+
_apply_codex_accounting_change_ledger(conn)
|
|
8458
|
+
conn.commit()
|
|
8459
|
+
|
|
8460
|
+
|
|
7685
8461
|
# === Region 7d: Stats migration 008_recompute_weekly_cost_snapshots_dedup_fix ===
|
|
7686
8462
|
|
|
7687
8463
|
@stats_migration("008_recompute_weekly_cost_snapshots_dedup_fix")
|