docs/discord-rooms.md

Discord rooms behind the curtain

An owner may explicitly link a private text channel to the main Clankie conversation with clankie discord set --main-conversation-channel-ids CHANNEL_ID (comma-separated for several; none removes all links). The same field is in the shared Discord settings API and the settings UI. This sends each admitted message directly to the main conversation's existing runner, without a room intermediary or child. A live native head receives a correlated escalation; otherwise the normal service runner continues that same main conversation. The room keeps an execution receipt naming mainConversationId. Only the correlated answer returns to the original Discord message; ordinary main-session output is never mirrored. Attachments arrive as untrusted Discord references the head may inspect with its own tools; the room backlog is not copied into the main context.

The sender must match the configured owner account, the server policy must be owners: me, the transport must be the official bot, and the channel must pass the existing owner-only audience proof and machine-access checks. Room-skill channels cannot use this link. These checks run again before native admission, before returning an answer and before replaying a saved private result. A shared audience, removed binding or changed owner suspends the route. A definite native refusal may select the normal service runner; an accepted or uncertain native delivery never moves to another executor. Voice and unlinked rooms retain their existing routes.

House-hunting feedback uses authenticated Discord IDs. An owner may explicitly bind an exact legacy author label within one household through discord.houseHuntingAuthorBindings in the shared settings API. Every entry requires ownerConfirmed: true; names never prove identity. A binding alone changes nothing. A later reconsideration by that ID appends explicit legacy targets while preserving original authors and other people's rejections. See the CLI contract and ADR 0252.

One Clankie has parallel room threads. /conversation reads the room's native heard/said/tool history; it does not turn inspection into authority to send or run tools there (ADRs 0124, 0176, and 0186).

Each voice ask_clankie handoff and admitted text request has its own thread under Clankie in the dock and app. A text follow-up from the same person under the same grant while their request is still running steers that request instead (reported absorbed), so a burst gets one answer; other people keep their own threads. The card shows who asked, the request, current work and result; selecting it opens that request's transcript. Up to four handoffs run globally, at most two per room. A 32-job waiting bound returns a clear busy result when full. Finished jobs collapse so active work and seats remain visible. Spoken answers retain the room's normal floor. Replies return to the original asking room. Approval requests still continue on the authenticated operator surface. With a Codex head, all non-owner room work runs on Pi with the room's own tools, including explicit actor and server ownership grants. Mixed audiences and skill rooms always use Pi; only owner-only rooms can use native children. See ADR 0229 and the verification record.

clankie discord rooms and TUI /discord rooms add delivery health. The API is GET /v1/discord/rooms; its strict node-free contract lives in packages/protocol/src/discord-rooms.ts. Counters describe observed outcomes in the bounded retained delivery window (up to 4096 deliveries per room), not an estimate of everything Discord might have sent while disconnected. A confirmed Discord send counts as answered. Model completion does not. Buffered messages remain pending; absorbed messages count as answered only when their linked send is confirmed. Explicit backlog eviction counts as missed. No observations means unknown, not zero evidence of failure. Silence reports volitional_silence, not an invented explanation of the model's reasoning. Failures/escalations wake the owner once per delivery through the existing content-free device push path.

Failed accepted text requests remain in the durable Discord inbox. On the next healthy recovery round, Clankie checks for an existing bot reply before retrying the saved request once. If that retry fails, the accepted message gets one short failure notice asking the sender to resend. Legacy failures with no saved request are reopened from the ingress receipts and get that notice without rerunning tools from a possibly edited message. Neither a model result nor an attempted notice closes the delivery; Discord confirmation does. A failed attempt waits before the next: 30 seconds, doubling to at most ten minutes, and a restarted bridge retries at once. Only a permanent refusal ends recovery. The service refuses a reply (identity_required) or its reconciliation (discord_reply_reconciliation_origin_required) before touching Discord when it holds no origin for that message, as for messages from before it kept delivery origins. After three refusals the delivery closes as abandoned with that code. Nothing is posted, so the asker never gets a late answer or notice, and it is never retried (VUH-2394).

unrecoveredFailed counts failed messages still owed a confirmed reply or notice, and failed reflects that current unresolved count. A recovery that chose silence has answered. abandoned counts failures closed by a permanent refusal, separately. Durable service turn and confirmed reply receipts repair the projection after lost bridge observations; a recovery reply clears pending and failed instead of leaving old failures displayed. clankie doctor reports unresolved room deliveries as a next step and abandoned ones beside them as a record.

The ingress receipts are the bridge's content-free journal, discord-live-receipts.jsonl. Past 8 MiB it drops each repeated retry receipt but the latest and keeps its newest 4 MiB, weeks of ordinary traffic. A rebuilt inbox seeds only from the last day of it.

