@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.
Files changed (259) hide show
  1. package/.claude-plugin/marketplace.json +14 -0
  2. package/.github/workflows/ci.yml +22 -0
  3. package/.github/workflows/publish.yml +28 -0
  4. package/AGENTS.md +113 -0
  5. package/LICENSE +190 -0
  6. package/README.md +828 -0
  7. package/assets/banner.png +0 -0
  8. package/assets/demo.gif +0 -0
  9. package/assets/demo.mp4 +0 -0
  10. package/benchmark/QUALITY.md +73 -0
  11. package/benchmark/REAL-EMBEDDINGS.md +67 -0
  12. package/benchmark/SCALE.md +110 -0
  13. package/benchmark/dataset.ts +293 -0
  14. package/benchmark/quality-eval.ts +643 -0
  15. package/benchmark/real-embeddings-eval.ts +405 -0
  16. package/benchmark/scale-eval.ts +398 -0
  17. package/dist/cli.d.mts +1 -0
  18. package/dist/cli.mjs +137 -0
  19. package/dist/cli.mjs.map +1 -0
  20. package/dist/docker-compose.yml +14 -0
  21. package/dist/hooks/notification.d.mts +1 -0
  22. package/dist/hooks/notification.mjs +45 -0
  23. package/dist/hooks/notification.mjs.map +1 -0
  24. package/dist/hooks/post-tool-failure.d.mts +1 -0
  25. package/dist/hooks/post-tool-failure.mjs +45 -0
  26. package/dist/hooks/post-tool-failure.mjs.map +1 -0
  27. package/dist/hooks/post-tool-use.d.mts +1 -0
  28. package/dist/hooks/post-tool-use.mjs +53 -0
  29. package/dist/hooks/post-tool-use.mjs.map +1 -0
  30. package/dist/hooks/pre-compact.d.mts +1 -0
  31. package/dist/hooks/pre-compact.mjs +50 -0
  32. package/dist/hooks/pre-compact.mjs.map +1 -0
  33. package/dist/hooks/pre-tool-use.d.mts +1 -0
  34. package/dist/hooks/pre-tool-use.mjs +69 -0
  35. package/dist/hooks/pre-tool-use.mjs.map +1 -0
  36. package/dist/hooks/prompt-submit.d.mts +1 -0
  37. package/dist/hooks/prompt-submit.mjs +40 -0
  38. package/dist/hooks/prompt-submit.mjs.map +1 -0
  39. package/dist/hooks/session-end.d.mts +1 -0
  40. package/dist/hooks/session-end.mjs +61 -0
  41. package/dist/hooks/session-end.mjs.map +1 -0
  42. package/dist/hooks/session-start.d.mts +1 -0
  43. package/dist/hooks/session-start.mjs +42 -0
  44. package/dist/hooks/session-start.mjs.map +1 -0
  45. package/dist/hooks/stop.d.mts +1 -0
  46. package/dist/hooks/stop.mjs +33 -0
  47. package/dist/hooks/stop.mjs.map +1 -0
  48. package/dist/hooks/subagent-start.d.mts +1 -0
  49. package/dist/hooks/subagent-start.mjs +43 -0
  50. package/dist/hooks/subagent-start.mjs.map +1 -0
  51. package/dist/hooks/subagent-stop.d.mts +1 -0
  52. package/dist/hooks/subagent-stop.mjs +45 -0
  53. package/dist/hooks/subagent-stop.mjs.map +1 -0
  54. package/dist/hooks/task-completed.d.mts +1 -0
  55. package/dist/hooks/task-completed.mjs +46 -0
  56. package/dist/hooks/task-completed.mjs.map +1 -0
  57. package/dist/iii-config.yaml +51 -0
  58. package/dist/index.d.mts +2 -0
  59. package/dist/index.mjs +13776 -0
  60. package/dist/index.mjs.map +1 -0
  61. package/dist/src-QxitMPfJ.mjs +13775 -0
  62. package/dist/src-QxitMPfJ.mjs.map +1 -0
  63. package/dist/standalone.d.mts +1 -0
  64. package/dist/standalone.mjs +1155 -0
  65. package/dist/standalone.mjs.map +1 -0
  66. package/dist/transformers-BX_tgxdO.mjs +38684 -0
  67. package/dist/transformers-BX_tgxdO.mjs.map +1 -0
  68. package/dist/transformers-KMm1i9no.mjs +38683 -0
  69. package/dist/transformers-KMm1i9no.mjs.map +1 -0
  70. package/docker-compose.yml +14 -0
  71. package/iii-config.yaml +51 -0
  72. package/package.json +59 -0
  73. package/plugin/.claude-plugin/plugin.json +10 -0
  74. package/plugin/hooks/hooks.json +77 -0
  75. package/plugin/scripts/diagnostics.mjs +551 -0
  76. package/plugin/scripts/notification.mjs +45 -0
  77. package/plugin/scripts/post-tool-failure.mjs +45 -0
  78. package/plugin/scripts/post-tool-use.mjs +53 -0
  79. package/plugin/scripts/pre-compact.mjs +50 -0
  80. package/plugin/scripts/pre-tool-use.mjs +69 -0
  81. package/plugin/scripts/prompt-submit.mjs +40 -0
  82. package/plugin/scripts/session-end.mjs +61 -0
  83. package/plugin/scripts/session-start.mjs +42 -0
  84. package/plugin/scripts/stop.mjs +33 -0
  85. package/plugin/scripts/subagent-start.mjs +43 -0
  86. package/plugin/scripts/subagent-stop.mjs +45 -0
  87. package/plugin/scripts/task-completed.mjs +46 -0
  88. package/plugin/skills/forget/SKILL.md +32 -0
  89. package/plugin/skills/recall/SKILL.md +18 -0
  90. package/plugin/skills/remember/SKILL.md +25 -0
  91. package/plugin/skills/session-history/SKILL.md +17 -0
  92. package/src/auth.ts +12 -0
  93. package/src/cli.ts +159 -0
  94. package/src/config.ts +221 -0
  95. package/src/eval/metrics-store.ts +65 -0
  96. package/src/eval/quality.ts +51 -0
  97. package/src/eval/schemas.ts +124 -0
  98. package/src/eval/self-correct.ts +28 -0
  99. package/src/eval/validator.ts +31 -0
  100. package/src/functions/actions.ts +288 -0
  101. package/src/functions/audit.ts +61 -0
  102. package/src/functions/auto-forget.ts +169 -0
  103. package/src/functions/branch-aware.ts +169 -0
  104. package/src/functions/cascade.ts +80 -0
  105. package/src/functions/checkpoints.ts +209 -0
  106. package/src/functions/claude-bridge.ts +161 -0
  107. package/src/functions/compress.ts +194 -0
  108. package/src/functions/consolidate.ts +212 -0
  109. package/src/functions/consolidation-pipeline.ts +258 -0
  110. package/src/functions/context.ts +169 -0
  111. package/src/functions/crystallize.ts +293 -0
  112. package/src/functions/dedup.ts +57 -0
  113. package/src/functions/diagnostics.ts +785 -0
  114. package/src/functions/enrich.ts +132 -0
  115. package/src/functions/evict.ts +163 -0
  116. package/src/functions/export-import.ts +508 -0
  117. package/src/functions/facets.ts +248 -0
  118. package/src/functions/file-index.ts +106 -0
  119. package/src/functions/flow-compress.ts +214 -0
  120. package/src/functions/frontier.ts +196 -0
  121. package/src/functions/governance.ts +131 -0
  122. package/src/functions/graph-retrieval.ts +277 -0
  123. package/src/functions/graph.ts +275 -0
  124. package/src/functions/leases.ts +216 -0
  125. package/src/functions/lessons.ts +253 -0
  126. package/src/functions/mesh.ts +434 -0
  127. package/src/functions/migrate.ts +165 -0
  128. package/src/functions/observe.ts +144 -0
  129. package/src/functions/obsidian-export.ts +310 -0
  130. package/src/functions/patterns.ts +138 -0
  131. package/src/functions/privacy.ts +39 -0
  132. package/src/functions/profile.ts +155 -0
  133. package/src/functions/query-expansion.ts +186 -0
  134. package/src/functions/relations.ts +237 -0
  135. package/src/functions/remember.ts +162 -0
  136. package/src/functions/retention.ts +235 -0
  137. package/src/functions/routines.ts +289 -0
  138. package/src/functions/search.ts +80 -0
  139. package/src/functions/sentinels.ts +417 -0
  140. package/src/functions/signals.ts +186 -0
  141. package/src/functions/sketches.ts +274 -0
  142. package/src/functions/sliding-window.ts +257 -0
  143. package/src/functions/smart-search.ts +115 -0
  144. package/src/functions/snapshot.ts +219 -0
  145. package/src/functions/summarize.ts +155 -0
  146. package/src/functions/team.ts +147 -0
  147. package/src/functions/temporal-graph.ts +476 -0
  148. package/src/functions/timeline.ts +138 -0
  149. package/src/functions/verify.ts +117 -0
  150. package/src/health/monitor.ts +110 -0
  151. package/src/health/thresholds.ts +73 -0
  152. package/src/hooks/notification.ts +52 -0
  153. package/src/hooks/post-tool-failure.ts +58 -0
  154. package/src/hooks/post-tool-use.ts +62 -0
  155. package/src/hooks/pre-compact.ts +60 -0
  156. package/src/hooks/pre-tool-use.ts +72 -0
  157. package/src/hooks/prompt-submit.ts +46 -0
  158. package/src/hooks/session-end.ts +71 -0
  159. package/src/hooks/session-start.ts +48 -0
  160. package/src/hooks/stop.ts +39 -0
  161. package/src/hooks/subagent-start.ts +49 -0
  162. package/src/hooks/subagent-stop.ts +54 -0
  163. package/src/hooks/task-completed.ts +54 -0
  164. package/src/index.ts +342 -0
  165. package/src/mcp/in-memory-kv.ts +61 -0
  166. package/src/mcp/server.ts +1455 -0
  167. package/src/mcp/standalone.ts +177 -0
  168. package/src/mcp/tools-registry.ts +769 -0
  169. package/src/mcp/transport.ts +91 -0
  170. package/src/prompts/compression.ts +67 -0
  171. package/src/prompts/consolidation.ts +48 -0
  172. package/src/prompts/graph-extraction.ts +35 -0
  173. package/src/prompts/summary.ts +38 -0
  174. package/src/prompts/xml.ts +26 -0
  175. package/src/providers/agent-sdk.ts +34 -0
  176. package/src/providers/anthropic.ts +35 -0
  177. package/src/providers/circuit-breaker.ts +82 -0
  178. package/src/providers/embedding/cohere.ts +46 -0
  179. package/src/providers/embedding/gemini.ts +54 -0
  180. package/src/providers/embedding/index.ts +39 -0
  181. package/src/providers/embedding/local.ts +52 -0
  182. package/src/providers/embedding/openai.ts +45 -0
  183. package/src/providers/embedding/openrouter.ts +51 -0
  184. package/src/providers/embedding/voyage.ts +46 -0
  185. package/src/providers/fallback-chain.ts +31 -0
  186. package/src/providers/index.ts +84 -0
  187. package/src/providers/openrouter.ts +71 -0
  188. package/src/providers/resilient.ts +37 -0
  189. package/src/state/hybrid-search.ts +295 -0
  190. package/src/state/index-persistence.ts +63 -0
  191. package/src/state/keyed-mutex.ts +18 -0
  192. package/src/state/kv.ts +33 -0
  193. package/src/state/schema.ts +71 -0
  194. package/src/state/search-index.ts +245 -0
  195. package/src/state/stemmer.ts +104 -0
  196. package/src/state/synonyms.ts +63 -0
  197. package/src/state/vector-index.ts +130 -0
  198. package/src/telemetry/setup.ts +116 -0
  199. package/src/triggers/api.ts +1904 -0
  200. package/src/triggers/events.ts +71 -0
  201. package/src/types.ts +769 -0
  202. package/src/version.ts +1 -0
  203. package/src/viewer/index.html +2497 -0
  204. package/src/viewer/server.ts +207 -0
  205. package/src/xenova.d.ts +3 -0
  206. package/test/actions.test.ts +490 -0
  207. package/test/audit.test.ts +108 -0
  208. package/test/auto-forget.test.ts +188 -0
  209. package/test/cascade.test.ts +277 -0
  210. package/test/checkpoints.test.ts +493 -0
  211. package/test/circuit-breaker.test.ts +107 -0
  212. package/test/claude-bridge.test.ts +178 -0
  213. package/test/confidence.test.ts +247 -0
  214. package/test/consistency.test.ts +61 -0
  215. package/test/consolidation-pipeline.test.ts +251 -0
  216. package/test/crystallize.test.ts +521 -0
  217. package/test/diagnostics.test.ts +638 -0
  218. package/test/embedding-provider.test.ts +49 -0
  219. package/test/enrich.test.ts +209 -0
  220. package/test/eval.test.ts +300 -0
  221. package/test/export-import.test.ts +251 -0
  222. package/test/facets.test.ts +448 -0
  223. package/test/fallback-chain.test.ts +93 -0
  224. package/test/frontier.test.ts +485 -0
  225. package/test/governance.test.ts +147 -0
  226. package/test/graph-retrieval.test.ts +186 -0
  227. package/test/graph.test.ts +160 -0
  228. package/test/helpers/mocks.ts +40 -0
  229. package/test/hybrid-search.test.ts +145 -0
  230. package/test/index-persistence.test.ts +124 -0
  231. package/test/integration.test.ts +265 -0
  232. package/test/leases.test.ts +399 -0
  233. package/test/mcp-prompts.test.ts +218 -0
  234. package/test/mcp-resources.test.ts +286 -0
  235. package/test/mcp-standalone.test.ts +113 -0
  236. package/test/mesh.test.ts +700 -0
  237. package/test/privacy.test.ts +87 -0
  238. package/test/profile.test.ts +161 -0
  239. package/test/query-expansion.test.ts +154 -0
  240. package/test/relations.test.ts +198 -0
  241. package/test/retention.test.ts +245 -0
  242. package/test/routines.test.ts +497 -0
  243. package/test/schema-fingerprint.test.ts +81 -0
  244. package/test/schema.test.ts +42 -0
  245. package/test/search-index.test.ts +128 -0
  246. package/test/sentinels.test.ts +626 -0
  247. package/test/signals.test.ts +410 -0
  248. package/test/sketches.test.ts +549 -0
  249. package/test/sliding-window.test.ts +199 -0
  250. package/test/smart-search.test.ts +169 -0
  251. package/test/snapshot.test.ts +165 -0
  252. package/test/team.test.ts +156 -0
  253. package/test/temporal-graph.test.ts +378 -0
  254. package/test/timeline.test.ts +148 -0
  255. package/test/vector-index.test.ts +79 -0
  256. package/test/verify.test.ts +209 -0
  257. package/test/xml.test.ts +65 -0
  258. package/tsconfig.json +22 -0
  259. package/tsdown.config.ts +62 -0
