nemoris 0.1.0 → 0.1.2

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 (248) hide show
  1. package/.env.example +49 -49
  2. package/LICENSE +21 -21
  3. package/README.md +209 -209
  4. package/SECURITY.md +59 -119
  5. package/bin/nemoris +46 -46
  6. package/config/agents/agent.toml.example +28 -28
  7. package/config/agents/content.toml +23 -0
  8. package/config/agents/default.toml +22 -22
  9. package/config/agents/heartbeat.toml +35 -0
  10. package/config/agents/iris.toml +23 -0
  11. package/config/agents/lab.toml +23 -0
  12. package/config/agents/main.toml +45 -0
  13. package/config/agents/nemo.toml +21 -0
  14. package/config/agents/ops.toml +38 -0
  15. package/config/agents/orchestrator.toml +18 -18
  16. package/config/agents/revenue.toml +23 -0
  17. package/config/agents/testyboo.toml +19 -0
  18. package/config/delivery.toml +73 -73
  19. package/config/embeddings.toml +5 -5
  20. package/config/identity/content-purpose.md +11 -0
  21. package/config/identity/content-soul.md +45 -0
  22. package/config/identity/default-purpose.md +1 -1
  23. package/config/identity/default-soul.md +3 -3
  24. package/config/identity/heartbeat-purpose.md +9 -0
  25. package/config/identity/heartbeat-soul.md +16 -0
  26. package/config/identity/iris-purpose.md +17 -0
  27. package/config/identity/iris-soul.md +68 -0
  28. package/config/identity/lab-purpose.md +10 -0
  29. package/config/identity/lab-soul.md +38 -0
  30. package/config/identity/main-purpose.md +17 -0
  31. package/config/identity/main-soul.md +66 -0
  32. package/config/identity/main-user.md +22 -0
  33. package/config/identity/ops-purpose.md +9 -0
  34. package/config/identity/ops-soul.md +16 -0
  35. package/config/identity/orchestrator-purpose.md +1 -1
  36. package/config/identity/orchestrator-soul.md +1 -1
  37. package/config/identity/revenue-purpose.md +9 -0
  38. package/config/identity/revenue-soul.md +41 -0
  39. package/config/identity/testyboo-purpose.md +13 -0
  40. package/config/identity/testyboo-soul.md +20 -0
  41. package/config/improvement-targets.toml +15 -15
  42. package/config/jobs/heartbeat-check.toml +30 -30
  43. package/config/jobs/memory-rollup.toml +46 -46
  44. package/config/jobs/workspace-health.toml +63 -63
  45. package/config/mcp.toml +16 -16
  46. package/config/output-contracts.toml +17 -17
  47. package/config/peers.toml +32 -32
  48. package/config/peers.toml.example +32 -32
  49. package/config/policies/memory-default.toml +10 -10
  50. package/config/policies/memory-heartbeat.toml +5 -5
  51. package/config/policies/memory-ops.toml +10 -10
  52. package/config/policies/tools-heartbeat-minimal.toml +8 -8
  53. package/config/policies/tools-interactive-safe.toml +8 -8
  54. package/config/policies/tools-ops-bounded.toml +8 -8
  55. package/config/policies/tools-orchestrator.toml +7 -7
  56. package/config/providers/anthropic.toml +15 -15
  57. package/config/providers/ollama.toml +5 -5
  58. package/config/providers/openai-codex.toml +9 -9
  59. package/config/providers/openrouter.toml +5 -5
  60. package/config/router.toml +22 -22
  61. package/config/runtime.toml +114 -114
  62. package/config/skills/self-improvement.toml +15 -15
  63. package/config/skills/telegram-onboarding-spec.md +240 -240
  64. package/config/skills/workspace-monitor.toml +15 -15
  65. package/config/task-router.toml +42 -42
  66. package/install.sh +50 -50
  67. package/package.json +91 -90
  68. package/src/auth/auth-profiles.js +169 -169
  69. package/src/auth/openai-codex-oauth.js +285 -285
  70. package/src/battle.js +449 -449
  71. package/src/cli/help.js +265 -265
  72. package/src/cli/output-filter.js +49 -49
  73. package/src/cli/runtime-control.js +704 -704
  74. package/src/cli-main.js +2763 -2763
  75. package/src/cli.js +78 -78
  76. package/src/config/loader.js +332 -332
  77. package/src/config/schema-validator.js +214 -214
  78. package/src/config/toml-lite.js +8 -8
  79. package/src/daemon/action-handlers.js +71 -71
  80. package/src/daemon/healing-tick.js +87 -87
  81. package/src/daemon/health-probes.js +90 -90
  82. package/src/daemon/notifier.js +57 -57
  83. package/src/daemon/nurse.js +218 -218
  84. package/src/daemon/repair-log.js +106 -106
  85. package/src/daemon/rule-staging.js +90 -90
  86. package/src/daemon/rules.js +29 -29
  87. package/src/daemon/telegram-commands.js +54 -54
  88. package/src/daemon/updater.js +85 -85
  89. package/src/jobs/job-runner.js +78 -78
  90. package/src/mcp/consumer.js +129 -129
  91. package/src/memory/active-recall.js +171 -171
  92. package/src/memory/backend-manager.js +97 -97
  93. package/src/memory/backends/file-backend.js +38 -38
  94. package/src/memory/backends/qmd-backend.js +219 -219
  95. package/src/memory/embedding-guards.js +24 -24
  96. package/src/memory/embedding-index.js +118 -118
  97. package/src/memory/embedding-service.js +179 -179
  98. package/src/memory/file-index.js +177 -177
  99. package/src/memory/memory-signature.js +5 -5
  100. package/src/memory/memory-store.js +648 -648
  101. package/src/memory/retrieval-planner.js +66 -66
  102. package/src/memory/scoring.js +145 -145
  103. package/src/memory/simhash.js +78 -78
  104. package/src/memory/sqlite-active-store.js +824 -824
  105. package/src/memory/write-policy.js +36 -36
  106. package/src/onboarding/aliases.js +33 -33
  107. package/src/onboarding/auth/api-key.js +224 -224
  108. package/src/onboarding/auth/ollama-detect.js +42 -42
  109. package/src/onboarding/clack-prompter.js +77 -77
  110. package/src/onboarding/doctor.js +530 -530
  111. package/src/onboarding/lock.js +42 -42
  112. package/src/onboarding/model-catalog.js +344 -344
  113. package/src/onboarding/phases/auth.js +576 -589
  114. package/src/onboarding/phases/build.js +130 -130
  115. package/src/onboarding/phases/choose.js +82 -82
  116. package/src/onboarding/phases/detect.js +98 -98
  117. package/src/onboarding/phases/hatch.js +216 -216
  118. package/src/onboarding/phases/identity.js +79 -79
  119. package/src/onboarding/phases/ollama.js +345 -345
  120. package/src/onboarding/phases/scaffold.js +99 -99
  121. package/src/onboarding/phases/telegram.js +377 -377
  122. package/src/onboarding/phases/validate.js +204 -204
  123. package/src/onboarding/phases/verify.js +206 -206
  124. package/src/onboarding/platform.js +482 -482
  125. package/src/onboarding/status-bar.js +95 -95
  126. package/src/onboarding/templates.js +794 -794
  127. package/src/onboarding/toml-writer.js +38 -38
  128. package/src/onboarding/tui.js +250 -250
  129. package/src/onboarding/uninstall.js +153 -153
  130. package/src/onboarding/wizard.js +516 -499
  131. package/src/providers/anthropic.js +168 -168
  132. package/src/providers/base.js +247 -247
  133. package/src/providers/circuit-breaker.js +136 -136
  134. package/src/providers/ollama.js +163 -163
  135. package/src/providers/openai-codex.js +149 -149
  136. package/src/providers/openrouter.js +136 -136
  137. package/src/providers/registry.js +36 -36
  138. package/src/providers/router.js +16 -16
  139. package/src/runtime/bootstrap-cache.js +47 -47
  140. package/src/runtime/capabilities-prompt.js +25 -25
  141. package/src/runtime/completion-ping.js +99 -99
  142. package/src/runtime/config-validator.js +121 -121
  143. package/src/runtime/context-ledger.js +360 -360
  144. package/src/runtime/cutover-readiness.js +42 -42
  145. package/src/runtime/daemon.js +729 -729
  146. package/src/runtime/delivery-ack.js +195 -195
  147. package/src/runtime/delivery-adapters/local-file.js +41 -41
  148. package/src/runtime/delivery-adapters/openclaw-cli.js +94 -94
  149. package/src/runtime/delivery-adapters/openclaw-peer.js +98 -98
  150. package/src/runtime/delivery-adapters/shadow.js +13 -13
  151. package/src/runtime/delivery-adapters/standalone-http.js +98 -98
  152. package/src/runtime/delivery-adapters/telegram.js +104 -104
  153. package/src/runtime/delivery-adapters/tui.js +128 -128
  154. package/src/runtime/delivery-manager.js +807 -807
  155. package/src/runtime/delivery-store.js +168 -168
  156. package/src/runtime/dependency-health.js +118 -118
  157. package/src/runtime/envelope.js +114 -114
  158. package/src/runtime/evaluation.js +1089 -1089
  159. package/src/runtime/exec-approvals.js +216 -216
  160. package/src/runtime/executor.js +500 -500
  161. package/src/runtime/failure-ping.js +67 -67
  162. package/src/runtime/flows.js +83 -83
  163. package/src/runtime/guards.js +45 -45
  164. package/src/runtime/handoff.js +51 -51
  165. package/src/runtime/identity-cache.js +28 -28
  166. package/src/runtime/improvement-engine.js +109 -109
  167. package/src/runtime/improvement-harness.js +581 -581
  168. package/src/runtime/input-sanitiser.js +72 -72
  169. package/src/runtime/interaction-contract.js +347 -347
  170. package/src/runtime/lane-readiness.js +226 -226
  171. package/src/runtime/migration.js +323 -323
  172. package/src/runtime/model-resolution.js +78 -78
  173. package/src/runtime/network.js +64 -64
  174. package/src/runtime/notification-store.js +97 -97
  175. package/src/runtime/notifier.js +256 -256
  176. package/src/runtime/orchestrator.js +53 -53
  177. package/src/runtime/orphan-reaper.js +41 -41
  178. package/src/runtime/output-contract-schema.js +139 -139
  179. package/src/runtime/output-contract-validator.js +439 -439
  180. package/src/runtime/peer-readiness.js +69 -69
  181. package/src/runtime/peer-registry.js +133 -133
  182. package/src/runtime/pilot-status.js +108 -108
  183. package/src/runtime/prompt-builder.js +261 -261
  184. package/src/runtime/provider-attempt.js +582 -582
  185. package/src/runtime/report-fallback.js +71 -71
  186. package/src/runtime/result-normalizer.js +183 -183
  187. package/src/runtime/retention.js +74 -74
  188. package/src/runtime/review.js +244 -244
  189. package/src/runtime/route-job.js +15 -15
  190. package/src/runtime/run-store.js +38 -38
  191. package/src/runtime/schedule.js +88 -88
  192. package/src/runtime/scheduler-state.js +434 -434
  193. package/src/runtime/scheduler.js +656 -656
  194. package/src/runtime/session-compactor.js +182 -182
  195. package/src/runtime/session-search.js +155 -155
  196. package/src/runtime/slack-inbound.js +249 -249
  197. package/src/runtime/ssrf.js +102 -102
  198. package/src/runtime/status-aggregator.js +330 -330
  199. package/src/runtime/task-contract.js +140 -140
  200. package/src/runtime/task-packet.js +107 -107
  201. package/src/runtime/task-router.js +140 -140
  202. package/src/runtime/telegram-inbound.js +1565 -1565
  203. package/src/runtime/token-counter.js +134 -134
  204. package/src/runtime/token-estimator.js +59 -59
  205. package/src/runtime/tool-loop.js +200 -200
  206. package/src/runtime/transport-server.js +311 -311
  207. package/src/runtime/tui-server.js +411 -411
  208. package/src/runtime/ulid.js +44 -44
  209. package/src/security/ssrf-check.js +197 -197
  210. package/src/setup.js +369 -369
  211. package/src/shadow/bridge.js +303 -303
  212. package/src/skills/loader.js +84 -84
  213. package/src/tools/catalog.json +49 -49
  214. package/src/tools/cli-delegate.js +44 -44
  215. package/src/tools/mcp-client.js +106 -106
  216. package/src/tools/micro/cancel-task.js +6 -6
  217. package/src/tools/micro/complete-task.js +6 -6
  218. package/src/tools/micro/fail-task.js +6 -6
  219. package/src/tools/micro/http-fetch.js +74 -74
  220. package/src/tools/micro/index.js +36 -36
  221. package/src/tools/micro/lcm-recall.js +60 -60
  222. package/src/tools/micro/list-dir.js +17 -17
  223. package/src/tools/micro/list-skills.js +46 -46
  224. package/src/tools/micro/load-skill.js +38 -38
  225. package/src/tools/micro/memory-search.js +45 -45
  226. package/src/tools/micro/read-file.js +11 -11
  227. package/src/tools/micro/session-search.js +54 -54
  228. package/src/tools/micro/shell-exec.js +43 -43
  229. package/src/tools/micro/trigger-job.js +79 -79
  230. package/src/tools/micro/web-search.js +58 -58
  231. package/src/tools/micro/workspace-paths.js +39 -39
  232. package/src/tools/micro/write-file.js +14 -14
  233. package/src/tools/micro/write-memory.js +41 -41
  234. package/src/tools/registry.js +348 -348
  235. package/src/tools/tool-result-contract.js +36 -36
  236. package/src/tui/chat.js +835 -835
  237. package/src/tui/renderer.js +175 -175
  238. package/src/tui/socket-client.js +217 -217
  239. package/src/utils/canonical-json.js +29 -29
  240. package/src/utils/compaction.js +30 -30
  241. package/src/utils/env-loader.js +5 -5
  242. package/src/utils/errors.js +80 -80
  243. package/src/utils/fs.js +101 -101
  244. package/src/utils/ids.js +5 -5
  245. package/src/utils/model-context-limits.js +30 -30
  246. package/src/utils/token-budget.js +74 -74
  247. package/src/utils/usage-cost.js +25 -25
  248. package/src/utils/usage-metrics.js +14 -14
