Skip to content

V1→V2 importer keeps project_id='global' for non-git directories, so migrated sessions never appear in the per-directory session picker #50551

Closed as duplicate

Description

@phoenixgao

Description

After upgrading 1.18.32 → 2.0.12 (curl installer, same ~/.local/share/opencode/opencode.db), /sessions in a non-git directory shows none of that directory's V1 history. Switching the picker to "Show all project sessions" shows them, so the rows were imported — they just aren't attributed to the directory's project anymore.

Root cause, checked against the v2 branch and my DB:

  1. In V1, sessions from non-git directories live under the catch-all project global (worktree /), with session.path relative to / (e.g. Users/me/notes). The V1 picker queried {path: relative(worktree, cwd)} against that same global project, so they matched.
  2. V2 Project.resolve gives every directory a real project; non-git dirs get ID.make(Hash.fast("directory:" + dir)) (packages/core/src/project.ts). sha1("directory:/Users/me/notes") equals the project.id V2 created on first launch, byte for byte.
  3. The V1→V2 importer copies project_id verbatim and only falls back to global when the id is missing; it never re-resolves directory (packages/core/src/database/v1-migration.bun.ts:586): const projectID = projects.has(nextID.project_id) ? nextID.project_id : Project.ID.global — directory and path go through the INSERT ... SELECT unchanged.
  4. The per-directory picker sends {project: <real id>, subpath} and SessionStore.list filters eq(SessionTable.project_id, input.project) (packages/core/src/session/store.ts:107). Rows still tagged global can't match. The global → {directory} fallback in dialog-session-list.tsx is unreachable because the current project is no longer global.

V1's Project.fromDirectory had a claim step (UPDATE session SET project_id = ? WHERE project_id = 'global' AND directory = ?), but it only ran when a non-global project was resolved — i.e. git repos. V2's project.ts touches only ProjectTable, so nothing ever re-homes these rows.

On my machine: of ~1340 imported V1 sessions, 748 still have project_id = 'global' in session_v2, spread over a dozen directories V1 had filed under global (home dir, a notes directory, non-git work dirs). All 748 are visible only in "all projects" mode.

Check your own DB: SELECT directory, COUNT(*) FROM session_v2 WHERE project_id = 'global' GROUP BY directory ORDER BY 2 DESC;

Possible fixes:

  • Importer: for project_id = 'global' rows, run directory through Project.resolve, assign that id (creating the project row if needed) and rewrite path relative to the new root.
  • Or port the V1 claim step into V2 project persistence: when a directory-hash project is first persisted, adopt global sessions with that exact directory.
  • Or picker fallback: for a non-global current project, also match project_id = 'global' AND directory = <cwd>.

Related, not identical: #42230 (same symptom via /new after git init), #21230 (V1 claim step only matching exact directory, closed "cannot replicate"), #16579 (1.2.21-era ancestor of the worktree=/ mismatch), #38529 (non-git dirs sharing global), #49641 (importer is one-shot; the KV-reset hazard discussed there applies to any re-run-based fix).

Plugins

None relevant — the behaviour is in the core importer and picker SQL cited above.

OpenCode version

2.0.12, upgraded from 1.18.32 via curl -fsSL https://opencode.ai/v2/install | bash (shared opencode.db)

Steps to reproduce

  1. On 1.18.32, run opencode in a directory that is not a git repo; create a few sessions.
  2. Install 2.0.x with the curl installer against the same opencode.db; let the V1→V2 migration finish.
  3. Run opencode in that directory, open /sessions in "current directory" mode → no history.
  4. Toggle "Show all project sessions" → the sessions are there.
  5. sqlite3 ~/.local/share/opencode/opencode.db "SELECT project_id, path FROM session_v2 WHERE directory = '<dir>'" → global | Users/...; SELECT id FROM project WHERE worktree = '<dir>' returns the new sha1-based id.

Operating System

macOS 26.1 (Apple Silicon)

