Skip to content

Comments

fix(channels): sync channel messages to active OpenClaw conversation#881

Open
kuishou68 wants to merge 14 commits intomainfrom
fix/channel-message-sync
Open

fix(channels): sync channel messages to active OpenClaw conversation#881
kuishou68 wants to merge 14 commits intomainfrom
fix/channel-message-sync

Conversation

@kuishou68
Copy link
Collaborator

Summary

  • 当 AionUI 中存在活跃的 OpenClaw 会话时,将渠道(飞书/Telegram/钉钉)消息路由到该会话,而不是创建新的侧边栏条目
  • 使用独立的 gateway session key (channel) 发送渠道消息,避免与主 AionUI 会话的 rs_ 404 冲突
  • Gateway 会将响应广播到所有 WebSocket 客户端,因此主 AionUI 会话自动接收并渲染回复

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

  • 飞书发送消息 → AionUI OpenClaw 会话同步显示用户消息和 AI 回复
  • 消息不会创建新的侧边栏会话条目
  • 无重复响应(chat event 和 agent event 不会产生双重内容)
  • Telegram 发送消息 → 同上(代码路径与飞书一致,因 bot token 冲突跳过测试)
  • 钉钉发送消息 → 同上

…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
Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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() 里通过预置 currentStreamMsgIdhandleChatEvent(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() 返回结构中明确区分 idconversationId
  • 同时在 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. ChannelMessageServicedbResult 读取后未使用(死代码/可读性问题)

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.
@mohamedbouddi7777-dev
Copy link

…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.
@ImL1s
Copy link

ImL1s commented Feb 15, 2026

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.

@ImL1s
Copy link

ImL1s commented Feb 15, 2026

Great PR! This is a useful improvement for channel message routing.

The approach of using a separate gateway session key (channel) 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.

@kuishou68
Copy link
Collaborator Author

Great PR! This is a useful improvement for channel message routing.

The approach of using a separate gateway session key (channel) 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.

@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?

@ImL1s
Copy link

ImL1s commented Feb 16, 2026

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 lastActivity whenever a message is sent/received in a session.

Option 2: Primary/Default Session

Add 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 Chain

Combine both approaches:

  1. If multiple sessions exist, use most recently active
  2. Allow users to manually set a "primary" session override
  3. Document in comments that the first session found is used if no activity tracking exists

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants