refactor(uuid): resolve B-10 by deprecating agent_identities and removing PyYAML dependency

- Remove dead agent_identities read path and PyYAML import from workspace_uuid.py
- Defer eager PyYAML import in verify_session.py to lazy YAML fallback branch
- Simplify UUID resolution to 2-tier model (tier-1 own row ID -> tier-2 adapter scan)
- Clean up unused Drift D and ghost cache clearing in reconcile.sh and stop_session.sh
- Add 3 regression guards in tests/test_tier1_unit.py (266/266 PASS)
- Update IMPROVEMENTS.md, VERSIONS.md, and SKILL.md files
This commit is contained in:
2026-08-17 10:59:03 +09:00
parent 7e21077ded
commit 40576c44ab
14 changed files with 639 additions and 114 deletions
+4 -9
View File
@@ -1298,15 +1298,10 @@ verify_tui_viewport() {
# ---------------------------------------------------------------------------
# find_workspace_uuid <workspace> <agent>
#
# Workspace-SCOPED resolution of the resume UUID (P0-C). It NEVER returns a
# global agent_identities id unless that id's project_cwd matches THIS
# workspace. Resolution order:
# Workspace-SCOPED resolution of the resume UUID (P0-C). Resolution order:
# 1) herdr_sessions[] row whose pane.cwd == this workspace -> per-row own id
# (claude_session_id_own / agy_conversation_id_own)
# 2) on-disk scan scoped to this workspace
# (claude: ~/.claude/projects/<key>/*.jsonl ; agy: last_conversations.json[cwd])
# 3) agent_identities cache in d (primary) or $YAML_PATH (fallback), ONLY when its project_cwd == this workspace.
# (Note: DB is authority, YAML is mirror; tier-3 never prioritizes mirror over DB)
# 2) on-disk scan scoped to this workspace, via the agent adapter's discover()
# Prints the UUID on stdout (empty line if none). Always exits 0.
# ---------------------------------------------------------------------------
find_workspace_uuid() {
@@ -1323,8 +1318,8 @@ PYEOF
#
# Thin wrapper over find_workspace_uuid: resolves THIS workspace's conversation
# id (claude jsonl sessionId / agy db uuid) and prints it on stdout (empty line
# if none). find_workspace_uuid is already a workspace-scoped, 3-tier, race-free
# resolver (per-row own id -> workspace-scoped disk scan -> cwd-matched cache),
# if none). find_workspace_uuid is already a workspace-scoped, 2-tier, race-free
# resolver (per-row own id -> workspace-scoped disk scan),
# so recording its result into the row before kill guarantees tier-1 on the next
# resume. Pass session_name to prefer that specific row's recorded id. Always exits 0.
# ---------------------------------------------------------------------------
+4
View File
@@ -55,6 +55,10 @@ class BaseAgentAdapter:
@property
def identity_cache_fields(self) -> tuple:
"""agent_identities 캐시의 에이전트별 필드명.
B-10(Option A)로 캐시 읽기 경로가 제거되어 현재 생산 소비자는 0건이지만,
캐시 쓰기 경로가 도입되면 즉시 필요한 유일한 스키마 기술이므로 존치한다.
임의 삭제 금지 — 삭제 시 4개 어댑터에 필드명을 다시 흩뿌려야 한다."""
raise NotImplementedError
@property
+2 -1
View File
@@ -7,7 +7,7 @@ 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
import os, sys, json, sqlite3
override = os.environ.get("MAM_ORCHESTRATOR_UUIDS")
if override is not None:
if not override.strip():
@@ -47,6 +47,7 @@ def mam_orchestrator_uuids():
pass
if (d_obj is None or "orchestrator_uuids" not in d_obj) and os.path.exists(yaml_p):
try:
import yaml
with open(yaml_p) as f:
d_obj = yaml.safe_load(f) or {}
except Exception:
+1 -33
View File
@@ -1,7 +1,7 @@
# workspace_uuid.py — workspace UUID discovery logic
# Extracted from lib.sh find_workspace_uuid PYEOF block
import os, sys, json, sqlite3
import os, sys, json
from lib_py.verify_session import verify_session_uuid, mam_orchestrator_uuids, mam_row_own_uuid
OWN_KEY = {
@@ -80,38 +80,6 @@ def find_workspace_uuid_main():
for cand in adapter.discover(ctx):
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:
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__':
@@ -137,16 +137,6 @@ YAML: no such session
```
- **C-ambiguous. Multiple candidates detected**: If multiple candidate transcripts match an unassigned session, the monitor avoids random pinning, reports `C-ambiguous`, and sets `last_visible_status: "ambiguous: N candidates"`.
### D. Stale UUID (artifact gone)
```
YAML: agent_identities.claude.session_id=87dc548e-...
disk: ~/.claude/projects/.../87dc548e-...jsonl: missing
→ report it, but DO NOT delete from YAML
(the user may have moved the file or the disk may be temporarily unavailable;
only `--purge-conversation` should remove the id)
```
## Pitfalls
- **Don't expect `--once` to stay alive** — it does a single pass and exits. Use `--subscribe` for continuous monitoring.
@@ -790,43 +790,6 @@ for s in d.get('herdr_sessions', []):
else:
_pin_and_verify_resume(s, 'cline', cwd, uuid, degraded=True)
# === drift D: stale UUID (cache 의 artifact 가 사라짐) — 보고만, 변경 없음 ===
ai = d.get('agent_identities', {}) or {}
cl = (ai.get('claude') or {})
if cl.get('session_id'):
sid = cl['session_id']
if not glob.glob(f"{claude_project_dir}/*/{sid}.jsonl"):
drifts.append({'class': 'D', 'name': '(claude identity cache)',
'msg': f"stale UUID in agent_identities.claude.session_id: {sid} (jsonl missing)"})
ag = (ai.get('agy') or {})
if ag.get('conversation_id'):
cid = ag['conversation_id']
if not os.path.exists(f"{home}/.gemini/antigravity-cli/conversations/{cid}.db"):
drifts.append({'class': 'D', 'name': '(agy identity cache)',
'msg': f"stale UUID in agent_identities.agy.conversation_id: {cid} (.db missing)"})
hr = (ai.get('hermes') or {})
if hr.get('session_id'):
sid = hr['session_id']
hdb = f"{home}/.hermes/state.db"
has_session = False
if os.path.exists(hdb):
try:
conn = sqlite3.connect(hdb)
r = conn.execute("SELECT 1 FROM sessions WHERE id=?", (sid,)).fetchone()
conn.close()
has_session = r is not None
except Exception:
pass
if not has_session:
drifts.append({'class': 'D', 'name': '(hermes identity cache)',
'msg': f"stale UUID in agent_identities.hermes.session_id: {sid} (session missing from db)"})
cn = (ai.get('cline') or {})
if cn.get('session_id'):
sid = cn['session_id']
if not os.path.exists(f"{home}/.cline/data/sessions/{sid}/{sid}.json"):
drifts.append({'class': 'D', 'name': '(cline identity cache)',
'msg': f"stale UUID in agent_identities.cline.session_id: {sid} (session file missing)"})
result = {
'timestamp': now_iso,
'yaml_path': yaml_path,
@@ -47,15 +47,12 @@ ideal resume path:
## UUID resolution order
`agent-sessions.yaml` is the *primary* source. The skill reads in this order:
`agent-sessions.yaml` and on-disk discovery are used to resolve the UUID in this order:
1. **`agent-sessions.yaml``agent_identities.<agent>.session_id` (claude) / `conversation_id` (agy)** — explicit saved value
2. **`agent-sessions.yaml``agent_identities.<agent>.session_jsonl` (claude) / `conversation_db` (agy)** — the on-disk artifact
3. **Fallback: scan disk for the workspace's most recent conversation** (Note: `CLAUDE_PROJECT_DIR` overrides the default `~/.claude/projects/` path, and `HOME_DIR` overrides the `~` path) —
- claude: `ls -t $CLAUDE_PROJECT_DIR/<workspace-key>/*.jsonl | head -1` and parse the `sessionId` from the first line
- agy: `jq -r '."<workspace>"' $HOME_DIR/.gemini/antigravity-cli/cache/last_conversations.json`
1. **`herdr_sessions[]` row's per-row own id** (`claude_session_id_own` / `agy_conversation_id_own` / `hermes_conversation_id_own` / `cline_conversation_id_own`) — explicitly saved by `multi-agent-mux-stop` right before teardown (tier-1, race-free).
2. **Workspace-scoped on-disk scan** (adapter `discover()`)
If all three are empty → the workspace has no conversation yet. Fall back to `multi-agent-mux-create`.
If both are empty → the workspace has no conversation yet. Fall back to `multi-agent-mux-create`.
## Workflow
@@ -105,7 +105,6 @@ lab-paper-pdf2md-creator-claude default running alive clau
| `A` | YAML `running`, herdr dead | session died without going through `multi-agent-mux-stop`. *Could* auto-terminate but won't — that's `multi-agent-mux-monitor`'s job. |
| `B` | herdr alive, not in YAML | ad-hoc session someone started without `multi-agent-mux-create`. Suggest: "use multi-agent-mux-create to register, or multi-agent-mux-stop to clean up." |
| `C` | YAML has `claude_session_id_own: null` AND a new *.jsonl exists | new session id materialized; suggest: "run multi-agent-mux-resume or reconcile to register it." |
| `D` | YAML has UUID in `agent_identities`, but the on-disk artifact is gone | stale UUID; user should `multi-agent-mux-stop --purge-conversation` to clean up. |
## Pitfalls
@@ -164,7 +164,7 @@ if herdr has-session -t "$SESSION_NAME" 2>/dev/null; then
fi
# 캡처: kill 직전에 conversation id 를 해결 (process/jsonl 이 아직 살아있을 때).
# find_workspace_uuid 가 tier-1(row) -> tier-2(workspace-scoped disk scan) -> tier-3(cache)
# find_workspace_uuid 가 tier-1(row) -> tier-2(workspace-scoped disk scan)
# 를 알아서 시도하므로 herdr 생사와 무관하게 동작.
CAPTURED_UUID=""
if [ "$CAPTURE_ID" = "1" ] && [ -n "$TARGET_CWD" ]; then
@@ -284,12 +284,6 @@ if purge and purge_uuid:
for item in adapter.purge_artifacts(purge_uuid, ctx):
print(f"purged: {item}", flush=True)
target[adapter.own_key] = None
# agent_identities 는 cache — 이 워크스페이스 것일 때만 비운다
ai = (d.get('agent_identities') or {}).get(agent) or {}
if ai.get('project_cwd') == ws:
if adapter and (ai.get('session_id') == purge_uuid or ai.get('conversation_id') == purge_uuid):
for field in adapter.identity_cache_fields:
ai[field] = None
elif purge and not purge_uuid:
print("WARN: --purge-conversation requested but no workspace-scoped UUID resolved; nothing purged", flush=True)