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,857 @@
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 { WarmRegistry } from "../../src/registry/warm-registry.js";
7
+ import {
8
+ parseAddress,
9
+ formatAddress,
10
+ isRemoteAddress,
11
+ isBroadcastAddress,
12
+ } from "../../src/federation/address.js";
13
+ import type { Agent, Message } from "../../src/types.js";
14
+ import type {
15
+ MapConnection,
16
+ MapAgentConnectionClass,
17
+ IncomingMapMessage,
18
+ } from "../../src/map/map-client.js";
19
+
20
+ function makeAgent(id: string, scope = "default"): Agent {
21
+ const now = new Date().toISOString();
22
+ return {
23
+ agent_id: id,
24
+ display_name: id,
25
+ scope,
26
+ status: "active",
27
+ metadata: {},
28
+ registered_at: now,
29
+ last_active_at: now,
30
+ };
31
+ }
32
+
33
+ /**
34
+ * Integration tests — Step 19 from PLAN.md.
35
+ *
36
+ * These tests exercise the full flow across multiple components:
37
+ * MessageRouter + ConnectionManager + RoutingEngine + DeliveryQueue +
38
+ * TrustManager + WarmRegistry + address parsing.
39
+ */
40
+ describe("Federation Integration", () => {
41
+ let storage: InMemoryStorage;
42
+ let events: EventEmitter;
43
+ let router: MessageRouter;
44
+ let federation: ConnectionManager;
45
+ let registry: WarmRegistry;
46
+
47
+ beforeEach(() => {
48
+ vi.useFakeTimers();
49
+ storage = new InMemoryStorage();
50
+ events = new EventEmitter();
51
+ router = new MessageRouter(storage, events, "default");
52
+
53
+ federation = new ConnectionManager(events, {
54
+ systemId: "system-1",
55
+ trust: {
56
+ allowedServers: ["system-2", "hub-system"],
57
+ scopePermissions: {},
58
+ requireAuth: false,
59
+ },
60
+ routing: { strategy: "table" },
61
+ });
62
+
63
+ registry = new WarmRegistry(storage, events, {
64
+ gracePeriodMs: 5000,
65
+ retainExpiredMs: 30000,
66
+ requeueOnReconnect: true,
67
+ });
68
+
69
+ router.setFederation(federation);
70
+ });
71
+
72
+ afterEach(async () => {
73
+ await federation.destroy();
74
+ registry.destroy();
75
+ vi.useRealTimers();
76
+ });
77
+
78
+ describe("1. Federated send — local agent to remote agent", () => {
79
+ it("should route a message from local agent to remote agent via federation", async () => {
80
+ // Register local sender
81
+ storage.putAgent(makeAgent("agent-a"));
82
+
83
+ // Establish federation with system-2 and expose remote agent
84
+ await federation.federate({
85
+ systemId: "system-2",
86
+ url: "ws://system-2:3001",
87
+ });
88
+ federation.routing.updateFromExposure("system-2", [
89
+ { agentId: "agent-c" },
90
+ ]);
91
+
92
+ // Track federation routing event
93
+ const routeSpy = vi.fn();
94
+ events.on("federation.route", routeSpy);
95
+
96
+ // Send message from agent-a to agent-c@system-2
97
+ const msg = await router.routeMessage({
98
+ from: "agent-a",
99
+ to: "agent-c@system-2",
100
+ payload: "Hello from system-1!",
101
+ });
102
+
103
+ expect(msg.id).toBeDefined();
104
+ expect(msg.sender_id).toBe("agent-a");
105
+ expect(msg.recipients[0].agent_id).toBe("agent-c@system-2");
106
+ // Federation route completes synchronously, so delivered_at is set
107
+ expect(msg.recipients[0].delivered_at).toBeDefined();
108
+
109
+ // The federation route event should have fired
110
+ expect(routeSpy).toHaveBeenCalledWith(
111
+ expect.objectContaining({
112
+ peerId: "system-2",
113
+ message: expect.objectContaining({ id: msg.id }),
114
+ })
115
+ );
116
+ });
117
+
118
+ it("should store the message locally even when routing remotely", async () => {
119
+ storage.putAgent(makeAgent("agent-a"));
120
+ await federation.federate({
121
+ systemId: "system-2",
122
+ url: "ws://system-2:3001",
123
+ });
124
+ federation.routing.updateFromExposure("system-2", [
125
+ { agentId: "agent-c" },
126
+ ]);
127
+
128
+ const msg = await router.routeMessage({
129
+ from: "agent-a",
130
+ to: "agent-c@system-2",
131
+ payload: "Stored locally",
132
+ });
133
+
134
+ const stored = storage.getMessage(msg.id);
135
+ expect(stored).toBeDefined();
136
+ expect(stored!.content).toEqual({ type: "text", text: "Stored locally" });
137
+ });
138
+ });
139
+
140
+ describe("2. Hub relay — hierarchical routing", () => {
141
+ it("should delegate to upstream hub for agent-only address not in routing table", async () => {
142
+ const hubFederation = new ConnectionManager(events, {
143
+ systemId: "system-1",
144
+ trust: {
145
+ allowedServers: ["hub-system"],
146
+ scopePermissions: {},
147
+ requireAuth: false,
148
+ },
149
+ routing: {
150
+ strategy: "hierarchical",
151
+ upstream: ["hub-system"],
152
+ },
153
+ });
154
+
155
+ await hubFederation.federate({
156
+ systemId: "hub-system",
157
+ url: "ws://hub:3000",
158
+ });
159
+
160
+ const routeSpy = vi.fn();
161
+ events.on("federation.route", routeSpy);
162
+
163
+ // Use agent-only address (no @system) that's not in routing table.
164
+ // resolveRoute returns null → handleUnknownRoute → hierarchical → upstream hub
165
+ const msg: Message = {
166
+ id: "msg-hub-1",
167
+ scope: "default",
168
+ sender_id: "agent-a",
169
+ recipients: [{ agent_id: "unknown-agent@hub-system", kind: "to" }],
170
+ content: { type: "text", text: "relay me" },
171
+ importance: "normal",
172
+ metadata: {},
173
+ created_at: new Date().toISOString(),
174
+ };
175
+
176
+ const result = await hubFederation.route(msg);
177
+ // Address has @hub-system so resolveRoute returns "hub-system" directly,
178
+ // and since hub-system is connected, it delivers
179
+ expect(result.delivered).toBe(true);
180
+ expect(result.peerId).toBe("hub-system");
181
+ expect(routeSpy).toHaveBeenCalledWith(
182
+ expect.objectContaining({ peerId: "hub-system" })
183
+ );
184
+
185
+ await hubFederation.destroy();
186
+ });
187
+
188
+ it("should relay via upstream hub when system-qualified address has no direct peer link", async () => {
189
+ const hubFederation = new ConnectionManager(events, {
190
+ systemId: "system-1",
191
+ trust: {
192
+ allowedServers: ["hub-system"],
193
+ scopePermissions: {},
194
+ requireAuth: false,
195
+ },
196
+ routing: {
197
+ strategy: "hierarchical",
198
+ upstream: ["hub-system"],
199
+ },
200
+ });
201
+
202
+ await hubFederation.federate({
203
+ systemId: "hub-system",
204
+ url: "ws://hub:3000",
205
+ });
206
+
207
+ // Route to far-system which has no direct peer link.
208
+ // With hierarchical strategy, falls through to upstream hub relay.
209
+ const msg: Message = {
210
+ id: "msg-hub-2",
211
+ scope: "default",
212
+ sender_id: "agent-a",
213
+ recipients: [{ agent_id: "agent-x@far-system", kind: "to" }],
214
+ content: { type: "text", text: "no direct link" },
215
+ importance: "normal",
216
+ metadata: {},
217
+ created_at: new Date().toISOString(),
218
+ };
219
+
220
+ const result = await hubFederation.route(msg);
221
+ // Hierarchical strategy delegates to upstream hub instead of queuing
222
+ expect(result.delivered).toBe(true);
223
+ expect(result.peerId).toBe("hub-system");
224
+
225
+ await hubFederation.destroy();
226
+ });
227
+
228
+ it("should queue when system-qualified address has no peer link (table strategy)", async () => {
229
+ const tableFederation = new ConnectionManager(events, {
230
+ systemId: "system-1",
231
+ trust: {
232
+ allowedServers: [],
233
+ scopePermissions: {},
234
+ requireAuth: false,
235
+ },
236
+ routing: {
237
+ strategy: "table",
238
+ },
239
+ });
240
+
241
+ // No peers connected — route to far-system queues immediately
242
+ const msg: Message = {
243
+ id: "msg-table-1",
244
+ scope: "default",
245
+ sender_id: "agent-a",
246
+ recipients: [{ agent_id: "agent-x@far-system", kind: "to" }],
247
+ content: { type: "text", text: "no direct link" },
248
+ importance: "normal",
249
+ metadata: {},
250
+ created_at: new Date().toISOString(),
251
+ };
252
+
253
+ const result = await tableFederation.route(msg);
254
+ expect(result.delivered).toBe(false);
255
+ expect(result.queued).toBe(true);
256
+ expect(result.peerId).toBe("far-system");
257
+
258
+ await tableFederation.destroy();
259
+ });
260
+ });
261
+
262
+ describe("3. Warm registry — disconnect, queue, reconnect, flush", () => {
263
+ it("should queue messages for away agent and flush on reconnect", async () => {
264
+ // Register agent-b via warm registry
265
+ registry.register(makeAgent("agent-b"));
266
+ expect(registry.getStatus("agent-b")).toBe("active");
267
+
268
+ // Agent disconnects — goes to "away"
269
+ registry.disconnect("agent-b");
270
+ expect(registry.getStatus("agent-b")).toBe("away");
271
+ expect(registry.isRoutable("agent-b")).toBe(true);
272
+
273
+ // Agent reconnects before grace period
274
+ registry.reconnect("agent-b");
275
+ expect(registry.getStatus("agent-b")).toBe("active");
276
+
277
+ // Verify it didn't expire
278
+ vi.advanceTimersByTime(6000);
279
+ expect(registry.getStatus("agent-b")).toBe("active");
280
+ });
281
+
282
+ it("should expire agent after grace period", () => {
283
+ registry.register(makeAgent("agent-b"));
284
+ registry.disconnect("agent-b");
285
+
286
+ vi.advanceTimersByTime(5001);
287
+ expect(registry.getStatus("agent-b")).toBe("expired");
288
+ expect(registry.isRoutable("agent-b")).toBe(false);
289
+ });
290
+ });
291
+
292
+ describe("4. Offline peer — messages queue and flush on reconnect", () => {
293
+ it("should queue messages when peer goes offline and flush on reconnect", async () => {
294
+ // Federate then disconnect peer
295
+ await federation.federate({
296
+ systemId: "system-2",
297
+ url: "ws://system-2:3001",
298
+ });
299
+ federation.routing.updateFromExposure("system-2", [
300
+ { agentId: "agent-c" },
301
+ ]);
302
+ await federation.disconnect("system-2");
303
+
304
+ // Send message to offline peer — should be queued
305
+ const msg: Message = {
306
+ id: "msg-q-1",
307
+ scope: "default",
308
+ sender_id: "agent-a",
309
+ recipients: [{ agent_id: "agent-c@system-2", kind: "to" }],
310
+ content: { type: "text", text: "you're offline" },
311
+ importance: "normal",
312
+ metadata: {},
313
+ created_at: new Date().toISOString(),
314
+ };
315
+
316
+ const result = await federation.route(msg);
317
+ expect(result.delivered).toBe(false);
318
+ expect(result.queued).toBe(true);
319
+ expect(federation.queue.size("system-2")).toBe(1);
320
+
321
+ // Peer reconnects — queue should flush
322
+ const routeSpy = vi.fn();
323
+ events.on("federation.route", routeSpy);
324
+ events.on("federation.flushing", (data) => {
325
+ expect(data.count).toBe(1);
326
+ });
327
+
328
+ await federation.federate({
329
+ systemId: "system-2",
330
+ url: "ws://system-2:3001",
331
+ });
332
+
333
+ // Queue should be drained
334
+ expect(federation.queue.size("system-2")).toBe(0);
335
+ });
336
+ });
337
+
338
+ describe("5. Trust enforcement — unknown system rejected", () => {
339
+ it("should reject federation from unknown system", async () => {
340
+ await expect(
341
+ federation.federate({
342
+ systemId: "evil-system",
343
+ url: "ws://evil:6666",
344
+ })
345
+ ).rejects.toThrow("Federation denied");
346
+
347
+ expect(federation.isConnected("evil-system")).toBe(false);
348
+ });
349
+
350
+ it("should allow federation from approved systems", async () => {
351
+ const link = await federation.federate({
352
+ systemId: "system-2",
353
+ url: "ws://system-2:3001",
354
+ });
355
+ expect(link.status).toBe("connected");
356
+ });
357
+
358
+ it("should deny routing when scope trust fails", async () => {
359
+ const restrictedFederation = new ConnectionManager(events, {
360
+ systemId: "system-1",
361
+ trust: {
362
+ allowedServers: ["system-2"],
363
+ scopePermissions: { "system-2": ["allowed-scope"] },
364
+ requireAuth: false,
365
+ },
366
+ });
367
+
368
+ await restrictedFederation.federate({
369
+ systemId: "system-2",
370
+ url: "ws://system-2:3001",
371
+ });
372
+ restrictedFederation.routing.updateFromExposure("system-2", [
373
+ { agentId: "agent-x" },
374
+ ]);
375
+
376
+ // Scope trust is currently a stub (always passes), so this verifies
377
+ // the trust check is called in the route path
378
+ const msg: Message = {
379
+ id: "msg-trust-1",
380
+ scope: "default",
381
+ sender_id: "agent-a",
382
+ recipients: [{ agent_id: "agent-x@system-2", kind: "to" }],
383
+ content: { type: "text", text: "test" },
384
+ importance: "normal",
385
+ metadata: {},
386
+ created_at: new Date().toISOString(),
387
+ };
388
+
389
+ const result = await restrictedFederation.route(msg);
390
+ // Stub always allows, so delivery succeeds
391
+ expect(result.delivered).toBe(true);
392
+
393
+ await restrictedFederation.destroy();
394
+ });
395
+ });
396
+
397
+ describe("6. Routing strategies", () => {
398
+ // NOTE: In the current implementation, route() only processes recipients
399
+ // where isRemoteAddress() is true (i.e., address contains @system).
400
+ // resolveRoute() returns addr.system directly for system-qualified addresses.
401
+ // This means handleUnknownRoute (broadcast/hierarchical) is only reachable
402
+ // via direct calls or future changes to support agent-only federation routing.
403
+ // These tests verify the actual routing behavior through ConnectionManager.
404
+
405
+ it("table strategy: system-qualified address delivers to connected peer", async () => {
406
+ await federation.federate({
407
+ systemId: "system-2",
408
+ url: "ws://system-2:3001",
409
+ });
410
+
411
+ const routeSpy = vi.fn();
412
+ events.on("federation.route", routeSpy);
413
+
414
+ const msg: Message = {
415
+ id: "msg-direct-1",
416
+ scope: "default",
417
+ sender_id: "agent-a",
418
+ recipients: [{ agent_id: "unknown-agent@system-2", kind: "to" }],
419
+ content: { type: "text", text: "direct to system" },
420
+ importance: "normal",
421
+ metadata: {},
422
+ created_at: new Date().toISOString(),
423
+ };
424
+
425
+ const result = await federation.route(msg);
426
+ expect(result.delivered).toBe(true);
427
+ expect(result.peerId).toBe("system-2");
428
+ expect(routeSpy).toHaveBeenCalled();
429
+ });
430
+
431
+ it("table strategy: system-qualified address queues when peer offline", async () => {
432
+ await federation.federate({
433
+ systemId: "system-2",
434
+ url: "ws://system-2:3001",
435
+ });
436
+ await federation.disconnect("system-2");
437
+
438
+ const msg: Message = {
439
+ id: "msg-miss-1",
440
+ scope: "default",
441
+ sender_id: "agent-a",
442
+ recipients: [{ agent_id: "agent-c@system-2", kind: "to" }],
443
+ content: { type: "text", text: "peer offline" },
444
+ importance: "normal",
445
+ metadata: {},
446
+ created_at: new Date().toISOString(),
447
+ };
448
+
449
+ const result = await federation.route(msg);
450
+ expect(result.delivered).toBe(false);
451
+ expect(result.queued).toBe(true);
452
+ expect(result.peerId).toBe("system-2");
453
+ });
454
+
455
+ it("agent-only addresses are not treated as remote", async () => {
456
+ const msg: Message = {
457
+ id: "msg-local-1",
458
+ scope: "default",
459
+ sender_id: "agent-a",
460
+ recipients: [{ agent_id: "local-agent", kind: "to" }],
461
+ content: { type: "text", text: "local only" },
462
+ importance: "normal",
463
+ metadata: {},
464
+ created_at: new Date().toISOString(),
465
+ };
466
+
467
+ const result = await federation.route(msg);
468
+ expect(result.delivered).toBe(false);
469
+ expect(result.error).toContain("No remote recipients");
470
+ });
471
+
472
+ it("routing engine resolveRoute uses table for agent-only lookups", () => {
473
+ federation.routing.updateFromExposure("system-2", [
474
+ { agentId: "known-agent" },
475
+ ]);
476
+ expect(federation.routing.resolveRoute({ agent: "known-agent" })).toBe(
477
+ "system-2"
478
+ );
479
+ expect(
480
+ federation.routing.resolveRoute({ agent: "unknown-agent" })
481
+ ).toBeNull();
482
+ });
483
+
484
+ it("routing engine strategy accessors work correctly", () => {
485
+ expect(federation.routing.getStrategy()).toBe("table");
486
+
487
+ const broadcastCm = new ConnectionManager(events, {
488
+ systemId: "s",
489
+ routing: { strategy: "broadcast", broadcastTimeout: 3000 },
490
+ });
491
+ expect(broadcastCm.routing.getStrategy()).toBe("broadcast");
492
+ expect(broadcastCm.routing.getBroadcastTimeout()).toBe(3000);
493
+ broadcastCm.destroy();
494
+
495
+ const hierCm = new ConnectionManager(events, {
496
+ systemId: "s",
497
+ routing: { strategy: "hierarchical", upstream: ["hub-1"] },
498
+ });
499
+ expect(hierCm.routing.getStrategy()).toBe("hierarchical");
500
+ expect(hierCm.routing.getUpstream()).toEqual(["hub-1"]);
501
+ hierCm.destroy();
502
+ });
503
+
504
+ it("routing table entries expire based on TTL", () => {
505
+ const shortTtlCm = new ConnectionManager(events, {
506
+ systemId: "s",
507
+ routing: { strategy: "table", tableTTL: 100 },
508
+ });
509
+
510
+ shortTtlCm.routing.updateFromExposure("peer-1", [
511
+ { agentId: "temp-agent" },
512
+ ]);
513
+ expect(shortTtlCm.routing.lookupAgent("temp-agent")).toBe("peer-1");
514
+
515
+ // Advance past TTL
516
+ vi.advanceTimersByTime(150);
517
+ expect(shortTtlCm.routing.lookupAgent("temp-agent")).toBeNull();
518
+
519
+ shortTtlCm.destroy();
520
+ });
521
+ });
522
+
523
+ describe("7. Address parsing — all formats resolve correctly", () => {
524
+ it("local address", () => {
525
+ const addr = parseAddress("agent-beta");
526
+ expect(addr).toEqual({ agent: "agent-beta" });
527
+ expect(isRemoteAddress(addr)).toBe(false);
528
+ expect(isBroadcastAddress(addr)).toBe(false);
529
+ expect(formatAddress(addr)).toBe("agent-beta");
530
+ });
531
+
532
+ it("federated address", () => {
533
+ const addr = parseAddress("agent-beta@backend-team");
534
+ expect(addr).toEqual({ agent: "agent-beta", system: "backend-team" });
535
+ expect(isRemoteAddress(addr)).toBe(true);
536
+ expect(isBroadcastAddress(addr)).toBe(false);
537
+ expect(formatAddress(addr)).toBe("agent-beta@backend-team");
538
+ });
539
+
540
+ it("broadcast to system", () => {
541
+ const addr = parseAddress("@backend-team");
542
+ expect(addr).toEqual({ system: "backend-team" });
543
+ expect(isRemoteAddress(addr)).toBe(true);
544
+ expect(isBroadcastAddress(addr)).toBe(true);
545
+ expect(formatAddress(addr)).toBe("@backend-team");
546
+ });
547
+
548
+ it("scoped federated address", () => {
549
+ const addr = parseAddress("agent-beta@backend-team/engineering");
550
+ expect(addr).toEqual({
551
+ agent: "agent-beta",
552
+ system: "backend-team",
553
+ scope: "engineering",
554
+ });
555
+ expect(isRemoteAddress(addr)).toBe(true);
556
+ expect(formatAddress(addr)).toBe("agent-beta@backend-team/engineering");
557
+ });
558
+
559
+ it("round-trip all formats", () => {
560
+ const addresses = [
561
+ "agent-1",
562
+ "agent-1@system-2",
563
+ "@system-2",
564
+ "agent-1@system-2/scope-a",
565
+ ];
566
+ for (const original of addresses) {
567
+ expect(formatAddress(parseAddress(original))).toBe(original);
568
+ }
569
+ });
570
+ });
571
+
572
+ describe("8. System ID resolution", () => {
573
+ it("explicit config takes highest precedence", () => {
574
+ const cm = new ConnectionManager(events, { systemId: "explicit-id" });
575
+ expect(cm.getSystemId()).toEqual({ id: "explicit-id", source: "config" });
576
+ cm.destroy();
577
+ });
578
+
579
+ it("MAP systemInfo is tier 2 (overrides auto, not config)", () => {
580
+ const cm = new ConnectionManager(events, {}); // auto-generated
581
+ expect(cm.getSystemId().source).toBe("auto");
582
+
583
+ cm.updateSystemIdFromMap("map-derived");
584
+ expect(cm.getSystemId()).toEqual({ id: "map-derived", source: "map" });
585
+ cm.destroy();
586
+ });
587
+
588
+ it("explicit config cannot be overridden by MAP", () => {
589
+ const cm = new ConnectionManager(events, { systemId: "explicit-id" });
590
+ cm.updateSystemIdFromMap("map-derived");
591
+ expect(cm.getSystemId()).toEqual({ id: "explicit-id", source: "config" });
592
+ cm.destroy();
593
+ });
594
+
595
+ it("auto-generated ID is stable format", () => {
596
+ const cm = new ConnectionManager(events, {});
597
+ const sid = cm.getSystemId();
598
+ expect(sid.source).toBe("auto");
599
+ expect(sid.id).toMatch(/^inbox-[0-9a-f]{8}$/);
600
+ cm.destroy();
601
+ });
602
+ });
603
+
604
+ describe("End-to-end: full message lifecycle with federation", () => {
605
+ it("should handle send → store → route → queue → flush cycle", async () => {
606
+ // Setup: local agent + federated peer
607
+ storage.putAgent(makeAgent("agent-a"));
608
+ registry.register(makeAgent("agent-a"));
609
+
610
+ await federation.federate({
611
+ systemId: "system-2",
612
+ url: "ws://system-2:3001",
613
+ });
614
+ federation.routing.updateFromExposure("system-2", [
615
+ { agentId: "agent-c" },
616
+ ]);
617
+
618
+ // Step 1: Send a message — should route via federation
619
+ const msg1 = await router.routeMessage({
620
+ from: "agent-a",
621
+ to: "agent-c@system-2",
622
+ payload: "Hello agent-c!",
623
+ subject: "Greeting",
624
+ });
625
+ expect(storage.getMessage(msg1.id)).toBeDefined();
626
+
627
+ // Step 2: Peer goes offline — next message gets queued
628
+ await federation.disconnect("system-2");
629
+
630
+ // Direct route (not via router, to test queue)
631
+ const msg2: Message = {
632
+ id: "msg-e2e-2",
633
+ scope: "default",
634
+ sender_id: "agent-a",
635
+ recipients: [{ agent_id: "agent-c@system-2", kind: "to" }],
636
+ content: { type: "text", text: "Are you there?" },
637
+ importance: "normal",
638
+ metadata: {},
639
+ created_at: new Date().toISOString(),
640
+ };
641
+ const result2 = await federation.route(msg2);
642
+ expect(result2.queued).toBe(true);
643
+ expect(federation.queue.size("system-2")).toBe(1);
644
+
645
+ // Step 3: Peer comes back — queue flushes
646
+ const flushSpy = vi.fn();
647
+ events.on("federation.flushing", flushSpy);
648
+
649
+ await federation.federate({
650
+ systemId: "system-2",
651
+ url: "ws://system-2:3001",
652
+ });
653
+
654
+ expect(flushSpy).toHaveBeenCalledWith(
655
+ expect.objectContaining({ peerId: "system-2", count: 1 })
656
+ );
657
+ expect(federation.queue.size("system-2")).toBe(0);
658
+ });
659
+
660
+ it("should handle incoming federation message and store with correct recipient", async () => {
661
+ // Setup: local agent that should receive the message
662
+ storage.putAgent(makeAgent("agent-b"));
663
+
664
+ // Create federation with transport mock that captures onMessage handlers
665
+ const peerMessageHandlers: Array<(msg: IncomingMapMessage) => void> = [];
666
+ const sentMessages: Array<{
667
+ to: { agentId?: string; scope?: string };
668
+ payload: unknown;
669
+ meta?: Record<string, unknown>;
670
+ }> = [];
671
+
672
+ const mockSdkClass: MapAgentConnectionClass = {
673
+ connect: async () => ({
674
+ send: async (to, payload, meta) => {
675
+ sentMessages.push({ to, payload, meta });
676
+ },
677
+ onMessage: (handler: (msg: IncomingMapMessage) => void) => {
678
+ peerMessageHandlers.push(handler);
679
+ },
680
+ disconnect: async () => {},
681
+ }),
682
+ };
683
+
684
+ // Create federation with real transport + incoming message handler wired to router
685
+ const transportFederation = new ConnectionManager(
686
+ events,
687
+ {
688
+ systemId: "system-1",
689
+ trust: {
690
+ allowedServers: ["system-2"],
691
+ scopePermissions: {},
692
+ requireAuth: false,
693
+ },
694
+ },
695
+ {
696
+ sdkClass: mockSdkClass,
697
+ onIncomingMessage: ({ from, peerId, payload, meta }) => {
698
+ const targetAgent = (meta?.targetAgent as string) ?? from;
699
+ router.routeMessage({
700
+ from: `${from}@${peerId}`,
701
+ to: targetAgent,
702
+ payload,
703
+ subject: meta?.subject as string | undefined,
704
+ importance: meta?.importance as "high" | "normal" | undefined,
705
+ metadata: meta,
706
+ });
707
+ },
708
+ }
709
+ );
710
+ router.setFederation(transportFederation);
711
+
712
+ await transportFederation.federate({
713
+ systemId: "system-2",
714
+ url: "ws://system-2:3001",
715
+ });
716
+
717
+ // Simulate peer sending a message targeting local agent-b
718
+ expect(peerMessageHandlers).toHaveLength(1);
719
+ peerMessageHandlers[0]({
720
+ from: "agent-c",
721
+ payload: { type: "text", text: "Hello from system-2!" },
722
+ meta: {
723
+ targetAgent: "agent-b",
724
+ senderId: "agent-c",
725
+ sourceSystem: "system-2",
726
+ subject: "Cross-system greeting",
727
+ importance: "high",
728
+ },
729
+ });
730
+
731
+ // Wait for async routeMessage to complete
732
+ await vi.advanceTimersByTimeAsync(0);
733
+
734
+ // Verify the message landed in agent-b's inbox
735
+ const inbox = storage.getInbox("agent-b");
736
+ expect(inbox).toHaveLength(1);
737
+ expect(inbox[0].sender_id).toBe("agent-c@system-2");
738
+ expect(inbox[0].content).toEqual({
739
+ type: "text",
740
+ text: "Hello from system-2!",
741
+ });
742
+ expect(inbox[0].subject).toBe("Cross-system greeting");
743
+ expect(inbox[0].importance).toBe("high");
744
+
745
+ // Verify recipient is agent-b and is marked delivered
746
+ expect(inbox[0].recipients[0].agent_id).toBe("agent-b");
747
+ expect(inbox[0].recipients[0].delivered_at).toBeDefined();
748
+
749
+ await transportFederation.destroy();
750
+ });
751
+
752
+ it("should round-trip: send outbound with meta, receive inbound with meta", async () => {
753
+ storage.putAgent(makeAgent("agent-a"));
754
+ storage.putAgent(makeAgent("agent-b"));
755
+
756
+ const sentMessages: Array<{
757
+ to: { agentId?: string; scope?: string };
758
+ payload: unknown;
759
+ meta?: Record<string, unknown>;
760
+ }> = [];
761
+
762
+ const mockSdkClass: MapAgentConnectionClass = {
763
+ connect: async () => ({
764
+ send: async (to, payload, meta) => {
765
+ sentMessages.push({ to, payload, meta });
766
+ },
767
+ onMessage: () => {},
768
+ disconnect: async () => {},
769
+ }),
770
+ };
771
+
772
+ const transportFederation = new ConnectionManager(
773
+ events,
774
+ {
775
+ systemId: "system-1",
776
+ trust: {
777
+ allowedServers: ["system-2"],
778
+ scopePermissions: {},
779
+ requireAuth: false,
780
+ },
781
+ },
782
+ { sdkClass: mockSdkClass }
783
+ );
784
+ router.setFederation(transportFederation);
785
+
786
+ await transportFederation.federate({
787
+ systemId: "system-2",
788
+ url: "ws://system-2:3001",
789
+ });
790
+ transportFederation.routing.updateFromExposure("system-2", [
791
+ { agentId: "agent-c" },
792
+ ]);
793
+
794
+ // Send a message with subject and thread
795
+ await router.routeMessage({
796
+ from: "agent-a",
797
+ to: "agent-c@system-2",
798
+ payload: "Thread message",
799
+ subject: "Important thread",
800
+ threadTag: "thread-1",
801
+ importance: "high",
802
+ });
803
+
804
+ // Verify the outbound meta includes targetAgent and all fields
805
+ expect(sentMessages).toHaveLength(1);
806
+ expect(sentMessages[0].to.agentId).toBe("agent-c");
807
+ expect(sentMessages[0].meta?.targetAgent).toBe("agent-c");
808
+ expect(sentMessages[0].meta?.senderId).toBe("agent-a");
809
+ expect(sentMessages[0].meta?.sourceSystem).toBe("system-1");
810
+ expect(sentMessages[0].meta?.subject).toBe("Important thread");
811
+ expect(sentMessages[0].meta?.threadTag).toBe("thread-1");
812
+ expect(sentMessages[0].meta?.importance).toBe("high");
813
+
814
+ await transportFederation.destroy();
815
+ });
816
+
817
+ it("should handle mixed local and remote recipients", async () => {
818
+ storage.putAgent(makeAgent("agent-a"));
819
+ storage.putAgent(makeAgent("agent-b"));
820
+
821
+ await federation.federate({
822
+ systemId: "system-2",
823
+ url: "ws://system-2:3001",
824
+ });
825
+ federation.routing.updateFromExposure("system-2", [
826
+ { agentId: "agent-c" },
827
+ ]);
828
+
829
+ const routeSpy = vi.fn();
830
+ events.on("federation.route", routeSpy);
831
+
832
+ const msg = await router.routeMessage({
833
+ from: "agent-a",
834
+ to: [
835
+ { agent_id: "agent-b", kind: "to" },
836
+ { agent_id: "agent-c@system-2", kind: "cc" },
837
+ ],
838
+ payload: "Hello everyone",
839
+ });
840
+
841
+ // Local recipient gets delivered_at
842
+ const localRecipient = msg.recipients.find(
843
+ (r) => r.agent_id === "agent-b"
844
+ );
845
+ expect(localRecipient?.delivered_at).toBeDefined();
846
+
847
+ // Remote recipient gets delivered_at from federation route (synchronous)
848
+ const remoteRecipient = msg.recipients.find(
849
+ (r) => r.agent_id === "agent-c@system-2"
850
+ );
851
+ expect(remoteRecipient?.delivered_at).toBeDefined();
852
+
853
+ // Federation route event should have fired for the remote recipient
854
+ expect(routeSpy).toHaveBeenCalled();
855
+ });
856
+ });
857
+ });