feat(sdk): Claude Desktop Band agent integration - #501
Open
AlexanderZ-Band wants to merge 40 commits into
Open
Conversation
AlexanderZ-Band
force-pushed
the
feat/intel-claude-desktop-claudeai-band-integration-ban-INT-1116
branch
from
July 27, 2026 18:06
8315300 to
1ffd3ec
Compare
AlexanderZ-Band
marked this pull request as ready for review
July 27, 2026 18:20
AlexanderZ-Band
force-pushed
the
feat/intel-claude-desktop-claudeai-band-integration-ban-INT-1116
branch
2 times, most recently
from
July 28, 2026 11:20
27fea58 to
3feac29
Compare
Collaborator
Author
Desktop constraints behind this design
|
AlexanderZ-Band
enabled auto-merge
July 29, 2026 08:56
AlexanderZ-Band
force-pushed
the
feat/intel-claude-desktop-claudeai-band-integration-ban-INT-1116
branch
from
July 29, 2026 08:56
80b79b2 to
b422071
Compare
Fanout runs on the leader's WebSocket event path, and drained each follower's socket with no bound. A follower that stopped reading — a frozen Desktop, a stopped process still holding its end — stalled the send queue it shares with every other follower, starving all of them of room events rather than only itself. Delivery is now bounded by band_relay_fanout_timeout_s and a follower that misses it is dropped like one that raised. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUzAucWVaLChjBgzRA27eW
Desktop answers an unactivated ui/message with a JSON-RPC error, not an isError result. The view classified every thrown error as a transport failure, so the deterministic refusal came back through retry_wakes and was re-asked on every tick — measured live at two failed round trips per tick, with the same stale mention ids riding along in wake_requests for the rest of the conversation. A rejection now carries whether the host answered it, so only a call that never arrived is retried. The ledger also gives up on a mention after MAX_REOFFERS, so a future misclassification cannot loop forever; the monitor loop remains the guarantee that the mention is answered. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUzAucWVaLChjBgzRA27eW
Observed live: the agent answered a message typed mid-wait and never resumed monitoring. The room went unwatched for minutes while the view's display loop kept ticking, so both the widget and the transcript looked healthy — and the agent had no way to tell, since nothing it can see distinguishes a watched room from one it stopped watching. Monitor calls now name their caller, so the view's display ticks no longer stand in for the agent's. A read whose last model tick is older than STALE_AFTER_TICKS quanta carries a server-authored monitoring notice, which the view relays into model context beside the briefing. The notice rides every tick and empties itself once the loop is running again — folding it into the cached briefing would have nagged forever. Repair still lands on the agent's next turn: Desktop declares sampling=false and gates ui/message behind user activation, so nothing here can start a turn on its own. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUzAucWVaLChjBgzRA27eW
A stalled loop leaves the room unwatched while the view keeps ticking, so it looks healthy from every surface. The notice the agent gets is now also on the record, which is how anyone diagnoses this after the fact. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUzAucWVaLChjBgzRA27eW
Caught live: the agent's loop was stopped for minutes and nothing said so. On a live socket a quiet tick answers from the last read's snapshot without reading, and that snapshot was taken while the loop was still running — so the one state that changes precisely when nothing else does was the one being reported stale. The quiet path now takes the monitoring status fresh, as it already did for the transport and the host. The regression guard runs on a live transport, which is what makes the tick skip its read; the earlier tests all read every tick and so could not see this. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUzAucWVaLChjBgzRA27eW
Adding one enum field put a $ref/$defs pair in the monitor tool's input schema, and Claude Desktop validates arguments against that schema itself. It mis-parsed the whole document: chat_id arrived undefined and timeout_seconds arrived as the string "25", so every monitor call failed validation and the agent stopped watching the room entirely. Tool schemas are now emitted self-contained, with internal refs inlined and the field's own words kept over the type's. Pydantic emits a $ref for any enum or nested model, so this belongs at the boundary rather than as a rule about how the input models may be written. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUzAucWVaLChjBgzRA27eW
The staleness limit came off the install default, but the quantum is the model's to pick per call — and the briefing tells it to wait up to 30s once the room is quiet. On a default install that read a healthy loop as stopped after a single wait, and injected "you are NOT monitoring" into its context on every tick while it was monitoring: worse than the silence it replaced, because it teaches the agent to disregard the notice. Each model tick now records the quantum it asked for, and the limit follows it. The matching warning is claimed once per outage rather than logged per tick, since the view keeps ticking for as long as the conversation is open and would otherwise bury the log in repeats. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUzAucWVaLChjBgzRA27eW
Claude Desktop sends numbers as JSON strings. Arguments were passing through two gates — the framework's JSON Schema pass and the input models — and the strict one rejected "30" against an integer before the model that reads it as 30 ever saw it. Every monitor call naming a timeout failed, which is every call the briefing asks for, so the agent lost its loop and said so: "validation snag on every value I tried". The input models are now the only gate, and a call they cannot read is refused in the caller's own terms rather than by schema path. Tests missed this because they invoke handlers with Python types; the guard added here calls the tool the way a host does, with the numbers spelled as strings. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUzAucWVaLChjBgzRA27eW
The limit was a multiple of the agent's quantum, which is the wrong shape: what happens between two monitor calls is one wait plus whatever the agent does with what the wait returned, and that work does not scale with how long it chose to wait. So the multiple ran 90s on a 30s quantum — measured live, a room unwatched that long before anything said so — while giving only 15s on the 5s quantum the briefing asks for during a conversation, less than answering a single mention takes. Adding a fixed grace to the agent's own wait tracks the real iteration: 60s at a 30s quantum, 35s at 5s. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUzAucWVaLChjBgzRA27eW
Live, with the stopped-loop notice reaching it, the agent reported its state correctly for the first time — "idle now" rather than the "watching room as tom" it used to claim while its loop was dead — and then asked whether it should resume. Both the notice and the monitoring contract said to call the tool; neither said the call was not the user's to approve. A turn that ends on that question leaves the room unwatched until they answer, which is the outage it was meant to end. Joining the room is the standing instruction to watch it, and both texts now say so. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUzAucWVaLChjBgzRA27eW
Asked why it had stopped watching, the agent recited the contract back correctly — "each turn should end with resume monitor" — and resumed. So the instruction was reaching it; the instruction was wrong. It framed turns as ending normally, and every ending leaves the room unwatched until the user happens to type, since nothing here can start a turn. The flood measured what makes that unnecessary: the host delivers a message typed mid-turn at the next tool-call boundary, so the agent can answer its user between two monitoring calls without pausing the loop. The contract now says the turn is the loop and stopping is the only thing that unwatches the room. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUzAucWVaLChjBgzRA27eW
Verified against the MCP Apps protocol: ui/update-model-context does not trigger a follow-up — the host defers the latest update until the next user message, including ui/message. So the stale notice cannot restart a stopped loop by itself; until the user acts it is a diagnostic, and the one actor who can act was being shown a green "Live · synced" badge computed from transport health alone. The room has two liveness axes — event delivery and agent attention — and the display now reports the weaker: "Agent stopped watching" with the idle gap when the model's loop is stale, and a Wake agent button whose click carries the user activation ui/message requires. The click relays the server-authored notice as the message, starting the turn that the deferred context update rides in with; the view still writes no model-facing text. On recovery the next payload clears notice, badge, and button together. Also: a widget instance the host mounted but never fed (Desktop renders one instance while the call runs and hands the result to another) no longer reports "WebSocket down · polling" — it has no transport to report, and says it is waiting for a room. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUzAucWVaLChjBgzRA27eW
A room event preempts the wait instantly and a quiet tick on a live socket costs no REST, so a longer quantum buys only fewer tool round trips — and its entire price is the human: a message they type lands at the next tool boundary, making the ceiling their worst-case wait to be heard. The briefing was steering the agent to 30 seconds on a quiet room, which is far past what a person accepts from a chat surface with them sitting at it. Ceiling lowered to 15 seconds, and the briefing now tells the agent to stay at the default once the user goes quiet instead of reaching for the ceiling. Worst case as measured — quantum plus model latency — drops from ~35s to ~13s at the default, ~8s while conversing. The cost is ~4 quiet ticks a minute instead of 2 on an idle room, on a surface whose sessions already last hours, not days. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUzAucWVaLChjBgzRA27eW
Measured live: the platform does not echo an agent's own messages back to its own WebSocket. A message the agent posts through band-mcp (a separate process on the same identity) produces no event in the room view's relay, so the skip-read proof — broker version unchanged on a live socket means the room is provably unchanged — was built on a false premise. Its failure compounded: reads were skipped while the quiet-tick cursor advanced past the unseen posts, so the agent's own messages never rendered at all. The widget froze on green "Live · synced" and only a manual Refresh showed what the agent had been saying. A tick now ends in a page-1 REST read regardless; the event's only job is ending the wait early, and a caller behind the newest message ever read is answered before waiting at all. ReadPulse keeps the watermark and snapshot but no longer claims proof authority — its docstring records why, so the optimization can return if the platform ever echoes own messages. The regression test posts as the agent with no event and requires the next quiet tick to deliver it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUzAucWVaLChjBgzRA27eW
Room-first (the default, unchanged) parks the turn on the monitor loop: the room is answered in seconds and the user's typing waits one quantum. User-first inverts the trade for users who want to come first: no turn is held, the user is answered instantly, and the room is swept once at the start of each turn — so a mention waits until the user next speaks or clicks the view's Check room button, whose click carries the user activation ui/message needs and whose text is the server-authored check prompt. The mode is chosen in chat (attention on the room-opening tools, from phrasing like "answer me first"), switched in chat (attention on a monitor call — honored only from caller=model, so the view's display loop cannot flip it), and held server-side. The model only ever reads the active mode's contract: attention_contract() emits one behaviour, the other appearing solely as the line naming the way into it, because a briefing describing both gets blended. Everything the loop's health machinery assumes is gated to room-first — staleness, the notice, the wake accelerator — since in user-first not-watching is the chosen state and each would nag or fire refused ui/message calls per mention. Pending mentions stay in pending_requests for the next sweep; the widget becomes the inbox: "On demand · N waiting" with a calm standing Check room button. Switching back re-arms staleness freshly — the switching call is its own first tick — so an idle gap accrued by choice never fires the notice. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUzAucWVaLChjBgzRA27eW
…way in Tried live, the standing Check room button earned its removal twice over: Desktop treats even an activated ui/message as a composer prefill rather than a send, so the click still ended in the user pressing Enter — a worse way to say something — and every user message already sweeps the room by contract, so the button added chrome without adding a capability. The widget now only counts what is waiting; the red Wake agent button remains for its real job, the stopped-loop repair in room-first, which appears only during a fault and carries text the user should not have to compose. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUzAucWVaLChjBgzRA27eW
…ontrol The behaviour, stated once: the user talks to the agent in chat, and by default the user leads — the room is swept at the start of every turn, and at join the agent asks once whether to watch continuously. Saying "watch the room" (at join or any time) parks the turn on the monitor loop with a 5-second beat back to the user; both directions switch in chat and take effect in the same reply. The widget is a window into the room: resizable by its native handle, collapsible, counting what waits — and buttonless, because everything the user wants done is said in chat. That deleted the ui/message wake path whole: without activation the host refuses it, with activation it only prefills the composer, and a buttonless window has no activation source. Gone with it: the wake ledger, retry_wakes, wake_requests and wake_prompt, the refresh tool nothing called anymore, and the view code and tests that served them. Staleness detection stays as the diagnostic for a stopped watch — repair is the user's next message, which carries the notice in with it. The whole tuning surface for attention now lives in one declarative table: CONTRACTS in prompts.py maps each mode to its briefing bullets, its after-join instruction, and its per-tick resume line. Editing the strings is editing the behaviour; nothing else moves. The user guide gains transcripts of both modes and the switch; the architecture doc and setup skill say what the code now does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUzAucWVaLChjBgzRA27eW
Observed live: "join ... and answer me first" was answered with "want the room watched continuously?" — the ask-once was written for the default join, where the user expressed no preference, but the contract could not tell a chosen user_first from a defaulted one. The model can: it parsed the phrasing. The after-join instruction now says to ask only when the user has not already said how the room should get attention. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUzAucWVaLChjBgzRA27eW
Desktop sometimes mounts two widget instances for one join — one while the call runs, one for the result — and the orphan sat as a full-height empty "Loading transcript…" box. The view now boots collapsed to its one-line bar and expands only when the first payload names a room, so the fed instance opens the moment the join lands and an unfed orphan stays a quiet "waiting for a room" strip instead of a broken-looking twin. Also better loading behaviour for the real instance: a slim bar during the join read instead of an empty box. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUzAucWVaLChjBgzRA27eW
After a long conversation the widget lives far up in the scroll history — hosts offer no way to pin or move a mounted iframe, but a tool result naming the view resource mounts a fresh widget at the bottom. "Show the room" now does exactly that: band_show_room remounts the live view at the current point without touching the attention contract, each view instance names itself on its watch ticks, and the server awards the room's display to the most recently first-seen instance — every older widget is told it is superseded and retires itself to a one-line bar. An already-known instance can never steal ownership back, so the two loops cannot flap. This also tidies Desktop's occasional double-mount: whichever twin ticks second simply collapses. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUzAucWVaLChjBgzRA27eW
…irection RoomTranscriptService had quietly absorbed four jobs: transcript reads, attention mode, monitor health, and view-instance ownership. The last three are one responsibility — this conversation's relationship to a room, not the room's content — and now live together in RoomSession (attention.py), which the service composes and the server addresses directly. The untracked-room fallback follows the system default (user_first), so a widget remounted after a process restart is not greeted by an armed staleness alarm for a loop nobody promised. Also deletes the workflow-operation registry: an enum, a type alias, a dict, and a dispatch loop that served exactly one operation consumed by two callers, which now simply call the function. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUzAucWVaLChjBgzRA27eW
Colors, radius, and danger red come from the tokens Band's site ships (assets.band.ai entry stylesheet): ink #001319 on white/#f5f5f5 surfaces, #e4e4e7 borders, 6px radius, #ff6b6b danger, and the brand green #92ed92 — used as a fill behind ink for the mark and badge, with the deeper green-700 #54a554 carrying text-adjacent accents, since the bright brand green does not read at small sizes. Dark scheme derives from the same ink. Fonts stay the system stack: the widget's sandbox blocks external origins, so webfonts cannot load anyway. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUzAucWVaLChjBgzRA27eW
The staleness heartbeat now runs both ways. The widget's display ticks are the server's only proof a window is on screen, so RoomSession tracks them (a mounting tool's grant counts as the first tick) and the monitor summary tells the model to call band_show_room when they stop — after a Desktop restart, which kills widgets silently, resuming monitoring used to leave the user staring at a dead relic. Ownership is claimed before the wait, so a fresh mount takes the room over the moment it calls, and re-checked after it, so a takeover landing while the old widget is parked mid-wait reaches the result it is already owed instead of leaving both widgets on screen for a tick. Also corrects the architecture doc's stale default-mode marker and its reference to the renamed CONTRACTS table. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…back The relay coordinates Desktop's MCP processes through fcntl file locks and Unix sockets, so on Windows merely importing the server raises a bare ModuleNotFoundError naming neither the product nor the platform. The console script now enters through a thin cli module whose platform check runs before the server is imported. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two independent relay defects on the leader's event path: The accept handlers parked on the relay-wide stop event, which a WebSocket failover never sets, and server.wait_closed() waits for the handlers on Python 3.12+ — so a leader losing its socket while any follower was connected hung in its own cleanup holding the leader lock, its followers wired to a dead socket indefinitely. Handlers now park on a per-leadership event that every exit sets, and close their own writers when released. Fanout also delivered serially, so several wedged followers stalled the WebSocket callback for the sum of their timeouts. Deliveries now run concurrently: however many are frozen, the event path is parked at most one fanout timeout, and healthy followers still get the line. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A quiet read advanced next_since to this machine's clock. A local clock ahead of the platform — or a message still committing as the read ran — then left backend timestamps at or before the cursor, and the resume filter dropped those messages from every later read: an addressed message silently never displayed or answered. Only timestamps observed from Band become the cursor now; a quiet tick repeats the caller's own instead of inventing a newer one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Install guide says macOS-only before any command runs, with the reason (fcntl locks, Unix sockets) and the behaviour on Windows. - The quantum is the configured default (5 s, ceiling 15), not an adaptive 5/10 split; the typed-input beat is one quantum. - Room creation is the one mutation band-mcp does not perform: band_create_and_open_room creates the room itself. - The tool table lists band_show_room; band_refresh_room_view and its Refresh button no longer exist, and the OPEN_ROOM operation name is gone with the registry it belonged to. - There is no no-REST tick to prove: every tick reads, and the cursor only ever carries platform-observed timestamps. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Prerequisites up front: macOS-only with the reason, Claude Desktop and uv install links, and the agent key as its own section. - Agent key sourcing: issued at registration in the Band app, agent key vs user key, one consumer per key, production vs self-hosted BAND_BASE_URL. - Released install path first; the editable checkout is the developer variant. - Verification split into quick check and full validation; remount via 'show the room' added. - Day-to-day section gains restart recovery and in-chat mode switching; upgrade/uninstall section added. - Troubleshooting: restart-killed widget row; the dedup row no longer claims the cursor advances on quiet ticks (it no longer does); the on-demand contract distinguished from a stopped watch loop. - Legacy band-peer plugin check scoped to users who also run Claude Code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nment Observed live: the room_first contract only mapped 'answered first / on demand' phrasing to the mode switch, so 'stop monitoring' — the natural words — read as a third mode. The model stopped calling without telling the server: the room went silently unwatched, staleness nagged against the user's own choice, and turn-start sweeps never began, so a waiting mention sat unseen through a full user turn. The room_first exit line now names the stop phrasings and says there is no abandoned mode; the staleness notice carries the one exception to its resume order — a user-requested stop is recorded by switching to user_first, which also ends the notice. Docs and setup skill state that stopping still sweeps every turn start. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Quiet ticks shed the briefing while still carrying the mode, so after a switch the widget's cache holds the old mode's contract and re-syncs it into model context on every display tick — after 'stop monitoring' the model keeps reading 'resume before anything else', on the exact path the stop fix prescribes, and user_first disarms the staleness notice that could have corrected it. A payload that switches the mode now invalidates the cached briefing instead of letting the old orders speak on. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…modes The stop-to-user_first mapping lived only in the room_first briefing and the staleness notice; the always-visible schema vocabulary (ATTENTION_CHOICE) still named only the entry triggers, so a conversation running without a briefing in context could rediscover the third-mode misreading. The notice's stop exception also verified itself — nothing said the user's words are the only key, leaving a stalled agent free to rationalize the switch. ATTENTION_CHOICE now names the stop triggers, the notice states that a stall is not a stop order, and the AttentionContract docstring names the other surfaces the mode vocabulary speaks from instead of claiming CONTRACTS is the whole tuning surface. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The turn-start sweep is prompted behaviour with no server-side enforcement, and health detection is deliberately disarmed in user_first — so 'mentions wait at most until you next say anything' promised a bound nothing can hold. Say what is instructed, and point at the widget's waiting counter as the honest signal. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Join and create refreshed the profile before briefing; show reused the cached viewer, so a remount after the profile changed under a long-running process misnamed the agent and mis-matched its mentions. The refresh now belongs to the mounts_view property in the workflow executor: every tool that re-briefs the room re-reads the identity it briefs, and the monitor loop keeps its cheap cached reads. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…gration-ban-INT-1116
…test band_room_stale_grace_s is a float field, so (STALE_GRACE_S + quantum) // quantum stayed a float and range() rejected it with TypeError: 'float' object cannot be interpreted as an integer.
# Conflicts: # .github/workflows/ci.yml
AlexanderZ-Band
force-pushed
the
feat/intel-claude-desktop-claudeai-band-integration-ban-INT-1116
branch
from
July 31, 2026 16:09
0266c4f to
e1bd785
Compare
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.
Summary
Adds Claude Desktop as a Band agent.
band-mcp --scope agentperforms Band operations.band-room-viewkeeps the configured agent online, shows one agent-visible room transcript, and synchronizes it with Claude./api/v1/agent/me, uses the configured REST/WebSocket endpoints, and supports join, refresh, and waiting for room events.desktopextra,band-room-viewcommand, setup documentation, and tests.Boundary
Claude Desktop cannot start a new model turn from an unsolicited Band message. The room view can update while Desktop is open; replies require an active Desktop turn/monitor loop.
Verification
band-mcpsend and a delegated reply flow verified live.