- Add Rev. 3 technical analysis report from creator-agy-01 - Add implementation plan from planner-reviewer-claude-01 - Add peer review reports across analysis and implementation review loops (Claude, Grok, OpenCode)
11 KiB
11 KiB
🔬 Technical Analysis Report: Issue #3 (Rev. 3)
Herdr Daemon Process Separation (setsid) & 0-Turn Stopped Session Resume Recovery Gap
- Author:
creator-agy-01(Worker / Lead Technical Analyst) - Job ID:
6b391a80(Follow-up to842b96bf, incorporating peer review feedback fromplanner-reviewer-claude-01[job0338e7de] andreviewer-opencode-01[job7b6c16df]) - Target Issue: Issue #3 (
[Bug] herdr 데몬 프로세스 분리(setsid) 누락으로 인한 세션 리셋 및 UUID 미할당 세션 resume 데드락 문제) - Analyzed Components:
.agents/skills/lib.sh(lines 198–220,.mam/shim/herdrdaemon bootstrap).agents/skills/lib_py/verify_session.py(lines 83–109,verify_session_uuidescape hatch at lines 99–101).agents/skills/lib_py/workspace_uuid.py(lines 55–80,find_workspace_uuid).agents/skills/multi-agent-mux-resume/scripts/resume_session.sh(lines 50–57, 98–109).agents/skills/multi-agent-mux-create/scripts/create_session.sh(lines 177–195, 370–440).agents/skills/multi-agent-mux-stop/scripts/stop_session.sh(lines 183–191, 280–293)tests/test_uuid_target.py(test_t8_resume_unmaterialized_assigned_id)
- Scope Restriction: Analysis and defect determination only — no implementation or code changes performed in this job.
1. Executive Summary & Defect Verdict Matrix
| Item # | Reported Issue | Target Location | Live Mechanism & Root Cause | Structural Defect Status | Severity |
|---|---|---|---|---|---|
| 1 | Missing setsid on headless Herdr daemon bootstrap causes daemon termination and session resets during process-group teardown. |
lib.sh:211 (and .mam/shim/herdr) |
Uses nohup "$REAL_HERDR" ... server >/dev/null 2>&1 & disown. While SIGHUP is ignored on normal subshell exit, the daemon remains in the calling subshell's Process Group (PGID) because set -m is disabled in non-interactive scripts. Process group signal broadcasts (SIGTERM/SIGINT from test runners, CI fixtures, timeout wrappers, or supervisor group kills) kill the Herdr daemon, terminating all child panes and resetting session state. |
CONFIRMED REAL DEFECT (Process Group Signal Vulnerability) | HIGH (Causes total session/pane loss during process group teardown) |
| 2 (Class A) | Resuming a Discovery-bucket session (agy/hermes/opencode) stopped at 0-turns (before first message) fails with an unrecoverable error. |
resume_session.sh:54-57 |
Created with session_id_source: pending-discovery and own-key null. When stopped at 0-turns, no disk artifact exists, resolve_session_id.sh outputs empty string "", and resume_session.sh executes a hard exit ([ -z "$UUID" ] -> exit 1), blocking automated recovery. |
CONFIRMED REAL DEFECT (Automation / Reconciler Recovery Gap) | MEDIUM (Affects 0-turn recovery for agy, hermes, opencode) |
| 2 (Class B) | Resuming an Assigned-bucket session (claude/grok) stopped at 0-turns. |
verify_session.py:99-101, resume_session.sh |
Created with session_id_source: assigned and session_id_verified: false. When stopped at 0-turns, own-key is preserved. verify_session_uuid in revalidate mode hits the early-return escape hatch at verify_session.py:99-101 and returns True without checking disk artifacts. resume_session.sh succeeds with RC=0 using --session-id <uuid>. |
NOT A DEFECT (Working as designed; verified by passing test_t8) |
N/A (0-turn resume already functions correctly) |
2. Deep-Dive Analysis: Item 1 (Herdr Daemon setsid Omission)
2.1. Codebase Verification
In .agents/skills/lib.sh (lines 198–220), inside the heredoc template that generates .mam/shim/herdr:
# lib.sh:207-219
if [ "$_mam_session_running" != "1" ]; then
# Headless bootstrap avoids herdr's "nested herdr is disabled" guard...
nohup "$REAL_HERDR" --session "$_MAM_SESSION" server >/dev/null 2>&1 &
_mam_server_pid=$!
disown 2>/dev/null || true
for _mam_wait_i in $(seq 1 40); do
[ -S "${HOME:-$HOME_DIR}/.config/herdr/sessions/$_MAM_SESSION/herdr.sock" ] && break
kill -0 "$_mam_server_pid" 2>/dev/null || break
sleep 0.25
done
fi
2.2. OS Process Group & Signal Dynamics
- The Role of
nohupanddisown:nohupensuresSIGHUPis ignored (SIG_IGN).disownunlinks the job from the current bash shell's active job table so the shell does not emitSIGHUPupon exit.- Upon clean subshell termination, the child is reparented to
init/launchd(PID 1) and survives.
- The Vulnerability — Process Group (PGID) Retention:
- In non-interactive bash scripts (where job monitor
set -mis disabled by default), background processes (&) are placed in the same Process Group (PGID) as the caller. nohupanddisowndo not create a new process group or session ID.- Inspection of live production processes confirmed this vulnerability:
The daemon is reparented to PPID 1 but continues to run in its dead spawner's PGID (7526) and remains attached to controlling TTY
PID 7623 PGID 7526 PPID 1 TTY ttys001 /opt/homebrew/bin/herdr --session multi-agent-mux serverttys001. - When test runners (e.g. pytest subshell teardowns), timeout wrappers (
timeout --kill-after ...), CI runners, or parent supervisor scripts terminate a workflow via process group kill (kill -- -$PGIDorkill -TERM -$PGID), the signal (SIGTERMorSIGINT) is broadcast to every process in that PGID. - Because
nohuponly shields againstSIGHUP, the Herdr daemon receivesSIGTERM/SIGINTand terminates immediately.
- In non-interactive bash scripts (where job monitor
- Consequences:
- The Herdr daemon socket (
herdr.sock) is deleted. - All active workspaces, tabs, and agent panes managed under that Herdr session instance are destroyed, resulting in an unrecoverable runtime session reset.
- The Herdr daemon socket (
2.3. Platform Portability Consideration (macOS vs. Linux)
- On standard Linux systems,
setsidis commonly installed viautil-linux. - On macOS (Darwin), the
setsidcommand-line utility is not installed by default (command -v setsidexits with 1). Callingsetsid "$REAL_HERDR" ...in bash causescommand not found: setsid. - Portable Solution: Python is a mandatory runtime dependency across MAM (
lib_py). In Python,subprocess.Popen(..., start_new_session=True)natively callsos.setsid()in the child process beforeexecon all POSIX platforms (macOS, Linux, BSD), providing true session and process group detachment.
3. Deep-Dive Analysis: Item 2 (0-Turn Stopped Session Resume Recovery Gap)
3.1. Breakdown by Spawn Class
Class A: Discovery-Bucket Agents (agy, hermes, opencode) — [CONFIRMED REAL DEFECT]
- Creation:
create_session.sh(lines 189–193) spawns these agents without a UUID (agy --dangerously-skip-permissions,hermes --yolo --accept-hooks,opencode --auto --agent build).- In
.mam/agent-sessions.yaml, the row is stamped withsession_id_source: pending-discoveryand own-keys asnull/empty.
- 0-Turn Stop:
- When stopped before sending any message,
stop_session.sh:185callscapture_conversation_id, which finds no on-disk conversation files and returns"". stop_session.sh:281(if captured and not purge:) is skipped;resumable: trueis not set and own-keys remainnull. The row is markedstatus: stopped.
- When stopped before sending any message,
- Resume Attempt:
resume_session.sh:51callsresolve_session_id.sh, which searches the YAML row and scans disk viafind_workspace_uuid. Both are empty, returning"".resume_session.sh:54-57executes:if [ -z "$UUID" ]; then echo "ERROR: No saved session for $WORKSPACE ($AGENT). Use multi-agent-mux-create first." >&2 exit 1 fi- Hard exit with code 1.
- Impact:
- Reconcilers and automated workflows that hold only
--session,--workspace, and--agentcannot recover a stopped 0-turn Class A session viaresume_session.sh. - Handoff to
create_session.shfails becausecreate_session.shstrictly requires--role([ -n "$ROLE" ] || exit 2).
- Reconcilers and automated workflows that hold only
Class B: Assigned-Bucket Agents (claude, grok) — [NOT A DEFECT, WORKING AS DESIGNED]
- Creation:
create_session.sh:178-180pre-allocates a UUID viamam_gen_uuidand records it intoclaude_session_id_own/grok_session_id_ownwithsession_id_source: assignedandsession_id_verified: false(create_session.sh:412-413/435-436).
- 0-Turn Stop:
- When stopped before sending any message,
stop_session.shdoes not null or erase the create-time assigned own-key (stop_session.sh:281only updates ifcapturedis non-empty, leaving existing own-keys intact).
- When stopped before sending any message,
- Resume Attempt & The
verify_session.pyEscape Hatch:- When
resolve_session_id.shcallsfind_workspace_uuid(lib_py/workspace_uuid.py:65, 72), it evaluates candidate own-keys usingverify_session_uuid(ws, agent, cand, s, mode="revalidate"). - In
lib_py/verify_session.py(lines 99–101):if (mode == "revalidate" and row.get("session_id_source") == "assigned" and not row.get("session_id_verified")): return True - Because the session is stopped at 0 turns,
session_id_sourceis"assigned"andsession_id_verifiedremainsFalse. verify_session_uuidearly-returnsTrueimmediately, bypassing the on-disk transcript check (adapter.verify_artifact()).find_workspace_uuidreturns the candidate UUID.resume_session.shcallsresume-spec(claude.py:97), which detectsmaterialized: Falseand generates:CMD_FULL="${RESOLVED_BIN} --dangerously-skip-permissions --session-id ${SESSION_UUID}"resume_session.shexecutes with RC=0, successfully spawning the agent pane with its pre-assigned UUID!
- When
- Empirical Evidence:
- Covered and protected by existing regression test:
tests/test_uuid_target.py::test_t8_resume_unmaterialized_assigned_id PASSED [100%] - All 12/12 tests in
tests/test_uuid_target.pypass.
- Covered and protected by existing regression test:
4. Architectural Recommendations for Future Planning
- For Item 1 (Headless Herdr Daemon Detachment):
- Replace
nohup "$REAL_HERDR" ... server & disownin.mam/shim/herdr/lib.shwith a portable Python daemon spawner:python3 -c "import subprocess, sys; subprocess.Popen(sys.argv[1:], start_new_session=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL)" "$REAL_HERDR" --session "$_MAM_SESSION" server - This isolates the daemon in its own Session ID and Process Group across macOS and Linux, protecting it from parent process-group signals (
SIGTERM/SIGINT).
- Replace
- For Item 2 (Class A 0-Turn Stopped Session Recovery):
- Target Class A agents (
agy,hermes,opencode) only:- When
UUIDis empty for astatus: stoppedsession inresume_session.sh, instead of hard-failing at line 54, allowresume_session.shto fall back to launching the initial clean spawn command (CMD_FULL) into the pane (or allow--role-less creation handoff).
- When
- Do NOT modify Class B: Preserve the existing
verify_session.py:99-101escape hatch andtest_t8contract unchanged.
- Target Class A agents (