clawmem 0.9.0 → 0.10.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 +7 -3
- package/CLAUDE.md +7 -3
- package/README.md +23 -16
- package/SKILL.md +13 -5
- package/package.json +2 -2
- package/src/clawmem.ts +150 -23
- package/src/openclaw/compaction-threshold.ts +166 -0
- package/src/openclaw/engine.ts +520 -241
- package/src/openclaw/index.ts +151 -140
- package/src/openclaw/openclaw.plugin.json +4 -1
- package/src/openclaw/package.json +9 -0
- package/src/openclaw/session-state.ts +55 -0
- package/src/openclaw/transcript-resolver.ts +441 -0
package/src/openclaw/index.ts
CHANGED
|
@@ -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
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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:2226-2249`. Precompact-extract MUST run inside
|
|
41
|
+
* `handleBeforePromptBuild` (which IS awaited at `attempt.ts:1661`), 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 {
|
|
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
|
-
//
|
|
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:
|
|
66
|
-
|
|
67
|
-
|
|
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.0",
|
|
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
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
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:1661 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:2226-2249 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
|
-
|
|
144
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
167
|
-
|
|
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:
|
|
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})`);
|
|
@@ -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
|
+
}
|