@@ -0,0 +1,399 @@
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 { registerLeasesFunction } from "../src/functions/leases.js";
10
+ import type { Action, Lease } 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 makeAction(
49
+ id: string,
50
+ status: Action["status"] = "pending",
51
+ ): Action {
52
+ return {
53
+ id,
54
+ title: `Action ${id}`,
55
+ description: `Description for ${id}`,
56
+ status,
57
+ priority: 5,
58
+ createdAt: "2026-02-01T00:00:00Z",
59
+ updatedAt: "2026-02-01T00:00:00Z",
60
+ createdBy: "agent-setup",
61
+ tags: [],
62
+ sourceObservationIds: [],
63
+ sourceMemoryIds: [],
64
+ };
65
+ }
66
+
67
+ describe("Lease Functions", () => {
68
+ let sdk: ReturnType<typeof mockSdk>;
69
+ let kv: ReturnType<typeof mockKV>;
70
+
71
+ beforeEach(async () => {
72
+ sdk = mockSdk();
73
+ kv = mockKV();
74
+ registerLeasesFunction(sdk as never, kv as never);
75
+
76
+ await kv.set("mem:actions", "act_1", makeAction("act_1", "pending"));
77
+ await kv.set("mem:actions", "act_2", makeAction("act_2", "done"));
78
+ await kv.set("mem:actions", "act_3", makeAction("act_3", "cancelled"));
79
+ await kv.set("mem:actions", "act_4", makeAction("act_4", "pending"));
80
+ });
81
+
82
+ describe("mem::lease-acquire", () => {
83
+ it("acquires a lease for a valid action", async () => {
84
+ const result = (await sdk.trigger("mem::lease-acquire", {
85
+ actionId: "act_1",
86
+ agentId: "agent-a",
87
+ })) as { success: boolean; lease: Lease; renewed: boolean };
88
+
89
+ expect(result.success).toBe(true);
90
+ expect(result.lease.actionId).toBe("act_1");
91
+ expect(result.lease.agentId).toBe("agent-a");
92
+ expect(result.lease.status).toBe("active");
93
+ expect(result.renewed).toBe(false);
94
+ expect(result.lease.id).toMatch(/^lse_/);
95
+ });
96
+
97
+ it("returns error when actionId or agentId is missing", async () => {
98
+ const r1 = (await sdk.trigger("mem::lease-acquire", {
99
+ actionId: "act_1",
100
+ agentId: "",
101
+ })) as { success: boolean; error: string };
102
+ expect(r1.success).toBe(false);
103
+ expect(r1.error).toBe("actionId and agentId are required");
104
+
105
+ const r2 = (await sdk.trigger("mem::lease-acquire", {
106
+ actionId: "",
107
+ agentId: "agent-a",
108
+ })) as { success: boolean; error: string };
109
+ expect(r2.success).toBe(false);
110
+ expect(r2.error).toBe("actionId and agentId are required");
111
+ });
112
+
113
+ it("returns error for nonexistent action", async () => {
114
+ const result = (await sdk.trigger("mem::lease-acquire", {
115
+ actionId: "act_nonexistent",
116
+ agentId: "agent-a",
117
+ })) as { success: boolean; error: string };
118
+
119
+ expect(result.success).toBe(false);
120
+ expect(result.error).toBe("action not found");
121
+ });
122
+
123
+ it("returns error for done action", async () => {
124
+ const result = (await sdk.trigger("mem::lease-acquire", {
125
+ actionId: "act_2",
126
+ agentId: "agent-a",
127
+ })) as { success: boolean; error: string };
128
+
129
+ expect(result.success).toBe(false);
130
+ expect(result.error).toBe("action already completed");
131
+ });
132
+
133
+ it("returns error for cancelled action", async () => {
134
+ const result = (await sdk.trigger("mem::lease-acquire", {
135
+ actionId: "act_3",
136
+ agentId: "agent-a",
137
+ })) as { success: boolean; error: string };
138
+
139
+ expect(result.success).toBe(false);
140
+ expect(result.error).toBe("action already completed");
141
+ });
142
+
143
+ it("returns existing lease when same agent already holds it", async () => {
144
+ const first = (await sdk.trigger("mem::lease-acquire", {
145
+ actionId: "act_1",
146
+ agentId: "agent-a",
147
+ })) as { success: boolean; lease: Lease };
148
+
149
+ const second = (await sdk.trigger("mem::lease-acquire", {
150
+ actionId: "act_1",
151
+ agentId: "agent-a",
152
+ })) as { success: boolean; lease: Lease; renewed: boolean; message: string };
153
+
154
+ expect(second.success).toBe(true);
155
+ expect(second.lease.id).toBe(first.lease.id);
156
+ expect(second.renewed).toBe(false);
157
+ expect(second.message).toBe("Already holding this lease");
158
+ });
159
+
160
+ it("returns conflict error when different agent holds the lease", async () => {
161
+ await sdk.trigger("mem::lease-acquire", {
162
+ actionId: "act_1",
163
+ agentId: "agent-a",
164
+ });
165
+
166
+ const result = (await sdk.trigger("mem::lease-acquire", {
167
+ actionId: "act_1",
168
+ agentId: "agent-b",
169
+ })) as { success: boolean; error: string; heldBy: string; expiresAt: string };
170
+
171
+ expect(result.success).toBe(false);
172
+ expect(result.error).toBe("action already leased");
173
+ expect(result.heldBy).toBe("agent-a");
174
+ expect(result.expiresAt).toBeDefined();
175
+ });
176
+
177
+ it("sets action status to active after acquire", async () => {
178
+ await sdk.trigger("mem::lease-acquire", {
179
+ actionId: "act_1",
180
+ agentId: "agent-a",
181
+ });
182
+
183
+ const action = await kv.get<Action>("mem:actions", "act_1");
184
+ expect(action!.status).toBe("active");
185
+ expect(action!.assignedTo).toBe("agent-a");
186
+ });
187
+ });
188
+
189
+ describe("mem::lease-release", () => {
190
+ it("releases an active lease", async () => {
191
+ await sdk.trigger("mem::lease-acquire", {
192
+ actionId: "act_1",
193
+ agentId: "agent-a",
194
+ });
195
+
196
+ const result = (await sdk.trigger("mem::lease-release", {
197
+ actionId: "act_1",
198
+ agentId: "agent-a",
199
+ })) as { success: boolean; released: boolean };
200
+
201
+ expect(result.success).toBe(true);
202
+ expect(result.released).toBe(true);
203
+ });
204
+
205
+ it("sets action to done when result is provided", async () => {
206
+ await sdk.trigger("mem::lease-acquire", {
207
+ actionId: "act_1",
208
+ agentId: "agent-a",
209
+ });
210
+
211
+ await sdk.trigger("mem::lease-release", {
212
+ actionId: "act_1",
213
+ agentId: "agent-a",
214
+ result: "completed successfully",
215
+ });
216
+
217
+ const action = await kv.get<Action>("mem:actions", "act_1");
218
+ expect(action!.status).toBe("done");
219
+ expect(action!.result).toBe("completed successfully");
220
+ expect(action!.assignedTo).toBeUndefined();
221
+ });
222
+
223
+ it("sets action to pending when no result is provided", async () => {
224
+ await sdk.trigger("mem::lease-acquire", {
225
+ actionId: "act_1",
226
+ agentId: "agent-a",
227
+ });
228
+
229
+ await sdk.trigger("mem::lease-release", {
230
+ actionId: "act_1",
231
+ agentId: "agent-a",
232
+ });
233
+
234
+ const action = await kv.get<Action>("mem:actions", "act_1");
235
+ expect(action!.status).toBe("pending");
236
+ expect(action!.assignedTo).toBeUndefined();
237
+ });
238
+
239
+ it("returns error when no active lease exists for agent", async () => {
240
+ const result = (await sdk.trigger("mem::lease-release", {
241
+ actionId: "act_1",
242
+ agentId: "agent-a",
243
+ })) as { success: boolean; error: string };
244
+
245
+ expect(result.success).toBe(false);
246
+ expect(result.error).toBe("no active lease found for this agent");
247
+ });
248
+
249
+ it("returns error when actionId or agentId is missing", async () => {
250
+ const result = (await sdk.trigger("mem::lease-release", {
251
+ actionId: "",
252
+ agentId: "agent-a",
253
+ })) as { success: boolean; error: string };
254
+
255
+ expect(result.success).toBe(false);
256
+ expect(result.error).toBe("actionId and agentId are required");
257
+ });
258
+ });
259
+
260
+ describe("mem::lease-renew", () => {
261
+ it("renews an active non-expired lease", async () => {
262
+ const acquired = (await sdk.trigger("mem::lease-acquire", {
263
+ actionId: "act_1",
264
+ agentId: "agent-a",
265
+ })) as { success: boolean; lease: Lease };
266
+
267
+ const originalExpiry = acquired.lease.expiresAt;
268
+
269
+ const result = (await sdk.trigger("mem::lease-renew", {
270
+ actionId: "act_1",
271
+ agentId: "agent-a",
272
+ })) as { success: boolean; lease: Lease };
273
+
274
+ expect(result.success).toBe(true);
275
+ expect(result.lease.renewedAt).toBeDefined();
276
+ expect(
277
+ new Date(result.lease.expiresAt).getTime(),
278
+ ).toBeGreaterThanOrEqual(new Date(originalExpiry).getTime());
279
+ });
280
+
281
+ it("returns error when lease is expired", async () => {
282
+ const acquired = (await sdk.trigger("mem::lease-acquire", {
283
+ actionId: "act_1",
284
+ agentId: "agent-a",
285
+ })) as { success: boolean; lease: Lease };
286
+
287
+ acquired.lease.expiresAt = new Date(Date.now() - 60000).toISOString();
288
+ await kv.set("mem:leases", acquired.lease.id, acquired.lease);
289
+
290
+ const result = (await sdk.trigger("mem::lease-renew", {
291
+ actionId: "act_1",
292
+ agentId: "agent-a",
293
+ })) as { success: boolean; error: string };
294
+
295
+ expect(result.success).toBe(false);
296
+ expect(result.error).toBe("no active (non-expired) lease to renew");
297
+ });
298
+
299
+ it("returns error when actionId or agentId is missing", async () => {
300
+ const result = (await sdk.trigger("mem::lease-renew", {
301
+ actionId: "",
302
+ agentId: "agent-a",
303
+ })) as { success: boolean; error: string };
304
+
305
+ expect(result.success).toBe(false);
306
+ expect(result.error).toBe("actionId and agentId are required");
307
+ });
308
+ });
309
+
310
+ describe("mem::lease-cleanup", () => {
311
+ it("expires active leases past their expiresAt and resets actions to pending", async () => {
312
+ const acquired = (await sdk.trigger("mem::lease-acquire", {
313
+ actionId: "act_1",
314
+ agentId: "agent-a",
315
+ })) as { success: boolean; lease: Lease };
316
+
317
+ acquired.lease.expiresAt = new Date(Date.now() - 60000).toISOString();
318
+ await kv.set("mem:leases", acquired.lease.id, acquired.lease);
319
+
320
+ const result = (await sdk.trigger("mem::lease-cleanup", {})) as {
321
+ success: boolean;
322
+ expired: number;
323
+ };
324
+
325
+ expect(result.success).toBe(true);
326
+ expect(result.expired).toBe(1);
327
+
328
+ const lease = await kv.get<Lease>("mem:leases", acquired.lease.id);
329
+ expect(lease!.status).toBe("expired");
330
+
331
+ const action = await kv.get<Action>("mem:actions", "act_1");
332
+ expect(action!.status).toBe("pending");
333
+ expect(action!.assignedTo).toBeUndefined();
334
+ });
335
+
336
+ it("does not expire non-expired active leases", async () => {
337
+ await sdk.trigger("mem::lease-acquire", {
338
+ actionId: "act_1",
339
+ agentId: "agent-a",
340
+ });
341
+
342
+ const result = (await sdk.trigger("mem::lease-cleanup", {})) as {
343
+ success: boolean;
344
+ expired: number;
345
+ };
346
+
347
+ expect(result.success).toBe(true);
348
+ expect(result.expired).toBe(0);
349
+
350
+ const action = await kv.get<Action>("mem:actions", "act_1");
351
+ expect(action!.status).toBe("active");
352
+ });
353
+
354
+ it("handles multiple expired leases across different actions", async () => {
355
+ const a1 = (await sdk.trigger("mem::lease-acquire", {
356
+ actionId: "act_1",
357
+ agentId: "agent-a",
358
+ })) as { success: boolean; lease: Lease };
359
+
360
+ const a4 = (await sdk.trigger("mem::lease-acquire", {
361
+ actionId: "act_4",
362
+ agentId: "agent-b",
363
+ })) as { success: boolean; lease: Lease };
364
+
365
+ a1.lease.expiresAt = new Date(Date.now() - 60000).toISOString();
366
+ await kv.set("mem:leases", a1.lease.id, a1.lease);
367
+
368
+ a4.lease.expiresAt = new Date(Date.now() - 30000).toISOString();
369
+ await kv.set("mem:leases", a4.lease.id, a4.lease);
370
+
371
+ const result = (await sdk.trigger("mem::lease-cleanup", {})) as {
372
+ success: boolean;
373
+ expired: number;
374
+ };
375
+
376
+ expect(result.success).toBe(true);
377
+ expect(result.expired).toBe(2);
378
+ });
379
+
380
+ it("does not reset action when action is no longer active", async () => {
381
+ const acquired = (await sdk.trigger("mem::lease-acquire", {
382
+ actionId: "act_1",
383
+ agentId: "agent-a",
384
+ })) as { success: boolean; lease: Lease };
385
+
386
+ acquired.lease.expiresAt = new Date(Date.now() - 60000).toISOString();
387
+ await kv.set("mem:leases", acquired.lease.id, acquired.lease);
388
+
389
+ const action = await kv.get<Action>("mem:actions", "act_1");
390
+ action!.status = "done";
391
+ await kv.set("mem:actions", "act_1", action);
392
+
393
+ await sdk.trigger("mem::lease-cleanup", {});
394
+
395
+ const updatedAction = await kv.get<Action>("mem:actions", "act_1");
396
+ expect(updatedAction!.status).toBe("done");
397
+ });
398
+ });
399
+ });
@@ -0,0 +1,218 @@
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 { registerMcpEndpoints } from "../src/mcp/server.js";
10
+ import type { Session, SessionSummary, Memory } 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
+ const triggerOverrides = new Map<string, Function>();
36
+ return {
37
+ registerFunction: (opts: { id: string }, handler: Function) => {
38
+ functions.set(opts.id, handler);
39
+ },
40
+ registerTrigger: () => {},
41
+ trigger: async (id: string, data: unknown) => {
42
+ if (triggerOverrides.has(id)) {
43
+ return triggerOverrides.get(id)!(data);
44
+ }
45
+ const fn = functions.get(id);
46
+ if (!fn) throw new Error(`No function: ${id}`);
47
+ return fn(data);
48
+ },
49
+ overrideTrigger: (id: string, handler: Function) => {
50
+ triggerOverrides.set(id, handler);
51
+ },
52
+ getFunction: (id: string) => functions.get(id),
53
+ };
54
+ }
55
+
56
+ function makeReq(body?: unknown, headers?: Record<string, string>) {
57
+ return {
58
+ body,
59
+ headers: headers || {},
60
+ query_params: {},
61
+ };
62
+ }
63
+
64
+ describe("MCP Prompts", () => {
65
+ let sdk: ReturnType<typeof mockSdk>;
66
+ let kv: ReturnType<typeof mockKV>;
67
+
68
+ beforeEach(() => {
69
+ sdk = mockSdk();
70
+ kv = mockKV();
71
+ registerMcpEndpoints(sdk as never, kv as never);
72
+ });
73
+
74
+ it("lists 3 prompts", async () => {
75
+ const fn = sdk.getFunction("mcp::prompts::list")!;
76
+ const result = (await fn(makeReq())) as {
77
+ status_code: number;
78
+ body: { prompts: unknown[] };
79
+ };
80
+
81
+ expect(result.status_code).toBe(200);
82
+ expect(result.body.prompts).toHaveLength(3);
83
+ });
84
+
85
+ it("recall_context returns messages with search results", async () => {
86
+ sdk.overrideTrigger("mem::search", async () => ({
87
+ results: [{ observation: { title: "Found something" } }],
88
+ }));
89
+
90
+ const mem: Memory = {
91
+ id: "mem_1",
92
+ createdAt: new Date().toISOString(),
93
+ updatedAt: new Date().toISOString(),
94
+ type: "pattern",
95
+ title: "Auth pattern",
96
+ content: "Always use JWT",
97
+ concepts: ["auth"],
98
+ files: [],
99
+ sessionIds: [],
100
+ strength: 5,
101
+ version: 1,
102
+ isLatest: true,
103
+ };
104
+ await kv.set("mem:memories", "mem_1", mem);
105
+
106
+ const fn = sdk.getFunction("mcp::prompts::get")!;
107
+ const result = (await fn(
108
+ makeReq({
109
+ name: "recall_context",
110
+ arguments: { task_description: "implement auth" },
111
+ }),
112
+ )) as {
113
+ status_code: number;
114
+ body: { messages: Array<{ role: string; content: { text: string } }> };
115
+ };
116
+
117
+ expect(result.status_code).toBe(200);
118
+ expect(result.body.messages).toHaveLength(1);
119
+ expect(result.body.messages[0].role).toBe("user");
120
+ expect(result.body.messages[0].content.text).toContain("implement auth");
121
+ });
122
+
123
+ it("session_handoff returns session data", async () => {
124
+ const session: Session = {
125
+ id: "ses_1",
126
+ project: "/test",
127
+ cwd: "/test",
128
+ startedAt: "2026-02-01T00:00:00Z",
129
+ status: "completed",
130
+ observationCount: 10,
131
+ };
132
+ await kv.set("mem:sessions", "ses_1", session);
133
+
134
+ const summary: SessionSummary = {
135
+ sessionId: "ses_1",
136
+ project: "/test",
137
+ createdAt: "2026-02-01T00:00:00Z",
138
+ title: "Auth implementation",
139
+ narrative: "Implemented JWT auth",
140
+ keyDecisions: ["Used JWT"],
141
+ filesModified: ["src/auth.ts"],
142
+ concepts: ["auth"],
143
+ observationCount: 10,
144
+ };
145
+ await kv.set("mem:summaries", "ses_1", summary);
146
+
147
+ const fn = sdk.getFunction("mcp::prompts::get")!;
148
+ const result = (await fn(
149
+ makeReq({
150
+ name: "session_handoff",
151
+ arguments: { session_id: "ses_1" },
152
+ }),
153
+ )) as {
154
+ status_code: number;
155
+ body: { messages: Array<{ role: string; content: { text: string } }> };
156
+ };
157
+
158
+ expect(result.status_code).toBe(200);
159
+ expect(result.body.messages[0].content.text).toContain("Session Handoff");
160
+ expect(result.body.messages[0].content.text).toContain("ses_1");
161
+ });
162
+
163
+ it("detect_patterns returns analysis", async () => {
164
+ sdk.overrideTrigger("mem::patterns", async () => ({
165
+ fileCoOccurrence: [{ files: ["a.ts", "b.ts"], count: 5 }],
166
+ }));
167
+
168
+ const fn = sdk.getFunction("mcp::prompts::get")!;
169
+ const result = (await fn(
170
+ makeReq({
171
+ name: "detect_patterns",
172
+ arguments: { project: "/myapp" },
173
+ }),
174
+ )) as {
175
+ status_code: number;
176
+ body: { messages: Array<{ role: string; content: { text: string } }> };
177
+ };
178
+
179
+ expect(result.status_code).toBe(200);
180
+ expect(result.body.messages[0].content.text).toContain("Pattern Analysis");
181
+ });
182
+
183
+ it("returns 400 for missing required arg", async () => {
184
+ const fn = sdk.getFunction("mcp::prompts::get")!;
185
+ const result = (await fn(
186
+ makeReq({
187
+ name: "recall_context",
188
+ arguments: {},
189
+ }),
190
+ )) as { status_code: number };
191
+
192
+ expect(result.status_code).toBe(400);
193
+ });
194
+
195
+ it("returns 400 for unknown prompt name", async () => {
196
+ const fn = sdk.getFunction("mcp::prompts::get")!;
197
+ const result = (await fn(
198
+ makeReq({
199
+ name: "nonexistent_prompt",
200
+ arguments: {},
201
+ }),
202
+ )) as { status_code: number };
203
+
204
+ expect(result.status_code).toBe(400);
205
+ });
206
+
207
+ it("returns 400 for non-string argument value", async () => {
208
+ const fn = sdk.getFunction("mcp::prompts::get")!;
209
+ const result = (await fn(
210
+ makeReq({
211
+ name: "recall_context",
212
+ arguments: { task_description: 42 },
213
+ }),
214
+ )) as { status_code: number };
215
+
216
+ expect(result.status_code).toBe(400);
217
+ });
218
+ });