fix(host-service): let destroy complete when the project repo is gone from disk
Problem
A user deletes a workspace whose project repository directory no longer exists on disk (moved or deleted outside Superset) while the workspace's worktree directory still exists. workspaceCleanup.destroy fails every time with a 500 (Failed to open project repo at <repoPath>: Cannot use simple-git on a directory that does not exist), un-archives the row, and the workspace reappears. There is no way out from the UI: the events on HOST-SERVICE-3A are one user pressing delete on the same workspace ~10 times over 9 hours.
User-visible harm: a workspace whose project repo is gone can never be deleted from the UI; every attempt errors and the row comes back.
Reach: the events are from host-service 1.22.0; the code path (runDestroyPhases, step 3b) is unchanged on main, so this ships in the next host-service/desktop release and reaches every desktop user who deletes a workspace after moving or removing the project folder.
Why code, not archive/filter: archiving the issue leaves the user stuck; there is no other layer that can decide "the repo is gone" for the saga; the throw is a protective guard against orphaning disk, but the disk it protects is a dangling worktree whose gitdir pointer is already dead, so no retry can ever succeed. No in-flight PR touches this saga (checked #6381, #6605 — different files).
Root cause
runDestroyPhases step 3b (if (local && project)) always tries cleanupGitOps.resolveGitEnv(ctx, project.repoPath) to run git worktree remove in the project repo. When the repo directory is missing, simple-git's constructor throws, and because the worktree directory still exists the saga throws INTERNAL_SERVER_ERROR — a genuinely-unknown-state guard that is wrong for this specific, fully-known state.
Fix
Check the filesystem before opening the repo — no message matching, and only a genuine ENOENT counts: statSync(repoPath, { throwIfNoEntry: false }) === undefined. existsSync also answers false for a repo this process merely cannot read (EPERM under macOS privacy protection, EACCES), and that repo is intact — its worktree may hold uncommitted work — so it must not take this branch (triage-review addition, second commit). When the repo directory is really missing and the worktree directory still exists:
- skip the git-based
worktree remove— there is no repository to run it in; - delete the worktree folder directly only when it resolves strictly inside the project's managed worktrees root — new
isInsideProjectWorktreesRoot(path, projectId, worktreeBaseDir)inworktree-paths.ts, same shape asisInsideSessionsRoot(realpath-normalised, strict-prefix). Third commit (from review): the base directory is canonicalised too and<base>/<projectId>must itself sit strictly inside it, so a project-root entry that is a symlink out of the base cannot let a path beneath it pass; unit tests inworktree-paths.test.ts(the symlink case fails against the previous guard). The root is resolved the same way workspace creation resolves it:project.worktreeBaseDir ?? getHostWorktreeBaseDir(ctx)→<base>/<projectId>; - anything outside that root is left on disk with a warning (
Skipped worktree removal at …: project repo at … is missing and the folder is outside the managed worktrees root) and the delete still succeeds — an adopted or corruptworktreePathcan never berm -rf'd; - mark the worktree removed and continue; step 4 (branch delete) is naturally skipped because there is no git env (
branchDeleted: false).
Every other path is byte-for-byte unchanged: the repo-exists-but-won't-open case (permissions, corrupt .git, credential/remote failure) still throws Failed to open project repo … as a 500 — that is a genuine unknown state. Also unchanged: repo missing and worktree missing (still resolves via the old branch → warning + success, as before).
Still reports as 500 on this path, deliberately: Failed to open project repo … when the repo directory exists but cannot be opened, including when it exists but is unreadable by this process (EPERM/EACCES — pinned by the third test); Failed to remove worktree at … if the direct rm of a managed-root worktree fails (EACCES etc.). Both un-archive the row and stay retryable.
No typed cause: nothing reads one on this branch (no renderer branch, no retry decision), so it stays a plain TRPCError({ code, message }).
Not touched (different causes): Failed to verify worktree removal on task timeout/EAGAIN (HOST-SERVICE-17, HOST-SERVICE-2X) and Couldn't verify worktree state (HOST-SERVICE-28).
Verification
Both new tests reproduce the production failure against unfixed src/ (stashed):
$ git stash push -- src/ && bun test test/integration/workspace-cleanup.integration.test.ts -t "missing project repo"
httpStatus: 500,
stack: "TRPCError: Failed to open project repo at /private/var/folders/…/host-service-test-repo-1AgyuP: Cannot use simple-git on a directory that does not exist
at runDestroyPhases (…/workspace-cleanup.ts:454:15)
(fail) workspaceCleanup.destroy integration > missing project repo: worktree outside the managed root is left on disk with a warning [748.17ms]
0 pass
2 fail
With the fix:
$ bunx biome check src/trpc/router/workspace-cleanup/workspace-cleanup.ts src/trpc/router/workspace-creation/shared/worktree-paths.ts test/helpers/seed.ts test/integration/workspace-cleanup.integration.test.ts
Checked 4 files. No fixes applied. (one --write formatting pass on the test file before this)
$ cd packages/host-service && bun run typecheck
$ tsc --noEmit --emitDeclarationOnly false (clean)
$ bun test test/integration/workspace-cleanup.integration.test.ts
16 pass
0 fail
64 expect() calls
Ran 16 tests across 1 file. [9.95s]
$ bun test test/integration/workspace-create-delete.integration.test.ts test/integration/session-create-delete.integration.test.ts test/integration/workspace-delete-teardown.integration.test.ts src/trpc/router/workspace-cleanup
23 pass
0 fail
95 expect() calls
Ran 23 tests across 3 files. [11.44s]
(src/trpc/router/workspace-cleanup has no unit tests; the destroy saga is covered by the integration suites above.)
New tests in workspace-cleanup.integration.test.ts:
- positive — project with a custom
worktreeBaseDir, realgit worktree addunder<base>/<projectId>/…, repo directory removed,destroy→success,worktreeRemoved: true,branchDeleted: false, no warnings, worktree directory gone, row archived. - negative — worktree at a path outside the managed root, repo removed,
destroy→success,worktreeRemoved: false, exactly oneoutside the managed worktrees rootwarning, directory still on disk. - negative (unreadable ≠ missing) — repo parked under a parent with mode 000 so
statfails with EACCES;destroystill rejects withFailed to open project repoand the worktree stays on disk. Fails against theexistsSyncversion (the destroy resolved and deleted the worktree), passes with the ENOENT-only check. Skipped when running as root.
Fourth commit: CI caught four cases in test/workspace-cleanup.test.ts (mocked git layer, placeholder /repo path) that now routed into the direct-removal branch because their repo path did not exist on disk — fixture change only, they get a real empty temp directory. Full bun run test in packages/host-service: 1135 pass, 0 fail across 113 files.
Third-commit verification: bunx biome check clean; bun run typecheck clean; bun test over the four destroy suites + workspace-creation/shared: 102 pass, 0 fail.
Second-commit verification: bunx biome check clean on both files; bun run typecheck clean; bun test over workspace-cleanup + workspace-create-delete + session-create-delete + workspace-delete-teardown: 40 pass, 0 fail.
Refs HOST-SERVICE-3A
https://claude.ai/code/session_a9535131-e42d-466f-b011-268a5e960a27
Summary by CodeRabbit
- Bug Fixes
- Improved workspace cleanup when project repositories are unavailable.
- Safely removes missing worktrees only when they are within the managed worktree directory.
- Preserves external worktrees and provides a warning instead.
- Distinguishes inaccessible repositories from missing repositories, preserving them and reporting cleanup failures.
- Reliability
- Added path validation to prevent unintended directory deletion.
- Improved support for custom worktree base directories.
- Enhanced cleanup behavior for archived and temporary workspaces.