@cortexkit/anthropic-auth-core 1.15.1 → 1.16.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.
- package/dist/accounts.d.ts +14 -2
- package/dist/accounts.js +102 -22
- package/dist/cachekeep-registry.d.ts +21 -0
- package/dist/cachekeep-registry.js +124 -0
- package/dist/cachekeep.d.ts +16 -1
- package/dist/cachekeep.js +93 -14
- package/dist/dump.d.ts +19 -0
- package/dist/dump.js +162 -10
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/quota-manager.d.ts +4 -3
- package/dist/quota-manager.js +15 -7
- package/dist/routing.d.ts +3 -1
- package/dist/routing.js +20 -2
- package/dist/sticky-routing.d.ts +86 -0
- package/dist/sticky-routing.js +450 -0
- package/package.json +1 -1
package/dist/accounts.d.ts
CHANGED
|
@@ -70,7 +70,7 @@ export type OAuthQuotaSnapshot = Partial<Record<QuotaWindowName, AccountQuotaWin
|
|
|
70
70
|
scoped?: AccountScopedQuotaWindow[];
|
|
71
71
|
checkedAt?: number;
|
|
72
72
|
};
|
|
73
|
-
export type RoutingMode = 'main-first' | 'fallback-first';
|
|
73
|
+
export type RoutingMode = 'main-first' | 'fallback-first' | 'sticky-balanced';
|
|
74
74
|
export type KillswitchThresholds = Partial<Record<QuotaWindowName | '5h' | '1w' | 'scoped', number>>;
|
|
75
75
|
export type KillswitchConfig = {
|
|
76
76
|
enabled?: boolean;
|
|
@@ -134,6 +134,7 @@ export type AccountStorage = {
|
|
|
134
134
|
};
|
|
135
135
|
cacheKeep?: {
|
|
136
136
|
enabled?: boolean;
|
|
137
|
+
always?: boolean;
|
|
137
138
|
startHour?: number;
|
|
138
139
|
endHour?: number;
|
|
139
140
|
subagents?: boolean;
|
|
@@ -189,7 +190,13 @@ export declare function getAccountStoragePath(): string;
|
|
|
189
190
|
export declare function getAccountStatePath(configPath?: string): string;
|
|
190
191
|
export declare function createEmptyStorage(): AccountStorage;
|
|
191
192
|
export declare function loadAccounts(path?: string): Promise<AccountStorage | null>;
|
|
192
|
-
export
|
|
193
|
+
export interface SaveAccountsOptions {
|
|
194
|
+
/** Account ids intentionally removed by this mutation. */
|
|
195
|
+
removedAccountIds?: readonly string[];
|
|
196
|
+
/** Preserve disk order when a stale snapshot is missing newer accounts. */
|
|
197
|
+
preserveExistingAccountOrder?: boolean;
|
|
198
|
+
}
|
|
199
|
+
export declare function saveAccounts(storage: AccountStorage, path?: string, options?: SaveAccountsOptions): Promise<void>;
|
|
193
200
|
export declare function saveAccountState(storage: AccountStorage, path?: string, scope?: AccountStateSaveScope): Promise<void>;
|
|
194
201
|
export declare function acquireRefreshFileLock(options: {
|
|
195
202
|
name: string;
|
|
@@ -211,10 +218,15 @@ export declare function setDumpPersistentEnabled(enabled: boolean, path?: string
|
|
|
211
218
|
export declare function isFastModePersistentlyEnabled(storage: AccountStorage | null): boolean;
|
|
212
219
|
export declare function setFastModePersistentEnabled(enabled: boolean, path?: string): Promise<AccountStorage>;
|
|
213
220
|
export declare function setCacheKeepPersistentWindow(startHour: number, endHour: number, path?: string): Promise<AccountStorage>;
|
|
221
|
+
export declare function setCacheKeepPersistentAlways(path?: string): Promise<AccountStorage>;
|
|
214
222
|
export declare function setCacheKeepPersistentEnabled(enabled: boolean, path?: string): Promise<AccountStorage>;
|
|
215
223
|
export declare function isCacheKeepSubagentsEnabled(storage: AccountStorage | null): boolean;
|
|
216
224
|
export declare function setCacheKeepSubagentsEnabled(enabled: boolean, path?: string): Promise<AccountStorage>;
|
|
217
225
|
export declare function shouldFallbackStatus(status: number, storage: AccountStorage | null): boolean;
|
|
226
|
+
export declare function getQuotaMinimumRemainingThresholds(storage: AccountStorage | null): {
|
|
227
|
+
five_hour: number;
|
|
228
|
+
seven_day: number;
|
|
229
|
+
};
|
|
218
230
|
export declare function getRefreshIntervalMs(storage: AccountStorage | null): number;
|
|
219
231
|
export declare function hashRefreshToken(refreshToken: string): string;
|
|
220
232
|
export declare function buildRefreshOperationError(input: {
|
package/dist/accounts.js
CHANGED
|
@@ -517,19 +517,81 @@ async function writeJsonAtomic(path, value) {
|
|
|
517
517
|
throw error;
|
|
518
518
|
}
|
|
519
519
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
const
|
|
526
|
-
const
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
520
|
+
function sameAccountIdentity(left, right) {
|
|
521
|
+
return (left.id === right.id ||
|
|
522
|
+
Boolean(left.label && right.label && left.label === right.label));
|
|
523
|
+
}
|
|
524
|
+
function mergeAccountsForSave(existing, incoming, options) {
|
|
525
|
+
const removedIds = new Set(options.removedAccountIds ?? []);
|
|
526
|
+
const current = existing.filter((account) => !removedIds.has(account.id));
|
|
527
|
+
const next = incoming.filter((account) => !removedIds.has(account.id));
|
|
528
|
+
const missing = current.filter((account) => !next.some((candidate) => sameAccountIdentity(candidate, account)));
|
|
529
|
+
if (!missing.length)
|
|
530
|
+
return [...next];
|
|
531
|
+
if (options.preserveExistingAccountOrder === false) {
|
|
532
|
+
return [...next, ...missing];
|
|
533
|
+
}
|
|
534
|
+
const usedIncoming = new Set();
|
|
535
|
+
const merged = current.map((account) => {
|
|
536
|
+
const index = next.findIndex((candidate, candidateIndex) => !usedIncoming.has(candidateIndex) &&
|
|
537
|
+
sameAccountIdentity(candidate, account));
|
|
538
|
+
const candidate = next[index];
|
|
539
|
+
if (!candidate)
|
|
540
|
+
return account;
|
|
541
|
+
usedIncoming.add(index);
|
|
542
|
+
return candidate;
|
|
532
543
|
});
|
|
544
|
+
for (let index = 0; index < next.length; index++) {
|
|
545
|
+
const candidate = next[index];
|
|
546
|
+
if (!usedIncoming.has(index) && candidate)
|
|
547
|
+
merged.push(candidate);
|
|
548
|
+
}
|
|
549
|
+
return merged;
|
|
550
|
+
}
|
|
551
|
+
const ACCOUNT_CONFIG_LOCK_TTL_MS = 10_000;
|
|
552
|
+
const ACCOUNT_CONFIG_LOCK_WAIT_MS = 12_000;
|
|
553
|
+
async function acquireAccountConfigWriteLock(path) {
|
|
554
|
+
await mkdir(dirname(path), { recursive: true });
|
|
555
|
+
const deadline = Date.now() + ACCOUNT_CONFIG_LOCK_WAIT_MS;
|
|
556
|
+
while (true) {
|
|
557
|
+
const lock = await acquireRefreshFileLock({
|
|
558
|
+
name: 'config-write',
|
|
559
|
+
ttlMs: ACCOUNT_CONFIG_LOCK_TTL_MS,
|
|
560
|
+
path,
|
|
561
|
+
renew: true,
|
|
562
|
+
});
|
|
563
|
+
if (lock)
|
|
564
|
+
return lock;
|
|
565
|
+
if (Date.now() >= deadline) {
|
|
566
|
+
throw new Error('Timed out waiting for the account configuration write lock');
|
|
567
|
+
}
|
|
568
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
export function saveAccounts(storage, path = getAccountStoragePath(), options = {}) {
|
|
572
|
+
const resolvedPath = path;
|
|
573
|
+
return enqueueSave(() => saveAccountsLocked(storage, resolvedPath, options));
|
|
574
|
+
}
|
|
575
|
+
async function saveAccountsLocked(storage, path, options) {
|
|
576
|
+
const lock = await acquireAccountConfigWriteLock(path);
|
|
577
|
+
try {
|
|
578
|
+
const current = await loadAccounts(path);
|
|
579
|
+
const nextStorage = {
|
|
580
|
+
...storage,
|
|
581
|
+
accounts: mergeAccountsForSave(current?.accounts ?? [], storage.accounts, options),
|
|
582
|
+
};
|
|
583
|
+
const existing = await loadExistingTopLevelFields(path);
|
|
584
|
+
const nextConfig = { ...existing, ...configFromStorage(nextStorage) };
|
|
585
|
+
await writeJsonAtomic(path, nextConfig);
|
|
586
|
+
await saveAccountStateUnlocked(nextStorage, path, {
|
|
587
|
+
mainQuota: true,
|
|
588
|
+
mainRefresh: true,
|
|
589
|
+
accounts: true,
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
finally {
|
|
593
|
+
await lock.release();
|
|
594
|
+
}
|
|
533
595
|
}
|
|
534
596
|
function applyMainQuotaStatePatch(state, storage) {
|
|
535
597
|
state.main = state.main ?? {};
|
|
@@ -877,13 +939,27 @@ export async function setFastModePersistentEnabled(enabled, path = getAccountSto
|
|
|
877
939
|
export async function setCacheKeepPersistentWindow(startHour, endHour, path = getAccountStoragePath()) {
|
|
878
940
|
const storage = (await loadAccounts(path)) ?? createEmptyStorage();
|
|
879
941
|
storage.cacheKeep = {
|
|
942
|
+
...(storage.cacheKeep ?? {}),
|
|
880
943
|
enabled: true,
|
|
944
|
+
always: false,
|
|
881
945
|
startHour,
|
|
882
946
|
endHour,
|
|
883
947
|
};
|
|
884
948
|
await saveAccounts(storage, path);
|
|
885
949
|
return storage;
|
|
886
950
|
}
|
|
951
|
+
export async function setCacheKeepPersistentAlways(path = getAccountStoragePath()) {
|
|
952
|
+
const storage = (await loadAccounts(path)) ?? createEmptyStorage();
|
|
953
|
+
storage.cacheKeep = {
|
|
954
|
+
...(storage.cacheKeep ?? {}),
|
|
955
|
+
enabled: true,
|
|
956
|
+
always: true,
|
|
957
|
+
};
|
|
958
|
+
delete storage.cacheKeep.startHour;
|
|
959
|
+
delete storage.cacheKeep.endHour;
|
|
960
|
+
await saveAccounts(storage, path);
|
|
961
|
+
return storage;
|
|
962
|
+
}
|
|
887
963
|
export async function setCacheKeepPersistentEnabled(enabled, path = getAccountStoragePath()) {
|
|
888
964
|
const storage = (await loadAccounts(path)) ?? createEmptyStorage();
|
|
889
965
|
storage.cacheKeep = {
|
|
@@ -911,7 +987,7 @@ function getFallbackStatuses(storage) {
|
|
|
911
987
|
export function shouldFallbackStatus(status, storage) {
|
|
912
988
|
return getFallbackStatuses(storage).includes(status);
|
|
913
989
|
}
|
|
914
|
-
function
|
|
990
|
+
export function getQuotaMinimumRemainingThresholds(storage) {
|
|
915
991
|
const configured = storage?.quota?.minimumRemaining || {};
|
|
916
992
|
return {
|
|
917
993
|
five_hour: configured.five_hour ??
|
|
@@ -1141,7 +1217,7 @@ export function quotaSnapshotPassesModelScope(quota, model) {
|
|
|
1141
1217
|
export function quotaSnapshotPassesPolicy(quota, storage) {
|
|
1142
1218
|
if (!quotaEnabled(storage))
|
|
1143
1219
|
return true;
|
|
1144
|
-
const thresholds =
|
|
1220
|
+
const thresholds = getQuotaMinimumRemainingThresholds(storage);
|
|
1145
1221
|
for (const key of ['five_hour', 'seven_day']) {
|
|
1146
1222
|
const window = quota?.[key];
|
|
1147
1223
|
if (!window)
|
|
@@ -1293,8 +1369,9 @@ export async function removeAccountPersistent(id, path = getAccountStoragePath()
|
|
|
1293
1369
|
if (!storage)
|
|
1294
1370
|
return false;
|
|
1295
1371
|
const existed = removeAccount(storage, id);
|
|
1296
|
-
if (existed)
|
|
1297
|
-
await saveAccounts(storage, path);
|
|
1372
|
+
if (existed) {
|
|
1373
|
+
await saveAccounts(storage, path, { removedAccountIds: [id] });
|
|
1374
|
+
}
|
|
1298
1375
|
return existed;
|
|
1299
1376
|
}
|
|
1300
1377
|
export async function reorderAccountsPersistent(orderedIds, path = getAccountStoragePath()) {
|
|
@@ -1302,7 +1379,7 @@ export async function reorderAccountsPersistent(orderedIds, path = getAccountSto
|
|
|
1302
1379
|
if (!storage)
|
|
1303
1380
|
return;
|
|
1304
1381
|
reorderAccounts(storage, orderedIds);
|
|
1305
|
-
await saveAccounts(storage, path);
|
|
1382
|
+
await saveAccounts(storage, path, { preserveExistingAccountOrder: false });
|
|
1306
1383
|
}
|
|
1307
1384
|
export async function setAccountEnabledPersistent(id, enabled, path = getAccountStoragePath()) {
|
|
1308
1385
|
const storage = await loadAccounts(path);
|
|
@@ -1321,7 +1398,7 @@ export async function addAccountPersistent(account, path = getAccountStoragePath
|
|
|
1321
1398
|
export function getQuotaNextRefreshAt(quota, storage, now) {
|
|
1322
1399
|
if (!quotaEnabled(storage))
|
|
1323
1400
|
return now + getQuotaCheckIntervalMs(storage);
|
|
1324
|
-
const thresholds =
|
|
1401
|
+
const thresholds = getQuotaMinimumRemainingThresholds(storage);
|
|
1325
1402
|
const blockedResetTimes = [];
|
|
1326
1403
|
for (const key of ['five_hour', 'seven_day']) {
|
|
1327
1404
|
const window = quota?.[key];
|
|
@@ -1353,8 +1430,11 @@ function quotaSnapshotIsFresh(quota, storage, now) {
|
|
|
1353
1430
|
return Boolean(window && now - window.checkedAt < maxAge);
|
|
1354
1431
|
});
|
|
1355
1432
|
}
|
|
1356
|
-
function quotaIsStale(account, storage, now) {
|
|
1357
|
-
|
|
1433
|
+
function quotaIsStale(account, storage, now, modelId) {
|
|
1434
|
+
if (!quotaSnapshotIsFresh(account.quota, storage, now))
|
|
1435
|
+
return true;
|
|
1436
|
+
const scoped = getScopedQuotaWindowForModel(account.quota, modelId);
|
|
1437
|
+
return Boolean(scoped && now - scoped.checkedAt >= getQuotaCheckIntervalMs(storage));
|
|
1358
1438
|
}
|
|
1359
1439
|
function cachedQuotaWindowStillRelevant(window, now) {
|
|
1360
1440
|
if (!window)
|
|
@@ -1656,8 +1736,8 @@ export class FallbackAccountManager {
|
|
|
1656
1736
|
}
|
|
1657
1737
|
this.seedFallbackQuota(next, storage);
|
|
1658
1738
|
const stale = this.quotaManager
|
|
1659
|
-
? this.quotaManager.isFallbackStale(next.id, next.access)
|
|
1660
|
-
: quotaIsStale(next, storage, this.now());
|
|
1739
|
+
? this.quotaManager.isFallbackStale(next.id, next.access, options.modelId)
|
|
1740
|
+
: quotaIsStale(next, storage, this.now(), options.modelId);
|
|
1661
1741
|
// Skip the request-time refresh when this account's quota API is
|
|
1662
1742
|
// backed off (recent 429/5xx). Hitting it again would extend the
|
|
1663
1743
|
// backoff; evaluate policy on the cached/seeded quota instead. Mirrors
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare const CACHE_KEEP_REGISTRY_LEASE_MS: number;
|
|
2
|
+
export type CacheKeepTrackedSession = {
|
|
3
|
+
id: string;
|
|
4
|
+
cacheExpiresAt: number;
|
|
5
|
+
nextPrewarmAt: number;
|
|
6
|
+
};
|
|
7
|
+
export declare function getDefaultCacheKeepRegistryDirectory(scope: 'opencode' | 'pi'): string;
|
|
8
|
+
export declare class CacheKeepSessionRegistry {
|
|
9
|
+
private readonly options;
|
|
10
|
+
private readonly filePath;
|
|
11
|
+
private writeChain;
|
|
12
|
+
constructor(options: {
|
|
13
|
+
directory: string;
|
|
14
|
+
now?: () => number;
|
|
15
|
+
leaseMs?: number;
|
|
16
|
+
instanceId?: string;
|
|
17
|
+
});
|
|
18
|
+
publish(sessions: readonly CacheKeepTrackedSession[]): Promise<void>;
|
|
19
|
+
list(localSessions?: readonly CacheKeepTrackedSession[]): Promise<CacheKeepTrackedSession[]>;
|
|
20
|
+
dispose(): Promise<void>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdir, readdir, readFile, rename, rm, writeFile, } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
export const CACHE_KEEP_REGISTRY_LEASE_MS = 3 * 60_000;
|
|
6
|
+
function normalizeSession(value) {
|
|
7
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
8
|
+
return;
|
|
9
|
+
const record = value;
|
|
10
|
+
if (typeof record.id !== 'string' ||
|
|
11
|
+
record.id.length === 0 ||
|
|
12
|
+
typeof record.cacheExpiresAt !== 'number' ||
|
|
13
|
+
!Number.isFinite(record.cacheExpiresAt) ||
|
|
14
|
+
typeof record.nextPrewarmAt !== 'number' ||
|
|
15
|
+
!Number.isFinite(record.nextPrewarmAt)) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
id: record.id,
|
|
20
|
+
cacheExpiresAt: record.cacheExpiresAt,
|
|
21
|
+
nextPrewarmAt: record.nextPrewarmAt,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function normalizeRecord(value) {
|
|
25
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
26
|
+
return;
|
|
27
|
+
const record = value;
|
|
28
|
+
if (record.version !== 1 ||
|
|
29
|
+
typeof record.updatedAt !== 'number' ||
|
|
30
|
+
!Number.isFinite(record.updatedAt) ||
|
|
31
|
+
!Array.isArray(record.sessions)) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
version: 1,
|
|
36
|
+
updatedAt: record.updatedAt,
|
|
37
|
+
sessions: record.sessions.flatMap((session) => {
|
|
38
|
+
const normalized = normalizeSession(session);
|
|
39
|
+
return normalized ? [normalized] : [];
|
|
40
|
+
}),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function getDefaultCacheKeepRegistryDirectory(scope) {
|
|
44
|
+
return join(tmpdir(), 'opencode-anthropic-auth', 'cachekeep-sessions', scope);
|
|
45
|
+
}
|
|
46
|
+
export class CacheKeepSessionRegistry {
|
|
47
|
+
options;
|
|
48
|
+
filePath;
|
|
49
|
+
writeChain = Promise.resolve();
|
|
50
|
+
constructor(options) {
|
|
51
|
+
this.options = options;
|
|
52
|
+
const instanceId = options.instanceId ?? `${process.pid}-${randomUUID()}`;
|
|
53
|
+
this.filePath = join(options.directory, `${instanceId}.json`);
|
|
54
|
+
}
|
|
55
|
+
publish(sessions) {
|
|
56
|
+
const snapshot = sessions.map((session) => ({ ...session }));
|
|
57
|
+
this.writeChain = this.writeChain
|
|
58
|
+
.catch(() => { })
|
|
59
|
+
.then(async () => {
|
|
60
|
+
if (snapshot.length === 0) {
|
|
61
|
+
await rm(this.filePath, { force: true });
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
await mkdir(this.options.directory, { recursive: true, mode: 0o700 });
|
|
65
|
+
const tempPath = `${this.filePath}.${randomUUID()}.tmp`;
|
|
66
|
+
const record = {
|
|
67
|
+
version: 1,
|
|
68
|
+
updatedAt: this.options.now?.() ?? Date.now(),
|
|
69
|
+
sessions: snapshot,
|
|
70
|
+
};
|
|
71
|
+
await writeFile(tempPath, `${JSON.stringify(record)}\n`, {
|
|
72
|
+
mode: 0o600,
|
|
73
|
+
});
|
|
74
|
+
await rename(tempPath, this.filePath);
|
|
75
|
+
});
|
|
76
|
+
return this.writeChain;
|
|
77
|
+
}
|
|
78
|
+
async list(localSessions = []) {
|
|
79
|
+
await this.writeChain.catch(() => { });
|
|
80
|
+
const now = this.options.now?.() ?? Date.now();
|
|
81
|
+
const leaseMs = this.options.leaseMs ?? CACHE_KEEP_REGISTRY_LEASE_MS;
|
|
82
|
+
let names;
|
|
83
|
+
try {
|
|
84
|
+
names = (await readdir(this.options.directory)).filter((name) => name.endsWith('.json'));
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
names = [];
|
|
88
|
+
}
|
|
89
|
+
const records = await Promise.all(names.map(async (name) => {
|
|
90
|
+
try {
|
|
91
|
+
const raw = await readFile(join(this.options.directory, name), 'utf8');
|
|
92
|
+
return normalizeRecord(JSON.parse(raw));
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
}));
|
|
98
|
+
const sessions = new Map();
|
|
99
|
+
for (const record of records) {
|
|
100
|
+
if (!record ||
|
|
101
|
+
now - record.updatedAt > leaseMs ||
|
|
102
|
+
record.updatedAt > now) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
for (const session of record.sessions) {
|
|
106
|
+
const existing = sessions.get(session.id);
|
|
107
|
+
if (!existing || session.cacheExpiresAt > existing.cacheExpiresAt) {
|
|
108
|
+
sessions.set(session.id, session);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
for (const session of localSessions) {
|
|
113
|
+
const existing = sessions.get(session.id);
|
|
114
|
+
if (!existing || session.cacheExpiresAt > existing.cacheExpiresAt) {
|
|
115
|
+
sessions.set(session.id, { ...session });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return [...sessions.values()].sort((left, right) => left.id.localeCompare(right.id));
|
|
119
|
+
}
|
|
120
|
+
async dispose() {
|
|
121
|
+
await this.writeChain.catch(() => { });
|
|
122
|
+
await rm(this.filePath, { force: true });
|
|
123
|
+
}
|
|
124
|
+
}
|
package/dist/cachekeep.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AccountStorage } from './accounts.ts';
|
|
2
|
+
import type { CacheKeepTrackedSession } from './cachekeep-registry.ts';
|
|
2
3
|
export declare const CLAUDE_CACHE_KEEP_COMMAND_NAME = "claude-cachekeep";
|
|
3
4
|
export declare const CACHE_KEEP_TTL_MS: number;
|
|
4
5
|
export declare const CACHE_KEEP_PREWARM_LEAD_MS: number;
|
|
@@ -16,6 +17,8 @@ export type CacheKeepCommandAction = {
|
|
|
16
17
|
type: 'status';
|
|
17
18
|
} | {
|
|
18
19
|
type: 'disable';
|
|
20
|
+
} | {
|
|
21
|
+
type: 'always';
|
|
19
22
|
} | {
|
|
20
23
|
type: 'window';
|
|
21
24
|
startHour: number;
|
|
@@ -28,23 +31,29 @@ export type CacheKeepCommandAction = {
|
|
|
28
31
|
};
|
|
29
32
|
export type CacheKeepStatus = {
|
|
30
33
|
enabled: boolean;
|
|
34
|
+
always?: boolean;
|
|
31
35
|
window?: CacheKeepWindow;
|
|
32
36
|
trackedSessions?: number;
|
|
37
|
+
trackedSessionDetails?: CacheKeepTrackedSession[];
|
|
33
38
|
nextPrewarmAt?: number;
|
|
34
39
|
hybridActive?: boolean;
|
|
35
40
|
};
|
|
36
41
|
export declare function parseCacheKeepCommandAction(input: string): CacheKeepCommandAction;
|
|
37
42
|
export declare function getCacheKeepWindow(storage: AccountStorage | null): CacheKeepWindow | undefined;
|
|
43
|
+
export declare function isCacheKeepAlways(storage: AccountStorage | null): boolean;
|
|
38
44
|
export declare function isCacheKeepPersistentlyEnabled(storage: AccountStorage | null): boolean;
|
|
39
45
|
export declare function isCacheKeepHybridActive(storage: AccountStorage | null): boolean;
|
|
40
46
|
export declare function isWithinCacheKeepWindow(window: CacheKeepWindow | undefined, now?: Date): boolean;
|
|
47
|
+
export declare function isCacheKeepActiveNow(storage: AccountStorage | null, now?: Date): boolean;
|
|
41
48
|
export declare function localDayKey(now?: Date): string;
|
|
42
49
|
export declare function buildCacheKeepStatusSummary(status: CacheKeepStatus): string;
|
|
43
50
|
export declare function executeCacheKeepCommand(input: {
|
|
44
51
|
argumentsText: string;
|
|
45
52
|
enabled?: boolean;
|
|
53
|
+
always?: boolean;
|
|
46
54
|
window?: CacheKeepWindow;
|
|
47
55
|
trackedSessions?: number;
|
|
56
|
+
trackedSessionDetails?: CacheKeepTrackedSession[];
|
|
48
57
|
nextPrewarmAt?: number;
|
|
49
58
|
hybridActive?: boolean;
|
|
50
59
|
}): string;
|
|
@@ -86,14 +95,20 @@ export declare class CacheKeepManager {
|
|
|
86
95
|
now?: () => number;
|
|
87
96
|
prewarmTimeoutMs?: number;
|
|
88
97
|
prepareHeaders?: (headers: Headers, target: CacheKeepTarget) => Promise<Headers> | Headers;
|
|
98
|
+
onTrackedSessionsChanged?: (sessions: readonly CacheKeepTrackedSession[]) => Promise<void> | void;
|
|
89
99
|
});
|
|
90
100
|
start(): void;
|
|
91
101
|
stop(): void;
|
|
92
|
-
stats(window?: CacheKeepWindow, now?: number): {
|
|
102
|
+
stats(window?: CacheKeepWindow, now?: number, always?: boolean): {
|
|
93
103
|
trackedSessions: number;
|
|
94
104
|
nextPrewarmAt: number | undefined;
|
|
95
105
|
};
|
|
106
|
+
trackedSessions(): CacheKeepTrackedSession[];
|
|
107
|
+
trackedOAuthRoute(sessionId: string): {
|
|
108
|
+
oauthAccountId?: string;
|
|
109
|
+
} | undefined;
|
|
96
110
|
trackedCount(): number;
|
|
111
|
+
private publishTrackedSessions;
|
|
97
112
|
private totalBodyBytes;
|
|
98
113
|
private evictOldestTarget;
|
|
99
114
|
private pruneTargets;
|
package/dist/cachekeep.js
CHANGED
|
@@ -14,7 +14,7 @@ const STATUS_TITLE = '## Claude Cache Keep Status';
|
|
|
14
14
|
const ENABLED_TITLE = '## Claude Cache Keep Enabled';
|
|
15
15
|
const DISABLED_TITLE = '## Claude Cache Keep Disabled';
|
|
16
16
|
const USAGE_TITLE = '## Claude Cache Keep Usage';
|
|
17
|
-
const USAGE = 'Usage: `/claude-cachekeep`, `/claude-cachekeep off`, or `/claude-cachekeep HH-HH`.';
|
|
17
|
+
const USAGE = 'Usage: `/claude-cachekeep`, `/claude-cachekeep always`, `/claude-cachekeep off`, or `/claude-cachekeep HH-HH`.';
|
|
18
18
|
function padHour(hour) {
|
|
19
19
|
return String(hour).padStart(2, '0');
|
|
20
20
|
}
|
|
@@ -24,6 +24,8 @@ export function parseCacheKeepCommandAction(input) {
|
|
|
24
24
|
return { type: 'status' };
|
|
25
25
|
if (trimmed === 'off')
|
|
26
26
|
return { type: 'disable' };
|
|
27
|
+
if (trimmed === 'always')
|
|
28
|
+
return { type: 'always' };
|
|
27
29
|
if (trimmed === 'subagents on')
|
|
28
30
|
return { type: 'subagents', enabled: true };
|
|
29
31
|
if (trimmed === 'subagents off')
|
|
@@ -58,8 +60,12 @@ export function getCacheKeepWindow(storage) {
|
|
|
58
60
|
}
|
|
59
61
|
return { startHour, endHour };
|
|
60
62
|
}
|
|
63
|
+
export function isCacheKeepAlways(storage) {
|
|
64
|
+
return (storage?.cacheKeep?.enabled === true && storage.cacheKeep.always === true);
|
|
65
|
+
}
|
|
61
66
|
export function isCacheKeepPersistentlyEnabled(storage) {
|
|
62
|
-
return storage?.cacheKeep?.enabled === true &&
|
|
67
|
+
return (storage?.cacheKeep?.enabled === true &&
|
|
68
|
+
(isCacheKeepAlways(storage) || !!getCacheKeepWindow(storage)));
|
|
63
69
|
}
|
|
64
70
|
export function isCacheKeepHybridActive(storage) {
|
|
65
71
|
return (isCacheKeepPersistentlyEnabled(storage) &&
|
|
@@ -75,6 +81,12 @@ export function isWithinCacheKeepWindow(window, now = new Date()) {
|
|
|
75
81
|
}
|
|
76
82
|
return hour >= window.startHour || hour < window.endHour;
|
|
77
83
|
}
|
|
84
|
+
export function isCacheKeepActiveNow(storage, now = new Date()) {
|
|
85
|
+
if (storage?.cacheKeep?.enabled !== true)
|
|
86
|
+
return false;
|
|
87
|
+
return (isCacheKeepAlways(storage) ||
|
|
88
|
+
isWithinCacheKeepWindow(getCacheKeepWindow(storage), now));
|
|
89
|
+
}
|
|
78
90
|
export function localDayKey(now = new Date()) {
|
|
79
91
|
return `${now.getFullYear()}-${padHour(now.getMonth() + 1)}-${padHour(now.getDate())}`;
|
|
80
92
|
}
|
|
@@ -88,13 +100,18 @@ function localWindowKey(window, now = new Date()) {
|
|
|
88
100
|
}
|
|
89
101
|
return localDayKey(now);
|
|
90
102
|
}
|
|
103
|
+
function cacheKeepPeriodKey(storage, window, now = new Date()) {
|
|
104
|
+
return isCacheKeepAlways(storage) ? 'always' : localWindowKey(window, now);
|
|
105
|
+
}
|
|
91
106
|
export function buildCacheKeepStatusSummary(status) {
|
|
92
|
-
const
|
|
93
|
-
?
|
|
94
|
-
:
|
|
107
|
+
const schedule = status.always
|
|
108
|
+
? 'always (while this process is running)'
|
|
109
|
+
: status.window
|
|
110
|
+
? `${padHour(status.window.startHour)}-${padHour(status.window.endHour)}`
|
|
111
|
+
: 'not configured';
|
|
95
112
|
const lines = [
|
|
96
113
|
`Cache keep: ${status.enabled ? 'enabled' : 'disabled'}`,
|
|
97
|
-
`
|
|
114
|
+
`Schedule: ${schedule}`,
|
|
98
115
|
'Mode requirement: `/claude-cache mode hybrid` must be active.',
|
|
99
116
|
];
|
|
100
117
|
if (typeof status.hybridActive === 'boolean') {
|
|
@@ -103,6 +120,12 @@ export function buildCacheKeepStatusSummary(status) {
|
|
|
103
120
|
if (typeof status.trackedSessions === 'number') {
|
|
104
121
|
lines.push(`Tracked sessions: ${status.trackedSessions}`);
|
|
105
122
|
}
|
|
123
|
+
if (status.trackedSessionDetails?.length) {
|
|
124
|
+
lines.push('Sessions:');
|
|
125
|
+
for (const session of status.trackedSessionDetails) {
|
|
126
|
+
lines.push(`- ${session.id}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
106
129
|
if (status.nextPrewarmAt) {
|
|
107
130
|
lines.push(`Next prewarm: ${new Date(status.nextPrewarmAt).toLocaleString()}`);
|
|
108
131
|
}
|
|
@@ -112,8 +135,10 @@ export function executeCacheKeepCommand(input) {
|
|
|
112
135
|
const action = parseCacheKeepCommandAction(input.argumentsText);
|
|
113
136
|
const status = {
|
|
114
137
|
enabled: input.enabled ?? false,
|
|
138
|
+
always: input.always,
|
|
115
139
|
window: input.window,
|
|
116
140
|
trackedSessions: input.trackedSessions,
|
|
141
|
+
trackedSessionDetails: input.trackedSessionDetails,
|
|
117
142
|
nextPrewarmAt: input.nextPrewarmAt,
|
|
118
143
|
hybridActive: input.hybridActive,
|
|
119
144
|
};
|
|
@@ -127,6 +152,18 @@ export function executeCacheKeepCommand(input) {
|
|
|
127
152
|
buildCacheKeepStatusSummary({ ...status, enabled: false }),
|
|
128
153
|
].join('\n');
|
|
129
154
|
}
|
|
155
|
+
if (action.type === 'always') {
|
|
156
|
+
return [
|
|
157
|
+
ENABLED_TITLE,
|
|
158
|
+
'',
|
|
159
|
+
buildCacheKeepStatusSummary({
|
|
160
|
+
...status,
|
|
161
|
+
enabled: true,
|
|
162
|
+
always: true,
|
|
163
|
+
window: undefined,
|
|
164
|
+
}),
|
|
165
|
+
].join('\n');
|
|
166
|
+
}
|
|
130
167
|
if (action.type === 'window') {
|
|
131
168
|
return [
|
|
132
169
|
ENABLED_TITLE,
|
|
@@ -134,6 +171,7 @@ export function executeCacheKeepCommand(input) {
|
|
|
134
171
|
buildCacheKeepStatusSummary({
|
|
135
172
|
...status,
|
|
136
173
|
enabled: true,
|
|
174
|
+
always: false,
|
|
137
175
|
window: { startHour: action.startHour, endHour: action.endHour },
|
|
138
176
|
}),
|
|
139
177
|
].join('\n');
|
|
@@ -218,17 +256,47 @@ export class CacheKeepManager {
|
|
|
218
256
|
clearInterval(this.timer);
|
|
219
257
|
this.timer = null;
|
|
220
258
|
}
|
|
221
|
-
stats(window, now = this.options.now?.() ?? Date.now()) {
|
|
222
|
-
const today = localWindowKey(window, new Date(now));
|
|
259
|
+
stats(window, now = this.options.now?.() ?? Date.now(), always = false) {
|
|
260
|
+
const today = always ? 'always' : localWindowKey(window, new Date(now));
|
|
223
261
|
const targets = [...this.targets.values()].filter((target) => target.dayKey === today);
|
|
224
262
|
const nextPrewarmAt = targets.length
|
|
225
263
|
? Math.min(...targets.map((target) => target.cacheExpiresAt - CACHE_KEEP_PREWARM_LEAD_MS))
|
|
226
264
|
: undefined;
|
|
227
265
|
return { trackedSessions: targets.length, nextPrewarmAt };
|
|
228
266
|
}
|
|
267
|
+
trackedSessions() {
|
|
268
|
+
return [...this.targets.values()]
|
|
269
|
+
.map((target) => ({
|
|
270
|
+
id: target.id,
|
|
271
|
+
cacheExpiresAt: target.cacheExpiresAt,
|
|
272
|
+
nextPrewarmAt: target.cacheExpiresAt - CACHE_KEEP_PREWARM_LEAD_MS,
|
|
273
|
+
}))
|
|
274
|
+
.sort((left, right) => left.id.localeCompare(right.id));
|
|
275
|
+
}
|
|
276
|
+
trackedOAuthRoute(sessionId) {
|
|
277
|
+
const target = this.targets.get(sessionId);
|
|
278
|
+
return target ? { oauthAccountId: target.oauthAccountId } : undefined;
|
|
279
|
+
}
|
|
229
280
|
trackedCount() {
|
|
230
281
|
return this.targets.size;
|
|
231
282
|
}
|
|
283
|
+
publishTrackedSessions() {
|
|
284
|
+
const callback = this.options.onTrackedSessionsChanged;
|
|
285
|
+
if (!callback)
|
|
286
|
+
return;
|
|
287
|
+
try {
|
|
288
|
+
void Promise.resolve(callback(this.trackedSessions())).catch((error) => {
|
|
289
|
+
logger.warn('cachekeep', 'failed to publish tracked sessions', {
|
|
290
|
+
error: error instanceof Error ? error.message : String(error),
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
logger.warn('cachekeep', 'failed to publish tracked sessions', {
|
|
296
|
+
error: error instanceof Error ? error.message : String(error),
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
232
300
|
totalBodyBytes() {
|
|
233
301
|
return [...this.targets.values()].reduce((sum, target) => sum + target.bodyText.length, 0);
|
|
234
302
|
}
|
|
@@ -270,10 +338,10 @@ export class CacheKeepManager {
|
|
|
270
338
|
}
|
|
271
339
|
const now = this.options.now?.() ?? Date.now();
|
|
272
340
|
const window = getCacheKeepWindow(input.storage);
|
|
273
|
-
if (!
|
|
274
|
-
return { tracked: false, reason: 'outside configured
|
|
341
|
+
if (!isCacheKeepActiveNow(input.storage, new Date(now))) {
|
|
342
|
+
return { tracked: false, reason: 'outside configured schedule' };
|
|
275
343
|
}
|
|
276
|
-
const today =
|
|
344
|
+
const today = cacheKeepPeriodKey(input.storage, window, new Date(now));
|
|
277
345
|
this.pruneTargets(now, today);
|
|
278
346
|
if (this.targets.has(input.sessionId))
|
|
279
347
|
this.targets.delete(input.sessionId);
|
|
@@ -291,6 +359,7 @@ export class CacheKeepManager {
|
|
|
291
359
|
oauthAccountId: input.oauthAccountId,
|
|
292
360
|
});
|
|
293
361
|
this.pruneTargets(now, today);
|
|
362
|
+
this.publishTrackedSessions();
|
|
294
363
|
this.start();
|
|
295
364
|
return { tracked: true };
|
|
296
365
|
}
|
|
@@ -314,16 +383,25 @@ export class CacheKeepManager {
|
|
|
314
383
|
const storage = await this.options.loadStorage();
|
|
315
384
|
const window = getCacheKeepWindow(storage);
|
|
316
385
|
const now = this.options.now?.() ?? Date.now();
|
|
317
|
-
const today =
|
|
386
|
+
const today = cacheKeepPeriodKey(storage, window, new Date(now));
|
|
387
|
+
if (isCacheKeepAlways(storage)) {
|
|
388
|
+
for (const target of this.targets.values())
|
|
389
|
+
target.dayKey = 'always';
|
|
390
|
+
}
|
|
318
391
|
this.pruneTargets(now, today);
|
|
319
392
|
if (!this.targets.size) {
|
|
393
|
+
this.publishTrackedSessions();
|
|
320
394
|
this.stop();
|
|
321
395
|
return;
|
|
322
396
|
}
|
|
323
|
-
if (!isCacheKeepHybridActive(storage))
|
|
397
|
+
if (!isCacheKeepHybridActive(storage)) {
|
|
398
|
+
this.publishTrackedSessions();
|
|
324
399
|
return;
|
|
325
|
-
|
|
400
|
+
}
|
|
401
|
+
if (!isCacheKeepActiveNow(storage, new Date(now))) {
|
|
402
|
+
this.publishTrackedSessions();
|
|
326
403
|
return;
|
|
404
|
+
}
|
|
327
405
|
logger.debug('cachekeep', 'fired', { targets: this.targets.size });
|
|
328
406
|
const dueAt = now + CACHE_KEEP_PREWARM_LEAD_MS;
|
|
329
407
|
for (const target of this.targets.values()) {
|
|
@@ -331,6 +409,7 @@ export class CacheKeepManager {
|
|
|
331
409
|
continue;
|
|
332
410
|
await this.prewarm(target, now);
|
|
333
411
|
}
|
|
412
|
+
this.publishTrackedSessions();
|
|
334
413
|
}
|
|
335
414
|
async sendPrewarm(target) {
|
|
336
415
|
const prewarm = await buildCacheKeepPrewarmBody(target.bodyText);
|