veratown-prototype

Bot Runtime Architecture

This document describes the implemented runtime as it exists in src/app/start.ts, not a proposed target design. Feature-level behavior and BC packet acknowledgement details remain in their respective documents. Feature configuration ownership, startup defaults, and bot command sequences are catalogued in FEATURE-CONFIGURATION.md.

The optional web administration service is a separate operator-script composition root, not another bot plugin. It has no BC connection. It selects the same effective stored map as missing-room creation, validates configuration through feature-owned contracts, and exposes explicitly enabled, administrator-only configuration writes with concurrency checks and durable sanitized audits. Existing feature reload paths remain responsible for runtime adoption. A persisted web write is neither a BC acknowledgement nor an observation; live-map capture and live actions remain owned by the bot.

Administrator recovery reuses these validated mutations inside snapshot-read MongoDB transactions, with optimistic versions and paired audits. Configuration snapshots exclude secrets and player runtime data; selected-record restores never delete unrelated records. Heartbeat reports carry an optional logical roomKey for the web projection; legacy unscoped reports are omitted. Neither a recent heartbeat nor a successful restore proves feature adoption or live BC state.

Configuration consumer receipts

The composition root owns a bounded ConfigurationAdoption tracker and includes its optional configurationAdoption projection in every heartbeat write. scripts/admin-web/recovery.ts validates this field and supplies explicit unknown entries for legacy/malformed/missing reports. Reports remain room-scoped by the existing heartbeat roomKey; they never compare persisted documents to guess adoption. Each instance is displayed separately with its process freshness, including stale, stopped, disconnected, and clock-skewed reports.

Consumer identifier Actual reporting boundary Revision meaning
shared-game-config GameConfigRuntime.apply() installs the in-memory snapshot Shared document revision, not downstream game/session adoption
doors DoorFeaturePlugin.start() completes, or its shared admin/change-stream reload replaces running door/location configuration Unknown (null): the aggregate has no authoritative revision
room-creation-template The serialized reload in start.ts loads the latest prototypeRoomConfig settings (legacy veratownRooms fallback) and effective map before calling BcClient.setRoomCreationDefinition() Unknown (null); creation-template loading is separate from the supervisor’s journaled live-room settings reconciliation
command-aliases CommandAliasResolver validates/replaces its lazy cache during alias/help lookup Shared document revision used by that cache
casino CasinoGameService creates a new slots or roulette session with the validated rules Shared revision adopted by that new session; older rounds keep their own saved rules
shop ShopService command access checks or ShopFeaturePlugin location interactions configure access policy Shared revision used for that interaction, not all quotes/favors
location-entry-behavior Location messaging/disabled gating in LocationEntryBehaviorPlugin, or validated itemset selection in LocationEntryBehaviorService Shared policy revision, not an aggregate of referenced itemsets
shower ShowerService evaluates a real entry using settings and selects that session’s timing Shower-settings revision, not songs or existing sessions
truth-dare, hot-seat, dilemma, anonymous-whisperer A new active game session is successfully persisted with its dedicated config snapshot Per-session config revision; not the shared game-config revision, default provenance, or every active session
staged-table A new multiplayer lobby or solo session is successfully persisted with its dedicated config snapshot Per-session config revision; configuration provenance remains unknown
trick-or-treat A new route starts from validated settings and a ready multiplayer roster (or a player-started solo round); completed eligible rounds settle opt-in consequence progression Per-session config revision; map entries require observed position_update; appearance consequences require observed BC state
appearance AppearanceItemSetService validates an enabled itemset for a real application attempt No aggregate revision: itemset revisions belong to individual sets; BC outcome is separate
helper-bots A helper worker passes identity preparation, loads polling settings, and becomes ready No settings revision or default provenance is currently available; supervisor task handling remains uninstrumented

Shared snapshot changes mark the supervisor’s alias, casino, shop and location entry behavior consumers pending_next_interaction; persistence alone does not mark them loaded. Casino adoption waits for a newly created game session (not startup validation, help, entry grants, policy reads, or an existing round). Shop waits for a command or location interaction applying its access policy; disabled policies are applications too. Location entry behavior waits for policy-driven messaging, disabled gating, or itemset selection. Successful configuration application is distinct from a subsequent BC action’s success. If an older in-flight interaction completes while a newer revision is pending, its receipt does not consume that newer pending revision. Shower has no settings watcher, so it reports no speculative pending revision: real entries apply settings, while enabled-only mid-session checks never claim adoption of new timing or songs. Removed alias settings are applied as a disabled cache on lookup. Invalid settings fail closed and report configuration_invalid. Helper-only processes do not claim adoption by supervisor-only consumers. Party-game session receipts are created only after a new session uses its configuration; they do not infer that revision one means a seeded default, nor claim coverage of all active sessions. Appearance reports itemset selection for an application attempt, not an observed BC result. The admin runtime view compares only revisions sharing the same process-local GameConfigRuntime revision space; it cannot establish that the cache matches the persisted document. Independent game, itemset, and helper settings remain explicitly non-comparable or unknown.

Receipts contain last-loaded revision/time, optional pending revision, and only fixed reload-error identifiers/timestamps. They never contain configuration, error messages, access codes, or raw document identifiers. Reload failures retain the last-successful receipt, not a claim that it is still active: shared configuration and failed door reads retain their prior runtime configuration; invalid aliases disable the cache and failed template reloads disable room creation as before. A successful subsequent load clears the reported error. Repeated lookups/unchanged shared revisions do not advance the last-loaded time.

