@kunkun.sh/kuota 0.0.0-stage → 0.1.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.
@@ -0,0 +1,576 @@
1
+ import { Schema } from "effect";
2
+ // ../core/dist/index.mjs
3
+ const ProviderIdSchema = Schema.Literal("codex", "claude", "antigravity");
4
+ const QuotaStatusSchema = Schema.Literal("ok", "partial", "unauthenticated", "unavailable", "unsupported", "error");
5
+ const QuotaWindowSchema = Schema.Struct({
6
+ windowId: Schema.String,
7
+ usedPercent: Schema.NullOr(Schema.Number),
8
+ remainingPercent: Schema.NullOr(Schema.Number),
9
+ resetAt: Schema.NullOr(Schema.String)
10
+ });
11
+ Schema.Struct({
12
+ provider: ProviderIdSchema,
13
+ status: QuotaStatusSchema,
14
+ fetchedAt: Schema.String,
15
+ source: Schema.String,
16
+ windows: Schema.Array(QuotaWindowSchema),
17
+ errorCode: Schema.NullOr(Schema.String)
18
+ });
19
+ function safePercent(value) {
20
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 100 ? value : null;
21
+ }
22
+ function safeIsoDate(value) {
23
+ if (typeof value !== "string" && typeof value !== "number") return null;
24
+ const date = new Date(value);
25
+ return Number.isFinite(date.getTime()) ? date.toISOString() : null;
26
+ }
27
+ function normalizeWindow(input) {
28
+ const usedPercent = safePercent(input.usedPercent);
29
+ return {
30
+ windowId: input.windowId,
31
+ usedPercent,
32
+ remainingPercent: usedPercent === null ? null : Number((100 - usedPercent).toFixed(2)),
33
+ resetAt: safeIsoDate(input.resetAt)
34
+ };
35
+ }
36
+ function makeUnavailableSnapshot(provider, errorCode, status = "unavailable") {
37
+ return {
38
+ provider,
39
+ status,
40
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
41
+ source: "local-probe",
42
+ windows: [],
43
+ errorCode
44
+ };
45
+ }
46
+ /** Returns null when the provider has not proved a comparable fixed window. */
47
+ function calculatePace(input) {
48
+ if (input.semantics === "rolling" || input.semantics === "unknown" || input.startSource === "unknown" || input.semantics === "first_use" && input.startSource !== "provider" || input.startAt === null || input.resetAt === null || input.usedPercent === null || !Number.isFinite(input.usedPercent) || input.usedPercent < 0 || input.usedPercent > 100) return null;
49
+ const now = input.now.getTime();
50
+ const start = input.startAt.getTime();
51
+ const reset = input.resetAt.getTime();
52
+ if (!Number.isFinite(now) || !Number.isFinite(start) || !Number.isFinite(reset) || reset <= start || now < start) return null;
53
+ const elapsedFraction = Math.min(1, (now - start) / (reset - start));
54
+ const idealUsedPercent = elapsedFraction * 100;
55
+ return {
56
+ elapsedFraction,
57
+ idealUsedPercent,
58
+ idealRemainingPercent: 100 - idealUsedPercent,
59
+ actualUsedPercent: input.usedPercent,
60
+ paceDeltaPoints: input.usedPercent - idealUsedPercent,
61
+ paceRatio: idealUsedPercent === 0 ? null : input.usedPercent / idealUsedPercent,
62
+ remainingHours: Math.max(0, reset - now) / 36e5,
63
+ quality: input.startSource === "provider" ? "exact" : "inferred"
64
+ };
65
+ }
66
+ function iso(ms) {
67
+ return Number.isFinite(ms) ? new Date(ms).toISOString() : (/* @__PURE__ */ new Date(0)).toISOString();
68
+ }
69
+ /**
70
+ * Pure constructor. If `snapshot.fetchedAt` is not a valid timestamp we fall back to the
71
+ * injected `checkedAtMs` as the observation time, so no NaN / Invalid Date is ever emitted.
72
+ */
73
+ function makeObservation(input) {
74
+ const parsed = Date.parse(input.snapshot.fetchedAt);
75
+ const observedMs = Number.isFinite(parsed) ? parsed : input.checkedAtMs;
76
+ const next = input.nextAttemptAtMs;
77
+ return {
78
+ snapshot: input.snapshot,
79
+ freshness: input.freshness,
80
+ observedAt: iso(observedMs),
81
+ checkedAt: iso(input.checkedAtMs),
82
+ ageMs: Math.max(0, input.nowMs - observedMs),
83
+ nextAttemptAt: next === null || next === void 0 || !Number.isFinite(next) ? null : iso(next),
84
+ latestErrorCode: input.latestErrorCode ?? null
85
+ };
86
+ }
87
+ const K = new Uint32Array([
88
+ 1116352408,
89
+ 1899447441,
90
+ 3049323471,
91
+ 3921009573,
92
+ 961987163,
93
+ 1508970993,
94
+ 2453635748,
95
+ 2870763221,
96
+ 3624381080,
97
+ 310598401,
98
+ 607225278,
99
+ 1426881987,
100
+ 1925078388,
101
+ 2162078206,
102
+ 2614888103,
103
+ 3248222580,
104
+ 3835390401,
105
+ 4022224774,
106
+ 264347078,
107
+ 604807628,
108
+ 770255983,
109
+ 1249150122,
110
+ 1555081692,
111
+ 1996064986,
112
+ 2554220882,
113
+ 2821834349,
114
+ 2952996808,
115
+ 3210313671,
116
+ 3336571891,
117
+ 3584528711,
118
+ 113926993,
119
+ 338241895,
120
+ 666307205,
121
+ 773529912,
122
+ 1294757372,
123
+ 1396182291,
124
+ 1695183700,
125
+ 1986661051,
126
+ 2177026350,
127
+ 2456956037,
128
+ 2730485921,
129
+ 2820302411,
130
+ 3259730800,
131
+ 3345764771,
132
+ 3516065817,
133
+ 3600352804,
134
+ 4094571909,
135
+ 275423344,
136
+ 430227734,
137
+ 506948616,
138
+ 659060556,
139
+ 883997877,
140
+ 958139571,
141
+ 1322822218,
142
+ 1537002063,
143
+ 1747873779,
144
+ 1955562222,
145
+ 2024104815,
146
+ 2227730452,
147
+ 2361852424,
148
+ 2428436474,
149
+ 2756734187,
150
+ 3204031479,
151
+ 3329325298
152
+ ]);
153
+ const rotr = (x, n) => x >>> n | x << 32 - n;
154
+ /** Dependency-free SHA-256 (hex). Keeps @kuota/core free of node:crypto so it stays browser-safe. */
155
+ function sha256Hex(text) {
156
+ const data = new TextEncoder().encode(text);
157
+ const bitLength = data.length * 8;
158
+ const padded = new Uint8Array(data.length + 9 + 63 >> 6 << 6);
159
+ padded.set(data);
160
+ padded[data.length] = 128;
161
+ const view = new DataView(padded.buffer);
162
+ view.setUint32(padded.length - 8, Math.floor(bitLength / 2 ** 32));
163
+ view.setUint32(padded.length - 4, bitLength >>> 0);
164
+ const h = new Uint32Array([
165
+ 1779033703,
166
+ 3144134277,
167
+ 1013904242,
168
+ 2773480762,
169
+ 1359893119,
170
+ 2600822924,
171
+ 528734635,
172
+ 1541459225
173
+ ]);
174
+ const w = /* @__PURE__ */ new Uint32Array(64);
175
+ for (let offset = 0; offset < padded.length; offset += 64) {
176
+ for (let i = 0; i < 16; i++) w[i] = view.getUint32(offset + i * 4);
177
+ for (let i = 16; i < 64; i++) {
178
+ const s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ w[i - 15] >>> 3;
179
+ const s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ w[i - 2] >>> 10;
180
+ w[i] = w[i - 16] + s0 + w[i - 7] + s1 >>> 0;
181
+ }
182
+ let [a, b, c, d, e, f, g, hh] = h;
183
+ for (let i = 0; i < 64; i++) {
184
+ const s1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
185
+ const ch = e & f ^ ~e & g;
186
+ const t1 = hh + s1 + ch + K[i] + w[i] >>> 0;
187
+ const t2 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + (a & b ^ a & c ^ b & c) >>> 0;
188
+ hh = g;
189
+ g = f;
190
+ f = e;
191
+ e = d + t1 >>> 0;
192
+ d = c;
193
+ c = b;
194
+ b = a;
195
+ a = t1 + t2 >>> 0;
196
+ }
197
+ const next = [
198
+ a,
199
+ b,
200
+ c,
201
+ d,
202
+ e,
203
+ f,
204
+ g,
205
+ hh
206
+ ];
207
+ for (let i = 0; i < 8; i++) h[i] = h[i] + next[i] >>> 0;
208
+ }
209
+ return Array.from(h, (x) => x.toString(16).padStart(8, "0")).join("");
210
+ }
211
+ const HOUR = 36e5;
212
+ /**
213
+ * Window cadence heuristics, derived from window ids the adapters emit. Only cadences named in
214
+ * the id (5h / weekly / daily) are trusted; everything else stays `unknown` rather than guessed.
215
+ * Codex `primary` / `secondary` follow the documented 5h / weekly convention of its app-server.
216
+ */
217
+ function windowProfile(provider, windowId) {
218
+ if (provider === "codex") {
219
+ if (windowId === "primary") return {
220
+ semantics: "fixed",
221
+ durationMs: 5 * HOUR
222
+ };
223
+ if (windowId === "secondary") return {
224
+ semantics: "fixed",
225
+ durationMs: 168 * HOUR
226
+ };
227
+ }
228
+ const id = windowId.toLowerCase();
229
+ const has = (re) => re.test(id);
230
+ if (has(/(^|[.:_-])(5h|five-hour)([.:_-]|$)/)) return {
231
+ semantics: "fixed",
232
+ durationMs: 5 * HOUR
233
+ };
234
+ if (has(/(^|[.:_-])(weekly|7d|seven-day)([.:_-]|$)/)) return {
235
+ semantics: "fixed",
236
+ durationMs: 168 * HOUR
237
+ };
238
+ if (has(/(^|[.:_-])(daily|24h)([.:_-]|$)/)) return {
239
+ semantics: "fixed",
240
+ durationMs: 24 * HOUR
241
+ };
242
+ return {
243
+ semantics: "unknown",
244
+ durationMs: null
245
+ };
246
+ }
247
+ const IsoDate = Schema.String.pipe(Schema.filter((value) => Number.isFinite(Date.parse(value)) || "must be an ISO timestamp"));
248
+ const Percent = Schema.Number.pipe(Schema.filter((value) => Number.isFinite(value) && value >= 0 && value <= 100 || "0..100"));
249
+ const QuotaHistoryEventV1Schema = Schema.Struct({
250
+ schemaVersion: Schema.Literal(1),
251
+ kind: Schema.Literal("quota_observed", "quota_unavailable", "usage_observed"),
252
+ eventId: Schema.String,
253
+ observedAt: IsoDate,
254
+ recordedAt: IsoDate,
255
+ provider: Schema.String,
256
+ accountScope: Schema.NullOr(Schema.String),
257
+ deviceId: Schema.String,
258
+ windowId: Schema.String,
259
+ epochId: Schema.NullOr(Schema.String),
260
+ semantics: Schema.Literal("fixed", "rolling", "first_use", "unknown"),
261
+ startAt: Schema.NullOr(IsoDate),
262
+ resetAt: Schema.NullOr(IsoDate),
263
+ usedPercent: Schema.NullOr(Percent),
264
+ remainingPercent: Schema.NullOr(Percent),
265
+ source: Schema.String,
266
+ state: Schema.Literal("ok", "partial", "stale", "error", "unauthenticated"),
267
+ errorCode: Schema.NullOr(Schema.String),
268
+ usage: Schema.optional(Schema.Struct({
269
+ inputTokens: Schema.optional(Schema.Number),
270
+ outputTokens: Schema.optional(Schema.Number),
271
+ cacheReadTokens: Schema.optional(Schema.Number),
272
+ cacheWriteTokens: Schema.optional(Schema.Number),
273
+ model: Schema.optional(Schema.String),
274
+ estimatedUsd: Schema.optional(Schema.Number)
275
+ }))
276
+ });
277
+ const decode = Schema.decodeUnknownEither(QuotaHistoryEventV1Schema, { onExcessProperty: "error" });
278
+ /** Strict decode: unknown (possibly secret-bearing) fields are rejected, not silently kept. */
279
+ function decodeHistoryEvent(input) {
280
+ return decode(input);
281
+ }
282
+ const validPercent = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0 && v <= 100;
283
+ const minuteIso = (ms) => (/* @__PURE__ */ new Date(Math.round(ms / 6e4) * 6e4)).toISOString();
284
+ function makeId(parts) {
285
+ return sha256Hex(parts.map((part) => part === null ? "\0" : String(part)).join(""));
286
+ }
287
+ /** Pure normalization: one event per measurable window, or one sanitized unavailable event. */
288
+ function historyEventsFromSnapshot(snapshot, options) {
289
+ const observedMs = Date.parse(snapshot.fetchedAt);
290
+ if (!Number.isFinite(observedMs)) return [];
291
+ const observedAt = new Date(observedMs).toISOString();
292
+ const base = {
293
+ schemaVersion: 1,
294
+ observedAt,
295
+ recordedAt: options.recordedAt,
296
+ provider: snapshot.provider,
297
+ accountScope: options.accountScope,
298
+ deviceId: options.deviceId,
299
+ source: snapshot.source
300
+ };
301
+ const measurable = snapshot.windows.filter((w) => validPercent(w.usedPercent));
302
+ if (!(snapshot.status === "ok" || snapshot.status === "partial") || measurable.length === 0) {
303
+ const state = snapshot.status === "unauthenticated" ? "unauthenticated" : "error";
304
+ const errorCode = snapshot.errorCode ?? snapshot.status.toUpperCase();
305
+ return [{
306
+ ...base,
307
+ kind: "quota_unavailable",
308
+ eventId: makeId([
309
+ "quota_unavailable",
310
+ snapshot.provider,
311
+ options.accountScope,
312
+ options.deviceId,
313
+ observedAt,
314
+ state,
315
+ errorCode
316
+ ]),
317
+ windowId: "*",
318
+ epochId: null,
319
+ semantics: "unknown",
320
+ startAt: null,
321
+ resetAt: null,
322
+ usedPercent: null,
323
+ remainingPercent: null,
324
+ state,
325
+ errorCode
326
+ }];
327
+ }
328
+ const state = options.stale ? "stale" : snapshot.status === "partial" ? "partial" : "ok";
329
+ return measurable.map((window) => {
330
+ const profile = windowProfile(snapshot.provider, window.windowId);
331
+ const semantics = options.semanticsForWindow?.(window.windowId) ?? profile.semantics;
332
+ const resetMs = window.resetAt === null ? NaN : Date.parse(window.resetAt);
333
+ const hasReset = Number.isFinite(resetMs);
334
+ const epochId = hasReset ? minuteIso(resetMs) : null;
335
+ const providedStart = options.startForWindow?.(window.windowId) ?? null;
336
+ const startAt = providedStart !== null && Number.isFinite(Date.parse(providedStart)) ? new Date(providedStart).toISOString() : hasReset && profile.durationMs !== null ? new Date(resetMs - profile.durationMs).toISOString() : null;
337
+ const usedPercent = window.usedPercent;
338
+ const remaining = validPercent(window.remainingPercent) ? window.remainingPercent : Math.max(0, Math.min(100, 100 - usedPercent));
339
+ return {
340
+ ...base,
341
+ kind: "quota_observed",
342
+ eventId: makeId([
343
+ "quota_observed",
344
+ snapshot.provider,
345
+ options.accountScope,
346
+ options.deviceId,
347
+ window.windowId,
348
+ epochId,
349
+ observedAt,
350
+ usedPercent,
351
+ state
352
+ ]),
353
+ windowId: window.windowId,
354
+ epochId,
355
+ semantics,
356
+ startAt,
357
+ resetAt: hasReset ? new Date(resetMs).toISOString() : null,
358
+ usedPercent,
359
+ remainingPercent: remaining,
360
+ state,
361
+ errorCode: snapshot.status === "partial" ? snapshot.errorCode : null
362
+ };
363
+ });
364
+ }
365
+ const H = 36e5;
366
+ const EPOCH_TOLERANCE_MS = 6e5;
367
+ const DECREASE_TOLERANCE = .5;
368
+ const GAP_CAP_HOURS = 6;
369
+ const RECENT_HOURS = 24;
370
+ const STALE_LATEST_MS = 6 * H;
371
+ const SLOPE_EPSILON = 1e-6;
372
+ const LEVELS = [
373
+ "low",
374
+ "medium",
375
+ "high"
376
+ ];
377
+ const ms = (iso) => iso === null ? NaN : Date.parse(iso);
378
+ const boundary = (e) => {
379
+ const reset = ms(e.resetAt);
380
+ return Number.isFinite(reset) ? reset : ms(e.epochId);
381
+ };
382
+ function isMeasurable(e) {
383
+ return e.kind === "quota_observed" && (e.state === "ok" || e.state === "partial" || e.state === "stale") && e.usedPercent !== null && Number.isFinite(e.usedPercent) && Number.isFinite(ms(e.observedAt));
384
+ }
385
+ /**
386
+ * Reset/epoch detection. A new segment starts when the provider-reported reset boundary moves
387
+ * (beyond jitter tolerance) or when used% drops (credit / unannounced reset). Fixed windows only
388
+ * ever compare points inside one segment.
389
+ */
390
+ function detectEpochs(events) {
391
+ const sorted = events.filter(isMeasurable).map((e, i) => ({
392
+ e,
393
+ i
394
+ })).sort((a, b) => ms(a.e.observedAt) - ms(b.e.observedAt) || a.i - b.i).map(({ e }) => e);
395
+ const segments = [];
396
+ let lastTime = NaN;
397
+ for (const event of sorted) {
398
+ const time = ms(event.observedAt);
399
+ if (time === lastTime) continue;
400
+ lastTime = time;
401
+ const current = segments[segments.length - 1];
402
+ const prev = current?.events[current.events.length - 1];
403
+ if (!current || !prev) {
404
+ segments.push({
405
+ events: [event],
406
+ reason: null
407
+ });
408
+ continue;
409
+ }
410
+ const a = boundary(prev);
411
+ const b = boundary(event);
412
+ if (Number.isFinite(a) && Number.isFinite(b) && Math.abs(a - b) > EPOCH_TOLERANCE_MS) segments.push({
413
+ events: [event],
414
+ reason: "EPOCH_RESET_DETECTED"
415
+ });
416
+ else if (event.usedPercent < prev.usedPercent - DECREASE_TOLERANCE) segments.push({
417
+ events: [event],
418
+ reason: "USAGE_DECREASE_SPLIT"
419
+ });
420
+ else current.events.push(event);
421
+ }
422
+ return segments;
423
+ }
424
+ function weightedMedian(items) {
425
+ const sorted = [...items].sort((a, b) => a.value - b.value);
426
+ const total = sorted.reduce((sum, item) => sum + item.weight, 0);
427
+ let acc = 0;
428
+ for (const item of sorted) {
429
+ acc += item.weight;
430
+ if (acc >= total / 2) return item.value;
431
+ }
432
+ return 0;
433
+ }
434
+ const unsupported = (reasons) => ({
435
+ slopePercentPerHour: null,
436
+ exhaustedAt: null,
437
+ projectedUsedAtResetPercent: null,
438
+ sampleCount: 0,
439
+ observedSpanHours: 0,
440
+ quality: "unsupported",
441
+ reasons
442
+ });
443
+ function forecastQuota(input) {
444
+ const segments = detectEpochs(input.observations);
445
+ const last = segments[segments.length - 1];
446
+ if (!last) return {
447
+ ...unsupported([]),
448
+ quality: "insufficient",
449
+ reasons: ["NO_OBSERVATIONS"]
450
+ };
451
+ const latest = last.events[last.events.length - 1];
452
+ if (latest.semantics === "rolling") return unsupported(["ROLLING_WINDOW_UNMODELED"]);
453
+ if (latest.semantics === "unknown") return unsupported(["WINDOW_SEMANTICS_UNKNOWN"]);
454
+ const reasons = [];
455
+ if (last.reason) reasons.push(last.reason);
456
+ let samples = last.events;
457
+ const latestMs = ms(latest.observedAt);
458
+ if (input.mode === "recent-activity") samples = samples.filter((e) => ms(e.observedAt) >= latestMs - RECENT_HOURS * H);
459
+ const first = samples[0];
460
+ const spanHours = (latestMs - ms(first.observedAt)) / H;
461
+ const used = latest.usedPercent;
462
+ const base = {
463
+ sampleCount: samples.length,
464
+ observedSpanHours: spanHours
465
+ };
466
+ if (samples.length < 3 || spanHours < 2) return {
467
+ ...base,
468
+ slopePercentPerHour: null,
469
+ exhaustedAt: used >= 100 ? latest.observedAt : null,
470
+ projectedUsedAtResetPercent: null,
471
+ quality: "insufficient",
472
+ reasons: [
473
+ ...reasons,
474
+ "INSUFFICIENT_DATA",
475
+ ...used >= 100 ? ["ALREADY_EXHAUSTED"] : []
476
+ ]
477
+ };
478
+ const intervals = [];
479
+ let changes = 0;
480
+ for (let i = 1; i < samples.length; i++) {
481
+ const dt = (ms(samples[i].observedAt) - ms(samples[i - 1].observedAt)) / H;
482
+ const du = samples[i].usedPercent - samples[i - 1].usedPercent;
483
+ if (dt <= 0 || du < 0) continue;
484
+ if (du > 0) changes++;
485
+ intervals.push({
486
+ value: du / dt,
487
+ weight: Math.min(dt, GAP_CAP_HOURS)
488
+ });
489
+ }
490
+ const slope = input.mode === "all-window" ? (used - first.usedPercent) / spanHours : weightedMedian(intervals);
491
+ const positive = intervals.filter((i) => i.value > 0).map((i) => i.value);
492
+ const mean = positive.reduce((a, b) => a + b, 0) / (positive.length || 1);
493
+ const variance = positive.reduce((a, b) => a + (b - mean) ** 2, 0) / (positive.length || 1);
494
+ const lowVariability = mean > 0 && Math.sqrt(variance) / mean < .5;
495
+ let level = 0;
496
+ if (spanHours >= 24 && used >= 10) level = 1;
497
+ if (spanHours >= 72 && changes >= 20 && lowVariability && used >= 10) level = 2;
498
+ else if (spanHours >= 24 && changes < 6) level = 0;
499
+ if (samples.some((s) => s.state === "stale")) {
500
+ reasons.push("STALE_INPUT");
501
+ level = Math.max(0, level - 1);
502
+ }
503
+ if (input.now.getTime() - latestMs > STALE_LATEST_MS) {
504
+ reasons.push("LATEST_SAMPLE_OLD");
505
+ level = Math.max(0, level - 1);
506
+ }
507
+ const quality = LEVELS[level];
508
+ const resetMs = input.resetAt ? input.resetAt.getTime() : boundary(latest);
509
+ const hasReset = Number.isFinite(resetMs);
510
+ const roundedSlope = Math.max(0, slope);
511
+ let exhaustedAt = null;
512
+ let projected = null;
513
+ if (used >= 100) {
514
+ exhaustedAt = latest.observedAt;
515
+ projected = 100;
516
+ reasons.push("ALREADY_EXHAUSTED");
517
+ } else if (roundedSlope <= SLOPE_EPSILON) {
518
+ reasons.push("STABLE_AT_CURRENT_ACTIVITY");
519
+ projected = used;
520
+ } else {
521
+ const exhaustMs = latestMs + (100 - used) / roundedSlope * H;
522
+ projected = Math.min(100, used + roundedSlope * (hasReset ? Math.max(0, resetMs - latestMs) / H : 0));
523
+ if (!hasReset || exhaustMs <= resetMs) {
524
+ exhaustedAt = new Date(exhaustMs).toISOString();
525
+ reasons.push("EXHAUSTION_BEFORE_RESET");
526
+ } else reasons.push("ON_PACE");
527
+ if (!hasReset) projected = null;
528
+ }
529
+ return {
530
+ ...base,
531
+ slopePercentPerHour: roundedSlope,
532
+ exhaustedAt,
533
+ projectedUsedAtResetPercent: projected,
534
+ quality,
535
+ reasons
536
+ };
537
+ }
538
+ /** Account-safe wrapper: only events sharing the newest event's accountScope are considered. */
539
+ function buildForecastV1(input) {
540
+ const scoped = input.observations.filter((e) => e.provider === input.provider && e.windowId === input.windowId && isMeasurable(e)).sort((a, b) => ms(a.observedAt) - ms(b.observedAt));
541
+ const newest = scoped[scoped.length - 1];
542
+ const events = newest ? scoped.filter((e) => e.accountScope === newest.accountScope) : [];
543
+ const result = forecastQuota({
544
+ now: input.now,
545
+ resetAt: null,
546
+ observations: events,
547
+ mode: input.mode
548
+ });
549
+ const latest = events[events.length - 1];
550
+ const pace = latest ? calculatePace({
551
+ now: input.now,
552
+ startAt: latest.startAt ? new Date(latest.startAt) : null,
553
+ resetAt: latest.resetAt ? new Date(latest.resetAt) : null,
554
+ usedPercent: latest.usedPercent,
555
+ semantics: latest.semantics,
556
+ startSource: latest.startAt ? "inferred" : "unknown"
557
+ }) : null;
558
+ return {
559
+ schemaVersion: 1,
560
+ provider: input.provider,
561
+ windowId: input.windowId,
562
+ computedAt: input.now.toISOString(),
563
+ currentUsedPercent: latest ? latest.usedPercent : null,
564
+ idealUsedPercent: pace ? pace.idealUsedPercent : null,
565
+ observedSpanHours: result.observedSpanHours,
566
+ sampleCount: result.sampleCount,
567
+ slopePercentPerHour: result.slopePercentPerHour,
568
+ exhaustedAt: result.exhaustedAt,
569
+ projectedUsedAtResetPercent: result.projectedUsedAtResetPercent,
570
+ expectedTimeToLimitHours: result.exhaustedAt ? Math.max(0, (Date.parse(result.exhaustedAt) - input.now.getTime()) / H) : null,
571
+ mode: input.mode,
572
+ quality: result.quality,
573
+ reasons: [...result.reasons]
574
+ };
575
+ }
576
+ export { makeObservation as a, windowProfile as c, historyEventsFromSnapshot as i, calculatePace as n, makeUnavailableSnapshot as o, decodeHistoryEvent as r, normalizeWindow as s, buildForecastV1 as t };
@@ -0,0 +1,36 @@
1
+ import { c as QuotaHistoryEventV1 } from "./index-NHzh2TGn.mjs";
2
+ interface ReadOptions {
3
+ from: string;
4
+ to: string;
5
+ provider?: string;
6
+ windowId?: string;
7
+ }
8
+ interface ReadResult {
9
+ events: QuotaHistoryEventV1[];
10
+ /** Lines that were truncated, not JSON, or failed strict schema validation. */
11
+ invalidLines: number;
12
+ }
13
+ interface JournalStore {
14
+ append(events: readonly QuotaHistoryEventV1[]): Promise<{
15
+ written: number;
16
+ duplicates: number;
17
+ }>;
18
+ read(options: ReadOptions): AsyncIterable<QuotaHistoryEventV1>;
19
+ readAll(options: ReadOptions): Promise<ReadResult>;
20
+ /** Deletes whole UTC-day partitions older than `retentionDays`. Returns files removed. */
21
+ applyRetention(now?: Date): Promise<number>;
22
+ }
23
+ interface JournalOptions {
24
+ directory: string;
25
+ /** Rotate to `events-DATE.N.jsonl` beyond this size. Default 16 MiB. */
26
+ maxFileBytes?: number;
27
+ /** Default 400 days. */
28
+ retentionDays?: number;
29
+ /** fsync after each append batch. Default true. */
30
+ fsync?: boolean;
31
+ lockStaleMs?: number;
32
+ lockTimeoutMs?: number;
33
+ }
34
+ declare function createLocalJsonlJournal(options: JournalOptions): JournalStore;
35
+ declare function defaultHistoryDirectory(env?: NodeJS.ProcessEnv, platform?: string): string;
36
+ export { createLocalJsonlJournal as a, ReadResult as i, JournalStore as n, defaultHistoryDirectory as o, ReadOptions as r, JournalOptions as t };