fix(issue-13): refuse to purge the conversation the caller is running in
Review of0282312(planner-reviewer-claude-01, N1): conversation_in_use excluded the purge's whole ancestor lineage, and that lineage includes the agent CLI that launched the skill. An orchestrator purging its own UUID from its own session was therefore never refused - the only process naming the UUID was excluded, and claude keeps no transcript open for lsof - so its live conversation was deleted, contrary to what orc-onboard SKILL.md promises. - base.conversation_in_use: in use when one of the caller's session-id variables (CLAUDE_CODE_SESSION_ID, ANTIGRAVITY_CONVERSATION_ID, HERMES_SESSION_ID, GROK_SESSION_ID, OPENCODE_SESSION_ID) equals the uuid; the command-line check now excludes only the purge's own helper chain and stops at the nearest agent CLI ancestor (basename claude/agy/hermes/grok/ opencode/codex, read like detect_nearest_agent), which counts as a holder when it names the uuid; the lsof check is unchanged - tests: the non-agent helper chain is excluded but an agent ancestor is counted; the purge CLI is refused under an agent CLI naming the uuid and with each of the five session variables set to it; orc_onboard --remove --purge-conversation --yes is refused from the orchestrator's own session (exit 5, registration kept). The eight new cases fail on0282312- job_subscriber --registry-dir now defaults to default_registry_dir() like the other entry points (review R-d) - orc-onboard SKILL.md spells out how the calling session is recognised; VERSIONS.md v4.4.3 item 2 and the new-test count (24) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HCGpJXhSZasJLU38ksDghe
This commit is contained in:
1 parent
02823128fa
commit
7610258950
5 files changed
+151
-25
No files matched your search
@@ -6,21 +6,53 @@ from lib_py.paths import resolve_home
|
||||
from lib_py.verify_session import verify_session_uuid, workspace_key
|
||||
|
||||
|
||||
# Session-id variables the agent CLIs export to their tool shells (the list
|
||||
# orc_onboard.sh's auto-detection reads).
|
||||
AGENT_SESSION_ENV = ("CLAUDE_CODE_SESSION_ID", "ANTIGRAVITY_CONVERSATION_ID",
|
||||
"HERMES_SESSION_ID", "GROK_SESSION_ID", "OPENCODE_SESSION_ID")
|
||||
# Process basenames that are agent CLIs (orc_onboard.sh detect_nearest_agent).
|
||||
AGENT_CLI_NAMES = ("claude", "agy", "hermes", "grok", "opencode", "codex")
|
||||
|
||||
|
||||
def _is_agent_cli(command: str) -> bool:
|
||||
"""An agent CLI process: some token before the first option has an agent
|
||||
basename (`claude …`, `node …/claude …`, `/bin/sh …/agy …`) - the same
|
||||
reading orc_onboard.sh's detect_nearest_agent applies."""
|
||||
for tok in command.split():
|
||||
if tok.startswith("-"):
|
||||
break
|
||||
if os.path.basename(tok) in AGENT_CLI_NAMES:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def conversation_in_use(uuid: str, paths=()) -> bool:
|
||||
"""A live process still references the conversation (Issues #10, #13).
|
||||
|
||||
Either its id is on a command line other than this process's own lineage
|
||||
(`claude -r <uuid>`, `agy --conversation <uuid>`, ... - the purge command
|
||||
itself names the uuid, so its ancestors are excluded), or one of `paths`
|
||||
is held open (lsof, where available). A purge must not touch a
|
||||
conversation that is in use.
|
||||
A purge must not touch a conversation that is in use, which is any of:
|
||||
|
||||
- the caller runs inside that conversation's own agent session: one of its
|
||||
session-id variables (AGENT_SESSION_ENV) equals the uuid. An orchestrator
|
||||
purging itself from its own tool shell hits exactly this, and claude
|
||||
keeps no transcript open, so neither check below would see it;
|
||||
- a process outside this purge's own helper chain names the uuid on its
|
||||
command line (`claude -r <uuid>`, `agy --conversation <uuid>`, ...). The
|
||||
helper chain - this python and the shells/scripts that launched it -
|
||||
names the uuid too and is excluded, but only up to the nearest agent CLI
|
||||
ancestor: that ancestor is the session the purge runs in, and if its
|
||||
command line carries the uuid it is the conversation's live process;
|
||||
- one of `paths` is held open (lsof, where available).
|
||||
"""
|
||||
for var in AGENT_SESSION_ENV:
|
||||
if os.environ.get(var, "").strip() == uuid:
|
||||
return True
|
||||
try:
|
||||
out = subprocess.run(["ps", "-axo", "pid=,ppid=,command="],
|
||||
capture_output=True, text=True, timeout=10).stdout
|
||||
except Exception:
|
||||
out = ""
|
||||
parents: Dict[int, int] = {}
|
||||
commands: Dict[int, str] = {}
|
||||
holders: List[int] = []
|
||||
for line in out.splitlines():
|
||||
parts = line.split(None, 2)
|
||||
@@ -28,15 +60,19 @@ def conversation_in_use(uuid: str, paths=()) -> bool:
|
||||
continue
|
||||
pid, ppid = int(parts[0]), int(parts[1])
|
||||
parents[pid] = ppid
|
||||
if len(parts) > 2 and uuid in parts[2]:
|
||||
commands[pid] = parts[2] if len(parts) > 2 else ""
|
||||
if uuid in commands[pid]:
|
||||
holders.append(pid)
|
||||
if holders:
|
||||
lineage = set()
|
||||
p = os.getpid()
|
||||
while p and p not in lineage:
|
||||
lineage.add(p)
|
||||
me = os.getpid()
|
||||
helper_chain = set()
|
||||
p = me
|
||||
while p and p not in helper_chain:
|
||||
if p != me and _is_agent_cli(commands.get(p, "")):
|
||||
break
|
||||
helper_chain.add(p)
|
||||
p = parents.get(p, 0)
|
||||
if any(h not in lineage for h in holders):
|
||||
if any(h not in helper_chain for h in holders):
|
||||
return True
|
||||
existing = [p for p in paths if p and os.path.exists(p)]
|
||||
if existing and shutil.which("lsof"):
|
||||
|
||||
Reference in new issue
Block a user