openclaw-hybrid-memory 2026.2.171

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 ADDED
@@ -0,0 +1,40 @@
1
+ # OpenClaw memory-hybrid plugin
2
+
3
+ Hybrid memory plugin: **SQLite + FTS5** for structured facts and **LanceDB** for semantic search. Part of the [OpenClaw Hybrid Memory](https://github.com/markus-lassfolk/openclaw-hybrid-memory) v3 deployment.
4
+
5
+ **Repository:** [GitHub](https://github.com/markus-lassfolk/openclaw-hybrid-memory) · **Docs:** [v3 deployment guide](https://github.com/markus-lassfolk/openclaw-hybrid-memory/blob/main/docs/hybrid-memory-manager-v3.md) · [README / Quick Start](https://github.com/markus-lassfolk/openclaw-hybrid-memory#quick-start)
6
+
7
+ ## Credits
8
+
9
+ Based on the design in **[Give Your Clawdbot Permanent Memory](https://clawdboss.ai/posts/give-your-clawdbot-permanent-memory)** (Clawdboss.ai). The plugin has since been extended with auto-capture, auto-recall, decay/TTL, auto-classify, token caps, consolidation, verify/uninstall CLI, and more — see the repo root and [hybrid-memory-manager-v3.md](../../docs/hybrid-memory-manager-v3.md).
10
+
11
+ ## Requirements
12
+
13
+ - **OpenAI API key** — Required. The plugin uses it for embeddings (default model `text-embedding-3-small`); without a valid `embedding.apiKey` in config the plugin does not load. Optional features (auto-classify, summarize, consolidate) use the same key with a chat model (e.g. `gpt-4o-mini`). See the [v3 guide](../../docs/hybrid-memory-manager-v3.md) §1.5 and §4.
14
+ - **Build tools** for `better-sqlite3`: C++ toolchain (e.g. `build-essential` on Linux, Visual Studio Build Tools on Windows), Python 3.
15
+
16
+ ## Installation (use the v3 guide)
17
+
18
+ **Do not use the old setup prompts** in `docs/archive/` (SETUP-PROMPT-1..4). They target an older plugin version and do not match the current `index.ts` / `config.ts`. They are kept for **credit and history only**.
19
+
20
+ - **Install:** Follow the [Hybrid Memory Manager v3](../../docs/hybrid-memory-manager-v3.md) guide (§3, §8): copy this `memory-hybrid` directory into your OpenClaw extensions folder, run `npm install` in this directory, then configure and restart per v3.
21
+ - **Quick path:** See the repo root [README.md](../../README.md) and [SETUP-AUTONOMOUS.md](../../docs/SETUP-AUTONOMOUS.md) for the single deployment flow.
22
+
23
+ ## Files in this directory
24
+
25
+ | File | Description |
26
+ |------|-------------|
27
+ | `package.json` | npm package and OpenClaw extension entry |
28
+ | `openclaw.plugin.json` | Plugin manifest and config schema |
29
+ | `config.ts` | Decay classes, TTL defaults, config parsing (incl. autoRecall, store, etc.) |
30
+ | `index.ts` | Plugin implementation (SQLite+FTS5, LanceDB, tools, CLI, lifecycle) |
31
+ | `versionInfo.ts` | Plugin and memory-manager version metadata |
32
+
33
+ ## Dependencies
34
+
35
+ - `better-sqlite3` ^11.0.0
36
+ - `@lancedb/lancedb` ^0.23.0
37
+ - `openai` ^6.16.0
38
+ - `@sinclair/typebox` 0.34.47
39
+
40
+ Build tools required for `better-sqlite3`: C++ toolchain (e.g. `build-essential` on Linux, Visual Studio Build Tools on Windows), Python 3.
package/config.ts ADDED
@@ -0,0 +1,331 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+
4
+ export const DECAY_CLASSES = [
5
+ "permanent",
6
+ "stable",
7
+ "active",
8
+ "session",
9
+ "checkpoint",
10
+ ] as const;
11
+ export type DecayClass = (typeof DECAY_CLASSES)[number];
12
+
13
+ /** TTL defaults in seconds per decay class. null = never expires. */
14
+ export const TTL_DEFAULTS: Record<DecayClass, number | null> = {
15
+ permanent: null,
16
+ stable: 90 * 24 * 3600, // 90 days
17
+ active: 14 * 24 * 3600, // 14 days
18
+ session: 24 * 3600, // 24 hours
19
+ checkpoint: 4 * 3600, // 4 hours
20
+ };
21
+
22
+ export type AutoClassifyConfig = {
23
+ enabled: boolean;
24
+ model: string; // e.g. "gpt-4.1-nano", "gpt-4o-mini", or any chat model
25
+ batchSize: number; // facts per LLM call (default 20)
26
+ };
27
+
28
+ /** Auto-recall injection line format: full = [backend/category] text, short = category: text, minimal = text only */
29
+ export type AutoRecallInjectionFormat = "full" | "short" | "minimal";
30
+
31
+ /** Entity-centric recall: when prompt mentions an entity from the list, merge lookup(entity) facts into candidates */
32
+ export type EntityLookupConfig = {
33
+ enabled: boolean;
34
+ entities: string[]; // e.g. ["user", "owner", "decision"]; prompt matched case-insensitively
35
+ maxFactsPerEntity: number; // max facts to merge per matched entity (default 2)
36
+ };
37
+
38
+ /** Auto-recall: enable/disable plus token cap, format, limit, minScore, preferLongTerm, importance/recency, entity lookup, summary */
39
+ export type AutoRecallConfig = {
40
+ enabled: boolean;
41
+ maxTokens: number;
42
+ maxPerMemoryChars: number;
43
+ injectionFormat: AutoRecallInjectionFormat;
44
+ limit: number;
45
+ minScore: number;
46
+ preferLongTerm: boolean;
47
+ useImportanceRecency: boolean;
48
+ entityLookup: EntityLookupConfig;
49
+ summaryThreshold: number; // facts longer than this get a summary stored; 0 = disabled (default 300)
50
+ summaryMaxChars: number; // summary length when generated (default 80)
51
+ useSummaryInInjection: boolean; // inject summary instead of full text when present (default true)
52
+ summarizeWhenOverBudget: boolean; // when token cap forces dropping memories, LLM-summarize all into 2-3 sentences (1.4)
53
+ summarizeModel: string; // model for summarize-when-over-budget (default gpt-4o-mini)
54
+ };
55
+
56
+ /** Store options: fuzzy dedupe (2.3) uses normalized-text hash to skip near-duplicate facts. */
57
+ export type StoreConfig = {
58
+ fuzzyDedupe: boolean;
59
+ };
60
+
61
+ /** Credential types supported by the credentials store */
62
+ export const CREDENTIAL_TYPES = [
63
+ "token",
64
+ "password",
65
+ "api_key",
66
+ "ssh",
67
+ "bearer",
68
+ "other",
69
+ ] as const;
70
+ export type CredentialType = (typeof CREDENTIAL_TYPES)[number];
71
+
72
+ /** Opt-in credentials: structured, encrypted storage for API keys, tokens, etc. */
73
+ export type CredentialsConfig = {
74
+ enabled: boolean;
75
+ store: "sqlite";
76
+ /** Encryption key: "env:VAR_NAME" resolves from env, or raw string (not recommended) */
77
+ encryptionKey: string;
78
+ /** When enabled, detect credential patterns in conversation and prompt to store (default false) */
79
+ autoDetect?: boolean;
80
+ /** Days before expiry to warn (default 7) */
81
+ expiryWarningDays?: number;
82
+ };
83
+
84
+ export type HybridMemoryConfig = {
85
+ embedding: {
86
+ provider: "openai";
87
+ model: string;
88
+ apiKey: string;
89
+ };
90
+ lanceDbPath: string;
91
+ sqlitePath: string;
92
+ autoCapture: boolean;
93
+ autoRecall: AutoRecallConfig;
94
+ /** Max characters per captured/stored fact (filter and truncation). Default 5000. */
95
+ captureMaxChars: number;
96
+ categories: string[];
97
+ autoClassify: AutoClassifyConfig;
98
+ /** Store options (2.3): fuzzyDedupe = skip store when normalized text matches existing. */
99
+ store: StoreConfig;
100
+ /** Opt-in credential management: structured, encrypted storage (default: disabled) */
101
+ credentials: CredentialsConfig;
102
+ };
103
+
104
+ /** Default categories — can be extended via config.categories */
105
+ export const DEFAULT_MEMORY_CATEGORIES = [
106
+ "preference",
107
+ "fact",
108
+ "decision",
109
+ "entity",
110
+ "other",
111
+ ] as const;
112
+
113
+ /** Runtime categories: starts as defaults, extended by config */
114
+ let _runtimeCategories: string[] = [...DEFAULT_MEMORY_CATEGORIES];
115
+
116
+ export function getMemoryCategories(): readonly string[] {
117
+ return _runtimeCategories;
118
+ }
119
+
120
+ export function setMemoryCategories(categories: string[]): void {
121
+ // Always include defaults + any custom ones, deduplicated
122
+ const merged = new Set([...DEFAULT_MEMORY_CATEGORIES, ...categories]);
123
+ _runtimeCategories = [...merged];
124
+ }
125
+
126
+ export function isValidCategory(cat: string): boolean {
127
+ return _runtimeCategories.includes(cat);
128
+ }
129
+
130
+ export type MemoryCategory = string;
131
+
132
+ const DEFAULT_MODEL = "text-embedding-3-small";
133
+ const DEFAULT_LANCE_PATH = join(homedir(), ".openclaw", "memory", "lancedb");
134
+ const DEFAULT_SQLITE_PATH = join(homedir(), ".openclaw", "memory", "facts.db");
135
+
136
+ const EMBEDDING_DIMENSIONS: Record<string, number> = {
137
+ "text-embedding-3-small": 1536,
138
+ "text-embedding-3-large": 3072,
139
+ };
140
+
141
+ export function vectorDimsForModel(model: string): number {
142
+ const dims = EMBEDDING_DIMENSIONS[model];
143
+ if (!dims) throw new Error(`Unsupported embedding model: ${model}`);
144
+ return dims;
145
+ }
146
+
147
+ function resolveEnvVars(value: string): string {
148
+ return value.replace(/\$\{([^}]+)\}/g, (_, envVar) => {
149
+ const envValue = process.env[envVar];
150
+ if (!envValue) throw new Error(`Environment variable ${envVar} is not set`);
151
+ return envValue;
152
+ });
153
+ }
154
+
155
+ export const hybridConfigSchema = {
156
+ parse(value: unknown): HybridMemoryConfig {
157
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
158
+ throw new Error("memory-hybrid config required");
159
+ }
160
+ const cfg = value as Record<string, unknown>;
161
+
162
+ const embedding = cfg.embedding as Record<string, unknown> | undefined;
163
+ if (!embedding || typeof embedding.apiKey !== "string") {
164
+ throw new Error("embedding.apiKey is required. Set it in plugins.entries[\"memory-hybrid\"].config.embedding. Run 'openclaw hybrid-mem verify --fix' for help.");
165
+ }
166
+ const rawKey = (embedding.apiKey as string).trim();
167
+ if (rawKey.length < 10 || rawKey === "YOUR_OPENAI_API_KEY" || rawKey === "<OPENAI_API_KEY>") {
168
+ throw new Error("embedding.apiKey is missing or a placeholder. Set a valid OpenAI API key in config. Run 'openclaw hybrid-mem verify --fix' for help.");
169
+ }
170
+
171
+ const model =
172
+ typeof embedding.model === "string" ? embedding.model : DEFAULT_MODEL;
173
+ vectorDimsForModel(model);
174
+
175
+ // Parse custom categories
176
+ const customCategories: string[] = Array.isArray(cfg.categories)
177
+ ? (cfg.categories as string[]).filter((c) => typeof c === "string" && c.length > 0)
178
+ : [];
179
+
180
+ // Merge into runtime categories
181
+ if (customCategories.length > 0) {
182
+ setMemoryCategories(customCategories);
183
+ }
184
+
185
+ // Parse autoClassify config
186
+ // Model default: cheapest available chat model. Use "gpt-4o-mini" as a
187
+ // safe default; users can override with any model their API key supports.
188
+ const acCfg = cfg.autoClassify as Record<string, unknown> | undefined;
189
+ const autoClassify: AutoClassifyConfig = {
190
+ enabled: acCfg?.enabled === true,
191
+ model: typeof acCfg?.model === "string" ? acCfg.model : "gpt-4o-mini",
192
+ batchSize: typeof acCfg?.batchSize === "number" ? acCfg.batchSize : 20,
193
+ };
194
+
195
+ // Parse autoRecall: boolean (legacy) or { enabled?, maxTokens?, maxPerMemoryChars?, injectionFormat? }
196
+ const arRaw = cfg.autoRecall;
197
+ const VALID_FORMATS = ["full", "short", "minimal"] as const;
198
+ let autoRecall: AutoRecallConfig;
199
+ if (typeof arRaw === "object" && arRaw !== null && !Array.isArray(arRaw)) {
200
+ const ar = arRaw as Record<string, unknown>;
201
+ const format = typeof ar.injectionFormat === "string" && VALID_FORMATS.includes(ar.injectionFormat as typeof VALID_FORMATS[number])
202
+ ? (ar.injectionFormat as AutoRecallInjectionFormat)
203
+ : "full";
204
+ const limit = typeof ar.limit === "number" && ar.limit > 0 ? Math.floor(ar.limit) : 5;
205
+ const minScore = typeof ar.minScore === "number" && ar.minScore >= 0 && ar.minScore <= 1 ? ar.minScore : 0.3;
206
+ const preferLongTerm = ar.preferLongTerm === true;
207
+ const useImportanceRecency = ar.useImportanceRecency === true;
208
+ const entityLookupRaw = ar.entityLookup as Record<string, unknown> | undefined;
209
+ const entityLookup: EntityLookupConfig = {
210
+ enabled: entityLookupRaw?.enabled === true,
211
+ entities: Array.isArray(entityLookupRaw?.entities)
212
+ ? (entityLookupRaw.entities as string[]).filter((e) => typeof e === "string" && e.length > 0)
213
+ : [],
214
+ maxFactsPerEntity:
215
+ typeof entityLookupRaw?.maxFactsPerEntity === "number" && entityLookupRaw.maxFactsPerEntity > 0
216
+ ? Math.floor(entityLookupRaw.maxFactsPerEntity)
217
+ : 2,
218
+ };
219
+ const summaryThreshold =
220
+ typeof ar.summaryThreshold === "number" && ar.summaryThreshold >= 0 ? ar.summaryThreshold : 300;
221
+ const summaryMaxChars =
222
+ typeof ar.summaryMaxChars === "number" && ar.summaryMaxChars > 0 ? Math.min(ar.summaryMaxChars, 500) : 80;
223
+ const useSummaryInInjection = ar.useSummaryInInjection !== false;
224
+ const summarizeWhenOverBudget = ar.summarizeWhenOverBudget === true;
225
+ const summarizeModel = typeof ar.summarizeModel === "string" ? ar.summarizeModel : "gpt-4o-mini";
226
+ autoRecall = {
227
+ enabled: ar.enabled !== false,
228
+ maxTokens: typeof ar.maxTokens === "number" && ar.maxTokens > 0 ? ar.maxTokens : 800,
229
+ maxPerMemoryChars: typeof ar.maxPerMemoryChars === "number" && ar.maxPerMemoryChars >= 0 ? ar.maxPerMemoryChars : 0,
230
+ injectionFormat: format,
231
+ limit,
232
+ minScore,
233
+ preferLongTerm,
234
+ useImportanceRecency,
235
+ entityLookup,
236
+ summaryThreshold,
237
+ summaryMaxChars,
238
+ useSummaryInInjection,
239
+ summarizeWhenOverBudget,
240
+ summarizeModel,
241
+ };
242
+ } else {
243
+ autoRecall = {
244
+ enabled: arRaw !== false,
245
+ maxTokens: 800,
246
+ maxPerMemoryChars: 0,
247
+ injectionFormat: "full",
248
+ limit: 5,
249
+ minScore: 0.3,
250
+ preferLongTerm: false,
251
+ useImportanceRecency: false,
252
+ entityLookup: { enabled: false, entities: [], maxFactsPerEntity: 2 },
253
+ summaryThreshold: 300,
254
+ summaryMaxChars: 80,
255
+ useSummaryInInjection: true,
256
+ summarizeWhenOverBudget: false,
257
+ summarizeModel: "gpt-4o-mini",
258
+ };
259
+ }
260
+
261
+ const captureMaxChars =
262
+ typeof cfg.captureMaxChars === "number" && cfg.captureMaxChars > 0
263
+ ? cfg.captureMaxChars
264
+ : 5000;
265
+
266
+ const storeRaw = cfg.store as Record<string, unknown> | undefined;
267
+ const store: StoreConfig = {
268
+ fuzzyDedupe: storeRaw?.fuzzyDedupe === true,
269
+ };
270
+
271
+ // Parse credentials config (opt-in). Enable automatically when a valid encryption key is set.
272
+ const credRaw = cfg.credentials as Record<string, unknown> | undefined;
273
+ const explicitlyDisabled = credRaw?.enabled === false;
274
+ const encKeyRaw = typeof credRaw?.encryptionKey === "string" ? credRaw.encryptionKey : "";
275
+ let encryptionKey = "";
276
+ if (encKeyRaw.startsWith("env:")) {
277
+ const envVar = encKeyRaw.slice(4).trim();
278
+ const val = process.env[envVar];
279
+ if (val) encryptionKey = val;
280
+ } else if (encKeyRaw.length >= 16) {
281
+ encryptionKey = encKeyRaw;
282
+ }
283
+ const hasValidKey = encryptionKey.length >= 16;
284
+ const shouldEnable = !explicitlyDisabled && (credRaw?.enabled === true || hasValidKey);
285
+
286
+ let credentials: CredentialsConfig;
287
+ if (shouldEnable && hasValidKey) {
288
+ credentials = {
289
+ enabled: true,
290
+ store: "sqlite",
291
+ encryptionKey,
292
+ autoDetect: credRaw?.autoDetect === true,
293
+ expiryWarningDays: typeof credRaw?.expiryWarningDays === "number" && credRaw.expiryWarningDays >= 0
294
+ ? Math.floor(credRaw.expiryWarningDays)
295
+ : 7,
296
+ };
297
+ } else if (shouldEnable && !hasValidKey) {
298
+ if (encKeyRaw.startsWith("env:")) {
299
+ throw new Error(`Credentials encryption key env var ${encKeyRaw.slice(4).trim()} is not set. Run 'openclaw hybrid-mem verify --fix' for help.`);
300
+ }
301
+ throw new Error("credentials.encryptionKey must be at least 16 characters (or use env:VAR). Run 'openclaw hybrid-mem verify --fix' for help.");
302
+ } else {
303
+ credentials = {
304
+ enabled: false,
305
+ store: "sqlite",
306
+ encryptionKey: "",
307
+ autoDetect: false,
308
+ expiryWarningDays: 7,
309
+ };
310
+ }
311
+
312
+ return {
313
+ embedding: {
314
+ provider: "openai",
315
+ model,
316
+ apiKey: resolveEnvVars(embedding.apiKey),
317
+ },
318
+ lanceDbPath:
319
+ typeof cfg.lanceDbPath === "string" ? cfg.lanceDbPath : DEFAULT_LANCE_PATH,
320
+ sqlitePath:
321
+ typeof cfg.sqlitePath === "string" ? cfg.sqlitePath : DEFAULT_SQLITE_PATH,
322
+ autoCapture: cfg.autoCapture !== false,
323
+ autoRecall,
324
+ captureMaxChars,
325
+ categories: [...getMemoryCategories()],
326
+ autoClassify,
327
+ store,
328
+ credentials,
329
+ };
330
+ },
331
+ };