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,158 @@
1
+ import { spawn } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { logger } from './logger.mjs';
7
+ import { agentDir, brainDir } from './config.mjs';
8
+
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
+ const ROOT = path.join(__dirname, '..', '..');
11
+
12
+ const PID_FILE = path.join(brainDir, 'logs', 'daemon.pid');
13
+ const LOG_FILE = path.join(brainDir, 'logs', 'daemon.log');
14
+
15
+ /**
16
+ * Reads the daemon PID file and checks if the process is actually running.
17
+ * @returns {number|null} The active PID, or null if not running/invalid.
18
+ */
19
+ export function readPid() {
20
+ try {
21
+ if (!fs.existsSync(PID_FILE)) return null;
22
+ const pid = parseInt(fs.readFileSync(PID_FILE, 'utf8').trim(), 10);
23
+ if (isNaN(pid) || pid <= 0) return null;
24
+
25
+ // Check if the process is alive using signal 0
26
+ try {
27
+ process.kill(pid, 0);
28
+ return pid;
29
+ } catch {
30
+ return null;
31
+ }
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Returns the status of the daemon process.
39
+ * @returns {'running'|'dead'|'not_started'}
40
+ */
41
+ export function getDaemonStatus() {
42
+ try {
43
+ const pid = readPid();
44
+ if (pid) {
45
+ return 'running';
46
+ }
47
+
48
+ // If process is not running but PID file exists, it's considered dead
49
+ if (fs.existsSync(PID_FILE)) {
50
+ return 'dead';
51
+ }
52
+
53
+ return 'not_started';
54
+ } catch {
55
+ return 'not_started';
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Detaches the daemon script in the background and writes its PID to file.
61
+ * @returns {number} The PID of the newly spawned daemon.
62
+ */
63
+ export function startDaemon() {
64
+ const existingPid = readPid();
65
+ if (existingPid) {
66
+ logger.warn('daemon-control', `Daemon already running under PID ${existingPid}.`);
67
+ return existingPid;
68
+ }
69
+
70
+ const dreamScript = path.join(ROOT, 'src', 'core', 'daemon-loop.mjs');
71
+ if (!fs.existsSync(dreamScript)) {
72
+ // Fallback to dream.mjs for backward compatibility
73
+ const fallback = path.join(ROOT, 'src', 'core', 'dream.mjs');
74
+ if (fs.existsSync(fallback)) {
75
+ logger.warn('daemon-control', 'daemon-loop.mjs not found, falling back to dream.mjs');
76
+ } else {
77
+ throw new Error(`Daemon script not found at ${dreamScript}`);
78
+ }
79
+ }
80
+
81
+ const logsDir = path.dirname(LOG_FILE);
82
+ if (!fs.existsSync(logsDir)) {
83
+ fs.mkdirSync(logsDir, { recursive: true });
84
+ }
85
+
86
+ const logFd = fs.openSync(LOG_FILE, 'a');
87
+
88
+ // Use spawn to launch in a detached background state
89
+ const child = spawn(process.execPath, [dreamScript], {
90
+ detached: true,
91
+ stdio: ['ignore', logFd, logFd],
92
+ cwd: ROOT,
93
+ env: {
94
+ ...process.env,
95
+ NODE_ENV: 'production',
96
+ AGENT_DIR: agentDir,
97
+ },
98
+ });
99
+
100
+ child.unref();
101
+
102
+ // Write the PID file
103
+ fs.writeFileSync(PID_FILE, String(child.pid), 'utf8');
104
+ fs.closeSync(logFd);
105
+
106
+ logger.info('daemon-control', `Active Intelligence Daemon started detached (PID ${child.pid}).`);
107
+ return child.pid;
108
+ }
109
+
110
+ /**
111
+ * Stops the running background daemon.
112
+ * @returns {boolean} True if successfully stopped, false otherwise.
113
+ */
114
+ export function stopDaemon() {
115
+ const pid = readPid();
116
+ if (!pid) {
117
+ // Clear PID file in case it was a stale/dead record
118
+ try {
119
+ if (fs.existsSync(PID_FILE)) fs.unlinkSync(PID_FILE);
120
+ } catch {}
121
+ return false;
122
+ }
123
+
124
+ try {
125
+ process.kill(pid, 'SIGTERM');
126
+ logger.info('daemon-control', `Sent SIGTERM to daemon PID ${pid}.`);
127
+
128
+ // Wait briefly and verify cleanup
129
+ try {
130
+ if (fs.existsSync(PID_FILE)) fs.unlinkSync(PID_FILE);
131
+ } catch {}
132
+ return true;
133
+ } catch (err) {
134
+ logger.error('daemon-control', `Failed to terminate daemon PID ${pid}: ${err.message}`);
135
+ return false;
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Ensures that the daemon is in a running state. Auto-starts if not.
141
+ * @returns {number|null} The PID of the daemon.
142
+ */
143
+ export async function ensureDaemonRunning() {
144
+ const status = getDaemonStatus();
145
+ if (status === 'running') {
146
+ return readPid();
147
+ }
148
+
149
+ logger.info('daemon-control', `Daemon status is '${status}'. Initiating auto-start...`);
150
+ try {
151
+ const pid = startDaemon();
152
+ logger.info('daemon-control', `Auto-start successful. Daemon is now running on PID ${pid}.`);
153
+ return pid;
154
+ } catch (err) {
155
+ logger.error('daemon-control', `Failed to auto-start background daemon: ${err.message}`);
156
+ throw err;
157
+ }
158
+ }
@@ -0,0 +1,364 @@
1
+ /**
2
+ * Total Recall Active Intelligence Daemon Loop — v2 (Deterministic)
3
+ *
4
+ * A deterministic automation engine that runs crons, manages the research
5
+ * queue, ingests sessions, and maintains the skill folder. No local LLM required.
6
+ *
7
+ * Phases per tick:
8
+ * 1. Ingest new IDE conversation logs
9
+ * 2. Load/refresh task queue from disk
10
+ * 3. Dispatch next task (deterministic only — no LLM tasks)
11
+ * 4. Periodically recompile surfaces
12
+ *
13
+ * This script is spawned by `total-recall daemon start` and runs until killed.
14
+ */
15
+
16
+ import path from 'path';
17
+ import fs from 'fs';
18
+ import { createScheduler, updateTaskStatus } from './scheduler.mjs';
19
+ import { scanAndIngest } from './session-watcher.mjs';
20
+ import { logger } from './logger.mjs';
21
+ import { updateQueueItem, loadQueue } from './research-queue.mjs';
22
+ import { execFileSync } from "node:child_process";
23
+ import { agentDir, brainDir } from './config.mjs';
24
+
25
+ const AGENT_DIR = agentDir;
26
+ const BRAIN_DIR = brainDir;
27
+ const VAULT_DIR = path.join(BRAIN_DIR, 'memory-vault');
28
+ const SKILLS_DIR = path.join(AGENT_DIR, 'skills');
29
+ const DERIVED_DIR = path.join(BRAIN_DIR, 'memory-derived');
30
+ const CONFLICTS_DIR = path.join(BRAIN_DIR, 'memory-inbox', 'conflicts');
31
+ const SESSIONS_DIR = path.join(BRAIN_DIR, 'sessions');
32
+ const QUEUE_DIR = path.join(BRAIN_DIR, 'scheduler', 'queue');
33
+ const INSTRUCTIONS_FILE = path.join(BRAIN_DIR, 'INSTRUCTIONS.md');
34
+
35
+ // How often to recompile surfaces (every N task ticks)
36
+ const RECOMPILE_EVERY_N_TASKS = 20;
37
+
38
+ // Pause between task iterations (ms)
39
+ const TASK_SLEEP_MS = 10000;
40
+
41
+ // ─── Categories that require LLM (skipped in deterministic mode) ────────────────
42
+
43
+ const LLM_REQUIRED_CATEGORIES = new Set([
44
+ 'system2-deliberation',
45
+ 'conscious-enforcement',
46
+ 'cutoff-audit',
47
+ ]);
48
+
49
+ /**
50
+ * Check if a task requires an LLM to execute.
51
+ * These tasks are skipped — the daemon is deterministic.
52
+ */
53
+ function requiresLLM(task) {
54
+ if (LLM_REQUIRED_CATEGORIES.has(task.category)) return true;
55
+
56
+ const slug = task.slug;
57
+ if (slug.startsWith('research-deliberation-')) return true;
58
+ if (slug.startsWith('research-improvement-')) return true;
59
+ if (slug.startsWith('research-monitoring-')) return true;
60
+ if (slug.startsWith('research-expansion-')) return true;
61
+ if (slug.includes('self-diagnosis')) return true;
62
+ if (slug.includes('clarity-review')) return true;
63
+ if (slug.includes('staleness-check')) return true;
64
+ if (slug.includes('inference')) return true;
65
+ if (slug.includes('post-mortem')) return true;
66
+ if (slug.includes('compliance')) return true;
67
+
68
+ return false;
69
+ }
70
+
71
+ /**
72
+ * Dispatch a task to the appropriate engine.
73
+ * Only deterministic (non-LLM) tasks are executed.
74
+ */
75
+ async function dispatchTask(task) {
76
+ const category = task.category;
77
+
78
+ try {
79
+ switch (category) {
80
+ case 'memory-maintenance':
81
+ return await runMaintenanceTask(task);
82
+
83
+ default:
84
+ // All other categories (research, deliberation, inference, etc.)
85
+ // require LLM and are handled by IDE agents, not the daemon.
86
+ logger.info({
87
+ subsystem: 'daemon-loop',
88
+ message: `Skipping task (not deterministic): ${task.slug} [${category}]`,
89
+ });
90
+ return { success: true, output: 'Skipped — handled by IDE agents' };
91
+ }
92
+ } catch (err) {
93
+ logger.info({
94
+ subsystem: 'daemon-loop',
95
+ message: `Task ${task.slug} failed: ${err.message}`,
96
+ });
97
+ return { success: false, error: err.message };
98
+ }
99
+ }
100
+
101
+ // ─── Research Engine (Web Fetching Only — No LLM Synthesis) ─────────────────────
102
+
103
+ async function runResearchTask(task) {
104
+ const inboxDir = path.join(BRAIN_DIR, 'memory-inbox', 'pending');
105
+
106
+ // Knowledge acquisition — fetch from web, save raw results
107
+ if (task.slug.startsWith('research-acquisition-') || task.category === 'proactive-research' || task.slug.includes('fact-seeker') || task.slug.includes('knowledge-acquisition')) {
108
+ const { runKnowledgeAcquisitionCycle } = await import('./fact-seeker.mjs');
109
+ const forceTopic = task.target || null;
110
+ const result = await runKnowledgeAcquisitionCycle({
111
+ vaultDir: VAULT_DIR,
112
+ inboxDir,
113
+ queueDir: QUEUE_DIR,
114
+ forceTopic,
115
+ skillsDir: SKILLS_DIR,
116
+ derivedDir: DERIVED_DIR,
117
+ instructionsFile: INSTRUCTIONS_FILE,
118
+ });
119
+ if (result.skipped) return { success: true, output: `Knowledge acquisition: ${result.skipped}` };
120
+ const surfaceNote = result.surfaced ? ' [SURFACED]' : ' [staged]';
121
+ return {
122
+ success: true,
123
+ output: `Researched "${result.topic}": ${result.sources?.join(', ')} | confidence: ${result.confidence || 'n/a'} | slug: ${result.factSlug}${surfaceNote}`,
124
+ factSlug: result.factSlug,
125
+ };
126
+ }
127
+
128
+ // Conclusion writer — validate pending drafts (deterministic checks)
129
+ if (task.slug.includes('validate') || task.category === 'research-acquisition') {
130
+ const { runConclusionWriter } = await import('./conclusion-writer.mjs');
131
+ const result = await runConclusionWriter({
132
+ inboxDir,
133
+ vaultDir: VAULT_DIR,
134
+ quarantineDir: path.join(BRAIN_DIR, 'memory-inbox', 'quarantine'),
135
+ });
136
+ if (result.skipped) return { success: true, output: `Conclusion writer: ${result.skipped}` };
137
+ return {
138
+ success: true,
139
+ output: `Validated ${result.processed} drafts: ${result.approved} approved, ${result.rejected} rejected`,
140
+ };
141
+ }
142
+
143
+ return { success: true, output: 'No-op research task' };
144
+ }
145
+
146
+ // ─── Maintenance Engine (Deterministic Only) ────────────────────────────────────
147
+
148
+ async function runMaintenanceTask(task) {
149
+ // Lease vacuuming — purely filesystem, no LLM
150
+ try {
151
+ const leasesDir = path.join(BRAIN_DIR, 'leases');
152
+ if (fs.existsSync(leasesDir)) {
153
+ const workspaces = fs.readdirSync(leasesDir);
154
+ for (const ws of workspaces) {
155
+ const wsDir = path.join(leasesDir, ws);
156
+ if (!fs.statSync(wsDir).isDirectory()) continue;
157
+ const files = fs.readdirSync(wsDir);
158
+ for (const file of files) {
159
+ if (!file.endsWith('.lease.json')) continue;
160
+ const fp = path.join(wsDir, file);
161
+ try {
162
+ const lease = JSON.parse(fs.readFileSync(fp, 'utf8'));
163
+ if (new Date(lease.expires_at) < new Date()) {
164
+ fs.unlinkSync(fp);
165
+ logger.info({ subsystem: 'daemon-loop', message: `Vacuumed expired lease: ${fp}` });
166
+ }
167
+ } catch {
168
+ try { fs.unlinkSync(fp); } catch {}
169
+ }
170
+ }
171
+ }
172
+ }
173
+ } catch (err) {
174
+ logger.info({ subsystem: 'daemon-loop', message: `Lease vacuuming failed: ${err.message}` });
175
+ }
176
+
177
+ return { success: true, output: 'Maintenance complete' };
178
+ }
179
+
180
+ // ─── Interrupt Writer ───────────────────────────────────────────────────────────
181
+
182
+ /**
183
+ * Write an interrupt to the SKILL.md file so the agent picks it up on next turn.
184
+ * Replaces the old interrupts/pending.md approach.
185
+ */
186
+ export function writeInterrupt(message) {
187
+ const skillMdPath = path.join(SKILLS_DIR, 'total-recall', 'SKILL.md');
188
+ if (!fs.existsSync(skillMdPath)) return;
189
+
190
+ const content = fs.readFileSync(skillMdPath, 'utf8');
191
+ const marker = '<!-- daemon writes below this line, agent clears after reading -->';
192
+ const idx = content.indexOf(marker);
193
+ if (idx === -1) return;
194
+
195
+ const insertPoint = idx + marker.length;
196
+ const timestamp = new Date().toISOString();
197
+ const interrupt = `\n🔔 **[${timestamp}]** ${message}`;
198
+
199
+ const updated = content.slice(0, insertPoint) + interrupt + content.slice(insertPoint);
200
+ fs.writeFileSync(skillMdPath, updated);
201
+
202
+ logger.info({ subsystem: 'daemon-loop', message: `Interrupt written to SKILL.md: ${message}` });
203
+ }
204
+
205
+ // ─── Main Daemon Loop ───────────────────────────────────────────────────────────
206
+
207
+ let taskCount = 0;
208
+ let running = true;
209
+
210
+ process.on('SIGTERM', () => {
211
+ logger.info({ subsystem: 'daemon-loop', message: 'SIGTERM received — shutting down gracefully' });
212
+ running = false;
213
+ });
214
+
215
+ process.on('SIGINT', () => {
216
+ logger.info({ subsystem: 'daemon-loop', message: 'SIGINT received — shutting down gracefully' });
217
+ running = false;
218
+ });
219
+
220
+ async function main() {
221
+ logger.info({
222
+ subsystem: 'daemon-loop',
223
+ message: `Deterministic Daemon starting. Vault: ${VAULT_DIR}`,
224
+ });
225
+
226
+ // Initial session ingest
227
+ try {
228
+ const ingestResult = scanAndIngest(SESSIONS_DIR);
229
+ logger.info({
230
+ subsystem: 'daemon-loop',
231
+ message: `Boot ingest: ${ingestResult.ingested} new sessions`,
232
+ });
233
+ } catch (err) {
234
+ logger.info({ subsystem: 'daemon-loop', message: `Boot ingest failed: ${err.message}` });
235
+ }
236
+
237
+ while (running) {
238
+ try {
239
+ // Periodically recompile surfaces
240
+ if (taskCount > 0 && taskCount % RECOMPILE_EVERY_N_TASKS === 0) {
241
+ logger.info({ subsystem: 'daemon-loop', message: 'Running scheduled surface recompile...' });
242
+ try {
243
+ const { compileSurface } = await import('./surface.mjs');
244
+ await compileSurface({
245
+ vaultDir: VAULT_DIR,
246
+ skillsDir: SKILLS_DIR,
247
+ derivedDir: DERIVED_DIR,
248
+ instructionsFile: INSTRUCTIONS_FILE,
249
+ });
250
+ } catch (err) {
251
+ logger.info({ subsystem: 'daemon-loop', message: `Surface recompile error: ${err.message}` });
252
+ }
253
+ }
254
+
255
+ // Refresh scheduler from disk on each iteration
256
+ const scheduler = createScheduler({
257
+ queueDir: QUEUE_DIR,
258
+ vaultDir: VAULT_DIR,
259
+ sessionsDir: SESSIONS_DIR,
260
+ });
261
+
262
+ const { task, source } = scheduler.next();
263
+ taskCount++;
264
+
265
+ logger.info({
266
+ subsystem: 'daemon-loop',
267
+ message: `Task #${taskCount} [${source}] ${task.category}: ${task.slug}`,
268
+ });
269
+
270
+ // Mark in-progress
271
+ if ((source === 'explicit' || source === 'idle') && task._filepath) {
272
+ try {
273
+ updateTaskStatus(task, 'in-progress', QUEUE_DIR);
274
+ } catch (statusErr) {
275
+ logger.info({
276
+ subsystem: 'daemon-loop',
277
+ message: `Failed to mark task in-progress (non-fatal): ${statusErr.message}`,
278
+ });
279
+ }
280
+ }
281
+ if (task._research_id) {
282
+ try {
283
+ updateQueueItem(task._research_id, { status: 'in_progress' });
284
+ } catch (err) {
285
+ logger.error({
286
+ subsystem: 'daemon-loop',
287
+ message: `Failed to update research queue status: ${err.message}`,
288
+ });
289
+ }
290
+ }
291
+
292
+ const result = await dispatchTask(task);
293
+
294
+ // Mark complete
295
+ if ((source === 'explicit' || source === 'idle') && task._filepath) {
296
+ try {
297
+ updateTaskStatus(task, result.success ? 'completed' : 'failed', QUEUE_DIR);
298
+ } catch (statusErr) {
299
+ logger.info({
300
+ subsystem: 'daemon-loop',
301
+ message: `Failed to mark task complete (non-fatal): ${statusErr.message}`,
302
+ });
303
+ }
304
+ }
305
+ if (task._research_id) {
306
+ try {
307
+ const patch = {
308
+ status: result.success ? 'pending' : 'failed',
309
+ notes: result.output || result.error || null,
310
+ };
311
+ if (result.factSlug) {
312
+ patch.node_slug = result.factSlug;
313
+ }
314
+ // Only advance through acquisition phase — skip LLM phases
315
+ if (result.success && task._research_phase === 'acquisition' && result.factSlug) {
316
+ // Research stays in acquisition — no LLM phases to advance to
317
+ patch.status = 'done';
318
+ }
319
+ updateQueueItem(task._research_id, patch);
320
+ } catch (err) {
321
+ logger.error({
322
+ subsystem: 'daemon-loop',
323
+ message: `Failed to update research queue status: ${err.message}`,
324
+ });
325
+ }
326
+ }
327
+
328
+ logger.info({
329
+ subsystem: 'daemon-loop',
330
+ message: `Task #${taskCount} done: ${result.output || result.error || 'ok'}`,
331
+ });
332
+
333
+ // Throttle to protect local resources
334
+ await new Promise(r => setTimeout(r, TASK_SLEEP_MS));
335
+
336
+ } catch (loopErr) {
337
+ // Individual task failures must NEVER kill the daemon.
338
+ logger.info({
339
+ subsystem: 'daemon-loop',
340
+ message: `Task loop iteration crashed (non-fatal, continuing): ${loopErr.message}`,
341
+ });
342
+ await new Promise(r => setTimeout(r, 1000));
343
+ }
344
+ }
345
+
346
+ logger.info({ subsystem: 'daemon-loop', message: 'Deterministic Daemon stopped.' });
347
+ }
348
+
349
+ main().catch(async (err) => {
350
+ logger.info({ subsystem: 'daemon-loop', message: `Fatal error: ${err.message}` });
351
+
352
+ try {
353
+ const { writeEmergencyAlert } = await import('./emergency-alerts.mjs');
354
+ writeEmergencyAlert(
355
+ `The Daemon has CRASHED with a fatal error: ${err.message}. ` +
356
+ `No background automation is running. ` +
357
+ `Restart with: node bin/total-recall.mjs daemon start`
358
+ );
359
+ } catch {
360
+ // If even the alert system fails, we still exit
361
+ }
362
+
363
+ process.exit(1);
364
+ });