Repository navigation
Description
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:
- Use an existing macOS installation with projects previously migrated from v1 to v2. The migration ledger dates the affected project imports to 7 September.
- Continue using the existing project list on 1.29.0.
- Restart the computer. In this occurrence, Superset launched as 1.30.0.
- 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 --jsonreturns nine duplicate pairs, with different project IDs but identicalpathandrepovalues 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 inlocal.db'sv1_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
github-actions commentedon Sep 21, 2026
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.dbare the older bug, and the sidebar is a derived view that only renders projects with a local placement row — so duplicates can sit invisibly inhost.dbindefinitely 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.
mergeHostProjectskeys purely onrow.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 onrepoPathanywhere in the renderer, so twoprojectsrows with an identical path are simply two projects.Why they appeared, and at the top. A project renders only if
v2SidebarProjectsholds a row for it. Two reconcilers mint those rows:usePlaceProjectsInSidebarplaces every project this device's host serves that has no placement row (.../components/AgentHooks/hooks/usePlaceProjectsInSidebar/usePlaceProjectsInSidebar.ts:31-48, gate inselectProjectsToPlace.ts:21-34). Its own docstring describes exactly your situation — projects that "existed on the host and nowhere else".usePlaceWorktreesInSidebarplaces 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 withgetPrependTabOrder(...)(.../hooks/useDashboardSidebarState/sidebarMutations.ts:85-95;providers/CollectionsProvider/dashboardSidebarLocal/tabOrder.ts:7-14returnsmin - 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 fromcomponents/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 inlocal.dbare never deleted — sohasV1Datais permanently true and any loss or key-miss of those flags re-arms a full pass. In that pass,onWorkspaceAdoptedcallsensureWorkspaceInSidebar(id, projectId)with no options (V1AutoMigration.tsx:214-216), sorevealProjectdefaults totrue— unlike the background reconciler, which correctly passesrevealProject: false.Suspected root cause
Two separate layers:
v2SidebarProjectsrows for the nine duplicate host projects, each prepended. I can't see release diffs from here, so the specific thing to check is whetherusePlaceProjectsInSidebar(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, viaV1AutoMigration.tsx:214-216.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), andfindByPath's local short-circuit (:487-513) — but §4 and "Out of scope" explicitly ship no dedupe sweep and no unique index onprojects.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) andstartedOrgsRefis 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 athandlers.ts:121-123should have matched. A path-normalization difference is the other candidate — worth checking the rawrepo_pathbytes of both rows rather than the rendered CLI output, sincegit rev-parse --show-topleveloutput is what gets stored (.../project/utils/resolve-repo.ts:208-222).Reproduction
Manual, on a scratch profile:
projectsrow intohost.dbwith a fresh UUID and the samerepo_pathas an existing project (this is what your 7 September passes produced).Unit-level, faster to land as a regression test:
selectProjectsToPlace.test.ts: the selector's input type (ProjectForPlacementatselectProjectsToPlace.ts:1-5) carries onlyprojectKeyandhostIds— it structurally cannot dedupe twins. A test asserting two same-path projects collapse to one placement will fail today.sidebarMutations.test.ts: callensureSidebarProjectRecordfor a twin against a collection already holding the original, and assert the new row does not getmin - 1.projectsrows with onerepoPath, then assertproject.findByPathreturns a single, deterministic candidate —findFirst(project.ts:487-489) currently returns an arbitrary twin, and the migration's fallback mapv2ByRepoPathis likewise first-wins over unorderedproject.listoutput (renderer/lib/v1-migration/runV1Migration.ts:284-286), so successive passes can bind the same v1 project to different twins.Suggested fix direction
repoPathrows from the same host inmergeHostProjects, preferring the oldestcreatedAt, and/or skip placement inselectProjectsToPlacewhen a placed sibling already covers that path.{ revealProject: false }atV1AutoMigration.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, andusePlaceWorktreesInSidebaralready passesrevealProject: false); the one path that can undo it is that migration callback.findByPath's local lookup andv2ByRepoPathpick the oldest row rather than an arbitrary one.projects.repoPathunique index deferred in the August plan.One safety note if you clean these up by hand before a fix lands:
project.removenever deletes the repo directory, but it does rungit worktree removefor every workspace of that project that is nottype: "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 istype: "local"before removing the duplicate project.Useful follow-up data
id,repo_path,created_atfor both twins (byte comparison of the paths, not the CLI's rendering).typeandcreated_atof 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.v2SidebarProjects(renderer localStorage) held rows for the twin ids before the 1.30.0 launch.[v1-migration] auto pass finishedfrom both 7 and 21 September — two entries on 7 September would confirm the double-pass theory for the original duplication.