@digitalforgestudios/openclaw-sulcus 3.9.0 → 3.10.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/index.ts +61 -14
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -104,8 +104,8 @@ const hookHandlers: Record<string, HookHandler> = {
|
|
|
104
104
|
if (!prompt) return;
|
|
105
105
|
try {
|
|
106
106
|
const limit = config.limit ?? 5;
|
|
107
|
-
logger.debug(`sulcus: searching context for prompt: ${prompt.substring(0, 50)}
|
|
108
|
-
const res = await sulcusMem.search_memory(prompt, limit);
|
|
107
|
+
logger.debug(`sulcus: searching context for prompt: ${prompt.substring(0, 50)}... (namespace: ${namespace})`);
|
|
108
|
+
const res = await sulcusMem.search_memory(prompt, limit, namespace);
|
|
109
109
|
const results = res?.results ?? [];
|
|
110
110
|
if (!results || results.length === 0) {
|
|
111
111
|
return { prependSystemContext: FALLBACK_AWARENESS };
|
|
@@ -280,9 +280,10 @@ class SulcusCloudClient {
|
|
|
280
280
|
* search_memory — maps to POST /agent/search
|
|
281
281
|
* Server returns { results: [...] }; we normalise to the results array.
|
|
282
282
|
*/
|
|
283
|
-
async search_memory(query: string, limit?: number): Promise<{ results: any[] }> {
|
|
283
|
+
async search_memory(query: string, limit?: number, namespace?: string): Promise<{ results: any[] }> {
|
|
284
284
|
const body: any = { query };
|
|
285
285
|
if (limit !== undefined) body.limit = limit;
|
|
286
|
+
if (namespace !== undefined) body.namespace = namespace;
|
|
286
287
|
const res = await this.request("POST", "/api/v1/agent/search", body);
|
|
287
288
|
const results = res?.results ?? res?.items ?? res?.nodes ?? (Array.isArray(res) ? res : []);
|
|
288
289
|
return { results };
|
|
@@ -300,12 +301,15 @@ class SulcusCloudClient {
|
|
|
300
301
|
}
|
|
301
302
|
|
|
302
303
|
/**
|
|
303
|
-
* list_hot_nodes — maps to GET /agent/
|
|
304
|
+
* list_hot_nodes — maps to GET /agent/hot_nodes
|
|
304
305
|
* Returns hot_nodes list; normalised for memory_status tool.
|
|
306
|
+
* Note: was incorrectly calling /agent/memory/status which doesn't return hot_nodes.
|
|
305
307
|
*/
|
|
306
|
-
async list_hot_nodes(
|
|
307
|
-
const
|
|
308
|
-
const
|
|
308
|
+
async list_hot_nodes(limit?: number): Promise<{ nodes: any[] }> {
|
|
309
|
+
const q = limit ? `?limit=${limit}` : "";
|
|
310
|
+
const res = await this.request("GET", `/api/v1/agent/hot_nodes${q}`);
|
|
311
|
+
// Server returns an array directly from this endpoint
|
|
312
|
+
const nodes = Array.isArray(res) ? res : (res?.hot_nodes ?? res?.nodes ?? []);
|
|
309
313
|
return { nodes };
|
|
310
314
|
}
|
|
311
315
|
|
|
@@ -501,6 +505,20 @@ const JUNK_PATTERNS = [
|
|
|
501
505
|
/^<<<EXTERNAL_UNTRUSTED_CONTENT/i,
|
|
502
506
|
/^Runtime:/i,
|
|
503
507
|
/tool_call|function_call|<function_calls>/i,
|
|
508
|
+
// Subagent completion events — internal runtime artifacts, not memories
|
|
509
|
+
/\[Inter-session message\]\s*sourceSession=/i,
|
|
510
|
+
/<<<BEGIN_UNTRUSTED_CHILD_RESULT>>>/,
|
|
511
|
+
/<<<END_UNTRUSTED_CHILD_RESULT>>>/,
|
|
512
|
+
/\[Internal task completion event\]/i,
|
|
513
|
+
/^source:\s*subagent/im,
|
|
514
|
+
/session_key:\s*agent:main:subagent:/i,
|
|
515
|
+
// Cron task payloads — system prompts, not meaningful content
|
|
516
|
+
/^Sulcus validation cycle\./i,
|
|
517
|
+
/^Heartbeat prompt:/i,
|
|
518
|
+
/OpenClaw runtime context \(internal\)/i,
|
|
519
|
+
// Credential patterns — should never be stored
|
|
520
|
+
/\b(sk-[a-f0-9]{40,}|Bearer\s+[A-Za-z0-9._~+/=-]{20,})\b/,
|
|
521
|
+
/\b(api[_-]?key|secret|password|token)\s*[:=]\s*["']?[A-Za-z0-9._~+/=-]{16,}/i,
|
|
504
522
|
];
|
|
505
523
|
|
|
506
524
|
function isJunkMemory(text: string): boolean {
|
|
@@ -601,7 +619,8 @@ const toolDefinitions: Record<string, ToolDefinition> = {
|
|
|
601
619
|
description: "Search Sulcus memory for relevant context",
|
|
602
620
|
parameters: Type.Object({
|
|
603
621
|
query: Type.String({ description: "Search query string." }),
|
|
604
|
-
limit: Type.Optional(Type.Number({ default: 5, description: "Maximum number of results to return (1-10)." }))
|
|
622
|
+
limit: Type.Optional(Type.Number({ default: 5, description: "Maximum number of results to return (1-10)." })),
|
|
623
|
+
namespace: Type.Optional(Type.String({ description: "Namespace to search. Defaults to your own namespace. Specify another to cross-search (ACL enforced)." }))
|
|
605
624
|
}),
|
|
606
625
|
},
|
|
607
626
|
options: { name: "memory_recall" },
|
|
@@ -610,11 +629,14 @@ const toolDefinitions: Record<string, ToolDefinition> = {
|
|
|
610
629
|
if (!isAvailable) {
|
|
611
630
|
throw new Error(`Sulcus unavailable: ${nativeLoader.error || "WASM not loaded"}`);
|
|
612
631
|
}
|
|
613
|
-
|
|
632
|
+
// Default to agent's own namespace to prevent cross-namespace bleed.
|
|
633
|
+
// Agent can explicitly pass namespace to search another (ACL enforced server-side).
|
|
634
|
+
const searchNamespace = params.namespace ?? namespace;
|
|
635
|
+
const res = await sulcusMem.search_memory(params.query, params.limit ?? 5, searchNamespace);
|
|
614
636
|
const results = res?.results ?? res?.items ?? res?.nodes ?? res ?? [];
|
|
615
637
|
return {
|
|
616
638
|
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
|
617
|
-
details: { results, backend: backendMode, namespace }
|
|
639
|
+
details: { results, backend: backendMode, namespace: searchNamespace }
|
|
618
640
|
};
|
|
619
641
|
},
|
|
620
642
|
},
|
|
@@ -633,6 +655,7 @@ const toolDefinitions: Record<string, ToolDefinition> = {
|
|
|
633
655
|
Type.Literal("procedural"),
|
|
634
656
|
Type.Literal("fact")
|
|
635
657
|
], { description: "Memory type. preference=user preferences, procedural=how-to/processes, fact=stable knowledge, semantic=concepts/relationships, episodic=events/experiences. Default: episodic" })),
|
|
658
|
+
train: Type.Optional(Type.Boolean({ description: "Signal the SIU to learn from this manual store. When true, this memory+type becomes a positive training example for both SIVU (store=yes) and SICU (type=<memory_type>). Default: false" })),
|
|
636
659
|
}),
|
|
637
660
|
},
|
|
638
661
|
options: { name: "memory_store" },
|
|
@@ -652,9 +675,27 @@ const toolDefinitions: Record<string, ToolDefinition> = {
|
|
|
652
675
|
const res = await sulcusMem.add_memory(params.content, params.memory_type ?? null);
|
|
653
676
|
const nodeId = res?.id ?? "unknown";
|
|
654
677
|
const mtype = params.memory_type || "episodic";
|
|
678
|
+
// If train=true, submit a training signal to the SIU
|
|
679
|
+
let trainResult: string | null = null;
|
|
680
|
+
if (params.train && (sulcusMem as any).request) {
|
|
681
|
+
try {
|
|
682
|
+
await (sulcusMem as any).request("POST", "/api/v2/siu/signal", {
|
|
683
|
+
memory_id: nodeId,
|
|
684
|
+
text: params.content,
|
|
685
|
+
sivu_label: "store",
|
|
686
|
+
sicu_label: mtype,
|
|
687
|
+
source: "agent_manual_store",
|
|
688
|
+
});
|
|
689
|
+
trainResult = "training signal submitted";
|
|
690
|
+
logger.info(`sulcus: SIU training signal sent for memory ${nodeId} (store, ${mtype})`);
|
|
691
|
+
} catch (e: any) {
|
|
692
|
+
trainResult = `training signal failed: ${e.message}`;
|
|
693
|
+
logger.warn(`sulcus: SIU training signal failed: ${e.message}`);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
655
696
|
return {
|
|
656
|
-
content: [{ type: "text", text: `Stored [${mtype}] memory (id: ${nodeId}) → backend: ${backendMode}, namespace: ${namespace}` }],
|
|
657
|
-
details: { id: nodeId, memory_type: mtype, backend: backendMode, namespace, ...res }
|
|
697
|
+
content: [{ type: "text", text: `Stored [${mtype}] memory (id: ${nodeId}) → backend: ${backendMode}, namespace: ${namespace}${trainResult ? ` | SIU: ${trainResult}` : ""}` }],
|
|
698
|
+
details: { id: nodeId, memory_type: mtype, backend: backendMode, namespace, train: trainResult, ...res }
|
|
658
699
|
};
|
|
659
700
|
},
|
|
660
701
|
},
|
|
@@ -683,7 +724,11 @@ const toolDefinitions: Record<string, ToolDefinition> = {
|
|
|
683
724
|
};
|
|
684
725
|
}
|
|
685
726
|
try {
|
|
686
|
-
|
|
727
|
+
// Fetch both status info and hot nodes in parallel
|
|
728
|
+
const [statusInfo, hotNodes] = await Promise.all([
|
|
729
|
+
sulcusMem.request("GET", "/api/v1/agent/memory/status").catch(() => null),
|
|
730
|
+
sulcusMem.list_hot_nodes(20),
|
|
731
|
+
]);
|
|
687
732
|
const nodeList = hotNodes?.nodes ?? hotNodes ?? [];
|
|
688
733
|
const count = Array.isArray(nodeList) ? nodeList.length : 0;
|
|
689
734
|
return {
|
|
@@ -691,10 +736,12 @@ const toolDefinitions: Record<string, ToolDefinition> = {
|
|
|
691
736
|
status: "ok",
|
|
692
737
|
backend: backendMode,
|
|
693
738
|
namespace,
|
|
739
|
+
...(statusInfo?.capabilities ? { capabilities: statusInfo.capabilities } : {}),
|
|
740
|
+
...(statusInfo?.stats ? { stats: statusInfo.stats } : {}),
|
|
694
741
|
hot_node_count: count,
|
|
695
742
|
hot_nodes: nodeList,
|
|
696
743
|
}, null, 2) }],
|
|
697
|
-
details: { status: "ok", backend: backendMode, namespace, count }
|
|
744
|
+
details: { status: "ok", backend: backendMode, namespace, count, ...(statusInfo?.stats ?? {}) }
|
|
698
745
|
};
|
|
699
746
|
} catch (e: any) {
|
|
700
747
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@digitalforgestudios/openclaw-sulcus",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.10.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",
|