Repository navigation
[Bug]: Editor tabs for deleted/duplicated files resurrect across restarts (duplicate OpenFile records + UI-focus owner leak) #21122
Description
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
- Orca 1.4.204 (macOS arm64, local build from
357c9780f8), reproduced on the persisted store of a workstation paired with one remote runtime environment. - Same defect family reported on 1.4.137 ([Bug]: Session restore duplicates editor tab records in orca-data.json — closed editor tab resurrects on every restart #17185), 1.4.193 ([Bug]: Editor tabs won't stay closed — duplicate openFilesByWorktree + multi-identity unifiedTabs resurrect siblings (1.4.193) #17778), 1.4.194 ([Bug]: Document tabs (md/html) cannot be closed — close actions are silent no-ops, duplicate tabs pile up #16205).
Reproduction
- Pair a remote runtime environment and focus it in the UI (active workspace execution host or
settings.activeRuntimeEnvironmentId). - Open a file that belongs to a local worktree (for example via a path printed in a terminal).
- Close the tab. Press
Ctrl+Tab(defaulttab.previousRecent), or restart. - 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
- Persisted records carry no
id.buildEditorSessionData(src/renderer/src/lib/workspace-session.ts) serializesOpenFiles 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. - UI focus leaks into ownership.
open-file-apply.tsonly captures operation provenance for writableeditopens; ifcaptureEditorFileOperationProvenancethrows, thecatchfalls through tofile.runtimeEnvironmentId ?? settings.activeRuntimeEnvironmentId.tabs-create-actions.tsstampsTab.executionHostIdfromactiveWorkspaceExecutionHostId. Both are focus signals, not ownership, so a local worktree's file gets a remote owner and a second identity. - Orphan
OpenFiles are never pruned. Hydration dedupes tabs per(groupId, entityId)but keeps everyOpenFile;tabs-reconciliation.tsprunes 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. closeFilecloses one identity only. It removes the passedfileId, then finds the first unified tab with thatentityIdandbreaks. Sibling records stay inopenFiles,tabOrder,recentTabIds;Ctrl+Tab(handleSwitchRecentTab→activateCyclableTab→setActiveFile) reactivates them with no membership check.setActiveFilealso 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 orphanOpenFiles;closeFilesweeps every same-document sibling and all of its tabs with one recently-closed snapshot;setActiveFileignores 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.tsandtabs-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 (
unverifiableperdocs/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
- Tab-strip close resolution by id or entityId is handled by fix(tabs): resolve document tab close by id or entityId (#16205) #16480 (complementary).
- SSH partition adoption (fix(session): give an SSH workspace one owning partition so its tabs stop round-tripping as deletions #19572) and activation surface recovery (fix activation surface recovery ownership #20602) are adjacent but separate.
Activity
AmethystLiang commentedon Sep 17, 2026
Duplicate of #21121