summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-28 15:47:31 +0900
committerAdam Malczewski <[email protected]>2026-06-28 15:47:31 +0900
commit213fe2ab2074b4536ae326d0007dc5e45507407c (patch)
tree7423f7e4aab0fdf20f8416d385b563478f234132
parentb60586285863f8bb82242a9df49c4d47e1235755 (diff)
downloaddispatch-213fe2ab2074b4536ae326d0007dc5e45507407c.tar.gz
dispatch-213fe2ab2074b4536ae326d0007dc5e45507407c.zip
fix(predev): resolve merge conflicts between concurrency-fixes and workspace-star
-rw-r--r--packages/provider-concurrency/src/concurrency-manager.test.ts62
-rw-r--r--packages/provider-concurrency/src/concurrency-manager.ts34
-rw-r--r--packages/provider-concurrency/tsconfig.json2
3 files changed, 49 insertions, 49 deletions
diff --git a/packages/provider-concurrency/src/concurrency-manager.test.ts b/packages/provider-concurrency/src/concurrency-manager.test.ts
index 357a5d1..36c0ea3 100644
--- a/packages/provider-concurrency/src/concurrency-manager.test.ts
+++ b/packages/provider-concurrency/src/concurrency-manager.test.ts
@@ -504,7 +504,7 @@ describe("createConcurrencyManager", () => {
const { manager, timers } = createManager({ releaseCooldownMs: 200 });
manager.setLimit("umans", 1);
- const release1 = await manager.acquire("umans", "conv1", 0);
+ const release1 = await manager.acquire("umans", "conv1", "default", 0);
expect(manager.getStatus("umans")?.cooldownMs).toBe(200);
// Bump the cooldown to 500ms.
@@ -513,7 +513,7 @@ describe("createConcurrencyManager", () => {
// Queue a waiter.
let resolved = false;
- const promise2 = manager.acquire("umans", "conv2", 100).then((r) => {
+ const promise2 = manager.acquire("umans", "conv2", "default", 100).then((r) => {
resolved = true;
return r;
});
@@ -549,7 +549,7 @@ describe("createConcurrencyManager", () => {
manager.setCooldown("umans", 500);
expect(manager.getCooldown("umans")).toBe(500);
// No limit → acquire must be unlimited (no state with a limit imposed).
- const release = await manager.acquire("umans", "conv1", 0);
+ const release = await manager.acquire("umans", "conv1", "default", 0);
expect(typeof release).toBe("function");
release();
expect(manager.getStatus("umans")).toBeUndefined(); // no limit state created
@@ -634,16 +634,16 @@ describe("createConcurrencyManager", () => {
// Fill all 4 slots (fast-path, no gate).
const releases = await Promise.all([
- manager.acquire("umans", "c1", 0),
- manager.acquire("umans", "c2", 0),
- manager.acquire("umans", "c3", 0),
- manager.acquire("umans", "c4", 0),
+ manager.acquire("umans", "c1", "default", 0),
+ manager.acquire("umans", "c2", "default", 0),
+ manager.acquire("umans", "c3", "default", 0),
+ manager.acquire("umans", "c4", "default", 0),
]);
expect(manager.getStatus("umans")?.inFlight).toBe(4);
// 5th agent queues.
let resolved = false;
- const promise5 = manager.acquire("umans", "c5", 10).then((r) => {
+ const promise5 = manager.acquire("umans", "c5", "default", 10).then((r) => {
resolved = true;
return r;
});
@@ -680,14 +680,14 @@ describe("createConcurrencyManager", () => {
manager.setCooldown("umans", 0);
const releases = await Promise.all([
- manager.acquire("umans", "c1", 0),
- manager.acquire("umans", "c2", 0),
- manager.acquire("umans", "c3", 0),
- manager.acquire("umans", "c4", 0),
+ manager.acquire("umans", "c1", "default", 0),
+ manager.acquire("umans", "c2", "default", 0),
+ manager.acquire("umans", "c3", "default", 0),
+ manager.acquire("umans", "c4", "default", 0),
]);
let resolved = false;
- const promise5 = manager.acquire("umans", "c5", 10).then((r) => {
+ const promise5 = manager.acquire("umans", "c5", "default", 10).then((r) => {
resolved = true;
return r;
});
@@ -714,9 +714,9 @@ describe("createConcurrencyManager", () => {
manager.setLimit("umans", 1);
manager.setCooldown("umans", 0);
- const release1 = await manager.acquire("umans", "c1", 0);
+ const release1 = await manager.acquire("umans", "c1", "default", 0);
let resolved = false;
- const promise2 = manager.acquire("umans", "c2", 10).then((r) => {
+ const promise2 = manager.acquire("umans", "c2", "default", 10).then((r) => {
resolved = true;
return r;
});
@@ -740,20 +740,20 @@ describe("createConcurrencyManager", () => {
manager.setCooldown("umans", 0);
const releases = await Promise.all([
- manager.acquire("umans", "c1", 0),
- manager.acquire("umans", "c2", 0),
- manager.acquire("umans", "c3", 0),
- manager.acquire("umans", "c4", 0),
+ manager.acquire("umans", "c1", "default", 0),
+ manager.acquire("umans", "c2", "default", 0),
+ manager.acquire("umans", "c3", "default", 0),
+ manager.acquire("umans", "c4", "default", 0),
]);
// Queue two waiters.
let r5 = false;
let r6 = false;
- const p5 = manager.acquire("umans", "c5", 10).then((r) => {
+ const p5 = manager.acquire("umans", "c5", "default", 10).then((r) => {
r5 = true;
return r;
});
- const p6 = manager.acquire("umans", "c6", 20).then((r) => {
+ const p6 = manager.acquire("umans", "c6", "default", 20).then((r) => {
r6 = true;
return r;
});
@@ -784,9 +784,9 @@ describe("createConcurrencyManager", () => {
manager.setLimit("umans", 1);
manager.setCooldown("umans", 0);
- return manager.acquire("umans", "c1", 0).then(async (release1) => {
+ return manager.acquire("umans", "c1", "default", 0).then(async (release1) => {
// Queue a waiter (arms the 1s fallback timer).
- const p2 = manager.acquire("umans", "c2", 10);
+ const p2 = manager.acquire("umans", "c2", "default", 10);
await Promise.resolve();
await Promise.resolve();
@@ -818,12 +818,12 @@ describe("createConcurrencyManager", () => {
manager.setCooldown("umans", 0);
// Hold the single slot.
- const release1 = await manager.acquire("umans", "c1", 0);
+ const release1 = await manager.acquire("umans", "c1", "default", 0);
expect(manager.getStatus("umans")?.inFlight).toBe(1);
// Queue a waiter (c2). Cooldown is 0, but the gate defers admission until a poll.
let c2Granted = false;
- const p2 = manager.acquire("umans", "c2", 10).then((r) => {
+ const p2 = manager.acquire("umans", "c2", "default", 10).then((r) => {
c2Granted = true;
return r;
});
@@ -842,7 +842,7 @@ describe("createConcurrencyManager", () => {
// not fast-path. Even if it saw inFlight < limit, gatePolling would route it
// through the queue. Either way it must NOT be granted yet.
let c3Granted = false;
- const p3 = manager.acquire("umans", "c3", 20).then((r) => {
+ const p3 = manager.acquire("umans", "c3", "default", 20).then((r) => {
c3Granted = true;
return r;
});
@@ -870,7 +870,7 @@ describe("createConcurrencyManager", () => {
manager.setLimit("umans", 4);
// Nowhere near the limit, no recycle in progress → fast-path, no poll.
- const release = await manager.acquire("umans", "c1", 0);
+ const release = await manager.acquire("umans", "c1", "default", 0);
expect(manager.getStatus("umans")?.inFlight).toBe(1);
release();
});
@@ -890,9 +890,9 @@ describe("createConcurrencyManager", () => {
manager.setLimit("umans", 1);
manager.setCooldown("umans", 0);
- const release1 = await manager.acquire("umans", "c1", 0);
+ const release1 = await manager.acquire("umans", "c1", "default", 0);
// Queue a waiter; release → recycle → poll THROWS.
- const p2 = manager.acquire("umans", "c2", 10);
+ const p2 = manager.acquire("umans", "c2", "default", 10);
await Promise.resolve();
await Promise.resolve();
@@ -918,8 +918,8 @@ describe("createConcurrencyManager", () => {
manager.setLimit("umans", 1);
manager.setCooldown("umans", 0);
- const release1 = await manager.acquire("umans", "c1", 0);
- manager.acquire("umans", "c2", 10).then((r) => r()); // queue + auto-release
+ const release1 = await manager.acquire("umans", "c1", "default", 0);
+ manager.acquire("umans", "c2", "default", 10).then((r) => r()); // queue + auto-release
await Promise.resolve();
await Promise.resolve();
release1();
diff --git a/packages/provider-concurrency/src/concurrency-manager.ts b/packages/provider-concurrency/src/concurrency-manager.ts
index ea66a49..986493d 100644
--- a/packages/provider-concurrency/src/concurrency-manager.ts
+++ b/packages/provider-concurrency/src/concurrency-manager.ts
@@ -376,12 +376,6 @@ export function createConcurrencyManager(opts: ConcurrencyManagerOpts): Concurre
}
/**
- * Grant queued waiters WITHOUT the usage gate (the fast path used when no
- * `fetchUsage` is configured, or as the cooldown-only fallback when a poll
- * returns no usage info). Grants while there is internal room
- * (`inFlight < limit`). Synchronous.
- */
- function grantLoop(state: ProviderState, providerId: string): void {
* Priority comparator for queued waiters: starred-workspace agents first,
* then oldest-agent-first (ascending `promptStartedAt`) within each group.
* Called at sort time (both on insert and before granting) so a workspace
@@ -394,10 +388,15 @@ export function createConcurrencyManager(opts: ConcurrencyManagerOpts): Concurre
return a.promptStartedAt - b.promptStartedAt; // oldest first within group
}
- function tryGrantNext(providerId: string): void {
- const state = states.get(providerId);
- if (state === undefined) return;
- if (state.paused) return;
+ /**
+ * Grant queued waiters WITHOUT the usage gate (the fast path used when no
+ * `fetchUsage` is configured, or as the cooldown-only fallback when a poll
+ * returns no usage info). Grants while there is internal room
+ * (`inFlight < limit`). Synchronous. Re-sorts with {@link compareWaiters}
+ * (starred-workspace-first, then oldest-agent-first) before granting so a
+ * workspace starred AFTER an agent queued is re-evaluated.
+ */
+ function grantLoop(state: ProviderState, providerId: string): void {
// Re-sort before granting: a workspace may have been starred/unstarred
// since the waiters were enqueued, so priority may have changed.
state.queue.sort(compareWaiters);
@@ -425,6 +424,10 @@ export function createConcurrencyManager(opts: ConcurrencyManagerOpts): Concurre
function grantOne(state: ProviderState, providerId: string): void {
if (state.queue.length === 0) return;
if (state.inFlight >= state.limit) return;
+ // Re-sort before picking the front: starred-workspace agents must be
+ // admitted first, even on the usage-gated path (a workspace may have been
+ // starred since the waiters were enqueued).
+ state.queue.sort(compareWaiters);
const waiter = state.queue[0];
if (waiter === undefined) return;
state.queue.shift();
@@ -621,19 +624,16 @@ export function createConcurrencyManager(opts: ConcurrencyManagerOpts): Concurre
// Queue (starred-workspace-first, then oldest-agent-first).
return new Promise<() => void>((resolve) => {
- state.queue.push({ conversationId, promptStartedAt, resolve });
- // Keep sorted ascending by promptStartedAt (oldest first).
- state.queue.sort((a, b) => a.promptStartedAt - b.promptStartedAt);
+ state.queue.push({ conversationId, workspaceId, promptStartedAt, resolve });
+ // Keep sorted by priority (starred first, then oldest-agent-first).
+ // The queue is typically tiny (<20), so a simple sort is fine.
+ state.queue.sort(compareWaiters);
// If the usage gate is active, ensure the fallback repoll timer is
// armed (a release may not come for a while; the 1s timer covers an
// upstream count that drops on its own).
if (fetchUsage !== undefined) {
armGateRepoll(providerId, state);
}
- state.queue.push({ conversationId, workspaceId, promptStartedAt, resolve });
- // Keep sorted by priority (starred first, then oldest-agent-first).
- // The queue is typically tiny (<20), so a simple sort is fine.
- state.queue.sort(compareWaiters);
});
},
diff --git a/packages/provider-concurrency/tsconfig.json b/packages/provider-concurrency/tsconfig.json
index 44ed916..a4b26ef 100644
--- a/packages/provider-concurrency/tsconfig.json
+++ b/packages/provider-concurrency/tsconfig.json
@@ -2,5 +2,5 @@
"extends": "../../tsconfig.base.json",
"compilerOptions": { "rootDir": "src", "outDir": "dist", "composite": true },
"include": ["src/**/*.ts"],
- "references": [{ "path": "../kernel" }]
+ "references": [{ "path": "../conversation-store" }, { "path": "../kernel" }]
}