total-recall-brain 3.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 (170) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +173 -0
  3. package/bin/antigravity.mjs +212 -0
  4. package/bin/init.mjs +36 -0
  5. package/bin/oci-sniper.sh +69 -0
  6. package/bin/total-recall.mjs +229 -0
  7. package/docs/ARCHITECTURE.md +251 -0
  8. package/docs/IDE_INTEGRATION.md +112 -0
  9. package/package.json +71 -0
  10. package/scaffold/.agent/skills/total-recall/SKILL.md +195 -0
  11. package/scaffold/.agent/skills/total-recall/evals/evals.json +20 -0
  12. package/scaffold/.agent/skills/total-recall/memory-vault/concepts/cli-help-reference.md +145 -0
  13. package/scaffold/.agent/skills/total-recall/memory-vault/invariants/operating-instructions.md +75 -0
  14. package/scaffold/.agent/skills/total-recall/memory-vault/preferences/always-websearch-gap.md +16 -0
  15. package/scaffold/.agent/skills/total-recall/memory-vault/preferences/topic-research-sop.md +17 -0
  16. package/scaffold/.agent/skills/total-recall/references/api-reference.md +187 -0
  17. package/scaffold/.agent/skills/total-recall/references/architecture-reference.md +81 -0
  18. package/scaffold/.agent/skills/total-recall/references/cli-reference.md +103 -0
  19. package/scaffold/.agent/skills/total-recall/references/ssss-reference.md +88 -0
  20. package/scaffold/.agent/skills/total-recall/references/troubleshooting.md +68 -0
  21. package/scaffold/.agent/skills/total-recall/scripts/sync-repo.mjs +83 -0
  22. package/scaffold/.agent/skills/total-recall/skills/cli-agents/SKILL.md +122 -0
  23. package/scaffold/.agent/skills/total-recall/skills/cli-agents/agents.yml +27 -0
  24. package/scaffold/.agent/skills/total-recall/skills/cli-agents/references/antigravity.txt +20 -0
  25. package/scaffold/.agent/skills/total-recall/skills/cli-agents/references/claude.txt +58 -0
  26. package/scaffold/.agent/skills/total-recall/skills/cli-agents/references/codex.txt +81 -0
  27. package/scaffold/.agent/skills/total-recall/skills/cli-agents/references/gemini.txt +44 -0
  28. package/scaffold/.agent/skills/total-recall/skills/research/SKILL.md +337 -0
  29. package/scaffold/.agent/skills/total-recall/skills/skill/SKILL.md +281 -0
  30. package/scaffold/.agent/skills/total-recall/skills/skill/assets/skill-template/SKILL.md +20 -0
  31. package/scaffold/.agent/skills/total-recall/skills/skill/evals/evals.json +20 -0
  32. package/scaffold/.agent/skills/total-recall/skills/skill/hooks/on-skill-add.sh +14 -0
  33. package/scaffold/.agent/skills/total-recall/skills/skill/hooks/on-skill-delete.sh +14 -0
  34. package/scaffold/.agent/skills/total-recall/skills/skill/learnings/.gitkeep +0 -0
  35. package/scaffold/.agent/skills/total-recall/skills/skill/references/.gitkeep +0 -0
  36. package/scaffold/.agent/skills/total-recall/skills/skill/scripts/.gitkeep +0 -0
  37. package/scaffold/.agent/skills/total-recall/skills/skill/scripts/create-skill.sh +65 -0
  38. package/scaffold/.agent/skills/total-recall/skills/skill/scripts/enforce-skill-optimization.mjs +135 -0
  39. package/scaffold/.agent/skills/total-recall/skills/skill/scripts/find-skills.mjs +124 -0
  40. package/scaffold/.agent/skills/total-recall/skills/skill/scripts/scan-skill.mjs +207 -0
  41. package/scaffold/.agent/skills/total-recall/skills/skill/scripts/skill-watcher.mjs +64 -0
  42. package/scaffold/.agent/skills/total-recall/skills/skill/subagents/audit-all-skills.md +62 -0
  43. package/scaffold/.agent/skills/total-recall/skills/ssss/SKILL.md +27 -0
  44. package/scaffold/.agent/skills/total-recall/skills/ssss/evals/evals.json +17 -0
  45. package/scaffold/.agent/skills/total-recall/skills/ssss/references/ssss-spec.md +117 -0
  46. package/scaffold/.agent/skills/total-recall/skills/ssss/scripts/validate-schema.mjs +54 -0
  47. package/scaffold/.agent/skills/total-recall/skills/ssss/subagents/audit-ssss-node.md +9 -0
  48. package/scaffold/.agent/skills/total-recall/subagents/total-recall-diagnostician.md +13 -0
  49. package/src/cli/agent-dir.mjs +105 -0
  50. package/src/cli/backup.mjs +313 -0
  51. package/src/cli/brain.mjs +224 -0
  52. package/src/cli/chat.mjs +74 -0
  53. package/src/cli/config.mjs +247 -0
  54. package/src/cli/connect.mjs +872 -0
  55. package/src/cli/daemon.mjs +228 -0
  56. package/src/cli/deploy-ui.mjs +1793 -0
  57. package/src/cli/deploy.mjs +1726 -0
  58. package/src/cli/doctor.mjs +254 -0
  59. package/src/cli/dream.mjs +80 -0
  60. package/src/cli/forget.mjs +92 -0
  61. package/src/cli/friction.mjs +21 -0
  62. package/src/cli/generate-pat.mjs +65 -0
  63. package/src/cli/hash-password.mjs +22 -0
  64. package/src/cli/help.mjs +153 -0
  65. package/src/cli/import-rules.mjs +102 -0
  66. package/src/cli/ingest/index.mjs +318 -0
  67. package/src/cli/ingest/parsers/chrome-bookmarks.mjs +238 -0
  68. package/src/cli/ingest/parsers/google-keep.mjs +147 -0
  69. package/src/cli/ingest/parsers/search-history.mjs +142 -0
  70. package/src/cli/ingest/parsers/youtube-history.mjs +181 -0
  71. package/src/cli/ingest/utils/dedup.mjs +69 -0
  72. package/src/cli/ingest/utils/takeout-walker.mjs +101 -0
  73. package/src/cli/ingest.mjs +102 -0
  74. package/src/cli/init.mjs +736 -0
  75. package/src/cli/integration-dispatcher.mjs +128 -0
  76. package/src/cli/lint.mjs +166 -0
  77. package/src/cli/map.mjs +194 -0
  78. package/src/cli/migrate.mjs +87 -0
  79. package/src/cli/rebuild.mjs +88 -0
  80. package/src/cli/recall.mjs +219 -0
  81. package/src/cli/relay.mjs +518 -0
  82. package/src/cli/remember.mjs +306 -0
  83. package/src/cli/research.mjs +381 -0
  84. package/src/cli/reset-password.mjs +75 -0
  85. package/src/cli/restore.mjs +135 -0
  86. package/src/cli/setup.mjs +616 -0
  87. package/src/cli/share.mjs +180 -0
  88. package/src/cli/skill.mjs +313 -0
  89. package/src/cli/snapshot.mjs +108 -0
  90. package/src/cli/start.mjs +63 -0
  91. package/src/cli/status.mjs +296 -0
  92. package/src/cli/sync.mjs +383 -0
  93. package/src/cli/uninstall.mjs +613 -0
  94. package/src/cli/upgrade.mjs +141 -0
  95. package/src/cli/wizard.html +3682 -0
  96. package/src/core/blackboard.mjs +54 -0
  97. package/src/core/clarity-rewriter.mjs +770 -0
  98. package/src/core/conclusion-writer.mjs +259 -0
  99. package/src/core/config.mjs +277 -0
  100. package/src/core/conflict-detector.mjs +536 -0
  101. package/src/core/crypto.mjs +107 -0
  102. package/src/core/daemon-control.mjs +158 -0
  103. package/src/core/daemon-loop.mjs +364 -0
  104. package/src/core/dream.mjs +347 -0
  105. package/src/core/drift-detector.mjs +143 -0
  106. package/src/core/embeddings.mjs +632 -0
  107. package/src/core/emergency-alerts.mjs +137 -0
  108. package/src/core/evolution.mjs +107 -0
  109. package/src/core/fact-seeker.mjs +1602 -0
  110. package/src/core/friction.mjs +95 -0
  111. package/src/core/import-rules.mjs +308 -0
  112. package/src/core/inference-engine.mjs +367 -0
  113. package/src/core/logger.mjs +75 -0
  114. package/src/core/memory-layers.mjs +96 -0
  115. package/src/core/migrate.mjs +116 -0
  116. package/src/core/notifications.mjs +77 -0
  117. package/src/core/operation-validator.mjs +272 -0
  118. package/src/core/optimizer.mjs +330 -0
  119. package/src/core/pattern_detector.mjs +113 -0
  120. package/src/core/post-mortem.mjs +413 -0
  121. package/src/core/protect-instructions.mjs +148 -0
  122. package/src/core/quick-capture.mjs +145 -0
  123. package/src/core/research-queue.mjs +431 -0
  124. package/src/core/research.mjs +461 -0
  125. package/src/core/runtime.mjs +515 -0
  126. package/src/core/sandbox.mjs +163 -0
  127. package/src/core/scheduler.mjs +569 -0
  128. package/src/core/schema.mjs +333 -0
  129. package/src/core/search.mjs +219 -0
  130. package/src/core/semantic-index.mjs +276 -0
  131. package/src/core/session-watcher.mjs +850 -0
  132. package/src/core/snapshot.mjs +133 -0
  133. package/src/core/source-adapters.mjs +1038 -0
  134. package/src/core/steering.mjs +137 -0
  135. package/src/core/surface.mjs +444 -0
  136. package/src/core/task_runner.mjs +125 -0
  137. package/src/core/tts.mjs +142 -0
  138. package/src/core/usage-tracker.mjs +349 -0
  139. package/src/core/vault-cache.mjs +76 -0
  140. package/src/core/vault.mjs +273 -0
  141. package/src/core/watchdog.mjs +236 -0
  142. package/src/server/api.mjs +944 -0
  143. package/src/server/auth.mjs +410 -0
  144. package/src/server/index.mjs +868 -0
  145. package/src/server/keys.mjs +181 -0
  146. package/src/server/rest.mjs +1652 -0
  147. package/src/server/route-manifest.json +77 -0
  148. package/src/server/routes/_shared.mjs +93 -0
  149. package/src/server/routes/keys.mjs +55 -0
  150. package/src/server/routes/memory.mjs +249 -0
  151. package/src/server/routes/sessions.mjs +336 -0
  152. package/src/server/routes/share.mjs +138 -0
  153. package/src/server/tools.mjs +618 -0
  154. package/templates/Caddyfile +37 -0
  155. package/templates/com.totalrecall.backup.plist +42 -0
  156. package/templates/com.totalrecall.daemon.plist +40 -0
  157. package/templates/com.totalrecall.duckdns.plist +23 -0
  158. package/templates/com.totalrecall.server.plist +46 -0
  159. package/templates/default-config/budget.yml +6 -0
  160. package/templates/default-config/runtime.yml +11 -0
  161. package/templates/default-config/security.yml +53 -0
  162. package/templates/default-config/voice.yml +34 -0
  163. package/templates/obsidian-queries/conflicts.md +26 -0
  164. package/templates/obsidian-queries/daily.md +17 -0
  165. package/templates/obsidian-queries/index.md +38 -0
  166. package/templates/obsidian-queries/skills.md +14 -0
  167. package/templates/onboarding-interview.md +72 -0
  168. package/templates/skill_engineering.md +40 -0
  169. package/templates/total-recall-daemon.service +21 -0
  170. package/templates/total-recall-server.service +20 -0
