fix(agents): any spelling of a claude uuid is checked as claude's lowercase id (Issue #31, review)
_is_uuid took an uppercase or mixed-case uuid, and the paths built from it are the live lowercase session's own files on macOS's default case-insensitive APFS. The in-use checks compare exactly, though: session files and command lines carry the lowercase id, so `purge claude <UPPER>` found no holder and could remove a live session's files. _canonical now lowercases a canonical uuid, and the paths, the session-file and job matches, the in-use check and the residual all use it; a command line is searched for both spellings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
60779ba1f8
commit
c0248b88be
2 files changed
+60
-13
No files matched your search
@@ -7,11 +7,15 @@ from lib_py.verify_session import workspace_key
|
||||
_UUID = re.compile(r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", re.IGNORECASE)
|
||||
|
||||
|
||||
def _is_uuid(uuid) -> bool:
|
||||
"""claude names every session by a canonical uuid. Any other name in a
|
||||
session's place is not one: projects/<key>/memory/ (auto memory) sits
|
||||
beside the session dirs."""
|
||||
return isinstance(uuid, str) and bool(_UUID.fullmatch(uuid))
|
||||
def _canonical(uuid) -> Optional[str]:
|
||||
"""The id claude itself writes, in lowercase, for any spelling of a
|
||||
canonical uuid; None for anything else. Any other name in a session's
|
||||
place is not one: projects/<key>/memory/ (auto memory) sits beside the
|
||||
session dirs. Another spelling must go through the same checks as
|
||||
claude's own: on macOS's default case-insensitive APFS it names the
|
||||
same files, while the ids in session files and on command lines are
|
||||
compared exactly."""
|
||||
return uuid.lower() if isinstance(uuid, str) and _UUID.fullmatch(uuid) else None
|
||||
|
||||
|
||||
def _config_dir(ctx: DiscoveryContext) -> str:
|
||||
@@ -306,14 +310,16 @@ class ClaudeAgentAdapter(BaseAgentAdapter):
|
||||
# pre-#31 reach: the `<id>.jsonl` file alone.
|
||||
if not _plausible_id(uuid):
|
||||
return []
|
||||
canon = _canonical(uuid)
|
||||
cfg = _config_dir(ctx)
|
||||
found = []
|
||||
for pdir in _projects_dirs(ctx):
|
||||
base = os.path.join(pdir, ctx.ws_key, uuid)
|
||||
found += [(pdir, p) for p in ((base + ".jsonl", base) if _is_uuid(uuid) else (base + ".jsonl",))
|
||||
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 _is_uuid(uuid):
|
||||
if not canon:
|
||||
return found
|
||||
uuid = canon
|
||||
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)
|
||||
@@ -344,15 +350,17 @@ class ClaudeAgentAdapter(BaseAgentAdapter):
|
||||
def conversation_in_use(self, uuid: str, ctx: DiscoveryContext) -> bool:
|
||||
# No claude process holds a name that is not a uuid; searching
|
||||
# command lines for one ("a/b", "memory") only finds strangers.
|
||||
if not _is_uuid(uuid):
|
||||
canon = _canonical(uuid)
|
||||
if not canon:
|
||||
return False
|
||||
if conversation_in_use(uuid, [self.artifact_path(uuid, ctx)]):
|
||||
# claude's own spelling and the one asked about, on a command line.
|
||||
if any(conversation_in_use(u, [self.artifact_path(canon, ctx)]) for u in {canon, uuid}):
|
||||
return True
|
||||
return any(_session_file_live(data, pid) for _, pid, data in self._session_files(uuid, ctx))
|
||||
return any(_session_file_live(data, pid) for _, pid, data in self._session_files(canon, ctx))
|
||||
|
||||
def in_use_hints(self, uuid: str, ctx: DiscoveryContext) -> list:
|
||||
return [f"a claude process whose session file under {os.path.join(_config_dir(ctx), 'sessions')}/ names it"] \
|
||||
+ super().in_use_hints(uuid, ctx)
|
||||
+ super().in_use_hints(_canonical(uuid) or uuid, ctx)
|
||||
|
||||
def purge_artifacts(self, uuid: str, ctx: DiscoveryContext) -> list:
|
||||
# Best effort: never raises (stop runs this inside its registry
|
||||
@@ -360,6 +368,7 @@ 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
|
||||
purged = []
|
||||
job = self._owned_job(uuid, ctx)
|
||||
for path in self._owned_paths(uuid, ctx):
|
||||
@@ -379,8 +388,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 _is_uuid(uuid):
|
||||
if not _canonical(uuid):
|
||||
return sorted(p for _, p in self._candidates(uuid, ctx))
|
||||
uuid = _canonical(uuid)
|
||||
cfg = _config_dir(ctx)
|
||||
pdirs = _projects_dirs(ctx)
|
||||
found = set(scan_names(cfg, uuid, skip=pdirs))
|
||||
|
||||
Reference in new issue
Block a user