pi-automem-bridge 0.2.2 → 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 +10 -3
- 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/src/tools/memory-tools.ts +24 -40
- package/src/tools/relationship-tools.ts +13 -23
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
pi install npm:pi-automem-bridge
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
[](https://www.npmjs.com/package/pi-automem-bridge)
|
|
14
14
|
[](https://www.npmjs.com/package/pi-automem-bridge)
|
|
15
15
|
|
|
16
16
|
[](https://ko-fi.com/L2J320X82M)
|
|
@@ -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
|
|
@@ -135,6 +138,8 @@ You don't type these — pi does, in plain conversation. Tell it *"remember that
|
|
|
135
138
|
| `automem_link_memories` | Create a typed relationship between two existing memories. |
|
|
136
139
|
| `automem_correct_memory` | Store a correction and link old → new with a provenance relationship (EVOLVED_INTO or CONTRADICTS). |
|
|
137
140
|
|
|
141
|
+
Policy blocks, missing approval in non-interactive contexts, and invalid update requests surface as pi tool errors. User-cancelled confirmations and duplicate detection are normal control-flow results, so the agent can stop or choose the next write path deliberately.
|
|
142
|
+
|
|
138
143
|
---
|
|
139
144
|
|
|
140
145
|
## Write policy
|
|
@@ -203,6 +208,8 @@ Controls how much of the recalled context shows in chat. Injection into the syst
|
|
|
203
208
|
|
|
204
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`).
|
|
205
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
|
+
|
|
206
213
|
---
|
|
207
214
|
|
|
208
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
|
+
}
|
|
@@ -68,11 +68,10 @@ export function registerMemoryTools(pi: ExtensionAPI) {
|
|
|
68
68
|
config,
|
|
69
69
|
).catch(() => ({ text: "Similar recall failed.", matches: [] }));
|
|
70
70
|
|
|
71
|
-
return {
|
|
72
|
-
content: [{ type: "text" as const, text: formatProposal(decision.action, decision.reasons, decision.normalized, similarText, similarMatches) }],
|
|
73
|
-
details: { action: decision.action, reasons: decision.reasons, findings: decision.findings, candidate: decision.normalized, similarMatches },
|
|
74
|
-
|
|
75
|
-
};
|
|
71
|
+
return {
|
|
72
|
+
content: [{ type: "text" as const, text: formatProposal(decision.action, decision.reasons, decision.normalized, similarText, similarMatches) }],
|
|
73
|
+
details: { action: decision.action, reasons: decision.reasons, findings: decision.findings, candidate: decision.normalized, similarMatches },
|
|
74
|
+
};
|
|
76
75
|
},
|
|
77
76
|
});
|
|
78
77
|
|
|
@@ -87,15 +86,11 @@ export function registerMemoryTools(pi: ExtensionAPI) {
|
|
|
87
86
|
const config = loadConfig();
|
|
88
87
|
setAutoMemMcpServerName(config.mcpServerName);
|
|
89
88
|
const candidate = toCandidate(params);
|
|
90
|
-
const decision = evaluateWritePolicy(candidate, config);
|
|
91
|
-
|
|
92
|
-
if (decision.action === "block") {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
details: { action: decision.action, reasons: decision.reasons, findings: decision.findings },
|
|
96
|
-
isError: true,
|
|
97
|
-
};
|
|
98
|
-
}
|
|
89
|
+
const decision = evaluateWritePolicy(candidate, config);
|
|
90
|
+
|
|
91
|
+
if (decision.action === "block") {
|
|
92
|
+
throw new Error("Blocked by AutoMem write policy.\n" + decision.reasons.map((r: string) => "- " + r).join("\n"));
|
|
93
|
+
}
|
|
99
94
|
|
|
100
95
|
const needsConfirmation = decision.action !== "auto";
|
|
101
96
|
if (needsConfirmation && !params.approvedByUser) {
|
|
@@ -104,14 +99,10 @@ export function registerMemoryTools(pi: ExtensionAPI) {
|
|
|
104
99
|
if (!ok) {
|
|
105
100
|
return { content: [{ type: "text" as const, text: "AutoMem memory write cancelled." }], details: { cancelled: true } };
|
|
106
101
|
}
|
|
107
|
-
} else {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
isError: true,
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
}
|
|
102
|
+
} else {
|
|
103
|
+
throw new Error("Confirmation required before storing this memory. Re-run with approvedByUser=true only after explicit user approval.");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
115
106
|
|
|
116
107
|
// ── UPDATE path ──────────────────────────────────────────────────────
|
|
117
108
|
if (params.updateMemoryId) {
|
|
@@ -158,11 +149,10 @@ export function registerMemoryTools(pi: ExtensionAPI) {
|
|
|
158
149
|
" 2. Store anyway (new memory): call automem_commit_memory with dedupeQuery=\"\" to skip dedupe",
|
|
159
150
|
" 3. Cancel: do nothing if this is not worth storing separately",
|
|
160
151
|
].join("\n"),
|
|
161
|
-
}],
|
|
162
|
-
details: { duplicateDetected: true, existingMemoryId: top.id, existingContent: top.content, candidate: decision.normalized, allSimilar: similarMatches },
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
152
|
+
}],
|
|
153
|
+
details: { duplicateDetected: true, existingMemoryId: top.id, existingContent: top.content, candidate: decision.normalized, allSimilar: similarMatches },
|
|
154
|
+
};
|
|
155
|
+
}
|
|
166
156
|
|
|
167
157
|
// ── STORE path ───────────────────────────────────────────────────────
|
|
168
158
|
const result = await automemStore(
|
|
@@ -193,12 +183,9 @@ export function registerMemoryTools(pi: ExtensionAPI) {
|
|
|
193
183
|
promptSnippet: "Use after automem_commit_memory returns DUPLICATE_DETECTED, or when correcting a known memory. Requires the existing memory ID.",
|
|
194
184
|
parameters: UpdateParams,
|
|
195
185
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx: any) {
|
|
196
|
-
if (!params.memoryId) {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
isError: true,
|
|
200
|
-
};
|
|
201
|
-
}
|
|
186
|
+
if (!params.memoryId) {
|
|
187
|
+
throw new Error("memoryId is required for automem_update_memory.");
|
|
188
|
+
}
|
|
202
189
|
|
|
203
190
|
if (!params.approvedByUser) {
|
|
204
191
|
if (ctx && ctx.ui && typeof ctx.ui.confirm === "function") {
|
|
@@ -212,13 +199,10 @@ export function registerMemoryTools(pi: ExtensionAPI) {
|
|
|
212
199
|
if (!ok) {
|
|
213
200
|
return { content: [{ type: "text" as const, text: "AutoMem memory update cancelled." }], details: { cancelled: true } };
|
|
214
201
|
}
|
|
215
|
-
} else {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
}
|
|
202
|
+
} else {
|
|
203
|
+
throw new Error("Confirmation required before updating this memory. Re-run with approvedByUser=true only after explicit user approval.");
|
|
204
|
+
}
|
|
205
|
+
}
|
|
222
206
|
|
|
223
207
|
const result = await automemUpdate(params.memoryId, {
|
|
224
208
|
content: params.content,
|
|
@@ -31,14 +31,11 @@ export function registerRelationshipTools(pi: ExtensionAPI) {
|
|
|
31
31
|
parameters: LinkParams,
|
|
32
32
|
async execute(_toolCallId: string, params: any) {
|
|
33
33
|
const config = loadConfig();
|
|
34
|
-
setAutoMemMcpServerName(config.mcpServerName);
|
|
35
|
-
|
|
36
|
-
if (!params.approvedByUser) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
isError: true,
|
|
40
|
-
};
|
|
41
|
-
}
|
|
34
|
+
setAutoMemMcpServerName(config.mcpServerName);
|
|
35
|
+
|
|
36
|
+
if (!params.approvedByUser) {
|
|
37
|
+
throw new Error("Confirmation required before linking memories. Re-run with approvedByUser=true only after explicit user approval.\n\nWould link:\n " + params.memoryId1 + " -> " + params.relationship + " -> " + params.memoryId2);
|
|
38
|
+
}
|
|
42
39
|
|
|
43
40
|
const strength = typeof params.strength === "number" ? params.strength : 0.5;
|
|
44
41
|
const result = await automemAssociate(params.memoryId1, params.memoryId2, params.relationship, strength);
|
|
@@ -66,21 +63,14 @@ export function registerRelationshipTools(pi: ExtensionAPI) {
|
|
|
66
63
|
tags: Array.isArray(params.tags) ? params.tags : [],
|
|
67
64
|
importance: params.importance,
|
|
68
65
|
};
|
|
69
|
-
const decision = evaluateWritePolicy(candidate, config);
|
|
70
|
-
if (decision.action === "block") {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (!params.approvedByUser) {
|
|
79
|
-
return {
|
|
80
|
-
content: [{ type: "text" as const, text: "Confirmation required before correcting memory. Re-run with approvedByUser=true only after explicit user approval.\n\nWould correct memory " + params.memoryId + " with:\n " + params.correction }],
|
|
81
|
-
isError: true,
|
|
82
|
-
};
|
|
83
|
-
}
|
|
66
|
+
const decision = evaluateWritePolicy(candidate, config);
|
|
67
|
+
if (decision.action === "block") {
|
|
68
|
+
throw new Error("Blocked by AutoMem write policy.\n" + decision.reasons.map((r: string) => "- " + r).join("\n"));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!params.approvedByUser) {
|
|
72
|
+
throw new Error("Confirmation required before correcting memory. Re-run with approvedByUser=true only after explicit user approval.\n\nWould correct memory " + params.memoryId + " with:\n " + params.correction);
|
|
73
|
+
}
|
|
84
74
|
|
|
85
75
|
const rel = params.relationship === "CONTRADICTS" ? "CONTRADICTS" : "EVOLVED_INTO";
|
|
86
76
|
// Store the normalized candidate so corrections get the same alwaysTag,
|