Skip to content

A graph whose search index cannot be reconciled is permanently unopenable #429

Description

@vishaltandale00

Problem

A desktop profile whose graph database survives an unclean shutdown can become
permanently unopenable. The graph server fails closed at startup, every launch,
with no way back to the data.

Error: open graph database

Caused by:
    0: reconcile the search index
    1: graph engine failed: rebuilt search store failed revision validation

Electron surfaces this as a native Relayer could not start dialog and quits.

The check is in crates/relayer-graph-server/src/search_index/lifecycle.rs.
open_reconciled rebuilds the Ladybug search store from a snapshot of the
canonical SQLite graph, then requires the rebuilt revisions to match:

if !revisions_match(&index, snapshot).await? {
    return Err(GraphError::Internal(
        "rebuilt search store failed revision validation".into(),
    ));
}

The failure is not corruption. On the affected database PRAGMA integrity_check
returns ok. The rebuild is driven from canonical SQLite, so a stale or missing
search store should be recoverable by definition — yet the rebuild still does not
reproduce the revisions SQLite declares.

Observed on

A Relayer Dev profile last written 2026-08-31, carrying a 655 KB
un-checkpointed graph.sqlite3-wal and a graph.sqlite3.ladybug store stamped
the same minute — the signature of an unclean shutdown. Reproduced against
relayer-graph-server built from a55c50dd; origin/main at 2f694047 changes
nothing under crates/relayer-graph-server/src/search_index/ or
crates/relayer-graph-core/, so 0.2.28 does not alter this path.

Reproduction

  1. Copy an affected profile's graph database with its sidecars
    (graph.sqlite3, -wal, -shm, and the graph.sqlite3.ladybug directory).
    Copying the main file alone yields a misleading
    database disk image is malformed, which is an artifact of the incomplete
    copy, not the defect.
  2. ./target/debug/relayer-graph-server --database <copy>/graph.sqlite3 --control-token <token> --port 0
  3. The server exits with the error above.

What does not recover it

Each tried against an independent copy, with nothing else holding the database:

Attempt Result
Delete graph.sqlite3.ladybug and let the store rebuild from scratch still fails revision validation
PRAGMA wal_checkpoint(TRUNCATE) then open still fails revision validation
Fresh empty database (control) starts normally, {"ready":true,...}

The control confirms the binary is healthy. Discarding the graph is the only
known way forward, which means losing the user's threads and graphs.

Why this matters

docs/prd/index.html already requires the opposite of the current behavior:

If migration or startup fails, Relayer must fail closed. It must not change the
last recoverable database state. It must offer a recovery procedure.

Failing closed is correct and is happening. Not changing the last recoverable
state appears to hold — the Ladybug store is not overwritten on the failing path.
The missing half is the recovery procedure: today the user gets a modal dialog,
the app quits, and nothing in the product offers a way back.

Desired outcome

An existing graph whose search index cannot be reconciled is recoverable without
discarding the user's data, and the failure explains itself.

Acceptance criteria

  • Establish why a rebuild driven from canonical SQLite produces revisions that
    disagree with SQLite. Name the divergence rather than widening the tolerance;
    a reconcile that cannot reproduce canonical revisions is either reading a state
    SQLite does not actually declare, or the snapshot is taken at a boundary that
    does not match what it validates against.
  • Fix that divergence so an unclean-shutdown profile reconciles, or, where the
    state is genuinely unreconcilable, define and implement the recovery procedure
    the PRD requires: rebuild search from canonical SQLite as an explicit repair,
    and preserve the graph.
  • The startup failure reports something a user can act on, not an internal
    message. It must remain fail-closed and must not mutate the last recoverable
    state.
  • Deterministic coverage that opens a fixture graph in the unreconcilable state
    and proves the repair path restores service with the graph intact. The existing
    npm run check:graph-crash-reconciliation lane is the natural home.
  • npm run check and npm run build pass on the final tree.

Notes

Found while running the desktop app in development during the customer-feedback
pass (#411-#418, PR #423). Unrelated to that PR, which contains no Rust. The
installed 0.2.25 app is unaffected and its profile was never touched; the dev
build uses a separate Relayer Dev profile via app.setName in
desktop/main/index.mjs.

Workaround for development in the meantime:
RELAYER_DESKTOP_USER_DATA_DIR=/tmp/relayer-demo npm run desktop:dev.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingeffort:8Provisional 8 relative effort points for remaining scope, including verification; not days.needs-triageNeeds initial triage and routingurgency:P1Next: correctness, authority/privacy, recovery, release proof, or enabling prerequisite.

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions