dsh-cortex-memory 0.1.0 → 0.2.0-rc.13
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/LICENSE +3 -3
- package/README.md +85 -146
- package/bench/ab-engine.mjs +60 -0
- package/bench/ann-bench.mjs +78 -0
- package/bench/hnsw-bench.mjs +79 -0
- package/bench/metrics.js +8 -0
- package/bench/migration-bench.mjs +69 -0
- package/bench/run.js +5 -2
- package/bench/synthetic.js +51 -7
- package/bench/vector-ab.mjs +76 -0
- package/bin/cortex.mjs +2 -3
- package/client.js +286 -4
- package/cordis.patch.yml +1 -1
- package/docs/install-windows.md +115 -27
- package/docs/models.md +165 -30
- package/docs/multimodal.md +112 -0
- package/docs/scopes.md +142 -0
- package/examples/README.md +72 -0
- package/examples/setup-cortex-bge.ps1 +66 -0
- package/examples/uninstall-cortex.ps1 +91 -0
- package/index.js +15 -1
- package/package.json +5 -12
- package/src/adapters/markdown-import.js +20 -1
- package/src/adapters/markdown.js +12 -2
- package/src/adapters/shiori.js +1 -1
- package/src/api/routes.js +148 -4
- package/src/api/validate.js +26 -0
- package/src/core/attachment-reader.js +31 -0
- package/src/core/attachments.js +39 -0
- package/src/core/config.js +36 -1
- package/src/core/model-config.js +3 -1
- package/src/core/profile.js +32 -0
- package/src/engines/develop.js +1 -1
- package/src/engines/encode.js +68 -30
- package/src/engines/extract.js +13 -8
- package/src/engines/growth.js +1 -1
- package/src/engines/reflect.js +1 -1
- package/src/engines/retrieve.js +44 -13
- package/src/host/runtime.js +1 -1
- package/src/memory/query.js +1 -1
- package/src/memory/tokenizer.js +19 -4
- package/src/security/scope-grants.js +73 -0
- package/src/self/appraisal.js +3 -3
- package/src/self/emotion.js +1 -1
- package/src/self/identity.js +1 -1
- package/src/self/relationship.js +2 -2
- package/src/service/cortex.js +191 -3
- package/src/store/database.js +33 -1
- package/src/store/repositories/edges.js +48 -4
- package/src/store/repositories/traces.js +23 -12
- package/src/store/schema.sql +1 -0
- package/src/tools/definitions.js +64 -5
- package/src/vector/external-ann.js +37 -0
- package/src/vector/hnsw.js +246 -0
- package/src/vector/index.js +85 -2
- package/docs/compatibility.md +0 -39
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small dependency-free HNSW index for Cortex.
|
|
3
|
+
*
|
|
4
|
+
* This is a candidate replacement for the single-layer NSW in ann.js. It is
|
|
5
|
+
* intentionally not wired into the production vector index until a benchmark
|
|
6
|
+
* shows acceptable recall. The implementation uses simple sorted frontiers
|
|
7
|
+
* (not a binary heap) because it is easier to audit; this is a benchmark
|
|
8
|
+
* candidate, not a final large-corpus engine.
|
|
9
|
+
*/
|
|
10
|
+
function mulberry32(seed) {
|
|
11
|
+
let state = seed >>> 0;
|
|
12
|
+
return () => {
|
|
13
|
+
state = (state + 0x6d2b79f5) >>> 0;
|
|
14
|
+
let t = state;
|
|
15
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
16
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
17
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function cosine(a, b) {
|
|
22
|
+
let dot = 0;
|
|
23
|
+
let na = 0;
|
|
24
|
+
let nb = 0;
|
|
25
|
+
const n = Math.min(a.length, b.length);
|
|
26
|
+
for (let i = 0; i < n; i += 1) {
|
|
27
|
+
dot += a[i] * b[i];
|
|
28
|
+
na += a[i] * a[i];
|
|
29
|
+
nb += b[i] * b[i];
|
|
30
|
+
}
|
|
31
|
+
if (!na || !nb) return 0;
|
|
32
|
+
return dot / Math.sqrt(na * nb);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function distance(a, b) {
|
|
36
|
+
return 1 - cosine(a, b);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function createHeap(compare) {
|
|
40
|
+
const items = [];
|
|
41
|
+
const swap = (a, b) => { const value = items[a]; items[a] = items[b]; items[b] = value; };
|
|
42
|
+
function bubbleUp(index) {
|
|
43
|
+
while (index > 0) {
|
|
44
|
+
const parent = (index - 1) >> 1;
|
|
45
|
+
if (compare(items[index], items[parent]) >= 0) break;
|
|
46
|
+
swap(index, parent);
|
|
47
|
+
index = parent;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function bubbleDown(index) {
|
|
51
|
+
for (;;) {
|
|
52
|
+
const left = index * 2 + 1;
|
|
53
|
+
const right = left + 1;
|
|
54
|
+
let best = index;
|
|
55
|
+
if (left < items.length && compare(items[left], items[best]) < 0) best = left;
|
|
56
|
+
if (right < items.length && compare(items[right], items[best]) < 0) best = right;
|
|
57
|
+
if (best === index) break;
|
|
58
|
+
swap(index, best);
|
|
59
|
+
index = best;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
push(item) { items.push(item); bubbleUp(items.length - 1); },
|
|
64
|
+
pop() {
|
|
65
|
+
if (!items.length) return undefined;
|
|
66
|
+
const first = items[0];
|
|
67
|
+
const last = items.pop();
|
|
68
|
+
if (items.length) { items[0] = last; bubbleDown(0); }
|
|
69
|
+
return first;
|
|
70
|
+
},
|
|
71
|
+
peek() { return items[0]; },
|
|
72
|
+
get size() { return items.length; },
|
|
73
|
+
toArray() { return items.slice(); },
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function createHnswIndex({ M = 16, efConstruction = 100, efSearch = 64, seed = 42, maxLevel = 16 } = {}) {
|
|
78
|
+
const maxNeighbors = Math.max(2, Math.min(64, Number(M) || 16));
|
|
79
|
+
const constructionWidth = Math.max(maxNeighbors, Number(efConstruction) || 100);
|
|
80
|
+
const searchWidth = Math.max(10, Number(efSearch) || 64);
|
|
81
|
+
const levelCap = Math.max(1, Math.min(32, Number(maxLevel) || 16));
|
|
82
|
+
const levelNormalizer = 1 / Math.log(Math.max(2, maxNeighbors));
|
|
83
|
+
const random = mulberry32(seed);
|
|
84
|
+
const nodes = new Map();
|
|
85
|
+
let entry = null;
|
|
86
|
+
let topLevel = -1;
|
|
87
|
+
|
|
88
|
+
function randomLevel() {
|
|
89
|
+
const value = -Math.log(Math.max(1e-12, random())) * levelNormalizer;
|
|
90
|
+
return Math.min(levelCap, Math.floor(value));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function nearestInLayer(query, startId, layer) {
|
|
94
|
+
let current = nodes.get(startId);
|
|
95
|
+
if (!current) return null;
|
|
96
|
+
let currentDistance = distance(query, current.vector);
|
|
97
|
+
let improved = true;
|
|
98
|
+
while (improved) {
|
|
99
|
+
improved = false;
|
|
100
|
+
for (const neighborId of current.neighbors[layer] ?? []) {
|
|
101
|
+
const neighbor = nodes.get(neighborId);
|
|
102
|
+
if (!neighbor) continue;
|
|
103
|
+
const candidateDistance = distance(query, neighbor.vector);
|
|
104
|
+
if (candidateDistance < currentDistance) {
|
|
105
|
+
current = neighbor;
|
|
106
|
+
currentDistance = candidateDistance;
|
|
107
|
+
improved = true;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return { id: current.id, d: currentDistance };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function searchLayer(query, entryPoints, width, layer) {
|
|
115
|
+
const visited = new Set();
|
|
116
|
+
const candidates = createHeap((a, b) => a.d - b.d);
|
|
117
|
+
const results = createHeap((a, b) => b.d - a.d);
|
|
118
|
+
for (const id of entryPoints) {
|
|
119
|
+
if (visited.has(id)) continue;
|
|
120
|
+
const node = nodes.get(id);
|
|
121
|
+
if (!node) continue;
|
|
122
|
+
const d = distance(query, node.vector);
|
|
123
|
+
visited.add(id);
|
|
124
|
+
candidates.push({ id, d });
|
|
125
|
+
results.push({ id, d });
|
|
126
|
+
}
|
|
127
|
+
while (candidates.size) {
|
|
128
|
+
const current = candidates.pop();
|
|
129
|
+
const worst = results.size >= width ? results.peek().d : Infinity;
|
|
130
|
+
if (current.d > worst && results.size >= width) break;
|
|
131
|
+
const node = nodes.get(current.id);
|
|
132
|
+
if (!node) continue;
|
|
133
|
+
for (const neighborId of node.neighbors[layer] ?? []) {
|
|
134
|
+
if (visited.has(neighborId)) continue;
|
|
135
|
+
visited.add(neighborId);
|
|
136
|
+
const neighbor = nodes.get(neighborId);
|
|
137
|
+
if (!neighbor) continue;
|
|
138
|
+
const d = distance(query, neighbor.vector);
|
|
139
|
+
if (results.size >= width && d >= results.peek().d) continue;
|
|
140
|
+
candidates.push({ id: neighborId, d });
|
|
141
|
+
results.push({ id: neighborId, d });
|
|
142
|
+
if (results.size > width) results.pop();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return results.toArray().sort((a, b) => a.d - b.d);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function connect(node, candidates, layer) {
|
|
149
|
+
const selected = candidates
|
|
150
|
+
.filter((item) => item.id !== node.id && nodes.has(item.id))
|
|
151
|
+
.sort((a, b) => a.d - b.d)
|
|
152
|
+
.slice(0, maxNeighbors);
|
|
153
|
+
node.neighbors[layer] = selected.map((item) => item.id);
|
|
154
|
+
for (const candidate of selected) {
|
|
155
|
+
const other = nodes.get(candidate.id);
|
|
156
|
+
if (!other) continue;
|
|
157
|
+
const neighbors = other.neighbors[layer] ?? [];
|
|
158
|
+
if (!neighbors.includes(node.id)) neighbors.push(node.id);
|
|
159
|
+
if (neighbors.length > maxNeighbors) {
|
|
160
|
+
neighbors.sort((a, b) => distance(other.vector, nodes.get(a)?.vector ?? []) - distance(other.vector, nodes.get(b)?.vector ?? []));
|
|
161
|
+
neighbors.length = maxNeighbors;
|
|
162
|
+
}
|
|
163
|
+
other.neighbors[layer] = neighbors;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function upsert(id, vector) {
|
|
168
|
+
if (!id || !Array.isArray(vector) || !vector.length) throw new Error('hnsw upsert needs id and vector');
|
|
169
|
+
const existing = nodes.get(id);
|
|
170
|
+
if (existing) {
|
|
171
|
+
existing.vector = vector.slice();
|
|
172
|
+
return existing;
|
|
173
|
+
}
|
|
174
|
+
const node = { id, vector: vector.slice(), level: randomLevel(), neighbors: [] };
|
|
175
|
+
nodes.set(id, node);
|
|
176
|
+
if (!entry) {
|
|
177
|
+
entry = id;
|
|
178
|
+
topLevel = node.level;
|
|
179
|
+
return node;
|
|
180
|
+
}
|
|
181
|
+
let current = entry;
|
|
182
|
+
for (let layer = topLevel; layer > node.level; layer -= 1) {
|
|
183
|
+
const nearest = nearestInLayer(node.vector, current, layer);
|
|
184
|
+
if (nearest) current = nearest.id;
|
|
185
|
+
}
|
|
186
|
+
for (let layer = Math.min(node.level, topLevel); layer >= 0; layer -= 1) {
|
|
187
|
+
const candidates = searchLayer(node.vector, [current], constructionWidth, layer);
|
|
188
|
+
connect(node, candidates, layer);
|
|
189
|
+
if (candidates.length) current = candidates[0].id;
|
|
190
|
+
}
|
|
191
|
+
if (node.level > topLevel) {
|
|
192
|
+
entry = id;
|
|
193
|
+
topLevel = node.level;
|
|
194
|
+
}
|
|
195
|
+
return node;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function search(query, k = 10, { ef = searchWidth } = {}) {
|
|
199
|
+
if (!entry || !nodes.size) return [];
|
|
200
|
+
const width = Math.max(Math.max(1, Number(k) || 10), Number(ef) || searchWidth);
|
|
201
|
+
let current = entry;
|
|
202
|
+
for (let layer = topLevel; layer > 0; layer -= 1) {
|
|
203
|
+
const nearest = nearestInLayer(query, current, layer);
|
|
204
|
+
if (nearest) current = nearest.id;
|
|
205
|
+
}
|
|
206
|
+
const ranked = searchLayer(query, [current], width, 0);
|
|
207
|
+
return ranked.slice(0, Math.max(1, Number(k) || 10)).map((item) => ({ id: item.id, score: 1 - item.d }));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function remove(id) {
|
|
211
|
+
const node = nodes.get(id);
|
|
212
|
+
if (!node) return false;
|
|
213
|
+
for (const other of nodes.values()) {
|
|
214
|
+
for (const neighbors of other.neighbors) {
|
|
215
|
+
const index = neighbors.indexOf(id);
|
|
216
|
+
if (index >= 0) neighbors.splice(index, 1);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
nodes.delete(id);
|
|
220
|
+
if (entry === id) {
|
|
221
|
+
const first = nodes.values().next().value;
|
|
222
|
+
entry = first?.id ?? null;
|
|
223
|
+
topLevel = -1;
|
|
224
|
+
for (const other of nodes.values()) topLevel = Math.max(topLevel, other.level);
|
|
225
|
+
}
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function rebuild(entries = []) {
|
|
230
|
+
nodes.clear();
|
|
231
|
+
entry = null;
|
|
232
|
+
topLevel = -1;
|
|
233
|
+
for (const item of entries) upsert(item.id, item.vector);
|
|
234
|
+
return nodes.size;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
upsert,
|
|
239
|
+
search,
|
|
240
|
+
remove,
|
|
241
|
+
rebuild,
|
|
242
|
+
size: () => nodes.size,
|
|
243
|
+
entry: () => entry,
|
|
244
|
+
config: () => ({ M: maxNeighbors, efConstruction: constructionWidth, efSearch: searchWidth, seed }),
|
|
245
|
+
};
|
|
246
|
+
}
|
package/src/vector/index.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { hashText } from '../core/ids.js';
|
|
2
2
|
import { nowIso } from '../core/time.js';
|
|
3
3
|
import { cosine } from './embedder.js';
|
|
4
|
+
import { createAnnIndex } from './ann.js';
|
|
5
|
+
import { createHnswIndex } from './hnsw.js';
|
|
6
|
+
import { createExternalAnnClient } from './external-ann.js';
|
|
4
7
|
import { CortexError, ERROR_CODES } from '../core/errors.js';
|
|
5
8
|
|
|
6
9
|
function toBlob(vector) {
|
|
@@ -18,11 +21,37 @@ function fromBlob(blob) {
|
|
|
18
21
|
* personal memories is small (thousands, not billions), and a full scan keeps
|
|
19
22
|
* the code auditable. A real ANN backend can replace it behind this interface.
|
|
20
23
|
*/
|
|
21
|
-
export function createVectorIndex({
|
|
24
|
+
export function createVectorIndex({
|
|
25
|
+
db,
|
|
26
|
+
embedder,
|
|
27
|
+
model = null,
|
|
28
|
+
dim = null,
|
|
29
|
+
annEnabled = false,
|
|
30
|
+
annMin = 500,
|
|
31
|
+
annEngine = 'hnsw',
|
|
32
|
+
annOptions = {},
|
|
33
|
+
externalAnnEnabled = false,
|
|
34
|
+
externalAnnOptions = {},
|
|
35
|
+
} = {}) {
|
|
22
36
|
if (!db || !embedder) throw new Error('vector index needs db and embedder');
|
|
23
37
|
const effectiveModel = model ?? embedder.model ?? 'unknown';
|
|
24
38
|
const effectiveDim = Number(dim ?? embedder.dim ?? 0);
|
|
25
39
|
if (!effectiveDim) throw new CortexError(ERROR_CODES.INVALID_INPUT, 'embedder dim is required');
|
|
40
|
+
const annMinCount = Math.max(1, Math.min(100000, Number(annMin) || 500));
|
|
41
|
+
const ann = annEnabled
|
|
42
|
+
? (annEngine === 'nsw' ? createAnnIndex(annOptions) : createHnswIndex(annOptions))
|
|
43
|
+
: null;
|
|
44
|
+
const externalAnn = externalAnnEnabled && externalAnnOptions?.endpoint
|
|
45
|
+
? createExternalAnnClient(externalAnnOptions)
|
|
46
|
+
: null;
|
|
47
|
+
let annBuilt = false;
|
|
48
|
+
|
|
49
|
+
function ensureAnn() {
|
|
50
|
+
if (!ann || annBuilt) return;
|
|
51
|
+
const rows = db.prepare('SELECT trace_id, vector FROM embeddings WHERE model = ? AND dim = ?').all(effectiveModel, effectiveDim);
|
|
52
|
+
ann.rebuild(rows.map((row) => ({ id: row.trace_id, vector: Array.from(fromBlob(row.vector)) })));
|
|
53
|
+
annBuilt = true;
|
|
54
|
+
}
|
|
26
55
|
|
|
27
56
|
function upsert(traceId, text) {
|
|
28
57
|
if (!traceId) return null;
|
|
@@ -46,6 +75,10 @@ export function createVectorIndex({ db, embedder, model = null, dim = null } = {
|
|
|
46
75
|
model = excluded.model, dim = excluded.dim, vector = excluded.vector,
|
|
47
76
|
hash = excluded.hash, updated_at = excluded.updated_at
|
|
48
77
|
`).run(traceId, effectiveModel, effectiveDim, toBlob(vector), hash, nowIso());
|
|
78
|
+
if (ann) {
|
|
79
|
+
ann.upsert(traceId, Array.from(vector));
|
|
80
|
+
annBuilt = true;
|
|
81
|
+
}
|
|
49
82
|
return { traceId, model: effectiveModel, dim: effectiveDim, hash };
|
|
50
83
|
}
|
|
51
84
|
|
|
@@ -57,16 +90,45 @@ export function createVectorIndex({ db, embedder, model = null, dim = null } = {
|
|
|
57
90
|
}
|
|
58
91
|
|
|
59
92
|
function remove(traceId) {
|
|
93
|
+
if (ann) ann.remove(traceId);
|
|
60
94
|
return db.prepare('DELETE FROM embeddings WHERE trace_id = ?').run(traceId).changes > 0;
|
|
61
95
|
}
|
|
62
96
|
|
|
63
97
|
function status() {
|
|
64
98
|
const row = db.prepare('SELECT COUNT(*) AS c FROM embeddings').get();
|
|
65
|
-
return {
|
|
99
|
+
return {
|
|
100
|
+
model: effectiveModel,
|
|
101
|
+
dim: effectiveDim,
|
|
102
|
+
count: Number(row?.c ?? 0),
|
|
103
|
+
async: embedder.kind === 'openai-embedding' || embedder.async === true,
|
|
104
|
+
ann: ann ? { enabled: true, engine: annEngine === 'nsw' ? 'nsw' : 'hnsw', size: ann.size(), min: annMinCount, built: annBuilt } : { enabled: false },
|
|
105
|
+
externalAnn: externalAnn ? { enabled: true, endpoint: externalAnn.endpoint } : { enabled: false },
|
|
106
|
+
};
|
|
66
107
|
}
|
|
67
108
|
|
|
68
109
|
function searchWithVector(queryVector, { scopeId = 'global', limit = 20, minScore = 0.05, includeArchived = false } = {}) {
|
|
69
110
|
const stateClause = includeArchived ? "t.state IN ('active','archived')" : "t.state = 'active'";
|
|
111
|
+
if (ann) {
|
|
112
|
+
ensureAnn();
|
|
113
|
+
if (ann.size() >= annMinCount) {
|
|
114
|
+
const approx = ann.search(Array.from(queryVector), Math.max(limit * 3, 20));
|
|
115
|
+
const one = db.prepare(`
|
|
116
|
+
SELECT e.vector FROM embeddings e JOIN traces t ON t.id = e.trace_id
|
|
117
|
+
WHERE e.trace_id = ? AND t.scope_id = ? AND ${stateClause} AND e.model = ? AND e.dim = ?
|
|
118
|
+
`);
|
|
119
|
+
const hits = [];
|
|
120
|
+
for (const candidate of approx) {
|
|
121
|
+
const row = one.get(candidate.id, scopeId, effectiveModel, effectiveDim);
|
|
122
|
+
if (!row) continue;
|
|
123
|
+
const score = cosine(queryVector, fromBlob(row.vector));
|
|
124
|
+
if (score < minScore) continue;
|
|
125
|
+
hits.push({ traceId: candidate.id, score: Math.round(score * 10000) / 10000 });
|
|
126
|
+
}
|
|
127
|
+
if (hits.length >= Math.min(limit, 5)) {
|
|
128
|
+
return hits.sort((a, b) => b.score - a.score).slice(0, Math.max(1, Math.min(200, limit)));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
70
132
|
const rows = db.prepare(`
|
|
71
133
|
SELECT e.trace_id, e.vector, e.model, e.dim, t.importance, t.confidence
|
|
72
134
|
FROM embeddings e JOIN traces t ON t.id = e.trace_id
|
|
@@ -93,10 +155,30 @@ export function createVectorIndex({ db, embedder, model = null, dim = null } = {
|
|
|
93
155
|
if (!text) return [];
|
|
94
156
|
const queryVector = await embedder.embed(text);
|
|
95
157
|
if (!queryVector) return [];
|
|
158
|
+
if (externalAnn) {
|
|
159
|
+
try {
|
|
160
|
+
const approximate = await externalAnn.search(Array.from(queryVector), Math.max(20, (Number(options.limit) || 20) * 3));
|
|
161
|
+
const stateClause = options.includeArchived ? "t.state IN ('active','archived')" : "t.state = 'active'";
|
|
162
|
+
const one = db.prepare(`
|
|
163
|
+
SELECT e.trace_id, e.vector FROM embeddings e JOIN traces t ON t.id = e.trace_id
|
|
164
|
+
WHERE e.trace_id = ? AND t.scope_id = ? AND ${stateClause} AND e.model = ? AND e.dim = ?
|
|
165
|
+
`);
|
|
166
|
+
const hits = [];
|
|
167
|
+
for (const candidate of approximate) {
|
|
168
|
+
const row = one.get(candidate.id, options.scopeId ?? 'global', effectiveModel, effectiveDim);
|
|
169
|
+
if (!row) continue;
|
|
170
|
+
hits.push({ traceId: candidate.id, score: Math.round(Number(candidate.score) * 10000) / 10000 });
|
|
171
|
+
}
|
|
172
|
+
if (hits.length >= Math.min(Number(options.limit) || 20, 5)) return hits;
|
|
173
|
+
} catch {
|
|
174
|
+
/* fall through to exact scan */
|
|
175
|
+
}
|
|
176
|
+
}
|
|
96
177
|
return searchWithVector(queryVector, options);
|
|
97
178
|
}
|
|
98
179
|
|
|
99
180
|
function rebuild({ scopeId = 'global', limit = 5000 } = {}) {
|
|
181
|
+
if (ann) { ann.rebuild([]); annBuilt = true; }
|
|
100
182
|
const traces = db.prepare(`
|
|
101
183
|
SELECT id, title, content, summary FROM traces
|
|
102
184
|
WHERE scope_id = ? AND state = 'active' ORDER BY recorded_at DESC LIMIT ?
|
|
@@ -106,6 +188,7 @@ export function createVectorIndex({ db, embedder, model = null, dim = null } = {
|
|
|
106
188
|
}
|
|
107
189
|
|
|
108
190
|
async function rebuildAsync({ scopeId = 'global', limit = 5000 } = {}) {
|
|
191
|
+
if (ann) { ann.rebuild([]); annBuilt = true; }
|
|
109
192
|
const traces = db.prepare(`
|
|
110
193
|
SELECT id, title, content, summary FROM traces
|
|
111
194
|
WHERE scope_id = ? AND state = 'active' ORDER BY recorded_at DESC LIMIT ?
|
package/docs/compatibility.md
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
# Windows / DSH compatibility
|
|
2
|
-
|
|
3
|
-
## Tested here
|
|
4
|
-
|
|
5
|
-
| Item | Value |
|
|
6
|
-
|---|---|
|
|
7
|
-
| OS | Windows 11 |
|
|
8
|
-
| DSH | 0.2.2-rc.1 |
|
|
9
|
-
| Node | 26.13.0 |
|
|
10
|
-
| Public package | `dsh-cortex-memory` 0.1.0 |
|
|
11
|
-
| Data root | fresh `~/.dsh/cortex` |
|
|
12
|
-
| Native dependencies | none required |
|
|
13
|
-
| Tests | 373 passing |
|
|
14
|
-
| Package smoke | packed tgz extracted and started |
|
|
15
|
-
|
|
16
|
-
## What was actually checked
|
|
17
|
-
|
|
18
|
-
1. `npm pack` produced a 99-file tgz with `dsh.bundle.patch`.
|
|
19
|
-
2. The tgz was extracted; `createCortex` wrote, searched and health-checked.
|
|
20
|
-
3. The public source tests passed: 373 / 0.
|
|
21
|
-
4. The client bundle is a DSH web client plugin; the host row is a bundle patch.
|
|
22
|
-
5. No absolute personal paths or credentials are required.
|
|
23
|
-
|
|
24
|
-
## Hard requirements
|
|
25
|
-
|
|
26
|
-
- **Node.js >= 24** — the engine uses `node:sqlite`.
|
|
27
|
-
- **DSH >=0.2.1-rc.2 <0.4.0**.
|
|
28
|
-
- A profile may run only one plugin whose host id is `cortex`.
|
|
29
|
-
|
|
30
|
-
## Not yet verified
|
|
31
|
-
|
|
32
|
-
- Installation into a brand-new DSH profile from the exact release tgz.
|
|
33
|
-
- A click-through browser E2E of every panel control.
|
|
34
|
-
- Node 20/22 users.
|
|
35
|
-
- Very old DSH releases.
|
|
36
|
-
- Running alongside the private development build that uses the same `cortex` id.
|
|
37
|
-
|
|
38
|
-
Do not claim “works for every Windows/DSH user” until a clean-profile
|
|
39
|
-
installation is done on a machine without the development build.
|