@seanhogg/builderforce-memory 2026.9.34 → 2026.10.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.
Files changed (97) hide show
  1. package/dist/errors/SSMError.d.ts +1 -1
  2. package/dist/errors/SSMError.d.ts.map +1 -1
  3. package/dist/errors/SSMError.js.map +1 -1
  4. package/dist/handoff/index.d.ts +5 -5
  5. package/dist/handoff/index.js +5 -5
  6. package/dist/handoff/note.d.ts +1 -1
  7. package/dist/handoff/note.js +1 -1
  8. package/dist/handoff/pack.d.ts +13 -4
  9. package/dist/handoff/pack.d.ts.map +1 -1
  10. package/dist/handoff/pack.js +31 -10
  11. package/dist/handoff/pack.js.map +1 -1
  12. package/dist/memory/MemoryStore.d.ts +120 -181
  13. package/dist/memory/MemoryStore.d.ts.map +1 -1
  14. package/dist/memory/MemoryStore.js +201 -389
  15. package/dist/memory/MemoryStore.js.map +1 -1
  16. package/dist/memory/SemanticRecall.d.ts +64 -0
  17. package/dist/memory/SemanticRecall.d.ts.map +1 -0
  18. package/dist/memory/SemanticRecall.js +188 -0
  19. package/dist/memory/SemanticRecall.js.map +1 -0
  20. package/dist/memory/records.d.ts +26 -0
  21. package/dist/memory/records.d.ts.map +1 -0
  22. package/dist/memory/records.js +71 -0
  23. package/dist/memory/records.js.map +1 -0
  24. package/dist/memory/ttlSweeper.d.ts +12 -0
  25. package/dist/memory/ttlSweeper.d.ts.map +1 -0
  26. package/dist/memory/ttlSweeper.js +28 -0
  27. package/dist/memory/ttlSweeper.js.map +1 -0
  28. package/dist/memory/types.d.ts +142 -0
  29. package/dist/memory/types.d.ts.map +1 -0
  30. package/dist/memory/types.js +6 -0
  31. package/dist/memory/types.js.map +1 -0
  32. package/dist/storage/ScopedFactRepository.d.ts +156 -0
  33. package/dist/storage/ScopedFactRepository.d.ts.map +1 -0
  34. package/dist/storage/ScopedFactRepository.js +278 -0
  35. package/dist/storage/ScopedFactRepository.js.map +1 -0
  36. package/dist/storage/factImport.d.ts +21 -0
  37. package/dist/storage/factImport.d.ts.map +1 -0
  38. package/dist/storage/factImport.js +61 -0
  39. package/dist/storage/factImport.js.map +1 -0
  40. package/dist/storage/factMutations.d.ts +46 -0
  41. package/dist/storage/factMutations.d.ts.map +1 -0
  42. package/dist/storage/factMutations.js +73 -0
  43. package/dist/storage/factMutations.js.map +1 -0
  44. package/dist/storage/factQueries.d.ts +16 -0
  45. package/dist/storage/factQueries.d.ts.map +1 -0
  46. package/dist/storage/factQueries.js +36 -0
  47. package/dist/storage/factQueries.js.map +1 -0
  48. package/dist/storage/factRetention.d.ts +30 -0
  49. package/dist/storage/factRetention.d.ts.map +1 -0
  50. package/dist/storage/factRetention.js +65 -0
  51. package/dist/storage/factRetention.js.map +1 -0
  52. package/dist/storage/fileLock.d.ts +56 -0
  53. package/dist/storage/fileLock.d.ts.map +1 -0
  54. package/dist/storage/fileLock.js +138 -0
  55. package/dist/storage/fileLock.js.map +1 -0
  56. package/dist/storage/fileTransaction.d.ts +33 -0
  57. package/dist/storage/fileTransaction.d.ts.map +1 -0
  58. package/dist/storage/fileTransaction.js +108 -0
  59. package/dist/storage/fileTransaction.js.map +1 -0
  60. package/dist/storage/idbAdapter.d.ts +95 -0
  61. package/dist/storage/idbAdapter.d.ts.map +1 -0
  62. package/dist/storage/idbAdapter.js +311 -0
  63. package/dist/storage/idbAdapter.js.map +1 -0
  64. package/dist/storage/nodeFileAdapter.d.ts +127 -0
  65. package/dist/storage/nodeFileAdapter.d.ts.map +1 -0
  66. package/dist/storage/nodeFileAdapter.js +275 -0
  67. package/dist/storage/nodeFileAdapter.js.map +1 -0
  68. package/dist/storage/syncJournal.d.ts +19 -0
  69. package/dist/storage/syncJournal.d.ts.map +1 -0
  70. package/dist/storage/syncJournal.js +46 -0
  71. package/dist/storage/syncJournal.js.map +1 -0
  72. package/dist/storage/types.d.ts +207 -0
  73. package/dist/storage/types.d.ts.map +1 -0
  74. package/dist/storage/types.js +90 -0
  75. package/dist/storage/types.js.map +1 -0
  76. package/package.json +3 -3
  77. package/src/errors/SSMError.ts +1 -0
  78. package/src/handoff/handoff.test.ts +52 -2
  79. package/src/handoff/index.ts +5 -5
  80. package/src/handoff/note.ts +1 -1
  81. package/src/handoff/pack.ts +32 -9
  82. package/src/memory/MemoryStore.ts +245 -525
  83. package/src/memory/SemanticRecall.ts +204 -0
  84. package/src/memory/records.ts +79 -0
  85. package/src/memory/ttlSweeper.ts +29 -0
  86. package/src/memory/types.ts +149 -0
  87. package/src/storage/ScopedFactRepository.ts +373 -0
  88. package/src/storage/factImport.ts +73 -0
  89. package/src/storage/factMutations.ts +106 -0
  90. package/src/storage/factQueries.ts +41 -0
  91. package/src/storage/factRetention.ts +82 -0
  92. package/src/storage/fileLock.ts +162 -0
  93. package/src/storage/fileTransaction.ts +116 -0
  94. package/src/storage/idbAdapter.ts +373 -0
  95. package/src/storage/nodeFileAdapter.ts +331 -0
  96. package/src/storage/syncJournal.ts +47 -0
  97. package/src/storage/types.ts +262 -0
