Commit Graph

57 Commits

Author SHA1 Message Date
3cde4a7795 refactor(policy): move policy CRUD to /dirigent_policy slash command with explicit channelId 2026-03-08 07:17:23 +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
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
96a1f18d1b refactor(plugin): extract channel/decision parsing modules and unify channelId resolution 2026-03-07 22:12:27 +00:00
zhi
211ad9246f feat: wait for human reply (waitIdentifier 👤)
- 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)
2026-03-07 17:32:28 +00:00
zhi
e4454bfc1a refactor: remove turn tools, rename discord tools, rewrite installer
- Remove turn management tools (turn-status/advance/reset) — internal only,
  accessible via /dirigent slash commands
- Rename discord tools: dirigent_discord_channel_create,
  dirigent_discord_channel_update, dirigent_discord_member_list
- Rewrite install script:
  - Dynamic OpenClaw dir resolution (OPENCLAW_DIR env → openclaw CLI → ~/.openclaw)
  - Plugin installed to $(openclaw_dir)/plugins/dirigent
  - New --update mode: git pull from latest branch + reinstall
  - Cleaner uninstall: removes installed plugin files
- Update docs (FEAT.md, README.md, CHANGELOG.md, TASKLIST.md)
2026-03-07 17:24:36 +00:00
zhi
0729e83b38 feat: split dirigent_tools into individual tools + human @mention override
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
2026-03-07 16:55:01 +00:00
zhi
5c9c979f26 fix: resolve pluginDir correctly and ensure no-reply-api is copied during install
- 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
2026-03-03 20:23:29 +00:00
zhi
b79cc1eb84 fix: use api.resolvePath for plugin directory
- Replace import.meta.url with api.resolvePath('.') for reliable path resolution
- Fixes no-reply API not starting due to incorrect pluginDir calculation
2026-03-03 18:16:47 +00:00
zhi
8e26744162 feat: lifecycle management - no-reply API + moderator bot follow gateway start/stop
- 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
2026-03-03 16:01:34 +00:00
zhi
af33d747d9 feat: complete Dirigent rename + all TASKLIST items
- 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
2026-03-03 10:10:27 +00:00
zhi
cf9be60145 fix: advance turn in before_message_write to prevent race condition
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.
2026-03-02 11:34:40 +00:00
zhi
6d17e6a911 fix: advance turn in before_message_write to prevent race condition
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.
2026-03-02 11:04:41 +00:00
zhi
d90083317b fix: channelId extraction, sender identification, and per-channel turn order
- 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.
2026-03-01 11:08:41 +00:00
zhi
a4bc9990db fix: add sessionAccountId mapping and use in before_message_write
- Add sessionAccountId Map to track sessionKey -> accountId
- Save accountId in before_model_resolve when resolving accountId
- Use sessionChannelId/sessionAccountId fallback in before_message_write
2026-03-01 09:16:55 +00:00
zhi
435a7712b8 fix: add sessionChannelId mapping for message_sent
- 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
2026-03-01 09:13:03 +00:00
zhi
9e754d32cf fix: make before_message_write synchronous
- 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
2026-03-01 04:59:45 +00:00
zhi
692111eeda fix: implement turn gate and handoff improvements
1. Fix channelId priority: use ctx.channelId > conv.chat_id > conv.channel_id
2. Add sessionAllowed state: track if session was allowed (true) or forced no-reply (false)
3. Add sessionInjected Set: implement one-time prependContext injection
4. Add before_message_write hook: detect NO_REPLY and handle turn advancement
   - forced no-reply: don't advance turn
   - allowed + NO_REPLY: advance turn + handoff
   - dormant state: don't trigger handoff
