@digitalforgestudios/openclaw-sulcus 3.1.0 → 3.2.0
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/hooks.defaults.json +29 -0
- package/index.ts +405 -128
- package/openclaw.plugin.json +9 -1
- package/package.json +2 -1
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "hooks-config",
|
|
3
|
+
"version": 1,
|
|
4
|
+
"hooks": {
|
|
5
|
+
"before_prompt_build": {
|
|
6
|
+
"action": "inject_awareness",
|
|
7
|
+
"enabled": true
|
|
8
|
+
},
|
|
9
|
+
"before_agent_start": {
|
|
10
|
+
"action": "auto_recall",
|
|
11
|
+
"enabled": false,
|
|
12
|
+
"limit": 5,
|
|
13
|
+
"minScore": 0.3
|
|
14
|
+
},
|
|
15
|
+
"agent_end": {
|
|
16
|
+
"action": "none",
|
|
17
|
+
"enabled": true
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"tools": {
|
|
21
|
+
"memory_recall": { "enabled": true },
|
|
22
|
+
"memory_store": { "enabled": true },
|
|
23
|
+
"memory_status": { "enabled": true },
|
|
24
|
+
"consolidate": { "enabled": false },
|
|
25
|
+
"export_markdown": { "enabled": false },
|
|
26
|
+
"import_markdown": { "enabled": false },
|
|
27
|
+
"evaluate_triggers": { "enabled": false }
|
|
28
|
+
}
|
|
29
|
+
}
|
package/index.ts
CHANGED
|
@@ -42,6 +42,92 @@ const FALLBACK_AWARENESS = `<sulcus_context token_budget="500">
|
|
|
42
42
|
</cheatsheet>
|
|
43
43
|
</sulcus_context>`;
|
|
44
44
|
|
|
45
|
+
// ─── HOOKS CONFIG TYPES ──────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
interface HookConfig {
|
|
48
|
+
action: string;
|
|
49
|
+
enabled: boolean;
|
|
50
|
+
limit?: number;
|
|
51
|
+
minScore?: number;
|
|
52
|
+
[key: string]: any;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface ToolConfig {
|
|
56
|
+
enabled: boolean;
|
|
57
|
+
[key: string]: any;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface HooksConfig {
|
|
61
|
+
$schema?: string;
|
|
62
|
+
version?: number;
|
|
63
|
+
hooks: Record<string, HookConfig>;
|
|
64
|
+
tools: Record<string, ToolConfig>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface HookHandlerCtx {
|
|
68
|
+
sulcusMem: any;
|
|
69
|
+
backendMode: string;
|
|
70
|
+
namespace: string;
|
|
71
|
+
logger: any;
|
|
72
|
+
nativeError?: string | null;
|
|
73
|
+
storeLibPath?: string;
|
|
74
|
+
vectorsLibPath?: string;
|
|
75
|
+
wasmDir?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
type HookHandler = (event: any, config: HookConfig, ctx: HookHandlerCtx) => Promise<any>;
|
|
79
|
+
|
|
80
|
+
// ─── HOOK HANDLERS ───────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
const hookHandlers: Record<string, HookHandler> = {
|
|
83
|
+
/**
|
|
84
|
+
* inject_awareness — inject static Sulcus awareness into every prompt build.
|
|
85
|
+
* No network call — just a static string describing available tools.
|
|
86
|
+
*/
|
|
87
|
+
inject_awareness: async (_event: any, _config: HookConfig, _ctx: HookHandlerCtx) => {
|
|
88
|
+
return { appendSystemContext: STATIC_AWARENESS };
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* auto_recall — search Sulcus memory for context relevant to the incoming prompt.
|
|
93
|
+
* Only runs when enabled. Falls back to FALLBACK_AWARENESS on error.
|
|
94
|
+
*/
|
|
95
|
+
auto_recall: async (event: any, config: HookConfig, ctx: HookHandlerCtx) => {
|
|
96
|
+
const { sulcusMem, namespace, logger } = ctx;
|
|
97
|
+
if (!sulcusMem) return;
|
|
98
|
+
logger.info(`memory-sulcus: before_agent_start hook triggered for agent ${event.agentId}`);
|
|
99
|
+
if (!event.prompt) return;
|
|
100
|
+
try {
|
|
101
|
+
const limit = config.limit ?? 5;
|
|
102
|
+
logger.debug(`memory-sulcus: searching context for prompt: ${event.prompt.substring(0, 50)}...`);
|
|
103
|
+
const res = await sulcusMem.search_memory(event.prompt, limit);
|
|
104
|
+
const results = res?.results ?? [];
|
|
105
|
+
if (!results || results.length === 0) {
|
|
106
|
+
return { prependSystemContext: FALLBACK_AWARENESS };
|
|
107
|
+
}
|
|
108
|
+
// Format results as a concise XML context block
|
|
109
|
+
const items = results.map((r: any) =>
|
|
110
|
+
` <memory id="${r.id}" heat="${(r.current_heat ?? r.score ?? 0).toFixed(2)}" type="${r.memory_type ?? "unknown"}">${r.label ?? r.pointer_summary ?? ""}</memory>`
|
|
111
|
+
).join("\n");
|
|
112
|
+
const context = `<sulcus_context token_budget="500" namespace="${namespace}">\n${items}\n</sulcus_context>`;
|
|
113
|
+
logger.info(`memory-sulcus: injecting ${results.length} recalled memories (${context.length} chars)`);
|
|
114
|
+
return { prependSystemContext: context };
|
|
115
|
+
} catch (e) {
|
|
116
|
+
// build_context failed — inject fallback so the LLM isn't flying blind
|
|
117
|
+
logger.warn(`memory-sulcus: context build failed: ${e} — injecting fallback awareness`);
|
|
118
|
+
return { prependSystemContext: FALLBACK_AWARENESS };
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* none — no-op handler. Used for hooks that are enabled but should do nothing
|
|
124
|
+
* (e.g., agent_end where we want to log but not auto-record).
|
|
125
|
+
*/
|
|
126
|
+
none: async (event: any, _config: HookConfig, ctx: HookHandlerCtx) => {
|
|
127
|
+
ctx.logger.debug(`memory-sulcus: hook fired (action=none) for agent ${event.agentId ?? "(unknown)"} (no-op)`);
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
|
|
45
131
|
// ─── NATIVE LIB LOADER ──────────────────────────────────────────────────────
|
|
46
132
|
// Loads libsulcus_store.dylib (embedded PG) and libsulcus_vectors.dylib (embeddings)
|
|
47
133
|
// via koffi FFI. Provides queryFn and embedFn callbacks for SulcusMem.create().
|
|
@@ -202,80 +288,88 @@ function isJunkMemory(text: string): boolean {
|
|
|
202
288
|
return false;
|
|
203
289
|
}
|
|
204
290
|
|
|
205
|
-
// ───
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
:
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
291
|
+
// ─── HOOKS CONFIG LOADER ─────────────────────────────────────────────────────
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Load and merge hooks config.
|
|
295
|
+
* Precedence: user config (api.config.hooks/tools) > defaults from hooks.defaults.json
|
|
296
|
+
* Legacy `autoRecall` flag maps to hooks.before_agent_start.enabled for backward compat.
|
|
297
|
+
*/
|
|
298
|
+
function loadHooksConfig(apiConfig: any): HooksConfig {
|
|
299
|
+
// Load defaults
|
|
300
|
+
const defaultsPath = resolve(__dirname, "hooks.defaults.json");
|
|
301
|
+
let defaults: HooksConfig;
|
|
302
|
+
try {
|
|
303
|
+
defaults = JSON.parse(require("fs").readFileSync(defaultsPath, "utf-8")) as HooksConfig;
|
|
304
|
+
} catch (_e) {
|
|
305
|
+
// Fallback inline defaults if file is missing (safety net)
|
|
306
|
+
defaults = {
|
|
307
|
+
version: 1,
|
|
308
|
+
hooks: {
|
|
309
|
+
before_prompt_build: { action: "inject_awareness", enabled: true },
|
|
310
|
+
before_agent_start: { action: "auto_recall", enabled: false, limit: 5, minScore: 0.3 },
|
|
311
|
+
agent_end: { action: "none", enabled: true },
|
|
312
|
+
},
|
|
313
|
+
tools: {
|
|
314
|
+
memory_recall: { enabled: true },
|
|
315
|
+
memory_store: { enabled: true },
|
|
316
|
+
memory_status: { enabled: true },
|
|
317
|
+
consolidate: { enabled: false },
|
|
318
|
+
export_markdown: { enabled: false },
|
|
319
|
+
import_markdown: { enabled: false },
|
|
320
|
+
evaluate_triggers: { enabled: false },
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
}
|
|
236
324
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
325
|
+
// Deep-merge user hook overrides (per-hook object merge, not replace)
|
|
326
|
+
const userHooks: Record<string, Partial<HookConfig>> = apiConfig?.hooks ?? {};
|
|
327
|
+
const userTools: Record<string, Partial<ToolConfig>> = apiConfig?.tools ?? {};
|
|
240
328
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
329
|
+
const mergedHooks: Record<string, HookConfig> = { ...defaults.hooks };
|
|
330
|
+
for (const [name, override] of Object.entries(userHooks)) {
|
|
331
|
+
mergedHooks[name] = { ...(mergedHooks[name] ?? { action: "none", enabled: false }), ...override };
|
|
332
|
+
}
|
|
244
333
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const { SulcusMem, on_init } = require(wasmJsPath);
|
|
250
|
-
// on_init sets up WASM internals (panic hooks etc.)
|
|
251
|
-
if (typeof on_init === "function") on_init();
|
|
334
|
+
const mergedTools: Record<string, ToolConfig> = { ...defaults.tools };
|
|
335
|
+
for (const [name, override] of Object.entries(userTools)) {
|
|
336
|
+
mergedTools[name] = { ...(mergedTools[name] ?? { enabled: false }), ...override };
|
|
337
|
+
}
|
|
252
338
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
backendMode = "unavailable";
|
|
261
|
-
}
|
|
262
|
-
} else {
|
|
263
|
-
api.logger.warn(`memory-sulcus: WASM module not found at ${wasmJsPath}`);
|
|
264
|
-
}
|
|
265
|
-
} else {
|
|
266
|
-
api.logger.warn(`memory-sulcus: native libs unavailable — ${nativeLoader.error}`);
|
|
267
|
-
}
|
|
339
|
+
// ── Legacy compat: autoRecall flag → hooks.before_agent_start.enabled ──
|
|
340
|
+
if (apiConfig?.autoRecall === true) {
|
|
341
|
+
mergedHooks["before_agent_start"] = {
|
|
342
|
+
...(mergedHooks["before_agent_start"] ?? { action: "auto_recall", enabled: false }),
|
|
343
|
+
enabled: true,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
268
346
|
|
|
269
|
-
|
|
347
|
+
return { version: defaults.version, hooks: mergedHooks, tools: mergedTools };
|
|
348
|
+
}
|
|
270
349
|
|
|
271
|
-
|
|
272
|
-
STATIC_AWARENESS = buildStaticAwareness(backendMode, namespace);
|
|
350
|
+
// ─── TOOL DEFINITIONS ────────────────────────────────────────────────────────
|
|
273
351
|
|
|
274
|
-
|
|
352
|
+
interface ToolDefinition {
|
|
353
|
+
schema: any;
|
|
354
|
+
options: { name: string };
|
|
355
|
+
makeExecute: (deps: ToolDeps) => (id: string, params: any) => Promise<any>;
|
|
356
|
+
}
|
|
275
357
|
|
|
276
|
-
|
|
358
|
+
interface ToolDeps {
|
|
359
|
+
sulcusMem: any;
|
|
360
|
+
backendMode: string;
|
|
361
|
+
namespace: string;
|
|
362
|
+
nativeLoader: NativeLibLoader;
|
|
363
|
+
storeLibPath: string;
|
|
364
|
+
vectorsLibPath: string;
|
|
365
|
+
wasmDir: string;
|
|
366
|
+
logger: any;
|
|
367
|
+
isAvailable: boolean;
|
|
368
|
+
}
|
|
277
369
|
|
|
278
|
-
|
|
370
|
+
const toolDefinitions: Record<string, ToolDefinition> = {
|
|
371
|
+
memory_recall: {
|
|
372
|
+
schema: {
|
|
279
373
|
name: "memory_recall",
|
|
280
374
|
label: "Memory Recall",
|
|
281
375
|
description: "Search Sulcus memory for relevant context",
|
|
@@ -283,20 +377,24 @@ const sulcusPlugin = {
|
|
|
283
377
|
query: Type.String({ description: "Search query string." }),
|
|
284
378
|
limit: Type.Optional(Type.Number({ default: 5, description: "Maximum number of results to return (1-10)." }))
|
|
285
379
|
}),
|
|
286
|
-
|
|
380
|
+
},
|
|
381
|
+
options: { name: "memory_recall" },
|
|
382
|
+
makeExecute: ({ sulcusMem, backendMode, namespace, nativeLoader, isAvailable }) =>
|
|
383
|
+
async (_id: string, params: any) => {
|
|
287
384
|
if (!isAvailable) {
|
|
288
385
|
throw new Error(`Sulcus unavailable: ${nativeLoader.error || "WASM not loaded"}`);
|
|
289
386
|
}
|
|
290
387
|
const res = await sulcusMem.search_memory(params.query, params.limit ?? 5);
|
|
291
|
-
const results = res?.results ?? res;
|
|
388
|
+
const results = res?.results ?? res?.items ?? res?.nodes ?? res ?? [];
|
|
292
389
|
return {
|
|
293
390
|
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
|
294
391
|
details: { results, backend: backendMode, namespace }
|
|
295
392
|
};
|
|
296
|
-
}
|
|
297
|
-
|
|
393
|
+
},
|
|
394
|
+
},
|
|
298
395
|
|
|
299
|
-
|
|
396
|
+
memory_store: {
|
|
397
|
+
schema: {
|
|
300
398
|
name: "memory_store",
|
|
301
399
|
label: "Memory Store",
|
|
302
400
|
description: "Record information in Sulcus memory. Supports Markdown formatting. You control the memory type at creation time.",
|
|
@@ -310,34 +408,41 @@ const sulcusPlugin = {
|
|
|
310
408
|
Type.Literal("fact")
|
|
311
409
|
], { description: "Memory type. preference=user preferences, procedural=how-to/processes, fact=stable knowledge, semantic=concepts/relationships, episodic=events/experiences. Default: episodic" })),
|
|
312
410
|
}),
|
|
313
|
-
|
|
411
|
+
},
|
|
412
|
+
options: { name: "memory_store" },
|
|
413
|
+
makeExecute: ({ sulcusMem, backendMode, namespace, nativeLoader, isAvailable, logger }) =>
|
|
414
|
+
async (_id: string, params: any) => {
|
|
314
415
|
// Pre-send junk filter
|
|
315
416
|
if (isJunkMemory(params.content)) {
|
|
316
|
-
|
|
417
|
+
logger.debug(`memory-sulcus: filtered junk memory: "${(params.content || "").substring(0, 50)}..."`);
|
|
317
418
|
return {
|
|
318
419
|
content: [{ type: "text", text: `Filtered: content looks like system noise, not a meaningful memory.` }],
|
|
319
420
|
details: { filtered: true, reason: "junk_pattern" }
|
|
320
421
|
};
|
|
321
422
|
}
|
|
322
|
-
|
|
323
423
|
if (!isAvailable) {
|
|
324
424
|
throw new Error(`Sulcus unavailable: ${nativeLoader.error || "WASM not loaded"}`);
|
|
325
425
|
}
|
|
326
|
-
|
|
327
426
|
const res = await sulcusMem.add_memory(params.content, params.memory_type ?? null);
|
|
427
|
+
const nodeId = res?.id ?? "unknown";
|
|
428
|
+
const mtype = params.memory_type || "episodic";
|
|
328
429
|
return {
|
|
329
|
-
content: [{ type: "text", text: `Stored [${
|
|
330
|
-
details: {
|
|
430
|
+
content: [{ type: "text", text: `Stored [${mtype}] memory (id: ${nodeId}) → backend: ${backendMode}, namespace: ${namespace}` }],
|
|
431
|
+
details: { id: nodeId, memory_type: mtype, backend: backendMode, namespace, ...res }
|
|
331
432
|
};
|
|
332
|
-
}
|
|
333
|
-
|
|
433
|
+
},
|
|
434
|
+
},
|
|
334
435
|
|
|
335
|
-
|
|
436
|
+
memory_status: {
|
|
437
|
+
schema: {
|
|
336
438
|
name: "memory_status",
|
|
337
439
|
label: "Memory Status",
|
|
338
440
|
description: "Check Sulcus memory backend status: connection, namespace, capabilities, and hot nodes.",
|
|
339
441
|
parameters: Type.Object({}),
|
|
340
|
-
|
|
442
|
+
},
|
|
443
|
+
options: { name: "memory_status" },
|
|
444
|
+
makeExecute: ({ sulcusMem, backendMode, namespace, nativeLoader, storeLibPath, vectorsLibPath, wasmDir, isAvailable }) =>
|
|
445
|
+
async (_id: string, _params: any) => {
|
|
341
446
|
if (!isAvailable) {
|
|
342
447
|
return {
|
|
343
448
|
content: [{ type: "text", text: JSON.stringify({
|
|
@@ -375,57 +480,229 @@ const sulcusPlugin = {
|
|
|
375
480
|
}, null, 2) }],
|
|
376
481
|
};
|
|
377
482
|
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
api.logger.debug(`memory-sulcus: autoRecall is disabled, skipping context build`);
|
|
398
|
-
return;
|
|
399
|
-
}
|
|
400
|
-
if (!isAvailable) return;
|
|
401
|
-
api.logger.info(`memory-sulcus: before_agent_start hook triggered for agent ${event.agentId}`);
|
|
402
|
-
if (!event.prompt) return;
|
|
403
|
-
try {
|
|
404
|
-
api.logger.debug(`memory-sulcus: searching context for prompt: ${event.prompt.substring(0, 50)}...`);
|
|
405
|
-
const res = await sulcusMem.search_memory(event.prompt, 5);
|
|
406
|
-
const results = res?.results ?? [];
|
|
407
|
-
if (!results || results.length === 0) {
|
|
408
|
-
return { prependSystemContext: FALLBACK_AWARENESS };
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
|
|
486
|
+
// ── New WASM-capability tools (disabled by default) ──
|
|
487
|
+
|
|
488
|
+
consolidate: {
|
|
489
|
+
schema: {
|
|
490
|
+
name: "consolidate",
|
|
491
|
+
label: "Memory Consolidate",
|
|
492
|
+
description: "Consolidate cold memories: merges, prunes, or archives nodes below the given heat threshold.",
|
|
493
|
+
parameters: Type.Object({
|
|
494
|
+
min_heat: Type.Optional(Type.Number({ default: 0.1, description: "Nodes with heat below this value are candidates for consolidation (0.0–1.0)." }))
|
|
495
|
+
}),
|
|
496
|
+
},
|
|
497
|
+
options: { name: "consolidate" },
|
|
498
|
+
makeExecute: ({ sulcusMem, backendMode, namespace, nativeLoader, isAvailable }) =>
|
|
499
|
+
async (_id: string, params: any) => {
|
|
500
|
+
if (!isAvailable) {
|
|
501
|
+
throw new Error(`Sulcus unavailable: ${nativeLoader.error || "WASM not loaded"}`);
|
|
409
502
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
503
|
+
const res = await sulcusMem.consolidate(params.min_heat ?? 0.1);
|
|
504
|
+
return {
|
|
505
|
+
content: [{ type: "text", text: JSON.stringify({ result: res, backend: backendMode, namespace }, null, 2) }],
|
|
506
|
+
details: { result: res, backend: backendMode, namespace }
|
|
507
|
+
};
|
|
508
|
+
},
|
|
509
|
+
},
|
|
510
|
+
|
|
511
|
+
export_markdown: {
|
|
512
|
+
schema: {
|
|
513
|
+
name: "export_markdown",
|
|
514
|
+
label: "Export Memory (Markdown)",
|
|
515
|
+
description: "Export all memories in the current namespace as a Markdown document.",
|
|
516
|
+
parameters: Type.Object({}),
|
|
517
|
+
},
|
|
518
|
+
options: { name: "export_markdown" },
|
|
519
|
+
makeExecute: ({ sulcusMem, backendMode, namespace, nativeLoader, isAvailable }) =>
|
|
520
|
+
async (_id: string, _params: any) => {
|
|
521
|
+
if (!isAvailable) {
|
|
522
|
+
throw new Error(`Sulcus unavailable: ${nativeLoader.error || "WASM not loaded"}`);
|
|
523
|
+
}
|
|
524
|
+
const markdown: string = await sulcusMem.export_markdown();
|
|
525
|
+
return {
|
|
526
|
+
content: [{ type: "text", text: markdown }],
|
|
527
|
+
details: { backend: backendMode, namespace, length: markdown.length }
|
|
528
|
+
};
|
|
529
|
+
},
|
|
530
|
+
},
|
|
531
|
+
|
|
532
|
+
import_markdown: {
|
|
533
|
+
schema: {
|
|
534
|
+
name: "import_markdown",
|
|
535
|
+
label: "Import Memory (Markdown)",
|
|
536
|
+
description: "Import memories from a Markdown document into the current namespace.",
|
|
537
|
+
parameters: Type.Object({
|
|
538
|
+
text: Type.String({ description: "Markdown content to import into Sulcus memory." })
|
|
539
|
+
}),
|
|
540
|
+
},
|
|
541
|
+
options: { name: "import_markdown" },
|
|
542
|
+
makeExecute: ({ sulcusMem, backendMode, namespace, nativeLoader, isAvailable }) =>
|
|
543
|
+
async (_id: string, params: any) => {
|
|
544
|
+
if (!isAvailable) {
|
|
545
|
+
throw new Error(`Sulcus unavailable: ${nativeLoader.error || "WASM not loaded"}`);
|
|
546
|
+
}
|
|
547
|
+
const res = await sulcusMem.import_markdown(params.text);
|
|
548
|
+
return {
|
|
549
|
+
content: [{ type: "text", text: JSON.stringify({ result: res, backend: backendMode, namespace }, null, 2) }],
|
|
550
|
+
details: { result: res, backend: backendMode, namespace }
|
|
551
|
+
};
|
|
552
|
+
},
|
|
553
|
+
},
|
|
554
|
+
|
|
555
|
+
evaluate_triggers: {
|
|
556
|
+
schema: {
|
|
557
|
+
name: "evaluate_triggers",
|
|
558
|
+
label: "Evaluate Memory Triggers",
|
|
559
|
+
description: "Evaluate reactive memory triggers against an event and context.",
|
|
560
|
+
parameters: Type.Object({
|
|
561
|
+
event: Type.String({ description: "Event name to evaluate triggers against (e.g. 'agent_end', 'user_message')." }),
|
|
562
|
+
context_json: Type.Optional(Type.String({ description: "JSON string of additional context to pass to trigger evaluation." }))
|
|
563
|
+
}),
|
|
564
|
+
},
|
|
565
|
+
options: { name: "evaluate_triggers" },
|
|
566
|
+
makeExecute: ({ sulcusMem, backendMode, namespace, nativeLoader, isAvailable }) =>
|
|
567
|
+
async (_id: string, params: any) => {
|
|
568
|
+
if (!isAvailable) {
|
|
569
|
+
throw new Error(`Sulcus unavailable: ${nativeLoader.error || "WASM not loaded"}`);
|
|
570
|
+
}
|
|
571
|
+
const res = await sulcusMem.evaluate_triggers(params.event, params.context_json ?? "{}");
|
|
572
|
+
return {
|
|
573
|
+
content: [{ type: "text", text: JSON.stringify({ result: res, backend: backendMode, namespace }, null, 2) }],
|
|
574
|
+
details: { result: res, backend: backendMode, namespace }
|
|
575
|
+
};
|
|
576
|
+
},
|
|
577
|
+
},
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
// ─── PLUGIN ──────────────────────────────────────────────────────────────────
|
|
581
|
+
|
|
582
|
+
const sulcusPlugin = {
|
|
583
|
+
id: "memory-sulcus",
|
|
584
|
+
name: "Sulcus vMMU",
|
|
585
|
+
description: "Sulcus-backed vMMU memory for OpenClaw — thermodynamic decay, reactive triggers, local-first",
|
|
586
|
+
kind: "memory" as const,
|
|
587
|
+
|
|
588
|
+
register(api: any) {
|
|
589
|
+
// ── Configuration ──
|
|
590
|
+
const libDir = api.config?.libDir
|
|
591
|
+
? resolve(api.config.libDir)
|
|
592
|
+
: resolve(process.env.HOME || "~", ".sulcus/lib");
|
|
593
|
+
|
|
594
|
+
const storeLibPath = api.config?.storeLibPath
|
|
595
|
+
? resolve(api.config.storeLibPath)
|
|
596
|
+
: resolve(libDir, process.platform === "darwin" ? "libsulcus_store.dylib" : "libsulcus_store.so");
|
|
597
|
+
|
|
598
|
+
const vectorsLibPath = api.config?.vectorsLibPath
|
|
599
|
+
? resolve(api.config.vectorsLibPath)
|
|
600
|
+
: resolve(libDir, process.platform === "darwin" ? "libsulcus_vectors.dylib" : "libsulcus_vectors.so");
|
|
601
|
+
|
|
602
|
+
const wasmDir = api.config?.wasmDir
|
|
603
|
+
? resolve(api.config.wasmDir)
|
|
604
|
+
: resolve(__dirname, "wasm");
|
|
605
|
+
|
|
606
|
+
// Default namespace = agent name (prevents everything landing in "default")
|
|
607
|
+
const agentId = api.config?.agentId || api.pluginConfig?.agentId;
|
|
608
|
+
const namespace = api.config?.namespace === "default" && agentId
|
|
609
|
+
? agentId
|
|
610
|
+
: (api.config?.namespace || agentId || "default");
|
|
611
|
+
|
|
612
|
+
// ── Load hooks config (config-driven dispatch) ──
|
|
613
|
+
const hooksConfig = loadHooksConfig(api.config);
|
|
614
|
+
|
|
615
|
+
// ── Load native dylibs ──
|
|
616
|
+
const nativeLoader = new NativeLibLoader(storeLibPath, vectorsLibPath);
|
|
617
|
+
nativeLoader.init(api.logger);
|
|
618
|
+
|
|
619
|
+
// ── Load WASM module ──
|
|
620
|
+
let sulcusMem: any = null;
|
|
621
|
+
let backendMode = "unavailable";
|
|
622
|
+
|
|
623
|
+
if (nativeLoader.loaded) {
|
|
624
|
+
const wasmJsPath = resolve(wasmDir, "sulcus_wasm.js");
|
|
625
|
+
if (existsSync(wasmJsPath)) {
|
|
626
|
+
try {
|
|
627
|
+
const { SulcusMem, on_init } = require(wasmJsPath);
|
|
628
|
+
// on_init sets up WASM internals (panic hooks etc.)
|
|
629
|
+
if (typeof on_init === "function") on_init();
|
|
630
|
+
|
|
631
|
+
const queryFn = nativeLoader.makeQueryFn();
|
|
632
|
+
const embedFn = nativeLoader.makeEmbedFn();
|
|
633
|
+
sulcusMem = SulcusMem.create(queryFn, embedFn);
|
|
634
|
+
backendMode = "wasm";
|
|
635
|
+
api.logger.info(`memory-sulcus: SulcusMem created via WASM (wasm: ${wasmJsPath})`);
|
|
636
|
+
} catch (e: any) {
|
|
637
|
+
api.logger.warn(`memory-sulcus: WASM load failed: ${e.message}`);
|
|
638
|
+
backendMode = "unavailable";
|
|
639
|
+
}
|
|
640
|
+
} else {
|
|
641
|
+
api.logger.warn(`memory-sulcus: WASM module not found at ${wasmJsPath}`);
|
|
421
642
|
}
|
|
422
|
-
}
|
|
643
|
+
} else {
|
|
644
|
+
api.logger.warn(`memory-sulcus: native libs unavailable — ${nativeLoader.error}`);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const isAvailable = sulcusMem !== null;
|
|
648
|
+
|
|
649
|
+
// Update static awareness with runtime info
|
|
650
|
+
STATIC_AWARENESS = buildStaticAwareness(backendMode, namespace);
|
|
651
|
+
|
|
652
|
+
api.logger.info(`memory-sulcus: registered (backend: ${backendMode}, namespace: ${namespace}, available: ${isAvailable})`);
|
|
423
653
|
|
|
424
|
-
//
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
654
|
+
// ── Shared deps for tool executors ──
|
|
655
|
+
const toolDeps: ToolDeps = {
|
|
656
|
+
sulcusMem,
|
|
657
|
+
backendMode,
|
|
658
|
+
namespace,
|
|
659
|
+
nativeLoader,
|
|
660
|
+
storeLibPath,
|
|
661
|
+
vectorsLibPath,
|
|
662
|
+
wasmDir,
|
|
663
|
+
logger: api.logger,
|
|
664
|
+
isAvailable,
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
// ── Shared context for hook handlers ──
|
|
668
|
+
const handlerCtx: HookHandlerCtx = {
|
|
669
|
+
sulcusMem,
|
|
670
|
+
backendMode,
|
|
671
|
+
namespace,
|
|
672
|
+
logger: api.logger,
|
|
673
|
+
nativeError: nativeLoader.error,
|
|
674
|
+
storeLibPath,
|
|
675
|
+
vectorsLibPath,
|
|
676
|
+
wasmDir,
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
// ── Config-driven hook registration ──
|
|
680
|
+
for (const [hookName, hookConfig] of Object.entries(hooksConfig.hooks)) {
|
|
681
|
+
if (!hookConfig.enabled) continue;
|
|
682
|
+
const handler = hookHandlers[hookConfig.action];
|
|
683
|
+
if (handler) {
|
|
684
|
+
api.on(hookName, (event: any) => handler(event, hookConfig, handlerCtx));
|
|
685
|
+
} else {
|
|
686
|
+
api.logger.warn(`memory-sulcus: unknown hook action "${hookConfig.action}" for hook "${hookName}"`);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// ── Config-driven tool registration ──
|
|
691
|
+
for (const [toolName, toolConfig] of Object.entries(hooksConfig.tools)) {
|
|
692
|
+
if (!toolConfig.enabled) continue;
|
|
693
|
+
const toolDef = toolDefinitions[toolName];
|
|
694
|
+
if (toolDef) {
|
|
695
|
+
const schema = {
|
|
696
|
+
...toolDef.schema,
|
|
697
|
+
async execute(id: string, params: any) {
|
|
698
|
+
return toolDef.makeExecute(toolDeps)(id, params);
|
|
699
|
+
},
|
|
700
|
+
};
|
|
701
|
+
api.registerTool(schema, toolDef.options);
|
|
702
|
+
} else {
|
|
703
|
+
api.logger.warn(`memory-sulcus: unknown tool "${toolName}" in config — skipping`);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
429
706
|
|
|
430
707
|
// No service registration needed — no background process to manage
|
|
431
708
|
}
|
package/openclaw.plugin.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"description": "Reactive, thermodynamic memory for AI agents. Runs entirely local via WASM + native dylibs (embedded PostgreSQL, sulcus-vectors). No network calls. Optional cloud sync via sulcus-sync dylib when serverUrl/apiKey are configured.",
|
|
6
6
|
"privacy": {
|
|
7
7
|
"consentModel": "local-first",
|
|
8
|
-
"consentNote": "Plugin runs entirely in-process. No network calls. All data stays local in ~/.sulcus/. Cloud sync is opt-in via serverUrl/apiKey config, handled by sulcus-sync dylib loaded by sulcus
|
|
8
|
+
"consentNote": "Plugin runs entirely in-process. No network calls. All data stays local in ~/.sulcus/. Cloud sync is opt-in via serverUrl/apiKey config, handled by sulcus-sync dylib loaded by sulcus.",
|
|
9
9
|
"crossNamespaceAccess": "local-only",
|
|
10
10
|
"crossNamespaceNote": "All memory operations are local. Cross-namespace access only applies if cloud sync is configured separately.",
|
|
11
11
|
"dataFlows": [
|
|
@@ -108,6 +108,14 @@
|
|
|
108
108
|
"type": "number",
|
|
109
109
|
"description": "Max memories to auto-capture per agent turn",
|
|
110
110
|
"default": 3
|
|
111
|
+
},
|
|
112
|
+
"hooks": {
|
|
113
|
+
"type": "object",
|
|
114
|
+
"description": "Hook→action mapping overrides. Merged with defaults from hooks.defaults.json. User values win. Example: { \"before_agent_start\": { \"enabled\": true, \"limit\": 10 } }"
|
|
115
|
+
},
|
|
116
|
+
"tools": {
|
|
117
|
+
"type": "object",
|
|
118
|
+
"description": "Tool enable/disable flags. Merged with defaults from hooks.defaults.json. Example: { \"consolidate\": { \"enabled\": true }, \"export_markdown\": { \"enabled\": true } }"
|
|
111
119
|
}
|
|
112
120
|
}
|
|
113
121
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@digitalforgestudios/openclaw-sulcus",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "Sulcus — reactive, thermodynamic memory plugin for OpenClaw. Opt-in persistent memory with heat-based decay, semantic search, and cross-agent sync. Auto-recall and auto-capture disabled by default.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openclaw",
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"index.ts",
|
|
42
42
|
"wasm/",
|
|
43
43
|
"openclaw.plugin.json",
|
|
44
|
+
"hooks.defaults.json",
|
|
44
45
|
"README.md"
|
|
45
46
|
],
|
|
46
47
|
"dependencies": {
|