feat(KB-138): add priority-based scheduling to AgentSemaphore

- Add priority levels (PRIORITY_MERGE=2, PRIORITY_EXECUTE=1, PRIORITY_SPECIFY=0) to AgentSemaphore
- Update acquire() and run() to accept a numeric priority parameter with FIFO ordering within same level
- Wire priority constants into executor (PRIORITY_EXECUTE) and triage (PRIORITY_SPECIFY) callers
- Add comprehensive tests for priority ordering, FIFO within same priority, and dynamic limit interaction
- Include changeset for the priority-based agent scheduling feature
This commit is contained in:
Dustin Byrne
2026-03-27 23:53:40 -04:00
parent d19b51f7fa
commit 65b958581f
7 changed files with 221 additions and 20 deletions

View File

@@ -1,5 +1,5 @@
import { describe, it, expect, vi } from "vitest";
import { AgentSemaphore } from "./concurrency.js";
import { AgentSemaphore, PRIORITY_MERGE, PRIORITY_EXECUTE, PRIORITY_SPECIFY } from "./concurrency.js";
describe("AgentSemaphore", () => {
it("allows immediate acquire when under limit", async () => {
@@ -243,4 +243,141 @@ describe("AgentSemaphore", () => {
expect(ran).toBe(true);
});
// ── Priority scheduling tests ──────────────────────────────────────
it("priority: highest-priority waiter is served first when slot is released", async () => {
const sem = new AgentSemaphore(1);
await sem.acquire(); // fill the single slot
const order: string[] = [];
// Queue three waiters in non-priority order: specify, merge, execute
const pSpecify = sem.acquire(PRIORITY_SPECIFY).then(() => order.push("specify"));
const pMerge = sem.acquire(PRIORITY_MERGE).then(() => order.push("merge"));
const pExecute = sem.acquire(PRIORITY_EXECUTE).then(() => order.push("execute"));
// Release slots one at a time and observe drain order
sem.release();
await pMerge;
expect(order).toEqual(["merge"]);
sem.release();
await pExecute;
expect(order).toEqual(["merge", "execute"]);
sem.release();
await pSpecify;
expect(order).toEqual(["merge", "execute", "specify"]);
sem.release(); // cleanup
expect(sem.activeCount).toBe(0);
});
it("priority: FIFO order is preserved among equal-priority waiters", async () => {
const sem = new AgentSemaphore(1);
await sem.acquire(); // fill the slot
const order: number[] = [];
const p1 = sem.acquire(PRIORITY_EXECUTE).then(() => order.push(1));
const p2 = sem.acquire(PRIORITY_EXECUTE).then(() => order.push(2));
const p3 = sem.acquire(PRIORITY_EXECUTE).then(() => order.push(3));
sem.release();
await p1;
sem.release();
await p2;
sem.release();
await p3;
expect(order).toEqual([1, 2, 3]);
sem.release();
});
it("priority: run() forwards priority to acquire()", async () => {
const sem = new AgentSemaphore(1);
await sem.acquire(); // fill the slot
const order: string[] = [];
const pLow = sem.run(async () => { order.push("low"); }, PRIORITY_SPECIFY);
const pHigh = sem.run(async () => { order.push("high"); }, PRIORITY_MERGE);
// Release — high priority should go first, then its run() releases the
// slot automatically, allowing the low-priority waiter to proceed.
sem.release();
await pHigh;
await pLow;
expect(order).toEqual(["high", "low"]);
expect(sem.activeCount).toBe(0);
});
it("priority: mixed-priority integration — 1 slot, arbitrary enqueue order, correct drain", async () => {
const sem = new AgentSemaphore(1);
await sem.acquire(); // hold the single slot
const order: string[] = [];
// Enqueue in a scrambled order: execute, specify, merge, specify, execute, merge
const promises = [
sem.acquire(PRIORITY_EXECUTE).then(() => { order.push("execute-1"); }),
sem.acquire(PRIORITY_SPECIFY).then(() => { order.push("specify-1"); }),
sem.acquire(PRIORITY_MERGE).then(() => { order.push("merge-1"); }),
sem.acquire(PRIORITY_SPECIFY).then(() => { order.push("specify-2"); }),
sem.acquire(PRIORITY_EXECUTE).then(() => { order.push("execute-2"); }),
sem.acquire(PRIORITY_MERGE).then(() => { order.push("merge-2"); }),
];
// Expected drain order:
// merge-1, merge-2 (highest, FIFO within),
// execute-1, execute-2 (middle, FIFO within),
// specify-1, specify-2 (lowest, FIFO within)
for (let i = 0; i < 6; i++) {
sem.release();
// Wait for the next promise to settle
await Promise.resolve();
await Promise.resolve();
}
await Promise.all(promises);
expect(order).toEqual([
"merge-1", "merge-2",
"execute-1", "execute-2",
"specify-1", "specify-2",
]);
sem.release(); // cleanup
expect(sem.activeCount).toBe(0);
});
it("priority constants have correct values", () => {
expect(PRIORITY_MERGE).toBe(2);
expect(PRIORITY_EXECUTE).toBe(1);
expect(PRIORITY_SPECIFY).toBe(0);
expect(PRIORITY_MERGE).toBeGreaterThan(PRIORITY_EXECUTE);
expect(PRIORITY_EXECUTE).toBeGreaterThan(PRIORITY_SPECIFY);
});
it("priority: default priority (no argument) behaves as PRIORITY_SPECIFY (0)", async () => {
const sem = new AgentSemaphore(1);
await sem.acquire(); // fill the slot
const order: string[] = [];
// acquire() with no priority arg — should be treated as 0
const pDefault = sem.acquire().then(() => order.push("default"));
const pMerge = sem.acquire(PRIORITY_MERGE).then(() => order.push("merge"));
sem.release();
await pMerge;
expect(order).toEqual(["merge"]);
sem.release();
await pDefault;
expect(order).toEqual(["merge", "default"]);
sem.release();
});
});