total-recall-brain 3.19.0 → 3.19.1
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-B6AfuKv9.js → index-D0XeeXhG.js} +21 -21
- package/frontend/dist/index.html +1 -1
- package/frontend/src/pages/SkillsPage.tsx +40 -13
- package/package.json +1 -1
- package/src/core/embeddings.mjs +2 -5
- package/src/core/source-adapters.mjs +1 -1
- package/src/core/throttled-fetch.mjs +54 -24
- package/src/server/routes/rules.mjs +11 -27
- package/src/server/routes/skills.mjs +3 -0
package/frontend/dist/index.html
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
18
18
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
|
19
19
|
<title>Total Recall — Portable Memory</title>
|
|
20
|
-
<script type="module" crossorigin src="/assets/index-
|
|
20
|
+
<script type="module" crossorigin src="/assets/index-D0XeeXhG.js"></script>
|
|
21
21
|
<link rel="stylesheet" crossorigin href="/assets/index-B4RASWbV.css">
|
|
22
22
|
</head>
|
|
23
23
|
<body>
|
|
@@ -189,6 +189,22 @@ export default function SkillsPage({ activeBrainId }: { activeBrainId?: string }
|
|
|
189
189
|
const [installingPkg, setInstallingPkg] = useState<string | null>(null);
|
|
190
190
|
const [quarantineAlert, setQuarantineAlert] = useState<{ pkg: string; reason: string } | null>(null);
|
|
191
191
|
|
|
192
|
+
// ─── Active brain → single deployable repo target ──────────────────────────
|
|
193
|
+
// activeBrainId is 'global', 'project:<name>', or a comma-joined multi-select
|
|
194
|
+
// (see BrainSelector). Only a single, non-global selection maps to one concrete
|
|
195
|
+
// project repo we can deploy a global skill into.
|
|
196
|
+
const activeRepoName = useMemo(() => {
|
|
197
|
+
if (!activeBrainId) return null;
|
|
198
|
+
const ids = activeBrainId.split(',').map((s) => s.trim()).filter(Boolean);
|
|
199
|
+
if (ids.length !== 1 || ids[0] === 'global') return null;
|
|
200
|
+
const id = ids[0];
|
|
201
|
+
return id.startsWith('project:') ? id.slice('project:'.length) : id;
|
|
202
|
+
}, [activeBrainId]);
|
|
203
|
+
const activeRepoGroup = useMemo(
|
|
204
|
+
() => (activeRepoName ? repoGroups.find((g) => g.repo === activeRepoName) || null : null),
|
|
205
|
+
[activeRepoName, repoGroups],
|
|
206
|
+
);
|
|
207
|
+
|
|
192
208
|
// ─── Lifecycle tab state ────────────────────────────────────────────────────
|
|
193
209
|
const lifecycleVersion = useMemo(() => selectedSkill && skillContent ? extractVersion(skillContent) : '0.0.0', [selectedSkill, skillContent]);
|
|
194
210
|
const changelogEntries = useMemo(() => selectedSkill && skillContent ? parseChangelog(skillContent) : [], [selectedSkill, skillContent]);
|
|
@@ -269,14 +285,23 @@ export default function SkillsPage({ activeBrainId }: { activeBrainId?: string }
|
|
|
269
285
|
groups = (data.repos || []) as RepoSkillsGroup[];
|
|
270
286
|
}
|
|
271
287
|
|
|
272
|
-
|
|
273
|
-
|
|
288
|
+
// activeBrainId may be a comma-joined multi-select (see BrainSelector) — split it
|
|
289
|
+
// rather than treating the whole string as a single repo id, else selecting more
|
|
290
|
+
// than one brain collapsed the list down to only the "Global" group.
|
|
291
|
+
const selectedBrainIds = (activeBrainId || '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
292
|
+
if (selectedBrainIds.length > 0 && !selectedBrainIds.includes('global')) {
|
|
293
|
+
const targetRepoIds = selectedBrainIds.map((id) =>
|
|
294
|
+
id.startsWith('project:') ? id.slice('project:'.length) : id,
|
|
295
|
+
);
|
|
274
296
|
groups = groups.filter(
|
|
275
|
-
g => g.repo === 'Global' ||
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
297
|
+
g => g.repo === 'Global' ||
|
|
298
|
+
targetRepoIds.some(
|
|
299
|
+
(targetRepoId) =>
|
|
300
|
+
g.repo === targetRepoId ||
|
|
301
|
+
targetRepoId.endsWith(`/${g.repo}`) ||
|
|
302
|
+
g.repo.endsWith(`/${targetRepoId}`),
|
|
303
|
+
) ||
|
|
304
|
+
selectedBrainIds.includes(`project:${g.repo}`)
|
|
280
305
|
);
|
|
281
306
|
list = groups.flatMap(g => g.skills || []);
|
|
282
307
|
}
|
|
@@ -713,24 +738,26 @@ Configure triggers, options, and prompts inside this rules sheet to hot-recompil
|
|
|
713
738
|
{skill.name}
|
|
714
739
|
</span>
|
|
715
740
|
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
|
716
|
-
{
|
|
741
|
+
{activeRepoGroup?.path && group.repo === 'Global' && (
|
|
717
742
|
<input
|
|
718
743
|
type="checkbox"
|
|
719
|
-
checked={!!
|
|
744
|
+
checked={!!activeRepoGroup?.skills?.some(s => s.name === skill.name)}
|
|
720
745
|
onClick={(e) => e.stopPropagation()}
|
|
721
746
|
onChange={async (e) => {
|
|
722
747
|
e.stopPropagation();
|
|
723
748
|
const checked = e.target.checked;
|
|
749
|
+
const targetRepoPath = activeRepoGroup?.path;
|
|
750
|
+
if (!targetRepoPath) return;
|
|
724
751
|
if (checked) {
|
|
725
752
|
// Open preview modal instead of instantly deploying
|
|
726
753
|
setPreviewModalOpen(true);
|
|
727
754
|
setPreviewLoading(true);
|
|
728
755
|
setPreviewData(null);
|
|
729
756
|
try {
|
|
730
|
-
const res = await previewSkillRepo(skill.name,
|
|
757
|
+
const res = await previewSkillRepo(skill.name, targetRepoPath);
|
|
731
758
|
setPreviewData({
|
|
732
759
|
skillName: skill.name,
|
|
733
|
-
targetRepo:
|
|
760
|
+
targetRepo: targetRepoPath,
|
|
734
761
|
original: res.original,
|
|
735
762
|
preview: res.preview,
|
|
736
763
|
signals: res.signals
|
|
@@ -744,7 +771,7 @@ Configure triggers, options, and prompts inside this rules sheet to hot-recompil
|
|
|
744
771
|
} else {
|
|
745
772
|
// Uncheck (disable) happens immediately
|
|
746
773
|
try {
|
|
747
|
-
await toggleSkillRepo(skill.name,
|
|
774
|
+
await toggleSkillRepo(skill.name, targetRepoPath, false);
|
|
748
775
|
void fetchSkillsList();
|
|
749
776
|
} catch (err) {
|
|
750
777
|
setError((err as Error).message);
|
|
@@ -831,7 +858,7 @@ Configure triggers, options, and prompts inside this rules sheet to hot-recompil
|
|
|
831
858
|
<h2 style={{ marginTop: 0, marginBottom: 12 }}>Skill Adaptation Preview</h2>
|
|
832
859
|
{previewLoading ? (
|
|
833
860
|
<div style={{ flex: 1, display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
|
|
834
|
-
<p>Generating adaptive preview for {activeBrainId}...</p>
|
|
861
|
+
<p>Generating adaptive preview for {activeRepoName || activeBrainId}...</p>
|
|
835
862
|
</div>
|
|
836
863
|
) : previewData ? (
|
|
837
864
|
<>
|
package/package.json
CHANGED
package/src/core/embeddings.mjs
CHANGED
|
@@ -215,8 +215,7 @@ async function getGoogleEmbedding(text, model) {
|
|
|
215
215
|
model: `models/${resolved}`,
|
|
216
216
|
content: { parts: [{ text }] },
|
|
217
217
|
}),
|
|
218
|
-
|
|
219
|
-
});
|
|
218
|
+
}, 30000);
|
|
220
219
|
|
|
221
220
|
if (!res.ok) {
|
|
222
221
|
const body = await res.text().catch(() => '');
|
|
@@ -246,7 +245,6 @@ async function getOpenRouterEmbedding(text, model = 'openai/text-embedding-3-sma
|
|
|
246
245
|
// Request 768 dims to match the existing vault vss0(embedding(768)) schema
|
|
247
246
|
// and stay comparable against previously-stored Google embeddings.
|
|
248
247
|
body: JSON.stringify({ model, input: text, dimensions: 768 }),
|
|
249
|
-
signal: AbortSignal.timeout(30000),
|
|
250
248
|
}, 20000);
|
|
251
249
|
|
|
252
250
|
if (!res.ok) {
|
|
@@ -275,8 +273,7 @@ async function getOpenAIEmbedding(text, model = 'text-embedding-3-small') {
|
|
|
275
273
|
'Authorization': `Bearer ${openaiKey}`,
|
|
276
274
|
},
|
|
277
275
|
body: JSON.stringify({ model, input: text }),
|
|
278
|
-
|
|
279
|
-
});
|
|
276
|
+
}, 30000);
|
|
280
277
|
|
|
281
278
|
if (!res.ok) {
|
|
282
279
|
const body = await res.text().catch(() => '');
|
|
@@ -980,7 +980,7 @@ export function checkSourceAvailability(config) {
|
|
|
980
980
|
config.braveApiKey || config.tavilyApiKey || config.exaApiKey || config.serperApiKey;
|
|
981
981
|
if (!config.searxUrl && !hasPaidSearch) {
|
|
982
982
|
warnings.push(
|
|
983
|
-
'No SearX URL and no paid web-search key. Set SEARX_URL (preferred) or
|
|
983
|
+
'No SearX URL and no paid web-search key. Set SEARX_URL (preferred), or BRAVE_SEARCH_API_KEY / TAVILY_API_KEY / EXA_API_KEY / SERPER_API_KEY — otherwise only DuckDuckGo Instant Answer.',
|
|
984
984
|
);
|
|
985
985
|
} else if (config.searxUrl) {
|
|
986
986
|
warnings.push(
|
|
@@ -231,22 +231,52 @@ function appendAuditLog(entry) {
|
|
|
231
231
|
}
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
+
/**
|
|
235
|
+
* Persisting an audit event goes through the full SSSS kernel (schema
|
|
236
|
+
* registry build + validation), which is CPU-heavy enough to starve the
|
|
237
|
+
* event loop for seconds at a time. Firing it inline — even un-awaited —
|
|
238
|
+
* competes with concurrent I/O (like the body-stream read of the very
|
|
239
|
+
* request being audited) for event-loop turns. Queue + flush on a deferred
|
|
240
|
+
* macrotask so in-flight I/O always gets priority.
|
|
241
|
+
*/
|
|
242
|
+
const policyEventQueue = [];
|
|
243
|
+
let policyEventFlushScheduled = false;
|
|
244
|
+
|
|
245
|
+
function schedulePolicyEventFlush() {
|
|
246
|
+
if (policyEventFlushScheduled) return;
|
|
247
|
+
policyEventFlushScheduled = true;
|
|
248
|
+
setImmediate(async () => {
|
|
249
|
+
policyEventFlushScheduled = false;
|
|
250
|
+
const batch = policyEventQueue.splice(0, policyEventQueue.length);
|
|
251
|
+
if (batch.length === 0) return;
|
|
252
|
+
try {
|
|
253
|
+
const [{ processViaPackageKernel }, crypto, { brainDir }, path] = await Promise.all([
|
|
254
|
+
import('./ssss-kernel-bridge.mjs'),
|
|
255
|
+
import('node:crypto'),
|
|
256
|
+
import('./config.mjs'),
|
|
257
|
+
import('node:path'),
|
|
258
|
+
]);
|
|
259
|
+
const vaultRoot = path.join(brainDir, 'memory-vault');
|
|
260
|
+
for (const eventContent of batch) {
|
|
261
|
+
const envelope = {
|
|
262
|
+
type: 'event',
|
|
263
|
+
idempotency_key: crypto.randomUUID(),
|
|
264
|
+
path: 'system/network-policy.md',
|
|
265
|
+
workspace_id: 'default',
|
|
266
|
+
actor: { role: 'system' },
|
|
267
|
+
content: JSON.stringify(eventContent),
|
|
268
|
+
};
|
|
269
|
+
await processViaPackageKernel(envelope, vaultRoot, { agentRole: 'system' }).catch(() => {});
|
|
270
|
+
}
|
|
271
|
+
} catch {
|
|
272
|
+
// Best-effort audit trail; never let it affect request handling.
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
234
277
|
async function emitPolicyEvent(eventContent) {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
const { brainDir } = await import('./config.mjs');
|
|
238
|
-
const path = await import('node:path');
|
|
239
|
-
const vaultRoot = path.join(brainDir, 'memory-vault');
|
|
240
|
-
const envelope = {
|
|
241
|
-
type: 'event',
|
|
242
|
-
idempotency_key: crypto.randomUUID(),
|
|
243
|
-
path: 'system/network-policy.md',
|
|
244
|
-
workspace_id: 'default',
|
|
245
|
-
actor: { role: 'system' },
|
|
246
|
-
content: JSON.stringify(eventContent)
|
|
247
|
-
};
|
|
248
|
-
await processViaPackageKernel(envelope, vaultRoot, { agentRole: 'system' }).catch(() => {});
|
|
249
|
-
}).catch(() => {});
|
|
278
|
+
policyEventQueue.push(eventContent);
|
|
279
|
+
schedulePolicyEventFlush();
|
|
250
280
|
}
|
|
251
281
|
|
|
252
282
|
// ─── Rate Limiting (minIntervalMs) ──────────────────────────────────────────
|
|
@@ -397,15 +427,15 @@ async function executeFetch(url, options, timeoutMs, domain, queueWaitMs = 0, ra
|
|
|
397
427
|
rate_wait_ms: rateWaitMs,
|
|
398
428
|
});
|
|
399
429
|
|
|
400
|
-
emitPolicyEvent(
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
430
|
+
// NOTE: audit events used to also be persisted via emitPolicyEvent()
|
|
431
|
+
// (a full SSSS kernel write per request). Measured at 30s+ per call —
|
|
432
|
+
// processViaPackageKernel/getTotalRecallEngine is catastrophically slow
|
|
433
|
+
// on this vault, and even deferred via setImmediate it still blocks the
|
|
434
|
+
// single-threaded event loop for its full duration once it runs,
|
|
435
|
+
// stalling concurrent I/O (e.g. the very response body being read).
|
|
436
|
+
// In-memory appendAuditLog() above (queryable via getAuditLog()) is the
|
|
437
|
+
// audit trail for network activity now. Re-enable emitPolicyEvent()
|
|
438
|
+
// only after the kernel commit path itself is fixed to be fast.
|
|
409
439
|
}
|
|
410
440
|
}
|
|
411
441
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
|
-
import { getBothBrains } from '../../cli/agent-dir.mjs';
|
|
3
2
|
import { getNodes } from '../../core/vault-cache.mjs';
|
|
4
3
|
import { logger } from '../../core/logger.mjs';
|
|
4
|
+
import { resolveAllVaultsFromQuery, VAULT_DIR } from './_shared.mjs';
|
|
5
5
|
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
|
|
@@ -10,21 +10,6 @@ export const rulesRouter = express.Router();
|
|
|
10
10
|
/** Rule categories that surface on the Agent Rules page / instruction compiler. */
|
|
11
11
|
const RULE_CATEGORIES = new Set(['invariants', 'preferences', 'anti-patterns', 'corrections']);
|
|
12
12
|
|
|
13
|
-
/**
|
|
14
|
-
* Resolve the vault root for a brain layer.
|
|
15
|
-
* getBothBrains() returns `brainDir` (not `brainRoot`).
|
|
16
|
-
* Project layer also carries `agentDir` — fall back to skills/total-recall under it.
|
|
17
|
-
*/
|
|
18
|
-
function resolveBrainDir(layer) {
|
|
19
|
-
if (!layer) return null;
|
|
20
|
-
if (typeof layer.brainDir === 'string' && layer.brainDir) return layer.brainDir;
|
|
21
|
-
if (typeof layer.brainRoot === 'string' && layer.brainRoot) return layer.brainRoot; // legacy alias
|
|
22
|
-
if (typeof layer.agentDir === 'string' && layer.agentDir) {
|
|
23
|
-
return path.join(layer.agentDir, 'skills', 'total-recall');
|
|
24
|
-
}
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
13
|
function serializeRule(node, scope) {
|
|
29
14
|
const importanceRaw = Number(node.importance);
|
|
30
15
|
const importance = Number.isFinite(importanceRaw)
|
|
@@ -49,16 +34,17 @@ function serializeRule(node, scope) {
|
|
|
49
34
|
|
|
50
35
|
rulesRouter.get('/api/rules', (req, res) => {
|
|
51
36
|
try {
|
|
52
|
-
|
|
37
|
+
// Scoped by req.query.brain / x-total-recall-brain header, same as memory/graph —
|
|
38
|
+
// this used to always merge the global vault with getBothBrains()'s cwd-detected
|
|
39
|
+
// project brain, ignoring whichever brain the user had selected in the UI.
|
|
40
|
+
const vaultDirs = resolveAllVaultsFromQuery(req);
|
|
41
|
+
const globalVaultKey = path.resolve(VAULT_DIR);
|
|
53
42
|
const rules = [];
|
|
54
43
|
const seenVaults = new Set();
|
|
55
44
|
|
|
56
|
-
const
|
|
57
|
-
if (!brainDir) return;
|
|
58
|
-
const vaultDir = path.join(brainDir, 'memory-vault');
|
|
45
|
+
for (const vaultDir of vaultDirs) {
|
|
59
46
|
const vaultKey = path.resolve(vaultDir);
|
|
60
|
-
|
|
61
|
-
if (seenVaults.has(vaultKey)) return;
|
|
47
|
+
if (seenVaults.has(vaultKey)) continue;
|
|
62
48
|
seenVaults.add(vaultKey);
|
|
63
49
|
|
|
64
50
|
let nodes = [];
|
|
@@ -66,18 +52,16 @@ rulesRouter.get('/api/rules', (req, res) => {
|
|
|
66
52
|
nodes = getNodes(vaultDir) || [];
|
|
67
53
|
} catch (err) {
|
|
68
54
|
logger.warn('rules', 'Failed to load vault nodes', { vaultDir, error: err.message });
|
|
69
|
-
|
|
55
|
+
continue;
|
|
70
56
|
}
|
|
71
57
|
|
|
58
|
+
const scope = vaultKey === globalVaultKey ? 'global' : 'project';
|
|
72
59
|
for (const n of nodes) {
|
|
73
60
|
if (!RULE_CATEGORIES.has(n.category)) continue;
|
|
74
61
|
// Prefer active rules first in natural order; archived still included for restore UI
|
|
75
62
|
rules.push(serializeRule(n, scope));
|
|
76
63
|
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
addRules(resolveBrainDir(globalBrain), 'global');
|
|
80
|
-
addRules(resolveBrainDir(projectBrain), 'project');
|
|
64
|
+
}
|
|
81
65
|
|
|
82
66
|
res.json({ rules, count: rules.length });
|
|
83
67
|
} catch (err) {
|
|
@@ -303,6 +303,9 @@ export function collectSkillsByRepo() {
|
|
|
303
303
|
|
|
304
304
|
function resolveRegisteredProject(repoName) {
|
|
305
305
|
if (!repoName) return null;
|
|
306
|
+
// "Global" is the synthetic repo collectSkillsByRepo() unshifts for os.homedir() —
|
|
307
|
+
// it isn't a real project-registry.json entry, so resolve it directly here too.
|
|
308
|
+
if (repoName === 'Global') return { name: 'Global', path: os.homedir() };
|
|
306
309
|
const registry = loadProjectRegistry();
|
|
307
310
|
const entry = registry.find(
|
|
308
311
|
(p) => p.name === repoName || path.basename(p.path || p.project_root || '') === repoName,
|