Commit Graph

33 Commits

Author SHA1 Message Date
zhi
8e3f8e7c4e feat: improve turn order initialization and member management
1. Turn order now activates via moderator on first creation (discovery-based),
   ensuring no agent consumes messages before the turn list is ready.
   All agents are blocked (dormant) until moderator sends handoff to first speaker.

2. channel-private-create: pre-populates turn order from allowedUserIds,
   filtering to only bot accounts (excluding humans and moderator).
   Immediately activates first speaker via moderator handoff.

3. channel-private-update: updates turn order when members are added/removed.
   If current speaker is removed, activates next available speaker.

4. Any member (human or bot) can now trigger turn activation when dormant,
   not just humans. Human messages still reset the cycle.

5. Added resolveAccountIdByUserId helper to map Discord user IDs back to
   account names from bot tokens.

6. turn-manager: added initTurnOrderPrePopulated, updateTurnMembers,
   activateFirstSpeaker, hasTurnState exports.
2026-03-02 00:55:55 +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
d6f908b813 feat(policy): add per-channel channelPolicies with hot-reload list mode/lists 2026-02-26 00:23:47 +00:00
6d463a4572 feat(config): add hot-reload config + listMode (human-list/agent-list) 2026-02-26 00:18:05 +00:00
0f526346f4 feat(tool): register optional discord_control tool in whispergate plugin and align defaults 2026-02-25 23:37:15 +00:00
32405fa3e2 feat(plugin): inject 🔚 ending instruction for bypass/end-symbol discord turns 2026-02-25 11:04:01 +00:00
f09972c083 feat(plugin): add sender normalization, TTL, and one-shot decisions 2026-02-25 10:43:11 +00:00
83b9d517ec feat(plugin): extract rule engine and harden channel/session handling 2026-02-25 10:39:47 +00:00
7728892d15 feat(plugin): add WhisperGate rule engine and model override hook 2026-02-25 10:36:59 +00:00