Problem
Spawning a session (orchestrator or worker) fails with a 409 BRANCH_CHECKED_OUT_ELSEWHERE whenever the target branch is still checked out in a stale/orphaned worktree left behind by a session that no longer exists in the daemon. This happens frequently in practice.
Observed error:
HTTP/1.1 409 Conflict
{"error":"conflict","code":"BRANCH_CHECKED_OUT_ELSEWHERE",
"message":"spawn ao-agents-1: workspace: branch is already checked out in another worktree:
\"ao/ao-agents-orchestrator\" is checked out at
\".../data/worktrees/ao-agents/orchestrator/ao-agents-orchestrator\""}
In this repro the daemon had no ao-agents session record (active or terminated), yet git worktree list still showed ao/ao-agents-orchestrator checked out at the data-dir worktree path. Because no session owned it, ao session cleanup could not reclaim it, so every re-spawn kept hitting the 409. The only way out today is manual git worktree remove + git branch -D against the project repo.
Root cause
addWorktree refuses early when the branch is already checked out at a different path:
backend/internal/adapters/workspace/gitworktree/workspace.go:250-252 returns ErrBranchCheckedOutElsewhere.
backend/internal/service/session/service.go:456 maps it to the 409 BRANCH_CHECKED_OUT_ELSEWHERE envelope.
backend/internal/ports/outbound.go:117 defines the sentinel.
The check is correct as a guard, but it makes no distinction between a worktree owned by a live session (a genuine conflict) and an orphaned worktree whose owning session is gone (should be reclaimable).
Desired behavior
When spawn finds the target branch checked out in another worktree, decide based on whether a live session owns that worktree and whether it is dirty. Applies to both worker and orchestrator spawns (the logic lives in shared addWorktree):
| Conflicting worktree |
Behavior |
| Owned by a live session |
Keep current 409 BRANCH_CHECKED_OUT_ELSEWHERE (real conflict, never touch it). |
| Orphan (no live session), clean |
Reclaim it: git worktree remove + prune, then create the new worktree at the canonical path. |
| Orphan (no live session), dirty |
Adopt it in place: reuse the existing worktree for the new session. Never destroy uncommitted work. |
This respects the existing hard rule in AGENTS.md ("Do not force-delete dirty registered worktrees") while making the frequent (clean-orphan) case just work, and never losing work in the dirty case.
Acceptance criteria
- Re-spawning a session/orchestrator whose previous worktree was orphaned and clean succeeds (no manual
git worktree remove needed); the stale worktree is removed and a fresh one created.
- Re-spawning when the orphaned worktree is dirty succeeds by adopting the existing worktree; uncommitted changes are preserved and the new session points at that path.
- A worktree owned by a live session still returns
409 BRANCH_CHECKED_OUT_ELSEWHERE (no regression, no stealing a live session's worktree).
- "Orphan" is determined by the absence of a live (non-terminated) session owning the worktree, not merely by a failed runtime probe (see the existing rule: do not treat a failed/unknown probe as proof a session is dead).
- Tests cover all three branches in the table, mirroring
backend/internal/adapters/workspace/gitworktree/workspace_test.go and backend/internal/service/session/service_test.go.
Notes / open questions
- Worktree path naming: an adopted dirty orphan may sit at a non-canonical path. Confirm the session record persists the actual workspace path so adoption is consistent with how restore/list resolve paths.
- Consider whether session deletion/reaping should also clean up its worktree, to reduce orphan creation in the first place (could be a follow-up).
Problem
Spawning a session (orchestrator or worker) fails with a
409 BRANCH_CHECKED_OUT_ELSEWHEREwhenever the target branch is still checked out in a stale/orphaned worktree left behind by a session that no longer exists in the daemon. This happens frequently in practice.Observed error:
In this repro the daemon had no
ao-agentssession record (active or terminated), yetgit worktree liststill showedao/ao-agents-orchestratorchecked out at the data-dir worktree path. Because no session owned it,ao session cleanupcould not reclaim it, so every re-spawn kept hitting the 409. The only way out today is manualgit worktree remove+git branch -Dagainst the project repo.Root cause
addWorktreerefuses early when the branch is already checked out at a different path:backend/internal/adapters/workspace/gitworktree/workspace.go:250-252returnsErrBranchCheckedOutElsewhere.backend/internal/service/session/service.go:456maps it to the409 BRANCH_CHECKED_OUT_ELSEWHEREenvelope.backend/internal/ports/outbound.go:117defines the sentinel.The check is correct as a guard, but it makes no distinction between a worktree owned by a live session (a genuine conflict) and an orphaned worktree whose owning session is gone (should be reclaimable).
Desired behavior
When spawn finds the target branch checked out in another worktree, decide based on whether a live session owns that worktree and whether it is dirty. Applies to both worker and orchestrator spawns (the logic lives in shared
addWorktree):409 BRANCH_CHECKED_OUT_ELSEWHERE(real conflict, never touch it).git worktree remove+ prune, then create the new worktree at the canonical path.This respects the existing hard rule in
AGENTS.md("Do not force-delete dirty registered worktrees") while making the frequent (clean-orphan) case just work, and never losing work in the dirty case.Acceptance criteria
git worktree removeneeded); the stale worktree is removed and a fresh one created.409 BRANCH_CHECKED_OUT_ELSEWHERE(no regression, no stealing a live session's worktree).backend/internal/adapters/workspace/gitworktree/workspace_test.goandbackend/internal/service/session/service_test.go.Notes / open questions