total-recall-brain 3.18.3 → 3.19.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 (50) hide show
  1. package/frontend/dist/assets/{index-CGqvdwHT.js → index-B6AfuKv9.js} +115 -115
  2. package/frontend/dist/index.html +1 -1
  3. package/frontend/src/api/update.ts +29 -4
  4. package/frontend/src/pages/HealthPage.tsx +12 -16
  5. package/frontend/src/pages/SettingsPage.tsx +22 -1
  6. package/package.json +1 -1
  7. package/src/cli/agent-dir.mjs +15 -1
  8. package/src/cli/chat.mjs +1 -1
  9. package/src/cli/daemon.mjs +2 -1
  10. package/src/cli/dream.mjs +14 -10
  11. package/src/cli/forget.mjs +14 -2
  12. package/src/cli/friction.mjs +1 -1
  13. package/src/cli/import-rules.mjs +8 -3
  14. package/src/cli/ingest.mjs +3 -2
  15. package/src/cli/lint.mjs +9 -6
  16. package/src/cli/rebuild.mjs +9 -4
  17. package/src/cli/remember.mjs +21 -15
  18. package/src/cli/secret.mjs +4 -1
  19. package/src/cli/share.mjs +5 -3
  20. package/src/cli/skill-projection.mjs +21 -2
  21. package/src/cli/snapshot.mjs +10 -8
  22. package/src/cli/update.mjs +2 -1
  23. package/src/core/clarity-rewriter.mjs +31 -12
  24. package/src/core/config.mjs +10 -4
  25. package/src/core/daemon-loop.mjs +2 -1
  26. package/src/core/embeddings.mjs +51 -3
  27. package/src/core/memory-title.mjs +70 -0
  28. package/src/core/runtime.mjs +11 -1
  29. package/src/core/secrets-store.mjs +7 -1
  30. package/src/core/skills-registry.mjs +12 -0
  31. package/src/core/surface.mjs +4 -4
  32. package/src/core/throttled-fetch.mjs +5 -0
  33. package/src/core/vault.mjs +7 -1
  34. package/src/server/index.mjs +56 -0
  35. package/src/server/route-manifest.json +16 -0
  36. package/src/server/routes/_shared.mjs +33 -1
  37. package/src/server/routes/context.mjs +14 -9
  38. package/src/server/routes/dashboard.mjs +39 -18
  39. package/src/server/routes/docs.mjs +27 -13
  40. package/src/server/routes/dream.mjs +10 -11
  41. package/src/server/routes/export.mjs +43 -12
  42. package/src/server/routes/field.mjs +11 -9
  43. package/src/server/routes/graph.mjs +31 -13
  44. package/src/server/routes/import.mjs +35 -16
  45. package/src/server/routes/instructions.mjs +41 -5
  46. package/src/server/routes/memory.mjs +61 -21
  47. package/src/server/routes/share.mjs +2 -1
  48. package/src/server/routes/ssss.mjs +11 -3
  49. package/src/server/routes/update.mjs +22 -3
  50. package/src/server/routes/vault.mjs +53 -28
@@ -23,18 +23,14 @@ import { buildEmbeddingsIndex, buildSessionEmbeddingsIndex } from '../../core/em
23
23
  import { detectAndResolve } from '../../core/conflict-detector.mjs';
24
24
  import { listQueue, updateQueueItem } from '../../core/research-queue.mjs';
25
25
  import {
26
- BRAIN_DIR,
27
26
  VAULT_DIR,
28
- SKILLS_DIR,
29
- DERIVED_DIR,
30
- SESSIONS_DIR,
31
- INSTRUCTIONS,
32
27
  notFound,
33
28
  badRequest,
34
29
  serverError,
35
30
  sanitizeNode,
36
31
  resolveVaultFromQuery,
37
32
  resolveAllVaultsFromQuery,
33
+ pathsForVault,
38
34
  } from './_shared.mjs';
39
35
 
40
36
  const router = express.Router();
@@ -42,6 +38,8 @@ const router = express.Router();
42
38
  /* ── OPT-6: debounce recompilation across rapid consecutive writes ── */
43
39
  let recompileTimer = null;
44
40
  const RECOMPILE_DEBOUNCE_MS = 2000; // 2-second quiet period
41
+ /** @type {Map<string, ReturnType<typeof setTimeout>>} */
42
+ const recompileTimersByVault = new Map();
45
43
 
