Design: the replicator's authority, revision 7 (discovery replay reconciles the counted admissions' settlements before the overlay is cleared; the recorder participant registered at Start and a never-started one closed as unrecorded after proven helper exit; the abort transitions and the pending set's cleanup, ambiguous commits read back by token; the lineage record's crash-durable write protocol and a migration marker; the discovery mutex named as new; the restore statement limited; the companion's xAccess aligned)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2 files changed,
+9
−9
docs/superpowers/specs/2026-10-03-replicator-pinned-roots-design.md+8 −8docs/superpowers/specs/2026-10-03-replicator-pinned-roots-spikes.md+1 −1
CI passed — run details
diff --git a/docs/superpowers/specs/2026-10-03-replicator-pinned-roots-design.md b/docs/superpowers/specs/2026-10-03-replicator-pinned-roots-design.md| index 232f1cd51..b1a004b83 100644 |
| --- a/docs/superpowers/specs/2026-10-03-replicator-pinned-roots-design.md |
| +++ b/docs/superpowers/specs/2026-10-03-replicator-pinned-roots-design.md |
| @@ -1,6 +1,6 @@ |
| # The replicator's authority: retained descriptors, a durable admission, claims by identity |
| -Date: 2026-10-03. Status: design, revision 6, for review (revision 5 was rejected by Astra on two P1s: the admission's insertion had no ordering point against discovery, so a discovery between the proof and the commit counted nothing and a later send fell outside the closed uncertainty; construction did not backfill already-closed intervals for a successor's new owners before acknowledgement; and P2s: the transition table and the helper-death rule disagreed and the recorder participant was synthesised while its worker could still run; proof-only callers of admit would have created admissions with no terminal; a terminal `.` lost the directory requirement; conflicting summaries; each answered here. Revision 4 was rejected by Astra on four P1s: the helper has several connections of its own and needs SQLite's within-process lock bookkeeping between them; the producer fence did not cover a helper that outlives the agent with an admission in hand; an intermediate fleet closed before acknowledging broke transitive protection; a fleet could be marked closed with a discovery uncommitted; and four P2s: a dead helper's admissions had no terminal in the running agent, the sleeping binding persisted a reusable inode, the lineage predicate named header fields that identify nothing, the walk's grammar refused a `.` target the predicate accepts; each answered here. Revision 3 was rejected by Astra on five P1s: two SQLite VFSs in one process cannot coordinate through POSIX locks, since the library's own per-inode bookkeeping is in-process; a recorder that abandons early could settle an admission before its transport ended; an admission-only barrier does not cover a discovery whose uncertainty write failed after every admission settled; links at acknowledgement contradicted acknowledging only after settlement, and an inherited uncertainty vanished at the next successor; the ledger's lock does not prove the old process gone; and four P2s: closure at zero pending and the margin's extra day, sleeping claims with no binding, absolute and `.` spellings in the walk, `(dev, ino)` as a lineage; each answered here. Revision 2 was rejected by Astra on three P1s: the VFS delegated SQLite's Unix file methods, which open the SHM and check moved files by pathname themselves; every settled admission wrote a permanent hold, so ordinary replication never read complete; a late owner-scoped extension did not reach successors that had already acknowledged; and six P2s: orphan admissions never terminated, `..` and chain edges unspecified, `dup` sharing the directory offset, the platform-link predicate not preserved and terminal links unclassified, the uid producer without a lifecycle, one billing owner not one replicator; each answered here. Revision 1 was rejected by Astra on five P1s: SQLite canonicalises a `/proc` pathname before it opens anything and this driver passes no `nofollow`; `os.Root` confines but re-resolves every name, so a swap between a listing and an inspection still moves the answer; a `Start` row is not an admission, since the meter sends without one; the attempt-derived window lost a request completed before the change was noticed and the days between an admission and a finish; `closed_at` was not a handover; plus the directory open before `O_DIRECTORY`, per-owner holds the ledger cannot store, a uid with no producer, I3 against Part D, and tests). Branch `design-replicator-pinned-root`, off `feat/carloku-metered-replicator-20261001` at `e1280811`. It answers the eleven findings the twentieth review deferred (`DISPOSITIONS-metered-replicator.md`, review twenty) and keeps decisions 10 to 13 and approval H (`DECISIONS-FOR-PAUL.md`, section D, on the launch branch) as parameters. Nothing is built, wired or applied. |
| +Date: 2026-10-03. Status: design, revision 7, for review (revision 6 was rejected by Astra on one P1: a discovery whose write failed had no rule for the admissions that settled before its retry, so the replayed interval could lose a settlement; and three P2s: a helper dying before its recorder started left an admission nobody could finish; the transition table lacked the abort edges and the pending set's cleanup; the lineage record cited a durability discipline the shadow state does not have; each answered here. Revision 5 was rejected by Astra on two P1s: the admission's insertion had no ordering point against discovery, so a discovery between the proof and the commit counted nothing and a later send fell outside the closed uncertainty; construction did not backfill already-closed intervals for a successor's new owners before acknowledgement; and P2s: the transition table and the helper-death rule disagreed and the recorder participant was synthesised while its worker could still run; proof-only callers of admit would have created admissions with no terminal; a terminal `.` lost the directory requirement; conflicting summaries; each answered here. Revision 4 was rejected by Astra on four P1s: the helper has several connections of its own and needs SQLite's within-process lock bookkeeping between them; the producer fence did not cover a helper that outlives the agent with an admission in hand; an intermediate fleet closed before acknowledging broke transitive protection; a fleet could be marked closed with a discovery uncommitted; and four P2s: a dead helper's admissions had no terminal in the running agent, the sleeping binding persisted a reusable inode, the lineage predicate named header fields that identify nothing, the walk's grammar refused a `.` target the predicate accepts; each answered here. Revision 3 was rejected by Astra on five P1s: two SQLite VFSs in one process cannot coordinate through POSIX locks, since the library's own per-inode bookkeeping is in-process; a recorder that abandons early could settle an admission before its transport ended; an admission-only barrier does not cover a discovery whose uncertainty write failed after every admission settled; links at acknowledgement contradicted acknowledging only after settlement, and an inherited uncertainty vanished at the next successor; the ledger's lock does not prove the old process gone; and four P2s: closure at zero pending and the margin's extra day, sleeping claims with no binding, absolute and `.` spellings in the walk, `(dev, ino)` as a lineage; each answered here. Revision 2 was rejected by Astra on three P1s: the VFS delegated SQLite's Unix file methods, which open the SHM and check moved files by pathname themselves; every settled admission wrote a permanent hold, so ordinary replication never read complete; a late owner-scoped extension did not reach successors that had already acknowledged; and six P2s: orphan admissions never terminated, `..` and chain edges unspecified, `dup` sharing the directory offset, the platform-link predicate not preserved and terminal links unclassified, the uid producer without a lifecycle, one billing owner not one replicator; each answered here. Revision 1 was rejected by Astra on five P1s: SQLite canonicalises a `/proc` pathname before it opens anything and this driver passes no `nofollow`; `os.Root` confines but re-resolves every name, so a swap between a listing and an inspection still moves the answer; a `Start` row is not an admission, since the meter sends without one; the attempt-derived window lost a request completed before the change was noticed and the days between an admission and a finish; `closed_at` was not a handover; plus the directory open before `O_DIRECTORY`, per-owner holds the ledger cannot store, a uid with no producer, I3 against Part D, and tests). Branch `design-replicator-pinned-root`, off `feat/carloku-metered-replicator-20261001` at `e1280811`. It answers the eleven findings the twentieth review deferred (`DISPOSITIONS-metered-replicator.md`, review twenty) and keeps decisions 10 to 13 and approval H (`DECISIONS-FOR-PAUL.md`, section D, on the launch branch) as parameters. Nothing is built, wired or applied. |
| ## What stands today |
| @@ -8,9 +8,9 @@ The replicator (`internal/ops/litestream_metered.go`, `litestream_owners.go`; pa |
| ## Three invariants, made precise |
| -**I1, identity through retained descriptors.** (a) *Containment*: no operation of the replicator or of litestream on a tenant's files resolves a pathname above the directory the fleet accepted for that file; (b) *retention*: the descriptor that listed a directory is the descriptor against which its entries are inspected, its links read, their targets classified and its files opened, for that look, so nothing between a listing and an inspection can move the answer; (c) *acceptance*: an owned file is opened by the descriptor of its accepted directory with no link followed in the last component, and the opened file's `(dev, ino)` equals the accepted facts before any use. Retention is authority over directories and checked identities, not a frozen inventory, and it has two roots: the *accepted traversal root* (a platform root or a tenant root) through which acquisition and link classification may traverse retained ancestors, and the *accepted immediate directory* of an owned file, which every data open uses and nothing else. Ancestor replacement cannot redirect an operation, and an identity mismatch at acquisition refuses use; entries inside a retained directory can change, and a swap put back between two looks is not detected by looking; what (a) to (c) promise is that no operation is steered by such a change, and that every change a look does see is held for. Ascent never uses the kernel's `..`: it pops the retained stack. Each acquisition compares what it opened with the facts of the entry it was acquired from, and each later look compares every named edge (parent descriptor, name) with the retained child's facts, so a moved or replaced component is seen as a change at the next look. |
| +**I1, identity through retained descriptors.** (a) *Containment*: no operation of the replicator or of litestream on a tenant's files resolves a pathname above the directory the fleet accepted for that file; (b) *retention*: the descriptor that listed a directory is the descriptor against which its entries are inspected, its links read, their targets classified and its files opened, for that look, so the inspection uses the same directory authority as the listing and acquisition refuses a mismatching identity (an entry inside a retained directory can still change; what cannot happen is an answer about one directory being applied to another); (c) *acceptance*: an owned file is opened by the descriptor of its accepted directory with no link followed in the last component, and the opened file's `(dev, ino)` equals the accepted facts before any use. Retention is authority over directories and checked identities, not a frozen inventory, and it has two roots: the *accepted traversal root* (a platform root or a tenant root) through which acquisition and link classification may traverse retained ancestors, and the *accepted immediate directory* of an owned file, which every data open uses and nothing else. Ancestor replacement cannot redirect an operation, and an identity mismatch at acquisition refuses use; entries inside a retained directory can change, and a swap put back between two looks is not detected by looking; what (a) to (c) promise is that no operation is steered by such a change, and that every change a look does see is held for. Ascent never uses the kernel's `..`: it pops the retained stack. Each acquisition compares what it opened with the facts of the entry it was acquired from, and each later look compares every named edge (parent descriptor, name) with the retained child's facts, so a moved or replaced component is seen as a change at the next look. |
| -**I2, a durable admission, two kinds of hold.** No request is sent until a row naming it (fleet, owner, admission instant) is durable. A *barrier* is temporary: a day is not published complete for an admission's owner while that admission is unsettled, and lifts when it settles. An *uncertainty* is permanent: an interval written when a fleet's authority is found changed, from the earlier of today's lower bound (the registry proof, the last clean look's start) and the earliest admission of the fleet then unsettled, minus the margin, open until every admission of the fleet is settled, then closed at the latest settlement plus the margin; it holds its days for the fleet's owners and for every owner linked to it (below). An admission that settles with no uncertainty open on its fleet writes nothing permanent: the ordinary day of an unchanged fleet becomes complete. Discovery and its first hold are recorded in one transaction; if that transaction fails, a publication-visible *pending discovery* (the ledger's failure latch, extended with the lower bound) bars every day from that bound for every owner the fleet reaches until the write succeeds, and the fleet refuses every request meanwhile. Settlement is the last of the recorder's and the transport's completions, written by whichever lands last, with the upper bound no earlier than both. A fleet reaches, from its construction, every uncertainty and every unsettled admission of every ancestor not yet acknowledged, existing or discovered later; acknowledgement is root-first and copies the predecessor's whole effective set, open and closed, into the successor's; a fleet's construction materialises, for its owners, every interval its ancestry already holds; a fleet is never closed with a discovery of its own uncommitted. A successor acknowledges a predecessor only when the predecessor is terminal, and the acknowledgement links the successor's owners to every uncertainty of the predecessor still open, so a late extension reaches them. |
| +**I2, a durable admission, two kinds of hold.** No request is sent until a row naming it (fleet, owner, admission instant) is durable. A *barrier* is temporary: a day is not published complete for an admission's owner while that admission is unsettled, and lifts when it settles. An *uncertainty* is permanent: an interval written when a fleet's authority is found changed, from the earlier of today's lower bound (the registry proof, the last clean look's start) and the earliest admission of the fleet then unsettled, minus the margin, open until every admission of the fleet is settled, then closed at the latest settlement plus the margin; it holds its days for the fleet's owners and for every owner linked to it (below). An admission that settles with no uncertainty open on its fleet writes nothing permanent: the ordinary day of an unchanged fleet becomes complete. Discovery and its first hold are recorded in one transaction; if that transaction fails, a publication-visible *pending discovery* (the ledger's failure latch, extended with the lower bound) bars every day from that bound for every owner the fleet reaches until the write succeeds, and the fleet refuses every request meanwhile. Settlement is the last of the recorder's and the transport's completions, written by whichever lands last, with the upper bound no earlier than both. A fleet reaches, from its construction, every uncertainty and every unsettled admission of every ancestor not yet acknowledged, existing or discovered later; acknowledgement is root-first and copies the predecessor's whole effective set, open and closed, into the successor's; a fleet's construction materialises, for its owners, every interval its ancestry already holds; a fleet is never closed with a discovery of its own uncommitted. A discovery whose write failed is replayed as one transaction that opens the uncertainty at the original lower bound, materialises the settlement of every admission it counted that settled meanwhile, and closes it only if none of them remains unsettled; the overlay clears after that commit and never before. |
| **I3, one billing owner and one replication authority by file identity.** A file is billed to one `(account, app)` and replicated by one authority (the external replicator, the activator's hibernation path, or this fleet; today the first two exist and the fleet is unwired); any number of routes of that one owner may claim it through any spelling; two owners on one `(dev, ino)`, or two replication authorities, whether the file is listed or skipped, whether a claimant is live or hibernating, make it unowned (or refused for this fleet) with every claimant named, until an explicit handover of authority exists; every claimant's own authority check must pass; a claim with no identity yet is recorded and reconciled when it resolves; identity grouping is a look, not a snapshot, and relaxes none of today's hard-link, sidecar or filesystem checks; a database's lineage (which replica prefix continues) is a durable record in the shadow state, not an inode number; a required platform database that cannot be reached is an incomplete box. |
| @@ -34,7 +34,7 @@ Two layers. |
| 1. **The fork's own file operations** (`db.go`: the long-lived descriptor, WAL reads, shadow WAL, checkpoints, the `Stat`s of the database and its directory; `replica.go:186` and every LTX, temporary and cleanup path; enumerated in the plan's task by reading every call, not by sampling) take two retained directories, `SetDataDir(*authority.Dir, name)` for the database's directory and `SetMetaDir(*authority.Dir)` for the platform's shadow-state directory, and become `openat`/`fstatat`/`renameat`/`unlinkat` relative to them with `O_NOFOLLOW`; a sidecar is opened with `O_NONBLOCK` and refused unless `fstat` says regular (a FIFO put in a WAL's place is refused, not read). `db.f` is opened first through `OpenRegular` against the accepted facts and its identity is the database's for the life of the `DB`; before every reopen or sidecar open the fork compares `fstatat(dirfd, name)` with that identity and refuses on a difference (an error the fleet treats as a change). |
| -2. **SQLite's own opens, through a VFS this design owns whole, in a process of its own.** POSIX record locks belong to a process, and SQLite coordinates connections within one process through its own in-memory bookkeeping (`unixInodeInfo`: a second VFS in the same process reproducing the byte ranges would take a lock the ordinary connection believes it holds). So the fork and its VFS run in a **helper process** (`carlos-replicate`, one per fleet), in which every SQLite connection is one of the VFS's own, and the VFS keeps the within-process bookkeeping SQLite's Unix VFS keeps (litestream holds a read transaction on one pooled connection while working through another, so the helper has several): one shared record per inode in the helper, with the count of connections holding each shared lock, local exclusion checked before an exclusive lock is taken (as `unixInodeInfo` does), a POSIX unlock issued only when the last local holder releases, and a descriptor on an inode closed only when no object of that inode holds a lock (deferred otherwise), with the fork's raw descriptors (`db.f`, the WAL reads) opened through the same bookkeeping so their closes are deferred the same way. In that arrangement: the agent builds the inventory, holds the roots and the witness, and admits requests; it hands the helper, over a unix socket with `SCM_RIGHTS`, the accepted immediate directory descriptor and name of each database, the shadow-state directory descriptor, and a socket on which the helper asks for each admission (the gate crosses the boundary as a request and an answer; the admission row is the agent's to write) and reports each transport's end; the helper's SQLite connections then coordinate with the application's and the agent's through the kernel's locks, as two processes do today, and the fleet's close is the helper's exit (which also ends every descriptor the helper held). A helper that exits unexpectedly is a fleet that died: the agent sees the socket close, then waits for the child's exit (`waitpid`, with `SIGKILL` after a bound), and only then, with the helper proven gone, writes `transport-lost` with `transport_done_at = now` for each of the helper's admissions whose transport had not completed (idempotent transactions it retries until they commit), which settles those whose recorder is done and leaves the rest barring until their recorder worker finishes or abandons, each settlement writing its interval under the open uncertainty the helper's death opens for the fleet; then rebuilds. Only process recovery, with the fence, writes `recovered-unknown`. The library opens the database, journal, WAL and SHM by pathname through its VFS after `xFullPathname` has canonicalised what it was given, and the default Unix VFS's *file* methods also act by pathname on their own: `xShmMap` opens the `-shm` file by deriving its name from the file's stored path and calling `open` directly, the SHM's removal calls the syscall table directly, the moved-file check stats the stored path, and the directory sync opens the directory by path. So delegating file methods to the Unix VFS is not confinement, and revision 2's delegation is withdrawn. The connection is given a **descriptor-rooted VFS** whose `sqlite3_file` object and every method are this design's: `xFullPathname` returns the name unchanged; `xOpen` is `openat(dirfd, name, flags|O_NOFOLLOW|O_CLOEXEC|O_NOCTTY)` for the main database and for each derived name (journal, WAL, SHM; `O_NONBLOCK` and a regular-file `fstat` for the sidecars), and for the main database compares the opened `(dev, ino)` with the accepted facts; `xDelete` is `unlinkat` on the descriptor; `xRead`, `xWrite`, `xTruncate`, `xSync`, `xFileSize` act on the descriptor; `xLock`, `xUnlock` and `xCheckReservedLock` are the POSIX advisory locks the Unix VFS uses, on the descriptor, with the same byte ranges, so the application's own connection and this one agree; `xShmMap`, `xShmLock`, `xShmBarrier` and `xShmUnmap` open the `-shm` through `openat` on the directory descriptor and `mmap` it, with the Unix VFS's lock byte layout, so the WAL index is shared correctly with the application; `xSync` of the directory is `fsync` of the retained directory descriptor; `xFileControl` answers the controls the library asks of a WAL-mode file (`SQLITE_FCNTL_PERSIST_WAL`, `SQLITE_FCNTL_POWERSAFE_OVERWRITE`, `SQLITE_FCNTL_SIZE_HINT`, `SQLITE_FCNTL_HAS_MOVED` answered from `fstatat(dirfd, name)` against the retained identity) and `SQLITE_NOTFOUND` for the rest; no method takes a pathname from the stored name after `xOpen`. `xAccess` answers from `fstatat(dirfd, name, AT_SYMLINK_NOFOLLOW)` and reports a link as absent rather than escaping through it; `xOpen` of a temporary or null-name file (the library's transient files) is answered in the shadow-state directory descriptor, never the tenant's; permitted names are the main name and its four derived names, and any other name is refused; `xClose` closes the descriptor and releases the locks the object held; `xDeviceCharacteristics` reports the same as the Unix VFS for the filesystem; URI parameters are escaped as SQLite's URI grammar requires. The DSN is `file:<name>?vfs=carlos-dirfd-<id>` and the driver passes `vfs` through: that selects the VFS and confines nothing by itself; the confinement is the validated names and the descriptor-relative operations. |
| +2. **SQLite's own opens, through a VFS this design owns whole, in a process of its own.** POSIX record locks belong to a process, and SQLite coordinates connections within one process through its own in-memory bookkeeping (`unixInodeInfo`: a second VFS in the same process reproducing the byte ranges would take a lock the ordinary connection believes it holds). So the fork and its VFS run in a **helper process** (`carlos-replicate`, one per fleet), in which every SQLite connection is one of the VFS's own, and the VFS keeps the within-process bookkeeping SQLite's Unix VFS keeps (litestream holds a read transaction on one pooled connection while working through another, so the helper has several): one shared record per inode in the helper, with the count of connections holding each shared lock, local exclusion checked before an exclusive lock is taken (as `unixInodeInfo` does), a POSIX unlock issued only when the last local holder releases, and a descriptor on an inode closed only when no object of that inode holds a lock (deferred otherwise), with the fork's raw descriptors (`db.f`, the WAL reads) opened through the same bookkeeping so their closes are deferred the same way. In that arrangement: the agent builds the inventory, holds the roots and the witness, and admits requests; it hands the helper, over a unix socket with `SCM_RIGHTS`, the accepted immediate directory descriptor and name of each database, the shadow-state directory descriptor, and a socket on which the helper asks for each admission (the gate crosses the boundary as a request and an answer; the admission row is the agent's to write) and reports each transport's end; the helper's SQLite connections then coordinate with the application's and the agent's through the kernel's locks, as two processes do today, and the fleet's close is the helper's exit (which also ends every descriptor the helper held). A helper that exits unexpectedly is a fleet that died: the agent sees the socket close, then waits for the child's exit (`waitpid`, with `SIGKILL` after a bound), and only then, with the helper proven gone, closes the helper's lifecycle guard, drains what the socket still holds, and then writes `transport-lost` with `transport_done_at = now` for each of the helper's admissions whose transport had not completed, and `unrecorded` for each whose recorder participant was never registered (idempotent transactions it retries until they commit), which settles those whose recorder is done or never started and leaves the rest barring until their registered recorder worker finishes or abandons (a worker whose `Finish` was in the drained socket finishes; one whose input ended abandons, as today's late path does), each settlement writing its interval under the open uncertainty the helper's death opens for the fleet; then rebuilds. Only process recovery, with the fence, writes `recovered-unknown`. The library opens the database, journal, WAL and SHM by pathname through its VFS after `xFullPathname` has canonicalised what it was given, and the default Unix VFS's *file* methods also act by pathname on their own: `xShmMap` opens the `-shm` file by deriving its name from the file's stored path and calling `open` directly, the SHM's removal calls the syscall table directly, the moved-file check stats the stored path, and the directory sync opens the directory by path. So delegating file methods to the Unix VFS is not confinement, and revision 2's delegation is withdrawn. The connection is given a **descriptor-rooted VFS** whose `sqlite3_file` object and every method are this design's: `xFullPathname` returns the name unchanged; `xOpen` is `openat(dirfd, name, flags|O_NOFOLLOW|O_CLOEXEC|O_NOCTTY)` for the main database and for each derived name (journal, WAL, SHM; `O_NONBLOCK` and a regular-file `fstat` for the sidecars), and for the main database compares the opened `(dev, ino)` with the accepted facts; `xDelete` is `unlinkat` on the descriptor; `xRead`, `xWrite`, `xTruncate`, `xSync`, `xFileSize` act on the descriptor; `xLock`, `xUnlock` and `xCheckReservedLock` are the POSIX advisory locks the Unix VFS uses, on the descriptor, with the same byte ranges, so the application's own connection and this one agree; `xShmMap`, `xShmLock`, `xShmBarrier` and `xShmUnmap` open the `-shm` through `openat` on the directory descriptor and `mmap` it, with the Unix VFS's lock byte layout, so the WAL index is shared correctly with the application; `xSync` of the directory is `fsync` of the retained directory descriptor; `xFileControl` answers the controls the library asks of a WAL-mode file (`SQLITE_FCNTL_PERSIST_WAL`, `SQLITE_FCNTL_POWERSAFE_OVERWRITE`, `SQLITE_FCNTL_SIZE_HINT`, `SQLITE_FCNTL_HAS_MOVED` answered from `fstatat(dirfd, name)` against the retained identity) and `SQLITE_NOTFOUND` for the rest; no method takes a pathname from the stored name after `xOpen`. `xAccess` answers from `fstatat(dirfd, name, AT_SYMLINK_NOFOLLOW)` and reports a link as absent rather than escaping through it; `xOpen` of a temporary or null-name file (the library's transient files) is answered in the shadow-state directory descriptor, never the tenant's; permitted names are the main name and its four derived names, and any other name is refused; `xClose` closes the descriptor and releases the locks the object held; `xDeviceCharacteristics` reports the same as the Unix VFS for the filesystem; URI parameters are escaped as SQLite's URI grammar requires. The DSN is `file:<name>?vfs=carlos-dirfd-<id>` and the driver passes `vfs` through: that selects the VFS and confines nothing by itself; the confinement is the validated names and the descriptor-relative operations. |
| What this closes: every open the library makes for the database and its sidecars, including reopens, checkpoints, the SHM and the first open's initialisation writes, happens beneath the accepted immediate directory with no link followed and no pathname above it walked, and the helper's own connections are coordinated among themselves by the VFS's per-inode bookkeeping and with every connection in other processes by the kernel's locks. What it leaves, stated: within its own directory a tenant can still replace `<name>` or a sidecar with another regular file it owns between two opens; the identity comparison at `xOpen` and before each of the fork's opens refuses the main database's replacement; a sidecar swapped under a live connection is a corruption of the replica, which litestream's own consistency checks are the place to catch and which the plan's task for the fork adds a test for. That residual is "the owner's own doing" only under decision 12 (a foreign user's directory inside a tenant's tree can supply sidecars today); until 12 is decided it is stated as a residual with two possible authors. |
| @@ -42,9 +42,9 @@ The VFS is the plan's first spike and it must prove, before anything else of Par |
| ## Part C. The durable admission, two kinds of hold, and a handover on disk (Astra 3, 4) |
| -**Proof and admission are two things.** Fleet construction, `Open` and every watch tick ask the ownership *proof* (today's `admit`: the witness unchanged, the fleet not stale) and create nothing; only the request gate (`lease.RequestGate`, asked by the helper for each physical request) performs an *admission*, which is a proof followed by a row, so an idle healthy fleet has no admission and nothing bars its days. **The admission is a row, written before the send, with one ordering point against discovery.** Under the fleet's mutex (`f.mu`, the one `admit` and `lookAgain` already share) the gate checks the proof and, if the fleet is not stale and no discovery is in progress, registers the admission in the fleet's in-memory *pending set* with its `admitted_at`; it releases the mutex and commits the row in the durability ledger's own database (`admissions`: `token`, `fleet_id`, `owner account and app`, `admitted_at`, `transport_done_at`, `recorder_done_at`, `settled_at`, `how`), `synchronous=FULL`, within a bounded budget; after the commit it takes the mutex again and, if the fleet went stale or a discovery began since registration, marks the row `unissued` and refuses. Discovery, under the same mutex, sets the in-progress flag and counts the pending set, durable or not yet, as unsettled admissions when it computes its lower bound, and a registered admission that the discovery has counted and that then commits is inside the uncertainty (it was counted); one that commits after the discovery began is refused. So an admission is either counted by every discovery that follows its registration or refused, and Astra's 23:58:10 sequence (proof, discovery, commit, a send on the 4th) ends in a refusal at the recheck, not a send outside the uncertainty. The gate waits for the commit. A commit that does not return within the budget refuses the request (`ErrAdmissionNotDurable`; litestream sees a failed upload and does not advance) and the goroutine that awaited it stays to see the commit land and then marks the row `how = unissued, settled_at = now` in its own transaction, so a late commit never authorises anything and never blocks a day; if the process dies between, recovery classifies the row as below. The meter's `Start` row carries the token (`Start` may still fail or be late, as today; what is now mandatory is the admission, not the `Start`). An admission has two participants, the transport and the recorder, and is settled by whichever finishes last. The transport writes `transport_done_at` from the SDK middleware's finalize step, or `transport-lost` when its helper is proven gone (below). The recorder writes `recorder_done_at` with its outcome: `finished` (`Finish`), `abandoned` (`Abandon`), or `unrecorded` (the request path, when `Start` failed and made no attempt row, which today calls neither; it now writes this terminal); the recorder participant is never synthesised while its worker can still run: today's meter lets recording workers outlive the request path, and the admission stays unsettled (barring) until that worker finishes or abandons. The participant that finds the other already done writes `settled_at = max(transport_done_at, recorder_done_at)` and `how`, and performs, in that same transaction, the uncertainty extension or closure below. **The transition table is authoritative and is the plan's model:** states `registered` (in memory only), `admitted` (durable), `transport done`, `recorder done`, `settled`; edges `registered → admitted` (the commit), `registered → unissued` (the recheck after the commit found the fleet stale or a discovery begun; the row is written as the terminal), `admitted → transport done | recorder done`, `transport done → settled` (the recorder's completion), `recorder done → settled` (the transport's completion or `transport-lost`), and `admitted | transport done | recorder done → recovered-unknown` at process recovery only (all missing timestamps `now`); `transport-lost` is a transport completion, not a terminal, so an admission whose recorder is still running after its helper died stays `recorder pending`. Admissions are the ledger's pending state moved to before the send and given an owner. |
| +**Proof and admission are two things.** Fleet construction, `Open` and every watch tick ask the ownership *proof* (today's `admit`: the witness unchanged, the fleet not stale) and create nothing; only the request gate (`lease.RequestGate`, asked by the helper for each physical request) performs an *admission*, which is a proof followed by a row, so an idle healthy fleet has no admission and nothing bars its days. **The admission is a row, written before the send, with one ordering point against discovery.** Under a *discovery mutex* on the fleet (a new requirement: today `admit` and `lookAgain` do not share one, and the admission comments allow a discovery to run unlocked; this design serialises registration, the recheck and the start of a discovery on it) the gate checks the proof and, if the fleet is not stale and no discovery is in progress, registers the admission in the fleet's in-memory *pending set* with its `admitted_at`; it releases the mutex and commits the row in the durability ledger's own database (`admissions`: `token`, `fleet_id`, `owner account and app`, `admitted_at`, `transport_done_at`, `recorder_started_at`, `recorder_done_at`, `settled_at`, `how`), `synchronous=FULL`, within a bounded budget; after the commit it takes the mutex again and, if the fleet went stale or a discovery began since registration, marks the row `unissued` and refuses. Discovery, under the same mutex, sets the in-progress flag and counts the pending set, durable or not yet, as unsettled admissions when it computes its lower bound, and a registered admission that the discovery has counted and that then commits is inside the uncertainty (it was counted); one that commits after the discovery began is refused. So an admission is either counted by every discovery that follows its registration or refused, and Astra's 23:58:10 sequence (proof, discovery, commit, a send on the 4th) ends in a refusal at the recheck, not a send outside the uncertainty. The gate waits for the commit. A commit that does not return within the budget refuses the request (`ErrAdmissionNotDurable`; litestream sees a failed upload and does not advance) and the goroutine that awaited it stays to see the commit land and then marks the row `how = unissued, settled_at = now` in its own transaction, so a late commit never authorises anything and never blocks a day; if the process dies between, recovery classifies the row as below. The meter's `Start` row carries the token (`Start` may still fail or be late, as today; what is now mandatory is the admission, not the `Start`). An admission has two participants, the transport and the recorder, and is settled by whichever finishes last. The transport writes `transport_done_at` from the SDK middleware's finalize step, or `transport-lost` when its helper is proven gone (below). The recorder writes `recorder_done_at` with its outcome: `finished` (`Finish`), `abandoned` (`Abandon`), or `unrecorded` (the request path, when `Start` failed and made no attempt row, which today calls neither; it now writes this terminal); the recorder participant is never synthesised while its worker can still run: today's meter lets recording workers outlive the request path, and the admission stays unsettled (barring) until that worker finishes or abandons. The recorder runs in the agent, fed by the helper's messages over the fleet socket (`Start`, `Finish`, `Abandon` with the token), and the agent *registers* the recorder participant when it receives `Start` (`recorder_started_at`, under an agent-owned lifecycle guard per helper); a `Start` that the agent receives after it has begun the helper's termination handling is refused (the guard is closed first, then the socket drained, so registration and exit handling cannot interleave). The participant that finds the other already done writes `settled_at = max(transport_done_at, recorder_done_at)` and `how`, and performs, in that same transaction, the uncertainty extension or closure below. **The transition table is authoritative and is the plan's model:** states `registered` (in memory only), `admitted` (durable), `transport done`, `recorder done`, `settled`; edges `registered → admitted` (the commit), `admitted → unissued` (the recheck after the commit found the fleet stale or a discovery begun, or the gate's budget elapsed before the answer was returned: the row is rewritten as the terminal and the helper never receives the token), `registered → cancelled` (the commit definitely failed, no row exists: the registration leaves the pending set), `admitted → transport done | recorder done`, `transport done → settled` (the recorder's completion), `recorder done → settled` (the transport's completion or `transport-lost`), and `admitted | transport done | recorder done → recovered-unknown` at process recovery only (all missing timestamps `now`); `transport-lost` is a transport completion, not a terminal, so an admission whose recorder is still running after its helper died stays `recorder pending`. **The pending set and the abort edges.** A registration leaves the pending set only when its terminal, or its row, is durable: after `admitted` the row is the record and the set entry is dropped; after `cancelled` the entry is dropped once the cancellation is decided. A discovery that counted a registration treats its cancellation or its `unissued` as a settlement at that instant with an empty interval, so an uncertainty that waited only on it closes then, and the closure test ("no counted admission unsettled") ranges over rows and pending registrations alike. **An ambiguous commit** (an error or a timeout after the write may have landed) keeps the registration in the pending set, barring, until a read-back by `token` answers: a row found takes the `admitted → unissued` edge; none found takes `registered → cancelled`; the read-back is retried until it answers. Every terminal write is keyed by `token` and idempotent, retried until it commits; nothing is dropped from the pending set before its terminal is durable. Admissions are the ledger's pending state moved to before the send and given an owner. |
| -**Two kinds of hold.** A *barrier* is the ledger's completeness rule: a day is not published complete for an admission's owner while an admission of that owner admitted on or before that day is unsettled (box-wide while Part C2's owner scope is not in use); it is a query over `admissions`, writes nothing, and lifts at settlement. An *uncertainty* is a row (`uncertainties`: `id`, `fleet_id`, `from`, `to` null while open, `reason`) written when a fleet's authority is found changed (by the witness, by `Recheck`, by a changed file), in the transaction that also writes the holds for the days of `[from, now]` for every owner the uncertainty reaches: `from = min(the registry proof, the last clean look's start, the earliest admitted_at of the fleet's admissions then unsettled) - margin`. If discovery finds no admission of the fleet unsettled, the uncertainty is opened and closed in that transaction (`to = discovery + margin`). While an uncertainty is open, settlements of that fleet's admissions extend it: each settlement, in its own transaction, writes the holds for every day of `[its admitted_at, its settled_at]` for every owner the uncertainty reaches, and the last settlement closes it with `to = max(discovery, that settlement) + margin` and materialises every day of the closed interval, the margin's extra day across midnight included; recovery uses the same helper and advances the publication watermark past the days it materialised. An admission that settles while no uncertainty of its fleet is open writes nothing permanent: the ordinary day of an unchanged fleet becomes complete when its barrier lifts. The multiple-midnights sequence (admitted on the 2nd, the change at 23:58:10, sent on the 3rd, finished on the 4th) holds the 2nd, 3rd and 4th through the settlement's interval, whichever participant finished last; the request completed before the change was noticed is inside the lower bound as today. **If the discovery transaction fails** the barrier is not enough (every admission may already be settled): discovery first latches a *pending discovery* in the ledger's in-memory failure overlay (`latchFailure`, extended to carry the lower bound and the owners reached), which the publisher's snapshot already overlays and detects across a drain (today's `snapshotOnce` and `drainFailures` behaviour, extended rather than replaced: a failed drain must not stop the overlay from making the snapshot incomplete), so every day from the bound reads incomplete for those owners until the uncertainty commits; the fleet refuses every request meanwhile and retries the write; the fleet cannot be marked `closed` while the discovery is uncommitted (above); a process that dies with the latch held leaves a fleet not `closed`, which recovery covers from the fleet's `opened_at`. |
| +**Two kinds of hold.** A *barrier* is the ledger's completeness rule: a day is not published complete for an admission's owner while an admission of that owner admitted on or before that day is unsettled (box-wide while Part C2's owner scope is not in use); it is a query over `admissions`, writes nothing, and lifts at settlement. An *uncertainty* is a row (`uncertainties`: `id`, `fleet_id`, `from`, `to` null while open, `reason`) written when a fleet's authority is found changed (by the witness, by `Recheck`, by a changed file), in the transaction that also writes the holds for the days of `[from, now]` for every owner the uncertainty reaches: `from = min(the registry proof, the last clean look's start, the earliest admitted_at of the fleet's admissions then unsettled) - margin`. If discovery finds no admission of the fleet unsettled, the uncertainty is opened and closed in that transaction (`to = discovery + margin`). While an uncertainty is open, settlements of that fleet's admissions extend it: each settlement, in its own transaction, writes the holds for every day of `[its admitted_at, its settled_at]` for every owner the uncertainty reaches, and the last settlement closes it with `to = max(discovery, that settlement) + margin` and materialises every day of the closed interval, the margin's extra day across midnight included; recovery uses the same helper and advances the publication watermark past the days it materialised. An admission that settles while no uncertainty of its fleet is open writes nothing permanent: the ordinary day of an unchanged fleet becomes complete when its barrier lifts. The multiple-midnights sequence (admitted on the 2nd, the change at 23:58:10, sent on the 3rd, finished on the 4th) holds the 2nd, 3rd and 4th through the settlement's interval, whichever participant finished last; the request completed before the change was noticed is inside the lower bound as today. **If the discovery transaction fails** the barrier is not enough (every admission may already be settled): discovery first latches a *pending discovery* in the ledger's in-memory failure overlay (`latchFailure`, extended to carry the lower bound and the owners reached), which the publisher's snapshot already overlays and detects across a drain (today's `snapshotOnce` and `drainFailures` behaviour, extended rather than replaced: a failed drain must not stop the overlay from making the snapshot incomplete), so every day from the bound reads incomplete for those owners until the uncertainty commits; the fleet refuses every request meanwhile and retries the write. The latch carries the lower bound, the owners reached and the tokens of the admissions the discovery counted; settlements of those admissions meanwhile find no uncertainty open and write no hold, but their rows record `settled_at` as always. **The replay is one transaction:** it opens the uncertainty with the original `from`; for each counted token whose row is settled it writes the holds for `[its admitted_at, its settled_at]` for every owner reached (and for a token that was cancelled or unissued, nothing); if any counted token is still unsettled the uncertainty stays open and the usual settlement rule closes it later; otherwise it closes with `to = replay instant + margin` (which is no earlier than the discovery and every counted settlement) and materialises every day of the closed interval; the overlay clears only after that commit, so no snapshot sees neither the latch nor the row. Astra's sequence (T counted at 23:58:10 on the 2nd, the write failing, T settling at 00:02 on the 4th, the replay at 00:03) writes the 2nd, 3rd and 4th through T's interval and closes at 00:04 on the 4th, and a successor constructed after the replay backfills that closed interval for its owners; the fleet cannot be marked `closed` while the discovery is uncommitted (above); a process that dies with the latch held leaves a fleet not `closed`, which recovery covers from the fleet's `opened_at`. |
| **Who an uncertainty reaches, and what a fleet inherits.** Reach is computed over the chain, not stored per uncertainty: every fleet row names its predecessor, and a fleet's *unacknowledged ancestry* is the set of ancestors reached by following predecessors through fleets that have not been acknowledged (an acknowledged step ends the walk, because acknowledgement copies). An uncertainty, whenever it is opened, extended or closed, writes holds for the owners of its fleet and of every fleet whose unacknowledged ancestry contains that fleet (a query at write time, so an uncertainty discovered after a successor's construction reaches it, and so does one on a grandparent when the parent closed unopened without acknowledging); and **construction backfills**: the transaction that writes a new fleet's `open` row also writes, for the new fleet's owners, the holds of every interval already held by any uncertainty of its unacknowledged ancestry, open or closed (a successor built at 23:58:20 against a fleet whose uncertainty closed at 23:58:10 holds the 2nd for its owners from its first instant, before any acknowledgement), and the publisher's pending-discovery overlay reaches a fleet's owners through the same ancestry query; the barrier for an owner consults the unsettled admissions of its fleet and of its whole unacknowledged ancestry. Acknowledgement is terminal and **root-first**: a fleet may acknowledge its predecessor only when the predecessor is `closed` and has itself acknowledged its own predecessor (or has none), and it copies the predecessor's effective set (own and inherited, open and closed) into its inherited set (`inherited`: `fleet_id`, `uncertainty_id`) and writes holds for the closed ones its owners have not been held for, in one transaction; after that the chain walk stops there and the copy carries on. Astra's sequence: A has U open and an admission unsettled; B is built (its ancestry: A) and closes unopened without acknowledging; C is built against B (its ancestry: B, then A, since B has not acknowledged); on the 4th A's settlement extends U and the write reaches X, B's owners and C's owners through the ancestry; when A closes, B acknowledges A (root-first), then C acknowledges B and inherits U. Under owner scope the October sequence: Y, built at 23:58:20 with A in its ancestry, is barred on the 3rd by A's unsettled admission and held on the 4th by the settlement's write. |
| @@ -60,7 +60,7 @@ The VFS is the plan's first spike and it must prove, before anything else of Par |
| **Replication authority.** Separately from the owner, each identity has at most one replication authority: the external replicator (the box today), the activator's hibernation path (which replicates a hibernating exec route's database and holds its own stream), or this fleet. The fleet replicates an identity only when no other authority has it: a hibernating exec route whose database resolves to the same identity as a live unit tree's listed file is a conflict of authorities, refused for this fleet with both named, exactly as today's live/hibernating conflict is, until an explicit handover (a route's hibernation path releasing the database to the fleet) exists, which is not designed here. Deduplicating two spellings of one identity within the fleet is allowed only when both claimants are routes this fleet would replicate. |
| -**Lineage.** A replica key today is `(account, path)`, and an inode number is an observation, not a database: a deleted and recreated file can reuse it across a restart. The lineage is therefore a durable record in the shadow state (platform-owned; one record per replica key, written with the shadow state's own fsync-and-rename discipline before the database is opened for replication, so no replica write precedes its lineage): `lineage_id` (random), `owner`, the accepted spelling, `(dev, ino)` and the file's birth time as `statx` reports it with `STATX_BTIME` set. Continuity means the same filesystem incarnation of the file, nothing more: within one fleet's life another accepted spelling of the same `(dev, ino)` reuses the key (one stream, one file); across a restart a key is resumed only when `(dev, ino)` and a valid birth time match the record; a filesystem that reports no valid birth time, or a mismatch, rotates to a new lineage (a new prefix) after the hold the change causes. A database replaced in place over the same incarnation (a restore written into the same inode) is not distinguished by this record and is not claimed to be; it is the owner's own replacement and litestream's generation handling continues the stream. No SQLite header field identifies a database incarnation and none is used. Existing path-based prefixes are adopted as the first lineage of the file found under that path at the first build after this design, recorded then; a later mismatch rotates as above. |
| +**Lineage.** A replica key today is `(account, path)`, and an inode number is an observation, not a database: a deleted and recreated file can reuse it across a restart. The lineage is therefore a durable record in the shadow state (platform-owned; one record per replica key, written with a protocol this design states, since the shadow state has none to borrow (the fork's LTX writes sync the file and rename it without syncing the directory, and the shadow directory's creation syncs nothing): the record is written to a temporary name in the shadow directory, `fsync`ed, renamed over its final name, the directory `fsync`ed, and every directory created for it `fsync`ed in its parent at creation; replication authority for the key is granted only after the directory's `fsync` returns, so no replica write precedes a lineage whose name and ancestry are durable; the migration that adopts existing prefixes is marked by a record written the same way, and once that marker exists a key whose replica data exists with no record or a corrupt record is a fault, not a first build: its replication authority is withheld and an operator review raised, never re-adopted): `lineage_id` (random), `owner`, the accepted spelling, `(dev, ino)` and the file's birth time as `statx` reports it with `STATX_BTIME` set. Continuity means the same filesystem incarnation of the file, nothing more: within one fleet's life another accepted spelling of the same `(dev, ino)` reuses the key (one stream, one file); across a restart a key is resumed only when `(dev, ino)` and a valid birth time match the record; a filesystem that reports no valid birth time, or a mismatch, rotates to a new lineage (a new prefix) after the hold the change causes. A database replaced in place over the same incarnation (a restore written into the same inode) is not distinguished by this record and is not claimed to be; it is the owner's own replacement; what applies to it is the fork's existing behaviour when a WAL salt or LTX position no longer matches (a new snapshot), and no other in-place restore behaviour is claimed supported. No SQLite header field identifies a database incarnation and none is used. Existing path-based prefixes are adopted as the first lineage of the file found under that path at the first build after this design, recorded then; a later mismatch rotates as above. |
| **Required coverage.** The platform's required databases are a list: the registry this process has open, the console database when the agent's role configuration says the box is a console box, `CONSOLE_DB` when set. A required database unreachable through its root is a problem that makes the inventory incomplete and is written as a durable box-wide hold for the day before and independent of whether a fleet is built. Before any cutover, a read-only operator command lists this inventory beside the external replicator's configuration. |
| @@ -89,7 +89,7 @@ The VFS is the plan's first spike and it must prove, before anything else of Par |
| - **Admission before send.** The middleware order makes an admission-less dispatch impossible: a fake gate that refuses sees no request on the wire; a gate whose commit is held past the budget refuses, the request is not sent, and the late commit's row is marked unissued. (A `Start`-less send remains possible, as today, and is not claimed otherwise.) |
| - **Part A.** A platform ancestry with a tenant-owned, group-writable or linked component refused; a directory swapped between `List` and `Walk` changes nothing; a listed subdirectory replaced by a FIFO fails `Child` with `ENOTDIR` and nothing blocks; a `..` past the root, and a `..` after a named component, refused; a retained child moved under another parent is a changed edge at `Recheck`; `List` twice on one `Dir` gives the same entries; `Child(name, expect)` refuses when the entry changed between the listing and the acquisition; each link class classified as written and none silent. |
| - **Part B.** The spike's own tests, then the fork's suite under the roots in the helper process; a WAL replaced by a link or a FIFO refused; a database replaced within its directory refused at reopen; locks honoured against an ordinary connection in another process, and the same-process arrangement shown unsafe; a helper that dies seen as a change; the SQLite identity asserted by a write through the connection, a checkpoint, and a read through `db.f`. |
| -- **Part C, as a model with the transition table and then against the ledger.** A settled admission on an unchanged fleet writes no permanent hold and its day becomes complete; a change with an unsettled admission opens an uncertainty from the earlier bound; the three-midnight interval in both completion orders (transport last; recorder last, including `abandoned` first and `unrecorded`); discovery with every admission already settled closes at once and materialises the margin's extra day across midnight; a discovery whose write fails with no admission pending bars the day through the pending-discovery latch until the retry commits; a successor linked at construction barred by the predecessor's unsettled admission and held by its late extension; A opens and closes U, healthy B acknowledges A, healthy C acknowledges B, and C's inherited set holds U; the fleet rows across build, stale, closing with a late settlement, closed, and acknowledgement, with a kill at each transition and the ledger reopened by a second process, orphans classified with all three timestamps and later days completing; the fence: a process that lost the ledger's lock but not the fence cannot be recovered over, and one without the fence cannot admit; a `closed` healthy chain contributing nothing. |
| +- **Part C, as a model with the transition table and then against the ledger.** A settled admission on an unchanged fleet writes no permanent hold and its day becomes complete; a change with an unsettled admission opens an uncertainty from the earlier bound; the three-midnight interval in both completion orders (transport last; recorder last, including `abandoned` first and `unrecorded`); discovery with every admission already settled closes at once and materialises the margin's extra day across midnight; a discovery whose write fails with no admission pending bars the day through the pending-discovery latch until the retry commits; a successor linked at construction barred by the predecessor's unsettled admission and held by its late extension; A opens and closes U, healthy B acknowledges A, healthy C acknowledges B, and C's inherited set holds U; the fleet rows across build, stale, closing with a late settlement, closed, and acknowledgement, with a kill at each transition and the ledger reopened by a second process, orphans classified with all three timestamps and later days completing; the fence: a process that lost the ledger's lock but not the fence cannot be recovered over, and one without the fence cannot admit; a `closed` healthy chain contributing nothing; a discovery whose write fails with counted admissions that all settle before the replay (the replayed interval covers their settlements, a successor constructed after the replay inherits it, and no snapshot sees neither latch nor row); a commit that fails definitely, one that is ambiguous and read back both ways, and a recheck refusal after commit, each leaving the pending set only with a durable terminal and each closing an uncertainty that waited only on it; a helper dying before its recorder registered (`unrecorded` after proven exit), a `Start` racing the exit (refused by the closed guard or registered and then abandoned), and a death with a live registered worker (kept until it ends); the lineage record's protocol with a kill after each of its five steps, and a missing record after the migration marker withholding authority. |
| - **Part D.** The identity cases; the authority conflict (live tree and hibernating exec route on one identity); the lineage record (a file deleted and recreated with a reused inode across a restart gets a new prefix; the same file resumes; an existing path-based prefix adopted once); a required database missing writing the hold without a fleet; a claim refused while the route's invocation is unknown or stale; a sleeping route with no binding, or whose private spelling resolves to another identity, refused; an observation that is not coherent (the pid changing between the two reads) leaving the route unknown. |
| - **Mutants** in a tree copy with the killing test named, per part, after the assertions above exist. |
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 d83df7104..6cb610854 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 |
| @@ -5,7 +5,7 @@ Read-only findings (no code written or run), 2026-10-03 06:45, revised 07:25 for |
| ## The descriptor-rooted VFS on `modernc.org/sqlite` v1.55.0 |
| - 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), written by us with per-inode bookkeeping across the helper's own connections (shared-lock counts, local exclusion, deferred descriptor close), since the Unix VFS's versions open the SHM and check moved files by pathname and cannot be delegated to. |
| +- 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` as `fstatat(dirfd, name, AT_SYMLINK_NOFOLLOW)` (a link reads as absent) and `xDelete` as `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), written by us with per-inode bookkeeping across the helper's own connections (shared-lock counts, local exclusion, deferred descriptor close), since the Unix VFS's versions open the SHM and check moved files by pathname and cannot be delegated to. |
| - 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). |