# Implementation prompt: repair BarBot's Cypher-empty fallback loop

You are working in the BarBotJS repository on the branch stack based on `feat/compose-http`. Implement and validate a focused repair for a confirmed infinite Discord-message loop that occurs when BarBot's two in-memory cypher membership representations diverge.

## Outcome required

When Dynamic mode has no active cypher participants, BarBot must run the Classic sequence without repeatedly posting `Cypher List empty - reverting to Classic mode`. When a participant subsequently joins, Dynamic mode must automatically use the Cypher sequence again. Clearing or removing the final participant must transition back to effective Classic behavior once, without executing the remainder of a stale Cypher turn.

The implementation must preserve Dynamic mode as the configured policy. Do not permanently assign `server.cypherMode = 'classic'` merely because the rotation is temporarily empty.

## Confirmed diagnosis

BarBot currently holds cypher membership in two mutable collections:

1. `server.activeCypherList` (`CypherList`) is read by many legacy turn and Discord display functions.
2. `server.cypherRotation.activeCypherDeque` (`CypherRotation`) is read by `getEffectiveMode()` and newer dashboard/API behavior.

The two collections can diverge because several mutation paths update only one of them. The clearest reproduction is the menu Clear Cypher action:

- Join users through a path that writes both collections.
- Start in configured Dynamic mode.
- Execute Clear Cypher.
- `activeCypherList` becomes empty.
- `cypherRotation.activeCypherDeque` remains populated.
- `getEffectiveMode('dynamic')` therefore continues returning `cypher`.
- Cypher stages then read `activeCypherList`, find no current user, and call `revertToDefaultSequence()`.
- `revertToDefaultSequence()` only sends a Discord message. It neither reconciles membership nor terminates the current Cypher pipeline.
- `turnLoop()` captured `effectiveMode` once at the beginning, so the remainder of the same stale Cypher pipeline continues invoking more fallback sites.
- The outer task loop immediately begins another iteration and repeats indefinitely.

The locally reproduced contradictory state is:

```json
{
  "activeCypherListLength": 0,
  "activeCypherDequeLength": 1,
  "effectiveMode": "cypher",
  "legacyCurrentUser": null
}
```

This explains the observed Discord output: a randomized `TURN DONE`, `ALMOST DONE`, or `GET READY` heading followed by several identical fallback messages, repeating continuously.

## Relevant code areas

Inspect these areas before editing; line numbers may have moved:

- `src/models/CypherRotation.ts`
  - `getEffectiveMode()` uses `activeCypherDeque.length`.
  - `joinRotation()`, `leaveRotation()`, `removeAbsentUsers()`, `rotateCypher()`, and `clearAll()` mutate the newer canonical candidate.
- `src/models/CypherList.ts`
  - `dequeList`, `joinLiveCypherList()`, `removeFromLiveCypherList()`, `rotateCypher()`, and `clearCypherList()` hold the legacy live list.
- `src/models/Server.ts`
  - Owns both `activeCypherList` and `cypherRotation`, plus a separate legacy `waitingQueue`.
- `src/services/Helpers.ts`
  - `turnLoop()` snapshots the effective mode for the full turn.
  - `cypherGetReady()`, `makeCypherMessage()`, `muteAllExceptCurrent()`, `getCurrentInCypher()`, and `getNextInCypher()` contain empty-current fallback paths.
  - `cypherStartOfTurn()` may remove absent users from `CypherRotation` during a turn.
  - `revertToDefaultSequence()` currently emits a message but does not perform a transition.
- `src/menus/actions.ts`
  - `cmdClearCypher()` clears only `activeCypherList`.
  - Audit `cmdJoin`, `cmdLeave`, `cmdAddAll`, `selAddUserTarget`, `selRemoveUserTarget`, and related capacity/waiting-list behavior.
- `src/cogs/CypherRotation.ts`
  - Audit slash commands, voice-state auto-join/removal, moderator removal, and size changes.
