@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
@@ -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 ??
@@ -6,7 +6,7 @@
6
6
  // scope, orders the result the way the primitives expect, and carries each
7
7
  // entry's (scope, label) so a decision names the limit it judged rather than
8
8
  // the position it occupied.
9
- import { ALL_SCOPE, readQuotaCounters, } from "./905bffc0220f43d0-map.js";
9
+ import { ALL_SCOPE, isQuotaCredits, readQuotaCounters, } from "./905bffc0220f43d0-map.js";
10
10
  function project(entry) {
11
11
  if (entry.kind === 'retired') {
12
12
  return {
@@ -95,12 +95,14 @@ export function projectQuota(map, scope = ALL_SCOPE) {
95
95
  : undefined;
96
96
  const budget = map?.budget?.kind === 'reading' ? projectBudget(map.budget) : undefined;
97
97
  const counters = readQuotaCounters(map?.counters);
98
+ const credits = isQuotaCredits(map?.credits) ? map.credits : undefined;
98
99
  return {
99
100
  scope,
100
101
  limits,
101
102
  ...(checkedAt === undefined ? {} : { checkedAt }),
102
103
  ...(budget === undefined ? {} : { budget }),
103
104
  ...(counters === undefined ? {} : { counters }),
105
+ ...(credits === undefined ? {} : { credits }),
104
106
  };
105
107
  }
106
108
  /**
@@ -1,8 +1,5 @@
1
- import {
2
- execFileSync as defaultExecFileSync,
3
- spawn as defaultSpawn
4
- } from "node:child_process";
5
- export function openUrl(url, platform = process.platform, execFileSync = defaultExecFileSync, spawn = defaultSpawn) {
1
+ import { spawn as defaultSpawn } from "node:child_process";
2
+ export function openUrl(url, platform = process.platform, spawn = defaultSpawn) {
6
3
  try {
7
4
  if (platform === "win32") {
8
5
  const command = `Start-Process '${url.replaceAll("'", "''")}'`, encodedCommand = Buffer.from(command, "utf16le").toString("base64"), child = spawn("powershell.exe", [
@@ -17,11 +14,12 @@ export function openUrl(url, platform = process.platform, execFileSync = default
17
14
  child.unref();
18
15
  return child.pid !== void 0;
19
16
  }
20
- execFileSync(platform === "darwin" ? "open" : "xdg-open", [url], {
21
- stdio: "ignore",
22
- timeout: 3000
17
+ const child = spawn(platform === "darwin" ? "open" : "xdg-open", [url], {
18
+ stdio: "ignore"
23
19
  });
24
- return !0;
20
+ child.on("error", () => {});
21
+ child.unref();
22
+ return child.pid !== void 0;
25
23
  } catch {
26
24
  return !1;
27
25
  }
@@ -21,6 +21,7 @@ export class LockOwnershipError extends Error {
21
21
  export async function withLock(target, options, fn) {
22
22
  const started = performance.now();
23
23
  for (;;) {
24
+ options.signal?.throwIfAborted();
24
25
  const lock = await acquireRefreshFileLock({
25
26
  path: target,
26
27
  name: options.name,
@@ -33,6 +34,7 @@ export async function withLock(target, options, fn) {
33
34
  });
34
35
  if (lock) {
35
36
  try {
37
+ options.signal?.throwIfAborted();
36
38
  return await fn(lock);
37
39
  }
38
40
  finally {
@@ -11,7 +11,7 @@
11
11
  // no entry gets an absence record. A pair outside the coverage is never
12
12
  // touched, so a header-shaped partial observation (which covers only what it
13
13
  // carries) cannot erase a family limit or the budget.
14
- import { ALL_SCOPE, decodeQuotaMap, entryTime, isQuotaCounters, limitKey, } from "./905bffc0220f43d0-map.js";
14
+ import { ALL_SCOPE, decodeQuotaMap, entryTime, isQuotaCounters, isQuotaCredits, limitKey, } from "./905bffc0220f43d0-map.js";
15
15
  /** Thrown by `mergeQuotaObservation` when either argument is malformed. */
16
16
  export class QuotaCodecError extends TypeError {
17
17
  constructor(message) {
@@ -84,7 +84,8 @@ export function isQuotaObservation(value) {
84
84
  if (!coverage.every(isObservedPair))
85
85
  return false;
86
86
  return ((value.budget === undefined || isObservedBudget(value.budget)) &&
87
- (value.counters === undefined || isQuotaCounters(value.counters)));
87
+ (value.counters === undefined || isQuotaCounters(value.counters)) &&
88
+ (value.credits === undefined || isQuotaCredits(value.credits)));
88
89
  }
89
90
  function compareEntries(left, right) {
90
91
  if (left.scope !== right.scope)
@@ -110,6 +111,17 @@ function mergeCounters(stored, observed) {
110
111
  }
111
112
  return counters.size === 0 ? undefined : Object.fromEntries(counters);
112
113
  }
114
+ // Credits merge like a single counter: the newer record wins, an equal time
115
+ // applies (the observation applied last wins), and an observation without
116
+ // credits leaves the stored record alone.
117
+ function mergeCredits(stored, observed) {
118
+ if (observed === undefined)
119
+ return stored;
120
+ if (stored !== undefined && observed.checkedAt < stored.checkedAt) {
121
+ return stored;
122
+ }
123
+ return { ...observed };
124
+ }
113
125
  /**
114
126
  * Applies `observation` to `stored` (undefined for a row with no map yet) and
115
127
  * returns the new map. Pure: neither argument is modified. Throws
@@ -166,13 +178,15 @@ export function mergeQuotaObservation(stored, observation) {
166
178
  : { scope, label: pair.label, kind: 'retired', retiredAt: at });
167
179
  }
168
180
  // Keys this module does not recognise are carried over untouched.
169
- const { limits: _limits, budget: _budget, counters: _counters, ...unknownKeys } = base;
181
+ const { limits: _limits, budget: _budget, counters: _counters, credits: _credits, ...unknownKeys } = base;
170
182
  const budget = mergeBudget(base.budget, observation.budget, at);
171
183
  const counters = mergeCounters(base.counters, observation.counters);
184
+ const credits = mergeCredits(base.credits, observation.credits);
172
185
  return {
173
186
  ...unknownKeys,
174
187
  limits: [...entries.values()].sort(compareEntries),
175
188
  ...(budget === undefined ? {} : { budget }),
176
189
  ...(counters === undefined ? {} : { counters }),
190
+ ...(credits === undefined ? {} : { credits }),
177
191
  };
178
192
  }
@@ -0,0 +1,253 @@
1
+ import { resolve } from "node:path";
2
+ import { createLogger } from "./cc9e0702a752cea7-index.js";
3
+ import { isQuotaObservation } from "./d5a8f1061ee2f114-index.js";
4
+ import { applyVaultQuota, mutateVaultRoster, } from "./30f5bc480f31f51c-roster.js";
5
+ // Every consumer of the same file shares one mailbox and one lock waiter.
6
+ const writers = new Map();
7
+ const logger = createLogger('claustrum');
8
+ const COALESCE_MS = 100;
9
+ function writerFor(path) {
10
+ const key = resolve(path);
11
+ let writer = writers.get(key);
12
+ if (!writer) {
13
+ writer = {
14
+ path: key,
15
+ pending: new Map(),
16
+ listeners: new Set(),
17
+ wake: new Set(),
18
+ abort: new AbortController(),
19
+ failures: 0,
20
+ draining: false,
21
+ order: 0,
22
+ };
23
+ writers.set(key, writer);
24
+ }
25
+ return writer;
26
+ }
27
+ /** Retain the shared writer; the last consumer's close drains it. */
28
+ export function retainVaultQuotaWriter(path, listener) {
29
+ const writer = writerFor(path);
30
+ writer.listeners.add(listener);
31
+ let released;
32
+ return () => (released ??= (() => {
33
+ writer.listeners.delete(listener);
34
+ return writer.listeners.size ? Promise.resolve() : drainVaultQuota(path);
35
+ })());
36
+ }
37
+ // Split one observation into its independently dated parts: each rate-limit
38
+ // window (scope and label), each covered-but-unread window, the budget, each
39
+ // counter and the credits record. The mailbox keeps the newest of each part on
40
+ // its own, so a later observation that carries only some parts can't erase an
41
+ // older but still newest reading of another part, which is how
42
+ // mergeQuotaObservation treats them on disk. Each key also carries the
43
+ // receipt's identity, so the identity fence still applies to every part.
44
+ function fields(publication) {
45
+ const result = new Map();
46
+ const { observation } = publication;
47
+ const witness = JSON.stringify([
48
+ publication.routeId,
49
+ publication.receipt?.credentialId,
50
+ publication.receipt?.accountIdentitySource,
51
+ publication.receipt?.expectedAccountIdentity,
52
+ ]);
53
+ const add = (key, at, part) => result.set(`${witness}:${key}`, {
54
+ at,
55
+ order: 0,
56
+ publication: { ...publication, observation: part },
57
+ });
58
+ const keyFor = (scope, label) => JSON.stringify([scope ?? 'all', label]);
59
+ const read = new Set();
60
+ for (const reading of observation.readings ?? []) {
61
+ const key = keyFor(reading.scope, reading.label);
62
+ read.add(key);
63
+ add(key, observation.checkedAt, {
64
+ checkedAt: observation.checkedAt,
65
+ readings: [{ ...reading }],
66
+ });
67
+ }
68
+ for (const pair of observation.coverage ?? []) {
69
+ const key = keyFor(pair.scope, pair.label);
70
+ if (!read.has(key))
71
+ add(key, observation.checkedAt, {
72
+ checkedAt: observation.checkedAt,
73
+ coverage: [{ ...pair }],
74
+ });
75
+ }
76
+ if (observation.budget)
77
+ add('budget', observation.checkedAt, {
78
+ checkedAt: observation.checkedAt,
79
+ budget: { ...observation.budget },
80
+ });
81
+ for (const [name, counter] of Object.entries(observation.counters ?? {})) {
82
+ add(`counter:${name}`, counter.checkedAt, {
83
+ checkedAt: observation.checkedAt,
84
+ counters: { [name]: { ...counter } },
85
+ });
86
+ }
87
+ if (observation.credits)
88
+ add('credits', observation.credits.checkedAt, {
89
+ checkedAt: observation.checkedAt,
90
+ credits: { ...observation.credits },
91
+ });
92
+ // Even an empty observation can bind an account proved by its receipt.
93
+ if (!result.size)
94
+ add('empty', observation.checkedAt, { checkedAt: observation.checkedAt });
95
+ return result;
96
+ }
97
+ /** Enqueue without filesystem work; each quota field retains its own receipt fence. */
98
+ export function enqueueVaultQuota(path, publication) {
99
+ if (!isQuotaObservation(publication.observation))
100
+ return Promise.resolve(false);
101
+ const writer = writerFor(path);
102
+ const account = JSON.stringify([
103
+ publication.accountIdentity ?? null,
104
+ publication.accountIdentity === undefined ? publication.routeId : null,
105
+ ]);
106
+ const pending = writer.pending.get(account) ?? new Map();
107
+ for (const [key, field] of fields(publication)) {
108
+ const previous = pending.get(key);
109
+ if (!previous || field.at >= previous.at) {
110
+ field.order = ++writer.order;
111
+ // A covered disappearance after an unflushed reading is a retirement,
112
+ // not an absence. Retain just enough history to preserve that distinction.
113
+ if (field.publication.observation.coverage)
114
+ field.priorReading = previous?.publication.observation.readings
115
+ ? previous.publication
116
+ : previous?.priorReading;
117
+ pending.set(key, field);
118
+ }
119
+ }
120
+ if (pending.size)
121
+ writer.pending.set(account, pending);
122
+ for (const listener of writer.listeners)
123
+ listener(publication);
124
+ schedule(writer, COALESCE_MS);
125
+ return Promise.resolve(true);
126
+ }
127
+ function orderedFields(pending) {
128
+ // Equal observation times are last-enqueue-wins even across different
129
+ // credentials or account-only readings. Map insertion order cannot prove it.
130
+ return [...pending.values()]
131
+ .flatMap((account) => [...account.values()])
132
+ .sort((a, b) => a.order - b.order);
133
+ }
134
+ /** Overlay uncommitted observations when a consumer reloads the durable roster. */
135
+ export function pendingVaultQuota(path, roster) {
136
+ const writer = writers.get(resolve(path));
137
+ if (writer) {
138
+ for (const field of orderedFields(writer.pending)) {
139
+ if (field.priorReading)
140
+ roster = applyVaultQuota(roster, field.priorReading).roster;
141
+ roster = applyVaultQuota(roster, field.publication).roster;
142
+ }
143
+ }
144
+ return roster;
145
+ }
146
+ function schedule(writer, delay) {
147
+ if (writer.running ||
148
+ writer.timer ||
149
+ !writer.pending.size ||
150
+ writer.abort.signal.aborted)
151
+ return;
152
+ writer.timer = setTimeout(() => {
153
+ writer.timer = undefined;
154
+ flush(writer);
155
+ }, delay);
156
+ if (!writer.draining)
157
+ writer.timer.unref?.();
158
+ }
159
+ function flush(writer) {
160
+ if (writer.running || writer.abort.signal.aborted)
161
+ return;
162
+ const batch = new Map([...writer.pending].map(([account, pending]) => [
163
+ account,
164
+ new Map(pending),
165
+ ]));
166
+ writer.running = (async () => {
167
+ try {
168
+ await mutateVaultRoster(writer.path, (current) => {
169
+ let next = current;
170
+ for (const field of orderedFields(batch)) {
171
+ if (field.priorReading)
172
+ next = applyVaultQuota(next, field.priorReading).roster;
173
+ next = applyVaultQuota(next, field.publication).roster;
174
+ }
175
+ return { ...(next !== current && { next }), result: undefined };
176
+ }, { signal: writer.abort.signal });
177
+ for (const [account, fields] of batch) {
178
+ const pending = writer.pending.get(account);
179
+ for (const [key, field] of fields) {
180
+ // A newer enqueue must survive completion of an older flush.
181
+ if (pending?.get(key) === field)
182
+ pending.delete(key);
183
+ }
184
+ if (!pending?.size)
185
+ writer.pending.delete(account);
186
+ }
187
+ writer.failures = 0;
188
+ }
189
+ catch (error) {
190
+ if (!writer.abort.signal.aborted && writer.failures++ === 0) {
191
+ logger.warn('vault quota publication delayed; retrying', {
192
+ error: error instanceof Error ? error.message : String(error),
193
+ });
194
+ }
195
+ // Keep the batch in the mailbox, including any fresher fields enqueued meanwhile.
196
+ }
197
+ finally {
198
+ writer.running = undefined;
199
+ for (const wake of writer.wake)
200
+ wake();
201
+ const backoff = writer.failures
202
+ ? Math.min(5_000, 100 * 2 ** Math.min(writer.failures, 6)) *
203
+ (0.5 + Math.random())
204
+ : COALESCE_MS;
205
+ schedule(writer, backoff);
206
+ }
207
+ })();
208
+ }
209
+ /** Wait at most timeoutMs for detached publication; shutdown loss is acceptable. */
210
+ export async function drainVaultQuota(path, timeoutMs = 1_000) {
211
+ const key = resolve(path);
212
+ const writer = writers.get(key);
213
+ if (!writer)
214
+ return;
215
+ writer.draining = true;
216
+ clearTimeout(writer.timer);
217
+ writer.timer = undefined;
218
+ let deadline;
219
+ let wake;
220
+ const drained = new Promise((done) => {
221
+ wake = () => {
222
+ if (!writer.pending.size || writer.abort.signal.aborted)
223
+ done();
224
+ };
225
+ writer.wake.add(wake);
226
+ wake();
227
+ });
228
+ try {
229
+ if (writer.pending.size)
230
+ flush(writer);
231
+ await Promise.race([
232
+ drained,
233
+ new Promise((done) => {
234
+ deadline = setTimeout(done, Math.max(0, timeoutMs));
235
+ }),
236
+ ]);
237
+ }
238
+ finally {
239
+ clearTimeout(deadline);
240
+ if (wake)
241
+ writer.wake.delete(wake);
242
+ writer.draining = false;
243
+ // A still-open consumer owns the mailbox, so another consumer's disposal
244
+ // must not discard its observations or stop its retries.
245
+ if (!writer.listeners.size) {
246
+ writer.abort.abort();
247
+ clearTimeout(writer.timer);
248
+ writer.pending.clear();
249
+ if (writers.get(key) === writer)
250
+ writers.delete(key);
251
+ }
252
+ }
253
+ }
@@ -14,6 +14,9 @@
14
14
  // reset clock. It is a tri-state: absent (never reported), a reading, or
15
15
  // cleared (the provider reported that no budget exists).
16
16
  //
17
+ // Provider-named counters and the prepaid credits record sit beside them too,
18
+ // each judged by its own observation time rather than the limits'.
19
+ //
17
20
  // The map is plain JSON so the store can persist it without interpreting it.
18
21
  /** The scope of a limit that binds every request regardless of model. */
19
22
  export const ALL_SCOPE = 'all';
@@ -93,6 +96,18 @@ function isQuotaCounter(value) {
93
96
  export function isQuotaCounters(value) {
94
97
  return isRecord(value) && Object.values(value).every(isQuotaCounter);
95
98
  }
99
+ function isBoolean(value) {
100
+ return typeof value === 'boolean';
101
+ }
102
+ /** True when `value` is a well-formed prepaid-credits record. */
103
+ export function isQuotaCredits(value) {
104
+ return (isRecord(value) &&
105
+ typeof value.hasCredits === 'boolean' &&
106
+ typeof value.unlimited === 'boolean' &&
107
+ optional(value, 'balance', isFiniteNumber) &&
108
+ optional(value, 'overageLimitReached', isBoolean) &&
109
+ isFiniteNumber(value.checkedAt));
110
+ }
96
111
  /** Keep usable counters without letting one malformed count hide a quota map. */
97
112
  export function readQuotaCounters(value) {
98
113
  if (!isRecord(value))
@@ -102,20 +117,29 @@ export function readQuotaCounters(value) {
102
117
  ? undefined
103
118
  : Object.fromEntries(entries);
104
119
  }
105
- /** Decode persisted quota, dropping malformed counters but not limit evidence. */
120
+ /**
121
+ * Decode persisted quota, dropping malformed counters or a malformed credits
122
+ * record but not limit or budget evidence.
123
+ */
106
124
  export function decodeQuotaMap(value) {
107
125
  // A valid map is returned as the same object, so stored quota keeps the
108
- // identity it had before counters existed; only a map with a malformed
109
- // counter is copied without it.
126
+ // identity it had before counters and credits existed. A map with a
127
+ // malformed counter or credits record is copied with those malformed
128
+ // fields left out.
110
129
  if (isQuotaMap(value))
111
130
  return value;
112
131
  if (!isRecord(value))
113
132
  return undefined;
114
- const { counters: rawCounters, ...rest } = value;
133
+ const { counters: rawCounters, credits: rawCredits, ...rest } = value;
115
134
  if (!isQuotaMap(rest))
116
135
  return undefined;
117
136
  const counters = readQuotaCounters(rawCounters);
118
- return { ...rest, ...(counters === undefined ? {} : { counters }) };
137
+ const credits = isQuotaCredits(rawCredits) ? rawCredits : undefined;
138
+ return {
139
+ ...rest,
140
+ ...(counters === undefined ? {} : { counters }),
141
+ ...(credits === undefined ? {} : { credits }),
142
+ };
119
143
  }
120
144
  /**
121
145
  * True when `value` is a well-formed quota map. Unrecognised top-level keys
@@ -136,5 +160,6 @@ export function isQuotaMap(value) {
136
160
  seen.add(key);
137
161
  }
138
162
  return (optional(value, 'budget', isCreditBudgetEntry) &&
139
- optional(value, 'counters', isQuotaCounters));
163
+ optional(value, 'counters', isQuotaCounters) &&
164
+ optional(value, 'credits', isQuotaCredits));
140
165
  }
@@ -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
+ }