[Bug] 같은 작업 폴더에 agy 세션이 2개 이상이면 대화 ID가 겹쳐 기록되어 resume 이 실패하거나 다른 세션의 대화로 이어짐 #63

Closed
opened 2026-10-19 00:17:38 +00:00 by Godopu · 5 comments
Owner

증상

같은 workspace 에서 agy 세션 2개(creator-agy-image, creator-agy-writer)를 운영하다가
multi-agent-mux-stop 으로 둘 다 종료한 뒤 multi-agent-mux-resume 을 실행하니,
두 행의 agy_conversation_id_own 이 모두 creator-agy-image 의 대화(5fa7efe7…)로 기록되어 있었다.

  • creator-agy-image: 정상 resume (자기 대화)
  • creator-agy-writer: resume 실패 (exit 1)
    VALIDATE: Duplicate running conversation ID '5fa7efe7-…' detected between 'creator-agy-image' and 'creator-agy-writer'

writer 의 실제 대화는 b9a9490e… 였다. 대화 db 안의 작업 지시서 경로·세션 이름으로 확인했다.
등록부(.mam/agent-sessions.yaml, .mam/agent-sessions.db)의 값을 손으로 고친 뒤에야 resume 되었다.

검사가 없었다면 writer 가 image 의 대화를 이어받아 두 세션이 같은 대화를 쓰게 되었을 것이다.

원인

lib_py/agents/adapters/agy.py 의 discover() 는
~/.gemini/antigravity-cli/cache/last_conversations.json 에서 workspace 경로를 키로 대화 ID 하나를 읽는다.

cand = json.load(f).get(ctx.workspace)

이 파일은 workspace 마다 "마지막 대화" 하나만 기억한다. 그래서 같은 workspace 의 agy 세션이 여럿이면
모두 같은 ID(가장 최근에 저장된 대화)로 판정된다.

create 로 새로 만든 agy 행은 처음에 own id 가 비어 있다. 이후 reconcile·stop(capture_conversation_id → find_workspace_uuid tier-2 디스크 스캔)이
이 값을 own id 로 기록하면서 다른 세션의 ID가 들어간다. 이렇게 한 번 잘못 들어가면 tier-1 으로 굳어져 이후 stop 에서도 그대로 유지된다.

재현

  1. 같은 workspace 에 multi-agent-mux-create --agent agy 로 세션 A, B 를 만든다.
  2. A, B 모두 작업을 시켜 각자 대화를 만든다 (마지막 작업은 A).
  3. multi-agent-mux-stop 으로 A, B 를 종료한다.
  4. 등록부의 두 행 agy_conversation_id_own 이 같은 값인지 확인한다.
  5. multi-agent-mux-resume 으로 A, B 를 차례로 연다. B 에서 Duplicate running conversation ID 로 실패한다.

제안

  • agy 대화 ID 를 workspace 가 아니라 세션(pane) 단위로 확정한다.
    예: create 직후 onboarding 응답을 받을 때 그 pane 의 대화 ID 를 own id 로 기록하고, 이후에는 디스크 스캔으로 덮어쓰지 않는다.
  • tier-2 스캔 결과가 이미 다른 행이 쓰는 ID와 같으면 기록하지 않고 경고한다 (stop·reconcile 공통).
  • 같은 workspace 에 agy 행이 2개 이상일 때 last_conversations.json 결과는 신뢰할 수 없다고 보고 own id 가 없는 행은 resume 을 거부하고 안내한다.

환경

  • 대상 저장소: lac/python_getting_started_lecturenotes (herdr session: mam-pylecturenotes)
  • 세션: creator-agy-image, creator-agy-writer (둘 다 agy, 같은 workspace)
  • 발생일: 2026-10-19
## 증상 같은 workspace 에서 agy 세션 2개(creator-agy-image, creator-agy-writer)를 운영하다가 `multi-agent-mux-stop` 으로 둘 다 종료한 뒤 `multi-agent-mux-resume` 을 실행하니, 두 행의 `agy_conversation_id_own` 이 모두 creator-agy-image 의 대화(5fa7efe7…)로 기록되어 있었다. - creator-agy-image: 정상 resume (자기 대화) - creator-agy-writer: resume 실패 (exit 1) `VALIDATE: Duplicate running conversation ID '5fa7efe7-…' detected between 'creator-agy-image' and 'creator-agy-writer'` writer 의 실제 대화는 b9a9490e… 였다. 대화 db 안의 작업 지시서 경로·세션 이름으로 확인했다. 등록부(`.mam/agent-sessions.yaml`, `.mam/agent-sessions.db`)의 값을 손으로 고친 뒤에야 resume 되었다. 검사가 없었다면 writer 가 image 의 대화를 이어받아 두 세션이 같은 대화를 쓰게 되었을 것이다. ## 원인 `lib_py/agents/adapters/agy.py` 의 `discover()` 는 `~/.gemini/antigravity-cli/cache/last_conversations.json` 에서 **workspace 경로를 키로** 대화 ID 하나를 읽는다. cand = json.load(f).get(ctx.workspace) 이 파일은 workspace 마다 "마지막 대화" 하나만 기억한다. 그래서 같은 workspace 의 agy 세션이 여럿이면 모두 같은 ID(가장 최근에 저장된 대화)로 판정된다. `create` 로 새로 만든 agy 행은 처음에 own id 가 비어 있다. 이후 reconcile·stop(`capture_conversation_id` → `find_workspace_uuid` tier-2 디스크 스캔)이 이 값을 own id 로 기록하면서 다른 세션의 ID가 들어간다. 이렇게 한 번 잘못 들어가면 tier-1 으로 굳어져 이후 stop 에서도 그대로 유지된다. ## 재현 1. 같은 workspace 에 `multi-agent-mux-create --agent agy` 로 세션 A, B 를 만든다. 2. A, B 모두 작업을 시켜 각자 대화를 만든다 (마지막 작업은 A). 3. `multi-agent-mux-stop` 으로 A, B 를 종료한다. 4. 등록부의 두 행 `agy_conversation_id_own` 이 같은 값인지 확인한다. 5. `multi-agent-mux-resume` 으로 A, B 를 차례로 연다. B 에서 Duplicate running conversation ID 로 실패한다. ## 제안 - agy 대화 ID 를 workspace 가 아니라 **세션(pane) 단위**로 확정한다. 예: create 직후 onboarding 응답을 받을 때 그 pane 의 대화 ID 를 own id 로 기록하고, 이후에는 디스크 스캔으로 덮어쓰지 않는다. - tier-2 스캔 결과가 이미 **다른 행이 쓰는 ID**와 같으면 기록하지 않고 경고한다 (stop·reconcile 공통). - 같은 workspace 에 agy 행이 2개 이상일 때 `last_conversations.json` 결과는 신뢰할 수 없다고 보고 own id 가 없는 행은 resume 을 거부하고 안내한다. ## 환경 - 대상 저장소: lac/python_getting_started_lecturenotes (herdr session: mam-pylecturenotes) - 세션: creator-agy-image, creator-agy-writer (둘 다 agy, 같은 workspace) - 발생일: 2026-10-19
Author
Owner

