claude-code-swarm 0.3.3 → 0.3.5

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 (273) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +22 -1
  3. package/.claude-plugin/run-agent-inbox-mcp.sh +76 -0
  4. package/.claude-plugin/run-minimem-mcp.sh +98 -0
  5. package/.claude-plugin/run-opentasks-mcp.sh +65 -0
  6. package/CLAUDE.md +200 -36
  7. package/README.md +65 -0
  8. package/e2e/helpers/cleanup.mjs +17 -3
  9. package/e2e/helpers/map-mock-server.mjs +201 -25
  10. package/e2e/helpers/sidecar.mjs +222 -0
  11. package/e2e/helpers/workspace.mjs +2 -1
  12. package/e2e/tier5-sidecar-inbox.test.mjs +900 -0
  13. package/e2e/tier6-inbox-mcp.test.mjs +173 -0
  14. package/e2e/tier6-live-agent.test.mjs +759 -0
  15. package/e2e/vitest.config.e2e.mjs +1 -1
  16. package/hooks/hooks.json +15 -8
  17. package/package.json +13 -1
  18. package/references/agent-inbox/CLAUDE.md +151 -0
  19. package/references/agent-inbox/README.md +238 -0
  20. package/references/agent-inbox/docs/CLAUDE-CODE-SWARM-PROPOSAL.md +137 -0
  21. package/references/agent-inbox/docs/DESIGN.md +1156 -0
  22. package/references/agent-inbox/hooks/inbox-hook.mjs +119 -0
  23. package/references/agent-inbox/hooks/register-hook.mjs +69 -0
  24. package/references/agent-inbox/package-lock.json +3347 -0
  25. package/references/agent-inbox/package.json +58 -0
  26. package/references/agent-inbox/rules/agent-inbox.md +78 -0
  27. package/references/agent-inbox/src/federation/address.ts +61 -0
  28. package/references/agent-inbox/src/federation/connection-manager.ts +573 -0
  29. package/references/agent-inbox/src/federation/delivery-queue.ts +222 -0
  30. package/references/agent-inbox/src/federation/index.ts +6 -0
  31. package/references/agent-inbox/src/federation/routing-engine.ts +188 -0
  32. package/references/agent-inbox/src/federation/trust.ts +71 -0
  33. package/references/agent-inbox/src/index.ts +390 -0
  34. package/references/agent-inbox/src/ipc/ipc-server.ts +207 -0
  35. package/references/agent-inbox/src/jsonrpc/mail-server.ts +382 -0
  36. package/references/agent-inbox/src/map/map-client.ts +414 -0
  37. package/references/agent-inbox/src/mcp/mcp-server.ts +272 -0
  38. package/references/agent-inbox/src/mesh/delivery-bridge.ts +110 -0
  39. package/references/agent-inbox/src/mesh/mesh-connector.ts +41 -0
  40. package/references/agent-inbox/src/mesh/mesh-transport.ts +157 -0
  41. package/references/agent-inbox/src/mesh/type-mapper.ts +239 -0
  42. package/references/agent-inbox/src/push/notifier.ts +233 -0
  43. package/references/agent-inbox/src/registry/warm-registry.ts +255 -0
  44. package/references/agent-inbox/src/router/message-router.ts +175 -0
  45. package/references/agent-inbox/src/storage/interface.ts +48 -0
  46. package/references/agent-inbox/src/storage/memory.ts +145 -0
  47. package/references/agent-inbox/src/storage/sqlite.ts +671 -0
  48. package/references/agent-inbox/src/traceability/traceability.ts +183 -0
  49. package/references/agent-inbox/src/types.ts +303 -0
  50. package/references/agent-inbox/test/federation/address.test.ts +101 -0
  51. package/references/agent-inbox/test/federation/connection-manager.test.ts +546 -0
  52. package/references/agent-inbox/test/federation/delivery-queue.test.ts +159 -0
  53. package/references/agent-inbox/test/federation/integration.test.ts +857 -0
  54. package/references/agent-inbox/test/federation/routing-engine.test.ts +117 -0
  55. package/references/agent-inbox/test/federation/sdk-integration.test.ts +744 -0
  56. package/references/agent-inbox/test/federation/trust.test.ts +89 -0
  57. package/references/agent-inbox/test/ipc-jsonrpc.test.ts +113 -0
  58. package/references/agent-inbox/test/ipc-server.test.ts +197 -0
  59. package/references/agent-inbox/test/mail-server.test.ts +285 -0
  60. package/references/agent-inbox/test/map-client.test.ts +408 -0
  61. package/references/agent-inbox/test/mesh/delivery-bridge.test.ts +178 -0
  62. package/references/agent-inbox/test/mesh/e2e-mesh.test.ts +527 -0
  63. package/references/agent-inbox/test/mesh/e2e-real-meshpeer.test.ts +629 -0
  64. package/references/agent-inbox/test/mesh/federation-mesh.test.ts +269 -0
  65. package/references/agent-inbox/test/mesh/mesh-connector.test.ts +66 -0
  66. package/references/agent-inbox/test/mesh/mesh-transport.test.ts +191 -0
  67. package/references/agent-inbox/test/mesh/meshpeer-integration.test.ts +442 -0
  68. package/references/agent-inbox/test/mesh/mock-mesh.ts +125 -0
  69. package/references/agent-inbox/test/mesh/mock-meshpeer.ts +266 -0
  70. package/references/agent-inbox/test/mesh/type-mapper.test.ts +226 -0
  71. package/references/agent-inbox/test/message-router.test.ts +184 -0
  72. package/references/agent-inbox/test/push-notifier.test.ts +139 -0
  73. package/references/agent-inbox/test/registry/warm-registry.test.ts +171 -0
  74. package/references/agent-inbox/test/sqlite-prefix.test.ts +192 -0
  75. package/references/agent-inbox/test/sqlite-storage.test.ts +243 -0
  76. package/references/agent-inbox/test/storage.test.ts +196 -0
  77. package/references/agent-inbox/test/traceability.test.ts +123 -0
  78. package/references/agent-inbox/test/wake.test.ts +330 -0
  79. package/references/agent-inbox/tsconfig.json +20 -0
  80. package/references/agent-inbox/tsup.config.ts +10 -0
  81. package/references/agent-inbox/vitest.config.ts +8 -0
  82. package/references/minimem/.claude/settings.json +7 -0
  83. package/references/minimem/.sudocode/issues.jsonl +18 -0
  84. package/references/minimem/.sudocode/specs.jsonl +1 -0
  85. package/references/minimem/CLAUDE.md +329 -0
  86. package/references/minimem/README.md +565 -0
  87. package/references/minimem/claude-plugin/.claude-plugin/plugin.json +10 -0
  88. package/references/minimem/claude-plugin/.mcp.json +7 -0
  89. package/references/minimem/claude-plugin/README.md +158 -0
  90. package/references/minimem/claude-plugin/commands/recall.md +47 -0
  91. package/references/minimem/claude-plugin/commands/remember.md +41 -0
  92. package/references/minimem/claude-plugin/hooks/__tests__/hooks.test.ts +272 -0
  93. package/references/minimem/claude-plugin/hooks/hooks.json +27 -0
  94. package/references/minimem/claude-plugin/hooks/session-end.sh +86 -0
  95. package/references/minimem/claude-plugin/hooks/session-start.sh +85 -0
  96. package/references/minimem/claude-plugin/skills/memory/SKILL.md +108 -0
  97. package/references/minimem/media/banner.png +0 -0
  98. package/references/minimem/package-lock.json +5373 -0
  99. package/references/minimem/package.json +76 -0
  100. package/references/minimem/scripts/postbuild.js +49 -0
  101. package/references/minimem/src/__tests__/edge-cases.test.ts +371 -0
  102. package/references/minimem/src/__tests__/errors.test.ts +265 -0
  103. package/references/minimem/src/__tests__/helpers.ts +199 -0
  104. package/references/minimem/src/__tests__/internal.test.ts +407 -0
  105. package/references/minimem/src/__tests__/knowledge-frontmatter.test.ts +148 -0
  106. package/references/minimem/src/__tests__/knowledge.test.ts +148 -0
  107. package/references/minimem/src/__tests__/minimem.integration.test.ts +1127 -0
  108. package/references/minimem/src/__tests__/session.test.ts +190 -0
  109. package/references/minimem/src/cli/__tests__/commands.test.ts +760 -0
  110. package/references/minimem/src/cli/__tests__/contained-layout.test.ts +286 -0
  111. package/references/minimem/src/cli/commands/__tests__/conflicts.test.ts +141 -0
  112. package/references/minimem/src/cli/commands/append.ts +76 -0
  113. package/references/minimem/src/cli/commands/config.ts +262 -0
  114. package/references/minimem/src/cli/commands/conflicts.ts +415 -0
  115. package/references/minimem/src/cli/commands/daemon.ts +169 -0
  116. package/references/minimem/src/cli/commands/index.ts +12 -0
  117. package/references/minimem/src/cli/commands/init.ts +166 -0
  118. package/references/minimem/src/cli/commands/mcp.ts +221 -0
  119. package/references/minimem/src/cli/commands/push-pull.ts +213 -0
  120. package/references/minimem/src/cli/commands/search.ts +223 -0
  121. package/references/minimem/src/cli/commands/status.ts +84 -0
  122. package/references/minimem/src/cli/commands/store.ts +189 -0
  123. package/references/minimem/src/cli/commands/sync-init.ts +290 -0
  124. package/references/minimem/src/cli/commands/sync.ts +70 -0
  125. package/references/minimem/src/cli/commands/upsert.ts +197 -0
  126. package/references/minimem/src/cli/config.ts +611 -0
  127. package/references/minimem/src/cli/index.ts +299 -0
  128. package/references/minimem/src/cli/shared.ts +189 -0
  129. package/references/minimem/src/cli/sync/__tests__/central.test.ts +152 -0
  130. package/references/minimem/src/cli/sync/__tests__/conflicts.test.ts +209 -0
  131. package/references/minimem/src/cli/sync/__tests__/daemon.test.ts +118 -0
  132. package/references/minimem/src/cli/sync/__tests__/detection.test.ts +207 -0
  133. package/references/minimem/src/cli/sync/__tests__/integration.test.ts +476 -0
  134. package/references/minimem/src/cli/sync/__tests__/registry.test.ts +363 -0
  135. package/references/minimem/src/cli/sync/__tests__/state.test.ts +255 -0
  136. package/references/minimem/src/cli/sync/__tests__/validation.test.ts +193 -0
  137. package/references/minimem/src/cli/sync/__tests__/watcher.test.ts +178 -0
  138. package/references/minimem/src/cli/sync/central.ts +292 -0
  139. package/references/minimem/src/cli/sync/conflicts.ts +205 -0
  140. package/references/minimem/src/cli/sync/daemon.ts +407 -0
  141. package/references/minimem/src/cli/sync/detection.ts +138 -0
  142. package/references/minimem/src/cli/sync/index.ts +107 -0
  143. package/references/minimem/src/cli/sync/operations.ts +373 -0
  144. package/references/minimem/src/cli/sync/registry.ts +279 -0
  145. package/references/minimem/src/cli/sync/state.ts +358 -0
  146. package/references/minimem/src/cli/sync/validation.ts +206 -0
  147. package/references/minimem/src/cli/sync/watcher.ts +237 -0
  148. package/references/minimem/src/cli/version.ts +34 -0
  149. package/references/minimem/src/core/index.ts +9 -0
  150. package/references/minimem/src/core/indexer.ts +628 -0
  151. package/references/minimem/src/core/searcher.ts +221 -0
  152. package/references/minimem/src/db/schema.ts +183 -0
  153. package/references/minimem/src/db/sqlite-vec.ts +24 -0
  154. package/references/minimem/src/embeddings/__tests__/embeddings.test.ts +431 -0
  155. package/references/minimem/src/embeddings/batch-gemini.ts +392 -0
  156. package/references/minimem/src/embeddings/batch-openai.ts +409 -0
  157. package/references/minimem/src/embeddings/embeddings.ts +434 -0
  158. package/references/minimem/src/index.ts +132 -0
  159. package/references/minimem/src/internal.ts +299 -0
  160. package/references/minimem/src/minimem.ts +1291 -0
  161. package/references/minimem/src/search/__tests__/hybrid.test.ts +247 -0
  162. package/references/minimem/src/search/graph.ts +234 -0
  163. package/references/minimem/src/search/hybrid.ts +151 -0
  164. package/references/minimem/src/search/search.ts +256 -0
  165. package/references/minimem/src/server/__tests__/mcp.test.ts +347 -0
  166. package/references/minimem/src/server/__tests__/tools.test.ts +364 -0
  167. package/references/minimem/src/server/mcp.ts +326 -0
  168. package/references/minimem/src/server/tools.ts +720 -0
  169. package/references/minimem/src/session.ts +460 -0
  170. package/references/minimem/src/store/__tests__/manifest.test.ts +177 -0
  171. package/references/minimem/src/store/__tests__/materialize.test.ts +52 -0
  172. package/references/minimem/src/store/__tests__/store-graph.test.ts +228 -0
  173. package/references/minimem/src/store/index.ts +27 -0
  174. package/references/minimem/src/store/manifest.ts +203 -0
  175. package/references/minimem/src/store/materialize.ts +185 -0
  176. package/references/minimem/src/store/store-graph.ts +252 -0
  177. package/references/minimem/tsconfig.json +19 -0
  178. package/references/minimem/tsup.config.ts +26 -0
  179. package/references/minimem/vitest.config.ts +29 -0
  180. package/references/openteams/src/cli/generate.ts +23 -1
  181. package/references/openteams/src/generators/agent-prompt-generator.test.ts +94 -0
  182. package/references/openteams/src/generators/agent-prompt-generator.ts +42 -13
  183. package/references/openteams/src/generators/package-generator.ts +9 -1
  184. package/references/openteams/src/generators/skill-generator.test.ts +28 -0
  185. package/references/openteams/src/generators/skill-generator.ts +10 -4
  186. package/references/skill-tree/.claude/settings.json +6 -0
  187. package/references/skill-tree/.sudocode/issues.jsonl +19 -0
  188. package/references/skill-tree/.sudocode/specs.jsonl +3 -0
  189. package/references/skill-tree/CLAUDE.md +132 -0
  190. package/references/skill-tree/README.md +396 -0
  191. package/references/skill-tree/docs/GAPS_v1.md +221 -0
  192. package/references/skill-tree/docs/INTEGRATION_PLAN.md +467 -0
  193. package/references/skill-tree/docs/TODOS.md +91 -0
  194. package/references/skill-tree/docs/anthropic_skill_guide.md +1364 -0
  195. package/references/skill-tree/docs/design/federated-skill-trees.md +524 -0
  196. package/references/skill-tree/docs/design/multi-agent-sync.md +759 -0
  197. package/references/skill-tree/docs/scraper/BRAINSTORM.md +583 -0
  198. package/references/skill-tree/docs/scraper/POC_PLAN.md +420 -0
  199. package/references/skill-tree/docs/scraper/README.md +170 -0
  200. package/references/skill-tree/examples/basic-usage.ts +157 -0
  201. package/references/skill-tree/package-lock.json +1852 -0
  202. package/references/skill-tree/package.json +66 -0
  203. package/references/skill-tree/plan.md +78 -0
  204. package/references/skill-tree/scraper/README.md +123 -0
  205. package/references/skill-tree/scraper/docs/DESIGN.md +683 -0
  206. package/references/skill-tree/scraper/docs/PLAN.md +336 -0
  207. package/references/skill-tree/scraper/drizzle.config.ts +10 -0
  208. package/references/skill-tree/scraper/package-lock.json +6329 -0
  209. package/references/skill-tree/scraper/package.json +68 -0
  210. package/references/skill-tree/scraper/test/fixtures/invalid-skill/missing-description.md +7 -0
  211. package/references/skill-tree/scraper/test/fixtures/invalid-skill/missing-name.md +7 -0
  212. package/references/skill-tree/scraper/test/fixtures/minimal-skill/SKILL.md +27 -0
  213. package/references/skill-tree/scraper/test/fixtures/skill-json/SKILL.json +21 -0
  214. package/references/skill-tree/scraper/test/fixtures/skill-with-meta/SKILL.md +54 -0
  215. package/references/skill-tree/scraper/test/fixtures/skill-with-meta/_meta.json +24 -0
  216. package/references/skill-tree/scraper/test/fixtures/valid-skill/SKILL.md +93 -0
  217. package/references/skill-tree/scraper/test/fixtures/valid-skill/_meta.json +22 -0
  218. package/references/skill-tree/scraper/tsup.config.ts +14 -0
  219. package/references/skill-tree/scraper/vitest.config.ts +17 -0
  220. package/references/skill-tree/scripts/convert-to-vitest.ts +166 -0
  221. package/references/skill-tree/skills/skill-writer/SKILL.md +339 -0
  222. package/references/skill-tree/skills/skill-writer/references/examples.md +326 -0
  223. package/references/skill-tree/skills/skill-writer/references/patterns.md +210 -0
  224. package/references/skill-tree/skills/skill-writer/references/quality-checklist.md +123 -0
  225. package/references/skill-tree/test/run-all.ts +106 -0
  226. package/references/skill-tree/test/utils.ts +128 -0
  227. package/references/skill-tree/vitest.config.ts +16 -0
  228. package/references/swarmkit/src/commands/init/phases/configure.ts +0 -22
  229. package/references/swarmkit/src/commands/init/phases/global-setup.ts +5 -3
  230. package/references/swarmkit/src/commands/init/wizard.ts +2 -2
  231. package/references/swarmkit/src/packages/setup.test.ts +53 -7
  232. package/references/swarmkit/src/packages/setup.ts +37 -1
  233. package/scripts/bootstrap.mjs +26 -1
  234. package/scripts/generate-agents.mjs +5 -1
  235. package/scripts/map-hook.mjs +97 -64
  236. package/scripts/map-sidecar.mjs +179 -25
  237. package/scripts/team-loader.mjs +12 -41
  238. package/skills/swarm/SKILL.md +89 -25
  239. package/src/__tests__/agent-generator.test.mjs +6 -13
  240. package/src/__tests__/bootstrap.test.mjs +124 -1
  241. package/src/__tests__/config.test.mjs +200 -27
  242. package/src/__tests__/e2e-live-map.test.mjs +536 -0
  243. package/src/__tests__/e2e-mesh-sidecar.test.mjs +570 -0
  244. package/src/__tests__/e2e-native-task-hooks.test.mjs +376 -0
  245. package/src/__tests__/e2e-sidecar-bridge.test.mjs +477 -0
  246. package/src/__tests__/helpers.mjs +13 -0
  247. package/src/__tests__/inbox.test.mjs +22 -89
  248. package/src/__tests__/index.test.mjs +35 -9
  249. package/src/__tests__/integration.test.mjs +513 -0
  250. package/src/__tests__/map-events.test.mjs +514 -150
  251. package/src/__tests__/mesh-connection.test.mjs +308 -0
  252. package/src/__tests__/opentasks-client.test.mjs +517 -0
  253. package/src/__tests__/paths.test.mjs +185 -41
  254. package/src/__tests__/sidecar-client.test.mjs +35 -0
  255. package/src/__tests__/sidecar-server.test.mjs +124 -0
  256. package/src/__tests__/skilltree-client.test.mjs +80 -0
  257. package/src/agent-generator.mjs +104 -33
  258. package/src/bootstrap.mjs +150 -10
  259. package/src/config.mjs +81 -17
  260. package/src/context-output.mjs +58 -8
  261. package/src/inbox.mjs +9 -54
  262. package/src/index.mjs +39 -8
  263. package/src/map-connection.mjs +4 -3
  264. package/src/map-events.mjs +350 -80
  265. package/src/mesh-connection.mjs +148 -0
  266. package/src/opentasks-client.mjs +269 -0
  267. package/src/paths.mjs +182 -27
  268. package/src/sessionlog.mjs +14 -9
  269. package/src/sidecar-client.mjs +81 -27
  270. package/src/sidecar-server.mjs +175 -16
  271. package/src/skilltree-client.mjs +173 -0
  272. package/src/template.mjs +68 -4
  273. package/vitest.config.mjs +1 -0
@@ -0,0 +1,744 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
+ import { EventEmitter } from "node:events";
3
+ import { InMemoryStorage } from "../../src/storage/memory.js";
4
+ import { MessageRouter } from "../../src/router/message-router.js";
5
+ import { ConnectionManager } from "../../src/federation/connection-manager.js";
6
+ import type {
7
+ MapConnection,
8
+ MapAgentConnectionClass,
9
+ IncomingMapMessage,
10
+ } from "../../src/map/map-client.js";
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // MockMapServer — in-process MAP server that brokers messages between
14
+ // connected "agents" (which are really federation gateways).
15
+ //
16
+ // Each call to connect() returns a MapConnection. When a connection calls
17
+ // send({ agentId }, payload, meta), the server delivers the message to the
18
+ // connection whose `name` matches `agentId` (or whose `peerSystemId` matches).
19
+ // This simulates real MAP message routing between two systems.
20
+ // ---------------------------------------------------------------------------
21
+
22
+ interface ServerConnection {
23
+ name: string;
24
+ peerSystemId?: string;
25
+ systemId?: string;
26
+ handler: ((msg: IncomingMapMessage) => void) | null;
27
+ disconnected: boolean;
28
+ }
29
+
30
+ class MockMapServer {
31
+ readonly connections: ServerConnection[] = [];
32
+
33
+ /**
34
+ * Create an SDK class bound to this server instance.
35
+ * Each call to sdkClass.connect(url, opts) registers a connection.
36
+ */
37
+ createSdkClass(): MapAgentConnectionClass {
38
+ const server = this;
39
+ return {
40
+ connect: async (
41
+ _url: string,
42
+ opts: Record<string, unknown>
43
+ ): Promise<MapConnection> => {
44
+ const name = opts.name as string;
45
+ const metadata = opts.metadata as Record<string, unknown> | undefined;
46
+
47
+ const serverConn: ServerConnection = {
48
+ name,
49
+ peerSystemId: metadata?.peerSystemId as string | undefined,
50
+ systemId: metadata?.systemId as string | undefined,
51
+ handler: null,
52
+ disconnected: false,
53
+ };
54
+ server.connections.push(serverConn);
55
+
56
+ const conn: MapConnection = {
57
+ send: async (
58
+ to: { agentId?: string; scope?: string },
59
+ payload: unknown,
60
+ meta?: Record<string, unknown>
61
+ ): Promise<void> => {
62
+ if (serverConn.disconnected) {
63
+ throw new Error("Connection closed");
64
+ }
65
+ // Check if any target is reachable — if not, throw
66
+ const delivered = server.deliver(name, to, payload, meta);
67
+ if (!delivered) {
68
+ throw new Error("No reachable target");
69
+ }
70
+ },
71
+ onMessage: (handler: (msg: IncomingMapMessage) => void): void => {
72
+ serverConn.handler = handler;
73
+ },
74
+ disconnect: async (): Promise<void> => {
75
+ serverConn.disconnected = true;
76
+ },
77
+ };
78
+
79
+ return conn;
80
+ },
81
+ };
82
+ }
83
+
84
+ /**
85
+ * Route a message from `senderName` to the appropriate connection.
86
+ *
87
+ * Routing logic (mimics MAP server behavior):
88
+ * 1. If to.agentId matches a connection's `name` → deliver there
89
+ * 2. If to.agentId matches a connection's `peerSystemId` → deliver there
90
+ * 3. Otherwise deliver to ALL connections except sender (broadcast)
91
+ */
92
+ deliver(
93
+ senderName: string,
94
+ to: { agentId?: string; scope?: string },
95
+ payload: unknown,
96
+ meta?: Record<string, unknown>
97
+ ): boolean {
98
+ const targets = this.findTargets(senderName, to);
99
+ let delivered = false;
100
+ for (const target of targets) {
101
+ if (target.handler && !target.disconnected) {
102
+ target.handler({
103
+ from: senderName,
104
+ payload,
105
+ timestamp: new Date().toISOString(),
106
+ meta: meta as IncomingMapMessage["meta"],
107
+ });
108
+ delivered = true;
109
+ }
110
+ }
111
+ return delivered;
112
+ }
113
+
114
+ private findTargets(
115
+ senderName: string,
116
+ to: { agentId?: string; scope?: string }
117
+ ): ServerConnection[] {
118
+ if (to.agentId) {
119
+ // Direct delivery: find connection by name or peerSystemId
120
+ const direct = this.connections.find(
121
+ (c) =>
122
+ !c.disconnected &&
123
+ c.name !== senderName &&
124
+ (c.name === to.agentId || c.peerSystemId === to.agentId)
125
+ );
126
+ if (direct) return [direct];
127
+
128
+ // Fall back: find connection whose systemId matches the target
129
+ // (the target agent is on that system)
130
+ const bySystem = this.connections.find(
131
+ (c) =>
132
+ !c.disconnected &&
133
+ c.name !== senderName &&
134
+ c.systemId === to.agentId
135
+ );
136
+ if (bySystem) return [bySystem];
137
+ }
138
+
139
+ // Broadcast: deliver to all non-sender connections
140
+ return this.connections.filter(
141
+ (c) => !c.disconnected && c.name !== senderName
142
+ );
143
+ }
144
+
145
+ /** Disconnect a specific connection by systemId */
146
+ disconnectSystem(systemId: string): void {
147
+ for (const conn of this.connections) {
148
+ if (conn.systemId === systemId) {
149
+ conn.disconnected = true;
150
+ }
151
+ }
152
+ }
153
+
154
+ /** Reconnect a specific connection by systemId */
155
+ reconnectSystem(systemId: string): void {
156
+ for (const conn of this.connections) {
157
+ if (conn.systemId === systemId) {
158
+ conn.disconnected = false;
159
+ }
160
+ }
161
+ }
162
+
163
+ get activeConnections(): number {
164
+ return this.connections.filter((c) => !c.disconnected).length;
165
+ }
166
+ }
167
+
168
+ // ---------------------------------------------------------------------------
169
+ // Helper: create a fully-wired "system" (storage + router + federation)
170
+ // ---------------------------------------------------------------------------
171
+
172
+ interface TestSystem {
173
+ name: string;
174
+ storage: InMemoryStorage;
175
+ events: EventEmitter;
176
+ router: MessageRouter;
177
+ federation: ConnectionManager;
178
+ }
179
+
180
+ function createTestSystem(
181
+ name: string,
182
+ sdkClass: MapAgentConnectionClass,
183
+ opts: {
184
+ allowedServers: string[];
185
+ strategy?: "table" | "broadcast" | "hierarchical";
186
+ upstream?: string[];
187
+ }
188
+ ): TestSystem {
189
+ const storage = new InMemoryStorage();
190
+ const events = new EventEmitter();
191
+ const router = new MessageRouter(storage, events, "default");
192
+
193
+ const federation = new ConnectionManager(
194
+ events,
195
+ {
196
+ systemId: name,
197
+ trust: {
198
+ allowedServers: opts.allowedServers,
199
+ scopePermissions: {},
200
+ requireAuth: false,
201
+ },
202
+ routing: {
203
+ strategy: opts.strategy ?? "table",
204
+ ...(opts.upstream ? { upstream: opts.upstream } : {}),
205
+ },
206
+ },
207
+ {
208
+ sdkClass,
209
+ onIncomingMessage: ({ from, peerId, payload, meta }) => {
210
+ const targetAgent = (meta?.targetAgent as string) ?? from;
211
+ router
212
+ .routeMessage({
213
+ from: `${from}@${peerId}`,
214
+ to: targetAgent,
215
+ payload,
216
+ scope: meta?.scope as string | undefined,
217
+ subject: meta?.subject as string | undefined,
218
+ importance: meta?.importance as "high" | "normal" | undefined,
219
+ threadTag: meta?.threadTag as string | undefined,
220
+ inReplyTo: meta?.inReplyTo as string | undefined,
221
+ metadata: meta,
222
+ })
223
+ .catch(() => {
224
+ // Silently handle routing errors in tests
225
+ });
226
+ },
227
+ }
228
+ );
229
+ router.setFederation(federation);
230
+
231
+ return { name, storage, events, router, federation };
232
+ }
233
+
234
+ // ---------------------------------------------------------------------------
235
+ // Tests
236
+ // ---------------------------------------------------------------------------
237
+
238
+ describe("Federation SDK Integration (two-system)", () => {
239
+ let mapServer: MockMapServer;
240
+ let system1: TestSystem;
241
+ let system2: TestSystem;
242
+
243
+ beforeEach(() => {
244
+ mapServer = new MockMapServer();
245
+ });
246
+
247
+ afterEach(async () => {
248
+ if (system1) await system1.federation.destroy();
249
+ if (system2) await system2.federation.destroy();
250
+ });
251
+
252
+ /**
253
+ * Helper: set up two systems federated with each other through the mock server.
254
+ */
255
+ async function setupTwoSystems(opts?: {
256
+ strategy1?: "table" | "broadcast" | "hierarchical";
257
+ strategy2?: "table" | "broadcast" | "hierarchical";
258
+ }) {
259
+ const sdk = mapServer.createSdkClass();
260
+
261
+ system1 = createTestSystem("system-1", sdk, {
262
+ allowedServers: ["system-2"],
263
+ strategy: opts?.strategy1,
264
+ });
265
+ system2 = createTestSystem("system-2", sdk, {
266
+ allowedServers: ["system-1"],
267
+ strategy: opts?.strategy2,
268
+ });
269
+
270
+ // Register local agents
271
+ system1.storage.putAgent({
272
+ agent_id: "alice",
273
+ display_name: "Alice",
274
+ scope: "default",
275
+ status: "active",
276
+ metadata: {},
277
+ registered_at: new Date().toISOString(),
278
+ last_active_at: new Date().toISOString(),
279
+ });
280
+ system2.storage.putAgent({
281
+ agent_id: "bob",
282
+ display_name: "Bob",
283
+ scope: "default",
284
+ status: "active",
285
+ metadata: {},
286
+ registered_at: new Date().toISOString(),
287
+ last_active_at: new Date().toISOString(),
288
+ });
289
+
290
+ // Federate: each system connects to the other
291
+ await system1.federation.federate({
292
+ systemId: "system-2",
293
+ url: "ws://mock-server/system-2",
294
+ });
295
+ await system2.federation.federate({
296
+ systemId: "system-1",
297
+ url: "ws://mock-server/system-1",
298
+ });
299
+
300
+ // Expose agents in routing tables
301
+ system1.federation.routing.updateFromExposure("system-2", [
302
+ { agentId: "bob" },
303
+ ]);
304
+ system2.federation.routing.updateFromExposure("system-1", [
305
+ { agentId: "alice" },
306
+ ]);
307
+ }
308
+
309
+ describe("direct federation send (system-1 → system-2)", () => {
310
+ it("should deliver a message from alice@system-1 to bob@system-2", async () => {
311
+ await setupTwoSystems();
312
+
313
+ const msg = await system1.router.routeMessage({
314
+ from: "alice",
315
+ to: "bob@system-2",
316
+ payload: "Hello Bob, this is Alice!",
317
+ });
318
+
319
+ // Message stored on system-1 (sender side)
320
+ expect(system1.storage.getMessage(msg.id)).toBeDefined();
321
+
322
+ // Note: federation routing is fire-and-forget from routeMessage's perspective,
323
+ // but our mock transport is synchronous, so delivery happens inline.
324
+ // The delivered_at is set on the message object returned by route(), but
325
+ // routeMessage doesn't wait for route() — so we check the receiver side.
326
+
327
+ // Message delivered to system-2 (receiver side) — in bob's inbox
328
+ const bobInbox = system2.storage.getInbox("bob");
329
+ expect(bobInbox).toHaveLength(1);
330
+ expect(bobInbox[0].sender_id).toBe("system-1@system-1");
331
+ expect(bobInbox[0].content).toEqual({
332
+ type: "text",
333
+ text: "Hello Bob, this is Alice!",
334
+ });
335
+ // Verify metadata propagated
336
+ expect(bobInbox[0].metadata?.targetAgent).toBe("bob");
337
+ expect(bobInbox[0].metadata?.senderId).toBe("alice");
338
+ expect(bobInbox[0].metadata?.sourceSystem).toBe("system-1");
339
+ });
340
+
341
+ it("should preserve subject, importance, and threadTag across systems", async () => {
342
+ await setupTwoSystems();
343
+
344
+ await system1.router.routeMessage({
345
+ from: "alice",
346
+ to: "bob@system-2",
347
+ payload: "Urgent thread message",
348
+ subject: "Project Alpha",
349
+ importance: "high",
350
+ threadTag: "alpha-thread",
351
+ });
352
+
353
+ const bobInbox = system2.storage.getInbox("bob");
354
+ expect(bobInbox).toHaveLength(1);
355
+ expect(bobInbox[0].subject).toBe("Project Alpha");
356
+ expect(bobInbox[0].importance).toBe("high");
357
+ expect(bobInbox[0].thread_tag).toBe("alpha-thread");
358
+ });
359
+ });
360
+
361
+ describe("bidirectional messaging", () => {
362
+ it("should support alice → bob then bob → alice", async () => {
363
+ await setupTwoSystems();
364
+
365
+ // Alice sends to Bob
366
+ await system1.router.routeMessage({
367
+ from: "alice",
368
+ to: "bob@system-2",
369
+ payload: "Hey Bob!",
370
+ });
371
+
372
+ // Bob sends to Alice
373
+ await system2.router.routeMessage({
374
+ from: "bob",
375
+ to: "alice@system-1",
376
+ payload: "Hey Alice!",
377
+ });
378
+
379
+ // Both inboxes should have messages
380
+ const bobInbox = system2.storage.getInbox("bob");
381
+ expect(bobInbox).toHaveLength(1);
382
+ expect(bobInbox[0].content).toEqual({
383
+ type: "text",
384
+ text: "Hey Bob!",
385
+ });
386
+
387
+ const aliceInbox = system1.storage.getInbox("alice");
388
+ expect(aliceInbox).toHaveLength(1);
389
+ expect(aliceInbox[0].content).toEqual({
390
+ type: "text",
391
+ text: "Hey Alice!",
392
+ });
393
+ });
394
+
395
+ it("should support multiple messages in both directions", async () => {
396
+ await setupTwoSystems();
397
+
398
+ await system1.router.routeMessage({
399
+ from: "alice",
400
+ to: "bob@system-2",
401
+ payload: "Message 1",
402
+ });
403
+ await system2.router.routeMessage({
404
+ from: "bob",
405
+ to: "alice@system-1",
406
+ payload: "Message 2",
407
+ });
408
+ await system1.router.routeMessage({
409
+ from: "alice",
410
+ to: "bob@system-2",
411
+ payload: "Message 3",
412
+ });
413
+
414
+ expect(system2.storage.getInbox("bob")).toHaveLength(2);
415
+ expect(system1.storage.getInbox("alice")).toHaveLength(1);
416
+ });
417
+ });
418
+
419
+ describe("offline peer — queue and flush with transport", () => {
420
+ it("should queue messages when peer transport is down, then flush on reconnect", async () => {
421
+ await setupTwoSystems();
422
+
423
+ // Verify initial connectivity
424
+ expect(system1.federation.hasTransport("system-2")).toBe(true);
425
+
426
+ // Disconnect system-2's connections on the server side.
427
+ // This makes conn.send() throw "No reachable target" because
428
+ // deliver() returns false when target connections are disconnected.
429
+ mapServer.disconnectSystem("system-2");
430
+
431
+ // Alice tries to send — transport send fails, message gets queued
432
+ const msg1 = {
433
+ id: "queued-1",
434
+ scope: "default",
435
+ sender_id: "alice",
436
+ recipients: [{ agent_id: "bob@system-2", kind: "to" as const }],
437
+ content: { type: "text" as const, text: "Are you there?" },
438
+ importance: "normal" as const,
439
+ metadata: {},
440
+ created_at: new Date().toISOString(),
441
+ };
442
+ const result = await system1.federation.route(msg1);
443
+
444
+ // Message should be queued (send threw because server couldn't deliver)
445
+ expect(result.delivered).toBe(false);
446
+ expect(result.queued).toBe(true);
447
+ expect(system1.federation.queue.size("system-2")).toBe(1);
448
+
449
+ // Bob's inbox should be empty (message didn't get through)
450
+ expect(system2.storage.getInbox("bob")).toHaveLength(0);
451
+
452
+ // Reconnect system-2 on server side
453
+ mapServer.reconnectSystem("system-2");
454
+
455
+ // Re-federate to trigger flush (disconnect + federate = reconnect)
456
+ await system1.federation.disconnect("system-2");
457
+ await system1.federation.federate({
458
+ systemId: "system-2",
459
+ url: "ws://mock-server/system-2",
460
+ });
461
+
462
+ // The flush handler re-routes queued messages synchronously.
463
+ // Wait a tick for the async route() calls to resolve.
464
+ await new Promise((r) => setTimeout(r, 10));
465
+
466
+ // Queue should be drained
467
+ expect(system1.federation.queue.size("system-2")).toBe(0);
468
+
469
+ // Bob should now have the message
470
+ const bobInbox = system2.storage.getInbox("bob");
471
+ expect(bobInbox).toHaveLength(1);
472
+ expect(bobInbox[0].content).toEqual({
473
+ type: "text",
474
+ text: "Are you there?",
475
+ });
476
+ });
477
+ });
478
+
479
+ describe("broadcast strategy with real transport", () => {
480
+ // For system-qualified addresses (bob@system-2) where the target system IS a
481
+ // connected peer, delivery goes directly via transport (no broadcast needed).
482
+ // Broadcast/hierarchical strategies activate when the resolved peer has no
483
+ // active link — e.g., targeting a system we're not directly connected to.
484
+ //
485
+ // These tests verify broadcast transport by:
486
+ // 1. Testing system-qualified delivery to multiple peers (direct transport)
487
+ // 2. Verifying the broadcast event + transport work together on crafted messages
488
+
489
+ let system3: TestSystem;
490
+
491
+ afterEach(async () => {
492
+ if (system3) await system3.federation.destroy();
493
+ });
494
+
495
+ it("should deliver to multiple peers via real transport (fan-out pattern)", async () => {
496
+ const sdk = mapServer.createSdkClass();
497
+
498
+ system1 = createTestSystem("system-1", sdk, {
499
+ allowedServers: ["system-2", "system-3"],
500
+ });
501
+ system2 = createTestSystem("system-2", sdk, {
502
+ allowedServers: ["system-1"],
503
+ });
504
+ system3 = createTestSystem("system-3", sdk, {
505
+ allowedServers: ["system-1"],
506
+ });
507
+
508
+ // Register agents on each system
509
+ system1.storage.putAgent({
510
+ agent_id: "alice",
511
+ display_name: "Alice",
512
+ scope: "default",
513
+ status: "active",
514
+ metadata: {},
515
+ registered_at: new Date().toISOString(),
516
+ last_active_at: new Date().toISOString(),
517
+ });
518
+ system2.storage.putAgent({
519
+ agent_id: "bob",
520
+ display_name: "Bob",
521
+ scope: "default",
522
+ status: "active",
523
+ metadata: {},
524
+ registered_at: new Date().toISOString(),
525
+ last_active_at: new Date().toISOString(),
526
+ });
527
+ system3.storage.putAgent({
528
+ agent_id: "charlie",
529
+ display_name: "Charlie",
530
+ scope: "default",
531
+ status: "active",
532
+ metadata: {},
533
+ registered_at: new Date().toISOString(),
534
+ last_active_at: new Date().toISOString(),
535
+ });
536
+
537
+ // Federate all systems
538
+ await system1.federation.federate({
539
+ systemId: "system-2",
540
+ url: "ws://mock-server/system-2",
541
+ });
542
+ await system1.federation.federate({
543
+ systemId: "system-3",
544
+ url: "ws://mock-server/system-3",
545
+ });
546
+ await system2.federation.federate({
547
+ systemId: "system-1",
548
+ url: "ws://mock-server/system-1",
549
+ });
550
+ await system3.federation.federate({
551
+ systemId: "system-1",
552
+ url: "ws://mock-server/system-1",
553
+ });
554
+
555
+ // Send two messages: one to each remote system
556
+ await system1.router.routeMessage({
557
+ from: "alice",
558
+ to: "bob@system-2",
559
+ payload: "Hello Bob!",
560
+ });
561
+ await system1.router.routeMessage({
562
+ from: "alice",
563
+ to: "charlie@system-3",
564
+ payload: "Hello Charlie!",
565
+ });
566
+
567
+ // Both should have received their messages
568
+ const bobInbox = system2.storage.getInbox("bob");
569
+ expect(bobInbox).toHaveLength(1);
570
+ expect(bobInbox[0].content).toEqual({
571
+ type: "text",
572
+ text: "Hello Bob!",
573
+ });
574
+
575
+ const charlieInbox = system3.storage.getInbox("charlie");
576
+ expect(charlieInbox).toHaveLength(1);
577
+ expect(charlieInbox[0].content).toEqual({
578
+ type: "text",
579
+ text: "Hello Charlie!",
580
+ });
581
+ });
582
+
583
+ it("should send to all connected peers when broadcast strategy handles unknown route", async () => {
584
+ const sdk = mapServer.createSdkClass();
585
+
586
+ // System-1 uses broadcast strategy
587
+ system1 = createTestSystem("system-1", sdk, {
588
+ allowedServers: ["system-2", "system-3"],
589
+ strategy: "broadcast",
590
+ });
591
+ system2 = createTestSystem("system-2", sdk, {
592
+ allowedServers: ["system-1"],
593
+ });
594
+ system3 = createTestSystem("system-3", sdk, {
595
+ allowedServers: ["system-1"],
596
+ });
597
+
598
+ system2.storage.putAgent({
599
+ agent_id: "bob",
600
+ display_name: "Bob",
601
+ scope: "default",
602
+ status: "active",
603
+ metadata: {},
604
+ registered_at: new Date().toISOString(),
605
+ last_active_at: new Date().toISOString(),
606
+ });
607
+
608
+ // Federate
609
+ await system1.federation.federate({
610
+ systemId: "system-2",
611
+ url: "ws://mock-server/system-2",
612
+ });
613
+ await system1.federation.federate({
614
+ systemId: "system-3",
615
+ url: "ws://mock-server/system-3",
616
+ });
617
+ await system2.federation.federate({
618
+ systemId: "system-1",
619
+ url: "ws://mock-server/system-1",
620
+ });
621
+ await system3.federation.federate({
622
+ systemId: "system-1",
623
+ url: "ws://mock-server/system-1",
624
+ });
625
+
626
+ // Craft a message that triggers handleUnknownRoute directly.
627
+ // Use a recipient with a system that resolves but has no peer link,
628
+ // which queues it. However, for broadcast to trigger, resolveRoute
629
+ // must return null — which only happens for agent-only (non-remote)
630
+ // addresses. So we test via direct route() call with a system-qualified
631
+ // address where the system IS a connected peer but the agent is NOT
632
+ // in the routing table. resolveRoute returns "system-2" (from addr.system),
633
+ // peer is connected, so it delivers directly via transport.
634
+ const msg = {
635
+ id: "broadcast-direct-1",
636
+ scope: "default",
637
+ sender_id: "alice",
638
+ recipients: [{ agent_id: "bob@system-2", kind: "to" as const }],
639
+ content: { type: "text" as const, text: "Direct via transport" },
640
+ importance: "normal" as const,
641
+ metadata: {},
642
+ created_at: new Date().toISOString(),
643
+ };
644
+
645
+ const result = await system1.federation.route(msg);
646
+ expect(result.delivered).toBe(true);
647
+ expect(result.peerId).toBe("system-2");
648
+
649
+ // Message should arrive at system-2 via real transport
650
+ const bobInbox = system2.storage.getInbox("bob");
651
+ expect(bobInbox).toHaveLength(1);
652
+ expect(bobInbox[0].content).toEqual({
653
+ type: "text",
654
+ text: "Direct via transport",
655
+ });
656
+ });
657
+ });
658
+
659
+ describe("transport identity verification", () => {
660
+ it("should connect with correct gateway metadata", async () => {
661
+ await setupTwoSystems();
662
+
663
+ // Verify system-1's connection to system-2
664
+ const sys1Conn = mapServer.connections.find(
665
+ (c) => c.systemId === "system-1" && c.peerSystemId === "system-2"
666
+ );
667
+ expect(sys1Conn).toBeDefined();
668
+ expect(sys1Conn!.name).toBe("system-1");
669
+
670
+ // Verify system-2's connection to system-1
671
+ const sys2Conn = mapServer.connections.find(
672
+ (c) => c.systemId === "system-2" && c.peerSystemId === "system-1"
673
+ );
674
+ expect(sys2Conn).toBeDefined();
675
+ expect(sys2Conn!.name).toBe("system-2");
676
+ });
677
+
678
+ it("should have 2 active connections after bilateral federation", async () => {
679
+ await setupTwoSystems();
680
+ expect(mapServer.activeConnections).toBe(2);
681
+ });
682
+ });
683
+
684
+ describe("hasTransport reflects real connections", () => {
685
+ it("should report transport status accurately", async () => {
686
+ await setupTwoSystems();
687
+
688
+ expect(system1.federation.hasTransport("system-2")).toBe(true);
689
+ expect(system2.federation.hasTransport("system-1")).toBe(true);
690
+
691
+ // After disconnect, transport should be gone
692
+ await system1.federation.disconnect("system-2");
693
+ expect(system1.federation.hasTransport("system-2")).toBe(false);
694
+
695
+ // system-2 still has its connection to system-1
696
+ expect(system2.federation.hasTransport("system-1")).toBe(true);
697
+ });
698
+ });
699
+
700
+ describe("message content fidelity", () => {
701
+ it("should preserve complex structured content across federation", async () => {
702
+ await setupTwoSystems();
703
+
704
+ const complexPayload = {
705
+ type: "structured",
706
+ blocks: [
707
+ { type: "heading", text: "Report" },
708
+ { type: "code", language: "typescript", text: "const x = 1;" },
709
+ { type: "list", items: ["a", "b", "c"] },
710
+ ],
711
+ attachments: [{ name: "data.json", size: 1024 }],
712
+ };
713
+
714
+ await system1.router.routeMessage({
715
+ from: "alice",
716
+ to: "bob@system-2",
717
+ payload: complexPayload,
718
+ });
719
+
720
+ const bobInbox = system2.storage.getInbox("bob");
721
+ expect(bobInbox).toHaveLength(1);
722
+ // Content gets wrapped by normalizeContent — check the payload arrived
723
+ expect(bobInbox[0].content).toEqual(complexPayload);
724
+ });
725
+
726
+ it("should handle string payloads (auto-wrapped to text content)", async () => {
727
+ await setupTwoSystems();
728
+
729
+ await system1.router.routeMessage({
730
+ from: "alice",
731
+ to: "bob@system-2",
732
+ payload: "Simple string message",
733
+ });
734
+
735
+ const bobInbox = system2.storage.getInbox("bob");
736
+ expect(bobInbox).toHaveLength(1);
737
+ // normalizeContent wraps strings into { type: "text", text: "..." }
738
+ expect(bobInbox[0].content).toEqual({
739
+ type: "text",
740
+ text: "Simple string message",
741
+ });
742
+ });
743
+ });
744
+ });