fix(agents): a claude leftover behind a link still selects claude for its residual (Issue #31, review)

has_artifacts counted only the paths a purge may remove. When all that was
left lay behind a linked dir, `purge claude <uuid>` (or auto) found no
adapter and exited 5 with an empty residual, hiding the path the purge
had refused to follow. has_artifacts now counts every candidate; _inside
still decides what is removed, so the CLI exits 0, removes nothing and
lists the linked path as residual.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
GodopuandClaude Opus 5.5 committed 2026-10-16 04:07:49 +00:00
1 parent c0248b88be
commit dd292c12cb
2 files changed
+24 -2

No files matched your search

@@ -344,8 +344,10 @@ class ClaudeAgentAdapter(BaseAgentAdapter):
return [p for root, p in self._candidates(uuid, ctx) if _inside(root, p)] return [p for root, p in self._candidates(uuid, ctx) if _inside(root, p)]
def has_artifacts(self, uuid: str, ctx: DiscoveryContext) -> bool: def has_artifacts(self, uuid: str, ctx: DiscoveryContext) -> bool:
# Leftovers count without the transcript (a pre-#31 purge left them). # Leftovers count without the transcript (a pre-#31 purge left them),
return bool(self._owned_paths(uuid, ctx)) # and so does one behind a link: the purge keeps it, but picking
# claude is what gets it reported as residual.
return bool(self._candidates(uuid, ctx))
def conversation_in_use(self, uuid: str, ctx: DiscoveryContext) -> bool: def conversation_in_use(self, uuid: str, ctx: DiscoveryContext) -> bool:
# No claude process holds a name that is not a uuid; searching # No claude process holds a name that is not a uuid; searching
+20
View File
@@ -355,6 +355,26 @@ def test_claude_purge_never_follows_a_linked_dir_below_a_root(home, tmp_path, co
str(cfg / "projects" / key / conv), str(cfg / "file-history" / conv)]) str(cfg / "projects" / key / conv), str(cfg / "file-history" / conv)])
def test_purge_cli_reports_a_linked_orphan_it_will_not_remove(home, tmp_path, conv):
"""A candidate behind a link is all that is left: the CLI still picks
claude, so its residual names it, and removes nothing."""
ws = tmp_path / "ws"
ws.mkdir()
keep = tmp_path / "outside" / conv / "keep.txt"
keep.parent.mkdir(parents=True)
keep.write_text("keep")
troot = tmp_path / "ctmp" / f"claude-{UID}"
troot.mkdir(parents=True)
(troot / "linked-workspace").symlink_to(tmp_path / "outside")
res = _agents_cli(["purge", "claude", conv, str(ws)], ws)
assert res.returncode == 0, res.stdout + res.stderr
assert json.loads(res.stdout) == {"agent": "claude", "purged": [],
"residual": [str(troot / "linked-workspace" / conv)], "in_use": False}
assert keep.exists()
def test_claude_purge_removes_only_the_link_at_the_leaf(home, tmp_path, conv): def test_claude_purge_removes_only_the_link_at_the_leaf(home, tmp_path, conv):
ws = tmp_path / "ws" ws = tmp_path / "ws"
ws.mkdir() ws.mkdir()