Skip to content

fix(sqlite): publish complete stores and recover interrupted initialization - #4805

Open
ScriptedAlchemy wants to merge 24 commits into
masterfrom
fix/4513-reserve-sqlite-sidecars-20261108
Open

ScriptedAlchemy wants to merge 24 commits into
masterfrom
fix/4513-reserve-sqlite-sidecars-20261108

Conversation

@ScriptedAlchemy

@ScriptedAlchemy ScriptedAlchemy commented Nov 8, 2026 •

Copy link
Copy Markdown
Owner

Refs #4513. Keep the issue open for the live-daemon endurance work and verification on master.

SQLite initialization and recovery

The registered store publisher installs the final schema in an owner-private sibling directory on the same filesystem. It closes the staged workers, folds WAL into the main file, stamps SQLite's application ID and syncs the completed database. An exclusive initialization lease serializes competing creators.

Before publishing the main file, initialization claims the canonical WAL, SHM and rollback-journal names using exclusive hard links to empty private reservation files. A no-replace rename publishes the completed main at its ordinary SQLite path. The publisher verifies the main inode and all three empty reservations before atomically recording the handoff to SQLite. The final attachment checks that it opened the published generation.

A retry after process death can discard unpublished schema and resume its own reservations. An existing open can finish an interrupted handoff from the retained inode witnesses. Occupied, missing or mismatched members cause refusal while ownership is unresolved. Successful cleanup leaves only the private directory and its coordination lock. Ordinary existing stores continue at their canonical paths. For unmarked legacy databases, a main containing only a header or runtime-ledger tables and an occupied sidecar receives a typed refusal before attachment workers open the family. A legacy database whose application schema exists only in an uncheckpointed WAL can also receive that refusal.

Verification

  • Native macOS Bazel passed all 78 registry tests, including process exit during installation, reservation, main publication and handoff cleanup, competing initializers, cancellation followed by successful registered-store initialization, and a separate read-only SQLite connection checking the resulting schema and integrity.
  • The focused native macOS rusqlite test passed for an unmarked legacy main with an ambiguous sidecar. Repository CI tracks Linux validation. The Windows workflow targets both affected unit-test binaries. Windows workflow

Remaining scope and reviewer holds

Prospective initialization requires hard links and same-filesystem no-replace rename. The private staging directory isolates unpublished schema. Once ownership transfers, SQLite's VFS controls the canonical sidecars. A noncooperating process with permission to replace those directory entries can still interfere. Endurance of a production daemon with an independent sqlite3 reader across restart, platform CI and verification on master remain part of #4513.

The related verified-reader POSIX lock work remains in #4803. The owner's separate hold on the Kev/decision-model draft remains in force. Do not merge #4776.

@changeset-bot

changeset-bot Bot commented Nov 8, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 26311e5

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Copy link
Copy Markdown
Owner Author

Review/validation hold: This PR is marked ready solely so the existing PR CI can execute on its exact head. Please do not merge until nonzero hosted Bazel tests, native macOS/Windows SQLite sidecar and locking behavior, and the preserved-failure/retry tradeoff are reviewed. The default PR workflow skips native macOS; a skipped check is not platform acceptance. The cross-process replacement and initial sidecar refusal tests are intended to be exercised by //crates/tracedecay-rusqlite-runtime:unit_test on the current commit. The broader #4513 acceptance remains open.

Copy link
Copy Markdown
Owner Author

Refs #4513 — follow-up fix published on this same issue-specific PR at 1738c8e, fast-forwarded from exact prior head 92d53ff3f0de without force, three owned files only.

New data-loss path on retry: create_new creates the empty main DB before exclusively reserving -wal/-shm/-journal. If any sidecar is occupied, it refuses without deleting foreign files, but leaves the empty sessions.db. A later existing-mode attachment can open that empty file, start SQLite in WAL mode, and erase the exact foreign sidecar that caused the earlier refusal. An independent disposable Linux/native SQLite 3.46.1 schedule reproduced sentinel loss for all 3 suffixes on retry; these are standalone reproductions, not Bazel/Rust passes.

Patch: both attach and attach_read_only now call OpenedDatabaseFile::pin_initialized before starting SQLite workers. It verifies the completed SQLite 16-byte file magic through the already pinned descriptor, avoiding an extra Unix close that would drop unrelated POSIX locks. Missing/invalid headers fail closed, rather than allowing SQLite to bootstrap an incomplete basename. Added falsifiable Rust regression for each occupied suffix's failed-create → existing-retry sequence, plus invalid-header refusal and valid initialized DB acceptance.

Independent native SQLite external read-only subprocess / concurrent writer probes also passed 32 nonzero Linux assertions, 8 each in rollback and WAL modes (reader result and quick_check). Neither those probes nor this review count as repository Bazel, hosted CI, native macOS or Windows qualification. New-head hosted CI must run nonzero //crates/tracedecay-rusqlite-runtime:unit_test and all relevant integration gates; macOS still needs a native workflow-dispatched run.

Not complete acceptance: an initialization that got far enough to write a valid database header can still leave an incomplete family, and post-admission POSIX pathname replacement lacks a stronger private ownership/VFS fence. Daemon-level external sqlite3 endurance, restart recovery and platform verification remain open. Keep #4513 OPEN and this PR on review/verification hold. Ordinary merging belongs to the existing PR-maintenance lane; do not merge held draft #4776 (Kev/decision model).

