- 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)
13 KiB
📋 Implementation Plan — Issue #3 Fix (Job a9c8d6d3)
- Planner:
planner-reviewer-claude-01 - Based on:
.agents/reports/creator-agy-01/issue-3-analysis.md(Rev. 3, job6b391a80), independently validated PASS by bothplanner-reviewer-claude-01(job9406c304) andreviewer-opencode-01across the peer-review cycle. - Scope: implement both confirmed defects from the Rev. 3 analysis, add regression tests, verify the full suite.
1. Item 1 — Headless herdr daemon process-group detachment
Defect (Rev. 3, CONFIRMED, HIGH): .agents/skills/lib.sh:207-219 (inside the heredoc that generates .mam/shim/herdr) launched the headless daemon via nohup "$REAL_HERDR" ... server >/dev/null 2>&1 & disown. nohup only ignores SIGHUP; disown only drops bash's own job-table tracking. Neither creates a new process group or session, so the daemon stays in the caller's PGID and dies when a process-group signal (SIGTERM/SIGINT from a test runner, timeout wrapper, or supervisor teardown) is broadcast — matching the live evidence from three independent reviewers' process-table inspections (PID 7623/PGID 7526/PPID 1, still TTY-attached, never setsid()'d).
Fix: replaced the nohup ... & disown line with a Python subprocess.Popen(..., start_new_session=True, ...) spawner (invoked via python3 -c '...'), which calls os.setsid() in the child before exec — portable across macOS (no setsid(1) binary by default) and Linux. disown is no longer needed since the process is no longer a bash job at all (spawned by the python3 child, not via &). The kill -0 "$_mam_server_pid" liveness-wait loop is unchanged; only how the PID is obtained changed (captured from the Python spawner's stdout instead of $!).
File: .agents/skills/lib.sh:207-224 (the generated .mam/shim/herdr is a runtime artifact regenerated by _init_herdr_isolation, not git-tracked — no separate edit needed there).
Test: tests/test_herdr_shim_contract.py::test_h26_daemon_spawn_uses_process_group_detachment — (a) static check that the generated shim no longer contains the old nohup ...server pattern and does contain start_new_session=True; (b) behavioral check that extracts the actual shipped spawner statement from the generated shim and runs it standalone against a fake REAL_HERDR (a plain sleep 30), asserting the spawned process's PGID differs from the calling shell's PGID — i.e. genuine detachment, not just a textual change. Verified fail-old/pass-new: reverting lib.sh to the pre-fix version makes this test fail (old nohup pattern still present); restoring the fix makes it pass.
2. Item 2 — Class A (agy/hermes/opencode) 0-turn resume fallback
Defect (Rev. 3, CONFIRMED, MEDIUM — Class A only): a session for agy/hermes/opencode stopped before its first message has session_id_source: pending-discovery and a null own-key. resolve_session_id.sh correctly returns "", and resume_session.sh:54-57 hard-exits (RC=1) telling the caller to use multi-agent-mux-create, which in turn requires --role (create_session.sh:96) — an argument resume callers don't hold. Explicitly not a defect for Class B (claude/grok): their verify_session.py:99-101 assigned+unverified escape hatch already resolves the pre-assigned UUID without requiring an on-disk transcript, proven by the pre-existing test_t8_resume_unmaterialized_assigned_id (still green, untouched).
Fix (.agents/skills/multi-agent-mux-resume/scripts/resume_session.sh):
- When
resolve_session_id.shreturns an empty UUID, branch on agent:agy/hermes/opencodesetFRESH_SPAWN=1and continue instead of exiting;claude/grok/any other agent hit the exact original hard-exit (byte-identical error message and RC=1) — the Class B path is completely unmodified. - When
FRESH_SPAWN=1,CMD_FULLis computed vialib_py.agents spawn-spec(the same callcreate_session.shuses for a first-time launch) instead ofresume-spec, with a matching inline fallback case statement foragy/hermes/opencodeonly. - This bypasses the broken
resume_session.sh → create_session.sh --rolehandoff entirely rather than trying to makecreate_session.shcallable without a role — the recovery happens insideresume_session.shitself, reusing the same command the agent was originally spawned with.
Fix (.agents/skills/multi-agent-mux-resume/scripts/update_yaml_resumed.sh): the final YAML update needs --uuid, which is empty for a fresh-spawn resume. The hard [ -n "$UUID" ] || exit 2 guard was relaxed to allow empty (documented in usage()), and a new optional --cmd-full flag lets resume_session.sh pass through the already-computed spawn command for display. The Python payload:
last_visible_statusbranches:"resumed conversation {uuid} ..."when non-empty,"resumed (fresh spawn, no prior turn) ..."when empty.- Only the
agy/hermes/opencodeblocks got anif uuid: ... else: ...guard (own-key set + uuid-interpolatedcmd_fullwhen present;--cmd-fullpassthrough with a static fallback when absent). Theclaudeandgrokblocks are byte-identical to before — per the brief's explicit instruction, Class B is never touched.
Files: .agents/skills/multi-agent-mux-resume/scripts/resume_session.sh, .agents/skills/multi-agent-mux-resume/scripts/update_yaml_resumed.sh.
Tests:
tests/test_uuid_target.py::test_t14_resume_class_a_fresh_spawn_fallback— creates anagysession, stops it at 0 turns, confirms the YAML row hasstatus: stoppedand no own-key, then runsresume_session.sh --dry-runand asserts RC=0 with a plain spawn command (--dangerously-skip-permissions, no--conversation). Verified fail-old/pass-new: revertingresume_session.shalone reproduces the old hard-fail (ERROR: No saved session for ...) and the test fails; restoring the fix makes it pass.tests/test_uuid_target.py::test_t15_resume_class_b_still_hard_fails_when_truly_unresolvable— a genuinely never-createdclaudesession must still hard-fail with the original error and RC=1, guarding against the fallback ever leaking into Class B.
3. Verification
- Targeted suite (
test_tier1-4,test_uuid_target,test_c1_tui_readiness,test_herdr_shim_contract): 164 passed pre-existing + 3 new = 167, all green, before adding the final full-suite run. - Full suite (
tests/): run in background; result to be confirmed and cited in the final job report. - No changes to
verify_session.py,workspace_uuid.py, or theclaude/grokbranches ofupdate_yaml_resumed.sh— the Class B escape hatch andtest_t8contract are untouched, per the brief's explicit requirement.
4. SemVer
Both changes are backward-compatible bug fixes (no public interface/behavior removed; resume_session.sh's new fallback only activates on a previously-hard-failing input; update_yaml_resumed.sh's relaxed --uuid requirement is additive). This qualifies as a PATCH under SemVer 2.0.0 §6. Version bump and VERSIONS.md/SKILL.md changelog entries are left for a subsequent release-packaging job (out of this job's stated scope, which is analysis-driven code improvement, not release cutting).
Rev. 2 — Refinement in response to creator-agy-01's architectural challenge (job c4b0a075)
Independent verification of the challenge (before accepting it): re-read the live, already-implemented update_yaml_resumed.sh and confirmed the challenge's citation is exact — the existing-row branch (else: at line 150, running through line 163, immediately before the shared target['status'] = 'running' at line 165) genuinely never touches herdr_session_epoch or herdr_session_created_at. Cross-checked the two supporting mechanisms it depends on: lib_py/verify_session.py:89 (epoch = row.get("herdr_session_epoch", 0) if mode == "discover" else 0) confirms the epoch watermark only matters in discover mode, and lib_py/agents/adapters/agy.py:49 (if ctx.epoch and os.path.getmtime(path) < ctx.epoch: return False) confirms the exact mtime >= epoch acceptance rule the challenge describes. reconcile.sh does call verify_session_uuid(..., mode="discover") for agy/hermes/opencode (lines 708/766/876). This is a genuine defect I introduced in the Item 2 fix — a Class A fresh-spawn resume leaves the row's discovery watermark stuck at the original create_session.sh timestamp, so reconcile.sh's next sweep could pin an unrelated, older on-disk conversation (created any time after that original watermark, including before the resume even happened) to the freshly-resumed, still-silent agent.
One precision correction to the challenge's own proposed diff: session_id_source/session_id_verified are not currently set on agy/hermes/opencode rows at all — create_session.sh's per-agent block only assigns those two fields for claude/grok (see create_session.sh:402-437). Setting them on a Class A row in the proposed fix is therefore not "resetting" pre-existing state — it introduces a field that wasn't there before. This is harmless (nothing currently reads session_id_source/session_id_verified on a non-claude/grok row; verify_session.py:99's escape hatch is gated on session_id_source == "assigned", which a Class A row will never have), but the refined fix below keeps it anyway for forward-consistency with the fields create_session.sh already sets for Class B, rather than dropping it — it costs nothing and makes a future reader's mental model ("every row always carries these two fields") uniformly true.
Refined fix
In .agents/skills/multi-agent-mux-resume/scripts/update_yaml_resumed.sh, inside the shared post-branch code (i.e. after line 165's target['status'] = 'running', applying uniformly regardless of which branch — new-row or existing-row — populated target, and reusing the epoch/now locals already computed at lines 117-118), add:
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
Gated strictly on if not uuid: (the same condition already used at line 175 for the last_visible_status branch), so it fires only on the new Class A fresh-spawn path from this same job's Item 2 fix — it can never touch a normal resume (uuid non-empty, any agent) or any Class B path, since claude/grok never reach update_yaml_resumed.sh with an empty --uuid (per Item 2's resume_session.sh branch, only agy/hermes/opencode set FRESH_SPAWN=1).
New test spec
tests/test_uuid_target.py — extend test_t14_resume_class_a_fresh_spawn_fallback's fixture (or add a sibling test_t16_resume_class_a_fresh_spawn_resets_discovery_epoch): after creating+stopping the agy session at 0 turns, seed a conversation artifact under the workspace with an mtime between the original creation time and "now" (simulating the challenge's T_2 stale transcript), run resume_session.sh (not --dry-run, since the epoch write only happens in update_yaml_resumed.sh's real YAML mutation), then assert in agent-sessions.yaml that herdr_session_epoch was advanced to at or after the resume's own timestamp (not the stale creation-time value) — i.e. the pre-existing stale artifact's mtime is now < the row's watermark and would be correctly rejected by a subsequent reconcile.sh discover-mode sweep. Should be verified fail-old/pass-new like the other two Item 2 tests: written against the pre-refinement code, herdr_session_epoch stays at the original creation epoch and the assertion fails; after applying the refined fix, it advances and the test passes.
Updated action list for re-implementation
- Apply the
if not uuid:epoch/watermark-reset block above toupdate_yaml_resumed.sh, placed after the sharedtarget['status'] = 'running'line so it applies regardless of which branch (new-row vs. existing-row) populatedtarget. - Add the new regression test per the spec above, and verify fail-old/pass-new for it specifically (not just that it passes).
- Re-run the full test suite and confirm no regression against the 454-passed baseline from the original
a9c8d6d3implementation. - No changes needed to Item 1,
resume_session.sh's branch logic,verify_session.py,workspace_uuid.py, or any Class B (claude/grok) code path — this refinement is fully contained withinupdate_yaml_resumed.sh's shared post-branch section.