@digitalforgestudios/openclaw-sulcus 3.1.1 → 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.
@@ -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
- // ─── PLUGIN ──────────────────────────────────────────────────────────────────
206
-
207
- const sulcusPlugin = {
208
- id: "memory-sulcus",
209
- name: "Sulcus vMMU",
210
- description: "Sulcus-backed vMMU memory for OpenClaw thermodynamic decay, reactive triggers, local-first",
211
- kind: "memory" as const,
212
-
213
- register(api: any) {
214
- // ── Configuration ──
215
- const libDir = api.config?.libDir
216
- ? resolve(api.config.libDir)
217
- : resolve(process.env.HOME || "~", ".sulcus/lib");
218
-
219
- const storeLibPath = api.config?.storeLibPath
220
- ? resolve(api.config.storeLibPath)
221
- : resolve(libDir, process.platform === "darwin" ? "libsulcus_store.dylib" : "libsulcus_store.so");
222
-
223
- const vectorsLibPath = api.config?.vectorsLibPath
224
- ? resolve(api.config.vectorsLibPath)
225
- : resolve(libDir, process.platform === "darwin" ? "libsulcus_vectors.dylib" : "libsulcus_vectors.so");
226
-
227
- const wasmDir = api.config?.wasmDir
228
- ? resolve(api.config.wasmDir)
229
- : resolve(__dirname, "wasm");
230
-
231
- // Default namespace = agent name (prevents everything landing in "default")
232
- const agentId = api.config?.agentId || api.pluginConfig?.agentId;
233
- const namespace = api.config?.namespace === "default" && agentId
234
- ? agentId
235
- : (api.config?.namespace || agentId || "default");
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
- // ── Load native dylibs ──
238
- const nativeLoader = new NativeLibLoader(storeLibPath, vectorsLibPath);
239
- nativeLoader.init(api.logger);
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
- // ── Load WASM module ──
242
- let sulcusMem: any = null;
243
- let backendMode = "unavailable";
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
- if (nativeLoader.loaded) {
246
- const wasmJsPath = resolve(wasmDir, "sulcus_wasm.js");
247
- if (existsSync(wasmJsPath)) {
248
- try {
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
- const queryFn = nativeLoader.makeQueryFn();
254
- const embedFn = nativeLoader.makeEmbedFn();
255
- sulcusMem = SulcusMem.create(queryFn, embedFn);
256
- backendMode = "wasm";
257
- api.logger.info(`memory-sulcus: SulcusMem created via WASM (wasm: ${wasmJsPath})`);
258
- } catch (e: any) {
259
- api.logger.warn(`memory-sulcus: WASM load failed: ${e.message}`);
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
- const isAvailable = sulcusMem !== null;
347
+ return { version: defaults.version, hooks: mergedHooks, tools: mergedTools };
348
+ }
270
349
 
271
- // Update static awareness with runtime info
272
- STATIC_AWARENESS = buildStaticAwareness(backendMode, namespace);
350
+ // ─── TOOL DEFINITIONS ────────────────────────────────────────────────────────
273
351
 
274
- api.logger.info(`memory-sulcus: registered (backend: ${backendMode}, namespace: ${namespace}, available: ${isAvailable})`);
352
+ interface ToolDefinition {
353
+ schema: any;
354
+ options: { name: string };
355
+ makeExecute: (deps: ToolDeps) => (id: string, params: any) => Promise<any>;
356
+ }
275
357
 
276
- // ── Core memory tools ──
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
- api.registerTool({
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,7 +377,10 @@ 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
- async execute(_id: string, params: any) {
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
  }
@@ -293,10 +390,11 @@ const sulcusPlugin = {
293
390
  content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
294
391
  details: { results, backend: backendMode, namespace }
295
392
  };
296
- }
297
- }, { name: "memory_recall" });
393
+ },
394
+ },
298
395
 
299
- api.registerTool({
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,20 +408,21 @@ 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
- async execute(_id: string, params: any) {
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
- api.logger.debug(`memory-sulcus: filtered junk memory: "${(params.content || "").substring(0, 50)}..."`);
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);
328
427
  const nodeId = res?.id ?? "unknown";
329
428
  const mtype = params.memory_type || "episodic";
@@ -331,15 +430,19 @@ const sulcusPlugin = {
331
430
  content: [{ type: "text", text: `Stored [${mtype}] memory (id: ${nodeId}) → backend: ${backendMode}, namespace: ${namespace}` }],
332
431
  details: { id: nodeId, memory_type: mtype, backend: backendMode, namespace, ...res }
333
432
  };
334
- }
335
- }, { name: "memory_store" });
433
+ },
434
+ },
336
435
 
337
- api.registerTool({
436
+ memory_status: {
437
+ schema: {
338
438
  name: "memory_status",
339
439
  label: "Memory Status",
340
440
  description: "Check Sulcus memory backend status: connection, namespace, capabilities, and hot nodes.",
341
441
  parameters: Type.Object({}),
342
- async execute(_id: string, _params: any) {
442
+ },
443
+ options: { name: "memory_status" },
444
+ makeExecute: ({ sulcusMem, backendMode, namespace, nativeLoader, storeLibPath, vectorsLibPath, wasmDir, isAvailable }) =>
445
+ async (_id: string, _params: any) => {
343
446
  if (!isAvailable) {
344
447
  return {
345
448
  content: [{ type: "text", text: JSON.stringify({
@@ -377,57 +480,229 @@ const sulcusPlugin = {
377
480
  }, null, 2) }],
378
481
  };
379
482
  }
380
- }
381
- }, { name: "memory_status" });
382
-
383
- // ── Context injection: before every agent turn ──
384
-
385
- // ── STATIC AWARENESS: fires on EVERY prompt build, unconditionally ──
386
- // This guarantees the LLM always knows Sulcus exists and can use
387
- // memory_store/memory_recall tools, even when autoRecall is off.
388
- // No network call — just a static string describing available tools.
389
- api.on("before_prompt_build", async (_event: any) => {
390
- return { appendSystemContext: STATIC_AWARENESS };
391
- });
392
-
393
- // ── DYNAMIC CONTEXT: fires before each agent turn with live data ──
394
- // GATED by autoRecall config (default: false). No API call unless opt-in.
395
- const autoRecallEnabled = api.config?.autoRecall === true;
396
- api.on("before_agent_start", async (event: any) => {
397
- // Only call the Sulcus API if autoRecall is explicitly enabled
398
- if (!autoRecallEnabled) {
399
- api.logger.debug(`memory-sulcus: autoRecall is disabled, skipping context build`);
400
- return;
401
- }
402
- if (!isAvailable) return;
403
- api.logger.info(`memory-sulcus: before_agent_start hook triggered for agent ${event.agentId}`);
404
- if (!event.prompt) return;
405
- try {
406
- api.logger.debug(`memory-sulcus: searching context for prompt: ${event.prompt.substring(0, 50)}...`);
407
- const res = await sulcusMem.search_memory(event.prompt, 5);
408
- const results = res?.results ?? [];
409
- if (!results || results.length === 0) {
410
- 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"}`);
411
502
  }
412
- // Format results as a concise XML context block
413
- const items = results.map((r: any) =>
414
- ` <memory id="${r.id}" heat="${(r.current_heat ?? r.score ?? 0).toFixed(2)}" type="${r.memory_type ?? "unknown"}">${r.label ?? r.pointer_summary ?? ""}</memory>`
415
- ).join("\n");
416
- const context = `<sulcus_context token_budget="500" namespace="${namespace}">\n${items}\n</sulcus_context>`;
417
- api.logger.info(`memory-sulcus: injecting ${results.length} recalled memories (${context.length} chars)`);
418
- return { prependSystemContext: context };
419
- } catch (e) {
420
- // build_context failed — inject fallback so the LLM isn't flying blind
421
- api.logger.warn(`memory-sulcus: context build failed: ${e} — injecting fallback awareness`);
422
- return { prependSystemContext: FALLBACK_AWARENESS };
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}`);
423
642
  }
424
- });
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})`);
425
653
 
426
- // agent_end: Do NOT auto-record raw conversation turns.
427
- // The LLM has memory_store as a tool — it decides what's worth remembering.
428
- api.on("agent_end", async (event: any) => {
429
- api.logger.debug(`memory-sulcus: agent_end hook triggered for agent ${event.agentId} (no auto-record)`);
430
- });
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
+ }
431
706
 
432
707
  // No service registration needed — no background process to manage
433
708
  }
@@ -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.1.1",
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": {