@unblocklabs/unblock-memory 0.1.1 → 0.1.2
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/dist/src/analysis.d.ts +2 -6
- package/dist/src/analysis.js +45 -48
- package/dist/src/plugin.js +32 -119
- package/openclaw.plugin.json +1 -1
- package/package.json +4 -2
package/dist/src/analysis.d.ts
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
import type { QMDStore } from "@unblocklabs/qmd";
|
|
2
|
-
export declare const DEFAULT_CLUSTER_LIMIT = 20;
|
|
3
|
-
export declare const MAX_CLUSTER_LIMIT = 50;
|
|
4
|
-
export declare const DEFAULT_MEMBER_LIMIT = 20;
|
|
5
|
-
export declare const MAX_MEMBER_LIMIT = 50;
|
|
6
2
|
type AnalysisDatabase = QMDStore["internal"]["db"];
|
|
7
3
|
export type MemoryReclusterOptions = {
|
|
8
4
|
space?: {
|
|
@@ -26,7 +22,7 @@ export type AnalysisRunner = (params: {
|
|
|
26
22
|
options?: MemoryReclusterOptions;
|
|
27
23
|
signal?: AbortSignal;
|
|
28
24
|
}) => Promise<void>;
|
|
29
|
-
|
|
25
|
+
type MemoryAnalysisMember = {
|
|
30
26
|
hash: string;
|
|
31
27
|
seq: number;
|
|
32
28
|
probability: number;
|
|
@@ -52,7 +48,7 @@ export type MemoryAnalysisSummary = {
|
|
|
52
48
|
stale: boolean;
|
|
53
49
|
staleSince: string | null;
|
|
54
50
|
};
|
|
55
|
-
|
|
51
|
+
type MemoryClusterSummary = {
|
|
56
52
|
clusterId: string;
|
|
57
53
|
size: number;
|
|
58
54
|
availableSize: number;
|
package/dist/src/analysis.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
const DEFAULT_CLUSTER_LIMIT = 20;
|
|
4
|
+
const MAX_CLUSTER_LIMIT = 50;
|
|
5
|
+
const DEFAULT_MEMBER_LIMIT = 20;
|
|
6
|
+
const MAX_MEMBER_LIMIT = 50;
|
|
7
7
|
const MAX_EXCERPT_BYTES = 2_000;
|
|
8
8
|
const MAX_TOTAL_EXCERPT_BYTES = 12_000;
|
|
9
9
|
const MAX_ALIASES_PER_MEMBER = 5;
|
|
@@ -47,6 +47,19 @@ export function ensureMemoryAnalysisSchema(db) {
|
|
|
47
47
|
|
|
48
48
|
CREATE INDEX IF NOT EXISTS idx_memory_analysis_memberships_cluster
|
|
49
49
|
ON memory_analysis_memberships(run_id, cluster_id, representative_rank);
|
|
50
|
+
|
|
51
|
+
CREATE VIEW IF NOT EXISTS memory_analysis_available_memberships AS
|
|
52
|
+
SELECT
|
|
53
|
+
m.run_id, m.hash, m.seq, m.cluster_id, m.probability, m.outlier_score,
|
|
54
|
+
m.x, m.y, m.representative_rank, cv.pos, cv.chunk_len, c.doc
|
|
55
|
+
FROM memory_analysis_memberships m
|
|
56
|
+
JOIN content_vectors cv ON cv.hash = m.hash AND cv.seq = m.seq
|
|
57
|
+
JOIN content c ON c.hash = m.hash
|
|
58
|
+
WHERE EXISTS (
|
|
59
|
+
SELECT 1
|
|
60
|
+
FROM documents d
|
|
61
|
+
WHERE d.hash = m.hash AND d.active = 1
|
|
62
|
+
);
|
|
50
63
|
`);
|
|
51
64
|
}
|
|
52
65
|
export function markMemoryAnalysisStale(db) {
|
|
@@ -137,10 +150,7 @@ export function latestAnalysisRunId(db) {
|
|
|
137
150
|
function count(db, sql, runId) {
|
|
138
151
|
return db.prepare(sql).get(runId)?.count ?? 0;
|
|
139
152
|
}
|
|
140
|
-
|
|
141
|
-
const run = latestRun(db);
|
|
142
|
-
if (!run)
|
|
143
|
-
return undefined;
|
|
153
|
+
function analysisSummary(db, run) {
|
|
144
154
|
const clusters = count(db, "SELECT COUNT(*) AS count FROM memory_analysis_clusters WHERE run_id = ?", run.id);
|
|
145
155
|
const members = count(db, "SELECT COUNT(*) AS count FROM memory_analysis_memberships WHERE run_id = ?", run.id);
|
|
146
156
|
const expectedNonNoise = count(db, "SELECT COALESCE(SUM(size), 0) AS count FROM memory_analysis_clusters WHERE run_id = ?", run.id);
|
|
@@ -171,6 +181,14 @@ export function readAnalysisSummary(db) {
|
|
|
171
181
|
staleSince: run.stale_at,
|
|
172
182
|
};
|
|
173
183
|
}
|
|
184
|
+
export function readAnalysisSummary(db) {
|
|
185
|
+
const run = latestRun(db);
|
|
186
|
+
return run ? analysisSummary(db, run) : undefined;
|
|
187
|
+
}
|
|
188
|
+
function latestValidRun(db) {
|
|
189
|
+
const run = latestRun(db);
|
|
190
|
+
return run && analysisSummary(db, run) ? run : undefined;
|
|
191
|
+
}
|
|
174
192
|
function sourcePaths(db, hash, limit) {
|
|
175
193
|
if (limit <= 0)
|
|
176
194
|
return [];
|
|
@@ -202,16 +220,9 @@ function members(db, runId, clusterId, limit, maxExcerptBytes = MAX_EXCERPT_BYTE
|
|
|
202
220
|
const rows = db.prepare(`
|
|
203
221
|
SELECT
|
|
204
222
|
m.hash, m.seq, m.probability, m.outlier_score, m.x, m.y,
|
|
205
|
-
m.representative_rank,
|
|
206
|
-
FROM
|
|
207
|
-
JOIN content_vectors cv ON cv.hash = m.hash AND cv.seq = m.seq
|
|
208
|
-
JOIN content c ON c.hash = m.hash
|
|
223
|
+
m.representative_rank, m.pos, m.chunk_len, m.doc
|
|
224
|
+
FROM memory_analysis_available_memberships m
|
|
209
225
|
WHERE m.run_id = ? AND m.cluster_id = ?
|
|
210
|
-
AND EXISTS (
|
|
211
|
-
SELECT 1
|
|
212
|
-
FROM documents d
|
|
213
|
-
WHERE d.hash = m.hash AND d.active = 1
|
|
214
|
-
)
|
|
215
226
|
ORDER BY ${noiseOrder}
|
|
216
227
|
LIMIT ?
|
|
217
228
|
`).all(runId, clusterId, limit);
|
|
@@ -240,15 +251,8 @@ function members(db, runId, clusterId, limit, maxExcerptBytes = MAX_EXCERPT_BYTE
|
|
|
240
251
|
function availableSize(db, runId, clusterId) {
|
|
241
252
|
return db.prepare(`
|
|
242
253
|
SELECT COUNT(*) AS count
|
|
243
|
-
FROM
|
|
244
|
-
|
|
245
|
-
JOIN content c ON c.hash = m.hash
|
|
246
|
-
WHERE m.run_id = ? AND m.cluster_id = ?
|
|
247
|
-
AND EXISTS (
|
|
248
|
-
SELECT 1
|
|
249
|
-
FROM documents d
|
|
250
|
-
WHERE d.hash = m.hash AND d.active = 1
|
|
251
|
-
)
|
|
254
|
+
FROM memory_analysis_available_memberships
|
|
255
|
+
WHERE run_id = ? AND cluster_id = ?
|
|
252
256
|
`).get(runId, clusterId)?.count ?? 0;
|
|
253
257
|
}
|
|
254
258
|
function readMetadata(run) {
|
|
@@ -295,22 +299,20 @@ function noiseRow(db, runId) {
|
|
|
295
299
|
SELECT
|
|
296
300
|
-1 AS cluster_id,
|
|
297
301
|
COUNT(*) AS size,
|
|
298
|
-
|
|
299
|
-
SELECT
|
|
300
|
-
FROM
|
|
301
|
-
WHERE
|
|
302
|
-
)
|
|
302
|
+
(
|
|
303
|
+
SELECT COUNT(*)
|
|
304
|
+
FROM memory_analysis_available_memberships available
|
|
305
|
+
WHERE available.run_id = ? AND available.cluster_id = -1
|
|
306
|
+
) AS available_size,
|
|
303
307
|
COALESCE(AVG(m.probability), 0) AS mean_probability
|
|
304
308
|
FROM memory_analysis_memberships m
|
|
305
|
-
LEFT JOIN content_vectors cv ON cv.hash = m.hash AND cv.seq = m.seq
|
|
306
|
-
LEFT JOIN content c ON c.hash = m.hash
|
|
307
309
|
WHERE m.run_id = ? AND m.cluster_id = -1
|
|
308
310
|
HAVING COUNT(*) > 0
|
|
309
|
-
`).get(runId);
|
|
311
|
+
`).get(runId, runId);
|
|
310
312
|
}
|
|
311
313
|
export function readClusters(db, requestedLimit = DEFAULT_CLUSTER_LIMIT) {
|
|
312
|
-
const run =
|
|
313
|
-
if (!run
|
|
314
|
+
const run = latestValidRun(db);
|
|
315
|
+
if (!run) {
|
|
314
316
|
return { status: "not_analyzed", ...readMetadata(), clusters: [], noise: null };
|
|
315
317
|
}
|
|
316
318
|
const limit = Math.max(1, Math.min(MAX_CLUSTER_LIMIT, Math.floor(requestedLimit)));
|
|
@@ -318,19 +320,14 @@ export function readClusters(db, requestedLimit = DEFAULT_CLUSTER_LIMIT) {
|
|
|
318
320
|
SELECT
|
|
319
321
|
c.cluster_id,
|
|
320
322
|
c.size,
|
|
321
|
-
|
|
322
|
-
SELECT
|
|
323
|
-
FROM
|
|
324
|
-
WHERE
|
|
325
|
-
)
|
|
323
|
+
(
|
|
324
|
+
SELECT COUNT(*)
|
|
325
|
+
FROM memory_analysis_available_memberships available
|
|
326
|
+
WHERE available.run_id = c.run_id AND available.cluster_id = c.cluster_id
|
|
327
|
+
) AS available_size,
|
|
326
328
|
c.mean_probability
|
|
327
329
|
FROM memory_analysis_clusters c
|
|
328
|
-
LEFT JOIN memory_analysis_memberships m
|
|
329
|
-
ON m.run_id = c.run_id AND m.cluster_id = c.cluster_id
|
|
330
|
-
LEFT JOIN content_vectors cv ON cv.hash = m.hash AND cv.seq = m.seq
|
|
331
|
-
LEFT JOIN content content_row ON content_row.hash = m.hash
|
|
332
330
|
WHERE c.run_id = ?
|
|
333
|
-
GROUP BY c.cluster_id, c.size, c.mean_probability
|
|
334
331
|
ORDER BY c.size DESC, c.cluster_id
|
|
335
332
|
LIMIT ?
|
|
336
333
|
`).all(run.id, limit);
|
|
@@ -366,8 +363,8 @@ function resolveClusterId(db, runId, reference) {
|
|
|
366
363
|
return clusterIds.find((row) => clusterReference(runId, row.cluster_id) === reference)?.cluster_id;
|
|
367
364
|
}
|
|
368
365
|
export function readCluster(db, clusterReferenceId, requestedLimit = DEFAULT_MEMBER_LIMIT) {
|
|
369
|
-
const run =
|
|
370
|
-
if (!run
|
|
366
|
+
const run = latestValidRun(db);
|
|
367
|
+
if (!run) {
|
|
371
368
|
return { status: "not_analyzed", ...readMetadata() };
|
|
372
369
|
}
|
|
373
370
|
const metadata = readMetadata(run);
|
package/dist/src/plugin.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
|
-
import {
|
|
2
|
+
import { Value } from "typebox/value";
|
|
3
|
+
import { jsonResult } from "openclaw/plugin-sdk/agent-runtime";
|
|
3
4
|
import { resolveConfig } from "./config.js";
|
|
4
5
|
import { QmdMemoryRuntime } from "./runtime.js";
|
|
5
6
|
function getContext(ctx) {
|
|
@@ -8,11 +9,16 @@ function getContext(ctx) {
|
|
|
8
9
|
return undefined;
|
|
9
10
|
return { cfg, agentId: ctx.agentId };
|
|
10
11
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
12
|
+
const searchParameters = Type.Object({
|
|
13
|
+
query: Type.String({ pattern: "\\S" }),
|
|
14
|
+
maxResults: Type.Optional(Type.Integer({ minimum: 1, maximum: 20 })),
|
|
15
|
+
minScore: Type.Optional(Type.Number({ minimum: 0, maximum: 1 })),
|
|
16
|
+
}, { additionalProperties: false });
|
|
17
|
+
const getParameters = Type.Object({
|
|
18
|
+
path: Type.String({ pattern: "\\S" }),
|
|
19
|
+
from: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
20
|
+
lines: Type.Optional(Type.Integer({ minimum: 1, maximum: 1000 })),
|
|
21
|
+
}, { additionalProperties: false });
|
|
16
22
|
function createSearchTool(runtime, ctx) {
|
|
17
23
|
const active = getContext(ctx);
|
|
18
24
|
if (!active)
|
|
@@ -21,18 +27,10 @@ function createSearchTool(runtime, ctx) {
|
|
|
21
27
|
name: "memory_search",
|
|
22
28
|
label: "Memory Search",
|
|
23
29
|
description: "Search canonical Markdown memory with semantic vector retrieval.",
|
|
24
|
-
parameters:
|
|
25
|
-
query: Type.String(),
|
|
26
|
-
maxResults: Type.Optional(Type.Integer({ minimum: 1, maximum: 20 })),
|
|
27
|
-
minScore: Type.Optional(Type.Number({ minimum: 0, maximum: 1 })),
|
|
28
|
-
}),
|
|
30
|
+
parameters: searchParameters,
|
|
29
31
|
async execute(_toolCallId, params, signal) {
|
|
30
|
-
const
|
|
31
|
-
const query =
|
|
32
|
-
if (!query)
|
|
33
|
-
throw new Error("query is required");
|
|
34
|
-
const maxResults = typeof raw.maxResults === "number" ? raw.maxResults : undefined;
|
|
35
|
-
const minScore = typeof raw.minScore === "number" ? raw.minScore : undefined;
|
|
32
|
+
const { query: untrimmedQuery, maxResults, minScore } = Value.Parse(searchParameters, params);
|
|
33
|
+
const query = untrimmedQuery.trim();
|
|
36
34
|
const { manager, error } = await runtime.getMemorySearchManager(active);
|
|
37
35
|
if (!manager)
|
|
38
36
|
return jsonResult({ results: [], error: error ?? "memory unavailable" });
|
|
@@ -49,23 +47,17 @@ function createGetTool(runtime, ctx) {
|
|
|
49
47
|
name: "memory_get",
|
|
50
48
|
label: "Memory Get",
|
|
51
49
|
description: "Read an exact qmd:// path returned by memory_search.",
|
|
52
|
-
parameters:
|
|
53
|
-
path: Type.String(),
|
|
54
|
-
from: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
55
|
-
lines: Type.Optional(Type.Integer({ minimum: 1, maximum: 1000 })),
|
|
56
|
-
}),
|
|
50
|
+
parameters: getParameters,
|
|
57
51
|
async execute(_toolCallId, params) {
|
|
58
|
-
const
|
|
59
|
-
const path =
|
|
60
|
-
if (!path)
|
|
61
|
-
throw new Error("path is required");
|
|
52
|
+
const { path: untrimmedPath, from, lines } = Value.Parse(getParameters, params);
|
|
53
|
+
const path = untrimmedPath.trim();
|
|
62
54
|
const { manager, error } = await runtime.getMemorySearchManager(active);
|
|
63
55
|
if (!manager)
|
|
64
56
|
return jsonResult({ status: "unavailable", error: error ?? "memory unavailable" });
|
|
65
57
|
return jsonResult(await manager.readFile({
|
|
66
58
|
relPath: path,
|
|
67
|
-
from
|
|
68
|
-
lines
|
|
59
|
+
from,
|
|
60
|
+
lines,
|
|
69
61
|
}));
|
|
70
62
|
},
|
|
71
63
|
};
|
|
@@ -86,80 +78,6 @@ const reclusterParameters = Type.Object({
|
|
|
86
78
|
}, { additionalProperties: false })),
|
|
87
79
|
seed: Type.Optional(Type.Integer({ minimum: 0, maximum: 4_294_967_295 })),
|
|
88
80
|
}, { additionalProperties: false });
|
|
89
|
-
function requireObject(value, name) {
|
|
90
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
91
|
-
throw new Error(`${name} must be an object`);
|
|
92
|
-
}
|
|
93
|
-
return Object.fromEntries(Object.entries(value));
|
|
94
|
-
}
|
|
95
|
-
function optionalNumber(value, key, constraints) {
|
|
96
|
-
const candidate = value[key];
|
|
97
|
-
if (candidate === undefined)
|
|
98
|
-
return undefined;
|
|
99
|
-
const valid = typeof candidate === "number" && Number.isFinite(candidate) &&
|
|
100
|
-
candidate >= constraints.minimum &&
|
|
101
|
-
(constraints.maximum === undefined || candidate <= constraints.maximum) &&
|
|
102
|
-
(!constraints.integer || Number.isInteger(candidate));
|
|
103
|
-
if (!valid)
|
|
104
|
-
throw new Error(`${key} is invalid`);
|
|
105
|
-
return candidate;
|
|
106
|
-
}
|
|
107
|
-
function requireOnlyKeys(value, allowed) {
|
|
108
|
-
const unexpected = Object.keys(value).find((key) => !allowed.includes(key));
|
|
109
|
-
if (unexpected)
|
|
110
|
-
throw new Error(`${unexpected} is not allowed`);
|
|
111
|
-
}
|
|
112
|
-
function parseReclusterOptions(params) {
|
|
113
|
-
const raw = toolParams(params);
|
|
114
|
-
requireOnlyKeys(raw, ["space", "hdbscan", "seed"]);
|
|
115
|
-
const options = {};
|
|
116
|
-
if (raw.space !== undefined) {
|
|
117
|
-
const space = requireObject(raw.space, "space");
|
|
118
|
-
requireOnlyKeys(space, ["method", "nComponents", "nNeighbors", "minDist"]);
|
|
119
|
-
const method = space.method;
|
|
120
|
-
if (method !== undefined && method !== "umap" && method !== "none") {
|
|
121
|
-
throw new Error("space.method is invalid");
|
|
122
|
-
}
|
|
123
|
-
options.space = {
|
|
124
|
-
...(method === undefined ? {} : { method }),
|
|
125
|
-
...optionalEntry("nComponents", optionalNumber(space, "nComponents", { minimum: 2, maximum: 100, integer: true })),
|
|
126
|
-
...optionalEntry("nNeighbors", optionalNumber(space, "nNeighbors", { minimum: 2, maximum: 200, integer: true })),
|
|
127
|
-
...optionalEntry("minDist", optionalNumber(space, "minDist", { minimum: 0, maximum: 1 })),
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
if (raw.hdbscan !== undefined) {
|
|
131
|
-
const hdbscan = requireObject(raw.hdbscan, "hdbscan");
|
|
132
|
-
requireOnlyKeys(hdbscan, [
|
|
133
|
-
"minClusterSize",
|
|
134
|
-
"minSamples",
|
|
135
|
-
"clusterSelectionMethod",
|
|
136
|
-
"clusterSelectionEpsilon",
|
|
137
|
-
"allowSingleCluster",
|
|
138
|
-
]);
|
|
139
|
-
const method = hdbscan.clusterSelectionMethod;
|
|
140
|
-
if (method !== undefined && method !== "eom" && method !== "leaf") {
|
|
141
|
-
throw new Error("hdbscan.clusterSelectionMethod is invalid");
|
|
142
|
-
}
|
|
143
|
-
const allowSingleCluster = hdbscan.allowSingleCluster;
|
|
144
|
-
if (allowSingleCluster !== undefined && typeof allowSingleCluster !== "boolean") {
|
|
145
|
-
throw new Error("hdbscan.allowSingleCluster is invalid");
|
|
146
|
-
}
|
|
147
|
-
options.hdbscan = {
|
|
148
|
-
...optionalEntry("minClusterSize", optionalNumber(hdbscan, "minClusterSize", { minimum: 2, maximum: 100_000, integer: true })),
|
|
149
|
-
...optionalEntry("minSamples", optionalNumber(hdbscan, "minSamples", { minimum: 1, maximum: 100_000, integer: true })),
|
|
150
|
-
...(method === undefined ? {} : { clusterSelectionMethod: method }),
|
|
151
|
-
...optionalEntry("clusterSelectionEpsilon", optionalNumber(hdbscan, "clusterSelectionEpsilon", { minimum: 0 })),
|
|
152
|
-
...(allowSingleCluster === undefined ? {} : { allowSingleCluster }),
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
const seed = optionalNumber(raw, "seed", { minimum: 0, maximum: 4_294_967_295, integer: true });
|
|
156
|
-
if (seed !== undefined)
|
|
157
|
-
options.seed = seed;
|
|
158
|
-
return options;
|
|
159
|
-
}
|
|
160
|
-
function optionalEntry(key, value) {
|
|
161
|
-
return value === undefined ? {} : { [key]: value };
|
|
162
|
-
}
|
|
163
81
|
function createReclusterTool(runtime, ctx) {
|
|
164
82
|
const active = getContext(ctx);
|
|
165
83
|
if (!active)
|
|
@@ -170,7 +88,7 @@ function createReclusterTool(runtime, ctx) {
|
|
|
170
88
|
description: "Rebuild memory clusters from existing QMD vectors. Call only when memory_list_clusters reports missing or stale analysis.",
|
|
171
89
|
parameters: reclusterParameters,
|
|
172
90
|
async execute(_toolCallId, params, signal) {
|
|
173
|
-
const options =
|
|
91
|
+
const options = Value.Parse(reclusterParameters, params);
|
|
174
92
|
const { manager, error } = await runtime.getMemorySearchManager(active);
|
|
175
93
|
if (!manager)
|
|
176
94
|
return jsonResult({ status: "unavailable", error: error ?? "memory unavailable" });
|
|
@@ -186,6 +104,9 @@ function createReclusterTool(runtime, ctx) {
|
|
|
186
104
|
},
|
|
187
105
|
};
|
|
188
106
|
}
|
|
107
|
+
const listClustersParameters = Type.Object({
|
|
108
|
+
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 50 })),
|
|
109
|
+
}, { additionalProperties: false });
|
|
189
110
|
function createListClustersTool(runtime, ctx) {
|
|
190
111
|
const active = getContext(ctx);
|
|
191
112
|
if (!active)
|
|
@@ -194,13 +115,9 @@ function createListClustersTool(runtime, ctx) {
|
|
|
194
115
|
name: "memory_list_clusters",
|
|
195
116
|
label: "List Memory Clusters",
|
|
196
117
|
description: "List current memory clusters and freshness. Call this before memory_recluster or memory_fetch_cluster.",
|
|
197
|
-
parameters:
|
|
198
|
-
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 50 })),
|
|
199
|
-
}, { additionalProperties: false }),
|
|
118
|
+
parameters: listClustersParameters,
|
|
200
119
|
async execute(_toolCallId, params) {
|
|
201
|
-
const
|
|
202
|
-
requireOnlyKeys(raw, ["limit"]);
|
|
203
|
-
const limit = optionalNumber(raw, "limit", { minimum: 1, maximum: 50, integer: true });
|
|
120
|
+
const { limit } = Value.Parse(listClustersParameters, params);
|
|
204
121
|
const { manager, error } = await runtime.getMemorySearchManager(active);
|
|
205
122
|
if (!manager)
|
|
206
123
|
return jsonResult({ status: "unavailable", error: error ?? "memory unavailable" });
|
|
@@ -208,6 +125,10 @@ function createListClustersTool(runtime, ctx) {
|
|
|
208
125
|
},
|
|
209
126
|
};
|
|
210
127
|
}
|
|
128
|
+
const fetchClusterParameters = Type.Object({
|
|
129
|
+
clusterId: Type.String({ pattern: "^[0-9a-f]{10}$" }),
|
|
130
|
+
topK: Type.Optional(Type.Integer({ minimum: 1, maximum: 50 })),
|
|
131
|
+
}, { additionalProperties: false });
|
|
211
132
|
function createFetchClusterTool(runtime, ctx) {
|
|
212
133
|
const active = getContext(ctx);
|
|
213
134
|
if (!active)
|
|
@@ -216,17 +137,9 @@ function createFetchClusterTool(runtime, ctx) {
|
|
|
216
137
|
name: "memory_fetch_cluster",
|
|
217
138
|
label: "Fetch Memory Cluster",
|
|
218
139
|
description: "Fetch the top representative QMD chunks for a clusterId returned by memory_list_clusters.",
|
|
219
|
-
parameters:
|
|
220
|
-
clusterId: Type.String({ pattern: "^[0-9a-f]{10}$" }),
|
|
221
|
-
topK: Type.Optional(Type.Integer({ minimum: 1, maximum: 50 })),
|
|
222
|
-
}, { additionalProperties: false }),
|
|
140
|
+
parameters: fetchClusterParameters,
|
|
223
141
|
async execute(_toolCallId, params) {
|
|
224
|
-
const
|
|
225
|
-
requireOnlyKeys(raw, ["clusterId", "topK"]);
|
|
226
|
-
const clusterId = readStringParam(raw, "clusterId");
|
|
227
|
-
if (!clusterId || !/^[0-9a-f]{10}$/.test(clusterId))
|
|
228
|
-
throw new Error("clusterId is invalid");
|
|
229
|
-
const topK = optionalNumber(raw, "topK", { minimum: 1, maximum: 50, integer: true });
|
|
142
|
+
const { clusterId, topK } = Value.Parse(fetchClusterParameters, params);
|
|
230
143
|
const { manager, error } = await runtime.getMemorySearchManager(active);
|
|
231
144
|
if (!manager)
|
|
232
145
|
return jsonResult({ status: "unavailable", error: error ?? "memory unavailable" });
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "unblock-memory",
|
|
3
3
|
"name": "Unblock Memory",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.2",
|
|
5
5
|
"description": "Indexes, retrieves, and analyzes configured workspace memory with existing QMD vectors.",
|
|
6
6
|
"kind": "memory",
|
|
7
7
|
"activation": { "onStartup": false },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unblocklabs/unblock-memory",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Workspace-native memory for OpenClaw, powered by QMD",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,11 +22,12 @@
|
|
|
22
22
|
"scripts": {
|
|
23
23
|
"build": "tsc -p tsconfig.build.json",
|
|
24
24
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
25
|
+
"knip": "knip --reporter compact",
|
|
25
26
|
"test": "node --import tsx --test --test-concurrency=1 tests/**/*.test.ts",
|
|
26
27
|
"plugin:inspect": "plugin-inspector check --config plugin-inspector.config.json --no-openclaw",
|
|
27
28
|
"plugin:inspect:runtime": "plugin-inspector check --config plugin-inspector.config.json --no-openclaw --runtime --mock-sdk --allow-execute",
|
|
28
29
|
"release:check": "node scripts/check-release-version.mjs",
|
|
29
|
-
"preflight": "npm run build && npm run typecheck && npm test && npm run plugin:inspect && npm run plugin:inspect:runtime && npm pack --dry-run"
|
|
30
|
+
"preflight": "npm run knip && npm run build && npm run typecheck && npm test && npm run plugin:inspect && npm run plugin:inspect:runtime && npm pack --dry-run"
|
|
30
31
|
},
|
|
31
32
|
"dependencies": {
|
|
32
33
|
"@unblocklabs/qmd": "github:unblocklabs-ai/qmd#0a6b15d",
|
|
@@ -38,6 +39,7 @@
|
|
|
38
39
|
"@openclaw/plugin-inspector": "^0.3.10",
|
|
39
40
|
"@types/node": "^24.6.0",
|
|
40
41
|
"@types/picomatch": "^4.0.2",
|
|
42
|
+
"knip": "^6.32.2",
|
|
41
43
|
"openclaw": "2026.8.1-beta.3",
|
|
42
44
|
"tsx": "^4.20.6",
|
|
43
45
|
"typescript": "^5.9.3"
|