fix(issue-45): MAM herdr servers start with restore-on-restart off; resume closes its own restored empty shell (v4.8.5) #48

Merged
Godopu merged 14 commits from fix/issue-45 into main 2026-10-05 09:56:40 +00:00
Owner

Closes #45

Scope agreed with the issue owner on 2026-10-04: A + B only. Proposals 2 (resume's "already running" command-line check) and 4 (stopping agents herdr has no name for) are out of scope. A server started by the user's own herdr session attach gets no env var, so restore stays on there. That path is handled by user care, not code: after a reboot, run /multi-agent-mux-resume before attaching.

A. MAM's isolated herdr servers start with restore-on-restart off

  • herdr 0.9.1 has [session] resume_agents_on_restore (default true). The headless bootstrap in lib.sh now passes the server HERDR_CONFIG_PATH.
  • The file it points to is rebuilt on every server start by lib_py/herdr_config.py. It is the user's herdr config with only that key forced to false.
  • Where the source comes from: HERDR_CONFIG_PATH, then XDG_CONFIG_HOME, then ~/.config, the same order herdr uses.
  • Where the output goes: next to the source, so relative paths still resolve. The user's own file is never modified.
  • Safety checks:
    • The merge is parse → set → serialize, then re-parsed. It must equal the source plus the key.
    • The written file must pass herdr config check. herdr drops a file it cannot use and silently restores again, so this check matters.
    • On any failure, a file holding only the key is written instead.
    • The output never overwrites the source or a file that was not generated for that source.

B. Restored empty shells

  • With restore off, a saved pane still comes back. It returns as an empty shell, in $HOME if its directory is gone, and still holds its agent_session.
  • Just before spawning, resume_session.sh closes the panes that meet all of these:
    • they are in an isolated MAM server;
    • they have no live agent;
    • their agent_session equals the id being resumed;
    • only the shell is in the foreground.
  • User panes and other sessions' panes are never touched.

Verification

  • Loop: planner plus 4 reviewers (claude, agy, grok, codex), unanimous PASS in round 3.
    • Round 1 fixes: check the file with herdr config check; keep any TOML spelling; never overwrite the source.
    • Round 2 fix: the .mam fallback follows the same no-overwrite rules.
  • New tests: tests/test_issue45_restore.py drives a real herdr server under a throwaway HOME and session. It covers restore-off with a restore-on control, the close conditions, and create → crash → resume leaving one pane.
  • Real-agent trial (orchestrator, real claude, throwaway session): this covers what the fake-claude tests do not.
    • The MAM bootstrap passed HERDR_CONFIG_PATH.
    • After kill -9 of the server and claude, a MAM restart did not relaunch claude.
    • The restored shell carrying the same agent_session was closed by the B step.
    • The generated config equals the user config plus the key.
  • Control run on the default config: claude --resume <id> came back without permission flags, reproducing #45.
  • Full suite at 35935f9 in a detached worktree: 12 failed / 1446 passed / 20 skipped (49 min). The 12 are the known baseline failures in tests/test_issue10_13_purge.py.

🤖 Generated with Claude Code

Closes #45 Scope agreed with the issue owner on 2026-10-04: **A + B** only. Proposals 2 (resume's "already running" command-line check) and 4 (stopping agents herdr has no name for) are out of scope. A server started by the user's own `herdr session attach` gets no env var, so restore stays on there. That path is handled by user care, not code: after a reboot, run `/multi-agent-mux-resume` before attaching. ## A. MAM's isolated herdr servers start with restore-on-restart off - herdr 0.9.1 has `[session] resume_agents_on_restore` (default `true`). The headless bootstrap in `lib.sh` now passes the server `HERDR_CONFIG_PATH`. - The file it points to is rebuilt on every server start by `lib_py/herdr_config.py`. It is the user's herdr config with only that key forced to `false`. - Where the source comes from: `HERDR_CONFIG_PATH`, then `XDG_CONFIG_HOME`, then `~/.config`, the same order herdr uses. - Where the output goes: next to the source, so relative paths still resolve. The user's own file is never modified. - Safety checks: - The merge is parse → set → serialize, then re-parsed. It must equal the source plus the key. - The written file must pass `herdr config check`. herdr drops a file it cannot use and silently restores again, so this check matters. - On any failure, a file holding only the key is written instead. - The output never overwrites the source or a file that was not generated for that source. ## B. Restored empty shells - With restore off, a saved pane still comes back. It returns as an empty shell, in `$HOME` if its directory is gone, and still holds its `agent_session`. - Just before spawning, `resume_session.sh` closes the panes that meet all of these: - they are in an isolated MAM server; - they have no live agent; - their `agent_session` equals the id being resumed; - only the shell is in the foreground. - User panes and other sessions' panes are never touched. ## Verification - **Loop:** planner plus 4 reviewers (claude, agy, grok, codex), unanimous PASS in round 3. - Round 1 fixes: check the file with `herdr config check`; keep any TOML spelling; never overwrite the source. - Round 2 fix: the `.mam` fallback follows the same no-overwrite rules. - **New tests:** `tests/test_issue45_restore.py` drives a real herdr server under a throwaway HOME and session. It covers restore-off with a restore-on control, the close conditions, and create → crash → resume leaving one pane. - **Real-agent trial (orchestrator, real claude, throwaway session):** this covers what the fake-claude tests do not. - The MAM bootstrap passed `HERDR_CONFIG_PATH`. - After `kill -9` of the server and claude, a MAM restart did not relaunch claude. - The restored shell carrying the same `agent_session` was closed by the B step. - The generated config equals the user config plus the key. - **Control run on the default config:** `claude --resume <id>` came back without permission flags, reproducing #45. - **Full suite at 35935f9 in a detached worktree:** 12 failed / 1446 passed / 20 skipped (49 min). The 12 are the known baseline failures in `tests/test_issue10_13_purge.py`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Godopu added 10 commits 2026-10-04 12:52:23 +00:00
lib_py/herdr_config.py builds the user's herdr config (HERDR_CONFIG_PATH, else
XDG_CONFIG_HOME, else ~/.config, as herdr reads it) with only
[session] resume_agents_on_restore forced to false, written next to the source
so relative paths resolve the same. The text merge is re-parsed; a merge that
does not leave the rest equal falls back to the key alone, since herdr drops a
config that does not parse and would restore again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The headless bootstrap regenerates the server config on every start and hands
it to the server as HERDR_CONFIG_PATH. Without a file the server still starts,
with a warning. mam_sandbox unsets HERDR_CONFIG_PATH/XDG_CONFIG_HOME so the
file lands in the sandbox HOME.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
S11 keeps the generated .mam/shim/herdr from changing by accident; #45 changes
it on purpose, so its base moves to ed73f67.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The shim runs a bare python3. Before 3.11 the module failed to import and the
server started with restore on; now it gets the key alone, with a warning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Issue #45 A+B: MAM's isolated herdr servers start with restore-on-restart off,
and resume closes the session's own restored empty shell. Not reviewed yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
R1: tomllib accepting a file is not herdr accepting it (a schema error makes
herdr drop the whole file, restore included). The shim passes REAL_HERDR and
the written file must pass `herdr config check` ("parse error"/"using defaults"
reject; unknown keys and sound-file notices do not), else the key-only file is
written and checked. A non-table `session` and a source that is not UTF-8 reach
that fallback instead of raising.

