@cortexkit/opencode-openai-auth 1.2.0 → 1.4.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.
Files changed (90) hide show
  1. package/dist/bin.js +8 -8
  2. package/dist/{codex-request-CjXETpTA.d.ts → codex-request-hw0XcldE.d.ts} +146 -29
  3. package/dist/index-1eq2s1cp.js +2 -0
  4. package/dist/{index-qp5hdpw8.js → index-1tdnrt68.js} +1 -1
  5. package/dist/index-5230x6s8.js +2 -0
  6. package/dist/{index-qxbchh2d.js → index-ag04nyv7.js} +7 -7
  7. package/dist/index-brbqqer5.js +90 -0
  8. package/dist/{index-xwvze96g.js → index-d98f1xdp.js} +1 -1
  9. package/dist/index-jcyrm58f.js +65 -0
  10. package/dist/{index-htvcmtcx.js → index-rsvnabbe.js} +2 -2
  11. package/dist/{index-ftkhhr6d.js → index-s88n2bjn.js} +1 -1
  12. package/dist/{index-bc89akak.js → index-t5m0dhmq.js} +1 -1
  13. package/dist/index-vy729a0b.js +69 -0
  14. package/dist/index-wgwze391.js +2 -0
  15. package/dist/{index-ffg0sgg5.js → index-yhw3hnsh.js} +2 -2
  16. package/dist/index.d.ts +11 -3
  17. package/dist/index.js +1 -1
  18. package/dist/{model-CQcw10oW.d.ts → model-DphbMQce.d.ts} +1 -1
  19. package/dist/{port-file-BDJnzgHH.d.ts → port-file-Cx-wwe5Q.d.ts} +1 -1
  20. package/dist/{protocol-C-kagE6O.d.ts → protocol-CDqdjh74.d.ts} +1 -1
  21. package/dist/rpc/port-file.d.ts +1 -1
  22. package/dist/rpc/port-file.js +1 -1
  23. package/dist/rpc/protocol.d.ts +1 -1
  24. package/dist/rpc/protocol.js +1 -1
  25. package/dist/rpc/rpc-client.d.ts +3 -3
  26. package/dist/rpc/rpc-client.js +1 -1
  27. package/dist/rpc/rpc-dir.js +1 -1
  28. package/dist/{sidebar-state-BfQ4mpZO.d.ts → sidebar-state-pTd9gg9W.d.ts} +71 -7
  29. package/dist/sidebar-state.d.ts +2 -2
  30. package/dist/sidebar-state.js +1 -1
  31. package/dist/{token-z6qq7a1d.js → token-0z5g59p8.js} +1 -1
  32. package/dist/token-util-jf9g6stk.js +1 -0
  33. package/dist/tui-preferences.js +1 -1
  34. package/dist/tui.d.ts +30 -3
  35. package/dist/v2/server.d.ts +2 -2
  36. package/dist/v2/server.js +2 -2
  37. package/package.json +6 -6
  38. package/src/tui-compiled/raw/shared/0332701351b3eeec-format.js +119 -6
  39. package/src/tui-compiled/raw/shared/0f77ebc3c914c9c6-custody.js +294 -16
  40. package/src/tui-compiled/raw/shared/169b2b4bf30909bc-consumer.js +178 -26
  41. package/src/tui-compiled/raw/shared/2577ec69f9973ae9-quota-normalize.js +45 -1
  42. package/src/tui-compiled/raw/shared/30f5bc480f31f51c-roster.js +100 -43
  43. package/src/tui-compiled/raw/shared/375d9cca2def037d-private-files.js +10 -2
  44. package/src/tui-compiled/raw/shared/39c3cb05738fcf6d-projection.js +3 -1
  45. package/src/tui-compiled/raw/shared/42ae93ab554fcec2-open-url.js +7 -9
  46. package/src/tui-compiled/raw/shared/4825e03209d6ae3e-with-lock.js +2 -0
  47. package/src/tui-compiled/raw/shared/4f36d333d081d040-merge.js +17 -3
  48. package/src/tui-compiled/raw/shared/81dddd203f1b92a3-quota-writer.js +253 -0
  49. package/src/tui-compiled/raw/shared/905bffc0220f43d0-map.js +31 -6
  50. package/src/tui-compiled/raw/shared/94f53cfd572ba4bb-availability.js +106 -0
  51. package/src/tui-compiled/raw/shared/abd062d633e29fd2-pool-routing.js +116 -12
  52. package/src/tui-compiled/raw/shared/bfbdd0f9aae56b47-pool-quota.js +15 -3
  53. package/src/tui-compiled/raw/shared/d0f0b6be200311af-insula.js +348 -21
  54. package/src/tui-compiled/raw/shared/d5a8f1061ee2f114-index.js +2 -2
  55. package/src/tui-compiled/raw/shared/dbc61cd405941828-accounts.js +5 -4
  56. package/src/tui-compiled/raw/shared/dc9290c2a7552ba6-watch.js +254 -0
  57. package/src/tui-compiled/raw/shared/e8d3c8da776ef9cd-index.js +5 -1
  58. package/src/tui-compiled/raw/sidebar-state.js +32 -3
  59. package/src/tui-compiled/raw/tui/v2-dialog.js +2 -1
  60. package/src/tui-compiled/raw/tui.tsx +170 -26
  61. package/src/tui-compiled/runtime/shared/0332701351b3eeec-format.js +119 -6
  62. package/src/tui-compiled/runtime/shared/0f77ebc3c914c9c6-custody.js +294 -16
  63. package/src/tui-compiled/runtime/shared/169b2b4bf30909bc-consumer.js +178 -26
  64. package/src/tui-compiled/runtime/shared/2577ec69f9973ae9-quota-normalize.js +45 -1
  65. package/src/tui-compiled/runtime/shared/30f5bc480f31f51c-roster.js +100 -43
  66. package/src/tui-compiled/runtime/shared/375d9cca2def037d-private-files.js +10 -2
  67. package/src/tui-compiled/runtime/shared/39c3cb05738fcf6d-projection.js +3 -1
  68. package/src/tui-compiled/runtime/shared/42ae93ab554fcec2-open-url.js +7 -9
  69. package/src/tui-compiled/runtime/shared/4825e03209d6ae3e-with-lock.js +2 -0
  70. package/src/tui-compiled/runtime/shared/4f36d333d081d040-merge.js +17 -3
  71. package/src/tui-compiled/runtime/shared/81dddd203f1b92a3-quota-writer.js +253 -0
  72. package/src/tui-compiled/runtime/shared/905bffc0220f43d0-map.js +31 -6
  73. package/src/tui-compiled/runtime/shared/94f53cfd572ba4bb-availability.js +106 -0
  74. package/src/tui-compiled/runtime/shared/abd062d633e29fd2-pool-routing.js +116 -12
  75. package/src/tui-compiled/runtime/shared/bfbdd0f9aae56b47-pool-quota.js +15 -3
  76. package/src/tui-compiled/runtime/shared/d0f0b6be200311af-insula.js +348 -21
  77. package/src/tui-compiled/runtime/shared/d5a8f1061ee2f114-index.js +2 -2
  78. package/src/tui-compiled/runtime/shared/dbc61cd405941828-accounts.js +5 -4
  79. package/src/tui-compiled/runtime/shared/dc9290c2a7552ba6-watch.js +254 -0
  80. package/src/tui-compiled/runtime/shared/e8d3c8da776ef9cd-index.js +5 -1
  81. package/src/tui-compiled/runtime/sidebar-state.js +32 -3
  82. package/src/tui-compiled/runtime/tui/v2-dialog.js +2 -1
  83. package/src/tui-compiled/runtime/tui.js +350 -219
  84. package/dist/index-03em8hv1.js +0 -69
  85. package/dist/index-6n4pnfkg.js +0 -2
  86. package/dist/index-ehjj89rx.js +0 -2
  87. package/dist/index-f21addzm.js +0 -90
  88. package/dist/index-r2s51ac1.js +0 -2
  89. package/dist/index-rwbrhkse.js +0 -65
  90. package/dist/token-util-cf0nyh7n.js +0 -1
