@unblocklabs/unblock-memory 0.2.6 → 0.3.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 +71 -11
- package/dist/src/analysis.d.ts +12 -2
- package/dist/src/analysis.js +106 -12
- package/dist/src/config.d.ts +12 -1
- package/dist/src/config.js +69 -15
- package/dist/src/curation.d.ts +70 -0
- package/dist/src/curation.js +191 -0
- package/dist/src/manager.d.ts +23 -0
- package/dist/src/manager.js +257 -29
- package/dist/src/plugin.js +80 -1
- package/dist/src/runtime.d.ts +8 -0
- package/dist/src/runtime.js +14 -2
- package/dist/src/skill-whisperer.d.ts +16 -0
- package/dist/src/skill-whisperer.js +113 -0
- package/dist/src/sources.d.ts +4 -3
- package/dist/src/sources.js +34 -1
- package/openclaw.plugin.json +40 -4
- package/package.json +2 -2
- package/skills/memory-curator/SKILL.md +12 -1
package/dist/src/plugin.js
CHANGED
|
@@ -3,6 +3,7 @@ import { Value } from "typebox/value";
|
|
|
3
3
|
import { jsonResult } from "openclaw/plugin-sdk/agent-runtime";
|
|
4
4
|
import { resolveConfig } from "./config.js";
|
|
5
5
|
import { QmdMemoryRuntime } from "./runtime.js";
|
|
6
|
+
import { registerSkillWhisperer } from "./skill-whisperer.js";
|
|
6
7
|
function getContext(ctx) {
|
|
7
8
|
const cfg = ctx.getRuntimeConfig?.() ?? ctx.runtimeConfig ?? ctx.config;
|
|
8
9
|
if (!cfg || !ctx.agentId)
|
|
@@ -43,7 +44,7 @@ function createSearchTool(runtime, ctx) {
|
|
|
43
44
|
return {
|
|
44
45
|
name: "memory_search",
|
|
45
46
|
label: "Memory Search",
|
|
46
|
-
description: "Search configured
|
|
47
|
+
description: "Search configured memory corpora with semantic vector retrieval. The isolated skills corpus is never included.",
|
|
47
48
|
parameters: searchParameters,
|
|
48
49
|
async execute(_toolCallId, params, signal) {
|
|
49
50
|
const { query: untrimmedQuery, corpora, sessionFilter, maxResults, minScore } = Value.Parse(searchParameters, params);
|
|
@@ -219,6 +220,81 @@ function createFetchClusterTool(runtime, ctx) {
|
|
|
219
220
|
},
|
|
220
221
|
};
|
|
221
222
|
}
|
|
223
|
+
const maintenanceStatus = Type.Union([
|
|
224
|
+
Type.Literal("pending"),
|
|
225
|
+
Type.Literal("resolved"),
|
|
226
|
+
Type.Literal("deferred"),
|
|
227
|
+
Type.Literal("irrelevant"),
|
|
228
|
+
]);
|
|
229
|
+
const listMaintenanceParameters = Type.Object({
|
|
230
|
+
status: Type.Optional(maintenanceStatus),
|
|
231
|
+
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 10 })),
|
|
232
|
+
}, { additionalProperties: false });
|
|
233
|
+
function createListMaintenanceTool(runtime, ctx) {
|
|
234
|
+
const active = getContext(ctx);
|
|
235
|
+
if (!active)
|
|
236
|
+
return null;
|
|
237
|
+
return {
|
|
238
|
+
name: "memory_list_maintenance_tasks",
|
|
239
|
+
label: "List Memory Maintenance Tasks",
|
|
240
|
+
description: "List a bounded curation inbox of memory chronology and duplicate-review proposals.",
|
|
241
|
+
parameters: listMaintenanceParameters,
|
|
242
|
+
async execute(_toolCallId, params) {
|
|
243
|
+
const options = Value.Parse(listMaintenanceParameters, params);
|
|
244
|
+
const { manager, error } = await runtime.getMemorySearchManager(active);
|
|
245
|
+
if (!manager)
|
|
246
|
+
return jsonResult({ status: "unavailable", error: error ?? "memory unavailable" });
|
|
247
|
+
return jsonResult({ status: "ok", tasks: manager.listMaintenanceTasks(options) });
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
const isoTimestamp = Type.String({
|
|
252
|
+
pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,9})?(?:Z|[+-]\\d{2}:\\d{2})$",
|
|
253
|
+
});
|
|
254
|
+
const updateMaintenanceParameters = Type.Object({
|
|
255
|
+
taskId: Type.String({ pattern: "\\S" }),
|
|
256
|
+
action: Type.Union([
|
|
257
|
+
Type.Literal("resolve"),
|
|
258
|
+
Type.Literal("defer"),
|
|
259
|
+
Type.Literal("irrelevant"),
|
|
260
|
+
]),
|
|
261
|
+
note: Type.Optional(Type.String({ minLength: 1, maxLength: 500 })),
|
|
262
|
+
annotation: Type.Optional(Type.Object({
|
|
263
|
+
scope: Type.Optional(Type.Union([Type.Literal("chunk"), Type.Literal("document")])),
|
|
264
|
+
eventTime: isoTimestamp,
|
|
265
|
+
basis: Type.Union([
|
|
266
|
+
Type.Literal("path"),
|
|
267
|
+
Type.Literal("frontmatter"),
|
|
268
|
+
Type.Literal("session"),
|
|
269
|
+
Type.Literal("agent_verified"),
|
|
270
|
+
]),
|
|
271
|
+
evidence: Type.String({ minLength: 1, maxLength: 500 }),
|
|
272
|
+
}, { additionalProperties: false })),
|
|
273
|
+
}, { additionalProperties: false });
|
|
274
|
+
function createUpdateMaintenanceTool(runtime, ctx) {
|
|
275
|
+
const active = getContext(ctx);
|
|
276
|
+
if (!active)
|
|
277
|
+
return null;
|
|
278
|
+
return {
|
|
279
|
+
name: "memory_update_maintenance_task",
|
|
280
|
+
label: "Update Memory Maintenance Task",
|
|
281
|
+
description: "Resolve completed work, defer outstanding work, or dismiss an irrelevant memory-maintenance proposal. This tool never edits source Markdown.",
|
|
282
|
+
parameters: updateMaintenanceParameters,
|
|
283
|
+
async execute(_toolCallId, params) {
|
|
284
|
+
const { taskId, action, note, annotation } = Value.Parse(updateMaintenanceParameters, params);
|
|
285
|
+
const { manager, error } = await runtime.getMemorySearchManager(active);
|
|
286
|
+
if (!manager)
|
|
287
|
+
return jsonResult({ status: "unavailable", error: error ?? "memory unavailable" });
|
|
288
|
+
const updated = manager.updateMaintenanceTask({
|
|
289
|
+
id: taskId,
|
|
290
|
+
status: action === "resolve" ? "resolved" : action === "defer" ? "deferred" : "irrelevant",
|
|
291
|
+
note,
|
|
292
|
+
...(annotation ? { annotation: { ...annotation, scope: annotation.scope ?? "chunk" } } : {}),
|
|
293
|
+
});
|
|
294
|
+
return jsonResult(updated ? { status: "ok", task: updated } : { status: "not_found" });
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
}
|
|
222
298
|
function formatDateInTimezone(timestamp, timezone) {
|
|
223
299
|
const parts = new Intl.DateTimeFormat("en-US", {
|
|
224
300
|
timeZone: timezone,
|
|
@@ -305,6 +381,7 @@ export function registerUnblockMemory(api) {
|
|
|
305
381
|
runtime,
|
|
306
382
|
};
|
|
307
383
|
api.registerMemoryCapability(capability);
|
|
384
|
+
registerSkillWhisperer(api, runtime, config.skillWhisperer);
|
|
308
385
|
api.registerTool((ctx) => createSearchTool(runtime, ctx), { names: ["memory_search"] });
|
|
309
386
|
api.registerTool((ctx) => createGetTool(runtime, ctx), { names: ["memory_get"] });
|
|
310
387
|
api.registerTool((ctx) => createSyncSessionsTool(runtime, ctx), { names: ["memory_sync_sessions"] });
|
|
@@ -312,4 +389,6 @@ export function registerUnblockMemory(api) {
|
|
|
312
389
|
api.registerTool((ctx) => createReclusterTool(runtime, ctx), { names: ["memory_recluster"] });
|
|
313
390
|
api.registerTool((ctx) => createListClustersTool(runtime, ctx), { names: ["memory_list_clusters"] });
|
|
314
391
|
api.registerTool((ctx) => createFetchClusterTool(runtime, ctx), { names: ["memory_fetch_cluster"] });
|
|
392
|
+
api.registerTool((ctx) => createListMaintenanceTool(runtime, ctx), { names: ["memory_list_maintenance_tasks"] });
|
|
393
|
+
api.registerTool((ctx) => createUpdateMaintenanceTool(runtime, ctx), { names: ["memory_update_maintenance_task"] });
|
|
315
394
|
}
|
package/dist/src/runtime.d.ts
CHANGED
|
@@ -68,5 +68,13 @@ export declare class QmdMemoryRuntime implements MemoryPluginRuntimeContract {
|
|
|
68
68
|
agentId: string;
|
|
69
69
|
}): Promise<void>;
|
|
70
70
|
closeAllMemorySearchManagers(): Promise<void>;
|
|
71
|
+
searchSkills(params: {
|
|
72
|
+
cfg: OpenClawConfig;
|
|
73
|
+
agentId: string;
|
|
74
|
+
}, query: string, minScore: number, limit: number): Promise<import("./manager.js").SkillSearchCandidate[]>;
|
|
75
|
+
resolveSkillPath(params: {
|
|
76
|
+
cfg: OpenClawConfig;
|
|
77
|
+
agentId: string;
|
|
78
|
+
}, path: string): string | undefined;
|
|
71
79
|
}
|
|
72
80
|
export {};
|
package/dist/src/runtime.js
CHANGED
|
@@ -5,7 +5,7 @@ import { resolveAgentDir, resolveAgentWorkspaceDir, resolveStateDir, } from "ope
|
|
|
5
5
|
import { resolveAgentIdentity } from "openclaw/plugin-sdk/agent-runtime";
|
|
6
6
|
import { QmdMemoryManager } from "./manager.js";
|
|
7
7
|
import { resolveTimezone } from "./session-projector.js";
|
|
8
|
-
import { resolveSessionSource, resolveSources } from "./sources.js";
|
|
8
|
+
import { resolveConfiguredSkillPath, resolveSessionSource, resolveSources } from "./sources.js";
|
|
9
9
|
import { classifyWorkspaceMemoryPaths } from "./workspace-path-classifier.js";
|
|
10
10
|
const activeSessionSyncs = new Map();
|
|
11
11
|
async function readJson(path) {
|
|
@@ -190,10 +190,21 @@ export class QmdMemoryRuntime {
|
|
|
190
190
|
this.#managers.clear();
|
|
191
191
|
await Promise.all(managers.map(async (pending) => (await pending).close()));
|
|
192
192
|
}
|
|
193
|
+
async searchSkills(params, query, minScore, limit) {
|
|
194
|
+
const { manager, error } = await this.getMemorySearchManager(params);
|
|
195
|
+
if (!manager)
|
|
196
|
+
throw new Error(error ?? "memory unavailable");
|
|
197
|
+
return manager.searchSkills(query, minScore, limit);
|
|
198
|
+
}
|
|
199
|
+
resolveSkillPath(params, path) {
|
|
200
|
+
const workspaceDir = resolveAgentWorkspaceDir(params.cfg, params.agentId);
|
|
201
|
+
const skillCorpora = this.#corpora.filter((corpus) => corpus.kind === "skills");
|
|
202
|
+
return resolveConfiguredSkillPath(workspaceDir, path, resolveSources(workspaceDir, skillCorpora));
|
|
203
|
+
}
|
|
193
204
|
async #createManager(cfg, agentId) {
|
|
194
205
|
const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
|
|
195
206
|
const stateDir = join(this.#stateRoot, "agents", agentId, "unblock-memory");
|
|
196
|
-
const fileCorpora = this.#corpora.filter((corpus) => corpus.kind === "files");
|
|
207
|
+
const fileCorpora = this.#corpora.filter((corpus) => corpus.kind === "files" || corpus.kind === "skills");
|
|
197
208
|
const sessionCorpus = this.#corpora.find((corpus) => corpus.kind === "sessions");
|
|
198
209
|
const sources = resolveSources(workspaceDir, fileCorpora);
|
|
199
210
|
const sessionSource = sessionCorpus
|
|
@@ -204,6 +215,7 @@ export class QmdMemoryRuntime {
|
|
|
204
215
|
const manager = new QmdMemoryManager({
|
|
205
216
|
workspaceDir,
|
|
206
217
|
dbPath: join(stateDir, "index.sqlite"),
|
|
218
|
+
curationPath: join(stateDir, "curation.sqlite"),
|
|
207
219
|
sources,
|
|
208
220
|
keepModelsWarm: this.#keepEmbeddingModelWarm,
|
|
209
221
|
analysisExecutable: this.#analysisExecutable,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { OpenClawConfig, OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
|
|
2
|
+
import type { UnblockMemoryConfig } from "./config.js";
|
|
3
|
+
import type { SkillSearchCandidate } from "./manager.js";
|
|
4
|
+
type SkillWhispererRuntime = {
|
|
5
|
+
searchSkills(params: {
|
|
6
|
+
cfg: OpenClawConfig;
|
|
7
|
+
agentId: string;
|
|
8
|
+
}, query: string, minScore: number, limit: number): Promise<SkillSearchCandidate[]>;
|
|
9
|
+
resolveSkillPath(params: {
|
|
10
|
+
cfg: OpenClawConfig;
|
|
11
|
+
agentId: string;
|
|
12
|
+
}, path: string): string | undefined;
|
|
13
|
+
};
|
|
14
|
+
export declare function buildSkillWhispererQuery(prompt: string, messages: readonly unknown[], historyMessages: number): string;
|
|
15
|
+
export declare function registerSkillWhisperer(api: OpenClawPluginApi, runtime: SkillWhispererRuntime, config: UnblockMemoryConfig["skillWhisperer"]): void;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
const CANDIDATE_LIMIT = 10;
|
|
3
|
+
const MAX_QUERY_CHARS = 12_000;
|
|
4
|
+
function isRecord(value) {
|
|
5
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
function messageText(message) {
|
|
8
|
+
if (!isRecord(message) || (message.role !== "user" && message.role !== "assistant"))
|
|
9
|
+
return undefined;
|
|
10
|
+
if (typeof message.content === "string") {
|
|
11
|
+
const text = message.content.trim();
|
|
12
|
+
return text ? { role: message.role, text } : undefined;
|
|
13
|
+
}
|
|
14
|
+
if (!Array.isArray(message.content))
|
|
15
|
+
return undefined;
|
|
16
|
+
const text = message.content.flatMap((part) => {
|
|
17
|
+
return isRecord(part) && part.type === "text" && typeof part.text === "string" ? [part.text] : [];
|
|
18
|
+
}).join("\n").trim();
|
|
19
|
+
return text ? { role: message.role, text } : undefined;
|
|
20
|
+
}
|
|
21
|
+
export function buildSkillWhispererQuery(prompt, messages, historyMessages) {
|
|
22
|
+
const availableHistory = messages.flatMap((message) => {
|
|
23
|
+
const parsed = messageText(message);
|
|
24
|
+
return parsed ? [`${parsed.role}: ${parsed.text}`] : [];
|
|
25
|
+
});
|
|
26
|
+
const history = historyMessages === 0 ? [] : availableHistory.slice(-historyMessages);
|
|
27
|
+
return [...history, `user: ${prompt.trim()}`].join("\n\n").slice(-MAX_QUERY_CHARS);
|
|
28
|
+
}
|
|
29
|
+
function readPath(params) {
|
|
30
|
+
for (const value of [params.path, params.file_path, params.filePath]) {
|
|
31
|
+
if (typeof value === "string" && basename(value).toLowerCase() === "skill.md")
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
function sessionScope(context) {
|
|
37
|
+
return context.sessionId || context.sessionKey;
|
|
38
|
+
}
|
|
39
|
+
export function registerSkillWhisperer(api, runtime, config) {
|
|
40
|
+
if (!config.enabled)
|
|
41
|
+
return;
|
|
42
|
+
const sessions = new Map();
|
|
43
|
+
const stateFor = (scope) => {
|
|
44
|
+
let state = sessions.get(scope);
|
|
45
|
+
if (!state) {
|
|
46
|
+
state = { turn: 0, skills: new Map() };
|
|
47
|
+
sessions.set(scope, state);
|
|
48
|
+
}
|
|
49
|
+
return state;
|
|
50
|
+
};
|
|
51
|
+
const active = (agentId) => ({ cfg: api.config, agentId });
|
|
52
|
+
api.on("before_prompt_build", async (event, context) => {
|
|
53
|
+
const scope = sessionScope(context);
|
|
54
|
+
if (context.trigger !== "user" || !scope || !context.runId || !context.agentId)
|
|
55
|
+
return;
|
|
56
|
+
const state = stateFor(scope);
|
|
57
|
+
if (state.lastRunId === context.runId)
|
|
58
|
+
return;
|
|
59
|
+
state.lastRunId = context.runId;
|
|
60
|
+
state.turn += 1;
|
|
61
|
+
try {
|
|
62
|
+
const candidates = await runtime.searchSkills(active(context.agentId), buildSkillWhispererQuery(event.prompt, event.messages, config.historyMessages), config.minScore, CANDIDATE_LIMIT);
|
|
63
|
+
const selected = candidates.find((candidate) => {
|
|
64
|
+
if (candidate.score < config.minScore)
|
|
65
|
+
return false;
|
|
66
|
+
const history = state.skills.get(candidate.path);
|
|
67
|
+
const lastSeen = Math.max(history?.suggested ?? -Infinity, history?.opened ?? -Infinity);
|
|
68
|
+
return state.turn - lastSeen > config.cooldownTurns;
|
|
69
|
+
});
|
|
70
|
+
if (!selected)
|
|
71
|
+
return;
|
|
72
|
+
const history = state.skills.get(selected.path) ?? {};
|
|
73
|
+
history.suggested = state.turn;
|
|
74
|
+
state.skills.set(selected.path, history);
|
|
75
|
+
return {
|
|
76
|
+
prependContext: `A potentially relevant skill is available: ${JSON.stringify(selected.name)} ` +
|
|
77
|
+
`at ${JSON.stringify(selected.path)}. Check it before proceeding if applicable.`,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
api.logger.warn(`unblock-memory skill whisperer search failed: ${String(error)}`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
api.on("after_tool_call", (event, context) => {
|
|
86
|
+
if (event.toolName !== "read" || event.error ||
|
|
87
|
+
(isRecord(event.result) && event.result.isError === true) || !context.agentId)
|
|
88
|
+
return;
|
|
89
|
+
const scope = sessionScope(context);
|
|
90
|
+
const path = scope ? readPath(event.params) : undefined;
|
|
91
|
+
if (!scope || !path)
|
|
92
|
+
return;
|
|
93
|
+
try {
|
|
94
|
+
const canonicalPath = runtime.resolveSkillPath(active(context.agentId), path);
|
|
95
|
+
if (!canonicalPath)
|
|
96
|
+
return;
|
|
97
|
+
const state = stateFor(scope);
|
|
98
|
+
const history = state.skills.get(canonicalPath) ?? {};
|
|
99
|
+
history.opened = state.turn;
|
|
100
|
+
state.skills.set(canonicalPath, history);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
api.logger.warn(`unblock-memory skill whisperer read tracking failed: ${String(error)}`);
|
|
104
|
+
}
|
|
105
|
+
}, { matcher: ["read"] });
|
|
106
|
+
api.on("session_end", (event, context) => {
|
|
107
|
+
sessions.delete(event.sessionId);
|
|
108
|
+
if (event.sessionKey)
|
|
109
|
+
sessions.delete(event.sessionKey);
|
|
110
|
+
if (context.sessionKey)
|
|
111
|
+
sessions.delete(context.sessionKey);
|
|
112
|
+
});
|
|
113
|
+
}
|
package/dist/src/sources.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type { ChatType, FileCorpusConfig } from "./config.js";
|
|
1
|
+
import type { ChatType, FileCorpusConfig, SkillCorpusConfig } from "./config.js";
|
|
2
2
|
export type ResolvedSource = {
|
|
3
3
|
collection: string;
|
|
4
4
|
corpus: string;
|
|
5
5
|
configuredPath: string;
|
|
6
|
-
kind: "files" | "sessions";
|
|
6
|
+
kind: "files" | "skills" | "sessions";
|
|
7
7
|
root: string;
|
|
8
8
|
pattern: string;
|
|
9
9
|
watchPath: string;
|
|
@@ -11,7 +11,8 @@ export type ResolvedSource = {
|
|
|
11
11
|
};
|
|
12
12
|
export declare function resolveSource(workspaceDir: string, configuredPath: string, corpus?: string): ResolvedSource;
|
|
13
13
|
export declare function resolveSessionSource(sessionsDir: string, chatTypes: readonly ChatType[]): ResolvedSource;
|
|
14
|
-
export declare function resolveSources(workspaceDir: string, corpora: readonly FileCorpusConfig[]): ResolvedSource[];
|
|
14
|
+
export declare function resolveSources(workspaceDir: string, corpora: readonly (FileCorpusConfig | SkillCorpusConfig)[]): ResolvedSource[];
|
|
15
|
+
export declare function resolveConfiguredSkillPath(workspaceDir: string, inputPath: string, sources: readonly ResolvedSource[]): string | undefined;
|
|
15
16
|
export declare function parseSafeVirtualPath(virtualPath: string, sources: ReadonlyMap<string, ResolvedSource>): {
|
|
16
17
|
source: ResolvedSource;
|
|
17
18
|
relativePath: string;
|
package/dist/src/sources.js
CHANGED
|
@@ -68,6 +68,9 @@ export function resolveSource(workspaceDir, configuredPath, corpus = "memory") {
|
|
|
68
68
|
assertWorkspaceSourceHasNoSymlinkRoot(workspaceDir, configuredPath, root);
|
|
69
69
|
return { collection: collectionName(absolute), corpus, configuredPath, kind: "files", root, pattern, watchPath: root };
|
|
70
70
|
}
|
|
71
|
+
function resolveFileSource(workspaceDir, configuredPath, corpus) {
|
|
72
|
+
return { ...resolveSource(workspaceDir, configuredPath, corpus.name), kind: corpus.kind };
|
|
73
|
+
}
|
|
71
74
|
export function resolveSessionSource(sessionsDir, chatTypes) {
|
|
72
75
|
return {
|
|
73
76
|
...resolveSource(sessionsDir, sessionsDir, "sessions"),
|
|
@@ -81,7 +84,7 @@ export function resolveSources(workspaceDir, corpora) {
|
|
|
81
84
|
const configured = new Map();
|
|
82
85
|
for (const corpus of corpora) {
|
|
83
86
|
for (const path of corpus.paths) {
|
|
84
|
-
const source =
|
|
87
|
+
const source = resolveFileSource(workspaceDir, path, corpus);
|
|
85
88
|
const identity = `${source.root}\0${source.pattern}`;
|
|
86
89
|
const duplicate = configured.get(identity);
|
|
87
90
|
if (duplicate) {
|
|
@@ -94,6 +97,36 @@ export function resolveSources(workspaceDir, corpora) {
|
|
|
94
97
|
}
|
|
95
98
|
return sources;
|
|
96
99
|
}
|
|
100
|
+
export function resolveConfiguredSkillPath(workspaceDir, inputPath, sources) {
|
|
101
|
+
if (basename(inputPath).toLowerCase() !== "skill.md")
|
|
102
|
+
return undefined;
|
|
103
|
+
const target = resolve(isAbsolute(expandHome(inputPath))
|
|
104
|
+
? expandHome(inputPath)
|
|
105
|
+
: resolve(workspaceDir, inputPath));
|
|
106
|
+
let canonicalTarget;
|
|
107
|
+
try {
|
|
108
|
+
canonicalTarget = realpathSync(target);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
for (const source of sources) {
|
|
114
|
+
if (source.kind !== "skills")
|
|
115
|
+
continue;
|
|
116
|
+
let canonicalRoot;
|
|
117
|
+
try {
|
|
118
|
+
canonicalRoot = realpathSync(source.root);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const relativePath = relative(canonicalRoot, canonicalTarget);
|
|
124
|
+
const safe = parseSafeVirtualPath(`qmd://${source.collection}/${relativePath.split(sep).join("/")}`, new Map([[source.collection, source]]));
|
|
125
|
+
if (safe)
|
|
126
|
+
return canonicalTarget;
|
|
127
|
+
}
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
97
130
|
export function parseSafeVirtualPath(virtualPath, sources) {
|
|
98
131
|
const match = /^qmd:\/\/([^/]+)\/(.+)$/.exec(virtualPath.trim());
|
|
99
132
|
if (!match)
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "unblock-memory",
|
|
3
3
|
"name": "Unblock Memory",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"description": "Indexes, retrieves, and analyzes configured workspace memory with existing QMD vectors.",
|
|
6
6
|
"kind": "memory",
|
|
7
7
|
"activation": { "onStartup": false },
|
|
8
8
|
"skills": ["./skills"],
|
|
9
|
-
"contracts": { "tools": ["memory_search", "memory_get", "memory_sync_sessions", "memory_sync_status", "memory_recluster", "memory_list_clusters", "memory_fetch_cluster"] },
|
|
9
|
+
"contracts": { "tools": ["memory_search", "memory_get", "memory_sync_sessions", "memory_sync_status", "memory_recluster", "memory_list_clusters", "memory_fetch_cluster", "memory_list_maintenance_tasks", "memory_update_maintenance_task"] },
|
|
10
10
|
"toolMetadata": {
|
|
11
11
|
"memory_sync_sessions": { "sideEffecting": true },
|
|
12
12
|
"memory_sync_status": { "replaySafe": true },
|
|
13
13
|
"memory_recluster": { "sideEffecting": true },
|
|
14
14
|
"memory_list_clusters": { "replaySafe": true },
|
|
15
|
-
"memory_fetch_cluster": { "replaySafe": true }
|
|
15
|
+
"memory_fetch_cluster": { "replaySafe": true },
|
|
16
|
+
"memory_list_maintenance_tasks": { "replaySafe": true },
|
|
17
|
+
"memory_update_maintenance_task": { "sideEffecting": true }
|
|
16
18
|
},
|
|
17
19
|
"uiHints": {
|
|
18
20
|
"keepEmbeddingModelWarm": {
|
|
@@ -21,7 +23,11 @@
|
|
|
21
23
|
},
|
|
22
24
|
"corpora": {
|
|
23
25
|
"label": "Memory Corpora",
|
|
24
|
-
"help": "Named groups of exact Markdown files, directories, or globs. Relative paths resolve from each agent workspace."
|
|
26
|
+
"help": "Named groups of exact Markdown files, directories, or globs. Relative paths resolve from each agent workspace. Skill files use the isolated skills corpus."
|
|
27
|
+
},
|
|
28
|
+
"skillWhisperer.enabled": {
|
|
29
|
+
"label": "Skill Whisperer",
|
|
30
|
+
"help": "Suggest at most one semantically relevant configured skill before a user turn. Requires hook conversation access."
|
|
25
31
|
},
|
|
26
32
|
"analysis.executable": {
|
|
27
33
|
"label": "Memory Analysis Worker",
|
|
@@ -69,6 +75,20 @@
|
|
|
69
75
|
"default": ["channel", "group"]
|
|
70
76
|
}
|
|
71
77
|
}
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"type": "object",
|
|
81
|
+
"additionalProperties": false,
|
|
82
|
+
"required": ["name", "kind", "paths"],
|
|
83
|
+
"properties": {
|
|
84
|
+
"name": { "const": "skills" },
|
|
85
|
+
"kind": { "const": "skills" },
|
|
86
|
+
"paths": {
|
|
87
|
+
"type": "array",
|
|
88
|
+
"minItems": 1,
|
|
89
|
+
"items": { "type": "string", "pattern": "\\S" }
|
|
90
|
+
}
|
|
91
|
+
}
|
|
72
92
|
}
|
|
73
93
|
]
|
|
74
94
|
},
|
|
@@ -86,6 +106,22 @@
|
|
|
86
106
|
"properties": {
|
|
87
107
|
"executable": { "type": "string", "minLength": 1 }
|
|
88
108
|
}
|
|
109
|
+
},
|
|
110
|
+
"skillWhisperer": {
|
|
111
|
+
"type": "object",
|
|
112
|
+
"additionalProperties": false,
|
|
113
|
+
"properties": {
|
|
114
|
+
"enabled": { "type": "boolean", "default": false },
|
|
115
|
+
"historyMessages": { "type": "integer", "minimum": 0, "default": 5 },
|
|
116
|
+
"minScore": { "type": "number", "minimum": 0, "maximum": 1, "default": 0.4 },
|
|
117
|
+
"cooldownTurns": { "type": "integer", "minimum": 0, "default": 10 }
|
|
118
|
+
},
|
|
119
|
+
"default": {
|
|
120
|
+
"enabled": false,
|
|
121
|
+
"historyMessages": 5,
|
|
122
|
+
"minScore": 0.4,
|
|
123
|
+
"cooldownTurns": 10
|
|
124
|
+
}
|
|
89
125
|
}
|
|
90
126
|
}
|
|
91
127
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unblocklabs/unblock-memory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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": "https://github.com/unblocklabs-ai/qmd/releases/download/v2.9.
|
|
33
|
+
"@unblocklabs/qmd": "https://github.com/unblocklabs-ai/qmd/releases/download/v2.9.3/unblocklabs-qmd-2.9.3.tgz",
|
|
34
34
|
"chokidar": "5.0.0",
|
|
35
35
|
"picomatch": "^4.0.5",
|
|
36
36
|
"typebox": "1.3.6"
|
|
@@ -16,7 +16,9 @@ weak, duplicative, or easily looked-up knowledge.
|
|
|
16
16
|
`memory_recluster`, then list again.
|
|
17
17
|
2. Fetch a useful cluster with `memory_fetch_cluster`. Start with
|
|
18
18
|
`sort: "representative"`; use `score_desc`, `date_asc`, or `date_desc` and
|
|
19
|
-
pagination when relevance, evolution, or recent state matters.
|
|
19
|
+
pagination when relevance, evolution, or recent state matters. Treat
|
|
20
|
+
`eventTime` as event chronology; `sourceModifiedAt` is only a labeled
|
|
21
|
+
fallback when `eventTime` is unresolved.
|
|
20
22
|
3. State the question the cluster raises: what may be repeated, contradictory,
|
|
21
23
|
changing, or worth understanding?
|
|
22
24
|
4. Search existing knowledge with `memory_search`, using
|
|
@@ -67,6 +69,15 @@ rigid document template.
|
|
|
67
69
|
## Finish the cycle
|
|
68
70
|
|
|
69
71
|
- Do not rewrite raw memory or session projections.
|
|
72
|
+
- Review a small page from `memory_list_maintenance_tasks`. For ambiguous dates,
|
|
73
|
+
investigate supporting evidence and use `memory_update_maintenance_task` to
|
|
74
|
+
attach a chunk or document date only when supported; otherwise defer or mark
|
|
75
|
+
it irrelevant. For exact-duplicate proposals, decide whether cleanup should
|
|
76
|
+
be proposed, but do not treat repetition across historical files as an error.
|
|
77
|
+
Mark intentional repetition `irrelevant`, and keep accidental duplication
|
|
78
|
+
`deferred` until the source is actually cleaned. Mark it `resolved` only after
|
|
79
|
+
that cleanup is complete. The maintenance tools never change source Markdown,
|
|
80
|
+
and generated session projections must never be manually cleaned.
|
|
70
81
|
- Verify an updated file with `memory_search`, using
|
|
71
82
|
`corpora: ["knowledge"]`, and check all-corpora ranking when useful.
|
|
72
83
|
- Report the questions investigated, evidence consulted beyond each cluster,
|