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,569 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import matter from 'gray-matter';
|
|
4
|
+
import crypto from 'crypto';
|
|
5
|
+
import { loadNodes, atomicWrite, safeStringify } from './vault.mjs';
|
|
6
|
+
import { logger } from './logger.mjs';
|
|
7
|
+
import { loadQueue, updateQueueItem } from './research-queue.mjs';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Total Recall Task Scheduler
|
|
11
|
+
*
|
|
12
|
+
* A priority queue that dispatches tasks to cognitive layer engines.
|
|
13
|
+
* The queue is NEVER empty — when explicit tasks run out, the scheduler
|
|
14
|
+
* auto-generates idle improvement tasks from vault analysis.
|
|
15
|
+
*
|
|
16
|
+
* Task categories map to cognitive layers:
|
|
17
|
+
* conscious-enforcement → Conscious Layer (weight 1.0)
|
|
18
|
+
* system2-deliberation → System 2 Layer (weight 0.8)
|
|
19
|
+
* memory-maintenance → Maintenance (weight 0.6)
|
|
20
|
+
* research-acquisition → Research Layer (weight 0.4)
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
// ─── Category Weights ───────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
export const LAYER_WEIGHTS = Object.freeze({
|
|
26
|
+
'conscious-enforcement': 1.0,
|
|
27
|
+
'system2-deliberation': 0.8,
|
|
28
|
+
'cutoff-audit': 0.9, // High weight — correctness beats most maintenance
|
|
29
|
+
'memory-maintenance': 0.6,
|
|
30
|
+
'research-acquisition': 0.4,
|
|
31
|
+
'skill-engineering': 0.5,
|
|
32
|
+
'proactive-research': 0.4,
|
|
33
|
+
'self-evaluation': 0.3,
|
|
34
|
+
'exploration': 0.2,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// ─── Priority Queue ─────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
export class TaskQueue {
|
|
40
|
+
constructor() {
|
|
41
|
+
this._items = [];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get length() {
|
|
45
|
+
return this._items.length;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Enqueue a task. Effective priority = task.priority * layer weight.
|
|
50
|
+
*/
|
|
51
|
+
enqueue(task) {
|
|
52
|
+
const layerWeight = LAYER_WEIGHTS[task.category] || 0.5;
|
|
53
|
+
const effectivePriority = (task.priority || 50) * layerWeight;
|
|
54
|
+
this._items.push({ task, effectivePriority });
|
|
55
|
+
this._items.sort((a, b) => b.effectivePriority - a.effectivePriority);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Dequeue the highest-priority task.
|
|
60
|
+
* Returns null if the queue is empty.
|
|
61
|
+
*/
|
|
62
|
+
dequeue() {
|
|
63
|
+
const item = this._items.shift();
|
|
64
|
+
return item ? item.task : null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Peek at the top task without removing it.
|
|
69
|
+
*/
|
|
70
|
+
peek() {
|
|
71
|
+
return this._items.length > 0 ? this._items[0].task : null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get all tasks (for inspection / status).
|
|
76
|
+
*/
|
|
77
|
+
all() {
|
|
78
|
+
return this._items.map((i) => i.task);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Check if the queue has any tasks of a specific category.
|
|
83
|
+
*/
|
|
84
|
+
hasCategory(category) {
|
|
85
|
+
return this._items.some((i) => i.task.category === category);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ─── Task Loading from Disk ─────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Load pending tasks from the scheduler queue directory.
|
|
93
|
+
*
|
|
94
|
+
* @param {string} queueDir Path to .agent/skills/total-recall/scheduler/queue/
|
|
95
|
+
* @returns {object[]} Array of task objects
|
|
96
|
+
*/
|
|
97
|
+
export function loadPendingTasks(queueDir) {
|
|
98
|
+
if (!fs.existsSync(queueDir)) return [];
|
|
99
|
+
|
|
100
|
+
const files = fs.readdirSync(queueDir).filter((f) => f.endsWith('.md'));
|
|
101
|
+
const tasks = [];
|
|
102
|
+
|
|
103
|
+
for (const file of files) {
|
|
104
|
+
try {
|
|
105
|
+
const raw = fs.readFileSync(path.join(queueDir, file), 'utf8');
|
|
106
|
+
const { data, content } = matter(raw);
|
|
107
|
+
if (data.status === 'pending' || data.status === 'in-progress') {
|
|
108
|
+
tasks.push({
|
|
109
|
+
...data,
|
|
110
|
+
slug: data.slug || path.basename(file, '.md'),
|
|
111
|
+
body: content,
|
|
112
|
+
_filepath: path.join(queueDir, file),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
// skip malformed
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return tasks;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Update a task's status on disk.
|
|
125
|
+
*/
|
|
126
|
+
export function updateTaskStatus(task, newStatus, queueDir) {
|
|
127
|
+
const filepath = task._filepath || path.join(queueDir, `${task.slug}.md`);
|
|
128
|
+
if (!fs.existsSync(filepath)) return;
|
|
129
|
+
|
|
130
|
+
const raw = fs.readFileSync(filepath, 'utf8');
|
|
131
|
+
const { data, content } = matter(raw);
|
|
132
|
+
data.status = newStatus;
|
|
133
|
+
if (newStatus === 'completed') {
|
|
134
|
+
data.completed_at = new Date().toISOString();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Also update in-memory object properties so they stay in sync
|
|
138
|
+
task.status = newStatus;
|
|
139
|
+
if (newStatus === 'completed') {
|
|
140
|
+
task.completed_at = data.completed_at;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Strip undefined values — js-yaml (used by gray-matter) crashes on explicit undefined
|
|
144
|
+
for (const key of Object.keys(data)) {
|
|
145
|
+
if (data[key] === undefined) delete data[key];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
atomicWrite(filepath, safeStringify(content, data));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Persist a task (especially generated idle tasks) to disk as a pending markdown file.
|
|
153
|
+
*
|
|
154
|
+
* @param {object} task Task object
|
|
155
|
+
* @param {string} queueDir Path to queue directory
|
|
156
|
+
*/
|
|
157
|
+
export function persistTaskToDisk(task, queueDir) {
|
|
158
|
+
if (!fs.existsSync(queueDir)) {
|
|
159
|
+
fs.mkdirSync(queueDir, { recursive: true });
|
|
160
|
+
}
|
|
161
|
+
const filepath = path.join(queueDir, `${task.slug}.md`);
|
|
162
|
+
const { body, ...frontmatter } = task;
|
|
163
|
+
|
|
164
|
+
// Ensure default fields
|
|
165
|
+
if (frontmatter.progress === undefined) {
|
|
166
|
+
frontmatter.progress = 0;
|
|
167
|
+
}
|
|
168
|
+
if (frontmatter.estimated_calls === undefined) {
|
|
169
|
+
frontmatter.estimated_calls = 5;
|
|
170
|
+
}
|
|
171
|
+
if (frontmatter.deadline === undefined) {
|
|
172
|
+
frontmatter.deadline = new Date(Date.now() + 86400000).toISOString().split('T')[0];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Strip undefined values — js-yaml (used by gray-matter) crashes on explicit undefined
|
|
176
|
+
for (const key of Object.keys(frontmatter)) {
|
|
177
|
+
if (frontmatter[key] === undefined) delete frontmatter[key];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const raw = safeStringify(body || '', frontmatter);
|
|
181
|
+
atomicWrite(filepath, raw);
|
|
182
|
+
task._filepath = filepath;
|
|
183
|
+
return filepath;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ─── Idle Task Generation ───────────────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
let _idleCycleCounter = 0;
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Generate an idle improvement task when the explicit queue is empty.
|
|
192
|
+
* Weighted toward proactive research — 3 of every 9 ticks do real web search.
|
|
193
|
+
*
|
|
194
|
+
* Cycle (9 ticks):
|
|
195
|
+
* 0: proactive-research — web search + synthesize from agenda
|
|
196
|
+
* 1: inference — draw System 2 conclusions from vault clusters
|
|
197
|
+
* 2: proactive-research — web search (second topic)
|
|
198
|
+
* 3: staleness-check — flag outdated facts
|
|
199
|
+
* 4: proactive-research — web search (third topic)
|
|
200
|
+
* 5: inference — draw more conclusions
|
|
201
|
+
* 6: post-mortem — re-analyze most recent session
|
|
202
|
+
* 7: clarity-review — improve a vault node
|
|
203
|
+
* 8: cutoff-audit — check for training-drift
|
|
204
|
+
*/
|
|
205
|
+
export function generateIdleTask({ vaultDir, sessionsDir }) {
|
|
206
|
+
// Only allow purely local task generation strategies.
|
|
207
|
+
// Absolutely no automated/proactive web searches or staleness checks that make internet queries.
|
|
208
|
+
const cleanStrategies = [
|
|
209
|
+
() => generateInferenceTask(vaultDir),
|
|
210
|
+
() => generatePostMortemTask(sessionsDir),
|
|
211
|
+
() => generateClarityReviewTask(vaultDir),
|
|
212
|
+
];
|
|
213
|
+
|
|
214
|
+
const strategy = cleanStrategies[_idleCycleCounter % cleanStrategies.length];
|
|
215
|
+
_idleCycleCounter++;
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
return strategy();
|
|
219
|
+
} catch {
|
|
220
|
+
// Local fallback only — never query the internet or make automated web searches
|
|
221
|
+
return makeFallbackTask('memory-maintenance', 'Wait for active conversation task');
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Generate a proactive research task — pulls the next topic from the agenda
|
|
227
|
+
* and queues a full web search + synthesis cycle.
|
|
228
|
+
*/
|
|
229
|
+
function generateProactiveResearchTask() {
|
|
230
|
+
return {
|
|
231
|
+
type: 'task',
|
|
232
|
+
slug: `proactive-research-${Date.now().toString(36)}`,
|
|
233
|
+
priority: 55,
|
|
234
|
+
category: 'proactive-research',
|
|
235
|
+
status: 'pending',
|
|
236
|
+
created_by: 'scheduler-idle',
|
|
237
|
+
reason: 'Idle task: pull next topic from research agenda and execute web search + synthesis.',
|
|
238
|
+
body: 'Run one knowledge acquisition cycle: pull highest-priority pending topic from agenda, gather from web/wikipedia/arXiv/npm/github, synthesize with LLM, write cited fact node to vault.',
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Pick a random active memory node and create a clarity review task.
|
|
244
|
+
*/
|
|
245
|
+
function generateClarityReviewTask(vaultDir) {
|
|
246
|
+
const nodes = loadNodes(vaultDir).filter((n) => n.status === 'active');
|
|
247
|
+
if (nodes.length === 0) {
|
|
248
|
+
return makeFallbackTask('memory-maintenance', 'No active nodes to review');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const target = nodes[crypto.randomInt(0, nodes.length)];
|
|
252
|
+
return {
|
|
253
|
+
type: 'task',
|
|
254
|
+
slug: `clarity-review-${target.slug}-${Date.now().toString(36)}`,
|
|
255
|
+
priority: 30,
|
|
256
|
+
category: 'memory-maintenance',
|
|
257
|
+
target: target.slug,
|
|
258
|
+
status: 'pending',
|
|
259
|
+
created_by: 'scheduler-idle',
|
|
260
|
+
reason: `Idle task: review "${target.title}" for clarity and actionability.`,
|
|
261
|
+
body: `## Objective\nReview memory node "${target.slug}" and evaluate:\n1. Is the title descriptive enough for a 7-item skill injection?\n2. Is the body actionable by an AI agent?\n3. Does the frontmatter accurately reflect the content?\n\nIf improvements are needed, generate a rewrite proposal.`,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Pick the oldest fact node and create a staleness check task.
|
|
267
|
+
*/
|
|
268
|
+
function generateStalenessCheckTask(vaultDir) {
|
|
269
|
+
const nodes = loadNodes(vaultDir)
|
|
270
|
+
.filter((n) => n.status === 'active' && n.category === 'facts')
|
|
271
|
+
.sort((a, b) => {
|
|
272
|
+
const aDate = new Date(a.last_accessed || a.updated || 0).getTime();
|
|
273
|
+
const bDate = new Date(b.last_accessed || b.updated || 0).getTime();
|
|
274
|
+
return aDate - bDate; // oldest first
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
if (nodes.length === 0) {
|
|
278
|
+
// No facts? Pick any old node
|
|
279
|
+
const allNodes = loadNodes(vaultDir).filter((n) => n.status === 'active');
|
|
280
|
+
if (allNodes.length === 0) {
|
|
281
|
+
return makeFallbackTask('research-acquisition', 'No facts to verify');
|
|
282
|
+
}
|
|
283
|
+
const oldest = allNodes.sort((a, b) => {
|
|
284
|
+
return new Date(a.updated || 0).getTime() - new Date(b.updated || 0).getTime();
|
|
285
|
+
})[0];
|
|
286
|
+
return {
|
|
287
|
+
type: 'task',
|
|
288
|
+
slug: `staleness-check-${oldest.slug}-${Date.now().toString(36)}`,
|
|
289
|
+
priority: 25,
|
|
290
|
+
category: 'research-acquisition',
|
|
291
|
+
target: oldest.slug,
|
|
292
|
+
status: 'pending',
|
|
293
|
+
created_by: 'scheduler-idle',
|
|
294
|
+
reason: `Idle task: verify if "${oldest.title}" is still current.`,
|
|
295
|
+
body: `## Objective\nEvaluate whether memory node "${oldest.slug}" is still accurate given its age and domain.`,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const target = nodes[0];
|
|
300
|
+
return {
|
|
301
|
+
type: 'task',
|
|
302
|
+
slug: `staleness-check-${target.slug}-${Date.now().toString(36)}`,
|
|
303
|
+
priority: 25,
|
|
304
|
+
category: 'research-acquisition',
|
|
305
|
+
target: target.slug,
|
|
306
|
+
status: 'pending',
|
|
307
|
+
created_by: 'scheduler-idle',
|
|
308
|
+
reason: `Idle task: verify if fact "${target.title}" is still current.`,
|
|
309
|
+
body: `## Objective\nThis fact was last accessed on ${target.last_accessed || 'unknown'}.\nDetermine if it is: STILL_VALID, POSSIBLY_STALE, or LIKELY_OUTDATED.\nIf stale, suggest a verification search query.`,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Pick a cluster of related nodes and create an inference task.
|
|
315
|
+
*/
|
|
316
|
+
function generateInferenceTask(vaultDir) {
|
|
317
|
+
const nodes = loadNodes(vaultDir).filter((n) => n.status === 'active');
|
|
318
|
+
if (nodes.length < 3) {
|
|
319
|
+
return makeFallbackTask('system2-deliberation', 'Not enough nodes for inference');
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Find nodes that share tags
|
|
323
|
+
const tagMap = new Map();
|
|
324
|
+
for (const node of nodes) {
|
|
325
|
+
for (const tag of (node.tags || [])) {
|
|
326
|
+
if (!tagMap.has(tag)) tagMap.set(tag, []);
|
|
327
|
+
tagMap.get(tag).push(node);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Find the largest cluster
|
|
332
|
+
let bestTag = null;
|
|
333
|
+
let bestCluster = [];
|
|
334
|
+
for (const [tag, cluster] of tagMap.entries()) {
|
|
335
|
+
if (cluster.length > bestCluster.length && cluster.length >= 2) {
|
|
336
|
+
bestTag = tag;
|
|
337
|
+
bestCluster = cluster;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (bestCluster.length < 2) {
|
|
342
|
+
return makeFallbackTask('system2-deliberation', 'No tag clusters found for inference');
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const selected = bestCluster.slice(0, 5); // max 5 nodes per inference task
|
|
346
|
+
return {
|
|
347
|
+
type: 'task',
|
|
348
|
+
slug: `inference-${bestTag}-${Date.now().toString(36)}`,
|
|
349
|
+
priority: 35,
|
|
350
|
+
category: 'system2-deliberation',
|
|
351
|
+
target: selected.map((n) => n.slug).join(','),
|
|
352
|
+
status: 'pending',
|
|
353
|
+
created_by: 'scheduler-idle',
|
|
354
|
+
reason: `Idle task: draw conclusions from ${selected.length} nodes tagged "${bestTag}".`,
|
|
355
|
+
body: `## Objective\nAnalyze these related memory nodes and determine:\n1. What higher-level conclusions can be drawn?\n2. Are there implicit patterns that should be explicit rules?\n3. Do any subtly contradict each other?\n\n## Nodes\n${selected.map((n) => `- ${n.slug}: "${n.title}"`).join('\n')}`,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Pick the most recent unprocessed session for post-mortem analysis.
|
|
361
|
+
*/
|
|
362
|
+
function generatePostMortemTask(sessionsDir) {
|
|
363
|
+
if (!fs.existsSync(sessionsDir)) {
|
|
364
|
+
return makeFallbackTask('conscious-enforcement', 'No sessions directory');
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const files = fs.readdirSync(sessionsDir)
|
|
368
|
+
.filter((f) => f.endsWith('.jsonl'))
|
|
369
|
+
.map((f) => ({
|
|
370
|
+
name: f,
|
|
371
|
+
mtime: fs.statSync(path.join(sessionsDir, f)).mtimeMs,
|
|
372
|
+
}))
|
|
373
|
+
.sort((a, b) => b.mtime - a.mtime); // newest first
|
|
374
|
+
|
|
375
|
+
if (files.length === 0) {
|
|
376
|
+
return makeFallbackTask('conscious-enforcement', 'No sessions to analyze');
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Pick the newest session
|
|
380
|
+
const target = files[0];
|
|
381
|
+
return {
|
|
382
|
+
type: 'task',
|
|
383
|
+
slug: `post-mortem-${path.basename(target.name, '.jsonl')}-${Date.now().toString(36)}`,
|
|
384
|
+
priority: 40,
|
|
385
|
+
category: 'conscious-enforcement',
|
|
386
|
+
target: target.name,
|
|
387
|
+
status: 'pending',
|
|
388
|
+
created_by: 'scheduler-idle',
|
|
389
|
+
reason: `Idle task: post-mortem analysis of session ${target.name}.`,
|
|
390
|
+
body: `## Objective\nRead the session transcript and extract:\n1. New patterns observed (user preferences, coding style)\n2. New facts mentioned (APIs, tools, versions)\n3. Skill gaps (agent struggled with topic X)\n4. Rule violations (did the agent follow all invariants?)\n\nFor each extraction, produce a draft memory node with appropriate x_memory_layer.`,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Generate a cutoff audit task — scans vault for nodes with intrinsic LLM
|
|
396
|
+
* knowledge that may have drifted since the model's training cutoff.
|
|
397
|
+
*/
|
|
398
|
+
function generateCutoffAuditTask() {
|
|
399
|
+
return {
|
|
400
|
+
type: 'task',
|
|
401
|
+
slug: `cutoff-audit-${Date.now().toString(36)}`,
|
|
402
|
+
priority: 60,
|
|
403
|
+
category: 'cutoff-audit',
|
|
404
|
+
status: 'pending',
|
|
405
|
+
created_by: 'scheduler-idle',
|
|
406
|
+
reason: 'Idle task: scan vault for training-cutoff assumption drift',
|
|
407
|
+
body: 'Identify memory nodes containing time-sensitive claims based on intrinsic LLM knowledge. Flag high-drift items (API versions, pricing, model specs, framework changes) for verification against current real-world sources.',
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function makeFallbackTask(category, reason) {
|
|
412
|
+
return {
|
|
413
|
+
type: 'task',
|
|
414
|
+
slug: `idle-${category}-${Date.now().toString(36)}`,
|
|
415
|
+
priority: 10,
|
|
416
|
+
category,
|
|
417
|
+
status: 'pending',
|
|
418
|
+
created_by: 'scheduler-idle',
|
|
419
|
+
reason: `Fallback idle task: ${reason}`,
|
|
420
|
+
body: 'No actionable work found. Waiting for new tasks.',
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// ─── Scheduler State ────────────────────────────────────────────────────────────
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Create a populated scheduler from disk.
|
|
428
|
+
*
|
|
429
|
+
* @param {object} opts
|
|
430
|
+
* @param {string} opts.queueDir Path to .agent/skills/total-recall/scheduler/queue/
|
|
431
|
+
* @param {string} opts.vaultDir Path to .agent/skills/total-recall/memory-vault/
|
|
432
|
+
* @param {string} opts.sessionsDir Path to .agent/skills/total-recall/sessions/
|
|
433
|
+
* @returns {{ queue: TaskQueue, stats: object }}
|
|
434
|
+
*/
|
|
435
|
+
export function createScheduler({ queueDir, vaultDir, sessionsDir }) {
|
|
436
|
+
const queue = new TaskQueue();
|
|
437
|
+
|
|
438
|
+
// Load explicit tasks from disk
|
|
439
|
+
const diskTasks = loadPendingTasks(queueDir);
|
|
440
|
+
for (const task of diskTasks) {
|
|
441
|
+
queue.enqueue(task);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Load pending research queue tasks
|
|
445
|
+
try {
|
|
446
|
+
const COOLDOWN_MS = process.env.RESEARCH_COOLDOWN_MS
|
|
447
|
+
? parseInt(process.env.RESEARCH_COOLDOWN_MS, 10)
|
|
448
|
+
: 60 * 60 * 1000; // 1 hour default
|
|
449
|
+
|
|
450
|
+
const allItems = loadQueue();
|
|
451
|
+
let resetCount = 0;
|
|
452
|
+
|
|
453
|
+
for (const item of allItems) {
|
|
454
|
+
if (item.status === 'done' || item.status === 'failed') {
|
|
455
|
+
const completedTime = new Date(item.completed_at || item.updated_at || 0).getTime();
|
|
456
|
+
if (Date.now() - completedTime >= COOLDOWN_MS) {
|
|
457
|
+
try {
|
|
458
|
+
updateQueueItem(item.id, { status: 'pending' });
|
|
459
|
+
resetCount++;
|
|
460
|
+
} catch (updateErr) {
|
|
461
|
+
logger.error({
|
|
462
|
+
subsystem: 'scheduler',
|
|
463
|
+
message: `Failed to reset research queue item ${item.id} to pending: ${updateErr.message}`,
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (resetCount > 0) {
|
|
471
|
+
logger.info({
|
|
472
|
+
subsystem: 'scheduler',
|
|
473
|
+
message: `Auto-reset ${resetCount} completed/failed research items back to pending after cooldown.`,
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const researchItems = loadQueue()
|
|
478
|
+
.filter((i) => i.status === 'pending')
|
|
479
|
+
.sort((a, b) => new Date(a.updated_at || a.created_at || 0).getTime() - new Date(b.updated_at || b.created_at || 0).getTime());
|
|
480
|
+
|
|
481
|
+
for (let idx = 0; idx < researchItems.length; idx++) {
|
|
482
|
+
const item = researchItems[idx];
|
|
483
|
+
const dynamicPriority = 85 + (researchItems.length - idx);
|
|
484
|
+
const phase = item.research_phase || 'acquisition';
|
|
485
|
+
|
|
486
|
+
let category = 'proactive-research';
|
|
487
|
+
let slug = `research-acquisition-${item.id}`;
|
|
488
|
+
let reason = `Queued research project (Phase 1/5: Acquisition): ${item.topic}`;
|
|
489
|
+
let body = `Run knowledge acquisition cycle for queued topic: ${item.topic}.\nNotes: ${item.notes || 'None'}`;
|
|
490
|
+
|
|
491
|
+
if (phase === 'deliberation') {
|
|
492
|
+
category = 'system2-deliberation';
|
|
493
|
+
slug = `research-deliberation-${item.id}`;
|
|
494
|
+
reason = `Queued research project (Phase 2/5: Deliberation): ${item.topic}`;
|
|
495
|
+
body = `Run deep System 2 cognitive deliberation for topic: ${item.topic}.\nTarget node: ${item.node_slug || 'pending'}`;
|
|
496
|
+
} else if (phase === 'improvement') {
|
|
497
|
+
category = 'memory-maintenance';
|
|
498
|
+
slug = `research-improvement-${item.id}`;
|
|
499
|
+
reason = `Queued research project (Phase 3/5: Improvement): ${item.topic}`;
|
|
500
|
+
body = `Run document improvement and formatting refinement for topic: ${item.topic}.\nTarget node: ${item.node_slug || 'pending'}`;
|
|
501
|
+
} else if (phase === 'monitoring') {
|
|
502
|
+
category = 'proactive-research';
|
|
503
|
+
slug = `research-monitoring-${item.id}`;
|
|
504
|
+
reason = `Queued research project (Phase 4/5: Monitoring): ${item.topic}`;
|
|
505
|
+
body = `Identify reliable sources of ongoing information, news, feeds or release notes for: ${item.topic}.\nTarget node: ${item.node_slug || 'pending'}`;
|
|
506
|
+
} else if (phase === 'expansion') {
|
|
507
|
+
category = 'exploration';
|
|
508
|
+
slug = `research-expansion-${item.id}`;
|
|
509
|
+
reason = `Queued research project (Phase 5/5: Expansion): ${item.topic}`;
|
|
510
|
+
body = `Discover adjacent research domains, brainstorm tangents, and auto-spawn follow-up tasks for: ${item.topic}.\nTarget node: ${item.node_slug || 'pending'}`;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
queue.enqueue({
|
|
514
|
+
type: 'task',
|
|
515
|
+
slug,
|
|
516
|
+
priority: dynamicPriority,
|
|
517
|
+
category,
|
|
518
|
+
target: item.topic,
|
|
519
|
+
status: 'pending',
|
|
520
|
+
created_by: 'research-queue',
|
|
521
|
+
reason,
|
|
522
|
+
body,
|
|
523
|
+
_research_id: item.id,
|
|
524
|
+
_research_phase: phase,
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
if (researchItems.length > 0) {
|
|
528
|
+
logger.info({
|
|
529
|
+
subsystem: 'scheduler',
|
|
530
|
+
message: `Enqueued ${researchItems.length} pending research tasks from the dynamic queue.`,
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
} catch (err) {
|
|
534
|
+
logger.error({
|
|
535
|
+
subsystem: 'scheduler',
|
|
536
|
+
message: `Failed to load research queue items into scheduler: ${err.message}`,
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
logger.info({
|
|
541
|
+
subsystem: 'scheduler',
|
|
542
|
+
message: `Initialized with ${diskTasks.length} explicit tasks from queue.`,
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
return {
|
|
546
|
+
queue,
|
|
547
|
+
stats: {
|
|
548
|
+
explicitTasks: diskTasks.length,
|
|
549
|
+
categories: Object.fromEntries(
|
|
550
|
+
[...new Set(diskTasks.map((t) => t.category))].map((cat) => [
|
|
551
|
+
cat,
|
|
552
|
+
diskTasks.filter((t) => t.category === cat).length,
|
|
553
|
+
]),
|
|
554
|
+
),
|
|
555
|
+
},
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Get the next task. If the queue is empty, generate an idle task.
|
|
559
|
+
*/
|
|
560
|
+
next() {
|
|
561
|
+
const explicit = queue.dequeue();
|
|
562
|
+
if (explicit) return { task: explicit, source: 'explicit' };
|
|
563
|
+
|
|
564
|
+
const idle = generateIdleTask({ vaultDir, sessionsDir });
|
|
565
|
+
persistTaskToDisk(idle, queueDir);
|
|
566
|
+
return { task: idle, source: 'idle' };
|
|
567
|
+
},
|
|
568
|
+
};
|
|
569
|
+
}
|