@digitalforgestudios/openclaw-sulcus 3.6.0 → 3.8.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/README.md CHANGED
@@ -51,6 +51,46 @@ Then restart: `openclaw gateway restart`
51
51
  | `memory_store` | Store with auto-detected type |
52
52
  | `memory_forget` | Delete by ID |
53
53
 
54
+ ## Hooks & Extended Tools
55
+
56
+ All hooks and tools are **enabled by default**. Override in your `openclaw.json` plugin config:
57
+
58
+ ```json
59
+ "openclaw-sulcus": {
60
+ "enabled": true,
61
+ "config": {
62
+ "apiKey": "sk-YOUR_KEY_HERE",
63
+ "hooks": {
64
+ "before_prompt_build": { "action": "inject_awareness", "enabled": true },
65
+ "before_agent_start": { "action": "auto_recall", "enabled": true, "limit": 5, "minScore": 0.3 },
66
+ "agent_end": { "action": "none", "enabled": true }
67
+ },
68
+ "tools": {
69
+ "memory_recall": { "enabled": true },
70
+ "memory_store": { "enabled": true },
71
+ "memory_status": { "enabled": true },
72
+ "consolidate": { "enabled": true },
73
+ "export_markdown": { "enabled": true },
74
+ "import_markdown": { "enabled": true },
75
+ "evaluate_triggers": { "enabled": true }
76
+ }
77
+ }
78
+ }
79
+ ```
80
+
81
+ | Hook | Default | Description |
82
+ |---|---|---|
83
+ | `before_prompt_build` | ON | Inject memory awareness context into prompts |
84
+ | `before_agent_start` | ON | Auto-recall relevant memories each turn |
85
+ | `agent_end` | ON | Post-turn processing |
86
+
87
+ | Extended Tool | Default | Description |
88
+ |---|---|---|
89
+ | `consolidate` | ON | Cluster and merge similar memories |
90
+ | `export_markdown` | ON | Export memories as markdown |
91
+ | `import_markdown` | ON | Import memories from markdown |
92
+ | `evaluate_triggers` | ON | Run reactive trigger evaluations |
93
+
54
94
  ## Features
55
95
 
56
96
  - **Auto-recall** — relevant memories injected before each agent turn
@@ -18,12 +18,16 @@
18
18
  }
19
19
  },
20
20
  "tools": {
21
- "memory_recall": { "enabled": false },
22
- "memory_store": { "enabled": false },
23
- "memory_status": { "enabled": false },
21
+ "memory_recall": { "enabled": true },
22
+ "memory_store": { "enabled": true },
23
+ "memory_status": { "enabled": true },
24
24
  "consolidate": { "enabled": false },
25
25
  "export_markdown": { "enabled": false },
26
26
  "import_markdown": { "enabled": false },
27
- "evaluate_triggers": { "enabled": false }
27
+ "evaluate_triggers": { "enabled": false },
28
+ "siu_label": { "enabled": false },
29
+ "siu_status": { "enabled": false },
30
+ "siu_retrain": { "enabled": false },
31
+ "trigger_feedback": { "enabled": false }
28
32
  }
29
33
  }
package/index.ts CHANGED
@@ -98,12 +98,14 @@ const hookHandlers: Record<string, HookHandler> = {
98
98
  auto_recall: async (event: any, config: HookConfig, ctx: HookHandlerCtx) => {
99
99
  const { sulcusMem, namespace, logger } = ctx;
100
100
  if (!sulcusMem) return;
101
- logger.info(`sulcus: before_agent_start hook triggered for agent ${event.agentId}`);
102
- if (!event.prompt) return;
101
+ const agentLabel = event?.agentId ?? "(unknown)";
102
+ logger.info(`sulcus: before_agent_start hook triggered for agent ${agentLabel}`);
103
+ const prompt = typeof event?.prompt === "string" ? event.prompt : "";
104
+ if (!prompt) return;
103
105
  try {
104
106
  const limit = config.limit ?? 5;
105
- logger.debug(`sulcus: searching context for prompt: ${event.prompt.substring(0, 50)}...`);
106
- const res = await sulcusMem.search_memory(event.prompt, limit);
107
+ logger.debug(`sulcus: searching context for prompt: ${prompt.substring(0, 50)}...`);
108
+ const res = await sulcusMem.search_memory(prompt, limit);
107
109
  const results = res?.results ?? [];
108
110
  if (!results || results.length === 0) {
109
111
  return { prependSystemContext: FALLBACK_AWARENESS };
@@ -129,6 +131,74 @@ const hookHandlers: Record<string, HookHandler> = {
129
131
  none: async (event: any, _config: HookConfig, ctx: HookHandlerCtx) => {
130
132
  ctx.logger.debug(`sulcus: hook fired (action=none) for agent ${event.agentId ?? "(unknown)"} (no-op)`);
131
133
  },
134
+
135
+ /**
136
+ * sivu_auto_capture — SIU v2 quality-gated auto-capture on agent_end.
137
+ *
138
+ * When fired after each turn, extracts the user message from the event,
139
+ * runs it through SIVU (store/reject gate) and SICU (type classifier),
140
+ * and stores the memory only if SIVU approves. Falls back to basic
141
+ * junk-filtering + episodic capture if SIU v2 endpoint is unavailable.
142
+ *
143
+ * Config options:
144
+ * min_store_confidence: number (default 0.5) — minimum SIVU confidence to store
145
+ * fallback_on_error: boolean (default true) — store as episodic if SIU unavailable
146
+ */
147
+ sivu_auto_capture: async (event: any, config: HookConfig, ctx: HookHandlerCtx) => {
148
+ const { sulcusMem, logger } = ctx;
149
+ if (!sulcusMem) return;
150
+
151
+ // Extract user message from the event
152
+ const userMessage = event?.userMessage ?? event?.prompt ?? event?.text ?? "";
153
+ if (!userMessage || typeof userMessage !== "string") {
154
+ logger.debug("sulcus: sivu_auto_capture — no user message in event, skipping");
155
+ return;
156
+ }
157
+
158
+ // Pre-filter obvious junk before hitting the API
159
+ if (isJunkMemory(userMessage)) {
160
+ logger.debug(`sulcus: sivu_auto_capture — pre-filtered junk: "${userMessage.substring(0, 50)}..."`);
161
+ return;
162
+ }
163
+
164
+ const minConfidence = config.min_store_confidence ?? 0.5;
165
+ const fallbackOnError = config.fallback_on_error !== false; // default true
166
+
167
+ // Try SIU v2 endpoint for quality-gated classification
168
+ if (sulcusMem instanceof SulcusCloudClient) {
169
+ try {
170
+ const siuResult = await (sulcusMem as SulcusCloudClient).request("POST", "/api/v2/siu/label", {
171
+ text: userMessage,
172
+ });
173
+
174
+ const shouldStore = siuResult?.store === true && (siuResult?.store_confidence ?? 0) >= minConfidence;
175
+ const memoryType = siuResult?.memory_type ?? "episodic";
176
+ const modelVersion = siuResult?.model_version ?? "unknown";
177
+
178
+ if (!shouldStore) {
179
+ logger.info(`sulcus: sivu_auto_capture — SIVU rejected (confidence: ${siuResult?.store_confidence?.toFixed(3) ?? "?"}, model: ${modelVersion}): "${userMessage.substring(0, 60)}..."`);
180
+ return;
181
+ }
182
+
183
+ // SIVU approved — store with SICU-predicted type
184
+ const res = await sulcusMem.add_memory(userMessage, memoryType);
185
+ logger.info(`sulcus: sivu_auto_capture — stored [${memoryType}] (id: ${res?.id ?? "?"}, sivu_conf: ${siuResult?.store_confidence?.toFixed(3)}, sicu_conf: ${siuResult?.type_confidence?.toFixed(3)}, model: ${modelVersion}): "${userMessage.substring(0, 60)}..."`);
186
+ return;
187
+ } catch (e: any) {
188
+ logger.warn(`sulcus: sivu_auto_capture — SIU v2 endpoint error: ${e.message}`);
189
+ if (!fallbackOnError) return;
190
+ // Fall through to basic capture
191
+ }
192
+ }
193
+
194
+ // Fallback: store as episodic (no SIU gating available)
195
+ try {
196
+ const res = await sulcusMem.add_memory(userMessage, "episodic");
197
+ logger.info(`sulcus: sivu_auto_capture — fallback stored [episodic] (id: ${res?.id ?? "?"}): "${userMessage.substring(0, 60)}..."`);
198
+ } catch (e: any) {
199
+ logger.warn(`sulcus: sivu_auto_capture — fallback store failed: ${e.message}`);
200
+ }
201
+ },
132
202
  };
133
203
 
134
204
  // ─── CLOUD HTTP CLIENT ───────────────────────────────────────────────────────
@@ -147,7 +217,7 @@ class SulcusCloudClient {
147
217
  }
148
218
 
149
219
  /** Low-level HTTP helper. Returns parsed JSON response body. */
150
- private request(method: string, path: string, body?: any): Promise<any> {
220
+ request(method: string, path: string, body?: any): Promise<any> {
151
221
  return new Promise((resolve, reject) => {
152
222
  let parsedUrl: URL;
153
223
  try {
@@ -519,6 +589,8 @@ interface ToolDeps {
519
589
  wasmDir: string;
520
590
  logger: any;
521
591
  isAvailable: boolean;
592
+ /** HTTP request helper for SIU v2 endpoints — null when cloud backend is not configured. */
593
+ siuRequest: ((method: string, path: string, body?: any) => Promise<any>) | null;
522
594
  }
523
595
 
524
596
  const toolDefinitions: Record<string, ToolDefinition> = {
@@ -729,6 +801,155 @@ const toolDefinitions: Record<string, ToolDefinition> = {
729
801
  };
730
802
  },
731
803
  },
804
+
805
+ // ── SIU v2 Tools ───────────────────────────────────────────────────────────
806
+ // These tools call the SIU v2 server endpoints for text classification.
807
+ // Requires cloud backend (serverUrl + apiKey). Uses /api/v2/siu/* endpoints.
808
+
809
+ siu_label: {
810
+ schema: {
811
+ name: "siu_label",
812
+ label: "SIU Label",
813
+ description: "Classify text using SIU v2 — returns SIVU store/reject decision and SICU memory type classification with confidence scores.",
814
+ parameters: Type.Object({
815
+ text: Type.String({ description: "Text to classify." }),
816
+ classify_only: Type.Optional(Type.Boolean({ description: "Skip SIVU quality gate, only run SICU type classification." })),
817
+ }),
818
+ },
819
+ options: { name: "siu_label" },
820
+ makeExecute: ({ backendMode, siuRequest, logger }) =>
821
+ async (_id: string, params: any) => {
822
+ if (!siuRequest) {
823
+ return {
824
+ content: [{ type: "text", text: "SIU label requires cloud backend (serverUrl + apiKey)." }],
825
+ details: { error: "cloud_required" },
826
+ };
827
+ }
828
+ try {
829
+ const res = await siuRequest("POST", "/api/v2/siu/label", {
830
+ text: params.text,
831
+ classify_only: params.classify_only ?? false,
832
+ });
833
+ return {
834
+ content: [{ type: "text", text: JSON.stringify(res, null, 2) }],
835
+ details: res,
836
+ };
837
+ } catch (e: any) {
838
+ logger.warn(`sulcus: siu_label failed: ${e.message}`);
839
+ return {
840
+ content: [{ type: "text", text: `SIU label failed: ${e.message}` }],
841
+ details: { error: e.message },
842
+ };
843
+ }
844
+ },
845
+ },
846
+
847
+ siu_status: {
848
+ schema: {
849
+ name: "siu_status",
850
+ label: "SIU Status",
851
+ description: "Check SIU v2 model availability, deployed versions, and training signal statistics.",
852
+ parameters: Type.Object({}),
853
+ },
854
+ options: { name: "siu_status" },
855
+ makeExecute: ({ siuRequest, logger }) =>
856
+ async (_id: string, _params: any) => {
857
+ if (!siuRequest) {
858
+ return {
859
+ content: [{ type: "text", text: "SIU status requires cloud backend (serverUrl + apiKey)." }],
860
+ details: { error: "cloud_required" },
861
+ };
862
+ }
863
+ try {
864
+ const res = await siuRequest("GET", "/api/v2/siu/status");
865
+ return {
866
+ content: [{ type: "text", text: JSON.stringify(res, null, 2) }],
867
+ details: res,
868
+ };
869
+ } catch (e: any) {
870
+ logger.warn(`sulcus: siu_status failed: ${e.message}`);
871
+ return {
872
+ content: [{ type: "text", text: `SIU status failed: ${e.message}` }],
873
+ details: { error: e.message },
874
+ };
875
+ }
876
+ },
877
+ },
878
+
879
+ siu_retrain: {
880
+ schema: {
881
+ name: "siu_retrain",
882
+ label: "SIU Retrain",
883
+ description: "Trigger an async retrain of SIU v2 models using accumulated training signals. Returns job status.",
884
+ parameters: Type.Object({}),
885
+ },
886
+ options: { name: "siu_retrain" },
887
+ makeExecute: ({ siuRequest, logger }) =>
888
+ async (_id: string, _params: any) => {
889
+ if (!siuRequest) {
890
+ return {
891
+ content: [{ type: "text", text: "SIU retrain requires cloud backend (serverUrl + apiKey)." }],
892
+ details: { error: "cloud_required" },
893
+ };
894
+ }
895
+ try {
896
+ const res = await siuRequest("POST", "/api/v2/siu/retrain");
897
+ return {
898
+ content: [{ type: "text", text: JSON.stringify(res, null, 2) }],
899
+ details: res,
900
+ };
901
+ } catch (e: any) {
902
+ logger.warn(`sulcus: siu_retrain failed: ${e.message}`);
903
+ return {
904
+ content: [{ type: "text", text: `SIU retrain failed: ${e.message}` }],
905
+ details: { error: e.message },
906
+ };
907
+ }
908
+ },
909
+ },
910
+ trigger_feedback: {
911
+ schema: {
912
+ name: "trigger_feedback",
913
+ label: "Trigger Feedback",
914
+ description:
915
+ "Record feedback on a trigger fire (for SITU training). Use to report false positives (fired but shouldn't have), false negatives (should have fired but didn't), or confirm correct fires.",
916
+ parameters: Type.Object({
917
+ feedback_type: Type.String({
918
+ description:
919
+ 'One of: "false_positive" (fired wrongly), "false_negative" (missed fire), "correct" (good fire), "wrong_action" (fired but wrong action)',
920
+ }),
921
+ trigger_id: Type.Optional(Type.String({ description: "UUID of the trigger rule" })),
922
+ trigger_log_id: Type.Optional(Type.String({ description: "UUID of the trigger fire log entry" })),
923
+ event_type: Type.Optional(Type.String({ description: "Event type: memory_created, heat_threshold, recall, etc." })),
924
+ memory_id: Type.Optional(Type.String({ description: "UUID of the memory involved" })),
925
+ expected_action: Type.Optional(Type.String({ description: "What should have happened: fire, no_fire, different_action" })),
926
+ notes: Type.Optional(Type.String({ description: "Free-text explanation of the feedback" })),
927
+ }),
928
+ },
929
+ options: { name: "trigger_feedback" },
930
+ makeExecute: ({ siuRequest, logger }) =>
931
+ async (_id: string, params: any) => {
932
+ if (!siuRequest) {
933
+ return {
934
+ content: [{ type: "text", text: "Trigger feedback requires cloud backend (serverUrl + apiKey)." }],
935
+ details: { error: "cloud_required" },
936
+ };
937
+ }
938
+ try {
939
+ const res = await siuRequest("POST", "/api/v1/triggers/feedback", params);
940
+ return {
941
+ content: [{ type: "text", text: JSON.stringify(res, null, 2) }],
942
+ details: res,
943
+ };
944
+ } catch (e: any) {
945
+ logger.warn(`sulcus: trigger_feedback failed: ${e.message}`);
946
+ return {
947
+ content: [{ type: "text", text: `Trigger feedback failed: ${e.message}` }],
948
+ details: { error: e.message },
949
+ };
950
+ }
951
+ },
952
+ },
732
953
  };
733
954
 
734
955
  // ─── PLUGIN ──────────────────────────────────────────────────────────────────
@@ -832,6 +1053,12 @@ const sulcusPlugin = {
832
1053
  api.logger.warn(`sulcus: ✗ unavailable — ${hints.join("; ") || "unknown reason"}. Configure serverUrl+apiKey for cloud, or install native dylibs for local.`);
833
1054
  }
834
1055
 
1056
+ // ── SIU v2 request helper (bound to cloud client if available) ──
1057
+ // SIU endpoints live on the same server as the Sulcus API.
1058
+ const siuRequestFn = (backendMode === "cloud" && sulcusMem instanceof SulcusCloudClient)
1059
+ ? (method: string, path: string, body?: any) => (sulcusMem as SulcusCloudClient).request(method, path, body)
1060
+ : null;
1061
+
835
1062
  // ── Shared deps for tool executors ──
836
1063
  const toolDeps: ToolDeps = {
837
1064
  sulcusMem,
@@ -843,6 +1070,7 @@ const sulcusPlugin = {
843
1070
  wasmDir,
844
1071
  logger: api.logger,
845
1072
  isAvailable,
1073
+ siuRequest: siuRequestFn,
846
1074
  };
847
1075
 
848
1076
  // ── Shared context for hook handlers ──
@@ -858,11 +1086,21 @@ const sulcusPlugin = {
858
1086
  };
859
1087
 
860
1088
  // ── Config-driven hook registration ──
1089
+ // Each handler is wrapped in a defensive try-catch to prevent plugin errors
1090
+ // from crashing the host agent's startup pipeline (OpenClaw bug workaround:
1091
+ // normalizeResolvedModel() doesn't guard params.model being undefined).
861
1092
  for (const [hookName, hookConfig] of Object.entries(hooksConfig.hooks)) {
862
1093
  if (!hookConfig.enabled) continue;
863
1094
  const handler = hookHandlers[hookConfig.action];
864
1095
  if (handler) {
865
- api.on(hookName, (event: any) => handler(event, hookConfig, handlerCtx));
1096
+ api.on(hookName, async (event: any) => {
1097
+ try {
1098
+ return await handler(event, hookConfig, handlerCtx);
1099
+ } catch (err) {
1100
+ api.logger.warn(`sulcus: hook "${hookName}" (action=${hookConfig.action}) threw: ${err} — returning empty result`);
1101
+ return undefined;
1102
+ }
1103
+ });
866
1104
  } else {
867
1105
  api.logger.warn(`sulcus: unknown hook action "${hookConfig.action}" for hook "${hookName}"`);
868
1106
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@digitalforgestudios/openclaw-sulcus",
3
- "version": "3.6.0",
3
+ "version": "3.8.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",