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,461 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import crypto from 'crypto';
|
|
4
|
+
import matter from 'gray-matter';
|
|
5
|
+
import { callLocalRuntime } from './runtime.mjs';
|
|
6
|
+
import { atomicWrite, safeStringify } from './vault.mjs';
|
|
7
|
+
import { logger } from './logger.mjs';
|
|
8
|
+
import {
|
|
9
|
+
loadResearchConfig,
|
|
10
|
+
braveSearch,
|
|
11
|
+
serperSearch,
|
|
12
|
+
webSearch,
|
|
13
|
+
arxivSearch,
|
|
14
|
+
npmSearch,
|
|
15
|
+
githubSearch,
|
|
16
|
+
wikipediaFetch,
|
|
17
|
+
smartFetch,
|
|
18
|
+
checkSourceAvailability,
|
|
19
|
+
} from './source-adapters.mjs';
|
|
20
|
+
import { addToAgenda, runKnowledgeAcquisitionCycle, getLocalizedDateTime } from './fact-seeker.mjs';
|
|
21
|
+
import { brainDir } from './config.mjs';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Total Recall Deep Research Engine
|
|
25
|
+
*
|
|
26
|
+
* Replaces the old placeholder implementation with real multi-source fetching.
|
|
27
|
+
*
|
|
28
|
+
* Two modes:
|
|
29
|
+
* handleProactiveResearch() — frontier-model orchestrated deep research (structured task)
|
|
30
|
+
* handleQuickResearch() — local-LLM + direct source fetch (fast, no frontier required)
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
const BRAIN_DIR = brainDir;
|
|
34
|
+
|
|
35
|
+
// ─── Frontier-Orchestrated Deep Research ────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Multi-phase deep research using the frontier model for planning + synthesis,
|
|
39
|
+
* and real source adapters for data gathering.
|
|
40
|
+
*
|
|
41
|
+
* @param {object} task - { target, body }
|
|
42
|
+
* @param {object} context - { runtimeConfig }
|
|
43
|
+
*/
|
|
44
|
+
export async function handleProactiveResearch(task, context = {}) {
|
|
45
|
+
const inboxDir = path.join(BRAIN_DIR, 'memory-inbox', 'pending');
|
|
46
|
+
if (!fs.existsSync(inboxDir)) fs.mkdirSync(inboxDir, { recursive: true });
|
|
47
|
+
|
|
48
|
+
const researchConfig = loadResearchConfig();
|
|
49
|
+
const availability = checkSourceAvailability(researchConfig);
|
|
50
|
+
|
|
51
|
+
logger.info({
|
|
52
|
+
subsystem: 'deep-research',
|
|
53
|
+
message: `Starting deep research: "${task.target}" | sources: ${availability.available.join(', ')}`,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Phase 1: Plan — use frontier if available, else use local LLM
|
|
57
|
+
const queries = await planResearchQueries(task, context);
|
|
58
|
+
logger.info({ subsystem: 'deep-research', message: `Phase 1 complete: ${queries.length} queries` });
|
|
59
|
+
|
|
60
|
+
// Phase 2: Parallel fetch from all real sources
|
|
61
|
+
const allResults = await Promise.all(
|
|
62
|
+
queries.map(async (query, i) => {
|
|
63
|
+
logger.info({ subsystem: 'deep-research', message: ` → Agent ${i + 1}: "${query}"` });
|
|
64
|
+
return gatherForQuery(query, researchConfig, availability);
|
|
65
|
+
}),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const flatResults = allResults.flat();
|
|
69
|
+
logger.info({
|
|
70
|
+
subsystem: 'deep-research',
|
|
71
|
+
message: `Phase 2 complete: ${flatResults.length} total results from ${[...new Set(flatResults.map(r => r.source))].join(', ')}`,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (flatResults.length === 0) {
|
|
75
|
+
logger.info({ subsystem: 'deep-research', message: 'No results gathered — research aborted' });
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Phase 3: Write and integrate findings into a single consolidated main draft node
|
|
80
|
+
for (const [i, results] of allResults.entries()) {
|
|
81
|
+
if (results.length === 0) continue;
|
|
82
|
+
writeOrUpdateConsolidatedDraft(task.target, queries[i], results, inboxDir);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Phase 4: Synthesize using local runtime (which consists of high-powered CLI agents)
|
|
86
|
+
logger.info({ subsystem: 'deep-research', message: 'Phase 4: Synthesizing research results...' });
|
|
87
|
+
const finalReport = await synthesizeLocally(task, flatResults, context.runtimeConfig);
|
|
88
|
+
|
|
89
|
+
// Save the beautiful synthesized executive summary at the top of our main consolidated document
|
|
90
|
+
if (finalReport) {
|
|
91
|
+
saveSynthesizedReportToDraft(task.target, finalReport, inboxDir);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Phase 5: Also add topic to the Research Agenda for ongoing tracking
|
|
95
|
+
addToAgenda({
|
|
96
|
+
topic: task.target,
|
|
97
|
+
priority: 75,
|
|
98
|
+
source: 'deep-research-task',
|
|
99
|
+
rationale: task.body || '',
|
|
100
|
+
tags: ['deep-research'],
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
return finalReport;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ─── Planning ───────────────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
async function planResearchQueries(task, context) {
|
|
109
|
+
const today = getLocalizedDateTime();
|
|
110
|
+
const cutoff = context.runtimeConfig?.training_cutoff || 'January 2025';
|
|
111
|
+
|
|
112
|
+
const planSystem = `You are a Research Planner. Today's date and time is ${today}. The model's training data cutoff is ${cutoff}.
|
|
113
|
+
Decompose the research objective into 3 distinct, specific search queries. Prioritize queries targeting fresh, timely information that has arisen or been updated since the training cutoff.
|
|
114
|
+
Output ONLY valid JSON: { "queries": ["query 1", "query 2", "query 3"], "source_hints": { "query 1": ["brave-search", "arxiv"] } }`;
|
|
115
|
+
|
|
116
|
+
const planPrompt = `Research Objective: ${task.target}\nDetails: ${task.body || ''}\nGenerate 3 targeted search queries targeting active, timely information post-${cutoff}.`;
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const raw = await callLocalRuntime(planPrompt, planSystem, context.runtimeConfig);
|
|
120
|
+
const match = raw.match(/\{[\s\S]*\}/);
|
|
121
|
+
if (match) {
|
|
122
|
+
const plan = JSON.parse(match[0]);
|
|
123
|
+
if (Array.isArray(plan.queries) && plan.queries.length > 0) return plan.queries;
|
|
124
|
+
}
|
|
125
|
+
} catch (err) {
|
|
126
|
+
logger.debug('research: callLocalRuntime failed during plan generation', { err: err.message });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Final fallback: use the target itself as the query
|
|
130
|
+
return [task.target, `${task.target} tutorial`, `${task.target} best practices`];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ─── Real Source Gathering ───────────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
async function gatherForQuery(query, researchConfig, availability) {
|
|
136
|
+
const results = [];
|
|
137
|
+
const fetches = [];
|
|
138
|
+
|
|
139
|
+
// Always try: Wikipedia + DuckDuckGo
|
|
140
|
+
fetches.push(
|
|
141
|
+
wikipediaFetch(query, researchConfig).then(r => r && results.push(r)).catch(() => {}),
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
// 1. Web search — Brave with Serper fallback
|
|
145
|
+
if (availability.available.includes('brave-search') || availability.available.includes('serper')) {
|
|
146
|
+
fetches.push(
|
|
147
|
+
webSearch(query, researchConfig, 4).then(r => results.push(...r)).catch(err => {
|
|
148
|
+
logger.info({ subsystem: 'deep-research', message: `Web search failed for "${query}": ${err.message}` });
|
|
149
|
+
}),
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// arXiv for academic topics
|
|
154
|
+
if (/\b(model|llm|neural|ml|ai|research|paper|algorithm|training)\b/i.test(query)) {
|
|
155
|
+
fetches.push(
|
|
156
|
+
arxivSearch(query, researchConfig, 2).then(r => results.push(...r)).catch(() => {}),
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// npm for JS/package topics
|
|
161
|
+
if (/\b(npm|node|javascript|typescript|package|library|framework)\b/i.test(query)) {
|
|
162
|
+
fetches.push(
|
|
163
|
+
npmSearch(query, researchConfig, 3).then(r => results.push(...r)).catch(() => {}),
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// GitHub for code/tool topics
|
|
168
|
+
if (/\b(github|repo|open.source|tool|cli|sdk|api|integration)\b/i.test(query)) {
|
|
169
|
+
fetches.push(
|
|
170
|
+
githubSearch(query, researchConfig, 'repositories', 3)
|
|
171
|
+
.then(r => results.push(...r))
|
|
172
|
+
.catch(() => {}),
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
await Promise.all(fetches);
|
|
177
|
+
|
|
178
|
+
// Deep-crawl top web result using smartFetch (Playwright if installed, plain fetch otherwise)
|
|
179
|
+
const topWebResult = results.find(r => ['brave-search', 'serper'].includes(r.source) && r.url);
|
|
180
|
+
if (topWebResult) {
|
|
181
|
+
try {
|
|
182
|
+
const crawled = await smartFetch(topWebResult.url, researchConfig);
|
|
183
|
+
topWebResult.fullText = crawled.fullText?.slice(0, 3000);
|
|
184
|
+
if (crawled.source === 'playwright') {
|
|
185
|
+
topWebResult.source = 'playwright'; // Upgrade source label
|
|
186
|
+
topWebResult.type = 'webpage-rendered';
|
|
187
|
+
}
|
|
188
|
+
} catch (err) {
|
|
189
|
+
logger.debug('research: smartFetch failed during topWebResult crawl', { err: err.message });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return results;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ─── Draft Node Writing ──────────────────────────────────────────────────────────
|
|
197
|
+
|
|
198
|
+
function slugify(text) {
|
|
199
|
+
return text
|
|
200
|
+
.toString()
|
|
201
|
+
.toLowerCase()
|
|
202
|
+
.trim()
|
|
203
|
+
.replace(/\s+/g, '-')
|
|
204
|
+
.replace(/[^\w\-]+/g, '')
|
|
205
|
+
.replace(/\-\-+/g, '-');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function writeOrUpdateConsolidatedDraft(parentTopic, query, results, inboxDir) {
|
|
209
|
+
const slug = `research-report-${slugify(parentTopic)}`;
|
|
210
|
+
const filePath = path.join(inboxDir, `${slug}.md`);
|
|
211
|
+
const now = new Date().toISOString();
|
|
212
|
+
|
|
213
|
+
let existingFrontmatter = null;
|
|
214
|
+
let existingBody = '';
|
|
215
|
+
|
|
216
|
+
if (fs.existsSync(filePath)) {
|
|
217
|
+
try {
|
|
218
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
219
|
+
const { data, content } = matter(raw);
|
|
220
|
+
existingFrontmatter = data;
|
|
221
|
+
existingBody = content.trim();
|
|
222
|
+
} catch {}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const newCitations = results.map(r => ({
|
|
226
|
+
source: r.source || 'web',
|
|
227
|
+
title: r.title || 'Untitled Source',
|
|
228
|
+
url: r.url || '',
|
|
229
|
+
published: r.published || now,
|
|
230
|
+
relevance: 1.0,
|
|
231
|
+
accessed: now
|
|
232
|
+
}));
|
|
233
|
+
|
|
234
|
+
const formatFriendly = (isoString) => {
|
|
235
|
+
try {
|
|
236
|
+
const d = new Date(isoString);
|
|
237
|
+
if (isNaN(d.getTime())) return isoString;
|
|
238
|
+
return d.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
|
|
239
|
+
} catch {
|
|
240
|
+
return isoString;
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const citations = existingFrontmatter?.x_citations
|
|
245
|
+
? [...existingFrontmatter.x_citations, ...newCitations]
|
|
246
|
+
: newCitations;
|
|
247
|
+
|
|
248
|
+
const uniqueCitationsMap = new Map();
|
|
249
|
+
for (const c of citations) {
|
|
250
|
+
uniqueCitationsMap.set(c.url || c.title, c);
|
|
251
|
+
}
|
|
252
|
+
const mergedCitations = Array.from(uniqueCitationsMap.values());
|
|
253
|
+
|
|
254
|
+
const sourcesList = results.map(r => ({ source: r.source, url: r.url, published: r.published }));
|
|
255
|
+
const mergedSources = existingFrontmatter?.x_sources
|
|
256
|
+
? [...existingFrontmatter.x_sources, ...sourcesList]
|
|
257
|
+
: sourcesList;
|
|
258
|
+
|
|
259
|
+
const uniqueSourcesMap = new Map();
|
|
260
|
+
for (const s of mergedSources) {
|
|
261
|
+
uniqueSourcesMap.set(s.url || s.source, s);
|
|
262
|
+
}
|
|
263
|
+
const mergedSourcesUnique = Array.from(uniqueSourcesMap.values());
|
|
264
|
+
|
|
265
|
+
const bodyLines = [];
|
|
266
|
+
if (existingBody) {
|
|
267
|
+
bodyLines.push(existingBody);
|
|
268
|
+
bodyLines.push('');
|
|
269
|
+
bodyLines.push('---');
|
|
270
|
+
bodyLines.push(`### Research Query: "${query}"`);
|
|
271
|
+
bodyLines.push('');
|
|
272
|
+
bodyLines.push('#### Integrated Sources');
|
|
273
|
+
results.forEach(r => {
|
|
274
|
+
bodyLines.push(`- [${r.source}] **${r.title}** — ${r.url}${r.published ? ` (${r.published})` : ''}`);
|
|
275
|
+
if (r.snippet) bodyLines.push(` ${r.snippet.slice(0, 200)}`);
|
|
276
|
+
});
|
|
277
|
+
} else {
|
|
278
|
+
const temporalFriendly = formatFriendly(now);
|
|
279
|
+
bodyLines.push(`> [!NOTE]`);
|
|
280
|
+
bodyLines.push(`> **Temporal Context**: Consolidated research current as of ${temporalFriendly}.`);
|
|
281
|
+
bodyLines.push('');
|
|
282
|
+
bodyLines.push(`# Consolidated Research: ${parentTopic}`);
|
|
283
|
+
bodyLines.push('');
|
|
284
|
+
bodyLines.push(`### Research Query: "${query}"`);
|
|
285
|
+
bodyLines.push('');
|
|
286
|
+
bodyLines.push('#### Integrated Sources');
|
|
287
|
+
results.forEach(r => {
|
|
288
|
+
bodyLines.push(`- [${r.source}] **${r.title}** — ${r.url}${r.published ? ` (${r.published})` : ''}`);
|
|
289
|
+
if (r.snippet) bodyLines.push(` ${r.snippet.slice(0, 200)}`);
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const frontmatter = {
|
|
294
|
+
type: 'memory',
|
|
295
|
+
slug,
|
|
296
|
+
category: 'facts',
|
|
297
|
+
title: `Consolidated Research: ${parentTopic}`,
|
|
298
|
+
status: 'draft',
|
|
299
|
+
confidence: 0.8,
|
|
300
|
+
importance: 4,
|
|
301
|
+
created: existingFrontmatter?.created || now,
|
|
302
|
+
updated: now,
|
|
303
|
+
last_accessed: now,
|
|
304
|
+
source: {
|
|
305
|
+
type: 'knowledge-acquisition',
|
|
306
|
+
session_id: existingFrontmatter?.source?.session_id || `deep-research-${Date.now().toString(36)}`,
|
|
307
|
+
agent: 'deep-research-engine',
|
|
308
|
+
evidence_count: mergedCitations.length,
|
|
309
|
+
parent_topic: parentTopic,
|
|
310
|
+
},
|
|
311
|
+
supersedes: existingFrontmatter?.supersedes || [],
|
|
312
|
+
superseded_by: null,
|
|
313
|
+
contradicts: existingFrontmatter?.contradicts || [],
|
|
314
|
+
tags: ['research', 'deep-research', 'consolidated-report', 'cited'],
|
|
315
|
+
related: existingFrontmatter?.related || [],
|
|
316
|
+
routes_to_skills: [],
|
|
317
|
+
sentiment_polarity: 'descriptive',
|
|
318
|
+
sentiment_target: 'external-knowledge',
|
|
319
|
+
modality: 'should',
|
|
320
|
+
subject: 'agent',
|
|
321
|
+
predicate: 'know',
|
|
322
|
+
object: parentTopic,
|
|
323
|
+
decay: { half_life_days: 60, access_count: 1 },
|
|
324
|
+
schema_version: 2,
|
|
325
|
+
x_memory_layer: 'research',
|
|
326
|
+
x_temporal_context: now,
|
|
327
|
+
x_citations: mergedCitations,
|
|
328
|
+
x_sources: mergedSourcesUnique,
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
atomicWrite(filePath, safeStringify(bodyLines.join('\n'), frontmatter));
|
|
332
|
+
return filePath;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function saveSynthesizedReportToDraft(parentTopic, finalReport, inboxDir) {
|
|
336
|
+
const slug = `research-report-${slugify(parentTopic)}`;
|
|
337
|
+
const filePath = path.join(inboxDir, `${slug}.md`);
|
|
338
|
+
const now = new Date().toISOString();
|
|
339
|
+
|
|
340
|
+
let existingFrontmatter = null;
|
|
341
|
+
let existingBody = '';
|
|
342
|
+
|
|
343
|
+
if (fs.existsSync(filePath)) {
|
|
344
|
+
try {
|
|
345
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
346
|
+
const { data, content } = matter(raw);
|
|
347
|
+
existingFrontmatter = data;
|
|
348
|
+
existingBody = content.trim();
|
|
349
|
+
} catch {}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
let sourcesSection = '';
|
|
353
|
+
if (existingBody) {
|
|
354
|
+
const idx = existingBody.indexOf('### Research Query:');
|
|
355
|
+
if (idx !== -1) {
|
|
356
|
+
sourcesSection = existingBody.slice(idx);
|
|
357
|
+
} else {
|
|
358
|
+
sourcesSection = existingBody;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const formatFriendly = (isoString) => {
|
|
363
|
+
try {
|
|
364
|
+
const d = new Date(isoString);
|
|
365
|
+
if (isNaN(d.getTime())) return isoString;
|
|
366
|
+
return d.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
|
|
367
|
+
} catch {
|
|
368
|
+
return isoString;
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
const temporalFriendly = formatFriendly(now);
|
|
373
|
+
const temporalCallout = `> [!NOTE]\n> **Temporal Context**: Integrated research current as of ${temporalFriendly}.`;
|
|
374
|
+
|
|
375
|
+
const bodyLines = [
|
|
376
|
+
temporalCallout,
|
|
377
|
+
'',
|
|
378
|
+
`# Consolidated Research Report: ${parentTopic}`,
|
|
379
|
+
'',
|
|
380
|
+
finalReport,
|
|
381
|
+
'',
|
|
382
|
+
'## Appendix: Gathered Search Batches & Sources',
|
|
383
|
+
'',
|
|
384
|
+
sourcesSection
|
|
385
|
+
];
|
|
386
|
+
|
|
387
|
+
const frontmatter = {
|
|
388
|
+
type: 'memory',
|
|
389
|
+
slug,
|
|
390
|
+
category: 'facts',
|
|
391
|
+
title: `Consolidated Research Report: ${parentTopic}`,
|
|
392
|
+
status: 'draft',
|
|
393
|
+
confidence: 0.9,
|
|
394
|
+
importance: 4,
|
|
395
|
+
created: existingFrontmatter?.created || now,
|
|
396
|
+
updated: now,
|
|
397
|
+
last_accessed: now,
|
|
398
|
+
source: {
|
|
399
|
+
type: 'knowledge-acquisition',
|
|
400
|
+
session_id: existingFrontmatter?.source?.session_id || `deep-research-${Date.now().toString(36)}`,
|
|
401
|
+
agent: 'deep-research-engine',
|
|
402
|
+
evidence_count: existingFrontmatter?.source?.evidence_count || 0,
|
|
403
|
+
parent_topic: parentTopic,
|
|
404
|
+
},
|
|
405
|
+
supersedes: existingFrontmatter?.supersedes || [],
|
|
406
|
+
superseded_by: null,
|
|
407
|
+
contradicts: existingFrontmatter?.contradicts || [],
|
|
408
|
+
tags: ['research', 'deep-research', 'consolidated-report', 'cited', 'master-artifact'],
|
|
409
|
+
related: existingFrontmatter?.related || [],
|
|
410
|
+
routes_to_skills: [],
|
|
411
|
+
sentiment_polarity: 'descriptive',
|
|
412
|
+
sentiment_target: 'external-knowledge',
|
|
413
|
+
modality: 'should',
|
|
414
|
+
subject: 'agent',
|
|
415
|
+
predicate: 'know',
|
|
416
|
+
object: parentTopic,
|
|
417
|
+
decay: { half_life_days: 60, access_count: 1 },
|
|
418
|
+
schema_version: 2,
|
|
419
|
+
x_memory_layer: 'research',
|
|
420
|
+
x_temporal_context: now,
|
|
421
|
+
x_citations: existingFrontmatter?.x_citations || [],
|
|
422
|
+
x_sources: existingFrontmatter?.x_sources || [],
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
atomicWrite(filePath, safeStringify(bodyLines.join('\n'), frontmatter));
|
|
426
|
+
return filePath;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// ─── Synthesis ───────────────────────────────────────────────────────────────────
|
|
430
|
+
|
|
431
|
+
async function synthesizeLocally(task, results, runtimeConfig) {
|
|
432
|
+
const today = getLocalizedDateTime();
|
|
433
|
+
const cutoff = runtimeConfig?.training_cutoff || 'January 2025';
|
|
434
|
+
|
|
435
|
+
const sourceSummary = results
|
|
436
|
+
.map(r => `[${r.source}] ${r.title}: ${r.snippet?.slice(0, 300)}`)
|
|
437
|
+
.join('\n');
|
|
438
|
+
|
|
439
|
+
const system = `You are a Deep Research Synthesizer. Today's date and time is ${today}. The model's training data cutoff is ${cutoff}.
|
|
440
|
+
Synthesize research results into a concise report. Cite sources inline [Source: URL]. Prioritize fresh, timely information published after the cutoff.`;
|
|
441
|
+
const prompt = `Topic: "${task.target}"\n\nResults:\n${sourceSummary.slice(0, 6000)}`;
|
|
442
|
+
|
|
443
|
+
try {
|
|
444
|
+
return callLocalRuntime(prompt, system, runtimeConfig);
|
|
445
|
+
} catch (err) {
|
|
446
|
+
logger.error('research: callLocalRuntime failed during synthesis', { err: err.message });
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Quick research: add a topic to the agenda and immediately run one acquisition cycle.
|
|
453
|
+
* Used for direct research instructions from users.
|
|
454
|
+
*
|
|
455
|
+
* @param {string} topic
|
|
456
|
+
* @param {object} opts
|
|
457
|
+
*/
|
|
458
|
+
export async function handleQuickResearch(topic, { vaultDir, inboxDir, queueDir, runtimeConfig }) {
|
|
459
|
+
addToAgenda({ topic, priority: 90, source: 'user-instruction', directInstruction: true });
|
|
460
|
+
return runKnowledgeAcquisitionCycle({ vaultDir, inboxDir, queueDir, runtimeConfig, forceTopic: topic });
|
|
461
|
+
}
|