5. message_sent: continues to handle real messages with end symbols
2026-02-28 21:44:53 +00:00
zhi
80439b0912 Revert "Merge pull request 'fix: use systemPrompt instead of prependContext for end marker instruction' (#5) from fix/moderator-and-system-prompt into feat/turn-based-speaking"
This reverts commit 6a81f75fd0, reversing
changes made to 86fdc63802.
2026-02-28 19:34:37 +00:00
zhi
f03d32e15f fix: add turn check debug logs + one-time prompt injection
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
2026-02-28 19:27:39 +00:00
zhi
81758b9a54 fix: use systemPrompt instead of prependContext for end marker instruction
- 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
2026-02-28 19:24:50 +00:00
zhi
385990ab90 fix: turn check runs independently of evaluateDecision
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.
2026-02-28 12:41:46 +00:00
zhi
a6f2be44b7 feat: moderator bot presence via Discord Gateway
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.
2026-02-28 12:33:58 +00:00
zhi
54ff78cffe feat: moderator bot for turn handoff messages
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
2026-02-28 12:10:52 +00:00
zhi
476308d0df refactor: auto-managed turn order + dormant state + identity injection
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
2026-02-28 12:10:52 +00:00
zhi
1d8881577d feat: turn-based speaking + slash commands + enhanced prompts
1. Rename tool: whispergateway_tools → whispergate_tools

2. Turn-based speaking mechanism:
   - New turn-manager.ts maintains per-channel turn state
   - ChannelPolicy新增turnOrder字段配置发言顺序
   - before_model_resolve hook检查当前agent是否为发言人
   - 非当前发言人直接切换到no-reply模型
   - message_sent hook检测结束符或NO_REPLY时推进turn
   - message_received检测到human消息时重置turn

3. 注入提示词增强:
   - buildEndMarkerInstruction增加isGroupChat参数
   - 群聊时追加规则:与自己无关时主动回复NO_REPLY

4. Slash command支持:
   - /whispergate status - 查看频道策略
   - /whispergate turn-status - 查看轮流状态
   - /whispergate turn-advance - 手动推进轮流
   - /whispergate turn-reset - 重置轮流顺序
2026-02-28 12:10:52 +00:00
zhi
3749de981f fix: use configured endSymbols in injected prompt and exempt gateway keywords
- 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
2026-02-27 15:16:06 +00:00
zhi
f23d9049a7 fix: bypass DM sessions without metadata and make tool globally visible
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.
2026-02-27 14:14:39 +00:00
a4836097e4 fix: add default values for optional config fields
- Add default values for enableDiscordControlTool, enableWhispergatePolicyTool,
  discordControlApiBaseUrl, enableDebugLogs, debugLogChannelIds
- Merge defaults in both baseConfig and getLivePluginConfig
- Fixes issue where whispergateway_tools tool was not exposed due to missing
  config fields in openclaw.json
2026-02-26 22:16:23 +00:00
4622173787 fix: restore model after no-reply executes (needsRestore flag) 2026-02-26 13:39:43 +00:00
211a94233f chore(debug): log parsed sender/channel fields in hook decision recompute 2026-02-26 11:47:23 +00:00
zhi
f33dc13af4 fix: extract senderId from event.prompt instead of ctx in hooks
Root cause: PluginHookAgentContext in before_model_resolve only has
agentId, sessionKey, sessionId, workspaceDir, messageProvider.
senderId, channelId, input are NOT available in this hook phase.

The plugin was reading ctx.senderId (undefined) -> inHumanList=false
for ALL Discord sessions -> shouldUseNoReply=true -> all silenced.

Fix: use event.prompt which contains the full user message including
the 'Conversation info (untrusted metadata)' JSON block, and extract
sender_id from there. Same fix applied to before_prompt_build.
2026-02-26 08:56:24 +00:00
8eade446b3 fix(hooks): move first-pass decision to before_model_resolve and keep message_received for debug only 2026-02-26 02:33:15 +00:00
51149dd6a0 feat(debug): add whispergate hook diagnostics with channel-scoped debug logs 2026-02-26 01:58:01 +00:00
46ea43b3fe fix(rules): inject end-marker prompt for every non-no-reply discord turn 2026-02-26 01:47:27 +00:00
ca96779159 refactor(tooling): merge discord_control and whispergate_policy into whispergateway_tools 2026-02-26 01:31:31 +00:00
e5999743fe feat(policy-runtime): in-memory policy state with whispergate_policy tool and atomic persist 2026-02-26 00:35:17 +00:00
682d9a336e feat(policy-file): move channel overrides to standalone channelPoliciesFile with hot reload 2026-02-26 00:28:34 +00:00