opencode-fractal-memory 0.6.13 → 0.6.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -6
- package/dist/management/helpers.js +19 -1
- package/dist/management/routes.js +5 -1
- package/dist/plugin/hooks.js +6 -1
- package/management/public/app.js +80 -0
- package/management/public/index.html +9 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -377,12 +377,18 @@ Opens at [http://localhost:8787](http://localhost:8787). The server starts as a
|
|
|
377
377
|
- **Scroll** to zoom in/out
|
|
378
378
|
- **Left-click** a node to select and inspect it
|
|
379
379
|
- **Right-click drag** to pan
|
|
380
|
-
- Nodes are color-coded by type (
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
-
|
|
385
|
-
-
|
|
380
|
+
- Nodes are color-coded by level and type (skill = gold icosahedron, playbook = orange torus, note = blue sphere)
|
|
381
|
+
- Playbook nodes render as orange torus shapes with steps visible in the detail panel
|
|
382
|
+
|
|
383
|
+
**Filters** — narrow down visible nodes:
|
|
384
|
+
- **Scope** (global/project)
|
|
385
|
+
- **Level** (L0–L5), **Type** (note, skill, playbook, etc.), **Shape**, **Custom Type**
|
|
386
|
+
- **Project** — when multiple projects exist, filter by project name
|
|
387
|
+
- **Clear All Filters** button resets everything at once
|
|
388
|
+
- **Search** — find nodes by content, label, or type:
|
|
389
|
+
- Type a query and press Enter
|
|
390
|
+
- Results show relevance scores and preview snippets
|
|
391
|
+
- Click a result to navigate to it in the graph
|
|
386
392
|
|
|
387
393
|
**Inspect** — when you click a node (graph or search results):
|
|
388
394
|
- View full content and summary
|
|
@@ -506,3 +512,21 @@ Unified SQLite database with `project_name` discriminator:
|
|
|
506
512
|
## License
|
|
507
513
|
|
|
508
514
|
MIT
|
|
515
|
+
|
|
516
|
+
## Changelog
|
|
517
|
+
|
|
518
|
+
### v0.6.15 (2026-06-06)
|
|
519
|
+
- **Project switcher** — filter memory nodes by project name in management UI
|
|
520
|
+
- **Clear all filters** — one-click reset of all active filters
|
|
521
|
+
- **Playbook nodes** — now render as orange torus with step details in management UI
|
|
522
|
+
- **TYPE_COLORS** — playbooks (orange) and skills (gold) have dedicated colors in 3D scene
|
|
523
|
+
- Backend: `project_name` in API responses, `GET /api/projects` endpoint
|
|
524
|
+
|
|
525
|
+
### v0.6.14 (2026-06-06)
|
|
526
|
+
- Session reference counter fix — management server only stops when all sessions end
|
|
527
|
+
|
|
528
|
+
### v0.6.13 (2026-06-06)
|
|
529
|
+
- Event hook refactor — management server lifecycle tied to real `session.created`/`session.deleted` events
|
|
530
|
+
|
|
531
|
+
### v0.6.12 (2026-06-06)
|
|
532
|
+
- Fixed management server lifecycle — SIGKILL instead of SIGTERM, proper event hooks
|
|
@@ -75,6 +75,7 @@ export function rowToNode(r) {
|
|
|
75
75
|
parentIds: r.parent_ids ? JSON.parse(r.parent_ids) : null,
|
|
76
76
|
contentLength: r.content_length,
|
|
77
77
|
metadata: r.metadata ? JSON.parse(r.metadata) : null,
|
|
78
|
+
projectName: r.project_name ?? null,
|
|
78
79
|
};
|
|
79
80
|
}
|
|
80
81
|
export function queryNodes(scope) {
|
|
@@ -85,7 +86,7 @@ export function queryNodes(scope) {
|
|
|
85
86
|
SELECT id, label, content, summary, level, type, importance,
|
|
86
87
|
usefulness_score, times_used, times_helpful, access_count,
|
|
87
88
|
sticky, confidence, created_at, updated_at, parent_ids,
|
|
88
|
-
LENGTH(content) as content_length, metadata
|
|
89
|
+
LENGTH(content) as content_length, metadata, project_name
|
|
89
90
|
FROM memory_nodes
|
|
90
91
|
WHERE scope = ?
|
|
91
92
|
ORDER BY level, importance DESC
|
|
@@ -169,6 +170,7 @@ export function computeStats(nodes) {
|
|
|
169
170
|
const nodesPerType = {};
|
|
170
171
|
const nodesPerCustomType = {};
|
|
171
172
|
const nodesPerShape = {};
|
|
173
|
+
const nodesPerProject = {};
|
|
172
174
|
let totalImportance = 0;
|
|
173
175
|
let totalUsefulness = 0;
|
|
174
176
|
let totalAccessCount = 0;
|
|
@@ -183,6 +185,8 @@ export function computeStats(nodes) {
|
|
|
183
185
|
}
|
|
184
186
|
const shape = resolveNodeShape(node);
|
|
185
187
|
nodesPerShape[shape] = (nodesPerShape[shape] ?? 0) + 1;
|
|
188
|
+
const project = node.projectName || "(default)";
|
|
189
|
+
nodesPerProject[project] = (nodesPerProject[project] ?? 0) + 1;
|
|
186
190
|
totalImportance += node.importance;
|
|
187
191
|
totalUsefulness += node.usefulnessScore;
|
|
188
192
|
totalAccessCount += node.accessCount;
|
|
@@ -196,12 +200,26 @@ export function computeStats(nodes) {
|
|
|
196
200
|
nodesPerType,
|
|
197
201
|
nodesPerCustomType,
|
|
198
202
|
nodesPerShape,
|
|
203
|
+
nodesPerProject,
|
|
199
204
|
avgImportance: Math.round((totalImportance / n) * 100) / 100,
|
|
200
205
|
avgUsefulness: Math.round((totalUsefulness / n) * 100) / 100,
|
|
201
206
|
totalAccessCount,
|
|
202
207
|
stickyCount,
|
|
203
208
|
};
|
|
204
209
|
}
|
|
210
|
+
export function getAvailableProjects(scope) {
|
|
211
|
+
const db = openDb(scope);
|
|
212
|
+
if (!db)
|
|
213
|
+
return [];
|
|
214
|
+
const rows = db.query(`
|
|
215
|
+
SELECT DISTINCT COALESCE(project_name, '') as project_name
|
|
216
|
+
FROM memory_nodes
|
|
217
|
+
WHERE scope = ?
|
|
218
|
+
ORDER BY project_name
|
|
219
|
+
`).all(scope);
|
|
220
|
+
db.close();
|
|
221
|
+
return rows.map(r => r.project_name || "(default)");
|
|
222
|
+
}
|
|
205
223
|
export function readProjectConfig() {
|
|
206
224
|
const configPath = path.join(os.homedir(), ".config", "opencode", "opencode-mem.json");
|
|
207
225
|
try {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { memLog } from "../logging";
|
|
2
2
|
import { generateEmbedding } from "../embeddings";
|
|
3
|
-
import { queryNodes, getAvailableScopes, extractLinks, computeStats, readProjectConfig, writeProjectConfig, rowToNode, withDb, jsonResponse, cosineSimilarity, } from "./helpers";
|
|
3
|
+
import { queryNodes, getAvailableScopes, extractLinks, computeStats, readProjectConfig, writeProjectConfig, rowToNode, withDb, jsonResponse, cosineSimilarity, getAvailableProjects, } from "./helpers";
|
|
4
4
|
function handleScopes() {
|
|
5
5
|
return jsonResponse(getAvailableScopes());
|
|
6
6
|
}
|
|
@@ -181,11 +181,15 @@ async function handleNodeDelete(ctx) {
|
|
|
181
181
|
return jsonResponse({ success: true });
|
|
182
182
|
});
|
|
183
183
|
}
|
|
184
|
+
function handleProjects(ctx) {
|
|
185
|
+
return jsonResponse(getAvailableProjects(ctx.scope));
|
|
186
|
+
}
|
|
184
187
|
export function registerRoutes(router) {
|
|
185
188
|
router.get(/^\/api\/scopes$/, () => handleScopes());
|
|
186
189
|
router.get(/^\/api\/nodes$/, (_, ctx) => handleNodes(ctx));
|
|
187
190
|
router.get(/^\/api\/links$/, (_, ctx) => handleLinks(ctx));
|
|
188
191
|
router.get(/^\/api\/stats$/, (_, ctx) => handleStats(ctx));
|
|
192
|
+
router.get(/^\/api\/projects$/, (_, ctx) => handleProjects(ctx));
|
|
189
193
|
router.get(/^\/api\/config$/, () => handleConfigGet());
|
|
190
194
|
router.put(/^\/api\/config$/, (req) => handleConfigSave(req));
|
|
191
195
|
router.post(/^\/api\/config$/, (req) => handleConfigSave(req));
|
package/dist/plugin/hooks.js
CHANGED
|
@@ -2,6 +2,7 @@ import { memLog, memLogSimple, setSessionId } from "../logging";
|
|
|
2
2
|
import { generateFileSummary, generateFileLabel, SOURCE_FILE_EXTENSIONS } from "../file-summary";
|
|
3
3
|
import { distillRules, predictiveRateToolCall, applyScoreDecay } from "../hooks";
|
|
4
4
|
import * as fs from "node:fs";
|
|
5
|
+
let activeSessionCount = 0;
|
|
5
6
|
export function createHookHandlers(store, client, memConfig, ruleCache, ruleCacheDirty, sessionInjectionLock, latestUserMessage, managementServer) {
|
|
6
7
|
return {
|
|
7
8
|
"experimental.chat.system.transform": async (input, output) => {
|
|
@@ -155,6 +156,7 @@ export function createHookHandlers(store, client, memConfig, ruleCache, ruleCach
|
|
|
155
156
|
const sessionId = properties.info?.id ?? "unknown";
|
|
156
157
|
await store.createSessionMetrics(sessionId);
|
|
157
158
|
setSessionId(sessionId);
|
|
159
|
+
activeSessionCount++;
|
|
158
160
|
managementServer.start();
|
|
159
161
|
}
|
|
160
162
|
else if (type === "session.idle") {
|
|
@@ -168,7 +170,10 @@ export function createHookHandlers(store, client, memConfig, ruleCache, ruleCach
|
|
|
168
170
|
}
|
|
169
171
|
}
|
|
170
172
|
else if (type === "session.deleted") {
|
|
171
|
-
|
|
173
|
+
activeSessionCount = Math.max(0, activeSessionCount - 1);
|
|
174
|
+
if (activeSessionCount === 0) {
|
|
175
|
+
managementServer.stop();
|
|
176
|
+
}
|
|
172
177
|
}
|
|
173
178
|
},
|
|
174
179
|
};
|
package/management/public/app.js
CHANGED
|
@@ -17,9 +17,15 @@ const TYPE_SHAPES = {
|
|
|
17
17
|
improvement: "sphere",
|
|
18
18
|
howto: "sphere",
|
|
19
19
|
skill: "icosahedron",
|
|
20
|
+
playbook: "torus",
|
|
20
21
|
unknown: "sphere",
|
|
21
22
|
};
|
|
22
23
|
|
|
24
|
+
const TYPE_COLORS = {
|
|
25
|
+
skill: 0xfbbf24,
|
|
26
|
+
playbook: 0xff8c00,
|
|
27
|
+
};
|
|
28
|
+
|
|
23
29
|
const CUSTOM_TYPE_COLORS = {
|
|
24
30
|
'middle-term': 0xff6b6b,
|
|
25
31
|
};
|
|
@@ -36,6 +42,7 @@ class NodeFilterEngine {
|
|
|
36
42
|
this.types = new Set();
|
|
37
43
|
this.customTypes = new Set();
|
|
38
44
|
this.shapes = new Set();
|
|
45
|
+
this.projects = new Set();
|
|
39
46
|
this.searchQuery = "";
|
|
40
47
|
this.searchMode = "text";
|
|
41
48
|
this.serverSearchIds = null;
|
|
@@ -47,17 +54,31 @@ class NodeFilterEngine {
|
|
|
47
54
|
this.types.clear();
|
|
48
55
|
this.customTypes.clear();
|
|
49
56
|
this.shapes.clear();
|
|
57
|
+
this.projects.clear();
|
|
50
58
|
|
|
51
59
|
Object.keys(stats.nodesPerLevel || {}).map(Number).sort((a, b) => a - b).forEach(l => this.levels.add(l));
|
|
52
60
|
Object.keys(stats.nodesPerType || {}).sort().forEach(t => this.types.add(t));
|
|
53
61
|
Object.keys(stats.nodesPerCustomType || {}).sort().forEach(ct => this.customTypes.add(ct));
|
|
54
62
|
Object.keys(stats.nodesPerShape || {}).sort().forEach(s => this.shapes.add(s));
|
|
63
|
+
Object.keys(stats.nodesPerProject || {}).sort().forEach(p => this.projects.add(p));
|
|
55
64
|
}
|
|
56
65
|
|
|
57
66
|
toggleLevel(v) { this._toggle(this.levels, v); this.changed(); }
|
|
58
67
|
toggleType(v) { this._toggle(this.types, v); this.changed(); }
|
|
59
68
|
toggleCustomType(v) { this._toggle(this.customTypes, v); this.changed(); }
|
|
60
69
|
toggleShape(v) { this._toggle(this.shapes, v); this.changed(); }
|
|
70
|
+
toggleProject(v) { this._toggle(this.projects, v); this.changed(); }
|
|
71
|
+
|
|
72
|
+
clearAll() {
|
|
73
|
+
this.levels.clear();
|
|
74
|
+
this.types.clear();
|
|
75
|
+
this.customTypes.clear();
|
|
76
|
+
this.shapes.clear();
|
|
77
|
+
this.projects.clear();
|
|
78
|
+
this.searchQuery = "";
|
|
79
|
+
this.serverSearchIds = null;
|
|
80
|
+
this.changed();
|
|
81
|
+
}
|
|
61
82
|
|
|
62
83
|
setSearchQuery(q) { this.searchQuery = (q || "").toLowerCase(); }
|
|
63
84
|
setSearchMode(m) { this.searchMode = m; }
|
|
@@ -86,6 +107,11 @@ class NodeFilterEngine {
|
|
|
86
107
|
if (!this.shapes.has(getNodeShape(node))) return false;
|
|
87
108
|
}
|
|
88
109
|
|
|
110
|
+
if (this.projects.size > 0) {
|
|
111
|
+
const p = node.projectName || "(default)";
|
|
112
|
+
if (!this.projects.has(p)) return false;
|
|
113
|
+
}
|
|
114
|
+
|
|
89
115
|
if (this.searchQuery) {
|
|
90
116
|
if (this.searchMode === "text") {
|
|
91
117
|
const q = this.searchQuery;
|
|
@@ -305,6 +331,9 @@ class SceneController {
|
|
|
305
331
|
if (customType && CUSTOM_TYPE_COLORS[customType]) {
|
|
306
332
|
color = CUSTOM_TYPE_COLORS[customType];
|
|
307
333
|
shape = CUSTOM_TYPE_SHAPES[customType] ?? "sphere";
|
|
334
|
+
} else if (node.type && TYPE_COLORS[node.type]) {
|
|
335
|
+
color = TYPE_COLORS[node.type];
|
|
336
|
+
shape = TYPE_SHAPES[node.type] ?? "sphere";
|
|
308
337
|
} else {
|
|
309
338
|
color = LEVEL_COLORS[node.level] ?? 0x888888;
|
|
310
339
|
shape = TYPE_SHAPES[node.type] ?? "sphere";
|
|
@@ -670,9 +699,23 @@ function setupEventListeners() {
|
|
|
670
699
|
} else if (btn.dataset.shape !== undefined) {
|
|
671
700
|
filterEngine.toggleShape(btn.dataset.shape);
|
|
672
701
|
btn.classList.toggle("active");
|
|
702
|
+
} else if (btn.dataset.project !== undefined) {
|
|
703
|
+
filterEngine.toggleProject(btn.dataset.project);
|
|
704
|
+
btn.classList.toggle("active");
|
|
673
705
|
}
|
|
674
706
|
});
|
|
675
707
|
|
|
708
|
+
// Clear all filters button
|
|
709
|
+
document.getElementById("clear-filters").addEventListener("click", () => {
|
|
710
|
+
filterEngine.initFromStats(statsData);
|
|
711
|
+
filterEngine.setSearchQuery("");
|
|
712
|
+
filterEngine.setServerSearchIds(null);
|
|
713
|
+
document.getElementById("search-input").value = "";
|
|
714
|
+
document.getElementById("search-info").textContent = "";
|
|
715
|
+
buildFilters();
|
|
716
|
+
sceneCtrl.updateVisibility(filterEngine);
|
|
717
|
+
});
|
|
718
|
+
|
|
676
719
|
// Search mode toggles
|
|
677
720
|
document.querySelectorAll(".search-mode-btn").forEach(btn => {
|
|
678
721
|
btn.addEventListener("click", () => {
|
|
@@ -920,6 +963,16 @@ function buildFilters() {
|
|
|
920
963
|
`<button class="filter-btn active" data-shape="${s}">${shapeLabels[s] || s} (${statsData.nodesPerShape[s]})</button>`
|
|
921
964
|
).join("");
|
|
922
965
|
}
|
|
966
|
+
|
|
967
|
+
// Project filters
|
|
968
|
+
const projects = Object.keys(statsData.nodesPerProject || {}).sort();
|
|
969
|
+
const projectContainer = document.getElementById("project-filters");
|
|
970
|
+
if (projectContainer && projects.length > 1) {
|
|
971
|
+
projectContainer.closest(".section").style.display = "block";
|
|
972
|
+
projectContainer.innerHTML = projects.map(p =>
|
|
973
|
+
`<button class="filter-btn active" data-project="${p}">${p} (${statsData.nodesPerProject[p]})</button>`
|
|
974
|
+
).join("");
|
|
975
|
+
}
|
|
923
976
|
}
|
|
924
977
|
|
|
925
978
|
function buildLegend() {
|
|
@@ -937,6 +990,7 @@ function buildLegend() {
|
|
|
937
990
|
html += `<div class="legend-item"><div class="legend-dot" style="background: #4a9eff; border-radius: 2px;"></div><span class="legend-label">Box = Event/Episode</span></div>`;
|
|
938
991
|
html += `<div class="legend-item"><div class="legend-dot" style="background: #4a9eff; clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);"></div><span class="legend-label">Diamond = Concept/Summary</span></div>`;
|
|
939
992
|
html += `<div class="legend-item"><div class="legend-dot" style="background: #4a9eff; clip-path: polygon(50% 0%, 100% 38%, 82% 100%, 18% 100%, 0% 38%);"></div><span class="legend-label">Pentagon = Skill</span></div>`;
|
|
993
|
+
html += `<div class="legend-item"><div class="legend-dot" style="background: #ff8c00; border-radius: 50%;"></div><span class="legend-label">Torus = Playbook</span></div>`;
|
|
940
994
|
html += `<div class="legend-item"><div class="legend-dot" style="background: #ff6b6b; border-radius: 50%;"></div><span class="legend-label">Torus = Middle-Term</span></div>`;
|
|
941
995
|
html += `</div>`;
|
|
942
996
|
legend.innerHTML = html;
|
|
@@ -971,6 +1025,9 @@ function buildNodeList() {
|
|
|
971
1025
|
if (node.type === 'skill') {
|
|
972
1026
|
customIndicator += ' <span style="color: #fbbf24; font-size: 10px;">[SKILL]</span>';
|
|
973
1027
|
}
|
|
1028
|
+
if (node.type === 'playbook') {
|
|
1029
|
+
customIndicator += ' <span style="color: #ff8c00; font-size: 10px;">[PLAYBOOK]</span>';
|
|
1030
|
+
}
|
|
974
1031
|
|
|
975
1032
|
return `
|
|
976
1033
|
<div class="node-list-item ${isSelected ? 'selected' : ''}" data-node-id="${node.id}">
|
|
@@ -1023,6 +1080,28 @@ function showDetailPanel(node) {
|
|
|
1023
1080
|
`;
|
|
1024
1081
|
}
|
|
1025
1082
|
|
|
1083
|
+
let playbookHtml = "";
|
|
1084
|
+
if (node.type === 'playbook') {
|
|
1085
|
+
const steps = node.metadata?.steps;
|
|
1086
|
+
let stepsHtml = "";
|
|
1087
|
+
if (steps && Array.isArray(steps)) {
|
|
1088
|
+
stepsHtml = steps.map((s, i) => `
|
|
1089
|
+
<div style="padding: 6px 8px; margin-bottom: 4px; background: rgba(255,140,0,0.1); border-radius: 4px; border-left: 3px solid #ff8c00;">
|
|
1090
|
+
<div style="font-size: 12px; color: #ff8c00; font-weight: 600;">Step ${i + 1}: ${escapeHtml(s.description || s.toolName || "Unnamed")}</div>
|
|
1091
|
+
<div style="font-size: 11px; color: #aaa; margin-top: 2px;">Tool: ${escapeHtml(s.toolName || "none")}${s.critical ? ' <span style="color: #f44;">[CRITICAL]</span>' : ''}</div>
|
|
1092
|
+
</div>
|
|
1093
|
+
`).join("");
|
|
1094
|
+
} else {
|
|
1095
|
+
stepsHtml = '<div style="font-size: 12px; color: #888;">No steps defined in metadata</div>';
|
|
1096
|
+
}
|
|
1097
|
+
playbookHtml = `
|
|
1098
|
+
<div class="detail-section">
|
|
1099
|
+
<h4>Playbook Steps</h4>
|
|
1100
|
+
${stepsHtml}
|
|
1101
|
+
</div>
|
|
1102
|
+
`;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1026
1105
|
content.innerHTML = `
|
|
1027
1106
|
<div class="detail-section">
|
|
1028
1107
|
<h4>ID</h4>
|
|
@@ -1043,6 +1122,7 @@ function showDetailPanel(node) {
|
|
|
1043
1122
|
</div>
|
|
1044
1123
|
${metadataHtml}
|
|
1045
1124
|
${skillHtml}
|
|
1125
|
+
${playbookHtml}
|
|
1046
1126
|
<div class="detail-section">
|
|
1047
1127
|
<h4>Timestamps</h4>
|
|
1048
1128
|
<div class="detail-value">Created: ${created}</div>
|
|
@@ -254,6 +254,15 @@
|
|
|
254
254
|
<div class="filter-group" id="shape-filters"></div>
|
|
255
255
|
</div>
|
|
256
256
|
|
|
257
|
+
<div class="section" style="display:none;">
|
|
258
|
+
<h3>Filter by Project</h3>
|
|
259
|
+
<div class="filter-group" id="project-filters"></div>
|
|
260
|
+
</div>
|
|
261
|
+
|
|
262
|
+
<div class="section">
|
|
263
|
+
<button class="search-btn" id="clear-filters" style="border-color: rgba(255,100,100,0.4); background: rgba(255,100,100,0.15);">Clear All Filters</button>
|
|
264
|
+
</div>
|
|
265
|
+
|
|
257
266
|
<div class="section">
|
|
258
267
|
<h3>Legend</h3>
|
|
259
268
|
<div id="legend"></div>
|