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.
- package/README.md +1 -1
- package/extension/background.js +14 -2
- package/extension/content-script.js +18 -1
- package/extension/lib/brain-client.js +86 -52
- package/extension/lib/preconfigured.js +5 -0
- package/extension/manifest.json +1 -1
- package/extension/options/options.html +1 -0
- package/extension/options/options.js +12 -6
- package/extension/popup/popup.html +1 -0
- package/extension/popup/popup.js +29 -4
- package/extension/sidepanel/sidepanel.html +1 -0
- package/extension/sidepanel/sidepanel.js +24 -6
- package/frontend/index.html +8 -0
- package/frontend/src/api.ts +1 -2
- package/frontend/src/pages/ChatPage.tsx +9 -6
- package/frontend/src/pages/IntegrationsPage.tsx +144 -3
- package/package.json +2 -2
- package/scaffold/.agent/skills/total-recall/SKILL.md +55 -6
- package/scaffold/.agent/skills/total-recall/memory-vault/invariants/operating-instructions.md +2 -2
- package/scaffold/.agent/skills/total-recall/skills/cli-agents/agents.yml +1 -6
- package/scaffold/.agent/skills/total-recall/skills/ssss/SKILL.md +19 -0
- package/scaffold/.agent/skills/total-recall/skills/ssss/references/ssss-spec.md +33 -5
- package/src/cli/forget.mjs +18 -12
- package/src/cli/ingest/index.mjs +3 -2
- package/src/cli/init.mjs +30 -0
- package/src/cli/map.mjs +2 -2
- package/src/cli/rebuild.mjs +1 -1
- package/src/cli/remember.mjs +21 -11
- package/src/cli/setup.mjs +10 -1
- package/src/cli/share.mjs +7 -2
- package/src/core/append-log.mjs +131 -0
- package/src/core/clarity-rewriter.mjs +10 -6
- package/src/core/conclusion-writer.mjs +3 -2
- package/src/core/config.mjs +6 -0
- package/src/core/conflict-detector.mjs +4 -2
- package/src/core/context-compiler.mjs +412 -0
- package/src/core/daemon-loop.mjs +15 -0
- package/src/core/dream.mjs +4 -4
- package/src/core/drift-detector.mjs +3 -3
- package/src/core/embeddings.mjs +60 -10
- package/src/core/fact-seeker.mjs +70 -44
- package/src/core/import-rules.mjs +3 -2
- package/src/core/inference-engine.mjs +5 -4
- package/src/core/migrate.mjs +4 -3
- package/src/core/optimizer.mjs +6 -5
- package/src/core/parallel-context.mjs +288 -0
- package/src/core/post-mortem.mjs +4 -3
- package/src/core/research-queue.mjs +4 -3
- package/src/core/runtime.mjs +2 -2
- package/src/core/sandbox.mjs +56 -26
- package/src/core/scheduler.mjs +6 -5
- package/src/core/schema.mjs +2 -2
- package/src/core/search.mjs +167 -101
- package/src/core/surface.mjs +203 -53
- package/src/core/validated-write.mjs +100 -0
- package/src/core/vault-cache.mjs +46 -18
- package/src/core/vault.mjs +17 -29
- package/src/core/vector-field.mjs +659 -0
- package/src/core/vector-store.mjs +85 -0
- package/src/server/api.mjs +4 -3
- package/src/server/index.mjs +13 -61
- package/src/server/rest.mjs +238 -29
- package/src/server/route-manifest.json +344 -77
- package/src/server/routes/memory.mjs +85 -39
- package/src/server/routes/sessions.mjs +6 -2
- 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 {
|
|
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
|
-
|
|
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 {
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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;
|
package/src/core/config.mjs
CHANGED
|
@@ -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,
|
|
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 =
|
|
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
|
}
|