@@ -0,0 +1,367 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import matter from 'gray-matter';
4
+ import crypto from 'crypto';
5
+ import { callLocalRuntime, cleanAndParseJSON } from './runtime.mjs';
6
+ import { loadNodes, atomicWrite, safeStringify } from './vault.mjs';
7
+ import { logger } from './logger.mjs';
8
+ import { brainDir } from './config.mjs';
9
+
10
+ /**
11
+ * Total Recall System 2 Inference Engine
12
+ *
13
+ * Reads clusters of related memory nodes and asks the local LLM to:
14
+ * 1. Draw higher-level conclusions
15
+ * 2. Identify implicit patterns that should be explicit rules
16
+ * 3. Find subtle contradictions that Jaccard wouldn't catch
17
+ *
18
+ * Produces new nodes with category: decisions or concepts,
19
+ * x_memory_layer: system2, status: draft in the inbox.
20
+ * All outputs are proposals — never direct mutations.
21
+ */
22
+
23
+ // ─── Prompts ────────────────────────────────────────────────────────────────────
24
+
25
+ const INFERENCE_SYSTEM = `You are a System 2 Reasoning Engine for a sovereign AI memory system called Total Recall.
26
+
27
+ You will be given a cluster of related memory nodes. Your job is to reason deeply about them and produce higher-level conclusions.
28
+
29
+ Output valid JSON matching this schema:
30
+ {
31
+ "conclusions": [
32
+ {
33
+ "title": "string",
34
+ "body": "string",
35
+ "category": "decisions" | "concepts" | "patterns",
36
+ "confidence": 0.0-1.0,
37
+ "reasoning": "string"
38
+ }
39
+ ],
40
+ "contradictions": [
41
+ {
42
+ "node_a": "slug string",
43
+ "node_b": "slug string",
44
+ "description": "string"
45
+ }
46
+ ],
47
+ "merge_candidates": [
48
+ {
49
+ "node_a": "slug string",
50
+ "node_b": "slug string",
51
+ "reason": "string"
52
+ }
53
+ ]
54
+ }
55
+
56
+ Guidelines:
57
+ - Only include conclusions that are genuinely new — not just summaries of existing nodes.
58
+ - Confidence reflects how well the evidence supports the conclusion.
59
+ - "contradictions" are only real logical contradictions, not just different topics.
60
+ - "merge_candidates" are nodes that say the same thing differently.
61
+ - Output ONLY valid JSON.`;
62
+
63
+ function buildInferencePrompt(nodes) {
64
+ const nodeTexts = nodes.map(n => [
65
+ `### [${n.slug}] ${n.title}`,
66
+ `Category: ${n.category} | Confidence: ${n.confidence} | Layer: ${n.x_memory_layer || 'inferred'}`,
67
+ (n.body || '').slice(0, 800),
68
+ ].join('\n')).join('\n\n');
69
+
70
+ return `Analyze these ${nodes.length} related memory nodes and reason about them:\n\n${nodeTexts}\n\nDraw higher-level conclusions, identify contradictions, and flag merge candidates.`;
71
+ }
72
+
73
+ // ─── Inference Execution ────────────────────────────────────────────────────────
74
+
75
+ /**
76
+ * Run inference over a set of node slugs.
77
+ *
78
+ * @param {string[]} slugs Node slugs to analyze
79
+ * @param {object} opts
80
+ * @param {string} opts.vaultDir Path to memory vault
81
+ * @param {string} opts.inboxDir Path to memory inbox
82
+ * @param {object} opts.runtimeConfig Local LLM config
83
+ * @returns {{ conclusions: object[], contradictions: object[], merge_candidates: object[], error? }}
84
+ */
85
+ export async function runInferenceTask(slugs, { vaultDir, inboxDir, runtimeConfig }) {
86
+ const allNodes = loadNodes(vaultDir);
87
+ const nodes = allNodes.filter(n => slugs.includes(n.slug) && n.status === 'active');
88
+
89
+ if (nodes.length < 2) {
90
+ return { conclusions: [], contradictions: [], merge_candidates: [], skipped: 'too-few-nodes' };
91
+ }
92
+
93
+ const prompt = buildInferencePrompt(nodes);
94
+
95
+ let result;
96
+ try {
97
+ const rawResponse = await callLocalRuntime(prompt, INFERENCE_SYSTEM, runtimeConfig);
98
+ const jsonMatch = rawResponse.match(/\{[\s\S]*\}/);
99
+ if (!jsonMatch) throw new Error('No JSON in response');
100
+ result = cleanAndParseJSON(jsonMatch[0]);
101
+ } catch (err) {
102
+ logger.info({
103
+ subsystem: 'inference-engine',
104
+ message: `Inference failed for slugs [${slugs.join(', ')}]: ${err.message}`,
105
+ });
106
+ return { conclusions: [], contradictions: [], merge_candidates: [], error: err.message };
107
+ }
108
+
109
+ const conclusions = Array.isArray(result.conclusions) ? result.conclusions : [];
110
+ const contradictions = Array.isArray(result.contradictions) ? result.contradictions : [];
111
+ const mergeCandidates = Array.isArray(result.merge_candidates) ? result.merge_candidates : [];
112
+
113
+ // Write conclusions as draft nodes in the inbox
114
+ if (!fs.existsSync(inboxDir)) {
115
+ fs.mkdirSync(inboxDir, { recursive: true });
116
+ }
117
+
118
+ for (const conclusion of conclusions) {
119
+ writeConclusionNode(inboxDir, conclusion, slugs, vaultDir);
120
+ }
121
+
122
+ // Write contradictions as conflict records
123
+ if (contradictions.length > 0) {
124
+ const conflictsDir = path.join(brainDir, 'memory-inbox', 'conflicts');
125
+ for (const contradiction of contradictions) {
126
+ writeContradictionRecord(conflictsDir, contradiction);
127
+ }
128
+ }
129
+
130
+ // Write merge proposals
131
+ if (mergeCandidates.length > 0) {
132
+ const proposalsDir = path.join(vaultDir, 'proposals');
133
+ for (const candidate of mergeCandidates) {
134
+ writeMergeProposal(proposalsDir, candidate);
135
+ }
136
+ }
137
+
138
+ logger.info({
139
+ subsystem: 'inference-engine',
140
+ message: `Inference complete: ${conclusions.length} conclusions, ${contradictions.length} contradictions, ${mergeCandidates.length} merge candidates`,
141
+ });
142
+
143
+ return { conclusions, contradictions, merge_candidates: mergeCandidates };
144
+ }
145
+
146
+ // ─── Node Writers ───────────────────────────────────────────────────────────────
147
+
148
+ function writeConclusionNode(inboxDir, conclusion, sourceSlugList, vaultDir) {
149
+ const slug = `inference-${crypto.randomBytes(4).toString('hex')}`;
150
+ const now = new Date().toISOString();
151
+
152
+ let latestPublished = null;
153
+ let citations = null;
154
+
155
+ if (vaultDir) {
156
+ try {
157
+ const allNodes = loadNodes(vaultDir);
158
+ citations = sourceSlugList.map(s => {
159
+ const matchingNode = allNodes.find(n => n.slug === s);
160
+ const cat = matchingNode ? matchingNode.category : 'facts';
161
+ const published = matchingNode ? (matchingNode.updated || matchingNode.created || now) : now;
162
+ if (!latestPublished || new Date(published) > new Date(latestPublished)) {
163
+ latestPublished = published;
164
+ }
165
+ return {
166
+ source: 'memory-vault',
167
+ title: matchingNode ? matchingNode.title : `Memory Node: ${s}`,
168
+ url: `file://${path.join(vaultDir, cat, s + '.md')}`,
169
+ published,
170
+ relevance: 1.0,
171
+ accessed: now
172
+ };
173
+ });
174
+ } catch (err) {
175
+ logger.info({
176
+ subsystem: 'inference-engine',
177
+ message: `Failed to build citations for conclusion node: ${err.message}`
178
+ });
179
+ }
180
+ }
181
+
182
+ if (!citations) {
183
+ citations = sourceSlugList.map(s => ({
184
+ source: 'memory-vault',
185
+ title: `Memory Node: ${s}`,
186
+ url: `memory://${s}`,
187
+ published: now,
188
+ relevance: 1.0,
189
+ accessed: now
190
+ }));
191
+ }
192
+
193
+ const temporalContext = latestPublished || now;
194
+
195
+ const frontmatter = {
196
+ type: 'memory',
197
+ slug,
198
+ category: conclusion.category || 'concepts',
199
+ title: conclusion.title || 'Inferred Conclusion',
200
+ status: 'draft',
201
+ confidence: Math.min(1.0, Math.max(0.0, Number(conclusion.confidence) || 0.7)),
202
+ importance: 3,
203
+ created: now,
204
+ updated: now,
205
+ last_accessed: now,
206
+ source: {
207
+ type: 'dream-cycle',
208
+ session_id: `inference-${Date.now().toString(36)}`,
209
+ agent: 'inference-engine',
210
+ evidence_count: sourceSlugList.length,
211
+ },
212
+ supersedes: [],
213
+ superseded_by: null,
214
+ contradicts: [],
215
+ tags: ['inference', 'system2', 'auto-generated'],
216
+ related: sourceSlugList,
217
+ routes_to_skills: [],
218
+ sentiment_polarity: 'descriptive',
219
+ sentiment_target: 'knowledge',
220
+ modality: 'should',
221
+ subject: 'agent',
222
+ predicate: 'conclude',
223
+ object: 'knowledge',
224
+ decay: { half_life_days: 180, access_count: 1 },
225
+ schema_version: 2,
226
+ x_memory_layer: 'system2',
227
+ x_reasoning: (conclusion.reasoning || '').slice(0, 500),
228
+ x_temporal_context: temporalContext,
229
+ x_citations: citations,
230
+ };
231
+
232
+ const body = conclusion.body || '';
233
+ atomicWrite(path.join(inboxDir, `${slug}.md`), safeStringify(body, frontmatter));
234
+ }
235
+
236
+ function writeContradictionRecord(conflictsDir, contradiction) {
237
+ if (!fs.existsSync(conflictsDir)) {
238
+ fs.mkdirSync(conflictsDir, { recursive: true });
239
+ }
240
+
241
+ const slug = `contradiction-${crypto.randomBytes(4).toString('hex')}`;
242
+ const now = new Date().toISOString();
243
+
244
+ const data = {
245
+ type: 'conflict',
246
+ slug,
247
+ status: 'detected',
248
+ detected_at: now,
249
+ detected_by: 'inference-engine',
250
+ node_a: contradiction.node_a,
251
+ node_b: contradiction.node_b,
252
+ description: contradiction.description || 'Logical contradiction detected by inference engine',
253
+ };
254
+
255
+ atomicWrite(
256
+ path.join(conflictsDir, `${slug}.md`),
257
+ safeStringify('', data),
258
+ );
259
+ }
260
+
261
+ function writeMergeProposal(proposalsDir, candidate) {
262
+ if (!fs.existsSync(proposalsDir)) {
263
+ fs.mkdirSync(proposalsDir, { recursive: true });
264
+ }
265
+
266
+ const slug = `merge-proposal-${crypto.randomBytes(4).toString('hex')}`;
267
+ const now = new Date().toISOString();
268
+
269
+ const data = {
270
+ type: 'proposal',
271
+ slug,
272
+ category: 'memory-cleanup',
273
+ status: 'draft',
274
+ proposed_by: 'inference-engine',
275
+ proposed_at: now,
276
+ target_path: `${candidate.node_a},${candidate.node_b}`,
277
+ summary: `Merge ${candidate.node_a} and ${candidate.node_b}`,
278
+ rationale: candidate.reason || 'Nodes appear to express the same concept differently.',
279
+ };
280
+
281
+ atomicWrite(
282
+ path.join(proposalsDir, `${slug}.md`),
283
+ safeStringify('', data),
284
+ );
285
+ }
286
+
287
+ // ─── Memory Synthesizer ─────────────────────────────────────────────────────────
288
+
289
+ const SYNTHESIS_SYSTEM = `You are a Memory Synthesizer for a sovereign AI memory system.
290
+
291
+ Given two memory nodes that may be similar, determine:
292
+ 1. Are they saying the same thing? → "MERGE"
293
+ 2. Are they complementary? → "LINK"
294
+ 3. Are they actually about different things? → "KEEP"
295
+
296
+ Output valid JSON:
297
+ {
298
+ "verdict": "MERGE" | "LINK" | "KEEP",
299
+ "reason": "string",
300
+ "merged_title": "string (only if MERGE)",
301
+ "merged_body": "string (only if MERGE)"
302
+ }`;
303
+
304
+ /**
305
+ * Compare two nodes semantically and suggest merge/link/keep.
306
+ *
307
+ * @param {string} slugA First node slug
308
+ * @param {string} slugB Second node slug
309
+ * @param {object} opts
310
+ * @param {string} opts.vaultDir
311
+ * @param {string} opts.inboxDir
312
+ * @param {object} opts.runtimeConfig
313
+ */
314
+ export async function runSynthesisTask(slugA, slugB, { vaultDir, inboxDir, runtimeConfig }) {
315
+ const allNodes = loadNodes(vaultDir);
316
+ const nodeA = allNodes.find(n => n.slug === slugA);
317
+ const nodeB = allNodes.find(n => n.slug === slugB);
318
+
319
+ if (!nodeA || !nodeB) {
320
+ return { verdict: 'KEEP', error: 'One or both nodes not found' };
321
+ }
322
+
323
+ const prompt = [
324
+ `## Node A: ${nodeA.title}`,
325
+ (nodeA.body || '').slice(0, 1000),
326
+ '',
327
+ `## Node B: ${nodeB.title}`,
328
+ (nodeB.body || '').slice(0, 1000),
329
+ '',
330
+ 'Should these nodes be MERGED, LINKED, or KEPT separate?',
331
+ ].join('\n');
332
+
333
+ try {
334
+ const rawResponse = await callLocalRuntime(prompt, SYNTHESIS_SYSTEM, runtimeConfig);
335
+ const jsonMatch = rawResponse.match(/\{[\s\S]*\}/);
336
+ if (!jsonMatch) throw new Error('No JSON in response');
337
+ const result = cleanAndParseJSON(jsonMatch[0]);
338
+
339
+ if (result.verdict === 'MERGE' && result.merged_title) {
340
+ writeConclusionNode(inboxDir, {
341
+ title: result.merged_title,
342
+ body: result.merged_body || '',
343
+ category: nodeA.category,
344
+ confidence: Math.max(nodeA.confidence || 0, nodeB.confidence || 0),
345
+ reasoning: `Merged from ${slugA} and ${slugB}: ${result.reason}`,
346
+ }, [slugA, slugB], vaultDir);
347
+
348
+ logger.info({
349
+ subsystem: 'inference-engine',
350
+ message: `MERGE proposed: ${slugA} + ${slugB} → ${result.merged_title}`,
351
+ });
352
+ } else if (result.verdict === 'LINK') {
353
+ logger.info({
354
+ subsystem: 'inference-engine',
355
+ message: `LINK suggested: ${slugA} ↔ ${slugB}`,
356
+ });
357
+ }
358
+
359
+ return result;
360
+ } catch (err) {
361
+ logger.info({
362
+ subsystem: 'inference-engine',
363
+ message: `Synthesis failed for ${slugA}/${slugB}: ${err.message}`,
364
+ });
365
+ return { verdict: 'KEEP', error: err.message };
366
+ }
367
+ }
@@ -0,0 +1,75 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import { EventEmitter } from 'node:events';
5
+ import { brainDir } from './config.mjs';
6
+
7
+ const BRAIN_DIR = brainDir;
8
+ const LOG_DIR = path.join(BRAIN_DIR, 'logs');
9
+
10
+ if (!fs.existsSync(LOG_DIR)) {
11
+ fs.mkdirSync(LOG_DIR, { recursive: true });
12
+ }
13
+
14
+ const getLogFile = () => {
15
+ const d = new Date();
16
+ return path.join(LOG_DIR, `system-${d.toISOString().split('T')[0]}.jsonl`);
17
+ };
18
+
19
+ export const logEvents = new EventEmitter();
20
+ logEvents.setMaxListeners(50);
21
+
22
+ export const logger = {
23
+ log: (subsystemOrEntry, level, message, meta = {}) => {
24
+ let entry;
25
+ if (typeof subsystemOrEntry === 'object' && subsystemOrEntry !== null) {
26
+ entry = {
27
+ timestamp: new Date().toISOString(),
28
+ level: level || subsystemOrEntry.level || 'info',
29
+ subsystem: subsystemOrEntry.subsystem || 'unknown',
30
+ message: subsystemOrEntry.message || '',
31
+ ...subsystemOrEntry
32
+ };
33
+ } else if (typeof subsystemOrEntry === 'string' && message === undefined) {
34
+ entry = {
35
+ timestamp: new Date().toISOString(),
36
+ level: level || 'info',
37
+ subsystem: 'system',
38
+ message: subsystemOrEntry,
39
+ ...meta
40
+ };
41
+ } else {
42
+ entry = {
43
+ timestamp: new Date().toISOString(),
44
+ level: level || 'info',
45
+ subsystem: subsystemOrEntry || 'unknown',
46
+ message: message || '',
47
+ ...meta
48
+ };
49
+ }
50
+
51
+ const line = JSON.stringify(entry) + '\n';
52
+
53
+ // Server rule: stderr only
54
+ console.error(line.trim());
55
+
56
+ try {
57
+ fs.appendFileSync(getLogFile(), line);
58
+ } catch (e) {
59
+ // If we fail to write to log file, we still printed to stderr
60
+ }
61
+
62
+ // Emit so watchdog and other in-process monitors can react without tailing the file.
63
+ try {
64
+ logEvents.emit('log', entry);
65
+ } catch (_) {
66
+ // Listeners must not break the logger.
67
+ }
68
+ },
69
+ info: (subsystem, message, meta) => logger.log(subsystem, 'info', message, meta),
70
+ warn: (subsystem, message, meta) => logger.log(subsystem, 'warn', message, meta),
71
+ error: (subsystem, message, meta) => logger.log(subsystem, 'error', message, meta),
72
+ debug: (subsystem, message, meta) => logger.log(subsystem, 'debug', message, meta)
73
+ };
74
+
75
+ export { LOG_DIR, getLogFile };
@@ -0,0 +1,96 @@
1
+ export const MEMORY_LAYERS = Object.freeze(['conscious', 'system2', 'research']);
2
+
3
+ export const DEFAULT_MEMORY_LAYER = 'system2';
4
+
5
+ const LAYER_ALIASES = new Map([
6
+ ['conscious', 'conscious'],
7
+ ['awareness', 'conscious'],
8
+ ['working-memory', 'conscious'],
9
+ ['working_memory', 'conscious'],
10
+ ['working memory', 'conscious'],
11
+ ['hot', 'conscious'],
12
+ ['system2', 'system2'],
13
+ ['system-2', 'system2'],
14
+ ['system_2', 'system2'],
15
+ ['system 2', 'system2'],
16
+ ['deliberation', 'system2'],
17
+ ['reasoning', 'system2'],
18
+ ['research', 'research'],
19
+ ['knowledge-acquisition', 'research'],
20
+ ['knowledge_acquisition', 'research'],
21
+ ['knowledge acquisition', 'research'],
22
+ ['acquisition', 'research'],
23
+ ]);
24
+
25
+ const CONSCIOUS_CATEGORIES = new Set(['invariants', 'patterns', 'anti-patterns', 'preferences']);
26
+ const SYSTEM2_CATEGORIES = new Set(['decisions', 'concepts', 'proposals']);
27
+ const RESEARCH_CATEGORIES = new Set(['facts', 'research', 'lore']);
28
+ const SYSTEM2_TAGS = new Set(['system2', 'system-2', 'deliberation', 'reasoning', 'synthesis', 'validation']);
29
+ const RESEARCH_TAGS = new Set(['research', 'web', 'knowledge-acquisition', 'stale-knowledge-refresh']);
30
+ const RESEARCH_SOURCES = new Set(['web-search', 'deep-research', 'research', 'external-citation']);
31
+
32
+ export function normalizeMemoryLayer(layer) {
33
+ if (typeof layer !== 'string') return null;
34
+ const trimmed = layer.trim().toLowerCase();
35
+ if (!trimmed) return null;
36
+ return LAYER_ALIASES.get(trimmed) || null;
37
+ }
38
+
39
+ function normalizedTags(node) {
40
+ if (!Array.isArray(node?.tags)) return new Set();
41
+ return new Set(node.tags.map((tag) => String(tag).trim().toLowerCase()).filter(Boolean));
42
+ }
43
+
44
+ function hasAny(tags, candidates) {
45
+ for (const tag of tags) {
46
+ if (candidates.has(tag)) return true;
47
+ }
48
+ return false;
49
+ }
50
+
51
+ export function inferMemoryLayer(node = {}) {
52
+ const explicit = normalizeMemoryLayer(
53
+ node.x_memory_layer || node.memory_layer || node.cognitive_layer
54
+ );
55
+ if (explicit) return explicit;
56
+
57
+ const category = String(node.category || '').trim().toLowerCase();
58
+ const tags = normalizedTags(node);
59
+ const sourceType = String(node.source?.type || '').trim().toLowerCase();
60
+
61
+ if (hasAny(tags, RESEARCH_TAGS) || RESEARCH_SOURCES.has(sourceType)) return 'research';
62
+ if (hasAny(tags, SYSTEM2_TAGS)) return 'system2';
63
+ if (node.priority === 'absolute') return 'conscious';
64
+ if (CONSCIOUS_CATEGORIES.has(category)) return 'conscious';
65
+ if (SYSTEM2_CATEGORIES.has(category)) return 'system2';
66
+ if (RESEARCH_CATEGORIES.has(category)) return 'research';
67
+
68
+ return DEFAULT_MEMORY_LAYER;
69
+ }
70
+
71
+ export function memoryLayerRoutingWeight(layer) {
72
+ switch (normalizeMemoryLayer(layer) || DEFAULT_MEMORY_LAYER) {
73
+ case 'conscious':
74
+ return 1.25;
75
+ case 'research':
76
+ return 0.75;
77
+ case 'system2':
78
+ default:
79
+ return 1;
80
+ }
81
+ }
82
+
83
+ export function buildMemoryLayerIndex(nodes) {
84
+ return nodes.map((node) => ({
85
+ v: 1,
86
+ slug: node.slug,
87
+ layer: inferMemoryLayer(node),
88
+ category: node.category,
89
+ status: node.status,
90
+ confidence: node.confidence,
91
+ importance: node.importance,
92
+ title: node.title,
93
+ updated: node.updated || null,
94
+ routes_to_skills: Array.isArray(node.routes_to_skills) ? node.routes_to_skills : [],
95
+ }));
96
+ }
@@ -0,0 +1,116 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import crypto from 'crypto';
4
+ import { loadNodes, writeNode, atomicWrite } from './vault.mjs';
5
+ import { createSnapshot } from './snapshot.mjs';
6
+ import { SSSS_SCHEMAS } from './schema.mjs';
7
+ import { logger } from './logger.mjs';
8
+
9
+ /**
10
+ * Executes a schema migration.
11
+ * Because a migration is destructive, it MUST:
12
+ * 1. Take a safety snapshot.
13
+ * 2. Execute the migration script over the vault nodes.
14
+ * 3. Write a `type: migration` record tracking the result.
15
+ */
16
+ export async function runMigration(vaultDir, fromVersion, toVersion, migrationFn, description) {
17
+ logger.info('migration', `Initiating migration from v${fromVersion} to v${toVersion}`);
18
+ logger.info('migration', `Description: ${description}`);
19
+
20
+ // 1. Safety Snapshot
21
+ logger.info('migration', 'Creating pre-migration safety snapshot...');
22
+ const snapshotId = await createSnapshot(vaultDir);
23
+ logger.info('migration', `Safety snapshot created: ${snapshotId}`);
24
+
25
+ const migrationId = `mig_${crypto.randomBytes(6).toString('hex')}`;
26
+ const migrationRecord = {
27
+ type: 'migration',
28
+ migration_id: migrationId,
29
+ from_version: fromVersion,
30
+ to_version: toVersion,
31
+ status: 'pending',
32
+ description,
33
+ };
34
+
35
+ try {
36
+ const nodes = loadNodes(vaultDir);
37
+ logger.info('migration', `Loaded ${nodes.length} nodes for migration processing.`);
38
+
39
+ // 2. Execute Migration
40
+ const migratedNodes = await migrationFn(nodes);
41
+
42
+ logger.info('migration', `Writing ${migratedNodes.length} migrated nodes back to vault...`);
43
+ // Note: in a real system we'd diff and only write changed, but migration often touches all.
44
+ for (const node of migratedNodes) {
45
+ // In a real migration we might delete old files if slug changes, but this assumes in-place update
46
+ writeNode(node, vaultDir);
47
+ }
48
+
49
+ migrationRecord.status = 'applied';
50
+ migrationRecord.applied_at = new Date().toISOString();
51
+ logger.info('migration', `Migration to v${toVersion} completed successfully.`);
52
+
53
+ } catch (err) {
54
+ logger.error('migration', `Migration failed: ${err.message}`);
55
+ migrationRecord.status = 'failed';
56
+ migrationRecord.applied_at = new Date().toISOString();
57
+
58
+ // Auto-rollback on failure could be triggered here.
59
+ logger.error('migration', `WARNING: Vault may be in an inconsistent state. Please run 'total-recall snapshot rollback ${snapshotId}' immediately to recover.`);
60
+ throw err;
61
+ } finally {
62
+ // 3. Record the migration event
63
+ const migrationsDir = path.join(vaultDir, 'system', 'migrations');
64
+ if (!fs.existsSync(migrationsDir)) fs.mkdirSync(migrationsDir, { recursive: true });
65
+
66
+ // Ensure slug doesn't conflict
67
+ const slug = `migration-v${toVersion}-${migrationId}`;
68
+ writeNode({ ...migrationRecord, slug }, migrationsDir);
69
+ }
70
+
71
+ return { migrationId, status: migrationRecord.status };
72
+ }
73
+
74
+ /**
75
+ * Migration Test Harness
76
+ * Simulates a migration in-memory to verify it produces valid schema shapes.
77
+ */
78
+ export async function testMigration(vaultDir, migrationFn, targetSchemaType) {
79
+ logger.info('migration', 'Running migration harness in isolated context...');
80
+
81
+ const nodes = loadNodes(vaultDir).slice(0, 50); // Sample a subset
82
+
83
+ try {
84
+ const testMigrated = await migrationFn(nodes);
85
+
86
+ let validCount = 0;
87
+ let invalidCount = 0;
88
+
89
+ const schema = SSSS_SCHEMAS[targetSchemaType];
90
+
91
+ for (const node of testMigrated) {
92
+ if (node.type === targetSchemaType) {
93
+ if (schema) {
94
+ const res = schema.safeParse(node);
95
+ if (res.success) {
96
+ validCount++;
97
+ } else {
98
+ invalidCount++;
99
+ logger.error('migration', `Schema violation on node ${node.slug}`, { errors: res.error.errors });
100
+ }
101
+ }
102
+ }
103
+ }
104
+
105
+ if (invalidCount === 0) {
106
+ logger.info('migration', `Harness passed. ${validCount} target nodes successfully conform to new schema.`);
107
+ return true;
108
+ } else {
109
+ logger.error('migration', `Harness failed. ${invalidCount} nodes failed schema validation.`);
110
+ return false;
111
+ }
112
+ } catch (err) {
113
+ logger.error('migration', `Harness crashed during execution: ${err.message}`);
114
+ return false;
115
+ }
116
+ }