opencode session discovery never finds a member's record, so agentSessionId is always null and resume silently does nothing #206

Closed
opened 2026-08-31 09:26:10 +02:00 by ltms · 2 comments
Owner

Found while reviewing PR #203 (#175). It is the reason that PR cannot work, but it is a separate, pre-existing defect and it is worth fixing on its own.

The claim

OpenCodeSessionDiscovery.sessionIdForDirectory(cwd) appears to return null for every live opencode member. If so, nothing that depends on it works, and nothing reports that.

The evidence

1. No session record exists for any recent member. OpenCodeLauncher.defaultDiscoveryRoot() is ~/.local/share/opencode. Four members ran on terra (kind: opencode) on 2026-08-31 between roughly 08:20 and 09:40. The newest session record anywhere under that root:

$ find ~/.local/share/opencode/storage -name 'ses_*.json' | xargs stat -f '%Sm %N' -t '%m-%d %H:%M' | sort -r | head -2
01-22 14:39  .../storage/session/global/ses_41b79fc90ffeI9E8uZv6VprUn2.json
01-21 16:02  .../storage/session_diff/ses_4203e488affemnho0UCn8inCZY.json

Newest is 22 January. 228 records total, none from any recent member. A home-wide search for ses_*.json found nothing newer either.

2. fleet_list never reports agentSessionId for an opencode member. The tool documents that field as present "when its backend knows one". Across every fleet_list call today, no terra member carried it — consistent with discovery returning null every time.

3. The code already expects this to be null for a while, but not forever. SessionAwareHandle.agentSessionId() says:

Lazy + retried, never a spawn-time blocker: opencode writes the session record only when the session is first persisted, so null here is the correct interim answer and the caller re-calls later

That is correct as written. The observation here is stronger: the record does not appear later either.

Why it matters

  • resumeSessionId silently does nothing for opencode. fleet_spawn{resumeSessionId} is documented to relaunch onto a prior conversation, and to be refused rather than silently starting fresh when a backend cannot do it. Since agentSessionId is never known, there is nothing to pass, so the feature is unreachable for every opencode profile — which is most of the fleet.
  • It blocks #175. Any model verification that reads the session record can never fire. See PR #203.
  • It is invisible. Nothing logs "I looked for a session record and found none". A reader of the code would reasonably assume this works.

What I did not establish

I did not determine why the record is absent. Candidates, in the order I would check them:

  1. Members write somewhere else. The launcher sets OPENCODE_CONFIG per spawn but does not appear to set a data/storage dir, so members should share the operator's root — but the evidence says they do not. Check whether opencode derives its storage root from something the member's environment changes. The CB-633 ZDOTDIR scrub rewrites the member's shell startup and could plausibly affect HOME or an XDG variable.
  2. Records are only written on session persist, and a member's session never persists. Possible if members are torn down before opencode flushes.
  3. The layout moved. The discovery javadoc already warns the layout is version-coupled and not a stable contract.

Candidate 1 is the one I would check first, because it would also explain why the operator's own opencode sessions stopped appearing in January — around when member spawning became routine.

Suggested direction

Whatever the cause, make the failure visible. A discovery that finds nothing should say so once per member, at DEBUG or INFO with the root it searched and the cwd it wanted. The current silence is what let this sit unnoticed; the same silence is the thing #175 is fundamentally about.

Found while reviewing PR #203 (#175). It is the reason that PR cannot work, but it is a separate, pre-existing defect and it is worth fixing on its own. ## The claim `OpenCodeSessionDiscovery.sessionIdForDirectory(cwd)` appears to return `null` for every live opencode member. If so, nothing that depends on it works, and nothing reports that. ## The evidence **1. No session record exists for any recent member.** `OpenCodeLauncher.defaultDiscoveryRoot()` is `~/.local/share/opencode`. Four members ran on `terra` (`kind: opencode`) on 2026-08-31 between roughly 08:20 and 09:40. The newest session record anywhere under that root: ``` $ find ~/.local/share/opencode/storage -name 'ses_*.json' | xargs stat -f '%Sm %N' -t '%m-%d %H:%M' | sort -r | head -2 01-22 14:39 .../storage/session/global/ses_41b79fc90ffeI9E8uZv6VprUn2.json 01-21 16:02 .../storage/session_diff/ses_4203e488affemnho0UCn8inCZY.json ``` Newest is **22 January**. 228 records total, none from any recent member. A home-wide search for `ses_*.json` found nothing newer either. **2. `fleet_list` never reports `agentSessionId` for an opencode member.** The tool documents that field as present "when its backend knows one". Across every `fleet_list` call today, no `terra` member carried it — consistent with discovery returning `null` every time. **3. The code already expects this to be null for a while, but not forever.** `SessionAwareHandle.agentSessionId()` says: > Lazy + retried, never a spawn-time blocker: opencode writes the session record only when the session is first persisted, so null here is the correct interim answer and the caller re-calls later That is correct as written. The observation here is stronger: the record does not appear **later** either. ## Why it matters - **`resumeSessionId` silently does nothing for opencode.** `fleet_spawn{resumeSessionId}` is documented to relaunch onto a prior conversation, and to be *refused* rather than silently starting fresh when a backend cannot do it. Since `agentSessionId` is never known, there is nothing to pass, so the feature is unreachable for every opencode profile — which is most of the fleet. - **It blocks #175.** Any model verification that reads the session record can never fire. See PR #203. - **It is invisible.** Nothing logs "I looked for a session record and found none". A reader of the code would reasonably assume this works. ## What I did not establish I did not determine **why** the record is absent. Candidates, in the order I would check them: 1. **Members write somewhere else.** The launcher sets `OPENCODE_CONFIG` per spawn but does not appear to set a data/storage dir, so members should share the operator's root — but the evidence says they do not. Check whether opencode derives its storage root from something the member's environment changes. The CB-633 `ZDOTDIR` scrub rewrites the member's shell startup and could plausibly affect `HOME` or an XDG variable. 2. **Records are only written on session persist, and a member's session never persists.** Possible if members are torn down before opencode flushes. 3. **The layout moved.** The discovery javadoc already warns the layout is version-coupled and not a stable contract. Candidate 1 is the one I would check first, because it would also explain why the operator's own opencode sessions stopped appearing in January — around when member spawning became routine. ## Suggested direction Whatever the cause, **make the failure visible**. A discovery that finds nothing should say so once per member, at DEBUG or INFO with the root it searched and the cwd it wanted. The current silence is what let this sit unnoticed; the same silence is the thing #175 is fundamentally about.
Author
Owner

