fix(daemon): reconcile blob publications periodically - #3886
Conversation
Problem: Startup reconciliation clears terminal publication reservations, but reservations can become safely classifiable during a long-lived daemon run. Unresolved reservations must remain protected for explicit abandonment policy. What changed: Add an independently scheduled, coordinator-owned reconciliation loop beside blob GC. Reuse the exclusion-backed classifier with a maintenance actor, register the loop in daemon startup and schema-blocked inventories, and cover repeated safe cleanup plus lifecycle cancellation through real archive fixtures. Compatibility: No abandoned-reservation policy or live cleanup behavior changes. Unresolved reservations remain durable. Verification: direnv exec . devtools test tests/unit/daemon/test_blob_gc_periodic.py tests/unit/daemon/test_daemon_cli.py::test_run_daemon_services_waits_for_fts_startup_before_watcher tests/unit/daemon/test_daemon_cli.py::test_run_daemon_services_drains_servers_when_main_task_is_cancelled tests/unit/daemon/test_catch_up_gate_timeout.py::test_run_daemon_services_schema_block_logs_parked_loops_and_emits_event; direnv exec . devtools verify --quick Ref polylogue-7rds Co-Authored-By: Claude <noreply@anthropic.com>
Preserve the concrete reconciliation result type across the shared startup and periodic daemon route instead of widening the new return value to Any.
Record the real-route verification for polylogue-7rds and close the implementation scope. Link the unavailable default testmon gate to polylogue-93xe, which owns the exact-master verification baseline.
|
Warning Review limit reached
Next review available in: 42 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (5)
📝 WalkthroughWalkthroughThe daemon now runs blob-publication reconciliation every blob-GC interval after catch-up. It uses the write coordinator, retries transient SQLite locks, preserves unresolved reservations, and includes startup and cancellation lifecycle tests. ChangesBlob-publication reconciliation
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant Daemon
participant ReconciliationLoop
participant WriteCoordinator
Daemon->>ReconciliationLoop: start after watcher catch-up
ReconciliationLoop->>WriteCoordinator: reconcile publication reservations
WriteCoordinator-->>ReconciliationLoop: return reconciliation result
ReconciliationLoop-->>Daemon: emit maintenance event
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.beads/issues.jsonl:
- Line 912: Update the issue record for polylogue-7rds so the relates-to
dependency on polylogue-93xe is created before the closed_at timestamp and
closure metadata, preserving consistent dependency ordering and counts. Keep the
existing residual-transfer relationship and closure disposition intact.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 8a4153ee-5faa-4799-b978-93590bc47492
📒 Files selected for processing (5)
.beads/issues.jsonlpolylogue/daemon/blob_gc_periodic.pypolylogue/daemon/cli.pytests/unit/daemon/test_blob_gc_periodic.pytests/unit/daemon/test_daemon_cli.py
Correct the successor-edge timestamp to UTC so the durable residual transfer precedes the polylogue-7rds closure record.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d90ed88438
ℹ️ 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".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| while True: | ||
| await asyncio.sleep(BLOB_PUBLICATION_RECONCILIATION_INTERVAL_SECONDS) | ||
| try: | ||
| await _reconcile_blob_publications(actor="maintenance.blob_publication_reconciliation") |
There was a problem hiding this comment.
Resolve the active index before periodic reconciliation
When .index-active-pointer selects a generation different from archive_root/index.db—the supported interrupted-promotion state—the new periodic loop delegates to _reconcile_blob_publications, which still passes the conventional root / "index.db" path. A crash-left receipt referenced only by an attachment in the active generation is therefore classified as unresolved and retained on every tick instead of being cleared; resolve the active index with resolve_active_index_path(root) before invoking the reconciler.
Useful? React with 👍 / 👎.
| while True: | ||
| await asyncio.sleep(BLOB_PUBLICATION_RECONCILIATION_INTERVAL_SECONDS) | ||
| try: | ||
| await _reconcile_blob_publications(actor="maintenance.blob_publication_reconciliation") |
There was a problem hiding this comment.
Bound each periodic reconciliation pass
When a failed bulk publication leaves a large reservation backlog, this scheduled call holds the daemon write coordinator while inspect_blob_publication_receipts loads every row, performs per-row filesystem/reference checks, and then deletes the entire clearable set in one transaction. Unlike the adjacent blob-GC loop's 200-row cap, there is no batch limit here, so a periodic tick can monopolize Polylogue's sole writer and stall live ingestion and API writes for an unbounded interval; paginate a bounded number of reservations per pass and leave the remainder for later ticks.
AGENTS.md reference: AGENTS.md:L181-L183
Useful? React with 👍 / 👎.
Resolve attachment references against the active index generation. Page periodic reservation scans in 200-row windows with a stable cursor so unresolved rows do not starve later terminal cleanup or monopolize the daemon writer.
Summary
Reconcile safely terminal blob-publication reservations every 900 seconds during daemon uptime. The periodic path uses the daemon write coordinator, resolves the active index generation, advances through bounded 200-row pages, and preserves unresolved reservations that still have bytes but no reference.
Problem
Blob-publication reconciliation ran only during daemon startup. A long-lived daemon could retain reservations that later became safely clearable until the next restart, while the two measured unresolved reservations correctly remained outside automatic cleanup.
Solution
Add a periodic maintenance loop beside blob GC, register it in daemon service lifecycle and schema blocking, and return a typed reconciliation outcome from the shared operation. A stable publication-ID cursor prevents retained rows from starving later cleanup. Real source and active-generation fixtures prove that missing-blob and referenced reservations are cleared while present, unreferenced reservations survive repeated ticks.
Verification
devtools test tests/unit/daemon/test_blob_gc_periodic.py tests/unit/daemon/test_daemon_cli.py tests/unit/pipeline/test_blob_publication_crash_matrix.py tests/unit/pipeline/test_acquisition_blob_gc_age_gate.py -k 'publication or reconcile_blob_publications'Result: 11 passed, 128 deselected.
The same four files without the selector produced 134 passes and five failures. All five node IDs are present in the exact-master baseline ledger.
devtools verify --quickResult: all 24 steps passed in
20260808T124246Z-quick-1875233-196795f4at625a0851ca6110f41428baa632a17eed172aaba2.devtools verifyrefused before test selection because current master has no valid testmon seed. The exact-master full baseline20260808T062115Z-full-1249172-f00c535fhas 414 failures and 16 errors. That verification-stack residual remains underpolylogue-93xe.Bead disposition matrix
polylogue-7rdspolylogue-93xeRisks and Follow-ups
No production mutation was performed in this lane. The periodic operation deliberately does not clear present, unreferenced reservations; explicit abandonment retains its existing confirmation and receipt protocol.