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,329 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { Database } from "bun:sqlite";
|
|
5
|
+
import { runMigrations, getConfig, setConfig } from "./migrations";
|
|
6
|
+
import { getHNSWIndex } from "../hnsw-index";
|
|
7
|
+
import { tokenize, extractLinks, embeddingToBlob, blobToEmbedding, withRetry, withRetryableTransaction } from "./utils";
|
|
8
|
+
export { extractLinks, embeddingToBlob, blobToEmbedding, tokenize, withRetry, withRetryableTransaction };
|
|
9
|
+
import { queryListNodes, queryGetNodeByLabel, queryGetNodeByLabelFull, queryGetNodeByPrefix, queryCreateNode, queryUpdateNode, queryDeleteNode } from "./queries/nodes";
|
|
10
|
+
import { queryStoreLinks, queryUpdateLinksForNewNode, queryGetLinks, queryDeleteLinks } from "./queries/links";
|
|
11
|
+
import { updateBM25Index, removeBM25Index } from "./queries/search-helpers";
|
|
12
|
+
import { CompressionHelper, COMPRESSION_LEVELS } from "./compression";
|
|
13
|
+
export { CompressionHelper, COMPRESSION_LEVELS };
|
|
14
|
+
import { memLog } from "../logging";
|
|
15
|
+
import { insertToolUsageLog, queryToolPatterns, queryFrequentSequences, deleteUsageLog, getToolCategory } from "./tool-usage";
|
|
16
|
+
import { insertAgentToolCall, createSessionMetrics as createSessionMetricsRow, updateSessionMetrics, incrementSessionToolCall, getSessionStats as getSessionStatsForSession } from "./session-tracking";
|
|
17
|
+
import { insertInjectionMetrics, getPendingInjections as getPendingInjectionRows, markInjectionProcessed, updateMemoryToolCall, finalizeInjection, insertInjectionFeedback, queryInjectionMetrics, querySessionMetrics } from "./injection-events";
|
|
18
|
+
import { runScoreDecay as runScoreDecayFn, calculateNodeConfidence as calculateNodeConfidenceFn } from "./scoring";
|
|
19
|
+
import { ensureSeed as ensureSeedFn, resolveNode as resolveNodeFn, getNode as getNodeFn, verifyNode as verifyNodeFn } from "./lifecycle";
|
|
20
|
+
import { searchByEmbedding as searchByEmbeddingFn, detectTopicBoundaries as detectTopicBoundariesFn, drilldownQuery as drilldownQueryFn, getDrilldownPath as getDrilldownPathFn } from "./search";
|
|
21
|
+
import { retrieveFractal as retrieveFractalFn, getFractalStats as getFractalStatsFn } from "./navigation";
|
|
22
|
+
import { getCompressionCandidates as getCompressionCandidatesFn, runCompression as runCompressionFn, runPatternExtraction as runPatternExtractionFn } from "./compress-ops";
|
|
23
|
+
import { getExpiredNodes as getExpiredNodesFn, deleteExpiredNodes as deleteExpiredNodesFn, pruneNodes as pruneNodesFn } from "./expiration";
|
|
24
|
+
import { backfillLinks as backfillLinksFn, backfillBinaryEmbeddingsAndBM25 as backfillBinaryEmbeddingsAndBM25Fn, rebuildHNSWIndex as rebuildHNSWIndexFn } from "./maintenance";
|
|
25
|
+
const SEED_BLOCKS = [
|
|
26
|
+
{ scope: "global", label: "persona" },
|
|
27
|
+
{ scope: "global", label: "human" },
|
|
28
|
+
{ scope: "project", label: "project" },
|
|
29
|
+
];
|
|
30
|
+
function scopeDbPath(projectDirectory, scope, globalDbPath) {
|
|
31
|
+
return scope === "global"
|
|
32
|
+
? (globalDbPath ?? path.join(os.homedir(), ".config", "opencode", "memory.db"))
|
|
33
|
+
: path.join(projectDirectory, ".opencode", "memory.db");
|
|
34
|
+
}
|
|
35
|
+
function validateLabel(label) {
|
|
36
|
+
const trimmed = label.trim();
|
|
37
|
+
if (!/^[a-z0-9][a-z0-9-_:]{1,60}$/i.test(trimmed)) {
|
|
38
|
+
throw new Error(`Invalid label "${label}". Use letters/numbers/dash/underscore/colon (2-61 chars).`);
|
|
39
|
+
}
|
|
40
|
+
return trimmed;
|
|
41
|
+
}
|
|
42
|
+
class SqliteMemoryStore {
|
|
43
|
+
dbs = new Map();
|
|
44
|
+
dbInitPromises = new Map();
|
|
45
|
+
idScopeCache = new Map();
|
|
46
|
+
projectDirectory;
|
|
47
|
+
globalDbPath;
|
|
48
|
+
constructor(projectDirectory, globalDbPath) {
|
|
49
|
+
this.projectDirectory = projectDirectory;
|
|
50
|
+
this.globalDbPath = globalDbPath;
|
|
51
|
+
}
|
|
52
|
+
async getDb(scope) {
|
|
53
|
+
const key = `${scope}:${this.projectDirectory}:${this.globalDbPath ?? ""}`;
|
|
54
|
+
if (this.dbs.has(key)) {
|
|
55
|
+
return this.dbs.get(key);
|
|
56
|
+
}
|
|
57
|
+
const existing = this.dbInitPromises.get(key);
|
|
58
|
+
if (existing)
|
|
59
|
+
return existing;
|
|
60
|
+
const promise = this.initDb(key, scope);
|
|
61
|
+
this.dbInitPromises.set(key, promise);
|
|
62
|
+
try {
|
|
63
|
+
const db = await promise;
|
|
64
|
+
return db;
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
this.dbInitPromises.delete(key);
|
|
68
|
+
throw err;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async initDb(key, scope) {
|
|
72
|
+
const dbPath = scopeDbPath(this.projectDirectory, scope, this.globalDbPath);
|
|
73
|
+
const dbDir = path.dirname(dbPath);
|
|
74
|
+
await fs.mkdir(dbDir, { recursive: true });
|
|
75
|
+
const db = new Database(dbPath);
|
|
76
|
+
db.run("PRAGMA journal_mode = WAL");
|
|
77
|
+
db.run("PRAGMA synchronous = NORMAL");
|
|
78
|
+
db.run("PRAGMA busy_timeout = 5000");
|
|
79
|
+
runMigrations(db);
|
|
80
|
+
this.dbs.set(key, db);
|
|
81
|
+
this.dbInitPromises.delete(key);
|
|
82
|
+
return db;
|
|
83
|
+
}
|
|
84
|
+
async getGlobalDb() {
|
|
85
|
+
return this.getDb("global");
|
|
86
|
+
}
|
|
87
|
+
async runScoreDecay(decayDays) {
|
|
88
|
+
return runScoreDecayFn((s) => this.getDb(s), decayDays);
|
|
89
|
+
}
|
|
90
|
+
async close() {
|
|
91
|
+
for (const [key, db] of this.dbs) {
|
|
92
|
+
try {
|
|
93
|
+
db.close();
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
memLog("error", "storage", `Error closing database ${key}:`, { error });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
this.dbs.clear();
|
|
100
|
+
this.idScopeCache.clear();
|
|
101
|
+
}
|
|
102
|
+
async ensureSeed() {
|
|
103
|
+
return ensureSeedFn((s) => this.getDb(s), SEED_BLOCKS);
|
|
104
|
+
}
|
|
105
|
+
async listNodes(scope, level, limit = 50, offset = 0, includeExpired) {
|
|
106
|
+
const scopes = scope === "all" ? ["global", "project"] : [scope];
|
|
107
|
+
const nodes = [];
|
|
108
|
+
for (const s of scopes) {
|
|
109
|
+
const db = await this.getDb(s);
|
|
110
|
+
const rows = await queryListNodes(db, s, level, limit, offset, includeExpired);
|
|
111
|
+
nodes.push(...rows);
|
|
112
|
+
}
|
|
113
|
+
return nodes;
|
|
114
|
+
}
|
|
115
|
+
async getNode(id) {
|
|
116
|
+
return getNodeFn((s) => this.getDb(s), id);
|
|
117
|
+
}
|
|
118
|
+
async getNodeByLabel(scope, label) {
|
|
119
|
+
const db = await this.getDb(scope);
|
|
120
|
+
return queryGetNodeByLabelFull(db, scope, label, false);
|
|
121
|
+
}
|
|
122
|
+
async getNodeByPrefix(prefix) {
|
|
123
|
+
const node = await queryGetNodeByPrefix(await this.getDb("global"), prefix)
|
|
124
|
+
?? await queryGetNodeByPrefix(await this.getDb("project"), prefix);
|
|
125
|
+
return node;
|
|
126
|
+
}
|
|
127
|
+
async createNode(node) {
|
|
128
|
+
const db = await this.getDb(node.scope);
|
|
129
|
+
return await withRetryableTransaction(db, async () => {
|
|
130
|
+
return await queryCreateNode(db, node, (scope, id, content) => this.storeLinks(scope, id, content), (scope, label, id) => this.updateLinksForNewNode(scope, label, id), (db, id, content, label, scope) => updateBM25Index(db, id, content, label, scope));
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
async updateNode(id, updates) {
|
|
134
|
+
const { db, scope } = await resolveNodeFn((s) => this.getDb(s), this.idScopeCache, id);
|
|
135
|
+
await withRetryableTransaction(db, async () => {
|
|
136
|
+
await queryUpdateNode(db, id, updates);
|
|
137
|
+
if (updates.content !== undefined) {
|
|
138
|
+
await this.storeLinks(scope, id, updates.content);
|
|
139
|
+
const labelRow = db.query("SELECT label FROM memory_nodes WHERE id = ?").get(id);
|
|
140
|
+
updateBM25Index(db, id, updates.content, labelRow?.label, scope);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
async deleteNode(id) {
|
|
145
|
+
const { db, scope } = await resolveNodeFn((s) => this.getDb(s), this.idScopeCache, id);
|
|
146
|
+
await withRetryableTransaction(db, async () => {
|
|
147
|
+
queryDeleteNode(db, id);
|
|
148
|
+
queryDeleteLinks(db, id);
|
|
149
|
+
removeBM25Index(db, id);
|
|
150
|
+
});
|
|
151
|
+
this.idScopeCache.delete(id);
|
|
152
|
+
const hnsw = getHNSWIndex();
|
|
153
|
+
await hnsw.removeNode(scope, id);
|
|
154
|
+
}
|
|
155
|
+
async getConfig(scope, key, defaultValue) {
|
|
156
|
+
const db = await this.getDb(scope);
|
|
157
|
+
return getConfig(db, key, defaultValue);
|
|
158
|
+
}
|
|
159
|
+
async setConfig(scope, key, value) {
|
|
160
|
+
const db = await this.getDb(scope);
|
|
161
|
+
await withRetry(() => setConfig(db, key, value));
|
|
162
|
+
}
|
|
163
|
+
async storeLinks(scope, sourceId, content) {
|
|
164
|
+
const db = await this.getDb(scope);
|
|
165
|
+
await queryStoreLinks(db, sourceId, content, queryGetNodeByLabel);
|
|
166
|
+
}
|
|
167
|
+
async updateLinksForNewNode(scope, label, nodeId) {
|
|
168
|
+
const db = await this.getDb(scope);
|
|
169
|
+
queryUpdateLinksForNewNode(db, label, nodeId);
|
|
170
|
+
}
|
|
171
|
+
async backfillLinks(scope) {
|
|
172
|
+
const db = await this.getDb(scope);
|
|
173
|
+
return backfillLinksFn(db);
|
|
174
|
+
}
|
|
175
|
+
async getLinkedNodes(scope, sourceId) {
|
|
176
|
+
const db = await this.getDb(scope);
|
|
177
|
+
const rows = queryGetLinks(db, sourceId);
|
|
178
|
+
const linkedNodes = [];
|
|
179
|
+
for (const row of rows) {
|
|
180
|
+
if (row.target_id) {
|
|
181
|
+
try {
|
|
182
|
+
const node = await this.getNode(row.target_id);
|
|
183
|
+
linkedNodes.push(node);
|
|
184
|
+
}
|
|
185
|
+
catch { /* Node was deleted */ }
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return linkedNodes;
|
|
189
|
+
}
|
|
190
|
+
async backfillBinaryEmbeddingsAndBM25(scope) {
|
|
191
|
+
const db = await this.getDb(scope);
|
|
192
|
+
return backfillBinaryEmbeddingsAndBM25Fn(db, scope);
|
|
193
|
+
}
|
|
194
|
+
async rebuildHNSWIndex(scope) {
|
|
195
|
+
return rebuildHNSWIndexFn((s) => this.getDb(s), scope);
|
|
196
|
+
}
|
|
197
|
+
async searchByEmbedding(query, limit = 5, options) {
|
|
198
|
+
return searchByEmbeddingFn((s) => this.getDb(s), query, limit, options);
|
|
199
|
+
}
|
|
200
|
+
async getCompressionCandidates(scope, level, maxAgeMs, force) {
|
|
201
|
+
return getCompressionCandidatesFn((s) => this.getDb(s), scope, level, maxAgeMs, force);
|
|
202
|
+
}
|
|
203
|
+
async runCompression(scope, force, client) {
|
|
204
|
+
return runCompressionFn({
|
|
205
|
+
getCompressionCandidates: (s, l, maxAge, f) => this.getCompressionCandidates(s, l, maxAge, f),
|
|
206
|
+
createNode: (node) => this.createNode(node),
|
|
207
|
+
updateNode: (id, updates) => this.updateNode(id, updates),
|
|
208
|
+
}, scope, force, client);
|
|
209
|
+
}
|
|
210
|
+
async runPatternExtraction(scope, minSourceCount = 2) {
|
|
211
|
+
return runPatternExtractionFn({
|
|
212
|
+
listNodes: (s, level, limit, offset, includeExpired) => this.listNodes(s, level, limit, offset, includeExpired),
|
|
213
|
+
createNode: (node) => this.createNode(node),
|
|
214
|
+
updateNode: (id, updates) => this.updateNode(id, updates),
|
|
215
|
+
}, scope, minSourceCount);
|
|
216
|
+
}
|
|
217
|
+
async getFractalStats(scope) {
|
|
218
|
+
return getFractalStatsFn((s) => this.listNodes(s), (id) => this.getNode(id), scope);
|
|
219
|
+
}
|
|
220
|
+
async retrieveFractal(id, maxDepth = 10) {
|
|
221
|
+
return retrieveFractalFn((id) => this.getNode(id), id, maxDepth);
|
|
222
|
+
}
|
|
223
|
+
async detectTopicBoundaries(scope, minSimilarity = 0.7) {
|
|
224
|
+
return detectTopicBoundariesFn((s) => this.getDb(s), scope, minSimilarity);
|
|
225
|
+
}
|
|
226
|
+
async logToolCall(toolName, resultTokens, contextWarning, success, durationMs = 0) {
|
|
227
|
+
const db = await this.getDb("global");
|
|
228
|
+
insertToolUsageLog(db, toolName, resultTokens, contextWarning, success, durationMs);
|
|
229
|
+
}
|
|
230
|
+
async getToolPatterns(_scope) {
|
|
231
|
+
const db = await this.getDb("global");
|
|
232
|
+
return queryToolPatterns(db);
|
|
233
|
+
}
|
|
234
|
+
async getFrequentSequences(_scope, minCount = 3) {
|
|
235
|
+
const db = await this.getDb("global");
|
|
236
|
+
return queryFrequentSequences(db, minCount);
|
|
237
|
+
}
|
|
238
|
+
async pruneUsageLog(maxAgeMs) {
|
|
239
|
+
const db = await this.getDb("global");
|
|
240
|
+
return deleteUsageLog(db, maxAgeMs);
|
|
241
|
+
}
|
|
242
|
+
getToolCategory(toolName) {
|
|
243
|
+
return getToolCategory(toolName);
|
|
244
|
+
}
|
|
245
|
+
async recordAgentToolCall(sessionId, toolName, args, output, success, durationMs) {
|
|
246
|
+
const db = await this.getGlobalDb();
|
|
247
|
+
const category = getToolCategory(toolName);
|
|
248
|
+
insertAgentToolCall(db, sessionId, toolName, args, output, success, durationMs, category);
|
|
249
|
+
if (sessionId) {
|
|
250
|
+
await this.incrementSessionToolCall(sessionId, toolName, success ?? true, null);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
async createSessionMetrics(sessionId, startedAt) {
|
|
254
|
+
const db = await this.getGlobalDb();
|
|
255
|
+
createSessionMetricsRow(db, sessionId, startedAt);
|
|
256
|
+
}
|
|
257
|
+
async updateSessionMetrics(sessionId, updates) {
|
|
258
|
+
const db = await this.getGlobalDb();
|
|
259
|
+
updateSessionMetrics(db, sessionId, updates);
|
|
260
|
+
}
|
|
261
|
+
async incrementSessionToolCall(sessionId, toolName, success, filePath) {
|
|
262
|
+
const db = await this.getGlobalDb();
|
|
263
|
+
incrementSessionToolCall(db, sessionId, toolName, success, filePath);
|
|
264
|
+
}
|
|
265
|
+
async getSessionStats(sessionId) {
|
|
266
|
+
const db = await this.getGlobalDb();
|
|
267
|
+
return getSessionStatsForSession(db, sessionId);
|
|
268
|
+
}
|
|
269
|
+
async verifyNode(id) {
|
|
270
|
+
return verifyNodeFn((s) => this.getDb(s), id);
|
|
271
|
+
}
|
|
272
|
+
calculateNodeConfidence(node) {
|
|
273
|
+
return calculateNodeConfidenceFn(node);
|
|
274
|
+
}
|
|
275
|
+
async drilldownQuery(query, maxResults = 20) {
|
|
276
|
+
return drilldownQueryFn({
|
|
277
|
+
getDb: (s) => this.getDb(s),
|
|
278
|
+
searchByEmbedding: (q, limit, opts) => this.searchByEmbedding(q, limit, opts),
|
|
279
|
+
getDrilldownPath: (nodeId, maxDepth) => getDrilldownPathFn((id) => this.getNode(id), nodeId, maxDepth),
|
|
280
|
+
}, query, maxResults);
|
|
281
|
+
}
|
|
282
|
+
async pruneNodes(scope, options = {}) {
|
|
283
|
+
return pruneNodesFn({
|
|
284
|
+
getDb: (s) => this.getDb(s),
|
|
285
|
+
listNodes: (s, level, limit, offset, includeExpired) => this.listNodes(s, level, limit, offset, includeExpired),
|
|
286
|
+
}, scope, options);
|
|
287
|
+
}
|
|
288
|
+
async getExpiredNodes(scope = "all") {
|
|
289
|
+
return getExpiredNodesFn((s) => this.getDb(s), scope);
|
|
290
|
+
}
|
|
291
|
+
async deleteExpiredNodes(scope = "all") {
|
|
292
|
+
return deleteExpiredNodesFn((s) => this.getDb(s), scope);
|
|
293
|
+
}
|
|
294
|
+
async logInjectionMetrics(sessionId, data) {
|
|
295
|
+
const db = await this.getGlobalDb();
|
|
296
|
+
insertInjectionMetrics(db, sessionId, data);
|
|
297
|
+
}
|
|
298
|
+
async getPendingInjections() {
|
|
299
|
+
const db = await this.getGlobalDb();
|
|
300
|
+
return getPendingInjectionRows(db);
|
|
301
|
+
}
|
|
302
|
+
async markInjectionProcessed(id) {
|
|
303
|
+
const db = await this.getGlobalDb();
|
|
304
|
+
markInjectionProcessed(db, id);
|
|
305
|
+
}
|
|
306
|
+
async recordMemoryToolCall(sessionId, toolName, _args) {
|
|
307
|
+
const db = await this.getGlobalDb();
|
|
308
|
+
updateMemoryToolCall(db, sessionId, toolName);
|
|
309
|
+
}
|
|
310
|
+
async finalizeInjection(sessionId, effectivenessScore, taskDescription) {
|
|
311
|
+
const db = await this.getGlobalDb();
|
|
312
|
+
finalizeInjection(db, sessionId, effectivenessScore, taskDescription);
|
|
313
|
+
}
|
|
314
|
+
async recordInjectionFeedback(sessionId, upvotes, downvotes, taskOutcome, neededNodes) {
|
|
315
|
+
const db = await this.getGlobalDb();
|
|
316
|
+
insertInjectionFeedback(db, sessionId, upvotes, downvotes, taskOutcome, neededNodes);
|
|
317
|
+
}
|
|
318
|
+
async getInjectionMetrics(limit = 100) {
|
|
319
|
+
const db = await this.getGlobalDb();
|
|
320
|
+
return queryInjectionMetrics(db, limit);
|
|
321
|
+
}
|
|
322
|
+
async getSessionMetrics(sessionId) {
|
|
323
|
+
const db = await this.getGlobalDb();
|
|
324
|
+
return querySessionMetrics(db, sessionId);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
export function createSqliteMemoryStore(projectDirectory, globalDbPath) {
|
|
328
|
+
return new SqliteMemoryStore(projectDirectory, globalDbPath);
|
|
329
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { withRetry } from "./utils";
|
|
3
|
+
export async function insertToolUsageLog(db, toolName, resultTokens, contextWarning, success, durationMs = 0) {
|
|
4
|
+
await withRetry(() => {
|
|
5
|
+
db.run("INSERT INTO memory_usage_log (id, tool_name, timestamp, result_tokens, context_warning, success, duration_ms) VALUES (?, ?, ?, ?, ?, ?, ?)", [randomUUID(), toolName, Date.now(), resultTokens, contextWarning ? 1 : 0, success ? 1 : 0, Math.round(durationMs)]);
|
|
6
|
+
});
|
|
7
|
+
}
|
|
8
|
+
export function queryToolPatterns(db) {
|
|
9
|
+
const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000;
|
|
10
|
+
const rows = db.query("SELECT tool_name, COUNT(*) as count, AVG(result_tokens) as avg_tokens, AVG(duration_ms) as avg_duration, AVG(context_warning) as warning_rate, AVG(success) as success_rate FROM memory_usage_log WHERE timestamp > ? GROUP BY tool_name ORDER BY count DESC").all(cutoff);
|
|
11
|
+
return rows.map(r => ({
|
|
12
|
+
toolName: r.tool_name,
|
|
13
|
+
count: r.count,
|
|
14
|
+
avgTokens: Math.round(r.avg_tokens),
|
|
15
|
+
avgDurationMs: Math.round(r.avg_duration ?? 0),
|
|
16
|
+
warningRate: Math.round(r.warning_rate * 100),
|
|
17
|
+
successRate: Math.round(r.success_rate * 100),
|
|
18
|
+
}));
|
|
19
|
+
}
|
|
20
|
+
export function queryFrequentSequences(db, minCount = 3) {
|
|
21
|
+
const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000;
|
|
22
|
+
const rows = db.query("SELECT tool_name, timestamp FROM memory_usage_log WHERE timestamp > ? ORDER BY timestamp ASC").all(cutoff);
|
|
23
|
+
const pairs = new Map();
|
|
24
|
+
for (let i = 0; i < rows.length - 1; i++) {
|
|
25
|
+
const prev = rows[i].tool_name;
|
|
26
|
+
const next = rows[i + 1].tool_name;
|
|
27
|
+
if (prev === next)
|
|
28
|
+
continue;
|
|
29
|
+
const key = `${prev}\u2192${next}`;
|
|
30
|
+
pairs.set(key, (pairs.get(key) ?? 0) + 1);
|
|
31
|
+
}
|
|
32
|
+
const result = [];
|
|
33
|
+
for (const [key, count] of pairs) {
|
|
34
|
+
if (count >= minCount) {
|
|
35
|
+
const [prev, next] = key.split("\u2192");
|
|
36
|
+
result.push({ prev: prev, next: next, count });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
result.sort((a, b) => b.count - a.count);
|
|
40
|
+
return result.slice(0, 10);
|
|
41
|
+
}
|
|
42
|
+
export async function deleteUsageLog(db, maxAgeMs) {
|
|
43
|
+
const threshold = maxAgeMs ?? 30 * 24 * 60 * 60 * 1000;
|
|
44
|
+
const cutoff = Date.now() - threshold;
|
|
45
|
+
const result = await withRetry(() => db.run("DELETE FROM memory_usage_log WHERE timestamp < ?", [cutoff]));
|
|
46
|
+
return result.changes;
|
|
47
|
+
}
|
|
48
|
+
export function getToolCategory(toolName) {
|
|
49
|
+
if (toolName.startsWith("memory_") || toolName.startsWith("journal_"))
|
|
50
|
+
return "memory";
|
|
51
|
+
if (["read", "edit", "write", "glob", "grep", "search"].includes(toolName))
|
|
52
|
+
return "file";
|
|
53
|
+
if (["bash", "shell"].includes(toolName))
|
|
54
|
+
return "shell";
|
|
55
|
+
return "other";
|
|
56
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
export function extractLinks(content) {
|
|
2
|
+
const links = [];
|
|
3
|
+
let i = 0;
|
|
4
|
+
while (i < content.length) {
|
|
5
|
+
if (content[i] === "[" && content[i + 1] === "[") {
|
|
6
|
+
const end = content.indexOf("]]", i + 2);
|
|
7
|
+
if (end !== -1) {
|
|
8
|
+
const label = content.slice(i + 2, end).trim();
|
|
9
|
+
if (label && label.length <= 60 && /^[a-z0-9][a-z0-9-_]{1,60}$/i.test(label)) {
|
|
10
|
+
if (!links.includes(label)) {
|
|
11
|
+
links.push(label);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
i = end + 2;
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
i++;
|
|
19
|
+
}
|
|
20
|
+
return links;
|
|
21
|
+
}
|
|
22
|
+
export function embeddingToBlob(embedding) {
|
|
23
|
+
const floats = new Float32Array(embedding);
|
|
24
|
+
return Buffer.from(floats.buffer);
|
|
25
|
+
}
|
|
26
|
+
export function blobToEmbedding(blob) {
|
|
27
|
+
const floats = new Float32Array(blob.buffer, blob.byteOffset, blob.byteLength / 4);
|
|
28
|
+
return Array.from(floats);
|
|
29
|
+
}
|
|
30
|
+
export function tokenize(text) {
|
|
31
|
+
return text.toLowerCase()
|
|
32
|
+
.replace(/[^\w\s]/g, " ")
|
|
33
|
+
.split(/\s+/)
|
|
34
|
+
.filter(t => t.length >= 2);
|
|
35
|
+
}
|
|
36
|
+
// Retry wrapper for database operations with exponential backoff
|
|
37
|
+
export async function withRetry(operation, maxRetries = 3, baseDelay = 100) {
|
|
38
|
+
let lastError;
|
|
39
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
40
|
+
try {
|
|
41
|
+
return await operation();
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
lastError = error;
|
|
45
|
+
const isBusyError = error instanceof Error &&
|
|
46
|
+
(error.message.includes("database is locked") ||
|
|
47
|
+
error.message.includes("database is busy") ||
|
|
48
|
+
error.message.includes("SQLITE_BUSY") ||
|
|
49
|
+
error.message.includes("SQLITE_LOCKED"));
|
|
50
|
+
if (!isBusyError || attempt === maxRetries) {
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
const delay = baseDelay * Math.pow(2, attempt);
|
|
54
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
throw lastError;
|
|
58
|
+
}
|
|
59
|
+
// Transaction wrapper for atomic operations
|
|
60
|
+
export async function withTransaction(db, operation) {
|
|
61
|
+
db.run("BEGIN");
|
|
62
|
+
try {
|
|
63
|
+
const result = await operation();
|
|
64
|
+
db.run("COMMIT");
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
db.run("ROLLBACK");
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Retryable transaction: retries entire transaction on busy errors
|
|
73
|
+
export async function withRetryableTransaction(db, operation, maxRetries = 3) {
|
|
74
|
+
let lastError;
|
|
75
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
76
|
+
try {
|
|
77
|
+
return await withTransaction(db, operation);
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
lastError = error;
|
|
81
|
+
const isBusyError = error instanceof Error &&
|
|
82
|
+
(error.message.includes("database is locked") ||
|
|
83
|
+
error.message.includes("database is busy") ||
|
|
84
|
+
error.message.includes("SQLITE_BUSY") ||
|
|
85
|
+
error.message.includes("SQLITE_LOCKED"));
|
|
86
|
+
if (!isBusyError || attempt === maxRetries) {
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
const delay = 100 * Math.pow(2, attempt);
|
|
90
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
throw lastError;
|
|
94
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
import { generateEmbedding } from "../embeddings";
|
|
3
|
+
import { rerankDocuments } from "../ollama";
|
|
4
|
+
export function createMemoryAutoTest(store) {
|
|
5
|
+
const testQuery = "JSON parsing and validation best practices";
|
|
6
|
+
return tool({
|
|
7
|
+
description: "Test auto-retrieval pipeline with a hardcoded query - used for debugging",
|
|
8
|
+
args: {},
|
|
9
|
+
async execute() {
|
|
10
|
+
try {
|
|
11
|
+
const queryEmbedding = await generateEmbedding(testQuery);
|
|
12
|
+
const candidates = await store.searchByEmbedding(queryEmbedding, 10, { bm25Weight: 0.4 });
|
|
13
|
+
if (candidates.length === 0)
|
|
14
|
+
return "No candidates found";
|
|
15
|
+
const results = await rerankDocuments(testQuery, candidates.map((c) => ({ id: c.id, label: c.label ?? c.id, content: c.content.slice(0, 200) })), { topK: 3 });
|
|
16
|
+
const selected = candidates.filter((c) => results.some((r) => r.id === c.id));
|
|
17
|
+
return `Pipeline test: query="${testQuery}", candidates=${candidates.length}, selected=${selected.length}\n\nSelected:\n${selected.map((n, i) => `${i + 1}. ${n.label}: ${n.content.slice(0, 100)}`).join("\n")}`;
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
return `Error: ${err}`;
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
import { getWorkingCache } from "../cache";
|
|
3
|
+
import { wrapWithTracking } from "./shared";
|
|
4
|
+
export function MemoryCacheStatus(store) {
|
|
5
|
+
const t = tool({
|
|
6
|
+
description: "Show current working-memory cache usage (size, max size, recent files).",
|
|
7
|
+
args: {},
|
|
8
|
+
async execute(_args) {
|
|
9
|
+
const cache = getWorkingCache("dashboard");
|
|
10
|
+
const maxSize = 8;
|
|
11
|
+
const ttlHours = 2;
|
|
12
|
+
const lines = [
|
|
13
|
+
"## Working Memory Cache",
|
|
14
|
+
"",
|
|
15
|
+
`Current size: ${cache.length} / ${maxSize}`,
|
|
16
|
+
`TTL: ${ttlHours} hours`,
|
|
17
|
+
"",
|
|
18
|
+
];
|
|
19
|
+
if (cache.length === 0) {
|
|
20
|
+
lines.push("Cache is empty. Nodes are added to the working cache as they are accessed during a session.");
|
|
21
|
+
return lines.join("\n");
|
|
22
|
+
}
|
|
23
|
+
lines.push("### Cached Nodes");
|
|
24
|
+
lines.push("| Label | Importance | Cached At |");
|
|
25
|
+
lines.push("|-------|------------|-----------|");
|
|
26
|
+
for (const n of cache) {
|
|
27
|
+
const date = new Date(n.cachedAt).toLocaleString();
|
|
28
|
+
lines.push(`| ${n.label} | ${n.importance.toFixed(2)} | ${date} |`);
|
|
29
|
+
}
|
|
30
|
+
lines.push("");
|
|
31
|
+
lines.push("_Cache entries expire after ${ttlHours} hours or when the cache exceeds ${maxSize} entries._");
|
|
32
|
+
return lines.join("\n");
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
return wrapWithTracking(t, store, "memory_cache_status");
|
|
36
|
+
}
|