cctally 1.118.0 → 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.
@@ -289,21 +289,34 @@ _AI_TITLE_UPSERT_SQL = (
289
289
  "ai_title=excluded.ai_title, source_path=excluded.source_path, byte_offset=excluded.byte_offset"
290
290
  )
291
291
 
292
- def _ai_title_upsert_sql(table: str = "conversation_ai_titles") -> str:
292
+ def _ai_title_upsert_sql(table: str = "conversation_ai_titles", *,
293
+ owner_only: bool = False) -> str:
293
294
  """The AI-title upsert, targeted at the live table or its staging twin.
294
295
 
295
296
  A rebuild builds its replacement generation into staging and leaves the
296
- live table alone until the publish (#752), so the ONE statement that writes
297
- a title has to be able to name either. Parameterised through this helper
298
- rather than by two literals, because two literals drift.
297
+ live table alone until the publish (#752) — except for a file replayed
298
+ under a NEW incarnation, whose live title rows are replaced,
299
+ owner-restricted, at its batch commit (#1062 §4.2). So the ONE statement
300
+ that writes a title has to be able to name either table. Parameterised
301
+ through this helper rather than by several literals, because literals
302
+ drift.
303
+
304
+ ``owner_only`` is that #1062 §4.2 live-table write. The file's own live
305
+ rows were just deleted, so a conflicting live row belongs to ANOTHER file
306
+ of the same session and is kept until the publish installs the replayed
307
+ generation's choice; a later title line of the same file still replaces an
308
+ earlier one, as in the walk.
299
309
  """
300
- return (
310
+ sql = (
301
311
  f"INSERT INTO {table}(session_id,ai_title,source_path,byte_offset) "
302
312
  "VALUES(?,?,?,?) "
303
313
  "ON CONFLICT(session_id) DO UPDATE SET "
304
314
  "ai_title=excluded.ai_title, source_path=excluded.source_path, "
305
315
  "byte_offset=excluded.byte_offset"
306
316
  )
317
+ if owner_only:
318
+ sql += f" WHERE {table}.source_path = excluded.source_path"
319
+ return sql
307
320
 
308
321
 
309
322
  # ---------------------------------------------------------------------------
@@ -1736,6 +1749,97 @@ class CodexWalkDeadline(Exception):
1736
1749
  identity instead of deferring the file."""
1737
1750
 
1738
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
+
1739
1843
  class CodexResetAnchorResolver:
1740
1844
  """Resolve one Codex quota observation's canonical reset anchor.
1741
1845
 
@@ -1805,10 +1909,20 @@ class CodexResetAnchorResolver:
1805
1909
  if components is not None:
1806
1910
  self._groups[group] = components
1807
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
1808
1924
  components = _lib_quota.ResetAnchorComponents()
1809
1925
  if not self._seed_failed:
1810
- candidates = _lib_jsonl.codex_snap_equivalent_limit_keys(
1811
- str(logical_limit_key))
1812
1926
  placeholders = ",".join("?" for _ in candidates)
1813
1927
  # #901 Q11: the unary `+` keeps the plan this read always had, a
1814
1928
  # walk of the `(source, source_path, line_offset, ...)` unique
@@ -5118,7 +5232,20 @@ def _clear_codex_derived_rows(conn: sqlite3.Connection) -> bool:
5118
5232
  "SELECT CAST(value AS INTEGER), 'full' FROM cache_meta "
5119
5233
  "WHERE key='codex_accounting_mutation_seq'"
5120
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)
5121
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)
5122
5249
  conn.execute("DELETE FROM codex_conversation_threads")
5123
5250
  conn.execute("DELETE FROM codex_conversation_events")
5124
5251
  conn.execute("DELETE FROM codex_session_files")
