packages/discord-presence-core/README.md

@clankie/discord-presence-core

Transport-neutral Discord participation. Everything here is blind to whether Clankie is wearing the official bot or the personal-lab user session, which is what lets both bodies be one character (ADR 0024, ADR 0048).

ModuleResponsibility
presence-sessionGateway/voice phase lifecycle, typed phase events, act-tool revoke fence
presence-action-advertiserRetains the live catalogue and forwards phase as an execution fence
discord-restShared bounded REST writes for both Discord transports
captain-action-controlAuthenticated local control requests from captain tools
text-ingressNormalises gateway messages into bounded, allowlist-gated captain turns, images included (ADR 0081)
room-textShared admission for typed room experience across text, voice, and game threads (ADR 0124)
voice-addressPhonetic name-mention: opens a session; the offered turn decides whether to speak (ADR 0119)
voice-floorDormant ↔ engaged floor: wake, offer (silence-ok), listen, decay, volition (ADR 0119)
realtime-sessionInjectable OpenAI/xAI realtime boundaries: transcription, conversation, and ask_clankie round trips
elevenlabs-ttsInjectable ElevenLabs multi-context streaming-TTS boundary (ADR 0070)
external-voicePairs a text-modality realtime session with a TTS mouth behind the one conversation port (ADR 0070)
voice-sessionVox-backed attributed speech/text input, shared group floor, deliberate barge-in and playback
voice-compositionShared voice dependency assembly for bot and user-session bodies
voice-controlLocal join/leave control request handling
voice-musicShared bounded queue and transport controls
voice-ingressRoutes one ask_clankie handoff to the continuing discord_voice captain lane
voice-consentEphemeral consent under explicit or owner-selected presence policy; opt-out always wins
voice-audioShared provider/local-voice PCM helpers and content-free RMS measurement
receipt-storeAppend-only, content-free receipts for both planes

Voice receipts use the discord.voice.* vocabulary — joined, consent, utterance, text_input, floor, response, volition, overlap, interrupted, failed, left — and every field is a content-free scalar: ids, counts, durations, and typed outcomes, never transcript, prompt, audio, or PCM.

Rules

  • Never import discord.js. A bot-shaped client is a transport detail and belongs in the app that owns that transport. The bot bridge uses discord.js; the user-session bridge uses a bounded raw gateway plus fetch.
  • Derive the lane address from discordPresenceLaneAddress. It is keyed by where the conversation happens (discord:<guildId|dm>:<channelId>), never by which transport observed it. A transport-local identifier would fork one conversation into two captain lanes and split the character in half.
  • transportKind is configuration, not inference. Both ingress paths take it from their host process; neither guesses.
  • Visuals are selected here, never in a bridge. Both transports map their raw attachment and gifv embed shapes and call selectInboundImageAttachments, so one rule decides what he can be shown. A policy that admitted an image on one body and not the other would be two characters, not one (ADR 0081).
  • This package never fetches attachment bytes. It carries references; the clankie service resolves them at the last hop before the model.
  • The active voice room owns its attached text chat. Text-only messages in that exact guild/channel enter VoiceFloor, where the realtime room persona speaks, hands off, or stays silent. The bridge does not also launch a text captain turn (ADR 0124).
  • Voice identity stays attached to a gateway stream. Speakers use separate transcription inputs. Only attributed JSON transcript items converge into the shared engaged conversation; overlapping raw audio is never interleaved and guessed after the fact. An open session hears consented speech; the floor decides who gets a spoken turn (ADR 0119).
  • Speaker listeners are bounded. An inactive per-speaker transcription session closes after two minutes and reopens on demand. At 25 retained listeners, the least recently active idle listener is evicted before another opens; active captures and pending transcript correlation are never evicted.
  • Vox is the sole voice media owner. Both media-enabled Discord bodies use native Vox capture, TTS playback, DAVE readiness, and audible music; a text-only bot does not spawn it. This package owns policy and correlation only; ordinary leave clears Vox's primary role without closing the process or touching its stream-watch/publish roles. TTS is audible only after Vox reports started; buffered means queued and does not occupy the floor, while drained means finished PCM and trailing frames crossed the sender. Fresh app-level readiness evidence records mediaOwner: vox; the joined receipt separately proves positive role-scoped DAVE. A leave qualifies only after the account gateway confirms detachment. See ADR 0128.

Consumers