@qvac/sdk 0.10.2 → 0.11.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 +92 -54
- package/dist/client/api/cancel.d.ts +24 -32
- package/dist/client/api/cancel.js +58 -31
- package/dist/client/api/client-request-id.d.ts +17 -0
- package/dist/client/api/client-request-id.js +27 -0
- package/dist/client/api/completion-stream.d.ts +1 -1
- package/dist/client/api/completion-stream.js +34 -3
- package/dist/client/api/diffusion.d.ts +46 -0
- package/dist/client/api/diffusion.js +50 -1
- package/dist/client/api/download-asset.d.ts +12 -2
- package/dist/client/api/download-asset.js +21 -3
- package/dist/client/api/embed.d.ts +8 -4
- package/dist/client/api/embed.js +14 -1
- package/dist/client/api/get-model-info.d.ts +1 -1
- package/dist/client/api/index.d.ts +1 -0
- package/dist/client/api/index.js +1 -0
- package/dist/client/api/load-model.d.ts +12 -4
- package/dist/client/api/load-model.js +25 -1
- package/dist/client/api/rag.d.ts +9 -3
- package/dist/client/api/rag.js +23 -3
- package/dist/client/api/transcribe.d.ts +20 -8
- package/dist/client/api/transcribe.js +58 -4
- package/dist/client/api/unload-model.d.ts +5 -2
- package/dist/client/api/unload-model.js +9 -4
- package/dist/client/api/upscale.d.ts +45 -0
- package/dist/client/api/upscale.js +99 -0
- package/dist/client/rpc/expo-rpc-client.js +2 -1
- package/dist/client/rpc/node-rpc-client.js +18 -7
- package/dist/client/rpc/rpc-client.d.ts +2 -2
- package/dist/client/rpc/rpc-client.js +10 -4
- package/dist/client/rpc/rpc-error.d.ts +9 -0
- package/dist/client/rpc/rpc-error.js +92 -0
- package/dist/examples/cancel-by-request-id.d.ts +38 -0
- package/dist/examples/cancel-by-request-id.js +103 -0
- package/dist/examples/diffusion-esrgan-upscale.d.ts +2 -0
- package/dist/examples/diffusion-esrgan-upscale.js +78 -0
- package/dist/examples/download-with-cancel.js +10 -10
- package/dist/examples/embed-multi-gpu.d.ts +2 -0
- package/dist/examples/embed-multi-gpu.js +46 -0
- package/dist/examples/llamacpp-multi-gpu.d.ts +2 -0
- package/dist/examples/llamacpp-multi-gpu.js +57 -0
- package/dist/examples/rag/rag-hyperdb/cancellation.js +20 -17
- package/dist/examples/tools/llamacpp-tools-gemma4.d.ts +2 -0
- package/dist/examples/tools/llamacpp-tools-gemma4.js +76 -0
- package/dist/examples/tools/llamacpp-tools-qwen35.d.ts +2 -0
- package/dist/examples/tools/llamacpp-tools-qwen35.js +70 -0
- package/dist/examples/transcription/whispercpp-microphone-conversation.d.ts +2 -0
- package/dist/examples/transcription/whispercpp-microphone-conversation.js +102 -0
- package/dist/examples/tts/supertonic.js +1 -1
- package/dist/expo/plugins/withMobileBundle.d.ts +20 -4
- package/dist/expo/plugins/withMobileBundle.js +44 -6
- package/dist/index.d.ts +5 -2
- package/dist/index.js +21 -1
- package/dist/models/registry/models.d.ts +186 -318
- package/dist/models/registry/models.js +1522 -1667
- package/dist/pear/pre.d.ts +3 -0
- package/dist/pear/pre.js +3 -0
- package/dist/schemas/cancel.d.ts +167 -28
- package/dist/schemas/cancel.js +119 -19
- package/dist/schemas/common.d.ts +427 -208
- package/dist/schemas/common.js +3 -1
- package/dist/schemas/completion-event.d.ts +14 -0
- package/dist/schemas/completion-event.js +11 -1
- package/dist/schemas/completion-stream.d.ts +15 -1
- package/dist/schemas/completion-stream.js +25 -7
- package/dist/schemas/download-asset.d.ts +15 -9
- package/dist/schemas/download-asset.js +17 -6
- package/dist/schemas/embed.d.ts +1 -0
- package/dist/schemas/embed.js +5 -0
- package/dist/schemas/error.d.ts +31 -0
- package/dist/schemas/error.js +29 -1
- package/dist/schemas/finetune.d.ts +2 -0
- package/dist/schemas/finetune.js +5 -0
- package/dist/schemas/get-model-info.d.ts +6 -6
- package/dist/schemas/llamacpp-config.d.ts +54 -8
- package/dist/schemas/llamacpp-config.js +12 -0
- package/dist/schemas/load-model.d.ts +1911 -1260
- package/dist/schemas/load-model.js +15 -0
- package/dist/schemas/model-src-utils.d.ts +14 -14
- package/dist/schemas/model-types.d.ts +10 -10
- package/dist/schemas/ocr.d.ts +3 -3
- package/dist/schemas/plugin.d.ts +40 -1
- package/dist/schemas/plugin.js +10 -1
- package/dist/schemas/rag.d.ts +9 -0
- package/dist/schemas/rag.js +17 -9
- package/dist/schemas/registry.d.ts +30 -30
- package/dist/schemas/reload-config.d.ts +14 -14
- package/dist/schemas/sdcpp-config.d.ts +131 -17
- package/dist/schemas/sdcpp-config.js +163 -1
- package/dist/schemas/sdk-config.d.ts +649 -271
- package/dist/schemas/sdk-errors-client.d.ts +1 -0
- package/dist/schemas/sdk-errors-client.js +5 -0
- package/dist/schemas/sdk-errors-server.d.ts +5 -0
- package/dist/schemas/sdk-errors-server.js +25 -0
- package/dist/schemas/text-to-speech.d.ts +78 -78
- package/dist/schemas/transcription-config.d.ts +24 -24
- package/dist/schemas/transcription.d.ts +56 -2
- package/dist/schemas/transcription.js +27 -0
- package/dist/schemas/translate.d.ts +2 -0
- package/dist/schemas/translate.js +13 -2
- package/dist/schemas/translation-config.d.ts +40 -40
- package/dist/schemas/unload-model.d.ts +2 -1
- package/dist/schemas/unload-model.js +4 -1
- package/dist/scripts/run-unit-tests.js +15 -2
- package/dist/server/bare/ops/cancel.d.ts +27 -1
- package/dist/server/bare/ops/cancel.js +36 -10
- package/dist/server/bare/ops/embed.d.ts +3 -3
- package/dist/server/bare/ops/embed.js +144 -28
- package/dist/server/bare/ops/kv-cache-utils.d.ts +0 -7
- package/dist/server/bare/ops/kv-cache-utils.js +8 -47
- package/dist/server/bare/ops/transcribe.d.ts +10 -5
- package/dist/server/bare/ops/transcribe.js +196 -41
- package/dist/server/bare/ops/translate.d.ts +1 -1
- package/dist/server/bare/ops/translate.js +197 -88
- package/dist/server/bare/plugins/llamacpp-completion/ops/completion-stream.d.ts +12 -3
- package/dist/server/bare/plugins/llamacpp-completion/ops/completion-stream.js +170 -172
- package/dist/server/bare/plugins/llamacpp-completion/ops/finetune.js +125 -21
- package/dist/server/bare/plugins/llamacpp-completion/ops/kv-cache-session.d.ts +154 -0
- package/dist/server/bare/plugins/llamacpp-completion/ops/kv-cache-session.js +448 -0
- package/dist/server/bare/plugins/llamacpp-completion/ops/kv-cache-state.d.ts +21 -73
- package/dist/server/bare/plugins/llamacpp-completion/ops/kv-cache-state.js +18 -101
- package/dist/server/bare/plugins/llamacpp-completion/plugin.d.ts +30 -3
- package/dist/server/bare/plugins/llamacpp-completion/plugin.js +157 -85
- package/dist/server/bare/plugins/llamacpp-completion/transform.d.ts +10 -0
- package/dist/server/bare/plugins/llamacpp-completion/transform.js +34 -0
- package/dist/server/bare/plugins/llamacpp-embedding/plugin.d.ts +7 -0
- package/dist/server/bare/plugins/llamacpp-embedding/plugin.js +10 -1
- package/dist/server/bare/plugins/nmtcpp-translation/plugin.d.ts +17 -15
- package/dist/server/bare/plugins/nmtcpp-translation/plugin.js +5 -1
- package/dist/server/bare/plugins/onnx-ocr/plugin.d.ts +3 -3
- package/dist/server/bare/plugins/onnx-ocr/plugin.js +3 -0
- package/dist/server/bare/plugins/onnx-tts/plugin.d.ts +39 -39
- package/dist/server/bare/plugins/onnx-tts/plugin.js +6 -0
- package/dist/server/bare/plugins/parakeet-transcription/plugin.d.ts +29 -21
- package/dist/server/bare/plugins/parakeet-transcription/plugin.js +4 -1
- package/dist/server/bare/plugins/sdcpp-generation/ops/diffusion.js +8 -0
- package/dist/server/bare/plugins/sdcpp-generation/ops/upscale.d.ts +3 -0
- package/dist/server/bare/plugins/sdcpp-generation/ops/upscale.js +40 -0
- package/dist/server/bare/plugins/sdcpp-generation/plugin.d.ts +91 -16
- package/dist/server/bare/plugins/sdcpp-generation/plugin.js +97 -7
- package/dist/server/bare/plugins/whispercpp-transcription/plugin.d.ts +22 -3
- package/dist/server/bare/plugins/whispercpp-transcription/plugin.js +50 -14
- package/dist/server/bare/rag-hyperdb/index.d.ts +1 -1
- package/dist/server/bare/rag-hyperdb/index.js +1 -1
- package/dist/server/bare/rag-hyperdb/rag-operation-manager.d.ts +26 -4
- package/dist/server/bare/rag-hyperdb/rag-operation-manager.js +65 -28
- package/dist/server/bare/runtime/disposable-scope.d.ts +36 -0
- package/dist/server/bare/runtime/disposable-scope.js +69 -0
- package/dist/server/bare/runtime/index.d.ts +8 -0
- package/dist/server/bare/runtime/index.js +3 -0
- package/dist/server/bare/runtime/request-context.d.ts +49 -0
- package/dist/server/bare/runtime/request-context.js +1 -0
- package/dist/server/bare/runtime/request-id.d.ts +12 -0
- package/dist/server/bare/runtime/request-id.js +14 -0
- package/dist/server/bare/runtime/request-registry-singleton.d.ts +4 -0
- package/dist/server/bare/runtime/request-registry-singleton.js +27 -0
- package/dist/server/bare/runtime/request-registry.d.ts +136 -0
- package/dist/server/bare/runtime/request-registry.js +297 -0
- package/dist/server/bare/runtime/with-request-context.d.ts +22 -0
- package/dist/server/bare/runtime/with-request-context.js +53 -0
- package/dist/server/bare/types/addon-responses.d.ts +11 -1
- package/dist/server/rpc/handler-registry.js +26 -7
- package/dist/server/rpc/handlers/cancel-delegated.js +28 -31
- package/dist/server/rpc/handlers/cancelHandler.d.ts +30 -1
- package/dist/server/rpc/handlers/cancelHandler.js +54 -22
- package/dist/server/rpc/handlers/delete-cache.d.ts +9 -0
- package/dist/server/rpc/handlers/delete-cache.js +13 -15
- package/dist/server/rpc/handlers/download-asset.js +133 -40
- package/dist/server/rpc/handlers/index.d.ts +2 -0
- package/dist/server/rpc/handlers/index.js +2 -0
- package/dist/server/rpc/handlers/load-model/download-manager.d.ts +60 -2
- package/dist/server/rpc/handlers/load-model/download-manager.js +141 -8
- package/dist/server/rpc/handlers/load-model/handler.js +213 -98
- package/dist/server/rpc/handlers/load-model/http.js +3 -3
- package/dist/server/rpc/handlers/load-model/hyperdrive.js +1 -1
- package/dist/server/rpc/handlers/load-model/registry.js +1 -1
- package/dist/server/rpc/handlers/load-model/resolve-session.d.ts +20 -1
- package/dist/server/rpc/handlers/load-model/resolve-session.js +6 -5
- package/dist/server/rpc/handlers/load-model/resolve.d.ts +3 -2
- package/dist/server/rpc/handlers/load-model/resolve.js +9 -6
- package/dist/server/rpc/handlers/load-model/types.d.ts +24 -0
- package/dist/server/rpc/handlers/rag.js +133 -15
- package/dist/server/rpc/handlers/upscale-stream.d.ts +3 -0
- package/dist/server/rpc/handlers/upscale-stream.js +4 -0
- package/dist/server/utils/completion-normalizer.js +8 -0
- package/dist/server/utils/tools/dialect.js +4 -0
- package/dist/server/utils/tools/parser.js +16 -1
- package/dist/server/utils/tools/parsers/gemma4native.d.ts +4 -0
- package/dist/server/utils/tools/parsers/gemma4native.js +60 -0
- package/dist/server/utils/tools/parsers/qwen35.d.ts +4 -0
- package/dist/server/utils/tools/parsers/qwen35.js +120 -0
- package/dist/utils/aggregate-events.d.ts +9 -0
- package/dist/utils/aggregate-events.js +18 -3
- package/dist/utils/decorate-promise.d.ts +34 -0
- package/dist/utils/decorate-promise.js +35 -0
- package/dist/utils/errors-client.d.ts +3 -0
- package/dist/utils/errors-client.js +5 -0
- package/dist/utils/errors-server.d.ts +85 -0
- package/dist/utils/errors-server.js +97 -0
- package/package.json +20 -15
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { generateConfigHash } from "../../../ops/kv-cache-utils";
|
|
2
|
+
import type { CacheMessage } from "../../../../utils/index";
|
|
3
|
+
import type { Logger } from "../../../../../logging/types";
|
|
4
|
+
export interface TurnHandle {
|
|
5
|
+
/** Resolved on-disk cache file path the addon will read from / write to. */
|
|
6
|
+
readonly cachePath: string;
|
|
7
|
+
/**
|
|
8
|
+
* Snapshot of the on-disk saved-message count at `beginTurn` time
|
|
9
|
+
* (0 if the cache was just primed). Consumed by `decideCachedHistorySlice`
|
|
10
|
+
* to pick the message tail for the next addon call.
|
|
11
|
+
*/
|
|
12
|
+
readonly savedCount: number;
|
|
13
|
+
}
|
|
14
|
+
export interface BeginCustomTurnInput {
|
|
15
|
+
kind: "custom";
|
|
16
|
+
/** User-provided session key (`completion({ kvCache: "session-a" })`). */
|
|
17
|
+
customKey: string;
|
|
18
|
+
/** Hash of system prompt + (static) tool names. */
|
|
19
|
+
configHash: string;
|
|
20
|
+
/**
|
|
21
|
+
* Prime the cache by sending system prompt + (static) tools to the
|
|
22
|
+
* addon. Called when the cache doesn't exist in-memory OR on disk.
|
|
23
|
+
* Kept as an injected closure so this module has no dependency on the
|
|
24
|
+
* model registry / addon — the handler closes over `model` and tools.
|
|
25
|
+
*/
|
|
26
|
+
primeIfMissing: (cachePath: string) => Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
export interface BeginAutoTurnInput {
|
|
29
|
+
kind: "auto";
|
|
30
|
+
/** Hash of system prompt + (static) tool names. */
|
|
31
|
+
configHash: string;
|
|
32
|
+
/** Conversation history used to compute the pre-response cache key. */
|
|
33
|
+
history: CacheMessage[];
|
|
34
|
+
/** See `BeginCustomTurnInput.primeIfMissing`. */
|
|
35
|
+
primeIfMissing: (cachePath: string) => Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
export type BeginTurnInput = BeginCustomTurnInput | BeginAutoTurnInput;
|
|
38
|
+
export interface StaticCommitResult {
|
|
39
|
+
kind: "static";
|
|
40
|
+
/** `history.length + 1` — recorded at the turn's current `cachePath`. */
|
|
41
|
+
messageCount: number;
|
|
42
|
+
}
|
|
43
|
+
export interface AutoRenameCommitResult {
|
|
44
|
+
kind: "autoRename";
|
|
45
|
+
/**
|
|
46
|
+
* Destination path the addon's pre-response cache file should be
|
|
47
|
+
* renamed to (computed from `cacheMessages + responseText`). The
|
|
48
|
+
* stale entry at the source path is dropped from `cachedMessageCounts`
|
|
49
|
+
* and the new count is recorded at this target path.
|
|
50
|
+
*/
|
|
51
|
+
targetCachePath: string;
|
|
52
|
+
/** Number of messages the renamed cache represents (`savedHistory.length`). */
|
|
53
|
+
messageCount: number;
|
|
54
|
+
}
|
|
55
|
+
export type CommitResult = StaticCommitResult | AutoRenameCommitResult;
|
|
56
|
+
export interface KvCacheSession {
|
|
57
|
+
/**
|
|
58
|
+
* Open a new turn against the cache. Resolves the cache file path,
|
|
59
|
+
* primes the system-prompt cache if needed (delegated to
|
|
60
|
+
* `input.primeIfMissing`), marks the cache initialized, and returns a
|
|
61
|
+
* `TurnHandle` the handler attaches to `ctx.scope.defer(...)` for the
|
|
62
|
+
* rollback hook.
|
|
63
|
+
*/
|
|
64
|
+
beginTurn(input: BeginTurnInput): Promise<TurnHandle>;
|
|
65
|
+
/**
|
|
66
|
+
* Commit a successful turn — records the new saved-message count,
|
|
67
|
+
* preserves the cache file, and (for auto-cache turns) renames the
|
|
68
|
+
* addon's pre-response file to the post-response path. Flips the
|
|
69
|
+
* turn's internal `committed` flag so the deferred `rollback` becomes
|
|
70
|
+
* a no-op on the happy path.
|
|
71
|
+
*/
|
|
72
|
+
commitTurn(turn: TurnHandle, result: CommitResult): Promise<void>;
|
|
73
|
+
/**
|
|
74
|
+
* Roll back an in-flight turn — atomically deletes the on-disk cache
|
|
75
|
+
* file, clears the in-memory `initializedCaches` entry, and forgets
|
|
76
|
+
* the `cachedMessageCounts` entry. **All three layers, always, in
|
|
77
|
+
* one place.** Idempotent: a turn that has already been committed
|
|
78
|
+
* or rolled back is a no-op on subsequent calls. Handlers register
|
|
79
|
+
* this via `ctx.scope.defer(...)` so it runs regardless of how the
|
|
80
|
+
* handler exits (success branch removes itself via `commitTurn`).
|
|
81
|
+
*/
|
|
82
|
+
rollback(turn: TurnHandle): Promise<void>;
|
|
83
|
+
/**
|
|
84
|
+
* Forget the in-memory saved-message count for the turn's path
|
|
85
|
+
* without unlinking the file or clearing the init flag. Used when
|
|
86
|
+
* `decideCachedHistorySlice` detects a stale boundary
|
|
87
|
+
* (`clearStaleCount: true`) — the next turn re-sends the full history
|
|
88
|
+
* but the cache itself is still usable.
|
|
89
|
+
*/
|
|
90
|
+
dropStaleSavedCount(turn: TurnHandle): void;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Construct a session bound to one `(modelId, turn-owning request)`
|
|
94
|
+
* scope. `options.logger` is the per-instance logger the session emits
|
|
95
|
+
* through (typically `withRequestContext(getServerLogger(), ctx)`);
|
|
96
|
+
* falls back to the module-scoped logger when omitted.
|
|
97
|
+
*/
|
|
98
|
+
export declare function createKvCacheSession(modelId: string, options?: {
|
|
99
|
+
logger?: Logger;
|
|
100
|
+
}): KvCacheSession;
|
|
101
|
+
/**
|
|
102
|
+
* Atomically delete every layer of KV-cache state for a
|
|
103
|
+
* `(kvCacheKey, modelId)` pair, or wipe everything. Single entry point
|
|
104
|
+
* — the only mutation point for cross-model state outside of
|
|
105
|
+
* turn-scoped `commitTurn`/`rollback`.
|
|
106
|
+
*
|
|
107
|
+
* Why this isn't a method on `KvCacheSession`: deletes are
|
|
108
|
+
* cross-model (`all: true` has no model; the keyed form has
|
|
109
|
+
* `modelId` optional on the wire). A session, by contrast, is
|
|
110
|
+
* created with a *fixed* `modelId` for the duration of a turn. Making
|
|
111
|
+
* delete a method would force callers to materialise an irrelevant
|
|
112
|
+
* session for cross-model administrative cleanups.
|
|
113
|
+
*
|
|
114
|
+
* Layers cleared, in order:
|
|
115
|
+
* 1. On-disk: `deleteCache(...)` removes the matching directory
|
|
116
|
+
* tree (or wipes and recreates the root for `all: true`).
|
|
117
|
+
* 2. `cachedMessageCounts`: prefix-cleanup by the removed directory
|
|
118
|
+
* so any per-cache count under the deleted tree is forgotten.
|
|
119
|
+
* 3. `initializedCaches`: scope clear by `(kvCacheKey[, modelId])`,
|
|
120
|
+
* matching the on-disk scope.
|
|
121
|
+
*
|
|
122
|
+
* Concurrency with in-flight turns: this delete is wire-async with
|
|
123
|
+
* respect to any turn currently holding a `TurnHandle` for the same
|
|
124
|
+
* cache key. Worst case the on-disk `.bin` is removed while a turn is
|
|
125
|
+
* mid-write; the turn's eventual `commitTurn(...)` then fails the
|
|
126
|
+
* `verifySaveAndRecord` probe (file gone) and rolls back idempotently.
|
|
127
|
+
* No coordination primitive is needed because every layer's mutation
|
|
128
|
+
* is idempotent (`unlink` no-ops if missing, `Map.delete` / `Set.delete`
|
|
129
|
+
* no-op on absent keys).
|
|
130
|
+
*/
|
|
131
|
+
export declare function deleteKvCacheState(target: {
|
|
132
|
+
kvCacheKey: string;
|
|
133
|
+
modelId?: string;
|
|
134
|
+
} | {
|
|
135
|
+
all: true;
|
|
136
|
+
}): Promise<void>;
|
|
137
|
+
/**
|
|
138
|
+
* Test-only access to the module-scoped state. Production code reaches
|
|
139
|
+
* for cache state exclusively through the session API; the unit suite
|
|
140
|
+
* for `kv-cache-session.test.ts` needs to seed and inspect raw state
|
|
141
|
+
* to assert the rollback / commit invariants. Not part of the public
|
|
142
|
+
* SDK surface.
|
|
143
|
+
*
|
|
144
|
+
* @internal
|
|
145
|
+
*/
|
|
146
|
+
export declare const __kvCacheSessionTestHooks: {
|
|
147
|
+
getSavedCount(cachePath: string): number | undefined;
|
|
148
|
+
setSavedCountForTest(cachePath: string, count: number): void;
|
|
149
|
+
hasInitializedKey(modelId: string, configHash: string, cacheKey: string): boolean;
|
|
150
|
+
markInitializedForTest(modelId: string, configHash: string, cacheKey: string): void;
|
|
151
|
+
resetForTest(): void;
|
|
152
|
+
};
|
|
153
|
+
export { generateConfigHash };
|
|
154
|
+
//# sourceMappingURL=kv-cache-session.d.ts.map
|
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
import { promises as fsPromises } from "bare-fs";
|
|
2
|
+
import path from "bare-path";
|
|
3
|
+
import { findMatchingCache, generateConfigHash, getCacheFilePath, getCurrentCacheInfo, renameCacheFile, deleteCache as deleteCacheUtil, } from "../../../ops/kv-cache-utils.js";
|
|
4
|
+
import { logCacheSaveError, logCacheStatus, } from "./cache-logger.js";
|
|
5
|
+
import { getServerLogger } from "../../../../../logging/index.js";
|
|
6
|
+
// Used by cross-model paths that have no `RequestContext` (e.g.
|
|
7
|
+
// `deleteKvCacheState`). Per-session call sites receive a logger from
|
|
8
|
+
// the caller — typically `withRequestContext(...)`.
|
|
9
|
+
const moduleLogger = getServerLogger();
|
|
10
|
+
/**
|
|
11
|
+
* Single owner of the three KV-cache bookkeeping layers.
|
|
12
|
+
*
|
|
13
|
+
* The llama.cpp completion handler has three independent layers it must
|
|
14
|
+
* keep consistent across every cancel/error branch:
|
|
15
|
+
*
|
|
16
|
+
* 1. `cachedMessageCounts: Map<path, count>` — the "n messages
|
|
17
|
+
* currently on disk" tracker.
|
|
18
|
+
* 2. `initializedCaches: Set<key>` — the "addon defers disk writes;
|
|
19
|
+
* we know this cache is primed" tracker.
|
|
20
|
+
* 3. On-disk `.bin` files written by the addon.
|
|
21
|
+
*
|
|
22
|
+
* Without a single owner, every cancel / zero-token / rename-failed /
|
|
23
|
+
* tool-call exit would need to touch all three; any branch that forgets
|
|
24
|
+
* a layer produces three-layer drift bugs.
|
|
25
|
+
*
|
|
26
|
+
* `KvCacheSession` collapses the three layers behind one object with
|
|
27
|
+
* three operations:
|
|
28
|
+
*
|
|
29
|
+
* - `beginTurn` — resolves the cache file path, primes the system
|
|
30
|
+
* prompt cache if missing (delegated to a caller-supplied closure
|
|
31
|
+
* so the session doesn't depend on the model addon), marks the
|
|
32
|
+
* cache initialized, and returns a `TurnHandle` carrying the
|
|
33
|
+
* resolved path + the snapshot of the on-disk saved count.
|
|
34
|
+
* - `commitTurn` — records the new saved count (for custom-key
|
|
35
|
+
* turns) or renames the addon's pre-response file to the
|
|
36
|
+
* post-response path and records the count there (for auto-cache
|
|
37
|
+
* turns). Flips the turn's internal `committed` flag so the
|
|
38
|
+
* deferred `rollback` becomes a no-op on the happy path.
|
|
39
|
+
* - `rollback` — atomically deletes the on-disk file, clears the
|
|
40
|
+
* in-memory init entry, and forgets the saved count. **All three
|
|
41
|
+
* layers, always, in one place.** Handlers call it once via
|
|
42
|
+
* `ctx.scope.defer(() => session.rollback(turn))`; `commitTurn`
|
|
43
|
+
* short-circuits it on success.
|
|
44
|
+
*
|
|
45
|
+
* The module-level `deleteKvCacheState(...)` function (below) provides
|
|
46
|
+
* an administrative cross-model delete API for the
|
|
47
|
+
* `handleDeleteCache` RPC handler.
|
|
48
|
+
*
|
|
49
|
+
* The module-scoped `cachedMessageCounts` and `initializedCaches` maps
|
|
50
|
+
* are *private* to this file — no other module reaches into them.
|
|
51
|
+
* Callers that need cache-status info do so through the session API.
|
|
52
|
+
*/
|
|
53
|
+
// ----- module-scoped state. The session is the single mutation point
|
|
54
|
+
// for the in-memory KV-cache bookkeeping. -----
|
|
55
|
+
/**
|
|
56
|
+
* Number of chat messages the kv-cache file on disk is known to cover,
|
|
57
|
+
* keyed by cache path. Written by `commitTurn`, read by `getSavedCount`,
|
|
58
|
+
* deleted by `rollback` / `delete` / `dropStaleSavedCount`. The same
|
|
59
|
+
* INVARIANT that existed in `kv-cache-state.ts` still holds: an entry is
|
|
60
|
+
* present only when the corresponding `.bin` file is considered
|
|
61
|
+
* trustworthy. Cancelled or zero-token turns must remove the entry so
|
|
62
|
+
* the next-turn slice doesn't read a stale boundary.
|
|
63
|
+
*/
|
|
64
|
+
const cachedMessageCounts = new Map();
|
|
65
|
+
/**
|
|
66
|
+
* In-memory registry of caches initialized this session. The addon
|
|
67
|
+
* defers disk writes, so the absence of a `.bin` file on disk isn't
|
|
68
|
+
* proof that the cache hasn't been primed in this worker process. Keyed
|
|
69
|
+
* by `${modelId}:${configHash}:${cacheKey}`, so on-disk caches from
|
|
70
|
+
* older worker runs still hit the lazy-load path in `beginTurn`.
|
|
71
|
+
*/
|
|
72
|
+
const initializedCaches = new Set();
|
|
73
|
+
function initRegistryKey(modelId, configHash, cacheKey) {
|
|
74
|
+
return `${modelId}:${configHash}:${cacheKey}`;
|
|
75
|
+
}
|
|
76
|
+
// ----- factory -----
|
|
77
|
+
/**
|
|
78
|
+
* Construct a session bound to one `(modelId, turn-owning request)`
|
|
79
|
+
* scope. `options.logger` is the per-instance logger the session emits
|
|
80
|
+
* through (typically `withRequestContext(getServerLogger(), ctx)`);
|
|
81
|
+
* falls back to the module-scoped logger when omitted.
|
|
82
|
+
*/
|
|
83
|
+
export function createKvCacheSession(modelId, options) {
|
|
84
|
+
const logger = options?.logger ?? moduleLogger;
|
|
85
|
+
// Per-session map: each `TurnHandle` carries an opaque entry here. A
|
|
86
|
+
// WeakMap so handles drop their state once the handler scope releases
|
|
87
|
+
// the reference; the module-scoped maps above survive.
|
|
88
|
+
const turnState = new WeakMap();
|
|
89
|
+
function makeHandle(cachePath, registryKey) {
|
|
90
|
+
const handle = {
|
|
91
|
+
cachePath,
|
|
92
|
+
savedCount: cachedMessageCounts.get(cachePath) ?? 0,
|
|
93
|
+
};
|
|
94
|
+
turnState.set(handle, {
|
|
95
|
+
cachePath,
|
|
96
|
+
registryKey,
|
|
97
|
+
committed: false,
|
|
98
|
+
rolledBack: false,
|
|
99
|
+
});
|
|
100
|
+
return handle;
|
|
101
|
+
}
|
|
102
|
+
async function beginCustom(input) {
|
|
103
|
+
const cachePath = await getCacheFilePath(modelId, input.configHash, input.customKey);
|
|
104
|
+
const registryKey = initRegistryKey(modelId, input.configHash, input.customKey);
|
|
105
|
+
// In-memory registry check first — the addon defers disk writes, so
|
|
106
|
+
// a freshly-primed cache may not yet exist on disk. If the
|
|
107
|
+
// in-memory flag isn't set, fall back to a filesystem probe so
|
|
108
|
+
// caches surviving across worker restarts still hit the reuse path.
|
|
109
|
+
let exists = initializedCaches.has(registryKey);
|
|
110
|
+
if (!exists) {
|
|
111
|
+
try {
|
|
112
|
+
await fsPromises.access(cachePath);
|
|
113
|
+
exists = true;
|
|
114
|
+
initializedCaches.add(registryKey);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
exists = false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
logCacheStatus(input.customKey, exists);
|
|
121
|
+
if (!exists) {
|
|
122
|
+
await input.primeIfMissing(cachePath);
|
|
123
|
+
await verifyPrimedFile(cachePath, logger);
|
|
124
|
+
initializedCaches.add(registryKey);
|
|
125
|
+
}
|
|
126
|
+
return makeHandle(cachePath, registryKey);
|
|
127
|
+
}
|
|
128
|
+
async function beginAuto(input) {
|
|
129
|
+
// The pre-response cache key is derived from
|
|
130
|
+
// `history.slice(0, -1)` — `findMatchingCache` does that
|
|
131
|
+
// internally. The post-response key (used after a successful turn)
|
|
132
|
+
// is computed by the caller and passed to `commitTurn` as
|
|
133
|
+
// `targetCachePath`.
|
|
134
|
+
const existingCache = await findMatchingCache(modelId, input.configHash, input.history);
|
|
135
|
+
const preResponseCacheInfo = await getCurrentCacheInfo(modelId, input.configHash, input.history);
|
|
136
|
+
const cachePath = existingCache !== null
|
|
137
|
+
? existingCache.cachePath
|
|
138
|
+
: preResponseCacheInfo.cachePath;
|
|
139
|
+
const cacheKeyForRegistry = existingCache !== null
|
|
140
|
+
? existingCache.cacheKey
|
|
141
|
+
: preResponseCacheInfo.cacheKey;
|
|
142
|
+
const registryKey = initRegistryKey(modelId, input.configHash, cacheKeyForRegistry);
|
|
143
|
+
const cacheExists = existingCache !== null;
|
|
144
|
+
logCacheStatus("auto", cacheExists);
|
|
145
|
+
if (!cacheExists) {
|
|
146
|
+
await input.primeIfMissing(cachePath);
|
|
147
|
+
await verifyPrimedFile(cachePath, logger);
|
|
148
|
+
initializedCaches.add(registryKey);
|
|
149
|
+
}
|
|
150
|
+
return makeHandle(cachePath, registryKey);
|
|
151
|
+
}
|
|
152
|
+
async function beginTurn(input) {
|
|
153
|
+
if (input.kind === "custom")
|
|
154
|
+
return beginCustom(input);
|
|
155
|
+
return beginAuto(input);
|
|
156
|
+
}
|
|
157
|
+
async function commitTurn(turn, result) {
|
|
158
|
+
const state = turnState.get(turn);
|
|
159
|
+
if (!state) {
|
|
160
|
+
// Handle from a different session or already torn down. Treat as
|
|
161
|
+
// no-op — caller shouldn't be reaching into another session's
|
|
162
|
+
// state, but failing loudly here punishes the rollback-after-end
|
|
163
|
+
// path more than it helps.
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (state.committed || state.rolledBack)
|
|
167
|
+
return;
|
|
168
|
+
if (result.kind === "static") {
|
|
169
|
+
// Custom-key path: the addon wrote the new cache state inline
|
|
170
|
+
// at the same path. Verify the file persisted (the addon
|
|
171
|
+
// currently swallows save errors — see TODO in
|
|
172
|
+
// `verifySaveAndRecord`) and record the new boundary.
|
|
173
|
+
const ok = await verifySaveAndRecord(state.cachePath, result.messageCount);
|
|
174
|
+
if (!ok) {
|
|
175
|
+
// The expected save didn't land — treat the turn as a rollback
|
|
176
|
+
// so the next turn re-primes cleanly.
|
|
177
|
+
await runRollback(state);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
state.committed = true;
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
// Auto-rename path: the pre-response file is now stale (its key
|
|
184
|
+
// refers to history minus the last user turn). Rename it to the
|
|
185
|
+
// post-response key and record the new count there.
|
|
186
|
+
if (!(await renameCacheFile(state.cachePath, result.targetCachePath))) {
|
|
187
|
+
logger.warn(`[kv-cache] Auto cache rename failed; rolling back. from=${state.cachePath} to=${result.targetCachePath}`);
|
|
188
|
+
await runRollback(state);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
// The source path's entry is gone (the file moved). Drop it and
|
|
192
|
+
// record the new count at the rename target.
|
|
193
|
+
cachedMessageCounts.delete(state.cachePath);
|
|
194
|
+
const ok = await verifySaveAndRecord(result.targetCachePath, result.messageCount);
|
|
195
|
+
if (!ok) {
|
|
196
|
+
// Rename succeeded but the file isn't where we expected. Roll
|
|
197
|
+
// back via the target path instead of the (now-empty) source.
|
|
198
|
+
state.cachePath = result.targetCachePath;
|
|
199
|
+
await runRollback(state);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
// Successful auto-rename. The handle's `cachePath` field still
|
|
203
|
+
// points at the (now-gone) source path — that's fine, the handle
|
|
204
|
+
// is committed and won't roll back. Future turns compute fresh
|
|
205
|
+
// paths.
|
|
206
|
+
state.committed = true;
|
|
207
|
+
}
|
|
208
|
+
async function rollback(turn) {
|
|
209
|
+
const state = turnState.get(turn);
|
|
210
|
+
if (!state)
|
|
211
|
+
return;
|
|
212
|
+
if (state.committed || state.rolledBack)
|
|
213
|
+
return;
|
|
214
|
+
await runRollback(state);
|
|
215
|
+
}
|
|
216
|
+
async function runRollback(state) {
|
|
217
|
+
// Order matters only weakly: unlink first so a partial disk-state
|
|
218
|
+
// can't be re-loaded by a sibling turn between the file delete and
|
|
219
|
+
// the in-memory clear. In practice handlers serialise per model;
|
|
220
|
+
// the order is belt-and-suspenders.
|
|
221
|
+
try {
|
|
222
|
+
await fsPromises.unlink(state.cachePath);
|
|
223
|
+
}
|
|
224
|
+
catch (unlinkError) {
|
|
225
|
+
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)}`);
|
|
226
|
+
}
|
|
227
|
+
initializedCaches.delete(state.registryKey);
|
|
228
|
+
cachedMessageCounts.delete(state.cachePath);
|
|
229
|
+
state.rolledBack = true;
|
|
230
|
+
}
|
|
231
|
+
function dropStaleSavedCount(turn) {
|
|
232
|
+
const state = turnState.get(turn);
|
|
233
|
+
if (!state)
|
|
234
|
+
return;
|
|
235
|
+
cachedMessageCounts.delete(state.cachePath);
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
beginTurn,
|
|
239
|
+
commitTurn,
|
|
240
|
+
rollback,
|
|
241
|
+
dropStaleSavedCount,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
// ----- module-level administrative API -----
|
|
245
|
+
/**
|
|
246
|
+
* Atomically delete every layer of KV-cache state for a
|
|
247
|
+
* `(kvCacheKey, modelId)` pair, or wipe everything. Single entry point
|
|
248
|
+
* — the only mutation point for cross-model state outside of
|
|
249
|
+
* turn-scoped `commitTurn`/`rollback`.
|
|
250
|
+
*
|
|
251
|
+
* Why this isn't a method on `KvCacheSession`: deletes are
|
|
252
|
+
* cross-model (`all: true` has no model; the keyed form has
|
|
253
|
+
* `modelId` optional on the wire). A session, by contrast, is
|
|
254
|
+
* created with a *fixed* `modelId` for the duration of a turn. Making
|
|
255
|
+
* delete a method would force callers to materialise an irrelevant
|
|
256
|
+
* session for cross-model administrative cleanups.
|
|
257
|
+
*
|
|
258
|
+
* Layers cleared, in order:
|
|
259
|
+
* 1. On-disk: `deleteCache(...)` removes the matching directory
|
|
260
|
+
* tree (or wipes and recreates the root for `all: true`).
|
|
261
|
+
* 2. `cachedMessageCounts`: prefix-cleanup by the removed directory
|
|
262
|
+
* so any per-cache count under the deleted tree is forgotten.
|
|
263
|
+
* 3. `initializedCaches`: scope clear by `(kvCacheKey[, modelId])`,
|
|
264
|
+
* matching the on-disk scope.
|
|
265
|
+
*
|
|
266
|
+
* Concurrency with in-flight turns: this delete is wire-async with
|
|
267
|
+
* respect to any turn currently holding a `TurnHandle` for the same
|
|
268
|
+
* cache key. Worst case the on-disk `.bin` is removed while a turn is
|
|
269
|
+
* mid-write; the turn's eventual `commitTurn(...)` then fails the
|
|
270
|
+
* `verifySaveAndRecord` probe (file gone) and rolls back idempotently.
|
|
271
|
+
* No coordination primitive is needed because every layer's mutation
|
|
272
|
+
* is idempotent (`unlink` no-ops if missing, `Map.delete` / `Set.delete`
|
|
273
|
+
* no-op on absent keys).
|
|
274
|
+
*/
|
|
275
|
+
export async function deleteKvCacheState(target) {
|
|
276
|
+
if ("all" in target) {
|
|
277
|
+
const removed = await deleteCacheUtil({ all: true });
|
|
278
|
+
cachedMessageCounts.clear();
|
|
279
|
+
initializedCaches.clear();
|
|
280
|
+
// `removed` is the kv-cache root dir; surfaces it for ops
|
|
281
|
+
// visibility but isn't part of the contract.
|
|
282
|
+
moduleLogger.debug(`[kv-cache] Cleared all caches under ${removed}`);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const removedPath = await deleteCacheUtil({
|
|
286
|
+
kvCacheKey: target.kvCacheKey,
|
|
287
|
+
...(target.modelId !== undefined && { modelId: target.modelId }),
|
|
288
|
+
});
|
|
289
|
+
// Prefix-cleanup the in-memory counts. The on-disk directory tree
|
|
290
|
+
// is `{kvCacheRoot}/{kvCacheKey}[/{modelId}]/`, so every entry in
|
|
291
|
+
// `cachedMessageCounts` whose key is the removed directory itself
|
|
292
|
+
// or sits beneath it must go.
|
|
293
|
+
clearCachedMessageCountsByPrefix(removedPath, path.sep);
|
|
294
|
+
// The in-memory init-set keys are
|
|
295
|
+
// `${modelId}:${configHash}:${kvCacheKey}` — clear by the user-
|
|
296
|
+
// facing kvCacheKey (and optionally narrow by modelId).
|
|
297
|
+
clearInitializedCachesByScope({
|
|
298
|
+
cacheKey: target.kvCacheKey,
|
|
299
|
+
...(target.modelId !== undefined && { modelId: target.modelId }),
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
// ----- private helpers -----
|
|
303
|
+
/**
|
|
304
|
+
* Verify that the addon actually persisted a usable cache file after a
|
|
305
|
+
* prime. Mirrors the `verifySaveAndRecord` access-probe used at commit
|
|
306
|
+
* time, applied at prime time so the session doesn't mark a cache
|
|
307
|
+
* `initializedCaches.add(...)` against a path that's missing or empty
|
|
308
|
+
* on disk.
|
|
309
|
+
*
|
|
310
|
+
* Failure modes this catches:
|
|
311
|
+
*
|
|
312
|
+
* - The addon's `model.run({ saveSessionPath })` was interrupted
|
|
313
|
+
* before the save call ran (e.g. signal abort during prefill); the
|
|
314
|
+
* prime closure resolves cleanly because addon save errors are not
|
|
315
|
+
* propagated, but no file is on disk.
|
|
316
|
+
* - The addon's `llama_state_save_file` was called but produced an
|
|
317
|
+
* empty file (out-of-space / fs error swallowed by the addon).
|
|
318
|
+
*
|
|
319
|
+
* Failure modes this does **NOT** catch:
|
|
320
|
+
*
|
|
321
|
+
* - A partial-but-nonzero file written by the addon (e.g. header +
|
|
322
|
+
* truncated KV state). Catching this requires either an
|
|
323
|
+
* addon-side change (have `CacheManager::writeCacheFile` check the
|
|
324
|
+
* return value of `llama_state_save_file` and throw on failure) or
|
|
325
|
+
* a structural hash check we can't currently compute from the
|
|
326
|
+
* SDK. Filed as a follow-up — see `cache-api.md` in the addon
|
|
327
|
+
* repo / tracking ticket.
|
|
328
|
+
*
|
|
329
|
+
* On failure we best-effort `unlink` an empty leftover file (so the
|
|
330
|
+
* next existence probe doesn't trust it) and throw — the handler in
|
|
331
|
+
* `completion-stream.ts` lets the error propagate up and no
|
|
332
|
+
* `initializedCaches` entry is recorded.
|
|
333
|
+
*/
|
|
334
|
+
async function verifyPrimedFile(cachePath, logger) {
|
|
335
|
+
let stats;
|
|
336
|
+
try {
|
|
337
|
+
stats = await fsPromises.stat(cachePath);
|
|
338
|
+
}
|
|
339
|
+
catch (statError) {
|
|
340
|
+
// ENOENT is the common case here — addon prime returned without
|
|
341
|
+
// calling save (most often: signal abort during prefill).
|
|
342
|
+
throw new Error(`[kv-cache] prime closure resolved but no cache file was written. path=${cachePath} cause=${statError instanceof Error ? statError.message : String(statError)}`);
|
|
343
|
+
}
|
|
344
|
+
if (stats.size === 0) {
|
|
345
|
+
// Best-effort cleanup so a future probe doesn't trust the empty
|
|
346
|
+
// file. Unlink failure is non-fatal — we still throw on the
|
|
347
|
+
// primary "prime didn't persist" condition.
|
|
348
|
+
try {
|
|
349
|
+
await fsPromises.unlink(cachePath);
|
|
350
|
+
}
|
|
351
|
+
catch (unlinkError) {
|
|
352
|
+
logger.warn(`[kv-cache] Failed to remove empty primed cache file. path=${cachePath} error=${unlinkError instanceof Error ? unlinkError.message : String(unlinkError)}`);
|
|
353
|
+
}
|
|
354
|
+
throw new Error(`[kv-cache] prime closure resolved but cache file is empty. path=${cachePath}`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Verify the addon actually persisted the cache file before recording
|
|
359
|
+
* its message count. The addon currently swallows write errors
|
|
360
|
+
* silently, so a missing file means the next turn must resend the full
|
|
361
|
+
* history rather than slicing against a stale `savedCount`.
|
|
362
|
+
*
|
|
363
|
+
* TODO: once the addon surfaces save failures (e.g. throws
|
|
364
|
+
* `UnableToSaveSessionFile` when `llama_state_save_file` returns
|
|
365
|
+
* false), drop the `access()` probe and wrap the `model.run()` call in
|
|
366
|
+
* a real try/catch that forwards the error.
|
|
367
|
+
*/
|
|
368
|
+
async function verifySaveAndRecord(cachePath, messageCount) {
|
|
369
|
+
try {
|
|
370
|
+
await fsPromises.access(cachePath);
|
|
371
|
+
cachedMessageCounts.set(cachePath, messageCount);
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
374
|
+
catch (err) {
|
|
375
|
+
cachedMessageCounts.delete(cachePath);
|
|
376
|
+
logCacheSaveError(cachePath, err);
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
function clearCachedMessageCountsByPrefix(prefix, sep) {
|
|
381
|
+
if (!prefix) {
|
|
382
|
+
cachedMessageCounts.clear();
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
for (const key of cachedMessageCounts.keys()) {
|
|
386
|
+
if (key === prefix) {
|
|
387
|
+
cachedMessageCounts.delete(key);
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
if (!key.startsWith(prefix + sep))
|
|
391
|
+
continue;
|
|
392
|
+
cachedMessageCounts.delete(key);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
function clearInitializedCachesByScope(scope) {
|
|
396
|
+
if (scope.cacheKey === undefined && scope.modelId === undefined) {
|
|
397
|
+
initializedCaches.clear();
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
for (const key of initializedCaches) {
|
|
401
|
+
const firstSep = key.indexOf(":");
|
|
402
|
+
const secondSep = key.indexOf(":", firstSep + 1);
|
|
403
|
+
if (firstSep === -1 || secondSep === -1)
|
|
404
|
+
continue;
|
|
405
|
+
const entryModelId = key.slice(0, firstSep);
|
|
406
|
+
const entryCacheKey = key.slice(secondSep + 1);
|
|
407
|
+
if (scope.cacheKey !== undefined && entryCacheKey !== scope.cacheKey) {
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
if (scope.modelId !== undefined && entryModelId !== scope.modelId) {
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
initializedCaches.delete(key);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
// ----- test-only escape hatches -----
|
|
417
|
+
/**
|
|
418
|
+
* Test-only access to the module-scoped state. Production code reaches
|
|
419
|
+
* for cache state exclusively through the session API; the unit suite
|
|
420
|
+
* for `kv-cache-session.test.ts` needs to seed and inspect raw state
|
|
421
|
+
* to assert the rollback / commit invariants. Not part of the public
|
|
422
|
+
* SDK surface.
|
|
423
|
+
*
|
|
424
|
+
* @internal
|
|
425
|
+
*/
|
|
426
|
+
export const __kvCacheSessionTestHooks = {
|
|
427
|
+
getSavedCount(cachePath) {
|
|
428
|
+
return cachedMessageCounts.get(cachePath);
|
|
429
|
+
},
|
|
430
|
+
setSavedCountForTest(cachePath, count) {
|
|
431
|
+
cachedMessageCounts.set(cachePath, count);
|
|
432
|
+
},
|
|
433
|
+
hasInitializedKey(modelId, configHash, cacheKey) {
|
|
434
|
+
return initializedCaches.has(initRegistryKey(modelId, configHash, cacheKey));
|
|
435
|
+
},
|
|
436
|
+
markInitializedForTest(modelId, configHash, cacheKey) {
|
|
437
|
+
initializedCaches.add(initRegistryKey(modelId, configHash, cacheKey));
|
|
438
|
+
},
|
|
439
|
+
resetForTest() {
|
|
440
|
+
cachedMessageCounts.clear();
|
|
441
|
+
initializedCaches.clear();
|
|
442
|
+
},
|
|
443
|
+
};
|
|
444
|
+
// Re-export `generateConfigHash` from the path utilities so callers of
|
|
445
|
+
// the session can compute the hash without separately importing
|
|
446
|
+
// `kv-cache-utils`. The function itself stays in `kv-cache-utils.ts`
|
|
447
|
+
// (pure, no state) — only the re-export lives here.
|
|
448
|
+
export { generateConfigHash };
|