opencode-fractal-memory 0.2.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 (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +493 -0
  3. package/agent/memory-hints.md +98 -0
  4. package/agent/memory-researcher.md +56 -0
  5. package/commands/memory-auto-test.md +10 -0
  6. package/commands/memory-cache-status.md +13 -0
  7. package/commands/memory-check-context.md +4 -0
  8. package/commands/memory-compress.md +13 -0
  9. package/commands/memory-dashboard.md +23 -0
  10. package/commands/memory-delete.md +24 -0
  11. package/commands/memory-detect-topics.md +28 -0
  12. package/commands/memory-distill.md +35 -0
  13. package/commands/memory-drilldown-query.md +28 -0
  14. package/commands/memory-drilldown.md +11 -0
  15. package/commands/memory-extract-patterns.md +4 -0
  16. package/commands/memory-generate-embeddings.md +26 -0
  17. package/commands/memory-get.md +26 -0
  18. package/commands/memory-help.md +55 -0
  19. package/commands/memory-injection-feedback.md +26 -0
  20. package/commands/memory-injection-stats.md +11 -0
  21. package/commands/memory-list.md +4 -0
  22. package/commands/memory-llm-compress.md +34 -0
  23. package/commands/memory-mcp.md +20 -0
  24. package/commands/memory-prune.md +4 -0
  25. package/commands/memory-rate.md +48 -0
  26. package/commands/memory-reflect.md +37 -0
  27. package/commands/memory-replace.md +26 -0
  28. package/commands/memory-retrieve.md +34 -0
  29. package/commands/memory-search.md +28 -0
  30. package/commands/memory-session-stats.md +4 -0
  31. package/commands/memory-set.md +31 -0
  32. package/commands/memory-stats.md +11 -0
  33. package/commands/memory-summarize.md +29 -0
  34. package/commands/memory-tool-stats.md +4 -0
  35. package/commands/memory-total-tokens.md +10 -0
  36. package/commands/memory-verify.md +4 -0
  37. package/commands/memory-version.md +9 -0
  38. package/dist/cache.js +39 -0
  39. package/dist/config.js +120 -0
  40. package/dist/embeddings.js +125 -0
  41. package/dist/ensure-models.js +70 -0
  42. package/dist/file-summary.js +143 -0
  43. package/dist/frontmatter.js +28 -0
  44. package/dist/hnsw-index.js +138 -0
  45. package/dist/hooks/auto-discover.js +4 -0
  46. package/dist/hooks/auto-distill.js +120 -0
  47. package/dist/hooks/auto-retrieve/content.js +47 -0
  48. package/dist/hooks/auto-retrieve/detection.js +50 -0
  49. package/dist/hooks/auto-retrieve/formatting.js +19 -0
  50. package/dist/hooks/auto-retrieve/index.js +163 -0
  51. package/dist/hooks/auto-retrieve/scoring.js +56 -0
  52. package/dist/hooks/auto-retrieve.js +1 -0
  53. package/dist/hooks/index.js +4 -0
  54. package/dist/hooks/predictive-rating.js +87 -0
  55. package/dist/journal.js +279 -0
  56. package/dist/logging.js +147 -0
  57. package/dist/management/helpers.js +227 -0
  58. package/dist/management/router.js +48 -0
  59. package/dist/management/routes.js +197 -0
  60. package/dist/management-server.js +4 -0
  61. package/dist/management-standalone.js +31 -0
  62. package/dist/mcp/logging.js +57 -0
  63. package/dist/mcp/server.js +251 -0
  64. package/dist/mcp/transform.js +48 -0
  65. package/dist/mcp-server.js +18 -0
  66. package/dist/memory.js +2 -0
  67. package/dist/ollama.js +74 -0
  68. package/dist/plugin/hooks.js +168 -0
  69. package/dist/plugin/index.js +28 -0
  70. package/dist/plugin/init.js +109 -0
  71. package/dist/plugin/state.js +75 -0
  72. package/dist/plugin/tools.js +45 -0
  73. package/dist/plugin.js +2 -0
  74. package/dist/procedural/store.js +1 -0
  75. package/dist/procedural/types.js +1 -0
  76. package/dist/seed-nodes.js +804 -0
  77. package/dist/storage/compress-ops.js +129 -0
  78. package/dist/storage/compression/formatters.js +243 -0
  79. package/dist/storage/compression/index.js +107 -0
  80. package/dist/storage/compression/patterns.js +138 -0
  81. package/dist/storage/expiration.js +66 -0
  82. package/dist/storage/index.js +1 -0
  83. package/dist/storage/injection-events.js +82 -0
  84. package/dist/storage/lifecycle.js +65 -0
  85. package/dist/storage/maintenance.js +60 -0
  86. package/dist/storage/migrations/definitions.js +374 -0
  87. package/dist/storage/migrations/index.js +21 -0
  88. package/dist/storage/navigation.js +98 -0
  89. package/dist/storage/queries/base.js +44 -0
  90. package/dist/storage/queries/links.js +32 -0
  91. package/dist/storage/queries/nodes.js +189 -0
  92. package/dist/storage/queries/search-helpers.js +239 -0
  93. package/dist/storage/scoring.js +36 -0
  94. package/dist/storage/search.js +233 -0
  95. package/dist/storage/session-tracking.js +180 -0
  96. package/dist/storage/sqlite.js +329 -0
  97. package/dist/storage/tool-usage.js +56 -0
  98. package/dist/storage/types.js +1 -0
  99. package/dist/storage/utils.js +94 -0
  100. package/dist/tools/auto-test.js +24 -0
  101. package/dist/tools/cache-status.js +36 -0
  102. package/dist/tools/compress.js +186 -0
  103. package/dist/tools/core.js +307 -0
  104. package/dist/tools/dashboard.js +97 -0
  105. package/dist/tools/help.js +59 -0
  106. package/dist/tools/index.js +12 -0
  107. package/dist/tools/inject.js +91 -0
  108. package/dist/tools/injection-debug.js +48 -0
  109. package/dist/tools/journal.js +105 -0
  110. package/dist/tools/llm-compress.js +41 -0
  111. package/dist/tools/middle-term.js +68 -0
  112. package/dist/tools/playbook.js +64 -0
  113. package/dist/tools/reflect.js +291 -0
  114. package/dist/tools/search.js +188 -0
  115. package/dist/tools/session.js +189 -0
  116. package/dist/tools/shared.js +74 -0
  117. package/dist/tools/skill.js +37 -0
  118. package/dist/tools/stats.js +256 -0
  119. package/dist/tools/version.js +13 -0
  120. package/dist/tools.js +18 -0
  121. package/dist/utils/hybridScore.js +67 -0
  122. package/management/public/app.js +1529 -0
  123. package/management/public/index.html +486 -0
  124. package/management/public/three.min.js +6 -0
  125. package/package.json +65 -0
  126. package/scripts/download-models.ts +16 -0
  127. package/scripts/postinstall.cjs +30 -0
@@ -0,0 +1,129 @@
1
+ import { rowToNode } from "./queries/base";
2
+ import { generateEmbedding } from "../embeddings";
3
+ import { CompressionHelper, COMPRESSION_LEVELS } from "./compression";
4
+ export async function getCompressionCandidates(getDb, scope, level, maxAgeMs, force) {
5
+ const config = COMPRESSION_LEVELS[level];
6
+ if (!config)
7
+ return [];
8
+ const scopes = scope === "all" ? ["global", "project"] : [scope];
9
+ const candidates = [];
10
+ for (const s of scopes) {
11
+ const db = await getDb(s);
12
+ if (force) {
13
+ const rows = db.query("SELECT * FROM memory_nodes WHERE scope = ? AND level = ? AND length(content) > 100 AND (sticky IS NULL OR sticky = 0) ORDER BY importance DESC").all(s, level);
14
+ candidates.push(...rows.map(rowToNode));
15
+ }
16
+ else {
17
+ const threshold = maxAgeMs ?? config.maxAgeMs;
18
+ const cutoffTime = Date.now() - threshold;
19
+ const rows = db.query("SELECT * FROM memory_nodes WHERE scope = ? AND level = ? AND updated_at < ? AND length(content) > 100 AND (sticky IS NULL OR sticky = 0) ORDER BY importance DESC").all(s, level, cutoffTime);
20
+ candidates.push(...rows.map(rowToNode));
21
+ }
22
+ }
23
+ return candidates;
24
+ }
25
+ export async function runCompression(deps, scope, force, client) {
26
+ let compressed = 0;
27
+ let created = 0;
28
+ const scopes = scope === "all" ? ["global", "project"] : [scope];
29
+ for (const s of scopes) {
30
+ for (const level of [0, 1, 2, 3]) {
31
+ const config = COMPRESSION_LEVELS[level];
32
+ if (!config)
33
+ continue;
34
+ const candidates = await deps.getCompressionCandidates(s, level, undefined, force);
35
+ const clusters = CompressionHelper.findRelatedNodes(candidates, 0.3);
36
+ for (const cluster of clusters) {
37
+ const useLlm = !!client;
38
+ const summary = useLlm
39
+ ? await CompressionHelper.generateLLMSummary(cluster, client, 500)
40
+ : CompressionHelper.generateStructuredSummary(cluster);
41
+ let summaryEmbedding = null;
42
+ try {
43
+ summaryEmbedding = await generateEmbedding(summary);
44
+ }
45
+ catch {
46
+ // Embedding generation failed, continue without it
47
+ }
48
+ const summaryNode = await deps.createNode({
49
+ scope: s,
50
+ label: `summary-l${config.nextLevel}-group-${Date.now()}`,
51
+ content: summary,
52
+ summary: `Fractal summary of ${cluster.length} related nodes`,
53
+ level: config.nextLevel,
54
+ parentIds: cluster.map((node) => node.id),
55
+ embedding: summaryEmbedding,
56
+ importance: Math.max(...cluster.map((n) => n.importance)) * 0.9,
57
+ type: "summary",
58
+ metadata: null,
59
+ });
60
+ for (const node of cluster) {
61
+ await deps.updateNode(node.id, {
62
+ summary: `Group compressed into ${summaryNode.id.slice(0, 8)}`,
63
+ });
64
+ }
65
+ compressed += cluster.length;
66
+ created++;
67
+ }
68
+ }
69
+ }
70
+ return { compressed, created };
71
+ }
72
+ export async function runPatternExtraction(deps, scope, minSourceCount = 2) {
73
+ let created = 0;
74
+ let sources = 0;
75
+ const scopes = scope === "all" ? ["global", "project"] : [scope];
76
+ for (const s of scopes) {
77
+ const nodes = await deps.listNodes(s, 0);
78
+ const eligibleNodes = nodes.filter((n) => n.content.length > 50 &&
79
+ !n.sticky &&
80
+ n.type !== "summary");
81
+ if (eligibleNodes.length < 2)
82
+ continue;
83
+ const patterns = CompressionHelper.extractPatterns(eligibleNodes);
84
+ const sourceNodeIds = eligibleNodes.map((n) => n.id);
85
+ sources = sourceNodeIds.length;
86
+ const totalPatternCount = patterns.decisions.size +
87
+ patterns.preferences.size +
88
+ patterns.conventions.size +
89
+ patterns.tools.size +
90
+ patterns.files.size;
91
+ if (totalPatternCount === 0)
92
+ continue;
93
+ const summary = CompressionHelper.generatePatternSummary(patterns, sourceNodeIds);
94
+ let summaryEmbedding = null;
95
+ try {
96
+ summaryEmbedding = await generateEmbedding(summary);
97
+ }
98
+ catch {
99
+ // Embedding generation failed, continue without it
100
+ }
101
+ const summaryNode = await deps.createNode({
102
+ scope: s,
103
+ label: `patterns-${Date.now()}`,
104
+ content: summary,
105
+ summary: `Cross-layer patterns extracted from ${sourceNodeIds.length} nodes`,
106
+ level: 1,
107
+ parentIds: sourceNodeIds,
108
+ embedding: summaryEmbedding,
109
+ importance: 0.7,
110
+ type: "summary",
111
+ metadata: {
112
+ patternTypes: [
113
+ patterns.decisions.size > 0 ? "decisions" : null,
114
+ patterns.preferences.size > 0 ? "preferences" : null,
115
+ patterns.conventions.size > 0 ? "conventions" : null,
116
+ patterns.tools.size > 0 ? "tools" : null,
117
+ patterns.files.size > 0 ? "files" : null,
118
+ ].filter(Boolean),
119
+ },
120
+ });
121
+ for (const node of eligibleNodes) {
122
+ await deps.updateNode(node.id, {
123
+ summary: `Pattern extraction: ${summaryNode.id.slice(0, 8)}`,
124
+ });
125
+ }
126
+ created++;
127
+ }
128
+ return { created, sources };
129
+ }
@@ -0,0 +1,243 @@
1
+ export const LEVEL_FORMATTERS = {
2
+ 1: (nodes) => {
3
+ const lines = ["## Weekly Summary", ""];
4
+ const byDay = new Map();
5
+ for (const node of nodes) {
6
+ const day = node.createdAt.toISOString().split('T')[0];
7
+ if (!byDay.has(day))
8
+ byDay.set(day, []);
9
+ byDay.get(day).push(node);
10
+ }
11
+ for (const [day, dayNodes] of byDay) {
12
+ lines.push(`### ${day}`);
13
+ for (const n of dayNodes) {
14
+ lines.push(`- ${n.content.slice(0, 100)}...`);
15
+ }
16
+ lines.push("");
17
+ }
18
+ return lines.join("\n");
19
+ },
20
+ 2: (nodes) => {
21
+ const lines = ["## Monthly Themes", ""];
22
+ const themes = new Map();
23
+ for (const node of nodes) {
24
+ const theme = node.label ?? node.type ?? "general";
25
+ if (!themes.has(theme))
26
+ themes.set(theme, []);
27
+ themes.get(theme).push(node);
28
+ }
29
+ for (const [theme, themeNodes] of themes) {
30
+ lines.push(`### ${theme} (${themeNodes.length} items)`);
31
+ for (const n of themeNodes.slice(0, 5)) {
32
+ lines.push(`- ${n.content.slice(0, 80)}...`);
33
+ }
34
+ if (themeNodes.length > 5) {
35
+ lines.push(` ... and ${themeNodes.length - 5} more`);
36
+ }
37
+ lines.push("");
38
+ }
39
+ return lines.join("\n");
40
+ },
41
+ 3: (nodes) => {
42
+ const lines = ["## Quarterly Report", "", "### Key Trends"];
43
+ const sorted = [...nodes].sort((a, b) => b.importance - a.importance);
44
+ for (const node of sorted.slice(0, 10)) {
45
+ lines.push(`- **${node.label ?? "Item"}**: ${node.content.slice(0, 100)}`);
46
+ }
47
+ lines.push("", "### Outcomes");
48
+ const highImportance = nodes.filter(n => n.importance > 0.7);
49
+ lines.push(`- ${highImportance.length} high-priority items identified`);
50
+ lines.push(`- ${nodes.length - highImportance.length} standard items`);
51
+ lines.push("");
52
+ lines.push(`_Summary of ${nodes.length} items_`);
53
+ return lines.join("\n");
54
+ },
55
+ };
56
+ export function generateSummary(node) {
57
+ if (LEVEL_FORMATTERS[node.level]) {
58
+ return LEVEL_FORMATTERS[node.level]([node]);
59
+ }
60
+ const content = node.content;
61
+ const lines = content.split('\n').filter(l => l.trim());
62
+ if (lines.length <= 5) {
63
+ return content;
64
+ }
65
+ const headingLines = lines.filter(l => l.startsWith('#') || l.startsWith('##'));
66
+ const bulletLines = lines.filter(l => l.startsWith('- ') || l.startsWith('* '));
67
+ const importantLines = lines.filter(l => l.length > 30 &&
68
+ !l.startsWith('#') &&
69
+ !l.startsWith('-'));
70
+ const summaryParts = [];
71
+ if (headingLines.length > 0) {
72
+ summaryParts.push(`Topics covered (${headingLines.length}):`);
73
+ summaryParts.push(...headingLines.slice(0, 5));
74
+ summaryParts.push('');
75
+ }
76
+ if (bulletLines.length > 0) {
77
+ summaryParts.push(`Key points (${bulletLines.length} items):`);
78
+ summaryParts.push(...bulletLines.slice(0, 8));
79
+ if (bulletLines.length > 8) {
80
+ summaryParts.push(`... and ${bulletLines.length - 8} more items`);
81
+ }
82
+ }
83
+ else if (importantLines.length > 0) {
84
+ summaryParts.push(`Key insights:`);
85
+ summaryParts.push(...importantLines.slice(0, 5));
86
+ }
87
+ if (summaryParts.length === 0) {
88
+ const firstFew = lines.slice(0, 3);
89
+ const lastFew = lines.slice(-2);
90
+ summaryParts.push(`Summary of ${lines.length} lines:`);
91
+ summaryParts.push(...firstFew);
92
+ summaryParts.push(`... (${lines.length - 5} more lines)`);
93
+ summaryParts.push(...lastFew);
94
+ }
95
+ return summaryParts.join('\n');
96
+ }
97
+ export function generateStructuredSummary(nodes) {
98
+ const allContent = nodes.map(n => n.content).join('\n\n');
99
+ const lines = allContent.split('\n').map(l => l.trim()).filter(l => l.length > 0);
100
+ const decisions = [];
101
+ const files = [];
102
+ const patterns = [];
103
+ const nextSteps = [];
104
+ const concepts = [];
105
+ const tools = [];
106
+ const filePattern = /\b[\w-]+\.(ts|js|tsx|jsx|py|md|json|yml|yaml|sql|sh)\b/gi;
107
+ const toolPattern = /\b(memory_\w+|journal_\w+|bash|git|npm|bun|tsc|test)\b/gi;
108
+ const decisionPatterns = [
109
+ /decides?:?\s*/i,
110
+ /chose\s+\w+/i,
111
+ /agreed\s+(to|that)/i,
112
+ /settled\s+on/i,
113
+ /opted\s+(for|in)/i,
114
+ /will\s+use/i,
115
+ /using\s+\w+\s+for/i,
116
+ ];
117
+ const nextPatterns = [
118
+ /^[-*]\s*(next|todo|will|should|need to|plan to)/i,
119
+ /next\s+step/i,
120
+ /pending/i,
121
+ /^#+\s*(next|todo|tasks?)/i,
122
+ ];
123
+ const conceptPatterns = [
124
+ /learned:?\s*/i,
125
+ /pattern:?\s*/i,
126
+ /convention:?\s*/i,
127
+ /architecture/i,
128
+ /design\s+decision/i,
129
+ ];
130
+ for (const line of lines) {
131
+ if (decisionPatterns.some(p => p.test(line)) && !decisionPatterns[5]?.test(line.slice(0, 20))) {
132
+ const clean = line.replace(/^[-*#\s]+/, '').slice(0, 120);
133
+ if (clean.length > 20 && !decisions.includes(clean)) {
134
+ decisions.push(clean);
135
+ }
136
+ }
137
+ if (nextPatterns.some(p => p.test(line))) {
138
+ const clean = line.replace(/^[-*#\s]+/, '').slice(0, 120);
139
+ if (clean.length > 10 && !nextSteps.includes(clean)) {
140
+ nextSteps.push(clean);
141
+ }
142
+ }
143
+ if (conceptPatterns.some(p => p.test(line))) {
144
+ const clean = line.replace(/^[-*#\s]+/, '').slice(0, 120);
145
+ if (clean.length > 10 && !patterns.includes(clean)) {
146
+ patterns.push(clean);
147
+ }
148
+ }
149
+ const fileMatches = line.match(filePattern);
150
+ if (fileMatches) {
151
+ for (const f of fileMatches) {
152
+ if (!files.includes(f)) {
153
+ files.push(f);
154
+ }
155
+ }
156
+ }
157
+ const toolMatches = line.match(toolPattern);
158
+ if (toolMatches) {
159
+ for (const t of toolMatches) {
160
+ if (!tools.includes(t)) {
161
+ tools.push(t);
162
+ }
163
+ }
164
+ }
165
+ }
166
+ for (const line of lines.slice(0, 20)) {
167
+ const headingMatch = line.match(/^#+\s+(.+)/);
168
+ if (headingMatch && !headingMatch[1].toLowerCase().includes('summary')) {
169
+ const concept = headingMatch[1].slice(0, 80);
170
+ if (!concepts.includes(concept)) {
171
+ concepts.push(concept);
172
+ }
173
+ }
174
+ }
175
+ const result = ['## Structured Summary', ''];
176
+ if (decisions.length > 0) {
177
+ result.push('### Decisions');
178
+ for (const d of decisions.slice(0, 5)) {
179
+ result.push(`- ${d}`);
180
+ }
181
+ if (decisions.length > 5) {
182
+ result.push(`- ... and ${decisions.length - 5} more`);
183
+ }
184
+ result.push('');
185
+ }
186
+ if (files.length > 0) {
187
+ result.push('### Files');
188
+ result.push(`_Modified or referenced: ${files.slice(0, 10).join(', ')}_`);
189
+ if (files.length > 10) {
190
+ result.push(`_... and ${files.length - 10} more_`);
191
+ }
192
+ result.push('');
193
+ }
194
+ if (tools.length > 0) {
195
+ result.push('### Tools');
196
+ result.push(`_${tools.slice(0, 8).join(', ')}_`);
197
+ result.push('');
198
+ }
199
+ if (patterns.length > 0) {
200
+ result.push('### Patterns & Conventions');
201
+ for (const p of patterns.slice(0, 5)) {
202
+ result.push(`- ${p}`);
203
+ }
204
+ result.push('');
205
+ }
206
+ if (concepts.length > 0) {
207
+ result.push('### Topics');
208
+ for (const c of concepts.slice(0, 6)) {
209
+ result.push(`- ${c}`);
210
+ }
211
+ result.push('');
212
+ }
213
+ if (nextSteps.length > 0) {
214
+ result.push('### Next Steps');
215
+ for (const n of nextSteps.slice(0, 5)) {
216
+ result.push(`- ${n}`);
217
+ }
218
+ if (nextSteps.length > 5) {
219
+ result.push(`- ... and ${nextSteps.length - 5} more`);
220
+ }
221
+ result.push('');
222
+ }
223
+ if (decisions.length === 0 && files.length === 0 && patterns.length === 0 && concepts.length === 0 && nextSteps.length === 0) {
224
+ const topics = [...new Set(lines.filter(l => l.startsWith('-') || l.startsWith('*')).map(l => l.replace(/^[-*]\s+/, '').slice(0, 60)))];
225
+ result.push('### Key Points');
226
+ for (const t of topics.slice(0, 8)) {
227
+ result.push(`- ${t}`);
228
+ }
229
+ result.push('');
230
+ }
231
+ result.push(`_Compressed from ${nodes.length} node(s)_`);
232
+ return result.join('\n');
233
+ }
234
+ export function generateLLMSummaryPrompt(node) {
235
+ return `Create a concise summary of the following memory content. Focus on the key points and essential information. Keep it under 200 words.
236
+
237
+ Memory content:
238
+ ---
239
+ ${node.content}
240
+ ---
241
+
242
+ Summary:`;
243
+ }
@@ -0,0 +1,107 @@
1
+ import { cosineSimilarity } from "../queries/search-helpers";
2
+ import { generateSummary, generateStructuredSummary, generateLLMSummaryPrompt, } from "./formatters";
3
+ import { extractPatterns as ep, generatePatternSummary } from "./patterns";
4
+ export { generateSummary, generateStructuredSummary, generateLLMSummaryPrompt } from "./formatters";
5
+ export { extractPatterns, generatePatternSummary } from "./patterns";
6
+ export const COMPRESSION_LEVELS = {
7
+ 0: { maxAgeMs: 7 * 24 * 60 * 60 * 1000, nextLevel: 1, label: "Weekly", format: "events" },
8
+ 1: { maxAgeMs: 30 * 24 * 60 * 60 * 1000, nextLevel: 2, label: "Monthly", format: "themes" },
9
+ 2: { maxAgeMs: 90 * 24 * 60 * 60 * 1000, nextLevel: 3, label: "Quarterly", format: "trends" },
10
+ 3: { maxAgeMs: 365 * 24 * 60 * 60 * 1000, nextLevel: 4, label: "Yearly", format: "milestones" },
11
+ };
12
+ export class CompressionHelper {
13
+ static async findCompressionCandidates(nodes, level, maxAgeMs, force) {
14
+ const config = COMPRESSION_LEVELS[level];
15
+ if (!config)
16
+ return [];
17
+ if (force) {
18
+ return nodes.filter(n => n.level === level && n.content.length > 100 && !n.sticky);
19
+ }
20
+ const threshold = maxAgeMs ?? config.maxAgeMs;
21
+ const cutoffTime = Date.now() - threshold;
22
+ return nodes.filter(n => n.level === level &&
23
+ n.updatedAt.getTime() < cutoffTime &&
24
+ n.content.length > 100 &&
25
+ !n.sticky);
26
+ }
27
+ static findRelatedNodes(nodes, threshold = 0.3) {
28
+ const visited = new Set();
29
+ const clusters = [];
30
+ for (let i = 0; i < nodes.length; i++) {
31
+ const nodeA = nodes[i];
32
+ if (visited.has(nodeA.id))
33
+ continue;
34
+ if (!nodeA.embedding)
35
+ continue;
36
+ const cluster = [nodeA];
37
+ visited.add(nodeA.id);
38
+ for (let j = i + 1; j < nodes.length; j++) {
39
+ const nodeB = nodes[j];
40
+ if (visited.has(nodeB.id))
41
+ continue;
42
+ if (!nodeB.embedding)
43
+ continue;
44
+ const similarity = cosineSimilarity(nodeA.embedding, nodeB.embedding);
45
+ if (similarity >= threshold) {
46
+ cluster.push(nodeB);
47
+ visited.add(nodeB.id);
48
+ }
49
+ }
50
+ if (cluster.length >= 2) {
51
+ clusters.push(cluster);
52
+ }
53
+ }
54
+ return clusters;
55
+ }
56
+ static generateSummary(node) {
57
+ return generateSummary(node);
58
+ }
59
+ static generateLLMSummaryPrompt(node) {
60
+ return generateLLMSummaryPrompt(node);
61
+ }
62
+ static generateStructuredSummary(nodes) {
63
+ return generateStructuredSummary(nodes);
64
+ }
65
+ static async generateLLMSummary(nodes, client, maxTokens = 500) {
66
+ const allContent = nodes.map(n => `---\nLabel: ${n.label ?? "unnamed"}\nLevel: ${n.level}\nImportance: ${n.importance}\n\n${n.content}`).join('\n\n');
67
+ const prompt = `Create a structured summary of the following ${nodes.length} related memory nodes. Extract the key information and organize it into sections.
68
+
69
+ Extract:
70
+ 1. Key decisions or conclusions
71
+ 2. Files, libraries, or code patterns referenced
72
+ 3. Tools or commands used
73
+ 4. Design patterns or conventions discovered
74
+ 5. Next steps or pending items
75
+
76
+ Nodes:
77
+ ${allContent}
78
+
79
+ Structured summary:`;
80
+ try {
81
+ const sessionClient = client;
82
+ const result = await sessionClient.session?.prompt({
83
+ path: { id: 'compression' },
84
+ body: {
85
+ noReply: true,
86
+ parts: [{ type: 'text', text: prompt }],
87
+ },
88
+ });
89
+ if (result) {
90
+ const response = await result.text();
91
+ const trimmed = response.trim();
92
+ if (trimmed)
93
+ return trimmed;
94
+ }
95
+ }
96
+ catch (e) {
97
+ // LLM failed, fall through to regex fallback
98
+ }
99
+ return generateStructuredSummary(nodes);
100
+ }
101
+ static extractPatterns(nodes) {
102
+ return ep(nodes);
103
+ }
104
+ static generatePatternSummary(patterns, sourceNodeIds) {
105
+ return generatePatternSummary(patterns, sourceNodeIds);
106
+ }
107
+ }
@@ -0,0 +1,138 @@
1
+ export function extractPatterns(nodes) {
2
+ const files = new Map();
3
+ const tools = new Map();
4
+ const conventions = new Map();
5
+ const preferences = new Map();
6
+ const decisions = new Map();
7
+ const filePattern = /\b[\w-]+\.(ts|js|tsx|jsx|py|md|json|yml|yaml|sql|sh|css|html)\b/gi;
8
+ const toolPattern = /\b(memory_\w+|journal_\w+|git|npm|bun|tsc|test|make|cmake|pip|python|node)\b/gi;
9
+ const conventionPatterns = [
10
+ /convention:?\s*/i,
11
+ /standard:?\s*/i,
12
+ /best\s+practice/i,
13
+ /always\s+(use|run|follow)/i,
14
+ /never\s+(use|do)/i,
15
+ /rule:?\s*/i,
16
+ ];
17
+ const preferencePatterns = [
18
+ /user\s+(prefers?|likes?|wants?)/i,
19
+ /prefers?\s+\w+/i,
20
+ /likes?\s+\w+/i,
21
+ /uses?\s+\w+/i,
22
+ /favorite:?\s*/i,
23
+ ];
24
+ const decisionPatterns = [
25
+ /decided?\s+(to|on|that)/i,
26
+ /chose\s+\w+/i,
27
+ /agreed?\s+(to|that)/i,
28
+ /will\s+use/i,
29
+ /using\s+\w+\s+for/i,
30
+ ];
31
+ for (const node of nodes) {
32
+ const contentLines = node.content.split('\n');
33
+ for (const line of contentLines) {
34
+ const fileMatches = line.match(filePattern);
35
+ if (fileMatches) {
36
+ for (const f of fileMatches) {
37
+ const existing = files.get(f) || [];
38
+ if (!existing.includes(node.id)) {
39
+ existing.push(node.id);
40
+ files.set(f, existing);
41
+ }
42
+ }
43
+ }
44
+ const toolMatches = line.match(toolPattern);
45
+ if (toolMatches) {
46
+ for (const t of toolMatches) {
47
+ const existing = tools.get(t) || [];
48
+ if (!existing.includes(node.id)) {
49
+ existing.push(node.id);
50
+ tools.set(t, existing);
51
+ }
52
+ }
53
+ }
54
+ for (const pattern of conventionPatterns) {
55
+ if (pattern.test(line)) {
56
+ const key = line.replace(/^[-*#\s]+/, '').slice(0, 80);
57
+ const existing = conventions.get(key) || [];
58
+ if (!existing.includes(node.id)) {
59
+ existing.push(node.id);
60
+ conventions.set(key, existing);
61
+ }
62
+ break;
63
+ }
64
+ }
65
+ for (const pattern of preferencePatterns) {
66
+ if (pattern.test(line)) {
67
+ const key = line.replace(/^[-*#\s]+/, '').slice(0, 80);
68
+ const existing = preferences.get(key) || [];
69
+ if (!existing.includes(node.id)) {
70
+ existing.push(node.id);
71
+ preferences.set(key, existing);
72
+ }
73
+ break;
74
+ }
75
+ }
76
+ for (const pattern of decisionPatterns) {
77
+ if (pattern.test(line)) {
78
+ const key = line.replace(/^[-*#\s]+/, '').slice(0, 80);
79
+ const existing = decisions.get(key) || [];
80
+ if (!existing.includes(node.id)) {
81
+ existing.push(node.id);
82
+ decisions.set(key, existing);
83
+ }
84
+ break;
85
+ }
86
+ }
87
+ }
88
+ }
89
+ return { files, tools, conventions, preferences, decisions };
90
+ }
91
+ export function generatePatternSummary(patterns, sourceNodeIds) {
92
+ const result = ['## Cross-Layer Pattern Summary', ''];
93
+ if (patterns.decisions.size > 0) {
94
+ result.push('### Key Decisions');
95
+ for (const [decision, sources] of patterns.decisions) {
96
+ if (sources.length >= 1) {
97
+ result.push(`- ${decision}`);
98
+ }
99
+ }
100
+ result.push('');
101
+ }
102
+ if (patterns.preferences.size > 0) {
103
+ result.push('### User Preferences');
104
+ for (const [pref, sources] of patterns.preferences) {
105
+ if (sources.length >= 1) {
106
+ result.push(`- ${pref}`);
107
+ }
108
+ }
109
+ result.push('');
110
+ }
111
+ if (patterns.conventions.size > 0) {
112
+ result.push('### Conventions & Standards');
113
+ for (const [conv, sources] of patterns.conventions) {
114
+ if (sources.length >= 1) {
115
+ result.push(`- ${conv}`);
116
+ }
117
+ }
118
+ result.push('');
119
+ }
120
+ if (patterns.tools.size > 0) {
121
+ const toolCounts = Array.from(patterns.tools.entries())
122
+ .map(([tool, sources]) => ({ tool, count: sources.length }))
123
+ .sort((a, b) => b.count - a.count);
124
+ result.push('### Tools Used');
125
+ result.push(`_${toolCounts.slice(0, 10).map(t => `${t.tool} (${t.count}x)`).join(', ')}_`);
126
+ result.push('');
127
+ }
128
+ if (patterns.files.size > 0) {
129
+ const fileCounts = Array.from(patterns.files.entries())
130
+ .map(([file, sources]) => ({ file, count: sources.length }))
131
+ .sort((a, b) => b.count - a.count);
132
+ result.push('### Key Files');
133
+ result.push(`_${fileCounts.slice(0, 10).map(f => f.file).join(', ')}_`);
134
+ result.push('');
135
+ }
136
+ result.push(`_Extracted from ${sourceNodeIds.length} source nodes_`);
137
+ return result.join('\n');
138
+ }
@@ -0,0 +1,66 @@
1
+ import { rowToNode } from "./queries/base";
2
+ import { withRetry } from "./utils";
3
+ export async function getExpiredNodes(getDb, scope = "all") {
4
+ const scopes = scope === "all" ? ["global", "project"] : [scope];
5
+ const expired = [];
6
+ const now = Date.now();
7
+ for (const s of scopes) {
8
+ const db = await getDb(s);
9
+ const rows = db.query("SELECT * FROM memory_nodes WHERE expires_at IS NOT NULL AND expires_at <= ? AND (type IS NULL OR type != 'skill')").all(now);
10
+ expired.push(...rows.map(rowToNode));
11
+ }
12
+ return expired;
13
+ }
14
+ export async function deleteExpiredNodes(getDb, scope = "all") {
15
+ const scopes = scope === "all" ? ["global", "project"] : [scope];
16
+ let deleted = 0;
17
+ for (const s of scopes) {
18
+ const db = await getDb(s);
19
+ const result = db.run("DELETE FROM memory_nodes WHERE expires_at IS NOT NULL AND expires_at <= ? AND (type IS NULL OR type != 'skill')", [Date.now()]);
20
+ deleted += Number(result.changes);
21
+ }
22
+ return deleted;
23
+ }
24
+ export async function pruneNodes(deps, scope, options = {}) {
25
+ const { minAccessCount = 0, maxAgeDays = 90, minImportance = 0, excludeSticky = true, excludeCore = true, dryRun = true, } = options;
26
+ const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1000;
27
+ const cutoffTime = Date.now() - maxAgeMs;
28
+ const scopes = scope === "all" ? ["global", "project"] : [scope];
29
+ const prunable = [];
30
+ const coreLabels = new Set(["persona", "human", "current-state", "project"]);
31
+ for (const s of scopes) {
32
+ const nodes = await deps.listNodes(s);
33
+ for (const node of nodes) {
34
+ if (excludeSticky && node.sticky)
35
+ continue;
36
+ if (excludeCore && node.label && coreLabels.has(node.label))
37
+ continue;
38
+ if (node.importance >= 0.9)
39
+ continue;
40
+ const isLowAccess = node.accessCount <= minAccessCount;
41
+ const isStale = node.updatedAt.getTime() < cutoffTime;
42
+ const isLowImportance = node.importance < minImportance;
43
+ if ((isLowAccess || isStale) && isLowImportance) {
44
+ prunable.push(node);
45
+ }
46
+ }
47
+ }
48
+ let pruned = 0;
49
+ if (!dryRun && prunable.length > 0) {
50
+ const byScope = new Map();
51
+ for (const node of prunable) {
52
+ const list = byScope.get(node.scope);
53
+ if (list)
54
+ list.push(node.id);
55
+ else
56
+ byScope.set(node.scope, [node.id]);
57
+ }
58
+ for (const [s, ids] of byScope) {
59
+ const db = await deps.getDb(s);
60
+ const placeholders = ids.map(() => "?").join(",");
61
+ await withRetry(() => db.run(`DELETE FROM memory_nodes WHERE id IN (${placeholders})`, ids));
62
+ pruned += ids.length;
63
+ }
64
+ }
65
+ return { prunable, pruned };
66
+ }
@@ -0,0 +1 @@
1
+ export { createSqliteMemoryStore } from "./sqlite";