A completed handoff's saved answer is replayed to its original room without running its tools again. Before replaying an uncertain write, the authenticated body reads that channel's reply history. A found reply settles the write; a complete scan proving absence allows the existing body-lease recovery to release an abandoned mouth. Failed or truncated reads preserve uncertainty and queue the delivery. The native Discord nonce remains a second duplicate safeguard. POST /v1/discord/presence-actions?reconcile=1 performs only this read, under the same current authenticated transport and live presence claim as writes. It returns {reply: result} or {reply: null}; errors prove neither delivery nor absence. A predecessor still running cannot have its write released this way.

Owner-directed room_turn names its room by conversation ID or by Discord channel ID. A channel ID reaches a channel Clankie has not talked in yet, or one with lane history but no conversation record. The service reads the channel through the active body. It accepts a text, announcement or thread channel in a server in ingressGuildIds (and in ingressChannelIds when that list is set), then creates the room conversation the way ingress does. Other channels are refused with room_fork_channel_unavailable, room_fork_not_a_text_room or room_fork_channel_not_admitted before any turn runs. A proven owner-only audience gets machine tools and private context. Any other room gets a social one-shot with no machine tools and no private or fleet context beyond the brief (ADR 0251). Participant servers keep every other limit: no server management and no posts on a non-owner's request.

file posts with the message whether or not replyTo is set. Its path is relative to, or absolute inside, the operator conversation's working directory. That directory is the service's working directory (the home folder by default), not the calling seat's cwd or scratch folder. A file outside it fails with delivered_file_outside_conversation_workspace. Failures include their concrete code in worker MCP output and the service log.

clankie discord guide CONVERSATION_ID TEXT or TUI /discord guide queues bounded private context for that room's next naturally admitted turn. --clear clears it. Nothing is posted as James and no new turn is started. Clankie decides how to use it and whether to answer. Guidance never upgrades the room's tools or source grants. Writes use POST /v1/discord/room-guidance with an expected revision. The local operator or an active paired device with explicit steer may use this dedicated route. This does not make the device an operator. Consumption rechecks the original author and current source; restart expires guidance whose original authority cannot be proven. The private pending text is visible only to authorized observers, separate from public Discord messages.

clankie discord call and TUI /discord call show the current call, speech, listening/thinking state, consented participant count and active captain handoffs. Before anyone joins, clankie doctor checks the selected provider's listener admission. clankie discord call and /discord call expose the same providerReadiness through GET /v1/discord/room-voice. OpenAI (also Anthropic's OpenAI transcription dependency) and xAI use their real listener port. The probe sends no audio, commits no input and requests no generated response. It closes on acceptance/refusal or after five seconds, with no meaningful usage spend. A private disk cache under CLANKIE_STATE/voice-provider-readiness (default ~/.clankie/voice-provider-readiness) shares results across processes for five minutes, including failures and in-flight reservations. A brokered API key or listener-model change selects a new cache; unreadable cache storage never causes an uncached provider request. There is no force-refresh switch.

providerReadiness reports ready, refused (for example credit_balance_exhausted), missing_key, unconfirmed, unavailable, checking or disabled, with check/expiry timestamps and whether the result was cached. Only a normalized provider code is exposed; never the key or provider message. When voice is enabled, a provider refusal takes doctor's headline ahead of unrelated seat-bridge warnings. ready proves listener admission, not speech output, conversation-model quota, or a successful end-to-end call. No synthesis or paid response is attempted to establish those.

They also show the latest stay's pipelines: whether his hearing and answering are working, broken or unproven, read from that stay's content-free receipts. broken means the realtime provider's session was lost after that leg last worked: no transcription from that speaker (hearing) or completed response (answering) has arrived since. Hearing stays broken while any eligible speaker's listener is lost; pipelines.hearing.brokenUserIds names the affected gateway user ids, also shown by /discord call and doctor. A transcription from another speaker never clears that loss. Consent withdrawal or departure retires that speaker's listener from the report; a later opt-in starts unproven. Legacy loss receipts without a userId stay unattributed and cannot be cleared by someone else's transcription. A scheduled lifetime rotation reconnects on its own, so it counts as a loss but never breaks a leg. The last loss carries its code, how the socket ended (closeReason), the provider's close code and its last error code, never message text. clankie doctor shows the latest stay on its card. Its next step names a call that could not hear or answer, with the provider's error code, only while that call is live or within an hour of its recorded end. A missing endedAt alone is not proof of a live call: idle state suppresses that headline; unknown state requires a loss within the last hour; an active replacement stay does not revive old failures. Historical details remain on the card (VUH-2728, VUH-2765). Join waits up to five seconds for the voice provider to confirm a listener session. When it refuses (for example an account with no credit) or never answers, the join fails as provider_refused, with the provider's code when it gave one, and no stay begins. joinRefusal then shows the newest such refusal since the latest stay, and doctor names it as the next step for an hour. clankie discord call join CONVERSATION_ID joins an existing exact voice room only when the configured owner is currently there. leave, mute_output, and unmute_output require both conversation id and exact stay id. Mute Clankie speech output interrupts and suppresses his speech, including queued speech; it does not mute participants, change consent, pause music, or mute Go Live publishing audio. Unmute never replays suppressed speech. These controls need actual operator authority and preserve the original voice owner's authority and lease incarnation. An unknown or replaced stay fails closed. Handoffs remain model-owned actions, shown as observations rather than a new synthetic trigger. Current speaker identities come only from consented active captures and the room roster. Missing identity observations remain unknown; names are untrusted display text. Mute suppresses immediately, but quiet remains unconfirmed until the exact native playback reports stopped or drained. Lost confirmation keeps status unknown and blocks unmute until exact native proof or the stay ends.

