total-recall-brain 3.13.0 → 3.13.2

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 (152) hide show
  1. package/README.md +91 -108
  2. package/bin/antigravity.mjs +81 -52
  3. package/bin/total-recall.mjs +32 -57
  4. package/extension/background.js +1 -1
  5. package/extension/options/options.js +9 -3
  6. package/extension/sidepanel/sidepanel.js +0 -2
  7. package/frontend/index.html +4 -3
  8. package/frontend/package.json +1 -0
  9. package/frontend/src/App.tsx +109 -152
  10. package/frontend/src/api.ts +605 -23
  11. package/frontend/src/components/BrainSelector.tsx +211 -199
  12. package/frontend/src/components/DocumentEditorModal.tsx +57 -0
  13. package/frontend/src/components/DocumentTable.tsx +47 -0
  14. package/frontend/src/components/Graph3D.tsx +120 -71
  15. package/frontend/src/components/UsageChart.tsx +3 -3
  16. package/frontend/src/components/brand/BrandMark.tsx +95 -0
  17. package/frontend/src/index.css +286 -86
  18. package/frontend/src/pages/ApiKeysPage.tsx +1878 -215
  19. package/frontend/src/pages/ChatPage.tsx +103 -45
  20. package/frontend/src/pages/DesignDocsPage.tsx +5 -5
  21. package/frontend/src/pages/FilesPage.tsx +10 -6
  22. package/frontend/src/pages/GraphPage.tsx +5 -3
  23. package/frontend/src/pages/HelpPage.tsx +2 -2
  24. package/frontend/src/pages/InboxPage.tsx +82 -0
  25. package/frontend/src/pages/InstructionsPage.tsx +1 -1
  26. package/frontend/src/pages/IntegrationsPage.tsx +21 -10
  27. package/frontend/src/pages/LoginPage.tsx +95 -77
  28. package/frontend/src/pages/MemoryPage.tsx +31 -8
  29. package/frontend/src/pages/OnboardingPage.tsx +690 -0
  30. package/frontend/src/pages/OpenWikiPage.tsx +412 -323
  31. package/frontend/src/pages/SettingsPage.tsx +825 -922
  32. package/frontend/src/pages/SkillsPage.tsx +272 -147
  33. package/frontend/src/pages/UsagePage.tsx +23 -31
  34. package/frontend/src/pages/VaultPage.tsx +223 -0
  35. package/frontend/src/types.ts +13 -0
  36. package/package.json +4 -4
  37. package/scaffold/.agent/skills/total-recall/SKILL.md +43 -4
  38. package/scaffold/.agent/skills/total-recall/modules/README.md +10 -0
  39. package/scaffold/.agent/skills/total-recall/modules/agents/README.md +5 -0
  40. package/scaffold/.agent/skills/total-recall/{skills/tr-cli-agents → modules/agents}/agents.yml +8 -0
  41. package/scaffold/.agent/skills/total-recall/modules/skill-deploy/README.md +9 -0
  42. package/scaffold/.agent/skills/total-recall/openwiki/README.md +10 -0
  43. package/scaffold/.agent/skills/total-recall/openwiki/architecture.md +5 -0
  44. package/scaffold/.agent/skills/total-recall/openwiki/ide.md +10 -0
  45. package/scaffold/.agent/skills/total-recall/openwiki/memory.md +11 -0
  46. package/scaffold/.agent/skills/total-recall/openwiki/secrets.md +7 -0
  47. package/scaffold/.agent/skills/total-recall/openwiki/skills.md +7 -0
  48. package/src/cli/brain.mjs +63 -5
  49. package/src/cli/connect.mjs +11 -19
  50. package/src/cli/daemon.mjs +5 -4
  51. package/src/cli/deploy-ui.mjs +8 -3
  52. package/src/cli/deploy.mjs +84 -27
  53. package/src/cli/ingest/index.mjs +1 -1
  54. package/src/cli/init.mjs +63 -34
  55. package/src/cli/remember.mjs +3 -3
  56. package/src/cli/secret.mjs +812 -0
  57. package/src/cli/setup.mjs +34 -7
  58. package/src/cli/share.mjs +2 -2
  59. package/src/cli/skill.mjs +396 -35
  60. package/src/cli/sync.mjs +4 -4
  61. package/src/cli/task.mjs +287 -0
  62. package/src/core/clarity-rewriter.mjs +5 -5
  63. package/src/core/config.mjs +44 -2
  64. package/src/core/conflict-detector.mjs +5 -5
  65. package/src/core/crons.mjs +54 -0
  66. package/src/core/daemon-loop.mjs +87 -235
  67. package/src/core/dream.mjs +170 -10
  68. package/src/core/env-import.mjs +456 -0
  69. package/src/core/fact-seeker.mjs +14 -14
  70. package/src/core/import-rules.mjs +20 -7
  71. package/src/core/inference-engine.mjs +3 -3
  72. package/src/core/migrate.mjs +14 -8
  73. package/src/core/okf-adapter.mjs +2 -2
  74. package/src/core/operation-validator.mjs +39 -331
  75. package/src/core/optimizer.mjs +7 -0
  76. package/src/core/post-mortem.mjs +2 -2
  77. package/src/core/project-brain.mjs +349 -0
  78. package/src/core/provider-catalog.mjs +296 -0
  79. package/src/core/quick-capture.mjs +1 -1
  80. package/src/core/remote-vault-sync.mjs +194 -0
  81. package/src/core/research-queue.mjs +7 -11
  82. package/src/core/runtime.mjs +33 -9
  83. package/src/core/scheduler.mjs +59 -5
  84. package/src/core/schema.mjs +14 -0
  85. package/src/core/search.mjs +1 -1
  86. package/src/core/secrets-env-export.mjs +323 -0
  87. package/src/core/secrets-rotate.mjs +245 -0
  88. package/src/core/secrets-store.mjs +645 -0
  89. package/src/core/semantic-index.mjs +3 -3
  90. package/src/core/skills-registry.mjs +1062 -0
  91. package/src/core/source-adapters.mjs +4 -1
  92. package/src/core/ssss-host-extension.mjs +94 -0
  93. package/src/core/ssss-kernel-bridge.mjs +530 -0
  94. package/src/core/surface.mjs +14 -0
  95. package/src/core/task-envelope.mjs +298 -0
  96. package/src/core/task-executors.mjs +600 -0
  97. package/src/core/validated-write.mjs +124 -56
  98. package/src/core/vault.mjs +21 -34
  99. package/src/core/webauthn-store.mjs +272 -0
  100. package/src/server/api.mjs +26 -16
  101. package/src/server/auth.mjs +51 -0
  102. package/src/server/index.mjs +3 -3
  103. package/src/server/keys.mjs +30 -4
  104. package/src/server/rest.mjs +198 -25
  105. package/src/server/route-manifest.json +156 -0
  106. package/src/server/routes/_shared.mjs +40 -1
  107. package/src/server/routes/docs.mjs +260 -0
  108. package/src/server/routes/memory.mjs +145 -32
  109. package/src/server/routes/research.mjs +30 -0
  110. package/src/server/routes/secrets.mjs +470 -0
  111. package/src/server/routes/share.mjs +2 -2
  112. package/src/server/routes/skills.mjs +452 -54
  113. package/src/server/routes/sync.mjs +48 -0
  114. package/src/server/routes/webauthn.mjs +184 -0
  115. package/src/server/update-route-manifest.mjs +39 -0
  116. package/templates/onboarding-interview.md +32 -22
  117. package/templates/openwiki/README.md +10 -0
  118. package/templates/openwiki/architecture.md +5 -0
  119. package/templates/openwiki/ide.md +10 -0
  120. package/templates/openwiki/memory.md +11 -0
  121. package/templates/openwiki/secrets.md +7 -0
  122. package/templates/openwiki/skills.md +7 -0
  123. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/SKILL.md +0 -121
  124. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/antigravity.txt +0 -20
  125. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/claude.txt +0 -58
  126. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/codex.txt +0 -81
  127. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/gemini.txt +0 -44
  128. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/scripts/.gitkeep +0 -0
  129. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/subagents/.gitkeep +0 -0
  130. package/scaffold/.agent/skills/total-recall/skills/tr-research/SKILL.md +0 -338
  131. package/scaffold/.agent/skills/total-recall/skills/tr-skill/SKILL.md +0 -282
  132. package/scaffold/.agent/skills/total-recall/skills/tr-skill/assets/skill-template/SKILL.md +0 -20
  133. package/scaffold/.agent/skills/total-recall/skills/tr-skill/evals/evals.json +0 -20
  134. package/scaffold/.agent/skills/total-recall/skills/tr-skill/hooks/on-skill-add.sh +0 -14
  135. package/scaffold/.agent/skills/total-recall/skills/tr-skill/hooks/on-skill-delete.sh +0 -14
  136. package/scaffold/.agent/skills/total-recall/skills/tr-skill/learnings/.gitkeep +0 -0
  137. package/scaffold/.agent/skills/total-recall/skills/tr-skill/references/.gitkeep +0 -0
  138. package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/.gitkeep +0 -0
  139. package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/create-skill.sh +0 -65
  140. package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/enforce-skill-optimization.mjs +0 -135
  141. package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/skill-watcher.mjs +0 -64
  142. package/scaffold/.agent/skills/total-recall/skills/tr-skill/subagents/audit-all-skills.md +0 -62
  143. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/SKILL.md +0 -298
  144. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/evals/evals.json +0 -17
  145. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/references/CHANGELOG.md +0 -124
  146. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/references/ssss-spec.md +0 -1325
  147. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/scripts/validate-schema.mjs +0 -232
  148. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/subagents/audit-ssss-node.md +0 -9
  149. /package/scaffold/.agent/skills/total-recall/{skills/tr-cli-agents/evals → modules/skill-deploy/scripts}/.gitkeep +0 -0
  150. /package/scaffold/.agent/skills/total-recall/{skills/tr-skill → modules/skill-deploy}/scripts/find-skills.mjs +0 -0
  151. /package/scaffold/.agent/skills/total-recall/{skills/tr-skill → modules/skill-deploy}/scripts/install-skill.mjs +0 -0
  152. /package/scaffold/.agent/skills/total-recall/{skills/tr-skill → modules/skill-deploy}/scripts/scan-skill.mjs +0 -0
