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
+26 -4
View File
@@ -135,11 +135,33 @@ export class Semaphore {
}
release(): void {
const next = this.#queue.shift();
if (next) {
if (this.#current > 0) this.#current--;
// Admit the next waiter only if we are under the (possibly just-lowered) ceiling.
if (this.#current < this.#max) {
const next = this.#queue.shift();
if (next) {
this.#current++;
next();
}
}
}
/**
* Adjust the maximum concurrency in place. Raising the ceiling immediately
* admits queued waiters that now fit; lowering it lets in-flight holders
* drain naturally (new acquires keep blocking until `#current` falls below
* the new max). Resizing the single shared instance — instead of replacing
* it — keeps in-flight slots counted, so a runtime or mixed limit change can
* never push concurrency past the cap (issue #3464 review feedback).
*/
resize(max: number): void {
const normalizedMax = Number.isFinite(max) ? Math.trunc(max) : 0;
this.#max = normalizedMax > 0 ? normalizedMax : Number.POSITIVE_INFINITY;
while (this.#current < this.#max) {
const next = this.#queue.shift();
if (!next) break;
this.#current++;
next();
} else {
this.#current--;
}
}
}