Skip to content

migration: V1→V2 data migration permanently stalls on legacy session row with directory='.' #57602

Closed

Description

@yifeizhang-hku

Summary

The V1→V2 data migration permanently stalls when the legacy session table contains a row with directory = '.' (relative). The migration copies the row into session_v2 verbatim, then the re-read through the strict directoryColumn schema throws Path is not absolute: ., the whole transaction (including the cursor update) rolls back, and every subsequent startup retries the same row and fails identically. The migration can never complete on its own.

Environment

Reproduction

  1. Have a database whose legacy session table contains at least one row with a non-absolute directory. In my case: 13 rows with directory = '.' — "ACP Session <uuid>" sessions created in Feb 2026 on v1.1.53 via the ACP integration (Zed), which recorded cwd as .. Verify with:
    SELECT id, directory FROM session WHERE directory = '.';
  2. Run any 2.x server so the V1→V2 data migration starts (kv key migration.v1-v2, phase sessions).
  3. The migration walks the legacy table ORDER BY id DESC with a cursor, hits the bad row, and dies. It aborts at every startup from then on.

Expected Behavior

  • A single legacy row with an invalid/relative directory should not be able to wedge the migration permanently. The migration should either normalize the value (e.g. resolve . against a sensible base) or skip-and-log the row, like transformSession already does for off-format message/part rows.
  • directoryColumn already deliberately tolerates the empty string ("Legacy sessions may persist an empty directory"), but . hits the absolute() throw in packages/core/src/database/path.ts.

Actual Behavior

Every startup logs:

level=ERROR message="V1 migration failed" cause="Cause([Die(EffectDrizzleQueryError: Failed query: select \"id\", \"project_id\", ... from \"session_v2\" where \"session_v2\".\"id\" = ?\nparams: [redacted] (cause: Cause([Fail(Error: Path is not absolute: .)])))])" role=server

The TUI shows a "Data migration failed" toast on every launch. The kv state stays at {"phase":"sessions","cursor":"..."} pointing at the last successfully processed row, so the same bad row is retried (and fails) on every boot.

Root Cause (from source, v2.1.1 / current main)

In packages/core/src/database/v1-migration.bun.ts, the sessions phase loop per row:

  1. Updates the kv cursor to nextID.id (inside the transaction).
  2. INSERT OR IGNORE INTO session_v2 (...) SELECT id, ..., directory, path, ... FROM session WHERE id = ${nextID.id} — copies directory verbatim, with no normalization.
  3. Re-reads the row through Drizzle: tx.select().from(SessionTable).where(eq(SessionTable.id, ...)) — this is the failing query.
  4. directoryColumn.fromDriver calls absolute() in packages/core/src/database/path.ts, which throws Path is not absolute: ${input} for '.' (posix.isAbsolute('.') === false and isWindowsStoragePath('.') === false).
  5. The failure dies the transaction, so the cursor update from step 1 rolls back too — the migration is stuck on the same row forever.

Workaround (verified)

Back up the database, repair the legacy rows, restart the service:

UPDATE session SET directory = 'C:/Users/User' WHERE directory = '.';

After restart the migration resumed from its cursor, processed the remaining 283 sessions, and completed: migration.v1-v2 → {"phase":"completed"}, all 383 legacy sessions present in session_v2, no further startup errors.

Additional Context

