@cortexkit/common-auth 0.2.4 → 0.2.6
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/commands/builtins.d.ts +71 -0
- package/dist/commands/builtins.js +508 -0
- package/dist/commands/index.d.ts +10 -1
- package/dist/commands/index.js +5 -2
- package/dist/commands/menu.d.ts +39 -0
- package/dist/commands/menu.js +249 -0
- package/dist/commands/model.d.ts +188 -0
- package/dist/commands/model.js +16 -0
- package/dist/commands/pi.d.ts +22 -0
- package/dist/commands/pi.js +178 -0
- package/dist/commands/seam.d.ts +36 -0
- package/dist/commands/seam.js +185 -0
- package/dist/dump/index.d.ts +90 -1
- package/dist/dump/index.js +292 -2
- package/dist/store/errors.d.ts +1 -1
- package/dist/store/index.d.ts +2 -0
- package/dist/store/index.js +1 -0
- package/dist/store/pool.d.ts +12 -0
- package/dist/store/pool.js +3 -0
- package/dist/store/settings.d.ts +63 -0
- package/dist/store/settings.js +120 -0
- 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
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { AddInput, PoolLockSpec, PoolRow, PoolStore, RemoveOptions } from '../store/index.js';
|
|
2
|
+
import type { CommandInvocation, KnobValues, MenuChoice, MenuKnob } from './model.js';
|
|
3
|
+
import type { ResolvedSection } from './seam.js';
|
|
4
|
+
/**
|
|
5
|
+
* What a plugin's login hands back. `ready` adds the account now. `pending`
|
|
6
|
+
* means the user must finish somewhere else (a browser, a device code):
|
|
7
|
+
* `message` tells them how, and the account is added when `completion`
|
|
8
|
+
* settles, with the result sent to the invocation that started it.
|
|
9
|
+
*/
|
|
10
|
+
export type LoginOutcome = {
|
|
11
|
+
status: 'ready';
|
|
12
|
+
account: AddInput;
|
|
13
|
+
} | {
|
|
14
|
+
status: 'pending';
|
|
15
|
+
message: string;
|
|
16
|
+
/** Resolves with the account to add, or undefined when the user gave up. */
|
|
17
|
+
completion: Promise<AddInput | undefined>;
|
|
18
|
+
} | {
|
|
19
|
+
status: 'cancelled';
|
|
20
|
+
message?: string;
|
|
21
|
+
};
|
|
22
|
+
export interface AccountsSectionOptions {
|
|
23
|
+
/** What to show as an account's identity; defaults to its recorded identity. */
|
|
24
|
+
describeIdentity?(row: PoolRow): string | undefined;
|
|
25
|
+
/** The plugin's login; without it the section offers no add action. */
|
|
26
|
+
login?: {
|
|
27
|
+
label?: string;
|
|
28
|
+
knobs?: MenuKnob[];
|
|
29
|
+
run(values: KnobValues, invocation: CommandInvocation): Promise<LoginOutcome>;
|
|
30
|
+
};
|
|
31
|
+
/** Passed to `store.remove`: a reason refuses removing that id. */
|
|
32
|
+
protect?: RemoveOptions['protect'];
|
|
33
|
+
}
|
|
34
|
+
export interface QuotaSectionOptions {
|
|
35
|
+
/** The quota scope to show (`all` or a model family); defaults to `all`. */
|
|
36
|
+
scope?: string;
|
|
37
|
+
/**
|
|
38
|
+
* Checks quota now for the named rows. Without it, the menu asks the store
|
|
39
|
+
* for a reading of each row and waits for the pulls to settle.
|
|
40
|
+
*/
|
|
41
|
+
check?(ids: readonly string[], invocation: CommandInvocation): Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
export interface RoutingSectionOptions {
|
|
44
|
+
/**
|
|
45
|
+
* Further `routing.mode` values the plugin routes as `ordered` with the
|
|
46
|
+
* former main row moved (`main-first`, `fallback-first`), offered between
|
|
47
|
+
* `ordered` and `sticky-balanced`.
|
|
48
|
+
*/
|
|
49
|
+
orderedVariants?: MenuChoice[];
|
|
50
|
+
formerMainId?: string;
|
|
51
|
+
}
|
|
52
|
+
export interface LimitsSectionOptions {
|
|
53
|
+
/**
|
|
54
|
+
* The quota window labels a floor can be set for. Defaults to every label
|
|
55
|
+
* the pool's quota readings carry, or `primary` when there are none.
|
|
56
|
+
*/
|
|
57
|
+
labels?: readonly string[];
|
|
58
|
+
}
|
|
59
|
+
export interface BuiltinOptions {
|
|
60
|
+
store: PoolStore;
|
|
61
|
+
extraLocks?: readonly PoolLockSpec[];
|
|
62
|
+
now: () => number;
|
|
63
|
+
accounts?: AccountsSectionOptions;
|
|
64
|
+
quota?: QuotaSectionOptions;
|
|
65
|
+
routing?: RoutingSectionOptions;
|
|
66
|
+
limits?: LimitsSectionOptions;
|
|
67
|
+
}
|
|
68
|
+
/** `disabledReason` recorded when the user disables an account from the menu. */
|
|
69
|
+
export declare const MENU_DISABLED_REASON = "disabled from the command menu";
|
|
70
|
+
/** The four built-in sections, in their fixed order, from one read of the store. */
|
|
71
|
+
export declare function builtinSections(options: BuiltinOptions): Promise<ResolvedSection[]>;
|