@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.
@@ -1,9 +1,9 @@
1
- import { a as jaccardSimilarity, i as generateId, n as STREAM, r as fingerprintId, t as KV } from "./schema-Dsr_V2Wp.mjs";
1
+ import { a as jaccardSimilarity, i as generateId, n as STREAM, r as fingerprintId, t as KV } from "./schema-CNiCWzV9.mjs";
2
2
  import { n as logger, t as bootLog } from "./logger-yHTcEBAI.mjs";
3
- import { t as VERSION } from "./version-BMFYFFut.mjs";
3
+ import { t as VERSION } from "./version-9DNrRyph.mjs";
4
4
  import { a as isManagedImagePath, n as deleteImage, r as getMaxBytes, t as IMAGES_DIR } from "./image-store-Gpo2mgM9.mjs";
5
- import { r as withKeyedLock, t as getImageRefCount } from "./image-refs-C7h9L5wx.mjs";
6
- import { _ as loadConfig, a as getAgentId, b as loadSnapshotConfig, c as getFollowupWindowSeconds, d as isAutoCompressEnabled, f as isConsolidationEnabled, g as loadClaudeBridgeConfig, h as isGraphExtractionEnabled, i as detectLlmProviderKind, m as isDropStaleIndexEnabled, n as getVisibleTools, o as getConsolidationDecayDays, p as isContextInjectionEnabled, r as detectEmbeddingProvider, s as getEnvVar, t as getAllTools, u as isAgentScopeIsolated, v as loadEmbeddingConfig, x as loadTeamConfig, y as loadFallbackConfig } from "./tools-registry-Dzxv9iUu.mjs";
5
+ import { r as withKeyedLock, t as getImageRefCount } from "./image-refs-B5Zf_Djo.mjs";
6
+ import { _ as loadConfig, a as getAgentId, b as loadSnapshotConfig, c as getFollowupWindowSeconds, d as isAutoCompressEnabled, f as isConsolidationEnabled, g as loadClaudeBridgeConfig, h as isGraphExtractionEnabled, i as detectLlmProviderKind, m as isDropStaleIndexEnabled, n as getVisibleTools, o as getConsolidationDecayDays, p as isContextInjectionEnabled, r as detectEmbeddingProvider, s as getEnvVar, t as getAllTools, u as isAgentScopeIsolated, v as loadEmbeddingConfig, x as loadTeamConfig, y as loadFallbackConfig } from "./tools-registry-D8XVVPbJ.mjs";
7
7
  import { createRequire } from "node:module";
8
8
  import { execFile } from "node:child_process";
