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 CHANGED
@@ -5,6 +5,14 @@ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [1.118.2] - 2026-11-07
9
+
10
+ ### Changed
11
+ - The Codex hooks that `cctally setup` installs no longer re-read the whole quota history to resolve reset anchors, so recording new Codex usage stays fast on a long history.
12
+
13
+ ### Fixed
14
+ - The dashboard and TUI now regroup a project within about a minute after a git worktree is removed, a repository is created or moved, or a folder becomes a repository; previously they kept the old grouping until restarted.
15
+
8
16
  ## [1.118.1] - 2026-11-06
9
17
 
10
18
  ### Fixed
@@ -1749,6 +1749,97 @@ class CodexWalkDeadline(Exception):
1749
1749
  identity instead of deferring the file."""
1750
1750
 
1751
1751
 
1752
+ def _codex_reset_anchor_summary_sql(key_count: int) -> str:
1753
+ """The summary seed's ONE statement (#1033 spec §5.2).
1754
+
1755
+ One read snapshot returns a header row (``kind = 0``: the readiness value
1756
+ and how many of the table and its three triggers exist) and the group's
1757
+ summary rows (``kind = 1``), each a primary-key seek per candidate key.
1758
+ UNION ALL and scalar subqueries build no temp structure.
1759
+ """
1760
+ db = _cctally_db_sib
1761
+ names = [name for _kind, name in db.CODEX_RESET_ANCHOR_EVIDENCE_OBJECTS]
1762
+ triggers = [name for kind, name in db.CODEX_RESET_ANCHOR_EVIDENCE_OBJECTS
1763
+ if kind == "trigger"]
1764
+ placeholders = ",".join("?" for _ in range(key_count))
1765
+ return (
1766
+ "SELECT 0, (SELECT value FROM cache_meta WHERE key = "
1767
+ f"'{db.CODEX_RESET_ANCHOR_EVIDENCE_STATE_KEY}'), "
1768
+ "(SELECT COUNT(*) FROM sqlite_master WHERE "
1769
+ f"(type = 'table' AND name = '{names[0]}') OR (type = 'trigger' "
1770
+ "AND tbl_name = 'quota_window_snapshots' AND name IN ("
1771
+ + ", ".join(f"'{name}'" for name in triggers)
1772
+ + "))), NULL, NULL, NULL, NULL "
1773
+ "UNION ALL "
1774
+ "SELECT 1, resets_at_utc, row_count, min_source_path, "
1775
+ "min_line_offset, min_id, stale "
1776
+ "FROM codex_reset_anchor_evidence "
1777
+ "WHERE source_root_key = ? AND observed_slot = ? "
1778
+ f"AND logical_limit_key IN ({placeholders})")
1779
+
1780
+
1781
+ def _codex_reset_anchor_summary_seed(
1782
+ conn, root_key: str, slot: str, keys: "Sequence[str]", *,
1783
+ deadline: "float | None", clock,
1784
+ ) -> "_lib_quota.ResetAnchorComponents | None":
1785
+ """Seed one anchor group from the evidence summary, or ``None``.
1786
+
1787
+ ``None`` means fall back to the full history read: the summary is not
1788
+ ``ready``, its table or a trigger is missing, a returned bucket is stale,
1789
+ the statement raised a database error (for example the table does not
1790
+ exist yet, as during the pre-053 migration backfills), or a returned row
1791
+ is corrupt (a ``ValueError`` or ``TypeError`` while folding it, such as a
1792
+ count below one or a non-integer offset). The full read's
1793
+ ``_seed_failed`` meaning is untouched. Inside a write transaction the
1794
+ summary carries that transaction's own uncommitted rows, because the
1795
+ triggers ran in it, exactly as the full read would see them.
1796
+
1797
+ A budgeted walk arms ``_codex_cancellable_read`` around the statement and
1798
+ checks the deadline every ``_CODEX_FINALIZATION_CHECK_EVERY`` entries;
1799
+ ``CodexWalkDeadline`` propagates and nothing is returned, so a partial
1800
+ group is never cached.
1801
+ """
1802
+ sql = _codex_reset_anchor_summary_sql(len(keys))
1803
+ params = (str(root_key), str(slot), *keys)
1804
+ try:
1805
+ if deadline is None:
1806
+ rows = conn.execute(sql, params).fetchall()
1807
+ else:
1808
+ with _codex_cancellable_read(
1809
+ conn, deadline, clock, cancelled=CodexWalkDeadline):
1810
+ rows = conn.execute(sql, params).fetchall()
1811
+ except sqlite3.DatabaseError:
1812
+ return None
1813
+ header = [row for row in rows if row[0] == 0]
1814
+ if (len(header) != 1
1815
+ or header[0][1] != _cctally_db_sib.CODEX_RESET_ANCHOR_EVIDENCE_READY
1816
+ or header[0][2] != 4):
1817
+ return None
1818
+ entries = []
1819
+ index = 0
1820
+ try:
1821
+ for row in rows:
1822
+ if row[0] != 1:
1823
+ continue
1824
+ if row[6] != 0:
1825
+ return None
1826
+ if (deadline is not None and index
1827
+ and index % _CODEX_FINALIZATION_CHECK_EVERY == 0
1828
+ and clock() >= deadline):
1829
+ raise CodexWalkDeadline("deadline")
1830
+ index += 1
1831
+ parsed = _parse_anchor_iso(row[1])
1832
+ if parsed is not None:
1833
+ entries.append((parsed, (str(row[3]), int(row[4]),
1834
+ int(row[5])), int(row[2])))
1835
+ return _lib_quota.ResetAnchorComponents.from_evidence(entries)
1836
+ except (ValueError, TypeError):
1837
+ # A corrupt row (the table's CHECKs bypassed) seeds nothing; the full
1838
+ # read decides, as for a database error. `CodexWalkDeadline` is
1839
+ # neither, so it propagates.
1840
+ return None
1841
+
1842
+
1752
1843
  class CodexResetAnchorResolver:
1753
1844
  """Resolve one Codex quota observation's canonical reset anchor.