46
44
  function nodes(vaultDir = VAULT_DIR) {
47
45
  return getNodes(vaultDir);
@@ -52,40 +50,47 @@ function nodes(vaultDir = VAULT_DIR) {
52
50
  * Auto-mutates the brain in real time when any fact is written, updated, or deleted!
53
51
  */
54
52
  async function triggerMutation(node, vaultDir = VAULT_DIR) {
53
+ const paths = pathsForVault(vaultDir);
54
+
55
55
  // 1. Semantic conflict detection runs IMMEDIATELY (lightweight, per-node)
56
56
  try {
57
57
  const existing = nodes(vaultDir);
58
58
  if (node && node.type === 'memory') {
59
59
  detectAndResolve(node, existing, {
60
60
  vaultDir,
61
- inboxDir: path.join(BRAIN_DIR, 'memory-inbox'),
61
+ inboxDir: paths.inboxDir,
62
62
  });
63
63
  }
64
64
  } catch (conflictErr) {
65
65
  // Non-fatal fallback
66
66
  }
67
67
 
68
- // 2+3. Debounce the EXPENSIVE recompile + embeddings rebuild.
68
+ // 2+3. Debounce the EXPENSIVE recompile + embeddings rebuild per vault.
69
69
  // Rapid consecutive writes accumulate; only ONE compile fires
70
70
  // after a quiet period. Vault-cache is already invalidated
71
71
  // immediately at each call-site via invalidate().
72
- if (recompileTimer) clearTimeout(recompileTimer);
73
- recompileTimer = setTimeout(async () => {
74
- recompileTimer = null;
72
+ const prev = recompileTimersByVault.get(vaultDir);
73
+ if (prev) clearTimeout(prev);
74
+ // Keep legacy single-timer field in sync for default vault (tests/introspection)
75
+ if (vaultDir === VAULT_DIR && recompileTimer) clearTimeout(recompileTimer);
76
+
77
+ const timer = setTimeout(async () => {
78
+ recompileTimersByVault.delete(vaultDir);
79
+ if (vaultDir === VAULT_DIR) recompileTimer = null;
75
80
  try {
76
- // Recompile instructions surface
81
+ // Recompile instructions surface for THIS vault's brain
77
82
  await compileSurface({
78
83
  vaultDir,
79
- skillsDir: SKILLS_DIR,
80
- derivedDir: DERIVED_DIR,
81
- instructionsFile: INSTRUCTIONS,
84
+ skillsDir: paths.skillsDir,
85
+ derivedDir: paths.derivedDir,
86
+ instructionsFile: paths.instructionsFile,
82
87
  });
83
88
 
84
- // Rebuild dense embeddings index
89
+ // Rebuild dense embeddings index for THIS vault
85
90
  try {
86
91
  const vaultNodes = nodes(vaultDir);
87
- await buildEmbeddingsIndex(vaultNodes, DERIVED_DIR);
88
- await buildSessionEmbeddingsIndex(SESSIONS_DIR, DERIVED_DIR);
92
+ await buildEmbeddingsIndex(vaultNodes, paths.derivedDir);
93
+ await buildSessionEmbeddingsIndex(paths.sessionsDir, paths.derivedDir);
89
94
  } catch (embedErr) {
90
95
  // local_llm/embeddings offline non-fatal
91
96
  }
@@ -93,6 +98,9 @@ async function triggerMutation(node, vaultDir = VAULT_DIR) {
93
98
  // Non-fatal background log
94
99
  }
95
100
  }, RECOMPILE_DEBOUNCE_MS);
101
+
102
+ recompileTimersByVault.set(vaultDir, timer);
103
+ if (vaultDir === VAULT_DIR) recompileTimer = timer;
96
104
  }
97
105
 
98
106
 
@@ -414,16 +422,48 @@ router.delete('/api/memory/:slug', requireAuth, requireScope('memory:write'), (r
414
422
 
415
423
  /**
416
424
  * POST /api/memory/search/semantic
417
- * Body: { query: string, top_k?: number }
425
+ * Body: { query: string, top_k?: number, brainId?: string }
426
+ * Query/header: brain (same as other memory routes) — project:name, global, or multi.
418
427
  * Returns top-k vault nodes ranked by vector similarity to the query.
419
428
  */
420
429
  router.post('/api/memory/search/semantic', requireAuth, requireScope('memory:read'), async (req, res) => {
421
430
  try {
422
431
  const { query, top_k, include_sessions = true } = req.body || {};
423
432
  if (!query) return badRequest(res, 'query is required');
424
- const results = await semanticSearch(query, { vaultDir: VAULT_DIR, derivedDir: DERIVED_DIR, top_k, includeSessions: include_sessions });
425
- if (results.length === 0) return res.status(503).json({ error: 'Embeddings index is empty. Run POST /api/vault/compile to build it.' });
426
- res.json({ query, top_k: Math.min(Number(top_k) || 5, 20), results });
433
+
434
+ const k = Math.min(Number(top_k) || 5, 20);
435
+ const vaultDirs = resolveAllVaultsFromQuery(req);
436
+ const merged = [];
437
+ let anyIndex = false;
438
+
439
+ for (const vaultDir of vaultDirs) {
440
+ const { derivedDir } = pathsForVault(vaultDir);
441
+ try {
442
+ const part = await semanticSearch(query, {
443
+ vaultDir,
444
+ derivedDir,
445
+ top_k: k,
446
+ // Sessions only on the primary (first) vault to avoid cross-brain noise
447
+ includeSessions: include_sessions && vaultDir === vaultDirs[0],
448
+ });
449
+ if (part.length > 0) anyIndex = true;
450
+ for (const r of part) {
451
+ merged.push({ ...r, _brainVault: vaultDir });
452
+ }
453
+ } catch {
454
+ // per-vault search failure is non-fatal when multi-brain
455
+ }
456
+ }
457
+
458
+ merged.sort((a, b) => (b.score || 0) - (a.score || 0));
459
+ const results = merged.slice(0, k);
460
+
461
+ if (!anyIndex || results.length === 0) {
462
+ return res.status(503).json({
463
+ error: 'Embeddings index is empty. Run POST /api/vault/compile to build it.',
464
+ });
465
+ }
466
+ res.json({ query, top_k: k, results });
427
467
  } catch (err) {
428
468
  serverError(res, err);
429
469
  }
@@ -129,7 +129,8 @@ router.post('/api/share', requireAuth, requireScope('memory:write'), async (req,
129
129
  validation: vaultResult.validation,
130
130
  });
131
131
  }
132
- invalidate();
132
+ // Invalidate only the vault we wrote to (avoid thrashing other brain caches)
133
+ invalidate(vaultDir);
133
134
 
134
135
  // Mark extension as connected when share comes from the extension
135
136
  if (source && source.includes('extension')) {
@@ -139,15 +139,23 @@ router.get('/api/ssss', requireAuth, requireScope('ssss:read'), (req, res) => {
139
139
 
140
140
  router.get('/api/ssss/instructions', requireAuth, requireScope('ssss:read', 'instructions:read'), (req, res) => {
141
141
  const surface = req.query.surface;
142
+ // Prefer live INSTRUCTIONS constant (agent brain), then package ROOT fallback
143
+ const instructionsPath = fs.existsSync(INSTRUCTIONS)
144
+ ? INSTRUCTIONS
145
+ : path.join(ROOT, 'INSTRUCTIONS.md');
142
146
  if (surface) {
143
147
  if (surface === 'INSTRUCTIONS.md') {
144
- return sendTextResource(res, path.join(ROOT, 'INSTRUCTIONS.md'), 'instructions');
148
+ return sendTextResource(res, instructionsPath, 'instructions');
145
149
  }
146
150
  const safeSurface = path.basename(surface);
147
- const surfacePath = path.join(ROOT, safeSurface);
151
+ const candidates = [
152
+ path.join(path.dirname(instructionsPath), safeSurface),
153
+ path.join(ROOT, safeSurface),
154
+ ];
155
+ const surfacePath = candidates.find(p => fs.existsSync(p)) || candidates[0];
148
156
  return sendTextResource(res, surfacePath, safeSurface);
149
157
  }
150
- return sendTextResource(res, path.join(ROOT, 'INSTRUCTIONS.md'), 'instructions');
158
+ return sendTextResource(res, instructionsPath, 'instructions');
151
159
  });
152
160
 
153
161
  router.get('/api/ssss/skill/ssss', requireAuth, requireScope('ssss:read'), (_req, res) => {
@@ -100,11 +100,30 @@ router.post('/api/update/run', requireAuth, requireScope('config:write'), async
100
100
  }
101
101
  }
102
102
 
103
+ const failed = Number(summary?.failed || 0);
104
+ const updated = Number(summary?.updated || 0);
105
+ const upToDate = Number(summary?.up_to_date || 0);
106
+ const skipped = Number(
107
+ (summary?.results || []).filter((r) => String(r.status || '').startsWith('skipped')).length,
108
+ );
109
+ const ok = !summary?.skipped && failed === 0;
110
+ const parts = [];
111
+ if (updated) parts.push(`${updated} updated`);
112
+ if (upToDate) parts.push(`${upToDate} already current`);
113
+ if (skipped) parts.push(`${skipped} skipped`);
114
+ if (failed) parts.push(`${failed} failed`);
115
+ const detail = parts.length ? parts.join(', ') : 'no projects checked';
116
+
103
117
  res.json({
104
- updating: !dryRun,
118
+ success: ok,
119
+ updating: !dryRun && ok,
105
120
  message: dryRun
106
- ? 'Dry-run complete — see projects list'
107
- : 'Package auto-update finished for registered projects',
121
+ ? `Dry-run complete — ${detail}`
122
+ : ok
123
+ ? `Package auto-update finished for registered projects (${detail}). Latest: ${summary?.latest || 'n/a'}`
124
+ : summary?.reason === 'npm-view-failed'
125
+ ? 'Could not resolve latest version from npm (network or registry). Try again.'
126
+ : `Package auto-update finished with errors (${detail}). Latest: ${summary?.latest || 'n/a'}`,
108
127
  summary,
109
128
  });
110
129
  } catch (err) {
@@ -1,10 +1,17 @@
1
1
  import { Router } from 'express';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
- import crypto from 'node:crypto';
5
4
  import { requireAuth, requireScope } from '../auth.mjs';
6
- import { serverError, ROOT, BRAIN_DIR, VAULT_DIR, SKILLS_DIR, INSTRUCTIONS, DERIVED_DIR, SESSIONS_DIR } from './_shared.mjs';
7
- import { logger } from '../../core/logger.mjs';
5
+ import {
6
+ serverError,
7
+ VAULT_DIR,
8
+ SKILLS_DIR,
9
+ INSTRUCTIONS,
10
+ DERIVED_DIR,
11
+ SESSIONS_DIR,
12
+ resolveVaultFromQuery,
13
+ pathsForVault,
14
+ } from './_shared.mjs';
8
15
  import { compileSurface } from '../../core/surface.mjs';
9
16
  import { getNodes, invalidate } from '../../core/vault-cache.mjs';
10
17
  import { loadEmbeddingsIndex, loadSessionEmbeddingsIndex } from '../../core/embeddings.mjs';
@@ -14,22 +21,32 @@ const router = Router();
14
21
  router.post('/api/vault/compile', requireAuth, requireScope('memory:recompile'), async (req, res) => {
15
22
  try {
16
23
  const start = Date.now();
24
+ const vaultDir = resolveVaultFromQuery(req);
25
+ const paths = pathsForVault(vaultDir);
26
+
17
27
  await compileSurface({
18
- vaultDir: VAULT_DIR,
19
- skillsDir: SKILLS_DIR,
20
- derivedDir: DERIVED_DIR,
21
- instructionsFile: INSTRUCTIONS,
28
+ vaultDir,
29
+ skillsDir: paths.skillsDir,
30
+ derivedDir: paths.derivedDir,
31
+ instructionsFile: paths.instructionsFile,
32
+ force: true,
22
33
  });
23
- // Incrementally embed any new vault nodes and sessions
34
+ // Incrementally embed any new vault nodes and sessions for THIS brain
24
35
  let vaultEmbed = null, sessionEmbed = null;
25
36
  try {
26
37
  const { buildEmbeddingsIndex, buildSessionEmbeddingsIndex } = await import('../../core/embeddings.mjs');
27
- invalidate();
28
- const vaultNodes = getNodes(VAULT_DIR);
29
- vaultEmbed = await buildEmbeddingsIndex(vaultNodes, DERIVED_DIR);
30
- sessionEmbed = await buildSessionEmbeddingsIndex(SESSIONS_DIR, DERIVED_DIR);
38
+ invalidate(vaultDir);
39
+ const vaultNodes = getNodes(vaultDir);
40
+ vaultEmbed = await buildEmbeddingsIndex(vaultNodes, paths.derivedDir);
41
+ sessionEmbed = await buildSessionEmbeddingsIndex(paths.sessionsDir, paths.derivedDir);
31
42
  } catch { /* embedding service may be unavailable — non-fatal */ }
32
- res.json({ compiled: true, elapsed_ms: Date.now() - start, vault_embeddings: vaultEmbed, session_embeddings: sessionEmbed });
43
+ res.json({
44
+ compiled: true,
45
+ elapsed_ms: Date.now() - start,
46
+ vault_dir: vaultDir,
47
+ vault_embeddings: vaultEmbed,
48
+ session_embeddings: sessionEmbed,
49
+ });
33
50
  } catch (err) {
34
51
  serverError(res, err);
35
52
  }
@@ -47,9 +64,11 @@ router.post('/api/vault/compact', requireAuth, requireScope('memory:write'), asy
47
64
 
48
65
  router.get('/api/vault/hash', requireAuth, requireScope('memory:read'), (req, res) => {
49
66
  try {
50
- const hashFile = path.join(DERIVED_DIR, 'vault-hash.txt');
67
+ const vaultDir = resolveVaultFromQuery(req);
68
+ const { derivedDir } = pathsForVault(vaultDir);
69
+ const hashFile = path.join(derivedDir, 'vault-hash.txt');
51
70
  const hash = fs.existsSync(hashFile) ? fs.readFileSync(hashFile, 'utf8').trim() : null;
52
- res.json({ vault_hash: hash });
71
+ res.json({ vault_hash: hash, vault_dir: vaultDir });
53
72
  } catch (err) {
54
73
  serverError(res, err);
55
74
  }
@@ -57,19 +76,25 @@ router.get('/api/vault/hash', requireAuth, requireScope('memory:read'), (req, re
57
76
 
58
77
  router.get('/api/vault/status', requireAuth, requireScope('memory:read'), async (req, res) => {
59
78
  try {
60
- // Use vault-cache instead of scanning every .md file from disk
61
- const nodeCount = getNodes(VAULT_DIR).length;
62
- const skillCount = fs.existsSync(SKILLS_DIR) ? fs.readdirSync(SKILLS_DIR, { withFileTypes: true }).filter(d =>
63
- d.isDirectory() && fs.existsSync(path.join(SKILLS_DIR, d.name, 'SKILL.md'))).length : 0;
64
- const lastCompile = fs.existsSync(INSTRUCTIONS)
65
- ? fs.statSync(INSTRUCTIONS).mtime.toISOString()
79
+ const vaultDir = resolveVaultFromQuery(req);
80
+ const paths = pathsForVault(vaultDir);
81
+ // Prefer request-scoped paths; fall back to global constants for skills when agent dir is odd
82
+ const skillsDir = paths.skillsDir || SKILLS_DIR;
83
+ const derivedDir = paths.derivedDir || DERIVED_DIR;
84
+ const instructionsFile = paths.instructionsFile || INSTRUCTIONS;
85
+ const sessionsDir = paths.sessionsDir || SESSIONS_DIR;
86
+
87
+ const nodeCount = getNodes(vaultDir).length;
88
+ const skillCount = fs.existsSync(skillsDir) ? fs.readdirSync(skillsDir, { withFileTypes: true }).filter(d =>
89
+ d.isDirectory() && fs.existsSync(path.join(skillsDir, d.name, 'SKILL.md'))).length : 0;
90
+ const lastCompile = fs.existsSync(instructionsFile)
91
+ ? fs.statSync(instructionsFile).mtime.toISOString()
66
92
  : null;
67
- const derivedFiles = fs.existsSync(DERIVED_DIR)
68
- ? fs.readdirSync(DERIVED_DIR).length : 0;
93
+ const derivedFiles = fs.existsSync(derivedDir)
94
+ ? fs.readdirSync(derivedDir).length : 0;
69
95
 
70
- // Use mtime-cached loaders instead of raw readFile + JSON.parse
71
- const vaultEmbedCount = Object.keys(loadEmbeddingsIndex(DERIVED_DIR)).length;
72
- const sessionEmbedCount = Object.keys(loadSessionEmbeddingsIndex(DERIVED_DIR)).length;
96
+ const vaultEmbedCount = Object.keys(loadEmbeddingsIndex(derivedDir)).length;
97
+ const sessionEmbedCount = Object.keys(loadSessionEmbeddingsIndex(derivedDir)).length;
73
98
 
74
99
  // CLI agent availability (best-effort)
75
100
  const { findBinaryInPath } = await import('../../core/runtime.mjs');
@@ -79,12 +104,12 @@ router.get('/api/vault/status', requireAuth, requireScope('memory:read'), async
79
104
  }
80
105
 
81
106
  res.json({
82
- vault_dir: VAULT_DIR,
107
+ vault_dir: vaultDir,
83
108
  node_count: nodeCount,
84
109
  skill_count: skillCount,
85
110
  derived_files: derivedFiles,
86
111
  last_compile: lastCompile,
87
- instructions_exists: fs.existsSync(INSTRUCTIONS),
112
+ instructions_exists: fs.existsSync(instructionsFile),
88
113
  embeddings: {
89
114
  vault_nodes: vaultEmbedCount,
90
115
  session_chunks: sessionEmbedCount,