@unblocklabs/unblock-memory 0.1.2 → 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/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 +53 -4
- 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 +2 -2
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/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;
|