Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,14 @@ GitHub에 `vX.Y.Z` 태그를 올리면 Release 워크플로가 Apple Silicon·In

핵심은 데몬이에요. 데스크톱 앱과 `crew` CLI가 Unix 소켓으로 JSON 요청을 보내면, 데몬이 에이전트 프로세스를 실행해요.

### 알려진 한계: 데몬 두 개가 동시에 뜰 수 있어요

데몬은 부팅할 때 소켓이 살아 있는지 물어보고, 아니면 남은 소켓 파일을 지우고, 로스터를 열고, 마지막에 `bind` 해요. 두 데몬이 같은 순간에 시작하면 둘 다 "소켓 없음"을 보기 때문에 둘 다 로스터를 열고, `bind` 에서 한쪽이 져요. 검사와 `bind` 가 하나의 연산이 아니라서 생기는 틈이고, 크래시 복구를 위해 남은 소켓을 지워야 하는 한 없앨 수 없어요.

측정하면 피해는 거의 없어요. 실제로 쓰는 봇(`grok`/`claude`/`codex`)은 헤드리스라 부팅이 CLI 프로세스를 띄우지 않고, PTY 봇이라면 진 데몬의 자식들은 그 데몬과 함께 죽어요. 아주 드물게 둘 다 `bind` 에 성공하는 경우는 로스터가 빈 상태에서만 재현되는데, 그때 닿을 수 없게 된 데몬은 아무것도 들고 있지 않아요.

봇을 여러 개 쓰면서 `crew` 명령과 데스크톱 앱을 같은 순간에 처음 실행하는 게 유일한 트리거예요. 한쪽이 뜬 뒤에 다른 쪽을 실행하면 생기지 않아요.

---

## 라이선스
Expand Down
17 changes: 16 additions & 1 deletion crates/crew/src/daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -897,6 +897,21 @@ pub async fn run() -> anyhow::Result<()> {
}
paths::remove_stale_socket();

// `serve` opens the roster before it binds, so by the time it finds out it
// lost a start race — two daemons can both pass the check above while
// neither has bound — every agent is already open. Returning that error
// straight from here left all of them running, because the teardown lived
// at the far end of the accept loop.
//
// Only the agents. The socket, pid and version name whoever is serving,
// and on this path that is somebody else: clearing them here would unlink
// the live socket of the daemon that won.
let served = serve().await;
shutdown_agents();
served
}

async fn serve() -> anyhow::Result<()> {
crate::transcript::set_seal_hook(on_assistant_sealed);

let cfg = Config::load()?;
Expand Down Expand Up @@ -983,7 +998,7 @@ pub async fn run() -> anyhow::Result<()> {
}
}

shutdown_agents();
// A clean stop, so these are ours to clear. `run` kills the agents.
paths::remove_stale_socket();
paths::remove_pid();
let _ = events().send(Event::Shutdown);
Expand Down
Loading