@unblocklabs/unblock-memory 0.2.0 → 0.2.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/README.md +2 -1
- package/dist/src/manager.d.ts +1 -1
- package/dist/src/manager.js +36 -3
- package/dist/src/plugin.js +19 -13
- package/dist/src/runtime.d.ts +33 -0
- package/dist/src/runtime.js +44 -0
- package/dist/src/workspace-path-classifier.d.ts +4 -0
- package/dist/src/workspace-path-classifier.js +41 -0
- package/openclaw.plugin.json +3 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -103,7 +103,8 @@ eligible.
|
|
|
103
103
|
The optional `sessions` corpus reads the current agent's normal OpenClaw SQLite
|
|
104
104
|
store and indexes its active user/assistant transcript branch. It defaults to
|
|
105
105
|
channel and group conversations; add `direct` explicitly to include DMs. Run
|
|
106
|
-
`memory_sync_sessions` to
|
|
106
|
+
`memory_sync_sessions` to start a refresh, then use `memory_sync_status` to
|
|
107
|
+
check its progress or result. Projections are private derived Markdown
|
|
107
108
|
under the agent's `unblock-memory/sessions` state directory and can be rebuilt
|
|
108
109
|
from OpenClaw at any time. Session results include provider, chat type,
|
|
109
110
|
conversation identity, and start time. They participate in the same search and
|
package/dist/src/manager.d.ts
CHANGED
|
@@ -37,7 +37,7 @@ export declare class QmdMemoryManager implements MemorySearchManagerContract {
|
|
|
37
37
|
});
|
|
38
38
|
start(): Promise<void>;
|
|
39
39
|
sync(params?: MemorySyncParams): Promise<void>;
|
|
40
|
-
syncSessions(force?: boolean): Promise<SessionSyncResult>;
|
|
40
|
+
syncSessions(force?: boolean, onPhase?: (phase: "projecting" | "indexing") => void): Promise<SessionSyncResult>;
|
|
41
41
|
recluster(options?: MemoryReclusterOptions, signal?: AbortSignal): Promise<MemoryAnalysisSummary>;
|
|
42
42
|
listClusters(limit?: number): Promise<MemoryClusterList>;
|
|
43
43
|
fetchCluster(params: {
|
package/dist/src/manager.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdir } from "node:fs/promises";
|
|
1
|
+
import { mkdir, stat } from "node:fs/promises";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import chokidar from "chokidar";
|
|
4
4
|
import { ensureMemoryAnalysisSchema, latestAnalysisRunId, markMemoryAnalysisStale, readAnalysisSummary, readCluster, readClusters, runAnalysisWorker, } from "./analysis.js";
|
|
@@ -151,6 +151,7 @@ export class QmdMemoryManager {
|
|
|
151
151
|
#files = 0;
|
|
152
152
|
#dirty = true;
|
|
153
153
|
#sessionMetadata = new Map();
|
|
154
|
+
#sessionManifestMtimeNs;
|
|
154
155
|
constructor(params) {
|
|
155
156
|
this.#dbPath = params.dbPath;
|
|
156
157
|
this.#workspaceDir = params.workspaceDir;
|
|
@@ -162,12 +163,39 @@ export class QmdMemoryManager {
|
|
|
162
163
|
}
|
|
163
164
|
async start() {
|
|
164
165
|
if (this.#sessions) {
|
|
165
|
-
|
|
166
|
+
await this.#reloadSessionMetadata();
|
|
166
167
|
}
|
|
167
168
|
this.#startWatcher();
|
|
168
169
|
await this.sync({ reason: "first-use" });
|
|
169
170
|
await this.#watchReady;
|
|
170
171
|
}
|
|
172
|
+
async #manifestMtimeNs(path) {
|
|
173
|
+
try {
|
|
174
|
+
return (await stat(path, { bigint: true })).mtimeNs;
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
if (error.code === "ENOENT")
|
|
178
|
+
return undefined;
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
async #reloadSessionMetadata() {
|
|
183
|
+
const sessions = this.#sessions;
|
|
184
|
+
if (!sessions)
|
|
185
|
+
return;
|
|
186
|
+
const mtimeNs = await this.#manifestMtimeNs(sessions.manifestPath);
|
|
187
|
+
const manifest = await readSessionManifest(sessions.manifestPath);
|
|
188
|
+
this.#sessionMetadata = sessionMetadataByPath(manifest);
|
|
189
|
+
this.#sessionManifestMtimeNs = mtimeNs;
|
|
190
|
+
}
|
|
191
|
+
async #refreshSessionMetadata() {
|
|
192
|
+
const sessions = this.#sessions;
|
|
193
|
+
if (!sessions)
|
|
194
|
+
return;
|
|
195
|
+
const mtimeNs = await this.#manifestMtimeNs(sessions.manifestPath);
|
|
196
|
+
if (mtimeNs !== this.#sessionManifestMtimeNs)
|
|
197
|
+
await this.#reloadSessionMetadata();
|
|
198
|
+
}
|
|
171
199
|
#startWatcher() {
|
|
172
200
|
const paths = [...new Set([...this.#sources.values()]
|
|
173
201
|
.filter((source) => source.kind === "files")
|
|
@@ -291,16 +319,18 @@ export class QmdMemoryManager {
|
|
|
291
319
|
};
|
|
292
320
|
return this.#enqueue(run);
|
|
293
321
|
}
|
|
294
|
-
syncSessions(force = false) {
|
|
322
|
+
syncSessions(force = false, onPhase) {
|
|
295
323
|
return this.#enqueue(async () => {
|
|
296
324
|
const sessions = this.#sessions;
|
|
297
325
|
if (!sessions)
|
|
298
326
|
throw new Error('memory session sync requires a configured "sessions" corpus');
|
|
327
|
+
onPhase?.("projecting");
|
|
299
328
|
const store = await this.#getStore();
|
|
300
329
|
const synced = await syncSessionProjections({
|
|
301
330
|
...sessions,
|
|
302
331
|
force,
|
|
303
332
|
index: async () => {
|
|
333
|
+
onPhase?.("indexing");
|
|
304
334
|
const update = await store.update({ collections: [sessions.collection] });
|
|
305
335
|
this.#cleanupRemovedDocuments?.(update.updated + update.removed);
|
|
306
336
|
const analysisStore = store;
|
|
@@ -380,6 +410,9 @@ export class QmdMemoryManager {
|
|
|
380
410
|
opts?.signal?.throwIfAborted();
|
|
381
411
|
await this.#operationChain;
|
|
382
412
|
const sessions = this.#sessions;
|
|
413
|
+
if (opts?.sessionFilter && sessions && collections.includes(sessions.collection)) {
|
|
414
|
+
await this.#refreshSessionMetadata();
|
|
415
|
+
}
|
|
383
416
|
const allowedPaths = opts?.sessionFilter && sessions && collections.includes(sessions.collection)
|
|
384
417
|
? sessionAllowedPaths(this.#sessionMetadata, sessions.collection, opts.sessionFilter)
|
|
385
418
|
: undefined;
|
package/dist/src/plugin.js
CHANGED
|
@@ -35,6 +35,7 @@ const getParameters = Type.Object({
|
|
|
35
35
|
const syncSessionsParameters = Type.Object({
|
|
36
36
|
force: Type.Optional(Type.Boolean()),
|
|
37
37
|
}, { additionalProperties: false });
|
|
38
|
+
const syncStatusParameters = Type.Object({}, { additionalProperties: false });
|
|
38
39
|
function createSearchTool(runtime, ctx) {
|
|
39
40
|
const active = getContext(ctx);
|
|
40
41
|
if (!active)
|
|
@@ -91,22 +92,26 @@ function createSyncSessionsTool(runtime, ctx) {
|
|
|
91
92
|
return {
|
|
92
93
|
name: "memory_sync_sessions",
|
|
93
94
|
label: "Sync Memory Sessions",
|
|
94
|
-
description: "
|
|
95
|
+
description: "Start projecting and indexing this agent's configured OpenClaw session transcripts. Use memory_sync_status to check completion.",
|
|
95
96
|
parameters: syncSessionsParameters,
|
|
96
97
|
async execute(_toolCallId, params) {
|
|
97
98
|
const { force } = Value.Parse(syncSessionsParameters, params);
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
99
|
+
return jsonResult(runtime.startSessionSync(active, force));
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function createSyncStatusTool(runtime, ctx) {
|
|
104
|
+
const active = getContext(ctx);
|
|
105
|
+
if (!active)
|
|
106
|
+
return null;
|
|
107
|
+
return {
|
|
108
|
+
name: "memory_sync_status",
|
|
109
|
+
label: "Memory Session Sync Status",
|
|
110
|
+
description: "Check the current or latest session transcript sync.",
|
|
111
|
+
parameters: syncStatusParameters,
|
|
112
|
+
async execute(_toolCallId, params) {
|
|
113
|
+
Value.Parse(syncStatusParameters, params);
|
|
114
|
+
return jsonResult(runtime.sessionSyncStatus(active.agentId));
|
|
110
115
|
},
|
|
111
116
|
};
|
|
112
117
|
}
|
|
@@ -281,6 +286,7 @@ export function registerUnblockMemory(api) {
|
|
|
281
286
|
api.registerTool((ctx) => createSearchTool(runtime, ctx), { names: ["memory_search"] });
|
|
282
287
|
api.registerTool((ctx) => createGetTool(runtime, ctx), { names: ["memory_get"] });
|
|
283
288
|
api.registerTool((ctx) => createSyncSessionsTool(runtime, ctx), { names: ["memory_sync_sessions"] });
|
|
289
|
+
api.registerTool((ctx) => createSyncStatusTool(runtime, ctx), { names: ["memory_sync_status"] });
|
|
284
290
|
api.registerTool((ctx) => createReclusterTool(runtime, ctx), { names: ["memory_recluster"] });
|
|
285
291
|
api.registerTool((ctx) => createListClustersTool(runtime, ctx), { names: ["memory_list_clusters"] });
|
|
286
292
|
api.registerTool((ctx) => createFetchClusterTool(runtime, ctx), { names: ["memory_fetch_cluster"] });
|
package/dist/src/runtime.d.ts
CHANGED
|
@@ -2,6 +2,33 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/plugin-entry";
|
|
|
2
2
|
import type { CorpusConfig } from "./config.js";
|
|
3
3
|
import type { MemoryPluginRuntimeContract } from "./contracts.js";
|
|
4
4
|
import { QmdMemoryManager } from "./manager.js";
|
|
5
|
+
import type { SessionSyncResult } from "./session-sync.js";
|
|
6
|
+
export type SessionSyncStatus = {
|
|
7
|
+
status: "idle";
|
|
8
|
+
} | {
|
|
9
|
+
status: "running";
|
|
10
|
+
phase: "queued" | "projecting" | "indexing";
|
|
11
|
+
startedAt: string;
|
|
12
|
+
} | ({
|
|
13
|
+
status: "completed";
|
|
14
|
+
startedAt: string;
|
|
15
|
+
completedAt: string;
|
|
16
|
+
} & SessionSyncResult) | {
|
|
17
|
+
status: "failed";
|
|
18
|
+
startedAt: string;
|
|
19
|
+
completedAt: string;
|
|
20
|
+
error: string;
|
|
21
|
+
};
|
|
22
|
+
export type SessionSyncStartResult = {
|
|
23
|
+
status: "started";
|
|
24
|
+
startedAt: string;
|
|
25
|
+
} | {
|
|
26
|
+
status: "already_running";
|
|
27
|
+
startedAt: string;
|
|
28
|
+
} | {
|
|
29
|
+
status: "unavailable";
|
|
30
|
+
error: string;
|
|
31
|
+
};
|
|
5
32
|
export declare class QmdMemoryRuntime implements MemoryPluginRuntimeContract {
|
|
6
33
|
#private;
|
|
7
34
|
constructor(corpora: readonly CorpusConfig[], analysisExecutable?: string);
|
|
@@ -18,6 +45,12 @@ export declare class QmdMemoryRuntime implements MemoryPluginRuntimeContract {
|
|
|
18
45
|
resolveMemoryBackendConfig(): {
|
|
19
46
|
backend: "builtin";
|
|
20
47
|
};
|
|
48
|
+
classifyWorkspaceMemoryPaths: NonNullable<MemoryPluginRuntimeContract["classifyWorkspaceMemoryPaths"]>;
|
|
49
|
+
startSessionSync(params: {
|
|
50
|
+
cfg: OpenClawConfig;
|
|
51
|
+
agentId: string;
|
|
52
|
+
}, force?: boolean): SessionSyncStartResult;
|
|
53
|
+
sessionSyncStatus(agentId: string): SessionSyncStatus;
|
|
21
54
|
closeMemorySearchManager(params: {
|
|
22
55
|
agentId: string;
|
|
23
56
|
}): Promise<void>;
|
package/dist/src/runtime.js
CHANGED
|
@@ -4,10 +4,12 @@ import { resolveAgentIdentity } from "openclaw/plugin-sdk/agent-runtime";
|
|
|
4
4
|
import { QmdMemoryManager } from "./manager.js";
|
|
5
5
|
import { resolveTimezone } from "./session-projector.js";
|
|
6
6
|
import { resolveSessionSource, resolveSources } from "./sources.js";
|
|
7
|
+
import { classifyWorkspaceMemoryPaths } from "./workspace-path-classifier.js";
|
|
7
8
|
export class QmdMemoryRuntime {
|
|
8
9
|
#corpora;
|
|
9
10
|
#analysisExecutable;
|
|
10
11
|
#managers = new Map();
|
|
12
|
+
#sessionSyncStatuses = new Map();
|
|
11
13
|
constructor(corpora, analysisExecutable) {
|
|
12
14
|
this.#corpora = corpora;
|
|
13
15
|
this.#analysisExecutable = analysisExecutable;
|
|
@@ -29,6 +31,48 @@ export class QmdMemoryRuntime {
|
|
|
29
31
|
resolveMemoryBackendConfig() {
|
|
30
32
|
return { backend: "builtin" };
|
|
31
33
|
}
|
|
34
|
+
classifyWorkspaceMemoryPaths = classifyWorkspaceMemoryPaths;
|
|
35
|
+
startSessionSync(params, force = false) {
|
|
36
|
+
if (!this.#corpora.some((corpus) => corpus.kind === "sessions")) {
|
|
37
|
+
return {
|
|
38
|
+
status: "unavailable",
|
|
39
|
+
error: 'memory session sync requires a configured "sessions" corpus',
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const current = this.sessionSyncStatus(params.agentId);
|
|
43
|
+
if (current.status === "running") {
|
|
44
|
+
return { status: "already_running", startedAt: current.startedAt };
|
|
45
|
+
}
|
|
46
|
+
const startedAt = new Date().toISOString();
|
|
47
|
+
this.#sessionSyncStatuses.set(params.agentId, { status: "running", phase: "queued", startedAt });
|
|
48
|
+
const run = async () => {
|
|
49
|
+
const { manager, error } = await this.getMemorySearchManager(params);
|
|
50
|
+
if (!manager)
|
|
51
|
+
throw new Error(error ?? "memory unavailable");
|
|
52
|
+
return await manager.syncSessions(force, (phase) => {
|
|
53
|
+
this.#sessionSyncStatuses.set(params.agentId, { status: "running", phase, startedAt });
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
void run().then((result) => {
|
|
57
|
+
this.#sessionSyncStatuses.set(params.agentId, {
|
|
58
|
+
status: "completed",
|
|
59
|
+
startedAt,
|
|
60
|
+
completedAt: new Date().toISOString(),
|
|
61
|
+
...result,
|
|
62
|
+
});
|
|
63
|
+
}, (error) => {
|
|
64
|
+
this.#sessionSyncStatuses.set(params.agentId, {
|
|
65
|
+
status: "failed",
|
|
66
|
+
startedAt,
|
|
67
|
+
completedAt: new Date().toISOString(),
|
|
68
|
+
error: error instanceof Error ? error.message : String(error),
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
return { status: "started", startedAt };
|
|
72
|
+
}
|
|
73
|
+
sessionSyncStatus(agentId) {
|
|
74
|
+
return this.#sessionSyncStatuses.get(agentId) ?? { status: "idle" };
|
|
75
|
+
}
|
|
32
76
|
async closeMemorySearchManager(params) {
|
|
33
77
|
const pending = this.#managers.get(params.agentId);
|
|
34
78
|
this.#managers.delete(params.agentId);
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { MemoryPluginRuntimeContract } from "./contracts.js";
|
|
2
|
+
type ClassifyWorkspaceMemoryPaths = NonNullable<MemoryPluginRuntimeContract["classifyWorkspaceMemoryPaths"]>;
|
|
3
|
+
export declare const classifyWorkspaceMemoryPaths: ClassifyWorkspaceMemoryPaths;
|
|
4
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { isPathStrictlyInside } from "openclaw/plugin-sdk/file-access-runtime";
|
|
4
|
+
import { readMemoryArtifactProvenance } from "openclaw/plugin-sdk/memory-core-host-runtime-core";
|
|
5
|
+
export const classifyWorkspaceMemoryPaths = async (params) => await Promise.all(params.relativePaths.map(async (relativePath) => {
|
|
6
|
+
let workspacePath;
|
|
7
|
+
let filePath;
|
|
8
|
+
try {
|
|
9
|
+
[workspacePath, filePath] = await Promise.all([
|
|
10
|
+
fs.realpath(params.workspaceDir),
|
|
11
|
+
fs.realpath(path.resolve(params.workspaceDir, relativePath)),
|
|
12
|
+
]);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return { relativePath, originClass: "untrusted" };
|
|
16
|
+
}
|
|
17
|
+
if (!isPathStrictlyInside(workspacePath, filePath)) {
|
|
18
|
+
return { relativePath, originClass: "untrusted" };
|
|
19
|
+
}
|
|
20
|
+
const workspaceRelativePath = path.relative(workspacePath, filePath);
|
|
21
|
+
const segments = workspaceRelativePath.split(path.sep);
|
|
22
|
+
const curatedRoot = segments.length === 1 &&
|
|
23
|
+
["MEMORY.md", "memory.md", "USER.md"].includes(segments[0]);
|
|
24
|
+
if ((segments.length === 1 && ["DREAMS.md", "dreams.md"].includes(segments[0])) ||
|
|
25
|
+
(segments[0] === "memory" && ["dreaming", ".dreams"].includes(segments[1]))) {
|
|
26
|
+
return { relativePath, originClass: "system" };
|
|
27
|
+
}
|
|
28
|
+
const isWorkspaceMemory = curatedRoot ||
|
|
29
|
+
(segments[0] === "memory" && segments.at(-1)?.endsWith(".md") === true);
|
|
30
|
+
const normalizedPath = workspaceRelativePath.replaceAll(path.sep, "/");
|
|
31
|
+
const recorded = isWorkspaceMemory
|
|
32
|
+
? await readMemoryArtifactProvenance({
|
|
33
|
+
workspaceDir: params.workspaceDir,
|
|
34
|
+
relativePath: normalizedPath,
|
|
35
|
+
})
|
|
36
|
+
: undefined;
|
|
37
|
+
return {
|
|
38
|
+
relativePath,
|
|
39
|
+
originClass: recorded?.originClass ?? (isWorkspaceMemory ? "agent" : "untrusted"),
|
|
40
|
+
};
|
|
41
|
+
}));
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "unblock-memory",
|
|
3
3
|
"name": "Unblock Memory",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.2",
|
|
5
5
|
"description": "Indexes, retrieves, and analyzes configured workspace memory with existing QMD vectors.",
|
|
6
6
|
"kind": "memory",
|
|
7
7
|
"activation": { "onStartup": false },
|
|
8
|
-
"contracts": { "tools": ["memory_search", "memory_get", "memory_sync_sessions", "memory_recluster", "memory_list_clusters", "memory_fetch_cluster"] },
|
|
8
|
+
"contracts": { "tools": ["memory_search", "memory_get", "memory_sync_sessions", "memory_sync_status", "memory_recluster", "memory_list_clusters", "memory_fetch_cluster"] },
|
|
9
9
|
"toolMetadata": {
|
|
10
10
|
"memory_sync_sessions": { "sideEffecting": true },
|
|
11
|
+
"memory_sync_status": { "replaySafe": true },
|
|
11
12
|
"memory_recluster": { "sideEffecting": true },
|
|
12
13
|
"memory_list_clusters": { "replaySafe": true },
|
|
13
14
|
"memory_fetch_cluster": { "replaySafe": true }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unblocklabs/unblock-memory",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Workspace-native memory for OpenClaw, powered by QMD",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
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"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@unblocklabs/qmd": "github
|
|
33
|
+
"@unblocklabs/qmd": "https://github.com/unblocklabs-ai/qmd/releases/download/v2.9.1/unblocklabs-qmd-2.9.1.tgz",
|
|
34
34
|
"chokidar": "5.0.0",
|
|
35
35
|
"picomatch": "^4.0.5",
|
|
36
36
|
"typebox": "1.3.6"
|