@digitalforgestudios/openclaw-sulcus 3.9.0 → 3.11.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.
Files changed (2) hide show
  1. package/index.ts +95 -14
  2. 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/memory/status
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(_limit?: number): Promise<{ nodes: any[] }> {
307
- const res = await this.request("GET", "/api/v1/agent/memory/status");
308
- const nodes = res?.hot_nodes ?? res?.nodes ?? [];
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
 
@@ -318,6 +322,15 @@ class SulcusCloudClient {
318
322
  return this.request("POST", "/api/v1/agent/consolidate", body);
319
323
  }
320
324
 
325
+ /**
326
+ * delete_memory — maps to DELETE /agent/nodes/:id?train=true|false
327
+ * If train=true, snapshots content before deletion and records a 'reject' training signal for SIVU.
328
+ */
329
+ async delete_memory(id: string, train?: boolean): Promise<any> {
330
+ const trainParam = train ? "true" : "false";
331
+ return this.request("DELETE", `/api/v1/agent/nodes/${encodeURIComponent(id)}?train=${trainParam}`);
332
+ }
333
+
321
334
  /**
322
335
  * export_markdown — maps to GET /agent/export?format=markdown
323
336
  * Returns raw markdown string.
@@ -501,6 +514,20 @@ const JUNK_PATTERNS = [
501
514
  /^<<<EXTERNAL_UNTRUSTED_CONTENT/i,
502
515
  /^Runtime:/i,
503
516
  /tool_call|function_call|<function_calls>/i,
517
+ // Subagent completion events — internal runtime artifacts, not memories
518
+ /\[Inter-session message\]\s*sourceSession=/i,
519
+ /<<<BEGIN_UNTRUSTED_CHILD_RESULT>>>/,
520
+ /<<<END_UNTRUSTED_CHILD_RESULT>>>/,
521
+ /\[Internal task completion event\]/i,
522
+ /^source:\s*subagent/im,
523
+ /session_key:\s*agent:main:subagent:/i,
524
+ // Cron task payloads — system prompts, not meaningful content
525
+ /^Sulcus validation cycle\./i,
526
+ /^Heartbeat prompt:/i,
527
+ /OpenClaw runtime context \(internal\)/i,
528
+ // Credential patterns — should never be stored
529
+ /\b(sk-[a-f0-9]{40,}|Bearer\s+[A-Za-z0-9._~+/=-]{20,})\b/,
530
+ /\b(api[_-]?key|secret|password|token)\s*[:=]\s*["']?[A-Za-z0-9._~+/=-]{16,}/i,
504
531
  ];
505
532
 
506
533
  function isJunkMemory(text: string): boolean {
@@ -601,7 +628,8 @@ const toolDefinitions: Record<string, ToolDefinition> = {
601
628
  description: "Search Sulcus memory for relevant context",
602
629
  parameters: Type.Object({
603
630
  query: Type.String({ description: "Search query string." }),
604
- limit: Type.Optional(Type.Number({ default: 5, description: "Maximum number of results to return (1-10)." }))
631
+ limit: Type.Optional(Type.Number({ default: 5, description: "Maximum number of results to return (1-10)." })),
632
+ namespace: Type.Optional(Type.String({ description: "Namespace to search. Defaults to your own namespace. Specify another to cross-search (ACL enforced)." }))
605
633
  }),
606
634
  },
607
635
  options: { name: "memory_recall" },
@@ -610,11 +638,14 @@ const toolDefinitions: Record<string, ToolDefinition> = {
610
638
  if (!isAvailable) {
611
639
  throw new Error(`Sulcus unavailable: ${nativeLoader.error || "WASM not loaded"}`);
612
640
  }
613
- const res = await sulcusMem.search_memory(params.query, params.limit ?? 5);
641
+ // Default to agent's own namespace to prevent cross-namespace bleed.
642
+ // Agent can explicitly pass namespace to search another (ACL enforced server-side).
643
+ const searchNamespace = params.namespace ?? namespace;
644
+ const res = await sulcusMem.search_memory(params.query, params.limit ?? 5, searchNamespace);
614
645
  const results = res?.results ?? res?.items ?? res?.nodes ?? res ?? [];
615
646
  return {
616
647
  content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
617
- details: { results, backend: backendMode, namespace }
648
+ details: { results, backend: backendMode, namespace: searchNamespace }
618
649
  };
619
650
  },
620
651
  },
@@ -633,6 +664,7 @@ const toolDefinitions: Record<string, ToolDefinition> = {
633
664
  Type.Literal("procedural"),
634
665
  Type.Literal("fact")
635
666
  ], { description: "Memory type. preference=user preferences, procedural=how-to/processes, fact=stable knowledge, semantic=concepts/relationships, episodic=events/experiences. Default: episodic" })),
667
+ 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
668
  }),
637
669
  },
638
670
  options: { name: "memory_store" },
@@ -652,9 +684,27 @@ const toolDefinitions: Record<string, ToolDefinition> = {
652
684
  const res = await sulcusMem.add_memory(params.content, params.memory_type ?? null);
653
685
  const nodeId = res?.id ?? "unknown";
654
686
  const mtype = params.memory_type || "episodic";
687
+ // If train=true, submit a training signal to the SIU
688
+ let trainResult: string | null = null;
689
+ if (params.train && (sulcusMem as any).request) {
690
+ try {
691
+ await (sulcusMem as any).request("POST", "/api/v2/siu/signal", {
692
+ memory_id: nodeId,
693
+ text: params.content,
694
+ sivu_label: "store",
695
+ sicu_label: mtype,
696
+ source: "agent_manual_store",
697
+ });
698
+ trainResult = "training signal submitted";
699
+ logger.info(`sulcus: SIU training signal sent for memory ${nodeId} (store, ${mtype})`);
700
+ } catch (e: any) {
701
+ trainResult = `training signal failed: ${e.message}`;
702
+ logger.warn(`sulcus: SIU training signal failed: ${e.message}`);
703
+ }
704
+ }
655
705
  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 }
706
+ content: [{ type: "text", text: `Stored [${mtype}] memory (id: ${nodeId}) → backend: ${backendMode}, namespace: ${namespace}${trainResult ? ` | SIU: ${trainResult}` : ""}` }],
707
+ details: { id: nodeId, memory_type: mtype, backend: backendMode, namespace, train: trainResult, ...res }
658
708
  };
659
709
  },
660
710
  },
@@ -683,7 +733,11 @@ const toolDefinitions: Record<string, ToolDefinition> = {
683
733
  };
684
734
  }
685
735
  try {
686
- const hotNodes = await sulcusMem.list_hot_nodes(20);
736
+ // Fetch both status info and hot nodes in parallel
737
+ const [statusInfo, hotNodes] = await Promise.all([
738
+ sulcusMem.request("GET", "/api/v1/agent/memory/status").catch(() => null),
739
+ sulcusMem.list_hot_nodes(20),
740
+ ]);
687
741
  const nodeList = hotNodes?.nodes ?? hotNodes ?? [];
688
742
  const count = Array.isArray(nodeList) ? nodeList.length : 0;
689
743
  return {
@@ -691,10 +745,12 @@ const toolDefinitions: Record<string, ToolDefinition> = {
691
745
  status: "ok",
692
746
  backend: backendMode,
693
747
  namespace,
748
+ ...(statusInfo?.capabilities ? { capabilities: statusInfo.capabilities } : {}),
749
+ ...(statusInfo?.stats ? { stats: statusInfo.stats } : {}),
694
750
  hot_node_count: count,
695
751
  hot_nodes: nodeList,
696
752
  }, null, 2) }],
697
- details: { status: "ok", backend: backendMode, namespace, count }
753
+ details: { status: "ok", backend: backendMode, namespace, count, ...(statusInfo?.stats ?? {}) }
698
754
  };
699
755
  } catch (e: any) {
700
756
  return {
@@ -802,6 +858,31 @@ const toolDefinitions: Record<string, ToolDefinition> = {
802
858
  },
803
859
  },
804
860
 
861
+ memory_delete: {
862
+ schema: {
863
+ name: "memory_delete",
864
+ label: "Delete Memory",
865
+ description: "Delete a memory node by ID. With train=true (default), the deleted content trains SIVU to reject similar content in the future. Use this to clean up junk, duplicates, or noise memories.",
866
+ parameters: Type.Object({
867
+ id: Type.String({ description: "Memory node ID to delete." }),
868
+ train: Type.Optional(Type.Boolean({ default: true, description: "Train SIVU to reject similar content (default true). Set false to delete without training." })),
869
+ }),
870
+ },
871
+ options: { name: "memory_delete" },
872
+ makeExecute: ({ sulcusMem, backendMode, namespace, nativeLoader, isAvailable }) =>
873
+ async (_id: string, params: any) => {
874
+ if (!isAvailable) {
875
+ throw new Error(`Sulcus unavailable: ${nativeLoader.error || "WASM not loaded"}`);
876
+ }
877
+ const train = params.train !== false; // default true
878
+ const res = await (sulcusMem as SulcusCloudClient).delete_memory(params.id, train);
879
+ return {
880
+ content: [{ type: "text", text: `Deleted memory ${params.id}${train ? " (trained SIVU to reject similar)" : ""}. Backend: ${backendMode}, namespace: ${namespace}` }],
881
+ details: { id: params.id, trained: train, result: res, backend: backendMode, namespace }
882
+ };
883
+ },
884
+ },
885
+
805
886
  // ── SIU v2 Tools ───────────────────────────────────────────────────────────
806
887
  // These tools call the SIU v2 server endpoints for text classification.
807
888
  // Requires cloud backend (serverUrl + apiKey). Uses /api/v2/siu/* endpoints.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@digitalforgestudios/openclaw-sulcus",
3
- "version": "3.9.0",
3
+ "version": "3.11.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",