Skip to content

fix(storage): a re-keyed raw replaces its stale verdict-cache row - #4693

Merged
Sinity merged 1 commit into
masterfrom
lane/verdict-cache-rekey
Sep 5, 2026
Merged

fix(storage): a re-keyed raw replaces its stale verdict-cache row#4693
Sinity merged 1 commit into
masterfrom
lane/verdict-cache-rekey

Conversation

@Sinity

@Sinity Sinity commented Sep 5, 2026

Copy link
Copy Markdown
Owner

Summary

write_raw_authority_verdict_cache deleted a cohort's rows by logical key and inserted by raw_id. A raw cached under a pending key and written again under its resolved key collided on the unique raw_id; the converger's raw_authority_verdict_cache stage then failed on every batch (84 UNIQUE constraint failed: raw_authority_verdicts.raw_id in rehearsal-10, 2026-09-05, /realm/tmp/work/rehearsal-4/daemon.log). The writer now also removes the incoming raw ids' rows before inserting.

Verification

devtools test tests/unit/storage/test_raw_authority_verdict_cache.py (new test_rekeyed_raw_replaces_its_stale_cache_row; red without the raw_id delete). Quick gate via the pre-push hook.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DNGJ3awfNrsLaMdHgQZvid

The cache writer deleted the cohort's rows by logical key and inserted by
raw_id; a raw cached under a pending key and written again under its
resolved key collided on the unique raw_id, and the converger's
verdict-cache stage failed on every batch (84 times in rehearsal-10).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DNGJ3awfNrsLaMdHgQZvid
@Sinity
Sinity enabled auto-merge (squash) September 5, 2026 15:45
@coderabbitai

coderabbitai Bot commented Sep 5, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Team

