@digitalforgestudios/openclaw-sulcus 3.12.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 +837 -516
- package/openclaw.plugin.json +86 -21
- package/package.json +2 -2
package/index.ts
CHANGED
|
@@ -6,11 +6,8 @@ import { URL } from "node:url";
|
|
|
6
6
|
import { Type } from "@sinclair/typebox";
|
|
7
7
|
|
|
8
8
|
// ─── STATIC AWARENESS ───────────────────────────────────────────────────────
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
// It fires even if build_context crashes, times out, or returns empty.
|
|
12
|
-
// Build static awareness with runtime backend info
|
|
13
|
-
function buildStaticAwareness(backendMode: string, namespace: string) {
|
|
9
|
+
|
|
10
|
+
function buildStaticAwareness(backendMode: string, namespace: string): string {
|
|
14
11
|
return `## Persistent Memory (Sulcus)
|
|
15
12
|
You have Sulcus — a persistent, reactive, thermodynamic memory system with reactive triggers.
|
|
16
13
|
Memories survive across sessions. They have heat (0.0–1.0) that decays over time.
|
|
@@ -29,19 +26,15 @@ Memories survive across sessions. They have heat (0.0–1.0) that decays over ti
|
|
|
29
26
|
**Memory types:** episodic (events, fast decay) · semantic (knowledge, slow) · preference (opinions, slower) · procedural (how-tos, slowest) · fact (data, slow)`;
|
|
30
27
|
}
|
|
31
28
|
|
|
32
|
-
// Legacy static string for backward compat (overwritten at register time)
|
|
33
29
|
let STATIC_AWARENESS = buildStaticAwareness("local", "default");
|
|
34
30
|
|
|
35
|
-
// Fallback context when build_context fails — includes the cheatsheet
|
|
36
|
-
// but warns that dynamic context is unavailable.
|
|
37
31
|
const FALLBACK_AWARENESS = `<sulcus_context token_budget="500">
|
|
38
32
|
<cheatsheet>
|
|
39
33
|
You have Sulcus — persistent memory with reactive triggers.
|
|
40
34
|
STORE: memory_store (content, memory_type)
|
|
41
35
|
FIND: memory_recall (query, limit)
|
|
42
36
|
TYPES: episodic (fast fade), semantic (slow), preference, procedural (slowest), fact
|
|
43
|
-
|
|
44
|
-
Below is your active context. Search for deeper recall. Unlimited storage.
|
|
37
|
+
Context build failed this turn — use memory_recall to search manually.
|
|
45
38
|
</cheatsheet>
|
|
46
39
|
</sulcus_context>`;
|
|
47
40
|
|
|
@@ -52,12 +45,12 @@ interface HookConfig {
|
|
|
52
45
|
enabled: boolean;
|
|
53
46
|
limit?: number;
|
|
54
47
|
minScore?: number;
|
|
55
|
-
[key: string]:
|
|
48
|
+
[key: string]: unknown;
|
|
56
49
|
}
|
|
57
50
|
|
|
58
51
|
interface ToolConfig {
|
|
59
52
|
enabled: boolean;
|
|
60
|
-
[key: string]:
|
|
53
|
+
[key: string]: unknown;
|
|
61
54
|
}
|
|
62
55
|
|
|
63
56
|
interface HooksConfig {
|
|
@@ -68,162 +61,255 @@ interface HooksConfig {
|
|
|
68
61
|
}
|
|
69
62
|
|
|
70
63
|
interface HookHandlerCtx {
|
|
71
|
-
sulcusMem:
|
|
64
|
+
sulcusMem: SulcusCloudClient | null;
|
|
72
65
|
backendMode: string;
|
|
73
66
|
namespace: string;
|
|
74
|
-
logger:
|
|
67
|
+
logger: PluginLogger;
|
|
75
68
|
nativeError?: string | null;
|
|
76
69
|
storeLibPath?: string;
|
|
77
70
|
vectorsLibPath?: string;
|
|
78
71
|
wasmDir?: string;
|
|
79
72
|
}
|
|
80
73
|
|
|
81
|
-
|
|
74
|
+
interface PluginLogger {
|
|
75
|
+
debug?: (msg: string) => void;
|
|
76
|
+
info: (msg: string) => void;
|
|
77
|
+
warn: (msg: string) => void;
|
|
78
|
+
error: (msg: string) => void;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
type HookHandler = (event: Record<string, unknown>, config: HookConfig, ctx: HookHandlerCtx) => Promise<unknown>;
|
|
82
82
|
|
|
83
83
|
// ─── HOOK HANDLERS ───────────────────────────────────────────────────────────
|
|
84
84
|
|
|
85
85
|
const hookHandlers: Record<string, HookHandler> = {
|
|
86
|
-
|
|
87
|
-
* inject_awareness — inject static Sulcus awareness into every prompt build.
|
|
88
|
-
* No network call — just a static string describing available tools.
|
|
89
|
-
*/
|
|
90
|
-
inject_awareness: async (_event: any, _config: HookConfig, _ctx: HookHandlerCtx) => {
|
|
86
|
+
inject_awareness: async (_event, _config, _ctx) => {
|
|
91
87
|
return { appendSystemContext: STATIC_AWARENESS };
|
|
92
88
|
},
|
|
93
89
|
|
|
94
|
-
|
|
95
|
-
* auto_recall — search Sulcus memory for context relevant to the incoming prompt.
|
|
96
|
-
* Only runs when enabled. Falls back to FALLBACK_AWARENESS on error.
|
|
97
|
-
*/
|
|
98
|
-
auto_recall: async (event: any, config: HookConfig, ctx: HookHandlerCtx) => {
|
|
90
|
+
auto_recall: async (event, config, ctx) => {
|
|
99
91
|
const { sulcusMem, namespace, logger } = ctx;
|
|
100
92
|
if (!sulcusMem) return;
|
|
101
|
-
const agentLabel = event?.agentId ?? "(unknown)";
|
|
93
|
+
const agentLabel = (event?.agentId as string) ?? "(unknown)";
|
|
102
94
|
logger.info(`sulcus: before_agent_start hook triggered for agent ${agentLabel}`);
|
|
103
95
|
const prompt = typeof event?.prompt === "string" ? event.prompt : "";
|
|
104
96
|
if (!prompt) return;
|
|
105
97
|
try {
|
|
106
|
-
const limit = config.limit ?? 5;
|
|
107
|
-
logger.debug(`sulcus: searching context for prompt: ${prompt.substring(0, 50)}... (namespace: ${namespace})`);
|
|
98
|
+
const limit = (config.limit as number) ?? 5;
|
|
99
|
+
logger.debug?.(`sulcus: searching context for prompt: ${prompt.substring(0, 50)}... (namespace: ${namespace})`);
|
|
108
100
|
const res = await sulcusMem.search_memory(prompt, limit, namespace);
|
|
109
101
|
const results = res?.results ?? [];
|
|
110
102
|
if (!results || results.length === 0) {
|
|
111
103
|
return { prependSystemContext: FALLBACK_AWARENESS };
|
|
112
104
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
105
|
+
const items = results.map((r: Record<string, unknown>) => {
|
|
106
|
+
const heat = ((r.current_heat as number) ?? (r.score as number) ?? 0).toFixed(2);
|
|
107
|
+
const mtype = (r.memory_type as string) ?? "unknown";
|
|
108
|
+
const label = (r.label as string) ?? (r.pointer_summary as string) ?? "";
|
|
109
|
+
return ` <memory id="${r.id}" heat="${heat}" type="${mtype}">${label}</memory>`;
|
|
110
|
+
}).join("\n");
|
|
117
111
|
const context = `<sulcus_context token_budget="500" namespace="${namespace}">\n${items}\n</sulcus_context>`;
|
|
118
112
|
logger.info(`sulcus: injecting ${results.length} recalled memories (${context.length} chars)`);
|
|
119
113
|
return { prependSystemContext: context };
|
|
120
114
|
} catch (e) {
|
|
121
|
-
// build_context failed — inject fallback so the LLM isn't flying blind
|
|
122
115
|
logger.warn(`sulcus: context build failed: ${e} — injecting fallback awareness`);
|
|
123
116
|
return { prependSystemContext: FALLBACK_AWARENESS };
|
|
124
117
|
}
|
|
125
118
|
},
|
|
126
119
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
* (e.g., agent_end where we want to log but not auto-record).
|
|
130
|
-
*/
|
|
131
|
-
none: async (event: any, _config: HookConfig, ctx: HookHandlerCtx) => {
|
|
132
|
-
ctx.logger.debug(`sulcus: hook fired (action=none) for agent ${event.agentId ?? "(unknown)"} (no-op)`);
|
|
120
|
+
none: async (event, _config, ctx) => {
|
|
121
|
+
ctx.logger.debug?.(`sulcus: hook fired (action=none) for agent ${(event.agentId as string) ?? "(unknown)"} (no-op)`);
|
|
133
122
|
},
|
|
134
123
|
|
|
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) => {
|
|
124
|
+
sivu_auto_capture: async (event, config, ctx) => {
|
|
148
125
|
const { sulcusMem, logger } = ctx;
|
|
149
126
|
if (!sulcusMem) return;
|
|
150
127
|
|
|
151
|
-
//
|
|
152
|
-
const
|
|
128
|
+
// Skip captures from system/automated event sources
|
|
129
|
+
const eventTrigger = (event?.trigger as string) ?? "";
|
|
130
|
+
const skippedTriggers = ["exec-event", "cron-event", "heartbeat"];
|
|
131
|
+
if (skippedTriggers.some((t) => eventTrigger === t)) {
|
|
132
|
+
logger.debug?.(`sulcus: sivu_auto_capture — skipping trigger="${eventTrigger}"`);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const userMessage = (event?.userMessage ?? event?.prompt ?? event?.text ?? "") as string;
|
|
153
137
|
if (!userMessage || typeof userMessage !== "string") {
|
|
154
|
-
logger.debug("sulcus: sivu_auto_capture — no user message in event, skipping");
|
|
138
|
+
logger.debug?.("sulcus: sivu_auto_capture — no user message in event, skipping");
|
|
155
139
|
return;
|
|
156
140
|
}
|
|
157
141
|
|
|
158
|
-
// Pre-filter obvious junk before hitting the API
|
|
159
142
|
if (isJunkMemory(userMessage)) {
|
|
160
|
-
logger.debug(`sulcus: sivu_auto_capture — pre-filtered junk: "${userMessage.substring(0, 50)}..."`);
|
|
143
|
+
logger.debug?.(`sulcus: sivu_auto_capture — pre-filtered junk: "${userMessage.substring(0, 50)}..."`);
|
|
161
144
|
return;
|
|
162
145
|
}
|
|
163
146
|
|
|
164
|
-
|
|
165
|
-
|
|
147
|
+
if (!shouldCapture(userMessage)) {
|
|
148
|
+
logger.debug?.("sulcus: sivu_auto_capture — dedup skip");
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const minConfidence = (config.min_store_confidence as number) ?? 0.5;
|
|
153
|
+
const fallbackOnError = config.fallback_on_error !== false;
|
|
166
154
|
|
|
167
|
-
// Try SIU v2 endpoint for quality-gated classification
|
|
168
155
|
if (sulcusMem instanceof SulcusCloudClient) {
|
|
169
156
|
try {
|
|
170
|
-
const siuResult = await
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const
|
|
175
|
-
const memoryType = siuResult?.memory_type ?? "episodic";
|
|
176
|
-
const modelVersion = siuResult?.model_version ?? "unknown";
|
|
157
|
+
const siuResult = await sulcusMem.request("POST", "/api/v2/siu/label", { text: userMessage }) as Record<string, unknown>;
|
|
158
|
+
const storeConf = (siuResult?.store_confidence as number) ?? 0;
|
|
159
|
+
const shouldStore = siuResult?.store === true && storeConf >= minConfidence;
|
|
160
|
+
const memoryType = (siuResult?.memory_type as string) ?? "episodic";
|
|
161
|
+
const modelVersion = (siuResult?.model_version as string) ?? "unknown";
|
|
177
162
|
|
|
178
163
|
if (!shouldStore) {
|
|
179
|
-
logger.info(`sulcus: sivu_auto_capture — SIVU rejected (confidence: ${
|
|
164
|
+
logger.info(`sulcus: sivu_auto_capture — SIVU rejected (confidence: ${storeConf.toFixed(3)}, model: ${modelVersion}): "${userMessage.substring(0, 60)}..."`);
|
|
180
165
|
return;
|
|
181
166
|
}
|
|
182
167
|
|
|
183
|
-
// SIVU approved — store with SICU-predicted type
|
|
184
168
|
const res = await sulcusMem.add_memory(userMessage, memoryType);
|
|
185
|
-
|
|
169
|
+
const typeConf = ((siuResult?.type_confidence as number) ?? 0).toFixed(3);
|
|
170
|
+
logger.info(`sulcus: sivu_auto_capture — stored [${memoryType}] (id: ${res?.id ?? "?"}, sivu_conf: ${storeConf.toFixed(3)}, sicu_conf: ${typeConf}, model: ${modelVersion}): "${userMessage.substring(0, 60)}..."`);
|
|
186
171
|
return;
|
|
187
|
-
} catch (e:
|
|
188
|
-
|
|
172
|
+
} catch (e: unknown) {
|
|
173
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
174
|
+
logger.warn(`sulcus: sivu_auto_capture — SIU v2 endpoint error: ${msg}`);
|
|
189
175
|
if (!fallbackOnError) return;
|
|
190
|
-
// Fall through to basic capture
|
|
191
176
|
}
|
|
192
177
|
}
|
|
193
178
|
|
|
194
|
-
// Fallback: store as episodic (no SIU gating available)
|
|
195
179
|
try {
|
|
196
180
|
const res = await sulcusMem.add_memory(userMessage, "episodic");
|
|
197
181
|
logger.info(`sulcus: sivu_auto_capture — fallback stored [episodic] (id: ${res?.id ?? "?"}): "${userMessage.substring(0, 60)}..."`);
|
|
198
|
-
} catch (e:
|
|
199
|
-
|
|
182
|
+
} catch (e: unknown) {
|
|
183
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
184
|
+
logger.warn(`sulcus: sivu_auto_capture — fallback store failed: ${msg}`);
|
|
185
|
+
}
|
|
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}`);
|
|
200
290
|
}
|
|
201
291
|
},
|
|
202
292
|
};
|
|
203
293
|
|
|
204
294
|
// ─── CLOUD HTTP CLIENT ───────────────────────────────────────────────────────
|
|
205
|
-
// Lightweight fallback client for users without local dylibs/WASM.
|
|
206
|
-
// Uses Node.js built-in https/http — ZERO external dependencies.
|
|
207
|
-
// Activates only when serverUrl + apiKey are configured and local libs are absent.
|
|
208
295
|
|
|
209
296
|
class SulcusCloudClient {
|
|
210
297
|
private serverUrl: string;
|
|
211
298
|
private apiKey: string;
|
|
212
299
|
|
|
213
300
|
constructor(serverUrl: string, apiKey: string) {
|
|
214
|
-
// Strip trailing slash for clean path concatenation
|
|
215
301
|
this.serverUrl = serverUrl.replace(/\/+$/, "");
|
|
216
302
|
this.apiKey = apiKey;
|
|
217
303
|
}
|
|
218
304
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
return new Promise((resolve, reject) => {
|
|
305
|
+
request(method: string, path: string, body?: unknown): Promise<unknown> {
|
|
306
|
+
return new Promise((resolveP, rejectP) => {
|
|
222
307
|
let parsedUrl: URL;
|
|
223
308
|
try {
|
|
224
309
|
parsedUrl = new URL(this.serverUrl + path);
|
|
225
|
-
} catch (e:
|
|
226
|
-
|
|
310
|
+
} catch (e: unknown) {
|
|
311
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
312
|
+
return rejectP(new Error(`SulcusCloudClient: invalid URL ${this.serverUrl}${path}: ${msg}`));
|
|
227
313
|
}
|
|
228
314
|
|
|
229
315
|
const isHttps = parsedUrl.protocol === "https:";
|
|
@@ -253,129 +339,96 @@ class SulcusCloudClient {
|
|
|
253
339
|
res.on("end", () => {
|
|
254
340
|
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
255
341
|
if (!res.statusCode || res.statusCode >= 400) {
|
|
256
|
-
return
|
|
257
|
-
}
|
|
258
|
-
if (!raw || raw.trim() === "") {
|
|
259
|
-
return resolve(null);
|
|
342
|
+
return rejectP(new Error(`SulcusCloudClient: HTTP ${res.statusCode} for ${method} ${path}: ${raw.substring(0, 200)}`));
|
|
260
343
|
}
|
|
344
|
+
if (!raw || raw.trim() === "") return resolveP(null);
|
|
261
345
|
try {
|
|
262
|
-
|
|
346
|
+
resolveP(JSON.parse(raw));
|
|
263
347
|
} catch (_e) {
|
|
264
|
-
|
|
265
|
-
resolve(raw);
|
|
348
|
+
resolveP(raw);
|
|
266
349
|
}
|
|
267
350
|
});
|
|
268
351
|
});
|
|
269
352
|
|
|
270
|
-
req.on("error", (e: Error) =>
|
|
271
|
-
|
|
272
|
-
if (bodyStr !== undefined) {
|
|
273
|
-
req.write(bodyStr);
|
|
274
|
-
}
|
|
353
|
+
req.on("error", (e: Error) => rejectP(new Error(`SulcusCloudClient: network error for ${method} ${path}: ${e.message}`)));
|
|
354
|
+
if (bodyStr !== undefined) req.write(bodyStr);
|
|
275
355
|
req.end();
|
|
276
356
|
});
|
|
277
357
|
}
|
|
278
358
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
* Server returns { results: [...] }; we normalise to the results array.
|
|
282
|
-
*/
|
|
283
|
-
async search_memory(query: string, limit?: number, namespace?: string): Promise<{ results: any[] }> {
|
|
284
|
-
const body: any = { query };
|
|
359
|
+
async search_memory(query: string, limit?: number, namespace?: string): Promise<{ results: Record<string, unknown>[] }> {
|
|
360
|
+
const body: Record<string, unknown> = { query };
|
|
285
361
|
if (limit !== undefined) body.limit = limit;
|
|
286
362
|
if (namespace !== undefined) body.namespace = namespace;
|
|
287
|
-
const res = await this.request("POST", "/api/v1/agent/search", body);
|
|
288
|
-
const results = res?.results ?? res?.items ?? res?.nodes ?? (Array.isArray(res) ? res : []);
|
|
363
|
+
const res = await this.request("POST", "/api/v1/agent/search", body) as Record<string, unknown> | null;
|
|
364
|
+
const results = (res?.results ?? res?.items ?? res?.nodes ?? (Array.isArray(res) ? res : [])) as Record<string, unknown>[];
|
|
289
365
|
return { results };
|
|
290
366
|
}
|
|
291
367
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
* Server returns { id, ... }; pass through.
|
|
295
|
-
*/
|
|
296
|
-
async add_memory(content: string, memoryType?: string | null): Promise<{ id: string; [key: string]: any }> {
|
|
297
|
-
const body: any = { label: content };
|
|
368
|
+
async add_memory(content: string, memoryType?: string | null): Promise<{ id: string; [key: string]: unknown }> {
|
|
369
|
+
const body: Record<string, unknown> = { label: content };
|
|
298
370
|
if (memoryType) body.memory_type = memoryType;
|
|
299
|
-
const res = await this.request("POST", "/api/v1/agent/nodes", body);
|
|
300
|
-
return res ?? { id: "unknown" };
|
|
371
|
+
const res = await this.request("POST", "/api/v1/agent/nodes", body) as Record<string, unknown> | null;
|
|
372
|
+
return (res ?? { id: "unknown" }) as { id: string; [key: string]: unknown };
|
|
301
373
|
}
|
|
302
374
|
|
|
303
|
-
|
|
304
|
-
* list_hot_nodes — maps to GET /agent/hot_nodes
|
|
305
|
-
* Returns hot_nodes list; normalised for memory_status tool.
|
|
306
|
-
* Note: was incorrectly calling /agent/memory/status which doesn't return hot_nodes.
|
|
307
|
-
*/
|
|
308
|
-
async list_hot_nodes(limit?: number): Promise<{ nodes: any[] }> {
|
|
375
|
+
async list_hot_nodes(limit?: number): Promise<{ nodes: Record<string, unknown>[] }> {
|
|
309
376
|
const q = limit ? `?limit=${limit}` : "";
|
|
310
|
-
const res = await this.request("GET", `/api/v1/agent/hot_nodes${q}`);
|
|
311
|
-
|
|
312
|
-
const nodes = Array.isArray(res) ? res : (res?.hot_nodes ?? res?.nodes ?? []);
|
|
377
|
+
const res = await this.request("GET", `/api/v1/agent/hot_nodes${q}`) as Record<string, unknown> | unknown[] | null;
|
|
378
|
+
const nodes = (Array.isArray(res) ? res : ((res as Record<string, unknown>)?.hot_nodes ?? (res as Record<string, unknown>)?.nodes ?? [])) as Record<string, unknown>[];
|
|
313
379
|
return { nodes };
|
|
314
380
|
}
|
|
315
381
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
*/
|
|
319
|
-
async consolidate(minHeat?: number): Promise<any> {
|
|
320
|
-
const body: any = {};
|
|
382
|
+
async consolidate(minHeat?: number): Promise<unknown> {
|
|
383
|
+
const body: Record<string, unknown> = {};
|
|
321
384
|
if (minHeat !== undefined) body.min_heat = minHeat;
|
|
322
385
|
return this.request("POST", "/api/v1/agent/consolidate", body);
|
|
323
386
|
}
|
|
324
387
|
|
|
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> {
|
|
388
|
+
async delete_memory(id: string, train?: boolean): Promise<unknown> {
|
|
330
389
|
const trainParam = train ? "true" : "false";
|
|
331
390
|
return this.request("DELETE", `/api/v1/agent/nodes/${encodeURIComponent(id)}?train=${trainParam}`);
|
|
332
391
|
}
|
|
333
392
|
|
|
334
|
-
/**
|
|
335
|
-
* export_markdown — maps to GET /agent/export?format=markdown
|
|
336
|
-
* Returns raw markdown string.
|
|
337
|
-
*/
|
|
338
393
|
async export_markdown(): Promise<string> {
|
|
339
394
|
const res = await this.request("GET", "/api/v1/agent/export?format=markdown");
|
|
340
|
-
// Server may return { content: "..." } or raw string
|
|
341
395
|
if (typeof res === "string") return res;
|
|
342
|
-
|
|
396
|
+
const r = res as Record<string, unknown>;
|
|
397
|
+
return (r?.content ?? r?.markdown ?? JSON.stringify(res, null, 2)) as string;
|
|
343
398
|
}
|
|
344
399
|
|
|
345
|
-
|
|
346
|
-
* import_markdown — maps to POST /agent/import
|
|
347
|
-
*/
|
|
348
|
-
async import_markdown(text: string): Promise<any> {
|
|
400
|
+
async import_markdown(text: string): Promise<unknown> {
|
|
349
401
|
return this.request("POST", "/api/v1/agent/import", { format: "markdown", content: text });
|
|
350
402
|
}
|
|
351
403
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
*/
|
|
355
|
-
async evaluate_triggers(event: any, contextJson?: string): Promise<any> {
|
|
356
|
-
const body: any = { event };
|
|
404
|
+
async evaluate_triggers(event: unknown, contextJson?: string): Promise<unknown> {
|
|
405
|
+
const body: Record<string, unknown> = { event };
|
|
357
406
|
if (contextJson) {
|
|
358
|
-
try {
|
|
359
|
-
|
|
360
|
-
} catch (_e) {
|
|
361
|
-
body.context = contextJson;
|
|
362
|
-
}
|
|
407
|
+
try { body.context = JSON.parse(contextJson); }
|
|
408
|
+
catch (_e) { body.context = contextJson; }
|
|
363
409
|
}
|
|
364
410
|
return this.request("POST", "/api/v1/triggers/evaluate", body);
|
|
365
411
|
}
|
|
412
|
+
|
|
413
|
+
async probe(): Promise<boolean> {
|
|
414
|
+
try {
|
|
415
|
+
await this.search_memory("probe", 1);
|
|
416
|
+
return true;
|
|
417
|
+
} catch {
|
|
418
|
+
return false;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
366
421
|
}
|
|
367
422
|
|
|
368
423
|
// ─── NATIVE LIB LOADER ──────────────────────────────────────────────────────
|
|
369
|
-
// Loads libsulcus_store.dylib (embedded PG) and libsulcus_vectors.dylib (embeddings)
|
|
370
|
-
// via koffi FFI. Provides queryFn and embedFn callbacks for SulcusMem.create().
|
|
371
424
|
|
|
372
425
|
class NativeLibLoader {
|
|
373
|
-
|
|
374
|
-
private
|
|
375
|
-
private
|
|
376
|
-
private
|
|
377
|
-
|
|
378
|
-
//
|
|
426
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
427
|
+
private koffi: unknown = null;
|
|
428
|
+
private storeLib: unknown = null;
|
|
429
|
+
private vectorsLib: unknown = null;
|
|
430
|
+
private vectorsHandle: unknown = null;
|
|
431
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
379
432
|
private fn_store_init: any = null;
|
|
380
433
|
private fn_store_query: any = null;
|
|
381
434
|
private fn_store_free: any = null;
|
|
@@ -386,22 +439,18 @@ class NativeLibLoader {
|
|
|
386
439
|
public loaded = false;
|
|
387
440
|
public error: string | null = null;
|
|
388
441
|
|
|
389
|
-
constructor(
|
|
390
|
-
private storeLibPath: string,
|
|
391
|
-
private vectorsLibPath: string
|
|
392
|
-
) {}
|
|
442
|
+
constructor(private storeLibPath: string, private vectorsLibPath: string) {}
|
|
393
443
|
|
|
394
|
-
init(logger:
|
|
444
|
+
init(logger: PluginLogger): void {
|
|
395
445
|
try {
|
|
396
|
-
//
|
|
446
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
397
447
|
this.koffi = require("koffi");
|
|
398
|
-
} catch (e:
|
|
399
|
-
this.error = `koffi not available: ${e.message}`;
|
|
448
|
+
} catch (e: unknown) {
|
|
449
|
+
this.error = `koffi not available: ${e instanceof Error ? e.message : e}`;
|
|
400
450
|
logger.warn(`sulcus: ${this.error}`);
|
|
401
451
|
return;
|
|
402
452
|
}
|
|
403
453
|
|
|
404
|
-
// ── Load libsulcus_store.dylib ──
|
|
405
454
|
if (!existsSync(this.storeLibPath)) {
|
|
406
455
|
this.error = `libsulcus_store not found at ${this.storeLibPath}`;
|
|
407
456
|
logger.warn(`sulcus: ${this.error}`);
|
|
@@ -414,48 +463,47 @@ class NativeLibLoader {
|
|
|
414
463
|
}
|
|
415
464
|
|
|
416
465
|
try {
|
|
417
|
-
|
|
418
|
-
this.
|
|
419
|
-
this.
|
|
420
|
-
this.
|
|
421
|
-
|
|
422
|
-
|
|
466
|
+
const k = this.koffi as any;
|
|
467
|
+
this.storeLib = k.load(this.storeLibPath);
|
|
468
|
+
this.fn_store_init = (this.storeLib as any).func("sulcus_store_init", "int", ["str", "uint16"]);
|
|
469
|
+
this.fn_store_query = (this.storeLib as any).func("sulcus_store_query", "char*", ["str"]);
|
|
470
|
+
this.fn_store_free = (this.storeLib as any).func("sulcus_store_free_string", "void", ["char*"]);
|
|
471
|
+
} catch (e: unknown) {
|
|
472
|
+
this.error = `Failed to load libsulcus_store: ${e instanceof Error ? e.message : e}`;
|
|
423
473
|
logger.warn(`sulcus: ${this.error}`);
|
|
424
474
|
return;
|
|
425
475
|
}
|
|
426
476
|
|
|
427
477
|
try {
|
|
428
|
-
|
|
429
|
-
this.
|
|
430
|
-
this.
|
|
431
|
-
this.
|
|
432
|
-
|
|
433
|
-
|
|
478
|
+
const k = this.koffi as any;
|
|
479
|
+
this.vectorsLib = k.load(this.vectorsLibPath);
|
|
480
|
+
this.fn_vectors_create = (this.vectorsLib as any).func("sulcus_vectors_create", "void*", []);
|
|
481
|
+
this.fn_vectors_text = (this.vectorsLib as any).func("sulcus_vectors_text", "char*", ["void*", "str"]);
|
|
482
|
+
this.fn_vectors_free = (this.vectorsLib as any).func("sulcus_vectors_free_string", "void", ["char*"]);
|
|
483
|
+
} catch (e: unknown) {
|
|
484
|
+
this.error = `Failed to load libsulcus_vectors: ${e instanceof Error ? e.message : e}`;
|
|
434
485
|
logger.warn(`sulcus: ${this.error}`);
|
|
435
486
|
return;
|
|
436
487
|
}
|
|
437
488
|
|
|
438
|
-
// ── Initialise embedded PG store ──
|
|
439
489
|
try {
|
|
440
490
|
const dataDir = resolve(process.env.HOME || "~", ".sulcus/data");
|
|
441
|
-
const
|
|
442
|
-
const rc = this.fn_store_init(dataDir, port);
|
|
491
|
+
const rc = this.fn_store_init(dataDir, 15432);
|
|
443
492
|
if (rc !== 0) {
|
|
444
493
|
this.error = `sulcus_store_init returned ${rc}`;
|
|
445
494
|
logger.warn(`sulcus: ${this.error}`);
|
|
446
495
|
return;
|
|
447
496
|
}
|
|
448
|
-
} catch (e:
|
|
449
|
-
this.error = `sulcus_store_init failed: ${e.message}`;
|
|
497
|
+
} catch (e: unknown) {
|
|
498
|
+
this.error = `sulcus_store_init failed: ${e instanceof Error ? e.message : e}`;
|
|
450
499
|
logger.warn(`sulcus: ${this.error}`);
|
|
451
500
|
return;
|
|
452
501
|
}
|
|
453
502
|
|
|
454
|
-
// ── Create embed handle ──
|
|
455
503
|
try {
|
|
456
504
|
this.vectorsHandle = this.fn_vectors_create();
|
|
457
|
-
} catch (e:
|
|
458
|
-
this.error = `sulcus_vectors_create failed: ${e.message}`;
|
|
505
|
+
} catch (e: unknown) {
|
|
506
|
+
this.error = `sulcus_vectors_create failed: ${e instanceof Error ? e.message : e}`;
|
|
459
507
|
logger.warn(`sulcus: ${this.error}`);
|
|
460
508
|
return;
|
|
461
509
|
}
|
|
@@ -464,29 +512,17 @@ class NativeLibLoader {
|
|
|
464
512
|
logger.info(`sulcus: native libs loaded (store: ${this.storeLibPath}, vectors: ${this.vectorsLibPath})`);
|
|
465
513
|
}
|
|
466
514
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
// passed as a JSON payload — the store lib handles parameterisation internally).
|
|
470
|
-
makeQueryFn(): (sql: string, params: any[]) => Promise<any[]> {
|
|
471
|
-
return async (sql: string, _params: any[]): Promise<any[]> => {
|
|
515
|
+
makeQueryFn(): (sql: string, params: unknown[]) => Promise<unknown[]> {
|
|
516
|
+
return async (sql: string, params: unknown[]): Promise<unknown[]> => {
|
|
472
517
|
if (!this.loaded) throw new Error("Sulcus store not available");
|
|
473
|
-
|
|
474
|
-
const payload = JSON.stringify({ sql, params: _params });
|
|
475
|
-
const raw: string = this.fn_store_query(payload);
|
|
518
|
+
const raw: string = this.fn_store_query(JSON.stringify({ sql, params }));
|
|
476
519
|
if (!raw) return [];
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
} finally {
|
|
481
|
-
// NOTE: koffi manages string memory automatically for char* returns;
|
|
482
|
-
// no manual free needed with koffi's default charset handling.
|
|
483
|
-
// If the ABI requires explicit free, call fn_store_free here.
|
|
484
|
-
}
|
|
520
|
+
const parsed = JSON.parse(raw);
|
|
521
|
+
const p = parsed as Record<string, unknown>;
|
|
522
|
+
return Array.isArray(parsed) ? (parsed as unknown[]) : ((Array.isArray(p?.rows) ? p.rows as unknown[] : [parsed as unknown]));
|
|
485
523
|
};
|
|
486
524
|
}
|
|
487
525
|
|
|
488
|
-
// embedFn: async (text: string) => Float32Array
|
|
489
|
-
// Calls sulcus_vectors_text which returns a JSON float array string.
|
|
490
526
|
makeEmbedFn(): (text: string) => Promise<Float32Array> {
|
|
491
527
|
return async (text: string): Promise<Float32Array> => {
|
|
492
528
|
if (!this.loaded) throw new Error("Sulcus vectors not available");
|
|
@@ -499,9 +535,8 @@ class NativeLibLoader {
|
|
|
499
535
|
}
|
|
500
536
|
|
|
501
537
|
// ─── PRE-SEND FILTER ─────────────────────────────────────────────────────────
|
|
502
|
-
// Rule-based junk filter. Catches obvious noise before it hits the API.
|
|
503
538
|
|
|
504
|
-
const JUNK_PATTERNS = [
|
|
539
|
+
const JUNK_PATTERNS: RegExp[] = [
|
|
505
540
|
/^(HEARTBEAT_OK|NO_REPLY|NOOP)$/i,
|
|
506
541
|
/^\s*$/,
|
|
507
542
|
/^system:\s/i,
|
|
@@ -514,18 +549,15 @@ const JUNK_PATTERNS = [
|
|
|
514
549
|
/^<<<EXTERNAL_UNTRUSTED_CONTENT/i,
|
|
515
550
|
/^Runtime:/i,
|
|
516
551
|
/tool_call|function_call|<function_calls>/i,
|
|
517
|
-
// Subagent completion events — internal runtime artifacts, not memories
|
|
518
552
|
/\[Inter-session message\]\s*sourceSession=/i,
|
|
519
553
|
/<<<BEGIN_UNTRUSTED_CHILD_RESULT>>>/,
|
|
520
554
|
/<<<END_UNTRUSTED_CHILD_RESULT>>>/,
|
|
521
555
|
/\[Internal task completion event\]/i,
|
|
522
556
|
/^source:\s*subagent/im,
|
|
523
557
|
/session_key:\s*agent:main:subagent:/i,
|
|
524
|
-
// Cron task payloads — system prompts, not meaningful content
|
|
525
558
|
/^Sulcus validation cycle\./i,
|
|
526
559
|
/^Heartbeat prompt:/i,
|
|
527
560
|
/OpenClaw runtime context \(internal\)/i,
|
|
528
|
-
// Credential patterns — should never be stored
|
|
529
561
|
/\b(sk-[a-f0-9]{40,}|Bearer\s+[A-Za-z0-9._~+/=-]{20,})\b/,
|
|
530
562
|
/\b(api[_-]?key|secret|password|token)\s*[:=]\s*["']?[A-Za-z0-9._~+/=-]{16,}/i,
|
|
531
563
|
];
|
|
@@ -539,27 +571,39 @@ function isJunkMemory(text: string): boolean {
|
|
|
539
571
|
return false;
|
|
540
572
|
}
|
|
541
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
|
+
|
|
542
590
|
// ─── HOOKS CONFIG LOADER ─────────────────────────────────────────────────────
|
|
543
591
|
|
|
544
|
-
|
|
545
|
-
* Load and merge hooks config.
|
|
546
|
-
* Precedence: user config (api.config.hooks/tools) > defaults from hooks.defaults.json
|
|
547
|
-
* Legacy `autoRecall` flag maps to hooks.before_agent_start.enabled for backward compat.
|
|
548
|
-
*/
|
|
549
|
-
function loadHooksConfig(apiConfig: any): HooksConfig {
|
|
550
|
-
// Load defaults
|
|
592
|
+
function loadHooksConfig(apiConfig: Record<string, unknown>): HooksConfig {
|
|
551
593
|
const defaultsPath = resolve(__dirname, "hooks.defaults.json");
|
|
552
594
|
let defaults: HooksConfig;
|
|
553
595
|
try {
|
|
596
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
554
597
|
defaults = JSON.parse(require("fs").readFileSync(defaultsPath, "utf-8")) as HooksConfig;
|
|
555
598
|
} catch (_e) {
|
|
556
|
-
// Fallback inline defaults if file is missing (safety net)
|
|
557
599
|
defaults = {
|
|
558
600
|
version: 1,
|
|
559
601
|
hooks: {
|
|
560
602
|
before_prompt_build: { action: "inject_awareness", enabled: true },
|
|
561
603
|
before_agent_start: { action: "auto_recall", enabled: false, limit: 5, minScore: 0.3 },
|
|
562
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 },
|
|
563
607
|
},
|
|
564
608
|
tools: {
|
|
565
609
|
memory_recall: { enabled: true },
|
|
@@ -569,13 +613,13 @@ function loadHooksConfig(apiConfig: any): HooksConfig {
|
|
|
569
613
|
export_markdown: { enabled: false },
|
|
570
614
|
import_markdown: { enabled: false },
|
|
571
615
|
evaluate_triggers: { enabled: false },
|
|
616
|
+
__sulcus_workflow__: { enabled: true },
|
|
572
617
|
},
|
|
573
618
|
};
|
|
574
619
|
}
|
|
575
620
|
|
|
576
|
-
|
|
577
|
-
const
|
|
578
|
-
const userTools: Record<string, Partial<ToolConfig>> = apiConfig?.tools ?? {};
|
|
621
|
+
const userHooks = (apiConfig?.hooks ?? {}) as Record<string, Partial<HookConfig>>;
|
|
622
|
+
const userTools = (apiConfig?.tools ?? {}) as Record<string, Partial<ToolConfig>>;
|
|
579
623
|
|
|
580
624
|
const mergedHooks: Record<string, HookConfig> = { ...defaults.hooks };
|
|
581
625
|
for (const [name, override] of Object.entries(userHooks)) {
|
|
@@ -587,7 +631,7 @@ function loadHooksConfig(apiConfig: any): HooksConfig {
|
|
|
587
631
|
mergedTools[name] = { ...(mergedTools[name] ?? { enabled: false }), ...override };
|
|
588
632
|
}
|
|
589
633
|
|
|
590
|
-
//
|
|
634
|
+
// Legacy compat: autoRecall flag → hooks.before_agent_start.enabled
|
|
591
635
|
if (apiConfig?.autoRecall === true) {
|
|
592
636
|
mergedHooks["before_agent_start"] = {
|
|
593
637
|
...(mergedHooks["before_agent_start"] ?? { action: "auto_recall", enabled: false }),
|
|
@@ -598,26 +642,206 @@ function loadHooksConfig(apiConfig: any): HooksConfig {
|
|
|
598
642
|
return { version: defaults.version, hooks: mergedHooks, tools: mergedTools };
|
|
599
643
|
}
|
|
600
644
|
|
|
601
|
-
// ───
|
|
645
|
+
// ─── RELATIVE TIME FORMATTER ─────────────────────────────────────────────────
|
|
602
646
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
647
|
+
function formatRelativeTime(isoTimestamp: string): string {
|
|
648
|
+
try {
|
|
649
|
+
const dt = new Date(isoTimestamp);
|
|
650
|
+
const now = new Date();
|
|
651
|
+
const seconds = (now.getTime() - dt.getTime()) / 1000;
|
|
652
|
+
const minutes = seconds / 60;
|
|
653
|
+
const hours = seconds / 3600;
|
|
654
|
+
const days = seconds / 86400;
|
|
655
|
+
if (minutes < 2) return "just now";
|
|
656
|
+
if (minutes < 60) return `${Math.floor(minutes)}m ago`;
|
|
657
|
+
if (hours < 24) return `${Math.floor(hours)}h ago`;
|
|
658
|
+
if (days < 7) return `${Math.floor(days)}d ago`;
|
|
659
|
+
const month = dt.toLocaleString("en", { month: "short" });
|
|
660
|
+
if (dt.getFullYear() === now.getFullYear()) return `${dt.getDate()} ${month}`;
|
|
661
|
+
return `${dt.getDate()} ${month}, ${dt.getFullYear()}`;
|
|
662
|
+
} catch {
|
|
663
|
+
return "";
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// ─── SDK RECALL HANDLER (for before_agent_start with prependContext) ──────────
|
|
668
|
+
|
|
669
|
+
interface ProfileCache {
|
|
670
|
+
preferences: Record<string, unknown>[];
|
|
671
|
+
facts: Record<string, unknown>[];
|
|
672
|
+
cachedAt: number;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function buildSdkRecallHandler(
|
|
676
|
+
sulcusMem: SulcusCloudClient,
|
|
677
|
+
namespace: string,
|
|
678
|
+
maxResults: number,
|
|
679
|
+
profileFrequency: number,
|
|
680
|
+
logger: PluginLogger
|
|
681
|
+
) {
|
|
682
|
+
let turnCount = 0;
|
|
683
|
+
let profileCache: ProfileCache | null = null;
|
|
684
|
+
|
|
685
|
+
return async (event: Record<string, unknown>, _ctx: unknown): Promise<{ prependContext: string } | undefined> => {
|
|
686
|
+
const prompt = typeof event?.prompt === "string" ? event.prompt : "";
|
|
687
|
+
if (!prompt || prompt.length < 5) return undefined;
|
|
688
|
+
|
|
689
|
+
turnCount++;
|
|
690
|
+
const includeProfile = turnCount === 1 || turnCount % profileFrequency === 0;
|
|
691
|
+
|
|
692
|
+
try {
|
|
693
|
+
const searchRes = await sulcusMem.search_memory(prompt, maxResults, namespace);
|
|
694
|
+
const searchResults = searchRes?.results ?? [];
|
|
695
|
+
|
|
696
|
+
let preferences: Record<string, unknown>[] = [];
|
|
697
|
+
let facts: Record<string, unknown>[] = [];
|
|
698
|
+
|
|
699
|
+
if (includeProfile) {
|
|
700
|
+
try {
|
|
701
|
+
const prefRes = await sulcusMem.search_memory("user preference", Math.min(maxResults, 5), namespace);
|
|
702
|
+
const factRes = await sulcusMem.search_memory("fact data knowledge", Math.min(maxResults, 5), namespace);
|
|
703
|
+
preferences = (prefRes?.results ?? []).filter((r) => r.memory_type === "preference");
|
|
704
|
+
facts = (factRes?.results ?? []).filter((r) => r.memory_type === "fact");
|
|
705
|
+
profileCache = { preferences, facts, cachedAt: Date.now() };
|
|
706
|
+
} catch {
|
|
707
|
+
// profile fetch failed — continue without
|
|
708
|
+
}
|
|
709
|
+
} else if (profileCache) {
|
|
710
|
+
preferences = profileCache.preferences;
|
|
711
|
+
facts = profileCache.facts;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const profileIds = new Set([
|
|
715
|
+
...preferences.map((r) => r.id as string),
|
|
716
|
+
...facts.map((r) => r.id as string),
|
|
717
|
+
]);
|
|
718
|
+
const dedupedSearch = searchResults.filter((r) => !profileIds.has(r.id as string));
|
|
719
|
+
|
|
720
|
+
const sections: string[] = [];
|
|
721
|
+
|
|
722
|
+
if (includeProfile && (preferences.length > 0 || facts.length > 0)) {
|
|
723
|
+
const profileLines: string[] = [];
|
|
724
|
+
for (const r of preferences) {
|
|
725
|
+
const label = (r.label ?? r.pointer_summary ?? r.id ?? "") as string;
|
|
726
|
+
profileLines.push(`- [preference] ${label}`);
|
|
727
|
+
}
|
|
728
|
+
for (const r of facts) {
|
|
729
|
+
const label = (r.label ?? r.pointer_summary ?? r.id ?? "") as string;
|
|
730
|
+
profileLines.push(`- [fact] ${label}`);
|
|
731
|
+
}
|
|
732
|
+
if (profileLines.length > 0) {
|
|
733
|
+
sections.push(`## User Profile (from preferences + facts)\n${profileLines.join("\n")}`);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
if (dedupedSearch.length > 0) {
|
|
738
|
+
const memLines = dedupedSearch.slice(0, maxResults).map((r) => {
|
|
739
|
+
const heat = ((r.current_heat as number) ?? (r.score as number) ?? 0);
|
|
740
|
+
const pct = `[${Math.round(heat * 100)}%]`;
|
|
741
|
+
const updatedAt = r.updated_at as string | undefined;
|
|
742
|
+
const timeStr = updatedAt ? `[${formatRelativeTime(updatedAt)}]` : "";
|
|
743
|
+
const label = (r.label ?? r.pointer_summary ?? r.id ?? "") as string;
|
|
744
|
+
return `- ${pct} ${timeStr} ${label}`.trim();
|
|
745
|
+
});
|
|
746
|
+
sections.push(`## Relevant Memories (with relevance %)\n${memLines.join("\n")}`);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
if (sections.length === 0) return undefined;
|
|
750
|
+
|
|
751
|
+
const intro =
|
|
752
|
+
"The following is background context from long-term memory. Use it silently to inform your understanding — only reference it when the conversation naturally calls for it.";
|
|
753
|
+
const context = `<sulcus_context token_budget="500" namespace="${namespace}">\n${intro}\n\n${sections.join("\n\n")}\n</sulcus_context>`;
|
|
754
|
+
|
|
755
|
+
logger.info(`sulcus: SDK recall injecting context (${context.length} chars, turn ${turnCount})`);
|
|
756
|
+
return { prependContext: context };
|
|
757
|
+
} catch (err) {
|
|
758
|
+
logger.warn(`sulcus: SDK recall failed: ${err}`);
|
|
759
|
+
return undefined;
|
|
760
|
+
}
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// ─── MEMORY RUNTIME BUILDER ───────────────────────────────────────────────────
|
|
765
|
+
|
|
766
|
+
function buildMemoryRuntime(sulcusMem: SulcusCloudClient, backendMode: string) {
|
|
767
|
+
const searchManager = {
|
|
768
|
+
status() {
|
|
769
|
+
return {
|
|
770
|
+
backend: "builtin" as const,
|
|
771
|
+
provider: "sulcus",
|
|
772
|
+
model: backendMode === "cloud" ? "sulcus-cloud" : "sulcus-local",
|
|
773
|
+
custom: { backendMode, transport: backendMode === "cloud" ? "remote" : "local" },
|
|
774
|
+
};
|
|
775
|
+
},
|
|
776
|
+
async probeEmbeddingAvailability() {
|
|
777
|
+
try {
|
|
778
|
+
const ok = await sulcusMem.probe();
|
|
779
|
+
return { ok };
|
|
780
|
+
} catch (err) {
|
|
781
|
+
return { ok: false, error: err instanceof Error ? err.message : "sulcus unreachable" };
|
|
782
|
+
}
|
|
783
|
+
},
|
|
784
|
+
async probeVectorAvailability() { return true; },
|
|
785
|
+
async sync() { /* cloud sync is continuous */ },
|
|
786
|
+
async close() { /* no-op for HTTP client */ },
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
return {
|
|
790
|
+
async getMemorySearchManager() { return { manager: searchManager }; },
|
|
791
|
+
resolveMemoryBackendConfig() { return { backend: "builtin" as const }; },
|
|
792
|
+
async closeAllMemorySearchManagers() { /* no-op */ },
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// ─── PROMPT SECTION BUILDER ───────────────────────────────────────────────────
|
|
797
|
+
|
|
798
|
+
function buildPromptSection(params: { availableTools: Set<string> }): string[] {
|
|
799
|
+
const hasRecall = params.availableTools.has("memory_recall");
|
|
800
|
+
const hasStore = params.availableTools.has("memory_store");
|
|
801
|
+
if (!hasRecall && !hasStore) return [];
|
|
802
|
+
|
|
803
|
+
const lines: string[] = [
|
|
804
|
+
"## Memory (Sulcus)",
|
|
805
|
+
"",
|
|
806
|
+
"You have persistent thermodynamic memory powered by Sulcus.",
|
|
807
|
+
"Relevant memories are automatically injected at the start of each conversation.",
|
|
808
|
+
"",
|
|
809
|
+
];
|
|
810
|
+
|
|
811
|
+
if (hasRecall) lines.push("- Use `memory_recall` to search prior conversations, preferences, and facts.");
|
|
812
|
+
if (hasStore) lines.push("- Use `memory_store` to save information the user asks you to remember.");
|
|
813
|
+
if (params.availableTools.has("memory_delete")) lines.push("- Use `memory_delete` to remove incorrect or stale memories.");
|
|
814
|
+
if (params.availableTools.has("memory_status")) lines.push("- Use `memory_status` to check backend connection and hot nodes.");
|
|
815
|
+
if (params.availableTools.has("consolidate")) lines.push("- Use `consolidate` to prune cold memories below a heat threshold.");
|
|
816
|
+
if (params.availableTools.has("export_markdown")) lines.push("- Use `export_markdown` to export all memories as Markdown.");
|
|
817
|
+
if (params.availableTools.has("import_markdown")) lines.push("- Use `import_markdown` to import memories from a Markdown document.");
|
|
818
|
+
if (params.availableTools.has("evaluate_triggers")) lines.push("- Use `evaluate_triggers` to evaluate reactive memory triggers.");
|
|
819
|
+
|
|
820
|
+
lines.push("");
|
|
821
|
+
lines.push("Memory types: episodic (events, fast decay), semantic (knowledge, slow), preference (opinions, slower), procedural (how-tos, slowest), fact (data, slow)");
|
|
822
|
+
|
|
823
|
+
return lines;
|
|
607
824
|
}
|
|
608
825
|
|
|
826
|
+
// ─── TOOL DEFINITIONS ────────────────────────────────────────────────────────
|
|
827
|
+
|
|
609
828
|
interface ToolDeps {
|
|
610
|
-
sulcusMem:
|
|
829
|
+
sulcusMem: SulcusCloudClient | null;
|
|
611
830
|
backendMode: string;
|
|
612
831
|
namespace: string;
|
|
613
832
|
nativeLoader: NativeLibLoader;
|
|
614
833
|
storeLibPath: string;
|
|
615
834
|
vectorsLibPath: string;
|
|
616
835
|
wasmDir: string;
|
|
617
|
-
logger:
|
|
836
|
+
logger: PluginLogger;
|
|
618
837
|
isAvailable: boolean;
|
|
619
|
-
|
|
620
|
-
|
|
838
|
+
siuRequest: ((method: string, path: string, body?: unknown) => Promise<unknown>) | null;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
interface ToolDefinition {
|
|
842
|
+
schema: Record<string, unknown>;
|
|
843
|
+
options: { name: string };
|
|
844
|
+
makeExecute: (deps: ToolDeps) => (id: string, params: Record<string, unknown>) => Promise<{ content: { type: string; text: string }[]; details?: Record<string, unknown> }>;
|
|
621
845
|
}
|
|
622
846
|
|
|
623
847
|
const toolDefinitions: Record<string, ToolDefinition> = {
|
|
@@ -629,23 +853,19 @@ const toolDefinitions: Record<string, ToolDefinition> = {
|
|
|
629
853
|
parameters: Type.Object({
|
|
630
854
|
query: Type.String({ description: "Search query string." }),
|
|
631
855
|
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.
|
|
856
|
+
namespace: Type.Optional(Type.String({ description: "Namespace to search. Defaults to your own namespace." })),
|
|
633
857
|
}),
|
|
634
858
|
},
|
|
635
859
|
options: { name: "memory_recall" },
|
|
636
860
|
makeExecute: ({ sulcusMem, backendMode, namespace, nativeLoader, isAvailable }) =>
|
|
637
|
-
async (_id
|
|
638
|
-
if (!isAvailable) {
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
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);
|
|
645
|
-
const results = res?.results ?? res?.items ?? res?.nodes ?? res ?? [];
|
|
861
|
+
async (_id, params) => {
|
|
862
|
+
if (!isAvailable || !sulcusMem) throw new Error(`Sulcus unavailable: ${nativeLoader.error || "not loaded"}`);
|
|
863
|
+
const searchNamespace = (params.namespace as string | undefined) ?? namespace;
|
|
864
|
+
const res = await sulcusMem.search_memory(params.query as string, (params.limit as number | undefined) ?? 5, searchNamespace);
|
|
865
|
+
const results = res?.results ?? [];
|
|
646
866
|
return {
|
|
647
867
|
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
|
648
|
-
details: { results, backend: backendMode, namespace: searchNamespace }
|
|
868
|
+
details: { results: results as unknown as Record<string, unknown>[], backend: backendMode, namespace: searchNamespace },
|
|
649
869
|
};
|
|
650
870
|
},
|
|
651
871
|
},
|
|
@@ -658,54 +878,41 @@ const toolDefinitions: Record<string, ToolDefinition> = {
|
|
|
658
878
|
parameters: Type.Object({
|
|
659
879
|
content: Type.String({ description: "Memory content. Supports Markdown formatting for structured content." }),
|
|
660
880
|
memory_type: Type.Optional(Type.Union([
|
|
661
|
-
Type.Literal("episodic"),
|
|
662
|
-
Type.Literal("
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
Type.Literal("fact")
|
|
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" })),
|
|
881
|
+
Type.Literal("episodic"), Type.Literal("semantic"), Type.Literal("preference"),
|
|
882
|
+
Type.Literal("procedural"), Type.Literal("fact"),
|
|
883
|
+
], { description: "Memory type. Default: episodic" })),
|
|
884
|
+
train: Type.Optional(Type.Boolean({ description: "Signal the SIU to learn from this manual store. Default: false" })),
|
|
668
885
|
}),
|
|
669
886
|
},
|
|
670
887
|
options: { name: "memory_store" },
|
|
671
888
|
makeExecute: ({ sulcusMem, backendMode, namespace, nativeLoader, isAvailable, logger }) =>
|
|
672
|
-
async (_id
|
|
673
|
-
|
|
674
|
-
if (isJunkMemory(
|
|
675
|
-
logger.debug(`sulcus: filtered junk memory: "${
|
|
676
|
-
return {
|
|
677
|
-
content: [{ type: "text", text: `Filtered: content looks like system noise, not a meaningful memory.` }],
|
|
678
|
-
details: { filtered: true, reason: "junk_pattern" }
|
|
679
|
-
};
|
|
889
|
+
async (_id, params) => {
|
|
890
|
+
const content = params.content as string;
|
|
891
|
+
if (isJunkMemory(content)) {
|
|
892
|
+
logger.debug?.(`sulcus: filtered junk memory: "${content.substring(0, 50)}..."`);
|
|
893
|
+
return { content: [{ type: "text", text: "Filtered: content looks like system noise, not a meaningful memory." }], details: { filtered: true } };
|
|
680
894
|
}
|
|
681
|
-
if (!isAvailable) {
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
const res = await sulcusMem.add_memory(params.content, params.memory_type ?? null);
|
|
895
|
+
if (!isAvailable || !sulcusMem) throw new Error(`Sulcus unavailable: ${nativeLoader.error || "not loaded"}`);
|
|
896
|
+
const mtype = (params.memory_type as string | undefined) || "episodic";
|
|
897
|
+
const res = await sulcusMem.add_memory(content, mtype);
|
|
685
898
|
const nodeId = res?.id ?? "unknown";
|
|
686
|
-
const mtype = params.memory_type || "episodic";
|
|
687
|
-
// If train=true, submit a training signal to the SIU
|
|
688
899
|
let trainResult: string | null = null;
|
|
689
|
-
if (params.train
|
|
900
|
+
if (params.train === true) {
|
|
690
901
|
try {
|
|
691
|
-
await
|
|
692
|
-
memory_id: nodeId,
|
|
693
|
-
|
|
694
|
-
corrected_store: true,
|
|
695
|
-
corrected_type: mtype,
|
|
696
|
-
content_snapshot: params.content,
|
|
697
|
-
source: "plugin",
|
|
902
|
+
await sulcusMem.request("POST", "/api/v2/siu/signal", {
|
|
903
|
+
memory_id: nodeId, signal_type: "accept", corrected_store: true,
|
|
904
|
+
corrected_type: mtype, content_snapshot: content, source: "plugin",
|
|
698
905
|
});
|
|
699
906
|
trainResult = "training signal submitted";
|
|
700
907
|
logger.info(`sulcus: SIU training signal sent for memory ${nodeId} (store, ${mtype})`);
|
|
701
|
-
} catch (e:
|
|
702
|
-
trainResult = `training signal failed: ${e.message}`;
|
|
703
|
-
logger.warn(`sulcus: SIU training signal failed: ${
|
|
908
|
+
} catch (e: unknown) {
|
|
909
|
+
trainResult = `training signal failed: ${e instanceof Error ? e.message : e}`;
|
|
910
|
+
logger.warn(`sulcus: SIU training signal failed: ${trainResult}`);
|
|
704
911
|
}
|
|
705
912
|
}
|
|
706
913
|
return {
|
|
707
914
|
content: [{ type: "text", text: `Stored [${mtype}] memory (id: ${nodeId}) → backend: ${backendMode}, namespace: ${namespace}${trainResult ? ` | SIU: ${trainResult}` : ""}` }],
|
|
708
|
-
details: { id: nodeId, memory_type: mtype, backend: backendMode, namespace, train: trainResult,
|
|
915
|
+
details: { ...res, id: nodeId, memory_type: mtype, backend: backendMode, namespace, train: trainResult as unknown as Record<string, unknown> },
|
|
709
916
|
};
|
|
710
917
|
},
|
|
711
918
|
},
|
|
@@ -719,75 +926,40 @@ const toolDefinitions: Record<string, ToolDefinition> = {
|
|
|
719
926
|
},
|
|
720
927
|
options: { name: "memory_status" },
|
|
721
928
|
makeExecute: ({ sulcusMem, backendMode, namespace, nativeLoader, storeLibPath, vectorsLibPath, wasmDir, isAvailable }) =>
|
|
722
|
-
async (_id
|
|
723
|
-
if (!isAvailable) {
|
|
724
|
-
return {
|
|
725
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
726
|
-
status: "unavailable",
|
|
727
|
-
backend: backendMode,
|
|
728
|
-
namespace,
|
|
729
|
-
error: nativeLoader.error || "WASM not loaded",
|
|
730
|
-
storeLib: storeLibPath,
|
|
731
|
-
vectorsLib: vectorsLibPath,
|
|
732
|
-
wasmDir,
|
|
733
|
-
}, null, 2) }],
|
|
734
|
-
};
|
|
929
|
+
async (_id, _params) => {
|
|
930
|
+
if (!isAvailable || !sulcusMem) {
|
|
931
|
+
return { content: [{ type: "text", text: JSON.stringify({ status: "unavailable", backend: backendMode, namespace, error: nativeLoader.error || "not loaded", storeLib: storeLibPath, vectorsLib: vectorsLibPath, wasmDir }, null, 2) }] };
|
|
735
932
|
}
|
|
736
933
|
try {
|
|
737
|
-
// Fetch both status info and hot nodes in parallel
|
|
738
934
|
const [statusInfo, hotNodes] = await Promise.all([
|
|
739
935
|
sulcusMem.request("GET", "/api/v1/agent/memory/status").catch(() => null),
|
|
740
936
|
sulcusMem.list_hot_nodes(20),
|
|
741
937
|
]);
|
|
742
|
-
const nodeList = hotNodes?.nodes ??
|
|
743
|
-
const
|
|
744
|
-
return {
|
|
745
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
746
|
-
status: "ok",
|
|
747
|
-
backend: backendMode,
|
|
748
|
-
namespace,
|
|
749
|
-
...(statusInfo?.capabilities ? { capabilities: statusInfo.capabilities } : {}),
|
|
750
|
-
...(statusInfo?.stats ? { stats: statusInfo.stats } : {}),
|
|
751
|
-
hot_node_count: count,
|
|
752
|
-
hot_nodes: nodeList,
|
|
753
|
-
}, null, 2) }],
|
|
754
|
-
details: { status: "ok", backend: backendMode, namespace, count, ...(statusInfo?.stats ?? {}) }
|
|
755
|
-
};
|
|
756
|
-
} catch (e: any) {
|
|
938
|
+
const nodeList = hotNodes?.nodes ?? [];
|
|
939
|
+
const si = statusInfo as Record<string, unknown> | null;
|
|
757
940
|
return {
|
|
758
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
759
|
-
|
|
760
|
-
backend: backendMode,
|
|
761
|
-
namespace,
|
|
762
|
-
error: e.message,
|
|
763
|
-
}, null, 2) }],
|
|
941
|
+
content: [{ type: "text", text: JSON.stringify({ status: "ok", backend: backendMode, namespace, ...(si?.capabilities ? { capabilities: si.capabilities } : {}), ...(si?.stats ? { stats: si.stats } : {}), hot_node_count: nodeList.length, hot_nodes: nodeList }, null, 2) }],
|
|
942
|
+
details: { status: "ok", backend: backendMode, namespace, count: nodeList.length },
|
|
764
943
|
};
|
|
944
|
+
} catch (e: unknown) {
|
|
945
|
+
return { content: [{ type: "text", text: JSON.stringify({ status: "error", backend: backendMode, namespace, error: e instanceof Error ? e.message : String(e) }, null, 2) }] };
|
|
765
946
|
}
|
|
766
947
|
},
|
|
767
948
|
},
|
|
768
949
|
|
|
769
|
-
// ── New WASM-capability tools (disabled by default) ──
|
|
770
|
-
|
|
771
950
|
consolidate: {
|
|
772
951
|
schema: {
|
|
773
952
|
name: "consolidate",
|
|
774
953
|
label: "Memory Consolidate",
|
|
775
954
|
description: "Consolidate cold memories: merges, prunes, or archives nodes below the given heat threshold.",
|
|
776
|
-
parameters: Type.Object({
|
|
777
|
-
min_heat: Type.Optional(Type.Number({ default: 0.1, description: "Nodes with heat below this value are candidates for consolidation (0.0–1.0)." }))
|
|
778
|
-
}),
|
|
955
|
+
parameters: Type.Object({ min_heat: Type.Optional(Type.Number({ default: 0.1, description: "Heat threshold (0.0–1.0)." })) }),
|
|
779
956
|
},
|
|
780
957
|
options: { name: "consolidate" },
|
|
781
958
|
makeExecute: ({ sulcusMem, backendMode, namespace, nativeLoader, isAvailable }) =>
|
|
782
|
-
async (_id
|
|
783
|
-
if (!isAvailable) {
|
|
784
|
-
|
|
785
|
-
}
|
|
786
|
-
const res = await sulcusMem.consolidate(params.min_heat ?? 0.1);
|
|
787
|
-
return {
|
|
788
|
-
content: [{ type: "text", text: JSON.stringify({ result: res, backend: backendMode, namespace }, null, 2) }],
|
|
789
|
-
details: { result: res, backend: backendMode, namespace }
|
|
790
|
-
};
|
|
959
|
+
async (_id, params) => {
|
|
960
|
+
if (!isAvailable || !sulcusMem) throw new Error(`Sulcus unavailable: ${nativeLoader.error || "not loaded"}`);
|
|
961
|
+
const res = await sulcusMem.consolidate((params.min_heat as number | undefined) ?? 0.1);
|
|
962
|
+
return { content: [{ type: "text", text: JSON.stringify({ result: res, backend: backendMode, namespace }, null, 2) }], details: { result: res as Record<string, unknown>, backend: backendMode, namespace } };
|
|
791
963
|
},
|
|
792
964
|
},
|
|
793
965
|
|
|
@@ -800,15 +972,10 @@ const toolDefinitions: Record<string, ToolDefinition> = {
|
|
|
800
972
|
},
|
|
801
973
|
options: { name: "export_markdown" },
|
|
802
974
|
makeExecute: ({ sulcusMem, backendMode, namespace, nativeLoader, isAvailable }) =>
|
|
803
|
-
async (_id
|
|
804
|
-
if (!isAvailable) {
|
|
805
|
-
|
|
806
|
-
}
|
|
807
|
-
const markdown: string = await sulcusMem.export_markdown();
|
|
808
|
-
return {
|
|
809
|
-
content: [{ type: "text", text: markdown }],
|
|
810
|
-
details: { backend: backendMode, namespace, length: markdown.length }
|
|
811
|
-
};
|
|
975
|
+
async (_id, _params) => {
|
|
976
|
+
if (!isAvailable || !sulcusMem) throw new Error(`Sulcus unavailable: ${nativeLoader.error || "not loaded"}`);
|
|
977
|
+
const markdown = await sulcusMem.export_markdown();
|
|
978
|
+
return { content: [{ type: "text", text: markdown }], details: { backend: backendMode, namespace, length: markdown.length } };
|
|
812
979
|
},
|
|
813
980
|
},
|
|
814
981
|
|
|
@@ -817,21 +984,14 @@ const toolDefinitions: Record<string, ToolDefinition> = {
|
|
|
817
984
|
name: "import_markdown",
|
|
818
985
|
label: "Import Memory (Markdown)",
|
|
819
986
|
description: "Import memories from a Markdown document into the current namespace.",
|
|
820
|
-
parameters: Type.Object({
|
|
821
|
-
text: Type.String({ description: "Markdown content to import into Sulcus memory." })
|
|
822
|
-
}),
|
|
987
|
+
parameters: Type.Object({ text: Type.String({ description: "Markdown content to import." }) }),
|
|
823
988
|
},
|
|
824
989
|
options: { name: "import_markdown" },
|
|
825
990
|
makeExecute: ({ sulcusMem, backendMode, namespace, nativeLoader, isAvailable }) =>
|
|
826
|
-
async (_id
|
|
827
|
-
if (!isAvailable) {
|
|
828
|
-
|
|
829
|
-
}
|
|
830
|
-
const res = await sulcusMem.import_markdown(params.text);
|
|
831
|
-
return {
|
|
832
|
-
content: [{ type: "text", text: JSON.stringify({ result: res, backend: backendMode, namespace }, null, 2) }],
|
|
833
|
-
details: { result: res, backend: backendMode, namespace }
|
|
834
|
-
};
|
|
991
|
+
async (_id, params) => {
|
|
992
|
+
if (!isAvailable || !sulcusMem) throw new Error(`Sulcus unavailable: ${nativeLoader.error || "not loaded"}`);
|
|
993
|
+
const res = await sulcusMem.import_markdown(params.text as string);
|
|
994
|
+
return { content: [{ type: "text", text: JSON.stringify({ result: res, backend: backendMode, namespace }, null, 2) }], details: { result: res as Record<string, unknown>, backend: backendMode, namespace } };
|
|
835
995
|
},
|
|
836
996
|
},
|
|
837
997
|
|
|
@@ -842,20 +1002,15 @@ const toolDefinitions: Record<string, ToolDefinition> = {
|
|
|
842
1002
|
description: "Evaluate reactive memory triggers against an event and context.",
|
|
843
1003
|
parameters: Type.Object({
|
|
844
1004
|
event: Type.String({ description: "Event name to evaluate triggers against (e.g. 'agent_end', 'user_message')." }),
|
|
845
|
-
context_json: Type.Optional(Type.String({ description: "JSON string of additional context
|
|
1005
|
+
context_json: Type.Optional(Type.String({ description: "JSON string of additional context." })),
|
|
846
1006
|
}),
|
|
847
1007
|
},
|
|
848
1008
|
options: { name: "evaluate_triggers" },
|
|
849
1009
|
makeExecute: ({ sulcusMem, backendMode, namespace, nativeLoader, isAvailable }) =>
|
|
850
|
-
async (_id
|
|
851
|
-
if (!isAvailable) {
|
|
852
|
-
|
|
853
|
-
}
|
|
854
|
-
const res = await sulcusMem.evaluate_triggers(params.event, params.context_json ?? "{}");
|
|
855
|
-
return {
|
|
856
|
-
content: [{ type: "text", text: JSON.stringify({ result: res, backend: backendMode, namespace }, null, 2) }],
|
|
857
|
-
details: { result: res, backend: backendMode, namespace }
|
|
858
|
-
};
|
|
1010
|
+
async (_id, params) => {
|
|
1011
|
+
if (!isAvailable || !sulcusMem) throw new Error(`Sulcus unavailable: ${nativeLoader.error || "not loaded"}`);
|
|
1012
|
+
const res = await sulcusMem.evaluate_triggers(params.event, (params.context_json as string | undefined) ?? "{}");
|
|
1013
|
+
return { content: [{ type: "text", text: JSON.stringify({ result: res, backend: backendMode, namespace }, null, 2) }], details: { result: res as Record<string, unknown>, backend: backendMode, namespace } };
|
|
859
1014
|
},
|
|
860
1015
|
},
|
|
861
1016
|
|
|
@@ -863,65 +1018,46 @@ const toolDefinitions: Record<string, ToolDefinition> = {
|
|
|
863
1018
|
schema: {
|
|
864
1019
|
name: "memory_delete",
|
|
865
1020
|
label: "Delete Memory",
|
|
866
|
-
description: "Delete a memory node by ID. With train=true (default),
|
|
1021
|
+
description: "Delete a memory node by ID. With train=true (default), trains SIVU to reject similar content.",
|
|
867
1022
|
parameters: Type.Object({
|
|
868
1023
|
id: Type.String({ description: "Memory node ID to delete." }),
|
|
869
|
-
train: Type.Optional(Type.Boolean({ default: true, description: "Train SIVU to reject similar content (default true).
|
|
1024
|
+
train: Type.Optional(Type.Boolean({ default: true, description: "Train SIVU to reject similar content (default true)." })),
|
|
870
1025
|
}),
|
|
871
1026
|
},
|
|
872
1027
|
options: { name: "memory_delete" },
|
|
873
1028
|
makeExecute: ({ sulcusMem, backendMode, namespace, nativeLoader, isAvailable }) =>
|
|
874
|
-
async (_id
|
|
875
|
-
if (!isAvailable) {
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
const train = params.train !== false; // default true
|
|
879
|
-
const res = await (sulcusMem as SulcusCloudClient).delete_memory(params.id, train);
|
|
1029
|
+
async (_id, params) => {
|
|
1030
|
+
if (!isAvailable || !sulcusMem) throw new Error(`Sulcus unavailable: ${nativeLoader.error || "not loaded"}`);
|
|
1031
|
+
const train = params.train !== false;
|
|
1032
|
+
const res = await sulcusMem.delete_memory(params.id as string, train);
|
|
880
1033
|
return {
|
|
881
|
-
content: [{ type: "text", text: `Deleted memory ${params.id}${train ? " (trained SIVU to reject similar)" : ""}. Backend: ${backendMode}, namespace: ${namespace}` }],
|
|
882
|
-
details: { id: params.id, trained: train, result: res, backend: backendMode, namespace }
|
|
1034
|
+
content: [{ type: "text", text: `Deleted memory ${params.id as string}${train ? " (trained SIVU to reject similar)" : ""}. Backend: ${backendMode}, namespace: ${namespace}` }],
|
|
1035
|
+
details: { id: params.id as string, trained: train, result: res as Record<string, unknown>, backend: backendMode, namespace },
|
|
883
1036
|
};
|
|
884
1037
|
},
|
|
885
1038
|
},
|
|
886
1039
|
|
|
887
|
-
// ── SIU v2 Tools ───────────────────────────────────────────────────────────
|
|
888
|
-
// These tools call the SIU v2 server endpoints for text classification.
|
|
889
|
-
// Requires cloud backend (serverUrl + apiKey). Uses /api/v2/siu/* endpoints.
|
|
890
|
-
|
|
891
1040
|
siu_label: {
|
|
892
1041
|
schema: {
|
|
893
1042
|
name: "siu_label",
|
|
894
1043
|
label: "SIU Label",
|
|
895
|
-
description: "Classify text using SIU v2 — returns SIVU store/reject decision and SICU memory type classification
|
|
1044
|
+
description: "Classify text using SIU v2 — returns SIVU store/reject decision and SICU memory type classification.",
|
|
896
1045
|
parameters: Type.Object({
|
|
897
1046
|
text: Type.String({ description: "Text to classify." }),
|
|
898
1047
|
classify_only: Type.Optional(Type.Boolean({ description: "Skip SIVU quality gate, only run SICU type classification." })),
|
|
899
1048
|
}),
|
|
900
1049
|
},
|
|
901
1050
|
options: { name: "siu_label" },
|
|
902
|
-
makeExecute: ({
|
|
903
|
-
async (_id
|
|
904
|
-
if (!siuRequest) {
|
|
905
|
-
return {
|
|
906
|
-
content: [{ type: "text", text: "SIU label requires cloud backend (serverUrl + apiKey)." }],
|
|
907
|
-
details: { error: "cloud_required" },
|
|
908
|
-
};
|
|
909
|
-
}
|
|
1051
|
+
makeExecute: ({ siuRequest, logger }) =>
|
|
1052
|
+
async (_id, params) => {
|
|
1053
|
+
if (!siuRequest) return { content: [{ type: "text", text: "SIU label requires cloud backend (serverUrl + apiKey)." }] };
|
|
910
1054
|
try {
|
|
911
|
-
const res = await siuRequest("POST", "/api/v2/siu/label", {
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
details: res,
|
|
918
|
-
};
|
|
919
|
-
} catch (e: any) {
|
|
920
|
-
logger.warn(`sulcus: siu_label failed: ${e.message}`);
|
|
921
|
-
return {
|
|
922
|
-
content: [{ type: "text", text: `SIU label failed: ${e.message}` }],
|
|
923
|
-
details: { error: e.message },
|
|
924
|
-
};
|
|
1055
|
+
const res = await siuRequest("POST", "/api/v2/siu/label", { text: params.text as string, classify_only: params.classify_only ?? false });
|
|
1056
|
+
return { content: [{ type: "text", text: JSON.stringify(res, null, 2) }], details: res as Record<string, unknown> };
|
|
1057
|
+
} catch (e: unknown) {
|
|
1058
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1059
|
+
logger.warn(`sulcus: siu_label failed: ${msg}`);
|
|
1060
|
+
return { content: [{ type: "text", text: `SIU label failed: ${msg}` }] };
|
|
925
1061
|
}
|
|
926
1062
|
},
|
|
927
1063
|
},
|
|
@@ -935,25 +1071,15 @@ const toolDefinitions: Record<string, ToolDefinition> = {
|
|
|
935
1071
|
},
|
|
936
1072
|
options: { name: "siu_status" },
|
|
937
1073
|
makeExecute: ({ siuRequest, logger }) =>
|
|
938
|
-
async (_id
|
|
939
|
-
if (!siuRequest) {
|
|
940
|
-
return {
|
|
941
|
-
content: [{ type: "text", text: "SIU status requires cloud backend (serverUrl + apiKey)." }],
|
|
942
|
-
details: { error: "cloud_required" },
|
|
943
|
-
};
|
|
944
|
-
}
|
|
1074
|
+
async (_id, _params) => {
|
|
1075
|
+
if (!siuRequest) return { content: [{ type: "text", text: "SIU status requires cloud backend." }] };
|
|
945
1076
|
try {
|
|
946
1077
|
const res = await siuRequest("GET", "/api/v2/siu/status");
|
|
947
|
-
return {
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
};
|
|
951
|
-
|
|
952
|
-
logger.warn(`sulcus: siu_status failed: ${e.message}`);
|
|
953
|
-
return {
|
|
954
|
-
content: [{ type: "text", text: `SIU status failed: ${e.message}` }],
|
|
955
|
-
details: { error: e.message },
|
|
956
|
-
};
|
|
1078
|
+
return { content: [{ type: "text", text: JSON.stringify(res, null, 2) }], details: res as Record<string, unknown> };
|
|
1079
|
+
} catch (e: unknown) {
|
|
1080
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1081
|
+
logger.warn(`sulcus: siu_status failed: ${msg}`);
|
|
1082
|
+
return { content: [{ type: "text", text: `SIU status failed: ${msg}` }] };
|
|
957
1083
|
}
|
|
958
1084
|
},
|
|
959
1085
|
},
|
|
@@ -962,44 +1088,31 @@ const toolDefinitions: Record<string, ToolDefinition> = {
|
|
|
962
1088
|
schema: {
|
|
963
1089
|
name: "siu_retrain",
|
|
964
1090
|
label: "SIU Retrain",
|
|
965
|
-
description: "Trigger an async retrain of SIU v2 models using accumulated training signals.
|
|
1091
|
+
description: "Trigger an async retrain of SIU v2 models using accumulated training signals.",
|
|
966
1092
|
parameters: Type.Object({}),
|
|
967
1093
|
},
|
|
968
1094
|
options: { name: "siu_retrain" },
|
|
969
1095
|
makeExecute: ({ siuRequest, logger }) =>
|
|
970
|
-
async (_id
|
|
971
|
-
if (!siuRequest) {
|
|
972
|
-
return {
|
|
973
|
-
content: [{ type: "text", text: "SIU retrain requires cloud backend (serverUrl + apiKey)." }],
|
|
974
|
-
details: { error: "cloud_required" },
|
|
975
|
-
};
|
|
976
|
-
}
|
|
1096
|
+
async (_id, _params) => {
|
|
1097
|
+
if (!siuRequest) return { content: [{ type: "text", text: "SIU retrain requires cloud backend." }] };
|
|
977
1098
|
try {
|
|
978
1099
|
const res = await siuRequest("POST", "/api/v2/siu/retrain");
|
|
979
|
-
return {
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
};
|
|
983
|
-
|
|
984
|
-
logger.warn(`sulcus: siu_retrain failed: ${e.message}`);
|
|
985
|
-
return {
|
|
986
|
-
content: [{ type: "text", text: `SIU retrain failed: ${e.message}` }],
|
|
987
|
-
details: { error: e.message },
|
|
988
|
-
};
|
|
1100
|
+
return { content: [{ type: "text", text: JSON.stringify(res, null, 2) }], details: res as Record<string, unknown> };
|
|
1101
|
+
} catch (e: unknown) {
|
|
1102
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1103
|
+
logger.warn(`sulcus: siu_retrain failed: ${msg}`);
|
|
1104
|
+
return { content: [{ type: "text", text: `SIU retrain failed: ${msg}` }] };
|
|
989
1105
|
}
|
|
990
1106
|
},
|
|
991
1107
|
},
|
|
1108
|
+
|
|
992
1109
|
trigger_feedback: {
|
|
993
1110
|
schema: {
|
|
994
1111
|
name: "trigger_feedback",
|
|
995
1112
|
label: "Trigger Feedback",
|
|
996
|
-
description:
|
|
997
|
-
"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.",
|
|
1113
|
+
description: "Record feedback on a trigger fire (for SITU training).",
|
|
998
1114
|
parameters: Type.Object({
|
|
999
|
-
feedback_type: Type.String({
|
|
1000
|
-
description:
|
|
1001
|
-
'One of: "false_positive" (fired wrongly), "false_negative" (missed fire), "correct" (good fire), "wrong_action" (fired but wrong action)',
|
|
1002
|
-
}),
|
|
1115
|
+
feedback_type: Type.String({ description: 'One of: "false_positive", "false_negative", "correct", "wrong_action"' }),
|
|
1003
1116
|
trigger_id: Type.Optional(Type.String({ description: "UUID of the trigger rule" })),
|
|
1004
1117
|
trigger_log_id: Type.Optional(Type.String({ description: "UUID of the trigger fire log entry" })),
|
|
1005
1118
|
event_type: Type.Optional(Type.String({ description: "Event type: memory_created, heat_threshold, recall, etc." })),
|
|
@@ -1010,30 +1123,111 @@ const toolDefinitions: Record<string, ToolDefinition> = {
|
|
|
1010
1123
|
},
|
|
1011
1124
|
options: { name: "trigger_feedback" },
|
|
1012
1125
|
makeExecute: ({ siuRequest, logger }) =>
|
|
1013
|
-
async (_id
|
|
1014
|
-
if (!siuRequest) {
|
|
1015
|
-
return {
|
|
1016
|
-
content: [{ type: "text", text: "Trigger feedback requires cloud backend (serverUrl + apiKey)." }],
|
|
1017
|
-
details: { error: "cloud_required" },
|
|
1018
|
-
};
|
|
1019
|
-
}
|
|
1126
|
+
async (_id, params) => {
|
|
1127
|
+
if (!siuRequest) return { content: [{ type: "text", text: "Trigger feedback requires cloud backend." }] };
|
|
1020
1128
|
try {
|
|
1021
1129
|
const res = await siuRequest("POST", "/api/v1/triggers/feedback", params);
|
|
1022
|
-
return {
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
};
|
|
1026
|
-
|
|
1027
|
-
logger.warn(`sulcus: trigger_feedback failed: ${e.message}`);
|
|
1028
|
-
return {
|
|
1029
|
-
content: [{ type: "text", text: `Trigger feedback failed: ${e.message}` }],
|
|
1030
|
-
details: { error: e.message },
|
|
1031
|
-
};
|
|
1130
|
+
return { content: [{ type: "text", text: JSON.stringify(res, null, 2) }], details: res as Record<string, unknown> };
|
|
1131
|
+
} catch (e: unknown) {
|
|
1132
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1133
|
+
logger.warn(`sulcus: trigger_feedback failed: ${msg}`);
|
|
1134
|
+
return { content: [{ type: "text", text: `Trigger feedback failed: ${msg}` }] };
|
|
1032
1135
|
}
|
|
1033
1136
|
},
|
|
1034
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
|
+
},
|
|
1035
1162
|
};
|
|
1036
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
|
+
|
|
1037
1231
|
// ─── PLUGIN ──────────────────────────────────────────────────────────────────
|
|
1038
1232
|
|
|
1039
1233
|
const sulcusPlugin = {
|
|
@@ -1042,106 +1236,106 @@ const sulcusPlugin = {
|
|
|
1042
1236
|
description: "Sulcus-backed vMMU memory for OpenClaw — thermodynamic decay, reactive triggers, local-first",
|
|
1043
1237
|
kind: "memory" as const,
|
|
1044
1238
|
|
|
1045
|
-
register(api:
|
|
1239
|
+
register(api: Record<string, unknown>) {
|
|
1240
|
+
const logger = api.logger as PluginLogger;
|
|
1241
|
+
const pluginConfig = (api.pluginConfig ?? {}) as Record<string, unknown>;
|
|
1242
|
+
|
|
1046
1243
|
// ── Configuration ──
|
|
1047
|
-
const libDir =
|
|
1048
|
-
? resolve(
|
|
1244
|
+
const libDir = pluginConfig?.libDir
|
|
1245
|
+
? resolve(pluginConfig.libDir as string)
|
|
1049
1246
|
: resolve(process.env.HOME || "~", ".sulcus/lib");
|
|
1050
1247
|
|
|
1051
|
-
const storeLibPath =
|
|
1052
|
-
? resolve(
|
|
1248
|
+
const storeLibPath = pluginConfig?.storeLibPath
|
|
1249
|
+
? resolve(pluginConfig.storeLibPath as string)
|
|
1053
1250
|
: resolve(libDir, process.platform === "darwin" ? "libsulcus_store.dylib" : "libsulcus_store.so");
|
|
1054
1251
|
|
|
1055
|
-
const vectorsLibPath =
|
|
1056
|
-
? resolve(
|
|
1252
|
+
const vectorsLibPath = pluginConfig?.vectorsLibPath
|
|
1253
|
+
? resolve(pluginConfig.vectorsLibPath as string)
|
|
1057
1254
|
: resolve(libDir, process.platform === "darwin" ? "libsulcus_vectors.dylib" : "libsulcus_vectors.so");
|
|
1058
1255
|
|
|
1059
|
-
const wasmDir =
|
|
1060
|
-
? resolve(
|
|
1256
|
+
const wasmDir = pluginConfig?.wasmDir
|
|
1257
|
+
? resolve(pluginConfig.wasmDir as string)
|
|
1061
1258
|
: resolve(__dirname, "wasm");
|
|
1062
1259
|
|
|
1063
|
-
|
|
1064
|
-
const
|
|
1065
|
-
const apiKey: string | undefined = api.pluginConfig?.apiKey;
|
|
1260
|
+
const serverUrl = pluginConfig?.serverUrl as string | undefined;
|
|
1261
|
+
const apiKey = pluginConfig?.apiKey as string | undefined;
|
|
1066
1262
|
|
|
1067
|
-
|
|
1068
|
-
const
|
|
1069
|
-
const namespace = api.pluginConfig?.namespace === "default" && agentId
|
|
1263
|
+
const agentId = pluginConfig?.agentId as string | undefined;
|
|
1264
|
+
const namespace = pluginConfig?.namespace === "default" && agentId
|
|
1070
1265
|
? agentId
|
|
1071
|
-
: (
|
|
1266
|
+
: ((pluginConfig?.namespace as string | undefined) || agentId || "default");
|
|
1267
|
+
|
|
1268
|
+
// New config options (v4.0.0)
|
|
1269
|
+
const autoRecall: boolean = (pluginConfig?.autoRecall as boolean | undefined) ?? false;
|
|
1270
|
+
const autoCapture: boolean = (pluginConfig?.autoCapture as boolean | undefined) ?? false;
|
|
1271
|
+
const maxRecallResults: number = Math.min(20, Math.max(1, (pluginConfig?.maxRecallResults as number | undefined) ?? 5));
|
|
1272
|
+
const profileFrequency: number = Math.min(500, Math.max(1, (pluginConfig?.profileFrequency as number | undefined) ?? 10));
|
|
1072
1273
|
|
|
1073
|
-
// ── Load hooks config
|
|
1074
|
-
const hooksConfig = loadHooksConfig(
|
|
1274
|
+
// ── Load hooks config ──
|
|
1275
|
+
const hooksConfig = loadHooksConfig(pluginConfig);
|
|
1075
1276
|
|
|
1076
|
-
// ── Backend init
|
|
1077
|
-
let sulcusMem:
|
|
1277
|
+
// ── Backend init ──
|
|
1278
|
+
let sulcusMem: SulcusCloudClient | null = null;
|
|
1078
1279
|
let backendMode = "unavailable";
|
|
1079
1280
|
const nativeLoader = new NativeLibLoader(storeLibPath, vectorsLibPath);
|
|
1080
1281
|
|
|
1081
|
-
// Priority 1: Cloud mode — if serverUrl + apiKey are configured, use HTTP.
|
|
1082
|
-
// No local dylibs needed. This is the path for cloud-only users.
|
|
1083
1282
|
if (serverUrl && apiKey) {
|
|
1084
1283
|
try {
|
|
1085
|
-
|
|
1086
|
-
sulcusMem = cloudClient;
|
|
1284
|
+
sulcusMem = new SulcusCloudClient(serverUrl, apiKey);
|
|
1087
1285
|
backendMode = "cloud";
|
|
1088
|
-
|
|
1089
|
-
} catch (e:
|
|
1090
|
-
|
|
1286
|
+
logger.info(`sulcus: using cloud backend (server: ${serverUrl})`);
|
|
1287
|
+
} catch (e: unknown) {
|
|
1288
|
+
logger.warn(`sulcus: cloud client init failed: ${e instanceof Error ? e.message : e}`);
|
|
1091
1289
|
}
|
|
1092
1290
|
}
|
|
1093
1291
|
|
|
1094
|
-
// Priority 2: Local WASM+native — if cloud isn't configured or failed,
|
|
1095
|
-
// try loading native dylibs + WASM module for fully local operation.
|
|
1096
1292
|
if (sulcusMem === null) {
|
|
1097
|
-
nativeLoader.init(
|
|
1293
|
+
nativeLoader.init(logger);
|
|
1098
1294
|
if (nativeLoader.loaded) {
|
|
1099
1295
|
const wasmJsPath = resolve(wasmDir, "sulcus_wasm.js");
|
|
1100
1296
|
if (existsSync(wasmJsPath)) {
|
|
1101
1297
|
try {
|
|
1102
|
-
|
|
1298
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
1299
|
+
const { SulcusMem, on_init } = require(wasmJsPath) as { SulcusMem: { create: (q: unknown, e: unknown) => SulcusCloudClient }; on_init?: () => void };
|
|
1103
1300
|
if (typeof on_init === "function") on_init();
|
|
1104
|
-
|
|
1105
|
-
const queryFn = nativeLoader.makeQueryFn();
|
|
1106
|
-
const embedFn = nativeLoader.makeEmbedFn();
|
|
1107
|
-
sulcusMem = SulcusMem.create(queryFn, embedFn);
|
|
1301
|
+
sulcusMem = SulcusMem.create(nativeLoader.makeQueryFn(), nativeLoader.makeEmbedFn());
|
|
1108
1302
|
backendMode = "wasm";
|
|
1109
|
-
|
|
1110
|
-
} catch (e:
|
|
1111
|
-
|
|
1303
|
+
logger.info(`sulcus: SulcusMem created via WASM (wasm: ${wasmJsPath})`);
|
|
1304
|
+
} catch (e: unknown) {
|
|
1305
|
+
logger.warn(`sulcus: WASM load failed: ${e instanceof Error ? e.message : e}`);
|
|
1112
1306
|
}
|
|
1113
1307
|
} else {
|
|
1114
|
-
|
|
1308
|
+
logger.warn(`sulcus: WASM module not found at ${wasmJsPath}`);
|
|
1115
1309
|
}
|
|
1116
1310
|
} else {
|
|
1117
|
-
|
|
1311
|
+
logger.info(`sulcus: local mode skipped — ${nativeLoader.error || "dylibs not found"}`);
|
|
1118
1312
|
}
|
|
1119
1313
|
}
|
|
1120
1314
|
|
|
1121
1315
|
const isAvailable = sulcusMem !== null;
|
|
1316
|
+
const isCloudBackend = backendMode === "cloud" && sulcusMem instanceof SulcusCloudClient;
|
|
1122
1317
|
|
|
1123
1318
|
// Update static awareness with runtime info
|
|
1124
1319
|
STATIC_AWARENESS = buildStaticAwareness(backendMode, namespace);
|
|
1125
1320
|
|
|
1126
1321
|
// ── Startup summary ──
|
|
1127
1322
|
if (isAvailable) {
|
|
1128
|
-
|
|
1323
|
+
logger.info(`sulcus: registered (backend: ${backendMode}, namespace: ${namespace}, autoRecall: ${autoRecall}, autoCapture: ${autoCapture})`);
|
|
1129
1324
|
} else {
|
|
1130
1325
|
const hints: string[] = [];
|
|
1131
1326
|
if (!serverUrl && !apiKey) hints.push("no serverUrl/apiKey for cloud mode");
|
|
1132
|
-
if (serverUrl && !apiKey) hints.push("serverUrl set but apiKey missing");
|
|
1133
|
-
if (!serverUrl && apiKey) hints.push("apiKey set but serverUrl missing");
|
|
1327
|
+
else if (serverUrl && !apiKey) hints.push("serverUrl set but apiKey missing");
|
|
1328
|
+
else if (!serverUrl && apiKey) hints.push("apiKey set but serverUrl missing");
|
|
1134
1329
|
if (nativeLoader.error) hints.push(`local: ${nativeLoader.error}`);
|
|
1135
|
-
|
|
1330
|
+
logger.warn(`sulcus: unavailable — ${hints.join("; ") || "unknown reason"}`);
|
|
1136
1331
|
}
|
|
1137
1332
|
|
|
1138
|
-
// ── SIU v2 request helper
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
? (method: string, path: string, body?: any) => (sulcusMem as SulcusCloudClient).request(method, path, body)
|
|
1333
|
+
// ── SIU v2 request helper ──
|
|
1334
|
+
const siuRequestFn = isCloudBackend && sulcusMem
|
|
1335
|
+
? (method: string, path: string, body?: unknown) => (sulcusMem as SulcusCloudClient).request(method, path, body)
|
|
1142
1336
|
: null;
|
|
1143
1337
|
|
|
1144
|
-
// ── Shared deps
|
|
1338
|
+
// ── Shared deps ──
|
|
1145
1339
|
const toolDeps: ToolDeps = {
|
|
1146
1340
|
sulcusMem,
|
|
1147
1341
|
backendMode,
|
|
@@ -1150,62 +1344,189 @@ const sulcusPlugin = {
|
|
|
1150
1344
|
storeLibPath,
|
|
1151
1345
|
vectorsLibPath,
|
|
1152
1346
|
wasmDir,
|
|
1153
|
-
logger
|
|
1347
|
+
logger,
|
|
1154
1348
|
isAvailable,
|
|
1155
1349
|
siuRequest: siuRequestFn,
|
|
1156
1350
|
};
|
|
1157
1351
|
|
|
1158
|
-
// ── Shared context for hook handlers ──
|
|
1159
1352
|
const handlerCtx: HookHandlerCtx = {
|
|
1160
1353
|
sulcusMem,
|
|
1161
1354
|
backendMode,
|
|
1162
1355
|
namespace,
|
|
1163
|
-
logger
|
|
1356
|
+
logger,
|
|
1164
1357
|
nativeError: nativeLoader.error,
|
|
1165
1358
|
storeLibPath,
|
|
1166
1359
|
vectorsLibPath,
|
|
1167
1360
|
wasmDir,
|
|
1168
1361
|
};
|
|
1169
1362
|
|
|
1170
|
-
//
|
|
1171
|
-
//
|
|
1172
|
-
//
|
|
1173
|
-
|
|
1363
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1364
|
+
// SDK INTEGRATIONS (v4.0.0)
|
|
1365
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1366
|
+
|
|
1367
|
+
// 1. registerMemoryRuntime — Sulcus becomes the OpenClaw memory backend
|
|
1368
|
+
if (isCloudBackend && sulcusMem && typeof (api.registerMemoryRuntime as unknown) === "function") {
|
|
1369
|
+
try {
|
|
1370
|
+
(api.registerMemoryRuntime as (r: unknown) => void)(buildMemoryRuntime(sulcusMem as SulcusCloudClient, backendMode));
|
|
1371
|
+
logger.info("sulcus: registered as memory runtime (MemoryPluginRuntime)");
|
|
1372
|
+
} catch (e: unknown) {
|
|
1373
|
+
logger.warn(`sulcus: registerMemoryRuntime failed: ${e instanceof Error ? e.message : e}`);
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// 2. registerMemoryPromptSection — dynamic system prompt guidance
|
|
1378
|
+
if (typeof (api.registerMemoryPromptSection as unknown) === "function") {
|
|
1379
|
+
try {
|
|
1380
|
+
(api.registerMemoryPromptSection as (b: unknown) => void)(buildPromptSection);
|
|
1381
|
+
logger.info("sulcus: registered memory prompt section");
|
|
1382
|
+
} catch (e: unknown) {
|
|
1383
|
+
logger.warn(`sulcus: registerMemoryPromptSection failed: ${e instanceof Error ? e.message : e}`);
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
// 3. registerMemoryFlushPlan — no custom compaction flush
|
|
1388
|
+
if (typeof (api.registerMemoryFlushPlan as unknown) === "function") {
|
|
1389
|
+
try {
|
|
1390
|
+
(api.registerMemoryFlushPlan as (r: unknown) => void)(() => null);
|
|
1391
|
+
logger.info("sulcus: registered memory flush plan (no-op)");
|
|
1392
|
+
} catch (e: unknown) {
|
|
1393
|
+
logger.warn(`sulcus: registerMemoryFlushPlan failed: ${e instanceof Error ? e.message : e}`);
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
// 4. registerService — lifecycle management
|
|
1398
|
+
if (typeof (api.registerService as unknown) === "function") {
|
|
1399
|
+
try {
|
|
1400
|
+
(api.registerService as (s: unknown) => void)({
|
|
1401
|
+
id: "openclaw-sulcus",
|
|
1402
|
+
start: async (ctx: Record<string, unknown>) => {
|
|
1403
|
+
const svcLogger = (ctx?.logger ?? logger) as PluginLogger;
|
|
1404
|
+
if (!isAvailable || !sulcusMem) {
|
|
1405
|
+
svcLogger.warn("sulcus: service start — backend unavailable, running in degraded mode");
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
if (isCloudBackend) {
|
|
1409
|
+
try {
|
|
1410
|
+
const ok = await (sulcusMem as SulcusCloudClient).probe();
|
|
1411
|
+
if (ok) svcLogger.info(`sulcus: service started — cloud backend connected (${serverUrl}, namespace: ${namespace})`);
|
|
1412
|
+
else svcLogger.warn(`sulcus: service started — cloud backend unreachable (${serverUrl})`);
|
|
1413
|
+
} catch (e
|
|
1414
|
+
: unknown) {
|
|
1415
|
+
svcLogger.warn("sulcus: service start probe failed");
|
|
1416
|
+
}
|
|
1417
|
+
} else {
|
|
1418
|
+
svcLogger.info("sulcus: service started (backend: " + backendMode + ", namespace: " + namespace + ")");
|
|
1419
|
+
}
|
|
1420
|
+
},
|
|
1421
|
+
stop: async (ctx: Record<string, unknown>) => {
|
|
1422
|
+
const svcLogger = (ctx?.logger ?? logger) as PluginLogger;
|
|
1423
|
+
svcLogger.info("sulcus: service stopped");
|
|
1424
|
+
},
|
|
1425
|
+
});
|
|
1426
|
+
logger.info("sulcus: registered service lifecycle");
|
|
1427
|
+
} catch (e: unknown) {
|
|
1428
|
+
logger.warn("sulcus: registerService failed: " + (e instanceof Error ? e.message : String(e)));
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
// 5. Enhanced before_agent_start with prependContext (SDK path)
|
|
1433
|
+
// When autoRecall=true and cloud backend available, use prependContext SDK pattern.
|
|
1434
|
+
// Falls back to legacy hook-based path when SDK is not available.
|
|
1435
|
+
if (autoRecall && isCloudBackend && sulcusMem) {
|
|
1436
|
+
const sdkRecallHandler = buildSdkRecallHandler(
|
|
1437
|
+
sulcusMem as SulcusCloudClient,
|
|
1438
|
+
namespace,
|
|
1439
|
+
maxRecallResults,
|
|
1440
|
+
profileFrequency,
|
|
1441
|
+
logger
|
|
1442
|
+
);
|
|
1443
|
+
const apiOn = api.on as (event: string, handler: unknown) => void;
|
|
1444
|
+
apiOn("before_agent_start", async (event: Record<string, unknown>, ctx: unknown) => {
|
|
1445
|
+
try {
|
|
1446
|
+
return await sdkRecallHandler(event, ctx);
|
|
1447
|
+
} catch (err) {
|
|
1448
|
+
logger.warn("sulcus: SDK recall hook threw: " + err);
|
|
1449
|
+
return undefined;
|
|
1450
|
+
}
|
|
1451
|
+
});
|
|
1452
|
+
logger.info("sulcus: registered SDK auto-recall (prependContext path)");
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
// 6. auto-capture on agent_end
|
|
1456
|
+
if (autoCapture) {
|
|
1457
|
+
const agentEndCaptureConfig: HookConfig = {
|
|
1458
|
+
action: "sivu_auto_capture",
|
|
1459
|
+
enabled: true,
|
|
1460
|
+
min_store_confidence: 0.5,
|
|
1461
|
+
fallback_on_error: true,
|
|
1462
|
+
};
|
|
1463
|
+
const apiOn = api.on as (event: string, handler: unknown) => void;
|
|
1464
|
+
apiOn("agent_end", async (event: Record<string, unknown>, _ctx: unknown) => {
|
|
1465
|
+
try {
|
|
1466
|
+
return await hookHandlers.sivu_auto_capture(event, agentEndCaptureConfig, handlerCtx);
|
|
1467
|
+
} catch (err) {
|
|
1468
|
+
logger.warn("sulcus: auto-capture hook threw: " + err);
|
|
1469
|
+
return undefined;
|
|
1470
|
+
}
|
|
1471
|
+
});
|
|
1472
|
+
logger.info("sulcus: registered auto-capture (agent_end)");
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1476
|
+
// LEGACY HOOK REGISTRATION (config-driven, backward compat)
|
|
1477
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1478
|
+
|
|
1174
1479
|
for (const [hookName, hookConfig] of Object.entries(hooksConfig.hooks)) {
|
|
1175
1480
|
if (!hookConfig.enabled) continue;
|
|
1481
|
+
|
|
1482
|
+
// Skip before_agent_start if we already registered the SDK path
|
|
1483
|
+
if (hookName === "before_agent_start" && autoRecall && isCloudBackend) continue;
|
|
1484
|
+
// Skip agent_end if autoCapture SDK path already registered
|
|
1485
|
+
if (hookName === "agent_end" && autoCapture && hookConfig.action === "sivu_auto_capture") continue;
|
|
1486
|
+
|
|
1176
1487
|
const handler = hookHandlers[hookConfig.action];
|
|
1177
1488
|
if (handler) {
|
|
1178
|
-
api.on
|
|
1489
|
+
const apiOn = api.on as (event: string, handler: unknown) => void;
|
|
1490
|
+
apiOn(hookName, async (event: Record<string, unknown>) => {
|
|
1179
1491
|
try {
|
|
1180
1492
|
return await handler(event, hookConfig, handlerCtx);
|
|
1181
1493
|
} catch (err) {
|
|
1182
|
-
|
|
1494
|
+
logger.warn("sulcus: hook " + hookName + " (action=" + hookConfig.action + ") threw: " + err);
|
|
1183
1495
|
return undefined;
|
|
1184
1496
|
}
|
|
1185
1497
|
});
|
|
1186
1498
|
} else {
|
|
1187
|
-
|
|
1499
|
+
logger.warn("sulcus: unknown hook action " + hookConfig.action + " for hook " + hookName);
|
|
1188
1500
|
}
|
|
1189
1501
|
}
|
|
1190
1502
|
|
|
1191
|
-
//
|
|
1503
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1504
|
+
// TOOL REGISTRATION
|
|
1505
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1506
|
+
|
|
1192
1507
|
for (const [toolName, toolConfig] of Object.entries(hooksConfig.tools)) {
|
|
1193
1508
|
if (!toolConfig.enabled) continue;
|
|
1194
1509
|
const toolDef = toolDefinitions[toolName];
|
|
1195
1510
|
if (toolDef) {
|
|
1196
1511
|
const schema = {
|
|
1197
1512
|
...toolDef.schema,
|
|
1198
|
-
async execute(id: string, params:
|
|
1513
|
+
async execute(id: string, params: Record<string, unknown>) {
|
|
1199
1514
|
return toolDef.makeExecute(toolDeps)(id, params);
|
|
1200
1515
|
},
|
|
1201
1516
|
};
|
|
1202
|
-
api.registerTool(schema,
|
|
1517
|
+
const registerTool = api.registerTool as (schema: unknown, opts: unknown) => void;
|
|
1518
|
+
registerTool(schema, toolDef.options);
|
|
1203
1519
|
} else {
|
|
1204
|
-
|
|
1520
|
+
logger.warn("sulcus: unknown tool " + toolName + " in config — skipping");
|
|
1205
1521
|
}
|
|
1206
1522
|
}
|
|
1207
1523
|
|
|
1208
|
-
//
|
|
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
|
+
}
|
|
1209
1530
|
}
|
|
1210
1531
|
};
|
|
1211
1532
|
|