@@ -1,8 +1,13 @@
1
- import { projectQuota } from "./d5a8f1061ee2f114-index.js";
1
+ import {
2
+ budgetExhaustedResetAt,
3
+ isQuotaCredits,
4
+ projectQuota
5
+ } from "./d5a8f1061ee2f114-index.js";
2
6
  import {
3
7
  admit,
4
8
  decideStickyBreak,
5
9
  orderForPlacement,
10
+ QUOTA_STALENESS_MS,
6
11
  routeOrdered,
7
12
  routeSticky
8
13
  } from "./3e7d45a77c6ad4d5-index.js";
@@ -65,6 +70,70 @@ export function waitForFirstQuotaReading(input, waitMs = FIRST_QUOTA_READING_WAI
65
70
  function isConfirmedExhaustion(refusal) {
66
71
  return refusal.reason === "exhausted" || refusal.reason === "budget-spent";
67
72
  }
73
+ export function creditsUsable(credits, now) {
74
+ if (!isQuotaCredits(credits))
75
+ return !1;
76
+ const record = credits;
77
+ if (!(now - record.checkedAt <= QUOTA_STALENESS_MS))
78
+ return !1;
79
+ if (record.overageLimitReached === !0)
80
+ return !1;
81
+ if (record.unlimited)
82
+ return !0;
83
+ return record.hasCredits && (record.balance === void 0 || record.balance > 0);
84
+ }
85
+ function creditsTiers(rows, input, admission) {
86
+ const passes = (id) => input.killswitch.get(id) !== !1, free = new Set(admission.admitted.filter((row) => row.lastPath !== !0 && passes(row.id)).map((row) => row.id)), refusals = new Map(admission.refused.map((refusal) => [refusal.id, refusal])), credits = rows.filter((row) => {
87
+ if (row.kind !== "oauth" || !passes(row.id))
88
+ return !1;
89
+ if (refusals.get(row.id)?.reason !== "exhausted")
90
+ return !1;
91
+ const projection = projectQuota(row.quota);
92
+ if (budgetExhaustedResetAt(projection, input.now))
93
+ return !1;
94
+ return creditsUsable(projection.credits, input.now);
95
+ }).map((row) => row.id);
96
+ return { free, credits };
97
+ }
98
+ function admitQuietly(input, rows = input.rows) {
99
+ return admit({
100
+ rows,
101
+ now: input.now,
102
+ rateLimitMarks: input.rateLimitMarks,
103
+ refreshBackoff: input.refreshBackoff,
104
+ requestPull: () => {}
105
+ });
106
+ }
107
+ export function creditsFallbackIds(input) {
108
+ return [...creditsTiers(input.rows, input, admitQuietly(input)).credits];
109
+ }
110
+
111
+ export class CreditsServing {
112
+ #ids = new Set;
113
+ served(input, id, log) {
114
+ const admission = admitQuietly(input), tiers = creditsTiers(input.rows, input, admission), credits = new Set(tiers.credits);
115
+ if (tiers.free.has(id)) {
116
+ this.#ids.clear();
117
+ return;
118
+ }
119
+ for (const marked of [...this.#ids])
120
+ if (!credits.has(marked))
121
+ this.#ids.delete(marked);
122
+ if (!credits.has(id) || this.#ids.has(id))
123
+ return;
124
+ this.#ids.add(id);
125
+ log?.info("serving requests on prepaid credits", {
126
+ accountId: id,
127
+ reason: "free quota used up"
128
+ });
129
+ }
130
+ has(id) {
131
+ return this.#ids.has(id);
132
+ }
133
+ ids() {
134
+ return new Set(this.#ids);
135
+ }
136
+ }
68
137
  export function blockFor(id, admission, killswitch) {
69
138
  if (id === void 0)
70
139
  return { reason: "no-credential" };
@@ -97,11 +166,13 @@ export function planOrdered(input) {
97
166
  refreshBackoff: input.refreshBackoff,
98
167
  now: input.now,
99
168
  requestPull: input.requestPull
100
- }), ids = orderForPlacement(input.rows.map((row) => row.id), placement, mainId), confirmed = new Set(route.admission.refused.filter((refusal) => isConfirmedExhaustion(refusal) && input.killswitch.get(refusal.id) !== !1).map((refusal) => refusal.id)), lastPath = ids.filter((id) => confirmed.has(id));
169
+ }), ids = orderForPlacement(input.rows.map((row) => row.id), placement, mainId), confirmed = new Set(route.admission.refused.filter((refusal) => isConfirmedExhaustion(refusal) && input.killswitch.get(refusal.id) !== !1).map((refusal) => refusal.id)), lastPath = ids.filter((id) => confirmed.has(id)), tiers = creditsTiers(input.rows, input, route.admission), free = route.order.filter((id) => tiers.free.has(id)), credits = ids.filter((id) => tiers.credits.includes(id));
101
170
  if (!replayable) {
102
- const hasMain = ids.includes(mainId), target = hasMain ? mainId : route.order[0] ?? lastPath[0];
171
+ const hasMain = ids.includes(mainId), target = hasMain ? mainId : free[0] ?? credits[0] ?? route.order[0] ?? lastPath[0];
103
172
  if (target !== void 0 && route.order.includes(target))
104
173
  return { kind: "send", order: [target], lastPath: !1 };
174
+ if (target !== void 0 && credits.includes(target))
175
+ return { kind: "send", order: [target], lastPath: !1 };
105
176
  if (target !== void 0 && confirmed.has(target))
106
177
  return { kind: "send", order: [target], lastPath: !0 };
107
178
  return {
@@ -109,6 +180,16 @@ export function planOrdered(input) {
109
180
  block: hasMain ? blockFor(mainId, route.admission, input.killswitch) : routeBlock(ids, route.admission, input.killswitch)
110
181
  };
111
182
  }
183
+ if (free.length > 0)
184
+ return { kind: "send", order: free, lastPath: !1 };
185
+ if (credits.length > 0) {
186
+ const rest = route.order.length > 0 ? route.order : lastPath;
187
+ return {
188
+ kind: "send",
189
+ order: [...credits, ...rest.filter((id) => !credits.includes(id))],
190
+ lastPath: !1
191
+ };
192
+ }
112
193
  if (route.order.length > 0)
113
194
  return { kind: "send", order: route.order, lastPath: !1 };
114
195
  if (lastPath.length > 0)
@@ -160,23 +241,46 @@ function routePoolSticky(input, options, extra) {
160
241
  });
161
242
  }
162
243
  export function selectStickyRow(input, options, exclude, pendingBytes) {
163
- const route = routePoolSticky(input, options, { exclude, pendingBytes });
164
- if (route.outcome !== "dispatch" || route.source === "pin")
165
- return;
166
- return {
244
+ const route = routePoolSticky(input, options, { exclude, pendingBytes }), rows = input.rows.filter((row) => !exclude.has(row.id)), tiers = creditsTiers(rows, input, route.admission), selected = route.outcome === "dispatch" && route.source !== "pin" ? {
167
245
  accountId: route.accountId,
168
246
  source: route.source,
169
247
  ...route.quotaCheckedAt === void 0 ? {} : { quotaCheckedAt: route.quotaCheckedAt }
248
+ } : void 0;
249
+ if (selected && tiers.free.has(selected.accountId))
250
+ return selected;
251
+ const credits = tiers.credits[0];
252
+ if (credits === void 0)
253
+ return selected;
254
+ const checkedAt = projectQuota(rows.find((row) => row.id === credits)?.quota).checkedAt;
255
+ return {
256
+ accountId: credits,
257
+ source: "mode-fallback",
258
+ ...checkedAt === void 0 ? {} : { quotaCheckedAt: checkedAt }
170
259
  };
171
260
  }
172
261
  export function routePinnedRow(input, options, id) {
173
- const killed = input.killswitch.get(id) === !1, route = routePoolSticky(input, options, { pin: id });
262
+ const killed = input.killswitch.get(id) === !1, move = {
263
+ kind: "move",
264
+ reason: killed ? "killswitch" : "exhausted"
265
+ }, route = routePoolSticky(input, options, { pin: id }), tiers = creditsTiers(input.rows, input, route.admission), onCredits = tiers.credits.includes(id), othersOnCredits = tiers.credits.some((other) => other !== id);
174
266
  if (route.outcome === "dispatch") {
175
- if (route.pin.action === "assign")
176
- return { kind: "move", reason: killed ? "killswitch" : "exhausted" };
177
- return route.accountId === id ? { kind: "serve" } : { kind: "detour" };
267
+ const toLastPath = !tiers.free.has(route.accountId);
268
+ if (route.pin.action === "assign") {
269
+ if (toLastPath && onCredits)
270
+ return { kind: "serve" };
271
+ return move;
272
+ }
273
+ if (route.accountId !== id)
274
+ return { kind: "detour" };
275
+ if (toLastPath && othersOnCredits)
276
+ return move;
277
+ return { kind: "serve" };
178
278
  }
179
- const refusal = route.admission.refused.find((entry) => entry.id === id);
279
+ const refusal = route.admission.refused.find((entry) => entry.id === id), confirmed = killed || refusal !== void 0 && isConfirmedExhaustion(refusal);
280
+ if (onCredits)
281
+ return { kind: "serve" };
282
+ if (othersOnCredits)
283
+ return confirmed ? move : { kind: "detour" };
180
284
  if (!killed && refusal && isConfirmedExhaustion(refusal))
181
285
  return { kind: "last-path" };
182
286
  return { kind: "none" };
@@ -42,6 +42,17 @@ function budgetFor(snapshot) {
42
42
  return { kind: "cleared" };
43
43
  return;
44
44
  }
45
+ function prepaidCreditsFor(value, checkedAt) {
46
+ if (!isRecord(value) || typeof value.hasCredits !== "boolean" || typeof value.unlimited !== "boolean")
47
+ return;
48
+ return {
49
+ hasCredits: value.hasCredits,
50
+ unlimited: value.unlimited,
51
+ ...finite(value.balance) && value.balance >= 0 ? { balance: value.balance } : {},
52
+ ...typeof value.overageLimitReached === "boolean" ? { overageLimitReached: value.overageLimitReached } : {},
53
+ checkedAt
54
+ };
55
+ }
45
56
  export function observationFromSnapshot(snapshot, checkedAt, complete) {
46
57
  if (!isRecord(snapshot) || !finite(checkedAt))
47
58
  return;
@@ -53,15 +64,16 @@ export function observationFromSnapshot(snapshot, checkedAt, complete) {
53
64
  else if (complete)
54
65
  coverage.push({ label });
55
66
  }
56
- const budget = budgetFor(snapshot), credits = snapshot.resetCreditsAvailable, counters = typeof credits === "number" && Number.isSafeInteger(credits) && credits >= 0 ? { resetCredits: { value: credits, checkedAt } } : void 0;
57
- if (readings.length === 0 && coverage.length === 0 && !budget && !counters)
67
+ const budget = budgetFor(snapshot), credits = snapshot.resetCreditsAvailable, counters = typeof credits === "number" && Number.isSafeInteger(credits) && credits >= 0 ? { resetCredits: { value: credits, checkedAt } } : void 0, prepaid = prepaidCreditsFor(snapshot.credits, checkedAt);
68
+ if (readings.length === 0 && coverage.length === 0 && !budget && !counters && !prepaid)
58
69
  return;
59
70
  return {
60
71
  checkedAt,
61
72
  ...readings.length > 0 ? { readings } : {},
62
73
  ...coverage.length > 0 ? { coverage } : {},
63
74
  ...budget ? { budget } : {},
64
- ...counters ? { counters } : {}
75
+ ...counters ? { counters } : {},
76
+ ...prepaid ? { credits: prepaid } : {}
65
77
  };
66
78
  }
67
79
  export function windowsFromQuotaMap(map) {
@@ -5,6 +5,8 @@ import { storeIdentity } from "@cortexkit/claustrum-client";
5
5
  // import makes bundlers emit a separate chunk that a running host loses when
6
6
  // its dist folder is rebuilt underneath it.
7
7
  import { SubcClient } from "@cortexkit/subc-client";
8
+ import { classifyVaultFailure } from "./94f53cfd572ba4bb-availability.js";
9
+ import { ChangeNoticeReader, CoalescedRead, RESUBSCRIBE_BACKOFF_MS, WATCH_RETRY_MS, } from "./dc9290c2a7552ba6-watch.js";
8
10
  /** Constructing this transport opens no connection; getUsage opens one per call. */
9
11
  export function insulaUsageTransport(options) {
10
12
  const identity = options.identity ??
@@ -49,10 +51,86 @@ export function insulaUsageTransport(options) {
49
51
  client?.close();
50
52
  }
51
53
  },
54
+ watchUsage(onNotice) {
55
+ // The watch holds its own connection for as long as it is open: each
56
+ // usage.get above opens and closes a short-lived one, and the scoped
57
+ // credential connection is never shared with insula. The method name
58
+ // alone makes the request a watch; subc-client does not yet set the
59
+ // SUBSCRIPTION flag, and insula does not require it.
60
+ let client;
61
+ let subscription;
62
+ let stopped = false;
63
+ const closed = (async () => {
64
+ let deadline;
65
+ const timeout = new Promise((_resolve, reject) => {
66
+ deadline = setTimeout(
67
+ // Coded as a timeout, so InsulaQuotaSource treats a watch that could
68
+ // not open in time as an unreachable insula and tries again later,
69
+ // not as a refusal.
70
+ () => reject(Object.assign(new Error('insula watch timeout'), {
71
+ code: 'timeout',
72
+ })), 3_000);
73
+ deadline.unref?.();
74
+ });
75
+ const open = (async () => {
76
+ const handshakeTimeoutMs = Math.min(options.handshakeTimeoutMs ?? 3_000, 3_000);
77
+ const connected = options.connector
78
+ ? await options.connector({
79
+ connectionFile: options.connectionFile,
80
+ handshakeTimeoutMs,
81
+ })
82
+ : await SubcClient.connect({
83
+ connectionFile: options.connectionFile,
84
+ handshakeTimeoutMs,
85
+ });
86
+ client = connected;
87
+ // A connection that arrives after stop() or after the open deadline
88
+ // must not keep a route or socket alive.
89
+ if (stopped) {
90
+ connected.close();
91
+ return;
92
+ }
93
+ const route = await connected.routeOpen({ kind: 'management_surface', module_id: 'insula' }, identity);
94
+ if (stopped) {
95
+ connected.close();
96
+ return;
97
+ }
98
+ subscription = connected.subscribe(route, { method: 'usage.watch' }, onNotice);
99
+ })();
100
+ try {
101
+ await Promise.race([open, timeout]);
102
+ }
103
+ catch (error) {
104
+ stopped = true;
105
+ throw error;
106
+ }
107
+ finally {
108
+ clearTimeout(deadline);
109
+ }
110
+ if (subscription)
111
+ await subscription.closed;
112
+ })().finally(() => {
113
+ stopped = true;
114
+ client?.close();
115
+ });
116
+ return {
117
+ stop() {
118
+ if (stopped)
119
+ return;
120
+ stopped = true;
121
+ subscription?.unsubscribe();
122
+ client?.close();
123
+ },
124
+ closed,
125
+ };
126
+ },
52
127
  };
53
128
  }
54
129
  const MINUTE = 60_000;
55
130
  const MAX_AGE = 4 * MINUTE;
131
+ // consumer-contract.md:331 refetches a slot after 60 s. Two such intervals
132
+ // bound our willingness to wait, not a claim that insula completed its sweep.
133
+ const COLD_WAIT = 2 * MINUTE;
56
134
  const sources = new Map();
57
135
  // Keep only the minute budget after the last owner closes, so project churn
58
136
  // cannot reset the bound. Expired budgets are pruned on the next tick.
@@ -60,18 +138,107 @@ const attempts = new Map();
60
138
  function record(value) {
61
139
  return typeof value === 'object' && value !== null && !Array.isArray(value);
62
140
  }
141
+ function decodeUsageNotice(body) {
142
+ let value;
143
+ try {
144
+ value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(body));
145
+ }
146
+ catch {
147
+ return undefined;
148
+ }
149
+ if (!record(value) ||
150
+ typeof value.kind !== 'string' ||
151
+ typeof value.epoch !== 'string' ||
152
+ value.epoch.length === 0 ||
153
+ typeof value.seq !== 'number' ||
154
+ !Number.isSafeInteger(value.seq) ||
155
+ value.seq < 1 ||
156
+ (value.provider !== undefined && typeof value.provider !== 'string'))
157
+ return undefined;
158
+ return {
159
+ kind: value.kind,
160
+ epoch: value.epoch,
161
+ seq: value.seq,
162
+ ...(typeof value.provider === 'string' && { provider: value.provider }),
163
+ };
164
+ }
165
+ function refusalCode(error) {
166
+ return error instanceof Error &&
167
+ 'code' in error &&
168
+ typeof error.code === 'string'
169
+ ? error.code
170
+ : undefined;
171
+ }
63
172
  export class InsulaQuotaSource {
64
173
  transport;
65
174
  key;
66
175
  #entries = [];
67
- #pending;
176
+ #available = false;
177
+ #completeProviders = new Set();
178
+ #coldSince = new Map();
179
+ /** Every usage.get, polled or watch-driven, shares this one slot. */
180
+ #reads = new CoalescedRead(async () => {
181
+ try {
182
+ const reply = await this.transport.getUsage();
183
+ if (!record(reply) ||
184
+ !Array.isArray(reply.result) ||
185
+ reply.error !== undefined)
186
+ throw new Error('unavailable insula usage');
187
+ // Each response is a snapshot, not a delta. Only completeProviders
188
+ // asserts completeness; neither pending nor the entry count does.
189
+ this.#entries = reply.result.filter((entry) => record(entry) && typeof entry.provider === 'string');
190
+ this.#available =
191
+ this.#entries.length === reply.result.length &&
192
+ (reply.completeProviders === undefined ||
193
+ (Array.isArray(reply.completeProviders) &&
194
+ reply.completeProviders.every((value) => typeof value === 'string')));
195
+ this.#completeProviders = new Set(this.#available && Array.isArray(reply.completeProviders)
196
+ ? reply.completeProviders
197
+ : []);
198
+ for (const provider of this.#completeProviders)
199
+ this.#coldSince.delete(provider);
200
+ }
201
+ catch {
202
+ // Refusal is absence, even if the previous reading was still young.
203
+ this.#entries = [];
204
+ this.#available = false;
205
+ this.#completeProviders.clear();
206
+ }
207
+ }, () => { });
208
+ #reader = new ChangeNoticeReader();
209
+ #mode = 'polling';
210
+ #watchers = 0;
211
+ /**
212
+ * How many registered watchers read every provider. Watchers limited to one
213
+ * provider are counted per provider in `#providers` instead.
214
+ */
215
+ #everyProvider = 0;
216
+ #providers = new Map();
217
+ #watch;
218
+ #timer;
219
+ #timers = {};
220
+ /**
221
+ * How many accepted watches in a row have closed; each one doubles the wait
222
+ * before the next re-subscribe. Reset once a watch brings a second notice.
223
+ */
224
+ #closes = 0;
68
225
  constructor(transport, key) {
69
226
  this.transport = transport;
70
227
  this.key = key;
71
228
  }
229
+ /**
230
+ * The background poll: at most one usage.get per minute per daemon and
231
+ * enrollment token. While a watch is open it reads nothing, because insula's
232
+ * notices announce every new reading. If insula stops announcing an account,
233
+ * that account's last entry grows older than the freshness limit `current()`
234
+ * applies, `current()` returns nothing for it, and the plugin falls back to
235
+ * its own provider poll for that account.
236
+ */
72
237
  refresh(now) {
73
- if (this.#pending)
74
- return this.#pending;
238
+ if (this.#reads.running)
239
+ return this.#reads.running;
240
+ if (this.#mode === 'watching')
241
+ return Promise.resolve();
75
242
  for (const [key, time] of attempts) {
76
243
  if (now - time >= MINUTE)
77
244
  attempts.delete(key);
@@ -81,25 +248,141 @@ export class InsulaQuotaSource {
81
248
  return Promise.resolve();
82
249
  // Reserve the minute before I/O, so concurrent projects cannot each poll.
83
250
  attempts.set(this.key, now);
84
- this.#pending = (async () => {
85
- try {
86
- const reply = await this.transport.getUsage();
87
- if (!record(reply) ||
88
- !Array.isArray(reply.result) ||
89
- reply.error !== undefined)
90
- throw new Error('unavailable insula usage');
91
- // Each response is a snapshot, not a delta. Missing accounts fall back;
92
- // neither pending nor the entry count asserts provider completeness.
93
- this.#entries = reply.result.filter((entry) => record(entry) && typeof entry.provider === 'string');
94
- }
95
- catch {
96
- // Refusal is absence, even if the previous reading was still young.
97
- this.#entries = [];
251
+ return this.#reads.request();
252
+ }
253
+ /** How this source currently learns about usage changes, for logs. */
254
+ watchMode() {
255
+ return this.#mode;
256
+ }
257
+ /**
258
+ * Hold a `usage.watch` open while any watcher wants it, and re-read
259
+ * usage.get when a notice says something changed. Watchers of one source
260
+ * share one watch, since insula admits only 64 per process. Returns the
261
+ * release; the last release closes the watch.
262
+ */
263
+ watch(options = {}) {
264
+ this.#watchers++;
265
+ const provider = options.provider;
266
+ if (provider === undefined)
267
+ this.#everyProvider++;
268
+ else
269
+ this.#providers.set(provider, (this.#providers.get(provider) ?? 0) + 1);
270
+ if (this.#watchers === 1) {
271
+ this.#timers = options;
272
+ this.#open();
273
+ }
274
+ let released = false;
275
+ return () => {
276
+ if (released)
277
+ return;
278
+ released = true;
279
+ if (provider === undefined)
280
+ this.#everyProvider--;
281
+ else {
282
+ const left = (this.#providers.get(provider) ?? 1) - 1;
283
+ if (left > 0)
284
+ this.#providers.set(provider, left);
285
+ else
286
+ this.#providers.delete(provider);
98
287
  }
99
- })().finally(() => {
100
- this.#pending = undefined;
101
- });
102
- return this.#pending;
288
+ if (--this.#watchers === 0)
289
+ this.#stopWatch();
290
+ };
291
+ }
292
+ #stopWatch() {
293
+ if (this.#timer)
294
+ (this.#timers.clearTimeoutImpl ?? clearTimeout)(this.#timer);
295
+ this.#timer = undefined;
296
+ const watch = this.#watch;
297
+ this.#watch = undefined;
298
+ watch?.handle?.stop();
299
+ this.#mode = 'polling';
300
+ }
301
+ #later(delay) {
302
+ this.#timer = (this.#timers.setTimeoutImpl ?? setTimeout)(() => {
303
+ this.#timer = undefined;
304
+ if (this.#watchers > 0 && !this.#watch)
305
+ this.#open();
306
+ }, delay);
307
+ this.#timer.unref?.();
308
+ }
309
+ #open() {
310
+ const watchUsage = this.transport.watchUsage;
311
+ if (!watchUsage) {
312
+ // A custom transport that cannot watch: poll exactly as before.
313
+ this.#mode = 'fallback';
314
+ return;
315
+ }
316
+ this.#mode = 'polling';
317
+ this.#reader.restart();
318
+ const watch = { notices: 0 };
319
+ this.#watch = watch;
320
+ let handle;
321
+ try {
322
+ handle = watchUsage.call(this.transport, (body) => this.#notice(watch, body));
323
+ }
324
+ catch (error) {
325
+ this.#closed(watch, error);
326
+ return;
327
+ }
328
+ watch.handle = handle;
329
+ handle.closed.then(() => this.#closed(watch, undefined), (error) => this.#closed(watch, error));
330
+ }
331
+ #notice(watch, body) {
332
+ // Frames already in transport can arrive after a local stop.
333
+ if (this.#watch !== watch)
334
+ return;
335
+ watch.notices++;
336
+ if (watch.notices === 1)
337
+ this.#mode = 'watching';
338
+ // A second notice means the watch stayed up past its acceptance.
339
+ if (watch.notices === 2)
340
+ this.#closes = 0;
341
+ const notice = decodeUsageNotice(body);
342
+ // An unreadable notice may hide any change: treat it as a break, and
343
+ // make the next notice read as one too.
344
+ if (!notice)
345
+ this.#reader.restart();
346
+ const continuous = notice ? this.#reader.continuous(notice) : false;
347
+ // The first notice of every watch names no provider, so it always reads:
348
+ // nothing was watched before it. After a gap or a new epoch notices were
349
+ // lost, so read even when this notice names another provider.
350
+ if (!continuous ||
351
+ notice?.provider === undefined ||
352
+ this.#everyProvider > 0 ||
353
+ this.#providers.has(notice.provider))
354
+ void this.#reads.request();
355
+ }
356
+ #closed(watch, error) {
357
+ if (this.#watch !== watch)
358
+ return;
359
+ this.#watch = undefined;
360
+ const code = refusalCode(error);
361
+ if (code === 'unknown_method' || code === 'usage_watch_limit') {
362
+ // An insula that predates usage.watch, or one already holding 64
363
+ // watches: poll exactly as before until every watcher has released.
364
+ this.#mode = 'fallback';
365
+ return;
366
+ }
367
+ if (watch.notices > 0) {
368
+ // An accepted watch that closed (insula shut down, or the connection
369
+ // dropped) gets one new watch, whose first notice reads usage.get.
370
+ const delay = Math.min(RESUBSCRIBE_BACKOFF_MS * 2 ** Math.min(this.#closes, 16), WATCH_RETRY_MS);
371
+ this.#closes++;
372
+ this.#mode = 'polling';
373
+ this.#later(delay);
374
+ return;
375
+ }
376
+ // Closed before any notice, so never accepted: read once to learn
377
+ // whether insula answers at all, then poll as before. A refusal insula
378
+ // answered stands until every watcher has released. A close it did not
379
+ // explain (a
380
+ // dropped connection, a timeout, a stream ended at once while insula
381
+ // shuts down) earns one new watch much later.
382
+ this.#mode = 'fallback';
383
+ void this.#reads.request();
384
+ if (error === undefined || classifyVaultFailure(error) === 'unreachable')
385
+ this.#later(WATCH_RETRY_MS);
103
386
  }
104
387
  current(provider, accountId, now) {
105
388
  for (const entry of this.#entries) {
@@ -126,6 +409,50 @@ export class InsulaQuotaSource {
126
409
  }
127
410
  return undefined;
128
411
  }
412
+ /** Cache-only background lookup; current() retains its original miss behavior. */
413
+ lookup(provider, accountId, now) {
414
+ if (!this.#available)
415
+ return { miss: 'unreachable' };
416
+ const entry = this.#entries.find((value) => value.provider === provider &&
417
+ typeof value.account === 'string' &&
418
+ value.account.trim() === accountId);
419
+ // An unlabelled error describes the provider's credential path, not an
420
+ // unrelated labelled account. Never borrow another account's verdict.
421
+ const errorClass = (entry ??
422
+ this.#entries.find((value) => value.provider === provider &&
423
+ value.account === undefined &&
424
+ value.errorClass !== undefined))?.errorClass;
425
+ if (errorClass === 'upstream_failed')
426
+ return { miss: 'upstream-failed' };
427
+ if (errorClass === 'credential_absent' ||
428
+ errorClass === 'credential_unusable' ||
429
+ errorClass === 'credential_rejected')
430
+ return { miss: 'credential' };
431
+ if (errorClass !== undefined)
432
+ return { miss: 'unreachable' };
433
+ if (entry) {
434
+ const fetchedAt = typeof entry.fetchedAt === 'string'
435
+ ? Date.parse(entry.fetchedAt)
436
+ : Number.NaN;
437
+ if (!record(entry.usage) || !Number.isFinite(fetchedAt))
438
+ return { miss: 'unreachable' };
439
+ const reading = {
440
+ entry,
441
+ context: {
442
+ fetchedAt,
443
+ ...(typeof entry.source === 'string' && { source: entry.source }),
444
+ },
445
+ };
446
+ return now >= fetchedAt + MAX_AGE || entry.stale !== undefined
447
+ ? { miss: 'stale', reading }
448
+ : { reading };
449
+ }
450
+ if (this.#completeProviders.has(provider))
451
+ return { miss: 'absent' };
452
+ const since = this.#coldSince.get(provider) ?? now;
453
+ this.#coldSince.set(provider, since);
454
+ return { miss: now - since >= COLD_WAIT ? 'unreachable' : 'cold' };
455
+ }
129
456
  }
130
457
  export function acquireInsulaQuotaSource(transport, tokenPath) {
131
458
  const key = JSON.stringify([
@@ -1,5 +1,5 @@
1
1
  export { quotaCodec } from "./053cc17b5419261e-codec.js";
2
- export { formatQuota, isQuotaStale, NO_LIMITS_REPORTED, NO_QUOTA_READING, QUOTA_STALE_AFTER_MS, quotaTextParts, quotaWindowName, quotaWindowParts, } from "./0332701351b3eeec-format.js";
3
- export { ALL_SCOPE, DEFAULT_REQUIRED_LABELS, decodeQuotaMap, emptyQuotaMap, isQuotaMap, } from "./905bffc0220f43d0-map.js";
2
+ export { formatQuota, isQuotaStale, NO_LIMITS_REPORTED, NO_QUOTA_READING, QUOTA_STALE_AFTER_MS, quotaCreditsText, quotaTextParts, quotaWindowName, quotaWindowParts, } from "./0332701351b3eeec-format.js";
3
+ export { ALL_SCOPE, DEFAULT_REQUIRED_LABELS, decodeQuotaMap, emptyQuotaMap, isQuotaCredits, isQuotaMap, } from "./905bffc0220f43d0-map.js";
4
4
  export { isQuotaObservation, mergeQuotaObservation, QuotaCodecError, } from "./4f36d333d081d040-merge.js";
5
5
  export { budgetExhaustedResetAt, futureResetAt, projectQuota, readsExhausted, } from "./39c3cb05738fcf6d-projection.js";
@@ -154,13 +154,14 @@ function normalizeQuota(value) {
154
154
  };
155
155
  }
156
156
  const credits = value.credits;
157
- if (isRecord(credits) && typeof credits.hasCredits === "boolean" && typeof credits.unlimited === "boolean" && typeof credits.overageLimitReached === "boolean") {
158
- const balance = Number(credits.balance);
157
+ if (isRecord(credits) && typeof credits.hasCredits === "boolean" && typeof credits.unlimited === "boolean" && (credits.overageLimitReached === void 0 || typeof credits.overageLimitReached === "boolean")) {
158
+ const balance = Number(credits.balance), checkedAt = typeof credits.checkedAt === "number" ? credits.checkedAt : Number.NaN;
159
159
  quota.credits = {
160
160
  hasCredits: credits.hasCredits,
161
161
  unlimited: credits.unlimited,
162
- overageLimitReached: credits.overageLimitReached,
163
- ...Number.isFinite(balance) ? { balance } : {}
162
+ ...typeof credits.overageLimitReached === "boolean" ? { overageLimitReached: credits.overageLimitReached } : {},
163
+ ...Number.isFinite(balance) ? { balance } : {},
164
+ ...Number.isFinite(checkedAt) ? { checkedAt } : {}
164
165
  };
165
166
  }
166
167
  return Object.keys(quota).length ? quota : void 0;