pi-agent-memory 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/LICENSE ADDED
@@ -0,0 +1,10 @@
1
+ This package is part of claude-mem (https://github.com/thedotmack/claude-mem)
2
+ and is licensed under the GNU Affero General Public License v3.0 (AGPL-3.0).
3
+
4
+ Copyright (C) 2025 Alex Newman (@thedotmack). All rights reserved.
5
+ Pi-agent adapter contributed by ArtemisAI.
6
+
7
+ See the full license text at:
8
+ https://github.com/thedotmack/claude-mem/blob/main/LICENSE
9
+
10
+ SPDX-License-Identifier: AGPL-3.0-or-later
package/README.md ADDED
@@ -0,0 +1,134 @@
1
+ # pi-agent-memory
2
+
3
+ Persistent memory extension for [pi-agents](https://github.com/badlogic/pi-mono) powered by [claude-mem](https://github.com/thedotmack/claude-mem).
4
+
5
+ Gives pi-coding-agent and any pi-mono-based runtime cross-session, cross-engine memory by connecting to claude-mem's worker service.
6
+
7
+ ## Installation
8
+
9
+ Requires the claude-mem worker running on `localhost:37777`. Install claude-mem first via `npx claude-mem install` or run the worker from source.
10
+
11
+ ### From npm (recommended)
12
+
13
+ ```bash
14
+ pi install npm:pi-agent-memory
15
+ ```
16
+
17
+ ### From git
18
+
19
+ ```bash
20
+ pi install git:github.com/thedotmack/claude-mem
21
+ ```
22
+
23
+ ### Manual
24
+
25
+ ```bash
26
+ cp extensions/pi-mem.ts ~/.pi/agent/extensions/pi-mem.ts
27
+ ```
28
+
29
+ ### Verify
30
+
31
+ ```bash
32
+ # Start pi — the extension auto-loads
33
+ pi
34
+
35
+ # Check connectivity
36
+ /memory-status
37
+ ```
38
+
39
+ ## What It Does
40
+
41
+ - **Captures observations** — every tool call your pi-agent makes is recorded to claude-mem's database
42
+ - **Injects context** — relevant past observations are automatically injected into the LLM context each turn
43
+ - **Memory search** — a `memory_recall` tool is registered for the LLM to explicitly search past work
44
+ - **Cross-engine sharing** — pi-agent observations live alongside Claude Code, Cursor, Codex, and OpenClaw memories in the same database
45
+
46
+ ## Architecture
47
+
48
+ ```text
49
+ Pi-Agent (pi-coding-agent / OpenClaw / custom)
50
+
51
+ ├── pi-mem extension (this package)
52
+ │ ├── session_start ──→ (local state init only)
53
+ │ ├── before_agent_start ──→ POST /api/sessions/init (with prompt)
54
+ │ ├── context ──→ GET /api/context/inject
55
+ │ ├── tool_result ──→ POST /api/sessions/observations
56
+ │ ├── agent_end ──→ POST /api/sessions/summarize
57
+ │ │ POST /api/sessions/complete
58
+ │ ├── session_compact ──→ (preserve session state)
59
+ │ └── session_shutdown ──→ (cleanup)
60
+
61
+ └── memory_recall tool ──→ GET /api/search
62
+
63
+
64
+ claude-mem worker (port 37777)
65
+ SQLite + FTS5 + Chroma
66
+ Shared across all engines
67
+ ```
68
+
69
+ ## Event Mapping
70
+
71
+ | Pi-Mono Event | Worker API | Purpose |
72
+ |---|---|---|
73
+ | `session_start` | — (local state only) | Derive project name, generate session ID |
74
+ | `before_agent_start` | `POST /api/sessions/init` | Capture user prompt for privacy filtering |
75
+ | `context` | `GET /api/context/inject` | Inject past observations into LLM context |
76
+ | `tool_result` | `POST /api/sessions/observations` | Record what tools did (fire-and-forget) |
77
+ | `agent_end` | `POST /api/sessions/summarize` + `complete` | AI-compress the session |
78
+ | `session_compact` | — | Preserve session ID across context compaction |
79
+ | `session_shutdown` | — | Clean up local state |
80
+
81
+ Derived from the OpenClaw plugin (`claude-mem/openclaw/src/index.ts`), which is a proven integration of claude-mem into a pi-mono-based runtime.
82
+
83
+ ## Configuration
84
+
85
+ | Variable | Default | Description |
86
+ |---|---|---|
87
+ | `CLAUDE_MEM_PORT` | `37777` | Worker service port |
88
+ | `CLAUDE_MEM_HOST` | `127.0.0.1` | Worker service host |
89
+ | `PI_MEM_PROJECT` | (derived from cwd) | Project name for scoping observations |
90
+ | `PI_MEM_DISABLED` | — | Set to `1` to disable the extension |
91
+
92
+ ## Cross-Engine Memory
93
+
94
+ All engines write to the same `~/.claude-mem/claude-mem.db`, tagged by `platform_source`:
95
+
96
+ | Engine | Platform Source |
97
+ |---|---|
98
+ | Claude Code | `claude` |
99
+ | OpenClaw | `openclaw` |
100
+ | Pi-Agent | `pi-agent` |
101
+ | Cursor | `cursor` |
102
+ | Codex | `codex` |
103
+
104
+ Context injection returns observations from all engines for the same project by default.
105
+
106
+ ## Related Packages
107
+
108
+ Other independent claude-mem adapters published to npm:
109
+
110
+ - [`@ephemushroom/opencode-claude-mem`](https://www.npmjs.com/package/@ephemushroom/opencode-claude-mem) — OpenCode adapter (MIT)
111
+ - [`opencode-cmem`](https://www.npmjs.com/package/opencode-cmem) — OpenCode adapter (MIT)
112
+
113
+ Other pi memory extensions (standalone, not claude-mem based):
114
+
115
+ - [`@samfp/pi-memory`](https://www.npmjs.com/package/@samfp/pi-memory) — Learns corrections/preferences from sessions
116
+ - [`@zhafron/pi-memory`](https://www.npmjs.com/package/@zhafron/pi-memory) — Memory management + identity
117
+ - [`@db0-ai/pi`](https://www.npmjs.com/package/@db0-ai/pi) — Auto fact extraction, local SQLite
118
+
119
+ ## Development
120
+
121
+ ```bash
122
+ # Edit the extension
123
+ vim extensions/pi-mem.ts
124
+
125
+ # Test locally
126
+ pi -e ./extensions/pi-mem.ts
127
+
128
+ # Or install from local path
129
+ pi install ./pi-agent
130
+ ```
131
+
132
+ ## License
133
+
134
+ AGPL-3.0 — same as [claude-mem](https://github.com/thedotmack/claude-mem/blob/main/LICENSE).
@@ -0,0 +1,400 @@
1
+ /**
2
+ * Pi-Mem — claude-mem extension for pi-mono agents
3
+ *
4
+ * Gives pi-agents (pi-coding-agent, custom pi-mono runtimes) persistent
5
+ * cross-session memory by connecting to the claude-mem worker HTTP API.
6
+ *
7
+ * Derived from the OpenClaw plugin (claude-mem/openclaw/src/index.ts) which
8
+ * is a proven integration pattern for pi-mono-based runtimes.
9
+ *
10
+ * Install:
11
+ * pi install npm:pi-agent-memory
12
+ * — or —
13
+ * pi install git:github.com/thedotmack/claude-mem --extensions pi-agent/extensions
14
+ *
15
+ * Requires: claude-mem worker running on localhost:37777
16
+ */
17
+
18
+ import { Type } from "@mariozechner/pi-ai";
19
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
20
+ import { basename } from "node:path";
21
+
22
+ // =============================================================================
23
+ // Configuration
24
+ // =============================================================================
25
+
26
+ const WORKER_PORT = parseInt(process.env.CLAUDE_MEM_PORT || "37777", 10);
27
+ const WORKER_HOST = process.env.CLAUDE_MEM_HOST || "127.0.0.1";
28
+ const PLATFORM_SOURCE = "pi-agent";
29
+ const MAX_TOOL_RESPONSE_LENGTH = 1000;
30
+ const SESSION_COMPLETE_DELAY_MS = 3000;
31
+ const WORKER_FETCH_TIMEOUT_MS = 10_000;
32
+ const MAX_SEARCH_LIMIT = 100;
33
+
34
+ // =============================================================================
35
+ // HTTP Helpers
36
+ //
37
+ // Mirrors the pattern from openclaw/src/index.ts (lines 267-340).
38
+ // Three variants: awaited POST, fire-and-forget POST, awaited GET.
39
+ // All awaited calls use AbortController for timeout protection.
40
+ // =============================================================================
41
+
42
+ function workerUrl(path: string): string {
43
+ return `http://${WORKER_HOST}:${WORKER_PORT}${path}`;
44
+ }
45
+
46
+ /** Create an AbortController that auto-aborts after the configured timeout. */
47
+ function createTimeoutController(): { controller: AbortController; clear: () => void } {
48
+ const controller = new AbortController();
49
+ const timer = setTimeout(() => controller.abort(), WORKER_FETCH_TIMEOUT_MS);
50
+ return { controller, clear: () => clearTimeout(timer) };
51
+ }
52
+
53
+ async function workerPost(path: string, body: Record<string, unknown>): Promise<Record<string, unknown> | null> {
54
+ const { controller, clear } = createTimeoutController();
55
+ try {
56
+ const response = await fetch(workerUrl(path), {
57
+ method: "POST",
58
+ headers: { "Content-Type": "application/json" },
59
+ body: JSON.stringify(body),
60
+ signal: controller.signal,
61
+ });
62
+ if (!response.ok) {
63
+ console.error(`[pi-mem] Worker POST ${path} returned ${response.status}`);
64
+ return null;
65
+ }
66
+ return (await response.json()) as Record<string, unknown>;
67
+ } catch (error: unknown) {
68
+ if (error instanceof DOMException && error.name === "AbortError") {
69
+ console.error(`[pi-mem] Worker POST ${path} timed out after ${WORKER_FETCH_TIMEOUT_MS}ms`);
70
+ return null;
71
+ }
72
+ const message = error instanceof Error ? error.message : String(error);
73
+ console.error(`[pi-mem] Worker POST ${path} failed: ${message}`);
74
+ return null;
75
+ } finally {
76
+ clear();
77
+ }
78
+ }
79
+
80
+ function workerPostFireAndForget(path: string, body: Record<string, unknown>): void {
81
+ fetch(workerUrl(path), {
82
+ method: "POST",
83
+ headers: { "Content-Type": "application/json" },
84
+ body: JSON.stringify(body),
85
+ }).catch((error: unknown) => {
86
+ const message = error instanceof Error ? error.message : String(error);
87
+ console.error(`[pi-mem] Worker POST ${path} failed: ${message}`);
88
+ });
89
+ }
90
+
91
+ async function workerGetText(path: string): Promise<string | null> {
92
+ const { controller, clear } = createTimeoutController();
93
+ try {
94
+ const response = await fetch(workerUrl(path), { signal: controller.signal });
95
+ if (!response.ok) {
96
+ console.error(`[pi-mem] Worker GET ${path} returned ${response.status}`);
97
+ return null;
98
+ }
99
+ return await response.text();
100
+ } catch (error: unknown) {
101
+ if (error instanceof DOMException && error.name === "AbortError") {
102
+ console.error(`[pi-mem] Worker GET ${path} timed out after ${WORKER_FETCH_TIMEOUT_MS}ms`);
103
+ return null;
104
+ }
105
+ const message = error instanceof Error ? error.message : String(error);
106
+ console.error(`[pi-mem] Worker GET ${path} failed: ${message}`);
107
+ return null;
108
+ } finally {
109
+ clear();
110
+ }
111
+ }
112
+
113
+ // =============================================================================
114
+ // Project Name Derivation
115
+ //
116
+ // Scopes observations by project. Uses PI_MEM_PROJECT env var if set,
117
+ // otherwise derives from the working directory basename with a "pi-" prefix.
118
+ // =============================================================================
119
+
120
+ function deriveProjectName(cwd: string): string {
121
+ if (process.env.PI_MEM_PROJECT) {
122
+ return process.env.PI_MEM_PROJECT;
123
+ }
124
+ const dir = basename(cwd);
125
+ return `pi-${dir}`;
126
+ }
127
+
128
+ // =============================================================================
129
+ // Extension Factory
130
+ // =============================================================================
131
+
132
+ export default function piMemExtension(pi: ExtensionAPI) {
133
+ // --- Extension state ---
134
+ let contentSessionId: string | null = null;
135
+ let projectName = "pi-agent";
136
+ let sessionCwd = process.cwd();
137
+
138
+ // Check kill switch
139
+ if (process.env.PI_MEM_DISABLED === "1") {
140
+ return;
141
+ }
142
+
143
+ // =========================================================================
144
+ // Event: session_start
145
+ //
146
+ // Initialize local state only. The worker init happens in
147
+ // before_agent_start (which has the user prompt). We set up the session ID
148
+ // here so tool_result handlers have a target from the first turn.
149
+ // =========================================================================
150
+
151
+ pi.on("session_start", async (_event, ctx) => {
152
+ sessionCwd = ctx.cwd;
153
+ projectName = deriveProjectName(sessionCwd);
154
+ contentSessionId = `pi-${projectName}-${Date.now()}`;
155
+
156
+ // Persist session ID into the session file for compaction recovery
157
+ pi.appendEntry("pi-mem-session", { contentSessionId, projectName });
158
+ });
159
+
160
+ // =========================================================================
161
+ // Event: before_agent_start
162
+ //
163
+ // Initialize the session in the worker with the user's prompt.
164
+ // The worker needs the prompt for privacy filtering — observations are
165
+ // queued until a prompt is registered.
166
+ //
167
+ // Mirrors openclaw/src/index.ts lines 722-741.
168
+ // =========================================================================
169
+
170
+ pi.on("before_agent_start", async (event) => {
171
+ if (!contentSessionId) return;
172
+
173
+ await workerPost("/api/sessions/init", {
174
+ contentSessionId,
175
+ project: projectName,
176
+ prompt: event.prompt || "pi-agent session",
177
+ platform_source: PLATFORM_SOURCE,
178
+ });
179
+
180
+ return undefined;
181
+ });
182
+
183
+ // =========================================================================
184
+ // Event: context
185
+ //
186
+ // Inject past observations into the LLM context. Calls the worker's
187
+ // context injection endpoint which returns a formatted timeline of
188
+ // relevant past work.
189
+ //
190
+ // Does NOT filter by platform_source so that pi-agents see observations
191
+ // from Claude Code, Cursor, OpenClaw, etc. — enabling cross-engine memory.
192
+ //
193
+ // Mirrors openclaw/src/index.ts lines 743-759, but uses pi-mono's
194
+ // ContextEventResult (returning messages array) instead of OpenClaw's
195
+ // appendSystemContext.
196
+ // =========================================================================
197
+
198
+ pi.on("context", async (event) => {
199
+ if (!contentSessionId) return;
200
+
201
+ const projects = encodeURIComponent(projectName);
202
+ const contextText = await workerGetText(`/api/context/inject?projects=${projects}`);
203
+
204
+ if (!contextText || contextText.trim().length === 0) return;
205
+
206
+ // Inject as a system message in the conversation
207
+ return {
208
+ messages: [
209
+ ...event.messages,
210
+ {
211
+ role: "user" as const,
212
+ content: [
213
+ {
214
+ type: "text" as const,
215
+ text: `<pi-mem-context>\n${contextText}\n</pi-mem-context>`,
216
+ },
217
+ ],
218
+ },
219
+ ],
220
+ };
221
+ });
222
+
223
+ // =========================================================================
224
+ // Event: tool_result
225
+ //
226
+ // Capture tool observations. Fire-and-forget to avoid slowing down the
227
+ // agent loop. Skips memory_recall to prevent recursive observation loops.
228
+ //
229
+ // Mirrors openclaw/src/index.ts lines 764-808.
230
+ // =========================================================================
231
+
232
+ pi.on("tool_result", (event) => {
233
+ if (!contentSessionId) return;
234
+
235
+ const toolName = event.toolName;
236
+ if (!toolName) return;
237
+
238
+ // Skip memory tools to prevent recursive observation loops
239
+ if (toolName === "memory_recall") return;
240
+
241
+ // Extract result text from content blocks
242
+ let toolResponseText = "";
243
+ if (Array.isArray(event.content)) {
244
+ toolResponseText = event.content
245
+ .filter((block): block is { type: "text"; text: string } => block.type === "text" && "text" in block)
246
+ .map((block) => block.text)
247
+ .join("\n");
248
+ }
249
+
250
+ // Truncate to prevent oversized payloads
251
+ if (toolResponseText.length > MAX_TOOL_RESPONSE_LENGTH) {
252
+ toolResponseText = toolResponseText.slice(0, MAX_TOOL_RESPONSE_LENGTH);
253
+ }
254
+
255
+ workerPostFireAndForget("/api/sessions/observations", {
256
+ contentSessionId,
257
+ tool_name: toolName,
258
+ tool_input: event.input || {},
259
+ tool_response: toolResponseText,
260
+ cwd: sessionCwd,
261
+ });
262
+
263
+ return undefined;
264
+ });
265
+
266
+ // =========================================================================
267
+ // Event: agent_end
268
+ //
269
+ // Summarize the session and schedule completion. Uses await for summarize
270
+ // to ensure the worker processes it before the completion call. Completion
271
+ // is delayed to let in-flight fire-and-forget observations land.
272
+ //
273
+ // Mirrors openclaw/src/index.ts lines 813-845.
274
+ // =========================================================================
275
+
276
+ pi.on("agent_end", async (event) => {
277
+ if (!contentSessionId) return;
278
+
279
+ // Extract last assistant message for summarization
280
+ let lastAssistantMessage = "";
281
+ if (Array.isArray(event.messages)) {
282
+ for (let i = event.messages.length - 1; i >= 0; i--) {
283
+ const msg = event.messages[i];
284
+ if (msg?.role === "assistant") {
285
+ if (typeof msg.content === "string") {
286
+ lastAssistantMessage = msg.content;
287
+ } else if (Array.isArray(msg.content)) {
288
+ lastAssistantMessage = msg.content
289
+ .filter((block): block is { type: "text"; text: string } => block.type === "text")
290
+ .map((block) => block.text)
291
+ .join("\n");
292
+ }
293
+ break;
294
+ }
295
+ }
296
+ }
297
+
298
+ // Await summarize so the worker receives it before complete
299
+ await workerPost("/api/sessions/summarize", {
300
+ contentSessionId,
301
+ last_assistant_message: lastAssistantMessage,
302
+ });
303
+
304
+ // Delay completion to let in-flight observations arrive
305
+ const sid = contentSessionId;
306
+ setTimeout(() => {
307
+ workerPostFireAndForget("/api/sessions/complete", {
308
+ contentSessionId: sid,
309
+ });
310
+ }, SESSION_COMPLETE_DELAY_MS);
311
+ });
312
+
313
+ // =========================================================================
314
+ // Event: session_compact
315
+ //
316
+ // Preserve session state across context compaction. The LLM's context
317
+ // window was trimmed, but our session continues — do NOT create a new
318
+ // session or re-init the worker.
319
+ //
320
+ // Mirrors openclaw/src/index.ts lines 714-717.
321
+ // =========================================================================
322
+
323
+ pi.on("session_compact", () => {
324
+ // Nothing to do — contentSessionId persists in extension state.
325
+ // Re-injection happens automatically via the next `context` event.
326
+ });
327
+
328
+ // =========================================================================
329
+ // Event: session_shutdown
330
+ //
331
+ // Clean up local state on process exit.
332
+ // =========================================================================
333
+
334
+ pi.on("session_shutdown", () => {
335
+ contentSessionId = null;
336
+ });
337
+
338
+ // =========================================================================
339
+ // Tool: memory_recall
340
+ //
341
+ // Registered tool that lets the LLM explicitly search past work sessions.
342
+ // Uses the worker's search API (hybrid FTS5 + Chroma).
343
+ // Does NOT filter by platform_source — returns results from all engines.
344
+ // =========================================================================
345
+
346
+ pi.registerTool({
347
+ name: "memory_recall",
348
+ label: "Memory Recall",
349
+ description:
350
+ "Search past work sessions for relevant context. Use when the user asks about previous work, or when you need context about how something was done before.",
351
+ parameters: Type.Object({
352
+ query: Type.String({ description: "Natural language search query" }),
353
+ limit: Type.Optional(Type.Number({ description: "Max results to return (default: 5, max: 100)" })),
354
+ }),
355
+
356
+ async execute(_toolCallId, params) {
357
+ const query = encodeURIComponent(String(params.query));
358
+ const limit = Math.max(1, Math.min(typeof params.limit === "number" ? Math.floor(params.limit) : 5, MAX_SEARCH_LIMIT));
359
+ const project = encodeURIComponent(projectName);
360
+
361
+ const result = await workerGetText(`/api/search?q=${query}&limit=${limit}&project=${project}`);
362
+
363
+ const text = result || "No matching memories found.";
364
+ return {
365
+ content: [{ type: "text" as const, text }],
366
+ details: undefined,
367
+ };
368
+ },
369
+ });
370
+
371
+ // =========================================================================
372
+ // Command: /memory-status
373
+ //
374
+ // Quick health check — verifies the worker is reachable and shows
375
+ // current session state.
376
+ // =========================================================================
377
+
378
+ pi.registerCommand("memory-status", {
379
+ description: "Show pi-mem connection status and current session info",
380
+ handler: async (_args, ctx) => {
381
+ const { controller, clear } = createTimeoutController();
382
+ try {
383
+ const response = await fetch(workerUrl("/api/health"), { signal: controller.signal });
384
+ if (response.ok) {
385
+ const data = (await response.json()) as Record<string, unknown>;
386
+ ctx.ui.notify(
387
+ `pi-mem: connected to worker v${data.version || "?"} | session: ${contentSessionId || "none"} | project: ${projectName}`,
388
+ "info",
389
+ );
390
+ } else {
391
+ ctx.ui.notify(`pi-mem: worker returned HTTP ${response.status}`, "warning");
392
+ }
393
+ } catch {
394
+ ctx.ui.notify("pi-mem: worker not reachable at " + workerUrl("/api/health"), "error");
395
+ } finally {
396
+ clear();
397
+ }
398
+ },
399
+ });
400
+ }
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "pi-agent-memory",
3
+ "version": "0.1.0",
4
+ "description": "Claude-mem persistent memory extension for pi-agents. Cross-session, cross-engine memory via claude-mem's worker API.",
5
+ "keywords": [
6
+ "pi-package",
7
+ "claude-mem",
8
+ "pi-agent",
9
+ "pi-coding-agent",
10
+ "memory",
11
+ "persistent-memory",
12
+ "cross-session",
13
+ "cross-engine"
14
+ ],
15
+ "author": "ArtemisAI",
16
+ "license": "AGPL-3.0",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/thedotmack/claude-mem.git",
20
+ "directory": "pi-agent"
21
+ },
22
+ "homepage": "https://github.com/thedotmack/claude-mem/tree/main/pi-agent#readme",
23
+ "bugs": {
24
+ "url": "https://github.com/thedotmack/claude-mem/issues"
25
+ },
26
+ "pi": {
27
+ "extensions": ["extensions"],
28
+ "skills": ["skills"]
29
+ },
30
+ "peerDependencies": {
31
+ "@mariozechner/pi-coding-agent": "*",
32
+ "@mariozechner/pi-ai": "*"
33
+ },
34
+ "files": [
35
+ "extensions",
36
+ "skills",
37
+ "README.md",
38
+ "LICENSE"
39
+ ]
40
+ }
@@ -0,0 +1,45 @@
1
+ ---
2
+ name: mem-search
3
+ description: Search pi-mem's persistent cross-session memory database. Use when user asks "did we already solve this?", "how did we do X last time?", or needs work from previous sessions.
4
+ ---
5
+
6
+ # Memory Search (Pi-Mem)
7
+
8
+ Search past work across all pi-agent sessions. The `memory_recall` tool is registered automatically by the pi-mem extension.
9
+
10
+ ## When to Use
11
+
12
+ Use when users ask about PREVIOUS sessions (not current conversation):
13
+
14
+ - "Did we already fix this?"
15
+ - "How did we solve X last time?"
16
+ - "What happened last week?"
17
+ - "What do you remember about the auth refactor?"
18
+
19
+ ## Usage
20
+
21
+ The `memory_recall` tool is available in your tool list. Call it with a natural language query:
22
+
23
+ ```text
24
+ memory_recall(query="authentication middleware refactor", limit=10)
25
+ ```
26
+
27
+ **Parameters:**
28
+
29
+ - `query` (string, required) — Natural language search term
30
+ - `limit` (number, optional) — Max results, default 5
31
+
32
+ ## Tips
33
+
34
+ - Search broad first, then narrow: "auth" before "JWT token rotation in middleware"
35
+ - The tool searches across ALL engines (Claude Code, OpenClaw, other pi-agents) for the same project
36
+ - Results include observation summaries, session titles, and timestamps
37
+ - If you need more detail, ask follow-up questions using specific terms from the initial results
38
+
39
+ ## How It Works
40
+
41
+ The `memory_recall` tool calls the claude-mem worker's search API, which uses hybrid search:
42
+ 1. **FTS5** — full-text keyword matching on observation content
43
+ 2. **Chroma** — vector similarity search for semantic meaning
44
+
45
+ Results are merged and ranked by relevance.