Root cause found: opencode moved its session store from JSON files to SQLite

Candidate 3 was the right one — "the layout moved". Candidates 1 and 2 are ruled out.

opencode now keeps sessions in ~/.local/share/opencode/opencode.db, a SQLite database. The JSON
tree under storage/ that OpenCodeSessionDiscovery scans is a dead migration artefact.

$ ls -la ~/.local/share/opencode/
-rw-r--r--  1 dai.ha  staff  841256960 Aug 31 14:30 opencode.db
-rw-r--r--  1 dai.ha  staff      32768 Aug 31 14:30 opencode.db-shm
-rw-r--r--  1 dai.ha  staff    3184792 Aug 31 14:32 opencode.db-wal
drwxr-xr-x 10 dai.ha  staff        320 Jan  3  2026 storage        <-- frozen

The database was written today. The storage/ tree stopped in January, which is exactly the
cutoff in the original report. There is a storage/migration marker file and a data_migration
table in the database, so this was a one-way migration, not a dual write.

The data discovery needs is all there

$ sqlite3 'file:opencode.db?mode=ro' 'PRAGMA table_info(session);'
0|id|TEXT           5|directory|TEXT          26|time_created|INTEGER
4|slug|TEXT          8|version|TEXT           27|time_updated|INTEGER

id, directory and time_updated map one-to-one onto what sessionIdForDirectory already does:
match on the worker's cwd, newest wins.

$ sqlite3 'file:opencode.db?mode=ro' "SELECT count(*) FROM session;"
117
$ sqlite3 'file:opencode.db?mode=ro' "SELECT count(*) FROM session WHERE directory LIKE '%bridged-worktrees%';"
57
$ sqlite3 'file:opencode.db?mode=ro' \
    "SELECT id, directory, datetime(time_updated/1000,'unixepoch') FROM session ORDER BY time_updated DESC LIMIT 3;"
ses_fa945e505ffepqOZNfQ24cfOjs|/Users/dai.ha/LTMS/claude-bridge|2026-08-31 07:32:40
ses_fa9547c84ffeDVzjqnkEFfNGPI|/Users/dai.ha/LTMS/.bridged-worktrees/4d16e9-6|2026-08-31 07:15:54
ses_fa96271adffeVP5xOPskaAMKnZ|/Users/dai.ha/LTMS/.bridged-worktrees/166f8a-4|2026-08-31 07:06:40

57 member sessions are recorded, keyed by the exact worktree paths fleetd provisions. So the
directory-match strategy in the class javadoc was sound all along — it was reading a store that had
stopped being written.

What this changes

  • The seam holds. storageRoot stays ~/.local/share/opencode; only what the class reads inside it
    changes, from session/<projectID>/ses_*.json to opencode.db.
  • The "never throws, null means not resolved yet" contract stays exactly as it is. A locked or
    missing database is another null.
  • Reads must be read-only and must not disturb a live opencode process. The database runs in WAL
    mode, so a read-only connection is safe alongside writers.
  • Java needs a way to read SQLite. Choosing org.xerial:sqlite-jdbc over shelling out to the
    sqlite3 binary: no undeclared external-binary requirement, and it works on a headless host where
    the CLI may not be installed. It is a new dependency, so it needs the usual Mend.io CVE check on
    pom.xml.

The suggested direction still stands

Discovery finding nothing must say so. That silence is why a store that stopped being written in
January went unnoticed until August, through every one of those 57 member spawns.

Same shape elsewhere

Worth asking of anything else that reads another tool's private on-disk layout: what tells us it is
still being written? A reader that finds nothing and an empty store are indistinguishable without a
log line.