Helpers, appearance/itemsets, and uninstrumented party-game consumers remain explicitly unknown. Staged Table reports the revision used by each new lobby or solo session, without inferring whether it came from startup defaults. Casino receipts describe the most recent newly created session, not every concurrent session. Shop quotes/favors, location entry itemsets, shower songs and pre-existing shower timing are not covered by their feature’s receipt. A failed configuration read/validation preserves last-loaded details, but does not fall back to those details to authorize a new interaction. No receipt proves BC dispatch, acknowledgement, observation, or account persistence.

At a glance

flowchart TD
    Entry[src/index.ts] --> Main[src/app/start.ts<br/>composition root]
    Main --> Config[src/app/config.ts<br/>Zod bootstrap + effective config]
    Main --> PluginHost[BotPluginHost]
    Main --> RuntimeRole[Validated runtime role]
    Main --> BotProfiles[MongoBotInstanceProfileRepository]
    BotProfiles --> BotInstances[(prototypeBotInstances)]
    BotProfiles --> BotInstanceRevisions[(prototypeBotInstanceRevisions)]
    Main --> BcClient[BcClient<br/>single Socket.IO connection + send queue]
    Main --> ApiPlane[BcApiPlane<br/>typed packet contracts/read-backs]
    Main --> Coordinator[ActionCoordinator]
    Main --> HelpContext[HelpContextPort<br/>room membership + revision snapshot]
    Main --> AliasResolver[CommandAliasResolver<br/>revision-cached room config]
    Coordinator --> Journal[MongoActionJournal]
    Main --> Mongo[(MongoDB<br/>target-specific database)]
    BotProfiles --> Mongo
    Main --> MutationLease[MongoCharacterMutationLease]

    PluginHost --> Doors[DoorFeaturePlugin]
    PluginHost --> LocationMessages[LocationEntryMessagePlugin]
    PluginHost --> LocationAppearance[LocationAppearanceTriggerPlugin]
    PluginHost --> EmergencyRelease[EmergencyReleasePlugin]
    PluginHost --> LocationEntryBehavior[LocationEntryBehaviorPlugin]
    PluginHost --> SafetyStop[SafetyStopPlugin]
    PluginHost --> WorldAdmin[WorldAdminPlugin]
    PluginHost --> Communication[CommunicationPlugin]
    PluginHost --> Status[PrototypeStatusPlugin]
    PluginHost --> Appearance[AppearancePlugin]
    PluginHost --> Whisperer[AnonymousWhispererPlugin]
    PluginHost --> RoulettePresentation[RoulettePresentationRouter]
    PluginHost --> Casino[CasinoFeaturePlugin]
    PluginHost --> Shop[ShopFeaturePlugin]
    PluginHost --> TruthDare[TruthDarePlugin]
    PluginHost --> HotSeat[HotSeatPlugin<br/>disabled by room config]
    PluginHost --> StagedTable[StagedTablePlugin<br/>disabled by room config]
    PluginHost --> Dilemma[DilemmaPlugin]
    PluginHost --> Helpers[HelperBotsPlugin<br/>supervisor or worker mode]
    PluginHost --> Abcl[AbclFeaturePlugin<br/>supervisor only]
    PluginHost --> Shower[ShowerFeaturePlugin<br/>opt-in]
    Main --> MapTeleport[MapTeleportAction]
    EmergencyRelease --> MapTeleport
    Main --> PositionBus[MapPositionObservationBus<br/>five-second bounded cache]
    Main --> AppearanceBus[SharedAppearanceObservationBus<br/>per-observer appearance cache]
    Main --> ShowerRepository[MongoShowerRepository]
    Doors --> PositionBus
    Helpers --> ObservationSession[Optional supervisor/helper HTTP session]
    Abcl --> AbclHelperTasks[Typed ABCL helper tasks]
    Abcl --> AbclConsent[MongoAbclParticipantRepository]
    AbclHelperTasks --> Helpers
    Helpers --> AbclState[Short-lived ABCL decisions and sanitized signals]
    AbclState --> Abcl
    MutationLease --> Mongo
    Helpers --> TaskNotifier[HelperTaskNotifier<br/>hint after durable enqueue]
    TaskNotifier --> ObservationSession
    ObservationSession --> TaskWakeups[HelperTaskWakeups<br/>coalesced queue check]
    TaskWakeups --> Helpers
    ObservationSession <--> PrivateStream[Railway private WireGuard stream]
    PrivateStream <--> HelperPositionBus[Helper-local MapPositionObservationBus]
    AppearanceBus <--> PrivateStream
    MapTeleport --> HelperPositionBus
    PluginHost --> Router[CommandRouterPlugin]
    Main --> Economy[EconomyService<br/>shared application capability]
    Economy --> EconomyPort[EconomyPort]
    Economy --> EconomyRepository[MongoEconomyRepository]
    EconomyRepository --> Mongo
    Casino --> EconomyPort
    Main --> WhispererRepository[MongoWhispererRepository]
    WhispererRepository --> Mongo
    Main --> TruthDareService[TruthDareService]
    Main --> TruthDareRepository[MongoTruthDareRepository]
    Main --> StagedTableRepository[MongoStagedTableRepository]
    Main --> StagedTableService[StagedTableService]
    StagedTable --> StagedTableService
    StagedTable --> SafetyStop
    StagedTableService --> StagedTableRepository
    StagedTableRepository --> Mongo
    TruthDare --> TruthDareService
    TruthDareService --> EconomyPort
    TruthDareService --> TruthDareRepository
    TruthDareRepository --> PartyConfigs[("prototypePartyGameConfigs")]
    TruthDareRepository --> TruthPrompts[("prototypeTruthDarePrompts")]
    TruthDareRepository --> TruthSessions[("prototypeTruthDareSessions")]
    HotSeat --> EconomyPort
    HotSeat --> HotSeatService[HotSeatService]
    HotSeatService --> HotSeatRepository[MongoHotSeatRepository]
    HotSeatRepository --> Mongo
    Main --> DilemmaRepository[MongoDilemmaRepository]
    DilemmaRepository --> Mongo
    Dilemma --> DilemmaRepository
    Dilemma --> EconomyPort
    Dilemma --> CommunicationApi
    Shop --> LocationBus : observed shop-region membership
    Shop --> Parser
    ShopService --> CommunicationApi
    Main --> ShopService[ShopService]
    Main --> ShopRepository[MongoShopRepository]
    Shop --> ShopService
    ShopService --> EconomyPort
    ShopService --> AppearanceApi
    ShopService --> ShopRepository
    ShopRepository --> Mongo
    Main --> HelperTaskRepository[MongoHelperTaskRepository]
    Helpers --> HelperTaskRepository
    RoulettePresentation --> HelperTaskRepository
    ShopService --> HelperTaskRepository : optional reviewed shop templates
    HelperTaskRepository --> HelperTasks[(prototypeHelperTasks)]
    HelperTaskRepository --> Mongo
    Helpers --> CommunicationApi
    Helpers --> MapTeleport : optional configured startup position
    Casino --> RoulettePresentation : open, spin, and settled-result stages

    Doors --> MapRuntime[VeratownMapRuntime]
    Doors --> DoorAdminService[DoorAdminService]
    DoorAdminService -->|persistence port| MapRepository[MapRepository]
    Main --> MongoMapRepository[MongoMapRepository]
    MongoMapRepository --> MapRepository
    MongoMapRepository --> Mongo
    Doors --> DoorCommands[Door command suites]
    LocationMessages --> LocationBus
    LocationAppearance --> LocationBus
    LocationAppearance --> AppearanceItemSets[AppearanceItemSetService]
    EmergencyRelease --> LocationBus
    EmergencyRelease --> AppearanceItemSets
    EmergencyRelease --> MapTeleport
    EmergencyRelease --> Parser
    EmergencyRelease --> CommunicationApi
    LocationEntryBehavior --> LocationBus[MapLocationEventBus]
    WorldAdmin --> WorldAdminCommands[World admin BC command adapter]
    WorldAdmin --> GameConfigRuntime[GameConfigRuntime]
    WorldAdmin --> MapWatch[Map/room configuration watchers]
    MapWatch --> CreationTemplate[Validated highest-revision creation template]
    CreationTemplate --> BcClient
    WorldAdminCommands --> WorldServices[Location/Map/GameConfig admin services]
    WorldAdminCommands --> ShowerAdminService[ShowerAdminService]
    ShowerAdminService --> ShowerRepository
    Shower --> ShowerService[ShowerService]
    ShowerService --> ShowerRepository
    ShowerService --> AppearanceApi
    ShowerService --> CommunicationApi
    ShowerService --> LocationBus
    Doors --> LocationBus
    Communication --> CommunicationApi[CharacterCommunicationApi]
    Appearance --> AppearanceApi[BcAppearanceAction + itemsets]
    Whisperer --> WhispererService[WhispererService]
    WhispererService --> CommunicationApi
    WhispererService --> EconomyPort
    WhispererService --> WhispererRepository
    DoorCommands --> Coordinator
    Communication --> Coordinator
    Appearance --> Coordinator
    MapTeleport --> Coordinator
    MapTeleport --> MapRepository
    MapTeleport --> BcClient
    Coordinator --> BcClient
    ApiPlane --> BcClient
    Router --> BcParser[BcCommandParser<br/>BC packet validation and normalization]
    BcParser --> Parser[BotCommandParser<br/>transport-neutral registry]
    Parser --> HelpCatalog[CommandHelpCatalog<br/>disposable descriptors]
    Parser --> AliasResolver
    Parser --> ScopedHelp[ScopedHelpService]
    AliasResolver --> GameConfigRuntime
    ScopedHelp --> HelpCatalog
    ScopedHelp --> HelpContext
    ScopedHelp --> CommunicationApi[CharacterCommunicationApi<br/>private reply]
    HelpContext --> Snapshots
    DoorCommands --> Parser
    Communication --> Parser
    Status --> Parser
    Appearance --> Parser
    Whisperer --> Parser
    Dilemma --> Parser

    BcClient <--> BC[Bondage Club Socket.IO server]
    BcClient --> Snapshots[BC-synced room/player observations]
    Snapshots --> Doors
    Snapshots --> Communication
    Snapshots --> Appearance
    Snapshots --> Shower
    Mongo --> FeatureConfig[Room/map, doors, groups,<br/>locations, itemsets, location-entry policy, game config,<br/>shower config/songs, command aliases, helper bots,<br/>Whisperer, Truth/Dare, Dilemma, Hot Seat, Staged Table,<br/>Trick-or-Treat and shop data]
    FeatureConfig --> ShowerRepository
    FeatureConfig --> Doors
    FeatureConfig --> Appearance
    FeatureConfig --> LocationEntryBehavior
    FeatureConfig --> Casino
    FeatureConfig --> Shop
    FeatureConfig --> Whisperer
    FeatureConfig --> HotSeat
    FeatureConfig --> StagedTable
    FeatureConfig --> HotSeatPrompts[prototypeHotSeatPrompts]
    FeatureConfig --> Dilemma
    FeatureConfig --> TruthDare
    FeatureConfig --> Abcl
    AbclConsent --> Mongo
    AbclState --> Mongo
    Coordinator --> Journal
    Main --> RuntimeState[prototypeRuntime heartbeat]
    Main --> DebugLog[prototypeDoorDebug, optional]
    Main --> Mongo