Exact voice words remain in the existing opt-in transcript path. Turning logging off stops reads as well as retention. Room observation and transcripts require operator or active paired terminalObserve; the existing captain transcript consumer remains compatible. Paired requests keep their original device bearer through the relay and recheck grants after awaited work.

GET/POST /v1/discord/settings reads or revision-fences the complete non-secret Discord settings object. Writes require actual operator authority or the narrow Discord-only hosted owner permit. The latter binds the exact encrypted request, tenant, installation and current account connection grant; the body checks that grant live before admission. Discord disconnect/reinstall revokes old permits, without creating a paired device or general operator authority. The hosted paired operator bridge still requires mintedBy=hosted-account-operator and terminalControl; steer does not grant settings access. The canonical schema lives in protocol and is reexported by settings. All canonical fields are editable under TUI /discord → Advanced and existing clankie discord set/clear. The optional setup member contains the shared server/role, fleet and tracking controls from protocol's discord-setup.ts. The TUI, app and hosted dashboard consume that definition; raw IDs remain under Advanced. Server selection uses the connected account's directory. Participant has no setup channel list: Discord permissions and overwrites decide which rooms Clankie can use. Admin grants full control of a dedicated server, with server deletion and ownership transfer refused at the adapter. Machine grants remain separate. ADR 0227 supersedes the four-sentence setup. Managed hosts also return optional managedPolicy status: synced, pending, conflict, unavailable or disconnected. A saved revision is distinct from the revision acknowledged by the edge. Policy writes also compare a monotonic sequence so an old write cannot succeed after a revision changes and reverts. Conflicts reread the edge fence and retry the body's current policy; the persisted settings are reconciled after restart. managedPolicy.revision fingerprints the effective policy, including environment overrides; revision on the settings snapshot still fences the stored owner edit.

The role-correct invitation requests Administrator for Admin and normal member text/thread/voice permissions for Participant. Optional setup.checks reports passed, failed or not_checked from the connected body's own guild and self-member role evidence. Missing evidence is never success. Actual Participant room access follows channel overwrites and Discord's platform permissions. Opening setup, reading checks and saving controls do not post.

discord.guilds[GUILD_ID] holds each server’s role, owners, fleetEnabled, fleetChannelId and trackingLevel. Fleet and tracking are independent per server. Participant fleet/tracking messages use the designated fleetChannelId; Admin can create and place fleet channels. Admin tracks each already bound project in the verified workspace as a channel or forum, with one thread/post per issue. Levels are off, project_updates, project_activity (status changes, milestones, new/finished issues) and all_issues. Private event and destination mappings survive turning tracking off. Unconfirmed mutations stay uncertain without automatic replay. Disabling fleet display retains that server’s room webhooks.

Each server has a role (participant or admin) and owners: Just me (me, the configured ownerUserId), Everyone (everyone), or a Discord role (role with ownerRoleId). discord.guilds records these choices. Admin manages the server but gives its members no machine authority. discord.servers and the single selected fields remain compatibility projections for older clients. Migrating a single choice retains its projection options; other policies start with fleet and tracking off. Tracking deliveries are journaled per guild.

discord.roomSkills adds one named skill to a room. The initial house-hunting adapter reads and maintains that room's household criteria and listing ledger, using the installed skill's listing API and social browser. It accepts structured operations, never a command, executable, arbitrary path or credential. Households are isolated per room; only the recorded legacy migration retains the existing household. A skill room has no shell, fleet tools or owner work context.

The CLI writes the same revision-fenced GET/POST /v1/discord/settings API:

clankie discord server --server SERVER_ID --owners me --role participant --fleet off
clankie discord owners --server SERVER_ID --owners role --owner-role ROLE_ID
clankie discord room-skill --server SERVER_ID --channel CHANNEL_ID --skill house-hunting
clankie discord room-skill --server SERVER_ID --channel CHANNEL_ID --skill off

