프로세스 기동 전 --session-id <uuid> 플래그로 UUID를 주입할 수 있어, create_session.sh 실행 즉시 agent-sessions.yaml에 assigned 상태로 기록됩니다.
사후 감지형 (Post-discovered, Class A: agy, opencode, codex, hermes):
기동 시점에는 UUID를 미리 주입할 수 없으며, 에이전트가 실제로 첫 프롬프트(온보딩 핸드셰이크 등)를 수행한 후에야 에이전트 내부 엔진이 대화 파일(.db 또는 세션 파일)을 디스크에 생성합니다.
따라서 create_session.sh 시점에는 own_id가 null (session_id_source: pending-discovery)로 등록됩니다.
현재 구조의 한계와 누락 발생 원인
create_session.sh의 --onboard 흐름에서는 에이전트가 started 이벤트를 쏘자마자 백그라운드로 reconcile.sh --once &를 1회 실행합니다.
그러나 Class A 에이전트(agy 등)는 started 시점에는 이제 막 프롬프트를 수신한 상태라 디스크에 대화 DB가 아직 생성되지 않았습니다. 따라서 이 최초 reconcile은 ID를 발견하지 못하고 종료됩니다.
이후 에이전트가 온보딩을 완수하고 completed 이벤트를 발행하여 실제 대화 DB가 디스크에 생성된 시점에는 아무도 자동 Reconcile을 호출하지 않습니다.
결과적으로 백그라운드 모니터 데몬(reconcile.sh --subscribe)이 실행 중이지 않은 환경에서는 사람이 수동으로 reconcile.sh를 실행할 때까지 agy_conversation_id_own: null 상태로 방치되어, 추후 resume 실패나 세션 모호성(drift)을 유발합니다.
제안 사항 (Proposed Design)
에이전트마다 생성 및 ID 획득 방식이 다르므로, 이 책임을 외부 쉘 스크립트나 운영자의 수동 개입에 맡기지 않고 에이전트 추상 어댑터 인터페이스(BaseAgentAdapter)의 표준 규격으로 공식 정의할 것을 제안합니다.
1. BaseAgentAdapter 추상 인터페이스 확장 (lib_py/agents/base.py)
classBaseAgentAdapter:...@propertydefid_lifecycle(self)->str:"""
세션 ID 할당 전략:
- 'pre_assigned' : 기동 전에 UUID를 생성하여 CLI에 주입 (Claude, Grok)
- 'post_discovered': 첫 턴 수행 후 디스크/런타임에서 감지 (agy, opencode, codex, hermes 등)
"""return'post_discovered'defcapture_active_session_id(self,ctx:DiscoveryContext)->Optional[str]:"""
[사후 감지형 필수 구현]
첫 턴(온보딩 등)이 완료된 후, 해당 에이전트가 방금 생성한
고유 대화/세션 UUID를 디스크/캐시/런타임으로부터 결정론적(deterministic)으로 추출.
"""candidates=self.discover(ctx)returncandidates[0]ifcandidateselseNonedefauto_pin_if_needed(self,row:dict,workspace:str)->Optional[str]:"""
온보딩이나 첫 작업 완료 시 파이썬 레이어에서 공통 호출되는 훅.
해당 세션의 own_key가 null인 경우 capture_active_session_id()를 실행하여
YAML 및 SQLite 레지스트리에 즉시 확정(Pin / resume_verified) 기록.
"""...
2. 어댑터별 구현 분리
AgyAgentAdapter (lib_py/agents/adapters/agy.py):
id_lifecycle = 'post_discovered'
capture_active_session_id: ~/.gemini/antigravity-cli/cache/last_conversations.json 및 가장 최근 생성된 .db 매핑을 통해 확정 추출.
에이전트가 온보딩 또는 첫 작업 완료 시 publish_event.py --event completed를 실행할 때:
해당 agent_session의 own_id가 미등록(null) 상태라면, 파이썬 코드 레벨에서 즉시 해당 어댑터의 auto_pin_if_needed()를 자동 트리거하도록 강제합니다.
기대 효과 (Benefits)
완전한 자동화 (Zero Manual Step):
에이전트가 온보딩이나 첫 잡을 완료하는 순간 파이썬 레벨에서 즉시 own_id가 확정 등록되어, 수동 reconcile 호출이나 백그라운드 데몬 의존성이 사라집니다.
단일 책임 원칙 및 캡슐화:
create_session.sh 등 쉘 스크립트에서 에이전트별 특성을 하드코딩할 필요 없이, 에이전트 고유의 세션 획득 규칙이 각 어댑터 내부로 캡슐화됩니다.
신규 에이전트 확장성:
향후 새로운 AI CLI 에이전트가 추가되더라도 BaseAgentAdapter 규격만 구현하면 세션 생성·추적 파이프라인에 안전하게 통합됩니다.
## 배경 및 문제점 (Problem Statement)
MAM(multi-agent-mux) 아키텍처에서는 에이전트 CLI별로 세션 ID(대화 UUID)가 결정되는 시점과 방식이 상이합니다:
1. **사전 할당형 (Pre-assigned, Class B: `claude`, `grok`)**:
- 프로세스 기동 전 `--session-id <uuid>` 플래그로 UUID를 주입할 수 있어, `create_session.sh` 실행 즉시 `agent-sessions.yaml`에 `assigned` 상태로 기록됩니다.
2. **사후 감지형 (Post-discovered, Class A: `agy`, `opencode`, `codex`, `hermes`)**:
- 기동 시점에는 UUID를 미리 주입할 수 없으며, **에이전트가 실제로 첫 프롬프트(온보딩 핸드셰이크 등)를 수행한 후에야 에이전트 내부 엔진이 대화 파일(`.db` 또는 세션 파일)을 디스크에 생성**합니다.
- 따라서 `create_session.sh` 시점에는 `own_id`가 `null` (`session_id_source: pending-discovery`)로 등록됩니다.
### 현재 구조의 한계와 누락 발생 원인
- `create_session.sh`의 `--onboard` 흐름에서는 에이전트가 `started` 이벤트를 쏘자마자 백그라운드로 `reconcile.sh --once &`를 1회 실행합니다.
- 그러나 Class A 에이전트(`agy` 등)는 **`started` 시점에는 이제 막 프롬프트를 수신한 상태라 디스크에 대화 DB가 아직 생성되지 않았습니다.** 따라서 이 최초 reconcile은 ID를 발견하지 못하고 종료됩니다.
- 이후 에이전트가 온보딩을 완수하고 `completed` 이벤트를 발행하여 **실제 대화 DB가 디스크에 생성된 시점에는 아무도 자동 Reconcile을 호출하지 않습니다.**
- 결과적으로 백그라운드 모니터 데몬(`reconcile.sh --subscribe`)이 실행 중이지 않은 환경에서는 사람이 수동으로 `reconcile.sh`를 실행할 때까지 `agy_conversation_id_own: null` 상태로 방치되어, 추후 `resume` 실패나 세션 모호성(drift)을 유발합니다.
---
## 제안 사항 (Proposed Design)
에이전트마다 생성 및 ID 획득 방식이 다르므로, 이 책임을 외부 쉘 스크립트나 운영자의 수동 개입에 맡기지 않고 **에이전트 추상 어댑터 인터페이스(`BaseAgentAdapter`)의 표준 규격으로 공식 정의**할 것을 제안합니다.
### 1. `BaseAgentAdapter` 추상 인터페이스 확장 (`lib_py/agents/base.py`)
```python
class BaseAgentAdapter:
...
@property
def id_lifecycle(self) -> str:
"""
세션 ID 할당 전략:
- 'pre_assigned' : 기동 전에 UUID를 생성하여 CLI에 주입 (Claude, Grok)
- 'post_discovered': 첫 턴 수행 후 디스크/런타임에서 감지 (agy, opencode, codex, hermes 등)
"""
return 'post_discovered'
def capture_active_session_id(self, ctx: DiscoveryContext) -> Optional[str]:
"""
[사후 감지형 필수 구현]
첫 턴(온보딩 등)이 완료된 후, 해당 에이전트가 방금 생성한
고유 대화/세션 UUID를 디스크/캐시/런타임으로부터 결정론적(deterministic)으로 추출.
"""
candidates = self.discover(ctx)
return candidates[0] if candidates else None
def auto_pin_if_needed(self, row: dict, workspace: str) -> Optional[str]:
"""
온보딩이나 첫 작업 완료 시 파이썬 레이어에서 공통 호출되는 훅.
해당 세션의 own_key가 null인 경우 capture_active_session_id()를 실행하여
YAML 및 SQLite 레지스트리에 즉시 확정(Pin / resume_verified) 기록.
"""
...
```
### 2. 어댑터별 구현 분리
- **`AgyAgentAdapter` (`lib_py/agents/adapters/agy.py`)**:
- `id_lifecycle = 'post_discovered'`
- `capture_active_session_id`: `~/.gemini/antigravity-cli/cache/last_conversations.json` 및 가장 최근 생성된 `.db` 매핑을 통해 확정 추출.
- **`ClaudeAgentAdapter` (`lib_py/agents/adapters/claude.py`)**:
- `id_lifecycle = 'pre_assigned'` (별도 캡처 불필요).
- **`OpencodeAgentAdapter` / `CodexAgentAdapter`**:
- 각자의 세션 파일 디렉토리 및 프로세스/환경변수 기반 캡처 구현.
### 3. 파이썬 위임/이벤트 파이프라인 연동 (`publish_event.py` / `job_subscriber.py`)
- 에이전트가 온보딩 또는 첫 작업 완료 시 `publish_event.py --event completed`를 실행할 때:
- 해당 `agent_session`의 `own_id`가 미등록(`null`) 상태라면, 파이썬 코드 레벨에서 즉시 해당 어댑터의 `auto_pin_if_needed()`를 자동 트리거하도록 강제합니다.
---
## 기대 효과 (Benefits)
1. **완전한 자동화 (Zero Manual Step)**:
- 에이전트가 온보딩이나 첫 잡을 완료하는 순간 파이썬 레벨에서 즉시 `own_id`가 확정 등록되어, 수동 reconcile 호출이나 백그라운드 데몬 의존성이 사라집니다.
2. **단일 책임 원칙 및 캡슐화**:
- `create_session.sh` 등 쉘 스크립트에서 에이전트별 특성을 하드코딩할 필요 없이, 에이전트 고유의 세션 획득 규칙이 각 어댑터 내부로 캡슐화됩니다.
3. **신규 에이전트 확장성**:
- 향후 새로운 AI CLI 에이전트가 추가되더라도 `BaseAgentAdapter` 규격만 구현하면 세션 생성·추적 파이프라인에 안전하게 통합됩니다.
검토 — 반영 권장, 단 제안 3(트리거 지점)은 소비자 측으로 (병합 워크스페이스, 2026-09-26, v4.7.4 기준)
진단 검증 (코드 기준)
맞다: create_session.sh 는 agy·opencode·codex·hermes 의 own id 를 null / session_id_source: pending-discovery 로 기록하고(:556-587), 온보딩 주입 직후 reconcile.sh --once & 를 한 번만 부른다(:704). 그 시점에 Class A 는 대화 DB 가 아직 없으므로 첫 reconcile 은 헛돈다. "언제 ID 가 확정되는가" 는 어댑터가 아니라 create/resume/stop 의 셸 case 문(FRESH_SPAWN, <agent>_id_own 필드 선택)에 흩어져 있다 — #17 항목 4 기준으로 "bash 가 결정하는 자리" 다.
보정: "이후 아무도 reconcile 을 호출하지 않는다" 는 데몬이 없을 때만 참이다. create 가 띄우는 구독 데몬(reconcile.sh --subscribe)은 폴링 주기마다 --once 를 돌려 늦게라도 pin 한다(reconcile.sh:402). 이 간극이 실제로 걸리는 곳은 워크트리 creator 다: worktree_prepare.sh --creator-agent 는 설계상 MAM_NO_SUBSCRIBER_DAEMON=1 로 세션을 만들고(#21 A8), 제어 루트 데몬이 돌지 않으면 그 세션은 사람이 reconcile 을 돌릴 때까지 null 로 남는다. 이슈의 시나리오로 이것을 명시해 두는 게 좋겠다.
증거: 이 워크스페이스의 실제 행에서 agy 는 id 가 있는데 session_id_source 가 비어 있고, codex 는 id 가 있는데도 pending-discovery / session_id_verified: false 다. id 는 stop 의 캡처가 잡아 준 것이고 reconcile 의 pin 경로(_pin_and_verify_resume)를 거치지 않아 메타데이터가 어긋나 있다. 이 이슈에서 함께 정리할 만하다.
제안 1·2 (어댑터 인터페이스) — 채택 권장
id_lifecycle(pre_assigned / post_discovered), capture_active_session_id, auto_pin_if_needed 를 BaseAgentAdapter 에 두는 것은 #17 항목 4 의 경계 기준("결정을 내리는 코드는 Python")과 같은 방향이다. 셸의 Class A/B 분기를 어댑터 한 곳으로 모으고, 기존 discover()·verify_session()·reconcile 의 _pin_and_verify_resume 를 재사용하면 새 코드는 많지 않다.
capture_active_session_id 는 "결정론적" 이어야 한다는 요구가 핵심이다. reconcile 이 이미 갖고 있는 안전장치(세션 epoch 보다 새로운 후보가 둘 이상이면 pin 하지 않음, TUI 뷰포트 교차 확인, 다른 행이 이미 가진 id 배제)를 어댑터 메서드로 옮기되 약화시키지 않아야 한다.
제안 3 (트리거 지점) — 수정 필요
publish_event.py --event completed 안에서 auto_pin_if_needed() 를 부르자는 것인데, 이 스크립트는 에이전트 프로세스가 실행한다. 그러면 에이전트가 오케스트레이터 소유의 세션 등록부(YAML·SQLite)에 쓰게 되어 #15/#20 에서 굳힌 "제어 평면은 오케스트레이터 것" 경계가 흐려진다. 워크트리 creator 는 제어 평면 바인딩(#21)을 따로 풀어야 하고, 원격 러너(transport: remote)에서는 로컬 디스크가 없어 무의미하며, pin 의 검증 단계(TUI 뷰포트 확인)는 herdr 접근이 필요하다.
대안: 같은 completed 이벤트를 받는 쪽에서 트리거한다. 래퍼의 구독자(job_subscriber 의 terminal 처리) 또는 루프 러너의 wait_for_job 직후에, 그 세션의 own id 가 null 이면 어댑터의 auto_pin_if_needed 를 부른다. 오케스트레이터 측 프로세스라 바인딩·herdr·잠금이 제자리에 있고, 데몬 없이도 "첫 잡 완료 즉시 pin" 이라는 목표를 그대로 달성한다. #17 의 한 줄 기준으로도 이쪽이 맞다.
우선순위와 순서
크기 중간. create 경로와 어댑터를 건드리므로 #17 항목 4(셸 축소)와 같은 영역이다. 항목 4 PR 에 흡수하거나 그 직후에 하기를 권한다(두 번 고치지 않도록). #33/#30 보다 급하지는 않다.
## 검토 — 반영 권장, 단 제안 3(트리거 지점)은 소비자 측으로 (병합 워크스페이스, 2026-09-26, v4.7.4 기준)
### 진단 검증 (코드 기준)
- 맞다: `create_session.sh` 는 agy·opencode·codex·hermes 의 own id 를 `null` / `session_id_source: pending-discovery` 로 기록하고(`:556-587`), 온보딩 주입 직후 `reconcile.sh --once &` 를 한 번만 부른다(`:704`). 그 시점에 Class A 는 대화 DB 가 아직 없으므로 첫 reconcile 은 헛돈다. "언제 ID 가 확정되는가" 는 어댑터가 아니라 create/resume/stop 의 셸 `case` 문(`FRESH_SPAWN`, `<agent>_id_own` 필드 선택)에 흩어져 있다 — #17 항목 4 기준으로 "bash 가 결정하는 자리" 다.
- 보정: "이후 아무도 reconcile 을 호출하지 않는다" 는 **데몬이 없을 때만** 참이다. create 가 띄우는 구독 데몬(`reconcile.sh --subscribe`)은 폴링 주기마다 `--once` 를 돌려 늦게라도 pin 한다(`reconcile.sh:402`). 이 간극이 실제로 걸리는 곳은 **워크트리 creator** 다: `worktree_prepare.sh --creator-agent` 는 설계상 `MAM_NO_SUBSCRIBER_DAEMON=1` 로 세션을 만들고(#21 A8), 제어 루트 데몬이 돌지 않으면 그 세션은 사람이 reconcile 을 돌릴 때까지 `null` 로 남는다. 이슈의 시나리오로 이것을 명시해 두는 게 좋겠다.
- 증거: 이 워크스페이스의 실제 행에서 agy 는 id 가 있는데 `session_id_source` 가 비어 있고, codex 는 id 가 있는데도 `pending-discovery` / `session_id_verified: false` 다. id 는 stop 의 캡처가 잡아 준 것이고 reconcile 의 pin 경로(`_pin_and_verify_resume`)를 거치지 않아 메타데이터가 어긋나 있다. 이 이슈에서 함께 정리할 만하다.
### 제안 1·2 (어댑터 인터페이스) — 채택 권장
- `id_lifecycle`(`pre_assigned` / `post_discovered`), `capture_active_session_id`, `auto_pin_if_needed` 를 `BaseAgentAdapter` 에 두는 것은 #17 항목 4 의 경계 기준("결정을 내리는 코드는 Python")과 같은 방향이다. 셸의 Class A/B 분기를 어댑터 한 곳으로 모으고, 기존 `discover()`·`verify_session()`·reconcile 의 `_pin_and_verify_resume` 를 재사용하면 새 코드는 많지 않다.
- `capture_active_session_id` 는 "결정론적" 이어야 한다는 요구가 핵심이다. reconcile 이 이미 갖고 있는 안전장치(세션 epoch 보다 새로운 후보가 둘 이상이면 pin 하지 않음, TUI 뷰포트 교차 확인, 다른 행이 이미 가진 id 배제)를 어댑터 메서드로 옮기되 약화시키지 않아야 한다.
### 제안 3 (트리거 지점) — 수정 필요
- `publish_event.py --event completed` 안에서 `auto_pin_if_needed()` 를 부르자는 것인데, 이 스크립트는 **에이전트 프로세스**가 실행한다. 그러면 에이전트가 오케스트레이터 소유의 세션 등록부(YAML·SQLite)에 쓰게 되어 #15/#20 에서 굳힌 "제어 평면은 오케스트레이터 것" 경계가 흐려진다. 워크트리 creator 는 제어 평면 바인딩(#21)을 따로 풀어야 하고, 원격 러너(`transport: remote`)에서는 로컬 디스크가 없어 무의미하며, pin 의 검증 단계(TUI 뷰포트 확인)는 herdr 접근이 필요하다.
- 대안: 같은 `completed` 이벤트를 **받는 쪽**에서 트리거한다. 래퍼의 구독자(`job_subscriber` 의 terminal 처리) 또는 루프 러너의 `wait_for_job` 직후에, 그 세션의 own id 가 null 이면 어댑터의 `auto_pin_if_needed` 를 부른다. 오케스트레이터 측 프로세스라 바인딩·herdr·잠금이 제자리에 있고, 데몬 없이도 "첫 잡 완료 즉시 pin" 이라는 목표를 그대로 달성한다. `#17` 의 한 줄 기준으로도 이쪽이 맞다.
### 우선순위와 순서
- 크기 중간. create 경로와 어댑터를 건드리므로 #17 항목 4(셸 축소)와 같은 영역이다. 항목 4 PR 에 흡수하거나 그 직후에 하기를 권한다(두 번 고치지 않도록). #33/#30 보다 급하지는 않다.
PR #41 리뷰에서 (C) 항목으로 네 리뷰어에게 물었습니다. 결론(합의): 트리거는 completed 를 검증하는 수신 측입니다. 기본 진입점은 래퍼 구독자(워크트리 creator·MAM_NO_SUBSCRIBER_DAEMON=1 에서도 잡마다 래퍼 구독자가 completed 를 받음), 루프 wait_for_job 직후는 누락 복구 경로로 같은 처리기를 호출하되 job/token/attempt 단위로 중복 capture/pin 을 막습니다(grok 조건부 동의의 조건, codex 동의 근거). 어댑터는 id_lifecycle·capture·pin 사실/동작만 제공하고, 실행 시점과 제어 평면 쓰기 권한은 수신 측이 갖습니다. 에이전트의 publish_event 에 YAML/SQLite 쓰기를 넣지 않는 이유는 소유권(#15/#20/#21)과 워크트리 경로 문제입니다.
순서: #17 항목 4(래퍼 종결 처리의 Python 이전)에 흡수하거나 바로 다음에 진행합니다. 이 이슈는 그때 착수합니다.
PR #41 리뷰에서 (C) 항목으로 네 리뷰어에게 물었습니다. 결론(합의): 트리거는 `completed` 를 **검증하는 수신 측**입니다. 기본 진입점은 래퍼 구독자(워크트리 creator·`MAM_NO_SUBSCRIBER_DAEMON=1` 에서도 잡마다 래퍼 구독자가 `completed` 를 받음), 루프 `wait_for_job` 직후는 누락 복구 경로로 같은 처리기를 호출하되 job/token/attempt 단위로 중복 capture/pin 을 막습니다(grok 조건부 동의의 조건, codex 동의 근거). 어댑터는 `id_lifecycle`·capture·pin 사실/동작만 제공하고, 실행 시점과 제어 평면 쓰기 권한은 수신 측이 갖습니다. 에이전트의 `publish_event` 에 YAML/SQLite 쓰기를 넣지 않는 이유는 소유권(#15/#20/#21)과 워크트리 경로 문제입니다.
순서: #17 항목 4(래퍼 종결 처리의 Python 이전)에 흡수하거나 바로 다음에 진행합니다. 이 이슈는 그때 착수합니다.
추가 시나리오: agy 미등록 세션이 다른 작업 공간의 대화를 등록함 (#64 리뷰에서 발견, v4.9.8 기준)
미등록 상태가 길어지는 문제에 더해, 그 사이에 다른 작업 공간의 대화 ID가 등록될 수 있다는 것이 확인되었습니다. #64 리뷰에서 claude 리뷰어가 임시 HOME 에서 재현했고, #64 수정 전후 모두 같습니다. #64 범위 밖이라 여기에 붙입니다.
예시
proj-A: MAM 으로 agy 세션을 새로 띄우는 곳
proj-B: agy 로 작업 중인 곳 (대화 B123)
두 작업 공간의 대화 파일은 모두 호스트 공용 폴더 ~/.gemini/antigravity-cli/conversations/ 에 있습니다.
last_conversations.json 에는 "proj-B": "B123" 이 적혀 있습니다.
시점
일
1
proj-A 에서 agy 세션 생성. A 의 대화 파일은 아직 없고 agy_conversation_id_own: null
2
proj-B 의 agy 에 작업이 들어가 B123.db 가 갱신됨 (MAM 세션이 아니어도 됨)
3
누군가 reconcile 을 실행함 (모니터 데몬 주기, status.sh, 다른 create 의 마지막 단계)
4
drift C(agy)가 공용 폴더의 *.db 를 모두 훑음. 세션 시작 뒤 갱신된 후보는 B123 하나
5
A 의 행에 agy_conversation_id_own: B123 이 기록됨
원인 (코드)
reconcile.sh drift C(agy) (:786-832): last_conversations.json 을 쓰지 않고 대화 폴더 전체를 glob 해 verify_session_uuid(..., mode="discover") 로 거릅니다.
AgyAgentAdapter.verify_artifact (agy.py:60-72): last_conversations.json 에서 이 cwd 의 대화가 아니면(cache_match 거짓), 같은 cwd 의 형제가 이미 가진 ID 만 거절합니다. 다른 작업 공간에 매핑된 ID 는 거절하지 않습니다.
verify_tui_viewport (lib.sh:1981): A 의 pane 에 작업 공간 이름이 보이는지만 봅니다. 후보 대화가 누구 것인지는 보지 않으므로 이 경우를 막지 못합니다.
비교: 어댑터의 discover() (agy.py:165)는 last_conversations.json[workspace] 만 보므로 정확합니다. drift C 는 이것을 쓰지 않습니다.
영향
A 세션 resume: agy --conversation B123 으로 열려 A 의 context 가 B 의 대화로 바뀝니다.
A 세션 stop --purge-conversation: 기록된 ID 의 대화 파일이 삭제 대상이 되므로 B 의 대화가 지워질 수 있습니다 (코드 흐름상; 실제 삭제는 재현하지 않음).
onboard 직후: create --onboard 는 온보딩 완료 뒤 reconcile.sh --once 를 백그라운드로 한 번 부릅니다 (create_session.sh:748). 그사이 B 도 갱신되었으면 후보가 둘이라 C-ambiguous 로 등록하지 않습니다. B 가 계속 쓰이면 A 는 계속 미등록으로 남습니다.
이 이슈에 반영할 것 (제안)
agy 등록 경로 단일화: capture_active_session_id (agy)는 last_conversations.json[cwd] 를 근거로 합니다. drift C(agy)도 같은 어댑터 경로를 쓰고 폴더 전체 glob 은 없앱니다.
다른 작업 공간 ID 거절: verify_artifact 의 discover 모드는 last_conversations.json 이 그 ID 를 다른 cwd 에 매핑하면 거절합니다.
완료 조건:
위 예시(A 미등록, B 만 갱신)에서 A 행이 B123 을 등록하지 않는다.
A 의 온보딩이 끝나면 B 가 함께 갱신되어도 A 의 대화가 등록된다.
테스트 수정: tests/test_issue64_ro_connect.py::test_unpinned_agy_reconcile_leaves_other_workspace_clean_db_without_sidecars 는 현재 동작을 단언합니다 (:407, verify_session_uuid(ws_a, "agy", uuid_b, ...) is True). 수정 시 이 단언을 is False 로 바꾸거나 빼고, 사이드카 비교만 남깁니다.
## 추가 시나리오: agy 미등록 세션이 다른 작업 공간의 대화를 등록함 (#64 리뷰에서 발견, v4.9.8 기준)
미등록 상태가 길어지는 문제에 더해, 그 사이에 **다른 작업 공간의 대화 ID가 등록될 수 있다**는 것이 확인되었습니다. #64 리뷰에서 claude 리뷰어가 임시 HOME 에서 재현했고, #64 수정 전후 모두 같습니다. #64 범위 밖이라 여기에 붙입니다.
### 예시
- `proj-A`: MAM 으로 agy 세션을 새로 띄우는 곳
- `proj-B`: agy 로 작업 중인 곳 (대화 `B123`)
- 두 작업 공간의 대화 파일은 모두 호스트 공용 폴더 `~/.gemini/antigravity-cli/conversations/` 에 있습니다.
- `last_conversations.json` 에는 `"proj-B": "B123"` 이 적혀 있습니다.
| 시점 | 일 |
|---|---|
| 1 | proj-A 에서 agy 세션 생성. A 의 대화 파일은 아직 없고 `agy_conversation_id_own: null` |
| 2 | proj-B 의 agy 에 작업이 들어가 `B123.db` 가 갱신됨 (MAM 세션이 아니어도 됨) |
| 3 | 누군가 reconcile 을 실행함 (모니터 데몬 주기, `status.sh`, 다른 create 의 마지막 단계) |
| 4 | drift C(agy)가 공용 폴더의 `*.db` 를 모두 훑음. 세션 시작 뒤 갱신된 후보는 `B123` 하나 |
| 5 | A 의 행에 `agy_conversation_id_own: B123` 이 기록됨 |
### 원인 (코드)
- **`reconcile.sh` drift C(agy)** (`:786-832`): `last_conversations.json` 을 쓰지 않고 대화 폴더 전체를 glob 해 `verify_session_uuid(..., mode="discover")` 로 거릅니다.
- **`AgyAgentAdapter.verify_artifact`** (`agy.py:60-72`): `last_conversations.json` 에서 이 cwd 의 대화가 아니면(`cache_match` 거짓), 같은 cwd 의 형제가 이미 가진 ID 만 거절합니다. **다른 작업 공간에 매핑된 ID 는 거절하지 않습니다.**
- **`verify_tui_viewport`** (`lib.sh:1981`): A 의 pane 에 작업 공간 이름이 보이는지만 봅니다. 후보 대화가 누구 것인지는 보지 않으므로 이 경우를 막지 못합니다.
- **비교**: 어댑터의 `discover()` (`agy.py:165`)는 `last_conversations.json[workspace]` 만 보므로 정확합니다. drift C 는 이것을 쓰지 않습니다.
### 영향
- **A 세션 resume**: `agy --conversation B123` 으로 열려 A 의 context 가 B 의 대화로 바뀝니다.
- **A 세션 `stop --purge-conversation`**: 기록된 ID 의 대화 파일이 삭제 대상이 되므로 B 의 대화가 지워질 수 있습니다 (코드 흐름상; 실제 삭제는 재현하지 않음).
- **onboard 직후**: `create --onboard` 는 온보딩 완료 뒤 `reconcile.sh --once` 를 백그라운드로 한 번 부릅니다 (`create_session.sh:748`). 그사이 B 도 갱신되었으면 후보가 둘이라 `C-ambiguous` 로 등록하지 않습니다. B 가 계속 쓰이면 A 는 계속 미등록으로 남습니다.
### 이 이슈에 반영할 것 (제안)
- **agy 등록 경로 단일화**: `capture_active_session_id` (agy)는 `last_conversations.json[cwd]` 를 근거로 합니다. drift C(agy)도 같은 어댑터 경로를 쓰고 폴더 전체 glob 은 없앱니다.
- **다른 작업 공간 ID 거절**: `verify_artifact` 의 discover 모드는 `last_conversations.json` 이 그 ID 를 다른 cwd 에 매핑하면 거절합니다.
- **완료 조건**:
- 위 예시(A 미등록, B 만 갱신)에서 A 행이 `B123` 을 등록하지 않는다.
- A 의 온보딩이 끝나면 B 가 함께 갱신되어도 A 의 대화가 등록된다.
- **테스트 수정**: `tests/test_issue64_ro_connect.py::test_unpinned_agy_reconcile_leaves_other_workspace_clean_db_without_sidecars` 는 현재 동작을 단언합니다 (`:407`, `verify_session_uuid(ws_a, "agy", uuid_b, ...) is True`). 수정 시 이 단언을 `is False` 로 바꾸거나 빼고, 사이드카 비교만 남깁니다.
어댑터 규격: id_lifecycle(pre_assigned claude·grok / post_discovered 나머지), capture_active_session_id. reconcile drift C 도 같은 capture 를 쓴다.
agy:
last_conversations.json 이 다른 작업 공간에 매핑한 ID 는 거절한다.
대화 폴더 *.db glob 은 없앴다.
캐시가 가리키는 대화도 pane 프로세스(또는 자손)가 그 lock·DB 를 열고 있음을 lsof·ps 로 확인해야 등록한다.
등록 표시: 새 경로로 등록된 행은 session_id_source=discovered, session_id_verified=true.
완료 조건 (tests/test_issue40_auto_pin.py, 27개, base 에서 실패)
다른 작업 공간의 ID 를 등록하지 않는다. test_issue64_ro_connect.py 의 해당 단언도 이제 False 를 기대한다.
데몬 없이 첫 completed 뒤 agy·opencode·codex·hermes 가 등록된다.
같은 잡을 두 번 처리해도 등록은 한 번이다. 후보가 모호하면 등록하지 않고 이유를 남긴다.
claude·grok 행과 이미 ID 가 있는 행은 바뀌지 않는다.
검증 (macOS, Python 3.9)
--plan --all-reviewer 1회차 4/4 PASS.
전체 스위트 2068 passed, 56 skipped. 실패 3개는 스킬 블록 목록의 줄 번호였고 같은 PR 에서 갱신했다.
Linux 에서 확인할 것
tests/test_issue40_auto_pin.py 와 tests/test_issue64_ro_connect.py 가 모두 통과하는가.
lsof 가 설치되어 있는가. agy 자동 등록은 lsof·ps 에 기대므로 없으면 등록되지 않는다. 이 경우 행에 unverifiable-owner … lsof is unavailable 이유가 남는지 본다.
(가능하면) 실제 agy creator 하나를 --onboard 로 만들어, 온보딩 completed 뒤 agy_conversation_id_own 이 채워지는가. 새로 띄운 agy 의 첫 턴 직후는 macOS 에서도 실제로 확인하지 못했다.
남은 것 (비차단)
reconcile 은 ambiguous 외의 capture 실패 이유(unverifiable-owner·owner-mismatch·rejected)를 남기지 않는다.
id 가 빈 claude 행의 pre-assigned 보호 테스트가 없다.
실패 결과도 중복 마커에 남으므로, 같은 잡은 루프 복구에서 다시 시도하지 않는다. 다음 잡이나 reconcile 이 다시 시도한다.
stop 의 캡처 경로 정리는 범위 밖이다.
Linux 확인까지 이 이슈는 열어 둔다.
## PR #67 병합 (v4.9.9) — macOS 확인 완료
**구현**
- **등록 주체**: 잡의 completed 를 확인한 쪽이 세션 ID 를 등록한다.
- 진입점: 위임 래퍼, `create --onboard`, 루프 `wait_for_job` 복구.
- 잡·토큰·시도 하나는 한 번만 처리한다.
- `publish_event.py` 에는 쓰기를 넣지 않았다.
- remote·human 잡은 하지 않는다.
- **어댑터 규격**: `id_lifecycle`(`pre_assigned` claude·grok / `post_discovered` 나머지), `capture_active_session_id`. reconcile drift C 도 같은 capture 를 쓴다.
- **agy**:
- `last_conversations.json` 이 다른 작업 공간에 매핑한 ID 는 거절한다.
- 대화 폴더 `*.db` glob 은 없앴다.
- 캐시가 가리키는 대화도 pane 프로세스(또는 자손)가 그 lock·DB 를 열고 있음을 `lsof`·`ps` 로 확인해야 등록한다.
- **등록 표시**: 새 경로로 등록된 행은 `session_id_source=discovered`, `session_id_verified=true`.
**완료 조건** (`tests/test_issue40_auto_pin.py`, 27개, base 에서 실패)
1. 다른 작업 공간의 ID 를 등록하지 않는다. `test_issue64_ro_connect.py` 의 해당 단언도 이제 False 를 기대한다.
2. 데몬 없이 첫 completed 뒤 agy·opencode·codex·hermes 가 등록된다.
3. 같은 잡을 두 번 처리해도 등록은 한 번이다. 후보가 모호하면 등록하지 않고 이유를 남긴다.
4. claude·grok 행과 이미 ID 가 있는 행은 바뀌지 않는다.
**검증 (macOS, Python 3.9)**
- `--plan --all-reviewer` 1회차 4/4 PASS.
- 전체 스위트 2068 passed, 56 skipped. 실패 3개는 스킬 블록 목록의 줄 번호였고 같은 PR 에서 갱신했다.
**Linux 에서 확인할 것**
- `tests/test_issue40_auto_pin.py` 와 `tests/test_issue64_ro_connect.py` 가 모두 통과하는가.
- `lsof` 가 설치되어 있는가. agy 자동 등록은 `lsof`·`ps` 에 기대므로 없으면 등록되지 않는다. 이 경우 행에 `unverifiable-owner … lsof is unavailable` 이유가 남는지 본다.
- (가능하면) 실제 agy creator 하나를 `--onboard` 로 만들어, 온보딩 completed 뒤 `agy_conversation_id_own` 이 채워지는가. 새로 띄운 agy 의 첫 턴 직후는 macOS 에서도 실제로 확인하지 못했다.
**남은 것 (비차단)**
- reconcile 은 `ambiguous` 외의 capture 실패 이유(`unverifiable-owner`·`owner-mismatch`·`rejected`)를 남기지 않는다.
- id 가 빈 claude 행의 `pre-assigned` 보호 테스트가 없다.
- 실패 결과도 중복 마커에 남으므로, 같은 잡은 루프 복구에서 다시 시도하지 않는다. 다음 잡이나 reconcile 이 다시 시도한다.
- stop 의 캡처 경로 정리는 범위 밖이다.
Linux 확인까지 이 이슈는 열어 둔다.
그 대화의 파일을 행의 pane 프로세스(또는 그 자손)가 열고 있을 것 (lsof -t)
대상 파일은 presence/<uuid>.lock, conversations/<uuid>.db, -wal, -shm 이다. macOS 에서는 resume 된 agy 로만 확인했다. Linux 용 agy 가 실행 중에 이 파일들을 열어 두는지가 Linux 확인의 핵심이다.
1. lsof 설치 여부
command -v lsof 로 본다. 최소 설치나 컨테이너 이미지에는 없을 수 있다.
없으면 agy 는 자동 등록되지 않고, 행에 autopin unverifiable-owner … lsof is unavailable 이 남는다. opencode·codex·hermes 는 lsof 를 쓰지 않는다.
2. 실행 중인 agy 가 대화 파일을 열고 있는지 (실제 홈은 ls·lsof 로만 본다. sqlite 로 열지 않는다)
UUID=<agy 가 지금 쓰는 대화 uuid>
lsof -t ~/.gemini/antigravity-cli/presence/$UUID.lock \
~/.gemini/antigravity-cli/conversations/$UUID.db*
출력에 agy 프로세스(또는 그 자식)의 pid 가 나오면 등록 조건을 만족한다.
아무것도 안 나오면 Linux 에서는 agy 가 자동 등록되지 않는다(행에 no process holds … 가 남는다). 이 경우 결과를 여기 남겨 주면 확인 방법을 다시 정한다.
3. 종단 확인 (가능하면)
실제 agy creator 하나를 --onboard 로 만든다.
온보딩 completed 뒤 그 행의 agy_conversation_id_own 이 채워지고 session_id_source: discovered 인지 본다.
채워지지 않으면 last_visible_status 의 autopin … 이유를 함께 남긴다.
참고: Ubuntu 데스크톱의 lsof 경고
lsof: WARNING: can't stat() fuse.gvfsd-fuse … 같은 경고가 흔히 나온다.
아무도 열지 않은 경우 이 경고 때문에 이유가 no process holds … 대신 lsof is unavailable or failed 로 남을 수 있다. 둘 다 등록하지 않으므로 결과는 같다.
agy 가 파일을 열고 있으면 pid 가 나오므로 경고와 무관하게 등록된다.
## 보충: Linux 에서 agy 자동 등록 확인 방법
v4.9.9 의 agy 자동 등록은 두 가지를 확인해야 ID 를 기록한다.
- `last_conversations.json` 이 이 작업 공간에 매핑한 대화일 것
- 그 대화의 파일을 행의 pane 프로세스(또는 그 자손)가 **열고 있을 것** (`lsof -t`)
대상 파일은 `presence/<uuid>.lock`, `conversations/<uuid>.db`, `-wal`, `-shm` 이다. macOS 에서는 resume 된 agy 로만 확인했다. **Linux 용 agy 가 실행 중에 이 파일들을 열어 두는지**가 Linux 확인의 핵심이다.
**1. lsof 설치 여부**
- `command -v lsof` 로 본다. 최소 설치나 컨테이너 이미지에는 없을 수 있다.
- 없으면 agy 는 자동 등록되지 않고, 행에 `autopin unverifiable-owner … lsof is unavailable` 이 남는다. opencode·codex·hermes 는 lsof 를 쓰지 않는다.
**2. 실행 중인 agy 가 대화 파일을 열고 있는지** (실제 홈은 `ls`·`lsof` 로만 본다. sqlite 로 열지 않는다)
```bash
UUID=<agy 가 지금 쓰는 대화 uuid>
lsof -t ~/.gemini/antigravity-cli/presence/$UUID.lock \
~/.gemini/antigravity-cli/conversations/$UUID.db*
```
- 출력에 agy 프로세스(또는 그 자식)의 pid 가 나오면 등록 조건을 만족한다.
- 아무것도 안 나오면 Linux 에서는 agy 가 자동 등록되지 않는다(행에 `no process holds …` 가 남는다). 이 경우 결과를 여기 남겨 주면 확인 방법을 다시 정한다.
**3. 종단 확인 (가능하면)**
- 실제 agy creator 하나를 `--onboard` 로 만든다.
- 온보딩 completed 뒤 그 행의 `agy_conversation_id_own` 이 채워지고 `session_id_source: discovered` 인지 본다.
- 채워지지 않으면 `last_visible_status` 의 `autopin …` 이유를 함께 남긴다.
**참고: Ubuntu 데스크톱의 lsof 경고**
- `lsof: WARNING: can't stat() fuse.gvfsd-fuse …` 같은 경고가 흔히 나온다.
- 아무도 열지 않은 경우 이 경고 때문에 이유가 `no process holds …` 대신 `lsof is unavailable or failed` 로 남을 수 있다. 둘 다 등록하지 않으므로 결과는 같다.
- agy 가 파일을 열고 있으면 pid 가 나오므로 경고와 무관하게 등록된다.
어느 경로가 등록했는지: create 는 온보딩이 끝나면 reconcile.sh --once 를 백그라운드로 띄운 뒤 completed 처리기를 부른다. 이번에는 reconcile 쪽이 먼저 등록한 것으로 보인다.
그 잡의 autopin.json 결과가 already-set 이다.
두 경로는 같은 capture()(lsof 확인 포함)와 pin_row() 를 쓰므로 행에 남는 값도 같다.
completed 처리기만 따로 확인하려고 임시 HOME 과 임시 레지스트리로 handle_completed 를 돌렸다. 가짜 lsof·ps 는 쓰지 않았다.
pane 프로세스의 손자가 lock 을 연 상태에서 pinned 가 되었다(discovered, verified=true).
PATH 에서 lsof 를 빼면 등록되지 않았고, 행에 autopin unverifiable-owner (job …): lsof is unavailable or failed 가 남았다.
전체 스위트의 error 1건
test_workspace_scope.py::test_drift_b_cwd_gate 의 teardown 에서 #55 실제 홈 감시가 실패했다.
감시가 잡은 4개는 모두 위 종단 확인으로 생긴 agy 대화 파일이다(annotations·brain·conversations·presence). 종단 확인을 스위트와 동시에 돌려서, 그 시각에 돌던 테스트에 걸렸다.
그 파일만 다시 돌리면 2 passed 다. 테스트 쪽 문제는 아니다.
관찰 (비차단)
같은 작업 공간에 agy 가 둘이면 last_conversations.json 의 그 cwd 항목은 나중에 연 쪽으로 바뀐다. 먼저 있던 agy 는 이미 ID 가 등록되어 있어 영향이 없었다.
수정: 필요 없다. 브랜치와 PR 은 만들지 않았다.
## Linux 확인 결과 (v4.9.9, dc886ec)
**환경:** Ubuntu 26.04.1 LTS, Python 3.14.7, SQLite 3.53.4, lsof 4.99.4, agy 1.4.3. 테스트는 `XDG_*` 를 비우고 돌렸다.
| 항목 | 결과 |
| --- | --- |
| `test_issue40_auto_pin.py`·`test_issue64_ro_connect.py` | 70 passed (27 + 43) |
| 전체 스위트 | 2107 passed / 20 skipped / 1 error. error 는 아래처럼 확인 방법 때문에 생긴 것이고, 그 파일을 다시 돌리면 통과한다. |
| lsof | 설치되어 있다. 이 호스트에서는 Ubuntu 데스크톱의 fuse 경고가 나오지 않았다. |
| 실행 중인 agy 가 대화 파일을 여는가 | 연다. pid 가 pane 프로세스와 같다. |
| 종단 확인 (`create --onboard`) | **등록됨.** `session_id_source: discovered`, `session_id_verified: true` |
**1. lsof 동작 (임시 파일)**
- 열린 파일: rc 0, pid 출력.
- 아무도 열지 않은 파일: rc 1, 출력 없음, stderr 비어 있음.
- 열린 파일과 안 열린 파일을 함께 주면: rc 1 이지만 pid 는 출력된다.
- 없는 경로: rc 1, stderr 에 `status error`. 코드가 미리 걸러내므로 영향이 없다.
- 즉 `_holder_pids` 의 가정과 맞다.
**2. 실행 중인 agy 가 여는 파일** (`lsof` 로만 봤다. DB 는 열지 않았다)
- resume 된 agy: `presence/<uuid>.lock` 만 연다(쓰기 잠금). DB 는 열어 두지 않는다.
- 새로 띄운 agy: `presence/<uuid>.lock`·`<uuid>.db`·`-wal`·`-shm` 을 모두 연다.
- 두 경우 모두 연 프로세스가 행의 `pane.pid` 와 같다. 그래서 소유 확인이 첫 단계에서 통과한다. 대상에 lock 이 들어 있어 resume 된 agy 도 문제없다.
**3. 종단 확인**
- 실제 agy creator 하나를 `--onboard` 로 만들었다. 모니터 데몬은 띄우지 않았다(`MAM_NO_SUBSCRIBER_DAEMON=1`).
- 온보딩은 36초 만에 completed 로 끝났다.
- 행의 `agy_conversation_id_own` 은 그때 새로 생긴 대화 파일, 그리고 `last_conversations.json` 이 이 작업 공간에 매핑한 ID 와 같다.
- `session_id_source: discovered`, `session_id_verified: true`, `last_visible_status: resume_verified`
- **어느 경로가 등록했는지:** create 는 온보딩이 끝나면 `reconcile.sh --once` 를 백그라운드로 띄운 뒤 completed 처리기를 부른다. 이번에는 reconcile 쪽이 먼저 등록한 것으로 보인다.
- 그 잡의 `autopin.json` 결과가 `already-set` 이다.
- 두 경로는 같은 `capture()`(lsof 확인 포함)와 `pin_row()` 를 쓰므로 행에 남는 값도 같다.
- completed 처리기만 따로 확인하려고 임시 HOME 과 임시 레지스트리로 `handle_completed` 를 돌렸다. 가짜 lsof·ps 는 쓰지 않았다.
- pane 프로세스의 손자가 lock 을 연 상태에서 `pinned` 가 되었다(`discovered`, `verified=true`).
- PATH 에서 lsof 를 빼면 등록되지 않았고, 행에 `autopin unverifiable-owner (job …): lsof is unavailable or failed` 가 남았다.
**전체 스위트의 error 1건**
- `test_workspace_scope.py::test_drift_b_cwd_gate` 의 teardown 에서 #55 실제 홈 감시가 실패했다.
- 감시가 잡은 4개는 모두 위 종단 확인으로 생긴 agy 대화 파일이다(`annotations`·`brain`·`conversations`·`presence`). 종단 확인을 스위트와 동시에 돌려서, 그 시각에 돌던 테스트에 걸렸다.
- 그 파일만 다시 돌리면 2 passed 다. 테스트 쪽 문제는 아니다.
**관찰 (비차단)**
- 같은 작업 공간에 agy 가 둘이면 `last_conversations.json` 의 그 cwd 항목은 나중에 연 쪽으로 바뀐다. 먼저 있던 agy 는 이미 ID 가 등록되어 있어 영향이 없었다.
**수정:** 필요 없다. 브랜치와 PR 은 만들지 않았다.
스위트 error 1건: 종단 확인을 스위트와 동시에 돌려 #55 실제 홈 감시에 걸린 것이다. 그 파일을 다시 돌리면 통과하므로 결함으로 보지 않는다.
lsof: 결과 코드와 출력이 _holder_pids 의 가정(열린 파일은 pid, 아무도 안 연 파일은 rc 1 에 빈 stderr)과 맞는다.
agy 가 여는 파일: 실행 중인 agy 는 Linux 에서도 대화 파일을 연다. resume 된 agy 는 presence lock 만, 새로 띄운 agy 는 lock 과 DB·사이드카를 연다. 연 프로세스가 pane pid 와 같으므로 소유 확인이 통과한다.
종단 확인: 데몬 없이 create --onboard 뒤 대화 ID 가 discovered·verified=true 로 등록됐다. 실제 lsof 로 손자 프로세스 경로와 lsof 없음 경로도 확인됐다.
남은 것
v4.9.9 에 적은 비차단 항목(reconcile 의 실패 이유 기록, 빈 claude 행의 pre-assigned 보호 테스트)은 필요할 때 따로 다룬다.
같은 작업 공간의 agy 두 개는 #63 에서 다룬다. 관찰한 대로 last_conversations.json 은 나중에 연 쪽으로 바뀐다. 그래서 먼저 뜬 agy 가 등록되기 전에 두 번째가 열리면, 먼저 뜬 agy 는 자동 등록되지 않는다(owner-mismatch, 오등록은 없음). stop·purge 의 대화 ID 조회(find_workspace_uuid)는 아직 #40 의 확인을 거치지 않는다.
Linux 확인 결과를 확인했다. macOS·Linux 모두 완료 조건 1~4 를 만족하므로 닫는다.
- **테스트**: 새 회귀 27개와 #64 테스트 43개가 통과했다.
- **스위트 error 1건**: 종단 확인을 스위트와 동시에 돌려 #55 실제 홈 감시에 걸린 것이다. 그 파일을 다시 돌리면 통과하므로 결함으로 보지 않는다.
- **lsof**: 결과 코드와 출력이 `_holder_pids` 의 가정(열린 파일은 pid, 아무도 안 연 파일은 rc 1 에 빈 stderr)과 맞는다.
- **agy 가 여는 파일**: 실행 중인 agy 는 Linux 에서도 대화 파일을 연다. resume 된 agy 는 presence lock 만, 새로 띄운 agy 는 lock 과 DB·사이드카를 연다. 연 프로세스가 pane pid 와 같으므로 소유 확인이 통과한다.
- **종단 확인**: 데몬 없이 `create --onboard` 뒤 대화 ID 가 `discovered`·`verified=true` 로 등록됐다. 실제 lsof 로 손자 프로세스 경로와 lsof 없음 경로도 확인됐다.
**남은 것**
- v4.9.9 에 적은 비차단 항목(reconcile 의 실패 이유 기록, 빈 claude 행의 `pre-assigned` 보호 테스트)은 필요할 때 따로 다룬다.
- 같은 작업 공간의 agy 두 개는 #63 에서 다룬다. 관찰한 대로 `last_conversations.json` 은 나중에 연 쪽으로 바뀐다. 그래서 먼저 뜬 agy 가 등록되기 전에 두 번째가 열리면, 먼저 뜬 agy 는 자동 등록되지 않는다(`owner-mismatch`, 오등록은 없음). stop·purge 의 대화 ID 조회(`find_workspace_uuid`)는 아직 #40 의 확인을 거치지 않는다.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
배경 및 문제점 (Problem Statement)
MAM(multi-agent-mux) 아키텍처에서는 에이전트 CLI별로 세션 ID(대화 UUID)가 결정되는 시점과 방식이 상이합니다:
claude,grok):--session-id <uuid>플래그로 UUID를 주입할 수 있어,create_session.sh실행 즉시agent-sessions.yaml에assigned상태로 기록됩니다.agy,opencode,codex,hermes):.db또는 세션 파일)을 디스크에 생성합니다.create_session.sh시점에는own_id가null(session_id_source: pending-discovery)로 등록됩니다.현재 구조의 한계와 누락 발생 원인
create_session.sh의--onboard흐름에서는 에이전트가started이벤트를 쏘자마자 백그라운드로reconcile.sh --once &를 1회 실행합니다.agy등)는started시점에는 이제 막 프롬프트를 수신한 상태라 디스크에 대화 DB가 아직 생성되지 않았습니다. 따라서 이 최초 reconcile은 ID를 발견하지 못하고 종료됩니다.completed이벤트를 발행하여 실제 대화 DB가 디스크에 생성된 시점에는 아무도 자동 Reconcile을 호출하지 않습니다.reconcile.sh --subscribe)이 실행 중이지 않은 환경에서는 사람이 수동으로reconcile.sh를 실행할 때까지agy_conversation_id_own: null상태로 방치되어, 추후resume실패나 세션 모호성(drift)을 유발합니다.제안 사항 (Proposed Design)
에이전트마다 생성 및 ID 획득 방식이 다르므로, 이 책임을 외부 쉘 스크립트나 운영자의 수동 개입에 맡기지 않고 에이전트 추상 어댑터 인터페이스(
BaseAgentAdapter)의 표준 규격으로 공식 정의할 것을 제안합니다.1.
BaseAgentAdapter추상 인터페이스 확장 (lib_py/agents/base.py)2. 어댑터별 구현 분리
AgyAgentAdapter(lib_py/agents/adapters/agy.py):id_lifecycle = 'post_discovered'capture_active_session_id:~/.gemini/antigravity-cli/cache/last_conversations.json및 가장 최근 생성된.db매핑을 통해 확정 추출.ClaudeAgentAdapter(lib_py/agents/adapters/claude.py):id_lifecycle = 'pre_assigned'(별도 캡처 불필요).OpencodeAgentAdapter/CodexAgentAdapter:3. 파이썬 위임/이벤트 파이프라인 연동 (
publish_event.py/job_subscriber.py)publish_event.py --event completed를 실행할 때:agent_session의own_id가 미등록(null) 상태라면, 파이썬 코드 레벨에서 즉시 해당 어댑터의auto_pin_if_needed()를 자동 트리거하도록 강제합니다.기대 효과 (Benefits)
own_id가 확정 등록되어, 수동 reconcile 호출이나 백그라운드 데몬 의존성이 사라집니다.create_session.sh등 쉘 스크립트에서 에이전트별 특성을 하드코딩할 필요 없이, 에이전트 고유의 세션 획득 규칙이 각 어댑터 내부로 캡슐화됩니다.BaseAgentAdapter규격만 구현하면 세션 생성·추적 파이프라인에 안전하게 통합됩니다.검토 — 반영 권장, 단 제안 3(트리거 지점)은 소비자 측으로 (병합 워크스페이스, 2026-09-26, v4.7.4 기준)
진단 검증 (코드 기준)
create_session.sh는 agy·opencode·codex·hermes 의 own id 를null/session_id_source: pending-discovery로 기록하고(:556-587), 온보딩 주입 직후reconcile.sh --once &를 한 번만 부른다(:704). 그 시점에 Class A 는 대화 DB 가 아직 없으므로 첫 reconcile 은 헛돈다. "언제 ID 가 확정되는가" 는 어댑터가 아니라 create/resume/stop 의 셸case문(FRESH_SPAWN,<agent>_id_own필드 선택)에 흩어져 있다 — #17 항목 4 기준으로 "bash 가 결정하는 자리" 다.reconcile.sh --subscribe)은 폴링 주기마다--once를 돌려 늦게라도 pin 한다(reconcile.sh:402). 이 간극이 실제로 걸리는 곳은 워크트리 creator 다:worktree_prepare.sh --creator-agent는 설계상MAM_NO_SUBSCRIBER_DAEMON=1로 세션을 만들고(#21 A8), 제어 루트 데몬이 돌지 않으면 그 세션은 사람이 reconcile 을 돌릴 때까지null로 남는다. 이슈의 시나리오로 이것을 명시해 두는 게 좋겠다.session_id_source가 비어 있고, codex 는 id 가 있는데도pending-discovery/session_id_verified: false다. id 는 stop 의 캡처가 잡아 준 것이고 reconcile 의 pin 경로(_pin_and_verify_resume)를 거치지 않아 메타데이터가 어긋나 있다. 이 이슈에서 함께 정리할 만하다.제안 1·2 (어댑터 인터페이스) — 채택 권장
id_lifecycle(pre_assigned/post_discovered),capture_active_session_id,auto_pin_if_needed를BaseAgentAdapter에 두는 것은 #17 항목 4 의 경계 기준("결정을 내리는 코드는 Python")과 같은 방향이다. 셸의 Class A/B 분기를 어댑터 한 곳으로 모으고, 기존discover()·verify_session()·reconcile 의_pin_and_verify_resume를 재사용하면 새 코드는 많지 않다.capture_active_session_id는 "결정론적" 이어야 한다는 요구가 핵심이다. reconcile 이 이미 갖고 있는 안전장치(세션 epoch 보다 새로운 후보가 둘 이상이면 pin 하지 않음, TUI 뷰포트 교차 확인, 다른 행이 이미 가진 id 배제)를 어댑터 메서드로 옮기되 약화시키지 않아야 한다.제안 3 (트리거 지점) — 수정 필요
publish_event.py --event completed안에서auto_pin_if_needed()를 부르자는 것인데, 이 스크립트는 에이전트 프로세스가 실행한다. 그러면 에이전트가 오케스트레이터 소유의 세션 등록부(YAML·SQLite)에 쓰게 되어 #15/#20 에서 굳힌 "제어 평면은 오케스트레이터 것" 경계가 흐려진다. 워크트리 creator 는 제어 평면 바인딩(#21)을 따로 풀어야 하고, 원격 러너(transport: remote)에서는 로컬 디스크가 없어 무의미하며, pin 의 검증 단계(TUI 뷰포트 확인)는 herdr 접근이 필요하다.completed이벤트를 받는 쪽에서 트리거한다. 래퍼의 구독자(job_subscriber의 terminal 처리) 또는 루프 러너의wait_for_job직후에, 그 세션의 own id 가 null 이면 어댑터의auto_pin_if_needed를 부른다. 오케스트레이터 측 프로세스라 바인딩·herdr·잠금이 제자리에 있고, 데몬 없이도 "첫 잡 완료 즉시 pin" 이라는 목표를 그대로 달성한다.#17의 한 줄 기준으로도 이쪽이 맞다.우선순위와 순서
PR #41 리뷰에서 (C) 항목으로 네 리뷰어에게 물었습니다. 결론(합의): 트리거는
completed를 검증하는 수신 측입니다. 기본 진입점은 래퍼 구독자(워크트리 creator·MAM_NO_SUBSCRIBER_DAEMON=1에서도 잡마다 래퍼 구독자가completed를 받음), 루프wait_for_job직후는 누락 복구 경로로 같은 처리기를 호출하되 job/token/attempt 단위로 중복 capture/pin 을 막습니다(grok 조건부 동의의 조건, codex 동의 근거). 어댑터는id_lifecycle·capture·pin 사실/동작만 제공하고, 실행 시점과 제어 평면 쓰기 권한은 수신 측이 갖습니다. 에이전트의publish_event에 YAML/SQLite 쓰기를 넣지 않는 이유는 소유권(#15/#20/#21)과 워크트리 경로 문제입니다.순서: #17 항목 4(래퍼 종결 처리의 Python 이전)에 흡수하거나 바로 다음에 진행합니다. 이 이슈는 그때 착수합니다.
추가 시나리오: agy 미등록 세션이 다른 작업 공간의 대화를 등록함 (#64 리뷰에서 발견, v4.9.8 기준)
미등록 상태가 길어지는 문제에 더해, 그 사이에 다른 작업 공간의 대화 ID가 등록될 수 있다는 것이 확인되었습니다. #64 리뷰에서 claude 리뷰어가 임시 HOME 에서 재현했고, #64 수정 전후 모두 같습니다. #64 범위 밖이라 여기에 붙입니다.
예시
proj-A: MAM 으로 agy 세션을 새로 띄우는 곳proj-B: agy 로 작업 중인 곳 (대화B123)~/.gemini/antigravity-cli/conversations/에 있습니다.last_conversations.json에는"proj-B": "B123"이 적혀 있습니다.agy_conversation_id_own: nullB123.db가 갱신됨 (MAM 세션이 아니어도 됨)status.sh, 다른 create 의 마지막 단계)*.db를 모두 훑음. 세션 시작 뒤 갱신된 후보는B123하나agy_conversation_id_own: B123이 기록됨원인 (코드)
reconcile.shdrift C(agy) (:786-832):last_conversations.json을 쓰지 않고 대화 폴더 전체를 glob 해verify_session_uuid(..., mode="discover")로 거릅니다.AgyAgentAdapter.verify_artifact(agy.py:60-72):last_conversations.json에서 이 cwd 의 대화가 아니면(cache_match거짓), 같은 cwd 의 형제가 이미 가진 ID 만 거절합니다. 다른 작업 공간에 매핑된 ID 는 거절하지 않습니다.verify_tui_viewport(lib.sh:1981): A 의 pane 에 작업 공간 이름이 보이는지만 봅니다. 후보 대화가 누구 것인지는 보지 않으므로 이 경우를 막지 못합니다.discover()(agy.py:165)는last_conversations.json[workspace]만 보므로 정확합니다. drift C 는 이것을 쓰지 않습니다.영향
agy --conversation B123으로 열려 A 의 context 가 B 의 대화로 바뀝니다.stop --purge-conversation: 기록된 ID 의 대화 파일이 삭제 대상이 되므로 B 의 대화가 지워질 수 있습니다 (코드 흐름상; 실제 삭제는 재현하지 않음).create --onboard는 온보딩 완료 뒤reconcile.sh --once를 백그라운드로 한 번 부릅니다 (create_session.sh:748). 그사이 B 도 갱신되었으면 후보가 둘이라C-ambiguous로 등록하지 않습니다. B 가 계속 쓰이면 A 는 계속 미등록으로 남습니다.이 이슈에 반영할 것 (제안)
capture_active_session_id(agy)는last_conversations.json[cwd]를 근거로 합니다. drift C(agy)도 같은 어댑터 경로를 쓰고 폴더 전체 glob 은 없앱니다.verify_artifact의 discover 모드는last_conversations.json이 그 ID 를 다른 cwd 에 매핑하면 거절합니다.B123을 등록하지 않는다.tests/test_issue64_ro_connect.py::test_unpinned_agy_reconcile_leaves_other_workspace_clean_db_without_sidecars는 현재 동작을 단언합니다 (:407,verify_session_uuid(ws_a, "agy", uuid_b, ...) is True). 수정 시 이 단언을is False로 바꾸거나 빼고, 사이드카 비교만 남깁니다.PR #67 병합 (v4.9.9) — macOS 확인 완료
구현
create --onboard, 루프wait_for_job복구.publish_event.py에는 쓰기를 넣지 않았다.id_lifecycle(pre_assignedclaude·grok /post_discovered나머지),capture_active_session_id. reconcile drift C 도 같은 capture 를 쓴다.last_conversations.json이 다른 작업 공간에 매핑한 ID 는 거절한다.*.dbglob 은 없앴다.lsof·ps로 확인해야 등록한다.session_id_source=discovered,session_id_verified=true.완료 조건 (
tests/test_issue40_auto_pin.py, 27개, base 에서 실패)test_issue64_ro_connect.py의 해당 단언도 이제 False 를 기대한다.검증 (macOS, Python 3.9)
--plan --all-reviewer1회차 4/4 PASS.Linux 에서 확인할 것
tests/test_issue40_auto_pin.py와tests/test_issue64_ro_connect.py가 모두 통과하는가.lsof가 설치되어 있는가. agy 자동 등록은lsof·ps에 기대므로 없으면 등록되지 않는다. 이 경우 행에unverifiable-owner … lsof is unavailable이유가 남는지 본다.--onboard로 만들어, 온보딩 completed 뒤agy_conversation_id_own이 채워지는가. 새로 띄운 agy 의 첫 턴 직후는 macOS 에서도 실제로 확인하지 못했다.남은 것 (비차단)
ambiguous외의 capture 실패 이유(unverifiable-owner·owner-mismatch·rejected)를 남기지 않는다.pre-assigned보호 테스트가 없다.Linux 확인까지 이 이슈는 열어 둔다.
보충: Linux 에서 agy 자동 등록 확인 방법
v4.9.9 의 agy 자동 등록은 두 가지를 확인해야 ID 를 기록한다.
last_conversations.json이 이 작업 공간에 매핑한 대화일 것lsof -t)대상 파일은
presence/<uuid>.lock,conversations/<uuid>.db,-wal,-shm이다. macOS 에서는 resume 된 agy 로만 확인했다. Linux 용 agy 가 실행 중에 이 파일들을 열어 두는지가 Linux 확인의 핵심이다.1. lsof 설치 여부
command -v lsof로 본다. 최소 설치나 컨테이너 이미지에는 없을 수 있다.autopin unverifiable-owner … lsof is unavailable이 남는다. opencode·codex·hermes 는 lsof 를 쓰지 않는다.2. 실행 중인 agy 가 대화 파일을 열고 있는지 (실제 홈은
ls·lsof로만 본다. sqlite 로 열지 않는다)no process holds …가 남는다). 이 경우 결과를 여기 남겨 주면 확인 방법을 다시 정한다.3. 종단 확인 (가능하면)
--onboard로 만든다.agy_conversation_id_own이 채워지고session_id_source: discovered인지 본다.last_visible_status의autopin …이유를 함께 남긴다.참고: Ubuntu 데스크톱의 lsof 경고
lsof: WARNING: can't stat() fuse.gvfsd-fuse …같은 경고가 흔히 나온다.no process holds …대신lsof is unavailable or failed로 남을 수 있다. 둘 다 등록하지 않으므로 결과는 같다.Linux 확인 결과 (v4.9.9,
dc886ec)환경: Ubuntu 26.04.1 LTS, Python 3.14.7, SQLite 3.53.4, lsof 4.99.4, agy 1.4.3. 테스트는
XDG_*를 비우고 돌렸다.test_issue40_auto_pin.py·test_issue64_ro_connect.pycreate --onboard)session_id_source: discovered,session_id_verified: true1. lsof 동작 (임시 파일)
status error. 코드가 미리 걸러내므로 영향이 없다._holder_pids의 가정과 맞다.2. 실행 중인 agy 가 여는 파일 (
lsof로만 봤다. DB 는 열지 않았다)presence/<uuid>.lock만 연다(쓰기 잠금). DB 는 열어 두지 않는다.presence/<uuid>.lock·<uuid>.db·-wal·-shm을 모두 연다.pane.pid와 같다. 그래서 소유 확인이 첫 단계에서 통과한다. 대상에 lock 이 들어 있어 resume 된 agy 도 문제없다.3. 종단 확인
--onboard로 만들었다. 모니터 데몬은 띄우지 않았다(MAM_NO_SUBSCRIBER_DAEMON=1).agy_conversation_id_own은 그때 새로 생긴 대화 파일, 그리고last_conversations.json이 이 작업 공간에 매핑한 ID 와 같다.session_id_source: discovered,session_id_verified: true,last_visible_status: resume_verifiedreconcile.sh --once를 백그라운드로 띄운 뒤 completed 처리기를 부른다. 이번에는 reconcile 쪽이 먼저 등록한 것으로 보인다.autopin.json결과가already-set이다.capture()(lsof 확인 포함)와pin_row()를 쓰므로 행에 남는 값도 같다.handle_completed를 돌렸다. 가짜 lsof·ps 는 쓰지 않았다.pinned가 되었다(discovered,verified=true).autopin unverifiable-owner (job …): lsof is unavailable or failed가 남았다.전체 스위트의 error 1건
test_workspace_scope.py::test_drift_b_cwd_gate의 teardown 에서 #55 실제 홈 감시가 실패했다.annotations·brain·conversations·presence). 종단 확인을 스위트와 동시에 돌려서, 그 시각에 돌던 테스트에 걸렸다.관찰 (비차단)
last_conversations.json의 그 cwd 항목은 나중에 연 쪽으로 바뀐다. 먼저 있던 agy 는 이미 ID 가 등록되어 있어 영향이 없었다.수정: 필요 없다. 브랜치와 PR 은 만들지 않았다.
Linux 확인 결과를 확인했다. macOS·Linux 모두 완료 조건 1~4 를 만족하므로 닫는다.
_holder_pids의 가정(열린 파일은 pid, 아무도 안 연 파일은 rc 1 에 빈 stderr)과 맞는다.create --onboard뒤 대화 ID 가discovered·verified=true로 등록됐다. 실제 lsof 로 손자 프로세스 경로와 lsof 없음 경로도 확인됐다.남은 것
pre-assigned보호 테스트)은 필요할 때 따로 다룬다.last_conversations.json은 나중에 연 쪽으로 바뀐다. 그래서 먼저 뜬 agy 가 등록되기 전에 두 번째가 열리면, 먼저 뜬 agy 는 자동 등록되지 않는다(owner-mismatch, 오등록은 없음). stop·purge 의 대화 ID 조회(find_workspace_uuid)는 아직 #40 의 확인을 거치지 않는다.