@agentmemory/agentmemory 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +14 -0
- package/.github/workflows/ci.yml +22 -0
- package/.github/workflows/publish.yml +28 -0
- package/AGENTS.md +113 -0
- package/LICENSE +190 -0
- package/README.md +828 -0
- package/assets/banner.png +0 -0
- package/assets/demo.gif +0 -0
- package/assets/demo.mp4 +0 -0
- package/benchmark/QUALITY.md +73 -0
- package/benchmark/REAL-EMBEDDINGS.md +67 -0
- package/benchmark/SCALE.md +110 -0
- package/benchmark/dataset.ts +293 -0
- package/benchmark/quality-eval.ts +643 -0
- package/benchmark/real-embeddings-eval.ts +405 -0
- package/benchmark/scale-eval.ts +398 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +137 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/docker-compose.yml +14 -0
- package/dist/hooks/notification.d.mts +1 -0
- package/dist/hooks/notification.mjs +45 -0
- package/dist/hooks/notification.mjs.map +1 -0
- package/dist/hooks/post-tool-failure.d.mts +1 -0
- package/dist/hooks/post-tool-failure.mjs +45 -0
- package/dist/hooks/post-tool-failure.mjs.map +1 -0
- package/dist/hooks/post-tool-use.d.mts +1 -0
- package/dist/hooks/post-tool-use.mjs +53 -0
- package/dist/hooks/post-tool-use.mjs.map +1 -0
- package/dist/hooks/pre-compact.d.mts +1 -0
- package/dist/hooks/pre-compact.mjs +50 -0
- package/dist/hooks/pre-compact.mjs.map +1 -0
- package/dist/hooks/pre-tool-use.d.mts +1 -0
- package/dist/hooks/pre-tool-use.mjs +69 -0
- package/dist/hooks/pre-tool-use.mjs.map +1 -0
- package/dist/hooks/prompt-submit.d.mts +1 -0
- package/dist/hooks/prompt-submit.mjs +40 -0
- package/dist/hooks/prompt-submit.mjs.map +1 -0
- package/dist/hooks/session-end.d.mts +1 -0
- package/dist/hooks/session-end.mjs +61 -0
- package/dist/hooks/session-end.mjs.map +1 -0
- package/dist/hooks/session-start.d.mts +1 -0
- package/dist/hooks/session-start.mjs +42 -0
- package/dist/hooks/session-start.mjs.map +1 -0
- package/dist/hooks/stop.d.mts +1 -0
- package/dist/hooks/stop.mjs +33 -0
- package/dist/hooks/stop.mjs.map +1 -0
- package/dist/hooks/subagent-start.d.mts +1 -0
- package/dist/hooks/subagent-start.mjs +43 -0
- package/dist/hooks/subagent-start.mjs.map +1 -0
- package/dist/hooks/subagent-stop.d.mts +1 -0
- package/dist/hooks/subagent-stop.mjs +45 -0
- package/dist/hooks/subagent-stop.mjs.map +1 -0
- package/dist/hooks/task-completed.d.mts +1 -0
- package/dist/hooks/task-completed.mjs +46 -0
- package/dist/hooks/task-completed.mjs.map +1 -0
- package/dist/iii-config.yaml +51 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +13776 -0
- package/dist/index.mjs.map +1 -0
- package/dist/src-QxitMPfJ.mjs +13775 -0
- package/dist/src-QxitMPfJ.mjs.map +1 -0
- package/dist/standalone.d.mts +1 -0
- package/dist/standalone.mjs +1155 -0
- package/dist/standalone.mjs.map +1 -0
- package/dist/transformers-BX_tgxdO.mjs +38684 -0
- package/dist/transformers-BX_tgxdO.mjs.map +1 -0
- package/dist/transformers-KMm1i9no.mjs +38683 -0
- package/dist/transformers-KMm1i9no.mjs.map +1 -0
- package/docker-compose.yml +14 -0
- package/iii-config.yaml +51 -0
- package/package.json +59 -0
- package/plugin/.claude-plugin/plugin.json +10 -0
- package/plugin/hooks/hooks.json +77 -0
- package/plugin/scripts/diagnostics.mjs +551 -0
- package/plugin/scripts/notification.mjs +45 -0
- package/plugin/scripts/post-tool-failure.mjs +45 -0
- package/plugin/scripts/post-tool-use.mjs +53 -0
- package/plugin/scripts/pre-compact.mjs +50 -0
- package/plugin/scripts/pre-tool-use.mjs +69 -0
- package/plugin/scripts/prompt-submit.mjs +40 -0
- package/plugin/scripts/session-end.mjs +61 -0
- package/plugin/scripts/session-start.mjs +42 -0
- package/plugin/scripts/stop.mjs +33 -0
- package/plugin/scripts/subagent-start.mjs +43 -0
- package/plugin/scripts/subagent-stop.mjs +45 -0
- package/plugin/scripts/task-completed.mjs +46 -0
- package/plugin/skills/forget/SKILL.md +32 -0
- package/plugin/skills/recall/SKILL.md +18 -0
- package/plugin/skills/remember/SKILL.md +25 -0
- package/plugin/skills/session-history/SKILL.md +17 -0
- package/src/auth.ts +12 -0
- package/src/cli.ts +159 -0
- package/src/config.ts +221 -0
- package/src/eval/metrics-store.ts +65 -0
- package/src/eval/quality.ts +51 -0
- package/src/eval/schemas.ts +124 -0
- package/src/eval/self-correct.ts +28 -0
- package/src/eval/validator.ts +31 -0
- package/src/functions/actions.ts +288 -0
- package/src/functions/audit.ts +61 -0
- package/src/functions/auto-forget.ts +169 -0
- package/src/functions/branch-aware.ts +169 -0
- package/src/functions/cascade.ts +80 -0
- package/src/functions/checkpoints.ts +209 -0
- package/src/functions/claude-bridge.ts +161 -0
- package/src/functions/compress.ts +194 -0
- package/src/functions/consolidate.ts +212 -0
- package/src/functions/consolidation-pipeline.ts +258 -0
- package/src/functions/context.ts +169 -0
- package/src/functions/crystallize.ts +293 -0
- package/src/functions/dedup.ts +57 -0
- package/src/functions/diagnostics.ts +785 -0
- package/src/functions/enrich.ts +132 -0
- package/src/functions/evict.ts +163 -0
- package/src/functions/export-import.ts +508 -0
- package/src/functions/facets.ts +248 -0
- package/src/functions/file-index.ts +106 -0
- package/src/functions/flow-compress.ts +214 -0
- package/src/functions/frontier.ts +196 -0
- package/src/functions/governance.ts +131 -0
- package/src/functions/graph-retrieval.ts +277 -0
- package/src/functions/graph.ts +275 -0
- package/src/functions/leases.ts +216 -0
- package/src/functions/lessons.ts +253 -0
- package/src/functions/mesh.ts +434 -0
- package/src/functions/migrate.ts +165 -0
- package/src/functions/observe.ts +144 -0
- package/src/functions/obsidian-export.ts +310 -0
- package/src/functions/patterns.ts +138 -0
- package/src/functions/privacy.ts +39 -0
- package/src/functions/profile.ts +155 -0
- package/src/functions/query-expansion.ts +186 -0
- package/src/functions/relations.ts +237 -0
- package/src/functions/remember.ts +162 -0
- package/src/functions/retention.ts +235 -0
- package/src/functions/routines.ts +289 -0
- package/src/functions/search.ts +80 -0
- package/src/functions/sentinels.ts +417 -0
- package/src/functions/signals.ts +186 -0
- package/src/functions/sketches.ts +274 -0
- package/src/functions/sliding-window.ts +257 -0
- package/src/functions/smart-search.ts +115 -0
- package/src/functions/snapshot.ts +219 -0
- package/src/functions/summarize.ts +155 -0
- package/src/functions/team.ts +147 -0
- package/src/functions/temporal-graph.ts +476 -0
- package/src/functions/timeline.ts +138 -0
- package/src/functions/verify.ts +117 -0
- package/src/health/monitor.ts +110 -0
- package/src/health/thresholds.ts +73 -0
- package/src/hooks/notification.ts +52 -0
- package/src/hooks/post-tool-failure.ts +58 -0
- package/src/hooks/post-tool-use.ts +62 -0
- package/src/hooks/pre-compact.ts +60 -0
- package/src/hooks/pre-tool-use.ts +72 -0
- package/src/hooks/prompt-submit.ts +46 -0
- package/src/hooks/session-end.ts +71 -0
- package/src/hooks/session-start.ts +48 -0
- package/src/hooks/stop.ts +39 -0
- package/src/hooks/subagent-start.ts +49 -0
- package/src/hooks/subagent-stop.ts +54 -0
- package/src/hooks/task-completed.ts +54 -0
- package/src/index.ts +342 -0
- package/src/mcp/in-memory-kv.ts +61 -0
- package/src/mcp/server.ts +1455 -0
- package/src/mcp/standalone.ts +177 -0
- package/src/mcp/tools-registry.ts +769 -0
- package/src/mcp/transport.ts +91 -0
- package/src/prompts/compression.ts +67 -0
- package/src/prompts/consolidation.ts +48 -0
- package/src/prompts/graph-extraction.ts +35 -0
- package/src/prompts/summary.ts +38 -0
- package/src/prompts/xml.ts +26 -0
- package/src/providers/agent-sdk.ts +34 -0
- package/src/providers/anthropic.ts +35 -0
- package/src/providers/circuit-breaker.ts +82 -0
- package/src/providers/embedding/cohere.ts +46 -0
- package/src/providers/embedding/gemini.ts +54 -0
- package/src/providers/embedding/index.ts +39 -0
- package/src/providers/embedding/local.ts +52 -0
- package/src/providers/embedding/openai.ts +45 -0
- package/src/providers/embedding/openrouter.ts +51 -0
- package/src/providers/embedding/voyage.ts +46 -0
- package/src/providers/fallback-chain.ts +31 -0
- package/src/providers/index.ts +84 -0
- package/src/providers/openrouter.ts +71 -0
- package/src/providers/resilient.ts +37 -0
- package/src/state/hybrid-search.ts +295 -0
- package/src/state/index-persistence.ts +63 -0
- package/src/state/keyed-mutex.ts +18 -0
- package/src/state/kv.ts +33 -0
- package/src/state/schema.ts +71 -0
- package/src/state/search-index.ts +245 -0
- package/src/state/stemmer.ts +104 -0
- package/src/state/synonyms.ts +63 -0
- package/src/state/vector-index.ts +130 -0
- package/src/telemetry/setup.ts +116 -0
- package/src/triggers/api.ts +1904 -0
- package/src/triggers/events.ts +71 -0
- package/src/types.ts +769 -0
- package/src/version.ts +1 -0
- package/src/viewer/index.html +2497 -0
- package/src/viewer/server.ts +207 -0
- package/src/xenova.d.ts +3 -0
- package/test/actions.test.ts +490 -0
- package/test/audit.test.ts +108 -0
- package/test/auto-forget.test.ts +188 -0
- package/test/cascade.test.ts +277 -0
- package/test/checkpoints.test.ts +493 -0
- package/test/circuit-breaker.test.ts +107 -0
- package/test/claude-bridge.test.ts +178 -0
- package/test/confidence.test.ts +247 -0
- package/test/consistency.test.ts +61 -0
- package/test/consolidation-pipeline.test.ts +251 -0
- package/test/crystallize.test.ts +521 -0
- package/test/diagnostics.test.ts +638 -0
- package/test/embedding-provider.test.ts +49 -0
- package/test/enrich.test.ts +209 -0
- package/test/eval.test.ts +300 -0
- package/test/export-import.test.ts +251 -0
- package/test/facets.test.ts +448 -0
- package/test/fallback-chain.test.ts +93 -0
- package/test/frontier.test.ts +485 -0
- package/test/governance.test.ts +147 -0
- package/test/graph-retrieval.test.ts +186 -0
- package/test/graph.test.ts +160 -0
- package/test/helpers/mocks.ts +40 -0
- package/test/hybrid-search.test.ts +145 -0
- package/test/index-persistence.test.ts +124 -0
- package/test/integration.test.ts +265 -0
- package/test/leases.test.ts +399 -0
- package/test/mcp-prompts.test.ts +218 -0
- package/test/mcp-resources.test.ts +286 -0
- package/test/mcp-standalone.test.ts +113 -0
- package/test/mesh.test.ts +700 -0
- package/test/privacy.test.ts +87 -0
- package/test/profile.test.ts +161 -0
- package/test/query-expansion.test.ts +154 -0
- package/test/relations.test.ts +198 -0
- package/test/retention.test.ts +245 -0
- package/test/routines.test.ts +497 -0
- package/test/schema-fingerprint.test.ts +81 -0
- package/test/schema.test.ts +42 -0
- package/test/search-index.test.ts +128 -0
- package/test/sentinels.test.ts +626 -0
- package/test/signals.test.ts +410 -0
- package/test/sketches.test.ts +549 -0
- package/test/sliding-window.test.ts +199 -0
- package/test/smart-search.test.ts +169 -0
- package/test/snapshot.test.ts +165 -0
- package/test/team.test.ts +156 -0
- package/test/temporal-graph.test.ts +378 -0
- package/test/timeline.test.ts +148 -0
- package/test/vector-index.test.ts +79 -0
- package/test/verify.test.ts +209 -0
- package/test/xml.test.ts +65 -0
- package/tsconfig.json +22 -0
- package/tsdown.config.ts +62 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import type { ISdk } from "iii-sdk";
|
|
2
|
+
import type { StateKV } from "../state/kv.js";
|
|
3
|
+
import { KV, fingerprintId } from "../state/schema.js";
|
|
4
|
+
import type { Lesson } from "../types.js";
|
|
5
|
+
import { recordAudit } from "./audit.js";
|
|
6
|
+
|
|
7
|
+
function reinforceLesson(lesson: Lesson): void {
|
|
8
|
+
const now = new Date().toISOString();
|
|
9
|
+
lesson.reinforcements++;
|
|
10
|
+
lesson.confidence = Math.min(
|
|
11
|
+
1.0,
|
|
12
|
+
lesson.confidence + 0.1 * (1 - lesson.confidence),
|
|
13
|
+
);
|
|
14
|
+
lesson.lastReinforcedAt = now;
|
|
15
|
+
lesson.updatedAt = now;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function registerLessonsFunctions(sdk: ISdk, kv: StateKV): void {
|
|
19
|
+
sdk.registerFunction(
|
|
20
|
+
{ id: "mem::lesson-save" },
|
|
21
|
+
async (data: {
|
|
22
|
+
content: string;
|
|
23
|
+
context?: string;
|
|
24
|
+
confidence?: number;
|
|
25
|
+
project?: string;
|
|
26
|
+
tags?: string[];
|
|
27
|
+
source?: "crystal" | "manual" | "consolidation";
|
|
28
|
+
sourceIds?: string[];
|
|
29
|
+
}) => {
|
|
30
|
+
if (!data.content?.trim()) {
|
|
31
|
+
return { success: false, error: "content is required" };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const fp = fingerprintId("lsn", data.content.trim().toLowerCase());
|
|
35
|
+
const existing = await kv.get<Lesson>(KV.lessons, fp);
|
|
36
|
+
|
|
37
|
+
if (existing && !existing.deleted) {
|
|
38
|
+
reinforceLesson(existing);
|
|
39
|
+
if (data.context && !existing.context) {
|
|
40
|
+
existing.context = data.context;
|
|
41
|
+
}
|
|
42
|
+
await kv.set(KV.lessons, existing.id, existing);
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
await recordAudit(kv, "lesson_strengthen", "mem::lesson-save", [
|
|
46
|
+
existing.id,
|
|
47
|
+
]);
|
|
48
|
+
} catch {}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
success: true,
|
|
52
|
+
action: "strengthened",
|
|
53
|
+
lesson: existing,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const confidence =
|
|
58
|
+
typeof data.confidence === "number" &&
|
|
59
|
+
data.confidence >= 0 &&
|
|
60
|
+
data.confidence <= 1
|
|
61
|
+
? data.confidence
|
|
62
|
+
: 0.5;
|
|
63
|
+
|
|
64
|
+
const now = new Date().toISOString();
|
|
65
|
+
const lesson: Lesson = {
|
|
66
|
+
id: fp,
|
|
67
|
+
content: data.content.trim(),
|
|
68
|
+
context: data.context?.trim() || "",
|
|
69
|
+
confidence,
|
|
70
|
+
reinforcements: 0,
|
|
71
|
+
source: data.source || "manual",
|
|
72
|
+
sourceIds: data.sourceIds || [],
|
|
73
|
+
project: data.project,
|
|
74
|
+
tags: data.tags || [],
|
|
75
|
+
createdAt: now,
|
|
76
|
+
updatedAt: now,
|
|
77
|
+
decayRate: 0.05,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
await kv.set(KV.lessons, lesson.id, lesson);
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
await recordAudit(kv, "lesson_save", "mem::lesson-save", [lesson.id]);
|
|
84
|
+
} catch {}
|
|
85
|
+
|
|
86
|
+
return { success: true, action: "created", lesson };
|
|
87
|
+
},
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
sdk.registerFunction(
|
|
91
|
+
{ id: "mem::lesson-recall" },
|
|
92
|
+
async (data: {
|
|
93
|
+
query: string;
|
|
94
|
+
project?: string;
|
|
95
|
+
minConfidence?: number;
|
|
96
|
+
limit?: number;
|
|
97
|
+
}) => {
|
|
98
|
+
if (!data.query?.trim()) {
|
|
99
|
+
return { success: false, error: "query is required" };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const query = data.query.toLowerCase();
|
|
103
|
+
const minConfidence = data.minConfidence ?? 0.1;
|
|
104
|
+
const limit = data.limit ?? 10;
|
|
105
|
+
|
|
106
|
+
let lessons = await kv.list<Lesson>(KV.lessons);
|
|
107
|
+
|
|
108
|
+
lessons = lessons.filter(
|
|
109
|
+
(l) => !l.deleted && l.confidence >= minConfidence,
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
if (data.project) {
|
|
113
|
+
lessons = lessons.filter((l) => l.project === data.project);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const scored = lessons
|
|
117
|
+
.map((l) => {
|
|
118
|
+
const text = `${l.content} ${l.context} ${l.tags.join(" ")}`.toLowerCase();
|
|
119
|
+
const terms = query.split(/\s+/).filter((t) => t.length > 1);
|
|
120
|
+
const matchCount = terms.filter((t) => text.includes(t)).length;
|
|
121
|
+
if (matchCount === 0) return null;
|
|
122
|
+
|
|
123
|
+
const relevance = matchCount / terms.length;
|
|
124
|
+
const daysSinceReinforced = l.lastReinforcedAt
|
|
125
|
+
? (Date.now() - new Date(l.lastReinforcedAt).getTime()) /
|
|
126
|
+
(1000 * 60 * 60 * 24)
|
|
127
|
+
: (Date.now() - new Date(l.createdAt).getTime()) /
|
|
128
|
+
(1000 * 60 * 60 * 24);
|
|
129
|
+
const recencyBoost = 1 / (1 + daysSinceReinforced * 0.01);
|
|
130
|
+
const score = l.confidence * relevance * recencyBoost;
|
|
131
|
+
|
|
132
|
+
return { lesson: l, score };
|
|
133
|
+
})
|
|
134
|
+
.filter(Boolean) as Array<{ lesson: Lesson; score: number }>;
|
|
135
|
+
|
|
136
|
+
scored.sort((a, b) => b.score - a.score);
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
await recordAudit(kv, "lesson_recall", "mem::lesson-recall", [], {
|
|
140
|
+
query: data.query,
|
|
141
|
+
resultCount: scored.length,
|
|
142
|
+
});
|
|
143
|
+
} catch {}
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
success: true,
|
|
147
|
+
lessons: scored.slice(0, limit).map((s) => ({
|
|
148
|
+
...s.lesson,
|
|
149
|
+
score: Math.round(s.score * 1000) / 1000,
|
|
150
|
+
})),
|
|
151
|
+
};
|
|
152
|
+
},
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
sdk.registerFunction(
|
|
156
|
+
{ id: "mem::lesson-list" },
|
|
157
|
+
async (data: {
|
|
158
|
+
project?: string;
|
|
159
|
+
source?: string;
|
|
160
|
+
minConfidence?: number;
|
|
161
|
+
limit?: number;
|
|
162
|
+
}) => {
|
|
163
|
+
const limit = data.limit ?? 50;
|
|
164
|
+
const minConfidence = data.minConfidence ?? 0;
|
|
165
|
+
let lessons = await kv.list<Lesson>(KV.lessons);
|
|
166
|
+
|
|
167
|
+
lessons = lessons.filter(
|
|
168
|
+
(l) => !l.deleted && l.confidence >= minConfidence,
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
if (data.project) {
|
|
172
|
+
lessons = lessons.filter((l) => l.project === data.project);
|
|
173
|
+
}
|
|
174
|
+
if (data.source) {
|
|
175
|
+
lessons = lessons.filter((l) => l.source === data.source);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
lessons.sort((a, b) => b.confidence - a.confidence);
|
|
179
|
+
|
|
180
|
+
return { success: true, lessons: lessons.slice(0, limit) };
|
|
181
|
+
},
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
sdk.registerFunction(
|
|
185
|
+
{ id: "mem::lesson-strengthen" },
|
|
186
|
+
async (data: { lessonId: string }) => {
|
|
187
|
+
if (!data.lessonId) {
|
|
188
|
+
return { success: false, error: "lessonId is required" };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const lesson = await kv.get<Lesson>(KV.lessons, data.lessonId);
|
|
192
|
+
if (!lesson || lesson.deleted) {
|
|
193
|
+
return { success: false, error: "lesson not found" };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
reinforceLesson(lesson);
|
|
197
|
+
|
|
198
|
+
await kv.set(KV.lessons, lesson.id, lesson);
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
await recordAudit(kv, "lesson_strengthen", "mem::lesson-strengthen", [
|
|
202
|
+
lesson.id,
|
|
203
|
+
]);
|
|
204
|
+
} catch {}
|
|
205
|
+
|
|
206
|
+
return { success: true, lesson };
|
|
207
|
+
},
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
sdk.registerFunction(
|
|
211
|
+
{ id: "mem::lesson-decay-sweep" },
|
|
212
|
+
async () => {
|
|
213
|
+
const lessons = await kv.list<Lesson>(KV.lessons);
|
|
214
|
+
let decayed = 0;
|
|
215
|
+
let softDeleted = 0;
|
|
216
|
+
const now = Date.now();
|
|
217
|
+
const timestamp = new Date().toISOString();
|
|
218
|
+
const dirty: Lesson[] = [];
|
|
219
|
+
|
|
220
|
+
for (const lesson of lessons) {
|
|
221
|
+
if (lesson.deleted) continue;
|
|
222
|
+
|
|
223
|
+
const baseline = lesson.lastDecayedAt || lesson.lastReinforcedAt || lesson.createdAt;
|
|
224
|
+
const weeksSinceBaseline =
|
|
225
|
+
(now - new Date(baseline).getTime()) / (1000 * 60 * 60 * 24 * 7);
|
|
226
|
+
|
|
227
|
+
if (weeksSinceBaseline < 1) continue;
|
|
228
|
+
|
|
229
|
+
const decay = lesson.decayRate * weeksSinceBaseline;
|
|
230
|
+
const newConfidence = Math.max(0.05, lesson.confidence - decay);
|
|
231
|
+
|
|
232
|
+
if (newConfidence !== lesson.confidence) {
|
|
233
|
+
lesson.confidence = Math.round(newConfidence * 1000) / 1000;
|
|
234
|
+
lesson.lastDecayedAt = timestamp;
|
|
235
|
+
lesson.updatedAt = timestamp;
|
|
236
|
+
|
|
237
|
+
if (lesson.confidence <= 0.1 && lesson.reinforcements === 0) {
|
|
238
|
+
lesson.deleted = true;
|
|
239
|
+
softDeleted++;
|
|
240
|
+
} else {
|
|
241
|
+
decayed++;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
dirty.push(lesson);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
await Promise.all(dirty.map((l) => kv.set(KV.lessons, l.id, l)));
|
|
249
|
+
|
|
250
|
+
return { success: true, decayed, softDeleted, total: lessons.length };
|
|
251
|
+
},
|
|
252
|
+
);
|
|
253
|
+
}
|
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
import type { ISdk } from "iii-sdk";
|
|
2
|
+
import type { StateKV } from "../state/kv.js";
|
|
3
|
+
import { KV, generateId } from "../state/schema.js";
|
|
4
|
+
import { withKeyedLock } from "../state/keyed-mutex.js";
|
|
5
|
+
import type {
|
|
6
|
+
MeshPeer,
|
|
7
|
+
Memory,
|
|
8
|
+
Action,
|
|
9
|
+
SemanticMemory,
|
|
10
|
+
ProceduralMemory,
|
|
11
|
+
MemoryRelation,
|
|
12
|
+
GraphNode,
|
|
13
|
+
GraphEdge,
|
|
14
|
+
} from "../types.js";
|
|
15
|
+
import { lookup } from "node:dns/promises";
|
|
16
|
+
import { isIP } from "node:net";
|
|
17
|
+
|
|
18
|
+
function isPrivateIP(ip: string): boolean {
|
|
19
|
+
if (ip === "127.0.0.1" || ip === "::1" || ip === "0.0.0.0") return true;
|
|
20
|
+
if (ip.startsWith("10.") || ip.startsWith("192.168.")) return true;
|
|
21
|
+
if (/^172\.(1[6-9]|2\d|3[01])\./.test(ip)) return true;
|
|
22
|
+
if (ip === "169.254.169.254") return true;
|
|
23
|
+
if (ip.startsWith("fe80:") || ip.startsWith("fc00:") || ip.startsWith("fd")) return true;
|
|
24
|
+
if (ip.startsWith("::ffff:")) {
|
|
25
|
+
const v4 = ip.slice(7);
|
|
26
|
+
return isPrivateIP(v4);
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function isAllowedUrl(urlStr: string): Promise<boolean> {
|
|
32
|
+
try {
|
|
33
|
+
const parsed = new URL(urlStr);
|
|
34
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
|
|
35
|
+
if (parsed.username || parsed.password) return false;
|
|
36
|
+
const host = parsed.hostname.toLowerCase();
|
|
37
|
+
|
|
38
|
+
if (host === "localhost") return false;
|
|
39
|
+
if (isIP(host) && isPrivateIP(host)) return false;
|
|
40
|
+
|
|
41
|
+
if (!isIP(host)) {
|
|
42
|
+
try {
|
|
43
|
+
const resolved = await lookup(host, { all: true });
|
|
44
|
+
if (resolved.some((r) => isPrivateIP(r.address))) return false;
|
|
45
|
+
} catch {
|
|
46
|
+
// DNS resolution failed — allow the URL (the actual fetch will fail if unreachable)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return true;
|
|
51
|
+
} catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const DEFAULT_SHARED_SCOPES = [
|
|
57
|
+
"memories",
|
|
58
|
+
"actions",
|
|
59
|
+
"semantic",
|
|
60
|
+
"procedural",
|
|
61
|
+
"relations",
|
|
62
|
+
"graph:nodes",
|
|
63
|
+
"graph:edges",
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
interface MeshSyncPayload {
|
|
67
|
+
memories?: Memory[];
|
|
68
|
+
actions?: Action[];
|
|
69
|
+
semantic?: SemanticMemory[];
|
|
70
|
+
procedural?: ProceduralMemory[];
|
|
71
|
+
relations?: MemoryRelation[];
|
|
72
|
+
graphNodes?: GraphNode[];
|
|
73
|
+
graphEdges?: GraphEdge[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function lwwMergeList<T extends { id: string }>(
|
|
77
|
+
kv: StateKV,
|
|
78
|
+
scope: string,
|
|
79
|
+
items: T[] | undefined,
|
|
80
|
+
lockPrefix: string,
|
|
81
|
+
tsField: "updatedAt" | "createdAt",
|
|
82
|
+
): Promise<number> {
|
|
83
|
+
if (!items || !Array.isArray(items)) return 0;
|
|
84
|
+
let count = 0;
|
|
85
|
+
for (const item of items) {
|
|
86
|
+
if (!item.id || typeof item.id !== "string") continue;
|
|
87
|
+
const ts = (item as Record<string, unknown>)[tsField];
|
|
88
|
+
if (typeof ts !== "string" || Number.isNaN(new Date(ts).getTime())) continue;
|
|
89
|
+
const wrote = await withKeyedLock(`${lockPrefix}:${item.id}`, async () => {
|
|
90
|
+
const existing = await kv.get<T>(scope, item.id);
|
|
91
|
+
if (!existing) {
|
|
92
|
+
await kv.set(scope, item.id, item);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
const existingTs = (existing as Record<string, unknown>)[tsField] as string;
|
|
96
|
+
if (new Date(ts) > new Date(existingTs)) {
|
|
97
|
+
await kv.set(scope, item.id, item);
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
});
|
|
102
|
+
if (wrote) count++;
|
|
103
|
+
}
|
|
104
|
+
return count;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function graphNodeTs(node: GraphNode): string {
|
|
108
|
+
return node.updatedAt || node.createdAt;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function lwwMergeGraphNodes(
|
|
112
|
+
kv: StateKV,
|
|
113
|
+
items: GraphNode[] | undefined,
|
|
114
|
+
): Promise<number> {
|
|
115
|
+
if (!items || !Array.isArray(items)) return 0;
|
|
116
|
+
let count = 0;
|
|
117
|
+
for (const item of items) {
|
|
118
|
+
if (!item.id || typeof item.id !== "string") continue;
|
|
119
|
+
const ts = graphNodeTs(item);
|
|
120
|
+
if (!ts || Number.isNaN(new Date(ts).getTime())) continue;
|
|
121
|
+
const wrote = await withKeyedLock(`mem:gnode:${item.id}`, async () => {
|
|
122
|
+
const existing = await kv.get<GraphNode>(KV.graphNodes, item.id);
|
|
123
|
+
if (!existing) {
|
|
124
|
+
await kv.set(KV.graphNodes, item.id, item);
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
if (new Date(ts) > new Date(graphNodeTs(existing))) {
|
|
128
|
+
await kv.set(KV.graphNodes, item.id, item);
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
return false;
|
|
132
|
+
});
|
|
133
|
+
if (wrote) count++;
|
|
134
|
+
}
|
|
135
|
+
return count;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function registerMeshFunction(sdk: ISdk, kv: StateKV): void {
|
|
139
|
+
sdk.registerFunction(
|
|
140
|
+
{ id: "mem::mesh-register" },
|
|
141
|
+
async (data: {
|
|
142
|
+
url: string;
|
|
143
|
+
name: string;
|
|
144
|
+
sharedScopes?: string[];
|
|
145
|
+
syncFilter?: { project?: string };
|
|
146
|
+
}) => {
|
|
147
|
+
if (!data.url || !data.name) {
|
|
148
|
+
return { success: false, error: "url and name are required" };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (!(await isAllowedUrl(data.url))) {
|
|
152
|
+
return { success: false, error: "URL blocked: private/local address not allowed" };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const existing = await kv.list<MeshPeer>(KV.mesh);
|
|
156
|
+
const duplicate = existing.find((p) => p.url === data.url);
|
|
157
|
+
if (duplicate) {
|
|
158
|
+
return { success: false, error: "peer already registered", peerId: duplicate.id };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const peer: MeshPeer = {
|
|
162
|
+
id: generateId("peer"),
|
|
163
|
+
url: data.url,
|
|
164
|
+
name: data.name,
|
|
165
|
+
status: "disconnected",
|
|
166
|
+
sharedScopes: data.sharedScopes || DEFAULT_SHARED_SCOPES,
|
|
167
|
+
syncFilter: data.syncFilter,
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
await kv.set(KV.mesh, peer.id, peer);
|
|
171
|
+
return { success: true, peer };
|
|
172
|
+
},
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
sdk.registerFunction(
|
|
176
|
+
{ id: "mem::mesh-list" },
|
|
177
|
+
async () => {
|
|
178
|
+
const peers = await kv.list<MeshPeer>(KV.mesh);
|
|
179
|
+
return { success: true, peers };
|
|
180
|
+
},
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
sdk.registerFunction(
|
|
184
|
+
{ id: "mem::mesh-sync" },
|
|
185
|
+
async (data: { peerId?: string; scopes?: string[]; direction?: "push" | "pull" | "both" }) => {
|
|
186
|
+
const direction = data.direction || "both";
|
|
187
|
+
let peers: MeshPeer[];
|
|
188
|
+
|
|
189
|
+
if (data.peerId) {
|
|
190
|
+
const peer = await kv.get<MeshPeer>(KV.mesh, data.peerId);
|
|
191
|
+
if (!peer) return { success: false, error: "peer not found" };
|
|
192
|
+
peers = [peer];
|
|
193
|
+
} else {
|
|
194
|
+
peers = await kv.list<MeshPeer>(KV.mesh);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const results: Array<{
|
|
198
|
+
peerId: string;
|
|
199
|
+
peerName: string;
|
|
200
|
+
pushed: number;
|
|
201
|
+
pulled: number;
|
|
202
|
+
errors: string[];
|
|
203
|
+
}> = [];
|
|
204
|
+
|
|
205
|
+
for (const peer of peers) {
|
|
206
|
+
const result = {
|
|
207
|
+
peerId: peer.id,
|
|
208
|
+
peerName: peer.name,
|
|
209
|
+
pushed: 0,
|
|
210
|
+
pulled: 0,
|
|
211
|
+
errors: [] as string[],
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
peer.status = "syncing";
|
|
215
|
+
await kv.set(KV.mesh, peer.id, peer);
|
|
216
|
+
|
|
217
|
+
const scopes = data.scopes || peer.sharedScopes;
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
if (!(await isAllowedUrl(peer.url))) {
|
|
221
|
+
result.errors.push("peer URL blocked: private/local address not allowed");
|
|
222
|
+
peer.status = "error";
|
|
223
|
+
await kv.set(KV.mesh, peer.id, peer);
|
|
224
|
+
results.push(result);
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (direction === "push" || direction === "both") {
|
|
229
|
+
const pushData = await collectSyncData(kv, scopes, peer.lastSyncAt, peer.syncFilter);
|
|
230
|
+
try {
|
|
231
|
+
const response = await fetch(`${peer.url}/agentmemory/mesh/receive`, {
|
|
232
|
+
method: "POST",
|
|
233
|
+
headers: { "Content-Type": "application/json" },
|
|
234
|
+
body: JSON.stringify(pushData),
|
|
235
|
+
signal: AbortSignal.timeout(30000),
|
|
236
|
+
redirect: "error",
|
|
237
|
+
});
|
|
238
|
+
if (response.ok) {
|
|
239
|
+
const body = (await response.json()) as { accepted: number };
|
|
240
|
+
result.pushed = body.accepted || 0;
|
|
241
|
+
} else {
|
|
242
|
+
result.errors.push(`push failed: HTTP ${response.status}`);
|
|
243
|
+
}
|
|
244
|
+
} catch (err) {
|
|
245
|
+
result.errors.push(`push failed: ${String(err)}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (direction === "pull" || direction === "both") {
|
|
250
|
+
try {
|
|
251
|
+
const response = await fetch(
|
|
252
|
+
`${peer.url}/agentmemory/mesh/export?since=${peer.lastSyncAt || ""}`,
|
|
253
|
+
{ signal: AbortSignal.timeout(30000), redirect: "error" },
|
|
254
|
+
);
|
|
255
|
+
if (response.ok) {
|
|
256
|
+
const pullData = (await response.json()) as {
|
|
257
|
+
memories?: Memory[];
|
|
258
|
+
actions?: Action[];
|
|
259
|
+
};
|
|
260
|
+
result.pulled = await applySyncData(kv, pullData, scopes);
|
|
261
|
+
} else {
|
|
262
|
+
result.errors.push(`pull failed: HTTP ${response.status}`);
|
|
263
|
+
}
|
|
264
|
+
} catch (err) {
|
|
265
|
+
result.errors.push(`pull failed: ${String(err)}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
peer.status = result.errors.length > 0 ? "error" : "connected";
|
|
270
|
+
if (result.errors.length === 0) {
|
|
271
|
+
peer.lastSyncAt = new Date().toISOString();
|
|
272
|
+
}
|
|
273
|
+
} catch (err) {
|
|
274
|
+
peer.status = "disconnected";
|
|
275
|
+
result.errors.push(String(err));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
await kv.set(KV.mesh, peer.id, peer);
|
|
279
|
+
results.push(result);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return { success: true, results };
|
|
283
|
+
},
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
sdk.registerFunction(
|
|
287
|
+
{ id: "mem::mesh-receive" },
|
|
288
|
+
async (data: MeshSyncPayload) => {
|
|
289
|
+
let accepted = 0;
|
|
290
|
+
|
|
291
|
+
accepted += await lwwMergeList(kv, KV.memories, data.memories, "mem:memory", "updatedAt");
|
|
292
|
+
accepted += await lwwMergeList(kv, KV.actions, data.actions, "mem:action", "updatedAt");
|
|
293
|
+
accepted += await lwwMergeList(kv, KV.semantic, data.semantic, "mem:semantic", "updatedAt");
|
|
294
|
+
accepted += await lwwMergeList(kv, KV.procedural, data.procedural, "mem:procedural", "updatedAt");
|
|
295
|
+
if (data.relations && Array.isArray(data.relations)) {
|
|
296
|
+
for (const rel of data.relations) {
|
|
297
|
+
if (!rel.sourceId || !rel.targetId || !rel.type) continue;
|
|
298
|
+
const relKey = `${rel.sourceId}:${rel.targetId}:${rel.type}`;
|
|
299
|
+
await withKeyedLock(`mem:relation:${relKey}`, async () => {
|
|
300
|
+
const existing = await kv.get<MemoryRelation>(KV.relations, relKey);
|
|
301
|
+
if (!existing) {
|
|
302
|
+
await kv.set(KV.relations, relKey, rel);
|
|
303
|
+
accepted++;
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
accepted += await lwwMergeGraphNodes(kv, data.graphNodes);
|
|
309
|
+
accepted += await lwwMergeList(kv, KV.graphEdges, data.graphEdges, "mem:gedge", "createdAt");
|
|
310
|
+
|
|
311
|
+
return { success: true, accepted };
|
|
312
|
+
},
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
sdk.registerFunction(
|
|
316
|
+
{ id: "mem::mesh-remove" },
|
|
317
|
+
async (data: { peerId: string }) => {
|
|
318
|
+
if (!data.peerId) {
|
|
319
|
+
return { success: false, error: "peerId is required" };
|
|
320
|
+
}
|
|
321
|
+
await kv.delete(KV.mesh, data.peerId);
|
|
322
|
+
return { success: true };
|
|
323
|
+
},
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function deltaFilter<T>(
|
|
328
|
+
items: T[],
|
|
329
|
+
sinceTime: number,
|
|
330
|
+
tsField: "updatedAt" | "createdAt",
|
|
331
|
+
): T[] {
|
|
332
|
+
return items.filter(
|
|
333
|
+
(item) => new Date((item as Record<string, unknown>)[tsField] as string).getTime() > sinceTime,
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function collectSyncData(
|
|
338
|
+
kv: StateKV,
|
|
339
|
+
scopes: string[],
|
|
340
|
+
since?: string,
|
|
341
|
+
syncFilter?: { project?: string },
|
|
342
|
+
): Promise<MeshSyncPayload> {
|
|
343
|
+
const result: MeshSyncPayload = {};
|
|
344
|
+
const parsed = since ? new Date(since).getTime() : 0;
|
|
345
|
+
const sinceTime = Number.isNaN(parsed) ? 0 : parsed;
|
|
346
|
+
|
|
347
|
+
if (scopes.includes("memories")) {
|
|
348
|
+
const all = await kv.list<Memory>(KV.memories);
|
|
349
|
+
result.memories = deltaFilter(all, sinceTime, "updatedAt");
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (scopes.includes("actions")) {
|
|
353
|
+
let all = await kv.list<Action>(KV.actions);
|
|
354
|
+
if (syncFilter?.project) {
|
|
355
|
+
all = all.filter((a) => a.project === syncFilter.project);
|
|
356
|
+
}
|
|
357
|
+
result.actions = deltaFilter(all, sinceTime, "updatedAt");
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const projectScoped = !!syncFilter?.project;
|
|
361
|
+
|
|
362
|
+
if (scopes.includes("semantic") && !projectScoped) {
|
|
363
|
+
const all = await kv.list<SemanticMemory>(KV.semantic);
|
|
364
|
+
result.semantic = deltaFilter(all, sinceTime, "updatedAt");
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (scopes.includes("procedural") && !projectScoped) {
|
|
368
|
+
const all = await kv.list<ProceduralMemory>(KV.procedural);
|
|
369
|
+
result.procedural = deltaFilter(all, sinceTime, "updatedAt");
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (scopes.includes("relations") && !projectScoped) {
|
|
373
|
+
const all = await kv.list<MemoryRelation>(KV.relations);
|
|
374
|
+
result.relations = deltaFilter(all, sinceTime, "createdAt");
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (scopes.includes("graph:nodes") && !projectScoped) {
|
|
378
|
+
const all = await kv.list<GraphNode>(KV.graphNodes);
|
|
379
|
+
result.graphNodes = all.filter(
|
|
380
|
+
(n) => new Date(graphNodeTs(n)).getTime() > sinceTime,
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (scopes.includes("graph:edges") && !projectScoped) {
|
|
385
|
+
const all = await kv.list<GraphEdge>(KV.graphEdges);
|
|
386
|
+
result.graphEdges = deltaFilter(all, sinceTime, "createdAt");
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return result;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async function applySyncData(
|
|
393
|
+
kv: StateKV,
|
|
394
|
+
data: MeshSyncPayload,
|
|
395
|
+
scopes: string[],
|
|
396
|
+
): Promise<number> {
|
|
397
|
+
let applied = 0;
|
|
398
|
+
|
|
399
|
+
if (scopes.includes("memories")) {
|
|
400
|
+
applied += await lwwMergeList(kv, KV.memories, data.memories, "mem:memory", "updatedAt");
|
|
401
|
+
}
|
|
402
|
+
if (scopes.includes("actions")) {
|
|
403
|
+
applied += await lwwMergeList(kv, KV.actions, data.actions, "mem:action", "updatedAt");
|
|
404
|
+
}
|
|
405
|
+
if (scopes.includes("semantic")) {
|
|
406
|
+
applied += await lwwMergeList(kv, KV.semantic, data.semantic, "mem:semantic", "updatedAt");
|
|
407
|
+
}
|
|
408
|
+
if (scopes.includes("procedural")) {
|
|
409
|
+
applied += await lwwMergeList(kv, KV.procedural, data.procedural, "mem:procedural", "updatedAt");
|
|
410
|
+
}
|
|
411
|
+
if (scopes.includes("relations") && data.relations) {
|
|
412
|
+
for (const rel of data.relations) {
|
|
413
|
+
if (!rel.sourceId || !rel.targetId || !rel.type) continue;
|
|
414
|
+
const relKey = `${rel.sourceId}:${rel.targetId}:${rel.type}`;
|
|
415
|
+
const wrote = await withKeyedLock(`mem:relation:${relKey}`, async () => {
|
|
416
|
+
const existing = await kv.get<MemoryRelation>(KV.relations, relKey);
|
|
417
|
+
if (!existing) {
|
|
418
|
+
await kv.set(KV.relations, relKey, rel);
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
421
|
+
return false;
|
|
422
|
+
});
|
|
423
|
+
if (wrote) applied++;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
if (scopes.includes("graph:nodes")) {
|
|
427
|
+
applied += await lwwMergeGraphNodes(kv, data.graphNodes);
|
|
428
|
+
}
|
|
429
|
+
if (scopes.includes("graph:edges")) {
|
|
430
|
+
applied += await lwwMergeList(kv, KV.graphEdges, data.graphEdges, "mem:gedge", "createdAt");
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return applied;
|
|
434
|
+
}
|