R2: the line edit is replaced by parse -> set -> serialize; the result must
parse back to the source plus the key, so dotted keys and inline tables keep
the user's other settings.

R3: the output is never the source (aliases included) nor a file not generated
for the same source; then a per-source name beside it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fallback wrote <.mam>/herdr-server-config.toml unchecked, so it could
replace a foreign file there, the source itself, or a link to it. It now goes
through output_path like the normal path (source identity incl. aliases,
generated-for-this-source ownership, then a per-source name) and writes
nothing when no name is safe.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Godopu added 2 commits 2026-10-05 08:18:45 +00:00
tests/test_issue45_herdr_config.py imported tomllib at module level. On
Python 3.9 - macOS's /usr/bin/python3, which the venvs and the bootstrap's
python3 use here - the import failed during collection and pytest ran no
test of the whole suite.

The import is optional now, and the 29 tests that parse a result with
tomllib or expect the user's settings kept are marked needs_tomllib: on
Python < 3.11 herdr_config deliberately writes the key-only config (restore
still off, the other settings dropped, a warning on stderr), which
test_without_tomllib_restore_is_still_off covers on every Python.
Python 3.9: 22 passed / 29 skipped; Python 3.11: 51 passed.

create SKILL.md (same line, so the #42 inventory is unchanged) and the
4.8.5 entry state that building the server config needs tomllib (Python
3.11+) in the python3 on PATH, and what an older python3 loses. Checked
with Python 3.9 in a throwaway HOME: the warning reaches stderr (the
bootstrap captures only stdout) and the user's file is untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
claude round 1, B2 and B3 (B1 was the previous commit).

- B2, tests/test_issue45_restore.py: the throwaway root is resolved, because
  on macOS /tmp links to /private/tmp and herdr reports real paths (two B
  tests compared /tmp/... with /private/tmp/...). The server's environment
  is read from /proc on Linux and from `ps -E` elsewhere (macOS has no /proc;
  two A tests failed with FileNotFoundError).
- B3, tests/test_issue9_server_env.py: the bootstrap builds the config with
  the python3 on PATH, which here is 3.9 without tomllib, so the test's
  merged-settings expectation could not hold and its own `import tomllib`
  failed. The test now parses the result with that same python3; when it
  has no tomllib, it expects the key-only config and the bootstrap's
  "no tomllib" warning instead.

macOS, Python 3.9 venv: test_issue45_restore, test_issue9_server_env and
test_issue45_herdr_config 42 passed / 29 skipped (the tomllib-only checks).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
Godopu added 1 commit 2026-10-05 08:50:17 +00:00
codex round 2 (PR #48).

- R1: when `herdr config check` could not run (timeout, OSError), the
  merged config was handed to the server as checked. A merge with a schema
  error makes herdr drop the whole file, restore back on - the #45 failure.
  The merged body is no longer used unchecked: _write_checked falls to the
  fixed MINIMAL config with a warning. When the check cannot run on MINIMAL
  either, MINIMAL is still used (with its own warning) rather than no
  config: returning none would leave restore on, and MINIMAL is a fixed
  two-line file the real-herdr test accepts. Unknown non-zero exits stay as
  they were - herdr exits non-zero for harmless warnings (an unknown
  section) that do not drop the file.
- R2: _shell_in_foreground compared shell_pid and the foreground group with
  ==, so 0/0, False/False, ""/"" or -1/-1 closed the pane. Both must now be
  positive ints (bool excluded) before they are compared.
- R3: the re-parse check used ==, and nan != nan, so a valid TOML nan threw
  away every other user setting even on Python 3.11. _same_doc compares
  documents recursively with nan equal to nan.

Tests: R1 (timeout, OSError) and R3 (top-level, array, nested nan) in
test_issue45_herdr_config.py; R2 in the new tests/test_issue45_restore_unit.py
(a herdr stub: zero, false, empty, negative, null, string, true, float;
missing info; a real shell closed; a busy pane kept). On the previous code
12 of them fail. Python 3.11: 67 passed; Python 3.9: 33 passed / 34 skipped
(tomllib-only). Real-herdr restore tests and related files on 3.9: 197
passed / 1 skipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
Godopu merged commit 7209750ac7 into main 2026-10-05 09:56:40 +00:00
Author
Owner

병합 요약 (v4.8.5)

병합 커밋 7209750. Closes #45 (범위 A+B).

회차 head claude agy grok codex
1 35935f9 NOT PASS NOT PASS NOT PASS 판정 없음
2 e8d9151 PASS PASS PASS NOT PASS
3 ab93776 PASS PASS PASS PASS

1회차 — 테스트 이식성 (36f0b8d, e8d9151)

  • tests/test_issue45_herdr_config.py 가 모듈 머리에서 import tomllib 을 해, Python 3.9(macOS 기본 /usr/bin/python3)에서는 스위트 전체가 수집 단계에서 멈췄다. import 를 선택적으로 바꾸고 tomllib 이 필요한 검사만 skip.
  • tests/test_issue45_restore.py 의 Linux 가정: macOS 에 /proc 가 없고 /tmp 는 /private/tmp 로 보고된다 → 경로 정규화, ps -E 로 환경 확인.
  • tests/test_issue9_server_env.py 는 bootstrap 이 쓰는 PATH 의 python3 로 결과를 확인(그 python3 에 tomllib 이 없으면 키 하나짜리 설정과 경고를 기대).
  • 문서: 설정 병합은 PATH 의 python3 에 tomllib(3.11+)이 필요하며, 그보다 오래된 python3 에서는 복원은 꺼지지만 사용자의 다른 herdr 설정 없이 뜬다(경고 표시).
  • codex 는 모델 용량 초과("Selected model is at capacity")로 1 시간 예산이 다해 판정을 내지 못했다.

2회차 — codex 의 제품 코드 지적 (ab93776)

  • R1 herdr config check 를 실행하지 못하면(timeout·OSError) 검사 안 된 병합을 넘겼다. 스키마 오류가 있으면 herdr 가 파일 전체를 버려 복원이 다시 켜진다(#45 재발). → 검사 못 한 병합은 쓰지 않고 키 하나짜리 MINIMAL 로. MINIMAL 의 검사도 실행하지 못하면 경로를 안 넘기는 대신 MINIMAL 을 경고와 함께 쓴다(아무것도 안 넘기면 복원이 켜진 채 남는다 — 3회차에서 claude 동의).
  • R2 pid 응답이 0·False·""·-1 이어도 같다고 보고 pane 을 닫았다 → 양의 정수일 때만 비교.
  • R3 유효한 TOML nan 이 재파싱 비교(==)를 깨 Python 3.11 에서도 사용자 설정을 모두 버렸다 → nan 끼리 같다고 보는 재귀 비교.
  • 회귀 테스트(새 tests/test_issue45_restore_unit.py 포함) 12 건이 수정 전 코드에서 실패.

테스트

  • 전체 스위트(ab93776, macOS·Python 3.9, MQTT 환경변수 없이): 1456 passed / 0 failed / 38 skipped(65 분). skip 중 34 건은 3.9 의 tomllib 전용 검사(3.11 에서는 herdr_config·restore_unit 67 passed).
  • PR 본문의 "purge 12 건 기준 실패"는 Linux(#28)의 것이고 이 호스트에서는 통과.

후속 (비차단)

  • Python 3.11 미만에서도 사용자 herdr 설정을 유지하려면 tomli 백포트로 병합(grok 제안). 이번에는 문서화로 정했다.
  • 범위 밖으로 합의된 #45 제안 2(resume 의 "already running" 명령줄 검증)와 4(이름 없이 뜬 에이전트 정지), 사용자가 직접 attach 해 띄운 서버의 복원.

🤖 Generated with Claude Code

## 병합 요약 (v4.8.5) 병합 커밋 7209750. Closes #45 (범위 A+B). | 회차 | head | claude | agy | grok | codex | |---|---|---|---|---|---| | 1 | 35935f9 | NOT PASS | NOT PASS | NOT PASS | 판정 없음 | | 2 | e8d9151 | PASS | PASS | PASS | NOT PASS | | 3 | ab93776 | PASS | PASS | PASS | PASS | **1회차 — 테스트 이식성 (36f0b8d, e8d9151)** - `tests/test_issue45_herdr_config.py` 가 모듈 머리에서 `import tomllib` 을 해, Python 3.9(macOS 기본 `/usr/bin/python3`)에서는 **스위트 전체가 수집 단계에서 멈췄다**. import 를 선택적으로 바꾸고 `tomllib` 이 필요한 검사만 skip. - `tests/test_issue45_restore.py` 의 Linux 가정: macOS 에 `/proc` 가 없고 `/tmp` 는 `/private/tmp` 로 보고된다 → 경로 정규화, `ps -E` 로 환경 확인. - `tests/test_issue9_server_env.py` 는 bootstrap 이 쓰는 PATH 의 `python3` 로 결과를 확인(그 python3 에 `tomllib` 이 없으면 키 하나짜리 설정과 경고를 기대). - 문서: 설정 병합은 PATH 의 `python3` 에 `tomllib`(3.11+)이 필요하며, 그보다 오래된 python3 에서는 복원은 꺼지지만 사용자의 다른 herdr 설정 없이 뜬다(경고 표시). - codex 는 모델 용량 초과("Selected model is at capacity")로 1 시간 예산이 다해 판정을 내지 못했다. **2회차 — codex 의 제품 코드 지적 (ab93776)** - **R1** `herdr config check` 를 실행하지 못하면(timeout·OSError) 검사 안 된 병합을 넘겼다. 스키마 오류가 있으면 herdr 가 파일 전체를 버려 복원이 다시 켜진다(#45 재발). → 검사 못 한 병합은 쓰지 않고 키 하나짜리 MINIMAL 로. MINIMAL 의 검사도 실행하지 못하면 경로를 안 넘기는 대신 MINIMAL 을 경고와 함께 쓴다(아무것도 안 넘기면 복원이 켜진 채 남는다 — 3회차에서 claude 동의). - **R2** pid 응답이 0·False·""·-1 이어도 같다고 보고 pane 을 닫았다 → 양의 정수일 때만 비교. - **R3** 유효한 TOML `nan` 이 재파싱 비교(`==`)를 깨 Python 3.11 에서도 사용자 설정을 모두 버렸다 → nan 끼리 같다고 보는 재귀 비교. - 회귀 테스트(새 `tests/test_issue45_restore_unit.py` 포함) 12 건이 수정 전 코드에서 실패. **테스트** - 전체 스위트(ab93776, macOS·Python 3.9, MQTT 환경변수 없이): **1456 passed / 0 failed / 38 skipped**(65 분). skip 중 34 건은 3.9 의 `tomllib` 전용 검사(3.11 에서는 herdr_config·restore_unit 67 passed). - PR 본문의 "purge 12 건 기준 실패"는 Linux(#28)의 것이고 이 호스트에서는 통과. **후속 (비차단)** - Python 3.11 미만에서도 사용자 herdr 설정을 유지하려면 `tomli` 백포트로 병합(grok 제안). 이번에는 문서화로 정했다. - 범위 밖으로 합의된 #45 제안 2(resume 의 "already running" 명령줄 검증)와 4(이름 없이 뜬 에이전트 정지), 사용자가 직접 attach 해 띄운 서버의 복원. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: tmpl/multi-agent-mux#48