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:
Godopu committed 2026-09-05 23:02:29 +09:00
1 parent de0a9c2e81
commit 76f10b2096
45 files changed
+2045 -276

No files matched your search

+34 -3
View File
@@ -550,7 +550,8 @@ elif cmd1 == "agent":
"agy": f"Antigravity Ready in {resolved_cwd}",
"hermes": f"Hermes Ready in {resolved_cwd}",
"grok": f"Grok Ready in {resolved_cwd}",
"opencode": f"OpenCode Ready in {resolved_cwd}"
"opencode": f"OpenCode Ready in {resolved_cwd}",
"codex": f"Codex Ready in {resolved_cwd}"
}.get(agent_type, f"Ready in {resolved_cwd}")
agents = state.get("agents", {})
@@ -581,7 +582,8 @@ elif cmd1 == "agent":
"agy": "agy_conversation_id_own",
"hermes": "hermes_conversation_id_own",
"grok": "grok_session_id_own",
"opencode": "opencode_session_id_own"
"opencode": "opencode_session_id_own",
"codex": "codex_session_id_own"
}
own_key = own_key_map.get(agent_type)
if own_key:
@@ -642,6 +644,21 @@ elif cmd1 == "agent":
conn.execute("INSERT OR REPLACE INTO session (id, directory, time_created, cwd, created_at) VALUES (?, ?, ?, ?, ?)", (session_uuid, ws_abs, now_ms, ws_abs, time.time()))
conn.commit()
conn.close()
elif agent_type == "codex":
codex_home = os.environ.get("CODEX_HOME", os.path.join(home_dir, ".codex"))
os.makedirs(codex_home, exist_ok=True)
db_path = os.path.join(codex_home, "state_5.sqlite")
conn = sqlite3.connect(db_path)
conn.execute("CREATE TABLE IF NOT EXISTS threads (id TEXT PRIMARY KEY, cwd TEXT, rollout_path TEXT, created_at INTEGER, updated_at INTEGER)")
sessions_dir = os.path.join(codex_home, "sessions", "2026", "09", "05")
os.makedirs(sessions_dir, exist_ok=True)
rollout_file = os.path.join(sessions_dir, f"rollout-001-{session_uuid}.jsonl")
now_ts = int(time.time())
with open(rollout_file, "w") as rf:
rf.write(json.dumps({"payload": {"cwd": ws_abs, "id": session_uuid}}) + "\\\\n")
conn.execute("INSERT OR REPLACE INTO threads (id, cwd, rollout_path, created_at, updated_at) VALUES (?, ?, ?, ?, ?)", (session_uuid, ws_abs, rollout_file, now_ts, now_ts))
conn.commit()
conn.close()
state["agents"] = agents
save_state()
@@ -989,7 +1006,21 @@ sys.exit(0)
""")
opencode_bin.chmod(0o755)
# 6. uuidgen mock to guarantee isolated creation UUIDs
# 6. codex mock
codex_bin = bin_dir / "codex"
codex_bin.write_text("""#!/usr/bin/env python3
import sys, os
args = sys.argv[1:]
if len(args) >= 1 and args[0] == "--version":
print("codex 0.1.0")
sys.exit(0)
if len(args) >= 1 and args[0] == "delete":
sys.exit(0)
sys.exit(0)
""")
codex_bin.chmod(0o755)
# 7. uuidgen mock to guarantee isolated creation UUIDs
uuidgen_bin = bin_dir / "uuidgen"
uuidgen_bin.write_text("""#!/usr/bin/env python3
import uuid
+161 -2
View File
@@ -32,6 +32,7 @@ def test_agent_adapter_registry():
'hermes': 'hermes_conversation_id_own',
'grok': 'grok_session_id_own',
'opencode': 'opencode_session_id_own',
'codex': 'codex_session_id_own',
}
for agent, expected in EXPECTED_OWN_KEYS.items():
adapter = get_adapter(agent)
@@ -75,6 +76,7 @@ def test_adapter_required_properties():
'hermes': ('Hermes|Welcome to Hermes Agent|NOUS HERMES', '/exit', 'hermes-agent', ('session_id',)),
'grok': ('Grok|xAI|Assistant|❯|>>>', '/exit', 'grok-build', ('session_id', 'session_jsonl')),
'opencode': ('OpenCode|Chat|Ask anything|tab agents|ctrl\\+p|Build auto|commands', '/exit', 'opencode-cli', ('session_id',)),
'codex': ('Codex|Ask Codex to do anything', 'Ctrl+C', 'codex-cli', ('session_id',)),
}
for agent, (toks, exitk, delk, cache_f) in expected.items():
adapter = get_adapter(agent)
@@ -96,6 +98,7 @@ def test_adapter_modal_tokens_contract():
'hermes': None,
'grok': None,
'opencode': None,
'codex': None,
}
for agent, expected in expected_modal.items():
adapter = get_adapter(agent)
@@ -109,7 +112,7 @@ def test_facts_bridge_eval_contract():
env = os.environ.copy()
skills_dir = str(Path(__file__).resolve().parent.parent / ".agents" / "skills")
env["PYTHONPATH"] = f"{skills_dir}:{env.get('PYTHONPATH', '')}"
for agent in ('claude', 'agy', 'hermes', 'grok', 'opencode'):
for agent in ('claude', 'agy', 'hermes', 'grok', 'opencode', 'codex'):
res = subprocess.run([sys.executable, "-m", "lib_py.agents", "facts", agent], capture_output=True, text=True, env=env)
assert res.returncode == 0
facts_output = res.stdout
@@ -355,6 +358,12 @@ def test_adapter_spawn_and_resume_specs():
assert opencode.resume_spec('opencode', 'u1', materialized=True) == 'opencode --session u1 --auto --agent build'
assert opencode.resume_spec('opencode', 'u1', materialized=False) == 'opencode --auto --agent build'
codex = get_adapter('codex')
assert codex.spawn_spec('codex', 'u1') == 'codex --dangerously-bypass-approvals-and-sandbox'
assert codex.spawn_spec('codex', '') == 'codex --dangerously-bypass-approvals-and-sandbox'
assert codex.resume_spec('codex', 'u1', materialized=True) == 'codex resume --dangerously-bypass-approvals-and-sandbox u1'
assert codex.resume_spec('codex', 'u1', materialized=False) == 'codex --dangerously-bypass-approvals-and-sandbox'
def test_adapter_auth_ok(tmp_path, monkeypatch):
monkeypatch.setenv("HOME_DIR", str(tmp_path))
# Claude auth runner
@@ -395,6 +404,17 @@ def test_adapter_auth_ok(tmp_path, monkeypatch):
opencode_auth.write_text("{}")
assert opencode.auth_ok() is True
# Codex auth check (env var, auth.json file, or CLI)
codex = get_adapter('codex')
assert codex.auth_ok(run_cmd=lambda cmd: (1, '', '')) is False
monkeypatch.setenv("OPENAI_API_KEY", "test-key")
assert codex.auth_ok() is True
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
codex_auth = tmp_path / ".codex" / "auth.json"
codex_auth.parent.mkdir(parents=True, exist_ok=True)
codex_auth.write_text("{}")
assert codex.auth_ok() is True
def test_adapter_discover(tmp_path):
import sqlite3
from lib_py.agents.base import DiscoveryContext
@@ -457,6 +477,23 @@ def test_adapter_discover(tmp_path):
_seed_opencode_session(o_db, 'u-o1', ws, 2000.0)
assert opencode.discover(ctx_o) == ['u-o1']
# 6. Codex
codex = get_adapter('codex')
ctx_x = DiscoveryContext(workspace=ws, agent_name='codex', home_dir=home)
codex_home = f"{home}/.codex"
os.makedirs(codex_home, exist_ok=True)
c_db = f"{codex_home}/state_5.sqlite"
conn = sqlite3.connect(c_db)
conn.execute("CREATE TABLE threads (id TEXT PRIMARY KEY, cwd TEXT, rollout_path TEXT)")
c_rollout = f"{codex_home}/sessions/2026/09/05/rollout-001-u-x1.jsonl"
os.makedirs(os.path.dirname(c_rollout), exist_ok=True)
with open(c_rollout, 'w') as f:
f.write('{"payload": {"cwd": "' + ws + '", "id": "u-x1"}}\n')
conn.execute("INSERT INTO threads VALUES ('u-x1', ?, ?)", (ws, c_rollout))
conn.commit()
conn.close()
assert codex.discover(ctx_x) == ['u-x1']
def test_cli_bridge_subcommands_and_quote_safety():
import subprocess, sys
from pathlib import Path
@@ -475,7 +512,7 @@ def test_cli_bridge_subcommands_and_quote_safety():
assert res.stdout.strip() == "/bin/claude --dangerously-skip-permissions --session-id uuid-test"
# 3. exit-key
for agent, expected_key in [('claude', '/exit'), ('agy', 'Exit'), ('hermes', '/exit'), ('grok', '/exit'), ('opencode', '/exit')]:
for agent, expected_key in [('claude', '/exit'), ('agy', 'Exit'), ('hermes', '/exit'), ('grok', '/exit'), ('opencode', '/exit'), ('codex', 'Ctrl+C')]:
res = subprocess.run([sys.executable, "-m", "lib_py.agents", "exit-key", agent], capture_output=True, text=True, env=env)
assert res.returncode == 0
assert res.stdout.strip() == expected_key
@@ -488,6 +525,7 @@ def test_delegate_agent_resolution_and_fallback():
'hermes': 'hermes-agent',
'grok': 'grok-build',
'opencode': 'opencode-cli',
'codex': 'codex-cli',
}
# 1. Adapter property
for agent, expected_key in expected_map.items():
@@ -507,6 +545,7 @@ def test_delegate_agent_resolution_and_fallback():
agy) delegate_agent="antigravity-cli" ;;
grok) delegate_agent="grok-build" ;;
opencode) delegate_agent="opencode-cli" ;;
codex) delegate_agent="codex-cli" ;;
*) echo "ERROR: cannot resolve delegate agent key for '$AGENT'" >&2; exit 2 ;;
esac
fi
@@ -837,6 +876,126 @@ exit 1
assert adapter._resolve_db(ctx) == expected_db
def _seed_codex_session(home_dir, session_id, ws_path, epoch_sec):
import sqlite3, json, time
codex_home = f"{home_dir}/.codex"
os.makedirs(codex_home, exist_ok=True)
db_path = f"{codex_home}/state_5.sqlite"
conn = sqlite3.connect(db_path)
conn.execute("CREATE TABLE IF NOT EXISTS threads (id TEXT PRIMARY KEY, cwd TEXT, rollout_path TEXT)")
sessions_dir = f"{codex_home}/sessions/2026/09/05"
os.makedirs(sessions_dir, exist_ok=True)
rollout_file = f"{sessions_dir}/rollout-001-{session_id}.jsonl"
with open(rollout_file, "w") as rf:
rf.write(json.dumps({"payload": {"cwd": ws_path, "id": session_id}}) + "\n")
if epoch_sec:
os.utime(rollout_file, (epoch_sec, epoch_sec))
conn.execute("INSERT OR REPLACE INTO threads (id, cwd, rollout_path) VALUES (?, ?, ?)", (session_id, ws_path, rollout_file))
conn.commit()
conn.close()
return rollout_file
def test_codex_purge_artifacts(tmp_path):
from lib_py.agents.registry import get_adapter
from lib_py.agents.base import DiscoveryContext
ws = str(tmp_path / "ws")
home = str(tmp_path / "home")
os.makedirs(ws, exist_ok=True)
os.makedirs(home, exist_ok=True)
adapter = get_adapter('codex')
ctx = DiscoveryContext(workspace=ws, agent_name='codex', home_dir=home)
rollout = _seed_codex_session(home, 'u-del', ws, 2000.0)
assert os.path.exists(rollout)
purged = adapter.purge_artifacts('u-del', ctx)
assert len(purged) == 2
assert not os.path.exists(rollout)
import sqlite3
conn = sqlite3.connect(f"{home}/.codex/state_5.sqlite")
assert conn.execute("SELECT count(*) FROM threads WHERE id='u-del'").fetchone()[0] == 0
conn.close()
def test_codex_verify_artifact_epoch_filtering(tmp_path):
from lib_py.agents.registry import get_adapter
from lib_py.agents.base import DiscoveryContext
ws = str(tmp_path / "ws")
home = str(tmp_path / "home")
os.makedirs(ws, exist_ok=True)
os.makedirs(home, exist_ok=True)
adapter = get_adapter('codex')
ctx_old = DiscoveryContext(workspace=ws, agent_name='codex', home_dir=home, epoch=1500)
_seed_codex_session(home, 'u-old', ws, 1000.0)
_seed_codex_session(home, 'u-new', ws, 2000.0)
assert adapter.verify_artifact('u-old', ctx_old) is False
assert adapter.verify_artifact('u-new', ctx_old) is True
def test_codex_reconcile_dual_direction_epoch_guards(tmp_path):
from lib_py.agents.registry import get_adapter
from lib_py.agents.base import DiscoveryContext
from lib_py.verify_session import verify_session_uuid
ws = str(tmp_path / "ws")
home = str(tmp_path / "home")
os.makedirs(ws, exist_ok=True)
os.makedirs(home, exist_ok=True)
adapter = get_adapter('codex')
_seed_codex_session(home, 'u-hist', ws, 1000.0)
_seed_codex_session(home, 'u-fresh1', ws, 2000.0)
epoch = 1500
s_row = {'name': 'sess1', 'pane': {'cwd': ws}, 'herdr_session_epoch': epoch}
valid = [u for u in adapter.discover(DiscoveryContext(workspace=ws, agent_name='codex', home_dir=home, epoch=epoch, row=s_row)) if verify_session_uuid(ws, 'codex', u, s_row, home_dir=home, mode="discover")]
assert valid == ['u-fresh1']
assert len(valid) == 1
_seed_codex_session(home, 'u-fresh2', ws, 2100.0)
valid2 = [u for u in adapter.discover(DiscoveryContext(workspace=ws, agent_name='codex', home_dir=home, epoch=epoch, row=s_row)) if verify_session_uuid(ws, 'codex', u, s_row, home_dir=home, mode="discover")]
assert len(valid2) == 2
def test_codex_reconcile_full_block_integration(tmp_path):
"""Verify codex drift-C block behavior with sibling_claimed exclusion."""
from lib_py.agents.registry import get_adapter
from lib_py.agents.base import DiscoveryContext
from lib_py.verify_session import verify_session_uuid
ws = str(tmp_path / "ws")
home = str(tmp_path / "home")
os.makedirs(ws, exist_ok=True)
os.makedirs(home, exist_ok=True)
adapter = get_adapter('codex')
_seed_codex_session(home, 'u-claimed', ws, 2000.0)
_seed_codex_session(home, 'u-target', ws, 2001.0)
epoch = 1900
s_eval = {
'name': 'codex-target-01',
'pane': {'cwd': ws},
'herdr_session_epoch': epoch,
'_sibling_claimed_uuids': ['u-claimed']
}
candidates = adapter.discover(DiscoveryContext(workspace=ws, agent_name='codex', home_dir=home, epoch=epoch, row=s_eval))
valid_candidates = []
for uuid in candidates:
if uuid in s_eval['_sibling_claimed_uuids']:
continue
if verify_session_uuid(ws, 'codex', uuid, s_eval, home_dir=home, mode="discover"):
valid_candidates.append(uuid)
assert valid_candidates == ['u-target']
+5 -1
View File
@@ -488,7 +488,7 @@ def test_c10_create_session_timeout_dead_pid_kills_session(mam_sandbox, mock_her
def test_adapter_strong_weak_partition():
"""N-D: Verify strong_ready_tokens and weak_ready_tokens partition ready_tokens for all adapters."""
from lib_py.agents.registry import get_adapter
for agent in ('claude', 'agy', 'hermes', 'grok', 'opencode'):
for agent in ('claude', 'agy', 'hermes', 'grok', 'opencode', 'codex'):
adapter = get_adapter(agent)
assert adapter is not None
ready_set = set(t for t in adapter.ready_tokens.split('|') if t)
@@ -527,6 +527,8 @@ echo "7: $(check_sks 'claude')"
echo "9: $(check_sks 'grok-02')"
echo "10: $(check_sks 'creator-opencode-01')"
echo "11: $(check_sks 'opencode')"
echo "12: $(check_sks 'creator-codex-01')"
echo "13: $(check_sks 'codex')"
"""
res = _run_lib_helpers(script)
assert res.returncode == 0, res.stderr + res.stdout
@@ -538,6 +540,8 @@ echo "11: $(check_sks 'opencode')"
assert "9: RESOLVED_AGENT=grok" in res.stdout
assert "10: RESOLVED_AGENT=opencode" in res.stdout
assert "11: RESOLVED_AGENT=opencode" in res.stdout
assert "12: RESOLVED_AGENT=codex" in res.stdout
assert "13: RESOLVED_AGENT=codex" in res.stdout
def test_f2_upsell_modal_defense_with_real_facts_and_banner():
+55 -6
View File
@@ -960,18 +960,18 @@ def test_grok_shell_and_scripts_integration(mam_sandbox):
lib_path = mam_sandbox / "skills" / "lib.sh"
lib_content = lib_path.read_text()
assert '*-creator-grok|*-planner-grok|*-reviewer-grok) kind="grok"' in lib_content
assert "'claude', 'agy', 'hermes', 'grok', 'opencode'" in lib_content
assert "'claude', 'agy', 'hermes', 'grok', 'opencode', 'codex'" in lib_content
assert '[[ "$sess" =~ "grok" ]]' in lib_content
# 2. create_session.sh validation
create_path = mam_sandbox / "skills" / "multi-agent-mux-create" / "scripts" / "create_session.sh"
create_content = create_path.read_text()
assert 'claude|agy|hermes|grok|opencode)' in create_content
assert 'claude|agy|hermes|grok|opencode|codex)' in create_content
# 3. stop_session.sh validation & state capture
stop_path = mam_sandbox / "skills" / "multi-agent-mux-stop" / "scripts" / "stop_session.sh"
stop_content = stop_path.read_text()
assert 'claude|agy|hermes|grok|opencode)' in stop_content
assert 'claude|agy|hermes|grok|opencode|codex)' in stop_content
assert "target['grok_session_id_own'] = captured" in stop_content
# 4. workspace_uuid OWN_KEY
@@ -986,12 +986,12 @@ def test_opencode_shell_and_scripts_integration(mam_sandbox):
lib_path = mam_sandbox / "skills" / "lib.sh"
lib_content = lib_path.read_text()
assert '*-creator-opencode|*-planner-opencode|*-reviewer-opencode) kind="opencode"' in lib_content
assert "'claude', 'agy', 'hermes', 'grok', 'opencode'" in lib_content
assert "'claude', 'agy', 'hermes', 'grok', 'opencode', 'codex'" in lib_content
# 2. create_session.sh validation & OPENCODE_PERMISSION behavior (unset & preset)
create_path = mam_sandbox / "skills" / "multi-agent-mux-create" / "scripts" / "create_session.sh"
create_content = create_path.read_text()
assert 'claude|agy|hermes|grok|opencode)' in create_content
assert 'claude|agy|hermes|grok|opencode|codex)' in create_content
# Test unset path
env_unset = {k: v for k, v in os.environ.items() if k != "OPENCODE_PERMISSION"}
res_c_unset = subprocess.run(
@@ -1030,7 +1030,7 @@ def test_opencode_shell_and_scripts_integration(mam_sandbox):
# 4. stop_session.sh validation & state capture
stop_path = mam_sandbox / "skills" / "multi-agent-mux-stop" / "scripts" / "stop_session.sh"
stop_content = stop_path.read_text()
assert 'claude|agy|hermes|grok|opencode)' in stop_content
assert 'claude|agy|hermes|grok|opencode|codex)' in stop_content
assert "target['opencode_session_id_own'] = captured" in stop_content
# 5. workspace_uuid OWN_KEY
@@ -1044,6 +1044,55 @@ def test_opencode_shell_and_scripts_integration(mam_sandbox):
assert 'op_adapter = get_adapter(\'opencode\')' in recon_content
def test_codex_shell_and_scripts_integration(mam_sandbox):
"""Verify lib.sh, create_session.sh, stop_session.sh, and workspace_uuid contain codex."""
# 1. lib.sh kind mapping & binaries
lib_path = mam_sandbox / "skills" / "lib.sh"
lib_content = lib_path.read_text()
assert '*-creator-codex|*-planner-codex|*-reviewer-codex)' in lib_content
assert "'claude', 'agy', 'hermes', 'grok', 'opencode', 'codex'" in lib_content
# 2. create_session.sh validation
create_path = mam_sandbox / "skills" / "multi-agent-mux-create" / "scripts" / "create_session.sh"
create_content = create_path.read_text()
assert 'claude|agy|hermes|grok|opencode|codex)' in create_content
# 3. resume_session.sh validation
resume_path = mam_sandbox / "skills" / "multi-agent-mux-resume" / "scripts" / "resume_session.sh"
resume_content = resume_path.read_text()
assert 'claude|agy|hermes|grok|opencode|codex)' in resume_content
# 4. stop_session.sh validation & state capture
stop_path = mam_sandbox / "skills" / "multi-agent-mux-stop" / "scripts" / "stop_session.sh"
stop_content = stop_path.read_text()
assert 'claude|agy|hermes|grok|opencode|codex)' in stop_content
assert "target['codex_session_id_own'] = captured" in stop_content
# 5. workspace_uuid OWN_KEY
from lib_py.workspace_uuid import OWN_KEY
assert OWN_KEY.get('codex') == 'codex_session_id_own'
# 6. reconcile.sh drift C block contains codex
recon_path = mam_sandbox / "skills" / "multi-agent-mux-monitor" / "scripts" / "reconcile.sh"
recon_content = recon_path.read_text()
assert 'drift C (codex)' in recon_content
def test_orc_onboard_codex_flag_tolerant_regex():
"""Verify orc_onboard.sh flag-tolerant regex correctly extracts UUID from codex resume command."""
import subprocess
cmd_line = "codex resume --dangerously-bypass-approvals-and-sandbox 12345678-1234-1234-1234-123456789abc"
sh_snippet = f'''
cmd_line="{cmd_line}"
detected_id=$(echo "$cmd_line" | grep -oE 'resume[[:space:]]+.*[0-9a-fA-F]{{8}}-[0-9a-fA-F]{{4}}-[0-9a-fA-F]{{4}}-[0-9a-fA-F]{{4}}-[0-9a-fA-F]{{12}}' | grep -oE '[0-9a-fA-F]{{8}}-[0-9a-fA-F]{{4}}-[0-9a-fA-F]{{4}}-[0-9a-fA-F]{{4}}-[0-9a-fA-F]{{12}}' | head -n 1 || true)
echo "$detected_id"
'''
res = subprocess.run(["bash", "-c", sh_snippet], capture_output=True, text=True)
assert res.returncode == 0
assert res.stdout.strip() == "12345678-1234-1234-1234-123456789abc"
+50
View File
@@ -1276,3 +1276,53 @@ def test_comp_opencode_create_and_reconcile_and_stop(mam_sandbox, mock_herdr, mo
assert s_stopped["resumable"] is True
def test_comp_codex_create_and_reconcile_and_stop(mam_sandbox, mock_herdr, mock_agents):
"""Verify create_session -> reconcile drift-C materialization -> stop flow for codex."""
create_script = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-create" / "scripts" / "create_session.sh"
res = subprocess.run([
"bash", str(create_script),
"--workspace", str(mam_sandbox),
"--agent", "codex",
"--role", "creator"
], capture_output=True, text=True, cwd=str(mam_sandbox))
assert res.returncode == 0, res.stderr
yaml_path = mam_sandbox / ".mam" / "agent-sessions.yaml"
import yaml
with open(yaml_path) as f:
data = yaml.safe_load(f)
sessions = data.get("herdr_sessions", [])
assert len(sessions) == 1
s = sessions[0]
session_name = s["name"]
assert s["status"] == "running"
assert s["codex_session_id_own"] is None
assert s["session_id_source"] == "pending-discovery"
# Run reconcile.sh to trigger drift-C materialization
reconcile_script = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-monitor" / "scripts" / "reconcile.sh"
res_rec = subprocess.run(["bash", str(reconcile_script), "--once", "--emit-diff"], capture_output=True, text=True, cwd=str(mam_sandbox), env={**os.environ, "WORKSPACE_ROOT": str(mam_sandbox)})
assert res_rec.returncode == 0, res_rec.stderr
with open(yaml_path) as f:
data_after = yaml.safe_load(f)
s_after = [x for x in data_after.get("herdr_sessions", []) if x.get("name") == session_name][0]
assert s_after.get("codex_session_id_own") is not None
assert s_after.get("last_visible_status") in ("pinned", "resume_verified")
# Stop session
stop_script = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-stop" / "scripts" / "stop_session.sh"
res_stop = subprocess.run([
"bash", str(stop_script),
"--session", session_name,
"--agent", "codex"
], capture_output=True, text=True, cwd=str(mam_sandbox))
assert res_stop.returncode == 0, res_stop.stderr
with open(yaml_path) as f:
data_stopped = yaml.safe_load(f)
s_stopped = [x for x in data_stopped.get("herdr_sessions", []) if x.get("name") == session_name][0]
assert s_stopped["status"] == "stopped"
assert s_stopped["resumable"] is True