total-recall-brain 3.3.0 → 3.5.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 (62) hide show
  1. package/README.md +1 -1
  2. package/bin/antigravity.mjs +19 -8
  3. package/extension/background.js +13 -1
  4. package/extension/content-script.js +18 -1
  5. package/extension/lib/brain-client.js +18 -5
  6. package/extension/options/options.js +9 -4
  7. package/frontend/index.html +8 -0
  8. package/frontend/src/api.ts +20 -1
  9. package/frontend/src/pages/ChatPage.tsx +9 -6
  10. package/frontend/src/pages/HealthPage.tsx +179 -7
  11. package/frontend/src/pages/IntegrationsPage.tsx +4 -2
  12. package/frontend/src/pages/SettingsPage.tsx +197 -2
  13. package/frontend/src/types.ts +16 -0
  14. package/package.json +2 -2
  15. package/scaffold/.agent/skills/total-recall/SKILL.md +55 -6
  16. package/scaffold/.agent/skills/total-recall/memory-vault/invariants/operating-instructions.md +2 -2
  17. package/scaffold/.agent/skills/total-recall/research-queue.jsonl +1 -0
  18. package/scaffold/.agent/skills/total-recall/skills/cli-agents/agents.yml +1 -6
  19. package/scaffold/.agent/skills/total-recall/skills/ssss/SKILL.md +19 -0
  20. package/scaffold/.agent/skills/total-recall/skills/ssss/references/ssss-spec.md +33 -5
  21. package/src/cli/forget.mjs +18 -12
  22. package/src/cli/ingest/index.mjs +3 -2
  23. package/src/cli/map.mjs +2 -2
  24. package/src/cli/rebuild.mjs +1 -1
  25. package/src/cli/remember.mjs +21 -11
  26. package/src/cli/share.mjs +7 -2
  27. package/src/core/append-log.mjs +131 -0
  28. package/src/core/clarity-rewriter.mjs +10 -6
  29. package/src/core/conclusion-writer.mjs +3 -2
  30. package/src/core/config.mjs +6 -0
  31. package/src/core/conflict-detector.mjs +4 -2
  32. package/src/core/context-compiler.mjs +412 -0
  33. package/src/core/daemon-loop.mjs +15 -0
  34. package/src/core/dream.mjs +4 -4
  35. package/src/core/drift-detector.mjs +3 -3
  36. package/src/core/embeddings.mjs +60 -10
  37. package/src/core/fact-seeker.mjs +70 -44
  38. package/src/core/import-rules.mjs +3 -2
  39. package/src/core/inference-engine.mjs +5 -4
  40. package/src/core/migrate.mjs +4 -3
  41. package/src/core/optimizer.mjs +6 -5
  42. package/src/core/parallel-context.mjs +288 -0
  43. package/src/core/post-mortem.mjs +4 -3
  44. package/src/core/research-queue.mjs +4 -3
  45. package/src/core/runtime.mjs +30 -7
  46. package/src/core/sandbox.mjs +56 -26
  47. package/src/core/scheduler.mjs +6 -5
  48. package/src/core/schema.mjs +483 -2
  49. package/src/core/search.mjs +167 -101
  50. package/src/core/surface.mjs +203 -53
  51. package/src/core/validated-write.mjs +100 -0
  52. package/src/core/vault-cache.mjs +46 -18
  53. package/src/core/vault.mjs +17 -29
  54. package/src/core/vector-field.mjs +659 -0
  55. package/src/core/vector-store.mjs +85 -0
  56. package/src/server/api.mjs +4 -3
  57. package/src/server/index.mjs +13 -61
  58. package/src/server/rest.mjs +341 -63
  59. package/src/server/route-manifest.json +352 -77
  60. package/src/server/routes/memory.mjs +85 -39
  61. package/src/server/routes/sessions.mjs +6 -2
  62. package/src/server/routes/share.mjs +9 -2
@@ -7,13 +7,70 @@
7
7
  * MCP and REST are thin wrappers; all logic lives here.
