fix(resume): allow fresh spawn fallback and refresh epoch on 0-turn Class A resume

- Branch on Class A agents (agy, hermes, opencode) in resume_session.sh to fall back to clean spawn (CMD_FULL) when stopped before first turn (empty UUID)
- Update update_yaml_resumed.sh to support empty UUID with --cmd-full and refresh herdr_session_epoch on fresh spawn resume to prevent stale transcript capture
- Strictly preserve Class B (claude, grok) assigned UUID escape hatch and test_t8 contract
- Add regression tests test_t14, test_t15, test_t16 (Closes #3 Item 2)
This commit is contained in:
2026-08-31 10:30:44 +09:00
parent c49ee3bdd8
commit 7e15081d87
3 changed files with 204 additions and 23 deletions
+123
View File
@@ -1,5 +1,6 @@
import os
import json
import time
import uuid
import pytest
import subprocess
@@ -389,3 +390,125 @@ def test_t13_mam_workspace_key_equivalence(mam_sandbox):
abs_path = os.path.realpath(v)
py_key = abs_path.replace("/", "-").replace("_", "-")
assert sh_key == py_key, f"Mismatch for {v}: shell={sh_key}, py={py_key}"
def test_t14_resume_class_a_fresh_spawn_fallback(mam_sandbox, mock_herdr, mock_agents):
"""T-14 (Issue #3): agy (Class A) stopped at 0-turns has no conversation id to
resolve; resume_session.sh must fall back to a fresh spawn (RC=0, plain spawn
command) rather than hard-failing with 'No saved session'."""
ws = str(mam_sandbox / "resume_class_a")
os.makedirs(ws, exist_ok=True)
create_script = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-create" / "scripts" / "create_session.sh"
stop_script = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-stop" / "scripts" / "stop_session.sh"
resume_script = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-resume" / "scripts" / "resume_session.sh"
cmd = [
"bash", str(create_script),
"--workspace", ws,
"--agent", "agy",
"--role", "creator",
"--session", "test-t14-agy",
"--no-onboard"
]
subprocess.run(cmd, check=True, cwd=str(mam_sandbox))
# Stop the session without ever sending a prompt (0-turn stop).
subprocess.run(
["bash", str(stop_script), "--session", "test-t14-agy", "--agent", "agy"],
check=True, cwd=str(mam_sandbox)
)
yaml_path = mam_sandbox / ".mam" / "agent-sessions.yaml"
with open(yaml_path) as f:
d = yaml.safe_load(f)
session = [s for s in d["herdr_sessions"] if s["name"] == "test-t14-agy"][0]
assert session["status"] == "stopped"
assert not session.get("agy_conversation_id_own")
res = subprocess.run([
"bash", str(resume_script),
"--workspace", ws,
"--agent", "agy",
"--session", "test-t14-agy",
"--dry-run"
], capture_output=True, text=True, cwd=str(mam_sandbox))
assert res.returncode == 0, f"stderr: {res.stderr}"
assert "would spawn:" in res.stdout
assert "--dangerously-skip-permissions" in res.stdout
assert "--conversation" not in res.stdout
def test_t15_resume_class_b_still_hard_fails_when_truly_unresolvable(mam_sandbox, mock_herdr, mock_agents):
"""T-15 (Issue #3 regression guard): claude/grok (Class B) must NOT receive the
Class A fresh-spawn fallback - a claude session with no resolvable id at all
(never created) still hard-fails exactly as before."""
ws = str(mam_sandbox / "resume_class_b_missing")
os.makedirs(ws, exist_ok=True)
resume_script = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-resume" / "scripts" / "resume_session.sh"
res = subprocess.run([
"bash", str(resume_script),
"--workspace", ws,
"--agent", "claude",
"--session", "never-created-claude",
"--dry-run"
], capture_output=True, text=True, cwd=str(mam_sandbox))
assert res.returncode == 1
assert "ERROR: No saved session for" in res.stderr
def test_t16_resume_class_a_fresh_spawn_resets_discovery_epoch(mam_sandbox, mock_herdr, mock_agents):
"""T-16 (Issue #3 challenge refinement): a Class A (agy) 0-turn resume must reset
herdr_session_epoch to the resume timestamp so reconcile.sh does not auto-capture
stale conversations created between the original create and the resume."""
ws = str(mam_sandbox / "resume_class_a_epoch")
os.makedirs(ws, exist_ok=True)
create_script = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-create" / "scripts" / "create_session.sh"
stop_script = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-stop" / "scripts" / "stop_session.sh"
resume_script = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-resume" / "scripts" / "resume_session.sh"
cmd = [
"bash", str(create_script),
"--workspace", ws,
"--agent", "agy",
"--role", "creator",
"--session", "test-t16-agy",
"--no-onboard"
]
subprocess.run(cmd, check=True, cwd=str(mam_sandbox))
yaml_path = mam_sandbox / ".mam" / "agent-sessions.yaml"
with open(yaml_path) as f:
d = yaml.safe_load(f)
initial_epoch = [s for s in d["herdr_sessions"] if s["name"] == "test-t16-agy"][0]["herdr_session_epoch"]
# Stop at 0-turns.
subprocess.run(
["bash", str(stop_script), "--session", "test-t16-agy", "--agent", "agy"],
check=True, cwd=str(mam_sandbox)
)
# Wait to guarantee the epoch advances by at least 1 second.
time.sleep(1.1)
# Resume the session (live execution, updating YAML).
res = subprocess.run([
"bash", str(resume_script),
"--workspace", ws,
"--agent", "agy",
"--session", "test-t16-agy"
], capture_output=True, text=True, cwd=str(mam_sandbox))
assert res.returncode == 0, f"stderr: {res.stderr}\nstdout: {res.stdout}"
with open(yaml_path) as f:
d = yaml.safe_load(f)
session = [s for s in d["herdr_sessions"] if s["name"] == "test-t16-agy"][0]
assert session["status"] == "running"
assert session["herdr_session_epoch"] > initial_epoch, (
f"herdr_session_epoch was not refreshed: {session['herdr_session_epoch']} <= {initial_epoch}"
)
assert session.get("session_id_source") == "pending-discovery"
assert session.get("session_id_verified") is False