@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.
- package/hooks.defaults.json +29 -0
- package/index.ts +400 -125
- package/openclaw.plugin.json +8 -0
- 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,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
|
-
|
|
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
|
-
|
|
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,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
|
-
|
|
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);
|
|
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
|
-
|
|
433
|
+
},
|
|
434
|
+
},
|
|
336
435
|
|
|
337
|
-
|
|
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
|
-
|
|
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
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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
|
-
//
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
}
|
package/openclaw.plugin.json
CHANGED
|
@@ -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": {
|