fix(desktop): import pre-account v1 workspaces on machines that land straight on v2

Merged
KI
2mo ago
+167-16

Links

  • Linear: SUPER-1390 (app-update breakage and data loss tracker), related SUPER-808

Summary

  • Users who had v1 data without an account, updated (1.25 to 1.28/1.29), and then created an account saw only the workspaces they made on v2. Their pre-existing worktree workspaces were intact in local.db but never reached host.db, which the v2 board and sidebar read.
  • The v2 surface now runs the full ledger-guarded v1 to v2 import whenever the org's migration has not completed and local.db still holds v1 projects or workspaces. Affected users get their workspaces back on the next launch with no manual steps.

Why / Context

runV1Migration is the only thing that copies local.db projects, worktrees and workspaces into host.db. V1AutoMigration runs it on the v1 surface, and on the v2 surface only when a follow-up flag is set or the forced-flip backstop is active (V1_FORCED_FLIP_VERSION is null, so never).

An account created after the v2-only cutoff is isV2OnlyUser, so the app boots directly onto v2. Neither condition holds, only the groups backfill runs, and the import is skipped forever. File > Open Project then creates the project row plus a default main workspace in host.db, which matches the report: no duplicate project, but none of the old worktree workspaces.

The old gate assumed a machine only reaches v2 after completing the migration or through a forced flip. v2-only accounts, explicit opt-in and dev builds all break that assumption.

How It Works

  • New pure planner planV2SurfacePass({ followUpPending, migrationComplete, hasV1Data }) in lib/v1-migration/pass.ts returns "full" or "groups-only".
  • On v2, V1AutoMigration reads the completion marker and, only when incomplete with no follow-up, probes readV1Projects / readV1Workspaces for v1 rows. v1 data present means the full pass; an empty local.db stays groups-only so v2-native users do not get v1 default presets imported.
  • The pass itself is unchanged: ledger-guarded, idempotent, never deletes, and links to a project the user already re-opened on v2 instead of duplicating it.
  • markV1MigrationComplete takes { armFlipHandoff }. Completion reached on v2 no longer arms the flip welcome card, the continuity redirect, or the first-completion follow-up, because no flip happens.

Manual QA Checklist

Verified over CDP in the dev desktop app from this worktree (port owner and renderer URL confirmed, signed-in session), with local.db and host.db backed up and restored afterwards.

Seed: scratch repo with real feat-a / feat-b worktrees; one v1 project, two worktrees, three workspaces in local.db; project and workspace ledger rows cleared; app on v2 with no v1-migration-* flags; project re-opened on v2 so host.db had the project plus a main workspace.

  • Before (pre-fix sources, reload as next launch): board and sidebar show only rodrigo-repo / main; ledger empty; no flags; board text has no Feature A / Feature B
  • After (this branch, reload): board and sidebar show main, Feature A (feat-a), Feature B (feat-b); Idle column 21 to 23
  • Ledger after: project linked, two workspaces success, main linked; project not duplicated in host.db
  • Only v1-migration-complete-<org> is set; no welcome, continuity or follow-up flags
  • Second reload: still 1 project and 3 workspaces (idempotent)

Not end-to-end: the Open Project step was done by calling the same host procedures the UI hook calls (findByPath, project.create with importLocal, createLocal) instead of the native folder picker; "next launch" was a renderer reload, not a full restart; dev's default v2 surface stood in for a real post-cutoff account. Not tested against a real customer local.db or a packaged build.

Testing

  • bun test src/renderer/lib/v1-migration (apps/desktop): 95 pass, 0 fail
  • bun test src/renderer/hooks/useIsV2CloudEnabled.test.tsx src/renderer/routes/_authenticated/settings/experimental: 14 pass, 0 fail
  • bunx biome check on the touched directories: clean
  • bun run typecheck (apps/desktop): exit 0

New tests: pass.test.ts (4 cases; the first fails under the old gating) and a runV1Migration scenario that seeds one project, two worktrees and three workspaces against a host that already holds the re-opened project and its main workspace, then asserts workspace.list returns all three, the project is not duplicated, and a second pass makes no mutations.

Design Decisions

  • Fix the gate, not the importer. The importer already handled this state correctly; only the decision to run it was wrong. No second import path, no special case per user.
  • Probe for v1 data instead of always running. settings.getTerminalPresets can return v1 defaults, so a blanket full pass would import presets for brand-new v2 users.

Known Limitations

  • Behaviour change: users who explicitly opted in to v2 and never imported their v1 data now get it imported automatically. Same for a user whose migration completed under org A and who later opens org B on v2 (host-service and ledger are per org, so org A is untouched). The v1 surface already behaved this way per org.
  • Workspaces whose worktree is gone from disk still skip as no-worktree-on-disk, and readV1Projects still excludes projects with a null tab_order, so an affected user may get back fewer than every row in local.db.
  • A worktree restored on disk after the migration completed on v2 is still not adopted, because the full pass has stopped by then.
  • The opposite report (empty local.db, full host.db) is a different state and is not addressed here.

Follow-ups

  • Derive "owes a full pass" from the ledger (any v1 project or workspace without a ledger row) instead of the localStorage completion marker plus a data probe. It is durable and self-clearing; deferred to keep the blast radius of a customer fix small. Today a v2-native user with an empty local.db repeats two cheap local reads on every boot.

Risks / Rollout

  • Risk: low to medium. The pass only adds and links rows in host.db; it never deletes or rewrites local.db. Ledger rows already marked done are not redone on v2, so workspaces deleted in v2 stay deleted.
  • Rollback: revert this commit. Imported workspaces remain in host.db and are harmless; no data recovery needed.

Summary by cubic

Users who updated from a pre-account v1 install and created an account after the v2-only cutoff only saw the workspaces they made on v2; their pre-existing worktree workspaces never reached host.db, which the v2 board and sidebar read. The v2 surface now runs the full ledger-guarded v1-to-v2 import whenever the org's migration hasn't completed and local.db still holds v1 projects or workspaces, so affected users get those workspaces back on the next launch with no manual steps.

Notes

  • An empty local.db still only backfills groups, so v2-native users don't get v1 default presets imported.
  • Completion reached on v2 no longer arms the flip welcome card, continuity redirect, or first-completion follow-up, because no flip happens.
  • This is a behavior change for users who explicitly opted into v2 or whose migration completed under another org: their v1 data now imports automatically, matching the v1 surface behavior per org. The pass is idempotent and never deletes.

Summary by CodeRabbit

  • Improvements
    • Improved v1-to-v2 migration handling for installations with existing v2 projects and workspaces.
    • Migration now selects the appropriate pass based on the current installation state, including full migration or groups-only updates.
    • Existing projects, workspaces, and branches are preserved and linked during migration instead of being recreated.
    • Migration completion can now finish without enabling the one-time continuity and welcome handoff when appropriate.
    • Repeated migration passes remain safe and do not apply duplicate changes.