Replicator authority: spike notes updated for the helper process, the same-process negative case and the transition model
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 file changed,
+2
−2
docs/superpowers/specs/2026-10-03-replicator-pinned-roots-spikes.md+2 −2
CI passed — run details
diff --git a/docs/superpowers/specs/2026-10-03-replicator-pinned-roots-spikes.md b/docs/superpowers/specs/2026-10-03-replicator-pinned-roots-spikes.md| index ca6136036..7de127e98 100644 |
| --- a/docs/superpowers/specs/2026-10-03-replicator-pinned-roots-spikes.md |
| +++ b/docs/superpowers/specs/2026-10-03-replicator-pinned-roots-spikes.md |
| @@ -6,10 +6,10 @@ Read-only findings (no code written or run), 2026-10-03 06:45, for plan step 0 o |
| - The driver ships a Go-registered VFS: `modernc.org/sqlite/vfs` registers a VFS table with `sqlite3.Xsqlite3_vfs_register` from Go (`vfs/patches.go:195`) and keeps Go state in `pAppData`. Its method table is a C source (`vfs/c/vfs.c`, 713 lines: `vfsOpen`, `vfsDelete`, `vfsAccess`, `vfsFullPathname`, `vfsRead`, `vfsWrite`, and locking that is a no-op) compiled through the same toolchain as the library, over an `fs.FS` and read-oriented. |
| - So the route exists: a VFS implemented outside the library, registered by name, chosen per connection with `?vfs=<name>`. What ours must differ in: `xOpen` as `openat` from a retained directory descriptor with `O_NOFOLLOW|O_CLOEXEC|O_NOCTTY` for the main database and each derived name (journal, WAL, SHM); `xAccess` and `xDelete` as `faccessat` and `unlinkat` on it; `xFullPathname` returning the name unchanged (so no canonicalisation walks a tenant's ancestry); real `xLock`/`xUnlock`/`xCheckReservedLock` and `xShmMap`/`xShmLock`/`xShmBarrier`/`xShmUnmap` (WAL mode needs them; the demo's no-op locks are not acceptable), which can delegate to the default Unix VFS's file methods once the descriptor is open, since those operate on the file descriptor, not the path. |
| -- The spike's deliverable, when run: a throw-away program that registers such a VFS, opens a database and its WAL through a directory descriptor, writes, checkpoints, reopens, and shows with `fstat` that every file SQLite opened is beneath the descriptor; and the list of library entry points that take a pathname outside the VFS (none are expected for the connection; `sqlite3_open_v2` with `SQLITE_OPEN_URI` and the `vfs` parameter is the only path in). |
| +- Revision 3's review added what the spike must settle about locking: SQLite coordinates connections within one process through its own per-inode bookkeeping (`unixInodeInfo`), so a second VFS in the same process cannot exclude an ordinary connection by reproducing POSIX byte ranges; the design therefore runs the fork and its VFS in a helper process per fleet. The spike's deliverable, when run: a throw-away helper program that registers such a VFS, receives a directory descriptor over a unix socket (`SCM_RIGHTS`), opens a database and its WAL through it, writes, checkpoints, closes and reopens, while a second program holds an ordinary connection and writes, reads and checkpoints concurrently, with the locks observed to exclude correctly across the two processes; the same-process arrangement run as the negative case and shown unsafe; SHM created and removed through the VFS; the moved-file check; closure releasing only the helper's locks; a system-call trace (`strace -f`) of the library's operations rather than `fstat` alone, since identity does not show ancestry; and the list of library entry points that take a pathname outside the VFS (none are expected for the connection; `sqlite3_open_v2` with `SQLITE_OPEN_URI` and the `vfs` parameter is the only path in). On amd64 and arm64. |
| - Risk noted: the generated library's VFS structs are per-platform (`vfs_linux_amd64.go` and siblings); a VFS written in Go against them is per-platform too, and the agent builds for linux/arm64 as well as amd64 (the cross builds in the gate). |
| ## The admission's cost on the request path |
| - `DurabilityClient.Start` (`internal/usage/durabilityledger.go:1007`) is already one transaction per attempt on a `synchronous=FULL` WAL database: an `INSERT` into `attempts`, `touchDay`, `noteInstant`, `persistSeq`, commit. The meter gives it `requestMeterBudget` (25 ms) and sends regardless of its outcome (`internal/lease/requestmeter.go`). |
| -- The design's admission is that same commit moved to before permission: the gate's answer waits for it, and a commit that misses the budget refuses the request instead of sending anyway. The cost is therefore not a second commit but a dependency: the request path's latency includes the ledger's commit, and a slow disk refuses requests rather than sending unrecorded ones. The spike's deliverable: the ledger's commit latency distribution on a box's disk under the replicator's request rate (litestream's sync interval per database times the fleet's size), and the budget to set from it. |
| +- The design's admission is that same commit moved to before permission: the gate's answer waits for it, and a commit that misses the budget refuses the request instead of sending anyway (the late commit is then marked unissued by the goroutine that awaited it). The cost is therefore not a second commit but a dependency: the request path's latency includes the ledger's commit, and a slow disk refuses requests rather than sending unrecorded ones. With the helper process the admission crosses a socket as well. The spike's deliverable: the ledger's commit latency distribution on a box's disk under the replicator's request rate (litestream's sync interval per database times the fleet's size) plus the socket round trip, and the budget to set from it; and, as an executable model, the transition table of revision 4 (both completion orders; `unissued`, `unrecorded`, `recovered-unknown`; discovery with zero pending; a failed discovery write under the pending-discovery latch; construction links and transitive inheritance across three fleets; the producer fence at a shutdown timeout; a kill at each boundary). |