1754
1845
 
@@ -1818,10 +1909,20 @@ class CodexResetAnchorResolver:
1818
1909
  if components is not None:
1819
1910
  self._groups[group] = components
1820
1911
  return components
1912
+ candidates = _lib_jsonl.codex_snap_equivalent_limit_keys(
1913
+ str(logical_limit_key))
1914
+ # #1033 spec §5.2: the exact evidence summary seeds the same
1915
+ # components as the full read below from O(distinct raw resets) rows;
1916
+ # `None` (not ready, an object missing, a stale bucket, a database
1917
+ # error) falls back to that read unchanged.
1918
+ components = _codex_reset_anchor_summary_seed(
1919
+ self._conn, str(group[0]), group[1], candidates,
1920
+ deadline=self._deadline, clock=self._clock)
1921
+ if components is not None:
1922
+ self._groups[group] = components
1923
+ return components
1821
1924
  components = _lib_quota.ResetAnchorComponents()
1822
1925
  if not self._seed_failed:
1823
- candidates = _lib_jsonl.codex_snap_equivalent_limit_keys(
1824
- str(logical_limit_key))
1825
1926
  placeholders = ",".join("?" for _ in candidates)
1826
1927
  # #901 Q11: the unary `+` keeps the plan this read always had, a
1827
1928
  # walk of the `(source, source_path, line_offset, ...)` unique
@@ -5131,7 +5232,20 @@ def _clear_codex_derived_rows(conn: sqlite3.Connection) -> bool:
5131
5232
  "SELECT CAST(value AS INTEGER), 'full' FROM cache_meta "
5132
5233
  "WHERE key='codex_accounting_mutation_seq'"
5133
5234
  )
5235
+ # #1033 spec §5.4: the per-row evidence triggers are no-ops while the
5236
+ # state is not ``ready``; the cleared summary is then exact and empty.
5237
+ _evidence = conn.execute(
5238
+ "SELECT 1 FROM sqlite_master WHERE type='table' AND name=?",
5239
+ (_cctally_db_sib.CODEX_RESET_ANCHOR_EVIDENCE_TABLE,)).fetchone()
5240
+ if _evidence is not None:
5241
+ _cctally_db_sib._set_codex_reset_anchor_evidence_state(
5242
+ conn, _cctally_db_sib.CODEX_RESET_ANCHOR_EVIDENCE_BOOTSTRAP)
5134
5243
  conn.execute("DELETE FROM quota_window_snapshots WHERE source = 'codex'")