작업 참고 (v4.9.9 기준 코드 확인)

#40(v4.9.9)은 reconcile 과 completed 처리 경로만 고쳤다. 이 이슈의 경로(stop·purge·resume 의 대화 ID 조회)는 아직 그대로다.

1. 잘못된 ID 가 들어오는 곳: lib_py/workspace_uuid.py 의 find_workspace_uuid

  • stop: 프로세스를 끄기 전에 이 함수로 ID 를 찾아 행에 기록한다(stop_session.sh:223, capture_conversation_id).
  • purge: --purge-conversation 의 삭제 대상도 이 함수로 정한다(stop_session.sh:204).
  • resume: 행에 ID 가 없으면 같은 함수를 쓴다(resolve_session_id.sh:43).

2. 이 함수의 구멍 세 개

  • running 행만 제외 (workspace_uuid.py:30-38): 다른 행의 ID 를 걸러낼 때 status == running 인 행만 본다. 그래서 A 를 먼저 stop 하면 A 의 ID 가 제외 목록에서 빠지고, 이어서 B 를 stop 할 때 B 의 행에 기록된다. 본문의 재현 순서와 같다.
  • 형제 행의 ID 를 그대로 내보냄 (workspace_uuid.py:69-73): 같은 폴더에서 이름이 -creator-<agent> 로 끝나는 행의 own id 를 검증만 하고 내보낸다. 대상 세션의 것인지는 보지 않는다.
  • 캐시 값 하나 (workspace_uuid.py:82, adapter.discover): agy 는 last_conversations.json[cwd] 값 하나를 돌려준다. 같은 폴더에 agy 가 여럿이면 마지막에 연 쪽의 대화다.

3. purge 위험

B 를 --purge-conversation 으로 지우면 위 경로로 A 의 대화 ID 가 삭제 대상이 될 수 있다(코드 흐름으로 확인, 재현은 안 함). 삭제 직전의 사용 중 검사(stop_session.sh:290-294)는 A 가 실행 중일 때만 막는다. A 가 이미 stop 된 상태라면 A 의 대화가 지워진다.

4. 고치는 방향

  • 조회 교체: 세션 ID 가 나중에 생기는 에이전트(agy·opencode·codex·hermes)는 이 탐색 대신 #40 의 BaseAgentAdapter.capture_active_session_id 를 쓴다.
    • 이 함수는 다른 행이 가진 ID 를 멈춘 행 것까지 제외한다.
    • agy 는 pane 프로세스(또는 자손)가 그 대화의 presence lock 이나 DB 를 열고 있는지 lsof·ps 로 확인한다.
    • 판정을 새로 만들지 말고 이 함수를 다시 쓴다.
  • stop 순서: 프로세스를 끄기 전에 ID 를 찾아야 소유 확인이 통과한다. 지금도 kill(:237 이후)보다 앞에서 찾는다. herdr 가 이미 죽은 행은 확인할 수 없으니 기록하지 않고 이유를 남긴다.
  • purge: 행에 기록된 own id 이거나 확인을 통과한 ID 만 지운다. 캐시가 추측한 ID 로는 지우지 않는다.
  • resume: own id 가 없고 같은 폴더에 같은 에이전트 행이 둘 이상이면 추측하지 않는다. Class A 는 새 대화로 띄우고 그 사실을 알린다(본문 제안 3).
  • claude·grok: ID 를 미리 정하는 에이전트(pre_assigned)라 이 문제와 무관하다. 동작을 바꾸지 않는다.

5. (선택) 먼저 뜬 agy 가 등록되지 않는 문제

  • #40 Linux 확인에서 같은 폴더에 agy 를 하나 더 열면 last_conversations.json 의 그 폴더 항목이 나중에 연 쪽으로 바뀌는 것을 확인했다.
  • 그래서 먼저 뜬 agy 가 등록되기 전에 두 번째가 열리면, 먼저 뜬 agy 는 자동 등록되지 않는다(owner-mismatch, 잘못된 ID 를 기록하지는 않음).
  • 캐시 대신 pane 프로세스(또는 자손)가 연 presence/<uuid>.lock 에서 ID 를 얻으면 같은 폴더에 agy 가 여럿이어도 세션마다 정해진다(본문 제안 1).
  • 근거: #40 Linux 확인에서 resume 된 agy 는 lock 만, 새로 띄운 agy 는 lock 과 DB 를 연다. 두 경우 모두 연 프로세스가 pane pid 였다.
  • 범위가 커지면 따로 떼어도 된다.

