@unblocklabs/unblock-memory 0.2.5 → 0.2.6
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 +52 -39
- package/dist/src/analysis.d.ts +10 -1
- package/dist/src/analysis.js +49 -19
- package/dist/src/manager.d.ts +3 -1
- package/dist/src/manager.js +1 -1
- package/dist/src/plugin.js +11 -3
- package/dist/src/session-projector.js +1 -18
- package/dist/src/session-sync.js +3 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/skills/memory-curator/SKILL.md +63 -66
package/README.md
CHANGED
|
@@ -53,14 +53,9 @@ directories, or globs into named corpora:
|
|
|
53
53
|
chatTypes: ["channel", "group"],
|
|
54
54
|
},
|
|
55
55
|
{
|
|
56
|
-
name: "
|
|
56
|
+
name: "knowledge",
|
|
57
57
|
kind: "files",
|
|
58
|
-
paths: ["knowledge
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
name: "reflections",
|
|
62
|
-
kind: "files",
|
|
63
|
-
paths: ["knowledge/reflections/**/*.md"],
|
|
58
|
+
paths: ["knowledge/**/*.md"],
|
|
64
59
|
},
|
|
65
60
|
],
|
|
66
61
|
// Optional: omit unless the local analysis worker is installed.
|
|
@@ -85,7 +80,7 @@ context resident after first use. Set it to `false` to restore QMD's five-minute
|
|
|
85
80
|
idle unload behavior.
|
|
86
81
|
|
|
87
82
|
`memory_search` searches every configured corpus by default. Pass
|
|
88
|
-
`corpora: ["
|
|
83
|
+
`corpora: ["knowledge"]` to search selected corpora or `corpora: ["all"]` to
|
|
89
84
|
request all of them explicitly. Search results include their corpus name and
|
|
90
85
|
remain readable by passing the returned `qmd://` path to `memory_get`.
|
|
91
86
|
|
|
@@ -115,12 +110,15 @@ The optional `sessions` corpus reads the current agent's normal OpenClaw SQLite
|
|
|
115
110
|
store and indexes its active user/assistant transcript branch. It defaults to
|
|
116
111
|
channel and group conversations; add `direct` explicitly to include DMs. Run
|
|
117
112
|
`memory_sync_sessions` to start a refresh, then use `memory_sync_status` to
|
|
118
|
-
check its progress or result. Projections are private derived Markdown
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
113
|
+
check its progress or result. Projections are private derived Markdown under the
|
|
114
|
+
agent's `unblock-memory/sessions` state directory and can be rebuilt from
|
|
115
|
+
OpenClaw at any time. Their embedded text contains only `# Transcript` and
|
|
116
|
+
timestamped speaker messages; filtering metadata remains in the session
|
|
117
|
+
manifest. The projected file modification time matches the session start time
|
|
118
|
+
for meaningful chronological cluster reads. Session results include provider,
|
|
119
|
+
chat type, conversation identity, and start time as an ISO 8601 timestamp. They
|
|
120
|
+
participate in the same search and clustering index as file memory. This phase
|
|
121
|
+
does not sync sessions at startup or on a schedule; refreshes are manual through
|
|
124
122
|
`memory_sync_sessions`.
|
|
125
123
|
|
|
126
124
|
Indexes live at `~/.openclaw/agents/<agentId>/unblock-memory/index.sqlite` (or the
|
|
@@ -166,17 +164,26 @@ Unblock Memory exposes:
|
|
|
166
164
|
- `memory_list_clusters` to cheaply list current clusters and report whether the
|
|
167
165
|
retained analysis is stale
|
|
168
166
|
- `memory_recluster` to explicitly rebuild clusters when the list is missing or stale
|
|
169
|
-
- `memory_fetch_cluster` to return
|
|
170
|
-
short `clusterId` returned by `memory_list_clusters`
|
|
167
|
+
- `memory_fetch_cluster` to return a sorted, paginated selection of QMD chunks
|
|
168
|
+
for a short `clusterId` returned by `memory_list_clusters`
|
|
171
169
|
|
|
172
170
|
`memory_recluster` optionally accepts UMAP controls (`method`, components,
|
|
173
171
|
neighbors, and minimum distance), HDBSCAN controls (minimum cluster size,
|
|
174
172
|
minimum samples, selection method and epsilon, and single-cluster behavior),
|
|
175
173
|
and a deterministic seed. Omitting them uses the worker's defaults.
|
|
176
174
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
175
|
+
`memory_fetch_cluster` accepts `topK` (1–50), a zero-based `offset`, and
|
|
176
|
+
`sort`: `representative` (the default), `score_desc`, `score_asc`, `date_desc`,
|
|
177
|
+
or `date_asc`. Score is cluster membership probability for normal clusters and
|
|
178
|
+
outlier score for noise. Each member includes `sourceDate`, the latest
|
|
179
|
+
modification time among its active source aliases. For projected sessions that
|
|
180
|
+
date is the session start time. Responses include page totals and the next
|
|
181
|
+
offset when more members remain.
|
|
182
|
+
|
|
183
|
+
Member excerpts are capped at 2 KB each and 12 KB across a response; source
|
|
184
|
+
aliases are capped at five per member and 50 across a response. These budgets
|
|
185
|
+
are shared across the page so every returned member receives a useful excerpt
|
|
186
|
+
and at least one source path, including a full 50-member page.
|
|
180
187
|
|
|
181
188
|
If indexing changes content or vectors, the previous derived analysis is kept
|
|
182
189
|
and marked stale. Cluster reads include the analysis timestamp, stale timestamp,
|
|
@@ -186,33 +193,37 @@ A failed rebuild leaves the stale result intact, while a successful rebuild
|
|
|
186
193
|
atomically replaces it. Analysis is never scheduled automatically. If the worker
|
|
187
194
|
is absent or fails, `memory_search` and `memory_get` continue to work.
|
|
188
195
|
|
|
189
|
-
## Curating
|
|
196
|
+
## Curating knowledge
|
|
190
197
|
|
|
191
198
|
The plugin bundles the `memory-curator` skill for turning useful clusters into
|
|
192
199
|
durable knowledge. It becomes available when the plugin is enabled. If the
|
|
193
200
|
agent has an explicit skill allowlist, include `memory-curator`.
|
|
194
201
|
|
|
195
|
-
Keep
|
|
202
|
+
Keep maintained knowledge outside `memory/**` so each file belongs to only one
|
|
203
|
+
corpus. Use stable topic files updated in place:
|
|
196
204
|
|
|
197
205
|
```text
|
|
198
206
|
knowledge/
|
|
199
|
-
├──
|
|
200
|
-
|
|
201
|
-
└──
|
|
202
|
-
|
|
207
|
+
├── fleet.md
|
|
208
|
+
├── people/
|
|
209
|
+
│ └── rico.md
|
|
210
|
+
└── projects/
|
|
211
|
+
└── unblock-memory.md
|
|
203
212
|
```
|
|
204
213
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
and
|
|
208
|
-
|
|
209
|
-
|
|
214
|
+
Knowledge is the agent's maintained, current understanding of its unique world:
|
|
215
|
+
facts such as fleet membership, local decisions and preferences, assessments,
|
|
216
|
+
and explicit uncertainty that would be expensive to reconstruct from scattered
|
|
217
|
+
history. Each claim should carry its own epistemic qualification so it remains
|
|
218
|
+
honest when semantic chunking retrieves it alone. Remove stale conclusions
|
|
219
|
+
instead of preserving history, changelogs, or `Supersedes` passages in the same
|
|
220
|
+
file; raw memory and sessions retain the evidence history.
|
|
210
221
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
authoritative.
|
|
214
|
-
|
|
215
|
-
|
|
222
|
+
Public or vendor-owned facts, generic command syntax, and behavior likely to
|
|
223
|
+
change with third-party releases should normally be looked up from the current
|
|
224
|
+
authoritative source. A local policy or deliberate divergence may belong in
|
|
225
|
+
knowledge, but the local decision—not copied generic documentation—is the
|
|
226
|
+
durable content.
|
|
216
227
|
|
|
217
228
|
For a manual run, ask the agent:
|
|
218
229
|
|
|
@@ -226,10 +237,12 @@ For recurring curation, use an OpenClaw automation with the same thin message:
|
|
|
226
237
|
Use $memory-curator to run the scheduled memory curation cycle.
|
|
227
238
|
```
|
|
228
239
|
|
|
229
|
-
The skill
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
240
|
+
The skill treats a cluster as an incomplete attention signal. It frames the
|
|
241
|
+
question raised, uses representative, score, and chronological views as useful,
|
|
242
|
+
searches existing knowledge and adjacent corpora, and investigates live systems,
|
|
243
|
+
files, documentation, or the web when those are better evidence. It then updates
|
|
244
|
+
a stable knowledge topic or correctly writes nothing. Its own writes are indexed
|
|
245
|
+
for the next cycle; it does not recluster recursively in the same run.
|
|
233
246
|
|
|
234
247
|
Existing `unblock-qmd` indexes are derived caches and may be left in place;
|
|
235
248
|
Unblock Memory rebuilds its own index from configured corpora.
|
package/dist/src/analysis.d.ts
CHANGED
|
@@ -30,9 +30,11 @@ type MemoryAnalysisMember = {
|
|
|
30
30
|
x: number;
|
|
31
31
|
y: number;
|
|
32
32
|
representativeRank: number | null;
|
|
33
|
+
sourceDate: string;
|
|
33
34
|
text: string;
|
|
34
35
|
sourcePaths: string[];
|
|
35
36
|
};
|
|
37
|
+
export type MemoryClusterSort = "representative" | "score_desc" | "score_asc" | "date_desc" | "date_asc";
|
|
36
38
|
export type MemoryAnalysisSummary = {
|
|
37
39
|
status: "ok";
|
|
38
40
|
runId: string;
|
|
@@ -72,6 +74,13 @@ export type MemoryClusterDetail = AnalysisReadMetadata & {
|
|
|
72
74
|
runId?: string;
|
|
73
75
|
cluster?: Omit<MemoryClusterSummary, "preview">;
|
|
74
76
|
members?: MemoryAnalysisMember[];
|
|
77
|
+
page?: {
|
|
78
|
+
offset: number;
|
|
79
|
+
returned: number;
|
|
80
|
+
total: number;
|
|
81
|
+
hasMore: boolean;
|
|
82
|
+
nextOffset?: number;
|
|
83
|
+
};
|
|
75
84
|
};
|
|
76
85
|
export declare function ensureMemoryAnalysisSchema(db: AnalysisDatabase): void;
|
|
77
86
|
export declare function markMemoryAnalysisStale(db: AnalysisDatabase): void;
|
|
@@ -85,5 +94,5 @@ export declare function runAnalysisWorker(params: {
|
|
|
85
94
|
export declare function latestAnalysisRunId(db: AnalysisDatabase): string | undefined;
|
|
86
95
|
export declare function readAnalysisSummary(db: AnalysisDatabase): MemoryAnalysisSummary | undefined;
|
|
87
96
|
export declare function readClusters(db: AnalysisDatabase, requestedLimit?: number): MemoryClusterList;
|
|
88
|
-
export declare function readCluster(db: AnalysisDatabase, clusterReferenceId: string, requestedLimit?: number): MemoryClusterDetail;
|
|
97
|
+
export declare function readCluster(db: AnalysisDatabase, clusterReferenceId: string, requestedLimit?: number, requestedOffset?: number, sort?: MemoryClusterSort): MemoryClusterDetail;
|
|
89
98
|
export {};
|
package/dist/src/analysis.js
CHANGED
|
@@ -208,8 +208,8 @@ function byteSlice(text, maxBytes) {
|
|
|
208
208
|
return "";
|
|
209
209
|
return bytes.subarray(0, maxBytes - 3).toString("utf8").replace(/\uFFFD$/u, "") + "…";
|
|
210
210
|
}
|
|
211
|
-
function members(db, runId, clusterId, limit, maxExcerptBytes = MAX_EXCERPT_BYTES, maxTotalBytes = MAX_TOTAL_EXCERPT_BYTES, maxTotalAliases = MAX_TOTAL_ALIASES) {
|
|
212
|
-
const
|
|
211
|
+
function members(db, runId, clusterId, limit, offset = 0, sort = "representative", maxExcerptBytes = MAX_EXCERPT_BYTES, maxTotalBytes = MAX_TOTAL_EXCERPT_BYTES, maxTotalAliases = MAX_TOTAL_ALIASES) {
|
|
212
|
+
const representativeOrder = clusterId === -1
|
|
213
213
|
? "m.outlier_score DESC, m.hash, m.seq"
|
|
214
214
|
: `CASE WHEN m.representative_rank IS NULL THEN 1 ELSE 0 END,
|
|
215
215
|
m.representative_rank,
|
|
@@ -217,23 +217,40 @@ function members(db, runId, clusterId, limit, maxExcerptBytes = MAX_EXCERPT_BYTE
|
|
|
217
217
|
m.outlier_score,
|
|
218
218
|
m.hash,
|
|
219
219
|
m.seq`;
|
|
220
|
+
const score = clusterId === -1 ? "m.outlier_score" : "m.probability";
|
|
221
|
+
const order = {
|
|
222
|
+
representative: representativeOrder,
|
|
223
|
+
score_desc: `${score} DESC, m.hash, m.seq`,
|
|
224
|
+
score_asc: `${score} ASC, m.hash, m.seq`,
|
|
225
|
+
date_desc: "m.source_date DESC, m.hash, m.seq",
|
|
226
|
+
date_asc: "m.source_date ASC, m.hash, m.seq",
|
|
227
|
+
}[sort];
|
|
220
228
|
const rows = db.prepare(`
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
+
WITH member_rows AS (
|
|
230
|
+
SELECT
|
|
231
|
+
m.hash, m.seq, m.probability, m.outlier_score, m.x, m.y,
|
|
232
|
+
m.representative_rank, m.pos, m.chunk_len, m.doc,
|
|
233
|
+
(
|
|
234
|
+
SELECT MAX(d.modified_at)
|
|
235
|
+
FROM documents d
|
|
236
|
+
WHERE d.hash = m.hash AND d.active = 1
|
|
237
|
+
) AS source_date
|
|
238
|
+
FROM memory_analysis_available_memberships m
|
|
239
|
+
WHERE m.run_id = ? AND m.cluster_id = ?
|
|
240
|
+
)
|
|
241
|
+
SELECT * FROM member_rows m
|
|
242
|
+
ORDER BY ${order}
|
|
243
|
+
LIMIT ? OFFSET ?
|
|
244
|
+
`).all(runId, clusterId, limit, offset);
|
|
229
245
|
let remaining = maxTotalBytes;
|
|
230
246
|
let remainingAliases = maxTotalAliases;
|
|
231
|
-
return rows.map((row) => {
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
247
|
+
return rows.map((row, index) => {
|
|
248
|
+
const remainingRows = rows.length - index;
|
|
249
|
+
const excerptBudget = Math.min(maxExcerptBytes, Math.floor(remaining / remainingRows));
|
|
250
|
+
const text = byteSlice(row.doc.slice(row.pos, row.pos + row.chunk_len), excerptBudget);
|
|
235
251
|
remaining -= Buffer.byteLength(text);
|
|
236
|
-
const
|
|
252
|
+
const aliasBudget = Math.min(MAX_ALIASES_PER_MEMBER, Math.floor(remainingAliases / remainingRows));
|
|
253
|
+
const aliases = sourcePaths(db, row.hash, aliasBudget);
|
|
237
254
|
remainingAliases -= aliases.length;
|
|
238
255
|
return {
|
|
239
256
|
hash: row.hash,
|
|
@@ -243,6 +260,7 @@ function members(db, runId, clusterId, limit, maxExcerptBytes = MAX_EXCERPT_BYTE
|
|
|
243
260
|
x: row.x,
|
|
244
261
|
y: row.y,
|
|
245
262
|
representativeRank: row.representative_rank,
|
|
263
|
+
sourceDate: row.source_date,
|
|
246
264
|
text,
|
|
247
265
|
sourcePaths: aliases,
|
|
248
266
|
};
|
|
@@ -276,7 +294,7 @@ function readMetadata(run) {
|
|
|
276
294
|
}
|
|
277
295
|
function toSummary(db, run, row, includePreview, previewBytes = 600, aliasLimit = MAX_TOTAL_ALIASES) {
|
|
278
296
|
const preview = includePreview
|
|
279
|
-
? members(db, run.id, row.cluster_id, 1, previewBytes, previewBytes, aliasLimit)[0]
|
|
297
|
+
? members(db, run.id, row.cluster_id, 1, 0, "representative", previewBytes, previewBytes, aliasLimit)[0]
|
|
280
298
|
: undefined;
|
|
281
299
|
return {
|
|
282
300
|
clusterId: clusterReference(run.id, row.cluster_id),
|
|
@@ -362,7 +380,7 @@ function resolveClusterId(db, runId, reference) {
|
|
|
362
380
|
`).all(runId, runId);
|
|
363
381
|
return clusterIds.find((row) => clusterReference(runId, row.cluster_id) === reference)?.cluster_id;
|
|
364
382
|
}
|
|
365
|
-
export function readCluster(db, clusterReferenceId, requestedLimit = DEFAULT_MEMBER_LIMIT) {
|
|
383
|
+
export function readCluster(db, clusterReferenceId, requestedLimit = DEFAULT_MEMBER_LIMIT, requestedOffset = 0, sort = "representative") {
|
|
366
384
|
const run = latestValidRun(db);
|
|
367
385
|
if (!run) {
|
|
368
386
|
return { status: "not_analyzed", ...readMetadata() };
|
|
@@ -388,6 +406,11 @@ export function readCluster(db, clusterReferenceId, requestedLimit = DEFAULT_MEM
|
|
|
388
406
|
return { status: "not_found", runId: run.id, ...metadata };
|
|
389
407
|
}
|
|
390
408
|
const limit = Math.max(1, Math.min(MAX_MEMBER_LIMIT, Math.floor(requestedLimit)));
|
|
409
|
+
const offset = Math.max(0, Math.floor(requestedOffset));
|
|
410
|
+
const total = availableSize(db, run.id, clusterId);
|
|
411
|
+
const pageMembers = members(db, run.id, clusterId, limit, offset, sort);
|
|
412
|
+
const nextOffset = offset + pageMembers.length;
|
|
413
|
+
const hasMore = nextOffset < total;
|
|
391
414
|
return {
|
|
392
415
|
status: "ok",
|
|
393
416
|
runId: run.id,
|
|
@@ -395,9 +418,16 @@ export function readCluster(db, clusterReferenceId, requestedLimit = DEFAULT_MEM
|
|
|
395
418
|
cluster: {
|
|
396
419
|
clusterId: clusterReferenceId,
|
|
397
420
|
size: row.size,
|
|
398
|
-
availableSize:
|
|
421
|
+
availableSize: total,
|
|
399
422
|
meanProbability: row.mean_probability,
|
|
400
423
|
},
|
|
401
|
-
members:
|
|
424
|
+
members: pageMembers,
|
|
425
|
+
page: {
|
|
426
|
+
offset,
|
|
427
|
+
returned: pageMembers.length,
|
|
428
|
+
total,
|
|
429
|
+
hasMore,
|
|
430
|
+
...(hasMore ? { nextOffset } : {}),
|
|
431
|
+
},
|
|
402
432
|
};
|
|
403
433
|
}
|
package/dist/src/manager.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { QMDStore } from "@unblocklabs/qmd";
|
|
2
|
-
import { type AnalysisRunner, type MemoryAnalysisSummary, type MemoryClusterDetail, type MemoryClusterList, type MemoryReclusterOptions } from "./analysis.js";
|
|
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, MemorySearchManagerContract, MemorySyncParams } from "./contracts.js";
|
|
4
4
|
import type { ChatType } from "./config.js";
|
|
5
5
|
import { type SessionSyncResult } from "./session-sync.js";
|
|
@@ -44,6 +44,8 @@ export declare class QmdMemoryManager implements MemorySearchManagerContract {
|
|
|
44
44
|
fetchCluster(params: {
|
|
45
45
|
clusterId: string;
|
|
46
46
|
topK?: number;
|
|
47
|
+
offset?: number;
|
|
48
|
+
sort?: MemoryClusterSort;
|
|
47
49
|
}): Promise<MemoryClusterDetail>;
|
|
48
50
|
search(query: string, opts?: CorpusSearchOptions): Promise<CorpusMemorySearchResult[]>;
|
|
49
51
|
readFile(params: {
|
package/dist/src/manager.js
CHANGED
|
@@ -391,7 +391,7 @@ export class QmdMemoryManager {
|
|
|
391
391
|
return this.#enqueue(async () => readClusters((await this.#getAnalysisStore()).internal.db, limit));
|
|
392
392
|
}
|
|
393
393
|
fetchCluster(params) {
|
|
394
|
-
return this.#enqueue(async () => readCluster((await this.#getAnalysisStore()).internal.db, params.clusterId, params.topK));
|
|
394
|
+
return this.#enqueue(async () => readCluster((await this.#getAnalysisStore()).internal.db, params.clusterId, params.topK, params.offset, params.sort));
|
|
395
395
|
}
|
|
396
396
|
async #getAnalysisStore() {
|
|
397
397
|
const store = await this.#getStore();
|
package/dist/src/plugin.js
CHANGED
|
@@ -192,6 +192,14 @@ function createListClustersTool(runtime, ctx) {
|
|
|
192
192
|
const fetchClusterParameters = Type.Object({
|
|
193
193
|
clusterId: Type.String({ pattern: "^[0-9a-f]{10}$" }),
|
|
194
194
|
topK: Type.Optional(Type.Integer({ minimum: 1, maximum: 50 })),
|
|
195
|
+
offset: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
196
|
+
sort: Type.Optional(Type.Union([
|
|
197
|
+
Type.Literal("representative"),
|
|
198
|
+
Type.Literal("score_desc"),
|
|
199
|
+
Type.Literal("score_asc"),
|
|
200
|
+
Type.Literal("date_desc"),
|
|
201
|
+
Type.Literal("date_asc"),
|
|
202
|
+
])),
|
|
195
203
|
}, { additionalProperties: false });
|
|
196
204
|
function createFetchClusterTool(runtime, ctx) {
|
|
197
205
|
const active = getContext(ctx);
|
|
@@ -200,14 +208,14 @@ function createFetchClusterTool(runtime, ctx) {
|
|
|
200
208
|
return {
|
|
201
209
|
name: "memory_fetch_cluster",
|
|
202
210
|
label: "Fetch Memory Cluster",
|
|
203
|
-
description: "Fetch
|
|
211
|
+
description: "Fetch a sorted page of QMD chunks for a clusterId returned by memory_list_clusters.",
|
|
204
212
|
parameters: fetchClusterParameters,
|
|
205
213
|
async execute(_toolCallId, params) {
|
|
206
|
-
const { clusterId, topK } = Value.Parse(fetchClusterParameters, params);
|
|
214
|
+
const { clusterId, topK, offset, sort } = Value.Parse(fetchClusterParameters, params);
|
|
207
215
|
const { manager, error } = await runtime.getMemorySearchManager(active);
|
|
208
216
|
if (!manager)
|
|
209
217
|
return jsonResult({ status: "unavailable", error: error ?? "memory unavailable" });
|
|
210
|
-
return jsonResult(await manager.fetchCluster({ clusterId, topK }));
|
|
218
|
+
return jsonResult(await manager.fetchCluster({ clusterId, topK, offset, sort }));
|
|
211
219
|
},
|
|
212
220
|
};
|
|
213
221
|
}
|
|
@@ -100,9 +100,6 @@ function formatTimestamp(value, timezone) {
|
|
|
100
100
|
return `${part("year")}-${part("month")}-${part("day")} ` +
|
|
101
101
|
`${part("hour")}:${part("minute")}:${part("second")} ${part("timeZoneName")}`.trim();
|
|
102
102
|
}
|
|
103
|
-
function inline(value) {
|
|
104
|
-
return value.replace(/[\r\n]+/gu, " ").replaceAll("`", "\\`");
|
|
105
|
-
}
|
|
106
103
|
export function projectSession(input) {
|
|
107
104
|
const messages = input.events.flatMap((event) => {
|
|
108
105
|
const projected = projectMessage(event, input);
|
|
@@ -110,22 +107,8 @@ export function projectSession(input) {
|
|
|
110
107
|
});
|
|
111
108
|
if (messages.length === 0)
|
|
112
109
|
return undefined;
|
|
113
|
-
const provider = input.provider ?? "unknown";
|
|
114
|
-
const header = [
|
|
115
|
-
"# Session",
|
|
116
|
-
"",
|
|
117
|
-
`- Session ID: \`${inline(input.sessionId)}\``,
|
|
118
|
-
`- Provider: ${inline(provider)}`,
|
|
119
|
-
`- Chat type: ${input.chatType}`,
|
|
120
|
-
...(input.label ? [`- Conversation: ${inline(input.label)}`] : []),
|
|
121
|
-
...(input.conversationId ? [`- Conversation ID: \`${inline(input.conversationId)}\``] : []),
|
|
122
|
-
`- Started: ${new Date(input.startedAt).toISOString()}`,
|
|
123
|
-
"",
|
|
124
|
-
"## Transcript",
|
|
125
|
-
"",
|
|
126
|
-
];
|
|
127
110
|
const transcript = messages.map((message) => `${formatTimestamp(message.timestamp, input.timezone)} — ${message.speaker}: ${message.text}`);
|
|
128
|
-
return
|
|
111
|
+
return `# Transcript\n\n${transcript.join("\n\n")}\n`;
|
|
129
112
|
}
|
|
130
113
|
function hash(value) {
|
|
131
114
|
return createHash("sha256").update(value).digest("hex").slice(0, 16);
|
package/dist/src/session-sync.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { existsSync, lstatSync } from "node:fs";
|
|
3
|
-
import { chmod, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { chmod, mkdir, readFile, rename, unlink, utimes, writeFile } from "node:fs/promises";
|
|
4
4
|
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 =
|
|
8
|
+
const PROJECTOR_VERSION = 2;
|
|
9
9
|
const SUPPORTED_SCHEMA_VERSION = 17;
|
|
10
10
|
const REQUIRED_COLUMNS = {
|
|
11
11
|
schema_meta: ["meta_key", "role", "schema_version", "agent_id", "app_version"],
|
|
@@ -261,6 +261,7 @@ export async function syncSessionProjections(params) {
|
|
|
261
261
|
}
|
|
262
262
|
const target = projectionPath(params.outputDir, documentPath);
|
|
263
263
|
await atomicWrite(target, content, 0o600);
|
|
264
|
+
await utimes(target, new Date(), new Date(metadata.startedAt));
|
|
264
265
|
if (previous?.documentPath && previous.documentPath !== documentPath) {
|
|
265
266
|
await remove(projectionPath(params.outputDir, previous.documentPath));
|
|
266
267
|
}
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "unblock-memory",
|
|
3
3
|
"name": "Unblock Memory",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.6",
|
|
5
5
|
"description": "Indexes, retrieves, and analyzes configured workspace memory with existing QMD vectors.",
|
|
6
6
|
"kind": "memory",
|
|
7
7
|
"activation": { "onStartup": false },
|
package/package.json
CHANGED
|
@@ -1,78 +1,75 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: memory-curator
|
|
3
|
-
description:
|
|
3
|
+
description: Investigate Unblock Memory clusters and maintain supported, agent-specific knowledge that would otherwise be difficult to reconstruct.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Memory Curator
|
|
7
7
|
|
|
8
|
-
Use
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
##
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
-
|
|
58
|
-
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
plainly and include the `qmd://` evidence examined. Reflections are not
|
|
67
|
-
authoritative instructions.
|
|
8
|
+
Use semantic clusters as attention signals for maintaining the agent's current
|
|
9
|
+
understanding of its world. A cluster shows similarity, not a complete timeline,
|
|
10
|
+
truth, or consensus. Do not write from a cluster alone, and prefer no write over
|
|
11
|
+
weak, duplicative, or easily looked-up knowledge.
|
|
12
|
+
|
|
13
|
+
## Investigate
|
|
14
|
+
|
|
15
|
+
1. Call `memory_list_clusters`. If analysis is missing or stale, call
|
|
16
|
+
`memory_recluster`, then list again.
|
|
17
|
+
2. Fetch a useful cluster with `memory_fetch_cluster`. Start with
|
|
18
|
+
`sort: "representative"`; use `score_desc`, `date_asc`, or `date_desc` and
|
|
19
|
+
pagination when relevance, evolution, or recent state matters.
|
|
20
|
+
3. State the question the cluster raises: what may be repeated, contradictory,
|
|
21
|
+
changing, or worth understanding?
|
|
22
|
+
4. Search existing knowledge with `memory_search`, using
|
|
23
|
+
`corpora: ["knowledge"]`. If that corpus is not configured, report that and
|
|
24
|
+
do not create an unindexed file.
|
|
25
|
+
5. Investigate the evidence needed to answer the question. Follow important
|
|
26
|
+
`qmd://` sources with `memory_get`, search adjacent memory or sessions, inspect
|
|
27
|
+
other clusters, and check live systems, local files, authoritative docs, or
|
|
28
|
+
the web when they are the right source. The selected cluster is not assumed
|
|
29
|
+
to contain the whole timeline or the current truth.
|
|
30
|
+
6. Distinguish underlying evidence from earlier agent-created knowledge.
|
|
31
|
+
Derived repetition can locate or challenge a conclusion, but is not
|
|
32
|
+
independent corroboration.
|
|
33
|
+
|
|
34
|
+
## Decide what belongs in knowledge
|
|
35
|
+
|
|
36
|
+
Write knowledge when the result is agent-specific, evolving, and expensive to
|
|
37
|
+
reconstruct, such as current fleet composition, relationships, preferences,
|
|
38
|
+
project decisions, local conventions, or an assessment synthesized across
|
|
39
|
+
time.
|
|
40
|
+
|
|
41
|
+
Prefer a lookup for public or vendor-owned facts, generic command syntax, and
|
|
42
|
+
behavior likely to change with a third-party release. Those sources may verify
|
|
43
|
+
a local conclusion, but do not copy ordinary documentation into knowledge. A
|
|
44
|
+
local policy or deliberate divergence can belong when its local meaning and
|
|
45
|
+
rationale are the durable part.
|
|
46
|
+
|
|
47
|
+
Choose one outcome: update knowledge or no write. No write is successful when
|
|
48
|
+
the evidence is insufficient, the information is already accurate, or lookup
|
|
49
|
+
is better than memory.
|
|
50
|
+
|
|
51
|
+
## Maintain knowledge
|
|
52
|
+
|
|
53
|
+
Use a stable `knowledge/<topic>.md` file and update it in place. Facts, agent
|
|
54
|
+
assessments, and uncertainty may coexist. Qualify each claim where it appears so
|
|
55
|
+
it remains honest if semantic chunking retrieves it alone:
|
|
56
|
+
|
|
57
|
+
- identify what is verified and when or against which current source;
|
|
58
|
+
- label an interpretation as the agent's assessment rather than a settled fact;
|
|
59
|
+
- state uncertainty or an open question directly with the affected claim.
|
|
60
|
+
|
|
61
|
+
Keep the file focused on current understanding. Remove stale conclusions rather
|
|
62
|
+
than retaining `Supersedes`, history, changelog, migration, or old-process
|
|
63
|
+
sections. Raw memory and sessions preserve the evidence history. Include a
|
|
64
|
+
human-readable update time and useful evidence citations, but do not force a
|
|
65
|
+
rigid document template.
|
|
68
66
|
|
|
69
67
|
## Finish the cycle
|
|
70
68
|
|
|
71
69
|
- Do not rewrite raw memory or session projections.
|
|
72
|
-
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
intentional skips.
|
|
70
|
+
- Verify an updated file with `memory_search`, using
|
|
71
|
+
`corpora: ["knowledge"]`, and check all-corpora ranking when useful.
|
|
72
|
+
- Report the questions investigated, evidence consulted beyond each cluster,
|
|
73
|
+
files changed, current uncertainties, and intentional no-write decisions.
|
|
77
74
|
- Do not recluster again after this cycle's writes. Let them enter the next
|
|
78
75
|
scheduled cycle so the run cannot recursively react to its own output.
|