agent-relay-server 0.123.2 → 0.124.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/openapi.json +1 -1
- package/package.json +3 -3
- package/public/assets/{MessageBubble-NCggGKTA.js → MessageBubble-SQK8-Mzp.js} +2 -2
- package/public/assets/{MessageBubble-NCggGKTA.js.map → MessageBubble-SQK8-Mzp.js.map} +1 -1
- package/public/assets/{PlanGraphPanel-CSdszmGk.js → PlanGraphPanel-CS-aow4S.js} +2 -2
- package/public/assets/{PlanGraphPanel-CSdszmGk.js.map → PlanGraphPanel-CS-aow4S.js.map} +1 -1
- package/public/assets/{activity-Cyp2nolX.js → activity-W31XvhC2.js} +2 -2
- package/public/assets/{activity-Cyp2nolX.js.map → activity-W31XvhC2.js.map} +1 -1
- package/public/assets/{agent-profiles-D5Izg0Vn.js → agent-profiles-DXysA9To.js} +2 -2
- package/public/assets/{agent-profiles-D5Izg0Vn.js.map → agent-profiles-DXysA9To.js.map} +1 -1
- package/public/assets/{agents-DIOxuJ6M.js → agents-B5GRNRnN.js} +2 -2
- package/public/assets/{agents-DIOxuJ6M.js.map → agents-B5GRNRnN.js.map} +1 -1
- package/public/assets/{automation-Bn6mOLAR.js → automation-DYlgHYcb.js} +2 -2
- package/public/assets/{automation-Bn6mOLAR.js.map → automation-DYlgHYcb.js.map} +1 -1
- package/public/assets/{chat-eUaPz399.js → chat-Clz6qOtn.js} +2 -2
- package/public/assets/{chat-eUaPz399.js.map → chat-Clz6qOtn.js.map} +1 -1
- package/public/assets/{formatted-body-CvrQ2D1J.js → formatted-body-BYQ-SPtj.js} +3 -3
- package/public/assets/{formatted-body-CvrQ2D1J.js.map → formatted-body-BYQ-SPtj.js.map} +1 -1
- package/public/assets/{formatted-body-impl-BJ7jdbpR.js → formatted-body-impl-L1EMnNW6.js} +2 -2
- package/public/assets/{formatted-body-impl-BJ7jdbpR.js.map → formatted-body-impl-L1EMnNW6.js.map} +1 -1
- package/public/assets/{index-DnUo0Zqg.js → index-0uIGXT14.js} +6 -6
- package/public/assets/{index-DnUo0Zqg.js.map → index-0uIGXT14.js.map} +1 -1
- package/public/assets/index-DOJ7KidA.css +2 -0
- package/public/assets/{insights-CQGH5LhD.js → insights-DwZxq1Dn.js} +2 -2
- package/public/assets/{insights-CQGH5LhD.js.map → insights-DwZxq1Dn.js.map} +1 -1
- package/public/assets/{maintenance-7c7UvpAt.js → maintenance-COcmUDnf.js} +2 -2
- package/public/assets/{maintenance-7c7UvpAt.js.map → maintenance-COcmUDnf.js.map} +1 -1
- package/public/assets/{managed-agents-yh4HPO5a.js → managed-agents-D9-dgMeX.js} +2 -2
- package/public/assets/{managed-agents-yh4HPO5a.js.map → managed-agents-D9-dgMeX.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-BuOn5-Uc.js → markdown-preview-impl-DH372bL-.js} +2 -2
- package/public/assets/{markdown-preview-impl-BuOn5-Uc.js.map → markdown-preview-impl-DH372bL-.js.map} +1 -1
- package/public/assets/{memory-Qv0JN8CE.js → memory-B9hRK8sk.js} +2 -2
- package/public/assets/{memory-Qv0JN8CE.js.map → memory-B9hRK8sk.js.map} +1 -1
- package/public/assets/{messages-CgjHJmc2.js → messages-BHkHlXzF.js} +2 -2
- package/public/assets/{messages-CgjHJmc2.js.map → messages-BHkHlXzF.js.map} +1 -1
- package/public/assets/{orchestrators-CgL4CFzb.js → orchestrators-CXy3SDSn.js} +2 -2
- package/public/assets/{orchestrators-CgL4CFzb.js.map → orchestrators-CXy3SDSn.js.map} +1 -1
- package/public/assets/{overview-Bri04eYE.js → overview-D2yRyCQq.js} +2 -2
- package/public/assets/{overview-Bri04eYE.js.map → overview-D2yRyCQq.js.map} +1 -1
- package/public/assets/{pairs-C78LwUH2.js → pairs-5tXdHylI.js} +2 -2
- package/public/assets/{pairs-C78LwUH2.js.map → pairs-5tXdHylI.js.map} +1 -1
- package/public/assets/{prompt-settings-DDEkMqPZ.js → prompt-settings-Bn5mmi9w.js} +2 -2
- package/public/assets/{prompt-settings-DDEkMqPZ.js.map → prompt-settings-Bn5mmi9w.js.map} +1 -1
- package/public/assets/registry-C3sB7qv5.js +4 -0
- package/public/assets/registry-C3sB7qv5.js.map +1 -0
- package/public/assets/{security-wvnRKFvm.js → security-CmD-YE-_.js} +2 -2
- package/public/assets/{security-wvnRKFvm.js.map → security-CmD-YE-_.js.map} +1 -1
- package/public/assets/{select-BIfbVmLf.js → select-C0UlhK2k.js} +2 -2
- package/public/assets/{select-BIfbVmLf.js.map → select-C0UlhK2k.js.map} +1 -1
- package/public/assets/{settings-B6Ntahx7.js → settings-DrqMG5nG.js} +2 -2
- package/public/assets/{settings-B6Ntahx7.js.map → settings-DrqMG5nG.js.map} +1 -1
- package/public/assets/{tasks-CUglafo5.js → tasks-ChQjtXHu.js} +2 -2
- package/public/assets/{tasks-CUglafo5.js.map → tasks-ChQjtXHu.js.map} +1 -1
- package/public/assets/{teams-EE8RHhHM.js → teams-itjNlDLp.js} +2 -2
- package/public/assets/{teams-EE8RHhHM.js.map → teams-itjNlDLp.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-DBRkdyag.js → terminal-viewer-impl-B9M6s2vB.js} +2 -2
- package/public/assets/{terminal-viewer-impl-DBRkdyag.js.map → terminal-viewer-impl-B9M6s2vB.js.map} +1 -1
- package/public/assets/{work-queue-BFQVVL4T.js → work-queue-Kw0uMkEF.js} +2 -2
- package/public/assets/{work-queue-BFQVVL4T.js.map → work-queue-Kw0uMkEF.js.map} +1 -1
- package/public/index.html +3 -3
- package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/src/db/provisioning-registry.ts +174 -1
- package/src/routes/provisioning-registry.ts +3 -0
- package/src/routing-policy-store.ts +55 -0
- package/src/spawn-targets.ts +130 -28
- package/public/assets/index-DcmG5gLR.css +0 -2
- package/public/assets/registry-5KhP5FE5.js +0 -2
- package/public/assets/registry-5KhP5FE5.js.map +0 -1
package/src/spawn-targets.ts
CHANGED
|
@@ -6,8 +6,23 @@ import { renderTemplate } from "./prompt-resolver";
|
|
|
6
6
|
import { getRoutingPolicy } from "./routing-policy-store";
|
|
7
7
|
import { isPathWithinBase } from "./utils";
|
|
8
8
|
import { McpNotFoundError } from "./mcp-errors";
|
|
9
|
-
import {
|
|
10
|
-
|
|
9
|
+
import {
|
|
10
|
+
DEFAULT_QUOTA_PACE_DELTA,
|
|
11
|
+
DEFAULT_QUOTA_PACE_THRESHOLDS,
|
|
12
|
+
DEFAULT_QUOTA_PREFER_SLACK_AT,
|
|
13
|
+
DEFAULT_QUOTA_TARGET_UTILIZATION,
|
|
14
|
+
formatPaceLabel,
|
|
15
|
+
QUOTA_PACE_WINDOW_LENGTH_MS,
|
|
16
|
+
quotaOvershootPenalty,
|
|
17
|
+
quotaPaceBandForWeight,
|
|
18
|
+
quotaPaceCombinedBand,
|
|
19
|
+
quotaPaceMetrics,
|
|
20
|
+
quotaPaceWindowMetrics,
|
|
21
|
+
type QuotaPaceBand,
|
|
22
|
+
type QuotaPaceRoleSignal,
|
|
23
|
+
type QuotaPaceThresholds,
|
|
24
|
+
} from "agent-relay-sdk/provider-quota";
|
|
25
|
+
import { isSpawnProvider, type ProviderQuotaRoutingConfig, type QuotaPacingConfig, type RoutingPolicy } from "agent-relay-sdk";
|
|
11
26
|
import type { Orchestrator, ProviderCatalogSummary, ProviderQuotaBurnRole, ProviderQuotaBurnWindow, ProviderQuotaRecord, QuotaState, SpawnProvider } from "./types";
|
|
12
27
|
|
|
13
28
|
// #308 — spawn discovery + target selection. Home for the live capability matrix that
|
|
@@ -83,9 +98,17 @@ interface SpawnQuotaBurnWindow {
|
|
|
83
98
|
projectedWallAt: number | null;
|
|
84
99
|
earlyWallMs: number | null;
|
|
85
100
|
// #760 — symmetric underburn signal (see ProviderQuotaBurnWindow.slack), re-derived against
|
|
86
|
-
// the per-provider target
|
|
101
|
+
// the per-provider target. #1098: demoted to a tiebreaker/secondary signal — still surfaced
|
|
102
|
+
// (and still the input to the "prefer" overlay below) but no longer the primary band driver.
|
|
87
103
|
projectedUtilAtReset: number | null;
|
|
88
104
|
slack: number | null;
|
|
105
|
+
// #1098 — the pace-error control signal: present position vs. the ideal linear pace line,
|
|
106
|
+
// and the legibility label ("+9% ahead of pace" beats "projected 115%"). null when timeToReset
|
|
107
|
+
// is unknown (no reset boundary to pace against).
|
|
108
|
+
paceTarget: number | null;
|
|
109
|
+
paceError: number | null;
|
|
110
|
+
paceWeight: number | null;
|
|
111
|
+
pace: string | null;
|
|
89
112
|
prefer: boolean;
|
|
90
113
|
penalty: number;
|
|
91
114
|
blockedNow: boolean;
|
|
@@ -112,6 +135,32 @@ const DEFAULT_QUOTA_ADVISORY: SpawnQuotaAdvisory = {
|
|
|
112
135
|
recommendedAction: "No quota burn projection available; spawn normally.",
|
|
113
136
|
};
|
|
114
137
|
|
|
138
|
+
// #1098/#1105 — resolved pacing tunables: delta/thresholds/windowLengthMs are GLOBAL (one dial per
|
|
139
|
+
// relay instance, not per-provider — matches the design's "δ a tunable constant"), sourced from
|
|
140
|
+
// RoutingPolicy.quotaPacing with the provider-quota.ts constants as fallback defaults, so an
|
|
141
|
+
// unconfigured instance reproduces the shipped behaviour exactly.
|
|
142
|
+
interface ResolvedQuotaPacing {
|
|
143
|
+
delta: number;
|
|
144
|
+
thresholds: QuotaPaceThresholds;
|
|
145
|
+
windowLengthMs: Record<ProviderQuotaBurnRole, number>;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function resolveQuotaPacing(policy: RoutingPolicy): ResolvedQuotaPacing {
|
|
149
|
+
const cfg: QuotaPacingConfig = policy.quotaPacing ?? {};
|
|
150
|
+
return {
|
|
151
|
+
delta: cfg.delta ?? DEFAULT_QUOTA_PACE_DELTA,
|
|
152
|
+
thresholds: {
|
|
153
|
+
preferAt: cfg.thresholds?.preferAt ?? DEFAULT_QUOTA_PACE_THRESHOLDS.preferAt,
|
|
154
|
+
normalAt: cfg.thresholds?.normalAt ?? DEFAULT_QUOTA_PACE_THRESHOLDS.normalAt,
|
|
155
|
+
cautionAt: cfg.thresholds?.cautionAt ?? DEFAULT_QUOTA_PACE_THRESHOLDS.cautionAt,
|
|
156
|
+
},
|
|
157
|
+
windowLengthMs: {
|
|
158
|
+
"5h": cfg.windowLengthMs?.["5h"] ?? QUOTA_PACE_WINDOW_LENGTH_MS["5h"],
|
|
159
|
+
"7d": cfg.windowLengthMs?.["7d"] ?? QUOTA_PACE_WINDOW_LENGTH_MS["7d"],
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
115
164
|
// #926 — the orchestrator's REPORTED catalog is built from the static SDK manifest only
|
|
116
165
|
// (orchestrator/src/provider-probe.ts uses providerCatalogList(), which has no DB access), while
|
|
117
166
|
// spawn VALIDATION resolves against effectiveProviderCatalogList() (static catalog + DB
|
|
@@ -189,21 +238,22 @@ export function providerQuotaAdvisories(now = Date.now()): Map<SpawnProvider, Sp
|
|
|
189
238
|
const provider = record.provider;
|
|
190
239
|
byProvider.set(provider, [...(byProvider.get(provider) ?? []), record]);
|
|
191
240
|
}
|
|
241
|
+
const pacing = resolveQuotaPacing(policy);
|
|
192
242
|
return new Map([...byProvider.entries()].map(([provider, records]) => [
|
|
193
243
|
provider,
|
|
194
|
-
quotaAdvisoryFor(records, now,
|
|
244
|
+
quotaAdvisoryFor(records, now, pacing, policy.providerQuota?.[provider]),
|
|
195
245
|
]));
|
|
196
246
|
}
|
|
197
247
|
|
|
198
248
|
function quotaAdvisoryFor(
|
|
199
249
|
records: ProviderQuotaRecord[],
|
|
200
250
|
now: number,
|
|
201
|
-
|
|
251
|
+
pacing: ResolvedQuotaPacing,
|
|
202
252
|
providerCfg?: ProviderQuotaRoutingConfig,
|
|
203
253
|
): SpawnQuotaAdvisory {
|
|
204
254
|
const selected = new Map<ProviderQuotaBurnRole, SpawnQuotaBurnWindow>();
|
|
205
255
|
for (const record of records) {
|
|
206
|
-
for (const window of quotaWindowsFor(record, now, providerCfg)) {
|
|
256
|
+
for (const window of quotaWindowsFor(record, now, providerCfg, pacing)) {
|
|
207
257
|
const existing = selected.get(window.role);
|
|
208
258
|
if (!existing || compareQuotaWindows(window, existing) > 0) selected.set(window.role, window);
|
|
209
259
|
}
|
|
@@ -211,15 +261,28 @@ function quotaAdvisoryFor(
|
|
|
211
261
|
const windows = [...selected.values()].sort((a, b) => roleSort(a.role) - roleSort(b.role));
|
|
212
262
|
const quotaPenalty = Math.max(0, ...windows.map((window) => window.penalty));
|
|
213
263
|
const blockedNow = windows.some((window) => window.blockedNow);
|
|
214
|
-
|
|
215
|
-
//
|
|
216
|
-
// to
|
|
217
|
-
|
|
264
|
+
// #1098 — the provider-level band: the MORE-BINDING window's pace weight drives the ladder;
|
|
265
|
+
// hard-avoid additionally requires the binding window's paceError beyond δ AND no other window
|
|
266
|
+
// able to absorb the slack (or blockedNow). Replaces the old max-penalty-then-threshold cliff.
|
|
267
|
+
const signals: QuotaPaceRoleSignal[] = windows
|
|
268
|
+
.filter((window): window is SpawnQuotaBurnWindow & { paceWeight: number; paceError: number } =>
|
|
269
|
+
window.paceWeight !== null && window.paceError !== null)
|
|
270
|
+
.map((window) => ({ role: window.role, paceError: window.paceError, weight: window.paceWeight, blockedNow: window.blockedNow }));
|
|
271
|
+
const { band: paceBand } = quotaPaceCombinedBand(signals, pacing.delta, pacing.thresholds);
|
|
272
|
+
// #760 — provider-level "prefer" tiebreaker: every selected window is projected (the #1072
|
|
273
|
+
// slow-EWMA trend, demoted to secondary per #1098) to land ≥ preferSlackAt below target — e.g. a
|
|
274
|
+
// just-reset window whose RAW utilization reads mildly "ahead" of a near-zero pace line (paceBand
|
|
275
|
+
// normal/caution) but whose sustained trend shows it is nowhere near the target. The binding
|
|
276
|
+
// constraint is the LEAST-slack window, so a tight 5h window vetoes prefer even if 7d is wide
|
|
277
|
+
// open. Reserved for normal/caution — avoid-large/hard-avoid are real over-pace signals that a
|
|
278
|
+
// secondary projection should not paper over.
|
|
218
279
|
const preferSlackAt = providerCfg?.preferSlackAt ?? DEFAULT_QUOTA_PREFER_SLACK_AT;
|
|
219
280
|
const slacks = windows.map((window) => window.slack).filter((slack): slack is number => slack !== null);
|
|
220
281
|
const minSlack = slacks.length === windows.length && slacks.length > 0 ? Math.min(...slacks) : null;
|
|
221
|
-
const prefer = !blockedNow
|
|
222
|
-
|
|
282
|
+
const prefer = !blockedNow
|
|
283
|
+
&& (paceBand === "prefer"
|
|
284
|
+
|| ((paceBand === "normal" || paceBand === "caution") && minSlack !== null && minSlack >= preferSlackAt));
|
|
285
|
+
const quotaPenaltyBand: SpawnQuotaBurnBand = prefer ? "prefer" : paceBand;
|
|
223
286
|
return {
|
|
224
287
|
quotaBurn: { windows },
|
|
225
288
|
quotaPenalty,
|
|
@@ -254,10 +317,15 @@ export function providerQuotaBands(now = Date.now()): ProviderQuotaBand[] {
|
|
|
254
317
|
}));
|
|
255
318
|
}
|
|
256
319
|
|
|
257
|
-
function quotaWindowsFor(
|
|
320
|
+
function quotaWindowsFor(
|
|
321
|
+
record: ProviderQuotaRecord,
|
|
322
|
+
now: number,
|
|
323
|
+
providerCfg: ProviderQuotaRoutingConfig | undefined,
|
|
324
|
+
pacing: ResolvedQuotaPacing,
|
|
325
|
+
): SpawnQuotaBurnWindow[] {
|
|
258
326
|
const quotaByName = new Map((record.quota?.windows ?? []).map((window) => [window.name, window]));
|
|
259
327
|
return (record.burn?.windows ?? [])
|
|
260
|
-
.map((burnWindow) => spawnQuotaWindow(record, burnWindow, quotaByName.get(burnWindow.name), now, providerCfg))
|
|
328
|
+
.map((burnWindow) => spawnQuotaWindow(record, burnWindow, quotaByName.get(burnWindow.name), now, providerCfg, pacing))
|
|
261
329
|
.filter((window): window is SpawnQuotaBurnWindow => Boolean(window));
|
|
262
330
|
}
|
|
263
331
|
|
|
@@ -266,7 +334,8 @@ function spawnQuotaWindow(
|
|
|
266
334
|
burnWindow: ProviderQuotaBurnWindow,
|
|
267
335
|
quotaWindow: QuotaState["windows"][number] | undefined,
|
|
268
336
|
now: number,
|
|
269
|
-
providerCfg
|
|
337
|
+
providerCfg: ProviderQuotaRoutingConfig | undefined,
|
|
338
|
+
pacing: ResolvedQuotaPacing,
|
|
270
339
|
): SpawnQuotaBurnWindow | undefined {
|
|
271
340
|
if (!quotaWindow) return undefined;
|
|
272
341
|
const timeToResetMs = typeof quotaWindow.resetsAt === "number" ? Math.max(0, quotaWindow.resetsAt - now) : null;
|
|
@@ -288,20 +357,36 @@ function spawnQuotaWindow(
|
|
|
288
357
|
burnRatio = quotaPaceMetrics({ utilization: quotaWindow.utilization, observedRatePerMs: burnWindow.observedRatePerMs, timeToResetMs, target }).burnRatio;
|
|
289
358
|
}
|
|
290
359
|
|
|
291
|
-
// #1072 —
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
// hard-escalate on an extrapolated slope. Idle and active share this single formula — an idle
|
|
295
|
-
// over-target provider is held by the current-overshoot term (confidence-independent, so the
|
|
296
|
-
// band is flat tick-to-tick), an active climb escalates as the confidence-weighted trend crosses.
|
|
360
|
+
// #1072 (kept, demoted to secondary) — the unified overshoot penalty/slack against the
|
|
361
|
+
// per-provider target. #1098 no longer uses `penalty` to drive the band, but it's still surfaced
|
|
362
|
+
// (informational) and `slack` still feeds the "prefer" tiebreaker below.
|
|
297
363
|
const confidence = burnWindow.confidenceScore ?? 0;
|
|
298
364
|
const { slack, penalty: rawPenalty } = projectedUtilAtReset !== null
|
|
299
365
|
? quotaOvershootPenalty(projectedUtilAtReset, quotaWindow.utilization, target, confidence)
|
|
300
366
|
: { slack: burnWindow.slack, penalty: 0 };
|
|
301
367
|
const penalty = quotaWindow.utilization < utilizationFloor ? 0 : rawPenalty;
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
368
|
+
|
|
369
|
+
// #1098 — the pace-error control signal: present position (utilization) vs. the ideal linear
|
|
370
|
+
// pace line (elapsed/windowLength), NOT a forecast. Drives the band; see quotaAdvisoryFor for
|
|
371
|
+
// the cross-window (5h+7d, more-binding-wins) combination and the hard-avoid escalation gate.
|
|
372
|
+
const paceMetrics = timeToResetMs !== null
|
|
373
|
+
? quotaPaceWindowMetrics({
|
|
374
|
+
utilization: quotaWindow.utilization,
|
|
375
|
+
timeToResetMs,
|
|
376
|
+
windowLengthMs: pacing.windowLengthMs[burnWindow.role],
|
|
377
|
+
delta: pacing.delta,
|
|
378
|
+
})
|
|
379
|
+
: null;
|
|
380
|
+
const windowBand: QuotaPaceBand | null = paceMetrics ? quotaPaceBandForWeight(paceMetrics.weight, pacing.thresholds) : null;
|
|
381
|
+
// #1098 — per-window "prefer": either the weight ladder itself climbed past neutral into active
|
|
382
|
+
// promotion (use-it-or-lose-it, near reset with util below pace), or the window carries ample
|
|
383
|
+
// future slack per the secondary/tiebreaker projection (the #1072 target-buffer signal — e.g. a
|
|
384
|
+
// just-reset window reading mildly "ahead" of a near-zero pace line but nowhere near the target
|
|
385
|
+
// on the sustained trend). Reserved for normal/caution; avoid-large/hard-avoid are real over-pace
|
|
386
|
+
// signals a secondary projection should not paper over.
|
|
387
|
+
const prefer = !blockedNow && windowBand !== null
|
|
388
|
+
&& (windowBand === "prefer"
|
|
389
|
+
|| ((windowBand === "normal" || windowBand === "caution") && slack !== null && slack >= preferSlackAt));
|
|
305
390
|
return {
|
|
306
391
|
role: burnWindow.role,
|
|
307
392
|
name: burnWindow.name,
|
|
@@ -316,6 +401,10 @@ function spawnQuotaWindow(
|
|
|
316
401
|
earlyWallMs: burnWindow.earlyWallMs,
|
|
317
402
|
projectedUtilAtReset,
|
|
318
403
|
slack,
|
|
404
|
+
paceTarget: paceMetrics?.target ?? null,
|
|
405
|
+
paceError: paceMetrics?.paceError ?? null,
|
|
406
|
+
paceWeight: paceMetrics?.weight ?? null,
|
|
407
|
+
pace: paceMetrics ? formatPaceLabel(paceMetrics.paceError) : null,
|
|
319
408
|
prefer,
|
|
320
409
|
penalty,
|
|
321
410
|
blockedNow,
|
|
@@ -357,7 +446,8 @@ export function getProviderBandInfo(provider: string, now: number): ProviderBand
|
|
|
357
446
|
const policy = getRoutingPolicy();
|
|
358
447
|
const records = listProviderQuotas({ provider, historyLimit: 0, now }).quotas;
|
|
359
448
|
if (records.length === 0) return undefined;
|
|
360
|
-
const
|
|
449
|
+
const pacing = resolveQuotaPacing(policy);
|
|
450
|
+
const advisory = quotaAdvisoryFor(records, now, pacing, policy.providerQuota?.[provider as SpawnProvider]);
|
|
361
451
|
const quotaBands = {
|
|
362
452
|
cautionAt: policy.quotaBands?.cautionAt ?? 0.15,
|
|
363
453
|
avoidLargeAt: policy.quotaBands?.avoidLargeAt ?? 0.5,
|
|
@@ -365,8 +455,11 @@ export function getProviderBandInfo(provider: string, now: number): ProviderBand
|
|
|
365
455
|
};
|
|
366
456
|
const windows: ProviderBandWindow[] = advisory.quotaBurn.windows.map((w) => ({
|
|
367
457
|
role: w.role,
|
|
368
|
-
// #
|
|
369
|
-
|
|
458
|
+
// #1098 — per-window prefer overlay: w.prefer already encodes the pace-ladder "prefer" state OR
|
|
459
|
+
// "on pace + ≥ preferSlackAt slack". Otherwise fall back to the window's own pace-weight band
|
|
460
|
+
// (no cross-window hard-avoid escalation at this per-window level — that needs the OTHER
|
|
461
|
+
// window's state and is applied only at the provider level; see quotaAdvisoryFor).
|
|
462
|
+
band: w.prefer ? "prefer" : quotaPaceBandForWeight(w.paceWeight ?? 0.5, pacing.thresholds),
|
|
370
463
|
penalty: w.penalty,
|
|
371
464
|
utilization: w.utilization,
|
|
372
465
|
burnRatio: w.burnRatio,
|
|
@@ -380,7 +473,16 @@ export function getProviderBandInfo(provider: string, now: number): ProviderBand
|
|
|
380
473
|
|
|
381
474
|
function compareQuotaWindows(a: SpawnQuotaBurnWindow, b: SpawnQuotaBurnWindow): number {
|
|
382
475
|
if (a.blockedNow !== b.blockedNow) return a.blockedNow ? 1 : -1;
|
|
383
|
-
|
|
476
|
+
// #1098 — the more-BINDING (lower pace weight) window is "worse"; unknown weight (no reset
|
|
477
|
+
// boundary) reads as neutral (0.5), same as an on-pace window.
|
|
478
|
+
const aw = a.paceWeight ?? 0.5;
|
|
479
|
+
const bw = b.paceWeight ?? 0.5;
|
|
480
|
+
if (aw !== bw) return bw - aw;
|
|
481
|
+
// #1098 — projectedUtilAtReset demoted to a tiebreaker: when pace weight ties, the more
|
|
482
|
+
// concerning (higher) projection wins the "worse" slot.
|
|
483
|
+
const ap = a.projectedUtilAtReset ?? -Infinity;
|
|
484
|
+
const bp = b.projectedUtilAtReset ?? -Infinity;
|
|
485
|
+
if (ap !== bp) return ap - bp;
|
|
384
486
|
return a.utilization - b.utilization;
|
|
385
487
|
}
|
|
386
488
|
|