Skip to content

fix(sync): never destroy a healthy database on a driver fault (v1.52.3) - #179

Merged
paulkr merged 3 commits into
mainfrom
feature/int-4393-sync-opendatabase-destroys-a-healthy-database-on-a-node-abi
Aug 11, 2026
Merged

fix(sync): never destroy a healthy database on a driver fault (v1.52.3)#179
paulkr merged 3 commits into
mainfrom
feature/int-4393-sync-opendatabase-destroys-a-healthy-database-on-a-node-abi

Conversation

@siddharth-bhansali

Copy link
Copy Markdown
Collaborator

Fixes #178 · INT-4393

Problem

openDatabase() treated any new Database(path) failure as "the file is corrupt": it renamed the database to a fixed <db>.bak and wrote an empty replacement.

better-sqlite3 loads its native addon lazily, inside the Database constructor, so await import('better-sqlite3') succeeds under a mismatched Node and the ABI error only surfaces at open time — landing in a bare catch {} that discarded the reason. And because renameSync overwrites 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.

one is a #!/usr/bin/env node shim, 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 faultisDriverFault() 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 a one sync install hint. The file is left untouched.

2. Verify before discardingpassesIntegrityCheck() opens a read-only probe (so the check itself can never mutate the file) and requires PRAGMA quick_check to 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 backupbackupPathFor() produces <db>.bak.<ISO>, with :/. substituted so the name is legal on NTFS.

4. sync doctor gates properly — it detected this case correctly but still exited 0, so one sync doctor && one sync run ... passed straight through. Now sets process.exitCode = 1 on ok:false (via exitCode, not process.exit(), so the postAction hook still closes the backend and flushes telemetry).

Tests

15 new tests in src/lib/memory/sync/db-integrity.test.ts covering 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-sqlite3 is an optionalDependency and 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 --noEmit passes.
  • The new suite passes 15/15.
  • I did not run the full npm test suite: on Windows it reads and writes the developer's real ~/.one (every test sandboxes process.env.HOME, which os.homedir() ignores on win32). Filed separately — it is unrelated to this change.
  • Version bumped 1.52.1 → 1.52.2. package-lock.json version fields were hand-edited rather than regenerated, because npm install on Node 24 drops the pg optional dependency from the lockfile.

🤖 Generated with Claude Code

…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>
@linear

linear Bot commented Aug 11, 2026

Copy link
Copy Markdown

INT-4393

Both this branch and #183 (INT-4396) were cut from 1.52.1 and independently
claimed 1.52.2, so whichever merged second would have published a duplicate
version. Sequenced behind #183, which lands the CI that verifies this one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@siddharth-bhansali siddharth-bhansali changed the title fix(sync): never destroy a healthy database on a driver fault (v1.52.2) fix(sync): never destroy a healthy database on a driver fault (v1.52.3) Aug 11, 2026
@siddharth-bhansali

Copy link
Copy Markdown
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:

Order PR Version
1 #183 — test isolation + CI 1.52.2
2 #179 (this one) 1.52.3
3 #180 — re-enrich 1.53.0
4 #181 — profile drift 1.54.0

#183 first is deliberate: it adds the CI that actually verifies the other three.

@paulkr
paulkr merged commit 554aca2 into main Aug 11, 2026
7 checks passed
@paulkr
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>
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.

sync: a Node ABI mismatch makes openDatabase() destroy a healthy database (renames it to .bak, writes an empty one, overwrites the prior .bak)

2 participants