Skip to content

Git worktrees of one repository resolve to separate rooms, so agents in worktrees cannot coordinate #61

Description

@mostlydev

Observed on v0.10.0 while two harnesses (Claude Code and Codex) worked the same repository, one of them from a git worktree.

Problem

Agents working in different git worktrees of the same repository land in different rooms and cannot see each other — even though they share a remote, share branches, and can collide on the same files and the same live deployment.

Worktrees are the natural way to let two agents work a repo concurrently without fighting over one checkout. Right now, choosing that isolation silently costs you coordination, which is the thing you needed the isolation to make safe.

Evidence

Same repository, main checkout plus two worktrees:

/Users/…/tiverton-house    toplevel=/Users/…/tiverton-house
                           common-dir=/Users/…/tiverton-house/.git
/tmp/tvh-94                toplevel=/private/tmp/tvh-94
                           common-dir=/Users/…/tiverton-house/.git
/tmp/tvh-rollback          toplevel=/private/tmp/tvh-rollback
                           common-dir=/Users/…/tiverton-house/.git

tt list from inside /tmp/tvh-94 returns 0 rooms, while the main checkout has an active room with 4 members.

Mechanism

resolveGitRoot in src/path-resolution.ts:91 shells out to:

git -C <path> rev-parse --show-toplevel

In a worktree, --show-toplevel returns that worktree's root, so resolveWorkspaceRoot yields a different workspace root per worktree and each gets its own room.

git rev-parse --path-format=absolute --git-common-dir is identical across all worktrees of a repository (see output above) and is the primitive that already expresses "same repository".

Suggested direction

Resolve the workspace root through the common git directory rather than the per-worktree toplevel — roughly, ask for --git-common-dir, and take its parent when it ends in /.git — so every worktree of a repository maps to one room by default.

Worth deciding deliberately rather than assuming: some users may genuinely want per-worktree isolation (two unrelated tasks, no shared state). If so, sharing should be the default with an opt-out, not the reverse — the failure mode of accidental isolation is silent and the failure mode of accidental sharing is loud.

Whatever the default, tt list/tt state reporting which worktree each member is in would help a lot. Members currently surface host_id and pid but nothing that distinguishes two agents on the same machine in different checkouts.

Why this matters — what it cost us

The room accumulated four members across sessions: two Codex, two Claude. That happens partly because harness session identity rotates (a Claude session restart produced a new claude:… id and left the previous one in the room looking active), and partly because worktree-based agents rejoin as new participants.

With multiple members, an ordinary tt release handed the turn to fair ordering, which reserved it for a Codex that had been idle 11 minutes rather than the Codex actively long-polling and holding the review context. The idle session never consumed it, so the live session's waits kept returning quiet and correctly reported "no handoff has arrived" — its wait was healthy, the turn had simply gone elsewhere.

tt take refused, correctly by its own rules:

Takeover is not available: Not your turn — turn 100 is reserved for codex:… (claim expires in 17m)

So the handoff was stranded for up to 17 minutes with no in-protocol way to redirect it, and a human had to intervene across panes to pass the assignment to the right session.

Two smaller things that would have defused this independently of the worktree fix:

  1. Fair ordering could prefer members with a live wait. A member idle for many minutes is a worse recipient than one actively long-polling, and the room already tracks last_seen_at and wait_intent.
  2. A reservation held by a demonstrably stale member could be redirectablett assign to a specific live member, or takeover permitted when the reserved member's last_seen_at is far older than its claim. tt kick exists but is a blunt instrument for what is really "this turn went to the wrong session".

Happy to open a PR for the --git-common-dir resolution if the default-sharing direction is the one you want.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions