@agentmemory/agentmemory 0.9.26 → 0.9.27

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/dist/index.mjs CHANGED
@@ -119,10 +119,12 @@ function detectProvider(env) {
119
119
  function loadConfig() {
120
120
  const env = getMergedEnv();
121
121
  const provider = detectProvider(env);
122
+ const restPort = parseInt(env["III_REST_PORT"] || "3111", 10) || 3111;
123
+ const streamsPort = parseInt(env["III_STREAM_PORT"] || env["III_STREAMS_PORT"] || "", 10) || restPort + 1;
122
124
  return {
123
- engineUrl: env["III_ENGINE_URL"] || "ws://localhost:49134",
124
- restPort: parseInt(env["III_REST_PORT"] || "3111", 10) || 3111,
125
- streamsPort: parseInt(env["III_STREAMS_PORT"] || "3112", 10) || 3112,
125
+ engineUrl: env["III_ENGINE_URL"] || `ws://localhost:${parseInt(env["III_ENGINE_PORT"] || "", 10) || restPort + 46023}`,
126
+ restPort,
127
+ streamsPort,
126
128
  provider,
127
129
  tokenBudget: safeParseInt(env["TOKEN_BUDGET"], 2e3),
128
130
  maxObservationsPerSession: safeParseInt(env["MAX_OBS_PER_SESSION"], 500),
@@ -1326,6 +1328,10 @@ const KV = {
1326
1328
  claudeBridge: "mem:claude-bridge",
1327
1329
  graphNodes: "mem:graph:nodes",
1328
1330
  graphEdges: "mem:graph:edges",
1331
+ graphSnapshot: "mem:graph:snapshot",
1332
+ graphNameIndex: "mem:graph:name-index",
1333
+ graphEdgeKey: "mem:graph:edge-key",
1334
+ graphNodeDegree: "mem:graph:node-degree",
1329
1335
  semantic: "mem:semantic",
1330
1336
  procedural: "mem:procedural",
1331
1337
  teamShared: (teamId) => `mem:team:${teamId}:shared`,
@@ -3482,6 +3488,12 @@ function registerSearchFunction(sdk, kv) {
3482
3488
  }
3483
3489
  const projectFilter = typeof data.project === "string" && data.project.trim().length > 0 ? data.project.trim() : void 0;
3484
3490
  const cwdFilter = typeof data.cwd === "string" && data.cwd.trim().length > 0 ? data.cwd.trim() : void 0;
3491
+ const isolated = isAgentScopeIsolated();
3492
+ const explicitAgentId = typeof data.agentId === "string" && data.agentId.trim().length > 0 ? data.agentId.trim() : void 0;
3493
+ const wildcardAgent = explicitAgentId === "*";
3494
+ const envAgentId = isolated ? getAgentId() : void 0;
3495
+ const filterAgentId = wildcardAgent ? void 0 : explicitAgentId ?? envAgentId;
3496
+ if (isolated && !wildcardAgent && !explicitAgentId && !envAgentId) throw new Error("mem::search: AGENTMEMORY_AGENT_SCOPE=isolated is set but no agent id is available (env AGENT_ID unset and no explicit agentId in the call). Refusing to read cross-agent rows. Pass agentId: \"*\" to opt in to a wildcard read.");
3485
3497
  const format = typeof data.format === "string" ? data.format : "full";
3486
3498
  if (![
3487
3499
  "full",
@@ -3497,7 +3509,7 @@ function registerSearchFunction(sdk, kv) {
3497
3509
  const count = await rebuildIndex(kv);
3498
3510
  logger.info("Search index rebuilt", { entries: count });
3499
3511
  }
3500
- const filtering = !!(projectFilter || cwdFilter);
3512
+ const filtering = !!(projectFilter || cwdFilter || filterAgentId);
3501
3513
  const fetchLimit = filtering ? Math.max(effectiveLimit * 10, 100) : effectiveLimit;
3502
3514
  const results = idx.search(query, fetchLimit);
3503
3515
  const sessionCache = /* @__PURE__ */ new Map();
@@ -3514,9 +3526,10 @@ function registerSearchFunction(sdk, kv) {
3514
3526
  memoryProjectCache.set(obsId, proj);
3515
3527
  return proj;
3516
3528
  };
3529
+ const earlyCap = filterAgentId ? fetchLimit : effectiveLimit;
3517
3530
  const candidates = [];
3518
3531
  for (const r of results) {
3519
- if (candidates.length >= effectiveLimit) break;
3532
+ if (candidates.length >= earlyCap) break;
3520
3533
  if (filtering) {
3521
3534
  const s = await loadSession(r.sessionId);
3522
3535
  if (s) {
@@ -3538,7 +3551,10 @@ function registerSearchFunction(sdk, kv) {
3538
3551
  const enriched = [];
3539
3552
  for (let i = 0; i < candidates.length; i++) {
3540
3553
  const obs = obsResults[i];
3541
- if (obs) enriched.push({
3554
+ if (!obs) continue;
3555
+ if (filterAgentId !== void 0 && obs.agentId !== filterAgentId) continue;
3556
+ if (enriched.length >= effectiveLimit) break;
3557
+ enriched.push({
3542
3558
  observation: obs,
3543
3559
  score: candidates[i].score,
3544
3560
  sessionId: candidates[i].sessionId
@@ -6688,7 +6704,7 @@ async function findByKeyword(kv, keyword, project) {
6688
6704
  }
6689
6705
  //#endregion
6690
6706
  //#region src/version.ts
6691
- const VERSION = "0.9.26";
6707
+ const VERSION = "0.9.27";
6692
6708
  //#endregion
6693
6709
  //#region src/telemetry/setup.ts
6694
6710
  const OTEL_CONFIG = {
@@ -6763,7 +6779,10 @@ function registerSmartSearchFunction(sdk, kv, searchFn) {
6763
6779
  sdk.registerFunction("mem::smart-search", async (data) => {
6764
6780
  const isolated = isAgentScopeIsolated();
6765
6781
  const explicitAgentId = typeof data.agentId === "string" && data.agentId.trim().length > 0 ? data.agentId.trim() : void 0;
6766
- const filterAgentId = explicitAgentId === "*" ? void 0 : explicitAgentId ?? (isolated ? getAgentId() : void 0);
6782
+ const wildcardAgent = explicitAgentId === "*";
6783
+ const envAgentId = isolated ? getAgentId() : void 0;
6784
+ const filterAgentId = wildcardAgent ? void 0 : explicitAgentId ?? envAgentId;
6785
+ if (isolated && !wildcardAgent && !explicitAgentId && !envAgentId) throw new Error("mem::smart-search: AGENTMEMORY_AGENT_SCOPE=isolated is set but no agent id is available (env AGENT_ID unset and no explicit agentId in the call). Refusing to read cross-agent rows. Pass agentId: \"*\" to opt in to a wildcard read.");
6767
6786
  if (data.expandIds && data.expandIds.length > 0) {
6768
6787
  const raw = data.expandIds.slice(0, 20);
6769
6788
  const items = raw.map((entry) => {
@@ -7314,7 +7333,8 @@ function registerExportImportFunction(sdk, kv) {
7314
7333
  "0.9.23",
7315
7334
  "0.9.24",
7316
7335
  "0.9.25",
7317
- "0.9.26"
7336
+ "0.9.26",
7337
+ "0.9.27"
7318
7338
  ]).has(importData.version)) return {
7319
7339
  success: false,
7320
7340
  error: `Unsupported export version: ${importData.version}`
@@ -7838,19 +7858,167 @@ function buildGraphExtractionPrompt(observations) {
7838
7858
  //#region src/functions/graph.ts
7839
7859
  const DEFAULT_GRAPH_QUERY_LIMIT = 500;
7840
7860
  const MAX_GRAPH_QUERY_LIMIT = 5e3;
7841
- function resolvePagination(rawLimit, rawOffset) {
7861
+ const SNAPSHOT_TOP_NODES = DEFAULT_GRAPH_QUERY_LIMIT;
7862
+ const SNAPSHOT_KEY = "current";
7863
+ const LIVE_ENUMERATION_BUDGET_MS = 6e3;
7864
+ function withTimeout(p, ms, label) {
7865
+ return new Promise((resolve, reject) => {
7866
+ const t = setTimeout(() => reject(/* @__PURE__ */ new Error(`${label}: exceeded ${ms}ms budget`)), ms);
7867
+ p.then((v) => {
7868
+ clearTimeout(t);
7869
+ resolve(v);
7870
+ }, (err) => {
7871
+ clearTimeout(t);
7872
+ reject(err);
7873
+ });
7874
+ });
7875
+ }
7876
+ function emptySnapshot() {
7842
7877
  return {
7843
- limit: Math.max(1, Math.min(typeof rawLimit === "number" && Number.isFinite(rawLimit) ? Math.floor(rawLimit) : DEFAULT_GRAPH_QUERY_LIMIT, MAX_GRAPH_QUERY_LIMIT)),
7844
- offset: Math.max(0, typeof rawOffset === "number" && Number.isFinite(rawOffset) ? Math.floor(rawOffset) : 0)
7878
+ version: 1,
7879
+ topNodes: [],
7880
+ topEdges: [],
7881
+ topDegrees: {},
7882
+ stats: {
7883
+ totalNodes: 0,
7884
+ totalEdges: 0,
7885
+ nodesByType: {},
7886
+ edgesByType: {}
7887
+ },
7888
+ updatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
7889
+ dirty: true
7845
7890
  };
7846
7891
  }
7847
- function rankByDegree(nodes, edges) {
7892
+ async function readSnapshot(kv) {
7893
+ try {
7894
+ const snap = await kv.get(KV.graphSnapshot, SNAPSHOT_KEY);
7895
+ if (snap && typeof snap === "object" && snap.version === 1) return snap;
7896
+ return null;
7897
+ } catch (err) {
7898
+ logger.warn("Graph snapshot read failed", { error: err instanceof Error ? err.message : String(err) });
7899
+ return null;
7900
+ }
7901
+ }
7902
+ function buildSnapshotFromArrays(nodes, edges) {
7903
+ const liveNodes = nodes.filter((n) => !n.stale);
7904
+ const liveEdges = edges.filter((e) => !e.stale);
7848
7905
  const degree = /* @__PURE__ */ new Map();
7849
- for (const edge of edges) {
7850
- degree.set(edge.sourceNodeId, (degree.get(edge.sourceNodeId) ?? 0) + 1);
7851
- degree.set(edge.targetNodeId, (degree.get(edge.targetNodeId) ?? 0) + 1);
7906
+ for (const e of liveEdges) {
7907
+ degree.set(e.sourceNodeId, (degree.get(e.sourceNodeId) ?? 0) + 1);
7908
+ degree.set(e.targetNodeId, (degree.get(e.targetNodeId) ?? 0) + 1);
7909
+ }
7910
+ const ranked = [...liveNodes].sort((a, b) => (degree.get(b.id) ?? 0) - (degree.get(a.id) ?? 0)).slice(0, SNAPSHOT_TOP_NODES);
7911
+ const rankedIds = new Set(ranked.map((n) => n.id));
7912
+ const topEdges = liveEdges.filter((e) => rankedIds.has(e.sourceNodeId) && rankedIds.has(e.targetNodeId));
7913
+ const topDegrees = {};
7914
+ for (const n of ranked) topDegrees[n.id] = degree.get(n.id) ?? 0;
7915
+ const nodesByType = {};
7916
+ for (const n of liveNodes) nodesByType[n.type] = (nodesByType[n.type] || 0) + 1;
7917
+ const edgesByType = {};
7918
+ for (const e of liveEdges) edgesByType[e.type] = (edgesByType[e.type] || 0) + 1;
7919
+ return {
7920
+ version: 1,
7921
+ topNodes: ranked,
7922
+ topEdges,
7923
+ topDegrees,
7924
+ stats: {
7925
+ totalNodes: liveNodes.length,
7926
+ totalEdges: liveEdges.length,
7927
+ nodesByType,
7928
+ edgesByType
7929
+ },
7930
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
7931
+ dirty: false
7932
+ };
7933
+ }
7934
+ function paginateFromSnapshot(snap, filterType, limit, offset) {
7935
+ const filteredNodes = filterType ? snap.topNodes.filter((n) => n.type === filterType) : snap.topNodes;
7936
+ const total = filterType ? snap.stats.nodesByType[filterType] ?? 0 : snap.stats.totalNodes;
7937
+ const pageNodes = filteredNodes.slice(offset, offset + limit);
7938
+ const pageIds = new Set(pageNodes.map((n) => n.id));
7939
+ return {
7940
+ nodes: pageNodes,
7941
+ edges: snap.topEdges.filter((e) => pageIds.has(e.sourceNodeId) && pageIds.has(e.targetNodeId)),
7942
+ depth: 0,
7943
+ totalNodes: total,
7944
+ totalEdges: snap.stats.totalEdges,
7945
+ truncated: total > pageNodes.length,
7946
+ limit,
7947
+ offset,
7948
+ fromSnapshot: true
7949
+ };
7950
+ }
7951
+ const REBUILD_SAFE_NODE_CEILING = 25e3;
7952
+ function nameIndexKey(type, name) {
7953
+ return `${type}|${name}`;
7954
+ }
7955
+ function edgeIndexKey(sourceNodeId, targetNodeId, type) {
7956
+ return `${sourceNodeId}|${targetNodeId}|${type}`;
7957
+ }
7958
+ async function applyDegreeDelta(kv, snap, nodeId, delta) {
7959
+ const prev = await kv.get(KV.graphNodeDegree, nodeId) ?? 0;
7960
+ const next = Math.max(0, prev + delta);
7961
+ await kv.set(KV.graphNodeDegree, nodeId, next);
7962
+ if (snap.topNodes.findIndex((n) => n.id === nodeId) !== -1) {
7963
+ snap.topDegrees[nodeId] = next;
7964
+ snap.topNodes.sort((a, b) => (snap.topDegrees[b.id] ?? 0) - (snap.topDegrees[a.id] ?? 0));
7965
+ return next;
7966
+ }
7967
+ if (snap.topNodes.length < SNAPSHOT_TOP_NODES) {
7968
+ const node = await kv.get(KV.graphNodes, nodeId);
7969
+ if (node && !node.stale) {
7970
+ snap.topNodes.push(node);
7971
+ snap.topDegrees[node.id] = next;
7972
+ snap.topNodes.sort((a, b) => (snap.topDegrees[b.id] ?? 0) - (snap.topDegrees[a.id] ?? 0));
7973
+ }
7974
+ return next;
7975
+ }
7976
+ const tailEntry = snap.topNodes[snap.topNodes.length - 1];
7977
+ if (!tailEntry) return next;
7978
+ if (next > (snap.topDegrees[tailEntry.id] ?? 0)) {
7979
+ const node = await kv.get(KV.graphNodes, nodeId);
7980
+ if (node && !node.stale) {
7981
+ const evicted = snap.topNodes.pop();
7982
+ if (evicted) delete snap.topDegrees[evicted.id];
7983
+ snap.topNodes.push(node);
7984
+ snap.topDegrees[node.id] = next;
7985
+ snap.topNodes.sort((a, b) => (snap.topDegrees[b.id] ?? 0) - (snap.topDegrees[a.id] ?? 0));
7986
+ }
7987
+ }
7988
+ return next;
7989
+ }
7990
+ function snapshotPushEdgeIfBothInTop(snap, edge) {
7991
+ const topIds = new Set(snap.topNodes.map((n) => n.id));
7992
+ if (topIds.has(edge.sourceNodeId) && topIds.has(edge.targetNodeId)) {
7993
+ if (!snap.topEdges.find((e) => e.id === edge.id)) snap.topEdges.push(edge);
7852
7994
  }
7853
- return [...nodes].sort((a, b) => (degree.get(b.id) ?? 0) - (degree.get(a.id) ?? 0));
7995
+ }
7996
+ function mergeNode(existing, incoming, obsIds, capturedAt) {
7997
+ return {
7998
+ ...existing,
7999
+ sourceObservationIds: [...new Set([
8000
+ ...existing.sourceObservationIds,
8001
+ ...incoming.sourceObservationIds,
8002
+ ...obsIds
8003
+ ])],
8004
+ properties: {
8005
+ ...existing.properties,
8006
+ ...incoming.properties
8007
+ },
8008
+ updatedAt: capturedAt
8009
+ };
8010
+ }
8011
+ function mergeEdge(existing, obsIds) {
8012
+ return {
8013
+ ...existing,
8014
+ sourceObservationIds: [...new Set([...existing.sourceObservationIds, ...obsIds])]
8015
+ };
8016
+ }
8017
+ function resolvePagination(rawLimit, rawOffset) {
8018
+ return {
8019
+ limit: Math.max(1, Math.min(typeof rawLimit === "number" && Number.isFinite(rawLimit) ? Math.floor(rawLimit) : DEFAULT_GRAPH_QUERY_LIMIT, MAX_GRAPH_QUERY_LIMIT)),
8020
+ offset: Math.max(0, typeof rawOffset === "number" && Number.isFinite(rawOffset) ? Math.floor(rawOffset) : 0)
8021
+ };
7854
8022
  }
7855
8023
  function paginate(nodes, allEdges, depth, limit, offset) {
7856
8024
  const totalNodes = nodes.length;
@@ -7947,45 +8115,76 @@ function registerGraphFunction(sdk, kv, provider) {
7947
8115
  const response = await provider.compress(GRAPH_EXTRACTION_SYSTEM, prompt);
7948
8116
  const obsIds = data.observations.map((o) => o.id);
7949
8117
  const { nodes, edges } = parseGraphXml(response, obsIds);
7950
- const existingNodes = await kv.list(KV.graphNodes);
7951
- const existingEdges = await kv.list(KV.graphEdges);
8118
+ const snap = await readSnapshot(kv) ?? emptySnapshot();
8119
+ const capturedAt = (/* @__PURE__ */ new Date()).toISOString();
8120
+ let newNodeCount = 0;
8121
+ let newEdgeCount = 0;
8122
+ const newEdgesForTopCheck = [];
7952
8123
  for (const node of nodes) {
7953
- const existing = existingNodes.find((n) => n.name === node.name && n.type === node.type);
8124
+ const indexKey = nameIndexKey(node.type, node.name);
8125
+ const existingId = await kv.get(KV.graphNameIndex, indexKey);
8126
+ let existing = null;
8127
+ if (existingId) {
8128
+ existing = await kv.get(KV.graphNodes, existingId);
8129
+ if (existing && snap.resetAt && typeof existing.createdAt === "string" && existing.createdAt < snap.resetAt) existing = null;
8130
+ }
7954
8131
  if (existing) {
7955
- const merged = {
7956
- ...existing,
7957
- sourceObservationIds: [...new Set([...existing.sourceObservationIds, ...obsIds])],
7958
- properties: {
7959
- ...existing.properties,
7960
- ...node.properties
7961
- }
7962
- };
8132
+ const merged = mergeNode(existing, node, obsIds, capturedAt);
7963
8133
  await kv.set(KV.graphNodes, existing.id, merged);
7964
- const idx = existingNodes.findIndex((n) => n.id === existing.id);
7965
- if (idx !== -1) existingNodes[idx] = merged;
8134
+ const topIdx = snap.topNodes.findIndex((n) => n.id === existing.id);
8135
+ if (topIdx !== -1) snap.topNodes[topIdx] = merged;
7966
8136
  } else {
7967
8137
  await kv.set(KV.graphNodes, node.id, node);
7968
- existingNodes.push(node);
8138
+ await kv.set(KV.graphNameIndex, indexKey, node.id);
8139
+ await kv.set(KV.graphNodeDegree, node.id, 0);
8140
+ snap.stats.totalNodes += 1;
8141
+ snap.stats.nodesByType[node.type] = (snap.stats.nodesByType[node.type] ?? 0) + 1;
8142
+ newNodeCount += 1;
8143
+ if (snap.topNodes.length < SNAPSHOT_TOP_NODES) {
8144
+ snap.topNodes.push(node);
8145
+ snap.topDegrees[node.id] = 0;
8146
+ }
7969
8147
  }
7970
8148
  }
7971
8149
  for (const edge of edges) {
7972
- const edgeKey = `${edge.sourceNodeId}|${edge.targetNodeId}|${edge.type}`;
7973
- const existingEdge = existingEdges.find((e) => `${e.sourceNodeId}|${e.targetNodeId}|${e.type}` === edgeKey);
7974
- if (existingEdge) {
7975
- existingEdge.sourceObservationIds = [...new Set([...existingEdge.sourceObservationIds, ...obsIds])];
7976
- await kv.set(KV.graphEdges, existingEdge.id, existingEdge);
8150
+ const eKey = edgeIndexKey(edge.sourceNodeId, edge.targetNodeId, edge.type);
8151
+ const existingId = await kv.get(KV.graphEdgeKey, eKey);
8152
+ let existing = null;
8153
+ if (existingId) {
8154
+ existing = await kv.get(KV.graphEdges, existingId);
8155
+ if (existing && snap.resetAt && typeof existing.createdAt === "string" && existing.createdAt < snap.resetAt) existing = null;
8156
+ }
8157
+ if (existing) {
8158
+ const merged = mergeEdge(existing, obsIds);
8159
+ await kv.set(KV.graphEdges, existing.id, merged);
8160
+ const topIdx = snap.topEdges.findIndex((e) => e.id === existing.id);
8161
+ if (topIdx !== -1) snap.topEdges[topIdx] = merged;
7977
8162
  } else {
7978
8163
  await kv.set(KV.graphEdges, edge.id, edge);
7979
- existingEdges.push(edge);
8164
+ await kv.set(KV.graphEdgeKey, eKey, edge.id);
8165
+ snap.stats.totalEdges += 1;
8166
+ snap.stats.edgesByType[edge.type] = (snap.stats.edgesByType[edge.type] ?? 0) + 1;
8167
+ newEdgeCount += 1;
8168
+ await applyDegreeDelta(kv, snap, edge.sourceNodeId, 1);
8169
+ await applyDegreeDelta(kv, snap, edge.targetNodeId, 1);
8170
+ newEdgesForTopCheck.push(edge);
7980
8171
  }
7981
8172
  }
8173
+ for (const edge of newEdgesForTopCheck) snapshotPushEdgeIfBothInTop(snap, edge);
8174
+ if (newNodeCount > 0 || newEdgeCount > 0) {
8175
+ snap.updatedAt = capturedAt;
8176
+ snap.dirty = false;
8177
+ await kv.set(KV.graphSnapshot, SNAPSHOT_KEY, snap);
8178
+ }
7982
8179
  await recordAudit(kv, "observe", "mem::graph-extract", obsIds, {
7983
8180
  nodesExtracted: nodes.length,
7984
8181
  edgesExtracted: edges.length
7985
8182
  });
7986
8183
  logger.info("Graph extraction complete", {
7987
8184
  nodes: nodes.length,
7988
- edges: edges.length
8185
+ edges: edges.length,
8186
+ newNodes: newNodeCount,
8187
+ newEdges: newEdgeCount
7989
8188
  });
7990
8189
  return {
7991
8190
  success: true,
@@ -8002,10 +8201,49 @@ function registerGraphFunction(sdk, kv, provider) {
8002
8201
  }
8003
8202
  });
8004
8203
  sdk.registerFunction("mem::graph-query", async (data) => {
8005
- const allNodes = (await kv.list(KV.graphNodes)).filter((n) => !n.stale);
8006
- const allEdges = (await kv.list(KV.graphEdges)).filter((e) => !e.stale);
8007
8204
  const maxDepth = Math.min(data.maxDepth || 3, 5);
8008
8205
  const { limit, offset } = resolvePagination(data.limit, data.offset);
8206
+ if (!data.query && !data.startNodeId) {
8207
+ const snap = await readSnapshot(kv);
8208
+ if (snap && snap.stats.totalNodes > 0) return paginateFromSnapshot(snap, data.nodeType, limit, offset);
8209
+ return {
8210
+ nodes: [],
8211
+ edges: [],
8212
+ depth: 0,
8213
+ totalNodes: 0,
8214
+ totalEdges: 0,
8215
+ truncated: false,
8216
+ limit,
8217
+ offset,
8218
+ warning: "No graph snapshot available. Either no graph has been extracted yet, or you are on a legacy corpus from a pre-#814 agentmemory build. Run POST /agentmemory/graph/snapshot-rebuild (safe up to ~25K nodes) or POST /agentmemory/graph/reset to wipe and let future extracts repopulate."
8219
+ };
8220
+ }
8221
+ let allNodes;
8222
+ let allEdges;
8223
+ try {
8224
+ const [rawNodes, rawEdges] = await withTimeout(Promise.all([kv.list(KV.graphNodes), kv.list(KV.graphEdges)]), LIVE_ENUMERATION_BUDGET_MS, "graph-query enumeration");
8225
+ allNodes = rawNodes.filter((n) => !n.stale);
8226
+ allEdges = rawEdges.filter((e) => !e.stale);
8227
+ } catch (err) {
8228
+ const msg = err instanceof Error ? err.message : String(err);
8229
+ logger.warn("Graph query enumeration timed out, using snapshot", { error: msg });
8230
+ const snap = await readSnapshot(kv);
8231
+ if (snap) return {
8232
+ ...paginateFromSnapshot(snap, data.nodeType, limit, offset),
8233
+ warning: "Live graph enumeration exceeded budget. Query / startNodeId paths degrade on >25K-node corpora until a per-node edge index lands. Result reflects top-degree snapshot, not the requested walk."
8234
+ };
8235
+ return {
8236
+ nodes: [],
8237
+ edges: [],
8238
+ depth: 0,
8239
+ totalNodes: 0,
8240
+ totalEdges: 0,
8241
+ truncated: false,
8242
+ limit,
8243
+ offset,
8244
+ warning: "Graph enumeration exceeded budget and no snapshot is available."
8245
+ };
8246
+ }
8009
8247
  if (data.query) {
8010
8248
  const lower = data.query.toLowerCase();
8011
8249
  return paginate(allNodes.filter((n) => n.name.toLowerCase().includes(lower) || Object.values(n.properties).some((v) => typeof v === "string" && v.toLowerCase().includes(lower))), allEdges, 0, limit, offset);
@@ -8042,22 +8280,116 @@ function registerGraphFunction(sdk, kv, provider) {
8042
8280
  }
8043
8281
  return paginate(resultNodes, resultEdges, maxDepth, limit, offset);
8044
8282
  }
8045
- let filtered = allNodes;
8046
- if (data.nodeType) filtered = allNodes.filter((n) => n.type === data.nodeType);
8047
- return paginate(rankByDegree(filtered, allEdges), allEdges, 0, limit, offset);
8283
+ return paginate([], [], 0, limit, offset);
8048
8284
  });
8049
8285
  sdk.registerFunction("mem::graph-stats", async () => {
8050
- const nodes = await kv.list(KV.graphNodes);
8051
- const edges = await kv.list(KV.graphEdges);
8052
- const nodesByType = {};
8053
- for (const n of nodes) nodesByType[n.type] = (nodesByType[n.type] || 0) + 1;
8054
- const edgesByType = {};
8055
- for (const e of edges) edgesByType[e.type] = (edgesByType[e.type] || 0) + 1;
8286
+ const snap = await readSnapshot(kv);
8287
+ if (snap) return {
8288
+ ...snap.stats,
8289
+ fromSnapshot: true,
8290
+ updatedAt: snap.updatedAt,
8291
+ ...snap.dirty ? { warning: "Snapshot is marked dirty (write was in-flight when read). Counts are eventually consistent." } : {}
8292
+ };
8056
8293
  return {
8057
- totalNodes: nodes.length,
8058
- totalEdges: edges.length,
8059
- nodesByType,
8060
- edgesByType
8294
+ totalNodes: 0,
8295
+ totalEdges: 0,
8296
+ nodesByType: {},
8297
+ edgesByType: {},
8298
+ fromSnapshot: false,
8299
+ warning: "No graph snapshot available. Run POST /agentmemory/graph/snapshot-rebuild (safe up to ~25K nodes) or POST /agentmemory/graph/reset to wipe and let future extracts repopulate."
8300
+ };
8301
+ });
8302
+ sdk.registerFunction("mem::graph-snapshot-rebuild", async (data) => {
8303
+ const started = Date.now();
8304
+ const forceRebuild = data?.force === true;
8305
+ try {
8306
+ if (!await readSnapshot(kv) && !forceRebuild) {
8307
+ logger.warn("Graph snapshot rebuild refused: no prior snapshot", { hint: "legacy corpus or empty store" });
8308
+ return {
8309
+ success: false,
8310
+ legacyCorpus: true,
8311
+ error: "No prior snapshot found. Rebuild would call kv.list on KV.graphNodes/Edges, which heartbeat-crashes the worker on corpora past the iii state response budget (~25K nodes). Either (a) call POST /agentmemory/graph/reset to drop into incremental-only mode and rebuild from new extracts, or (b) re-send with `force: true` if you're certain the corpus is small."
8312
+ };
8313
+ }
8314
+ } catch (err) {
8315
+ const msg = err instanceof Error ? err.message : String(err);
8316
+ logger.warn("Graph snapshot pre-flight read failed", { error: msg });
8317
+ }
8318
+ try {
8319
+ const [nodes, edges] = await withTimeout(Promise.all([kv.list(KV.graphNodes), kv.list(KV.graphEdges)]), LIVE_ENUMERATION_BUDGET_MS, "graph-snapshot-rebuild enumeration");
8320
+ if (nodes.length > REBUILD_SAFE_NODE_CEILING) {
8321
+ logger.warn("Graph snapshot rebuild aborted: corpus too large", {
8322
+ totalNodes: nodes.length,
8323
+ ceiling: REBUILD_SAFE_NODE_CEILING
8324
+ });
8325
+ return {
8326
+ success: false,
8327
+ tooLarge: true,
8328
+ totalNodes: nodes.length,
8329
+ ceiling: REBUILD_SAFE_NODE_CEILING,
8330
+ error: `Corpus has ${nodes.length} graph nodes; safe-rebuild ceiling is ${REBUILD_SAFE_NODE_CEILING}. Run POST /agentmemory/graph/reset to wipe and let future extracts rebuild incrementally.`
8331
+ };
8332
+ }
8333
+ const liveNodes = nodes.filter((n) => !n.stale);
8334
+ const liveEdges = edges.filter((e) => !e.stale);
8335
+ const degree = /* @__PURE__ */ new Map();
8336
+ for (const e of liveEdges) {
8337
+ degree.set(e.sourceNodeId, (degree.get(e.sourceNodeId) ?? 0) + 1);
8338
+ degree.set(e.targetNodeId, (degree.get(e.targetNodeId) ?? 0) + 1);
8339
+ }
8340
+ const BATCH_SIZE = 100;
8341
+ for (let i = 0; i < liveNodes.length; i += BATCH_SIZE) {
8342
+ const batch = liveNodes.slice(i, i + BATCH_SIZE);
8343
+ await Promise.all(batch.flatMap((n) => [kv.set(KV.graphNameIndex, nameIndexKey(n.type, n.name), n.id), kv.set(KV.graphNodeDegree, n.id, degree.get(n.id) ?? 0)]));
8344
+ }
8345
+ for (let i = 0; i < liveEdges.length; i += BATCH_SIZE) {
8346
+ const batch = liveEdges.slice(i, i + BATCH_SIZE);
8347
+ await Promise.all(batch.map((e) => kv.set(KV.graphEdgeKey, edgeIndexKey(e.sourceNodeId, e.targetNodeId, e.type), e.id)));
8348
+ }
8349
+ const snap = buildSnapshotFromArrays(nodes, edges);
8350
+ await kv.set(KV.graphSnapshot, SNAPSHOT_KEY, snap);
8351
+ const tookMs = Date.now() - started;
8352
+ logger.info("Graph snapshot rebuilt", {
8353
+ totalNodes: snap.stats.totalNodes,
8354
+ totalEdges: snap.stats.totalEdges,
8355
+ topNodes: snap.topNodes.length,
8356
+ topEdges: snap.topEdges.length,
8357
+ tookMs
8358
+ });
8359
+ return {
8360
+ success: true,
8361
+ ...snap.stats,
8362
+ topNodes: snap.topNodes.length,
8363
+ topEdges: snap.topEdges.length,
8364
+ updatedAt: snap.updatedAt,
8365
+ tookMs
8366
+ };
8367
+ } catch (err) {
8368
+ const msg = err instanceof Error ? err.message : String(err);
8369
+ logger.error("Graph snapshot rebuild failed", { error: msg });
8370
+ return {
8371
+ success: false,
8372
+ error: msg
8373
+ };
8374
+ }
8375
+ });
8376
+ sdk.registerFunction("mem::graph-reset", async () => {
8377
+ const started = Date.now();
8378
+ const resetSnapshot = {
8379
+ ...emptySnapshot(),
8380
+ resetAt: (/* @__PURE__ */ new Date()).toISOString()
8381
+ };
8382
+ await kv.set(KV.graphSnapshot, SNAPSHOT_KEY, resetSnapshot);
8383
+ const counts = { [KV.graphSnapshot]: 1 };
8384
+ const tookMs = Date.now() - started;
8385
+ logger.info("Graph state reset", {
8386
+ counts,
8387
+ tookMs
8388
+ });
8389
+ return {
8390
+ success: true,
8391
+ cleared: counts,
8392
+ tookMs
8061
8393
  };
8062
8394
  });
8063
8395
  }
@@ -15372,6 +15704,7 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
15372
15704
  });
15373
15705
  sdk.registerFunction("api::search", async (req) => {
15374
15706
  const body = req.body ?? {};
15707
+ const queryAgentId = typeof req.query_params?.["agentId"] === "string" ? req.query_params["agentId"] : void 0;
15375
15708
  if (typeof body.query !== "string" || !body.query.trim()) return {
15376
15709
  status_code: 400,
15377
15710
  body: { error: "query is required and must be a non-empty string" }
@@ -15400,13 +15733,15 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
15400
15733
  status_code: 400,
15401
15734
  body: { error: "token_budget must be a positive integer" }
15402
15735
  };
15736
+ const bodyAgentId = typeof body.agentId === "string" && body.agentId.trim().length > 0 ? body.agentId.trim() : void 0;
15403
15737
  const payload = {
15404
15738
  query: body.query.trim(),
15405
15739
  limit: body.limit,
15406
15740
  project: body.project,
15407
15741
  cwd: body.cwd,
15408
15742
  format: typeof body.format === "string" ? body.format.trim().toLowerCase() : void 0,
15409
- token_budget: body.token_budget
15743
+ token_budget: body.token_budget,
15744
+ agentId: bodyAgentId ?? queryAgentId
15410
15745
  };
15411
15746
  return {
15412
15747
  status_code: 200,
@@ -16342,6 +16677,52 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
16342
16677
  http_method: "GET"
16343
16678
  }
16344
16679
  });
16680
+ sdk.registerFunction("api::graph-snapshot-rebuild", async (req) => {
16681
+ const authErr = checkAuth(req, secret);
16682
+ if (authErr) return authErr;
16683
+ try {
16684
+ return {
16685
+ status_code: 200,
16686
+ body: await sdk.trigger({
16687
+ function_id: "mem::graph-snapshot-rebuild",
16688
+ payload: {}
16689
+ })
16690
+ };
16691
+ } catch {
16692
+ return graphDisabledResponse();
16693
+ }
16694
+ });
16695
+ sdk.registerTrigger({
16696
+ type: "http",
16697
+ function_id: "api::graph-snapshot-rebuild",
16698
+ config: {
16699
+ api_path: "/agentmemory/graph/snapshot-rebuild",
16700
+ http_method: "POST"
16701
+ }
16702
+ });
16703
+ sdk.registerFunction("api::graph-reset", async (req) => {
16704
+ const authErr = checkAuth(req, secret);
16705
+ if (authErr) return authErr;
16706
+ try {
16707
+ return {
16708
+ status_code: 200,
16709
+ body: await sdk.trigger({
16710
+ function_id: "mem::graph-reset",
16711
+ payload: {}
16712
+ })
16713
+ };
16714
+ } catch {
16715
+ return graphDisabledResponse();
16716
+ }
16717
+ });
16718
+ sdk.registerTrigger({
16719
+ type: "http",
16720
+ function_id: "api::graph-reset",
16721
+ config: {
16722
+ api_path: "/agentmemory/graph/reset",
16723
+ http_method: "POST"
16724
+ }
16725
+ });
16345
16726
  sdk.registerFunction("api::graph-extract", async (req) => {
16346
16727
  const authErr = checkAuth(req, secret);
16347
16728
  if (authErr) return authErr;
@@ -19885,13 +20266,15 @@ function registerMcpEndpoints(sdk, kv, secret) {
19885
20266
  status_code: 400,
19886
20267
  body: { error: "token_budget must be a positive integer" }
19887
20268
  };
20269
+ const recallAgentId = typeof args.agentId === "string" && args.agentId.trim().length > 0 ? args.agentId.trim() : void 0;
19888
20270
  const result = await sdk.trigger({
19889
20271
  function_id: "mem::search",
19890
20272
  payload: {
19891
20273
  query: args.query,
19892
20274
  limit: typeof args.limit === "number" ? args.limit : 10,
19893
20275
  format,
19894
- token_budget: tokenBudget
20276
+ token_budget: tokenBudget,
20277
+ agentId: recallAgentId
19895
20278
  }
19896
20279
  });
19897
20280
  return {
@@ -21379,7 +21762,10 @@ function registerMcpEndpoints(sdk, kv, secret) {
21379
21762
  limit: 10
21380
21763
  }
21381
21764
  }).catch(() => ({ results: [] }));
21382
- const relevant = (await kv.list(KV.memories)).filter((m) => m.isLatest).slice(0, 5);
21765
+ const memories = await kv.list(KV.memories);
21766
+ const isolated = isAgentScopeIsolated();
21767
+ const activeAgentId = isolated ? getAgentId() : void 0;
21768
+ const relevant = isolated && activeAgentId === void 0 ? [] : memories.filter((m) => m.isLatest).filter((m) => activeAgentId === void 0 || m.agentId === activeAgentId).slice(0, 5);
21383
21769
  return {
21384
21770
  status_code: 200,
21385
21771
  body: { messages: [{
@@ -21750,7 +22136,7 @@ async function main() {
21750
22136
  console.warn(`[agentmemory] Failed to backfill memories into BM25:`, err);
21751
22137
  }
21752
22138
  bootLog(`Ready. ${embeddingProvider ? "Triple-stream (BM25+Vector+Graph)" : "BM25+Graph"} search active.`);
21753
- bootLog(`REST API: 126 endpoints at http://localhost:${config.restPort}/agentmemory/*`);
22139
+ bootLog(`REST API: 128 endpoints at http://localhost:${config.restPort}/agentmemory/*`);
21754
22140
  bootLog(`MCP surface (opt-in via \`npx @agentmemory/mcp\`): ${getAllTools().length} tools · 6 resources · 3 prompts`);
21755
22141
  const viewerServer = startViewerServer(config.restPort + 2, kv, sdk, secret, config.restPort);
21756
22142
  const autoForgetIntervalMs = parseInt(process.env.AUTO_FORGET_INTERVAL_MS || "3600000", 10);