When the turn manager determines it's an agent's turn (checkTurn.allowed),
the rules engine's evaluateDecision() could still override the model to
no-reply with reason 'rule_match_no_end_symbol'. This happened because:
1. The sender of the triggering message (another agent) was not in the
humanList, so the rules fell through to the end-symbol check.
2. getLastChar() operates on the full prompt (including system content
like Runtime info), so it never found the end symbol even when the
actual message ended with one.
Fix: return early from before_model_resolve after the turn check passes,
skipping the rules-based no-reply override entirely. The turn manager is
the authoritative source for multi-agent turn coordination.
Tested: 3-agent counting chain ran successfully (3→11) with correct
NO_REPLY handling when count exceeded threshold.
Problems fixed:
1. before_message_write treated empty content (isEmpty) as NO_REPLY.
Tool-call-only assistant messages (thinking + toolCall, no text)
had empty extracted text, causing false NO_REPLY detection.
In single-agent channels this immediately set turn to dormant,
blocking all subsequent responses for the entire model run.
2. Added explicit toolCall/tool_call/tool_use detection in
before_message_write — skip turn processing for intermediate
tool-call messages entirely.
3. no-reply-api/server.mjs: default model name changed from
'dirigent-no-reply-v1' to 'no-reply' to match the configured
model id in openclaw.json, fixing model list discovery.
Changes:
- plugin/hooks/before-message-write.ts: toolCall detection + remove isEmpty
- plugin/hooks/message-sent.ts: remove isEmpty from wasNoReply
- no-reply-api/server.mjs: fix default model name
- dist/dirigent/index.ts: same fixes applied to monolithic build
- dist/no-reply-api/server.mjs: same model name fix
- 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