@qvac/sdk 0.17.1 → 0.18.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/NOTICE +124 -64
- package/README.md +19 -0
- package/dist/client/api/audio-gen.d.ts +17 -0
- package/dist/client/api/audio-gen.js +17 -0
- package/dist/client/api/finetune.d.ts +2 -0
- package/dist/client/api/finetune.js +7 -1
- package/dist/client/api/index.d.ts +1 -1
- package/dist/client/api/index.js +1 -1
- package/dist/client/api/load-model.d.ts +8 -0
- package/dist/client/api/transcribe.d.ts +19 -2
- package/dist/client/api/transcribe.js +66 -27
- package/dist/client/api/translate.d.ts +1 -0
- package/dist/client/api/translate.js +40 -19
- package/dist/client/api/vla.d.ts +42 -1
- package/dist/client/api/vla.js +50 -1
- package/dist/commands/bundle/entry-gen.d.ts +4 -0
- package/dist/commands/bundle/entry-gen.js +6 -0
- package/dist/commands/bundle/index.js +19 -12
- package/dist/examples/asr/parakeet-indic-conformer-filesystem.d.ts +2 -0
- package/dist/examples/asr/parakeet-indic-conformer-filesystem.js +52 -0
- package/dist/examples/audiogen/generate-cover.d.ts +2 -0
- package/dist/examples/audiogen/generate-cover.js +100 -0
- package/dist/examples/llamacpp-fallback.d.ts +2 -0
- package/dist/examples/llamacpp-fallback.js +34 -0
- package/dist/examples/multi-job-completion.d.ts +28 -0
- package/dist/examples/multi-job-completion.js +128 -0
- package/dist/examples/profiling/basic.js +32 -1
- package/dist/examples/system-resources.d.ts +2 -0
- package/dist/examples/system-resources.js +86 -0
- package/dist/examples/tts/audio8.d.ts +2 -0
- package/dist/examples/tts/audio8.js +54 -0
- package/dist/examples/tts/cosyvoice3.d.ts +2 -0
- package/dist/examples/tts/cosyvoice3.js +52 -0
- package/dist/examples/vla-groot.js +92 -49
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/models/registry/models.d.ts +782 -6
- package/dist/models/registry/models.js +2664 -1859
- package/dist/schemas/audio-gen.d.ts +103 -0
- package/dist/schemas/audio-gen.js +135 -4
- package/dist/schemas/common.d.ts +550 -11
- package/dist/schemas/constants-registry.d.ts +18 -4
- package/dist/schemas/constants-registry.js +16 -3
- package/dist/schemas/index.d.ts +1 -1
- package/dist/schemas/index.js +1 -1
- package/dist/schemas/llamacpp-config.d.ts +10 -10
- package/dist/schemas/llamacpp-config.js +12 -5
- package/dist/schemas/load-model.d.ts +3678 -323
- package/dist/schemas/load-model.js +21 -3
- package/dist/schemas/sdk-config.d.ts +60 -24
- package/dist/schemas/sdk-errors-server.d.ts +1 -0
- package/dist/schemas/sdk-errors-server.js +5 -0
- package/dist/schemas/text-to-speech.d.ts +1086 -110
- package/dist/schemas/text-to-speech.js +194 -4
- package/dist/schemas/tools.d.ts +0 -16
- package/dist/schemas/tools.js +0 -15
- package/dist/schemas/transcription-config.d.ts +2 -0
- package/dist/schemas/transcription-config.js +10 -4
- package/dist/schemas/transcription.d.ts +3 -0
- package/dist/schemas/vla.d.ts +83 -0
- package/dist/schemas/vla.js +105 -2
- package/dist/scripts/link-workspace-inference.d.ts +2 -0
- package/dist/scripts/link-workspace-inference.js +31 -0
- package/dist/server/bare/ops/cancel.d.ts +1 -1
- package/dist/server/bare/ops/cancel.js +1 -1
- package/dist/server/bare/ops/kv-cache-retention.js +11 -0
- package/dist/server/bare/ops/kv-cache-utils.d.ts +1 -1
- package/dist/server/bare/ops/kv-cache-utils.js +40 -12
- package/dist/server/bare/ops/transcribe.d.ts +2 -2
- package/dist/server/bare/ops/transcribe.js +54 -48
- package/dist/server/bare/ops/translate-stats.d.ts +5 -0
- package/dist/server/bare/ops/translate-stats.js +11 -0
- package/dist/server/bare/ops/translate.js +66 -38
- package/dist/server/bare/ops/unload-model.js +30 -24
- package/dist/server/bare/plugins/audiogen-ggml/ops/audio-gen-input.d.ts +20 -0
- package/dist/server/bare/plugins/audiogen-ggml/ops/audio-gen-input.js +153 -0
- package/dist/server/bare/plugins/audiogen-ggml/ops/audio-gen-stream.js +52 -1
- package/dist/server/bare/plugins/audiogen-ggml/plugin.d.ts +28 -0
- package/dist/server/bare/plugins/ggml-ocr/plugin.js +3 -1
- package/dist/server/bare/plugins/ggml-vla/ops/vla-set-embodiment.d.ts +3 -0
- package/dist/server/bare/plugins/ggml-vla/ops/vla-set-embodiment.js +9 -0
- package/dist/server/bare/plugins/ggml-vla/plugin.d.ts +37 -0
- package/dist/server/bare/plugins/ggml-vla/plugin.js +21 -4
- package/dist/server/bare/plugins/llamacpp-completion/ops/batch-completion-stream.js +16 -15
- package/dist/server/bare/plugins/llamacpp-completion/ops/cache-logger.d.ts +1 -1
- package/dist/server/bare/plugins/llamacpp-completion/ops/cache-logger.js +2 -2
- package/dist/server/bare/plugins/llamacpp-completion/ops/completion-stats.d.ts +6 -0
- package/dist/server/bare/plugins/llamacpp-completion/ops/completion-stats.js +10 -0
- package/dist/server/bare/plugins/llamacpp-completion/ops/completion-stream.d.ts +1 -0
- package/dist/server/bare/plugins/llamacpp-completion/ops/completion-stream.js +131 -123
- package/dist/server/bare/plugins/llamacpp-completion/ops/finetune.js +110 -20
- package/dist/server/bare/plugins/llamacpp-completion/ops/kv-cache-session.d.ts +49 -23
- package/dist/server/bare/plugins/llamacpp-completion/ops/kv-cache-session.js +308 -143
- package/dist/server/bare/plugins/llamacpp-completion/ops/kv-cache-state.d.ts +1 -0
- package/dist/server/bare/plugins/llamacpp-completion/ops/kv-cache-state.js +2 -2
- package/dist/server/bare/plugins/llamacpp-completion/plugin.d.ts +6 -6
- package/dist/server/bare/plugins/llamacpp-completion/plugin.js +55 -36
- package/dist/server/bare/plugins/llamacpp-completion/transform.js +1 -7
- package/dist/server/bare/plugins/llamacpp-embedding/plugin.js +2 -3
- package/dist/server/bare/plugins/nmtcpp-translation/plugin.js +6 -5
- package/dist/server/bare/plugins/parakeet-transcription/plugin.js +27 -16
- package/dist/server/bare/plugins/tts-ggml/ops/parler-options.js +23 -3
- package/dist/server/bare/plugins/tts-ggml/plugin.d.ts +302 -4
- package/dist/server/bare/plugins/tts-ggml/plugin.js +131 -0
- package/dist/server/bare/plugins/whispercpp-transcription/plugin.js +48 -39
- package/dist/server/bare/registry/model-registry.d.ts +1 -0
- package/dist/server/bare/runtime/request-registry-singleton.js +29 -15
- package/dist/server/bare/runtime/request-registry.d.ts +27 -0
- package/dist/server/bare/runtime/request-registry.js +246 -76
- package/dist/server/bare/types/addon-responses.d.ts +4 -12
- package/dist/server/rpc/handlers/load-model/handler.js +2 -2
- package/dist/server/rpc/handlers/load-model/resolve-session.d.ts +1 -1
- package/dist/server/rpc/handlers/load-model/resolve-session.js +3 -3
- package/dist/server/rpc/handlers/load-model/resolve.d.ts +12 -2
- package/dist/server/rpc/handlers/load-model/resolve.js +90 -12
- package/dist/server/utils/audio/decoder.d.ts +37 -1
- package/dist/server/utils/audio/decoder.js +81 -51
- package/dist/server/utils/config-transform.d.ts +3 -0
- package/dist/server/utils/config-transform.js +6 -0
- package/dist/server/utils/index.d.ts +0 -1
- package/dist/server/utils/index.js +0 -1
- package/dist/server/utils/shard-utils.d.ts +1 -0
- package/dist/server/utils/shard-utils.js +14 -0
- package/dist/server/utils/tool-integration.d.ts +3 -10
- package/dist/server/utils/tool-integration.js +3 -12
- package/dist/utils/errors-server.d.ts +3 -0
- package/dist/utils/errors-server.js +5 -0
- package/package.json +12 -14
- package/dist/examples/llamacpp-dynamic-tools.d.ts +0 -2
- package/dist/examples/llamacpp-dynamic-tools.js +0 -152
- package/dist/models/update-models/codegen.d.ts +0 -3
- package/dist/models/update-models/codegen.js +0 -196
- package/dist/models/update-models/companions.d.ts +0 -30
- package/dist/models/update-models/companions.js +0 -296
- package/dist/models/update-models/history.d.ts +0 -24
- package/dist/models/update-models/history.js +0 -107
- package/dist/models/update-models/index.d.ts +0 -2
- package/dist/models/update-models/index.js +0 -123
- package/dist/models/update-models/naming.d.ts +0 -4
- package/dist/models/update-models/naming.js +0 -396
- package/dist/models/update-models/processing.d.ts +0 -8
- package/dist/models/update-models/processing.js +0 -65
- package/dist/models/update-models/registry.d.ts +0 -5
- package/dist/models/update-models/registry.js +0 -87
- package/dist/models/update-models/shards.d.ts +0 -4
- package/dist/models/update-models/shards.js +0 -57
- package/dist/models/update-models/types.d.ts +0 -88
- package/dist/models/update-models/types.js +0 -1
- package/dist/models/update-models/utils.d.ts +0 -3
- package/dist/models/update-models/utils.js +0 -21
- package/dist/server/bare/plugins/llamacpp-completion/ops/batch-cancelled.d.ts +0 -23
- package/dist/server/bare/plugins/llamacpp-completion/ops/batch-cancelled.js +0 -30
- package/dist/server/utils/expand-gguf-shards.d.ts +0 -3
- package/dist/server/utils/expand-gguf-shards.js +0 -22
|
@@ -11,36 +11,22 @@ import { getServerLogger } from '../../../../../logging/index.js';
|
|
|
11
11
|
// the caller — typically `withRequestContext(...)`.
|
|
12
12
|
const moduleLogger = getServerLogger();
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* Every turn must finish through `commitTurn` or `rollback` so all
|
|
23
|
-
* inference state stays aligned, the active-path ref is released, and
|
|
24
|
-
* marker metadata follows the cache directory lifecycle.
|
|
14
|
+
* What the kv-cache file on disk is known to cover, keyed by cache path.
|
|
15
|
+
* Written by `commitTurn`, read by `getSavedCount`, deleted by `rollback` /
|
|
16
|
+
* `delete` / `dropStaleSavedCount`. The same INVARIANT that existed in
|
|
17
|
+
* `kv-cache-state.ts` still holds: an entry is present only when the
|
|
18
|
+
* corresponding `.bin` file is considered trustworthy. Cancelled or
|
|
19
|
+
* zero-token turns must remove the entry so the next-turn slice doesn't read
|
|
20
|
+
* a stale boundary.
|
|
25
21
|
*/
|
|
26
|
-
|
|
27
|
-
// for the in-memory KV-cache bookkeeping. -----
|
|
28
|
-
/**
|
|
29
|
-
* Number of chat messages the kv-cache file on disk is known to cover,
|
|
30
|
-
* keyed by cache path. Written by `commitTurn`, read by `getSavedCount`,
|
|
31
|
-
* deleted by `rollback` / `delete` / `dropStaleSavedCount`. The same
|
|
32
|
-
* INVARIANT that existed in `kv-cache-state.ts` still holds: an entry is
|
|
33
|
-
* present only when the corresponding `.bin` file is considered
|
|
34
|
-
* trustworthy. Cancelled or zero-token turns must remove the entry so
|
|
35
|
-
* the next-turn slice doesn't read a stale boundary.
|
|
36
|
-
*/
|
|
37
|
-
const cachedMessageCounts = new Map();
|
|
22
|
+
const cachedPrefixes = new Map();
|
|
38
23
|
/**
|
|
39
24
|
* In-memory registry of caches initialized this session. The addon
|
|
40
25
|
* defers disk writes, so the absence of a `.bin` file on disk isn't
|
|
41
26
|
* proof that the cache hasn't been primed in this worker process. Keyed
|
|
42
|
-
* by
|
|
43
|
-
* older worker runs still hit the lazy-load
|
|
27
|
+
* by the resolved cache path, so aliased keys that name one file share an
|
|
28
|
+
* entry and on-disk caches from older worker runs still hit the lazy-load
|
|
29
|
+
* path in `beginTurn`.
|
|
44
30
|
*/
|
|
45
31
|
const initializedCaches = new Set();
|
|
46
32
|
const activeCachePaths = new Map();
|
|
@@ -51,8 +37,72 @@ const AUTO_CACHE_SWEEP_INTERVAL_MS = 5 * 60 * 1000;
|
|
|
51
37
|
let lastAutoCacheSweepMs = 0;
|
|
52
38
|
let autoCacheSweepInFlight = null;
|
|
53
39
|
let cacheStateLockTail = Promise.resolve();
|
|
54
|
-
|
|
55
|
-
|
|
40
|
+
const cachePathLocks = new Map();
|
|
41
|
+
// Internal sentinel so acquisition paths prune directories/markers only when a
|
|
42
|
+
// queued lock wait was aborted, not when lock acquisition failed for another
|
|
43
|
+
// reason.
|
|
44
|
+
class CacheLockAbortError extends Error {
|
|
45
|
+
constructor(cause) {
|
|
46
|
+
super('cache lock wait aborted', cause !== undefined ? { cause } : undefined);
|
|
47
|
+
this.name = 'CacheLockAbortError';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// Case-fold the lock-map key so case-only path variants (e.g. "Session" vs
|
|
51
|
+
// "session"), which name the SAME file on case-insensitive filesystems (default
|
|
52
|
+
// macOS/Windows), serialise on one lock and can't interleave writes. Over-locks
|
|
53
|
+
// case-only variants on case-sensitive filesystems, which is safe. NOTE: the
|
|
54
|
+
// other per-path bookkeeping (activeCachePaths, cachedPrefixes, retention)
|
|
55
|
+
// stays case-sensitive, so case-only key variants are otherwise unsupported —
|
|
56
|
+
// see the KV-cache system doc.
|
|
57
|
+
function lockKeyFor(cachePath) {
|
|
58
|
+
return cachePath.toLowerCase();
|
|
59
|
+
}
|
|
60
|
+
function releaseCachePathWriteLock(lockKey) {
|
|
61
|
+
const lock = cachePathLocks.get(lockKey);
|
|
62
|
+
if (!lock)
|
|
63
|
+
return;
|
|
64
|
+
const next = lock.waiters.shift();
|
|
65
|
+
if (next) {
|
|
66
|
+
next.grant();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
lock.held = false;
|
|
70
|
+
cachePathLocks.delete(lockKey);
|
|
71
|
+
}
|
|
72
|
+
// Returns the release fn. Aborting while queued rejects and removes the waiter.
|
|
73
|
+
async function acquireCachePathWriteLock(cachePath, signal) {
|
|
74
|
+
const lockKey = lockKeyFor(cachePath);
|
|
75
|
+
let lock = cachePathLocks.get(lockKey);
|
|
76
|
+
if (!lock) {
|
|
77
|
+
lock = { held: false, waiters: [] };
|
|
78
|
+
cachePathLocks.set(lockKey, lock);
|
|
79
|
+
}
|
|
80
|
+
if (!lock.held) {
|
|
81
|
+
lock.held = true;
|
|
82
|
+
return () => releaseCachePathWriteLock(lockKey);
|
|
83
|
+
}
|
|
84
|
+
await new Promise((resolve, reject) => {
|
|
85
|
+
const waiter = {
|
|
86
|
+
grant: () => {
|
|
87
|
+
signal?.removeEventListener('abort', onAbort);
|
|
88
|
+
resolve();
|
|
89
|
+
},
|
|
90
|
+
drop: (reason) => reject(reason)
|
|
91
|
+
};
|
|
92
|
+
const onAbort = () => {
|
|
93
|
+
const index = lock.waiters.indexOf(waiter);
|
|
94
|
+
if (index !== -1)
|
|
95
|
+
lock.waiters.splice(index, 1);
|
|
96
|
+
waiter.drop(new CacheLockAbortError(signal?.reason));
|
|
97
|
+
};
|
|
98
|
+
if (signal?.aborted) {
|
|
99
|
+
onAbort();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
lock.waiters.push(waiter);
|
|
103
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
104
|
+
});
|
|
105
|
+
return () => releaseCachePathWriteLock(lockKey);
|
|
56
106
|
}
|
|
57
107
|
function markCachePathActive(cachePath) {
|
|
58
108
|
activeCachePaths.set(cachePath, (activeCachePaths.get(cachePath) ?? 0) + 1);
|
|
@@ -67,6 +117,10 @@ function releaseCachePath(cachePath) {
|
|
|
67
117
|
}
|
|
68
118
|
activeCachePaths.set(cachePath, count - 1);
|
|
69
119
|
}
|
|
120
|
+
// Paths in-flight turns hold, so pruning skips a directory still in use.
|
|
121
|
+
function snapshotActivePaths() {
|
|
122
|
+
return Array.from(activeCachePaths.keys());
|
|
123
|
+
}
|
|
70
124
|
function isCacheKeyActive(cacheKey) {
|
|
71
125
|
const cacheDirectory = path.join(getKVCacheDir(), cacheKey);
|
|
72
126
|
return Array.from(activeCachePaths.keys()).some((cachePath) => isCachePathWithinDirectory(cacheDirectory, cachePath));
|
|
@@ -151,15 +205,18 @@ export function createKvCacheSession(modelId, options) {
|
|
|
151
205
|
// WeakMap so handles drop their state once the handler scope releases
|
|
152
206
|
// the reference; the module-scoped maps above survive.
|
|
153
207
|
const turnState = new WeakMap();
|
|
154
|
-
function makeHandle(cachePath,
|
|
208
|
+
function makeHandle(cachePath, autoCacheKey, releaseWriteLock = () => { }, signal) {
|
|
209
|
+
const cached = cachedPrefixes.get(cachePath);
|
|
155
210
|
const handle = {
|
|
156
211
|
cachePath,
|
|
157
|
-
savedCount:
|
|
212
|
+
savedCount: cached?.messages ?? 0,
|
|
213
|
+
toolBlockCached: cached?.toolBlock ?? false
|
|
158
214
|
};
|
|
159
215
|
turnState.set(handle, {
|
|
160
216
|
cachePath,
|
|
161
|
-
registryKey,
|
|
162
217
|
...(autoCacheKey !== undefined && { autoCacheKey }),
|
|
218
|
+
...(signal !== undefined && { signal }),
|
|
219
|
+
releaseWriteLock,
|
|
163
220
|
committed: false,
|
|
164
221
|
rolledBack: false
|
|
165
222
|
});
|
|
@@ -168,19 +225,41 @@ export function createKvCacheSession(modelId, options) {
|
|
|
168
225
|
}
|
|
169
226
|
async function beginCustom(input) {
|
|
170
227
|
const cachePath = await getCacheFilePath(modelId, input.configHash, input.customKey);
|
|
171
|
-
|
|
172
|
-
|
|
228
|
+
// Held for the whole turn so a same-file peer can't interleave writes.
|
|
229
|
+
// Released on commit/rollback/error; throws if aborted while queued.
|
|
230
|
+
let releaseWriteLock;
|
|
231
|
+
try {
|
|
232
|
+
releaseWriteLock = await acquireCachePathWriteLock(cachePath, input.signal);
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
// Aborted while queued: no lock is held to release, and getCacheFilePath
|
|
236
|
+
// already mkdir'd this key's parent (a distinct dir under a case variant),
|
|
237
|
+
// so prune it before the cancellation propagates.
|
|
238
|
+
if (error instanceof CacheLockAbortError) {
|
|
239
|
+
await pruneEmptyCacheDirectories(cachePath, snapshotActivePaths());
|
|
240
|
+
}
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
// A turn cancelled by the time it holds the lock must not prime (native work).
|
|
244
|
+
// getCacheFilePath already mkdir'd the parent, so prune it before surfacing
|
|
245
|
+
// the cancellation the plugin rides.
|
|
246
|
+
if (input.signal?.aborted) {
|
|
247
|
+
await pruneEmptyCacheDirectories(cachePath, snapshotActivePaths());
|
|
248
|
+
releaseWriteLock();
|
|
249
|
+
throw new CacheLockAbortError(input.signal.reason);
|
|
250
|
+
}
|
|
251
|
+
const handle = makeHandle(cachePath, undefined, releaseWriteLock, input.signal);
|
|
173
252
|
try {
|
|
174
253
|
// In-memory registry check first — the addon defers disk writes, so
|
|
175
254
|
// a freshly-primed cache may not yet exist on disk. If the
|
|
176
255
|
// in-memory flag isn't set, fall back to a filesystem probe so
|
|
177
256
|
// caches surviving across worker restarts still hit the reuse path.
|
|
178
|
-
let exists = initializedCaches.has(
|
|
257
|
+
let exists = initializedCaches.has(cachePath);
|
|
179
258
|
if (!exists) {
|
|
180
259
|
try {
|
|
181
260
|
await fsPromises.access(cachePath);
|
|
182
261
|
exists = true;
|
|
183
|
-
initializedCaches.add(
|
|
262
|
+
initializedCaches.add(cachePath);
|
|
184
263
|
}
|
|
185
264
|
catch {
|
|
186
265
|
exists = false;
|
|
@@ -188,49 +267,109 @@ export function createKvCacheSession(modelId, options) {
|
|
|
188
267
|
}
|
|
189
268
|
logCacheStatus(input.customKey, exists);
|
|
190
269
|
if (!exists) {
|
|
270
|
+
// Recreate the parent dir if a same-key peer's rollback pruned it after our lock wait.
|
|
271
|
+
await fsPromises.mkdir(path.dirname(cachePath), { recursive: true });
|
|
272
|
+
// The access probe / mkdir above yielded, so a cancel may have landed
|
|
273
|
+
// since the acquire-time check — re-check right before native priming.
|
|
274
|
+
// The catch below prunes the directory just created.
|
|
275
|
+
if (input.signal?.aborted)
|
|
276
|
+
throw new CacheLockAbortError(input.signal.reason);
|
|
191
277
|
await input.primeIfMissing(cachePath);
|
|
192
278
|
await verifyPrimedFile(cachePath, logger);
|
|
193
|
-
initializedCaches.add(
|
|
279
|
+
initializedCaches.add(cachePath);
|
|
194
280
|
}
|
|
195
281
|
return handle;
|
|
196
282
|
}
|
|
197
283
|
catch (error) {
|
|
198
284
|
releaseCachePath(cachePath);
|
|
285
|
+
await pruneEmptyCacheDirectories(cachePath, snapshotActivePaths());
|
|
286
|
+
releaseWriteLock();
|
|
199
287
|
throw error;
|
|
200
288
|
}
|
|
201
289
|
}
|
|
202
290
|
async function beginAuto(input) {
|
|
203
|
-
// The
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
|
|
207
|
-
// `targetCachePath`.
|
|
208
|
-
const setup = await withCacheStateLock(async () => {
|
|
291
|
+
// The path comes from a history lookup, so: discover it, lock it (outside
|
|
292
|
+
// the state lock, or commitTurn's rename could deadlock a waiter), then
|
|
293
|
+
// re-check existence under the lock in case a same-file peer just renamed it.
|
|
294
|
+
const discovered = await withCacheStateLock(async () => {
|
|
209
295
|
const existingCache = await findMatchingCache(modelId, input.configHash, input.history);
|
|
210
296
|
const cacheInfo = existingCache ?? (await getCurrentCacheInfo(modelId, input.configHash, input.history));
|
|
211
|
-
const registryKey = initRegistryKey(modelId, input.configHash, cacheInfo.cacheKey);
|
|
212
297
|
return {
|
|
213
|
-
cacheExists: existingCache !== null,
|
|
214
298
|
cachePath: cacheInfo.cachePath,
|
|
215
|
-
cacheKey: cacheInfo.cacheKey
|
|
216
|
-
registryKey,
|
|
217
|
-
handle: makeHandle(cacheInfo.cachePath, registryKey, cacheInfo.cacheKey)
|
|
299
|
+
cacheKey: cacheInfo.cacheKey
|
|
218
300
|
};
|
|
219
301
|
});
|
|
220
|
-
const {
|
|
302
|
+
const { cachePath, cacheKey } = discovered;
|
|
303
|
+
let releaseWriteLock;
|
|
304
|
+
try {
|
|
305
|
+
releaseWriteLock = await acquireCachePathWriteLock(cachePath, input.signal);
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
// Aborted while queued: no lock is held to release. Discovery already
|
|
309
|
+
// mkdir'd the parent and wrote the auto marker; clean both before the
|
|
310
|
+
// cancellation propagates.
|
|
311
|
+
if (error instanceof CacheLockAbortError) {
|
|
312
|
+
await pruneEmptyCacheDirectories(cachePath, snapshotActivePaths());
|
|
313
|
+
await removeAutoCacheMarkerIfMissing(cacheKey);
|
|
314
|
+
}
|
|
315
|
+
throw error;
|
|
316
|
+
}
|
|
317
|
+
// A turn cancelled by the time it holds the lock must not prime (native work).
|
|
318
|
+
// Discovery already mkdir'd the parent and wrote the auto marker; clean both
|
|
319
|
+
// before surfacing the cancellation.
|
|
320
|
+
if (input.signal?.aborted) {
|
|
321
|
+
await pruneEmptyCacheDirectories(cachePath, snapshotActivePaths());
|
|
322
|
+
await removeAutoCacheMarkerIfMissing(cacheKey);
|
|
323
|
+
releaseWriteLock();
|
|
324
|
+
throw new CacheLockAbortError(input.signal.reason);
|
|
325
|
+
}
|
|
326
|
+
let handle;
|
|
327
|
+
let cacheExists;
|
|
328
|
+
try {
|
|
329
|
+
const resolved = await withCacheStateLock(async () => {
|
|
330
|
+
// Mark the path active under the state lock so retention can't evict it.
|
|
331
|
+
const h = makeHandle(cachePath, cacheKey, releaseWriteLock, input.signal);
|
|
332
|
+
let exists = initializedCaches.has(cachePath);
|
|
333
|
+
if (!exists) {
|
|
334
|
+
try {
|
|
335
|
+
await fsPromises.access(cachePath);
|
|
336
|
+
exists = true;
|
|
337
|
+
initializedCaches.add(cachePath);
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
exists = false;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return { h, exists };
|
|
344
|
+
});
|
|
345
|
+
handle = resolved.h;
|
|
346
|
+
cacheExists = resolved.exists;
|
|
347
|
+
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
releaseWriteLock();
|
|
350
|
+
throw error;
|
|
351
|
+
}
|
|
221
352
|
logCacheStatus('auto', cacheExists);
|
|
222
353
|
try {
|
|
223
354
|
if (!cacheExists) {
|
|
355
|
+
// Recreate the parent dir if a same-file peer's rename pruned it.
|
|
356
|
+
await fsPromises.mkdir(path.dirname(cachePath), { recursive: true });
|
|
357
|
+
// The discovery / mkdir above yielded, so a cancel may have landed since
|
|
358
|
+
// the acquire-time check — re-check right before native priming. The
|
|
359
|
+
// catch below prunes the directory and auto marker.
|
|
360
|
+
if (input.signal?.aborted)
|
|
361
|
+
throw new CacheLockAbortError(input.signal.reason);
|
|
224
362
|
await input.primeIfMissing(cachePath);
|
|
225
363
|
await verifyPrimedFile(cachePath, logger);
|
|
226
|
-
initializedCaches.add(
|
|
364
|
+
initializedCaches.add(cachePath);
|
|
227
365
|
}
|
|
228
366
|
return handle;
|
|
229
367
|
}
|
|
230
368
|
catch (error) {
|
|
231
369
|
releaseCachePath(cachePath);
|
|
232
|
-
await pruneEmptyCacheDirectories(cachePath);
|
|
370
|
+
await pruneEmptyCacheDirectories(cachePath, snapshotActivePaths());
|
|
233
371
|
await removeAutoCacheMarkerIfMissing(cacheKey);
|
|
372
|
+
releaseWriteLock();
|
|
234
373
|
throw error;
|
|
235
374
|
}
|
|
236
375
|
}
|
|
@@ -255,7 +394,7 @@ export function createKvCacheSession(modelId, options) {
|
|
|
255
394
|
// at the same path. Verify the file persisted (the addon
|
|
256
395
|
// currently swallows save errors — see TODO in
|
|
257
396
|
// `verifySaveAndRecord`) and record the new boundary.
|
|
258
|
-
const ok = await verifySaveAndRecord(state.cachePath, result.messageCount);
|
|
397
|
+
const ok = await verifySaveAndRecord(state.cachePath, result.messageCount, result.toolBlockCached);
|
|
259
398
|
if (!ok) {
|
|
260
399
|
// The expected save didn't land — treat the turn as a rollback
|
|
261
400
|
// so the next turn re-primes cleanly.
|
|
@@ -264,51 +403,79 @@ export function createKvCacheSession(modelId, options) {
|
|
|
264
403
|
}
|
|
265
404
|
state.committed = true;
|
|
266
405
|
releaseCachePath(state.cachePath);
|
|
406
|
+
state.releaseWriteLock();
|
|
267
407
|
return;
|
|
268
408
|
}
|
|
269
409
|
const sourceCachePath = state.cachePath;
|
|
270
410
|
const sourceCacheKey = state.autoCacheKey;
|
|
271
411
|
const targetCacheKey = path.basename(path.dirname(path.dirname(result.targetCachePath)));
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
await
|
|
280
|
-
return
|
|
412
|
+
// Hold the target's lock across the rename AND commit verification, so a peer
|
|
413
|
+
// resolving to the same file can't observe it before its saved count is
|
|
414
|
+
// published. Abortable: a cancel while waiting for the lock rejects here.
|
|
415
|
+
const releaseTargetLock = await acquireCachePathWriteLock(result.targetCachePath, state.signal);
|
|
416
|
+
try {
|
|
417
|
+
// A cancel that landed while we waited for the target lock must not commit.
|
|
418
|
+
if (state.signal?.aborted) {
|
|
419
|
+
await runRollback(state);
|
|
420
|
+
return;
|
|
281
421
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
422
|
+
const renamed = await withCacheStateLock(async () => {
|
|
423
|
+
markCachePathActive(result.targetCachePath);
|
|
424
|
+
try {
|
|
425
|
+
await fsPromises.mkdir(path.dirname(result.targetCachePath), { recursive: true });
|
|
426
|
+
await markAutoCacheKey(targetCacheKey);
|
|
427
|
+
if (!(await renameCacheFile(sourceCachePath, result.targetCachePath))) {
|
|
428
|
+
releaseCachePath(result.targetCachePath);
|
|
429
|
+
await pruneEmptyCacheDirectories(result.targetCachePath, snapshotActivePaths());
|
|
430
|
+
await removeAutoCacheMarkerIfMissing(targetCacheKey);
|
|
431
|
+
return false;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
catch (setupError) {
|
|
435
|
+
// mkdir/markAutoCacheKey/rename threw after the target active-ref was
|
|
436
|
+
// taken: release it (first, so it can't leak) and prune any directory
|
|
437
|
+
// or marker created above, then propagate.
|
|
438
|
+
releaseCachePath(result.targetCachePath);
|
|
439
|
+
await pruneEmptyCacheDirectories(result.targetCachePath, snapshotActivePaths());
|
|
440
|
+
await removeAutoCacheMarkerIfMissing(targetCacheKey);
|
|
441
|
+
throw setupError;
|
|
442
|
+
}
|
|
443
|
+
releaseCachePath(sourceCachePath);
|
|
444
|
+
state.cachePath = result.targetCachePath;
|
|
445
|
+
state.autoCacheKey = targetCacheKey;
|
|
446
|
+
await pruneEmptyCacheDirectories(sourceCachePath, snapshotActivePaths());
|
|
447
|
+
if (sourceCacheKey !== undefined) {
|
|
448
|
+
await removeAutoCacheMarkerIfMissing(sourceCacheKey);
|
|
449
|
+
}
|
|
450
|
+
cachedPrefixes.delete(sourceCachePath);
|
|
451
|
+
// state.cachePath was just reassigned to the target; clear the SOURCE
|
|
452
|
+
// entry, not the freshly-valid target.
|
|
453
|
+
initializedCaches.delete(sourceCachePath);
|
|
454
|
+
return true;
|
|
455
|
+
});
|
|
456
|
+
if (!renamed) {
|
|
457
|
+
logger.warn(`[kv-cache] Auto cache rename failed; rolling back. from=${sourceCachePath} to=${result.targetCachePath}`);
|
|
458
|
+
await runRollback(state);
|
|
459
|
+
return;
|
|
288
460
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
461
|
+
const ok = await verifySaveAndRecord(result.targetCachePath, result.messageCount, result.toolBlockCached);
|
|
462
|
+
if (!ok) {
|
|
463
|
+
// Rename succeeded but the file isn't where we expected. Roll back via
|
|
464
|
+
// the target path instead of the (now-empty) source.
|
|
465
|
+
await runRollback(state);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
// Successful auto-rename. The handle's `cachePath` field still points at
|
|
469
|
+
// the (now-gone) source path — fine, the handle is committed and won't
|
|
470
|
+
// roll back. Future turns compute fresh paths.
|
|
471
|
+
state.committed = true;
|
|
472
|
+
releaseCachePath(state.cachePath);
|
|
473
|
+
state.releaseWriteLock();
|
|
474
|
+
scheduleAutoCacheSweep(logger);
|
|
297
475
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
// Rename succeeded but the file isn't where we expected. Roll
|
|
301
|
-
// back via the target path instead of the (now-empty) source.
|
|
302
|
-
await runRollback(state);
|
|
303
|
-
return;
|
|
476
|
+
finally {
|
|
477
|
+
releaseTargetLock();
|
|
304
478
|
}
|
|
305
|
-
// Successful auto-rename. The handle's `cachePath` field still
|
|
306
|
-
// points at the (now-gone) source path — that's fine, the handle
|
|
307
|
-
// is committed and won't roll back. Future turns compute fresh
|
|
308
|
-
// paths.
|
|
309
|
-
state.committed = true;
|
|
310
|
-
releaseCachePath(state.cachePath);
|
|
311
|
-
scheduleAutoCacheSweep(logger);
|
|
312
479
|
}
|
|
313
480
|
async function rollback(turn) {
|
|
314
481
|
const state = turnState.get(turn);
|
|
@@ -329,14 +496,17 @@ export function createKvCacheSession(modelId, options) {
|
|
|
329
496
|
catch (unlinkError) {
|
|
330
497
|
logger.warn(`[kv-cache] Failed to remove cache file during rollback; next turn may load stale KV state. path=${state.cachePath} error=${unlinkError instanceof Error ? unlinkError.message : String(unlinkError)}`);
|
|
331
498
|
}
|
|
332
|
-
|
|
499
|
+
// Release before pruning so an empty parent can go; a sibling still holding
|
|
500
|
+
// the path keeps it in the active snapshot and protects the directory.
|
|
501
|
+
releaseCachePath(state.cachePath);
|
|
502
|
+
await pruneEmptyCacheDirectories(state.cachePath, snapshotActivePaths());
|
|
333
503
|
if (state.autoCacheKey !== undefined) {
|
|
334
504
|
await removeAutoCacheMarkerIfMissing(state.autoCacheKey);
|
|
335
505
|
}
|
|
336
|
-
initializedCaches.delete(state.
|
|
337
|
-
|
|
338
|
-
releaseCachePath(state.cachePath);
|
|
506
|
+
initializedCaches.delete(state.cachePath);
|
|
507
|
+
cachedPrefixes.delete(state.cachePath);
|
|
339
508
|
state.rolledBack = true;
|
|
509
|
+
state.releaseWriteLock();
|
|
340
510
|
if (state.autoCacheKey !== undefined)
|
|
341
511
|
scheduleAutoCacheSweep(logger);
|
|
342
512
|
}
|
|
@@ -344,7 +514,7 @@ export function createKvCacheSession(modelId, options) {
|
|
|
344
514
|
const state = turnState.get(turn);
|
|
345
515
|
if (!state)
|
|
346
516
|
return;
|
|
347
|
-
|
|
517
|
+
cachedPrefixes.delete(state.cachePath);
|
|
348
518
|
}
|
|
349
519
|
return {
|
|
350
520
|
beginTurn,
|
|
@@ -370,24 +540,26 @@ export function createKvCacheSession(modelId, options) {
|
|
|
370
540
|
* Layers cleared, in order:
|
|
371
541
|
* 1. On-disk: `deleteCache(...)` removes the matching directory
|
|
372
542
|
* tree (or wipes and recreates the root for `all: true`).
|
|
373
|
-
* 2. `
|
|
543
|
+
* 2. `cachedPrefixes`: prefix-cleanup by the removed directory
|
|
374
544
|
* so any per-cache count under the deleted tree is forgotten.
|
|
375
|
-
* 3. `initializedCaches`:
|
|
376
|
-
*
|
|
545
|
+
* 3. `initializedCaches`: prefix-cleanup by the same removed
|
|
546
|
+
* directory, since it is keyed by the resolved cache path.
|
|
377
547
|
*
|
|
378
|
-
* Concurrency with in-flight turns: this delete
|
|
379
|
-
*
|
|
380
|
-
*
|
|
381
|
-
*
|
|
382
|
-
* `
|
|
383
|
-
*
|
|
384
|
-
*
|
|
385
|
-
*
|
|
548
|
+
* Concurrency with in-flight turns: this delete does not take the
|
|
549
|
+
* per-cache-path write locks, so it races any turn holding a `TurnHandle`
|
|
550
|
+
* for the same key. Deleting a key that is in active use is unsupported.
|
|
551
|
+
* Each individual mutation is idempotent (`unlink` no-ops if missing,
|
|
552
|
+
* `Map.delete` / `Set.delete` no-op on absent keys), but the layers are
|
|
553
|
+
* cleared without a lock, so an interleaving that lands the delete's
|
|
554
|
+
* in-memory cleanup after a concurrent turn has already renamed/committed
|
|
555
|
+
* its file splits state: the file stays on disk while its saved-count and
|
|
556
|
+
* init flag are cleared, so the next turn sees the file, skips priming, and
|
|
557
|
+
* reports `savedCount=0`. Callers must not delete a key that is in active use.
|
|
386
558
|
*/
|
|
387
559
|
export async function deleteKvCacheState(target) {
|
|
388
560
|
if ('all' in target) {
|
|
389
561
|
const removed = await deleteCacheUtil({ all: true });
|
|
390
|
-
|
|
562
|
+
cachedPrefixes.clear();
|
|
391
563
|
initializedCaches.clear();
|
|
392
564
|
// `removed` is the kv-cache root dir; surfaces it for ops
|
|
393
565
|
// visibility but isn't part of the contract.
|
|
@@ -399,20 +571,20 @@ export async function deleteKvCacheState(target) {
|
|
|
399
571
|
...(target.modelId !== undefined && { modelId: target.modelId })
|
|
400
572
|
});
|
|
401
573
|
if (target.modelId === undefined) {
|
|
402
|
-
|
|
574
|
+
// Remove the marker by the root-relative key: an alias like "./<16hex>"
|
|
575
|
+
// still resolves to the canonical auto key, while a nested key such as
|
|
576
|
+
// "tenant/<16hex>" stays distinct — path.basename would wrongly collapse it
|
|
577
|
+
// onto the unrelated top-level auto marker.
|
|
578
|
+
await removeAutoCacheMarker(path.relative(getKVCacheDir(), removedPath));
|
|
403
579
|
}
|
|
404
580
|
// Prefix-cleanup the in-memory counts. The on-disk directory tree
|
|
405
581
|
// is `{kvCacheRoot}/{kvCacheKey}[/{modelId}]/`, so every entry in
|
|
406
|
-
// `
|
|
582
|
+
// `cachedPrefixes` whose key is the removed directory itself
|
|
407
583
|
// or sits beneath it must go.
|
|
408
584
|
clearCachedMessageCountsByPrefix(removedPath, path.sep);
|
|
409
|
-
//
|
|
410
|
-
//
|
|
411
|
-
|
|
412
|
-
clearInitializedCachesByScope({
|
|
413
|
-
cacheKey: target.kvCacheKey,
|
|
414
|
-
...(target.modelId !== undefined && { modelId: target.modelId })
|
|
415
|
-
});
|
|
585
|
+
// initializedCaches is keyed by the resolved cachePath too, so clear it by
|
|
586
|
+
// the same removed-directory prefix as cachedPrefixes above.
|
|
587
|
+
clearInitializedCachesByPrefix(removedPath, path.sep);
|
|
416
588
|
}
|
|
417
589
|
// ----- private helpers -----
|
|
418
590
|
/**
|
|
@@ -454,7 +626,7 @@ async function verifyPrimedFile(cachePath, logger) {
|
|
|
454
626
|
catch (statError) {
|
|
455
627
|
// ENOENT is the common case here — addon prime returned without
|
|
456
628
|
// calling save (most often: signal abort during prefill).
|
|
457
|
-
await pruneEmptyCacheDirectories(cachePath);
|
|
629
|
+
await pruneEmptyCacheDirectories(cachePath, snapshotActivePaths());
|
|
458
630
|
throw new Error(`[kv-cache] prime closure resolved but no cache file was written. path=${cachePath} cause=${statError instanceof Error ? statError.message : String(statError)}`);
|
|
459
631
|
}
|
|
460
632
|
if (stats.size === 0) {
|
|
@@ -463,7 +635,7 @@ async function verifyPrimedFile(cachePath, logger) {
|
|
|
463
635
|
// primary "prime didn't persist" condition.
|
|
464
636
|
try {
|
|
465
637
|
await fsPromises.unlink(cachePath);
|
|
466
|
-
await pruneEmptyCacheDirectories(cachePath);
|
|
638
|
+
await pruneEmptyCacheDirectories(cachePath, snapshotActivePaths());
|
|
467
639
|
}
|
|
468
640
|
catch (unlinkError) {
|
|
469
641
|
logger.warn(`[kv-cache] Failed to remove empty primed cache file. path=${cachePath} error=${unlinkError instanceof Error ? unlinkError.message : String(unlinkError)}`);
|
|
@@ -482,52 +654,41 @@ async function verifyPrimedFile(cachePath, logger) {
|
|
|
482
654
|
* false), drop the `access()` probe and wrap the `model.run()` call in
|
|
483
655
|
* a real try/catch that forwards the error.
|
|
484
656
|
*/
|
|
485
|
-
async function verifySaveAndRecord(cachePath, messageCount) {
|
|
657
|
+
async function verifySaveAndRecord(cachePath, messageCount, toolBlockCached) {
|
|
486
658
|
try {
|
|
487
659
|
await fsPromises.access(cachePath);
|
|
488
|
-
|
|
660
|
+
cachedPrefixes.set(cachePath, { messages: messageCount, toolBlock: toolBlockCached });
|
|
489
661
|
return true;
|
|
490
662
|
}
|
|
491
663
|
catch (err) {
|
|
492
|
-
|
|
664
|
+
cachedPrefixes.delete(cachePath);
|
|
493
665
|
logCacheSaveError(cachePath, err);
|
|
494
666
|
return false;
|
|
495
667
|
}
|
|
496
668
|
}
|
|
497
669
|
function clearCachedMessageCountsByPrefix(prefix, sep) {
|
|
498
670
|
if (!prefix) {
|
|
499
|
-
|
|
671
|
+
cachedPrefixes.clear();
|
|
500
672
|
return;
|
|
501
673
|
}
|
|
502
|
-
for (const key of
|
|
674
|
+
for (const key of cachedPrefixes.keys()) {
|
|
503
675
|
if (key === prefix) {
|
|
504
|
-
|
|
676
|
+
cachedPrefixes.delete(key);
|
|
505
677
|
continue;
|
|
506
678
|
}
|
|
507
679
|
if (!key.startsWith(prefix + sep))
|
|
508
680
|
continue;
|
|
509
|
-
|
|
681
|
+
cachedPrefixes.delete(key);
|
|
510
682
|
}
|
|
511
683
|
}
|
|
512
|
-
function
|
|
513
|
-
if (
|
|
684
|
+
function clearInitializedCachesByPrefix(prefix, sep) {
|
|
685
|
+
if (!prefix) {
|
|
514
686
|
initializedCaches.clear();
|
|
515
687
|
return;
|
|
516
688
|
}
|
|
517
689
|
for (const key of initializedCaches) {
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
if (firstSep === -1 || secondSep === -1)
|
|
521
|
-
continue;
|
|
522
|
-
const entryModelId = key.slice(0, firstSep);
|
|
523
|
-
const entryCacheKey = key.slice(secondSep + 1);
|
|
524
|
-
if (scope.cacheKey !== undefined && entryCacheKey !== scope.cacheKey) {
|
|
525
|
-
continue;
|
|
526
|
-
}
|
|
527
|
-
if (scope.modelId !== undefined && entryModelId !== scope.modelId) {
|
|
528
|
-
continue;
|
|
529
|
-
}
|
|
530
|
-
initializedCaches.delete(key);
|
|
690
|
+
if (key === prefix || key.startsWith(prefix + sep))
|
|
691
|
+
initializedCaches.delete(key);
|
|
531
692
|
}
|
|
532
693
|
}
|
|
533
694
|
// ----- test-only escape hatches -----
|
|
@@ -542,16 +703,19 @@ function clearInitializedCachesByScope(scope) {
|
|
|
542
703
|
*/
|
|
543
704
|
export const __kvCacheSessionTestHooks = {
|
|
544
705
|
getSavedCount(cachePath) {
|
|
545
|
-
return
|
|
706
|
+
return cachedPrefixes.get(cachePath)?.messages;
|
|
546
707
|
},
|
|
547
708
|
setSavedCountForTest(cachePath, count) {
|
|
548
|
-
|
|
709
|
+
cachedPrefixes.set(cachePath, { messages: count, toolBlock: false });
|
|
710
|
+
},
|
|
711
|
+
getToolBlockCachedForTest(cachePath) {
|
|
712
|
+
return cachedPrefixes.get(cachePath)?.toolBlock ?? false;
|
|
549
713
|
},
|
|
550
|
-
|
|
551
|
-
return initializedCaches.has(
|
|
714
|
+
hasInitializedPath(cachePath) {
|
|
715
|
+
return initializedCaches.has(cachePath);
|
|
552
716
|
},
|
|
553
|
-
|
|
554
|
-
|
|
717
|
+
getActivePathCountForTest(cachePath) {
|
|
718
|
+
return activeCachePaths.get(cachePath) ?? 0;
|
|
555
719
|
},
|
|
556
720
|
getLastAutoCacheSweepMsForTest() {
|
|
557
721
|
return lastAutoCacheSweepMs;
|
|
@@ -560,12 +724,13 @@ export const __kvCacheSessionTestHooks = {
|
|
|
560
724
|
lastAutoCacheSweepMs = value;
|
|
561
725
|
},
|
|
562
726
|
resetForTest() {
|
|
563
|
-
|
|
727
|
+
cachedPrefixes.clear();
|
|
564
728
|
initializedCaches.clear();
|
|
565
729
|
activeCachePaths.clear();
|
|
566
730
|
lastAutoCacheSweepMs = 0;
|
|
567
731
|
autoCacheSweepInFlight = null;
|
|
568
732
|
cacheStateLockTail = Promise.resolve();
|
|
733
|
+
cachePathLocks.clear();
|
|
569
734
|
},
|
|
570
735
|
waitForAutoCacheSweepForTest() {
|
|
571
736
|
return autoCacheSweepInFlight ?? Promise.resolve();
|