sqlite: document lifecycle and concurrency behaviour

User asked what happens if the connection is never closed and whether
concurrent calls are safe. Adds a Lifecycle and concurrency section
to README.md covering:

- not closing only leaks fds; data is durable on commit
- 3-tier concurrency model: same service / multi-CLI same service /
  multi-process, with the actual busy_timeout default (5s) and what
  to do if a longer wait is needed (run PRAGMA busy_timeout via
  sqlite_exec)
- :memory: semantics (gone on close, even within one process)
- NFS/SMB caveat

No code changes; current defaults (busy_timeout 5s, synchronous
FULL, locking normal) are sensible and the existing test sweep
already verified end-to-end.
This commit is contained in:
opencode-agent
2026-10-08 17:20:21 +08:00
parent 9aef200ac9
commit f873990e72
+57
View File
@@ -267,3 +267,60 @@ client logs; plugin loading shows up in the service log at
- The plugin keeps connections in memory. They are closed on OpenCode - The plugin keeps connections in memory. They are closed on OpenCode
shutdown. The cleanup hook also runs on plugin reload, so editing the shutdown. The cleanup hook also runs on plugin reload, so editing the
file in place does not leak file descriptors. file in place does not leak file descriptors.
## Lifecycle and concurrency
**What happens if I never call `sqlite_close`?**
Data is safe. `better-sqlite3` auto-commits every transaction and the
default `synchronous=FULL` setting fsyncs on commit, so a successful
`sqlite_exec` return means the bytes are on disk. The only cost is a
leaked file descriptor: each open connection holds one fd on the
database file (plus, in WAL mode, one each on the `-wal` and `-shm`
sidecar files). Connections are released when:
- the model calls `sqlite_close`,
- the plugin is reloaded (edit `index.ts`, save),
- the OpenCode service is restarted.
For a one-off task it is fine to skip `sqlite_close` and let the next
service restart clean up. For a long-lived service that opens many
aliases, call `sqlite_close` to keep the fd count predictable.
**`":memory:"` databases** are gone the moment the last handle closes,
even within the same process. The two `sqlite_open(":memory:")` calls
above are *not* the same database.
**Can I call the tools concurrently?**
There are three levels to think about:
1. *Within one OpenCode service process* — Node is single-threaded and
`better-sqlite3` is a synchronous API, so even if the model fires
several tool calls in parallel, the service processes them one at a
time. The database itself never sees a real concurrent call from
here, so you do not need locks.
2. *Multiple OpenCode CLI / TUI sessions against the same service* —
the service is a single process, so the same single-threaded rule
applies.
3. *Multiple independent OpenCode services (or any other process)
opening the same database file* — this is real concurrency.
`better-sqlite3` opens the file with `locking_mode=normal` and the
default `busy_timeout=5000ms`, so the second writer politely waits
up to 5 seconds for the first writer's transaction to commit. If
the wait expires, the call returns `SQLITE_BUSY` and the model
should retry. There is no built-in knob on this plugin to raise
the timeout; if you need a longer wait, run
`PRAGMA busy_timeout = 30000;` once after `sqlite_open` (use
`sqlite_exec`).
4. *Network filesystems (NFS, SMB, sshfs)* — SQLite's locking is
unreliable on them and `SQLITE_BUSY` is the best case. Copy the
database to local disk first, or run a real database server.
**Read-only mode (`mode: "ro"`)** opens the file normally and rejects
writes inside the plugin. The journal mode is left at the default
(`delete` for files, `memory` for `:memory:`), so concurrent reads
work as expected.