Skip to content

fix(maintenance): add scoped cursor authority reconciliation - #3860

Merged
Sinity merged 6 commits into
masterfrom
feature/fix/cursor-authority-reconcile-v2
Aug 6, 2026
Merged

fix(maintenance): add scoped cursor authority reconciliation#3860
Sinity merged 6 commits into
masterfrom
feature/fix/cursor-authority-reconcile-v2

Conversation

@Sinity

@Sinity Sinity commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Summary

Add the scoped maintenance surface required to reconcile the one known cursor-ahead violation without disabling the global fail-closed authority gate.

Problem

PR #3823 made cursor authority load-bearing across watcher, catch-up, flush, convergence, recovery, and reindex selection. That correctly parks ordinary ingestion while the known cursor-ahead relation remains, but leaves no safe route to reconcile the exact affected path. The live archive also contains 725/2 typed incomparable rows that must remain visible and must not be mistaken for the true ahead violation.

Solution

Add polylogue ops maintenance cursor-authority-reconcile, dry-run by default. The plan path is supplied through a mode-0600 file, the archive root is fixed to /realm/db/polylogue, daemon ownership is required to be absent, source/index/ops/audit fingerprints and schema versions are captured, the selected byte-authoritative accepted head is checked against a two-stat source-prefix hash, and the plan is immutable and self-digested. Apply requires a verified full_evidence backup, reacquires writer ownership, revalidates every binding, installs a context-local single-use authorization bound to path digest, cursor offset, accepted frontier, and plan digest, then invokes the existing full-ingest route. No direct cursor, accepted-head, or source-row edits are present. The postcondition is either reconciled or an explicitly typed deferred attempt; the pre-existing incomparable population must remain unchanged. Behavior tests cover the real watcher seam, immutable plans, permissions, tampering, multiple-ahead refusal, source mutation, single-use and changed-frontier authorization, healthy-archive refusal, backup evidence, CLI surface, raw-frontier behavior, and exact-frontier ingestion.

Verification

direnv exec . devtools test tests/unit/maintenance/test_cursor_authority_reconcile.py produced 23 passed.

direnv exec . devtools test tests/unit/sources/test_live_watcher.py -k 'cursor_authority or authoritative_frontier' produced 2 passed, 107 deselected.

direnv exec . devtools test tests/unit/storage/test_raw_retention.py -k cursor produced 6 passed, 59 deselected.

direnv exec . devtools test tests/unit/cli/test_archive_maintenance_cli.py -k 'cursor_authority or cursor or maintenance_help' produced 9 passed, 69 deselected.

direnv exec . devtools verify --quick produced exit code 0, including format, lint, mypy, generated surfaces, layering, graph, schema, CI, documentation, and policy checks.

Follow-ups

The implementation is intentionally partial. polylogue-cursor-authority-live-proof remains open for the operator-controlled production dry-run, fresh backup, apply, immutable receipt, and consumption by polylogue-live-operation-receipts. No production access or live receipt is included here.

Ref polylogue-cursor-authority-reconcile-implementation

Summary by CodeRabbit

  • New Features

    • Added the cursor-authority-reconcile maintenance command with dry-run planning and controlled apply workflows.
    • Added backup validation, integrity checks, authorization safeguards, and durable receipts for reconciliation results.
    • Added protections against unintended cursor changes, source mutations, and incomplete recovery evidence.
  • Documentation

    • Documented prerequisites, backup requirements, plan application, receipt handling, and operational restrictions.
  • Bug Fixes

    • Improved archive index resolution and safeguards around live ingestion and cursor handling.

Sinity and others added 2 commits August 6, 2026 15:31
Problem: live ingestion fails closed when a cursor is ahead of accepted byte authority, leaving no scoped recovery route.

What changed: add digest-bound dry-run planning and full-evidence backup validation; route apply through LiveBatchProcessor's normal full-ingest path under a context-local single-use path and frontier authorization; emit atomic plan and receipt evidence without direct cursor or accepted-head writes.

Compatibility/migration: dry-run is default and resolves only /realm/db/polylogue; apply requires the daemon stopped, an immutable plan, a verified full_evidence backup, and a new receipt.

Co-Authored-By: Claude <noreply@anthropic.com>
Problem: the scoped reconciliation route rejected the real incomparable cursor population and did not model the typed deferred outcome.

What changed: preserve incomparable rows while binding the one true ahead row, make plans immutable, validate the unchanged gap census, and expand behavior tests for the scoped authorization and backup boundary.

Compatibility/migration: the command remains dry-run by default and production apply remains backup-gated.

Ref polylogue-cursor-authority-reconcile-implementation

Co-Authored-By: Claude <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a backup-gated cursor-authority-reconcile workflow. It creates deterministic plans, validates archive and backup evidence, authorizes one source-specific live ingestion, verifies postconditions, and writes receipts without direct cursor or accepted-head writes.

Changes

Cursor authority reconciliation

