nemoris 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/.env.example +49 -0
  2. package/LICENSE +21 -0
  3. package/README.md +209 -0
  4. package/SECURITY.md +119 -0
  5. package/bin/nemoris +46 -0
  6. package/config/agents/agent.toml.example +28 -0
  7. package/config/agents/default.toml +22 -0
  8. package/config/agents/orchestrator.toml +18 -0
  9. package/config/delivery.toml +73 -0
  10. package/config/embeddings.toml +5 -0
  11. package/config/identity/default-purpose.md +1 -0
  12. package/config/identity/default-soul.md +3 -0
  13. package/config/identity/orchestrator-purpose.md +1 -0
  14. package/config/identity/orchestrator-soul.md +1 -0
  15. package/config/improvement-targets.toml +15 -0
  16. package/config/jobs/heartbeat-check.toml +30 -0
  17. package/config/jobs/memory-rollup.toml +46 -0
  18. package/config/jobs/workspace-health.toml +63 -0
  19. package/config/mcp.toml +16 -0
  20. package/config/output-contracts.toml +17 -0
  21. package/config/peers.toml +32 -0
  22. package/config/peers.toml.example +32 -0
  23. package/config/policies/memory-default.toml +10 -0
  24. package/config/policies/memory-heartbeat.toml +5 -0
  25. package/config/policies/memory-ops.toml +10 -0
  26. package/config/policies/tools-heartbeat-minimal.toml +8 -0
  27. package/config/policies/tools-interactive-safe.toml +8 -0
  28. package/config/policies/tools-ops-bounded.toml +8 -0
  29. package/config/policies/tools-orchestrator.toml +7 -0
  30. package/config/providers/anthropic.toml +15 -0
  31. package/config/providers/ollama.toml +5 -0
  32. package/config/providers/openai-codex.toml +9 -0
  33. package/config/providers/openrouter.toml +5 -0
  34. package/config/router.toml +22 -0
  35. package/config/runtime.toml +114 -0
  36. package/config/skills/self-improvement.toml +15 -0
  37. package/config/skills/telegram-onboarding-spec.md +240 -0
  38. package/config/skills/workspace-monitor.toml +15 -0
  39. package/config/task-router.toml +42 -0
  40. package/install.sh +50 -0
  41. package/package.json +90 -0
  42. package/src/auth/auth-profiles.js +169 -0
  43. package/src/auth/openai-codex-oauth.js +285 -0
  44. package/src/battle.js +449 -0
  45. package/src/cli/help.js +265 -0
  46. package/src/cli/output-filter.js +49 -0
  47. package/src/cli/runtime-control.js +704 -0
  48. package/src/cli-main.js +2763 -0
  49. package/src/cli.js +78 -0
  50. package/src/config/loader.js +332 -0
  51. package/src/config/schema-validator.js +214 -0
  52. package/src/config/toml-lite.js +8 -0
  53. package/src/daemon/action-handlers.js +71 -0
  54. package/src/daemon/healing-tick.js +87 -0
  55. package/src/daemon/health-probes.js +90 -0
  56. package/src/daemon/notifier.js +57 -0
  57. package/src/daemon/nurse.js +218 -0
  58. package/src/daemon/repair-log.js +106 -0
  59. package/src/daemon/rule-staging.js +90 -0
  60. package/src/daemon/rules.js +29 -0
  61. package/src/daemon/telegram-commands.js +54 -0
  62. package/src/daemon/updater.js +85 -0
  63. package/src/jobs/job-runner.js +78 -0
  64. package/src/mcp/consumer.js +129 -0
  65. package/src/memory/active-recall.js +171 -0
  66. package/src/memory/backend-manager.js +97 -0
  67. package/src/memory/backends/file-backend.js +38 -0
  68. package/src/memory/backends/qmd-backend.js +219 -0
  69. package/src/memory/embedding-guards.js +24 -0
  70. package/src/memory/embedding-index.js +118 -0
  71. package/src/memory/embedding-service.js +179 -0
  72. package/src/memory/file-index.js +177 -0
  73. package/src/memory/memory-signature.js +5 -0
  74. package/src/memory/memory-store.js +648 -0
  75. package/src/memory/retrieval-planner.js +66 -0
  76. package/src/memory/scoring.js +145 -0
  77. package/src/memory/simhash.js +78 -0
  78. package/src/memory/sqlite-active-store.js +824 -0
  79. package/src/memory/write-policy.js +36 -0
  80. package/src/onboarding/aliases.js +33 -0
  81. package/src/onboarding/auth/api-key.js +224 -0
  82. package/src/onboarding/auth/ollama-detect.js +42 -0
  83. package/src/onboarding/clack-prompter.js +77 -0
  84. package/src/onboarding/doctor.js +530 -0
  85. package/src/onboarding/lock.js +42 -0
  86. package/src/onboarding/model-catalog.js +344 -0
  87. package/src/onboarding/phases/auth.js +589 -0
  88. package/src/onboarding/phases/build.js +130 -0
  89. package/src/onboarding/phases/choose.js +82 -0
  90. package/src/onboarding/phases/detect.js +98 -0
  91. package/src/onboarding/phases/hatch.js +216 -0
  92. package/src/onboarding/phases/identity.js +79 -0
  93. package/src/onboarding/phases/ollama.js +345 -0
  94. package/src/onboarding/phases/scaffold.js +99 -0
  95. package/src/onboarding/phases/telegram.js +377 -0
  96. package/src/onboarding/phases/validate.js +204 -0
  97. package/src/onboarding/phases/verify.js +206 -0
  98. package/src/onboarding/platform.js +482 -0
  99. package/src/onboarding/status-bar.js +95 -0
  100. package/src/onboarding/templates.js +794 -0
  101. package/src/onboarding/toml-writer.js +38 -0
  102. package/src/onboarding/tui.js +250 -0
  103. package/src/onboarding/uninstall.js +153 -0
  104. package/src/onboarding/wizard.js +499 -0
  105. package/src/providers/anthropic.js +168 -0
  106. package/src/providers/base.js +247 -0
  107. package/src/providers/circuit-breaker.js +136 -0
  108. package/src/providers/ollama.js +163 -0
  109. package/src/providers/openai-codex.js +149 -0
  110. package/src/providers/openrouter.js +136 -0
  111. package/src/providers/registry.js +36 -0
  112. package/src/providers/router.js +16 -0
  113. package/src/runtime/bootstrap-cache.js +47 -0
  114. package/src/runtime/capabilities-prompt.js +25 -0
  115. package/src/runtime/completion-ping.js +99 -0
  116. package/src/runtime/config-validator.js +121 -0
  117. package/src/runtime/context-ledger.js +360 -0
  118. package/src/runtime/cutover-readiness.js +42 -0
  119. package/src/runtime/daemon.js +729 -0
  120. package/src/runtime/delivery-ack.js +195 -0
  121. package/src/runtime/delivery-adapters/local-file.js +41 -0
  122. package/src/runtime/delivery-adapters/openclaw-cli.js +94 -0
  123. package/src/runtime/delivery-adapters/openclaw-peer.js +98 -0
  124. package/src/runtime/delivery-adapters/shadow.js +13 -0
  125. package/src/runtime/delivery-adapters/standalone-http.js +98 -0
  126. package/src/runtime/delivery-adapters/telegram.js +104 -0
  127. package/src/runtime/delivery-adapters/tui.js +128 -0
  128. package/src/runtime/delivery-manager.js +807 -0
  129. package/src/runtime/delivery-store.js +168 -0
  130. package/src/runtime/dependency-health.js +118 -0
  131. package/src/runtime/envelope.js +114 -0
  132. package/src/runtime/evaluation.js +1089 -0
  133. package/src/runtime/exec-approvals.js +216 -0
  134. package/src/runtime/executor.js +500 -0
  135. package/src/runtime/failure-ping.js +67 -0
  136. package/src/runtime/flows.js +83 -0
  137. package/src/runtime/guards.js +45 -0
  138. package/src/runtime/handoff.js +51 -0
  139. package/src/runtime/identity-cache.js +28 -0
  140. package/src/runtime/improvement-engine.js +109 -0
  141. package/src/runtime/improvement-harness.js +581 -0
  142. package/src/runtime/input-sanitiser.js +72 -0
  143. package/src/runtime/interaction-contract.js +347 -0
  144. package/src/runtime/lane-readiness.js +226 -0
  145. package/src/runtime/migration.js +323 -0
  146. package/src/runtime/model-resolution.js +78 -0
  147. package/src/runtime/network.js +64 -0
  148. package/src/runtime/notification-store.js +97 -0
  149. package/src/runtime/notifier.js +256 -0
  150. package/src/runtime/orchestrator.js +53 -0
  151. package/src/runtime/orphan-reaper.js +41 -0
  152. package/src/runtime/output-contract-schema.js +139 -0
  153. package/src/runtime/output-contract-validator.js +439 -0
  154. package/src/runtime/peer-readiness.js +69 -0
  155. package/src/runtime/peer-registry.js +133 -0
  156. package/src/runtime/pilot-status.js +108 -0
  157. package/src/runtime/prompt-builder.js +261 -0
  158. package/src/runtime/provider-attempt.js +582 -0
  159. package/src/runtime/report-fallback.js +71 -0
  160. package/src/runtime/result-normalizer.js +183 -0
  161. package/src/runtime/retention.js +74 -0
  162. package/src/runtime/review.js +244 -0
  163. package/src/runtime/route-job.js +15 -0
  164. package/src/runtime/run-store.js +38 -0
  165. package/src/runtime/schedule.js +88 -0
  166. package/src/runtime/scheduler-state.js +434 -0
  167. package/src/runtime/scheduler.js +656 -0
  168. package/src/runtime/session-compactor.js +182 -0
  169. package/src/runtime/session-search.js +155 -0
  170. package/src/runtime/slack-inbound.js +249 -0
  171. package/src/runtime/ssrf.js +102 -0
  172. package/src/runtime/status-aggregator.js +330 -0
  173. package/src/runtime/task-contract.js +140 -0
  174. package/src/runtime/task-packet.js +107 -0
  175. package/src/runtime/task-router.js +140 -0
  176. package/src/runtime/telegram-inbound.js +1565 -0
  177. package/src/runtime/token-counter.js +134 -0
  178. package/src/runtime/token-estimator.js +59 -0
  179. package/src/runtime/tool-loop.js +200 -0
  180. package/src/runtime/transport-server.js +311 -0
  181. package/src/runtime/tui-server.js +411 -0
  182. package/src/runtime/ulid.js +44 -0
  183. package/src/security/ssrf-check.js +197 -0
  184. package/src/setup.js +369 -0
  185. package/src/shadow/bridge.js +303 -0
  186. package/src/skills/loader.js +84 -0
  187. package/src/tools/catalog.json +49 -0
  188. package/src/tools/cli-delegate.js +44 -0
  189. package/src/tools/mcp-client.js +106 -0
  190. package/src/tools/micro/cancel-task.js +6 -0
  191. package/src/tools/micro/complete-task.js +6 -0
  192. package/src/tools/micro/fail-task.js +6 -0
  193. package/src/tools/micro/http-fetch.js +74 -0
  194. package/src/tools/micro/index.js +36 -0
  195. package/src/tools/micro/lcm-recall.js +60 -0
  196. package/src/tools/micro/list-dir.js +17 -0
  197. package/src/tools/micro/list-skills.js +46 -0
  198. package/src/tools/micro/load-skill.js +38 -0
  199. package/src/tools/micro/memory-search.js +45 -0
  200. package/src/tools/micro/read-file.js +11 -0
  201. package/src/tools/micro/session-search.js +54 -0
  202. package/src/tools/micro/shell-exec.js +43 -0
  203. package/src/tools/micro/trigger-job.js +79 -0
  204. package/src/tools/micro/web-search.js +58 -0
  205. package/src/tools/micro/workspace-paths.js +39 -0
  206. package/src/tools/micro/write-file.js +14 -0
  207. package/src/tools/micro/write-memory.js +41 -0
  208. package/src/tools/registry.js +348 -0
  209. package/src/tools/tool-result-contract.js +36 -0
  210. package/src/tui/chat.js +835 -0
  211. package/src/tui/renderer.js +175 -0
  212. package/src/tui/socket-client.js +217 -0
  213. package/src/utils/canonical-json.js +29 -0
  214. package/src/utils/compaction.js +30 -0
  215. package/src/utils/env-loader.js +5 -0
  216. package/src/utils/errors.js +80 -0
  217. package/src/utils/fs.js +101 -0
  218. package/src/utils/ids.js +5 -0
  219. package/src/utils/model-context-limits.js +30 -0
  220. package/src/utils/token-budget.js +74 -0
  221. package/src/utils/usage-cost.js +25 -0
  222. package/src/utils/usage-metrics.js +14 -0
  223. package/vendor/smol-toml-1.5.2.tgz +0 -0
@@ -0,0 +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
+ }