100 Commits

Author SHA1 Message Date
32dc9a4233 refactor: new design — sidecar services, moderator Gateway client, tool execute API
- Replace standalone no-reply-api Docker service with unified sidecar (services/main.mjs)
  that routes /no-reply/* and /moderator/* and starts/stops with openclaw-gateway
- Add moderator Discord Gateway client (services/moderator/index.mjs) for real-time
  MESSAGE_CREATE push instead of polling; notifies plugin via HTTP callback
- Add plugin HTTP routes (plugin/web/dirigent-api.ts) for moderator → plugin callbacks
  (wake-from-dormant, interrupt tail-match)
- Fix tool registration format: AgentTool requires execute: not handler:; factory form
  for tools needing ctx
- Rename no-reply-process.ts → sidecar-process.ts, startNoReplyApi → startSideCar
- Remove dead config fields from openclaw.plugin.json (humanList, agentList, listMode,
  channelPoliciesFile, endSymbols, waitIdentifier, multiMessage*, bypassUserIds, etc.)
- Rename noReplyPort → sideCarPort
- Remove docker-compose.yml, dev-up/down scripts, package-plugin.mjs, test-no-reply-api.mjs
- Update install.mjs: clean dist before build, copy services/, drop dead config writes
- Update README, Makefile, smoke script for new architecture

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 08:07:59 +01:00
d8ac9ee0f9 fix: correct dormancy detection — isEmptyTurn last-line + blocked_pending drain
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>
2026-04-09 15:50:19 +01:00
9e61af4a16 fix: concluded discussions suppress turns and send single auto-reply
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>
2026-04-09 09:02:10 +01:00
27c968fa69 fix: prevent idle reminder from re-waking dormant discussion channel
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>
2026-04-09 08:17:55 +01:00
c40b756bec fix: cap blocked-pending counter to prevent unbounded drain loops
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>
2026-04-09 08:11:44 +01:00
74e6d61d4d fix: correct channelId extraction and dormant check in hooks
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>
2026-04-09 06:24:05 +01:00
b9cbb7e895 fix: change control page routes from gateway to plugin auth
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>
2026-04-09 01:15:29 +01:00
b5196e972c feat: rewrite plugin as v2 with globalThis-based turn management
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>
2026-04-08 22:41:25 +01:00
dea345698b feat: add /list-guilds slash command 2026-04-05 19:42:36 +00:00
fd33290266 feat: add /add-guild slash command 2026-04-05 19:24:06 +00:00
zhi
895cfe3bab fix: align discussion workspace and tool schemas 2026-04-02 11:52:20 +00:00
zhi
7bccb660df fix: wake origin workflow after discussion callback 2026-04-02 08:20:23 +00:00
zhi
15f7d211d7 test: cover discussion hook close and idle behavior 2026-04-02 07:19:57 +00:00
zhi
b11c15d8c8 test: stabilize channel mode and discussion coverage 2026-04-02 06:18:16 +00:00
zhi
b40838f259 Refine discussion closure messaging 2026-04-02 05:48:00 +00:00
zhi
b7b405f416 test: cover discussion callback flows 2026-04-02 05:18:04 +00:00
zhi
7670d41785 Complete A6 and A7 tasks: Implement closed discussion channel protections and update tasklist
- Complete tasks A6.1, A6.2, A6.3: Turn manager discussion mode handling
- Complete tasks A7.1, A7.2, A7.3, A7.5: Hook-level discussion channel protections
- Add closed discussion channel check to message-sent hook to prevent handoffs
- Update TASKLIST.md to mark completed tasks with [x]
2026-04-02 05:04:47 +00:00
zhi
d44204fabf feat: wire channel mode runtime config and docs 2026-04-02 04:48:20 +00:00
zhi
bfbe40b3c6 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
2026-04-02 04:36:36 +00:00
zhi
684f8f9ee7 Refine discussion moderator messaging flow 2026-04-02 04:18:45 +00:00
zhi
d9bb5c2e21 Fix discussion channel closure handling 2026-04-02 03:49:03 +00:00
zhi
62cd2f20cf feat(csm): bootstrap discussion callback flow 2026-04-02 02:35:08 +00:00
0a99abc7e3 Use pluginConfig directly for Dirigent runtime config 2026-04-01 20:01:57 +00:00
nav
4905f37c1a fix: limit no-reply keywords and log 2026-03-10 13:53:32 +00:00
nav
e6b20e9d52 feat: treat NO/empty as gateway no-reply 2026-03-10 13:42:50 +00:00
zhi
08f42bfd92 fix: skip rules-based no-reply override when turn manager allows agent
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.
2026-03-09 21:16:53 +00:00
zhi
1f846fa7aa fix: stop treating empty content as NO_REPLY, detect tool calls
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
2026-03-09 12:00:36 +00:00
7e0f187f34 fix(turn): keep mention override speaker/order when membership refresh runs 2026-03-08 07:56:26 +00:00
a995b7d6bf debug(turn): add mention-override and ensureTurnOrder state transition logs 2026-03-08 07:38:33 +00:00
3cde4a7795 refactor(policy): move policy CRUD to /dirigent_policy slash command with explicit channelId 2026-03-08 07:17:23 +00:00
d497631b99 refactor(tools): drop guild member-list tool and remove dirigent_ prefix from tool names 2026-03-08 07:05:37 +00:00
bb10d6913e fix(plugin): start moderator reliably and colocate no-reply-api under plugin dir 2026-03-08 06:57:22 +00:00
4d50826f2a refactor(installer): rename to install.mjs, remove record/restore flow, add --no-reply-port and wire config port 2026-03-08 06:38:33 +00:00
e6b445fb97 chore(cleanup): remove discord-control-api sidecar and stale config/docs hooks 2026-03-08 06:19:41 +00:00
0806edb7d9 feat(cache): include source and guildId metadata in channel member cache 2026-03-08 06:15:19 +00:00
12b0f4c6cc feat(turn-init): persist channel bot membership cache to local file 2026-03-08 06:13:38 +00:00
307235e207 feat(turn-init): use internal Discord channel member resolver for bootstrap (no public tool) 2026-03-08 06:08:51 +00:00
121786e6e3 refactor(tools): remove channel-member-list public tool (internal-use only) 2026-03-08 05:53:38 +00:00
3e35da83ca feat(discord-tools): replace sidecar API with direct Discord REST + channel member listing 2026-03-08 05:50:10 +00:00
63009f3925 refactor(plugin): extract shared session state and decision pruning 2026-03-08 05:43:52 +00:00
ea501ccef8 refactor(plugin): extract no-reply child process lifecycle module 2026-03-08 05:39:27 +00:00
0c06aeb36c refactor(plugin): extract common utils and moderator discord helpers 2026-03-08 05:31:08 +00:00
f9ac401877 refactor(plugin): extract turn bootstrap/cache helpers into core module 2026-03-08 05:25:53 +00:00
05b902c0b2 refactor(plugin): extract identity and mention helpers into core modules 2026-03-08 05:19:40 +00:00
e258e99ed2 refactor(plugin): extract live config and policy store modules 2026-03-07 22:38:11 +00:00
d021b0c06c refactor(plugin): extract discord/policy tool registration module 2026-03-07 22:33:28 +00:00
3a8b85eb7b refactor(plugin): extract dirigent slash command and fix recursive prepare copy 2026-03-07 22:27:03 +00:00
b63c1dfe94 refactor(plugin): extract message_received hook and slim index imports 2026-03-07 22:24:48 +00:00
5c4340d5a9 refactor(plugin): extract before_prompt_build and before_message_write hooks 2026-03-07 22:21:16 +00:00
c15ea0d471 refactor(plugin): extract before_model_resolve and message_sent hooks 2026-03-07 22:18:09 +00:00