clawmem 0.9.0 → 0.10.1

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.
@@ -1,56 +1,81 @@
1
1
  /**
2
- * ClawMem OpenClaw Plugin — Entry Point
2
+ * ClawMem OpenClaw Plugin — Entry Point (§14.3 pure-memory migration)
3
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
4
+ * Registers ClawMem as an OpenClaw `kind: "memory"` plugin. The previous
5
+ * `kind: "context-engine"` registration is gone ClawMem no longer
6
+ * implements the `ContextEngine` interface and OpenClaw's built-in
7
+ * `LegacyContextEngine` (or any third-party LCM plugin the user installs)
8
+ * now owns compaction.
9
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 runtime
14
- * - assemble() is minimal pass-through (retrieval already injected via hook)
10
+ * What ClawMem provides under the memory slot:
11
+ * 1. MemoryPluginCapability via `api.registerMemoryCapability()` gives
12
+ * OpenClaw a `MemoryPluginRuntime.resolveMemoryBackendConfig()` so the
13
+ * runtime knows ClawMem owns memory for this agent. The
14
+ * `getMemorySearchManager` slot is a stub (ClawMem retrieval flows
15
+ * through the existing hook + REST API path, not OpenClaw's memory
16
+ * search manager).
17
+ *
18
+ * 2. PluginHookName event subscriptions via `api.on()` for the lifecycle
19
+ * work that the deleted ContextEngine class used to handle:
20
+ *
21
+ * before_prompt_build — context surfacing + pre-emptive precompact
22
+ * (the only awaited correctness path for
23
+ * precompact-extract on the per-turn lifecycle)
24
+ * agent_end — decision-extractor, handoff-generator,
25
+ * feedback-loop (eventually-consistent vault
26
+ * writes; fire-and-forget context is OK)
27
+ * before_compaction — fire-and-forget precompact-extract fallback
28
+ * (defense-in-depth only, never load-bearing)
29
+ * session_start — session-bootstrap hook + cache result for
30
+ * first-turn before_prompt_build consumption
31
+ * session_end — clearSessionState
32
+ * before_reset — extraction one last time + clearSessionState
33
+ *
34
+ * 3. Agent tools registration (clawmem_search, clawmem_get, etc.) when
35
+ * enableTools is set in plugin config — unchanged from prior version.
36
+ *
37
+ * 4. REST API service (`clawmem serve`) lifecycle — unchanged.
38
+ *
39
+ * §14.3 critical correctness contract: `agent_end` is fire-and-forget at
40
+ * `attempt.ts:2198-2224`. Precompact-extract MUST run inside
41
+ * `handleBeforePromptBuild` (which IS awaited at `attempt.ts:1642`), gated
42
+ * by the proximity heuristic in `compaction-threshold.ts`. See `engine.ts`
43
+ * top-of-file comment for the full rationale.
15
44
  */
16
45
 
17
- import { ClawMemContextEngine } from "./engine.js";
18
- import { resolveClawMemBin, execHook, parseHookOutput, extractContext } from "./shell.js";
46
+ import { resolveClawMemBin } from "./shell.js";
19
47
  import type { ClawMemConfig } from "./shell.js";
20
48
  import { createTools } from "./tools.js";
49
+ import {
50
+ handleAgentEnd,
51
+ handleBeforeCompaction,
52
+ handleBeforePromptBuild,
53
+ handleBeforeReset,
54
+ handleSessionEnd,
55
+ handleSessionStart,
56
+ type AgentEndContext,
57
+ type AgentEndEvent,
58
+ type BeforeCompactionContext,
59
+ type BeforeCompactionEvent,
60
+ type BeforePromptBuildContext,
61
+ type BeforePromptBuildEvent,
62
+ type BeforeResetContext,
63
+ type BeforeResetEvent,
64
+ type Logger,
65
+ type SessionEndEvent,
66
+ type SessionStartContext,
67
+ type SessionStartEvent,
68
+ } from "./engine.js";
69
+ import {
70
+ DEFAULT_CONTEXT_WINDOW_TOKENS,
71
+ DEFAULT_RESERVE_TOKENS_FLOOR,
72
+ DEFAULT_SOFT_THRESHOLD_TOKENS,
73
+ PRECOMPACT_PROXIMITY_RATIO_DEFAULT,
74
+ type CompactionThresholdConfig,
75
+ } from "./compaction-threshold.js";
21
76
 
22
77
  // =============================================================================
23
- // Prompt Cleaning (strips OpenClaw noise for better retrieval)
24
- // Pattern extracted from memory-core-plus (MIT, aloong-planet)
25
- // =============================================================================
26
-
27
- /**
28
- * Strip OpenClaw-specific noise from the user prompt before using it as a
29
- * search query. Gateway prompts contain metadata, system events, timestamps,
30
- * and previously injected context that degrade embedding/BM25 quality.
31
- */
32
- function cleanPromptForSearch(prompt: string): string {
33
- let cleaned = prompt;
34
- // Strip previously injected vault-context (avoid re-searching our own output)
35
- cleaned = cleaned.replace(/<vault-context>[\s\S]*?<\/vault-context>/g, "");
36
- cleaned = cleaned.replace(/<vault-routing>[\s\S]*?<\/vault-routing>/g, "");
37
- cleaned = cleaned.replace(/<vault-session>[\s\S]*?<\/vault-session>/g, "");
38
- // Strip OpenClaw sender metadata block
39
- cleaned = cleaned.replace(/Sender\s*\(untrusted metadata\)\s*:\s*```json\n[\s\S]*?```/g, "");
40
- cleaned = cleaned.replace(/Sender\s*\(untrusted metadata\)\s*:\s*\{[\s\S]*?\}\s*/g, "");
41
- // Strip OpenClaw runtime context blocks
42
- cleaned = cleaned.replace(/OpenClaw runtime context \(internal\):[\s\S]*?(?=\n\n|\n?$)/g, "");
43
- // Strip "System: ..." single-line event entries
44
- cleaned = cleaned.replace(/^System:.*$/gm, "");
45
- // Strip timestamp prefixes e.g. "[Sat 2026-03-14 16:19 GMT+8] "
46
- cleaned = cleaned.replace(/^\[.*?GMT[+-]\d+\]\s*/gm, "");
47
- // Collapse excessive whitespace
48
- cleaned = cleaned.replace(/\n{3,}/g, "\n\n").trim();
49
- return cleaned || prompt;
50
- }
51
-
52
- // =============================================================================
53
- // Plugin Definition
78
+ // Plugin definition
54
79
  // =============================================================================
55
80
 
56
81
  const PROFILE_BUDGETS: Record<string, number> = {
@@ -62,9 +87,10 @@ const PROFILE_BUDGETS: Record<string, number> = {
62
87
  const clawmemPlugin = {
63
88
  id: "clawmem",
64
89
  name: "ClawMem",
65
- description: "On-device hybrid memory system with composite scoring, graph traversal, and lifecycle management",
66
- version: "0.2.0",
67
- kind: "context-engine" as const,
90
+ description:
91
+ "On-device hybrid memory layer for OpenClaw — composite scoring, graph traversal, lifecycle management, and pre-emptive compaction state extraction",
92
+ version: "0.10.1",
93
+ kind: "memory" as const,
68
94
 
69
95
  register(api: any) {
70
96
  // ----- Resolve config -----
@@ -86,110 +112,95 @@ const clawmemPlugin = {
86
112
  },
87
113
  };
88
114
 
89
- const logger = api.logger;
90
- logger.info(`clawmem: plugin registered (bin: ${cfg.clawmemBin}, profile: ${profile}, budget: ${tokenBudget})`);
91
-
92
- // ----- Register ContextEngine -----
93
- const engine = new ClawMemContextEngine(cfg, logger);
94
- api.registerContextEngine("clawmem", () => engine);
95
-
96
- // ----- Track first-turn per session -----
97
- const surfacedSessions = new Set<string>();
98
-
99
- // ----- Plugin Hook: before_prompt_build -----
100
- // This is WHERE retrieval happens (P1 finding: assemble() lacks the user prompt)
101
- api.on("before_prompt_build", async (
102
- event: { prompt: string; messages?: unknown[] },
103
- ctx: { sessionId?: string; sessionKey?: string }
104
- ) => {
105
- if (!event.prompt || event.prompt.length < 5) return;
106
-
107
- const sessionId = ctx.sessionId || "unknown";
108
- const isFirstTurn = !surfacedSessions.has(sessionId);
109
-
110
- let context = "";
111
-
112
- // On first turn: consume cached bootstrap context from engine.bootstrap()
113
- // (avoids duplicate session-bootstrap hook invocation)
114
- if (isFirstTurn) {
115
- surfacedSessions.add(sessionId);
116
-
117
- const bootstrapContext = engine.takeBootstrapContext(sessionId);
118
- if (bootstrapContext) {
119
- context += bootstrapContext + "\n\n";
120
- }
121
- }
115
+ const thresholdCfg: CompactionThresholdConfig = {
116
+ contextWindowTokens:
117
+ (pluginCfg.compactionContextWindow as number | undefined) ?? DEFAULT_CONTEXT_WINDOW_TOKENS,
118
+ precompactProximityRatio:
119
+ (pluginCfg.precompactProximityRatio as number | undefined) ??
120
+ PRECOMPACT_PROXIMITY_RATIO_DEFAULT,
121
+ softThresholdTokens:
122
+ (pluginCfg.softThresholdTokens as number | undefined) ?? DEFAULT_SOFT_THRESHOLD_TOKENS,
123
+ reserveTokensFloor:
124
+ (pluginCfg.reserveTokensFloor as number | undefined) ?? DEFAULT_RESERVE_TOKENS_FLOOR,
125
+ };
122
126
 
123
- // Every turn: run context-surfacing for prompt-aware retrieval
124
- // Clean the prompt to remove OpenClaw noise before search
125
- const searchPrompt = cleanPromptForSearch(event.prompt);
126
- const surfacingResult = await execHook(cfg, "context-surfacing", {
127
- session_id: sessionId,
128
- prompt: searchPrompt,
129
- });
130
-
131
- if (surfacingResult.exitCode === 0) {
132
- const parsed = parseHookOutput(surfacingResult.stdout);
133
- const surfacedContext = extractContext(parsed);
134
- if (surfacedContext) {
135
- context += surfacedContext;
136
- }
137
- } else {
138
- logger.warn(`clawmem: context-surfacing failed: ${surfacingResult.stderr}`);
139
- }
127
+ const logger = api.logger as Logger;
128
+ logger.info(
129
+ `clawmem: plugin registered (kind=memory, bin=${cfg.clawmemBin}, profile=${profile}, budget=${tokenBudget})`,
130
+ );
131
+
132
+ // ----- Register memory capability -----
133
+ // ClawMem owns the memory slot for this agent. The runtime stub returns
134
+ // null for getMemorySearchManager because ClawMem retrieval flows through
135
+ // the before_prompt_build hook + REST API path, not OpenClaw's memory
136
+ // search manager interface. resolveMemoryBackendConfig signals "builtin"
137
+ // so OpenClaw's auto-reply memory-flush path treats this agent as
138
+ // memory-managed.
139
+ api.registerMemoryCapability({
140
+ runtime: {
141
+ async getMemorySearchManager(_params: {
142
+ cfg: unknown;
143
+ agentId: string;
144
+ purpose?: "default" | "status";
145
+ }) {
146
+ return { manager: null };
147
+ },
148
+ resolveMemoryBackendConfig(_params: { cfg: unknown; agentId: string }) {
149
+ return { backend: "builtin" as const };
150
+ },
151
+ },
152
+ });
140
153
 
141
- if (!context.trim()) return;
154
+ // ----- Plugin Hook: before_prompt_build (AWAITED — load-bearing path) -----
155
+ // Both context-surfacing retrieval injection and pre-emptive precompact
156
+ // extraction live here. handleBeforePromptBuild is async and the OpenClaw
157
+ // attempt path awaits the result at attempt.ts:1642 before building the
158
+ // effective prompt. precompact-extract therefore runs strictly before
159
+ // the LLM call that could trigger compaction on this turn.
160
+ api.on(
161
+ "before_prompt_build",
162
+ async (event: BeforePromptBuildEvent, ctx: BeforePromptBuildContext) => {
163
+ return handleBeforePromptBuild(cfg, thresholdCfg, logger, event, ctx);
164
+ },
165
+ { priority: 10 },
166
+ );
167
+
168
+ // ----- Plugin Hook: agent_end (FIRE-AND-FORGET in core) -----
169
+ // Decision-extractor, handoff-generator, and feedback-loop run here.
170
+ // These writes are eventually-consistent (saveMemory dedupes), so the
171
+ // fire-and-forget context at attempt.ts:2198-2224 is acceptable.
172
+ // precompact-extract is intentionally NOT in this handler — it lives
173
+ // in handleBeforePromptBuild for correctness reasons.
174
+ api.on("agent_end", async (event: AgentEndEvent, ctx: AgentEndContext) => {
175
+ await handleAgentEnd(cfg, logger, event, ctx);
176
+ });
142
177
 
143
- return { prependContext: context.trim() };
144
- }, { priority: 10 }); // Run early to prepend context before other hooks
178
+ // ----- Plugin Hook: before_compaction (FIRE-AND-FORGET fallback only) -----
179
+ // Defense-in-depth only the load-bearing precompact path is in
180
+ // before_prompt_build above. This handler races with compaction itself
181
+ // and offers no correctness guarantee on its own.
182
+ api.on(
183
+ "before_compaction",
184
+ async (event: BeforeCompactionEvent, ctx: BeforeCompactionContext) => {
185
+ await handleBeforeCompaction(cfg, thresholdCfg, logger, event, ctx);
186
+ },
187
+ );
145
188
 
146
189
  // ----- Plugin Hook: session_start -----
147
- api.on("session_start", async (
148
- event: { sessionId: string; sessionKey?: string },
149
- _ctx: unknown
150
- ) => {
151
- logger.info?.(`clawmem: session started ${event.sessionId}`);
190
+ api.on("session_start", async (event: SessionStartEvent, ctx: SessionStartContext) => {
191
+ await handleSessionStart(cfg, logger, event, ctx);
152
192
  });
153
193
 
154
194
  // ----- Plugin Hook: session_end -----
155
- api.on("session_end", async (
156
- event: { sessionId: string; sessionKey?: string; messageCount: number },
157
- _ctx: unknown
158
- ) => {
159
- // Cleanup tracked state
160
- surfacedSessions.delete(event.sessionId);
161
- engine.clearSession(event.sessionId);
162
- logger.info?.(`clawmem: session ended ${event.sessionId} (${event.messageCount} messages)`);
195
+ api.on("session_end", async (event: SessionEndEvent, _ctx: unknown) => {
196
+ handleSessionEnd(logger, event);
163
197
  });
164
198
 
165
199
  // ----- Plugin Hook: before_reset -----
166
- // Safety net for /new and /reset — ensure extraction runs before session clears
167
- api.on("before_reset", async (
168
- event: { sessionFile?: string; messages?: unknown[]; reason?: string },
169
- ctx: { sessionId?: string; sessionKey?: string }
170
- ) => {
171
- if (!event.sessionFile || !ctx.sessionId) return;
172
-
173
- // Run extraction hooks before reset clears the session
174
- const hookInput = {
175
- session_id: ctx.sessionId,
176
- transcript_path: event.sessionFile,
177
- };
178
-
179
- await Promise.allSettled([
180
- execHook(cfg, "decision-extractor", hookInput),
181
- execHook(cfg, "handoff-generator", hookInput),
182
- execHook(cfg, "feedback-loop", hookInput),
183
- ]);
184
-
185
- surfacedSessions.delete(ctx.sessionId);
186
- engine.clearSession(ctx.sessionId);
200
+ api.on("before_reset", async (event: BeforeResetEvent, ctx: BeforeResetContext) => {
201
+ await handleBeforeReset(cfg, logger, event, ctx);
187
202
  });
188
203
 
189
- // NOTE: before_compaction hook removed — precompact-extract now fires only
190
- // in engine.compact() before delegating to the runtime compactor. This avoids
191
- // the duplicate invocation that previously existed.
192
-
193
204
  // ----- Register Tools -----
194
205
  if (cfg.enableTools) {
195
206
  const tools = createTools(cfg, logger);
@@ -204,7 +215,7 @@ const clawmemPlugin = {
204
215
  return tool.execute(toolCallId, params);
205
216
  },
206
217
  },
207
- { name: tool.name }
218
+ { name: tool.name },
208
219
  );
209
220
  }
210
221
  logger.info(`clawmem: registered ${tools.length} agent tools`);
@@ -215,7 +226,7 @@ const clawmemPlugin = {
215
226
 
216
227
  api.registerService({
217
228
  id: "clawmem-api",
218
- async start(svcCtx: { logger: typeof logger }) {
229
+ async start(svcCtx: { logger: Logger }) {
219
230
  const { spawnBackground } = await import("./shell.js");
220
231
  serveChild = spawnBackground(cfg, ["serve", "--port", String(cfg.servePort)], svcCtx.logger);
221
232
  svcCtx.logger.info(`clawmem: REST API spawned (pid=${serveChild.pid})`);
@@ -1,6 +1,9 @@
1
1
  {
2
2
  "id": "clawmem",
3
- "kind": "context-engine",
3
+ "kind": "memory",
4
+ "activation": {
5
+ "onCapabilities": ["hook", "tool"]
6
+ },
4
7
  "uiHints": {
5
8
  "clawmemBin": {
6
9
  "label": "ClawMem Binary Path",
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "clawmem-openclaw-plugin",
3
+ "version": "0.10.1",
4
+ "description": "OpenClaw plugin adapter for ClawMem — on-device hybrid memory layer",
5
+ "type": "module",
6
+ "openclaw": {
7
+ "extensions": ["./index.ts"]
8
+ }
9
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * ClawMem OpenClaw Plugin — Session-scoped state
3
+ *
4
+ * Module-level state previously owned by the deleted `ClawMemContextEngine`
5
+ * instance (engine.ts). After §14.3 migration to `kind: "memory"`, the engine
6
+ * class is gone but per-session bookkeeping is still needed for two flows:
7
+ *
8
+ * - bootstrapContexts: cached `session-bootstrap` hook output keyed by
9
+ * sessionId. Written from `session_start`, consumed once on the first
10
+ * `before_prompt_build` for that session, then cleared.
11
+ *
12
+ * - surfacedSessions: tracks which sessions have already received their
13
+ * first-turn bootstrap injection. Keyed by sessionId.
14
+ *
15
+ * Cleared together via `clearSessionState(sessionId)` from `session_end` and
16
+ * `before_reset` event handlers.
17
+ */
18
+
19
+ const bootstrapContexts = new Map<string, string>();
20
+ const surfacedSessions = new Set<string>();
21
+
22
+ export function setBootstrapContext(sessionId: string, ctx: string): void {
23
+ bootstrapContexts.set(sessionId, ctx);
24
+ }
25
+
26
+ /**
27
+ * One-shot read: returns the cached bootstrap context and removes it from
28
+ * the cache. Subsequent calls for the same sessionId return undefined.
29
+ */
30
+ export function takeBootstrapContext(sessionId: string): string | undefined {
31
+ const ctx = bootstrapContexts.get(sessionId);
32
+ if (ctx !== undefined) bootstrapContexts.delete(sessionId);
33
+ return ctx;
34
+ }
35
+
36
+ export function markSessionSurfaced(sessionId: string): void {
37
+ surfacedSessions.add(sessionId);
38
+ }
39
+
40
+ export function isSessionSurfaced(sessionId: string): boolean {
41
+ return surfacedSessions.has(sessionId);
42
+ }
43
+
44
+ export function clearSessionState(sessionId: string): void {
45
+ bootstrapContexts.delete(sessionId);
46
+ surfacedSessions.delete(sessionId);
47
+ }
48
+
49
+ /**
50
+ * Test-only helper: wipes all module state. Production code MUST NOT call this.
51
+ */
52
+ export function _resetAllSessionStateForTests(): void {
53
+ bootstrapContexts.clear();
54
+ surfacedSessions.clear();
55
+ }