6. 완료 조건 (회귀 테스트로)

  1. 같은 폴더의 agy A·B 가 각자 대화를 만들고 마지막 작업이 A 일 때, A→B 순서로 stop 해도 B 의 행에 A 의 ID 가 기록되지 않는다. B→A 순서도 같다.
  2. B 를 --purge-conversation --yes 로 지워도 A 의 대화 파일(conversations/<A>.db, 사이드카, brain/, annotations/, presence/)은 남는다.
  3. own id 가 없는 B 를 resume 할 때 A 의 대화로 이어지지 않는다.
  4. 행에 own id 가 이미 있는 경우와 claude·grok 행은 동작이 그대로다.
  5. 기존 tests/test_issue40_auto_pin.py, tests/test_issue64_ro_connect.py, tests/test_issue10_13_purge.py 가 그대로 통과한다.

7. 테스트와 제약

  • 임시 HOME·임시 대화 DB·mock herdr 와 가짜 lsof·ps 로 재현한다(#40 테스트의 픽스처를 재사용할 수 있다). reconcile 을 거치는 테스트는 mock herdr 에 그 세션을 running agent 로 넣어야 drift C 까지 간다.
  • 실제 에이전트 홈(~/.gemini, ~/.codex, ~/.hermes, ~/.claude, opencode data)의 DB 는 sqlite 로 열지 않는다(읽기 전용도 금지). DB 조회는 lib_py.agents.base._ro_connect 를 쓴다.
  • .mam.env 를 export 하지 않는다. 실제 .mam, 모니터, herdr 서버를 건드리지 않는다. lib.sh 를 source 하는 테스트는 own_workspace_root fixture 를 쓴다.
  • macOS(bash 3.2, Python 3.9)와 Linux 양쪽에서 돌아야 한다. 새 판정은 셸이 아니라 Python 에 둔다.
  • 버전 번호와 VERSIONS.md 는 건드리지 않는다. PR 본문은 Refs #63.
## 작업 참고 (v4.9.9 기준 코드 확인) #40(v4.9.9)은 reconcile 과 completed 처리 경로만 고쳤다. 이 이슈의 경로(stop·purge·resume 의 대화 ID 조회)는 아직 그대로다. ### 1. 잘못된 ID 가 들어오는 곳: `lib_py/workspace_uuid.py` 의 `find_workspace_uuid` - **stop**: 프로세스를 끄기 전에 이 함수로 ID 를 찾아 행에 기록한다(`stop_session.sh:223`, `capture_conversation_id`). - **purge**: `--purge-conversation` 의 삭제 대상도 이 함수로 정한다(`stop_session.sh:204`). - **resume**: 행에 ID 가 없으면 같은 함수를 쓴다(`resolve_session_id.sh:43`). ### 2. 이 함수의 구멍 세 개 - **`running` 행만 제외** (`workspace_uuid.py:30-38`): 다른 행의 ID 를 걸러낼 때 `status == running` 인 행만 본다. 그래서 A 를 먼저 stop 하면 A 의 ID 가 제외 목록에서 빠지고, 이어서 B 를 stop 할 때 B 의 행에 기록된다. 본문의 재현 순서와 같다. - **형제 행의 ID 를 그대로 내보냄** (`workspace_uuid.py:69-73`): 같은 폴더에서 이름이 `-creator-<agent>` 로 끝나는 행의 own id 를 검증만 하고 내보낸다. 대상 세션의 것인지는 보지 않는다. - **캐시 값 하나** (`workspace_uuid.py:82`, `adapter.discover`): agy 는 `last_conversations.json[cwd]` 값 하나를 돌려준다. 같은 폴더에 agy 가 여럿이면 마지막에 연 쪽의 대화다. ### 3. purge 위험 B 를 `--purge-conversation` 으로 지우면 위 경로로 A 의 대화 ID 가 삭제 대상이 될 수 있다(코드 흐름으로 확인, 재현은 안 함). 삭제 직전의 사용 중 검사(`stop_session.sh:290-294`)는 A 가 실행 중일 때만 막는다. A 가 이미 stop 된 상태라면 A 의 대화가 지워진다. ### 4. 고치는 방향 - **조회 교체**: 세션 ID 가 나중에 생기는 에이전트(agy·opencode·codex·hermes)는 이 탐색 대신 #40 의 `BaseAgentAdapter.capture_active_session_id` 를 쓴다. - 이 함수는 다른 행이 가진 ID 를 멈춘 행 것까지 제외한다. - agy 는 pane 프로세스(또는 자손)가 그 대화의 presence lock 이나 DB 를 열고 있는지 `lsof`·`ps` 로 확인한다. - 판정을 새로 만들지 말고 이 함수를 다시 쓴다. - **stop 순서**: 프로세스를 끄기 전에 ID 를 찾아야 소유 확인이 통과한다. 지금도 kill(:237 이후)보다 앞에서 찾는다. herdr 가 이미 죽은 행은 확인할 수 없으니 기록하지 않고 이유를 남긴다. - **purge**: 행에 기록된 own id 이거나 확인을 통과한 ID 만 지운다. 캐시가 추측한 ID 로는 지우지 않는다. - **resume**: own id 가 없고 같은 폴더에 같은 에이전트 행이 둘 이상이면 추측하지 않는다. Class A 는 새 대화로 띄우고 그 사실을 알린다(본문 제안 3). - **claude·grok**: ID 를 미리 정하는 에이전트(`pre_assigned`)라 이 문제와 무관하다. 동작을 바꾸지 않는다. ### 5. (선택) 먼저 뜬 agy 가 등록되지 않는 문제 - #40 Linux 확인에서 같은 폴더에 agy 를 하나 더 열면 `last_conversations.json` 의 그 폴더 항목이 나중에 연 쪽으로 바뀌는 것을 확인했다. - 그래서 먼저 뜬 agy 가 등록되기 전에 두 번째가 열리면, 먼저 뜬 agy 는 자동 등록되지 않는다(`owner-mismatch`, 잘못된 ID 를 기록하지는 않음). - 캐시 대신 **pane 프로세스(또는 자손)가 연 `presence/<uuid>.lock`** 에서 ID 를 얻으면 같은 폴더에 agy 가 여럿이어도 세션마다 정해진다(본문 제안 1). - 근거: #40 Linux 확인에서 resume 된 agy 는 lock 만, 새로 띄운 agy 는 lock 과 DB 를 연다. 두 경우 모두 연 프로세스가 pane pid 였다. - 범위가 커지면 따로 떼어도 된다. ### 6. 완료 조건 (회귀 테스트로) 1. 같은 폴더의 agy A·B 가 각자 대화를 만들고 마지막 작업이 A 일 때, A→B 순서로 stop 해도 B 의 행에 A 의 ID 가 기록되지 않는다. B→A 순서도 같다. 2. B 를 `--purge-conversation --yes` 로 지워도 A 의 대화 파일(`conversations/<A>.db`, 사이드카, `brain/`, `annotations/`, `presence/`)은 남는다. 3. own id 가 없는 B 를 resume 할 때 A 의 대화로 이어지지 않는다. 4. 행에 own id 가 이미 있는 경우와 claude·grok 행은 동작이 그대로다. 5. 기존 `tests/test_issue40_auto_pin.py`, `tests/test_issue64_ro_connect.py`, `tests/test_issue10_13_purge.py` 가 그대로 통과한다. ### 7. 테스트와 제약 - 임시 HOME·임시 대화 DB·mock herdr 와 가짜 `lsof`·`ps` 로 재현한다(#40 테스트의 픽스처를 재사용할 수 있다). reconcile 을 거치는 테스트는 mock herdr 에 그 세션을 running agent 로 넣어야 drift C 까지 간다. - 실제 에이전트 홈(`~/.gemini`, `~/.codex`, `~/.hermes`, `~/.claude`, opencode data)의 DB 는 sqlite 로 열지 않는다(읽기 전용도 금지). DB 조회는 `lib_py.agents.base._ro_connect` 를 쓴다. - `.mam.env` 를 export 하지 않는다. 실제 `.mam`, 모니터, herdr 서버를 건드리지 않는다. lib.sh 를 source 하는 테스트는 `own_workspace_root` fixture 를 쓴다. - macOS(bash 3.2, Python 3.9)와 Linux 양쪽에서 돌아야 한다. 새 판정은 셸이 아니라 Python 에 둔다. - 버전 번호와 VERSIONS.md 는 건드리지 않는다. PR 본문은 `Refs #63`.
Author
Owner

작업 범위 (v4.9.9 기준 확인 후 확정)

확인 결과

  • 위 댓글의 구멍 세 개를 임시 HOME 으로 재현했다(find_workspace_uuid 를 직접 호출). 같은 폴더의 agy A·B, 캐시는 A 를 가리킴, B 는 own id 없음:
    • A 실행 중이고 A 의 ID 가 기록됨 → B 는 빈 값 (정상)
    • A 를 먼저 stop → B 가 A 의 ID 를 받음
    • 둘 다 ID 없음 → B 가 A 의 ID 를 받음
    • 이름이 -creator-agy 로 끝나는 stop 된 행이 A 의 ID 를 가짐 → B 가 A 의 ID 를 받음
  • 본문의 세션 이름(creator-agy-<역할>)은 -creator-agy 로 끝나지 않으므로, 본문 사례의 원인은 캐시 쪽이다.

추가로 찾은 것

  1. resume 은 띄운 뒤에 검사한다. resume_session.sh 가 agy --conversation <추측한 ID> 로 세션을 띄운 다음(:245) 레지스트리를 갱신하고, 중복 검사(atomic_yaml.py:143)는 그 갱신에서야 돈다. 본문의 VALIDATE: Duplicate running conversation ID 가 났을 때 writer 쪽 pane 에는 이미 image 의 대화가 열려 있었다.
  2. resume 의 "이미 실행 중" 분기(:155)도 행에 ID 가 없으면 추측한 ID 를 실행 중인 행에 기록한다. 상대 행이 stop 된 상태면 중복 검사도 통과한다.
  3. codex·hermes·opencode 의 기본 판정(BaseAgentAdapter.capture_active_session_id)에는 소유 확인이 없다(후보 하나·세션보다 새것·다른 행이 기록하지 않은 것만). 같은 폴더에 같은 종류 둘이 다 ID 가 없으면 틀릴 수 있다. 아래 "생성 순서 규칙" 으로 함께 막는다.

이번에 고치는 것

  1. 조회 교체 (위 댓글 4): stop·resume 에서 ID 가 나중에 생기는 에이전트(agy·codex·hermes·opencode)는 find_workspace_uuid 의 추측 대신 #40 의 판정(autopin.capture → capture_active_session_id)을 쓴다. claude·grok 은 바꾸지 않는다.
  2. stop: 프로세스를 끄기 전에 확인한다. herdr 가 이미 죽은 행은 확인할 수 없으므로 ID 를 기록하지 않고 이유를 남긴다.
  3. purge: 행에 기록된 own id 이거나 확인을 통과한 ID 만 지운다. 다른 행이 기록한 ID 는 지우지 않는다.
  4. resume:
    • own id 가 없고 같은 폴더에 같은 종류 행이 둘 이상이면 추측하지 않는다. 새 대화로 띄우고 그 사실을 알린다. 하나뿐이면 지금처럼 찾는다.
    • 띄우기 전에 해결한 ID 를 다른 행이 이미 기록했는지 확인하고, 그렇다면 띄우지 않고 거부한다.
    • "이미 실행 중" 분기도 같은 판정을 쓴다.
  5. 생성 순서 규칙 (새로 추가): 같은 폴더에 ID 가 나중에 생기는 같은 종류 에이전트가 생성 중이거나 아직 ID 가 기록되지 않은 채 실행 중이면, 새 create·resume 은 그 ID 가 기록될 때까지 기다렸다가 진행한다.
    • 대기 상한을 넘으면 거부하고 이유(어느 세션을 기다렸는지)를 알린다. 상한은 환경 변수로 바꿀 수 있게 한다.
    • "생성 중" 은 레지스트리에 행이 아직 없으므로, 폴더 단위 잠금으로 알린다(같은 herdr 서버 안의 pane 잠금만으로는 서버가 다르면 지나친다).
    • 이 규칙이 있으면 먼저 뜬 세션의 ID 가 캐시 덮어쓰기 전에 기록되므로, 위 댓글 5(presence lock 으로 ID 찾기)는 이번에는 하지 않는다. MAM 밖에서 사람이 직접 같은 폴더에 띄운 에이전트까지 다뤄야 할 때 다시 본다.
    • --no-onboard 로 만든 세션은 첫 대화 전까지 ID 가 생기지 않아 뒤 세션이 상한까지 기다린다. 이 옵션은 #68 에서 없앤다.

완료 조건

위 댓글 6 의 1~5 에 더해:
6. 같은 폴더·같은 종류 에이전트의 create 가 겹치면 뒤 create 는 앞 세션의 ID 가 기록된 뒤에 띄워진다. 상한을 넘기면 아무것도 띄우지 않고 거부한다.
7. resume 이 다른 행이 기록한 ID 로 에이전트를 띄우지 않는다(띄우기 전에 거부).

테스트와 제약은 위 댓글 7 그대로다. 버전 번호와 VERSIONS.md 는 건드리지 않고, PR 본문은 Refs #63.

## 작업 범위 (v4.9.9 기준 확인 후 확정) ### 확인 결과 - 위 댓글의 구멍 세 개를 임시 HOME 으로 재현했다(`find_workspace_uuid` 를 직접 호출). 같은 폴더의 agy A·B, 캐시는 A 를 가리킴, B 는 own id 없음: - A 실행 중이고 A 의 ID 가 기록됨 → B 는 빈 값 (정상) - **A 를 먼저 stop** → B 가 A 의 ID 를 받음 - **둘 다 ID 없음** → B 가 A 의 ID 를 받음 - 이름이 `-creator-agy` 로 끝나는 stop 된 행이 A 의 ID 를 가짐 → B 가 A 의 ID 를 받음 - 본문의 세션 이름(`creator-agy-<역할>`)은 `-creator-agy` 로 끝나지 않으므로, 본문 사례의 원인은 캐시 쪽이다. ### 추가로 찾은 것 1. **resume 은 띄운 뒤에 검사한다.** `resume_session.sh` 가 `agy --conversation <추측한 ID>` 로 세션을 띄운 다음(`:245`) 레지스트리를 갱신하고, 중복 검사(`atomic_yaml.py:143`)는 그 갱신에서야 돈다. 본문의 `VALIDATE: Duplicate running conversation ID` 가 났을 때 writer 쪽 pane 에는 이미 image 의 대화가 열려 있었다. 2. **resume 의 "이미 실행 중" 분기**(`:155`)도 행에 ID 가 없으면 추측한 ID 를 실행 중인 행에 기록한다. 상대 행이 stop 된 상태면 중복 검사도 통과한다. 3. codex·hermes·opencode 의 기본 판정(`BaseAgentAdapter.capture_active_session_id`)에는 소유 확인이 없다(후보 하나·세션보다 새것·다른 행이 기록하지 않은 것만). 같은 폴더에 같은 종류 둘이 다 ID 가 없으면 틀릴 수 있다. 아래 "생성 순서 규칙" 으로 함께 막는다. ### 이번에 고치는 것 1. **조회 교체** (위 댓글 4): stop·resume 에서 ID 가 나중에 생기는 에이전트(agy·codex·hermes·opencode)는 `find_workspace_uuid` 의 추측 대신 #40 의 판정(`autopin.capture` → `capture_active_session_id`)을 쓴다. claude·grok 은 바꾸지 않는다. 2. **stop**: 프로세스를 끄기 전에 확인한다. herdr 가 이미 죽은 행은 확인할 수 없으므로 ID 를 기록하지 않고 이유를 남긴다. 3. **purge**: 행에 기록된 own id 이거나 확인을 통과한 ID 만 지운다. 다른 행이 기록한 ID 는 지우지 않는다. 4. **resume**: - own id 가 없고 같은 폴더에 같은 종류 행이 둘 이상이면 추측하지 않는다. 새 대화로 띄우고 그 사실을 알린다. 하나뿐이면 지금처럼 찾는다. - **띄우기 전에** 해결한 ID 를 다른 행이 이미 기록했는지 확인하고, 그렇다면 띄우지 않고 거부한다. - "이미 실행 중" 분기도 같은 판정을 쓴다. 5. **생성 순서 규칙 (새로 추가)**: 같은 폴더에 ID 가 나중에 생기는 같은 종류 에이전트가 **생성 중이거나 아직 ID 가 기록되지 않은 채 실행 중**이면, 새 create·resume 은 그 ID 가 기록될 때까지 **기다렸다가** 진행한다. - 대기 상한을 넘으면 거부하고 이유(어느 세션을 기다렸는지)를 알린다. 상한은 환경 변수로 바꿀 수 있게 한다. - "생성 중" 은 레지스트리에 행이 아직 없으므로, 폴더 단위 잠금으로 알린다(같은 herdr 서버 안의 pane 잠금만으로는 서버가 다르면 지나친다). - 이 규칙이 있으면 먼저 뜬 세션의 ID 가 캐시 덮어쓰기 전에 기록되므로, 위 댓글 5(presence lock 으로 ID 찾기)는 이번에는 하지 않는다. MAM 밖에서 사람이 직접 같은 폴더에 띄운 에이전트까지 다뤄야 할 때 다시 본다. - `--no-onboard` 로 만든 세션은 첫 대화 전까지 ID 가 생기지 않아 뒤 세션이 상한까지 기다린다. 이 옵션은 #68 에서 없앤다. ### 완료 조건 위 댓글 6 의 1~5 에 더해: 6. 같은 폴더·같은 종류 에이전트의 create 가 겹치면 뒤 create 는 앞 세션의 ID 가 기록된 뒤에 띄워진다. 상한을 넘기면 아무것도 띄우지 않고 거부한다. 7. resume 이 다른 행이 기록한 ID 로 에이전트를 띄우지 않는다(띄우기 전에 거부). 테스트와 제약은 위 댓글 7 그대로다. 버전 번호와 VERSIONS.md 는 건드리지 않고, PR 본문은 `Refs #63`.
Author
Owner

PR #69 병합 (v4.10.0)

PR #69 를 리뷰·테스트한 뒤 main 에 병합했다(머지 커밋 0461a63, 릴리스 커밋 0454fe9). 변경 요약은 VERSIONS.md 의 v4.10.0 항목에 있다.

리뷰 중 추가된 수정

4인 리뷰(--all-reviewer, 리뷰 전용 루프)에서 codex 가 고아 "생성 중" 표시 처리의 차단 결함을 회차마다 찾았고, creator 가 아래 커밋으로 고쳤다.

커밋 지적 수정
ede9c62 create·resume 이 pane 을 띄운 뒤 행 등록 전에 죽으면, 다음 대기가 owner 가 죽었다는 이유만으로 표시를 지움 표시에 herdr 서버를 기록하고, 그 세션이 살아 있으면 계속 막음
8f64174 herdr 조회 실패를 "세션 없음"으로 판단 세션이 없다는 확실한 응답에서만 지우고, 조회는 대기 상한 안에서만
7cac6f6 대기 상한 0 이어도 조회가 최대 30 초 server_not_running 은 "모름", 상한 0 이면 herdr 를 묻지 않음
1614af5 같은 세션 이름의 죽은 owner 표시는 확인 없이 지움 같은 고아 규칙을 적용

마지막 루프 1회차(@1614af5)에서 4/4 PASS.

검증 (macOS, Python 3.9.6, @1614af5)

  • 전체 스위트: 2097 passed, 56 skipped, 1 failed.
    • 실패는 tests/test_issue50_dispatch_herdr.py::test_c11_… 하나다. 이 PR 은 위임 코드를 건드리지 않고, 단독으로 세 번 돌리면 매번 통과했다(시간 의존).
  • tests/test_issue63_same_cwd_ids.py 27개 통과.
  • 스위트가 실제 에이전트 홈, herdr 서버 pane 목록, opencode 데이터에 남긴 것은 없었다.

남은 비차단 (리뷰어 의견)

  • herdr 조회가 계속 실패하면 고아 표시가 남아, 이후 create·resume 이 매번 MAM_ID_ORDER_WAIT_S 까지 기다린다. 거부 메시지가 herdr kill-session 명령을 알려 준다.
  • 등록 중에 herdr 목록이 잠깐 비어 있으면 표시가 풀릴 수 있다.
  • --no-onboard 세션 뒤 create 의 대기는 #68 에서 다룬다.
  • presence lock 으로 ID 를 찾는 것은 범위 밖이다.
  • resume 의 대기 호출은 셸 단계 테스트로 고정되어 있지 않다(dry-run 은 대기 전에 끝남).

Linux 확인 제안

작성자의 Linux 스위트와 실제 agy 두 개 시험은 a907cc5·d115bf7 기준이었다. 그 뒤 id_order.py 의 herdr 조회 부분이 바뀌었으므로, Linux 에서 다음을 한 번 확인하면 이슈를 닫을 수 있다.

  1. main(v4.10.0)에서 tests/test_issue63_same_cwd_ids.py 와 전체 스위트.
  2. 같은 폴더 agy 동시 create: 뒤 세션이 기다렸다가 앞 세션의 ID 가 기록된 뒤 뜨고, 두 ID 가 다른지.
  3. (선택) 앞 create 를 spawn 직후 kill 했을 때, 다음 create 가 상한까지 기다리고 거부 메시지에 정리 명령이 나오는지.
## PR #69 병합 (v4.10.0) PR #69 를 리뷰·테스트한 뒤 `main` 에 병합했다(머지 커밋 `0461a63`, 릴리스 커밋 `0454fe9`). 변경 요약은 `VERSIONS.md` 의 `v4.10.0` 항목에 있다. ### 리뷰 중 추가된 수정 4인 리뷰(`--all-reviewer`, 리뷰 전용 루프)에서 codex 가 고아 "생성 중" 표시 처리의 차단 결함을 회차마다 찾았고, creator 가 아래 커밋으로 고쳤다. | 커밋 | 지적 | 수정 | |---|---|---| | `ede9c62` | create·resume 이 pane 을 띄운 뒤 행 등록 전에 죽으면, 다음 대기가 owner 가 죽었다는 이유만으로 표시를 지움 | 표시에 herdr 서버를 기록하고, 그 세션이 살아 있으면 계속 막음 | | `8f64174` | herdr 조회 실패를 "세션 없음"으로 판단 | 세션이 없다는 확실한 응답에서만 지우고, 조회는 대기 상한 안에서만 | | `7cac6f6` | 대기 상한 0 이어도 조회가 최대 30 초 | `server_not_running` 은 "모름", 상한 0 이면 herdr 를 묻지 않음 | | `1614af5` | 같은 세션 이름의 죽은 owner 표시는 확인 없이 지움 | 같은 고아 규칙을 적용 | 마지막 루프 1회차(@`1614af5`)에서 4/4 PASS. ### 검증 (macOS, Python 3.9.6, @`1614af5`) - 전체 스위트: 2097 passed, 56 skipped, 1 failed. - 실패는 `tests/test_issue50_dispatch_herdr.py::test_c11_…` 하나다. 이 PR 은 위임 코드를 건드리지 않고, 단독으로 세 번 돌리면 매번 통과했다(시간 의존). - `tests/test_issue63_same_cwd_ids.py` 27개 통과. - 스위트가 실제 에이전트 홈, herdr 서버 pane 목록, opencode 데이터에 남긴 것은 없었다. ### 남은 비차단 (리뷰어 의견) - herdr 조회가 계속 실패하면 고아 표시가 남아, 이후 create·resume 이 매번 `MAM_ID_ORDER_WAIT_S` 까지 기다린다. 거부 메시지가 `herdr kill-session` 명령을 알려 준다. - 등록 중에 herdr 목록이 잠깐 비어 있으면 표시가 풀릴 수 있다. - `--no-onboard` 세션 뒤 create 의 대기는 #68 에서 다룬다. - presence lock 으로 ID 를 찾는 것은 범위 밖이다. - resume 의 대기 호출은 셸 단계 테스트로 고정되어 있지 않다(dry-run 은 대기 전에 끝남). ### Linux 확인 제안 작성자의 Linux 스위트와 실제 agy 두 개 시험은 `a907cc5`·`d115bf7` 기준이었다. 그 뒤 `id_order.py` 의 herdr 조회 부분이 바뀌었으므로, Linux 에서 다음을 한 번 확인하면 이슈를 닫을 수 있다. 1. `main`(v4.10.0)에서 `tests/test_issue63_same_cwd_ids.py` 와 전체 스위트. 2. 같은 폴더 agy 동시 create: 뒤 세션이 기다렸다가 앞 세션의 ID 가 기록된 뒤 뜨고, 두 ID 가 다른지. 3. (선택) 앞 create 를 spawn 직후 kill 했을 때, 다음 create 가 상한까지 기다리고 거부 메시지에 정리 명령이 나오는지.
Author
Owner

Linux 확인 (v4.10.0, main @0461a63)

환경: Linux, Python 3.14.7, bash 5.3, herdr 0.9.3, agy 1.4.6.

1. 테스트

  • tests/test_issue63_same_cwd_ids.py: 27 passed.
  • 전체 스위트: 2135 passed, 19 skipped, 0 failed. XDG_* 를 비우고 -p no:cacheprovider, 트리 밖 --basetemp 로 돌렸다.

2. 같은 폴더 agy 동시 create

main 의 별도 복사본과 별도 herdr 서버를 썼다. A 를 띄우고 3 초 뒤 B 를 띄웠다.

  • B 는 id-order: waiting for <A> … 를 내고 기다렸다.
  • A 의 온보딩이 completed 된 직후(자동 등록 2 초 이내) B 의 pane 이 떴다.
  • 두 create 모두 exit 0 이었다. 두 ID 는 서로 달랐고, 각 대화의 presence lock 은 자기 pane pid 만 쥐고 있었다.

3. 앞 create 를 띄운 직후 kill

C 의 create 를 pane 이 뜨자마자(약 1 초) 프로세스 그룹째 SIGKILL 했다. C 의 pane 은 살아 있고 행은 등록되지 않았다. "생성 중" 표시에는 herdr 서버가 기록되어 있었다.

  • MAM_ID_ORDER_WAIT_S=30 으로 D 를 create 했다.
    • D 는 C 를 기다렸고, 31 초 뒤 exit 1 로 거부했다. D 의 pane 은 뜨지 않았고 행도 생기지 않았다.
    • 거부 메시지 두 줄:
      • ERROR: <C> in <cwd> still has no recorded agy id after 30s; not starting <D> …
      • ERROR: <C>'s create/resume exited before recording it, and its herdr session is alive; close it with: HERDR_SESSION_NAME=<server> herdr kill-session -t <C>
  • 메시지의 명령을 그대로 실행하자 C 의 pane 이 내려갔다.
    • 다시 create 한 D 는 기다리지 않고 바로 떴다(exit 0). C 의 고아 표시는 그때 지워졌다.
    • D 의 ID 는 A·B 와 달랐다.

정리

  • 시험 세션 A·B·D 는 --purge-conversation --yes 로 지웠다. 대화 파일이 남지 않았고 행도 없다.
  • 시험 서버와 복사본도 지웠다.
  • agy 의 cache/last_conversations.json·history.jsonl 에는 지운 ID 가 남아 있다(#40 과 같음).

고칠 것은 찾지 못했다. 이 이슈는 닫아도 된다.

## Linux 확인 (v4.10.0, `main` @`0461a63`) 환경: Linux, Python 3.14.7, bash 5.3, herdr 0.9.3, agy 1.4.6. ### 1. 테스트 - `tests/test_issue63_same_cwd_ids.py`: 27 passed. - 전체 스위트: 2135 passed, 19 skipped, 0 failed. `XDG_*` 를 비우고 `-p no:cacheprovider`, 트리 밖 `--basetemp` 로 돌렸다. ### 2. 같은 폴더 agy 동시 create `main` 의 별도 복사본과 별도 herdr 서버를 썼다. A 를 띄우고 3 초 뒤 B 를 띄웠다. - B 는 `id-order: waiting for <A> …` 를 내고 기다렸다. - A 의 온보딩이 completed 된 직후(자동 등록 2 초 이내) B 의 pane 이 떴다. - 두 create 모두 exit 0 이었다. 두 ID 는 서로 달랐고, 각 대화의 presence lock 은 자기 pane pid 만 쥐고 있었다. ### 3. 앞 create 를 띄운 직후 kill C 의 create 를 pane 이 뜨자마자(약 1 초) 프로세스 그룹째 SIGKILL 했다. C 의 pane 은 살아 있고 행은 등록되지 않았다. "생성 중" 표시에는 herdr 서버가 기록되어 있었다. - `MAM_ID_ORDER_WAIT_S=30` 으로 D 를 create 했다. - D 는 C 를 기다렸고, 31 초 뒤 exit 1 로 거부했다. D 의 pane 은 뜨지 않았고 행도 생기지 않았다. - 거부 메시지 두 줄: - `ERROR: <C> in <cwd> still has no recorded agy id after 30s; not starting <D> …` - `ERROR: <C>'s create/resume exited before recording it, and its herdr session is alive; close it with: HERDR_SESSION_NAME=<server> herdr kill-session -t <C>` - 메시지의 명령을 그대로 실행하자 C 의 pane 이 내려갔다. - 다시 create 한 D 는 기다리지 않고 바로 떴다(exit 0). C 의 고아 표시는 그때 지워졌다. - D 의 ID 는 A·B 와 달랐다. ### 정리 - 시험 세션 A·B·D 는 `--purge-conversation --yes` 로 지웠다. 대화 파일이 남지 않았고 행도 없다. - 시험 서버와 복사본도 지웠다. - agy 의 `cache/last_conversations.json`·`history.jsonl` 에는 지운 ID 가 남아 있다(#40 과 같음). 고칠 것은 찾지 못했다. 이 이슈는 닫아도 된다.
Author
Owner

Linux 확인 결과 확인

v4.10.0(main @0461a63)의 Linux 확인 결과, 제안한 세 항목이 모두 기대대로 동작했다.

  1. 테스트: test_issue63_same_cwd_ids.py 27 passed. 전체 스위트는 실패 없이 2135 passed, 19 skipped. macOS 에서 한 번 실패한 test_issue50_dispatch_herdr.py::test_c11_… 도 통과했다.
  2. 같은 폴더 agy 동시 create: B 는 A 의 ID 가 기록될 때까지 기다린 뒤 떴다. 두 ID 는 달랐고, 각 presence lock 은 자기 pane 만 쥐고 있었다.
  3. 띄운 직후 kill 한 create(C):
    • 다음 create(D)는 상한(30 s)까지 기다린 뒤, 아무것도 띄우지 않고 거부했다(exit 1).
    • 거부 메시지가 알려 준 herdr kill-session 명령으로 C 를 정리하자, D 가 바로 떴다. 이때 고아 표시도 지워졌다.
    • 리뷰 중 고친 고아 "생성 중" 표시 처리(ede9c62·8f64174·7cac6f6·1614af5)가 실제 herdr 에서도 의도대로 동작한다.

agy 의 last_conversations.json·history.jsonl 에 지운 ID 가 남는 것은 #40 과 같은 기존 동작이라 이 이슈의 범위가 아니다. 완료 조건을 모두 확인했으므로 닫는다.

## Linux 확인 결과 확인 v4.10.0(main @`0461a63`)의 Linux 확인 결과, 제안한 세 항목이 모두 기대대로 동작했다. 1. 테스트: `test_issue63_same_cwd_ids.py` 27 passed. 전체 스위트는 실패 없이 2135 passed, 19 skipped. macOS 에서 한 번 실패한 `test_issue50_dispatch_herdr.py::test_c11_…` 도 통과했다. 2. 같은 폴더 agy 동시 create: B 는 A 의 ID 가 기록될 때까지 기다린 뒤 떴다. 두 ID 는 달랐고, 각 presence lock 은 자기 pane 만 쥐고 있었다. 3. 띄운 직후 kill 한 create(C): - 다음 create(D)는 상한(30 s)까지 기다린 뒤, 아무것도 띄우지 않고 거부했다(exit 1). - 거부 메시지가 알려 준 `herdr kill-session` 명령으로 C 를 정리하자, D 가 바로 떴다. 이때 고아 표시도 지워졌다. - 리뷰 중 고친 고아 "생성 중" 표시 처리(`ede9c62`·`8f64174`·`7cac6f6`·`1614af5`)가 실제 herdr 에서도 의도대로 동작한다. agy 의 `last_conversations.json`·`history.jsonl` 에 지운 ID 가 남는 것은 #40 과 같은 기존 동작이라 이 이슈의 범위가 아니다. 완료 조건을 모두 확인했으므로 닫는다.
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: tmpl/multi-agent-mux#63