total-recall-brain 3.6.7 → 3.6.9
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/package.json
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from './schema.mjs';
|
|
11
11
|
import { atomicWrite, safeStringify } from './vault.mjs';
|
|
12
12
|
import { logger } from './logger.mjs';
|
|
13
|
+
import { validateMemoryNode } from './total-recall-memory-validator.mjs';
|
|
13
14
|
|
|
14
15
|
const APPEND_TYPES = new Set(['conversation', 'run']);
|
|
15
16
|
const PROTOCOL_PATHS = [
|
|
@@ -158,8 +159,14 @@ export function processOperation(envelope, vaultRoot, options = {}) {
|
|
|
158
159
|
if (!resolvedType) errors.push('Missing required frontmatter field: type');
|
|
159
160
|
else if (!SSSS_SCHEMAS[resolvedType]) errors.push(`Unknown SSSS type: '${resolvedType}'.`);
|
|
160
161
|
else {
|
|
161
|
-
|
|
162
|
-
|
|
162
|
+
if (resolvedType === 'memory' && envelope.path.includes('.agent/memory-vault/')) {
|
|
163
|
+
const vResult = validateMemoryNode(fmData);
|
|
164
|
+
if (!vResult.success) vResult.errors.forEach(e => errors.push(e));
|
|
165
|
+
} else {
|
|
166
|
+
const r = SSSS_SCHEMAS[resolvedType].safeParse(fmData);
|
|
167
|
+
if (!r.success) r.error.issues.forEach(i => errors.push(`${i.path.join('.')}: ${i.message}`));
|
|
168
|
+
}
|
|
169
|
+
|
|
163
170
|
if (envelope.type === 'operation' && APPEND_TYPES.has(resolvedType)) {
|
|
164
171
|
const ap = resolveVfsPath(vaultRoot, envelope.path);
|
|
165
172
|
if (fs.existsSync(ap)) {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Promotes a feedback-derived memory node to a wider scope after applying
|
|
3
|
+
* strict provenance stripping and privacy guarantees.
|
|
4
|
+
*
|
|
5
|
+
* Flow: workspace -> system_candidate -> system_promoted
|
|
6
|
+
*
|
|
7
|
+
* @param {object} memoryNode - The memory node data (frontmatter + __body__).
|
|
8
|
+
* @param {string} targetScope - 'system_candidate' or 'system_promoted'
|
|
9
|
+
* @returns {object} { success: boolean, node: object, errors: string[] }
|
|
10
|
+
*/
|
|
11
|
+
export function promoteFeedback(memoryNode, targetScope) {
|
|
12
|
+
const currentScope = memoryNode.feedback_scope || 'workspace';
|
|
13
|
+
|
|
14
|
+
if (targetScope === 'system_candidate') {
|
|
15
|
+
if (currentScope !== 'workspace' && currentScope !== 'local_thread') {
|
|
16
|
+
return { success: false, node: null, errors: [`Cannot promote to system_candidate from ${currentScope}`] };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Provenance stripping
|
|
20
|
+
const strippedNode = JSON.parse(JSON.stringify(memoryNode));
|
|
21
|
+
strippedNode.feedback_scope = 'system_candidate';
|
|
22
|
+
|
|
23
|
+
// Remove PII / workspace leaking fields
|
|
24
|
+
delete strippedNode.workspace_id;
|
|
25
|
+
delete strippedNode.user_id;
|
|
26
|
+
if (strippedNode.source) {
|
|
27
|
+
delete strippedNode.source.session_id;
|
|
28
|
+
delete strippedNode.source.agent;
|
|
29
|
+
}
|
|
30
|
+
delete strippedNode.x_browser_context;
|
|
31
|
+
delete strippedNode.x_location;
|
|
32
|
+
delete strippedNode.x_citations; // Citations might contain private workspace doc links
|
|
33
|
+
|
|
34
|
+
// Scrub body for UUIDs
|
|
35
|
+
if (strippedNode.__body__) {
|
|
36
|
+
strippedNode.__body__ = strippedNode.__body__.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/ig, '[REDACTED_UUID]');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return { success: true, node: strippedNode, errors: [] };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (targetScope === 'system_promoted') {
|
|
43
|
+
if (currentScope !== 'system_candidate') {
|
|
44
|
+
return { success: false, node: null, errors: [`Cannot promote to system_promoted directly from ${currentScope}. Must be system_candidate first.`] };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const promotedNode = JSON.parse(JSON.stringify(memoryNode));
|
|
48
|
+
promotedNode.feedback_scope = 'system_promoted';
|
|
49
|
+
|
|
50
|
+
return { success: true, node: promotedNode, errors: [] };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return { success: false, node: null, errors: [`Unknown target scope: ${targetScope}`] };
|
|
54
|
+
}
|
package/src/core/schema.mjs
CHANGED
|
@@ -24,8 +24,8 @@ export const MemoryNodeSchema = z.object({
|
|
|
24
24
|
category: z.string(),
|
|
25
25
|
title: z.string(),
|
|
26
26
|
status: z.enum(['active', 'superseded', 'deprecated', 'draft']),
|
|
27
|
-
confidence: z.number().min(0).max(1),
|
|
28
|
-
importance: z.number().int().min(1).max(5),
|
|
27
|
+
confidence: z.number().min(0).max(1).optional(),
|
|
28
|
+
importance: z.number().int().min(1).max(5).optional(),
|
|
29
29
|
created: ssssDatetime(),
|
|
30
30
|
updated: ssssDatetime(),
|
|
31
31
|
last_accessed: ssssDatetime(),
|
|
@@ -41,12 +41,12 @@ export const MemoryNodeSchema = z.object({
|
|
|
41
41
|
tags: z.array(z.string()),
|
|
42
42
|
related: z.array(z.string()),
|
|
43
43
|
routes_to_skills: z.array(z.string()),
|
|
44
|
-
sentiment_polarity: z.enum(['directive_must', 'directive_must_not', 'descriptive', 'preference']),
|
|
45
|
-
sentiment_target: z.string(),
|
|
46
|
-
modality: z.enum(['must', 'must_not', 'should', 'should_not', 'descriptive', 'preference']),
|
|
47
|
-
subject: z.string().regex(/^[a-zA-Z0-9_\s.-]+$/),
|
|
48
|
-
predicate: z.string().regex(/^[a-zA-Z0-9_\s.-]+$/),
|
|
49
|
-
object: z.string(),
|
|
44
|
+
sentiment_polarity: z.enum(['directive_must', 'directive_must_not', 'descriptive', 'preference']).optional(),
|
|
45
|
+
sentiment_target: z.string().optional(),
|
|
46
|
+
modality: z.enum(['must', 'must_not', 'should', 'should_not', 'descriptive', 'preference']).optional(),
|
|
47
|
+
subject: z.string().regex(/^[a-zA-Z0-9_\s.-]+$/).optional(),
|
|
48
|
+
predicate: z.string().regex(/^[a-zA-Z0-9_\s.-]+$/).optional(),
|
|
49
|
+
object: z.string().optional(),
|
|
50
50
|
decay: z.object({
|
|
51
51
|
half_life_days: z.number(),
|
|
52
52
|
access_count: z.number().int(),
|
|
@@ -87,7 +87,8 @@ export const MemoryNodeSchema = z.object({
|
|
|
87
87
|
expires_at: ssssDatetimeNullable().optional(),
|
|
88
88
|
// Absolute Invariant Extensions
|
|
89
89
|
priority: z.enum(['absolute', 'high', 'normal', 'low']).optional(),
|
|
90
|
-
immutable: z.boolean().optional()
|
|
90
|
+
immutable: z.boolean().optional(),
|
|
91
|
+
feedback_scope: z.enum(['local_thread', 'workspace', 'account', 'system_candidate', 'system_promoted']).optional()
|
|
91
92
|
});
|
|
92
93
|
|
|
93
94
|
export const ConflictRecordSchema = z.object({
|
|
@@ -464,6 +465,7 @@ export const EventSchema = z.object({
|
|
|
464
465
|
comment: z.string().optional().nullable(),
|
|
465
466
|
comment_lang: z.string().optional().nullable(),
|
|
466
467
|
timestamp: z.string().optional().nullable(),
|
|
468
|
+
feedback_scope: z.enum(['local_thread', 'workspace', 'account', 'system_candidate', 'system_promoted']).optional().nullable(),
|
|
467
469
|
});
|
|
468
470
|
|
|
469
471
|
export const LanguageConventionSchema = z.object({
|
package/src/core/surface.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'path';
|
|
|
3
3
|
import crypto from 'crypto';
|
|
4
4
|
import { loadSkills, atomicWrite, walkMd } from './vault.mjs';
|
|
5
5
|
import { getNodes } from './vault-cache.mjs';
|
|
6
|
+
import matter from 'gray-matter';
|
|
6
7
|
import {
|
|
7
8
|
buildMemoryLayerIndex,
|
|
8
9
|
inferMemoryLayer,
|
|
@@ -362,6 +363,32 @@ Show all available commands.
|
|
|
362
363
|
}
|
|
363
364
|
}
|
|
364
365
|
|
|
366
|
+
// 3. Inject Installed Skills Inventory
|
|
367
|
+
if (skillsDir && fs.existsSync(skillsDir)) {
|
|
368
|
+
const installedSkills = [];
|
|
369
|
+
try {
|
|
370
|
+
const entries = fs.readdirSync(skillsDir);
|
|
371
|
+
for (const entry of entries) {
|
|
372
|
+
const fullPath = path.join(skillsDir, entry);
|
|
373
|
+
if (fs.statSync(fullPath).isDirectory()) {
|
|
374
|
+
const skillMdPath = path.join(fullPath, 'SKILL.md');
|
|
375
|
+
if (fs.existsSync(skillMdPath)) {
|
|
376
|
+
const raw = fs.readFileSync(skillMdPath, 'utf8');
|
|
377
|
+
const { data } = matter(raw);
|
|
378
|
+
if (data && data.name && data.description) {
|
|
379
|
+
installedSkills.push(`- **${data.name}** (\`.agent/skills/${entry}/SKILL.md\`): ${data.description}`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (installedSkills.length > 0) {
|
|
385
|
+
combined += `\n\n## Installed Agent Skills\n\nYou have access to specialized 'skills' to help you with complex tasks. If a skill seems relevant to your current task, you MUST read its SKILL.md file before proceeding.\n\nAvailable skills:\n${installedSkills.join('\n')}`;
|
|
386
|
+
}
|
|
387
|
+
} catch (err) {
|
|
388
|
+
console.error('Error in skills injection:', err);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
365
392
|
return combined.trim();
|
|
366
393
|
}
|
|
367
394
|
|
|
@@ -425,14 +452,14 @@ async function writeShim(shimPath, skillsDir, nodes = [], { vaultDir, derivedDir
|
|
|
425
452
|
*/
|
|
426
453
|
const CLIENT_SHIMS = {
|
|
427
454
|
cursor: ['.cursorrules'],
|
|
428
|
-
'claude-code': ['
|
|
429
|
-
antigravity: [
|
|
430
|
-
gemini: [
|
|
431
|
-
codex: ['
|
|
455
|
+
'claude-code': ['.clauderules'],
|
|
456
|
+
antigravity: [],
|
|
457
|
+
gemini: [],
|
|
458
|
+
codex: ['.codexrules'],
|
|
432
459
|
vscode: ['.github/copilot-instructions.md', '.vscode/copilot-instructions.md'],
|
|
433
|
-
pi: [
|
|
460
|
+
pi: [],
|
|
434
461
|
aider: ['.aider.rules.md'],
|
|
435
|
-
windsurf: ['.windsurfrules'
|
|
462
|
+
windsurf: ['.windsurfrules'],
|
|
436
463
|
};
|
|
437
464
|
|
|
438
465
|
/**
|
|
@@ -465,11 +492,7 @@ async function compilePointers(instructionsFile, skillsDir, nodes = [], { vaultD
|
|
|
465
492
|
const clientsPath = path.join(baseDir, '.agent', 'config', 'clients.json');
|
|
466
493
|
const connectedClients = readConnectedClients(clientsPath);
|
|
467
494
|
|
|
468
|
-
if (connectedClients
|
|
469
|
-
// Backward compat loop removed to prevent context exhaustion.
|
|
470
|
-
// By default, we only write INSTRUCTIONS.md.
|
|
471
|
-
// Users must explicitly use 'npx total-recall connect' to register client IDEs.
|
|
472
|
-
} else {
|
|
495
|
+
if (connectedClients !== null) {
|
|
473
496
|
// Only write shims for connected clients
|
|
474
497
|
for (const client of connectedClients) {
|
|
475
498
|
const files = CLIENT_SHIMS[client];
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { SSSS_SCHEMAS } from './schema.mjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Validates a memory node against the SSSS standard, enforcing
|
|
5
|
+
* Schema V2 conditional requirements for Total Recall.
|
|
6
|
+
*
|
|
7
|
+
* @param {object} fmData - The parsed YAML frontmatter of the memory node.
|
|
8
|
+
* @returns {object} { success: boolean, errors: string[] }
|
|
9
|
+
*/
|
|
10
|
+
export function validateMemoryNode(fmData) {
|
|
11
|
+
const errors = [];
|
|
12
|
+
|
|
13
|
+
// 1. Base schema validation (types, bounds, basic required fields)
|
|
14
|
+
const r = SSSS_SCHEMAS['memory'].safeParse(fmData);
|
|
15
|
+
if (!r.success) {
|
|
16
|
+
r.error.issues.forEach(i => errors.push(`${i.path.join('.')}: ${i.message}`));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// 2. Schema V2 Conditional Validation
|
|
20
|
+
// If schema_version === 2, the following fields are strictly required:
|
|
21
|
+
if (fmData.schema_version === 2) {
|
|
22
|
+
const v2Required = [
|
|
23
|
+
'confidence',
|
|
24
|
+
'importance',
|
|
25
|
+
'modality',
|
|
26
|
+
'subject',
|
|
27
|
+
'predicate',
|
|
28
|
+
'object',
|
|
29
|
+
'sentiment_polarity',
|
|
30
|
+
'sentiment_target'
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
v2Required.forEach(field => {
|
|
34
|
+
if (fmData[field] === undefined || fmData[field] === null) {
|
|
35
|
+
errors.push(`V2 Schema Requirement: Missing required field '${field}'`);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
success: errors.length === 0,
|
|
42
|
+
errors
|
|
43
|
+
};
|
|
44
|
+
}
|