clawmem 0.1.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/AGENTS.md +660 -0
- package/CLAUDE.md +660 -0
- package/LICENSE +21 -0
- package/README.md +993 -0
- package/SKILL.md +717 -0
- package/bin/clawmem +75 -0
- package/package.json +72 -0
- package/src/amem.ts +797 -0
- package/src/beads.ts +263 -0
- package/src/clawmem.ts +1849 -0
- package/src/collections.ts +405 -0
- package/src/config.ts +178 -0
- package/src/consolidation.ts +123 -0
- package/src/directory-context.ts +248 -0
- package/src/errors.ts +41 -0
- package/src/formatter.ts +427 -0
- package/src/graph-traversal.ts +247 -0
- package/src/hooks/context-surfacing.ts +317 -0
- package/src/hooks/curator-nudge.ts +89 -0
- package/src/hooks/decision-extractor.ts +639 -0
- package/src/hooks/feedback-loop.ts +214 -0
- package/src/hooks/handoff-generator.ts +345 -0
- package/src/hooks/postcompact-inject.ts +226 -0
- package/src/hooks/precompact-extract.ts +314 -0
- package/src/hooks/pretool-inject.ts +79 -0
- package/src/hooks/session-bootstrap.ts +324 -0
- package/src/hooks/staleness-check.ts +130 -0
- package/src/hooks.ts +367 -0
- package/src/indexer.ts +327 -0
- package/src/intent.ts +294 -0
- package/src/limits.ts +26 -0
- package/src/llm.ts +1175 -0
- package/src/mcp.ts +2138 -0
- package/src/memory.ts +336 -0
- package/src/mmr.ts +93 -0
- package/src/observer.ts +269 -0
- package/src/openclaw/engine.ts +283 -0
- package/src/openclaw/index.ts +221 -0
- package/src/openclaw/plugin.json +83 -0
- package/src/openclaw/shell.ts +207 -0
- package/src/openclaw/tools.ts +304 -0
- package/src/profile.ts +346 -0
- package/src/promptguard.ts +218 -0
- package/src/retrieval-gate.ts +106 -0
- package/src/search-utils.ts +127 -0
- package/src/server.ts +783 -0
- package/src/splitter.ts +325 -0
- package/src/store.ts +4062 -0
- package/src/validation.ts +67 -0
- package/src/watcher.ts +58 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClawMem ContextEngine — OpenClaw integration
|
|
3
|
+
*
|
|
4
|
+
* Implements the ContextEngine interface for OpenClaw's plugin system.
|
|
5
|
+
* Phase 1: thin shim that shells out to `clawmem hook <name>` for lifecycle operations.
|
|
6
|
+
*
|
|
7
|
+
* Architecture (per GPT 5.4 High review):
|
|
8
|
+
* - assemble(): minimal pass-through (real retrieval happens in before_prompt_build hook)
|
|
9
|
+
* - afterTurn(): shells out to decision-extractor, handoff-generator, feedback-loop
|
|
10
|
+
* - compact(): shells out to precompact-extract, then delegates to legacy compaction (ownsCompaction: false)
|
|
11
|
+
* - ingest(): no-op (ClawMem captures at turn boundaries, not per-message)
|
|
12
|
+
* - bootstrap(): session registration only (context injection via before_prompt_build hook)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { ClawMemConfig } from "./shell.js";
|
|
16
|
+
import { execHook, parseHookOutput } from "./shell.js";
|
|
17
|
+
|
|
18
|
+
// =============================================================================
|
|
19
|
+
// Types (matching OpenClaw's ContextEngine interface without importing it)
|
|
20
|
+
// =============================================================================
|
|
21
|
+
|
|
22
|
+
type ContextEngineInfo = {
|
|
23
|
+
id: string;
|
|
24
|
+
name: string;
|
|
25
|
+
version?: string;
|
|
26
|
+
ownsCompaction?: boolean;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type AssembleResult = {
|
|
30
|
+
messages: unknown[];
|
|
31
|
+
estimatedTokens: number;
|
|
32
|
+
systemPromptAddition?: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type CompactResult = {
|
|
36
|
+
ok: boolean;
|
|
37
|
+
compacted: boolean;
|
|
38
|
+
reason?: string;
|
|
39
|
+
result?: {
|
|
40
|
+
summary?: string;
|
|
41
|
+
firstKeptEntryId?: string;
|
|
42
|
+
tokensBefore: number;
|
|
43
|
+
tokensAfter?: number;
|
|
44
|
+
details?: unknown;
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
type IngestResult = { ingested: boolean };
|
|
49
|
+
type IngestBatchResult = { ingestedCount: number };
|
|
50
|
+
type BootstrapResult = {
|
|
51
|
+
bootstrapped: boolean;
|
|
52
|
+
importedMessages?: number;
|
|
53
|
+
reason?: string;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
type SubagentSpawnPreparation = { rollback: () => void | Promise<void> };
|
|
57
|
+
type SubagentEndReason = "deleted" | "completed" | "swept" | "released";
|
|
58
|
+
|
|
59
|
+
// =============================================================================
|
|
60
|
+
// Logger interface
|
|
61
|
+
// =============================================================================
|
|
62
|
+
|
|
63
|
+
type Logger = {
|
|
64
|
+
debug?: (msg: string) => void;
|
|
65
|
+
info: (msg: string) => void;
|
|
66
|
+
warn: (msg: string) => void;
|
|
67
|
+
error: (msg: string) => void;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// =============================================================================
|
|
71
|
+
// ClawMem ContextEngine
|
|
72
|
+
// =============================================================================
|
|
73
|
+
|
|
74
|
+
export class ClawMemContextEngine {
|
|
75
|
+
readonly info: ContextEngineInfo = {
|
|
76
|
+
id: "clawmem",
|
|
77
|
+
name: "ClawMem Memory System",
|
|
78
|
+
version: "0.2.0",
|
|
79
|
+
ownsCompaction: false, // Delegate compaction to legacy engine
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
private bootstrappedSessions = new Set<string>();
|
|
83
|
+
|
|
84
|
+
constructor(
|
|
85
|
+
private readonly cfg: ClawMemConfig,
|
|
86
|
+
private readonly logger: Logger,
|
|
87
|
+
) {}
|
|
88
|
+
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// bootstrap — register session, no context injection (P1 finding: bootstrap
|
|
91
|
+
// return type has no prompt injection field)
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
async bootstrap(params: {
|
|
95
|
+
sessionId: string;
|
|
96
|
+
sessionKey?: string;
|
|
97
|
+
sessionFile: string;
|
|
98
|
+
}): Promise<BootstrapResult> {
|
|
99
|
+
this.bootstrappedSessions.add(params.sessionId);
|
|
100
|
+
|
|
101
|
+
// Fire session-bootstrap hook for session registration + side effects
|
|
102
|
+
// (profile refresh, session_log entry). Context is NOT injected here —
|
|
103
|
+
// that happens in the before_prompt_build plugin hook.
|
|
104
|
+
const result = await execHook(this.cfg, "session-bootstrap", {
|
|
105
|
+
session_id: params.sessionId,
|
|
106
|
+
transcript_path: params.sessionFile,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
if (result.exitCode !== 0) {
|
|
110
|
+
this.logger.warn(`clawmem: bootstrap hook failed: ${result.stderr}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { bootstrapped: true };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// ingest — no-op (ClawMem captures at turn boundaries via afterTurn)
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
async ingest(_params: {
|
|
121
|
+
sessionId: string;
|
|
122
|
+
sessionKey?: string;
|
|
123
|
+
message: unknown;
|
|
124
|
+
isHeartbeat?: boolean;
|
|
125
|
+
}): Promise<IngestResult> {
|
|
126
|
+
return { ingested: false };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// ingestBatch — no-op
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
async ingestBatch?(_params: {
|
|
134
|
+
sessionId: string;
|
|
135
|
+
sessionKey?: string;
|
|
136
|
+
messages: unknown[];
|
|
137
|
+
isHeartbeat?: boolean;
|
|
138
|
+
}): Promise<IngestBatchResult> {
|
|
139
|
+
return { ingestedCount: 0 };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// assemble — minimal pass-through (P1 finding: assemble() lacks the user
|
|
144
|
+
// prompt, so retrieval must happen in before_prompt_build hook instead)
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
async assemble(params: {
|
|
148
|
+
sessionId: string;
|
|
149
|
+
sessionKey?: string;
|
|
150
|
+
messages: unknown[];
|
|
151
|
+
tokenBudget?: number;
|
|
152
|
+
}): Promise<AssembleResult> {
|
|
153
|
+
// Pass-through: retrieval already injected via before_prompt_build hook
|
|
154
|
+
return {
|
|
155
|
+
messages: params.messages,
|
|
156
|
+
estimatedTokens: 0, // Caller handles estimation
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
// afterTurn — run extraction hooks (decision-extractor, handoff-generator,
|
|
162
|
+
// feedback-loop) in parallel. These process the completed turn and persist
|
|
163
|
+
// decisions, handoff notes, and confidence boosts.
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
async afterTurn(params: {
|
|
167
|
+
sessionId: string;
|
|
168
|
+
sessionKey?: string;
|
|
169
|
+
sessionFile: string;
|
|
170
|
+
messages: unknown[];
|
|
171
|
+
prePromptMessageCount: number;
|
|
172
|
+
autoCompactionSummary?: string;
|
|
173
|
+
isHeartbeat?: boolean;
|
|
174
|
+
tokenBudget?: number;
|
|
175
|
+
}): Promise<void> {
|
|
176
|
+
// Skip extraction for heartbeat turns
|
|
177
|
+
if (params.isHeartbeat) return;
|
|
178
|
+
|
|
179
|
+
// Skip if too few messages (no meaningful content to extract)
|
|
180
|
+
const newMessages = params.messages.length - params.prePromptMessageCount;
|
|
181
|
+
if (newMessages < 2) return;
|
|
182
|
+
|
|
183
|
+
const hookInput = {
|
|
184
|
+
session_id: params.sessionId,
|
|
185
|
+
transcript_path: params.sessionFile,
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
// Fire all three Stop hooks in parallel (independent operations)
|
|
189
|
+
const [decisionResult, handoffResult, feedbackResult] = await Promise.allSettled([
|
|
190
|
+
execHook(this.cfg, "decision-extractor", hookInput),
|
|
191
|
+
execHook(this.cfg, "handoff-generator", hookInput),
|
|
192
|
+
execHook(this.cfg, "feedback-loop", hookInput),
|
|
193
|
+
]);
|
|
194
|
+
|
|
195
|
+
// Log failures but don't throw (fail-open)
|
|
196
|
+
for (const [name, result] of [
|
|
197
|
+
["decision-extractor", decisionResult],
|
|
198
|
+
["handoff-generator", handoffResult],
|
|
199
|
+
["feedback-loop", feedbackResult],
|
|
200
|
+
] as const) {
|
|
201
|
+
if (result.status === "rejected") {
|
|
202
|
+
this.logger.warn(`clawmem: afterTurn ${name} failed: ${result.reason}`);
|
|
203
|
+
} else if (result.value.exitCode !== 0) {
|
|
204
|
+
this.logger.warn(`clawmem: afterTurn ${name} error: ${result.value.stderr}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
// compact — run precompact-extract for state preservation, then return
|
|
211
|
+
// ownsCompaction: false so OpenClaw's legacy compaction still runs.
|
|
212
|
+
// P1 finding: compact() must still perform real compaction; precompact-extract
|
|
213
|
+
// is a side effect, not a compactor. We delegate real compaction to legacy.
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
async compact(params: {
|
|
217
|
+
sessionId: string;
|
|
218
|
+
sessionKey?: string;
|
|
219
|
+
sessionFile: string;
|
|
220
|
+
tokenBudget?: number;
|
|
221
|
+
force?: boolean;
|
|
222
|
+
currentTokenCount?: number;
|
|
223
|
+
compactionTarget?: "budget" | "threshold";
|
|
224
|
+
customInstructions?: string;
|
|
225
|
+
}): Promise<CompactResult> {
|
|
226
|
+
// Run precompact-extract to preserve state before compaction
|
|
227
|
+
const extractResult = await execHook(this.cfg, "precompact-extract", {
|
|
228
|
+
session_id: params.sessionId,
|
|
229
|
+
transcript_path: params.sessionFile,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
if (extractResult.exitCode !== 0) {
|
|
233
|
+
this.logger.warn(`clawmem: precompact-extract failed: ${extractResult.stderr}`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Return not-compacted — let legacy engine handle actual compaction
|
|
237
|
+
// (ownsCompaction: false means OpenClaw will call legacy compact() after us)
|
|
238
|
+
return {
|
|
239
|
+
ok: true,
|
|
240
|
+
compacted: false,
|
|
241
|
+
reason: "clawmem-precompact-only",
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
// prepareSubagentSpawn — placeholder for future per-subagent memory scoping
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
|
|
249
|
+
async prepareSubagentSpawn?(_params: {
|
|
250
|
+
parentSessionKey: string;
|
|
251
|
+
childSessionKey: string;
|
|
252
|
+
ttlMs?: number;
|
|
253
|
+
}): Promise<SubagentSpawnPreparation | undefined> {
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ---------------------------------------------------------------------------
|
|
258
|
+
// onSubagentEnded — placeholder
|
|
259
|
+
// ---------------------------------------------------------------------------
|
|
260
|
+
|
|
261
|
+
async onSubagentEnded?(_params: {
|
|
262
|
+
childSessionKey: string;
|
|
263
|
+
reason: SubagentEndReason;
|
|
264
|
+
}): Promise<void> {
|
|
265
|
+
// No-op for now
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ---------------------------------------------------------------------------
|
|
269
|
+
// dispose — cleanup
|
|
270
|
+
// ---------------------------------------------------------------------------
|
|
271
|
+
|
|
272
|
+
async dispose(): Promise<void> {
|
|
273
|
+
this.bootstrappedSessions.clear();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ---------------------------------------------------------------------------
|
|
277
|
+
// Helper: check if session has been bootstrapped (used by prompt hook)
|
|
278
|
+
// ---------------------------------------------------------------------------
|
|
279
|
+
|
|
280
|
+
isBootstrapped(sessionId: string): boolean {
|
|
281
|
+
return this.bootstrappedSessions.has(sessionId);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClawMem OpenClaw Plugin — Entry Point
|
|
3
|
+
*
|
|
4
|
+
* Registers ClawMem as an OpenClaw ContextEngine plugin with:
|
|
5
|
+
* 1. ContextEngine (engine.ts) — lifecycle: afterTurn, compact, bootstrap
|
|
6
|
+
* 2. Plugin hooks — before_prompt_build (retrieval), session_start/end, before_reset
|
|
7
|
+
* 3. Agent tools — clawmem_search, clawmem_get, clawmem_session_log, clawmem_timeline, clawmem_similar
|
|
8
|
+
* 4. Service — starts clawmem serve (REST API) for tool HTTP calls
|
|
9
|
+
*
|
|
10
|
+
* Architecture per GPT 5.4 High review:
|
|
11
|
+
* - Prompt-aware retrieval goes through before_prompt_build (has the user prompt)
|
|
12
|
+
* - Post-turn extraction goes through ContextEngine.afterTurn() (has messages[])
|
|
13
|
+
* - Compaction goes through ContextEngine.compact() → precompact-extract → delegate to legacy
|
|
14
|
+
* - assemble() is minimal pass-through (retrieval already injected via hook)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { ClawMemContextEngine } from "./engine.js";
|
|
18
|
+
import { resolveClawMemBin, execHook, parseHookOutput, extractContext } from "./shell.js";
|
|
19
|
+
import type { ClawMemConfig } from "./shell.js";
|
|
20
|
+
import { createTools } from "./tools.js";
|
|
21
|
+
|
|
22
|
+
// =============================================================================
|
|
23
|
+
// Plugin Definition
|
|
24
|
+
// =============================================================================
|
|
25
|
+
|
|
26
|
+
const PROFILE_BUDGETS: Record<string, number> = {
|
|
27
|
+
speed: 400,
|
|
28
|
+
balanced: 800,
|
|
29
|
+
deep: 1200,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const clawmemPlugin = {
|
|
33
|
+
id: "clawmem",
|
|
34
|
+
name: "ClawMem",
|
|
35
|
+
description: "On-device hybrid memory system with composite scoring, graph traversal, and lifecycle management",
|
|
36
|
+
version: "0.2.0",
|
|
37
|
+
kind: "context-engine" as const,
|
|
38
|
+
|
|
39
|
+
register(api: any) {
|
|
40
|
+
// ----- Resolve config -----
|
|
41
|
+
const pluginCfg = (api.pluginConfig || {}) as Record<string, unknown>;
|
|
42
|
+
const profile = (pluginCfg.profile as string) || "balanced";
|
|
43
|
+
const tokenBudget = (pluginCfg.tokenBudget as number) || PROFILE_BUDGETS[profile] || 800;
|
|
44
|
+
|
|
45
|
+
const cfg: ClawMemConfig = {
|
|
46
|
+
clawmemBin: resolveClawMemBin(pluginCfg.clawmemBin as string | undefined),
|
|
47
|
+
tokenBudget,
|
|
48
|
+
profile,
|
|
49
|
+
enableTools: pluginCfg.enableTools !== false,
|
|
50
|
+
servePort: (pluginCfg.servePort as number) || 7438,
|
|
51
|
+
env: {
|
|
52
|
+
...(pluginCfg.gpuEmbed ? { CLAWMEM_EMBED_URL: pluginCfg.gpuEmbed as string } : {}),
|
|
53
|
+
...(pluginCfg.gpuLlm ? { CLAWMEM_LLM_URL: pluginCfg.gpuLlm as string } : {}),
|
|
54
|
+
...(pluginCfg.gpuRerank ? { CLAWMEM_RERANK_URL: pluginCfg.gpuRerank as string } : {}),
|
|
55
|
+
CLAWMEM_PROFILE: profile,
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const logger = api.logger;
|
|
60
|
+
logger.info(`clawmem: plugin registered (bin: ${cfg.clawmemBin}, profile: ${profile}, budget: ${tokenBudget})`);
|
|
61
|
+
|
|
62
|
+
// ----- Register ContextEngine -----
|
|
63
|
+
const engine = new ClawMemContextEngine(cfg, logger);
|
|
64
|
+
api.registerContextEngine("clawmem", () => engine);
|
|
65
|
+
|
|
66
|
+
// ----- Track first-turn per session -----
|
|
67
|
+
const surfacedSessions = new Set<string>();
|
|
68
|
+
|
|
69
|
+
// ----- Plugin Hook: before_prompt_build -----
|
|
70
|
+
// This is WHERE retrieval happens (P1 finding: assemble() lacks the user prompt)
|
|
71
|
+
api.on("before_prompt_build", async (
|
|
72
|
+
event: { prompt: string; messages?: unknown[] },
|
|
73
|
+
ctx: { sessionId?: string; sessionKey?: string }
|
|
74
|
+
) => {
|
|
75
|
+
if (!event.prompt || event.prompt.length < 5) return;
|
|
76
|
+
|
|
77
|
+
const sessionId = ctx.sessionId || "unknown";
|
|
78
|
+
const isFirstTurn = !surfacedSessions.has(sessionId);
|
|
79
|
+
|
|
80
|
+
let context = "";
|
|
81
|
+
|
|
82
|
+
// On first turn: run session-bootstrap for profile + handoff + decisions + stale
|
|
83
|
+
if (isFirstTurn) {
|
|
84
|
+
surfacedSessions.add(sessionId);
|
|
85
|
+
|
|
86
|
+
const bootstrapResult = await execHook(cfg, "session-bootstrap", {
|
|
87
|
+
session_id: sessionId,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (bootstrapResult.exitCode === 0) {
|
|
91
|
+
const parsed = parseHookOutput(bootstrapResult.stdout);
|
|
92
|
+
// Session-bootstrap outputs context directly to stdout (not via hookSpecificOutput)
|
|
93
|
+
// It uses the system_message field for SessionStart hooks
|
|
94
|
+
const bootstrapContext = (parsed?.systemMessage as string) || "";
|
|
95
|
+
if (bootstrapContext) {
|
|
96
|
+
context += bootstrapContext + "\n\n";
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
logger.warn(`clawmem: session-bootstrap failed: ${bootstrapResult.stderr}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Every turn: run context-surfacing for prompt-aware retrieval
|
|
104
|
+
const surfacingResult = await execHook(cfg, "context-surfacing", {
|
|
105
|
+
session_id: sessionId,
|
|
106
|
+
prompt: event.prompt,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
if (surfacingResult.exitCode === 0) {
|
|
110
|
+
const parsed = parseHookOutput(surfacingResult.stdout);
|
|
111
|
+
const surfacedContext = extractContext(parsed);
|
|
112
|
+
if (surfacedContext) {
|
|
113
|
+
context += surfacedContext;
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
logger.warn(`clawmem: context-surfacing failed: ${surfacingResult.stderr}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!context.trim()) return;
|
|
120
|
+
|
|
121
|
+
return { prependContext: context.trim() };
|
|
122
|
+
}, { priority: 10 }); // Run early to prepend context before other hooks
|
|
123
|
+
|
|
124
|
+
// ----- Plugin Hook: session_start -----
|
|
125
|
+
api.on("session_start", async (
|
|
126
|
+
event: { sessionId: string; sessionKey?: string },
|
|
127
|
+
_ctx: unknown
|
|
128
|
+
) => {
|
|
129
|
+
logger.info?.(`clawmem: session started ${event.sessionId}`);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// ----- Plugin Hook: session_end -----
|
|
133
|
+
api.on("session_end", async (
|
|
134
|
+
event: { sessionId: string; sessionKey?: string; messageCount: number },
|
|
135
|
+
_ctx: unknown
|
|
136
|
+
) => {
|
|
137
|
+
// Cleanup tracked state
|
|
138
|
+
surfacedSessions.delete(event.sessionId);
|
|
139
|
+
logger.info?.(`clawmem: session ended ${event.sessionId} (${event.messageCount} messages)`);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// ----- Plugin Hook: before_reset -----
|
|
143
|
+
// Safety net for /new and /reset — ensure extraction runs before session clears
|
|
144
|
+
api.on("before_reset", async (
|
|
145
|
+
event: { sessionFile?: string; messages?: unknown[]; reason?: string },
|
|
146
|
+
ctx: { sessionId?: string; sessionKey?: string }
|
|
147
|
+
) => {
|
|
148
|
+
if (!event.sessionFile || !ctx.sessionId) return;
|
|
149
|
+
|
|
150
|
+
// Run extraction hooks before reset clears the session
|
|
151
|
+
const hookInput = {
|
|
152
|
+
session_id: ctx.sessionId,
|
|
153
|
+
transcript_path: event.sessionFile,
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
await Promise.allSettled([
|
|
157
|
+
execHook(cfg, "decision-extractor", hookInput),
|
|
158
|
+
execHook(cfg, "handoff-generator", hookInput),
|
|
159
|
+
execHook(cfg, "feedback-loop", hookInput),
|
|
160
|
+
]);
|
|
161
|
+
|
|
162
|
+
surfacedSessions.delete(ctx.sessionId);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// ----- Plugin Hook: before_compaction -----
|
|
166
|
+
// Fire precompact-extract before compaction starts (additional safety — compact()
|
|
167
|
+
// also runs it, but before_compaction fires earlier in the pipeline)
|
|
168
|
+
api.on("before_compaction", async (
|
|
169
|
+
event: { sessionFile?: string; messageCount: number },
|
|
170
|
+
ctx: { sessionId?: string }
|
|
171
|
+
) => {
|
|
172
|
+
if (!event.sessionFile || !ctx.sessionId) return;
|
|
173
|
+
|
|
174
|
+
await execHook(cfg, "precompact-extract", {
|
|
175
|
+
session_id: ctx.sessionId,
|
|
176
|
+
transcript_path: event.sessionFile,
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// ----- Register Tools -----
|
|
181
|
+
if (cfg.enableTools) {
|
|
182
|
+
const tools = createTools(cfg, logger);
|
|
183
|
+
for (const tool of tools) {
|
|
184
|
+
api.registerTool(
|
|
185
|
+
{
|
|
186
|
+
name: tool.name,
|
|
187
|
+
label: tool.label,
|
|
188
|
+
description: tool.description,
|
|
189
|
+
parameters: tool.parameters,
|
|
190
|
+
async execute(toolCallId: string, params: Record<string, unknown>) {
|
|
191
|
+
return tool.execute(toolCallId, params);
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
{ name: tool.name }
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
logger.info(`clawmem: registered ${tools.length} agent tools`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ----- Register Service (REST API) -----
|
|
201
|
+
let serveChild: import("node:child_process").ChildProcess | null = null;
|
|
202
|
+
|
|
203
|
+
api.registerService({
|
|
204
|
+
id: "clawmem-api",
|
|
205
|
+
async start(svcCtx: { logger: typeof logger }) {
|
|
206
|
+
const { spawnBackground } = await import("./shell.js");
|
|
207
|
+
serveChild = spawnBackground(cfg, ["serve", "--port", String(cfg.servePort)], svcCtx.logger);
|
|
208
|
+
svcCtx.logger.info(`clawmem: REST API spawned (pid=${serveChild.pid})`);
|
|
209
|
+
},
|
|
210
|
+
stop() {
|
|
211
|
+
if (serveChild && !serveChild.killed) {
|
|
212
|
+
serveChild.kill("SIGTERM");
|
|
213
|
+
logger.info("clawmem: REST API service stopped");
|
|
214
|
+
}
|
|
215
|
+
serveChild = null;
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
export default clawmemPlugin;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "clawmem",
|
|
3
|
+
"kind": "context-engine",
|
|
4
|
+
"uiHints": {
|
|
5
|
+
"clawmemBin": {
|
|
6
|
+
"label": "ClawMem Binary Path",
|
|
7
|
+
"placeholder": "/usr/local/bin/clawmem",
|
|
8
|
+
"help": "Path to clawmem binary. Auto-detected from PATH if not set."
|
|
9
|
+
},
|
|
10
|
+
"tokenBudget": {
|
|
11
|
+
"label": "Context Token Budget",
|
|
12
|
+
"placeholder": "800",
|
|
13
|
+
"help": "Max tokens for context surfacing injection (default: 800)"
|
|
14
|
+
},
|
|
15
|
+
"profile": {
|
|
16
|
+
"label": "Retrieval Profile",
|
|
17
|
+
"help": "speed (400 tokens) | balanced (800) | deep (1200)"
|
|
18
|
+
},
|
|
19
|
+
"enableTools": {
|
|
20
|
+
"label": "Register Agent Tools",
|
|
21
|
+
"help": "Register ClawMem retrieval tools for agent-initiated queries"
|
|
22
|
+
},
|
|
23
|
+
"servePort": {
|
|
24
|
+
"label": "REST API Port",
|
|
25
|
+
"placeholder": "7438",
|
|
26
|
+
"help": "Port for ClawMem HTTP REST API (used by tools)",
|
|
27
|
+
"advanced": true
|
|
28
|
+
},
|
|
29
|
+
"gpuEmbed": {
|
|
30
|
+
"label": "Embedding Endpoint",
|
|
31
|
+
"placeholder": "http://localhost:8088",
|
|
32
|
+
"help": "URL for granite-embed embedding service",
|
|
33
|
+
"advanced": true
|
|
34
|
+
},
|
|
35
|
+
"gpuLlm": {
|
|
36
|
+
"label": "LLM Endpoint",
|
|
37
|
+
"placeholder": "http://localhost:8089",
|
|
38
|
+
"help": "URL for ClawMem LLM (query expansion, extraction)",
|
|
39
|
+
"advanced": true
|
|
40
|
+
},
|
|
41
|
+
"gpuRerank": {
|
|
42
|
+
"label": "Reranker Endpoint",
|
|
43
|
+
"placeholder": "http://localhost:8090",
|
|
44
|
+
"help": "URL for reranking service",
|
|
45
|
+
"advanced": true
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"configSchema": {
|
|
49
|
+
"type": "object",
|
|
50
|
+
"additionalProperties": false,
|
|
51
|
+
"properties": {
|
|
52
|
+
"clawmemBin": {
|
|
53
|
+
"type": "string"
|
|
54
|
+
},
|
|
55
|
+
"tokenBudget": {
|
|
56
|
+
"type": "number",
|
|
57
|
+
"minimum": 100,
|
|
58
|
+
"maximum": 4000
|
|
59
|
+
},
|
|
60
|
+
"profile": {
|
|
61
|
+
"type": "string",
|
|
62
|
+
"enum": ["speed", "balanced", "deep"]
|
|
63
|
+
},
|
|
64
|
+
"enableTools": {
|
|
65
|
+
"type": "boolean"
|
|
66
|
+
},
|
|
67
|
+
"servePort": {
|
|
68
|
+
"type": "number",
|
|
69
|
+
"minimum": 1024,
|
|
70
|
+
"maximum": 65535
|
|
71
|
+
},
|
|
72
|
+
"gpuEmbed": {
|
|
73
|
+
"type": "string"
|
|
74
|
+
},
|
|
75
|
+
"gpuLlm": {
|
|
76
|
+
"type": "string"
|
|
77
|
+
},
|
|
78
|
+
"gpuRerank": {
|
|
79
|
+
"type": "string"
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|