5244
+ if _evidence is not None:
5245
+ conn.execute("DELETE FROM codex_reset_anchor_evidence")
5246
+ _cctally_db_sib._advance_codex_reset_anchor_evidence_generation(conn)
5247
+ _cctally_db_sib._set_codex_reset_anchor_evidence_state(
5248
+ conn, _cctally_db_sib.CODEX_RESET_ANCHOR_EVIDENCE_READY)
5135
5249
  conn.execute("DELETE FROM codex_conversation_threads")
5136
5250
  conn.execute("DELETE FROM codex_conversation_events")
5137
5251
  conn.execute("DELETE FROM codex_session_files")
@@ -7069,18 +7183,27 @@ def _codex_executor_next_unit(
7069
7183
  """The next non-prune unit the executor owes, re-read from the store.
7070
7184
 
7071
7185
  Finalization debt first (a refused projection waits on its model flag),
7072
- then the ingest catch-up a budgeted walk deferred at the anchor seed
7073
- deadline (#980 F4), then the accounting change-log prune, then one
7186
+ then the reset-anchor evidence bootstrap (#1033, so a deferred catch-up
7187
+ file seeds from a ready summary), then the ingest catch-up a budgeted walk
7188
+ deferred at the anchor seed deadline (#980 F4), then the evidence repair
7189
+ (it runs only once prune debt is empty, so one run's repairs are batched
7190
+ after its pruning), then the accounting change-log prune, then one
7074
7191
  TRUNCATE attempt. A kind in ``blocked`` failed or stalled earlier in this
7075
7192
  run and waits for the next.
7076
7193
  """
7077
7194
  if ("finalization" not in blocked
7078
7195
  and not load_codex_finalization_debt(conn).empty):
7079
7196
  return "finalization"
7197
+ evidence = _codex_reset_anchor_evidence_work(conn)
7198
+ if (evidence == "bootstrap"
7199
+ and CODEX_EVIDENCE_BOOTSTRAP_UNIT not in blocked):
7200
+ return CODEX_EVIDENCE_BOOTSTRAP_UNIT
7080
7201
  if "ingest_catch_up" not in blocked:
7081
7202
  whole_tree, paths = load_codex_owed_catch_up(conn)
7082
7203
  if whole_tree or paths:
7083
7204
  return "ingest_catch_up"
7205
+ if evidence == "repair" and CODEX_EVIDENCE_REPAIR_UNIT not in blocked:
7206
+ return CODEX_EVIDENCE_REPAIR_UNIT
7084
7207
  owed = load_codex_owed_maintenance(conn)
7085
7208
  if owed.get("change_log_prune") and "change_log_prune" not in blocked:
7086
7209
  return "change_log_prune"
@@ -7219,6 +7342,298 @@ def _run_codex_ingest_catch_up_unit(conn: sqlite3.Connection, adm) -> str:
7219
7342
  return "done"
7220
7343
 
7221
7344
 
7345
+ # ── #1033 spec §5.3: bootstrap and repair of the reset-anchor evidence ───────
7346
+
7347
+ CODEX_EVIDENCE_BOOTSTRAP_UNIT = "reset_anchor_evidence_bootstrap"
7348
+ CODEX_EVIDENCE_REPAIR_UNIT = "reset_anchor_evidence_repair"
7349
+
7350
+ #: Prepare/publish attempts one unit kind may spend per run (one drain run,
7351
+ #: or one inline ``cache-sync`` run), shared across yields: a token mismatch,
7352
+ #: a ``None`` token and every admission refusal (the setup step's included)
7353
+ #: each spend one. Once spent, the unit is blocked for the rest of the run,
7354
+ #: its work still owed.
7355
+ _CODEX_EVIDENCE_ATTEMPTS = 3
7356
+
7357
+ #: Private test seam: called after each prepare, before admission, as
7358
+ #: ``hook(kind, attempt)``.
7359
+ _CODEX_EVIDENCE_PREPUBLISH_HOOK: "Callable[[str, int], None] | None" = None
7360
+
7361
+
7362
+ def _codex_reset_anchor_evidence_columns_ok(conn: sqlite3.Connection) -> bool:
7363
+ """The columns the triggers read exist (a legacy-shape cache whose schema
7364
+ apply returned early never receives the summary, and owes nothing)."""
7365
+ columns = {
7366
+ str(row[0]) for row in conn.execute(
7367
+ "SELECT name FROM pragma_table_info('quota_window_snapshots')")
7368
+ }
7369
+ return set(
7370
+ _cctally_db_sib._CODEX_RESET_ANCHOR_EVIDENCE_UPDATE_OF) <= columns
7371
+
7372
+
7373
+ def _codex_reset_anchor_evidence_objects(conn: sqlite3.Connection) -> int:
7374
+ names = [name for _kind, name
7375
+ in _cctally_db_sib.CODEX_RESET_ANCHOR_EVIDENCE_OBJECTS]
7376
+ return int(conn.execute(
7377
+ "SELECT COUNT(*) FROM sqlite_master WHERE name IN "
7378
+ f"({','.join('?' for _ in names)})", names).fetchone()[0])
7379
+
7380
+
7381
+ def _codex_reset_anchor_evidence_work(conn: sqlite3.Connection) -> "str | None":
7382
+ """The evidence summary's owed work: ``"bootstrap"`` when any of its five
7383
+ objects is missing or the state is not ``ready``, ``"repair"`` when a
7384
+ stale bucket exists (served by the partial index), else ``None``."""
7385
+ db = _cctally_db_sib
7386
+ if not _codex_reset_anchor_evidence_columns_ok(conn):
7387
+ return None
7388
+ if _codex_reset_anchor_evidence_objects(conn) != len(
7389
+ db.CODEX_RESET_ANCHOR_EVIDENCE_OBJECTS):
7390
+ return "bootstrap"
7391
+ state = conn.execute(
7392
+ "SELECT value FROM cache_meta WHERE key=?",
7393
+ (db.CODEX_RESET_ANCHOR_EVIDENCE_STATE_KEY,)).fetchone()
7394
+ if state is None or state[0] != db.CODEX_RESET_ANCHOR_EVIDENCE_READY:
7395
+ return "bootstrap"
7396
+ if conn.execute(
7397
+ "SELECT 1 FROM codex_reset_anchor_evidence WHERE stale = 1 "
7398
+ "LIMIT 1").fetchone() is not None:
7399
+ return "repair"
7400
+ return None
7401
+
7402
+
7403
+ def _codex_reset_anchor_evidence_aggregate(rows) -> dict:
7404
+ """``(root, slot, key, raw) -> [count, (path, offset, id)]`` with the
7405
+ LEXICOGRAPHIC minimum tuple (never per-column minima), in one pass."""
7406
+ out: dict = {}
7407
+ for root, slot, key, raw, path, offset, row_id in rows:
7408
+ bucket = (root, slot, key, raw)
7409
+ order = (path, offset, row_id)
7410
+ entry = out.get(bucket)
7411
+ if entry is None:
7412
+ out[bucket] = [1, order]
7413
+ else:
7414
+ entry[0] += 1
7415
+ if order < entry[1]:
7416
+ entry[1] = order
7417
+ return out
7418
+
7419
+
7420
+ def _codex_reset_anchor_evidence_token(conn: sqlite3.Connection):
7421
+ """The publication token, or ``None`` when publication cannot be trusted:
7422
+ the walk-share token (store identity, schema version, the quota ledger's
7423
+ sequence, the physical mutation sequence) plus the evidence generation,
7424
+ the state value and how many of the five objects exist. Any insert or
7425
+ delete moves the ledger sequence; a key or order UPDATE (``id`` included,
7426
+ which the ledger cannot see) moves the generation."""
7427
+ walk = _CodexWalkShare.token(conn)
7428
+ if walk is None:
7429
+ return None
7430
+ db = _cctally_db_sib
7431
+ meta = dict(conn.execute(
7432
+ "SELECT key, value FROM cache_meta WHERE key IN (?, ?)",
7433
+ (db.CODEX_RESET_ANCHOR_EVIDENCE_STATE_KEY,
7434
+ db.CODEX_RESET_ANCHOR_EVIDENCE_GENERATION_KEY)).fetchall())
7435
+ return (walk, meta.get(db.CODEX_RESET_ANCHOR_EVIDENCE_GENERATION_KEY),
7436
+ meta.get(db.CODEX_RESET_ANCHOR_EVIDENCE_STATE_KEY),
7437
+ _codex_reset_anchor_evidence_objects(conn))
7438
+
7439
+
7440
+ _EVIDENCE_SCAN_COLUMNS = (
7441
+ "source_root_key, observed_slot, logical_limit_key, resets_at_utc, "
7442
+ "source_path, line_offset, id")
7443
+
7444
+
7445
+ def _codex_reset_anchor_evidence_prepare(conn: sqlite3.Connection, kind: str):
7446
+ """One read snapshot: the token and the aggregate (no locks, no
7447
+ admission). Returns ``(token, payload)``; ``payload`` is the whole
7448
+ aggregate for a bootstrap, ``{group: aggregate}`` for a repair."""
7449
+ if conn.in_transaction:
7450
+ conn.rollback()
7451
+ conn.execute("BEGIN")
7452
+ try:
7453
+ token = _codex_reset_anchor_evidence_token(conn)
7454
+ if kind == CODEX_EVIDENCE_BOOTSTRAP_UNIT:
7455
+ payload = _codex_reset_anchor_evidence_aggregate(conn.execute(
7456
+ f"SELECT {_EVIDENCE_SCAN_COLUMNS} FROM quota_window_snapshots "
7457
+ "WHERE source = 'codex' AND observed_slot IS NOT NULL"))
7458
+ else:
7459
+ groups = {tuple(row) for row in conn.execute(
7460
+ "SELECT source_root_key, observed_slot, logical_limit_key "
7461
+ "FROM codex_reset_anchor_evidence WHERE stale = 1")}
7462
+ payload = {
7463
+ group: _codex_reset_anchor_evidence_aggregate(conn.execute(
7464
+ f"SELECT {_EVIDENCE_SCAN_COLUMNS} "
7465
+ "FROM quota_window_snapshots "
7466
+ "WHERE source = 'codex' AND source_root_key = ? "
7467
+ "AND logical_limit_key = ? AND observed_slot = ?",
7468
+ (group[0], group[2], group[1])))
7469
+ for group in sorted(groups)}
7470
+ finally:
7471
+ conn.rollback()
7472
+ return token, payload
7473
+
7474
+
7475
+ def _codex_reset_anchor_evidence_insert(conn, aggregate: dict) -> None:
7476
+ conn.executemany(
7477
+ "INSERT INTO codex_reset_anchor_evidence (source_root_key, "
7478
+ "observed_slot, logical_limit_key, resets_at_utc, row_count, "
7479
+ "min_source_path, min_line_offset, min_id, stale) "
7480
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0)",
7481
+ [(*bucket, count, *minimum)
7482
+ for bucket, (count, minimum) in sorted(aggregate.items())])
7483
+
7484
+
7485
+ def _run_codex_reset_anchor_evidence_unit(
7486
+ conn: sqlite3.Connection, adm, kind: str, *,
7487
+ budget: "dict[str, int] | None" = None,
7488
+ ) -> str:
7489
+ """Bootstrap or repair the reset-anchor evidence summary (#1033 §5.3).
7490
+
7491
+ Missing objects first get an admitted setup step: the shared schema
7492
+ helper re-creates them, invalidates readiness and advances the
7493
+ generation, and commits on its own. Then prepare/publish attempts:
7494
+ prepare reads the token and the aggregate in one snapshot with no
7495
+ admission and no store locks; publication admits through the global gate
7496
+ with both writer locks, takes ``BEGIN IMMEDIATE``, re-reads the token and
7497
+ publishes only when it is unchanged (a mismatch rolls back and
7498
+ re-prepares). A bootstrap replaces the whole summary and records
7499
+ ``ready``; a repair replaces each stale group's rows with exact,
7500
+ non-stale ones. Neither touches stored canonical anchors or the physical
7501
+ mutation sequence.
7502
+
7503
+ ``budget`` is the caller's per-run map of remaining attempts per kind
7504
+ (``_CODEX_EVIDENCE_ATTEMPTS`` when absent; ``None`` makes this call its
7505
+ own run). It is shared across yields, so a run that waits out requests
7506
+ and calls again does not start over: a token mismatch, a ``None`` token
7507
+ and an admission refusal (the setup step's included) each spend one.
7508
+
7509
+ Returns ``"published"``, ``"yielded"`` (admission refused with attempts
7510
+ left), ``"blocked"`` (the run's attempts are spent, or an error; the work
7511
+ stays owed for a later run) or ``"none"`` (nothing was owed). ``conn``
7512
+ holds no transaction on entry or exit.
7513
+ """
7514
+ if conn.in_transaction:
7515
+ conn.rollback()
7516
+ work = _codex_reset_anchor_evidence_work(conn)
7517
+ if work is None:
7518
+ return "none"
7519
+ expected = (CODEX_EVIDENCE_BOOTSTRAP_UNIT if work == "bootstrap"
7520
+ else CODEX_EVIDENCE_REPAIR_UNIT)
7521
+ if expected != kind:
7522
+ return "none"
7523
+ if budget is None:
7524
+ budget = {}
7525
+ budget.setdefault(kind, _CODEX_EVIDENCE_ATTEMPTS)
7526
+
7527
+ def spend() -> bool:
7528
+ """Spend one attempt; True while attempts remain."""
7529
+ budget[kind] -= 1
7530
+ return budget[kind] > 0
7531
+
7532
+ if budget[kind] <= 0:
7533
+ return "blocked"
7534
+ db = _cctally_db_sib
7535
+ if (kind == CODEX_EVIDENCE_BOOTSTRAP_UNIT
7536
+ and _codex_reset_anchor_evidence_objects(conn)
7537
+ != len(db.CODEX_RESET_ANCHOR_EVIDENCE_OBJECTS)):
7538
+ admission = adm.admit(_cctally_core.APP_DIR, _cache_store_locks,
7539
+ _release_cache_store_locks)
7540
+ if not admission.admitted:
7541
+ return "yielded" if spend() else "blocked"
7542
+ try:
7543
+ db._apply_codex_reset_anchor_evidence(conn)
7544
+ except sqlite3.Error as exc:
7545
+ eprint(f"[codex] reset-anchor evidence setup failed: {exc}")
7546
+ if conn.in_transaction:
7547
+ conn.rollback()
7548
+ return "blocked"
7549
+ finally:
7550
+ adm.finish(admission, _release_cache_store_locks)
7551
+ if _codex_reset_anchor_evidence_work(conn) is None:
7552
+ return "published" # an empty store came back ready
7553
+ while budget[kind] > 0:
7554
+ attempt = _CODEX_EVIDENCE_ATTEMPTS - budget[kind] + 1
7555
+ try:
7556
+ token, payload = _codex_reset_anchor_evidence_prepare(conn, kind)
7557
+ except sqlite3.Error as exc:
7558
+ eprint(f"[codex] reset-anchor evidence {kind} failed: {exc}")
7559
+ return "blocked"
7560
+ if _CODEX_EVIDENCE_PREPUBLISH_HOOK is not None:
7561
+ _CODEX_EVIDENCE_PREPUBLISH_HOOK(kind, attempt)
7562
+ if token is None:
7563
+ spend()
7564
+ continue
7565
+ admission = adm.admit(_cctally_core.APP_DIR, _cache_store_locks,
7566
+ _release_cache_store_locks)
7567
+ if not admission.admitted:
7568
+ return "yielded" if spend() else "blocked"
7569
+ try:
7570
+ conn.execute("BEGIN IMMEDIATE")
7571
+ if _codex_reset_anchor_evidence_token(conn) != token:
7572
+ conn.rollback()
7573
+ spend()
7574
+ continue
7575
+ if kind == CODEX_EVIDENCE_BOOTSTRAP_UNIT:
7576
+ conn.execute("DELETE FROM codex_reset_anchor_evidence")
7577
+ _codex_reset_anchor_evidence_insert(conn, payload)
7578
+ db._set_codex_reset_anchor_evidence_state(
7579
+ conn, db.CODEX_RESET_ANCHOR_EVIDENCE_READY)
7580
+ else:
7581
+ for (root, slot, key), aggregate in payload.items():
7582
+ conn.execute(
7583
+ "DELETE FROM codex_reset_anchor_evidence "
7584
+ "WHERE source_root_key = ? AND observed_slot = ? "
7585
+ "AND logical_limit_key = ?", (root, slot, key))
7586
+ _codex_reset_anchor_evidence_insert(conn, aggregate)
7587
+ conn.commit()
7588
+ return "published"
7589
+ except sqlite3.Error as exc:
7590
+ eprint(f"[codex] reset-anchor evidence {kind} failed: {exc}")
7591
+ if conn.in_transaction:
7592
+ conn.rollback()
7593
+ return "blocked"
7594
+ except BaseException:
7595
+ if conn.in_transaction:
7596
+ conn.rollback()
7597
+ raise
7598
+ finally:
7599
+ adm.finish(admission, _release_cache_store_locks)
7600
+ return "blocked"
7601
+
7602
+
7603
+ def run_codex_reset_anchor_evidence(*, wait_for_requests: bool = True) -> int:
7604
+ """Run the evidence units inline (``cache-sync``), with the drain's bounds.
7605
+
7606
+ The same function the drain runs, with one attempt budget per kind for
7607
+ this run, shared across yields; a yield waits out the requests when
7608
+ ``wait_for_requests`` and stops otherwise, and a kind whose budget is
7609
+ spent is blocked. Returns the number of units published."""
7610
+ import _lib_codex_drain_admission as adm
7611
+ published = 0
7612
+ blocked: "set[str]" = set()
7613
+ budget: "dict[str, int]" = {}
7614
+ while True:
7615
+ conn = open_cache_db()
7616
+ try:
7617
+ work = _codex_reset_anchor_evidence_work(conn)
7618
+ conn.rollback()
7619
+ kind = {"bootstrap": CODEX_EVIDENCE_BOOTSTRAP_UNIT,
7620
+ "repair": CODEX_EVIDENCE_REPAIR_UNIT}.get(work)
7621
+ if kind is None or kind in blocked:
7622
+ return published
7623
+ outcome = _run_codex_reset_anchor_evidence_unit(
7624
+ conn, adm, kind, budget=budget)
7625
+ finally:
7626
+ conn.close()
7627
+ if outcome == "published":
7628
+ published += 1
7629
+ elif outcome == "yielded":
7630
+ if not wait_for_requests:
7631
+ return published
7632
+ _drain_backoff(adm)
7633
+ else:
7634
+ blocked.add(kind)
7635
+
7636
+
7222
7637
  def _next_codex_debt_row(conn: sqlite3.Connection, table: str,
7223
7638
  after: "tuple[str, str] | None"):
