Files
multi-agent-mux/.agents/skills/lib_py/herdr_restore.py
T
GodopuandClaude Opus 5.5 fb1380fdc1 fix(issue-45): resume closes the session's own restored empty shell
After a restart with restore off, the session's saved pane comes back as an
empty shell (in $HOME when its directory is gone) holding the agent_session it
reported. A resume with a CP_PREFIX cannot reuse it, so it stayed next to the
new pane. Just before spawning, resume closes panes whose agent_session is the
id being resumed, with no live agent and only the shell in the foreground; the
registry is not consulted, since a session whose server died stays running.

tests/test_issue45_restore.py drives a real herdr server under a throwaway HOME
and session with a fake claude: restore stays off through the MAM bootstrap
(with a restore-on control), the close conditions, and create -> crash ->
resume leaving one pane.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 10:32:49 +00:00

89 lines
3.7 KiB
Python

"""Issue #45 (B): close the restored empty shells of the session being resumed.
With restore-on-restart off (lib_py/herdr_config.py) a restarted MAM server
still brings every saved pane back - as an empty shell in its old cwd, or in
$HOME when that directory is gone. A pane keeps the `agent_session` its agent
reported before the server died. resume_session.sh calls this once, right
before it spawns, with the id it is about to resume; a pane is closed only when
all of these hold:
1. the target is an isolated MAM server (HERDR_SESSION_NAME set, not default);
2. herdr sees no live agent in it;
3. its agent_session value is that id - so a user's pane, another session's
pane and a freshly split pane (no agent_session yet) are never candidates;
4. its foreground process is the shell itself (nothing running in it).
The registry is not consulted: a session whose server died stays `running`
there, so "running" cannot tell a live pane from a restored one.
CLI: `python3 -m lib_py.herdr_restore close-restored-shells --session-id ID
[--herdr PATH]`. One stderr line per closed pane; exit 1 when herdr cannot be
queried (the caller only warns).
"""
from __future__ import annotations
import argparse
import json
import os
import subprocess
import sys
from typing import List, Optional
# A pane exports these; they would point the query at the caller's own server
# and workspace instead of the --session named here.
_SCOPE_ENV = ("HERDR_WORKSPACE_ID", "HERDR_PANE_ID", "HERDR_TAB_ID", "HERDR_SOCKET_PATH")
def _herdr(herdr: str, session: str, *args: str) -> dict:
env = {k: v for k, v in os.environ.items() if k not in _SCOPE_ENV}
out = subprocess.run([herdr, "--session", session, *args], env=env,
capture_output=True, text=True, timeout=30, check=True).stdout
return json.loads(out).get("result") or {}
def _shell_in_foreground(herdr: str, session: str, pane_id: str) -> bool:
info = _herdr(herdr, session, "pane", "process-info", "--pane", pane_id).get("process_info") or {}
shell = info.get("shell_pid")
return shell is not None and info.get("foreground_process_group_id") == shell
def close_restored_shells(session_id: str, herdr: str = "herdr",
session: Optional[str] = None) -> List[str]:
"""Close the matching panes; return their ids."""
session = os.environ.get("HERDR_SESSION_NAME", "") if session is None else session
if not session_id or session in ("", "default"):
return []
closed = []
for p in _herdr(herdr, session, "pane", "list").get("panes") or []:
pane_id = p.get("pane_id")
if not pane_id or p.get("agent"):
continue
if (p.get("agent_session") or {}).get("value") != session_id:
continue
if not _shell_in_foreground(herdr, session, pane_id):
continue
_herdr(herdr, session, "pane", "close", pane_id)
print(f"closed restored pane {pane_id} (cwd={p.get('cwd', '')}, agent_session={session_id})",
file=sys.stderr)
closed.append(pane_id)
return closed
def main(argv=None) -> int:
p = argparse.ArgumentParser(description="close a resumed session's restored empty shells (Issue #45)")
sub = p.add_subparsers(dest="cmd", required=True)
c = sub.add_parser("close-restored-shells")
c.add_argument("--session-id", required=True)
c.add_argument("--herdr", default="", help="the real herdr binary (not the MAM shim)")
args = p.parse_args(argv)
try:
close_restored_shells(args.session_id, args.herdr or "herdr")
except (OSError, subprocess.SubprocessError, ValueError) as e:
print(f"close-restored-shells: {e}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
sys.exit(main())