cctally 1.118.1 → 1.118.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/bin/_cctally_cache.py +465 -7
- package/bin/_cctally_dashboard.py +73 -7
- package/bin/_cctally_dashboard_sources.py +7 -0
- package/bin/_cctally_db.py +304 -0
- package/bin/_cctally_source_analytics.py +14 -3
- package/bin/_cctally_tui.py +189 -1
- package/bin/_lib_quota.py +31 -0
- package/bin/_lib_snapshot_cache.py +480 -1
- package/package.json +1 -1
|
@@ -6871,9 +6871,14 @@ def build_cached_claude_range_aggregates_from_capture(
|
|
|
6871
6871
|
try:
|
|
6872
6872
|
if "projects" in failures:
|
|
6873
6873
|
raise failures["projects"]
|
|
6874
|
+
# #1059: observing memos, so a spelling first resolved on an
|
|
6875
|
+
# incremental tick is registered for the identity sweep too. A flip
|
|
6876
|
+
# bumps the generation in `base`, which forces the cold branch below
|
|
6877
|
+
# with a fresh memo in the same build.
|
|
6878
|
+
_sc = _cctally()._load_sibling("_lib_snapshot_cache")
|
|
6874
6879
|
if incremental:
|
|
6875
6880
|
project_mut = copy.deepcopy(prior["project_mut"])
|
|
6876
|
-
resolver_cache =
|
|
6881
|
+
resolver_cache = _sc.observing_project_memo(prior["resolver_cache"])
|
|
6877
6882
|
for row, priced_entry in zip(rows, prepared):
|
|
6878
6883
|
_fold_projects_entry(
|
|
6879
6884
|
project_mut,
|
|
@@ -6883,7 +6888,8 @@ def build_cached_claude_range_aggregates_from_capture(
|
|
|
6883
6888
|
priced_entry=priced_entry,
|
|
6884
6889
|
)
|
|
6885
6890
|
else:
|
|
6886
|
-
|
|
6891
|
+
_sc.note_project_work("rangeFullRefolds")
|
|
6892
|
+
resolver_cache = _sc.observing_project_memo()
|
|
6887
6893
|
project_mut = fold_projects_over_range(
|
|
6888
6894
|
rows,
|
|
6889
6895
|
resolver_cache=resolver_cache,
|
|
@@ -7080,6 +7086,7 @@ def _assemble_projects_via_cache(
|
|
|
7080
7086
|
cw_end: "dt.datetime",
|
|
7081
7087
|
cur_max_id: int,
|
|
7082
7088
|
cur_max_seq: int,
|
|
7089
|
+
resolver_cache: "dict | None" = None,
|
|
7083
7090
|
) -> "tuple[dict, dict, dict]":
|
|
7084
7091
|
"""Flag-ON assembly (#269 §14 Win 2): recompute only the CURRENT week each
|
|
7085
7092
|
warm tick; serve CLOSED weeks from the per-(project, week) cache on a hit and
|
|
@@ -7097,10 +7104,17 @@ def _assemble_projects_via_cache(
|
|
|
7097
7104
|
tick's ``MAX(mutation_seq)`` (the #270 §8 delta watermark, so an id-stable
|
|
7098
7105
|
in-place finalization is caught). The accumulator is single-writer here (this
|
|
7099
7106
|
runs only on the sync-thread ``use_projects_env_cache=True`` path).
|
|
7107
|
+
|
|
7108
|
+
#1059: the memo is an OBSERVING memo (``resolver_cache`` when the caller
|
|
7109
|
+
supplies one), because every resolution here can be retained — by the
|
|
7110
|
+
current-week accumulator, a cold seed or a closed-week miss — and a
|
|
7111
|
+
retained resolution the identity sweep cannot see would keep a stale
|
|
7112
|
+
grouping until a restart.
|
|
7100
7113
|
"""
|
|
7101
7114
|
c = _cctally()
|
|
7102
7115
|
sc = c._load_sibling("_lib_snapshot_cache")
|
|
7103
|
-
resolver_cache
|
|
7116
|
+
if resolver_cache is None:
|
|
7117
|
+
resolver_cache = sc.observing_project_memo()
|
|
7104
7118
|
buckets: dict = {}
|
|
7105
7119
|
total_cost_by_week: dict = {}
|
|
7106
7120
|
key_by_bucket: dict = {}
|
|
@@ -7196,6 +7210,9 @@ def _assemble_projects_via_cache(
|
|
|
7196
7210
|
if hit is not None:
|
|
7197
7211
|
week_buckets, week_total = hit
|
|
7198
7212
|
else:
|
|
7213
|
+
# #1059 SD3: counted when attempted, so a raising compute is
|
|
7214
|
+
# still visible as work.
|
|
7215
|
+
sc.note_project_work("closedWeekComputes")
|
|
7199
7216
|
week_buckets, week_total = _aggregate_projects_week(
|
|
7200
7217
|
conn, week_start=w, week_end=w_end,
|
|
7201
7218
|
resolver_cache=resolver_cache,
|
|
@@ -7264,6 +7281,8 @@ def _build_projects_envelope(
|
|
|
7264
7281
|
weeks_back: int = 12,
|
|
7265
7282
|
use_projects_env_cache: bool = False,
|
|
7266
7283
|
content_revision_leg: "str | None" = None,
|
|
7284
|
+
project_resolver_cache: "dict | None" = None,
|
|
7285
|
+
use_envelope_memo: bool = True,
|
|
7267
7286
|
) -> dict:
|
|
7268
7287
|
"""Build the ``projects.{current_week, trend}`` envelope block.
|
|
7269
7288
|
|
|
@@ -7300,6 +7319,13 @@ def _build_projects_envelope(
|
|
|
7300
7319
|
lookup; the memo key carries it, so an in-place ``weekly_percent``
|
|
7301
7320
|
correction (which moves no ``MAX(id)``) rebuilds the envelope. ``None``
|
|
7302
7321
|
reads it here, on ``conn``.
|
|
7322
|
+
|
|
7323
|
+
#1059: ``project_resolver_cache`` is the resolver memo of the full-window
|
|
7324
|
+
walk, passed through to the cached assembly; ``None`` keeps a fresh plain
|
|
7325
|
+
dict (the walk) or an observing memo (the assembly). An owner build that
|
|
7326
|
+
retains this result passes an observing memo so the identity sweep sees
|
|
7327
|
+
every spelling. ``use_envelope_memo=False`` neither reads nor writes
|
|
7328
|
+
``_PROJECTS_ENV_MEMO``, for request-thread callers.
|
|
7303
7329
|
"""
|
|
7304
7330
|
c = _cctally()
|
|
7305
7331
|
|
|
@@ -7354,7 +7380,7 @@ def _build_projects_envelope(
|
|
|
7354
7380
|
cw_key = getattr(current_week, "week_start_at", None)
|
|
7355
7381
|
memo_key = (max_id, max_wus_id, entry_mutation_seq, cw_key, weeks_back,
|
|
7356
7382
|
content_revision_leg)
|
|
7357
|
-
cached = _PROJECTS_ENV_MEMO.get("value")
|
|
7383
|
+
cached = _PROJECTS_ENV_MEMO.get("value") if use_envelope_memo else None
|
|
7358
7384
|
if cached is not None and _PROJECTS_ENV_MEMO.get("key") == memo_key:
|
|
7359
7385
|
return cached
|
|
7360
7386
|
|
|
@@ -7420,13 +7446,17 @@ def _build_projects_envelope(
|
|
|
7420
7446
|
buckets, total_cost_by_week, key_by_bucket = _assemble_projects_via_cache(
|
|
7421
7447
|
conn, week_bounds=week_bounds, cw_start=cw_start, cw_end=cw_end,
|
|
7422
7448
|
cur_max_id=max_id, cur_max_seq=entry_mutation_seq,
|
|
7449
|
+
resolver_cache=project_resolver_cache,
|
|
7423
7450
|
)
|
|
7424
7451
|
else:
|
|
7425
7452
|
# `_resolve_project_key` is the production resolver; we use git-root
|
|
7426
7453
|
# mode (default for `cmd_project --group` absent) — matches the
|
|
7427
7454
|
# CLI's default.
|
|
7428
7455
|
_resolve_project_key = c._resolve_project_key
|
|
7429
|
-
resolver_cache: dict =
|
|
7456
|
+
resolver_cache: dict = (
|
|
7457
|
+
{} if project_resolver_cache is None else project_resolver_cache)
|
|
7458
|
+
c._load_sibling("_lib_snapshot_cache").note_project_work(
|
|
7459
|
+
"fullWindowProjectWalks")
|
|
7430
7460
|
|
|
7431
7461
|
buckets = {}
|
|
7432
7462
|
total_cost_by_week = {}
|
|
@@ -7787,8 +7817,9 @@ def _build_projects_envelope(
|
|
|
7787
7817
|
"current_week": cw_block,
|
|
7788
7818
|
"trend": trend_block,
|
|
7789
7819
|
}
|
|
7790
|
-
|
|
7791
|
-
|
|
7820
|
+
if use_envelope_memo:
|
|
7821
|
+
_PROJECTS_ENV_MEMO["key"] = memo_key
|
|
7822
|
+
_PROJECTS_ENV_MEMO["value"] = result
|
|
7792
7823
|
return result
|
|
7793
7824
|
|
|
7794
7825
|
|
|
@@ -7849,11 +7880,15 @@ def _project_detail_for_window(
|
|
|
7849
7880
|
if projects_envelope is not None:
|
|
7850
7881
|
env = projects_envelope
|
|
7851
7882
|
else:
|
|
7883
|
+
# #1059: this fallback runs on a request thread (the `/api/project`
|
|
7884
|
+
# handler passes `None` after a failed envelope build), so it must
|
|
7885
|
+
# neither read nor write the owner's process-wide envelope memo.
|
|
7852
7886
|
env = _build_projects_envelope(
|
|
7853
7887
|
conn,
|
|
7854
7888
|
now_utc=now_utc,
|
|
7855
7889
|
current_week=current_week,
|
|
7856
7890
|
weeks_back=weeks_back,
|
|
7891
|
+
use_envelope_memo=False,
|
|
7857
7892
|
)
|
|
7858
7893
|
|
|
7859
7894
|
# Resolve project_key → bucket_path via the envelope's identity map.
|
|
@@ -8724,6 +8759,30 @@ def _debug_memory_owner(stats, *, prefix: str = "") -> dict:
|
|
|
8724
8759
|
}
|
|
8725
8760
|
|
|
8726
8761
|
|
|
8762
|
+
def _debug_project_work() -> dict:
|
|
8763
|
+
"""#1059 SD3: the project work counters, or ``{}`` when unreadable."""
|
|
8764
|
+
cctally = sys.modules.get("cctally")
|
|
8765
|
+
loader = getattr(cctally, "_load_sibling", None)
|
|
8766
|
+
if not callable(loader):
|
|
8767
|
+
return {}
|
|
8768
|
+
try:
|
|
8769
|
+
return dict(loader("_lib_snapshot_cache").project_work_stats())
|
|
8770
|
+
except Exception: # noqa: BLE001 -- diagnostics fail closed.
|
|
8771
|
+
return {}
|
|
8772
|
+
|
|
8773
|
+
|
|
8774
|
+
def _debug_project_identity() -> dict:
|
|
8775
|
+
"""#1059: the project-identity sweep block, or ``{}`` when unreadable."""
|
|
8776
|
+
cctally = sys.modules.get("cctally")
|
|
8777
|
+
loader = getattr(cctally, "_load_sibling", None)
|
|
8778
|
+
if not callable(loader):
|
|
8779
|
+
return {}
|
|
8780
|
+
try:
|
|
8781
|
+
return dict(loader("_lib_snapshot_cache").project_identity_stats())
|
|
8782
|
+
except Exception: # noqa: BLE001 -- diagnostics fail closed.
|
|
8783
|
+
return {}
|
|
8784
|
+
|
|
8785
|
+
|
|
8727
8786
|
def _debug_retained_memory(hub, *, main_frontier_stats=None) -> dict:
|
|
8728
8787
|
"""Numeric retained-owner ceilings; never values, keys or source paths."""
|
|
8729
8788
|
owners: dict[str, dict] = {}
|
|
@@ -9610,6 +9669,13 @@ class DashboardHTTPHandler(BaseHTTPRequestHandler):
|
|
|
9610
9669
|
# #901 §5.5: kernel-accounted process writes, the trailing
|
|
9611
9670
|
# five-minute statistic and its budget verdict. Additive camelCase.
|
|
9612
9671
|
"writeIo": _debug_write_io(),
|
|
9672
|
+
# #1059 SD3: cumulative counts at the project work sites, so a
|
|
9673
|
+
# warm-tick measurement can prove no closed week was rebuilt.
|
|
9674
|
+
# Additive and opaque, like every field of this diagnostic.
|
|
9675
|
+
"projectWork": _debug_project_work(),
|
|
9676
|
+
# #1059: the identity registry and sweep diagnostics, reported
|
|
9677
|
+
# separately from the accelerator memory budget.
|
|
9678
|
+
"projectIdentity": _debug_project_identity(),
|
|
9613
9679
|
}
|
|
9614
9680
|
if body["phases"] is None:
|
|
9615
9681
|
body["note"] = "tracing_disabled"
|
|
@@ -7641,6 +7641,9 @@ def _capture_codex_accounting(
|
|
|
7641
7641
|
active_roots,
|
|
7642
7642
|
_cache_database_identity(context.cache_conn),
|
|
7643
7643
|
),
|
|
7644
|
+
# #1059: one observing memo per load, so every spelling the
|
|
7645
|
+
# qualified resolution visits is registered for the regroup
|
|
7646
|
+
# sweep.
|
|
7644
7647
|
load_all=lambda: load_qualified_codex_entries(
|
|
7645
7648
|
accounting_start,
|
|
7646
7649
|
accounting_end,
|
|
@@ -7648,6 +7651,8 @@ def _capture_codex_accounting(
|
|
|
7648
7651
|
sync=False,
|
|
7649
7652
|
cache_conn=context.cache_conn,
|
|
7650
7653
|
inventory=inventory,
|
|
7654
|
+
project_resolver_cache=(
|
|
7655
|
+
_lib_snapshot_cache.observing_project_memo()),
|
|
7651
7656
|
),
|
|
7652
7657
|
load_paths=lambda identities: load_qualified_codex_entries(
|
|
7653
7658
|
accounting_start,
|
|
@@ -7657,6 +7662,8 @@ def _capture_codex_accounting(
|
|
|
7657
7662
|
cache_conn=context.cache_conn,
|
|
7658
7663
|
source_identities=identities,
|
|
7659
7664
|
inventory=inventory,
|
|
7665
|
+
project_resolver_cache=(
|
|
7666
|
+
_lib_snapshot_cache.observing_project_memo()),
|
|
7660
7667
|
),
|
|
7661
7668
|
path_of=lambda entry: (
|
|
7662
7669
|
str(entry.source_root_key), str(entry.source_path),
|
package/bin/_cctally_db.py
CHANGED
|
@@ -4908,6 +4908,269 @@ def _apply_codex_quota_latest_summary(
|
|
|
4908
4908
|
conn.execute("RELEASE codex_quota_latest_apply")
|
|
4909
4909
|
|
|
4910
4910
|
|
|
4911
|
+
# ── #1033 §5.1: the exact reset-anchor evidence summary ─────────────────────
|
|
4912
|
+
#
|
|
4913
|
+
# `CodexResetAnchorResolver._components_for` seeded every anchor group by
|
|
4914
|
+
# reading the group's whole quota history (328,727 rows for the measured weekly
|
|
4915
|
+
# group) on every appended Codex hook, twice (walk and journal applier). The
|
|
4916
|
+
# components depend only on each distinct raw reset's minimum order key and the
|
|
4917
|
+
# row count, so this table holds exactly that per in-scope
|
|
4918
|
+
# (root, slot, limit key, raw reset) bucket: `row_count`, the lexicographic
|
|
4919
|
+
# minimum `(source_path, line_offset, id)` (never independently minimized
|
|
4920
|
+
# columns) and `stale`. A stale row's stored minimum is the deleted minimum, a
|
|
4921
|
+
# strict lower bound of every survivor; the seed never reads a stale group and
|
|
4922
|
+
# the drain's repair unit re-derives it.
|
|
4923
|
+
#
|
|
4924
|
+
# Membership is `source = 'codex' AND observed_slot IS NOT NULL`; the raw TEXT
|
|
4925
|
+
# spelling is the key and the seed folds spellings and skips unparseable raws.
|
|
4926
|
+
#
|
|
4927
|
+
# The triggers use built-in functions only, so a head-52 binary opening this
|
|
4928
|
+
# store keeps them firing, and they never upsert (an outer INSERT OR IGNORE
|
|
4929
|
+
# propagates its conflict policy into trigger bodies): an UPDATE, then an
|
|
4930
|
+
# INSERT ... WHERE NOT EXISTS. Insert and delete maintenance runs only while
|
|
4931
|
+
# the state is `ready`; every statement is a primary-key operation, and the
|
|
4932
|
+
# delete trigger never searches quota_window_snapshots for a replacement
|
|
4933
|
+
# minimum (#971). An insert whose (source_path, line_offset) is at or below a
|
|
4934
|
+
# stale minimum's is the exact minimum (that pair is unique within a bucket),
|
|
4935
|
+
# so a heal's same-line reinsert clears the staleness its delete caused. An
|
|
4936
|
+
# UPDATE of any key or order column flips the state to `bootstrap` and
|
|
4937
|
+
# advances the generation, which the drain's publication token reads (the
|
|
4938
|
+
# change ledger excludes `id`). A REPLACE against the snapshots table would
|
|
4939
|
+
# delete without firing the delete trigger; test_no_writer_replaces_quota_rows
|
|
4940
|
+
# refuses one.
|
|
4941
|
+
|
|
4942
|
+
CODEX_RESET_ANCHOR_EVIDENCE_STATE_KEY = "codex_reset_anchor_evidence"
|
|
4943
|
+
CODEX_RESET_ANCHOR_EVIDENCE_GENERATION_KEY = (
|
|
4944
|
+
"codex_reset_anchor_evidence_generation")
|
|
4945
|
+
CODEX_RESET_ANCHOR_EVIDENCE_READY = "ready"
|
|
4946
|
+
CODEX_RESET_ANCHOR_EVIDENCE_BOOTSTRAP = "bootstrap"
|
|
4947
|
+
CODEX_RESET_ANCHOR_EVIDENCE_TABLE = "codex_reset_anchor_evidence"
|
|
4948
|
+
|
|
4949
|
+
#: The five objects whose joint presence (with a valid state) means the summary
|
|
4950
|
+
#: has been maintained since it was last published.
|
|
4951
|
+
CODEX_RESET_ANCHOR_EVIDENCE_OBJECTS = (
|
|
4952
|
+
("table", "codex_reset_anchor_evidence"),
|
|
4953
|
+
("index", "idx_codex_reset_anchor_evidence_stale"),
|
|
4954
|
+
("trigger", "trg_qws_anchor_evidence_ins"),
|
|
4955
|
+
("trigger", "trg_qws_anchor_evidence_del"),
|
|
4956
|
+
("trigger", "trg_qws_anchor_evidence_upd"),
|
|
4957
|
+
)
|
|
4958
|
+
|
|
4959
|
+
#: Every key or order column; an UPDATE of any of them invalidates readiness.
|
|
4960
|
+
_CODEX_RESET_ANCHOR_EVIDENCE_UPDATE_OF = (
|
|
4961
|
+
"source", "source_root_key", "observed_slot", "logical_limit_key",
|
|
4962
|
+
"resets_at_utc", "source_path", "line_offset", "id",
|
|
4963
|
+
)
|
|
4964
|
+
|
|
4965
|
+
|
|
4966
|
+
def _codex_reset_anchor_evidence_ddl() -> "tuple[str, ...]":
|
|
4967
|
+
"""DDL for the summary, its stale index and its three triggers.
|
|
4968
|
+
|
|
4969
|
+
Triggers are dropped and re-created on every apply, like the latest
|
|
4970
|
+
summary's, so a definition change never leaves an older body behind; the
|
|
4971
|
+
table keeps IF NOT EXISTS and its rows survive.
|
|
4972
|
+
"""
|
|
4973
|
+
state = CODEX_RESET_ANCHOR_EVIDENCE_STATE_KEY
|
|
4974
|
+
generation = CODEX_RESET_ANCHOR_EVIDENCE_GENERATION_KEY
|
|
4975
|
+
ready = (f"(SELECT value FROM cache_meta WHERE key = '{state}')"
|
|
4976
|
+
f" = '{CODEX_RESET_ANCHOR_EVIDENCE_READY}'")
|
|
4977
|
+
|
|
4978
|
+
def bucket(image: str) -> str:
|
|
4979
|
+
return (f"source_root_key = {image}.source_root_key"
|
|
4980
|
+
f" AND observed_slot = {image}.observed_slot"
|
|
4981
|
+
f" AND logical_limit_key = {image}.logical_limit_key"
|
|
4982
|
+
f" AND resets_at_utc = {image}.resets_at_utc")
|
|
4983
|
+
|
|
4984
|
+
def in_scope(image: str) -> str:
|
|
4985
|
+
return (f"{image}.source = 'codex'"
|
|
4986
|
+
f" AND {image}.observed_slot IS NOT NULL")
|
|
4987
|
+
|
|
4988
|
+
takes = (
|
|
4989
|
+
"((stale = 0 AND (NEW.source_path, NEW.line_offset, NEW.id)"
|
|
4990
|
+
" < (min_source_path, min_line_offset, min_id))"
|
|
4991
|
+
" OR (stale = 1 AND (NEW.source_path, NEW.line_offset)"
|
|
4992
|
+
" <= (min_source_path, min_line_offset)))")
|
|
4993
|
+
differs = " OR ".join(
|
|
4994
|
+
f"OLD.{column} IS NOT NEW.{column}"
|
|
4995
|
+
for column in _CODEX_RESET_ANCHOR_EVIDENCE_UPDATE_OF)
|
|
4996
|
+
return (
|
|
4997
|
+
"CREATE TABLE IF NOT EXISTS codex_reset_anchor_evidence (\n"
|
|
4998
|
+
" source_root_key TEXT NOT NULL,\n"
|
|
4999
|
+
" observed_slot TEXT NOT NULL,\n"
|
|
5000
|
+
" logical_limit_key TEXT NOT NULL,\n"
|
|
5001
|
+
" resets_at_utc TEXT NOT NULL,\n"
|
|
5002
|
+
" row_count INTEGER NOT NULL CHECK (row_count > 0),\n"
|
|
5003
|
+
" min_source_path TEXT NOT NULL,\n"
|
|
5004
|
+
" min_line_offset INTEGER NOT NULL,\n"
|
|
5005
|
+
" min_id INTEGER NOT NULL,\n"
|
|
5006
|
+
" stale INTEGER NOT NULL DEFAULT 0"
|
|
5007
|
+
" CHECK (stale IN (0, 1)),\n"
|
|
5008
|
+
" PRIMARY KEY (source_root_key, observed_slot,"
|
|
5009
|
+
" logical_limit_key, resets_at_utc)\n"
|
|
5010
|
+
" ) WITHOUT ROWID",
|
|
5011
|
+
"CREATE INDEX IF NOT EXISTS idx_codex_reset_anchor_evidence_stale"
|
|
5012
|
+
" ON codex_reset_anchor_evidence"
|
|
5013
|
+
" (source_root_key, observed_slot, logical_limit_key)"
|
|
5014
|
+
" WHERE stale = 1",
|
|
5015
|
+
"DROP TRIGGER IF EXISTS trg_qws_anchor_evidence_ins",
|
|
5016
|
+
"CREATE TRIGGER trg_qws_anchor_evidence_ins\n"
|
|
5017
|
+
" AFTER INSERT ON quota_window_snapshots\n"
|
|
5018
|
+
f" WHEN {in_scope('NEW')} AND {ready}\n"
|
|
5019
|
+
" BEGIN\n"
|
|
5020
|
+
" UPDATE codex_reset_anchor_evidence"
|
|
5021
|
+
" SET row_count = row_count + 1,"
|
|
5022
|
+
f" min_source_path = CASE WHEN {takes} THEN NEW.source_path"
|
|
5023
|
+
" ELSE min_source_path END,"
|
|
5024
|
+
f" min_line_offset = CASE WHEN {takes} THEN NEW.line_offset"
|
|
5025
|
+
" ELSE min_line_offset END,"
|
|
5026
|
+
f" min_id = CASE WHEN {takes} THEN NEW.id ELSE min_id END,"
|
|
5027
|
+
f" stale = CASE WHEN {takes} THEN 0 ELSE stale END"
|
|
5028
|
+
f" WHERE {bucket('NEW')};\n"
|
|
5029
|
+
" INSERT INTO codex_reset_anchor_evidence"
|
|
5030
|
+
" (source_root_key, observed_slot, logical_limit_key, resets_at_utc,"
|
|
5031
|
+
" row_count, min_source_path, min_line_offset, min_id, stale)"
|
|
5032
|
+
" SELECT NEW.source_root_key, NEW.observed_slot,"
|
|
5033
|
+
" NEW.logical_limit_key, NEW.resets_at_utc, 1, NEW.source_path,"
|
|
5034
|
+
" NEW.line_offset, NEW.id, 0"
|
|
5035
|
+
" WHERE NOT EXISTS (SELECT 1 FROM codex_reset_anchor_evidence"
|
|
5036
|
+
f" WHERE {bucket('NEW')});\n"
|
|
5037
|
+
" END",
|
|
5038
|
+
"DROP TRIGGER IF EXISTS trg_qws_anchor_evidence_del",
|
|
5039
|
+
"CREATE TRIGGER trg_qws_anchor_evidence_del\n"
|
|
5040
|
+
" AFTER DELETE ON quota_window_snapshots\n"
|
|
5041
|
+
f" WHEN {in_scope('OLD')} AND {ready}\n"
|
|
5042
|
+
" BEGIN\n"
|
|
5043
|
+
" UPDATE cache_meta"
|
|
5044
|
+
f" SET value = '{CODEX_RESET_ANCHOR_EVIDENCE_BOOTSTRAP}'"
|
|
5045
|
+
f" WHERE key = '{state}'"
|
|
5046
|
+
" AND NOT EXISTS (SELECT 1 FROM codex_reset_anchor_evidence"
|
|
5047
|
+
f" WHERE {bucket('OLD')});\n"
|
|
5048
|
+
" DELETE FROM codex_reset_anchor_evidence"
|
|
5049
|
+
f" WHERE {bucket('OLD')} AND row_count = 1;\n"
|
|
5050
|
+
" UPDATE codex_reset_anchor_evidence"
|
|
5051
|
+
" SET row_count = row_count - 1,"
|
|
5052
|
+
" stale = CASE WHEN (min_source_path, min_line_offset, min_id)"
|
|
5053
|
+
" = (OLD.source_path, OLD.line_offset, OLD.id) THEN 1 ELSE stale END"
|
|
5054
|
+
f" WHERE {bucket('OLD')};\n"
|
|
5055
|
+
" END",
|
|
5056
|
+
"DROP TRIGGER IF EXISTS trg_qws_anchor_evidence_upd",
|
|
5057
|
+
"CREATE TRIGGER trg_qws_anchor_evidence_upd\n"
|
|
5058
|
+
" AFTER UPDATE OF "
|
|
5059
|
+
f"{', '.join(_CODEX_RESET_ANCHOR_EVIDENCE_UPDATE_OF)}"
|
|
5060
|
+
" ON quota_window_snapshots\n"
|
|
5061
|
+
f" WHEN (({in_scope('OLD')}) OR ({in_scope('NEW')}))"
|
|
5062
|
+
f" AND ({differs})\n"
|
|
5063
|
+
" BEGIN\n"
|
|
5064
|
+
" UPDATE cache_meta"
|
|
5065
|
+
" SET value = CAST(CAST(value AS INTEGER) + 1 AS TEXT)"
|
|
5066
|
+
f" WHERE key = '{generation}';\n"
|
|
5067
|
+
" INSERT INTO cache_meta (key, value)"
|
|
5068
|
+
f" SELECT '{generation}', '1' WHERE NOT EXISTS"
|
|
5069
|
+
f" (SELECT 1 FROM cache_meta WHERE key = '{generation}');\n"
|
|
5070
|
+
" UPDATE cache_meta"
|
|
5071
|
+
f" SET value = '{CODEX_RESET_ANCHOR_EVIDENCE_BOOTSTRAP}'"
|
|
5072
|
+
f" WHERE key = '{state}'"
|
|
5073
|
+
f" AND value = '{CODEX_RESET_ANCHOR_EVIDENCE_READY}';\n"
|
|
5074
|
+
" END",
|
|
5075
|
+
)
|
|
5076
|
+
|
|
5077
|
+
|
|
5078
|
+
def _advance_codex_reset_anchor_evidence_generation(
|
|
5079
|
+
conn: sqlite3.Connection,
|
|
5080
|
+
) -> None:
|
|
5081
|
+
"""Advance (or initialize) the invalidation generation in the caller's
|
|
5082
|
+
transaction. A malformed stored value restarts from it parsed as 0."""
|
|
5083
|
+
row = conn.execute(
|
|
5084
|
+
"SELECT value FROM cache_meta WHERE key = ?",
|
|
5085
|
+
(CODEX_RESET_ANCHOR_EVIDENCE_GENERATION_KEY,)).fetchone()
|
|
5086
|
+
try:
|
|
5087
|
+
current = int(row[0]) if row is not None else 0
|
|
5088
|
+
except (TypeError, ValueError):
|
|
5089
|
+
current = 0
|
|
5090
|
+
conn.execute(
|
|
5091
|
+
"INSERT INTO cache_meta(key, value) VALUES (?, ?) "
|
|
5092
|
+
"ON CONFLICT(key) DO UPDATE SET value=excluded.value",
|
|
5093
|
+
(CODEX_RESET_ANCHOR_EVIDENCE_GENERATION_KEY, str(current + 1)))
|
|
5094
|
+
|
|
5095
|
+
|
|
5096
|
+
def _set_codex_reset_anchor_evidence_state(
|
|
5097
|
+
conn: sqlite3.Connection, state: str,
|
|
5098
|
+
) -> None:
|
|
5099
|
+
conn.execute(
|
|
5100
|
+
"INSERT INTO cache_meta(key, value) VALUES (?, ?) "
|
|
5101
|
+
"ON CONFLICT(key) DO UPDATE SET value=excluded.value",
|
|
5102
|
+
(CODEX_RESET_ANCHOR_EVIDENCE_STATE_KEY, state))
|
|
5103
|
+
|
|
5104
|
+
|
|
5105
|
+
def _apply_codex_reset_anchor_evidence(conn: sqlite3.Connection) -> None:
|
|
5106
|
+
"""Install the reset-anchor evidence summary on every delivery path.
|
|
5107
|
+
|
|
5108
|
+
Shared by ``_apply_cache_schema`` and migration 053 so fresh and upgraded
|
|
5109
|
+
stores converge. Atomic like ``_apply_codex_retained_identity_inventory``:
|
|
5110
|
+
the table, its stale index, the three triggers and the state commit
|
|
5111
|
+
together, as a savepoint inside the caller's transaction or as an
|
|
5112
|
+
IMMEDIATE transaction of their own.
|
|
5113
|
+
|
|
5114
|
+
The state is decided only when any of the five objects was missing before
|
|
5115
|
+
this call, or the state key is missing or malformed: every summary row is
|
|
5116
|
+
deleted, the generation advances and the state becomes ``ready`` when no
|
|
5117
|
+
in-scope Codex quota row exists, else ``bootstrap`` for the drain's
|
|
5118
|
+
off-hook bootstrap unit. A store carrying all five objects and a valid
|
|
5119
|
+
state has been maintained since they were created, so its recorded state
|
|
5120
|
+
stands (a markerless retry changes nothing). A history scan never runs
|
|
5121
|
+
here: the population check is one LIMIT 1 probe. Guarded on the columns
|
|
5122
|
+
the triggers read, which a legacy-shape cache whose schema apply returned
|
|
5123
|
+
early can lack, and on ``cache_meta``.
|
|
5124
|
+
"""
|
|
5125
|
+
columns = {
|
|
5126
|
+
str(row[0]) for row in conn.execute(
|
|
5127
|
+
"SELECT name FROM pragma_table_info('quota_window_snapshots')")
|
|
5128
|
+
}
|
|
5129
|
+
if not ({"id", *_CODEX_RESET_ANCHOR_EVIDENCE_UPDATE_OF} <= columns
|
|
5130
|
+
and _table_exists(conn, "cache_meta")):
|
|
5131
|
+
return
|
|
5132
|
+
own_transaction = not conn.in_transaction
|
|
5133
|
+
conn.execute(
|
|
5134
|
+
"BEGIN IMMEDIATE" if own_transaction
|
|
5135
|
+
else "SAVEPOINT codex_reset_anchor_evidence_apply")
|
|
5136
|
+
try:
|
|
5137
|
+
present = {
|
|
5138
|
+
(str(row[0]), str(row[1])) for row in conn.execute(
|
|
5139
|
+
"SELECT type, name FROM sqlite_master "
|
|
5140
|
+
"WHERE type IN ('table', 'index', 'trigger')")
|
|
5141
|
+
}
|
|
5142
|
+
complete = all(
|
|
5143
|
+
item in present for item in CODEX_RESET_ANCHOR_EVIDENCE_OBJECTS)
|
|
5144
|
+
state_row = conn.execute(
|
|
5145
|
+
"SELECT value FROM cache_meta WHERE key = ?",
|
|
5146
|
+
(CODEX_RESET_ANCHOR_EVIDENCE_STATE_KEY,)).fetchone()
|
|
5147
|
+
valid_state = state_row is not None and state_row[0] in (
|
|
5148
|
+
CODEX_RESET_ANCHOR_EVIDENCE_READY,
|
|
5149
|
+
CODEX_RESET_ANCHOR_EVIDENCE_BOOTSTRAP)
|
|
5150
|
+
for statement in _codex_reset_anchor_evidence_ddl():
|
|
5151
|
+
conn.execute(statement)
|
|
5152
|
+
if not (complete and valid_state):
|
|
5153
|
+
conn.execute("DELETE FROM codex_reset_anchor_evidence")
|
|
5154
|
+
_advance_codex_reset_anchor_evidence_generation(conn)
|
|
5155
|
+
populated = conn.execute(
|
|
5156
|
+
"SELECT 1 FROM quota_window_snapshots WHERE source = 'codex' "
|
|
5157
|
+
"AND observed_slot IS NOT NULL LIMIT 1").fetchone()
|
|
5158
|
+
_set_codex_reset_anchor_evidence_state(
|
|
5159
|
+
conn, CODEX_RESET_ANCHOR_EVIDENCE_BOOTSTRAP if populated
|
|
5160
|
+
else CODEX_RESET_ANCHOR_EVIDENCE_READY)
|
|
5161
|
+
except BaseException:
|
|
5162
|
+
if own_transaction:
|
|
5163
|
+
conn.rollback()
|
|
5164
|
+
else:
|
|
5165
|
+
conn.execute("ROLLBACK TO codex_reset_anchor_evidence_apply")
|
|
5166
|
+
conn.execute("RELEASE codex_reset_anchor_evidence_apply")
|
|
5167
|
+
raise
|
|
5168
|
+
if own_transaction:
|
|
5169
|
+
conn.commit()
|
|
5170
|
+
else:
|
|
5171
|
+
conn.execute("RELEASE codex_reset_anchor_evidence_apply")
|
|
5172
|
+
|
|
5173
|
+
|
|
4911
5174
|
def _apply_codex_quota_load_order_index(conn: sqlite3.Connection) -> None:
|
|
4912
5175
|
"""The general loader's ORDER BY, served by an index (#901 §5.1, T1/T3).
|
|
4913
5176
|
|
|
@@ -5527,6 +5790,11 @@ CACHE_REDERIVABLE_OBJECTS: "tuple[SchemaDeliveryObject, ...]" = (
|
|
|
5527
5790
|
"index", "idx_codex_files_root_thread",
|
|
5528
5791
|
"_apply_codex_accounting_change_ledger",
|
|
5529
5792
|
"052_codex_delete_trigger_plans"),
|
|
5793
|
+
# #1033 spec §5.5: migration 053's handler calls the shared helper.
|
|
5794
|
+
*(SchemaDeliveryObject(
|
|
5795
|
+
kind, name, "_apply_codex_reset_anchor_evidence",
|
|
5796
|
+
"053_codex_reset_anchor_evidence")
|
|
5797
|
+
for kind, name in CODEX_RESET_ANCHOR_EVIDENCE_OBJECTS),
|
|
5530
5798
|
)
|
|
5531
5799
|
|
|
5532
5800
|
|
|
@@ -6370,6 +6638,8 @@ def _apply_cache_schema(conn: sqlite3.Connection) -> None:
|
|
|
6370
6638
|
# window-attribution read order (Amendment 1 item 1). BEFORE the
|
|
6371
6639
|
# legacy-FTS early-return, so an old-shape cache.db gains them.
|
|
6372
6640
|
_apply_codex_quota_latest_summary(conn)
|
|
6641
|
+
# #1033 §5.5: the reset-anchor evidence summary and its readiness state.
|
|
6642
|
+
_apply_codex_reset_anchor_evidence(conn)
|
|
6373
6643
|
_apply_codex_quota_load_order_index(conn)
|
|
6374
6644
|
_apply_codex_metadata_lookup_indexes(conn)
|
|
6375
6645
|
_apply_pricing_model_indexes(conn)
|
|
@@ -6571,6 +6841,10 @@ def _apply_conversations_schema(conn: sqlite3.Connection) -> None:
|
|
|
6571
6841
|
-- #927: the retained-identity inventory is cache.db prune state. Its
|
|
6572
6842
|
-- triggers went with the four family tables dropped above.
|
|
6573
6843
|
DROP TABLE IF EXISTS codex_retained_identities;
|
|
6844
|
+
-- #1033: the reset-anchor evidence summary is a cache.db read model
|
|
6845
|
+
-- over quota_window_snapshots, whose drop above took its triggers;
|
|
6846
|
+
-- dropping the table takes its stale index.
|
|
6847
|
+
DROP TABLE IF EXISTS codex_reset_anchor_evidence;
|
|
6574
6848
|
|
|
6575
6849
|
CREATE TABLE IF NOT EXISTS conversation_source_files (
|
|
6576
6850
|
path TEXT PRIMARY KEY,
|
|
@@ -10848,6 +11122,36 @@ def _052_codex_delete_trigger_plans(conn: sqlite3.Connection) -> None:
|
|
|
10848
11122
|
conn.commit()
|
|
10849
11123
|
|
|
10850
11124
|
|
|
11125
|
+
@cache_migration("053_codex_reset_anchor_evidence")
|
|
11126
|
+
def _053_codex_reset_anchor_evidence(conn: sqlite3.Connection) -> None:
|
|
11127
|
+
"""#1033 spec §5.5: deliver the reset-anchor evidence summary.
|
|
11128
|
+
|
|
11129
|
+
The schema apply that declares the table, its stale index and its three
|
|
11130
|
+
triggers is version-gated, so only a registered migration reaches an
|
|
11131
|
+
already-current store. The shared helper records ``ready`` when no
|
|
11132
|
+
in-scope Codex quota row exists, else ``bootstrap``: the drain's
|
|
11133
|
+
``reset_anchor_evidence_bootstrap`` unit (or ``cache-sync`` inline)
|
|
11134
|
+
aggregates an existing history off the hook, never here, so an ordinary
|
|
11135
|
+
open pays no history scan. Until then every seed falls back to the full
|
|
11136
|
+
read, which is today's behaviour.
|
|
11137
|
+
|
|
11138
|
+
Downgrade: a head-52 binary opening a head-53 store never reads the
|
|
11139
|
+
summary, and the dispatcher's version-ahead recovery trims this marker.
|
|
11140
|
+
The built-in-only triggers keep maintaining a ``ready`` summary under its
|
|
11141
|
+
writes, including its row-by-row ``_clear_codex_derived_rows``, which
|
|
11142
|
+
empties the summary exactly; a ``bootstrap`` summary stays inert. A
|
|
11143
|
+
re-upgrade re-runs this handler, finds every object and a valid state, and
|
|
11144
|
+
changes nothing.
|
|
11145
|
+
|
|
11146
|
+
Takes no provider flock: the helper's IMMEDIATE transaction (or savepoint)
|
|
11147
|
+
makes the trigger creation and the state decision atomic against every
|
|
11148
|
+
writer. Re-running is a no-op. NO self-stamp — the dispatcher
|
|
11149
|
+
central-stamps on a clean return (#140).
|
|
11150
|
+
"""
|
|
11151
|
+
_apply_codex_reset_anchor_evidence(conn)
|
|
11152
|
+
conn.commit()
|
|
11153
|
+
|
|
11154
|
+
|
|
10851
11155
|
# === Region 7d: Stats migration 008_recompute_weekly_cost_snapshots_dedup_fix ===
|
|
10852
11156
|
|
|
10853
11157
|
@stats_migration("008_recompute_weekly_cost_snapshots_dedup_fix")
|
|
@@ -1160,9 +1160,16 @@ class _CodexProjectResolution:
|
|
|
1160
1160
|
|
|
1161
1161
|
__slots__ = ("_group", "_resolver_cache", "_by_cwd", "_by_git_json")
|
|
1162
1162
|
|
|
1163
|
-
def __init__(
|
|
1163
|
+
def __init__(
|
|
1164
|
+
self, group: str = "git-root",
|
|
1165
|
+
resolver_cache: "dict[object, object] | None" = None,
|
|
1166
|
+
) -> None:
|
|
1164
1167
|
self._group = group
|
|
1165
|
-
|
|
1168
|
+
# #1059: the owner's dashboard loads pass an observing memo so every
|
|
1169
|
+
# spelling this resolution visits is registered for the regroup
|
|
1170
|
+
# sweep; every other caller keeps a private plain dict.
|
|
1171
|
+
self._resolver_cache: dict[object, object] = (
|
|
1172
|
+
{} if resolver_cache is None else resolver_cache)
|
|
1166
1173
|
self._by_cwd: dict[str, object] = {}
|
|
1167
1174
|
self._by_git_json: dict[str, str | None] = {}
|
|
1168
1175
|
|
|
@@ -1248,6 +1255,7 @@ def load_qualified_codex_entries(
|
|
|
1248
1255
|
source_identities: Iterable[tuple[str, str]] | None = None,
|
|
1249
1256
|
inventory: "CodexThreadMetadataInventory | None" = None,
|
|
1250
1257
|
_on_sync: "Callable[[object], object] | None" = None,
|
|
1258
|
+
project_resolver_cache: "dict[object, object] | None" = None,
|
|
1251
1259
|
) -> tuple[QualifiedCodexEntry, ...]:
|
|
1252
1260
|
"""Load exactly one bounded, root-qualified Codex accounting read.
|
|
1253
1261
|
|
|
@@ -1316,7 +1324,10 @@ def load_qualified_codex_entries(
|
|
|
1316
1324
|
conn.row_factory = previous_row_factory
|
|
1317
1325
|
|
|
1318
1326
|
return _qualify_codex_rows(
|
|
1319
|
-
rows, inherited_metadata, speed=speed, group=group
|
|
1327
|
+
rows, inherited_metadata, speed=speed, group=group,
|
|
1328
|
+
projects=_CodexProjectResolution(
|
|
1329
|
+
group, resolver_cache=project_resolver_cache),
|
|
1330
|
+
)
|
|
1320
1331
|
|
|
1321
1332
|
|
|
1322
1333
|
def _qualify_codex_rows(
|