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.
- package/README.md +1 -1
- package/bin/antigravity.mjs +19 -8
- package/extension/background.js +13 -1
- package/extension/content-script.js +18 -1
- package/extension/lib/brain-client.js +18 -5
- package/extension/options/options.js +9 -4
- package/frontend/index.html +8 -0
- package/frontend/src/api.ts +20 -1
- package/frontend/src/pages/ChatPage.tsx +9 -6
- package/frontend/src/pages/HealthPage.tsx +179 -7
- package/frontend/src/pages/IntegrationsPage.tsx +4 -2
- package/frontend/src/pages/SettingsPage.tsx +197 -2
- package/frontend/src/types.ts +16 -0
- package/package.json +2 -2
- package/scaffold/.agent/skills/total-recall/SKILL.md +55 -6
- package/scaffold/.agent/skills/total-recall/memory-vault/invariants/operating-instructions.md +2 -2
- package/scaffold/.agent/skills/total-recall/research-queue.jsonl +1 -0
- package/scaffold/.agent/skills/total-recall/skills/cli-agents/agents.yml +1 -6
- package/scaffold/.agent/skills/total-recall/skills/ssss/SKILL.md +19 -0
- package/scaffold/.agent/skills/total-recall/skills/ssss/references/ssss-spec.md +33 -5
- package/src/cli/forget.mjs +18 -12
- package/src/cli/ingest/index.mjs +3 -2
- package/src/cli/map.mjs +2 -2
- package/src/cli/rebuild.mjs +1 -1
- package/src/cli/remember.mjs +21 -11
- package/src/cli/share.mjs +7 -2
- package/src/core/append-log.mjs +131 -0
- package/src/core/clarity-rewriter.mjs +10 -6
- package/src/core/conclusion-writer.mjs +3 -2
- package/src/core/config.mjs +6 -0
- package/src/core/conflict-detector.mjs +4 -2
- package/src/core/context-compiler.mjs +412 -0
- package/src/core/daemon-loop.mjs +15 -0
- package/src/core/dream.mjs +4 -4
- package/src/core/drift-detector.mjs +3 -3
- package/src/core/embeddings.mjs +60 -10
- package/src/core/fact-seeker.mjs +70 -44
- package/src/core/import-rules.mjs +3 -2
- package/src/core/inference-engine.mjs +5 -4
- package/src/core/migrate.mjs +4 -3
- package/src/core/optimizer.mjs +6 -5
- package/src/core/parallel-context.mjs +288 -0
- package/src/core/post-mortem.mjs +4 -3
- package/src/core/research-queue.mjs +4 -3
- package/src/core/runtime.mjs +30 -7
- package/src/core/sandbox.mjs +56 -26
- package/src/core/scheduler.mjs +6 -5
- package/src/core/schema.mjs +483 -2
- package/src/core/search.mjs +167 -101
- package/src/core/surface.mjs +203 -53
- package/src/core/validated-write.mjs +100 -0
- package/src/core/vault-cache.mjs +46 -18
- package/src/core/vault.mjs +17 -29
- package/src/core/vector-field.mjs +659 -0
- package/src/core/vector-store.mjs +85 -0
- package/src/server/api.mjs +4 -3
- package/src/server/index.mjs +13 -61
- package/src/server/rest.mjs +341 -63
- package/src/server/route-manifest.json +352 -77
- package/src/server/routes/memory.mjs +85 -39
- package/src/server/routes/sessions.mjs +6 -2
- package/src/server/routes/share.mjs +9 -2
package/src/core/search.mjs
CHANGED
|
@@ -7,13 +7,70 @@
|
|
|
7
7
|
* MCP and REST are thin wrappers; all logic lives here.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
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
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
98
|
-
const
|
|
99
|
-
|
|
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
|
-
}
|
|
104
|
-
|
|
105
|
-
try {
|
|
106
|
-
let filteredNodes = loadNodes(vaultDir);
|
|
184
|
+
}
|
|
185
|
+
lexicalRanked.sort((a, b) => b.score - a.score);
|
|
107
186
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
152
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
}
|