total-recall-brain 3.2.3 → 3.4.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.
Files changed (66) hide show
  1. package/README.md +1 -1
  2. package/extension/background.js +14 -2
  3. package/extension/content-script.js +18 -1
  4. package/extension/lib/brain-client.js +86 -52
  5. package/extension/lib/preconfigured.js +5 -0
  6. package/extension/manifest.json +1 -1
  7. package/extension/options/options.html +1 -0
  8. package/extension/options/options.js +12 -6
  9. package/extension/popup/popup.html +1 -0
  10. package/extension/popup/popup.js +29 -4
  11. package/extension/sidepanel/sidepanel.html +1 -0
  12. package/extension/sidepanel/sidepanel.js +24 -6
  13. package/frontend/index.html +8 -0
  14. package/frontend/src/api.ts +1 -2
  15. package/frontend/src/pages/ChatPage.tsx +9 -6
  16. package/frontend/src/pages/IntegrationsPage.tsx +144 -3
  17. package/package.json +2 -2
  18. package/scaffold/.agent/skills/total-recall/SKILL.md +55 -6
  19. package/scaffold/.agent/skills/total-recall/memory-vault/invariants/operating-instructions.md +2 -2
  20. package/scaffold/.agent/skills/total-recall/skills/cli-agents/agents.yml +1 -6
  21. package/scaffold/.agent/skills/total-recall/skills/ssss/SKILL.md +19 -0
  22. package/scaffold/.agent/skills/total-recall/skills/ssss/references/ssss-spec.md +33 -5
  23. package/src/cli/forget.mjs +18 -12
  24. package/src/cli/ingest/index.mjs +3 -2
  25. package/src/cli/init.mjs +30 -0
  26. package/src/cli/map.mjs +2 -2
  27. package/src/cli/rebuild.mjs +1 -1
  28. package/src/cli/remember.mjs +21 -11
  29. package/src/cli/setup.mjs +10 -1
  30. package/src/cli/share.mjs +7 -2
  31. package/src/core/append-log.mjs +131 -0
  32. package/src/core/clarity-rewriter.mjs +10 -6
  33. package/src/core/conclusion-writer.mjs +3 -2
  34. package/src/core/config.mjs +6 -0
  35. package/src/core/conflict-detector.mjs +4 -2
  36. package/src/core/context-compiler.mjs +412 -0
  37. package/src/core/daemon-loop.mjs +15 -0
  38. package/src/core/dream.mjs +4 -4
  39. package/src/core/drift-detector.mjs +3 -3
  40. package/src/core/embeddings.mjs +60 -10
  41. package/src/core/fact-seeker.mjs +70 -44
  42. package/src/core/import-rules.mjs +3 -2
  43. package/src/core/inference-engine.mjs +5 -4
  44. package/src/core/migrate.mjs +4 -3
  45. package/src/core/optimizer.mjs +6 -5
  46. package/src/core/parallel-context.mjs +288 -0
  47. package/src/core/post-mortem.mjs +4 -3
  48. package/src/core/research-queue.mjs +4 -3
  49. package/src/core/runtime.mjs +2 -2
  50. package/src/core/sandbox.mjs +56 -26
  51. package/src/core/scheduler.mjs +6 -5
  52. package/src/core/schema.mjs +2 -2
  53. package/src/core/search.mjs +167 -101
  54. package/src/core/surface.mjs +203 -53
  55. package/src/core/validated-write.mjs +100 -0
  56. package/src/core/vault-cache.mjs +46 -18
  57. package/src/core/vault.mjs +17 -29
  58. package/src/core/vector-field.mjs +659 -0
  59. package/src/core/vector-store.mjs +85 -0
  60. package/src/server/api.mjs +4 -3
  61. package/src/server/index.mjs +13 -61
  62. package/src/server/rest.mjs +238 -29
  63. package/src/server/route-manifest.json +344 -77
  64. package/src/server/routes/memory.mjs +85 -39
  65. package/src/server/routes/sessions.mjs +6 -2
  66. package/src/server/routes/share.mjs +9 -2
package/src/cli/share.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import crypto from 'node:crypto';
2
2
  import { parseLayerFlag, resolveBrainDir, getBothBrains, defaultLayerForCategory } from './agent-dir.mjs';
3
- import { writeNode, createMemoryNode } from '../core/vault.mjs';
3
+ import { createMemoryNode } from '../core/vault.mjs';
4
+ import { writeNodeValidated } from '../core/validated-write.mjs';
4
5
  import { addToQueue } from '../core/research-queue.mjs';
5
6
  import { compileSurface } from '../core/surface.mjs';
6
7
  import path from 'node:path';
@@ -160,7 +161,11 @@ export default async function share(args) {
160
161
  }];
