Skip to content

fix: prune workspace session state when a project is removed (#9024)#9025

Open
PannenetsF wants to merge 2 commits into
stablyai:mainfrom
PannenetsF:fix/prune-workspace-session-on-project-delete
Open

fix: prune workspace session state when a project is removed (#9024)#9025
PannenetsF wants to merge 2 commits into
stablyai:mainfrom
PannenetsF:fix/prune-workspace-session-on-project-delete

Conversation

@PannenetsF

@PannenetsF PannenetsF commented Jul 16, 2026

Copy link
Copy Markdown

Summary

Deleting a project left an orphaned "unknown" workspace in the sidebar that returned after every restart (#9024).

removeProjectpruneWorktreeStateForRepo only cleaned worktreeMeta, worktreeLineageById, and workspaceLineageByChildKey. It never removed the deleted repo's worktrees from:

  • workspaceSession (the legacy blob) — lastVisitedAtByWorktreeId, sleepingAgentSessionsByPaneKey, tab state, activeWorktreeId, …
  • workspaceSessionsByHostId — the per-host session partitions

Because lastVisitedAtByWorktreeId still referenced ${repoId}::${path}, on the next launch the runtime treated those worktrees as recently-active and re-materialized worktreeMeta for them. With the owning repo/project gone, the UI rendered them as an orphaned "unknown" workspace — and hand-editing worktreeMeta out of orca-data.json didn't help because it was rebuilt from the session state.

Fix: in pruneWorktreeStateForRepo, collect the repo's owner keys before the worktreeMeta delete loop (so belongsToHost can still read meta.hostId), then run the existing removeWorkspaceSessionOwner cleanup against both this.state.workspaceSession and every this.state.workspaceSessionsByHostId partition. This reuses the exact single-owner cleanup already used by removeFolderWorkspace and deleteProjectGroup, so behavior stays consistent across all deletion paths.

Screenshots

No visual change. Behavior-only fix in the main-process persistence layer (src/main/persistence.ts).

Testing

  • pnpm lint — ran oxlint on the changed files (pass); did not run the full lint script (it also runs localization/reliability gate scripts unrelated to this change)
  • pnpm typecheck — pass (tsc --noEmit -p config/tsconfig.node.json)
  • pnpm test — ran the targeted suite vitest run src/main/persistence.test.ts (19 passed); could not run the full suite locally because the native node-pty postinstall build fails in my environment (unrelated to this change)
  • pnpm build — not run locally (blocked by the same native-module build issue)
  • Added or updated high-quality tests that would catch regressions

Added two regression tests to persistence.test.ts:

  • removeProject prunes the repo worktrees from workspace session state (legacy blob)
  • removeProject prunes the repo worktrees from per-host workspace session partitions (workspaceSessionsByHostId)

Both fail on main (the stale keys survive) and pass with this change — verified by reverting the source fix and re-running the suite.

AI Review Report

Reviewed with an AI coding agent focused on the persistence-layer cascade:

  • Main risk checked — host classification correctness: belongsToHost reads meta.hostId from worktreeMeta, so it must run before the meta delete loop. The fix collects owner keys up front (from worktreeMeta plus the session maps) to preserve the existing host-scoped semantics of pruneWorktreeStateForRepo(id, hostId); the host-scoped path (removeProjectForHost) still only prunes the target host's entries.
  • Flagged and addressed — session keys not in worktreeMeta: a worktree can be referenced in lastVisitedAtByWorktreeId without a live worktreeMeta entry (exactly the orphan case). Owner keys are therefore collected from the session maps as well, not only from worktreeMeta.
  • Verified — reuse over reimplementation: cleanup delegates to the existing removeWorkspaceSessionOwner helper (already used by removeFolderWorkspace/deleteProjectGroup), which clears tabs, layouts, browser tabs, active pointers, lastVisitedAtByWorktreeId, and sleeping agent sessions — avoiding a divergent partial cleanup.
  • Cross-platform compatibility (macOS / Linux / Windows): this change is pure in-memory state bookkeeping in the main process. It touches no filesystem paths, shell invocation, keyboard shortcuts, labels, or Electron platform APIs, so there is no OS-specific behavior. Owner keys are treated as opaque ${repoId}::${path} strings and are neither parsed nor normalized per-platform.

Security Audit

  • Input handling: no new external input. Operates only on already-persisted, app-owned state keyed by internal repo/worktree ids.
  • Command execution / path handling: none. No shell, no child_process, no filesystem access; owner keys are opaque map keys, never resolved to disk paths.
  • Auth / secrets: untouched. No credentials, tokens, or auth flows involved.
  • Dependencies: none added or changed.
  • IPC: no IPC surface changed; this is internal persistence logic invoked by the existing removeProject path.
  • Follow-up: none identified.

Notes

removeProject → pruneWorktreeStateForRepo cleared worktreeMeta, lineage,
and workspace lineage, but left the removed repo's worktrees behind in
workspaceSession (lastVisitedAtByWorktreeId, sleepingAgentSessionsByPaneKey,
tab state, …) and in every workspaceSessionsByHostId partition.

Those dangling references caused the runtime to treat the deleted repo's
worktrees as recently-active on the next launch and re-materialize their
worktreeMeta. With no owning project left, the UI rendered them as an
orphaned "unknown" workspace that returned after every restart.

Reuse the existing removeWorkspaceSessionOwner cleanup (already used by
removeFolderWorkspace and deleteProjectGroup) against the legacy session
blob and each per-host partition, collecting owner keys before the
worktreeMeta delete loop so host classification still works.

Fixes stablyai#9024

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 7c378115-7e6c-45ef-84c9-9f01b4115c4e

📥 Commits

Reviewing files that changed from the base of the PR and between 4b05fa0 and 53e7f18.

📒 Files selected for processing (2)
  • src/main/persistence.test.ts
  • src/main/persistence.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/persistence.ts

📝 Walkthrough

Walkthrough

Project removal now collects affected worktree owner keys before deleting worktree metadata. It prunes matching entries from global workspace session state and host-scoped workspace session partitions. Tests cover cleanup for global and host-scoped lastVisitedAtByWorktreeId entries while preserving worktrees from other repositories.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: pruning workspace session state when a project is removed.
Description check ✅ Passed The description includes all required sections and covers summary, screenshots, testing, AI review, security, and notes.
Linked Issues check ✅ Passed The code and tests satisfy #9024 by clearing deleted repo worktrees from legacy and per-host session state without affecting other hosts.
Out of Scope Changes check ✅ Passed The changes stay within the stated project-removal cleanup scope and add only targeted persistence logic plus regression tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@e-jung

e-jung commented Jul 16, 2026

Copy link
Copy Markdown

Thanks for tackling #9024. I tested the current head and confirmed that the full removeProject path fixes the orphaned-workspace problem: the new tests pass with this change and fail against the parent commit.

I found one cross-host regression in the host-scoped removeProjectForHost path. Owner keys do not include the host ID, but the new cleanup applies each matching key to the local session and every workspaceSessionsByHostId partition.

Reproduction: register the same repo ID and path on local and ssh:ssh-a, with local lastVisitedAt=111 and SSH lastVisitedAt=222. After removeProjectForHost("shared", "ssh:ssh-a"), the surviving local value is unexpectedly removed instead of remaining 111. This can also discard the surviving host's tabs, sleeping-agent state, and active-worktree pointer.

Could the cleanup be scoped to the selected host when hostId is non-null, while retaining the all-partitions cleanup for full removeProject? A regression test using the same owner key on two hosts should cover the distinction.

Otherwise, the fix looks focused and the full-removal behavior appears correct.

The workspace-session cleanup applied every collected owner key to the legacy
(local) session blob AND every workspaceSessionsByHostId partition. Owner keys
are `${repoId}::${path}` and carry no host, so a host-scoped removal
(removeProjectForHost) of one host wrongly wiped a surviving host's session for
a shared repo id/path — discarding its lastVisitedAt, tabs, sleeping-agent
state, and active-worktree pointer.

Scope the session prune to the removed host: prune the legacy blob only when the
target host is local (or on a full removeProject where hostId is null), and
prune only the matching workspaceSessionsByHostId partition for a non-local
host; a full removal still clears every partition. Collect session owner keys by
`${repoId}::` prefix (not belongsToHost, which is worktreeMeta-host-classified)
so a partition-only key is still prunable.

Adds regression tests for a shared repo id + path across local and an SSH host,
covering removeProjectForHost on the SSH host (local session survives) and on
the local host (SSH session survives).

Addresses review feedback on stablyai#9024.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@PannenetsF

Copy link
Copy Markdown
Author

@e-jung Thanks for the careful review and the concrete repro — you're right, that was a real cross-host bug.

I pushed 53e7f18 to scope the host-partition cleanup to the removed host:

  • When hostId is non-null, cleanup now only touches that host's workspaceSessionsByHostId partition and skips the others, so a surviving host sharing the same repo id + path keeps its lastVisitedAt, tabs, sleeping-agent state, and active-worktree pointer.
  • The legacy/local session blob is only pruned when hostId === null || hostId === LOCAL_EXECUTION_HOST_ID.
  • Full removeProject (hostId === null) still clears every partition, as before.

I also added the two regression tests you suggested, both using the same owner key on two hosts:

  • removeProjectForHost keeps the surviving host session for a shared repo id + path — removing ssh:ssh-a keeps local 111.
  • removeProjectForHost on the local host keeps a surviving SSH host session — removing local keeps SSH 222.

Would you mind taking another look when you have a moment? Thanks again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Deleting a project leaves an orphaned "unknown" workspace that returns after restart

3 participants