Skip to content

db migration: v1→v2 leaves pre-upgrade sessions invisible in session list (project_id/path not remapped) #55381

Closed as duplicate

Description

@Maveric79

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.jsonc contains only an ollama provider entry, no plugins and no project/worktree overrides.

Reproduction

  1. 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'.
  2. Upgrade to opencode 2.0.x (this triggers the migration.v1-v2 migration, which copies the v1 session rows into session_v2).
  3. Run opencode 2.0.x in the same directory and open the session list via ctrl+p or /sessions.
  4. 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_v2 returns all sessions, and session_message / part rows are intact for every one of them.
  • opencode session list (CLI) lists all of them correctly.
  • GET /api/session without a subpath query 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 upgrade

Database 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:

  1. project_id is not remapped. v1 sessions from a non-git directory are stored under the global project (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.
  2. path semantics changed and were not recomputed. In v1 the migrated rows carry path = 'Users/<user>' — a path relative to the global project's root /, with no leading slash. In v2 the same session would be path = '' 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 list and opencode -s <session-id> both work and can open the sessions even while the picker is empty, because neither path applies the subpath filter.
  • Possible fix locations: the v1-v2 migration should resolve each session's project_id from session.directory and recompute path as the path relative to that project's worktree. Independently, the list query would be more robust if subpath === "" matched both '' and NULL, or if path were normalized to a single representation on write.

Activity

  1. opencode-agent commented on Oct 14, 2026

    @opencode-agent
    Contributor

    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 /sessions list hides them. The path half you describe (NULL, or a path relative to /, where v2 expects '') has been reported as well, and the exact-match path filter is still in packages/core/src/session/store.ts on dev.

    Possible duplicates and closely related issues, ordered from most to least likely:

    Open 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 UPDATE workaround (after backing up opencode.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.

  2. opencode-agent commented on Oct 15, 2026

    @opencode-agent
    Contributor

    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 a path that hasn't been updated for v2, so the folder-scoped /sessions list 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 UPDATE workaround (back up opencode.db first), 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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions