@digitalforgestudios/openclaw-sulcus 3.8.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.
Files changed (2) hide show
  1. package/index.ts +63 -16
  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
 
@@ -337,7 +341,7 @@ class SulcusCloudClient {
337
341
  }
338
342
 
339
343
  /**
340
- * evaluate_triggers — maps to POST /agent/triggers/evaluate
344
+ * evaluate_triggers — maps to POST /triggers/evaluate
341
345
  */
342
346
  async evaluate_triggers(event: any, contextJson?: string): Promise<any> {
343
347
  const body: any = { event };
@@ -348,7 +352,7 @@ class SulcusCloudClient {
348
352
  body.context = contextJson;
349
353
  }
350
354
  }
351
- return this.request("POST", "/api/v1/agent/triggers/evaluate", body);
355
+ return this.request("POST", "/api/v1/triggers/evaluate", body);
352
356
  }
353
357
  }
354
358
 
@@ -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
- const res = await sulcusMem.search_memory(params.query, params.limit ?? 5);
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
- const hotNodes = await sulcusMem.list_hot_nodes(20);
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.8.0",
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",