@unblocklabs/unblock-memory 0.3.12 → 0.3.13
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 +155 -6
- package/dist/src/config.d.ts +20 -0
- package/dist/src/config.js +103 -2
- package/dist/src/contracts.d.ts +4 -0
- package/dist/src/curation.d.ts +5 -2
- package/dist/src/curation.js +38 -2
- package/dist/src/manager.d.ts +35 -1
- package/dist/src/manager.js +37 -4
- package/dist/src/memory-whisperer.d.ts +15 -0
- package/dist/src/memory-whisperer.js +134 -0
- package/dist/src/plugin.js +46 -2
- package/dist/src/quality-audit.d.ts +60 -0
- package/dist/src/quality-audit.js +151 -0
- package/dist/src/skill-whisperer.d.ts +1 -1
- package/dist/src/skill-whisperer.js +45 -19
- package/dist/src/typesafe.d.ts +47 -0
- package/dist/src/typesafe.js +196 -0
- package/dist/src/whisperer-context.d.ts +13 -0
- package/dist/src/whisperer-context.js +35 -0
- package/openclaw.plugin.json +67 -1
- package/package.json +1 -1
- package/skills/memory-curator/SKILL.md +26 -1
package/README.md
CHANGED
|
@@ -80,6 +80,12 @@ directories, or globs into named corpora:
|
|
|
80
80
|
minScore: 0.5,
|
|
81
81
|
cooldownTurns: 10,
|
|
82
82
|
},
|
|
83
|
+
typesafe: {
|
|
84
|
+
enabled: true, // Default; shared by enabled Skill and Memory Whisperers.
|
|
85
|
+
// Alternatively set TYPESAFE_API_KEY in the Gateway environment.
|
|
86
|
+
apiKeyFile: "/absolute/path/to/.env",
|
|
87
|
+
timeoutMs: 1500,
|
|
88
|
+
},
|
|
83
89
|
people: {
|
|
84
90
|
enabled: false,
|
|
85
91
|
whisperer: { enabled: false, maxChars: 1200 },
|
|
@@ -110,6 +116,68 @@ idle unload behavior.
|
|
|
110
116
|
request all of them explicitly. Search results include their corpus name and
|
|
111
117
|
remain readable by passing the returned `qmd://` path to `memory_get`.
|
|
112
118
|
|
|
119
|
+
### Memory Whisperer
|
|
120
|
+
|
|
121
|
+
Memory Whisperer is optional and **off by default**. It proactively retrieves
|
|
122
|
+
historical context before user-triggered turns, without changing `memory_search`
|
|
123
|
+
or `memory_get`. Enable it in the plugin config with an explicit corpus allowlist:
|
|
124
|
+
|
|
125
|
+
```json5
|
|
126
|
+
memoryWhisperer: {
|
|
127
|
+
enabled: true,
|
|
128
|
+
corpora: ["knowledge"], // Must exist in corpora; approve its contents for all agent audiences.
|
|
129
|
+
historyMessages: 5,
|
|
130
|
+
minUsefulness: 0.9,
|
|
131
|
+
maxHints: 2,
|
|
132
|
+
cooldownTurns: 10,
|
|
133
|
+
timeoutMs: 3000,
|
|
134
|
+
},
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Requires `hooks.allowConversationAccess: true` on the plugin entry, prompt
|
|
138
|
+
injection permission, and the shared TypeSafe credentials described below.
|
|
139
|
+
An empty allowlist is invalid when enabled; `all`, unknown names, and `skills`
|
|
140
|
+
are not accepted. File corpora are approved for **every audience using the agent**:
|
|
141
|
+
do not allowlist private dossiers for an agent that also serves shared channels.
|
|
142
|
+
If `sessions` is allowlisted, only the exact current session is searched, including
|
|
143
|
+
its older indexed messages. Missing session identity excludes that corpus. Other
|
|
144
|
+
sessions, even in the same channel, are excluded before sending excerpts to TypeSafe.
|
|
145
|
+
Session availability still depends on the normal indexing/sync schedule.
|
|
146
|
+
|
|
147
|
+
QMD searches the current request plus the last N user/assistant messages (at most
|
|
148
|
+
12,000 characters), retrieving up to eight vector candidates without query expansion,
|
|
149
|
+
the local reranker, or a similarity-score cutoff. TypeSafe evaluates one independent
|
|
150
|
+
Noul question per candidate in a single request: does the excerpt add material value
|
|
151
|
+
beyond what the conversation already contains? Merely related, redundant,
|
|
152
|
+
wrong-person/project, and clearly superseded information should be rejected;
|
|
153
|
+
useful contradictory evidence can qualify. `minUsefulness` thresholds the probability
|
|
154
|
+
of yes, not a calibrated guarantee of accuracy. Evaluate it on your own conversations.
|
|
155
|
+
|
|
156
|
+
**Privacy and budgets:** this feature sends up to 16,000 characters of the available
|
|
157
|
+
user/assistant conversation, prioritizing the current request and recent messages,
|
|
158
|
+
plus up to eight 1,200-character excerpts, corpus names, and session dates to
|
|
159
|
+
`api.typesafe.ai`. Session excerpts retain a complete turn or message when it fits,
|
|
160
|
+
otherwise the complete matched chunk. Chunks exceeding the excerpt budget are
|
|
161
|
+
skipped, never sliced; ordinary `memory_search` is unchanged.
|
|
162
|
+
It does not fetch a complete historical transcript; the host may
|
|
163
|
+
already have compacted the available context. Truncation is marked in the judge's
|
|
164
|
+
input. System messages, thinking blocks, images, and tool-result messages are omitted;
|
|
165
|
+
anything quoted in ordinary user/assistant text can still be transmitted.
|
|
166
|
+
|
|
167
|
+
At most two qualifying excerpts are injected verbatim with source references and
|
|
168
|
+
historical/untrusted-data framing. Excerpts are deduplicated by normalized content
|
|
169
|
+
and overlapping source lines; recently injected content has a ten-user-turn cooldown
|
|
170
|
+
by default. Cooldown state is in memory and resets on session end or Gateway restart.
|
|
171
|
+
The complete hint payload is capped at 5,000 characters plus a short framing paragraph.
|
|
172
|
+
|
|
173
|
+
Unlike Skill Whisperer, **disabled TypeSafe, a missing key, no qualifying hits, or any
|
|
174
|
+
failure means no memory hint**—there is no vector-only fallback. The overall process
|
|
175
|
+
has a 3-second deadline, with the shared 1.5-second TypeSafe request deadline inside it;
|
|
176
|
+
neither performs retries. Timed-out or superseded runs cannot inject late hints.
|
|
177
|
+
Already-running local QMD work may finish in the background, but does not keep the
|
|
178
|
+
agent waiting beyond the deadline. No new indexing, clustering, or summarization runs
|
|
179
|
+
are triggered by this feature beyond the memory manager's normal initialization.
|
|
180
|
+
|
|
113
181
|
### Skill Whisperer
|
|
114
182
|
|
|
115
183
|
Skill Whisperer is an optional semantic reminder for user turns. Configure one
|
|
@@ -117,13 +185,40 @@ isolated `skills` corpus, set `skillWhisperer.enabled` to `true`, and authorize
|
|
|
117
185
|
`plugins.entries.unblock-memory.hooks.allowConversationAccess`. The feature
|
|
118
186
|
embeds the current prompt plus the configured number of prior user/assistant
|
|
119
187
|
messages, compares it with each configured skill's frontmatter `name` and
|
|
120
|
-
`description
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
188
|
+
`description`. With TypeSafe enabled and a key available, the top three valid
|
|
189
|
+
candidates are sent to TypeSafe, without a vector-score cutoff. TypeSafe chooses
|
|
190
|
+
one skill or none. A "none" decision never falls back to a vector hint. Full skill
|
|
191
|
+
procedures do not influence routing; no skill is invoked automatically.
|
|
192
|
+
|
|
193
|
+
The shared `typesafe` configuration defaults to `enabled: true` and
|
|
194
|
+
`timeoutMs: 1500`. Skill and Memory Whisperers share it. Credentials come from
|
|
195
|
+
`typesafe.apiKey`, an absolute `typesafe.apiKeyFile`, or (when neither is set)
|
|
196
|
+
the Gateway's `TYPESAFE_API_KEY` environment variable. Configure at most one of
|
|
197
|
+
`apiKey` and `apiKeyFile`. A key file may contain just the key or dotenv entries
|
|
198
|
+
including `TYPESAFE_API_KEY`; it is reread each turn to support rotation. A dotenv
|
|
199
|
+
file is not sourced as shell code and does not change the process environment.
|
|
200
|
+
Missing/empty files or dotenv files without that variable count as no key;
|
|
201
|
+
an explicit file never falls back to an unrelated environment key. Protect key
|
|
202
|
+
files with owner-only permissions. Workspace `.env` files are not auto-discovered:
|
|
203
|
+
point `apiKeyFile` at the intended file or load the variable into the Gateway.
|
|
204
|
+
|
|
205
|
+
If TypeSafe is disabled or no key is found, selection uses the original local
|
|
206
|
+
vector process and `skillWhisperer.minScore`. With a key present, an API error,
|
|
207
|
+
invalid response, or timeout emits no hint and logs a sanitized warning; it does
|
|
208
|
+
not switch to vector-only selection. There are no automatic HTTP retries. Other
|
|
209
|
+
credential-file read errors likewise produce a warning and no hint.
|
|
210
|
+
|
|
211
|
+
**Privacy:** enabled TypeSafe selection sends up to 12,000 characters of current
|
|
212
|
+
prompt/recent user-assistant text, plus the shortlisted names/descriptions, to
|
|
213
|
+
`api.typesafe.ai`. Source-path fields, full skill procedures, tool-result messages,
|
|
214
|
+
and system messages are excluded; dossiers and ordinary memory files are not read
|
|
215
|
+
for this call. Material already quoted in user/assistant text can still be included.
|
|
216
|
+
Disable `typesafe.enabled` to keep Skill Whisperer entirely local. The pinned model
|
|
217
|
+
is `jev-1.13.0`.
|
|
218
|
+
|
|
219
|
+
The defaults use five prior messages, a vector-only score threshold of `0.5`,
|
|
125
220
|
and a ten-turn cooldown. A skill is cooling down after either a suggestion or a
|
|
126
|
-
successful direct `read` of its indexed `SKILL.md`. When the
|
|
221
|
+
successful direct `read` of its indexed `SKILL.md`. When the selected
|
|
127
222
|
skill is cooling down, no hint is emitted; Skill Whisperer does not fall through
|
|
128
223
|
to a weaker match. Cooldown state is per session and intentionally resets with
|
|
129
224
|
the Gateway. Shell-command reads are not tracked.
|
|
@@ -275,6 +370,60 @@ dates and maintenance proposals live separately in `curation.sqlite`, so a QMD
|
|
|
275
370
|
index rebuild does not discard them. The first lookup builds the index;
|
|
276
371
|
Markdown filesystem changes queue a debounced, serialized background refresh.
|
|
277
372
|
|
|
373
|
+
## Memory quality audit
|
|
374
|
+
|
|
375
|
+
`memory_audit_quality` is an on-demand, source-read-only audit. TypeSafe flags likely
|
|
376
|
+
ingestion noise for agent investigation; it never deletes, rewrites, or suppresses
|
|
377
|
+
memory. Enable it with explicit approval for the corpora sent to TypeSafe:
|
|
378
|
+
|
|
379
|
+
```json5
|
|
380
|
+
qualityAudit: {
|
|
381
|
+
enabled: true,
|
|
382
|
+
corpora: ["memory", "knowledge"], // Must be configured non-skill corpora.
|
|
383
|
+
minNoise: 0.8,
|
|
384
|
+
},
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
Off by default. Uses the shared TypeSafe credentials and request timeout. Missing
|
|
388
|
+
credentials or disabled TypeSafe produces no audit. Approval includes transmission
|
|
389
|
+
of full eligible chunks and visibility of findings to all audiences using the agent.
|
|
390
|
+
Unlike Memory Whisperer, approving `sessions` includes **all indexed sessions** in
|
|
391
|
+
that corpus, including configured direct conversations. Only approve that when intended.
|
|
392
|
+
|
|
393
|
+
Call with `{ "limit": 10 }` (maximum 20 indexed chunk occurrences per page), then
|
|
394
|
+
pass the returned `next` as `after` until `done` is true. A `partial` result preserves
|
|
395
|
+
the completed cursor; retry there, or from the beginning if no cursor exists. This
|
|
396
|
+
is not a full-document audit: unindexed content is not scanned. Chunks over 6,000
|
|
397
|
+
characters are counted as skipped, not silently truncated. No clustering is required.
|
|
398
|
+
|
|
399
|
+
Two independent Noul questions distinguish ingestion noise from identifiable useful
|
|
400
|
+
evidence. High values for both can indicate valuable content trapped in a wrapper.
|
|
401
|
+
Low evidence alone does not create a junk finding. JSON, logs, code, terse facts,
|
|
402
|
+
historical records, and missing context are not automatically defects. Empty chunks
|
|
403
|
+
are detected locally. A JSON string that decodes to a message envelope is also
|
|
404
|
+
flagged as a possible double-encoding defect, even when its content is useful.
|
|
405
|
+
An ordinary JSON message object is not flagged from its shape alone. These are
|
|
406
|
+
review clues, never verdicts about whether the information should be kept.
|
|
407
|
+
|
|
408
|
+
At most four unique chunks (24,000 characters) and their source kinds are sent in
|
|
409
|
+
one request, without conversation context or source paths. Requests do not retry
|
|
410
|
+
automatically and stop starting new work after a 30-second audit deadline; existing
|
|
411
|
+
manager initialization/indexing may finish later. Judgments are cached in the
|
|
412
|
+
curation database by content, source kind, model and question version. A rescan from
|
|
413
|
+
the beginning reuses cached results, including after corpus/index changes. Changes
|
|
414
|
+
behind a page cursor are picked up on the next rescan.
|
|
415
|
+
|
|
416
|
+
Suspect chunks become `quality_review` tasks in `memory_list_maintenance_tasks`.
|
|
417
|
+
The audit returns page-local groups by configured source and suspected issue,
|
|
418
|
+
with up to three examples each, not a claim that a whole cluster is defective.
|
|
419
|
+
Findings include source references, bounded previews, probabilities and content
|
|
420
|
+
fingerprints. Reviewed tasks are not reopened for unchanged content. The curator
|
|
421
|
+
inspects the original source and ingestion path, proposes or performs authorized
|
|
422
|
+
repairs, and verifies the resulting source/index before resolving with a required
|
|
423
|
+
note. Prefer repairing a common extractor or inclusion rule over many symptoms;
|
|
424
|
+
never manually edit generated session projections. Thresholds need evaluation on
|
|
425
|
+
your data; model probability is not proof of a defect.
|
|
426
|
+
|
|
278
427
|
## Memory analysis
|
|
279
428
|
|
|
280
429
|
Analysis is opt-in. Core indexing, `memory_search`, and `memory_get` need only
|
package/dist/src/config.d.ts
CHANGED
|
@@ -25,6 +25,17 @@ export type UnblockMemoryConfig = {
|
|
|
25
25
|
analysis: {
|
|
26
26
|
executable?: string;
|
|
27
27
|
};
|
|
28
|
+
typesafe: {
|
|
29
|
+
enabled: boolean;
|
|
30
|
+
apiKey?: string;
|
|
31
|
+
apiKeyFile?: string;
|
|
32
|
+
timeoutMs: number;
|
|
33
|
+
};
|
|
34
|
+
qualityAudit: {
|
|
35
|
+
enabled: boolean;
|
|
36
|
+
corpora: readonly string[];
|
|
37
|
+
minNoise: number;
|
|
38
|
+
};
|
|
28
39
|
people: {
|
|
29
40
|
enabled: boolean;
|
|
30
41
|
whisperer: {
|
|
@@ -41,6 +52,15 @@ export type UnblockMemoryConfig = {
|
|
|
41
52
|
minScore: number;
|
|
42
53
|
cooldownTurns: number;
|
|
43
54
|
};
|
|
55
|
+
memoryWhisperer: {
|
|
56
|
+
enabled: boolean;
|
|
57
|
+
corpora: readonly string[];
|
|
58
|
+
historyMessages: number;
|
|
59
|
+
minUsefulness: number;
|
|
60
|
+
maxHints: number;
|
|
61
|
+
cooldownTurns: number;
|
|
62
|
+
timeoutMs: number;
|
|
63
|
+
};
|
|
44
64
|
};
|
|
45
65
|
export declare const DEFAULT_PEOPLE_CONFIG: UnblockMemoryConfig["people"];
|
|
46
66
|
export declare function resolveConfig(value: unknown): UnblockMemoryConfig;
|
package/dist/src/config.js
CHANGED
|
@@ -15,12 +15,108 @@ export const DEFAULT_PEOPLE_CONFIG = {
|
|
|
15
15
|
whisperer: { enabled: false, maxChars: 1200 },
|
|
16
16
|
todos: { maxOpen: 1000 },
|
|
17
17
|
};
|
|
18
|
+
const DEFAULT_TYPESAFE_CONFIG = {
|
|
19
|
+
enabled: true,
|
|
20
|
+
timeoutMs: 1500,
|
|
21
|
+
};
|
|
22
|
+
const DEFAULT_QUALITY_AUDIT = {
|
|
23
|
+
enabled: false, corpora: [], minNoise: 0.8,
|
|
24
|
+
};
|
|
25
|
+
function resolveQualityAudit(value, corpora) {
|
|
26
|
+
if (value === undefined)
|
|
27
|
+
return { ...DEFAULT_QUALITY_AUDIT };
|
|
28
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
29
|
+
throw new Error("qualityAudit must be an object");
|
|
30
|
+
const config = value;
|
|
31
|
+
assertOnlyKeys(config, ["enabled", "corpora", "minNoise"], "qualityAudit");
|
|
32
|
+
const enabled = config.enabled ?? false;
|
|
33
|
+
const selected = config.corpora ?? [];
|
|
34
|
+
const minNoise = config.minNoise ?? DEFAULT_QUALITY_AUDIT.minNoise;
|
|
35
|
+
if (typeof enabled !== "boolean")
|
|
36
|
+
throw new Error("qualityAudit.enabled must be a boolean");
|
|
37
|
+
if (!Array.isArray(selected) || !selected.every((name) => typeof name === "string" && corpora.some(corpus => corpus.name === name && corpus.kind !== "skills"))) {
|
|
38
|
+
throw new Error("qualityAudit.corpora must list configured non-skill corpora");
|
|
39
|
+
}
|
|
40
|
+
if (enabled && !selected.length)
|
|
41
|
+
throw new Error("enabled qualityAudit requires explicit corpora");
|
|
42
|
+
if (typeof minNoise !== "number" || !Number.isFinite(minNoise) || minNoise < 0 || minNoise > 1) {
|
|
43
|
+
throw new Error("qualityAudit.minNoise must be between 0 and 1");
|
|
44
|
+
}
|
|
45
|
+
return { enabled, corpora: [...new Set(selected)], minNoise };
|
|
46
|
+
}
|
|
47
|
+
function resolveTypeSafe(value) {
|
|
48
|
+
if (value === undefined)
|
|
49
|
+
return { ...DEFAULT_TYPESAFE_CONFIG };
|
|
50
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
51
|
+
throw new Error("unblock-memory typesafe must be an object");
|
|
52
|
+
}
|
|
53
|
+
const config = value;
|
|
54
|
+
assertOnlyKeys(config, ["enabled", "apiKey", "apiKeyFile", "timeoutMs"], "typesafe");
|
|
55
|
+
const enabled = config.enabled ?? true;
|
|
56
|
+
if (typeof enabled !== "boolean")
|
|
57
|
+
throw new Error("unblock-memory typesafe.enabled must be a boolean");
|
|
58
|
+
for (const key of ["apiKey", "apiKeyFile"]) {
|
|
59
|
+
if (config[key] !== undefined && (typeof config[key] !== "string" || !config[key].trim())) {
|
|
60
|
+
throw new Error(`unblock-memory typesafe.${key} must be a non-empty string`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const apiKey = typeof config.apiKey === "string" ? config.apiKey.trim() : undefined;
|
|
64
|
+
const apiKeyFile = typeof config.apiKeyFile === "string" ? config.apiKeyFile.trim() : undefined;
|
|
65
|
+
if (apiKey && apiKeyFile)
|
|
66
|
+
throw new Error("unblock-memory typesafe accepts apiKey or apiKeyFile, not both");
|
|
67
|
+
if (apiKeyFile && !isAbsolute(apiKeyFile)) {
|
|
68
|
+
throw new Error("unblock-memory typesafe.apiKeyFile must be an absolute path");
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
enabled, ...(apiKey ? { apiKey } : {}), ...(apiKeyFile ? { apiKeyFile } : {}),
|
|
72
|
+
timeoutMs: positiveInteger(config.timeoutMs, DEFAULT_TYPESAFE_CONFIG.timeoutMs, "typesafe.timeoutMs", 10_000),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
18
75
|
const DEFAULT_SKILL_WHISPERER = {
|
|
19
76
|
enabled: false,
|
|
20
77
|
historyMessages: 5,
|
|
21
78
|
minScore: 0.5,
|
|
22
79
|
cooldownTurns: 10,
|
|
23
80
|
};
|
|
81
|
+
const DEFAULT_MEMORY_WHISPERER = {
|
|
82
|
+
enabled: false, corpora: [], historyMessages: 5, minUsefulness: 0.9,
|
|
83
|
+
maxHints: 2, cooldownTurns: 10, timeoutMs: 3000,
|
|
84
|
+
};
|
|
85
|
+
function resolveMemoryWhisperer(value, corpora) {
|
|
86
|
+
if (value === undefined)
|
|
87
|
+
return { ...DEFAULT_MEMORY_WHISPERER };
|
|
88
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
89
|
+
throw new Error("unblock-memory memoryWhisperer must be an object");
|
|
90
|
+
}
|
|
91
|
+
const config = value;
|
|
92
|
+
assertOnlyKeys(config, Object.keys(DEFAULT_MEMORY_WHISPERER), "memoryWhisperer");
|
|
93
|
+
const enabled = config.enabled ?? false;
|
|
94
|
+
if (typeof enabled !== "boolean")
|
|
95
|
+
throw new Error("unblock-memory memoryWhisperer.enabled must be a boolean");
|
|
96
|
+
const selected = config.corpora ?? [];
|
|
97
|
+
if (!Array.isArray(selected) || !selected.every((name) => typeof name === "string" && corpora.some(corpus => corpus.name === name && corpus.kind !== "skills"))) {
|
|
98
|
+
throw new Error("unblock-memory memoryWhisperer.corpora must list configured non-skill corpora");
|
|
99
|
+
}
|
|
100
|
+
if (enabled && !selected.length)
|
|
101
|
+
throw new Error("unblock-memory enabled memoryWhisperer requires explicit corpora");
|
|
102
|
+
const historyMessages = config.historyMessages ?? 5;
|
|
103
|
+
const cooldownTurns = config.cooldownTurns ?? 10;
|
|
104
|
+
if (typeof historyMessages !== "number" || !Number.isInteger(historyMessages) || historyMessages < 0 || historyMessages > 50) {
|
|
105
|
+
throw new Error("unblock-memory memoryWhisperer.historyMessages must be an integer between 0 and 50");
|
|
106
|
+
}
|
|
107
|
+
if (typeof cooldownTurns !== "number" || !Number.isInteger(cooldownTurns) || cooldownTurns < 0 || cooldownTurns > 1000) {
|
|
108
|
+
throw new Error("unblock-memory memoryWhisperer.cooldownTurns must be an integer between 0 and 1000");
|
|
109
|
+
}
|
|
110
|
+
const minUsefulness = config.minUsefulness ?? 0.9;
|
|
111
|
+
if (typeof minUsefulness !== "number" || !Number.isFinite(minUsefulness) || minUsefulness < 0 || minUsefulness > 1) {
|
|
112
|
+
throw new Error("unblock-memory memoryWhisperer.minUsefulness must be between 0 and 1");
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
enabled, corpora: [...new Set(selected)], historyMessages, cooldownTurns, minUsefulness,
|
|
116
|
+
maxHints: positiveInteger(config.maxHints, 2, "memoryWhisperer.maxHints", 2),
|
|
117
|
+
timeoutMs: positiveInteger(config.timeoutMs, 3000, "memoryWhisperer.timeoutMs", 10_000),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
24
120
|
function assertOnlyKeys(value, allowed, label) {
|
|
25
121
|
const unknown = Object.keys(value).find((key) => !allowed.includes(key));
|
|
26
122
|
if (unknown)
|
|
@@ -160,15 +256,18 @@ export function resolveConfig(value) {
|
|
|
160
256
|
corpora: DEFAULT_CORPORA,
|
|
161
257
|
keepEmbeddingModelWarm: true,
|
|
162
258
|
analysis: {},
|
|
259
|
+
typesafe: { ...DEFAULT_TYPESAFE_CONFIG },
|
|
260
|
+
qualityAudit: { ...DEFAULT_QUALITY_AUDIT },
|
|
163
261
|
people: DEFAULT_PEOPLE_CONFIG,
|
|
164
262
|
skillWhisperer: DEFAULT_SKILL_WHISPERER,
|
|
263
|
+
memoryWhisperer: { ...DEFAULT_MEMORY_WHISPERER },
|
|
165
264
|
};
|
|
166
265
|
}
|
|
167
266
|
if (typeof value !== "object" || Array.isArray(value)) {
|
|
168
267
|
throw new Error("unblock-memory config must be an object");
|
|
169
268
|
}
|
|
170
269
|
const config = value;
|
|
171
|
-
assertOnlyKeys(config, ["corpora", "keepEmbeddingModelWarm", "analysis", "people", "skillWhisperer"], "config");
|
|
270
|
+
assertOnlyKeys(config, ["corpora", "keepEmbeddingModelWarm", "analysis", "people", "skillWhisperer", "memoryWhisperer", "typesafe", "qualityAudit"], "config");
|
|
172
271
|
const corpora = resolveCorpora(config.corpora);
|
|
173
272
|
const people = resolvePeople(config.people);
|
|
174
273
|
if (config.keepEmbeddingModelWarm !== undefined &&
|
|
@@ -227,5 +326,7 @@ export function resolveConfig(value) {
|
|
|
227
326
|
if (skillWhisperer.enabled && !corpora.some((corpus) => corpus.kind === "skills")) {
|
|
228
327
|
throw new Error('unblock-memory enabled skillWhisperer requires a corpus named "skills" with kind "skills"');
|
|
229
328
|
}
|
|
230
|
-
return { corpora, keepEmbeddingModelWarm, analysis: analysisConfig, people, skillWhisperer
|
|
329
|
+
return { corpora, keepEmbeddingModelWarm, analysis: analysisConfig, people, skillWhisperer,
|
|
330
|
+
qualityAudit: resolveQualityAudit(config.qualityAudit, corpora),
|
|
331
|
+
memoryWhisperer: resolveMemoryWhisperer(config.memoryWhisperer, corpora), typesafe: resolveTypeSafe(config.typesafe) };
|
|
231
332
|
}
|
package/dist/src/contracts.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export type CorpusMemorySearchResult = MemorySearchResult & {
|
|
|
12
12
|
session?: SessionMetadata;
|
|
13
13
|
};
|
|
14
14
|
export type SessionSearchFilter = {
|
|
15
|
+
/** Internal exact-session scope used by proactive hints; not exposed by the search tool. */
|
|
16
|
+
sessionId?: string;
|
|
15
17
|
startedFrom?: string;
|
|
16
18
|
startedTo?: string;
|
|
17
19
|
provider?: string;
|
|
@@ -22,6 +24,8 @@ export type SessionSearchFilter = {
|
|
|
22
24
|
export type MemoryRequestContext = Pick<OpenClawPluginToolContext, "sessionKey" | "sessionId" | "messageChannel" | "agentAccountId" | "nativeChannelId" | "deliveryContext">;
|
|
23
25
|
export type CorpusSearchOptions = NonNullable<Parameters<MemorySearchManagerContract["search"]>[1]> & {
|
|
24
26
|
corpora?: readonly string[];
|
|
27
|
+
/** Internal vector-hint budget; oversized matched chunks are omitted, never sliced. */
|
|
28
|
+
maxSnippetChars?: number;
|
|
25
29
|
sessionFilter?: SessionSearchFilter;
|
|
26
30
|
requestContext?: MemoryRequestContext;
|
|
27
31
|
};
|
package/dist/src/curation.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import type { QualityJudgment } from "./typesafe.js";
|
|
1
2
|
declare const TEMPORAL_BASES: readonly ["path", "frontmatter", "session", "agent_verified"];
|
|
2
3
|
export type TemporalBasis = typeof TEMPORAL_BASES[number];
|
|
3
|
-
declare const MAINTENANCE_TASK_TYPES: readonly ["ambiguous_event_time", "exact_duplicate"];
|
|
4
|
+
declare const MAINTENANCE_TASK_TYPES: readonly ["ambiguous_event_time", "exact_duplicate", "quality_review"];
|
|
4
5
|
export type MaintenanceTaskType = typeof MAINTENANCE_TASK_TYPES[number];
|
|
5
6
|
declare const MAINTENANCE_STATUSES: readonly ["pending", "resolved", "deferred", "irrelevant"];
|
|
6
7
|
export type MaintenanceStatus = typeof MAINTENANCE_STATUSES[number];
|
|
@@ -36,6 +37,8 @@ export declare class CurationStore {
|
|
|
36
37
|
#private;
|
|
37
38
|
constructor(path: string);
|
|
38
39
|
close(): void;
|
|
40
|
+
qualityJudgment(key: string): QualityJudgment | undefined;
|
|
41
|
+
cacheQualityJudgment(key: string, judgment: QualityJudgment): void;
|
|
39
42
|
annotations(): TemporalAnnotation[];
|
|
40
43
|
addTask(candidate: {
|
|
41
44
|
type: MaintenanceTaskType;
|
|
@@ -45,7 +48,7 @@ export declare class CurationStore {
|
|
|
45
48
|
reason: string;
|
|
46
49
|
contentFingerprint?: string;
|
|
47
50
|
detail?: string;
|
|
48
|
-
}):
|
|
51
|
+
}): MaintenanceTask;
|
|
49
52
|
listTasks(params?: {
|
|
50
53
|
status?: MaintenanceStatus;
|
|
51
54
|
limit?: number;
|
package/dist/src/curation.js
CHANGED
|
@@ -3,7 +3,7 @@ import { chmodSync, mkdirSync } from "node:fs";
|
|
|
3
3
|
import { dirname } from "node:path";
|
|
4
4
|
import { DatabaseSync } from "node:sqlite";
|
|
5
5
|
const TEMPORAL_BASES = ["path", "frontmatter", "session", "agent_verified"];
|
|
6
|
-
const MAINTENANCE_TASK_TYPES = ["ambiguous_event_time", "exact_duplicate"];
|
|
6
|
+
const MAINTENANCE_TASK_TYPES = ["ambiguous_event_time", "exact_duplicate", "quality_review"];
|
|
7
7
|
const MAINTENANCE_STATUSES = ["pending", "resolved", "deferred", "irrelevant"];
|
|
8
8
|
function annotation(row) {
|
|
9
9
|
return {
|
|
@@ -66,12 +66,17 @@ export class CurationStore {
|
|
|
66
66
|
|
|
67
67
|
`);
|
|
68
68
|
this.#ensureMaintenanceSchema();
|
|
69
|
+
this.#db.exec(`CREATE TABLE IF NOT EXISTS quality_judgments (
|
|
70
|
+
cache_key TEXT PRIMARY KEY,
|
|
71
|
+
noise REAL NOT NULL CHECK (noise BETWEEN 0 AND 1),
|
|
72
|
+
evidence REAL NOT NULL CHECK (evidence BETWEEN 0 AND 1)
|
|
73
|
+
)`);
|
|
69
74
|
}
|
|
70
75
|
#ensureMaintenanceSchema() {
|
|
71
76
|
this.#db.exec(`
|
|
72
77
|
CREATE TABLE IF NOT EXISTS maintenance_tasks (
|
|
73
78
|
id TEXT PRIMARY KEY,
|
|
74
|
-
type TEXT NOT NULL CHECK (type IN ('ambiguous_event_time', 'exact_duplicate')),
|
|
79
|
+
type TEXT NOT NULL CHECK (type IN ('ambiguous_event_time', 'exact_duplicate', 'quality_review')),
|
|
75
80
|
corpus TEXT NOT NULL,
|
|
76
81
|
collection TEXT NOT NULL,
|
|
77
82
|
path TEXT NOT NULL,
|
|
@@ -85,6 +90,23 @@ export class CurationStore {
|
|
|
85
90
|
UNIQUE (type, corpus, collection, path, reason, content_fingerprint)
|
|
86
91
|
);
|
|
87
92
|
`);
|
|
93
|
+
const schema = this.#db.prepare("SELECT sql FROM sqlite_master WHERE name = 'maintenance_tasks'")
|
|
94
|
+
.get();
|
|
95
|
+
if (!schema.sql.includes("'quality_review'")) {
|
|
96
|
+
this.#db.exec("BEGIN IMMEDIATE");
|
|
97
|
+
try {
|
|
98
|
+
this.#db.exec(schema.sql.replace("maintenance_tasks", "maintenance_tasks_quality")
|
|
99
|
+
.replace("'exact_duplicate'", "'exact_duplicate', 'quality_review'"));
|
|
100
|
+
this.#db.exec(`INSERT INTO maintenance_tasks_quality SELECT * FROM maintenance_tasks;
|
|
101
|
+
DROP TABLE maintenance_tasks;
|
|
102
|
+
ALTER TABLE maintenance_tasks_quality RENAME TO maintenance_tasks;`);
|
|
103
|
+
this.#db.exec("COMMIT");
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
this.#db.exec("ROLLBACK");
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
88
110
|
this.#db.exec(`
|
|
89
111
|
CREATE INDEX IF NOT EXISTS maintenance_tasks_status_created
|
|
90
112
|
ON maintenance_tasks(status, created_at);
|
|
@@ -93,6 +115,14 @@ export class CurationStore {
|
|
|
93
115
|
close() {
|
|
94
116
|
this.#db.close();
|
|
95
117
|
}
|
|
118
|
+
qualityJudgment(key) {
|
|
119
|
+
return this.#db.prepare("SELECT noise, evidence FROM quality_judgments WHERE cache_key = ?")
|
|
120
|
+
.get(key);
|
|
121
|
+
}
|
|
122
|
+
cacheQualityJudgment(key, judgment) {
|
|
123
|
+
this.#db.prepare("INSERT OR REPLACE INTO quality_judgments(cache_key, noise, evidence) VALUES (?, ?, ?)")
|
|
124
|
+
.run(key, judgment.noise, judgment.evidence);
|
|
125
|
+
}
|
|
96
126
|
annotations() {
|
|
97
127
|
return this.#db.prepare(`
|
|
98
128
|
SELECT * FROM temporal_annotations
|
|
@@ -115,6 +145,9 @@ export class CurationStore {
|
|
|
115
145
|
ELSE maintenance_tasks.updated_at
|
|
116
146
|
END
|
|
117
147
|
`).run(randomUUID(), candidate.type, candidate.corpus, candidate.collection, candidate.path, candidate.reason, candidate.contentFingerprint ?? "", candidate.detail ?? null, now, now);
|
|
148
|
+
return task(this.#db.prepare(`SELECT * FROM maintenance_tasks
|
|
149
|
+
WHERE type = ? AND corpus = ? AND collection = ? AND path = ? AND reason = ? AND content_fingerprint = ?`)
|
|
150
|
+
.get(candidate.type, candidate.corpus, candidate.collection, candidate.path, candidate.reason, candidate.contentFingerprint ?? ""));
|
|
118
151
|
}
|
|
119
152
|
listTasks(params = {}) {
|
|
120
153
|
const status = params.status ?? "pending";
|
|
@@ -136,6 +169,9 @@ export class CurationStore {
|
|
|
136
169
|
return undefined;
|
|
137
170
|
}
|
|
138
171
|
const now = new Date().toISOString();
|
|
172
|
+
if (row.type === "quality_review" && params.status === "resolved" && !params.note?.trim()) {
|
|
173
|
+
throw new Error("resolving a quality review requires a note describing source/index verification");
|
|
174
|
+
}
|
|
139
175
|
if (row.type === "ambiguous_event_time" && params.status === "resolved" && !params.annotation) {
|
|
140
176
|
throw new Error("resolving an ambiguous event-time task requires a date annotation");
|
|
141
177
|
}
|
package/dist/src/manager.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ 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";
|
|
7
7
|
import { type ResolvedSource } from "./sources.js";
|
|
8
|
+
import { type QualityCursor } from "./quality-audit.js";
|
|
8
9
|
export type ManagerStore = Pick<QMDStore, "update" | "embed" | "getStatus" | "listCollections" | "searchLex" | "vsearch" | "get" | "getDocumentBody" | "close">;
|
|
9
10
|
export type ManagerSessionConfig = {
|
|
10
11
|
agentId: string;
|
|
@@ -19,6 +20,7 @@ export type ManagerSessionConfig = {
|
|
|
19
20
|
};
|
|
20
21
|
export type SkillSearchCandidate = {
|
|
21
22
|
name: string;
|
|
23
|
+
description: string;
|
|
22
24
|
path: string;
|
|
23
25
|
score: number;
|
|
24
26
|
};
|
|
@@ -31,7 +33,7 @@ export declare function buildReadResult(params: {
|
|
|
31
33
|
from?: number;
|
|
32
34
|
lines?: number;
|
|
33
35
|
}): MemoryReadResult;
|
|
34
|
-
export declare function expandSessionSearchHit(result: Pick<VectorSearchResult, "body" | "bestChunk" | "chunkPos" | "chunkLen">, maxTokens: number, countTokens: (text: string) => Promise<number
|
|
36
|
+
export declare function expandSessionSearchHit(result: Pick<VectorSearchResult, "body" | "bestChunk" | "chunkPos" | "chunkLen">, maxTokens: number, countTokens: (text: string) => Promise<number>, maxChars?: number): Promise<{
|
|
35
37
|
text: string;
|
|
36
38
|
position: number;
|
|
37
39
|
}>;
|
|
@@ -63,6 +65,38 @@ export declare class QmdMemoryManager implements MemorySearchManagerContract {
|
|
|
63
65
|
status?: MaintenanceStatus;
|
|
64
66
|
limit?: number;
|
|
65
67
|
}): import("./curation.js").MaintenanceTask[];
|
|
68
|
+
auditQuality(params: {
|
|
69
|
+
corpora: readonly string[];
|
|
70
|
+
apiKey: string;
|
|
71
|
+
timeoutMs: number;
|
|
72
|
+
minNoise: number;
|
|
73
|
+
limit?: number;
|
|
74
|
+
after?: QualityCursor;
|
|
75
|
+
signal: AbortSignal;
|
|
76
|
+
}): Promise<{
|
|
77
|
+
status: "ok" | "partial";
|
|
78
|
+
done: boolean;
|
|
79
|
+
next: QualityCursor | undefined;
|
|
80
|
+
scanned: number;
|
|
81
|
+
judged: number;
|
|
82
|
+
cached: number;
|
|
83
|
+
skippedOversized: number;
|
|
84
|
+
skippedStale: number;
|
|
85
|
+
flagged: number;
|
|
86
|
+
groups: {
|
|
87
|
+
corpus: string;
|
|
88
|
+
source: string;
|
|
89
|
+
reason: string;
|
|
90
|
+
pending: number;
|
|
91
|
+
examples: import("./curation.js").MaintenanceTask[];
|
|
92
|
+
}[];
|
|
93
|
+
policy: string;
|
|
94
|
+
scope: string;
|
|
95
|
+
} | {
|
|
96
|
+
status: "busy";
|
|
97
|
+
} | {
|
|
98
|
+
status: "unavailable";
|
|
99
|
+
}>;
|
|
66
100
|
updateMaintenanceTask(params: {
|
|
67
101
|
id: string;
|
|
68
102
|
status: Exclude<MaintenanceStatus, "pending">;
|