Source-backed service audit

One coordinator.
Many players.
Explicit identity.

BarBot can become the single control surface for multiple Discord music identities, but the current runtime is structurally one player per guild. The lowest-disruption path is additive: repair Phase 0, introduce assignment and session identity, isolate transport state per player agent, then migrate controls.

Human decisions requiredStatic audit complete8 Phase-0 repairs5 migration phases6 primary touchpoints
Observed in sourceProposed designRuntime or policy decision
01 / DIAGNOSIS

The blocker is identity, not Discord commands.

The current implementation routes Discord components, HTTP requests, WebSocket topics, guild state, Lavalink players, timers, votes, and cyphers through guild identity.

“Guild ID selects the system.” That works for one player per guild. It cannot deterministically select one of several same-guild cyphers.
1Discord token/client at startup
1BotServer state bundle per guild
8prerequisite correctness/security repairs
3new routing keys: assignment, session, epoch
02 / TOPOLOGY

Current and target topology

Toggle the system view. The migration preserves the existing single-player path as an explicit compatibility assignment instead of rewriting playback wholesale.

CURRENT · GUILD-KEYED CONTROL + SINGLE IDENTITY config.botTokenone credentialDiscord Clientone logged-in identityLavalinkClientone Shoukaku bindingPlayerControllerguild-keyed mapsControl surfacescomponents · HTTP · WSroute via guildIdHelpers / BotServerqueue · vote · cypherone bundle per guildAdapter globalsplayer · seek · snapshotMap<guildId, state>Approved voice channelalso becomes routing surrogate Collision zoneA second player inthe same guild hitsthe same BotServer,player registry,controller state andcontrol topic.
current collisionexplicit routingenforced invariant
03 / COLLISION MAP

Five state collisions define the refactor.

The core work is extracting ownership from the guild aggregate while keeping current single-player behavior available through a compatibility projection.

Playback bundle

BotServer · guildIdRuntimeSession · sessionId + epoch

Profile policy

implicit guild settingsVoiceProfile · revision

Identity capacity

static bot IDsIdentityRecord + Lease

Transport

module globals · guildIdPlayerAgent-local state

Control context

guild / current voiceInteractionContext
SG-1 · VERIFIED

One BotServer per guild

Approved channel, queue, vote, cypher, and autopause are one shared bundle.

src/models/Server.ts:72
SG-2 · VERIFIED

Guild-keyed adapter state

Player, snapshot, and seek maps collide; join returns the existing guild player.

src/services/lavalink/index.ts:80,318
SG-3 · VERIFIED

Guild-keyed controller maps

Readiness, player, voice, and recovery state cannot represent two identities independently.

src/services/lavalink/PlayerController.ts:33
SG-4 · VERIFIED

Guild-only control routing

Components, HTTP, and WebSocket topics cannot select one concurrent session.

src/menus/handler.ts:59 · src/server/websocket.ts:45
SG-5 · OPEN POLICY

Same-channel cyphers

Muting and autopause operate on a singular Discord voice channel. Independent cyphers sharing one call need an explicit product rule.

src/cogs/CypherRotation.ts:35
04 / PHASE ZERO

Repair the foundation before adding capacity.

These issues already exist in the current single-player system. Multi-identity orchestration would amplify them, so they are prerequisites, not scope creep.

P0-A

HTTP authorization

Arbitrary bearer-shaped values and client-selected X-User-ID are accepted; role enforcement is not mounted across guild routes.

src/server/middleware.ts:15 · src/server/index.ts:44
P0-B

WebSocket authorization

Caller-selected guild subscriptions lack demonstrated handshake and topic authorization.

src/server/websocket.ts:17,45
P0-C

Durable mutation boundary

Routes directly mutate serializable fields while scheduled persistence saves only dirty servers.

src/server/routes/settings.ts:39 · PersistenceService.ts:137
P0-D

Typed startup restoration

Reconnect precedes hydration; restored channel IDs are not resolved into the typed live references playback consumes.

src/index.ts:264 · Serialization.ts:113
P0-E

Lifecycle parity

