@unblocklabs/unblock-memory 0.3.9 → 0.3.11
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 +19 -10
- package/dist/src/config.d.ts +1 -0
- package/dist/src/config.js +4 -1
- package/dist/src/manager.d.ts +6 -1
- package/dist/src/manager.js +40 -18
- package/dist/src/runtime.js +1 -0
- package/dist/src/session-projector.d.ts +11 -0
- package/dist/src/session-projector.js +30 -1
- package/dist/src/session-sync.js +7 -6
- package/openclaw.plugin.json +8 -2
- package/package.json +6 -6
- package/skills/memory-curator/SKILL.md +15 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Workspace-native memory for OpenClaw, powered internally by `@unblocklabs/qmd`.
|
|
4
4
|
It keeps one warm QMD store per agent and exposes the standard `memory_search`
|
|
5
|
-
and `memory_get` tools. Search uses semantic chunking
|
|
5
|
+
and `memory_get` tools. Search uses semantic chunking and direct QMD vector
|
|
6
6
|
search without query expansion or a reranker, so only the embedding model loads.
|
|
7
7
|
|
|
8
8
|
Optional memory analysis uses those same stored vectors in the same SQLite
|
|
@@ -55,6 +55,7 @@ directories, or globs into named corpora:
|
|
|
55
55
|
name: "sessions",
|
|
56
56
|
kind: "sessions",
|
|
57
57
|
chatTypes: ["channel", "group"],
|
|
58
|
+
maxExpandedTokens: 500,
|
|
58
59
|
},
|
|
59
60
|
{
|
|
60
61
|
name: "knowledge",
|
|
@@ -237,15 +238,22 @@ eligible.
|
|
|
237
238
|
|
|
238
239
|
The optional `sessions` corpus reads the current agent's normal OpenClaw SQLite
|
|
239
240
|
store and indexes its active user/assistant transcript branch. It defaults to
|
|
240
|
-
channel and group conversations; add `direct` explicitly to include DMs.
|
|
241
|
+
channel and group conversations; add `direct` explicitly to include DMs. A
|
|
242
|
+
session vector hit expands to its complete user/assistant turn when the turn
|
|
243
|
+
fits `maxExpandedTokens`, or to its complete enclosing message when only that
|
|
244
|
+
fits. The default is `500`; the original semantic chunk is preserved when
|
|
245
|
+
neither complete context fits, so expansion never clips the matched evidence.
|
|
246
|
+
Run
|
|
241
247
|
`memory_sync_sessions` to start a refresh, then use `memory_sync_status` to
|
|
242
|
-
check its progress or result.
|
|
248
|
+
check its progress or result. The read-only adapter explicitly supports OpenClaw
|
|
249
|
+
agent database schemas 17, 18, and 19 and validates its required columns before
|
|
250
|
+
reading. Projections are private derived Markdown under the
|
|
243
251
|
agent's `unblock-memory/sessions` state directory and can be rebuilt from
|
|
244
252
|
OpenClaw at any time. Their embedded text contains only `# Transcript` and
|
|
245
|
-
timestamped speaker messages; filtering metadata remains in the
|
|
246
|
-
manifest. The projected file modification time matches the session
|
|
247
|
-
for meaningful chronological cluster reads. Session results include
|
|
248
|
-
chat type, conversation identity, and start time as an ISO 8601 timestamp. They
|
|
253
|
+
role-labeled, timestamped speaker messages; filtering metadata remains in the
|
|
254
|
+
session manifest. The projected file modification time matches the session
|
|
255
|
+
start time for meaningful chronological cluster reads. Session results include
|
|
256
|
+
provider, chat type, conversation identity, and start time as an ISO 8601 timestamp. They
|
|
249
257
|
participate in the same search and clustering index as file memory. This phase
|
|
250
258
|
does not sync sessions at startup or on a schedule; refreshes are manual through
|
|
251
259
|
`memory_sync_sessions`.
|
|
@@ -260,7 +268,7 @@ Markdown filesystem changes queue a debounced, serialized background refresh.
|
|
|
260
268
|
|
|
261
269
|
Analysis is opt-in. Core indexing, `memory_search`, and `memory_get` need only
|
|
262
270
|
Unblock Memory and its automatically installed QMD dependency. To enable
|
|
263
|
-
clustering, install the
|
|
271
|
+
clustering, install the public
|
|
264
272
|
[`unblock-cluster`](https://github.com/unblocklabs-ai/unblock-cluster) worker once
|
|
265
273
|
on the same host:
|
|
266
274
|
|
|
@@ -268,7 +276,7 @@ on the same host:
|
|
|
268
276
|
git clone https://github.com/unblocklabs-ai/unblock-cluster.git
|
|
269
277
|
cd unblock-cluster
|
|
270
278
|
python3 -m venv .venv
|
|
271
|
-
.venv/bin/python -m pip install -r requirements.txt
|
|
279
|
+
.venv/bin/python -m pip install -r requirements-analysis.txt
|
|
272
280
|
```
|
|
273
281
|
|
|
274
282
|
Set `analysis.executable` to the absolute path of
|
|
@@ -284,11 +292,12 @@ analyzed and `memory_recluster` reports that analysis is unavailable. Ordinary
|
|
|
284
292
|
memory search and reads continue to work.
|
|
285
293
|
|
|
286
294
|
The analysis worker reads QMD's existing semantic vectors and writes only
|
|
287
|
-
derived results into
|
|
295
|
+
derived results into four namespaced tables in that same `index.sqlite`:
|
|
288
296
|
|
|
289
297
|
- `memory_analysis_runs`
|
|
290
298
|
- `memory_analysis_clusters`
|
|
291
299
|
- `memory_analysis_memberships`
|
|
300
|
+
- `memory_analysis_duplicate_occurrences`
|
|
292
301
|
|
|
293
302
|
Unblock Memory exposes:
|
|
294
303
|
|
package/dist/src/config.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ type SessionCorpusConfig = {
|
|
|
14
14
|
name: "sessions";
|
|
15
15
|
kind: "sessions";
|
|
16
16
|
chatTypes: readonly ChatType[];
|
|
17
|
+
maxExpandedTokens: number;
|
|
17
18
|
};
|
|
18
19
|
export type CorpusConfig = FileCorpusConfig | SkillCorpusConfig | SessionCorpusConfig;
|
|
19
20
|
export declare const DEFAULT_CORPORA: readonly FileCorpusConfig[];
|
package/dist/src/config.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { isAbsolute } from "node:path";
|
|
2
2
|
const DEFAULT_PATHS = ["MEMORY.md", "USER.md", "memory/**/*.md"];
|
|
3
|
+
const DEFAULT_SESSION_MAX_EXPANDED_TOKENS = 500;
|
|
4
|
+
const MAX_SESSION_MAX_EXPANDED_TOKENS = 10_000;
|
|
3
5
|
const CHAT_TYPES = ["channel", "group", "direct"];
|
|
4
6
|
export const DEFAULT_CORPORA = [
|
|
5
7
|
{
|
|
@@ -59,7 +61,7 @@ function resolveCorpora(value) {
|
|
|
59
61
|
return { name: "skills", kind: "skills", paths: corpus.paths.map((path) => path.trim()) };
|
|
60
62
|
}
|
|
61
63
|
if (corpus.kind === "sessions") {
|
|
62
|
-
assertOnlyKeys(corpus, ["name", "kind", "chatTypes"], `corpora[${index}]`);
|
|
64
|
+
assertOnlyKeys(corpus, ["name", "kind", "chatTypes", "maxExpandedTokens"], `corpora[${index}]`);
|
|
63
65
|
if (name !== "sessions") {
|
|
64
66
|
throw new Error('unblock-memory session corpus must be named "sessions"');
|
|
65
67
|
}
|
|
@@ -73,6 +75,7 @@ function resolveCorpora(value) {
|
|
|
73
75
|
name: "sessions",
|
|
74
76
|
kind: "sessions",
|
|
75
77
|
chatTypes: [...new Set(chatTypes)],
|
|
78
|
+
maxExpandedTokens: positiveInteger(corpus.maxExpandedTokens, DEFAULT_SESSION_MAX_EXPANDED_TOKENS, "corpus sessions maxExpandedTokens", MAX_SESSION_MAX_EXPANDED_TOKENS),
|
|
76
79
|
};
|
|
77
80
|
}
|
|
78
81
|
assertOnlyKeys(corpus, ["name", "kind", "paths"], `corpora[${index}]`);
|
package/dist/src/manager.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { QMDStore } from "@unblocklabs/qmd";
|
|
1
|
+
import type { QMDStore, VectorSearchResult } from "@unblocklabs/qmd";
|
|
2
2
|
import { type AnalysisRunner, type MemoryAnalysisSummary, type MemoryClusterDetail, type MemoryClusterList, type MemoryClusterSort, type MemoryReclusterOptions } from "./analysis.js";
|
|
3
3
|
import type { CorpusMemorySearchResult, CorpusSearchOptions, MemoryEmbeddingProbeResult, MemoryProviderStatus, MemoryReadResult, MemoryRequestContext, MemorySearchManagerContract, MemorySyncParams } from "./contracts.js";
|
|
4
4
|
import type { ChatType } from "./config.js";
|
|
@@ -10,6 +10,7 @@ export type ManagerSessionConfig = {
|
|
|
10
10
|
agentId: string;
|
|
11
11
|
agentName: string;
|
|
12
12
|
chatTypes: readonly ChatType[];
|
|
13
|
+
maxExpandedTokens: number;
|
|
13
14
|
collection: string;
|
|
14
15
|
databasePath: string;
|
|
15
16
|
manifestPath: string;
|
|
@@ -30,6 +31,10 @@ export declare function buildReadResult(params: {
|
|
|
30
31
|
from?: number;
|
|
31
32
|
lines?: number;
|
|
32
33
|
}): MemoryReadResult;
|
|
34
|
+
export declare function expandSessionSearchHit(result: Pick<VectorSearchResult, "body" | "bestChunk" | "chunkPos" | "chunkLen">, maxTokens: number, countTokens: (text: string) => Promise<number>): Promise<{
|
|
35
|
+
text: string;
|
|
36
|
+
position: number;
|
|
37
|
+
}>;
|
|
33
38
|
export declare class QmdMemoryManager implements MemorySearchManagerContract {
|
|
34
39
|
#private;
|
|
35
40
|
constructor(params: {
|
package/dist/src/manager.js
CHANGED
|
@@ -6,6 +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 { sessionContextSpans } from "./session-projector.js";
|
|
9
10
|
import { parseSafeVirtualPath, sourceMatchesPath } from "./sources.js";
|
|
10
11
|
const DEFAULT_READ_LINES = 120;
|
|
11
12
|
const MAX_READ_CHARS = 12_000;
|
|
@@ -174,12 +175,27 @@ export function buildReadResult(params) {
|
|
|
174
175
|
...(nextFrom ? { nextFrom } : {}),
|
|
175
176
|
};
|
|
176
177
|
}
|
|
177
|
-
function lineSpan(
|
|
178
|
-
const before =
|
|
178
|
+
function lineSpan(body, position, text) {
|
|
179
|
+
const before = body.slice(0, position);
|
|
179
180
|
const startLine = before.split("\n").length;
|
|
180
|
-
const endLine = startLine + Math.max(0,
|
|
181
|
+
const endLine = startLine + Math.max(0, text.split("\n").length - 1);
|
|
181
182
|
return { startLine, endLine };
|
|
182
183
|
}
|
|
184
|
+
export async function expandSessionSearchHit(result, maxTokens, countTokens) {
|
|
185
|
+
const leaf = { text: result.bestChunk, position: result.chunkPos };
|
|
186
|
+
const spans = sessionContextSpans(result.body, result.chunkPos);
|
|
187
|
+
if (!spans)
|
|
188
|
+
return leaf;
|
|
189
|
+
const leafEnd = result.chunkPos + result.chunkLen;
|
|
190
|
+
for (const span of [spans.turn, spans.message]) {
|
|
191
|
+
if (span.start > result.chunkPos || span.end < leafEnd)
|
|
192
|
+
continue;
|
|
193
|
+
const text = result.body.slice(span.start, span.end).trimEnd();
|
|
194
|
+
if (await countTokens(text) <= maxTokens)
|
|
195
|
+
return { text, position: span.start };
|
|
196
|
+
}
|
|
197
|
+
return leaf;
|
|
198
|
+
}
|
|
183
199
|
function lexicalResult(hit, corpus, session) {
|
|
184
200
|
const body = hit.body ?? hit.title;
|
|
185
201
|
const endLine = Math.max(1, body.split("\n").length);
|
|
@@ -703,30 +719,36 @@ export class QmdMemoryManager {
|
|
|
703
719
|
allowedPaths,
|
|
704
720
|
expand: false,
|
|
705
721
|
});
|
|
706
|
-
|
|
722
|
+
const tokenizer = store.internal?.llm;
|
|
723
|
+
const results = [];
|
|
724
|
+
for (const hit of hits) {
|
|
707
725
|
const collection = /^qmd:\/\/([^/]+)\//.exec(hit.file)?.[1];
|
|
708
726
|
const corpus = collection ? this.#sources.get(collection)?.corpus : undefined;
|
|
709
727
|
if (!corpus)
|
|
710
|
-
|
|
711
|
-
const span = lineSpan(hit);
|
|
728
|
+
continue;
|
|
712
729
|
const relativePath = collection && hit.file.startsWith(`qmd://${collection}/`)
|
|
713
730
|
? hit.file.slice(`qmd://${collection}/`.length)
|
|
714
731
|
: undefined;
|
|
715
732
|
const session = corpus === "sessions" && relativePath
|
|
716
733
|
? this.#sessionMetadata.get(relativePath)
|
|
717
734
|
: undefined;
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
735
|
+
const selected = corpus === "sessions" && this.#sessions && tokenizer
|
|
736
|
+
? await expandSessionSearchHit(hit, this.#sessions.maxExpandedTokens, (text) => tokenizer.countTokens(text))
|
|
737
|
+
: { text: hit.bestChunk, position: hit.chunkPos };
|
|
738
|
+
const span = lineSpan(hit.body, selected.position, selected.text);
|
|
739
|
+
results.push({
|
|
740
|
+
path: hit.file,
|
|
741
|
+
...span,
|
|
742
|
+
score: hit.score,
|
|
743
|
+
vectorScore: hit.score,
|
|
744
|
+
snippet: selected.text,
|
|
745
|
+
source: "memory",
|
|
746
|
+
corpus,
|
|
747
|
+
...(session ? { session } : {}),
|
|
748
|
+
citation: `${hit.displayPath}#L${span.startLine}-L${span.endLine}`,
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
return results;
|
|
730
752
|
}
|
|
731
753
|
async searchSkills(query, minScore, limit) {
|
|
732
754
|
const collections = this.#skillCollectionNames();
|
package/dist/src/runtime.js
CHANGED
|
@@ -224,6 +224,7 @@ export class QmdMemoryRuntime {
|
|
|
224
224
|
agentId,
|
|
225
225
|
agentName: resolveAgentIdentity(cfg, agentId)?.name?.trim() || agentId,
|
|
226
226
|
chatTypes: sessionCorpus.chatTypes,
|
|
227
|
+
maxExpandedTokens: sessionCorpus.maxExpandedTokens,
|
|
227
228
|
collection: sessionSource.collection,
|
|
228
229
|
databasePath: join(resolveAgentDir(cfg, agentId), "openclaw-agent.sqlite"),
|
|
229
230
|
manifestPath: join(stateDir, "sessions-manifest.json"),
|
|
@@ -16,6 +16,17 @@ export type SessionProjectionInput = SessionMetadata & {
|
|
|
16
16
|
createdAt: number;
|
|
17
17
|
}[];
|
|
18
18
|
};
|
|
19
|
+
export type SessionContextSpans = {
|
|
20
|
+
message: {
|
|
21
|
+
start: number;
|
|
22
|
+
end: number;
|
|
23
|
+
};
|
|
24
|
+
turn: {
|
|
25
|
+
start: number;
|
|
26
|
+
end: number;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
19
29
|
export declare function projectSession(input: SessionProjectionInput): string | undefined;
|
|
30
|
+
export declare function sessionContextSpans(content: string, position: number): SessionContextSpans | undefined;
|
|
20
31
|
export declare function sessionDocumentPath(metadata: SessionMetadata): string;
|
|
21
32
|
export declare function resolveTimezone(configured?: string): string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
const MESSAGE_HEADING = /^## (User|Assistant) — .* — \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} \S.*$/gmu;
|
|
2
3
|
function record(value) {
|
|
3
4
|
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
4
5
|
? value
|
|
@@ -79,6 +80,7 @@ function projectMessage(row, input) {
|
|
|
79
80
|
if (!text)
|
|
80
81
|
return undefined;
|
|
81
82
|
return {
|
|
83
|
+
role,
|
|
82
84
|
speaker: speaker.replace(/[\r\n]+/gu, " "),
|
|
83
85
|
text,
|
|
84
86
|
timestamp: timestamp(eventRecord.timestamp) ?? row.createdAt ?? timestamp(message.timestamp) ?? input.startedAt,
|
|
@@ -107,9 +109,36 @@ export function projectSession(input) {
|
|
|
107
109
|
});
|
|
108
110
|
if (messages.length === 0)
|
|
109
111
|
return undefined;
|
|
110
|
-
const transcript = messages.map((message) =>
|
|
112
|
+
const transcript = messages.map((message) => `## ${message.role === "user" ? "User" : "Assistant"} — ${message.speaker} — ` +
|
|
113
|
+
`${formatTimestamp(message.timestamp, input.timezone)}\n\n${message.text}`);
|
|
111
114
|
return `# Transcript\n\n${transcript.join("\n\n")}\n`;
|
|
112
115
|
}
|
|
116
|
+
export function sessionContextSpans(content, position) {
|
|
117
|
+
const markers = [...content.matchAll(MESSAGE_HEADING)].map((match) => ({
|
|
118
|
+
start: match.index,
|
|
119
|
+
role: match[1] === "User" ? "user" : "assistant",
|
|
120
|
+
}));
|
|
121
|
+
const containing = markers.findLastIndex((marker) => marker.start <= position);
|
|
122
|
+
if (containing < 0)
|
|
123
|
+
return undefined;
|
|
124
|
+
const message = {
|
|
125
|
+
start: markers[containing].start,
|
|
126
|
+
end: markers[containing + 1]?.start ?? content.length,
|
|
127
|
+
};
|
|
128
|
+
let turnStart = containing;
|
|
129
|
+
while (turnStart > 0 && markers[turnStart].role !== "user")
|
|
130
|
+
turnStart -= 1;
|
|
131
|
+
if (markers[turnStart].role !== "user")
|
|
132
|
+
turnStart = containing;
|
|
133
|
+
const nextUser = markers.findIndex((marker, index) => index > turnStart && marker.role === "user");
|
|
134
|
+
return {
|
|
135
|
+
message,
|
|
136
|
+
turn: {
|
|
137
|
+
start: markers[turnStart].start,
|
|
138
|
+
end: nextUser < 0 ? content.length : markers[nextUser].start,
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
113
142
|
function hash(value) {
|
|
114
143
|
return createHash("sha256").update(value).digest("hex").slice(0, 16);
|
|
115
144
|
}
|
package/dist/src/session-sync.js
CHANGED
|
@@ -5,8 +5,8 @@ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
|
5
5
|
import { DatabaseSync } from "node:sqlite";
|
|
6
6
|
import { projectSession, sessionDocumentPath, } from "./session-projector.js";
|
|
7
7
|
const MANIFEST_VERSION = 1;
|
|
8
|
-
const PROJECTOR_VERSION =
|
|
9
|
-
const
|
|
8
|
+
const PROJECTOR_VERSION = 3;
|
|
9
|
+
const SUPPORTED_SCHEMA_VERSIONS = new Set([17, 18, 19]);
|
|
10
10
|
const REQUIRED_COLUMNS = {
|
|
11
11
|
schema_meta: ["meta_key", "role", "schema_version", "agent_id", "app_version"],
|
|
12
12
|
session_windows: [
|
|
@@ -45,9 +45,10 @@ function projectionPath(outputDir, documentPath) {
|
|
|
45
45
|
}
|
|
46
46
|
function assertSchema(db, expectedAgentId) {
|
|
47
47
|
const pragma = db.prepare("PRAGMA user_version").get();
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
const schemaVersion = pragma?.user_version;
|
|
49
|
+
if (typeof schemaVersion !== "number" || !SUPPORTED_SCHEMA_VERSIONS.has(schemaVersion)) {
|
|
50
|
+
throw new Error("unsupported OpenClaw agent database schema: expected one of 17, 18, 19, " +
|
|
51
|
+
`found ${String(schemaVersion ?? "unknown")}`);
|
|
51
52
|
}
|
|
52
53
|
for (const [table, required] of Object.entries(REQUIRED_COLUMNS)) {
|
|
53
54
|
const columns = new Set(db.prepare(`PRAGMA table_info(${table})`).all()
|
|
@@ -58,7 +59,7 @@ function assertSchema(db, expectedAgentId) {
|
|
|
58
59
|
}
|
|
59
60
|
const meta = db.prepare("SELECT role, schema_version AS schemaVersion, agent_id AS agentId " +
|
|
60
61
|
"FROM schema_meta WHERE meta_key = 'primary' LIMIT 1").get();
|
|
61
|
-
if (meta?.role !== "agent" || meta.schemaVersion !==
|
|
62
|
+
if (meta?.role !== "agent" || meta.schemaVersion !== schemaVersion) {
|
|
62
63
|
throw new Error("unsupported OpenClaw agent database primary schema metadata");
|
|
63
64
|
}
|
|
64
65
|
if (meta.agentId !== expectedAgentId) {
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "unblock-memory",
|
|
3
3
|
"name": "Unblock Memory",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.11",
|
|
5
5
|
"description": "Indexes, retrieves, and analyzes configured workspace memory with existing QMD vectors.",
|
|
6
6
|
"kind": "memory",
|
|
7
7
|
"activation": { "onStartup": false },
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
},
|
|
42
42
|
"corpora": {
|
|
43
43
|
"label": "Memory Corpora",
|
|
44
|
-
"help": "Named groups of exact Markdown files, directories, or globs. Relative paths resolve from each agent workspace. Skill files use the isolated skills corpus."
|
|
44
|
+
"help": "Named groups of exact Markdown files, directories, or globs. Relative paths resolve from each agent workspace. Skill files use the isolated skills corpus. Session corpora can expand matching semantic chunks up to maxExpandedTokens."
|
|
45
45
|
},
|
|
46
46
|
"skillWhisperer.enabled": {
|
|
47
47
|
"label": "Skill Whisperer",
|
|
@@ -99,6 +99,12 @@
|
|
|
99
99
|
"minItems": 1,
|
|
100
100
|
"items": { "enum": ["channel", "group", "direct"] },
|
|
101
101
|
"default": ["channel", "group"]
|
|
102
|
+
},
|
|
103
|
+
"maxExpandedTokens": {
|
|
104
|
+
"type": "integer",
|
|
105
|
+
"minimum": 1,
|
|
106
|
+
"maximum": 10000,
|
|
107
|
+
"default": 500
|
|
102
108
|
}
|
|
103
109
|
}
|
|
104
110
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unblocklabs/unblock-memory",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.11",
|
|
4
4
|
"description": "Workspace-native memory for OpenClaw, powered by QMD",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -35,18 +35,18 @@
|
|
|
35
35
|
"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"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@unblocklabs/qmd": "https://github.com/unblocklabs-ai/qmd/releases/download/v2.9.
|
|
38
|
+
"@unblocklabs/qmd": "https://github.com/unblocklabs-ai/qmd/releases/download/v2.9.4/unblocklabs-qmd-2.9.4.tgz",
|
|
39
39
|
"chokidar": "5.0.0",
|
|
40
40
|
"picomatch": "^4.0.5",
|
|
41
41
|
"typebox": "1.3.6"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@openclaw/ai": "2026.8.1
|
|
44
|
+
"@openclaw/ai": "2026.8.1",
|
|
45
45
|
"@openclaw/plugin-inspector": "^0.3.10",
|
|
46
46
|
"@types/node": "^24.6.0",
|
|
47
47
|
"@types/picomatch": "^4.0.2",
|
|
48
48
|
"knip": "^6.32.2",
|
|
49
|
-
"openclaw": "2026.8.1
|
|
49
|
+
"openclaw": "2026.8.1",
|
|
50
50
|
"tsx": "^4.20.6",
|
|
51
51
|
"typescript": "^5.9.3"
|
|
52
52
|
},
|
|
@@ -70,8 +70,8 @@
|
|
|
70
70
|
"minGatewayVersion": "2026.8.1-beta.3"
|
|
71
71
|
},
|
|
72
72
|
"build": {
|
|
73
|
-
"openclawVersion": "2026.8.1
|
|
74
|
-
"pluginSdkVersion": "2026.8.1
|
|
73
|
+
"openclawVersion": "2026.8.1",
|
|
74
|
+
"pluginSdkVersion": "2026.8.1"
|
|
75
75
|
},
|
|
76
76
|
"install": {
|
|
77
77
|
"npmSpec": "@unblocklabs/unblock-memory",
|
|
@@ -10,6 +10,20 @@ understanding of its world. A cluster shows similarity, not a complete timeline,
|
|
|
10
10
|
truth, or consensus. Do not write from a cluster alone, and prefer no write over
|
|
11
11
|
weak, duplicative, or easily looked-up knowledge.
|
|
12
12
|
|
|
13
|
+
## Clusters and the knowledge corpus
|
|
14
|
+
|
|
15
|
+
Clustering and knowledge are separate. `memory_recluster` analyzes every
|
|
16
|
+
configured non-skill corpus, so a cluster may connect raw memory, sessions,
|
|
17
|
+
meeting notes, other source material, and previously maintained knowledge.
|
|
18
|
+
|
|
19
|
+
`knowledge` is not a special cluster type. It is the dedicated indexed corpus
|
|
20
|
+
for the agent's maintained synthesis, normally backed by
|
|
21
|
+
`knowledge/**/*.md`. Source corpora preserve what happened or was recorded;
|
|
22
|
+
the knowledge corpus preserves the agent's supported current understanding
|
|
23
|
+
when that understanding would be expensive to reconstruct. A cluster is only
|
|
24
|
+
an invitation to investigate, not evidence that something belongs in
|
|
25
|
+
knowledge.
|
|
26
|
+
|
|
13
27
|
## Investigate
|
|
14
28
|
|
|
15
29
|
1. Call `memory_list_clusters`. If analysis is missing or stale, call
|
|
@@ -21,7 +35,7 @@ weak, duplicative, or easily looked-up knowledge.
|
|
|
21
35
|
fallback when `eventTime` is unresolved.
|
|
22
36
|
3. State the question the cluster raises: what may be repeated, contradictory,
|
|
23
37
|
changing, or worth understanding?
|
|
24
|
-
4. Search existing knowledge with `memory_search`, using
|
|
38
|
+
4. Search existing maintained knowledge with `memory_search`, using
|
|
25
39
|
`corpora: ["knowledge"]`. If that corpus is not configured, report that and
|
|
26
40
|
do not create an unindexed file.
|
|
27
41
|
5. Investigate the evidence needed to answer the question. Follow important
|