council: a race says which worktree it is cutting, and ctrl+c still works - #298
Merged
Conversation
…orks /arena prepared its worktrees inside the render loop, so for as long as git took the room drew nothing and read no keys. Parallel sessions against one repository put that in front of the operator: a held index.lock, and a frozen room with ctrl+c unread — the one act that could have ended the wait, queued behind the very command it was waiting on. The setup runs as a command now. dispatch starts it and returns; the turn is born later, in applyArenaSetup, through the extracted sendTurn. The per-seat `git worktree add` calls stay serial, because those adds write the repository's own refs and would contend for exactly the lock this change exists to survive. While it runs the footer names the step in words — "arena: preparing worktree for codex…" — with a spinner beside it and no percentage, no count and no elapsed figure: council cannot measure how long a checkout takes. The spinner is liveness, not progress; without it a working room and a dead one draw the same sentence. The setup carries one 90-second deadline, through gitOutCtx, a context-carrying sibling of gitOut used by this path and nowhere else. Measured on this box: a five-seat setup against a 540-file, 8 MB repository of telltale's own shape ran 2.3s cold and 1.3s warm, so the deadline is ~40x the measured case — sized for a lock, which is unbounded, and never tight enough to kill a checkout that would have finished. Every ending hands the room back. A deadline or a git refusal stops the setup wholesale and lands on the room's existing arena notice, leading with the step and then quoting git verbatim; the brief returns to the composer. ctrl+c stops the setup in every mode and does not quit. Worktrees already added are kept and named, per this feature's own rule. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
/arenaprepared its worktrees inside the render loop.arenaSetupran inline indispatch, which runs insideUpdate, so for as long as git took the room drew no frame and read no key. The operator measured it the hard way: parallel sessions against one repository, a lock held by another of them, and a room frozen with ctrl+c unread — the one act that could have ended the wait, sitting in a queue whose only drainer was blocked insidegit worktree add.What changed
The setup is a command. It runs on a goroutine and reports back through a channel (
internal/council/arenasetup.go).dispatchstops at the point of preparing and returns; the turn is born later inapplyArenaSetup, which calls the extractedsendTurnwith what the setup measured. Everything the turn stamps — its clock, its context, the snapshot of the previous replies — is now stamped at the spawn rather than at the keypress, which is the honest reading of every duration it renders.The worktree adds stay serial. That is a ruling, not an unfinished optimisation: those adds write the repository's own refs and administrative files, so N at once contend for exactly the lock this change exists to survive.
TestWorktreesAreAddedOneAtATimemeasures it rather than assuming it — when seat N's add is announced, seat N-1's tree already exists on disk.The frame names the step, never the progress.
arena: preparing worktree for codex…in the footer, with a spinner beside it and nothing else. No percentage, no "2 of 4", no elapsed figure — council cannot measure how long a checkout takes, and a number it did not measure is one it may not draw (§4a.1). The spinner is liveness rather than progress: a step that takes a minute prints the same sentence throughout, so without a moving cell a working room and a dead one render identically, which was the old lie.TestSetupStepsNameTheWorkAndNeverTheProgressfails on any step carrying a digit or a%.One deadline, on the setup path only.
gitOutCtxis a context-carrying sibling ofgitOutwith exactly one caller.gitOuttakes no context and can never be handed one, so every other git call council makes is un-deadlined by construction — a diff read or a commit killed by somebody's guess at a timeout is worse than a slow one. One deadline over the whole setup rather than one per call, because the number an operator experiences is how long the room was unusable.The measurement, and the deadline it bought
Measured once on this box (Intel Mac, macOS 26.5.2, 2026-08-17): a five-seat setup against a synthetic repository built to this repo's own shape — 540 files in 60 directories, ~8 MB, against telltale's 526 tracked files and 8 MB — ran end to end, worktree adds included, in 2.3s cold and 1.3s / 1.4s warm.
The deadline is 90 seconds, ~40x that. The margin is the decision rather than the number: the failure it exists for is a lock another session holds, which is unbounded by nature and says nothing about how large the repository is. What it must never be is tight enough to kill a setup that would have finished, since a
git worktree addkilled mid-checkout leaves a half-created tree the operator clears by hand.Every ending hands the room back
A deadline hit or a git refusal ends the setup wholesale — a clock is a fact about the room's patience, not about a seat, so recording it as per-seat skips would blame the vendors for one timer and then race whatever survived as if the operator had asked for a partial race. It lands on the room's existing arena notice, leading with the step before quoting git verbatim (
arena: preparing worktree for codex: fatal: …): git's line names a lock, not which of eight calls met it. A process the context killed is never quoted as if git had refused it. The brief returns to the composer and the room composes again.ctrl+cstops the setup in every mode and does not quit; trees already added are kept and named, per §9.37's own rule that worktrees live until the user deletes them.Verification
go vet ./...— clean.go test ./internal/council -timeout 20m— ok, 42s. Also clean under-race.go test ./...— every package ok.arena-setup.txt, read before acceptance; no existing golden moved.index.locknow ends in a notice instead of a freeze rests on the tests and on an expired-deadline fixture, not on the lock that started this. Stated in design.md §9.37's amendment rather than implied paid.Design record:
docs/design.md§9.37, amendment dated 2026-08-17. User-facing behaviour indocs/council.md.🤖 Generated with Claude Code