@unblocklabs/unblock-memory 0.3.17 → 0.3.18

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
@@ -731,12 +731,12 @@ session manifest. The projected file modification time matches the session
731
731
  start time for meaningful chronological cluster reads. Session results include
732
732
  provider, chat type, conversation identity, and start time as an ISO 8601 timestamp. They
733
733
  participate in the same search and clustering index as file memory. The plugin
734
- automatically refreshes each configured agent's sessions every 15 minutes while
734
+ automatically checks each configured agent's sessions every 60 minutes while
735
735
  the Gateway runs. Set `syncIntervalMinutes` on the `sessions` corpus to an integer
736
736
  from `1` to `1440`, or `0` for manual-only syncing. For example:
737
737
 
738
738
  ```json
739
- { "name": "sessions", "kind": "sessions", "syncIntervalMinutes": 15 }
739
+ { "name": "sessions", "kind": "sessions", "syncIntervalMinutes": 60 }
740
740
  ```
741
741
 
742
742
  The first refresh runs after one interval, not during startup. Restart the
@@ -745,6 +745,17 @@ sync is skipped, and failures are visible through `memory_sync_status` and retri
745
745
  at the next interval. `memory_sync_sessions` still provides an immediate manual
746
746
  refresh. Syncing and embedding run inside the Gateway process, without an LLM turn.
747
747
 
748
+ Quiet checks compare source metadata and the last successful index checkpoint
749
+ before initializing the memory manager. Unchanged sessions skip QMD updates and
750
+ embedding. New assistant answers count too, not just human messages. Changed
751
+ transcripts are projected and content-hashed; tool-only or filtered additions
752
+ that leave the indexed text unchanged also skip indexing. Empty/filtered sessions
753
+ are remembered. Index changes, missing projections, changed projection settings,
754
+ QMD upgrades and incomplete runs invalidate the skip checkpoint; `force: true`
755
+ bypasses both gates. `memory_sync_status` reports `lastCheckedAt`, `lastIndexedAt`
756
+ and `skipReason` (`no_changes` or `no_indexable_changes`) separately. Existing
757
+ explicit intervals remain unchanged on upgrade; set them to `60` for hourly checks.
758
+
748
759
  Indexes live at `~/.openclaw/agents/<agentId>/unblock-memory/index.sqlite` (or the
749
760
  equivalent configured OpenClaw state directory). Durable agent-supplied event
750
761
  dates and maintenance proposals live separately in `curation.sqlite`, so a QMD
@@ -172,7 +172,7 @@ function resolveCorpora(value) {
172
172
  !chatTypes.every((chatType) => CHAT_TYPES.includes(chatType))) {
173
173
  throw new Error(`unblock-memory corpus sessions chatTypes must contain channel, group, or direct`);
174
174
  }
