Skip to content

Desktop: duplicate project entries appear after restarting into 1.30.0 #7702

Open

Description

@fraser-langton

Where did this happen?

Desktop app (macOS)

Version

Desktop 1.30.0, verified from the installed app's Info.plist. The previous launch was 1.29.0, according to the updater log. Bundled CLI: 1.30.0.

What happened?

After restarting my computer on 21 September, nine extra project headings appeared at the top of the Projects sidebar. The original entries were still present farther down. Each extra heading contained a local workspace (local, main, or develop), making it unclear which copy to use.

Read-only checks confirmed nine pairs of distinct project records on the same host. Within every pair, both records point to the exact same local repository path and GitHub repository URL. These are duplicate Superset registrations; the records do not point to additional clones.

Expected: restarting or updating preserves the existing project list and workspace associations, without surfacing a second heading for the same local checkout.

Actual: nine previously unseen duplicate headings were prepended after this restart. Existing worktrees and terminal history are mostly associated with the original entries.

Steps to reproduce

Observed sequence; a clean, deterministic reproduction has not been established:

  1. Use an existing macOS installation with projects previously migrated from v1 to v2. The migration ledger dates the affected project imports to 7 September.
  2. Continue using the existing project list on 1.29.0.
  3. Restart the computer. In this occurrence, Superset launched as 1.30.0.
  4. Open the Projects sidebar: nine additional headings now appear above the existing list, repeating projects farther down.

This was observed once after the restart.

Logs, screenshots, or recordings

Sanitized findings from the local CLI and SQLite databases:

  • superset projects list --local --json returns nine duplicate pairs, with different project IDs but identical path and repo values within each pair.
  • In the host's host.db, the earlier records were created on 7 September, 09:06:59–09:07:20. The second set was created on 7 September, 09:07:31–09:07:44, and those IDs match successful project entries in local.db's v1_migration_state.
  • Each second-set project has exactly one local workspace. The earlier records hold the additional worktrees where present; one earlier record has 12 workspace records, compared with one local workspace on its duplicate.
  • Therefore the duplicate registrations predate this restart. The new symptom is their appearance in the sidebar.
  • The updater log shows 1.30.0 downloaded on 20 September and the first logged 1.30.0 launch at 13:57:44 on 21 September.
  • The migration ledger contains additional project-linking and terminal-migration entries at 13:58:08–13:58:10 on 21 September, immediately after that launch.

All times above are Australia/Melbourne (UTC+10).

Suspected cause, not confirmed: the startup migration or sidebar restoration exposed older duplicate registrations from the previous migration. The original duplicate-creation mechanism and the exact visibility trigger have not been established.

The sidebar offers Hide from Sidebar, but this has not been applied or verified across another restart. No projects, workspaces, or repository folders were deleted during diagnosis.

