Skip to content

[Bug]: Editor tabs for deleted/duplicated files resurrect across restarts (duplicate OpenFile records + UI-focus owner leak) #21121

Open

Description

@BearMett

Summary

Editor tabs for files that were closed (or that no longer exist on disk) keep coming back, and one file can accumulate several tab/record identities. Consolidates the mechanisms behind #17185, #17778 and the duplicate-tab half of #16205, and explains why the restore-time dedupe from #17370 was not enough.

Environment

Reproduction

  1. Pair a remote runtime environment and focus it in the UI (active workspace execution host or settings.activeRuntimeEnvironmentId).
  2. Open a file that belongs to a local worktree (for example via a path printed in a terminal).
  3. Close the tab. Press Ctrl+Tab (default tab.previousRecent), or restart.
  4. The tab returns. Over a session the same file accumulates more copies.

Evidence (real persisted store, paths scrubbed)

workspaceSession.openFilesByWorktree for one local worktree key:

filePath runtimeEnvironmentId count
…/kahi-2026-07-qty-by-listing.csv 107a064f-… (a paired remote runtime) 2
…/brand-requests.txt null 1
…/brand-requests.txt 107a064f-… 3

unifiedTabs holds a composite editor:<worktreeId>:107a064f…:<path> tab and a plain-uuid tab for the same path in the same group, all stamped executionHostId: "runtime:107a064f-…" although the paths are local. tabGroups[].tabOrder / recentTabIds reference both, and activeFileIdByWorktree names one of them.

The five rolling orca-data.json.bak.N files show duplicate counts growing within a session and collapsing at each restart (csv/txt: 2/0 → 1/0 → 1/1 → 1/1 → 2/1 → 2/3). This is a running leak, not a one-off corruption.

Root cause — four independent defects

  1. Persisted records carry no id. buildEditorSessionData (src/renderer/src/lib/workspace-session.ts) serializes OpenFiles without their id, so N live records sharing (worktreeId, filePath, runtimeEnvironmentId) persist as N identical rows. Restore collapses them to one; the next session mints them again.
  2. UI focus leaks into ownership. open-file-apply.ts only captures operation provenance for writable edit opens; if captureEditorFileOperationProvenance throws, the catch falls through to file.runtimeEnvironmentId ?? settings.activeRuntimeEnvironmentId. tabs-create-actions.ts stamps Tab.executionHostId from activeWorkspaceExecutionHostId. Both are focus signals, not ownership, so a local worktree's file gets a remote owner and a second identity.
  3. Orphan OpenFiles are never pruned. Hydration dedupes tabs per (groupId, entityId) but keeps every OpenFile; tabs-reconciliation.ts prunes tabs without a file but never files without a tab. tabs-surface.ts's last-resort fallback (state.openFiles.find(worktreeId match)) then selects the orphan — the visible resurrection.
  4. closeFile closes one identity only. It removes the passed fileId, then finds the first unified tab with that entityId and breaks. Sibling records stay in openFiles, tabOrder, recentTabIds; Ctrl+Tab (handleSwitchRecentTab → activateCyclableTab → setActiveFile) reactivates them with no membership check. setActiveFile also accepts an id with no backing record.

Why #17370 was not enough

It dedupes at restore time by the exact (filePath, worktreeId, runtimeEnvironmentId) tuple. It cannot see owner-mismatched siblings (null vs 107a064f…), does nothing about live minting during a session, and leaves orphaned OpenFiles in place.

Proposed fix (stacked)

  • PR 1 — stop the leak, heal existing stores, make close atomic: dedupe indistinguishable records on the write path; at hydration normalize owners to the worktree's stamped route (only when it resolves, never for read-only/live-tail/external-SSH records, and keeping divergent unsaved drafts apart), redirect superseded ids onto the survivor, correct contradicting tab executionHostIds, prune clean orphan OpenFiles; closeFile sweeps every same-document sibling and all of its tabs with one recently-closed snapshot; setActiveFile ignores ids with no record.
  • PR 2 — one owner per editor file: derive the owner from the worktree's operation route instead of UI focus in open-file-apply.ts and tabs-create-actions.ts; align the runtime mirror's editor ids with the owner-qualified id scheme.
  • PR 3 — missing-file UX: show a "file missing" state with Retry / Close for local documents instead of an empty editor; never prune remote records on loss of contact (unverifiable per docs/reference/ssh-execution-boundary.md).

Reproducing against your own store

The heal PR adds tests/tools/editor-ghost-tab-heal-repro.mjs. It copies an orca-data.json into an isolated user-data directory (the original is only read), auto-detects (worktree, path) pairs with more than one OpenFile record or mixed runtimeEnvironmentId owners, launches Orca in the background, and prints before/after snapshots around close + Ctrl+Tab, the flushed session and a restart pass:

npx electron-vite build --mode e2e
ORCA_BACKGROUND_LAUNCH=1 node tests/tools/editor-ghost-tab-heal-repro.mjs --data <copy-of-orca-data.json>

Out of scope / related