@@ -7056,18 +7183,27 @@ def _codex_executor_next_unit(
7056
7183
  """The next non-prune unit the executor owes, re-read from the store.
7057
7184
 
7058
7185
  Finalization debt first (a refused projection waits on its model flag),
7059
- then the ingest catch-up a budgeted walk deferred at the anchor seed
7060
- 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
7061
7191
  TRUNCATE attempt. A kind in ``blocked`` failed or stalled earlier in this
7062
7192
  run and waits for the next.
7063
7193
  """
7064
7194
  if ("finalization" not in blocked
7065
7195
  and not load_codex_finalization_debt(conn).empty):
7066
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
7067
7201
  if "ingest_catch_up" not in blocked:
7068
7202
  whole_tree, paths = load_codex_owed_catch_up(conn)
7069
7203
  if whole_tree or paths:
7070
7204
  return "ingest_catch_up"
7205
+ if evidence == "repair" and CODEX_EVIDENCE_REPAIR_UNIT not in blocked:
7206
+ return CODEX_EVIDENCE_REPAIR_UNIT
7071
7207
  owed = load_codex_owed_maintenance(conn)
7072
7208
  if owed.get("change_log_prune") and "change_log_prune" not in blocked:
7073
7209
  return "change_log_prune"
@@ -7206,6 +7342,298 @@ def _run_codex_ingest_catch_up_unit(conn: sqlite3.Connection, adm) -> str:
7206
7342
  return "done"
7207
7343
 
7208
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
+
7209
7637
  def _next_codex_debt_row(conn: sqlite3.Connection, table: str,
7210
7638
  after: "tuple[str, str] | None"):
7211
7639
  """The next debt row in key order, strictly past ``after`` when given.
@@ -7257,6 +7685,8 @@ def run_codex_prune_drain(*, wait_for_requests: bool = True,
7257
7685
  deleted = dropped = yielded = 0
7258
7686
  finalization_units = owed_units = wal_attempts = 0
7259
7687
  blocked: "set[str]" = set()
7688
+ # #1033 §5.3: the evidence units' attempts per kind for this run.
7689
+ evidence_budget: "dict[str, int]" = {}
7260
7690
  discovery = _CodexPruneDiscoveryCache() # one discovery per pass (N10)
7261
7691
  # #970 spec 5.6: an identity whose absence is unknown keeps its debt and
7262
7692
  # is passed over for the rest of this run (keyset past it).
@@ -7277,6 +7707,21 @@ def run_codex_prune_drain(*, wait_for_requests: bool = True,
7277
7707
  # admission, re-reads its state under the gate and commits.
7278
7708
  unit = _codex_executor_next_unit(conn, blocked)
7279
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
7280
7725
  if unit == "ingest_catch_up":
7281
7726
  # #980 F4: its sync owns the locks and its own admission.
7282
7727
  caught = _run_codex_ingest_catch_up_unit(conn, adm)
@@ -7296,6 +7741,21 @@ def run_codex_prune_drain(*, wait_for_requests: bool = True,
7296
7741
  conn.rollback()
7297
7742
  if row is None and unit is None:
7298
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
7299
7759
  if row is None:
7300
7760
  admission = adm.admit(_cctally_core.APP_DIR,
7301
7761
  _cache_store_locks,
@@ -7310,11 +7770,15 @@ def run_codex_prune_drain(*, wait_for_requests: bool = True,
7310
7770
  try:
7311
7771
  unit = _codex_executor_next_unit(conn, blocked)
7312
7772
  conn.rollback()
7313
- 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):
7314
7777
  # #981 R2: a catch-up a hook recorded since the
7315
7778
  # unlocked read runs through its own path (its sync
7316
7779
  # owns the locks): release this admission and let
7317
- # the next pass dispatch it.
7780
+ # the next pass dispatch it. The #1033 evidence
7781
+ # units likewise run through their own path.
7318
7782
  continue
7319
7783
  ok, model_cleared = _run_codex_executor_unit(conn, unit)
7320
7784
  except BaseException:
@@ -17343,7 +17807,11 @@ def load_codex_owed_catch_up(
17343
17807
 
17344
17808
  def _codex_owed_anything(conn: sqlite3.Connection) -> bool:
17345
17809
  flags, whole_tree, paths = _load_codex_owed_record(conn)
17346
- 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
17347
17815
 
17348
17816
 
17349
17817
  def _store_codex_owed_maintenance(
@@ -21527,12 +21995,36 @@ def scope_conversations_db_to_account(
21527
21995
  f"""
21528
21996
  CREATE TEMP VIEW conversation_messages AS
21529
21997
  SELECT * FROM main.conversation_messages WHERE {stored_predicate};
21998
+ -- #1062: a title is in scope iff (a) its own file's message walk has
21999
+ -- committed past the title's line, (b) that file still holds a
22000
+ -- retained in-scope message of the title's session, (c) no retained
22001
+ -- message of the session, in any file, is outside the scope, and
22002
+ -- (d) no Claude conversation rebuild is in progress (its message
22003
+ -- table may be partial). (b) reads the scoped TEMP view; (a), (c)
22004
+ -- and (d) read main. The fragment's unqualified account_key binds to
22005
+ -- the innermost FROM (main.conversation_messages m). The base rowid
22006
+ -- is projected as `rowid` so the title FTS joins resolve on a scoped
22007
+ -- connection and return the base row (C1).
21530
22008
  CREATE TEMP VIEW conversation_ai_titles AS
21531
- SELECT t.* FROM main.conversation_ai_titles t
22009
+ SELECT t.rowid AS rowid, t.* FROM main.conversation_ai_titles t
21532
22010
  WHERE EXISTS (
22011
+ SELECT 1 FROM main.conversation_source_files f
22012
+ WHERE f.path = t.source_path
22013
+ AND f.last_byte_offset > t.byte_offset
22014
+ )
22015
+ AND EXISTS (
21533
22016
  SELECT 1 FROM conversation_messages m
21534
- WHERE m.source_path=t.source_path
21535
- AND m.byte_offset=t.byte_offset
22017
+ WHERE m.session_id = t.session_id
22018
+ AND m.source_path = t.source_path
22019
+ )
22020
+ AND NOT EXISTS (
22021
+ SELECT 1 FROM main.conversation_messages m
22022
+ WHERE m.session_id = t.session_id
22023
+ AND NOT ({stored_predicate})
22024
+ )
22025
+ AND NOT EXISTS (
22026
+ SELECT 1 FROM main.cache_meta
22027
+ WHERE key = 'conversation_rebuild_claude_pending'
21536
22028
  );
21537
22029
  CREATE TEMP VIEW conversation_file_touches AS
21538
22030
  SELECT t.* FROM main.conversation_file_touches t
@@ -23642,6 +24134,19 @@ def _write_claude_prepared_file(
23642
24134
  and the scoped ``_fill_file_touches`` never commits."""
23643
24135
  path_str = prepared.path
23644
24136
  sessions: set[str] = set()
24137
+ # #1062 §4.2: a rebuild writes titles to staging and leaves the live table
24138
+ # to the publish (#752) — except for a file replayed under a NEW
24139
+ # incarnation. `prepared.truncated` means exactly that (#1062 F8): a
24140
+ # replacement, a truncation below the cursor, or missing identity
24141
+ # evidence; a continuous file that merely shrank (a trimmed unfinished
24142
+ # tail) is not truncated and keeps its committed rows. A new incarnation's
24143
+ # live rows get what an incremental sync of the file would leave: the
24144
+ # path's rows deleted and its replayed titles re-upserted, owner-restricted,
24145
+ # here in the file's savepoint, so they commit with its cursor. Without it
24146
+ # the fold carries a title the file has lost into the new generation, and
24147
+ # after an interrupted rebuild no later run detects the replacement again.
24148
+ refresh_live = (prepared.truncated
24149
+ and title_table != "conversation_ai_titles")
23645
24150
  if prepared.truncated:
23646
24151
  sessions.update(
23647
24152
  row[0]
@@ -23664,6 +24169,11 @@ def _write_claude_prepared_file(
23664
24169
  f"DELETE FROM {title_table} WHERE source_path=?",
23665
24170
  (path_str,),
23666
24171
  )
24172
+ if refresh_live:
24173
+ conn.execute(
24174
+ "DELETE FROM conversation_ai_titles WHERE source_path=?",
24175
+ (path_str,),
24176
+ )
23667
24177
  if prepared.conv_rows:
23668
24178
  conn.executemany(_CONV_ACCOUNT_INSERT_SQL, prepared.conv_rows)
23669
24179
  _fill_file_touches(
@@ -23683,6 +24193,9 @@ def _write_claude_prepared_file(
23683
24193
  )
23684
24194
  if prepared.ai_rows:
23685
24195
  conn.executemany(_ai_title_upsert_sql(title_table), prepared.ai_rows)
24196
+ if refresh_live:
24197
+ conn.executemany(
24198
+ _ai_title_upsert_sql(owner_only=True), prepared.ai_rows)
23686
24199
  size, mtime_ns, final_offset, device_id, inode, incarnation_id, prefix = (
23687
24200
  prepared.cursor)
23688
24201
  conn.execute(
@@ -24028,9 +24541,11 @@ def sync_claude_conversations(
24028
24541
  # nothing, so every one of its records is first observed now and takes
24029
24542
  # the active identity rather than being read as historical-and-unknown.
24030
24543
  high_water: "dict[tuple[str, str], int]" = {}
24031
- # #752: a rebuild writes its titles and rollup into staging and does
24032
- # not touch the live tables until the publish, so readers keep seeing
24033
- # the previous complete generation for the whole replay.
24544
+ # #752: a rebuild writes its titles and rollup into staging and
24545
+ # otherwise leaves the live tables to the publish, so readers keep
24546
+ # seeing the previous complete generation for the replay. The one
24547
+ # exception is #1062 §4.2: a file replayed under a NEW incarnation has
24548
+ # its live title rows replaced, owner-restricted, at its batch commit.
24034
24549
  title_table = "conversation_ai_titles"
24035
24550
  rollup_table = "conversation_sessions"
24036
24551
  if rebuild:
@@ -24132,6 +24647,11 @@ def sync_claude_conversations(
24132
24647
  stats.files_skipped_unchanged += 1
24133
24648
  _report_conversation_progress(progress, "ingest", stats)
24134
24649
  continue
24650
+ # The raw shrink decides only the targeted deferral below,
24651
+ # which runs before the incarnation resolver and so cannot yet
24652
+ # tell a truncation below the cursor from a trimmed unfinished
24653
+ # tail; deferring both to a full sync is conservative. Past the
24654
+ # resolver, `truncated` is redefined as "a new incarnation".
24135
24655
  truncated = prev is not None and size < prev[0]
24136
24656
  if targeted and truncated:
24137
24657
  # #939 D1: every exit resolves the open batch.
@@ -24165,13 +24685,20 @@ def sync_claude_conversations(
24165
24685
  start_offset = incarnation.start_offset
24166
24686
  file_high_water = high_water.get(
24167
24687
  (path_str, incarnation.incarnation_id), 0)
24168
- if incarnation.is_new:
24169
- # A new incarnation forces byte-zero treatment: the
24170
- # bytes before the cursor are not the bytes the cursor
24171
- # described, so resuming from it would skip real records
24172
- # and stamp the rest against an identity that never held
24173
- # them.
24174
- truncated = prev is not None
24688
+ # A new incarnation forces byte-zero treatment: the
24689
+ # bytes before the cursor are not the bytes the cursor
24690
+ # described, so resuming from it would skip real records
24691
+ # and stamp the rest against an identity that never held
24692
+ # them. A CONTINUOUS incarnation is never a truncation,
24693
+ # even when the file shrank (#1062 F8): a pass that ended
24694
+ # on an unfinished last line records a size past its
24695
+ # cursor, and trimming that line shrinks the file without
24696
+ # moving a committed byte. Its committed rows stay; the
24697
+ # walk resumes from the cursor and the cursor row records
24698
+ # the smaller size. Treating that shrink as a truncation
24699
+ # deleted every row of the path and re-inserted only the
24700
+ # tail.
24701
+ truncated = incarnation.is_new and prev is not None
24175
24702
  # #939 D1: admission is decided BEFORE the parse. The
24176
24703
  # read limit is the size the incarnation decision saw
24177
24704
  # (the same fstat recorded as the change detector
@@ -27066,6 +27593,9 @@ def cmd_cache_sync(args: argparse.Namespace) -> int:
27066
27593
  # identity inventory inline, through the same admitted executor the
27067
27594
  # detached worker runs (a no-op once it is ready).
27068
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)
27069
27599
  prune_result = run_codex_prune_drain(wait_for_requests=True)
27070
27600
  verify_result = run_codex_container_verification_drain(wait_for_requests=True)
27071
27601
  run_codex_reset_drain(wait_for_requests=True)