Creation and lifecycle

BOT_INSTANCE_ID optionally selects a typed, versioned profile from the database pinned by RUNTIME_TARGET. After the bootstrap connection is established, the profile is validated and its role and non-secret runtime settings override their environment counterparts. The role then selects the existing static composition: supervisor is the default and includes the game-config watcher and ABCL coordination, while helper starts the game-config watcher and only the helper worker. There is no dedicated ABCL runtime role. Without a profile, a legacy deployment with HELPER_AGENT_ID and no explicit role still selects helper mode. Credentials, Mongo connection/database selection, and observation bearer tokens remain deployment settings. Profile updates are not hot-applied; heartbeats report the actually loaded revision and role beside the latest desired revision and role. ABCL remains inactive unless rooms.main["abcl.rules"], its enabled region, and the registered abcl-capable helper are valid; missing, disabled, or invalid ABCL configuration does not prevent the supervisor from starting. Existing abcl profiles remain readable for migration but cannot start a process.

src/core/logging/log-event.ts defines the process-independent logging port; src/adapters/logging/json-line-logger.ts emits one schema-versioned JSON object per line with timestamp, severity, deployment, service, replica, correlation, and event fields. Startup phases use this logger. Fatal process records retain the same envelope while preserving nested causes and redacting configured secrets/URIs. Configuration validation reports field names and issue codes without input values. Plugin startup/rollback errors retain plugin IDs and original causes. See the Railway diagnostic guide.

  1. src/index.ts invokes main() through the process boundary in src/app/process.ts. Startup failures, unhandled rejections, and uncaught exceptions produce a synchronous, single-line JSON stderr record before exiting with status 1. Fatal process events do not attempt asynchronous cleanup or resume an unsafe runtime; normal signal shutdown is unchanged.
  2. main() parses the bootstrap values RUNTIME_TARGET, MONGODB_URI, optional fixed-mapping MONGODB_DATABASE, and optional BOT_INSTANCE_ID, then connects to the database fixed for that target: veratown_prototype for production or veratown_prototype_test for testing. When an instance ID is present, it loads and validates the matching profile before parsing the effective runtime configuration; a missing, disabled, deleted, malformed, or wrong-target profile fails startup.
  3. Before connecting to BC, the composition root loads the latest versioned room settings (falling back to the legacy room template) and effective map configuration, initializes the action journal and helper task control plane, and constructs one BcClient, BcApiPlane, ActionCoordinator, shared communication API, appearance services, map-location event bus, shared economy service, and feature repositories, including the Whisperer, Truth/Dare, Dilemma, Hot Seat, Staged Table, Trick-or-Treat, shop, and ABCL participant/decision repositories. The helper role uses its configured HELPER_AGENT_ID.
  4. BotPluginHost starts the statically listed plugins in this order: game-config watcher, room-config watcher, shared safeword, doors/map, location-entry messages, delayed location appearance, emergency release, location-entry behavior, world administration, communication, prototype status, appearance, Anonymous Whisperer, casino, shop, Truth/Dare, Dilemma, Hot Seat, Staged Table, Trick-or-Treat, helper bots, ABCL coordination, shower, then command routing. ABCL stops before helper bots, so it fences and drains task creation before the helper worker stops. Shower stops before helper bots and before BC closes, so graceful compensation can still use local room updates and optional helper observations. A helper process starts only the game-config watcher, room-config watcher, and helper worker, without command routing or other feature subscriptions. ABCL commands are registered with the supervisor’s shared command router; the helper observes ABCL messages and executes assigned typed tasks. Command handlers and event consumers are therefore registered before the BC connection starts routing incoming messages.
  5. Before feature configuration reads, the composition root initializes the typed heartbeat adapter, world map-admin repository, room-config repository, map repository, shared economy, Whisperer, Truth/Dare, Dilemma, Hot Seat, Staged Table, Trick-or-Treat, shop, helper-task, and ABCL participant/decision repositories, including the room-scoped shower settings, songs, and capacity counter collections. Supervisor and helper roles use the ABCL consent and bounded TTL decision repositories; neither stores raw ABCL mod statistics. Economy initialization verifies the casino migration marker before the bot can connect to BC. These create the heartbeat and terminal game-session TTL indexes, Dilemma’s unique active-room and prompt indexes, Hot Seat’s unique active-room and approved-prompt indexes, Staged Table’s unique active-room, recovery and terminal TTL indexes, Trick-or-Treat’s unique active-room and terminal TTL indexes, additive map query indexes, and a partial unique (roomKey, revision) indexes for append-only world-map and room-settings revisions. Truth/Dare indexes enforce one active session per room, support bounded recovery/config/prompt lookups, and expire only terminal sessions. Legacy maps are excluded from revision uniqueness. It then idempotently seeds the validated casino and shop baselines under prototypeGameConfig.rooms.main["casino.rules"] and prototypeGameConfig.rooms.main["shop.rules"] and the bet, slots, and balance starter aliases under prototypeGameConfig.rooms.main.command.aliases only when their keys are absent, preserving existing configuration, and loads the resulting game-config runtime snapshot before plugin startup. The Whisperer repository seeds its disabled room config and ten curated prompts without overwriting existing documents. After index initialization, the composition root calls seedDefaults("main", now) on the Truth/Dare, Hot Seat, Staged Table, Dilemma, and Trick-or-Treat repositories to insert disabled configs; prompt-based games also receive their reviewed English prompts. Before loading map configuration, the composition root also inserts missing room-wide room_admin and room_whitelist door groups in keypadGroupDefinitions, under the __room__ management namespace with accessScope: "room" and the public default code knock. The existing map configuration watch refreshes edits. Keypad authorization requires a matching configured group code and live role or active static membership; no role bypasses the code prompt. Room scope covers existing and future doors without duplicating role groups per door. These insert-only seeds preserve enabled state, operator settings, prompt revisions, and moderation decisions. Casino and location behavior baseline seeding logs whether each baseline was inserted and its document revision. Shop defaults are seeded only when its room key is absent; the region and itemset favors are never invented. The bot persists starting, calls BcClient.start(), then records ready and periodic heartbeat states in prototypeRuntime. The client resolves start only after room join/create and a matching room sync. In the supervisor role, the composition root then imports a baseline from that synchronized room if no prototypeRoomConfig revision exists and applies the current revision via ApplyRoomSettingsAction. Startup proceeds only after matching room settings are observed (or already satisfied); timeout or BC rejection fails startup. Invalid required room or character data keeps the process alive but leaves the client not ready: room-dependent traffic is fenced and the same socket reconnects with capped exponential backoff until a valid snapshot arrives. Malformed optional Ownership metadata is ignored and leaves HasOwner unknown. A helper worker with startLocationKey then requests MapTeleportAction; task polling starts only after the destination is already observed locally or through an enabled, fresh supervisor position stream, or a matching local map-position sync arrives. The optional helper receiver starts before the helper can reach ready; its handshake is bounded and loss leaves the worker fenced.
  6. Shutdown stops plugins in reverse order, closes the BC connection, drains pending appearance work, records stopped, flushes optional door-debug writes, and closes MongoDB in a finally block. Startup failure rolls back plugins already entered by the host.

