@unblocklabs/unblock-memory 0.3.11 → 0.3.12

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
@@ -254,9 +254,20 @@ role-labeled, timestamped speaker messages; filtering metadata remains in the
254
254
  session manifest. The projected file modification time matches the session
255
255
  start time for meaningful chronological cluster reads. Session results include
256
256
  provider, chat type, conversation identity, and start time as an ISO 8601 timestamp. They
257
- participate in the same search and clustering index as file memory. This phase
258
- does not sync sessions at startup or on a schedule; refreshes are manual through
259
- `memory_sync_sessions`.
257
+ participate in the same search and clustering index as file memory. The plugin
258
+ automatically refreshes each configured agent's sessions every 15 minutes while
259
+ the Gateway runs. Set `syncIntervalMinutes` on the `sessions` corpus to an integer
260
+ from `1` to `1440`, or `0` for manual-only syncing. For example:
261
+
262
+ ```json
263
+ { "name": "sessions", "kind": "sessions", "syncIntervalMinutes": 15 }
264
+ ```
265
+
266
+ The first refresh runs after one interval, not during startup. Restart the
267
+ Gateway after changing the interval. Refreshes are incremental; an already-running
268
+ sync is skipped, and failures are visible through `memory_sync_status` and retried
269
+ at the next interval. `memory_sync_sessions` still provides an immediate manual
270
+ refresh. Syncing and embedding run inside the Gateway process, without an LLM turn.
260
271
 
261
272
  Indexes live at `~/.openclaw/agents/<agentId>/unblock-memory/index.sqlite` (or the
262
273
  equivalent configured OpenClaw state directory). Durable agent-supplied event
@@ -15,6 +15,7 @@ type SessionCorpusConfig = {
15
15
  kind: "sessions";
16
16
  chatTypes: readonly ChatType[];
17
17
  maxExpandedTokens: number;
18
+ syncIntervalMinutes: number;
18
19
  };
19
20
  export type CorpusConfig = FileCorpusConfig | SkillCorpusConfig | SessionCorpusConfig;
20
21
  export declare const DEFAULT_CORPORA: readonly FileCorpusConfig[];
@@ -61,7 +61,7 @@ function resolveCorpora(value) {
61
61
  return { name: "skills", kind: "skills", paths: corpus.paths.map((path) => path.trim()) };
62
62
  }
