1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
|
import { conversationStoreHandle } from "@dispatch/conversation-store";
import type { Extension, HostAPI, Logger, Manifest, StorageNamespace } from "@dispatch/kernel";
import type { ConcurrencyManagerOpts, ConcurrencyService } from "./concurrency-manager.js";
import { createConcurrencyManager } from "./concurrency-manager.js";
import { concurrencyServiceHandle } from "./service.js";
export const manifest: Manifest = {
id: "provider-concurrency",
name: "Provider Concurrency Limits",
version: "0.0.0",
apiVersion: "^0.1.0",
trust: "bundled",
activation: "eager",
capabilities: { db: true },
dependsOn: ["conversation-store"],
contributes: { services: ["provider-concurrency/service"] },
};
/**
* Default tuning constants.
*
* - `SLOT_TIMEOUT_MS` (5 min): a slot held longer than this is force-reclaimed
* by the watchdog (deadlock / stuck-agent recovery). Generation streams
* rarely exceed 2–3 minutes; 5 min is a generous safety margin.
* - `WATCHDOG_INTERVAL_MS` (30s): how often the watchdog sweeps for stale slots.
* - `DEFAULT_PAUSE_MS` (30s): default 429 backoff when no Retry-After is given.
* Umans docs note each concurrency 429 deprioritizes the account for ~30 min,
* but a 30s queue pause prevents immediate re-overshoot while still allowing
* recovery. Combined with adaptive headroom (limit reduced by 1) + the usage
* gate, the resumed queue no longer re-overshoots — so the pause is kept
* (gives upstream a breather) rather than dropped.
* - `RELEASE_COOLDOWN_MS` (350ms): after a slot is released, hold it for this
* duration before recycling it to the next waiter. Covers the upstream
* provider's accounting lag — the provider's concurrent_sessions counter may
* not decrement the instant our stream completes, so re-admitting immediately
* risks an N+1 overshoot that triggers a 429. Raised from 200ms to 350ms
* (Umans's accounting lag exceeded the 200ms cooldown, causing overshoot at 4
* connections). Configurable + persisted per provider (PUT
* /concurrency/cooldown/:providerId).
*/
const SLOT_TIMEOUT_MS = 5 * 60 * 1000;
const WATCHDOG_INTERVAL_MS = 30 * 1000;
const DEFAULT_PAUSE_MS = 30 * 1000;
const RELEASE_COOLDOWN_MS = 350;
/**
* Storage key prefixes. Limits are stored under the bare `<providerId>` key
* (unchanged for backward compatibility). Cooldowns + the adaptive-headroom
* auto-reduce marker are stored under their own prefixed keys so they persist
* independently without loadLimits misreading them as limits.
*/
const COOLDOWN_KEY_PREFIX = "cooldown:";
const AUTOREDUCE_KEY_PREFIX = "auto-reduce:";
/**
* Wrap a `ConcurrencyService` so `setLimit`/`removeLimit`/`setCooldown` persist
* to the given `StorageNamespace`. All other methods delegate directly to the
* inner service. Persistence is fire-and-forget — a storage write failure logs
* a warning but does NOT fail the API call (the in-memory value is already set).
*
* `restoreLimit` is NOT persisted here — it is a startup restore FROM disk, so
* it delegates straight through (the value is already on disk).
*/
function createPersistedService(
inner: ConcurrencyService,
storage: StorageNamespace,
logger: Logger,
): ConcurrencyService {
return {
acquire: inner.acquire.bind(inner),
reportRateLimit: inner.reportRateLimit.bind(inner),
setLimit(providerId, limit) {
inner.setLimit(providerId, limit);
storage.set(providerId, String(limit)).catch((err) =>
logger.warn("provider-concurrency: failed to persist limit", {
providerId,
err: err instanceof Error ? err.message : String(err),
}),
);
// A MANUAL limit set clears the auto-reduce notice (the user took
// control) → drop the persisted auto-reduce marker too.
storage.delete(`${AUTOREDUCE_KEY_PREFIX}${providerId}`).catch(() => {
/* absent marker is fine */
});
},
restoreLimit: inner.restoreLimit.bind(inner),
removeLimit(providerId) {
inner.removeLimit(providerId);
storage.delete(providerId).catch((err) =>
logger.warn("provider-concurrency: failed to delete persisted limit", {
providerId,
err: err instanceof Error ? err.message : String(err),
}),
);
storage.delete(`${AUTOREDUCE_KEY_PREFIX}${providerId}`).catch(() => {
/* absent marker is fine */
});
},
setCooldown(providerId, cooldownMs) {
inner.setCooldown(providerId, cooldownMs);
storage.set(`${COOLDOWN_KEY_PREFIX}${providerId}`, String(cooldownMs)).catch((err) =>
logger.warn("provider-concurrency: failed to persist cooldown", {
providerId,
err: err instanceof Error ? err.message : String(err),
}),
);
},
getLimit: inner.getLimit.bind(inner),
getLimits: inner.getLimits.bind(inner),
getCooldown: inner.getCooldown.bind(inner),
getCooldowns: inner.getCooldowns.bind(inner),
getStatus: inner.getStatus.bind(inner),
getStatusAll: inner.getStatusAll.bind(inner),
notifyWorkspaceStarred: inner.notifyWorkspaceStarred.bind(inner),
destroy: inner.destroy.bind(inner),
};
}
/**
* Load saved limits from storage and apply them to the manager via
* `restoreLimit` (NOT `setLimit` — Bug 3). `setLimit` is a MANUAL user action
* that clears the auto-reduce notice; using it at startup would wipe the
* persisted auto-reduce banner. `restoreLimit` seeds the limit WITHOUT clearing
* the notice, and `loadAutoReduce` re-applies the notice afterward.
*
* Skips prefixed keys (cooldown:/auto-reduce:) — those are loaded by their
* own loaders.
*/
async function loadLimits(
storage: StorageNamespace,
manager: ConcurrencyService,
logger: Logger,
): Promise<void> {
const keys = await storage.keys();
for (const key of keys) {
if (key.startsWith(COOLDOWN_KEY_PREFIX)) continue; // cooldown settings
if (key.startsWith(AUTOREDUCE_KEY_PREFIX)) continue; // auto-reduce markers
const providerId = key;
const raw = await storage.get(providerId);
if (raw === null) continue;
const limit = Number.parseInt(raw, 10);
if (!Number.isNaN(limit) && limit > 0) {
manager.restoreLimit(providerId, limit);
logger.info(`provider-concurrency: restored limit ${limit} for "${providerId}"`);
}
}
}
/**
* Load saved auto-reduce markers and re-apply them via `restoreLimit` so the
* frontend banner survives a restart (Bug 3). A marker is stored under
* `auto-reduce:<providerId>` with the value = the ORIGINAL limit before
* reduction (autoReducedFrom). The current (reduced) limit was already restored
* by {@link loadLimits}; this call re-marks it as auto-reduced.
*/
async function loadAutoReduce(
storage: StorageNamespace,
manager: ConcurrencyService,
logger: Logger,
): Promise<void> {
const keys = await storage.keys(AUTOREDUCE_KEY_PREFIX);
for (const key of keys) {
const providerId = key.slice(AUTOREDUCE_KEY_PREFIX.length);
if (providerId.length === 0) continue;
const raw = await storage.get(key);
if (raw === null) continue;
const autoReducedFrom = Number.parseInt(raw, 10);
if (!Number.isNaN(autoReducedFrom) && autoReducedFrom > 0) {
const currentLimit = manager.getLimit(providerId);
if (currentLimit !== undefined && currentLimit < autoReducedFrom) {
manager.restoreLimit(providerId, currentLimit, autoReducedFrom);
logger.info(
`provider-concurrency: restored auto-reduce notice for "${providerId}" ` +
`(${autoReducedFrom} -> ${currentLimit})`,
);
}
}
}
}
/**
* Load saved cooldowns from storage and apply them to the manager.
* Cooldowns are stored under `cooldown:<providerId>` keys (distinct from the
* bare-`<providerId>` limit keys) so the two settings persist independently.
*/
async function loadCooldowns(
storage: StorageNamespace,
manager: ConcurrencyService,
logger: Logger,
): Promise<void> {
const keys = await storage.keys(COOLDOWN_KEY_PREFIX);
for (const key of keys) {
const providerId = key.slice(COOLDOWN_KEY_PREFIX.length);
if (providerId.length === 0) continue;
const raw = await storage.get(key);
if (raw === null) continue;
const cooldownMs = Number.parseInt(raw, 10);
if (!Number.isNaN(cooldownMs) && cooldownMs >= 0) {
manager.setCooldown(providerId, cooldownMs);
logger.info(`provider-concurrency: restored cooldown ${cooldownMs}ms for "${providerId}"`);
}
}
}
export async function activate(host: HostAPI): Promise<void> {
const logger = host.logger;
const storage = host.storage("provider-concurrency");
// Build the injected usage-poll effect from the host's provider registry.
// Lazy (called at poll time, not activate time) so activation order with the
// provider extensions doesn't matter. A provider that doesn't expose
// `getUsage` (or isn't registered) → returns undefined → the manager's usage
// gate falls back to cooldown-only recycling for that provider. This keeps
// the manager pure (the HTTP poll is an injected effect, not hardcoded fetch).
const fetchUsage = async (providerId: string) => {
const provider = host.getProviders().get(providerId);
if (provider === undefined || provider.getUsage === undefined) return undefined;
return provider.getUsage();
};
// Resolve the conversation store to seed the in-memory starred-workspace
// cache. The `isWorkspaceStarred` callback reads this cache synchronously
// (the queue sort comparator is sync), so we must populate it before the
// manager handles its first acquire. `dependsOn: ["conversation-store"]`
// in the manifest guarantees the store is registered before we activate.
const conversationStore = host.getService(conversationStoreHandle);
// The manager owns the in-memory `starredWorkspaces` set internally (the
// default `isWorkspaceStarred` callback checks it). We seed it by calling
// `notifyWorkspaceStarred` for each starred workspace found in the store.
const managerOpts: ConcurrencyManagerOpts = {
now: () => Date.now(),
slotTimeoutMs: SLOT_TIMEOUT_MS,
watchdogIntervalMs: WATCHDOG_INTERVAL_MS,
defaultPauseMs: DEFAULT_PAUSE_MS,
releaseCooldownMs: RELEASE_COOLDOWN_MS,
fetchUsage,
onWatchdogReclaim: (providerId, conversationId, heldMs) => {
logger.warn("provider-concurrency: watchdog reclaimed stale slot", {
providerId,
conversationId,
heldMs,
});
},
onPause: (providerId, durationMs) => {
logger.warn("provider-concurrency: 429 backoff — pausing queue", {
providerId,
durationMs,
});
},
onLimitReduced: (providerId, newLimit, oldLimit) => {
logger.warn("provider-concurrency: 429 adaptive headroom — limit reduced", {
providerId,
oldLimit,
newLimit,
});
// Persist the reduced (one-way) limit so it survives a restart, AND the
// auto-reduce marker (autoReducedFrom) so the banner survives too (Bug 3).
storage.set(providerId, String(newLimit)).catch((err) =>
logger.warn("provider-concurrency: failed to persist auto-reduced limit", {
providerId,
err: err instanceof Error ? err.message : String(err),
}),
);
storage.set(`${AUTOREDUCE_KEY_PREFIX}${providerId}`, String(oldLimit)).catch((err) =>
logger.warn("provider-concurrency: failed to persist auto-reduce marker", {
providerId,
err: err instanceof Error ? err.message : String(err),
}),
);
},
onUsagePollError: (providerId, err) => {
// A throwing getUsage() is treated as "no usage info" (cooldown-only
// fallback) by the manager — this is WARN-level observability only (Bug 2).
logger.warn("provider-concurrency: usage poll failed — falling back to cooldown-only", {
providerId,
err: err instanceof Error ? err.message : String(err),
});
},
};
const inner = createConcurrencyManager(managerOpts);
// Restore persisted limits + auto-reduce notices + cooldowns before registering
// the service so the first request sees the correct configuration.
await loadLimits(storage, inner, logger);
await loadAutoReduce(storage, inner, logger);
await loadCooldowns(storage, inner, logger);
// Seed the in-memory starred cache from the conversation store so the
// priority scheduling is correct on a fresh server start (previously-starred
// workspaces are respected without requiring the user to re-star them).
try {
const workspaces = await conversationStore.listWorkspaces();
for (const ws of workspaces) {
if (ws.starred) {
inner.notifyWorkspaceStarred(ws.id, true);
}
}
if (workspaces.some((w) => w.starred)) {
logger.info("provider-concurrency: restored starred workspaces", {
count: workspaces.filter((w) => w.starred).length,
});
}
} catch (err) {
logger.warn("provider-concurrency: failed to load starred workspaces", {
err: err instanceof Error ? err.message : String(err),
});
}
const service = createPersistedService(inner, storage, logger);
host.provideService(concurrencyServiceHandle, service);
logger.info("provider-concurrency: registered");
}
export const extension: Extension = {
manifest,
activate,
};
|