@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,108 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
vi.mock("iii-sdk", () => ({
|
|
4
|
+
getContext: () => ({
|
|
5
|
+
logger: { info: vi.fn(), error: vi.fn(), warn: vi.fn() },
|
|
6
|
+
}),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
import { recordAudit, queryAudit } from "../src/functions/audit.js";
|
|
10
|
+
|
|
11
|
+
function mockKV() {
|
|
12
|
+
const store = new Map<string, Map<string, unknown>>();
|
|
13
|
+
return {
|
|
14
|
+
get: async <T>(scope: string, key: string): Promise<T | null> => {
|
|
15
|
+
return (store.get(scope)?.get(key) as T) ?? null;
|
|
16
|
+
},
|
|
17
|
+
set: async <T>(scope: string, key: string, data: T): Promise<T> => {
|
|
18
|
+
if (!store.has(scope)) store.set(scope, new Map());
|
|
19
|
+
store.get(scope)!.set(key, data);
|
|
20
|
+
return data;
|
|
21
|
+
},
|
|
22
|
+
delete: async (scope: string, key: string): Promise<void> => {
|
|
23
|
+
store.get(scope)?.delete(key);
|
|
24
|
+
},
|
|
25
|
+
list: async <T>(scope: string): Promise<T[]> => {
|
|
26
|
+
const entries = store.get(scope);
|
|
27
|
+
return entries ? (Array.from(entries.values()) as T[]) : [];
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("Audit Functions", () => {
|
|
33
|
+
let kv: ReturnType<typeof mockKV>;
|
|
34
|
+
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
kv = mockKV();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("recordAudit creates an entry with proper fields", async () => {
|
|
40
|
+
const entry = await recordAudit(
|
|
41
|
+
kv as never,
|
|
42
|
+
"observe",
|
|
43
|
+
"mem::compress",
|
|
44
|
+
["obs_1", "obs_2"],
|
|
45
|
+
{ count: 2 },
|
|
46
|
+
0.85,
|
|
47
|
+
"user-1",
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
expect(entry.id).toMatch(/^aud_/);
|
|
51
|
+
expect(entry.timestamp).toBeDefined();
|
|
52
|
+
expect(entry.operation).toBe("observe");
|
|
53
|
+
expect(entry.functionId).toBe("mem::compress");
|
|
54
|
+
expect(entry.targetIds).toEqual(["obs_1", "obs_2"]);
|
|
55
|
+
expect(entry.details).toEqual({ count: 2 });
|
|
56
|
+
expect(entry.qualityScore).toBe(0.85);
|
|
57
|
+
expect(entry.userId).toBe("user-1");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("queryAudit returns entries sorted by timestamp desc", async () => {
|
|
61
|
+
await recordAudit(kv as never, "observe", "fn1", ["a"], {});
|
|
62
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
63
|
+
await recordAudit(kv as never, "delete", "fn2", ["b"], {});
|
|
64
|
+
|
|
65
|
+
const entries = await queryAudit(kv as never);
|
|
66
|
+
expect(entries.length).toBe(2);
|
|
67
|
+
expect(
|
|
68
|
+
new Date(entries[0].timestamp).getTime(),
|
|
69
|
+
).toBeGreaterThanOrEqual(new Date(entries[1].timestamp).getTime());
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("queryAudit filters by operation", async () => {
|
|
73
|
+
await recordAudit(kv as never, "observe", "fn1", [], {});
|
|
74
|
+
await recordAudit(kv as never, "delete", "fn2", [], {});
|
|
75
|
+
await recordAudit(kv as never, "observe", "fn3", [], {});
|
|
76
|
+
|
|
77
|
+
const entries = await queryAudit(kv as never, { operation: "observe" });
|
|
78
|
+
expect(entries.length).toBe(2);
|
|
79
|
+
expect(entries.every((e) => e.operation === "observe")).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("queryAudit filters by dateFrom/dateTo", async () => {
|
|
83
|
+
const early = await recordAudit(kv as never, "observe", "fn1", [], {});
|
|
84
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
85
|
+
const late = await recordAudit(kv as never, "delete", "fn2", [], {});
|
|
86
|
+
|
|
87
|
+
const entries = await queryAudit(kv as never, {
|
|
88
|
+
dateFrom: late.timestamp,
|
|
89
|
+
});
|
|
90
|
+
expect(entries.length).toBe(1);
|
|
91
|
+
expect(entries[0].operation).toBe("delete");
|
|
92
|
+
|
|
93
|
+
const entriesBefore = await queryAudit(kv as never, {
|
|
94
|
+
dateTo: early.timestamp,
|
|
95
|
+
});
|
|
96
|
+
expect(entriesBefore.length).toBe(1);
|
|
97
|
+
expect(entriesBefore[0].operation).toBe("observe");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("queryAudit respects limit", async () => {
|
|
101
|
+
for (let i = 0; i < 10; i++) {
|
|
102
|
+
await recordAudit(kv as never, "observe", `fn${i}`, [], {});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const entries = await queryAudit(kv as never, { limit: 3 });
|
|
106
|
+
expect(entries.length).toBe(3);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
vi.mock("iii-sdk", () => ({
|
|
4
|
+
getContext: () => ({
|
|
5
|
+
logger: { info: vi.fn(), error: vi.fn(), warn: vi.fn() },
|
|
6
|
+
}),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
import { registerAutoForgetFunction } from "../src/functions/auto-forget.js";
|
|
10
|
+
import type { Memory, CompressedObservation, Session } from "../src/types.js";
|
|
11
|
+
|
|
12
|
+
function mockKV() {
|
|
13
|
+
const store = new Map<string, Map<string, unknown>>();
|
|
14
|
+
return {
|
|
15
|
+
get: async <T>(scope: string, key: string): Promise<T | null> => {
|
|
16
|
+
return (store.get(scope)?.get(key) as T) ?? null;
|
|
17
|
+
},
|
|
18
|
+
set: async <T>(scope: string, key: string, data: T): Promise<T> => {
|
|
19
|
+
if (!store.has(scope)) store.set(scope, new Map());
|
|
20
|
+
store.get(scope)!.set(key, data);
|
|
21
|
+
return data;
|
|
22
|
+
},
|
|
23
|
+
delete: async (scope: string, key: string): Promise<void> => {
|
|
24
|
+
store.get(scope)?.delete(key);
|
|
25
|
+
},
|
|
26
|
+
list: async <T>(scope: string): Promise<T[]> => {
|
|
27
|
+
const entries = store.get(scope);
|
|
28
|
+
return entries ? (Array.from(entries.values()) as T[]) : [];
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function mockSdk() {
|
|
34
|
+
const functions = new Map<string, Function>();
|
|
35
|
+
return {
|
|
36
|
+
registerFunction: (opts: { id: string }, handler: Function) => {
|
|
37
|
+
functions.set(opts.id, handler);
|
|
38
|
+
},
|
|
39
|
+
registerTrigger: () => {},
|
|
40
|
+
trigger: async (id: string, data: unknown) => {
|
|
41
|
+
const fn = functions.get(id);
|
|
42
|
+
if (!fn) throw new Error(`No function: ${id}`);
|
|
43
|
+
return fn(data);
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function makeMemory(overrides: Partial<Memory> = {}): Memory {
|
|
49
|
+
return {
|
|
50
|
+
id: "mem_1",
|
|
51
|
+
createdAt: new Date().toISOString(),
|
|
52
|
+
updatedAt: new Date().toISOString(),
|
|
53
|
+
type: "pattern",
|
|
54
|
+
title: "Test memory",
|
|
55
|
+
content: "This is a test memory with enough words for comparison",
|
|
56
|
+
concepts: ["test"],
|
|
57
|
+
files: [],
|
|
58
|
+
sessionIds: ["ses_1"],
|
|
59
|
+
strength: 5,
|
|
60
|
+
version: 1,
|
|
61
|
+
isLatest: true,
|
|
62
|
+
...overrides,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
describe("Auto-Forget Function", () => {
|
|
67
|
+
let sdk: ReturnType<typeof mockSdk>;
|
|
68
|
+
let kv: ReturnType<typeof mockKV>;
|
|
69
|
+
|
|
70
|
+
beforeEach(() => {
|
|
71
|
+
sdk = mockSdk();
|
|
72
|
+
kv = mockKV();
|
|
73
|
+
registerAutoForgetFunction(sdk as never, kv as never);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("detects and deletes TTL-expired memories", async () => {
|
|
77
|
+
const expired = makeMemory({
|
|
78
|
+
id: "mem_expired",
|
|
79
|
+
forgetAfter: "2020-01-01T00:00:00Z",
|
|
80
|
+
});
|
|
81
|
+
await kv.set("mem:memories", "mem_expired", expired);
|
|
82
|
+
|
|
83
|
+
const result = (await sdk.trigger("mem::auto-forget", {})) as {
|
|
84
|
+
ttlExpired: string[];
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
expect(result.ttlExpired).toContain("mem_expired");
|
|
88
|
+
const deleted = await kv.get("mem:memories", "mem_expired");
|
|
89
|
+
expect(deleted).toBeNull();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("detects contradiction between very similar memories", async () => {
|
|
93
|
+
const mem1 = makeMemory({
|
|
94
|
+
id: "mem_1",
|
|
95
|
+
content: "Use React hooks for state management in all components",
|
|
96
|
+
createdAt: "2026-01-01T00:00:00Z",
|
|
97
|
+
});
|
|
98
|
+
const mem2 = makeMemory({
|
|
99
|
+
id: "mem_2",
|
|
100
|
+
content: "Use React hooks for state management in all components",
|
|
101
|
+
createdAt: "2026-02-01T00:00:00Z",
|
|
102
|
+
});
|
|
103
|
+
await kv.set("mem:memories", "mem_1", mem1);
|
|
104
|
+
await kv.set("mem:memories", "mem_2", mem2);
|
|
105
|
+
|
|
106
|
+
const result = (await sdk.trigger("mem::auto-forget", {})) as {
|
|
107
|
+
contradictions: Array<{
|
|
108
|
+
memoryA: string;
|
|
109
|
+
memoryB: string;
|
|
110
|
+
similarity: number;
|
|
111
|
+
}>;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
expect(result.contradictions.length).toBe(1);
|
|
115
|
+
const older = await kv.get<Memory>("mem:memories", "mem_1");
|
|
116
|
+
expect(older!.isLatest).toBe(false);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("evicts low-value old observations", async () => {
|
|
120
|
+
const session: Session = {
|
|
121
|
+
id: "ses_1",
|
|
122
|
+
project: "my-project",
|
|
123
|
+
cwd: "/tmp",
|
|
124
|
+
startedAt: "2025-01-01T00:00:00Z",
|
|
125
|
+
status: "completed",
|
|
126
|
+
observationCount: 1,
|
|
127
|
+
};
|
|
128
|
+
await kv.set("mem:sessions", "ses_1", session);
|
|
129
|
+
|
|
130
|
+
const oldLowObs: CompressedObservation = {
|
|
131
|
+
id: "obs_old",
|
|
132
|
+
sessionId: "ses_1",
|
|
133
|
+
timestamp: "2025-01-01T00:00:00Z",
|
|
134
|
+
type: "other",
|
|
135
|
+
title: "trivial event",
|
|
136
|
+
facts: [],
|
|
137
|
+
narrative: "nothing important",
|
|
138
|
+
concepts: [],
|
|
139
|
+
files: [],
|
|
140
|
+
importance: 1,
|
|
141
|
+
};
|
|
142
|
+
await kv.set("mem:obs:ses_1", "obs_old", oldLowObs);
|
|
143
|
+
|
|
144
|
+
const result = (await sdk.trigger("mem::auto-forget", {})) as {
|
|
145
|
+
lowValueObs: string[];
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
expect(result.lowValueObs).toContain("obs_old");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("dryRun mode identifies but does not delete anything", async () => {
|
|
152
|
+
const expired = makeMemory({
|
|
153
|
+
id: "mem_expired",
|
|
154
|
+
forgetAfter: "2020-01-01T00:00:00Z",
|
|
155
|
+
});
|
|
156
|
+
await kv.set("mem:memories", "mem_expired", expired);
|
|
157
|
+
|
|
158
|
+
const result = (await sdk.trigger("mem::auto-forget", { dryRun: true })) as {
|
|
159
|
+
ttlExpired: string[];
|
|
160
|
+
dryRun: boolean;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
expect(result.dryRun).toBe(true);
|
|
164
|
+
expect(result.ttlExpired).toContain("mem_expired");
|
|
165
|
+
|
|
166
|
+
const stillExists = await kv.get("mem:memories", "mem_expired");
|
|
167
|
+
expect(stillExists).not.toBeNull();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("does not flag non-similar memories as contradictions", async () => {
|
|
171
|
+
const mem1 = makeMemory({
|
|
172
|
+
id: "mem_1",
|
|
173
|
+
content: "We use TypeScript with strict mode enabled for all backend services",
|
|
174
|
+
});
|
|
175
|
+
const mem2 = makeMemory({
|
|
176
|
+
id: "mem_2",
|
|
177
|
+
content: "The deployment pipeline runs integration tests before merging to main",
|
|
178
|
+
});
|
|
179
|
+
await kv.set("mem:memories", "mem_1", mem1);
|
|
180
|
+
await kv.set("mem:memories", "mem_2", mem2);
|
|
181
|
+
|
|
182
|
+
const result = (await sdk.trigger("mem::auto-forget", {})) as {
|
|
183
|
+
contradictions: unknown[];
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
expect(result.contradictions.length).toBe(0);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
vi.mock("iii-sdk", () => ({
|
|
4
|
+
getContext: () => ({
|
|
5
|
+
logger: { info: vi.fn(), error: vi.fn(), warn: vi.fn() },
|
|
6
|
+
}),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
import { registerCascadeFunction } from "../src/functions/cascade.js";
|
|
10
|
+
import type { Memory, GraphNode, GraphEdge } from "../src/types.js";
|
|
11
|
+
import { mockKV, mockSdk } from "./helpers/mocks.js";
|
|
12
|
+
|
|
13
|
+
describe("Cascade Update Function", () => {
|
|
14
|
+
let sdk: ReturnType<typeof mockSdk>;
|
|
15
|
+
let kv: ReturnType<typeof mockKV>;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
sdk = mockSdk();
|
|
19
|
+
kv = mockKV();
|
|
20
|
+
vi.clearAllMocks();
|
|
21
|
+
registerCascadeFunction(sdk as never, kv as never);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("returns error when supersededMemoryId is missing", async () => {
|
|
25
|
+
const result = (await sdk.trigger("mem::cascade-update", {})) as {
|
|
26
|
+
success: boolean;
|
|
27
|
+
error: string;
|
|
28
|
+
};
|
|
29
|
+
expect(result.success).toBe(false);
|
|
30
|
+
expect(result.error).toBe("supersededMemoryId is required");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("returns error for non-existent memory", async () => {
|
|
34
|
+
const result = (await sdk.trigger("mem::cascade-update", {
|
|
35
|
+
supersededMemoryId: "mem_missing",
|
|
36
|
+
})) as { success: boolean; error: string };
|
|
37
|
+
expect(result.success).toBe(false);
|
|
38
|
+
expect(result.error).toBe("superseded memory not found");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("flags graph nodes referencing superseded observation IDs", async () => {
|
|
42
|
+
const memory: Memory = {
|
|
43
|
+
id: "mem_old",
|
|
44
|
+
createdAt: "2026-03-01T00:00:00Z",
|
|
45
|
+
updatedAt: "2026-03-01T00:00:00Z",
|
|
46
|
+
type: "fact",
|
|
47
|
+
title: "Old fact",
|
|
48
|
+
content: "Old content",
|
|
49
|
+
concepts: ["react"],
|
|
50
|
+
files: [],
|
|
51
|
+
sessionIds: [],
|
|
52
|
+
strength: 5,
|
|
53
|
+
version: 1,
|
|
54
|
+
isLatest: false,
|
|
55
|
+
sourceObservationIds: ["obs_a", "obs_b"],
|
|
56
|
+
};
|
|
57
|
+
await kv.set("mem:memories", "mem_old", memory);
|
|
58
|
+
|
|
59
|
+
const node: GraphNode = {
|
|
60
|
+
id: "node_1",
|
|
61
|
+
type: "concept",
|
|
62
|
+
name: "react",
|
|
63
|
+
properties: {},
|
|
64
|
+
sourceObservationIds: ["obs_a"],
|
|
65
|
+
createdAt: "2026-03-01T00:00:00Z",
|
|
66
|
+
};
|
|
67
|
+
await kv.set("mem:graph:nodes", "node_1", node);
|
|
68
|
+
|
|
69
|
+
const unrelatedNode: GraphNode = {
|
|
70
|
+
id: "node_2",
|
|
71
|
+
type: "file",
|
|
72
|
+
name: "index.ts",
|
|
73
|
+
properties: {},
|
|
74
|
+
sourceObservationIds: ["obs_c"],
|
|
75
|
+
createdAt: "2026-03-01T00:00:00Z",
|
|
76
|
+
};
|
|
77
|
+
await kv.set("mem:graph:nodes", "node_2", unrelatedNode);
|
|
78
|
+
|
|
79
|
+
const result = (await sdk.trigger("mem::cascade-update", {
|
|
80
|
+
supersededMemoryId: "mem_old",
|
|
81
|
+
})) as { success: boolean; flagged: { nodes: number; edges: number } };
|
|
82
|
+
|
|
83
|
+
expect(result.success).toBe(true);
|
|
84
|
+
expect(result.flagged.nodes).toBe(1);
|
|
85
|
+
|
|
86
|
+
const updated = await kv.get<GraphNode>("mem:graph:nodes", "node_1");
|
|
87
|
+
expect(updated!.stale).toBe(true);
|
|
88
|
+
|
|
89
|
+
const unchanged = await kv.get<GraphNode>("mem:graph:nodes", "node_2");
|
|
90
|
+
expect(unchanged!.stale).toBeUndefined();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("flags graph edges referencing superseded observation IDs", async () => {
|
|
94
|
+
const memory: Memory = {
|
|
95
|
+
id: "mem_old2",
|
|
96
|
+
createdAt: "2026-03-01T00:00:00Z",
|
|
97
|
+
updatedAt: "2026-03-01T00:00:00Z",
|
|
98
|
+
type: "pattern",
|
|
99
|
+
title: "Old pattern",
|
|
100
|
+
content: "Old pattern content",
|
|
101
|
+
concepts: ["testing"],
|
|
102
|
+
files: [],
|
|
103
|
+
sessionIds: [],
|
|
104
|
+
strength: 5,
|
|
105
|
+
version: 1,
|
|
106
|
+
isLatest: false,
|
|
107
|
+
sourceObservationIds: ["obs_x"],
|
|
108
|
+
};
|
|
109
|
+
await kv.set("mem:memories", "mem_old2", memory);
|
|
110
|
+
|
|
111
|
+
const edge: GraphEdge = {
|
|
112
|
+
id: "edge_1",
|
|
113
|
+
type: "uses",
|
|
114
|
+
sourceNodeId: "node_a",
|
|
115
|
+
targetNodeId: "node_b",
|
|
116
|
+
weight: 1,
|
|
117
|
+
sourceObservationIds: ["obs_x", "obs_y"],
|
|
118
|
+
createdAt: "2026-03-01T00:00:00Z",
|
|
119
|
+
};
|
|
120
|
+
await kv.set("mem:graph:edges", "edge_1", edge);
|
|
121
|
+
|
|
122
|
+
const result = (await sdk.trigger("mem::cascade-update", {
|
|
123
|
+
supersededMemoryId: "mem_old2",
|
|
124
|
+
})) as { success: boolean; flagged: { edges: number } };
|
|
125
|
+
|
|
126
|
+
expect(result.success).toBe(true);
|
|
127
|
+
expect(result.flagged.edges).toBe(1);
|
|
128
|
+
|
|
129
|
+
const updated = await kv.get<GraphEdge>("mem:graph:edges", "edge_1");
|
|
130
|
+
expect(updated!.stale).toBe(true);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("counts sibling memories sharing 2+ concepts", async () => {
|
|
134
|
+
const superseded: Memory = {
|
|
135
|
+
id: "mem_superseded",
|
|
136
|
+
createdAt: "2026-03-01T00:00:00Z",
|
|
137
|
+
updatedAt: "2026-03-01T00:00:00Z",
|
|
138
|
+
type: "architecture",
|
|
139
|
+
title: "React architecture",
|
|
140
|
+
content: "Old arch",
|
|
141
|
+
concepts: ["react", "frontend", "typescript"],
|
|
142
|
+
files: [],
|
|
143
|
+
sessionIds: [],
|
|
144
|
+
strength: 5,
|
|
145
|
+
version: 1,
|
|
146
|
+
isLatest: false,
|
|
147
|
+
};
|
|
148
|
+
await kv.set("mem:memories", "mem_superseded", superseded);
|
|
149
|
+
|
|
150
|
+
const sibling: Memory = {
|
|
151
|
+
id: "mem_sibling",
|
|
152
|
+
createdAt: "2026-03-01T00:00:00Z",
|
|
153
|
+
updatedAt: "2026-03-01T00:00:00Z",
|
|
154
|
+
type: "pattern",
|
|
155
|
+
title: "React patterns",
|
|
156
|
+
content: "Sibling memory sharing concepts",
|
|
157
|
+
concepts: ["react", "typescript"],
|
|
158
|
+
files: [],
|
|
159
|
+
sessionIds: [],
|
|
160
|
+
strength: 6,
|
|
161
|
+
version: 1,
|
|
162
|
+
isLatest: true,
|
|
163
|
+
};
|
|
164
|
+
await kv.set("mem:memories", "mem_sibling", sibling);
|
|
165
|
+
|
|
166
|
+
const unrelated: Memory = {
|
|
167
|
+
id: "mem_unrelated",
|
|
168
|
+
createdAt: "2026-03-01T00:00:00Z",
|
|
169
|
+
updatedAt: "2026-03-01T00:00:00Z",
|
|
170
|
+
type: "fact",
|
|
171
|
+
title: "Python setup",
|
|
172
|
+
content: "Unrelated memory",
|
|
173
|
+
concepts: ["python", "backend"],
|
|
174
|
+
files: [],
|
|
175
|
+
sessionIds: [],
|
|
176
|
+
strength: 5,
|
|
177
|
+
version: 1,
|
|
178
|
+
isLatest: true,
|
|
179
|
+
};
|
|
180
|
+
await kv.set("mem:memories", "mem_unrelated", unrelated);
|
|
181
|
+
|
|
182
|
+
const result = (await sdk.trigger("mem::cascade-update", {
|
|
183
|
+
supersededMemoryId: "mem_superseded",
|
|
184
|
+
})) as { success: boolean; flagged: { siblingMemories: number }; total: number };
|
|
185
|
+
|
|
186
|
+
expect(result.success).toBe(true);
|
|
187
|
+
expect(result.flagged.siblingMemories).toBe(1);
|
|
188
|
+
expect(result.total).toBeGreaterThanOrEqual(1);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("skips already stale nodes", async () => {
|
|
192
|
+
const memory: Memory = {
|
|
193
|
+
id: "mem_skip",
|
|
194
|
+
createdAt: "2026-03-01T00:00:00Z",
|
|
195
|
+
updatedAt: "2026-03-01T00:00:00Z",
|
|
196
|
+
type: "fact",
|
|
197
|
+
title: "Skip test",
|
|
198
|
+
content: "Content",
|
|
199
|
+
concepts: [],
|
|
200
|
+
files: [],
|
|
201
|
+
sessionIds: [],
|
|
202
|
+
strength: 5,
|
|
203
|
+
version: 1,
|
|
204
|
+
isLatest: false,
|
|
205
|
+
sourceObservationIds: ["obs_s"],
|
|
206
|
+
};
|
|
207
|
+
await kv.set("mem:memories", "mem_skip", memory);
|
|
208
|
+
|
|
209
|
+
const node: GraphNode = {
|
|
210
|
+
id: "node_stale",
|
|
211
|
+
type: "concept",
|
|
212
|
+
name: "already stale",
|
|
213
|
+
properties: {},
|
|
214
|
+
sourceObservationIds: ["obs_s"],
|
|
215
|
+
createdAt: "2026-03-01T00:00:00Z",
|
|
216
|
+
stale: true,
|
|
217
|
+
};
|
|
218
|
+
await kv.set("mem:graph:nodes", "node_stale", node);
|
|
219
|
+
|
|
220
|
+
const result = (await sdk.trigger("mem::cascade-update", {
|
|
221
|
+
supersededMemoryId: "mem_skip",
|
|
222
|
+
})) as { success: boolean; flagged: { nodes: number } };
|
|
223
|
+
|
|
224
|
+
expect(result.success).toBe(true);
|
|
225
|
+
expect(result.flagged.nodes).toBe(0);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("does not flag siblings when fewer than 2 shared concepts", async () => {
|
|
229
|
+
const memory: Memory = {
|
|
230
|
+
id: "mem_one_concept",
|
|
231
|
+
createdAt: "2026-03-01T00:00:00Z",
|
|
232
|
+
updatedAt: "2026-03-01T00:00:00Z",
|
|
233
|
+
type: "fact",
|
|
234
|
+
title: "One concept",
|
|
235
|
+
content: "Content",
|
|
236
|
+
concepts: ["react"],
|
|
237
|
+
files: [],
|
|
238
|
+
sessionIds: [],
|
|
239
|
+
strength: 5,
|
|
240
|
+
version: 1,
|
|
241
|
+
isLatest: false,
|
|
242
|
+
};
|
|
243
|
+
await kv.set("mem:memories", "mem_one_concept", memory);
|
|
244
|
+
|
|
245
|
+
const result = (await sdk.trigger("mem::cascade-update", {
|
|
246
|
+
supersededMemoryId: "mem_one_concept",
|
|
247
|
+
})) as { success: boolean; flagged: { siblingMemories: number } };
|
|
248
|
+
|
|
249
|
+
expect(result.success).toBe(true);
|
|
250
|
+
expect(result.flagged.siblingMemories).toBe(0);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("returns zero counts when no sourceObservationIds and < 2 concepts", async () => {
|
|
254
|
+
const memory: Memory = {
|
|
255
|
+
id: "mem_empty",
|
|
256
|
+
createdAt: "2026-03-01T00:00:00Z",
|
|
257
|
+
updatedAt: "2026-03-01T00:00:00Z",
|
|
258
|
+
type: "fact",
|
|
259
|
+
title: "Empty refs",
|
|
260
|
+
content: "No references",
|
|
261
|
+
concepts: [],
|
|
262
|
+
files: [],
|
|
263
|
+
sessionIds: [],
|
|
264
|
+
strength: 5,
|
|
265
|
+
version: 1,
|
|
266
|
+
isLatest: false,
|
|
267
|
+
};
|
|
268
|
+
await kv.set("mem:memories", "mem_empty", memory);
|
|
269
|
+
|
|
270
|
+
const result = (await sdk.trigger("mem::cascade-update", {
|
|
271
|
+
supersededMemoryId: "mem_empty",
|
|
272
|
+
})) as { success: boolean; total: number };
|
|
273
|
+
|
|
274
|
+
expect(result.success).toBe(true);
|
|
275
|
+
expect(result.total).toBe(0);
|
|
276
|
+
});
|
|
277
|
+
});
|