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:
@@ -51,9 +51,23 @@ esac
|
|||||||
UUID=$(bash "$(dirname "${BASH_SOURCE[0]}")/resolve_session_id.sh" \
|
UUID=$(bash "$(dirname "${BASH_SOURCE[0]}")/resolve_session_id.sh" \
|
||||||
--workspace "$WORKSPACE" --agent "$AGENT" --session "$SESSION_NAME")
|
--workspace "$WORKSPACE" --agent "$AGENT" --session "$SESSION_NAME")
|
||||||
|
|
||||||
|
# ISSUE-3: agy/hermes/opencode (Class A) are spawned without a UUID - their
|
||||||
|
# own conversation id is only assigned by the agent itself on its first turn.
|
||||||
|
# A session stopped before any message therefore has no UUID to resolve here,
|
||||||
|
# but IS still cleanly recoverable: relaunching the agent's plain first-run
|
||||||
|
# spawn command restores the exact state multi-agent-mux-create would have
|
||||||
|
# produced. claude/grok (Class B) always resolve a UUID via the
|
||||||
|
# verify_session.py:99-101 assigned-but-unverified escape hatch (see test_t8)
|
||||||
|
# and never reach this branch - do not extend the fallback to them.
|
||||||
|
FRESH_SPAWN=0
|
||||||
if [ -z "$UUID" ]; then
|
if [ -z "$UUID" ]; then
|
||||||
|
case "$AGENT" in
|
||||||
|
agy|hermes|opencode) FRESH_SPAWN=1 ;;
|
||||||
|
*)
|
||||||
echo "ERROR: No saved session for $WORKSPACE ($AGENT). Use multi-agent-mux-create first." >&2
|
echo "ERROR: No saved session for $WORKSPACE ($AGENT). Use multi-agent-mux-create first." >&2
|
||||||
exit 1
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -n "$HERDR_SERVER_OPT" ]; then
|
if [ -n "$HERDR_SERVER_OPT" ]; then
|
||||||
@@ -96,6 +110,18 @@ if [ "$(uname)" = "Darwin" ] && [ -f "$RESOLVED_BIN" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Determine CMD_FULL via adapter
|
# Determine CMD_FULL via adapter
|
||||||
|
if [ "$FRESH_SPAWN" = "1" ]; then
|
||||||
|
# ISSUE-3: no UUID to resume with - spawn the same plain first-run command
|
||||||
|
# multi-agent-mux-create would use (spawn-spec, not resume-spec).
|
||||||
|
CMD_FULL="$("$(_delegate_py_bin)" -m lib_py.agents spawn-spec "$AGENT" "$RESOLVED_BIN" "" 0 2>/dev/null || true)"
|
||||||
|
if [ -z "$CMD_FULL" ]; then
|
||||||
|
case "$AGENT" in
|
||||||
|
agy) CMD_FULL="${RESOLVED_BIN} --dangerously-skip-permissions" ;;
|
||||||
|
hermes) CMD_FULL="${RESOLVED_BIN} --yolo --accept-hooks" ;;
|
||||||
|
opencode) CMD_FULL="${RESOLVED_BIN} --auto --agent build" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
else
|
||||||
CMD_FULL="$("$(_delegate_py_bin)" -m lib_py.agents resume-spec "$AGENT" "$RESOLVED_BIN" "$UUID" "$WORKSPACE" 2>/dev/null || true)"
|
CMD_FULL="$("$(_delegate_py_bin)" -m lib_py.agents resume-spec "$AGENT" "$RESOLVED_BIN" "$UUID" "$WORKSPACE" 2>/dev/null || true)"
|
||||||
if [ -z "$CMD_FULL" ]; then
|
if [ -z "$CMD_FULL" ]; then
|
||||||
case "$AGENT" in
|
case "$AGENT" in
|
||||||
@@ -107,6 +133,7 @@ if [ -z "$CMD_FULL" ]; then
|
|||||||
*) echo "ERROR: unsupported agent: $AGENT" >&2; exit 2 ;;
|
*) echo "ERROR: unsupported agent: $AGENT" >&2; exit 2 ;;
|
||||||
esac
|
esac
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# Validate binary exists and is executable
|
# Validate binary exists and is executable
|
||||||
if [ -f "$RESOLVED_BIN" ] || [[ "$RESOLVED_BIN" == /* ]] || [[ "$RESOLVED_BIN" == ~/* ]]; then
|
if [ -f "$RESOLVED_BIN" ] || [[ "$RESOLVED_BIN" == /* ]] || [[ "$RESOLVED_BIN" == ~/* ]]; then
|
||||||
@@ -145,7 +172,7 @@ sleep 2
|
|||||||
# 5. Update agent-sessions.yaml: status running, last_visible_status
|
# 5. Update agent-sessions.yaml: status running, last_visible_status
|
||||||
bash "$(dirname "${BASH_SOURCE[0]}")/update_yaml_resumed.sh" \
|
bash "$(dirname "${BASH_SOURCE[0]}")/update_yaml_resumed.sh" \
|
||||||
--session "$SESSION_NAME" --uuid "$UUID" --agent "$AGENT" --workspace "$WORKSPACE" \
|
--session "$SESSION_NAME" --uuid "$UUID" --agent "$AGENT" --workspace "$WORKSPACE" \
|
||||||
--herdr-session "$HERDR_SESSION_NAME" \
|
--herdr-session "$HERDR_SESSION_NAME" --cmd-full "$CMD_FULL" \
|
||||||
${HERDR_WORKSPACE_OPT:+--herdr-workspace "$HERDR_WORKSPACE_OPT"}
|
${HERDR_WORKSPACE_OPT:+--herdr-workspace "$HERDR_WORKSPACE_OPT"}
|
||||||
|
|
||||||
echo "Successfully resumed $SESSION_NAME ($AGENT)"
|
echo "Successfully resumed $SESSION_NAME ($AGENT)"
|
||||||
|
|||||||
@@ -11,7 +11,11 @@ source "$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/lib.sh"
|
|||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
Usage: $0 --session <name> --uuid <id> [--agent claude|agy|hermes|grok|opencode] [--herdr-session <name>]
|
Usage: $0 --session <name> --uuid <id> [--agent claude|agy|hermes|grok|opencode] [--herdr-session <name>] [--cmd-full <cmd>]
|
||||||
|
|
||||||
|
--uuid may be empty for an Issue-3 Class A fresh-spawn resume (agy/hermes/
|
||||||
|
opencode 0-turn stopped session, no conversation id ever assigned) - pass
|
||||||
|
--cmd-full so the display command can be recorded without a uuid to embed.
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,6 +26,7 @@ WORKSPACE=""
|
|||||||
ROLE=""
|
ROLE=""
|
||||||
HERDR_SERVER_OPT=""
|
HERDR_SERVER_OPT=""
|
||||||
HERDR_WORKSPACE_OPT=""
|
HERDR_WORKSPACE_OPT=""
|
||||||
|
CMD_FULL_OPT=""
|
||||||
|
|
||||||
while [ $# -gt 0 ]; do
|
while [ $# -gt 0 ]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
@@ -32,13 +37,13 @@ while [ $# -gt 0 ]; do
|
|||||||
--role) ROLE="$2"; shift 2 ;;
|
--role) ROLE="$2"; shift 2 ;;
|
||||||
--herdr-session|--herdr-server) HERDR_SERVER_OPT="$2"; shift 2 ;;
|
--herdr-session|--herdr-server) HERDR_SERVER_OPT="$2"; shift 2 ;;
|
||||||
--herdr-workspace) HERDR_WORKSPACE_OPT="$2"; shift 2 ;;
|
--herdr-workspace) HERDR_WORKSPACE_OPT="$2"; shift 2 ;;
|
||||||
|
--cmd-full) CMD_FULL_OPT="$2"; shift 2 ;;
|
||||||
-h|--help) usage; exit 0 ;;
|
-h|--help) usage; exit 0 ;;
|
||||||
*) echo "ERROR: unknown arg: $1" >&2; exit 2 ;;
|
*) echo "ERROR: unknown arg: $1" >&2; exit 2 ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
[ -n "$SESSION_NAME" ] || { echo "ERROR: --session required" >&2; exit 2; }
|
[ -n "$SESSION_NAME" ] || { echo "ERROR: --session required" >&2; exit 2; }
|
||||||
[ -n "$UUID" ] || { echo "ERROR: --uuid required" >&2; exit 2; }
|
|
||||||
[ -f "$AGENT_SESSIONS_YAML" ] || { echo "ERROR: $AGENT_SESSIONS_YAML not found" >&2; exit 1; }
|
[ -f "$AGENT_SESSIONS_YAML" ] || { echo "ERROR: $AGENT_SESSIONS_YAML not found" >&2; exit 1; }
|
||||||
|
|
||||||
if [ -n "$HERDR_SERVER_OPT" ]; then
|
if [ -n "$HERDR_SERVER_OPT" ]; then
|
||||||
@@ -104,7 +109,8 @@ atomic_dump_yaml "$AGENT_SESSIONS_YAML" \
|
|||||||
SESSION_NAME="$SESSION_NAME" UUID="$UUID" AGENT="$AGENT" NOW_ISO="$NOW_ISO" \
|
SESSION_NAME="$SESSION_NAME" UUID="$UUID" AGENT="$AGENT" NOW_ISO="$NOW_ISO" \
|
||||||
NOW_EPOCH="$NOW_EPOCH" TARGET_WORKSPACE="${WORKSPACE:-$WORKSPACE_ROOT}" ROLE="$ROLE" \
|
NOW_EPOCH="$NOW_EPOCH" TARGET_WORKSPACE="${WORKSPACE:-$WORKSPACE_ROOT}" ROLE="$ROLE" \
|
||||||
PANE_PID="$PANE_PID" CHILD_PID="$CHILD_PID" HERDR_SERVER_OPT_EXPLICIT="${HERDR_SERVER_OPT_EXPLICIT:-0}" \
|
PANE_PID="$PANE_PID" CHILD_PID="$CHILD_PID" HERDR_SERVER_OPT_EXPLICIT="${HERDR_SERVER_OPT_EXPLICIT:-0}" \
|
||||||
MAM_WS_LABEL="$MAM_WS_LABEL" MAM_WS_LABEL_EXPLICIT="${MAM_WS_LABEL_EXPLICIT:-0}" <<'PYEOF'
|
MAM_WS_LABEL="$MAM_WS_LABEL" MAM_WS_LABEL_EXPLICIT="${MAM_WS_LABEL_EXPLICIT:-0}" \
|
||||||
|
CMD_FULL_OPT="$CMD_FULL_OPT" <<'PYEOF'
|
||||||
name = os.environ['SESSION_NAME']
|
name = os.environ['SESSION_NAME']
|
||||||
uuid = os.environ['UUID']
|
uuid = os.environ['UUID']
|
||||||
agent = os.environ['AGENT']
|
agent = os.environ['AGENT']
|
||||||
@@ -157,6 +163,18 @@ else:
|
|||||||
target['herdr_workspace'] = wsl
|
target['herdr_workspace'] = wsl
|
||||||
|
|
||||||
target['status'] = 'running'
|
target['status'] = 'running'
|
||||||
|
if not uuid:
|
||||||
|
# ISSUE-3 (challenge c4b0a075): a Class A fresh-spawn resume re-arms
|
||||||
|
# discovery from scratch - the resumed agent has not spoken yet, so any
|
||||||
|
# on-disk conversation older than THIS resume must not be auto-pinned to
|
||||||
|
# it by reconcile.sh's mode="discover" mtime >= epoch check. Without this,
|
||||||
|
# the row's watermark stays at the original create_session.sh timestamp,
|
||||||
|
# letting reconcile.sh capture a stale/unrelated transcript before the
|
||||||
|
# resumed agent's first real turn.
|
||||||
|
target['herdr_session_epoch'] = epoch
|
||||||
|
target['herdr_session_created_at'] = now
|
||||||
|
target['session_id_source'] = 'pending-discovery'
|
||||||
|
target['session_id_verified'] = False
|
||||||
target.pop('terminated_at', None)
|
target.pop('terminated_at', None)
|
||||||
target.pop('terminated_at_epoch', None)
|
target.pop('terminated_at_epoch', None)
|
||||||
target.pop('termination_mode', None)
|
target.pop('termination_mode', None)
|
||||||
@@ -166,7 +184,11 @@ target.pop('stopped_at', None)
|
|||||||
target.pop('stopped_at_epoch', None)
|
target.pop('stopped_at_epoch', None)
|
||||||
target.pop('stop_reason', None)
|
target.pop('stop_reason', None)
|
||||||
target.pop('resumable', None)
|
target.pop('resumable', None)
|
||||||
|
if uuid:
|
||||||
target['last_visible_status'] = f'resumed conversation {uuid} at {now}'
|
target['last_visible_status'] = f'resumed conversation {uuid} at {now}'
|
||||||
|
else:
|
||||||
|
# ISSUE-3 Class A fresh-spawn resume: no conversation id existed yet.
|
||||||
|
target['last_visible_status'] = f'resumed (fresh spawn, no prior turn) at {now}'
|
||||||
|
|
||||||
target.setdefault('pane', {})
|
target.setdefault('pane', {})
|
||||||
if pane_pid.isdigit():
|
if pane_pid.isdigit():
|
||||||
@@ -178,15 +200,21 @@ if agent == 'claude':
|
|||||||
target['claude_session_id_own'] = uuid
|
target['claude_session_id_own'] = uuid
|
||||||
elif agent == 'agy':
|
elif agent == 'agy':
|
||||||
target['pane']['cmd'] = 'agy'
|
target['pane']['cmd'] = 'agy'
|
||||||
|
if uuid:
|
||||||
target['pane']['cmd_full'] = f'agy --dangerously-skip-permissions --conversation {uuid}'
|
target['pane']['cmd_full'] = f'agy --dangerously-skip-permissions --conversation {uuid}'
|
||||||
target['agy_conversation_id_own'] = uuid
|
target['agy_conversation_id_own'] = uuid
|
||||||
|
else:
|
||||||
|
target['pane']['cmd_full'] = os.environ.get('CMD_FULL_OPT', '') or 'agy --dangerously-skip-permissions'
|
||||||
cp = os.environ.get('CHILD_PID', '0')
|
cp = os.environ.get('CHILD_PID', '0')
|
||||||
if cp.isdigit() and int(cp) > 0:
|
if cp.isdigit() and int(cp) > 0:
|
||||||
target['child_pid'] = int(cp)
|
target['child_pid'] = int(cp)
|
||||||
elif agent == 'hermes':
|
elif agent == 'hermes':
|
||||||
target['pane']['cmd'] = 'hermes'
|
target['pane']['cmd'] = 'hermes'
|
||||||
|
if uuid:
|
||||||
target['pane']['cmd_full'] = f'hermes --resume {uuid}'
|
target['pane']['cmd_full'] = f'hermes --resume {uuid}'
|
||||||
target['hermes_conversation_id_own'] = uuid
|
target['hermes_conversation_id_own'] = uuid
|
||||||
|
else:
|
||||||
|
target['pane']['cmd_full'] = os.environ.get('CMD_FULL_OPT', '') or 'hermes --yolo --accept-hooks'
|
||||||
cp = os.environ.get('CHILD_PID', '0')
|
cp = os.environ.get('CHILD_PID', '0')
|
||||||
if cp.isdigit() and int(cp) > 0:
|
if cp.isdigit() and int(cp) > 0:
|
||||||
target['child_pid'] = int(cp)
|
target['child_pid'] = int(cp)
|
||||||
@@ -199,8 +227,11 @@ elif agent == 'grok':
|
|||||||
target['child_pid'] = int(cp)
|
target['child_pid'] = int(cp)
|
||||||
elif agent == 'opencode':
|
elif agent == 'opencode':
|
||||||
target['pane']['cmd'] = 'opencode'
|
target['pane']['cmd'] = 'opencode'
|
||||||
|
if uuid:
|
||||||
target['pane']['cmd_full'] = f'opencode --session {uuid} --auto --agent build'
|
target['pane']['cmd_full'] = f'opencode --session {uuid} --auto --agent build'
|
||||||
target['opencode_session_id_own'] = uuid
|
target['opencode_session_id_own'] = uuid
|
||||||
|
else:
|
||||||
|
target['pane']['cmd_full'] = os.environ.get('CMD_FULL_OPT', '') or 'opencode --auto --agent build'
|
||||||
cp = os.environ.get('CHILD_PID', '0')
|
cp = os.environ.get('CHILD_PID', '0')
|
||||||
if cp.isdigit() and int(cp) > 0:
|
if cp.isdigit() and int(cp) > 0:
|
||||||
target['child_pid'] = int(cp)
|
target['child_pid'] = int(cp)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
import pytest
|
import pytest
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -389,3 +390,125 @@ def test_t13_mam_workspace_key_equivalence(mam_sandbox):
|
|||||||
abs_path = os.path.realpath(v)
|
abs_path = os.path.realpath(v)
|
||||||
py_key = abs_path.replace("/", "-").replace("_", "-")
|
py_key = abs_path.replace("/", "-").replace("_", "-")
|
||||||
assert sh_key == py_key, f"Mismatch for {v}: shell={sh_key}, py={py_key}"
|
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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user