@@ -1,26 +1,25 @@
1
1
  /**
2
- * Total Recall Active Intelligence Daemon Loop — v2 (Deterministic)
2
+ * Total Recall Active Intelligence Daemon Loop — v3
3
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.
4
+ * Durable worker over the open task envelope queue.
5
+ * - Dispatches via executor registry (fail-loud on unknown)
6
+ * - Does NOT invent idle work unless TR_IDLE_TASKS=1
7
+ * - Periodically runs dream (system memory consolidation)
8
+ * - Ingests sessions on boot; recompiles surfaces every N ticks
6
9
  *
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.
10
+ * Spawned by `total-recall daemon start`.
14
11
  */
15
12
 
16
13
  import path from 'path';
17
14
  import fs from 'fs';
18
- import { createScheduler, updateTaskStatus } from './scheduler.mjs';
15
+ import { createScheduler, updateTaskStatus, persistTaskToDisk } from './scheduler.mjs';
19
16
  import { scanAndIngest } from './session-watcher.mjs';
20
17
  import { logger } from './logger.mjs';
21
- import { updateQueueItem, loadQueue } from './research-queue.mjs';
22
- import { execFileSync } from "node:child_process";
18
+ import { updateQueueItem } from './research-queue.mjs';
23
19
  import { agentDir, brainDir } from './config.mjs';
20
+ import { runCrons } from './crons.mjs';
21
+ import { dispatchTask as registryDispatch } from './task-executors.mjs';
22
+ import { buildTaskEnvelope } from './task-envelope.mjs';
24
23
 
25
24
  const AGENT_DIR = agentDir;
26
25
  const BRAIN_DIR = brainDir;
@@ -35,84 +34,32 @@ const INSTRUCTIONS_FILE = path.join(BRAIN_DIR, 'INSTRUCTIONS.md');
35
34
  // How often to recompile surfaces (every N task ticks)
36
35
  const RECOMPILE_EVERY_N_TASKS = 20;
37
36
 
38
- // Pause between task iterations (ms)
37
+ // Empty-queue sleep (ms) — longer when idle so we don't spin
39
38
  const TASK_SLEEP_MS = 10000;
40
-
41
- // ─── Tasks are no longer skipped to conserve resources. We use CLI agents! ───
39
+ const EMPTY_SLEEP_MS = 30000;
40
+
41
+ // Run a system dream every N empty ticks (memory consolidation without busy-work)
42
+ const DREAM_EVERY_N_EMPTY = 20;
43
+
44
+ function executorContext(runtimeConfig) {
45
+ return {
46
+ brainDir: BRAIN_DIR,
47
+ vaultDir: VAULT_DIR,
48
+ skillsDir: SKILLS_DIR,
49
+ derivedDir: DERIVED_DIR,
50
+ sessionsDir: SESSIONS_DIR,
51
+ queueDir: QUEUE_DIR,
52
+ conflictsDir: CONFLICTS_DIR,
53
+ instructionsFile: INSTRUCTIONS_FILE,
54
+ runtimeConfig,
55
+ };
56
+ }
42
57
 
