veratown-prototype

The Anonymous Whisperer — Feature Proposal

Status: Variant B is implemented and disabled by default. This proposal is retained as the design record; enabling the game remains a separate, room-specific operator decision.

Archived: 2026-10-16 (UTC). See the live feature guide for current runtime behavior and release status.

Purpose and design constraint

Implement an independent, room-scoped social guessing game. Participants privately answer a curated adult prompt, the bot publishes that answer without naming its author, and opted-in room members make validated accusations before a deadline. A correct accusation ends the anonymous phase; if nobody identifies the author and quorum is met, the submitter may receive a configured bonus from the shared economy.

There is a direct conflict between two requirements:

  1. The requested game accepts and queues free-form private whispers, while all game state is requested to be database-driven.
  2. Existing repository rules prohibit storing or logging raw chat/whisper text. A database record of a submitted secret would violate that rule even if it had a TTL; encryption alone would not make it compliant.

This design does not authorize persistent storage of submissions. Variant B was selected: free text is bounded and ephemeral in process memory, while session metadata and the curated prompt catalog are database-backed.

Variants considered

Variant A — database-driven catalog, no user-submitted secret text

This alternative was not selected. Curate approved secret-like statements in prototypeWhispererPrompts; players privately submit a prompt ID or choose from the approved catalog. The bot posts the selected record and room members accuse the opted-in submitter. This preserves DB-backed content, room config, and durable recovery, but it is not a personal free-text confession game.

Variant B — closest gameplay match, ephemeral user text

Selected and implemented. This variant accepts an explicit, game-prefixed private Whisper, but keeps its content only in a bounded process-memory buffer until the round resolves. MongoDB stores session metadata, author member number, prompt/config IDs, phase, deadline, and outcome, never the text. The anonymous feature holds an exclusive per-room owner lease; a second process cannot serve the same room. After a process restart or lease loss, the active round is cancelled, in-memory text is discarded, and no bonus/penalty is claimed. The ten curated database prompts seed the round topic; they do not replace the private free-text response.

Variant C — privacy-policy exception

If persistent free-text queues are essential, the current storage prohibition must be formally changed first. That requires a separate privacy/security review, explicit retention/key management, access controls, backup/restore analysis, and consent language. Do not implement this variant under the current repository rules.

Behavior common to variants

Configuration and data

Use a typed per-room record in prototypePartyGameConfigs with game ID <roomKey>:anonymous-whisperer. Missing or invalid config disables the game. Settings are all database values with code-level bounds:

Setting Proposed validation / intent
enabled Explicit room-level opt-in; false when absent.
minPlayers, maxPlayers Bounded opted-in roster.
submissionWindowMs, accusationWindowMs Positive bounded durations; persisted deadlines.
maxSubmissionLength Positive value no higher than a small hard code-level ceiling.
Submission bytes Hard code-level ceiling of 1,024 UTF-8 bytes.
minAccusations Quorum required before an economic bonus can be awarded.
submitterBonusCredits Bounded non-negative integer, idempotent per submission.
followupMode disabled only in this implementation.

prototypeWhispererPrompts contains reviewed, approved, non-identifying round prompts and revisions; startup seeds ten adult-oriented conversation prompts without replacing operator edits. Optional follow-up is disabled. User-provided text remains memory-only. The session stores only a prompt ID, author member number, bounded accusations, config revision, deadlines, outcome, and stable settlement ID. It never stores the free-text submission.

Persistence, concurrency, and recovery

prototypeWhispererSessions is room-scoped, bounded, and revisioned. Use a unique active key so there is one session per room. Variant B acquires a single-owner lease and keeps the ephemeral text associated with that lease in process memory; the lease is not permission to persist or log the text. Lease loss or process restart voids the active session and discards the buffer. The text is not reconstructed from chat history. A separate per-room lease record coordinates ownership.

