fix(agents): 같은 폴더의 같은 종류 에이전트가 서로의 대화 ID 를 쓰지 않는다 (Issue #63) #69

Merged
Godopu merged 14 commits from fix/issue-63 into main 2026-10-23 06:46:13 +00:00
Owner

Refs #63

변경

범위는 #63 의 작업 범위 댓글 그대로다.

  • ID 판정 (lib_py/agents/session_ids.py, 새 파일): stop·purge·resume 이 같은 판정을 쓴다.
    • 행에 기록된 own id 가 우선이다.
    • ID 가 나중에 생기는 에이전트(agy·codex·hermes·opencode)는 find_workspace_uuid 의 추측 대신 #40 의 판정(autopin.capture)을 쓴다. claude·grok 은 그대로다.
    • 다른 행이 기록한 ID 는 쓰지도, 지우지도 않는다.
  • stop: 프로세스를 끄기 전에 ID 를 정한다. herdr 가 이미 죽은 행은 확인할 수 없으므로 ID 를 기록하지 않고 이유를 행에 남긴다.
  • purge: 행의 own id 이거나 확인을 통과한 ID 만 지운다. 레지스트리를 쓰는 시점에 다른 행이 기록했는지 다시 확인한다.
  • resume:
    • own id 가 없고 같은 폴더에 같은 종류 행이 둘 이상이면 추측하지 않는다. 새 대화로 띄우고 그 사실을 알린다.
    • 해결한 ID 를 다른 행이 이미 기록했으면 띄우기 전에 거부한다(exit 1).
    • "이미 실행 중" 분기도 같은 판정을 쓴다.
    • 기록된 ID 가 더 이상 유효하지 않으면 새 대화로 띄우고 그 ID 를 지운다(update_yaml_resumed.sh --drop-stale-id).
  • 생성 순서 규칙 (lib_py/agents/id_order.py, 새 파일):
    • 같은 폴더에서 ID 가 나중에 생기는 같은 종류 에이전트가 생성 중이거나, ID 가 기록되지 않은 채 실행 중이면, 새 create·resume 은 그 ID 가 기록될 때까지 기다린다.
    • 폴더·에이전트 단위 잠금과 "생성 중" 표시를 쓴다. pane 잠금은 herdr 서버 단위라서 서버가 다르면 지나치기 때문이다.
    • 상한은 MAM_ID_ORDER_WAIT_S(기본 600 s, 0 이면 기다리지 않음)다. 넘으면 아무것도 띄우지 않고, 기다린 세션 이름을 알리며 거부한다.
    • resume 이 찾았지만 아직 행에 없는 ID 로 띄울 때도 "생성 중" 표시를 남긴다.
    • 폴더는 실제 경로(realpath)로 비교한다.
  • find_workspace_uuid() 가 ID 를 반환하고, 다른 행의 ID 를 후보 단계에서 뺀다(exclude_ids). CLI 출력은 그대로다.
  • 정리: 쓰이지 않게 된 capture_conversation_id(lib.sh)와 CAPTURE_ID(stop) 를 지웠다. 스킬 블록 목록(tests/data/skill_code_blocks.json)의 stop 줄 번호를 갱신했다. 틀리게 된 SKILL.md 줄만 고쳤다.
  • 버전 번호와 VERSIONS.md 는 바꾸지 않았다.

검증

  • 리뷰: --plan --all-reviewer 루프를 3회차까지 돌렸다.
    • 1·2회차의 NOT PASS 지적은 각각 다음 커밋에서 고쳤다.
      • --session 없는 조회의 퇴행.
      • 이름 변환이 같아지는 서로 다른 폴더(project_a·project-a).
      • 생성 중 표시가 너무 일찍 풀림.
      • resume 이 찾은, 아직 기록되지 않은 ID.
    • 마지막 기능 커밋(d115bf7)에 리뷰어 4명이 모두 PASS 를 냈다. 한 리뷰어는 사용량 한도로 3회차를 놓쳐 리뷰만 따로 다시 돌렸다.
  • 회귀: tests/test_issue63_same_cwd_ids.py 23개. 완료 조건 1~7 의 회귀 테스트는 base(dc886ec) 에서 실패하고 head 에서 통과한다.
  • 전체 스위트(Linux, Python 3.14): d115bf7 에서 2130 passed, 20 skipped. 정리 커밋(a907cc5)에서 2129 passed, 20 skipped, 1 failed.
    • 실패는 test_issue34_job_tree.py::test_t5b_a_…[False] 하나다. 하위 잡 표에 닫힌 이유(deadline_timeout)가 아직 기록되지 않은 채 읽혔다.
    • 따로 5번 돌리면 매번 통과했다(10/10). 이 브랜치는 루프·잡 코드를 건드리지 않으므로 타이밍 문제로 본다.
  • 실제 agy 두 개 시험(Linux, 별도 복사본과 별도 herdr 서버):
    • 같은 폴더 동시 create: 뒤 세션이 대기 메시지를 내고 기다렸다. 앞 세션의 ID 가 기록된 그 초에 떴고, 두 ID 는 서로 달랐다.
    • stop: 두 행의 ID 가 바뀌지 않았다.
    • 동시 resume: 둘 다 자기 ID 의 대화로 떴다(lock 을 쥔 프로세스로 확인).
    • B purge: A 의 대화 파일 4종(DB·brain·lock·annotations)이 남았다.
    • 시험 세션·서버·복사본은 지웠다.

비차단 / 남은 것

  • presence lock 으로 ID 를 찾는 것(#63 첫 댓글의 5)은 하지 않았다. MAM 밖에서 사람이 같은 폴더에 띄운 에이전트까지 다뤄야 할 때 다시 본다.
  • --no-onboard 로 만든 세션은 첫 대화 전까지 ID 가 없어, 같은 폴더의 다음 세션이 상한까지 기다린다. 이 옵션은 #68 에서 없앤다.
  • claude 를 첫 대화 전에 purge 하면, 다른 행이 기록하지 않은 transcript 를 고를 수 있다(base 와 같음).
  • macOS(bash 3.2, Python 3.9)에서는 아직 돌리지 않았다. 문법 검사(Python 3.9 문법, bash -n)만 했다.

🤖 Generated with Claude Code

Refs #63 ## 변경 범위는 #63 의 작업 범위 댓글 그대로다. - **ID 판정 (`lib_py/agents/session_ids.py`, 새 파일)**: stop·purge·resume 이 같은 판정을 쓴다. - 행에 기록된 own id 가 우선이다. - ID 가 나중에 생기는 에이전트(agy·codex·hermes·opencode)는 `find_workspace_uuid` 의 추측 대신 #40 의 판정(`autopin.capture`)을 쓴다. claude·grok 은 그대로다. - 다른 행이 기록한 ID 는 쓰지도, 지우지도 않는다. - **stop**: 프로세스를 끄기 전에 ID 를 정한다. herdr 가 이미 죽은 행은 확인할 수 없으므로 ID 를 기록하지 않고 이유를 행에 남긴다. - **purge**: 행의 own id 이거나 확인을 통과한 ID 만 지운다. 레지스트리를 쓰는 시점에 다른 행이 기록했는지 다시 확인한다. - **resume**: - own id 가 없고 같은 폴더에 같은 종류 행이 둘 이상이면 추측하지 않는다. 새 대화로 띄우고 그 사실을 알린다. - 해결한 ID 를 다른 행이 이미 기록했으면 **띄우기 전에** 거부한다(exit 1). - "이미 실행 중" 분기도 같은 판정을 쓴다. - 기록된 ID 가 더 이상 유효하지 않으면 새 대화로 띄우고 그 ID 를 지운다(`update_yaml_resumed.sh --drop-stale-id`). - **생성 순서 규칙 (`lib_py/agents/id_order.py`, 새 파일)**: - 같은 폴더에서 ID 가 나중에 생기는 같은 종류 에이전트가 생성 중이거나, ID 가 기록되지 않은 채 실행 중이면, 새 create·resume 은 그 ID 가 기록될 때까지 기다린다. - 폴더·에이전트 단위 잠금과 "생성 중" 표시를 쓴다. pane 잠금은 herdr 서버 단위라서 서버가 다르면 지나치기 때문이다. - 상한은 `MAM_ID_ORDER_WAIT_S`(기본 600 s, 0 이면 기다리지 않음)다. 넘으면 아무것도 띄우지 않고, 기다린 세션 이름을 알리며 거부한다. - resume 이 찾았지만 아직 행에 없는 ID 로 띄울 때도 "생성 중" 표시를 남긴다. - 폴더는 실제 경로(realpath)로 비교한다. - `find_workspace_uuid()` 가 ID 를 반환하고, 다른 행의 ID 를 후보 단계에서 뺀다(`exclude_ids`). CLI 출력은 그대로다. - 정리: 쓰이지 않게 된 `capture_conversation_id`(lib.sh)와 `CAPTURE_ID`(stop) 를 지웠다. 스킬 블록 목록(`tests/data/skill_code_blocks.json`)의 stop 줄 번호를 갱신했다. 틀리게 된 SKILL.md 줄만 고쳤다. - 버전 번호와 VERSIONS.md 는 바꾸지 않았다. ## 검증 - 리뷰: `--plan --all-reviewer` 루프를 3회차까지 돌렸다. - 1·2회차의 NOT PASS 지적은 각각 다음 커밋에서 고쳤다. - `--session` 없는 조회의 퇴행. - 이름 변환이 같아지는 서로 다른 폴더(`project_a`·`project-a`). - 생성 중 표시가 너무 일찍 풀림. - resume 이 찾은, 아직 기록되지 않은 ID. - 마지막 기능 커밋(d115bf7)에 리뷰어 4명이 모두 PASS 를 냈다. 한 리뷰어는 사용량 한도로 3회차를 놓쳐 리뷰만 따로 다시 돌렸다. - 회귀: `tests/test_issue63_same_cwd_ids.py` 23개. 완료 조건 1~7 의 회귀 테스트는 base(dc886ec) 에서 실패하고 head 에서 통과한다. - 전체 스위트(Linux, Python 3.14): d115bf7 에서 2130 passed, 20 skipped. 정리 커밋(a907cc5)에서 2129 passed, 20 skipped, 1 failed. - 실패는 `test_issue34_job_tree.py::test_t5b_a_…[False]` 하나다. 하위 잡 표에 닫힌 이유(`deadline_timeout`)가 아직 기록되지 않은 채 읽혔다. - 따로 5번 돌리면 매번 통과했다(10/10). 이 브랜치는 루프·잡 코드를 건드리지 않으므로 타이밍 문제로 본다. - 실제 agy 두 개 시험(Linux, 별도 복사본과 별도 herdr 서버): - 같은 폴더 동시 create: 뒤 세션이 대기 메시지를 내고 기다렸다. 앞 세션의 ID 가 기록된 그 초에 떴고, 두 ID 는 서로 달랐다. - stop: 두 행의 ID 가 바뀌지 않았다. - 동시 resume: 둘 다 자기 ID 의 대화로 떴다(lock 을 쥔 프로세스로 확인). - B purge: A 의 대화 파일 4종(DB·brain·lock·annotations)이 남았다. - 시험 세션·서버·복사본은 지웠다. ## 비차단 / 남은 것 - presence lock 으로 ID 를 찾는 것(#63 첫 댓글의 5)은 하지 않았다. MAM 밖에서 사람이 같은 폴더에 띄운 에이전트까지 다뤄야 할 때 다시 본다. - `--no-onboard` 로 만든 세션은 첫 대화 전까지 ID 가 없어, 같은 폴더의 다음 세션이 상한까지 기다린다. 이 옵션은 #68 에서 없앤다. - claude 를 첫 대화 전에 purge 하면, 다른 행이 기록하지 않은 transcript 를 고를 수 있다(base 와 같음). - macOS(bash 3.2, Python 3.9)에서는 아직 돌리지 않았다. 문법 검사(Python 3.9 문법, `bash -n`)만 했다. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Godopu added 9 commits 2026-10-22 00:53:48 +00:00
Godopu added 5 commits 2026-10-23 06:46:09 +00:00
A create/resume killed after spawning its pane but before registering its
row left a marker whose owner pid was dead; the next same-cwd waiter pruned
it on the pid alone and could start a second session that discovers the
same workspace conversation id (PR #69 review P1).

- Markers record the herdr server. A dead owner's marker is pruned only once
  its row is registered for the attempt (running, newer epoch: the row
  decides) or `_herdr has-session` reports the session gone; an unanswered
  probe counts as alive. On timeout the kill-session command is printed.
- A live marker of another process for the waiter's own session name is kept
  and blocks instead of being deleted; the waiter's own pid's markers go.
- Tests: orphan / registered / gone / same-session / missing pane pid cases,
  and a create-level reproduction against mock herdr (fails before the fix).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PR #69 review round 2.

- The herdr probe is tri-state (present / absent / unknown) from a parsed
  `_herdr agent list`: absent only when the list parses without the name or
  herdr answers server_not_running. A failed, unparseable or timed-out lookup
  is unknown and keeps the marker (codex P1: has-session's exit 1 meant both
  "absent" and "lookup failed").
- Probes run with MAM_NO_AUTOSTART=1 so a check never starts a server (agy P2).
- Probe time is capped by max(deadline, start + PROBE_FLOOR_S=2 s), shared by
  all markers, so a slow herdr cannot hold the wait or its lock past the cap
  (codex P2); MAM_ID_ORDER_WAIT_S=0 still gets one bounded look.
- Tests: unknown probe keeps and refuses; delayed probe at caps 0/1/3 stays
  within the bound; real probe via lib.sh mam_id_order_wait against mock herdr
  (listed / unlisted / failing list / server_not_running).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PR #69 review round 3 (rebuttal overruled; codex r2 applied as written).

- A failed `agent list`, herdr's server_not_running included, is unknown and
  keeps an orphaned marker; only a successful list without the name prunes.
- No probe floor: probes get only the time left until the wait deadline, so
  MAM_ID_ORDER_WAIT_S=0 asks herdr nothing (unknown, kept) and the refusal
  says so.
- The timeout message reports the last definite answer of this wait, so an
  alive orphan whose final probe ran out of time still reads as alive.
- Tests follow: server_not_running and cap 0 keep the marker and refuse;
  delayed-probe bound is the cap itself; probing tests use a small cap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A marker left on the invoking session name by a killed create/resume was
pruned before herdr was asked, so a retry under the same name took it over
while the orphaned pane was still alive (and create's failure cleanup could
then kill it). It is now kept and blocks like any other orphan until its
row is registered for the attempt or herdr reports the session gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
Godopu merged commit 0461a632eb into main 2026-10-23 06:46:13 +00:00
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#69