TUI /discord offers Server owners and options and This room can use, using names from the connected account. Raw policy JSON and IDs stay under Advanced. App and hosted dashboard ownership/skill pickers are follow-ups in their respective repositories; their existing setup controls retain the new fields when saving.

Ownership is resolved on each delivery. Discord role membership comes from the credential-owning body, never from message text. An owner in a mixed room keeps one-shot machine tools, but the context withholds private work and fleet detail, including when an owner asks. Non-owners get social tools and exactly the room's skill adapter. Skill and mixed-room turns use fresh Pi sessions with bounded room history. Revocation rechecks settings at tool execution.

Owner outreach, room relays, tracking messages and fleet webhooks require an owner-only audience. Everyone-owned servers satisfy that ownership condition; private rooms otherwise need live channel overwrites, guild-owner and role permission evidence. Administrator bypasses overwrites, so a non-owner admin role makes the audience unsafe. Missing evidence refuses delivery with discord_owner_audience_required. Being an Admin server alone is insufficient.

Household feedback records the authenticated Discord speaker ID. Old name-attributed rejections stay readable and excluded; explicit owner-confirmed legacy bindings are tracked in VUH-1834. Display names are never proof of authorship.

Legacy systemActorGuildIds and systemActorChannelIds, including environment overrides, no longer grant machine authority. Existing settings normalize on load and persist on the next normal write. Only the recorded house-hunting room migrates to its named skill; other old room grants fail closed. Explicit individual systemActorUserIds remain compatibility grants. See ADR 0251.

POST /v1/discord/setup/test-post remains an explicit owner diagnostic using guildId, channelId and expectedRevision. It requires settings-level operator authority and rechecks configuration, connected account and permission before dispatch. The fixed message disables mentions. A receipt returns posted and a message ID; missing receipts return unconfirmed and are never automatically retried. The CLI is clankie discord setup test-post --channel general. This diagnostic is outside normal setup.

Credentials remain in the credential broker. Environment overrides and settings that require a body restart retain their existing behavior; changing a stored field does not claim the running body already applied it.

The official bot supports both roles. The advanced personal-lab user body keeps its recorded channel consent. Its current opt-in cannot express full-server Admin consent, so server management is refused there. Participant projection posts also need the designated channel inside that existing consent. Selecting Admin does not widen a lab account's recorded scope.

Body evidence uses an authenticated current Discord body session, exact transport and current room allowlists. It cannot grant tools or reconstruct a source owner. Voice health uses accepted input, explicit floor silence/failure, and actual audible playback completion in the same live stay. Provider completion and suppressed output never count as an answered room turn. Unattributed/ambient outputs and unobservable outcomes remain unknown; there is no inferred voice missed counter. Voice control nonces and lease references are host-only and never returned by room snapshots. No endpoint acquires a second body or bypasses recovery.

Discord directory for settings pickers

GET /v1/discord/directory?kind=servers lists servers the connected account can see. kind=channels|roles|people requires guildId. Entries carry their IDs, display names and kinds; limit defaults to 100 (maximum 200), and nextCursor becomes the next request's after. Sort order is Discord ID order, so rename/reordering does not move a cursor. Results describe a live cache; refreshing is appropriate after membership or permissions change.

clankie discord directory and clankie discord directory channels --server ID read the same authenticated API. Use roles or people for those pickers. The active official-bot or user-session body supplies its own gateway view over its existing loopback control server using its brokered bridge bearer. The API uses the authenticated managed provider instead on hosted bodies, scoped to the customer's bound tenant/server and current installation. Changing guildId cannot read another tenant's cache, and replacing the installation invalidates the old body's reads. The hosted dashboard uses its narrow owner permit for the same schema and pagination. The ordinary paired API reuses room Observe authorization, retains the original paired bearer through the relay, and rechecks authority and active body before returning data.

state distinguishes connected, disconnected, partial, and unavailable; reason explains an incomplete or unavailable read. An empty connected server list means the account is in no known servers. A disconnected or failed read never masquerades as that result. Cached people are explicitly partial: no new privileged member intent or member enumeration is requested. Channels are partial because archived threads are not enumerated; the user-session cache also omits threads rather than guessing membership. Missing permission evidence omits the affected channel and reports permissions_unknown.

Bot channels use discord.js permission calculation plus private-thread membership. User-session channels use the account's delivered roles, own membership and Discord’s documented overwrite order, with BigInt permission flags. Hidden channel IDs and names are omitted. The directory never falls back to the other account, reads stored Discord credentials, posts a message, joins a server, or grants computer access. Names remain untrusted display text.