feat: implement multi-message mode and shuffle mode features

- Add multi-message mode with start/end/prompt markers
- Implement turn order shuffling with /turn-shuffling command
- Add channel mode state management
- Update hooks to handle multi-message mode behavior
- Update plugin config with new markers
- Update TASKLIST.md with completed tasks
This commit is contained in:
zhi
2026-04-02 04:36:36 +00:00
parent 684f8f9ee7
commit bfbe40b3c6
8 changed files with 251 additions and 98 deletions

View File

@@ -11,6 +11,8 @@
* - If sender IS in turn order → current = next after sender
*/
import { isMultiMessageMode, exitMultiMessageMode } from "./core/channel-modes.js";
export type ChannelTurnState = {
/** Ordered accountIds for this channel (auto-populated, shuffled) */
turnOrder: string[];
@@ -46,6 +48,26 @@ function shuffleArray<T>(arr: T[]): T[] {
return a;
}
function reshuffleTurnOrder(channelId: string, currentOrder: string[], lastSpeaker?: string): string[] {
const shufflingEnabled = getChannelShuffling(channelId);
if (!shufflingEnabled) return currentOrder;
const shuffled = shuffleArray(currentOrder);
// If there's a last speaker and they're in the order, ensure they're not first
if (lastSpeaker && shuffled.length > 1 && shuffled[0] === lastSpeaker) {
// Find another speaker to swap with
for (let i = 1; i < shuffled.length; i++) {
if (shuffled[i] !== lastSpeaker) {
[shuffled[0], shuffled[i]] = [shuffled[i], shuffled[0]];
break;
}
}
}
return shuffled;
}
// --- public API ---
/**
@@ -176,6 +198,13 @@ export function onNewMessage(channelId: string, senderAccountId: string | undefi
const state = channelTurns.get(channelId);
if (!state || state.turnOrder.length === 0) return;
// Check for multi-message mode exit condition
if (isMultiMessageMode(channelId) && isHuman) {
// In multi-message mode, human messages don't trigger turn activation
// We only exit multi-message mode if the end marker is detected in a higher-level hook
return;
}
if (isHuman) {
// Human message: clear wait-for-human, restore original order if overridden, activate from first
state.waitingForHuman = false;
@@ -330,6 +359,7 @@ export function onSpeakerDone(channelId: string, accountId: string, wasNoReply:
state.noRepliedThisCycle = new Set();
}
const prevSpeaker = state.currentSpeaker;
const next = advanceTurn(channelId);
// Check if override cycle completed (returned to first agent)
@@ -341,6 +371,18 @@ export function onSpeakerDone(channelId: string, accountId: string, wasNoReply:
return null; // go dormant after override cycle completes
}
// Check if we've completed a full cycle (all agents spoke once)
// This happens when we're back to the first agent in the turn order
const isFirstSpeakerAgain = next === state.turnOrder[0];
if (!wasNoReply && !state.overrideFirstAgent && next && isFirstSpeakerAgain && state.noRepliedThisCycle.size === 0) {
// Completed a full cycle without anyone NO_REPLYing - reshuffle if enabled
const newOrder = reshuffleTurnOrder(channelId, state.turnOrder, prevSpeaker);
if (newOrder !== state.turnOrder) {
state.turnOrder = newOrder;
console.log(`[dirigent][turn-debug] reshuffled turn order for channel=${channelId} newOrder=${JSON.stringify(newOrder)}`);
}
}
return next;
}