fix(startup): a database that opens but cannot be written names its remedy (#478)
A database that opens but cannot be written now names its remedy.
`openStorage()` guarded opening the database and nothing else. The first WRITE —
`failStaleSessions()`, clearing what a previous shutdown orphaned — sat 77 lines
past the end of the try/catch, so it threw outside every message the guard exists
to print. SQLite opens a database it cannot write without complaint and raises only
when something writes, so a root-owned `iris.sqlite` bind-mounted into a container
that has dropped to an unprivileged uid got past `mkdirSync`, past `accessSync`,
past `new Store()`, and died on the first write with a bare `attempt to write a
readonly database` carrying no errno, path or uid — while this same file already
held the `sudo chown 1000:1000 …` that fixes it, three lines below the throw.
The UIC deployment paid for that eight times on 2026-09-14: the image started
running as uid 1000, the host data directory was uid 0, the container exited at
boot, and the deploy's health gate rolled it back. Eight runs, none naming
ownership.
Three rounds, all approved, each of the first two carrying real notes:
- The first write moved inside the guard; `openStorage()` returns `{ store, stale }`.
- Round 1: the WAL sidecars are probed too. This deployment runs in WAL, `-wal` and
`-shm` are separate files with their own owners, and switching an existing file to
WAL needs no write — so a root-owned set reached the same throw. Without them the
one-file `chown` the guard prints is a remedy that does not work, and the next boot
reports that the cause is not ownership at all. The three checks are spelled out
rather than filtered, because the enumeration pin has to be able to read which
paths they are.
- Round 1: the exactly-once pin counted prose, not calls.
- Round 2: the failure line dropped the SQLite condition. `e.code ?? e.message`
never reaches the message when every node:sqlite error carries the same code, so
the operator read `(ERR_SQLITE_ERROR)` and the string identifying the failure
appeared nowhere. Nothing had pinned that line before; three assertions and a
vacuity guard do now.
- Round 2: the count's residual — trailing and block comments.
- Round 3: nothing new.
Every pin was mutation-checked, because none of them adds a test case and the count
stayed at 1708 throughout.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>