Confirmed defect · BarBotJS · Dynamic mode

Cypher says “Classic.” The loop still says “Cypher.”

Two mutable rotation lists disagree. The dispatcher trusts one; the turn stages trust the other. The fallback only speaks—it never transitions—so Discord gets trapped in an immediate, repeating notification loop.

Download detailed fix prompt Inherited base PR Observed 15 Jul 2026 · Deep Breath
Confirmed
Reproduced invariant failure
4–5 notices
Possible per stale turn
Not Lavalink
Inherited state defect
01

The finding

A false transition message is only the visible symptom. The state machine never actually leaves its stale Cypher dispatch.

Discord screenshot showing BarBot X Dev repeatedly posting Cypher List empty, reverting to Classic mode between TURN DONE, ALMOST DONE, and GET READY headings.
Original user-supplied capture · BarBot X Dev · 3:38 PM · repeated fallback notices visible across successive turn headings.
Primary incident evidence

The screenshot shows a transition that never completes.

The headings continue to come from the Cypher pipeline while every stage reports an empty list. That combination is the visible signature of a stale Cypher mode snapshot operating against a different, already-cleared membership collection.

Same warningRepeated several times inside each turn
Cypher headingsTURN DONE · ALMOST DONE · GET READY
No transitionClassic is announced but never dispatched
Observed Discord behavior
Cypher List empty — reverting to Classic mode

# TURN DONE
Cypher List empty — reverting to Classic mode
Cypher List empty — reverting to Classic mode
Cypher List empty — reverting to Classic mode
…repeat immediately…
Minimal reproduction state
{
  "activeCypherList": 0,
  "activeCypherDeque": 1,
  "effectiveMode": "cypher",
  "turnCurrent": null
}

The contradictory state was reproduced locally using the actual CypherList and CypherRotation classes.

02

The split-brain architecture

Both objects describe “the active cypher,” but they are independently mutable and have different consumers.

0
Cleared by the UI

activeCypherList

CypherList · legacy turn projection
  • Read by cypherGetReady()
  • Read by makeCypherMessage()
  • Read by mute, next-user and display helpers
  • cmdClearCypher() empties this collection
1
Still populated

activeCypherDeque

CypherRotation · newer canonical candidate
  • Read by getEffectiveMode()
  • Used by dashboard/API rotation behavior
  • Owns newer waiting and rotation features
  • Not cleared by the menu Clear Cypher action
BROKEN INVARIANT   activeCypherList IDs + order  ≠  cypherRotation.activeCypherDeque IDs + order
03

Why it repeats forever

turnLoop() snapshots the effective mode once, then every selected Cypher stage independently encounters the empty legacy list.

flowchart TD
  A["Outer cancellable task loop"] --> B["Snapshot effective mode"]
  B --> C{"activeCypherDeque has users?"}
  C -->|"Yes, stale user"| D["Select the full Cypher pipeline"]
  C -->|"No"| K["Select Classic pipeline"]
  D --> E["Startup check reads empty activeCypherList"]
  E --> F["Fallback notice 1"]
  F --> G["Get-ready posts TURN DONE heading"]
  G --> H["Fallback notice 2"]
  H --> I["Mute or message stage finds no current user"]
  I --> J["Fallback notices 3 and 4"]
  J --> L["Next-user stage also finds empty list"]
  L --> M["End iteration without changing dispatch state"]
  M ==> A

  classDef fault fill:#b53a2d22,stroke:#b53a2d,stroke-width:2px
  classDef warn fill:#a5660822,stroke:#a56608,stroke-width:1.5px
  classDef normal fill:#1d4f7318,stroke:#1d4f73,stroke-width:1.5px
  classDef safe fill:#23705722,stroke:#237057,stroke-width:1.5px
  class C,D,E,F,G,H,I,J,L,M fault
  class A,B normal
  class K safe
          
Key semantic detail

Do not “fix” this by permanently assigning Classic mode. Configured Dynamic mode should remain Dynamic. Its effective mode is Classic only while membership is empty, then Cypher again after a join. The repair must make that temporary transition atomic and observable once.

04

Problem areas in the current stack

The clear action is the most direct trigger, but several other asymmetric mutation paths can create ghost or missing participants.

AreaCurrent behaviorFailureRepair role
getEffectiveMode()Reads only activeCypherDeque.lengthWrong dispatchRead the documented canonical source after reconciliation.
turnLoop()Snapshots one mode for the whole turnStale pipelineEnd the iteration when a membership-changing stage changes effective mode.
revertToDefaultSequence()Sends text only; called by many stagesMessage amplificationCentralize a single transition-based notification.
cmdClearCypher()Clears only activeCypherListPrimary triggerCanonical clear plus projection and waiting-state cleanup.
cmdAddAll()Adds only to the legacy listInvisible membersAdd canonically, then project only accepted active users.
Moderator remove selectorRemoves only from CypherRotationGhost displayRemove canonically and refresh the projection.
Slash remove / sizeCan mutate or replace only the legacy listDivergent stateRoute through the same coordinator boundary.
Dashboard/API routesPrimarily mutate CypherRotationLive-loop lagProject canonical changes into the live turn surface.
Existing clear testAsserts only one list is emptyFalse confidenceAssert identity, order, effective mode and no repeated notices.
05

A minimal, durable repair

Contain the active bug in a reviewable PR while creating a clear migration direction away from duplicate state.

1

Choose authority

Make CypherRotation authoritative. Treat activeCypherList as a compatibility projection until all legacy consumers migrate.

2

Centralize mutation

Clear, join, leave, add, remove, rotate and sort through one coordinator, then refresh the projection with the same user objects and order.

3

Make turns atomic

Reconcile before mode resolution. After a membership-changing stage, re-evaluate; if the mode changed, stop the stale turn immediately.

4

Notify on transition

Emit one accurate message only when effective mode moves from Cypher to Classic—not once from every helper that sees no current user.

5

Cover every surface

Audit menus, slash commands, voice state, dashboard HTTP routes, waiting promotion, capacity, removal and rotation order.

6

Prove the round trip

Dynamic empty → Classic; join → Cypher; clear/leave → Classic; join again → Cypher. Configured mode stays dynamic.

Before

Speech without transition

configured = dynamic
effective = cypher
legacy current = null
fallback messages = unbounded
After

Policy preserved, dispatch corrected

configured = dynamic
effective = classic
canonical membership = empty
transition messages ≤ 1
06

What “fixed” must prove

A passing clear method is insufficient. The test must observe dispatch, message count, dynamic re-entry and the real Discord flow.

Automated acceptance
  • Clear empties active and waiting state in every representation.
  • IDs and order match after every mutation surface.
  • Several loop iterations emit at most one transition notice.
  • No Cypher heading appears after effective Classic transition.
  • Final-user voice removal aborts the remaining Cypher stages.
  • Explicit Classic and Nonstop behavior remain unchanged.
Deep Breath acceptance
  • Run a normal Dynamic Cypher turn with one participant.
  • Use the same Clear action that produced the screenshot.
  • Observe for at least two normal turn intervals.
  • Hear/see normal Classic behavior with no fallback flood.
  • Join again and confirm Cypher resumes automatically.
  • Capture timestamps and logs without exposing credentials.
Why a cooldown is not a repair
A delay, debounce or Discord rate limiter can reduce visible spam but cannot restore the state invariant, select the correct turn pipeline, or guarantee automatic Dynamic-mode re-entry. It converts a hot wrong loop into a slower wrong loop.
Why assigning Classic permanently is also wrong
Dynamic is a configured policy; Classic and Cypher are its effective states. Persisting Classic on empty membership would stop the spam but remove Dynamic mode's core behavior: automatically returning to Cypher when someone joins.
07

Implementation handoff

The downloadable prompt is deliberately detailed enough for Baron’s GPT—or another engineer—to implement, test and live-validate without reconstructing this investigation.

Download the full engineering prompt

Contains the confirmed reproduction, canonical-state decision, required mutation audit, atomic turn-transition design, regression matrix, Deep Breath acceptance test, constraints and definition of done.

root causeimplementation contractregression suitelive acceptancenon-goals
Download .md