openclaw - 💡(How to fix) Fix [Bug]: Feishu plugin: per-chat serial queue prevents messages.queue.mode = "collect" from batching queued messages [1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#54409Fetched 2026-04-08 01:27:59
View on GitHub
Comments
0
Participants
1
Timeline
4
Reactions
0
Participants
Timeline (top)
labeled ×2referenced ×2

Feishu plugin's per-chat serial queue (createChatQueue in extensions/feishu/src/monitor.account.ts) blocks queued messages from reaching the gateway's followup queue, making messages.queue.mode = "collect" ineffective — each message is processed as a separate agent turn instead of being batched.

Error Message

void task().catch((err) => error(bypassed dispatch failed: ${err}));

Root Cause

Root cause: createChatQueue() in extensions/feishu/src/monitor.account.ts wraps each message in a task chained via prev.then(task, task). Each task awaits the full dispatchReplyFromConfig → agent run cycle. When task A completes, isActive becomes false before task B starts, so the gateway treats B as a new request rather than a followup.

Fix Action

Fix / Workaround

Root cause: createChatQueue() in extensions/feishu/src/monitor.account.ts wraps each message in a task chained via prev.then(task, task). Each task awaits the full dispatchReplyFromConfig → agent run cycle. When task A completes, isActive becomes false before task B starts, so the gateway treats B as a new request rather than a followup.

The dispatch function wraps each message as a serial task:

const dispatchFeishuMessage = async (event: FeishuMessageEvent) => { const chatId = event.message.chat_id?.trim() || "unknown"; const task = () => handleFeishuMessage({ cfg, event, ... }); await enqueue(chatId, task); // <-- awaits full agent run before next message can start };

Workaround: bypass the serial queue when a task is already active for the same chat, so the message reaches the gateway while isActive is still true:

Code Example

# Relevant source: extensions/feishu/src/monitor.account.ts

# The per-chat serial queue:
function createChatQueue() {
  const queues = new Map<string, Promise<void>>();
  return (chatId: string, task: () => Promise<void>): Promise<void> => {
    const prev = queues.get(chatId) ?? Promise.resolve();
    const next = prev.then(task, task);  // <-- blocks until previous task completes
    queues.set(chatId, next);
    void next.finally(() => {
      if (queues.get(chatId) === next) queues.delete(chatId);
    });
    return next;
  };
}

# The dispatch function wraps each message as a serial task:
const dispatchFeishuMessage = async (event: FeishuMessageEvent) => {
  const chatId = event.message.chat_id?.trim() || "unknown";
  const task = () => handleFeishuMessage({ cfg, event, ... });
  await enqueue(chatId, task);  // <-- awaits full agent run before next message can start
};

# Gateway's decision function (pi-embedded-DgYXShcG.js):
function resolveActiveRunQueueAction(params) {
  if (!params.isActive) return "run-now";       // <-- always hit because serial queue waits
  if (params.isHeartbeat) return "drop";
  if (params.shouldFollowup || params.queueMode === "steer") return "enqueue-followup";
  return "run-now";
}

# Timeline showing why collect never activates:
#   msg A: task starts -> isActive=true -> agent runs -> task ends -> isActive=false
#   msg B: [blocked in Promise chain] ──────────────────────────> task starts (isActive=false!) -> "run-now"
#   msg C: [blocked in Promise chain] ──────────────────────────────────────────────────────> same pattern

# The official @larksuiteoapi/feishu-openclaw-plugin@2026.3.8 has the same architecture
# (chat-queue.js with enqueueFeishuChatTask using identical Promise chaining).
RAW_BUFFERClick to expand / collapse

Bug type

Behavior bug (incorrect output/state without crash)

Summary

Feishu plugin's per-chat serial queue (createChatQueue in extensions/feishu/src/monitor.account.ts) blocks queued messages from reaching the gateway's followup queue, making messages.queue.mode = "collect" ineffective — each message is processed as a separate agent turn instead of being batched.

Steps to reproduce

  1. Set messages.queue.mode = "collect" and messages.queue.debounceMs = 2000 in openclaw.json.
  2. Send a message to the Feishu bot that triggers a long agent run (e.g. ask it to run sleep 30).
  3. While the agent is processing, send 3 additional messages (spacing does not matter).
  4. Observe: after message 1 completes, messages 2, 3, and 4 are processed individually as separate agent turns, not batched.

Expected behavior

Messages 2, 3, and 4 should be batched into a single followup turn via the collect queue, as documented for messages.queue.mode = "collect". This is the behavior observed on Discord and other channels that do not implement a per-chat serial queue.

Actual behavior

Each message is processed as an independent agent turn in sequence. Gateway logs show no "enqueue-followup" action for messages 2-4 — they all enter resolveActiveRunQueueAction with isActive=false and receive "run-now".

Root cause: createChatQueue() in extensions/feishu/src/monitor.account.ts wraps each message in a task chained via prev.then(task, task). Each task awaits the full dispatchReplyFromConfig → agent run cycle. When task A completes, isActive becomes false before task B starts, so the gateway treats B as a new request rather than a followup.

OpenClaw version

2026.3.23

Operating system

Ubuntu 24.04 (WSL2)

Install method

npm global

Model

google-vertex/gemini-3.1-pro-preview

Provider / routing chain

openclaw -> google-vertex (direct)

Additional provider/model setup details

NOT_ENOUGH_INFO

Logs, screenshots, and evidence

# Relevant source: extensions/feishu/src/monitor.account.ts

# The per-chat serial queue:
function createChatQueue() {
  const queues = new Map<string, Promise<void>>();
  return (chatId: string, task: () => Promise<void>): Promise<void> => {
    const prev = queues.get(chatId) ?? Promise.resolve();
    const next = prev.then(task, task);  // <-- blocks until previous task completes
    queues.set(chatId, next);
    void next.finally(() => {
      if (queues.get(chatId) === next) queues.delete(chatId);
    });
    return next;
  };
}

# The dispatch function wraps each message as a serial task:
const dispatchFeishuMessage = async (event: FeishuMessageEvent) => {
  const chatId = event.message.chat_id?.trim() || "unknown";
  const task = () => handleFeishuMessage({ cfg, event, ... });
  await enqueue(chatId, task);  // <-- awaits full agent run before next message can start
};

# Gateway's decision function (pi-embedded-DgYXShcG.js):
function resolveActiveRunQueueAction(params) {
  if (!params.isActive) return "run-now";       // <-- always hit because serial queue waits
  if (params.isHeartbeat) return "drop";
  if (params.shouldFollowup || params.queueMode === "steer") return "enqueue-followup";
  return "run-now";
}

# Timeline showing why collect never activates:
#   msg A: task starts -> isActive=true -> agent runs -> task ends -> isActive=false
#   msg B: [blocked in Promise chain] ──────────────────────────> task starts (isActive=false!) -> "run-now"
#   msg C: [blocked in Promise chain] ──────────────────────────────────────────────────────> same pattern

# The official @larksuiteoapi/[email protected] has the same architecture
# (chat-queue.js with enqueueFeishuChatTask using identical Promise chaining).

Impact and severity

Affected: All Feishu channel users who configure messages.queue.mode = "collect" Severity: Medium (feature silently ineffective, no crash or data loss) Frequency: Always (100% reproducible) Consequence: Users who send multiple messages while agent is busy get separate replies for each, consuming extra API tokens and creating fragmented conversation context. The documented collect feature appears broken for Feishu.

Additional information

Workaround: bypass the serial queue when a task is already active for the same chat, so the message reaches the gateway while isActive is still true:

function createChatQueue() { const queues = new Map<string, Promise<void>>(); const enqueue = (chatId: string, task: () => Promise<void>): Promise<void> => { const prev = queues.get(chatId) ?? Promise.resolve(); const next = prev.then(task, task); queues.set(chatId, next); void next.finally(() => { if (queues.get(chatId) === next) queues.delete(chatId); }); return next; }; enqueue.has = (chatId: string): boolean => queues.has(chatId); return enqueue; }

// In dispatchFeishuMessage: if (enqueue.has(chatId)) { void task().catch((err) => error(bypassed dispatch failed: ${err})); return; } await enqueue(chatId, task);

This preserves ordering for the first message while letting subsequent messages reach the gateway's followup queue. Verified working on 2026.3.2.

A potentially cleaner upstream fix might be at the gateway level — e.g. having dispatchReplyFromConfig detect an active session and enqueue the followup internally, rather than requiring each channel plugin to bypass its own serial queue.

Note: the Feishu plugin correctly sets OriginatingTo (bot.ts line 1301), so resolveCrossChannelKey is not a factor in this issue.

extent analysis

Fix Plan

To resolve the issue with the Feishu plugin's per-chat serial queue blocking queued messages from reaching the gateway's followup queue, we need to modify the createChatQueue function and the dispatchFeishuMessage function.

Here are the steps:

  • Modify the createChatQueue function to add a has method that checks if a chat ID is already in the queue.
  • Modify the dispatchFeishuMessage function to check if a task is already active for the same chat ID. If it is, bypass the serial queue and let the message reach the gateway while isActive is still true.

Code Changes

// Modified createChatQueue function
function createChatQueue() {
  const queues = new Map<string, Promise<void>>();
  const enqueue = (chatId: string, task: () => Promise<void>): Promise<void> => {
    const prev = queues.get(chatId) ?? Promise.resolve();
    const next = prev.then(task, task);
    queues.set(chatId, next);
    void next.finally(() => { if (queues.get(chatId) === next) queues.delete(chatId); });
    return next;
  };
  enqueue.has = (chatId: string): boolean => queues.has(chatId);
  return enqueue;
}

// Modified dispatchFeishuMessage function
const dispatchFeishuMessage = async (event: FeishuMessageEvent) => {
  const chatId = event.message.chat_id?.trim() || "unknown";
  const task = () => handleFeishuMessage({ cfg, event, /* ... */ });
  const enqueue = createChatQueue();
  if (enqueue.has(chatId)) {
    void task().catch((err) => error(`bypassed dispatch failed: ${err}`));
    return;
  }
  await enqueue(chatId, task);
};

Verification

To verify that the fix worked, follow these steps:

  • Set messages.queue.mode to "collect" and messages.queue.debounceMs to a suitable value (e.g., 2000) in openclaw.json.
  • Send a message to the Feishu bot that triggers a long agent run.
  • While the agent is processing, send multiple additional messages.
  • Observe that the subsequent messages are now batched into a single followup turn via the collect queue.

Extra Tips

  • This fix preserves the ordering of messages for the first message while letting subsequent messages reach the gateway's followup queue.
  • A potentially cleaner upstream fix might be at the gateway level, having dispatchReplyFromConfig detect an active session and enqueue the followup internally.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

Messages 2, 3, and 4 should be batched into a single followup turn via the collect queue, as documented for messages.queue.mode = "collect". This is the behavior observed on Discord and other channels that do not implement a per-chat serial queue.

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - 💡(How to fix) Fix [Bug]: Feishu plugin: per-chat serial queue prevents messages.queue.mode = "collect" from batching queued messages [1 participants]