@cortexkit/common-auth 0.2.4 → 0.2.5
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.
- package/dist/auth-menu/accounts.d.ts +88 -0
- package/dist/auth-menu/accounts.js +401 -0
- package/dist/auth-menu/ansi.d.ts +24 -0
- package/dist/auth-menu/ansi.js +67 -0
- package/dist/auth-menu/confirm.d.ts +7 -0
- package/dist/auth-menu/confirm.js +21 -0
- package/dist/auth-menu/doctor.d.ts +52 -0
- package/dist/auth-menu/doctor.js +81 -0
- package/dist/auth-menu/index.d.ts +17 -1
- package/dist/auth-menu/index.js +12 -2
- package/dist/auth-menu/login.d.ts +47 -0
- package/dist/auth-menu/login.js +63 -0
- package/dist/auth-menu/menu.d.ts +59 -0
- package/dist/auth-menu/menu.js +73 -0
- package/dist/auth-menu/opencode-v1.d.ts +49 -0
- package/dist/auth-menu/opencode-v1.js +47 -0
- package/dist/auth-menu/select.d.ts +21 -0
- package/dist/auth-menu/select.js +153 -0
- package/dist/auth-menu/terminal.d.ts +43 -0
- package/dist/auth-menu/terminal.js +19 -0
- package/dist/cachekeep/index.d.ts +4 -1
- package/dist/cachekeep/index.js +2 -2
- package/dist/cachekeep/manager.d.ts +214 -0
- package/dist/cachekeep/manager.js +424 -0
- package/dist/cachekeep/window.d.ts +13 -0
- package/dist/cachekeep/window.js +33 -0
- package/dist/dump/index.d.ts +90 -1
- package/dist/dump/index.js +292 -2
- package/package.json +1 -1
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import { isWithinCacheKeepWindow } from './window.js';
|
|
2
|
+
const DEFAULT_TTL_MS = 5 * 60_000;
|
|
3
|
+
const DEFAULT_MAX_IDLE_WARM_MS = 60 * 60_000;
|
|
4
|
+
const DEFAULT_MAX_SUBAGENT_IDLE_MS = 30 * 60_000;
|
|
5
|
+
const DEFAULT_TICK_INTERVAL_MS = 60_000;
|
|
6
|
+
const DEFAULT_MAX_TARGETS = 32;
|
|
7
|
+
const DEFAULT_MAX_BYTES = 8 * 1024 * 1024;
|
|
8
|
+
const DEFAULT_WARM_TIMEOUT_MS = 30_000;
|
|
9
|
+
const DEFAULT_BACKOFF_MS = 10 * 60_000;
|
|
10
|
+
function positive(value, fallback) {
|
|
11
|
+
return typeof value === 'number' && Number.isFinite(value) && value > 0
|
|
12
|
+
? value
|
|
13
|
+
: fallback;
|
|
14
|
+
}
|
|
15
|
+
function errorMessage(error) {
|
|
16
|
+
return error instanceof Error ? error.message : String(error);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Keeps idle prompt caches warm: one target per session holding the latest
|
|
20
|
+
* request body, replayed just before the provider's cache would expire.
|
|
21
|
+
*
|
|
22
|
+
* Bounds, in the order they act: the clock window gates capture and warming
|
|
23
|
+
* (targets captured earlier survive outside it); idle caps prune a target its
|
|
24
|
+
* session stopped using (`sustain` lifts only the main-session cap); the
|
|
25
|
+
* target-count and byte caps evict the least recently touched target; a
|
|
26
|
+
* per-target warm cap retires short-lived sessions; and a failed warm backs
|
|
27
|
+
* that target off without touching the others.
|
|
28
|
+
*/
|
|
29
|
+
export class CacheKeepManager {
|
|
30
|
+
targets = new Map();
|
|
31
|
+
adapter;
|
|
32
|
+
now;
|
|
33
|
+
log;
|
|
34
|
+
ttlMs;
|
|
35
|
+
leadMs;
|
|
36
|
+
maxIdleWarmMs;
|
|
37
|
+
maxSubagentIdleMs;
|
|
38
|
+
tickIntervalMs;
|
|
39
|
+
maxTargets;
|
|
40
|
+
maxBytes;
|
|
41
|
+
warmTimeoutMs;
|
|
42
|
+
backoff;
|
|
43
|
+
getWindow;
|
|
44
|
+
getSustain;
|
|
45
|
+
setIntervalImpl;
|
|
46
|
+
clearIntervalImpl;
|
|
47
|
+
timer = null;
|
|
48
|
+
startedAt = null;
|
|
49
|
+
totalBytes = 0;
|
|
50
|
+
tickPromise = null;
|
|
51
|
+
disposed = false;
|
|
52
|
+
abortController = new AbortController();
|
|
53
|
+
constructor(options) {
|
|
54
|
+
this.adapter = options.adapter;
|
|
55
|
+
this.now = options.now;
|
|
56
|
+
this.log = options.logger;
|
|
57
|
+
this.ttlMs = positive(options.ttlMs, DEFAULT_TTL_MS);
|
|
58
|
+
this.tickIntervalMs = positive(options.tickIntervalMs, DEFAULT_TICK_INTERVAL_MS);
|
|
59
|
+
// One tick of slack so a target that becomes due between ticks is still
|
|
60
|
+
// warmed before its cache expires.
|
|
61
|
+
this.leadMs = positive(options.leadMs, this.tickIntervalMs + 15_000);
|
|
62
|
+
this.maxIdleWarmMs = positive(options.maxIdleWarmMs, DEFAULT_MAX_IDLE_WARM_MS);
|
|
63
|
+
this.maxSubagentIdleMs = positive(options.maxSubagentIdleMs, DEFAULT_MAX_SUBAGENT_IDLE_MS);
|
|
64
|
+
this.maxTargets = positive(options.maxTargets, DEFAULT_MAX_TARGETS);
|
|
65
|
+
this.maxBytes = positive(options.maxBytes, DEFAULT_MAX_BYTES);
|
|
66
|
+
this.warmTimeoutMs = positive(options.warmTimeoutMs, DEFAULT_WARM_TIMEOUT_MS);
|
|
67
|
+
this.backoff = options.backoffMs ?? DEFAULT_BACKOFF_MS;
|
|
68
|
+
this.getWindow = options.getWindow;
|
|
69
|
+
this.getSustain = options.getSustain;
|
|
70
|
+
this.setIntervalImpl = options.setIntervalImpl ?? globalThis.setInterval;
|
|
71
|
+
this.clearIntervalImpl =
|
|
72
|
+
options.clearIntervalImpl ?? globalThis.clearInterval;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Capture a session's latest request. The freshest body replaces the
|
|
76
|
+
* previous one and restarts its cache clock, idle clock and warm count.
|
|
77
|
+
* Arms the timer.
|
|
78
|
+
*/
|
|
79
|
+
track(input) {
|
|
80
|
+
const { sessionKey, bodyText } = input;
|
|
81
|
+
this.pruneStale();
|
|
82
|
+
const bodyBytes = Buffer.byteLength(bodyText, 'utf8');
|
|
83
|
+
if (bodyBytes > this.maxBytes) {
|
|
84
|
+
this.log?.debug('cachekeep track skipped (body exceeds maxBytes)', {
|
|
85
|
+
sessionKey,
|
|
86
|
+
bodyBytes,
|
|
87
|
+
maxBytes: this.maxBytes,
|
|
88
|
+
});
|
|
89
|
+
return { tracked: false, reason: 'body-exceeds-max-bytes' };
|
|
90
|
+
}
|
|
91
|
+
const window = this.getWindow?.();
|
|
92
|
+
if (window && !isWithinCacheKeepWindow(window, new Date(this.now()))) {
|
|
93
|
+
this.log?.debug('cachekeep track skipped (outside window)', {
|
|
94
|
+
sessionKey,
|
|
95
|
+
startHour: window.startHour,
|
|
96
|
+
endHour: window.endHour,
|
|
97
|
+
});
|
|
98
|
+
return { tracked: false, reason: 'outside-window' };
|
|
99
|
+
}
|
|
100
|
+
// Delete before re-adding so a retracked session is also the most recent
|
|
101
|
+
// in insertion order.
|
|
102
|
+
this.drop(sessionKey);
|
|
103
|
+
while (this.targets.size >= this.maxTargets ||
|
|
104
|
+
(this.totalBytes + bodyBytes > this.maxBytes && this.targets.size > 0)) {
|
|
105
|
+
const evictKey = this.leastRecentlyTouched();
|
|
106
|
+
if (evictKey === undefined)
|
|
107
|
+
break;
|
|
108
|
+
this.log?.debug('cachekeep evicted least recently used target', {
|
|
109
|
+
sessionKey: evictKey,
|
|
110
|
+
});
|
|
111
|
+
this.drop(evictKey);
|
|
112
|
+
}
|
|
113
|
+
const isSubagent = input.isSubagent === true;
|
|
114
|
+
const accountId = input.accountId || undefined;
|
|
115
|
+
const profile = this.adapter.profile?.({
|
|
116
|
+
bodyText,
|
|
117
|
+
isSubagent,
|
|
118
|
+
accountId,
|
|
119
|
+
meta: input.meta,
|
|
120
|
+
});
|
|
121
|
+
const ttlMs = positive(profile?.ttlMs, this.ttlMs);
|
|
122
|
+
const now = this.now();
|
|
123
|
+
this.targets.set(sessionKey, {
|
|
124
|
+
bodyText,
|
|
125
|
+
bodyBytes,
|
|
126
|
+
accountId,
|
|
127
|
+
isSubagent,
|
|
128
|
+
meta: input.meta,
|
|
129
|
+
ttlMs,
|
|
130
|
+
maxWarms: profile?.maxWarms !== undefined && profile.maxWarms > 0
|
|
131
|
+
? Math.floor(profile.maxWarms)
|
|
132
|
+
: undefined,
|
|
133
|
+
maxIdleMs: profile?.maxIdleMs !== undefined
|
|
134
|
+
? positive(profile.maxIdleMs, this.idleDefault(isSubagent))
|
|
135
|
+
: undefined,
|
|
136
|
+
cacheExpiresAt: now + ttlMs,
|
|
137
|
+
lastRealRequestAt: now,
|
|
138
|
+
failures: 0,
|
|
139
|
+
warmCount: 0,
|
|
140
|
+
});
|
|
141
|
+
this.totalBytes += bodyBytes;
|
|
142
|
+
this.log?.debug('cachekeep captured target', { sessionKey, accountId });
|
|
143
|
+
this.start();
|
|
144
|
+
return { tracked: true };
|
|
145
|
+
}
|
|
146
|
+
/** Forget a session, e.g. when the host deletes it. */
|
|
147
|
+
remove(sessionKey) {
|
|
148
|
+
if (this.drop(sessionKey)) {
|
|
149
|
+
this.log?.debug('cachekeep removed target', { sessionKey });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
start() {
|
|
153
|
+
this.disposed = false;
|
|
154
|
+
if (this.timer)
|
|
155
|
+
return;
|
|
156
|
+
this.startedAt = this.now();
|
|
157
|
+
this.timer = this.setIntervalImpl(() => {
|
|
158
|
+
void this.tick().catch((error) => {
|
|
159
|
+
this.log?.warn('cachekeep tick failed', { error: errorMessage(error) });
|
|
160
|
+
});
|
|
161
|
+
}, this.tickIntervalMs);
|
|
162
|
+
if (typeof this.timer === 'object' && 'unref' in this.timer) {
|
|
163
|
+
this.timer.unref();
|
|
164
|
+
}
|
|
165
|
+
this.log?.debug('cachekeep started', {
|
|
166
|
+
ttlMs: this.ttlMs,
|
|
167
|
+
leadMs: this.leadMs,
|
|
168
|
+
maxIdleWarmMs: this.maxIdleWarmMs,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
/** Disarm, abort any warm in flight and forget every target. */
|
|
172
|
+
stop() {
|
|
173
|
+
this.disposed = true;
|
|
174
|
+
this.abortController.abort();
|
|
175
|
+
this.abortController = new AbortController();
|
|
176
|
+
if (this.timer) {
|
|
177
|
+
this.clearIntervalImpl(this.timer);
|
|
178
|
+
this.timer = null;
|
|
179
|
+
}
|
|
180
|
+
this.startedAt = null;
|
|
181
|
+
this.targets.clear();
|
|
182
|
+
this.totalBytes = 0;
|
|
183
|
+
this.log?.debug('cachekeep stopped');
|
|
184
|
+
}
|
|
185
|
+
/** A snapshot without captured bodies, which can hold prompt content. */
|
|
186
|
+
status() {
|
|
187
|
+
const window = this.getWindow?.();
|
|
188
|
+
return {
|
|
189
|
+
running: this.timer !== null,
|
|
190
|
+
tracked: this.targets.size,
|
|
191
|
+
totalBytes: this.totalBytes,
|
|
192
|
+
generatedAt: this.now(),
|
|
193
|
+
startedAt: this.startedAt,
|
|
194
|
+
ttlMs: this.ttlMs,
|
|
195
|
+
leadMs: this.leadMs,
|
|
196
|
+
maxIdleWarmMs: this.maxIdleWarmMs,
|
|
197
|
+
maxSubagentIdleMs: this.maxSubagentIdleMs,
|
|
198
|
+
sustain: this.getSustain?.() === true,
|
|
199
|
+
...(window ? { window } : {}),
|
|
200
|
+
targets: [...this.targets].map(([sessionKey, target]) => ({
|
|
201
|
+
sessionKey,
|
|
202
|
+
accountId: target.accountId,
|
|
203
|
+
isSubagent: target.isSubagent,
|
|
204
|
+
cacheExpiresAt: target.cacheExpiresAt,
|
|
205
|
+
lastRealRequestAt: target.lastRealRequestAt,
|
|
206
|
+
...(target.lastWarmedAt !== undefined
|
|
207
|
+
? { lastWarmedAt: target.lastWarmedAt }
|
|
208
|
+
: {}),
|
|
209
|
+
...(target.backoffUntil !== undefined
|
|
210
|
+
? { backoffUntil: target.backoffUntil }
|
|
211
|
+
: {}),
|
|
212
|
+
failures: target.failures,
|
|
213
|
+
warmCount: target.warmCount,
|
|
214
|
+
ttlMs: target.ttlMs,
|
|
215
|
+
bodyBytes: target.bodyBytes,
|
|
216
|
+
})),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Prune, then warm every target within `leadMs` of expiry that is not
|
|
221
|
+
* backing off. Overlapping calls share the tick already running, so a slow
|
|
222
|
+
* warm never stacks a second send for the same target.
|
|
223
|
+
*/
|
|
224
|
+
tick() {
|
|
225
|
+
if (this.tickPromise)
|
|
226
|
+
return this.tickPromise;
|
|
227
|
+
const run = this.runTick().finally(() => {
|
|
228
|
+
if (this.tickPromise === run)
|
|
229
|
+
this.tickPromise = null;
|
|
230
|
+
});
|
|
231
|
+
this.tickPromise = run;
|
|
232
|
+
return run;
|
|
233
|
+
}
|
|
234
|
+
async runTick() {
|
|
235
|
+
this.pruneStale();
|
|
236
|
+
// Outside the window nothing fires, but captured targets stay so they
|
|
237
|
+
// warm again when it reopens.
|
|
238
|
+
const window = this.getWindow?.();
|
|
239
|
+
if (window && !isWithinCacheKeepWindow(window, new Date(this.now()))) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const now = this.now();
|
|
243
|
+
const leadBound = now + this.leadMs;
|
|
244
|
+
for (const [sessionKey, target] of [...this.targets]) {
|
|
245
|
+
if (this.disposed)
|
|
246
|
+
return;
|
|
247
|
+
if (this.targets.get(sessionKey) !== target)
|
|
248
|
+
continue;
|
|
249
|
+
if (target.backoffUntil !== undefined && now < target.backoffUntil) {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (target.cacheExpiresAt > leadBound)
|
|
253
|
+
continue;
|
|
254
|
+
await this.warm(sessionKey, target);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
isCurrent(sessionKey, target) {
|
|
258
|
+
return !this.disposed && this.targets.get(sessionKey) === target;
|
|
259
|
+
}
|
|
260
|
+
view(sessionKey, target) {
|
|
261
|
+
return {
|
|
262
|
+
sessionKey,
|
|
263
|
+
accountId: target.accountId,
|
|
264
|
+
bodyText: target.bodyText,
|
|
265
|
+
isSubagent: target.isSubagent,
|
|
266
|
+
meta: target.meta,
|
|
267
|
+
warmCount: target.warmCount,
|
|
268
|
+
ttlMs: target.ttlMs,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
fail(sessionKey, target, message, fields = {}) {
|
|
272
|
+
if (!this.isCurrent(sessionKey, target))
|
|
273
|
+
return;
|
|
274
|
+
target.failures += 1;
|
|
275
|
+
const delay = typeof this.backoff === 'function'
|
|
276
|
+
? this.backoff({ sessionKey, failures: target.failures })
|
|
277
|
+
: this.backoff;
|
|
278
|
+
target.backoffUntil =
|
|
279
|
+
this.now() + (Number.isFinite(delay) && delay > 0 ? delay : 0);
|
|
280
|
+
this.log?.warn(message, {
|
|
281
|
+
sessionKey,
|
|
282
|
+
accountId: target.accountId,
|
|
283
|
+
failures: target.failures,
|
|
284
|
+
backoffUntil: target.backoffUntil,
|
|
285
|
+
...fields,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
async warm(sessionKey, target) {
|
|
289
|
+
const view = this.view(sessionKey, target);
|
|
290
|
+
if (this.adapter.activeAccount) {
|
|
291
|
+
let active;
|
|
292
|
+
try {
|
|
293
|
+
active = await this.adapter.activeAccount(sessionKey, view);
|
|
294
|
+
}
|
|
295
|
+
catch (error) {
|
|
296
|
+
this.fail(sessionKey, target, 'cachekeep skip (active account unknown)', {
|
|
297
|
+
error: errorMessage(error),
|
|
298
|
+
});
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (!this.isCurrent(sessionKey, target))
|
|
302
|
+
return;
|
|
303
|
+
if (active !== undefined && active !== target.accountId) {
|
|
304
|
+
this.log?.debug('cachekeep dropped target (session changed account)', {
|
|
305
|
+
sessionKey,
|
|
306
|
+
accountId: target.accountId,
|
|
307
|
+
activeAccountId: active,
|
|
308
|
+
});
|
|
309
|
+
this.drop(sessionKey);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
let body;
|
|
314
|
+
try {
|
|
315
|
+
body = await this.adapter.buildBody(view);
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
this.fail(sessionKey, target, 'cachekeep skip (replay body unbuildable)', {
|
|
319
|
+
error: errorMessage(error),
|
|
320
|
+
});
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
if (!this.isCurrent(sessionKey, target))
|
|
324
|
+
return;
|
|
325
|
+
const signal = AbortSignal.any([
|
|
326
|
+
AbortSignal.timeout(this.warmTimeoutMs),
|
|
327
|
+
this.abortController.signal,
|
|
328
|
+
]);
|
|
329
|
+
let response;
|
|
330
|
+
let text = '';
|
|
331
|
+
try {
|
|
332
|
+
response = await this.adapter.send({ target: view, body, signal });
|
|
333
|
+
// Drain the body so the connection is released; its text is only
|
|
334
|
+
// diagnostic.
|
|
335
|
+
text = await response.text().catch(() => '');
|
|
336
|
+
}
|
|
337
|
+
catch (error) {
|
|
338
|
+
this.fail(sessionKey, target, 'cachekeep failed', {
|
|
339
|
+
error: errorMessage(error),
|
|
340
|
+
});
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
if (!this.isCurrent(sessionKey, target))
|
|
344
|
+
return;
|
|
345
|
+
if (!response.ok) {
|
|
346
|
+
this.fail(sessionKey, target, 'cachekeep failed', {
|
|
347
|
+
status: response.status,
|
|
348
|
+
responseBody: text.slice(0, 600),
|
|
349
|
+
});
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
let usage;
|
|
353
|
+
try {
|
|
354
|
+
usage = this.adapter.readUsage?.({ target: view, response, text });
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
// Usage is for the log line only and must never fail a warm that worked.
|
|
358
|
+
}
|
|
359
|
+
const now = this.now();
|
|
360
|
+
target.cacheExpiresAt = now + target.ttlMs;
|
|
361
|
+
target.lastWarmedAt = now;
|
|
362
|
+
target.backoffUntil = undefined;
|
|
363
|
+
target.failures = 0;
|
|
364
|
+
target.warmCount += 1;
|
|
365
|
+
this.log?.debug('cachekeep fired', {
|
|
366
|
+
sessionKey,
|
|
367
|
+
accountId: target.accountId,
|
|
368
|
+
warmCount: target.warmCount,
|
|
369
|
+
...usage,
|
|
370
|
+
});
|
|
371
|
+
if (target.maxWarms !== undefined && target.warmCount >= target.maxWarms) {
|
|
372
|
+
this.log?.debug('cachekeep retired target (warm cap reached)', {
|
|
373
|
+
sessionKey,
|
|
374
|
+
warmCount: target.warmCount,
|
|
375
|
+
});
|
|
376
|
+
this.drop(sessionKey);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
idleDefault(isSubagent) {
|
|
380
|
+
return isSubagent ? this.maxSubagentIdleMs : this.maxIdleWarmMs;
|
|
381
|
+
}
|
|
382
|
+
pruneStale() {
|
|
383
|
+
const now = this.now();
|
|
384
|
+
const sustain = this.getSustain?.() === true;
|
|
385
|
+
for (const [sessionKey, target] of [...this.targets]) {
|
|
386
|
+
// Sustain exempts main sessions from the idle cap only; the window,
|
|
387
|
+
// count, byte and warm caps still apply to them.
|
|
388
|
+
if (sustain && !target.isSubagent)
|
|
389
|
+
continue;
|
|
390
|
+
const maxIdleMs = target.maxIdleMs ?? this.idleDefault(target.isSubagent);
|
|
391
|
+
if (target.lastRealRequestAt >= now - maxIdleMs)
|
|
392
|
+
continue;
|
|
393
|
+
this.log?.debug('cachekeep pruned idle target', {
|
|
394
|
+
sessionKey,
|
|
395
|
+
accountId: target.accountId,
|
|
396
|
+
lastRealRequestAt: target.lastRealRequestAt,
|
|
397
|
+
maxIdleMs,
|
|
398
|
+
});
|
|
399
|
+
this.drop(sessionKey);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
leastRecentlyTouched() {
|
|
403
|
+
let evictKey;
|
|
404
|
+
let evictTouchedAt = Number.POSITIVE_INFINITY;
|
|
405
|
+
for (const [key, target] of this.targets) {
|
|
406
|
+
// A warm counts as a touch, so a session kept warm outlives one whose
|
|
407
|
+
// only activity is an older real request.
|
|
408
|
+
const touchedAt = Math.max(target.lastRealRequestAt, target.lastWarmedAt ?? 0);
|
|
409
|
+
if (touchedAt < evictTouchedAt) {
|
|
410
|
+
evictKey = key;
|
|
411
|
+
evictTouchedAt = touchedAt;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return evictKey;
|
|
415
|
+
}
|
|
416
|
+
drop(sessionKey) {
|
|
417
|
+
const target = this.targets.get(sessionKey);
|
|
418
|
+
if (!target)
|
|
419
|
+
return false;
|
|
420
|
+
this.totalBytes -= target.bodyBytes;
|
|
421
|
+
this.targets.delete(sessionKey);
|
|
422
|
+
return true;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** A local clock-hour range in which idle caches are kept warm. */
|
|
2
|
+
export interface CacheKeepWindow {
|
|
3
|
+
startHour: number;
|
|
4
|
+
endHour: number;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Reads a persisted `{ startHour, endHour }` pair. Anything other than two
|
|
8
|
+
* distinct whole hours in 0-23 is no window at all, which callers treat as
|
|
9
|
+
* "always warm".
|
|
10
|
+
*/
|
|
11
|
+
export declare function normalizeCacheKeepWindow(value: unknown): CacheKeepWindow | undefined;
|
|
12
|
+
/** Whether the local hour of `now` falls inside the window; no window is never inside. */
|
|
13
|
+
export declare function isWithinCacheKeepWindow(window: CacheKeepWindow | undefined, now?: Date): boolean;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads a persisted `{ startHour, endHour }` pair. Anything other than two
|
|
3
|
+
* distinct whole hours in 0-23 is no window at all, which callers treat as
|
|
4
|
+
* "always warm".
|
|
5
|
+
*/
|
|
6
|
+
export function normalizeCacheKeepWindow(value) {
|
|
7
|
+
if (!value || typeof value !== 'object')
|
|
8
|
+
return undefined;
|
|
9
|
+
const record = value;
|
|
10
|
+
const startHour = Number(record.startHour);
|
|
11
|
+
const endHour = Number(record.endHour);
|
|
12
|
+
if (!Number.isInteger(startHour) ||
|
|
13
|
+
!Number.isInteger(endHour) ||
|
|
14
|
+
startHour < 0 ||
|
|
15
|
+
startHour > 23 ||
|
|
16
|
+
endHour < 0 ||
|
|
17
|
+
endHour > 23 ||
|
|
18
|
+
startHour === endHour) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
return { startHour, endHour };
|
|
22
|
+
}
|
|
23
|
+
/** Whether the local hour of `now` falls inside the window; no window is never inside. */
|
|
24
|
+
export function isWithinCacheKeepWindow(window, now = new Date()) {
|
|
25
|
+
if (!window)
|
|
26
|
+
return false;
|
|
27
|
+
const hour = now.getHours();
|
|
28
|
+
if (window.startHour < window.endHour) {
|
|
29
|
+
return hour >= window.startHour && hour < window.endHour;
|
|
30
|
+
}
|
|
31
|
+
// Overnight wrap (e.g. 22-6): in window at or after the start, or before the end.
|
|
32
|
+
return hour >= window.startHour || hour < window.endHour;
|
|
33
|
+
}
|
package/dist/dump/index.d.ts
CHANGED
|
@@ -1 +1,90 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export interface DumpLogger {
|
|
2
|
+
debug(message: string, data?: unknown): void;
|
|
3
|
+
warn(message: string, data?: unknown): void;
|
|
4
|
+
}
|
|
5
|
+
export interface DumpOptions {
|
|
6
|
+
/** Directory dumps are written to; a function is read on every dump. */
|
|
7
|
+
dir: string | (() => string);
|
|
8
|
+
/** Initial state; `setEnabled` switches it at runtime. Default off. */
|
|
9
|
+
enabled?: boolean;
|
|
10
|
+
/** Written into every filename. Default the current process id. */
|
|
11
|
+
pid?: number;
|
|
12
|
+
/** Clock for filenames and `createdAt`. Default `Date.now`. */
|
|
13
|
+
now?: () => number;
|
|
14
|
+
logger?: DumpLogger;
|
|
15
|
+
/**
|
|
16
|
+
* Key names redacted wherever they appear in headers, request metadata and
|
|
17
|
+
* the body, on top of the common credential set. Compared lower-cased with
|
|
18
|
+
* `-` and `_` removed, so `ChatGPT-Account-Id` and `chatgptAccountId` match
|
|
19
|
+
* `chatgptaccountid`.
|
|
20
|
+
*/
|
|
21
|
+
secretKeys?: readonly string[];
|
|
22
|
+
/** Value patterns scrubbed from every string, on top of the common token patterns. */
|
|
23
|
+
secretPatterns?: readonly RegExp[];
|
|
24
|
+
/**
|
|
25
|
+
* Keys, at any depth of the body, whose values are declarations rather than
|
|
26
|
+
* data: their strings are scrubbed but their key names are never redacted.
|
|
27
|
+
* Default `['tools']`.
|
|
28
|
+
*/
|
|
29
|
+
schemaKeys?: readonly string[];
|
|
30
|
+
/**
|
|
31
|
+
* Provider summary of the parsed body, stored in the metadata as `body`.
|
|
32
|
+
* It receives the redacted body.
|
|
33
|
+
*/
|
|
34
|
+
summarize?: (body: Record<string, unknown>) => Record<string, unknown>;
|
|
35
|
+
}
|
|
36
|
+
export interface DumpInput {
|
|
37
|
+
/** The session the request belongs to; missing means `session-unknown`. */
|
|
38
|
+
session?: string | null;
|
|
39
|
+
/**
|
|
40
|
+
* Diff baseline group: a dump is diffed against the previous dump of the
|
|
41
|
+
* same session and channel (for example, one per transport).
|
|
42
|
+
*/
|
|
43
|
+
channel: string;
|
|
44
|
+
/** Finer label for the filename, e.g. prewarm or main; it does not split the baseline. */
|
|
45
|
+
phase?: string;
|
|
46
|
+
bodyText: string;
|
|
47
|
+
accountId?: string;
|
|
48
|
+
url?: string;
|
|
49
|
+
method?: string;
|
|
50
|
+
headers?: ConstructorParameters<typeof Headers>[0];
|
|
51
|
+
status?: number;
|
|
52
|
+
error?: string;
|
|
53
|
+
/** Extra metadata, redacted like the body. */
|
|
54
|
+
meta?: Record<string, unknown>;
|
|
55
|
+
}
|
|
56
|
+
export interface DumpDiff {
|
|
57
|
+
changed: boolean;
|
|
58
|
+
firstByte: number;
|
|
59
|
+
lastPreviousByte: number;
|
|
60
|
+
lastCurrentByte: number;
|
|
61
|
+
changedPreviousBytes?: number;
|
|
62
|
+
changedCurrentBytes?: number;
|
|
63
|
+
previousBytes: number;
|
|
64
|
+
currentBytes: number;
|
|
65
|
+
}
|
|
66
|
+
export interface DumpResult {
|
|
67
|
+
id: string;
|
|
68
|
+
files: {
|
|
69
|
+
body: string;
|
|
70
|
+
metadata: string;
|
|
71
|
+
request: string;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export interface Dumper {
|
|
75
|
+
isEnabled(): boolean;
|
|
76
|
+
setEnabled(enabled: boolean): void;
|
|
77
|
+
/**
|
|
78
|
+
* Write `<id>.body.json`, `<id>.meta.json` and `<id>.request.json`. Returns
|
|
79
|
+
* undefined when dumps are off or the write failed; dumping is diagnostic
|
|
80
|
+
* and never throws into the request path.
|
|
81
|
+
*/
|
|
82
|
+
dump(input: DumpInput): Promise<DumpResult | undefined>;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Where two bodies first and last differ, in string offsets into the dumped
|
|
86
|
+
* (redacted) text. Offsets count UTF-16 units, as the plugins' cache analysis
|
|
87
|
+
* always has.
|
|
88
|
+
*/
|
|
89
|
+
export declare function diffDumpBodies(previous: string | undefined, current: string): DumpDiff | null;
|
|
90
|
+
export declare function createDumper(options: DumpOptions): Dumper;
|