Skip to content

[Bug]: Feishu plugin: per-chat serial queue prevents messages.queue.mode = "collect" from batching queued messages #54409

@gongkouhorse

Description

@gongkouhorse

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/feishu-openclaw-plugin@2026.3.8 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>();
const enqueue = (chatId: string, task: () => Promise): Promise => {
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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Normal backlog priority with limited blast radius.bugSomething isn't workingbug:behaviorIncorrect behavior without a crashclawsweeper:fix-shape-clearClawSweeper found a clear likely implementation shape for this issue.clawsweeper:queueable-fixClawSweeper marked this issue as an existing queue_fix_pr work candidate.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.impact:session-stateSession, memory, transcript, context, or agent state can drift or corrupt.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions