node:sqlite enables SQLITE_OPEN_URI from Node 22.15 on. Below that -- 22.13 is
the package floor -- a `file:...` location is a literal filename, so the
immutable open failed as CANTOPEN and the copy quietly stood in for it. That
was the right outcome by accident; the test asserted the newer behaviour and
failed on the floor.
The support question is now asked once per process, with an in-memory URI that
touches no filesystem whichever answer comes back, and the immutable open is
attempted only when the answer is yes. The test asks the same question rather
than skipping, so both CI lines assert something: rows are correct either way,
in place where URI filenames work and from a copy where they do not.
Four things the copy fallback got wrong.
A database whose -wal is absent or empty has no un-checkpointed frames, so
there is nothing to go stale and nothing worth copying: immutable=1 opens the
source in place and SQLite skips the -shm it cannot create. The copy is now
taken only when a non-empty -wal exists, which is the case where dropping it
would lose rows.
A copy is published under a name carrying its fingerprint, so refreshing one
never has to unlink a file another process may still hold open, which Windows
does not allow. The -wal is published before the database so a reader can
never see the database without the sidecar holding its newest rows, and losing
a publish race to an identical copy is not an error. That removes the metadata
sidecar: the name is the fingerprint.
Superseded copies are evicted rather than overwritten -- the one in use plus at
most one predecessor, and anything untouched for a day, which is also what a
source path that no longer exists looks like. Reuse touches the copy, so its
mtime is last use.
A cache directory that cannot be written no longer fails the same way the bug
did. It emits the once-per-database notice naming the database and the reason
before the database is skipped, instead of going quiet.
A read-only parent with a -wal but no -shm fails as SQLITE_CANTOPEN (14),
not SQLITE_READONLY (8), so the fallback never ran and the un-checkpointed
rows in the -wal stayed invisible. openReadonlyCache already re-throws the
original error when the database itself is missing, which is the other
CANTOPEN, so widening the trigger keeps that case distinguishable.
Also stops copying the source -shm: SQLite rebuilds the wal-index from the
-wal in the writable cache directory, so the copy is dead weight.
`openDatabase` opens provider databases with `readOnly: true`, which is not
enough for a WAL-mode database: SQLite has to create `<db>-shm` and an empty
`<db>-wal` in the database's own directory unless they already exist. Two
things follow, and both are real.
The open writes. On a writable directory it succeeds and leaves two files
behind in the user's provider directory, which is not what "CodeBurn only reads
your session logs" implies.
On a non-writable parent it fails outright with "attempt to write a readonly
database". It is conditional on -shm being absent, which is exactly the state
after the tool exits cleanly, so the symptom is intermittent: a provider's
spend disappears whenever that tool is not running. Both discovery sites
swallowed it with a bare `catch { continue }`, so the provider reported zero
sessions with nothing on stderr - indistinguishable from the tool not being
installed. It covers cursor, cursor-agent, opencode, goose, warp, kilo-code,
zerostack and the copilot agent-traces DB.
The direct open stays the fast path and is unchanged when it succeeds: no stat,
no permission probe. Only when SQLite reports SQLITE_READONLY does the fallback
run, copying the database and its -wal/-shm siblings into the CodeBurn cache
directory and opening the copy there. The copy is fingerprinted the same way
session-cache fingerprints a SQLite source, so an unchanged database is not
copied twice, and there is one bounded entry per source path.
The discovery sites now tell SQLITE_READONLY apart from ENOENT and emit one
notice per source path rather than per session, matching what the parse-time
paths already do.
This is not specific to any one sandbox: it applies to a database on read-only
media, a restrictive-permissions setup, and both the Flatpak and snap
confinements. The snap was narrowed to a read-only personal-files plug in #978
and is likely affected; I have no snap install to confirm that on.