agent-dealer 1.2.1 → 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.
Files changed (43) hide show
  1. package/bundle/server/dist/adapters/linear-inbox.js +9 -5
  2. package/bundle/server/dist/capacity/adapter.js +2 -0
  3. package/bundle/server/dist/capacity/claude-events.js +18 -0
  4. package/bundle/server/dist/capacity/claude-events.test.js +7 -0
  5. package/bundle/server/dist/capacity/codex-app-server.js +100 -10
  6. package/bundle/server/dist/capacity/codex-app-server.test.js +195 -2
  7. package/bundle/server/dist/capacity/cursor-individual-credentials.js +238 -0
  8. package/bundle/server/dist/capacity/cursor-individual-credentials.test.js +185 -0
  9. package/bundle/server/dist/capacity/cursor-individual.js +970 -0
  10. package/bundle/server/dist/capacity/cursor-individual.test.js +815 -0
  11. package/bundle/server/dist/capacity/muse.js +94 -43
  12. package/bundle/server/dist/capacity/muse.test.js +95 -13
  13. package/bundle/server/dist/capacity/service.js +1 -0
  14. package/bundle/server/dist/db/index.js +30 -0
  15. package/bundle/server/dist/db/schema.sql +27 -0
  16. package/bundle/server/dist/index.js +2 -0
  17. package/bundle/server/dist/repository/cursor-individual-billing.js +84 -0
  18. package/bundle/server/dist/repository/repository-mappings.js +46 -0
  19. package/bundle/server/dist/repository/repository-mappings.test.js +83 -0
  20. package/bundle/server/dist/repository/runtime-capacity.js +8 -3
  21. package/bundle/server/dist/repository/runtime-capacity.test.js +4 -0
  22. package/bundle/server/dist/routes/cursor-individual-billing.test.js +80 -0
  23. package/bundle/server/dist/routes/index.js +32 -0
  24. package/bundle/server/dist/routes/intake-linear.test.js +55 -1
  25. package/bundle/server/dist/routes/repository-mappings.js +22 -0
  26. package/bundle/server/dist/routes/repository-mappings.test.js +110 -0
  27. package/bundle/server/dist/routes/runtime-capacity.test.js +72 -2
  28. package/bundle/server/package.json +2 -2
  29. package/bundle/server/static-ui/assets/index-0GDYfbaM.js +60 -0
  30. package/bundle/server/static-ui/assets/index-DUYzboWh.css +1 -0
  31. package/bundle/server/static-ui/index.html +2 -2
  32. package/bundle/shared/dist/index.d.ts +6 -6
  33. package/bundle/shared/dist/linear-intake.d.ts +71 -0
  34. package/bundle/shared/dist/linear-intake.js +90 -0
  35. package/bundle/shared/dist/linear-intake.test.js +80 -1
  36. package/bundle/shared/dist/runtime-capacity.d.ts +89 -0
  37. package/bundle/shared/dist/runtime-capacity.js +60 -0
  38. package/bundle/shared/dist/runtime-capacity.test.js +1 -0
  39. package/bundle/shared/package.json +1 -1
  40. package/dist/doctor.js +6 -0
  41. package/package.json +1 -1
  42. package/bundle/server/static-ui/assets/index-ChnI0F02.css +0 -1
  43. package/bundle/server/static-ui/assets/index-CyB1573Q.js +0 -60
@@ -1,5 +1,6 @@
1
- import { resolveLinearRepoLabels } from "@agent-dealer/shared";
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: resolveLinearRepoLabels(labels),
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
- return nodes.map(nodeToCandidate);
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
- return nodeToCandidate(data.issue);
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 out = [];
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
- const reading = windowReadingFromEntry(entry, { windowKey: `codex_rate_limit_${sanitizeKeySegment(name)}`, providerBucket: name }, observedAt);
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
- out.push(reading);
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
- out.push(reading);
247
+ pair[sub] = reading;
200
248
  }
201
- if (!nested) {
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
- out.push(reading);
259
+ flat.push(reading);
209
260
  }
210
261
  }
211
262
  }
212
- return out.length > 0 ? { windows: out } : null;
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 { runtime: CODEX_RUNTIME, windows, unavailable };
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, [SENTINEL_WINDOW_KEY]);
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");