total-recall-brain 3.1.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/LICENSE +21 -0
- package/README.md +173 -0
- package/bin/antigravity.mjs +212 -0
- package/bin/init.mjs +36 -0
- package/bin/oci-sniper.sh +69 -0
- package/bin/total-recall.mjs +229 -0
- package/docs/ARCHITECTURE.md +251 -0
- package/docs/IDE_INTEGRATION.md +112 -0
- package/package.json +71 -0
- package/scaffold/.agent/skills/total-recall/SKILL.md +195 -0
- package/scaffold/.agent/skills/total-recall/evals/evals.json +20 -0
- package/scaffold/.agent/skills/total-recall/memory-vault/concepts/cli-help-reference.md +145 -0
- package/scaffold/.agent/skills/total-recall/memory-vault/invariants/operating-instructions.md +75 -0
- package/scaffold/.agent/skills/total-recall/memory-vault/preferences/always-websearch-gap.md +16 -0
- package/scaffold/.agent/skills/total-recall/memory-vault/preferences/topic-research-sop.md +17 -0
- package/scaffold/.agent/skills/total-recall/references/api-reference.md +187 -0
- package/scaffold/.agent/skills/total-recall/references/architecture-reference.md +81 -0
- package/scaffold/.agent/skills/total-recall/references/cli-reference.md +103 -0
- package/scaffold/.agent/skills/total-recall/references/ssss-reference.md +88 -0
- package/scaffold/.agent/skills/total-recall/references/troubleshooting.md +68 -0
- package/scaffold/.agent/skills/total-recall/scripts/sync-repo.mjs +83 -0
- package/scaffold/.agent/skills/total-recall/skills/cli-agents/SKILL.md +122 -0
- package/scaffold/.agent/skills/total-recall/skills/cli-agents/agents.yml +27 -0
- package/scaffold/.agent/skills/total-recall/skills/cli-agents/references/antigravity.txt +20 -0
- package/scaffold/.agent/skills/total-recall/skills/cli-agents/references/claude.txt +58 -0
- package/scaffold/.agent/skills/total-recall/skills/cli-agents/references/codex.txt +81 -0
- package/scaffold/.agent/skills/total-recall/skills/cli-agents/references/gemini.txt +44 -0
- package/scaffold/.agent/skills/total-recall/skills/research/SKILL.md +337 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/SKILL.md +281 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/assets/skill-template/SKILL.md +20 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/evals/evals.json +20 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/hooks/on-skill-add.sh +14 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/hooks/on-skill-delete.sh +14 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/learnings/.gitkeep +0 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/references/.gitkeep +0 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/scripts/.gitkeep +0 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/scripts/create-skill.sh +65 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/scripts/enforce-skill-optimization.mjs +135 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/scripts/find-skills.mjs +124 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/scripts/scan-skill.mjs +207 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/scripts/skill-watcher.mjs +64 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/subagents/audit-all-skills.md +62 -0
- package/scaffold/.agent/skills/total-recall/skills/ssss/SKILL.md +27 -0
- package/scaffold/.agent/skills/total-recall/skills/ssss/evals/evals.json +17 -0
- package/scaffold/.agent/skills/total-recall/skills/ssss/references/ssss-spec.md +117 -0
- package/scaffold/.agent/skills/total-recall/skills/ssss/scripts/validate-schema.mjs +54 -0
- package/scaffold/.agent/skills/total-recall/skills/ssss/subagents/audit-ssss-node.md +9 -0
- package/scaffold/.agent/skills/total-recall/subagents/total-recall-diagnostician.md +13 -0
- package/src/cli/agent-dir.mjs +105 -0
- package/src/cli/backup.mjs +313 -0
- package/src/cli/brain.mjs +224 -0
- package/src/cli/chat.mjs +74 -0
- package/src/cli/config.mjs +247 -0
- package/src/cli/connect.mjs +872 -0
- package/src/cli/daemon.mjs +228 -0
- package/src/cli/deploy-ui.mjs +1793 -0
- package/src/cli/deploy.mjs +1726 -0
- package/src/cli/doctor.mjs +254 -0
- package/src/cli/dream.mjs +80 -0
- package/src/cli/forget.mjs +92 -0
- package/src/cli/friction.mjs +21 -0
- package/src/cli/generate-pat.mjs +65 -0
- package/src/cli/hash-password.mjs +22 -0
- package/src/cli/help.mjs +153 -0
- package/src/cli/import-rules.mjs +102 -0
- package/src/cli/ingest/index.mjs +318 -0
- package/src/cli/ingest/parsers/chrome-bookmarks.mjs +238 -0
- package/src/cli/ingest/parsers/google-keep.mjs +147 -0
- package/src/cli/ingest/parsers/search-history.mjs +142 -0
- package/src/cli/ingest/parsers/youtube-history.mjs +181 -0
- package/src/cli/ingest/utils/dedup.mjs +69 -0
- package/src/cli/ingest/utils/takeout-walker.mjs +101 -0
- package/src/cli/ingest.mjs +102 -0
- package/src/cli/init.mjs +736 -0
- package/src/cli/integration-dispatcher.mjs +128 -0
- package/src/cli/lint.mjs +166 -0
- package/src/cli/map.mjs +194 -0
- package/src/cli/migrate.mjs +87 -0
- package/src/cli/rebuild.mjs +88 -0
- package/src/cli/recall.mjs +219 -0
- package/src/cli/relay.mjs +518 -0
- package/src/cli/remember.mjs +306 -0
- package/src/cli/research.mjs +381 -0
- package/src/cli/reset-password.mjs +75 -0
- package/src/cli/restore.mjs +135 -0
- package/src/cli/setup.mjs +616 -0
- package/src/cli/share.mjs +180 -0
- package/src/cli/skill.mjs +313 -0
- package/src/cli/snapshot.mjs +108 -0
- package/src/cli/start.mjs +63 -0
- package/src/cli/status.mjs +296 -0
- package/src/cli/sync.mjs +383 -0
- package/src/cli/uninstall.mjs +613 -0
- package/src/cli/upgrade.mjs +141 -0
- package/src/cli/wizard.html +3682 -0
- package/src/core/blackboard.mjs +54 -0
- package/src/core/clarity-rewriter.mjs +770 -0
- package/src/core/conclusion-writer.mjs +259 -0
- package/src/core/config.mjs +277 -0
- package/src/core/conflict-detector.mjs +536 -0
- package/src/core/crypto.mjs +107 -0
- package/src/core/daemon-control.mjs +158 -0
- package/src/core/daemon-loop.mjs +364 -0
- package/src/core/dream.mjs +347 -0
- package/src/core/drift-detector.mjs +143 -0
- package/src/core/embeddings.mjs +632 -0
- package/src/core/emergency-alerts.mjs +137 -0
- package/src/core/evolution.mjs +107 -0
- package/src/core/fact-seeker.mjs +1602 -0
- package/src/core/friction.mjs +95 -0
- package/src/core/import-rules.mjs +308 -0
- package/src/core/inference-engine.mjs +367 -0
- package/src/core/logger.mjs +75 -0
- package/src/core/memory-layers.mjs +96 -0
- package/src/core/migrate.mjs +116 -0
- package/src/core/notifications.mjs +77 -0
- package/src/core/operation-validator.mjs +272 -0
- package/src/core/optimizer.mjs +330 -0
- package/src/core/pattern_detector.mjs +113 -0
- package/src/core/post-mortem.mjs +413 -0
- package/src/core/protect-instructions.mjs +148 -0
- package/src/core/quick-capture.mjs +145 -0
- package/src/core/research-queue.mjs +431 -0
- package/src/core/research.mjs +461 -0
- package/src/core/runtime.mjs +515 -0
- package/src/core/sandbox.mjs +163 -0
- package/src/core/scheduler.mjs +569 -0
- package/src/core/schema.mjs +333 -0
- package/src/core/search.mjs +219 -0
- package/src/core/semantic-index.mjs +276 -0
- package/src/core/session-watcher.mjs +850 -0
- package/src/core/snapshot.mjs +133 -0
- package/src/core/source-adapters.mjs +1038 -0
- package/src/core/steering.mjs +137 -0
- package/src/core/surface.mjs +444 -0
- package/src/core/task_runner.mjs +125 -0
- package/src/core/tts.mjs +142 -0
- package/src/core/usage-tracker.mjs +349 -0
- package/src/core/vault-cache.mjs +76 -0
- package/src/core/vault.mjs +273 -0
- package/src/core/watchdog.mjs +236 -0
- package/src/server/api.mjs +944 -0
- package/src/server/auth.mjs +410 -0
- package/src/server/index.mjs +868 -0
- package/src/server/keys.mjs +181 -0
- package/src/server/rest.mjs +1652 -0
- package/src/server/route-manifest.json +77 -0
- package/src/server/routes/_shared.mjs +93 -0
- package/src/server/routes/keys.mjs +55 -0
- package/src/server/routes/memory.mjs +249 -0
- package/src/server/routes/sessions.mjs +336 -0
- package/src/server/routes/share.mjs +138 -0
- package/src/server/tools.mjs +618 -0
- package/templates/Caddyfile +37 -0
- package/templates/com.totalrecall.backup.plist +42 -0
- package/templates/com.totalrecall.daemon.plist +40 -0
- package/templates/com.totalrecall.duckdns.plist +23 -0
- package/templates/com.totalrecall.server.plist +46 -0
- package/templates/default-config/budget.yml +6 -0
- package/templates/default-config/runtime.yml +11 -0
- package/templates/default-config/security.yml +53 -0
- package/templates/default-config/voice.yml +34 -0
- package/templates/obsidian-queries/conflicts.md +26 -0
- package/templates/obsidian-queries/daily.md +17 -0
- package/templates/obsidian-queries/index.md +38 -0
- package/templates/obsidian-queries/skills.md +14 -0
- package/templates/onboarding-interview.md +72 -0
- package/templates/skill_engineering.md +40 -0
- package/templates/total-recall-daemon.service +21 -0
- package/templates/total-recall-server.service +20 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { loadNodes, writeNode, atomicWrite, walkMd } from './vault.mjs';
|
|
5
|
+
import { logger } from './logger.mjs';
|
|
6
|
+
import { agentDir, brainDir } from './config.mjs';
|
|
7
|
+
import { loadQueue } from './research-queue.mjs';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Write a daily dream-cycle summary to memory-vault/daily/YYYY-MM-DD.md.
|
|
11
|
+
* SSSS node natively; Obsidian Daily Notes plugin reads these files directly.
|
|
12
|
+
*/
|
|
13
|
+
function writeDailyNote(vaultDir, summaryLines) {
|
|
14
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
15
|
+
const now = new Date().toISOString();
|
|
16
|
+
const dailyDir = path.join(vaultDir, 'daily');
|
|
17
|
+
if (!fs.existsSync(dailyDir)) fs.mkdirSync(dailyDir, { recursive: true });
|
|
18
|
+
|
|
19
|
+
const filePath = path.join(dailyDir, `${today}.md`);
|
|
20
|
+
|
|
21
|
+
if (fs.existsSync(filePath)) {
|
|
22
|
+
// Append this run's summary to the existing note
|
|
23
|
+
const existing = fs.readFileSync(filePath, 'utf8');
|
|
24
|
+
const runBlock = `\n## Dream Cycle – ${now}\n\n${summaryLines.map(l => `- ${l}`).join('\n')}\n`;
|
|
25
|
+
atomicWrite(filePath, existing.trimEnd() + runBlock);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const frontmatter = [
|
|
30
|
+
'---',
|
|
31
|
+
'type: memory',
|
|
32
|
+
`slug: "daily-${today}"`,
|
|
33
|
+
'category: daily',
|
|
34
|
+
`title: "Daily Note: ${today}"`,
|
|
35
|
+
'status: active',
|
|
36
|
+
'confidence: 1.0',
|
|
37
|
+
'importance: 2',
|
|
38
|
+
`created: "${now}"`,
|
|
39
|
+
`updated: "${now}"`,
|
|
40
|
+
`last_accessed: "${now}"`,
|
|
41
|
+
'source:',
|
|
42
|
+
' type: dream-cycle',
|
|
43
|
+
` session_id: "dream-${today}"`,
|
|
44
|
+
' evidence_count: 1',
|
|
45
|
+
'supersedes: []',
|
|
46
|
+
'superseded_by: null',
|
|
47
|
+
'contradicts: []',
|
|
48
|
+
'tags: [daily, dream-cycle]',
|
|
49
|
+
'related: []',
|
|
50
|
+
'routes_to_skills: []',
|
|
51
|
+
'sentiment_polarity: descriptive',
|
|
52
|
+
'sentiment_target: system',
|
|
53
|
+
'modality: should',
|
|
54
|
+
'subject: system',
|
|
55
|
+
'predicate: ran',
|
|
56
|
+
'object: dream-cycle',
|
|
57
|
+
'decay:',
|
|
58
|
+
' half_life_days: 30',
|
|
59
|
+
' access_count: 0',
|
|
60
|
+
'schema_version: 2',
|
|
61
|
+
'---',
|
|
62
|
+
''
|
|
63
|
+
].join('\n');
|
|
64
|
+
|
|
65
|
+
const body = [
|
|
66
|
+
`# Daily Note: ${today}`,
|
|
67
|
+
'',
|
|
68
|
+
`## Dream Cycle – ${now}`,
|
|
69
|
+
'',
|
|
70
|
+
...summaryLines.map(l => `- ${l}`),
|
|
71
|
+
''
|
|
72
|
+
].join('\n');
|
|
73
|
+
|
|
74
|
+
atomicWrite(filePath, frontmatter + body);
|
|
75
|
+
}
|
|
76
|
+
import { detectConflicts, quarantineConflict } from './steering.mjs';
|
|
77
|
+
import { compileSurface } from './surface.mjs';
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Phase 5: Automatic Storage & Memory Pruning.
|
|
81
|
+
* Keeps the VFS local directories clean of old logs, draft files,
|
|
82
|
+
* expired proposals, and temporary IDE session transcripts.
|
|
83
|
+
*/
|
|
84
|
+
export function autoPruneStorage(brainDir, vaultDir, conflictsDir) {
|
|
85
|
+
const logsDir = path.join(brainDir, 'logs');
|
|
86
|
+
const proposalsDir = path.join(vaultDir, 'proposals');
|
|
87
|
+
const inboxDir = path.join(brainDir, 'memory-inbox');
|
|
88
|
+
|
|
89
|
+
const logMaxAgeMs = 3 * 24 * 60 * 60 * 1000; // Keep logs and proposals for 3 days
|
|
90
|
+
const draftMaxAgeMs = 24 * 60 * 60 * 1000; // Aggressively clear background drafts (like RLHF files) after 24 hours
|
|
91
|
+
const now = Date.now();
|
|
92
|
+
|
|
93
|
+
// Retrieve node slugs of active research projects so we never prune their drafts in progress
|
|
94
|
+
const activeResearchSlugs = new Set();
|
|
95
|
+
try {
|
|
96
|
+
const queue = loadQueue();
|
|
97
|
+
for (const item of queue) {
|
|
98
|
+
if (item.status === 'pending' || item.status === 'in_progress') {
|
|
99
|
+
if (item.node_slug) {
|
|
100
|
+
activeResearchSlugs.add(item.node_slug);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
} catch (err) {
|
|
105
|
+
logger.warn('dream', `Failed to load active research queue during pruning: ${err.message}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const pruneDir = (dir, maxAge) => {
|
|
109
|
+
if (!fs.existsSync(dir)) return;
|
|
110
|
+
try {
|
|
111
|
+
const files = fs.readdirSync(dir);
|
|
112
|
+
for (const file of files) {
|
|
113
|
+
const fullPath = path.join(dir, file);
|
|
114
|
+
const stat = fs.statSync(fullPath);
|
|
115
|
+
if (stat.isFile()) {
|
|
116
|
+
// Exempt active research drafts from pruning
|
|
117
|
+
const baseName = path.basename(file, '.md');
|
|
118
|
+
if (activeResearchSlugs.has(baseName)) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (now - stat.mtimeMs > maxAge) {
|
|
122
|
+
fs.unlinkSync(fullPath);
|
|
123
|
+
}
|
|
124
|
+
} else if (stat.isDirectory()) {
|
|
125
|
+
pruneDir(fullPath, maxAge);
|
|
126
|
+
if (fs.readdirSync(fullPath).length === 0) {
|
|
127
|
+
fs.rmdirSync(fullPath);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
} catch {}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
pruneDir(logsDir, logMaxAgeMs);
|
|
135
|
+
pruneDir(proposalsDir, logMaxAgeMs);
|
|
136
|
+
pruneDir(inboxDir, draftMaxAgeMs); // Keeps the local dev inbox fully clean of automatic research noise
|
|
137
|
+
if (conflictsDir) pruneDir(conflictsDir, draftMaxAgeMs);
|
|
138
|
+
|
|
139
|
+
// Automatically prune transient planning files in every active/historical conversation folder,
|
|
140
|
+
// while permanently preserving the conversations/threads themselves (.system_generated)
|
|
141
|
+
try {
|
|
142
|
+
const homeDir = process.env._TR_TEST_HOME_DIR || os.homedir();
|
|
143
|
+
const agAppDir = path.join(homeDir, '.gemini', 'antigravity');
|
|
144
|
+
const agBrainDir = path.join(agAppDir, 'brain');
|
|
145
|
+
if (fs.existsSync(agBrainDir)) {
|
|
146
|
+
const convs = fs.readdirSync(agBrainDir);
|
|
147
|
+
for (const convId of convs) {
|
|
148
|
+
const convPath = path.join(agBrainDir, convId);
|
|
149
|
+
const convStat = fs.statSync(convPath);
|
|
150
|
+
if (convStat.isDirectory()) {
|
|
151
|
+
const rootFiles = fs.readdirSync(convPath);
|
|
152
|
+
for (const item of rootFiles) {
|
|
153
|
+
const itemPath = path.join(convPath, item);
|
|
154
|
+
const itemStat = fs.statSync(itemPath);
|
|
155
|
+
if (itemStat.isFile()) {
|
|
156
|
+
const isTransientFile = item.endsWith('.md') || item.endsWith('.json');
|
|
157
|
+
if (isTransientFile && now - itemStat.mtimeMs > draftMaxAgeMs) {
|
|
158
|
+
fs.unlinkSync(itemPath);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
} catch (err) {
|
|
166
|
+
logger.warn('dream', `Failed to prune Antigravity transient conversation folders: ${err.message}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
import {
|
|
171
|
+
generateMemoryCleanupProposals,
|
|
172
|
+
generateStaleKnowledgeRefreshProposals,
|
|
173
|
+
evaluateProposalGate
|
|
174
|
+
} from './optimizer.mjs';
|
|
175
|
+
|
|
176
|
+
const DREAM_PROMOTION_THRESHOLD = 0.7;
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Phase 1: Light Sleep. Scan for modifications.
|
|
180
|
+
*/
|
|
181
|
+
export function scanModifiedVault(vaultDir, sinceHours = 24) {
|
|
182
|
+
const cutoff = Date.now() - sinceHours * 60 * 60 * 1000;
|
|
183
|
+
return walkMd(vaultDir).filter(fp => {
|
|
184
|
+
try {
|
|
185
|
+
return fs.statSync(fp).mtimeMs >= cutoff;
|
|
186
|
+
} catch {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Phase 2: REM Sleep. Score nodes, check conflicts, promote/decay.
|
|
194
|
+
*/
|
|
195
|
+
export function evaluateCandidates(candidates, existingNodes, conflictsDir) {
|
|
196
|
+
const promoted = [];
|
|
197
|
+
const allConflicts = [];
|
|
198
|
+
|
|
199
|
+
for (const candidate of candidates) {
|
|
200
|
+
const conflicts = detectConflicts(candidate, existingNodes);
|
|
201
|
+
|
|
202
|
+
if (conflicts.length > 0) {
|
|
203
|
+
for (const c of conflicts) {
|
|
204
|
+
quarantineConflict(c, conflictsDir);
|
|
205
|
+
}
|
|
206
|
+
allConflicts.push(...conflicts);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const score = (candidate.evidence_count || 1) * 0.2 + (candidate.importance || 1) * 0.1;
|
|
211
|
+
if (score >= DREAM_PROMOTION_THRESHOLD || candidate.confidence >= 0.8) {
|
|
212
|
+
const node = {
|
|
213
|
+
...candidate,
|
|
214
|
+
status: 'active',
|
|
215
|
+
confidence: Math.min(1.0, (candidate.confidence || 0.5) + 0.1),
|
|
216
|
+
};
|
|
217
|
+
promoted.push(node);
|
|
218
|
+
existingNodes.push(node);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return { promoted, conflicted: allConflicts };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Main Dream Cycle execution.
|
|
227
|
+
*/
|
|
228
|
+
export async function runDreamCycle({
|
|
229
|
+
vaultDir, skillsDir, derivedDir, conflictsDir, instructionsFile
|
|
230
|
+
}) {
|
|
231
|
+
const sessionsDir = path.join(brainDir, 'sessions');
|
|
232
|
+
|
|
233
|
+
logger.info('dream', 'PHASE 0 — Session Ingestion (IDE Conversation Logs)');
|
|
234
|
+
try {
|
|
235
|
+
const { scanAndIngest } = await import('./session-watcher.mjs');
|
|
236
|
+
const ingestResult = scanAndIngest(sessionsDir);
|
|
237
|
+
logger.info('dream', `Ingested: ${ingestResult.ingested} new sessions`);
|
|
238
|
+
} catch (err) {
|
|
239
|
+
logger.warn('dream', `Session ingestion skipped: ${err.message}`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
logger.info('dream', 'PHASE 1 — Light Sleep (Scan)');
|
|
243
|
+
const modified = scanModifiedVault(vaultDir);
|
|
244
|
+
logger.info('dream', `Modified vault files: ${modified.length}`);
|
|
245
|
+
|
|
246
|
+
// In a real system, we'd extract candidates from sessions. For now, we simulate.
|
|
247
|
+
const candidates = [];
|
|
248
|
+
|
|
249
|
+
logger.info('dream', 'PHASE 2 — REM (Pattern Recognition)');
|
|
250
|
+
const existingNodes = loadNodes(vaultDir);
|
|
251
|
+
|
|
252
|
+
if (candidates.length > 0) {
|
|
253
|
+
const { promoted, conflicted } = evaluateCandidates(candidates, existingNodes, conflictsDir);
|
|
254
|
+
logger.info('dream', `Promoted: ${promoted.length} | Conflicts: ${conflicted.length}`);
|
|
255
|
+
for (const node of promoted) {
|
|
256
|
+
writeNode(node, vaultDir);
|
|
257
|
+
}
|
|
258
|
+
} else {
|
|
259
|
+
logger.info('dream', 'No new candidates to evaluate.');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
logger.info('dream', 'PHASE 3 — Deep Sleep (Recompile Surface)');
|
|
263
|
+
try {
|
|
264
|
+
await compileSurface({ vaultDir, skillsDir, derivedDir, instructionsFile });
|
|
265
|
+
logger.info('dream', 'Surface recompiled successfully');
|
|
266
|
+
} catch (err) {
|
|
267
|
+
logger.error('dream', `Deep Sleep compile failed: ${err.message}`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
logger.info('dream', 'PHASE 4 — Lucid Dreaming (Optimizer Proposals)');
|
|
271
|
+
let proposalCount = 0;
|
|
272
|
+
try {
|
|
273
|
+
const proposalsDir = path.join(vaultDir, 'proposals');
|
|
274
|
+
if (!fs.existsSync(proposalsDir)) fs.mkdirSync(proposalsDir, { recursive: true });
|
|
275
|
+
|
|
276
|
+
const cleanupProposals = await generateMemoryCleanupProposals(vaultDir);
|
|
277
|
+
const staleProposals = await generateStaleKnowledgeRefreshProposals(vaultDir);
|
|
278
|
+
const allProposals = [...cleanupProposals, ...staleProposals];
|
|
279
|
+
|
|
280
|
+
if (allProposals.length > 0) {
|
|
281
|
+
logger.info('dream', `Generated ${allProposals.length} optimization proposals.`);
|
|
282
|
+
for (const p of allProposals) {
|
|
283
|
+
const passed = await evaluateProposalGate(p, null);
|
|
284
|
+
logger.info('dream', `Proposal [${p.category}]: ${p.summary} -> ${passed ? 'ACCEPTED' : 'REJECTED'}`);
|
|
285
|
+
writeNode(p, proposalsDir);
|
|
286
|
+
if (passed) proposalCount++;
|
|
287
|
+
}
|
|
288
|
+
} else {
|
|
289
|
+
logger.info('dream', 'No new proposals generated.');
|
|
290
|
+
}
|
|
291
|
+
} catch (err) {
|
|
292
|
+
logger.error('dream', `Optimizer failed: ${err.message}`);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Write daily note summary (native SSSS node; Obsidian Daily Notes reads it directly)
|
|
296
|
+
try {
|
|
297
|
+
const existingNodes = loadNodes(vaultDir);
|
|
298
|
+
writeDailyNote(vaultDir, [
|
|
299
|
+
`Modified vault files scanned: ${modified.length}`,
|
|
300
|
+
`Active nodes: ${existingNodes.filter(n => n.status === 'active').length}`,
|
|
301
|
+
`Proposals accepted: ${proposalCount}`,
|
|
302
|
+
]);
|
|
303
|
+
} catch (err) {
|
|
304
|
+
logger.error('dream', `Daily note write failed: ${err.message}`);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
logger.info('dream', 'PHASE 5 — Automatic Storage & Memory Pruning');
|
|
308
|
+
try {
|
|
309
|
+
autoPruneStorage(brainDir, vaultDir, conflictsDir);
|
|
310
|
+
logger.info('dream', 'Automated VFS storage pruning completed successfully');
|
|
311
|
+
} catch (err) {
|
|
312
|
+
logger.error('dream', `Storage pruning failed: ${err.message}`);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return { status: 'success' };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ─── Standalone Daemon Execution ────────────────────────────────────────────────
|
|
319
|
+
|
|
320
|
+
import { fileURLToPath } from 'url';
|
|
321
|
+
import os from 'os';
|
|
322
|
+
|
|
323
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
324
|
+
const vaultDir = path.join(brainDir, 'memory-vault');
|
|
325
|
+
const skillsDir = path.join(agentDir, 'skills');
|
|
326
|
+
const derivedDir = path.join(brainDir, 'memory-derived');
|
|
327
|
+
const conflictsDir = path.join(brainDir, 'memory-inbox', 'conflicts');
|
|
328
|
+
const instructionsFile = path.join(agentDir, 'INSTRUCTIONS.md');
|
|
329
|
+
|
|
330
|
+
logger.info('dream', 'Total Recall Dream Cycle Daemon Started');
|
|
331
|
+
|
|
332
|
+
async function daemonLoop() {
|
|
333
|
+
while (true) {
|
|
334
|
+
try {
|
|
335
|
+
await runDreamCycle({
|
|
336
|
+
vaultDir, skillsDir, derivedDir, conflictsDir, instructionsFile
|
|
337
|
+
});
|
|
338
|
+
} catch (err) {
|
|
339
|
+
logger.error('dream', 'Dream cycle iteration failed', { err: err.message });
|
|
340
|
+
}
|
|
341
|
+
// Sleep for 60 seconds before next cycle
|
|
342
|
+
await new Promise(resolve => setTimeout(resolve, 60000));
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
daemonLoop();
|
|
347
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import { loadNodes } from './vault.mjs';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* SSSS Drift Detector
|
|
8
|
+
*
|
|
9
|
+
* Implements §10 of the SSSS spec (Derived Artifacts).
|
|
10
|
+
* Detects discrepancies between the canonical Markdown vault and
|
|
11
|
+
* derived disposable projections (e.g., graph-index.jsonl).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Compare two simple objects for equality on a specific set of keys.
|
|
16
|
+
*/
|
|
17
|
+
function propertiesMatch(a, b, keys) {
|
|
18
|
+
for (const key of keys) {
|
|
19
|
+
if (a[key] !== b[key]) return false;
|
|
20
|
+
}
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Scan for drift between the vault and the graph index.
|
|
26
|
+
*
|
|
27
|
+
* @param {string} vaultDir - Path to the memory-vault directory.
|
|
28
|
+
* @param {string} derivedDir - Path to the memory-derived directory.
|
|
29
|
+
* @returns {{ drifted: boolean, missing_in_index: string[], stale_in_index: string[], missing_in_vault: string[] }}
|
|
30
|
+
*/
|
|
31
|
+
export function detectIndexDrift(vaultDir, derivedDir, globalVaultDir = undefined) {
|
|
32
|
+
const result = {
|
|
33
|
+
drifted: false,
|
|
34
|
+
missing_in_index: [],
|
|
35
|
+
stale_in_index: [],
|
|
36
|
+
missing_in_vault: []
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const graphIndexPath = path.join(derivedDir, 'graph-index.jsonl');
|
|
40
|
+
|
|
41
|
+
if (!fs.existsSync(graphIndexPath)) {
|
|
42
|
+
// No index means total drift
|
|
43
|
+
result.drifted = true;
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Load canonical nodes
|
|
48
|
+
const nodes = loadNodes(vaultDir);
|
|
49
|
+
if (globalVaultDir && fs.existsSync(globalVaultDir)) {
|
|
50
|
+
try {
|
|
51
|
+
const globalNodes = loadNodes(globalVaultDir);
|
|
52
|
+
nodes.push(...globalNodes);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
// Ignore or log global vault load failure
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const canonicalMap = new Map();
|
|
58
|
+
for (const node of nodes) {
|
|
59
|
+
canonicalMap.set(node.slug, node);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Load derived index
|
|
63
|
+
const indexMap = new Map();
|
|
64
|
+
try {
|
|
65
|
+
const lines = fs.readFileSync(graphIndexPath, 'utf8').trim().split('\n');
|
|
66
|
+
for (const line of lines) {
|
|
67
|
+
if (!line) continue;
|
|
68
|
+
const entry = JSON.parse(line);
|
|
69
|
+
indexMap.set(entry.slug, entry);
|
|
70
|
+
}
|
|
71
|
+
} catch (err) {
|
|
72
|
+
// Malformed index is total drift
|
|
73
|
+
result.drifted = true;
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const keysToCheck = ['title', 'category', 'status', 'confidence'];
|
|
78
|
+
|
|
79
|
+
// Check for missing or stale entries in the index
|
|
80
|
+
for (const [slug, canonicalNode] of canonicalMap.entries()) {
|
|
81
|
+
const indexEntry = indexMap.get(slug);
|
|
82
|
+
if (!indexEntry) {
|
|
83
|
+
result.missing_in_index.push(slug);
|
|
84
|
+
result.drifted = true;
|
|
85
|
+
} else {
|
|
86
|
+
if (!propertiesMatch(canonicalNode, indexEntry, keysToCheck)) {
|
|
87
|
+
result.stale_in_index.push(slug);
|
|
88
|
+
result.drifted = true;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Check for ghost entries in the index (exist in index but not in vault)
|
|
94
|
+
for (const slug of indexMap.keys()) {
|
|
95
|
+
if (!canonicalMap.has(slug)) {
|
|
96
|
+
result.missing_in_vault.push(slug);
|
|
97
|
+
result.drifted = true;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Verify event log append-only invariant.
|
|
106
|
+
* Checks that the event log file is well-formed JSONL and strictly temporally ordered.
|
|
107
|
+
*/
|
|
108
|
+
export function checkEventLogIntegrity(eventLogPath) {
|
|
109
|
+
if (!fs.existsSync(eventLogPath)) {
|
|
110
|
+
return { ok: true, reason: 'Log does not exist' };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const lines = fs.readFileSync(eventLogPath, 'utf8').trim().split('\n');
|
|
115
|
+
let lastTs = null;
|
|
116
|
+
let lineNum = 0;
|
|
117
|
+
|
|
118
|
+
for (const line of lines) {
|
|
119
|
+
lineNum++;
|
|
120
|
+
if (!line) continue;
|
|
121
|
+
const event = JSON.parse(line);
|
|
122
|
+
|
|
123
|
+
if (!event.ts) {
|
|
124
|
+
return { ok: false, error: `Missing timestamp on line ${lineNum}` };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const currentTs = new Date(event.ts).getTime();
|
|
128
|
+
if (isNaN(currentTs)) {
|
|
129
|
+
return { ok: false, error: `Invalid timestamp '${event.ts}' on line ${lineNum}` };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (lastTs !== null && currentTs < lastTs) {
|
|
133
|
+
return { ok: false, error: `Temporal violation on line ${lineNum}: ${event.ts} is before previous event.` };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
lastTs = currentTs;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { ok: true, events_checked: lineNum };
|
|
140
|
+
} catch (err) {
|
|
141
|
+
return { ok: false, error: `Parse failure: ${err.message}` };
|
|
142
|
+
}
|
|
143
|
+
}
|