@psiclawops/hypermem 0.1.0 → 0.5.1

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 (153) hide show
  1. package/ARCHITECTURE.md +4 -3
  2. package/README.md +457 -174
  3. package/dist/background-indexer.d.ts +19 -4
  4. package/dist/background-indexer.d.ts.map +1 -1
  5. package/dist/background-indexer.js +329 -17
  6. package/dist/cache.d.ts +110 -0
  7. package/dist/cache.d.ts.map +1 -0
  8. package/dist/cache.js +495 -0
  9. package/dist/compaction-fence.d.ts +1 -1
  10. package/dist/compaction-fence.js +1 -1
  11. package/dist/compositor.d.ts +114 -27
  12. package/dist/compositor.d.ts.map +1 -1
  13. package/dist/compositor.js +1678 -229
  14. package/dist/content-type-classifier.d.ts +41 -0
  15. package/dist/content-type-classifier.d.ts.map +1 -0
  16. package/dist/content-type-classifier.js +181 -0
  17. package/dist/cross-agent.d.ts +5 -0
  18. package/dist/cross-agent.d.ts.map +1 -1
  19. package/dist/cross-agent.js +5 -0
  20. package/dist/db.d.ts +1 -1
  21. package/dist/db.d.ts.map +1 -1
  22. package/dist/db.js +6 -2
  23. package/dist/desired-state-store.d.ts +1 -1
  24. package/dist/desired-state-store.d.ts.map +1 -1
  25. package/dist/desired-state-store.js +15 -5
  26. package/dist/doc-chunk-store.d.ts +26 -1
  27. package/dist/doc-chunk-store.d.ts.map +1 -1
  28. package/dist/doc-chunk-store.js +114 -1
  29. package/dist/doc-chunker.d.ts +1 -1
  30. package/dist/doc-chunker.js +1 -1
  31. package/dist/dreaming-promoter.d.ts +86 -0
  32. package/dist/dreaming-promoter.d.ts.map +1 -0
  33. package/dist/dreaming-promoter.js +381 -0
  34. package/dist/episode-store.d.ts +2 -1
  35. package/dist/episode-store.d.ts.map +1 -1
  36. package/dist/episode-store.js +4 -4
  37. package/dist/fact-store.d.ts +19 -1
  38. package/dist/fact-store.d.ts.map +1 -1
  39. package/dist/fact-store.js +64 -3
  40. package/dist/fleet-store.d.ts +1 -1
  41. package/dist/fleet-store.js +1 -1
  42. package/dist/fos-mod.d.ts +178 -0
  43. package/dist/fos-mod.d.ts.map +1 -0
  44. package/dist/fos-mod.js +416 -0
  45. package/dist/hybrid-retrieval.d.ts +5 -1
  46. package/dist/hybrid-retrieval.d.ts.map +1 -1
  47. package/dist/hybrid-retrieval.js +7 -3
  48. package/dist/image-eviction.d.ts +49 -0
  49. package/dist/image-eviction.d.ts.map +1 -0
  50. package/dist/image-eviction.js +251 -0
  51. package/dist/index.d.ts +50 -11
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +73 -43
  54. package/dist/keystone-scorer.d.ts +51 -0
  55. package/dist/keystone-scorer.d.ts.map +1 -0
  56. package/dist/keystone-scorer.js +52 -0
  57. package/dist/knowledge-graph.d.ts +1 -1
  58. package/dist/knowledge-graph.js +1 -1
  59. package/dist/knowledge-lint.d.ts +29 -0
  60. package/dist/knowledge-lint.d.ts.map +1 -0
  61. package/dist/knowledge-lint.js +116 -0
  62. package/dist/knowledge-store.d.ts +1 -1
  63. package/dist/knowledge-store.d.ts.map +1 -1
  64. package/dist/knowledge-store.js +8 -2
  65. package/dist/library-schema.d.ts +3 -3
  66. package/dist/library-schema.d.ts.map +1 -1
  67. package/dist/library-schema.js +324 -3
  68. package/dist/message-store.d.ts +15 -2
  69. package/dist/message-store.d.ts.map +1 -1
  70. package/dist/message-store.js +51 -1
  71. package/dist/metrics-dashboard.d.ts +114 -0
  72. package/dist/metrics-dashboard.d.ts.map +1 -0
  73. package/dist/metrics-dashboard.js +260 -0
  74. package/dist/obsidian-exporter.d.ts +57 -0
  75. package/dist/obsidian-exporter.d.ts.map +1 -0
  76. package/dist/obsidian-exporter.js +274 -0
  77. package/dist/obsidian-watcher.d.ts +147 -0
  78. package/dist/obsidian-watcher.d.ts.map +1 -0
  79. package/dist/obsidian-watcher.js +403 -0
  80. package/dist/open-domain.d.ts +46 -0
  81. package/dist/open-domain.d.ts.map +1 -0
  82. package/dist/open-domain.js +125 -0
  83. package/dist/preference-store.d.ts +1 -1
  84. package/dist/preference-store.js +1 -1
  85. package/dist/preservation-gate.d.ts +1 -1
  86. package/dist/preservation-gate.js +1 -1
  87. package/dist/proactive-pass.d.ts +63 -0
  88. package/dist/proactive-pass.d.ts.map +1 -0
  89. package/dist/proactive-pass.js +239 -0
  90. package/dist/profiles.d.ts +44 -0
  91. package/dist/profiles.d.ts.map +1 -0
  92. package/dist/profiles.js +227 -0
  93. package/dist/provider-translator.d.ts +13 -3
  94. package/dist/provider-translator.d.ts.map +1 -1
  95. package/dist/provider-translator.js +63 -9
  96. package/dist/rate-limiter.d.ts +1 -1
  97. package/dist/rate-limiter.js +1 -1
  98. package/dist/repair-tool-pairs.d.ts +38 -0
  99. package/dist/repair-tool-pairs.d.ts.map +1 -0
  100. package/dist/repair-tool-pairs.js +138 -0
  101. package/dist/retrieval-policy.d.ts +51 -0
  102. package/dist/retrieval-policy.d.ts.map +1 -0
  103. package/dist/retrieval-policy.js +77 -0
  104. package/dist/schema.d.ts +2 -2
  105. package/dist/schema.d.ts.map +1 -1
  106. package/dist/schema.js +28 -2
  107. package/dist/secret-scanner.d.ts +1 -1
  108. package/dist/secret-scanner.js +1 -1
  109. package/dist/seed.d.ts +2 -2
  110. package/dist/seed.js +2 -2
  111. package/dist/session-flusher.d.ts +53 -0
  112. package/dist/session-flusher.d.ts.map +1 -0
  113. package/dist/session-flusher.js +69 -0
  114. package/dist/session-topic-map.d.ts +41 -0
  115. package/dist/session-topic-map.d.ts.map +1 -0
  116. package/dist/session-topic-map.js +77 -0
  117. package/dist/spawn-context.d.ts +54 -0
  118. package/dist/spawn-context.d.ts.map +1 -0
  119. package/dist/spawn-context.js +159 -0
  120. package/dist/system-store.d.ts +1 -1
  121. package/dist/system-store.js +1 -1
  122. package/dist/temporal-store.d.ts +80 -0
  123. package/dist/temporal-store.d.ts.map +1 -0
  124. package/dist/temporal-store.js +149 -0
  125. package/dist/topic-detector.d.ts +35 -0
  126. package/dist/topic-detector.d.ts.map +1 -0
  127. package/dist/topic-detector.js +249 -0
  128. package/dist/topic-store.d.ts +1 -1
  129. package/dist/topic-store.js +1 -1
  130. package/dist/topic-synthesizer.d.ts +51 -0
  131. package/dist/topic-synthesizer.d.ts.map +1 -0
  132. package/dist/topic-synthesizer.js +315 -0
  133. package/dist/trigger-registry.d.ts +63 -0
  134. package/dist/trigger-registry.d.ts.map +1 -0
  135. package/dist/trigger-registry.js +163 -0
  136. package/dist/types.d.ts +214 -10
  137. package/dist/types.d.ts.map +1 -1
  138. package/dist/types.js +1 -1
  139. package/dist/vector-store.d.ts +43 -5
  140. package/dist/vector-store.d.ts.map +1 -1
  141. package/dist/vector-store.js +189 -10
  142. package/dist/version.d.ts +34 -0
  143. package/dist/version.d.ts.map +1 -0
  144. package/dist/version.js +34 -0
  145. package/dist/wiki-page-emitter.d.ts +65 -0
  146. package/dist/wiki-page-emitter.d.ts.map +1 -0
  147. package/dist/wiki-page-emitter.js +258 -0
  148. package/dist/work-store.d.ts +1 -1
  149. package/dist/work-store.js +1 -1
  150. package/package.json +15 -5
  151. package/dist/redis.d.ts +0 -188
  152. package/dist/redis.d.ts.map +0 -1
  153. package/dist/redis.js +0 -534
