total-recall-brain 3.2.3 → 3.4.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/extension/background.js +14 -2
- package/extension/content-script.js +18 -1
- package/extension/lib/brain-client.js +86 -52
- package/extension/lib/preconfigured.js +5 -0
- package/extension/manifest.json +1 -1
- package/extension/options/options.html +1 -0
- package/extension/options/options.js +12 -6
- package/extension/popup/popup.html +1 -0
- package/extension/popup/popup.js +29 -4
- package/extension/sidepanel/sidepanel.html +1 -0
- package/extension/sidepanel/sidepanel.js +24 -6
- package/frontend/index.html +8 -0
- package/frontend/src/api.ts +1 -2
- package/frontend/src/pages/ChatPage.tsx +9 -6
- package/frontend/src/pages/IntegrationsPage.tsx +144 -3
- 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/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/init.mjs +30 -0
- package/src/cli/map.mjs +2 -2
- package/src/cli/rebuild.mjs +1 -1
- package/src/cli/remember.mjs +21 -11
- package/src/cli/setup.mjs +10 -1
- 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 +2 -2
- package/src/core/sandbox.mjs +56 -26
- package/src/core/scheduler.mjs +6 -5
- package/src/core/schema.mjs +2 -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 +238 -29
- package/src/server/route-manifest.json +344 -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
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/core/context-compiler.mjs
|
|
3
|
+
*
|
|
4
|
+
* Streaming Context Compiler — Dynamic per-request context assembly.
|
|
5
|
+
*
|
|
6
|
+
* The traditional surface compiler produces a STATIC INSTRUCTIONS.md
|
|
7
|
+
* that is injected wholesale into every agent turn. This module replaces
|
|
8
|
+
* that with a DYNAMIC context document assembled per-request from the
|
|
9
|
+
* vault, scored by temporal relevance, and packed within a token budget.
|
|
10
|
+
*
|
|
11
|
+
* Architecture:
|
|
12
|
+
* 1. CLASSIFY — route query to relevant domains
|
|
13
|
+
* 2. RETRIEVE — pull candidate nodes from vault
|
|
14
|
+
* 3. SCORE — rank by temporal_boost × semantic_similarity
|
|
15
|
+
* 4. BUDGET — pack into token budget via greedy knapsack
|
|
16
|
+
* 5. ASSEMBLE — build final context string from variable slots
|
|
17
|
+
*
|
|
18
|
+
* Variable Slots:
|
|
19
|
+
* ${identity} — agent identity (lore nodes, minimal)
|
|
20
|
+
* ${invariants} — absolute rules (always present, priority: absolute)
|
|
21
|
+
* ${corrections} — active corrections relevant to query domain
|
|
22
|
+
* ${preferences} — user preferences relevant to query domain
|
|
23
|
+
* ${facts} — domain facts retrieved by similarity
|
|
24
|
+
* ${decisions} — relevant design decisions
|
|
25
|
+
* ${patterns} — applicable patterns/anti-patterns
|
|
26
|
+
* ${skills} — routed SKILL.md content
|
|
27
|
+
* ${sessions} — relevant prior session context
|
|
28
|
+
*
|
|
29
|
+
* Every token earns its place through relevance. Nothing is injected by default.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { getNodes } from './vault-cache.mjs';
|
|
33
|
+
import {
|
|
34
|
+
getEmbedding,
|
|
35
|
+
cosineSimilarity,
|
|
36
|
+
loadEmbeddingsIndex,
|
|
37
|
+
loadSessionEmbeddingsIndex,
|
|
38
|
+
} from './embeddings.mjs';
|
|
39
|
+
import { inferMemoryLayer, memoryLayerRoutingWeight } from './memory-layers.mjs';
|
|
40
|
+
import { logger } from './logger.mjs';
|
|
41
|
+
|
|
42
|
+
// ─── Token Budget Defaults ──────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
const DEFAULT_BUDGET = {
|
|
45
|
+
total: 12000, // max tokens for compiled context
|
|
46
|
+
identity: 200, // lore/identity ceiling
|
|
47
|
+
invariants: 2000, // absolute rules ceiling
|
|
48
|
+
corrections: 1000, // corrections ceiling
|
|
49
|
+
preferences: 500, // preferences ceiling
|
|
50
|
+
facts: 3000, // domain facts ceiling
|
|
51
|
+
decisions: 1500, // design decisions ceiling
|
|
52
|
+
patterns: 1500, // patterns + anti-patterns ceiling
|
|
53
|
+
skills: 3000, // skill instructions ceiling
|
|
54
|
+
sessions: 1500, // session continuity ceiling
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Rough token estimation: ~4 chars per token (conservative)
|
|
58
|
+
const CHARS_PER_TOKEN = 4;
|
|
59
|
+
|
|
60
|
+
function estimateTokens(text) {
|
|
61
|
+
if (!text) return 0;
|
|
62
|
+
return Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ─── Temporal Scoring ───────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Compute temporal relevance boost for a vault node.
|
|
69
|
+
* Same formula as search.mjs but extracted for reuse.
|
|
70
|
+
*/
|
|
71
|
+
function temporalScore(node) {
|
|
72
|
+
if (!node) return 1.0;
|
|
73
|
+
const now = Date.now();
|
|
74
|
+
|
|
75
|
+
// Recency: exponential decay
|
|
76
|
+
let recency = 1.0;
|
|
77
|
+
const lastAccessed = node.last_accessed || node.updated || node.created;
|
|
78
|
+
if (lastAccessed) {
|
|
79
|
+
const ageMs = now - new Date(lastAccessed).getTime();
|
|
80
|
+
const halfLifeDays = node.decay?.half_life_days || 7;
|
|
81
|
+
const halfLifeMs = halfLifeDays * 24 * 60 * 60 * 1000;
|
|
82
|
+
recency = Math.pow(0.5, ageMs / halfLifeMs);
|
|
83
|
+
recency = Math.max(recency, 0.05);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Confidence
|
|
87
|
+
const confidence = typeof node.confidence === 'number' ? node.confidence : 1.0;
|
|
88
|
+
const confidenceWeight = 0.5 + (confidence * 0.5);
|
|
89
|
+
|
|
90
|
+
// Frequency
|
|
91
|
+
const accessCount = node.decay?.access_count ?? 0;
|
|
92
|
+
const frequencySignal = 1.0 + (Math.log10(accessCount + 1) * 0.2);
|
|
93
|
+
|
|
94
|
+
// Importance
|
|
95
|
+
const importance = typeof node.importance === 'number' ? node.importance : 3;
|
|
96
|
+
const importanceBoost = 0.8 + ((importance - 1) * 0.1);
|
|
97
|
+
|
|
98
|
+
// Priority
|
|
99
|
+
const priorityBoost = node.priority === 'absolute' ? 1.3 : 1.0;
|
|
100
|
+
|
|
101
|
+
// Layer
|
|
102
|
+
const layer = inferMemoryLayer(node);
|
|
103
|
+
const layerWeight = memoryLayerRoutingWeight(layer);
|
|
104
|
+
|
|
105
|
+
const momentumBoost = node._momentum ? 1.25 : 1.0;
|
|
106
|
+
return recency * confidenceWeight * frequencySignal * importanceBoost * priorityBoost * layerWeight * momentumBoost;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ─── Node Formatting ────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
function formatNode(node) {
|
|
112
|
+
const body = (node.body || node.content || '').trim();
|
|
113
|
+
if (!body) return '';
|
|
114
|
+
return body.startsWith('-') ? body : `- ${body}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function formatNodeWithMeta(node) {
|
|
118
|
+
const body = (node.body || node.content || '').trim();
|
|
119
|
+
if (!body) return '';
|
|
120
|
+
const meta = [];
|
|
121
|
+
if (node.title) meta.push(node.title);
|
|
122
|
+
if (node.slug) meta.push(`[${node.slug}]`);
|
|
123
|
+
const prefix = meta.length > 0 ? `**${meta.join(' ')}**: ` : '';
|
|
124
|
+
return `${prefix}${body}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ─── Context Slot Resolvers ─────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Resolve the invariants slot. These are ALWAYS included (guaranteed budget).
|
|
131
|
+
* Only absolute-priority and active invariants.
|
|
132
|
+
*/
|
|
133
|
+
function resolveInvariants(nodes, budget) {
|
|
134
|
+
const invariants = nodes
|
|
135
|
+
.filter(n => n.category === 'invariants' && n.status === 'active')
|
|
136
|
+
.sort((a, b) => {
|
|
137
|
+
// absolute priority first, then by importance desc
|
|
138
|
+
if (a.priority === 'absolute' && b.priority !== 'absolute') return -1;
|
|
139
|
+
if (b.priority === 'absolute' && a.priority !== 'absolute') return 1;
|
|
140
|
+
return (b.importance || 3) - (a.importance || 3);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
let result = '';
|
|
144
|
+
let tokens = 0;
|
|
145
|
+
for (const node of invariants) {
|
|
146
|
+
const text = formatNode(node);
|
|
147
|
+
const t = estimateTokens(text);
|
|
148
|
+
if (tokens + t > budget) break;
|
|
149
|
+
result += text + '\n';
|
|
150
|
+
tokens += t;
|
|
151
|
+
}
|
|
152
|
+
return { text: result.trim(), tokens };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Resolve a category slot with temporal scoring.
|
|
157
|
+
* Returns top nodes by temporal score within budget.
|
|
158
|
+
*/
|
|
159
|
+
function resolveCategory(nodes, category, budget, queryEmbedding, embeddingsIndex) {
|
|
160
|
+
const candidates = nodes.filter(n => n.category === category && n.status === 'active');
|
|
161
|
+
if (candidates.length === 0) return { text: '', tokens: 0 };
|
|
162
|
+
|
|
163
|
+
// Score each candidate
|
|
164
|
+
const scored = candidates.map(node => {
|
|
165
|
+
let score = temporalScore(node);
|
|
166
|
+
|
|
167
|
+
// Blend with semantic similarity if embedding available
|
|
168
|
+
if (queryEmbedding && embeddingsIndex[node.slug]) {
|
|
169
|
+
const sim = cosineSimilarity(queryEmbedding, embeddingsIndex[node.slug].embedding);
|
|
170
|
+
// Semantic similarity has strong weight for non-invariant categories
|
|
171
|
+
score *= (0.3 + sim * 0.7);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return { node, score };
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// Sort by score descending
|
|
178
|
+
scored.sort((a, b) => b.score - a.score);
|
|
179
|
+
|
|
180
|
+
// Pack into budget
|
|
181
|
+
let result = '';
|
|
182
|
+
let tokens = 0;
|
|
183
|
+
for (const { node } of scored) {
|
|
184
|
+
const text = formatNode(node);
|
|
185
|
+
const t = estimateTokens(text);
|
|
186
|
+
if (tokens + t > budget) break;
|
|
187
|
+
result += text + '\n';
|
|
188
|
+
tokens += t;
|
|
189
|
+
}
|
|
190
|
+
return { text: result.trim(), tokens };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Resolve session context with temporal and semantic scoring.
|
|
195
|
+
*/
|
|
196
|
+
function resolveSessions(derivedDir, budget, queryEmbedding) {
|
|
197
|
+
if (!queryEmbedding || !derivedDir) return { text: '', tokens: 0 };
|
|
198
|
+
|
|
199
|
+
const sessionIndex = loadSessionEmbeddingsIndex(derivedDir);
|
|
200
|
+
const entries = Object.entries(sessionIndex);
|
|
201
|
+
if (entries.length === 0) return { text: '', tokens: 0 };
|
|
202
|
+
|
|
203
|
+
const now = Date.now();
|
|
204
|
+
const scored = entries.map(([key, entry]) => {
|
|
205
|
+
const sim = cosineSimilarity(queryEmbedding, entry.embedding);
|
|
206
|
+
// Temporal boost for sessions: recent sessions score higher
|
|
207
|
+
const generatedAt = entry.generated_at ? new Date(entry.generated_at).getTime() : now;
|
|
208
|
+
const ageMs = now - generatedAt;
|
|
209
|
+
const halfLifeMs = 3 * 24 * 60 * 60 * 1000; // 3 day half-life for sessions
|
|
210
|
+
const recency = Math.max(Math.pow(0.5, ageMs / halfLifeMs), 0.05);
|
|
211
|
+
return { key, entry, score: sim * recency };
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
scored.sort((a, b) => b.score - a.score);
|
|
215
|
+
|
|
216
|
+
let result = '';
|
|
217
|
+
let tokens = 0;
|
|
218
|
+
for (const { entry } of scored) {
|
|
219
|
+
const snippet = entry.snippet || '';
|
|
220
|
+
if (!snippet) continue;
|
|
221
|
+
const text = `> ${snippet}`;
|
|
222
|
+
const t = estimateTokens(text);
|
|
223
|
+
if (tokens + t > budget) break;
|
|
224
|
+
result += text + '\n';
|
|
225
|
+
tokens += t;
|
|
226
|
+
}
|
|
227
|
+
return { text: result.trim(), tokens };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ─── Main Compiler ──────────────────────────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Compile a dynamic context document for a specific query.
|
|
234
|
+
*
|
|
235
|
+
* @param {object} opts
|
|
236
|
+
* @param {string} opts.query - The user query/task description
|
|
237
|
+
* @param {string} opts.vaultDir - Path to the vault
|
|
238
|
+
* @param {string} opts.derivedDir - Path to derived indexes
|
|
239
|
+
* @param {object} [opts.budget] - Token budget overrides per slot
|
|
240
|
+
* @param {string} [opts.consumer='api'] - 'api' or 'ide'
|
|
241
|
+
* @param {string[]} [opts.momentumSlugs] - Recent node slugs for momentum boost
|
|
242
|
+
* @returns {Promise<{ context: string, stats: object }>}
|
|
243
|
+
*/
|
|
244
|
+
export async function compileContext({
|
|
245
|
+
query,
|
|
246
|
+
vaultDir,
|
|
247
|
+
derivedDir,
|
|
248
|
+
budget = {},
|
|
249
|
+
consumer = 'api',
|
|
250
|
+
momentumSlugs = [],
|
|
251
|
+
} = {}) {
|
|
252
|
+
const startTime = Date.now();
|
|
253
|
+
const b = { ...DEFAULT_BUDGET, ...budget };
|
|
254
|
+
const allNodes = getNodes(vaultDir).map(n => ({ ...n }));
|
|
255
|
+
const stats = { total_nodes: allNodes.length, slots: {} };
|
|
256
|
+
|
|
257
|
+
// ── Generate query embedding for semantic scoring ──
|
|
258
|
+
let queryEmbedding = null;
|
|
259
|
+
try {
|
|
260
|
+
if (query) {
|
|
261
|
+
queryEmbedding = await getEmbedding(String(query));
|
|
262
|
+
}
|
|
263
|
+
} catch { /* graceful degradation */ }
|
|
264
|
+
|
|
265
|
+
// ── Load embeddings index for similarity scoring ──
|
|
266
|
+
let embeddingsIndex = {};
|
|
267
|
+
try {
|
|
268
|
+
embeddingsIndex = loadEmbeddingsIndex(derivedDir);
|
|
269
|
+
} catch { /* graceful degradation */ }
|
|
270
|
+
|
|
271
|
+
// ── Apply momentum boost to recently-touched nodes ──
|
|
272
|
+
if (momentumSlugs.length > 0) {
|
|
273
|
+
const momentumSet = new Set(momentumSlugs);
|
|
274
|
+
for (const node of allNodes) {
|
|
275
|
+
if (momentumSet.has(node.slug)) {
|
|
276
|
+
// Temporarily boost last_accessed to now for momentum nodes
|
|
277
|
+
node._momentum = true;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ── Resolve each variable slot ──
|
|
283
|
+
const sections = [];
|
|
284
|
+
let totalTokens = 0;
|
|
285
|
+
|
|
286
|
+
// SLOT 1: Invariants (guaranteed — always present)
|
|
287
|
+
const invariants = resolveInvariants(allNodes, b.invariants);
|
|
288
|
+
if (invariants.text) {
|
|
289
|
+
sections.push({ label: 'Rules', content: invariants.text });
|
|
290
|
+
totalTokens += invariants.tokens;
|
|
291
|
+
stats.slots.invariants = { nodes: invariants.text.split('\n').length, tokens: invariants.tokens };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// SLOT 2: Corrections (query-relevant anti-patterns)
|
|
295
|
+
const remaining = b.total - totalTokens;
|
|
296
|
+
const corrections = resolveCategory(allNodes, 'anti-patterns', Math.min(b.corrections, remaining), queryEmbedding, embeddingsIndex);
|
|
297
|
+
if (corrections.text) {
|
|
298
|
+
sections.push({ label: 'Corrections', content: corrections.text });
|
|
299
|
+
totalTokens += corrections.tokens;
|
|
300
|
+
stats.slots.corrections = { tokens: corrections.tokens };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// SLOT 3: Preferences (query-relevant)
|
|
304
|
+
const prefBudget = Math.min(b.preferences, b.total - totalTokens);
|
|
305
|
+
const preferences = resolveCategory(allNodes, 'preferences', prefBudget, queryEmbedding, embeddingsIndex);
|
|
306
|
+
if (preferences.text) {
|
|
307
|
+
sections.push({ label: 'Preferences', content: preferences.text });
|
|
308
|
+
totalTokens += preferences.tokens;
|
|
309
|
+
stats.slots.preferences = { tokens: preferences.tokens };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// SLOT 4: Facts (semantically retrieved domain knowledge)
|
|
313
|
+
const factBudget = Math.min(b.facts, b.total - totalTokens);
|
|
314
|
+
const facts = resolveCategory(allNodes, 'facts', factBudget, queryEmbedding, embeddingsIndex);
|
|
315
|
+
if (facts.text) {
|
|
316
|
+
sections.push({ label: 'Context', content: facts.text });
|
|
317
|
+
totalTokens += facts.tokens;
|
|
318
|
+
stats.slots.facts = { tokens: facts.tokens };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// SLOT 5: Decisions (relevant design decisions)
|
|
322
|
+
const decBudget = Math.min(b.decisions, b.total - totalTokens);
|
|
323
|
+
const decisions = resolveCategory(allNodes, 'decisions', decBudget, queryEmbedding, embeddingsIndex);
|
|
324
|
+
if (decisions.text) {
|
|
325
|
+
sections.push({ label: 'Decisions', content: decisions.text });
|
|
326
|
+
totalTokens += decisions.tokens;
|
|
327
|
+
stats.slots.decisions = { tokens: decisions.tokens };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// SLOT 6: Patterns + Anti-patterns
|
|
331
|
+
const patBudget = Math.min(b.patterns, b.total - totalTokens);
|
|
332
|
+
const patterns = resolveCategory(allNodes, 'patterns', patBudget, queryEmbedding, embeddingsIndex);
|
|
333
|
+
if (patterns.text) {
|
|
334
|
+
sections.push({ label: 'Patterns', content: patterns.text });
|
|
335
|
+
totalTokens += patterns.tokens;
|
|
336
|
+
stats.slots.patterns = { tokens: patterns.tokens };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// SLOT 7: Concepts (deep understanding nodes)
|
|
340
|
+
const conBudget = Math.min(b.facts, b.total - totalTokens); // shares budget class with facts
|
|
341
|
+
const concepts = resolveCategory(allNodes, 'concepts', conBudget, queryEmbedding, embeddingsIndex);
|
|
342
|
+
if (concepts.text) {
|
|
343
|
+
sections.push({ label: 'Concepts', content: concepts.text });
|
|
344
|
+
totalTokens += concepts.tokens;
|
|
345
|
+
stats.slots.concepts = { tokens: concepts.tokens };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// SLOT 8: Lore (identity, only if relevant)
|
|
349
|
+
const loreBudget = Math.min(b.identity, b.total - totalTokens);
|
|
350
|
+
const lore = resolveCategory(allNodes, 'lore', loreBudget, queryEmbedding, embeddingsIndex);
|
|
351
|
+
if (lore.text) {
|
|
352
|
+
sections.push({ label: 'Identity', content: lore.text });
|
|
353
|
+
totalTokens += lore.tokens;
|
|
354
|
+
stats.slots.lore = { tokens: lore.tokens };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// SLOT 9: Sessions (prior conversation context)
|
|
358
|
+
const sessBudget = Math.min(b.sessions, b.total - totalTokens);
|
|
359
|
+
const sessions = resolveSessions(derivedDir, sessBudget, queryEmbedding);
|
|
360
|
+
if (sessions.text) {
|
|
361
|
+
sections.push({ label: 'Prior Context', content: sessions.text });
|
|
362
|
+
totalTokens += sessions.tokens;
|
|
363
|
+
stats.slots.sessions = { tokens: sessions.tokens };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// ── Assemble final context ──
|
|
367
|
+
const context = sections
|
|
368
|
+
.map(s => `## ${s.label}\n\n${s.content}`)
|
|
369
|
+
.join('\n\n---\n\n');
|
|
370
|
+
|
|
371
|
+
stats.total_tokens = totalTokens;
|
|
372
|
+
stats.budget_used = totalTokens;
|
|
373
|
+
stats.budget_remaining = b.total - totalTokens;
|
|
374
|
+
stats.compile_ms = Date.now() - startTime;
|
|
375
|
+
stats.slots_filled = sections.length;
|
|
376
|
+
|
|
377
|
+
logger.info('context-compiler', `Compiled ${sections.length} slots, ${totalTokens} tokens in ${stats.compile_ms}ms`);
|
|
378
|
+
|
|
379
|
+
return { context, stats };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Lightweight variant that returns pre-scored candidates without embedding lookup.
|
|
384
|
+
* Used for fast context previews and budget estimation.
|
|
385
|
+
*/
|
|
386
|
+
export function previewContext({ vaultDir, budget = {} } = {}) {
|
|
387
|
+
const b = { ...DEFAULT_BUDGET, ...budget };
|
|
388
|
+
const allNodes = getNodes(vaultDir).map(n => ({ ...n }));
|
|
389
|
+
const active = allNodes.filter(n => n.status === 'active');
|
|
390
|
+
|
|
391
|
+
const candidates = active.map(node => ({
|
|
392
|
+
slug: node.slug,
|
|
393
|
+
category: node.category,
|
|
394
|
+
title: node.title,
|
|
395
|
+
temporal_score: temporalScore(node),
|
|
396
|
+
tokens: estimateTokens(node.body || node.content || ''),
|
|
397
|
+
layer: inferMemoryLayer(node),
|
|
398
|
+
priority: node.priority || 'normal',
|
|
399
|
+
importance: node.importance || 3,
|
|
400
|
+
confidence: node.confidence ?? 1.0,
|
|
401
|
+
last_accessed: node.last_accessed || null,
|
|
402
|
+
}));
|
|
403
|
+
|
|
404
|
+
candidates.sort((a, b) => b.temporal_score - a.temporal_score);
|
|
405
|
+
|
|
406
|
+
return {
|
|
407
|
+
candidates,
|
|
408
|
+
budget: b,
|
|
409
|
+
total_candidates: candidates.length,
|
|
410
|
+
total_tokens: candidates.reduce((sum, c) => sum + c.tokens, 0),
|
|
411
|
+
};
|
|
412
|
+
}
|
package/src/core/daemon-loop.mjs
CHANGED
|
@@ -80,6 +80,10 @@ async function dispatchTask(task) {
|
|
|
80
80
|
case 'memory-maintenance':
|
|
81
81
|
return await runMaintenanceTask(task);
|
|
82
82
|
|
|
83
|
+
case 'proactive-research':
|
|
84
|
+
case 'research-acquisition':
|
|
85
|
+
return await runResearchTask(task);
|
|
86
|
+
|
|
83
87
|
default:
|
|
84
88
|
// All other categories (research, deliberation, inference, etc.)
|
|
85
89
|
// require LLM and are handled by IDE agents, not the daemon.
|
|
@@ -250,6 +254,17 @@ async function main() {
|
|
|
250
254
|
} catch (err) {
|
|
251
255
|
logger.info({ subsystem: 'daemon-loop', message: `Surface recompile error: ${err.message}` });
|
|
252
256
|
}
|
|
257
|
+
|
|
258
|
+
// Compact any append logs with accumulated dirty entries
|
|
259
|
+
try {
|
|
260
|
+
const { compactAppendLogs } = await import('./append-log.mjs');
|
|
261
|
+
const compactResult = compactAppendLogs();
|
|
262
|
+
if (compactResult.logs_compacted > 0) {
|
|
263
|
+
logger.info({ subsystem: 'daemon-loop', message: `Compacted ${compactResult.logs_compacted} append logs (${compactResult.total_entries} entries)` });
|
|
264
|
+
}
|
|
265
|
+
} catch (err) {
|
|
266
|
+
logger.info({ subsystem: 'daemon-loop', message: `Append log compaction error: ${err.message}` });
|
|
267
|
+
}
|
|
253
268
|
}
|
|
254
269
|
|
|
255
270
|
// Refresh scheduler from disk on each iteration
|
package/src/core/dream.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import os from 'os';
|
|
4
|
-
import {
|
|
4
|
+
import { writeNode, atomicWrite, walkMd } from './vault.mjs';
|
|
5
|
+
import { getNodes } from './vault-cache.mjs';
|
|
5
6
|
import { logger } from './logger.mjs';
|
|
6
7
|
import { agentDir, brainDir } from './config.mjs';
|
|
7
8
|
import { loadQueue } from './research-queue.mjs';
|
|
@@ -247,7 +248,7 @@ export async function runDreamCycle({
|
|
|
247
248
|
const candidates = [];
|
|
248
249
|
|
|
249
250
|
logger.info('dream', 'PHASE 2 — REM (Pattern Recognition)');
|
|
250
|
-
const existingNodes =
|
|
251
|
+
const existingNodes = getNodes(vaultDir);
|
|
251
252
|
|
|
252
253
|
if (candidates.length > 0) {
|
|
253
254
|
const { promoted, conflicted } = evaluateCandidates(candidates, existingNodes, conflictsDir);
|
|
@@ -294,7 +295,7 @@ export async function runDreamCycle({
|
|
|
294
295
|
|
|
295
296
|
// Write daily note summary (native SSSS node; Obsidian Daily Notes reads it directly)
|
|
296
297
|
try {
|
|
297
|
-
const existingNodes =
|
|
298
|
+
const existingNodes = getNodes(vaultDir);
|
|
298
299
|
writeDailyNote(vaultDir, [
|
|
299
300
|
`Modified vault files scanned: ${modified.length}`,
|
|
300
301
|
`Active nodes: ${existingNodes.filter(n => n.status === 'active').length}`,
|
|
@@ -318,7 +319,6 @@ export async function runDreamCycle({
|
|
|
318
319
|
// ─── Standalone Daemon Execution ────────────────────────────────────────────────
|
|
319
320
|
|
|
320
321
|
import { fileURLToPath } from 'url';
|
|
321
|
-
import os from 'os';
|
|
322
322
|
|
|
323
323
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
324
324
|
const vaultDir = path.join(brainDir, 'memory-vault');
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import crypto from 'node:crypto';
|
|
4
|
-
import {
|
|
4
|
+
import { getNodes } from './vault-cache.mjs';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* SSSS Drift Detector
|
|
@@ -45,10 +45,10 @@ export function detectIndexDrift(vaultDir, derivedDir, globalVaultDir = undefine
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
// Load canonical nodes
|
|
48
|
-
const nodes =
|
|
48
|
+
const nodes = getNodes(vaultDir);
|
|
49
49
|
if (globalVaultDir && fs.existsSync(globalVaultDir)) {
|
|
50
50
|
try {
|
|
51
|
-
const globalNodes =
|
|
51
|
+
const globalNodes = getNodes(globalVaultDir);
|
|
52
52
|
nodes.push(...globalNodes);
|
|
53
53
|
} catch (err) {
|
|
54
54
|
// Ignore or log global vault load failure
|
package/src/core/embeddings.mjs
CHANGED
|
@@ -360,10 +360,14 @@ export function loadEmbeddingsIndex(derivedDir) {
|
|
|
360
360
|
* @param {string} [model]
|
|
361
361
|
*/
|
|
362
362
|
export function saveEmbeddingToIndex(derivedDir, slug, embedding, model = DEFAULT_EMBED_MODEL) {
|
|
363
|
-
|
|
363
|
+
const p = indexPath(derivedDir);
|
|
364
364
|
const index = loadEmbeddingsIndex(derivedDir);
|
|
365
365
|
index[slug] = { embedding, model, generated_at: new Date().toISOString() };
|
|
366
|
-
fs.
|
|
366
|
+
fs.mkdirSync(derivedDir, { recursive: true });
|
|
367
|
+
fs.writeFileSync(p, JSON.stringify(index), 'utf8');
|
|
368
|
+
cachedIndex = index;
|
|
369
|
+
cachedIndexPath = p;
|
|
370
|
+
cachedIndexMtime = fs.statSync(p).mtimeMs;
|
|
367
371
|
}
|
|
368
372
|
|
|
369
373
|
/**
|
|
@@ -374,10 +378,13 @@ export function saveEmbeddingToIndex(derivedDir, slug, embedding, model = DEFAUL
|
|
|
374
378
|
*/
|
|
375
379
|
export function removeEmbeddingFromIndex(derivedDir, slug) {
|
|
376
380
|
const p = indexPath(derivedDir);
|
|
377
|
-
if (!fs.existsSync(p)) return;
|
|
378
381
|
const index = loadEmbeddingsIndex(derivedDir);
|
|
379
382
|
delete index[slug];
|
|
383
|
+
fs.mkdirSync(derivedDir, { recursive: true });
|
|
380
384
|
fs.writeFileSync(p, JSON.stringify(index), 'utf8');
|
|
385
|
+
cachedIndex = index;
|
|
386
|
+
cachedIndexPath = p;
|
|
387
|
+
cachedIndexMtime = fs.statSync(p).mtimeMs;
|
|
381
388
|
}
|
|
382
389
|
|
|
383
390
|
// ─── Text representation ─────────────────────────────────────────────────────
|
|
@@ -390,13 +397,26 @@ export function removeEmbeddingFromIndex(derivedDir, slug) {
|
|
|
390
397
|
* @returns {string}
|
|
391
398
|
*/
|
|
392
399
|
export function nodeToEmbedText(node) {
|
|
393
|
-
const parts = [
|
|
400
|
+
const parts = [];
|
|
401
|
+
const title = node.title || node.slug;
|
|
402
|
+
parts.push(`Title: ${title}`);
|
|
394
403
|
if (node.category) parts.push(`Category: ${node.category}`);
|
|
395
404
|
if (node.tags?.length) parts.push(`Tags: ${node.tags.join(', ')}`);
|
|
396
|
-
if (node.body) parts.push(node.body.slice(0, 2000));
|
|
405
|
+
if (node.body) parts.push(`Body: ${node.body.slice(0, 2000)}`);
|
|
397
406
|
return parts.join('\n\n');
|
|
398
407
|
}
|
|
399
408
|
|
|
409
|
+
/**
|
|
410
|
+
* Split a node's body into paragraph chunks for hierarchical child matching.
|
|
411
|
+
*/
|
|
412
|
+
export function chunkNodeBody(body) {
|
|
413
|
+
if (!body) return [];
|
|
414
|
+
return body
|
|
415
|
+
.split(/\n\n+/)
|
|
416
|
+
.map(p => p.trim())
|
|
417
|
+
.filter(p => p.length > 50); // only chunk substantial paragraphs
|
|
418
|
+
}
|
|
419
|
+
|
|
400
420
|
// ─── Bulk index builder ──────────────────────────────────────────────────────
|
|
401
421
|
|
|
402
422
|
/**
|
|
@@ -430,14 +450,44 @@ export async function buildEmbeddingsIndex(nodes, derivedDir, opts = {}) {
|
|
|
430
450
|
let built = 0, skipped = 0, failed = 0;
|
|
431
451
|
|
|
432
452
|
for (const node of nodes) {
|
|
453
|
+
const text = nodeToEmbedText(node);
|
|
454
|
+
const contentHash = crypto.createHash('sha256').update(text).digest('hex');
|
|
455
|
+
|
|
456
|
+
// Skip if existing entry has matching content hash (content unchanged)
|
|
433
457
|
if (!force && existing[node.slug]) {
|
|
434
|
-
|
|
435
|
-
|
|
458
|
+
const cachedHash = existing[node.slug].content_sha256;
|
|
459
|
+
if (cachedHash && cachedHash === contentHash) {
|
|
460
|
+
skipped++;
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
436
463
|
}
|
|
464
|
+
|
|
437
465
|
try {
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
|
|
466
|
+
const parentEmbedding = await getEmbedding(text, undefined, model);
|
|
467
|
+
|
|
468
|
+
// Calculate embeddings for children chunks
|
|
469
|
+
const childChunks = chunkNodeBody(node.body);
|
|
470
|
+
const chunks = [];
|
|
471
|
+
|
|
472
|
+
for (const chunkText of childChunks) {
|
|
473
|
+
try {
|
|
474
|
+
const chunkEmb = await getEmbedding(`Parent: ${node.title}\nChunk: ${chunkText}`, undefined, model);
|
|
475
|
+
chunks.push({
|
|
476
|
+
text: chunkText,
|
|
477
|
+
embedding: chunkEmb
|
|
478
|
+
});
|
|
479
|
+
} catch {
|
|
480
|
+
// Ignore failures for individual chunks
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
index[node.slug] = {
|
|
485
|
+
embedding: parentEmbedding,
|
|
486
|
+
chunks,
|
|
487
|
+
model,
|
|
488
|
+
generated_at: new Date().toISOString(),
|
|
489
|
+
content_sha256: contentHash
|
|
490
|
+
};
|
|
441
491
|
built++;
|
|
442
492
|
onProgress?.({ slug: node.slug, built, skipped, failed });
|
|
443
493
|
} catch (err) {
|