fleetd #206: read opencode sessions from opencode.db instead of the frozen JSON tree #207

Closed
agent wants to merge 0 commits from worker/cb206-opencode-sqlite-128718-2 into main
Member

Fixes #206.

opencode migrated its session store to SQLite (opencode.db) in January 2026. OpenCodeSessionDiscovery still scanned the old JSON tree under storage/session//ses_*.json, which stopped being written, so it returned null for every member forever and fleet_spawn{resumeSessionId} was unreachable for opencode profiles.

Changes

  • Add org.xerial:sqlite-jdbc 3.53.4.0 to fleetd/pom.xml. Only known CVE (CVE-2023-32697) is fixed in 3.41.2.2, well below this version; no open OSV.dev advisory against 3.53.4.0. Documented in the pom's dependency-security comment block.
  • Rewrite OpenCodeSessionDiscovery to query opencode.db read-only (SQLiteConfig.setReadOnly, PreparedStatement, no string-built SQL): SELECT id FROM session WHERE directory = ? ORDER BY time_updated DESC LIMIT 1. Never creates/writes the file. Contract unchanged: never throws, missing db / no match / corrupt db all return null.
  • Added logging for the silence that let this go unnoticed: WARN once per instance when opencode.db is missing entirely (layout moved again); DEBUG when it exists but no row matches yet (normal right after a spawn). No session title/metadata/content is ever logged.
  • Replaced the JSON-fixture unit tests with a synthetic SQLite fixture (exact-match, newest-time_updated-wins, no-match, missing db, empty db, corrupt file) and updated OpenCodeLauncherTest's shared writeRecord helper call to the new signature.

Verification

  • mvn clean install in fleetd/: BUILD SUCCESS, Tests run: 1050, Failures: 0, Errors: 0, Skipped: 0.
  • Read-only probe (throwaway main, deleted after use) against the real ~/.local/share/opencode/opencode.db returned the correct session id for a live worktree directory, cross-checked against a raw read-only sqlite3 query. Confirmed the main .db file's mtime was unchanged afterward.

Out of scope (per ticket): #175 / PR #203, switching to opencode's headless HTTP server, backfilling old JSON records.

Fixes #206. opencode migrated its session store to SQLite (opencode.db) in January 2026. OpenCodeSessionDiscovery still scanned the old JSON tree under storage/session/<projectID>/ses_*.json, which stopped being written, so it returned null for every member forever and fleet_spawn{resumeSessionId} was unreachable for opencode profiles. **Changes** - Add org.xerial:sqlite-jdbc 3.53.4.0 to fleetd/pom.xml. Only known CVE (CVE-2023-32697) is fixed in 3.41.2.2, well below this version; no open OSV.dev advisory against 3.53.4.0. Documented in the pom's dependency-security comment block. - Rewrite OpenCodeSessionDiscovery to query opencode.db read-only (SQLiteConfig.setReadOnly, PreparedStatement, no string-built SQL): SELECT id FROM session WHERE directory = ? ORDER BY time_updated DESC LIMIT 1. Never creates/writes the file. Contract unchanged: never throws, missing db / no match / corrupt db all return null. - Added logging for the silence that let this go unnoticed: WARN once per instance when opencode.db is missing entirely (layout moved again); DEBUG when it exists but no row matches yet (normal right after a spawn). No session title/metadata/content is ever logged. - Replaced the JSON-fixture unit tests with a synthetic SQLite fixture (exact-match, newest-time_updated-wins, no-match, missing db, empty db, corrupt file) and updated OpenCodeLauncherTest's shared writeRecord helper call to the new signature. **Verification** - `mvn clean install` in fleetd/: BUILD SUCCESS, Tests run: 1050, Failures: 0, Errors: 0, Skipped: 0. - Read-only probe (throwaway main, deleted after use) against the real ~/.local/share/opencode/opencode.db returned the correct session id for a live worktree directory, cross-checked against a raw read-only sqlite3 query. Confirmed the main .db file's mtime was unchanged afterward. **Out of scope (per ticket)**: #175 / PR #203, switching to opencode's headless HTTP server, backfilling old JSON records.
agent added 1 commit 2026-08-31 10:53:00 +02:00
fleetd #206: read opencode session ids from opencode.db (SQLite), not the frozen JSON tree
CI / contract (pull_request) Successful in 1m5s
CI / build (pull_request) Successful in 1m44s
3743789e8d
opencode migrated its session store to SQLite in January 2026; the JSON tree under
storage/session/<projectID>/ses_*.json stopped being written, so OpenCodeSessionDiscovery
returned null for every member forever, and fleet_spawn{resumeSessionId} was unreachable.