8
8
  */
9
9
 
10
- import { loadNodes } from './vault.mjs';
10
+ import { getNodes } from './vault-cache.mjs';
11
11
  import {
12
12
  getEmbedding,
13
13
  cosineSimilarity,
14
14
  loadEmbeddingsIndex,
15
15
  loadSessionEmbeddingsIndex,
16
16
  } from './embeddings.mjs';
17
+ import { inferMemoryLayer, memoryLayerRoutingWeight } from './memory-layers.mjs';
18
+ import { VectorStore } from './vector-store.mjs';
19
+
20
+ /**
21
+ * Compute a temporal relevance boost for a vault node.
22
+ * Blends recency, confidence, access frequency, importance,
23
+ * memory layer, and priority into a multiplier applied to
24
+ * cosine similarity scores.
25
+ *
26
+ * Range: ~0.2 (cold/stale/research-tier) to ~2.0 (hot/absolute/conscious)
27
+ * Neutral (no temporal data): 1.0 (pure cosine similarity)
28
+ *
29
+ * @param {object|null} node - The vault node with frontmatter fields
30
+ * @returns {number} Temporal boost multiplier
31
+ */
32
+ function computeTemporalBoost(node) {
33
+ if (!node) return 1.0;
34
+ const now = Date.now();
35
+
36
+ // ── Recency: exponential decay from last_accessed ──
37
+ // Uses the node's own half_life_days if set, otherwise 7 days default
38
+ let recency = 1.0;
39
+ const lastAccessed = node.last_accessed || node.updated || node.created;
40
+ if (lastAccessed) {
41
+ const ageMs = now - new Date(lastAccessed).getTime();
42
+ const halfLifeDays = node.decay?.half_life_days || 7;
43
+ const halfLifeMs = halfLifeDays * 24 * 60 * 60 * 1000;
44
+ recency = Math.pow(0.5, ageMs / halfLifeMs);
45
+ recency = Math.max(recency, 0.05); // floor: never fully invisible
46
+ }
47
+
48
+ // ── Confidence: linear weight ──
49
+ // confidence 1.0 → 1.0, confidence 0.5 → 0.75, confidence 0 → 0.5
50
+ const confidence = typeof node.confidence === 'number' ? node.confidence : 1.0;
51
+ const confidenceWeight = 0.5 + (confidence * 0.5);
52
+
53
+ // ── Access frequency: log scale (from decay.access_count) ──
54
+ // 0 accesses → 1.0, 10 accesses → 1.23, 100 accesses → 1.46
55
+ const accessCount = node.decay?.access_count ?? 0;
56
+ const frequencySignal = 1.0 + (Math.log10(accessCount + 1) * 0.2);
57
+
58
+ // ── Importance tier: linear boost ──
59
+ // importance 1 → 0.8, 3 → 1.0, 5 → 1.2
60
+ const importance = typeof node.importance === 'number' ? node.importance : 3;
61
+ const importanceBoost = 0.8 + ((importance - 1) * 0.1);
62
+
63
+ // ── Priority override ──
64
+ // absolute priority nodes get a fixed boost to ensure they rank highly
65
+ const priorityBoost = node.priority === 'absolute' ? 1.3 : 1.0;
66
+
67
+ // ── Memory layer routing weight ──
68
+ // conscious=1.25, system2=1.0, research=0.75
69
+ const layer = inferMemoryLayer(node);
70
+ const layerWeight = memoryLayerRoutingWeight(layer);
71
+
72
+ return recency * confidenceWeight * frequencySignal * importanceBoost * priorityBoost * layerWeight;
73
+ }
17
74
 