@@ -0,0 +1,381 @@
1
+ /**
2
+ * dreaming-promoter.ts
3
+ *
4
+ * hypermem-native dreaming promotion pass.
5
+ *
6
+ * Unlike the stock memory-core dreaming feature (which appends raw content to
7
+ * MEMORY.md), this promoter generates pointer-format entries that match the
8
+ * council's MEMORY.md convention:
9
+ *
10
+ * - **{domain} — {title}:** {summary}
11
+ * → `memory_search("{query}")`
12
+ *
13
+ * Scoring uses confidence, decay, recency, and domain cluster weight.
14
+ * Dedup prevents re-promoting topics already covered by existing pointers.
15
+ *
16
+ * Dry-run mode returns what would be written without modifying any files.
17
+ */
18
+ import * as fs from 'node:fs/promises';
19
+ import * as path from 'node:path';
20
+ import * as os from 'node:os';
21
+ export const DEFAULT_DREAMER_CONFIG = {
22
+ enabled: false,
23
+ minScore: 0.75,
24
+ minConfidence: 0.70,
25
+ maxPromotionsPerRun: 5,
26
+ tickInterval: 12,
27
+ dryRun: false,
28
+ recencyHalfLifeDays: 7,
29
+ maxAgeDays: 30,
30
+ };
31
+ // ─── Workspace path resolution ───────────────────────────────────────────────
32
+ /**
33
+ * Resolve the workspace directory for an agent.
34
+ * Council agents live at ~/.openclaw/workspace-council/{agentId}/
35
+ * Other agents at ~/.openclaw/workspace/{agentId}/
36
+ */
37
+ export async function resolveAgentWorkspacePath(agentId) {
38
+ const home = os.homedir();
39
+ const councilPath = path.join(home, '.openclaw', 'workspace-council', agentId);
40
+ const workspacePath = path.join(home, '.openclaw', 'workspace', agentId);
41
+ try {
42
+ await fs.access(councilPath);
43
+ return councilPath;
44
+ }
45
+ catch {
46
+ try {
47
+ await fs.access(workspacePath);
48
+ return workspacePath;
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ }
54
+ }
55
+ // ─── Scoring ─────────────────────────────────────────────────────────────────
56
+ /**
57
+ * Composite promotion score.
58
+ *
59
+ * score = confidence_factor × recency_factor × quality_factor
60
+ *
61
+ * confidence_factor: 0..1 — the raw confidence, penalized by decay
62
+ * recency_factor: 0..1 — exponential decay from age (half-life = config)
63
+ * quality_factor: 0..1 — length/richness proxy (bonus for medium-length facts)
64
+ */
65
+ function scoreCandidate(fact, config) {
66
+ const confidenceFactor = fact.confidence * (1 - fact.decayScore * 0.5);
67
+ const halfLife = config.recencyHalfLifeDays;
68
+ const recencyFactor = Math.exp(-(Math.LN2 / halfLife) * fact.ageDays);
69
+ // Quality: medium-length facts (80–180 chars) score best. Very long or very
70
+ // short get penalized — they're likely fragments or noisy captures.
71
+ const len = fact.content.length;
72
+ const qualityFactor = len < 60 ? 0.6 : len > 220 ? 0.75 : 1.0;
73
+ return confidenceFactor * recencyFactor * qualityFactor;
74
+ }
75
+ // ─── Pointer generation ───────────────────────────────────────────────────────
76
+ /**
77
+ * Extract a concise title and search query from fact content.
78
+ * Avoids NLP — purely string heuristics, fast and deterministic.
79
+ */
80
+ function extractPointerMeta(content, domain) {
81
+ // Clean up trailing code artifacts
82
+ const cleaned = content
83
+ .replace(/['"]\s*\).*$/, '') // strip trailing '); or '),
84
+ .replace(/',\s*tool_calls:.*$/, '') // strip tool_calls artifacts
85
+ .replace(/…$/, '') // strip ellipsis
86
+ .replace(/\s+/g, ' ')
87
+ .trim();
88
+ // Title: first meaningful clause (up to first period, comma, or 50 chars)
89
+ let title = cleaned.split(/[.,;:]/)[0].trim();
90
+ if (title.length > 55)
91
+ title = title.slice(0, 52) + '…';
92
+ // Query: leading noun phrase — first 6-8 significant words, excluding articles/prepositions
93
+ const stopWords = new Set([
94
+ 'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
95
+ 'to', 'of', 'in', 'on', 'at', 'by', 'for', 'with', 'as', 'it', 'its',
96
+ 'this', 'that', 'we', 'i', 'you', 'they', 'and', 'or', 'but', 'so',
97
+ ]);
98
+ const words = cleaned
99
+ .split(/\s+/)
100
+ .map(w => w.replace(/[^a-zA-Z0-9_-]/g, '').toLowerCase())
101
+ .filter(w => w.length > 2 && !stopWords.has(w));
102
+ const queryWords = words.slice(0, 7);
103
+ // Prepend domain as context anchor so search hits the right agent scope
104
+ const query = `${domain} ${queryWords.join(' ')}`.slice(0, 60).trim();
105
+ return { title, query };
106
+ }
107
+ /**
108
+ * Format a promoted fact as a MEMORY.md pointer entry.
109
+ */
110
+ function formatPointer(domain, title, summary, query) {
111
+ // Capitalize domain label for display
112
+ const domainLabel = domain.charAt(0).toUpperCase() + domain.slice(1);
113
+ return `- **${domainLabel} — ${title}:** ${summary}\n → \`memory_search("${query}")\``;
114
+ }
115
+ // ─── Dedup ───────────────────────────────────────────────────────────────────
116
+ /**
117
+ * Parse existing memory_search() calls from MEMORY.md content.
118
+ * Returns a set of normalized query strings already indexed.
119
+ */
120
+ function parseExistingPointers(memoryContent) {
121
+ const existing = new Set();
122
+ const re = /memory_search\("([^"]+)"\)/g;
123
+ let m;
124
+ while ((m = re.exec(memoryContent)) !== null) {
125
+ existing.add(m[1].toLowerCase().trim());
126
+ }
127
+ return existing;
128
+ }
129
+ /**
130
+ * Check if a proposed query overlaps significantly with any existing pointer.
131
+ * Uses word-level Jaccard similarity (threshold 0.4).
132
+ */
133
+ function isDuplicatePointer(proposedQuery, existing) {
134
+ if (existing.has(proposedQuery.toLowerCase().trim()))
135
+ return true;
136
+ const proposedWords = new Set(proposedQuery.toLowerCase().split(/\s+/).filter(w => w.length > 2));
137
+ if (proposedWords.size === 0)
138
+ return false;
139
+ for (const existingQuery of existing) {
140
+ const existingWords = new Set(existingQuery.split(/\s+/).filter(w => w.length > 2));
141
+ const intersection = [...proposedWords].filter(w => existingWords.has(w)).length;
142
+ const union = new Set([...proposedWords, ...existingWords]).size;
143
+ const jaccard = union > 0 ? intersection / union : 0;
144
+ if (jaccard >= 0.4)
145
+ return true;
146
+ }
147
+ return false;
148
+ }
149
+ // ─── MEMORY.md write ─────────────────────────────────────────────────────────
150
+ const PROMOTED_SECTION_HEADER = '## Promoted Facts';
151
+ const PROMOTED_SECTION_MARKER = '<!-- hypermem:dreaming-promoted -->';
152
+ /**
153
+ * Append promoted pointer entries to MEMORY.md.
154
+ * Writes into an existing "## Promoted Facts" section, or appends one.
155
+ * Non-destructive: only appends, never rewrites existing content.
156
+ */
157
+ async function appendToMemoryFile(memoryPath, entries) {
158
+ if (entries.length === 0)
159
+ return;
160
+ let existing = '';
161
+ try {
162
+ existing = await fs.readFile(memoryPath, 'utf-8');
163
+ }
164
+ catch {
165
+ // File doesn't exist — start fresh (unlikely but handle gracefully)
166
+ existing = `# MEMORY.md\n\n_This is an index, not a store. Use \`memory_search\` for full context on any topic._\n\n`;
167
+ }
168
+ const newLines = entries.map(e => e.pointer).join('\n');
169
+ const timestamp = new Date().toISOString().slice(0, 10);
170
+ if (existing.includes(PROMOTED_SECTION_HEADER)) {
171
+ // Inject after the section header line
172
+ const headerIdx = existing.indexOf(PROMOTED_SECTION_HEADER);
173
+ const afterHeader = existing.indexOf('\n', headerIdx) + 1;
174
+ const insertPoint = afterHeader;
175
+ const updated = existing.slice(0, insertPoint) +
176
+ `<!-- promoted ${timestamp} -->\n${newLines}\n` +
177
+ existing.slice(insertPoint);
178
+ await fs.writeFile(memoryPath, updated, 'utf-8');
179
+ }
180
+ else {
181
+ // Append a new section at end of file
182
+ const appendBlock = `\n${PROMOTED_SECTION_HEADER}\n${PROMOTED_SECTION_MARKER}\n` +
183
+ `<!-- promoted ${timestamp} -->\n${newLines}\n`;
184
+ await fs.writeFile(memoryPath, existing.trimEnd() + '\n' + appendBlock, 'utf-8');
185
+ }
186
+ }
187
+ // ─── Promotion-time content filter ─────────────────────────────────────────
188
+ /**
189
+ * Reject facts that are clearly noise at promotion time.
190
+ * A second line of defense — the indexer's isQualityFact() is the primary filter,
191
+ * but legacy noise in the DB (pre-TUNE-013) still needs to be caught here.
192
+ */
193
+ function isPromotable(content) {
194
+ // Multi-line content — reject both actual newlines AND escaped \n sequences
195
+ // (some facts stored pre-TUNE-013 have literal \n in the string value)
196
+ if (content.includes('\n') || content.includes('\\n'))
197
+ return false;
198
+ // External content markers
199
+ if (content.includes('EXTERNAL_UNTRUSTED_CONTENT') || content.includes('<<<'))
200
+ return false;
201
+ // Markdown heading fragments
202
+ if (/^#{1,4}\s/.test(content))
203
+ return false;
204
+ // Code/tool artifacts
205
+ if (/tool_calls:|\)\s*[;,]\s*$|',\s*$/.test(content))
206
+ return false;
207
+ // Escaped newlines embedded in content (stored as literal \n in DB)
208
+ if (/\\n/.test(content))
209
+ return false;
210
+ // URL-heavy (external research/docs, not fleet knowledge)
211
+ const urlCount = (content.match(/https?:\/\//g) || []).length;
212
+ if (urlCount >= 2)
213
+ return false;
214
+ // Fragment starts — no subject (lowercase/article/conjunction lead-ins)
215
+ if (/^(and |or |but |to |in |of |the |a |an |by |for |at |on |with |\d+\.|\* |\- )/.test(content.trim()))
216
+ return false;
217
+ // Context-free references: starts with a possessive/pronoun referencing something outside
218
+ if (/^(it |its |this |that |these |those |they |their |we |our |i |my )/.test(content.toLowerCase().trim()))
219
+ return false;
220
+ // External research noise: benchmark/tool comparison content
221
+ if (/\b(LOCOMO|LoCoMo|LangMem|SuperMemory|Zep|Honcho|Mem0)\b/i.test(content) &&
222
+ /\b(study|benchmark|dataset|employed|researchers|similarity|retrieval|accuracy)\b/i.test(content))
223
+ return false;
224
+ // Apache license boilerplate
225
+ if (/AS IS.*BASIS|distributed under the License/i.test(content))
226
+ return false;
227
+ // Pure CLI output or file path patterns
228
+ if (/^(git |npm |node |python3? |bash |echo |cat |ls |curl )/.test(content.trim()))
229
+ return false;
230
+ // Templated TODO/example patterns (from docs, not real fleet state)
231
+ if (/TODO:|implement Z|capture\.py/.test(content))
232
+ return false;
233
+ // Must start with a capital letter or number (complete sentences only)
234
+ if (!/^[A-Z0-9]/.test(content.trim()))
235
+ return false;
236
+ // Minimum meaningful length: 50 chars as promotion floor (stricter than indexer's 40)
237
+ if (content.trim().length < 50)
238
+ return false;
239
+ return true;
240
+ }
241
+ /**
242
+ * Run the dreaming promotion pass for a single agent.
243
+ *
244
+ * Reads qualified facts from library.db, scores them, deduplicates against
245
+ * existing MEMORY.md pointers, and writes new pointer entries.
246
+ */
247
+ export async function runDreamingPromoter(agentId, libraryDb, config = {}) {
248
+ const cfg = { ...DEFAULT_DREAMER_CONFIG, ...config };
249
+ const result = {
250
+ agentId,
251
+ candidates: 0,
252
+ promoted: 0,
253
+ skippedDuplicate: 0,
254
+ skippedThreshold: 0,
255
+ entries: [],
256
+ memoryPath: null,
257
+ dryRun: cfg.dryRun,
258
+ };
259
+ // 1. Resolve workspace path
260
+ const wsPath = await resolveAgentWorkspacePath(agentId);
261
+ if (!wsPath) {
262
+ return result; // No workspace — skip silently
263
+ }
264
+ const memoryPath = path.join(wsPath, 'MEMORY.md');
265
+ result.memoryPath = memoryPath;
266
+ // 2. Read existing MEMORY.md (for dedup)
267
+ let memoryContent = '';
268
+ try {
269
+ memoryContent = await fs.readFile(memoryPath, 'utf-8');
270
+ }
271
+ catch {
272
+ // File may not exist — will be created on first promotion
273
+ }
274
+ const existingPointers = parseExistingPointers(memoryContent);
275
+ // 3. Query candidate facts from library.db
276
+ // Criteria: active (not superseded), has domain, meets confidence floor,
277
+ // within age window, not null content
278
+ const cutoffDate = new Date(Date.now() - cfg.maxAgeDays * 86400_000).toISOString();
279
+ const rawFacts = libraryDb.prepare(`
280
+ SELECT
281
+ id,
282
+ agent_id,
283
+ domain,
284
+ content,
285
+ confidence,
286
+ decay_score,
287
+ ROUND((julianday('now') - julianday(created_at)), 2) AS age_days
288
+ FROM facts
289
+ WHERE agent_id = ?
290
+ AND superseded_by IS NULL
291
+ AND domain IS NOT NULL
292
+ AND confidence >= ?
293
+ AND created_at >= ?
294
+ AND LENGTH(content) >= 40
295
+ AND LENGTH(content) <= 300
296
+ ORDER BY confidence DESC, decay_score ASC, created_at DESC
297
+ LIMIT 200
298
+ `).all(agentId, cfg.minConfidence, cutoffDate);
299
+ result.candidates = rawFacts.length;
300
+ // 4. Score and rank
301
+ const scored = rawFacts
302
+ .filter(f => isPromotable(f.content))
303
+ .map(f => ({
304
+ id: f.id,
305
+ agentId: f.agent_id,
306
+ domain: f.domain,
307
+ content: f.content,
308
+ confidence: f.confidence,
309
+ decayScore: f.decay_score,
310
+ ageDays: f.age_days,
311
+ score: scoreCandidate({ confidence: f.confidence, decayScore: f.decay_score, ageDays: f.age_days, content: f.content }, cfg),
312
+ }));
313
+ scored.sort((a, b) => b.score - a.score);
314
+ // 5. Select up to maxPromotionsPerRun entries, with dedup
315
+ // Track which queries we've already added this run (cross-entry dedup)
316
+ const addedThisRun = new Set(existingPointers);
317
+ const toPromote = [];
318
+ for (const fact of scored) {
319
+ if (toPromote.length >= cfg.maxPromotionsPerRun)
320
+ break;
321
+ // Score threshold
322
+ if (fact.score < cfg.minScore) {
323
+ result.skippedThreshold++;
324
+ continue;
325
+ }
326
+ const { title, query } = extractPointerMeta(fact.content, fact.domain);
327
+ // Dedup check against existing pointers + already-selected entries this run
328
+ if (isDuplicatePointer(query, addedThisRun)) {
329
+ result.skippedDuplicate++;
330
+ continue;
331
+ }
332
+ // Trim summary to a clean one-liner (max 120 chars)
333
+ const rawSummary = fact.content
334
+ .replace(/['"]\s*\).*$/, '')
335
+ .replace(/\s+/g, ' ')
336
+ .trim();
337
+ const summary = rawSummary.length > 120 ? rawSummary.slice(0, 117) + '…' : rawSummary;
338
+ const pointer = formatPointer(fact.domain, title, summary, query);
339
+ const entry = {
340
+ factId: fact.id,
341
+ domain: fact.domain,
342
+ pointer,
343
+ title,
344
+ summary,
345
+ query,
346
+ score: Math.round(fact.score * 1000) / 1000,
347
+ dryRun: cfg.dryRun,
348
+ };
349
+ toPromote.push(entry);
350
+ addedThisRun.add(query.toLowerCase().trim());
351
+ }
352
+ result.entries = toPromote;
353
+ result.promoted = toPromote.length;
354
+ // 6. Write to MEMORY.md (unless dry-run)
355
+ if (!cfg.dryRun && toPromote.length > 0) {
356
+ await appendToMemoryFile(memoryPath, toPromote);
357
+ console.log(`[dreaming] Promoted ${toPromote.length} facts to ${memoryPath} ` +
358
+ `(${result.skippedDuplicate} dupes, ${result.skippedThreshold} below threshold)`);
359
+ }
360
+ return result;
361
+ }
362
+ /**
363
+ * Run the dreaming promotion pass for all agents in a fleet.
364
+ * Called from the BackgroundIndexer on every N ticks.
365
+ */
366
+ export async function runDreamingPassForFleet(agentIds, libraryDb, config = {}) {
367
+ const results = [];
368
+ for (const agentId of agentIds) {
369
+ try {
370
+ const r = await runDreamingPromoter(agentId, libraryDb, config);
371
+ if (r.promoted > 0 || r.dryRun) {
372
+ results.push(r);
373
+ }
374
+ }
375
+ catch (err) {
376
+ console.warn(`[dreaming] Failed for agent ${agentId} (non-fatal):`, err.message);
377
+ }
378
+ }
379
+ return results;
380
+ }
381
+ //# sourceMappingURL=dreaming-promoter.js.map
@@ -1,5 +1,5 @@
1
1
  /**
2
- * HyperMem Episode Store
2
+ * hypermem Episode Store
3
3
  *
4
4
  * Significant events in an agent's lifetime.
5
5
  * Lives in the central library DB.
@@ -18,6 +18,7 @@ export declare class EpisodeStore {
18
18
  visibility?: string;
19
19
  participants?: string[];
20
20
  sessionKey?: string;
21
+ sourceMessageId?: number;
21
22
  }): Episode;
22
23
  /**
23
24
  * Get recent episodes for an agent.
@@ -1 +1 @@
1
- {"version":3,"file":"episode-store.d.ts","sourceRoot":"","sources":["../src/episode-store.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAsBvD,qBAAa,YAAY;IACX,OAAO,CAAC,QAAQ,CAAC,EAAE;gBAAF,EAAE,EAAE,YAAY;IAE7C;;OAEG;IACH,MAAM,CACJ,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,WAAW,EACtB,OAAO,EAAE,MAAM,EACf,IAAI,CAAC,EAAE;QACL,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;QACxB,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,GACA,OAAO;IA0CV;;OAEG;IACH,SAAS,CACP,OAAO,EAAE,MAAM,EACf,IAAI,CAAC,EAAE;QACL,SAAS,CAAC,EAAE,WAAW,CAAC;QACxB,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,GACA,OAAO,EAAE;IA4BZ;;OAEG;IACH,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,GAAE,MAAW,GAAG,OAAO,EAAE;IAWlE;;OAEG;IACH,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,GAAE,MAAc,GAAG,MAAM;IAUzD;;OAEG;IACH,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM;IAQ9B;;OAEG;IACH,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,EAAE;CAa1D"}
1
+ {"version":3,"file":"episode-store.d.ts","sourceRoot":"","sources":["../src/episode-store.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAsBvD,qBAAa,YAAY;IACX,OAAO,CAAC,QAAQ,CAAC,EAAE;gBAAF,EAAE,EAAE,YAAY;IAE7C;;OAEG;IACH,MAAM,CACJ,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,WAAW,EACtB,OAAO,EAAE,MAAM,EACf,IAAI,CAAC,EAAE;QACL,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;QACxB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,eAAe,CAAC,EAAE,MAAM,CAAC;KAC1B,GACA,OAAO;IA2CV;;OAEG;IACH,SAAS,CACP,OAAO,EAAE,MAAM,EACf,IAAI,CAAC,EAAE;QACL,SAAS,CAAC,EAAE,WAAW,CAAC;QACxB,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,GACA,OAAO,EAAE;IA4BZ;;OAEG;IACH,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,GAAE,MAAW,GAAG,OAAO,EAAE;IAWlE;;OAEG;IACH,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,GAAE,MAAc,GAAG,MAAM;IAUzD;;OAEG;IACH,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM;IAQ9B;;OAEG;IACH,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,EAAE;CAa1D"}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * HyperMem Episode Store
2
+ * hypermem Episode Store
3
3
  *
4
4
  * Significant events in an agent's lifetime.
5
5
  * Lives in the central library DB.
@@ -42,9 +42,9 @@ export class EpisodeStore {
42
42
  }
43
43
  const result = this.db.prepare(`
44
44
  INSERT INTO episodes (agent_id, event_type, summary, significance,
45
- visibility, participants, session_key, created_at, decay_score)
46
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0.0)
47
- `).run(agentId, eventType, summary, significance, resolvedVisibility, opts?.participants ? JSON.stringify(opts.participants) : null, opts?.sessionKey || null, now);
45
+ visibility, participants, session_key, source_message_id, created_at, decay_score)
46
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0.0)
47
+ `).run(agentId, eventType, summary, significance, resolvedVisibility, opts?.participants ? JSON.stringify(opts.participants) : null, opts?.sessionKey || null, opts?.sourceMessageId ?? null, now);
48
48
  const id = Number(result.lastInsertRowid);
49
49
  return {
50
50
  id,
@@ -1,5 +1,5 @@
1
1
  /**
2
- * HyperMem Fact Store
2
+ * hypermem Fact Store
3
3
  *
4
4
  * CRUD operations for facts (extracted knowledge that spans sessions).
5
5
  * Facts live in the central library DB, tagged by agent_id.
@@ -41,6 +41,24 @@ export declare class FactStore {
41
41
  visibility?: string;
42
42
  limit?: number;
43
43
  }): Fact[];
44
+ /**
45
+ * Mark an old fact as superseded by a new one.
46
+ *
47
+ * Sets `superseded_by` on the old fact row so it is excluded from active
48
+ * retrieval queries (both FTS and KNN paths check `superseded_by IS NULL`).
49
+ * Returns false if the fact is already superseded or does not exist.
50
+ */
51
+ markSuperseded(oldFactId: number, newFactId: number): boolean;
52
+ /**
53
+ * Find the most recent active fact for an agent whose content is a near-duplicate
54
+ * of the given content (same first 100 chars, different suffix, or same domain+topic).
55
+ * Used by the background indexer to detect supersedes relationships.
56
+ *
57
+ * Returns the existing fact id if a candidate is found, otherwise null.
58
+ */
59
+ findSupersedableByContent(agentId: string, content: string, opts?: {
60
+ domain?: string;
61
+ }): number | null;
44
62
  /**
45
63
  * Decay all facts by a fixed rate.
46
64
  */
@@ -1 +1 @@
1
- {"version":3,"file":"fact-store.d.ts","sourceRoot":"","sources":["../src/fact-store.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AA0BlD,qBAAa,SAAS;IACR,OAAO,CAAC,QAAQ,CAAC,EAAE;gBAAF,EAAE,EAAE,YAAY;IAE7C;;OAEG;IACH,OAAO,CACL,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,IAAI,CAAC,EAAE;QACL,KAAK,CAAC,EAAE,SAAS,CAAC;QAClB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,GACA,IAAI;IA0DP;;OAEG;IACH,cAAc,CACZ,OAAO,EAAE,MAAM,EACf,IAAI,CAAC,EAAE;QACL,KAAK,CAAC,EAAE,SAAS,CAAC;QAClB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,aAAa,CAAC,EAAE,MAAM,CAAC;KACxB,GACA,IAAI,EAAE;IAkCT;;OAEG;IACH,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAChC,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,GAAG,IAAI,EAAE;IAoCV;;OAEG;IACH,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,GAAE,MAAa,GAAG,MAAM;IAU7D;;OAEG;IACH,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM;IAanC;;OAEG;IACH,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM;CAMtC"}
1
+ {"version":3,"file":"fact-store.d.ts","sourceRoot":"","sources":["../src/fact-store.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AA2BlD,qBAAa,SAAS;IACR,OAAO,CAAC,QAAQ,CAAC,EAAE;gBAAF,EAAE,EAAE,YAAY;IAE7C;;OAEG;IACH,OAAO,CACL,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,IAAI,CAAC,EAAE;QACL,KAAK,CAAC,EAAE,SAAS,CAAC;QAClB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,GACA,IAAI;IA4EP;;OAEG;IACH,cAAc,CACZ,OAAO,EAAE,MAAM,EACf,IAAI,CAAC,EAAE;QACL,KAAK,CAAC,EAAE,SAAS,CAAC;QAClB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,aAAa,CAAC,EAAE,MAAM,CAAC;KACxB,GACA,IAAI,EAAE;IAkCT;;OAEG;IACH,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAChC,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,GAAG,IAAI,EAAE;IAoCV;;;;;;OAMG;IACH,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO;IAY7D;;;;;;OAMG;IACH,yBAAyB,CACvB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,IAAI,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GACzB,MAAM,GAAG,IAAI;IA0BhB;;OAEG;IACH,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,GAAE,MAAa,GAAG,MAAM;IAU7D;;OAEG;IACH,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM;IAanC;;OAEG;IACH,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM;CAMtC"}
@@ -1,10 +1,11 @@
1
1
  /**
2
- * HyperMem Fact Store
2
+ * hypermem Fact Store
3
3
  *
4
4
  * CRUD operations for facts (extracted knowledge that spans sessions).
5
5
  * Facts live in the central library DB, tagged by agent_id.
6
6
  * Facts have scope (agent/session/user), confidence, and decay.
7
7
  */
8
+ import { isSafeForSharedVisibility, requiresScan } from './secret-scanner.js';
8
9
  function nowIso() {
9
10
  return new Date().toISOString();
10
11
  }
@@ -38,6 +39,20 @@ export class FactStore {
38
39
  addFact(agentId, content, opts) {
39
40
  const now = nowIso();
40
41
  const scope = opts?.scope || 'agent';
42
+ // KL-01: global scope is not yet supported — write gate is deferred to 1.0.
43
+ // Log a warning if a caller somehow passes scope='global' (e.g. direct DB
44
+ // access bypassing TypeScript types or a future FactScope addition).
45
+ if (scope === 'global') {
46
+ console.warn(`[hypermem] WARNING: agent '${agentId}' attempted to write a fact with scope='global'. ` +
47
+ `Global-scope facts are not yet gated — this write will succeed but may propagate ` +
48
+ `to all agents sharing library.db. See KL-01 in KNOWN_LIMITATIONS.md.`);
49
+ }
50
+ // Secret gate: if requested visibility is shared, verify content is clean.
51
+ // Downgrade to 'private' rather than reject — matches episode-store pattern.
52
+ let resolvedVisibility = opts?.visibility || 'private';
53
+ if (requiresScan(resolvedVisibility) && !isSafeForSharedVisibility(content)) {
54
+ resolvedVisibility = 'private';
55
+ }
41
56
  // Check for exact duplicate
42
57
  const existing = this.db.prepare(`
43
58
  SELECT * FROM facts WHERE agent_id = ? AND content = ? AND scope = ?
@@ -53,7 +68,7 @@ export class FactStore {
53
68
  visibility, source_type, source_session_key, source_ref,
54
69
  created_at, updated_at, expires_at, decay_score)
55
70
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0.0)
56
- `).run(agentId, scope, opts?.domain || null, content, opts?.confidence || 1.0, opts?.visibility || 'private', opts?.sourceType || 'conversation', opts?.sourceSessionKey || null, opts?.sourceRef || null, now, now, opts?.expiresAt || null);
71
+ `).run(agentId, scope, opts?.domain || null, content, opts?.confidence || 1.0, resolvedVisibility, opts?.sourceType || 'conversation', opts?.sourceSessionKey || null, opts?.sourceRef || null, now, now, opts?.expiresAt || null);
57
72
  const id = Number(result.lastInsertRowid);
58
73
  return {
59
74
  id,
@@ -62,7 +77,7 @@ export class FactStore {
62
77
  domain: opts?.domain || null,
63
78
  content,
64
79
  confidence: opts?.confidence || 1.0,
65
- visibility: opts?.visibility || 'private',
80
+ visibility: resolvedVisibility,
66
81
  sourceType: opts?.sourceType || 'conversation',
67
82
  sourceSessionKey: opts?.sourceSessionKey || null,
68
83
  sourceRef: opts?.sourceRef || null,
@@ -139,6 +154,52 @@ export class FactStore {
139
154
  const rows = this.db.prepare(sql).all(...params);
140
155
  return rows.map(parseFactRow);
141
156
  }
157
+ /**
158
+ * Mark an old fact as superseded by a new one.
159
+ *
160
+ * Sets `superseded_by` on the old fact row so it is excluded from active
161
+ * retrieval queries (both FTS and KNN paths check `superseded_by IS NULL`).
162
+ * Returns false if the fact is already superseded or does not exist.
163
+ */
164
+ markSuperseded(oldFactId, newFactId) {
165
+ const now = new Date().toISOString();
166
+ const result = this.db
167
+ .prepare(`
168
+ UPDATE facts
169
+ SET superseded_by = ?, updated_at = ?
170
+ WHERE id = ? AND superseded_by IS NULL
171
+ `)
172
+ .run(newFactId, now, oldFactId);
173
+ return result.changes > 0;
174
+ }
175
+ /**
176
+ * Find the most recent active fact for an agent whose content is a near-duplicate
177
+ * of the given content (same first 100 chars, different suffix, or same domain+topic).
178
+ * Used by the background indexer to detect supersedes relationships.
179
+ *
180
+ * Returns the existing fact id if a candidate is found, otherwise null.
181
+ */
182
+ findSupersedableByContent(agentId, content, opts) {
183
+ // Look for active facts from the same agent whose content starts with the
184
+ // same 60-character prefix (covers rephrased facts about the same topic).
185
+ const prefix = content.slice(0, 60);
186
+ const params = [agentId, `${prefix}%`];
187
+ let sql = `
188
+ SELECT id FROM facts
189
+ WHERE agent_id = ?
190
+ AND content LIKE ?
191
+ AND content != ?
192
+ AND superseded_by IS NULL
193
+ `;
194
+ params.push(content);
195
+ if (opts?.domain) {
196
+ sql += ' AND domain = ?';
197
+ params.push(opts.domain);
198
+ }
199
+ sql += ' ORDER BY created_at DESC LIMIT 1';
200
+ const row = this.db.prepare(sql).get(...params);
201
+ return row?.id ?? null;
202
+ }
142
203
  /**
143
204
  * Decay all facts by a fixed rate.
144
205
  */
@@ -1,5 +1,5 @@
1
1
  /**
2
- * HyperMem Fleet Registry Store
2
+ * hypermem Fleet Registry Store
3
3
  *
4
4
  * Agent roster, org structure, roles, capabilities.
5
5
  * Lives in the central library DB.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * HyperMem Fleet Registry Store
2
+ * hypermem Fleet Registry Store
3
3
  *
4
4
  * Agent roster, org structure, roles, capabilities.
5
5
  * Lives in the central library DB.