Refs #4513

Remove the stale discard_created call left after unsafe pathname cleanup
was deleted. Abort now preserves the database and all sidecars, releases
shared descriptors only after normal worker teardown, and transfers a
lost-file initialization pin to the existing deferred worker cleanup.
Reject initialization publication once drain or close has begun.

Add real runtime regressions for abort/read-back/stale handles, separate-
process replacement of the database and WAL/SHM/journal after worker
close, idempotent abort, and a lost writer held across initialization abort.
These Rust tests still require nonzero Bazel and native macOS execution;
no Cargo or direct Rust invocation was used.

@ScriptedAlchemy ScriptedAlchemy left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Refs #4513 — code review of exact head 1738c8e1de983e062053fb2544c62f3a9c222300.

Three actionable safety findings are attached inline: (1) invalid-header rejection closes an independent raw database descriptor and can release another local SQLite connection's POSIX locks; (2) the SQLite magic does not establish completed initialization or authorize reusing a preserved sidecar family; (3) sidecar reservations do not fence a replacement between reservation and SQLite's first access.

I reproduced the mechanisms in disposable Linux fixtures with native SQLite 3.46.1: the rejected-header/raw-close schedule changed an independent subprocess writer from SQLITE_BUSY to admitted before the original transaction ended; a separate-process post-reservation replacement lost sentinel bytes for each of WAL/SHM/journal; and a header-bearing but otherwise uninitialized WAL database accepted a retry that overwrote/deleted each sentinel. These are standalone mechanism reproductions, NOT execution of this repository's Rust/Bazel tests, and not macOS/Windows qualification.