18
75
  /**
19
76
  * Run a semantic search across vault nodes and/or session history.
@@ -44,125 +101,134 @@ export async function semanticSearch(query, {
44
101
  priority = null
45
102
  } = {}) {
46
103
  const k = Math.min(Number(top_k) || 5, 20);
104
+
105
+ // 1. Load nodes and apply filters
106
+ const allNodes = getNodes(vaultDir);
107
+ let filteredNodes = allNodes;
108
+
109
+ if (category) {
110
+ const catLower = category.toLowerCase();
111
+ filteredNodes = filteredNodes.filter(n => n.category && n.category.toLowerCase() === catLower);
112
+ }
113
+ if (tags && tags.length > 0) {
114
+ filteredNodes = filteredNodes.filter(n => n.tags && tags.some(t => n.tags.includes(t)));
115
+ }
116
+ if (modality) {
117
+ const modLower = modality.toLowerCase();
118
+ filteredNodes = filteredNodes.filter(n => n.modality && n.modality.toLowerCase() === modLower);
119
+ }
120
+ if (importance !== null && importance !== undefined) {
121
+ const minImp = parseInt(importance, 10);
122
+ filteredNodes = filteredNodes.filter(n => {
123
+ const val = parseInt(n.importance, 10);
124
+ return !isNaN(val) && val >= minImp;
125
+ });
126
+ }
127
+ if (priority) {
128
+ const prioLower = priority.toLowerCase();
129
+ filteredNodes = filteredNodes.filter(n => n.priority && n.priority.toLowerCase() === prioLower);
130
+ }
131
+
132
+ const filteredSlugs = new Set(filteredNodes.map(n => n.slug));
133
+ const results = [];
134
+
135
+ // Get query embedding
47
136
  let queryEmbedding = null;
48
137
  try {
49
138
  queryEmbedding = await getEmbedding(String(query));
50
139
  } catch (err) {
51
- // Gracefully ignore embedding generation errors to allow text search fallback
140
+ // Gracefully ignore embedding generation errors
52
141
  }
53
- const results = [];
54
142
 
55
- // If metadata filters are active, session log search is automatically bypassed
56
- const hasMetaFilters = category || (tags && tags.length > 0) || modality || importance !== null || priority;
57
- const activeIncludeSessions = hasMetaFilters ? false : includeSessions;
58
-
59
- // ── Vault nodes ────────────────────────────────────────────────────────────
60
143
  const vaultIndex = loadEmbeddingsIndex(derivedDir);
61
144
  const vaultEntries = Object.entries(vaultIndex);
62
145
 
146
+ // 2. Perform Semantic Search Channel (if embedding & index available)
147
+ let semanticRanked = [];
63
148
  if (queryEmbedding && vaultEntries.length > 0) {
64
- let filteredNodes = loadNodes(vaultDir);
65
-
66
- if (category) {
67
- const catLower = category.toLowerCase();
68
- filteredNodes = filteredNodes.filter(n => n.category && n.category.toLowerCase() === catLower);
69
- }
70
- if (tags && tags.length > 0) {
71
- filteredNodes = filteredNodes.filter(n => n.tags && tags.some(t => n.tags.includes(t)));
72
- }
73
- if (modality) {
74
- const modLower = modality.toLowerCase();
75
- filteredNodes = filteredNodes.filter(n => n.modality && n.modality.toLowerCase() === modLower);
76
- }
77
- if (importance !== null && importance !== undefined) {
78
- const minImp = parseInt(importance, 10);
79
- filteredNodes = filteredNodes.filter(n => {
80
- const val = parseInt(n.importance, 10);
81
- return !isNaN(val) && val >= minImp;
82
- });
83
- }
84
- if (priority) {
85
- const prioLower = priority.toLowerCase();
86
- filteredNodes = filteredNodes.filter(n => n.priority && n.priority.toLowerCase() === prioLower);
87
- }
149
+ const store = new VectorStore();
150
+ store.load(vaultIndex);
151
+ const matches = store.search(queryEmbedding, filteredNodes.length, filteredSlugs, cosineSimilarity);
152
+
153
+ semanticRanked = matches.map(({ slug, similarity }) => {
154
+ const node = filteredNodes.find(n => n.slug === slug);
155
+ const temporal = computeTemporalBoost(node);
156
+ return { slug, score: similarity * temporal };
157
+ });
158
+ }
88
159
 
89
- const filteredSlugs = new Set(filteredNodes.map(n => n.slug));
160
+ // 3. Perform Lexical Search Channel (Keyword matcher)
161
+ const qLower = String(query).toLowerCase();
162
+ let lexicalRanked = [];
163
+ for (const node of filteredNodes) {
164
+ let rawLexicalScore = 0;
165
+ const slug = String(node.slug || '').toLowerCase();
166
+ const title = String(node.title || '').toLowerCase();
167
+ const body = String(node.body || '').toLowerCase();
168
+ const nTags = Array.isArray(node.tags) ? node.tags.map(t => String(t).toLowerCase()) : [];
90
169
 
91
- const scored = vaultEntries
92
- .filter(([slug]) => filteredSlugs.has(slug))
93
- .map(([slug, { embedding }]) => ({ slug, score: cosineSimilarity(queryEmbedding, embedding) }))
94
- .sort((a, b) => b.score - a.score)
95
- .slice(0, k);
170
+ if (slug === qLower || title === qLower) {
171
+ rawLexicalScore = 1.0;
172
+ } else if (slug.includes(qLower) || title.includes(qLower)) {
173
+ rawLexicalScore = 0.8;
174
+ } else if (nTags.some(t => t.includes(qLower))) {
175
+ rawLexicalScore = 0.7;
176
+ } else if (body.includes(qLower)) {
177
+ rawLexicalScore = 0.5;
178
+ }
96
179
 
97
- for (const { slug, score } of scored) {
98
- const node = filteredNodes.find(n => n.slug === slug);
99
- if (node) {
100
- results.push({ ...node, type: 'vault', score: Math.round(score * 1000) / 1000 });
101
- }
180
+ if (rawLexicalScore > 0) {
181
+ const temporal = computeTemporalBoost(node);
182
+ lexicalRanked.push({ slug: node.slug, score: rawLexicalScore * temporal });
102
183
  }
103
- } else {
104
- // ── Text Search Fallback for Vault Nodes ──
105
- try {
106
- let filteredNodes = loadNodes(vaultDir);
184
+ }
185
+ lexicalRanked.sort((a, b) => b.score - a.score);
107
186
 
108
- if (category) {
109
- const catLower = category.toLowerCase();
110
- filteredNodes = filteredNodes.filter(n => n.category && n.category.toLowerCase() === catLower);
111
- }
112
- if (tags && tags.length > 0) {
113
- filteredNodes = filteredNodes.filter(n => n.tags && tags.some(t => n.tags.includes(t)));
114
- }
115
- if (modality) {
116
- const modLower = modality.toLowerCase();
117
- filteredNodes = filteredNodes.filter(n => n.modality && n.modality.toLowerCase() === modLower);
118
- }
119
- if (importance !== null && importance !== undefined) {
120
- const minImp = parseInt(importance, 10);
121
- filteredNodes = filteredNodes.filter(n => {
122
- const val = parseInt(n.importance, 10);
123
- return !isNaN(val) && val >= minImp;
124
- });
125
- }
126
- if (priority) {
127
- const prioLower = priority.toLowerCase();
128
- filteredNodes = filteredNodes.filter(n => n.priority && n.priority.toLowerCase() === prioLower);
129
- }
187
+ // 4. Blend Rankings via Reciprocal Rank Fusion (RRF)
188
+ let blendedRanked = [];
189
+ if (semanticRanked.length > 0) {
190
+ const rrfMap = new Map();
191
+ const RRF_K = 60; // Standard RRF parameter constant
130
192
 
131
- const qLower = String(query).toLowerCase();
132
- const scoredText = [];
133
-
134
- for (const node of filteredNodes) {
135
- let score = 0;
136
- const slug = String(node.slug || '').toLowerCase();
137
- const title = String(node.title || '').toLowerCase();
138
- const body = String(node.body || '').toLowerCase();
139
- const nTags = Array.isArray(node.tags) ? node.tags.map(t => String(t).toLowerCase()) : [];
140
-
141
- if (slug === qLower || title === qLower) {
142
- score = 0.9;
143
- } else if (slug.includes(qLower) || title.includes(qLower)) {
144
- score = 0.7;
145
- } else if (nTags.some(t => t.includes(qLower))) {
146
- score = 0.6;
147
- } else if (body.includes(qLower)) {
148
- score = 0.5;
149
- }
193
+ // Index rank positions (1-indexed)
194
+ semanticRanked.forEach((item, index) => {
195
+ rrfMap.set(item.slug, 1 / (RRF_K + (index + 1)));
196
+ });
150
197
 
151
- if (score > 0) {
152
- scoredText.push({ node, score });
153
- }
154
- }
198
+ lexicalRanked.forEach((item, index) => {
199
+ const semScore = rrfMap.get(item.slug) || 0;
200
+ rrfMap.set(item.slug, semScore + (1 / (RRF_K + (index + 1))));
201
+ });
155
202
 
156
- scoredText.sort((a, b) => b.score - a.score);
157
- for (const { node, score } of scoredText.slice(0, k)) {
158
- results.push({ ...node, type: 'vault', score });
159
- }
160
- } catch {
161
- // Gracefully ignore filesystem reading issues
203
+ // Convert back, score, and sort
204
+ blendedRanked = Array.from(rrfMap.entries())
205
+ .map(([slug, rrfScore]) => {
206
+ const node = filteredNodes.find(n => n.slug === slug);
207
+ // Normalize RRF score to [0, 1] relative to theoretical max of 2/60 (rank 1 in both)
208
+ const maxPossibleRrf = 2 / RRF_K;
209
+ const normalizedScore = rrfScore / maxPossibleRrf;
210
+ return { node, score: Math.round(normalizedScore * 1000) / 1000 };
211
+ })
212
+ .sort((a, b) => b.score - a.score);
213
+ } else {
214
+ // If semantic search is unavailable, fallback entirely to lexical ranking
215
+ blendedRanked = lexicalRanked.map(item => {
216
+ const node = filteredNodes.find(n => n.slug === item.slug);
217
+ return { node, score: Math.round(item.score * 1000) / 1000 };
218
+ });
219
+ }
220
+
221
+ // Slice top results
222
+ for (const { node, score } of blendedRanked.slice(0, k)) {
223
+ if (node) {
224
+ results.push({ ...node, type: 'vault', score });
162
225
  }
163
226
  }
164
227
 
165
- // ── Sessions ───────────────────────────────────────────────────────────────
228
+ // 5. Sessions Search Channel
229
+ const hasMetaFilters = category || (tags && tags.length > 0) || modality || importance !== null || priority;
230
+ const activeIncludeSessions = hasMetaFilters ? false : includeSessions;
231
+
166
232
  if (activeIncludeSessions) {
167
233
  const sessionIndex = loadSessionEmbeddingsIndex(derivedDir);
168
234
  const sessionEntries = Object.entries(sessionIndex);
@@ -184,9 +250,8 @@ export async function semanticSearch(query, {
184
250
  results.push({ type: 'session', key, session_id, snippet, chunk, total_chunks, score: Math.round(score * 1000) / 1000 });
185
251
  }
186
252
  } else {
187
- // ── Text Search Fallback for Sessions ──
253
+ // Lexical Fallback for Sessions
188
254
  try {
189
- const qLower = String(query).toLowerCase();
190
255
  const scoredText = [];
191
256
  for (const [key, entry] of sessionEntries) {
192
257
  const snippet = String(entry.snippet || '').toLowerCase();
@@ -215,5 +280,6 @@ export async function semanticSearch(query, {
215
280
  if (!queryEmbedding || vaultEntries.length === 0) {
216
281
  finalResults.degradedTextSearch = true;
217
282
  }
283
+
218
284
  return finalResults;
219
285
  }