fix(sync): never destroy a healthy database on a driver fault (v1.52.3) - #179
Merged
paulkr merged 3 commits intoAug 11, 2026
Conversation
…2) (#178) openDatabase() treated any `new Database(path)` failure as corruption: it renamed the file to a fixed `<db>.bak` and wrote an empty replacement. better-sqlite3 loads its addon lazily inside the constructor, so a Node ABI mismatch surfaces there and was indistinguishable from a bad file. Because the rename overwrote any prior `.bak`, the first bad run was recoverable and the second was not. This destroyed a 747MB live Gmail sync database. - Classify the caught error: rethrow ERR_DLOPEN_FAILED / NODE_MODULE_VERSION / missing-bindings with a "run one sync install" hint, and leave the file alone. `one` is a #!/usr/bin/env node shim, so a bare PATH under cron, launchd, or an agent runner can pick a different interpreter than the shell. - Verify before discarding: probe read-only and require PRAGMA quick_check to actually report damage. A healthy database that merely failed to open (locked, read-only mount) is now reported, not rotated. - Timestamped `<db>.bak.<ISO>` backups, so a repeat failure cannot clobber the previous copy. - `one sync doctor` exits non-zero when it reports ok:false, so `one sync doctor && one sync run <platform>` is finally a real gate. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 11, 2026
Merged
Merged
Collaborator
Author
|
Version bumped 1.52.2 → 1.52.3. This branch and #183 were both cut from 1.52.1 and independently claimed 1.52.2, so whichever merged second would have published a duplicate version. Suggested merge order across today's four PRs, so no further rebasing is needed:
#183 first is deliberate: it adds the CI that actually verifies the other three. |
…a-healthy-database-on-a-node-abi
paulkr
approved these changes
Aug 11, 2026
paulkr
deleted the
feature/int-4393-sync-opendatabase-destroys-a-healthy-database-on-a-node-abi
branch
August 11, 2026 14:13
paulkr
pushed a commit
that referenced
this pull request
Aug 13, 2026
…3) (#179) Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #178 · INT-4393
Problem
openDatabase()treated anynew Database(path)failure as "the file is corrupt": it renamed the database to a fixed<db>.bakand wrote an empty replacement.better-sqlite3 loads its native addon lazily, inside the
Databaseconstructor, soawait import('better-sqlite3')succeeds under a mismatched Node and the ABI error only surfaces at open time — landing in a barecatch {}that discarded the reason. And becauserenameSyncoverwrites an existing.bak, the first bad run was recoverable and the second was not.This destroyed a 747MB live Gmail sync database on 2026-08-10 (CLI 1.52.1). Recovered only because it was the first occurrence.
oneis a#!/usr/bin/env nodeshim, so PATH picks the interpreter — which is what makes this easy to hit from cron, launchd, and agent runs with a minimal PATH, even when the CLI path itself is pinned.Changes
1. Never rotate on a driver fault —
isDriverFault()classifies the caught error (ERR_DLOPEN_FAILED,NODE_MODULE_VERSION, "compiled against a different Node.js version", missing bindings, and the platform loader messages). Those rethrow with the current Node version, the module version, why PATH is the likely cause, and aone sync installhint. The file is left untouched.2. Verify before discarding —
passesIntegrityCheck()opens a read-only probe (so the check itself can never mutate the file) and requiresPRAGMA quick_checkto actually report damage. A healthy database that merely failed to open — locked, read-only mount — is now reported rather than rotated.3. Never overwrite a backup —
backupPathFor()produces<db>.bak.<ISO>, with:/.substituted so the name is legal on NTFS.4.
sync doctorgates properly — it detected this case correctly but still exited 0, soone sync doctor && one sync run ...passed straight through. Now setsprocess.exitCode = 1onok:false(viaexitCode, notprocess.exit(), so thepostActionhook still closes the backend and flushes telemetry).Tests
15 new tests in
src/lib/memory/sync/db-integrity.test.tscovering all three data-loss guarantees: driver-fault classification (including the exact message from the report) vs. genuine corruption, the read-only probe and its verdict handling, probe-handle cleanup, and non-clobbering/Windows-legal backup names.They use an injected constructor rather than the real driver, so they run everywhere —
better-sqlite3is anoptionalDependencyand is frequently absent (it would not install on this machine under Node 24, which is the same precondition that triggers the bug).Verification notes
npx tsc --noEmitpasses.npm testsuite: on Windows it reads and writes the developer's real~/.one(every test sandboxesprocess.env.HOME, whichos.homedir()ignores on win32). Filed separately — it is unrelated to this change.package-lock.jsonversion fields were hand-edited rather than regenerated, becausenpm installon Node 24 drops thepgoptional dependency from the lockfile.🤖 Generated with Claude Code