Terminal

Reproduced in the TUI and in the Desktop app (1.18.30); the behaviour is server-side.

Activity

  1. github-actions commented on Sep 22, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

    • migrate-v1: non-git sessions in "global" project hidden after V1→V2 upgrade #49827: Same root cause — V1 sessions from non-git directories are stored with project_id='global' and become invisible in the per-directory session picker after upgrading to V2. That issue also includes a workaround (POST /api/session/{sessionID}/move) and confirms the bug is in the V1→V2 importer not re-keying global sessions to their directory's V2 project.

    Your analysis and reproduction steps are more detailed and add useful depth (especially the v1-migration.bun.ts:586 reference and the DB query), so this issue may still be worth keeping open as a more complete report, but please coordinate with the linked issue.

  2. VillagerTom commented on Sep 24, 2026

    @VillagerTom

    Same root cause — with a data point your analysis was missing: not all v1 sessions are affected.

    Ran the same drill on my DB (opencode 2.0.14, imported from 1.18.x):

    • Of 360 imported v1 sessions, 324 survived and every single one (324/324) was a git-project session (project id carried into the v2 project table, path already relative to the repo worktree).
    • The 36 lost ones were 100% project_id='global' in v1 itself (tmp/opencode, home/tom/..., media mounts) — and they were never reassigned: the importer's fallback at v1-migration.bun.ts:586 only fires when the id is missing (and logs Reassigned V1 session with missing project), but the literal 'global' row exists in project (worktree /), so projects.has('global') is true and the ternary keeps 'global' verbatim. Conclusion: this is a semantic mismatch between v1 and v2 resolution, not the fallback branch.

    Two additions beyond what's listed here:

    1. Dirs that are git today but were never tracked by v1 fall in the same lost group — v1 filed those sessions under 'global' too. Cross-referencing the global rows from your query against the filesystem: some of those directories are git repos today (the repo may even post-date the sessions), yet their imported rows still carry 'global' + a root-relative path. The discriminator is the v1 row's project_id='global', not the current .git presence — V1's claim step only ran for repos it recognized.
    2. A second, independent gap: sessions created around v1's 20260428004200_add_session_path schema change have path IS NULL (90 rows in our DB, 2026-04-03→05-05). They keep the correct project id but are still invisible — SessionStore.list's exact eq(path, subpath) (packages/core/src/session/store.ts:108) excludes NULL when subpath=''. Sister symptom to migrate: V1-era session exists only in legacy table, invisible to V2 clients #50980, but the rows do exist in session_v2; only path is null.

    Agree your three candidate fixes stand; my vote is the fix 1 (importer rewrite) plus the isNull+directory list fallback as the cheap safety net.

  3. Hpkings777 commented on Oct 11, 2026

    @Hpkings777

    Confirming this still reproduces on v2.0.26 (latest) — adding fresh, fully-verified evidence.

    Environment

    • opencode version: 2.0.26 (latest), upgraded from V1
    • OS: Linux 6.17.0-PRoot-Distro (aarch64) — Termux / PRoot-Distro on Android
    • Terminal: TERM=xterm-256color, COLORTERM=truecolor
    • Shell: /bin/bash
    • Install/channel: latest
    • Active plugins: @tarquinen/opencode-dcp, @cortexkit/opencode-magic-context, opencode-models-discovery (not required to reproduce)

    Symptom

    From /root (non-git dir), the directory-scoped TUI session picker (Ctrl+X then L, or /sessions) shows 1 session. Switching to "all projects" (ctrl+a) shows all of them. 44 legacy V1 sessions are silently hidden.

    Database evidence (~/.local/share/opencode/opencode.db)

    • session_v2: 197 rows, of which 175 keep project_id='global'.
    • /root: 45 rows — 44 with project_id='global' + 1 new V2 session with the freshly-created project cd31f2124f23b5c0556ebda23ab6bff6bdc91712.
    • Legacy session table has the same distribution → importer copied project_id verbatim.
    • Scope across the whole DB: global sessions stranded in /root (44), phorixgpa (42+11), samtron (33), exp (8), … — i.e. every non-git directory.

    Hash formula verified locally

    sha1("directory:" + dir) reproduces the V2 project id byte-for-byte:

    sha1("directory:/root") = cd31f2124f23b5c0556ebda23ab6bff6bdc91712   # == the V2 project created for /root
    

    This matches the Project.resolve analysis in #50551.

    API evidence (opencode api session.list)

    query result
    (no filter — the Ctrl+A view) 197
    directory=/root 45 (44 global + 1 new)
    project=cd31f212… 1 ← what the TUI sends for directory scope
    project=cd31f212…&subpath=root 0

    Why the TUI misses them

    The client resolves /root to its own V2 project (cd31f212…) instead of the legacy global project, so it scopes the query to { project: "cd31f212…", subpath: "" } → 1 hit, while the 44 legacy rows live under { project: "global", subpath: "root" }. The V1→V2 importer created the per-directory project rows but never re-homed the global sessions.

    Workaround

    POST /api/session/{sessionID}/move with the session's own directory re-resolves project_id and restores visibility (consistent with #51176 / #50800).

    Cross-refs: #50551 (code-level cause), #51176, #50800, #54521. Please keep this open until the importer re-keys global sessions by directory (or the list falls back to directory matching).

  4. michalholes commented on Oct 13, 2026

    @michalholes

    Additional finding on 2.0.26: fixing project_id alone is not enough — path must be normalized too

    Reproduced on 2.0.26 (latest, npm @opencode/cli, Raspberry Pi arm64). Same symptom as the OP, but with an important nuance: re-attributing project_id does not make the sessions visible. path must be normalized as well.

    My case

    V1 sessions created in three non-git directories lived under global (worktree /) with path = stripped absolute dir (e.g. home/pi, mnt/backup/docker/azcli). After upgrading to 2.0.26, V2 created real projects for those directories on first launch. I manually re-attributed the sessions (moved project_id from global to the new project hash). They were still hidden.

    Captured TUI query (strace)

    The /sessions dialog issues:

    GET /api/session?limit=50&order=desc&parentID=null&project=<hash>&subpath=
    

    The empty subpath= (="exactly the project root") is the filter that excludes them:

    session path result
    'home/pi' (relative to old / worktree) excluded
    'mnt/backup/docker/azcli' excluded
    '' (empty string) listed ✅
    NULL excluded ❌

    Fix that worked

    -- after moving project_id to the new project:
    UPDATE session_v2 SET path='' WHERE project_id='<new-hash>' AND path='<old-relative-path>';

    Note: '' (empty string), not NULL. New V2 sessions store '' at the project root; NULL does not match the subpath= filter either. I verified this: setting NULL still returned 0, setting '' returned all sessions.

    Applied to 3 projects (58 sessions total): /home/pi (41), /mnt/backup/docker/azcli (19), /mnt/backup/docker/hermes (5). All became visible in the TUI picker immediately.

    Implication for the fix

    Any migration/re-attribution fix must handle both:

    1. re-resolve project_id (the OP's point), and
    2. recompute path as relative to the new project worktree — normalizing the old /-relative stripped path to '' when the new worktree equals the session directory.

    Otherwise users who manually move sessions (or any future tool doing so) will still see them hidden.

    Environment

    • opencode: 2.0.26 (latest channel)
    • OS: Linux 6.18.50+rpt-rpi-v8, arm64 (Raspberry Pi)
    • Terminal: TERM=xterm-256color, over SSH
    • Shell: /bin/bash
    • Plugins: none
  5. rekram1-node commented on Oct 21, 2026

    @rekram1-node
    Contributor

    Same issue as #49827 (non-git V1 sessions stay under global and are hidden from project lists), which has more activity and the open fix (#54048). Tracking there.

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions