fix(issue-24): resume answers start-up dialogs for every agent, with the same switch as create

resume_session.sh called handle_startup_dialogs only when AGENT=claude and
passed no agent, so a resumed agy, grok or codex sat on its directory-trust
prompt exactly as the create path did before this branch. The gate is gone and
the agent is passed, which is the call create already makes.

--no-auto-trust / MAM_AUTO_TRUST apply to resume too. A resumed CLI shows the
same trust dialog as a fresh one, in the same directory, with the same
consequence for codex (trusting loads the project's local config, hooks and
exec policy), and handle_startup_dialogs already reads MAM_AUTO_TRUST - so the
only question was whether resume exposes the switch or silently inherits the
caller's environment. Exposing it keeps the two skills' contracts identical.

The tests run the shipped create/stop/resume scripts against the mock herdr.
The mock's pane always shows a ready banner, so they fix the call resume makes
(session, timeout, agent, MAM_AUTO_TRUST) by overriding the handler in the
sandbox copy of lib.sh, plus one unmodified run showing the real handler is
reached for a non-claude agent; which keys a dialog gets is fixed on the create
side (S4-S6).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
GodopuandClaude Fable 5.1 committed 2026-09-22 08:19:18 +00:00
1 parent ce802d21c8
commit 0dfbb2cc8a
3 files changed
+138 -15

No files matched your search

+5 -11
View File
@@ -101,17 +101,11 @@ case "$AGENT" in
esac esac
# 4. Spawn new herdr session + run agent with the saved id # 4. Spawn new herdr session + run agent with the saved id
case "$AGENT" in eval "herdr new-session -d -s \"$SESSION_NAME\" -x 140 -y 40 -c \"$WORKSPACE\" \"$CMD_FULL\""
claude) # Issue #24: start-up dialogs (trust prompts, blocking notices) are answered for
START_CMD="herdr new-session -d -s \"$SESSION_NAME\" -x 140 -y 40 -c \"$WORKSPACE\" \"$CMD_FULL\"" # every agent from the adapter's signatures - the same call create makes.
eval "$START_CMD" # --no-auto-trust leaves a directory-trust dialog to a human (MAM_AUTO_TRUST=0).
# auto-handle trust / bypass dialogs handle_startup_dialogs "$SESSION_NAME" 20 "$AGENT"
handle_startup_dialogs "$SESSION_NAME" 20
;;
agy|hermes|grok|opencode|codex)
eval "herdr new-session -d -s \"$SESSION_NAME\" -x 140 -y 40 -c \"$WORKSPACE\" \"$CMD_FULL\""
;;
esac
# 4. Update agent-sessions.yaml: status running, last_visible_status # 4. Update agent-sessions.yaml: status running, last_visible_status
# (Also automatically publishes a `progress --detail "resumed"` event to the multi-agent-mux-delegate-job registry if a delegate_job_id exists) # (Also automatically publishes a `progress --detail "resumed"` event to the multi-agent-mux-delegate-job registry if a delegate_job_id exists)
@@ -13,6 +13,8 @@ Usage: $0 --workspace <path> --agent <claude|agy|hermes|grok|opencode|codex> --s
Options: Options:
--herdr-session NAME specify isolated herdr session name (alias: --herdr-server) --herdr-session NAME specify isolated herdr session name (alias: --herdr-server)
--no-auto-trust do not answer a start-up directory-trust dialog (default: answer it;
other blocking notices are always answered) - same switch as create
--dry-run Simulates resume flow (resolves binary, environment) without writing --dry-run Simulates resume flow (resolves binary, environment) without writing
any updates to YAML or DB. Safe to execute inside active write transactions. any updates to YAML or DB. Safe to execute inside active write transactions.
EOF EOF
@@ -25,6 +27,10 @@ HERDR_SERVER_OPT=""
HERDR_WORKSPACE_OPT="" HERDR_WORKSPACE_OPT=""
DRY_RUN=0 DRY_RUN=0
# Issue #24: a resumed CLI shows the same directory-trust dialog as a fresh
# one, so resume takes the same switch as create (MAM_AUTO_TRUST is what
# handle_startup_dialogs reads).
AUTO_TRUST=1
while [ $# -gt 0 ]; do while [ $# -gt 0 ]; do
case "$1" in case "$1" in
@@ -33,6 +39,8 @@ while [ $# -gt 0 ]; do
--session) SESSION_NAME="$2"; shift 2 ;; --session) SESSION_NAME="$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 ;;
--auto-trust) AUTO_TRUST=1; shift ;;
--no-auto-trust) AUTO_TRUST=0; shift ;;
--dry-run) DRY_RUN=1; shift ;; --dry-run) DRY_RUN=1; shift ;;
-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 ;;
@@ -46,6 +54,7 @@ case "$AGENT" in
*) echo "ERROR: unsupported agent: $AGENT" >&2; exit 2 ;; *) echo "ERROR: unsupported agent: $AGENT" >&2; exit 2 ;;
esac esac
[ -n "$SESSION_NAME" ] || { echo "ERROR: --session required" >&2; exit 2; } [ -n "$SESSION_NAME" ] || { echo "ERROR: --session required" >&2; exit 2; }
export MAM_AUTO_TRUST="$AUTO_TRUST"
# Issue #21 phase 2 (A11): a session resumes only in the directory it ran in # Issue #21 phase 2 (A11): a session resumes only in the directory it ran in
# (its conversation is keyed by that cwd; a class A agent would otherwise start # (its conversation is keyed by that cwd; a class A agent would otherwise start
@@ -226,10 +235,10 @@ fi
# 4. Spawn new agent session (delegates to herdr translation shim) # 4. Spawn new agent session (delegates to herdr translation shim)
_herdr new-session -d -s "$SESSION_NAME" -c "$WORKSPACE" "$CP_PREFIX$CMD_FULL" _herdr new-session -d -s "$SESSION_NAME" -c "$WORKSPACE" "$CP_PREFIX$CMD_FULL"
if [ "$AGENT" = "claude" ]; then # Issue #24: every agent, the same call create makes. The signatures and accept
# auto-handle trust / bypass dialogs # keys come from the agent adapter; this used to run only for claude, so a
handle_startup_dialogs "$SESSION_NAME" 20 # resumed agy/grok/codex sat on its trust prompt.
fi handle_startup_dialogs "$SESSION_NAME" 20 "$AGENT"
# Wait for TUI readiness or let it settle # Wait for TUI readiness or let it settle
sleep 2 sleep 2
+120
View File
@@ -0,0 +1,120 @@
"""Issue #24, resume path: start-up dialogs are answered for every agent.
resume_session.sh called handle_startup_dialogs only for claude and passed no
agent, so a resumed agy/grok/codex sat on its directory-trust prompt exactly as
the create path did before this branch. These fix the call contract resume
now shares with create.
What is real: the shipped create/stop/resume scripts run end to end against
the mock herdr in a sandbox. What is not: the mock's pane always shows a
ready banner, so no test here drives a real trust dialog - the create-side
tests (test_issue23_24_startup_path.py, S4-S6) fix which keys each dialog
gets, and R4 shows the same shipped function is now reached from resume.
"""
import json
import os
import subprocess
import pytest
SCRIPTS = ("multi-agent-mux-create/scripts/create_session.sh",
"multi-agent-mux-stop/scripts/stop_session.sh",
"multi-agent-mux-resume/scripts/resume_session.sh")
def _script(tmp_path, rel):
return str(tmp_path / ".agents" / "skills" / rel)
def _create_and_stop(tmp_path, agent, name):
ws = str(tmp_path / ("ws_" + name))
os.makedirs(ws, exist_ok=True)
create, stop, _ = (_script(tmp_path, s) for s in SCRIPTS)
subprocess.run(["bash", create, "--workspace", ws, "--agent", agent,
"--role", "creator", "--session", name, "--no-onboard"],
check=True, cwd=str(tmp_path), capture_output=True, text=True)
subprocess.run(["bash", stop, "--session", name, "--agent", agent],
check=True, cwd=str(tmp_path), capture_output=True, text=True)
return ws
def _resume(tmp_path, agent, name, ws, extra=()):
_, _, resume = (_script(tmp_path, s) for s in SCRIPTS)
return subprocess.run(["bash", resume, "--workspace", ws, "--agent", agent,
"--session", name, *extra],
cwd=str(tmp_path), capture_output=True, text=True)
def _record_dialog_calls(tmp_path):
"""Override handle_startup_dialogs in the SANDBOX copy of lib.sh (never the
repo's) so the call resume makes - session, timeout, agent, MAM_AUTO_TRUST
- is written down. Bash keeps the last definition, so appending wins."""
log = tmp_path / "dialog_calls.log"
lib = tmp_path / ".agents" / "skills" / "lib.sh"
with open(lib, "a") as f:
f.write('\nhandle_startup_dialogs() {\n'
' printf \'%%s\\n\' "sess=$1 timeout=${2:-} agent=${3:-<none>} '
'auto_trust=${MAM_AUTO_TRUST:-<unset>}" >> "%s"\n'
'}\n' % log)
return log
def _calls(log):
return log.read_text().splitlines() if log.exists() else []
@pytest.mark.parametrize("agent", ["agy", "codex"])
def test_r1_resume_answers_dialogs_for_a_non_claude_agent_and_names_it(
mam_sandbox, mock_herdr, mock_agents, agent):
"""R1: the gate is gone and the agent is passed - the two halves of the
create contract resume was missing."""
tmp_path = mam_sandbox
name = "test-r1-" + agent
ws = _create_and_stop(tmp_path, agent, name)
log = _record_dialog_calls(tmp_path) # armed after create, so only resume is recorded
res = _resume(tmp_path, agent, name, ws)
assert res.returncode == 0, res.stdout + res.stderr
assert _calls(log) == ["sess=%s timeout=20 agent=%s auto_trust=1" % (name, agent)]
def test_r2_claude_keeps_its_dialog_handling(mam_sandbox, mock_herdr, mock_agents):
"""R2: claude is still handled, now by the same call as everyone else."""
tmp_path = mam_sandbox
ws = _create_and_stop(tmp_path, "claude", "test-r2-claude")
log = _record_dialog_calls(tmp_path)
res = _resume(tmp_path, "claude", "test-r2-claude", ws)
assert res.returncode == 0, res.stdout + res.stderr
assert _calls(log) == ["sess=test-r2-claude timeout=20 agent=claude auto_trust=1"]
def test_r3_no_auto_trust_reaches_the_dialog_handler(mam_sandbox, mock_herdr, mock_agents):
"""R3: --no-auto-trust on resume sets MAM_AUTO_TRUST=0 for the handler,
the same switch with the same meaning as create's."""
tmp_path = mam_sandbox
ws = _create_and_stop(tmp_path, "codex", "test-r3-codex")
log = _record_dialog_calls(tmp_path)
res = _resume(tmp_path, "codex", "test-r3-codex", ws, extra=("--no-auto-trust",))
assert res.returncode == 0, res.stdout + res.stderr
assert _calls(log) == ["sess=test-r3-codex timeout=20 agent=codex auto_trust=0"]
def test_r4_the_shipped_handler_really_runs_for_a_non_claude_resume(
mam_sandbox, mock_herdr, mock_agents):
"""R4 (no override): with the real handle_startup_dialogs, a codex resume
completes and the pane is read after the spawn - the handler was reached
and returned on the ready banner. Before this change no pane read happened
between new-session and the YAML update for a non-claude agent."""
tmp_path = mam_sandbox
ws = _create_and_stop(tmp_path, "codex", "test-r4-codex")
before = len(json.load(open(mock_herdr)).get("calls", []))
res = _resume(tmp_path, "codex", "test-r4-codex", ws)
assert res.returncode == 0, res.stdout + res.stderr
calls = json.load(open(mock_herdr)).get("calls", [])[before:]
def _has(c, *verbs): # the mock records argv with a leading `--session <name>`
return any(tuple(c[i:i + len(verbs)]) == verbs for i in range(len(c)))
spawn = next(i for i, c in enumerate(calls) if _has(c, "agent", "start"))
reads = [c for c in calls[spawn + 1:] if _has(c, "agent", "read") or _has(c, "pane", "read")]
assert reads, "no pane read after the spawn: handle_startup_dialogs never ran\n%s" % calls