openclaw config get returns redacted values for sensitive fields (apiKey,
token, etc). Reading a parent object and writing it back overwrites real
secrets with the __OPENCLAW_REDACTED__ sentinel.
- install step 5: set only the dirigent provider instead of all providers
- uninstall step 2: set only plugins.load.paths instead of entire plugins tree
Two bugs that prevented turn-manager dormancy from ever triggering:
1. isEmptyTurn too strict: agents output multi-line text ending with
"NO_REPLY" on the last line, but the regex ^NO_REPLY$ required the
entire string to match. Now checks only the last non-empty line.
2. blocked_pending counter inflation: non-speaker suppressions incremented
the counter but their stale NO_REPLYs were discarded at the
!isCurrentSpeaker early return without decrementing. Over a full cycle
the counter inflated by the number of suppressions, causing the agent's
real empty turn to be misidentified as stale when it finally arrived.
Fix: at both early-return points in agent_end (!isCurrentSpeaker and
!isTurnPending), drain blocked_pending when the turn is empty.
Also fixed: pollForTailMatch now uses any-message detection (instead of
tail-fingerprint content matching) with a 30 s timeout, avoiding infinite
polling when agents send concise Discord messages after verbose LLM output.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two bugs in concluded discussion channel handling:
1. before_model_resolve did not check rec.discussion.concluded, so it
still initialized the speaker list and ran turn management. Fixed by
returning NO_REPLY early for concluded discussions (same as report mode).
2. message_received fired for all agent VM contexts, causing multiple
"This discussion is closed" auto-replies per incoming message. Fixed
with process-level dedup keyed on channelId:messageId (same pattern
as agent_end runId dedup). Also fixed message_id extraction to look
in metadata.conversation_info.message_id first.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The moderator bot's own idle reminder message triggered message_received,
which saw senderId != currentSpeaker and called wakeFromDormant, immediately
undoing the dormant state just entered.
Fix: derive the moderator bot's Discord user ID from the token and skip
wake-from-dormant when the sender is the moderator bot itself.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
In busy channels, many messages arrive during a non-speaker turn,
each incrementing the blocked-pending counter. Without a cap the
counter grows faster than it drains, causing the speaker to spin
indefinitely consuming NO_REPLY completions.
Cap at MAX_BLOCKED_PENDING=3 in both incrementBlockedPending and
markTurnStarted (retroactive cap to recover from accumulated debt).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
message-received.ts:
- message_received ctx has channelId/accountId/conversationId (not
sessionKey). Add extraction from ctx.channelId and metadata.to
("channel:ID" format) before the conversation_info fallback.
agent-end.ts:
- When tail-match is interrupted, only call wakeFromDormant() if the
channel is actually dormant. For non-dormant interrupts (e.g. the
moderator bot's own trigger messages firing message_received on
other agents), fall through to normal advanceSpeaker() so the turn
cycle continues correctly instead of re-triggering the same speaker.
- Import isDormant from turn-manager.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
auth: "gateway" requires Bearer token in Authorization header,
which browser direct navigation never sends (no session cookies).
auth: "plugin" allows unauthenticated access on loopback, which
is sufficient since gateway is bound to 127.0.0.1 only.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Complete rewrite of the Dirigent plugin turn management system to work
correctly with OpenClaw's VM-context-per-session architecture:
- All turn state stored on globalThis (persists across VM context hot-reloads)
- Hooks registered unconditionally on every api instance; event-level dedup
(runId Set for agent_end, WeakSet for before_model_resolve) prevents
double-processing
- Gateway lifecycle events (gateway_start/stop) guarded once via globalThis flag
- Shared initializingChannels lock prevents concurrent channel init across VM
contexts in message_received and before_model_resolve
- New ChannelStore and IdentityRegistry replace old policy/session-state modules
- Added agent_end hook with tail-match polling for Discord delivery confirmation
- Added web control page, padded-cell auto-scan, discussion tool support
- Removed obsolete v1 modules: channel-resolver, channel-modes, discussion-service,
session-state, turn-bootstrap, policy/store, rules, decision-input
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add comprehensive tests for shuffle mode functionality
- Add comprehensive tests for multi-message mode functionality
- Add compatibility tests between different channel modes
- Update documentation to reflect completed implementation
- Mark all completed tasks as finished in TASKLIST.md
- Update CHANNEL_MODES_AND_SHUFFLE.md with implementation status and acceptance criteria
- Confirmed CSM MVP scope and requirements
- Finalized discussion idle reminder and closed channel templates
- Updated all remaining task statuses as completed
- Verified all functionality through tests
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.
Changes:
- Change default provider ID from 'dirigentway' to 'dirigent'
- Add no-reply model to agents.defaults.models allowlist during install
- Fix no-reply-api server default model name from 'dirigent-no-reply-v1' to 'no-reply'
This fixes the issue where dirigent/no-reply model was not showing in
'openclaw models list' and was being rejected as 'not allowed'.
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