refactor(lib): extract large inline python blocks to lib_py package and optimize abstraction layer
This commit is contained in:
+37
-653
@@ -265,20 +265,22 @@ print('\t'.join(env_flags) + '\n' + ' '.join(binary_tokens))
|
||||
final_cmd=$(echo "$parsed" | tail -n +2)
|
||||
fi
|
||||
|
||||
kind="cline"
|
||||
if echo "$name" | grep -qi "agy"; then
|
||||
kind="agy"
|
||||
elif echo "$name" | grep -qi "claude"; then
|
||||
kind="claude"
|
||||
elif echo "$name" | grep -qi "hermes"; then
|
||||
kind="hermes"
|
||||
elif echo "${final_cmd:-}" | grep -qi "agy"; then
|
||||
kind="agy"
|
||||
elif echo "${final_cmd:-}" | grep -qi "claude"; then
|
||||
kind="claude"
|
||||
elif echo "${final_cmd:-}" | grep -qi "hermes"; then
|
||||
kind="hermes"
|
||||
fi
|
||||
kind=""
|
||||
case "$name" in
|
||||
*-creator-claude|*-planner-claude|*-reviewer-claude) kind="claude" ;;
|
||||
*-creator-agy|*-planner-agy|*-reviewer-agy) kind="agy" ;;
|
||||
*-creator-hermes|*-planner-hermes|*-reviewer-hermes) kind="hermes" ;;
|
||||
*-creator-cline|*-planner-cline|*-reviewer-cline) kind="cline" ;;
|
||||
*)
|
||||
if echo "$name" | grep -qi "agy"; then kind="agy"
|
||||
elif echo "$name" | grep -qi "claude"; then kind="claude"
|
||||
elif echo "$name" | grep -qi "hermes"; then kind="hermes"
|
||||
elif echo "${final_cmd:-}" | grep -qi "agy"; then kind="agy"
|
||||
elif echo "${final_cmd:-}" | grep -qi "claude"; then kind="claude"
|
||||
elif echo "${final_cmd:-}" | grep -qi "hermes"; then kind="hermes"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
# Strip duplicate agent binary name/path from final_cmd if present, preventing Go flag.Parse() positional argument interruption
|
||||
final_cmd=$(python3 -c "
|
||||
@@ -821,6 +823,8 @@ herdr() {
|
||||
# Fallback to HERDR_SESSION_NAME or HERDR_SERVER_NAME or workspace slug or 'default' if not registered.
|
||||
# Prints the resolved server name on stdout.
|
||||
# ---------------------------------------------------------------------------
|
||||
# NOTE: Retained as small inline PYEOF block (39 lines) per Plan Rev.2 (Job 98393a97).
|
||||
# Small blocks have minimal overhead and high local cohesion; extraction into lib_py is reserved for large blocks (>200 lines).
|
||||
load_state_json() {
|
||||
env_python "$AGENT_SESSIONS_YAML" <<'PYEOF'
|
||||
import os, sys, sqlite3, json, yaml
|
||||
@@ -981,17 +985,18 @@ PYEOF
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# derive_session_name <workspace> <agent>
|
||||
# derive_session_name <workspace> <agent> [role]
|
||||
# ---------------------------------------------------------------------------
|
||||
derive_session_name() {
|
||||
local workspace="${1:-$PWD}" agent="${2:-}"
|
||||
local workspace="${1:-$PWD}" agent="${2:-}" role="${3:-creator}"
|
||||
role=$(echo "$role" | tr '[:upper:]' '[:lower:]')
|
||||
local base_slug
|
||||
base_slug="$(derive_workspace_slug "$workspace")"
|
||||
local slug="${base_slug#mam-}"
|
||||
if [ -n "$agent" ]; then
|
||||
printf '%s-creator-%s' "$slug" "$agent"
|
||||
printf '%s-%s-%s' "$slug" "$role" "$agent"
|
||||
else
|
||||
printf '%s-creator-' "$slug"
|
||||
printf '%s-%s-' "$slug" "$role"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -1022,7 +1027,7 @@ _validate_env_key() {
|
||||
|
||||
env_python() {
|
||||
local yaml_path="$1"; shift
|
||||
local -a envs=("YAML_PATH=$yaml_path" "HOME_DIR=$HOME_DIR" "CLAUDE_PROJECT_DIR=$CLAUDE_PROJECT_DIR" "LOCAL_BIN=$LOCAL_BIN")
|
||||
local -a envs=("YAML_PATH=$yaml_path" "HOME_DIR=$HOME_DIR" "CLAUDE_PROJECT_DIR=$CLAUDE_PROJECT_DIR" "LOCAL_BIN=$LOCAL_BIN" "PYTHONPATH=$SKILL_DIR:${PYTHONPATH:-}")
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
*=*)
|
||||
@@ -1084,7 +1089,7 @@ atomic_dump_yaml() {
|
||||
fi
|
||||
fi
|
||||
|
||||
local -a envs=("YAML_PATH=$yaml_path" "HOME_DIR=$HOME_DIR" "CLAUDE_PROJECT_DIR=$CLAUDE_PROJECT_DIR" "LOCAL_BIN=$LOCAL_BIN" "MAM_IS_NFS=$MAM_IS_NFS")
|
||||
local -a envs=("YAML_PATH=$yaml_path" "HOME_DIR=$HOME_DIR" "CLAUDE_PROJECT_DIR=$CLAUDE_PROJECT_DIR" "LOCAL_BIN=$LOCAL_BIN" "MAM_IS_NFS=$MAM_IS_NFS" "PYTHONPATH=$SKILL_DIR:${PYTHONPATH:-}")
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
*=*)
|
||||
@@ -1101,219 +1106,8 @@ atomic_dump_yaml() {
|
||||
local mutation=""; mutation="$(cat)"
|
||||
local pybin; pybin="$(_delegate_py_bin)"
|
||||
env "${envs[@]}" AGENT_SESSIONS_MUTATION="$mutation" "$pybin" - <<'PYEOF'
|
||||
import os, sys, tempfile, shutil, glob, subprocess, json, sqlite3, fcntl
|
||||
from datetime import datetime, timezone
|
||||
import yaml
|
||||
|
||||
yaml_path = os.environ['YAML_PATH']
|
||||
db_path = os.path.splitext(yaml_path)[0] + '.db'
|
||||
|
||||
def _validate(d):
|
||||
if not isinstance(d, dict):
|
||||
raise SystemExit("VALIDATE: top-level is not a mapping")
|
||||
sessions = d.get('herdr_sessions', [])
|
||||
if not isinstance(sessions, list):
|
||||
raise SystemExit("VALIDATE: herdr_sessions is not a list")
|
||||
valid = {'running', 'terminated', 'archived', 'stopped'}
|
||||
for i, s in enumerate(sessions):
|
||||
if not isinstance(s, dict):
|
||||
raise SystemExit(f"VALIDATE: herdr_sessions[{i}] not a mapping")
|
||||
if not s.get('name') or not s.get('status'):
|
||||
raise SystemExit(f"VALIDATE: herdr_sessions[{i}] missing name/status")
|
||||
if s.get('role') is not None and (not isinstance(s['role'], str) or not s['role'].strip()):
|
||||
raise SystemExit(f"VALIDATE: herdr_sessions[{i}] {s.get('name')!r} role must be a non-empty string")
|
||||
if s['status'] not in valid:
|
||||
raise SystemExit(f"VALIDATE: herdr_sessions[{i}] {s.get('name')!r} bad status {s['status']!r}")
|
||||
if not isinstance(s.get('pane'), dict):
|
||||
raise SystemExit(f"VALIDATE: herdr_sessions[{i}] {s.get('name')!r} missing pane")
|
||||
iso = s.get('isolation')
|
||||
if iso is not None:
|
||||
if not isinstance(iso, dict) or not iso.get('uuid') or not iso.get('root'):
|
||||
raise SystemExit(f"VALIDATE: herdr_sessions[{i}] {s.get('name')!r} isolation block requires uuid/root")
|
||||
orc_uuids = d.get('orchestrator_uuids')
|
||||
if orc_uuids is not None:
|
||||
if not isinstance(orc_uuids, list):
|
||||
raise SystemExit("VALIDATE: orchestrator_uuids is not a list")
|
||||
seen_orc = set()
|
||||
for u in orc_uuids:
|
||||
if not isinstance(u, str) or not u.strip():
|
||||
raise SystemExit("VALIDATE: orchestrator_uuids items must be non-empty strings")
|
||||
if u in seen_orc:
|
||||
raise SystemExit("VALIDATE: orchestrator_uuids contains duplicate item")
|
||||
seen_orc.add(u)
|
||||
|
||||
def get_terminal_set(d):
|
||||
return {s.get('name'): s.get('status') for s in d.get('herdr_sessions', []) if s.get('status') in ('stopped', 'terminated', 'archived')}
|
||||
|
||||
def get_all_sessions_status(d):
|
||||
import hashlib
|
||||
res = {}
|
||||
for s in d.get('herdr_sessions', []):
|
||||
name = s.get('name')
|
||||
ser = json.dumps(s, sort_keys=True)
|
||||
res[name] = hashlib.sha256(ser.encode('utf-8')).hexdigest()
|
||||
orc_uuids = d.get('orchestrator_uuids')
|
||||
if orc_uuids is not None:
|
||||
ser_orc = json.dumps(orc_uuids, sort_keys=True)
|
||||
res['__orchestrator_uuids__'] = hashlib.sha256(ser_orc.encode('utf-8')).hexdigest()
|
||||
return res
|
||||
|
||||
os.makedirs(os.path.dirname(db_path) or '.', exist_ok=True)
|
||||
_flock_f = open(db_path + '.lock', 'w')
|
||||
fcntl.flock(_flock_f, fcntl.LOCK_EX)
|
||||
conn = sqlite3.connect(db_path, timeout=60.0)
|
||||
conn.execute('PRAGMA busy_timeout = 60000')
|
||||
|
||||
for f in [db_path, db_path + '-wal', db_path + '-shm']:
|
||||
if os.path.exists(f):
|
||||
try:
|
||||
os.chmod(f, 0o600)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
is_nfs = os.environ.get('MAM_IS_NFS') == 'true'
|
||||
if is_nfs:
|
||||
conn.execute('PRAGMA journal_mode=DELETE')
|
||||
else:
|
||||
conn.execute('PRAGMA journal_mode=WAL')
|
||||
|
||||
try:
|
||||
# Disable auto-commit by explicitly starting a transaction with BEGIN IMMEDIATE
|
||||
# This prevents the read-modify-write lost update race condition.
|
||||
for _beg_attempt in range(300):
|
||||
try:
|
||||
conn.execute('BEGIN IMMEDIATE')
|
||||
break
|
||||
except sqlite3.OperationalError:
|
||||
import time
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
conn.execute('BEGIN IMMEDIATE')
|
||||
conn.execute('CREATE TABLE IF NOT EXISTS state (id INTEGER PRIMARY KEY, data TEXT)')
|
||||
conn.execute('CREATE TABLE IF NOT EXISTS sessions (name TEXT PRIMARY KEY, status TEXT, pane_cwd TEXT, data JSON)')
|
||||
conn.execute('CREATE INDEX IF NOT EXISTS idx_sessions_pane_cwd ON sessions(pane_cwd)')
|
||||
|
||||
row = conn.execute('SELECT data FROM state WHERE id=1').fetchone()
|
||||
if row:
|
||||
d = json.loads(row[0])
|
||||
else:
|
||||
# Seed from YAML
|
||||
if os.path.exists(yaml_path):
|
||||
with open(yaml_path) as f:
|
||||
d = yaml.safe_load(f) or {}
|
||||
else:
|
||||
d = {}
|
||||
|
||||
# Assemble d['herdr_sessions'] from sessions table if table contains data
|
||||
db_sessions = []
|
||||
cursor = conn.execute('SELECT name, status, pane_cwd, data FROM sessions')
|
||||
for s_row in cursor.fetchall():
|
||||
s_data = json.loads(s_row[3])
|
||||
s_data['name'] = s_row[0]
|
||||
s_data['status'] = s_row[1]
|
||||
if 'pane' not in s_data:
|
||||
s_data['pane'] = {}
|
||||
s_data['pane']['cwd'] = s_row[2]
|
||||
db_sessions.append(s_data)
|
||||
|
||||
if db_sessions:
|
||||
d['herdr_sessions'] = db_sessions
|
||||
elif 'herdr_sessions' not in d:
|
||||
d['herdr_sessions'] = []
|
||||
|
||||
old_sessions = get_all_sessions_status(d)
|
||||
old_roles = {s.get('name'): s.get('role') for s in db_sessions if s.get('role')}
|
||||
|
||||
# --- caller mutation (module scope: sees d, yaml, os, glob, subprocess) ---
|
||||
exec(compile(os.environ['AGENT_SESSIONS_MUTATION'], '<mutation>', 'exec'), globals())
|
||||
|
||||
# Role immutability check
|
||||
for s in d.get('herdr_sessions', []):
|
||||
name = s.get('name')
|
||||
if name in old_roles and s.get('role') != old_roles[name]:
|
||||
raise SystemExit(f"VALIDATE: role of session {name!r} cannot be modified from {old_roles[name]!r} to {s.get('role')!r}")
|
||||
|
||||
# ID Uniqueness Check (T2)
|
||||
running_keys = ['claude_session_id_own', 'agy_conversation_id_own', 'hermes_conversation_id_own', 'cline_conversation_id_own']
|
||||
id_to_session = {}
|
||||
for s in d.get('herdr_sessions', []):
|
||||
if s.get('status') == 'running':
|
||||
s_name = s.get('name')
|
||||
for k in running_keys:
|
||||
v = s.get(k)
|
||||
if v:
|
||||
if v in id_to_session and id_to_session[v] != s_name:
|
||||
raise SystemExit(f"VALIDATE: Duplicate running conversation ID {v!r} detected between {id_to_session[v]!r} and {s_name!r}")
|
||||
id_to_session[v] = s_name
|
||||
|
||||
_validate(d)
|
||||
|
||||
# Separate globals and sessions for normalization
|
||||
d_state = {k: v for k, v in d.items() if k != 'herdr_sessions'}
|
||||
conn.execute('REPLACE INTO state (id, data) VALUES (1, ?)', (json.dumps(d_state),))
|
||||
|
||||
current_names = []
|
||||
for s in d.get('herdr_sessions', []):
|
||||
name = s.get('name')
|
||||
status = s.get('status')
|
||||
pane_cwd = (s.get('pane') or {}).get('cwd', '')
|
||||
conn.execute('REPLACE INTO sessions (name, status, pane_cwd, data) VALUES (?, ?, ?, ?)',
|
||||
(name, status, pane_cwd, json.dumps(s)))
|
||||
current_names.append(name)
|
||||
|
||||
if current_names:
|
||||
placeholders = ','.join('?' for _ in current_names)
|
||||
conn.execute(f'DELETE FROM sessions WHERE name NOT IN ({placeholders})', current_names)
|
||||
else:
|
||||
conn.execute('DELETE FROM sessions')
|
||||
|
||||
new_sessions = get_all_sessions_status(d)
|
||||
|
||||
conn.commit()
|
||||
|
||||
# Write to YAML when sessions status or session list changes (e.g. create/stop/resume)
|
||||
if new_sessions != old_sessions:
|
||||
if os.path.exists(yaml_path):
|
||||
try:
|
||||
shutil.copy2(yaml_path, yaml_path + '.bak')
|
||||
except Exception:
|
||||
pass
|
||||
dir_ = os.path.dirname(yaml_path) or '.'
|
||||
fd, tmp = tempfile.mkstemp(dir=dir_, prefix='.agent-sessions.', suffix='.tmp')
|
||||
try:
|
||||
with os.fdopen(fd, 'w') as f:
|
||||
yaml.safe_dump(d, f, default_flow_style=False, sort_keys=False,
|
||||
allow_unicode=True, width=4096)
|
||||
os.replace(tmp, yaml_path)
|
||||
except Exception:
|
||||
if os.path.exists(tmp):
|
||||
os.remove(tmp)
|
||||
raise
|
||||
|
||||
try:
|
||||
conn.execute('PRAGMA wal_checkpoint(TRUNCATE)')
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
conn.rollback()
|
||||
raise
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
# H3: Re-apply chmod 0600 after close to cover newly created -wal / -shm files
|
||||
try:
|
||||
os.chmod(db_path, 0o600)
|
||||
wal = db_path + '-wal'
|
||||
if os.path.exists(wal): os.chmod(wal, 0o600)
|
||||
shm = db_path + '-shm'
|
||||
if os.path.exists(shm): os.chmod(shm, 0o600)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
fcntl.flock(_flock_f, fcntl.LOCK_UN)
|
||||
_flock_f.close()
|
||||
except Exception:
|
||||
pass
|
||||
from lib_py.atomic_yaml import atomic_dump_yaml_main
|
||||
atomic_dump_yaml_main()
|
||||
PYEOF
|
||||
}
|
||||
|
||||
@@ -1323,220 +1117,17 @@ PYEOF
|
||||
# verify_session_uuid <workspace> <agent> <uuid> [session_row_json] [mode]
|
||||
# Returns 0 if valid (passes Stage 1, 2, 3), 1 otherwise.
|
||||
# ---------------------------------------------------------------------------
|
||||
VERIFY_SESSION_PYTHON='
|
||||
_MAM_ORC_CACHE = None
|
||||
|
||||
def mam_orchestrator_uuids():
|
||||
global _MAM_ORC_CACHE
|
||||
if _MAM_ORC_CACHE is not None:
|
||||
return _MAM_ORC_CACHE
|
||||
import os, sys, json, sqlite3, yaml
|
||||
override = os.environ.get("MAM_ORCHESTRATOR_UUIDS")
|
||||
if override is not None:
|
||||
if not override.strip():
|
||||
_MAM_ORC_CACHE = []
|
||||
return _MAM_ORC_CACHE
|
||||
if override.strip().startswith("["):
|
||||
try:
|
||||
parsed = json.loads(override)
|
||||
if isinstance(parsed, list):
|
||||
_MAM_ORC_CACHE = [str(x) for x in parsed if x]
|
||||
return _MAM_ORC_CACHE
|
||||
except Exception:
|
||||
pass
|
||||
_MAM_ORC_CACHE = [x.strip() for x in override.split(",") if x.strip()]
|
||||
return _MAM_ORC_CACHE
|
||||
|
||||
res = []
|
||||
d_obj = None
|
||||
try:
|
||||
if "MAM_STATE_JSON" in os.environ and os.environ.get("MAM_STATE_JSON"):
|
||||
d_obj = json.loads(os.environ.get("MAM_STATE_JSON"))
|
||||
except Exception:
|
||||
d_obj = None
|
||||
|
||||
if d_obj is None or "orchestrator_uuids" not in d_obj:
|
||||
yaml_p = os.environ.get("AGENT_SESSIONS_YAML") or os.environ.get("YAML_PATH")
|
||||
if yaml_p:
|
||||
db_p = os.path.splitext(yaml_p)[0] + ".db"
|
||||
if os.path.exists(db_p):
|
||||
try:
|
||||
conn = sqlite3.connect(db_p, timeout=5.0)
|
||||
r = conn.execute("SELECT data FROM state WHERE id=1").fetchone()
|
||||
if r and r[0]:
|
||||
d_obj = json.loads(r[0])
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
if (d_obj is None or "orchestrator_uuids" not in d_obj) and os.path.exists(yaml_p):
|
||||
try:
|
||||
with open(yaml_p) as f:
|
||||
d_obj = yaml.safe_load(f) or {}
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
raw = d_obj.get("orchestrator_uuids") if isinstance(d_obj, dict) else None
|
||||
if raw is not None:
|
||||
if isinstance(raw, list) and all(isinstance(x, str) and x.strip() for x in raw):
|
||||
res = list(raw)
|
||||
else:
|
||||
sys.stderr.write("WARNING: malformed orchestrator_uuids in state, disabling orchestrator exclusion gate\n")
|
||||
res = []
|
||||
_MAM_ORC_CACHE = res
|
||||
return _MAM_ORC_CACHE
|
||||
|
||||
def mam_row_own_uuid(row):
|
||||
if not isinstance(row, dict):
|
||||
return None
|
||||
for k in ["claude_session_id_own", "agy_conversation_id_own", "hermes_conversation_id_own", "cline_conversation_id_own"]:
|
||||
v = row.get(k)
|
||||
if v:
|
||||
return v
|
||||
return None
|
||||
|
||||
def workspace_key(path):
|
||||
import os
|
||||
try:
|
||||
p = os.path.realpath(path)
|
||||
except Exception:
|
||||
p = path
|
||||
return p.replace("/", "-").replace("_", "-")
|
||||
|
||||
def verify_session_uuid(ws, agent, uuid, row=None, home_dir=None, claude_dir=None, mode="discover"):
|
||||
import os, json, sqlite3
|
||||
home = home_dir or os.environ.get("HOME_DIR", "")
|
||||
c_dir = claude_dir or os.environ.get("CLAUDE_PROJECT_DIR", f"{home}/.claude/projects")
|
||||
row = row or {}
|
||||
_iso = row.get("isolation")
|
||||
iso_root = _iso.get("root") if isinstance(_iso, dict) and _iso.get("root") else None
|
||||
|
||||
# The floor answers "could this transcript belong to a PREVIOUS incarnation
|
||||
# of this session?", which only matters while picking an unknown uuid off
|
||||
# disk. In "revalidate" the uuid is one this row already recorded, and a
|
||||
# session resumed but not yet written to legitimately has a transcript
|
||||
# older than its current process -- applying the floor there discards the
|
||||
# very conversation the resume was for.
|
||||
epoch = row.get("herdr_session_epoch", 0) if mode == "discover" else 0
|
||||
cwd = row.get("pane", {}).get("cwd", "") or ws
|
||||
|
||||
# ORDERING INVARIANT: the workspace check below MUST run before the
|
||||
# assigned-id shortcut. Moving the shortcut above it would return True for a
|
||||
# row belonging to a different workspace purely because it is assigned and
|
||||
# unverified, breaking the one guarantee find_workspace_uuid exists to give
|
||||
# -- never hand back an id that belongs to a different workspace. (T-12)
|
||||
if workspace_key(cwd) != workspace_key(ws):
|
||||
return False
|
||||
|
||||
if mode == "discover" and uuid in mam_orchestrator_uuids():
|
||||
if uuid != mam_row_own_uuid(row):
|
||||
return False
|
||||
|
||||
if (mode == "revalidate" and row.get("session_id_source") == "assigned"
|
||||
and not row.get("session_id_verified")):
|
||||
return True
|
||||
|
||||
if agent == "claude":
|
||||
base = (iso_root + "/projects") if iso_root else c_dir
|
||||
key = workspace_key(ws)
|
||||
path = f"{base}/{key}/{uuid}.jsonl"
|
||||
if not os.path.exists(path):
|
||||
return False
|
||||
if epoch and os.path.getmtime(path) < epoch:
|
||||
return False
|
||||
try:
|
||||
valid_session = False
|
||||
found_cwd = None
|
||||
with open(path) as f:
|
||||
for _ in range(50):
|
||||
line = f.readline()
|
||||
if not line:
|
||||
break
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
payload = json.loads(line)
|
||||
if payload.get("sessionId") == uuid:
|
||||
valid_session = True
|
||||
if payload.get("cwd"):
|
||||
found_cwd = payload.get("cwd")
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
if not valid_session:
|
||||
return False
|
||||
if found_cwd and workspace_key(found_cwd) != workspace_key(cwd):
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
elif agent == "agy":
|
||||
base = f"{iso_root or home}/.gemini/antigravity-cli/conversations"
|
||||
path = f"{base}/{uuid}.db"
|
||||
if not os.path.exists(path):
|
||||
return False
|
||||
if epoch and os.path.getmtime(path) < epoch:
|
||||
return False
|
||||
if mode == "discover":
|
||||
lc = f"{iso_root or home}/.gemini/antigravity-cli/cache/last_conversations.json"
|
||||
cache_match = False
|
||||
if os.path.exists(lc):
|
||||
try:
|
||||
with open(lc) as f:
|
||||
lc_data = json.load(f)
|
||||
cache_match = (lc_data.get(cwd) == uuid)
|
||||
except Exception:
|
||||
cache_match = False
|
||||
if not cache_match:
|
||||
if uuid in (row.get("_sibling_claimed_uuids") or []):
|
||||
return False
|
||||
try:
|
||||
conn = sqlite3.connect(path)
|
||||
r = conn.execute("SELECT count(*) FROM steps").fetchone()
|
||||
conn.close()
|
||||
if not r or r[0] < 1:
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
elif agent == "hermes":
|
||||
hdb = f"{iso_root or home}/.hermes/state.db"
|
||||
if not os.path.exists(hdb):
|
||||
return False
|
||||
if epoch and os.path.getmtime(hdb) < epoch:
|
||||
return False
|
||||
try:
|
||||
conn = sqlite3.connect(hdb)
|
||||
r = conn.execute("SELECT 1 FROM sessions WHERE id=?", (uuid,)).fetchone()
|
||||
conn.close()
|
||||
if not r:
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
elif agent == "cline":
|
||||
base = (iso_root + "/sessions") if iso_root else f"{home}/.cline/data/sessions"
|
||||
path = f"{base}/{uuid}/{uuid}.json"
|
||||
if not os.path.exists(path):
|
||||
return False
|
||||
if epoch and os.path.getmtime(path) < epoch:
|
||||
return False
|
||||
try:
|
||||
with open(path) as f:
|
||||
sdata = json.load(f)
|
||||
if sdata.get("session_id") != uuid:
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
return True
|
||||
'
|
||||
if [ -f "$SKILL_DIR/lib_py/verify_session.py" ]; then
|
||||
VERIFY_SESSION_PYTHON="$(cat "$SKILL_DIR/lib_py/verify_session.py")"
|
||||
else
|
||||
VERIFY_SESSION_PYTHON=""
|
||||
fi
|
||||
|
||||
verify_session_uuid() {
|
||||
local workspace="$1" agent="$2" uuid="$3" row_json="${4:-}" mode="${5:-discover}"
|
||||
MAM_VERIFY_PY="$VERIFY_SESSION_PYTHON" WS_ABS="$workspace" AGENT="$agent" UUID="$uuid" ROW_JSON="$row_json" MODE="$mode" env_python "$AGENT_SESSIONS_YAML" <<'PYEOF'
|
||||
WS_ABS="$workspace" AGENT="$agent" UUID="$uuid" ROW_JSON="$row_json" MODE="$mode" env_python "$AGENT_SESSIONS_YAML" <<'PYEOF'
|
||||
import os, sys, json
|
||||
exec(os.environ['MAM_VERIFY_PY'])
|
||||
from lib_py.verify_session import verify_session_uuid
|
||||
ws = os.environ['WS_ABS']
|
||||
agent = os.environ['AGENT']
|
||||
uuid = os.environ['UUID']
|
||||
@@ -1606,216 +1197,9 @@ verify_tui_viewport() {
|
||||
find_workspace_uuid() {
|
||||
local workspace="$1" agent="$2" session_name="${3:-}"
|
||||
local abs; abs="$(mam_abs_workspace "$workspace")"
|
||||
MAM_STATE_JSON="$(load_state_json)" WS_ABS="$abs" AGENT="$agent" TARGET_SESSION="$session_name" MAM_VERIFY_PY="$VERIFY_SESSION_PYTHON" env_python "$AGENT_SESSIONS_YAML" <<'PYEOF'
|
||||
import os, sys, json, glob, sqlite3
|
||||
|
||||
ws = os.environ['WS_ABS']
|
||||
agent = os.environ['AGENT']
|
||||
home = os.environ['HOME_DIR']
|
||||
claude_project_dir = os.environ.get('CLAUDE_PROJECT_DIR', f"{home}/.claude/projects")
|
||||
target = os.environ.get('TARGET_SESSION', '')
|
||||
|
||||
exec(os.environ['MAM_VERIFY_PY'])
|
||||
|
||||
OWN_KEY = {'claude': 'claude_session_id_own', 'agy': 'agy_conversation_id_own',
|
||||
'hermes': 'hermes_conversation_id_own', 'cline': 'cline_conversation_id_own'}
|
||||
|
||||
def iso_root_of(s):
|
||||
iso = s.get('isolation')
|
||||
return iso.get('root') if isinstance(iso, dict) else None
|
||||
|
||||
# Ingest the merged state dictionary from stdin pipeline
|
||||
try:
|
||||
d = json.loads(os.environ.get('MAM_STATE_JSON', '{}'))
|
||||
except Exception:
|
||||
d = {}
|
||||
|
||||
running_ids = set()
|
||||
for s_item in d.get('herdr_sessions', []):
|
||||
if s_item.get('status') == 'running':
|
||||
if target and s_item.get('name') == target:
|
||||
continue
|
||||
for k in ['claude_session_id_own', 'agy_conversation_id_own', 'hermes_conversation_id_own', 'cline_conversation_id_own']:
|
||||
val = s_item.get(k)
|
||||
if val:
|
||||
running_ids.add(val)
|
||||
|
||||
orchestrator_ids = set(mam_orchestrator_uuids())
|
||||
if target:
|
||||
for s_item in d.get('herdr_sessions', []):
|
||||
if s_item.get('name') == target:
|
||||
own = mam_row_own_uuid(s_item)
|
||||
if own:
|
||||
orchestrator_ids.discard(own)
|
||||
|
||||
def emit(u):
|
||||
if u in running_ids or u in orchestrator_ids:
|
||||
return
|
||||
print(u)
|
||||
raise SystemExit(0)
|
||||
|
||||
# Fetch all sessions matching this workspace
|
||||
sessions = []
|
||||
for s in d.get('herdr_sessions', []):
|
||||
s_cwd = (s.get('pane') or {}).get('cwd', '') if isinstance(s, dict) else ''
|
||||
if s_cwd and (s_cwd == ws or os.path.realpath(s_cwd) == os.path.realpath(ws)):
|
||||
sessions.append(s)
|
||||
|
||||
if target:
|
||||
for s in sessions:
|
||||
if s.get('name') != target:
|
||||
continue
|
||||
iso = iso_root_of(s)
|
||||
cand = s.get(OWN_KEY.get(agent, ''), None)
|
||||
if cand and verify_session_uuid(ws, agent, cand, s, mode="revalidate"):
|
||||
emit(cand)
|
||||
if iso:
|
||||
key = workspace_key(ws)
|
||||
if agent == 'claude':
|
||||
for j in sorted(glob.glob(f"{iso}/projects/{key}/*.jsonl"), key=os.path.getmtime, reverse=True):
|
||||
cand = os.path.basename(j)[:-6]
|
||||
if cand and verify_session_uuid(ws, agent, cand, s):
|
||||
emit(cand)
|
||||
elif agent == 'agy':
|
||||
for j in sorted(glob.glob(f"{iso}/.gemini/antigravity-cli/conversations/*.db"), key=os.path.getmtime, reverse=True):
|
||||
cand = os.path.basename(j)[:-3]
|
||||
if cand and verify_session_uuid(ws, agent, cand, s):
|
||||
emit(cand)
|
||||
elif agent == 'hermes':
|
||||
hdb = f"{iso}/.hermes/state.db"
|
||||
if os.path.exists(hdb):
|
||||
try:
|
||||
conn = sqlite3.connect(hdb)
|
||||
r = conn.execute("SELECT id FROM sessions WHERE cwd=? ORDER BY started_at DESC LIMIT 1", (ws,)).fetchone()
|
||||
conn.close()
|
||||
if r:
|
||||
cand = r[0]
|
||||
if verify_session_uuid(ws, agent, cand, s):
|
||||
emit(cand)
|
||||
except Exception:
|
||||
pass
|
||||
elif agent == 'cline':
|
||||
for folder in sorted(glob.glob(f"{iso}/sessions/*"), key=os.path.getmtime, reverse=True):
|
||||
fn = os.path.basename(folder)
|
||||
if os.path.exists(f"{folder}/{fn}.json"):
|
||||
if verify_session_uuid(ws, agent, fn, s):
|
||||
emit(fn)
|
||||
print('')
|
||||
raise SystemExit(0)
|
||||
|
||||
for s in sessions:
|
||||
name = s.get('name', '')
|
||||
if agent == 'claude' and name.endswith('-creator-claude'):
|
||||
cand = s.get('claude_session_id_own')
|
||||
if cand and verify_session_uuid(ws, agent, cand, s, mode="revalidate"):
|
||||
emit(cand)
|
||||
if agent == 'agy' and name.endswith('-creator-agy'):
|
||||
cand = s.get('agy_conversation_id_own')
|
||||
if cand and verify_session_uuid(ws, agent, cand, s, mode="revalidate"):
|
||||
emit(cand)
|
||||
if agent == 'hermes' and name.endswith('-creator-hermes'):
|
||||
cand = s.get('hermes_conversation_id_own')
|
||||
if cand and verify_session_uuid(ws, agent, cand, s, mode="revalidate"):
|
||||
emit(cand)
|
||||
if agent == 'cline' and name.endswith('-creator-cline'):
|
||||
cand = s.get('cline_conversation_id_own')
|
||||
if cand and verify_session_uuid(ws, agent, cand, s, mode="revalidate"):
|
||||
emit(cand)
|
||||
|
||||
if agent == 'claude':
|
||||
key = workspace_key(ws)
|
||||
proj = f"{claude_project_dir}/{key}"
|
||||
if os.path.isdir(proj):
|
||||
for j in sorted(glob.glob(f"{proj}/*.jsonl"), key=os.path.getmtime, reverse=True):
|
||||
cand = os.path.basename(j)[:-6]
|
||||
if cand and verify_session_uuid(ws, agent, cand):
|
||||
emit(cand)
|
||||
elif agent == 'agy':
|
||||
lc = f"{home}/.gemini/antigravity-cli/cache/last_conversations.json"
|
||||
if os.path.exists(lc):
|
||||
cand = None
|
||||
try:
|
||||
cand = json.load(open(lc)).get(ws)
|
||||
except Exception:
|
||||
cand = None
|
||||
if cand and verify_session_uuid(ws, agent, cand):
|
||||
emit(cand)
|
||||
elif agent == 'hermes':
|
||||
hdb = f"{home}/.hermes/state.db"
|
||||
if os.path.exists(hdb):
|
||||
cand = None
|
||||
try:
|
||||
conn = sqlite3.connect(hdb)
|
||||
r = conn.execute("SELECT id FROM sessions WHERE cwd=? ORDER BY started_at DESC LIMIT 1", (ws,)).fetchone()
|
||||
conn.close()
|
||||
if r:
|
||||
cand = r[0]
|
||||
except Exception:
|
||||
cand = None
|
||||
if cand and verify_session_uuid(ws, agent, cand):
|
||||
emit(cand)
|
||||
elif agent == 'cline':
|
||||
sessions_dir = f"{home}/.cline/data/sessions"
|
||||
if os.path.isdir(sessions_dir):
|
||||
candidates = []
|
||||
for session_folder in glob.glob(f"{sessions_dir}/*"):
|
||||
if os.path.isdir(session_folder):
|
||||
folder_name = os.path.basename(session_folder)
|
||||
json_file = f"{session_folder}/{folder_name}.json"
|
||||
if os.path.exists(json_file):
|
||||
candidates.append(json_file)
|
||||
candidates.sort(key=os.path.getmtime, reverse=True)
|
||||
for j in candidates:
|
||||
cand = os.path.basename(j)[:-5]
|
||||
if cand and verify_session_uuid(ws, agent, cand):
|
||||
emit(cand)
|
||||
|
||||
ai = d.get('agent_identities') if isinstance(d, dict) else None
|
||||
if not isinstance(ai, dict) or not ai:
|
||||
ai = {}
|
||||
try:
|
||||
yaml_path = os.environ['YAML_PATH']
|
||||
db_path = os.path.splitext(yaml_path)[0] + '.db'
|
||||
if os.path.exists(db_path):
|
||||
conn = sqlite3.connect(db_path, timeout=60.0)
|
||||
conn.execute('PRAGMA busy_timeout = 60000')
|
||||
try:
|
||||
row = conn.execute('SELECT data FROM state WHERE id=1').fetchone()
|
||||
if row:
|
||||
ai = json.loads(row[0]).get('agent_identities') or {}
|
||||
except sqlite3.OperationalError:
|
||||
pass
|
||||
conn.close()
|
||||
elif os.path.exists(yaml_path):
|
||||
import yaml
|
||||
with open(yaml_path) as f:
|
||||
_ydoc = yaml.safe_load(f) or {}
|
||||
ai = _ydoc.get('agent_identities') or {}
|
||||
except Exception as e:
|
||||
print(f"WARN: tier-3 identity lookup failed: {e}", file=sys.stderr)
|
||||
if not isinstance(ai, dict):
|
||||
ai = {}
|
||||
|
||||
ai_agent = ai.get(agent) or {}
|
||||
if ai_agent.get('project_cwd') == ws:
|
||||
if agent == 'claude':
|
||||
cand = ai_agent.get('session_id')
|
||||
if cand and verify_session_uuid(ws, agent, cand, mode="revalidate"):
|
||||
emit(cand)
|
||||
elif agent == 'agy':
|
||||
cand = ai_agent.get('conversation_id')
|
||||
if cand and verify_session_uuid(ws, agent, cand, mode="revalidate"):
|
||||
emit(cand)
|
||||
elif agent == 'hermes':
|
||||
cand = ai_agent.get('session_id') or ai_agent.get('conversation_id')
|
||||
if cand and verify_session_uuid(ws, agent, cand, mode="revalidate"):
|
||||
emit(cand)
|
||||
elif agent == 'cline':
|
||||
cand = ai_agent.get('session_id') or ai_agent.get('conversation_id')
|
||||
if cand and verify_session_uuid(ws, agent, cand, mode="revalidate"):
|
||||
emit(cand)
|
||||
|
||||
print('')
|
||||
MAM_STATE_JSON="$(load_state_json)" WS_ABS="$abs" AGENT="$agent" TARGET_SESSION="$session_name" env_python "$AGENT_SESSIONS_YAML" <<'PYEOF'
|
||||
from lib_py.workspace_uuid import find_workspace_uuid_main
|
||||
find_workspace_uuid_main()
|
||||
PYEOF
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
# lib_py package initialization
|
||||
@@ -0,0 +1,214 @@
|
||||
# atomic_yaml.py — atomic YAML/DB update logic
|
||||
# Extracted from lib.sh atomic_dump_yaml PYEOF block
|
||||
|
||||
import os, sys, tempfile, shutil, glob, subprocess, json, sqlite3, fcntl
|
||||
from datetime import datetime, timezone
|
||||
import yaml
|
||||
|
||||
def atomic_dump_yaml_main():
|
||||
yaml_path = os.environ['YAML_PATH']
|
||||
db_path = os.path.splitext(yaml_path)[0] + '.db'
|
||||
|
||||
def _validate(d):
|
||||
if not isinstance(d, dict):
|
||||
raise SystemExit("VALIDATE: top-level is not a mapping")
|
||||
sessions = d.get('herdr_sessions', [])
|
||||
if not isinstance(sessions, list):
|
||||
raise SystemExit("VALIDATE: herdr_sessions is not a list")
|
||||
valid = {'running', 'terminated', 'archived', 'stopped'}
|
||||
for i, s in enumerate(sessions):
|
||||
if not isinstance(s, dict):
|
||||
raise SystemExit(f"VALIDATE: herdr_sessions[{i}] not a mapping")
|
||||
if not s.get('name') or not s.get('status'):
|
||||
raise SystemExit(f"VALIDATE: herdr_sessions[{i}] missing name/status")
|
||||
if s.get('role') is not None and (not isinstance(s['role'], str) or not s['role'].strip()):
|
||||
raise SystemExit(f"VALIDATE: herdr_sessions[{i}] {s.get('name')!r} role must be a non-empty string")
|
||||
if s['status'] not in valid:
|
||||
raise SystemExit(f"VALIDATE: herdr_sessions[{i}] {s.get('name')!r} bad status {s['status']!r}")
|
||||
if not isinstance(s.get('pane'), dict):
|
||||
raise SystemExit(f"VALIDATE: herdr_sessions[{i}] {s.get('name')!r} missing pane")
|
||||
iso = s.get('isolation')
|
||||
if iso is not None:
|
||||
if not isinstance(iso, dict) or not iso.get('uuid') or not iso.get('root'):
|
||||
raise SystemExit(f"VALIDATE: herdr_sessions[{i}] {s.get('name')!r} isolation block requires uuid/root")
|
||||
orc_uuids = d.get('orchestrator_uuids')
|
||||
if orc_uuids is not None:
|
||||
if not isinstance(orc_uuids, list):
|
||||
raise SystemExit("VALIDATE: orchestrator_uuids is not a list")
|
||||
seen_orc = set()
|
||||
for u in orc_uuids:
|
||||
if not isinstance(u, str) or not u.strip():
|
||||
raise SystemExit("VALIDATE: orchestrator_uuids items must be non-empty strings")
|
||||
if u in seen_orc:
|
||||
raise SystemExit("VALIDATE: orchestrator_uuids contains duplicate item")
|
||||
seen_orc.add(u)
|
||||
|
||||
def get_terminal_set(d):
|
||||
return {s.get('name'): s.get('status') for s in d.get('herdr_sessions', []) if s.get('status') in ('stopped', 'terminated', 'archived')}
|
||||
|
||||
def get_all_sessions_status(d):
|
||||
import hashlib
|
||||
res = {}
|
||||
for s in d.get('herdr_sessions', []):
|
||||
name = s.get('name')
|
||||
ser = json.dumps(s, sort_keys=True)
|
||||
res[name] = hashlib.sha256(ser.encode('utf-8')).hexdigest()
|
||||
orc_uuids = d.get('orchestrator_uuids')
|
||||
if orc_uuids is not None:
|
||||
ser_orc = json.dumps(orc_uuids, sort_keys=True)
|
||||
res['__orchestrator_uuids__'] = hashlib.sha256(ser_orc.encode('utf-8')).hexdigest()
|
||||
return res
|
||||
|
||||
os.makedirs(os.path.dirname(db_path) or '.', exist_ok=True)
|
||||
_flock_f = open(db_path + '.lock', 'w')
|
||||
fcntl.flock(_flock_f, fcntl.LOCK_EX)
|
||||
conn = sqlite3.connect(db_path, timeout=60.0)
|
||||
conn.execute('PRAGMA busy_timeout = 60000')
|
||||
|
||||
for f in [db_path, db_path + '-wal', db_path + '-shm']:
|
||||
if os.path.exists(f):
|
||||
try:
|
||||
os.chmod(f, 0o600)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
is_nfs = os.environ.get('MAM_IS_NFS') == 'true'
|
||||
if is_nfs:
|
||||
conn.execute('PRAGMA journal_mode=DELETE')
|
||||
else:
|
||||
conn.execute('PRAGMA journal_mode=WAL')
|
||||
|
||||
try:
|
||||
for _beg_attempt in range(300):
|
||||
try:
|
||||
conn.execute('BEGIN IMMEDIATE')
|
||||
break
|
||||
except sqlite3.OperationalError:
|
||||
import time
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
conn.execute('BEGIN IMMEDIATE')
|
||||
conn.execute('CREATE TABLE IF NOT EXISTS state (id INTEGER PRIMARY KEY, data TEXT)')
|
||||
conn.execute('CREATE TABLE IF NOT EXISTS sessions (name TEXT PRIMARY KEY, status TEXT, pane_cwd TEXT, data JSON)')
|
||||
conn.execute('CREATE INDEX IF NOT EXISTS idx_sessions_pane_cwd ON sessions(pane_cwd)')
|
||||
|
||||
row = conn.execute('SELECT data FROM state WHERE id=1').fetchone()
|
||||
if row:
|
||||
d = json.loads(row[0])
|
||||
else:
|
||||
if os.path.exists(yaml_path):
|
||||
with open(yaml_path) as f:
|
||||
d = yaml.safe_load(f) or {}
|
||||
else:
|
||||
d = {}
|
||||
|
||||
db_sessions = []
|
||||
cursor = conn.execute('SELECT name, status, pane_cwd, data FROM sessions')
|
||||
for s_row in cursor.fetchall():
|
||||
s_data = json.loads(s_row[3])
|
||||
s_data['name'] = s_row[0]
|
||||
s_data['status'] = s_row[1]
|
||||
if 'pane' not in s_data:
|
||||
s_data['pane'] = {}
|
||||
s_data['pane']['cwd'] = s_row[2]
|
||||
db_sessions.append(s_data)
|
||||
|
||||
if db_sessions:
|
||||
d['herdr_sessions'] = db_sessions
|
||||
elif 'herdr_sessions' not in d:
|
||||
d['herdr_sessions'] = []
|
||||
|
||||
old_sessions = get_all_sessions_status(d)
|
||||
old_roles = {s.get('name'): s.get('role') for s in db_sessions if s.get('role')}
|
||||
|
||||
# --- caller mutation (module scope: sees d, yaml, os, glob, subprocess) ---
|
||||
mutation_ns = dict(globals())
|
||||
mutation_ns.update(locals())
|
||||
exec(compile(os.environ['AGENT_SESSIONS_MUTATION'], '<mutation>', 'exec'), mutation_ns)
|
||||
if 'd' in mutation_ns:
|
||||
d = mutation_ns['d']
|
||||
|
||||
for s in d.get('herdr_sessions', []):
|
||||
name = s.get('name')
|
||||
if name in old_roles and s.get('role') != old_roles[name]:
|
||||
raise SystemExit(f"VALIDATE: role of session {name!r} cannot be modified from {old_roles[name]!r} to {s.get('role')!r}")
|
||||
|
||||
running_keys = ['claude_session_id_own', 'agy_conversation_id_own', 'hermes_conversation_id_own', 'cline_conversation_id_own']
|
||||
id_to_session = {}
|
||||
for s in d.get('herdr_sessions', []):
|
||||
if s.get('status') == 'running':
|
||||
s_name = s.get('name')
|
||||
for k in running_keys:
|
||||
v = s.get(k)
|
||||
if v:
|
||||
if v in id_to_session and id_to_session[v] != s_name:
|
||||
raise SystemExit(f"VALIDATE: Duplicate running conversation ID {v!r} detected between {id_to_session[v]!r} and {s_name!r}")
|
||||
id_to_session[v] = s_name
|
||||
|
||||
_validate(d)
|
||||
|
||||
d_state = {k: v for k, v in d.items() if k != 'herdr_sessions'}
|
||||
conn.execute('REPLACE INTO state (id, data) VALUES (1, ?)', (json.dumps(d_state),))
|
||||
|
||||
current_names = []
|
||||
for s in d.get('herdr_sessions', []):
|
||||
name = s.get('name')
|
||||
status = s.get('status')
|
||||
pane_cwd = (s.get('pane') or {}).get('cwd', '')
|
||||
conn.execute('REPLACE INTO sessions (name, status, pane_cwd, data) VALUES (?, ?, ?, ?)',
|
||||
(name, status, pane_cwd, json.dumps(s)))
|
||||
current_names.append(name)
|
||||
|
||||
if current_names:
|
||||
placeholders = ','.join('?' for _ in current_names)
|
||||
conn.execute(f'DELETE FROM sessions WHERE name NOT IN ({placeholders})', current_names)
|
||||
else:
|
||||
conn.execute('DELETE FROM sessions')
|
||||
|
||||
new_sessions = get_all_sessions_status(d)
|
||||
|
||||
conn.commit()
|
||||
|
||||
if new_sessions != old_sessions:
|
||||
if os.path.exists(yaml_path):
|
||||
try:
|
||||
shutil.copy2(yaml_path, yaml_path + '.bak')
|
||||
except Exception:
|
||||
pass
|
||||
dir_ = os.path.dirname(yaml_path) or '.'
|
||||
fd, tmp = tempfile.mkstemp(dir=dir_, prefix='.agent-sessions.', suffix='.tmp')
|
||||
try:
|
||||
with os.fdopen(fd, 'w') as f:
|
||||
yaml.safe_dump(d, f, default_flow_style=False, sort_keys=False,
|
||||
allow_unicode=True, width=4096)
|
||||
os.replace(tmp, yaml_path)
|
||||
except Exception:
|
||||
if os.path.exists(tmp):
|
||||
os.remove(tmp)
|
||||
raise
|
||||
|
||||
try:
|
||||
conn.execute('PRAGMA wal_checkpoint(TRUNCATE)')
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
conn.rollback()
|
||||
raise
|
||||
finally:
|
||||
conn.close()
|
||||
try:
|
||||
os.chmod(db_path, 0o600)
|
||||
wal = db_path + '-wal'
|
||||
if os.path.exists(wal): os.chmod(wal, 0o600)
|
||||
shm = db_path + '-shm'
|
||||
if os.path.exists(shm): os.chmod(shm, 0o600)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
fcntl.flock(_flock_f, fcntl.LOCK_UN)
|
||||
_flock_f.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if __name__ == '__main__':
|
||||
atomic_dump_yaml_main()
|
||||
@@ -0,0 +1,204 @@
|
||||
# verify_session.py — session UUID verification logic
|
||||
# Extracted from lib.sh VERIFY_SESSION_PYTHON block
|
||||
|
||||
_MAM_ORC_CACHE = None
|
||||
|
||||
def mam_orchestrator_uuids():
|
||||
global _MAM_ORC_CACHE
|
||||
if _MAM_ORC_CACHE is not None:
|
||||
return _MAM_ORC_CACHE
|
||||
import os, sys, json, sqlite3, yaml
|
||||
override = os.environ.get("MAM_ORCHESTRATOR_UUIDS")
|
||||
if override is not None:
|
||||
if not override.strip():
|
||||
_MAM_ORC_CACHE = []
|
||||
return _MAM_ORC_CACHE
|
||||
if override.strip().startswith("["):
|
||||
try:
|
||||
parsed = json.loads(override)
|
||||
if isinstance(parsed, list):
|
||||
_MAM_ORC_CACHE = [str(x) for x in parsed if x]
|
||||
return _MAM_ORC_CACHE
|
||||
except Exception:
|
||||
pass
|
||||
_MAM_ORC_CACHE = [x.strip() for x in override.split(",") if x.strip()]
|
||||
return _MAM_ORC_CACHE
|
||||
|
||||
res = []
|
||||
d_obj = None
|
||||
try:
|
||||
if "MAM_STATE_JSON" in os.environ and os.environ.get("MAM_STATE_JSON"):
|
||||
d_obj = json.loads(os.environ.get("MAM_STATE_JSON"))
|
||||
except Exception:
|
||||
d_obj = None
|
||||
|
||||
if d_obj is None or "orchestrator_uuids" not in d_obj:
|
||||
yaml_p = os.environ.get("AGENT_SESSIONS_YAML") or os.environ.get("YAML_PATH")
|
||||
if yaml_p:
|
||||
db_p = os.path.splitext(yaml_p)[0] + ".db"
|
||||
if os.path.exists(db_p):
|
||||
try:
|
||||
conn = sqlite3.connect(db_p, timeout=5.0)
|
||||
r = conn.execute("SELECT data FROM state WHERE id=1").fetchone()
|
||||
if r and r[0]:
|
||||
d_obj = json.loads(r[0])
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
if (d_obj is None or "orchestrator_uuids" not in d_obj) and os.path.exists(yaml_p):
|
||||
try:
|
||||
with open(yaml_p) as f:
|
||||
d_obj = yaml.safe_load(f) or {}
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
raw = d_obj.get("orchestrator_uuids") if isinstance(d_obj, dict) else None
|
||||
if raw is not None:
|
||||
if isinstance(raw, list) and all(isinstance(x, str) and x.strip() for x in raw):
|
||||
res = list(raw)
|
||||
else:
|
||||
sys.stderr.write("WARNING: malformed orchestrator_uuids in state, disabling orchestrator exclusion gate\n")
|
||||
res = []
|
||||
_MAM_ORC_CACHE = res
|
||||
return _MAM_ORC_CACHE
|
||||
|
||||
def mam_row_own_uuid(row):
|
||||
if not isinstance(row, dict):
|
||||
return None
|
||||
for k in ["claude_session_id_own", "agy_conversation_id_own", "hermes_conversation_id_own", "cline_conversation_id_own"]:
|
||||
v = row.get(k)
|
||||
if v:
|
||||
return v
|
||||
return None
|
||||
|
||||
def workspace_key(path):
|
||||
import os
|
||||
try:
|
||||
p = os.path.realpath(path)
|
||||
except Exception:
|
||||
p = path
|
||||
return p.replace("/", "-").replace("_", "-")
|
||||
|
||||
def verify_session_uuid(ws, agent, uuid, row=None, home_dir=None, claude_dir=None, mode="discover"):
|
||||
import os, json, sqlite3
|
||||
home = home_dir or os.environ.get("HOME_DIR", "")
|
||||
c_dir = claude_dir or os.environ.get("CLAUDE_PROJECT_DIR", f"{home}/.claude/projects")
|
||||
row = row or {}
|
||||
_iso = row.get("isolation")
|
||||
iso_root = _iso.get("root") if isinstance(_iso, dict) and _iso.get("root") else None
|
||||
|
||||
epoch = row.get("herdr_session_epoch", 0) if mode == "discover" else 0
|
||||
cwd = row.get("pane", {}).get("cwd", "") or ws
|
||||
|
||||
if workspace_key(cwd) != workspace_key(ws):
|
||||
return False
|
||||
|
||||
if mode == "discover" and uuid in mam_orchestrator_uuids():
|
||||
if uuid != mam_row_own_uuid(row):
|
||||
return False
|
||||
|
||||
if (mode == "revalidate" and row.get("session_id_source") == "assigned"
|
||||
and not row.get("session_id_verified")):
|
||||
return True
|
||||
|
||||
if agent == "claude":
|
||||
base = (iso_root + "/projects") if iso_root else c_dir
|
||||
key = workspace_key(ws)
|
||||
path = f"{base}/{key}/{uuid}.jsonl"
|
||||
if not os.path.exists(path):
|
||||
return False
|
||||
if epoch and os.path.getmtime(path) < epoch:
|
||||
return False
|
||||
try:
|
||||
valid_session = False
|
||||
found_cwd = None
|
||||
with open(path) as f:
|
||||
for _ in range(50):
|
||||
line = f.readline()
|
||||
if not line:
|
||||
break
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
payload = json.loads(line)
|
||||
if payload.get("sessionId") == uuid:
|
||||
valid_session = True
|
||||
if payload.get("cwd"):
|
||||
found_cwd = payload.get("cwd")
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
if not valid_session:
|
||||
return False
|
||||
if found_cwd and workspace_key(found_cwd) != workspace_key(cwd):
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
elif agent == "agy":
|
||||
base = f"{iso_root or home}/.gemini/antigravity-cli/conversations"
|
||||
path = f"{base}/{uuid}.db"
|
||||
if not os.path.exists(path):
|
||||
return False
|
||||
if epoch and os.path.getmtime(path) < epoch:
|
||||
return False
|
||||
if mode == "discover":
|
||||
lc = f"{iso_root or home}/.gemini/antigravity-cli/cache/last_conversations.json"
|
||||
cache_match = False
|
||||
if os.path.exists(lc):
|
||||
try:
|
||||
with open(lc) as f:
|
||||
lc_data = json.load(f)
|
||||
cache_match = (lc_data.get(cwd) == uuid)
|
||||
except Exception:
|
||||
cache_match = False
|
||||
if not cache_match:
|
||||
if uuid in (row.get("_sibling_claimed_uuids") or []):
|
||||
return False
|
||||
try:
|
||||
conn = sqlite3.connect(path)
|
||||
r = conn.execute("SELECT count(*) FROM steps").fetchone()
|
||||
conn.close()
|
||||
if not r or r[0] < 1:
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
elif agent == "hermes":
|
||||
hdb = f"{iso_root or home}/.hermes/state.db"
|
||||
if not os.path.exists(hdb):
|
||||
return False
|
||||
if epoch and os.path.getmtime(hdb) < epoch:
|
||||
return False
|
||||
try:
|
||||
conn = sqlite3.connect(hdb)
|
||||
r = conn.execute("SELECT cwd FROM sessions WHERE id=?", (uuid,)).fetchone()
|
||||
conn.close()
|
||||
if not r:
|
||||
return False
|
||||
found_cwd = r[0]
|
||||
if found_cwd and workspace_key(found_cwd) != workspace_key(cwd):
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
elif agent == "cline":
|
||||
base = (iso_root + "/sessions") if iso_root else f"{home}/.cline/data/sessions"
|
||||
path = f"{base}/{uuid}/{uuid}.json"
|
||||
if not os.path.exists(path):
|
||||
return False
|
||||
if epoch and os.path.getmtime(path) < epoch:
|
||||
return False
|
||||
try:
|
||||
with open(path) as f:
|
||||
sdata = json.load(f)
|
||||
if sdata.get("session_id") != uuid:
|
||||
return False
|
||||
found_cwd = sdata.get("cwd") or sdata.get("workspace_root")
|
||||
if found_cwd and workspace_key(found_cwd) != workspace_key(cwd):
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
return True
|
||||
@@ -0,0 +1,218 @@
|
||||
# workspace_uuid.py — workspace UUID discovery logic
|
||||
# Extracted from lib.sh find_workspace_uuid PYEOF block
|
||||
|
||||
import os, sys, json, glob, sqlite3
|
||||
from lib_py.verify_session import verify_session_uuid, workspace_key, mam_orchestrator_uuids, mam_row_own_uuid
|
||||
|
||||
OWN_KEY = {
|
||||
'claude': 'claude_session_id_own',
|
||||
'agy': 'agy_conversation_id_own',
|
||||
'hermes': 'hermes_conversation_id_own',
|
||||
'cline': 'cline_conversation_id_own'
|
||||
}
|
||||
|
||||
def iso_root_of(s):
|
||||
iso = s.get('isolation')
|
||||
return iso.get('root') if isinstance(iso, dict) else None
|
||||
|
||||
def find_workspace_uuid_main():
|
||||
ws = os.environ['WS_ABS']
|
||||
agent = os.environ['AGENT']
|
||||
home = os.environ['HOME_DIR']
|
||||
claude_project_dir = os.environ.get('CLAUDE_PROJECT_DIR', f"{home}/.claude/projects")
|
||||
target = os.environ.get('TARGET_SESSION', '')
|
||||
|
||||
try:
|
||||
d = json.loads(os.environ.get('MAM_STATE_JSON', '{}'))
|
||||
except Exception:
|
||||
d = {}
|
||||
|
||||
running_ids = set()
|
||||
for s_item in d.get('herdr_sessions', []):
|
||||
if s_item.get('status') == 'running':
|
||||
if target and s_item.get('name') == target:
|
||||
continue
|
||||
for k in ['claude_session_id_own', 'agy_conversation_id_own', 'hermes_conversation_id_own', 'cline_conversation_id_own']:
|
||||
val = s_item.get(k)
|
||||
if val:
|
||||
running_ids.add(val)
|
||||
|
||||
orchestrator_ids = set(mam_orchestrator_uuids())
|
||||
if target:
|
||||
for s_item in d.get('herdr_sessions', []):
|
||||
if s_item.get('name') == target:
|
||||
own = mam_row_own_uuid(s_item)
|
||||
if own:
|
||||
orchestrator_ids.discard(own)
|
||||
|
||||
def emit(u):
|
||||
if u in running_ids or u in orchestrator_ids:
|
||||
return
|
||||
print(u)
|
||||
sys.exit(0)
|
||||
|
||||
# Fetch all sessions matching this workspace
|
||||
sessions = []
|
||||
for s in d.get('herdr_sessions', []):
|
||||
s_cwd = (s.get('pane') or {}).get('cwd', '') if isinstance(s, dict) else ''
|
||||
if s_cwd and (s_cwd == ws or os.path.realpath(s_cwd) == os.path.realpath(ws)):
|
||||
sessions.append(s)
|
||||
|
||||
if target:
|
||||
for s in sessions:
|
||||
if s.get('name') != target:
|
||||
continue
|
||||
iso = iso_root_of(s)
|
||||
cand = s.get(OWN_KEY.get(agent, ''), None)
|
||||
if cand and verify_session_uuid(ws, agent, cand, s, mode="revalidate"):
|
||||
emit(cand)
|
||||
if iso:
|
||||
key = workspace_key(ws)
|
||||
if agent == 'claude':
|
||||
for j in sorted(glob.glob(f"{iso}/projects/{key}/*.jsonl"), key=os.path.getmtime, reverse=True):
|
||||
cand = os.path.basename(j)[:-6]
|
||||
if cand and verify_session_uuid(ws, agent, cand, s):
|
||||
emit(cand)
|
||||
elif agent == 'agy':
|
||||
for j in sorted(glob.glob(f"{iso}/.gemini/antigravity-cli/conversations/*.db"), key=os.path.getmtime, reverse=True):
|
||||
cand = os.path.basename(j)[:-3]
|
||||
if cand and verify_session_uuid(ws, agent, cand, s):
|
||||
emit(cand)
|
||||
elif agent == 'hermes':
|
||||
hdb = f"{iso}/.hermes/state.db"
|
||||
if os.path.exists(hdb):
|
||||
try:
|
||||
conn = sqlite3.connect(hdb)
|
||||
r = conn.execute("SELECT id FROM sessions WHERE cwd=? ORDER BY started_at DESC LIMIT 1", (ws,)).fetchone()
|
||||
conn.close()
|
||||
if r:
|
||||
cand = r[0]
|
||||
if verify_session_uuid(ws, agent, cand, s):
|
||||
emit(cand)
|
||||
except Exception:
|
||||
pass
|
||||
elif agent == 'cline':
|
||||
for folder in sorted(glob.glob(f"{iso}/sessions/*"), key=os.path.getmtime, reverse=True):
|
||||
fn = os.path.basename(folder)
|
||||
if os.path.exists(f"{folder}/{fn}.json"):
|
||||
if verify_session_uuid(ws, agent, fn, s):
|
||||
emit(fn)
|
||||
print('')
|
||||
sys.exit(0)
|
||||
|
||||
for s in sessions:
|
||||
name = s.get('name', '')
|
||||
if agent == 'claude' and name.endswith('-creator-claude'):
|
||||
cand = s.get('claude_session_id_own')
|
||||
if cand and verify_session_uuid(ws, agent, cand, s, mode="revalidate"):
|
||||
emit(cand)
|
||||
if agent == 'agy' and name.endswith('-creator-agy'):
|
||||
cand = s.get('agy_conversation_id_own')
|
||||
if cand and verify_session_uuid(ws, agent, cand, s, mode="revalidate"):
|
||||
emit(cand)
|
||||
if agent == 'hermes' and name.endswith('-creator-hermes'):
|
||||
cand = s.get('hermes_conversation_id_own')
|
||||
if cand and verify_session_uuid(ws, agent, cand, s, mode="revalidate"):
|
||||
emit(cand)
|
||||
if agent == 'cline' and name.endswith('-creator-cline'):
|
||||
cand = s.get('cline_conversation_id_own')
|
||||
if cand and verify_session_uuid(ws, agent, cand, s, mode="revalidate"):
|
||||
emit(cand)
|
||||
|
||||
if agent == 'claude':
|
||||
key = workspace_key(ws)
|
||||
proj = f"{claude_project_dir}/{key}"
|
||||
if os.path.isdir(proj):
|
||||
for j in sorted(glob.glob(f"{proj}/*.jsonl"), key=os.path.getmtime, reverse=True):
|
||||
cand = os.path.basename(j)[:-6]
|
||||
if cand and verify_session_uuid(ws, agent, cand):
|
||||
emit(cand)
|
||||
elif agent == 'agy':
|
||||
lc = f"{home}/.gemini/antigravity-cli/cache/last_conversations.json"
|
||||
if os.path.exists(lc):
|
||||
cand = None
|
||||
try:
|
||||
cand = json.load(open(lc)).get(ws)
|
||||
except Exception:
|
||||
cand = None
|
||||
if cand and verify_session_uuid(ws, agent, cand):
|
||||
emit(cand)
|
||||
elif agent == 'hermes':
|
||||
hdb = f"{home}/.hermes/state.db"
|
||||
if os.path.exists(hdb):
|
||||
cand = None
|
||||
try:
|
||||
conn = sqlite3.connect(hdb)
|
||||
r = conn.execute("SELECT id FROM sessions WHERE cwd=? ORDER BY started_at DESC LIMIT 1", (ws,)).fetchone()
|
||||
conn.close()
|
||||
if r:
|
||||
cand = r[0]
|
||||
except Exception:
|
||||
cand = None
|
||||
if cand and verify_session_uuid(ws, agent, cand):
|
||||
emit(cand)
|
||||
elif agent == 'cline':
|
||||
sessions_dir = f"{home}/.cline/data/sessions"
|
||||
if os.path.isdir(sessions_dir):
|
||||
candidates = []
|
||||
for session_folder in glob.glob(f"{sessions_dir}/*"):
|
||||
if os.path.isdir(session_folder):
|
||||
folder_name = os.path.basename(session_folder)
|
||||
json_file = f"{session_folder}/{folder_name}.json"
|
||||
if os.path.exists(json_file):
|
||||
candidates.append(json_file)
|
||||
candidates.sort(key=os.path.getmtime, reverse=True)
|
||||
for j in candidates:
|
||||
cand = os.path.basename(j)[:-5]
|
||||
if cand and verify_session_uuid(ws, agent, cand):
|
||||
emit(cand)
|
||||
|
||||
ai = d.get('agent_identities') if isinstance(d, dict) else None
|
||||
if not isinstance(ai, dict) or not ai:
|
||||
ai = {}
|
||||
try:
|
||||
yaml_path = os.environ['YAML_PATH']
|
||||
db_path = os.path.splitext(yaml_path)[0] + '.db'
|
||||
if os.path.exists(db_path):
|
||||
conn = sqlite3.connect(db_path, timeout=60.0)
|
||||
conn.execute('PRAGMA busy_timeout = 60000')
|
||||
try:
|
||||
row = conn.execute('SELECT data FROM state WHERE id=1').fetchone()
|
||||
if row:
|
||||
ai = json.loads(row[0]).get('agent_identities') or {}
|
||||
except sqlite3.OperationalError:
|
||||
pass
|
||||
conn.close()
|
||||
elif os.path.exists(yaml_path):
|
||||
import yaml
|
||||
with open(yaml_path) as f:
|
||||
_ydoc = yaml.safe_load(f) or {}
|
||||
ai = _ydoc.get('agent_identities') or {}
|
||||
except Exception as e:
|
||||
print(f"WARN: tier-3 identity lookup failed: {e}", file=sys.stderr)
|
||||
if not isinstance(ai, dict):
|
||||
ai = {}
|
||||
|
||||
ai_agent = ai.get(agent) or {}
|
||||
if ai_agent.get('project_cwd') == ws:
|
||||
if agent == 'claude':
|
||||
cand = ai_agent.get('session_id')
|
||||
if cand and verify_session_uuid(ws, agent, cand, mode="revalidate"):
|
||||
emit(cand)
|
||||
elif agent == 'agy':
|
||||
cand = ai_agent.get('conversation_id')
|
||||
if cand and verify_session_uuid(ws, agent, cand, mode="revalidate"):
|
||||
emit(cand)
|
||||
elif agent == 'hermes':
|
||||
cand = ai_agent.get('session_id') or ai_agent.get('conversation_id')
|
||||
if cand and verify_session_uuid(ws, agent, cand, mode="revalidate"):
|
||||
emit(cand)
|
||||
elif agent == 'cline':
|
||||
cand = ai_agent.get('session_id') or ai_agent.get('conversation_id')
|
||||
if cand and verify_session_uuid(ws, agent, cand, mode="revalidate"):
|
||||
emit(cand)
|
||||
|
||||
print('')
|
||||
|
||||
if __name__ == '__main__':
|
||||
find_workspace_uuid_main()
|
||||
@@ -82,6 +82,10 @@ fi
|
||||
# Preflight
|
||||
[ -n "$WORKSPACE" ] || { echo "ERROR: --workspace required" >&2; usage; exit 2; }
|
||||
[ -n "$AGENT" ] || { echo "ERROR: --agent required" >&2; usage; exit 2; }
|
||||
case "$AGENT" in
|
||||
claude|agy|hermes|cline) ;;
|
||||
*) echo "ERROR: --agent must be claude, agy, hermes or cline, got: $AGENT" >&2; exit 2 ;;
|
||||
esac
|
||||
[ -n "$ROLE" ] || { echo "ERROR: --role required" >&2; usage; exit 2; }
|
||||
[ -d "$WORKSPACE" ] || { echo "ERROR: workspace $WORKSPACE not a directory" >&2; exit 1; }
|
||||
# B-3: `command -v herdr` matches lib.sh's herdr() function and `type -P herdr`
|
||||
@@ -117,7 +121,7 @@ fi
|
||||
|
||||
# 세션 이름 — lib.sh::derive_session_name 이 단일 소스 (P0-A)
|
||||
if [ -z "$SESSION_NAME" ]; then
|
||||
SESSION_NAME="$(derive_session_name "$WORKSPACE" "$AGENT")"
|
||||
SESSION_NAME="$(derive_session_name "$WORKSPACE" "$AGENT" "$ROLE")"
|
||||
fi
|
||||
|
||||
# 이미 살아있으면 실패
|
||||
@@ -168,6 +172,7 @@ spawn() {
|
||||
claude)
|
||||
if { [ -x "$WRAPPER" ] && [ "$(basename "$WRAPPER")" != "claude" ]; } || [ "$USE_WRAPPER" = "1" ]; then
|
||||
SESSION_UUID=""
|
||||
CMD_FULL="${RESOLVED_BIN} --dangerously-skip-permissions"
|
||||
nohup "$WRAPPER" >/dev/null 2>&1 &
|
||||
disown
|
||||
else
|
||||
|
||||
@@ -491,16 +491,40 @@ if herdr_confirmed:
|
||||
workspace_root = os.path.abspath(os.path.join(os.path.dirname(yaml_path), '..'))
|
||||
if os.path.exists(os.path.join(workspace_root, '.mam', f"purging-{name}")):
|
||||
continue
|
||||
if name.endswith('-creator-claude'):
|
||||
agent = 'claude'
|
||||
elif name.endswith('-creator-agy'):
|
||||
agent = 'agy'
|
||||
elif name.endswith('-creator-hermes'):
|
||||
agent = 'hermes'
|
||||
elif name.endswith('-creator-cline'):
|
||||
agent = 'cline'
|
||||
else:
|
||||
agent = None
|
||||
role = 'creator'
|
||||
for r_name in ('creator', 'planner', 'reviewer'):
|
||||
for a_name in ('claude', 'agy', 'hermes', 'cline'):
|
||||
if name.endswith(f"-{r_name}-{a_name}"):
|
||||
role = r_name
|
||||
agent = a_name
|
||||
break
|
||||
if agent:
|
||||
break
|
||||
|
||||
if not agent:
|
||||
# Check MAM_MANAGED env marker from pane process environment if available
|
||||
srv_opt = t.get('server', 'default')
|
||||
pm_check = pane_meta(name, srv_opt)
|
||||
if pm_check and pm_check.get('pid'):
|
||||
try:
|
||||
pid_val = pm_check['pid']
|
||||
env_output = subprocess.check_output(['ps', 'eww', '-p', str(pid_val)], text=True, stderr=subprocess.DEVNULL)
|
||||
for tok in env_output.split():
|
||||
if tok.startswith('MAM_MANAGED='):
|
||||
managed_path = tok.split('=', 1)[1]
|
||||
if os.path.realpath(managed_path) == os.path.realpath(workspace_root):
|
||||
for a_name in ('claude', 'agy', 'hermes', 'cline'):
|
||||
if a_name in pm_check.get('cmd', '') or a_name in pm_check.get('cmd_full', ''):
|
||||
agent = a_name
|
||||
break
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not agent:
|
||||
continue
|
||||
|
||||
srv = t.get('server', 'default')
|
||||
pm = pane_meta(name, srv)
|
||||
if not pm:
|
||||
@@ -528,6 +552,7 @@ if herdr_confirmed:
|
||||
entry = {
|
||||
'name': name,
|
||||
'status': 'running',
|
||||
'role': role,
|
||||
'herdr_session_created_at': datetime.fromtimestamp(created_epoch, tz=timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ'),
|
||||
'herdr_session_epoch': created_epoch,
|
||||
'herdr_session': srv,
|
||||
|
||||
@@ -122,6 +122,9 @@ detect_nearest_agent() {
|
||||
agy)
|
||||
detected_id=$(echo "$cmd_line" | grep -oE '--conversation[[:space:]=]+[^[:space:]]+' | head -n 1 | sed -E 's/^--conversation[[:space:]=]+//' || true)
|
||||
;;
|
||||
hermes)
|
||||
detected_id=$(echo "$cmd_line" | grep -oE '(--resume|--session)[[:space:]=]+[^[:space:]]+' | head -n 1 | sed -E 's/^(--resume|--session)[[:space:]=]+//' || true)
|
||||
;;
|
||||
cline)
|
||||
detected_id=$(echo "$cmd_line" | grep -oE '(--id|--session-id)[[:space:]=]+[^[:space:]]+' | head -n 1 | sed -E 's/^(--id|--session-id)[[:space:]=]+//' || true)
|
||||
;;
|
||||
|
||||
@@ -52,18 +52,49 @@ def resume_on_disk(s):
|
||||
# workspace-SCOPED check only — per-row own id, never a global identity (P0-C)
|
||||
name = s.get('name', '')
|
||||
cwd = (s.get('pane') or {}).get('cwd', '')
|
||||
if name.endswith('-creator-claude'):
|
||||
agent = None
|
||||
for a in ('claude', 'agy', 'hermes', 'cline'):
|
||||
if any(name.endswith(f'-{r}-{a}') for r in ('creator', 'planner', 'reviewer')) or name.endswith(f'-{a}'):
|
||||
agent = a
|
||||
break
|
||||
if not agent:
|
||||
for a in ('claude', 'agy', 'hermes', 'cline'):
|
||||
if f"-{a}" in name or f"_{a}" in name:
|
||||
agent = a
|
||||
break
|
||||
|
||||
if agent == 'claude':
|
||||
u = s.get('claude_session_id_own')
|
||||
if u:
|
||||
key = cwd.replace('/', '-').replace('_', '-')
|
||||
return 'yes' if os.path.exists(f"{claude_project_dir}/{key}/{u}.jsonl") else 'MISSING'
|
||||
key = cwd.replace('/', '-').replace('_', '-')
|
||||
return 'scan' if glob.glob(f"{claude_project_dir}/{key}/*.jsonl") else 'no'
|
||||
if name.endswith('-creator-agy'):
|
||||
if agent == 'agy':
|
||||
u = s.get('agy_conversation_id_own')
|
||||
if u:
|
||||
return 'yes' if os.path.exists(f"{home}/.gemini/antigravity-cli/conversations/{u}.db") else 'MISSING'
|
||||
return 'no'
|
||||
if agent == 'hermes':
|
||||
u = s.get('hermes_conversation_id_own')
|
||||
if u:
|
||||
hdb = f"{home}/.hermes/state.db"
|
||||
if not os.path.exists(hdb):
|
||||
return 'MISSING'
|
||||
try:
|
||||
import sqlite3
|
||||
conn = sqlite3.connect(hdb)
|
||||
r = conn.execute("SELECT 1 FROM sessions WHERE id=?", (u,)).fetchone()
|
||||
conn.close()
|
||||
return 'yes' if r else 'MISSING'
|
||||
except Exception:
|
||||
return 'MISSING'
|
||||
return 'no'
|
||||
if agent == 'cline':
|
||||
u = s.get('cline_conversation_id_own')
|
||||
if u:
|
||||
return 'yes' if os.path.exists(f"{home}/.cline/data/sessions/{u}/{u}.json") else 'MISSING'
|
||||
return 'no'
|
||||
return '?'
|
||||
|
||||
|
||||
|
||||
@@ -91,10 +91,10 @@ export HERDR_SESSION_NAME
|
||||
# --agent 미지정 시 이름 suffix 로 fallback (P1-F)
|
||||
if [ -z "$AGENT" ]; then
|
||||
case "$SESSION_NAME" in
|
||||
*-creator-claude) AGENT=claude ;;
|
||||
*-creator-agy) AGENT=agy ;;
|
||||
*-creator-hermes) AGENT=hermes ;;
|
||||
*-creator-cline) AGENT=cline ;;
|
||||
*-creator-claude|*-planner-claude|*-reviewer-claude) AGENT=claude ;;
|
||||
*-creator-agy|*-planner-agy|*-reviewer-agy) AGENT=agy ;;
|
||||
*-creator-hermes|*-planner-hermes|*-reviewer-hermes) AGENT=hermes ;;
|
||||
*-creator-cline|*-planner-cline|*-reviewer-cline) AGENT=cline ;;
|
||||
*) echo "ERROR: cannot infer agent from '$SESSION_NAME'; pass --agent" >&2; exit 2 ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user