Dashboard and Discord controls take different paths for skip, cypher start, and voice disconnect.

routes/musicplayer.ts:127 · menus/actions.ts:169
P0-F · STRONG INFERENCE

Recovery coordination

Controller and MusicPlayer can both react to a close without one coordinator or activation fence.

PlayerController.ts:155 · MusicPlayer.ts:171
P0-G

BarAPI schema parity

BarBot serializes fields not declared by the BarAPI record model, risking silent loss on round-trip.

Serialization.ts:77 · BarAPI/Models/discord_models.py:90
P0-H

Pure resolution

Looking up a guild server starts background refill, coupling state resolution to lifecycle side effects.

src/services/Helpers.ts:333
05 / REQUEST PATHS

Where identity is selected today.

The same hidden assumption appears in startup, Discord components, dashboard routes, WebSocket subscriptions, and recovery. This table identifies the exact resolution point and the seam that replaces it.

Startupindex.ts:266-275
Serialization.ts:113-120
Voice reconnect runs before persisted server state is hydrated. Channel IDs are restored into side-channel fields, not resolved live channel references. Seam: hydrate, resolve, then reconcile desired assignments.
Discord componentmenus/handler.ts:59-71customId selects an action, then interaction.guild selects the BotServer. Seam: opaque contextRef resolves an InteractionContext before handler dispatch.
Manual voiceroutes/voice.ts:43-56
menus/actions.ts:176-182
The approved voice channel is durable policy, live target, and permission check at once. Seam: ChannelAssignment owns the target; member voice can only permit or deny.
Dashboard + WSmiddleware.ts:17-47
websocket.ts:17-54
Caller-provided identity and guild subscriptions can reach guild-scoped state. Seam: verified actor claims plus authorized assignment/session topics.
Playback actionroutes/musicplayer.ts:127-140
menus/actions.ts:169-201
Dashboard skip stops the player; Discord skip may advance playback or start a vote. Seam: one lifecycle command service used by every control surface.
RecoveryPlayerController.ts:150-195
MusicPlayer.ts:171-190
Controller and cog both react to a transport close. Seam: one RecoveryCoordinator per session, fenced by activation epoch.
sequenceDiagram
  participant U as Discord / HTTP / WS
  participant R as Interaction Resolver
  participant A as Assignment Store
  participant P as PlayerAgent
  participant S as RuntimeSession
  U->>R: mutation + contextRef
  R->>A: resolve assignmentId
  A-->>R: sessionId, identityId, activationEpoch
  R->>R: authorize actor, guild, surface, epoch
  R->>P: command(sessionId, expectedEpoch)
  P->>S: apply only when epoch matches
  S-->>U: event + assignmentId + sessionId + epoch + seq
  Note over R,S: Member voice may deny an unsafe action. It never retargets it.
          
Proposed design Every mutation crosses the same resolver and epoch fence, regardless of control surface. Ctrl/Cmd + wheel zooms; drag pans.
06 / TARGET MODEL

Six boundaries, three routing keys.

This model matches the product: manual assignment, occupancy-triggered profiles, scheduled behavior, reserve-backed dynamic channels, and one command surface coordinating several identities.

InteractionContext
assignmentId
sessionId
expectedEpoch
  • actor claims
  • surface
  • voice safety
ChannelAssignment
assignmentId
  • channel binding
  • capacity mode
  • lifecycle
VoiceProfile
voiceProfileId
  • occupancy
  • empty policy
  • schedule
IdentityLease
identityId · leaseId
  • exclusive claim
  • heartbeat
  • activation epoch
PlayerAgent
identityId
  • Discord client
  • Lavalink client
  • local caches
RuntimeSession
sessionId · epoch
  • queue + vote
  • cypher + timers
  • recovery gate
01. Guild ID alone never selects an application playback session.
02. Every mutation resolves, authorizes, and validates expected epoch before execution.
03. One identity has at most one active lease under the approved policy.
04. Timers, callbacks, reconnects, and events carry activation epoch.
05. Voice state may deny an unsafe action; it never retargets an assignment.
06. Durable desired state reconciles idempotently against Discord observations.
07 / CONTRACTS