Activity

  1. innocarpe commented on Sep 18, 2026

    @innocarpe
    Contributor

    Opened a focused fix in #21473 — editor session persistence now stores OpenFile ids so restore does not mint a new identity for the same path.

  2. added theissue type on Sep 19, 2026
  3. Pr1p commented on Sep 27, 2026

    @Pr1p
    Contributor

    Opened #23347 for the first containment slice: closing an editable document now removes clean same-worktree/same-path sibling records and all linked tabs, while preserving divergent unsaved drafts and keeping external SSH files isolated. This targets close → Ctrl+Tab/restart resurrection only; owner-route correction, hydration healing, and missing-file UX remain out of scope for this PR.

  4. nwparker commented on Sep 27, 2026

    @nwparker
    Contributor

    Consolidating #17778 here: it reports the same editor-tab resurrection and duplicate openFilesByWorktree / unifiedTabs identities, and this issue explicitly includes #17778 in the defect family. The later field update on #17778 adds cross-host mirror echo evidence: mirrored rows with no local origin regrow across sync rounds, and clearing one surviving mirror is insufficient. Please retain that as an acceptance case for the owner/mirror fix.

  5. BearMett commented on Oct 6, 2026

    @BearMett
    ContributorAuthor

    Opened #25877 for the reciprocal-pairing case folded in from #17778: a paired desktop republished the editor tabs it was only mirroring from its peer, so copies came back on every connect and a close never stuck. It now publishes only the tabs it owns, and the mirror marker survives restart.

    Where the pieces for this issue stand:

    PR What it does State
    #21124 Stops duplicate editor records from resurrecting closed tabs (write side) open
    #21125 Heals duplicate and mis-owned records at restore open
    #23361 Cleans inactive restored tabs at boot open
    #24191 Folds duplicate open-file rows on session save open
    #25877 Stops paired hosts from echoing mirrored editor tabs back new

    The PR4 notes list the cases that stay with #21124/#21125: rows saved before the marker existed, and echoes from peers on older builds.

  6. BearMett commented on Oct 13, 2026

    @BearMett
    ContributorAuthor

    Status update. I re-checked this issue's original hypotheses against current main and split the remaining work into narrower PRs. The terminal re-creation work from #22149 belongs to #21341 and is now only a locally prepared alternative, so it is not listed here. "Proposed in" means the change is in an open PR, not merged.

    PRs

    PR Author What it does State
    #23361 me, continued by @nwparker Cleans editor-only workspaces at boot. The session save uses restore's filter and tab-to-group rule, so saved groups list only tabs restore keeps. open, updated
    My record-identity change (replaces #21125, not yet published) me Saves each editor record's id. Different unsaved drafts each come back in their own tab. Restore keeps the saved owner fields. A rename moves only the renaming host's records and is refused while an open copy's host cannot be placed. prepared locally; publication still under consideration
    #28120 (replaces the rest of #21124) me A pinned duplicate survives its twin's close. setActiveFile ignores an id with no open file. open
    #25877 me Paired hosts stop republishing mirrored editor tabs. open, rebased
    #23347 @Pr1p Closes duplicate document siblings. Adapted into main by c9f4cd7e27. related, not mine
    #24191 @ggbdpq Folds duplicate open-file owner tuples when the session is saved. related, not mine
    #21473 @innocarpe Saves OpenFile.id across restore. Overlaps the record-identity change above, using the same field. related, not mine

    On main already (@nwparker):

    • c9f4cd7e27 closes stale duplicate documents safely;
    • 8bd567a015 protects document backing files and completes close cleanup;
    • 31d85749b4 keeps placeholders for drafts during bulk close;
    • 14bae2e172 routes mirrored editor closes through their captured runtime owner.

    Original root causes, where they stand

    1. Saved records have no id. My record-identity change is not yet published; it is prepared locally and its publication is still under consideration. On that branch a test keeps 14 divergent drafts through three store save/load cycles, and a hidden-Electron restart capture shows main restoring one of two drafts and the branch restoring both.
    2. UI focus leaks into ownership. Not addressed. Records and tabs are still stamped from focus at open time (open-file-apply.ts, tabs-create-actions.ts). I withdrew the restore-time owner normalization from fix(editor): heal duplicate and mis-owned editor records at restore #21125, because a wrong guess attaches a draft to the wrong host. The record-identity work keeps the saved owner fields at restore instead.
    3. Orphan open-file records are never pruned. I withdrew the per-workspace prune of clean records with no tab. A missing tab reference is not evidence of a close. On the prepared record-identity branch, in the close → save → restore loop on the scrubbed store from this report, a closed document did not come back as a visible tab. That is the only scenario I checked, and it was not run against main alone.
    4. closeFile closes one identity; setActiveFile accepts a missing id. The close sweep is on main through the commits above. The pinned-sibling gap and the setActiveFile guard are proposed in fix(editor): keep a pinned duplicate open on close and ignore setActiveFile for a missing file #28120.

    Also withdrawn:

    • Treating the same absolute path as the same document across local, runtime and SSH owners.
    • Dropping diff, conflict and html-preview tabs from the live shared model. They are now filtered only when the renderer writes to disk.
    • Putting divergent drafts on the recently-closed stack, which holds 10 entries and is lost on quit.

    Still open:

  7. BearMett commented on Oct 13, 2026

    @BearMett
    ContributorAuthor

    Two updates to the table above:

  8. OrcaWin commented on Oct 15, 2026

    @OrcaWin
    Collaborator

    Update: #28120 is now merged and ships in v1.4.226. A pinned duplicate stays open when its twin closes, and setActiveFile ignores an id with no open file. Still open: saved editor records carry no id, records and tabs are still stamped with the focused host at open time, and paired hosts still republish mirrored editor tabs (#25877 is open).

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

    bugSomething isn't workingfile-editoros:macosIssues affecting macOS

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions