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,770 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import matter from 'gray-matter';
|
|
4
|
+
import crypto from 'crypto';
|
|
5
|
+
import { callLocalRuntime, cleanAndParseJSON } from './runtime.mjs';
|
|
6
|
+
import { loadNodes, atomicWrite, safeStringify } from './vault.mjs';
|
|
7
|
+
import { logger } from './logger.mjs';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Total Recall Clarity Rewriter + Staleness Verifier
|
|
11
|
+
*
|
|
12
|
+
* Clarity Rewriter: LLM reviews each memory node and asks:
|
|
13
|
+
* - Is the title descriptive enough for 7-item skill injection?
|
|
14
|
+
* - Is the body actionable by an AI agent?
|
|
15
|
+
* - Does the frontmatter accurately reflect the content?
|
|
16
|
+
*
|
|
17
|
+
* Staleness Verifier: LLM evaluates if a fact is still current and
|
|
18
|
+
* generates research tasks for stale knowledge.
|
|
19
|
+
*
|
|
20
|
+
* Both produce proposals — never direct mutations.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
// ─── Clarity Rewriter ───────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
const CLARITY_SYSTEM = `You are a Memory Quality Inspector for a sovereign AI memory system called Total Recall.
|
|
26
|
+
|
|
27
|
+
You will be given a memory node. Evaluate its quality and suggest improvements.
|
|
28
|
+
|
|
29
|
+
Output valid JSON:
|
|
30
|
+
{
|
|
31
|
+
"needs_rewrite": boolean,
|
|
32
|
+
"quality_score": 1-10,
|
|
33
|
+
"issues": ["string"],
|
|
34
|
+
"rewritten_title": "string (if needs_rewrite)",
|
|
35
|
+
"rewritten_body": "string (if needs_rewrite)"
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
Criteria for needs_rewrite = true:
|
|
39
|
+
- Title is vague (e.g., "Important thing to know") rather than descriptive
|
|
40
|
+
- Body is not actionable — an AI agent couldn't follow it
|
|
41
|
+
- Body is longer than 500 words when it could be concise
|
|
42
|
+
- Frontmatter category seems wrong for the content
|
|
43
|
+
|
|
44
|
+
Output ONLY valid JSON.`;
|
|
45
|
+
|
|
46
|
+
function buildClarityPrompt(node) {
|
|
47
|
+
return [
|
|
48
|
+
`## Memory Node: ${node.slug}`,
|
|
49
|
+
`Title: "${node.title}"`,
|
|
50
|
+
`Category: ${node.category}`,
|
|
51
|
+
`Modality: ${node.modality}`,
|
|
52
|
+
`Tags: ${(node.tags || []).join(', ')}`,
|
|
53
|
+
'',
|
|
54
|
+
'## Body',
|
|
55
|
+
(node.body || '').slice(0, 1500),
|
|
56
|
+
'',
|
|
57
|
+
'Evaluate this node for clarity and actionability.',
|
|
58
|
+
].join('\n');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Review a single node for clarity. Returns a rewrite proposal if needed.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} slug Node slug to review
|
|
65
|
+
* @param {object} opts
|
|
66
|
+
* @param {string} opts.vaultDir
|
|
67
|
+
* @param {string} opts.inboxDir
|
|
68
|
+
* @param {object} opts.runtimeConfig
|
|
69
|
+
* @returns {{ rewrote: boolean, qualityScore?: number, error?: string }}
|
|
70
|
+
*/
|
|
71
|
+
export async function runClarityReview(slug, { vaultDir, inboxDir, runtimeConfig }) {
|
|
72
|
+
const allNodes = loadNodes(vaultDir);
|
|
73
|
+
const node = allNodes.find(n => n.slug === slug);
|
|
74
|
+
|
|
75
|
+
if (!node) {
|
|
76
|
+
return { rewrote: false, error: 'Node not found' };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const prompt = buildClarityPrompt(node);
|
|
80
|
+
|
|
81
|
+
let result;
|
|
82
|
+
try {
|
|
83
|
+
const rawResponse = await callLocalRuntime(prompt, CLARITY_SYSTEM, runtimeConfig);
|
|
84
|
+
const jsonMatch = rawResponse.match(/\{[\s\S]*\}/);
|
|
85
|
+
if (!jsonMatch) throw new Error('No JSON in response');
|
|
86
|
+
result = cleanAndParseJSON(jsonMatch[0]);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
logger.info({
|
|
89
|
+
subsystem: 'clarity-rewriter',
|
|
90
|
+
message: `Clarity review failed for ${slug}: ${err.message}`,
|
|
91
|
+
});
|
|
92
|
+
return { rewrote: false, error: err.message };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!result.needs_rewrite) {
|
|
96
|
+
logger.info({
|
|
97
|
+
subsystem: 'clarity-rewriter',
|
|
98
|
+
message: `${slug}: quality score ${result.quality_score}/10 — no rewrite needed`,
|
|
99
|
+
});
|
|
100
|
+
return { rewrote: false, qualityScore: result.quality_score };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Write a rewrite proposal to the inbox
|
|
104
|
+
if (!fs.existsSync(inboxDir)) {
|
|
105
|
+
fs.mkdirSync(inboxDir, { recursive: true });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const proposalSlug = `clarity-rewrite-${slug}-${crypto.randomBytes(3).toString('hex')}`;
|
|
109
|
+
const now = new Date().toISOString();
|
|
110
|
+
|
|
111
|
+
const proposalFrontmatter = {
|
|
112
|
+
type: 'proposal',
|
|
113
|
+
slug: proposalSlug,
|
|
114
|
+
category: 'memory-cleanup',
|
|
115
|
+
status: 'draft',
|
|
116
|
+
proposed_by: 'clarity-rewriter',
|
|
117
|
+
proposed_at: now,
|
|
118
|
+
target_path: slug,
|
|
119
|
+
summary: `Rewrite "${node.title}" for clarity (quality score: ${result.quality_score}/10)`,
|
|
120
|
+
rationale: (result.issues || []).join('; '),
|
|
121
|
+
x_rewritten_title: result.rewritten_title || node.title,
|
|
122
|
+
x_quality_score: result.quality_score,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const proposalBody = result.rewritten_body
|
|
126
|
+
? `## Proposed Rewrite\n\n${result.rewritten_body}`
|
|
127
|
+
: '## No rewrite body provided';
|
|
128
|
+
|
|
129
|
+
atomicWrite(
|
|
130
|
+
path.join(inboxDir, `${proposalSlug}.md`),
|
|
131
|
+
safeStringify(proposalBody, proposalFrontmatter),
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
logger.info({
|
|
135
|
+
subsystem: 'clarity-rewriter',
|
|
136
|
+
message: `${slug}: rewrite proposal created (quality ${result.quality_score}/10)`,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
return { rewrote: true, qualityScore: result.quality_score };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ─── Staleness Verifier ─────────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
const STALENESS_SYSTEM = `You are a Knowledge Currency Analyst for a sovereign AI memory system.
|
|
145
|
+
|
|
146
|
+
Given a memory node with its age and domain, assess whether the information is still current.
|
|
147
|
+
|
|
148
|
+
Output valid JSON:
|
|
149
|
+
{
|
|
150
|
+
"verdict": "STILL_VALID" | "POSSIBLY_STALE" | "LIKELY_OUTDATED",
|
|
151
|
+
"confidence": 0.0-1.0,
|
|
152
|
+
"reasoning": "string",
|
|
153
|
+
"verification_query": "string (search query to verify if possibly stale/outdated)"
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
Consider:
|
|
157
|
+
- Technical facts (API endpoints, library versions) age faster than principles
|
|
158
|
+
- Programming language features change every few years
|
|
159
|
+
- Best practices evolve; fundamental concepts are stable
|
|
160
|
+
- If the node is about a technology released after 2023, be more conservative
|
|
161
|
+
|
|
162
|
+
Output ONLY valid JSON.`;
|
|
163
|
+
|
|
164
|
+
function buildStalenessPrompt(node) {
|
|
165
|
+
const age = node.updated
|
|
166
|
+
? Math.floor((Date.now() - new Date(node.updated).getTime()) / (1000 * 60 * 60 * 24))
|
|
167
|
+
: 'unknown';
|
|
168
|
+
|
|
169
|
+
return [
|
|
170
|
+
`## Memory Node: ${node.slug}`,
|
|
171
|
+
`Title: "${node.title}"`,
|
|
172
|
+
`Category: ${node.category}`,
|
|
173
|
+
`Tags: ${(node.tags || []).join(', ')}`,
|
|
174
|
+
`Age: ${age} days since last update`,
|
|
175
|
+
`Confidence: ${node.confidence}`,
|
|
176
|
+
'',
|
|
177
|
+
'## Content',
|
|
178
|
+
(node.body || '').slice(0, 1000),
|
|
179
|
+
'',
|
|
180
|
+
'Is this information likely still current?',
|
|
181
|
+
].join('\n');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Check if a node's knowledge is still current.
|
|
186
|
+
*
|
|
187
|
+
* @param {string} slug Node slug to check
|
|
188
|
+
* @param {object} opts
|
|
189
|
+
* @param {string} opts.vaultDir
|
|
190
|
+
* @param {string} opts.queueDir Path to scheduler queue
|
|
191
|
+
* @param {object} opts.runtimeConfig
|
|
192
|
+
* @returns {{ verdict: string, confidence: number, error?: string }}
|
|
193
|
+
*/
|
|
194
|
+
export async function runStalenessCheck(slug, { vaultDir, queueDir, runtimeConfig }) {
|
|
195
|
+
const allNodes = loadNodes(vaultDir);
|
|
196
|
+
const node = allNodes.find(n => n.slug === slug);
|
|
197
|
+
|
|
198
|
+
if (!node) {
|
|
199
|
+
return { verdict: 'STILL_VALID', error: 'Node not found' };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const prompt = buildStalenessPrompt(node);
|
|
203
|
+
|
|
204
|
+
let result;
|
|
205
|
+
try {
|
|
206
|
+
const rawResponse = await callLocalRuntime(prompt, STALENESS_SYSTEM, runtimeConfig);
|
|
207
|
+
const jsonMatch = rawResponse.match(/\{[\s\S]*\}/);
|
|
208
|
+
if (!jsonMatch) throw new Error('No JSON in response');
|
|
209
|
+
result = cleanAndParseJSON(jsonMatch[0]);
|
|
210
|
+
} catch (err) {
|
|
211
|
+
logger.info({
|
|
212
|
+
subsystem: 'staleness-verifier',
|
|
213
|
+
message: `Staleness check failed for ${slug}: ${err.message}`,
|
|
214
|
+
});
|
|
215
|
+
return { verdict: 'STILL_VALID', error: err.message };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const verdict = result.verdict || 'STILL_VALID';
|
|
219
|
+
|
|
220
|
+
logger.info({
|
|
221
|
+
subsystem: 'staleness-verifier',
|
|
222
|
+
message: `${slug}: ${verdict} (confidence ${result.confidence})`,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// If stale, generate a research refresh task
|
|
226
|
+
if ((verdict === 'POSSIBLY_STALE' || verdict === 'LIKELY_OUTDATED') && result.verification_query) {
|
|
227
|
+
if (!fs.existsSync(queueDir)) {
|
|
228
|
+
fs.mkdirSync(queueDir, { recursive: true });
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const taskSlug = `refresh-${slug}-${crypto.randomBytes(3).toString('hex')}`;
|
|
232
|
+
const priority = verdict === 'LIKELY_OUTDATED' ? 70 : 40;
|
|
233
|
+
|
|
234
|
+
const taskData = {
|
|
235
|
+
type: 'task',
|
|
236
|
+
slug: taskSlug,
|
|
237
|
+
priority,
|
|
238
|
+
category: 'research-acquisition',
|
|
239
|
+
target: slug,
|
|
240
|
+
status: 'pending',
|
|
241
|
+
created_by: 'staleness-verifier',
|
|
242
|
+
reason: `Node "${node.title}" is ${verdict.replace('_', ' ').toLowerCase()}. Verification needed.`,
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const taskBody = [
|
|
246
|
+
`## Objective`,
|
|
247
|
+
`Verify and refresh the knowledge in node "${slug}".`,
|
|
248
|
+
'',
|
|
249
|
+
`## Verification Query`,
|
|
250
|
+
result.verification_query,
|
|
251
|
+
'',
|
|
252
|
+
`## Staleness Reasoning`,
|
|
253
|
+
result.reasoning || 'No reasoning provided.',
|
|
254
|
+
].join('\n');
|
|
255
|
+
|
|
256
|
+
atomicWrite(
|
|
257
|
+
path.join(queueDir, `${taskSlug}.md`),
|
|
258
|
+
safeStringify(taskBody, taskData),
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
logger.info({
|
|
262
|
+
subsystem: 'staleness-verifier',
|
|
263
|
+
message: `Refresh task queued for "${slug}" with query: ${result.verification_query}`,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return { verdict, confidence: result.confidence || 0.5 };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ─── Fact Seeker ─────────────────────────────────────────────────────────────────
|
|
271
|
+
|
|
272
|
+
const FACT_SEEKER_SYSTEM = `You are a Knowledge Gap Analyst for a sovereign AI memory system called Total Recall.
|
|
273
|
+
|
|
274
|
+
Given a summary of what the vault currently knows, identify knowledge gaps that would make the system more useful.
|
|
275
|
+
|
|
276
|
+
Output valid JSON:
|
|
277
|
+
{
|
|
278
|
+
"gaps": [
|
|
279
|
+
{
|
|
280
|
+
"topic": "string",
|
|
281
|
+
"question": "What specific fact or knowledge is missing?",
|
|
282
|
+
"search_query": "string (web search query to fill the gap)",
|
|
283
|
+
"priority": 1-5,
|
|
284
|
+
"reasoning": "string"
|
|
285
|
+
}
|
|
286
|
+
]
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
Focus on:
|
|
290
|
+
- Technical facts mentioned but never fully documented
|
|
291
|
+
- Dependencies/tools referenced without version information
|
|
292
|
+
- Patterns that lack concrete examples
|
|
293
|
+
- Skills that reference external APIs without documenting their behavior
|
|
294
|
+
|
|
295
|
+
Output ONLY valid JSON. Maximum 5 gaps.`;
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Scan the vault for knowledge gaps and generate research tasks.
|
|
299
|
+
*
|
|
300
|
+
* @param {object} opts
|
|
301
|
+
* @param {string} opts.vaultDir
|
|
302
|
+
* @param {string} opts.queueDir
|
|
303
|
+
* @param {object} opts.runtimeConfig
|
|
304
|
+
* @returns {{ gaps: object[], error?: string }}
|
|
305
|
+
*/
|
|
306
|
+
export async function runFactSeeker({ vaultDir, queueDir, runtimeConfig }) {
|
|
307
|
+
const allNodes = loadNodes(vaultDir);
|
|
308
|
+
const activeNodes = allNodes.filter(n => n.status === 'active');
|
|
309
|
+
|
|
310
|
+
if (activeNodes.length === 0) {
|
|
311
|
+
return { gaps: [], skipped: 'empty-vault' };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Build a compact vault summary
|
|
315
|
+
const vaultSummary = activeNodes.map(n => [
|
|
316
|
+
`- [${n.slug}] ${n.title}`,
|
|
317
|
+
` Category: ${n.category} | Tags: ${(n.tags || []).join(', ')} | Confidence: ${n.confidence}`,
|
|
318
|
+
].join('\n')).join('\n');
|
|
319
|
+
|
|
320
|
+
const prompt = `## Current Vault Contents\n${vaultSummary}\n\nIdentify the most important knowledge gaps that would make this vault more useful.`;
|
|
321
|
+
|
|
322
|
+
let result;
|
|
323
|
+
try {
|
|
324
|
+
const rawResponse = await callLocalRuntime(prompt, FACT_SEEKER_SYSTEM, runtimeConfig);
|
|
325
|
+
const jsonMatch = rawResponse.match(/\{[\s\S]*\}/);
|
|
326
|
+
if (!jsonMatch) throw new Error('No JSON in response');
|
|
327
|
+
result = cleanAndParseJSON(jsonMatch[0]);
|
|
328
|
+
} catch (err) {
|
|
329
|
+
logger.info({
|
|
330
|
+
subsystem: 'fact-seeker',
|
|
331
|
+
message: `Fact seeker failed: ${err.message}`,
|
|
332
|
+
});
|
|
333
|
+
return { gaps: [], error: err.message };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const gaps = Array.isArray(result.gaps) ? result.gaps : [];
|
|
337
|
+
|
|
338
|
+
// Generate research tasks for high-priority gaps
|
|
339
|
+
if (!fs.existsSync(queueDir)) {
|
|
340
|
+
fs.mkdirSync(queueDir, { recursive: true });
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
for (const gap of gaps.filter(g => g.priority >= 3)) {
|
|
344
|
+
const taskSlug = `fact-seeker-${crypto.randomBytes(4).toString('hex')}`;
|
|
345
|
+
|
|
346
|
+
const taskData = {
|
|
347
|
+
type: 'task',
|
|
348
|
+
slug: taskSlug,
|
|
349
|
+
priority: gap.priority * 15, // 45-75 range
|
|
350
|
+
category: 'proactive-research',
|
|
351
|
+
status: 'pending',
|
|
352
|
+
created_by: 'fact-seeker',
|
|
353
|
+
reason: `Knowledge gap identified: ${gap.question}`,
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
const taskBody = [
|
|
357
|
+
`## Knowledge Gap`,
|
|
358
|
+
`**Topic**: ${gap.topic}`,
|
|
359
|
+
`**Question**: ${gap.question}`,
|
|
360
|
+
'',
|
|
361
|
+
`## Search Query`,
|
|
362
|
+
gap.search_query,
|
|
363
|
+
'',
|
|
364
|
+
`## Reasoning`,
|
|
365
|
+
gap.reasoning || '',
|
|
366
|
+
].join('\n');
|
|
367
|
+
|
|
368
|
+
atomicWrite(
|
|
369
|
+
path.join(queueDir, `${taskSlug}.md`),
|
|
370
|
+
safeStringify(taskBody, taskData),
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
logger.info({
|
|
375
|
+
subsystem: 'fact-seeker',
|
|
376
|
+
message: `Found ${gaps.length} knowledge gaps, queued ${gaps.filter(g => g.priority >= 3).length} research tasks`,
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
return { gaps };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// ─── Cutoff Assumption Auditor ───────────────────────────────────────────────────
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Domains where LLM knowledge decays fastest relative to the training cutoff.
|
|
386
|
+
* Nodes touching these domains are automatically flagged for verification.
|
|
387
|
+
*/
|
|
388
|
+
const HIGH_DRIFT_DOMAINS = [
|
|
389
|
+
'api', 'sdk', 'endpoint', 'version', 'release', 'changelog', 'deprecat',
|
|
390
|
+
'npm', 'package', 'library', 'framework', 'model', 'pricing', 'rate limit',
|
|
391
|
+
'token limit', 'context window', 'benchmark', 'leaderboard', 'sota',
|
|
392
|
+
'openai', 'anthropic', 'google', 'gemini', 'claude', 'gpt', 'llama',
|
|
393
|
+
'ollama', 'hugging face', 'github', 'docker', 'kubernetes', 'terraform',
|
|
394
|
+
'aws', 'gcp', 'azure', 'cloudflare', 'vercel', 'next.js', 'react', 'vite',
|
|
395
|
+
];
|
|
396
|
+
|
|
397
|
+
const CUTOFF_AUDIT_SYSTEM = `You are a Knowledge Cutoff Auditor for a sovereign AI memory system.
|
|
398
|
+
|
|
399
|
+
Your job is to identify claims in a memory node that are almost certainly based on the LLM's TRAINING DATA ONLY (intrinsic knowledge) and have a HIGH PROBABILITY of being outdated since the model's knowledge cutoff.
|
|
400
|
+
|
|
401
|
+
The current real-world date is: ${new Date().toISOString().slice(0, 10)}.
|
|
402
|
+
|
|
403
|
+
Output valid JSON:
|
|
404
|
+
{
|
|
405
|
+
"has_cutoff_risk": boolean,
|
|
406
|
+
"risk_level": "low" | "medium" | "high" | "critical",
|
|
407
|
+
"intrinsic_claims": [
|
|
408
|
+
{
|
|
409
|
+
"claim": "exact quote or paraphrase of the risky claim",
|
|
410
|
+
"domain": "what domain this is in (e.g. npm package version, API endpoint, model pricing)",
|
|
411
|
+
"likely_changed_because": "why this probably changed",
|
|
412
|
+
"verification_query": "specific web search query to verify current truth"
|
|
413
|
+
}
|
|
414
|
+
],
|
|
415
|
+
"cutoff_assumption_summary": "string — what the node ASSUMES to be true that may have changed"
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
Rules:
|
|
419
|
+
- ONLY flag claims that are time-sensitive (versions, prices, endpoints, rankings, availability)
|
|
420
|
+
- Do NOT flag fundamental concepts, algorithms, or stable architectural principles
|
|
421
|
+
- "critical" = something that would cause immediate incorrect behavior if wrong (wrong API endpoint, deprecated method)
|
|
422
|
+
- "high" = likely wrong for anyone running this system today (version numbers, feature availability)
|
|
423
|
+
- Output ONLY valid JSON.`;
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Determine if a node touches high-drift domains (fast heuristic pre-filter).
|
|
427
|
+
*/
|
|
428
|
+
function isHighDriftNode(node) {
|
|
429
|
+
const text = `${node.title} ${node.body || ''} ${(node.tags || []).join(' ')}`.toLowerCase();
|
|
430
|
+
return HIGH_DRIFT_DOMAINS.some(domain => text.includes(domain));
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Scan the vault for nodes containing LLM intrinsic assumptions that may have
|
|
435
|
+
* drifted past the model's knowledge cutoff. Queues verification tasks for each.
|
|
436
|
+
*
|
|
437
|
+
* A node is a candidate if:
|
|
438
|
+
* 1. It has no external source citation (source.type !== 'knowledge-acquisition' or 'web-search')
|
|
439
|
+
* 2. It touches a high-drift domain (API, version, framework, pricing, etc.)
|
|
440
|
+
* 3. It hasn't been verified in the last 30 days
|
|
441
|
+
*
|
|
442
|
+
* @param {object} opts
|
|
443
|
+
* @param {string} opts.vaultDir
|
|
444
|
+
* @param {string} opts.queueDir
|
|
445
|
+
* @param {object} opts.runtimeConfig
|
|
446
|
+
* @returns {{ audited: number, flagged: number, critical: number }}
|
|
447
|
+
*/
|
|
448
|
+
export async function runCutoffAudit({ vaultDir, queueDir, runtimeConfig }) {
|
|
449
|
+
const allNodes = loadNodes(vaultDir);
|
|
450
|
+
const now = Date.now();
|
|
451
|
+
const verifiedCutoff = 30 * 24 * 60 * 60 * 1000; // 30 days
|
|
452
|
+
|
|
453
|
+
// Candidate selection: unsourced + high-drift domain + not recently verified
|
|
454
|
+
const candidates = allNodes.filter(n => {
|
|
455
|
+
if (n.status !== 'active') return false;
|
|
456
|
+
const sourceType = n.source?.type || '';
|
|
457
|
+
const isExternallySourced = ['knowledge-acquisition', 'web-search', 'web-fetch', 'brave-search', 'arxiv'].includes(sourceType);
|
|
458
|
+
if (isExternallySourced) return false; // Already from a real source
|
|
459
|
+
if (!isHighDriftNode(n)) return false;
|
|
460
|
+
const lastVerified = new Date(n.x_cutoff_verified_at || n.updated || n.created || 0).getTime();
|
|
461
|
+
return (now - lastVerified) > verifiedCutoff;
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
if (candidates.length === 0) {
|
|
465
|
+
logger.info({ subsystem: 'cutoff-auditor', message: 'No cutoff-risk candidates found' });
|
|
466
|
+
return { audited: 0, flagged: 0, critical: 0 };
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Process up to 8 candidates per run
|
|
470
|
+
const batch = candidates.slice(0, 8);
|
|
471
|
+
let flagged = 0;
|
|
472
|
+
let critical = 0;
|
|
473
|
+
|
|
474
|
+
if (!fs.existsSync(queueDir)) fs.mkdirSync(queueDir, { recursive: true });
|
|
475
|
+
|
|
476
|
+
for (const node of batch) {
|
|
477
|
+
const prompt = [
|
|
478
|
+
`## Memory Node: ${node.slug}`,
|
|
479
|
+
`Title: "${node.title}"`,
|
|
480
|
+
`Source type: ${node.source?.type || 'intrinsic-llm-knowledge'}`,
|
|
481
|
+
`Last updated: ${node.updated || node.created || 'unknown'}`,
|
|
482
|
+
`Tags: ${(node.tags || []).join(', ')}`,
|
|
483
|
+
'',
|
|
484
|
+
'## Content',
|
|
485
|
+
(node.body || '').slice(0, 1200),
|
|
486
|
+
'',
|
|
487
|
+
'Audit this node for claims that may have drifted past the LLM knowledge cutoff.',
|
|
488
|
+
].join('\n');
|
|
489
|
+
|
|
490
|
+
let result;
|
|
491
|
+
try {
|
|
492
|
+
const raw = await callLocalRuntime(prompt, CUTOFF_AUDIT_SYSTEM, runtimeConfig);
|
|
493
|
+
const match = raw.match(/\{[\s\S]*\}/);
|
|
494
|
+
if (!match) continue;
|
|
495
|
+
result = cleanAndParseJSON(match[0]);
|
|
496
|
+
} catch (err) {
|
|
497
|
+
logger.info({ subsystem: 'cutoff-auditor', message: `Audit failed for ${node.slug}: ${err.message}` });
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (!result.has_cutoff_risk) {
|
|
502
|
+
// Mark as verified so we don't re-audit for 30 days
|
|
503
|
+
await stampVerified(node);
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
flagged++;
|
|
508
|
+
const isCritical = result.risk_level === 'critical';
|
|
509
|
+
if (isCritical) critical++;
|
|
510
|
+
|
|
511
|
+
const taskPriority = isCritical ? 95 : result.risk_level === 'high' ? 75 : 50;
|
|
512
|
+
const intrinsicClaims = Array.isArray(result.intrinsic_claims) ? result.intrinsic_claims : [];
|
|
513
|
+
|
|
514
|
+
logger.info({
|
|
515
|
+
subsystem: 'cutoff-auditor',
|
|
516
|
+
message: `CUTOFF RISK [${result.risk_level}] "${node.title}": ${intrinsicClaims.length} claims to verify`,
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
// Queue a verification task for each claim
|
|
520
|
+
for (const claim of intrinsicClaims.slice(0, 3)) {
|
|
521
|
+
const taskSlug = `cutoff-verify-${node.slug}-${crypto.randomBytes(3).toString('hex')}`;
|
|
522
|
+
|
|
523
|
+
const taskData = {
|
|
524
|
+
type: 'task',
|
|
525
|
+
slug: taskSlug,
|
|
526
|
+
priority: taskPriority,
|
|
527
|
+
category: 'proactive-research',
|
|
528
|
+
target: node.slug,
|
|
529
|
+
status: 'pending',
|
|
530
|
+
created_by: 'cutoff-auditor',
|
|
531
|
+
x_risk_level: result.risk_level,
|
|
532
|
+
x_original_claim: claim.claim,
|
|
533
|
+
reason: `Cutoff drift risk [${result.risk_level}]: "${claim.claim}" may have changed since training cutoff.`,
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
const taskBody = [
|
|
537
|
+
`## Cutoff Assumption Verification`,
|
|
538
|
+
`**Node**: ${node.slug} — "${node.title}"`,
|
|
539
|
+
`**Risk Level**: ${result.risk_level}`,
|
|
540
|
+
'',
|
|
541
|
+
`## Claim to Verify`,
|
|
542
|
+
`> ${claim.claim}`,
|
|
543
|
+
'',
|
|
544
|
+
`**Domain**: ${claim.domain}`,
|
|
545
|
+
`**Why it likely changed**: ${claim.likely_changed_because}`,
|
|
546
|
+
'',
|
|
547
|
+
`## Verification Query`,
|
|
548
|
+
claim.verification_query,
|
|
549
|
+
'',
|
|
550
|
+
`## Overall Assumption`,
|
|
551
|
+
result.cutoff_assumption_summary || '',
|
|
552
|
+
].join('\n');
|
|
553
|
+
|
|
554
|
+
atomicWrite(path.join(queueDir, `${taskSlug}.md`), safeStringify(taskBody, taskData));
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Stamp the node with an audit timestamp and risk level
|
|
558
|
+
await stampCutoffRisk(node, result.risk_level, intrinsicClaims.length);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
logger.info({
|
|
562
|
+
subsystem: 'cutoff-auditor',
|
|
563
|
+
message: `Cutoff audit: ${batch.length} audited, ${flagged} flagged, ${critical} critical`,
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
return { audited: batch.length, flagged, critical };
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
async function stampVerified(node) {
|
|
570
|
+
if (!node._filepath) return;
|
|
571
|
+
try {
|
|
572
|
+
const raw = fs.readFileSync(node._filepath, 'utf8');
|
|
573
|
+
const { data, content } = matter(raw);
|
|
574
|
+
data.x_cutoff_verified_at = new Date().toISOString();
|
|
575
|
+
data.x_cutoff_risk = 'none';
|
|
576
|
+
atomicWrite(node._filepath, safeStringify(content, data));
|
|
577
|
+
} catch (err) {
|
|
578
|
+
logger.debug('clarity-rewriter: stampCutoffRiskNone failed', { err: err.message });
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
async function stampCutoffRisk(node, riskLevel, claimCount) {
|
|
583
|
+
if (!node._filepath) return;
|
|
584
|
+
try {
|
|
585
|
+
const raw = fs.readFileSync(node._filepath, 'utf8');
|
|
586
|
+
const { data, content } = matter(raw);
|
|
587
|
+
data.x_cutoff_risk = riskLevel;
|
|
588
|
+
data.x_cutoff_claim_count = claimCount;
|
|
589
|
+
data.x_cutoff_flagged_at = new Date().toISOString();
|
|
590
|
+
// Lower confidence to reflect that claims are unverified
|
|
591
|
+
const confidencePenalty = riskLevel === 'critical' ? 0.3 : riskLevel === 'high' ? 0.2 : 0.1;
|
|
592
|
+
data.confidence = Math.max(0.1, (data.confidence || 0.5) - confidencePenalty);
|
|
593
|
+
atomicWrite(node._filepath, safeStringify(content, data));
|
|
594
|
+
} catch (err) {
|
|
595
|
+
logger.debug('clarity-rewriter: stampCutoffRisk failed', { err: err.message });
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// ─── Correction Writer ───────────────────────────────────────────────────────────
|
|
600
|
+
|
|
601
|
+
const CORRECTION_SYSTEM = `You are a Knowledge Correction Writer for a sovereign AI memory system.
|
|
602
|
+
|
|
603
|
+
You will be given:
|
|
604
|
+
1. An ORIGINAL memory node (what the system previously believed)
|
|
605
|
+
2. RESEARCH EVIDENCE (what was found by real-world sources)
|
|
606
|
+
|
|
607
|
+
Your job is to write an honest, explicit correction record. The system must be able to say: "I previously stated X based on my training data, but after verification I found the actual truth is Y."
|
|
608
|
+
|
|
609
|
+
Output valid JSON:
|
|
610
|
+
{
|
|
611
|
+
"correction_title": "string (concise, e.g. 'Ollama API endpoint changed in v0.2.0')",
|
|
612
|
+
"was_wrong_about": "string (precise description of what was incorrect)",
|
|
613
|
+
"actual_truth": "string (what is actually true, with citation)",
|
|
614
|
+
"correction_body": "string (full markdown explanation including the correction narrative)",
|
|
615
|
+
"confidence_in_correction": 0.0-1.0,
|
|
616
|
+
"original_should_be": "superseded" | "archived" | "deleted",
|
|
617
|
+
"applies_to_date": "string (ISO date — when this correction is accurate as of)"
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
The correction_body MUST include the phrase 'Previous belief:' and 'Verified truth:' as explicit sections.
|
|
621
|
+
Output ONLY valid JSON.`;
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Write an explicit correction record when research contradicts an existing node.
|
|
625
|
+
*
|
|
626
|
+
* Called by the conclusion writer or research engine when a validated fact
|
|
627
|
+
* contradicts an existing active node.
|
|
628
|
+
*
|
|
629
|
+
* @param {string} originalSlug Slug of the node being corrected
|
|
630
|
+
* @param {object} researchEvidence { title, body, sources[] } from fact-seeker
|
|
631
|
+
* @param {object} opts
|
|
632
|
+
* @param {string} opts.vaultDir
|
|
633
|
+
* @param {string} opts.inboxDir
|
|
634
|
+
* @param {object} opts.runtimeConfig
|
|
635
|
+
* @returns {{ correctionSlug: string, verdict: string, error?: string }}
|
|
636
|
+
*/
|
|
637
|
+
export async function writeCorrection(originalSlug, researchEvidence, { vaultDir, inboxDir, runtimeConfig }) {
|
|
638
|
+
const allNodes = loadNodes(vaultDir);
|
|
639
|
+
const original = allNodes.find(n => n.slug === originalSlug);
|
|
640
|
+
|
|
641
|
+
if (!original) {
|
|
642
|
+
return { correctionSlug: null, verdict: 'skip', error: 'Original node not found' };
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const prompt = [
|
|
646
|
+
`## Original Memory Node (What the system believed)`,
|
|
647
|
+
`Slug: ${original.slug}`,
|
|
648
|
+
`Title: "${original.title}"`,
|
|
649
|
+
`Source: ${original.source?.type || 'intrinsic-llm-knowledge'}`,
|
|
650
|
+
`Written: ${original.created || 'unknown'}`,
|
|
651
|
+
'',
|
|
652
|
+
'### Original Content',
|
|
653
|
+
(original.body || '').slice(0, 1000),
|
|
654
|
+
'',
|
|
655
|
+
`## Research Evidence (What was actually found)`,
|
|
656
|
+
`Title: "${researchEvidence.title}"`,
|
|
657
|
+
'',
|
|
658
|
+
'### Evidence Content',
|
|
659
|
+
(researchEvidence.body || '').slice(0, 1500),
|
|
660
|
+
'',
|
|
661
|
+
'### Sources Cited',
|
|
662
|
+
(researchEvidence.sources || []).map(s => `- [${s.source}] ${s.title}: ${s.url}`).join('\n'),
|
|
663
|
+
'',
|
|
664
|
+
'Write an explicit correction record for this node.',
|
|
665
|
+
].join('\n');
|
|
666
|
+
|
|
667
|
+
let result;
|
|
668
|
+
try {
|
|
669
|
+
const raw = await callLocalRuntime(prompt, CORRECTION_SYSTEM, runtimeConfig);
|
|
670
|
+
const match = raw.match(/\{[\s\S]*\}/);
|
|
671
|
+
if (!match) throw new Error('No JSON in response');
|
|
672
|
+
result = cleanAndParseJSON(match[0]);
|
|
673
|
+
} catch (err) {
|
|
674
|
+
logger.info({ subsystem: 'correction-writer', message: `Correction write failed for ${originalSlug}: ${err.message}` });
|
|
675
|
+
return { correctionSlug: null, verdict: 'error', error: err.message };
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Write the correction as a new draft node
|
|
679
|
+
const correctionSlug = `correction-${originalSlug}-${crypto.randomBytes(4).toString('hex')}`;
|
|
680
|
+
const now = new Date().toISOString();
|
|
681
|
+
|
|
682
|
+
const correctionFrontmatter = {
|
|
683
|
+
type: 'memory',
|
|
684
|
+
slug: correctionSlug,
|
|
685
|
+
category: 'corrections',
|
|
686
|
+
title: result.correction_title || `Correction: ${original.title}`,
|
|
687
|
+
status: 'draft',
|
|
688
|
+
confidence: result.confidence_in_correction || 0.8,
|
|
689
|
+
importance: Math.min(5, (original.importance || 3) + 1),
|
|
690
|
+
created: now,
|
|
691
|
+
updated: now,
|
|
692
|
+
last_accessed: now,
|
|
693
|
+
source: {
|
|
694
|
+
type: 'correction',
|
|
695
|
+
session_id: `correction-${Date.now().toString(36)}`,
|
|
696
|
+
agent: 'correction-writer',
|
|
697
|
+
evidence_count: (researchEvidence.sources || []).length,
|
|
698
|
+
corrects_node: originalSlug,
|
|
699
|
+
},
|
|
700
|
+
supersedes: [originalSlug],
|
|
701
|
+
superseded_by: null,
|
|
702
|
+
contradicts: [originalSlug],
|
|
703
|
+
tags: ['correction', 'verified', 'auto-corrected', ...(original.tags || [])],
|
|
704
|
+
related: [originalSlug],
|
|
705
|
+
routes_to_skills: original.routes_to_skills || [],
|
|
706
|
+
sentiment_polarity: 'corrective',
|
|
707
|
+
sentiment_target: 'accuracy',
|
|
708
|
+
modality: 'must',
|
|
709
|
+
subject: 'agent',
|
|
710
|
+
predicate: 'correct',
|
|
711
|
+
object: original.title,
|
|
712
|
+
decay: { half_life_days: 365, access_count: 1 },
|
|
713
|
+
schema_version: 2,
|
|
714
|
+
x_memory_layer: original.x_memory_layer || 'conscious',
|
|
715
|
+
x_corrects: originalSlug,
|
|
716
|
+
x_was_wrong_about: result.was_wrong_about,
|
|
717
|
+
x_applies_to_date: result.applies_to_date || now.slice(0, 10),
|
|
718
|
+
x_original_should_be: result.original_should_be || 'superseded',
|
|
719
|
+
x_temporal_context: now,
|
|
720
|
+
x_citations: (researchEvidence.sources || []).map(s => ({
|
|
721
|
+
url: s.url || '',
|
|
722
|
+
source: s.source || 'web',
|
|
723
|
+
title: s.title || 'Untitled Source',
|
|
724
|
+
published: s.published || now,
|
|
725
|
+
relevance: 1.0,
|
|
726
|
+
accessed: now
|
|
727
|
+
})),
|
|
728
|
+
};
|
|
729
|
+
|
|
730
|
+
const body = result.correction_body || [
|
|
731
|
+
`## Previous Belief`,
|
|
732
|
+
result.was_wrong_about || `The system previously believed: "${original.title}"`,
|
|
733
|
+
'',
|
|
734
|
+
`## Verified Truth`,
|
|
735
|
+
result.actual_truth || 'See research evidence.',
|
|
736
|
+
'',
|
|
737
|
+
`## Sources`,
|
|
738
|
+
(researchEvidence.sources || []).map(s => `- [${s.source}] ${s.title}: ${s.url}`).join('\n'),
|
|
739
|
+
].join('\n');
|
|
740
|
+
|
|
741
|
+
if (!fs.existsSync(inboxDir)) fs.mkdirSync(inboxDir, { recursive: true });
|
|
742
|
+
atomicWrite(path.join(inboxDir, `${correctionSlug}.md`), safeStringify(body, correctionFrontmatter));
|
|
743
|
+
|
|
744
|
+
// Stamp the original node as pending-correction
|
|
745
|
+
await stampNodeForCorrection(original, correctionSlug, result.original_should_be);
|
|
746
|
+
|
|
747
|
+
logger.info({
|
|
748
|
+
subsystem: 'correction-writer',
|
|
749
|
+
message: `Correction written: "${result.correction_title}" (supersedes ${originalSlug})`,
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
return { correctionSlug, verdict: result.original_should_be || 'superseded' };
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
async function stampNodeForCorrection(node, correctionSlug, disposition) {
|
|
756
|
+
if (!node._filepath) return;
|
|
757
|
+
try {
|
|
758
|
+
const raw = fs.readFileSync(node._filepath, 'utf8');
|
|
759
|
+
const { data, content } = matter(raw);
|
|
760
|
+
data.status = disposition === 'deleted' ? 'archived' : 'superseded';
|
|
761
|
+
data.superseded_by = correctionSlug;
|
|
762
|
+
data.x_correction_applied_at = new Date().toISOString();
|
|
763
|
+
// Lower confidence significantly — this fact has been proven wrong
|
|
764
|
+
data.confidence = Math.max(0.05, (data.confidence || 0.5) * 0.3);
|
|
765
|
+
data.updated = new Date().toISOString();
|
|
766
|
+
atomicWrite(node._filepath, safeStringify(content, data));
|
|
767
|
+
} catch (err) {
|
|
768
|
+
logger.debug('clarity-rewriter: stampNodeForCorrection failed', { err: err.message });
|
|
769
|
+
}
|
|
770
|
+
}
|