Keep the removal of unsafe pathname-based cleanup. The next change needs production-path regressions through attachment startup/failure/retry, rather than only calling repository_start_failure directly. Require nonzero Bazel results on the resulting exact head, plus native macOS locking/sidecar coverage and relevant Windows coverage. This review is not an approval or a claim of master completion. Keep #4513 open; ordinary merging remains with the PR-maintenance owner. Do not merge or modify held draft #4776 or release #4763.

}
})??;
if !initialized {
return Err(OpenedDatabaseFileError::InvalidHeader);

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

[P1] The invalid-header refusal itself can discard another connection's SQLite locks.

Self::pin(path) above opens a new raw File; returning here drops it. The ?? read-error exits do the same. Sharing that newly opened descriptor with this attachment's future workers does not protect a different, already-live SQLite connection in the same process.

Concrete standalone Linux/SQLite 3.46.1 reproduction: open an empty database, hold BEGIN IMMEDIATE (the main file is still zero bytes), and confirm a separate-process writer gets SQLITE_BUSY. Read the header through a second raw descriptor and close it as this refusal does. A fresh subprocess can now enter BEGIN IMMEDIATE before the original transaction rolls back. This reintroduces the lock-loss mechanism #4513 is intended to remove.

Make rejected admission use a lock-safe ownership/lifetime path as well; reading through a newly acquired pin is not sufficient. Add a Rust regression that actually calls pin_initialized while an independent connection holds that transaction and checks cross-process exclusion both before and after refusal. Cover error teardown without leaking descriptors indefinitely, and run it natively on macOS as well as Linux.

let mut header = [0_u8; 16];
match io::Read::read_exact(&mut reader, &mut header) {
Ok(()) => Ok(header == *b"SQLite format 3\0"),
Err(error) if error.kind() == io::ErrorKind::UnexpectedEof => Ok(false),

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

[P1] SQLite magic is not proof that the preserved initialization completed.

This admits a database after PRAGMA journal_mode=WAL has written its header but before application initialization completes. On a failed start/retry, there is still no proof that the current WAL/SHM/journal entries belong to this attachment. The new retry regression only exercises an empty main file, so it cannot catch this path.

Standalone Linux/SQLite 3.46.1 reproduction: create a database using only PRAGMA journal_mode=WAL, close it before creating application schema, place a foreign sentinel at one sidecar name, then run the existing-mode WAL/open/write sequence. The 16-byte check passes and the sentinel is overwritten/deleted for each of -wal, -shm, and -journal.

Gate retry on authoritative initialization/namespace ownership or return a typed incomplete-initialization refusal before workers touch ambiguous sidecars. Merely checking more SQLite header bytes would not establish that ownership. Add a real factory failure after header creation followed by both writable and read-only retry, asserting foreign bytes survive and a normally initialized store still reopens.

.map_err(|_| OpenedDatabaseFileError::Reserve)?;
let journal = reserve_new_sidecar(&sidecar_path(path, "-journal"))
.map_err(|_| OpenedDatabaseFileError::Reserve)?;
opened.created_sidecars = Some(Arc::new(Mutex::new(Some([wal, shm, journal]))));

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

[P1] Retained reservation descriptors do not protect the sidecar directory entries used by SQLite.

After these O_EXCL creations succeed, another process can atomically replace just sessions.db-wal (or -shm/-journal) while leaving the main database unchanged. The retained handle still pins the old sidecar inode, but the worker opens the replacement by pathname; main-file identity/HAS_MOVED checks do not detect that sidecar-only substitution.

A disposable Linux/SQLite 3.46.1 probe reproduced foreign sentinel loss for all three suffixes after a separate subprocess performed exactly that replacement and before the first WAL initialization. Holding the reservation fds throughout did not prevent it.

The new cross-process test swaps files and calls repository_start_failure directly, so it proves the removal of explicit unlink cleanup, not that SQLite startup preserves replacements. Add the sidecar-only replacement schedule through actual attachment startup, including failure/retry. Establish an ownership boundary that remains valid during SQLite's sidecar operations; another pre-open stat/recheck or an advisory lock ignored by the replacing process is not a race-safe fix.

Apply the exact rustfmt changes reported by hosted Bazel Repository
gates without touching SQLite behavior or other PR files.

Refs #4513

Copy link
Copy Markdown
Owner Author

Refs #4513 — repaired the exact four formatting differences reported by the failed Repository gates job at the peer's head a3c0a165. Commit 2e9b650 is a nonforce expected-old-head fast-forward and changes only formatting in the three pre-existing SQLite files, preserving the peer's new abort-initialization tests and production changes. New exact-head CI is queued; no Bazel Rust pass is claimed yet. This does not resolve the three existing P1 review findings: rejected pin/header read lock lifetime, valid-header incomplete-initialization retry, and sidecar-only replacement after O_EXCL reservation. Native macOS/Windows tests and daemon/external sqlite3 endurance also remain outstanding. Keep #4513 open and this PR on review hold. Do not merge protected draft #4776 or touch release #4763.

Avoid opening a raw database descriptor when an existing attachment
cannot contain the SQLite header. Closing that descriptor could unlock
another live SQLite writer; cover the refusal with a cross-process test.

This does not fix header-bearing incomplete stores or sidecar swaps.
Refs #4513
SQLite's minimum initialized database occupies one 512-byte page.
Refuse header-bearing shorter files without opening a raw Unix handle;
otherwise they bypass magic-only admission and can release live locks.

Refs #4513

Copy link
Copy Markdown
Owner Author

Refs #4513 — published narrow POSIX-lock preservation follow-ups on this PR: e6ea022 and 83d0221, via exact-old-head, non-force fast-forwards, preserving all prior commits.

pin_initialized previously opened a fresh raw Unix descriptor even when an existing database was too short to contain an initialized SQLite page; its error return closed that descriptor and could release another live SQLite connection's POSIX writer locks. Unix now refuses files shorter than SQLite's minimum 512-byte page by metadata alone, without opening/closing a raw descriptor. Added a real Rust subprocess lock regression for the zero-byte BEGIN IMMEDIATE case (blocked before and after refused admission, allowed after rollback), plus short-file cases bearing the correct 16-byte magic (lengths 16, 100, 511), which the original magic-only check incorrectly admitted. SQLite file-format minimum: https://www.sqlite.org/fileformat2.html.

Independent Linux system SQLite 3.46.1 probe confirmed the pre-fix raw-close lock loss and 5 nonzero metadata-only mitigation assertions; these are not repository Rust/Bazel passes. Hosted exact-head Bazel //crates/tracedecay-rusqlite-runtime:unit_test, broader applicable CI, and native macOS/Windows qualification remain unverified.

Scope is deliberately partial: this does not make invalid-header refusal safe for files >=512 bytes; a raw pin still closes on those errors. It also does not prove a header-bearing store completed initialization or protect the sidecar names against noncooperating POSIX replacement after reservation. Keep the earlier three P1 review threads open and PR on review hold. Require an ownership-safe lifecycle and daemon+external sqlite3 endurance/restart tests before #4513 can be closed on master. No merge/release/draft #4776 changes.

Existing-mode admission must not open sidecars for databases whose
file size cannot be a whole number of SQLite pages. Refuse those paths
using metadata before opening a descriptor, preserving foreign journal
entries and avoiding POSIX lock-loss on the refusal path.

Add production attachment regressions for truncated header-bearing
files and WAL, SHM, and journal sentinel preservation.

Refs #4513

Copy link
Copy Markdown
Owner Author

Refs #4513 — published 761cc49 to this issue-only PR, fast-forwarding exact old head 83d0221 without force and preserving concurrent changes.

Additional defensive admission fix: an existing SQLite DB must consist of complete pages; every legal SQLite page size is a multiple of 512 bytes. The prior >=512-byte check admitted header-bearing files of size 513 or 1025. Those could still reach the SQLite worker and allow it to delete or change unrelated journal/SHM sidecars despite the database being incomplete. Admission now rejects non-512-aligned lengths using filesystem metadata before opening a raw descriptor (avoiding an additional POSIX close on this refusal path), on all supported platforms.

Rust regressions added: direct existing-header tests for 513/1023/1025 bytes, plus a test through real RepositoryPhysicalAttachmentFactory::attach asserting InvalidHeader and byte-exact preservation of sessions.db and foreign -wal/-shm/-journal sentinels for 513/1025-byte databases. No CI bypass or test weakening.

Independent Linux SQLite 3.46.1 mechanism probe (NOT Bazel/Rust tests): of 6 unprotected invalid-page-length cases (2 lengths x 3 suffixes), 2 journal sentinels were deleted, and 6 metadata-only refusal cases preserved all original sentinels. The probe did not demonstrate every suffix is destroyed in this specific invalid-header schedule; it did confirm a real journal-loss path.

Still outstanding: this is only a necessary structural guard, not proof of completed initialization or sidecar ownership. Page-aligned but invalid or partially initialized DBs, a header-read failure after a fresh raw pin, and post-reservation pathname replacement remain unresolved. Native macOS/Windows, full external sqlite3 daemon-reader endurance, and nonzero exact-head Bazel tests are not yet proven. CI has not yet reported a run for this commit. Keep this PR under review hold and issue #4513 OPEN; no master completion/merge is claimed. Mixed draft #4776 and release #4763 remain untouched.

Match the pinned Bazel rustfmt_check_2024 output for the refused
empty-attachment writer-lock regression without changing its behavior.

Refs #4513

Copy link
Copy Markdown
Owner Author

Refs #4513 — fixed the exact-head hosted Bazel Repository gates rustfmt failure on #4805 without changing behavior. Commit 9a6e93d changes one line-wrap in crates/tracedecay-rusqlite-runtime/src/connection/path_identity_tests.rs, matching the pinned rustfmt diff at line 177 from failed run 42452325096 (job 128643991644). This was committed as a child of verified old head 761cc49 and pushed with force=false and expected_sha lease; all peer work remains intact. New-head CI is queued, not a Bazel passing result.

Independent disposable Linux SQLite 3.46.1 checks this run reproduced the successful-init overwrite of pre-existing -wal/-shm/-journal bytes (3/3), demonstrated 3/3 exclusive-create refusals preserving sentinels, and verified separate-process WAL read of committed row 1 while an uncommitted row 2 existed, plus external writer exclusion/release in both WAL and DELETE modes. These probes do not run this Rust code, prove the new-head Bazel tests, or qualify native macOS/Windows.

The existing remaining holds are unchanged: full-page malformed/retry lock safety, complete initialization/sidecar ownership, post-reservation substitution, ordinary external sqlite3 + isolated-daemon endurance/restart, native macOS/Windows and proof on master. Keep #4513 open and PR-merging with the existing maintenance lane. Never merge held draft #4776 or touch release #4763.

Copy link
Copy Markdown
Owner Author

Refs #4513 — published 45d0ed2 as an issue-focused regression-only update to this existing PR. The test now exercises RepositoryPhysicalAttachmentFactory::initialize, attach, and attach_read_only for each pre-existing -wal, -shm, -journal sentinel, verifying Reserve refusal, invalid-header retry refusal, unchanged zero-byte main file and byte-exact preservation of foreign sidecars. It covers the real factory entry points rather than only calling the pin helper. The commit is a one-file, 61-line addition and a verified nonforce expected-old-head fast-forward from 9a6e93d, preserving peer commits. Exact-head CI is pending; no repository Bazel or native macOS/Windows run is claimed. This does NOT fix post-reservation replacement, page-aligned incomplete initialization or full external-sqlite3 daemon endurance. Keep #4513 OPEN; ordinary merges remain with the maintenance owner. Draft #4776 and release #4763 remain untouched.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

Copy link
Copy Markdown
Owner Author

Refs #4513 — issue-specific follow-up published on #4805 (non-force, expected-old-head fast-forwards retaining prior and peer commits): b154e6b replaces the existing-store raw-file header read with a read-only immutable SQLite-VFS admission probe before acquiring the independent raw pin, and 02783ec adds a falsifiable Unix cross-process POSIX RESERVED-byte regression. 935d3d4 only normalizes test imports. Four SQLite-only files changed across these commits.

Specific fix: previously a page-aligned malformed database with a bad SQLite header could be opened via File::open for inspection, then refused and closed. On Unix, that extra close can release another live connection's process-owned record locks. The immutable SQLite probe is read-only, ignores WAL/SHM/journal and uses the SQLite VFS for its open/close lifecycle. The factory and direct tests now reject page-aligned false-header stores (512/4096 bytes) without altering foreign sidecars. A separate test preserves acceptance of a legitimate live WAL store whose schema changes remain uncheckpointed. The new subprocess test inspects SQLite's actual RESERVED advisory-lock byte before and after corrupt-header rejection, with a preheld writer and without vacuous zero-test acceptance.

Independent disposable Linux SQLite 3.46.1 mechanism checks passed: 6 malformed aligned-header/sidecar preservations and 10 external writer-lock/immutable-probe assertions, plus a separate direct POSIX-fcntl schedule demonstrating exclusion survives SQLite immutable refusal whereas an independent raw open/close releases it. These checks are NOT the Rust regressions, repository Bazel, or native macOS results.

Validation hold: exact-head CI run 42465671210 was queued when checked. Require nonzero bazel test //crates/tracedecay-rusqlite-runtime:unit_test --test_output=errors on this SHA, plus affected integration/format gates and a native macOS workflow run on the exact head. The full underlying issue is not solved: the immutable main-file probe does not prove completion of initialization or own the sidecars; post-reservation POSIX sidecar replacement, safe recovery from partial initializations, ordinary daemon + external sqlite3 endurance/restart, and native platform evidence remain open. Keep #4513 OPEN; do not merge from this comment. Ordinary PR merging stays with the maintenance lane. Held draft #4776 and release #4763 remain untouched.

Copy link
Copy Markdown
Owner Author

Refs #4513 — published issue-specific formatting repair fcd316b6ca08 directly on this PR using an exact-head, nonforce fast-forward from 935d3d4d8733; the remote is now at fcd316b6ca08, one commit ahead / zero behind that prior head. Only connection/mod.rs and connection/path_identity_tests.rs changed, with the three formatting hunks requested by Bazel's pinned rustfmt.

The preceding exact-head CI run 42465671210 failed Repository gates at //crates/tracedecay-rusqlite-runtime:rustfmt_check_2024 on precisely those hunks (immutable-inspection let wrapping and two OpenOptions chain layouts). Other CI jobs were still pending/queued when checked, not passed. This commit is behavior-preserving and does not fix the known post-reservation replacement race or incomplete-initialization recovery. Do not infer Rust unit-test or macOS acceptance until actual nonzero results on this new SHA.

Keep #4513 open; ordinary PR merging is owned by the existing maintenance lane. Held draft #4776 and release #4763 are untouched.

Copy link
Copy Markdown
Owner Author

Refs #4513 — published focused cross-process lock-lifetime regression 58d49b8 directly on this branch, from exact old head fcd316b6ca08, using a non-force expected-head fast-forward. The diff adds one non-vacuous Unix Rust test (31 lines) in connection/path_identity_tests.rs; no production, configuration, or peer code changed.

The new test covers the previously untested successful pin_initialized path on a valid existing DB while another SQLite connection in the same process holds a rollback-mode BEGIN IMMEDIATE transaction. It verifies the external process still cannot acquire SQLite's POSIX RESERVED-byte lock after successful admission, then checks lock release, committed data and integrity after orderly writer/pin teardown. The existing malformed-header lock test covers the refusal path, not success.

A disposable Linux system-SQLite 3.46.1 cross-process analog independently returned database is locked both before and after a successful immutable reader open/close; a raw open/close control released the writer's lock. This is not execution of this repository's Bazel/Rust test. Exact-head CI is queued; confirm nonzero //crates/tracedecay-rusqlite-runtime:unit_test and pinned rustfmt results. Native macOS remains unqualified.

This is a regression guard, not full repair of sidecar post-reservation replacement or partial initialization recovery. Keep #4513 open and preserve the separate #4803 and held draft #4776 boundaries. Ordinary merge remains with the existing PR-maintenance lane.

Copy link
Copy Markdown
Owner Author

Refs #4513 — published 3e5fbc7 on the existing issue-specific branch using a non-force expected-old-head fast-forward from 58d49b8. It moves Windows OpenOptionsExt from inside reserve_new_sidecar to the module import scope, addressing the existing inline review comment without modifying sidecar open/share behavior. Confirmed the exact remote head and one-file, three-line compare diff (one added cfg import pair, one removed inline import). This is a source-hygiene repair; no local Bazel or native Windows/macOS pass is claimed. Hosted checks must qualify the resulting exact head. #4513 remains open: post-reservation replacement, incomplete-store recovery, live daemon + external sqlite3 endurance and native macOS/Windows validation are still outstanding; neither #4803 nor #4805 is on master. No change to protected draft #4776, release #4763, or the PR maintenance workflow.

Existing fixture files created by a bare SQLite open remain zero bytes;
they are not completed SQLite stores and must be refused by admission.
Use an empty-schema SQLite header instead, preserving fixture intent
and the incomplete-initialization safety guard.

Refs #4513

Copy link
Copy Markdown
Owner Author

Refs #4513 — published issue-scoped fixture compatibility repair 76ed787711eb on this existing PR, expected-old-head non-force fast-forward from 3e5fbc7 (confirmed one commit ahead/zero behind; only three test source files changed).

New hosted Bazel findings (older head 3e5fbc7): run 42483966997 ran 183 Bazel test targets; 8 failed. //crates/tracedecay-rusqlite-runtime:rusqlite_suite executed 156 Rust tests (149 passed, 5 failed, 2 ignored). All five failures are Identity(InvalidHeader) from treating a bare rusqlite::Connection::open(&path) as an initialized existing database; opening/closing without a write creates a zero-byte main file. The <512-byte admission refusal is intentional and must not be weakened. //crates/tracedecay:storage_runtime_rusqlite_suite fails the same direct fixture. //crates/tracedecay-global-db:unit_test's cancellation fixture also explicitly File::created a zero-byte Existing store.

This commit materializes a real, page-aligned SQLite header with PRAGMA user_version = 0 before those tests attach, without adding schema objects or changing assertions. An independent Linux SQLite 3.46.1 probe verified a valid header, empty schema, and quick_check=ok (3/3 nonzero assertions); not a Rust/Bazel test. Exact-new-head CI 42496503283 is queued, not passed. The old CI also reports a separate //crates/tracedecay:transport_acceptance_suite registered-LCM runtime InvalidHeader failure that needs root-cause tracing; automation-runner and other failures have not been attributed to #4513 without evidence. No source/prod/CI weakening.

Open acceptance holds: live daemon + external sqlite3 endurance/restart, failed/partial initialization lifecycle, post-reservation POSIX namespace replacement, native macOS/Windows, and both #4803/#4805 proven passing and on master. Keep issue open, do not merge from this comment; ordinary merge ownership stays with the existing PR-maintenance workflow. Protected draft #4776 and release #4763 untouched.

ScriptedAlchemy commented Nov 8, 2026 •

Copy link
Copy Markdown
Owner Author

Refs #4513 — new reproducible existing-store admission data-loss case, distinct from the new-basename O_EXCL path, plus a deterministic post-reservation replacement schedule. This is a blocker, not a claimed fix.

On the current #4805 source head 76ed787711eb5950ab63679e1f558f24c8a5183c, attach and attach_read_only use pin_initialized (connection/mod.rs:95–123). That function accepts a valid 4096-byte SQLite main file after an immutable main-schema query, without establishing provenance for an existing -wal, -shm or -journal sibling. Later writable attachment calls the normal journal_mode=WAL writer open.

Disposable real-filesystem/native SQLite 3.46.1 reproduction, 3/3 sidecar suffixes: (1) create sessions.db with PRAGMA user_version=0 and close it (valid 4096-byte main file); (2) place distinct nonempty foreign sentinel bytes into one sibling sessions.db{suffix}; (3) successfully execute the exact read-only immutable SELECT count(*) FROM sqlite_schema admission analogue; (4) reopen the main database, set WAL mode, create a table, commit a row and close. For -wal, -shm, and -journal, the native SQLite writer succeeded and the original sentinel pathname was gone; none of the sentinel bytes survived. No filename replacement, competing process or failed initialization was necessary. This strengthens the preexisting incomplete-store warning: an initialized main header by itself still cannot authenticate preexisting sidecars, even when the immutable probe succeeds.

Separate post-reservation schedule, 3/3 suffixes: with O_EXCL-open descriptors retained for the main file and all three sidecars (the intended #4805 design), rename one reserved sidecar aside, substitute a foreign nonempty sentinel at its original name, then start the SQLite WAL writer. All three writes/commits succeeded; each substituted sentinel disappeared, while the original reserved inode remained held under its renamed path. Hence retained O_EXCL descriptors do not by themselves fence noncooperating POSIX pathname replacement after admission.

These are independent Python/native SQLite mechanism proofs, NOT execution of the TraceDecay Rust factory, Bazel, CI, or native macOS/Windows. Required source repair is an actual basename/sidecar provenance and open-time ownership boundary for existing as well as newly initialized stores, with an ownership-preserving VFS or private namespace policy for post-reservation replacements, and real factory/process regressions. Magic/length/schema probing alone, additional path stats, or unlink-by-path do not establish that authority. Keep recovery fail-closed rather than overwrite foreign bytes. The currently queued exact-head CI cannot establish this missing property; native macOS and isolated-daemon+external-sqlite3 endurance/restart are also still required. #4513 remains OPEN; neither #4805 nor #4803 is on master, and protected draft #4776 remains excluded.

Cross-process confirmation (same SQLite 3.46.1): repeated the post-reservation schedule with the replacer in a separate OS process while the parent held all four O_EXCL descriptors. For each suffix, the child renamed the original reserved inode and created foreign sentinel bytes at the original name; the parent proved the held fd still identified the renamed original, then initialized/committed through native SQLite. All 3/3 substituted sentinels disappeared, all 3/3 original inodes remained pinned under their new names, and all 3/3 committed rows survived. This is a deterministic cross-process standalone Python/SQLite reproduction of the uncooperative rename window, not execution of Rust or Bazel and not a macOS result.

Materialize valid empty-schema databases before ordinary attachment in
runtime-core registry and CLI integration fixtures. A bare SQLite open
leaves a zero-byte file, which the #4513 admission guard must refuse.

Refs #4513
Require the new typed invalid-header refusal for malformed graph stores.
The isolated missing-database retry fixture now removes its own closed
SQLite sidecars, so it models an absent basename without relaxing the
production refusal of unknown WAL, SHM or journal files.

Refs #4513

Copy link
Copy Markdown
Owner Author

Refs #4513 — two issue-scoped, test-only commits published on this PR by exact-old-head nonforce fast-forwards; verified each remote update is 1 commit ahead/0 behind its prior head, preserving concurrent changes.

  • a4d0871: Materialize initialized, empty-schema SQLite fixture pages with PRAGMA user_version=0 in runtime-core registry and CLI tests. Bare Connection::open left zero-byte main files; the new production incomplete-store admission guard must continue rejecting them. Three test files only; production safety checks unchanged.
  • 7506de7: Update one malformed graph-store test to require the earlier, stronger InvalidHeader refusal, and ensure the controlled missing-database retry fixture removes its own closed test-store sidecars so that it genuinely represents an absent entire SQLite basename. This is isolated fixture reconciliation, not permission for production to delete unknown WAL/SHM/journal.

Exact-old-head hosted Bazel findings (run 42496503283, SHA 76ed787): repository gates, Clippy, Feature gates and Dashboard passed; //crates/tracedecay-rusqlite-runtime:unit_test, rusqlite_suite and sessions unit targets passed. Full Linux fleet failed (5 of 183 Bazel test targets), including runtime-core, CLI and two daemon unit cases due in part to zero-byte fixtures / previous error expectation. Automation runner, hook tests and timeout failures are not attributed to #4513 without evidence. These commits address the identified fixture/expectation mismatches; they have not yet passed Bazel. New exact-head CI at 7506de7 is pending. Native macOS was skipped on previous CI and remains unqualified.

Separate Linux native Python/SQLite 3.46.1 probe on disposable files passed 15 nonzero checks, including initialized empty-schema pages, cross-process external read-only committed visibility and writer lock exclusion, plus the known dangerous existing initialized database + preexisting foreign WAL/SHM/journal overwrite on all 3 suffixes. Those are standalone mechanism probes, not Rust/Bazel or macOS tests.

Remaining high-priority safety blockers (unchanged): pin_initialized trusts the main DB header but does not authenticate existing sidecars; retained descriptors do not fence post-reservation pathname substitution by a noncooperating POSIX process; failed or partial initialization preserves ambiguous files but has no ownership-safe recovery; exact running-daemon external sqlite3 endurance/restart and native macOS/Windows are not proven. Neither this PR nor #4803 is on master. Keep #4513 open and do not merge protected draft #4776 (Kev/decision model); normal PR merging remains with the established separate maintenance task.

Refs #4513. Add a non-vacuous cross-process rollback/WAL regression that admits a page-initialized live store, checks a separate process cannot steal BEGIN IMMEDIATE, confirms read-only committed visibility, then verifies final integrity after proper pin release.

Copy link
Copy Markdown
Owner Author

Refs #4513 — published af51d02ce35d to this existing issue-scoped branch via expected-old-head non-force fast-forward from 7506de7. GitHub's exact-ref read and compare confirm 1 commit ahead, 0 behind, modifying only crates/tracedecay-rusqlite-runtime/src/connection/path_identity_tests.rs (+47 lines), preserving peer work.

This adds a non-vacuous Rust cross-process regression for the actual OpenedDatabaseFile::pin_initialized existing-store path while a different live SQLite connection in the same process holds BEGIN IMMEDIATE. In both DELETE and WAL modes, it verifies a second OS process cannot obtain a writer reservation before or after immutable-header inspection, can read only the last committed row, and can write after proper teardown; final committed row and SQLite integrity must survive. WAL main-file schema is checkpointed before the test so the immutable inspection is valid; it does not incorrectly assume an immutable reader sees uncheckpointed WAL.

An independent disposable Linux SQLite 3.46.1/Python cross-process analogue passed 16 nonzero behavioral assertions (writer exclusion, immutable schema read, committed visibility and integrity across DELETE/WAL); this is not this repository Rust/Bazel test execution. Exact-head hosted Bazel CI is pending. Native macOS remains unqualified and ordinary PR CI skips its opt-in job.

Partial safety coverage only, not issue acceptance: existing-store foreign sidecar provenance, post-reservation POSIX pathname replacement, safe incomplete-store recovery, exact daemon/external sqlite3 CLI endurance/restart, native macOS/Windows, and merge-to-master all remain open. Do not merge held draft #4776 (Kev/decision model). Ordinary merge decisions stay with the established PR-maintenance workflow. Keep #4513 open.

Refs #4513. Reject a nonregular store before opening its FIFO, without
letting the test's fallback writer block after that early refusal.
Preserve the original concurrent-open assertion if admission reaches
its blocked FIFO read path.

Copy link
Copy Markdown
Owner Author

Refs #4513 — published an issue-scoped CI regression repair on the existing separate #4805 branch: 6fb6bbbecb25. Verified exact old ref af51d02ce35d and pushed by expected-old-head non-force fast-forward; remote compare shows 1 ahead / 0 behind, exactly one test file modified. No production safety logic or unrelated PR was changed.

Actionable hosted Bazel failure on prior head af51d02: run 42509445405, //crates/tracedecay-runtime-core:unit_test timed out inside store_open_keeps_the_async_worker_serving_while_the_file_opens. The updated pin_initialized correctly rejects a FIFO as NotFile before opening it. The old test's delayed fallback thread then attempted a blocking FIFO write with no reader, hanging itself forever. The test repair distinguishes a fast, typed nonregular-file refusal from the preexisting blocked-open/released-by-async-worker path, and makes the fallback nonblocking on the timeout path. It still fails if an actual pending FIFO open stalls the runtime worker. A disposable Linux kernel FIFO probe passed 4 nonzero assertions for the refusal/no-reader deadlock mechanism; that is not a Rust/Bazel run.

Other failures in the same old-head CI are still not qualified as caused by this patch: automation_runner_test reported three scheduler backend_unconfigured results instead of expected scheduler reasons (167 passed / 3 failed), and crates/tracedecay:unit_test had a bootstrap-attempt-count mismatch (795 passed / 1 failed). The SQLite-specific hosted Bazel suites did pass on that old head: rusqlite-runtime unit 450/450, rusqlite suite 154 passed, 2 ignored, sessions unit 662/662. Do not mistake these scoped passes for a green entire workflow. New exact-head CI 42514311881 is queued; no new-head Bazel pass or native macOS evidence is claimed.

Independent disposable Linux SQLite 3.46.1 checks this run passed 14/14 assertions for separate-process read-only committed snapshots and write exclusion, and independently reproduced preexisting foreign WAL/SHM/journal overwrite on an initialized main DB (all 3 suffixes). These are native SQLite/Python probes, not the daemon + external sqlite3 CLI journey, Rust/Bazel validation, or native macOS. Existing-store sidecar provenance, post-reservation replacement races, incomplete-store recovery, daemon/CLI endurance/restart and platform qualification remain outstanding. The issue stays OPEN; ordinary merge remains in the separate PR-maintenance lane; draft #4776 and release #4763 are untouched.

devin-ai-integration[bot]

This comment was marked as resolved.

Refs #4513. Closing an O_RDONLY identity pin can release a different
SQLite writer's POSIX byte locks. Pin existing stores with O_PATH on
Linux/Android so the /proc/self/fd worker route retains inode identity
without that lock-loss side effect. Exercise pin release against a live
separate-process write contender in rollback and WAL modes.

Copy link
Copy Markdown
Owner Author

Refs #4513 — published eb2573aed459 to this existing issue-only PR using an expected-old-head, non-force fast-forward from 6fb6bbbecb25. GitHub compare verifies one commit ahead, zero behind, with only connection/mod.rs and connection/path_identity_tests.rs modified (no peer changes overwritten).

Additional real POSIX lock-loss found and narrowly repaired on Linux/Android: retaining an O_RDONLY identity descriptor for an existing SQLite file can release an unrelated writer connection's process-owned RESERVED lock when that pin is released, even if the pin acquired no lock itself. Linux/Android existing-store pins now use O_PATH: this retains inode/fstat identity and supports the existing /proc/self/fd/N SQLite worker route, but closing this path-only fd does not drop other connections' POSIX locks. Newly created stores and Windows/non-Linux Unix paths are unchanged. Added a falsifiable, non-vacuous Rust subprocess regression that releases the production pin_initialized pin while another live SQLite writer still holds a transaction, checks external writer exclusion in both rollback and WAL modes, then checks commit visibility and integrity.

Independent Linux SQLite 3.46.1 proof, not repository Bazel: 22 nonzero assertions across DELETE/WAL and O_RDONLY/O_PATH pins. With an O_RDONLY pin, rollback-mode external writer admission changed from blocked to allowed immediately on pin close. O_PATH retained blocked status; direct SQLite reads through /proc/self/fd/<O_PATH fd>, commit, and integrity checks passed. This does not establish native macOS equivalence.

Exact-head hosted CI is pending. No Rust/Bazel pass on this new head is claimed. Previous-head run 42514311881 had 182/183 Linux Bazel test targets passing, with unrelated automation_runner_test scheduler/backend assertions failing; macOS was skipped and Windows still in progress at last check.

Still open: foreign sidecar provenance for existing initialized databases, adversarial post-reservation namespace replacement, incomplete-store reconciliation, exact daemon + external sqlite3 endurance/restart, native macOS/Windows qualification, and landing proven acceptance on master. Do not close #4513. Excluded draft #4776 and release #4763 were untouched; ordinary non-model merging remains with the existing maintenance lane.

Repair three hosted automation_runner_test failures by supplying the
configured executable identity required by the production prefilter.
Use the same identity for the prefilter and retained runner, preserving
manual-schedule, exact-ledger, live-lock and lock-race assertions.

Add a production-prefilter regression for unconfigured refusal,
deduplication and same-occurrence recovery after binding, with zero
backend calls and no effectful terminal. Do not relax backend admission,
change timeouts, or alter the graph publication changes.

Refs #4532

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 2 new potential issues.

Devin Review

Comment on lines +125 to +133
if !initialized {
for suffix in ["-wal", "-shm", "-journal"] {
match std::fs::symlink_metadata(sidecar_path(path, suffix)) {
Ok(_) => return Err(OpenedDatabaseFileError::IncompleteInitialization),
Err(error) if error.kind() == io::ErrorKind::NotFound => {}
Err(_) => return Err(OpenedDatabaseFileError::Inspect),
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 Uncheckpointed WAL stores cannot reopen

When a store's first schema is committed only to WAL, pin_initialized rejects its valid main file as incomplete. The immutable inspection cannot see committed WAL tables, so existing read-only and writable attachments both fail.

Learn more

A SQLite WAL database can contain a valid committed schema in -wal while its main file still contains no application tables. pin_initialized inspects only the main file through open_immutable_reader, then treats the presence of any sidecar as proof of an incomplete initialization. Both attach and attach_read_only call this check before opening SQLite normally. This rejects a valid database with an uncheckpointed first schema, including the fixture exercised by initialized_wal_with_uncheckpointed_schema_remains_attachable.

Example: Create sessions.db in WAL mode, create probe and insert 42, then keep the writer open without checkpointing. The main file lacks probe, while sessions.db-wal contains it; an existing attachment returns IncompleteInitialization instead of reading 42.

Recommended fix: Distinguish an interrupted new-basename reservation from a legitimate existing WAL family using an ownership marker or publication witness, without treating an uncheckpointed main-file schema as an incomplete database or opening unowned sidecars for a genuinely incomplete store.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +232 to +236
for suffix in SIDECARS {
remove_if_present(&self.witness(suffix))?;
}
remove_if_present(&self.transferred())?;
sync_directory(&self.directory, SYNC)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Completed stores retain staging directories

After a successful publication, finish_publication leaves its lock file and .tracedecay-init directory beside every store. Temporary staging remains after the operation finishes, contrary to the repository's staging-cleanup rule.

Learn more

Initialization uses a private directory for schema creation and crash-recovery witnesses. After a successful handoff, finish_publication removes the witnesses and transferred link, but retains schema-installation.lock and its parent directory indefinitely. The repository's AGENTS.md requires temporary staging to be removed when the operation finishes, not left beside each initialized store.

Example: Initialize sessions.db successfully. Its database is published and the witnesses are gone, but sessions.db.tracedecay-init/schema-installation.lock persists across restarts.

Recommended fix: Retire the empty staging directory and its lock after successful publication while respecting live leases and avoiding pathname-based deletion of a replaced directory. Keep crash recovery available only until the handoff finishes.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

@ScriptedAlchemy ScriptedAlchemy changed the title fix(sqlite): reserve new sidecar namespace and preserve failed files fix(sqlite): publish complete stores and recover interrupted initialization Nov 8, 2026

This branch has not been deployed

No deployments
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