fix(channels): sync channel messages to active OpenClaw conversation#881
fix(channels): sync channel messages to active OpenClaw conversation#881
Conversation
…in AionUI When an active OpenClaw session exists, route channel (Feishu/Telegram/DingTalk) messages to it using a separate gateway session key, avoiding rs_ 404 errors. Gateway broadcasts responses to all WebSocket clients so the main AionUI conversation receives and renders replies automatically. - Add sendChannelMessage() to OpenClawAgent with pre-initialized streaming state to prevent duplicate responses from chat and agent event handlers - Add sendChannelMessage() wrapper to OpenClawAgentManager with cronBusyGuard - Detect OpenClaw tasks in ChannelMessageService and route via sendChannelMessage instead of regular sendMessage; persist user messages and emit IPC events - Detect active OpenClaw tasks in ActionExecutor before session creation to ensure channel messages always land in the original OpenClaw conversation
There was a problem hiding this comment.
Code Review
CRITICAL Issues
1. OpenClawAgentManager.sendChannelMessage() 未在成功路径释放 cronBusyGuard,会导致会话长期处于 busy 状态
File: src/process/task/OpenClawAgentManager.ts:206-231
async sendChannelMessage(data: { content: string; msg_id?: string }): Promise<void> {
cronBusyGuard.setProcessing(this.conversation_id, true);
this.status = 'running';
try {
await this.bootstrap;
const result = await this.agent.sendChannelMessage(data.content);
if (result.success === false) {
throw new Error(result.error.message || 'Failed to send channel message');
}
} catch (error) {
cronBusyGuard.setProcessing(this.conversation_id, false);
this.status = 'finished';
...
throw error;
}
}Problem: 成功发送后没有 cronBusyGuard.setProcessing(..., false),而 OpenClaw 的 busy 释放目前依赖 handleSignalEvent 收到 finish 才会执行。但你在 OpenClawAgent.sendChannelMessage() 里通过预置 currentStreamMsgId 让 handleChatEvent(delta) 跳过,且 finish 是否一定会以 agent.lifecycle(end) 或 chat.final 的形式到达并触发 handleEndTurn()/finish 并不受这里保证。一旦 gateway 没有发出对应 finish(或事件顺序/类型变化),busy 会被永久卡住,直接影响后续消息发送/任务调度。
Fix: 在 sendChannelMessage 成功路径也做兜底释放(推荐 finally),并保持与 sendMessage() 一致的状态收敛逻辑。例如:
async sendChannelMessage(data: { content: string; msg_id?: string }): Promise<void> {
cronBusyGuard.setProcessing(this.conversation_id, true);
this.status = 'running';
try {
await this.bootstrap;
const result = await this.agent.sendChannelMessage(data.content);
if (result.success === false) {
throw new Error(result.error.message || 'Failed to send channel message');
}
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
this.emitErrorMessage(`Failed to send channel message: ${errorMsg}`);
throw error;
} finally {
// 兜底:避免 gateway 未发 finish 时 busy 永久不释放
cronBusyGuard.setProcessing(this.conversation_id, false);
this.status = 'finished';
}
}HIGH Issues
2. ChannelMessageService 对 OpenClaw 的用户消息持久化使用了错误的 conversationId,会写入错误会话/造成数据错乱
File: src/channels/agent/ChannelMessageService.ts:206-224
const userMessage: TMessage = {
id: msgId,
msg_id: msgId,
type: 'text',
position: 'right',
conversation_id: conversationId,
content: { content: message },
createdAt: Date.now(),
};
addMessage(conversationId, userMessage);Problem: 这里的 conversationId 来自 ActionExecutor 的 session 路由逻辑。你在 ActionExecutor 中会把渠道消息强制路由到“当前活跃的 OpenClaw 任务”:
const activeOpenClawTask = WorkerManage.listTasks().find((t) => t.type === 'openclaw-gateway');
if (activeOpenClawTask && session?.conversationId !== activeOpenClawTask.id) {
session = this.sessionManager.createSessionWithConversation(channelUser, activeOpenClawTask.id, 'acp', undefined, chatId);
}但 activeOpenClawTask.id 很可能是 task id,而不是 conversation_id(在本项目里两者并不总是同一个概念;而且 OpenClawAgentManager 内部明确有 this.conversation_id = data.conversation_id)。如果这里传入的是 task id,则 ChannelMessageService 会把用户消息写入一个不存在/不匹配的 conversation,导致:
- DB 消息落错会话(侧边栏/历史记录错乱)
- renderer 订阅的 conversation stream 与 DB 不一致(表现为“UI看到了,但刷新后没了/跑到别的会话里”)
Fix:
- 在
ActionExecutor路由时,确保使用的是 conversation_id,而不是 task id。最稳妥做法是从 task 实例上取conversation_id(如果BaseAgentManager暴露),或在listTasks()返回结构中明确区分id与conversationId。 - 同时在
ChannelMessageService里避免“盲写 DB”,可以在写入前用db.getConversation(conversationId)校验存在性(失败则降级走原逻辑或直接报错),确保不会把消息写进错误会话。
3. 新增的 console.log 属于生产代码调试输出,违反 debug hygiene(会污染日志/泄露运行信息)
File: src/agent/openclaw/index.ts:108-116, src/process/task/OpenClawAgentManager.ts:170-174
console.log('[OpenClawAgent] Using gateway auth token from config');
...
console.log('[OpenClawAgentManager] Session key updated:', sessionKey);Problem: PR 引入/保留了新的 console.log(ESLint no-console: warn,但按项目规范需要在 review 中明确指出)。这些日志在生产环境会持续输出,且包含连接/会话相关信息,不适合作为默认日志级别。
Fix:
- 替换为项目统一 logger(若有),或至少改为
console.debug并确保生产构建会过滤;更推荐加一个可控的 debug flag。 - 对 auth token/password 的分支提示尤其建议移除或降级(避免给攻击者提供“当前使用了哪种认证方式”的侧信道信息)。
MEDIUM Issues
4. ChannelMessageService 中 dbResult 读取后未使用(死代码/可读性问题)
File: src/channels/agent/ChannelMessageService.ts
你把 db / dbResult 提前到 try 外:
const db = getDatabase();
const dbResult = db.getConversation(conversationId);但后续只用它来计算 isFromChannel,而该计算在 try 内进行;当前写法会让读者误以为 dbResult 在 try 外还有其他用途,并且 dbResult 的失败也不会被 try/catch 覆盖(虽然这里不一定会 throw,但结构上不一致)。
Recommendation: 把 db/getConversation 放回 try 内,或至少把 isFromChannel 的计算也移到 try 外并明确注释“不会 throw”。保持逻辑聚合,减少误读。
Summary
| Level | Count |
|---|---|
| CRITICAL | 1 |
| HIGH | 3 |
| MEDIUM | 1 |
🤖 This review was generated by AI and may contain inaccuracies. Please focus on issues you agree with and feel free to disregard any that seem incorrect. Thank you for your contribution!
…w agents Non-OpenClaw agents (Gemini/ACP/Codex) already handle user message display through their own sendMessage flow. The extra IPC user_content emission was causing user messages to be concatenated with AI responses in AionUI.
Add channelTurnActive flag to skip ALL chat.event processing during channel turns. Previously, handleEndTurn (from agent lifecycle end) reset the currentStreamMsgId, allowing late-arriving chat.event deltas to be processed as new content, causing duplicate AI responses in AionUI.
|
+م |
…hannel turns Instead of blocking all chat events during channel turns (which prevented chat.event.final from emitting the finish signal when no agent lifecycle event arrives), keep currentStreamMsgId set during channel turns so the existing delta-skip logic continues to block late chat.event deltas. This ensures: - chat.event.final always calls handleEndTurn → finish signal emitted - Late chat.event deltas still blocked (currentStreamMsgId remains set) - Works regardless of whether gateway sends agent or chat events
…UI rendering Move user message persistence and IPC emission from ChannelMessageService into OpenClawAgentManager.sendChannelMessage. Emit user_content to both openclawConversation and conversation response streams, matching the pattern used by handleStreamEvent for AI responses. Pass msg_id from ChannelMessageService to sendChannelMessage for proper message tracking.
Instead of separate ipcBridge.emit calls for user_content, route the channel user message through handleStreamEvent — the same proven code path used by AI responses. This ensures DB persistence and IPC emission use the identical execution context that works for AI content streaming. Skip channelEventBus for user_content to avoid echoing the user's own message back to the channel platform.
Previously channelTurnActive prevented handleEndTurn from resetting currentStreamMsgId, causing the second channel message's AI response to reuse the first message's msg_id. This led to content concatenation and reversed message order in AionUI. Now handleEndTurn always resets currentStreamMsgId to null so each turn gets a fresh msg_id. Late chat.event deltas during channel turns are blocked by the channelTurnActive flag directly in handleChatEvent.
|
Great PR! This is a useful improvement for channel message routing. The approach of using a separate gateway session key () to avoid rs_404 conflicts with the main AionUi session is clever. The code changes look clean and the separation of concerns is good. One suggestion for the test plan: Consider testing the edge case where multiple OpenClaw sessions are active simultaneously. Does it always route to the most recently active one, or does it have a specific selection logic? Also, might be worth documenting in the code comments which session gets selected when there are multiple OpenClaw sessions available. |
|
Great PR! This is a useful improvement for channel message routing. The approach of using a separate gateway session key ( One suggestion for the test plan: Consider testing the edge case where multiple OpenClaw sessions are active simultaneously. Does it always route to the most recently active one, or does it have a specific selection logic? Also, might be worth documenting in the code comments which session gets selected when there are multiple OpenClaw sessions available. |
@ImL1s the edge case where multiple OpenClaw sessions are active simultaneously I have indeed considered it, but I haven't figured out how to do it yet. Do you have any good solutions for this? |
|
Great question! Here are a few approaches for handling multiple active OpenClaw sessions: Option 1: Most Recently Active (Recommended)Track the last activity timestamp for each OpenClaw session and route to the most recently used one. This feels intuitive - users expect messages to go to the session they just interacted with. Implementation idea: // In WorkerManage or OpenClawAgentManager
const activeOpenClawTasks = WorkerManage.listTasks().filter(t => t.type === openclaw-gateway);
// Sort by lastActivity timestamp (you'd need to track this)
const mostRecent = activeOpenClawTasks.sort((a, b) => b.lastActivity - a.lastActivity)[0];You could update Option 2: Primary/Default SessionAdd a flag to mark one session as the "primary" or "default" recipient. Users could explicitly set which OpenClaw session should receive channel messages. This requires a UI change to let users select/assign a primary session, but gives explicit control. Option 3: Fallback ChainCombine both approaches:
For now, you could add a code comment explaining the current behavior (first OpenClaw task in the list gets selected) and noting that this could be improved with activity tracking in the future. Would any of these work for your use case? I'd be happy to help implement whichever direction you choose! |
- CRITICAL: use finally block in sendChannelMessage to always release cronBusyGuard, preventing permanent busy state when gateway does not emit a finish event - HIGH: replace console.log with console.debug for auth and session key logs to avoid polluting production logs - MEDIUM: move db/dbResult into try block in ChannelMessageService for better readability and consistent error handling Note: the task id vs conversation_id concern (review issue #2) is not applicable — in WorkerManage, tasks are keyed by conversation.id, so listTasks().id is always the conversation_id.
…uting Session already stores the correct conversationId per chatId. getTaskByIdRollbackBuild() handles task loading from memory/DB/file. The forced override broke per-chat isolation and caused non-deterministic routing when multiple OpenClaw sessions existed.
- Fix GeminiRequest field name: generationConfig -> config to match @google/genai SDK's GenerateContentParameters interface, ensuring responseModalities is actually sent to the API - Add model name detection for image-capable models (image/banana) to always set responseModalities: ['IMAGE', 'TEXT'] - Add edit image/analyze keywords to responseModalities detection - Add API key validation in img-gen ensureClient() with clear error
Re-apply the OpenClaw active task routing that was removed in 2b11516. When an active OpenClaw session exists in AionUI, channel messages (Feishu/Telegram/DingTalk) are routed to it so they appear in the same conversation the user is viewing. Also restores response action buttons for Lark and Telegram.
Summary
channel) 发送渠道消息,避免与主 AionUI 会话的 rs_ 404 冲突Changes
src/agent/openclaw/index.ts: 新增sendChannelMessage()方法,预初始化流状态防止 chat event 和 agent event 产生重复响应src/process/task/OpenClawAgentManager.ts: 新增sendChannelMessage()包装方法,带 cronBusyGuard 保护src/channels/agent/ChannelMessageService.ts: 检测 OpenClaw 任务类型并走sendChannelMessage路径;为 OpenClaw 会话持久化用户消息并通过 IPC 推送到渲染进程src/channels/gateway/ActionExecutor.ts: 在会话创建前检测活跃 OpenClaw 任务,确保渠道消息始终路由到原始 OpenClaw 会话Test plan