Skip to content

fix(desktop): close CloseTab-Snapshot/DeleteSession resurrection race#4406

Merged
esengine merged 1 commit into
esengine:main-v2from
ashishexee:fix/4384-session-resurrection-race
Jun 15, 2026
Merged

fix(desktop): close CloseTab-Snapshot/DeleteSession resurrection race#4406
esengine merged 1 commit into
esengine:main-v2from
ashishexee:fix/4384-session-resurrection-race

Conversation

@ashishexee

Copy link
Copy Markdown
Contributor

Closes #4384

Problem

After deleting a session from the history panel, the session sometimes reappears. CloseTab removes the tab from a.tabs and releases the lock before Snapshot() writes the session file. A concurrent DeleteSession checks a.tabs, doesn't see the tab, moves the file to trash — then Snapshot() recreates it at the original path.

Two resurrection vectors:

  1. The explicit Snapshot() in CloseTab after delete(a.tabs, tabID)
  2. The background tabSnapshotLoop goroutine that holds a raw pointer to the tab and calls Snapshot() independently

Why not just move Snapshot() inside the lock

PR #4400 takes this approach. It works for Vector 1 but has two problems:

  • Snapshot() does disk I/O under a.mu, which freezes the UI during the write
  • It does not fix Vector 2 — the autosave goroutine can still call Snapshot() after DeleteSession trashes the file, because it holds a captured session path from before SetSessionPath was cleared

What this fix does

Three changes, all in desktop/tabs.go:

  1. Clear the controller's session path after the final snapshot (SetSessionPath). This makes all future Snapshot() calls no-ops. Verified safe: snapshot() at controller.go:2000 captures path under lock, returns early when path is empty.

  2. Drain any in-flight autosave goroutine via a closing flag and sync.Cond (quiesceTabAutosave). CloseTab sets closing=true and waits until the loop finishes its current write. After quiesceTabAutosave returns, no background goroutine can call Snapshot again.

  3. scheduleTabSnapshot checks the closing flag before launching new work, so no new autosave loops start after CloseTab begins teardown.

The detach path (hasActiveRuntimeWork and detachSessionRuntime) is exempt — detached runtimes keep running and must keep saving.

Disk I/O stays outside a.mu. The lock is not held during any file write.

Files Changed

  • desktop/tabs.go — closing + saveCond fields on WorkspaceTab, quiesceTabAutosave(), scheduleTabSnapshot checks closing, tabSnapshotLoop broadcasts saveCond, CloseTab clears path + drains
  • desktop/app_autosave_test.go — 2 regression tests

Verification

cd desktop and go build ./... and go vet ./...
cd desktop and go test -run TestCloseTab -v -count=1
cd desktop and go test -race -run TestCloseTab -v -count=1
cd desktop and go test -run TestTurnDone -v -count=1

…esengine#4384)

CloseTab released a.mu before Snapshot() wrote the session file, so a
concurrent DeleteSession could trash the file and the deferred Snapshot
rewrote it — the deleted session 'resurrected.' Two vectors:

1. The explicit Snapshot() after delete(a.tabs).
2. The background tabSnapshotLoop holding a raw *WorkspaceTab pointer.

Fix: after the final snapshot (and skipping the detach path), clear the
controller's session path so future snapshots no-op, and drain any in-flight
autosave loop via a closing flag + sync.Cond before CloseTab returns.
Disk I/O stays outside a.mu. Adds regression tests proving no resurrection
and that survivor tabs keep autosaving.

Fixes esengine#4384.
@github-actions github-actions Bot added v2 Go rewrite (1.x) — main-v2 branch, active development desktop Wails desktop app (desktop/**) labels Jun 15, 2026
@esengine esengine merged commit df35995 into esengine:main-v2 Jun 15, 2026
14 checks passed
@ashishexee ashishexee deleted the fix/4384-session-resurrection-race branch June 15, 2026 01:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

desktop Wails desktop app (desktop/**) v2 Go rewrite (1.x) — main-v2 branch, active development

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Session resurrects after deletion — race condition in CloseTab

2 participants