diff --git a/plugins/sqlite/README.md b/plugins/sqlite/README.md index a32f032..3306c75 100644 --- a/plugins/sqlite/README.md +++ b/plugins/sqlite/README.md @@ -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 shutdown. The cleanup hook also runs on plugin reload, so editing the 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.