feat(agent): add full codex agent support (v4.2.0)
- Implement CodexAdapter in lib_py/agents/adapters/codex.py with safe flags and TUI readiness - Extend create_session, resume_session, stop_session, and reconcile for codex CLI - Support codex agent delegation and loop integration across all multi-agent skills - Add unit, component, and contract test suites for codex integration - Verify all 462 tests passing cleanly
This commit is contained in:
1 parent
de0a9c2e81
commit
76f10b2096
45 files changed
+2045
-276
No files matched your search
@@ -1,14 +1,14 @@
|
||||
---
|
||||
name: multi-agent-mux-monitor
|
||||
description: "Run a long-lived reconciler that watches .mam/agent-sessions.yaml against the actual herdr/agent runtime state and reconciles them. Use when you want live visibility into which agent sessions are running, which are dead, which have stale YAML entries, and which have new session ids that haven't been recorded yet. Runs as a persistent loop (`reconcile.sh --subscribe`) that keeps going until it times out, idles out, or is interrupted."
|
||||
version: 4.1.3
|
||||
version: 4.2.0
|
||||
author: godopu
|
||||
license: MIT
|
||||
platforms: [linux, macos]
|
||||
environments: [terminal, herdr]
|
||||
metadata:
|
||||
hermes:
|
||||
tags: [agent, herdr, claude, antigravity, agy, grok, monitor, observation, reconciliation]
|
||||
tags: [agent, herdr, claude, antigravity, agy, grok, opencode, codex, monitor, observation, reconciliation]
|
||||
related_skills: [multi-agent-mux-create, multi-agent-mux-resume, multi-agent-mux-stop, multi-agent-mux-status]
|
||||
prereq_skills: [multi-agent-mux-create]
|
||||
---
|
||||
|
||||
@@ -520,7 +520,7 @@ if herdr_confirmed:
|
||||
agent = None
|
||||
role = 'creator'
|
||||
for r_name in ('creator', 'planner', 'reviewer'):
|
||||
for a_name in ('claude', 'agy', 'hermes', 'grok', 'opencode'):
|
||||
for a_name in ('claude', 'agy', 'hermes', 'grok', 'opencode', 'codex'):
|
||||
if name.endswith(f"-{r_name}-{a_name}"):
|
||||
role = r_name
|
||||
agent = a_name
|
||||
@@ -539,7 +539,7 @@ if herdr_confirmed:
|
||||
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', 'grok', 'opencode'):
|
||||
for a_name in ('claude', 'agy', 'hermes', 'grok', 'opencode', 'codex'):
|
||||
if a_name in pm_check.get('cmd', '') or a_name in pm_check.get('cmd_full', ''):
|
||||
agent = a_name
|
||||
break
|
||||
@@ -547,6 +547,10 @@ if herdr_confirmed:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not agent:
|
||||
if pm_check and pm_check.get('cmd') in ('claude', 'agy', 'hermes', 'grok', 'opencode', 'codex'):
|
||||
agent = pm_check['cmd']
|
||||
|
||||
if not agent:
|
||||
continue
|
||||
|
||||
@@ -603,6 +607,9 @@ if herdr_confirmed:
|
||||
elif agent == 'opencode':
|
||||
entry['child_pid'] = 0
|
||||
entry['opencode_session_id_own'] = None
|
||||
elif agent == 'codex':
|
||||
entry['child_pid'] = 0
|
||||
entry['codex_session_id_own'] = None
|
||||
d.setdefault('herdr_sessions', []).append(entry)
|
||||
yaml_session_names.add(name)
|
||||
drifts.append({'class': 'B', 'name': name,
|
||||
@@ -613,7 +620,7 @@ def row_agent(s):
|
||||
return agent_of_row(s)
|
||||
|
||||
OWN_KEY_BY_AGENT = {
|
||||
a: _get_own_key(a) for a in ('claude', 'agy', 'hermes', 'grok', 'opencode')
|
||||
a: _get_own_key(a) for a in ('claude', 'agy', 'hermes', 'grok', 'opencode', 'codex')
|
||||
}
|
||||
|
||||
# === drift C0: 지정된 ID 는 발견이 아니라 '확인'만 필요하다 ===
|
||||
@@ -896,6 +903,88 @@ for s in d.get('herdr_sessions', []):
|
||||
else:
|
||||
_pin_and_verify_resume(s, 'opencode', cwd, uuid, degraded=True)
|
||||
|
||||
# === drift C (codex): codex 새 session id materialize (per-row own id) ===
|
||||
for s in d.get('herdr_sessions', []):
|
||||
if row_agent(s) != 'codex':
|
||||
continue
|
||||
if s.get('status') != 'running':
|
||||
continue
|
||||
if s.get('codex_session_id_own'):
|
||||
continue
|
||||
cwd = (s.get('pane') or {}).get('cwd', '')
|
||||
if not cwd:
|
||||
continue
|
||||
|
||||
codex_home = os.environ.get('CODEX_HOME') or f"{home}/.codex"
|
||||
epoch_threshold = s.get('herdr_session_epoch', 0)
|
||||
|
||||
sibling_claimed = [
|
||||
other.get('codex_session_id_own')
|
||||
for other in d.get('herdr_sessions', [])
|
||||
if other is not s
|
||||
and (other.get('pane') or {}).get('cwd') == cwd
|
||||
and other.get('status') not in ('stopped', 'terminated')
|
||||
and other.get('codex_session_id_own')
|
||||
]
|
||||
s_eval = dict(s)
|
||||
s_eval['_sibling_claimed_uuids'] = sibling_claimed
|
||||
|
||||
valid_candidates = []
|
||||
try:
|
||||
# Rev.2: state DB first (indexed, single query) — dynamic filename,
|
||||
# no hardcoded suffix (see §1.5 / §2 _state_db()).
|
||||
state_matches = glob.glob(f"{codex_home}/state_*.sqlite")
|
||||
state_db = max(state_matches, key=os.path.getmtime) if state_matches else None
|
||||
rows = []
|
||||
if state_db and os.path.exists(state_db):
|
||||
try:
|
||||
conn = sqlite3.connect(state_db)
|
||||
cols = [c[1] for c in conn.execute("PRAGMA table_info(threads)").fetchall()]
|
||||
order_col = "updated_at" if "updated_at" in cols else ("created_at" if "created_at" in cols else None)
|
||||
if order_col:
|
||||
rows = conn.execute(
|
||||
f"SELECT id, {order_col} FROM threads WHERE cwd=? ORDER BY {order_col} DESC LIMIT 20",
|
||||
(cwd,)
|
||||
).fetchall()
|
||||
else:
|
||||
rows = [(r[0], int(time.time())) for r in conn.execute("SELECT id FROM threads WHERE cwd=? LIMIT 20", (cwd,)).fetchall()]
|
||||
conn.close()
|
||||
except Exception:
|
||||
rows = []
|
||||
if not rows:
|
||||
# Fallback: bounded glob over the date-nested tree
|
||||
paths = sorted(
|
||||
glob.glob(f"{codex_home}/sessions/*/*/*/rollout-*.jsonl"),
|
||||
key=os.path.getmtime, reverse=True
|
||||
)[:50]
|
||||
rows = [(os.path.basename(p)[:-6][-36:], os.path.getmtime(p)) for p in paths]
|
||||
|
||||
for uuid, ts in rows:
|
||||
if epoch_threshold and ts and ts < epoch_threshold:
|
||||
continue
|
||||
if uuid in (sibling_claimed or []):
|
||||
continue
|
||||
if verify_session_uuid(cwd, 'codex', uuid, s_eval, mode="discover"):
|
||||
valid_candidates.append(uuid)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if len(valid_candidates) > 1:
|
||||
drifts.append({'class': 'C-ambiguous', 'name': s['name'],
|
||||
'msg': f"{s['name']}: {len(valid_candidates)} candidate transcripts newer than session epoch; not pinning"})
|
||||
s['last_visible_status'] = f"ambiguous: {len(valid_candidates)} candidates"
|
||||
actions.append(f"ambiguous candidates: {s['name']}")
|
||||
elif len(valid_candidates) == 1:
|
||||
uuid = valid_candidates[0]
|
||||
cmd = ['bash', '-c', f'source "{lib_sh}" && verify_tui_viewport "{s["name"]}" "codex" "{cwd}"']
|
||||
rc = subprocess.run(cmd).returncode
|
||||
if rc == 0:
|
||||
_pin_and_verify_resume(s, 'codex', cwd, uuid, degraded=False)
|
||||
elif rc == 1:
|
||||
drifts.append({'class': 'C-warn', 'name': s['name'], 'msg': f"{s['name']}: TUI viewport mismatch for candidate {uuid} — not pinned, will retry next cycle"})
|
||||
else:
|
||||
_pin_and_verify_resume(s, 'codex', cwd, uuid, degraded=True)
|
||||
|
||||
result = {
|
||||
'timestamp': now_iso,
|
||||
'yaml_path': yaml_path,
|
||||
|
||||
Reference in new issue
Block a user