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

@@ -1,6 +1,7 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { resolvePolicy, type DirigentConfig } from "../rules.js";
import { getTurnDebugInfo, onSpeakerDone, setWaitingForHuman } from "../turn-manager.js";
import { isMultiMessageMode } from "../core/channel-modes.js";
type DebugConfig = {
enableDebugLogs?: boolean;
@@ -181,15 +182,23 @@ export function registerBeforeMessageWriteHook(deps: BeforeMessageWriteDeps): vo
}
if (live.moderatorBotToken) {
const nextUserId = resolveDiscordUserId(api, nextSpeaker);
if (nextUserId) {
const schedulingId = live.schedulingIdentifier || "";
const handoffMsg = `<@${nextUserId}>${schedulingId}`;
void sendModeratorMessage(live.moderatorBotToken, channelId, handoffMsg, api.logger).catch((err) => {
api.logger.warn(`dirigent: before_message_write handoff failed: ${String(err)}`);
if (isMultiMessageMode(channelId)) {
// In multi-message mode, send the prompt marker instead of scheduling identifier
const promptMarker = live.multiMessagePromptMarker || "";
void sendModeratorMessage(live.moderatorBotToken, channelId, promptMarker, api.logger).catch((err) => {
api.logger.warn(`dirigent: before_message_write multi-message prompt marker failed: ${String(err)}`);
});
} else {
api.logger.warn(`dirigent: cannot resolve Discord userId for next speaker accountId=${nextSpeaker}`);
const nextUserId = resolveDiscordUserId(api, nextSpeaker);
if (nextUserId) {
const schedulingId = live.schedulingIdentifier || "➡️";
const handoffMsg = `<@${nextUserId}>${schedulingId}`;
void sendModeratorMessage(live.moderatorBotToken, channelId, handoffMsg, api.logger).catch((err) => {
api.logger.warn(`dirigent: before_message_write handoff failed: ${String(err)}`);
});
} else {
api.logger.warn(`dirigent: cannot resolve Discord userId for next speaker accountId=${nextSpeaker}`);
}
}
}
} else if (hasEndSymbol) {

View File

@@ -2,6 +2,7 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { evaluateDecision, type Decision, type DirigentConfig } from "../rules.js";
import { checkTurn } from "../turn-manager.js";
import { deriveDecisionInputFromPrompt } from "../decision-input.js";
import { isMultiMessageMode } from "../core/channel-modes.js";
type DebugConfig = {
enableDebugLogs?: boolean;
@@ -99,6 +100,16 @@ export function registerBeforeModelResolveHook(deps: BeforeModelResolveDeps): vo
noReply: true,
};
}
if (isMultiMessageMode(derived.channelId)) {
sessionAllowed.set(key, false);
api.logger.info(`dirigent: before_model_resolve forcing no-reply for multi-message mode channel=${derived.channelId} session=${key}`);
return {
model: ctx.model,
provider: ctx.provider,
noReply: true,
};
}
}
const resolvedAccountId = resolveAccountId(api, ctx.agentId || "");
if (resolvedAccountId) {

View File

@@ -2,6 +2,7 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { onNewMessage, setMentionOverride, getTurnDebugInfo } from "../turn-manager.js";
import { extractDiscordChannelId } from "../channel-resolver.js";
import type { DirigentConfig } from "../rules.js";
import { enterMultiMessageMode, exitMultiMessageMode, isMultiMessageMode } from "../core/channel-modes.js";
type DebugConfig = {
enableDebugLogs?: boolean;
@@ -79,21 +80,38 @@ export function registerMessageReceivedHook(deps: MessageReceivedDeps): void {
if (isHuman) {
const messageContent = ((e as Record<string, unknown>).content as string) || ((e as Record<string, unknown>).text as string) || "";
const mentionedUserIds = extractMentionedUserIds(messageContent);
// Handle multi-message mode markers
const startMarker = livePre.multiMessageStartMarker || "↗️";
const endMarker = livePre.multiMessageEndMarker || "↙️";
if (messageContent.includes(startMarker)) {
enterMultiMessageMode(preChannelId);
api.logger.info(`dirigent: entered multi-message mode channel=${preChannelId}`);
} else if (messageContent.includes(endMarker)) {
exitMultiMessageMode(preChannelId);
api.logger.info(`dirigent: exited multi-message mode channel=${preChannelId}`);
// After exiting multi-message mode, activate the turn system
onNewMessage(preChannelId, senderAccountId, isHuman);
} else {
const mentionedUserIds = extractMentionedUserIds(messageContent);
if (mentionedUserIds.length > 0) {
const userIdMap = buildUserIdToAccountIdMap(api);
const mentionedAccountIds = mentionedUserIds.map((uid) => userIdMap.get(uid)).filter((aid): aid is string => !!aid);
if (mentionedUserIds.length > 0) {
const userIdMap = buildUserIdToAccountIdMap(api);
const mentionedAccountIds = mentionedUserIds.map((uid) => userIdMap.get(uid)).filter((aid): aid is string => !!aid);
if (mentionedAccountIds.length > 0) {
await ensureTurnOrder(api, preChannelId);
const overrideSet = setMentionOverride(preChannelId, mentionedAccountIds);
if (overrideSet) {
api.logger.info(
`dirigent: mention override set channel=${preChannelId} mentionedAgents=${JSON.stringify(mentionedAccountIds)}`,
);
if (shouldDebugLog(livePre, preChannelId)) {
api.logger.info(`dirigent: turn state after override: ${JSON.stringify(getTurnDebugInfo(preChannelId))}`);
if (mentionedAccountIds.length > 0) {
await ensureTurnOrder(api, preChannelId);
const overrideSet = setMentionOverride(preChannelId, mentionedAccountIds);
if (overrideSet) {
api.logger.info(
`dirigent: mention override set channel=${preChannelId} mentionedAgents=${JSON.stringify(mentionedAccountIds)}`,
);
if (shouldDebugLog(livePre, preChannelId)) {
api.logger.info(`dirigent: turn state after override: ${JSON.stringify(getTurnDebugInfo(preChannelId))}`);
}
} else {
onNewMessage(preChannelId, senderAccountId, isHuman);
}
} else {
onNewMessage(preChannelId, senderAccountId, isHuman);
@@ -101,8 +119,6 @@ export function registerMessageReceivedHook(deps: MessageReceivedDeps): void {
} else {
onNewMessage(preChannelId, senderAccountId, isHuman);
}
} else {
onNewMessage(preChannelId, senderAccountId, isHuman);
}
} else {
onNewMessage(preChannelId, senderAccountId, isHuman);