@cortexkit/opencode-openai-auth 1.1.0 → 1.3.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 (66) hide show
  1. package/dist/bin.js +8 -8
  2. package/dist/{codex-request-BXDXToyt.d.ts → codex-request-B9D0e97Q.d.ts} +177 -28
  3. package/dist/{index-7c6sxtd6.js → index-1mbgsxsr.js} +2 -2
  4. package/dist/{index-4c8yw54k.js → index-2pgkrebq.js} +2 -2
  5. package/dist/index-4v2fs42x.js +65 -0
  6. package/dist/index-8x1aj7q3.js +2 -0
  7. package/dist/{index-f2fmb596.js → index-bc89akak.js} +1 -1
  8. package/dist/index-ex6nx3mq.js +2 -0
  9. package/dist/{index-zpm95mt5.js → index-ftkhhr6d.js} +1 -1
  10. package/dist/{index-msx2m78k.js → index-gn2b097f.js} +2 -2
  11. package/dist/{index-j8v2xwq6.js → index-m3ctrabv.js} +1 -1
  12. package/dist/index-psjyexj3.js +69 -0
  13. package/dist/{index-k6cydkgt.js → index-tddr876f.js} +2 -2
  14. package/dist/index-tn8x5tz8.js +90 -0
  15. package/dist/{index-csky2xpg.js → index-xwvze96g.js} +1 -1
  16. package/dist/index.d.ts +5 -3
  17. package/dist/index.js +1 -1
  18. package/dist/{model-YRg7Uowo.d.ts → model-BdITTmeb.d.ts} +1 -1
  19. package/dist/{port-file-CxEpgSHY.d.ts → port-file-DbHIVC9a.d.ts} +1 -1
  20. package/dist/{protocol-CDV1Pc6h.d.ts → protocol-MysYPAho.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-DjTWUbmf.d.ts → sidebar-state-CVSok7zv.d.ts} +114 -5
  29. package/dist/sidebar-state.d.ts +2 -2
  30. package/dist/sidebar-state.js +1 -1
  31. package/dist/token-util-cf0nyh7n.js +1 -0
  32. package/dist/{token-rz4t3ay6.js → token-z6qq7a1d.js} +1 -1
  33. package/dist/tui-preferences.js +1 -1
  34. package/dist/tui.d.ts +2 -2
  35. package/dist/v2/server.d.ts +109 -2
  36. package/dist/v2/server.js +3 -3
  37. package/package.json +4 -4
  38. package/src/tui-compiled/raw/shared/0332701351b3eeec-format.js +203 -18
  39. package/src/tui-compiled/raw/shared/0f77ebc3c914c9c6-custody.js +242 -16
  40. package/src/tui-compiled/raw/shared/169b2b4bf30909bc-consumer.js +168 -6
  41. package/src/tui-compiled/raw/shared/375d9cca2def037d-private-files.js +10 -2
  42. package/src/tui-compiled/raw/shared/94f53cfd572ba4bb-availability.js +106 -0
  43. package/src/tui-compiled/raw/shared/d0f0b6be200311af-insula.js +286 -21
  44. package/src/tui-compiled/raw/shared/d5a8f1061ee2f114-index.js +1 -1
  45. package/src/tui-compiled/raw/shared/dc9290c2a7552ba6-watch.js +253 -0
  46. package/src/tui-compiled/raw/shared/e8d3c8da776ef9cd-index.js +4 -1
  47. package/src/tui-compiled/raw/sidebar-state.js +63 -1
  48. package/src/tui-compiled/raw/tui.tsx +82 -30
  49. package/src/tui-compiled/runtime/shared/0332701351b3eeec-format.js +203 -18
  50. package/src/tui-compiled/runtime/shared/0f77ebc3c914c9c6-custody.js +242 -16
  51. package/src/tui-compiled/runtime/shared/169b2b4bf30909bc-consumer.js +168 -6
  52. package/src/tui-compiled/runtime/shared/375d9cca2def037d-private-files.js +10 -2
  53. package/src/tui-compiled/runtime/shared/94f53cfd572ba4bb-availability.js +106 -0
  54. package/src/tui-compiled/runtime/shared/d0f0b6be200311af-insula.js +286 -21
  55. package/src/tui-compiled/runtime/shared/d5a8f1061ee2f114-index.js +1 -1
  56. package/src/tui-compiled/runtime/shared/dc9290c2a7552ba6-watch.js +253 -0
  57. package/src/tui-compiled/runtime/shared/e8d3c8da776ef9cd-index.js +4 -1
  58. package/src/tui-compiled/runtime/sidebar-state.js +63 -1
  59. package/src/tui-compiled/runtime/tui.js +245 -208
  60. package/dist/index-0yh85bwb.js +0 -69
  61. package/dist/index-7eb3dw37.js +0 -90
  62. package/dist/index-8re8rprk.js +0 -2
  63. package/dist/index-edbdscy7.js +0 -65
  64. package/dist/index-m5qmvr6c.js +0 -2
  65. package/dist/map-Buq-GW7l.d.ts +0 -56
  66. package/dist/token-util-tpy2c7h6.js +0 -1
@@ -184,19 +184,25 @@ export async function replaceWindowsFile(from, to, options = {}) {
184
184
  to = resolve(to);
185
185
  const now = options.now ?? (() => performance.now());
186
186
  const start = now();
187
- const deadline = Math.min(start + 500, options.deadline ?? Infinity);
187
+ const leaseDeadline = options.deadline ?? Infinity;
188
+ let retryDeadline;
188
189
  const delays = [0, 10, 20, 40, 80, 100];
189
190
  let original;
190
191
  let attempts = 0;
191
192
  for (const delay of delays) {
193
+ const deadline = Math.min(retryDeadline ?? Infinity, leaseDeadline);
192
194
  if (now() >= deadline)
193
195
  break;
194
196
  if (delay)
195
197
  await (options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms))))(Math.min(delay, Math.max(0, deadline - now())));
196
198
  if (now() >= deadline)
197
199
  break;
200
+ const fenceStart = now();
198
201
  await options.beforeRename?.();
199
- if (now() >= deadline)
202
+ // Ownership checks fence every attempt but do not spend the sharing-retry budget.
203
+ if (retryDeadline !== undefined)
204
+ retryDeadline += now() - fenceStart;
205
+ if (now() >= leaseDeadline)
200
206
  break;
201
207
  attempts++;
202
208
  try {
@@ -212,6 +218,8 @@ export async function replaceWindowsFile(from, to, options = {}) {
212
218
  !detail.reason;
213
219
  if (!candidate)
214
220
  break;
221
+ // Start the retry window only once a rename has actually failed.
222
+ retryDeadline ??= now() + 500;
215
223
  }
216
224
  }
