openclaw - ✅(Solved) Fix Different Discord channel sessions still serialize through shared embedded global lane [1 pull requests, 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#55566Fetched 2026-04-08 01:37:55
View on GitHub
Comments
0
Participants
1
Timeline
1
Reactions
0
Author
Participants
Timeline (top)
cross-referenced ×1

In a multi-channel Discord setup, different channel-bound agents/sessions can still appear to queue behind each other even when session routing is already isolated correctly.

After local reproduction and a minimal runtime patch, the bottleneck appears to be the embedded runner's global lane fallback, not session.dmScope, not followup queue mode, and not memory-lancedb-pro.

Related:

  • #54162
  • #52655

Error Message

Two different Discord channels were used for concurrent testing.

Root Cause

For users running multiple independent Discord agents/channels, the current fallback can make unrelated sessions feel serialized even when routing is already correct.

This creates the impression that:

  • sessions are "stuck"
  • OpenClaw is effectively single-threaded across channels
  • multi-agent/channel deployments do not scale as expected

Fix Action

Fix / Workaround

After local reproduction and a minimal runtime patch, the bottleneck appears to be the embedded runner's global lane fallback, not session.dmScope, not followup queue mode, and not memory-lancedb-pro.

Observed before patch:

  • channel A starts a long reply
  • channel B sends a short request shortly after
  • channel B often waits until A mostly/fully finishes before replying

Minimal local patch that improved the problem

PR fix notes

PR #52655: feat: per-agent queue lanes (fixes #16055)

Description (problem / solution / changelog)

Problem

All inbound messages from all agents are serialized through the shared main queue lane, capped at agents.defaults.maxConcurrent (default 4). For multi-agent setups with 5+ independent bots, this creates a bottleneck where agents queue behind each other despite being completely independent (separate bot tokens, workspaces, and sessions).

Reported in #16055 — setting maxConcurrent: 100 does not help because the value wasn't propagating to the main lane correctly for per-agent isolation.

Solution

Add lane and laneConcurrency config fields to agents.list[] entries. When configured, an agent's inbound runs use a dedicated global queue lane instead of "main", enabling true parallel processing across agents.

Config example

{
  agents: {
    list: [
      { id: "agent-one", lane: "lane-one", laneConcurrency: 10 },
      { id: "agent-two", lane: "lane-two", laneConcurrency: 10 },
    ],
  },
}

Agents without a custom lane continue to use the shared main lane (backwards compatible).

Changes

  • src/config/zod-schema.agent-runtime.ts — Add lane (string) and laneConcurrency (positive int) to AgentEntrySchema
  • src/config/agent-limits.ts — Add resolveAgentLane() and resolveAgentLaneConcurrency() helpers
  • src/auto-reply/reply/agent-runner-execution.ts — Pass per-agent lane to runEmbeddedPiAgent in the auto-reply dispatch path
  • src/gateway/server-lanes.ts — Apply per-agent lane concurrency on gateway startup
  • src/gateway/server-reload-handlers.ts — Apply per-agent lane concurrency on hot-reload (SIGUSR1 / config change)
  • src/config/schema.labels.ts + schema.help.ts — Config labels and help text
  • src/config/agent-limits.test.ts — Unit tests for the new resolvers

How it works

The existing queue in command-queue.ts already supports arbitrary named lanes with independent concurrency caps via setCommandLaneConcurrency(). This PR simply:

  1. Lets users name a lane per agent in config
  2. Resolves that lane when dispatching inbound runs
  3. Applies the concurrency cap at startup and on config reload

When laneConcurrency is omitted, the custom lane inherits agents.defaults.maxConcurrent.

Backwards compatible

  • Agents without lane use the shared main lane as before
  • No changes to session-level serialization (still one run per session)
  • No changes to subagent/cron lanes

Fixes #16055

Changed files

  • docs/.generated/config-baseline.json (added, +65508/-0)
  • docs/.generated/config-baseline.jsonl (added, +5620/-0)
  • src/auto-reply/reply/agent-runner-execution.ts (modified, +5/-0)
  • src/auto-reply/reply/agent-runner-memory.ts (modified, +2/-0)
  • src/auto-reply/reply/followup-runner.ts (modified, +2/-0)
  • src/config/agent-limits.test.ts (added, +85/-0)
  • src/config/agent-limits.ts (modified, +31/-0)
  • src/config/schema.base.generated.ts (modified, +18/-0)
  • src/config/schema.help.ts (modified, +4/-0)
  • src/config/schema.labels.ts (modified, +2/-0)
  • src/config/types.agents.ts (modified, +4/-0)
  • src/config/zod-schema.agent-runtime.ts (modified, +4/-0)
  • src/gateway/server-lanes.ts (modified, +20/-1)
  • src/gateway/server-reload-handlers.ts (modified, +19/-1)

Code Example

const sessionLane = resolveSessionLane(params.sessionKey?.trim() || params.sessionId);
const globalLane = resolveGlobalLane(params.lane);
...
return enqueueSession(() =>
  enqueueGlobal(async () => {
    ...
  }),
);

---

export function resolveGlobalLane(lane?: string) {
  const cleaned = lane?.trim();
  if (cleaned === CommandLane.Cron) {
    return CommandLane.Nested;
  }
  return cleaned ? cleaned : CommandLane.Main;
}

---

const sessionLane = resolveSessionLane(params.sessionKey?.trim() || params.sessionId);
const resolvedLaneKey = params.sessionKey?.trim() || params.sessionId;
const globalLane =
  params.lane?.trim() ||
  (resolvedLaneKey ? `agent-run:${resolvedLaneKey}` : resolveGlobalLane());
RAW_BUFFERClick to expand / collapse

Summary

In a multi-channel Discord setup, different channel-bound agents/sessions can still appear to queue behind each other even when session routing is already isolated correctly.

After local reproduction and a minimal runtime patch, the bottleneck appears to be the embedded runner's global lane fallback, not session.dmScope, not followup queue mode, and not memory-lancedb-pro.

Related:

  • #54162
  • #52655

Environment

  • OpenClaw: 2026.3.23-2
  • Platform: Windows 10 x64
  • Channels: Discord
  • Setup: multiple Discord channels bound to different agents/workspaces via bindings

Observed behavior

Two different Discord channels were used for concurrent testing.

Expected:

  • different channels -> different sessions
  • different sessions should be able to run concurrently

Observed before patch:

  • channel A starts a long reply
  • channel B sends a short request shortly after
  • channel B often waits until A mostly/fully finishes before replying

This felt like session-level queueing, but the channels were already isolated.


What was ruled out

1. Session routing / dmScope

This did not appear to be the primary cause for the Discord channel case.

Why:

  • docs state group/channel chats get their own session keys
  • config already used channel bindings with separate agent ids/workspaces
  • the issue reproduced across different Discord channels, not only DMs

2. messages.queue mode

Changing queue behavior (collect / steer) changes followup behavior, but it does not provide true parallelism across already-isolated sessions.

3. memory-lancedb-pro

This was the initial suspicion, but the final successful test strongly suggests memory is not the main bottleneck here.


Relevant code path

The likely bottleneck is in:

src/agents/pi-embedded-runner/run.ts

Current structure:

const sessionLane = resolveSessionLane(params.sessionKey?.trim() || params.sessionId);
const globalLane = resolveGlobalLane(params.lane);
...
return enqueueSession(() =>
  enqueueGlobal(async () => {
    ...
  }),
);

And in:

src/agents/pi-embedded-runner/lanes.ts

export function resolveGlobalLane(lane?: string) {
  const cleaned = lane?.trim();
  if (cleaned === CommandLane.Cron) {
    return CommandLane.Nested;
  }
  return cleaned ? cleaned : CommandLane.Main;
}

This means that when no explicit lane is passed, many embedded runs from otherwise separate sessions still funnel through the same global lane (main).

That behavior matches the observed cross-channel serialization.


Minimal local patch that improved the problem

A minimal runtime patch was tested locally in the embedded runner so that the fallback global lane becomes session-scoped instead of always defaulting to main.

Patched logic

const sessionLane = resolveSessionLane(params.sessionKey?.trim() || params.sessionId);
const resolvedLaneKey = params.sessionKey?.trim() || params.sessionId;
const globalLane =
  params.lane?.trim() ||
  (resolvedLaneKey ? `agent-run:${resolvedLaneKey}` : resolveGlobalLane());

This preserves:

  • session-level serialization for the same session
  • explicit custom lane override when provided

But avoids funneling unrelated embedded runs into the same shared fallback lane.


Local test result

After applying the above minimal patch locally and restarting OpenClaw:

  • one Discord channel was given a long, intentionally slow request
  • another Discord channel was given a short request shortly after
  • the short request returned promptly instead of obviously waiting behind the long one

Initial validation result: successful

This is still a local/manual validation, but it strongly suggests the bottleneck is the embedded global lane fallback.


Why this matters

For users running multiple independent Discord agents/channels, the current fallback can make unrelated sessions feel serialized even when routing is already correct.

This creates the impression that:

  • sessions are "stuck"
  • OpenClaw is effectively single-threaded across channels
  • multi-agent/channel deployments do not scale as expected

Suggested direction

Consider changing the embedded runner fallback so that unrelated embedded runs do not all share CommandLane.Main unless explicitly desired.

Possible approaches:

  1. Session-scoped fallback global lane (minimal change)
    • e.g. agent-run:<sessionKey>
  2. Agent-scoped fallback global lane
    • would align with #52655 direction, but may be coarser than necessary
  3. Keep current default, but only when a run is truly intended to participate in the shared main lane

Relationship to existing reports

  • #52655 adds per-agent queue lanes, which is very relevant and points in the same general direction.
  • #54162 discusses queue/lane behavior under load. This issue is narrower: cross-channel embedded-run serialization caused by the fallback global lane in runEmbeddedPiAgent.

Suggested next step

If helpful, I can open a follow-up PR with the minimal patch described above and include a regression test covering:

  • two different isolated sessions
  • same process
  • no explicit custom lane
  • expectation: they should not serialize solely due to fallback global lane selection

extent analysis

Fix Plan

To address the issue of cross-channel embedded-run serialization caused by the fallback global lane, we will implement a session-scoped fallback global lane. This involves modifying the resolveGlobalLane function to use a session-scoped lane when no explicit lane is provided.

Step-by-Step Solution:

  1. Modify the resolveGlobalLane function:

    • Update the lanes.ts file to include a new function that generates a session-scoped lane.
    • Use this new function as the fallback when no explicit lane is provided.
  2. Update the run.ts file:

    • Modify the enqueueSession and enqueueGlobal functions to use the updated resolveGlobalLane function.

Example Code:

// lanes.ts
export function resolveSessionScopedLane(sessionKey: string): string {
  return `agent-run:${sessionKey}`;
}

export function resolveGlobalLane(lane?: string, sessionKey?: string): string {
  const cleaned = lane?.trim();
  if (cleaned === CommandLane.Cron) {
    return CommandLane.Nested;
  }
  return cleaned ? cleaned : (sessionKey ? resolveSessionScopedLane(sessionKey) : CommandLane.Main);
}
// run.ts
const sessionLane = resolveSessionLane(params.sessionKey?.trim() || params.sessionId);
const globalLane = resolveGlobalLane(params.lane, params.sessionKey?.trim() || params.sessionId);
...
return enqueueSession(() =>
  enqueueGlobal(async () => {
    ...
  }),
);

Verification

To verify that the fix worked, you can run the following tests:

  • Create two different isolated sessions with no explicit custom lane.
  • Send a long request to one session and a short request to the other session shortly after.
  • Verify that the short request returns promptly instead of waiting behind the long request.

Extra Tips

  • Make sure to include regression tests to cover different scenarios, such as same process, different processes, and explicit custom lanes.
  • Consider adding logging to monitor the lane usage and identify any potential issues.
  • Review the code changes carefully to ensure that they do not introduce any new bugs or performance issues.

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…

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 - ✅(Solved) Fix Different Discord channel sessions still serialize through shared embedded global lane [1 pull requests, 1 participants]