diff --git a/.agents/skills/lib_py/agents/adapters/claude.py b/.agents/skills/lib_py/agents/adapters/claude.py index a093a46..2510761 100644 --- a/.agents/skills/lib_py/agents/adapters/claude.py +++ b/.agents/skills/lib_py/agents/adapters/claude.py @@ -1,6 +1,6 @@ import os, re, json, glob, math, stat, time, shutil, calendar, subprocess from typing import Optional, Any -from lib_py.agents.base import BaseAgentAdapter, DiscoveryContext, conversation_in_use, scan_names, _plausible_id +from lib_py.agents.base import BaseAgentAdapter, DiscoveryContext, conversation_in_use, scan_names from lib_py.verify_session import workspace_key @@ -306,20 +306,15 @@ class ClaudeAgentAdapter(BaseAgentAdapter): never listed.""" # Checked before any path is built: whole directories named by the id # are removed, so "", "." or ".." would reach file-history/ itself, - # and "memory" the auto memory. An id that is not a uuid keeps the - # pre-#31 reach: the `.jsonl` file alone. - if not _plausible_id(uuid): + # and "memory" the auto memory. Only a uuid names a session. + uuid = _canonical(uuid) + if not uuid: return [] - canon = _canonical(uuid) cfg = _config_dir(ctx) found = [] for pdir in _projects_dirs(ctx): - base = os.path.join(pdir, ctx.ws_key, canon or uuid) - found += [(pdir, p) for p in ((base + ".jsonl", base) if canon else (base + ".jsonl",)) - if os.path.lexists(p)] - if not canon: - return found - uuid = canon + base = os.path.join(pdir, ctx.ws_key, uuid) + found += [(pdir, p) for p in (base + ".jsonl", base) if os.path.lexists(p)] found += [(cfg, p) for p in (os.path.join(cfg, "file-history", uuid), os.path.join(cfg, "session-env", uuid)) if os.path.lexists(p)] job = self._owned_job(uuid, ctx) @@ -370,7 +365,9 @@ class ClaudeAgentAdapter(BaseAgentAdapter): # shows up in residual_artifacts instead. # Ownership evidence (a job's state.json, a session file) is removed # only after what it vouches for, so a retry can still find the rest. - uuid = _canonical(uuid) or uuid + uuid = _canonical(uuid) + if not uuid: + return [] purged = [] job = self._owned_job(uuid, ctx) for path in self._owned_paths(uuid, ctx): @@ -390,9 +387,9 @@ class ClaudeAgentAdapter(BaseAgentAdapter): roots (two levels), plus the content-matched jobs dir and session files with their (masked) key files. projects is not walked deeper; it can be very large.""" - if not _canonical(uuid): - return sorted(p for _, p in self._candidates(uuid, ctx)) uuid = _canonical(uuid) + if not uuid: + return [] cfg = _config_dir(ctx) pdirs = _projects_dirs(ctx) found = set(scan_names(cfg, uuid, skip=pdirs)) diff --git a/tests/test_issue31_purge_residual.py b/tests/test_issue31_purge_residual.py index b48590f..bb21e4d 100644 --- a/tests/test_issue31_purge_residual.py +++ b/tests/test_issue31_purge_residual.py @@ -420,27 +420,29 @@ def test_claude_roots_may_be_links(home, tmp_path, monkeypatch, conv): assert adapter.residual_artifacts(conv, ctx) == [] -def test_a_non_uuid_id_takes_only_its_transcript_file(home, tmp_path): - """The pre-#31 reach, kept for ids that are not uuids (test_a4 purges - "uuid-c"): the `.jsonl` file, never a directory of that name.""" +@pytest.mark.parametrize("name", ["uuid-c", "memory", "notes"]) +def test_a_non_uuid_id_reaches_nothing(home, tmp_path, name): + """Only a uuid names a claude session: no path is built for any other id, + not even the `.jsonl` the adapter took before #31.""" ws = tmp_path / "ws" ws.mkdir() proj = home / ".claude" / "projects" / workspace_key(str(ws)) proj.mkdir() - jsonl = proj / "uuid-c.jsonl" - jsonl.write_text("{}\n") - kept = [proj / "uuid-c" / "a", home / ".claude" / "file-history" / "uuid-c" / "b"] - for p in kept: + jsonl = proj / f"{name}.jsonl" + jsonl.write_text(json.dumps({"sessionId": name, "cwd": os.path.realpath(str(ws))}) + "\n") + kept = [jsonl, proj / name / "a", home / ".claude" / "file-history" / name / "b"] + for p in kept[1:]: p.parent.mkdir(parents=True) p.write_text("x") adapter = get_adapter("claude") ctx = _ctx(ws, home) - assert adapter.has_artifacts("uuid-c", ctx) is True - assert adapter.residual_artifacts("uuid-c", ctx) == [str(jsonl)] - assert adapter.purge_artifacts("uuid-c", ctx) == [str(jsonl)] - assert not jsonl.exists() and all(p.exists() for p in kept) - assert adapter.residual_artifacts("uuid-c", ctx) == [] + assert adapter.has_artifacts(name, ctx) is False + assert adapter.residual_artifacts(name, ctx) == [] + assert adapter.purge_artifacts(name, ctx) == [] + res = _agents_cli(["purge", "claude", name, str(ws)], ws) + assert json.loads(res.stdout)["purged"] == [], res.stdout + res.stderr + assert all(p.exists() for p in kept) def test_purge_cli_resolves_claude_from_leftovers_only(home, tmp_path, conv):