pi-automem-bridge 0.2.3 → 0.2.4
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/README.md +7 -2
- package/package.json +1 -1
- package/src/commands/status.ts +10 -1
- package/src/index.ts +77 -26
- package/src/mcp-client.ts +30 -22
- package/src/recall.ts +20 -17
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ Plenty of agents can store a memory. Far fewer reach for it when it counts — o
|
|
|
34
34
|
|
|
35
35
|
Once installed, the bridge hooks into pi's session lifecycle:
|
|
36
36
|
|
|
37
|
-
- **At session start** it runs your startup recall queries
|
|
37
|
+
- **At session start** it eagerly warms the AutoMem MCP connection, runs your startup recall queries, and injects the results — your preferences, working style, and environment — into the system prompt.
|
|
38
38
|
- **Before each turn** it recalls memories relevant to the current task and the detected project, again injected silently.
|
|
39
39
|
- **When the agent writes a memory** the candidate passes through the write pipeline — normalize → secret-scan → policy check → dedupe → confirm or auto-store — so nothing unvetted reaches AutoMem.
|
|
40
40
|
- **Relationship tools** let the agent link memories or record corrections with provenance, building a connected graph over time.
|
|
@@ -78,7 +78,8 @@ Add an MCP server entry named `automem` to `~/.pi/agent/mcp.json`, pointing at t
|
|
|
78
78
|
"url": "https://your-automem-server.example.com/mcp",
|
|
79
79
|
"headers": {
|
|
80
80
|
"Authorization": "Bearer ${AUTOMEM_TOKEN}"
|
|
81
|
-
}
|
|
81
|
+
},
|
|
82
|
+
"lifecycle": "keep-alive"
|
|
82
83
|
}
|
|
83
84
|
}
|
|
84
85
|
}
|
|
@@ -86,6 +87,8 @@ Add an MCP server entry named `automem` to `~/.pi/agent/mcp.json`, pointing at t
|
|
|
86
87
|
|
|
87
88
|
This is the one step that can't be automated: the package has no way to know your server's address or auth token, and writing credentials on your behalf would be unsafe. Use `${ENV_VAR}` interpolation for the token — never hardcode secrets. The entry must be named `automem` (the name the extension looks for by default), or set a different name via `mcpServerName` in step 4.
|
|
88
89
|
|
|
90
|
+
`"lifecycle": "keep-alive"` is strongly recommended. pi-mcp-adapter defaults MCP servers to `"lazy"`, which means the AutoMem sidecar may not connect until a tool is called. Memory should be available from the first prompt, so `keep-alive` connects at startup and reconnects automatically if the connection drops. `eager` also connects at startup, but it does not auto-reconnect.
|
|
91
|
+
|
|
89
92
|
**Don't want to hand-edit JSON?** pi is a coding agent — tell it to do it: *"add an `automem` MCP server to my `mcp.json` at `https://my-server.example.com/mcp`, using `${AUTOMEM_TOKEN}` for auth."* Keep the real token in your environment so it never touches the file or the chat.
|
|
90
93
|
|
|
91
94
|
### 3. Reload pi
|
|
@@ -205,6 +208,8 @@ Controls how much of the recalled context shows in chat. Injection into the syst
|
|
|
205
208
|
|
|
206
209
|
Recall is best-effort context enrichment, so it runs on a short, bounded timeout instead of the full MCP request timeout — a slow or unreachable AutoMem server degrades gracefully to no injection rather than blocking your prompt. Tune with `turnRecall.timeoutMs` (default `8000`) and `startupRecall.timeoutMs` (default `15000`).
|
|
207
210
|
|
|
211
|
+
pi-mcp-adapter supports `lazy`, `eager`, and `keep-alive` MCP lifecycles. Use `keep-alive` for AutoMem. The bridge also performs an eager health check at session start to warm the AutoMem connection; if that early check races startup and misses, later turns retry with a short timeout and run the missed startup recall after AutoMem recovers. `/automem-status` is still useful for manual diagnostics, but it is not required as a startup kick.
|
|
212
|
+
|
|
208
213
|
---
|
|
209
214
|
|
|
210
215
|
## Development
|
package/package.json
CHANGED
package/src/commands/status.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
6
|
-
import { automemHealth, setAutoMemMcpServerName } from "../mcp-client";
|
|
6
|
+
import { automemHealth, getAutoMemMcpLifecycle, setAutoMemMcpServerName } from "../mcp-client";
|
|
7
7
|
import { loadConfig } from "../config";
|
|
8
8
|
|
|
9
9
|
export function registerStatusCommand(pi: {
|
|
@@ -19,6 +19,15 @@ export function registerStatusCommand(pi: {
|
|
|
19
19
|
setAutoMemMcpServerName(config.mcpServerName);
|
|
20
20
|
|
|
21
21
|
ctx.ui.notify("Checking AutoMem...", "info");
|
|
22
|
+
try {
|
|
23
|
+
const lifecycle = getAutoMemMcpLifecycle();
|
|
24
|
+
ctx.ui.notify("MCP lifecycle: " + lifecycle, lifecycle === "lazy" ? "warning" : "info");
|
|
25
|
+
if (lifecycle === "lazy") {
|
|
26
|
+
ctx.ui.notify('Set lifecycle to "keep-alive" in ~/.pi/agent/mcp.json so AutoMem connects at startup and reconnects automatically.', "warning");
|
|
27
|
+
}
|
|
28
|
+
} catch (err) {
|
|
29
|
+
ctx.ui.notify("Could not inspect MCP lifecycle: " + err, "warning");
|
|
30
|
+
}
|
|
22
31
|
|
|
23
32
|
const health = await automemHealth();
|
|
24
33
|
|
package/src/index.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
11
11
|
import { loadConfig } from "./config";
|
|
12
|
-
import { automemHealth, discoverTools, setAutoMemMcpServerName } from "./mcp-client";
|
|
12
|
+
import { automemHealth, discoverTools, getAutoMemMcpLifecycle, setAutoMemMcpServerName } from "./mcp-client";
|
|
13
13
|
import { startupRecall, turnRecall, type RecallResult } from "./recall";
|
|
14
14
|
import { detectProject } from "./project-detect";
|
|
15
15
|
import { buildContextMessage } from "./context-injector";
|
|
@@ -24,6 +24,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
24
24
|
let autoMemHealthy = false;
|
|
25
25
|
let autoMemCount: number | undefined;
|
|
26
26
|
let startupInjected = false;
|
|
27
|
+
let startupRecallAttempted = false;
|
|
27
28
|
let startupResult: RecallResult = { text: "", count: 0, truncated: false };
|
|
28
29
|
|
|
29
30
|
// Register commands and write tools
|
|
@@ -37,32 +38,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
37
38
|
config = loadConfig();
|
|
38
39
|
setAutoMemMcpServerName(config.mcpServerName);
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const health = await automemHealth();
|
|
43
|
-
autoMemHealthy = health.healthy;
|
|
44
|
-
autoMemCount = health.memoryCount;
|
|
45
|
-
|
|
46
|
-
if (health.healthy) {
|
|
47
|
-
const count = health.memoryCount != null ? " (" + health.memoryCount + ")" : "";
|
|
48
|
-
ctx.ui.notify("AutoMem: healthy" + count, "info");
|
|
49
|
-
} else {
|
|
50
|
-
ctx.ui.notify("AutoMem: unhealthy - " + (health.error || "unreachable"), "warning");
|
|
51
|
-
}
|
|
52
|
-
} catch (err) {
|
|
53
|
-
autoMemHealthy = false;
|
|
54
|
-
ctx.ui.notify("AutoMem health check failed: " + err, "warning");
|
|
55
|
-
}
|
|
41
|
+
notifyIfLazyMcpLifecycle(ctx);
|
|
42
|
+
await refreshAutoMemHealth(ctx, false);
|
|
56
43
|
|
|
57
44
|
if (config.startupRecall.enabled && autoMemHealthy) {
|
|
58
|
-
|
|
59
|
-
startupResult = await startupRecall(config);
|
|
60
|
-
if (startupResult.count > 0 && config.startupRecall.showStatus) {
|
|
61
|
-
ctx.ui.notify("AutoMem: recalled " + startupResult.count + " memories at startup", "info");
|
|
62
|
-
}
|
|
63
|
-
} catch (err) {
|
|
64
|
-
ctx.ui.notify("AutoMem startup recall failed: " + err, "warning");
|
|
65
|
-
}
|
|
45
|
+
await runStartupRecall(ctx);
|
|
66
46
|
}
|
|
67
47
|
|
|
68
48
|
updateStatusWidget(ctx);
|
|
@@ -70,7 +50,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
70
50
|
|
|
71
51
|
// before_agent_start - Turn-level recall + context injection
|
|
72
52
|
pi.on("before_agent_start", async function(event: any, ctx: any) {
|
|
73
|
-
if (!autoMemHealthy)
|
|
53
|
+
if (!autoMemHealthy) {
|
|
54
|
+
const recovered = await refreshAutoMemHealth(ctx, true);
|
|
55
|
+
if (!recovered) {
|
|
56
|
+
updateStatusWidget(ctx);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (config.startupRecall.enabled && !startupRecallAttempted) {
|
|
62
|
+
await runStartupRecall(ctx);
|
|
63
|
+
}
|
|
74
64
|
|
|
75
65
|
const prompt = event.prompt || "";
|
|
76
66
|
if (!prompt.trim()) return;
|
|
@@ -135,9 +125,70 @@ export default function (pi: ExtensionAPI) {
|
|
|
135
125
|
autoMemHealthy = false;
|
|
136
126
|
autoMemCount = undefined;
|
|
137
127
|
startupInjected = false;
|
|
128
|
+
startupRecallAttempted = false;
|
|
138
129
|
startupResult = { text: "", count: 0, truncated: false };
|
|
139
130
|
});
|
|
140
131
|
|
|
132
|
+
async function refreshAutoMemHealth(ctx: any, recoveringFromOffline: boolean): Promise<boolean> {
|
|
133
|
+
try {
|
|
134
|
+
if (!recoveringFromOffline) {
|
|
135
|
+
await discoverTools();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const healthTimeout = recoveringFromOffline
|
|
139
|
+
? Math.max(1000, Math.min(3000, Number(config.turnRecall.timeoutMs || 3000)))
|
|
140
|
+
: 30000;
|
|
141
|
+
const health = await automemHealth(healthTimeout);
|
|
142
|
+
autoMemHealthy = health.healthy;
|
|
143
|
+
autoMemCount = health.memoryCount;
|
|
144
|
+
|
|
145
|
+
if (health.healthy) {
|
|
146
|
+
const count = health.memoryCount != null ? " (" + health.memoryCount + ")" : "";
|
|
147
|
+
ctx.ui.notify(recoveringFromOffline ? "AutoMem: recovered" + count : "AutoMem: healthy" + count, "info");
|
|
148
|
+
} else if (!recoveringFromOffline) {
|
|
149
|
+
ctx.ui.notify("AutoMem: unhealthy - " + (health.error || "unreachable"), "warning");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return health.healthy;
|
|
153
|
+
} catch (err) {
|
|
154
|
+
autoMemHealthy = false;
|
|
155
|
+
if (!recoveringFromOffline) {
|
|
156
|
+
ctx.ui.notify("AutoMem health check failed: " + err, "warning");
|
|
157
|
+
} else {
|
|
158
|
+
console.warn("[automem] AutoMem still unavailable during turn health retry: " + err);
|
|
159
|
+
}
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function runStartupRecall(ctx: any): Promise<void> {
|
|
165
|
+
startupRecallAttempted = true;
|
|
166
|
+
try {
|
|
167
|
+
startupResult = await startupRecall(config);
|
|
168
|
+
startupRecallAttempted = startupResult.failed !== true;
|
|
169
|
+
if (startupResult.count > 0 && config.startupRecall.showStatus) {
|
|
170
|
+
ctx.ui.notify("AutoMem: recalled " + startupResult.count + " memories at startup", "info");
|
|
171
|
+
}
|
|
172
|
+
} catch (err) {
|
|
173
|
+
startupRecallAttempted = false;
|
|
174
|
+
ctx.ui.notify("AutoMem startup recall failed: " + err, "warning");
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function notifyIfLazyMcpLifecycle(ctx: any): void {
|
|
179
|
+
try {
|
|
180
|
+
const lifecycle = getAutoMemMcpLifecycle();
|
|
181
|
+
if (lifecycle === "lazy") {
|
|
182
|
+
ctx.ui.notify(
|
|
183
|
+
'AutoMem MCP lifecycle is "lazy". For automatic memory on every session, set the automem server in ~/.pi/agent/mcp.json to "lifecycle": "keep-alive".',
|
|
184
|
+
"warning",
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
} catch (err) {
|
|
188
|
+
console.warn("[automem] could not inspect MCP lifecycle: " + err);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
141
192
|
function updateStatusWidget(ctx: any) {
|
|
142
193
|
const theme = ctx.ui.theme;
|
|
143
194
|
if (autoMemHealthy) {
|
package/src/mcp-client.ts
CHANGED
|
@@ -19,11 +19,13 @@ export interface McpCallResult {
|
|
|
19
19
|
isError?: boolean;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
export interface McpHealth {
|
|
23
|
-
healthy: boolean;
|
|
24
|
-
memoryCount?: number;
|
|
25
|
-
error?: string;
|
|
26
|
-
}
|
|
22
|
+
export interface McpHealth {
|
|
23
|
+
healthy: boolean;
|
|
24
|
+
memoryCount?: number;
|
|
25
|
+
error?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type McpLifecycle = "lazy" | "eager" | "keep-alive" | string;
|
|
27
29
|
|
|
28
30
|
// ---------------------------------------------------------------------------
|
|
29
31
|
// MCP config reader
|
|
@@ -34,7 +36,7 @@ export interface McpHealth {
|
|
|
34
36
|
// cheap stat signature (mtime + size — the same quick-check make/rsync use), so
|
|
35
37
|
// an in-place mcp.json edit is still picked up even within a single mtime tick.
|
|
36
38
|
// An empty signature (stat failed) never matches, forcing a fresh read.
|
|
37
|
-
interface CachedServerConfig { url: string; auth: string; signature: string }
|
|
39
|
+
interface CachedServerConfig { url: string; auth: string; lifecycle: McpLifecycle; signature: string }
|
|
38
40
|
let mcpConfigCache: Map<string, CachedServerConfig> = new Map();
|
|
39
41
|
|
|
40
42
|
function loadMcpServerConfig(serverName: string): CachedServerConfig {
|
|
@@ -57,9 +59,9 @@ function loadMcpServerConfig(serverName: string): CachedServerConfig {
|
|
|
57
59
|
// different tools, so drop the discovery cache too.
|
|
58
60
|
if (cached) discoveredTools = null;
|
|
59
61
|
|
|
60
|
-
const mcpJson = JSON.parse(readFileSync(mcpJsonPath, "utf8")) as {
|
|
61
|
-
mcpServers?: Record<string, { url: string; headers?: Record<string, string
|
|
62
|
-
};
|
|
62
|
+
const mcpJson = JSON.parse(readFileSync(mcpJsonPath, "utf8")) as {
|
|
63
|
+
mcpServers?: Record<string, { url: string; headers?: Record<string, string>; lifecycle?: string }>;
|
|
64
|
+
};
|
|
63
65
|
|
|
64
66
|
const server = mcpJson.mcpServers ? mcpJson.mcpServers[serverName] : undefined;
|
|
65
67
|
if (!server) {
|
|
@@ -68,13 +70,14 @@ function loadMcpServerConfig(serverName: string): CachedServerConfig {
|
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
const entry: CachedServerConfig = {
|
|
71
|
-
url: server.url,
|
|
72
|
-
auth: resolveEnvVars(server.headers?.Authorization || ""),
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
73
|
+
url: server.url,
|
|
74
|
+
auth: resolveEnvVars(server.headers?.Authorization || ""),
|
|
75
|
+
lifecycle: server.lifecycle || "lazy",
|
|
76
|
+
signature,
|
|
77
|
+
};
|
|
78
|
+
mcpConfigCache.set(serverName, entry);
|
|
79
|
+
return entry;
|
|
80
|
+
}
|
|
78
81
|
|
|
79
82
|
// ---------------------------------------------------------------------------
|
|
80
83
|
// Response parsing — handles both JSON and text/event-stream (SSE)
|
|
@@ -114,9 +117,14 @@ export function setAutoMemMcpServerName(serverName: string | undefined): void {
|
|
|
114
117
|
}
|
|
115
118
|
}
|
|
116
119
|
|
|
117
|
-
function getAutoMemMcpServerName(): string {
|
|
118
|
-
return process.env.AUTOMEM_MCP_SERVER || configuredServerName || "automem";
|
|
119
|
-
}
|
|
120
|
+
function getAutoMemMcpServerName(): string {
|
|
121
|
+
return process.env.AUTOMEM_MCP_SERVER || configuredServerName || "automem";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function getAutoMemMcpLifecycle(): McpLifecycle {
|
|
125
|
+
const serverName = getAutoMemMcpServerName();
|
|
126
|
+
return loadMcpServerConfig(serverName).lifecycle;
|
|
127
|
+
}
|
|
120
128
|
|
|
121
129
|
async function mcpCall(
|
|
122
130
|
tool: string,
|
|
@@ -310,9 +318,9 @@ export async function automemRecall(
|
|
|
310
318
|
return mcpCall(resolveToolName("recall_memory"), args, timeoutMs);
|
|
311
319
|
}
|
|
312
320
|
|
|
313
|
-
export async function automemHealth(): Promise<McpHealth> {
|
|
314
|
-
try {
|
|
315
|
-
const result = await mcpCall(resolveToolName("check_database_health"), {});
|
|
321
|
+
export async function automemHealth(timeoutMs: number = 30000): Promise<McpHealth> {
|
|
322
|
+
try {
|
|
323
|
+
const result = await mcpCall(resolveToolName("check_database_health"), {}, timeoutMs);
|
|
316
324
|
const text = result.content && result.content[0] ? result.content[0].text : undefined;
|
|
317
325
|
if (text) {
|
|
318
326
|
try {
|
package/src/recall.ts
CHANGED
|
@@ -179,19 +179,21 @@ function formatMemoriesForContext(
|
|
|
179
179
|
// Startup recall
|
|
180
180
|
// ---------------------------------------------------------------------------
|
|
181
181
|
|
|
182
|
-
export interface RecallResult {
|
|
183
|
-
text: string;
|
|
184
|
-
count: number;
|
|
185
|
-
truncated: boolean;
|
|
186
|
-
|
|
182
|
+
export interface RecallResult {
|
|
183
|
+
text: string;
|
|
184
|
+
count: number;
|
|
185
|
+
truncated: boolean;
|
|
186
|
+
failed?: boolean;
|
|
187
|
+
}
|
|
187
188
|
|
|
188
189
|
export async function startupRecall(config: AutoMemConfig): Promise<RecallResult> {
|
|
189
190
|
if (!config.startupRecall.enabled) {
|
|
190
191
|
return { text: "", count: 0, truncated: false };
|
|
191
192
|
}
|
|
192
193
|
|
|
193
|
-
const allMemories: FormattedMemory[] = [];
|
|
194
|
-
const seenIds = new Set<string>();
|
|
194
|
+
const allMemories: FormattedMemory[] = [];
|
|
195
|
+
const seenIds = new Set<string>();
|
|
196
|
+
let failedQueries = 0;
|
|
195
197
|
|
|
196
198
|
for (let q = 0; q < config.startupRecall.queries.length; q++) {
|
|
197
199
|
const query = config.startupRecall.queries[q];
|
|
@@ -219,16 +221,17 @@ export async function startupRecall(config: AutoMemConfig): Promise<RecallResult
|
|
|
219
221
|
}
|
|
220
222
|
}
|
|
221
223
|
}
|
|
222
|
-
} catch (err) {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
224
|
+
} catch (err) {
|
|
225
|
+
failedQueries++;
|
|
226
|
+
console.warn('[automem] startup recall query failed: "' + query + '" - ' + err);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
226
229
|
|
|
227
230
|
const maxBytes = config.startupRecall.maxBytes;
|
|
228
231
|
const { text, included, overflowed } = formatMemoriesForContext(allMemories, maxBytes);
|
|
229
232
|
const truncated = included < allMemories.length || overflowed;
|
|
230
233
|
|
|
231
|
-
return { text, count: allMemories.length, truncated };
|
|
234
|
+
return { text, count: allMemories.length, truncated, failed: failedQueries > 0 };
|
|
232
235
|
}
|
|
233
236
|
|
|
234
237
|
// ---------------------------------------------------------------------------
|
|
@@ -276,8 +279,8 @@ export async function turnRecall(
|
|
|
276
279
|
const truncated = included < memories.length || overflowed;
|
|
277
280
|
|
|
278
281
|
return { text: formatted, count: memories.length, truncated };
|
|
279
|
-
} catch (err) {
|
|
280
|
-
console.warn("[automem] turn recall failed: " + err);
|
|
281
|
-
return { text: "", count: 0, truncated: false };
|
|
282
|
-
}
|
|
283
|
-
}
|
|
282
|
+
} catch (err) {
|
|
283
|
+
console.warn("[automem] turn recall failed: " + err);
|
|
284
|
+
return { text: "", count: 0, truncated: false, failed: true };
|
|
285
|
+
}
|
|
286
|
+
}
|