@digitalforgestudios/openclaw-sulcus 4.0.0 → 4.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.
@@ -15,6 +15,14 @@
15
15
  "agent_end": {
16
16
  "action": "none",
17
17
  "enabled": false
18
+ },
19
+ "after_tool_call": {
20
+ "action": "auto_error_capture",
21
+ "enabled": true
22
+ },
23
+ "before_compaction": {
24
+ "action": "pre_compaction_capture",
25
+ "enabled": true
18
26
  }
19
27
  },
20
28
  "tools": {
@@ -28,6 +36,7 @@
28
36
  "siu_label": { "enabled": false },
29
37
  "siu_status": { "enabled": false },
30
38
  "siu_retrain": { "enabled": false },
31
- "trigger_feedback": { "enabled": false }
39
+ "trigger_feedback": { "enabled": false },
40
+ "__sulcus_workflow__": { "enabled": true }
32
41
  }
33
42
  }
package/index.ts CHANGED
@@ -144,6 +144,11 @@ const hookHandlers: Record<string, HookHandler> = {
144
144
  return;
145
145
  }
146
146
 
147
+ if (!shouldCapture(userMessage)) {
148
+ logger.debug?.("sulcus: sivu_auto_capture — dedup skip");
149
+ return;
150
+ }
151
+
147
152
  const minConfidence = (config.min_store_confidence as number) ?? 0.5;
148
153
  const fallbackOnError = config.fallback_on_error !== false;
149
154
 
@@ -179,6 +184,111 @@ const hookHandlers: Record<string, HookHandler> = {
179
184
  logger.warn(`sulcus: sivu_auto_capture — fallback store failed: ${msg}`);
180
185
  }
181
186
  },
187
+
188
+ /**
189
+ * auto_error_capture — stores tool errors as episodic memories with boosted heat.
190
+ *
191
+ * Fires on after_tool_call when a tool returns an error.
192
+ * Stores the error context so the agent learns from past failures.
193
+ * Skips errors from Sulcus's own tools to avoid self-referential loops.
194
+ */
195
+ auto_error_capture: async (event: any, _config: HookConfig, ctx: HookHandlerCtx) => {
196
+ const { sulcusMem, logger } = ctx;
197
+ const errorText = event?.error?.trim?.();
198
+ if (!errorText || !sulcusMem) return; // No error or no backend — nothing to capture
199
+
200
+ const toolName = event?.toolName ?? event?.tool_name ?? "unknown";
201
+
202
+ // Skip errors from our own tools to prevent capture loops
203
+ if (typeof toolName === "string" && (
204
+ toolName.startsWith("memory_") ||
205
+ toolName.startsWith("sulcus_") ||
206
+ toolName === "consolidate" ||
207
+ toolName === "evaluate_triggers" ||
208
+ toolName === "export_markdown" ||
209
+ toolName === "import_markdown" ||
210
+ toolName === "siu_label" ||
211
+ toolName === "siu_retrain"
212
+ )) {
213
+ return;
214
+ }
215
+
216
+ // Normalize + truncate error text
217
+ const normalized = errorText.replace(/\s+/g, " ").trim();
218
+ const truncated = normalized.length > 500 ? normalized.slice(0, 500) + " [truncated]" : normalized;
219
+ const memoryContent = `Tool '${toolName}' failed: ${truncated}`;
220
+
221
+ try {
222
+ const res = await sulcusMem.add_memory(memoryContent, "episodic");
223
+ // Boost heat so error memories persist longer — failures are high-value learnings
224
+ if (res?.id && sulcusMem instanceof SulcusCloudClient) {
225
+ await sulcusMem.request("PATCH", `/api/v1/agent/memory/${res.id}`, {
226
+ current_heat: 0.8,
227
+ }).catch(() => {}); // best-effort boost
228
+ }
229
+ logger.info(`sulcus: auto_error_capture — stored tool error [episodic] (id: ${res?.id ?? "?"}): "${memoryContent.substring(0, 80)}..."`);
230
+ } catch (e: unknown) {
231
+ const msg = e instanceof Error ? e.message : String(e);
232
+ logger.debug?.(`sulcus: auto_error_capture — failed to store: ${msg}`);
233
+ }
234
+ },
235
+
236
+ pre_compaction_capture: async (event: Record<string, unknown>, _config: HookConfig, ctx: HookHandlerCtx) => {
237
+ const { sulcusMem, logger } = ctx;
238
+ if (!sulcusMem) return;
239
+
240
+ const messages = Array.isArray(event?.messages) ? event.messages as Record<string, unknown>[] : [];
241
+ if (messages.length === 0) return;
242
+
243
+ const firstUser = messages.find((m) => m.role === "user" || m.type === "human");
244
+ const lastAssistant = [...messages].reverse().find((m) => m.role === "assistant" || m.type === "ai");
245
+
246
+ const firstUserText = typeof firstUser?.content === "string"
247
+ ? firstUser.content.substring(0, 200)
248
+ : typeof firstUser?.text === "string"
249
+ ? (firstUser.text as string).substring(0, 200)
250
+ : "(none)";
251
+
252
+ const lastAssistantText = typeof lastAssistant?.content === "string"
253
+ ? lastAssistant.content.substring(0, 200)
254
+ : typeof lastAssistant?.text === "string"
255
+ ? (lastAssistant.text as string).substring(0, 200)
256
+ : "(none)";
257
+
258
+ const filesModified: string[] = [];
259
+ for (const msg of messages) {
260
+ const toolCalls = Array.isArray(msg.tool_calls) ? msg.tool_calls as Record<string, unknown>[] : [];
261
+ for (const tc of toolCalls) {
262
+ const name = (tc.name ?? tc.function) as string | undefined;
263
+ if (name === "Write" || name === "Edit" || name === "write" || name === "edit") {
264
+ const input = (tc.input ?? tc.arguments ?? {}) as Record<string, unknown>;
265
+ const fp = input?.file_path ?? input?.path;
266
+ if (fp && typeof fp === "string" && !filesModified.includes(fp)) filesModified.push(fp);
267
+ }
268
+ }
269
+ }
270
+
271
+ const summaryParts = [
272
+ `Session compaction — ${messages.length} messages`,
273
+ `First user message: ${firstUserText}`,
274
+ `Last assistant message: ${lastAssistantText}`,
275
+ ];
276
+ if (filesModified.length > 0) summaryParts.push(`Files modified: ${filesModified.join(", ")}`);
277
+ const summary = summaryParts.join("\n");
278
+
279
+ if (!shouldCapture(summary)) {
280
+ logger.debug?.("sulcus: pre_compaction_capture — dedup skip");
281
+ return;
282
+ }
283
+
284
+ try {
285
+ const res = await sulcusMem.add_memory(summary, "episodic");
286
+ logger.info(`sulcus: pre_compaction_capture — stored session summary (id: ${res?.id ?? "?"})`);
287
+ } catch (e: unknown) {
288
+ const msg = e instanceof Error ? e.message : String(e);
289
+ logger.debug?.(`sulcus: pre_compaction_capture — store failed: ${msg}`);
290
+ }
291
+ },
182
292
  };
183
293
 
184
294
  // ─── CLOUD HTTP CLIENT ───────────────────────────────────────────────────────
@@ -461,6 +571,22 @@ function isJunkMemory(text: string): boolean {
461
571
  return false;
462
572
  }
463
573
 
574
+ // ─── CAPTURE DEDUP ───────────────────────────────────────────────────────────
575
+
576
+ const captureDedup = new Map<string, number>();
577
+ const DEDUP_WINDOW_MS = 5 * 60 * 1000; // 5 minutes
578
+
579
+ function shouldCapture(content: string): boolean {
580
+ const key = content.substring(0, 120) + "|" + content.length;
581
+ const now = Date.now();
582
+ for (const [k, ts] of captureDedup.entries()) {
583
+ if (now - ts > DEDUP_WINDOW_MS) captureDedup.delete(k);
584
+ }
585
+ if (captureDedup.has(key)) return false;
586
+ captureDedup.set(key, now);
587
+ return true;
588
+ }
589
+
464
590
  // ─── HOOKS CONFIG LOADER ─────────────────────────────────────────────────────
465
591
 
466
592
  function loadHooksConfig(apiConfig: Record<string, unknown>): HooksConfig {
@@ -476,6 +602,8 @@ function loadHooksConfig(apiConfig: Record<string, unknown>): HooksConfig {
476
602
  before_prompt_build: { action: "inject_awareness", enabled: true },
477
603
  before_agent_start: { action: "auto_recall", enabled: false, limit: 5, minScore: 0.3 },
478
604
  agent_end: { action: "none", enabled: true },
605
+ after_tool_call: { action: "auto_error_capture", enabled: true },
606
+ before_compaction: { action: "pre_compaction_capture", enabled: true },
479
607
  },
480
608
  tools: {
481
609
  memory_recall: { enabled: true },
@@ -485,6 +613,7 @@ function loadHooksConfig(apiConfig: Record<string, unknown>): HooksConfig {
485
613
  export_markdown: { enabled: false },
486
614
  import_markdown: { enabled: false },
487
615
  evaluate_triggers: { enabled: false },
616
+ __sulcus_workflow__: { enabled: true },
488
617
  },
489
618
  };
490
619
  }
@@ -1006,8 +1135,99 @@ const toolDefinitions: Record<string, ToolDefinition> = {
1006
1135
  }
1007
1136
  },
1008
1137
  },
1138
+
1139
+ __sulcus_workflow__: {
1140
+ schema: {
1141
+ name: "__sulcus_workflow__",
1142
+ label: "Sulcus Workflow",
1143
+ description: "Call this when you are unsure what to do next with Sulcus memory tools. Returns a step-by-step workflow checklist so you always know the right action.",
1144
+ parameters: Type.Object({}),
1145
+ },
1146
+ options: { name: "__sulcus_workflow__" },
1147
+ makeExecute: (_deps: ToolDeps) =>
1148
+ async (_id: string, _params: Record<string, unknown>) => {
1149
+ const workflow = [
1150
+ { step: 1, action: "search first", tool: "memory_recall", description: "Before starting work, search memory for relevant context from prior sessions." },
1151
+ { step: 2, action: "store decisions/patterns/learnings", tool: "memory_store", description: "After significant work, store important decisions, patterns, corrections, or learnings." },
1152
+ { step: 3, action: "boost important memories", tool: "PATCH /api/v1/agent/memory/:id", description: "Use PATCH to set current_heat=0.9 on memories that should persist longer (memory_boost not yet exposed as a tool)." },
1153
+ { step: 4, action: "check triggers", tool: "evaluate_triggers", description: "Evaluate reactive rules to see if any triggers should fire based on current context." },
1154
+ { step: 5, action: "export if needed", tool: "export_markdown", description: "Export all memories as Markdown for backup or review." },
1155
+ ];
1156
+ return {
1157
+ content: [{ type: "text", text: JSON.stringify(workflow, null, 2) }],
1158
+ details: { workflow: workflow as unknown as Record<string, unknown> },
1159
+ };
1160
+ },
1161
+ },
1009
1162
  };
1010
1163
 
