@oh-my-pi/pi-mnemopi 18.5.0 → 18.5.1
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/CHANGELOG.md +11 -0
- package/dist/types/core/embeddings.d.ts +5 -24
- package/dist/types/core/fastembed-model-cache.d.ts +8 -2
- package/dist/types/core/query-cache.d.ts +3 -1
- package/package.json +7 -7
- package/src/core/embeddings.ts +7 -60
- package/src/core/fastembed-model-cache.ts +41 -31
- package/src/core/fastembed-runtime.ts +12 -1
- package/src/core/query-cache.ts +23 -7
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.5.1] - 2026-10-03
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Upgraded local embedding support to fastembed 3.0.0. Models now download from Hugging Face into the updated cache layout, with interrupted downloads resuming automatically; existing models are migrated on first use while producing the same vectors.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Fixed enhanced recall returning cached results from an unrelated longer query when answering a shorter query.
|
|
14
|
+
- Fixed local embedding setup on fresh caches and Linux ARM64, including compatibility with current model downloads and ARM64 tokenization support.
|
|
15
|
+
|
|
5
16
|
## [18.4.4] - 2026-09-29
|
|
6
17
|
|
|
7
18
|
### Added
|
|
@@ -32,30 +32,11 @@ export type LocalModelInitializer = (options: LocalModelInitOptions) => Promise<
|
|
|
32
32
|
*/
|
|
33
33
|
export declare function quarantineCorruptModelFile(message: string, cacheDir?: string): Promise<boolean>;
|
|
34
34
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* short-circuits on the existing model dir and never re-downloads, and
|
|
41
|
-
* `downloadFileFromGCS` reuses a leftover partial `<model>.tar.gz` as-is, so
|
|
42
|
-
* the cache is stuck broken forever: recall silently dies and the rebuild
|
|
43
|
-
* queue grows every session. Delete the incomplete model dir AND the leftover
|
|
44
|
-
* partial archive so the retry re-downloads from scratch. The model path is
|
|
45
|
-
* error-message CONTENT, so the dir is only removed when it is a direct child
|
|
46
|
-
* of the fastembed cache root — never touch a directory a dependency merely
|
|
47
|
-
* names. Returns true when a retry is worth attempting (the incomplete cache
|
|
48
|
-
* was cleared, or already gone from a concurrent heal).
|
|
49
|
-
* @internal exported for tests
|
|
50
|
-
*/
|
|
51
|
-
export declare function clearIncompleteModelCache(message: string, cacheDir?: string): Promise<boolean>;
|
|
52
|
-
/**
|
|
53
|
-
* Shared local-model initializer: FlagEmbedding.init with BOTH cache heals.
|
|
54
|
-
* Missing sidecars (config/tokenizer/tokens map) re-fetch and retry; a
|
|
55
|
-
* corrupt model blob (Protobuf parse failure) quarantines the file and
|
|
56
|
-
* retries THROUGH the sidecar heal, so a cache that is broken in both ways
|
|
57
|
-
* still recovers in one pass. Also the initializer the embed worker uses in
|
|
58
|
-
* its subprocess; the in-process seam stays {@link setLocalModelInitializer}.
|
|
35
|
+
* Shared local-model initializer: FlagEmbedding.init with a corrupt-cache heal.
|
|
36
|
+
* A corrupt model blob (Protobuf parse failure) quarantines the file and
|
|
37
|
+
* retries once; fastembed re-downloads any file missing from the model's
|
|
38
|
+
* cache directory. Also the initializer the embed worker uses in its
|
|
39
|
+
* subprocess; the in-process seam stays {@link setLocalModelInitializer}.
|
|
59
40
|
*/
|
|
60
41
|
export declare function defaultLocalModelInitializer(options: LocalModelInitOptions): Promise<LocalEmbeddingModel>;
|
|
61
42
|
export declare function embeddingsDisabled(): boolean;
|
|
@@ -1,2 +1,8 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Whether `FlagEmbedding.init` can load `model` from `cacheDir` without a
|
|
3
|
+
* download. fastembed caches each file under `<cacheDir>/<repo with / → _>/`
|
|
4
|
+
* and fetches only the files that are missing, so a model is cached exactly
|
|
5
|
+
* when every file is present. Unknown models report false. The agent's embed
|
|
6
|
+
* client uses this to decide whether an init shows download activity.
|
|
7
|
+
*/
|
|
8
|
+
export declare function isFastembedModelCached(model: string, cacheDir: string): Promise<boolean>;
|
|
@@ -31,7 +31,9 @@ export declare function isQueryCacheEnabled(useCache?: boolean, env?: Env, confi
|
|
|
31
31
|
*
|
|
32
32
|
* - Tier 1: exact normalized query match.
|
|
33
33
|
* - Tiers 2/3: query embedding cosine >= 0.88, or >= 0.78 plus word overlap.
|
|
34
|
-
* - Tier 4:
|
|
34
|
+
* - Tier 4: shared distinct words cover at least 70% of the new query and half of the
|
|
35
|
+
* cached one, so a reworded or trimmed query hits but a short query never lands
|
|
36
|
+
* inside a long cached prompt that merely contains its words.
|
|
35
37
|
*/
|
|
36
38
|
export declare class QueryCache<T = QueryCacheResult> {
|
|
37
39
|
#private;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-mnemopi",
|
|
4
|
-
"version": "18.5.
|
|
4
|
+
"version": "18.5.1",
|
|
5
5
|
"description": "Local SQLite memory engine for omp agents",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": {
|
|
@@ -42,13 +42,13 @@
|
|
|
42
42
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@oh-my-pi/pi-ai": "18.5.
|
|
46
|
-
"@oh-my-pi/pi-catalog": "18.5.
|
|
47
|
-
"@oh-my-pi/pi-natives": "18.5.
|
|
48
|
-
"@oh-my-pi/pi-utils": "18.5.
|
|
45
|
+
"@oh-my-pi/pi-ai": "18.5.1",
|
|
46
|
+
"@oh-my-pi/pi-catalog": "18.5.1",
|
|
47
|
+
"@oh-my-pi/pi-natives": "18.5.1",
|
|
48
|
+
"@oh-my-pi/pi-utils": "18.5.1"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
|
-
"fastembed": "
|
|
51
|
+
"fastembed": "3.0.0",
|
|
52
52
|
"onnxruntime-node": "1.21.0"
|
|
53
53
|
},
|
|
54
54
|
"peerDependenciesMeta": {
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@types/bun": "^1.3.14",
|
|
64
|
-
"fastembed": "
|
|
64
|
+
"fastembed": "3.0.0",
|
|
65
65
|
"onnxruntime-node": "1.26.0"
|
|
66
66
|
},
|
|
67
67
|
"engines": {
|
package/src/core/embeddings.ts
CHANGED
|
@@ -14,7 +14,6 @@ import {
|
|
|
14
14
|
} from "@oh-my-pi/pi-utils";
|
|
15
15
|
import { LRUCache } from "@oh-my-pi/pi-utils/lru";
|
|
16
16
|
import type { EmbeddingModel } from "fastembed";
|
|
17
|
-
import { ensureFastembedModelSidecars } from "./fastembed-model-cache";
|
|
18
17
|
import { loadFastembed } from "./fastembed-runtime";
|
|
19
18
|
import {
|
|
20
19
|
type EmbeddingOutput,
|
|
@@ -93,74 +92,22 @@ export async function quarantineCorruptModelFile(message: string, cacheDir?: str
|
|
|
93
92
|
}
|
|
94
93
|
|
|
95
94
|
/**
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* short-circuits on the existing model dir and never re-downloads, and
|
|
102
|
-
* `downloadFileFromGCS` reuses a leftover partial `<model>.tar.gz` as-is, so
|
|
103
|
-
* the cache is stuck broken forever: recall silently dies and the rebuild
|
|
104
|
-
* queue grows every session. Delete the incomplete model dir AND the leftover
|
|
105
|
-
* partial archive so the retry re-downloads from scratch. The model path is
|
|
106
|
-
* error-message CONTENT, so the dir is only removed when it is a direct child
|
|
107
|
-
* of the fastembed cache root — never touch a directory a dependency merely
|
|
108
|
-
* names. Returns true when a retry is worth attempting (the incomplete cache
|
|
109
|
-
* was cleared, or already gone from a concurrent heal).
|
|
110
|
-
* @internal exported for tests
|
|
111
|
-
*/
|
|
112
|
-
export async function clearIncompleteModelCache(message: string, cacheDir?: string): Promise<boolean> {
|
|
113
|
-
const match = /Model file not found at (.+?\.onnx)\b/i.exec(message);
|
|
114
|
-
if (!match) return false;
|
|
115
|
-
const modelFile = nodePath.resolve(match[1]);
|
|
116
|
-
const modelDir = nodePath.dirname(modelFile);
|
|
117
|
-
const cacheRoot = nodePath.resolve(cacheDir ?? getFastembedCacheDir());
|
|
118
|
-
// Only a direct child of the cache root: `<cacheRoot>/<model>`.
|
|
119
|
-
if (nodePath.dirname(modelDir) !== cacheRoot) return false;
|
|
120
|
-
try {
|
|
121
|
-
await fsp.rm(modelDir, { recursive: true, force: true });
|
|
122
|
-
await fsp.rm(`${modelDir}.tar.gz`, { force: true });
|
|
123
|
-
logger.warn("mnemopi: cleared incomplete local embedding model cache; re-downloading", { modelDir });
|
|
124
|
-
} catch {
|
|
125
|
-
// Concurrent heal or vanished dir: the single retry stays safe.
|
|
126
|
-
}
|
|
127
|
-
return true;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const SIDECAR_ERROR_RE =
|
|
131
|
-
/(?:Config file not found at .*config|Tokenizer file not found at .*tokenizer|Tokens map file not found at .*special_tokens_map)/u;
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Shared local-model initializer: FlagEmbedding.init with BOTH cache heals.
|
|
135
|
-
* Missing sidecars (config/tokenizer/tokens map) re-fetch and retry; a
|
|
136
|
-
* corrupt model blob (Protobuf parse failure) quarantines the file and
|
|
137
|
-
* retries THROUGH the sidecar heal, so a cache that is broken in both ways
|
|
138
|
-
* still recovers in one pass. Also the initializer the embed worker uses in
|
|
139
|
-
* its subprocess; the in-process seam stays {@link setLocalModelInitializer}.
|
|
95
|
+
* Shared local-model initializer: FlagEmbedding.init with a corrupt-cache heal.
|
|
96
|
+
* A corrupt model blob (Protobuf parse failure) quarantines the file and
|
|
97
|
+
* retries once; fastembed re-downloads any file missing from the model's
|
|
98
|
+
* cache directory. Also the initializer the embed worker uses in its
|
|
99
|
+
* subprocess; the in-process seam stays {@link setLocalModelInitializer}.
|
|
140
100
|
*/
|
|
141
101
|
export async function defaultLocalModelInitializer(options: LocalModelInitOptions): Promise<LocalEmbeddingModel> {
|
|
142
102
|
const cacheDir = options.cacheDir ?? getFastembedCacheDir();
|
|
143
103
|
const initOptions = options.cacheDir === undefined ? { ...options, cacheDir } : options;
|
|
144
104
|
const { FlagEmbedding } = await loadFastembed();
|
|
145
|
-
const initWithSidecarHeal = async (): Promise<LocalEmbeddingModel> => {
|
|
146
|
-
try {
|
|
147
|
-
return await FlagEmbedding.init(initOptions);
|
|
148
|
-
} catch (error) {
|
|
149
|
-
const message = error instanceof Error ? error.message : "";
|
|
150
|
-
if (!SIDECAR_ERROR_RE.test(message)) throw error;
|
|
151
|
-
if (!(await ensureFastembedModelSidecars(options.model, cacheDir))) throw error;
|
|
152
|
-
return FlagEmbedding.init(initOptions);
|
|
153
|
-
}
|
|
154
|
-
};
|
|
155
105
|
try {
|
|
156
|
-
return await
|
|
106
|
+
return await FlagEmbedding.init(initOptions);
|
|
157
107
|
} catch (error) {
|
|
158
108
|
const message = error instanceof Error ? error.message : "";
|
|
159
109
|
if (/Protobuf parsing failed/i.test(message) && (await quarantineCorruptModelFile(message, cacheDir))) {
|
|
160
|
-
return
|
|
161
|
-
}
|
|
162
|
-
if (await clearIncompleteModelCache(message, cacheDir)) {
|
|
163
|
-
return initWithSidecarHeal();
|
|
110
|
+
return FlagEmbedding.init(initOptions);
|
|
164
111
|
}
|
|
165
112
|
throw error;
|
|
166
113
|
}
|
|
@@ -1,39 +1,49 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
1
2
|
import * as path from "node:path";
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Hugging Face repo and weight files fastembed downloads for each built-in
|
|
6
|
+
* model. Mirrors fastembed's private `MODEL_SOURCES` table; fastembed
|
|
7
|
+
* exposes no API for its cache layout.
|
|
8
|
+
*/
|
|
9
|
+
const FASTEMBED_MODEL_SOURCES: Record<string, { repo: string; files: readonly string[] }> = {
|
|
10
|
+
"fast-all-MiniLM-L6-v2": { repo: "Qdrant/all-MiniLM-L6-v2-onnx", files: ["model.onnx"] },
|
|
11
|
+
"fast-bge-base-en": { repo: "Qdrant/fast-bge-base-en", files: ["model_optimized.onnx"] },
|
|
12
|
+
"fast-bge-base-en-v1.5": { repo: "Qdrant/bge-base-en-v1.5-onnx-Q", files: ["model_optimized.onnx"] },
|
|
13
|
+
"fast-bge-small-en": { repo: "Qdrant/bge-small-en", files: ["model_optimized.onnx"] },
|
|
14
|
+
"fast-bge-small-en-v1.5": { repo: "Qdrant/bge-small-en-v1.5-onnx-Q", files: ["model_optimized.onnx"] },
|
|
15
|
+
"fast-bge-small-zh-v1.5": { repo: "Qdrant/bge-small-zh-v1.5", files: ["model_optimized.onnx"] },
|
|
16
|
+
"fast-multilingual-e5-large": {
|
|
17
|
+
repo: "Qdrant/multilingual-e5-large-onnx",
|
|
18
|
+
files: ["model.onnx", "model.onnx_data"],
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const FASTEMBED_TOKENIZER_FILES = [
|
|
5
23
|
"tokenizer.json",
|
|
6
24
|
"tokenizer_config.json",
|
|
25
|
+
"config.json",
|
|
7
26
|
"special_tokens_map.json",
|
|
8
27
|
] as const;
|
|
9
28
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const response = await fetch(`https://huggingface.co/${repo}/resolve/main/${fileName}`);
|
|
31
|
-
if (!response.ok) {
|
|
32
|
-
throw new Error(
|
|
33
|
-
`Failed to download ${model} ${fileName} from ${repo}: ${response.status} ${response.statusText}`,
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
await Bun.write(target, await response.arrayBuffer());
|
|
37
|
-
}
|
|
38
|
-
return true;
|
|
29
|
+
/**
|
|
30
|
+
* Whether `FlagEmbedding.init` can load `model` from `cacheDir` without a
|
|
31
|
+
* download. fastembed caches each file under `<cacheDir>/<repo with / → _>/`
|
|
32
|
+
* and fetches only the files that are missing, so a model is cached exactly
|
|
33
|
+
* when every file is present. Unknown models report false. The agent's embed
|
|
34
|
+
* client uses this to decide whether an init shows download activity.
|
|
35
|
+
*/
|
|
36
|
+
export async function isFastembedModelCached(model: string, cacheDir: string): Promise<boolean> {
|
|
37
|
+
const source = FASTEMBED_MODEL_SOURCES[model];
|
|
38
|
+
if (source === undefined) return false;
|
|
39
|
+
const modelDir = path.join(cacheDir, source.repo.replace("/", "_"));
|
|
40
|
+
const present = await Promise.all(
|
|
41
|
+
[...source.files, ...FASTEMBED_TOKENIZER_FILES].map(file =>
|
|
42
|
+
fs.access(path.join(modelDir, file)).then(
|
|
43
|
+
() => true,
|
|
44
|
+
() => false,
|
|
45
|
+
),
|
|
46
|
+
),
|
|
47
|
+
);
|
|
48
|
+
return present.every(Boolean);
|
|
39
49
|
}
|
|
@@ -33,15 +33,26 @@ export interface FastembedRuntimeInstallPlan {
|
|
|
33
33
|
* manifest is inlined into a bundle. The runtime install deliberately does not
|
|
34
34
|
* override fastembed's `onnxruntime-node` dependency: the prebuilt native addon
|
|
35
35
|
* links against that package's bundled ORT dylib/so/dll name.
|
|
36
|
+
*
|
|
37
|
+
* It does override `@anush008/tokenizers`: every fastembed release pins
|
|
38
|
+
* `^0.0.0`, which resolves to a build without linux-arm64 bindings (#14083).
|
|
39
|
+
* {@link TOKENIZERS_SPEC} keeps the identical JS API and adds them.
|
|
36
40
|
*/
|
|
37
41
|
const FASTEMBED_SPEC = packageManifest.peerDependencies.fastembed;
|
|
38
42
|
|
|
43
|
+
/** `@anush008/tokenizers` release with linux-arm64 gnu/musl bindings (added in 0.5.0). */
|
|
44
|
+
const TOKENIZERS_SPEC = "0.6.0";
|
|
45
|
+
|
|
39
46
|
/** Build the deterministic fastembed runtime install plan used by local embeddings. */
|
|
40
47
|
export function fastembedRuntimeInstallPlan(): FastembedRuntimeInstallPlan {
|
|
41
48
|
return {
|
|
42
|
-
versionKey: `fastembed-${FASTEMBED_SPEC}_transitive-
|
|
49
|
+
versionKey: `fastembed-${FASTEMBED_SPEC}_transitive-ort_tokenizers-${TOKENIZERS_SPEC}`.replace(
|
|
50
|
+
/[^A-Za-z0-9._-]/g,
|
|
51
|
+
"_",
|
|
52
|
+
),
|
|
43
53
|
install: {
|
|
44
54
|
dependencies: { fastembed: FASTEMBED_SPEC },
|
|
55
|
+
overrides: { "@anush008/tokenizers": TOKENIZERS_SPEC },
|
|
45
56
|
trustedDependencies: ["onnxruntime-node"],
|
|
46
57
|
},
|
|
47
58
|
};
|
package/src/core/query-cache.ts
CHANGED
|
@@ -31,6 +31,8 @@ export interface QueryCacheStats {
|
|
|
31
31
|
interface CacheEntry<T> {
|
|
32
32
|
readonly scope: string;
|
|
33
33
|
readonly normalized: string;
|
|
34
|
+
/** Distinct words of {@link normalized}, for the tier-4 overlap test. */
|
|
35
|
+
readonly words: ReadonlySet<string>;
|
|
34
36
|
readonly results: readonly T[];
|
|
35
37
|
readonly embedding: QueryEmbedding | null;
|
|
36
38
|
}
|
|
@@ -66,7 +68,9 @@ export function isQueryCacheEnabled(useCache = true, env: Env = process.env, con
|
|
|
66
68
|
*
|
|
67
69
|
* - Tier 1: exact normalized query match.
|
|
68
70
|
* - Tiers 2/3: query embedding cosine >= 0.88, or >= 0.78 plus word overlap.
|
|
69
|
-
* - Tier 4:
|
|
71
|
+
* - Tier 4: shared distinct words cover at least 70% of the new query and half of the
|
|
72
|
+
* cached one, so a reworded or trimmed query hits but a short query never lands
|
|
73
|
+
* inside a long cached prompt that merely contains its words.
|
|
70
74
|
*/
|
|
71
75
|
export class QueryCache<T = QueryCacheResult> {
|
|
72
76
|
readonly maxSize: number;
|
|
@@ -122,7 +126,13 @@ export class QueryCache<T = QueryCacheResult> {
|
|
|
122
126
|
const results = JSON.parse(row.results_json) as T[];
|
|
123
127
|
const embedding = row.embedding_json === null ? null : (JSON.parse(row.embedding_json) as number[]);
|
|
124
128
|
const { scope, normalized } = splitEntryKey(row.normalized);
|
|
125
|
-
this.#entries.set(row.normalized, {
|
|
129
|
+
this.#entries.set(row.normalized, {
|
|
130
|
+
scope,
|
|
131
|
+
normalized,
|
|
132
|
+
words: this.#wordSet(normalized),
|
|
133
|
+
results,
|
|
134
|
+
embedding,
|
|
135
|
+
});
|
|
126
136
|
this.#rememberKey(row.normalized, now);
|
|
127
137
|
} catch {
|
|
128
138
|
// Match Python's best-effort persistence loading: corrupt rows are ignored.
|
|
@@ -197,11 +207,11 @@ export class QueryCache<T = QueryCacheResult> {
|
|
|
197
207
|
for (const [cachedKey, cached] of this.#entries) {
|
|
198
208
|
if (cached.scope !== scope) continue;
|
|
199
209
|
if (this.#isExpired(cachedKey, now)) continue;
|
|
200
|
-
queryWords ??=
|
|
210
|
+
queryWords ??= this.#wordSet(normalized);
|
|
201
211
|
if (queryWords.size === 0) continue;
|
|
202
|
-
let
|
|
203
|
-
for (const
|
|
204
|
-
if (
|
|
212
|
+
let shared = 0;
|
|
213
|
+
for (const word of queryWords) if (cached.words.has(word)) shared += 1;
|
|
214
|
+
if (shared >= 2 && shared >= queryWords.size * 0.7 && shared >= cached.words.size * 0.5) {
|
|
205
215
|
this.#touchKey(cachedKey);
|
|
206
216
|
this.hits += 1;
|
|
207
217
|
this.tier4Hits += 1;
|
|
@@ -222,7 +232,13 @@ export class QueryCache<T = QueryCacheResult> {
|
|
|
222
232
|
const storedEmbedding =
|
|
223
233
|
embedding !== undefined && embedding !== null && embedding.length !== 0 ? embedding : null;
|
|
224
234
|
this.#entries.delete(key);
|
|
225
|
-
this.#entries.set(key, {
|
|
235
|
+
this.#entries.set(key, {
|
|
236
|
+
scope,
|
|
237
|
+
normalized,
|
|
238
|
+
words: this.#wordSet(normalized),
|
|
239
|
+
results,
|
|
240
|
+
embedding: storedEmbedding,
|
|
241
|
+
});
|
|
226
242
|
this.#rememberKey(key, now);
|
|
227
243
|
this.#putPersistent(key, results, storedEmbedding);
|
|
228
244
|
this.#evictIfNeeded();
|