@unblocklabs/unblock-memory 0.3.2 → 0.3.4
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 +61 -1
- package/dist/src/analysis.js +21 -11
- package/dist/src/config.d.ts +15 -1
- package/dist/src/config.js +97 -13
- package/dist/src/contracts.d.ts +3 -0
- package/dist/src/manager.d.ts +2 -1
- package/dist/src/manager.js +47 -40
- package/dist/src/people-cli.d.ts +4 -0
- package/dist/src/people-cli.js +47 -0
- package/dist/src/people-evidence.d.ts +14 -0
- package/dist/src/people-evidence.js +93 -0
- package/dist/src/people-hooks.d.ts +5 -0
- package/dist/src/people-hooks.js +101 -0
- package/dist/src/people-refinement.d.ts +72 -0
- package/dist/src/people-refinement.js +254 -0
- package/dist/src/people-store.d.ts +134 -0
- package/dist/src/people-store.js +679 -0
- package/dist/src/people-tools.d.ts +5 -0
- package/dist/src/people-tools.js +190 -0
- package/dist/src/plugin.js +59 -16
- package/dist/src/skill-whisperer.js +11 -6
- package/dist/src/slack-directory.d.ts +37 -0
- package/dist/src/slack-directory.js +102 -0
- package/dist/src/sources.d.ts +1 -0
- package/dist/src/sources.js +7 -0
- package/openclaw.plugin.json +74 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@ directories, or globs into named corpora:
|
|
|
39
39
|
entries: {
|
|
40
40
|
"unblock-memory": {
|
|
41
41
|
hooks: {
|
|
42
|
-
// Required
|
|
42
|
+
// Required when either whisperer is enabled.
|
|
43
43
|
allowConversationAccess: true,
|
|
44
44
|
},
|
|
45
45
|
config: {
|
|
@@ -79,6 +79,10 @@ directories, or globs into named corpora:
|
|
|
79
79
|
minScore: 0.5,
|
|
80
80
|
cooldownTurns: 10,
|
|
81
81
|
},
|
|
82
|
+
people: {
|
|
83
|
+
enabled: false,
|
|
84
|
+
whisperer: { enabled: false, maxChars: 1200 },
|
|
85
|
+
},
|
|
82
86
|
// Optional: omit unless the local analysis worker is installed.
|
|
83
87
|
analysis: {
|
|
84
88
|
executable: "/absolute/path/to/unblock-cluster/bin/unblock-memory-analysis",
|
|
@@ -131,6 +135,62 @@ OpenClaw's effective skill inventory from `openclaw.json`. Configured skill
|
|
|
131
135
|
globs follow symlinked directories, including OpenClaw's `plugin-skills`
|
|
132
136
|
directory.
|
|
133
137
|
|
|
138
|
+
### People Whisperer
|
|
139
|
+
|
|
140
|
+
PeopleSQL is an optional agent-local people store. When `people.enabled` is
|
|
141
|
+
true, incoming Slack messages with a canonical agent session key and exact
|
|
142
|
+
account and sender IDs create or refresh a disabled-by-default person record.
|
|
143
|
+
Incomplete Slack identities create a bounded, deduplicated todo without storing
|
|
144
|
+
message content. Other channels are ignored.
|
|
145
|
+
|
|
146
|
+
PeopleSQL registers three optional tools when enabled:
|
|
147
|
+
|
|
148
|
+
- `memory_people_inspect` lets an owner read one exact person or bounded actionable todos;
|
|
149
|
+
- `memory_people_update` changes explicit policy, company, todo, deletion, or
|
|
150
|
+
restoration state; and
|
|
151
|
+
- `memory_people_sync` manually enriches one Slack account through OpenClaw's
|
|
152
|
+
authenticated directory CLI without accepting or reading a token.
|
|
153
|
+
|
|
154
|
+
Allow the tools you intend the agent to use through OpenClaw's `tools.allow`
|
|
155
|
+
configuration; enabling PeopleSQL alone does not expose optional tools.
|
|
156
|
+
|
|
157
|
+
Inspection, administrative updates, and directory sync require OpenClaw's
|
|
158
|
+
host-derived owner authorization. Dossier replacement runs only through the plugin-owned
|
|
159
|
+
Codex refinement command. Soft-deleted people can be restored explicitly; restoration
|
|
160
|
+
leaves both policies disabled. The sync is bounded to
|
|
161
|
+
200 normalized directory entries per call and is safe to rerun. With pinned
|
|
162
|
+
OpenClaw `2026.8.1-beta.3`, the directory contract supplies ID, name, and handle;
|
|
163
|
+
Unblock Memory ignores raw provider payloads. Slack requires the
|
|
164
|
+
`users:read` scope.
|
|
165
|
+
|
|
166
|
+
For weekly dossier maintenance, schedule the plugin-owned CLI with an
|
|
167
|
+
operator-authored OpenClaw command automation rather than adding a plugin
|
|
168
|
+
scheduler:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
openclaw automations create "0 4 * * 0" \
|
|
172
|
+
--name "People Whisperer refinement" \
|
|
173
|
+
--command-argv '["openclaw","unblock-memory","people","refine","--agent","main"]' \
|
|
174
|
+
--timeout-seconds 1800 \
|
|
175
|
+
--no-deliver
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
`openclaw unblock-memory people refine --agent <id>` selects a bounded candidate
|
|
179
|
+
batch, reads exact-sender session evidence, and invokes one
|
|
180
|
+
ephemeral, read-only `codex exec` with structured output. It validates the
|
|
181
|
+
complete result set, person IDs, dossier schema, and evidence locators before
|
|
182
|
+
writing each dossier transactionally. Refinement uses exact-attributed session
|
|
183
|
+
evidence; additional evidence sources can be added when they are implemented.
|
|
184
|
+
The host running the command must already have working Codex CLI authentication.
|
|
185
|
+
The plugin performs no model call on the prompt-injection path.
|
|
186
|
+
|
|
187
|
+
Set both `people.whisperer.enabled` and the person's injection policy to enable
|
|
188
|
+
prompt context. The plugin then prepends only that exact person's stored dossier
|
|
189
|
+
blurb, bounded by `maxChars`, once per session. Unknown, unavailable, or
|
|
190
|
+
incomplete identities produce no context. This non-bundled prompt hook requires
|
|
191
|
+
`plugins.entries.unblock-memory.hooks.allowConversationAccess: true` and remains
|
|
192
|
+
subject to OpenClaw's `allowPromptInjection` policy.
|
|
193
|
+
|
|
134
194
|
Use `sessionFilter` to restrict session results by metadata while leaving file
|
|
135
195
|
corpora searchable. Supported fields are `startedFrom` and `startedTo`
|
|
136
196
|
(inclusive ISO 8601 timestamps), `provider`, `chatType`, `accountId`, and
|
package/dist/src/analysis.js
CHANGED
|
@@ -123,30 +123,40 @@ export function runAnalysisWorker(params) {
|
|
|
123
123
|
stderrBytes += Math.min(chunk.length, remaining);
|
|
124
124
|
});
|
|
125
125
|
let settled = false;
|
|
126
|
-
let
|
|
127
|
-
|
|
128
|
-
|
|
126
|
+
let aborted = false;
|
|
127
|
+
let abortReason;
|
|
128
|
+
let forceKill;
|
|
129
|
+
const cleanup = () => {
|
|
130
|
+
params.signal?.removeEventListener("abort", onAbort);
|
|
131
|
+
if (forceKill)
|
|
132
|
+
clearTimeout(forceKill);
|
|
133
|
+
};
|
|
134
|
+
const finish = (error, shouldReject = error !== undefined) => {
|
|
129
135
|
if (settled)
|
|
130
136
|
return;
|
|
131
137
|
settled = true;
|
|
132
138
|
cleanup();
|
|
133
|
-
|
|
139
|
+
shouldReject ? reject(error) : resolve();
|
|
134
140
|
};
|
|
135
141
|
const onAbort = () => {
|
|
136
|
-
if (
|
|
142
|
+
if (aborted)
|
|
137
143
|
return;
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
: new Error("Memory reclustering aborted");
|
|
144
|
+
aborted = true;
|
|
145
|
+
abortReason = params.signal?.reason;
|
|
141
146
|
child.kill("SIGTERM");
|
|
147
|
+
forceKill = setTimeout(() => {
|
|
148
|
+
if (!settled)
|
|
149
|
+
child.kill("SIGKILL");
|
|
150
|
+
}, 250);
|
|
151
|
+
forceKill.unref();
|
|
142
152
|
};
|
|
143
153
|
params.signal?.addEventListener("abort", onAbort, { once: true });
|
|
144
154
|
if (params.signal?.aborted)
|
|
145
155
|
onAbort();
|
|
146
|
-
child.on("error", (error) => finish(
|
|
156
|
+
child.on("error", (error) => finish(aborted ? abortReason : error, true));
|
|
147
157
|
child.on("close", (code, signal) => {
|
|
148
|
-
if (
|
|
149
|
-
finish(
|
|
158
|
+
if (aborted) {
|
|
159
|
+
finish(abortReason, true);
|
|
150
160
|
return;
|
|
151
161
|
}
|
|
152
162
|
if (code === 0) {
|
package/dist/src/config.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export type SkillCorpusConfig = {
|
|
|
9
9
|
paths: readonly string[];
|
|
10
10
|
};
|
|
11
11
|
declare const CHAT_TYPES: readonly ["channel", "group", "direct"];
|
|
12
|
-
export type ChatType = typeof CHAT_TYPES[number];
|
|
12
|
+
export type ChatType = (typeof CHAT_TYPES)[number];
|
|
13
13
|
type SessionCorpusConfig = {
|
|
14
14
|
name: "sessions";
|
|
15
15
|
kind: "sessions";
|
|
@@ -23,6 +23,19 @@ export type UnblockMemoryConfig = {
|
|
|
23
23
|
analysis: {
|
|
24
24
|
executable?: string;
|
|
25
25
|
};
|
|
26
|
+
people: {
|
|
27
|
+
enabled: boolean;
|
|
28
|
+
refinement: {
|
|
29
|
+
maxPeoplePerRun: number;
|
|
30
|
+
};
|
|
31
|
+
whisperer: {
|
|
32
|
+
enabled: boolean;
|
|
33
|
+
maxChars: number;
|
|
34
|
+
};
|
|
35
|
+
todos: {
|
|
36
|
+
maxOpen: number;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
26
39
|
skillWhisperer: {
|
|
27
40
|
enabled: boolean;
|
|
28
41
|
historyMessages: number;
|
|
@@ -30,5 +43,6 @@ export type UnblockMemoryConfig = {
|
|
|
30
43
|
cooldownTurns: number;
|
|
31
44
|
};
|
|
32
45
|
};
|
|
46
|
+
export declare const DEFAULT_PEOPLE_CONFIG: UnblockMemoryConfig["people"];
|
|
33
47
|
export declare function resolveConfig(value: unknown): UnblockMemoryConfig;
|
|
34
48
|
export {};
|
package/dist/src/config.js
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import { isAbsolute } from "node:path";
|
|
2
2
|
const DEFAULT_PATHS = ["MEMORY.md", "USER.md", "memory/**/*.md"];
|
|
3
3
|
const CHAT_TYPES = ["channel", "group", "direct"];
|
|
4
|
-
export const DEFAULT_CORPORA = [
|
|
4
|
+
export const DEFAULT_CORPORA = [
|
|
5
|
+
{
|
|
5
6
|
name: "memory",
|
|
6
7
|
kind: "files",
|
|
7
8
|
paths: DEFAULT_PATHS,
|
|
8
|
-
}
|
|
9
|
+
},
|
|
10
|
+
];
|
|
11
|
+
export const DEFAULT_PEOPLE_CONFIG = {
|
|
12
|
+
enabled: false,
|
|
13
|
+
refinement: { maxPeoplePerRun: 10 },
|
|
14
|
+
whisperer: { enabled: false, maxChars: 1200 },
|
|
15
|
+
todos: { maxOpen: 1000 },
|
|
16
|
+
};
|
|
9
17
|
const DEFAULT_SKILL_WHISPERER = {
|
|
10
18
|
enabled: false,
|
|
11
19
|
historyMessages: 5,
|
|
@@ -44,7 +52,8 @@ function resolveCorpora(value) {
|
|
|
44
52
|
if (name !== "skills") {
|
|
45
53
|
throw new Error('unblock-memory skills corpus must be named "skills"');
|
|
46
54
|
}
|
|
47
|
-
if (!Array.isArray(corpus.paths) ||
|
|
55
|
+
if (!Array.isArray(corpus.paths) ||
|
|
56
|
+
corpus.paths.length === 0 ||
|
|
48
57
|
!corpus.paths.every((path) => typeof path === "string" && path.trim())) {
|
|
49
58
|
throw new Error("unblock-memory corpus skills paths must be a non-empty array of non-empty strings");
|
|
50
59
|
}
|
|
@@ -56,11 +65,16 @@ function resolveCorpora(value) {
|
|
|
56
65
|
throw new Error('unblock-memory session corpus must be named "sessions"');
|
|
57
66
|
}
|
|
58
67
|
const chatTypes = corpus.chatTypes ?? ["channel", "group"];
|
|
59
|
-
if (!Array.isArray(chatTypes) ||
|
|
68
|
+
if (!Array.isArray(chatTypes) ||
|
|
69
|
+
chatTypes.length === 0 ||
|
|
60
70
|
!chatTypes.every((chatType) => CHAT_TYPES.includes(chatType))) {
|
|
61
71
|
throw new Error(`unblock-memory corpus sessions chatTypes must contain channel, group, or direct`);
|
|
62
72
|
}
|
|
63
|
-
return {
|
|
73
|
+
return {
|
|
74
|
+
name: "sessions",
|
|
75
|
+
kind: "sessions",
|
|
76
|
+
chatTypes: [...new Set(chatTypes)],
|
|
77
|
+
};
|
|
64
78
|
}
|
|
65
79
|
assertOnlyKeys(corpus, ["name", "kind", "paths"], `corpora[${index}]`);
|
|
66
80
|
if (name === "sessions") {
|
|
@@ -72,7 +86,8 @@ function resolveCorpora(value) {
|
|
|
72
86
|
if (corpus.kind !== "files") {
|
|
73
87
|
throw new Error(`unblock-memory corpus ${name} must have kind "files", "skills", or "sessions"`);
|
|
74
88
|
}
|
|
75
|
-
if (!Array.isArray(corpus.paths) ||
|
|
89
|
+
if (!Array.isArray(corpus.paths) ||
|
|
90
|
+
corpus.paths.length === 0 ||
|
|
76
91
|
!corpus.paths.every((path) => typeof path === "string" && path.trim())) {
|
|
77
92
|
throw new Error(`unblock-memory corpus ${name} paths must be a non-empty array of non-empty strings`);
|
|
78
93
|
}
|
|
@@ -83,12 +98,70 @@ function resolveCorpora(value) {
|
|
|
83
98
|
}
|
|
84
99
|
return corpora;
|
|
85
100
|
}
|
|
101
|
+
function positiveInteger(value, fallback, label, maximum) {
|
|
102
|
+
const resolved = value ?? fallback;
|
|
103
|
+
if (typeof resolved !== "number" ||
|
|
104
|
+
!Number.isInteger(resolved) ||
|
|
105
|
+
resolved < 1 ||
|
|
106
|
+
resolved > maximum) {
|
|
107
|
+
throw new Error(`unblock-memory ${label} must be a positive integer no greater than ${maximum}`);
|
|
108
|
+
}
|
|
109
|
+
return resolved;
|
|
110
|
+
}
|
|
111
|
+
function resolvePeople(value) {
|
|
112
|
+
if (value === undefined)
|
|
113
|
+
return DEFAULT_PEOPLE_CONFIG;
|
|
114
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
115
|
+
throw new Error("unblock-memory people must be an object");
|
|
116
|
+
}
|
|
117
|
+
const people = value;
|
|
118
|
+
assertOnlyKeys(people, ["enabled", "refinement", "whisperer", "todos"], "people");
|
|
119
|
+
const enabled = people.enabled ?? false;
|
|
120
|
+
if (typeof enabled !== "boolean")
|
|
121
|
+
throw new Error("unblock-memory people.enabled must be a boolean");
|
|
122
|
+
const refinement = people.refinement ?? {};
|
|
123
|
+
if (!refinement || typeof refinement !== "object" || Array.isArray(refinement)) {
|
|
124
|
+
throw new Error("unblock-memory people.refinement must be an object");
|
|
125
|
+
}
|
|
126
|
+
const refinementRecord = refinement;
|
|
127
|
+
assertOnlyKeys(refinementRecord, ["maxPeoplePerRun"], "people.refinement");
|
|
128
|
+
const whisperer = people.whisperer ?? {};
|
|
129
|
+
if (!whisperer || typeof whisperer !== "object" || Array.isArray(whisperer)) {
|
|
130
|
+
throw new Error("unblock-memory people.whisperer must be an object");
|
|
131
|
+
}
|
|
132
|
+
const whispererRecord = whisperer;
|
|
133
|
+
assertOnlyKeys(whispererRecord, ["enabled", "maxChars"], "people.whisperer");
|
|
134
|
+
const whispererEnabled = whispererRecord.enabled ?? false;
|
|
135
|
+
if (typeof whispererEnabled !== "boolean") {
|
|
136
|
+
throw new Error("unblock-memory people.whisperer.enabled must be a boolean");
|
|
137
|
+
}
|
|
138
|
+
const todos = people.todos ?? {};
|
|
139
|
+
if (!todos || typeof todos !== "object" || Array.isArray(todos)) {
|
|
140
|
+
throw new Error("unblock-memory people.todos must be an object");
|
|
141
|
+
}
|
|
142
|
+
const todosRecord = todos;
|
|
143
|
+
assertOnlyKeys(todosRecord, ["maxOpen"], "people.todos");
|
|
144
|
+
return {
|
|
145
|
+
enabled,
|
|
146
|
+
refinement: {
|
|
147
|
+
maxPeoplePerRun: positiveInteger(refinementRecord.maxPeoplePerRun, DEFAULT_PEOPLE_CONFIG.refinement.maxPeoplePerRun, "people.refinement.maxPeoplePerRun", 50),
|
|
148
|
+
},
|
|
149
|
+
whisperer: {
|
|
150
|
+
enabled: whispererEnabled,
|
|
151
|
+
maxChars: positiveInteger(whispererRecord.maxChars, DEFAULT_PEOPLE_CONFIG.whisperer.maxChars, "people.whisperer.maxChars", 4000),
|
|
152
|
+
},
|
|
153
|
+
todos: {
|
|
154
|
+
maxOpen: positiveInteger(todosRecord.maxOpen, DEFAULT_PEOPLE_CONFIG.todos.maxOpen, "people.todos.maxOpen", 10_000),
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
86
158
|
export function resolveConfig(value) {
|
|
87
159
|
if (value === undefined || value === null) {
|
|
88
160
|
return {
|
|
89
161
|
corpora: DEFAULT_CORPORA,
|
|
90
162
|
keepEmbeddingModelWarm: true,
|
|
91
163
|
analysis: {},
|
|
164
|
+
people: DEFAULT_PEOPLE_CONFIG,
|
|
92
165
|
skillWhisperer: DEFAULT_SKILL_WHISPERER,
|
|
93
166
|
};
|
|
94
167
|
}
|
|
@@ -96,9 +169,11 @@ export function resolveConfig(value) {
|
|
|
96
169
|
throw new Error("unblock-memory config must be an object");
|
|
97
170
|
}
|
|
98
171
|
const config = value;
|
|
99
|
-
assertOnlyKeys(config, ["corpora", "keepEmbeddingModelWarm", "analysis", "skillWhisperer"], "config");
|
|
172
|
+
assertOnlyKeys(config, ["corpora", "keepEmbeddingModelWarm", "analysis", "people", "skillWhisperer"], "config");
|
|
100
173
|
const corpora = resolveCorpora(config.corpora);
|
|
101
|
-
|
|
174
|
+
const people = resolvePeople(config.people);
|
|
175
|
+
if (config.keepEmbeddingModelWarm !== undefined &&
|
|
176
|
+
typeof config.keepEmbeddingModelWarm !== "boolean") {
|
|
102
177
|
throw new Error("unblock-memory keepEmbeddingModelWarm must be a boolean");
|
|
103
178
|
}
|
|
104
179
|
const keepEmbeddingModelWarm = config.keepEmbeddingModelWarm ?? true;
|
|
@@ -119,7 +194,9 @@ export function resolveConfig(value) {
|
|
|
119
194
|
}
|
|
120
195
|
let skillWhisperer = DEFAULT_SKILL_WHISPERER;
|
|
121
196
|
if (config.skillWhisperer !== undefined) {
|
|
122
|
-
if (!config.skillWhisperer ||
|
|
197
|
+
if (!config.skillWhisperer ||
|
|
198
|
+
typeof config.skillWhisperer !== "object" ||
|
|
199
|
+
Array.isArray(config.skillWhisperer)) {
|
|
123
200
|
throw new Error("unblock-memory skillWhisperer must be an object");
|
|
124
201
|
}
|
|
125
202
|
const value = config.skillWhisperer;
|
|
@@ -130,13 +207,20 @@ export function resolveConfig(value) {
|
|
|
130
207
|
const cooldownTurns = value.cooldownTurns ?? 10;
|
|
131
208
|
if (typeof enabled !== "boolean")
|
|
132
209
|
throw new Error("unblock-memory skillWhisperer.enabled must be a boolean");
|
|
133
|
-
if (typeof historyMessages !== "number" ||
|
|
210
|
+
if (typeof historyMessages !== "number" ||
|
|
211
|
+
!Number.isInteger(historyMessages) ||
|
|
212
|
+
historyMessages < 0) {
|
|
134
213
|
throw new Error("unblock-memory skillWhisperer.historyMessages must be a non-negative integer");
|
|
135
214
|
}
|
|
136
|
-
if (typeof minScore !== "number" ||
|
|
215
|
+
if (typeof minScore !== "number" ||
|
|
216
|
+
!Number.isFinite(minScore) ||
|
|
217
|
+
minScore < 0 ||
|
|
218
|
+
minScore > 1) {
|
|
137
219
|
throw new Error("unblock-memory skillWhisperer.minScore must be between 0 and 1");
|
|
138
220
|
}
|
|
139
|
-
if (typeof cooldownTurns !== "number" ||
|
|
221
|
+
if (typeof cooldownTurns !== "number" ||
|
|
222
|
+
!Number.isInteger(cooldownTurns) ||
|
|
223
|
+
cooldownTurns < 0) {
|
|
140
224
|
throw new Error("unblock-memory skillWhisperer.cooldownTurns must be a non-negative integer");
|
|
141
225
|
}
|
|
142
226
|
skillWhisperer = { enabled, historyMessages, minScore, cooldownTurns };
|
|
@@ -144,5 +228,5 @@ export function resolveConfig(value) {
|
|
|
144
228
|
if (skillWhisperer.enabled && !corpora.some((corpus) => corpus.kind === "skills")) {
|
|
145
229
|
throw new Error('unblock-memory enabled skillWhisperer requires a corpus named "skills" with kind "skills"');
|
|
146
230
|
}
|
|
147
|
-
return { corpora, keepEmbeddingModelWarm, analysis: analysisConfig, skillWhisperer };
|
|
231
|
+
return { corpora, keepEmbeddingModelWarm, analysis: analysisConfig, people, skillWhisperer };
|
|
148
232
|
}
|
package/dist/src/contracts.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { MemoryPluginCapability } from "openclaw/plugin-sdk/memory-host-core";
|
|
2
|
+
import type { OpenClawPluginToolContext } from "openclaw/plugin-sdk/plugin-entry";
|
|
2
3
|
import type { SessionMetadata } from "./session-projector.js";
|
|
3
4
|
import type { ChatType } from "./config.js";
|
|
4
5
|
export type MemoryPluginRuntimeContract = NonNullable<MemoryPluginCapability["runtime"]>;
|
|
@@ -18,9 +19,11 @@ export type SessionSearchFilter = {
|
|
|
18
19
|
accountId?: string;
|
|
19
20
|
conversationId?: string;
|
|
20
21
|
};
|
|
22
|
+
export type MemoryRequestContext = Pick<OpenClawPluginToolContext, "sessionKey" | "sessionId" | "messageChannel" | "agentAccountId" | "nativeChannelId" | "deliveryContext">;
|
|
21
23
|
export type CorpusSearchOptions = NonNullable<Parameters<MemorySearchManagerContract["search"]>[1]> & {
|
|
22
24
|
corpora?: readonly string[];
|
|
23
25
|
sessionFilter?: SessionSearchFilter;
|
|
26
|
+
requestContext?: MemoryRequestContext;
|
|
24
27
|
};
|
|
25
28
|
export type MemoryEmbeddingProbeResult = Awaited<ReturnType<MemorySearchManagerContract["probeEmbeddingAvailability"]>>;
|
|
26
29
|
export type MemorySyncParams = Parameters<NonNullable<MemorySearchManagerContract["sync"]>>[0];
|
package/dist/src/manager.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { QMDStore } from "@unblocklabs/qmd";
|
|
2
2
|
import { type AnalysisRunner, type MemoryAnalysisSummary, type MemoryClusterDetail, type MemoryClusterList, type MemoryClusterSort, type MemoryReclusterOptions } from "./analysis.js";
|
|
3
|
-
import type { CorpusMemorySearchResult, CorpusSearchOptions, MemoryEmbeddingProbeResult, MemoryProviderStatus, MemoryReadResult, MemorySearchManagerContract, MemorySyncParams } from "./contracts.js";
|
|
3
|
+
import type { CorpusMemorySearchResult, CorpusSearchOptions, MemoryEmbeddingProbeResult, MemoryProviderStatus, MemoryReadResult, MemoryRequestContext, MemorySearchManagerContract, MemorySyncParams } from "./contracts.js";
|
|
4
4
|
import type { ChatType } from "./config.js";
|
|
5
5
|
import { type MaintenanceStatus, type TemporalBasis } from "./curation.js";
|
|
6
6
|
import { type SessionSyncResult } from "./session-sync.js";
|
|
@@ -75,6 +75,7 @@ export declare class QmdMemoryManager implements MemorySearchManagerContract {
|
|
|
75
75
|
relPath: string;
|
|
76
76
|
from?: number;
|
|
77
77
|
lines?: number;
|
|
78
|
+
requestContext?: MemoryRequestContext;
|
|
78
79
|
}): Promise<MemoryReadResult>;
|
|
79
80
|
status(): MemoryProviderStatus;
|
|
80
81
|
probeEmbeddingAvailability(): Promise<MemoryEmbeddingProbeResult>;
|
package/dist/src/manager.js
CHANGED
|
@@ -6,7 +6,7 @@ import picomatch from "picomatch";
|
|
|
6
6
|
import { ensureMemoryAnalysisSchema, latestAnalysisCollections, latestAnalysisRunId, markMemoryAnalysisStale, readAnalysisSummary, readCluster, readClusters, runAnalysisWorker, } from "./analysis.js";
|
|
7
7
|
import { CurationStore, chunkFingerprint, } from "./curation.js";
|
|
8
8
|
import { readSessionManifest, sessionMetadataByPath, syncSessionProjections, } from "./session-sync.js";
|
|
9
|
-
import { parseSafeVirtualPath } from "./sources.js";
|
|
9
|
+
import { parseSafeVirtualPath, sourceMatchesPath } from "./sources.js";
|
|
10
10
|
const DEFAULT_READ_LINES = 120;
|
|
11
11
|
const MAX_READ_CHARS = 12_000;
|
|
12
12
|
const WATCH_DEBOUNCE_MS = 250;
|
|
@@ -312,8 +312,11 @@ export class QmdMemoryManager {
|
|
|
312
312
|
this.#watcher.on("all", (_event, path) => {
|
|
313
313
|
if (this.#closed)
|
|
314
314
|
return;
|
|
315
|
-
|
|
315
|
+
const matchingSources = [...this.#sources.values()].filter((source) => source.kind !== "sessions" && sourceMatchesPath(source, path));
|
|
316
|
+
if (matchingSources.some((source) => source.kind === "skills"))
|
|
316
317
|
this.#skillIndex = undefined;
|
|
318
|
+
if (!matchingSources.some((source) => source.kind !== "skills"))
|
|
319
|
+
return;
|
|
317
320
|
this.#dirty = true;
|
|
318
321
|
if (this.#watchTimer)
|
|
319
322
|
clearTimeout(this.#watchTimer);
|
|
@@ -341,7 +344,7 @@ export class QmdMemoryManager {
|
|
|
341
344
|
dbPath: this.#dbPath,
|
|
342
345
|
keepModelsWarm: this.#keepModelsWarm,
|
|
343
346
|
config: {
|
|
344
|
-
collections: Object.fromEntries(
|
|
347
|
+
collections: Object.fromEntries(this.#qmdSources().map((source) => [
|
|
345
348
|
source.collection,
|
|
346
349
|
{ path: source.root, pattern: source.pattern },
|
|
347
350
|
])),
|
|
@@ -350,7 +353,7 @@ export class QmdMemoryManager {
|
|
|
350
353
|
enableSecureDelete(store);
|
|
351
354
|
ensureMemoryAnalysisSchema(store.internal.db);
|
|
352
355
|
markStaleForAnalysisCollectionChange(store.internal.db, this.#analysisCollectionNames(), this.#skillCollectionNames().length > 0);
|
|
353
|
-
const configuredCollections = new Set(this.#
|
|
356
|
+
const configuredCollections = new Set(this.#qmdSources().map((source) => source.collection));
|
|
354
357
|
const staleCollections = (await store.getStatus()).collections
|
|
355
358
|
.map((collection) => collection.name)
|
|
356
359
|
.filter((collection) => !configuredCollections.has(collection));
|
|
@@ -383,8 +386,8 @@ export class QmdMemoryManager {
|
|
|
383
386
|
this.#store = store;
|
|
384
387
|
return store;
|
|
385
388
|
}
|
|
386
|
-
#
|
|
387
|
-
return [...this.#sources.
|
|
389
|
+
#qmdSources() {
|
|
390
|
+
return [...this.#sources.values()].filter((source) => source.kind !== "skills");
|
|
388
391
|
}
|
|
389
392
|
#analysisCollectionNames() {
|
|
390
393
|
return [...this.#sources.values()]
|
|
@@ -421,7 +424,7 @@ export class QmdMemoryManager {
|
|
|
421
424
|
const store = await this.#getStore();
|
|
422
425
|
this.#dirty = true;
|
|
423
426
|
const analysisStore = store;
|
|
424
|
-
const collections =
|
|
427
|
+
const collections = this.#qmdSources().filter((source) => source.kind !== "sessions");
|
|
425
428
|
let analysisMarkedStale = false;
|
|
426
429
|
const markAnalysisStale = () => {
|
|
427
430
|
if (analysisMarkedStale || !analysisStore.internal)
|
|
@@ -444,16 +447,14 @@ export class QmdMemoryManager {
|
|
|
444
447
|
const update = await store.update({ collections: [source.collection] });
|
|
445
448
|
this.#cleanupRemovedDocuments?.(update.updated + update.removed);
|
|
446
449
|
const changed = update.indexed + update.updated + update.removed > 0 || update.needsEmbedding > 0;
|
|
447
|
-
if (
|
|
448
|
-
this.#skillIndex = undefined;
|
|
449
|
-
if (source.kind !== "skills" && (changed || params?.force === true))
|
|
450
|
+
if (changed || params?.force === true)
|
|
450
451
|
markAnalysisStale();
|
|
451
452
|
const embed = await store.embed({
|
|
452
453
|
collection: source.collection,
|
|
453
454
|
force: params?.force,
|
|
454
455
|
chunkStrategy: "semantic",
|
|
455
456
|
});
|
|
456
|
-
if (
|
|
457
|
+
if (completedEmbeddingCount(embed) > 0)
|
|
457
458
|
markAnalysisStale();
|
|
458
459
|
}
|
|
459
460
|
const status = await store.getStatus();
|
|
@@ -736,41 +737,47 @@ export class QmdMemoryManager {
|
|
|
736
737
|
if (!store.internal?.llm)
|
|
737
738
|
throw new Error("Skill Whisperer requires the QMD embedding model");
|
|
738
739
|
const llm = store.internal.llm;
|
|
739
|
-
this.#skillIndex
|
|
740
|
-
|
|
741
|
-
const
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
const
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
metadata.
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
740
|
+
let skillIndex = this.#skillIndex;
|
|
741
|
+
if (!skillIndex) {
|
|
742
|
+
const pending = (async () => {
|
|
743
|
+
const sourceOrder = new Map([...this.#sources.keys()].map((collection, index) => [collection, index]));
|
|
744
|
+
const metadata = new Map();
|
|
745
|
+
for (const source of this.#sources.values()) {
|
|
746
|
+
if (source.kind !== "skills")
|
|
747
|
+
continue;
|
|
748
|
+
for (const document of readSkillDocuments(source)) {
|
|
749
|
+
const name = frontmatterValue(document.body, "name") || basename(dirname(document.path));
|
|
750
|
+
const description = frontmatterValue(document.body, "description") ?? "";
|
|
751
|
+
const key = name.toLowerCase();
|
|
752
|
+
const order = sourceOrder.get(source.collection) ?? Number.MAX_SAFE_INTEGER;
|
|
753
|
+
const current = metadata.get(key);
|
|
754
|
+
if (!current || order < current.sourceOrder) {
|
|
755
|
+
metadata.set(key, {
|
|
756
|
+
candidate: { name, path: document.path },
|
|
757
|
+
description,
|
|
758
|
+
sourceOrder: order,
|
|
759
|
+
});
|
|
760
|
+
}
|
|
757
761
|
}
|
|
758
762
|
}
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
763
|
+
const skills = [...metadata.values()];
|
|
764
|
+
const embeddings = await llm.embedBatch(skills.map(({ candidate, description }) => embeddingText(candidate.name, description, llm.embedModelName)));
|
|
765
|
+
return skills.flatMap(({ candidate }, index) => {
|
|
766
|
+
const embedding = embeddings[index]?.embedding;
|
|
767
|
+
return embedding ? [{ ...candidate, score: 0, embedding }] : [];
|
|
768
|
+
});
|
|
769
|
+
})();
|
|
770
|
+
skillIndex = pending.catch((error) => {
|
|
771
|
+
if (this.#skillIndex === skillIndex)
|
|
772
|
+
this.#skillIndex = undefined;
|
|
773
|
+
throw error;
|
|
765
774
|
});
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
throw error;
|
|
769
|
-
});
|
|
775
|
+
this.#skillIndex = skillIndex;
|
|
776
|
+
}
|
|
770
777
|
const queryEmbedding = await llm.embed(queryText(query, llm.embedModelName), { isQuery: true });
|
|
771
778
|
if (!queryEmbedding)
|
|
772
779
|
return [];
|
|
773
|
-
const candidates = await
|
|
780
|
+
const candidates = await skillIndex;
|
|
774
781
|
return candidates
|
|
775
782
|
.map(({ embedding, ...candidate }) => ({
|
|
776
783
|
...candidate,
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
|
|
2
|
+
import type { UnblockMemoryConfig } from "./config.js";
|
|
3
|
+
import { type PeopleRefinementRunner } from "./people-refinement.js";
|
|
4
|
+
export declare function registerPeopleCli(api: OpenClawPluginApi, config: UnblockMemoryConfig["people"], runner?: PeopleRefinementRunner): void;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { resolveAgentDir } from "openclaw/plugin-sdk/memory-core-host-engine-foundation";
|
|
3
|
+
import { PeopleStores } from "./people-store.js";
|
|
4
|
+
import { codexPeopleRefinementRunner, refinePeople, } from "./people-refinement.js";
|
|
5
|
+
export function registerPeopleCli(api, config, runner = codexPeopleRefinementRunner) {
|
|
6
|
+
api.registerCli(({ program, config: openClawConfig }) => {
|
|
7
|
+
const root = program.command("unblock-memory").description("Unblock Memory administration");
|
|
8
|
+
const people = root.command("people").description("Maintain the agent-local people store");
|
|
9
|
+
people
|
|
10
|
+
.command("refine")
|
|
11
|
+
.description("Refine stale enabled people with Codex")
|
|
12
|
+
.requiredOption("--agent <id>", "Agent id")
|
|
13
|
+
.action(async (options) => {
|
|
14
|
+
const agentId = options.agent.trim();
|
|
15
|
+
if (!agentId)
|
|
16
|
+
throw new Error("--agent must be a non-empty string");
|
|
17
|
+
if (!config.enabled)
|
|
18
|
+
throw new Error("PeopleSQL is disabled");
|
|
19
|
+
const stores = new PeopleStores({
|
|
20
|
+
maxOpenTodos: config.todos.maxOpen,
|
|
21
|
+
maxBlurbChars: config.whisperer.maxChars,
|
|
22
|
+
});
|
|
23
|
+
try {
|
|
24
|
+
const summary = await refinePeople({
|
|
25
|
+
store: stores.get(agentId),
|
|
26
|
+
agentId,
|
|
27
|
+
agentDatabasePath: join(resolveAgentDir(openClawConfig, agentId), "openclaw-agent.sqlite"),
|
|
28
|
+
candidateLimit: config.refinement.maxPeoplePerRun,
|
|
29
|
+
maxBlurbChars: config.whisperer.maxChars,
|
|
30
|
+
runner,
|
|
31
|
+
});
|
|
32
|
+
process.stdout.write(`${JSON.stringify(summary)}\n`);
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
stores.closeAll();
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}, {
|
|
39
|
+
descriptors: [
|
|
40
|
+
{
|
|
41
|
+
name: "unblock-memory",
|
|
42
|
+
description: "Unblock Memory administration",
|
|
43
|
+
hasSubcommands: true,
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
});
|
|
47
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type PersonSessionEvidence = {
|
|
2
|
+
source: "session";
|
|
3
|
+
locator: string;
|
|
4
|
+
observedAt: string;
|
|
5
|
+
text: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function readPersonSessionEvidence(params: {
|
|
8
|
+
databasePath: string;
|
|
9
|
+
agentId: string;
|
|
10
|
+
accountScope: string;
|
|
11
|
+
externalId: string;
|
|
12
|
+
limit?: number;
|
|
13
|
+
maxMessageChars?: number;
|
|
14
|
+
}): PersonSessionEvidence[];
|