161
162
  }
162
163
 
163
- writeNode(node, vaultDir);
164
+ const vaultResult = writeNodeValidated(node, vaultDir);
165
+ if (!vaultResult.success) {
166
+ console.error(` ❌ Validation failed: ${vaultResult.validation.errors.join('; ')}`);
167
+ process.exit(1);
168
+ }
164
169
  const layerLabel = layer === 'project' ? '[project]' : '[global]';
165
170
  console.log(` ✅ Saved as ${category || 'facts'} ${layerLabel}: ${category || 'facts'}/${slug}.md`);
166
171
 
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Append-only JSONL log with last-write-wins semantics and compaction.
3
+ *
4
+ * Write: O(1) — fs.appendFileSync one line
5
+ * Read: O(n) on cold load, cached after first read
6
+ * Compact: Rewrites only latest version of each key
7
+ *
8
+ * File format: One JSON object per line, each with a mandatory `_key` field.
9
+ * Later entries for the same `_key` shadow earlier ones.
10
+ * Tombstones: { "_key": "slug", "_deleted": true }
11
+ */
12
+
13
+ import fs from 'node:fs';
14
+ import path from 'node:path';
15
+ import { atomicWrite } from './vault.mjs';
16
+ import { logger } from './logger.mjs';
17
+
18
+ /** @type {Map<string, AppendLog>} Registry of all active AppendLog instances */
19
+ const activeAppendLogs = new Map();
20
+
21
+ export class AppendLog {
22
+ #filePath;
23
+ #cache; // Map<string, object> — latest value per key
24
+ #dirty; // number of appended lines since last compact
25
+ #compactThreshold;
26
+
27
+ constructor(filePath, { compactThreshold = 500 } = {}) {
28
+ this.#filePath = filePath;
29
+ this.#cache = null;
30
+ this.#dirty = 0;
31
+ this.#compactThreshold = compactThreshold;
32
+ activeAppendLogs.set(filePath, this);
33
+ }
34
+
35
+ /** Read the full index. Returns Map<key, value>. Cached after first call. */
36
+ entries() {
37
+ if (this.#cache) return this.#cache;
38
+ this.#cache = new Map();
39
+ if (!fs.existsSync(this.#filePath)) return this.#cache;
40
+ const raw = fs.readFileSync(this.#filePath, 'utf8');
41
+ for (const line of raw.split('\n')) {
42
+ if (!line.trim()) continue;
43
+ try {
44
+ const entry = JSON.parse(line);
45
+ if (entry._deleted) this.#cache.delete(entry._key);
46
+ else this.#cache.set(entry._key, entry);
47
+ } catch { /* skip corrupt lines */ }
48
+ }
49
+ return this.#cache;
50
+ }
51
+
52
+ /** Get a single entry by key. */
53
+ get(key) {
54
+ return this.entries().get(key) ?? null;
55
+ }
56
+
57
+ /** Append a single entry. O(1) I/O. */
58
+ append(key, value) {
59
+ const dir = path.dirname(this.#filePath);
60
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
61
+ const entry = { _key: key, ...value };
62
+ fs.appendFileSync(this.#filePath, JSON.stringify(entry) + '\n');
63
+ // Update in-memory cache
64
+ this.entries().set(key, entry);
65
+ this.#dirty++;
66
+ if (this.#dirty >= this.#compactThreshold) {
67
+ this.compact();
68
+ }
69
+ }
70
+
71
+ /** Write a tombstone for a key. O(1) I/O. */
72
+ remove(key) {
73
+ const dir = path.dirname(this.#filePath);
74
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
75
+ fs.appendFileSync(this.#filePath, JSON.stringify({ _key: key, _deleted: true }) + '\n');
76
+ this.entries().delete(key);
77
+ this.#dirty++;
78
+ }
79
+
80
+ /** Number of live entries. */
81
+ get size() {
82
+ return this.entries().size;
83
+ }
84
+
85
+ /** Convert to plain object (for JSON.stringify compatibility). */
86
+ toObject() {
87
+ const obj = {};
88
+ for (const [key, value] of this.entries()) {
89
+ const { _key, ...rest } = value;
90
+ obj[key] = rest;
91
+ }
92
+ return obj;
93
+ }
94
+
95
+ /** Compact: rewrite file with only latest versions. */
96
+ compact() {
97
+ const entries = this.entries();
98
+ const lines = [];
99
+ for (const [, value] of entries) {
100
+ lines.push(JSON.stringify(value));
101
+ }
102
+ atomicWrite(this.#filePath, lines.join('\n') + '\n');
103
+ this.#dirty = 0;
104
+ logger.info('append-log', `Compacted ${this.#filePath}: ${lines.length} entries`);
105
+ }
106
+
107
+ /** Invalidate the in-memory cache (force reload on next read). */
108
+ invalidate() {
109
+ this.#cache = null;
110
+ this.#dirty = 0;
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Compact all active AppendLog instances.
116
+ * Returns summary with number of logs compacted and total entries.
117
+ */
118
+ export function compactAppendLogs() {
119
+ let logsCompacted = 0;
120
+ let totalEntries = 0;
121
+ for (const [filePath, log] of activeAppendLogs) {
122
+ try {
123
+ log.compact();
124
+ logsCompacted++;
125
+ totalEntries += log.size;
126
+ } catch (err) {
127
+ logger.warn('append-log', `Failed to compact ${filePath}: ${err.message}`);
128
+ }
129
+ }
130
+ return { logs_compacted: logsCompacted, total_entries: totalEntries };
131
+ }
@@ -3,7 +3,8 @@ import path from 'path';
3
3
  import matter from 'gray-matter';
4
4
  import crypto from 'crypto';
5
5
  import { callLocalRuntime, cleanAndParseJSON } from './runtime.mjs';
6
- import { loadNodes, atomicWrite, safeStringify } from './vault.mjs';
6
+ import { atomicWrite, safeStringify } from './vault.mjs';
7
+ import { getNodes, invalidate } from './vault-cache.mjs';
7
8
  import { logger } from './logger.mjs';
8
9
 
9
10
  /**
@@ -69,7 +70,7 @@ function buildClarityPrompt(node) {
69
70
  * @returns {{ rewrote: boolean, qualityScore?: number, error?: string }}
70
71
  */
71
72
  export async function runClarityReview(slug, { vaultDir, inboxDir, runtimeConfig }) {
72
- const allNodes = loadNodes(vaultDir);
73
+ const allNodes = getNodes(vaultDir);
73
74
  const node = allNodes.find(n => n.slug === slug);
74
75
 
75
76
  if (!node) {
@@ -192,7 +193,7 @@ function buildStalenessPrompt(node) {
192
193
  * @returns {{ verdict: string, confidence: number, error?: string }}
193
194
  */
194
195
  export async function runStalenessCheck(slug, { vaultDir, queueDir, runtimeConfig }) {
195
- const allNodes = loadNodes(vaultDir);
196
+ const allNodes = getNodes(vaultDir);
196
197
  const node = allNodes.find(n => n.slug === slug);
197
198
 
198
199
  if (!node) {
@@ -304,7 +305,7 @@ Output ONLY valid JSON. Maximum 5 gaps.`;
304
305
  * @returns {{ gaps: object[], error?: string }}
305
306
  */
306
307
  export async function runFactSeeker({ vaultDir, queueDir, runtimeConfig }) {
307
- const allNodes = loadNodes(vaultDir);
308
+ const allNodes = getNodes(vaultDir);
308
309
  const activeNodes = allNodes.filter(n => n.status === 'active');
309
310
 
310
311
  if (activeNodes.length === 0) {
@@ -446,7 +447,7 @@ function isHighDriftNode(node) {
446
447
  * @returns {{ audited: number, flagged: number, critical: number }}
447
448
  */
448
449
  export async function runCutoffAudit({ vaultDir, queueDir, runtimeConfig }) {
449
- const allNodes = loadNodes(vaultDir);
450
+ const allNodes = getNodes(vaultDir);
450
451
  const now = Date.now();
451
452
  const verifiedCutoff = 30 * 24 * 60 * 60 * 1000; // 30 days
452
453
 
@@ -574,6 +575,7 @@ async function stampVerified(node) {
574
575
  data.x_cutoff_verified_at = new Date().toISOString();
575
576
  data.x_cutoff_risk = 'none';
576
577
  atomicWrite(node._filepath, safeStringify(content, data));
578
+ invalidate(path.dirname(node._filepath));
577
579
  } catch (err) {
578
580
  logger.debug('clarity-rewriter: stampCutoffRiskNone failed', { err: err.message });
579
581
  }
@@ -591,6 +593,7 @@ async function stampCutoffRisk(node, riskLevel, claimCount) {
591
593
  const confidencePenalty = riskLevel === 'critical' ? 0.3 : riskLevel === 'high' ? 0.2 : 0.1;
592
594
  data.confidence = Math.max(0.1, (data.confidence || 0.5) - confidencePenalty);
593
595
  atomicWrite(node._filepath, safeStringify(content, data));
596
+ invalidate(path.dirname(node._filepath));
594
597
  } catch (err) {
595
598
  logger.debug('clarity-rewriter: stampCutoffRisk failed', { err: err.message });
596
599
  }
@@ -635,7 +638,7 @@ Output ONLY valid JSON.`;
635
638
  * @returns {{ correctionSlug: string, verdict: string, error?: string }}
636
639
  */
637
640
  export async function writeCorrection(originalSlug, researchEvidence, { vaultDir, inboxDir, runtimeConfig }) {
638
- const allNodes = loadNodes(vaultDir);
641
+ const allNodes = getNodes(vaultDir);
639
642
  const original = allNodes.find(n => n.slug === originalSlug);
640
643
 
641
644
  if (!original) {
@@ -764,6 +767,7 @@ async function stampNodeForCorrection(node, correctionSlug, disposition) {
764
767
  data.confidence = Math.max(0.05, (data.confidence || 0.5) * 0.3);
765
768
  data.updated = new Date().toISOString();
766
769
  atomicWrite(node._filepath, safeStringify(content, data));
770
+ invalidate(path.dirname(node._filepath));
767
771
  } catch (err) {
768
772
  logger.debug('clarity-rewriter: stampNodeForCorrection failed', { err: err.message });
769
773
  }
@@ -3,6 +3,7 @@ import path from 'path';
3
3
  import matter from 'gray-matter';
4
4
  import { callLocalRuntime } from './runtime.mjs';
5
5
  import { atomicWrite, safeStringify } from './vault.mjs';
6
+ import { getNodes, invalidate } from './vault-cache.mjs';
6
7
  import { logger } from './logger.mjs';
7
8
 
8
9
  /**
@@ -164,6 +165,7 @@ async function promoteToVault(draftNode, confidenceAdj, vaultDir) {
164
165
 
165
166
  const targetPath = path.join(targetDir, `${data.slug}.md`);
166
167
  atomicWrite(targetPath, safeStringify(content, data));
168
+ invalidate(vaultDir);
167
169
 
168
170
  // Remove from inbox
169
171
  try { fs.unlinkSync(draftNode._filepath); } catch { /* ignore */ }
@@ -222,8 +224,7 @@ export async function runConclusionWriter({ inboxDir, vaultDir, quarantineDir, r
222
224
  }
223
225
 
224
226
  // Load all active vault nodes for comparison
225
- const { loadNodes } = await import('./vault.mjs');
226
- const allActiveNodes = loadNodes(vaultDir).filter(n => n.status === 'active');
227
+ const allActiveNodes = getNodes(vaultDir).filter(n => n.status === 'active');
227
228
 
228
229
  let approved = 0;
229
230
  let rejected = 0;
@@ -96,6 +96,12 @@ if (!process.env._TR_TEST_AGENT_DIR) {
96
96
  let secrets = {};
97
97
  try {
98
98
  const pathsToCheck = [];
99
+ // 1. Prioritize workspace-local root secrets
100
+ pathsToCheck.push(path.join(process.cwd(), '.agent', 'secrets.enc'));
101
+ if (resolvedAgentDir) {
102
+ pathsToCheck.push(path.join(resolvedAgentDir, 'secrets.enc'));
103
+ }
104
+ // 2. Fallbacks
99
105
  if (projectBrainDir) {
100
106
  pathsToCheck.push(path.join(projectBrainDir, 'config', 'secrets.enc'));
101
107
  }
@@ -2,7 +2,8 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import matter from 'gray-matter';
4
4
  import crypto from 'node:crypto';
5
- import { atomicWrite, walkMd, loadNodes, writeNode, safeStringify } from './vault.mjs';
5
+ import { atomicWrite, walkMd, writeNode, safeStringify } from './vault.mjs';
6
+ import { getNodes, invalidate } from './vault-cache.mjs';
6
7
  import { logger } from './logger.mjs';
7
8
 
8
9
  /**
@@ -158,7 +159,7 @@ export function detectSemanticConflicts(candidate, existingNodes, thresholds = {
158
159
  * @returns {object[]} Array of conflict records (deduplicated by pair).
159
160
  */
160
161
  export function scanVaultForConflicts(vaultDir, thresholds = {}) {
161
- const nodes = loadNodes(vaultDir).filter(n => n.status === 'active');
162
+ const nodes = getNodes(vaultDir).filter(n => n.status === 'active');
162
163
  const seen = new Set();
163
164
  const conflicts = [];
164
165
 
@@ -420,6 +421,7 @@ export function applyAutoResolution(conflict, resolution, vaultDir) {
420
421
  try {
421
422
  writeNode(loserNode, vaultDir);
422
423
  writeNode(winnerNode, vaultDir);
424
+ invalidate(vaultDir);
423
425
  } catch (err) {
424
426
  logger.error('conflict-detector', `Failed to apply auto-resolution: ${err.message}`);
425
427
  }