Agent Deck v1.16.29
Terminal session manager for AI coding agents.
Installation
Homebrew (recommended):
brew install asheshgoplani/tap/agent-deckQuick Install:
curl -fsSL https://raw.githubusercontent.com/asheshgoplani/agent-deck/main/install.sh | bashGo Install:
go install github.com/asheshgoplani/agent-deck/cmd/agent-deck@v1.16.29What's new in 1.16.29
Security and toolchain
- Dependency updates:
golang.org/x/cryptov0.58.0,golang.org/x/sysv0.49.0,golang.org/x/termv0.47.0,golang.org/x/syncv0.24.0,google.golang.org/apiv0.301.0,charmbracelet/x/ansiv0.11.9 andgo-runewidthv0.0.31 (#2672, thanks @dependabot).
Sending
- A Claude prompt suggestion (the dim predicted next message Claude can show in an empty input box) now counts as an empty input box. Automated sends, queued sends, watcher dispatches and the conductor recovery turn reach an idle Claude session that shows one, instead of failing with
composer_blocked. agent-deck never presses a bare Enter, Tab or Right at a suggestion, so it can no longer submit or accept text nobody wrote; when the suggestion is still painted after typing, Enter waits until the typed message has replaced it. A send whose own message is still sitting unsent in the input box still gets the recovery Enter (#2651). - Sessions launched with a parent (workers, a conductor's children) start Claude with prompt suggestions off (
CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false), on start, restart, resume, the first start of a TUI fork with a parent, and a cross-harness switch into Claude. Root sessions are unchanged. Set it with[launch] prompt_suggestions = "children-off" | "on" | "off"(defaultchildren-off, alsoconfig set launch.prompt_suggestions), per profile with[profiles.<name>.claude] prompt_suggestions, or per session withsession set <id> prompt-suggestions. Changes apply on the next start or restart (#2651). session send-keys --enter,--named-key Enter|Tab|Right(any spelling tmux accepts, such asenter,C-mor^I) and a--textthat is only a CR, TAB or cursor-right check a Claude session's input box first. At a prompt suggestion they press Escape once, wait a second, and then refuse with exit 1,delivery: "composer_blocked"andreason: "prompt_suggestion"; while a turn runs they refuse without pressing Escape. Without a suggestion nothing changes.--stream(TUI insert mode) stays unguarded (#2651).session send --require-input-prompt(the guarded send the macOS app uses) delivers to a busy Claude session whose input box is empty, as a message Claude queues for after the current turn, instead of refusing withcomposer_blocked. A draft or paste marker in the busy input box still refuses withcomposer_blocked, and a menu, picker or permission dialog withmenu_open(a menu under a running spinner used to reportcomposer_blocked). Busy Codex, pi and shell sessions are still refused.launch -m ... --no-waitnow runs the same pre-send check as a waited launch (#2652).session send --wait --jsonto a Claude session over the socket transport no longer times out with "turn identity not established" when the reply has arrived. An idle socket wait returns the next fresh completed reply withwait_outcome: "observed_not_correlated"andverified,submittedandacknowledgedall false, because a socket message cannot be matched to the turn it caused. Observing the turn and waiting for its reply share the--timeoutbudget. A busy socket wait now prints its one unverified JSON result instead of nothing (#2653, thanks @jaideeppyne, reported by @godlen4332).oon the session list now opens a message composer above the footer for any running local session, not only Claude sessions, andShift+Qopens it to queue the message for after the current turn. Enter sends, Ctrl+J adds a newline, Esc cancels, and Ctrl+X then E opens the draft in$VISUAL/$EDITOR. OpenCode 2.x queues through its session API once its session id is detected. Delivery errors are shown instead of only being logged. Remote rows do not open the composer (#2517, thanks @dtvillafana).
Sessions and accounts
session switch-accountworks when both accounts'projectsdirectories point to the same transcript store: the conversation is already there, so nothing is copied, staged or archived, and the switch fails safely if the store is rebound while it runs.session set ... accountno longer prints a needless migration warning in that setup. Accounts with separate stores keep the existing checks (#2648, thanks @jaideeppyne, reported by @bautrey).- A launch with
-p <profile>from inside a session of another profile no longer runs on the caller's Claude account: the inheritedAGENTDECK_ACCOUNT,CLAUDE_CONFIG_DIRandCLAUDE_SECURESTORAGE_CONFIG_DIRare dropped when-pdiffers from the caller's profile, so the new session uses the target profile's Claude directory.--accountstill wins, same-profile launches are unchanged, and status line wrappers keep the original environment and account attribution (#2650, thanks @jaideeppyne, reported by @godlen4332). - A Codex session that shows Working with collapsed queued questions above the input box (
? 1 question) reads as running instead of idle, and sends treat it as busy. Queued questions without a live turn still read as idle (#2666, #2667, thanks @007hacky007). session archive <id> --expected-parent <parent-id> --expected-content-version <version>archives a worker only when it is still the same finished turn: the session must have that parent, be waiting, and its transcript must still have thecontent_versionfromsession output --json. A Codex worker's latest turn must be the completed reply with no unsent submission left over, and a Claude worker's last reply must end with a done sentinel. Otherwise it refuses with exit 1 and stops nothing, so automatic cleanup no longer kills a turn that started after the check. Both flags are needed together, and other tools are refused.session archivewithout them is unchanged (#2643, thanks @arnzchen).- OpenCode 2.x sessions get live status again (running or waiting within about a second of the turn changing) once their OpenCode session id is bound. 2.x has no per-session
--portserver, so agent-deck reads the shared service's URL fromopencode service status, follows its event stream and counts only the bound session's events; each reconnect re-seeds from the service's list of active sessions, so a turn that ended while the stream was down does not stay running. The service password is read from its own registration file ($XDG_STATE_HOME/opencode/service.json) only when that file names the same URL, and only for a loopback URL. OpenCode 1.x, sessions with no bound id, sandboxed and SSH sessions keep their current status path, and[opencode].disable_sse_status = trueturns the shared stream off too (#2515, thanks @3rwww1). limits,list --jsonandstatus -vnow tell a blocked account (oauth_org_not_allowed), a per-model cap and a plain usage limit apart, and they show the wait reason (for exampleweekly limit · resets 12pm). An expired 5h or 7d window is marked"expired": trueand no longer counts as limit reached, andlimits --jsonmarks a blocked account and lists the sessions to move, each with afixcommand.session set <id> account|model <v> --intended "<reason>"marks a switch as deliberate (--intended-for, default 24h, at most 168h), andsession set <id> intended-switch "<reason>"marks the current account and model. The mark shows asintended_switchinlist --jsonandsession show --jsonuntil it expires (#2701).- OpenCode:
opencode2is a launcher option in the new, edit and setup dialogs, kept across reload, restart and fork, while the session stays anopencodesession. 2.x launches set model and agent through the session API, forks can move to a worktree, and MCPs are written to OpenCode's nativemcp.serversconfig without touching other settings (#2516, thanks @dtvillafana). - Forking an OpenCode 2.x session works again. Before, the fork pane died at once because 2.x has no
--forkflag. The fork goes through the shared service, the new session is bound to the child at once so a restart resumes it, and model and agent overrides (or the[opencode]defaults) are applied to the child. A failed fork shows an error and leaves no orphan session (#2513, thanks @3rwww1). - Restarting a Hermes entry resumes that entry's own conversation, even when several entries share one working directory. Before, it could resume another entry's newest conversation. An entry with no recorded conversation in a shared directory starts a new one and shows a warning; a lone entry in its directory still resumes the newest conversation there (#2582, #2696, reported by @renky).
- Claude agent view: hooks from a background supervisor or worker (a
bgentry in Claude'ssessions/<pid>.json, orCLAUDE_JOB_DIRwhen there is no entry) are ignored, so they no longer rebind another pane's session. A restart follows acontinued-inrecord to the conversation's new session id instead of reopening the stalled copy.add --resume-session,launch --resume-sessionandsession set <id> claude-session-idrefuse an id that an archived session holds and name that session, instead of starting a new conversation or reverting silently (#2683, #2697, reported by @emcodeio). - A cross-harness switch into Claude pre-trusts the target folder, so the new session no longer waits on Claude's "Do you trust the contents of this directory?" prompt (#2677, #2687, reported by @koenzhao).
launch -account <slot>(and any launch whose Claude config dir comes from an account, conductor, group, profile or env) also seeds folder trust in that dir's.claude.json, so the session no longer dies on the trust prompt in a folder the slot never opened (#2673, #2688, reported by @bautrey).- A shell or editor session is no longer relabelled as an agent because an agent's name appears on screen (in a Neovim buffer, shell output or a conversation). The configured tool is never overwritten by what agent-deck observes, and restart keeps using the configured tool (#2456, thanks @dtvillafana).
Comms
- pi sessions are text producers in the Comms Ledger: each finished pi run adds one
turnrecord with the reply text (the last 16 KiB), tiered urgent when the reply carries a done sentinel or a question and info otherwise. A tool-only, aborted or failed run adds onestatusrecord and no text. Status display for pi sessions is unchanged. Needs the v3 pi extension: runagent-deck pi-hooks install(#2671, thanks @fpmx3bp). - A pi child that was revived (
session restartafter an error) wakes its parent again when a turn ends: pi'sturn_endhook now counts as a transition, like Claude'sStop(#2665, #2689, reported by @koenzhao).
Conductors and watchers
- A conductor keeps its
[conductors.<name>]settings (env_file, config_dir, command, model, meta.json env) when its title changes. The conductor name is now stored on the session:conductor setupwrites it, and an older conductor row adopts it on its next save. So a renamed conductor or a decorated title such as🎙 conductor-xstill sources its env_file and keeps its TELEGRAM_* variables. Only rows flagged as conductors keep a stored name: a worker that briefly carries a conductor title still resolves by its current title, so renaming it back ends the match.session send conductor-<name>, the heartbeat and the bridges keep reaching a renamed conductor, andconductor statuswarns when the title no longer matches (session_titleandtitle_mismatchin--json).conductor teardown --removeremoves the row it stopped. A prefix wrapper such asenv TELEGRAM_STATE_DIR=<dir> {command}now works with the env prelude: the command is passed as onebash -cargument, a{command}inside quotes is escaped for that quote, and a pane warning is shown when a wrapper's TELEGRAM_STATE_DIR is unset for a session that does not own the Telegram channel (#2694, #2708, #2709). - A conductor keeps its settings and its Telegram bot across an account switch (
session switch-account, the TUI switch, orsession set <id> account <slot>and a restart). A conductor with no stored name is now found by the conductor folder it runs in (conductor/<name>with ameta.json, using the namemeta.jsonrecords) before its title, so a title such asconductor-team-opsfor conductoropsno longer loses its[conductors.ops.claude]settings; a worker launched inside the conductor folder is not the conductor. On a switch, the Telegram state from the old config dir is linked atconductor/<name>/telegram(never copied, and an existing entry is never replaced), and a conductor with no configuredTELEGRAM_STATE_DIRis pointed at it on every start. Nothing is linked into the new account's shared config dir, so other sessions on that account never see the token. Discord and other channel plugins are not pointed automatically; the switch result says what to set (for Discord,DISCORD_STATE_DIRunder[conductors.<name>.claude.env]).session showandconductor statuswarn when a conductor's channel plugin has no state and will not start (channel_warningsin--json), andconductor statuswarns when more than one session resolves as the same conductor, which means two pollers on one bot (duplicate_session_ids) (#2703). - Mattermost is a fourth conductor bridge, next to Telegram, Slack and Discord (experimental). Set it up with
conductor setupor[conductor.mattermost]. It obeys one configured user in a DM or a channel, supports!status,!sessions,!restartand!help, sends NEED alerts and heartbeats, and catches up on missed posts after a reconnect. A plain-http server URL is refused unless it is localhost orallow_insecure_httpis set. Seedocs/conductor/mattermost.md(#2542, #2545, thanks @wallacms). agent-deck web --no-tuinow runs the profile's watchers: webhook and GitHub ports listen, ntfy and Slack subscribe, and routed events reach the conductor. Each profile runs its watcher engine in one process. Other TUI or web processes stand by and take over within about 5 seconds when the owner exits, so[instances] allow_multiple = trueno longer binds a port twice (#2530, #2638, thanks @AndreIntelas).- Watcher events routed to a conductor go through the durable send queue and are typed into the conductor at most once. They are no longer lost when the TUI or web process quits or is signalled, when the composer is busy, or when the conductor is stopped: the event waits until the conductor runs again.
[watcher] delivery_deadline(a duration such as"72h", no deadline by default) fails an event that is still undelivered after that long, andwatcher status <name>shows per conductor how many events still wait and since when (undeliveredin--json). An event for a conductor that switched harness follows it to the new session (#2537, #2641, thanks @AndreIntelas). - A queued Slack, Telegram or Discord message to a Claude conductor no longer reports
[Failed to send message to conductor]when the conductor accepted it and its turn ran past the synchronous wait. The reply is still delivered, and an accepted message is not sent again (#2509, thanks @kaneda-fr).
Fixes
- The status filters
%(open),&(errors) and^(archived) now apply to remote session rows as they do to local rows, so a remote with many stopped or failed sessions no longer floods a filtered view. A remote that sends no status keeps showing under%(#2627, #2645, thanks @koenzhao). /search and Enter on a session hidden by a status or time filter now jumps to it: the filters are cleared the same way the empty-view fallback clears them and collapsed groups are expanded. A session that is gone shows a notice instead of doing nothing (#2655, #2656, thanks @koenzhao).- The New Session, Edit Session and account selector option pills no longer show black gaps between them on terminals with a black background (seen on Kitty): the separators use the dialog's surface colour. The three dialogs share one pill renderer (#2647, thanks @siraben).
- MCP proxy clients (
mcp-proxy) exit when their stdin closes or their parent process exits, instead of reconnecting in a loop and staying behind as orphans. A backend disconnect with a live client still reconnects, and a request already partly sent is not replayed. The shared pool is unchanged (#2662, thanks @jaideeppyne, reported by @godlen4332). conductor setupon Linux creates the notifier and bridge log directories (owner-only for new ones) before it enables the systemd units, so the transition notifier no longer crash-loops with209/STDOUTon a fresh account. A log path that cannot be created fails setup before any service starts (#2646, #2654, thanks @jaideeppyne, reported by @tsblack).- Web: opening a
/s/<id>link after using Fleet shows that session's terminal instead of leaving Fleet on top. Skills and MCPs stay open if you were on them, and this also works with browser back and forward (#2679, #2684, thanks @littleKitchen, reported by @bautrey). - Web: the New Session dialog offers to create a working directory that does not exist yet, instead of failing with an error. It lists the folders it would create, and creates them only when you tick "Create this folder". Editing the path clears the tick, and if the list of missing folders changes before you submit, the dialog asks again. No git repository is set up (#2658, #2660, thanks @007hacky007).
/search opens with an empty query and the full session list, instead of the last query or a group-only list. Session titles, project paths and tools now match with small typos: exact text for 1 to 3 characters, one typo (a missing, extra or wrong letter) for 4 or 5, and two for 6 or more. Exact matches are listed first, so Enter on a fully typed name picks that session. The status words (waiting,running,idle,error,stopped) still filter exactly, and Recall is unchanged (#2681, thanks @dtvillafana).- Title sync no longer renames a deck row to the name of a Claude session running in another project: the name is used only when the Claude session record's working directory is inside one of the session's own paths (subdirectories and symlinks count). A record with no working directory, or a session with no path, syncs as before (#2678, #2685, reported by @scottyallen).
- The TUI no longer freezes for up to about 5 seconds (keys ignored,
status pass exceeds 250 ms budget) while agent-deck looks for the transcript of a Codex session that is not bound yet. The search of$CODEX_HOME/sessionsnow runs without holding the session lock, and its result is dropped if the session was stopped, restarted or bound in the meantime (#2674, #2686, reported by @alx-sik). - A local parent session's child list (for example a conductor's children) can be folded:
Tab,lor→on the parent row toggles it,hor←folds it, and a▾N/▸Nbadge shows the count. Folds are remembered across restarts (#2631, #2636, thanks @koenzhao). - Web: the New Session dialog suggests existing folders as you type the working directory. Pick one with the arrow keys and Enter or Tab, or click it. This also works above a phone keyboard (#2657, #2659, thanks @007hacky007).
- Web: opening a session's terminal no longer waits 5 to 7 seconds behind a session list refresh when many sessions are live (#2663, #2693, reported by @bautrey).
session stopfrom the CLI is no longer recorded aserrorwhileagent-deck webor the TUI is running. The running process now keeps the committedstoppedstatus (#2669, #2690, reported by @bautrey).- When
.agent-deck/worktree-setup.shfails, the TUI warning shows the last lines of the script's output instead of onlyexit status 1. This applies to create, fork and thebre-run (#2637, #2691, reported by @godlen4332). - After a TUI restart that skipped cleanup (crash, kill, auto-update), the leftover notification bar is no longer saved as your original
status-leftand shown again, soCtrl+b Nno longer fails withCan't find window(#2705, #2706, reported by @nezakoo). - The open-descriptor budget warning explains itself. The footer marks it
(advisory), andagent-deck healthprints how the budget is computed, when the warning matters, and anlsofcommand for that process, with a pointer todocs/runtime-health.md#descriptor-budget(#2668, #2692, reported by @netllama). list --jsonis faster with a long history of failed or stopped sessions: missing panes are checked with one tmux inventory per socket instead of one probe per row (#2644, thanks @arnzchen).
Upgrading
- Prompt suggestions: workers and conductor children start Claude with prompt suggestions off from their next start or restart. Set
[launch] prompt_suggestions = "on"to keep the old behaviour. A worker launched with--no-parentis a root session and keeps suggestions on; setoffif you want those off too. - Raw keys: a script that sends
session send-keys --enteror--named-key Tab|Rightto a Claude session can now get exit 1 withcomposer_blockedandreason: "prompt_suggestion"when a suggestion is shown. Type the text first with--text, then send--enter. - Guarded send:
--require-input-promptto a busy Claude session now delivers a queued message where it used to refuse. A menu drawn under a running spinner reportsmenu_openinstead ofcomposer_blocked. - Socket waits: a caller of
session send --wait --jsonover the socket transport should readwait_outcome;observed_not_correlatedmeans the output is the next reply observed, not a reply proven to answer this message. - Cross-profile launches:
agent-deck -p <other> launchfrom inside a session now uses the other profile's Claude account. Pass--accountto pick an account on purpose. - pi: run
agent-deck pi-hooks installon each host (remotes included) to get the v3 extension;pi-hooks statusreports v2 installs as outdated, and a v2 extension adds no pi records to the Comms Ledger. - OpenCode 2.x: live status needs the shared OpenCode service running on the same host (
opencode service statusprints a loopback URL) and the session's OpenCode id bound. Without either, status keeps reading the terminal screen as in 1.16.28. - Watchers: only one process per profile runs the watcher engine now.
watcher startsays whether the watcher runs within seconds or once the engine's process restarts. Routed conductor events wait for a stopped conductor with no time limit. Set[watcher] delivery_deadlineto put a limit on that. - Wrappers: a session wrapper with a prefix (
env X=Y {command}) now gets a compound command as onebash -c '<cmd>'argument. Suffix flags still reach the agent. For remote use, pass--sshor quote{command}. - Conductors: a conductor created before conductors were flagged (very old setups) is found by its exact
conductor-<name>title only, as before. To make it rename-proof, runagent-deck conductor setup <name>again; this flags it and stores its name. - Agent detection: agent names on screen no longer turn a shell into an agent, so an agent started inside Neovim, a shim or an unrecognized wrapper is not detected. Sessions that were wrongly saved as an agent are not repaired automatically. In both cases, configure the tool explicitly, or recreate the session.
Known issues
- Watchers: the local webhook adapter has no authentication (it listens on 127.0.0.1 only), and with
[watcher] delivery_deadlineunset, routed events wait in the durable send queue for a stopped conductor without a limit. A local process that floods the webhook can grow that queue. Setdelivery_deadline(for example"72h") if that matters on your host. - When
.agent-deck/worktree-setup.shfails, its full output is written to the agent-deck log at warn level without a size cap. Keep secrets out of what the setup script prints. - Contributors: on tmux 3.3a, a few tmux-driven tests (
TestCustomCommandExitStatusand two bootstrap cleanup tests) can fail intermittently because tmux sometimes does not report the exit of a pane process that ends at once. Released binaries are not affected: tracked commands read their exit code from a receipt file.
Thanks to @jaideeppyne, @koenzhao, @siraben, @007hacky007, @littleKitchen, @fpmx3bp, @arnzchen, @3rwww1, @dtvillafana, @AndreIntelas, @wallacms and @kaneda-fr for their pull requests, to @godlen4332, @bautrey, @tsblack, @scottyallen, @alx-sik, @renky, @emcodeio, @nezakoo and @netllama for their reports, and to @dependabot for the dependency updates.
Committed by Ashesh Goplani
Full Changelog: v1.16.28...v1.16.29