@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 CHANGED
@@ -53,14 +53,9 @@ directories, or globs into named corpora:
53
53
  chatTypes: ["channel", "group"],
54
54
  },
55
55
  {
56
- name: "canon",
56
+ name: "knowledge",
57
57
  kind: "files",
58
- paths: ["knowledge/canon/**/*.md"],
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: ["canon"]` to search selected corpora or `corpora: ["all"]` to
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
- under the agent's `unblock-memory/sessions` state directory and can be rebuilt
120
- from OpenClaw at any time. Session results include provider, chat type,
121
- conversation identity, and start time as an ISO 8601 timestamp. They participate
122
- in the same search and clustering index as file memory. This phase does not sync
123
- sessions at startup or on a schedule; refreshes are manual through
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 up to `topK` representative QMD chunks for a
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
- Cluster reads return at most 50 members. Member excerpts are capped at 2 KB
178
- each and 12 KB across a response; source aliases are capped at five per member
179
- and 50 across a response.
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 canon and reflections
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 curated files outside `memory/**` so each file belongs to only one corpus:
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
- ├── canon/
200
- │ └── gateway-restarts.md
201
- └── reflections/
202
- └── 2026-08-26.md
207
+ ├── fleet.md
208
+ ├── people/
209
+ └── rico.md
210
+ └── projects/
211
+ └── unblock-memory.md
203
212
  ```
204
213
 
205
- Canon files are stable topic files updated in place. Each one contains only the
206
- current affirmative rule or understanding, its update time, current rationale,
207
- and `qmd://` evidence links. Do not include old procedures, changelogs, or a
208
- `Supersedes` section: semantic chunking may retrieve those passages without the
209
- surrounding warning that they are obsolete.
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
- Reflection files are daily or timestamped and append-oriented. They hold useful
212
- patterns, hypotheses, contradictions, and uncertainty, but are not
213
- authoritative. Both corpora participate in later search and clustering, so a
214
- later run can reconsider prior reasoning. Repeated derived text is not
215
- independent corroboration; durable canon still needs underlying source evidence.
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 lists current clusters, reclusters only when analysis is missing or
230
- stale, follows representative sources with `memory_get`, and may correctly
231
- write nothing. Its own writes are indexed for the next cycle; it does not
232
- recluster recursively in the same run.
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.
@@ -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 {};
@@ -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 noiseOrder = clusterId === -1
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
- SELECT
222
- m.hash, m.seq, m.probability, m.outlier_score, m.x, m.y,
223
- m.representative_rank, m.pos, m.chunk_len, m.doc
224
- FROM memory_analysis_available_memberships m
225
- WHERE m.run_id = ? AND m.cluster_id = ?
226
- ORDER BY ${noiseOrder}
227
- LIMIT ?
228
- `).all(runId, clusterId, limit);
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 text = remaining <= 0
233
- ? ""
234
- : byteSlice(row.doc.slice(row.pos, row.pos + row.chunk_len), Math.min(maxExcerptBytes, remaining));
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 aliases = sourcePaths(db, row.hash, Math.min(MAX_ALIASES_PER_MEMBER, remainingAliases));
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: availableSize(db, run.id, clusterId),
421
+ availableSize: total,
399
422
  meanProbability: row.mean_probability,
400
423
  },
401
- members: members(db, run.id, clusterId, limit),
424
+ members: pageMembers,
425
+ page: {
426
+ offset,
427
+ returned: pageMembers.length,
428
+ total,
429
+ hasMore,
430
+ ...(hasMore ? { nextOffset } : {}),
431
+ },
402
432
  };
403
433
  }
@@ -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: {
@@ -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();
@@ -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 the top representative QMD chunks for a clusterId returned by memory_list_clusters.",
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 `${[...header, ...transcript].join("\n\n")}\n`;
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);
@@ -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 = 1;
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
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "unblock-memory",
3
3
  "name": "Unblock Memory",
4
- "version": "0.2.5",
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@unblocklabs/unblock-memory",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "Workspace-native memory for OpenClaw, powered by QMD",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,78 +1,75 @@
1
1
  ---
2
2
  name: memory-curator
3
- description: Review Unblock Memory clusters and turn supported current knowledge into canon or exploratory reasoning into reflections.
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 Unblock Memory's semantic clusters to maintain durable workspace knowledge.
9
- Clusters show similarity, not truth or consensus. Prefer no write over a weak or
10
- duplicative artifact.
11
-
12
- ## Review clusters
13
-
14
- 1. Call `memory_list_clusters`.
15
- 2. If analysis is missing or stale, call `memory_recluster`, then list again.
16
- 3. Fetch useful clusters with `memory_fetch_cluster`. Treat noise as optional
17
- review material, not automatically important content.
18
- 4. Follow representative `qmd://` source paths with `memory_get` whenever the
19
- excerpt lacks context or a conclusion could change durable knowledge.
20
- 5. Distinguish underlying memory or session evidence from earlier canon and
21
- reflections. Derived artifacts may help locate, challenge, or revise an
22
- understanding, but repetition does not make them independent evidence.
23
- 6. For each reviewed cluster, choose canon, reflection, or no write.
24
-
25
- ## Write canon
26
-
27
- Use `knowledge/canon/<stable-topic>.md` for a supported, durable rule or current
28
- understanding. Update the topic file in place instead of creating dated copies.
29
-
30
- Every semantic chunk in canon must remain correct if retrieved alone:
31
-
32
- - State only the current affirmative truth.
33
- - Remove obsolete instructions rather than preserving them for comparison.
34
- - Never add `Supersedes`, history, changelog, old-process, or migration sections.
35
- - Include a human-readable `Updated` timestamp, the current rationale, and
36
- `qmd://` evidence citations.
37
- - Preserve uncertainty in the claim itself. If the evidence does not support a
38
- stable current claim, write a reflection or nothing.
39
-
40
- A concise shape is sufficient:
41
-
42
- ```markdown
43
- # Topic
44
-
45
- Updated: 2026-08-26 14:30 EDT
46
-
47
- ## Current understanding
48
-
49
- Present-tense rule or facts.
50
-
51
- ## Rationale
52
-
53
- Why this is the current understanding.
54
-
55
- ## Evidence
56
-
57
- - qmd://memory/...
58
- - qmd://sessions/...
59
- ```
60
-
61
- ## Write reflections
62
-
63
- Use `knowledge/reflections/YYYY-MM-DD.md` for patterns, hypotheses,
64
- contradictions, open questions, or reasoning worth revisiting. Append a
65
- timestamped section when the day's file already exists. Label uncertainty
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
- - Avoid restating knowledge already captured accurately.
73
- - Verify new or updated knowledge with `memory_search`, selecting `canon` or
74
- `reflections` when useful.
75
- - Report clusters reviewed, files changed, evidence used, uncertainties, and
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.