@@ -0,0 +1,204 @@
1
+ /**
2
+ * memory/SemanticRecall.ts – The query side of {@link MemoryStore}: semantic
3
+ * (SSM-embedding cosine, Jaccard fallback) and hybrid (RRF + MMR) recall over
4
+ * an already-loaded fact list, plus the derived caches that make it cheap:
5
+ *
6
+ * - an LRU content→embedding cache (EVM-8);
7
+ * - a persistent dense vector store keyed by `key@seq` and the HNSW index
8
+ * built from it, reused until the fact set changes (EVM-1 / EVM-1b).
9
+ *
10
+ * Every cache is model-specific and is dropped when the runtime identity
11
+ * changes, or when the owning store calls {@link SemanticRecall.invalidate}
12
+ * (clear, cross-context write).
13
+ */
14
+
15
+ import { tokenize, jaccardSimilarity } from '../similarity/index.js';
16
+ import { hybridRetrieve, denseSearch, HnswIndex, type RetrievalCandidate, type HybridRetrieveOptions } from '../retrieval/index.js';
17
+ import type { MemoryEntry, RankedRecall, ScoredMemory, SSMRuntimeRef } from './types.js';
18
+
19
+ export interface SemanticRecallOptions {
20
+ /** Fact count at/above which dense search uses the HNSW index. */
21
+ annThreshold: number;
22
+ /** Max content→embedding vectors held in the LRU cache. */
23
+ embedCacheMax: number;
24
+ /** Hits scoring below this are dropped (0 = keep everything). */
25
+ minRelevance: number;
26
+ }
27
+
28
+ interface DenseView {
29
+ byKey: Map<string, MemoryEntry>;
30
+ items: Array<{ id: string; vector: Float32Array }>;
31
+ index: HnswIndex | null;
32
+ }
33
+
34
+ export class SemanticRecall {
35
+ private readonly _embedCache = new Map<string, Float32Array>();
36
+ private _embedRuntime: unknown = null;
37
+ private readonly _denseVectors = new Map<string, Float32Array>();
38
+ private _denseIndex: HnswIndex | null = null;
39
+ /** Signature of the fact set the current index was built from. */
40
+ private _denseSig = '';
41
+ private _denseRuntime: unknown = null;
42
+
43
+ constructor(private readonly _opts: SemanticRecallOptions) {}
44
+
45
+ /** Drops every derived cache (the fact set changed underneath us). */
46
+ invalidate(): void {
47
+ this._embedCache.clear();
48
+ this.resetDense();
49
+ }
50
+
51
+ /** Drops the dense vectors + index but keeps content embeddings. */
52
+ resetDense(): void {
53
+ this._denseVectors.clear();
54
+ this._denseIndex = null;
55
+ this._denseSig = '';
56
+ }
57
+
58
+ /**
59
+ * Top-K entries of `all` by real 0..1 relevance: SSM-embedding cosine when
60
+ * `runtime.embed` works, Jaccard word overlap otherwise.
61
+ */
62
+ async similar(all: MemoryEntry[], query: string, topK: number, runtime?: SSMRuntimeRef): Promise<ScoredMemory[]> {
63
+ if (all.length === 0) return [];
64
+ if (runtime != null && typeof runtime.embed === 'function') {
65
+ const queryVec = await this._embed(runtime, query);
66
+ if (queryVec) {
67
+ // EVM-1b: sync the persistent vector store + index (embeds only the
68
+ // delta), then query it — no per-call re-embed of every fact.
69
+ const dense = await this._syncDense(runtime, all);
70
+ if (dense) {
71
+ const hits = dense.index
72
+ ? dense.index.search(queryVec, topK)
73
+ : denseSearch(queryVec, dense.items, topK, this._opts.annThreshold);
74
+ return this._floor(hits
75
+ .map((h) => { const entry = dense.byKey.get(h.id); return entry ? { entry, score: h.score } : null; })
76
+ .filter((e): e is ScoredMemory => !!e));
77
+ }
78
+ }
79
+ // Any embedding failure falls through to the Jaccard path below.
80
+ }
81
+ const queryTokens = new Set(tokenize(query));
82
+ const scored = all.map((entry) => ({ entry, score: jaccardSimilarity(queryTokens, new Set(tokenize(entry.content))) }));
83
+ scored.sort((a, b) => b.score - a.score);
84
+ return this._floor(scored).slice(0, topK);
85
+ }
86
+
87
+ /**
88
+ * Hybrid recall: dense + BM25 fused by the shared `hybridRetrieve` primitive,
89
+ * with an honest `method` — `'embedding'` only when the query AND at least
90
+ * one candidate really embedded.
91
+ */
92
+ async ranked(
93
+ all: MemoryEntry[],
94
+ query: string,
95
+ topK: number,
96
+ runtime?: SSMRuntimeRef,
97
+ opts?: HybridRetrieveOptions,
98
+ ): Promise<RankedRecall> {
99
+ if (all.length === 0) return { hits: [], method: 'lexical' };
100
+ const canEmbed = runtime != null && typeof runtime.embed === 'function';
101
+ const queryVec = canEmbed ? (await this._embed(runtime, query)) ?? undefined : undefined;
102
+ const candidates: RetrievalCandidate[] = [];
103
+ for (const entry of all) {
104
+ const vector = queryVec ? (await this._embed(runtime, entry.content)) ?? undefined : undefined;
105
+ candidates.push({ id: entry.key, text: entry.content, vector });
106
+ }
107
+ const dense = !!queryVec && candidates.some((c) => !!c.vector);
108
+ const hits = hybridRetrieve({ text: query, vector: queryVec }, candidates, { topK, annThreshold: this._opts.annThreshold, ...opts });
109
+ const byKey = new Map(all.map((e) => [e.key, e]));
110
+ return {
111
+ hits: this._floor(hits
112
+ .map((h) => { const entry = byKey.get(h.id); return entry ? { entry, score: h.score } : null; })
113
+ .filter((h): h is ScoredMemory => !!h)),
114
+ method: dense ? 'embedding' : 'lexical',
115
+ };
116
+ }
117
+
118
+ private _floor(hits: ScoredMemory[]): ScoredMemory[] {
119
+ const min = this._opts.minRelevance;
120
+ return min > 0 ? hits.filter((h) => h.score >= min) : hits;
121
+ }
122
+
123
+ /**
124
+ * Cached embedding for `text`, computed via `runtime.embed()` on a miss.
125
+ * `null` (never throws) when embedding is unavailable, so callers fall back
126
+ * to lexical similarity.
127
+ */
128
+ private async _embed(runtime: SSMRuntimeRef, text: string): Promise<Float32Array | null> {
129
+ if (runtime !== this._embedRuntime) {
130
+ this._embedCache.clear();
131
+ this._embedRuntime = runtime;
132
+ }
133
+ const cached = this._embedCache.get(text);
134
+ if (cached) {
135
+ // LRU touch: re-insert so this entry becomes most-recently-used. (EVM-8)
136
+ this._embedCache.delete(text);
137
+ this._embedCache.set(text, cached);
138
+ return cached;
139
+ }
140
+ let vec: unknown;
141
+ try {
142
+ vec = await runtime.embed(text);
143
+ } catch {
144
+ // Embedding unavailable (no GPU, destroyed runtime, etc.) — the
145
+ // documented lexical fallback, reported to callers as `method`.
146
+ return null;
147
+ }
148
+ if (!(vec instanceof Float32Array) || vec.length === 0) return null;
149
+ if (this._embedCache.size >= this._opts.embedCacheMax) {
150
+ const lru = this._embedCache.keys().next().value;
151
+ if (lru !== undefined) this._embedCache.delete(lru);
152
+ }
153
+ this._embedCache.set(text, vec);
154
+ return vec;
155
+ }
156
+
157
+ /**
158
+ * Synchronise the dense vector store + HNSW index to `all`: embeds ONLY
159
+ * new/changed facts (keyed by `key@seq`), prunes removed ones, and rebuilds
160
+ * the index only when the fact set changed. `null` when any embedding is
161
+ * unavailable (caller falls back to lexical).
162
+ */
163
+ private async _syncDense(runtime: SSMRuntimeRef, all: MemoryEntry[]): Promise<DenseView | null> {
164
+ if (runtime !== this._denseRuntime) {
165
+ this.resetDense();
166
+ this._denseRuntime = runtime;
167
+ }
168
+ const vkeyOf = (e: MemoryEntry): string => `${e.key}@${e.seq ?? 0}`;
169
+ const currentVkeys = new Set<string>();
170
+ let sig = `${all.length}`;
171
+ for (const e of all) { const k = vkeyOf(e); currentVkeys.add(k); sig += '|' + k; }
172
+
173
+ const byKey = new Map<string, MemoryEntry>();
174
+ const items: Array<{ id: string; vector: Float32Array }> = [];
175
+ for (const entry of all) {
176
+ const vk = vkeyOf(entry);
177
+ let vec = this._denseVectors.get(vk);
178
+ if (!vec) {
179
+ const v = await this._embed(runtime, entry.content);
180
+ if (!v) return null;
181
+ vec = v;
182
+ this._denseVectors.set(vk, vec);
183
+ }
184
+ items.push({ id: entry.key, vector: vec });
185
+ byKey.set(entry.key, entry);
186
+ }
187
+ if (this._denseVectors.size > currentVkeys.size) {
188
+ for (const k of this._denseVectors.keys()) {
189
+ if (!currentVkeys.has(k)) this._denseVectors.delete(k);
190
+ }
191
+ }
192
+ if (all.length >= this._opts.annThreshold) {
193
+ if (sig !== this._denseSig || this._denseIndex === null) {
194
+ const index = new HnswIndex();
195
+ for (const it of items) index.add(it.id, it.vector);
196
+ this._denseIndex = index;
197
+ }
198
+ } else {
199
+ this._denseIndex = null;
200
+ }
201
+ this._denseSig = sig;
202
+ return { byKey, items, index: this._denseIndex };
203
+ }
204
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * memory/records.ts – Mapping between the public {@link MemoryEntry} shape and
3
+ * the durable {@link StoredFactRecord}, plus the TTL and ordering rules every
4
+ * MemoryStore read shares.
5
+ */
6
+
7
+ import type { RememberRecordInput } from '../storage/ScopedFactRepository.js';
8
+ import type { MemoryScope, StoredFactRecord } from '../storage/types.js';
9
+ import type { MemoryEntry, RememberOptions } from './types.js';
10
+
11
+ /** True once `ttlMs` has elapsed since the entry's (source) timestamp. */
12
+ export function isExpired(entry: { timestamp: number; ttlMs?: number }): boolean {
13
+ if (entry.ttlMs == null) return false;
14
+ return Date.now() > entry.timestamp + entry.ttlMs;
15
+ }
16
+
17
+ /**
18
+ * Newest first; same-millisecond ties break by the durable write sequence so
19
+ * ordering is deterministic (and survives restarts).
20
+ */
21
+ export function newestFirst(a: StoredFactRecord, b: StoredFactRecord): number {
22
+ return b.timestamp - a.timestamp || b.seq - a.seq;
23
+ }
24
+
25
+ /** Non-expired records, newest first, as public entries. */
26
+ export function liveEntries(records: StoredFactRecord[]): MemoryEntry[] {
27
+ return records.filter((r) => !isExpired(r)).sort(newestFirst).map(toEntry);
28
+ }
29
+
30
+ export function toEntry(record: StoredFactRecord): MemoryEntry {
31
+ return {
32
+ key : record.key,
33
+ content : record.content,
34
+ timestamp : record.timestamp,
35
+ seq : record.seq,
36
+ ttlMs : record.ttlMs,
37
+ type : record.type,
38
+ tags : record.tags,
39
+ importance : record.importance,
40
+ revision : record.revision,
41
+ opId : record.opId,
42
+ scope : { ...record.scope },
43
+ provenance : record.provenance,
44
+ pendingSync: record.pendingSync,
45
+ };
46
+ }
47
+
48
+ /** A `remember` call's options as a repository write (store default TTL applied). */
49
+ export function rememberInput(content: string, opts: RememberOptions | undefined, defaultTtlMs: number | undefined): RememberRecordInput {
50
+ return {
51
+ content,
52
+ timestamp : opts?.timestamp,
53
+ ttlMs : opts?.ttlMs ?? defaultTtlMs,
54
+ type : opts?.type,
55
+ tags : opts?.tags,
56
+ importance: opts?.importance,
57
+ provenance: opts?.provenance,
58
+ };
59
+ }
60
+
61
+ /** An exported entry as an import record for `scope` (id/revision reassigned by the repository). */
62
+ export function toImportRecord(e: MemoryEntry, scope: MemoryScope): StoredFactRecord {
63
+ return {
64
+ id : '',
65
+ key : e.key,
66
+ scope,
67
+ content : e.content,
68
+ timestamp : e.timestamp,
69
+ seq : e.seq ?? 0,
70
+ revision : 0,
71
+ opId : e.opId ?? '',
72
+ ttlMs : e.ttlMs,
73
+ type : e.type,
74
+ tags : e.tags,
75
+ importance : e.importance,
76
+ provenance : e.provenance,
77
+ pendingSync: false,
78
+ };
79
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * memory/ttlSweeper.ts – The background timer behind
3
+ * `MemoryStore.startTtlSweeper`: TTL is otherwise evaluated lazily on read, so
4
+ * an idle store would keep dead entries forever. (EVM-7)
5
+ */
6
+
7
+ export class TtlSweeper {
8
+ private _timer: ReturnType<typeof setInterval> | null = null;
9
+
10
+ /** Runs `sweep` every `intervalMs`. Idempotent while running. */
11
+ start(intervalMs: number, sweep: () => Promise<unknown>): void {
12
+ if (this._timer) return;
13
+ const timer = setInterval(() => { void sweep(); }, intervalMs);
14
+ // Don't keep a Node process alive just for the sweeper.
15
+ const maybeUnref = timer as unknown as { unref?: () => void };
16
+ /* istanbul ignore else -- unref exists in Node; absent in the browser */
17
+ if (typeof maybeUnref.unref === 'function') {
18
+ maybeUnref.unref();
19
+ }
20
+ this._timer = timer;
21
+ }
22
+
23
+ stop(): void {
24
+ if (this._timer) {
25
+ clearInterval(this._timer);
26
+ this._timer = null;
27
+ }
28
+ }
29
+ }
@@ -0,0 +1,149 @@
1
+ /**
2
+ * memory/types.ts – The public shapes of {@link MemoryStore}: entries, recall
3
+ * results, write options and store options.
4
+ */
5
+
6
+ import type { FactProvenance, FactStoragePort, MemoryScope } from '../storage/types.js';
7
+
8
+ export type FactType = 'text' | 'json' | 'number' | 'boolean';
9
+
10
+ export interface MemoryEntry {
11
+ key : string;
12
+ content : string;
13
+ timestamp : number;
14
+ /**
15
+ * Monotonic write sequence, used only to break `timestamp` ties so that
16
+ * ordering stays deterministic when several entries are written within the
17
+ * same millisecond (Date.now() resolution). Higher = written later.
18
+ * Optional for backward compatibility with entries persisted/imported
19
+ * before this field existed (those sort as seq 0).
20
+ */
21
+ seq? : number;
22
+ /** Optional time-to-live in milliseconds. */
23
+ ttlMs? : number;
24
+ /** Semantic type of the stored value. Default: 'text'. */
25
+ type? : FactType;
26
+ /** Tags for grouping and filtering. */
27
+ tags? : string[];
28
+ /** Importance weight in the range 0–1. Default: 0.5. */
29
+ importance?: number;
30
+ /**
31
+ * Durable revision of this record (the store generation that wrote it).
32
+ * Present on every record written through the repository; absent only on
33
+ * entries fabricated by callers for import.
34
+ */
35
+ revision? : number;
36
+ /** Durable identity of the operation that wrote this record. */
37
+ opId? : string;
38
+ /** Ownership partition this entry was recalled from. */
39
+ scope? : MemoryScope;
40
+ /** Where the fact came from; preserved across export/import (M06). */
41
+ provenance?: FactProvenance;
42
+ /** True while a sync acknowledgement for this record is outstanding. */
43
+ pendingSync?: boolean;
44
+ }
45
+
46
+ /**
47
+ * Which ranker produced a recall ordering. Matches the vocabulary the gateway's
48
+ * Evermind recall/validate surfaces already report, so a host can render one chip
49
+ * ("Semantic recall" / "Lexical recall (fallback)") regardless of which layer
50
+ * answered.
51
+ */
52
+ export type RecallMethod = 'embedding' | 'lexical';
53
+
54
+ /** A recalled entry with its relevance score (higher = closer). */
55
+ export interface ScoredMemory {
56
+ entry: MemoryEntry;
57
+ score: number;
58
+ }
59
+
60
+ /** A ranked recall: the hits plus the ranker that ordered them. */
61
+ export interface RankedRecall {
62
+ hits: ScoredMemory[];
63
+ method: RecallMethod;
64
+ }
65
+
66
+ export interface RememberOptions {
67
+ /** Override the store-level defaultTtlMs for this entry. */
68
+ ttlMs? : number;
69
+ type? : FactType;
70
+ tags? : string[];
71
+ importance?: number;
72
+ /** Provenance to record with the fact (surface, importer, source time). */
73
+ provenance?: FactProvenance;
74
+ /** Explicit source timestamp (import/replay). Default: now. */
75
+ timestamp? : number;
76
+ }
77
+
78
+ export interface MemoryStoreOptions {
79
+ /** IndexedDB database name. Default: 'ssmjs'. Ignored when `storage` is supplied. */
80
+ dbName? : string;
81
+ /** Key used for weight storage within the 'weights' object store. Default: 'ssmjs-weights'. */
82
+ weightsKey? : string;
83
+ /**
84
+ * IDBFactory to use instead of the global `indexedDB` (browser path only).
85
+ * fake-indexeddb is supported for tests, but it is IN-MEMORY: on Node.js,
86
+ * pass `storage` from `NodeFileStorageAdapter.open(...)` for durable facts.
87
+ */
88
+ idbFactory? : IDBFactory;
89
+ /**
90
+ * Durable storage port backing this store (M01). When omitted, an
91
+ * IndexedDB adapter over `dbName` is used.
92
+ */
93
+ storage? : FactStoragePort;
94
+ /**
95
+ * Explicit ownership partition for every fact in this store (M03).
96
+ * Default: the unbound scope — personal, never-connected memory.
97
+ */
98
+ scope? : MemoryScope;
99
+ /**
100
+ * Enable the sync journal, tombstones and pendingSync tracking (M04/M06).
101
+ * Unsynced facts are then never evicted by `maxEntries`. Default: false
102
+ * (legacy retention semantics — nothing to sync, nothing pending).
103
+ */
104
+ sync? : boolean;
105
+ /** Identity of this context for operation ids and cross-context notes. */
106
+ contextId? : string;
107
+ /**
108
+ * Default TTL applied to new entries when no per-entry ttlMs is provided.
109
+ * Entries with an expired TTL are filtered from recallAll() and related methods.
110
+ */
111
+ defaultTtlMs? : number;
112
+ /**
113
+ * Fact count at/above which semantic recall switches from an exact O(N)
114
+ * cosine scan to an O(log N) HNSW ANN index. Default 256 — small stores stay
115
+ * exact (simpler + faster), large stores stay fast. (EVM-1)
116
+ */
117
+ annThreshold? : number;
118
+ /**
119
+ * Hard cap on stored facts. When set, each write evicts the lowest-value
120
+ * SYNCED entries (expired first, then lowest importance, then oldest) so the
121
+ * store stays bounded. Facts with unsynced operations are never evicted
122
+ * (M06). Unset = unbounded (legacy). (EVM-7)
123
+ */
124
+ maxEntries? : number;
125
+ /**
126
+ * Max content→embedding vectors held in the LRU recall cache. Default 2000.
127
+ * (EVM-8 — bounded LRU instead of clear-on-overflow.)
128
+ */
129
+ embedCacheMax?: number;
130
+ /**
131
+ * Minimum relevance (0..1) a hit must reach to be recalled by
132
+ * `recallSimilarScored` / `recallRanked` (and their wrappers). Below it, a
133
+ * weak match returns nothing rather than noise. Default 0 (off): recall
134
+ * returns its best-ranked hits exactly as before this option existed. Opt in
135
+ * with e.g. 0.05 to drop zero-overlap matches (a query sharing no token with
136
+ * any fact scores 0 on the lexical path).
137
+ */
138
+ minRelevance? : number;
139
+ }
140
+
141
+ // Minimal interface to avoid importing SSMRuntime (circular dep)
142
+ export interface SaveLoadRuntime {
143
+ save(opts?: { storage: 'indexedDB'; key: string }): Promise<void>;
144
+ load(opts?: { key: string }): Promise<boolean>;
145
+ }
146
+
147
+ // Forward-declared to avoid circular dependency at import time
148
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
149
+ export type SSMRuntimeRef = any;