Run ID: f688c12d-43ed-4140-86aa-da5157f3d633


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 5, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-05T15:47:42.771486Z 60f387e PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@Sinity
Sinity merged commit 754553b into master Sep 5, 2026
4 checks passed
@Sinity
Sinity deleted the lane/verdict-cache-rekey branch September 5, 2026 15:51
Sinity added a commit that referenced this pull request Sep 5, 2026
…ut (#4694)

## Summary

During a chunked catch-up every chunk paid archive-wide convergence
costs, so per-chunk convergence grew with the archive. Whole-archive
stages now run once per catch-up (on the last chunk), the archive-wide
FTS readiness audit is its own deferred stage, hook-paste enrichment
reads only the batch's sessions' sidecars, FTS partition inspection uses
the `block_id` index instead of a `substr` scan, the raw-authority
warmer finds its work in two statements (its `raw_id` collision is fixed
by #4693, on which this branch is rebased), the periodic whole-archive
FTS sweep waits while catch-up is active, and the convergence ledger now
attributes stage `check` time. A separate defect found on the way: a
catch-up with no source file modified in the last hour ingested nothing.

## Problem

Rehearsal (fresh daemon, real sources, `/realm/tmp/work/rehearsal-4`):
chunk 1 parse 1.1 s / convergence 5.6 s; chunk 150 parse 23.6 s /
convergence 62.9 s for 1.9 MB; average over 150 chunks parse 10.6 s,
convergence 20.2 s, both growing with archive size (305 sessions, 45,657
messages). Chunk 149 held the writer 103.9 s while its stage ledger
summed to 2.5 s.

Profile (in-process sampler on a scratch daemon over 400 real files; SQL
timed against a copy of the rehearsal's 359-session index):

- `enrich_paste_from_hooks` after every chunk scanned every sidecar
journal in `hooks/` (371 files, 1.65 GB): 4.5 s warm, 72.9 s once the
page cache was evicted. Untimed (outside the stage ledger); this is the
size of chunk 149's unattributed time.
- `fts_invariant_snapshot_sync` (five whole-table aggregates over
`blocks`/`messages_fts_docsize`/identity) ran twice per chunk, from
`FtsConvergenceOwner._publish_readiness_projection` and
`_record_fts_freshness_after_insights`: 0.6 s per call at 90k blocks, 49
% of scratch convergence time (98 s of 200 s).
- FTS partition `inspect`/`publish` selected a session's identity rows
with `substr(block_id, 1, length(?)+1) = ? || ':'`: `SCAN
messages_fts_identity` per key, 10.5 ms at 85k rows vs 0.1 ms for the
index range.
- `raw_authority_verdict_cache.check_many` probed every cohort with two
statements (532 cohorts at chunk 150), and its warmup failed 54 times
with `UNIQUE constraint failed: raw_authority_verdicts.raw_id`,
re-running the whole-archive check on every following chunk.
- `claude_workflow` and `delegation_work_evidence` re-read every raw
artifact / every delegation row per chunk.
- `maintenance.fts_convergence` (whole-archive `inspect_all`) held the
writer 1061 s across 40 runs (max 132 s); chunks waited 1977 s on the
writer in total.
- Cold-start defect: `_catch_up` broke out of its (hot, cold) group loop
on the first empty group, so a backlog with no file modified in the last
hour was never planned. The after-run reproduced it (0 attempts, 0
cursors, `catch_up_complete` set after 40 s).

## Solution

- `ConvergenceStage.whole_archive`;
`DaemonConverger.converge_batch(whole_archive=False)` records such
stages `SKIPPED` (converged, no debt: their staleness is re-derived from
content). The watcher runs every catch-up chunk but the last in that
scope. Stages flagged: `raw_authority_verdict_cache`, `claude_workflow`,
`delegation_work_evidence`, and the new `fts_readiness`.
- `make_fts_readiness_stage` publishes the exact archive-wide FTS audit
once per pass, after `fts` and `derived`; partition-scoped FTS passes
and insight rebuilds no longer publish it. `_periodic_convergence_check`
skips the whole-archive FTS sweep while `watcher.catch_up_active`.
- `enrich_paste_from_hooks(db, session_ids=...)` reads only
`*-<native_id>.jsonl` for the batch's sessions (`session_ids_by_path` on
append, `changed_session_ids` on full ingest).
- FTS derivation: `[key || ':', key || ';')` range on the `block_id`
UNIQUE index in `inspect` (excess, duplicates) and `publish`.
- `find_raw_authority_verdict_cache_work`: one pass over `raw_sessions`
and one over `raw_authority_verdicts` (master's #4693 writer, which
removes a re-keyed raw's stale row, is kept as is).
- Converger ledger: `<stage>.check` and `hook_paste_enrichment` timings.
- Watcher: an empty priority group is skipped; only a stop request ends
the loop.

## Verification

- `.venv/bin/python -m devtools verify --quick` -> all 13 gates `ok`
(format, lint, mypy, generated-surfaces, layering, patterns,
doc-commands, schema-manifest, oracle-integrity, testmon-selection,
consumer-reachability, timestamp-doctrine, schema-privacy).
- `.venv/bin/python -m devtools test
tests/unit/daemon/test_catch_up_chunk_cost.py
tests/unit/daemon/test_daemon_convergence.py
tests/unit/sources/test_hook_paste_enrichment.py
tests/unit/storage/test_fts_derivation.py
tests/unit/storage/test_raw_authority_verdict_cache.py
tests/unit/daemon/test_convergence_stages.py
tests/unit/daemon/test_fts_readiness_fallback.py
tests/unit/daemon/test_fts_identity_convergence.py
tests/unit/sources/test_live_batch_convergence.py
tests/unit/pipeline/test_ingest_batch_fts_repair.py
tests/unit/sources/test_live_catchup_planning.py
tests/unit/sources/test_live_watcher.py
tests/unit/sources/test_live_hot_session_convergence.py
tests/unit/daemon/test_daemon_cli.py` -> pending (host pytest slot,
pueue task 1646); this line is updated with the receipt
- New tests:
`test_chunk_convergence_cost_does_not_grow_with_archive_size` (real
live-ingest route, archives of 2 and 14 sessions, one 2-file chunk:
product SQL statements inside `_converge_paths` equal, 2 sidecars read,
0 snapshot calls, no deferred stage in the ledger; reverting any
deferral adds two statements per cohort / one per artifact row),
`test_final_catch_up_chunk_runs_the_whole_archive_stages`, converger
chunk-scope and check-ledger, sidecar scoping, FTS `EXPLAIN QUERY PLAN`
has no `SCAN` of the identity ledger, raw-authority rebinding and
two-statement work finder, cold-backlog catch-up, last-chunk
`whole_archive_convergence` flag.

Scratch run (same host, same 400 files / 325 MB from
`~/.claude/projects` and `~/.codex/sessions`, 100 catch-up chunks,
`polylogued run` with an in-process sampler):

| | before (dfb1322) | after |
| --- | --- | --- |
| convergence total | 200.4 s | 64.1 s |
| convergence per chunk, by 20-chunk window | 1.61 / 1.89 / 2.14 / 2.60
/ 1.78 s | 0.71 / 0.56 / 0.70 / 0.69 / 0.54 s |
| `fts_invariant_snapshot_sync` inside the chunk path | 62 s | 0 s
(once, on the last chunk) |
| raw-authority warm inside the chunk path | 40 s | 0 s |
| periodic whole-archive FTS sweeps during catch-up | 16 | 0 |
| parse total | 406.0 s | 534.0 s (three chunks at 79 / 57 / 39 s in
plain `messages`/`blocks` inserts; a full-corpus pytest ran on the host
during the after run and was OOM-killed, pueue 1636 exit 137) |
| end to end | 0.54 MB/s | 0.55 MB/s (0.77 MB/s without the three
contended chunks) |

Per-chunk convergence no longer grows with the archive. End-to-end
throughput on this scratch is parse-bound (0.61–0.80 MB/s parse-only),
so the 2x-of-parse criterion from polylogue-t4iy5.11.1 still needs the
rehearsal driver on a quiet host for its operational proof.

## Residuals

- The medium health tier (`_check_fts_readiness_medium`) recomputes the
exact archive-wide FTS audit on every status payload: 51 s of the
after-run's profile, all from `/api/status` and the status collector.
The rehearsal driver probes status every 60 s; at 5M blocks each probe
would take tens of seconds. It should read the freshness ledger the
`fts_readiness` stage publishes.
- Index write cost is per-session CPU (parser-source fingerprinting is
22 % of `index_parsed_write`; `messages`/`blocks` inserts with 18
secondary indexes) plus page-cache-miss stalls under host memory
pressure; no whole-archive statement was found on the write path.
- `claude_workflow` and `delegation_work_evidence` still rebuild from
the whole archive whenever they run (once per catch-up now, then per
live batch).
- If a catch-up stops before its last chunk, the deferred stages run on
the next live batch or catch-up rather than immediately.

Beads: polylogue-t4iy5.11.1, polylogue-623q


🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01DNGJ3awfNrsLaMdHgQZvid

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant