@unblocklabs/unblock-memory 0.1.1 → 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/README.md +91 -19
- package/dist/src/analysis.d.ts +2 -6
- package/dist/src/analysis.js +45 -48
- package/dist/src/config.d.ts +16 -2
- package/dist/src/config.js +78 -15
- package/dist/src/contracts.d.ts +18 -0
- package/dist/src/manager.d.ts +16 -8
- package/dist/src/manager.js +175 -25
- package/dist/src/plugin.js +84 -122
- package/dist/src/runtime.d.ts +2 -1
- package/dist/src/runtime.js +30 -7
- package/dist/src/session-projector.d.ts +21 -0
- package/dist/src/session-projector.js +163 -0
- package/dist/src/session-sync.d.ts +43 -0
- package/dist/src/session-sync.js +302 -0
- package/dist/src/sources.d.ts +7 -1
- package/dist/src/sources.js +32 -2
- package/openclaw.plugin.json +48 -8
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ index. It does not re-embed memory, copy vectors, or create another database.
|
|
|
13
13
|
From npm:
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
openclaw plugins install
|
|
16
|
+
openclaw plugins install npm:@unblocklabs/unblock-memory
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
Or directly from GitHub:
|
|
@@ -22,10 +22,15 @@ Or directly from GitHub:
|
|
|
22
22
|
openclaw plugins install git:github.com/unblocklabs-ai/unblock-memory
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
Install the plugin once on each OpenClaw host, not once per agent. It installs
|
|
26
|
+
its pinned `@unblocklabs/qmd` runtime dependency automatically, so QMD does not
|
|
27
|
+
need to be installed separately. Each agent gets its own QMD index when it first
|
|
28
|
+
uses memory.
|
|
29
|
+
|
|
25
30
|
## Configuration
|
|
26
31
|
|
|
27
|
-
Select the plugin as the memory provider and
|
|
28
|
-
directories, or globs
|
|
32
|
+
Select the plugin as the memory provider and group exact Markdown files,
|
|
33
|
+
directories, or globs into named corpora:
|
|
29
34
|
|
|
30
35
|
```json5
|
|
31
36
|
{
|
|
@@ -34,14 +39,26 @@ directories, or globs to index:
|
|
|
34
39
|
entries: {
|
|
35
40
|
"unblock-memory": {
|
|
36
41
|
config: {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
corpora: [
|
|
43
|
+
{
|
|
44
|
+
name: "memory",
|
|
45
|
+
kind: "files",
|
|
46
|
+
paths: ["MEMORY.md", "USER.md", "memory/**/*.md"],
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: "projects",
|
|
50
|
+
kind: "files",
|
|
51
|
+
paths: ["/absolute/shared/**/*.md"],
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: "sessions",
|
|
55
|
+
kind: "sessions",
|
|
56
|
+
chatTypes: ["channel", "group"],
|
|
57
|
+
},
|
|
42
58
|
],
|
|
59
|
+
// Optional: omit unless the local analysis worker is installed.
|
|
43
60
|
analysis: {
|
|
44
|
-
executable: "/absolute/path/to/unblock-memory-analysis",
|
|
61
|
+
executable: "/absolute/path/to/unblock-cluster/bin/unblock-memory-analysis",
|
|
45
62
|
},
|
|
46
63
|
},
|
|
47
64
|
},
|
|
@@ -51,9 +68,47 @@ directories, or globs to index:
|
|
|
51
68
|
```
|
|
52
69
|
|
|
53
70
|
Relative entries resolve from each agent workspace. Absolute paths and `~/`
|
|
54
|
-
paths are supported. A directory means recursive Markdown. When `
|
|
55
|
-
omitted, the
|
|
56
|
-
|
|
71
|
+
paths are supported. A directory means recursive Markdown. When `corpora` is
|
|
72
|
+
omitted, the plugin creates a `memory` corpus containing `MEMORY.md`, `USER.md`,
|
|
73
|
+
and `memory/**/*.md`. Explicit configuration must include exactly one `memory`
|
|
74
|
+
corpus; other unique names may be added for custom material.
|
|
75
|
+
|
|
76
|
+
`memory_search` searches every configured corpus by default. Pass
|
|
77
|
+
`corpora: ["projects"]` to search selected corpora or `corpora: ["all"]` to
|
|
78
|
+
request all of them explicitly. Search results include their corpus name and
|
|
79
|
+
remain readable by passing the returned `qmd://` path to `memory_get`.
|
|
80
|
+
|
|
81
|
+
Use `sessionFilter` to restrict session results by metadata while leaving file
|
|
82
|
+
corpora searchable. Supported fields are `startedFrom` and `startedTo`
|
|
83
|
+
(inclusive ISO 8601 timestamps), `provider`, `chatType`, `accountId`, and
|
|
84
|
+
`conversationId`:
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
{
|
|
88
|
+
"query": "deployment decision",
|
|
89
|
+
"sessionFilter": {
|
|
90
|
+
"startedFrom": "2026-08-01T00:00:00Z",
|
|
91
|
+
"provider": "slack",
|
|
92
|
+
"chatType": "channel"
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Provider matching is case-normalized; `chatType` uses the lowercase values
|
|
98
|
+
shown in the configuration example. Account and conversation IDs are trimmed
|
|
99
|
+
and matched exactly. When only `sessions` is selected and no sessions match,
|
|
100
|
+
search returns no results. With other corpora selected, their results remain
|
|
101
|
+
eligible.
|
|
102
|
+
|
|
103
|
+
The optional `sessions` corpus reads the current agent's normal OpenClaw SQLite
|
|
104
|
+
store and indexes its active user/assistant transcript branch. It defaults to
|
|
105
|
+
channel and group conversations; add `direct` explicitly to include DMs. Run
|
|
106
|
+
`memory_sync_sessions` to refresh it. Projections are private derived Markdown
|
|
107
|
+
under the agent's `unblock-memory/sessions` state directory and can be rebuilt
|
|
108
|
+
from OpenClaw at any time. Session results include provider, chat type,
|
|
109
|
+
conversation identity, and start time. They participate in the same search and
|
|
110
|
+
clustering index as file memory. This phase does not sync sessions at startup or
|
|
111
|
+
on a schedule; refreshes are manual through `memory_sync_sessions`.
|
|
57
112
|
|
|
58
113
|
Indexes live at `~/.openclaw/agents/<agentId>/unblock-memory/index.sqlite` (or the
|
|
59
114
|
equivalent configured OpenClaw state directory). The first lookup builds the
|
|
@@ -62,12 +117,30 @@ refresh.
|
|
|
62
117
|
|
|
63
118
|
## Memory analysis
|
|
64
119
|
|
|
65
|
-
Analysis is opt-in
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
120
|
+
Analysis is opt-in. Core indexing, `memory_search`, and `memory_get` need only
|
|
121
|
+
Unblock Memory and its automatically installed QMD dependency. To enable
|
|
122
|
+
clustering, install the
|
|
123
|
+
[`unblock-cluster`](https://github.com/unblocklabs-ai/unblock-cluster) worker once
|
|
124
|
+
on the same host:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
git clone https://github.com/unblocklabs-ai/unblock-cluster.git
|
|
128
|
+
cd unblock-cluster
|
|
129
|
+
python3 -m venv .venv
|
|
130
|
+
.venv/bin/python -m pip install -r requirements.txt
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Set `analysis.executable` to the absolute path of
|
|
134
|
+
`bin/unblock-memory-analysis` in that checkout. One worker installation can
|
|
135
|
+
serve every agent on the host. The plugin invokes it directly with
|
|
136
|
+
`--db <the agent's known index path>` and, when requested, a validated
|
|
137
|
+
`--config-json <clustering options>` payload. Agents cannot choose a database,
|
|
69
138
|
executable, shell command, or arbitrary arguments.
|
|
70
139
|
|
|
140
|
+
Without the worker, `memory_list_clusters` reports that memory has not been
|
|
141
|
+
analyzed and `memory_recluster` reports that analysis is unavailable. Ordinary
|
|
142
|
+
memory search and reads continue to work.
|
|
143
|
+
|
|
71
144
|
The analysis worker reads QMD's existing semantic vectors and writes only
|
|
72
145
|
derived results into three namespaced tables in that same `index.sqlite`:
|
|
73
146
|
|
|
@@ -100,6 +173,5 @@ A failed rebuild leaves the stale result intact, while a successful rebuild
|
|
|
100
173
|
atomically replaces it. Analysis is never scheduled automatically. If the worker
|
|
101
174
|
is absent or fails, `memory_search` and `memory_get` continue to work.
|
|
102
175
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
rebuilds its new index from the configured workspace Markdown.
|
|
176
|
+
Existing `unblock-qmd` indexes are derived caches and may be left in place;
|
|
177
|
+
Unblock Memory rebuilds its own index from configured corpora.
|
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/config.d.ts
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
1
|
+
export type FileCorpusConfig = {
|
|
2
|
+
name: string;
|
|
3
|
+
kind: "files";
|
|
3
4
|
paths: readonly string[];
|
|
5
|
+
};
|
|
6
|
+
declare const CHAT_TYPES: readonly ["channel", "group", "direct"];
|
|
7
|
+
export type ChatType = typeof CHAT_TYPES[number];
|
|
8
|
+
type SessionCorpusConfig = {
|
|
9
|
+
name: "sessions";
|
|
10
|
+
kind: "sessions";
|
|
11
|
+
chatTypes: readonly ChatType[];
|
|
12
|
+
};
|
|
13
|
+
export type CorpusConfig = FileCorpusConfig | SessionCorpusConfig;
|
|
14
|
+
export declare const DEFAULT_CORPORA: readonly FileCorpusConfig[];
|
|
15
|
+
export type UnblockMemoryConfig = {
|
|
16
|
+
corpora: readonly CorpusConfig[];
|
|
4
17
|
analysis: {
|
|
5
18
|
executable?: string;
|
|
6
19
|
};
|
|
7
20
|
};
|
|
8
21
|
export declare function resolveConfig(value: unknown): UnblockMemoryConfig;
|
|
22
|
+
export {};
|
package/dist/src/config.js
CHANGED
|
@@ -1,27 +1,90 @@
|
|
|
1
1
|
import { isAbsolute } from "node:path";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
const DEFAULT_PATHS = ["MEMORY.md", "USER.md", "memory/**/*.md"];
|
|
3
|
+
const CHAT_TYPES = ["channel", "group", "direct"];
|
|
4
|
+
export const DEFAULT_CORPORA = [{
|
|
5
|
+
name: "memory",
|
|
6
|
+
kind: "files",
|
|
7
|
+
paths: DEFAULT_PATHS,
|
|
8
|
+
}];
|
|
9
|
+
function assertOnlyKeys(value, allowed, label) {
|
|
10
|
+
const unknown = Object.keys(value).find((key) => !allowed.includes(key));
|
|
11
|
+
if (unknown)
|
|
12
|
+
throw new Error(`unblock-memory ${label} has unknown property: ${unknown}`);
|
|
13
|
+
}
|
|
14
|
+
function resolveCorpora(value) {
|
|
15
|
+
if (value === undefined)
|
|
16
|
+
return DEFAULT_CORPORA;
|
|
17
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
18
|
+
throw new Error("unblock-memory corpora must be a non-empty array");
|
|
6
19
|
}
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
20
|
+
const names = new Set();
|
|
21
|
+
const corpora = value.map((entry, index) => {
|
|
22
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
23
|
+
throw new Error(`unblock-memory corpora[${index}] must be an object`);
|
|
24
|
+
}
|
|
25
|
+
const corpus = entry;
|
|
26
|
+
if (typeof corpus.name !== "string" || !corpus.name.trim()) {
|
|
27
|
+
throw new Error(`unblock-memory corpora[${index}].name must be a non-empty string`);
|
|
28
|
+
}
|
|
29
|
+
const name = corpus.name.trim();
|
|
30
|
+
if (name === "all") {
|
|
31
|
+
throw new Error(`unblock-memory corpus name is reserved: ${name}`);
|
|
32
|
+
}
|
|
33
|
+
if (names.has(name))
|
|
34
|
+
throw new Error(`unblock-memory corpus names must be unique: ${name}`);
|
|
35
|
+
names.add(name);
|
|
36
|
+
if (corpus.kind === "sessions") {
|
|
37
|
+
assertOnlyKeys(corpus, ["name", "kind", "chatTypes"], `corpora[${index}]`);
|
|
38
|
+
if (name !== "sessions") {
|
|
39
|
+
throw new Error('unblock-memory session corpus must be named "sessions"');
|
|
40
|
+
}
|
|
41
|
+
const chatTypes = corpus.chatTypes ?? ["channel", "group"];
|
|
42
|
+
if (!Array.isArray(chatTypes) || chatTypes.length === 0 ||
|
|
43
|
+
!chatTypes.every((chatType) => CHAT_TYPES.includes(chatType))) {
|
|
44
|
+
throw new Error(`unblock-memory corpus sessions chatTypes must contain channel, group, or direct`);
|
|
45
|
+
}
|
|
46
|
+
return { name: "sessions", kind: "sessions", chatTypes: [...new Set(chatTypes)] };
|
|
47
|
+
}
|
|
48
|
+
assertOnlyKeys(corpus, ["name", "kind", "paths"], `corpora[${index}]`);
|
|
49
|
+
if (name === "sessions") {
|
|
50
|
+
throw new Error('unblock-memory corpus named "sessions" must have kind "sessions"');
|
|
12
51
|
}
|
|
13
|
-
|
|
52
|
+
if (corpus.kind !== "files") {
|
|
53
|
+
throw new Error(`unblock-memory corpus ${name} must have kind "files" or "sessions"`);
|
|
54
|
+
}
|
|
55
|
+
if (!Array.isArray(corpus.paths) || corpus.paths.length === 0 ||
|
|
56
|
+
!corpus.paths.every((path) => typeof path === "string" && path.trim())) {
|
|
57
|
+
throw new Error(`unblock-memory corpus ${name} paths must be a non-empty array of non-empty strings`);
|
|
58
|
+
}
|
|
59
|
+
return { name, kind: "files", paths: corpus.paths.map((path) => path.trim()) };
|
|
60
|
+
});
|
|
61
|
+
if (corpora.filter((corpus) => corpus.name === "memory").length !== 1) {
|
|
62
|
+
throw new Error('unblock-memory corpora must contain exactly one corpus named "memory"');
|
|
14
63
|
}
|
|
64
|
+
return corpora;
|
|
65
|
+
}
|
|
66
|
+
export function resolveConfig(value) {
|
|
67
|
+
if (value === undefined || value === null) {
|
|
68
|
+
return { corpora: DEFAULT_CORPORA, analysis: {} };
|
|
69
|
+
}
|
|
70
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
71
|
+
throw new Error("unblock-memory config must be an object");
|
|
72
|
+
}
|
|
73
|
+
const config = value;
|
|
74
|
+
assertOnlyKeys(config, ["corpora", "analysis"], "config");
|
|
75
|
+
const corpora = resolveCorpora(config.corpora);
|
|
15
76
|
if (config.analysis === undefined)
|
|
16
|
-
return {
|
|
17
|
-
if (!config.analysis || typeof config.analysis !== "object") {
|
|
77
|
+
return { corpora, analysis: {} };
|
|
78
|
+
if (!config.analysis || typeof config.analysis !== "object" || Array.isArray(config.analysis)) {
|
|
18
79
|
throw new Error("unblock-memory analysis must be an object");
|
|
19
80
|
}
|
|
20
|
-
const
|
|
81
|
+
const analysis = config.analysis;
|
|
82
|
+
assertOnlyKeys(analysis, ["executable"], "analysis");
|
|
83
|
+
const configured = analysis.executable;
|
|
21
84
|
if (configured === undefined)
|
|
22
|
-
return {
|
|
85
|
+
return { corpora, analysis: {} };
|
|
23
86
|
if (typeof configured !== "string" || !configured.trim() || !isAbsolute(configured.trim())) {
|
|
24
87
|
throw new Error("unblock-memory analysis.executable must be an absolute non-empty path");
|
|
25
88
|
}
|
|
26
|
-
return {
|
|
89
|
+
return { corpora, analysis: { executable: configured.trim() } };
|
|
27
90
|
}
|
package/dist/src/contracts.d.ts
CHANGED
|
@@ -1,9 +1,27 @@
|
|
|
1
1
|
import type { MemoryPluginCapability } from "openclaw/plugin-sdk/memory-host-core";
|
|
2
|
+
import type { SessionMetadata } from "./session-projector.js";
|
|
3
|
+
import type { ChatType } from "./config.js";
|
|
2
4
|
export type MemoryPluginRuntimeContract = NonNullable<MemoryPluginCapability["runtime"]>;
|
|
3
5
|
type ManagerLookup = Awaited<ReturnType<MemoryPluginRuntimeContract["getMemorySearchManager"]>>;
|
|
4
6
|
export type MemorySearchManagerContract = NonNullable<ManagerLookup["manager"]>;
|
|
5
7
|
export type MemoryProviderStatus = ReturnType<MemorySearchManagerContract["status"]>;
|
|
6
8
|
export type MemorySearchResult = Awaited<ReturnType<MemorySearchManagerContract["search"]>>[number];
|
|
9
|
+
export type CorpusMemorySearchResult = MemorySearchResult & {
|
|
10
|
+
corpus: string;
|
|
11
|
+
session?: SessionMetadata;
|
|
12
|
+
};
|
|
13
|
+
export type SessionSearchFilter = {
|
|
14
|
+
startedFrom?: string;
|
|
15
|
+
startedTo?: string;
|
|
16
|
+
provider?: string;
|
|
17
|
+
chatType?: ChatType;
|
|
18
|
+
accountId?: string;
|
|
19
|
+
conversationId?: string;
|
|
20
|
+
};
|
|
21
|
+
export type CorpusSearchOptions = NonNullable<Parameters<MemorySearchManagerContract["search"]>[1]> & {
|
|
22
|
+
corpora?: readonly string[];
|
|
23
|
+
sessionFilter?: SessionSearchFilter;
|
|
24
|
+
};
|
|
7
25
|
export type MemoryEmbeddingProbeResult = Awaited<ReturnType<MemorySearchManagerContract["probeEmbeddingAvailability"]>>;
|
|
8
26
|
export type MemorySyncParams = Parameters<NonNullable<MemorySearchManagerContract["sync"]>>[0];
|
|
9
27
|
export type MemoryReadResult = {
|
package/dist/src/manager.d.ts
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
import type { QMDStore } from "@unblocklabs/qmd";
|
|
2
2
|
import { type AnalysisRunner, type MemoryAnalysisSummary, type MemoryClusterDetail, type MemoryClusterList, type MemoryReclusterOptions } from "./analysis.js";
|
|
3
|
-
import type { MemoryEmbeddingProbeResult, MemoryProviderStatus, MemoryReadResult, MemorySearchManagerContract,
|
|
3
|
+
import type { CorpusMemorySearchResult, CorpusSearchOptions, MemoryEmbeddingProbeResult, MemoryProviderStatus, MemoryReadResult, MemorySearchManagerContract, MemorySyncParams } from "./contracts.js";
|
|
4
|
+
import type { ChatType } from "./config.js";
|
|
5
|
+
import { type SessionSyncResult } from "./session-sync.js";
|
|
4
6
|
import { type ResolvedSource } from "./sources.js";
|
|
5
7
|
export type ManagerStore = Pick<QMDStore, "update" | "embed" | "getStatus" | "listCollections" | "searchLex" | "vsearch" | "get" | "getDocumentBody" | "close">;
|
|
8
|
+
export type ManagerSessionConfig = {
|
|
9
|
+
agentId: string;
|
|
10
|
+
agentName: string;
|
|
11
|
+
chatTypes: readonly ChatType[];
|
|
12
|
+
collection: string;
|
|
13
|
+
databasePath: string;
|
|
14
|
+
manifestPath: string;
|
|
15
|
+
outputDir: string;
|
|
16
|
+
timezone: string;
|
|
17
|
+
};
|
|
6
18
|
export declare function enableSecureDelete(store: QMDStore): void;
|
|
7
19
|
export declare function cleanupRemovedDocuments(store: QMDStore, changedDocuments?: number): number;
|
|
8
20
|
export declare function pruneStaleCollections(store: QMDStore, configuredCollections: ReadonlySet<string>): Promise<number>;
|
|
@@ -21,22 +33,18 @@ export declare class QmdMemoryManager implements MemorySearchManagerContract {
|
|
|
21
33
|
storeFactory?: () => Promise<ManagerStore>;
|
|
22
34
|
analysisExecutable?: string;
|
|
23
35
|
analysisRunner?: AnalysisRunner;
|
|
36
|
+
sessions?: ManagerSessionConfig;
|
|
24
37
|
});
|
|
25
38
|
start(): Promise<void>;
|
|
26
39
|
sync(params?: MemorySyncParams): Promise<void>;
|
|
40
|
+
syncSessions(force?: boolean): Promise<SessionSyncResult>;
|
|
27
41
|
recluster(options?: MemoryReclusterOptions, signal?: AbortSignal): Promise<MemoryAnalysisSummary>;
|
|
28
42
|
listClusters(limit?: number): Promise<MemoryClusterList>;
|
|
29
43
|
fetchCluster(params: {
|
|
30
44
|
clusterId: string;
|
|
31
45
|
topK?: number;
|
|
32
46
|
}): Promise<MemoryClusterDetail>;
|
|
33
|
-
search(query: string, opts?:
|
|
34
|
-
maxResults?: number;
|
|
35
|
-
minScore?: number;
|
|
36
|
-
lexicalOnly?: boolean;
|
|
37
|
-
sources?: Array<"memory" | "sessions">;
|
|
38
|
-
signal?: AbortSignal;
|
|
39
|
-
}): Promise<MemorySearchResult[]>;
|
|
47
|
+
search(query: string, opts?: CorpusSearchOptions): Promise<CorpusMemorySearchResult[]>;
|
|
40
48
|
readFile(params: {
|
|
41
49
|
relPath: string;
|
|
42
50
|
from?: number;
|