fix(worktrees): a session spelled through the case's symlink blocks removal

The removal guard and the per-worktree session count compared each
session's workingDir lexically against the worktree paths git lists,
which are real paths. POST /api/sessions stores workingDir as it was
spelled, so a session started in a worktree of a linked case whose
folder is a symlink (a repository on another disk, macOS /tmp, a
symlinked cases folder), named through that link, counted for no
worktree, and DELETE /api/cases/:name/worktrees/:worktree removed the
folder under the running agent (reproduced: 200 instead of 409).

Each session folder under the case path is now respelled onto the
repository's real top level before the comparison (respellUnder, pure
and lexical, so no filesystem call per session).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-10-14 05:31:37 +02:00
1 parent 3952963bad
commit 0ce6e09725
4 files changed
+85 -9

No files matched your search

+15
View File
@@ -173,6 +173,21 @@ export function isPathWithin(child: string, root: string): boolean {
return c === r || c.startsWith(r === sep ? r : r + sep);
}
/**
* `path` as the repository spells it: a path under `alias` (the case folder as its case resolves
* it, which can run through a symlink) moved under `root` (the repository's real top level), else
* `path` unchanged. Lexical, so it costs no filesystem call per session. Sessions keep the
* `workingDir` they were created with, while the worktree paths git lists are real, so without
* this a session spelled through the case's link counted for no worktree and did not block its
* removal.
*/
export function respellUnder(path: string, alias: string, root: string): string {
const p = resolve(path);
const a = resolve(alias);
if (a === resolve(root) || !isPathWithin(p, a)) return p;
return join(root, p.slice(a.length));
}
// ─── Pure: `git worktree list --porcelain` ───────────────────────────────────
export interface ParsedWorktree {