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 +10 -0
- package/README.md +134 -0
- package/extensions/pi-mem.ts +400 -0
- package/package.json +40 -0
- package/skills/mem-search/SKILL.md +45 -0
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.
|