## Root cause found: opencode moved its session store from JSON files to SQLite Candidate 3 was the right one — "the layout moved". Candidates 1 and 2 are ruled out. **opencode now keeps sessions in `~/.local/share/opencode/opencode.db`, a SQLite database.** The JSON tree under `storage/` that `OpenCodeSessionDiscovery` scans is a dead migration artefact. ``` $ ls -la ~/.local/share/opencode/ -rw-r--r-- 1 dai.ha staff 841256960 Aug 31 14:30 opencode.db -rw-r--r-- 1 dai.ha staff 32768 Aug 31 14:30 opencode.db-shm -rw-r--r-- 1 dai.ha staff 3184792 Aug 31 14:32 opencode.db-wal drwxr-xr-x 10 dai.ha staff 320 Jan 3 2026 storage <-- frozen ``` The database was written **today**. The `storage/` tree stopped in January, which is exactly the cutoff in the original report. There is a `storage/migration` marker file and a `data_migration` table in the database, so this was a one-way migration, not a dual write. ### The data discovery needs is all there ``` $ sqlite3 'file:opencode.db?mode=ro' 'PRAGMA table_info(session);' 0|id|TEXT 5|directory|TEXT 26|time_created|INTEGER 4|slug|TEXT 8|version|TEXT 27|time_updated|INTEGER ``` `id`, `directory` and `time_updated` map one-to-one onto what `sessionIdForDirectory` already does: match on the worker's cwd, newest wins. ``` $ sqlite3 'file:opencode.db?mode=ro' "SELECT count(*) FROM session;" 117 $ sqlite3 'file:opencode.db?mode=ro' "SELECT count(*) FROM session WHERE directory LIKE '%bridged-worktrees%';" 57 $ sqlite3 'file:opencode.db?mode=ro' \ "SELECT id, directory, datetime(time_updated/1000,'unixepoch') FROM session ORDER BY time_updated DESC LIMIT 3;" ses_fa945e505ffepqOZNfQ24cfOjs|/Users/dai.ha/LTMS/claude-bridge|2026-08-31 07:32:40 ses_fa9547c84ffeDVzjqnkEFfNGPI|/Users/dai.ha/LTMS/.bridged-worktrees/4d16e9-6|2026-08-31 07:15:54 ses_fa96271adffeVP5xOPskaAMKnZ|/Users/dai.ha/LTMS/.bridged-worktrees/166f8a-4|2026-08-31 07:06:40 ``` **57 member sessions are recorded**, keyed by the exact worktree paths fleetd provisions. So the directory-match strategy in the class javadoc was sound all along — it was reading a store that had stopped being written. ### What this changes - The seam holds. `storageRoot` stays `~/.local/share/opencode`; only what the class reads inside it changes, from `session/<projectID>/ses_*.json` to `opencode.db`. - The "never throws, null means not resolved yet" contract stays exactly as it is. A locked or missing database is another `null`. - Reads must be **read-only** and must not disturb a live opencode process. The database runs in WAL mode, so a read-only connection is safe alongside writers. - Java needs a way to read SQLite. Choosing `org.xerial:sqlite-jdbc` over shelling out to the `sqlite3` binary: no undeclared external-binary requirement, and it works on a headless host where the CLI may not be installed. It is a new dependency, so it needs the usual Mend.io CVE check on `pom.xml`. ### The suggested direction still stands Discovery finding nothing must say so. That silence is why a store that stopped being written in January went unnoticed until August, through every one of those 57 member spawns. ### Same shape elsewhere Worth asking of anything else that reads another tool's private on-disk layout: what tells us it is still being written? A reader that finds nothing and an empty store are indistinguishable without a log line.
Author
Owner

Fixed by PR #207, merged to main in a55079a, daemon redeployed onto jar 9ec5fab0f136.

The discovery itself is proven live: an opencode member spawned on a fresh worktree produced the row

ses_fa7b15930ffeOSrh436Vln7mcn|/Users/dai.ha/LTMS/.bridged-worktrees/4bde6e-1

and OpenCodeSessionDiscovery reads it correctly.

But resumeSessionId is still unusable for opencode, for a second and independent reason: SessionManager resolves agentSessionId once at spawn — before the row exists — and freezes it, so the value never reaches the roster. Filed as #209 with the live evidence.

Closing this one; the remaining half is tracked in #209.

Fixed by PR #207, merged to `main` in `a55079a`, daemon redeployed onto jar `9ec5fab0f136`. The discovery itself is proven live: an opencode member spawned on a fresh worktree produced the row ``` ses_fa7b15930ffeOSrh436Vln7mcn|/Users/dai.ha/LTMS/.bridged-worktrees/4bde6e-1 ``` and `OpenCodeSessionDiscovery` reads it correctly. **But `resumeSessionId` is still unusable for opencode**, for a second and independent reason: `SessionManager` resolves `agentSessionId` once at spawn — before the row exists — and freezes it, so the value never reaches the roster. Filed as #209 with the live evidence. Closing this one; the remaining half is tracked in #209.
ltms closed this issue 2026-08-31 16:59:36 +02:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: fleet/fleetd#206