fix(agents): in_use_hints lists the processes holding the conversation file (lsof) (#78)
This commit is contained in:
1 parent
a3364e82e1
commit
c6eb4564d2
4 files changed
+312
-6
No files matched your search
@@ -197,11 +197,14 @@ class AgyAgentAdapter(BaseAgentAdapter):
|
||||
# the DB already gone, and those orphans must still be reachable.
|
||||
return any(os.path.exists(p) for p in self._artifact_paths(uuid, ctx))
|
||||
|
||||
def in_use_paths(self, uuid: str, ctx: DiscoveryContext) -> list:
|
||||
return self._artifact_paths(uuid, ctx)
|
||||
|
||||
def conversation_in_use(self, uuid: str, ctx: DiscoveryContext) -> bool:
|
||||
# The holder may keep the presence lock (or a sidecar) open rather
|
||||
# than the DB, and may name the uuid nowhere; the lsof leg of the
|
||||
# check must see every path of the conversation.
|
||||
return conversation_in_use(uuid, self._artifact_paths(uuid, ctx))
|
||||
return conversation_in_use(uuid, self.in_use_paths(uuid, ctx))
|
||||
|
||||
def purge_artifacts(self, uuid: str, ctx: DiscoveryContext) -> list:
|
||||
# Issue #10: a purge must leave nothing resumable behind. Besides the
|
||||
|
||||
@@ -13,6 +13,11 @@ _LEGACY_TABLES = (("message", "session_id"), ("messages", "session_id"), ("part"
|
||||
# (binary, HOME, XDG_DATA_HOME) -> the db `debug paths` printed for them.
|
||||
_DB_PATHS = {}
|
||||
|
||||
|
||||
def _is_shared_service(cmd: str) -> bool:
|
||||
"""The per-user opencode background service (`serve --service`)."""
|
||||
return "serve --service" in (cmd or "")
|
||||
|
||||
class OpenCodeAgentAdapter(BaseAgentAdapter):
|
||||
@property
|
||||
def name(self) -> str:
|
||||
@@ -250,6 +255,13 @@ class OpenCodeAgentAdapter(BaseAgentAdapter):
|
||||
found += rows if i == uuid else [f"{r} [child session {i}]" for r in rows]
|
||||
return found
|
||||
|
||||
def in_use_paths(self, uuid: str, ctx: DiscoveryContext) -> list:
|
||||
db = self._resolve_db(ctx)
|
||||
return [db] if db else []
|
||||
|
||||
def _holder_counts(self, cmd: str) -> bool:
|
||||
return not _is_shared_service(cmd)
|
||||
|
||||
def conversation_in_use(self, uuid: str, ctx: DiscoveryContext) -> bool:
|
||||
"""A process naming the session, or an opencode process holding the
|
||||
shared database. The per-user background service (`serve --service`)
|
||||
@@ -270,13 +282,14 @@ class OpenCodeAgentAdapter(BaseAgentAdapter):
|
||||
timeout=10).stdout
|
||||
except Exception:
|
||||
return True
|
||||
if cmd.strip() and "serve --service" not in cmd:
|
||||
if cmd.strip() and not _is_shared_service(cmd):
|
||||
return True
|
||||
return False
|
||||
|
||||
def in_use_hints(self, uuid: str, ctx: DiscoveryContext) -> list:
|
||||
db = self._resolve_db(ctx)
|
||||
return super().in_use_hints(uuid, ctx) + [
|
||||
f"another opencode session holding {self._resolve_db(ctx)}: lsof {self._resolve_db(ctx)}"]
|
||||
f"another opencode session holding {db}: lsof {db}"]
|
||||
|
||||
# opencode 2.x: the default agent is build (`--agent` is gone from the
|
||||
# TUI), and --standalone keeps the server a child of the pane's TUI
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# base.py — BaseAgentAdapter abstract base class, SpawnSpec, and DiscoveryContext
|
||||
|
||||
import os, json, sqlite3, shutil, subprocess, tempfile
|
||||
from typing import Optional, Dict, Any, List
|
||||
from typing import Optional, Dict, Any, List, Tuple
|
||||
from urllib.parse import quote
|
||||
from lib_py.paths import resolve_home
|
||||
from lib_py.verify_session import verify_session_uuid, workspace_key
|
||||
@@ -86,6 +86,34 @@ def conversation_in_use(uuid: str, paths=()) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def file_holders(paths=()) -> List[Tuple[int, str]]:
|
||||
"""(pid, command_line) for each process holding one of `paths` open via lsof (#78).
|
||||
Informational only: returns [] when lsof is absent or fails."""
|
||||
existing = [p for p in paths if p and os.path.exists(p)]
|
||||
if not existing or not shutil.which("lsof"):
|
||||
return []
|
||||
try:
|
||||
r = subprocess.run(["lsof", "-t"] + existing, capture_output=True, text=True, timeout=10)
|
||||
except Exception:
|
||||
return []
|
||||
pids: List[int] = []
|
||||
for tok in (r.stdout or "").split():
|
||||
if tok.isdigit():
|
||||
pid = int(tok)
|
||||
if pid not in pids:
|
||||
pids.append(pid)
|
||||
out: List[Tuple[int, str]] = []
|
||||
for pid in pids:
|
||||
try:
|
||||
cmd = subprocess.run(["ps", "-o", "command=", "-p", str(pid)],
|
||||
capture_output=True, text=True, timeout=10).stdout.strip()
|
||||
except Exception:
|
||||
continue
|
||||
if cmd:
|
||||
out.append((pid, cmd))
|
||||
return out
|
||||
|
||||
|
||||
def _plausible_id(uuid: str) -> bool:
|
||||
"""An id safe to build a path from and to search for: not empty, not
|
||||
made of dots alone ("." names the parent dir itself), no "/", ".." or "*"."""
|
||||
@@ -354,14 +382,25 @@ class BaseAgentAdapter:
|
||||
path = self.artifact_path(uuid, ctx)
|
||||
return uuid in path and os.path.exists(path)
|
||||
|
||||
def in_use_paths(self, uuid: str, ctx: DiscoveryContext) -> list:
|
||||
"""On-disk paths checked with lsof for conversation_in_use and in_use_hints (#78)."""
|
||||
return [self.artifact_path(uuid, ctx)]
|
||||
|
||||
def _holder_counts(self, cmd: str) -> bool:
|
||||
return True
|
||||
|
||||
def conversation_in_use(self, uuid: str, ctx: DiscoveryContext) -> bool:
|
||||
"""True while a live process still references the conversation."""
|
||||
return conversation_in_use(uuid, [self.artifact_path(uuid, ctx)])
|
||||
return conversation_in_use(uuid, self.in_use_paths(uuid, ctx))
|
||||
|
||||
def in_use_hints(self, uuid: str, ctx: DiscoveryContext) -> list:
|
||||
"""Where to look for what holds the conversation, shown when a purge
|
||||
is refused because it is in use."""
|
||||
return [f"any process with it on its command line (an editor, a grep, ...): ps -axww | grep {uuid}"]
|
||||
hints = [f"any process with it on its command line (an editor, a grep, ...): ps -axww | grep {uuid}"]
|
||||
for pid, cmd in file_holders(self.in_use_paths(uuid, ctx)):
|
||||
if self._holder_counts(cmd):
|
||||
hints.append(f"held open by pid {pid}: {cmd} (not stopped by MAM; end it yourself if it is safe)")
|
||||
return hints
|
||||
|
||||
def spawn_spec(self, binary: str, session_uuid: str = "", use_wrapper: bool = False) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
Reference in new issue
Block a user