@noir-ai/memory 1.0.0-beta.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/dist/index.js ADDED
@@ -0,0 +1,762 @@
1
+ // src/capture.ts
2
+ var CAPTURE_HOOKS = ["PreToolUse", "PostToolUse", "UserPromptSubmit", "Stop"];
3
+ var DEFAULT_CAPTURE_HOOKS = ["Stop", "UserPromptSubmit"];
4
+ var DEFAULT_CAPTURE_POLICY = { hooks: DEFAULT_CAPTURE_HOOKS };
5
+ function toSaveInput(event, policy = DEFAULT_CAPTURE_POLICY) {
6
+ const allowed = policy.hooks ?? DEFAULT_CAPTURE_HOOKS;
7
+ if (!allowed.includes(event.event_type)) return null;
8
+ const content = buildContent(event);
9
+ if (content === null || content.trim().length === 0) return null;
10
+ const input = {
11
+ content,
12
+ type: inferType(event),
13
+ sessionId: event.sessionId ?? void 0
14
+ };
15
+ const files = extractFiles(event.payload);
16
+ if (files.length > 0) input.files = files;
17
+ return input;
18
+ }
19
+ function buildContent(event) {
20
+ const { event_type, payload } = event;
21
+ if (event_type === "Stop") {
22
+ return payload.summary ?? null;
23
+ }
24
+ if (event_type === "UserPromptSubmit") {
25
+ return payload.prompt ?? null;
26
+ }
27
+ if (typeof payload.summary === "string") return payload.summary;
28
+ if (typeof payload.prompt === "string") return payload.prompt;
29
+ return describeToolCall(payload);
30
+ }
31
+ function describeToolCall(payload) {
32
+ const { toolName, toolInput } = payload;
33
+ const cmd = stringField(toolInput, "command");
34
+ const filePath = stringField(toolInput, "file_path");
35
+ const pattern = stringField(toolInput, "pattern");
36
+ if (toolName !== void 0) {
37
+ if (cmd !== null) return `Ran ${toolName}: ${cmd}`;
38
+ if (filePath !== null) {
39
+ return isWriteTool(toolName) ? `Edited ${filePath}` : `${toolName}: ${filePath}`;
40
+ }
41
+ if (pattern !== null) return `${toolName} /${pattern}/`;
42
+ return `Used ${toolName}`;
43
+ }
44
+ if (cmd !== null) return `Ran command: ${cmd}`;
45
+ if (filePath !== null) return `Touched ${filePath}`;
46
+ return null;
47
+ }
48
+ function inferType(event) {
49
+ if (event.event_type === "UserPromptSubmit" && event.payload.prompt !== void 0) {
50
+ return looksDecisionShaped(event.payload.prompt) ? "decision" : "fact";
51
+ }
52
+ if (event.event_type === "PreToolUse" || event.event_type === "PostToolUse") {
53
+ return "workflow";
54
+ }
55
+ if (event.event_type === "Stop") {
56
+ return "workflow";
57
+ }
58
+ return "fact";
59
+ }
60
+ function extractFiles(payload) {
61
+ const filePath = stringField(payload.toolInput, "file_path");
62
+ return filePath !== null ? [filePath] : [];
63
+ }
64
+ function captureSource(eventType) {
65
+ return `auto:${String(eventType).toLowerCase()}`;
66
+ }
67
+ var DECISION_CUES = [
68
+ "decided",
69
+ "decision",
70
+ "should",
71
+ "let\u2019s",
72
+ "let's",
73
+ "going with",
74
+ "use ",
75
+ "adopt",
76
+ "convention",
77
+ "rule:",
78
+ "always ",
79
+ "never "
80
+ ];
81
+ function looksDecisionShaped(prompt) {
82
+ const lower = prompt.toLowerCase();
83
+ return DECISION_CUES.some((cue) => lower.includes(cue));
84
+ }
85
+ function isWriteTool(toolName) {
86
+ const t = toolName.toLowerCase();
87
+ return t === "edit" || t === "write" || t === "multiedit" || t === "notebookedit";
88
+ }
89
+ function stringField(obj, field) {
90
+ if (typeof obj !== "object" || obj === null) return null;
91
+ const rec = obj;
92
+ const val = rec[field];
93
+ return typeof val === "string" ? val : null;
94
+ }
95
+
96
+ // src/config.ts
97
+ function resolveMemoryConfig(raw) {
98
+ const cons = raw?.consolidation;
99
+ const enabled = cons?.enabled === true;
100
+ const provider = cons?.provider;
101
+ const model = cons?.model;
102
+ const types = cons?.types;
103
+ const consolidation = { enabled };
104
+ if (provider !== void 0) consolidation.provider = provider;
105
+ if (model !== void 0) consolidation.model = model;
106
+ if (types !== void 0) consolidation.types = types;
107
+ return { consolidation };
108
+ }
109
+
110
+ // src/consolidate.ts
111
+ import { randomUUID } from "crypto";
112
+
113
+ // src/store.ts
114
+ var OBS_PREFIX = "memory:obs:";
115
+ var SESSIONS_KEY = "memory:sessions";
116
+ var INDEX_KEY = "memory:index";
117
+ var CONSOLIDATION_MISS_KEY = "memory:consolidation:miss";
118
+ function obsKey(id) {
119
+ return `${OBS_PREFIX}${id}`;
120
+ }
121
+ function getObservation(store, id) {
122
+ return store.getState(obsKey(id));
123
+ }
124
+ function setObservation(store, obs) {
125
+ store.setState(obsKey(obs.id), obs);
126
+ }
127
+ function clearObservation(store, id) {
128
+ store.setState(obsKey(id), null);
129
+ }
130
+ function getObservationIds(store) {
131
+ return store.getState(INDEX_KEY) ?? [];
132
+ }
133
+ function setObservationIds(store, ids) {
134
+ store.setState(INDEX_KEY, ids);
135
+ }
136
+ function getSessions(store) {
137
+ return store.getState(SESSIONS_KEY) ?? [];
138
+ }
139
+ function setSessions(store, sessions) {
140
+ store.setState(SESSIONS_KEY, sessions);
141
+ }
142
+ function bumpSession(store, sessionId, project, ts) {
143
+ const sessions = getSessions(store);
144
+ const existing = sessions.find((s) => s.id === sessionId);
145
+ if (existing) {
146
+ existing.count += 1;
147
+ if (ts > existing.lastTs) existing.lastTs = ts;
148
+ } else {
149
+ sessions.push({ id: sessionId, project, count: 1, lastTs: ts });
150
+ }
151
+ setSessions(store, sessions);
152
+ }
153
+ function decrementSession(store, sessionId) {
154
+ const sessions = getSessions(store);
155
+ const existing = sessions.find((s) => s.id === sessionId);
156
+ if (existing === void 0) return;
157
+ existing.count -= 1;
158
+ if (existing.count <= 0) {
159
+ setSessions(
160
+ store,
161
+ sessions.filter((s) => s.id !== sessionId)
162
+ );
163
+ } else {
164
+ setSessions(store, sessions);
165
+ }
166
+ }
167
+ function getConsolidationMisses(store) {
168
+ return store.getState(CONSOLIDATION_MISS_KEY) ?? [];
169
+ }
170
+ function appendConsolidationMiss(store, miss) {
171
+ const misses = getConsolidationMisses(store);
172
+ misses.push(miss);
173
+ store.setState(CONSOLIDATION_MISS_KEY, misses);
174
+ }
175
+
176
+ // src/types.ts
177
+ var MEMORY_TYPES = [
178
+ "pattern",
179
+ "preference",
180
+ "architecture",
181
+ "bug",
182
+ "workflow",
183
+ "fact",
184
+ "decision",
185
+ "lesson"
186
+ ];
187
+ var DEFAULT_IMPORTANCE = 0.5;
188
+
189
+ // src/consolidate.ts
190
+ var DEFAULT_CONSOLIDATE_LIMIT = 50;
191
+ var CONSOLIDATION_SYSTEM_PROMPT = [
192
+ "You are consolidating a developer's cross-session memory observations.",
193
+ "Synthesize the given observations into ONE concise derived lesson.",
194
+ "State the general insight; do not merely list the inputs.",
195
+ "Output only the lesson text (no preamble, no JSON, no formatting)."
196
+ ].join(" ");
197
+ async function runConsolidation(deps, opts) {
198
+ const { store, model, config, projectId, indexDerived } = deps;
199
+ const cons = config.consolidation;
200
+ const provider = cons?.provider;
201
+ if (!cons?.enabled || !provider) {
202
+ appendConsolidationMiss(store, { ts: Date.now(), reason: "no-provider" });
203
+ return { ok: false, reason: "no-provider", logged: true };
204
+ }
205
+ if (model === void 0 || !cons.model) {
206
+ appendConsolidationMiss(store, {
207
+ ts: Date.now(),
208
+ reason: "model-unavailable",
209
+ provider
210
+ });
211
+ return { ok: false, reason: "model-unavailable", logged: true };
212
+ }
213
+ const modelId = cons.model;
214
+ const candidates = gatherCandidates(
215
+ store,
216
+ opts?.types ?? cons.types,
217
+ opts?.limit ?? DEFAULT_CONSOLIDATE_LIMIT
218
+ );
219
+ if (candidates.length === 0) {
220
+ appendConsolidationMiss(store, {
221
+ ts: Date.now(),
222
+ reason: "no-candidates",
223
+ provider
224
+ });
225
+ return { ok: false, reason: "no-candidates", logged: true };
226
+ }
227
+ const result = await model.complete({
228
+ system: CONSOLIDATION_SYSTEM_PROMPT,
229
+ prompt: serializeCandidates(candidates),
230
+ provider,
231
+ model: modelId,
232
+ tier: "consolidate"
233
+ });
234
+ if (result === null || !result.ok) {
235
+ appendConsolidationMiss(store, {
236
+ ts: Date.now(),
237
+ reason: "model-unavailable",
238
+ provider
239
+ });
240
+ return { ok: false, reason: "model-unavailable", logged: true };
241
+ }
242
+ const text = result.text.trim();
243
+ if (text.length === 0) {
244
+ appendConsolidationMiss(store, {
245
+ ts: Date.now(),
246
+ reason: "no-candidates",
247
+ provider
248
+ });
249
+ return { ok: false, reason: "no-candidates", logged: true };
250
+ }
251
+ const provenance = candidates.map((c) => c.id);
252
+ const ts = Date.now();
253
+ const lesson = {
254
+ id: randomUUID(),
255
+ type: "lesson",
256
+ content: text,
257
+ project: projectId,
258
+ sessionId: null,
259
+ ts,
260
+ lastAccessTs: ts,
261
+ importance: DEFAULT_IMPORTANCE,
262
+ concepts: dedupeConcepts(candidates),
263
+ files: [],
264
+ source: "explicit",
265
+ provenance
266
+ };
267
+ await indexDerived(lesson);
268
+ return { ok: true, lessons: [lesson], from: provenance };
269
+ }
270
+ function gatherCandidates(store, types, limit) {
271
+ const ids = getObservationIds(store);
272
+ const out = [];
273
+ for (let i = ids.length - 1; i >= 0 && out.length < limit; i--) {
274
+ const id = ids[i];
275
+ if (id === void 0) break;
276
+ const obs = getObservation(store, id);
277
+ if (obs === null) continue;
278
+ if (obs.type === "lesson") continue;
279
+ if (types !== void 0 && !types.includes(obs.type)) continue;
280
+ out.push(obs);
281
+ }
282
+ return out;
283
+ }
284
+ function serializeCandidates(candidates) {
285
+ return candidates.map((c, i) => `[${i + 1}] (type: ${c.type}, ts: ${c.ts}) ${c.content}`).join("\n\n");
286
+ }
287
+ function dedupeConcepts(candidates) {
288
+ const set = /* @__PURE__ */ new Set();
289
+ for (const c of candidates) {
290
+ for (const concept of c.concepts) set.add(concept);
291
+ }
292
+ return [...set];
293
+ }
294
+
295
+ // src/engine.ts
296
+ import { randomUUID as randomUUID2 } from "crypto";
297
+
298
+ // src/recall.ts
299
+ import { fuseRrf } from "@noir-ai/context";
300
+ var MEMORY_SOURCE = "memory";
301
+ var DEFAULT_RECALL_LIMIT = 10;
302
+ var MIN_ENTITY_LEN = 3;
303
+ var ENTITY_BOOST_PER_MATCH = 0.1;
304
+ var STOPWORDS = /* @__PURE__ */ new Set([
305
+ "the",
306
+ "and",
307
+ "for",
308
+ "with",
309
+ "that",
310
+ "this",
311
+ "from",
312
+ "have",
313
+ "your",
314
+ "was",
315
+ "are",
316
+ "can",
317
+ "how",
318
+ "when",
319
+ "what",
320
+ "where",
321
+ "why",
322
+ "who",
323
+ "use",
324
+ "using",
325
+ "used",
326
+ "into",
327
+ "but",
328
+ "not",
329
+ "you",
330
+ "all",
331
+ "any",
332
+ "get",
333
+ "set",
334
+ "put",
335
+ "new",
336
+ "one",
337
+ "two",
338
+ "via",
339
+ "our",
340
+ "its",
341
+ "etc"
342
+ ]);
343
+ function extractEntities(query) {
344
+ const set = /* @__PURE__ */ new Set();
345
+ const words = query.match(/\S+/g);
346
+ if (words === null) return [];
347
+ for (const token of words) {
348
+ if (/[/.:]/.test(token)) {
349
+ const lower = token.toLowerCase();
350
+ if (lower.length >= MIN_ENTITY_LEN) set.add(lower);
351
+ }
352
+ for (const piece of splitIdentifier(token)) {
353
+ if (piece.length < MIN_ENTITY_LEN) continue;
354
+ if (STOPWORDS.has(piece)) continue;
355
+ set.add(piece);
356
+ }
357
+ }
358
+ return [...set];
359
+ }
360
+ function splitIdentifier(token) {
361
+ const words = token.match(/[A-Za-z0-9]+/g);
362
+ if (words === null) return [];
363
+ const out = [];
364
+ for (const word of words) {
365
+ const spaced = word.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2");
366
+ for (const piece of spaced.split(/\s+/)) {
367
+ if (piece.length > 0) out.push(piece.toLowerCase());
368
+ }
369
+ }
370
+ return out;
371
+ }
372
+ function entityBoostForObs(obs, entities) {
373
+ if (entities.length === 0) return 0;
374
+ const contentLower = obs.content.toLowerCase();
375
+ let matched = 0;
376
+ for (const entity of entities) {
377
+ if (contentLower.includes(entity)) {
378
+ matched += 1;
379
+ continue;
380
+ }
381
+ if (obs.concepts.some((c) => c.toLowerCase() === entity)) {
382
+ matched += 1;
383
+ continue;
384
+ }
385
+ if (obs.files.some((f) => f.toLowerCase().includes(entity))) {
386
+ matched += 1;
387
+ }
388
+ }
389
+ return matched * ENTITY_BOOST_PER_MATCH;
390
+ }
391
+ async function recallMemory(deps, query, opts) {
392
+ const store = deps.store;
393
+ const limit = opts?.limit ?? DEFAULT_RECALL_LIMIT;
394
+ let ftsHits = [];
395
+ let ftsFailed = false;
396
+ try {
397
+ ftsHits = store.searchFt(query, { limit, source: MEMORY_SOURCE });
398
+ } catch {
399
+ ftsFailed = true;
400
+ }
401
+ let knnHits = [];
402
+ let knnFailed = false;
403
+ try {
404
+ const qvec = await deps.embed(query);
405
+ try {
406
+ knnHits = store.knn(qvec, { limit, source: MEMORY_SOURCE });
407
+ } catch {
408
+ knnFailed = true;
409
+ }
410
+ } catch {
411
+ knnFailed = true;
412
+ }
413
+ const mode = knnFailed ? "bm25-only" : "hybrid";
414
+ const degraded = knnFailed || ftsFailed;
415
+ const fused = fuseRrf(ftsHits, knnHits);
416
+ const entities = extractEntities(query);
417
+ const hits = [];
418
+ for (const row of fused) {
419
+ const obs = getObservation(store, row.id);
420
+ if (obs === null) continue;
421
+ if (opts?.type !== void 0 && obs.type !== opts.type) continue;
422
+ if (opts?.sessionId !== void 0 && obs.sessionId !== opts.sessionId) continue;
423
+ hits.push(toMemoryHit(obs, row.score + entityBoostForObs(obs, entities)));
424
+ }
425
+ hits.sort((a, b) => b.score - a.score);
426
+ return { hits: hits.slice(0, limit), degraded, mode };
427
+ }
428
+ function toMemoryHit(obs, score) {
429
+ return {
430
+ id: obs.id,
431
+ type: obs.type,
432
+ content: obs.content,
433
+ score,
434
+ concepts: obs.concepts,
435
+ files: obs.files,
436
+ ts: obs.ts,
437
+ importance: obs.importance,
438
+ source: obs.source
439
+ };
440
+ }
441
+
442
+ // src/engine.ts
443
+ var MEMORY_SOURCE2 = "memory";
444
+ var DEFAULT_SEARCH_LIMIT = 10;
445
+ var DEFAULT_TYPE = "fact";
446
+ var MemoryEngineImpl = class {
447
+ /** The daemon's single-writer store handle (possibly read-only). */
448
+ store;
449
+ /** Project root (stored for symmetry / future path normalization). */
450
+ root;
451
+ /** Canonical project identifier (NEVER a filesystem path — D6). */
452
+ projectId;
453
+ /**
454
+ * Persistent degradation flag (read-only store). Mutating ops throw a clear
455
+ * error upfront instead of letting the first write fail mid-run; reads
456
+ * (recall/search/sessions/status) keep working.
457
+ */
458
+ degraded;
459
+ embed;
460
+ model;
461
+ config;
462
+ // Single-flight serialization of ASYNC mutating ops (save / consolidate).
463
+ // `forget` is sync + atomic (see file header) and bypasses this chain.
464
+ chain = Promise.resolve();
465
+ constructor(opts) {
466
+ this.store = opts.store;
467
+ this.root = opts.root;
468
+ this.projectId = opts.projectId;
469
+ this.embed = opts.embed;
470
+ this.model = opts.model;
471
+ this.config = opts.config ?? {};
472
+ this.degraded = opts.storeDegraded === true;
473
+ }
474
+ // -------------------------------------------------------------------------
475
+ // save
476
+ // -------------------------------------------------------------------------
477
+ /** @inheritDoc MemoryEngine.save */
478
+ save(input) {
479
+ return this.serialized(() => this.saveInternal(input));
480
+ }
481
+ async saveInternal(input) {
482
+ this.assertNotDegraded("save");
483
+ const ts = Date.now();
484
+ const observation = {
485
+ id: randomUUID2(),
486
+ type: input.type ?? DEFAULT_TYPE,
487
+ content: input.content,
488
+ project: this.projectId,
489
+ sessionId: input.sessionId ?? null,
490
+ ts,
491
+ lastAccessTs: ts,
492
+ importance: input.importance ?? DEFAULT_IMPORTANCE,
493
+ concepts: input.concepts ?? [],
494
+ files: input.files ?? [],
495
+ source: "explicit"
496
+ };
497
+ const vec = await this.embedBestEffort(observation.content);
498
+ this.indexObservation(observation, vec);
499
+ return observation;
500
+ }
501
+ /**
502
+ * Write one observation to ALL three indexes in a single synchronous block:
503
+ * FTS5 (`indexDoc`, content searchable) + sqlite-vec (`upsertVec`, best-effort
504
+ * — skipped if the embedder or vec0 is unavailable, F8-style) + the
505
+ * authoritative KV row (`memory:obs:<id>`) + the id index + the sessions
506
+ * rollup. The `docs.meta` payload is the denormalized search projection
507
+ * (Observation minus content); the KV row is the source of truth (R4).
508
+ */
509
+ indexObservation(observation, vec) {
510
+ this.store.indexDoc({
511
+ id: observation.id,
512
+ source: MEMORY_SOURCE2,
513
+ content: observation.content,
514
+ meta: obsMeta(observation)
515
+ });
516
+ if (vec !== null) {
517
+ try {
518
+ this.store.upsertVec(observation.id, vec, { source: MEMORY_SOURCE2 });
519
+ } catch {
520
+ }
521
+ }
522
+ setObservation(this.store, observation);
523
+ const ids = getObservationIds(this.store);
524
+ if (!ids.includes(observation.id)) {
525
+ ids.push(observation.id);
526
+ setObservationIds(this.store, ids);
527
+ }
528
+ if (observation.sessionId !== null) {
529
+ bumpSession(this.store, observation.sessionId, observation.project, observation.ts);
530
+ }
531
+ }
532
+ // -------------------------------------------------------------------------
533
+ // get (extra public method — hydrate the FULL row from KV, DS-9)
534
+ // -------------------------------------------------------------------------
535
+ /**
536
+ * Hydrate the full {@link Observation} for `id` from the authoritative KV row.
537
+ * Returns `null` for an unknown / forgotten id. This is the only correct way
538
+ * to read an observation's complete `content` (DS-9).
539
+ */
540
+ get(id) {
541
+ return getObservation(this.store, id);
542
+ }
543
+ // -------------------------------------------------------------------------
544
+ // recall (t3: hybrid BM25 ∪ kNN + RRF + entity-boost — see recall.ts)
545
+ // -------------------------------------------------------------------------
546
+ /** @inheritDoc MemoryEngine.recall */
547
+ async recall(query, opts) {
548
+ const { hits } = await recallMemory({ store: this.store, embed: this.embed }, query, opts);
549
+ return hits;
550
+ }
551
+ // -------------------------------------------------------------------------
552
+ // search (BM25-only instant path — final design, DS-7)
553
+ // -------------------------------------------------------------------------
554
+ /** @inheritDoc MemoryEngine.search */
555
+ async search(query, opts) {
556
+ const limit = opts?.limit ?? DEFAULT_SEARCH_LIMIT;
557
+ let ftsHits = [];
558
+ try {
559
+ ftsHits = this.store.searchFt(query, { source: MEMORY_SOURCE2, limit });
560
+ } catch {
561
+ return [];
562
+ }
563
+ return this.hydrateHits(
564
+ ftsHits.map((h) => ({ id: h.id, score: h.score })),
565
+ void 0
566
+ );
567
+ }
568
+ /**
569
+ * Hydrate a ranked id list into {@link MemoryHit}s from the authoritative KV
570
+ * row, applying the optional `type` / `sessionId` filters. Each hit carries
571
+ * the FULL `content` (DS-9). A ranked id with no KV row (a stale vec-only hit
572
+ * whose row was forgotten) is dropped — never emitted with partial data.
573
+ */
574
+ hydrateHits(scored, opts) {
575
+ const out = [];
576
+ for (const row of scored) {
577
+ const obs = getObservation(this.store, row.id);
578
+ if (obs === null) continue;
579
+ if (opts?.type !== void 0 && obs.type !== opts.type) continue;
580
+ if (opts?.sessionId !== void 0 && obs.sessionId !== opts.sessionId) continue;
581
+ out.push(toMemoryHit2(obs, row.score));
582
+ }
583
+ return out;
584
+ }
585
+ // -------------------------------------------------------------------------
586
+ // sessions
587
+ // -------------------------------------------------------------------------
588
+ /** @inheritDoc MemoryEngine.sessions */
589
+ sessions() {
590
+ return getSessions(this.store);
591
+ }
592
+ // -------------------------------------------------------------------------
593
+ // forget (synchronous + atomic — see file header)
594
+ // -------------------------------------------------------------------------
595
+ /** @inheritDoc MemoryEngine.forget */
596
+ forget(ids) {
597
+ this.assertNotDegraded("forget");
598
+ let deleted = 0;
599
+ const idSet = new Set(ids);
600
+ for (const id of ids) {
601
+ const obs = getObservation(this.store, id);
602
+ if (obs === null) continue;
603
+ this.store.deleteDoc(id);
604
+ try {
605
+ this.store.deleteVec(id);
606
+ } catch {
607
+ }
608
+ clearObservation(this.store, id);
609
+ if (obs.sessionId !== null) {
610
+ decrementSession(this.store, obs.sessionId);
611
+ }
612
+ deleted += 1;
613
+ }
614
+ if (deleted > 0) {
615
+ const remaining = getObservationIds(this.store).filter((i) => !idSet.has(i));
616
+ setObservationIds(this.store, remaining);
617
+ }
618
+ return { deleted, ids };
619
+ }
620
+ // -------------------------------------------------------------------------
621
+ // consolidate (explicit, provider-gated — DS-6)
622
+ // -------------------------------------------------------------------------
623
+ /** @inheritDoc MemoryEngine.consolidate */
624
+ consolidate(opts) {
625
+ return this.serialized(() => this.consolidateInternal(opts));
626
+ }
627
+ async consolidateInternal(opts) {
628
+ return runConsolidation(
629
+ {
630
+ store: this.store,
631
+ model: this.model,
632
+ config: this.config,
633
+ projectId: this.projectId,
634
+ indexDerived: async (obs) => {
635
+ const vec = await this.embedBestEffort(obs.content);
636
+ this.indexObservation(obs, vec);
637
+ }
638
+ },
639
+ opts
640
+ );
641
+ }
642
+ // -------------------------------------------------------------------------
643
+ // status
644
+ // -------------------------------------------------------------------------
645
+ /** @inheritDoc MemoryEngine.status */
646
+ status() {
647
+ return {
648
+ ok: true,
649
+ projectId: this.store.projectId,
650
+ observations: getObservationIds(this.store).length,
651
+ degraded: this.degraded
652
+ };
653
+ }
654
+ // -------------------------------------------------------------------------
655
+ // shared internals
656
+ // -------------------------------------------------------------------------
657
+ /**
658
+ * Best-effort embedding: returns the vec, or `null` if the embedder is
659
+ * unavailable (`kind:'none'`, native load failure, provider error). A `null`
660
+ * vec skips `upsertVec` so the save still succeeds — the row is BM25-searchable
661
+ * + hydrated from KV (F8-style degradation).
662
+ */
663
+ async embedBestEffort(content) {
664
+ try {
665
+ return await this.embed(content);
666
+ } catch {
667
+ return null;
668
+ }
669
+ }
670
+ /** Throw a clear, typed error when the store handle is read-only. */
671
+ assertNotDegraded(op) {
672
+ if (this.degraded) {
673
+ throw new Error(`memory ${op} refused: store is read-only (daemon down)`);
674
+ }
675
+ }
676
+ /**
677
+ * Single-flight serialization of async mutating ops (mirrors the context
678
+ * indexer's `serialized`). Forces `work` to run strictly after the previous
679
+ * queued op completes. A failed op does NOT poison the queue — the chain
680
+ * advances regardless, and the caller observes the real outcome via `result`.
681
+ */
682
+ serialized(work) {
683
+ const result = this.chain.then(work);
684
+ this.chain = result.then(
685
+ () => void 0,
686
+ () => void 0
687
+ );
688
+ return result;
689
+ }
690
+ };
691
+ function createMemoryEngine(opts) {
692
+ return new MemoryEngineImpl(opts);
693
+ }
694
+ function obsMeta(obs) {
695
+ const meta = {
696
+ type: obs.type,
697
+ project: obs.project,
698
+ sessionId: obs.sessionId,
699
+ ts: obs.ts,
700
+ lastAccessTs: obs.lastAccessTs,
701
+ importance: obs.importance,
702
+ concepts: obs.concepts,
703
+ files: obs.files,
704
+ source: obs.source
705
+ };
706
+ if (obs.provenance !== void 0) meta.provenance = obs.provenance;
707
+ return meta;
708
+ }
709
+ function toMemoryHit2(obs, score) {
710
+ return {
711
+ id: obs.id,
712
+ type: obs.type,
713
+ content: obs.content,
714
+ score,
715
+ concepts: obs.concepts,
716
+ files: obs.files,
717
+ ts: obs.ts,
718
+ importance: obs.importance,
719
+ source: obs.source
720
+ };
721
+ }
722
+ export {
723
+ CAPTURE_HOOKS,
724
+ CONSOLIDATION_MISS_KEY,
725
+ CONSOLIDATION_SYSTEM_PROMPT,
726
+ DEFAULT_CAPTURE_HOOKS,
727
+ DEFAULT_CAPTURE_POLICY,
728
+ DEFAULT_CONSOLIDATE_LIMIT,
729
+ DEFAULT_IMPORTANCE,
730
+ INDEX_KEY,
731
+ MEMORY_TYPES,
732
+ MemoryEngineImpl,
733
+ OBS_PREFIX,
734
+ SESSIONS_KEY,
735
+ appendConsolidationMiss,
736
+ buildContent,
737
+ bumpSession,
738
+ captureSource,
739
+ clearObservation,
740
+ createMemoryEngine,
741
+ decrementSession,
742
+ dedupeConcepts,
743
+ describeToolCall,
744
+ extractEntities,
745
+ extractFiles,
746
+ gatherCandidates,
747
+ getConsolidationMisses,
748
+ getObservation,
749
+ getObservationIds,
750
+ getSessions,
751
+ inferType,
752
+ obsKey,
753
+ recallMemory,
754
+ resolveMemoryConfig,
755
+ runConsolidation,
756
+ serializeCandidates,
757
+ setObservation,
758
+ setObservationIds,
759
+ setSessions,
760
+ toSaveInput
761
+ };
762
+ //# sourceMappingURL=index.js.map