- Add waitIdentifier config (default: 👤) to DirigentConfig and plugin schema
- Prompt injection: tells agents to end with 👤 when they need a human reply,
warns to use sparingly (only when human is actively participating)
- Detection in before_message_write and message_sent hooks
- Turn manager: new waitingForHuman state
- checkTurn() blocks all agents when waiting
- onNewMessage() clears state on human message
- Non-human messages ignored while waiting
- resetTurn() also clears waiting state
- All agents routed to no-reply model during waiting state
- Update docs (FEAT.md, CHANGELOG.md, TASKLIST.md, README.md)
Feature 1: Split dirigent_tools
- Replace monolithic dirigent_tools (9 actions) with 9 individual tools
- Discord: dirigent_channel_create, dirigent_channel_update, dirigent_member_list
- Policy: dirigent_policy_get, dirigent_policy_set, dirigent_policy_delete
- Turn: dirigent_turn_status, dirigent_turn_advance, dirigent_turn_reset
- Extract shared executeDiscordAction() helper
Feature 2: Human @mention override
- When humanList user @mentions agents, temporarily override turn order
- Only mentioned agents cycle, ordered by their turn order position
- Original order restores when cycle returns to first agent or all NO_REPLY
- New: setMentionOverride(), hasMentionOverride(), extractMentionedUserIds()
- New: buildUserIdToAccountIdMap() for reverse userId→accountId resolution
Bump version to 0.3.0
- Use import.meta.url instead of api.resolvePath('.') to get script directory
- Add debug logging for no-reply-api and moderator bot startup
- Copy no-reply-api to dist during installation
- Replace import.meta.url with api.resolvePath('.') for reliable path resolution
- Fixes no-reply API not starting due to incorrect pluginDir calculation
- Added gateway_start hook: spawns no-reply API as child process, then starts moderator bot
- Added gateway_stop hook: kills no-reply API process, stops moderator bot
- No-reply API server.mjs is located relative to plugin dir via import.meta.url
- Moderator presence moved from register() to gateway_start for proper lifecycle
- Fix enableWhispergatePolicyTool → enableDirigentPolicyTool in config schema and example
- Fix whisper-gateway → dirigentway in install script
- Add v0.2.0 changelog entry
- Improve README with scheduling identifier docs and English text
- Clean up plugin README with moderator handoff format docs
- Reformat TASKLIST with cleaner done markers
- Task 1: Identity prompt now includes Discord userId
- Task 2: Added configurable schedulingIdentifier (default: ➡️)
- Task 3: Moderator handoff uses <@userId>+identifier instead of semantic messages
- Task 4: All prompts/comments/help text converted to English
- Task 5: Full project rename WhisperGate → Dirigent across all files
Breaking: config key changed from plugins.entries.whispergate to plugins.entries.dirigent
Breaking: channel policies file renamed to dirigent-channel-policies.json
Breaking: tool name changed from whispergate_tools to dirigent_tools
When a speaker finishes with an end symbol, the turn was only advanced
in the message_sent hook. But by that time, the message had already been
broadcast to other agents, whose before_model_resolve ran with the old
turn state, causing them to be blocked by the turn gate (forced no-reply).
Fix:
- Move turn advance for both NO_REPLY and end-symbol cases to
before_message_write, which fires before the message is broadcast.
- Guard 1: Only the current speaker can advance the turn (accountId check).
- Guard 2: Only process assistant messages (role check). before_message_write
fires for incoming user messages too, which contain end symbols from other
agents and would cause cascading turn advances.
- Use sessionTurnHandled set to prevent double-advancing in message_sent.
When a speaker finishes with an end symbol, the turn was only advanced
in the message_sent hook. But by that time, the message had already been
broadcast to other agents, whose before_model_resolve ran with the old
turn state, causing them to be blocked by the turn gate (forced no-reply).
Fix: Move turn advance for both NO_REPLY and end-symbol cases to
before_message_write, which fires before the message is broadcast.
Use sessionTurnHandled set to prevent double-advancing in message_sent.
- Fix channelId extraction: ctx.channelId is platform name ('discord'), not
the Discord channel snowflake. Now extracts from conversation_label field
('channel id:123456') and sessionKey fallback (':channel:123456').
- Fix extractDiscordChannelId: support 'discord:channel:xxx' format in
addition to 'channel:xxx' for conversationId/event.to fields.
- Fix sender identification in message_received: event.from returns channel
target, not sender ID. Now uses event.metadata.senderId for humanList
matching so human messages correctly reset turn order.
- Fix per-channel turn order: was using all server-wide bot accounts from
bindings, causing deadlock when turn landed on bots not in the channel.
Now dynamically tracks which bot accounts are seen per channel via
message_received and only includes those in turn order.
- Always save sessionChannelId/sessionAccountId mappings in before_model_resolve
regardless of turn check result, so downstream hooks can use them.
- Add comprehensive debug logging to message_sent hook.
- Add sessionAccountId Map to track sessionKey -> accountId
- Save accountId in before_model_resolve when resolving accountId
- Use sessionChannelId/sessionAccountId fallback in before_message_write
- Add sessionChannelId Map to track sessionKey -> channelId
- Save channelId in before_model_resolve when we have derived.channelId
- Fix message_sent to use sessionChannelId fallback when ctx.channelId is undefined
- Add debug logging to message_sent
- Remove async/await from before_message_write hook
- Use fire-and-forget for sendModeratorMessage (void ... .catch())
- OpenClaw's before_message_write is synchronous, not async
1. Turn check improvements:
- Add debug logs for ctx.agentId, resolved accountId, turnOrder length
- Fallback to ctx.accountId if resolveAccountId fails
- Add resolveDiscordUserId debug logs for handoff troubleshooting
2. One-time prompt injection:
- Add sessionInjected Set to track injected sessions
- Use prependContext (not systemPrompt) but only inject once per session
- Skip subsequent injections with debug log
- Change before_prompt_build hook to return systemPrompt instead of prependContext
- This ensures the end marker instruction is injected once per session as a system prompt, not repeatedly prepended to each user message
- Add moderatorBotToken to CONFIG.example.json for documentation
Root causes:
1. Multiple plugin subsystems each called startModeratorPresence,
creating competing WebSocket connections to the same bot token.
Discord only allows one connection per bot → 4008 rate limit →
infinite reconnect loop (1000+ connects → token reset by Discord)
2. Invalid session (op 9) handler called scheduleReconnect, but the
new connection would also get kicked → cascading reconnects
Fixes:
- Singleton guard: startModeratorPresence is a no-op if already started
- cleanup() nullifies old ws handlers before creating new connection
- Stale ws check: all callbacks verify they belong to current ws
- Exponential backoff with cap (max 60s) instead of fixed 2-5s delay
- heartbeat ACK tracking: detect zombie connections
- Non-recoverable codes (4004) properly stop all reconnection
Turn order should be enforced for ALL messages, not just non-human ones.
Previously, human messages bypassed turn check because they go through
human_list_sender path with shouldUseNoReply=false. Now turn check
always runs when channel has turn state.
Use Node.js built-in WebSocket to maintain a minimal Discord Gateway
connection for the moderator bot, keeping it 'online' with a
'Watching Moderating' status. Handles heartbeat, reconnect, and resume.
Also fix package-plugin.mjs to include moderator-presence.ts in dist.
Add a dedicated moderator Discord bot that sends handoff messages when
the current speaker says NO_REPLY. This solves the wakeup problem.
Flow:
1. Agent A is current speaker, receives message
2. Agent A responds with NO_REPLY
3. Plugin detects NO_REPLY in message_sent hook, advances turn to Agent B
4. Plugin sends via moderator bot: '轮到(@AgentB)了,如果没有想说的请直接回复NO_REPLY'
5. This real Discord message triggers Agent B's session
6. Turn manager allows Agent B to respond
Implementation:
- moderatorBotToken config field for the moderator bot's Discord token
- userIdFromToken() extracts Discord user ID from bot token (base64)
- resolveDiscordUserId() maps accountId → Discord user ID via account tokens
- sendModeratorMessage() calls Discord REST API directly
- message_received ignores moderator bot messages (transparent to turn state)
- Moderator bot is NOT in the turn order
Turn system redesign:
- Turn order auto-populated from config bindings (all bot accounts)
- No manual turnOrder config needed
- Humans (humanList) excluded from turn order automatically
- Dormant state: when all agents NO_REPLY in a cycle, currentSpeaker=null
- Reactivation: any new message wakes the system
- Human message → start from first in order
- Bot not in order → start from first
- Bot in order → next after sender
- Skip already-NO_REPLY'd agents when advancing
Identity injection:
- Group chat prompts now include agent identity
- Format: '你是 {name}(Discord 账号: {accountId})'
Other:
- Remove turnOrder from ChannelPolicy (no longer configurable)
- Add TURN-WAKEUP-PROBLEM.md documenting the NO_REPLY wake-up challenge
- Update message_received to call onNewMessage with proper human detection
- Update message_sent to call onSpeakerDone with NO_REPLY tracking
getLastChar used t[t.length-1] which only gets the trailing surrogate
of emoji like 🔚 (U+1F51A, a surrogate pair in UTF-16). This meant
end symbol matching ALWAYS failed for emoji symbols, causing every
non-humanList message to hit rule_match_no_end_symbol -> no-reply.
Fix: use Array.from(t) to correctly split by Unicode code points.
- buildEndMarkerInstruction() replaces hardcoded END_MARKER_INSTRUCTION,
dynamically using the resolved policy's endSymbols
- Instruction now explicitly exempts gateway keywords (NO_REPLY, HEARTBEAT_OK)
from requiring end symbols
- Export resolvePolicy from rules.ts for reuse in before_prompt_build hook
getLastChar was checking the last character of the full event.prompt,
which includes Conversation/Sender metadata blocks appended by OpenClaw
after the actual message. This meant end symbols like 🔚 at the end of
the message body were invisible — the last char was always backtick or
whitespace from the metadata JSON block.
Fix: strip trailing '(untrusted metadata)' blocks before extracting
the last character. This only affects non-humanList senders (humanList
senders bypass end symbol check via human_list_sender reason).
1. DM bypass: when neither senderId nor channelId can be extracted from
the prompt (DM sessions lack untrusted conversation info), skip the
no-reply gate and allow the message through with end-marker injection.
2. Tool visibility: change whispergateway_tools registration from
optional=true to optional=false so all agents can see the tool
without needing explicit tools.allow entries.