Concrete records and control contracts.

Everything below is a proposed additive contract, not a claim about current implementation. The names make ownership testable and give M1 through M3 a stable target.

erDiagram
  GUILD ||--o{ VOICE_PROFILE : owns
  GUILD ||--o{ CHANNEL_ASSIGNMENT : owns
  VOICE_PROFILE ||--o{ CHANNEL_ASSIGNMENT : configures
  CHANNEL_ASSIGNMENT ||--o| RUNTIME_SESSION : activates
  IDENTITY_RECORD ||--o{ IDENTITY_LEASE : grants
  IDENTITY_LEASE ||--|| RUNTIME_SESSION : fences
  PLAYER_AGENT ||--o{ RUNTIME_SESSION : hosts
  VOICE_PROFILE {
    string voiceProfileId PK
    string guildId
    int revision
    bool enabled
    json occupancyPolicy
    json schedulePolicy
  }
  CHANNEL_ASSIGNMENT {
    string assignmentId PK
    string guildId
    string voiceChannelId
    string voiceProfileId FK
    string capacityMode
    string lifecycle
  }
  IDENTITY_RECORD {
    string identityId PK
    string credentialRef
    string health
    string capacityState
  }
  IDENTITY_LEASE {
    string leaseId PK
    string identityId FK
    string assignmentId FK
    int activationEpoch
    datetime expiresAt
  }
  RUNTIME_SESSION {
    string sessionId PK
    string assignmentId FK
    string identityId FK
    int activationEpoch
    string state
  }
          
Proposed design Durable policy and bindings stay separate from live queue, timer, and transport state.
Core TypeScript shape proposal
interface ChannelAssignment {
  assignmentId: string;
  guildId: string;
  voiceChannelId: string;
  voiceProfileId: string;
  capacityMode: "fixed" | "reserve";
  lifecycle: AssignmentState;
}

interface RuntimeSession {
  sessionId: string;
  assignmentId: string;
  identityId: string;
  leaseId: string;
  activationEpoch: number;
  state: SessionState;
}

Queue, vote, cypher, timers, recovery, and observed transport belong to RuntimeSession, not ChannelAssignment.

Resolved mutation context proposal
interface InteractionContext {
  actorId: string;
  actorClaims: ActorClaims;
  guildId: string;
  assignmentId: string;
  sessionId: string;
  expectedEpoch: number;
  surface: "discord" | "http" | "ws";
  memberVoiceChannelId?: string;
}

The member voice field is a safety input. It cannot choose or replace assignmentId or sessionId.

Discord component ID proposal
bb:<contextRef>:<action>

contextRef -> {
  assignmentId,
  sessionId,
  expectedEpoch,
  actorScope,
  expiresAt
}

Use a short opaque reference with a server-side TTL record. Do not pack the complete routing context into the component ID.

Session event envelope proposal
{
  "type": "session.playback.changed",
  "guildId": "...",
  "assignmentId": "...",
  "sessionId": "...",
  "activationEpoch": 14,
  "seq": 208,
  "data": { "state": "playing" }
}

Consumers discard lower epochs and non-monotonic sequence values. Subscription authorization is checked against assignment/session scope.

Proposed routePurposeRequired contextLegacy behavior
POST /api/guilds/:guildId/assignmentsBind a profile and channelVerified actor + admin/role claimNo direct legacy equivalent
PATCH /api/guilds/:guildId/assignments/:assignmentIdChange profile, capacity, or lifecycle intentassignmentId + expected revisionSettings routes become wrappers
POST .../:assignmentId/activateClaim lease and create sessionexpected assignment revisionJoin route resolves compatibility assignment
POST .../:assignmentId/deactivateDrain session and release leasesessionId + expectedEpochDisconnect route awaits command completion
GET /api/guilds/:guildId/identitiesInventory, health, and capacityVerified operator scopeConfigured bot IDs remain read-only migration input
08 / LIFECYCLE

Lease first, session second, every callback fenced.

The activation epoch is not bookkeeping. It is the mechanism that makes delayed timers, reconnect callbacks, and WebSocket events harmless after ownership changes.

stateDiagram-v2
  direction LR
  [*] --> Draft
  Draft --> Bound: validated
  Bound --> Activating: triggered
  Activating --> Active: ready
  Activating --> Error: failed
  Active --> Draining: deactivate
  Active --> Fenced: superseded
  Draining --> Inactive: released
  Fenced --> Inactive: stale work dropped
  Error --> Bound: retry
  Inactive --> Activating: desired active
          
Proposed design Reconciliation is idempotent. A stale activation can observe state, but it cannot mutate the new session.
CLAIMLease creation is exclusive for identityId under the selected policy. Increment activationEpoch on every new claim.
EXECUTECommands compare expectedEpoch with the active lease before touching queue, player, timer, or cypher state.
RECOVEROne RecoveryCoordinator owns rejoin. Replay versus park is a policy injected into that coordinator.
RELEASEDrain transport, record outcome, release lease, then publish the final event. Crash expiry fences the old owner first.
Observed reason for the fence: PlayerController.ts:150-195 and MusicPlayer.ts:171-190 both contain recovery behavior. The audit proves competing paths, not that corruption always occurs.
09 / PROCESS MODEL

Start with one Discord identity per process.

This is an implementation recommendation with a deliberate reversal point. It reduces the M2 blast radius while duplicate-guild Lavalink behavior remains a runtime unknown.

ConcernOne identity per processSeveral clients per process
State isolationProcess boundary naturally contains client, adapter, controller, cache, and failure state.Every singleton and module-global map must be made instance-safe before the second client starts.
Operational costMore processes and health checks; simple ownership and restart semantics.Fewer processes; denser memory use and larger shared blast radius.
Migration fitPreserves current one-client startup inside each PlayerAgent worker.Requires startup and dependency injection refactor during transport isolation.
Lavalink unknownCan test separate identity-local clients against one or separate nodes.Still requires proof that the deployed client/node combination distinguishes duplicate guild players.
Failure recoveryRestart one identity without disturbing siblings.Client or event-loop faults may disturb every identity in the process.
Initial verdictRecommended for M2Revisit only after measured memory and operations pressure
Runtime probe before M2 approval

Start two identity-local clients for the same guild, join distinct channels where Discord permits it, play independent tracks, then verify player keys, voice updates, reconnects, leave behavior, and event attribution. Repeat against the exact deployed Lavalink and Shoukaku versions.

10 / MIGRATION

Add seams before moving behavior.

The sequence separates existing correctness repairs from product capability, then preserves the old route until evidence supports cutover.

P0

Repair

Auth, schema parity, startup, lifecycle operations, pure lookup, one recovery owner.

Gate: security denials + round-trip + restoration parity
M1

Model

Add profiles, assignments, identity inventory, leases, and a legacy compatibility projection.

Gate: no behavior regression for existing guilds
M2

Isolate

Instance-scope adapter state; introduce PlayerAgent and RuntimeSession; retain one-agent startup.

Gate: two same-guild sessions isolate all state
M3

Route

Assignment/session controls, opaque component context, authenticated session topics, guild wrappers.

Gate: moving voice channels never retargets
M4

Orchestrate

Occupancy, schedules, reserve leasing, dynamic channels, dashboard identity/capacity management.

Gate: contention and cleanup are deterministic
11 / TOUCHPOINTS

The smallest meaningful edit surface

These are the primary seams, not every downstream callsite. The highest-disruption work is confined to transport state isolation in M2.

PhaseFile / symbolCurrent responsibilityRequired changeDisruption
M1models/Server.ts:60-104
BotServer
Guild runtime bundleKeep as compatibility projection; move queue, vote, cypher, and timers into RuntimeSessionMedium
P0 / M1services/Serialization.ts:76-120Legacy durable server shape and ID side channelsVersioned profiles, assignments, inventory, and leases; resolve typed channels before reconcileMedium
P0 / M1services/PersistenceService.ts:48-85,137+Bulk hydrate and dirty-only syncOne mutation boundary marks durable aggregates dirty and writes schema-versioned recordsMedium
M2services/lavalink/index.ts:94-96,316-394
activePlayers · joinChannel
Module-global snapshot, seek, warning, and player registriesConstruct an AdapterManager per PlayerAgent; key live state by session inside that ownerHigh
M2lavalink/PlayerController.ts:33-38,150-195Guild-keyed transport/controller and recoveryPlayerAgent-local controller; delegate recovery decisions to session coordinatorHigh
P0 / M2index.ts:266-275,302-313One client, one Lavalink binding, reconnect before hydrateHydrate-first bootstrap; preserve this startup inside one-identity PlayerAgent workersMedium
M3menus/handler.ts:55-71Guild-scoped component routingResolve opaque contextRef, authorize, validate epoch, then dispatchMedium
P0 / M3server/middleware.ts:17-47API key, arbitrary bearer, or caller user IDVerified credential to ActorClaims; remove caller-selected identity trustHigh
P0 / M3server/websocket.ts:17-74Unauthenticated guild-topic subscriptionsAuthenticated assignment/session topics with epoch and sequence envelopeMedium
P0 / M3routes/musicplayer.ts:127-140
menus/actions.ts:169-201
Divergent skip lifecycleRoute both surfaces through one session command serviceMedium
P0routes/voice.ts:59-62Fire-and-forget disconnectAwait deactivation and expose drain failureMedium
P0services/Helpers.ts:345-363Resolution starts queue background refillMake lookup pure; move prewarm to explicit lifecycle hookLow
12 / VALIDATION

Evidence gates, not hopeful milestones

The audit was static by design. These are the executable gates that convert architectural claims into deployment confidence.

Integration

Authorization

  • forged bearer denied
  • forged identity denied
  • wrong guild/role denied
  • unauthorized WS denied
Integration

Startup restoration

  • hydrate before reconcile
  • resolve typed channels
  • idempotent reconciliation
Integration

Lifecycle parity

  • menu/API skip match
  • cypher start match
  • disconnect awaits leave
Internal harness

Same-guild sessions

  • players + queues isolated
  • votes + timers isolated
  • stale epochs rejected
Integration

Explicit routing

  • actor movement never retargets
  • voice context may only deny
Integration

Profiles + capacity

  • lease contention deterministic
  • crash cleanup releases
  • schedules reconcile
  • dynamic cleanup audited
13 / DECISIONS

The architecture is clear; these policies are not.

The final status is human-decision-required because product and operations choices determine lease semantics, process topology, and dynamic-channel behavior.

What credential and authenticated Discord identity model should protect dashboard and WebSocket control?
Multiple Discord clients in one process, or one identity per process?
Are configured bot IDs reserve inventory or legacy filtering data?
What are lease exclusivity, expiry, override, crash-cleanup, and audit rules?
After transport recovery, who decides replay versus park?
Are dynamic channels created or claimed, and how are permissions and cleanup handled?
What occupancy, pause/leave, recurrence, and timezone defaults apply?
May two independent cyphers share one voice channel?
Do legacy compatibility assignments activate automatically or by admin opt-in?
Does the deployed Lavalink version support duplicate guild players across identity-local clients?
14 / ADVERSARIAL

What survived the model panel

Kimi, GLM, and DeepSeek attacked the Terra draft; two further Terra agents verified the challenges against source.

AcceptedAuth gaps, dirty-state bypass, startup ordering, lifecycle divergence, schema loss, and global-state hazardsDirect static evidence supports the findings.
Partially acceptedCompeting close handlers prove downstream Shoukaku corruptionThe two recovery paths are observed; corruption itself is not statically proven.
RejectedAPI-key support makes dashboard routes safeArbitrary bearer and X-User-ID paths remain accepted.
RejectedStartup reconnect is always a no-opOrdering and hydration defects are verified; a universal runtime outcome is not.
UnresolvedCustom-ID limits and duplicate-guild transport support are knownThese require platform/runtime validation, not static inference.