v3.0.0 - Multiple sessions per ticket
A ticket used to mean a session. One card, one workspace, one Claude. That works right up until you open a PR and it sits in review, because now the ticket is busy and so are you. The work is done, nothing is happening, and the only way to start the next piece is to abandon the session that is waiting for a human.
So a ticket can now own several sessions. Drag a card to In Progress as before and you get one, exactly as you always did. When that session is parked in review, open the card and hit "Start another session", and a second one spins up in its own worktree set with its own branch, its own terminal, and its own Claude. The first one keeps running. The board still shows one card per ticket, because a ticket is still one piece of work, and the sessions live inside the detail panel where you switch between them.
A new session can also be built on an existing one. When you start another, you get a "Build on Session 1" checkbox, and if you tick it the new session branches from that session's branch instead of the repo's base. Everything the parent committed is already there. Uncommitted work in the parent's worktree does not come across, which is worth saying plainly because it is the one thing people assume works the other way. Git is the only real inheritance vector here, so committed history transfers and a dirty working tree does not. The kickoff prompt names the parent branch so the agent knows what it is building on.
Inheritance stops at one level. A session built from a session built from another records its direct parent, not the root, and the switcher shows "built from Session 2" rather than pretending there is a tree. Deeper nesting, with the navigation and naming that would need, is deliberately left for later. If the parent is cleaned up while the child is still alive, the caption quietly stops showing rather than pointing at something that no longer exists.
See it
Higher-quality video: dispatch-v3.0.0-second-session.webm
Everything underneath is per session now
The interesting work was not the second session. It was making every subsystem stop assuming there was only one, while there still only was one.
Hook events carry a session token, so a status update from one session cannot be attributed to its sibling. The marker watcher, the liveness detector and boot reconciliation each address a specific session, which means a card whose second session died still reads as live, and a restart re-adopts each session's ttyd on its own port instead of guessing from the card. The terminal proxy is keyed by session id, so switching sessions in the panel switches the terminal without leaking a connection to the previous one.
Cleanup got the same treatment. Tearing down one session removes only its worktrees and leaves its siblings untouched, and if it was the active one, another is promoted so the card is never left pointing at something that was cleaned. A ticket reaching Done schedules every session it owns rather than just the one the card happened to be mirroring. The cleanup modal names the session when a ticket owns more than one, and the uncommitted-work refusal is per session, so a dirty worktree in one blocks that teardown and nothing else.
Group cards keep working throughout. Members still mirror column moves atomically at all five write paths, and the eight single-card routes still refuse a member card with a 409 rather than half-applying something.
Upgrading, and downgrading
A board written by 2.9 migrates on first boot into the new shape with each card owning exactly one session, and nothing looks different afterwards. That was the requirement the whole design was built around, so it is checked rather than hoped for.
Downgrades are the more interesting direction, because dispatch installs through npx and one machine updating before another is ordinary rather than exotic. An older build opening a migrated board would read the flat fields fine but write them directly, drifting the two apart with no idea it was doing so. So the newer build now refuses to open a board written by a newer schema than it understands, on the grounds that it cannot know what a later migration moved, and it repairs drift an older build already caused, on the grounds that refusing there would strand you on a board whose only other reader is the thing that broke it.
The audit found four things
The last piece of work in this release was auditing every promise the rest of it made about not breaking the single-session case. It was not a review. It found four real defects that none of the earlier checks had caught, and fixed them.
Starting a second session minted its hook token against the wrong session. Because a reserved session is deliberately not promoted to active until it finishes starting, the token landed on the old, still-running one, and the new session's hook channel stayed unauthenticated until something else happened to repair it.
The kickoff path addressed tmux without exact-match targeting. With a suffixed sibling alive, tmux would silently prefix match onto the wrong session and report success, which meant kickoff text could be typed into a neighbouring pane. Inheritance made that considerably more reachable, since every inherited start creates exactly the kind of sibling that triggers it.
Session data was riding the wire twice, once flat and once nested, costing about 35% on board payload for the common single-session case. That is now down to about 16%, with the remainder being two fields the terminal genuinely reads.
And one of the measuring instruments had gone stale: its selector predated a UI change and was reading the wrong element, which would have quietly failed the audit's own final check.
Accessibility
Modals had no focus trap. Tabbing past the last control in a dialog dropped focus onto the page behind it, which has been true since modals were introduced and is now fixed. The session switcher and the start-another button are keyboard operable and announced, with the switcher's pills reachable by tab and operable by enter.
Settings, notifications and the workspace view
This release also carries a batch of work from Filipe Verseghi (#64), which is why the app looks different in places that have nothing to do with sessions.
Settings is now a full-screen page with a sidebar instead of a modal, and it gained three new sections. Models lets you edit the arguments Claude launches with, defaulting to the --dangerously-skip-permissions flag dispatch has always used, with a reset link if you change your mind. The arguments are tokenized rather than passed through a shell, and tmux execs the argv array directly, so there is no path from that field to shell injection. Workspaces gives you add, list and remove for your registered workspace folders, which onboarding could add to but never manage. Notifications shows whether desktop notifications are actually permitted, blocked or simply not asked for yet, and adds an optional chime when a card wants your attention. The chime is two notes synthesized with Web Audio rather than a bundled audio file, so it costs nothing in binary size, and a burst of cards transitioning at once still gets you one gentle ding rather than a pile-up.
The Board and Orca toggle used to be two bare icon buttons wedged between "New ticket" and the settings cluster, which made the app's second view easy to miss entirely. It is now a labeled segmented control sitting in the true center of the header. Orca is called Workspace now, and if you had the old view selected your preference migrates across the rename rather than silently resetting to Board.
The Workspace view itself gained grouping. You can group by status, which is the seven board columns and remains the default, or by the workspace folder a ticket's session actually runs in, with a bucket for tickets that have not started. A second level of nesting is available under either, and it excludes whichever dimension you already grouped by, since nesting a dimension under itself means nothing. Sorting is by ticket ID, numeric aware so ENG-2 comes before ENG-10, or by title. All three persist per browser.
Full changelog: v2.9.0...v3.0.0
Requires Node ≥ 22.22, tmux, ttyd, git, and the Claude Code CLI. Setup is in the README.