7224
7639
  """The next debt row in key order, strictly past ``after`` when given.
@@ -7270,6 +7685,8 @@ def run_codex_prune_drain(*, wait_for_requests: bool = True,
7270
7685
  deleted = dropped = yielded = 0
7271
7686
  finalization_units = owed_units = wal_attempts = 0
7272
7687
  blocked: "set[str]" = set()
7688
+ # #1033 §5.3: the evidence units' attempts per kind for this run.
7689
+ evidence_budget: "dict[str, int]" = {}
7273
7690
  discovery = _CodexPruneDiscoveryCache() # one discovery per pass (N10)
7274
7691
  # #970 spec 5.6: an identity whose absence is unknown keeps its debt and
7275
7692
  # is passed over for the rest of this run (keyset past it).
@@ -7290,6 +7707,21 @@ def run_codex_prune_drain(*, wait_for_requests: bool = True,
7290
7707
  # admission, re-reads its state under the gate and commits.
7291
7708
  unit = _codex_executor_next_unit(conn, blocked)
7292
7709
  conn.rollback()
7710
+ if unit == CODEX_EVIDENCE_BOOTSTRAP_UNIT:
7711
+ # #1033 §5.3: like the catch-up, special-cased ahead of
7712
+ # prune rows; its scan runs before its own admission.
7713
+ outcome = _run_codex_reset_anchor_evidence_unit(
7714
+ conn, adm, unit, budget=evidence_budget)
7715
+ if outcome == "yielded":
7716
+ yielded += 1
7717
+ if not wait_for_requests:
7718
+ break
7719
+ _drain_backoff(adm)
7720
+ elif outcome == "published":
7721
+ owed_units += 1
7722
+ elif outcome == "blocked":
7723
+ blocked.add(unit)
7724
+ continue
7293
7725
  if unit == "ingest_catch_up":
7294
7726
  # #980 F4: its sync owns the locks and its own admission.
7295
7727
  caught = _run_codex_ingest_catch_up_unit(conn, adm)
@@ -7309,6 +7741,21 @@ def run_codex_prune_drain(*, wait_for_requests: bool = True,
7309
7741
  conn.rollback()
7310
7742
  if row is None and unit is None:
7311
7743
  break
7744
+ if row is None and unit == CODEX_EVIDENCE_REPAIR_UNIT:
7745
+ # #1033 §5.3: after pruning; prepare runs unadmitted, so
7746
+ # it never goes through the admitted-only executor body.
7747
+ outcome = _run_codex_reset_anchor_evidence_unit(
7748
+ conn, adm, unit, budget=evidence_budget)
7749
+ if outcome == "yielded":
7750
+ yielded += 1
7751
+ if not wait_for_requests:
7752
+ break
7753
+ _drain_backoff(adm)
7754
+ elif outcome == "published":
7755
+ owed_units += 1
7756
+ elif outcome == "blocked":
7757
+ blocked.add(unit)
7758
+ continue
7312
7759
  if row is None:
7313
7760
  admission = adm.admit(_cctally_core.APP_DIR,
7314
7761
  _cache_store_locks,
@@ -7323,11 +7770,15 @@ def run_codex_prune_drain(*, wait_for_requests: bool = True,
7323
7770
  try:
7324
7771
  unit = _codex_executor_next_unit(conn, blocked)
7325
7772
  conn.rollback()
7326
- if unit is None or unit == "ingest_catch_up":
7773
+ if unit is None or unit in (
7774
+ "ingest_catch_up",
7775
+ CODEX_EVIDENCE_BOOTSTRAP_UNIT,
7776
+ CODEX_EVIDENCE_REPAIR_UNIT):
7327
7777
  # #981 R2: a catch-up a hook recorded since the
7328
7778
  # unlocked read runs through its own path (its sync
7329
7779
  # owns the locks): release this admission and let
7330
- # the next pass dispatch it.
7780
+ # the next pass dispatch it. The #1033 evidence
7781
+ # units likewise run through their own path.
7331
7782
  continue
7332
7783
  ok, model_cleared = _run_codex_executor_unit(conn, unit)
7333
7784
  except BaseException:
@@ -17356,7 +17807,11 @@ def load_codex_owed_catch_up(
17356
17807
 
17357
17808
  def _codex_owed_anything(conn: sqlite3.Connection) -> bool:
17358
17809
  flags, whole_tree, paths = _load_codex_owed_record(conn)
17359
- return any(flags.values()) or whole_tree or bool(paths)
17810
+ if any(flags.values()) or whole_tree or bool(paths):
17811
+ return True
17812
+ # #1033 spec §5.3: the reset-anchor evidence work is derived from its
17813
+ # state and an indexed stale probe, never from a second owed flag.
17814
+ return _codex_reset_anchor_evidence_work(conn) is not None
17360
17815
 
17361
17816
 
17362
17817
  def _store_codex_owed_maintenance(
@@ -27138,6 +27593,9 @@ def cmd_cache_sync(args: argparse.Namespace) -> int:
27138
27593
  # identity inventory inline, through the same admitted executor the
27139
27594
  # detached worker runs (a no-op once it is ready).
27140
27595
  run_codex_inventory_bootstrap(wait_for_requests=True)
27596
+ # #1033 spec §5.3: the reset-anchor evidence bootstrap/repair runs
27597
+ # inline too, through the drain's own function and bounds.
27598
+ run_codex_reset_anchor_evidence(wait_for_requests=True)
27141
27599
  prune_result = run_codex_prune_drain(wait_for_requests=True)
27142
27600
  verify_result = run_codex_container_verification_drain(wait_for_requests=True)
27143
27601
  run_codex_reset_drain(wait_for_requests=True)