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.
- package/LICENSE +21 -0
- package/README.md +493 -0
- package/agent/memory-hints.md +98 -0
- package/agent/memory-researcher.md +56 -0
- package/commands/memory-auto-test.md +10 -0
- package/commands/memory-cache-status.md +13 -0
- package/commands/memory-check-context.md +4 -0
- package/commands/memory-compress.md +13 -0
- package/commands/memory-dashboard.md +23 -0
- package/commands/memory-delete.md +24 -0
- package/commands/memory-detect-topics.md +28 -0
- package/commands/memory-distill.md +35 -0
- package/commands/memory-drilldown-query.md +28 -0
- package/commands/memory-drilldown.md +11 -0
- package/commands/memory-extract-patterns.md +4 -0
- package/commands/memory-generate-embeddings.md +26 -0
- package/commands/memory-get.md +26 -0
- package/commands/memory-help.md +55 -0
- package/commands/memory-injection-feedback.md +26 -0
- package/commands/memory-injection-stats.md +11 -0
- package/commands/memory-list.md +4 -0
- package/commands/memory-llm-compress.md +34 -0
- package/commands/memory-mcp.md +20 -0
- package/commands/memory-prune.md +4 -0
- package/commands/memory-rate.md +48 -0
- package/commands/memory-reflect.md +37 -0
- package/commands/memory-replace.md +26 -0
- package/commands/memory-retrieve.md +34 -0
- package/commands/memory-search.md +28 -0
- package/commands/memory-session-stats.md +4 -0
- package/commands/memory-set.md +31 -0
- package/commands/memory-stats.md +11 -0
- package/commands/memory-summarize.md +29 -0
- package/commands/memory-tool-stats.md +4 -0
- package/commands/memory-total-tokens.md +10 -0
- package/commands/memory-verify.md +4 -0
- package/commands/memory-version.md +9 -0
- package/dist/cache.js +39 -0
- package/dist/config.js +120 -0
- package/dist/embeddings.js +125 -0
- package/dist/ensure-models.js +70 -0
- package/dist/file-summary.js +143 -0
- package/dist/frontmatter.js +28 -0
- package/dist/hnsw-index.js +138 -0
- package/dist/hooks/auto-discover.js +4 -0
- package/dist/hooks/auto-distill.js +120 -0
- package/dist/hooks/auto-retrieve/content.js +47 -0
- package/dist/hooks/auto-retrieve/detection.js +50 -0
- package/dist/hooks/auto-retrieve/formatting.js +19 -0
- package/dist/hooks/auto-retrieve/index.js +163 -0
- package/dist/hooks/auto-retrieve/scoring.js +56 -0
- package/dist/hooks/auto-retrieve.js +1 -0
- package/dist/hooks/index.js +4 -0
- package/dist/hooks/predictive-rating.js +87 -0
- package/dist/journal.js +279 -0
- package/dist/logging.js +147 -0
- package/dist/management/helpers.js +227 -0
- package/dist/management/router.js +48 -0
- package/dist/management/routes.js +197 -0
- package/dist/management-server.js +4 -0
- package/dist/management-standalone.js +31 -0
- package/dist/mcp/logging.js +57 -0
- package/dist/mcp/server.js +251 -0
- package/dist/mcp/transform.js +48 -0
- package/dist/mcp-server.js +18 -0
- package/dist/memory.js +2 -0
- package/dist/ollama.js +74 -0
- package/dist/plugin/hooks.js +168 -0
- package/dist/plugin/index.js +28 -0
- package/dist/plugin/init.js +109 -0
- package/dist/plugin/state.js +75 -0
- package/dist/plugin/tools.js +45 -0
- package/dist/plugin.js +2 -0
- package/dist/procedural/store.js +1 -0
- package/dist/procedural/types.js +1 -0
- package/dist/seed-nodes.js +804 -0
- package/dist/storage/compress-ops.js +129 -0
- package/dist/storage/compression/formatters.js +243 -0
- package/dist/storage/compression/index.js +107 -0
- package/dist/storage/compression/patterns.js +138 -0
- package/dist/storage/expiration.js +66 -0
- package/dist/storage/index.js +1 -0
- package/dist/storage/injection-events.js +82 -0
- package/dist/storage/lifecycle.js +65 -0
- package/dist/storage/maintenance.js +60 -0
- package/dist/storage/migrations/definitions.js +374 -0
- package/dist/storage/migrations/index.js +21 -0
- package/dist/storage/navigation.js +98 -0
- package/dist/storage/queries/base.js +44 -0
- package/dist/storage/queries/links.js +32 -0
- package/dist/storage/queries/nodes.js +189 -0
- package/dist/storage/queries/search-helpers.js +239 -0
- package/dist/storage/scoring.js +36 -0
- package/dist/storage/search.js +233 -0
- package/dist/storage/session-tracking.js +180 -0
- package/dist/storage/sqlite.js +329 -0
- package/dist/storage/tool-usage.js +56 -0
- package/dist/storage/types.js +1 -0
- package/dist/storage/utils.js +94 -0
- package/dist/tools/auto-test.js +24 -0
- package/dist/tools/cache-status.js +36 -0
- package/dist/tools/compress.js +186 -0
- package/dist/tools/core.js +307 -0
- package/dist/tools/dashboard.js +97 -0
- package/dist/tools/help.js +59 -0
- package/dist/tools/index.js +12 -0
- package/dist/tools/inject.js +91 -0
- package/dist/tools/injection-debug.js +48 -0
- package/dist/tools/journal.js +105 -0
- package/dist/tools/llm-compress.js +41 -0
- package/dist/tools/middle-term.js +68 -0
- package/dist/tools/playbook.js +64 -0
- package/dist/tools/reflect.js +291 -0
- package/dist/tools/search.js +188 -0
- package/dist/tools/session.js +189 -0
- package/dist/tools/shared.js +74 -0
- package/dist/tools/skill.js +37 -0
- package/dist/tools/stats.js +256 -0
- package/dist/tools/version.js +13 -0
- package/dist/tools.js +18 -0
- package/dist/utils/hybridScore.js +67 -0
- package/management/public/app.js +1529 -0
- package/management/public/index.html +486 -0
- package/management/public/three.min.js +6 -0
- package/package.json +65 -0
- package/scripts/download-models.ts +16 -0
- package/scripts/postinstall.cjs +30 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
export async function getNodeDepth(getNode, node, depthCache, visited = new Set()) {
|
|
2
|
+
if (!node.parentIds || node.parentIds.length === 0)
|
|
3
|
+
return 0;
|
|
4
|
+
if (visited.has(node.id))
|
|
5
|
+
return 0;
|
|
6
|
+
const cached = depthCache.get(node.id);
|
|
7
|
+
if (cached !== undefined)
|
|
8
|
+
return cached;
|
|
9
|
+
visited.add(node.id);
|
|
10
|
+
let maxParentDepth = 0;
|
|
11
|
+
for (const parentId of node.parentIds) {
|
|
12
|
+
try {
|
|
13
|
+
const parent = await getNode(parentId);
|
|
14
|
+
const parentDepth = await getNodeDepth(getNode, parent, depthCache, visited);
|
|
15
|
+
maxParentDepth = Math.max(maxParentDepth, parentDepth);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// Parent not found, skip
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const depth = 1 + maxParentDepth;
|
|
22
|
+
depthCache.set(node.id, depth);
|
|
23
|
+
return depth;
|
|
24
|
+
}
|
|
25
|
+
export async function retrieveFractal(getNode, id, maxDepth = 10) {
|
|
26
|
+
const node = await getNode(id);
|
|
27
|
+
const path = [];
|
|
28
|
+
const visited = new Set();
|
|
29
|
+
let currentNode = node;
|
|
30
|
+
let depth = 0;
|
|
31
|
+
while (currentNode && depth < maxDepth) {
|
|
32
|
+
path.push(currentNode);
|
|
33
|
+
if (!currentNode.parentIds || currentNode.parentIds.length === 0)
|
|
34
|
+
break;
|
|
35
|
+
if (visited.has(currentNode.id))
|
|
36
|
+
break;
|
|
37
|
+
visited.add(currentNode.id);
|
|
38
|
+
const parentId = currentNode.parentIds[0];
|
|
39
|
+
if (!parentId)
|
|
40
|
+
break;
|
|
41
|
+
try {
|
|
42
|
+
currentNode = await getNode(parentId);
|
|
43
|
+
depth++;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
node,
|
|
51
|
+
path,
|
|
52
|
+
depth,
|
|
53
|
+
relevanceScore: node.importance,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export async function getFractalStats(listNodes, getNode, scope) {
|
|
57
|
+
const depthCache = new Map();
|
|
58
|
+
const allNodes = await listNodes(scope);
|
|
59
|
+
const nodesPerLevel = { 0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 };
|
|
60
|
+
let hasEmbeddings = 0;
|
|
61
|
+
const scopes = { global: 0, project: 0 };
|
|
62
|
+
const parentCounts = [];
|
|
63
|
+
let maxDepth = 0;
|
|
64
|
+
for (const node of allNodes) {
|
|
65
|
+
nodesPerLevel[node.level]++;
|
|
66
|
+
scopes[node.scope]++;
|
|
67
|
+
if (node.embedding)
|
|
68
|
+
hasEmbeddings++;
|
|
69
|
+
if (node.parentIds && node.parentIds.length > 0) {
|
|
70
|
+
parentCounts.push(node.parentIds.length);
|
|
71
|
+
const parentDepth = await getNodeDepth(getNode, node, depthCache);
|
|
72
|
+
maxDepth = Math.max(maxDepth, parentDepth);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const totalNodes = allNodes.length;
|
|
76
|
+
const compressionRatios = {};
|
|
77
|
+
for (let level = 0; level < 5; level++) {
|
|
78
|
+
const current = nodesPerLevel[level];
|
|
79
|
+
const next = nodesPerLevel[(level + 1)];
|
|
80
|
+
compressionRatios[level] = next > 0 && current > 0 ? current / next : 0;
|
|
81
|
+
}
|
|
82
|
+
const fractalDimension = totalNodes > 1 && maxDepth > 0
|
|
83
|
+
? Math.log(totalNodes) / Math.log(1 + maxDepth)
|
|
84
|
+
: 0;
|
|
85
|
+
const avgChildren = parentCounts.length > 0
|
|
86
|
+
? parentCounts.reduce((a, b) => a + b, 0) / parentCounts.length
|
|
87
|
+
: 0;
|
|
88
|
+
return {
|
|
89
|
+
totalNodes,
|
|
90
|
+
nodesPerLevel,
|
|
91
|
+
compressionRatios,
|
|
92
|
+
fractalDimension: Math.round(fractalDimension * 100) / 100,
|
|
93
|
+
avgChildrenPerNode: Math.round(avgChildren * 100) / 100,
|
|
94
|
+
treeDepth: maxDepth,
|
|
95
|
+
hasEmbeddings,
|
|
96
|
+
scopes,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export function blobToEmbedding(blob) {
|
|
2
|
+
if (!blob)
|
|
3
|
+
return null;
|
|
4
|
+
const floats = new Float32Array(blob.buffer, blob.byteOffset, blob.length / 4);
|
|
5
|
+
return Array.from(floats);
|
|
6
|
+
}
|
|
7
|
+
export function embeddingToBlob(embedding) {
|
|
8
|
+
const floatArray = new Float32Array(embedding);
|
|
9
|
+
return Buffer.from(floatArray.buffer);
|
|
10
|
+
}
|
|
11
|
+
export function rowToNode(row) {
|
|
12
|
+
let embedding = null;
|
|
13
|
+
if (row.embedding_blob) {
|
|
14
|
+
embedding = blobToEmbedding(row.embedding_blob);
|
|
15
|
+
}
|
|
16
|
+
else if (row.embedding) {
|
|
17
|
+
embedding = JSON.parse(row.embedding);
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
id: row.id,
|
|
21
|
+
scope: row.scope,
|
|
22
|
+
label: row.label || undefined,
|
|
23
|
+
content: row.content,
|
|
24
|
+
summary: row.summary,
|
|
25
|
+
level: row.level,
|
|
26
|
+
parentIds: row.parent_ids ? JSON.parse(row.parent_ids) : null,
|
|
27
|
+
embedding,
|
|
28
|
+
createdAt: new Date(row.created_at),
|
|
29
|
+
updatedAt: new Date(row.updated_at),
|
|
30
|
+
importance: row.importance,
|
|
31
|
+
accessCount: row.access_count,
|
|
32
|
+
lastAccessed: row.last_accessed ? new Date(row.last_accessed) : null,
|
|
33
|
+
type: row.type,
|
|
34
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : null,
|
|
35
|
+
sticky: Boolean(row.sticky),
|
|
36
|
+
ttlDays: row.ttl_days ?? null,
|
|
37
|
+
expiresAt: row.expires_at ? new Date(row.expires_at) : null,
|
|
38
|
+
confidence: row.confidence ?? 0.5,
|
|
39
|
+
lastVerified: row.last_verified ? new Date(row.last_verified) : null,
|
|
40
|
+
usefulnessScore: row.usefulness_score ?? 0,
|
|
41
|
+
timesUsed: row.times_used ?? 0,
|
|
42
|
+
timesHelpful: row.times_helpful ?? 0,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export async function queryStoreLinks(db, sourceId, content, getNodeByLabel) {
|
|
2
|
+
const { extractLinks } = await import("../utils");
|
|
3
|
+
const links = extractLinks(content);
|
|
4
|
+
db.run("DELETE FROM memory_links WHERE source_id = ?", [sourceId]);
|
|
5
|
+
for (const label of links) {
|
|
6
|
+
let targetId = null;
|
|
7
|
+
try {
|
|
8
|
+
const target = await getNodeByLabel(db, "global", label);
|
|
9
|
+
if (!target) {
|
|
10
|
+
const targetProject = await getNodeByLabel(db, "project", label);
|
|
11
|
+
targetId = targetProject?.id ?? null;
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
targetId = target.id;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
catch { /* Target doesn't exist yet */ }
|
|
18
|
+
db.run("INSERT OR REPLACE INTO memory_links (source_id, target_label, target_id) VALUES (?, ?, ?)", [sourceId, label, targetId]);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function queryUpdateLinksForNewNode(db, label, nodeId) {
|
|
22
|
+
db.run("UPDATE memory_links SET target_id = ? WHERE target_label = ? AND target_id IS NULL", [nodeId, label]);
|
|
23
|
+
}
|
|
24
|
+
export function queryGetLinks(db, sourceId) {
|
|
25
|
+
return db.query("SELECT target_label, target_id FROM memory_links WHERE source_id = ?").all(sourceId);
|
|
26
|
+
}
|
|
27
|
+
export function queryDeleteLinks(db, sourceId) {
|
|
28
|
+
db.run("DELETE FROM memory_links WHERE source_id = ?", [sourceId]);
|
|
29
|
+
}
|
|
30
|
+
export function queryDeleteLinksForTarget(db, targetId) {
|
|
31
|
+
db.run("DELETE FROM memory_links WHERE target_id = ?", [targetId]);
|
|
32
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { embeddingToBlob, rowToNode } from "./base";
|
|
3
|
+
import { getHNSWIndex } from "../../hnsw-index";
|
|
4
|
+
function validateLabel(label) {
|
|
5
|
+
const trimmed = label.trim();
|
|
6
|
+
if (!/^[a-z0-9][a-z0-9-_:]{1,60}$/i.test(trimmed)) {
|
|
7
|
+
throw new Error(`Invalid label "${label}". Use letters/numbers/dash/underscore/colon (2-61 chars).`);
|
|
8
|
+
}
|
|
9
|
+
return trimmed;
|
|
10
|
+
}
|
|
11
|
+
export async function queryListNodes(db, scope, level, limit = 50, offset = 0, includeExpired = false) {
|
|
12
|
+
let query = "SELECT * FROM memory_nodes";
|
|
13
|
+
const params = [];
|
|
14
|
+
const conditions = [];
|
|
15
|
+
conditions.push("scope = ?");
|
|
16
|
+
params.push(scope);
|
|
17
|
+
if (level !== undefined) {
|
|
18
|
+
conditions.push("level = ?");
|
|
19
|
+
params.push(level);
|
|
20
|
+
}
|
|
21
|
+
if (!includeExpired) {
|
|
22
|
+
conditions.push("(expires_at IS NULL OR expires_at > ?)");
|
|
23
|
+
params.push(Date.now());
|
|
24
|
+
}
|
|
25
|
+
if (conditions.length > 0) {
|
|
26
|
+
query += " WHERE " + conditions.join(" AND ");
|
|
27
|
+
}
|
|
28
|
+
query += " ORDER BY importance DESC, created_at DESC";
|
|
29
|
+
query += " LIMIT ? OFFSET ?";
|
|
30
|
+
params.push(limit, offset);
|
|
31
|
+
const rows = db.query(query).all(...params);
|
|
32
|
+
return rows.map(rowToNode);
|
|
33
|
+
}
|
|
34
|
+
export async function queryGetNode(db, id) {
|
|
35
|
+
const row = db.query("SELECT * FROM memory_nodes WHERE id = ?").get(id);
|
|
36
|
+
if (!row)
|
|
37
|
+
return null;
|
|
38
|
+
return rowToNode(row);
|
|
39
|
+
}
|
|
40
|
+
export async function queryGetNodeByLabel(db, scope, label, includeExpired = false) {
|
|
41
|
+
const safeLabel = validateLabel(label);
|
|
42
|
+
const sql = includeExpired
|
|
43
|
+
? "SELECT id FROM memory_nodes WHERE label = ? AND scope = ?"
|
|
44
|
+
: "SELECT id FROM memory_nodes WHERE label = ? AND scope = ? AND (expires_at IS NULL OR expires_at > ?)";
|
|
45
|
+
const params = includeExpired ? [safeLabel, scope] : [safeLabel, scope, Date.now()];
|
|
46
|
+
const row = db.query(sql).get(...params);
|
|
47
|
+
return row;
|
|
48
|
+
}
|
|
49
|
+
export async function queryGetNodeByLabelFull(db, scope, label, includeExpired = false) {
|
|
50
|
+
const safeLabel = validateLabel(label);
|
|
51
|
+
const sql = includeExpired
|
|
52
|
+
? "SELECT * FROM memory_nodes WHERE label = ? AND scope = ?"
|
|
53
|
+
: "SELECT * FROM memory_nodes WHERE label = ? AND scope = ? AND (expires_at IS NULL OR expires_at > ?)";
|
|
54
|
+
const params = includeExpired ? [safeLabel, scope] : [safeLabel, scope, Date.now()];
|
|
55
|
+
const row = db.query(sql).get(...params);
|
|
56
|
+
if (!row) {
|
|
57
|
+
throw new Error(`Memory node not found: ${scope}:${safeLabel}`);
|
|
58
|
+
}
|
|
59
|
+
db.run("UPDATE memory_nodes SET access_count = access_count + 1, last_accessed = ? WHERE id = ?", [Date.now(), row.id]);
|
|
60
|
+
return { ...rowToNode(row), accessCount: row.access_count + 1 };
|
|
61
|
+
}
|
|
62
|
+
export async function queryGetNodeByPrefix(db, prefix) {
|
|
63
|
+
const rows = db.query("SELECT * FROM memory_nodes WHERE id LIKE ?").all(`${prefix}%`);
|
|
64
|
+
if (rows.length === 1) {
|
|
65
|
+
const row = rows[0];
|
|
66
|
+
db.run("UPDATE memory_nodes SET access_count = access_count + 1, last_accessed = ? WHERE id = ?", [Date.now(), row.id]);
|
|
67
|
+
return rowToNode(row);
|
|
68
|
+
}
|
|
69
|
+
if (rows.length > 1) {
|
|
70
|
+
throw new Error(`Ambiguous ID prefix "${prefix}" matches ${rows.length} nodes. Use full ID.`);
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
export async function queryCreateNode(db, node, storeLinks, updateLinksForNewNode, updateBM25Index) {
|
|
75
|
+
const now = Date.now();
|
|
76
|
+
const id = randomUUID();
|
|
77
|
+
const sticky = node.type === "skill" ? 1 : (node.sticky ? 1 : 0);
|
|
78
|
+
const ttlDays = node.ttlDays ?? null;
|
|
79
|
+
const expiresAt = ttlDays ? now + ttlDays * 86400000 : null;
|
|
80
|
+
const confidence = node.confidence ?? 0.5;
|
|
81
|
+
const usefulnessScore = node.usefulnessScore ?? 0;
|
|
82
|
+
const timesUsed = node.timesUsed ?? 0;
|
|
83
|
+
const timesHelpful = node.timesHelpful ?? 0;
|
|
84
|
+
db.run("INSERT INTO memory_nodes (id, scope, label, content, summary, level, parent_ids, embedding, embedding_blob, created_at, updated_at, importance, access_count, last_accessed, type, metadata, sticky, ttl_days, expires_at, confidence, usefulness_score, times_used, times_helpful) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
|
|
85
|
+
id,
|
|
86
|
+
node.scope,
|
|
87
|
+
node.label ?? "",
|
|
88
|
+
node.content,
|
|
89
|
+
node.summary ?? null,
|
|
90
|
+
node.level ?? 0,
|
|
91
|
+
node.parentIds ? JSON.stringify(node.parentIds) : null,
|
|
92
|
+
node.embedding ? JSON.stringify(node.embedding) : null,
|
|
93
|
+
node.embedding ? embeddingToBlob(node.embedding) : null,
|
|
94
|
+
now,
|
|
95
|
+
now,
|
|
96
|
+
node.importance ?? 0.5,
|
|
97
|
+
0,
|
|
98
|
+
null,
|
|
99
|
+
node.type ?? null,
|
|
100
|
+
node.metadata ? JSON.stringify(node.metadata) : null,
|
|
101
|
+
sticky,
|
|
102
|
+
ttlDays,
|
|
103
|
+
expiresAt,
|
|
104
|
+
confidence,
|
|
105
|
+
usefulnessScore,
|
|
106
|
+
timesUsed,
|
|
107
|
+
timesHelpful,
|
|
108
|
+
]);
|
|
109
|
+
// Store links
|
|
110
|
+
await storeLinks(node.scope, id, node.content);
|
|
111
|
+
if (node.label) {
|
|
112
|
+
await updateLinksForNewNode(node.scope, node.label, id);
|
|
113
|
+
}
|
|
114
|
+
updateBM25Index(db, id, node.content, node.label, node.scope);
|
|
115
|
+
// HNSW add is outside transaction (in-memory)
|
|
116
|
+
if (node.embedding) {
|
|
117
|
+
const hnsw = getHNSWIndex();
|
|
118
|
+
await hnsw.addNode(node.scope, id, node.embedding);
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
id,
|
|
122
|
+
scope: node.scope,
|
|
123
|
+
label: node.label,
|
|
124
|
+
content: node.content,
|
|
125
|
+
summary: node.summary ?? null,
|
|
126
|
+
level: node.level ?? 0,
|
|
127
|
+
parentIds: node.parentIds ?? null,
|
|
128
|
+
embedding: node.embedding ?? null,
|
|
129
|
+
createdAt: new Date(now),
|
|
130
|
+
updatedAt: new Date(now),
|
|
131
|
+
importance: node.importance ?? 0.5,
|
|
132
|
+
accessCount: 0,
|
|
133
|
+
lastAccessed: null,
|
|
134
|
+
type: node.type ?? null,
|
|
135
|
+
metadata: node.metadata ?? null,
|
|
136
|
+
sticky: Boolean(sticky),
|
|
137
|
+
ttlDays,
|
|
138
|
+
expiresAt: expiresAt ? new Date(expiresAt) : null,
|
|
139
|
+
confidence: node.confidence ?? 0.5,
|
|
140
|
+
lastVerified: null,
|
|
141
|
+
usefulnessScore: node.usefulnessScore ?? 0,
|
|
142
|
+
timesUsed: node.timesUsed ?? 0,
|
|
143
|
+
timesHelpful: node.timesHelpful ?? 0,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
const UPDATE_FIELDS = {
|
|
147
|
+
content: (v) => [["content = ?", v]],
|
|
148
|
+
summary: (v) => [["summary = ?", v]],
|
|
149
|
+
level: (v) => [["level = ?", v]],
|
|
150
|
+
parentIds: (v) => [["parent_ids = ?", v ? JSON.stringify(v) : null]],
|
|
151
|
+
importance: (v) => [["importance = ?", v]],
|
|
152
|
+
type: (v) => [["type = ?", v]],
|
|
153
|
+
metadata: (v) => [["metadata = ?", v ? JSON.stringify(v) : null]],
|
|
154
|
+
embedding: (v) => [
|
|
155
|
+
["embedding = ?", v ? JSON.stringify(v) : null],
|
|
156
|
+
["embedding_blob = ?", v ? embeddingToBlob(v) : null],
|
|
157
|
+
],
|
|
158
|
+
sticky: (v) => [["sticky = ?", v ? 1 : 0]],
|
|
159
|
+
ttlDays: (v) => {
|
|
160
|
+
const now = Date.now();
|
|
161
|
+
return [
|
|
162
|
+
["ttl_days = ?", v],
|
|
163
|
+
["expires_at = ?", v ? now + v * 86400000 : null],
|
|
164
|
+
];
|
|
165
|
+
},
|
|
166
|
+
confidence: (v) => [["confidence = ?", v]],
|
|
167
|
+
usefulnessScore: (v) => [["usefulness_score = ?", v]],
|
|
168
|
+
timesHelpful: (v) => [["times_helpful = ?", v]],
|
|
169
|
+
};
|
|
170
|
+
export async function queryUpdateNode(db, id, updates) {
|
|
171
|
+
const setClauses = ["updated_at = ?"];
|
|
172
|
+
const params = [Date.now()];
|
|
173
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
174
|
+
if (value === undefined)
|
|
175
|
+
continue;
|
|
176
|
+
const field = UPDATE_FIELDS[key];
|
|
177
|
+
if (!field)
|
|
178
|
+
continue;
|
|
179
|
+
for (const [clause, param] of field(value)) {
|
|
180
|
+
setClauses.push(clause);
|
|
181
|
+
params.push(param);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
params.push(id);
|
|
185
|
+
db.run(`UPDATE memory_nodes SET ${setClauses.join(", ")} WHERE id = ?`, params);
|
|
186
|
+
}
|
|
187
|
+
export async function queryDeleteNode(db, id) {
|
|
188
|
+
db.run("DELETE FROM memory_nodes WHERE id = ?", [id]);
|
|
189
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { tokenize } from "../utils";
|
|
2
|
+
import { estimateTokens } from "../../embeddings";
|
|
3
|
+
const CHUNK_SIZE_TOKENS = 500;
|
|
4
|
+
const OVERLAP_TOKENS = 50;
|
|
5
|
+
export function cosineSimilarity(a, b) {
|
|
6
|
+
if (!a || !b || a.length !== b.length)
|
|
7
|
+
return 0;
|
|
8
|
+
let dotProduct = 0;
|
|
9
|
+
let normA = 0;
|
|
10
|
+
let normB = 0;
|
|
11
|
+
for (let i = 0; i < a.length; i++) {
|
|
12
|
+
const ai = a[i] ?? 0;
|
|
13
|
+
const bi = b[i] ?? 0;
|
|
14
|
+
dotProduct += ai * bi;
|
|
15
|
+
normA += ai * ai;
|
|
16
|
+
normB += bi * bi;
|
|
17
|
+
}
|
|
18
|
+
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
|
|
19
|
+
}
|
|
20
|
+
export function updateBM25Index(db, nodeId, content, label, scope) {
|
|
21
|
+
const tokens = tokenize(content + ' ' + (label ?? ''));
|
|
22
|
+
const termFreq = new Map();
|
|
23
|
+
for (const token of tokens) {
|
|
24
|
+
termFreq.set(token, (termFreq.get(token) ?? 0) + 1);
|
|
25
|
+
}
|
|
26
|
+
db.run("DELETE FROM bm25_index WHERE node_id = ?", [nodeId]);
|
|
27
|
+
db.run("DELETE FROM bm25_doc_stats WHERE node_id = ?", [nodeId]);
|
|
28
|
+
db.run("INSERT INTO bm25_doc_stats (node_id, token_count, scope) VALUES (?, ?, ?)", [nodeId, tokens.length, scope]);
|
|
29
|
+
for (const [term, freq] of termFreq) {
|
|
30
|
+
db.run("INSERT INTO bm25_index (term, node_id, frequency, scope) VALUES (?, ?, ?, ?)", [term, nodeId, freq, scope]);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function removeBM25Index(db, nodeId) {
|
|
34
|
+
db.run("DELETE FROM bm25_index WHERE node_id = ?", [nodeId]);
|
|
35
|
+
db.run("DELETE FROM bm25_doc_stats WHERE node_id = ?", [nodeId]);
|
|
36
|
+
}
|
|
37
|
+
export function calculateDynamicBm25Weight(queryLength, baseWeight) {
|
|
38
|
+
if (queryLength === 0)
|
|
39
|
+
return baseWeight;
|
|
40
|
+
const decay = Math.max(0.3, 0.6 - 0.002 * queryLength);
|
|
41
|
+
return Math.min(baseWeight, decay);
|
|
42
|
+
}
|
|
43
|
+
export function computeRecencyScore(lastAccessed) {
|
|
44
|
+
if (!lastAccessed)
|
|
45
|
+
return 0;
|
|
46
|
+
const hoursSinceAccess = (Date.now() - lastAccessed.getTime()) / (1000 * 60 * 60);
|
|
47
|
+
return Math.exp(-hoursSinceAccess / 24);
|
|
48
|
+
}
|
|
49
|
+
export function chunkContent(content, nodeId) {
|
|
50
|
+
const tokens = content.split(/\s+/);
|
|
51
|
+
const chunks = [];
|
|
52
|
+
const step = CHUNK_SIZE_TOKENS - OVERLAP_TOKENS;
|
|
53
|
+
for (let i = 0; i < tokens.length; i += step) {
|
|
54
|
+
const chunkTokens = tokens.slice(i, i + CHUNK_SIZE_TOKENS);
|
|
55
|
+
if (chunkTokens.length < 20) {
|
|
56
|
+
if (chunks.length > 0)
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
chunks.push({
|
|
60
|
+
id: `${nodeId}:chunk:${chunks.length}`,
|
|
61
|
+
nodeId,
|
|
62
|
+
content: chunkTokens.join(' '),
|
|
63
|
+
startToken: i,
|
|
64
|
+
endToken: Math.min(i + CHUNK_SIZE_TOKENS, tokens.length),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return chunks;
|
|
68
|
+
}
|
|
69
|
+
export function isLargeNode(content) {
|
|
70
|
+
return estimateTokens(content) > CHUNK_SIZE_TOKENS;
|
|
71
|
+
}
|
|
72
|
+
export function rerankResults(query, nodes, topK = 10) {
|
|
73
|
+
if (nodes.length <= topK) {
|
|
74
|
+
return nodes.map(n => ({ node: n, originalScore: n.importance ?? 0, rerankScore: 1, finalScore: n.importance ?? 0 }));
|
|
75
|
+
}
|
|
76
|
+
const queryTerms = new Set(query.toLowerCase().split(/\s+/).filter(t => t.length > 2));
|
|
77
|
+
const results = [];
|
|
78
|
+
for (const node of nodes) {
|
|
79
|
+
const contentLower = node.content.toLowerCase();
|
|
80
|
+
const labelLower = (node.label ?? '').toLowerCase();
|
|
81
|
+
let keywordScore = 0;
|
|
82
|
+
const contentTerms = new Set(contentLower.split(/\s+/).filter(t => t.length > 2));
|
|
83
|
+
for (const term of queryTerms) {
|
|
84
|
+
if (contentTerms.has(term))
|
|
85
|
+
keywordScore += 1;
|
|
86
|
+
if (labelLower.includes(term))
|
|
87
|
+
keywordScore += 0.5;
|
|
88
|
+
}
|
|
89
|
+
keywordScore = Math.min(1, keywordScore / Math.max(1, queryTerms.size));
|
|
90
|
+
const firstLine = contentLower.split('\n')[0] ?? '';
|
|
91
|
+
const positionScore = firstLine.includes(query.toLowerCase()) ? 0.3 : 0;
|
|
92
|
+
const termDensity = keywordScore * 100 / Math.max(1, estimateTokens(node.content) / 10);
|
|
93
|
+
const densityScore = Math.min(0.4, termDensity * 0.1);
|
|
94
|
+
const rerankScore = keywordScore * 0.4 + positionScore + densityScore;
|
|
95
|
+
const finalScore = (node.importance ?? 0) * 0.7 + rerankScore * 0.3;
|
|
96
|
+
results.push({ node, originalScore: node.importance ?? 0, rerankScore, finalScore });
|
|
97
|
+
}
|
|
98
|
+
return results
|
|
99
|
+
.sort((a, b) => b.finalScore - a.finalScore)
|
|
100
|
+
.slice(0, topK);
|
|
101
|
+
}
|
|
102
|
+
const BM25_K1 = 1.2;
|
|
103
|
+
const BM25_B = 0.75;
|
|
104
|
+
export function computeBM25TermScore(params) {
|
|
105
|
+
const { termFrequency, documentFrequency, documentLength, averageDocumentLength, totalDocuments } = params;
|
|
106
|
+
if (documentFrequency === 0 || totalDocuments === 0)
|
|
107
|
+
return 0;
|
|
108
|
+
const idf = Math.log(totalDocuments / documentFrequency);
|
|
109
|
+
const tf = termFrequency;
|
|
110
|
+
const dl = documentLength;
|
|
111
|
+
const avgdl = averageDocumentLength;
|
|
112
|
+
return idf * (tf * (BM25_K1 + 1)) / (tf + BM25_K1 * (1 - BM25_B + BM25_B * (dl / avgdl)));
|
|
113
|
+
}
|
|
114
|
+
export function computeBM25Scores(input) {
|
|
115
|
+
const { queryTerms, nodes } = input;
|
|
116
|
+
const scores = new Map();
|
|
117
|
+
if (queryTerms.length === 0 || nodes.length === 0)
|
|
118
|
+
return scores;
|
|
119
|
+
const nodeTokens = new Map();
|
|
120
|
+
let totalLength = 0;
|
|
121
|
+
for (const node of nodes) {
|
|
122
|
+
const tokens = tokenize(node.content + ' ' + (node.label ?? ''));
|
|
123
|
+
nodeTokens.set(node.id, { tokens, length: tokens.length });
|
|
124
|
+
totalLength += tokens.length;
|
|
125
|
+
}
|
|
126
|
+
const avgdl = totalLength / nodes.length;
|
|
127
|
+
for (const node of nodes) {
|
|
128
|
+
const { tokens, length } = nodeTokens.get(node.id);
|
|
129
|
+
let nodeScore = 0;
|
|
130
|
+
for (const term of queryTerms) {
|
|
131
|
+
const tf = tokens.filter(t => t === term).length;
|
|
132
|
+
if (tf === 0)
|
|
133
|
+
continue;
|
|
134
|
+
const df = nodes.filter(n => {
|
|
135
|
+
const nt = nodeTokens.get(n.id);
|
|
136
|
+
return nt && nt.tokens.includes(term);
|
|
137
|
+
}).length;
|
|
138
|
+
const score = computeBM25TermScore({
|
|
139
|
+
termFrequency: tf,
|
|
140
|
+
documentFrequency: df,
|
|
141
|
+
documentLength: length,
|
|
142
|
+
averageDocumentLength: avgdl,
|
|
143
|
+
totalDocuments: nodes.length,
|
|
144
|
+
});
|
|
145
|
+
nodeScore += score;
|
|
146
|
+
}
|
|
147
|
+
scores.set(node.id, nodeScore);
|
|
148
|
+
}
|
|
149
|
+
const maxScore = Math.max(...Array.from(scores.values()), 1);
|
|
150
|
+
for (const [id, score] of scores) {
|
|
151
|
+
scores.set(id, score / maxScore);
|
|
152
|
+
}
|
|
153
|
+
return scores;
|
|
154
|
+
}
|
|
155
|
+
export function computeBM25ScoresSQL(db, scope, queryTerms, nodeIds) {
|
|
156
|
+
const scores = new Map();
|
|
157
|
+
if (queryTerms.length === 0 || nodeIds.length === 0)
|
|
158
|
+
return scores;
|
|
159
|
+
const totalDocs = db.query("SELECT COUNT(*) as count FROM bm25_doc_stats WHERE scope = ?").get(scope)?.count ?? 0;
|
|
160
|
+
if (totalDocs === 0)
|
|
161
|
+
return scores;
|
|
162
|
+
const avgdl = db.query("SELECT AVG(token_count) as avg FROM bm25_doc_stats WHERE scope = ?").get(scope)?.avg ?? 0;
|
|
163
|
+
for (const nodeId of nodeIds) {
|
|
164
|
+
let nodeScore = 0;
|
|
165
|
+
for (const term of queryTerms) {
|
|
166
|
+
const tfRow = db.query("SELECT frequency FROM bm25_index WHERE term = ? AND node_id = ?").get(term, nodeId);
|
|
167
|
+
if (!tfRow)
|
|
168
|
+
continue;
|
|
169
|
+
const df = db.query("SELECT COUNT(DISTINCT node_id) as df FROM bm25_index WHERE term = ? AND scope = ?").get(term, scope)?.df ?? 0;
|
|
170
|
+
if (df === 0)
|
|
171
|
+
continue;
|
|
172
|
+
const docLength = db.query("SELECT token_count FROM bm25_doc_stats WHERE node_id = ?").get(nodeId)?.token_count ?? avgdl;
|
|
173
|
+
nodeScore += computeBM25TermScore({
|
|
174
|
+
termFrequency: tfRow.frequency,
|
|
175
|
+
documentFrequency: df,
|
|
176
|
+
documentLength: docLength,
|
|
177
|
+
averageDocumentLength: avgdl,
|
|
178
|
+
totalDocuments: totalDocs,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
scores.set(nodeId, nodeScore);
|
|
182
|
+
}
|
|
183
|
+
const maxScore = Math.max(...Array.from(scores.values()), 1);
|
|
184
|
+
for (const [id, score] of scores) {
|
|
185
|
+
scores.set(id, score / maxScore);
|
|
186
|
+
}
|
|
187
|
+
return scores;
|
|
188
|
+
}
|
|
189
|
+
export function buildScoredNodes(query, nodesWithEmbeddings, weights, options) {
|
|
190
|
+
const scoredNodes = [];
|
|
191
|
+
for (const { node, embedding, hnswScore } of nodesWithEmbeddings) {
|
|
192
|
+
const level = node.level;
|
|
193
|
+
if (options?.minLevel !== undefined && level < options.minLevel)
|
|
194
|
+
continue;
|
|
195
|
+
if (options?.maxLevel !== undefined && level > options.maxLevel)
|
|
196
|
+
continue;
|
|
197
|
+
if (options?.minUsefulness !== undefined && (node.usefulnessScore ?? 0) < options.minUsefulness)
|
|
198
|
+
continue;
|
|
199
|
+
const baseScore = hnswScore !== undefined
|
|
200
|
+
? (cosineSimilarity(query, embedding) + hnswScore) / 2
|
|
201
|
+
: cosineSimilarity(query, embedding);
|
|
202
|
+
const levelWeight = weights[level] ?? 1;
|
|
203
|
+
const confidence = node.confidence ?? 0.5;
|
|
204
|
+
const confidenceWeight = 0.5 + 0.5 * confidence;
|
|
205
|
+
scoredNodes.push({
|
|
206
|
+
...node,
|
|
207
|
+
importance: baseScore * levelWeight * confidenceWeight * (1 + (node.usefulnessScore ?? 0) * 0.1),
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
return scoredNodes;
|
|
211
|
+
}
|
|
212
|
+
export function computeFinalScores(scoredNodes, options) {
|
|
213
|
+
const bm25Weight = options?.bm25Weight ?? 0.4;
|
|
214
|
+
const queryText = options?.queryText ?? "";
|
|
215
|
+
const queryLength = queryText ? tokenize(queryText).length : 0;
|
|
216
|
+
const dynamicBm25Weight = calculateDynamicBm25Weight(queryLength, bm25Weight);
|
|
217
|
+
const bm25Scores = options?.bm25Scores ?? (bm25Weight > 0 && queryText
|
|
218
|
+
? computeBM25Scores({ queryTerms: tokenize(queryText), nodes: scoredNodes })
|
|
219
|
+
: new Map());
|
|
220
|
+
const finalNodes = [];
|
|
221
|
+
for (const node of scoredNodes) {
|
|
222
|
+
const bm25Score = bm25Scores.get(node.id) ?? 0;
|
|
223
|
+
const recencyScore = computeRecencyScore(node.lastAccessed);
|
|
224
|
+
let finalScore;
|
|
225
|
+
if (bm25Weight > 0 && queryText) {
|
|
226
|
+
const semanticWeight = 1 - dynamicBm25Weight;
|
|
227
|
+
finalScore = ((node.importance ?? 0) * semanticWeight + bm25Score * dynamicBm25Weight);
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
finalScore = node.importance ?? 0;
|
|
231
|
+
}
|
|
232
|
+
finalScore = finalScore * (1 + recencyScore * 0.2);
|
|
233
|
+
finalNodes.push({
|
|
234
|
+
...node,
|
|
235
|
+
importance: finalScore,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
return finalNodes;
|
|
239
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { withRetry } from "./utils";
|
|
2
|
+
import { memLog } from "../logging";
|
|
3
|
+
export async function runScoreDecay(getDb, decayDays) {
|
|
4
|
+
const now = Date.now();
|
|
5
|
+
const cutoffMs = now - decayDays * 24 * 60 * 60 * 1000;
|
|
6
|
+
const msPerDay = 24 * 60 * 60 * 1000;
|
|
7
|
+
let total = 0;
|
|
8
|
+
for (const scope of ["global", "project"]) {
|
|
9
|
+
const db = await getDb(scope);
|
|
10
|
+
const result = await withRetry(() => {
|
|
11
|
+
return db.run(`UPDATE memory_nodes SET
|
|
12
|
+
usefulness_score = MAX(0, usefulness_score - MIN(1.0, (? - COALESCE(last_accessed, updated_at)) / ?) * 0.5),
|
|
13
|
+
updated_at = ?
|
|
14
|
+
WHERE COALESCE(last_accessed, updated_at) < ? AND usefulness_score > 0 AND (type IS NULL OR type != 'skill')`, [now, decayDays * msPerDay, now, cutoffMs]);
|
|
15
|
+
});
|
|
16
|
+
total += Number(result.changes ?? 0);
|
|
17
|
+
}
|
|
18
|
+
if (total > 0) {
|
|
19
|
+
memLog("info", "score-decay", `Decayed ${total} nodes via SQL`);
|
|
20
|
+
}
|
|
21
|
+
return total;
|
|
22
|
+
}
|
|
23
|
+
export function calculateNodeConfidence(node) {
|
|
24
|
+
if (node.type === "skill")
|
|
25
|
+
return node.confidence;
|
|
26
|
+
const age = Date.now() - node.updatedAt.getTime();
|
|
27
|
+
const ageInDays = age / (24 * 60 * 60 * 1000);
|
|
28
|
+
const baseConfidence = node.confidence;
|
|
29
|
+
const decayFactor = Math.exp(-ageInDays / 365);
|
|
30
|
+
const ageConfidence = baseConfidence * decayFactor;
|
|
31
|
+
const verifiedBonus = node.lastVerified
|
|
32
|
+
? Math.max(0, 0.1 - (Date.now() - node.lastVerified.getTime()) / (24 * 60 * 60 * 1000 * 30))
|
|
33
|
+
: 0;
|
|
34
|
+
const finalConfidence = Math.max(0, Math.min(1, ageConfidence + verifiedBonus));
|
|
35
|
+
return Math.round(finalConfidence * 100) / 100;
|
|
36
|
+
}
|