Activity

  1. github-actions commented on Sep 21, 2026

    @github-actions
    Contributor

    Summary

    Nine host projects that were duplicate registrations of the same repos (created 7 September) became visible as extra sidebar headings on the first 1.30.0 launch. Your own diagnosis is consistent with what the code does: the duplicate rows in host.db are the older bug, and the sidebar is a derived view that only renders projects with a local placement row — so duplicates can sit invisibly in host.db indefinitely and then all surface at once the moment anything mints placement rows for them. The "prepended above the existing list" detail is a strong signal, because the only code path that creates a project placement row deliberately prepends.

    Affected code

    Why one repo can be two sidebar projects. Sidebar project identity is the host project id and nothing else. mergeHostProjects keys purely on row.id (apps/desktop/src/renderer/hooks/host-projects/useHostProjects/useHostProjects.utils.ts:299), and the sidebar joins placement rows to host projects on that same key (apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.ts:215-240). There is no de-duplication on repoPath anywhere in the renderer, so two projects rows with an identical path are simply two projects.

    Why they appeared, and at the top. A project renders only if v2SidebarProjects holds a row for it. Two reconcilers mint those rows:

    • usePlaceProjectsInSidebar places every project this device's host serves that has no placement row (.../components/AgentHooks/hooks/usePlaceProjectsInSidebar/usePlaceProjectsInSidebar.ts:31-48, gate in selectProjectsToPlace.ts:21-34). Its own docstring describes exactly your situation — projects that "existed on the host and nowhere else".
    • usePlaceWorktreesInSidebar places row-less workspaces and, as a side effect, creates the project's placement row (.../usePlaceWorktreesInSidebar/usePlaceWorktreesInSidebar.ts:84-86 → useDashboardSidebarState.ts:426-448).

    Both land in ensureSidebarProjectRecord, which inserts with getPrependTabOrder(...) (.../hooks/useDashboardSidebarState/sidebarMutations.ts:85-95; providers/CollectionsProvider/dashboardSidebarLocal/tabOrder.ts:7-14 returns min - 1). That is precisely "nine new headings above the existing list, originals untouched farther down".

    The 13:58 ledger activity. A v2-surface boot still runs a full migration pass whenever followUpPending || (!migrationComplete && hasV1Data) (renderer/lib/v1-migration/pass.ts:9-21, driven from components/V1AutoMigration/V1AutoMigration.tsx:137-217). Both flags are localStorage, per-org (renderer/lib/v1-migration/completion.ts:9, :22, :53-60, :147-153), while the v1 rows in local.db are never deleted — so hasV1Data is permanently true and any loss or key-miss of those flags re-arms a full pass. In that pass, onWorkspaceAdopted calls ensureWorkspaceInSidebar(id, projectId) with no options (V1AutoMigration.tsx:214-216), so revealProject defaults to true — unlike the background reconciler, which correctly passes revealProject: false.

    Suspected root cause

    Two separate layers:

    1. Visibility (the new symptom, 21 September). A placement-reconciler pass minted v2SidebarProjects rows for the nine duplicate host projects, each prepended. I can't see release diffs from here, so the specific thing to check is whether usePlaceProjectsInSidebar (or a change to its gating) shipped in 1.30.0 but not 1.29.0 — it is the only writer that places a project purely because the host serves it, with no user action and no workspace involved. The alternative trigger is the migration pass logged at 13:58:08, via V1AutoMigration.tsx:214-216.
    2. The duplicates themselves (7 September). This is the residue of the known v1-import duplication class documented in plans/done/2026-08-10-v1-import-duplicate-projects-design.md. That fix closed both write paths — createFromImportLocal's repo-path guard (packages/host-service/src/trpc/router/project/handlers.ts:117-130), project.setup's local-path collision check (.../project/project.ts:782-793), and findByPath's local short-circuit (:487-513) — but §4 and "Out of scope" explicitly ship no dedupe sweep and no unique index on projects.repoPath, and §"Known limitation" accepts that concurrent creates for the same path can still race the guard. Two full migration passes ~30 s apart matches that shape: the run lock is released between passes (apps/desktop/src/lib/trpc/routers/migration/index.ts:110-155) and startedOrgsRef is component state that a remount resets (V1AutoMigration.tsx:83,112-113), so a remount can start a second pass in the same session.

    What I could not confirm from code: why the second pass created rows instead of linking to the first set. With both twins sharing a byte-identical repoPath, the guard at handlers.ts:121-123 should have matched. A path-normalization difference is the other candidate — worth checking the raw repo_path bytes of both rows rather than the rendered CLI output, since git rev-parse --show-toplevel output is what gets stored (.../project/utils/resolve-repo.ts:208-222).

    Reproduction

    Manual, on a scratch profile:

    1. With the desktop closed, insert a second projects row into host.db with a fresh UUID and the same repo_path as an existing project (this is what your 7 September passes produced).
    2. Launch the desktop. Expected: one heading. Actual: a second heading for the same repo, prepended above the existing list.

    Unit-level, faster to land as a regression test:

    • selectProjectsToPlace.test.ts: the selector's input type (ProjectForPlacement at selectProjectsToPlace.ts:1-5) carries only projectKey and hostIds — it structurally cannot dedupe twins. A test asserting two same-path projects collapse to one placement will fail today.
    • sidebarMutations.test.ts: call ensureSidebarProjectRecord for a twin against a collection already holding the original, and assert the new row does not get min - 1.
    • Host-service: insert two projects rows with one repoPath, then assert project.findByPath returns a single, deterministic candidate — findFirst (project.ts:487-489) currently returns an arbitrary twin, and the migration's fallback map v2ByRepoPath is likewise first-wins over unordered project.list output (renderer/lib/v1-migration/runV1Migration.ts:284-286), so successive passes can bind the same v1 project to different twins.

    Suggested fix direction

    • Stop rendering the duplicates (safe, non-destructive, fixes the reported symptom for everyone carrying legacy duplicates): collapse same-repoPath rows from the same host in mergeHostProjects, preferring the oldest createdAt, and/or skip placement in selectProjectsToPlace when a placed sibling already covers that path.
    • Make the migration callback consistent with the documented contract: pass { revealProject: false } at V1AutoMigration.tsx:214-216. As written, a background pass that adopts a workspace can un-hide a project you hid — this is the direct answer to your open question about Hide from Sidebar. Hiding is durable across plain restarts (the hidden row still counts as "placed", so neither reconciler re-places it, and usePlaceWorktreesInSidebar already passes revealProject: false); the one path that can undo it is that migration callback.
    • Determinism: make findByPath's local lookup and v2ByRepoPath pick the oldest row rather than an arbitrary one.
    • Data cleanup, then the index: a one-shot host-side dedupe that re-points the younger twin's workspaces at the older row and deletes the younger project row is the prerequisite that finally unblocks the projects.repoPath unique index deferred in the August plan.

    One safety note if you clean these up by hand before a fix lands: project.remove never deletes the repo directory, but it does run git worktree remove for every workspace of that project that is not type: "local" and not the main checkout (project.ts:839-872). Since both twins point at the same repo, confirm each twin workspace you are about to discard is type: "local" before removing the duplicate project.

    Useful follow-up data

    • Raw id, repo_path, created_at for both twins (byte comparison of the paths, not the CLI's rendering).
    • type and created_at of each twin's single workspace — whether those workspaces date to 7 September or to 13:58 on 21 September distinguishes the two visibility triggers above.
    • Whether v2SidebarProjects (renderer localStorage) held rows for the twin ids before the 1.30.0 launch.
    • Console lines [v1-migration] auto pass finished from both 7 and 21 September — two entries on 7 September would confirm the double-pass theory for the original duplication.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    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