Skip to content

session: V1 to V2 migration hides legacy sessions from the default cwd-scoped list #54679

Closed as duplicate

Description

@Ivony

Summary

After migrating from OpenCode V1 (1.18.x) to V2 (2.0.26), all sessions that belonged to the V1 global project become invisible in the session list when OpenCode is started in the same working directory. The session list defaults to a cwd scope that filters by project + subpath, and the V1→V2 migration creates a new project for each non-git directory, so the pre-upgrade sessions (still attached to global) no longer match. The data is intact and is returned when filtering by directory instead of project.

Environment

  • opencode version: opencode v2.0.26
  • OS: Windows 11 Pro, 10.0.26300 (win32 x64)
  • Terminal: Windows Terminal (WT_SESSION set), TERM=xterm-256color
  • Shell: C:\WINDOWS\system32\cmd.exe
  • Install/channel: latest, bun global install (@opencode/cli@2.0.26)
  • Active plugins: two failing local plugins under ~/.config/opencode/plugins/ (git-ai.ts, utf8-encoding.ts) — unrelated V1-style plugin shape, reported separately

Reproduction

  1. Use OpenCode V1 (1.18.x) with a non-git working directory, e.g. the user home C:\Users\ivony. Create a number of sessions. Confirm they are stored under project global.
  2. Upgrade to V2 (2.0.26) and start OpenCode in the same directory.
  3. Open the session list (/sessions, or Ctrl+X then L). Only the sessions created after the upgrade are listed. Sessions from step 1 are missing.
  4. Run opencode api get "/api/session?directory=C%3A%5CUsers%5Civony&limit=500&parentID=null" — all the missing sessions are returned.

Expected Behavior

The V1→V2 migration should keep pre-upgrade sessions associated with the same effective location, so they still appear in the default session list of the directory they were created in. Or, at minimum, the default list scope should not hide them without any indication.

Actual Behavior

Sessions are silently filtered out of the default list.

State in ~/.local/share/opencode/opencode.db for directory = 'C:/Users/ivony':

session version project_id count
1.18.x (pre-upgrade) global 244
2.0.26 (post-upgrade) 3f432266b9eb53b3940d9e5a6122021303b35fe4 3

The project table gained a new row at upgrade time for the home directory:

id      = 3f432266b9eb53b3940d9e5a6122021303b35fe4
worktree= C:/Users/ivony
vcs     = (null)
time_created = 1791767834380   # upgrade time

while the pre-upgrade sessions still point at the legacy global project (worktree = '/').

API evidence (same running service, same directory):

$ opencode api get "/api/session?project=3f432266b9eb53b3940d9e5a6122021303b35fe4&limit=50&order=desc&parentID=null"
data.length = 3          # only post-upgrade sessions

$ opencode api get "/api/session?directory=C%3A%5CUsers%5Civony&limit=500&parentID=null"
data.length = 247        # all sessions, including the 244 legacy ones

Relevant client logic found in the shipped binary: the session-list dialog initializes allProjects = (tabs.scope !== "cwd"), and the default tabs.scope is "cwd", so the query becomes { project: <location project id>, subpath: <current dir relative to project.directory> }. It falls back to a directory-based query only when the resolved project is global. Search terms are applied on top of that same scope, so searching does not reveal the missing sessions either.

Also note kv table marker migration.v1-v2 = {"phase":"completed"} confirms the migration ran.

