total-recall-brain 3.6.8 → 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.8",
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({
@@ -492,15 +492,7 @@ async function compilePointers(instructionsFile, skillsDir, nodes = [], { vaultD
492
492
  const clientsPath = path.join(baseDir, '.agent', 'config', 'clients.json');
493
493
  const connectedClients = readConnectedClients(clientsPath);
494
494
 
495
- if (connectedClients === null) {
496
- // Write out-of-the-box dotfiles to support IDEs automatically,
497
- // avoiding duplicate .md files to prevent Antigravity context bloat.
498
- for (const files of Object.values(CLIENT_SHIMS)) {
499
- for (const file of files) {
500
- await writeShim(path.join(baseDir, file), skillsDir, nodes, { vaultDir, derivedDir });
501
- }
502
- }
503
- } else {
495
+ if (connectedClients !== null) {
504
496
  // Only write shims for connected clients
505
497
  for (const client of connectedClients) {
506
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
+ }