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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "total-recall-brain",
3
- "version": "3.6.7",
3
+ "version": "3.6.9",
4
4
  "description": "Sovereign AI system — database-free, SSSS-powered personal intelligence engine deployable to any POSIX host.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- const r = SSSS_SCHEMAS[resolvedType].safeParse(fmData);
162
- if (!r.success) r.error.issues.forEach(i => errors.push(`${i.path.join('.')}: ${i.message}`));
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
+ }
@@ -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({
@@ -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': ['CLAUDE.md', '.clauderules'],
429
- antigravity: ['AGENTS.md'],
430
- gemini: ['GEMINI.md'],
431
- codex: ['CODEX.md', '.codexrules'],
455
+ 'claude-code': ['.clauderules'],
456
+ antigravity: [],
457
+ gemini: [],
458
+ codex: ['.codexrules'],
432
459
  vscode: ['.github/copilot-instructions.md', '.vscode/copilot-instructions.md'],
433
- pi: ['AGENTS.md'],
460
+ pi: [],
434
461
  aider: ['.aider.rules.md'],
435
- windsurf: ['.windsurfrules', 'WINDSURF.md'],
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 === null) {
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
+ }