217
225
  const error = new PrivateFileError(to, 'rename', original ??
@@ -0,0 +1,106 @@
1
+ import { ClaustrumCredentialError } from "@cortexkit/claustrum-client";
2
+ import { SocketClosedError, SocketTimeoutError, SubcCallError, } from "@cortexkit/subc-client";
3
+ import { ClaustrumConsumerError } from "./ca0fccaba1d87af4-errors.js";
4
+ /**
5
+ * Codes from claustrum-client's decoder and its subc transport. A retry action
6
+ * alone is not evidence of an outage: even transient vault refusals must win
7
+ * over a previously served credential. New codes fail closed until reviewed.
8
+ */
9
+ export const VAULT_FAILURE_CLASSES = Object.freeze({
10
+ ECONNREFUSED: 'unreachable',
11
+ ENOENT: 'unreachable',
12
+ ECONNRESET: 'unreachable',
13
+ EPIPE: 'unreachable',
14
+ ETIMEDOUT: 'unreachable',
15
+ transport_error: 'unreachable',
16
+ unavailable: 'unreachable',
17
+ timeout: 'unreachable',
18
+ route_closed: 'unreachable',
19
+ daemon_draining: 'unreachable',
20
+ connection_closed: 'unreachable',
21
+ connection_dropped: 'unreachable',
22
+ request_deadline: 'unreachable',
23
+ deadline_exceeded_no_drop_observed: 'unreachable',
24
+ module_reloading: 'unreachable',
25
+ module_warming: 'unreachable',
26
+ target_unavailable: 'unreachable',
27
+ module_timeout: 'unreachable',
28
+ invalid_response: 'answered',
29
+ invalid_record_version: 'answered',
30
+ invalid_expiry: 'answered',
31
+ invalid_status: 'answered',
32
+ invalid_params: 'answered',
33
+ ttl_unsatisfiable: 'answered',
34
+ not_found: 'answered',
35
+ enrollment_revoked: 'answered',
36
+ enrollment_invalid: 'answered',
37
+ grant_denied: 'answered',
38
+ credential_unavailable: 'answered',
39
+ needs_reauth: 'answered',
40
+ declined: 'answered',
41
+ disabled: 'answered',
42
+ identity_changed: 'answered',
43
+ closed: 'answered',
44
+ client_closed: 'answered',
45
+ pending_exists: 'answered',
46
+ pending_queue_full: 'answered',
47
+ already_consumed: 'answered',
48
+ superseded: 'answered',
49
+ stale_generation: 'answered',
50
+ missing_identity: 'answered',
51
+ invalid_control_body: 'answered',
52
+ unknown_module: 'answered',
53
+ module_removed: 'answered',
54
+ capability_denied: 'answered',
55
+ unknown_channel: 'unreachable',
56
+ stale_route_epoch: 'unreachable',
57
+ capability_forbidden: 'answered',
58
+ capability_unprovided: 'answered',
59
+ capability_ambiguous: 'answered',
60
+ invalid_capability_identifier: 'answered',
61
+ binary_body_required: 'answered',
62
+ binary_call_requires_call_binary: 'answered',
63
+ corr_exhausted: 'answered',
64
+ late_route_cleanup_failed: 'answered',
65
+ reverse_request_capability_not_declared: 'answered',
66
+ stale_route_handle: 'answered',
67
+ reverse_request_unhandled: 'answered',
68
+ handler_error: 'answered',
69
+ health_error: 'answered',
70
+ duplicate_module_id: 'answered',
71
+ route_rejected: 'answered',
72
+ lock_busy: 'answered',
73
+ owner_invalid: 'answered',
74
+ renewal_failed: 'answered',
75
+ unsupported_platform: 'answered',
76
+ EACCES: 'answered',
77
+ EPERM: 'answered',
78
+ ERR_PRIVATE_FILES_SECURITY: 'answered',
79
+ });
80
+ /**
81
+ * An injected adapter may throw this ONLY when its connection/transport is
82
+ * unreachable, including a failed connect before getScoped. Do not wrap an
83
+ * adapter shutdown, cancellation, validation error or vault refusal in it.
84
+ * Its fixed message cannot echo enrollment or credential material, and its
85
+ * kind stays `unavailable` for callers handling ClaustrumConsumerError.
86
+ */
87
+ export class ClaustrumUnreachableError extends ClaustrumConsumerError {
88
+ constructor() {
89
+ super('unavailable', 'Claustrum scoped operation unavailable');
90
+ this.name = 'ClaustrumUnreachableError';
91
+ }
92
+ }
93
+ export function classifyVaultFailure(error) {
94
+ if (error instanceof ClaustrumUnreachableError ||
95
+ error instanceof SocketClosedError ||
96
+ error instanceof SocketTimeoutError)
97
+ return 'unreachable';
98
+ const code = error instanceof ClaustrumCredentialError || error instanceof SubcCallError
99
+ ? error.code
100
+ : error instanceof Error && 'code' in error
101
+ ? error.code
102
+ : undefined;
103
+ if (typeof code !== 'string' || !Object.hasOwn(VAULT_FAILURE_CLASSES, code))
104
+ return 'answered';
105
+ return VAULT_FAILURE_CLASSES[code];
106
+ }
@@ -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,6 +51,79 @@ 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;
@@ -60,18 +135,92 @@ const attempts = new Map();
60
135
  function record(value) {
61
136
  return typeof value === 'object' && value !== null && !Array.isArray(value);
62
137
  }
138
+ function decodeUsageNotice(body) {
139
+ let value;
140
+ try {
141
+ value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(body));
142
+ }
143
+ catch {
144
+ return undefined;
145
+ }
146
+ if (!record(value) ||
147
+ typeof value.kind !== 'string' ||
148
+ typeof value.epoch !== 'string' ||
149
+ value.epoch.length === 0 ||
150
+ typeof value.seq !== 'number' ||
151
+ !Number.isSafeInteger(value.seq) ||
152
+ value.seq < 1 ||
153
+ (value.provider !== undefined && typeof value.provider !== 'string'))
154
+ return undefined;
155
+ return {
156
+ kind: value.kind,
157
+ epoch: value.epoch,
158
+ seq: value.seq,
159
+ ...(typeof value.provider === 'string' && { provider: value.provider }),
160
+ };
161
+ }
162
+ function refusalCode(error) {
163
+ return error instanceof Error &&
164
+ 'code' in error &&
165
+ typeof error.code === 'string'
166
+ ? error.code
167
+ : undefined;
168
+ }
63
169
  export class InsulaQuotaSource {
64
170
  transport;
65
171
  key;
66
172
  #entries = [];
67
- #pending;
173
+ /** Every usage.get, polled or watch-driven, shares this one slot. */
174
+ #reads = new CoalescedRead(async () => {
175
+ try {
176
+ const reply = await this.transport.getUsage();
177
+ if (!record(reply) ||
178
+ !Array.isArray(reply.result) ||
179
+ reply.error !== undefined)
180
+ throw new Error('unavailable insula usage');
181
+ // Each response is a snapshot, not a delta. Missing accounts fall back;
182
+ // neither pending nor the entry count asserts provider completeness.
183
+ this.#entries = reply.result.filter((entry) => record(entry) && typeof entry.provider === 'string');
184
+ }
185
+ catch {
186
+ // Refusal is absence, even if the previous reading was still young.
187
+ this.#entries = [];
188
+ }
189
+ }, () => { });
190
+ #reader = new ChangeNoticeReader();
191
+ #mode = 'polling';
192
+ #watchers = 0;
193
+ /**
194
+ * How many registered watchers read every provider. Watchers limited to one
195
+ * provider are counted per provider in `#providers` instead.
196
+ */
197
+ #everyProvider = 0;
198
+ #providers = new Map();
199
+ #watch;
200
+ #timer;
201
+ #timers = {};
202
+ /**
203
+ * How many accepted watches in a row have closed; each one doubles the wait
204
+ * before the next re-subscribe. Reset once a watch brings a second notice.
205
+ */
206
+ #closes = 0;
68
207
  constructor(transport, key) {
69
208
  this.transport = transport;
70
209
  this.key = key;
71
210
  }
