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,648 +1,648 @@
1
- import path from "node:path";
2
- import { formatRuntimeError, RuntimeError } from "../utils/errors.js";
3
- import { appendJsonLine, ensureDir, listFiles, readJson, readJsonLines, removePath, writeJson } from "../utils/fs.js";
4
- import { DEFAULT_RETRIEVAL_BLEND, computeLexicalScore, scoreMemory } from "./scoring.js";
5
- import { evaluateDurableWrite } from "./write-policy.js";
6
- import { SqliteActiveStore, buildDuplicateSignature } from "./sqlite-active-store.js";
7
- import { buildResultSignature } from "./memory-signature.js";
8
- import { createRuntimeId } from "../utils/ids.js";
9
- import { computeSimhash, simhashToHex } from "./simhash.js";
10
-
11
- function nowIso() {
12
- return new Date().toISOString();
13
- }
14
-
15
- // --- Secret write guard ---
16
- const SECRET_PATTERN = /^(sk|key|token|secret|password)[_-][a-zA-Z0-9]{20,}/i;
17
- const KNOWN_PREFIXES = [
18
- /\bsk-ant-[a-zA-Z0-9_-]{10,}/,
19
- /\bsk-[a-zA-Z0-9]{20,}/,
20
- /\bkey-[a-zA-Z0-9]{20,}/
21
- ];
22
- const ENV_REF_PATTERN = /^\$\{?\w+\}?$/;
23
-
24
- export function containsPlaintextSecret(value) {
25
- if (value == null) return false;
26
- if (typeof value === "object") {
27
- return Object.values(value).some((v) => containsPlaintextSecret(v));
28
- }
29
- if (typeof value !== "string") return false;
30
- const trimmed = value.trim();
31
- if (ENV_REF_PATTERN.test(trimmed)) return false;
32
- if (SECRET_PATTERN.test(trimmed)) return true;
33
- for (const re of KNOWN_PREFIXES) {
34
- if (re.test(trimmed)) return true;
35
- }
36
- return false;
37
- }
38
-
39
- function guardAgainstSecrets(record) {
40
- if (containsPlaintextSecret(record)) {
41
- const err = new RuntimeError(
42
- "Rejected: plaintext secret detected in memory write. Use env-backed SecretRef pattern instead.",
43
- { category: "security", context: { guard: "secret_write" }, recoverable: false }
44
- );
45
- throw new Error(formatRuntimeError(err));
46
- }
47
- }
48
-
49
- // --- Cross-agent memory isolation ---
50
- function guardAgentAccess(storeAgentId, requestedAgentId, options = {}) {
51
- if (storeAgentId == null) return; // no owner configured — unrestricted
52
- if (requestedAgentId === storeAgentId) return;
53
- if (options.allowCrossAgentRead) return;
54
- const err = new RuntimeError(
55
- `Rejected: cross-agent memory access denied. Agent '${requestedAgentId}' cannot access memory of agent '${storeAgentId}'.`,
56
- { category: "memory", context: { storeAgentId, requestedAgentId }, recoverable: false }
57
- );
58
- throw new Error(formatRuntimeError(err));
59
- }
60
-
61
- // --- Protected path guard (inspired by Codex security model) ---
62
- const PROTECTED_PATH_PATTERNS = [
63
- /\bconfig\//,
64
- /\bTRUST_POLICY\.md\b/i,
65
- /\bruntime\.toml\b/i,
66
- /(?:^|[\s/])\.github\//,
67
- /\bpackage\.json\b/i,
68
- /\bpackage-lock\.json\b/i,
69
- /(?:^|[\s/])\.env\b/
70
- ];
71
-
72
- export function isProtectedPath(value) {
73
- if (value == null) return false;
74
- const str = String(value);
75
- return PROTECTED_PATH_PATTERNS.some((re) => re.test(str));
76
- }
77
-
78
- const PATH_LIKE_FIELDS = new Set(["title", "filePath", "path", "target", "ref"]);
79
-
80
- function guardProtectedPaths(record) {
81
- if (record == null) return;
82
- if (typeof record === "string") {
83
- if (isProtectedPath(record)) throwProtectedPathError(record);
84
- return;
85
- }
86
- if (typeof record !== "object") return;
87
- for (const [key, v] of Object.entries(record)) {
88
- if (typeof v !== "string") continue;
89
- // Only check path-like fields and short values that look like file references
90
- if (PATH_LIKE_FIELDS.has(key) || v.length < 120) {
91
- if (isProtectedPath(v)) throwProtectedPathError(v);
92
- }
93
- }
94
- }
95
-
96
- function throwProtectedPathError(path) {
97
- const err = new RuntimeError(
98
- `Rejected: memory write references protected path "${path}". Config, trust policy, and environment files are immutable from agent operations.`,
99
- { category: "security", context: { guard: "protected_path", path }, recoverable: false }
100
- );
101
- throw new Error(formatRuntimeError(err));
102
- }
103
-
104
- function sleep(ms) {
105
- return new Promise((resolve) => setTimeout(resolve, ms));
106
- }
107
-
108
- function basePaths(rootDir, agentId) {
109
- const agentRoot = path.join(rootDir, agentId);
110
- return {
111
- agentRoot,
112
- eventsFile: path.join(agentRoot, "events.jsonl"),
113
- factsFile: path.join(agentRoot, "facts.jsonl"),
114
- summariesFile: path.join(agentRoot, "summaries.jsonl"),
115
- scratchpadsDir: path.join(agentRoot, "scratchpads"),
116
- checkpointsDir: path.join(agentRoot, "checkpoints"),
117
- countersFile: path.join(agentRoot, "counters.json"),
118
- sqliteFile: path.join(agentRoot, "memory.sqlite"),
119
- lockDir: path.join(agentRoot, ".lock")
120
- };
121
- }
122
-
123
- export class MemoryStore {
124
- constructor({ rootDir, agentId = null, lockConfig = {}, embeddingIndex = null, embeddingConfig = null }) {
125
- this.rootDir = rootDir;
126
- this.agentId = agentId;
127
- this.sqliteStores = new Map();
128
- this.embeddingIndex = embeddingIndex;
129
- this.embeddingConfig = embeddingConfig || {};
130
- this.lockConfig = {
131
- ttlMs: Number(lockConfig.ttlMs ?? 15000),
132
- retryDelayMs: Number(lockConfig.retryDelayMs ?? 25),
133
- maxRetries: Number(lockConfig.maxRetries ?? 40)
134
- };
135
- }
136
-
137
- /**
138
- * Fire-and-forget: embed newly written entries if index_on_write is enabled.
139
- * Never blocks the write path. Logs warnings on failure.
140
- */
141
- _maybeIndexOnWrite(agentId, records) {
142
- if (!this.embeddingIndex) return;
143
- if (!this.embeddingConfig.indexOnWrite) return;
144
- const items = Array.isArray(records) ? records : [records];
145
- const valid = items.filter((r) => r && r.type && !r.skipped);
146
- if (valid.length === 0) return;
147
-
148
- // Fire-and-forget — do not await
149
- this.embeddingIndex.rebuild(agentId, valid).catch((err) => {
150
- console.warn(`[nemoris] index-on-write failed for ${agentId}: ${err?.message || err}`);
151
- });
152
- }
153
-
154
- async initAgent(agentId) {
155
- const paths = basePaths(this.rootDir, agentId);
156
- await ensureDir(paths.agentRoot);
157
- await ensureDir(paths.scratchpadsDir);
158
- await ensureDir(paths.checkpointsDir);
159
- const counters = await readJson(paths.countersFile, null);
160
- if (!counters) {
161
- await writeJson(paths.countersFile, { durableWrites: 0, reads: 0 });
162
- }
163
- return paths;
164
- }
165
-
166
- getSqliteStore(paths) {
167
- if (!this.sqliteStores.has(paths.sqliteFile)) {
168
- this.sqliteStores.set(paths.sqliteFile, new SqliteActiveStore({ dbPath: paths.sqliteFile }));
169
- }
170
- return this.sqliteStores.get(paths.sqliteFile);
171
- }
172
-
173
- async rebuildSqliteStore(paths) {
174
- const [facts, summaries, events] = await Promise.all([
175
- readJsonLines(paths.factsFile),
176
- readJsonLines(paths.summariesFile),
177
- readJsonLines(paths.eventsFile)
178
- ]);
179
- const scratchpads = await this.readScratchpadsFromFiles(paths);
180
- const sqlite = this.getSqliteStore(paths);
181
- sqlite.clearAll();
182
- const seenFactSignatures = new Set();
183
- const seenSummarySignatures = new Set();
184
- const seenEventDedupeKeys = new Set();
185
-
186
- for (const record of facts) {
187
- const duplicateSignature = buildDuplicateSignature(record, "fact");
188
- if (duplicateSignature && seenFactSignatures.has(duplicateSignature)) continue;
189
- sqlite.insert(record, {
190
- duplicateSignature
191
- });
192
- if (duplicateSignature) seenFactSignatures.add(duplicateSignature);
193
- }
194
- for (const record of summaries) {
195
- const duplicateSignature = buildDuplicateSignature(record, "summary");
196
- if (duplicateSignature && seenSummarySignatures.has(duplicateSignature)) continue;
197
- sqlite.insert(record, {
198
- duplicateSignature
199
- });
200
- if (duplicateSignature) seenSummarySignatures.add(duplicateSignature);
201
- }
202
- for (const record of events) {
203
- if (record.dedupeKey) {
204
- if (seenEventDedupeKeys.has(record.dedupeKey)) continue;
205
- seenEventDedupeKeys.add(record.dedupeKey);
206
- }
207
- sqlite.insert(record);
208
- }
209
- for (const { scratchpadId, record } of scratchpads) {
210
- sqlite.upsertScratchpad(scratchpadId, record);
211
- }
212
-
213
- return sqlite;
214
- }
215
-
216
- async ensureSqliteStore(paths) {
217
- const sqlite = this.getSqliteStore(paths);
218
- if (sqlite.count() === 0) {
219
- await this.rebuildSqliteStore(paths);
220
- }
221
- return sqlite;
222
- }
223
-
224
- async readScratchpadsFromFiles(paths) {
225
- const files = await listFiles(paths.scratchpadsDir);
226
- return Promise.all(
227
- files.map(async (fileName) => ({
228
- scratchpadId: path.basename(fileName, ".json"),
229
- record: await readJson(path.join(paths.scratchpadsDir, fileName), null),
230
- filePath: path.join(paths.scratchpadsDir, fileName)
231
- }))
232
- );
233
- }
234
-
235
- async appendEvent(agentId, event, options = {}) {
236
- guardAgentAccess(this.agentId, agentId, options);
237
- const paths = await this.initAgent(agentId);
238
- return this.withAgentLock(paths, "memory-write", async () => {
239
- const sqlite = await this.ensureSqliteStore(paths);
240
- const record = {
241
- type: "event",
242
- timestamp: nowIso(),
243
- salience: 0.35,
244
- ...event
245
- };
246
- if (event.dedupeKey && sqlite.hasEventDedupe(event.dedupeKey)) {
247
- return { skipped: true, record: null };
248
- }
249
- await appendJsonLine(paths.eventsFile, record);
250
- sqlite.insert(record);
251
- return record;
252
- });
253
- }
254
-
255
- async writeSummary(agentId, summary, options = {}) {
256
- guardAgentAccess(this.agentId, agentId, options);
257
- const paths = await this.initAgent(agentId);
258
- return this.withAgentLock(paths, "memory-write", async () => {
259
- const sqlite = await this.ensureSqliteStore(paths);
260
- guardAgainstSecrets(summary);
261
- guardProtectedPaths(summary);
262
- const record = {
263
- type: "summary",
264
- timestamp: nowIso(),
265
- salience: 0.7,
266
- accessCount: 0,
267
- ...summary
268
- };
269
- const duplicateSignature = buildDuplicateSignature(record, "summary");
270
- if (sqlite.hasDuplicateSignature("summary", duplicateSignature)) {
271
- return { skipped: true, record: null };
272
- }
273
- // Simhash near-duplicate check
274
- const hash = computeSimhash((summary.content || "") + " " + (summary.title || ""));
275
- const simhashHex = simhashToHex(hash);
276
- const nearDupes = sqlite.findNearDuplicates("summary", simhashHex, 3);
277
- if (nearDupes.length > 0) {
278
- const best = nearDupes[0];
279
- sqlite.boostSalience(best.entryId, 0.15);
280
- return { accepted: true, skipped: true, nearDuplicate: true, boostedEntryId: best.entryId, record: null };
281
- }
282
- record.simhash = simhashHex;
283
- await appendJsonLine(paths.summariesFile, record);
284
- sqlite.insert(record, { duplicateSignature });
285
- this._maybeIndexOnWrite(agentId, record);
286
- return record;
287
- });
288
- }
289
-
290
- async writeFact(agentId, fact, policy, options = {}) {
291
- guardAgentAccess(this.agentId, agentId, options);
292
- const paths = await this.initAgent(agentId);
293
- const writesThisRun = options.writesThisRun ?? 0;
294
- return this.withAgentLock(paths, "memory-write", async () => {
295
- guardAgainstSecrets(fact);
296
- guardProtectedPaths(fact);
297
- const counters = await readJson(paths.countersFile, { durableWrites: 0, reads: 0 });
298
- const sqlite = await this.ensureSqliteStore(paths);
299
- const evaluation = evaluateDurableWrite(fact, policy, writesThisRun);
300
- if (!evaluation.accepted) {
301
- return {
302
- accepted: false,
303
- reasons: evaluation.reasons
304
- };
305
- }
306
-
307
- const record = {
308
- type: "fact",
309
- timestamp: nowIso(),
310
- salience: 0.8,
311
- accessCount: 0,
312
- ...fact
313
- };
314
- const duplicateSignature = buildDuplicateSignature(record, "fact");
315
- if (sqlite.hasDuplicateSignature("fact", duplicateSignature)) {
316
- return {
317
- accepted: true,
318
- skipped: true,
319
- record: null
320
- };
321
- }
322
- // Simhash near-duplicate check
323
- const hash = computeSimhash((fact.content || "") + " " + (fact.title || ""));
324
- const simhashHex = simhashToHex(hash);
325
- const nearDupes = sqlite.findNearDuplicates("fact", simhashHex, 3);
326
- if (nearDupes.length > 0) {
327
- const best = nearDupes[0];
328
- sqlite.boostSalience(best.entryId, 0.15);
329
- return {
330
- accepted: true,
331
- skipped: true,
332
- nearDuplicate: true,
333
- boostedEntryId: best.entryId,
334
- record: null
335
- };
336
- }
337
- record.simhash = simhashHex;
338
- await appendJsonLine(paths.factsFile, record);
339
- sqlite.insert(record, { duplicateSignature });
340
- await writeJson(paths.countersFile, {
341
- ...counters,
342
- durableWrites: counters.durableWrites + 1
343
- });
344
- this._maybeIndexOnWrite(agentId, record);
345
- return {
346
- accepted: true,
347
- record
348
- };
349
- });
350
- }
351
-
352
- async putScratchpad(agentId, scratchpadId, scratchpad, options = {}) {
353
- guardAgentAccess(this.agentId, agentId, options);
354
- const paths = await this.initAgent(agentId);
355
- return this.withAgentLock(paths, "memory-write", async () => {
356
- const sqlite = await this.ensureSqliteStore(paths);
357
- const record = {
358
- type: "scratchpad",
359
- timestamp: nowIso(),
360
- expiresAt: scratchpad.expiresAt,
361
- salience: scratchpad.salience ?? 0.5,
362
- ...scratchpad
363
- };
364
- const filePath = path.join(paths.scratchpadsDir, `${scratchpadId}.json`);
365
- await writeJson(filePath, record);
366
- sqlite.upsertScratchpad(scratchpadId, record);
367
- return record;
368
- });
369
- }
370
-
371
- async getScratchpads(agentId, now = Date.now(), options = {}) {
372
- guardAgentAccess(this.agentId, agentId, options);
373
- const paths = await this.initAgent(agentId);
374
- const sqlite = await this.ensureSqliteStore(paths);
375
- const nowIsoValue = new Date(now).toISOString();
376
- const expiredEntryIds = sqlite.listExpiredScratchpads(nowIsoValue);
377
-
378
- if (expiredEntryIds.length) {
379
- await this.withAgentLock(paths, "memory-write", async () => {
380
- for (const entryId of expiredEntryIds) {
381
- const scratchpadId = entryId.replace(/^scratchpad:/, "");
382
- const filePath = path.join(paths.scratchpadsDir, `${scratchpadId}.json`);
383
- sqlite.deleteByEntryId(entryId);
384
- await removePath(filePath);
385
- }
386
- });
387
- }
388
- return sqlite
389
- .listAll(nowIsoValue)
390
- .filter((entry) => entry.type === "scratchpad");
391
- }
392
-
393
- async saveCheckpoint(agentId, checkpointId, checkpoint, options = {}) {
394
- guardAgentAccess(this.agentId, agentId, options);
395
- const paths = await this.initAgent(agentId);
396
- const record = {
397
- timestamp: nowIso(),
398
- ...checkpoint
399
- };
400
- await writeJson(path.join(paths.checkpointsDir, `${checkpointId}.json`), record);
401
- return record;
402
- }
403
-
404
- async loadCheckpoint(agentId, checkpointId = "latest", options = {}) {
405
- guardAgentAccess(this.agentId, agentId, options);
406
- const paths = await this.initAgent(agentId);
407
- return readJson(path.join(paths.checkpointsDir, `${checkpointId}.json`), null);
408
- }
409
-
410
- async listAll(agentId, now = Date.now(), options = {}) {
411
- guardAgentAccess(this.agentId, agentId, options);
412
- const paths = await this.initAgent(agentId);
413
- await this.getScratchpads(agentId, now, { allowCrossAgentRead: true });
414
- const sqlite = await this.ensureSqliteStore(paths);
415
- return sqlite.listAll(new Date(now).toISOString());
416
- }
417
-
418
- async sqliteStatus(agentId, options = {}) {
419
- guardAgentAccess(this.agentId, agentId, options);
420
- const paths = await this.initAgent(agentId);
421
- const sqlite = await this.ensureSqliteStore(paths);
422
- return sqlite.getWalStatus(options.mode || "passive");
423
- }
424
-
425
- async manageSqlite(agentId, options = {}) {
426
- guardAgentAccess(this.agentId, agentId, options);
427
- const paths = await this.initAgent(agentId);
428
- const sqlite = await this.ensureSqliteStore(paths);
429
- return sqlite.manageWal(options);
430
- }
431
-
432
- async getEmbeddingHealth(agentId, options = {}) {
433
- guardAgentAccess(this.agentId, agentId, options);
434
- const paths = await this.initAgent(agentId);
435
- const sqlite = await this.ensureSqliteStore(paths);
436
- let probe = null;
437
-
438
- if (options.probe && options.embeddingIndex) {
439
- try {
440
- const embedded = await options.embeddingIndex.embedQuery(options.probeText || "nemoris embedding health probe");
441
- if (embedded?.config?.provider) sqlite.setRuntimeMeta("embedding_last_provider", embedded.config.provider);
442
- if (embedded?.config?.model) sqlite.setRuntimeMeta("embedding_last_model", embedded.config.model);
443
- sqlite.setRuntimeMeta("embedding_last_success_at", nowIso());
444
- sqlite.setRuntimeMeta("embedding_last_query_mode", embedded?.vector ? "embedding_probe" : "lexical_only");
445
- sqlite.clearRuntimeMeta("embedding_last_error");
446
- sqlite.clearRuntimeMeta("embedding_last_error_at");
447
- probe = {
448
- ok: true,
449
- dimensions: Array.isArray(embedded?.vector) ? embedded.vector.length : 0,
450
- config: embedded?.config || null
451
- };
452
- } catch (error) {
453
- const message = error?.message || String(error);
454
- sqlite.setRuntimeMeta("embedding_last_error", message);
455
- sqlite.setRuntimeMeta("embedding_last_error_at", nowIso());
456
- sqlite.setRuntimeMeta("embedding_last_query_mode", "embedding_probe_failed");
457
- probe = {
458
- ok: false,
459
- error: message
460
- };
461
- }
462
- }
463
-
464
- return {
465
- agentId,
466
- totalEntries: sqlite.count(),
467
- embeddingHealth: sqlite.getEmbeddingHealth(),
468
- probe
469
- };
470
- }
471
-
472
- async rebuildEmbeddings(agentId, options = {}) {
473
- guardAgentAccess(this.agentId, agentId, options);
474
- if (!options.embeddingIndex) {
475
- throw new Error("No embedding index configured for rebuild");
476
- }
477
-
478
- const paths = await this.initAgent(agentId);
479
- const sqlite = await this.ensureSqliteStore(paths);
480
- const items = await this.listAll(agentId);
481
-
482
- try {
483
- const result = await options.embeddingIndex.rebuild(agentId, items);
484
- if (result?.config?.provider) sqlite.setRuntimeMeta("embedding_last_provider", result.config.provider);
485
- if (result?.config?.model) sqlite.setRuntimeMeta("embedding_last_model", result.config.model);
486
- sqlite.setRuntimeMeta("embedding_last_success_at", nowIso());
487
- sqlite.setRuntimeMeta("embedding_last_query_mode", "embedding_rebuild");
488
- sqlite.clearRuntimeMeta("embedding_last_error");
489
- sqlite.clearRuntimeMeta("embedding_last_error_at");
490
-
491
- return {
492
- agentId,
493
- itemCount: items.length,
494
- updatedAt: result.updatedAt,
495
- config: result.config,
496
- embeddingHealth: sqlite.getEmbeddingHealth()
497
- };
498
- } catch (error) {
499
- const message = error?.message || String(error);
500
- sqlite.setRuntimeMeta("embedding_last_error", message);
501
- sqlite.setRuntimeMeta("embedding_last_error_at", nowIso());
502
- sqlite.setRuntimeMeta("embedding_last_query_mode", "embedding_rebuild_failed");
503
- throw error;
504
- }
505
- }
506
-
507
- async query(agentId, query, options = {}) {
508
- guardAgentAccess(this.agentId, agentId, options);
509
- const limit = options.limit ?? 8;
510
- const now = options.now ?? Date.now();
511
- const retrievalBlend = {
512
- ...DEFAULT_RETRIEVAL_BLEND,
513
- ...(options.retrievalBlend || {})
514
- };
515
- const paths = await this.initAgent(agentId);
516
- await this.getScratchpads(agentId, now, { allowCrossAgentRead: true });
517
- const sqlite = await this.ensureSqliteStore(paths);
518
- let queryVector = null;
519
- let embeddingConfig = null;
520
- let embeddingError = null;
521
- let embeddingQueryMode = "lexical_only";
522
- if (options.embeddingIndex) {
523
- try {
524
- const embedded = await options.embeddingIndex.embedQuery(query);
525
- queryVector = embedded.vector;
526
- embeddingConfig = embedded.config || null;
527
- embeddingQueryMode = queryVector ? "embedding_query" : "lexical_only";
528
- if (embeddingConfig?.provider) sqlite.setRuntimeMeta("embedding_last_provider", embeddingConfig.provider);
529
- if (embeddingConfig?.model) sqlite.setRuntimeMeta("embedding_last_model", embeddingConfig.model);
530
- sqlite.setRuntimeMeta("embedding_last_success_at", nowIso());
531
- sqlite.setRuntimeMeta("embedding_last_query_mode", embeddingQueryMode);
532
- sqlite.clearRuntimeMeta("embedding_last_error");
533
- sqlite.clearRuntimeMeta("embedding_last_error_at");
534
- } catch (error) {
535
- queryVector = null;
536
- embeddingConfig = null;
537
- embeddingError = error?.message || String(error);
538
- embeddingQueryMode = "lexical_fallback";
539
- sqlite.setRuntimeMeta("embedding_last_error", embeddingError);
540
- sqlite.setRuntimeMeta("embedding_last_error_at", nowIso());
541
- sqlite.setRuntimeMeta("embedding_last_query_mode", embeddingQueryMode);
542
- }
543
- }
544
- const retrievalCandidates = sqlite.queryRetrievalCandidates(query, {
545
- nowIso: new Date(now).toISOString(),
546
- limit,
547
- queryVector
548
- });
549
-
550
- const ranked = retrievalCandidates
551
- .map((candidate) => ({
552
- ...candidate.item,
553
- ...scoreMemory(
554
- {
555
- ...candidate.item,
556
- lexicalScore: computeLexicalScore(
557
- query,
558
- [candidate.item.title, candidate.item.content, candidate.item.summary, candidate.item.reason, candidate.item.category]
559
- .filter(Boolean)
560
- .join(" ")
561
- ),
562
- embeddingSimilarity: candidate.embeddingSimilarity,
563
- embeddingFreshness: candidate.embeddingFreshness
564
- },
565
- query,
566
- now,
567
- retrievalBlend
568
- ),
569
- retrievalSources: candidate.retrievalSources,
570
- embeddingFreshness: candidate.embeddingFreshness,
571
- embeddingProvider: candidate.embeddingProvider,
572
- embeddingModel: candidate.embeddingModel,
573
- candidateSource: candidate.retrievalSources.includes("semantic") && candidate.retrievalSources.includes("lexical")
574
- ? "indexed+semantic"
575
- : candidate.retrievalSources.includes("semantic")
576
- ? "semantic"
577
- : "indexed"
578
- }))
579
- .sort((a, b) => b.score - a.score);
580
-
581
- const deduped = [];
582
- const seen = new Set();
583
- for (const item of ranked) {
584
- const key = buildResultSignature(item);
585
- if (seen.has(key)) continue;
586
- seen.add(key);
587
- deduped.push(item);
588
- if (deduped.length >= limit) break;
589
- }
590
-
591
- return {
592
- query,
593
- totalCandidates: sqlite.count(),
594
- indexedCandidates: retrievalCandidates.filter((item) => item.lexicalMatch).length,
595
- semanticCandidates: retrievalCandidates.filter((item) => item.embeddingSimilarity > 0).length,
596
- retrieval: {
597
- blend: retrievalBlend,
598
- embeddingQueryAvailable: Boolean(queryVector),
599
- embeddingConfig,
600
- embeddingQueryMode,
601
- embeddingError,
602
- embeddingHealth: sqlite.getEmbeddingHealth(),
603
- candidates: retrievalCandidates.map((candidate) => ({
604
- entryId: candidate.item.entryId,
605
- title: candidate.item.title,
606
- retrievalSources: candidate.retrievalSources,
607
- lexicalMatch: candidate.lexicalMatch,
608
- lexicalRank: candidate.lexicalRank,
609
- embeddingSimilarity: Number(candidate.embeddingSimilarity.toFixed(4)),
610
- embeddingFreshness: candidate.embeddingFreshness
611
- }))
612
- },
613
- items: deduped
614
- };
615
- }
616
-
617
- async withAgentLock(paths, lockKey, fn) {
618
- const sqlite = this.getSqliteStore(paths);
619
- const ownerId = createRuntimeId("memory-lock");
620
- const ttlMs = this.lockConfig.ttlMs;
621
- const retryDelayMs = this.lockConfig.retryDelayMs;
622
- const maxRetries = this.lockConfig.maxRetries;
623
-
624
- let acquired = false;
625
- for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
626
- const acquiredAt = new Date();
627
- const expiresAt = new Date(acquiredAt.getTime() + ttlMs);
628
- acquired = sqlite.tryAcquireLock(lockKey, {
629
- ownerId,
630
- acquiredAt: acquiredAt.toISOString(),
631
- expiresAt: expiresAt.toISOString()
632
- });
633
- if (acquired) break;
634
- if (attempt === maxRetries) {
635
- throw new Error(`Timed out waiting for SQLite memory lock ${lockKey}`);
636
- }
637
- await sleep(retryDelayMs);
638
- }
639
-
640
- try {
641
- return await fn();
642
- } finally {
643
- if (acquired) {
644
- sqlite.releaseLock(lockKey, ownerId);
645
- }
646
- }
647
- }
648
- }
1
+ import path from "node:path";
2
+ import { formatRuntimeError, RuntimeError } from "../utils/errors.js";
3
+ import { appendJsonLine, ensureDir, listFiles, readJson, readJsonLines, removePath, writeJson } from "../utils/fs.js";
4
+ import { DEFAULT_RETRIEVAL_BLEND, computeLexicalScore, scoreMemory } from "./scoring.js";
5
+ import { evaluateDurableWrite } from "./write-policy.js";
6
+ import { SqliteActiveStore, buildDuplicateSignature } from "./sqlite-active-store.js";
7
+ import { buildResultSignature } from "./memory-signature.js";
8
+ import { createRuntimeId } from "../utils/ids.js";
9
+ import { computeSimhash, simhashToHex } from "./simhash.js";
10
+
11
+ function nowIso() {
12
+ return new Date().toISOString();
13
+ }
14
+
15
+ // --- Secret write guard ---
16
+ const SECRET_PATTERN = /^(sk|key|token|secret|password)[_-][a-zA-Z0-9]{20,}/i;
17
+ const KNOWN_PREFIXES = [
18
+ /\bsk-ant-[a-zA-Z0-9_-]{10,}/,
19
+ /\bsk-[a-zA-Z0-9]{20,}/,
20
+ /\bkey-[a-zA-Z0-9]{20,}/
21
+ ];
22
+ const ENV_REF_PATTERN = /^\$\{?\w+\}?$/;
23
+
24
+ export function containsPlaintextSecret(value) {
25
+ if (value == null) return false;
26
+ if (typeof value === "object") {
27
+ return Object.values(value).some((v) => containsPlaintextSecret(v));
28
+ }
29
+ if (typeof value !== "string") return false;
30
+ const trimmed = value.trim();
31
+ if (ENV_REF_PATTERN.test(trimmed)) return false;
32
+ if (SECRET_PATTERN.test(trimmed)) return true;
33
+ for (const re of KNOWN_PREFIXES) {
34
+ if (re.test(trimmed)) return true;
35
+ }
36
+ return false;
37
+ }
38
+
39
+ function guardAgainstSecrets(record) {
40
+ if (containsPlaintextSecret(record)) {
41
+ const err = new RuntimeError(
42
+ "Rejected: plaintext secret detected in memory write. Use env-backed SecretRef pattern instead.",
43
+ { category: "security", context: { guard: "secret_write" }, recoverable: false }
44
+ );
45
+ throw new Error(formatRuntimeError(err));
46
+ }
47
+ }
48
+
49
+ // --- Cross-agent memory isolation ---
50
+ function guardAgentAccess(storeAgentId, requestedAgentId, options = {}) {
51
+ if (storeAgentId == null) return; // no owner configured — unrestricted
52
+ if (requestedAgentId === storeAgentId) return;
53
+ if (options.allowCrossAgentRead) return;
54
+ const err = new RuntimeError(
55
+ `Rejected: cross-agent memory access denied. Agent '${requestedAgentId}' cannot access memory of agent '${storeAgentId}'.`,
56
+ { category: "memory", context: { storeAgentId, requestedAgentId }, recoverable: false }
57
+ );
58
+ throw new Error(formatRuntimeError(err));
59
+ }
60
+
61
+ // --- Protected path guard (inspired by Codex security model) ---
62
+ const PROTECTED_PATH_PATTERNS = [
63
+ /\bconfig\//,
64
+ /\bTRUST_POLICY\.md\b/i,
65
+ /\bruntime\.toml\b/i,
66
+ /(?:^|[\s/])\.github\//,
67
+ /\bpackage\.json\b/i,
68
+ /\bpackage-lock\.json\b/i,
69
+ /(?:^|[\s/])\.env\b/
70
+ ];
71
+
72
+ export function isProtectedPath(value) {
73
+ if (value == null) return false;
74
+ const str = String(value);
75
+ return PROTECTED_PATH_PATTERNS.some((re) => re.test(str));
76
+ }
77
+
78
+ const PATH_LIKE_FIELDS = new Set(["title", "filePath", "path", "target", "ref"]);
79
+
80
+ function guardProtectedPaths(record) {
81
+ if (record == null) return;
82
+ if (typeof record === "string") {
83
+ if (isProtectedPath(record)) throwProtectedPathError(record);
84
+ return;
85
+ }
86
+ if (typeof record !== "object") return;
87
+ for (const [key, v] of Object.entries(record)) {
88
+ if (typeof v !== "string") continue;
89
+ // Only check path-like fields and short values that look like file references
90
+ if (PATH_LIKE_FIELDS.has(key) || v.length < 120) {
91
+ if (isProtectedPath(v)) throwProtectedPathError(v);
92
+ }
93
+ }
94
+ }
95
+
96
+ function throwProtectedPathError(path) {
97
+ const err = new RuntimeError(
98
+ `Rejected: memory write references protected path "${path}". Config, trust policy, and environment files are immutable from agent operations.`,
99
+ { category: "security", context: { guard: "protected_path", path }, recoverable: false }
100
+ );
101
+ throw new Error(formatRuntimeError(err));
102
+ }
103
+
104
+ function sleep(ms) {
105
+ return new Promise((resolve) => setTimeout(resolve, ms));
106
+ }
107
+
108
+ function basePaths(rootDir, agentId) {
109
+ const agentRoot = path.join(rootDir, agentId);
110
+ return {
111
+ agentRoot,
112
+ eventsFile: path.join(agentRoot, "events.jsonl"),
113
+ factsFile: path.join(agentRoot, "facts.jsonl"),
114
+ summariesFile: path.join(agentRoot, "summaries.jsonl"),
115
+ scratchpadsDir: path.join(agentRoot, "scratchpads"),
116
+ checkpointsDir: path.join(agentRoot, "checkpoints"),
117
+ countersFile: path.join(agentRoot, "counters.json"),
118
+ sqliteFile: path.join(agentRoot, "memory.sqlite"),
119
+ lockDir: path.join(agentRoot, ".lock")
120
+ };
121
+ }
122
+
123
+ export class MemoryStore {
124
+ constructor({ rootDir, agentId = null, lockConfig = {}, embeddingIndex = null, embeddingConfig = null }) {
125
+ this.rootDir = rootDir;
126
+ this.agentId = agentId;
127
+ this.sqliteStores = new Map();
128
+ this.embeddingIndex = embeddingIndex;
129
+ this.embeddingConfig = embeddingConfig || {};
130
+ this.lockConfig = {
131
+ ttlMs: Number(lockConfig.ttlMs ?? 15000),
132
+ retryDelayMs: Number(lockConfig.retryDelayMs ?? 25),
133
+ maxRetries: Number(lockConfig.maxRetries ?? 40)
134
+ };
135
+ }
136
+
137
+ /**
138
+ * Fire-and-forget: embed newly written entries if index_on_write is enabled.
139
+ * Never blocks the write path. Logs warnings on failure.
140
+ */
141
+ _maybeIndexOnWrite(agentId, records) {
142
+ if (!this.embeddingIndex) return;
143
+ if (!this.embeddingConfig.indexOnWrite) return;
144
+ const items = Array.isArray(records) ? records : [records];
145
+ const valid = items.filter((r) => r && r.type && !r.skipped);
146
+ if (valid.length === 0) return;
147
+
148
+ // Fire-and-forget — do not await
149
+ this.embeddingIndex.rebuild(agentId, valid).catch((err) => {
150
+ console.warn(`[nemoris] index-on-write failed for ${agentId}: ${err?.message || err}`);
151
+ });
152
+ }
153
+
154
+ async initAgent(agentId) {
155
+ const paths = basePaths(this.rootDir, agentId);
156
+ await ensureDir(paths.agentRoot);
157
+ await ensureDir(paths.scratchpadsDir);
158
+ await ensureDir(paths.checkpointsDir);
159
+ const counters = await readJson(paths.countersFile, null);
160
+ if (!counters) {
161
+ await writeJson(paths.countersFile, { durableWrites: 0, reads: 0 });
162
+ }
163
+ return paths;
164
+ }
165
+
166
+ getSqliteStore(paths) {
167
+ if (!this.sqliteStores.has(paths.sqliteFile)) {
168
+ this.sqliteStores.set(paths.sqliteFile, new SqliteActiveStore({ dbPath: paths.sqliteFile }));
169
+ }
170
+ return this.sqliteStores.get(paths.sqliteFile);
171
+ }
172
+
173
+ async rebuildSqliteStore(paths) {
174
+ const [facts, summaries, events] = await Promise.all([
175
+ readJsonLines(paths.factsFile),
176
+ readJsonLines(paths.summariesFile),
177
+ readJsonLines(paths.eventsFile)
178
+ ]);
179
+ const scratchpads = await this.readScratchpadsFromFiles(paths);
180
+ const sqlite = this.getSqliteStore(paths);
181
+ sqlite.clearAll();
182
+ const seenFactSignatures = new Set();
183
+ const seenSummarySignatures = new Set();
184
+ const seenEventDedupeKeys = new Set();
185
+
186
+ for (const record of facts) {
187
+ const duplicateSignature = buildDuplicateSignature(record, "fact");
188
+ if (duplicateSignature && seenFactSignatures.has(duplicateSignature)) continue;
189
+ sqlite.insert(record, {
190
+ duplicateSignature
191
+ });
192
+ if (duplicateSignature) seenFactSignatures.add(duplicateSignature);
193
+ }
194
+ for (const record of summaries) {
195
+ const duplicateSignature = buildDuplicateSignature(record, "summary");
196
+ if (duplicateSignature && seenSummarySignatures.has(duplicateSignature)) continue;
197
+ sqlite.insert(record, {
198
+ duplicateSignature
199
+ });
200
+ if (duplicateSignature) seenSummarySignatures.add(duplicateSignature);
201
+ }
202
+ for (const record of events) {
203
+ if (record.dedupeKey) {
204
+ if (seenEventDedupeKeys.has(record.dedupeKey)) continue;
205
+ seenEventDedupeKeys.add(record.dedupeKey);
206
+ }
207
+ sqlite.insert(record);
208
+ }
209
+ for (const { scratchpadId, record } of scratchpads) {
210
+ sqlite.upsertScratchpad(scratchpadId, record);
211
+ }
212
+
213
+ return sqlite;
214
+ }
215
+
216
+ async ensureSqliteStore(paths) {
217
+ const sqlite = this.getSqliteStore(paths);
218
+ if (sqlite.count() === 0) {
219
+ await this.rebuildSqliteStore(paths);
220
+ }
221
+ return sqlite;
222
+ }
223
+
224
+ async readScratchpadsFromFiles(paths) {
225
+ const files = await listFiles(paths.scratchpadsDir);
226
+ return Promise.all(
227
+ files.map(async (fileName) => ({
228
+ scratchpadId: path.basename(fileName, ".json"),
229
+ record: await readJson(path.join(paths.scratchpadsDir, fileName), null),
230
+ filePath: path.join(paths.scratchpadsDir, fileName)
231
+ }))
232
+ );
233
+ }
234
+
235
+ async appendEvent(agentId, event, options = {}) {
236
+ guardAgentAccess(this.agentId, agentId, options);
237
+ const paths = await this.initAgent(agentId);
238
+ return this.withAgentLock(paths, "memory-write", async () => {
239
+ const sqlite = await this.ensureSqliteStore(paths);
240
+ const record = {
241
+ type: "event",
242
+ timestamp: nowIso(),
243
+ salience: 0.35,
244
+ ...event
245
+ };
246
+ if (event.dedupeKey && sqlite.hasEventDedupe(event.dedupeKey)) {
247
+ return { skipped: true, record: null };
248
+ }
249
+ await appendJsonLine(paths.eventsFile, record);
250
+ sqlite.insert(record);
251
+ return record;
252
+ });
253
+ }
254
+
255
+ async writeSummary(agentId, summary, options = {}) {
256
+ guardAgentAccess(this.agentId, agentId, options);
257
+ const paths = await this.initAgent(agentId);
258
+ return this.withAgentLock(paths, "memory-write", async () => {
259
+ const sqlite = await this.ensureSqliteStore(paths);
260
+ guardAgainstSecrets(summary);
261
+ guardProtectedPaths(summary);
262
+ const record = {
263
+ type: "summary",
264
+ timestamp: nowIso(),
265
+ salience: 0.7,
266
+ accessCount: 0,
267
+ ...summary
268
+ };
269
+ const duplicateSignature = buildDuplicateSignature(record, "summary");
270
+ if (sqlite.hasDuplicateSignature("summary", duplicateSignature)) {
271
+ return { skipped: true, record: null };
272
+ }
273
+ // Simhash near-duplicate check
274
+ const hash = computeSimhash((summary.content || "") + " " + (summary.title || ""));
275
+ const simhashHex = simhashToHex(hash);
276
+ const nearDupes = sqlite.findNearDuplicates("summary", simhashHex, 3);
277
+ if (nearDupes.length > 0) {
278
+ const best = nearDupes[0];
279
+ sqlite.boostSalience(best.entryId, 0.15);
280
+ return { accepted: true, skipped: true, nearDuplicate: true, boostedEntryId: best.entryId, record: null };
281
+ }
282
+ record.simhash = simhashHex;
283
+ await appendJsonLine(paths.summariesFile, record);
284
+ sqlite.insert(record, { duplicateSignature });
285
+ this._maybeIndexOnWrite(agentId, record);
286
+ return record;
287
+ });
288
+ }
289
+
290
+ async writeFact(agentId, fact, policy, options = {}) {
291
+ guardAgentAccess(this.agentId, agentId, options);
292
+ const paths = await this.initAgent(agentId);
293
+ const writesThisRun = options.writesThisRun ?? 0;
294
+ return this.withAgentLock(paths, "memory-write", async () => {
295
+ guardAgainstSecrets(fact);
296
+ guardProtectedPaths(fact);
297
+ const counters = await readJson(paths.countersFile, { durableWrites: 0, reads: 0 });
298
+ const sqlite = await this.ensureSqliteStore(paths);
299
+ const evaluation = evaluateDurableWrite(fact, policy, writesThisRun);
300
+ if (!evaluation.accepted) {
301
+ return {
302
+ accepted: false,
303
+ reasons: evaluation.reasons
304
+ };
305
+ }
306
+
307
+ const record = {
308
+ type: "fact",
309
+ timestamp: nowIso(),
310
+ salience: 0.8,
311
+ accessCount: 0,
312
+ ...fact
313
+ };
314
+ const duplicateSignature = buildDuplicateSignature(record, "fact");
315
+ if (sqlite.hasDuplicateSignature("fact", duplicateSignature)) {
316
+ return {
317
+ accepted: true,
318
+ skipped: true,
319
+ record: null
320
+ };
321
+ }
322
+ // Simhash near-duplicate check
323
+ const hash = computeSimhash((fact.content || "") + " " + (fact.title || ""));
324
+ const simhashHex = simhashToHex(hash);
325
+ const nearDupes = sqlite.findNearDuplicates("fact", simhashHex, 3);
326
+ if (nearDupes.length > 0) {
327
+ const best = nearDupes[0];
328
+ sqlite.boostSalience(best.entryId, 0.15);
329
+ return {
330
+ accepted: true,
331
+ skipped: true,
332
+ nearDuplicate: true,
333
+ boostedEntryId: best.entryId,
334
+ record: null
335
+ };
336
+ }
337
+ record.simhash = simhashHex;
338
+ await appendJsonLine(paths.factsFile, record);
339
+ sqlite.insert(record, { duplicateSignature });
340
+ await writeJson(paths.countersFile, {
341
+ ...counters,
342
+ durableWrites: counters.durableWrites + 1
343
+ });
344
+ this._maybeIndexOnWrite(agentId, record);
345
+ return {
346
+ accepted: true,
347
+ record
348
+ };
349
+ });
350
+ }
351
+
352
+ async putScratchpad(agentId, scratchpadId, scratchpad, options = {}) {
353
+ guardAgentAccess(this.agentId, agentId, options);
354
+ const paths = await this.initAgent(agentId);
355
+ return this.withAgentLock(paths, "memory-write", async () => {
356
+ const sqlite = await this.ensureSqliteStore(paths);
357
+ const record = {
358
+ type: "scratchpad",
359
+ timestamp: nowIso(),
360
+ expiresAt: scratchpad.expiresAt,
361
+ salience: scratchpad.salience ?? 0.5,
362
+ ...scratchpad
363
+ };
364
+ const filePath = path.join(paths.scratchpadsDir, `${scratchpadId}.json`);
365
+ await writeJson(filePath, record);
366
+ sqlite.upsertScratchpad(scratchpadId, record);
367
+ return record;
368
+ });
369
+ }
370
+
371
+ async getScratchpads(agentId, now = Date.now(), options = {}) {
372
+ guardAgentAccess(this.agentId, agentId, options);
373
+ const paths = await this.initAgent(agentId);
374
+ const sqlite = await this.ensureSqliteStore(paths);
375
+ const nowIsoValue = new Date(now).toISOString();
376
+ const expiredEntryIds = sqlite.listExpiredScratchpads(nowIsoValue);
377
+
378
+ if (expiredEntryIds.length) {
379
+ await this.withAgentLock(paths, "memory-write", async () => {
380
+ for (const entryId of expiredEntryIds) {
381
+ const scratchpadId = entryId.replace(/^scratchpad:/, "");
382
+ const filePath = path.join(paths.scratchpadsDir, `${scratchpadId}.json`);
383
+ sqlite.deleteByEntryId(entryId);
384
+ await removePath(filePath);
385
+ }
386
+ });
387
+ }
388
+ return sqlite
389
+ .listAll(nowIsoValue)
390
+ .filter((entry) => entry.type === "scratchpad");
391
+ }
392
+
393
+ async saveCheckpoint(agentId, checkpointId, checkpoint, options = {}) {
394
+ guardAgentAccess(this.agentId, agentId, options);
395
+ const paths = await this.initAgent(agentId);
396
+ const record = {
397
+ timestamp: nowIso(),
398
+ ...checkpoint
399
+ };
400
+ await writeJson(path.join(paths.checkpointsDir, `${checkpointId}.json`), record);
401
+ return record;
402
+ }
403
+
404
+ async loadCheckpoint(agentId, checkpointId = "latest", options = {}) {
405
+ guardAgentAccess(this.agentId, agentId, options);
406
+ const paths = await this.initAgent(agentId);
407
+ return readJson(path.join(paths.checkpointsDir, `${checkpointId}.json`), null);
408
+ }
409
+
410
+ async listAll(agentId, now = Date.now(), options = {}) {
411
+ guardAgentAccess(this.agentId, agentId, options);
412
+ const paths = await this.initAgent(agentId);
413
+ await this.getScratchpads(agentId, now, { allowCrossAgentRead: true });
414
+ const sqlite = await this.ensureSqliteStore(paths);
415
+ return sqlite.listAll(new Date(now).toISOString());
416
+ }
417
+
418
+ async sqliteStatus(agentId, options = {}) {
419
+ guardAgentAccess(this.agentId, agentId, options);
420
+ const paths = await this.initAgent(agentId);
421
+ const sqlite = await this.ensureSqliteStore(paths);
422
+ return sqlite.getWalStatus(options.mode || "passive");
423
+ }
424
+
425
+ async manageSqlite(agentId, options = {}) {
426
+ guardAgentAccess(this.agentId, agentId, options);
427
+ const paths = await this.initAgent(agentId);
428
+ const sqlite = await this.ensureSqliteStore(paths);
429
+ return sqlite.manageWal(options);
430
+ }
431
+
432
+ async getEmbeddingHealth(agentId, options = {}) {
433
+ guardAgentAccess(this.agentId, agentId, options);
434
+ const paths = await this.initAgent(agentId);
435
+ const sqlite = await this.ensureSqliteStore(paths);
436
+ let probe = null;
437
+
438
+ if (options.probe && options.embeddingIndex) {
439
+ try {
440
+ const embedded = await options.embeddingIndex.embedQuery(options.probeText || "nemoris embedding health probe");
441
+ if (embedded?.config?.provider) sqlite.setRuntimeMeta("embedding_last_provider", embedded.config.provider);
442
+ if (embedded?.config?.model) sqlite.setRuntimeMeta("embedding_last_model", embedded.config.model);
443
+ sqlite.setRuntimeMeta("embedding_last_success_at", nowIso());
444
+ sqlite.setRuntimeMeta("embedding_last_query_mode", embedded?.vector ? "embedding_probe" : "lexical_only");
445
+ sqlite.clearRuntimeMeta("embedding_last_error");
446
+ sqlite.clearRuntimeMeta("embedding_last_error_at");
447
+ probe = {
448
+ ok: true,
449
+ dimensions: Array.isArray(embedded?.vector) ? embedded.vector.length : 0,
450
+ config: embedded?.config || null
451
+ };
452
+ } catch (error) {
453
+ const message = error?.message || String(error);
454
+ sqlite.setRuntimeMeta("embedding_last_error", message);
455
+ sqlite.setRuntimeMeta("embedding_last_error_at", nowIso());
456
+ sqlite.setRuntimeMeta("embedding_last_query_mode", "embedding_probe_failed");
457
+ probe = {
458
+ ok: false,
459
+ error: message
460
+ };
461
+ }
462
+ }
463
+
464
+ return {
465
+ agentId,
466
+ totalEntries: sqlite.count(),
467
+ embeddingHealth: sqlite.getEmbeddingHealth(),
468
+ probe
469
+ };
470
+ }
471
+
472
+ async rebuildEmbeddings(agentId, options = {}) {
473
+ guardAgentAccess(this.agentId, agentId, options);
474
+ if (!options.embeddingIndex) {
475
+ throw new Error("No embedding index configured for rebuild");
476
+ }
477
+
478
+ const paths = await this.initAgent(agentId);
479
+ const sqlite = await this.ensureSqliteStore(paths);
480
+ const items = await this.listAll(agentId);
481
+
482
+ try {
483
+ const result = await options.embeddingIndex.rebuild(agentId, items);
484
+ if (result?.config?.provider) sqlite.setRuntimeMeta("embedding_last_provider", result.config.provider);
485
+ if (result?.config?.model) sqlite.setRuntimeMeta("embedding_last_model", result.config.model);
486
+ sqlite.setRuntimeMeta("embedding_last_success_at", nowIso());
487
+ sqlite.setRuntimeMeta("embedding_last_query_mode", "embedding_rebuild");
488
+ sqlite.clearRuntimeMeta("embedding_last_error");
489
+ sqlite.clearRuntimeMeta("embedding_last_error_at");
490
+
491
+ return {
492
+ agentId,
493
+ itemCount: items.length,
494
+ updatedAt: result.updatedAt,
495
+ config: result.config,
496
+ embeddingHealth: sqlite.getEmbeddingHealth()
497
+ };
498
+ } catch (error) {
499
+ const message = error?.message || String(error);
500
+ sqlite.setRuntimeMeta("embedding_last_error", message);
501
+ sqlite.setRuntimeMeta("embedding_last_error_at", nowIso());
502
+ sqlite.setRuntimeMeta("embedding_last_query_mode", "embedding_rebuild_failed");
503
+ throw error;
504
+ }
505
+ }
506
+
507
+ async query(agentId, query, options = {}) {
508
+ guardAgentAccess(this.agentId, agentId, options);
509
+ const limit = options.limit ?? 8;
510
+ const now = options.now ?? Date.now();
511
+ const retrievalBlend = {
512
+ ...DEFAULT_RETRIEVAL_BLEND,
513
+ ...(options.retrievalBlend || {})
514
+ };
515
+ const paths = await this.initAgent(agentId);
516
+ await this.getScratchpads(agentId, now, { allowCrossAgentRead: true });
517
+ const sqlite = await this.ensureSqliteStore(paths);
518
+ let queryVector = null;
519
+ let embeddingConfig = null;
520
+ let embeddingError = null;
521
+ let embeddingQueryMode = "lexical_only";
522
+ if (options.embeddingIndex) {
523
+ try {
524
+ const embedded = await options.embeddingIndex.embedQuery(query);
525
+ queryVector = embedded.vector;
526
+ embeddingConfig = embedded.config || null;
527
+ embeddingQueryMode = queryVector ? "embedding_query" : "lexical_only";
528
+ if (embeddingConfig?.provider) sqlite.setRuntimeMeta("embedding_last_provider", embeddingConfig.provider);
529
+ if (embeddingConfig?.model) sqlite.setRuntimeMeta("embedding_last_model", embeddingConfig.model);
530
+ sqlite.setRuntimeMeta("embedding_last_success_at", nowIso());
531
+ sqlite.setRuntimeMeta("embedding_last_query_mode", embeddingQueryMode);
532
+ sqlite.clearRuntimeMeta("embedding_last_error");
533
+ sqlite.clearRuntimeMeta("embedding_last_error_at");
534
+ } catch (error) {
535
+ queryVector = null;
536
+ embeddingConfig = null;
537
+ embeddingError = error?.message || String(error);
538
+ embeddingQueryMode = "lexical_fallback";
539
+ sqlite.setRuntimeMeta("embedding_last_error", embeddingError);
540
+ sqlite.setRuntimeMeta("embedding_last_error_at", nowIso());
541
+ sqlite.setRuntimeMeta("embedding_last_query_mode", embeddingQueryMode);
542
+ }
543
+ }
544
+ const retrievalCandidates = sqlite.queryRetrievalCandidates(query, {
545
+ nowIso: new Date(now).toISOString(),
546
+ limit,
547
+ queryVector
548
+ });
549
+
550
+ const ranked = retrievalCandidates
551
+ .map((candidate) => ({
552
+ ...candidate.item,
553
+ ...scoreMemory(
554
+ {
555
+ ...candidate.item,
556
+ lexicalScore: computeLexicalScore(
557
+ query,
558
+ [candidate.item.title, candidate.item.content, candidate.item.summary, candidate.item.reason, candidate.item.category]
559
+ .filter(Boolean)
560
+ .join(" ")
561
+ ),
562
+ embeddingSimilarity: candidate.embeddingSimilarity,
563
+ embeddingFreshness: candidate.embeddingFreshness
564
+ },
565
+ query,
566
+ now,
567
+ retrievalBlend
568
+ ),
569
+ retrievalSources: candidate.retrievalSources,
570
+ embeddingFreshness: candidate.embeddingFreshness,
571
+ embeddingProvider: candidate.embeddingProvider,
572
+ embeddingModel: candidate.embeddingModel,
573
+ candidateSource: candidate.retrievalSources.includes("semantic") && candidate.retrievalSources.includes("lexical")
574
+ ? "indexed+semantic"
575
+ : candidate.retrievalSources.includes("semantic")
576
+ ? "semantic"
577
+ : "indexed"
578
+ }))
579
+ .sort((a, b) => b.score - a.score);
580
+
581
+ const deduped = [];
582
+ const seen = new Set();
583
+ for (const item of ranked) {
584
+ const key = buildResultSignature(item);
585
+ if (seen.has(key)) continue;
586
+ seen.add(key);
587
+ deduped.push(item);
588
+ if (deduped.length >= limit) break;
589
+ }
590
+
591
+ return {
592
+ query,
593
+ totalCandidates: sqlite.count(),
594
+ indexedCandidates: retrievalCandidates.filter((item) => item.lexicalMatch).length,
595
+ semanticCandidates: retrievalCandidates.filter((item) => item.embeddingSimilarity > 0).length,
596
+ retrieval: {
597
+ blend: retrievalBlend,
598
+ embeddingQueryAvailable: Boolean(queryVector),
599
+ embeddingConfig,
600
+ embeddingQueryMode,
601
+ embeddingError,
602
+ embeddingHealth: sqlite.getEmbeddingHealth(),
603
+ candidates: retrievalCandidates.map((candidate) => ({
604
+ entryId: candidate.item.entryId,
605
+ title: candidate.item.title,
606
+ retrievalSources: candidate.retrievalSources,
607
+ lexicalMatch: candidate.lexicalMatch,
608
+ lexicalRank: candidate.lexicalRank,
609
+ embeddingSimilarity: Number(candidate.embeddingSimilarity.toFixed(4)),
610
+ embeddingFreshness: candidate.embeddingFreshness
611
+ }))
612
+ },
613
+ items: deduped
614
+ };
615
+ }
616
+
617
+ async withAgentLock(paths, lockKey, fn) {
618
+ const sqlite = this.getSqliteStore(paths);
619
+ const ownerId = createRuntimeId("memory-lock");
620
+ const ttlMs = this.lockConfig.ttlMs;
621
+ const retryDelayMs = this.lockConfig.retryDelayMs;
622
+ const maxRetries = this.lockConfig.maxRetries;
623
+
624
+ let acquired = false;
625
+ for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
626
+ const acquiredAt = new Date();
627
+ const expiresAt = new Date(acquiredAt.getTime() + ttlMs);
628
+ acquired = sqlite.tryAcquireLock(lockKey, {
629
+ ownerId,
630
+ acquiredAt: acquiredAt.toISOString(),
631
+ expiresAt: expiresAt.toISOString()
632
+ });
633
+ if (acquired) break;
634
+ if (attempt === maxRetries) {
635
+ throw new Error(`Timed out waiting for SQLite memory lock ${lockKey}`);
636
+ }
637
+ await sleep(retryDelayMs);
638
+ }
639
+
640
+ try {
641
+ return await fn();
642
+ } finally {
643
+ if (acquired) {
644
+ sqlite.releaseLock(lockKey, ownerId);
645
+ }
646
+ }
647
+ }
648
+ }