feat(mux): implement onboarding prompt initialization, 4-tier UUID validation, and TUI 1:1 cross-matching

This commit is contained in:
2026-07-23 09:41:16 +09:00
parent c65b194d88
commit 15ffc8f6bb
9 changed files with 757 additions and 174 deletions
+196 -94
View File
@@ -874,6 +874,171 @@ 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='
def workspace_key(path):
return path.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 {}
epoch = row.get("herdr_session_epoch", 0)
cwd = row.get("pane", {}).get("cwd", "") or ws
iso = row.get("isolation", {}).get("root") if isinstance(row.get("isolation"), dict) else None
if workspace_key(cwd) != workspace_key(ws):
return False
if agent == "claude":
base = f"{iso}/projects" if iso 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:
with open(path) as f:
first = f.readline().strip()
if not first:
return False
payload = json.loads(first)
if payload.get("sessionId") != uuid:
return False
if payload.get("cwd") and payload.get("cwd") != cwd:
return False
except Exception:
return False
elif agent == "agy":
base = f"{iso}/.gemini/antigravity-cli/conversations" if iso else f"{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}/.gemini/antigravity-cli/cache/last_conversations.json" if iso else f"{home}/.gemini/antigravity-cli/cache/last_conversations.json"
if os.path.exists(lc):
try:
with open(lc) as f:
lc_data = json.load(f)
if lc_data.get(cwd) != uuid:
return False
except Exception:
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}/.hermes/state.db" if iso else f"{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 = f"{iso}/sessions" if iso 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
'
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'
import os, sys, json
exec(os.environ['MAM_VERIFY_PY'])
ws = os.environ['WS_ABS']
agent = os.environ['AGENT']
uuid = os.environ['UUID']
row_str = os.environ.get('ROW_JSON', '')
row = json.loads(row_str) if row_str else {}
mode = os.environ.get('MODE', 'discover')
if verify_session_uuid(ws, agent, uuid, row, mode=mode):
sys.exit(0)
sys.exit(1)
PYEOF
}
# verify_tui_viewport <session_name> <agent> <workspace>
#
# Viewport matching verification:
# | Return | Meaning | Action Required |
# |---|---|---|
# | 0 | Viewport matches workspace | Pin UUID, last_visible_status='pinned', drift class C |
# | 1 | Viewport mismatch | Do not pin, log C-warn, last_visible_status unchanged |
# | 2 | Viewport check unavailable | Pin UUID via stage 1-3 only, log C-degraded |
#
# Returns:
# 0 = verified match
# 1 = mismatch
# 2 = check not possible (capture failed, session gone)
verify_tui_viewport() {
local session_name="$1" agent="$2" workspace="$3"
local abs; abs="$(cd "$workspace" 2>/dev/null && pwd)" || abs="$workspace"
local base; base="$(basename "$abs")"
if ! _herdr has-session -t "$session_name" >/dev/null 2>&1; then
return 2
fi
local pane_content
pane_content=$(_pane_capture "$session_name" 2>/dev/null || true)
if [ -z "$pane_content" ]; then
return 2
fi
case "$agent" in
claude)
if printf '%s\n' "$pane_content" | grep -q "$base"; then
return 0
fi
return 1
;;
agy|hermes|cline)
if printf '%s\n' "$pane_content" | grep -q "$base"; then
return 0
fi
return 1
;;
*)
return 2
;;
esac
}
# ---------------------------------------------------------------------------
# find_workspace_uuid <workspace> <agent>
#
@@ -890,7 +1055,7 @@ PYEOF
find_workspace_uuid() {
local workspace="$1" agent="$2" session_name="${3:-}"
local abs; abs="$(cd "$workspace" 2>/dev/null && pwd)" || abs="$workspace"
MAM_STATE_JSON="$(load_state_json)" WS_ABS="$abs" AGENT="$agent" TARGET_SESSION="$session_name" env_python "$AGENT_SESSIONS_YAML" <<'PYEOF'
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']
@@ -899,49 +1064,15 @@ 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
def jsonl_exists(uuid, iso=None):
base = f"{iso}/projects" if iso else claude_project_dir
key = ws.replace('/', '-').replace('_', '-')
return os.path.exists(f"{base}/{key}/{uuid}.jsonl")
def db_exists(uuid, iso=None):
base = f"{iso}/.gemini/antigravity-cli/conversations" if iso else f"{home}/.gemini/antigravity-cli/conversations"
return os.path.exists(f"{base}/{uuid}.db")
def hermes_exists(uuid, iso=None):
hdb = f"{iso}/.hermes/state.db" if iso else f"{home}/.hermes/state.db"
if not os.path.exists(hdb):
return False
try:
conn = sqlite3.connect(hdb)
r = conn.execute("SELECT 1 FROM sessions WHERE id=?", (uuid,)).fetchone()
conn.close()
return r is not None
except Exception:
return False
def cline_exists(uuid, iso=None):
base = f"{iso}/sessions" if iso else f"{home}/.cline/data/sessions"
return os.path.exists(f"{base}/{uuid}/{uuid}.json")
def own_exists(a, uuid, iso=None):
return {'claude': jsonl_exists, 'agy': db_exists,
'hermes': hermes_exists, 'cline': cline_exists}[a](uuid, iso)
# Ingest the merged state dictionary from stdin pipeline
try:
d = json.loads(os.environ.get('MAM_STATE_JSON', '{}'))
@@ -958,51 +1089,38 @@ for s_item in d.get('herdr_sessions', []):
if val:
running_ids.add(val)
def emit(u):
if u in running_ids:
return
print(u)
raise SystemExit(0)
# Fetch all sessions matching this workspace
sessions = []
for s in d.get('herdr_sessions', []):
if isinstance(s, dict) and (s.get('pane') or {}).get('cwd') == ws:
sessions.append(s)
# T5: target-session mode — resolve strictly within the named row's scope.
# An isolated row's conversation lives ONLY in its isolation root (which holds
# at most one conversation), so we never fall through to the global tiers —
# those would guess another role's conversation (the C3 mtime bug).
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 own_exists(agent, cand, iso):
if cand and verify_session_uuid(ws, agent, cand, s, mode="revalidate"):
emit(cand)
if iso:
key = ws.replace('/', '-').replace('_', '-')
key = workspace_key(ws)
if agent == 'claude':
for j in sorted(glob.glob(f"{iso}/projects/{key}/*.jsonl"), key=os.path.getmtime, reverse=True):
sid = None
try:
with open(j) as f:
first = f.readline().strip()
if first:
sid = json.loads(first).get('sessionId')
except Exception:
sid = None
cand = sid or os.path.basename(j)[:-6]
if cand:
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):
emit(os.path.basename(j)[:-3])
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):
@@ -1011,55 +1129,46 @@ if target:
r = conn.execute("SELECT id FROM sessions WHERE cwd=? ORDER BY started_at DESC LIMIT 1", (ws,)).fetchone()
conn.close()
if r:
emit(r[0])
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"):
emit(fn)
# isolated row: nothing found inside its exclusive root -> no guess
if verify_session_uuid(ws, agent, fn, s):
emit(fn)
print('')
raise SystemExit(0)
# target row absent or legacy (non-isolated): fall through to legacy tiers
for s in sessions:
name = s.get('name', '')
iso = iso_root_of(s)
if agent == 'claude' and name.endswith('-creator-claude'):
cand = s.get('claude_session_id_own')
if cand and jsonl_exists(cand, iso):
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 db_exists(cand, iso):
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 hermes_exists(cand, iso):
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 cline_exists(cand, iso):
if cand and verify_session_uuid(ws, agent, cand, s, mode="revalidate"):
emit(cand)
# 2) disk scan scoped to THIS workspace
if agent == 'claude':
key = ws.replace('/', '-').replace('_', '-')
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):
sid = None
try:
with open(j) as f:
first = f.readline().strip()
if first:
sid = json.loads(first).get('sessionId')
except Exception:
sid = None
cand = sid or os.path.basename(j)[:-6]
if cand and jsonl_exists(cand):
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"
@@ -1069,7 +1178,7 @@ elif agent == 'agy':
cand = json.load(open(lc)).get(ws)
except Exception:
cand = None
if cand and db_exists(cand):
if cand and verify_session_uuid(ws, agent, cand):
emit(cand)
elif agent == 'hermes':
hdb = f"{home}/.hermes/state.db"
@@ -1083,7 +1192,7 @@ elif agent == 'hermes':
cand = r[0]
except Exception:
cand = None
if cand:
if cand and verify_session_uuid(ws, agent, cand):
emit(cand)
elif agent == 'cline':
sessions_dir = f"{home}/.cline/data/sessions"
@@ -1097,17 +1206,10 @@ elif agent == 'cline':
candidates.append(json_file)
candidates.sort(key=os.path.getmtime, reverse=True)
for j in candidates:
try:
with open(j) as f:
sdata = json.load(f)
if sdata.get('cwd') == ws or sdata.get('workspace_root') == ws:
sid = sdata.get('session_id')
if sid:
emit(sid)
except Exception:
pass
cand = os.path.basename(j)[:-5]
if cand and verify_session_uuid(ws, agent, cand):
emit(cand)
# 3) agent_identities cache, ONLY when its project_cwd == this workspace
ai = {}
try:
if os.path.exists(db_path):
@@ -1127,19 +1229,19 @@ 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 jsonl_exists(cand):
if cand and verify_session_uuid(ws, agent, cand, mode="revalidate"):
emit(cand)
elif agent == 'agy':
cand = ai.get('conversation_id')
if cand and db_exists(cand):
if cand and verify_session_uuid(ws, agent, cand, mode="revalidate"):
emit(cand)
elif agent == 'hermes':
cand = ai_agent.get('session_id') or ai.get('conversation_id')
if cand and hermes_exists(cand):
if cand and verify_session_uuid(ws, agent, cand, mode="revalidate"):
emit(cand)
elif agent == 'cline':
cand = ai_agent.get('session_id') or ai.get('conversation_id')
if cand and cline_exists(cand):
if cand and verify_session_uuid(ws, agent, cand, mode="revalidate"):
emit(cand)
print('')