- `src/server/routes/cypher.ts`
  - Dashboard/API paths primarily mutate `CypherRotation`; make their effects visible to the live turn loop.
- `tests/menus/cypher.scenarios.test.ts`
  - The existing clear test only asserts that `activeCypherList` is empty, allowing the broken invariant to pass.

Use `rg` to enumerate every mutation of either collection. Do not assume the list above is exhaustive.

## Required design

### 1. Establish one authoritative membership model

Treat `CypherRotation` as the authoritative rotation state for this repair because Dynamic mode resolution, dashboard/API state, waiting-list lifecycle, and newer rotation features already use it. Treat `activeCypherList` as a compatibility projection while legacy turn rendering is migrated.

Create one well-named synchronization/coordinator boundary instead of scattering ad hoc array assignments. It should project the canonical active order into `activeCypherList` using the same `UserObject` instances. If a shared server-level mutation service is already appropriate, use it. Avoid creating a third independent source of truth.

Document the invariant in code:

```text
activeCypherList user IDs and order == cypherRotation.activeCypherDeque user IDs and order
```

Waiting-list ownership also needs an explicit decision. `CypherRotation.cypherWaitingDeque` should be canonical. If `server.waitingQueue` must remain for compatibility, synchronize or retire it in the touched mutation paths; do not leave Clear Cypher with a ghost waiting list.

### 2. Repair all in-scope mutation paths

At minimum:

- Clear must call the canonical clear operation, clear compatibility projections, clear compatible waiting state, and leave effective Dynamic mode as Classic.
- Join/add must add canonically and then refresh the projection.
- Leave/remove must remove canonically and then refresh the projection.
- Voice-channel removal and auto-join must preserve the invariant.
- Moderator add/remove selectors must preserve the invariant.
- Slash-command add/remove/size paths must preserve the invariant.
- Dashboard/API join, leave, clear, rotate, and sort must refresh the projection used by the live loop.
- Rotation advances must keep both orders aligned.

Do not use two independent mutations without checking their return values. For example, if capacity prevents an active add and routes a user to waiting, the compatibility active list must not add that user.

### 3. Make turn transitions atomic

The turn pipeline must not continue executing callbacks selected for Cypher after membership becomes empty.

Implement a clear turn-boundary rule:

1. Reconcile/project canonical membership before resolving the effective mode.
2. Resolve the effective mode.
3. Execute a stage.
4. After any stage that can mutate membership, re-evaluate the effective mode or return an explicit stage result.
5. If effective mode changed, end the current turn iteration immediately. Let the next iteration dispatch the new mode.

In particular, if `cypherStartOfTurn()` removes the final absent user, the same iteration must not continue into `makeCypherMessage()`, `cypherEndOfTurn()`, or `getNextInCypher()` as though it were still a Cypher turn.

Prefer an explicit return value or small transition sentinel over exceptions used as normal control flow. Preserve abort/cancellation handling.

### 4. Make fallback notification transition-based

There should be at most one user-facing notification when effective mode changes from Cypher to Classic. Multiple helpers must not independently send the same message.

Track the last effective mode per guild, or centralize notification in the turn dispatcher. Suggested wording:

```text
Cypher list is empty — continuing in Classic mode until someone joins.
```

This wording accurately describes Dynamic mode. It must not claim the configured policy was permanently changed.

Do not solve the loop by adding a delay, debounce, arbitrary retry count, or message rate limiter. Those can mask the spam but leave the invalid state and wrong dispatch intact.

### 5. Heal or fail visibly on invariant violations

At the canonical boundary, add lightweight debug logging when the two projections differ. Include guild ID and list lengths/IDs, but no tokens or private credentials. In production, reconcile from the canonical collection deterministically.

Avoid silently choosing different winners in different call sites. A deployed restart will clear the currently corrupted in-memory state; after deployment all mutations must preserve the invariant.

## Regression tests required

Add focused tests that prove behavior, not just individual method calls.