9
9
  import { constants, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
@@ -3109,6 +3109,12 @@ function registerSearchFunction(sdk, kv) {
3109
3109
  }
3110
3110
  const projectFilter = typeof data.project === "string" && data.project.trim().length > 0 ? data.project.trim() : void 0;
3111
3111
  const cwdFilter = typeof data.cwd === "string" && data.cwd.trim().length > 0 ? data.cwd.trim() : void 0;
3112
+ const isolated = isAgentScopeIsolated();
3113
+ const explicitAgentId = typeof data.agentId === "string" && data.agentId.trim().length > 0 ? data.agentId.trim() : void 0;
3114
+ const wildcardAgent = explicitAgentId === "*";
3115
+ const envAgentId = isolated ? getAgentId() : void 0;
3116
+ const filterAgentId = wildcardAgent ? void 0 : explicitAgentId ?? envAgentId;
3117
+ 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.");
3112
3118
  const format = typeof data.format === "string" ? data.format : "full";
3113
3119
  if (![
3114
3120
  "full",
@@ -3124,7 +3130,7 @@ function registerSearchFunction(sdk, kv) {
3124
3130
  const count = await rebuildIndex(kv);
3125
3131
  logger.info("Search index rebuilt", { entries: count });
3126
3132
  }
3127
- const filtering = !!(projectFilter || cwdFilter);
3133
+ const filtering = !!(projectFilter || cwdFilter || filterAgentId);
3128
3134
  const fetchLimit = filtering ? Math.max(effectiveLimit * 10, 100) : effectiveLimit;
3129
3135
  const results = idx.search(query, fetchLimit);
3130
3136
  const sessionCache = /* @__PURE__ */ new Map();
@@ -3141,9 +3147,10 @@ function registerSearchFunction(sdk, kv) {
3141
3147
  memoryProjectCache.set(obsId, proj);
3142
3148
  return proj;
3143
3149
  };
3150
+ const earlyCap = filterAgentId ? fetchLimit : effectiveLimit;
3144
3151
  const candidates = [];
3145
3152
  for (const r of results) {
3146
- if (candidates.length >= effectiveLimit) break;
3153
+ if (candidates.length >= earlyCap) break;
3147
3154
  if (filtering) {
3148
3155
  const s = await loadSession(r.sessionId);
3149
3156
  if (s) {
@@ -3165,7 +3172,10 @@ function registerSearchFunction(sdk, kv) {
3165
3172
  const enriched = [];
3166
3173
  for (let i = 0; i < candidates.length; i++) {
3167
3174
  const obs = obsResults[i];
3168
- if (obs) enriched.push({
3175
+ if (!obs) continue;
3176
+ if (filterAgentId !== void 0 && obs.agentId !== filterAgentId) continue;
3177
+ if (enriched.length >= effectiveLimit) break;
3178
+ enriched.push({
3169
3179
  observation: obs,
3170
3180
  score: candidates[i].score,
3171
3181
  sessionId: candidates[i].sessionId
@@ -3330,7 +3340,7 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
3330
3340
  const { saveImageToDisk } = await import("./image-store-Gpo2mgM9.mjs").then((n) => n.i);
3331
3341
  const { filePath, bytesWritten } = await saveImageToDisk(pendingImageData);
3332
3342
  raw.imageData = filePath;
3333
- const { incrementImageRef } = await import("./image-refs-C7h9L5wx.mjs").then((n) => n.n);
3343
+ const { incrementImageRef } = await import("./image-refs-B5Zf_Djo.mjs").then((n) => n.n);
3334
3344
  await incrementImageRef(kv, filePath);
3335
3345
  sdk.trigger({
3336
3346
  function_id: "mem::disk-size-delta",
@@ -5651,7 +5661,7 @@ function registerRememberFunction(sdk, kv) {
5651
5661
  const deletedMemoryIds = [];
5652
5662
  const deletedObservationIds = [];
5653
5663
  let deletedSession = false;
5654
- const { decrementImageRef } = await import("./image-refs-C7h9L5wx.mjs").then((n) => n.n);
5664
+ const { decrementImageRef } = await import("./image-refs-B5Zf_Djo.mjs").then((n) => n.n);
5655
5665
  if (data.memoryId) {
5656
5666
  const mem = await kv.get(KV.memories, data.memoryId);
5657
5667
  await kv.delete(KV.memories, data.memoryId);
@@ -5758,7 +5768,7 @@ async function runRecoveredSessionConsolidation(sdk) {
5758
5768
  function registerEvictFunction(sdk, kv) {
5759
5769
  sdk.registerFunction("mem::evict", async (data) => {
5760
5770
  const dryRun = data?.dryRun ?? false;
5761
- const { decrementImageRef } = await import("./image-refs-C7h9L5wx.mjs").then((n) => n.n);
5771
+ const { decrementImageRef } = await import("./image-refs-B5Zf_Djo.mjs").then((n) => n.n);
5762
5772
  const configOverride = await kv.get(KV.config, "eviction").catch(() => null);
5763
5773
  const cfg = {
5764
5774
  ...DEFAULTS$1,
@@ -6278,7 +6288,10 @@ function registerSmartSearchFunction(sdk, kv, searchFn) {
6278
6288
  sdk.registerFunction("mem::smart-search", async (data) => {
6279
6289
  const isolated = isAgentScopeIsolated();
6280
6290
  const explicitAgentId = typeof data.agentId === "string" && data.agentId.trim().length > 0 ? data.agentId.trim() : void 0;
6281
- const filterAgentId = explicitAgentId === "*" ? void 0 : explicitAgentId ?? (isolated ? getAgentId() : void 0);
6291
+ const wildcardAgent = explicitAgentId === "*";
6292
+ const envAgentId = isolated ? getAgentId() : void 0;
6293
+ const filterAgentId = wildcardAgent ? void 0 : explicitAgentId ?? envAgentId;
6294
+ 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.");
6282
6295
  if (data.expandIds && data.expandIds.length > 0) {
6283
6296
  const raw = data.expandIds.slice(0, 20);
6284
6297
  const items = raw.map((entry) => {
@@ -6570,7 +6583,7 @@ function registerAutoForgetFunction(sdk, kv) {
6570
6583
  sdk.registerFunction("mem::auto-forget", async (data) => {
6571
6584
  const dryRun = data?.dryRun ?? false;
6572
6585
  const now = Date.now();
6573
- const { decrementImageRef } = await import("./image-refs-C7h9L5wx.mjs").then((n) => n.n);
6586
+ const { decrementImageRef } = await import("./image-refs-B5Zf_Djo.mjs").then((n) => n.n);
6574
6587
  const result = {
6575
6588
  ttlExpired: [],
6576
6589
  contradictions: [],
@@ -6829,7 +6842,8 @@ function registerExportImportFunction(sdk, kv) {
6829
6842
  "0.9.23",
6830
6843
  "0.9.24",
6831
6844
  "0.9.25",
6832
- "0.9.26"
6845
+ "0.9.26",
6846
+ "0.9.27"
6833
6847
  ]).has(importData.version)) return {
6834
6848
  success: false,
6835
6849
  error: `Unsupported export version: ${importData.version}`
@@ -7353,19 +7367,167 @@ function buildGraphExtractionPrompt(observations) {
7353
7367
  //#region src/functions/graph.ts
7354
7368
  const DEFAULT_GRAPH_QUERY_LIMIT = 500;
7355
7369
  const MAX_GRAPH_QUERY_LIMIT = 5e3;
7356
- function resolvePagination(rawLimit, rawOffset) {
7370
+ const SNAPSHOT_TOP_NODES = DEFAULT_GRAPH_QUERY_LIMIT;
7371
+ const SNAPSHOT_KEY = "current";
7372
+ const LIVE_ENUMERATION_BUDGET_MS = 6e3;
7373
+ function withTimeout(p, ms, label) {
7374
+ return new Promise((resolve, reject) => {
7375
+ const t = setTimeout(() => reject(/* @__PURE__ */ new Error(`${label}: exceeded ${ms}ms budget`)), ms);
7376
+ p.then((v) => {
7377
+ clearTimeout(t);
7378
+ resolve(v);
7379
+ }, (err) => {
7380
+ clearTimeout(t);
7381
+ reject(err);
7382
+ });
7383
+ });
7384
+ }
7385
+ function emptySnapshot() {
7357
7386
  return {
7358
- limit: Math.max(1, Math.min(typeof rawLimit === "number" && Number.isFinite(rawLimit) ? Math.floor(rawLimit) : DEFAULT_GRAPH_QUERY_LIMIT, MAX_GRAPH_QUERY_LIMIT)),
7359
- offset: Math.max(0, typeof rawOffset === "number" && Number.isFinite(rawOffset) ? Math.floor(rawOffset) : 0)
7387
+ version: 1,
7388
+ topNodes: [],
7389
+ topEdges: [],
7390
+ topDegrees: {},
7391
+ stats: {
7392
+ totalNodes: 0,
7393
+ totalEdges: 0,
7394
+ nodesByType: {},
7395
+ edgesByType: {}
7396
+ },
7397
+ updatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
7398
+ dirty: true
7360
7399
  };
7361
7400
  }
7362
- function rankByDegree(nodes, edges) {
7401
+ async function readSnapshot(kv) {
7402
+ try {
7403
+ const snap = await kv.get(KV.graphSnapshot, SNAPSHOT_KEY);
7404
+ if (snap && typeof snap === "object" && snap.version === 1) return snap;
7405
+ return null;
7406
+ } catch (err) {
7407
+ logger.warn("Graph snapshot read failed", { error: err instanceof Error ? err.message : String(err) });
7408
+ return null;
7409
+ }
7410
+ }
7411
+ function buildSnapshotFromArrays(nodes, edges) {
7412
+ const liveNodes = nodes.filter((n) => !n.stale);
7413
+ const liveEdges = edges.filter((e) => !e.stale);
7363
7414
  const degree = /* @__PURE__ */ new Map();
7364
- for (const edge of edges) {
7365
- degree.set(edge.sourceNodeId, (degree.get(edge.sourceNodeId) ?? 0) + 1);
7366
- degree.set(edge.targetNodeId, (degree.get(edge.targetNodeId) ?? 0) + 1);
7415
+ for (const e of liveEdges) {
7416
+ degree.set(e.sourceNodeId, (degree.get(e.sourceNodeId) ?? 0) + 1);
7417
+ degree.set(e.targetNodeId, (degree.get(e.targetNodeId) ?? 0) + 1);
7418
+ }
7419
+ const ranked = [...liveNodes].sort((a, b) => (degree.get(b.id) ?? 0) - (degree.get(a.id) ?? 0)).slice(0, SNAPSHOT_TOP_NODES);
7420
+ const rankedIds = new Set(ranked.map((n) => n.id));
7421
+ const topEdges = liveEdges.filter((e) => rankedIds.has(e.sourceNodeId) && rankedIds.has(e.targetNodeId));
7422
+ const topDegrees = {};
7423
+ for (const n of ranked) topDegrees[n.id] = degree.get(n.id) ?? 0;
7424
+ const nodesByType = {};
7425
+ for (const n of liveNodes) nodesByType[n.type] = (nodesByType[n.type] || 0) + 1;
7426
+ const edgesByType = {};
7427
+ for (const e of liveEdges) edgesByType[e.type] = (edgesByType[e.type] || 0) + 1;
7428
+ return {
7429
+ version: 1,
7430
+ topNodes: ranked,
7431
+ topEdges,
7432
+ topDegrees,
7433
+ stats: {
7434
+ totalNodes: liveNodes.length,
7435
+ totalEdges: liveEdges.length,
7436
+ nodesByType,
7437
+ edgesByType
7438
+ },
7439
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
7440
+ dirty: false
7441
+ };
7442
+ }
7443
+ function paginateFromSnapshot(snap, filterType, limit, offset) {
7444
+ const filteredNodes = filterType ? snap.topNodes.filter((n) => n.type === filterType) : snap.topNodes;
7445
+ const total = filterType ? snap.stats.nodesByType[filterType] ?? 0 : snap.stats.totalNodes;
7446
+ const pageNodes = filteredNodes.slice(offset, offset + limit);
7447
+ const pageIds = new Set(pageNodes.map((n) => n.id));
7448
+ return {
7449
+ nodes: pageNodes,
7450
+ edges: snap.topEdges.filter((e) => pageIds.has(e.sourceNodeId) && pageIds.has(e.targetNodeId)),
7451
+ depth: 0,
7452
+ totalNodes: total,
7453
+ totalEdges: snap.stats.totalEdges,
7454
+ truncated: total > pageNodes.length,
7455
+ limit,
7456
+ offset,
7457
+ fromSnapshot: true
7458
+ };
7459
+ }
7460
+ const REBUILD_SAFE_NODE_CEILING = 25e3;
7461
+ function nameIndexKey(type, name) {
7462
+ return `${type}|${name}`;
7463
+ }
7464
+ function edgeIndexKey(sourceNodeId, targetNodeId, type) {
7465
+ return `${sourceNodeId}|${targetNodeId}|${type}`;
7466
+ }
7467
+ async function applyDegreeDelta(kv, snap, nodeId, delta) {
7468
+ const prev = await kv.get(KV.graphNodeDegree, nodeId) ?? 0;
7469
+ const next = Math.max(0, prev + delta);
7470
+ await kv.set(KV.graphNodeDegree, nodeId, next);
7471
+ if (snap.topNodes.findIndex((n) => n.id === nodeId) !== -1) {
7472
+ snap.topDegrees[nodeId] = next;
7473
+ snap.topNodes.sort((a, b) => (snap.topDegrees[b.id] ?? 0) - (snap.topDegrees[a.id] ?? 0));
7474
+ return next;
7475
+ }
7476
+ if (snap.topNodes.length < SNAPSHOT_TOP_NODES) {
7477
+ const node = await kv.get(KV.graphNodes, nodeId);
7478
+ if (node && !node.stale) {
7479
+ snap.topNodes.push(node);
7480
+ snap.topDegrees[node.id] = next;
7481
+ snap.topNodes.sort((a, b) => (snap.topDegrees[b.id] ?? 0) - (snap.topDegrees[a.id] ?? 0));
7482
+ }
7483
+ return next;
7484
+ }
7485
+ const tailEntry = snap.topNodes[snap.topNodes.length - 1];
7486
+ if (!tailEntry) return next;
7487
+ if (next > (snap.topDegrees[tailEntry.id] ?? 0)) {
7488
+ const node = await kv.get(KV.graphNodes, nodeId);
7489
+ if (node && !node.stale) {
7490
+ const evicted = snap.topNodes.pop();
7491
+ if (evicted) delete snap.topDegrees[evicted.id];
7492
+ snap.topNodes.push(node);
7493
+ snap.topDegrees[node.id] = next;
7494
+ snap.topNodes.sort((a, b) => (snap.topDegrees[b.id] ?? 0) - (snap.topDegrees[a.id] ?? 0));
7495
+ }
7496
+ }
7497
+ return next;
7498
+ }
7499
+ function snapshotPushEdgeIfBothInTop(snap, edge) {
7500
+ const topIds = new Set(snap.topNodes.map((n) => n.id));
7501
+ if (topIds.has(edge.sourceNodeId) && topIds.has(edge.targetNodeId)) {
7502
+ if (!snap.topEdges.find((e) => e.id === edge.id)) snap.topEdges.push(edge);
7367
7503
  }
7368
- return [...nodes].sort((a, b) => (degree.get(b.id) ?? 0) - (degree.get(a.id) ?? 0));
7504
+ }
7505
+ function mergeNode(existing, incoming, obsIds, capturedAt) {
7506
+ return {
7507
+ ...existing,
7508
+ sourceObservationIds: [...new Set([
7509
+ ...existing.sourceObservationIds,
7510
+ ...incoming.sourceObservationIds,
7511
+ ...obsIds
7512
+ ])],
7513
+ properties: {
7514
+ ...existing.properties,
7515
+ ...incoming.properties
7516
+ },
7517
+ updatedAt: capturedAt
7518
+ };
7519
+ }
7520
+ function mergeEdge(existing, obsIds) {
7521
+ return {
7522
+ ...existing,
7523
+ sourceObservationIds: [...new Set([...existing.sourceObservationIds, ...obsIds])]
7524
+ };
7525
+ }
7526
+ function resolvePagination(rawLimit, rawOffset) {
7527
+ return {
7528
+ limit: Math.max(1, Math.min(typeof rawLimit === "number" && Number.isFinite(rawLimit) ? Math.floor(rawLimit) : DEFAULT_GRAPH_QUERY_LIMIT, MAX_GRAPH_QUERY_LIMIT)),
7529
+ offset: Math.max(0, typeof rawOffset === "number" && Number.isFinite(rawOffset) ? Math.floor(rawOffset) : 0)
7530
+ };
7369
7531
  }
7370
7532
  function paginate(nodes, allEdges, depth, limit, offset) {
7371
7533
  const totalNodes = nodes.length;
@@ -7462,45 +7624,76 @@ function registerGraphFunction(sdk, kv, provider) {
7462
7624
  const response = await provider.compress(GRAPH_EXTRACTION_SYSTEM, prompt);
7463
7625
  const obsIds = data.observations.map((o) => o.id);
7464
7626
  const { nodes, edges } = parseGraphXml(response, obsIds);
7465
- const existingNodes = await kv.list(KV.graphNodes);
7466
- const existingEdges = await kv.list(KV.graphEdges);
7627
+ const snap = await readSnapshot(kv) ?? emptySnapshot();
7628
+ const capturedAt = (/* @__PURE__ */ new Date()).toISOString();
7629
+ let newNodeCount = 0;
7630
+ let newEdgeCount = 0;
7631
+ const newEdgesForTopCheck = [];
7467
7632
  for (const node of nodes) {
7468
- const existing = existingNodes.find((n) => n.name === node.name && n.type === node.type);
7633
+ const indexKey = nameIndexKey(node.type, node.name);
7634
+ const existingId = await kv.get(KV.graphNameIndex, indexKey);
7635
+ let existing = null;
7636
+ if (existingId) {
7637
+ existing = await kv.get(KV.graphNodes, existingId);
7638
+ if (existing && snap.resetAt && typeof existing.createdAt === "string" && existing.createdAt < snap.resetAt) existing = null;
7639
+ }
7469
7640
  if (existing) {
7470
- const merged = {
7471
- ...existing,
7472
- sourceObservationIds: [...new Set([...existing.sourceObservationIds, ...obsIds])],
7473
- properties: {
7474
- ...existing.properties,
7475
- ...node.properties
7476
- }
7477
- };
7641
+ const merged = mergeNode(existing, node, obsIds, capturedAt);
7478
7642
  await kv.set(KV.graphNodes, existing.id, merged);
7479
- const idx = existingNodes.findIndex((n) => n.id === existing.id);
7480
- if (idx !== -1) existingNodes[idx] = merged;
7643
+ const topIdx = snap.topNodes.findIndex((n) => n.id === existing.id);
7644
+ if (topIdx !== -1) snap.topNodes[topIdx] = merged;
7481
7645
  } else {
7482
7646
  await kv.set(KV.graphNodes, node.id, node);
7483
- existingNodes.push(node);
7647
+ await kv.set(KV.graphNameIndex, indexKey, node.id);
7648
+ await kv.set(KV.graphNodeDegree, node.id, 0);
7649
+ snap.stats.totalNodes += 1;
7650
+ snap.stats.nodesByType[node.type] = (snap.stats.nodesByType[node.type] ?? 0) + 1;
7651
+ newNodeCount += 1;
7652
+ if (snap.topNodes.length < SNAPSHOT_TOP_NODES) {
7653
+ snap.topNodes.push(node);
7654
+ snap.topDegrees[node.id] = 0;
7655
+ }
7484
7656
  }
7485
7657
  }
7486
7658
  for (const edge of edges) {
7487
- const edgeKey = `${edge.sourceNodeId}|${edge.targetNodeId}|${edge.type}`;
7488
- const existingEdge = existingEdges.find((e) => `${e.sourceNodeId}|${e.targetNodeId}|${e.type}` === edgeKey);
7489
- if (existingEdge) {
7490
- existingEdge.sourceObservationIds = [...new Set([...existingEdge.sourceObservationIds, ...obsIds])];
7491
- await kv.set(KV.graphEdges, existingEdge.id, existingEdge);
7659
+ const eKey = edgeIndexKey(edge.sourceNodeId, edge.targetNodeId, edge.type);
7660
+ const existingId = await kv.get(KV.graphEdgeKey, eKey);
7661
+ let existing = null;
7662
+ if (existingId) {
7663
+ existing = await kv.get(KV.graphEdges, existingId);
7664
+ if (existing && snap.resetAt && typeof existing.createdAt === "string" && existing.createdAt < snap.resetAt) existing = null;
7665
+ }
7666
+ if (existing) {
7667
+ const merged = mergeEdge(existing, obsIds);
7668
+ await kv.set(KV.graphEdges, existing.id, merged);
7669
+ const topIdx = snap.topEdges.findIndex((e) => e.id === existing.id);
7670
+ if (topIdx !== -1) snap.topEdges[topIdx] = merged;
7492
7671
  } else {
7493
7672
  await kv.set(KV.graphEdges, edge.id, edge);
7494
- existingEdges.push(edge);
7673
+ await kv.set(KV.graphEdgeKey, eKey, edge.id);
7674
+ snap.stats.totalEdges += 1;
7675
+ snap.stats.edgesByType[edge.type] = (snap.stats.edgesByType[edge.type] ?? 0) + 1;
7676
+ newEdgeCount += 1;
7677
+ await applyDegreeDelta(kv, snap, edge.sourceNodeId, 1);
7678
+ await applyDegreeDelta(kv, snap, edge.targetNodeId, 1);
7679
+ newEdgesForTopCheck.push(edge);
7495
7680
  }
7496
7681
  }
7682
+ for (const edge of newEdgesForTopCheck) snapshotPushEdgeIfBothInTop(snap, edge);
7683
+ if (newNodeCount > 0 || newEdgeCount > 0) {
7684
+ snap.updatedAt = capturedAt;
7685
+ snap.dirty = false;
7686
+ await kv.set(KV.graphSnapshot, SNAPSHOT_KEY, snap);
7687
+ }
7497
7688
  await recordAudit(kv, "observe", "mem::graph-extract", obsIds, {
7498
7689
  nodesExtracted: nodes.length,
7499
7690
  edgesExtracted: edges.length
7500
7691
  });
7501
7692
  logger.info("Graph extraction complete", {
7502
7693
  nodes: nodes.length,
7503
- edges: edges.length
7694
+ edges: edges.length,
7695
+ newNodes: newNodeCount,
7696
+ newEdges: newEdgeCount
7504
7697
  });
7505
7698
  return {
7506
7699
  success: true,
@@ -7517,10 +7710,49 @@ function registerGraphFunction(sdk, kv, provider) {
7517
7710
  }
7518
7711
  });
7519
7712
  sdk.registerFunction("mem::graph-query", async (data) => {
7520
- const allNodes = (await kv.list(KV.graphNodes)).filter((n) => !n.stale);
7521
- const allEdges = (await kv.list(KV.graphEdges)).filter((e) => !e.stale);
7522
7713
  const maxDepth = Math.min(data.maxDepth || 3, 5);
7523
7714
  const { limit, offset } = resolvePagination(data.limit, data.offset);
7715
+ if (!data.query && !data.startNodeId) {
7716
+ const snap = await readSnapshot(kv);
7717
+ if (snap && snap.stats.totalNodes > 0) return paginateFromSnapshot(snap, data.nodeType, limit, offset);
7718
+ return {
7719
+ nodes: [],
7720
+ edges: [],
7721
+ depth: 0,
7722
+ totalNodes: 0,
7723
+ totalEdges: 0,
7724
+ truncated: false,
7725
+ limit,
7726
+ offset,
7727
+ 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."
7728
+ };
7729
+ }
7730
+ let allNodes;
7731
+ let allEdges;
7732
+ try {
7733
+ const [rawNodes, rawEdges] = await withTimeout(Promise.all([kv.list(KV.graphNodes), kv.list(KV.graphEdges)]), LIVE_ENUMERATION_BUDGET_MS, "graph-query enumeration");
7734
+ allNodes = rawNodes.filter((n) => !n.stale);
7735
+ allEdges = rawEdges.filter((e) => !e.stale);
7736
+ } catch (err) {
7737
+ const msg = err instanceof Error ? err.message : String(err);
7738
+ logger.warn("Graph query enumeration timed out, using snapshot", { error: msg });
7739
+ const snap = await readSnapshot(kv);
7740
+ if (snap) return {
7741
+ ...paginateFromSnapshot(snap, data.nodeType, limit, offset),
7742
+ 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."
7743
+ };
7744
+ return {
7745
+ nodes: [],
7746
+ edges: [],
7747
+ depth: 0,
7748
+ totalNodes: 0,
7749
+ totalEdges: 0,
7750
+ truncated: false,
7751
+ limit,
7752
+ offset,
7753
+ warning: "Graph enumeration exceeded budget and no snapshot is available."
7754
+ };
7755
+ }
7524
7756
  if (data.query) {
7525
7757
  const lower = data.query.toLowerCase();
7526
7758
  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);
@@ -7557,22 +7789,116 @@ function registerGraphFunction(sdk, kv, provider) {
7557
7789
  }
7558
7790
  return paginate(resultNodes, resultEdges, maxDepth, limit, offset);
7559
7791
  }
7560
- let filtered = allNodes;
7561
- if (data.nodeType) filtered = allNodes.filter((n) => n.type === data.nodeType);
7562
- return paginate(rankByDegree(filtered, allEdges), allEdges, 0, limit, offset);
7792
+ return paginate([], [], 0, limit, offset);
7563
7793
  });
7564
7794
  sdk.registerFunction("mem::graph-stats", async () => {
7565
- const nodes = await kv.list(KV.graphNodes);
7566
- const edges = await kv.list(KV.graphEdges);
7567
- const nodesByType = {};
7568
- for (const n of nodes) nodesByType[n.type] = (nodesByType[n.type] || 0) + 1;
7569
- const edgesByType = {};
7570
- for (const e of edges) edgesByType[e.type] = (edgesByType[e.type] || 0) + 1;
7795
+ const snap = await readSnapshot(kv);
7796
+ if (snap) return {
7797
+ ...snap.stats,
7798
+ fromSnapshot: true,
7799
+ updatedAt: snap.updatedAt,
7800
+ ...snap.dirty ? { warning: "Snapshot is marked dirty (write was in-flight when read). Counts are eventually consistent." } : {}
7801
+ };
7571
7802
  return {
7572
- totalNodes: nodes.length,
7573
- totalEdges: edges.length,
7574
- nodesByType,
7575
- edgesByType
7803
+ totalNodes: 0,
7804
+ totalEdges: 0,
7805
+ nodesByType: {},
7806
+ edgesByType: {},
7807
+ fromSnapshot: false,
7808
+ 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."
7809
+ };
7810
+ });
7811
+ sdk.registerFunction("mem::graph-snapshot-rebuild", async (data) => {
7812
+ const started = Date.now();
7813
+ const forceRebuild = data?.force === true;
7814
+ try {
7815
+ if (!await readSnapshot(kv) && !forceRebuild) {
7816
+ logger.warn("Graph snapshot rebuild refused: no prior snapshot", { hint: "legacy corpus or empty store" });
7817
+ return {
7818
+ success: false,
7819
+ legacyCorpus: true,
7820
+ 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."
7821
+ };
7822
+ }
7823
+ } catch (err) {
7824
+ const msg = err instanceof Error ? err.message : String(err);
7825
+ logger.warn("Graph snapshot pre-flight read failed", { error: msg });
7826
+ }
7827
+ try {
7828
+ const [nodes, edges] = await withTimeout(Promise.all([kv.list(KV.graphNodes), kv.list(KV.graphEdges)]), LIVE_ENUMERATION_BUDGET_MS, "graph-snapshot-rebuild enumeration");
7829
+ if (nodes.length > REBUILD_SAFE_NODE_CEILING) {
7830
+ logger.warn("Graph snapshot rebuild aborted: corpus too large", {
7831
+ totalNodes: nodes.length,
7832
+ ceiling: REBUILD_SAFE_NODE_CEILING
7833
+ });
7834
+ return {
7835
+ success: false,
7836
+ tooLarge: true,
7837
+ totalNodes: nodes.length,
7838
+ ceiling: REBUILD_SAFE_NODE_CEILING,
7839
+ 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.`
7840
+ };
7841
+ }
7842
+ const liveNodes = nodes.filter((n) => !n.stale);
7843
+ const liveEdges = edges.filter((e) => !e.stale);
7844
+ const degree = /* @__PURE__ */ new Map();
7845
+ for (const e of liveEdges) {
7846
+ degree.set(e.sourceNodeId, (degree.get(e.sourceNodeId) ?? 0) + 1);
7847
+ degree.set(e.targetNodeId, (degree.get(e.targetNodeId) ?? 0) + 1);
7848
+ }
7849
+ const BATCH_SIZE = 100;
7850
+ for (let i = 0; i < liveNodes.length; i += BATCH_SIZE) {
7851
+ const batch = liveNodes.slice(i, i + BATCH_SIZE);
7852
+ 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)]));
7853
+ }
7854
+ for (let i = 0; i < liveEdges.length; i += BATCH_SIZE) {
7855
+ const batch = liveEdges.slice(i, i + BATCH_SIZE);
7856
+ await Promise.all(batch.map((e) => kv.set(KV.graphEdgeKey, edgeIndexKey(e.sourceNodeId, e.targetNodeId, e.type), e.id)));
7857
+ }
7858
+ const snap = buildSnapshotFromArrays(nodes, edges);
7859
+ await kv.set(KV.graphSnapshot, SNAPSHOT_KEY, snap);
7860
+ const tookMs = Date.now() - started;
7861
+ logger.info("Graph snapshot rebuilt", {
7862
+ totalNodes: snap.stats.totalNodes,
7863
+ totalEdges: snap.stats.totalEdges,
7864
+ topNodes: snap.topNodes.length,
7865
+ topEdges: snap.topEdges.length,
7866
+ tookMs
7867
+ });
7868
+ return {
7869
+ success: true,
7870
+ ...snap.stats,
7871
+ topNodes: snap.topNodes.length,
7872
+ topEdges: snap.topEdges.length,
7873
+ updatedAt: snap.updatedAt,
7874
+ tookMs
7875
+ };
7876
+ } catch (err) {
7877
+ const msg = err instanceof Error ? err.message : String(err);
7878
+ logger.error("Graph snapshot rebuild failed", { error: msg });
7879
+ return {
7880
+ success: false,
7881
+ error: msg
7882
+ };
7883
+ }
7884
+ });
7885
+ sdk.registerFunction("mem::graph-reset", async () => {
7886
+ const started = Date.now();
7887
+ const resetSnapshot = {
7888
+ ...emptySnapshot(),
7889
+ resetAt: (/* @__PURE__ */ new Date()).toISOString()
7890
+ };
7891
+ await kv.set(KV.graphSnapshot, SNAPSHOT_KEY, resetSnapshot);
7892
+ const counts = { [KV.graphSnapshot]: 1 };
7893
+ const tookMs = Date.now() - started;
7894
+ logger.info("Graph state reset", {
7895
+ counts,
7896
+ tookMs
7897
+ });
7898
+ return {
7899
+ success: true,
7900
+ cleared: counts,
7901
+ tookMs
7576
7902
  };
7577
7903
  });
7578
7904
  }
@@ -13395,7 +13721,7 @@ function registerRetentionFunctions(sdk, kv) {
13395
13721
  const threshold = typeof data?.threshold === "number" && Number.isFinite(data.threshold) ? data.threshold : DEFAULT_DECAY.tierThresholds.cold;
13396
13722
  const maxEvictRaw = typeof data?.maxEvict === "number" && Number.isInteger(data.maxEvict) ? data.maxEvict : 50;
13397
13723
  const maxEvict = Math.min(1e3, Math.max(0, maxEvictRaw));
13398
- const { decrementImageRef } = await import("./image-refs-C7h9L5wx.mjs").then((n) => n.n);
13724
+ const { decrementImageRef } = await import("./image-refs-B5Zf_Djo.mjs").then((n) => n.n);
13399
13725
  const candidates = (await kv.list(KV.retentionScores)).filter((s) => s.score < threshold).sort((a, b) => a.score - b.score).slice(0, maxEvict);
13400
13726
  if (data?.dryRun) return {
13401
13727
  success: true,
@@ -14887,6 +15213,7 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
14887
15213
  });
14888
15214
  sdk.registerFunction("api::search", async (req) => {
14889
15215
  const body = req.body ?? {};
15216
+ const queryAgentId = typeof req.query_params?.["agentId"] === "string" ? req.query_params["agentId"] : void 0;
14890
15217
  if (typeof body.query !== "string" || !body.query.trim()) return {
14891
15218
  status_code: 400,
14892
15219
  body: { error: "query is required and must be a non-empty string" }
@@ -14915,13 +15242,15 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
14915
15242
  status_code: 400,
14916
15243
  body: { error: "token_budget must be a positive integer" }
14917
15244
  };
15245
+ const bodyAgentId = typeof body.agentId === "string" && body.agentId.trim().length > 0 ? body.agentId.trim() : void 0;
14918
15246
  const payload = {
14919
15247
  query: body.query.trim(),
14920
15248
  limit: body.limit,
14921
15249
  project: body.project,
14922
15250
  cwd: body.cwd,
14923
15251
  format: typeof body.format === "string" ? body.format.trim().toLowerCase() : void 0,
14924
- token_budget: body.token_budget
15252
+ token_budget: body.token_budget,
15253
+ agentId: bodyAgentId ?? queryAgentId
14925
15254
  };
14926
15255
  return {
14927
15256
  status_code: 200,
@@ -15857,6 +16186,52 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
15857
16186
  http_method: "GET"
15858
16187
  }
15859
16188
  });
16189
+ sdk.registerFunction("api::graph-snapshot-rebuild", async (req) => {
16190
+ const authErr = checkAuth(req, secret);
16191
+ if (authErr) return authErr;
16192
+ try {
16193
+ return {
16194
+ status_code: 200,
16195
+ body: await sdk.trigger({
16196
+ function_id: "mem::graph-snapshot-rebuild",
16197
+ payload: {}
16198
+ })
16199
+ };
16200
+ } catch {
16201
+ return graphDisabledResponse();
16202
+ }
16203
+ });
16204
+ sdk.registerTrigger({
16205
+ type: "http",
16206
+ function_id: "api::graph-snapshot-rebuild",
16207
+ config: {
16208
+ api_path: "/agentmemory/graph/snapshot-rebuild",
16209
+ http_method: "POST"
16210
+ }
16211
+ });
16212
+ sdk.registerFunction("api::graph-reset", async (req) => {
16213
+ const authErr = checkAuth(req, secret);
16214
+ if (authErr) return authErr;
16215
+ try {
16216
+ return {
16217
+ status_code: 200,
16218
+ body: await sdk.trigger({
16219
+ function_id: "mem::graph-reset",
16220
+ payload: {}
16221
+ })
16222
+ };
16223
+ } catch {
16224
+ return graphDisabledResponse();
16225
+ }
16226
+ });
16227
+ sdk.registerTrigger({
16228
+ type: "http",
16229
+ function_id: "api::graph-reset",
16230
+ config: {
16231
+ api_path: "/agentmemory/graph/reset",
16232
+ http_method: "POST"
16233
+ }
16234
+ });
15860
16235
  sdk.registerFunction("api::graph-extract", async (req) => {
15861
16236
  const authErr = checkAuth(req, secret);
15862
16237
  if (authErr) return authErr;
@@ -18291,13 +18666,15 @@ function registerMcpEndpoints(sdk, kv, secret) {
18291
18666
  status_code: 400,
18292
18667
  body: { error: "token_budget must be a positive integer" }
18293
18668
  };
18669
+ const recallAgentId = typeof args.agentId === "string" && args.agentId.trim().length > 0 ? args.agentId.trim() : void 0;
18294
18670
  const result = await sdk.trigger({
18295
18671
  function_id: "mem::search",
18296
18672
  payload: {
18297
18673
  query: args.query,
18298
18674
  limit: typeof args.limit === "number" ? args.limit : 10,
18299
18675
  format,
18300
- token_budget: tokenBudget
18676
+ token_budget: tokenBudget,
18677
+ agentId: recallAgentId
18301
18678
  }
18302
18679
  });
18303
18680
  return {
@@ -19785,7 +20162,10 @@ function registerMcpEndpoints(sdk, kv, secret) {
19785
20162
  limit: 10
19786
20163
  }
19787
20164
  }).catch(() => ({ results: [] }));
19788
- const relevant = (await kv.list(KV.memories)).filter((m) => m.isLatest).slice(0, 5);
20165
+ const memories = await kv.list(KV.memories);
20166
+ const isolated = isAgentScopeIsolated();
20167
+ const activeAgentId = isolated ? getAgentId() : void 0;
20168
+ const relevant = isolated && activeAgentId === void 0 ? [] : memories.filter((m) => m.isLatest).filter((m) => activeAgentId === void 0 || m.agentId === activeAgentId).slice(0, 5);
19789
20169
  return {
19790
20170
  status_code: 200,
19791
20171
  body: { messages: [{
@@ -20156,7 +20536,7 @@ async function main() {
20156
20536
  console.warn(`[agentmemory] Failed to backfill memories into BM25:`, err);
20157
20537
  }
20158
20538
  bootLog(`Ready. ${embeddingProvider ? "Triple-stream (BM25+Vector+Graph)" : "BM25+Graph"} search active.`);
20159
- bootLog(`REST API: 126 endpoints at http://localhost:${config.restPort}/agentmemory/*`);
20539
+ bootLog(`REST API: 128 endpoints at http://localhost:${config.restPort}/agentmemory/*`);
20160
20540
  bootLog(`MCP surface (opt-in via \`npx @agentmemory/mcp\`): ${getAllTools().length} tools · 6 resources · 3 prompts`);
20161
20541
  const viewerServer = startViewerServer(config.restPort + 2, kv, sdk, secret, config.restPort);
20162
20542
  const autoForgetIntervalMs = parseInt(process.env.AUTO_FORGET_INTERVAL_MS || "3600000", 10);
@@ -20233,4 +20613,4 @@ main().catch((err) => {
20233
20613
  //#endregion
20234
20614
  export {};
20235
20615
 
20236
- //# sourceMappingURL=src-u7kAEUC0.mjs.map
20616
+ //# sourceMappingURL=src-MOg2zKJs.mjs.map