211
+ /**
212
+ * The background poll: at most one usage.get per minute per daemon and
213
+ * enrollment token. While a watch is open it reads nothing, because insula's
214
+ * notices announce every new reading. If insula stops announcing an account,
215
+ * that account's last entry grows older than the freshness limit `current()`
216
+ * applies, `current()` returns nothing for it, and the plugin falls back to
217
+ * its own provider poll for that account.
218
+ */
72
219
  refresh(now) {
73
- if (this.#pending)
74
- return this.#pending;
220
+ if (this.#reads.running)
221
+ return this.#reads.running;
222
+ if (this.#mode === 'watching')
223
+ return Promise.resolve();
75
224
  for (const [key, time] of attempts) {
76
225
  if (now - time >= MINUTE)
77
226
  attempts.delete(key);
@@ -81,25 +230,141 @@ export class InsulaQuotaSource {
81
230
  return Promise.resolve();
82
231
  // Reserve the minute before I/O, so concurrent projects cannot each poll.
83
232
  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 = [];
233
+ return this.#reads.request();
234
+ }
235
+ /** How this source currently learns about usage changes, for logs. */
236
+ watchMode() {
237
+ return this.#mode;
238
+ }
239
+ /**
240
+ * Hold a `usage.watch` open while any watcher wants it, and re-read
241
+ * usage.get when a notice says something changed. Watchers of one source
242
+ * share one watch, since insula admits only 64 per process. Returns the
243
+ * release; the last release closes the watch.
244
+ */
245
+ watch(options = {}) {
246
+ this.#watchers++;
247
+ const provider = options.provider;
248
+ if (provider === undefined)
249
+ this.#everyProvider++;
250
+ else
251
+ this.#providers.set(provider, (this.#providers.get(provider) ?? 0) + 1);
252
+ if (this.#watchers === 1) {
253
+ this.#timers = options;
254
+ this.#open();
255
+ }
256
+ let released = false;
257
+ return () => {
258
+ if (released)
259
+ return;
260
+ released = true;
261
+ if (provider === undefined)
262
+ this.#everyProvider--;
263
+ else {
264
+ const left = (this.#providers.get(provider) ?? 1) - 1;
265
+ if (left > 0)
266
+ this.#providers.set(provider, left);
267
+ else
268
+ this.#providers.delete(provider);
98
269
  }
99
- })().finally(() => {
100
- this.#pending = undefined;
101
- });
102
- return this.#pending;
270
+ if (--this.#watchers === 0)
271
+ this.#stopWatch();
272
+ };
273
+ }
274
+ #stopWatch() {
275
+ if (this.#timer)
276
+ (this.#timers.clearTimeoutImpl ?? clearTimeout)(this.#timer);
277
+ this.#timer = undefined;
278
+ const watch = this.#watch;
279
+ this.#watch = undefined;
280
+ watch?.handle?.stop();
281
+ this.#mode = 'polling';
282
+ }
283
+ #later(delay) {
284
+ this.#timer = (this.#timers.setTimeoutImpl ?? setTimeout)(() => {
285
+ this.#timer = undefined;
286
+ if (this.#watchers > 0 && !this.#watch)
287
+ this.#open();
288
+ }, delay);
289
+ this.#timer.unref?.();
290
+ }
291
+ #open() {
292
+ const watchUsage = this.transport.watchUsage;
293
+ if (!watchUsage) {
294
+ // A custom transport that cannot watch: poll exactly as before.
295
+ this.#mode = 'fallback';
296
+ return;
297
+ }
298
+ this.#mode = 'polling';
299
+ this.#reader.restart();
300
+ const watch = { notices: 0 };
301
+ this.#watch = watch;
302
+ let handle;
303
+ try {
304
+ handle = watchUsage.call(this.transport, (body) => this.#notice(watch, body));
305
+ }
306
+ catch (error) {
307
+ this.#closed(watch, error);
308
+ return;
309
+ }
310
+ watch.handle = handle;
311
+ handle.closed.then(() => this.#closed(watch, undefined), (error) => this.#closed(watch, error));
312
+ }
313
+ #notice(watch, body) {
314
+ // Frames already in transport can arrive after a local stop.
315
+ if (this.#watch !== watch)
316
+ return;
317
+ watch.notices++;
318
+ if (watch.notices === 1)
319
+ this.#mode = 'watching';
320
+ // A second notice means the watch stayed up past its acceptance.
321
+ if (watch.notices === 2)
322
+ this.#closes = 0;
323
+ const notice = decodeUsageNotice(body);
324
+ // An unreadable notice may hide any change: treat it as a break, and
325
+ // make the next notice read as one too.
326
+ if (!notice)
327
+ this.#reader.restart();
328
+ const continuous = notice ? this.#reader.continuous(notice) : false;
329
+ // The first notice of every watch names no provider, so it always reads:
330
+ // nothing was watched before it. After a gap or a new epoch notices were
331
+ // lost, so read even when this notice names another provider.
332
+ if (!continuous ||
333
+ notice?.provider === undefined ||
334
+ this.#everyProvider > 0 ||
335
+ this.#providers.has(notice.provider))
336
+ void this.#reads.request();
337
+ }
338
+ #closed(watch, error) {
339
+ if (this.#watch !== watch)
340
+ return;
341
+ this.#watch = undefined;
342
+ const code = refusalCode(error);
343
+ if (code === 'unknown_method' || code === 'usage_watch_limit') {
344
+ // An insula that predates usage.watch, or one already holding 64
345
+ // watches: poll exactly as before until every watcher has released.
346
+ this.#mode = 'fallback';
347
+ return;
348
+ }
349
+ if (watch.notices > 0) {
350
+ // An accepted watch that closed (insula shut down, or the connection
351
+ // dropped) gets one new watch, whose first notice reads usage.get.
352
+ const delay = Math.min(RESUBSCRIBE_BACKOFF_MS * 2 ** Math.min(this.#closes, 16), WATCH_RETRY_MS);
353
+ this.#closes++;
354
+ this.#mode = 'polling';
355
+ this.#later(delay);
356
+ return;
357
+ }
358
+ // Closed before any notice, so never accepted: read once to learn
359
+ // whether insula answers at all, then poll as before. A refusal insula
360
+ // answered stands until every watcher has released. A close it did not
361
+ // explain (a
362
+ // dropped connection, a timeout, a stream ended at once while insula
363
+ // shuts down) earns one new watch much later.
364
+ this.#mode = 'fallback';
365
+ void this.#reads.request();
366
+ if (error === undefined || classifyVaultFailure(error) === 'unreachable')
367
+ this.#later(WATCH_RETRY_MS);
103
368
  }
104
369
  current(provider, accountId, now) {
105
370
  for (const entry of this.#entries) {
@@ -1,5 +1,5 @@
1
1
  export { quotaCodec } from "./053cc17b5419261e-codec.js";
2
- export { formatQuota, NO_LIMITS_REPORTED, NO_QUOTA_READING, QUOTA_STALE_AFTER_MS, quotaTextParts, quotaWindowName, quotaWindowParts, } from "./0332701351b3eeec-format.js";
2
+ export { formatQuota, isQuotaStale, NO_LIMITS_REPORTED, NO_QUOTA_READING, QUOTA_STALE_AFTER_MS, quotaTextParts, quotaWindowName, quotaWindowParts, } from "./0332701351b3eeec-format.js";
3
3
  export { ALL_SCOPE, DEFAULT_REQUIRED_LABELS, decodeQuotaMap, emptyQuotaMap, 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";