agent-dealer 1.2.0 → 1.2.2
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/bundle/server/dist/adapters/linear-inbox.js +9 -5
- package/bundle/server/dist/capacity/adapter.js +2 -0
- package/bundle/server/dist/capacity/claude-events.js +18 -0
- package/bundle/server/dist/capacity/claude-events.test.js +7 -0
- package/bundle/server/dist/capacity/codex-app-server.js +100 -10
- package/bundle/server/dist/capacity/codex-app-server.test.js +195 -2
- package/bundle/server/dist/capacity/cursor-individual-credentials.js +238 -0
- package/bundle/server/dist/capacity/cursor-individual-credentials.test.js +185 -0
- package/bundle/server/dist/capacity/cursor-individual.js +970 -0
- package/bundle/server/dist/capacity/cursor-individual.test.js +815 -0
- package/bundle/server/dist/capacity/muse.js +94 -43
- package/bundle/server/dist/capacity/muse.test.js +95 -13
- package/bundle/server/dist/capacity/service.js +1 -0
- package/bundle/server/dist/coordinator/developer-effect.js +12 -7
- package/bundle/server/dist/coordinator/developer-effect.test.js +142 -5
- package/bundle/server/dist/db/index.js +30 -0
- package/bundle/server/dist/db/schema.sql +27 -0
- package/bundle/server/dist/index.js +2 -0
- package/bundle/server/dist/repository/cursor-individual-billing.js +84 -0
- package/bundle/server/dist/repository/repository-mappings.js +46 -0
- package/bundle/server/dist/repository/repository-mappings.test.js +83 -0
- package/bundle/server/dist/repository/runtime-capacity.js +8 -3
- package/bundle/server/dist/repository/runtime-capacity.test.js +4 -0
- package/bundle/server/dist/routes/cursor-individual-billing.test.js +80 -0
- package/bundle/server/dist/routes/index.js +32 -0
- package/bundle/server/dist/routes/intake-linear.test.js +55 -1
- package/bundle/server/dist/routes/repository-mappings.js +22 -0
- package/bundle/server/dist/routes/repository-mappings.test.js +110 -0
- package/bundle/server/dist/routes/runtime-capacity.test.js +72 -2
- package/bundle/server/package.json +2 -2
- package/bundle/server/static-ui/assets/index-0GDYfbaM.js +60 -0
- package/bundle/server/static-ui/assets/index-DUYzboWh.css +1 -0
- package/bundle/server/static-ui/index.html +2 -2
- package/bundle/shared/dist/index.d.ts +6 -6
- package/bundle/shared/dist/linear-intake.d.ts +71 -0
- package/bundle/shared/dist/linear-intake.js +90 -0
- package/bundle/shared/dist/linear-intake.test.js +80 -1
- package/bundle/shared/dist/runtime-capacity.d.ts +89 -0
- package/bundle/shared/dist/runtime-capacity.js +60 -0
- package/bundle/shared/dist/runtime-capacity.test.js +1 -0
- package/bundle/shared/package.json +1 -1
- package/dist/doctor.js +6 -0
- package/package.json +1 -1
- package/bundle/server/static-ui/assets/index-J1u7JDns.css +0 -1
- package/bundle/server/static-ui/assets/index-obJbiSaU.js +0 -60
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolveLinearRepoWithMappings } from "@agent-dealer/shared";
|
|
2
2
|
import { DEFAULT_LINEAR_STATE_FILTER, getLinearIntakeConfig } from "../repository/intake-settings.js";
|
|
3
|
+
import { listRepositoryMappings } from "../repository/repository-mappings.js";
|
|
3
4
|
import { linearGraphqlRequest } from "./linear-graphql.js";
|
|
4
5
|
export { DEFAULT_LINEAR_STATE_FILTER };
|
|
5
6
|
const PAGE_SIZE = 50;
|
|
@@ -20,7 +21,7 @@ async function linearQuery(operation, query, variables, opts) {
|
|
|
20
21
|
* deterministic (one valid label resolves, zero is unresolved, several
|
|
21
22
|
* conflict, a bad value is invalid) and never guesses from anything else.
|
|
22
23
|
*/
|
|
23
|
-
export function nodeToCandidate(n) {
|
|
24
|
+
export function nodeToCandidate(n, mappings) {
|
|
24
25
|
const labels = n.labels?.nodes.map((l) => l.name) ?? [];
|
|
25
26
|
return {
|
|
26
27
|
id: n.id,
|
|
@@ -31,7 +32,7 @@ export function nodeToCandidate(n) {
|
|
|
31
32
|
state: n.state?.name,
|
|
32
33
|
teamId: n.team?.id,
|
|
33
34
|
labels,
|
|
34
|
-
repoResolution:
|
|
35
|
+
repoResolution: resolveLinearRepoWithMappings(labels, mappings ?? []),
|
|
35
36
|
};
|
|
36
37
|
}
|
|
37
38
|
const ISSUE_FIELDS = `
|
|
@@ -106,7 +107,9 @@ export async function listLinearCandidates() {
|
|
|
106
107
|
break;
|
|
107
108
|
after = data.issues.pageInfo.endCursor;
|
|
108
109
|
}
|
|
109
|
-
|
|
110
|
+
// NOT-260: mappings load once per list operation, not once per candidate.
|
|
111
|
+
const mappings = listRepositoryMappings();
|
|
112
|
+
return nodes.map((n) => nodeToCandidate(n, mappings));
|
|
110
113
|
}
|
|
111
114
|
export async function getLinearIssue(issueId) {
|
|
112
115
|
const data = (await linearQuery("getLinearIssue", `query Issue($id: String!) {
|
|
@@ -114,7 +117,8 @@ export async function getLinearIssue(issueId) {
|
|
|
114
117
|
}`, { id: issueId }));
|
|
115
118
|
if (!data.issue)
|
|
116
119
|
return null;
|
|
117
|
-
|
|
120
|
+
// NOT-260: mappings load once per lookup operation.
|
|
121
|
+
return nodeToCandidate(data.issue, listRepositoryMappings());
|
|
118
122
|
}
|
|
119
123
|
const RELATION_PAGE_FIELDS = `
|
|
120
124
|
nodes { type issue { id identifier state { name type } } }
|
|
@@ -50,6 +50,7 @@ export function normalizeAdapterWindow(runtime, reading, nowMs = Date.now()) {
|
|
|
50
50
|
source: reading.source,
|
|
51
51
|
unavailableReason,
|
|
52
52
|
evidenceRef: reading.evidenceRef ?? null,
|
|
53
|
+
criticalRole: reading.criticalRole ?? null,
|
|
53
54
|
};
|
|
54
55
|
}
|
|
55
56
|
/** Normalize one provider failure into the persistable snapshot shape. */
|
|
@@ -70,6 +71,7 @@ export function normalizeUnavailableWindow(runtime, reading, nowMs = Date.now())
|
|
|
70
71
|
source: "unavailable",
|
|
71
72
|
unavailableReason: reading.reason,
|
|
72
73
|
evidenceRef: null,
|
|
74
|
+
criticalRole: reading.criticalRole ?? null,
|
|
73
75
|
};
|
|
74
76
|
}
|
|
75
77
|
// ---------------------------------------------------------------------------
|
|
@@ -170,6 +170,22 @@ export function inferClaudeDurationMinutes(bucket) {
|
|
|
170
170
|
return 10080;
|
|
171
171
|
return null;
|
|
172
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* The account-wide critical window's identity, decided here — once,
|
|
175
|
+
* authoritatively — and never re-derived downstream. Deliberately an exact
|
|
176
|
+
* match, unlike `inferClaudeDurationMinutes`'s loose regex: a model-specific
|
|
177
|
+
* extra like `seven_day_sonnet`/`seven_day_opus` shares the 10,080-minute
|
|
178
|
+
* duration with the real account-wide `seven_day`/`weekly` bucket but is
|
|
179
|
+
* not the critical window, so identity can't reuse that regex.
|
|
180
|
+
*/
|
|
181
|
+
export function claudeCriticalRole(bucket) {
|
|
182
|
+
const b = bucket.trim().toLowerCase();
|
|
183
|
+
if (b === "five_hour")
|
|
184
|
+
return "five_hour";
|
|
185
|
+
if (b === "seven_day" || b === "weekly")
|
|
186
|
+
return "weekly";
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
173
189
|
function windowReadingFromEntry(bucket, entry, observedAt) {
|
|
174
190
|
const cleanBucket = bucket.trim();
|
|
175
191
|
if (!cleanBucket)
|
|
@@ -218,6 +234,7 @@ function windowReadingFromEntry(bucket, entry, observedAt) {
|
|
|
218
234
|
observedAt,
|
|
219
235
|
source: "observed_event",
|
|
220
236
|
evidenceRef: CLAUDE_UNIFIED_WINDOWS_EVIDENCE_REF,
|
|
237
|
+
criticalRole: claudeCriticalRole(cleanBucket),
|
|
221
238
|
};
|
|
222
239
|
}
|
|
223
240
|
/**
|
|
@@ -350,6 +367,7 @@ export function recordClaudeCapacityFromEvents(events, runtime, nowMs = Date.now
|
|
|
350
367
|
source: w.source,
|
|
351
368
|
unavailableReason: w.unavailableReason,
|
|
352
369
|
evidenceRef: w.evidenceRef,
|
|
370
|
+
criticalRole: w.criticalRole,
|
|
353
371
|
})));
|
|
354
372
|
return fresh.length;
|
|
355
373
|
}
|
|
@@ -67,6 +67,8 @@ test("five-hour and seven-day windows persist independently", () => {
|
|
|
67
67
|
assert.equal(fiveHour.remainingPercent, 83);
|
|
68
68
|
assert.equal(sevenDay.remainingPercent, 58);
|
|
69
69
|
assert.notEqual(fiveHour.resetAt, sevenDay.resetAt);
|
|
70
|
+
assert.equal(fiveHour.criticalRole, "five_hour");
|
|
71
|
+
assert.equal(sevenDay.criticalRole, "weekly");
|
|
70
72
|
// A later partial observation (five_hour only) upserts its row and leaves
|
|
71
73
|
// the seven-day sibling untouched.
|
|
72
74
|
const partial = [
|
|
@@ -94,6 +96,11 @@ test("model-specific and overage windows remain distinguishable", () => {
|
|
|
94
96
|
assert.equal(overage.durationMinutes, 10080);
|
|
95
97
|
assert.equal(sonnet.remainingPercent, 39);
|
|
96
98
|
assert.equal(overage.remainingPercent, 50);
|
|
99
|
+
// Same 10,080-minute duration as the real account-wide weekly window, but
|
|
100
|
+
// neither model-specific/overage extra is ever the critical window.
|
|
101
|
+
assert.equal(sonnet.criticalRole, null);
|
|
102
|
+
assert.equal(overage.criticalRole, null);
|
|
103
|
+
assert.equal(plain.criticalRole, "weekly");
|
|
97
104
|
});
|
|
98
105
|
test("legacy rejected fixture still caps (NOT-111) and fabricates no capacity", () => {
|
|
99
106
|
const events = parseNdjson(legacyRejectedFixture);
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
//
|
|
3
3
|
// NOT-246: Codex capacity adapter over the official App Server interface
|
|
4
4
|
// (https://developers.openai.com/codex/app-server).
|
|
5
|
+
// NOT-263: semantic deduplication of the aggregate `rateLimits` pair against
|
|
6
|
+
// `rateLimitsByLimitId` buckets, plus persisted-row reconciliation.
|
|
5
7
|
//
|
|
6
8
|
// Read-only account operation: the client performs the initialization
|
|
7
9
|
// handshake, then a single `account/rateLimits/read`, and optionally records
|
|
@@ -16,6 +18,12 @@
|
|
|
16
18
|
// Parsing is tolerant (camelCase/snake_case, epoch-second/ms or ISO resets)
|
|
17
19
|
// because only the canonical names above are contractual.
|
|
18
20
|
//
|
|
21
|
+
// When the aggregate primary/secondary pair exactly mirrors one detailed
|
|
22
|
+
// bucket's pair (same used scale, duration, and reset — never the display
|
|
23
|
+
// label alone), the aggregate aliases collapse and the detailed identity
|
|
24
|
+
// wins, so each logical window renders once. Genuinely distinct buckets stay
|
|
25
|
+
// visible even when they share a duration.
|
|
26
|
+
//
|
|
19
27
|
// Failure semantics (shared CapacityUnavailableReason enum only):
|
|
20
28
|
// - App Server unavailable / unauthenticated / timeout -> `missing`
|
|
21
29
|
// (supported surface, no snapshot recorded). The exact cause stays
|
|
@@ -141,8 +149,33 @@ function windowReadingFromEntry(entry, identity, observedAt) {
|
|
|
141
149
|
resetAt: normalizeCodexResetsAt(w.resetsAt ?? w.resets_at ?? w.resetAt ?? w.reset_at),
|
|
142
150
|
observedAt,
|
|
143
151
|
source: "supported_protocol",
|
|
152
|
+
criticalRole: identity.criticalRole ?? null,
|
|
144
153
|
};
|
|
145
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* Semantic window equality for dedup: same used scale and value, same
|
|
157
|
+
* duration, same normalized reset. Display labels (`5H`/`1W`) are never
|
|
158
|
+
* compared — two genuinely distinct buckets may share a duration.
|
|
159
|
+
*/
|
|
160
|
+
export function codexWindowValuesEqual(a, b) {
|
|
161
|
+
const aPercent = a.usedPercent ?? null;
|
|
162
|
+
const bPercent = b.usedPercent ?? null;
|
|
163
|
+
if (aPercent !== null || bPercent !== null) {
|
|
164
|
+
if (aPercent === null || bPercent === null || aPercent !== bPercent)
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
const aFraction = a.usedFraction ?? null;
|
|
169
|
+
const bFraction = b.usedFraction ?? null;
|
|
170
|
+
if (aFraction === null || bFraction === null || aFraction !== bFraction)
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
if ((a.durationMinutes ?? null) !== (b.durationMinutes ?? null))
|
|
174
|
+
return false;
|
|
175
|
+
if ((a.resetAt ?? null) !== (b.resetAt ?? null))
|
|
176
|
+
return false;
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
146
179
|
function nonEmptyString(value) {
|
|
147
180
|
return typeof value === "string" && value.length > 0 ? value : null;
|
|
148
181
|
}
|
|
@@ -162,20 +195,34 @@ function nonEmptyString(value) {
|
|
|
162
195
|
* and whose label is the bucket's `limitName`. A bucket without nested
|
|
163
196
|
* windows still parses as one legacy flat window so older servers keep
|
|
164
197
|
* working.
|
|
198
|
+
*
|
|
199
|
+
* Deduplication (NOT-263): when the aggregate `primary`/`secondary` pair
|
|
200
|
+
* exactly mirrors one detailed bucket's pair (same used scale, duration,
|
|
201
|
+
* and reset), the aggregate aliases collapse and the detailed identity wins
|
|
202
|
+
* — each logical window is reported once. The comparison is semantic, never
|
|
203
|
+
* label-based, so genuinely distinct buckets sharing a duration stay
|
|
204
|
+
* visible; a partial overlap (only one sub-window matches) never collapses.
|
|
165
205
|
*/
|
|
166
206
|
export function codexRateLimitsToReadings(payload, observedAt = new Date().toISOString()) {
|
|
167
207
|
if (!payload || typeof payload !== "object")
|
|
168
208
|
return null;
|
|
169
209
|
const p = payload;
|
|
170
|
-
const
|
|
210
|
+
const aggregate = [];
|
|
171
211
|
const primary = p.rateLimits;
|
|
172
212
|
if (primary && typeof primary === "object") {
|
|
173
213
|
for (const [name, entry] of Object.entries(primary)) {
|
|
174
|
-
|
|
214
|
+
// The `rateLimits` aggregate is, by construction, the account-wide
|
|
215
|
+
// summary — its `primary`/`secondary` entries are the critical 5H/1W
|
|
216
|
+
// pair whenever present; any other key the provider might add here is
|
|
217
|
+
// not part of that identity.
|
|
218
|
+
const criticalRole = name === "primary" ? "five_hour" : name === "secondary" ? "weekly" : null;
|
|
219
|
+
const reading = windowReadingFromEntry(entry, { windowKey: `codex_rate_limit_${sanitizeKeySegment(name)}`, providerBucket: name, criticalRole }, observedAt);
|
|
175
220
|
if (reading)
|
|
176
|
-
|
|
221
|
+
aggregate.push(reading);
|
|
177
222
|
}
|
|
178
223
|
}
|
|
224
|
+
const buckets = new Map();
|
|
225
|
+
const flat = [];
|
|
179
226
|
const byId = p.rateLimitsByLimitId ?? p.rate_limits_by_limit_id;
|
|
180
227
|
if (byId && typeof byId === "object") {
|
|
181
228
|
for (const [mapKey, bucket] of Object.entries(byId)) {
|
|
@@ -185,6 +232,7 @@ export function codexRateLimitsToReadings(payload, observedAt = new Date().toISO
|
|
|
185
232
|
const limitId = nonEmptyString(b.limitId) ?? nonEmptyString(b.limit_id) ?? mapKey;
|
|
186
233
|
const limitName = nonEmptyString(b.limitName) ?? nonEmptyString(b.limit_name) ?? nonEmptyString(b.name);
|
|
187
234
|
let nested = false;
|
|
235
|
+
const pair = {};
|
|
188
236
|
for (const sub of ["primary", "secondary"]) {
|
|
189
237
|
const entry = b[sub];
|
|
190
238
|
if (!entry || typeof entry !== "object")
|
|
@@ -196,20 +244,47 @@ export function codexRateLimitsToReadings(payload, observedAt = new Date().toISO
|
|
|
196
244
|
labelOverride: limitName,
|
|
197
245
|
}, observedAt);
|
|
198
246
|
if (reading)
|
|
199
|
-
|
|
247
|
+
pair[sub] = reading;
|
|
200
248
|
}
|
|
201
|
-
if (
|
|
249
|
+
if (nested) {
|
|
250
|
+
buckets.set(limitId, pair);
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
202
253
|
const reading = windowReadingFromEntry(bucket, {
|
|
203
254
|
windowKey: `codex_limit_${sanitizeKeySegment(limitId)}`,
|
|
204
255
|
providerBucket: limitId,
|
|
205
256
|
labelOverride: limitName,
|
|
206
257
|
}, observedAt);
|
|
207
258
|
if (reading)
|
|
208
|
-
|
|
259
|
+
flat.push(reading);
|
|
209
260
|
}
|
|
210
261
|
}
|
|
211
262
|
}
|
|
212
|
-
|
|
263
|
+
const collapsedAggregateKeys = [];
|
|
264
|
+
const aggPrimary = aggregate.find((w) => w.windowKey === "codex_rate_limit_primary");
|
|
265
|
+
const aggSecondary = aggregate.find((w) => w.windowKey === "codex_rate_limit_secondary");
|
|
266
|
+
if (aggPrimary && aggSecondary) {
|
|
267
|
+
for (const pair of buckets.values()) {
|
|
268
|
+
if (pair.primary &&
|
|
269
|
+
pair.secondary &&
|
|
270
|
+
codexWindowValuesEqual(aggPrimary, pair.primary) &&
|
|
271
|
+
codexWindowValuesEqual(aggSecondary, pair.secondary)) {
|
|
272
|
+
collapsedAggregateKeys.push(aggPrimary.windowKey, aggSecondary.windowKey);
|
|
273
|
+
// The surviving detailed pair now stands in for the aggregate it
|
|
274
|
+
// absorbed — it IS the account-wide identity, so it inherits the
|
|
275
|
+
// aggregate's criticalRole rather than reporting as non-critical.
|
|
276
|
+
pair.primary = { ...pair.primary, criticalRole: "five_hour" };
|
|
277
|
+
pair.secondary = { ...pair.secondary, criticalRole: "weekly" };
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
const out = [
|
|
283
|
+
...aggregate.filter((w) => !collapsedAggregateKeys.includes(w.windowKey)),
|
|
284
|
+
...[...buckets.values()].flatMap((pair) => [pair.primary, pair.secondary].filter((w) => w !== undefined)),
|
|
285
|
+
...flat,
|
|
286
|
+
];
|
|
287
|
+
return out.length > 0 ? { windows: out, collapsedAggregateKeys } : null;
|
|
213
288
|
}
|
|
214
289
|
export function codexCapacityTimeoutMs() {
|
|
215
290
|
const raw = process.env.AGENT_DEALER_CODEX_CAPACITY_TIMEOUT_MS;
|
|
@@ -434,7 +509,7 @@ function unavailableResult(nowMs, reason) {
|
|
|
434
509
|
reason,
|
|
435
510
|
observedAt: new Date(nowMs).toISOString(),
|
|
436
511
|
};
|
|
437
|
-
return { runtime: CODEX_RUNTIME, windows: [], unavailable: [reading] };
|
|
512
|
+
return { runtime: CODEX_RUNTIME, windows: [], unavailable: [reading], collapsedAggregateKeys: [] };
|
|
438
513
|
}
|
|
439
514
|
/**
|
|
440
515
|
* Operator-safe failure log: the static kind only, never server text, tokens,
|
|
@@ -516,7 +591,12 @@ export async function readCodexAppServerCapacity(opts = {}) {
|
|
|
516
591
|
logFailure("malformed");
|
|
517
592
|
return unavailableResult(nowMs, "unparsable");
|
|
518
593
|
}
|
|
519
|
-
return {
|
|
594
|
+
return {
|
|
595
|
+
runtime: CODEX_RUNTIME,
|
|
596
|
+
windows,
|
|
597
|
+
unavailable,
|
|
598
|
+
collapsedAggregateKeys: parsed.collapsedAggregateKeys,
|
|
599
|
+
};
|
|
520
600
|
}
|
|
521
601
|
/** Live Codex adapter for the shared capacity service (runtime `codex_local`). */
|
|
522
602
|
export function createCodexAppServerAdapter(opts = {}) {
|
|
@@ -530,17 +610,27 @@ export function createCodexAppServerAdapter(opts = {}) {
|
|
|
530
610
|
* Bounded refresh: run the live adapter and ingest into normalized snapshots.
|
|
531
611
|
* Reuses the shared ingest path; failures persist as N/A windows, never as
|
|
532
612
|
* health rows. Imported lazily to keep the adapter module free of DB binds.
|
|
613
|
+
*
|
|
614
|
+
* A successful read deletes the failure sentinel plus any aggregate alias
|
|
615
|
+
* keys the fresh payload collapsed (NOT-263): per-window upserts never
|
|
616
|
+
* delete siblings, so without this the obsolete `codex_rate_limit_*`
|
|
617
|
+
* duplicates from older versions would linger next to the surviving
|
|
618
|
+
* detailed rows indefinitely.
|
|
533
619
|
*/
|
|
534
620
|
export async function refreshCodexCapacityFromAppServer(opts = {}) {
|
|
535
621
|
const { ingestAdapterResult, getRuntimeCapacitySnapshot } = await import("./service.js");
|
|
536
622
|
const nowMs = opts.nowMs ?? Date.now();
|
|
537
623
|
const result = await readCodexAppServerCapacity({ ...opts, nowMs });
|
|
538
624
|
await ingestAdapterResult(result);
|
|
625
|
+
const obsolete = [...result.collapsedAggregateKeys];
|
|
539
626
|
if (result.windows.length > 0) {
|
|
540
627
|
// A successful read supersedes the failure sentinel: without this the
|
|
541
628
|
// N/A row lingers next to fresh windows until its own TTL expires.
|
|
629
|
+
obsolete.push(SENTINEL_WINDOW_KEY);
|
|
630
|
+
}
|
|
631
|
+
if (obsolete.length > 0) {
|
|
542
632
|
const { deleteCapacitySnapshots } = await import("../repository/runtime-capacity.js");
|
|
543
|
-
deleteCapacitySnapshots(CODEX_RUNTIME,
|
|
633
|
+
deleteCapacitySnapshots(CODEX_RUNTIME, obsolete);
|
|
544
634
|
}
|
|
545
635
|
return getRuntimeCapacitySnapshot(nowMs);
|
|
546
636
|
}
|
|
@@ -6,16 +6,19 @@
|
|
|
6
6
|
// `rateLimitsByLimitId` buckets stay distinguishable, failure modes map to
|
|
7
7
|
// explicit N/A reasons without touching runtime health, and the recorded
|
|
8
8
|
// protocol traffic proves the client never starts a model turn.
|
|
9
|
+
// NOT-263: an aggregate pair mirroring one detailed bucket collapses to the
|
|
10
|
+
// detailed identity (one 5H + one weekly window); distinct buckets stay
|
|
11
|
+
// visible and refreshes delete obsolete duplicate rows.
|
|
9
12
|
import { test, before } from "node:test";
|
|
10
13
|
import assert from "node:assert/strict";
|
|
11
14
|
import fs from "node:fs";
|
|
12
15
|
import os from "node:os";
|
|
13
16
|
import path from "node:path";
|
|
14
17
|
process.env.AGENT_DEALER_HOME = fs.mkdtempSync(path.join(os.tmpdir(), "dealer-codex-cap-"));
|
|
15
|
-
import { assertReadOnlyMethod, CODEX_CLIENT_VERSION, codexRateLimitsToReadings, createCodexAppServerAdapter, normalizeCodexResetsAt, readCodexAppServerCapacity, readCodexRateLimits, refreshCodexCapacityFromAppServer, refreshCodexCapacityIfStale, } from "./codex-app-server.js";
|
|
18
|
+
import { assertReadOnlyMethod, CODEX_CLIENT_VERSION, codexRateLimitsToReadings, codexWindowValuesEqual, createCodexAppServerAdapter, normalizeCodexResetsAt, readCodexAppServerCapacity, readCodexRateLimits, refreshCodexCapacityFromAppServer, refreshCodexCapacityIfStale, } from "./codex-app-server.js";
|
|
16
19
|
import { normalizeAdapterWindow } from "./adapter.js";
|
|
17
20
|
const { migrate } = await import("../db/index.js");
|
|
18
|
-
const { clearAllCapacitySnapshots, listCapacitySnapshots } = await import("../repository/runtime-capacity.js");
|
|
21
|
+
const { clearAllCapacitySnapshots, listCapacitySnapshots, recordCapacitySnapshots } = await import("../repository/runtime-capacity.js");
|
|
19
22
|
const { createAgent } = await import("../repository/agents.js");
|
|
20
23
|
const { clearAllRuntimeAvailability, runtimeAvailability, } = await import("../repository/runtime-availability.js");
|
|
21
24
|
const { refreshCapacityFromAdapters } = await import("./service.js");
|
|
@@ -55,10 +58,13 @@ test("rateLimits windows keep identity, percent, duration, and reset", () => {
|
|
|
55
58
|
assert.equal(primary.durationMinutes, 300);
|
|
56
59
|
assert.equal(primary.usedPercent, 40);
|
|
57
60
|
assert.equal(primary.resetAt, new Date((Math.floor(now / 1000) + 7200) * 1000).toISOString());
|
|
61
|
+
assert.equal(primary.criticalRole, "five_hour", "the rateLimits aggregate primary is always the critical 5H window");
|
|
58
62
|
const normalized = normalizeAdapterWindow("codex_local", primary, now);
|
|
59
63
|
assert.equal(normalized.displayLabel, "5H");
|
|
60
64
|
assert.equal(normalized.remainingPercent, 60);
|
|
65
|
+
assert.equal(normalized.criticalRole, "five_hour");
|
|
61
66
|
const secondary = parsed.windows.find((w) => w.providerBucket === "secondary");
|
|
67
|
+
assert.equal(secondary.criticalRole, "weekly", "the rateLimits aggregate secondary is always the critical 1W window");
|
|
62
68
|
assert.equal(normalizeAdapterWindow("codex_local", secondary, now).displayLabel, "1W");
|
|
63
69
|
assert.equal(normalizeAdapterWindow("codex_local", secondary, now).remainingPercent, 87.5);
|
|
64
70
|
});
|
|
@@ -82,6 +88,7 @@ test("rateLimitsByLimitId buckets never overwrite each other or rateLimits", ()
|
|
|
82
88
|
});
|
|
83
89
|
assert.ok(parsed);
|
|
84
90
|
assert.equal(parsed.windows.length, 5);
|
|
91
|
+
assert.deepEqual(parsed.collapsedAggregateKeys, []);
|
|
85
92
|
const keys = parsed.windows.map((w) => w.windowKey);
|
|
86
93
|
assert.equal(new Set(keys).size, 5);
|
|
87
94
|
assert.ok(keys.includes("codex_rate_limit_primary"));
|
|
@@ -93,6 +100,11 @@ test("rateLimitsByLimitId buckets never overwrite each other or rateLimits", ()
|
|
|
93
100
|
assert.equal(aPrimary.providerBucket, "team_a/primary");
|
|
94
101
|
assert.equal(aPrimary.providerLabel, "Team A");
|
|
95
102
|
assert.equal(aPrimary.usedPercent, 70);
|
|
103
|
+
// No secondary aggregate half at all: no collapse comparison runs, so the
|
|
104
|
+
// detailed team buckets are never promoted to critical.
|
|
105
|
+
assert.equal(aPrimary.criticalRole, null);
|
|
106
|
+
assert.equal(parsed.windows.find((w) => w.windowKey === "codex_limit_team_b_primary").criticalRole, null);
|
|
107
|
+
assert.equal(parsed.windows.find((w) => w.windowKey === "codex_rate_limit_primary").criticalRole, "five_hour");
|
|
96
108
|
});
|
|
97
109
|
test("nested bucket windows keep per-limit identity when limitId is only the map key", () => {
|
|
98
110
|
const parsed = codexRateLimitsToReadings({
|
|
@@ -118,6 +130,127 @@ test("flat legacy buckets without nested windows still parse", () => {
|
|
|
118
130
|
assert.equal(parsed.windows[0].windowKey, "codex_limit_flat_bucket");
|
|
119
131
|
assert.equal(parsed.windows[0].providerBucket, "flat_bucket");
|
|
120
132
|
});
|
|
133
|
+
test("aggregate pair mirroring one bucket collapses to the detailed identity", () => {
|
|
134
|
+
const now = Date.now();
|
|
135
|
+
const observedAt = new Date(now).toISOString();
|
|
136
|
+
const primary = { usedPercent: 9, windowDurationMins: 300, resetsAt: now + 2 * 3600_000 };
|
|
137
|
+
const secondary = { usedPercent: 2, windowDurationMins: 10080, resetsAt: now + 3 * 86400_000 };
|
|
138
|
+
const parsed = codexRateLimitsToReadings({
|
|
139
|
+
rateLimits: {
|
|
140
|
+
primary: { ...primary },
|
|
141
|
+
secondary: { ...secondary },
|
|
142
|
+
},
|
|
143
|
+
rateLimitsByLimitId: {
|
|
144
|
+
codex: {
|
|
145
|
+
limitId: "codex",
|
|
146
|
+
limitName: "Codex quota",
|
|
147
|
+
primary: { ...primary },
|
|
148
|
+
secondary: { ...secondary },
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
}, observedAt);
|
|
152
|
+
assert.ok(parsed);
|
|
153
|
+
assert.deepEqual(parsed.windows.map((w) => w.windowKey).sort(), ["codex_limit_codex_primary", "codex_limit_codex_secondary"]);
|
|
154
|
+
assert.deepEqual(parsed.collapsedAggregateKeys, [
|
|
155
|
+
"codex_rate_limit_primary",
|
|
156
|
+
"codex_rate_limit_secondary",
|
|
157
|
+
]);
|
|
158
|
+
const labels = parsed.windows.map((w) => normalizeAdapterWindow("codex_local", w, now));
|
|
159
|
+
assert.deepEqual(labels.map((w) => w.displayLabel).sort(), ["1W", "5H"]);
|
|
160
|
+
assert.deepEqual(labels.map((w) => w.remainingPercent).sort((a, b) => (a ?? 0) - (b ?? 0)), [91, 98]);
|
|
161
|
+
// The surviving detailed pair inherits the collapsed aggregate's identity
|
|
162
|
+
// rather than reporting as an ordinary non-critical bucket — this is what
|
|
163
|
+
// lets the client find it without re-deriving "which limit id is real".
|
|
164
|
+
const primaryOut = parsed.windows.find((w) => w.windowKey === "codex_limit_codex_primary");
|
|
165
|
+
const secondaryOut = parsed.windows.find((w) => w.windowKey === "codex_limit_codex_secondary");
|
|
166
|
+
assert.equal(primaryOut.criticalRole, "five_hour");
|
|
167
|
+
assert.equal(secondaryOut.criticalRole, "weekly");
|
|
168
|
+
});
|
|
169
|
+
test("NOT-264 finding: a second, distinct detailed pair alongside the collapsed one is never tagged critical", () => {
|
|
170
|
+
// The realistic Codex shape a PR review caught: the aggregate collapses
|
|
171
|
+
// into "main" (identical values) while an unrelated "extra" pair also
|
|
172
|
+
// exists. The client can no longer tell these apart by name, but the
|
|
173
|
+
// server can, by value equality against the aggregate before it is
|
|
174
|
+
// discarded — so exactly the true survivor gets tagged, never both, never
|
|
175
|
+
// neither.
|
|
176
|
+
const now = Date.now();
|
|
177
|
+
const primary = { usedPercent: 9, windowDurationMins: 300, resetsAt: now + 2 * 3600_000 };
|
|
178
|
+
const secondary = { usedPercent: 2, windowDurationMins: 10080, resetsAt: now + 3 * 86400_000 };
|
|
179
|
+
const parsed = codexRateLimitsToReadings({
|
|
180
|
+
rateLimits: { primary: { ...primary }, secondary: { ...secondary } },
|
|
181
|
+
rateLimitsByLimitId: {
|
|
182
|
+
main: { limitId: "main", primary: { ...primary }, secondary: { ...secondary } },
|
|
183
|
+
extra: {
|
|
184
|
+
limitId: "extra",
|
|
185
|
+
primary: { usedPercent: 70, windowDurationMins: 300, resetsAt: now + 3600_000 },
|
|
186
|
+
secondary: { usedPercent: 55, windowDurationMins: 10080, resetsAt: now + 4 * 86400_000 },
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
}, new Date(now).toISOString());
|
|
190
|
+
assert.ok(parsed);
|
|
191
|
+
assert.deepEqual(parsed.collapsedAggregateKeys, ["codex_rate_limit_primary", "codex_rate_limit_secondary"]);
|
|
192
|
+
const byKey = new Map(parsed.windows.map((w) => [w.windowKey, w]));
|
|
193
|
+
assert.equal(byKey.get("codex_limit_main_primary").criticalRole, "five_hour");
|
|
194
|
+
assert.equal(byKey.get("codex_limit_main_secondary").criticalRole, "weekly");
|
|
195
|
+
assert.equal(byKey.get("codex_limit_extra_primary").criticalRole, null);
|
|
196
|
+
assert.equal(byKey.get("codex_limit_extra_secondary").criticalRole, null);
|
|
197
|
+
});
|
|
198
|
+
test("partial overlap never collapses: only one sub-window matches", () => {
|
|
199
|
+
const now = Date.now();
|
|
200
|
+
const parsed = codexRateLimitsToReadings({
|
|
201
|
+
rateLimits: {
|
|
202
|
+
primary: { usedPercent: 70, windowDurationMins: 300, resetsAt: now + 3600_000 },
|
|
203
|
+
secondary: { usedPercent: 12, windowDurationMins: 10080, resetsAt: now + 3 * 86400_000 },
|
|
204
|
+
},
|
|
205
|
+
rateLimitsByLimitId: {
|
|
206
|
+
main: {
|
|
207
|
+
limitId: "main",
|
|
208
|
+
primary: { usedPercent: 70, windowDurationMins: 300, resetsAt: now + 3600_000 },
|
|
209
|
+
secondary: { usedPercent: 5, windowDurationMins: 10080, resetsAt: now + 6 * 86400_000 },
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
}, new Date(now).toISOString());
|
|
213
|
+
assert.ok(parsed);
|
|
214
|
+
assert.equal(parsed.windows.length, 4);
|
|
215
|
+
assert.deepEqual(parsed.collapsedAggregateKeys, []);
|
|
216
|
+
});
|
|
217
|
+
test("distinct buckets sharing a duration stay independently visible", () => {
|
|
218
|
+
const now = Date.now();
|
|
219
|
+
const parsed = codexRateLimitsToReadings({
|
|
220
|
+
rateLimitsByLimitId: {
|
|
221
|
+
team_a: {
|
|
222
|
+
limitId: "team_a",
|
|
223
|
+
primary: { usedPercent: 70, windowDurationMins: 300, resetsAt: now + 3600_000 },
|
|
224
|
+
},
|
|
225
|
+
team_b: {
|
|
226
|
+
limitId: "team_b",
|
|
227
|
+
primary: { usedPercent: 90, windowDurationMins: 300, resetsAt: now + 1800_000 },
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
}, new Date(now).toISOString());
|
|
231
|
+
assert.ok(parsed);
|
|
232
|
+
assert.equal(parsed.windows.length, 2);
|
|
233
|
+
const labels = parsed.windows.map((w) => normalizeAdapterWindow("codex_local", w, now));
|
|
234
|
+
assert.ok(labels.every((w) => w.displayLabel === "5H"), "same label, both still visible");
|
|
235
|
+
assert.deepEqual(parsed.collapsedAggregateKeys, []);
|
|
236
|
+
// No rateLimits aggregate at all here, so neither same-duration bucket is
|
|
237
|
+
// ever tagged critical — an honest "we don't know" rather than a guess.
|
|
238
|
+
assert.ok(parsed.windows.every((w) => w.criticalRole === null));
|
|
239
|
+
});
|
|
240
|
+
test("window equality compares values, never display labels", () => {
|
|
241
|
+
const base = {
|
|
242
|
+
windowKey: "k",
|
|
243
|
+
providerBucket: "b",
|
|
244
|
+
providerLabel: "l",
|
|
245
|
+
source: "supported_protocol",
|
|
246
|
+
};
|
|
247
|
+
const a = { ...base, usedPercent: 10, durationMinutes: 300, resetAt: "2026-09-20T12:00:00.000Z" };
|
|
248
|
+
assert.ok(codexWindowValuesEqual(a, { ...a, windowKey: "other", providerLabel: "5H" }));
|
|
249
|
+
assert.ok(!codexWindowValuesEqual(a, { ...a, usedPercent: 11 }));
|
|
250
|
+
assert.ok(!codexWindowValuesEqual(a, { ...a, durationMinutes: 10080 }));
|
|
251
|
+
assert.ok(!codexWindowValuesEqual(a, { ...a, resetAt: "2026-09-21T12:00:00.000Z" }));
|
|
252
|
+
assert.ok(!codexWindowValuesEqual(a, { ...a, usedPercent: undefined, usedFraction: 0.1 }));
|
|
253
|
+
});
|
|
121
254
|
test("resetsAt accepts epoch seconds, ms, and ISO; rejects garbage", () => {
|
|
122
255
|
const iso = "2026-09-20T12:00:00.000Z";
|
|
123
256
|
const epochSec = Math.floor(Date.parse(iso) / 1000);
|
|
@@ -186,6 +319,66 @@ test("initialize handshake carries a versioned client identity", async () => {
|
|
|
186
319
|
assert.equal(init.params?.clientInfo?.name, "agent-dealer");
|
|
187
320
|
assert.equal(init.params?.clientInfo?.version, CODEX_CLIENT_VERSION);
|
|
188
321
|
});
|
|
322
|
+
test("mirrored aggregate pair collapses end to end: one 5H, one weekly, distinct bucket kept", async () => {
|
|
323
|
+
clearAllCapacitySnapshots();
|
|
324
|
+
const { opts } = fakeOpts("mirror");
|
|
325
|
+
const result = await readCodexAppServerCapacity(opts);
|
|
326
|
+
assert.deepEqual(result.collapsedAggregateKeys, [
|
|
327
|
+
"codex_rate_limit_primary",
|
|
328
|
+
"codex_rate_limit_secondary",
|
|
329
|
+
]);
|
|
330
|
+
const snap = await refreshCodexCapacityFromAppServer(opts);
|
|
331
|
+
const codex = snap.runtimes.find((r) => r.runtime === "codex_local");
|
|
332
|
+
assert.ok(codex, "codex_local entry exists");
|
|
333
|
+
assert.equal(codex.unavailableReason, null);
|
|
334
|
+
const keys = codex.windows.map((w) => w.windowKey).sort();
|
|
335
|
+
assert.deepEqual(keys, [
|
|
336
|
+
"codex_limit_extra_primary",
|
|
337
|
+
"codex_limit_extra_secondary",
|
|
338
|
+
"codex_limit_main_primary",
|
|
339
|
+
"codex_limit_main_secondary",
|
|
340
|
+
]);
|
|
341
|
+
const mainPrimary = codex.windows.find((w) => w.windowKey === "codex_limit_main_primary");
|
|
342
|
+
assert.equal(mainPrimary.displayLabel, "5H");
|
|
343
|
+
assert.equal(mainPrimary.remainingPercent, 30);
|
|
344
|
+
const mainSecondary = codex.windows.find((w) => w.windowKey === "codex_limit_main_secondary");
|
|
345
|
+
assert.equal(mainSecondary.displayLabel, "1W");
|
|
346
|
+
assert.equal(mainSecondary.remainingPercent, 95);
|
|
347
|
+
});
|
|
348
|
+
test("refresh deletes obsolete duplicate aliases persisted by older versions", async () => {
|
|
349
|
+
clearAllCapacitySnapshots();
|
|
350
|
+
const seedNow = Date.now();
|
|
351
|
+
const seedObserved = new Date(seedNow).toISOString();
|
|
352
|
+
// Legacy shape: aggregate aliases plus the detailed rows describing the
|
|
353
|
+
// same logical pair (what pre-NOT-263 versions persisted).
|
|
354
|
+
function seedReading(windowKey, providerBucket, durationMinutes) {
|
|
355
|
+
return normalizeAdapterWindow("codex_local", {
|
|
356
|
+
windowKey,
|
|
357
|
+
providerBucket,
|
|
358
|
+
durationMinutes,
|
|
359
|
+
providerLabel: providerBucket,
|
|
360
|
+
usedValue: 9,
|
|
361
|
+
usedUnit: "percent",
|
|
362
|
+
usedPercent: 9,
|
|
363
|
+
resetAt: new Date(seedNow + 2 * 3600_000).toISOString(),
|
|
364
|
+
observedAt: seedObserved,
|
|
365
|
+
source: "supported_protocol",
|
|
366
|
+
}, seedNow);
|
|
367
|
+
}
|
|
368
|
+
recordCapacitySnapshots("codex_local", [
|
|
369
|
+
seedReading("codex_rate_limit_primary", "primary", 300),
|
|
370
|
+
seedReading("codex_rate_limit_secondary", "secondary", 10080),
|
|
371
|
+
seedReading("codex_limit_main_primary", "main/primary", 300),
|
|
372
|
+
seedReading("codex_limit_main_secondary", "main/secondary", 10080),
|
|
373
|
+
]);
|
|
374
|
+
assert.equal(listCapacitySnapshots("codex_local").length, 4);
|
|
375
|
+
const { opts } = fakeOpts("mirror");
|
|
376
|
+
await refreshCodexCapacityFromAppServer(opts);
|
|
377
|
+
const keys = listCapacitySnapshots("codex_local").map((w) => w.windowKey);
|
|
378
|
+
assert.ok(!keys.some((k) => k.startsWith("codex_rate_limit_")), `obsolete aliases deleted, got ${keys}`);
|
|
379
|
+
assert.ok(keys.includes("codex_limit_main_primary"), `detailed identity kept, got ${keys}`);
|
|
380
|
+
assert.ok(keys.includes("codex_limit_main_secondary"), `detailed identity kept, got ${keys}`);
|
|
381
|
+
});
|
|
189
382
|
test("a successful refresh clears a previously stored failure sentinel", async () => {
|
|
190
383
|
clearAllCapacitySnapshots();
|
|
191
384
|
const crashed = fakeOpts("crash");
|