@@ -1,824 +1,824 @@
1
- import { DatabaseSync } from "node:sqlite";
2
- import { stat } from "node:fs/promises";
3
- import { buildResultSignature } from "./memory-signature.js";
4
- import { createRuntimeId } from "../utils/ids.js";
5
- import { hammingDistance, hexToSimhash } from "./simhash.js";
6
-
7
- function uniqueId(prefix) {
8
- return createRuntimeId(prefix);
9
- }
10
-
11
- function normalizeQuery(query) {
12
- return String(query || "")
13
- .split(/[^a-z0-9]+/i)
14
- .map((token) => token.trim())
15
- .filter(Boolean)
16
- .map((token) => `"${token.replace(/"/g, "")}"`)
17
- .join(" OR ");
18
- }
19
-
20
- function toRow(record) {
21
- return {
22
- entryId: record.entryId || null,
23
- entrySignature: record.entrySignature || buildResultSignature(record),
24
- type: record.type,
25
- category: record.category || null,
26
- title: record.title || null,
27
- content: record.content || null,
28
- summary: record.summary || null,
29
- reason: record.reason || null,
30
- timestamp: record.timestamp || null,
31
- salience: record.salience ?? null,
32
- accessCount: record.accessCount ?? 0,
33
- expiresAt: record.expiresAt || null,
34
- sourceKind: record.sourceKind || null,
35
- dedupeKey: record.dedupeKey || null,
36
- simhash: record.simhash || null
37
- };
38
- }
39
-
40
- function fromRow(row) {
41
- return {
42
- entryId: row.entry_id,
43
- entrySignature: row.entry_signature,
44
- type: row.type,
45
- category: row.category,
46
- title: row.title,
47
- content: row.content,
48
- summary: row.summary,
49
- reason: row.reason,
50
- timestamp: row.timestamp,
51
- salience: row.salience,
52
- accessCount: row.access_count ?? 0,
53
- expiresAt: row.expires_at,
54
- sourceKind: row.source_kind,
55
- dedupeKey: row.dedupe_key,
56
- simhash: row.simhash
57
- };
58
- }
59
-
60
- function cosineSimilarity(a, b) {
61
- let dot = 0;
62
- let normA = 0;
63
- let normB = 0;
64
- const length = Math.min(a.length, b.length);
65
- for (let i = 0; i < length; i += 1) {
66
- dot += a[i] * b[i];
67
- normA += a[i] * a[i];
68
- normB += b[i] * b[i];
69
- }
70
- if (normA === 0 || normB === 0) return 0;
71
- return dot / (Math.sqrt(normA) * Math.sqrt(normB));
72
- }
73
-
74
- export function buildDuplicateSignature(record, kind) {
75
- if (kind === "summary") {
76
- return [record.title, record.category, record.summary, record.content].map((value) => String(value || "")).join("::");
77
- }
78
- if (kind === "fact") {
79
- return [record.title, record.category, record.content, record.reason].map((value) => String(value || "")).join("::");
80
- }
81
- return null;
82
- }
83
-
84
- export class SqliteActiveStore {
85
- constructor({ dbPath }) {
86
- this.dbPath = dbPath;
87
- this.walPath = `${dbPath}-wal`;
88
- this.db = new DatabaseSync(dbPath, {
89
- timeout: 5000
90
- });
91
- this.db.exec("pragma busy_timeout = 5000;");
92
- this.db.exec("pragma journal_mode = wal;");
93
- this.db.exec("pragma synchronous = normal;");
94
- this.ensureSchema();
95
- }
96
-
97
- ensureSchema() {
98
- this.db.exec(`
99
- create table if not exists memory_entries (
100
- rowid integer primary key autoincrement,
101
- entry_id text not null unique,
102
- entry_signature text not null,
103
- type text not null,
104
- category text,
105
- title text,
106
- content text,
107
- summary text,
108
- reason text,
109
- timestamp text,
110
- salience real,
111
- access_count integer default 0,
112
- expires_at text,
113
- source_kind text,
114
- dedupe_key text,
115
- duplicate_signature text
116
- );
117
- create unique index if not exists idx_memory_entries_dedupe_key on memory_entries(dedupe_key) where dedupe_key is not null;
118
- create unique index if not exists idx_memory_entries_duplicate_signature on memory_entries(type, duplicate_signature) where duplicate_signature is not null;
119
- create index if not exists idx_memory_entries_expires_at on memory_entries(expires_at);
120
- create virtual table if not exists memory_entries_fts using fts5(title, content, summary, reason, category);
121
- create table if not exists memory_embeddings (
122
- entry_id text primary key,
123
- entry_signature text not null,
124
- entry_json text not null,
125
- vector_json text not null,
126
- updated_at text,
127
- provider text,
128
- model text,
129
- dimensions integer,
130
- freshness_status text not null default 'fresh',
131
- embedded_signature text
132
- );
133
- create table if not exists embedding_meta (
134
- key text primary key,
135
- value text
136
- );
137
- create table if not exists runtime_meta (
138
- key text primary key,
139
- value text
140
- );
141
- create table if not exists memory_locks (
142
- lock_key text primary key,
143
- owner_id text not null,
144
- acquired_at text not null,
145
- expires_at text not null
146
- );
147
- `);
148
- this.ensureColumn("memory_entries", "entry_signature", "text");
149
- this.ensureEmbeddingSchema();
150
- this.backfillEntrySignatures();
151
- this.db.exec("create index if not exists idx_memory_entries_entry_signature on memory_entries(entry_signature);");
152
- this.db.exec("create index if not exists idx_memory_embeddings_signature on memory_embeddings(entry_signature);");
153
- this.ensureColumn("memory_entries", "simhash", "text");
154
- this.db.exec("create index if not exists idx_memory_entries_simhash on memory_entries(simhash);");
155
- this.ensureColumn("memory_entries", "last_accessed", "text");
156
- }
157
-
158
- ensureColumn(tableName, columnName, definition) {
159
- const columns = this.db.prepare(`pragma table_info(${tableName})`).all();
160
- if (columns.some((column) => column.name === columnName)) return;
161
- this.db.exec(`alter table ${tableName} add column ${columnName} ${definition}`);
162
- }
163
-
164
- updateAccessTime(entryIds) {
165
- if (!entryIds.length) return;
166
- const now = new Date().toISOString();
167
- const stmt = this.db.prepare("UPDATE memory_entries SET last_accessed = ? WHERE entry_id = ?");
168
- for (const id of entryIds) {
169
- stmt.run(now, id);
170
- }
171
- }
172
-
173
- ensureEmbeddingSchema() {
174
- const columns = this.db.prepare("pragma table_info(memory_embeddings)").all();
175
- const columnNames = new Set(columns.map((column) => column.name));
176
- const needsReset =
177
- columnNames.size > 0 &&
178
- (!columnNames.has("entry_id") ||
179
- !columnNames.has("freshness_status") ||
180
- !columnNames.has("embedded_signature"));
181
-
182
- if (!needsReset) return;
183
-
184
- this.db.exec("drop table if exists memory_embeddings;");
185
- this.db.exec("drop table if exists embedding_meta;");
186
- this.db.exec(`
187
- create table if not exists memory_embeddings (
188
- entry_id text primary key,
189
- entry_signature text not null,
190
- entry_json text not null,
191
- vector_json text not null,
192
- updated_at text,
193
- provider text,
194
- model text,
195
- dimensions integer,
196
- freshness_status text not null default 'fresh',
197
- embedded_signature text
198
- );
199
- create table if not exists embedding_meta (
200
- key text primary key,
201
- value text
202
- );
203
- `);
204
- }
205
-
206
- backfillEntrySignatures() {
207
- const rows = this.db
208
- .prepare(`
209
- select entry_id, type, category, title, content, summary, reason, timestamp, salience, access_count, expires_at, source_kind, dedupe_key
210
- from memory_entries
211
- where entry_signature is null or entry_signature = ''
212
- `)
213
- .all();
214
- const update = this.db.prepare("update memory_entries set entry_signature = ? where entry_id = ?");
215
- for (const row of rows) {
216
- update.run(buildResultSignature(fromRow(row)), row.entry_id);
217
- }
218
- }
219
-
220
- runTransaction(fn) {
221
- this.db.exec("begin immediate");
222
- try {
223
- const result = fn();
224
- this.db.exec("commit");
225
- return result;
226
- } catch (error) {
227
- try {
228
- this.db.exec("rollback");
229
- } catch {}
230
- throw error;
231
- }
232
- }
233
-
234
- findNearDuplicates(type, simhashHex, maxDistance = 3) {
235
- if (!simhashHex) return [];
236
- const targetHash = hexToSimhash(simhashHex);
237
- const rows = this.db.prepare(
238
- "select entry_id, simhash, salience, access_count from memory_entries where type = ? and simhash is not null"
239
- ).all(type);
240
-
241
- if (rows.length > 1000) {
242
- console.warn(`[Nemoris] simhash: ${rows.length} entries for type "${type}" — consider compaction`);
243
- }
244
-
245
- const matches = [];
246
- for (const row of rows) {
247
- const dist = hammingDistance(hexToSimhash(row.simhash), targetHash);
248
- if (dist <= maxDistance) {
249
- matches.push({
250
- entryId: row.entry_id,
251
- distance: dist,
252
- salience: row.salience,
253
- accessCount: row.access_count
254
- });
255
- }
256
- }
257
- return matches;
258
- }
259
-
260
- boostSalience(entryId, amount = 0.15) {
261
- this.db.prepare(`
262
- update memory_entries
263
- set salience = min(1.0, salience + ?),
264
- access_count = access_count + 1,
265
- timestamp = ?
266
- where entry_id = ?
267
- `).run(amount, new Date().toISOString(), entryId);
268
- }
269
-
270
- close() {
271
- this.db.close();
272
- }
273
-
274
- getCheckpointStats(mode = "passive") {
275
- const normalizedMode = String(mode || "passive").toLowerCase();
276
- const row = this.db.prepare(`pragma wal_checkpoint(${normalizedMode})`).get();
277
- return {
278
- mode: normalizedMode,
279
- busy: Number(row?.busy ?? 0),
280
- log: Number(row?.log ?? 0),
281
- checkpointed: Number(row?.checkpointed ?? 0)
282
- };
283
- }
284
-
285
- async getWalStatus(mode = "passive") {
286
- let walBytes;
287
- try {
288
- walBytes = (await stat(this.walPath)).size;
289
- } catch {
290
- walBytes = 0;
291
- }
292
-
293
- const checkpoint = this.getCheckpointStats(mode);
294
- return {
295
- dbPath: this.dbPath,
296
- walPath: this.walPath,
297
- walBytes,
298
- checkpoint
299
- };
300
- }
301
-
302
- async manageWal(options = {}) {
303
- const thresholdBytes = Number(options.thresholdBytes ?? 64 * 1024 * 1024);
304
- const passive = await this.getWalStatus("passive");
305
- const needsCheckpoint =
306
- passive.walBytes >= thresholdBytes ||
307
- (passive.checkpoint.log > 0 && passive.checkpoint.checkpointed < passive.checkpoint.log);
308
-
309
- if (!needsCheckpoint) {
310
- return {
311
- ...passive,
312
- action: "none"
313
- };
314
- }
315
-
316
- const restart = await this.getWalStatus("restart");
317
- const truncatable = restart.checkpoint.busy === 0;
318
- const finalStatus = truncatable ? await this.getWalStatus("truncate") : restart;
319
-
320
- return {
321
- ...finalStatus,
322
- action: truncatable ? "truncate" : "restart"
323
- };
324
- }
325
-
326
- count() {
327
- return this.db.prepare("select count(*) as count from memory_entries").get().count;
328
- }
329
-
330
- clearAll() {
331
- this.db.exec("delete from memory_entries; delete from memory_entries_fts; delete from memory_embeddings; delete from embedding_meta; delete from runtime_meta;");
332
- }
333
-
334
- hasEventDedupe(dedupeKey) {
335
- if (!dedupeKey) return false;
336
- return Boolean(
337
- this.db
338
- .prepare("select 1 from memory_entries where dedupe_key = ? limit 1")
339
- .get(String(dedupeKey))
340
- );
341
- }
342
-
343
- hasDuplicateSignature(type, duplicateSignature) {
344
- if (!duplicateSignature) return false;
345
- return Boolean(
346
- this.db
347
- .prepare("select 1 from memory_entries where type = ? and duplicate_signature = ? limit 1")
348
- .get(type, duplicateSignature)
349
- );
350
- }
351
-
352
- insertUnchecked(record, options = {}) {
353
- const entryId = options.entryId || uniqueId(record.type || "entry");
354
- const entrySignature = options.entrySignature || buildResultSignature(record);
355
- const duplicateSignature = options.duplicateSignature || null;
356
- const row = toRow(record);
357
- const insertEntry = this.db.prepare(`
358
- insert into memory_entries (
359
- entry_id, entry_signature, type, category, title, content, summary, reason, timestamp, salience, access_count, expires_at, source_kind, dedupe_key, duplicate_signature, simhash
360
- ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
361
- `);
362
- const insertFts = this.db.prepare(`
363
- insert into memory_entries_fts(rowid, title, content, summary, reason, category) values (?, ?, ?, ?, ?, ?)
364
- `);
365
-
366
- const result = insertEntry.run(
367
- entryId,
368
- entrySignature,
369
- row.type,
370
- row.category,
371
- row.title,
372
- row.content,
373
- row.summary,
374
- row.reason,
375
- row.timestamp,
376
- row.salience,
377
- row.accessCount,
378
- row.expiresAt,
379
- row.sourceKind,
380
- row.dedupeKey,
381
- duplicateSignature,
382
- row.simhash
383
- );
384
- insertFts.run(result.lastInsertRowid, row.title, row.content, row.summary, row.reason, row.category);
385
- this.markEmbeddingStateUnchecked(entryId, entrySignature, {
386
- status: "missing",
387
- preserveVector: false
388
- });
389
- return entryId;
390
- }
391
-
392
- insert(record, options = {}) {
393
- return this.runTransaction(() => this.insertUnchecked(record, options));
394
- }
395
-
396
- upsertScratchpad(scratchpadId, record) {
397
- const entryId = `scratchpad:${scratchpadId}`;
398
- const existing = this.db.prepare("select rowid from memory_entries where entry_id = ?").get(entryId);
399
- const row = toRow(record);
400
- const entrySignature = buildResultSignature(record);
401
-
402
- this.runTransaction(() => {
403
- if (existing?.rowid) {
404
- const previous = this.db.prepare("select entry_signature from memory_entries where rowid = ?").get(existing.rowid);
405
- this.db.prepare("delete from memory_entries_fts where rowid = ?").run(existing.rowid);
406
- this.db.prepare(`
407
- update memory_entries
408
- set entry_signature = ?, type = ?, category = ?, title = ?, content = ?, summary = ?, reason = ?, timestamp = ?, salience = ?, access_count = ?, expires_at = ?, source_kind = ?, dedupe_key = ?, duplicate_signature = null
409
- where rowid = ?
410
- `).run(
411
- entrySignature,
412
- row.type,
413
- row.category,
414
- row.title,
415
- row.content,
416
- row.summary,
417
- row.reason,
418
- row.timestamp,
419
- row.salience,
420
- row.accessCount,
421
- row.expiresAt,
422
- row.sourceKind,
423
- row.dedupeKey,
424
- existing.rowid
425
- );
426
- this.db.prepare(`
427
- insert into memory_entries_fts(rowid, title, content, summary, reason, category) values (?, ?, ?, ?, ?, ?)
428
- `).run(existing.rowid, row.title, row.content, row.summary, row.reason, row.category);
429
- this.markEmbeddingStateUnchecked(entryId, entrySignature, {
430
- status: previous?.entry_signature === entrySignature ? "fresh" : "stale",
431
- preserveVector: previous?.entry_signature === entrySignature
432
- });
433
- return;
434
- }
435
-
436
- this.insertUnchecked(record, {
437
- entryId,
438
- entrySignature
439
- });
440
- });
441
- return entryId;
442
- }
443
-
444
- deleteByEntryId(entryId) {
445
- const existing = this.db.prepare("select rowid from memory_entries where entry_id = ?").get(entryId);
446
- if (!existing?.rowid) return false;
447
- this.runTransaction(() => {
448
- this.db.prepare("delete from memory_entries_fts where rowid = ?").run(existing.rowid);
449
- this.db.prepare("delete from memory_entries where rowid = ?").run(existing.rowid);
450
- this.db.prepare("delete from memory_embeddings where entry_id = ?").run(entryId);
451
- });
452
- return true;
453
- }
454
-
455
- listAll(nowIso = null) {
456
- const rows = nowIso
457
- ? this.db
458
- .prepare("select * from memory_entries where expires_at is null or expires_at >= ? order by rowid asc")
459
- .all(nowIso)
460
- : this.db.prepare("select * from memory_entries order by rowid asc").all();
461
- return rows.map(fromRow);
462
- }
463
-
464
- listExpiredScratchpads(nowIso) {
465
- return this.db
466
- .prepare("select entry_id from memory_entries where type = 'scratchpad' and expires_at is not null and expires_at < ?")
467
- .all(nowIso)
468
- .map((row) => row.entry_id);
469
- }
470
-
471
- queryCandidates(query, limit = 32, nowIso = null) {
472
- const match = normalizeQuery(query);
473
- if (!match) {
474
- const rows = nowIso
475
- ? this.db
476
- .prepare("select * from memory_entries where expires_at is null or expires_at >= ? order by rowid desc limit ?")
477
- .all(nowIso, limit)
478
- : this.db.prepare("select * from memory_entries order by rowid desc limit ?").all(limit);
479
- return rows.map(fromRow);
480
- }
481
-
482
- const rows = nowIso
483
- ? this.db
484
- .prepare(`
485
- select e.* from memory_entries_fts f
486
- join memory_entries e on e.rowid = f.rowid
487
- where f.memory_entries_fts match ?
488
- and (e.expires_at is null or e.expires_at >= ?)
489
- limit ?
490
- `)
491
- .all(match, nowIso, limit)
492
- : this.db
493
- .prepare(`
494
- select e.* from memory_entries_fts f
495
- join memory_entries e on e.rowid = f.rowid
496
- where f.memory_entries_fts match ?
497
- limit ?
498
- `)
499
- .all(match, limit);
500
-
501
- if (rows.length >= limit) {
502
- return rows.map(fromRow);
503
- }
504
-
505
- const existingIds = new Set(rows.map((row) => row.entry_id));
506
- const fallback = (nowIso
507
- ? this.db
508
- .prepare("select * from memory_entries where (expires_at is null or expires_at >= ?) order by rowid desc limit ?")
509
- .all(nowIso, limit)
510
- : this.db.prepare("select * from memory_entries order by rowid desc limit ?").all(limit)
511
- ).filter((row) => !existingIds.has(row.entry_id));
512
-
513
- return [...rows, ...fallback].slice(0, limit).map(fromRow);
514
- }
515
-
516
- replaceEmbeddings({ items, config = null, updatedAt = null }) {
517
- const normalizedUpdatedAt = updatedAt || new Date().toISOString();
518
- const provider = config?.provider || null;
519
- const model = config?.model || null;
520
- const dimensions = Number(config?.dimensions ?? 0) || null;
521
-
522
- this.runTransaction(() => {
523
- this.db.prepare("delete from memory_embeddings").run();
524
- this.db.prepare("delete from embedding_meta").run();
525
-
526
- const insert = this.db.prepare(`
527
- insert into memory_embeddings (
528
- entry_id, entry_signature, entry_json, vector_json, updated_at, provider, model, dimensions, freshness_status, embedded_signature
529
- ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
530
- on conflict(entry_id) do update set
531
- entry_signature = excluded.entry_signature,
532
- entry_json = excluded.entry_json,
533
- vector_json = excluded.vector_json,
534
- updated_at = excluded.updated_at,
535
- provider = excluded.provider,
536
- model = excluded.model,
537
- dimensions = excluded.dimensions,
538
- freshness_status = excluded.freshness_status,
539
- embedded_signature = excluded.embedded_signature
540
- `);
541
- const upsertMeta = this.db.prepare(`
542
- insert into embedding_meta(key, value) values (?, ?)
543
- on conflict(key) do update set value = excluded.value
544
- `);
545
-
546
- const seenEntryIds = new Set();
547
- for (const item of items || []) {
548
- const entryId = item.entryId || item.item?.entryId;
549
- if (!entryId) continue;
550
- seenEntryIds.add(entryId);
551
- const entrySignature = item.signature || item.item?.entrySignature || buildResultSignature(item.item || {});
552
- insert.run(
553
- entryId,
554
- entrySignature,
555
- JSON.stringify(item.item || {}),
556
- JSON.stringify(item.vector || []),
557
- normalizedUpdatedAt,
558
- provider,
559
- model,
560
- dimensions,
561
- "fresh",
562
- entrySignature
563
- );
564
- }
565
-
566
- const existingRows = this.db.prepare("select entry_id from memory_entries").all();
567
- for (const row of existingRows) {
568
- if (seenEntryIds.has(row.entry_id)) continue;
569
- const entry = this.db.prepare("select entry_signature from memory_entries where entry_id = ?").get(row.entry_id);
570
- this.markEmbeddingStateUnchecked(row.entry_id, entry?.entry_signature || null, {
571
- status: "missing",
572
- preserveVector: false
573
- });
574
- }
575
-
576
- upsertMeta.run("updatedAt", normalizedUpdatedAt);
577
- upsertMeta.run("config", JSON.stringify(config || null));
578
- });
579
-
580
- return {
581
- updatedAt: normalizedUpdatedAt,
582
- config,
583
- items
584
- };
585
- }
586
-
587
- getEmbeddingIndex() {
588
- const rows = this.db
589
- .prepare(`
590
- select entry_id, entry_signature, entry_json, vector_json, updated_at, provider, model, dimensions, freshness_status, embedded_signature
591
- from memory_embeddings
592
- order by entry_id asc
593
- `)
594
- .all();
595
- const updatedAt = this.db.prepare("select value from embedding_meta where key = 'updatedAt'").get()?.value || null;
596
- const configRaw = this.db.prepare("select value from embedding_meta where key = 'config'").get()?.value || "null";
597
-
598
- return {
599
- updatedAt,
600
- config: JSON.parse(configRaw),
601
- items: rows.map((row) => ({
602
- entryId: row.entry_id,
603
- signature: row.entry_signature,
604
- item: JSON.parse(row.entry_json),
605
- vector: JSON.parse(row.vector_json),
606
- updatedAt: row.updated_at,
607
- provider: row.provider,
608
- model: row.model,
609
- dimensions: row.dimensions,
610
- freshnessStatus: row.freshness_status,
611
- embeddedSignature: row.embedded_signature
612
- }))
613
- };
614
- }
615
-
616
- getEmbeddingHealth() {
617
- const freshnessRows = this.db
618
- .prepare(`
619
- select freshness_status, count(*) as count
620
- from memory_embeddings
621
- group by freshness_status
622
- `)
623
- .all();
624
- const freshness = Object.fromEntries(
625
- freshnessRows.map((row) => [row.freshness_status, Number(row.count || 0)])
626
- );
627
- const lastError = this.getRuntimeMeta("embedding_last_error");
628
- const lastErrorAt = this.getRuntimeMeta("embedding_last_error_at");
629
- const lastProvider = this.getRuntimeMeta("embedding_last_provider");
630
- const lastModel = this.getRuntimeMeta("embedding_last_model");
631
- const lastSuccessAt = this.getRuntimeMeta("embedding_last_success_at");
632
- const lastMode = this.getRuntimeMeta("embedding_last_query_mode");
633
-
634
- return {
635
- totalTrackedCount: freshnessRows.reduce((sum, row) => sum + Number(row.count || 0), 0),
636
- freshCount: freshness.fresh || 0,
637
- staleCount: freshness.stale || 0,
638
- missingCount: freshness.missing || 0,
639
- failedCount: freshness.failed || 0,
640
- lastError,
641
- lastErrorAt,
642
- lastProvider,
643
- lastModel,
644
- lastSuccessAt,
645
- lastQueryMode: lastMode,
646
- degraded: Boolean(lastError) || (freshness.fresh || 0) === 0
647
- };
648
- }
649
-
650
- queryEmbeddingSimilarities(queryVector, limit = 8, options = {}) {
651
- const nowIso = options.nowIso || null;
652
- const candidateEntryIds = options.candidateEntryIds || null;
653
- const params = [];
654
- let sql = `
655
- select e.entry_id, e.entry_signature, e.type, e.category, e.title, e.content, e.summary, e.reason, e.timestamp, e.salience, e.access_count, e.expires_at, e.source_kind, e.dedupe_key,
656
- m.vector_json, m.freshness_status, m.updated_at, m.provider, m.model, m.dimensions
657
- from memory_embeddings m
658
- join memory_entries e on e.entry_id = m.entry_id
659
- where m.freshness_status = 'fresh'
660
- `;
661
- if (nowIso) {
662
- sql += " and (e.expires_at is null or e.expires_at >= ?)";
663
- params.push(nowIso);
664
- }
665
- if (candidateEntryIds?.length) {
666
- sql += ` and e.entry_id in (${candidateEntryIds.map(() => "?").join(", ")})`;
667
- params.push(...candidateEntryIds);
668
- }
669
- const rows = this.db.prepare(sql).all(...params);
670
-
671
- return rows
672
- .map((row) => ({
673
- entryId: row.entry_id,
674
- signature: row.entry_signature,
675
- similarity: cosineSimilarity(queryVector, JSON.parse(row.vector_json)),
676
- freshnessStatus: row.freshness_status,
677
- item: fromRow(row)
678
- }))
679
- .sort((a, b) => b.similarity - a.similarity)
680
- .slice(0, limit);
681
- }
682
-
683
- getEmbeddingState(entryId) {
684
- return this.db
685
- .prepare(`
686
- select entry_id, entry_signature, updated_at, provider, model, dimensions, freshness_status, embedded_signature
687
- from memory_embeddings where entry_id = ?
688
- `)
689
- .get(entryId) || null;
690
- }
691
-
692
- queryRetrievalCandidates(query, options = {}) {
693
- const nowIso = options.nowIso || null;
694
- const limit = Math.max(Number(options.limit ?? 8), 1);
695
- const candidateMultiplier = Math.max(Number(options.candidateMultiplier ?? 6), 2);
696
- const lexicalLimit = Math.max(limit * candidateMultiplier, 24);
697
- const queryVector = Array.isArray(options.queryVector) ? options.queryVector : null;
698
-
699
- const lexicalRows = this.queryCandidates(query, lexicalLimit, nowIso);
700
- const lexicalMap = new Map(lexicalRows.map((item, index) => [item.entryId, { item, lexicalRank: index }]));
701
- const semanticRows = queryVector
702
- ? this.queryEmbeddingSimilarities(queryVector, lexicalLimit, {
703
- nowIso,
704
- candidateEntryIds: lexicalRows.map((item) => item.entryId)
705
- })
706
- : [];
707
- const semanticMap = new Map(semanticRows.map((row, index) => [row.entryId, { ...row, semanticRank: index }]));
708
-
709
- const candidateEntryIds = new Set([...lexicalMap.keys(), ...semanticMap.keys()]);
710
- const results = [];
711
-
712
- for (const entryId of candidateEntryIds) {
713
- const lexical = lexicalMap.get(entryId) || null;
714
- const semantic = semanticMap.get(entryId) || null;
715
- const baseItem = semantic?.item || lexical?.item;
716
- if (!baseItem) continue;
717
- const embeddingState = this.getEmbeddingState(entryId);
718
- results.push({
719
- item: baseItem,
720
- lexicalMatch: Boolean(lexical),
721
- lexicalRank: lexical?.lexicalRank ?? null,
722
- embeddingSimilarity: semantic?.similarity ?? 0,
723
- semanticRank: semantic?.semanticRank ?? null,
724
- embeddingFreshness: embeddingState?.freshness_status || "missing",
725
- embeddingProvider: embeddingState?.provider || null,
726
- embeddingModel: embeddingState?.model || null,
727
- retrievalSources: [
728
- lexical ? "lexical" : null,
729
- semantic ? "semantic" : null
730
- ].filter(Boolean)
731
- });
732
- }
733
-
734
- return results;
735
- }
736
-
737
- markEmbeddingState(entryId, entrySignature, options = {}) {
738
- return this.runTransaction(() => this.markEmbeddingStateUnchecked(entryId, entrySignature, options));
739
- }
740
-
741
- markEmbeddingStateUnchecked(entryId, entrySignature, options = {}) {
742
- const existing = this.db.prepare("select * from memory_embeddings where entry_id = ?").get(entryId);
743
- const status = options.status || "missing";
744
- const preserveVector = options.preserveVector ?? status === "fresh";
745
- const updatedAt = options.updatedAt || existing?.updated_at || null;
746
- const provider = options.provider || existing?.provider || null;
747
- const model = options.model || existing?.model || null;
748
- const dimensions = options.dimensions ?? existing?.dimensions ?? null;
749
- const embeddedSignature =
750
- status === "fresh"
751
- ? entrySignature
752
- : options.embeddedSignature ?? (preserveVector ? existing?.embedded_signature : null);
753
- const entryJson = options.entryJson ?? existing?.entry_json ?? JSON.stringify({});
754
- const vectorJson = preserveVector ? existing?.vector_json || JSON.stringify([]) : JSON.stringify([]);
755
-
756
- this.db.prepare(`
757
- insert into memory_embeddings (
758
- entry_id, entry_signature, entry_json, vector_json, updated_at, provider, model, dimensions, freshness_status, embedded_signature
759
- ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
760
- on conflict(entry_id) do update set
761
- entry_signature = excluded.entry_signature,
762
- entry_json = excluded.entry_json,
763
- vector_json = excluded.vector_json,
764
- updated_at = excluded.updated_at,
765
- provider = excluded.provider,
766
- model = excluded.model,
767
- dimensions = excluded.dimensions,
768
- freshness_status = excluded.freshness_status,
769
- embedded_signature = excluded.embedded_signature
770
- `).run(
771
- entryId,
772
- entrySignature,
773
- entryJson,
774
- vectorJson,
775
- updatedAt,
776
- provider,
777
- model,
778
- dimensions,
779
- status,
780
- embeddedSignature
781
- );
782
- }
783
-
784
- setRuntimeMeta(key, value) {
785
- this.db.prepare(`
786
- insert into runtime_meta(key, value) values (?, ?)
787
- on conflict(key) do update set value = excluded.value
788
- `).run(String(key), value == null ? null : String(value));
789
- }
790
-
791
- getRuntimeMeta(key) {
792
- const row = this.db.prepare("select value from runtime_meta where key = ?").get(String(key));
793
- return row?.value ?? null;
794
- }
795
-
796
- clearRuntimeMeta(key) {
797
- this.db.prepare("delete from runtime_meta where key = ?").run(String(key));
798
- }
799
-
800
- tryAcquireLock(lockKey, { ownerId, acquiredAt, expiresAt }) {
801
- return this.runTransaction(() => {
802
- const existing = this.db.prepare("select * from memory_locks where lock_key = ?").get(lockKey);
803
- if (!existing) {
804
- this.db
805
- .prepare("insert into memory_locks(lock_key, owner_id, acquired_at, expires_at) values (?, ?, ?, ?)")
806
- .run(lockKey, ownerId, acquiredAt, expiresAt);
807
- return true;
808
- }
809
-
810
- if (existing.owner_id === ownerId || existing.expires_at <= acquiredAt) {
811
- this.db
812
- .prepare("update memory_locks set owner_id = ?, acquired_at = ?, expires_at = ? where lock_key = ?")
813
- .run(ownerId, acquiredAt, expiresAt, lockKey);
814
- return true;
815
- }
816
-
817
- return false;
818
- });
819
- }
820
-
821
- releaseLock(lockKey, ownerId) {
822
- this.db.prepare("delete from memory_locks where lock_key = ? and owner_id = ?").run(lockKey, ownerId);
823
- }
824
- }
1
+ import { DatabaseSync } from "node:sqlite";
2
+ import { stat } from "node:fs/promises";
3
+ import { buildResultSignature } from "./memory-signature.js";
4
+ import { createRuntimeId } from "../utils/ids.js";
5
+ import { hammingDistance, hexToSimhash } from "./simhash.js";
6
+
7
+ function uniqueId(prefix) {
8
+ return createRuntimeId(prefix);
9
+ }
10
+
11
+ function normalizeQuery(query) {
12
+ return String(query || "")
13
+ .split(/[^a-z0-9]+/i)
14
+ .map((token) => token.trim())
15
+ .filter(Boolean)
16
+ .map((token) => `"${token.replace(/"/g, "")}"`)
17
+ .join(" OR ");
18
+ }
19
+
20
+ function toRow(record) {
21
+ return {
22
+ entryId: record.entryId || null,
23
+ entrySignature: record.entrySignature || buildResultSignature(record),
24
+ type: record.type,
25
+ category: record.category || null,
26
+ title: record.title || null,
27
+ content: record.content || null,
28
+ summary: record.summary || null,
29
+ reason: record.reason || null,
30
+ timestamp: record.timestamp || null,
31
+ salience: record.salience ?? null,
32
+ accessCount: record.accessCount ?? 0,
33
+ expiresAt: record.expiresAt || null,
34
+ sourceKind: record.sourceKind || null,
35
+ dedupeKey: record.dedupeKey || null,
36
+ simhash: record.simhash || null
37
+ };
38
+ }
39
+
40
+ function fromRow(row) {
41
+ return {
42
+ entryId: row.entry_id,
43
+ entrySignature: row.entry_signature,
44
+ type: row.type,
45
+ category: row.category,
46
+ title: row.title,
47
+ content: row.content,
48
+ summary: row.summary,
49
+ reason: row.reason,
50
+ timestamp: row.timestamp,
51
+ salience: row.salience,
52
+ accessCount: row.access_count ?? 0,
53
+ expiresAt: row.expires_at,
54
+ sourceKind: row.source_kind,
55
+ dedupeKey: row.dedupe_key,
56
+ simhash: row.simhash
57
+ };
58
+ }
59
+
60
+ function cosineSimilarity(a, b) {
61
+ let dot = 0;
62
+ let normA = 0;
63
+ let normB = 0;
64
+ const length = Math.min(a.length, b.length);
65
+ for (let i = 0; i < length; i += 1) {
66
+ dot += a[i] * b[i];
67
+ normA += a[i] * a[i];
68
+ normB += b[i] * b[i];
69
+ }
70
+ if (normA === 0 || normB === 0) return 0;
71
+ return dot / (Math.sqrt(normA) * Math.sqrt(normB));
72
+ }
73
+
74
+ export function buildDuplicateSignature(record, kind) {
75
+ if (kind === "summary") {
76
+ return [record.title, record.category, record.summary, record.content].map((value) => String(value || "")).join("::");
77
+ }
78
+ if (kind === "fact") {
79
+ return [record.title, record.category, record.content, record.reason].map((value) => String(value || "")).join("::");
80
+ }
81
+ return null;
82
+ }
83
+
84
+ export class SqliteActiveStore {
85
+ constructor({ dbPath }) {
86
+ this.dbPath = dbPath;
87
+ this.walPath = `${dbPath}-wal`;
88
+ this.db = new DatabaseSync(dbPath, {
89
+ timeout: 5000
90
+ });
91
+ this.db.exec("pragma busy_timeout = 5000;");
92
+ this.db.exec("pragma journal_mode = wal;");
93
+ this.db.exec("pragma synchronous = normal;");
94
+ this.ensureSchema();
95
+ }
96
+
97
+ ensureSchema() {
98
+ this.db.exec(`
99
+ create table if not exists memory_entries (
100
+ rowid integer primary key autoincrement,
101
+ entry_id text not null unique,
102
+ entry_signature text not null,
103
+ type text not null,
104
+ category text,
105
+ title text,
106
+ content text,
107
+ summary text,
108
+ reason text,
109
+ timestamp text,
110
+ salience real,
111
+ access_count integer default 0,
112
+ expires_at text,
113
+ source_kind text,
114
+ dedupe_key text,
115
+ duplicate_signature text
116
+ );
117
+ create unique index if not exists idx_memory_entries_dedupe_key on memory_entries(dedupe_key) where dedupe_key is not null;
118
+ create unique index if not exists idx_memory_entries_duplicate_signature on memory_entries(type, duplicate_signature) where duplicate_signature is not null;
119
+ create index if not exists idx_memory_entries_expires_at on memory_entries(expires_at);
120
+ create virtual table if not exists memory_entries_fts using fts5(title, content, summary, reason, category);
121
+ create table if not exists memory_embeddings (
122
+ entry_id text primary key,
123
+ entry_signature text not null,
124
+ entry_json text not null,
125
+ vector_json text not null,
126
+ updated_at text,
127
+ provider text,
128
+ model text,
129
+ dimensions integer,
130
+ freshness_status text not null default 'fresh',
131
+ embedded_signature text
132
+ );
133
+ create table if not exists embedding_meta (
134
+ key text primary key,
135
+ value text
136
+ );
137
+ create table if not exists runtime_meta (
138
+ key text primary key,
139
+ value text
140
+ );
141
+ create table if not exists memory_locks (
142
+ lock_key text primary key,
143
+ owner_id text not null,
144
+ acquired_at text not null,
145
+ expires_at text not null
146
+ );
147
+ `);
148
+ this.ensureColumn("memory_entries", "entry_signature", "text");
149
+ this.ensureEmbeddingSchema();
150
+ this.backfillEntrySignatures();
151
+ this.db.exec("create index if not exists idx_memory_entries_entry_signature on memory_entries(entry_signature);");
152
+ this.db.exec("create index if not exists idx_memory_embeddings_signature on memory_embeddings(entry_signature);");
153
+ this.ensureColumn("memory_entries", "simhash", "text");
154
+ this.db.exec("create index if not exists idx_memory_entries_simhash on memory_entries(simhash);");
155
+ this.ensureColumn("memory_entries", "last_accessed", "text");
156
+ }
157
+
158
+ ensureColumn(tableName, columnName, definition) {
159
+ const columns = this.db.prepare(`pragma table_info(${tableName})`).all();
160
+ if (columns.some((column) => column.name === columnName)) return;
161
+ this.db.exec(`alter table ${tableName} add column ${columnName} ${definition}`);
162
+ }
163
+
164
+ updateAccessTime(entryIds) {
165
+ if (!entryIds.length) return;
166
+ const now = new Date().toISOString();
167
+ const stmt = this.db.prepare("UPDATE memory_entries SET last_accessed = ? WHERE entry_id = ?");
168
+ for (const id of entryIds) {
169
+ stmt.run(now, id);
170
+ }
171
+ }
172
+
173
+ ensureEmbeddingSchema() {
174
+ const columns = this.db.prepare("pragma table_info(memory_embeddings)").all();
175
+ const columnNames = new Set(columns.map((column) => column.name));
176
+ const needsReset =
177
+ columnNames.size > 0 &&
178
+ (!columnNames.has("entry_id") ||
179
+ !columnNames.has("freshness_status") ||
180
+ !columnNames.has("embedded_signature"));
181
+
182
+ if (!needsReset) return;
183
+
184
+ this.db.exec("drop table if exists memory_embeddings;");
185
+ this.db.exec("drop table if exists embedding_meta;");
186
+ this.db.exec(`
187
+ create table if not exists memory_embeddings (
188
+ entry_id text primary key,
189
+ entry_signature text not null,
190
+ entry_json text not null,
191
+ vector_json text not null,
192
+ updated_at text,
193
+ provider text,
194
+ model text,
195
+ dimensions integer,
196
+ freshness_status text not null default 'fresh',
197
+ embedded_signature text
198
+ );
199
+ create table if not exists embedding_meta (
200
+ key text primary key,
201
+ value text
202
+ );
203
+ `);
204
+ }
205
+
206
+ backfillEntrySignatures() {
207
+ const rows = this.db
208
+ .prepare(`
209
+ select entry_id, type, category, title, content, summary, reason, timestamp, salience, access_count, expires_at, source_kind, dedupe_key
210
+ from memory_entries
211
+ where entry_signature is null or entry_signature = ''
212
+ `)
213
+ .all();
214
+ const update = this.db.prepare("update memory_entries set entry_signature = ? where entry_id = ?");
215
+ for (const row of rows) {
216
+ update.run(buildResultSignature(fromRow(row)), row.entry_id);
217
+ }
218
+ }
219
+
220
+ runTransaction(fn) {
221
+ this.db.exec("begin immediate");
222
+ try {
223
+ const result = fn();
224
+ this.db.exec("commit");
225
+ return result;
226
+ } catch (error) {
227
+ try {
228
+ this.db.exec("rollback");
229
+ } catch {}
230
+ throw error;
231
+ }
232
+ }
233
+
234
+ findNearDuplicates(type, simhashHex, maxDistance = 3) {
235
+ if (!simhashHex) return [];
236
+ const targetHash = hexToSimhash(simhashHex);
237
+ const rows = this.db.prepare(
238
+ "select entry_id, simhash, salience, access_count from memory_entries where type = ? and simhash is not null"
239
+ ).all(type);
240
+
241
+ if (rows.length > 1000) {
242
+ console.warn(`[Nemoris] simhash: ${rows.length} entries for type "${type}" — consider compaction`);
243
+ }
244
+
245
+ const matches = [];
246
+ for (const row of rows) {
247
+ const dist = hammingDistance(hexToSimhash(row.simhash), targetHash);
248
+ if (dist <= maxDistance) {
249
+ matches.push({
250
+ entryId: row.entry_id,
251
+ distance: dist,
252
+ salience: row.salience,
253
+ accessCount: row.access_count
254
+ });
255
+ }
256
+ }
257
+ return matches;
258
+ }
259
+
260
+ boostSalience(entryId, amount = 0.15) {
261
+ this.db.prepare(`
262
+ update memory_entries
263
+ set salience = min(1.0, salience + ?),
264
+ access_count = access_count + 1,
265
+ timestamp = ?
266
+ where entry_id = ?
267
+ `).run(amount, new Date().toISOString(), entryId);
268
+ }
269
+
270
+ close() {
271
+ this.db.close();
272
+ }
273
+
274
+ getCheckpointStats(mode = "passive") {
275
+ const normalizedMode = String(mode || "passive").toLowerCase();
276
+ const row = this.db.prepare(`pragma wal_checkpoint(${normalizedMode})`).get();
277
+ return {
278
+ mode: normalizedMode,
279
+ busy: Number(row?.busy ?? 0),
280
+ log: Number(row?.log ?? 0),
281
+ checkpointed: Number(row?.checkpointed ?? 0)
282
+ };
283
+ }
284
+
285
+ async getWalStatus(mode = "passive") {
286
+ let walBytes;
287
+ try {
288
+ walBytes = (await stat(this.walPath)).size;
289
+ } catch {
290
+ walBytes = 0;
291
+ }
292
+
293
+ const checkpoint = this.getCheckpointStats(mode);
294
+ return {
295
+ dbPath: this.dbPath,
296
+ walPath: this.walPath,
297
+ walBytes,
298
+ checkpoint
299
+ };
300
+ }
301
+
302
+ async manageWal(options = {}) {
303
+ const thresholdBytes = Number(options.thresholdBytes ?? 64 * 1024 * 1024);
304
+ const passive = await this.getWalStatus("passive");
305
+ const needsCheckpoint =
306
+ passive.walBytes >= thresholdBytes ||
307
+ (passive.checkpoint.log > 0 && passive.checkpoint.checkpointed < passive.checkpoint.log);
308
+
309
+ if (!needsCheckpoint) {
310
+ return {
311
+ ...passive,
312
+ action: "none"
313
+ };
314
+ }
315
+
316
+ const restart = await this.getWalStatus("restart");
317
+ const truncatable = restart.checkpoint.busy === 0;
318
+ const finalStatus = truncatable ? await this.getWalStatus("truncate") : restart;
319
+
320
+ return {
321
+ ...finalStatus,
322
+ action: truncatable ? "truncate" : "restart"
323
+ };
324
+ }
325
+
326
+ count() {
327
+ return this.db.prepare("select count(*) as count from memory_entries").get().count;
328
+ }
329
+
330
+ clearAll() {
331
+ this.db.exec("delete from memory_entries; delete from memory_entries_fts; delete from memory_embeddings; delete from embedding_meta; delete from runtime_meta;");
332
+ }
333
+
334
+ hasEventDedupe(dedupeKey) {
335
+ if (!dedupeKey) return false;
336
+ return Boolean(
337
+ this.db
338
+ .prepare("select 1 from memory_entries where dedupe_key = ? limit 1")
339
+ .get(String(dedupeKey))
340
+ );
341
+ }
342
+
343
+ hasDuplicateSignature(type, duplicateSignature) {
344
+ if (!duplicateSignature) return false;
345
+ return Boolean(
346
+ this.db
347
+ .prepare("select 1 from memory_entries where type = ? and duplicate_signature = ? limit 1")
348
+ .get(type, duplicateSignature)
349
+ );
350
+ }
351
+
352
+ insertUnchecked(record, options = {}) {
353
+ const entryId = options.entryId || uniqueId(record.type || "entry");
354
+ const entrySignature = options.entrySignature || buildResultSignature(record);
355
+ const duplicateSignature = options.duplicateSignature || null;
356
+ const row = toRow(record);
357
+ const insertEntry = this.db.prepare(`
358
+ insert into memory_entries (
359
+ entry_id, entry_signature, type, category, title, content, summary, reason, timestamp, salience, access_count, expires_at, source_kind, dedupe_key, duplicate_signature, simhash
360
+ ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
361
+ `);
362
+ const insertFts = this.db.prepare(`
363
+ insert into memory_entries_fts(rowid, title, content, summary, reason, category) values (?, ?, ?, ?, ?, ?)
364
+ `);
365
+
366
+ const result = insertEntry.run(
367
+ entryId,
368
+ entrySignature,
369
+ row.type,
370
+ row.category,
371
+ row.title,
372
+ row.content,
373
+ row.summary,
374
+ row.reason,
375
+ row.timestamp,
376
+ row.salience,
377
+ row.accessCount,
378
+ row.expiresAt,
379
+ row.sourceKind,
380
+ row.dedupeKey,
381
+ duplicateSignature,
382
+ row.simhash
383
+ );
384
+ insertFts.run(result.lastInsertRowid, row.title, row.content, row.summary, row.reason, row.category);
385
+ this.markEmbeddingStateUnchecked(entryId, entrySignature, {
386
+ status: "missing",
387
+ preserveVector: false
388
+ });
389
+ return entryId;
390
+ }
391
+
392
+ insert(record, options = {}) {
393
+ return this.runTransaction(() => this.insertUnchecked(record, options));
394
+ }
395
+
396
+ upsertScratchpad(scratchpadId, record) {
397
+ const entryId = `scratchpad:${scratchpadId}`;
398
+ const existing = this.db.prepare("select rowid from memory_entries where entry_id = ?").get(entryId);
399
+ const row = toRow(record);
400
+ const entrySignature = buildResultSignature(record);
401
+
402
+ this.runTransaction(() => {
403
+ if (existing?.rowid) {
404
+ const previous = this.db.prepare("select entry_signature from memory_entries where rowid = ?").get(existing.rowid);
405
+ this.db.prepare("delete from memory_entries_fts where rowid = ?").run(existing.rowid);
406
+ this.db.prepare(`
407
+ update memory_entries
408
+ set entry_signature = ?, type = ?, category = ?, title = ?, content = ?, summary = ?, reason = ?, timestamp = ?, salience = ?, access_count = ?, expires_at = ?, source_kind = ?, dedupe_key = ?, duplicate_signature = null
409
+ where rowid = ?
410
+ `).run(
411
+ entrySignature,
412
+ row.type,
413
+ row.category,
414
+ row.title,
415
+ row.content,
416
+ row.summary,
417
+ row.reason,
418
+ row.timestamp,
419
+ row.salience,
420
+ row.accessCount,
421
+ row.expiresAt,
422
+ row.sourceKind,
423
+ row.dedupeKey,
424
+ existing.rowid
425
+ );
426
+ this.db.prepare(`
427
+ insert into memory_entries_fts(rowid, title, content, summary, reason, category) values (?, ?, ?, ?, ?, ?)
428
+ `).run(existing.rowid, row.title, row.content, row.summary, row.reason, row.category);
429
+ this.markEmbeddingStateUnchecked(entryId, entrySignature, {
430
+ status: previous?.entry_signature === entrySignature ? "fresh" : "stale",
431
+ preserveVector: previous?.entry_signature === entrySignature
432
+ });
433
+ return;
434
+ }
435
+
436
+ this.insertUnchecked(record, {
437
+ entryId,
438
+ entrySignature
439
+ });
440
+ });
441
+ return entryId;
442
+ }
443
+
444
+ deleteByEntryId(entryId) {
445
+ const existing = this.db.prepare("select rowid from memory_entries where entry_id = ?").get(entryId);
446
+ if (!existing?.rowid) return false;
447
+ this.runTransaction(() => {
448
+ this.db.prepare("delete from memory_entries_fts where rowid = ?").run(existing.rowid);
449
+ this.db.prepare("delete from memory_entries where rowid = ?").run(existing.rowid);
450
+ this.db.prepare("delete from memory_embeddings where entry_id = ?").run(entryId);
451
+ });
452
+ return true;
453
+ }
454
+
455
+ listAll(nowIso = null) {
456
+ const rows = nowIso
457
+ ? this.db
458
+ .prepare("select * from memory_entries where expires_at is null or expires_at >= ? order by rowid asc")
459
+ .all(nowIso)
460
+ : this.db.prepare("select * from memory_entries order by rowid asc").all();
461
+ return rows.map(fromRow);
462
+ }
463
+
464
+ listExpiredScratchpads(nowIso) {
465
+ return this.db
466
+ .prepare("select entry_id from memory_entries where type = 'scratchpad' and expires_at is not null and expires_at < ?")
467
+ .all(nowIso)
468
+ .map((row) => row.entry_id);
469
+ }
470
+
471
+ queryCandidates(query, limit = 32, nowIso = null) {
472
+ const match = normalizeQuery(query);
473
+ if (!match) {
474
+ const rows = nowIso
475
+ ? this.db
476
+ .prepare("select * from memory_entries where expires_at is null or expires_at >= ? order by rowid desc limit ?")
477
+ .all(nowIso, limit)
478
+ : this.db.prepare("select * from memory_entries order by rowid desc limit ?").all(limit);
479
+ return rows.map(fromRow);
480
+ }
481
+
482
+ const rows = nowIso
483
+ ? this.db
484
+ .prepare(`
485
+ select e.* from memory_entries_fts f
486
+ join memory_entries e on e.rowid = f.rowid
487
+ where f.memory_entries_fts match ?
488
+ and (e.expires_at is null or e.expires_at >= ?)
489
+ limit ?
490
+ `)
491
+ .all(match, nowIso, limit)
492
+ : this.db
493
+ .prepare(`
494
+ select e.* from memory_entries_fts f
495
+ join memory_entries e on e.rowid = f.rowid
496
+ where f.memory_entries_fts match ?
497
+ limit ?
498
+ `)
499
+ .all(match, limit);
500
+
501
+ if (rows.length >= limit) {
502
+ return rows.map(fromRow);
503
+ }
504
+
505
+ const existingIds = new Set(rows.map((row) => row.entry_id));
506
+ const fallback = (nowIso
507
+ ? this.db
508
+ .prepare("select * from memory_entries where (expires_at is null or expires_at >= ?) order by rowid desc limit ?")
509
+ .all(nowIso, limit)
510
+ : this.db.prepare("select * from memory_entries order by rowid desc limit ?").all(limit)
511
+ ).filter((row) => !existingIds.has(row.entry_id));
512
+
513
+ return [...rows, ...fallback].slice(0, limit).map(fromRow);
514
+ }
515
+
516
+ replaceEmbeddings({ items, config = null, updatedAt = null }) {
517
+ const normalizedUpdatedAt = updatedAt || new Date().toISOString();
518
+ const provider = config?.provider || null;
519
+ const model = config?.model || null;
520
+ const dimensions = Number(config?.dimensions ?? 0) || null;
521
+
522
+ this.runTransaction(() => {
523
+ this.db.prepare("delete from memory_embeddings").run();
524
+ this.db.prepare("delete from embedding_meta").run();
525
+
526
+ const insert = this.db.prepare(`
527
+ insert into memory_embeddings (
528
+ entry_id, entry_signature, entry_json, vector_json, updated_at, provider, model, dimensions, freshness_status, embedded_signature
529
+ ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
530
+ on conflict(entry_id) do update set
531
+ entry_signature = excluded.entry_signature,
532
+ entry_json = excluded.entry_json,
533
+ vector_json = excluded.vector_json,
534
+ updated_at = excluded.updated_at,
535
+ provider = excluded.provider,
536
+ model = excluded.model,
537
+ dimensions = excluded.dimensions,
538
+ freshness_status = excluded.freshness_status,
539
+ embedded_signature = excluded.embedded_signature
540
+ `);
541
+ const upsertMeta = this.db.prepare(`
542
+ insert into embedding_meta(key, value) values (?, ?)
543
+ on conflict(key) do update set value = excluded.value
544
+ `);
545
+
546
+ const seenEntryIds = new Set();
547
+ for (const item of items || []) {
548
+ const entryId = item.entryId || item.item?.entryId;
549
+ if (!entryId) continue;
550
+ seenEntryIds.add(entryId);
551
+ const entrySignature = item.signature || item.item?.entrySignature || buildResultSignature(item.item || {});
552
+ insert.run(
553
+ entryId,
554
+ entrySignature,
555
+ JSON.stringify(item.item || {}),
556
+ JSON.stringify(item.vector || []),
557
+ normalizedUpdatedAt,
558
+ provider,
559
+ model,
560
+ dimensions,
561
+ "fresh",
562
+ entrySignature
563
+ );
564
+ }
565
+
566
+ const existingRows = this.db.prepare("select entry_id from memory_entries").all();
567
+ for (const row of existingRows) {
568
+ if (seenEntryIds.has(row.entry_id)) continue;
569
+ const entry = this.db.prepare("select entry_signature from memory_entries where entry_id = ?").get(row.entry_id);
570
+ this.markEmbeddingStateUnchecked(row.entry_id, entry?.entry_signature || null, {
571
+ status: "missing",
572
+ preserveVector: false
573
+ });
574
+ }
575
+
576
+ upsertMeta.run("updatedAt", normalizedUpdatedAt);
577
+ upsertMeta.run("config", JSON.stringify(config || null));
578
+ });
579
+
580
+ return {
581
+ updatedAt: normalizedUpdatedAt,
582
+ config,
583
+ items
584
+ };
585
+ }
586
+
587
+ getEmbeddingIndex() {
588
+ const rows = this.db
589
+ .prepare(`
590
+ select entry_id, entry_signature, entry_json, vector_json, updated_at, provider, model, dimensions, freshness_status, embedded_signature
591
+ from memory_embeddings
592
+ order by entry_id asc
593
+ `)
594
+ .all();
595
+ const updatedAt = this.db.prepare("select value from embedding_meta where key = 'updatedAt'").get()?.value || null;
596
+ const configRaw = this.db.prepare("select value from embedding_meta where key = 'config'").get()?.value || "null";
597
+
598
+ return {
599
+ updatedAt,
600
+ config: JSON.parse(configRaw),
601
+ items: rows.map((row) => ({
602
+ entryId: row.entry_id,
603
+ signature: row.entry_signature,
604
+ item: JSON.parse(row.entry_json),
605
+ vector: JSON.parse(row.vector_json),
606
+ updatedAt: row.updated_at,
607
+ provider: row.provider,
608
+ model: row.model,
609
+ dimensions: row.dimensions,
610
+ freshnessStatus: row.freshness_status,
611
+ embeddedSignature: row.embedded_signature
612
+ }))
613
+ };
614
+ }
615
+
616
+ getEmbeddingHealth() {
617
+ const freshnessRows = this.db
618
+ .prepare(`
619
+ select freshness_status, count(*) as count
620
+ from memory_embeddings
621
+ group by freshness_status
622
+ `)
623
+ .all();
624
+ const freshness = Object.fromEntries(
625
+ freshnessRows.map((row) => [row.freshness_status, Number(row.count || 0)])
626
+ );
627
+ const lastError = this.getRuntimeMeta("embedding_last_error");
628
+ const lastErrorAt = this.getRuntimeMeta("embedding_last_error_at");
629
+ const lastProvider = this.getRuntimeMeta("embedding_last_provider");
630
+ const lastModel = this.getRuntimeMeta("embedding_last_model");
631
+ const lastSuccessAt = this.getRuntimeMeta("embedding_last_success_at");
632
+ const lastMode = this.getRuntimeMeta("embedding_last_query_mode");
633
+
634
+ return {
635
+ totalTrackedCount: freshnessRows.reduce((sum, row) => sum + Number(row.count || 0), 0),
636
+ freshCount: freshness.fresh || 0,
637
+ staleCount: freshness.stale || 0,
638
+ missingCount: freshness.missing || 0,
639
+ failedCount: freshness.failed || 0,
640
+ lastError,
641
+ lastErrorAt,
642
+ lastProvider,
643
+ lastModel,
644
+ lastSuccessAt,
645
+ lastQueryMode: lastMode,
646
+ degraded: Boolean(lastError) || (freshness.fresh || 0) === 0
647
+ };
648
+ }
649
+
650
+ queryEmbeddingSimilarities(queryVector, limit = 8, options = {}) {
651
+ const nowIso = options.nowIso || null;
652
+ const candidateEntryIds = options.candidateEntryIds || null;
653
+ const params = [];
654
+ let sql = `
655
+ select e.entry_id, e.entry_signature, e.type, e.category, e.title, e.content, e.summary, e.reason, e.timestamp, e.salience, e.access_count, e.expires_at, e.source_kind, e.dedupe_key,
656
+ m.vector_json, m.freshness_status, m.updated_at, m.provider, m.model, m.dimensions
657
+ from memory_embeddings m
658
+ join memory_entries e on e.entry_id = m.entry_id
659
+ where m.freshness_status = 'fresh'
660
+ `;
661
+ if (nowIso) {
662
+ sql += " and (e.expires_at is null or e.expires_at >= ?)";
663
+ params.push(nowIso);
664
+ }
665
+ if (candidateEntryIds?.length) {
666
+ sql += ` and e.entry_id in (${candidateEntryIds.map(() => "?").join(", ")})`;
667
+ params.push(...candidateEntryIds);
668
+ }
669
+ const rows = this.db.prepare(sql).all(...params);
670
+
671
+ return rows
672
+ .map((row) => ({
673
+ entryId: row.entry_id,
674
+ signature: row.entry_signature,
675
+ similarity: cosineSimilarity(queryVector, JSON.parse(row.vector_json)),
676
+ freshnessStatus: row.freshness_status,
677
+ item: fromRow(row)
678
+ }))
679
+ .sort((a, b) => b.similarity - a.similarity)
680
+ .slice(0, limit);
681
+ }
682
+
683
+ getEmbeddingState(entryId) {
684
+ return this.db
685
+ .prepare(`
686
+ select entry_id, entry_signature, updated_at, provider, model, dimensions, freshness_status, embedded_signature
687
+ from memory_embeddings where entry_id = ?
688
+ `)
689
+ .get(entryId) || null;
690
+ }
691
+
692
+ queryRetrievalCandidates(query, options = {}) {
693
+ const nowIso = options.nowIso || null;
694
+ const limit = Math.max(Number(options.limit ?? 8), 1);
695
+ const candidateMultiplier = Math.max(Number(options.candidateMultiplier ?? 6), 2);
696
+ const lexicalLimit = Math.max(limit * candidateMultiplier, 24);
697
+ const queryVector = Array.isArray(options.queryVector) ? options.queryVector : null;
698
+
699
+ const lexicalRows = this.queryCandidates(query, lexicalLimit, nowIso);
700
+ const lexicalMap = new Map(lexicalRows.map((item, index) => [item.entryId, { item, lexicalRank: index }]));
701
+ const semanticRows = queryVector
702
+ ? this.queryEmbeddingSimilarities(queryVector, lexicalLimit, {
703
+ nowIso,
704
+ candidateEntryIds: lexicalRows.map((item) => item.entryId)
705
+ })
706
+ : [];
707
+ const semanticMap = new Map(semanticRows.map((row, index) => [row.entryId, { ...row, semanticRank: index }]));
708
+
709
+ const candidateEntryIds = new Set([...lexicalMap.keys(), ...semanticMap.keys()]);
710
+ const results = [];
711
+
712
+ for (const entryId of candidateEntryIds) {
713
+ const lexical = lexicalMap.get(entryId) || null;
714
+ const semantic = semanticMap.get(entryId) || null;
715
+ const baseItem = semantic?.item || lexical?.item;
716
+ if (!baseItem) continue;
717
+ const embeddingState = this.getEmbeddingState(entryId);
718
+ results.push({
719
+ item: baseItem,
720
+ lexicalMatch: Boolean(lexical),
721
+ lexicalRank: lexical?.lexicalRank ?? null,
722
+ embeddingSimilarity: semantic?.similarity ?? 0,
723
+ semanticRank: semantic?.semanticRank ?? null,
724
+ embeddingFreshness: embeddingState?.freshness_status || "missing",
725
+ embeddingProvider: embeddingState?.provider || null,
726
+ embeddingModel: embeddingState?.model || null,
727
+ retrievalSources: [
728
+ lexical ? "lexical" : null,
729
+ semantic ? "semantic" : null
730
+ ].filter(Boolean)
731
+ });
732
+ }
733
+
734
+ return results;
735
+ }
736
+
737
+ markEmbeddingState(entryId, entrySignature, options = {}) {
738
+ return this.runTransaction(() => this.markEmbeddingStateUnchecked(entryId, entrySignature, options));
739
+ }
740
+
741
+ markEmbeddingStateUnchecked(entryId, entrySignature, options = {}) {
742
+ const existing = this.db.prepare("select * from memory_embeddings where entry_id = ?").get(entryId);
743
+ const status = options.status || "missing";
744
+ const preserveVector = options.preserveVector ?? status === "fresh";
745
+ const updatedAt = options.updatedAt || existing?.updated_at || null;
746
+ const provider = options.provider || existing?.provider || null;
747
+ const model = options.model || existing?.model || null;
748
+ const dimensions = options.dimensions ?? existing?.dimensions ?? null;
749
+ const embeddedSignature =
750
+ status === "fresh"
751
+ ? entrySignature
752
+ : options.embeddedSignature ?? (preserveVector ? existing?.embedded_signature : null);
753
+ const entryJson = options.entryJson ?? existing?.entry_json ?? JSON.stringify({});
754
+ const vectorJson = preserveVector ? existing?.vector_json || JSON.stringify([]) : JSON.stringify([]);
755
+
756
+ this.db.prepare(`
757
+ insert into memory_embeddings (
758
+ entry_id, entry_signature, entry_json, vector_json, updated_at, provider, model, dimensions, freshness_status, embedded_signature
759
+ ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
760
+ on conflict(entry_id) do update set
761
+ entry_signature = excluded.entry_signature,
762
+ entry_json = excluded.entry_json,
763
+ vector_json = excluded.vector_json,
764
+ updated_at = excluded.updated_at,
765
+ provider = excluded.provider,
766
+ model = excluded.model,
767
+ dimensions = excluded.dimensions,
768
+ freshness_status = excluded.freshness_status,
769
+ embedded_signature = excluded.embedded_signature
770
+ `).run(
771
+ entryId,
772
+ entrySignature,
773
+ entryJson,
774
+ vectorJson,
775
+ updatedAt,
776
+ provider,
777
+ model,
778
+ dimensions,
779
+ status,
780
+ embeddedSignature
781
+ );
782
+ }
783
+
784
+ setRuntimeMeta(key, value) {
785
+ this.db.prepare(`
786
+ insert into runtime_meta(key, value) values (?, ?)
787
+ on conflict(key) do update set value = excluded.value
788
+ `).run(String(key), value == null ? null : String(value));
789
+ }
790
+
791
+ getRuntimeMeta(key) {
792
+ const row = this.db.prepare("select value from runtime_meta where key = ?").get(String(key));
793
+ return row?.value ?? null;
794
+ }
795
+
796
+ clearRuntimeMeta(key) {
797
+ this.db.prepare("delete from runtime_meta where key = ?").run(String(key));
798
+ }
799
+
800
+ tryAcquireLock(lockKey, { ownerId, acquiredAt, expiresAt }) {
801
+ return this.runTransaction(() => {
802
+ const existing = this.db.prepare("select * from memory_locks where lock_key = ?").get(lockKey);
803
+ if (!existing) {
804
+ this.db
805
+ .prepare("insert into memory_locks(lock_key, owner_id, acquired_at, expires_at) values (?, ?, ?, ?)")
806
+ .run(lockKey, ownerId, acquiredAt, expiresAt);
807
+ return true;
808
+ }
809
+
810
+ if (existing.owner_id === ownerId || existing.expires_at <= acquiredAt) {
811
+ this.db
812
+ .prepare("update memory_locks set owner_id = ?, acquired_at = ?, expires_at = ? where lock_key = ?")
813
+ .run(ownerId, acquiredAt, expiresAt, lockKey);
814
+ return true;
815
+ }
816
+
817
+ return false;
818
+ });
819
+ }
820
+
821
+ releaseLock(lockKey, ownerId) {
822
+ this.db.prepare("delete from memory_locks where lock_key = ? and owner_id = ?").run(lockKey, ownerId);
823
+ }
824
+ }