Layer / File(s) Summary
Scoped cursor-authority gate
polylogue/sources/live/batch.py, tests/unit/sources/test_live_watcher.py, tests/unit/maintenance/test_cursor_authority_reconcile.py
Live ingestion now supports single-use, plan-bound authorization for one source path and frontier. Unmatched or reused authorization remains blocked. Active archive index lookups use ArchiveLocation.
Reconciliation planning and evidence
polylogue/maintenance/cursor_authority_reconcile.py, tests/unit/maintenance/test_cursor_authority_reconcile.py
The planner validates protected source-path input, captures SQLite and cursor evidence, redacts source paths with digests, preserves unrelated populations, and writes deterministic plans atomically.
Guarded reconciliation apply
polylogue/maintenance/cursor_authority_reconcile.py, tests/unit/maintenance/test_cursor_authority_reconcile.py
Apply mode validates plan digests, backup rollback evidence, tier fingerprints, daemon state, ingestion postconditions, and receipt integrity. It records reconciled, deferred, observed, and failed outcomes.
Maintenance CLI surface
polylogue/cli/commands/maintenance/*, docs/devtools.md, tests/unit/cli/test_archive_maintenance_cli.py
Registers and documents cursor-authority-reconcile. The CLI exposes scoped plan, backup, receipt, and apply options without unrestricted bypass options.
Test resource handling
tests/unit/storage/test_raw_retention.py
The raw-retention cleanup test now closes its SQLite connection with a context manager.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Operator
  participant MaintenanceCLI
  participant Reconciliation
  participant ArchiveSQLite
  participant LiveBatchProcessor
  Operator->>MaintenanceCLI: run reconciliation command
  MaintenanceCLI->>Reconciliation: build plan or apply plan
  Reconciliation->>ArchiveSQLite: validate archive, cursor, and backup evidence
  Reconciliation->>LiveBatchProcessor: ingest selected source with scoped authorization
  LiveBatchProcessor->>ArchiveSQLite: validate gate and persist normal ingestion
  Reconciliation->>Operator: return plan or receipt
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.88% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the maintenance change and the scoped cursor-authority reconciliation it adds.
Description check ✅ Passed The description covers the required summary, problem, solution, verification, scope disposition, and follow-up sections, but omits the required Unreleased changelog entry for this user-visible command.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/fix/cursor-authority-reconcile-v2

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

🤖 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 `@polylogue/maintenance/cursor_authority_reconcile.py`:
- Around line 511-525: Update the reconciliation receipt construction in the
recovery and typed-deferred branches to avoid asserting a fixed changed_rows
count: derive the cursor change from the before/after projections or record the
count as null when it cannot be verified. Validate and retrieve
plan["before_projection"] once before either branch, reusing the existing
defensive validation used near the earlier access, so missing or invalid data
raises CursorAuthorityReconciliationError instead of KeyError.
- Around line 432-453: Update _find_recovery_attempt and its caller
apply_reconciliation to require a completed ingest_attempts record whose
relevant timestamp is later than the reconciliation plan’s observation time,
passing that plan timestamp into the lookup. When such a recovery is found, mark
the receipt outcome as observed rather than performed and do not report it as
this command’s reconciliation or claim a locally changed cursor row.
- Around line 117-124: Update _sqlite_snapshot to open the tier file once and
derive both the SHA-256 digest and byte size from that same file descriptor,
using os.fstat for the size and hashing the opened stream. Remove the separate
path.stat() and _sha256_file(path) observations while preserving the returned
size_bytes and sha256 fields.
- Around line 420-425: Extract the repeated daemon precondition from both entry
points around the plan and apply flows into one shared helper, reusing
_archive_root, the minimal Config construction, running_daemon_pid, and
CursorAuthorityReconciliationError. Replace both inline guards with calls to
that helper so both operations consistently require the daemon to be stopped.
- Around line 75-96: Update _read_private_source_path to open path_file once and
retain its descriptor, use os.fstat on that descriptor for the regular-file,
single-link, ownership, and mode checks, then read the path contents from the
same descriptor instead of calling lstat followed by read_text. Preserve the
existing validation errors and path-resolution behavior.
- Around line 356-381: Require verification to be a dictionary containing
source_blobs_resolved, index_attachment_blobs_resolved, and
blob_inventory_exact, with all three values true; reject missing or incomplete
verification before the blob path checks in the reconciliation validation flow.
Also guard the backup_tier fingerprint file access in the tier loop so a missing
tier file is converted into CursorAuthorityReconciliationError with the existing
mismatch/error handling rather than allowing FileNotFoundError to escape.
- Around line 395-416: Update _write_atomic_json so refuse_existing uses an
atomic non-overwriting publication, such as linking temporary_path to path and
handling FileExistsError as CursorAuthorityReconciliationError, instead of
os.replace. Preserve os.replace for the overwrite-allowed case and retain
temporary-file cleanup and directory fsync behavior.
- Around line 245-249: Update the reconciliation logic around the frontier and
cursor-offset extraction to convert invalid or missing data into
CursorAuthorityReconciliationError. Validate head[5] and raw[3] before integer
conversion, and replace the unguarded next(...) lookup in _cursor_rows with
explicit missing-row handling that raises the typed error, preserving
apply_reconciliation’s recovery path.
- Around line 536-559: Update the post-ingest validation flow in
apply_reconciliation after _normal_ingest returns so every unexpected
postcondition failure writes the standard audit receipt before raising. Reuse
the existing receipt construction and persistence path, recording a
typed_deferred/failure verdict with the observed metrics and projection state,
then raise CursorAuthorityReconciliationError. Cover failures for raw-frontier
worsening, invalid cursor-ahead reconciliation, and changed pre-existing cursor
population.
- Around line 169-195: Update _private_projection to redact logical_source_key
with the same established digest used for source_path, and make both sample
branches retain dictionary samples even when source_path is missing, setting it
to None consistently. Also apply the logical_source_key redaction in
_head_details so plans and receipts never persist the live provider session
identifier.

In `@tests/unit/cli/test_archive_maintenance_cli.py`:
- Around line 1729-1742: Expand
test_cursor_authority_reconcile_cli_exposes_only_scoped_inputs to assert --plan
and --receipt, then add focused CLI invocations covering missing required
options and invalid mixed dry-run/apply combinations. Verify each invocation
returns the expected validation failure while preserving the existing
help-output scope assertions.

In `@tests/unit/maintenance/test_cursor_authority_reconcile.py`:
- Around line 232-247: Extend
test_backup_validation_requires_blob_rollback_evidence or add focused tests that
create the required blob directory and backup tier files, then cover
_validate_backup fingerprint mismatches and byte-level re-hash failures with the
expected reconciliation errors. Add tests for apply_reconciliation covering both
the recovered-attempt branch and the typed_deferred verdict, including their
expected state and outcomes.
- Around line 204-208: Update the SQLite mutation block in the cursor
reconciliation test to import and use contextlib.closing around sqlite3.connect,
while retaining the existing transaction context so changes are committed and
the connection is deterministically closed before the code under test reopens
the database.
🪄 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: 92fa2c72-dab9-44c5-80dd-bb6adc3b3172

📥 Commits

Reviewing files that changed from the base of the PR and between 6e680aa and 95fe989.

📒 Files selected for processing (8)
  • docs/devtools.md
  • polylogue/cli/commands/maintenance/__init__.py
  • polylogue/cli/commands/maintenance/_cursor_authority.py
  • polylogue/maintenance/cursor_authority_reconcile.py
  • polylogue/sources/live/batch.py
  • tests/unit/cli/test_archive_maintenance_cli.py
  • tests/unit/maintenance/test_cursor_authority_reconcile.py
  • tests/unit/sources/test_live_watcher.py

Comment thread polylogue/maintenance/cursor_authority_reconcile.py
Comment thread polylogue/maintenance/cursor_authority_reconcile.py
Comment thread polylogue/maintenance/cursor_authority_reconcile.py Outdated
Comment thread polylogue/maintenance/cursor_authority_reconcile.py Outdated
Comment thread polylogue/maintenance/cursor_authority_reconcile.py Outdated
Comment on lines +511 to +525
recovered_receipt_payload: dict[str, object] = {
"format": RECEIPT_FORMAT,
"verdict": "reconciled" if after_projection.overall_status == "healthy" else "typed_deferred",
"archive_identity": {"root": str(root.resolve())},
"plan_digest": plan["plan_digest"],
"backup": backup_evidence,
"before_projection": plan["before_projection"],
"after_projection": _private_projection(after_projection),
"changed_rows": {"cursor": 1, "accepted_head_direct_writes": 0},
"ingest_attempt_id": recovery_attempt,
"code_sha": plan["code_sha"],
"deployed_package_sha": plan["deployed_package_sha"],
"tier_fingerprints": _tier_snapshots(root),
"quick_check": _quick_checks(root),
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Do not assert changed_rows as a constant, and guard the before_projection subscript.

Line 519 writes "changed_rows": {"cursor": 1, "accepted_head_direct_writes": 0} as a literal in both receipt payloads (also line 572). The value is never measured. In the recovery branch no ingest ran in this command, and in the typed_deferred branch the ingest failed, yet both receipts still claim one cursor row changed. A receipt that states an unverified count is worse than one that omits it. Derive the count from the before and after projections, or record it as null.

Line 517 subscripts plan["before_projection"] directly. _load_plan verifies only the format and the digest, so the key can be absent and the apply then raises KeyError instead of CursorAuthorityReconciliationError. Lines 503-506 in the same branch already read the same field defensively, so the two accesses disagree. The second branch validates it at lines 552-554 before use; apply the same validation once, before both branches.

🤖 Prompt for 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.

In `@polylogue/maintenance/cursor_authority_reconcile.py` around lines 511 - 525,
Update the reconciliation receipt construction in the recovery and
typed-deferred branches to avoid asserting a fixed changed_rows count: derive
the cursor change from the before/after projections or record the count as null
when it cannot be verified. Validate and retrieve plan["before_projection"] once
before either branch, reusing the existing defensive validation used near the
earlier access, so missing or invalid data raises
CursorAuthorityReconciliationError instead of KeyError.

Comment thread polylogue/maintenance/cursor_authority_reconcile.py Outdated
Comment thread tests/unit/cli/test_archive_maintenance_cli.py
Comment on lines +204 to +208
with sqlite3.connect(cursor._db_path) as conn:
conn.execute(
"UPDATE ingest_cursor SET byte_offset = byte_offset + 1 WHERE source_path = ?",
(str(source_path),),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Close the SQLite connection after the cursor mutation.

with sqlite3.connect(...) commits the transaction but does not close the connection. The connection to ops.db stays open for the rest of the test, and the code under test re-opens the same file to rebuild the frontier projection. Use contextlib.closing so the write is released deterministically.

♻️ Proposed fix
-        with sqlite3.connect(cursor._db_path) as conn:
-            conn.execute(
-                "UPDATE ingest_cursor SET byte_offset = byte_offset + 1 WHERE source_path = ?",
-                (str(source_path),),
-            )
+        with closing(sqlite3.connect(cursor._db_path)) as conn, conn:
+            conn.execute(
+                "UPDATE ingest_cursor SET byte_offset = byte_offset + 1 WHERE source_path = ?",
+                (str(source_path),),
+            )

Add the import at the top of the file:

from contextlib import closing
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
with sqlite3.connect(cursor._db_path) as conn:
conn.execute(
"UPDATE ingest_cursor SET byte_offset = byte_offset + 1 WHERE source_path = ?",
(str(source_path),),
)
with closing(sqlite3.connect(cursor._db_path)) as conn, conn:
conn.execute(
"UPDATE ingest_cursor SET byte_offset = byte_offset + 1 WHERE source_path = ?",
(str(source_path),),
)
🤖 Prompt for 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.

In `@tests/unit/maintenance/test_cursor_authority_reconcile.py` around lines 204 -
208, Update the SQLite mutation block in the cursor reconciliation test to
import and use contextlib.closing around sqlite3.connect, while retaining the
existing transaction context so changes are committed and the connection is
deterministically closed before the code under test reopens the database.

Comment thread tests/unit/maintenance/test_cursor_authority_reconcile.py

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 95fe9893d7

ℹ️ 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".

raise CursorAuthorityReconciliationError("backup lacks a readable verification receipt") from exc
if not isinstance(manifest, dict) or manifest.get("profile") != "full_evidence":
raise CursorAuthorityReconciliationError("apply requires a full_evidence backup")
if not isinstance(receipt, dict) or receipt.get("verdict") != "success":

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Verify the backup receipt attestation

When an incomplete or fabricated backup directory is supplied, this accepts any JSON receipt whose verdict is success; the verification object is optional, and the blob checks only require a directory and inventory file to exist. It never calls verify_verification_receipt or binds the signed receipt evidence to the manifest, tier artifacts, and blobs, so an empty blob backup with hand-written metadata can authorize the source-tier mutation without usable rollback evidence.

Useful? React with 👍 / 👎.

Comment on lines +500 to +502
recovery_attempt = _find_recovery_attempt(root, current_path)
if recovery_attempt is None or after_projection.cursor_ahead_count != 0:
raise CursorAuthorityReconciliationError("plan bindings changed before archive ownership")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Bind recovered attempts to the reconciliation plan

When the planned violation disappears before apply, _find_recovery_attempt accepts any of the last 50 completed attempts for the same path, without requiring that it occurred after the plan or carried this plan digest and scoped authorization. A prior ordinary attempt will commonly satisfy this, so an unrelated cursor edit or ingest can produce a receipt claiming this plan reconciled the archive and citing an unrelated attempt ID.

Useful? React with 👍 / 👎.

Comment on lines +542 to +544
metrics.succeeded_file_count != 0
or str(current_path) not in metrics.failed_paths
or metrics.time_budget_exceeded

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Require a typed deferred disposition before issuing a receipt

When the authorized ingest fails while the cursor remains ahead, these checks classify every zero-success failed-path result as typed_deferred without inspecting the ingest attempt's disposition. For example, SchemaVersionMismatchError or an unclassified parser failure meets these metric conditions, so apply emits an accepted deferred receipt even though the normal ingest route recorded a structural or unknown failure rather than an explicitly typed deferral.

Useful? React with 👍 / 👎.

accepted_frontier=_plan_int(plan, "accepted_frontier"),
plan_digest=str(plan["plan_digest"]),
):
metrics = await processor.ingest_files([source_path], emit_event=False)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Force reconciliation through the full-ingest route

When the stored parser fingerprint is current and the source has bytes beyond the ahead cursor, this generic call allows _append_plan to start reading at the untrusted cursor.byte_offset rather than the proven accepted_frontier. If the ahead gap contains one or more complete records, those records are skipped permanently while the later append can succeed, advance the cursor, make the projection healthy, and produce a reconciled receipt despite missing source data.

Useful? React with 👍 / 👎.

sample = projection.cursor_ahead_samples[0]
if Path(sample.source_path).resolve() != source_path.resolve():
raise CursorAuthorityReconciliationError("selected source path is not the sole cursor-ahead path")
with closing(sqlite3.connect(f"file:{(root / 'index.db').resolve()}?mode=ro", uri=True)) as conn:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Resolve the active index generation before reconciling

When .index-active-pointer targets a generation different from the conventional root/index.db, this query reads the stale shadow index, and _normal_ingest later explicitly opens that same stale path. The command can therefore plan and write source/index state against a non-serving generation while leaving the actual active index unreconciled, creating cross-tier divergence instead of repairing the live archive.

Useful? React with 👍 / 👎.

@click.option("--source-path-file", type=click.Path(path_type=Path, dir_okay=False), default=None)
@click.option("--output-plan", type=click.Path(path_type=Path, dir_okay=False), default=None)
@click.option("--plan", "plan_path", type=click.Path(path_type=Path, dir_okay=False), default=None)
@click.option("--backup-manifest", type=click.Path(path_type=Path, dir_okay=False), default=None)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Accept the documented backup directory argument

When the operator follows the apply example in docs/devtools.md:335 and passes the full-evidence backup directory, Click rejects it because this option sets dir_okay=False, even though _backup_root explicitly supports a directory and the documented value is /private/full-evidence-backup. As written, the published apply command cannot reach reconciliation unless the operator instead guesses that they must pass the nested manifest.json file.

Useful? React with 👍 / 👎.

polylogue,
(WatchSource(name=source_path.parent.name, root=source_path.parent),),
cursor=cursor,
parser_fingerprint="live-batched-v2",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use the live parser fingerprint instead of a frozen literal

When the watcher parser fingerprint is bumped for a parser-semantics change, this maintenance processor remains pinned to live-batched-v2. Even if reconciliation correctly falls back to a full capture, it writes the repaired cursor with the stale literal, so the subsequently started daemon sees a parser-fingerprint mismatch and immediately revives and full-reingests the same potentially large source instead of treating the reconciliation result as current.

Useful? React with 👍 / 👎.

Sinity added 2 commits August 6, 2026 16:16
Problem: cursor-authority reconciliation trusted racy file observations, fixed receipt counts, and incomplete recovery and backup evidence.\n\nWhat changed: make source and tier observations descriptor-safe, scope recovery to later completed attempts, validate typed frontier and backup data, publish refuse-existing receipts atomically, and record observed, deferred, and failed outcomes with redacted projections. Extend the focused maintenance, CLI, and retention tests.\n\nCompatibility/migration: the route remains daemon-stopped, backup-gated, and limited to the normal scoped live-ingest path.\n\nCo-Authored-By: Claude <noreply@anthropic.com>
Problem: post-ingest receipt construction could fail while reporting the original reconciliation failure, and recovery variables shadowed the normal post-state.\n\nWhat changed: make failure receipts tolerant of unavailable post-state evidence, label recovery observations explicitly, and tighten typed test assertions.\n\nCompatibility/migration: no archive schema or mutation route changes.\n\nCo-Authored-By: Claude <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a89f2b2b11

ℹ️ 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".

attempt_id=attempt_id,
attempt_observation="performed",
evidence=evidence,
tolerate_state_errors=True,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject successful receipts when integrity checks fail

When post-ingest _tier_snapshots or PRAGMA quick_check fails—for example because reconciliation leaves a tier corrupt or unreadable—this flag suppresses the error and still writes a reconciled or typed_deferred receipt with both integrity fields set to null. That allows downstream operators to treat the repair as accepted precisely when its final archive integrity could not be established; successful receipt construction should remain fail-closed.

Useful? React with 👍 / 👎.

Comment on lines +707 to +711
failure_payload = _receipt_payload(
plan=plan,
backup=backup_evidence,
root=root,
verdict="failed",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve failure receipts when post-state evidence is unavailable

When ingest or a postcondition fails because an archive tier has become missing, unreadable, or otherwise uninspectable, this failure-path call retains the default tolerate_state_errors=False; _receipt_payload then repeats the failing tier snapshot/quick-check and exits before writing the promised failed receipt, often masking the original reconciliation error as well. The tolerant state-evidence behavior belongs on this failure receipt path rather than on accepted receipts.

Useful? React with 👍 / 👎.

Comment on lines +335 to +339
if projection.cursor_ahead_count != 1:
raise CursorAuthorityReconciliationError("refusing to guess among multiple cursor-ahead rows")
if projection.broken_head_count or projection.missing_source_raw_count:
raise CursorAuthorityReconciliationError(
"global raw-frontier violation set is not exactly one cursor-ahead row"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Require healthy sibling checks before authorizing reconciliation

When raw_materialization_readiness_snapshot is unavailable because one of its classification queries fails, the projection reports missing_source_raw_status="unknown" but leaves missing_source_raw_count at zero; because a proven cursor-ahead violation dominates unknown status, overall_status is still violated. These count-only checks therefore create a plan, and the scoped gate likewise accepts it, even though the command has not proven that missing source evidence is absent. Require the projection to be fully available, or explicitly require the broken-head and missing-source components to be healthy, before permitting the one-violation repair.

Useful? React with 👍 / 👎.

Comment on lines +310 to +312
def _build_plan(root: Path, source_path: Path, *, require_candidate: bool = True) -> dict[str, object]:
tiers = _tier_snapshots(root)
projection = _projection_for(root)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Fingerprint checkpointed tier bytes in reconciliation plans

When the stopped daemon leaves any required tier with nonempty WAL frames, _tier_snapshots hashes only the pre-checkpoint main database file. A subsequent verified backup calls daemon.backup._backup_sqlite, which checkpoints and truncates the WAL before recording and copying the tier, so the backup's source fingerprint and bytes no longer match this plan even though the logical archive did not change. The documented plan-plus-fresh-backup workflow is therefore unusable for this common SQLite state unless planning first obtains the same checkpointed snapshot or otherwise fingerprints the complete WAL-aware database image.

Useful? React with 👍 / 👎.

Comment on lines +621 to +624
if current_plan is None or not _same_plan_bindings(current_plan, plan):
recovery_projection = _projection_for(root)
recovery_attempt = _find_recovery_attempt(root, current_path, _plan_int(plan, "observed_at_ms"))
if recovery_attempt is None or recovery_projection.cursor_ahead_count != 0:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Acquire archive ownership before accepting observed recovery

When the planned violation has disappeared, this branch validates the projection and writes an accepted recovery receipt without ever acquiring the shared archive lease used by the normal branch below. A daemon can start after _require_daemon_stopped returns and mutate the tiers while the recovery projection, quick checks, and fingerprints are collected, producing a receipt that combines inconsistent observations or no longer describes the live archive. Acquire ownership before deciding between observed recovery and performed reconciliation.

AGENTS.md reference: AGENTS.md:L181-L183

Useful? React with 👍 / 👎.

Comment on lines +590 to +593
"backup": dict(backup),
"before_projection": dict(before_projection),
"after_projection": _private_projection(after_projection) if after_projection is not None else None,
"metrics": metrics.to_payload() if metrics is not None else None,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Redact private identities from reconciliation metrics

When reconciliation is deferred or fails, LiveBatchMetrics.to_payload() includes the selected absolute path in failed_paths; successful append-style metrics can also include raw session identities in new_sessions or updated_sessions. This payload is placed directly into the durable receipt and, for a returned typed-deferred result, echoed by the CLI, bypassing the command's mode-0600 path-file boundary and the digest redaction applied to the plan and projections. Serialize a reconciliation-specific redacted metrics payload instead.

Useful? React with 👍 / 👎.

@Sinity

Sinity commented Aug 6, 2026

Copy link
Copy Markdown
Owner Author

run-ci

@Sinity

Sinity commented Aug 6, 2026

Copy link
Copy Markdown
Owner Author

Reopening immediately to rerun the required CI gate after refreshing the structured PR scope carrier.

@Sinity Sinity closed this Aug 6, 2026
@Sinity Sinity reopened this Aug 6, 2026
Sinity added 2 commits August 6, 2026 17:05
Problem: cursor-authority reconciliation could observe recovery outside archive ownership, fingerprint only SQLite main files, accept untyped deferrals, and persist private path and identity material.\n\nWhat changed: bind the scoped authorization to the exact plan and path, route its one-use maintenance flow through full ingest, resolve the active index generation, attest durable backups, hash effective SQLite images, require healthy sibling projections, bind recovery attempts to planning events, and redact receipts and metrics.\n\nCompatibility/migration: ordinary live ingestion remains unchanged and no direct cursor or accepted-head writes are introduced.
Problem: the scoped authorization return path used an implicit empty return and token truthiness, which strict mypy rejected.\n\nWhat changed: return the optional authorization explicitly and pass the consumed typed token directly; tighten the backup validation test's result narrowing.\n\nCompatibility/migration: runtime routing and ordinary ingestion semantics are unchanged.
@Sinity
Sinity merged commit dec1eab into master Aug 6, 2026
2 of 3 checks passed
@Sinity
Sinity deleted the feature/fix/cursor-authority-reconcile-v2 branch August 6, 2026 15:50

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 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 `@polylogue/maintenance/cursor_authority_reconcile.py`:
- Around line 590-601: Update the planning_bindings accumulation in the
event-row loop so repeated rows for the same attempt preserve a prior match:
combine the existing binding with the current digest comparison using logical OR
instead of overwriting it. Keep the existing JSON validation and keying by
str(attempt_id) unchanged.
- Line 336: Replace cursor_authority_path_digest with _identity_digest when
deriving logical_source_key in the plan bindings, so _build_plan and
apply_reconciliation remain stable across working directories and match
_private_projection’s identity digest.
- Around line 843-845: Remove the second _build_plan invocation and the
redundant _same_plan_bindings check near the ownership reconciliation step;
reuse the current_plan value created earlier and already validated before
applying the archive ownership changes.
- Around line 533-540: Update the refuse_existing branch in _write_atomic_json
to handle non-FileExistsError OSError from os.link by using a link-free
exclusive publication fallback, preserving exclusive-create semantics and
cleanup of temporary_path. Keep FileExistsError mapped to
CursorAuthorityReconciliationError, and ensure failures in the fallback do not
silently overwrite an existing output.

In `@tests/unit/maintenance/test_cursor_authority_reconcile.py`:
- Around line 355-403: Extend
test_backup_validation_rehashes_and_rejects_mismatched_tier to construct plan
with active_index from reconcile._active_index_binding(tmp_path) and set the
manifest’s index tier path to a different value. Assert _validate_backup raises
CursorAuthorityReconciliationError matching “does not bind the active index
generation”, thereby covering the active-index path-binding branch.

In `@tests/unit/sources/test_live_watcher.py`:
- Around line 352-370: Update the shadow-index verification around shadow_before
and the final assertions to account for SQLite index.db-wal and index.db-shm
sidecars, not just shadow_index.read_bytes(). Capture and compare the complete
shadow snapshot state before and after ingestion, including sidecar presence and
contents, while preserving the existing full_file_count assertion.
🪄 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: 8e6e732f-092b-456d-8a91-635ec19697cc

📥 Commits

Reviewing files that changed from the base of the PR and between 95fe989 and 6588c6c.

📒 Files selected for processing (8)
  • docs/devtools.md
  • polylogue/cli/commands/maintenance/_cursor_authority.py
  • polylogue/maintenance/cursor_authority_reconcile.py
  • polylogue/sources/live/batch.py
  • tests/unit/cli/test_archive_maintenance_cli.py
  • tests/unit/maintenance/test_cursor_authority_reconcile.py
  • tests/unit/sources/test_live_watcher.py
  • tests/unit/storage/test_raw_retention.py

if prefix_digest != blob_hash:
raise CursorAuthorityReconciliationError("source prefix does not match the accepted raw blob hash")
return {
"logical_source_key": cursor_authority_path_digest(Path(logical_source_key)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not digest logical_source_key with the path digest helper.

logical_source_key is a provider identity such as codex:session-1. It is not a filesystem path. cursor_authority_path_digest calls path.resolve(), which resolves a relative name against the current working directory. The resulting digest therefore depends on the directory the operator ran the command from.

Two consequences follow:

  1. _build_plan stores this value in the plan. apply_reconciliation rebuilds the plan and compares it with _same_plan_bindings. If the plan and the apply run from different working directories, the comparison fails and the apply refuses a valid plan with "plan bindings changed before archive ownership".
  2. _private_projection already digests the same field with _identity_digest at line 241. The module produces two different digests for one identity.

Use _identity_digest here so both surfaces agree and the digest is independent of the working directory.

🐛 Proposed fix
     return {
-        "logical_source_key": cursor_authority_path_digest(Path(logical_source_key)),
+        "logical_source_key": _identity_digest(logical_source_key),
         "cursor_byte_offset": cursor_offset,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"logical_source_key": cursor_authority_path_digest(Path(logical_source_key)),
"logical_source_key": _identity_digest(logical_source_key),
🤖 Prompt for 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.

In `@polylogue/maintenance/cursor_authority_reconcile.py` at line 336, Replace
cursor_authority_path_digest with _identity_digest when deriving
logical_source_key in the plan bindings, so _build_plan and apply_reconciliation
remain stable across working directories and match _private_projection’s
identity digest.

Comment on lines +533 to +540
if refuse_existing:
try:
os.link(temporary_path, path)
except FileExistsError as exc:
raise CursorAuthorityReconciliationError(f"output path already exists: {path}") from exc
temporary_path.unlink()
else:
os.replace(temporary_path, path)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Handle os.link failures other than FileExistsError.

os.link fails with OSError on filesystems that do not support hard links, for example some FUSE and overlay mounts. Only FileExistsError is handled here. Any other OSError propagates.

This matters most in the apply failure path. apply_reconciliation calls _write_atomic_json(receipt, failure_payload, refuse_existing=True) after the ingest already ran. If os.link raises ENOSYS or EPERM, the durable failure receipt is lost and the original reconciliation error is replaced by an unrelated OSError.

Add a link-free exclusive publication fallback for that case.

🛡️ Proposed fix
         if refuse_existing:
             try:
                 os.link(temporary_path, path)
             except FileExistsError as exc:
                 raise CursorAuthorityReconciliationError(f"output path already exists: {path}") from exc
+            except OSError:
+                # This filesystem does not support hard links. Publish through an
+                # exclusive create instead so the receipt still lands.
+                try:
+                    exclusive = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
+                except FileExistsError as exc:
+                    raise CursorAuthorityReconciliationError(f"output path already exists: {path}") from exc
+                with os.fdopen(exclusive, "w", encoding="utf-8") as handle:
+                    handle.write(encoded)
+                    handle.flush()
+                    os.fsync(handle.fileno())
             temporary_path.unlink()
         else:
             os.replace(temporary_path, path)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if refuse_existing:
try:
os.link(temporary_path, path)
except FileExistsError as exc:
raise CursorAuthorityReconciliationError(f"output path already exists: {path}") from exc
temporary_path.unlink()
else:
os.replace(temporary_path, path)
if refuse_existing:
try:
os.link(temporary_path, path)
except FileExistsError as exc:
raise CursorAuthorityReconciliationError(f"output path already exists: {path}") from exc
except OSError:
# This filesystem does not support hard links. Publish through an
# exclusive create instead so the receipt still lands.
try:
exclusive = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
except FileExistsError as exc:
raise CursorAuthorityReconciliationError(f"output path already exists: {path}") from exc
with os.fdopen(exclusive, "w", encoding="utf-8") as handle:
handle.write(encoded)
handle.flush()
os.fsync(handle.fileno())
temporary_path.unlink()
else:
os.replace(temporary_path, path)
🤖 Prompt for 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.

In `@polylogue/maintenance/cursor_authority_reconcile.py` around lines 533 - 540,
Update the refuse_existing branch in _write_atomic_json to handle
non-FileExistsError OSError from os.link by using a link-free exclusive
publication fallback, preserving exclusive-create semantics and cleanup of
temporary_path. Keep FileExistsError mapped to
CursorAuthorityReconciliationError, and ensure failures in the fallback do not
silently overwrite an existing output.

Comment on lines +590 to +601
planning_bindings: dict[str, bool] = {}
for attempt_id, payload_json in event_rows:
try:
payload = json.loads(str(payload_json))
except (TypeError, ValueError):
continue
if not isinstance(payload, dict):
continue
planning_bindings[str(attempt_id)] = (
payload.get("cursor_authority_plan_digest") == plan_digest
and payload.get("cursor_authority_path_digest") == path_digest
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Accumulate the planning binding instead of overwriting it.

The loop assigns planning_bindings[str(attempt_id)] on every event row. If one attempt has more than one planning stage event, the last row iterated wins. The query orders by observed_at_ms DESC, so an older non-matching event overwrites a newer matching one and the attempt is then skipped. Combine the results with or so any matching planning event binds the attempt.

♻️ Proposed refactor
-        planning_bindings[str(attempt_id)] = (
-            payload.get("cursor_authority_plan_digest") == plan_digest
-            and payload.get("cursor_authority_path_digest") == path_digest
-        )
+        matches = (
+            payload.get("cursor_authority_plan_digest") == plan_digest
+            and payload.get("cursor_authority_path_digest") == path_digest
+        )
+        planning_bindings[str(attempt_id)] = planning_bindings.get(str(attempt_id), False) or matches
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
planning_bindings: dict[str, bool] = {}
for attempt_id, payload_json in event_rows:
try:
payload = json.loads(str(payload_json))
except (TypeError, ValueError):
continue
if not isinstance(payload, dict):
continue
planning_bindings[str(attempt_id)] = (
payload.get("cursor_authority_plan_digest") == plan_digest
and payload.get("cursor_authority_path_digest") == path_digest
)
planning_bindings: dict[str, bool] = {}
for attempt_id, payload_json in event_rows:
try:
payload = json.loads(str(payload_json))
except (TypeError, ValueError):
continue
if not isinstance(payload, dict):
continue
matches = (
payload.get("cursor_authority_plan_digest") == plan_digest
and payload.get("cursor_authority_path_digest") == path_digest
)
planning_bindings[str(attempt_id)] = planning_bindings.get(str(attempt_id), False) or matches
🤖 Prompt for 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.

In `@polylogue/maintenance/cursor_authority_reconcile.py` around lines 590 - 601,
Update the planning_bindings accumulation in the event-row loop so repeated rows
for the same attempt preserve a prior match: combine the existing binding with
the current digest comparison using logical OR instead of overwriting it. Keep
the existing JSON validation and keying by str(attempt_id) unchanged.

Comment on lines +843 to +845
current_plan = _build_plan(root, current_path)
if not _same_plan_bindings(current_plan, plan):
raise CursorAuthorityReconciliationError("plan bindings changed after archive ownership")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Remove the duplicate _build_plan call.

Line 797 already builds current_plan, and line 800 proves it matches the stored plan bindings. Line 843 rebuilds the same plan inside the same ownership hold, and line 844 repeats the same comparison. Nothing mutates the archive between the two calls.

_build_plan is expensive. It runs _tier_snapshots, which performs a full sqlite3.backup copy plus a SHA-256 pass over four tiers, and _head_details, which hashes the source prefix up to the accepted frontier. This doubles that cost on every apply.

Reuse the value from line 797.

♻️ Proposed refactor
-        current_plan = _build_plan(root, current_path)
-        if not _same_plan_bindings(current_plan, plan):
-            raise CursorAuthorityReconciliationError("plan bindings changed after archive ownership")
         metrics: LiveBatchMetrics | None = None
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
current_plan = _build_plan(root, current_path)
if not _same_plan_bindings(current_plan, plan):
raise CursorAuthorityReconciliationError("plan bindings changed after archive ownership")
🤖 Prompt for 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.

In `@polylogue/maintenance/cursor_authority_reconcile.py` around lines 843 - 845,
Remove the second _build_plan invocation and the redundant _same_plan_bindings
check near the ownership reconciliation step; reuse the current_plan value
created earlier and already validated before applying the archive ownership
changes.

Comment on lines +355 to +403
def test_backup_validation_rehashes_and_rejects_mismatched_tier(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
backup = tmp_path / "backup"
backup.mkdir()
(backup / "blob").mkdir()
(backup / "blob-inventory.json").write_text("{}", encoding="utf-8")
tiers: dict[str, dict[str, object]] = {}
for tier in ("source", "index", "ops", "audit"):
path = backup / f"{tier}.db"
with sqlite3.connect(path) as conn:
conn.execute("CREATE TABLE marker (value TEXT)")
tiers[tier] = reconcile._sqlite_snapshot(path)
plan = {"tier_fingerprints": tiers}
manifest = {
"profile": "full_evidence",
"included_tiers": [f"{tier}.db" for tier in tiers],
"tier_source_fingerprints": {f"{tier}.db": value for tier, value in tiers.items()},
}
(backup / "manifest.json").write_text(json.dumps(manifest), encoding="utf-8")
(backup / "verification-receipt.json").write_text(
json.dumps(
{
"verdict": "success",
"verification": {
"source_blobs_resolved": True,
"index_attachment_blobs_resolved": True,
"blob_inventory_exact": True,
},
}
),
encoding="utf-8",
)
monkeypatch.setattr(reconcile, "ARCHIVE_ROOT", tmp_path)
with pytest.raises(reconcile.CursorAuthorityReconciliationError, match="attestation"):
reconcile._validate_backup(backup, plan)
monkeypatch.setattr(reconcile, "verify_verification_receipt", lambda *args, **kwargs: None)
validated = reconcile._validate_backup(backup, plan)
assert isinstance(validated["root"], dict)
assert validated["root"]["basename"] == backup.name
(backup / "audit.db").unlink()
with pytest.raises(reconcile.CursorAuthorityReconciliationError, match="tier is missing"):
reconcile._validate_backup(backup, plan)
with sqlite3.connect(backup / "audit.db") as conn:
conn.execute("CREATE TABLE marker (value TEXT)")
with sqlite3.connect(backup / "source.db") as conn:
conn.execute("INSERT INTO marker VALUES ('tampered')")
with pytest.raises(reconcile.CursorAuthorityReconciliationError, match="image does not match"):
reconcile._validate_backup(backup, plan)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extend the backup test to cover the active-index binding branch.

plan here is {"tier_fingerprints": tiers} with no active_index key. _validate_backup reads plan.get("active_index"), gets None, and therefore skips both the "active index generation changed since planning" check and the tier == "index" path-binding check at polylogue/maintenance/cursor_authority_reconcile.py lines 497-505. Those two checks bind the apply to one index generation, so they remain untested.

Add a plan that carries active_index from reconcile._active_index_binding(tmp_path) and a manifest whose index path entry does not match the active index, then assert the "does not bind the active index generation" error.

🧰 Tools
🪛 ast-grep (0.45.0)

[info] 373-373: use jsonify instead of json.dumps for JSON output
Context: json.dumps(manifest)
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)


[info] 375-384: use jsonify instead of json.dumps for JSON output
Context: json.dumps(
{
"verdict": "success",
"verification": {
"source_blobs_resolved": True,
"index_attachment_blobs_resolved": True,
"blob_inventory_exact": True,
},
}
)
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)

🤖 Prompt for 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.

In `@tests/unit/maintenance/test_cursor_authority_reconcile.py` around lines 355 -
403, Extend test_backup_validation_rehashes_and_rejects_mismatched_tier to
construct plan with active_index from reconcile._active_index_binding(tmp_path)
and set the manifest’s index tier path to a different value. Assert
_validate_backup raises CursorAuthorityReconciliationError matching “does not
bind the active index generation”, thereby covering the active-index
path-binding branch.

Comment on lines +352 to +370
shadow_index = tmp_path / "index.db"
active_index = tmp_path / "generations" / "active" / "index.db"
active_index.parent.mkdir(parents=True)
shutil.copy2(shadow_index, active_index)
(tmp_path / ".index-active-pointer").write_text(f"{active_index}\n", encoding="utf-8")
projection = reconcile._projection_for(tmp_path)
sample = projection.cursor_ahead_samples[0]
shadow_before = shadow_index.read_bytes()
with scoped_cursor_authority_authorization(
source_path_digest=cursor_authority_path_digest(source_path),
cursor_byte_offset=sample.cursor_byte_offset,
accepted_frontier=sample.accepted_frontier,
plan_digest="active-index-test",
force_full_ingest=True,
):
metrics = await processor.ingest_files([source_path], emit_event=False)

assert metrics.full_file_count == 1
assert shadow_index.read_bytes() == shadow_before

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check how the live write path resolves the index and whether WAL is enabled for archive tiers.
set -euo pipefail

echo "== _open_archive_for_live_write"
ast-grep run --lang python --pattern 'def _open_archive_for_live_write($$$):
    $$$'

echo "== journal_mode / WAL configuration for archive connections"
rg -nP --type=py -C3 'journal_mode' polylogue

echo "== initialize_active_archive_root"
rg -nP --type=py -C5 'def initialize_active_archive_root' polylogue

Repository: Sinity/polylogue

Length of output: 186


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== repo files near target =="
git ls-files | rg '(^|/)test_live_watcher\.py$|pol|arch|reconcile|sources' | head -200

echo "== target file stats and nearby content =="
if [ -f tests/unit/sources/test_live_watcher.py ]; then
  wc -l tests/unit/sources/test_live_watcher.py
  sed -n '300,390p' tests/unit/sources/test_live_watcher.py
fi

echo "== search reconciliation helpers =="
rg -n --type=py -C3 '_projection_for|_open_archive_for_live_write|initialize_active_archive_root|journal_mode|WAL|sqlite' .

Repository: Sinity/polylogue

Length of output: 8385


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== repo files near source and test =="
git ls-files 'tests/unit/sources/*.py' 'polylogue/sources/**/*.py' 'polylogue/*sources*.py' | head -200

echo "== search live watcher symbols and sqlite config =="
rg -n --type=py -C3 'live_watcher|_projection_for|scoped_cursor_authority_authorization|process_files|ingest_files|index-active-pointer|journal_mode|WAL|_sqlite|connect' tests/unit/sources test* . 2>/dev/null | head -500

Repository: Sinity/polylogue

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== target test section =="
sed -n '320,375p' tests/unit/sources/test_live_watcher.py

echo "== live archive helpers =="
sed -n '1,260p' polylogue/sources/live/archive_open.py

echo "== sqlite connection helpers =="
fd 'connection\.py|wal|sqlite' polylogue/storage | sed -n '1,80p'
rg -n --type=py -C3 'journal_mode|WAL|initialize_archive_database|ArchiveTier\.INDEX|open_connection|BEGIN IMMEDIATE|PRAGMA' polylogue/storage polylogue/sources polylogue/archive tests/unit/sources/test_live_watcher.py | sed -n '1,260p'

Repository: Sinity/polylogue

Length of output: 24897


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== cursor authority helper =="
rg -n --type=py -C5 'def _seed_live_cursor_authority_case|cursor_authority_case|live_cursor' tests/unit/sources/test_live_watcher.py

echo "== reconcile projection open logic =="
rg -n --type=py -C5 'def _projection_for|cursor_ahead_samples|active-index-pointer|Open existing|archive_root|index.db' polylogue/maintenance polylogue/archive | sed -n '1,260p'

echo "== sqlite connection WAL defaults =="
sed -n '1,260p' polylogue/storage/sqlite/connection.py
rg -n --type=py -C3 'def open_connection|journal_mode|WAL|sqlite3\.connect' polylogue/storage/sqlite/connection.py polylogue/storage/sqlite/connection_profile.py | sed -n '1,220p'

echo "== sqlite snapshot write path =="
sed -n '1,240p' polylogue/sources/sqlite_snapshot.py

Repository: Sinity/polylogue

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== seed helper body =="
sed -n '124,210p' tests/unit/sources/test_live_watcher.py

echo "== sqlite snapshot initialization and write selection =="
sed -n '1,320p' polylogue/sources/sqlite_snapshot.py
rg -n --type=py -C4 'sqlite_snapshot|SqliteSnapshot|looks_like_sqlite_bytes|raw_id|raw_sessions|INSERT INTO raw_sessions|BEGIN IMMEDIATE|read_only=False|open_existing' polylogue/sources polylogue/storage sqlite | sed -n '1,260p'

echo "== reconcile projection exact implementation =="
rg -n --type=py -C6 'def _projection_for' polylogue/maintenance/cursor_authority_reconcile.py
sed -n '1,240p' polylogue/maintenance/cursor_authority_reconcile.py
sed -n '240,420p' polylogue/maintenance/cursor_authority_reconcile.py

Repository: Sinity/polylogue

Length of output: 33215


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== sqlite snapshot raw write flow =="
rg -n --type=py -C5 'raw_id|blob_hash|stage|sqlite_snapshot|SqliteSnapshot|write_raw_payload|bind_raw_revision' polylogue/storage polylogue/sources/live polylogue/archive | sed -n '1,360p'

echo "== raw session materialization write selection =="
rg -n --type=py -C5 'write_raw_sessions|raw_sessions|INSERT INTO raw_sessions|sqlite_snapshot|SqliteSnapshot|blob_path_for_hash|read_only=False|open_existing' polylogue/storage | sed -n '1,400p'

echo "== initialize active archive root implementation =="
rg -n --type=py -C8 'def initialize_active_archive_root' polylogue/storage polylogue
sed -n '1,220p' polylogue/storage/sqlite/async_sqlite_archive.py
rg -n --type=py -C6 'initialize_active_archive_root' polylogue/storage/sqlite/async_sqlite_archive.py

Repository: Sinity/polylogue

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== sqlite snapshot imports/search by symbol =="
rg -n "write_raw_payload|write_raw_sessions|write_from_path|BlobStore|SQLiteBlobSnapshot|stage_sqlite_snapshot|sqlite_snapshot" polylogue/sources polylogue/storage | sed -n '1,320p'

echo "== archive backend write helpers =="
fd -a 'archive.*\.py$|sqlite.*\.py$' polylogue/storage | sed -n '1,30p'
rg -n --type=py -C5 'class ArchiveStore|write_raw_payload|open_existing|read_only=False|raw_sessions|sqlite_snapshot|stage_sqlite_snapshot|snapshot_sqlite_to_blob' polylogue/storage polylogue/archive | sed -n '1,400p'

echo "== live batch processor open flow =="
rg -n --type=py -C6 'scoped_cursor_authority_authorization|cursor_authority_open_authorization|_open_archive_for_live_write|apply_raw_revision_replay|bind_raw_revision|write_raw_payload|force_full_ingest|ingest_files' polylogue/sources/live polylogue/maintenance | sed -n '1,400p'

Repository: Sinity/polylogue

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== batch sqlite acquisition functions =="
rg -n --type=py -C6 'acquire_sqlite_source|sqlite_snapshot|SQLiteBlobSnapshot|raw_source_revisions|snapshot_sqlite_to_blob|snapshot_sqlite_database|sqlite_source_revision|stage_sqlite_snapshot|write_from_path' polylogue/sources/live/batch.py | sed -n '1,280p'

echo "== write raw payload implementation =="
rg -n --type=py -C8 'def write_raw_payload|-- insert raw_sessions|INSERT INTO raw_sessions|source_revision|blob_hash|raw_id|sqlite_snapshot|snapshot_sqlite_to_blob' polylogue/storage/sqlite/archive_tiers/revision_governance.py polylogue/storage/sqlite/archive_tiers/archive.py polylogue/storage/sqlite/archive_tiers/archive_plan.py | sed -n '1,420p'

echo "== active pointer handling =="
rg -n --type=py -C8 'index.active|index-active-pointer|active_index|\.index-active|index_tier' polylogue/maintenance/cursor_authority_reconcile.py polylogue/sources/live/batch.py polylogue/storage | sed -n '1,320p'

Repository: Sinity/polylogue

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== sqlite snapshot snapshot to blob implementation =="
sed -n '32,212p' polylogue/sources/sqlite_snapshot.py

echo "== sqlite snapshot source revision implementation =="
sed -n '211,227p' polylogue/sources/sqlite_snapshot.py

echo "== sqlite snapshot database for sidecar implementation =="
sed -n '200,227p' polylogue/sources/sqlite_snapshot.py

echo "== reconcile projection and assertion helpers =="
sed -n '184,215p' polylogue/storage/sqlite/durable_change_train.py
sed -n '1,120p' polylogue/maintenance/cursor_authority_reconcile.py
sed -n '120,230p' polylogue/maintenance/cursor_authority_reconcile.py
sed -n '1240,1280p' polylogue/maintenance/cursor_authority_reconcile.py

Repository: Sinity/polylogue

Length of output: 19795


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== raw frontier integrity projection =="
rg -n --type=py -C8 'def raw_frontier_integrity_projection|class RawFrontierIntegrityProjection|cursor_ahead_samples|source_revision|sqlite_source_revision|active_index_path' polylogue/storage/raw_retention.py | sed -n '1,260p'

echo "== snapshot_sqlite_to_blob call path and SQLite read-sidecar handling =="
sed -n '226,260p' polylogue/storage/sqlite/durable_change_train.py
sed -n '39,80p' polylogue/storage/sqlite/durable_change_train.py

Repository: Sinity/polylogue

Length of output: 16883


Make the shadow-index assertion include SQLite WAL sidecars.

initialize_active_archive_root configures index.db in WAL mode, so shutil.copy2(shadow_index, active_index) only copies the main file when index.db-wal/index.db-shm can exist. Later, the active generation may apply the replay to a partial shadow backup; comparing only shadow_index.read_bytes() does not prove that ingestion did not reach the shadow tier because writes can land in WAL while shadow_index stays unchanged. Compare the active snapshot, including -wal/-shm state, or assert that the active generation acquired the new session.

🤖 Prompt for 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.

In `@tests/unit/sources/test_live_watcher.py` around lines 352 - 370, Update the
shadow-index verification around shadow_before and the final assertions to
account for SQLite index.db-wal and index.db-shm sidecars, not just
shadow_index.read_bytes(). Capture and compare the complete shadow snapshot
state before and after ingestion, including sidecar presence and contents, while
preserving the existing full_file_count assertion.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6588c6c56f

ℹ️ 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".

Comment on lines +465 to +469
verify_verification_receipt(
receipt,
tier="source",
live_tier_path=location.configured_tier("source").configured_path,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Bind the attested receipt to the supplied backup

When a valid successful receipt from an older backup is copied into a newly assembled backup directory, these calls only authenticate the receipt's HMAC; _validate_backup never compares the signed manifest_sha256, artifact_inventory, blob_inventory_root_sha256, or blob evidence to the supplied files. The fresh code now calls verify_verification_receipt, but a transplanted receipt plus plan-matching tier copies and an empty blob/ and blob-inventory.json still passes, allowing the durable reconciliation without usable blob rollback evidence.

Useful? React with 👍 / 👎.

if metrics is None:
return None
payload = metrics.to_payload()
payload["failed_paths"] = [_path_identity(Path(str(path))) for path in metrics.failed_paths]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Hash failed-source basenames in receipt metrics

When a source filename itself contains a private project, user, or session identity, this redaction still writes Path.name into the durable receipt and echoes it in the CLI result. The fresh redaction hashes the absolute path but explicitly preserves its basename, so it only partially addresses the prior identity-leak issue; failed paths should be represented solely by an opaque digest.

Useful? React with 👍 / 👎.

Comment on lines +322 to +323
if blob_size != frontier or len(blob_hash) != 64:
raise CursorAuthorityReconciliationError("accepted raw does not bind a complete byte frontier")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Handle byte-proven append heads during planning

When the selected cursor's accepted head is a normal byte-proven append revision, blob_size is only the appended slice length while accepted_frontier is the absolute append_end_offset, and the blob hash likewise covers only that slice rather than the complete source prefix. This check therefore rejects every such head at blob_size != frontier (and the following prefix-hash check would also fail), so the reconciliation command cannot plan repairs for incrementally ingested sources after their first accepted append.

Useful? React with 👍 / 👎.

Comment on lines +875 to +878
else:
if after_projection.cursor_ahead_status != "healthy":
raise CursorAuthorityReconciliationError("reconciliation did not prove a healthy cursor frontier")
verdict = "reconciled"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject reconciliation that only excludes the selected cursor

When the forced full-ingest route reclassifies the selected file as a non-session artifact, _mark_excluded_cursor marks it excluded and returns neither a succeeded nor failed path. Because the raw-frontier projection omits excluded cursor rows, cursor_ahead_count then becomes zero and this branch emits a reconciled receipt even though metrics.succeeded_file_count is zero and no accepted head was advanced; the source is also left permanently excluded from future watcher retries.

Useful? React with 👍 / 👎.

@Sinity
Sinity restored the feature/fix/cursor-authority-reconcile-v2 branch August 6, 2026 22:29
Sinity added a commit that referenced this pull request Aug 7, 2026
## Summary

Record the post-review state of two reindex Beads and preserve their
remaining implementation obligations as named successors.

## Problem

Late Codex review found that the cursor reconciliation implementation
still lacks a typed no-op path for the preserved incomparable population
and current blob-inventory revalidation at apply time. It also found
that the proof-edge correction has no executable negative guard for
removal of a required edge. Closing either Bead would release downstream
readiness prematurely.

## Solution

Reopen `polylogue-cursor-authority-reconcile-implementation` and link
hardening successor `polylogue-s8gcr`. Reopen
`polylogue-reindex-proof-edge-correction` and link guard successor
`polylogue-eqq02`; the guard also blocks preflight and terminal proof
readiness. Preserve the merged implementation and phase-edge evidence as
historical evidence. No production mutation, candidate generation,
acceptance, promotion, restart, or live receipt is claimed.

## Verification

`bd dep cycles --json` reports no dependency cycles. The pre-push Beads
validation scanned 1,749 issues with zero unhandled findings. The PR
carrier validates against the exact branch head and Beads snapshot.

## Bead disposition

| Bead | Disposition | Successor | Evidence |
| --- | --- | --- | --- |
| `polylogue-cursor-authority-reconcile-implementation` | Partial |
`polylogue-s8gcr` | PR #3860, Codex findings 3734483480 and 3734483485 |
| `polylogue-reindex-proof-edge-correction` | Partial |
`polylogue-eqq02` | PR #3869, Codex finding 3734483475 |

<!-- polylogue-pr-scope:v1
{
  "assigned_beads": [
    "polylogue-cursor-authority-reconcile-implementation",
    "polylogue-reindex-proof-edge-correction"
  ],
"beads_digest":
"52dc2676c0ae91412073d39b799dcd51bd3cd082a224f225e9857826578426dc",
  "dispositions": [
    {
      "bead_id": "polylogue-cursor-authority-reconcile-implementation",
      "disposition": "partial",
      "evidence": [
        {
          "kind": "commit",
          "ref": "dec1eab35cfb41a40d3bf3014d51b0624004ea94"
        },
        {
          "kind": "review",
          "ref": "PR #3860"
        }
      ],
      "successors": [
        "polylogue-s8gcr"
      ]
    },
    {
      "bead_id": "polylogue-reindex-proof-edge-correction",
      "disposition": "partial",
      "evidence": [
        {
          "kind": "commit",
          "ref": "dc88ecee89d8e5958e38d9058df4ab5375a2ee55"
        },
        {
          "kind": "command",
          "ref": "bd dep cycles --json"
        }
      ],
      "successors": [
        "polylogue-eqq02"
      ]
    }
  ],
  "head_sha": "b6438e090dcadd36c859a727a2806708e2245c35",
"scope_digest":
"c6bc811ecd2f02c86fbba1ab3960972a3274002e297a5f423b1685f54084fc76",
  "version": 1
}
-->
@Sinity
Sinity deleted the feature/fix/cursor-authority-reconcile-v2 branch August 10, 2026 13:11
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