1164
+ // ─── FIRST-INSTALL HISTORY IMPORT ────────────────────────────────────────────
1165
+
1166
+ async function importOpenClawHistory(sulcusMem: SulcusCloudClient, logger: PluginLogger): Promise<void> {
1167
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
1168
+ const fs = require("fs") as {
1169
+ existsSync: (p: string) => boolean;
1170
+ readFileSync: (p: string, enc: string) => string;
1171
+ readdirSync: (p: string) => string[];
1172
+ statSync: (p: string) => { mtimeMs: number };
1173
+ writeFileSync: (p: string, d: string, enc: string) => void;
1174
+ };
1175
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
1176
+ const path = require("path") as { join: (...args: string[]) => string };
1177
+
1178
+ const workspaceDir = process.env.OPENCLAW_WORKSPACE
1179
+ ? resolve(process.env.OPENCLAW_WORKSPACE)
1180
+ : resolve(process.env.HOME || "~", ".openclaw/workspace");
1181
+ const markerPath = path.join(workspaceDir, ".sulcus-imported");
1182
+
1183
+ if (fs.existsSync(markerPath)) return;
1184
+
1185
+ logger.info("sulcus: first-install history import starting...");
1186
+
1187
+ const memories: string[] = [];
1188
+
1189
+ const memoryMdPath = path.join(workspaceDir, "MEMORY.md");
1190
+ if (fs.existsSync(memoryMdPath)) {
1191
+ try {
1192
+ const text = fs.readFileSync(memoryMdPath, "utf-8");
1193
+ const entries = text.split(/\n(?:---+|\s*\n\s*\n)/g).map((s) => s.trim()).filter((s) => s.length > 20);
1194
+ memories.push(...entries);
1195
+ } catch (_e) { /* best-effort */ }
1196
+ }
1197
+
1198
+ const memDir = path.join(workspaceDir, "memory");
1199
+ if (fs.existsSync(memDir)) {
1200
+ try {
1201
+ const files = fs.readdirSync(memDir);
1202
+ const now = Date.now();
1203
+ const thirtyDaysMs = 30 * 24 * 60 * 60 * 1000;
1204
+ for (const file of files) {
1205
+ if (!/^\d{4}-\d{2}-\d{2}\.md$/.test(file)) continue;
1206
+ try {
1207
+ const stat = fs.statSync(path.join(memDir, file));
1208
+ if (now - stat.mtimeMs > thirtyDaysMs) continue;
1209
+ const text = fs.readFileSync(path.join(memDir, file), "utf-8");
1210
+ const entries = text.split(/\n---\n/g).map((s) => s.trim()).filter((s) => s.length > 20);
1211
+ memories.push(...entries);
1212
+ } catch (_e) { /* best-effort */ }
1213
+ }
1214
+ } catch (_e) { /* best-effort */ }
1215
+ }
1216
+
1217
+ let stored = 0;
1218
+ for (const mem of memories) {
1219
+ try {
1220
+ await sulcusMem.add_memory(mem, "episodic");
1221
+ stored++;
1222
+ } catch (_e) { /* best-effort */ }
1223
+ }
1224
+
1225
+ try {
1226
+ fs.writeFileSync(markerPath, new Date().toISOString(), "utf-8");
1227
+ logger.info(`sulcus: history import complete — stored ${stored} memories from OpenClaw workspace`);
1228
+ } catch (_e) { /* best-effort */ }
1229
+ }
1230
+
1011
1231
  // ─── PLUGIN ──────────────────────────────────────────────────────────────────
1012
1232
 
1013
1233
  const sulcusPlugin = {
@@ -1300,6 +1520,13 @@ const sulcusPlugin = {
1300
1520
  logger.warn("sulcus: unknown tool " + toolName + " in config — skipping");
1301
1521
  }
1302
1522
  }
1523
+
1524
+ // Fire-and-forget first-install history import
1525
+ if (isAvailable && sulcusMem instanceof SulcusCloudClient) {
1526
+ importOpenClawHistory(sulcusMem, logger).catch((e: unknown) => {
1527
+ logger.warn(`sulcus: history import failed: ${e instanceof Error ? e.message : String(e)}`);
1528
+ });
1529
+ }
1303
1530
  }
1304
1531
  };
1305
1532
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@digitalforgestudios/openclaw-sulcus",
3
- "version": "4.0.0",
3
+ "version": "4.2.0",
4
4
  "description": "Sulcus — thermodynamic memory + Apache AGE knowledge graph for OpenClaw agents. v4: registerMemoryRuntime, prependContext recall, registerMemoryPromptSection, registerService lifecycle, uiHints, provider-filtered auto-capture. SIU v2 pipeline auto-classifies and scores memories. Interaction-based decay (3 modes). Curator sleep-cycle. Relevance-weighted recall. Cross-agent sync.",
5
5
  "keywords": [
6
6
  "openclaw",