Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
11 KiB
Review: PR #73 / Issue #71 (macOS, snapshot 2)
Job fd90992c. Reviewer only. Checks ran on the review snapshot. The source worktree and the control-plane checkout were not edited.
Identity
- Snapshot:
/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261026T133440Z-c8961b/2 - Loop:
loop-20261026T133440Z-c8961b - Base:
29cc4fd90478936f15387a7665e2c95d13a3f14c(main, v4.10.1) - Head:
5f6065344f800c2c5ad6f802e95e9e8ff5cc7b26 - Tree:
98d57bdc60920238c115ece90c90cc694f2744bc - 14 commits, base is an ancestor, porcelain 0, no
.mamin the snapshot,git diff --checkclean - Diff: 28 files, +953/−176
- Host:
/usr/bin/python33.9.6, bash 3.2.57(1)-release, sqlite 3.51.0,LANG=en_US.UTF-8
tests/test_issue66_stall_watch_sanitized_name.py has an empty diff against 29cc4fd. Commit 0fc162e is an ancestor. The diff file list has no multi-agent-mux-update skill. MAM_VERSION stays 4.10.1.
Issue #71, the "범위 정리" comment (items 1–10), and PR #73 were read. The registry entry point is documented for #72 and is not wired to an update skill.
What was checked
attach_command
lib_py/agents/sanitize.py:34 builds herdr --session <server> agent attach <sanitized name>. An empty server becomes default, and both the server and the name go through shlex.quote, so default is written as --session default and a server that needs quotes is quoted.
The three writers call it:
- create,
create_session.sh:566 - resume,
update_yaml_resumed.sh:151(new row) and:161(existing row whose server is set) - reconcile,
reconcile.sh:676, on the row whose note isauto-registered by monitor
The shim agent attach (lib.sh:605-613) resolves the target with _resolve_herdr_target and runs _real_herdr agent attach "$tgt" "$@". Options after the name stay in "$@".
Removed fields and the stop hints
kill_command and start_command remain only as fields the migration pops and as assertions that they are absent. status.sh no longer emits start_command. The shim still has the fake new-session and kill-session verbs, which create, stop, and reconcile use. kill-session sanitizes the name and exits 1 when that fails.
Create exit 43 (create_session.sh:364) and id_order.py:330 and :344 tell the user to run stop_session.sh --session <name> --herdr-session <server>. The row-less tests below run that script.
Row-less stop
stop_rowless (stop_session.sh:123) runs only when the row lookup exits 7 (:189). A strict read failure exits 1 before that (:170). --purge-conversation with no row exits 1 (:124). A cwd outside this workspace exits 5. A session that survives kill-session exits 6. Those codes match multi-agent-mux-stop/SKILL.md:58-66.
Server order: --herdr-session, else a non-default HERDR_SESSION_NAME captured before resolve (:109, :128), else the workspace server. lib.sh stamps an unset name as default, and the row-less path treats that stamp as unset.
The cwd check realpaths both sides and accepts the workspace itself or a child (root + os.sep). That rejects a sibling prefix and a symlink whose target is outside the workspace, and it treats a trailing slash as the same directory. /tmp and /private/tmp are the same path on this Mac. The row-less tests compare registry bytes before and after and passed.
load_state_json strict (lib.sh:1594) is the bash reader. A missing state or sessions table raises in strict mode and the function exits 1. The argument-free call still prints {}.
One name rule
lib.sh has one _sanitize_herdr_agent_name (:44). 00000000 and lower()[:32] are absent. The shim receives the function through declare -f (:181).
The function was extracted with sed and run under /bin/bash 3.2 without sourcing lib.sh. Against sanitize_herdr_agent_name there were 0 mismatches under LC_ALL=C and LC_ALL=en_US.UTF-8 for empty, Upper-Case, -n, -e, a 33-character name, the long branch name, İstanbul-creator-claude, a newline, a\nb, café, the kelvin sign, Japanese, Korean 한글, and A B. Korean is two characters; the non-ASCII path uses python3 str.lower() and the same regex, so the replacement follows characters, and the result matched Python.
With PATH limited to tr, sed, awk, and cut, the long name exits 1 with no shasum, sha1sum, openssl or python3 to hash it and prints no name. 한글 exits 1 with not ASCII.
/usr/bin/python3 is 3.9.6. The diff adds no mapfile, ${var,,}, declare -A, match, or tomllib. A throwaway SQLite database opened with PRAGMA journal_mode=WAL, written, and closed still had a -wal (0 bytes) and a -shm (32768 bytes).
Registry format 2
atomic_yaml.py:118-137 migrates under the lock, after BEGIN IMMEDIATE and before the caller mutation, when stored_version < 2. Migration sets force_yaml, so YAML is rewritten even if the mutation changes no session. If the DB is already current and the YAML copy is still older, the next write rewrites the YAML. migrate (registry_format.py:50-66) updates every named row, including stopped and terminated, and sets registry_format to 2. A version already greater than 2 is not less than CURRENT, so the writer does not call migrate.
The entry point python -m lib_py.registry_format --yaml <path> prints registry-format: <from> -> <to>, rows changed: <n> and exits 0, or registry-format: error: and a nonzero status when neither file exists. The only yaml.safe_dump under .agents/skills is the writer. Readers (lib_py/state.py and the argument-free bash loader) do not migrate.
stall_watch and the edited tests
stall_watch.py:229 computes self.herdr_name once and passes it into HerdrSource. The issue #66 test file is unchanged and is in the passing run.
Edited assertions keep the old intent: pane-lock uses session_id_source (create sets it at create_session.sh:579) to tell a create row from a monitor row; the worktree test looks for a leaked WORKSPACE_ROOT= in attach_command; tier 2 checks the new attach string and the absence of the two dropped keys; tier 1 and tier 3 plant a row with no agent so "cannot infer agent" still exits 2. skill_code_blocks.json moved with the docs, and test_issue42_skill_blocks.py expects stop exits 0 through 6. That file passed aside from the skills-link skip.
S11 (test_issue23_24_startup_path.py:306-310) compares the shim with 0fc162e. That commit is in this checkout, so the comparison ran as part of the passing file. If the object is missing, the test calls pytest.skip with a message that names the commit. That skip is intentional: the PR says a squash would drop 0fc162e, and it asks for a merge commit.
The reconcile tests plant a mock pane (test_issue71_herdr_names.py:127-140) and require one new row. They passed, so registration ran.
Tests
Pytest was PYTHONDONTWRITEBYTECODE=1 with the control-plane venv, -p no:cacheprovider, and --basetemp=/tmp/mamfd9-fd90992c-pytest. Inherited HERDR_*, MAM_*, MQTT_*, WORKSPACE_ROOT, DELEGATE_JOB_LOGS_DIR, PYTHONPATH, the four XDG_* homes, and AGENT_SESSIONS_YAML were unset. AGENT_PYTHON_BIN pointed at the control-plane venv. The snapshot was the working directory and was not checked out.
Head, the files named in the brief plus test_b19_headless_reconcile_fixes.py, test_b4_session_created.py, and test_issue30_stall_watch.py: 546 passed, 2 skipped in 1458.68s, exit 0. tests/test_issue71_herdr_names.py collects 70 tests and they are in that pass. The two skips, confirmed with -rs on those two tests, are test_claude_skills_link_resolves_to_agents_skills (.claude/skills absent, test_issue42_skill_blocks.py:266) and test_s6_the_heartbeat_timer_is_not_a_background_child (Linux /proc, test_issue30_stall_watch.py:340).
Base: a --shared clone at /tmp/mamfd9-fd90992c-base, detached at 29cc4fd, with only this test file copied in. Same pytest flags, --basetemp=/tmp/mamfd9-fd90992c-base-pytest. 68 failed, 2 passed in 98.42s, exit 1. The two passes are test_the_names_need_converting_and_the_plain_one_does_not and the plain-name test_wrapper_attach_calls_herdr_with_the_herdr_name. The long and uppercase wrapper cases failed.
Residuals (non-blocking)
- Readers stay read-only. An old registry keeps a format-1
attach_commanduntil the next writer, or untilpython -m lib_py.registry_formatruns. The module docstring describes that window. The explicit entry point is left for #72. VERSIONS.mdis unchanged. The release sentence from the scope comment is in the PR body.migrateskips a row with noname(registry_format.py:57-58), so that row can keepkill_commandafter the format key becomes 2.lib_py/state.pyload_state_jsonstill returns{}on a read error. Stop uses the bash strict reader.- The SKILL.md exit-0 cell describes a stop or an already-stopped no-op. The pre-flight paragraph and the script header also describe the row-less close as exit 0. The pre-flight sentence names
--herdr-sessionandHERDR_SESSION_NAME; the code also uses the workspace server when both are absent. - A direct call to
migrateon a format already greater than 2 would write the key back to 2. The writer does not call it in that case. - S11 skips only when
0fc162ecannot be resolved. A squash merge would take that path. The PR forbids squash for that reason.
Host
Checked before the suite at 2026-10-27 01:43:34 and again after it at 02:08:54. No process whose first command token was opencode. lsof of the user opencode database returned exit 1. Database mtime 1792641027, size 36712448. opencode.log absent. Config service.json mtime 1792246838, size 64. State service.json absent. Herdr config mtime 1784456394, size 19. ~/.local/state/mam directory mtime 1792846843, size 128. id-order is a directory, mtime 1792846886, size 128; its entries were not listed. The same two herdr servers were present before and after: pid 11655 (/opt/homebrew/bin/herdr server) and pid 69309 (/opt/homebrew/bin/herdr --session multi-agent-mux server). No server argv contained the scratch prefix, the snapshot path, or the loop id. The database was not opened.
After the suite the snapshot was still head 5f60653, tree 98d57bdc, porcelain 0, no .mam. The control-plane checkout was still 29cc4fd, porcelain 0.
Rechecked after this report was written, at 2026-10-27 02:09:37. The snapshot was still head 5f60653, tree 98d57bdc, porcelain 0, with no .mam. The control plane was still 29cc4fd, porcelain 0. Opencode first-token count 0, database lsof exit 1, and the database, log, service files, herdr config, ~/.local/state/mam, and id-order mtimes and sizes were the same as the pre-suite reading. Herdr servers were still pid 11655 and pid 69309. No server argv contained the scratch prefix, the snapshot path, or the loop id.
[VERDICT: PASS]