fix(agents): only a canonical uuid reaches any claude path (Issue #31, review)
The user asked for no path at all from an id that is not a uuid. bd09c6a
still took `<id>.jsonl` for one (the pre-#31 reach), which has_artifacts and
the residual counted while in-use always said free: `purge claude notes`
deleted notes.jsonl. Now purge, has_artifacts and residual return nothing
for such an id, as in-use already did.
This makes test_a4_adapter_contract.py::test_purge_artifacts_composite fail:
it purges the claude id 'uuid-c' and expects uuid-c.jsonl gone. Editing an
existing test needs the user's approval; the one-line data change
(a canonical uuid in place of 'uuid-c') is kept outside the branch, in job
90efa4ce's report dir. Reverting this commit restores the jsonl-only reach.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
dd292c12cb
commit
81b6c2ea6b
2 files changed
+25
-26
No files matched your search
@@ -1,6 +1,6 @@
|
|||||||
import os, re, json, glob, math, stat, time, shutil, calendar, subprocess
|
import os, re, json, glob, math, stat, time, shutil, calendar, subprocess
|
||||||
from typing import Optional, Any
|
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
|
from lib_py.verify_session import workspace_key
|
||||||
|
|
||||||
|
|
||||||
@@ -306,20 +306,15 @@ class ClaudeAgentAdapter(BaseAgentAdapter):
|
|||||||
never listed."""
|
never listed."""
|
||||||
# Checked before any path is built: whole directories named by the id
|
# Checked before any path is built: whole directories named by the id
|
||||||
# are removed, so "", "." or ".." would reach file-history/ itself,
|
# are removed, so "", "." or ".." would reach file-history/ itself,
|
||||||
# and "memory" the auto memory. An id that is not a uuid keeps the
|
# and "memory" the auto memory. Only a uuid names a session.
|
||||||
# pre-#31 reach: the `<id>.jsonl` file alone.
|
uuid = _canonical(uuid)
|
||||||
if not _plausible_id(uuid):
|
if not uuid:
|
||||||
return []
|
return []
|
||||||
canon = _canonical(uuid)
|
|
||||||
cfg = _config_dir(ctx)
|
cfg = _config_dir(ctx)
|
||||||
found = []
|
found = []
|
||||||
for pdir in _projects_dirs(ctx):
|
for pdir in _projects_dirs(ctx):
|
||||||
base = os.path.join(pdir, ctx.ws_key, canon or uuid)
|
base = os.path.join(pdir, ctx.ws_key, uuid)
|
||||||
found += [(pdir, p) for p in ((base + ".jsonl", base) if canon else (base + ".jsonl",))
|
found += [(pdir, p) for p in (base + ".jsonl", base) if os.path.lexists(p)]
|
||||||
if os.path.lexists(p)]
|
|
||||||
if not canon:
|
|
||||||
return found
|
|
||||||
uuid = canon
|
|
||||||
found += [(cfg, p) for p in (os.path.join(cfg, "file-history", uuid),
|
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)]
|
os.path.join(cfg, "session-env", uuid)) if os.path.lexists(p)]
|
||||||
job = self._owned_job(uuid, ctx)
|
job = self._owned_job(uuid, ctx)
|
||||||
@@ -370,7 +365,9 @@ class ClaudeAgentAdapter(BaseAgentAdapter):
|
|||||||
# shows up in residual_artifacts instead.
|
# shows up in residual_artifacts instead.
|
||||||
# Ownership evidence (a job's state.json, a session file) is removed
|
# 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.
|
# 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 = []
|
purged = []
|
||||||
job = self._owned_job(uuid, ctx)
|
job = self._owned_job(uuid, ctx)
|
||||||
for path in self._owned_paths(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
|
roots (two levels), plus the content-matched jobs dir and session
|
||||||
files with their (masked) key files. projects is not walked deeper;
|
files with their (masked) key files. projects is not walked deeper;
|
||||||
it can be very large."""
|
it can be very large."""
|
||||||
if not _canonical(uuid):
|
|
||||||
return sorted(p for _, p in self._candidates(uuid, ctx))
|
|
||||||
uuid = _canonical(uuid)
|
uuid = _canonical(uuid)
|
||||||
|
if not uuid:
|
||||||
|
return []
|
||||||
cfg = _config_dir(ctx)
|
cfg = _config_dir(ctx)
|
||||||
pdirs = _projects_dirs(ctx)
|
pdirs = _projects_dirs(ctx)
|
||||||
found = set(scan_names(cfg, uuid, skip=pdirs))
|
found = set(scan_names(cfg, uuid, skip=pdirs))
|
||||||
|
|||||||
@@ -420,27 +420,29 @@ def test_claude_roots_may_be_links(home, tmp_path, monkeypatch, conv):
|
|||||||
assert adapter.residual_artifacts(conv, ctx) == []
|
assert adapter.residual_artifacts(conv, ctx) == []
|
||||||
|
|
||||||
|
|
||||||
def test_a_non_uuid_id_takes_only_its_transcript_file(home, tmp_path):
|
@pytest.mark.parametrize("name", ["uuid-c", "memory", "notes"])
|
||||||
"""The pre-#31 reach, kept for ids that are not uuids (test_a4 purges
|
def test_a_non_uuid_id_reaches_nothing(home, tmp_path, name):
|
||||||
"uuid-c"): the `<id>.jsonl` file, never a directory of that name."""
|
"""Only a uuid names a claude session: no path is built for any other id,
|
||||||
|
not even the `<id>.jsonl` the adapter took before #31."""
|
||||||
ws = tmp_path / "ws"
|
ws = tmp_path / "ws"
|
||||||
ws.mkdir()
|
ws.mkdir()
|
||||||
proj = home / ".claude" / "projects" / workspace_key(str(ws))
|
proj = home / ".claude" / "projects" / workspace_key(str(ws))
|
||||||
proj.mkdir()
|
proj.mkdir()
|
||||||
jsonl = proj / "uuid-c.jsonl"
|
jsonl = proj / f"{name}.jsonl"
|
||||||
jsonl.write_text("{}\n")
|
jsonl.write_text(json.dumps({"sessionId": name, "cwd": os.path.realpath(str(ws))}) + "\n")
|
||||||
kept = [proj / "uuid-c" / "a", home / ".claude" / "file-history" / "uuid-c" / "b"]
|
kept = [jsonl, proj / name / "a", home / ".claude" / "file-history" / name / "b"]
|
||||||
for p in kept:
|
for p in kept[1:]:
|
||||||
p.parent.mkdir(parents=True)
|
p.parent.mkdir(parents=True)
|
||||||
p.write_text("x")
|
p.write_text("x")
|
||||||
adapter = get_adapter("claude")
|
adapter = get_adapter("claude")
|
||||||
ctx = _ctx(ws, home)
|
ctx = _ctx(ws, home)
|
||||||
|
|
||||||
assert adapter.has_artifacts("uuid-c", ctx) is True
|
assert adapter.has_artifacts(name, ctx) is False
|
||||||
assert adapter.residual_artifacts("uuid-c", ctx) == [str(jsonl)]
|
assert adapter.residual_artifacts(name, ctx) == []
|
||||||
assert adapter.purge_artifacts("uuid-c", ctx) == [str(jsonl)]
|
assert adapter.purge_artifacts(name, ctx) == []
|
||||||
assert not jsonl.exists() and all(p.exists() for p in kept)
|
res = _agents_cli(["purge", "claude", name, str(ws)], ws)
|
||||||
assert adapter.residual_artifacts("uuid-c", ctx) == []
|
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):
|
def test_purge_cli_resolves_claude_from_leftovers_only(home, tmp_path, conv):
|
||||||
|
|||||||
Reference in new issue
Block a user