#23188: feat(config): per-channel conversation concurrency override
agents
size: XL
Cluster:
Signal and Discord Fixes
## Summary
- Problem: All commands for a given provider share a single concurrency lane — two unrelated conversations on the same Discord server contend for the same slots, with no way to limit simultaneous agents within a single conversation.
- Why it matters: A busy conversation can starve quieter ones of their concurrency budget; chatty peers trigger provider rate limits that affect everyone.
- What changed: Introduced a per-conversation lane (`conv:{channel}:{accountId}:{peerId}`) with configurable concurrency, sitting between session-level and global-level in the command queue. Added `maxConcurrentPerConversation` config field with per-channel cascade overrides.
- What did NOT change (scope boundary): No drain delay, no changes to session or global lane behavior, no new external dependencies, no API surface changes.
## Change Type (select all)
- [ ] Bug fix
- [x] Feature
- [ ] Refactor
- [ ] Docs
- [x] Security hardening
- [ ] Chore/infra
## Scope (select all touched areas)
- [x] Gateway / orchestration
- [ ] Skills / tool execution
- [ ] Auth / tokens
- [ ] Memory / storage
- [x] Integrations
- [ ] API / contracts
- [ ] UI / DX
- [ ] CI/CD / infra
## Linked Issue/PR
- Related #23188
## User-visible / Behavior Changes
- New optional config field `maxConcurrentPerConversation` (default: `1`). Controls how many agent runs can execute simultaneously within a single conversation.
- Per-channel overrides follow existing cascade patterns: Discord (channel → guild → provider → global), Telegram (group → provider → global), Slack (channel → provider → global).
- Capped at 10 in both schema validation and runtime enforcement.
## Security Impact (required)
- New permissions/capabilities? `No`
- Secrets/tokens handling changed? `No`
- New/changed network calls? `No`
- Command/tool execution surface changed? `No`
- Data access scope changed? `No`
Runtime cap (`Math.min(10, ...)`) enforced in `resolveAgentMaxConcurrentPerConversation` and per-channel `asPositiveInt` to match schema `.max(10)`, preventing config bypass via programmatic config construction. Idle conversation lanes evicted on completion and on early-return in `setCommandLaneConcurrency` to prevent unbounded memory growth. `CONV_LANE_PREFIX` shared constant prevents magic-string divergence between lane creation and eviction.
## Repro + Verification
### Environment
- OS: macOS / Linux (any Node.js 22+)
- Runtime/container: Node.js 22+
- Model/provider: Any LLM provider
- Integration/channel (if any): Discord, Telegram, Slack, or any flat provider
- Relevant config (redacted): `agents.defaults.maxConcurrentPerConversation: 1`
### Steps
1. Configure `maxConcurrentPerConversation: 1` (default)
2. Send multiple messages from the same peer on the same channel simultaneously
3. Observe that agents execute one-at-a-time per conversation while still respecting the global lane cap
### Expected
- One agent runs at a time per conversation (with default config)
- Different conversations on the same provider run independently up to the global cap
- Idle conversation lanes are evicted after completion
### Actual
- Matches expected behavior
## Evidence
- [x] Failing test/log before + passing after
- [ ] Trace/log snippets
- [ ] Screenshot/recording
- [ ] Perf numbers (if relevant)
Test suite: 880 files, 6967 tests passing. New tests cover:
- Cascade resolver: Discord 4-level, Telegram 3-level, Slack 3-level, flat provider 2-level
- Zod schema validation: accepts valid, rejects invalid, `.max(10)` enforced
- Defaults injection: `loadConfig()` injects `maxConcurrentPerConversation: 1`
- Lane resolver: session, conversation, and global lane key generation
- Command queue: conversation lane eviction on idle, `setCommandLaneConcurrency` idempotency
## Human Verification (required)
- Verified scenarios: Full CI pipeline (`pnpm format:check && pnpm tsgo && pnpm lint && pnpm test:fast`) passes locally on both concurrency and drain-delay branches
- Edge cases checked: Empty channel/peerId returns no lane (passthrough), `stripPeerPrefix` with no prefix returns value unchanged, schema rejects values > 10
- Functional tests: Ran it with a concurrency of 1 in two Discord channel with 7 agents, having them run load and concurrency tests
- What you did **not** verify: Production load testing with actual Discord/Telegram/Slack traffic; multi-process deployment behavior
## Compatibility / Migration
- Backward compatible? `Yes` — new field is optional, defaults to `1` (same effective behavior as before since conversations were not independently limited)
- Config/env changes? `Yes` — new optional `maxConcurrentPerConversation` field in agent defaults and per-channel configs
- Migration needed? `No`
## Failure Recovery (if this breaks)
- How to disable/revert this change quickly: Set `maxConcurrentPerConversation: 1` (default) — effectively a no-op since conversations are serialized by default. Or revert the PR entirely.
- Files/config to restore: No config migration; git revert is clean.
- Known bad symptoms reviewers should watch for: Unexpected queue starvation (agents waiting longer than expected), memory growth in `lanes` Map (mitigated by idle eviction).
## Risks and Mitigations
- Risk: Lane Map memory growth under high cardinality (many unique conversations)
- Mitigation: `evictIdleLane` deletes conv lanes when empty — both after task completion and on early-return in `setCommandLaneConcurrency`. Memory is bounded by concurrent (not cumulative) conversations.
- Risk: Config cascade resolver called per invocation
- Mitigation: Pure synchronous property lookups (< 1µs), no I/O or allocation. Idempotency guard in `setCommandLaneConcurrency` skips `drainLane` when value unchanged.
---
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Most Similar PRs
#18733: feat(infra): add LLM endpoint concurrency limiting (mutex)
by clawmander · 2026-02-17
69.6%
#16736: fix: stagger multi-account channel startup to avoid Discord rate li...
by rm289 · 2026-02-15
68.3%
#20078: feat(session): Add channelGroups config(optional config) for shared...
by demarlik01 · 2026-02-18
67.9%
#18962: feat: add priority preemption — heartbeat lane separation
by rsepulveda23 · 2026-02-17
65.5%
#23226: fix(msteams): proactive messaging, EADDRINUSE fix, tool status, ada...
by TarogStar · 2026-02-22
64.9%
#20394: feat(gateway): make chat history byte limit configurable via gatewa...
by mgratch · 2026-02-18
64.6%
#9266: fix(gateway): configure nested lane concurrency to prevent sessions...
by 100menotu001 · 2026-02-05
64.3%
#22642: fix(discord): Discord status state machine 2.0 (clean restart, foll...
by victorGPT · 2026-02-21
64.3%
#19403: feat(slack): add dm.threadSession option for per-message thread ses...
by Vasiliy-Bondarenko · 2026-02-17
63.8%
#13167: feat(agents): dispatch Claude Code CLI runs as persistent, resumabl...
by gyant · 2026-02-10
63.5%