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.
A false transition message is only the visible symptom. The state machine never actually leaves its stale Cypher dispatch.
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…
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.
Area
Current behavior
Failure
Repair role
getEffectiveMode()
Reads only activeCypherDeque.length
Wrong dispatch
Read the documented canonical source after reconciliation.
turnLoop()
Snapshots one mode for the whole turn
Stale pipeline
End the iteration when a membership-changing stage changes effective mode.
revertToDefaultSequence()
Sends text only; called by many stages
Message amplification
Centralize a single transition-based notification.
cmdClearCypher()
Clears only activeCypherList
Primary trigger
Canonical clear plus projection and waiting-state cleanup.
cmdAddAll()
Adds only to the legacy list
Invisible members
Add canonically, then project only accepted active users.
Moderator remove selector
Removes only from CypherRotation
Ghost display
Remove canonically and refresh the projection.
Slash remove / size
Can mutate or replace only the legacy list
Divergent state
Route through the same coordinator boundary.
Dashboard/API routes
Primarily mutate CypherRotation
Live-loop lag
Project canonical changes into the live turn surface.
Existing clear test
Asserts only one list is empty
False confidence
Assert 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.
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.