43
58
  async function dispatchTask(task) {
44
- const category = task.category;
45
-
46
59
  try {
47
60
  const { loadRuntimeConfig } = await import('./runtime.mjs');
48
61
  const runtimeConfig = await loadRuntimeConfig();
49
-
50
- // ─── Research & Proactive Tasks (LLM + Fetching) ───
51
- if (task.slug.startsWith('research-') || category === 'proactive-research' || category === 'research-acquisition') {
52
- if (task.slug.startsWith('staleness-check-')) {
53
- const { runStalenessCheck } = await import('./clarity-rewriter.mjs');
54
- const result = await runStalenessCheck(task.target, { vaultDir: VAULT_DIR, queueDir: QUEUE_DIR, runtimeConfig });
55
- return { success: true, output: `Staleness check complete: ${result.verdict} (conf: ${result.confidence})` };
56
- }
57
- return await runResearchTask(task, runtimeConfig);
58
- }
59
-
60
- // ─── System 2 Inference ───
61
- if (category === 'system2-deliberation') {
62
- const { runInferenceTask } = await import('./inference-engine.mjs');
63
- const slugs = task.target ? task.target.split(',') : [];
64
- const result = await runInferenceTask(slugs, {
65
- vaultDir: VAULT_DIR,
66
- inboxDir: path.join(BRAIN_DIR, 'memory-inbox', 'pending'),
67
- runtimeConfig
68
- });
69
- return { success: true, output: `Inference complete: ${result.conclusions?.length || 0} conclusions` };
70
- }
71
-
72
- // ─── Cutoff Audit ───
73
- if (category === 'cutoff-audit' || task.slug.startsWith('cutoff-audit-')) {
74
- const { runCutoffAudit } = await import('./clarity-rewriter.mjs');
75
- const result = await runCutoffAudit({ vaultDir: VAULT_DIR, queueDir: QUEUE_DIR, runtimeConfig });
76
- return { success: true, output: `Cutoff audit complete: ${result.audited} audited, ${result.flagged} flagged, ${result.critical} critical` };
77
- }
78
-
79
- // ─── Self Diagnosis & Conscious Enforcement ───
80
- if (category === 'conscious-enforcement' || task.slug.includes('self-diagnosis')) {
81
- if (task.slug.startsWith('post-mortem-')) {
82
- const { runPostMortem } = await import('./post-mortem.mjs');
83
- const sessionPath = path.join(brainDir, 'sessions', task.target);
84
- const result = await runPostMortem(sessionPath, { vaultDir: VAULT_DIR, inboxDir: path.join(BRAIN_DIR, 'memory-inbox', 'pending'), runtimeConfig });
85
- return { success: true, output: `Post-mortem complete: ${result.nodesCreated || 0} nodes extracted` };
86
- }
87
-
88
- if (task.slug.includes('self-diagnosis')) {
89
- const { runSelfDiagnosis } = await import('./fact-seeker.mjs');
90
- await runSelfDiagnosis({ vaultDir: VAULT_DIR, runtimeConfig });
91
- return { success: true, output: `Self-diagnosis complete` };
92
- }
93
- return { success: true, output: `Conscious enforcement handled` };
94
- }
95
-
96
- // ─── Memory Maintenance ───
97
- if (category === 'memory-maintenance') {
98
- if (task.slug.startsWith('clarity-review-')) {
99
- const { runClarityReview } = await import('./clarity-rewriter.mjs');
100
- const result = await runClarityReview(task.target, { vaultDir: VAULT_DIR, inboxDir: path.join(BRAIN_DIR, 'memory-inbox', 'pending'), runtimeConfig });
101
- return { success: true, output: `Clarity review complete: rewrote=${result.rewrote}` };
102
- }
103
- if (task.slug.startsWith('memory-compaction-')) {
104
- const { runMemoryCompaction } = await import('./fact-seeker.mjs');
105
- const result = await runMemoryCompaction({ vaultDir: VAULT_DIR, inboxDir: path.join(BRAIN_DIR, 'memory-inbox', 'pending'), runtimeConfig });
106
- return { success: true, output: `Memory compaction complete. Consolidated nodes: ${result.consolidatedCount || 0}` };
107
- }
108
- return await runMaintenanceTask(task);
109
- }
110
-
111
- logger.info({
112
- subsystem: 'daemon-loop',
113
- message: `Skipping unhandled task category: ${task.slug} [${category}]`,
114
- });
115
- return { success: true, output: 'Skipped unhandled task' };
62
+ return await registryDispatch(task, executorContext(runtimeConfig));
116
63
  } catch (err) {
117
64
  logger.info({
118
65
  subsystem: 'daemon-loop',
@@ -122,137 +69,6 @@ async function dispatchTask(task) {
122
69
  }
123
70
  }
124
71
 
125
- // ─── Research Engine (Web Fetching Only — No LLM Synthesis) ─────────────────────
126
-
127
- async function runResearchTask(task, runtimeConfig) {
128
- const inboxDir = path.join(BRAIN_DIR, 'memory-inbox', 'pending');
129
-
130
- // Phase 1: Knowledge acquisition
131
- if (task.slug.startsWith('research-acquisition-') || task.category === 'proactive-research' || task.slug.includes('fact-seeker') || task.slug.includes('knowledge-acquisition')) {
132
- const { runKnowledgeAcquisitionCycle } = await import('./fact-seeker.mjs');
133
- const forceTopic = task.target || null;
134
- const result = await runKnowledgeAcquisitionCycle({
135
- vaultDir: VAULT_DIR,
136
- inboxDir,
137
- queueDir: QUEUE_DIR,
138
- forceTopic,
139
- skillsDir: SKILLS_DIR,
140
- derivedDir: DERIVED_DIR,
141
- instructionsFile: INSTRUCTIONS_FILE,
142
- runtimeConfig
143
- });
144
- if (result.skipped) return { success: true, output: `Knowledge acquisition: ${result.skipped}` };
145
- const surfaceNote = result.surfaced ? ' [SURFACED]' : ' [staged]';
146
- return {
147
- success: true,
148
- output: `Researched "${result.topic}": ${result.sources?.join(', ')} | confidence: ${result.confidence || 'n/a'} | slug: ${result.factSlug}${surfaceNote}`,
149
- factSlug: result.factSlug,
150
- };
151
- }
152
-
153
- // Phase 2: Deliberation
154
- if (task.slug.startsWith('research-deliberation-')) {
155
- const { runResearchDeliberationCycle } = await import('./fact-seeker.mjs');
156
- const result = await runResearchDeliberationCycle({
157
- vaultDir: VAULT_DIR,
158
- nodeSlug: task._node_slug || 'pending',
159
- topic: task.title || task.target || 'Unknown Topic',
160
- runtimeConfig
161
- });
162
- return { success: true, output: `Deliberation complete: ${result.slug || 'done'}` };
163
- }
164
-
165
- // Phase 3: Improvement
166
- if (task.slug.startsWith('research-improvement-')) {
167
- const { runResearchImprovementCycle } = await import('./fact-seeker.mjs');
168
- const result = await runResearchImprovementCycle({
169
- vaultDir: VAULT_DIR,
170
- nodeSlug: task._node_slug || 'pending',
171
- topic: task.title || task.target || 'Unknown Topic',
172
- runtimeConfig
173
- });
174
- return { success: true, output: `Improvement complete` };
175
- }
176
-
177
- // Phase 4: Monitoring
178
- if (task.slug.startsWith('research-monitoring-')) {
179
- const { runResearchMonitoringCycle } = await import('./fact-seeker.mjs');
180
- const result = await runResearchMonitoringCycle({
181
- vaultDir: VAULT_DIR,
182
- nodeSlug: task._node_slug || 'pending',
183
- topic: task.title || task.target || 'Unknown Topic',
184
- runtimeConfig,
185
- skillsDir: SKILLS_DIR,
186
- derivedDir: DERIVED_DIR,
187
- instructionsFile: INSTRUCTIONS_FILE
188
- });
189
- return { success: true, output: `Monitoring complete` };
190
- }
191
-
192
- // Phase 5: Expansion
193
- if (task.slug.startsWith('research-expansion-')) {
194
- const { runResearchExpansionCycle } = await import('./fact-seeker.mjs');
195
- const result = await runResearchExpansionCycle({
196
- vaultDir: VAULT_DIR,
197
- nodeSlug: task._node_slug || 'pending',
198
- topic: task.title || task.target || 'Unknown Topic',
199
- runtimeConfig
200
- });
201
- return { success: true, output: `Expansion complete` };
202
- }
203
-
204
- // Conclusion writer — validate pending drafts (deterministic checks)
205
- if (task.slug.includes('validate')) {
206
- const { runConclusionWriter } = await import('./conclusion-writer.mjs');
207
- const result = await runConclusionWriter({
208
- inboxDir,
209
- vaultDir: VAULT_DIR,
210
- quarantineDir: path.join(BRAIN_DIR, 'memory-inbox', 'quarantine'),
211
- });
212
- if (result.skipped) return { success: true, output: `Conclusion writer: ${result.skipped}` };
213
- return {
214
- success: true,
215
- output: `Validated ${result.processed} drafts: ${result.approved} approved, ${result.rejected} rejected`,
216
- };
217
- }
218
-
219
- return { success: true, output: 'No-op research task' };
220
- }
221
-
222
- // ─── Maintenance Engine (Deterministic Only) ────────────────────────────────────
223
-
224
- async function runMaintenanceTask(task) {
225
- // Lease vacuuming — purely filesystem, no LLM
226
- try {
227
- const leasesDir = path.join(BRAIN_DIR, 'leases');
228
- if (fs.existsSync(leasesDir)) {
229
- const workspaces = fs.readdirSync(leasesDir);
230
- for (const ws of workspaces) {
231
- const wsDir = path.join(leasesDir, ws);
232
- if (!fs.statSync(wsDir).isDirectory()) continue;
233
- const files = fs.readdirSync(wsDir);
234
- for (const file of files) {
235
- if (!file.endsWith('.lease.json')) continue;
236
- const fp = path.join(wsDir, file);
237
- try {
238
- const lease = JSON.parse(fs.readFileSync(fp, 'utf8'));
239
- if (new Date(lease.expires_at) < new Date()) {
240
- fs.unlinkSync(fp);
241
- logger.info({ subsystem: 'daemon-loop', message: `Vacuumed expired lease: ${fp}` });
242
- }
243
- } catch {
244
- try { fs.unlinkSync(fp); } catch {}
245
- }
246
- }
247
- }
248
- }
249
- } catch (err) {
250
- logger.info({ subsystem: 'daemon-loop', message: `Lease vacuuming failed: ${err.message}` });
251
- }
252
-
253
- return { success: true, output: 'Maintenance complete' };
254
- }
255
-
256
72
  // ─── Interrupt Writer ───────────────────────────────────────────────────────────
257
73
 
258
74
  /**
@@ -304,7 +120,7 @@ process.on('unhandledRejection', (reason, promise) => {
304
120
  async function main() {
305
121
  logger.info({
306
122
  subsystem: 'daemon-loop',
307
- message: `Deterministic Daemon starting. Vault: ${VAULT_DIR}`,
123
+ message: `Daemon starting (open task envelope). Vault: ${VAULT_DIR} idle=${process.env.TR_IDLE_TASKS === '1' ? 'on' : 'off'}`,
308
124
  });
309
125
 
310
126
  // Initial session ingest
@@ -318,6 +134,8 @@ async function main() {
318
134
  logger.info({ subsystem: 'daemon-loop', message: `Boot ingest failed: ${err.message}` });
319
135
  }
320
136
 
137
+ let emptyTicks = 0;
138
+
321
139
  while (running) {
322
140
  try {
323
141
  // Periodically recompile surfaces
@@ -335,7 +153,6 @@ async function main() {
335
153
  logger.info({ subsystem: 'daemon-loop', message: `Surface recompile error: ${err.message}` });
336
154
  }
337
155
 
338
- // Compact any append logs with accumulated dirty entries
339
156
  try {
340
157
  const { compactAppendLogs } = await import('./append-log.mjs');
341
158
  const compactResult = compactAppendLogs();
@@ -345,25 +162,63 @@ async function main() {
345
162
  } catch (err) {
346
163
  logger.info({ subsystem: 'daemon-loop', message: `Append log compaction error: ${err.message}` });
347
164
  }
165
+
166
+ try {
167
+ await runCrons({ vaultDir: VAULT_DIR, skillsDir: SKILLS_DIR, brainDir: BRAIN_DIR });
168
+ } catch (err) {
169
+ logger.error({ subsystem: 'daemon-loop', message: `Cron execution failed: ${err.message}` });
170
+ }
348
171
  }
349
172
 
350
- // Refresh scheduler from disk on each iteration
351
173
  const scheduler = createScheduler({
352
174
  queueDir: QUEUE_DIR,
353
175
  vaultDir: VAULT_DIR,
354
176
  sessionsDir: SESSIONS_DIR,
355
177
  });
356
178
 
357
- const { task, source } = scheduler.next();
179
+ let { task, source } = scheduler.next();
180
+
181
+ // Empty queue: optional system dream, then sleep (no busy-work invention)
182
+ if (!task) {
183
+ emptyTicks++;
184
+ if (emptyTicks % DREAM_EVERY_N_EMPTY === 0) {
185
+ const dreamEnv = buildTaskEnvelope({
186
+ intent: 'Scheduled system dream cycle',
187
+ slug: `dream-system-${Date.now().toString(36)}`,
188
+ kind: 'system',
189
+ executor: 'dream',
190
+ category: 'memory-maintenance',
191
+ priority: 70,
192
+ system: true,
193
+ origin: { agent: 'daemon', created_by: 'daemon-system' },
194
+ capabilities: ['vault:read', 'vault:write'],
195
+ });
196
+ task = persistTaskToDisk(dreamEnv, QUEUE_DIR);
197
+ source = 'system';
198
+ logger.info({
199
+ subsystem: 'daemon-loop',
200
+ message: `Empty queue → enqueue system dream (${emptyTicks} empty ticks)`,
201
+ });
202
+ } else {
203
+ logger.info({
204
+ subsystem: 'daemon-loop',
205
+ message: `Queue empty (tick ${emptyTicks}) — sleeping ${EMPTY_SLEEP_MS}ms`,
206
+ });
207
+ await new Promise((r) => setTimeout(r, EMPTY_SLEEP_MS));
208
+ continue;
209
+ }
210
+ } else {
211
+ emptyTicks = 0;
212
+ }
213
+
358
214
  taskCount++;
359
215
 
360
216
  logger.info({
361
217
  subsystem: 'daemon-loop',
362
- message: `Task #${taskCount} [${source}] ${task.category}: ${task.slug}`,
218
+ message: `Task #${taskCount} [${source}] ${task.category || task.kind}: ${task.slug}`,
363
219
  });
364
220
 
365
- // Mark in-progress
366
- if ((source === 'explicit' || source === 'idle') && task._filepath) {
221
+ if ((source === 'explicit' || source === 'idle' || source === 'system') && task._filepath) {
367
222
  try {
368
223
  updateTaskStatus(task, 'in-progress', QUEUE_DIR);
369
224
  } catch (statusErr) {
@@ -386,22 +241,23 @@ async function main() {
386
241
 
387
242
  const result = await dispatchTask(task);
388
243
 
389
- // Mark complete
390
- if ((source === 'explicit' || source === 'idle') && task._filepath) {
244
+ if ((source === 'explicit' || source === 'idle' || source === 'system') && task._filepath) {
391
245
  try {
392
246
  if (result.skippedLLM) {
393
247
  updateTaskStatus(task, 'pending', QUEUE_DIR);
394
248
  } else if (result.success) {
395
249
  updateTaskStatus(task, 'completed', QUEUE_DIR);
396
250
  } else {
397
- // Dead Letter Queue / Retry Logic
398
251
  task.retry_count = (task.retry_count || 0) + 1;
252
+ task.last_error = result.error || 'Unknown error';
399
253
  if (task.retry_count >= 3) {
400
- logger.info({ subsystem: 'daemon-loop', message: `Task ${task.slug} failed 3 times. Moving to DLQ (failed).` });
401
- updateTaskStatus(task, 'failed', QUEUE_DIR);
254
+ logger.info({ subsystem: 'daemon-loop', message: `Task ${task.slug} failed ${task.retry_count} times. Moving to DLQ (failed). Last error: ${task.last_error}` });
255
+ updateTaskStatus(task, 'failed', QUEUE_DIR, result.error || 'Unknown error');
402
256
  } else {
403
- logger.info({ subsystem: 'daemon-loop', message: `Task ${task.slug} failed. Retry ${task.retry_count}/3. Requeueing.` });
404
- updateTaskStatus(task, 'pending', QUEUE_DIR);
257
+ const backoffMs = Math.min(Math.pow(2, task.retry_count) * 1000, 60000);
258
+ logger.info({ subsystem: 'daemon-loop', message: `Task ${task.slug} failed. Retry ${task.retry_count}/3 after ${backoffMs}ms backoff. Error: ${task.last_error}` });
259
+ updateTaskStatus(task, 'pending', QUEUE_DIR, result.error || 'Unknown error');
260
+ await new Promise(r => setTimeout(r, backoffMs));
405
261
  }
406
262
  }
407
263
  } catch (statusErr) {
@@ -415,13 +271,11 @@ async function main() {
415
271
  try {
416
272
  const patch = {
417
273
  status: result.success ? 'pending' : 'failed',
418
- // Append the output rather than completely overwriting the original rationale
419
274
  notes: result.output ? `Phase output: ${result.output}` : (result.error || null),
420
275
  };
421
276
  if (result.factSlug) {
422
277
  patch.node_slug = result.factSlug;
423
278
  }
424
- // If the task was skipped because it requires an LLM, DO NOT complete the research project.
425
279
  if (result.skippedLLM) {
426
280
  patch.status = 'pending';
427
281
  } else if (result.success) {
@@ -460,11 +314,9 @@ async function main() {
460
314
  message: `Task #${taskCount} done: ${result.output || result.error || 'ok'}`,
461
315
  });
462
316
 
463
- // Throttle to protect local resources
464
317
  await new Promise(r => setTimeout(r, TASK_SLEEP_MS));
465
318
 
466
319
  } catch (loopErr) {
467
- // Individual task failures must NEVER kill the daemon.
468
320
  logger.info({
469
321
  subsystem: 'daemon-loop',
470
322
  message: `Task loop iteration crashed (non-fatal, continuing): ${loopErr.message}`,
@@ -473,7 +325,7 @@ async function main() {
473
325
  }
474
326
  }
475
327
 
476
- logger.info({ subsystem: 'daemon-loop', message: 'Deterministic Daemon stopped.' });
328
+ logger.info({ subsystem: 'daemon-loop', message: 'Daemon stopped.' });
477
329
  }
478
330
 
479
331
  main().catch(async (err) => {
@@ -1,6 +1,8 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import os from 'os';
4
+ import crypto from 'crypto';
5
+ import matter from 'gray-matter';
4
6
  import { writeNode, atomicWrite, walkMd } from './vault.mjs';
5
7
  import { getNodes } from './vault-cache.mjs';
6
8
  import { logger } from './logger.mjs';
@@ -231,6 +233,139 @@ export function scanModifiedVault(vaultDir, sinceHours = 24) {
231
233
  });
232
234
  }
233
235
 
236
+ /**
237
+ * Load REM candidates from memory-inbox (pending + capture drafts).
238
+ */
239
+ export function loadCandidatesFromInbox(brainDirPath) {
240
+ const candidates = [];
241
+ const roots = [
242
+ path.join(brainDirPath, 'memory-inbox', 'pending'),
243
+ path.join(brainDirPath, 'memory-inbox', 'capture'),
244
+ ];
245
+
246
+ for (const dir of roots) {
247
+ if (!fs.existsSync(dir)) continue;
248
+ let files = [];
249
+ try {
250
+ files = fs.readdirSync(dir).filter((f) => f.endsWith('.md'));
251
+ } catch {
252
+ continue;
253
+ }
254
+ for (const file of files) {
255
+ try {
256
+ const raw = fs.readFileSync(path.join(dir, file), 'utf8');
257
+ const { data, content } = matter(raw);
258
+ if (!data || (!data.slug && !data.title && !String(content || '').trim())) continue;
259
+ const status = data.status || 'draft';
260
+ if (status === 'archived' || status === 'active') continue;
261
+ candidates.push({
262
+ ...data,
263
+ slug: data.slug || path.basename(file, '.md'),
264
+ category: data.category || 'facts',
265
+ title: data.title || data.slug || file,
266
+ status: 'draft',
267
+ confidence: data.confidence ?? 0.55,
268
+ importance: data.importance ?? 2,
269
+ evidence_count: data.evidence_count || data.source?.evidence_count || 1,
270
+ body: content,
271
+ _inbox_path: path.join(dir, file),
272
+ });
273
+ } catch (err) {
274
+ logger.warn('dream', `Inbox candidate skip ${file}: ${err.message}`);
275
+ }
276
+ }
277
+ }
278
+ return candidates;
279
+ }
280
+
281
+ /**
282
+ * Lightweight session → candidate extraction (deterministic heuristics).
283
+ * User lines that signal durable preferences/rules become draft candidates.
284
+ */
285
+ export function loadCandidatesFromSessions(sessionsDir, { maxFiles = 8, maxCandidates = 20 } = {}) {
286
+ const candidates = [];
287
+ if (!fs.existsSync(sessionsDir)) return candidates;
288
+
289
+ const signal =
290
+ /\b(always|never|prefer|remember|must not|must|invariant|don't|do not|from now on)\b/i;
291
+
292
+ let files = [];
293
+ try {
294
+ files = fs
295
+ .readdirSync(sessionsDir)
296
+ .filter((f) => f.endsWith('.jsonl'))
297
+ .map((f) => ({
298
+ name: f,
299
+ mtime: fs.statSync(path.join(sessionsDir, f)).mtimeMs,
300
+ }))
301
+ .sort((a, b) => b.mtime - a.mtime)
302
+ .slice(0, maxFiles);
303
+ } catch {
304
+ return candidates;
305
+ }
306
+
307
+ for (const file of files) {
308
+ let lines;
309
+ try {
310
+ lines = fs.readFileSync(path.join(sessionsDir, file.name), 'utf8').split('\n').filter(Boolean);
311
+ } catch {
312
+ continue;
313
+ }
314
+ for (const line of lines) {
315
+ if (candidates.length >= maxCandidates) return candidates;
316
+ let entry;
317
+ try {
318
+ entry = JSON.parse(line);
319
+ } catch {
320
+ continue;
321
+ }
322
+ const content = entry.content || entry.text || '';
323
+ if (!content || content.length < 24 || content.length > 2000) continue;
324
+ if (entry.type === 'tool_call') continue;
325
+ const role = entry.role;
326
+ if (role && role !== 'user' && role !== 'human') continue;
327
+ if (!signal.test(content)) continue;
328
+
329
+ const hash = crypto.createHash('sha256').update(content).digest('hex').slice(0, 10);
330
+ candidates.push({
331
+ slug: `session-extract-${hash}`,
332
+ category: 'preferences',
333
+ title: content.slice(0, 80).replace(/\s+/g, ' '),
334
+ status: 'draft',
335
+ confidence: 0.55,
336
+ importance: 3,
337
+ evidence_count: 1,
338
+ modality: /must not|never|don't|do not/i.test(content) ? 'must_not' : 'should',
339
+ subject: 'user',
340
+ predicate: 'prefers',
341
+ object: 'behavior',
342
+ tags: ['session-extract', 'dream-rem'],
343
+ source: {
344
+ type: 'session',
345
+ session_id: path.basename(file.name, '.jsonl'),
346
+ evidence_count: 1,
347
+ },
348
+ body: content,
349
+ });
350
+ }
351
+ }
352
+ return candidates;
353
+ }
354
+
355
+ /**
356
+ * Collect all REM candidates (inbox + sessions). Dedupe by slug.
357
+ */
358
+ export function collectRemCandidates({ brainDirPath, sessionsDir }) {
359
+ const bySlug = new Map();
360
+ for (const c of loadCandidatesFromInbox(brainDirPath)) {
361
+ bySlug.set(c.slug, c);
362
+ }
363
+ for (const c of loadCandidatesFromSessions(sessionsDir)) {
364
+ if (!bySlug.has(c.slug)) bySlug.set(c.slug, c);
365
+ }
366
+ return [...bySlug.values()];
367
+ }
368
+
234
369
  /**
235
370
  * Phase 2: REM Sleep. Score nodes, check conflicts, promote/decay.
236
371
  */
@@ -285,17 +420,43 @@ export async function runDreamCycle({
285
420
  const modified = scanModifiedVault(vaultDir);
286
421
  logger.info('dream', `Modified vault files: ${modified.length}`);
287
422
 
288
- // In a real system, we'd extract candidates from sessions. For now, we simulate.
289
- const candidates = [];
423
+ const candidates = collectRemCandidates({
424
+ brainDirPath: brainDir,
425
+ sessionsDir,
426
+ });
427
+ logger.info('dream', `REM candidates collected: ${candidates.length}`);
290
428
 
291
429
  logger.info('dream', 'PHASE 2 — REM (Pattern Recognition)');
292
430
  const existingNodes = getNodes(vaultDir);
293
-
294
- if (candidates.length > 0) {
295
- const { promoted, conflicted } = evaluateCandidates(candidates, existingNodes, conflictsDir);
431
+ const existingSlugs = new Set(existingNodes.map((n) => n.slug));
432
+ // Skip candidates already active in vault
433
+ const fresh = candidates.filter((c) => !existingSlugs.has(c.slug) || c.status === 'draft');
434
+
435
+ let promotedCount = 0;
436
+ let conflictedCount = 0;
437
+ if (fresh.length > 0) {
438
+ const { promoted, conflicted } = evaluateCandidates(fresh, existingNodes, conflictsDir);
439
+ promotedCount = promoted.length;
440
+ conflictedCount = conflicted.length;
296
441
  logger.info('dream', `Promoted: ${promoted.length} | Conflicts: ${conflicted.length}`);
297
442
  for (const node of promoted) {
298
- writeNode(node, vaultDir);
443
+ try {
444
+ // Strip inbox-only fields before vault write
445
+ const { body, _inbox_path, ...nodeFields } = node;
446
+ const toWrite = {
447
+ ...nodeFields,
448
+ status: 'active',
449
+ content: body || nodeFields.content,
450
+ };
451
+ await writeNode(toWrite, vaultDir);
452
+ if (_inbox_path && fs.existsSync(_inbox_path)) {
453
+ try {
454
+ fs.unlinkSync(_inbox_path);
455
+ } catch {}
456
+ }
457
+ } catch (err) {
458
+ logger.warn('dream', `Promote write failed for ${node.slug}: ${err.message}`);
459
+ }
299
460
  }
300
461
  } else {
301
462
  logger.info('dream', 'No new candidates to evaluate.');
@@ -312,9 +473,6 @@ export async function runDreamCycle({
312
473
  logger.info('dream', 'PHASE 4 — Lucid Dreaming (Optimizer Proposals)');
313
474
  let proposalCount = 0;
314
475
  try {
315
- const proposalsDir = path.join(vaultDir, 'proposals');
316
- if (!fs.existsSync(proposalsDir)) fs.mkdirSync(proposalsDir, { recursive: true });
317
-
318
476
  const cleanupProposals = await generateMemoryCleanupProposals(vaultDir);
319
477
  const staleProposals = await generateStaleKnowledgeRefreshProposals(vaultDir);
320
478
  const allProposals = [...cleanupProposals, ...staleProposals];
@@ -324,7 +482,8 @@ export async function runDreamCycle({
324
482
  for (const p of allProposals) {
325
483
  const passed = await evaluateProposalGate(p, null);
326
484
  logger.info('dream', `Proposal [${p.category}]: ${p.summary} -> ${passed ? 'ACCEPTED' : 'REJECTED'}`);
327
- writeNode(p, proposalsDir);
485
+ // Contract write under vault root → proposals/<slug>.md
486
+ await writeNode(p, vaultDir);
328
487
  if (passed) proposalCount++;
329
488
  }
330
489
  } else {
@@ -339,6 +498,7 @@ export async function runDreamCycle({
339
498
  const existingNodes = getNodes(vaultDir);
340
499
  writeDailyNote(vaultDir, [
341
500
  `Modified vault files scanned: ${modified.length}`,
501
+ `REM candidates: ${candidates.length} (promoted ${promotedCount}, conflicted ${conflictedCount})`,
342
502
  `Active nodes: ${existingNodes.filter(n => n.status === 'active').length}`,
343
503
  `Proposals accepted: ${proposalCount}`,
344
504
  ]);