@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.
- package/hooks.defaults.json +10 -1
- package/index.ts +227 -0
- package/package.json +1 -1
package/hooks.defaults.json
CHANGED
|
@@ -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.
|
|
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",
|