src/app/start.ts is the wiring/composition point. Features receive shared dependencies; they do not create sockets or Mongo clients. BcClient owns login, room join-or-create, server-event normalization, BC snapshots, reconnection and the global outbound queue (14 packets per 1,200 ms). MapTeleportAction resolves an enabled point or region from veratownLocations (using a deterministic in-region tile for regions), uses the guarded BcClient.updateOwnMapPosition() method, and journals the request with ActionCoordinator. The local BcClient snapshot changes only on inbound BC sync. When optional sharing is enabled, a fresh supervisor stream observation may supply independent evidence for the helper’s destination; either matching signal confirms convergence, not causal attribution. The helper startup destination is optional and operator-managed. The native-only emergency-release command applies the configured itemset directly to the requesting member through the guarded appearance path. It does not move either character or depend on a location observation. When POSITION_OBSERVATION_ENABLED=true, DoorFeaturePlugin separately publishes validated room-scoped position observations and redacted appearance observations to process-local core buses. The supervisor serves sequenced room-scoped observations over the Railway private HTTP stream, and helpers upload their own local observations back through an authenticated POST endpoint on that same service. Fresh positions are retained per observer so independent live reports are not overwritten by another observer’s conflicting coordinate. Each helper has a unique bearer token; the supervisor checks its enabled registry record and runtime/room, and the client validates scope, HMAC proof, sequence, epoch, freshness, and bounded frame size. The feature defaults off, uses no extra BC connection or database collection, and never treats historical coordinates as live evidence.

The same optional session implements separate transport-neutral HelperTaskNotifier (supervisor) and HelperTaskWakeups (helper) ports, injected by start.ts. The supervisor dispatches a sequenced, payload-free hint only after task persistence; helpers negotiate support through an HTTP request header. Hints and stream reconnection wake the existing serialized Mongo claim/lease worker, never a second executor. Periodic polling remains enabled for unavailable streams, lost notifications, and mixed-version deployments. Wake-ups coalesce while work is in flight. Startup positioning gates every wake-up; connection generations fence stale preparation/claims after disconnect or stop. The helper plug-in disposes its wake-up subscription before closing the session during shutdown. Hints are neither BC observations nor task results.

Command suites register bounded, reviewed CommandHelpDescriptor metadata beside their handlers; each registration disposer removes both handler and help metadata. The composition root supplies a core CommandAliasResolver with a read function over the main room’s command.aliases setting. The resolver parses and caches the value per game-config revision; admin writes are checked against the live help catalog. Real command paths win, an alias is expanded at most once, and fixed arguments precede player-supplied arguments. The parser sends global (/bot help, !help), scoped (/bot <feature> help), and explicitly marked bare-feature requests to ScopedHelpService. It filters by the current observed room membership, feature-wide enabled-state, room-admin status, and capability availability policies, then replies only through private whispers. A disabled feature hides every help descriptor, including its administrative setup commands; capability policies can still hide individual commands such as games unavailable at the requester’s location. Hidden and unknown scopes return the same neutral text. Help is recomputed per request, rate-limited, chunked, and suppressed if requester, permission, location/config, or catalog revisions change before enqueue. Command execution continues to perform its own authorization checks. Alias shortcuts appear next to a target only when that descriptor is currently visible; admin-only targets stay permission-checked by their handlers.

Features, commands, and actions

Plugins own lifecycle and subscriptions. A feature may also own a command suite, service/runtime, repository, and dedicated actions:

Shared core/async primitives own common mechanics without importing feature or transport code: KeyedDeadlines replaces and cancels per-session timers, SerialQueue/KeyedSerialQueue serialize dependent work without poisoning the queue on failure, and TrackedTasks drains in-flight work during shutdown. core/plugins/DisposerStack releases registrations, listeners, and asynchronous watchers in reverse acquisition order, attempting every cleanup even if one fails. Features retain their own authorization, error reporting, deadline policy, and stop order; these primitives are not BC acknowledgements or durable state authorities. Synchronous disposers detach listeners before the first asynchronous cleanup yield. drainTrackedTasks waits for every task group and reports all failures without abandoning the other group’s drain. ABCL and Helper Bots fence new work immediately on stop, then await already accepted polls, event work, and repository calls before Mongo closes; a blocked repository call can delay shutdown, but cannot execute a newly claimed task after the fence. Shop, Casino, Dilemma, Trick-or-Treat, and World administration use reverse-order resource cleanup, including partial-start rollback; a failed disposer does not skip service shutdown, and the original startup error remains visible. The map runtime, shop favors, and delayed location appearance use keyed, replaceable deadlines with injected schedulers for deterministic tests; map callback failures are reported through the door action-error/debug path rather than becoming unhandled timer rejections. Communication and party-game plug-ins drain accepted event work before their services stop and before the shared Mongo client closes. A deadline expiring is not BC evidence. adapters/mongo/revisioned-writes.ts shares atomic revision-CAS mechanics for Hot Seat and Staged Table configuration/session writes; each repository still owns document parsing, isolation guards, validation, and collection selection. A competing write returns the newly read revision, never a success-shaped fallback.

Native /bot <command> messages arrive as targeted hidden BC bot packets. Bang-prefixed commands are accepted in supported chat/emote/whisper messages. The parser handles routing and syntax, not feature permissions or business policy. Command handlers and event-driven features call services/actions; they do not directly emit BC packets.

State-changing BC operations use a dedicated action and the shared ActionCoordinator. It writes requested before dispatch, records dispatch, then records only evidence supported by the BC response, a later BC state observation, or a documented read-back. One-way sends remain unconfirmed. Timeouts are not successes and do not trigger blind retries. The coordinator does not establish causal attribution when BC responses have no request ID.

BcApiPlane is a typed event/request boundary for contracts, safe packet summaries, subscriptions, and selected response/read-back flows; it does not replace domain-specific actions or the guarded methods on BcClient. Each action has one journal owner. In particular, do not wrap an already journaled action in another journaled BcApiPlane.dispatch().

Feature Services And Channel Adapters

Feature use cases own domain validation and policy and depend on repository or action ports. Channel adapters own parsing, authentication/authorization, and response formatting. Location Entry Behavior uses world-config administration; its appearance sets use the standard appearance itemset editor. Permissions are channel-specific and must be checked independently by every adapter. Keep transport types and chat/Discord APIs out of shared feature services so another channel does not fork business behavior.

Where state comes from

Information Authority and use
BC room and character snapshots, member presence, map positions, appearance, pose, arousal progress, reputation, title, and relationship-presence observations Latest valid inbound BC sync/read-back held in memory by BcClient; owner/lovership are reduced to presence booleans, reputation/title are validated projections, and arousal retains only progress. Dispatch never optimistically edits these snapshots. They represent observed BC state, not proof this bot caused it.
Veratown economy balances prototypeEconomyAccounts through the shared EconomyService; condition credits reads available credits only, excluding reservations. BC Money is not used.
Shared helper position evidence Optional five-second bounded MapPositionObservationBus entry received from the authenticated supervisor stream. Original BC observation time/source and stream epoch/sequence are retained; disconnect/departure invalidates entries. It is never persisted or used for another member.
Shared helper appearance evidence Optional five-second bounded per-observer appearance observations received from the authenticated helper stream. Matching local BC room updates are sufficient for shower actions; helper corroboration is not required. Local room syncs clear only the local cache entry. Appearance observations are never persisted.
Room template and stored map revisions Shared validated Mongo reader selects the highest room revision (legacy fallback only without revisions). Watched and locally refreshed for missing-room creation only; inbound BC sync remains the live-room authority.
Persistent BC room settings Append-only prototypeRoomConfig revisions; the supervisor imports the synchronized room as revision 1 when no version exists and reconciles the latest version at process startup. Admin-Map writes alone never mutate live BC state.
Locations, door definitions, groups and memberships MongoDB configuration loaded at startup; the map plugin reloads it on change-stream notifications. Room/BC observations are still used to evaluate live access and occupants.
Generic feature configuration Revisioned prototypeGameConfig, with bounded JSON stored globally or under an explicit room key. Startup seeds absent main-room casino, shop, and location-entry behavior baselines; features validate strict schemas and room-scoped consumers never fall back to global values. Replaced revisions are backed up transactionally.
Shop offers prototypeGameConfig.rooms.main["shop.rules"], operator-managed shop region, and prototypeItemSets for itemset favors. The region and itemsets are not seeded; adult offers require explicit policy confirmation.
Shower settings and songs prototypeShowerConfig, prototypeShowerSongs, and atomic prototypeShowerSongCounts; only the selected runtime database. Settings and songs are disabled/unseeded; clothing snapshots remain in bounded process memory only.
Anonymous Whisperer configuration and prompts prototypePartyGameConfigs and prototypeWhispererPrompts, validated at startup. The room game is disabled by default; prompt seeds are idempotent and preserve operator edits.
Anonymous Whisperer active submission One bounded process-memory value under the room lease. It is never reconstructed from BC chat history or stored in MongoDB, logs, debug records, errors, or action journals.
Anonymous Whisperer session and lease prototypeWhispererSessions owns metadata-only revisioned rounds and durable pending awards; prototypeWhispererLeases owns room process exclusivity.
Appearance itemsets and location-entry policy Operator-managed standard prototypeItemSets and room-scoped prototypeGameConfig.rooms.main["location-entry-behavior.rules"], validated before use. The startup generic baseline is disabled; configured itemset timer options pass through to BC.
Action outcomes prototypeActions, written by the coordinator with lifecycle history, five-day TTL, and a 10,000-terminal-record FIFO cap. Structured stdout also emits each transition joined by operationId. This is an audit of the attempted operation and evidence—not an alternate live-state store.
Process status prototypeRuntime heartbeat/state records. These are operational liveness data, not BC room state.
Door/command diagnostics Optional prototypeDoorDebug records with 30-day TTL, enabled by DOOR_DEBUG=true. These supplement action records and are not feature configuration.

MongoDB Contracts And Ownership

src/adapters/mongo/ owns generic persistence adapters and the guarded connection. Feature repositories own feature-specific MongoDB operations; domain services, the core, and the composition root do not manipulate collections. Operator scripts are explicit exceptions and must be dry-run-first, target-isolated, bounded, and non-deleting.

Collection Owner / purpose Retention and compatibility
veratownRooms, veratownMap Shared Mongo room/map reader; world map-admin repository writes append-only revisions No automatic pruning/TTL. Legacy fallback preserved; indexed highest-revision reads, partial unique room/revision writes.
prototypeMapBackups World map-admin repository; stored-map snapshots Operator-managed retention; backup restore appends a revision.
prototypeRoomConfig World room-config repository; append-only persistent BC room settings by room key and revision No TTL or automatic pruning; indexed unique room/revision history, with the newest 50 revisions exposed in Admin-Map.
prototypeAdminAudit Independent web administration; sanitized actor, operation and affected-record metadata transactionally paired with configuration writes 30-day expiresAt TTL; no raw configuration, access codes, prompts, songs or map sign text.
prototypeAdminWebSessions Independent admin web service; local-login session keyed by the SHA-256 of the cookie token, with email, lastSeenAt and absolute expiresAt. Authentication only, never BC or configuration authority expiresAt TTL (1 hour); no tokens, passwords or roles (roles are resolved from current configuration).
prototypeAdminSnapshots Independent web administration; versioned, non-secret configuration recovery plus a per-room serialization guard 30-day expiresAt TTL and at most 20 snapshots per room; guard has no TTL. Maximum 8 MiB / 8,000 configuration records per snapshot.
prototypeGameConfig World game-config repository/runtime Revisioned bounded JSON with global and explicit room scopes; no automatic retention.
prototypePartyGameConfigs / prototypeWhispererPrompts Anonymous Whisperer repository; room rules and curated adult prompts Config and curated text have no TTL; startup seeds disabled defaults and ten prompts without overwriting operator edits.
prototypeWhispererSessions / prototypeWhispererLeases Anonymous Whisperer repository; metadata-only rounds and exclusive room ownership Terminal sessions expire after 30 days; pending bonus sessions and leases remain until settled or application expiry.
veratownMapBackups Configuration copy operator No runtime consumer or automatic retention policy; do not prune implicitly.
veratownLocations, keypadDoorDefinitions, keypadGroupDefinitions, keypadGroupMemberships Map repository Legacy map values normalize at the read boundary; only additive non-unique query indexes are initialized. Membership expiry remains application-enforced.
prototypeItemSets Shared appearance itemset repository Generic appearance contract, with location-entry behavior as a consumer; missing version is accepted only after full legacy validation. Item lists remain bounded.
prototypeEconomyAccounts Economy repository Global per-member available/reserved credits and entry-grant timestamp; no TTL.
prototypeEconomyLedger Economy repository Stable idempotency records; 60-day TTL after finalization. Unsettled reservations and the cutover marker remain durable.
prototypeShopOrders Shop repository Orders remain durable while open/unconfirmed; terminal orders expire after 60 days.
prototypeShowerConfig, prototypeShowerSongs, prototypeShowerSongCounts Shower repository; room-scoped settings, bounded song catalog and atomic capacity counters No TTL; settings default disabled, songs are admin-provided, and appearance snapshots are never persisted.
prototypeCasinoWallets, prototypeCasinoLedger Legacy casino migration/rollback Retained as rollback source; wallets have no TTL and settled ledger expiry is extended to 60 days.
prototypeCasinoSessions Casino session repository 30-day TTL after terminal closure. Accepting, settling, and recovery-pending sessions remain durable.
prototypePartyGameConfigs Room-scoped party-game repositories, including <roomKey>:staged-table and <roomKey>:trick-or-treat Revisioned settings; no TTL. Missing configuration is disabled; Staged Table appearance requires an explicit validated stage/release plan.
prototypeStagedTablePrompts Staged Table repository; reviewed English prompt revisions No TTL; edits append revisions and retire the previous prompt without overwriting its text.
prototypeStagedTableSessions Staged Table repository; bounded roster, in-progress hand, scores, markers, prompt attestations, and pinned appearance/release deadlines Active and cleanup-pending terminal sessions have no TTL; fully released terminal sessions expire after 30 days. Private cards are removed when a hand resolves or is voided.
prototypeHotSeatPrompts Trusted Hot Seat prompt operator Moderated records; no TTL. Only validated approved English prompts are selectable.
prototypeHotSeatSessions Hot Seat session repository Active sessions are durable without TTL; terminal sessions expire after 30 days.
prototypeTrickOrTreatSessions Trick-or-Treat session repository Bounded room roster, route, score, and consented interaction state; active sessions are durable and terminal sessions expire after 30 days.
prototypeTrickOrTreatProgress Trick-or-Treat session repository Per-room/member eligible loss streak; stage progress commits only after observed appearance and expires after 30 days.
prototypeTrickOrTreatProgress Trick-or-Treat session repository Per-room/member eligible loss streak; stage progress commits only after observed appearance and expires after 30 days.
prototypeHelperAgents, prototypeHelperSettings, prototypeHelperTemplates Helper Bots repository Operator-managed identity, bounded fleet settings, and public templates; no TTL.
prototypeHelperTasks Helper Bots repository and roulette presentation router Active/reconciliation tasks are preserved; roulette stages use stable session/stage IDs and bounded ordering; terminal tasks have a five-day TTL and 10,000-record FIFO cap.
prototypeHelperActionBudgets Helper Bots repository Per-helper rolling-minute action budgets; short TTL.
prototypeActions Generic Mongo action journal Five-day Date-based TTL; oldest terminal records are pruned after 10,000.
prototypeRuntime MongoRuntimeHeartbeat Each heartbeat refreshes expiresAt as a BSON Date; its 30-day TTL index is initialized before use.
prototypeDoorDebug Generic Mongo diagnostics adapter Optional diagnostics with existing 30-day Date-based TTL.