Accusations and closure use compare-and-set revision transitions. Repeated accusations, duplicate timer callbacks, and competing process instances cannot pay twice. Persist the terminal resolution before an idempotent submitter bonus. An interrupted bonus remains pending and is retried with the same operation ID. A departing author before publication voids the round; after publication, use the configured deadline and do not infer a correct or incorrect guess from departure.

For Variant B, implement a redacted submission boundary separate from generic command tracing/debug output. The raw message must never enter a journal, debug document, structured log, error string, test snapshot, or Mongo document. Bound memory capacity, clear buffers on every terminal path, disconnect, shutdown, lease loss, and failed startup.

UML

Component diagram

flowchart LR
    Root["start.ts"] --> Plugin["AnonymousWhispererPlugin"]
    Plugin --> Input["WhispererSubmissionHandler"]
    Plugin --> Commands["WhispererCommandSuite"]
    Plugin --> Service["WhispererService"]
    Input --> Service
    Commands --> Parser["BotCommandParser"]
    Commands --> Service
    Service --> Repo["WhispererRepository"]
    Service --> Economy["Injected EconomyPort"]
    Service --> Comms["Injected CharacterCommunicationApi"]
    Repo --> Config[("prototypePartyGameConfigs<br/>room-scoped")]
    Repo --> Catalog[("prototypeWhispererPrompts<br/>10 curated round prompts")]
    Repo --> Sessions[("prototypeWhispererSessions<br/>metadata only")]
    Repo --> Lease[("prototypeWhispererLeases")]
    Service -. "bounded ephemeral text" .-> Memory["Process-memory buffer"]
    Comms --> BC["Existing BcClient actions"]

Class diagram

classDiagram
    class AnonymousWhispererPlugin {
        +start()
        +stop()
    }
    class WhispererSubmissionHandler {
        +handle(message)
    }
    class WhispererCommandSuite {
        +register()
    }
    class WhispererService {
        +start(ownerId)
        +join(memberNumber)
        +leave(memberNumber)
        +startRound(memberNumber)
        +submit(memberNumber, text)
        +accuse(memberNumber, target)
        +stop()
    }
    class WhispererRepository {
        <<Mongo adapter; metadata only>>
        +loadConfig()
        +loadPrompts()
        +createSession(session)
        +compareAndSetSession(expectedRevision)
        +acquireLease(ownerId)
    }
    class EconomyPort {
        <<injected shared contract>>
        +grantOnce(operationId, member, amount, reason)
    }
    class CharacterCommunicationApi {
        <<injected shared API>>
        +chatRedacted(text)
        +whisper(memberNumber, text)
    }
    class BotCommandParser {
        <<core command router>>
        +register(command, description, handler)
    }
    AnonymousWhispererPlugin --> WhispererSubmissionHandler
    AnonymousWhispererPlugin --> WhispererCommandSuite
    AnonymousWhispererPlugin --> WhispererService
    WhispererSubmissionHandler --> WhispererService
    WhispererCommandSuite --> WhispererService
    WhispererService --> WhispererRepository
    WhispererService --> EconomyPort
    WhispererService --> CharacterCommunicationApi
    WhispererCommandSuite --> BotCommandParser

Use-case diagram

flowchart LR
    Admin((Room admin)) --> Enable([Enable room and consent rules])
    Submitter((Opted-in submitter)) --> Submit([Answer curated prompt by explicit private text])
    Voter((Opted-in voter)) --> Accuse([Accuse observed room member])
    Timer((Deadline worker)) --> Resolve([Resolve quorum and author match])
    Submit --> Session[(Metadata-only active session)]
    Accuse --> Session
    Resolve --> Outcome([Identify author or grant bonus once])

Activity diagram: submission to resolution

