Repository navigation
Description
Description
Sessions with path = NULL are missing from project session lists after the v1→v2 upgrade, even though their project_id is correct.
v1 began with no path column; 20260428004200_add_session_path added it (default NULL), and v1's inserts still left it NULL for root-level sessions for a while (in our DB: a clean NULL window 2026-04-03→05-05, with every later session writing ''). The v1→v2 importer copies path verbatim, so these rows arrive in session_v2 with path IS NULL. The project-scoped picker sends {project, subpath: ''} and SessionStore.list filters with an exact eq(SessionTable.path, input.subpath) (packages/core/src/session/store.ts:108) — in SQL, NULL = '' is not true, so the rows are excluded. The projector only rewrites the path of the session that triggered a Resolved event; historical rows are never backfilled.
Scope in our DB (opencode 2.0.14, upgraded from 1.18.x): 90 sessions — 88 in one git repo (87 top-level, mostly user-initiated forks, ~1,300 messages total) + 2 in a second repo. All have directory = project.worktree, so the correct value is ''. They are only reachable via "all projects" scope / global search.
This is a different defect from #50980 (where the row is absent from session_v2 entirely) and from the project_id='global' family (#50551, #50800, #50999): here the project attribution is right, only path is null.
Plugins
None relevant (server-side data/filter behavior).
OpenCode version
2.0.14 (affected, by source comparison: 2.0.14–2.0.16; not fixed on latest tag)
Steps to reproduce
- In opencode v1, create sessions in a git repo root during the window where v1 didn't write
path(around the20260428004200_add_session_pathchange — e.g. after thepathcolumn exists but before v1 started writing''). - Upgrade to v2.0.14–2.0.16 and open that repo. The import copies
path = NULLintosession_v2. - List sessions in the project: the NULL-path sessions don't appear, though "all projects" scope shows them.
Check your own DB:
SELECT project_id, directory, COUNT(*) FROM session_v2 WHERE path IS NULL GROUP BY 1, 2;Expected Behavior
Sessions whose directory equals the project worktree should be listed at subpath = '', i.e. NULL should be treated as the empty path (the dev branch already adds this: and(isNull(path), eq(directory, …)) at packages/opencode/src/session/session.ts:970-976).
Related issues
- Upgrade to 1.17.x hides pre-existing sessions — new path column not back-filled during migration #35750 — v1-era: new
pathcolumn not back-filled during migration (origin of these NULLs) - migrate: V1-era session exists only in legacy table, invisible to V2 clients #50980 — sister symptom: v1-era session missing from
session_v2entirely - V1→V2 importer keeps project_id='global' for non-git directories, so migrated sessions never appear in the per-directory session picker #50551 — the
project_id='global'family (different root cause, same picker filter)
Operating System
Arch Linux, x86_64
Terminal
kitty
Activity
5h4d0wn1k commentedon Sep 26, 2026
Thanks for this — the
eq(SessionTable.path, input.subpath)trace atstore.ts:108and theNULL = ''SQL point both check out on my install.One correction to the scope, from a much larger dataset (opencode 2.0.18, upgraded from 1.18.x; 4,364 rows in
session_v2across 43 projects):The
NULL = ''case is real, but it's the smaller half.eq(path, '')also excludes every row whose path is a real subdirectory, because'sub/dir' != ''. Measured over top-level sessions insession_v2:pathstatepath = ''path IS NULLpath = '<subdir>'So 231 of 247 top-level sessions are unreachable through the picker, and only 34 of those are the
NULLcase. About 85% of the hidden rows are hidden simply because the filter demands''while their path is an actual directory.I'd argue this is one defect rather than two. The client sends
subpath: ''unconditionally —packages/tui/src/component/dialog-session-list.tsx:70onv2:path.relative()returns""when the active directory is the project root, which is the common case. That""then becomes an equality filter. TheNULLsemantics you describe are the same filter, just meeting rows whose path happens to beNULLinstead of rows whose path is a directory.The fix is one line, on the client, and it cures both halves at once:
A genuine subdirectory still filters, exactly as before.
One caveat on scope: the server guard should stay
!== undefined.path = ''legitimately means "a session started at the project root" —Session.createderives it frompath.relative(project.directory, location.directory), which is""when the two match — so relaxing it server-side would remove the ability to filter for root-level sessions.Two things that may be useful for reproducing:
searchto it. A session that provably exists returns nothing when you type its title.GET /api/session?limit=50&order=desc&parentID=null&project=<ID>&subpath=against the same URL with
subpathomitted.I have this on a branch with regression tests (asserting the parameter is absent at a project root and present in a subdirectory; the root case fails against unfixed
v2). Posting the measurement first since it substantially widens the reported scope — I'll open a PR againstv2after this.5h4d0wn1k commentedon Sep 26, 2026
Checked
devand you're right it's already handled there —packages/opencode/src/session/session.ts:967-977guards the filter on truthiness and adds the nested-subdirectory and NULL branches. That's more complete than the one-liner I was proposing, so I'll drop that.The gap is that
devis the 1.18.x line (1.18.32inpackages/cli/package.json, which is the current release), while 2.0.x isv2(2.0.18there). Onv2the equivalent code ispackages/core/src/session/store.ts:108, still the unguardedeq(path, input.subpath), with the TUI sendingsubpath:unconditionally atpackages/tui/src/component/dialog-session-list.tsx:70. The numbers in my earlier comment are measured on 2.0.18, not on 1.18.x.The two lines have diverged in shape as well as state —
devrenamedsubpathtopathand added ascopefield — so this isn't a straight cherry-pick. The semantics do port over though:or,isNullandlikeare all already imported in v2'sstore.ts, so the nested and NULL branches can be carried across behind the same truthiness guard.Is a backport to
v2wanted, or is the 2.0.x line picking these up another way? Happy to prep it either way.Correction to my own PR (#51514)
My original reasoning was that
devdoesn't filter onsubpathat all. That was wrong — I had readpackages/core/src/session/store.tsondev, which is the v2 code path and not the one the dev TUI calls.devdoes filter, and does so correctly.5h4d0wn1k commentedon Sep 26, 2026
Correction to my own earlier comments on this issue, now that the PR (#51514) is up — it does the
opposite of what I recommended here, so the thread shouldn't be read without this.
!== undefinedto keep root-only filtering. My reasoning wasright about the mechanism but I had the priority backwards: I could not find any caller of that
filter in the
v2tree that needs root-only — the only one is the session picker, which wants thewhole project at a project root. So the guard is relaxed. The trade is real and I should have said
it earlier:
subpath=''no longer selects root-level sessions only.dev's nested andNULLbranches could be carried across behind the same truthiness guard.They port mechanically, but the
NULLbranch is the problem:devconstrains it witheq(directory, …)and a project-scopedv2query has no such conjunct, whileNULLmeans "thiscolumn was never populated" (bare
ALTER TABLEwith no backfill) rather than "at the project root"— 2 of the 34 top-level
NULLrows on my install are below their root. So the PR ports only thetruthiness guard.
The measurement tables in my first comment still stand.
opencodejr commentedon Oct 28, 2026
Thanks for the detailed report. I can confirm this on 2.0.14, and it still happens on the latest
dev(9adc42b).Steps I used (the script below does all of this):
opencode servein a fresh git repo and create two sessions at the repo root.path = NULLon one of them insession_v2. This matches the rows the v1 importer leaves behind:project_idanddirectorystay correct.GET /api/session?project=<id>returns both sessions.GET /api/session?project=<id>&subpath=(what the project picker sends) returns only the session withpath = ''. The NULL-path one is missing.As you found, the subpath filter in
SessionStore.listmatchespathexactly, soNULLnever equals''. Nothing back-fills the NULL rows either. Until this is fixed, these sessions can still be reached through the "all projects" view and global search.Related: #35750 (origin of the NULL
pathvalues), #54009 (add_session_path-era migration failure), #50980 and #59517 (sessions missing fromsession_v2entirely), #50551 and #58846 (theproject_id='global'family).Reproduction script (repro.sh)
Run it:
bash repro.sh