Activity

  1. opencode-agent commented on Oct 21, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed report. I was able to reproduce this on 2.1.1, and it still happens on the latest dev (aadd4d8).

    I tested it with opencode serve in a clean data directory on Linux, so this isn't specific to Windows:

    1. Started the server once to create the v2 database.
    2. Added legacy V1 session/message/part tables with two sessions: a newer one with an absolute directory and an older one with directory = '.'.
    3. Restarted the server twice.

    Both restarts logged V1 migration failed ... Path is not absolute: .. The migration.v1-v2 cursor stayed on the last good session, and the . session never reached session_v2, so the migration can't get past that row. The script below reproduces it.

    Related issues:

    Until this is fixed, your workaround (back up the database, then set those rows' directory to an absolute path) is the way to get the migration to finish.

    Reproduction script (repro.sh)
    #!/usr/bin/env bash
    # Reproduces #57602: V1->V2 migration stalls forever on a legacy session row with directory='.'
    # Usage: ./repro.sh            (uses `opencode` from PATH)
    #        OC_DEV=/path/to/opencode/checkout ./repro.sh   (runs from source)
    set -u
    H=$(mktemp -d /tmp/opencode/oc57602.XXXX)
    mkdir -p "$H/proj"
    export HOME=$H XDG_DATA_HOME=$H/data XDG_CONFIG_HOME=$H/config XDG_STATE_HOME=$H/state XDG_CACHE_HOME=$H/cache
    oc() {
      if [ -n "${OC_DEV:-}" ]; then
        bun --conditions=browser --preload="$OC_DEV/packages/tui/node_modules/@opentui/solid/scripts/preload.js" "$OC_DEV/packages/cli/src/index.ts" "$@"
      else
        opencode "$@"
      fi
    }
    cd "$H/proj"
    DB=$H/data/opencode/opencode.db
    
    echo "== 1. first boot: create the v2 database"
    timeout 15 bash -c "$(declare -f oc); OC_DEV='${OC_DEV:-}' oc serve --port 4599 --print-logs" >"$H/boot1.log" 2>&1
    # builds from source use the "local" channel database
    [ -f "$H/data/opencode/opencode-local.db" ] && DB=$H/data/opencode/opencode-local.db
    
    echo "== 2. seed legacy V1 tables: one good session, one with directory='.'"
    python3 - "$DB" <<'EOF'
    import sqlite3, sys, time
    c = sqlite3.connect(sys.argv[1])
    c.executescript("""
    CREATE TABLE session (
      id text PRIMARY KEY, project_id text NOT NULL, workspace_id text, parent_id text, slug text NOT NULL,
      directory text NOT NULL, path text, title text NOT NULL, version text NOT NULL, share_url text,
      summary_additions integer, summary_deletions integer, summary_files integer, summary_diffs text,
      metadata text, cost real DEFAULT 0, tokens_input integer DEFAULT 0, tokens_output integer DEFAULT 0,
      tokens_reasoning integer DEFAULT 0, tokens_cache_read integer DEFAULT 0, tokens_cache_write integer DEFAULT 0,
      revert text, permission text, agent text, model text,
      time_created integer NOT NULL, time_updated integer NOT NULL, time_compacting integer, time_archived integer);
    CREATE TABLE message (id text PRIMARY KEY, session_id text NOT NULL, time_created integer NOT NULL, time_updated integer NOT NULL, data text NOT NULL);
    CREATE TABLE part (id text PRIMARY KEY, message_id text NOT NULL, session_id text NOT NULL, time_created integer NOT NULL, time_updated integer NOT NULL, data text NOT NULL);
    """)
    now = int(time.time() * 1000)
    # newer, valid session (processed first: migration walks ORDER BY id DESC)
    c.execute("INSERT INTO session (id,project_id,slug,directory,path,title,version,time_created,time_updated) VALUES (?,?,?,?,?,?,?,?,?)",
              ("ses_zzzzgood000000000000000001", "global", "good", "/tmp/work", "", "Good session", "1.1.53", now, now))
    # older ACP session with a relative directory
    c.execute("INSERT INTO session (id,project_id,slug,directory,path,title,version,time_created,time_updated) VALUES (?,?,?,?,?,?,?,?,?)",
              ("ses_aaaaacp0000000000000000001", "global", "acp", ".", "", "ACP Session 1", "1.1.53", now, now))
    c.commit()
    EOF
    
    for i in 2 3; do
      echo "== $i. boot #$i: migration runs"
      timeout 15 bash -c "$(declare -f oc); OC_DEV='${OC_DEV:-}' oc serve --port 4599 --print-logs" >"$H/boot$i.log" 2>&1
      grep -o 'message="V1 migration failed".\{0,80\}\|Path is not absolute: [^)]*' "$H/boot$i.log" | sort -u
      python3 - "$DB" <<'EOF'
    import sqlite3, sys
    c = sqlite3.connect(sys.argv[1])
    print("  kv migration.v1-v2 =", c.execute("SELECT value FROM kv WHERE key='migration.v1-v2'").fetchone())
    print("  session_v2 ids     =", [r[0] for r in c.execute("SELECT id FROM session_v2")])
    EOF
    done
    echo "logs in $H"

    Run it: bash repro.sh

  2. self-assigned this
    on Oct 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions