fix(agent): released ollama-cloud semaphore slot when waiter aborts

Semaphore.acquire now accepts an AbortSignal so a queued waiter that is cancelled (parent task abort, wall-clock budget elapsing) removes itself from the wait queue instead of being resolved by the next release. The provider semaphore in runSubprocess passes the run's abortSignal through, preventing aborted ollama-cloud subagents from permanently draining the provider concurrency budget.

Fixes #3464
This commit is contained in:
roboomp
2026-06-25 12:14:45 +00:00
parent 80862b79da
commit 7e90e4d081
3 changed files with 59 additions and 5 deletions
@@ -17,6 +17,7 @@ import {
import { AuthStorage } from "@oh-my-pi/pi-coding-agent/session/auth-storage";
import { SessionManager } from "@oh-my-pi/pi-coding-agent/session/session-manager";
import { runSubprocess } from "@oh-my-pi/pi-coding-agent/task/executor";
import { Semaphore } from "@oh-my-pi/pi-coding-agent/task/parallel";
import type { AgentDefinition } from "@oh-my-pi/pi-coding-agent/task/types";
import { EventBus } from "@oh-my-pi/pi-coding-agent/utils/event-bus";
import { TempDir } from "@oh-my-pi/pi-utils";
@@ -217,4 +218,28 @@ describe("issue #3464: ollama-cloud task backoff", () => {
gates.get("CloudTwo")?.resolve();
await second;
});
it("frees a queued slot when its acquire waiter is aborted", async () => {
const semaphore = new Semaphore(1);
await semaphore.acquire();
const controller = new AbortController();
const aborted = semaphore.acquire(controller.signal);
controller.abort();
await aborted.then(
() => {
throw new Error("Aborted semaphore.acquire should reject");
},
() => {},
);
const nextStarted = deferred();
const next = (async () => {
await semaphore.acquire();
nextStarted.resolve();
})();
semaphore.release();
await nextStarted.promise;
semaphore.release();
await next;
});
});