Itemsets use one shared appearance schema and compatibility reader; conditional variants are part of that generic schema, not feature-owned fields. The AppearanceItemSetService resolves the first matching variant from the target’s latest BC-observed appearance and the BC adapter’s injected prerequisite check. Unknown evidence never selects a variant. This selection is separate from the transport-independent core/ condition contracts and does not mutate observed state. Feature policies may reject variants when they require a fixed itemset. The versioned schema remains strict for writes. Contract normalization can be inspected and, with explicit operator approval, applied with npm run migrate:mongodb-contracts -- --apply. It validates before writing, uses the selected fixed runtime database only, preserves document IDs, and deletes nothing. It does not deploy database validators or unique indexes.

The source/production database is only read by the dry-run-first, allowlisted configuration-copy script. The running bot reads and writes only the isolated prototype database; it does not query production player profiles or session state as a fallback.

Codebase comparison and improvement opportunities

The implementation matches the intended core boundaries: the core contains transport/database-independent contracts, one composition root owns shared instances, the BC adapter owns transport and snapshots, plugins clean up their subscriptions, and actions distinguish dispatch from evidence-backed outcomes. The action journal and isolation guard are implemented rather than aspirational.

The following are improvements to consider, not claims that the runtime already provides them:

  1. Keep persistence validation uniform. Appearance, location-entry behavior, and map repositories validate external documents. Map persistence and room creation share mapDataSchema, including bounded optional sign CellData; room settings still use the established explicit defaults. Preserve that shared map contract when adding new storage surfaces rather than introducing another projection.
  2. Reduce feature-to-adapter coupling selectively. Some feature plugins accept Db or concrete BcClient types directly. This is workable in the current composition, but narrower injected repository/observation ports would make feature logic easier to test and reduce migration cost. Extract ports only where they clarify ownership; do not add a generic DI framework.
  3. Keep runtime documentation tied to composition. The plugin list and ordering affect startup, event availability, and cleanup. Update this overview and the plugin guide whenever that list or its ordering changes.
  4. Treat live protocol qualification separately from tests. Fakes and unit tests verify packet shapes and outcome logic, but they cannot prove deployed BC acknowledgement timing or native timer behavior. Keep those as explicit operational qualification gates before enabling risky features.

See BC-API-PLANE.md for contracts and class-level UML, BC-SOCKET-API-ACKNOWLEDGEMENTS.md for the packet evidence matrix, and the feature guides for detailed flows.

Map configuration authority and sign preservation

flowchart LR
    Sync[Inbound BC room sync] --> Observed[Defensive live map observation]
    Observed --> Save[Authorized map save]
    Save --> Schema[Shared mapDataSchema including optional CellData]
    Schema --> Revisions[Append-only stored revisions]
    Editor[Authenticated visual layout draft] --> Layout[Catalog and protected-metadata validation]
    Layout --> Revisions
    Editor --> Analysis[Shared stateless map analyzer]
    Analysis --> Review[Exact candidates and explicit import decisions]
    Review --> Forms[Normal version-checked location and door editors]
    Revisions --> Backup[Validated backup and restore]
    Backup --> Revisions
    Revisions --> Template[Validated future room-creation template]
    Template --> Create[ChatRoomCreate only when room is missing]

Metadata edits preserve the complete validated map; restore appends a revision and refreshes the template without changing the live room. A malformed selected head fails rather than falling back. Sign text is bounded authorized map configuration, never action-journal/debug metadata. Door updates preserve unrelated signs, and only inbound BC sync changes observed state. Composition, plugin lifecycle, collections, indexes, and shutdown order are unchanged.

The visual editor appends layout_edit revisions with the existing transaction, expected-version and sanitized-audit boundaries. It preserves effects and CellData, rejecting object replacement at protected cells. Analysis consumes only bounded geometry/export input and reuses the offline analyzer, renderers and import planner; artifacts never include sign text or private cell metadata. Its same-origin embedded walkthrough is illustrative, not a movement/access authority. Import validation hands records to normal reviewed forms, never a second bulk-write path. Leaving the workspace disposes message listeners and artifact URLs.