@agentmemory/agentmemory 0.9.25 → 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/AGENTS.md +1 -1
- package/README.md +10 -8
- package/dist/cli.d.mts.map +1 -1
- package/dist/cli.mjs +41 -24
- package/dist/cli.mjs.map +1 -1
- package/dist/{image-refs-C7h9L5wx.mjs → image-refs-B5Zf_Djo.mjs} +2 -2
- package/dist/{image-refs-C7h9L5wx.mjs.map → image-refs-B5Zf_Djo.mjs.map} +1 -1
- package/dist/index.mjs +445 -58
- package/dist/index.mjs.map +1 -1
- package/dist/{schema-Dsr_V2Wp.mjs → schema-CNiCWzV9.mjs} +5 -1
- package/dist/schema-CNiCWzV9.mjs.map +1 -0
- package/dist/{src-fQOMXeCp.mjs → src-MOg2zKJs.mjs} +445 -64
- package/dist/src-MOg2zKJs.mjs.map +1 -0
- package/dist/{standalone-BzfA1zu8.mjs → standalone-BORXC71-.mjs} +4 -4
- package/dist/{standalone-BzfA1zu8.mjs.map → standalone-BORXC71-.mjs.map} +1 -1
- package/dist/standalone.mjs +1 -1
- package/dist/standalone.mjs.map +1 -1
- package/dist/{tools-registry-Dzxv9iUu.mjs → tools-registry-D8XVVPbJ.mjs} +6 -4
- package/dist/tools-registry-D8XVVPbJ.mjs.map +1 -0
- package/dist/version-9DNrRyph.mjs +6 -0
- package/dist/version-9DNrRyph.mjs.map +1 -0
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/plugin.json +1 -1
- package/dist/schema-Dsr_V2Wp.mjs.map +0 -1
- package/dist/src-fQOMXeCp.mjs.map +0 -1
- package/dist/tools-registry-Dzxv9iUu.mjs.map +0 -1
- package/dist/version-C3hZKw8n.mjs +0 -6
- package/dist/version-C3hZKw8n.mjs.map +0 -1
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"] ||
|
|
124
|
-
restPort
|
|
125
|
-
streamsPort
|
|
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`,
|
|
@@ -2988,7 +2994,7 @@ var IndexPersistence = class {
|
|
|
2988
2994
|
async loadShardedData(legacyKey, manifestKey, label) {
|
|
2989
2995
|
const manifest = await this.readIndexValue(KV.bm25Index, manifestKey, label, "manifest");
|
|
2990
2996
|
if (!manifest.ok) return null;
|
|
2991
|
-
if (manifest.value
|
|
2997
|
+
if (manifest.value != null && typeof manifest.value === "object") return this.loadManifestData(manifest.value, label);
|
|
2992
2998
|
const legacy = await this.readIndexValue(KV.bm25Index, legacyKey, label, "legacy");
|
|
2993
2999
|
if (!legacy.ok) return null;
|
|
2994
3000
|
if (legacy.value && typeof legacy.value === "string") return legacy.value;
|
|
@@ -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 >=
|
|
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)
|
|
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.
|
|
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
|
|
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) => {
|
|
@@ -7313,7 +7332,9 @@ function registerExportImportFunction(sdk, kv) {
|
|
|
7313
7332
|
"0.9.22",
|
|
7314
7333
|
"0.9.23",
|
|
7315
7334
|
"0.9.24",
|
|
7316
|
-
"0.9.25"
|
|
7335
|
+
"0.9.25",
|
|
7336
|
+
"0.9.26",
|
|
7337
|
+
"0.9.27"
|
|
7317
7338
|
]).has(importData.version)) return {
|
|
7318
7339
|
success: false,
|
|
7319
7340
|
error: `Unsupported export version: ${importData.version}`
|
|
@@ -7837,19 +7858,167 @@ function buildGraphExtractionPrompt(observations) {
|
|
|
7837
7858
|
//#region src/functions/graph.ts
|
|
7838
7859
|
const DEFAULT_GRAPH_QUERY_LIMIT = 500;
|
|
7839
7860
|
const MAX_GRAPH_QUERY_LIMIT = 5e3;
|
|
7840
|
-
|
|
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() {
|
|
7841
7877
|
return {
|
|
7842
|
-
|
|
7843
|
-
|
|
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
|
|
7844
7890
|
};
|
|
7845
7891
|
}
|
|
7846
|
-
function
|
|
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);
|
|
7847
7905
|
const degree = /* @__PURE__ */ new Map();
|
|
7848
|
-
for (const
|
|
7849
|
-
degree.set(
|
|
7850
|
-
degree.set(
|
|
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);
|
|
7851
7994
|
}
|
|
7852
|
-
|
|
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
|
+
};
|
|
7853
8022
|
}
|
|
7854
8023
|
function paginate(nodes, allEdges, depth, limit, offset) {
|
|
7855
8024
|
const totalNodes = nodes.length;
|
|
@@ -7946,45 +8115,76 @@ function registerGraphFunction(sdk, kv, provider) {
|
|
|
7946
8115
|
const response = await provider.compress(GRAPH_EXTRACTION_SYSTEM, prompt);
|
|
7947
8116
|
const obsIds = data.observations.map((o) => o.id);
|
|
7948
8117
|
const { nodes, edges } = parseGraphXml(response, obsIds);
|
|
7949
|
-
const
|
|
7950
|
-
const
|
|
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 = [];
|
|
7951
8123
|
for (const node of nodes) {
|
|
7952
|
-
const
|
|
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
|
+
}
|
|
7953
8131
|
if (existing) {
|
|
7954
|
-
const merged =
|
|
7955
|
-
...existing,
|
|
7956
|
-
sourceObservationIds: [...new Set([...existing.sourceObservationIds, ...obsIds])],
|
|
7957
|
-
properties: {
|
|
7958
|
-
...existing.properties,
|
|
7959
|
-
...node.properties
|
|
7960
|
-
}
|
|
7961
|
-
};
|
|
8132
|
+
const merged = mergeNode(existing, node, obsIds, capturedAt);
|
|
7962
8133
|
await kv.set(KV.graphNodes, existing.id, merged);
|
|
7963
|
-
const
|
|
7964
|
-
if (
|
|
8134
|
+
const topIdx = snap.topNodes.findIndex((n) => n.id === existing.id);
|
|
8135
|
+
if (topIdx !== -1) snap.topNodes[topIdx] = merged;
|
|
7965
8136
|
} else {
|
|
7966
8137
|
await kv.set(KV.graphNodes, node.id, node);
|
|
7967
|
-
|
|
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
|
+
}
|
|
7968
8147
|
}
|
|
7969
8148
|
}
|
|
7970
8149
|
for (const edge of edges) {
|
|
7971
|
-
const
|
|
7972
|
-
const
|
|
7973
|
-
|
|
7974
|
-
|
|
7975
|
-
await kv.
|
|
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;
|
|
7976
8162
|
} else {
|
|
7977
8163
|
await kv.set(KV.graphEdges, edge.id, edge);
|
|
7978
|
-
|
|
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);
|
|
7979
8171
|
}
|
|
7980
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
|
+
}
|
|
7981
8179
|
await recordAudit(kv, "observe", "mem::graph-extract", obsIds, {
|
|
7982
8180
|
nodesExtracted: nodes.length,
|
|
7983
8181
|
edgesExtracted: edges.length
|
|
7984
8182
|
});
|
|
7985
8183
|
logger.info("Graph extraction complete", {
|
|
7986
8184
|
nodes: nodes.length,
|
|
7987
|
-
edges: edges.length
|
|
8185
|
+
edges: edges.length,
|
|
8186
|
+
newNodes: newNodeCount,
|
|
8187
|
+
newEdges: newEdgeCount
|
|
7988
8188
|
});
|
|
7989
8189
|
return {
|
|
7990
8190
|
success: true,
|
|
@@ -8001,10 +8201,49 @@ function registerGraphFunction(sdk, kv, provider) {
|
|
|
8001
8201
|
}
|
|
8002
8202
|
});
|
|
8003
8203
|
sdk.registerFunction("mem::graph-query", async (data) => {
|
|
8004
|
-
const allNodes = (await kv.list(KV.graphNodes)).filter((n) => !n.stale);
|
|
8005
|
-
const allEdges = (await kv.list(KV.graphEdges)).filter((e) => !e.stale);
|
|
8006
8204
|
const maxDepth = Math.min(data.maxDepth || 3, 5);
|
|
8007
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
|
+
}
|
|
8008
8247
|
if (data.query) {
|
|
8009
8248
|
const lower = data.query.toLowerCase();
|
|
8010
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);
|
|
@@ -8041,22 +8280,116 @@ function registerGraphFunction(sdk, kv, provider) {
|
|
|
8041
8280
|
}
|
|
8042
8281
|
return paginate(resultNodes, resultEdges, maxDepth, limit, offset);
|
|
8043
8282
|
}
|
|
8044
|
-
|
|
8045
|
-
if (data.nodeType) filtered = allNodes.filter((n) => n.type === data.nodeType);
|
|
8046
|
-
return paginate(rankByDegree(filtered, allEdges), allEdges, 0, limit, offset);
|
|
8283
|
+
return paginate([], [], 0, limit, offset);
|
|
8047
8284
|
});
|
|
8048
8285
|
sdk.registerFunction("mem::graph-stats", async () => {
|
|
8049
|
-
const
|
|
8050
|
-
|
|
8051
|
-
|
|
8052
|
-
|
|
8053
|
-
|
|
8054
|
-
|
|
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
|
+
};
|
|
8055
8293
|
return {
|
|
8056
|
-
totalNodes:
|
|
8057
|
-
totalEdges:
|
|
8058
|
-
nodesByType,
|
|
8059
|
-
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
|
|
8060
8393
|
};
|
|
8061
8394
|
});
|
|
8062
8395
|
}
|
|
@@ -15371,6 +15704,7 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
|
|
|
15371
15704
|
});
|
|
15372
15705
|
sdk.registerFunction("api::search", async (req) => {
|
|
15373
15706
|
const body = req.body ?? {};
|
|
15707
|
+
const queryAgentId = typeof req.query_params?.["agentId"] === "string" ? req.query_params["agentId"] : void 0;
|
|
15374
15708
|
if (typeof body.query !== "string" || !body.query.trim()) return {
|
|
15375
15709
|
status_code: 400,
|
|
15376
15710
|
body: { error: "query is required and must be a non-empty string" }
|
|
@@ -15399,13 +15733,15 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
|
|
|
15399
15733
|
status_code: 400,
|
|
15400
15734
|
body: { error: "token_budget must be a positive integer" }
|
|
15401
15735
|
};
|
|
15736
|
+
const bodyAgentId = typeof body.agentId === "string" && body.agentId.trim().length > 0 ? body.agentId.trim() : void 0;
|
|
15402
15737
|
const payload = {
|
|
15403
15738
|
query: body.query.trim(),
|
|
15404
15739
|
limit: body.limit,
|
|
15405
15740
|
project: body.project,
|
|
15406
15741
|
cwd: body.cwd,
|
|
15407
15742
|
format: typeof body.format === "string" ? body.format.trim().toLowerCase() : void 0,
|
|
15408
|
-
token_budget: body.token_budget
|
|
15743
|
+
token_budget: body.token_budget,
|
|
15744
|
+
agentId: bodyAgentId ?? queryAgentId
|
|
15409
15745
|
};
|
|
15410
15746
|
return {
|
|
15411
15747
|
status_code: 200,
|
|
@@ -16341,6 +16677,52 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
|
|
|
16341
16677
|
http_method: "GET"
|
|
16342
16678
|
}
|
|
16343
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
|
+
});
|
|
16344
16726
|
sdk.registerFunction("api::graph-extract", async (req) => {
|
|
16345
16727
|
const authErr = checkAuth(req, secret);
|
|
16346
16728
|
if (authErr) return authErr;
|
|
@@ -19884,13 +20266,15 @@ function registerMcpEndpoints(sdk, kv, secret) {
|
|
|
19884
20266
|
status_code: 400,
|
|
19885
20267
|
body: { error: "token_budget must be a positive integer" }
|
|
19886
20268
|
};
|
|
20269
|
+
const recallAgentId = typeof args.agentId === "string" && args.agentId.trim().length > 0 ? args.agentId.trim() : void 0;
|
|
19887
20270
|
const result = await sdk.trigger({
|
|
19888
20271
|
function_id: "mem::search",
|
|
19889
20272
|
payload: {
|
|
19890
20273
|
query: args.query,
|
|
19891
20274
|
limit: typeof args.limit === "number" ? args.limit : 10,
|
|
19892
20275
|
format,
|
|
19893
|
-
token_budget: tokenBudget
|
|
20276
|
+
token_budget: tokenBudget,
|
|
20277
|
+
agentId: recallAgentId
|
|
19894
20278
|
}
|
|
19895
20279
|
});
|
|
19896
20280
|
return {
|
|
@@ -21378,7 +21762,10 @@ function registerMcpEndpoints(sdk, kv, secret) {
|
|
|
21378
21762
|
limit: 10
|
|
21379
21763
|
}
|
|
21380
21764
|
}).catch(() => ({ results: [] }));
|
|
21381
|
-
const
|
|
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);
|
|
21382
21769
|
return {
|
|
21383
21770
|
status_code: 200,
|
|
21384
21771
|
body: { messages: [{
|
|
@@ -21749,7 +22136,7 @@ async function main() {
|
|
|
21749
22136
|
console.warn(`[agentmemory] Failed to backfill memories into BM25:`, err);
|
|
21750
22137
|
}
|
|
21751
22138
|
bootLog(`Ready. ${embeddingProvider ? "Triple-stream (BM25+Vector+Graph)" : "BM25+Graph"} search active.`);
|
|
21752
|
-
bootLog(`REST API:
|
|
22139
|
+
bootLog(`REST API: 128 endpoints at http://localhost:${config.restPort}/agentmemory/*`);
|
|
21753
22140
|
bootLog(`MCP surface (opt-in via \`npx @agentmemory/mcp\`): ${getAllTools().length} tools · 6 resources · 3 prompts`);
|
|
21754
22141
|
const viewerServer = startViewerServer(config.restPort + 2, kv, sdk, secret, config.restPort);
|
|
21755
22142
|
const autoForgetIntervalMs = parseInt(process.env.AUTO_FORGET_INTERVAL_MS || "3600000", 10);
|