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.
Files changed (170) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +173 -0
  3. package/bin/antigravity.mjs +212 -0
  4. package/bin/init.mjs +36 -0
  5. package/bin/oci-sniper.sh +69 -0
  6. package/bin/total-recall.mjs +229 -0
  7. package/docs/ARCHITECTURE.md +251 -0
  8. package/docs/IDE_INTEGRATION.md +112 -0
  9. package/package.json +71 -0
  10. package/scaffold/.agent/skills/total-recall/SKILL.md +195 -0
  11. package/scaffold/.agent/skills/total-recall/evals/evals.json +20 -0
  12. package/scaffold/.agent/skills/total-recall/memory-vault/concepts/cli-help-reference.md +145 -0
  13. package/scaffold/.agent/skills/total-recall/memory-vault/invariants/operating-instructions.md +75 -0
  14. package/scaffold/.agent/skills/total-recall/memory-vault/preferences/always-websearch-gap.md +16 -0
  15. package/scaffold/.agent/skills/total-recall/memory-vault/preferences/topic-research-sop.md +17 -0
  16. package/scaffold/.agent/skills/total-recall/references/api-reference.md +187 -0
  17. package/scaffold/.agent/skills/total-recall/references/architecture-reference.md +81 -0
  18. package/scaffold/.agent/skills/total-recall/references/cli-reference.md +103 -0
  19. package/scaffold/.agent/skills/total-recall/references/ssss-reference.md +88 -0
  20. package/scaffold/.agent/skills/total-recall/references/troubleshooting.md +68 -0
  21. package/scaffold/.agent/skills/total-recall/scripts/sync-repo.mjs +83 -0
  22. package/scaffold/.agent/skills/total-recall/skills/cli-agents/SKILL.md +122 -0
  23. package/scaffold/.agent/skills/total-recall/skills/cli-agents/agents.yml +27 -0
  24. package/scaffold/.agent/skills/total-recall/skills/cli-agents/references/antigravity.txt +20 -0
  25. package/scaffold/.agent/skills/total-recall/skills/cli-agents/references/claude.txt +58 -0
  26. package/scaffold/.agent/skills/total-recall/skills/cli-agents/references/codex.txt +81 -0
  27. package/scaffold/.agent/skills/total-recall/skills/cli-agents/references/gemini.txt +44 -0
  28. package/scaffold/.agent/skills/total-recall/skills/research/SKILL.md +337 -0
  29. package/scaffold/.agent/skills/total-recall/skills/skill/SKILL.md +281 -0
  30. package/scaffold/.agent/skills/total-recall/skills/skill/assets/skill-template/SKILL.md +20 -0
  31. package/scaffold/.agent/skills/total-recall/skills/skill/evals/evals.json +20 -0
  32. package/scaffold/.agent/skills/total-recall/skills/skill/hooks/on-skill-add.sh +14 -0
  33. package/scaffold/.agent/skills/total-recall/skills/skill/hooks/on-skill-delete.sh +14 -0
  34. package/scaffold/.agent/skills/total-recall/skills/skill/learnings/.gitkeep +0 -0
  35. package/scaffold/.agent/skills/total-recall/skills/skill/references/.gitkeep +0 -0
  36. package/scaffold/.agent/skills/total-recall/skills/skill/scripts/.gitkeep +0 -0
  37. package/scaffold/.agent/skills/total-recall/skills/skill/scripts/create-skill.sh +65 -0
  38. package/scaffold/.agent/skills/total-recall/skills/skill/scripts/enforce-skill-optimization.mjs +135 -0
  39. package/scaffold/.agent/skills/total-recall/skills/skill/scripts/find-skills.mjs +124 -0
  40. package/scaffold/.agent/skills/total-recall/skills/skill/scripts/scan-skill.mjs +207 -0
  41. package/scaffold/.agent/skills/total-recall/skills/skill/scripts/skill-watcher.mjs +64 -0
  42. package/scaffold/.agent/skills/total-recall/skills/skill/subagents/audit-all-skills.md +62 -0
  43. package/scaffold/.agent/skills/total-recall/skills/ssss/SKILL.md +27 -0
  44. package/scaffold/.agent/skills/total-recall/skills/ssss/evals/evals.json +17 -0
  45. package/scaffold/.agent/skills/total-recall/skills/ssss/references/ssss-spec.md +117 -0
  46. package/scaffold/.agent/skills/total-recall/skills/ssss/scripts/validate-schema.mjs +54 -0
  47. package/scaffold/.agent/skills/total-recall/skills/ssss/subagents/audit-ssss-node.md +9 -0
  48. package/scaffold/.agent/skills/total-recall/subagents/total-recall-diagnostician.md +13 -0
  49. package/src/cli/agent-dir.mjs +105 -0
  50. package/src/cli/backup.mjs +313 -0
  51. package/src/cli/brain.mjs +224 -0
  52. package/src/cli/chat.mjs +74 -0
  53. package/src/cli/config.mjs +247 -0
  54. package/src/cli/connect.mjs +872 -0
  55. package/src/cli/daemon.mjs +228 -0
  56. package/src/cli/deploy-ui.mjs +1793 -0
  57. package/src/cli/deploy.mjs +1726 -0
  58. package/src/cli/doctor.mjs +254 -0
  59. package/src/cli/dream.mjs +80 -0
  60. package/src/cli/forget.mjs +92 -0
  61. package/src/cli/friction.mjs +21 -0
  62. package/src/cli/generate-pat.mjs +65 -0
  63. package/src/cli/hash-password.mjs +22 -0
  64. package/src/cli/help.mjs +153 -0
  65. package/src/cli/import-rules.mjs +102 -0
  66. package/src/cli/ingest/index.mjs +318 -0
  67. package/src/cli/ingest/parsers/chrome-bookmarks.mjs +238 -0
  68. package/src/cli/ingest/parsers/google-keep.mjs +147 -0
  69. package/src/cli/ingest/parsers/search-history.mjs +142 -0
  70. package/src/cli/ingest/parsers/youtube-history.mjs +181 -0
  71. package/src/cli/ingest/utils/dedup.mjs +69 -0
  72. package/src/cli/ingest/utils/takeout-walker.mjs +101 -0
  73. package/src/cli/ingest.mjs +102 -0
  74. package/src/cli/init.mjs +736 -0
  75. package/src/cli/integration-dispatcher.mjs +128 -0
  76. package/src/cli/lint.mjs +166 -0
  77. package/src/cli/map.mjs +194 -0
  78. package/src/cli/migrate.mjs +87 -0
  79. package/src/cli/rebuild.mjs +88 -0
  80. package/src/cli/recall.mjs +219 -0
  81. package/src/cli/relay.mjs +518 -0
  82. package/src/cli/remember.mjs +306 -0
  83. package/src/cli/research.mjs +381 -0
  84. package/src/cli/reset-password.mjs +75 -0
  85. package/src/cli/restore.mjs +135 -0
  86. package/src/cli/setup.mjs +616 -0
  87. package/src/cli/share.mjs +180 -0
  88. package/src/cli/skill.mjs +313 -0
  89. package/src/cli/snapshot.mjs +108 -0
  90. package/src/cli/start.mjs +63 -0
  91. package/src/cli/status.mjs +296 -0
  92. package/src/cli/sync.mjs +383 -0
  93. package/src/cli/uninstall.mjs +613 -0
  94. package/src/cli/upgrade.mjs +141 -0
  95. package/src/cli/wizard.html +3682 -0
  96. package/src/core/blackboard.mjs +54 -0
  97. package/src/core/clarity-rewriter.mjs +770 -0
  98. package/src/core/conclusion-writer.mjs +259 -0
  99. package/src/core/config.mjs +277 -0
  100. package/src/core/conflict-detector.mjs +536 -0
  101. package/src/core/crypto.mjs +107 -0
  102. package/src/core/daemon-control.mjs +158 -0
  103. package/src/core/daemon-loop.mjs +364 -0
  104. package/src/core/dream.mjs +347 -0
  105. package/src/core/drift-detector.mjs +143 -0
  106. package/src/core/embeddings.mjs +632 -0
  107. package/src/core/emergency-alerts.mjs +137 -0
  108. package/src/core/evolution.mjs +107 -0
  109. package/src/core/fact-seeker.mjs +1602 -0
  110. package/src/core/friction.mjs +95 -0
  111. package/src/core/import-rules.mjs +308 -0
  112. package/src/core/inference-engine.mjs +367 -0
  113. package/src/core/logger.mjs +75 -0
  114. package/src/core/memory-layers.mjs +96 -0
  115. package/src/core/migrate.mjs +116 -0
  116. package/src/core/notifications.mjs +77 -0
  117. package/src/core/operation-validator.mjs +272 -0
  118. package/src/core/optimizer.mjs +330 -0
  119. package/src/core/pattern_detector.mjs +113 -0
  120. package/src/core/post-mortem.mjs +413 -0
  121. package/src/core/protect-instructions.mjs +148 -0
  122. package/src/core/quick-capture.mjs +145 -0
  123. package/src/core/research-queue.mjs +431 -0
  124. package/src/core/research.mjs +461 -0
  125. package/src/core/runtime.mjs +515 -0
  126. package/src/core/sandbox.mjs +163 -0
  127. package/src/core/scheduler.mjs +569 -0
  128. package/src/core/schema.mjs +333 -0
  129. package/src/core/search.mjs +219 -0
  130. package/src/core/semantic-index.mjs +276 -0
  131. package/src/core/session-watcher.mjs +850 -0
  132. package/src/core/snapshot.mjs +133 -0
  133. package/src/core/source-adapters.mjs +1038 -0
  134. package/src/core/steering.mjs +137 -0
  135. package/src/core/surface.mjs +444 -0
  136. package/src/core/task_runner.mjs +125 -0
  137. package/src/core/tts.mjs +142 -0
  138. package/src/core/usage-tracker.mjs +349 -0
  139. package/src/core/vault-cache.mjs +76 -0
  140. package/src/core/vault.mjs +273 -0
  141. package/src/core/watchdog.mjs +236 -0
  142. package/src/server/api.mjs +944 -0
  143. package/src/server/auth.mjs +410 -0
  144. package/src/server/index.mjs +868 -0
  145. package/src/server/keys.mjs +181 -0
  146. package/src/server/rest.mjs +1652 -0
  147. package/src/server/route-manifest.json +77 -0
  148. package/src/server/routes/_shared.mjs +93 -0
  149. package/src/server/routes/keys.mjs +55 -0
  150. package/src/server/routes/memory.mjs +249 -0
  151. package/src/server/routes/sessions.mjs +336 -0
  152. package/src/server/routes/share.mjs +138 -0
  153. package/src/server/tools.mjs +618 -0
  154. package/templates/Caddyfile +37 -0
  155. package/templates/com.totalrecall.backup.plist +42 -0
  156. package/templates/com.totalrecall.daemon.plist +40 -0
  157. package/templates/com.totalrecall.duckdns.plist +23 -0
  158. package/templates/com.totalrecall.server.plist +46 -0
  159. package/templates/default-config/budget.yml +6 -0
  160. package/templates/default-config/runtime.yml +11 -0
  161. package/templates/default-config/security.yml +53 -0
  162. package/templates/default-config/voice.yml +34 -0
  163. package/templates/obsidian-queries/conflicts.md +26 -0
  164. package/templates/obsidian-queries/daily.md +17 -0
  165. package/templates/obsidian-queries/index.md +38 -0
  166. package/templates/obsidian-queries/skills.md +14 -0
  167. package/templates/onboarding-interview.md +72 -0
  168. package/templates/skill_engineering.md +40 -0
  169. package/templates/total-recall-daemon.service +21 -0
  170. 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
+ }