175
- const syncIntervalMinutes = corpus.syncIntervalMinutes ?? 15;
175
+ const syncIntervalMinutes = corpus.syncIntervalMinutes ?? 60;
176
176
  if (typeof syncIntervalMinutes !== "number" || !Number.isInteger(syncIntervalMinutes) ||
177
177
  syncIntervalMinutes < 0 || syncIntervalMinutes > 1440) {
178
178
  throw new Error("unblock-memory corpus sessions syncIntervalMinutes must be an integer between 0 and 1440");
@@ -534,12 +534,14 @@ export class QmdMemoryManager {
534
534
  if (!sessions)
535
535
  throw new Error('memory session sync requires a configured "sessions" corpus');
536
536
  onPhase?.("projecting");
537
- const store = await this.#getStore();
538
537
  const synced = await syncSessionProjections({
539
538
  ...sessions,
540
539
  force,
540
+ indexPath: this.#dbPath,
541
+ indexReady: async () => (await (await this.#getStore()).getStatus()).needsEmbedding === 0,
541
542
  index: async () => {
542
543
  onPhase?.("indexing");
544
+ const store = await this.#getStore();
543
545
  const update = await store.update({ collections: [sessions.collection] });
544
546
  this.#cleanupRemovedDocuments?.(update.updated + update.removed);
545
547
  const analysisStore = store;
@@ -561,6 +563,9 @@ export class QmdMemoryManager {
561
563
  },
562
564
  });
563
565
  this.#sessionMetadata = sessionMetadataByPath(synced.manifest);
566
+ if (synced.result.skipReason)
567
+ return synced.result;
568
+ const store = await this.#getStore();
564
569
  const status = await store.getStatus();
565
570
  const collections = await store.listCollections();
566
571
  this.#files = collections.reduce((total, collection) => total + collection.active_count, 0);
@@ -2,7 +2,7 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/plugin-entry";
2
2
  import type { CorpusConfig } from "./config.js";
3
3
  import type { MemoryPluginRuntimeContract } from "./contracts.js";
4
4
  import { QmdMemoryManager } from "./manager.js";
5
- import type { SessionSyncResult } from "./session-sync.js";
5
+ import { type SessionSyncResult } from "./session-sync.js";
6
6
  export type SessionSyncStatus = {
7
7
  status: "idle";
8
8
  } | {
@@ -5,6 +5,7 @@ import { resolveAgentDir, resolveAgentWorkspaceDir, resolveStateDir, } from "ope
5
5
  import { listAgentIds, resolveAgentIdentity } from "openclaw/plugin-sdk/agent-runtime";
6
6
  import { QmdMemoryManager } from "./manager.js";
7
7
  import { resolveTimezone } from "./session-projector.js";
8
+ import { unchangedSessionSync } from "./session-sync.js";
8
9
  import { resolveConfiguredSkillPath, resolveSessionSource, resolveSources } from "./sources.js";
9
10
  import { classifyWorkspaceMemoryPaths } from "./workspace-path-classifier.js";
10
11
  const activeSessionSyncs = new Map();
@@ -149,10 +150,15 @@ export class QmdMemoryRuntime {
149
150
  }));
150
151
  };
151
152
  try {
152
- const { manager, error } = await this.getMemorySearchManager(params);
153
- if (!manager)
154
- throw new Error(error ?? "memory unavailable");
155
- const result = await manager.syncSessions(force, writePhase);
153
+ const sessions = this.#sessionConfig(params.cfg, params.agentId);
154
+ let result = !force && sessions
155
+ ? await unchangedSessionSync(sessions, join(directory, "index.sqlite")) : undefined;
156
+ if (!result) {
157
+ const { manager, error } = await this.getMemorySearchManager(params);
158
+ if (!manager)
159
+ throw new Error(error ?? "memory unavailable");
160
+ result = await manager.syncSessions(force, writePhase);
161
+ }
156
162
  await statusWrites;
157
163
  await atomicWriteJson(statusPath, {
158
164
  status: "completed",
@@ -224,10 +230,10 @@ export class QmdMemoryRuntime {
224
230
  const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
225
231
  const stateDir = join(this.#stateRoot, "agents", agentId, "unblock-memory");
226
232
  const fileCorpora = this.#corpora.filter((corpus) => corpus.kind === "files" || corpus.kind === "skills");
227
- const sessionCorpus = this.#corpora.find((corpus) => corpus.kind === "sessions");
233
+ const sessions = this.#sessionConfig(cfg, agentId);
228
234
  const sources = resolveSources(workspaceDir, fileCorpora);
229
- const sessionSource = sessionCorpus
230
- ? resolveSessionSource(join(stateDir, "sessions"), sessionCorpus.chatTypes)
235
+ const sessionSource = sessions
236
+ ? resolveSessionSource(sessions.outputDir, sessions.chatTypes)
231
237
  : undefined;
232
238
  if (sessionSource)
233
239
  sources.push(sessionSource);
@@ -238,23 +244,26 @@ export class QmdMemoryRuntime {
238
244
  sources,
239
245
  keepModelsWarm: this.#keepEmbeddingModelWarm,
240
246
  analysisExecutable: this.#analysisExecutable,
241
- ...(sessionCorpus && sessionSource ? {
242
- sessions: {
243
- agentId,
244
- agentName: resolveAgentIdentity(cfg, agentId)?.name?.trim() || agentId,
245
- chatTypes: sessionCorpus.chatTypes,
246
- maxExpandedTokens: sessionCorpus.maxExpandedTokens,
247
- collection: sessionSource.collection,
248
- databasePath: join(resolveAgentDir(cfg, agentId), "openclaw-agent.sqlite"),
249
- manifestPath: join(stateDir, "sessions-manifest.json"),
250
- outputDir: sessionSource.root,
251
- timezone: resolveTimezone(cfg.agents?.defaults?.userTimezone?.trim()),
252
- },
253
- } : {}),
247
+ sessions,
254
248
  });
255
249
  await manager.start();
256
250
  return manager;
257
251
  }
252
+ #sessionConfig(cfg, agentId) {
253
+ const corpus = this.#corpora.find(corpus => corpus.kind === "sessions");
254
+ if (!corpus)
255
+ return;
256
+ const stateDir = this.#sessionSyncDirectory(agentId);
257
+ const source = resolveSessionSource(join(stateDir, "sessions"), corpus.chatTypes);
258
+ return {
259
+ agentId, agentName: resolveAgentIdentity(cfg, agentId)?.name?.trim() || agentId,
260
+ chatTypes: corpus.chatTypes, maxExpandedTokens: corpus.maxExpandedTokens,
261
+ collection: source.collection, outputDir: source.root,
262
+ databasePath: join(resolveAgentDir(cfg, agentId), "openclaw-agent.sqlite"),
263
+ manifestPath: join(stateDir, "sessions-manifest.json"),
264
+ timezone: resolveTimezone(cfg.agents?.defaults?.userTimezone?.trim()),
265
+ };
266
+ }
258
267
  #sessionSyncDirectory(agentId) {
259
268
  return join(this.#stateRoot, "agents", agentId, "unblock-memory");
260
269
  }
@@ -9,10 +9,15 @@ type IndexedSession = SessionMetadata & {
9
9
  projectionHash: string;
10
10
  documentPath: string;
11
11
  projectorVersion: number;
12
+ sourceFingerprint?: string;
12
13
  };
13
14
  export type SessionManifest = {
14
15
  version: number;
15
16
  lastSuccessfulSyncAt?: number;
17
+ lastIndexedAt?: number;
18
+ projectionKey?: string;
19
+ indexSignature?: string;
20
+ ignoredSessions?: Record<string, string>;
16
21
  sessions: Record<string, IndexedSession>;
17
22
  };
18
23
  export type SessionSyncResult = {
@@ -24,11 +29,12 @@ export type SessionSyncResult = {
24
29
  failed: number;
25
30
  embedded: number;
26
31
  lastSuccessfulSyncAt: number;
32
+ lastCheckedAt?: number;
33
+ lastIndexedAt?: number;
34
+ skipReason?: "no_changes" | "no_indexable_changes";
27
35
  diagnostics?: NonNullable<SessionProjectionInput["diagnostics"]>;
28
36
  };
29
- export declare function readSessionManifest(path: string): Promise<SessionManifest>;
30
- export declare function sessionMetadataByPath(manifest: SessionManifest): Map<string, SessionMetadata>;
31
- export declare function syncSessionProjections(params: {
37
+ type ProjectionOptions = {
32
38
  databasePath: string;
33
39
  outputDir: string;
34
40
  manifestPath: string;
@@ -36,7 +42,14 @@ export declare function syncSessionProjections(params: {
36
42
  agentName: string;
37
43
  timezone: string;
38
44
  chatTypes: readonly ChatType[];
45
+ };
46
+ export declare function unchangedSessionSync(params: ProjectionOptions, indexPath: string): Promise<SessionSyncResult | undefined>;
47
+ export declare function readSessionManifest(path: string): Promise<SessionManifest>;
48
+ export declare function sessionMetadataByPath(manifest: SessionManifest): Map<string, SessionMetadata>;
49
+ export declare function syncSessionProjections(params: ProjectionOptions & {
39
50
  force?: boolean;
51
+ indexPath?: string;
52
+ indexReady?: () => Promise<boolean>;
40
53
  index?: () => Promise<number>;
41
54
  }): Promise<{
42
55
  result: SessionSyncResult;
@@ -1,5 +1,5 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
- import { existsSync, lstatSync } from "node:fs";
2
+ import { existsSync, lstatSync, readFileSync, statSync } from "node:fs";
3
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";
@@ -7,6 +7,12 @@ import { projectSession, sessionDocumentPath, } from "./session-projector.js";
7
7
  const MANIFEST_VERSION = 1;
8
8
  export const PROJECTOR_VERSION = 6;
9
9
  const SUPPORTED_SCHEMA_VERSIONS = new Set([17, 18, 19]);
10
+ // Source lives in src/, published code in dist/src/. Read our own pinned dependency
11
+ // metadata, not QMD internals (which may also be substituted by runtime inspectors).
12
+ const sourcePackage = new URL("../package.json", import.meta.url);
13
+ const packageMetadata = JSON.parse(readFileSync(existsSync(sourcePackage)
14
+ ? sourcePackage : new URL("../../package.json", import.meta.url), "utf8"));
15
+ const indexVersion = [packageMetadata.version, packageMetadata.dependencies["@unblocklabs/qmd"]];
10
16
  const REQUIRED_COLUMNS = {
11
17
  schema_meta: ["meta_key", "role", "schema_version", "agent_id", "app_version"],
12
18
  session_windows: [
@@ -21,6 +27,32 @@ const REQUIRED_COLUMNS = {
21
27
  session_transcript_active_events: ["session_id", "active_position", "event_seq", "message_position"],
22
28
  transcript_rewrite_watermarks: ["session_id", "generation"],
23
29
  };
30
+ function projectionKey(params) {
31
+ return JSON.stringify([PROJECTOR_VERSION, params.databasePath, params.agentId,
32
+ params.agentName, params.timezone, [...params.chatTypes].sort()]);
33
+ }
34
+ // Conservative proof: any index/WAL write, replacement or QMD upgrade invalidates it.
35
+ // This avoids depending on QMD's private embedding schema or opening/loading its store.
36
+ function sessionIndexSignature(databasePath) {
37
+ try {
38
+ const fingerprint = (path) => {
39
+ const stat = statSync(path, { bigint: true });
40
+ return [stat.dev, stat.ino, stat.size, stat.mtimeNs, stat.ctimeNs].map(String);
41
+ };
42
+ let wal = null;
43
+ try {
44
+ wal = fingerprint(`${databasePath}-wal`);
45
+ }
46
+ catch (error) {
47
+ if (error.code !== "ENOENT")
48
+ throw error;
49
+ }
50
+ return JSON.stringify([indexVersion, fingerprint(databasePath), wal]);
51
+ }
52
+ catch {
53
+ return undefined;
54
+ }
55
+ }
24
56
  function projectionPath(outputDir, documentPath) {
25
57
  const root = resolve(outputDir);
26
58
  const target = resolve(root, documentPath);
@@ -106,6 +138,7 @@ function readSnapshot(params) {
106
138
  ORDER BY active.active_position
107
139
  `);
108
140
  const events = new Map();
141
+ const changed = new Set();
109
142
  for (const window of windows) {
110
143
  const metadata = {
111
144
  sessionId: window.sessionId,
@@ -117,18 +150,21 @@ function readSnapshot(params) {
117
150
  };
118
151
  const previous = params.previousManifest.sessions[window.sessionId];
119
152
  const documentPath = sessionDocumentPath(metadata);
153
+ const sourceFingerprint = JSON.stringify(window);
120
154
  const unchanged = !params.force &&
121
- previous?.sourceGeneration === window.sourceGeneration &&
122
- previous.maxSeq === (window.maxSeq ?? 0) &&
123
- previous.projectorVersion === PROJECTOR_VERSION &&
124
- previous.documentPath === documentPath &&
125
- existsSync(projectionPath(params.outputDir, documentPath));
155
+ (previous ? previous.sourceFingerprint === sourceFingerprint &&
156
+ previous.projectorVersion === PROJECTOR_VERSION &&
157
+ previous.documentPath === documentPath &&
158
+ existsSync(projectionPath(params.outputDir, documentPath)) :
159
+ params.previousManifest.ignoredSessions?.[window.sessionId] === sourceFingerprint);
126
160
  if (!unchanged) {
127
- events.set(window.sessionId, readEvents.all(window.sessionId));
161
+ changed.add(window.sessionId);
162
+ if (!params.metadataOnly)
163
+ events.set(window.sessionId, readEvents.all(window.sessionId));
128
164
  }
129
165
  }
130
166
  db.exec("COMMIT");
131
- return { windows, events };
167
+ return { windows, events, changed };
132
168
  }
133
169
  catch (error) {
134
170
  try {
@@ -141,6 +177,20 @@ function readSnapshot(params) {
141
177
  db.close();
142
178
  }
143
179
  }
180
+ export async function unchangedSessionSync(params, indexPath) {
181
+ const manifest = await readSessionManifest(params.manifestPath);
182
+ if (!manifest.lastSuccessfulSyncAt || manifest.projectionKey !== projectionKey(params) ||
183
+ !manifest.indexSignature || manifest.indexSignature !== sessionIndexSignature(indexPath))
184
+ return;
185
+ const snapshot = readSnapshot({ ...params, previousManifest: manifest, force: false, metadataOnly: true });
186
+ const ids = new Set(snapshot.windows.map(window => window.sessionId));
187
+ if (snapshot.changed.size || Object.keys(manifest.sessions).some(id => !ids.has(id)) ||
188
+ Object.keys(manifest.ignoredSessions ?? {}).some(id => !ids.has(id)))
189
+ return;
190
+ return { scanned: ids.size, unchanged: ids.size, updated: 0, removed: 0,
191
+ skipped: 0, failed: 0, embedded: 0, lastSuccessfulSyncAt: manifest.lastSuccessfulSyncAt,
192
+ lastCheckedAt: Date.now(), lastIndexedAt: manifest.lastIndexedAt, skipReason: "no_changes" };
193
+ }
144
194
  function emptyManifest() {
145
195
  return { version: MANIFEST_VERSION, sessions: {} };
146
196
  }
@@ -205,10 +255,11 @@ export async function syncSessionProjections(params) {
205
255
  const previousManifest = await readSessionManifest(params.manifestPath);
206
256
  const snapshot = readSnapshot({
207
257
  ...params,
208
- force: params.force === true,
258
+ force: params.force === true || previousManifest.projectionKey !== projectionKey(params),
209
259
  previousManifest,
210
260
  });
211
261
  const sessions = {};
262
+ const ignoredSessions = {};
212
263
  const counts = { unchanged: 0, updated: 0, removed: 0, skipped: 0, failed: 0 };
213
264
  const diagnostics = { internalMessagesCleaned: 0, attachmentsCleaned: 0, attachmentBudgetSkipped: 0 };
214
265
  await mkdir(params.outputDir, { recursive: true, mode: 0o700 });
@@ -226,7 +277,10 @@ export async function syncSessionProjections(params) {
226
277
  };
227
278
  const documentPath = sessionDocumentPath(metadata);
228
279
  if (events === undefined) {
229
- sessions[window.sessionId] = previous;
280
+ if (previous)
281
+ sessions[window.sessionId] = previous;
282
+ else
283
+ ignoredSessions[window.sessionId] = JSON.stringify(window);
230
284
  counts.unchanged += 1;
231
285
  continue;
232
286
  }
@@ -255,6 +309,7 @@ export async function syncSessionProjections(params) {
255
309
  continue;
256
310
  }
257
311
  if (!content) {
312
+ ignoredSessions[window.sessionId] = JSON.stringify(window);
258
313
  counts.skipped += 1;
259
314
  if (previous) {
260
315
  await remove(projectionPath(params.outputDir, previous.documentPath));
@@ -263,8 +318,13 @@ export async function syncSessionProjections(params) {
263
318
  continue;
264
319
  }
265
320
  const target = projectionPath(params.outputDir, documentPath);
266
- await atomicWrite(target, content, 0o600);
267
- await utimes(target, new Date(), new Date(metadata.startedAt));
321
+ const hash = projectionHash(content);
322
+ const contentChanged = params.force === true || previous?.projectorVersion !== PROJECTOR_VERSION ||
323
+ previous.projectionHash !== hash || previous.documentPath !== documentPath || !existsSync(target);
324
+ if (contentChanged) {
325
+ await atomicWrite(target, content, 0o600);
326
+ await utimes(target, new Date(), new Date(metadata.startedAt));
327
+ }
268
328
  if (previous?.documentPath && previous.documentPath !== documentPath) {
269
329
  await remove(projectionPath(params.outputDir, previous.documentPath));
270
330
  }
@@ -274,11 +334,15 @@ export async function syncSessionProjections(params) {
274
334
  maxSeq: window.maxSeq,
275
335
  activeEventCount: window.activeEventCount,
276
336
  sizeBytes: Buffer.byteLength(content),
277
- projectionHash: projectionHash(content),
337
+ projectionHash: hash,
278
338
  documentPath,
279
339
  projectorVersion: PROJECTOR_VERSION,
340
+ sourceFingerprint: JSON.stringify(window),
280
341
  };
281
- counts.updated += 1;
342
+ if (contentChanged)
343
+ counts.updated += 1;
344
+ else
345
+ counts.unchanged += 1;
282
346
  }
283
347
  for (const [sessionId, session] of Object.entries(previousManifest.sessions)) {
284
348
  if (sessions[sessionId] || snapshot.windows.some((window) => window.sessionId === sessionId))
@@ -286,11 +350,25 @@ export async function syncSessionProjections(params) {
286
350
  await remove(projectionPath(params.outputDir, session.documentPath));
287
351
  counts.removed += 1;
288
352
  }
289
- const embedded = await params.index?.() ?? 0;
353
+ const needsIndex = params.force === true || counts.updated > 0 || counts.removed > 0 ||
354
+ previousManifest.projectionKey !== projectionKey(params) ||
355
+ !previousManifest.indexSignature || !params.indexPath ||
356
+ previousManifest.indexSignature !== sessionIndexSignature(params.indexPath);
357
+ const embedded = needsIndex ? await params.index?.() ?? 0 : 0;
290
358
  const lastSuccessfulSyncAt = Date.now();
359
+ const indexed = needsIndex && params.index !== undefined;
360
+ const signature = params.indexPath ? sessionIndexSignature(params.indexPath) : undefined;
361
+ const indexReady = indexed && (await params.indexReady?.() ?? false);
362
+ const lastIndexedAt = indexed ? lastSuccessfulSyncAt : previousManifest.lastIndexedAt;
291
363
  const manifest = {
292
364
  version: MANIFEST_VERSION,
293
365
  lastSuccessfulSyncAt,
366
+ lastIndexedAt,
367
+ projectionKey: projectionKey(params),
368
+ // Never certify an index mutation that happened during a skipped run or readiness check.
369
+ indexSignature: counts.failed > 0 ? undefined : !needsIndex ? previousManifest.indexSignature :
370
+ indexReady && params.indexPath && signature === sessionIndexSignature(params.indexPath) ? signature : undefined,
371
+ ignoredSessions,
294
372
  sessions,
295
373
  };
296
374
  await atomicWrite(params.manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 0o600);
@@ -300,6 +378,9 @@ export async function syncSessionProjections(params) {
300
378
  ...counts,
301
379
  embedded,
302
380
  lastSuccessfulSyncAt,
381
+ lastCheckedAt: lastSuccessfulSyncAt,
382
+ lastIndexedAt,
383
+ ...(!needsIndex ? { skipReason: "no_indexable_changes" } : {}),
303
384
  diagnostics,
304
385
  },
305
386
  manifest,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "unblock-memory",
3
3
  "name": "Unblock Memory",
4
- "version": "0.3.17",
4
+ "version": "0.3.18",
5
5
  "description": "Indexes, retrieves, and analyzes configured workspace memory with existing QMD vectors.",
6
6
  "kind": "memory",
7
7
  "activation": { "onStartup": true },
@@ -186,8 +186,8 @@
186
186
  "type": "integer",
187
187
  "minimum": 0,
188
188
  "maximum": 1440,
189
- "default": 15,
190
- "description": "Refresh sessions every N minutes while the Gateway runs; 0 disables automatic sync. First refresh is after one interval."
189
+ "default": 60,
190
+ "description": "Check for session changes every N minutes while the Gateway runs; unchanged sessions skip indexing. 0 disables automatic sync. First check is after one interval."
191
191
  },
192
192
  "chatTypes": {
193
193
  "type": "array",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unblocklabs/unblock-memory",
3
- "version": "0.3.17",
3
+ "version": "0.3.18",
4
4
  "description": "Workspace-native memory for OpenClaw, powered by QMD",
5
5
  "type": "module",
6
6
  "license": "MIT",