Additional Context

  • The sessions are not lost. Verified by reading the DB directly and by the directory-filtered API call above; each missing session can be opened by id.
  • Workaround: press Ctrl+A in the session list to toggle "Show all project sessions", or set "tabs": { "scope": "global" } in ~/.config/opencode/cli.json. Both make the legacy sessions visible.
  • A proper per-session fix exists via the API: POST /api/session/{sessionID}/move with {"directory":"C:\\Users\\ivony"} re-associates one legacy session with the new home project and clears its subpath, after which it appears in the default cwd-scoped list. I used this to migrate 244 sessions one by one; it works, but there is no bulk/migration path, so an affected user with a large history must script it.
  • Related: opencode api get "/api/location?directory=..." ignores the query parameter and resolves the location from the process cwd, which makes it awkward to verify location resolution for another directory from the CLI.
  • Frequency: reproducible consistently for every non-git directory used with V1 that has sessions (the home directory is the main one here; the same applies to WSL UNC paths such as //wsl.localhost/Ubuntu/home/ivony).

Activity

  1. opencode-agent commented on Oct 12, 2026

    @opencode-agent
    Contributor

    Thanks for the very thorough write-up, including the DB evidence and the POST /api/session/{id}/move workaround. I think this is a duplicate of #49827: V1 sessions from non-git directories stay attached to the global project after the V1→V2 migration, while V2 creates a separate project for that directory. The default cwd-scoped session list then hides them, even though they're still in the database.

    Possible duplicates and closely related issues, ordered by likelihood:

    Until a fix lands, the workarounds you found still apply: press Ctrl+A in the session list, set "tabs": { "scope": "global" }, or move sessions with POST /api/session/{id}/move. If you have more details, such as the WSL UNC path case, adding them to #49827 would help.

    The bot will close this issue as a duplicate in 1 day unless you reply explaining how your problem is different.

  2. Ivony commented on Oct 12, 2026

    @Ivony
    Author

    Follow-up after digging into the 2.0.26 bundle — the root cause is broader than the one-shot V1→V2 migration, and there are two independent triggers. Confirmed from the shipped code:

    // session list (scope = "cwd")
    project.id === "global"
      ? { directory }
      : { project: project.id, subpath: relative(project.directory, directory).replaceAll("\\","/") }

    So a session is listed only when session.project_id === resolve(directory).id and session.path === relative(project.directory, directory).

    1. Migration rewrites project_id but not path

    For directories that already had a project when the migration ran (e.g. a git repo that got adopted), session_v2.project_id was updated to the new project while session_v2.path kept the old V1 value, which was relative to the old global project (/). The sessions then match the project but not the subpath, so they are filtered out.

    Real case in my DB: 106 sessions in a git worktree directory all had the correct project_id but path = "Users/ivony/slides/ai-biz" instead of "". Fixing only project_id (as the migration does) is not enough — path has to be recomputed too.

    2. Projects are created lazily, so sessions disappear the first time you use the directory

    Project rows are not created during migration for every directory; they are created on first location resolve (launching the TUI in that directory, or creating a session there). Non-git directories get their own project as well (deterministic id sha1("directory:" + dir)).

    Consequence: a directory's V1 sessions remain visible while the directory still resolves to global (the list falls back to a directory filter), and vanish from the default view the first time you actually open OpenCode in that directory. So this is not a one-time migration artefact — it silently affects every old directory the first time it is used after the upgrade.

    Also worth noting: GET /api/location?location[directory]=<dir> looks read-only but creates the project row as a side effect, which is enough to trigger the above.

    move is the fix, but it has a trap

    POST /api/session/{id}/move with {"directory": <the session's own directory>} re-resolves the project and recomputes path, which fixes both cases above. Two gotchas:

    • move does not create the target project. If the directory has no project row yet, the request returns 204 and silently leaves the session on global. The project has to exist first (a location resolve does it).
    • For directories that no longer exist, move fails with LocationNotFoundError: Directory does not exist, so those sessions cannot be reassigned at all.

    In my DB that was 244 sessions on the home directory plus 125 more elsewhere (106 stale-path, 18 directories that stopped being git worktrees, 1 deleted directory); all 550 non-archived sessions are now reachable from their own directory with 0 failures. No data was ever lost — ctrl+a / tabs.scope: "global" always showed them.

    Suggested fixes

    1. When the migration rewrites project_id, recompute subpath from the new project at the same time.
    2. Either backfill projects for all known directories during migration, or make the cwd-scoped list also fall back to matching by directory for sessions whose project was assigned before the project existed.
  3. opencode-agent commented on Oct 12, 2026

    @opencode-agent
    Contributor

    Thanks for digging into the bundle, and for the per-category numbers. They're very useful. I still think this is the same issue as #49827: legacy sessions don't match the project and subpath that the default cwd-scoped list uses. Your two triggers are already tracked there or in open PRs:

    The move gotchas you found are new information: it returns 204 and leaves the session on global when the target project row doesn't exist yet, and it fails with LocationNotFoundError for deleted directories. The same goes for GET /api/location creating the project row as a side effect. Could you post this analysis, including the gotchas, as a comment on #49827? That way it stays with the main issue and the PRs.

    The bot will still close this issue as a duplicate of #49827 unless your problem turns out to be something those issues and PRs don't cover.

  4. opencode-agent commented on Oct 13, 2026

    @opencode-agent
    Contributor

    Closing this as a duplicate of #49827. Legacy V1 sessions end up not matching the project and subpath that the default cwd-scoped session list filters on, so they're hidden even though the data is intact.

    Thanks again for the detailed investigation. Please carry your findings over so they aren't lost with this issue:

    If something here turns out not to be covered by those, reply and we can reopen.

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