- Add org.xerial:sqlite-jdbc 3.53.4.0, opened read-only (SQLiteConfig.setReadOnly), so it
  never disturbs a live opencode process writing the WAL-mode database.
- Rewrite sessionIdForDirectory to run a parameterized SELECT ... WHERE directory = ?
  ORDER BY time_updated DESC LIMIT 1 against the session table. Still never throws: a
  missing database, a locked/corrupt one, or no matching row all return null.
- Log the silence that let this go unnoticed: WARN once per instance when opencode.db
  itself is missing (the layout moved again), DEBUG when it exists but no row matches
  yet (the normal interim answer right after a spawn).
- Replace the JSON-fixture tests with a synthetic-SQLite-db fixture; delete the tests
  that only proved the old JSON scan worked.
ltms added 1 commit 2026-08-31 16:46:15 +02:00
#206: pin the read-only open with a test that actually fails without it
CI / build (pull_request) Successful in 1m18s
CI / contract (pull_request) Successful in 1m44s
a052975420
setReadOnly(true) is the whole thing keeping fleetd out of the operator's
live 841MB opencode.db, and no test failed when it was removed.

The obvious test does not work. Making the database file unwritable and
checking the read still succeeds passes either way, because SQLite silently
downgrades a read-write open of an unwritable file to read-only. I wrote that
test, watched it pass with the flag removed, and threw it away.

What works: extract a package-private openReadOnly(), then ask that connection
to INSERT and require the refusal. Watched red with the flag removed, green
with it restored.

Also switches the test's INSERT helper to a PreparedStatement -- hand-escaped
SQL in a test is a pattern that gets copied into main code.
Owner

Merged to main manually as e18ad47 (head a052975), integrated with #208 into a55079a. Full suite on main: 1061 tests, 0 failures. Daemon redeployed onto that jar (9ec5fab0f136).

Two changes were made on top of this branch before merging:

  • Extracted a package-private Connection openReadOnly() so the read-only property is testable. The first version of theDatabaseIsOpenedReadOnly made the fixture file unwritable and asserted the read still worked — that passed with config.setReadOnly(true) removed, because SQLite silently downgrades a read-write open of an unwritable file to read-only. The test now asks the connection to INSERT and requires the refusal, and was watched failing without the flag.
  • writeRecord now binds parameters instead of hand-escaping SQL.

Closing as merged. Note that the discovery this PR fixes still does not reach the roster — see #209.

Merged to `main` manually as `e18ad47` (head `a052975`), integrated with #208 into `a55079a`. Full suite on main: 1061 tests, 0 failures. Daemon redeployed onto that jar (`9ec5fab0f136`). Two changes were made on top of this branch before merging: - Extracted a package-private `Connection openReadOnly()` so the read-only property is testable. The first version of `theDatabaseIsOpenedReadOnly` made the fixture file unwritable and asserted the read still worked — that passed with `config.setReadOnly(true)` removed, because SQLite silently downgrades a read-write open of an unwritable file to read-only. The test now asks the connection to `INSERT` and requires the refusal, and was watched failing without the flag. - `writeRecord` now binds parameters instead of hand-escaping SQL. Closing as merged. Note that the discovery this PR fixes still does not reach the roster — see #209.
ltms closed this pull request 2026-08-31 16:58:34 +02:00
Some checks are pending
CI / build (pull_request) Successful in 1m18s
CI / contract (pull_request) Successful in 1m44s

Pull request closed

Sign in to join this conversation.