Files
multi-agent-mux/.agents/reports/reviewer-grok-01/report-f0df1be0.md
T
GodopuandClaude Opus 5.5 6fd417a7b7 release(v4.9.5): a claude purge removes every path the session owns (Issue #31, PR #59) - archive the review reports, bump the lockstep version, load logroot eagerly
Review loop (--review-only, --all-reviewer, focused on this Mac:
macOS, /usr/bin/python3 3.9.6, bash 3.2): round 1 (@6f4f40a) agy and
grok PASS, claude and codex NOT PASS (a non-uuid id `memory` purged the
auto-memory dir, a linked dir below a root was followed, a NaN
startedAt counted as dead), sent as a PR comment and fixed by the
author in eleven commits; round 2 (@2a8d738) confirmed all three fixed
but 4/4 NOT PASS on one test holder (`sh -c "sleep 120" <uuid>` loses
the uuid on macOS /bin/sh), fixed in de6f3fc; round 3 (@de6f3fc) 4/4
PASS.

Full suite on macOS / Python 3.9.6 @de6f3fc: 1934 passed, 56 skipped,
1 error (the known #55 false positive: agy conversations of another
workspace). The real ~/.claude, /private/tmp/claude-<uid> and herdr
panes unchanged.

Also names lib_py.delegation.logroot in eager.CORE_MODULES (the v4.9.4
review's non-blocking note).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
2026-10-18 01:25:15 +09:00

8.8 KiB
Raw Blame History

Review f0df1be0 — PR #59, Issue #31 claude purge

Reviewed only the snapshot /var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261016T003524Z-a51548/1 (loop-20261016T003524Z-a51548). Head 6f4f40a4adb2115d5997dffd31fb6473f05cab68, tree f4d9422ad0c1d616b5f37065f3e96b34c28f78d8, base 26e2c43753169671b600d03274901dbe203988dd is an ancestor. Thirteen commits, 12 files, +1127/−27. git diff --check is clean. Snapshot porcelain is empty and the snapshot has no .mam. Control-plane main is still 26e2c43753169671b600d03274901dbe203988dd, porcelain empty. No product file was edited.

Host tools: /usr/bin/python3 3.9.6, the control-plane venv the same 3.9.6, /bin/bash 3.2.57. bash -n accepts stop_session.sh and lib.sh. With set -u, the new CLAUDE_PROJECT_DIR default (lib.sh:78) expands to $HOME/.claude/projects when both variables are unset, and to $CLAUDE_CONFIG_DIR/projects when only the config dir is set.

Every purge, in-use, and stop --purge-conversation exercised here used a temporary HOME, CLAUDE_CONFIG_DIR, CLAUDE_PROJECT_DIR, CLAUDE_CODE_TMPDIR, and TMPDIR. The real ~/.claude and /private/tmp/claude-<uid> were listed with ls-level reads.

What this Mac already stores

~/.claude has real directories file-history, session-env, sessions, jobs, and projects. file-history holds 18 uuid-named directories and no symlinks. session-env holds 21 uuid-named directories and no symlinks. sessions holds 5 *.json files and 5 key files; every key name matches <pid>.<64 lowercase hex>.key. Each json has integer pid, sessionId, and a millisecond startedAt (13 digits). The filename stem equals pid in all 5. All 5 pids were alive at the read. jobs holds 2 real directories, both 8 characters long, and each state.json sessionId starts with that directory name. No job entry is a symlink. projects has 16 keys, none of them symlinks; across those keys the entries include 25 <uuid>.jsonl files and 7 <uuid> directories, plus shared names such as the memory directory the purge leaves alone.

/tmp is a symlink to /private/tmp. /tmp/claude-<uid> and /private/tmp/claude-<uid> both exist and share one real path. Under that root, project keys are real directories. Their children are uuid directories (16) plus 3 other directories whose names are 57 or 58 alphanumeric characters, contain no uuid, and are siblings of the uuid scratchpads. $TMPDIR/claude-<uid> does not exist. CLAUDE_CODE_TMPDIR was unset in this review process. That is the layout _tmp_roots (claude.py:25-48) and _owned_paths (claude.py:256-285) assume: an existing per-uid root, then <key>/<uuid>.

lstart on this host

ps -p <pid> -o lstart= with LC_ALL=C prints five fields. time.strptime of the collapsed line with %a %b %d %H:%M:%S %Y accepts both Oct 5 and the BSD space-padded Oct 5. For the five live session files, lstart - startedAt/1000 is between −2.43 s and −0.694 s, so lstart <= startedAt/1000 + 2 (claude.py:77) holds for every one of them. Zero of those live files would be treated as a reused pid.

A temp-home probe of _session_file_live: the current pid with startedAt set to now is live; the same pid with startedAt 1.5 s after its own lstart is live; missing startedAt, startedAt 0, a non-numeric startedAt, pid 0, a negative pid, and boolean True are live. A dead pid is stale. The same live pid with startedAt an hour earlier, or 3 s earlier than its lstart, is stale. The process locale reported en_US / UTF-8, and the English month from LC_ALL=C parsed. A parse failure returns live (claude.py:71-72), which refuses the purge.

Deletion stays on this session

On a temporary config root the purge removed the workspace <uuid>.jsonl, the sibling <uuid>/ directory, file-history/<uuid>/, session-env/<uuid>/, jobs/<uuid[:8]>/ whose state.json named the session, the dead sessions/<pid>.json, its <pid>.<64 hex>.key, and <CLAUDE_CODE_TMPDIR>/claude-<uid>/<key>/<uuid>/. The printed key label contains <pid>.*.key and the hex is absent from the purged list.

These stayed: another session's sessions/<pid>.json and key for a live pid, that session's scratchpad, history.jsonl whose body names the purged uuid, projects/<key>/memory/MEMORY.md, and a job directory whose 8-character name matches the uuid but whose state.json names a different session (claude.py:244-254). _plausible_id (base.py:89-92) rejects "", ., .., *, ../x, a/b, and .... (".." is a substring). Purging those ids left the tree unchanged.

Symlinks. A file-history/<uuid> symlink to an outside directory was unlinked; the outside secret.txt remained. A projects/<key>/<uuid> symlink to an outside directory was unlinked; the outside file remained. A directory symlink and a file symlink inside a real owned <uuid>/ directory were removed with the directory; both targets remained (_remove, claude.py:80-87, uses os.path.islink and then os.remove; shutil.rmtree sees the real directory). A jobs/<8> symlink whose target state.json names the session was unlinked; the target directory and its other.txt remained (_remove_job, claude.py:107-109). A sessions/<pid>.json symlink to an outside json was unlinked after the read saw this session id; the outside file remained.

A project key under the temp root that is itself a symlink is followed: <symlink>/<uuid> was removed on the far side. That far-side directory is still the one named this session's uuid, which is the scratchpad path when a key has been relocated. The real per-uid temp root on this Mac has no key symlinks. The cross-session paths above stayed in the same probe.

A live session file is omitted from _owned_paths (claude.py:274-278). Calling purge_artifacts directly while that pid was alive left sessions/<pid>.json in place and removed file-history/<uuid>/. After the process exited, the same call removed the session file. The purge CLI does not take that direct path: in-use was true, the process exited 4, and the file-history marker was still there (__main__.py:146-148). in-use with no holder exits 0. in-use without a uuid exits 1.

Stop

stop_session.sh:285-300 runs python -m lib_py.agents in-use <agent> <uuid> <cwd> --wait 5 before the registry write. Any status other than 0 exits 4 and leaves the row. The registry python then prints purged: lines and either WARN residual: or residual: none (agent: <agent>) (stop_session.sh:355-368). lib.sh exports PYTHONPATH to the skill dir at source time, so the new in-use module resolves the same way exit-key already does. DELEGATE_JOB_ID, DELEGATE_JOB_TOKEN, and DELEGATE_JOB_ATTEMPT are set before that call (stop_session.sh:146-161), including to empty strings, so set -u does not trip. The passthrough raise SystemExit(1) is now stop_session.sh:327, and tests/data/skill_code_blocks.json records that line. The stop SKILL.md diagram and the purge paragraph match the fenced blocks in that json (the skill-block test passed).

Tests

Pytest used the control-plane venv, PYTHONDONTWRITEBYTECODE=1, -p no:cacheprovider, and --basetemp /tmp/mamf0-f0df1be0-work/pybase. Inherited HERDR_*, MAM_*, MQTT_*, SKS_*, WORKSPACE_ROOT, DELEGATE_JOB_LOGS_DIR, HOME_DIR, CLAUDE_PROJECT_DIR, CLAUDE_CONFIG_DIR, CLAUDE_CODE_TMPDIR, CODEX_HOME, and PYTHONPATH were dropped. The suite's own fixtures then point HOME at the test tmp.

On the snapshot, 80 tests passed and 1 skipped in 28.31 s: all of tests/test_issue31_purge_residual.py (32 tests), tests/test_issue42_skill_blocks.py, and tests/test_issue55_agent_homes.py. The skip is test_issue42_skill_blocks.py:266, because .claude/skills is absent in this worktree. The issue-31 file covers the owned-path purge, the untouched neighbor files, the same-prefix job, malformed ids, the live-process refusal, the reused pid, a key that cannot be unlinked, a job directory that cannot be emptied, CLAUDE_CONFIG_DIR, the temp-root rules, the stop exit 4, the stray-key WARN residual:, and codex, grok, and opencode residuals.

The same issue-31 file copied onto an archive of base 26e2c43 failed 24 tests and passed 8 in 17.09 s. The failures are behavioral. The old purge left file-history/<uuid> and session-env/<uuid> behind. The old purge CLI, asked about a live pid whose argv does not contain the uuid, removed the jsonl and returned in_use false. The old stop did not exit 4 for that live process and did not print WARN residual: for a transcript under another project key. Two failures are the new helpers _session_file_live and _tmp_roots being absent; the assertions above fail on the old behavior itself.

After the report, snapshot porcelain is still empty, the snapshot still has no .mam, and control-plane porcelain is still empty at 26e2c43. The herdr config mtime is still 1784456394.

[VERDICT: PASS]