fix(agent): resize provider concurrency limiter in place instead of replacing it

The per-provider subagent limiter (providers.ollama-cloud.maxConcurrency)
created a fresh Semaphore whenever the configured limit changed, orphaning
in-flight slots on the old instance so a runtime or mixed limit value could
exceed the cap. getProviderSemaphore now always hands out one shared limiter
(Infinity when unlimited, so every run is still counted) and resizes it in
place. Semaphore.release() decrements before admitting, and the new
Semaphore.resize() raises the ceiling by admitting queued waiters while
lowering it drains in-flight holders without admitting past the new cap.

Refs #3464
This commit is contained in:
can1357
2026-06-25 19:04:50 +02:00
parent ded0976af2
commit ecd6f608e5
4 changed files with 118 additions and 9 deletions
+21 -5
View File
@@ -205,19 +205,35 @@ interface ProviderSemaphoreEntry {
const providerSemaphores = new Map<string, ProviderSemaphoreEntry>();
function getProviderConcurrencyLimit(settings: Settings, provider: string): number {
/**
* Resolve the configured concurrency ceiling for a provider, or `undefined`
* when the provider has no cap concept at all. A configured value `<= 0` means
* "unlimited" and maps to `Infinity` — still a tracked ceiling, so every run
* holds a slot and a later finite resize counts work started while unlimited.
*/
function getProviderConcurrencyLimit(settings: Settings, provider: string): number | undefined {
const settingPath = PROVIDER_MAX_CONCURRENCY_SETTINGS[provider];
if (!settingPath) return 0;
if (!settingPath) return undefined;
const raw = settings.get(settingPath);
const limit = Number.isFinite(raw) ? Math.trunc(raw) : 0;
return limit > 0 ? limit : 0;
return limit > 0 ? limit : Number.POSITIVE_INFINITY;
}
function getProviderSemaphore(settings: Settings, provider: string): Semaphore | undefined {
const limit = getProviderConcurrencyLimit(settings, provider);
if (limit <= 0) return undefined;
if (limit === undefined) return undefined;
// Always hand out (and acquire on) the single shared limiter, even when
// unlimited (Infinity). Resizing it in place — rather than replacing it —
// keeps every in-flight slot counted, so a runtime or mixed limit change can
// never push concurrency past the cap (issue #3464 review feedback).
const existing = providerSemaphores.get(provider);
if (existing?.limit === limit) return existing.semaphore;
if (existing) {
if (existing.limit !== limit) {
existing.limit = limit;
existing.semaphore.resize(limit);
}
return existing.semaphore;
}
const semaphore = new Semaphore(limit);
providerSemaphores.set(provider, { limit, semaphore });
return semaphore;