mindgraph-mcp 0.20.0 → 0.21.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/README.md CHANGED
@@ -174,7 +174,7 @@ and never block the coding session.
174
174
 
175
175
  ## What You Get
176
176
 
177
- **10 tools** covering the full knowledge-graph workflow (**12** in the coding
177
+ **11 tools** covering the full knowledge-graph workflow (**13** in the coding
178
178
  profile):
179
179
 
180
180
  | Tool | Purpose |
@@ -185,6 +185,7 @@ profile):
185
185
  | `mindgraph_plan` | Plans, tasks, procedures, governance policies, risk assessments, executions |
186
186
  | `mindgraph_retrieve` | Context retrieval with bounded graph expansion, search, min-cost traversal, document index. Read results return as compact rendered text (labels, uids, summaries, epistemic status tags, relationships, traversal depth/cost); set `max_output_chars` for a hard whole-item character budget, or pass `format: "json"` for the raw server response |
187
187
  | `mindgraph_memory` | Read-only M0 memory workflow: inspect honest integration status, retrieve bounded topic context, or recover authoritative task-first resume/continuity context. Returns model-ready text plus a structured envelope with scope, hashes, trace, budget, freshness, and warnings |
188
+ | `mindgraph_remember` | Small-text fast path: `remember` stores one fact/preference/decision synchronously and it is BM25- and vector-searchable on return; a stable `custom_id` makes re-sends an upsert of the same memory. `forget` reversibly removes by uid or custom_id, with `dry_run` previewing the affected edge uids |
188
189
  | `mindgraph_series_query` | Bounded, read-only dense-measurement queries: list an entity's Series, inspect latest values, page a time window, or calculate calendar aggregates |
189
190
  | `mindgraph_ingest` | Chunk / document / session ingestion with LLM-powered extraction. `job_status` without an id renders the 20 most recent jobs (`format: "json"` for the full list) |
190
191
  | `mindgraph_synthesize` | Project-scoped cross-document synthesis — mine signals (rendered as sections since 0.17.0), spawn Article-generation jobs |
@@ -300,3 +301,17 @@ npm run dev # tsup --watch
300
301
  ## License
301
302
 
302
303
  MIT
304
+
305
+ ### Engine failures and retries
306
+
307
+ Tool failures carry `isError: true`; `code`, `status`, and optional `retriable`
308
+ are preserved in both the JSON text and structured content. Lease-conflict fields
309
+ remain available to hooks. A failed retrieval is not an empty successful result.
310
+ Explicit `retriable: false` stops automatic retries, including index maintenance.
311
+ Other 503 responses retry only reviewed reads or atomically keyed work operations.
312
+
313
+ This adoption branch pins the TypeScript SDK to immutable source commit
314
+ `02bad10728836bf55129c08efabf71386432deee` so installs and CI exercise the same
315
+ retry implementation. Git installation builds the SDK through its `prepare` hook.
316
+ Replace this source pin and regenerate the lockfile only after a registry release
317
+ containing that implementation is verified; older SDK releases retry all 503s.
package/dist/cli.js CHANGED
@@ -27,25 +27,127 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
27
  ));
28
28
 
29
29
  // node_modules/mindgraph/dist/index.mjs
30
+ function errorFields(body) {
31
+ if (!body || typeof body !== "object" || Array.isArray(body)) return {};
32
+ const fields = body;
33
+ return {
34
+ ...typeof fields.code === "string" ? { code: fields.code } : {},
35
+ ...typeof fields.retriable === "boolean" ? { retriable: fields.retriable } : {}
36
+ };
37
+ }
38
+ function canRetryRequest(method, path8, body) {
39
+ if (method === "GET" || method === "HEAD") return true;
40
+ if (method !== "POST") return false;
41
+ if (READ_POSTS.has(path8)) return true;
42
+ const fields = body && typeof body === "object" && !Array.isArray(body) ? body : {};
43
+ if (typeof fields.action !== "string") return false;
44
+ if (Object.prototype.hasOwnProperty.call(READ_ACTIONS, path8) && READ_ACTIONS[path8].includes(fields.action)) return true;
45
+ return path8 === "/agent/plan" && IDEMPOTENT_WORK.has(fields.action) && typeof fields.idempotency_key === "string" && fields.idempotency_key.trim().length > 0;
46
+ }
47
+ function permitsRetry(fields) {
48
+ return fields.retriable !== false && ![
49
+ "query_memory_budget_exceeded",
50
+ "query_timeout",
51
+ "query_cancelled",
52
+ "vector_index_rebuilding"
53
+ ].includes(fields.code ?? "");
54
+ }
55
+ function retryDelayMs(header, backoff, attempt) {
56
+ const hint = Number(header);
57
+ return Number.isFinite(hint) && hint > 0 ? Math.min(hint * 1e3, 1e4) : Math.min(backoff * 2 ** Math.min(attempt, 32), 1e4);
58
+ }
30
59
  function seg(value) {
31
60
  return encodeURIComponent(String(value));
32
61
  }
62
+ function pathSteps(response) {
63
+ const wrapped = response && typeof response === "object" && !Array.isArray(response) ? response : void 0;
64
+ const raw = wrapped?.steps ?? response;
65
+ if (!Array.isArray(raw)) return [];
66
+ return raw.flatMap((value) => {
67
+ if (!value || typeof value !== "object" || Array.isArray(value)) return [];
68
+ const step = value;
69
+ const taggedName = (tagged) => {
70
+ if (typeof tagged === "string") return tagged;
71
+ if (!tagged || typeof tagged !== "object" || Array.isArray(tagged)) return null;
72
+ const custom = tagged.Custom;
73
+ return typeof custom === "string" ? custom : null;
74
+ };
75
+ return [{
76
+ ...step,
77
+ node_type: taggedName(step.node_type) ?? "Unknown",
78
+ edge_type: taggedName(step.edge_type)
79
+ }];
80
+ });
81
+ }
33
82
  function createRequestId() {
34
83
  if (typeof globalThis.crypto?.randomUUID === "function") {
35
84
  return globalThis.crypto.randomUUID();
36
85
  }
37
86
  return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
38
87
  }
