← Back to PRs

#23188: feat(config): per-channel conversation concurrency override

by El-Fitz open 2026-02-22 03:10 View on GitHub →
agents size: XL
## 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