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.
- package/frontend/dist/assets/{index-CGqvdwHT.js → index-B6AfuKv9.js} +115 -115
- package/frontend/dist/index.html +1 -1
- package/frontend/src/api/update.ts +29 -4
- package/frontend/src/pages/HealthPage.tsx +12 -16
- package/frontend/src/pages/SettingsPage.tsx +22 -1
- package/package.json +1 -1
- package/src/cli/agent-dir.mjs +15 -1
- package/src/cli/chat.mjs +1 -1
- package/src/cli/daemon.mjs +2 -1
- package/src/cli/dream.mjs +14 -10
- package/src/cli/forget.mjs +14 -2
- package/src/cli/friction.mjs +1 -1
- package/src/cli/import-rules.mjs +8 -3
- package/src/cli/ingest.mjs +3 -2
- package/src/cli/lint.mjs +9 -6
- package/src/cli/rebuild.mjs +9 -4
- package/src/cli/remember.mjs +21 -15
- package/src/cli/secret.mjs +4 -1
- package/src/cli/share.mjs +5 -3
- package/src/cli/skill-projection.mjs +21 -2
- package/src/cli/snapshot.mjs +10 -8
- package/src/cli/update.mjs +2 -1
- package/src/core/clarity-rewriter.mjs +31 -12
- package/src/core/config.mjs +10 -4
- package/src/core/daemon-loop.mjs +2 -1
- package/src/core/embeddings.mjs +51 -3
- package/src/core/memory-title.mjs +70 -0
- package/src/core/runtime.mjs +11 -1
- package/src/core/secrets-store.mjs +7 -1
- package/src/core/skills-registry.mjs +12 -0
- package/src/core/surface.mjs +4 -4
- package/src/core/throttled-fetch.mjs +5 -0
- package/src/core/vault.mjs +7 -1
- package/src/server/index.mjs +56 -0
- package/src/server/route-manifest.json +16 -0
- package/src/server/routes/_shared.mjs +33 -1
- package/src/server/routes/context.mjs +14 -9
- package/src/server/routes/dashboard.mjs +39 -18
- package/src/server/routes/docs.mjs +27 -13
- package/src/server/routes/dream.mjs +10 -11
- package/src/server/routes/export.mjs +43 -12
- package/src/server/routes/field.mjs +11 -9
- package/src/server/routes/graph.mjs +31 -13
- package/src/server/routes/import.mjs +35 -16
- package/src/server/routes/instructions.mjs +41 -5
- package/src/server/routes/memory.mjs +61 -21
- package/src/server/routes/share.mjs +2 -1
- package/src/server/routes/ssss.mjs +11 -3
- package/src/server/routes/update.mjs +22 -3
- 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:
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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:
|
|
80
|
-
derivedDir:
|
|
81
|
-
instructionsFile:
|
|
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,
|
|
88
|
-
await buildSessionEmbeddingsIndex(
|
|
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
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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
|
-
|
|
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,
|
|
148
|
+
return sendTextResource(res, instructionsPath, 'instructions');
|
|
145
149
|
}
|
|
146
150
|
const safeSurface = path.basename(surface);
|
|
147
|
-
const
|
|
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,
|
|
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
|
-
|
|
118
|
+
success: ok,
|
|
119
|
+
updating: !dryRun && ok,
|
|
105
120
|
message: dryRun
|
|
106
|
-
?
|
|
107
|
-
:
|
|
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 {
|
|
7
|
-
|
|
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
|
|
19
|
-
skillsDir:
|
|
20
|
-
derivedDir:
|
|
21
|
-
instructionsFile:
|
|
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(
|
|
29
|
-
vaultEmbed = await buildEmbeddingsIndex(vaultNodes,
|
|
30
|
-
sessionEmbed = await buildSessionEmbeddingsIndex(
|
|
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({
|
|
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
|
|
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
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
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(
|
|
68
|
-
? fs.readdirSync(
|
|
93
|
+
const derivedFiles = fs.existsSync(derivedDir)
|
|
94
|
+
? fs.readdirSync(derivedDir).length : 0;
|
|
69
95
|
|
|
70
|
-
|
|
71
|
-
const
|
|
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:
|
|
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(
|
|
112
|
+
instructions_exists: fs.existsSync(instructionsFile),
|
|
88
113
|
embeddings: {
|
|
89
114
|
vault_nodes: vaultEmbedCount,
|
|
90
115
|
session_chunks: sessionEmbedCount,
|