39
- var MindGraphError, MindGraph;
88
+ var READ_POSTS, READ_ACTIONS, IDEMPOTENT_WORK, MindGraphError, MindGraph;
40
89
  var init_dist = __esm({
41
90
  "node_modules/mindgraph/dist/index.mjs"() {
42
91
  "use strict";
92
+ READ_POSTS = /* @__PURE__ */ new Set([
93
+ "/search",
94
+ "/retrieve/context",
95
+ "/nodes/batch",
96
+ "/edges/batch",
97
+ "/subgraph",
98
+ "/embeddings/search",
99
+ "/embeddings/search-text",
100
+ "/ontology/query",
101
+ "/ontology/query/structured",
102
+ "/ontology/objects/search"
103
+ ]);
104
+ READ_ACTIONS = {
105
+ "/retrieve": [
106
+ "text",
107
+ "semantic",
108
+ "hybrid",
109
+ "active_goals",
110
+ "open_questions",
111
+ "preferences",
112
+ "weak_claims",
113
+ "pending_approvals",
114
+ "unresolved_contradictions",
115
+ "merge_candidates",
116
+ "stale_derivations",
117
+ "curation_counts",
118
+ "layer",
119
+ "recent"
120
+ ],
121
+ "/traverse": ["chain", "top_k_paths", "neighborhood", "path", "subgraph"],
122
+ "/reality/series": ["window", "aggregate", "latest", "list_for_entity", "batch_latest", "batch_aggregate"],
123
+ "/intent/deliberation": ["get_open"],
124
+ "/action/risk": ["get_assessments"],
125
+ "/action/skill": ["get", "list"],
126
+ "/memory/config": ["get_preferences", "get_policies"],
127
+ "/memory/sync": ["status"],
128
+ "/agent/plan": ["get_plan", "resume_work"],
129
+ "/agent/governance": ["get_pending", "check"],
130
+ "/agent/execution": ["get_executions"],
131
+ "/evolve": ["history", "snapshot"]
132
+ };
133
+ IDEMPOTENT_WORK = /* @__PURE__ */ new Set([
134
+ "claim_task",
135
+ "heartbeat",
136
+ "start_iteration",
137
+ "checkpoint_iteration",
138
+ "block_task",
139
+ "complete_task",
140
+ "abandon_iteration"
141
+ ]);
43
142
  MindGraphError = class extends Error {
44
143
  constructor(message, status, body) {
45
144
  super(message);
46
145
  this.status = status;
47
146
  this.body = body;
48
147
  this.name = "MindGraphError";
148
+ const fields = errorFields(body);
149
+ this.code = fields.code;
150
+ this.retriable = fields.retriable;
49
151
  }
50
152
  };
51
153
  MindGraph = class {
@@ -63,6 +165,18 @@ var init_dist = __esm({
63
165
  this.telemetrySurface = config.telemetrySurface;
64
166
  this.maxRetries = config.maxRetries ?? 3;
65
167
  this.retryBackoffMs = config.retryBackoffMs ?? 1e3;
168
+ if (!Number.isSafeInteger(this.maxRetries) || this.maxRetries < 0) {
169
+ throw new RangeError("maxRetries must be a non-negative safe integer");
170
+ }
171
+ if (!Number.isFinite(this.retryBackoffMs) || this.retryBackoffMs < 0) {
172
+ throw new RangeError("retryBackoffMs must be finite and non-negative");
173
+ }
174
+ if (config.timeoutMs !== void 0) {
175
+ if (!Number.isFinite(config.timeoutMs) || config.timeoutMs <= 0) {
176
+ throw new RangeError("timeoutMs must be a positive finite number of milliseconds");
177
+ }
178
+ this.timeoutMs = config.timeoutMs;
179
+ }
66
180
  }
67
181
  // ---- HTTP helpers ----
68
182
  async request(method, path8, body) {
@@ -79,9 +193,14 @@ var init_dist = __esm({
79
193
  if (body !== void 0) {
80
194
  init.body = JSON.stringify(body);
81
195
  }
196
+ const retrySafe = canRetryRequest(
197
+ method,
198
+ path8,
199
+ typeof init.body === "string" ? JSON.parse(init.body) : void 0
200
+ );
82
201
  let lastError;
83
202
  for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
84
- const res = await fetch(url, init);
203
+ const res = await this.fetchWithDeadline(url, init, method, path8);
85
204
  if (!res.ok) {
86
205
  const text22 = await res.text().catch(() => "");
87
206
  let parsed;
@@ -95,10 +214,9 @@ var init_dist = __esm({
95
214
  res.status,
96
215
  parsed
97
216
  );
98
- if (res.status === 503 && attempt < this.maxRetries) {
217
+ if (res.status === 503 && retrySafe && permitsRetry(err5) && attempt < this.maxRetries) {
99
218
  lastError = err5;
100
- const retryAfterSecs = Number(res.headers.get("retry-after"));
101
- const delay = Number.isFinite(retryAfterSecs) && retryAfterSecs > 0 ? Math.min(retryAfterSecs * 1e3, 1e4) : this.retryBackoffMs * 2 ** attempt;
219
+ const delay = retryDelayMs(res.headers.get("retry-after"), this.retryBackoffMs, attempt);
102
220
  await new Promise((r) => setTimeout(r, delay));
103
221
  continue;
104
222
  }
@@ -110,6 +228,33 @@ var init_dist = __esm({
110
228
  }
111
229
  throw lastError;
112
230
  }
231
+ /**
232
+ * One HTTP attempt. With `timeoutMs` set, the attempt is aborted at the
233
+ * deadline and surfaces as a non-retriable `MindGraphError` (`code:
234
+ * "timeout"`, `status: 0`). Without it the request is sent unchanged — no
235
+ * AbortSignal is attached, preserving the pre-0.16 behaviour.
236
+ */
237
+ async fetchWithDeadline(url, init, method, path8) {
238
+ if (this.timeoutMs === void 0) {
239
+ return fetch(url, init);
240
+ }
241
+ const controller = new AbortController();
242
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
243
+ try {
244
+ return await fetch(url, { ...init, signal: controller.signal });
245
+ } catch (error2) {
246
+ if (controller.signal.aborted) {
247
+ throw new MindGraphError(
248
+ `${method} ${path8} timed out after ${this.timeoutMs}ms`,
249
+ 0,
250
+ { error: "timeout", code: "timeout", retriable: false }
251
+ );
252
+ }
253
+ throw error2;
254
+ } finally {
255
+ clearTimeout(timer);
256
+ }
257
+ }
113
258
  get(path8) {
114
259
  return this.request("GET", path8);
115
260
  }
@@ -359,6 +504,40 @@ var init_dist = __esm({
359
504
  async memorySync(req) {
360
505
  return this.post("/memory/sync", req);
361
506
  }
507
+ // ---- Memory (fast path) ----
508
+ /**
509
+ * Store a small piece of text as a memory, synchronously. The node is
510
+ * BM25- and vector-searchable when this resolves (`searchable` says which).
511
+ * Pass `custom_id` to make re-sends an upsert of the same node.
512
+ */
513
+ async remember(text2, options = {}) {
514
+ return this.post("/memory/remember", { text: text2, ...options });
515
+ }
516
+ /**
517
+ * Reversibly remove a memory by `uid` or `custom_id` (tombstone plus its
518
+ * connected edges). `dry_run: true` previews the affected edge uids first.
519
+ * Undo with `restore(uid)` and `evolve({ action: "restore_edge", uid })`.
520
+ */
521
+ async forget(target, options = {}) {
522
+ return this.post("/memory/forget", { ...target, ...options });
523
+ }
524
+ /**
525
+ * Operator guidance for what agents should remember into a Space; surfaced
526
+ * on every `remember()` response and on `retrieveContext()`. Empty clears it.
527
+ */
528
+ async setRememberInstructions(text2, options = {}) {
529
+ return this.post("/memory/config", {
530
+ action: "set_remember_instructions",
531
+ text: text2,
532
+ ...options
533
+ });
534
+ }
535
+ async getRememberInstructions(options = {}) {
536
+ return this.post("/memory/config", {
537
+ action: "get_remember_instructions",
538
+ ...options
539
+ });
540
+ }
362
541
  // ---- Agent Layer ----
363
542
  async plan(req) {
364
543
  return this.post("/agent/plan", req);
@@ -581,7 +760,7 @@ var init_dist = __esm({
581
760
  start_uid: uid,
582
761
  max_depth: maxDepth
583
762
  });
584
- return r?.steps ?? r;
763
+ return pathSteps(r);
585
764
  }
586
765
  async neighborhood(uid, maxDepth = 1) {
587
766
  const r = await this.post("/traverse", {
@@ -589,7 +768,7 @@ var init_dist = __esm({
589
768
  start_uid: uid,
590
769
  max_depth: maxDepth
591
770
  });
592
- return r?.steps ?? r;
771
+ return pathSteps(r);
593
772
  }
594
773
  async subgraph(uid, opts) {
595
774
  return this.post("/subgraph", { start_uids: [uid], ...opts });
@@ -960,11 +1139,19 @@ var init_dist = __esm({
960
1139
  /**
961
1140
  * Create a domain object by hand (auto-approved). Returns the created node
962
1141
  * uid + the audit proposal id. Throws 409 if an object of the same type +
963
- * canonical_name already exists, unless `allow_duplicate` is set.
1142
+ * canonical_name already exists, unless `allow_duplicate` is set. The
1143
+ * schema's identity key is always enforced.
964
1144
  */
965
1145
  async createDomainObject(req) {
966
1146
  return this.post("/v1/ontology/objects", req);
967
1147
  }
1148
+ /**
1149
+ * Schema-validated authored update with optimistic concurrency. Identity
1150
+ * fields are immutable; create and explicitly merge when identity changes.
1151
+ */
1152
+ async updateDomainObject(uid, req) {
1153
+ return this.patch(`/v1/ontology/objects/${seg(uid)}`, req);
1154
+ }
968
1155
  async linkDomainObjects(req) {
969
1156
  return this.post("/ontology/relation", { action: "create", ...req });
970
1157
  }
@@ -1787,6 +1974,22 @@ function conflictState(e) {
1787
1974
  }
1788
1975
  return lifted;
1789
1976
  }
1977
+ function toolError(e, fallbackCode) {
1978
+ const body = e instanceof MindGraphError && e.body && typeof e.body === "object" && !Array.isArray(e.body) ? e.body : {};
1979
+ const code = typeof body.code === "string" ? body.code : fallbackCode;
1980
+ const payload = {
1981
+ error: errorDetail(e),
1982
+ ...conflictState(e),
1983
+ ...e instanceof MindGraphError ? { status: e.status } : {},
1984
+ ...code !== void 0 ? { code } : {},
1985
+ ...typeof body.retriable === "boolean" ? { retriable: body.retriable } : {}
1986
+ };
1987
+ return {
1988
+ content: [{ type: "text", text: JSON.stringify(payload) }],
1989
+ structuredContent: payload,
1990
+ isError: true
1991
+ };
1992
+ }
1790
1993
  var CONFLICT_STATE_KEYS;
1791
1994
  var init_error_detail = __esm({
1792
1995
  "src/error-detail.ts"() {
@@ -1994,7 +2197,7 @@ async function handleCodeTool(client2, args, adapter = new CodegraphAdapter({ cw
1994
2197
  if (cause instanceof UnknownRepositoryError) {
1995
2198
  return err("unknown_repository", cause.message);
1996
2199
  }
1997
- return err("code_tool_failed", errorDetail(cause));
2200
+ return toolError(cause, "code_tool_failed");
1998
2201
  }
1999
2202
  }
2000
2203
  async function attachCodeRefsToToolResult(client2, result, args, adapter) {
@@ -2738,7 +2941,7 @@ async function handleSyncTool(client2, args) {
2738
2941
  }
2739
2942
  return ok2(await remote.memorySync(request));
2740
2943
  } catch (error2) {
2741
- return err2("sync_failed", errorDetail(error2));
2944
+ return toolError(error2, "sync_failed");
2742
2945
  }
2743
2946
  }
2744
2947
  async function scanRepository(remote, repository, requested, provider, agentId) {
@@ -4179,6 +4382,9 @@ async function handleTool(client2, name, args, memoryConfig = {}) {
4179
4382
  case "mindgraph_memory":
4180
4383
  result = await handleMemoryTool(client2, args, memoryConfig);
4181
4384
  break;
4385
+ case "mindgraph_remember":
4386
+ result = await handleRemember(client2, args);
4387
+ break;
4182
4388
  case "mindgraph_series_query":
4183
4389
  result = await handleSeriesQuery(client2, args);
4184
4390
  break;
@@ -4211,19 +4417,7 @@ async function handleTool(client2, name, args, memoryConfig = {}) {
4211
4417
  ].includes(action);
4212
4418
  return attach ? await attachCodeRefsToToolResult(client2, result, args) : result;
4213
4419
  } catch (e) {
4214
- const lifted = conflictState(e);
4215
- if (Object.keys(lifted).length > 0) {
4216
- return {
4217
- content: [
4218
- {
4219
- type: "text",
4220
- text: JSON.stringify({ error: errorDetail(e), ...lifted })
4221
- }
4222
- ],
4223
- isError: true
4224
- };
4225
- }
4226
- return err3(errorDetail(e));
4420
+ return toolError(e);
4227
4421
  }
4228
4422
  }
4229
4423
  async function handleSeriesQuery(client2, args) {
@@ -4247,6 +4441,56 @@ async function handleSeriesQuery(client2, args) {
4247
4441
  await client2.series(args)
4248
4442
  );
4249
4443
  }
4444
+ async function handleRemember(client2, args) {
4445
+ const {
4446
+ action,
4447
+ text: text2,
4448
+ custom_id,
4449
+ label,
4450
+ uid,
4451
+ dry_run,
4452
+ cascade,
4453
+ reason,
4454
+ space_uid,
4455
+ on_near_duplicate,
4456
+ props,
4457
+ agent_id,
4458
+ // Adapter-owned; consumed locally, never sent to the API.
4459
+ invocation_context: _invocationContext,
4460
+ ...rest
4461
+ } = args;
4462
+ void _invocationContext;
4463
+ void rest;
4464
+ switch (action) {
4465
+ case "remember": {
4466
+ if (typeof text2 !== "string" || !text2.trim()) {
4467
+ return err3("remember requires non-empty text");
4468
+ }
4469
+ const options = {};
4470
+ if (typeof custom_id === "string" && custom_id.trim()) options.custom_id = custom_id.trim();
4471
+ if (typeof label === "string" && label.trim()) options.label = label;
4472
+ if (typeof space_uid === "string" && space_uid.trim()) options.space_uid = space_uid;
4473
+ if (on_near_duplicate === "reuse" || on_near_duplicate === "create") {
4474
+ options.on_near_duplicate = on_near_duplicate;
4475
+ }
4476
+ if (props && typeof props === "object") options.props = props;
4477
+ if (typeof agent_id === "string") options.agent_id = agent_id;
4478
+ return ok3(await client2.remember(text2, options));
4479
+ }
4480
+ case "forget": {
4481
+ const target = typeof uid === "string" && uid.trim() ? { uid: uid.trim() } : typeof custom_id === "string" && custom_id.trim() ? { custom_id: custom_id.trim() } : null;
4482
+ if (!target) return err3("forget requires uid or custom_id");
4483
+ const options = {};
4484
+ if (dry_run === true) options.dry_run = true;
4485
+ if (typeof cascade === "boolean") options.cascade = cascade;
4486
+ if (typeof reason === "string" && reason.trim()) options.reason = reason;
4487
+ if (typeof agent_id === "string") options.agent_id = agent_id;
4488
+ return ok3(await client2.forget(target, options));
4489
+ }
4490
+ default:
4491
+ return err3(`Unknown remember action: ${String(action)}`);
4492
+ }
4493
+ }
4250
4494
  async function handleCapture(client2, args) {
4251
4495
  const {
4252
4496
  action,
@@ -6430,6 +6674,66 @@ var init_tools = __esm({
6430
6674
  required: ["action"]
6431
6675
  }
6432
6676
  },
6677
+ {
6678
+ name: "mindgraph_remember",
6679
+ description: "Small-text memory fast path. 'remember' stores one fact, preference, decision or note synchronously; it is BM25- and vector-searchable the moment the call returns (the response says which). Pass a stable custom_id (e.g. 'pref:theme', 'deploy:target') so re-sending the same key UPDATES the same memory instead of creating a duplicate \u2014 that is the idempotency contract; without custom_id an exact duplicate is reused. 'forget' reversibly removes a memory by uid or custom_id (tombstone plus connected edges): call it with dry_run=true first to see the edge uids it would remove, then without. Undo with mindgraph_capture-independent server actions /evolve restore and restore_edge. Use mindgraph_capture for typed entities, sources and skills, and mindgraph_ingest for documents longer than a few paragraphs.",
6680
+ inputSchema: {
6681
+ type: "object",
6682
+ properties: {
6683
+ action: {
6684
+ type: "string",
6685
+ enum: ["remember", "forget"],
6686
+ description: "remember = store/upsert one small text; forget = reversible removal"
6687
+ },
6688
+ text: {
6689
+ type: "string",
6690
+ description: "remember: the memory text (\u2264 8 KiB). Use mindgraph_ingest for longer content."
6691
+ },
6692
+ custom_id: {
6693
+ type: "string",
6694
+ description: "Caller-owned key. remember: re-sending it edits the same memory (version bump, history kept). forget: the memory to remove."
6695
+ },
6696
+ label: {
6697
+ type: "string",
6698
+ description: "remember: optional short title; defaults to the first line of text"
6699
+ },
6700
+ uid: {
6701
+ type: "string",
6702
+ description: "forget: node uid to remove (alternative to custom_id)"
6703
+ },
6704
+ dry_run: {
6705
+ type: "boolean",
6706
+ description: "forget: preview the affected edge uids without changing anything"
6707
+ },
6708
+ cascade: {
6709
+ type: "boolean",
6710
+ description: "forget: also tombstone connected edges (default true)"
6711
+ },
6712
+ reason: {
6713
+ type: "string",
6714
+ description: "forget: why (recorded on the tombstone)"
6715
+ },
6716
+ space_uid: {
6717
+ type: "string",
6718
+ description: "remember: explicit target Space (default: the org space)"
6719
+ },
6720
+ on_near_duplicate: {
6721
+ type: "string",
6722
+ enum: ["reuse", "create"],
6723
+ description: "remember without custom_id: on an exact duplicate, reuse the existing memory (default) or create anyway"
6724
+ },
6725
+ props: {
6726
+ type: "object",
6727
+ description: "remember: extra Observation props"
6728
+ },
6729
+ agent_id: {
6730
+ type: "string",
6731
+ description: "Agent identifier (default: configured agent)"
6732
+ }
6733
+ },
6734
+ required: ["action"]
6735
+ }
6736
+ },
6433
6737
  MEMORY_TOOL,
6434
6738
  SYNC_TOOL,
6435
6739
  CODE_TOOL
@@ -6605,13 +6909,14 @@ async function handleGeneratedTool(client2, desc, args) {
6605
6909
  return err4(`unknown generated tool mapping: ${desc.maps_to}`);
6606
6910
  }
6607
6911
  } catch (e) {
6608
- return err4(e instanceof Error ? e.message : String(e));
6912
+ return toolError(e);
6609
6913
  }
6610
6914
  }
6611
6915
  var ok4, rendered, err4, CACHE_TTL_MS, FAILURE_COOLDOWN_MS, cache, retryNotBefore, EMPTY, DuplicateGeneratedToolError, STRUCTURED_QUERY_PASSTHROUGH;
6612
6916
  var init_generated_tools = __esm({
6613
6917
  "src/generated-tools.ts"() {
6614
6918
  "use strict";
6919
+ init_error_detail();
6615
6920
  init_render();
6616
6921
  ok4 = (data) => ({
6617
6922
  // Compact on purpose — pretty-printing doubled the token cost.
@@ -6671,6 +6976,7 @@ function unsupportedResponse(status, body) {
6671
6976
  function toolMutability(toolName, action) {
6672
6977
  if (toolName === "mindgraph_memory") return "read";
6673
6978
  if (toolName === "mindgraph_retrieve") return "read";
6979
+ if (toolName === "mindgraph_remember") return "write";
6674
6980
  if (toolName === "mindgraph_series_query") return "read";
6675
6981
  if (toolName === "mindgraph_plan") {
6676
6982
  return action && PLAN_READ_ACTIONS.has(action) ? "read" : "write";
@@ -7021,7 +7327,7 @@ async function main() {
7021
7327
  const transport = new import_stdio.StdioServerTransport();
7022
7328
  await server.connect(transport);
7023
7329
  console.error(
7024
- `MindGraph MCP server v${"0.20.0"} running on stdio (${BASE_URL})`
7330
+ `MindGraph MCP server v${"0.21.0"} running on stdio (${BASE_URL})`
7025
7331
  );
7026
7332
  }
7027
7333
  var import_server, import_stdio, import_types, import_node_path6, API_KEY, BASE_URL, AGENT_ID, ORG_ID, PROFILE, HARNESS, client, INSTRUCTIONS, RUNNER_REFRESH, HOOK_SKEW_NOTE, server, GOVERNANCE_CONFIG, MEMORY_RUNTIME_CONFIG, PROMPTS;
@@ -7073,11 +7379,13 @@ var init_src = __esm({
7073
7379
 
7074
7380
  **Answer from the bounded result.** Treat superseded, refuted, or out-of-validity items as warnings, not current facts. Retrieval proves only that the server returned context; it does not prove that a host automatically injected or used it.
7075
7381
 
7382
+ **Respect failed tool results.** An isError result is a failed operation, not evidence that the graph has no matches. Preserve its code and retry guidance. If retriable is false, do not automatically repeat the unchanged call or silently substitute another retrieval mode. Explain the failure; a narrower query or a later user-requested retry is a separate decision.
7383
+
7076
7384
  **Preserve selectively after material durable change.** Use the existing typed write tool for a fact, correction, decision, work-state transition, or reusable lesson. Do not write ephemeral conversation, routine mechanics, unsupported guesses, or information already represented accurately. An explicit no-write is the correct outcome when nothing durable changed.
7077
7385
 
7078
7386
  ## Topic retrieval details
7079
7387
 
7080
- Topic context is graph-only by default. Set \`include_chunks: true\` only for source text needed for quotes, citations, or missing detail. Query with a few proper nouns or technical terms rather than a full sentence. If keyword retrieval fails, use \`mindgraph_retrieve\` with \`semantic\` or \`hybrid\`; use \`document_index\` to inspect ingested sources.
7388
+ Topic context is graph-only by default. Set \`include_chunks: true\` only for source text needed for quotes, citations, or missing detail. Query with a few proper nouns or technical terms rather than a full sentence. If keyword retrieval succeeds but returns no matches, use \`mindgraph_retrieve\` with \`semantic\` or \`hybrid\`; use \`document_index\` to inspect ingested sources.
7081
7389
 
7082
7390
  ## Comprehensive read tools
7083
7391
 
@@ -7136,7 +7444,7 @@ Capture knowledge when the user shares something worth remembering:
7136
7444
  }
7137
7445
  RUNNER_REFRESH = (() => {
7138
7446
  try {
7139
- return refreshHookRunner("0.20.0", import_node_path6.default.join(__dirname, "cli.js"));
7447
+ return refreshHookRunner("0.21.0", import_node_path6.default.join(__dirname, "cli.js"));
7140
7448
  } catch {
7141
7449
  return void 0;
7142
7450
  }
@@ -7150,12 +7458,12 @@ Capture knowledge when the user shares something worth remembering:
7150
7458
  }
7151
7459
  })();
7152
7460
  console.error(
7153
- `[mindgraph-mcp] refreshed pinned hook runner ${RUNNER_REFRESH.runnerVersion ? `v${RUNNER_REFRESH.runnerVersion}` : "(pre-sidecar)"} \u2192 v${"0.20.0"}` + (refreshedSettings.length > 0 ? `; refreshed owned hook entries in ${refreshedSettings.join(", ")}` : "")
7461
+ `[mindgraph-mcp] refreshed pinned hook runner ${RUNNER_REFRESH.runnerVersion ? `v${RUNNER_REFRESH.runnerVersion}` : "(pre-sidecar)"} \u2192 v${"0.21.0"}` + (refreshedSettings.length > 0 ? `; refreshed owned hook entries in ${refreshedSettings.join(", ")}` : "")
7154
7462
  );
7155
7463
  }
7156
7464
  HOOK_SKEW_NOTE = (() => {
7157
7465
  try {
7158
- return versionSkewNote("0.20.0");
7466
+ return versionSkewNote("0.21.0");
7159
7467
  } catch {
7160
7468
  return void 0;
7161
7469
  }
@@ -7169,7 +7477,7 @@ ${HOOK_SKEW_NOTE}`;
7169
7477
  server = new import_server.Server(
7170
7478
  {
7171
7479
  name: "mindgraph",
7172
- version: "0.20.0"
7480
+ version: "0.21.0"
7173
7481
  },
7174
7482
  {
7175
7483
  capabilities: {
@@ -7194,7 +7502,7 @@ ${HOOK_SKEW_NOTE}`;
7194
7502
  }
7195
7503
  };
7196
7504
  MEMORY_RUNTIME_CONFIG = {
7197
- serverVersion: "0.20.0",
7505
+ serverVersion: "0.21.0",
7198
7506
  baseUrl: BASE_URL,
7199
7507
  profile: PROFILE,
7200
7508
  harness: HARNESS,
@@ -8961,7 +9269,7 @@ function withHooksFlag() {
8961
9269
  }
8962
9270
  function finishCodeInstall(apiKey, baseUrl, withHooks, agentId) {
8963
9271
  if (withHooks) {
8964
- const runner = installHookRunner(__filename, void 0, "0.20.0");
9272
+ const runner = installHookRunner(__filename, void 0, "0.21.0");
8965
9273
  console.log(`Installed pinned hook runner at ${runner}`);
8966
9274
  const result = installClaudeHooks("user");
8967
9275
  console.log(
@@ -9276,7 +9584,7 @@ ${total} skill(s) synced.`);
9276
9584
  if (hookHarness !== "claude-code" && hookHarness !== "codex") {
9277
9585
  throw new Error("--harness must be claude-code or codex");
9278
9586
  }
9279
- const runner = installHookRunner(__filename, void 0, "0.20.0");
9587
+ const runner = installHookRunner(__filename, void 0, "0.21.0");
9280
9588
  console.log(`Installed pinned hook runner at ${runner}`);
9281
9589
  const result = hookHarness === "codex" ? installCodexHooks(scope, projectDir) : installClaudeHooks(scope, projectDir);
9282
9590
  console.log(
package/dist/index.js CHANGED
@@ -89,6 +89,22 @@ function conflictState(e) {
89
89
  }
90
90
  return lifted;
91
91
  }
92
+ function toolError(e, fallbackCode) {
93
+ const body = e instanceof import_mindgraph.MindGraphError && e.body && typeof e.body === "object" && !Array.isArray(e.body) ? e.body : {};
94
+ const code = typeof body.code === "string" ? body.code : fallbackCode;
95
+ const payload = {
96
+ error: errorDetail(e),
97
+ ...conflictState(e),
98
+ ...e instanceof import_mindgraph.MindGraphError ? { status: e.status } : {},
99
+ ...code !== void 0 ? { code } : {},
100
+ ...typeof body.retriable === "boolean" ? { retriable: body.retriable } : {}
101
+ };
102
+ return {
103
+ content: [{ type: "text", text: JSON.stringify(payload) }],
104
+ structuredContent: payload,
105
+ isError: true
106
+ };
107
+ }
92
108
 
93
109
  // src/codegraph.ts
94
110
  var import_node_child_process = require("child_process");
@@ -1086,7 +1102,7 @@ async function handleCodeTool(client2, args, adapter = new CodegraphAdapter({ cw
1086
1102
  if (cause instanceof UnknownRepositoryError) {
1087
1103
  return err("unknown_repository", cause.message);
1088
1104
  }
1089
- return err("code_tool_failed", errorDetail(cause));
1105
+ return toolError(cause, "code_tool_failed");
1090
1106
  }
1091
1107
  }
1092
1108
  async function attachCodeRefsToToolResult(client2, result, args, adapter) {
@@ -1558,7 +1574,7 @@ async function handleSyncTool(client2, args) {
1558
1574
  }
1559
1575
  return ok2(await remote.memorySync(request));
1560
1576
  } catch (error2) {
1561
- return err2("sync_failed", errorDetail(error2));
1577
+ return toolError(error2, "sync_failed");
1562
1578
  }
1563
1579
  }
1564
1580
  async function scanRepository(remote, repository, requested, provider, agentId) {
@@ -3919,6 +3935,66 @@ var TOOLS = [
3919
3935
  required: ["action"]
3920
3936
  }
3921
3937
  },
3938
+ {
3939
+ name: "mindgraph_remember",
3940
+ description: "Small-text memory fast path. 'remember' stores one fact, preference, decision or note synchronously; it is BM25- and vector-searchable the moment the call returns (the response says which). Pass a stable custom_id (e.g. 'pref:theme', 'deploy:target') so re-sending the same key UPDATES the same memory instead of creating a duplicate \u2014 that is the idempotency contract; without custom_id an exact duplicate is reused. 'forget' reversibly removes a memory by uid or custom_id (tombstone plus connected edges): call it with dry_run=true first to see the edge uids it would remove, then without. Undo with mindgraph_capture-independent server actions /evolve restore and restore_edge. Use mindgraph_capture for typed entities, sources and skills, and mindgraph_ingest for documents longer than a few paragraphs.",
3941
+ inputSchema: {
3942
+ type: "object",
3943
+ properties: {
3944
+ action: {
3945
+ type: "string",
3946
+ enum: ["remember", "forget"],
3947
+ description: "remember = store/upsert one small text; forget = reversible removal"
3948
+ },
3949
+ text: {
3950
+ type: "string",
3951
+ description: "remember: the memory text (\u2264 8 KiB). Use mindgraph_ingest for longer content."
3952
+ },
3953
+ custom_id: {
3954
+ type: "string",
3955
+ description: "Caller-owned key. remember: re-sending it edits the same memory (version bump, history kept). forget: the memory to remove."
3956
+ },
3957
+ label: {
3958
+ type: "string",
3959
+ description: "remember: optional short title; defaults to the first line of text"
3960
+ },
3961
+ uid: {
3962
+ type: "string",
3963
+ description: "forget: node uid to remove (alternative to custom_id)"
3964
+ },
3965
+ dry_run: {
3966
+ type: "boolean",
3967
+ description: "forget: preview the affected edge uids without changing anything"
3968
+ },
3969
+ cascade: {
3970
+ type: "boolean",
3971
+ description: "forget: also tombstone connected edges (default true)"
3972
+ },
3973
+ reason: {
3974
+ type: "string",
3975
+ description: "forget: why (recorded on the tombstone)"
3976
+ },
3977
+ space_uid: {
3978
+ type: "string",
3979
+ description: "remember: explicit target Space (default: the org space)"
3980
+ },
3981
+ on_near_duplicate: {
3982
+ type: "string",
3983
+ enum: ["reuse", "create"],
3984
+ description: "remember without custom_id: on an exact duplicate, reuse the existing memory (default) or create anyway"
3985
+ },
3986
+ props: {
3987
+ type: "object",
3988
+ description: "remember: extra Observation props"
3989
+ },
3990
+ agent_id: {
3991
+ type: "string",
3992
+ description: "Agent identifier (default: configured agent)"
3993
+ }
3994
+ },
3995
+ required: ["action"]
3996
+ }
3997
+ },
3922
3998
  MEMORY_TOOL,
3923
3999
  SYNC_TOOL,
3924
4000
  CODE_TOOL
@@ -4004,6 +4080,9 @@ async function handleTool(client2, name, args, memoryConfig = {}) {
4004
4080
  case "mindgraph_memory":
4005
4081
  result = await handleMemoryTool(client2, args, memoryConfig);
4006
4082
  break;
4083
+ case "mindgraph_remember":
4084
+ result = await handleRemember(client2, args);
4085
+ break;
4007
4086
  case "mindgraph_series_query":
4008
4087
  result = await handleSeriesQuery(client2, args);
4009
4088
  break;
@@ -4036,19 +4115,7 @@ async function handleTool(client2, name, args, memoryConfig = {}) {
4036
4115
  ].includes(action);
4037
4116
  return attach ? await attachCodeRefsToToolResult(client2, result, args) : result;
4038
4117
  } catch (e) {
4039
- const lifted = conflictState(e);
4040
- if (Object.keys(lifted).length > 0) {
4041
- return {
4042
- content: [
4043
- {
4044
- type: "text",
4045
- text: JSON.stringify({ error: errorDetail(e), ...lifted })
4046
- }
4047
- ],
4048
- isError: true
4049
- };
4050
- }
4051
- return err3(errorDetail(e));
4118
+ return toolError(e);
4052
4119
  }
4053
4120
  }
4054
4121
  async function handleSeriesQuery(client2, args) {
@@ -4072,6 +4139,56 @@ async function handleSeriesQuery(client2, args) {
4072
4139
  await client2.series(args)
4073
4140
  );
4074
4141
  }
4142
+ async function handleRemember(client2, args) {
4143
+ const {
4144
+ action,
4145
+ text: text2,
4146
+ custom_id,
4147
+ label,
4148
+ uid,
4149
+ dry_run,
4150
+ cascade,
4151
+ reason,
4152
+ space_uid,
4153
+ on_near_duplicate,
4154
+ props,
4155
+ agent_id,
4156
+ // Adapter-owned; consumed locally, never sent to the API.
4157
+ invocation_context: _invocationContext,
4158
+ ...rest
4159
+ } = args;
4160
+ void _invocationContext;
4161
+ void rest;
4162
+ switch (action) {
4163
+ case "remember": {
4164
+ if (typeof text2 !== "string" || !text2.trim()) {
4165
+ return err3("remember requires non-empty text");
4166
+ }
4167
+ const options = {};
4168
+ if (typeof custom_id === "string" && custom_id.trim()) options.custom_id = custom_id.trim();
4169
+ if (typeof label === "string" && label.trim()) options.label = label;
4170
+ if (typeof space_uid === "string" && space_uid.trim()) options.space_uid = space_uid;
4171
+ if (on_near_duplicate === "reuse" || on_near_duplicate === "create") {
4172
+ options.on_near_duplicate = on_near_duplicate;
4173
+ }
4174
+ if (props && typeof props === "object") options.props = props;
4175
+ if (typeof agent_id === "string") options.agent_id = agent_id;
4176
+ return ok3(await client2.remember(text2, options));
4177
+ }
4178
+ case "forget": {
4179
+ const target = typeof uid === "string" && uid.trim() ? { uid: uid.trim() } : typeof custom_id === "string" && custom_id.trim() ? { custom_id: custom_id.trim() } : null;
4180
+ if (!target) return err3("forget requires uid or custom_id");
4181
+ const options = {};
4182
+ if (dry_run === true) options.dry_run = true;
4183
+ if (typeof cascade === "boolean") options.cascade = cascade;
4184
+ if (typeof reason === "string" && reason.trim()) options.reason = reason;
4185
+ if (typeof agent_id === "string") options.agent_id = agent_id;
4186
+ return ok3(await client2.forget(target, options));
4187
+ }
4188
+ default:
4189
+ return err3(`Unknown remember action: ${String(action)}`);
4190
+ }
4191
+ }
4075
4192
  async function handleCapture(client2, args) {
4076
4193
  const {
4077
4194
  action,
@@ -5346,7 +5463,7 @@ async function handleGeneratedTool(client2, desc, args) {
5346
5463
  return err4(`unknown generated tool mapping: ${desc.maps_to}`);
5347
5464
  }
5348
5465
  } catch (e) {
5349
- return err4(e instanceof Error ? e.message : String(e));
5466
+ return toolError(e);
5350
5467
  }
5351
5468
  }
5352
5469
 
@@ -5398,6 +5515,7 @@ var ONTOLOGY_READ_ACTIONS = /* @__PURE__ */ new Set([
5398
5515
  function toolMutability(toolName, action) {
5399
5516
  if (toolName === "mindgraph_memory") return "read";
5400
5517
  if (toolName === "mindgraph_retrieve") return "read";
5518
+ if (toolName === "mindgraph_remember") return "write";
5401
5519
  if (toolName === "mindgraph_series_query") return "read";
5402
5520
  if (toolName === "mindgraph_plan") {
5403
5521
  return action && PLAN_READ_ACTIONS.has(action) ? "read" : "write";
@@ -5958,11 +6076,13 @@ var INSTRUCTIONS = `You have access to a persistent knowledge graph via MindGrap
5958
6076
 
5959
6077
  **Answer from the bounded result.** Treat superseded, refuted, or out-of-validity items as warnings, not current facts. Retrieval proves only that the server returned context; it does not prove that a host automatically injected or used it.
5960
6078
 
6079
+ **Respect failed tool results.** An isError result is a failed operation, not evidence that the graph has no matches. Preserve its code and retry guidance. If retriable is false, do not automatically repeat the unchanged call or silently substitute another retrieval mode. Explain the failure; a narrower query or a later user-requested retry is a separate decision.
6080
+
5961
6081
  **Preserve selectively after material durable change.** Use the existing typed write tool for a fact, correction, decision, work-state transition, or reusable lesson. Do not write ephemeral conversation, routine mechanics, unsupported guesses, or information already represented accurately. An explicit no-write is the correct outcome when nothing durable changed.
5962
6082
 
5963
6083
  ## Topic retrieval details
5964
6084
 
5965
- Topic context is graph-only by default. Set \`include_chunks: true\` only for source text needed for quotes, citations, or missing detail. Query with a few proper nouns or technical terms rather than a full sentence. If keyword retrieval fails, use \`mindgraph_retrieve\` with \`semantic\` or \`hybrid\`; use \`document_index\` to inspect ingested sources.
6085
+ Topic context is graph-only by default. Set \`include_chunks: true\` only for source text needed for quotes, citations, or missing detail. Query with a few proper nouns or technical terms rather than a full sentence. If keyword retrieval succeeds but returns no matches, use \`mindgraph_retrieve\` with \`semantic\` or \`hybrid\`; use \`document_index\` to inspect ingested sources.
5966
6086
 
5967
6087
  ## Comprehensive read tools
5968
6088
 
@@ -6021,7 +6141,7 @@ if (PROFILE === "coding") {
6021
6141
  }
6022
6142
  var RUNNER_REFRESH = (() => {
6023
6143
  try {
6024
- return refreshHookRunner("0.20.0", import_node_path5.default.join(__dirname, "cli.js"));
6144
+ return refreshHookRunner("0.21.0", import_node_path5.default.join(__dirname, "cli.js"));
6025
6145
  } catch {
6026
6146
  return void 0;
6027
6147
  }
@@ -6035,12 +6155,12 @@ if (RUNNER_REFRESH?.refreshed) {
6035
6155
  }
6036
6156
  })();
6037
6157
  console.error(
6038
- `[mindgraph-mcp] refreshed pinned hook runner ${RUNNER_REFRESH.runnerVersion ? `v${RUNNER_REFRESH.runnerVersion}` : "(pre-sidecar)"} \u2192 v${"0.20.0"}` + (refreshedSettings.length > 0 ? `; refreshed owned hook entries in ${refreshedSettings.join(", ")}` : "")
6158
+ `[mindgraph-mcp] refreshed pinned hook runner ${RUNNER_REFRESH.runnerVersion ? `v${RUNNER_REFRESH.runnerVersion}` : "(pre-sidecar)"} \u2192 v${"0.21.0"}` + (refreshedSettings.length > 0 ? `; refreshed owned hook entries in ${refreshedSettings.join(", ")}` : "")
6039
6159
  );
6040
6160
  }
6041
6161
  var HOOK_SKEW_NOTE = (() => {
6042
6162
  try {
6043
- return versionSkewNote("0.20.0");
6163
+ return versionSkewNote("0.21.0");
6044
6164
  } catch {
6045
6165
  return void 0;
6046
6166
  }
@@ -6054,7 +6174,7 @@ ${HOOK_SKEW_NOTE}`;
6054
6174
  var server = new import_server.Server(
6055
6175
  {
6056
6176
  name: "mindgraph",
6057
- version: "0.20.0"
6177
+ version: "0.21.0"
6058
6178
  },
6059
6179
  {
6060
6180
  capabilities: {
@@ -6079,7 +6199,7 @@ var GOVERNANCE_CONFIG = {
6079
6199
  }
6080
6200
  };
6081
6201
  var MEMORY_RUNTIME_CONFIG = {
6082
- serverVersion: "0.20.0",
6202
+ serverVersion: "0.21.0",
6083
6203
  baseUrl: BASE_URL,
6084
6204
  profile: PROFILE,
6085
6205
  harness: HARNESS,
@@ -6281,7 +6401,7 @@ async function main() {
6281
6401
  const transport = new import_stdio.StdioServerTransport();
6282
6402
  await server.connect(transport);
6283
6403
  console.error(
6284
- `MindGraph MCP server v${"0.20.0"} running on stdio (${BASE_URL})`
6404
+ `MindGraph MCP server v${"0.21.0"} running on stdio (${BASE_URL})`
6285
6405
  );
6286
6406
  }
6287
6407
  main().catch((error2) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mindgraph-mcp",
3
- "version": "0.20.0",
3
+ "version": "0.21.0",
4
4
  "description": "MCP server for MindGraph — connect Claude Desktop, Claude Code, and any MCP client to your knowledge graph",
5
5
  "repository": {
6
6
  "type": "git",
@@ -32,7 +32,7 @@
32
32
  "license": "MIT",
33
33
  "dependencies": {
34
34
  "@modelcontextprotocol/sdk": "^1.12.1",
35
- "mindgraph": "^0.15.0"
35
+ "mindgraph": "^0.16.0"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/node": "^22.0.0",