@securecoms/agent-bridge 0.28.2 → 0.29.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.
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Unified memory (spec section 8.5, H-7, T-33): the messages between the bridge and its ranking worker. Only ids, scores and
3
+ * the opened texts of the items selected for a turn cross back. No message type FROM the worker carries a vector or a
4
+ * raw sealed buffer (worker-protocol.arch.test.ts, Task 14). The query vector goes TO the worker.
5
+ */
6
+ import type { ItemKind, ItemSource, ItemTrust, ScopeKind } from "@agentvault/connect-mls-store/memory-items";
7
+ export interface WorkerInit {
8
+ dbPath: string;
9
+ identityId: string;
10
+ storeKey: Uint8Array;
11
+ modelId: string;
12
+ threshold: number;
13
+ warm: boolean;
14
+ /** Plan Task 35, the recall eval only: replaces OWNER_SCOPE_PRIOR / RECENCY_SLOPE (0.05 since U1-G). Production never sets them (worker-protocol.arch.test.ts). */
15
+ priorOverride?: number;
16
+ recencySlopeOverride?: number;
17
+ }
18
+ export interface RankQuery {
19
+ scopes: string[];
20
+ ownerScope: string | null;
21
+ vec: Float32Array | null;
22
+ macs: Uint8Array[];
23
+ historyConversation: string;
24
+ currentMessageId: string;
25
+ maxItems: number;
26
+ maxChars: number;
27
+ nowMs: number;
28
+ excludeOriginIds: string[];
29
+ }
30
+ export interface OpenedItem {
31
+ itemId: number;
32
+ scopeKind: ScopeKind;
33
+ scopeId: string;
34
+ channelId: string;
35
+ kind: ItemKind;
36
+ originId: string;
37
+ originAt: number;
38
+ source: ItemSource;
39
+ trust: ItemTrust;
40
+ text: string;
41
+ }
42
+ export type ToWorker = {
43
+ type: "query";
44
+ id: number;
45
+ q: RankQuery;
46
+ } | {
47
+ type: "pins";
48
+ id: number;
49
+ scopeId: string;
50
+ maxChars: number;
51
+ } | {
52
+ type: "pending";
53
+ id: number;
54
+ limit: number;
55
+ } | {
56
+ type: "added";
57
+ itemIds: number[];
58
+ } | {
59
+ type: "removed";
60
+ itemIds: number[];
61
+ } | {
62
+ type: "reset";
63
+ };
64
+ export type FromWorker = {
65
+ type: "ready";
66
+ hot: number;
67
+ hotBytes: number;
68
+ } | {
69
+ type: "result";
70
+ id: number;
71
+ items: OpenedItem[];
72
+ openFailed: number;
73
+ } | {
74
+ type: "pins";
75
+ id: number;
76
+ pins: OpenedItem[];
77
+ more: number;
78
+ openFailed: number;
79
+ } | {
80
+ type: "pending";
81
+ id: number;
82
+ rows: Array<{
83
+ itemId: number;
84
+ channelId: string;
85
+ scopeKind: ScopeKind;
86
+ scopeId: string;
87
+ originId: string;
88
+ chunkIndex: number;
89
+ text: string;
90
+ }>;
91
+ } | {
92
+ type: "stats";
93
+ hot: number;
94
+ hotBytes: number;
95
+ }
96
+ /** Final review bridge M-4: a request whose handler threw; the client answers it `failed` (a miss), the worker lives on. */
97
+ | {
98
+ type: "failed";
99
+ id: number;
100
+ } | {
101
+ type: "fatal";
102
+ };
@@ -0,0 +1,583 @@
1
+ // src/memory/unified/ranking-worker.ts
2
+ import { parentPort, workerData } from "node:worker_threads";
3
+ import { DatabaseSync } from "node:sqlite";
4
+
5
+ // ../connect-core/dist/envelope/aead.js
6
+ import { gcm } from "@noble/ciphers/aes";
7
+ import { xchacha20poly1305 } from "@noble/ciphers/chacha";
8
+ import { randomBytes } from "@noble/hashes/utils";
9
+
10
+ // ../../node_modules/@agentvault/crypto/dist/chunk-FCTZTCBV.js
11
+ var ENCODER = new TextEncoder();
12
+
13
+ // ../connect-core/dist/base64.js
14
+ var ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
15
+ var LOOKUP = (() => {
16
+ const t = new Int16Array(256).fill(-1);
17
+ for (let i = 0; i < ALPHABET.length; i++)
18
+ t[ALPHABET.charCodeAt(i)] = i;
19
+ return t;
20
+ })();
21
+
22
+ // ../connect-core/dist/envelope/aead.js
23
+ var APPLICATION_KEY_BYTES = 32;
24
+ var NONCE_BYTES = {
25
+ xchacha20poly1305: 24,
26
+ aes256gcm: 12
27
+ };
28
+ function cipherFor(suite, key, nonce, aad) {
29
+ return suite === "aes256gcm" ? gcm(key, nonce, aad) : xchacha20poly1305(key, nonce, aad);
30
+ }
31
+ function assertKey(key) {
32
+ if (key.length !== APPLICATION_KEY_BYTES) {
33
+ throw new Error(`application key must be ${APPLICATION_KEY_BYTES} bytes (the MLS exporter output); got ${key.length}`);
34
+ }
35
+ }
36
+ function openPayload(p) {
37
+ assertKey(p.key);
38
+ const nlen = NONCE_BYTES[p.suite];
39
+ if (p.ciphertext.length < nlen) {
40
+ throw new Error("openPayload: ciphertext shorter than the AEAD nonce");
41
+ }
42
+ const nonce = p.ciphertext.subarray(0, nlen);
43
+ const body = p.ciphertext.subarray(nlen);
44
+ return cipherFor(p.suite, p.key, nonce, p.associatedData).decrypt(body);
45
+ }
46
+
47
+ // ../connect-mls-store/dist/memory-recall.js
48
+ var RECALL_MAX_DIMS = 1024;
49
+ var enc = new TextEncoder();
50
+ function decodeVec(b) {
51
+ if (b.length === 0 || b.length % 4 !== 0 || b.length / 4 > RECALL_MAX_DIMS)
52
+ return null;
53
+ const dv = new DataView(b.buffer, b.byteOffset, b.length);
54
+ const out = new Float32Array(b.length / 4);
55
+ for (let i = 0; i < out.length; i++) {
56
+ out[i] = dv.getFloat32(i * 4, true);
57
+ if (!Number.isFinite(out[i]))
58
+ return null;
59
+ }
60
+ return out;
61
+ }
62
+ var RECALL_FUTURE_SKEW_MS = 5 * 60 * 1e3;
63
+ var W9_SPENT_HORIZON_MS = 7 * 24 * 60 * 60 * 1e3;
64
+
65
+ // ../connect-mls-store/dist/memory-items.js
66
+ var ITEM_SOURCES = [
67
+ "owner_typed",
68
+ "owner_voice",
69
+ "agent_reply",
70
+ "owner_pin",
71
+ "migrated_note",
72
+ "migrated_recall",
73
+ "imported_history",
74
+ "room_member",
75
+ "room_voice",
76
+ "person",
77
+ "file",
78
+ "room_transcript"
79
+ ];
80
+ var OWNER_CAPABLE_SOURCES = /* @__PURE__ */ new Set(["owner_typed", "owner_voice", "owner_pin", "migrated_note", "migrated_recall"]);
81
+ var ITEM_CAP_DEFAULT = 1e5;
82
+ var BYTE_CAP_DEFAULT = 2 * 1024 * 1024 * 1024;
83
+ var ITEM_ROW_OVERHEAD_BYTES = Math.max(48, 2388);
84
+ var TOKEN_ROW_OVERHEAD_BYTES = Math.max(48, 180);
85
+ var NUL = "\0";
86
+ var enc2 = new TextEncoder();
87
+ var noNul = (v, what) => {
88
+ if (v.includes(NUL))
89
+ throw new RangeError(`${what} holds a NUL`);
90
+ };
91
+ function parseOwnerDmScopeId(scopeId) {
92
+ const parts = scopeId.split(NUL);
93
+ return parts.length === 2 && parts[0] && parts[1] ? { channelId: parts[0], ownerIdentityId: parts[1] } : null;
94
+ }
95
+ function assertScope(kind, scopeId) {
96
+ if (kind === "owner_dm") {
97
+ if (parseOwnerDmScopeId(scopeId) === null)
98
+ throw new RangeError("owner_dm scope id must be channel\u20160\u2016owner");
99
+ } else
100
+ noNul(scopeId, "scope id");
101
+ }
102
+ function assertFields(identityId, f) {
103
+ noNul(identityId, "identity id");
104
+ noNul(f.channelId, "channel id");
105
+ noNul(f.originId, "origin id");
106
+ noNul(f.parentOriginId, "parent origin id");
107
+ assertScope(f.scopeKind, f.scopeId);
108
+ if (!Number.isSafeInteger(f.chunkIndex) || f.chunkIndex < 0)
109
+ throw new RangeError("chunk index");
110
+ if (!Number.isSafeInteger(f.originAt) || !Number.isSafeInteger(f.storedAt))
111
+ throw new RangeError("timestamps");
112
+ const chunk = f.chunkIndex > 0;
113
+ if (chunk !== (f.parentId !== null) || chunk !== (f.parentOriginId !== ""))
114
+ throw new RangeError("parent fields must be set exactly for a chunk");
115
+ if (f.parentId !== null && (!Number.isSafeInteger(f.parentId) || f.parentId < 1))
116
+ throw new RangeError("parent id");
117
+ }
118
+ function itemRecordAad(identityId, f) {
119
+ assertFields(identityId, f);
120
+ return enc2.encode([
121
+ "agent_memory_item",
122
+ identityId,
123
+ f.scopeKind,
124
+ f.scopeId,
125
+ f.channelId,
126
+ f.kind,
127
+ f.originId,
128
+ String(f.chunkIndex),
129
+ f.parentOriginId,
130
+ f.parentId === null ? "" : String(f.parentId),
131
+ String(f.originAt),
132
+ String(f.storedAt)
133
+ ].join(NUL));
134
+ }
135
+ function itemVecAad(identityId, f, modelId) {
136
+ noNul(identityId, "identity id");
137
+ noNul(f.originId, "origin id");
138
+ noNul(modelId, "model id");
139
+ assertScope(f.scopeKind, f.scopeId);
140
+ if (!Number.isSafeInteger(f.chunkIndex) || f.chunkIndex < 0)
141
+ throw new RangeError("chunk index");
142
+ return enc2.encode(["agent_memory_item_vec", identityId, f.scopeKind, f.scopeId, f.originId, String(f.chunkIndex), modelId].join(NUL));
143
+ }
144
+ function decodeItemRecord(b) {
145
+ let o;
146
+ try {
147
+ o = JSON.parse(new TextDecoder().decode(b));
148
+ } catch {
149
+ return null;
150
+ }
151
+ if (!o || typeof o !== "object" || Array.isArray(o))
152
+ return null;
153
+ const { v, t, s, tr, sr, c } = o;
154
+ if (v !== 1 || typeof t !== "string" || typeof s !== "string" || !ITEM_SOURCES.includes(s))
155
+ return null;
156
+ if (tr !== "owner" && tr !== "reference")
157
+ return null;
158
+ if (tr === "owner" && !OWNER_CAPABLE_SOURCES.has(s))
159
+ return null;
160
+ if (sr !== void 0 && typeof sr !== "string")
161
+ return null;
162
+ if (c !== void 0 && (typeof c !== "number" || !Number.isSafeInteger(c) || c < 1))
163
+ return null;
164
+ return { text: t, source: s, trust: tr, ...sr !== void 0 ? { sourceRef: sr } : {}, ...c !== void 0 ? { chunks: c } : {} };
165
+ }
166
+
167
+ // src/memory/unified/ranking-core.ts
168
+ var DIMS = 384;
169
+ var OWNER_SCOPE_PRIOR = 0.05;
170
+ var RECENCY_FLOOR = 0.85;
171
+ var RECENCY_SLOPE = 0.05;
172
+ var RRF_K = 60;
173
+ var HOT_TOKEN_MAX_POSTINGS = 5e3;
174
+ var LIST_MAX = 50;
175
+ var FRESHNESS_TIE_EPSILON = 1e-12;
176
+ var HOT_SET_HEADROOM = 1024;
177
+ var HOT_SET_GROWTH = 1.25;
178
+ var DAY_MS = 864e5;
179
+ function recencyWeight(ageDays, slope = RECENCY_SLOPE) {
180
+ return Math.max(RECENCY_FLOOR, 1 - Math.max(0, ageDays) / 365 * slope);
181
+ }
182
+ function hotSetReserve(embeddedCount) {
183
+ return Math.min(Math.max(0, embeddedCount), ITEM_CAP_DEFAULT) + HOT_SET_HEADROOM;
184
+ }
185
+ function hotSetGrowth(cap, need) {
186
+ let next = Math.min(Math.ceil(cap * HOT_SET_GROWTH), ITEM_CAP_DEFAULT);
187
+ if (next <= cap) next = cap + HOT_SET_HEADROOM;
188
+ return Math.max(need, next, HOT_SET_HEADROOM);
189
+ }
190
+ var HotSet = class {
191
+ cap = 0;
192
+ n = 0;
193
+ q = new Int8Array(0);
194
+ scale = new Float32Array(0);
195
+ ids = new Float64Array(0);
196
+ at = new Float64Array(0);
197
+ scopeOf = new Int32Array(0);
198
+ pin = new Uint8Array(0);
199
+ slotOf = /* @__PURE__ */ new Map();
200
+ scopes = [];
201
+ scopeIdx = /* @__PURE__ */ new Map();
202
+ get size() {
203
+ return this.n;
204
+ }
205
+ get capacity() {
206
+ return this.cap;
207
+ }
208
+ vectorBytes() {
209
+ return this.n * DIMS;
210
+ }
211
+ bytes() {
212
+ return this.q.byteLength + this.scale.byteLength + this.ids.byteLength + this.at.byteLength + this.scopeOf.byteLength + this.pin.byteLength;
213
+ }
214
+ /** U1-G (D): sets the capacity to at least `slots` in one allocation (the warm reserve). */
215
+ reserve(slots) {
216
+ if (slots > this.cap) this.resize(slots);
217
+ }
218
+ grow(min) {
219
+ if (min > this.cap) this.resize(hotSetGrowth(this.cap, min));
220
+ }
221
+ /** The old buffers are dropped (unreferenced) as soon as they are copied. */
222
+ resize(cap) {
223
+ const q = new Int8Array(cap * DIMS);
224
+ q.set(this.q);
225
+ this.q = q;
226
+ const sc = new Float32Array(cap);
227
+ sc.set(this.scale);
228
+ this.scale = sc;
229
+ const ids = new Float64Array(cap);
230
+ ids.set(this.ids);
231
+ this.ids = ids;
232
+ const at = new Float64Array(cap);
233
+ at.set(this.at);
234
+ this.at = at;
235
+ const so = new Int32Array(cap);
236
+ so.set(this.scopeOf);
237
+ this.scopeOf = so;
238
+ const p = new Uint8Array(cap);
239
+ p.set(this.pin);
240
+ this.pin = p;
241
+ this.cap = cap;
242
+ }
243
+ add(e) {
244
+ if (e.vec.length !== DIMS) return;
245
+ let slot = this.slotOf.get(e.itemId);
246
+ if (slot === void 0) {
247
+ this.grow(this.n + 1);
248
+ slot = this.n++;
249
+ this.slotOf.set(e.itemId, slot);
250
+ }
251
+ let m = 0;
252
+ for (const x of e.vec) m = Math.max(m, Math.abs(x));
253
+ const s = m > 0 ? m / 127 : 1;
254
+ for (let i = 0; i < DIMS; i++) this.q[slot * DIMS + i] = Math.round(e.vec[i] / s);
255
+ let si = this.scopeIdx.get(e.scopeId);
256
+ if (si === void 0) {
257
+ si = this.scopes.length;
258
+ this.scopes.push(e.scopeId);
259
+ this.scopeIdx.set(e.scopeId, si);
260
+ }
261
+ this.scale[slot] = s;
262
+ this.ids[slot] = e.itemId;
263
+ this.at[slot] = e.originAt;
264
+ this.scopeOf[slot] = si;
265
+ this.pin[slot] = e.pinned ? 1 : 0;
266
+ }
267
+ remove(itemId) {
268
+ const slot = this.slotOf.get(itemId);
269
+ if (slot === void 0) return;
270
+ const last = this.n - 1;
271
+ if (slot !== last) {
272
+ this.q.copyWithin(slot * DIMS, last * DIMS, (last + 1) * DIMS);
273
+ this.scale[slot] = this.scale[last];
274
+ this.ids[slot] = this.ids[last];
275
+ this.at[slot] = this.at[last];
276
+ this.scopeOf[slot] = this.scopeOf[last];
277
+ this.pin[slot] = this.pin[last];
278
+ this.slotOf.set(this.ids[slot], slot);
279
+ }
280
+ this.slotOf.delete(itemId);
281
+ this.n--;
282
+ }
283
+ clear() {
284
+ this.n = 0;
285
+ this.slotOf.clear();
286
+ }
287
+ /** section 8.5: sim (int8 dot x scale), meaning threshold, then (sim + prior) x recency; the top `k` by that score. */
288
+ topK(query, allowed, o) {
289
+ const ok = new Uint8Array(this.scopes.length);
290
+ this.scopes.forEach((s, i) => {
291
+ ok[i] = allowed.has(s) ? 1 : 0;
292
+ });
293
+ const owner = o.ownerScope === null ? -1 : this.scopeIdx.get(o.ownerScope) ?? -2;
294
+ const top = [];
295
+ for (let slot = 0; slot < this.n; slot++) {
296
+ const si = this.scopeOf[slot];
297
+ if (!ok[si]) continue;
298
+ let dot = 0;
299
+ const base = slot * DIMS;
300
+ for (let i = 0; i < DIMS; i++) dot += this.q[base + i] * query[i];
301
+ const sim = dot * this.scale[slot];
302
+ if (sim < o.threshold) continue;
303
+ const prior = si === owner || this.pin[slot] === 1 ? o.prior ?? OWNER_SCOPE_PRIOR : 0;
304
+ const score = (sim + prior) * recencyWeight((o.nowMs - this.at[slot]) / DAY_MS, o.recencySlope);
305
+ if (top.length < o.k) {
306
+ top.push({ itemId: this.ids[slot], sim, score });
307
+ top.sort((a, b) => a.score - b.score);
308
+ } else if (score > top[0].score) {
309
+ top[0] = { itemId: this.ids[slot], sim, score };
310
+ top.sort((a, b) => a.score - b.score);
311
+ }
312
+ }
313
+ return top.reverse();
314
+ }
315
+ };
316
+ var UTF8 = new TextDecoder();
317
+ var SCOPE_B = "CAST(i.scope_id AS BLOB) AS scope_id_b";
318
+ function fixScope(r) {
319
+ if (r && r.scope_id_b) r.scope_id = UTF8.decode(r.scope_id_b);
320
+ return r;
321
+ }
322
+ var RankingCore = class {
323
+ constructor(init2, db) {
324
+ this.init = init2;
325
+ this.db = db;
326
+ }
327
+ hot = new HotSet();
328
+ stmts = /* @__PURE__ */ new Map();
329
+ /** Prepared once per distinct statement text (the keyword statement varies only with the MAC and scope counts). */
330
+ stmt(sql) {
331
+ let st = this.stmts.get(sql);
332
+ if (!st) {
333
+ if (this.stmts.size >= 64) this.stmts.clear();
334
+ st = this.db.prepare(sql);
335
+ this.stmts.set(sql, st);
336
+ }
337
+ return st;
338
+ }
339
+ hotCount() {
340
+ return this.hot.size;
341
+ }
342
+ hotCapacity() {
343
+ return this.hot.capacity;
344
+ }
345
+ /** The meaning list a query would fuse (section 8.5 scores, the init's eval overrides applied when set). */
346
+ meaningScores(vec, scopes, ownerScope, nowMs) {
347
+ return this.hot.topK(vec, new Set(scopes), {
348
+ k: LIST_MAX,
349
+ nowMs,
350
+ ownerScope,
351
+ threshold: this.init.threshold,
352
+ ...this.init.priorOverride !== void 0 ? { prior: this.init.priorOverride } : {},
353
+ ...this.init.recencySlopeOverride !== void 0 ? { recencySlope: this.init.recencySlopeOverride } : {}
354
+ });
355
+ }
356
+ hotBytes() {
357
+ return this.hot.bytes();
358
+ }
359
+ open(sealed, aad) {
360
+ try {
361
+ return openPayload({ ciphertext: sealed, key: this.init.storeKey, associatedData: aad, suite: "xchacha20poly1305" });
362
+ } catch {
363
+ return null;
364
+ }
365
+ }
366
+ fields(r) {
367
+ return {
368
+ scopeKind: r.scope_kind,
369
+ scopeId: r.scope_id,
370
+ channelId: r.channel_id,
371
+ kind: r.kind,
372
+ originId: r.origin_id,
373
+ chunkIndex: Number(r.chunk_index),
374
+ parentOriginId: r.parent_id === null ? "" : String(r.parent_origin_id ?? ""),
375
+ parentId: r.parent_id === null ? null : Number(r.parent_id),
376
+ originAt: Number(r.origin_at),
377
+ storedAt: Number(r.stored_at)
378
+ };
379
+ }
380
+ row(itemId) {
381
+ return fixScope(this.stmt(`SELECT i.*, ${SCOPE_B}, p.origin_id AS parent_origin_id FROM agent_memory_item i LEFT JOIN agent_memory_item p
382
+ ON p.identity_id=i.identity_id AND p.item_id=i.parent_id WHERE i.identity_id=? AND i.item_id=?`).get(this.init.identityId, itemId));
383
+ }
384
+ addRow(r) {
385
+ if (r.sealed_vec === null || r.model_id !== this.init.modelId) return;
386
+ const plain = this.open(r.sealed_vec, itemVecAad(this.init.identityId, this.fields(r), r.model_id));
387
+ const vec = plain ? decodeVec(plain) : null;
388
+ if (vec && vec.length === DIMS) this.hot.add({ itemId: Number(r.item_id), vec, scopeId: r.scope_id, originAt: Number(r.origin_at), pinned: r.kind === "pinned" });
389
+ }
390
+ /** section 8.5 warm: one row per read, a yield between rows; returns the hot count. A disabled warm (win32 keyword-only) loads nothing. */
391
+ async warm() {
392
+ if (!this.init.warm) return 0;
393
+ const count = this.stmt("SELECT COUNT(*) AS n FROM agent_memory_item WHERE identity_id=? AND sealed_vec IS NOT NULL AND model_id=?").get(this.init.identityId, this.init.modelId);
394
+ this.hot.reserve(hotSetReserve(Number(count?.n ?? 0)));
395
+ let after = 0;
396
+ const next = this.stmt(`SELECT i.*, ${SCOPE_B}, NULL AS parent_origin_id FROM agent_memory_item i WHERE i.identity_id=? AND i.item_id>? AND i.sealed_vec IS NOT NULL
397
+ AND i.model_id=? ORDER BY i.item_id LIMIT 1`);
398
+ for (; ; ) {
399
+ const r = fixScope(next.get(this.init.identityId, after, this.init.modelId));
400
+ if (!r) return this.hot.size;
401
+ after = Number(r.item_id);
402
+ const full = r.parent_id === null ? r : this.row(after);
403
+ if (full) this.addRow(full);
404
+ await new Promise((res) => setImmediate(res));
405
+ }
406
+ }
407
+ /** Opens a record; null on any failure or a source_ref mismatch (section 11.3). */
408
+ openRecord(r) {
409
+ const plain = this.open(r.sealed_record, itemRecordAad(this.init.identityId, this.fields(r)));
410
+ const rec = plain ? decodeItemRecord(plain) : null;
411
+ if (!rec || (r.source_ref ?? void 0) !== rec.sourceRef) return null;
412
+ return rec;
413
+ }
414
+ opened(r, rec) {
415
+ return {
416
+ itemId: Number(r.item_id),
417
+ scopeKind: r.scope_kind,
418
+ scopeId: r.scope_id,
419
+ channelId: r.channel_id,
420
+ kind: r.kind,
421
+ originId: r.parent_id === null ? r.origin_id : String(r.parent_origin_id),
422
+ originAt: Number(r.origin_at),
423
+ source: rec.source,
424
+ trust: rec.trust,
425
+ text: rec.text
426
+ };
427
+ }
428
+ /** H-7: one batched statement, LIMIT 5,001 per MAC inside the allowed scopes; a MAC that fills its limit is skipped. */
429
+ keyword(macs, scopes) {
430
+ if (macs.length === 0 || scopes.length === 0) return [];
431
+ const ph = scopes.map(() => "?").join(",");
432
+ const part = `SELECT * FROM (SELECT ? AS m, t.item_id AS item_id, i.origin_at AS origin_at FROM agent_memory_token t JOIN agent_memory_item i
433
+ ON i.identity_id=t.identity_id AND i.item_id=t.item_id WHERE t.identity_id=? AND t.token_mac=? AND i.scope_id IN (${ph}) LIMIT ${HOT_TOKEN_MAX_POSTINGS + 1})`;
434
+ const sql = macs.map(() => part).join(" UNION ALL ");
435
+ const args = [];
436
+ macs.forEach((m, i) => args.push(i, this.init.identityId, m, ...scopes));
437
+ const rows = this.stmt(sql).all(...args);
438
+ const per = /* @__PURE__ */ new Map();
439
+ for (const r of rows) per.set(r.m, (per.get(r.m) ?? 0) + 1);
440
+ const hits = /* @__PURE__ */ new Map();
441
+ for (const r of rows) {
442
+ if ((per.get(r.m) ?? 0) > HOT_TOKEN_MAX_POSTINGS) continue;
443
+ const h = hits.get(r.item_id) ?? { hits: 0, originAt: Number(r.origin_at) };
444
+ h.hits++;
445
+ hits.set(r.item_id, h);
446
+ }
447
+ return [...hits].map(([itemId, h]) => ({ itemId, ...h })).sort((a, b) => b.hits - a.hits || b.originAt - a.originAt).slice(0, LIST_MAX);
448
+ }
449
+ /** section 8.1 step 5: message ids of the newest 50 history turns of the conversation, plus the current message. */
450
+ excluded(q) {
451
+ const rows = this.stmt("SELECT message_id FROM agent_memory WHERE identity_id=? AND conversation_id=? AND message_id IS NOT NULL ORDER BY seq DESC LIMIT 50").all(this.init.identityId, q.historyConversation);
452
+ return /* @__PURE__ */ new Set([...rows.map((r) => r.message_id), q.currentMessageId, ...q.excludeOriginIds]);
453
+ }
454
+ rank(q) {
455
+ const allowed = new Set(q.scopes);
456
+ const meaning = q.vec ? this.meaningScores(q.vec, q.scopes, q.ownerScope, q.nowMs) : [];
457
+ const kw = this.keyword(q.macs, q.scopes);
458
+ const fused = /* @__PURE__ */ new Map();
459
+ meaning.forEach((m, rank) => fused.set(m.itemId, 1 / (RRF_K + rank + 1)));
460
+ const keywordOnlyRead = q.vec === null || !this.init.warm;
461
+ const kwOnly = /* @__PURE__ */ new Set();
462
+ kw.forEach((k, rank) => {
463
+ if (!fused.has(k.itemId)) kwOnly.add(k.itemId);
464
+ fused.set(k.itemId, (fused.get(k.itemId) ?? 0) + 1 / (RRF_K + rank + 1));
465
+ });
466
+ const ex = this.excluded(q);
467
+ const order = [...fused.keys()];
468
+ const items = [];
469
+ let chars = 0, openFailed = 0;
470
+ const candidates = [];
471
+ for (const id of order) {
472
+ const r = this.row(id);
473
+ if (!r || !allowed.has(r.scope_id)) continue;
474
+ const pendingEmbed = r.sealed_vec === null || r.model_id !== this.init.modelId;
475
+ if (kwOnly.has(id) && !keywordOnlyRead && !pendingEmbed) continue;
476
+ const fresh = 1 - Math.min(365, Math.max(0, (q.nowMs - Number(r.origin_at)) / DAY_MS)) / 365;
477
+ candidates.push({ r, score: fused.get(id), fresh });
478
+ }
479
+ candidates.sort((a, b) => Math.abs(b.score - a.score) > FRESHNESS_TIE_EPSILON ? b.score - a.score : b.fresh - a.fresh);
480
+ for (const { r } of candidates) {
481
+ if (items.length >= q.maxItems) break;
482
+ const origin = r.parent_id === null ? r.origin_id : String(r.parent_origin_id);
483
+ if (ex.has(origin)) continue;
484
+ const rec = this.openRecord(r);
485
+ if (rec === null) {
486
+ openFailed++;
487
+ continue;
488
+ }
489
+ if (rec.chunks !== void 0) continue;
490
+ if (chars + rec.text.length > q.maxChars) break;
491
+ chars += rec.text.length;
492
+ items.push(this.opened(r, rec));
493
+ }
494
+ return { items, openFailed };
495
+ }
496
+ pins(scopeId, maxChars) {
497
+ const rows = this.stmt(`SELECT i.*, ${SCOPE_B}, p.origin_id AS parent_origin_id FROM agent_memory_item i LEFT JOIN agent_memory_item p
498
+ ON p.identity_id=i.identity_id AND p.item_id=i.parent_id WHERE i.identity_id=? AND i.scope_id=? AND i.kind='pinned'
499
+ ORDER BY i.origin_at DESC, COALESCE(i.parent_id, i.item_id) DESC, i.chunk_index ASC`).all(this.init.identityId, scopeId);
500
+ rows.forEach(fixScope);
501
+ const pins = [];
502
+ let chars = 0, openFailed = 0, more = 0;
503
+ for (const r of rows) {
504
+ const rec = this.openRecord(r);
505
+ if (rec === null) {
506
+ openFailed++;
507
+ continue;
508
+ }
509
+ if (rec.chunks !== void 0) continue;
510
+ if (more > 0 || chars + rec.text.length > maxChars) {
511
+ more++;
512
+ continue;
513
+ }
514
+ chars += rec.text.length;
515
+ pins.push(this.opened(r, rec));
516
+ }
517
+ return { pins, more, openFailed };
518
+ }
519
+ pending(limit) {
520
+ const rows = this.stmt(`SELECT i.*, ${SCOPE_B}, p.origin_id AS parent_origin_id FROM agent_memory_item i LEFT JOIN agent_memory_item p
521
+ ON p.identity_id=i.identity_id AND p.item_id=i.parent_id WHERE i.identity_id=? AND (i.model_id IS NULL OR i.model_id<>?)
522
+ AND NOT EXISTS (SELECT 1 FROM agent_memory_item c WHERE c.identity_id=i.identity_id AND c.parent_id=i.item_id)
523
+ ORDER BY i.stored_at, i.item_id LIMIT ?`).all(this.init.identityId, this.init.modelId, limit);
524
+ rows.forEach(fixScope);
525
+ const out = [];
526
+ for (const r of rows) {
527
+ const rec = this.openRecord(r);
528
+ if (rec === null) continue;
529
+ out.push({ itemId: Number(r.item_id), channelId: r.channel_id, scopeKind: r.scope_kind, scopeId: r.scope_id, originId: r.origin_id, chunkIndex: Number(r.chunk_index), text: rec.text });
530
+ }
531
+ return out;
532
+ }
533
+ /**
534
+ * Final review bridge M-4: `handle` with a per-message catch. A request that throws (a prepare failure, a decode throw)
535
+ * is answered `failed` instead of becoming an unhandled rejection that kills the worker; a notification that throws is
536
+ * dropped (a later request still warms from the database). Content-free: the error is never sent or logged.
537
+ */
538
+ async handleSafe(m) {
539
+ try {
540
+ return await this.handle(m);
541
+ } catch {
542
+ return "id" in m ? { type: "failed", id: m.id } : null;
543
+ }
544
+ }
545
+ async handle(m) {
546
+ switch (m.type) {
547
+ case "query": {
548
+ const r = this.rank(m.q);
549
+ return { type: "result", id: m.id, items: r.items, openFailed: r.openFailed };
550
+ }
551
+ case "pins": {
552
+ const r = this.pins(m.scopeId, m.maxChars);
553
+ return { type: "pins", id: m.id, ...r };
554
+ }
555
+ case "pending":
556
+ return { type: "pending", id: m.id, rows: this.pending(m.limit) };
557
+ case "added":
558
+ for (const id of m.itemIds) {
559
+ const r = this.row(id);
560
+ if (r) this.addRow(r);
561
+ }
562
+ return { type: "stats", hot: this.hot.size, hotBytes: this.hot.bytes() };
563
+ case "removed":
564
+ for (const id of m.itemIds) this.hot.remove(id);
565
+ return { type: "stats", hot: this.hot.size, hotBytes: this.hot.bytes() };
566
+ case "reset":
567
+ this.hot.clear();
568
+ await this.warm();
569
+ return { type: "stats", hot: this.hot.size, hotBytes: this.hot.bytes() };
570
+ }
571
+ }
572
+ };
573
+
574
+ // src/memory/unified/ranking-worker.ts
575
+ var init = workerData;
576
+ var core = new RankingCore(init, new DatabaseSync(init.dbPath, { readOnly: true }));
577
+ var hot = await core.warm();
578
+ parentPort.postMessage({ type: "ready", hot, hotBytes: core.hotBytes() });
579
+ parentPort.on("message", (m) => {
580
+ void core.handleSafe(m).then((r) => {
581
+ if (r) parentPort.postMessage(r);
582
+ });
583
+ });
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const BRIDGE_VERSION = "0.28.2";
1
+ export declare const BRIDGE_VERSION = "0.29.0";