63
63
  if (corpus.kind === "sessions") {
64
- assertOnlyKeys(corpus, ["name", "kind", "chatTypes", "maxExpandedTokens"], `corpora[${index}]`);
64
+ assertOnlyKeys(corpus, ["name", "kind", "chatTypes", "maxExpandedTokens", "syncIntervalMinutes"], `corpora[${index}]`);
65
65
  if (name !== "sessions") {
66
66
  throw new Error('unblock-memory session corpus must be named "sessions"');
67
67
  }
@@ -71,8 +71,14 @@ function resolveCorpora(value) {
71
71
  !chatTypes.every((chatType) => CHAT_TYPES.includes(chatType))) {
72
72
  throw new Error(`unblock-memory corpus sessions chatTypes must contain channel, group, or direct`);
73
73
  }
74
+ const syncIntervalMinutes = corpus.syncIntervalMinutes ?? 15;
75
+ if (typeof syncIntervalMinutes !== "number" || !Number.isInteger(syncIntervalMinutes) ||
76
+ syncIntervalMinutes < 0 || syncIntervalMinutes > 1440) {
77
+ throw new Error("unblock-memory corpus sessions syncIntervalMinutes must be an integer between 0 and 1440");
78
+ }
74
79
  return {
75
80
  name: "sessions",
81
+ syncIntervalMinutes,
76
82
  kind: "sessions",
77
83
  chatTypes: [...new Set(chatTypes)],
78
84
  maxExpandedTokens: positiveInteger(corpus.maxExpandedTokens, DEFAULT_SESSION_MAX_EXPANDED_TOKENS, "corpus sessions maxExpandedTokens", MAX_SESSION_MAX_EXPANDED_TOKENS),
@@ -403,6 +403,12 @@ export function registerUnblockMemory(api) {
403
403
  runtime,
404
404
  };
405
405
  api.registerMemoryCapability(capability);
406
+ if (config.corpora.some((corpus) => corpus.kind === "sessions" && corpus.syncIntervalMinutes > 0)) {
407
+ api.on("gateway_start", () => runtime.startSessionSyncSchedule(api.config, (error) => {
408
+ api.logger.warn(`unblock-memory scheduled session sync could not start: ${String(error)}`);
409
+ }));
410
+ api.on("gateway_stop", () => runtime.stopSessionSyncSchedule());
411
+ }
406
412
  if (config.people.enabled) {
407
413
  const peopleStores = new PeopleStores({
408
414
  maxOpenTodos: config.people.todos.maxOpen,
@@ -40,6 +40,8 @@ type StoredRunningSessionSync = Extract<StoredSessionSyncStatus, {
40
40
  export declare function recoverInterruptedSessionSync(directory: string, statusPath: string, stale: StoredRunningSessionSync): Promise<SessionSyncStatus>;
41
41
  export declare class QmdMemoryRuntime implements MemoryPluginRuntimeContract {
42
42
  #private;
43
+ startSessionSyncSchedule(cfg: OpenClawConfig, onError: (error: unknown) => void): void;
44
+ stopSessionSyncSchedule(): void;
43
45
  constructor(corpora: readonly CorpusConfig[], options?: {
44
46
  analysisExecutable?: string;
45
47
  keepEmbeddingModelWarm?: boolean;
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
  import { resolveAgentDir, resolveAgentWorkspaceDir, resolveStateDir, } from "openclaw/plugin-sdk/memory-core-host-engine-foundation";
5
- import { resolveAgentIdentity } from "openclaw/plugin-sdk/agent-runtime";
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
8
  import { resolveConfiguredSkillPath, resolveSessionSource, resolveSources } from "./sources.js";
@@ -68,6 +68,24 @@ export class QmdMemoryRuntime {
68
68
  #keepEmbeddingModelWarm;
69
69
  #stateRoot;
70
70
  #managers = new Map();
71
+ #sessionSyncTimer;
72
+ startSessionSyncSchedule(cfg, onError) {
73
+ this.stopSessionSyncSchedule();
74
+ const sessions = this.#corpora.find((corpus) => corpus.kind === "sessions");
75
+ if (!sessions?.syncIntervalMinutes)
76
+ return;
77
+ this.#sessionSyncTimer = setInterval(() => {
78
+ for (const agentId of listAgentIds(cfg)) {
79
+ void this.startSessionSync({ cfg, agentId }).catch(onError);
80
+ }
81
+ }, sessions.syncIntervalMinutes * 60_000);
82
+ this.#sessionSyncTimer.unref();
83
+ }
84
+ stopSessionSyncSchedule() {
85
+ if (this.#sessionSyncTimer)
86
+ clearInterval(this.#sessionSyncTimer);
87
+ this.#sessionSyncTimer = undefined;
88
+ }
71
89
  constructor(corpora, options = {}) {
72
90
  this.#corpora = corpora;
73
91
  this.#analysisExecutable = options.analysisExecutable;
@@ -186,6 +204,7 @@ export class QmdMemoryRuntime {
186
204
  await (await pending)?.close();
187
205
  }
188
206
  async closeAllMemorySearchManagers() {
207
+ this.stopSessionSyncSchedule();
189
208
  const managers = [...this.#managers.values()];
190
209
  this.#managers.clear();
191
210
  await Promise.all(managers.map(async (pending) => (await pending).close()));
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "id": "unblock-memory",
3
3
  "name": "Unblock Memory",
4
- "version": "0.3.11",
4
+ "version": "0.3.12",
5
5
  "description": "Indexes, retrieves, and analyzes configured workspace memory with existing QMD vectors.",
6
6
  "kind": "memory",
7
- "activation": { "onStartup": false },
7
+ "activation": { "onStartup": true },
8
8
  "skills": ["./skills"],
9
9
  "contracts": {
10
10
  "tools": [
@@ -94,6 +94,13 @@
94
94
  "properties": {
95
95
  "name": { "const": "sessions" },
96
96
  "kind": { "const": "sessions" },
97
+ "syncIntervalMinutes": {
98
+ "type": "integer",
99
+ "minimum": 0,
100
+ "maximum": 1440,
101
+ "default": 15,
102
+ "description": "Refresh sessions every N minutes while the Gateway runs; 0 disables automatic sync. First refresh is after one interval."
103
+ },
97
104
  "chatTypes": {
98
105
  "type": "array",
99
106
  "minItems": 1,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unblocklabs/unblock-memory",
3
- "version": "0.3.11",
3
+ "version": "0.3.12",
4
4
  "description": "Workspace-native memory for OpenClaw, powered by QMD",
5
5
  "type": "module",
6
6
  "license": "MIT",