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
- 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.
./target/debug/relayer-graph-server --database <copy>/graph.sqlite3 --control-token <token> --port 0
- 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.
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.
Electron surfaces this as a native
Relayer could not startdialog and quits.The check is in
crates/relayer-graph-server/src/search_index/lifecycle.rs.open_reconciledrebuilds the Ladybug search store from a snapshot of thecanonical SQLite graph, then requires the rebuilt revisions to match:
The failure is not corruption. On the affected database
PRAGMA integrity_checkreturns
ok. The rebuild is driven from canonical SQLite, so a stale or missingsearch store should be recoverable by definition — yet the rebuild still does not
reproduce the revisions SQLite declares.
Observed on
A
Relayer Devprofile last written 2026-08-31, carrying a 655 KBun-checkpointed
graph.sqlite3-waland agraph.sqlite3.ladybugstore stampedthe same minute — the signature of an unclean shutdown. Reproduced against
relayer-graph-serverbuilt froma55c50dd;origin/mainat2f694047changesnothing under
crates/relayer-graph-server/src/search_index/orcrates/relayer-graph-core/, so 0.2.28 does not alter this path.Reproduction
(
graph.sqlite3,-wal,-shm, and thegraph.sqlite3.ladybugdirectory).Copying the main file alone yields a misleading
database disk image is malformed, which is an artifact of the incompletecopy, not the defect.
./target/debug/relayer-graph-server --database <copy>/graph.sqlite3 --control-token <token> --port 0What does not recover it
Each tried against an independent copy, with nothing else holding the database:
graph.sqlite3.ladybugand let the store rebuild from scratchPRAGMA wal_checkpoint(TRUNCATE)then open{"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.htmlalready requires the opposite of the current behavior: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
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.
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.
message. It must remain fail-closed and must not mutate the last recoverable
state.
and proves the repair path restores service with the graph intact. The existing
npm run check:graph-crash-reconciliationlane is the natural home.npm run checkandnpm run buildpass 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 Devprofile viaapp.setNameindesktop/main/index.mjs.Workaround for development in the meantime:
RELAYER_DESKTOP_USER_DATA_DIR=/tmp/relayer-demo npm run desktop:dev.