flowchart TD
    Start([Round requested]) --> Config{Enabled config + adults-only confirmation?}
    Config -->|No| Block[Do not accept submissions]
    Config -->|Yes| Lobby[Require enough opted-in present members and room lease]
    Lobby --> Catalog[Select reviewed DB prompt and submitter]
    Catalog --> Input[Accept explicit private Whisper and publication consent]
    Input --> Memory[Keep validated text in bounded process memory only]
    Memory --> Publish[Publish through redacted action without author]
    Publish --> Phase[CAS session to accusation phase]
    Phase --> Accuse[Accept validated member-number accusations]
    Accuse --> Deadline{Deadline reached?}
    Deadline -->|No| Accuse
    Deadline -->|Yes| Quorum{Minimum valid accusations met?}
    Quorum -->|No| Void[Persist void outcome; no award]
    Quorum -->|Yes| Match{Any valid accusation matches author?}
    Match -->|Yes| Identified[Persist identified outcome]
    Match -->|No| Bonus[Persist outcome and grant submitter bonus once]
    Identified --> Clear[Clear ephemeral content]
    Bonus --> Clear
    Void --> Clear
    Clear --> Finish([Close session])
    Block --> Finish

State machine

stateDiagram-v2
    [*] --> Disabled
    Disabled --> Lobby: enabled room config + adults-only confirmation + lease
    Lobby --> Submission: enough opted-in members; prompt privately dispatched
    Submission --> Accusing: valid consented response published
    Submission --> Voided: timeout / author departure / restart / disconnect / lease loss
    Accusing --> Accusing: valid accusation before deadline
    Accusing --> Identified: author correctly accused
    Accusing --> Voided: deadline without quorum
    Accusing --> BonusPending: quorum; no correct accusation
    BonusPending --> Complete: idempotent bonus result recorded
    BonusPending --> BonusPending: settlement failure; retry same operation ID
    Identified --> Complete
    Voided --> Complete: clear ephemeral buffer and close
    Complete --> [*]

Sequence / communication diagram

sequenceDiagram
    participant Submitter
    participant Input as Redacted private-input boundary
    participant Service as WhispererService
    participant DB as Metadata repository
    participant Memory as Bounded process memory
    participant Voter
    participant Cmd as WhispererCommandSuite
    participant Economy as EconomyPort
    participant BC as CharacterCommunicationApi
    Submitter->>Cmd: !whisperer start
    Cmd->>Service: startRound(member)
    Service->>DB: create metadata-only round
    Service->>BC: privately dispatch selected prompt (unconfirmed)
    Submitter->>Input: direct Whisper with explicit publication consent
    Input->>Service: sender + transient text
    Service->>Memory: hold transient content under active lease
    Service->>BC: publish content without author (unconfirmed dispatch)
    Service->>DB: CAS session to accusation phase
    Voter->>Cmd: accuse <memberNumber>
    Cmd->>Service: validated observed-room target
    Service->>DB: CAS bounded accusation
    Service->>DB: persist resolution before settlement
    alt correct accusation
        Service->>BC: announce identified outcome
    else no correct accusation and quorum met
        Service->>Economy: grantOnce(stable submission operation ID)
    else below quorum / lease lost
        Service->>DB: persist void; no economic mutation
    end
    Service->>Memory: clear content on every terminal path

Implemented decisions and release gates

Variant B is selected. Defaults are disabled, adults-only room confirmation is required, three minimum and twenty maximum players, two-minute phase windows, 280 characters / 1,024 UTF-8 bytes maximum submission, two accusations for quorum, a ten-credit bonus bounded to 100, and no follow-up. Participants self-affirm adult status and consent; the bot does not infer age. Prompt review remains operator-owned.

Acceptance tests cover accusation validation, observed-roster checks, revision transitions, early/duplicate timer callbacks, exclusive lease ownership, restart recovery, idempotent pending bonuses, and the absence of submission canaries from session documents and command traces. Redacted-action tests verify transport errors cannot place room submission text in the action journal. The runtime remains disabled until each target room is explicitly configured and separately qualified. Variant C remains blocked pending a formal privacy/security exception.

References