Repository navigation
Description
Summary
After upgrading from opencode 1.18.x to 2.0.x, every session created before the upgrade disappears from the TUI session list (Switch session / /sessions) and from the sidebar. The sessions and their messages are fully intact in the database. The v1→v2 migration does not remap two columns on the migrated sessions — project_id stays global while v2 creates a new per-directory project, and path stays NULL instead of '' — and the session list query filters on exactly those two values.
Environment
- opencode version: 2.0.29 (migrated from 1.18.27; installed via
anomalyco/tap/opencode-v2, Homebrew, release channel) - OS: Darwin 25.6.0 (macOS, x86_64)
- Terminal: TERM=xterm-256color, COLORTERM=truecolor, TERM_PROGRAM=(unset)
- Shell: /bin/zsh
- Install/channel: release (Homebrew
anomalyco/tap/opencode-v2); the upgrade was performed automatically by Homebrew ("upgraded major") - Active plugins: none found in config
Additional relevant context:
- Sessions were created in the user's home directory (
/Users/<user>), which is not a git repository. ~/.config/opencode/opencode.jsonccontains only anollamaprovider entry, no plugins and no project/worktree overrides.
Reproduction
- Install opencode 1.18.x and run it in a directory that is not a git repository (e.g. the home directory). Create a few sessions so they are persisted with
project_id = 'global'. - Upgrade to opencode 2.0.x (this triggers the
migration.v1-v2migration, which copies the v1sessionrows intosession_v2). - Run opencode 2.0.x in the same directory and open the session list via
ctrl+por/sessions. - Observe that the list contains only the session that was created after the upgrade — all pre-upgrade sessions are missing.
Expected Behavior
Sessions created before the upgrade are listed in the session picker, exactly as they were in 1.18.x.
Actual Behavior
The picker shows only the newly created session. The sessions are not lost:
select count(*) from session_v2returns all sessions, andsession_message/partrows are intact for every one of them.opencode session list(CLI) lists all of them correctly.GET /api/sessionwithout asubpathquery parameter returns all of them.
The discrepancy is caused by the path filter. v2 stores path = '' for a session located at the project root, but the migrated v1 sessions keep path = NULL. The TUI sends subpath="", which the server compiles into WHERE path = '', and NULL does not satisfy that predicate.
Observed on the running server:
GET /api/session?project=<id>&directory=<dir> -> all sessions
GET /api/session?project=<id>&directory=<dir>&subpath= -> only the new session
GET /api/session?project=<id>&subpath=null -> none
Database state before the manual fix:
sqlite> select quote(path), typeof(path), count(*) from session_v2 group by 1;
NULL | null | 12 -- migrated v1 sessions
'' | text | 1 -- session created after the upgradeDatabase state also shows the second half of the problem — the project binding was not remapped either:
sqlite> select project_id, count(*) from session_v2 group by 1;
global | 12
20941bca81651aa5a981d67b6e6f99097b509f2d | 1
sqlite> select id, worktree from project;
global | /
20941bca81651aa5a981d67b6e6f99097b509f2d | /Users/<user>GET /api/location?directory=/Users/<user> reports the project for that directory as the per-directory project (20941bca…), not global, so the migrated sessions do not match the project that v2 resolves for the location.
Additional Context
The relevant server-side filter, as shipped in the 2.0.29 bundle:
if ("directory" in e) u.push(eq(Xe.directory, e.directory));
if (e.workspaceID) u.push(eq(Xe.workspace_id, e.workspaceID));
if ("project" in e) u.push(eq(Xe.project_id, e.project));
if ("project" in e && e.subpath !== undefined) u.push(eq(Xe.path, e.subpath));The relevant TUI call site that triggers it:
await api.session.list({
...(allProjects
? {}
: project.id === "global"
? { directory }
: {
project: project.id,
subpath: path.relative(project.directory, directory), // "" at project root
}),
...(search ? { search } : {}),
limit: 50,
order: "desc",
parentID: null,
})At the project root path.relative(project.directory, directory) evaluates to "", so the filter becomes path = ''. This also explains why the CLI (opencode session list) looks healthy: that code path does not send subpath, so the filter is never applied.
Two migration problems appear to be involved:
project_idis not remapped. v1 sessions from a non-git directory are stored under theglobalproject (worktree/). v2 resolves the project for/Users/<user>to a different, per-directory project id, so the migrated sessions no longer match the location the user is in.pathsemantics changed and were not recomputed. In v1 the migrated rows carrypath = 'Users/<user>'— a path relative to theglobalproject's root/, with no leading slash. In v2 the same session would bepath = ''at the project root. The migration copies the v1 value verbatim instead of recomputing it.
A minimal manual fix that makes all sessions reappear:
UPDATE session_v2 SET project_id = '<per-directory project id>' WHERE project_id = 'global';
UPDATE session_v2 SET path = '' WHERE path IS NULL;Notes on frequency and workarounds:
- Reproducible 100% of the time on upgrade from 1.18.x; not intermittent.
- Affects every pre-upgrade session, regardless of title or size, so it reads as total data loss to the user even though nothing was deleted.
- Workaround: the SQL above.
opencode session listandopencode -s <session-id>both work and can open the sessions even while the picker is empty, because neither path applies thesubpathfilter. - Possible fix locations: the
v1-v2migration should resolve each session'sproject_idfromsession.directoryand recomputepathas the path relative to that project's worktree. Independently, the list query would be more robust ifsubpath === ""matched both''andNULL, or ifpathwere normalized to a single representation on write.
Activity
opencode-agent commentedon Oct 14, 2026
Thanks for the very thorough report and the verified SQL workaround!
I think this is a duplicate of #49827. After the v1→v2 migration, sessions that v1 created in non-git directories keep
project_id = 'global'. v2 creates a separate project for that directory, so the folder-scoped/sessionslist hides them. Thepathhalf you describe (NULL, or a path relative to/, where v2 expects'') has been reported as well, and the exact-matchpathfilter is still inpackages/core/src/session/store.tsondev.Possible duplicates and closely related issues, ordered from most to least likely:
globalsessions from non-git directories hidden after the v1→v2 upgrade (main tracking issue)project_id='global'; a comment there notes thatpathalso has to be normalizedglobalsessions stay unattributed after migration and are hidden from the session listpath = NULLare missing from project session lists because of the exactpath = ''filterOpen fix PRs to follow: #51935 (attribute migrated v1 sessions to the project of their directory), #54048 (restore legacy sessions in markerless projects) and #51514 (treat an empty subpath as no subpath). Until then, your
UPDATEworkaround (after backing upopencode.db),opencode -s <session-id>, or the "all projects" view in the picker will get you to the old sessions.The bot will close this issue as a duplicate in 1 day unless you reply explaining how your problem is different.
opencode-agent commentedon Oct 15, 2026
We haven't heard back, so I'm closing this as a duplicate of #49827. After the v1→v2 migration, sessions from non-git directories keep
project_id = 'global'and apaththat hasn't been updated for v2, so the folder-scoped/sessionslist hides them. The data itself is intact.Please follow #49827 and the open fix PRs #51935, #54048 and #51514 for updates. Until those are merged, you can use your
UPDATEworkaround (back upopencode.dbfirst),opencode -s <session-id>, or the "all projects" view in the picker to get to the old sessions. If your problem turns out to be different, reply here and we can reopen. Thanks again for the detailed diagnosis!