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 CHANGED
@@ -10,7 +10,7 @@
10
10
  pi install npm:pi-automem-bridge
11
11
  ```
12
12
 
13
- [![npm version](https://badge.fury.io/js/pi-automem-bridge.svg)](https://badge.fury.io/js/pi-automem-bridge)
13
+ [![npm version](https://img.shields.io/npm/v/pi-automem-bridge)](https://www.npmjs.com/package/pi-automem-bridge)
14
14
  [![npm downloads](https://img.shields.io/npm/dw/pi-automem-bridge)](https://www.npmjs.com/package/pi-automem-bridge)
15
15
 
16
16
  [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](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 against AutoMem and injects the results — your preferences, working style, and environment — into the system prompt.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-automem-bridge",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Automatic long-term memory recall and policy-gated writes for pi agents via AutoMem MCP",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -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
- try {
41
- await discoverTools();
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
- try {
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) return;
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
- signature,
74
- };
75
- mcpConfigCache.set(serverName, entry);
76
- return entry;
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
- console.warn('[automem] startup recall query failed: "' + query + '" - ' + err);
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
- isError: decision.action === "block",
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
- return {
94
- content: [{ type: "text" as const, text: "Blocked by AutoMem write policy.\n" + decision.reasons.map((r: string) => "- " + r).join("\n") }],
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
- return {
109
- content: [{ type: "text" as const, text: "Confirmation required before storing this memory. Re-run with approvedByUser=true only after explicit user approval." }],
110
- details: { action: decision.action, reasons: decision.reasons, candidate: decision.normalized },
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
- isError: false,
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
- return {
198
- content: [{ type: "text" as const, text: "memoryId is required for automem_update_memory." }],
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
- return {
217
- content: [{ type: "text" as const, text: "Confirmation required before updating this memory. Re-run with approvedByUser=true only after explicit user approval." }],
218
- isError: true,
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
- return {
38
- content: [{ type: "text" as const, text: "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 }],
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
- return {
72
- content: [{ type: "text" as const, text: "Blocked by AutoMem write policy.\n" + decision.reasons.map((r: string) => "- " + r).join("\n") }],
73
- details: { action: decision.action, reasons: decision.reasons, findings: decision.findings },
74
- isError: true,
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,