Files
multi-agent-mux/.agents/skills/lib_py/atomic_yaml.py
T
Godopu b4821fafa8 feat(a4,c3b): complete A-4 Phase 2 agent knowledge migration & Option B isolation removal
- Option B / C-3b: Deprecate isolation.root and remove its 4 legacy consumers in lib.sh, workspace_uuid.py, verify_session.py, and stop_session.sh.
- M2~M7 Migration: Implement artifact_path, verify_artifact, purge_artifacts, spawn_spec, resume_spec, auth_ok, discover, ready_tokens, exit_key, and delegate_agent_key across BaseAgentAdapter and all 4 adapters (Claude, Agy, Hermes, Cline).
- Migrate shell duplications in lib.sh, create_session.sh, resume_session.sh, stop_session.sh, and reconcile.sh to python -m lib_py.agents facts.
- Hardening & Corrections: Fix Cline resume_spec flag to '-i --id', apply shlex.quote across all facts variables with MAM_ prefix, add safe fallback in wait_for_tui_ready.
- Add comprehensive contract tests in tests/test_a4_adapter_contract.py and sync IMPROVEMENTS.md / LOG.md.
- Verified: 100% PASS across unit, component, contract, and integration tests.
2026-08-16 23:15:24 +09:00

211 lines
8.6 KiB
Python

# 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")
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()