### Clear regression

1. Join two users.
2. Assert both representations contain the same IDs and order.
3. Clear the cypher through the real menu action.
4. Assert both active collections are empty.
5. Assert both waiting representations are empty or consistently projected.
6. Assert `getEffectiveMode('dynamic') === 'classic'`.

### Repeated-turn spam regression

Construct the formerly broken split state or invoke the mutation that created it. Run several turn-loop iterations with fake timers and a captured `messageSender`. Assert:

- No unbounded loop occurs.
- At most one Classic-transition message is emitted.
- No Cypher `TURN DONE`, `ALMOST DONE`, or `GET READY` headings are emitted after the transition.
- The Classic sequence is selected on the next iteration.

### Dynamic round trip

Prove this state sequence:

```text
Dynamic + empty -> effective Classic
join user       -> effective Cypher
clear/last leave -> effective Classic
join user again -> effective Cypher
```

Assert `server.cypherMode` remains `dynamic` for the entire test.

### Mid-turn final-user removal

Start a Cypher turn with one user who is absent from the approved voice channel. Let `cypherStartOfTurn()` remove them. Assert the remaining Cypher stages do not run, the turn ends cleanly, and only one transition notice is sent.

### Mutation-surface invariants

Add table-driven tests for the touched menu, slash, voice-state, and HTTP mutation paths. After each action, compare IDs and order in both representations. Include join, leave, add user, remove user, add all, clear, rotate, and sort where practical.

### Explicit-mode safety

Prove configured Classic remains Classic regardless of list contents and configured Nonstop remains Nonstop. The repair must not accidentally make those modes dynamic.

## Validation commands

Run the repository's actual scripts from `package.json`; at minimum:

```bash
npm run build
npm test -- --run
```

If the entire suite is too slow, run the focused tests first and then the full relevant menu, cypher, API, serialization, and turn-loop suites. Report exact pass/fail counts and any unrelated pre-existing failures.

Also run:

```bash
git diff --check
git status --short
```

Preserve unrelated worktree changes. Do not reset, overwrite, or stage files unrelated to this repair.

## Live acceptance test

After unit tests pass, validate in the Deep Breath room with the development bot:

1. Start Dynamic mode with at least one participant and confirm a normal Cypher turn.
2. Clear the cypher through the same UI/action that originally reproduced the bug.
3. Observe for at least two normal turn intervals.
4. Confirm no repeated fallback messages and no repeated Cypher headings.
5. Confirm one accurate transition notice at most.
6. Confirm Classic words continue normally.
7. Join one participant again.
8. Confirm the next appropriate turn uses Cypher behavior without changing the configured Dynamic policy manually.

Capture timestamps and relevant logs. Do not expose Discord tokens or environment secrets.

## Non-goals and constraints

- Do not modify Lavalink buffer-rewrite or sound-effect playback behavior; the audio PR merely inherits this bug.
- Do not convert Dynamic mode permanently to Classic on an empty list.
- Do not fix only the displayed message while leaving dispatch incorrect.
- Do not depend on a sleep, cooldown, or Discord rate limiting.
- Do not broadly rewrite all rotation features if a coordinator plus focused migration produces a safe PR.
- Do not remove compatibility structures until all consumers are migrated and tests prove parity.
- Keep the PR reviewable for comparison with Baron's concurrent implementation.

## Definition of done

The work is complete only when:

- There is one documented authoritative membership source.
- Every touched mutation path preserves membership/order invariants.
- Empty Dynamic mode selects Classic without mutating the configured mode.
- A mid-turn transition aborts the stale Cypher pipeline.
- The transition message appears no more than once.
- Joining again automatically restores effective Cypher behavior.
- Focused and relevant full tests pass.
- A real Deep Breath validation reproduces the original action without the loop.
- The final PR description includes the root cause, before/after state, exact tests, live evidence, and a note that the bug predates and is independent of the Lavalink changes.
