Repository navigation
[Bug]: Editor tabs for deleted/duplicated files resurrect across restarts (duplicate OpenFile records + UI-focus owner leak) #21121
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
innocarpe commentedon Sep 18, 2026
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.
Pr1p commentedon Sep 27, 2026
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.
nwparker commentedon Sep 27, 2026
Consolidating #17778 here: it reports the same editor-tab resurrection and duplicate
openFilesByWorktree/unifiedTabsidentities, 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.BearMett commentedon Oct 6, 2026
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:
The PR4 notes list the cases that stay with #21124/#21125: rows saved before the marker existed, and echoes from peers on older builds.
BearMett commentedon Oct 13, 2026
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
setActiveFileignores an id with no open file.c9f4cd7e27.OpenFile.idacross restore. Overlaps the record-identity change above, using the same field.On main already (@nwparker):
c9f4cd7e27closes stale duplicate documents safely;8bd567a015protects document backing files and completes close cleanup;31d85749b4keeps placeholders for drafts during bulk close;14bae2e172routes mirrored editor closes through their captured runtime owner.Original root causes, where they stand
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.closeFilecloses one identity;setActiveFileaccepts a missing id. The close sweep is on main through the commits above. The pinned-sibling gap and thesetActiveFileguard are proposed in fix(editor): keep a pinned duplicate open on close and ignore setActiveFile for a missing file #28120.Also withdrawn:
Still open:
BearMett commentedon Oct 13, 2026
Two updates to the table above:
52e88e4469), so the pinned-sibling close and thesetActiveFileguard are on main. Credit for the same fix also goes to @SamuelAdigos's earlier fix(editor): keep a pinned duplicate open and ignore unknown ids in setActiveFile #28063.idfield and covers the divergent-draft recovery and host-scoped rename on top. Root cause 1 ("saved records have no id") is proposed there, not merged.OrcaWin commentedon Oct 15, 2026
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).