mono-pilot 0.2.9 → 0.2.12

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 (158) hide show
  1. package/README.md +270 -7
  2. package/dist/src/agents-paths.js +36 -0
  3. package/dist/src/brief/blocks.js +83 -0
  4. package/dist/src/brief/defaults.js +60 -0
  5. package/dist/src/brief/frontmatter.js +53 -0
  6. package/dist/src/brief/paths.js +10 -0
  7. package/dist/src/brief/reflection.js +27 -0
  8. package/dist/src/cli.js +62 -5
  9. package/dist/src/cluster/bus.js +102 -0
  10. package/dist/src/cluster/follower.js +137 -0
  11. package/dist/src/cluster/init.js +182 -0
  12. package/dist/src/cluster/leader.js +97 -0
  13. package/dist/src/cluster/log.js +49 -0
  14. package/dist/src/cluster/protocol.js +34 -0
  15. package/dist/src/cluster/services/bus.js +243 -0
  16. package/dist/src/cluster/services/embedding.js +12 -0
  17. package/dist/src/cluster/socket.js +86 -0
  18. package/dist/src/cluster/test-bus.js +175 -0
  19. package/dist/src/cluster_v2/connection-lifecycle.js +31 -0
  20. package/dist/src/cluster_v2/connection-lifecycle.test.js +24 -0
  21. package/dist/src/cluster_v2/connection.js +159 -0
  22. package/dist/src/cluster_v2/connection.test.js +55 -0
  23. package/dist/src/cluster_v2/events.js +102 -0
  24. package/dist/src/cluster_v2/index.js +2 -0
  25. package/dist/src/cluster_v2/observability.js +99 -0
  26. package/dist/src/cluster_v2/observability.test.js +46 -0
  27. package/dist/src/cluster_v2/rpc.js +389 -0
  28. package/dist/src/cluster_v2/rpc.test.js +110 -0
  29. package/dist/src/cluster_v2/runtime.failover.integration.test.js +156 -0
  30. package/dist/src/cluster_v2/runtime.js +531 -0
  31. package/dist/src/cluster_v2/runtime.lease-compromise.integration.test.js +91 -0
  32. package/dist/src/cluster_v2/runtime.lifecycle.integration.test.js +225 -0
  33. package/dist/src/cluster_v2/services/bus.integration.test.js +140 -0
  34. package/dist/src/cluster_v2/services/bus.js +450 -0
  35. package/dist/src/cluster_v2/services/discord/auth-store.js +82 -0
  36. package/dist/src/cluster_v2/services/discord/collector.js +569 -0
  37. package/dist/src/cluster_v2/services/discord/index.js +1 -0
  38. package/dist/src/cluster_v2/services/discord/oauth.js +87 -0
  39. package/dist/src/cluster_v2/services/discord/rpc-client.js +325 -0
  40. package/dist/src/cluster_v2/services/embedding.js +66 -0
  41. package/dist/src/cluster_v2/services/registry-cache.js +107 -0
  42. package/dist/src/cluster_v2/services/registry-cache.test.js +66 -0
  43. package/dist/src/cluster_v2/services/registry.js +36 -0
  44. package/dist/src/cluster_v2/services/twitter/collector.js +1055 -0
  45. package/dist/src/cluster_v2/services/twitter/index.js +1 -0
  46. package/dist/src/config/digest.js +78 -0
  47. package/dist/src/config/discord.js +143 -0
  48. package/dist/src/config/image-gen.js +48 -0
  49. package/dist/src/config/mono-pilot.js +31 -0
  50. package/dist/src/config/twitter.js +100 -0
  51. package/dist/src/extensions/cluster.js +311 -0
  52. package/dist/src/extensions/commands/build-memory.js +76 -0
  53. package/dist/src/extensions/commands/digest/backfill.js +779 -0
  54. package/dist/src/extensions/commands/digest/index.js +1133 -0
  55. package/dist/src/extensions/commands/image-model.js +214 -0
  56. package/dist/src/extensions/game/bus-injection.js +47 -0
  57. package/dist/src/extensions/game/identity.js +83 -0
  58. package/dist/src/extensions/game/mailbox.js +61 -0
  59. package/dist/src/extensions/game/system-prompt.js +134 -0
  60. package/dist/src/extensions/game/tools.js +28 -0
  61. package/dist/src/extensions/lifecycle.js +337 -0
  62. package/dist/src/extensions/mode-runtime.js +26 -2
  63. package/dist/src/extensions/mono-game.js +66 -0
  64. package/dist/src/extensions/mono-pilot.js +100 -18
  65. package/dist/src/extensions/nvim.js +47 -0
  66. package/dist/src/extensions/session-hints.js +60 -35
  67. package/dist/src/extensions/sftp.js +897 -0
  68. package/dist/src/extensions/status.js +676 -0
  69. package/dist/src/extensions/system-events.js +478 -0
  70. package/dist/src/extensions/system-prompt.js +24 -14
  71. package/dist/src/extensions/user-message.js +94 -50
  72. package/dist/src/lsp/client.js +235 -0
  73. package/dist/src/lsp/index.js +165 -0
  74. package/dist/src/lsp/runtime.js +67 -0
  75. package/dist/src/lsp/server.js +242 -0
  76. package/dist/src/mcp/config.js +112 -0
  77. package/dist/src/{utils/mcp-client.js → mcp/protocol.js} +1 -100
  78. package/dist/src/mcp/servers.js +90 -0
  79. package/dist/src/memory/build-memory.js +103 -0
  80. package/dist/src/memory/config/defaults.js +55 -0
  81. package/dist/src/memory/config/loader.js +29 -0
  82. package/dist/src/memory/config/paths.js +9 -0
  83. package/dist/src/memory/config/resolve.js +90 -0
  84. package/dist/src/memory/config/types.js +1 -0
  85. package/dist/src/memory/embeddings/batch-runner.js +39 -0
  86. package/dist/src/memory/embeddings/cache.js +47 -0
  87. package/dist/src/memory/embeddings/chunk-limits.js +26 -0
  88. package/dist/src/memory/embeddings/input-limits.js +48 -0
  89. package/dist/src/memory/embeddings/local.js +108 -0
  90. package/dist/src/memory/embeddings/types.js +1 -0
  91. package/dist/src/memory/index-manager.js +552 -0
  92. package/dist/src/memory/indexing/embeddings.js +67 -0
  93. package/dist/src/memory/indexing/files.js +180 -0
  94. package/dist/src/memory/indexing/index-file.js +105 -0
  95. package/dist/src/memory/log.js +38 -0
  96. package/dist/src/memory/paths.js +15 -0
  97. package/dist/src/memory/runtime/index.js +299 -0
  98. package/dist/src/memory/runtime/thread.js +116 -0
  99. package/dist/src/memory/search/fts.js +57 -0
  100. package/dist/src/memory/search/hybrid.js +50 -0
  101. package/dist/src/memory/search/text.js +30 -0
  102. package/dist/src/memory/search/vector.js +43 -0
  103. package/dist/src/memory/session/content-hash.js +7 -0
  104. package/dist/src/memory/session/entry.js +33 -0
  105. package/dist/src/memory/session/flush-policy.js +34 -0
  106. package/dist/src/memory/session/hook.js +191 -0
  107. package/dist/src/memory/session/paths.js +15 -0
  108. package/dist/src/memory/session/session-reader.js +88 -0
  109. package/dist/src/memory/session/transcript/content-hash.js +7 -0
  110. package/dist/src/memory/session/transcript/entry.js +28 -0
  111. package/dist/src/memory/session/transcript/flush.js +56 -0
  112. package/dist/src/memory/session/transcript/paths.js +28 -0
  113. package/dist/src/memory/session/transcript/reader.js +112 -0
  114. package/dist/src/memory/session/transcript/state.js +31 -0
  115. package/dist/src/memory/store/schema.js +89 -0
  116. package/dist/src/memory/store/sqlite.js +89 -0
  117. package/dist/src/memory/types.js +1 -0
  118. package/dist/src/memory/warm.js +25 -0
  119. package/dist/src/rules/discovery.js +41 -0
  120. package/dist/{tools → src/tools}/README.md +29 -3
  121. package/dist/{tools → src/tools}/apply-patch-description.md +8 -2
  122. package/dist/{tools → src/tools}/apply-patch.js +174 -104
  123. package/dist/{tools → src/tools}/apply-patch.test.js +52 -1
  124. package/dist/{tools/ask-question.js → src/tools/ask-user-question.js} +3 -3
  125. package/dist/src/tools/ast-grep.js +357 -0
  126. package/dist/src/tools/brief-write.js +122 -0
  127. package/dist/src/tools/bus-send.js +100 -0
  128. package/dist/{tools → src/tools}/call-mcp-tool.js +40 -124
  129. package/dist/src/tools/codex-apply-patch-description.md +52 -0
  130. package/dist/src/tools/codex-apply-patch.js +540 -0
  131. package/dist/{tools → src/tools}/delete.js +24 -0
  132. package/dist/src/tools/exit-plan-mode.js +83 -0
  133. package/dist/{tools → src/tools}/fetch-mcp-resource.js +56 -100
  134. package/dist/src/tools/generate-image.js +567 -0
  135. package/dist/{tools → src/tools}/glob.js +55 -1
  136. package/dist/{tools → src/tools}/list-mcp-resources.js +46 -57
  137. package/dist/{tools → src/tools}/list-mcp-tools.js +52 -63
  138. package/dist/src/tools/ls.js +48 -0
  139. package/dist/src/tools/lsp-diagnostics.js +67 -0
  140. package/dist/src/tools/lsp-symbols.js +54 -0
  141. package/dist/src/tools/mailbox.js +85 -0
  142. package/dist/src/tools/memory-get.js +90 -0
  143. package/dist/src/tools/memory-search.js +180 -0
  144. package/dist/{tools → src/tools}/plan-mode-reminder.md +3 -4
  145. package/dist/{tools → src/tools}/read-file.js +8 -19
  146. package/dist/{tools → src/tools}/rg.js +10 -20
  147. package/dist/{tools → src/tools}/shell.js +19 -42
  148. package/dist/{tools → src/tools}/subagent.js +255 -6
  149. package/dist/{tools → src/tools}/switch-mode.js +37 -6
  150. package/dist/{tools → src/tools}/web-fetch.js +105 -7
  151. package/dist/{tools → src/tools}/web-search.js +29 -1
  152. package/package.json +21 -9
  153. /package/dist/{tools → src/tools}/ask-mode-reminder.md +0 -0
  154. /package/dist/{tools → src/tools}/rg.test.js +0 -0
  155. /package/dist/{tools → src/tools}/semantic-search-description.md +0 -0
  156. /package/dist/{tools → src/tools}/semantic-search.js +0 -0
  157. /package/dist/{tools → src/tools}/shell-description.md +0 -0
  158. /package/dist/{tools → src/tools}/subagent-description.md +0 -0
@@ -0,0 +1,225 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import { tryListen } from "./connection.js";
3
+ import { bindRpcConnection, } from "./rpc.js";
4
+ import { createEmbeddingHandlers } from "./services/embedding.js";
5
+ import { createBusService } from "./services/bus.js";
6
+ import { ServiceRegistry } from "./services/registry.js";
7
+ import { closeClusterV2, initClusterV2 } from "./runtime.js";
8
+ function createMockEmbeddingProvider(model) {
9
+ return {
10
+ id: "local",
11
+ model,
12
+ embedQuery: async (text) => [text.length],
13
+ embedBatch: async (texts) => texts.map((text) => [text.length]),
14
+ dispose: async () => {
15
+ // no-op in tests
16
+ },
17
+ };
18
+ }
19
+ vi.mock("../memory/embeddings/local.js", () => ({
20
+ createLocalEmbeddingProvider: async (params) => createMockEmbeddingProvider(params.modelPath ?? "mock-local"),
21
+ }));
22
+ let externalLeader = null;
23
+ afterEach(async () => {
24
+ if (externalLeader) {
25
+ await externalLeader.close();
26
+ externalLeader = null;
27
+ }
28
+ await closeClusterV2();
29
+ });
30
+ function uniqueScope(prefix) {
31
+ return `${prefix}-${process.pid}-${Date.now()}-${Math.floor(Math.random() * 100000)}`;
32
+ }
33
+ async function sleep(ms) {
34
+ await new Promise((resolve) => setTimeout(resolve, ms));
35
+ }
36
+ async function waitForRole(params, role, timeoutMs) {
37
+ const deadline = Date.now() + timeoutMs;
38
+ let last = null;
39
+ while (Date.now() < deadline) {
40
+ last = await initClusterV2(params);
41
+ if (last.role === role) {
42
+ return last;
43
+ }
44
+ await sleep(25);
45
+ }
46
+ throw new Error(`timed out waiting for role=${role}, last=${last?.role ?? "none"}`);
47
+ }
48
+ function registerDefaultServices(registry) {
49
+ const services = [
50
+ { name: "embedding", version: "v2", capabilities: { methods: ["embedding.embedBatch"] } },
51
+ {
52
+ name: "bus",
53
+ version: "v2",
54
+ capabilities: {
55
+ methods: ["bus.register", "bus.subscribe", "bus.send", "bus.broadcast", "bus.roster"],
56
+ },
57
+ },
58
+ ];
59
+ for (const service of services) {
60
+ registry.register(service);
61
+ }
62
+ }
63
+ async function startExternalLeader(scope) {
64
+ const connection = await tryListen({ scope });
65
+ if (!connection) {
66
+ throw new Error("failed to start external leader for test");
67
+ }
68
+ const registry = new ServiceRegistry();
69
+ registerDefaultServices(registry);
70
+ const busService = createBusService();
71
+ const provider = createMockEmbeddingProvider("external-leader");
72
+ const embeddingHandlers = createEmbeddingHandlers(provider);
73
+ const handlers = {
74
+ "cluster.ping": async () => "pong",
75
+ "registry.list": async () => registry.snapshot(),
76
+ "registry.resolve": async (request) => {
77
+ const name = request.params?.name;
78
+ if (!name || typeof name !== "string") {
79
+ throw new Error("registry.resolve requires string name");
80
+ }
81
+ return {
82
+ revision: registry.getRevision(),
83
+ service: registry.resolve(name),
84
+ };
85
+ },
86
+ ...embeddingHandlers,
87
+ ...busService.handlers,
88
+ };
89
+ const sockets = new Set();
90
+ connection.server.on("connection", (socket) => {
91
+ sockets.add(socket);
92
+ socket.on("close", () => {
93
+ sockets.delete(socket);
94
+ });
95
+ bindRpcConnection(socket, async (request, rpcConnection) => {
96
+ const handler = handlers[request.method];
97
+ if (!handler) {
98
+ throw new Error(`unknown method: ${request.method}`);
99
+ }
100
+ return handler(request, rpcConnection);
101
+ }, {
102
+ onClose: (rpcConnection) => {
103
+ busService.onConnectionClosed(rpcConnection);
104
+ },
105
+ });
106
+ });
107
+ const bus = busService.createLeaderHandle("leader-harness", "Leader Harness");
108
+ return {
109
+ connection,
110
+ bus,
111
+ close: async () => {
112
+ for (const socket of sockets) {
113
+ socket.destroy();
114
+ }
115
+ await connection.close();
116
+ bus.close();
117
+ },
118
+ };
119
+ }
120
+ function parseLifecycleRecords(calls) {
121
+ const records = [];
122
+ for (const call of calls) {
123
+ const line = call[0];
124
+ if (typeof line !== "string" || !line.startsWith("[cluster_v2] ")) {
125
+ continue;
126
+ }
127
+ const payload = line.slice("[cluster_v2] ".length);
128
+ try {
129
+ const parsed = JSON.parse(payload);
130
+ records.push(parsed);
131
+ }
132
+ catch {
133
+ // ignore non-json log lines in tests
134
+ }
135
+ }
136
+ return records;
137
+ }
138
+ const ALLOWED_TRANSITIONS = new Set([
139
+ "disconnected->connecting",
140
+ "disconnected->closed",
141
+ "connecting->connected",
142
+ "connecting->disconnected",
143
+ "connecting->closed",
144
+ "connected->reconnecting",
145
+ "connected->disconnected",
146
+ "connected->closed",
147
+ "reconnecting->connecting",
148
+ "reconnecting->disconnected",
149
+ "reconnecting->closed",
150
+ "closed->connecting",
151
+ ]);
152
+ describe("cluster_v2 runtime lifecycle integration", () => {
153
+ it("keeps leader lifecycle transitions within the state machine", async () => {
154
+ const scope = uniqueScope("runtime-leader-lifecycle");
155
+ const infoSpy = vi.spyOn(console, "info").mockImplementation(() => {
156
+ // mute test logs
157
+ });
158
+ try {
159
+ const params = {
160
+ agentId: "leader-a",
161
+ displayName: "Leader A",
162
+ scope,
163
+ modelPath: "mock-local",
164
+ rpcTimeoutMs: 500,
165
+ getSessionId: () => "leader-session",
166
+ };
167
+ const service = await initClusterV2(params);
168
+ expect(service.role).toBe("leader");
169
+ await closeClusterV2();
170
+ const records = parseLifecycleRecords(infoSpy.mock.calls)
171
+ .filter((record) => record.event === "connection_lifecycle_transition" && record.scope === scope);
172
+ const leaderTransitions = records.filter((record) => record.role === "leader");
173
+ expect(leaderTransitions.length).toBeGreaterThanOrEqual(3);
174
+ for (const transition of leaderTransitions) {
175
+ const edge = `${transition.fromState ?? ""}->${transition.toState ?? ""}`;
176
+ expect(ALLOWED_TRANSITIONS.has(edge)).toBe(true);
177
+ }
178
+ expect(leaderTransitions.some((transition) => transition.fromState === "disconnected" && transition.toState === "connecting")).toBe(true);
179
+ expect(leaderTransitions.some((transition) => transition.fromState === "connecting" && transition.toState === "connected")).toBe(true);
180
+ expect(leaderTransitions.some((transition) => transition.fromState === "connected" &&
181
+ transition.toState === "closed" &&
182
+ transition.label?.startsWith("leader_close:"))).toBe(true);
183
+ }
184
+ finally {
185
+ infoSpy.mockRestore();
186
+ }
187
+ });
188
+ it("drives follower through reconnect lifecycle after leader loss", async () => {
189
+ const scope = uniqueScope("runtime-follower-lifecycle");
190
+ externalLeader = await startExternalLeader(scope);
191
+ const infoSpy = vi.spyOn(console, "info").mockImplementation(() => {
192
+ // mute test logs
193
+ });
194
+ try {
195
+ const params = {
196
+ agentId: "follower-a",
197
+ displayName: "Follower A",
198
+ scope,
199
+ modelPath: "mock-local",
200
+ rpcTimeoutMs: 500,
201
+ getSessionId: () => "follower-session",
202
+ };
203
+ const follower = await initClusterV2(params);
204
+ expect(follower.role).toBe("follower");
205
+ await externalLeader.close();
206
+ externalLeader = null;
207
+ await waitForRole(params, "leader", 4000);
208
+ const records = parseLifecycleRecords(infoSpy.mock.calls)
209
+ .filter((record) => record.event === "connection_lifecycle_transition" && record.scope === scope);
210
+ for (const transition of records) {
211
+ const edge = `${transition.fromState ?? ""}->${transition.toState ?? ""}`;
212
+ expect(ALLOWED_TRANSITIONS.has(edge)).toBe(true);
213
+ }
214
+ const followerTransitions = records.filter((record) => record.role === "follower");
215
+ expect(followerTransitions.some((transition) => transition.fromState === "connecting" && transition.toState === "connected")).toBe(true);
216
+ expect(followerTransitions.some((transition) => transition.fromState === "connected" &&
217
+ transition.toState === "reconnecting" &&
218
+ transition.label === "follower_disconnect")).toBe(true);
219
+ expect(followerTransitions.some((transition) => transition.fromState === "reconnecting" && transition.toState === "disconnected")).toBe(true);
220
+ }
221
+ finally {
222
+ infoSpy.mockRestore();
223
+ }
224
+ });
225
+ });
@@ -0,0 +1,140 @@
1
+ import net from "node:net";
2
+ import { mkdtemp, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { afterEach, describe, expect, it } from "vitest";
6
+ import { bindRpcConnection, ClusterRpcClient, } from "../rpc.js";
7
+ import { connectBusClient, createBusService } from "./bus.js";
8
+ const activeServers = [];
9
+ const activeClients = [];
10
+ const activeBuses = [];
11
+ afterEach(async () => {
12
+ for (const bus of activeBuses.splice(0)) {
13
+ bus.close();
14
+ }
15
+ for (const client of activeClients.splice(0)) {
16
+ client.close();
17
+ }
18
+ for (const server of activeServers.splice(0)) {
19
+ await new Promise((resolve) => server.server.close(() => resolve()));
20
+ await rm(server.dir, { recursive: true, force: true });
21
+ }
22
+ });
23
+ async function sleep(ms) {
24
+ await new Promise((resolve) => setTimeout(resolve, ms));
25
+ }
26
+ async function startBusServer() {
27
+ const busService = createBusService();
28
+ const handlers = {
29
+ ...busService.handlers,
30
+ };
31
+ const dir = await mkdtemp(join(tmpdir(), "cluster-v2-bus-test-"));
32
+ const socketPath = join(dir, "bus.sock");
33
+ const server = net.createServer((socket) => {
34
+ bindRpcConnection(socket, async (request, connection) => {
35
+ const handler = handlers[request.method];
36
+ if (!handler) {
37
+ throw new Error(`unknown method: ${request.method}`);
38
+ }
39
+ return handler(request, connection);
40
+ }, {
41
+ onClose: (connection) => {
42
+ busService.onConnectionClosed(connection);
43
+ },
44
+ });
45
+ });
46
+ await new Promise((resolve, reject) => {
47
+ server.once("error", reject);
48
+ server.listen(socketPath, () => resolve());
49
+ });
50
+ const handle = { dir, socketPath, server, busService };
51
+ activeServers.push(handle);
52
+ return handle;
53
+ }
54
+ async function connectClient(socketPath, agentId) {
55
+ const socket = await new Promise((resolve, reject) => {
56
+ const s = net.createConnection(socketPath);
57
+ s.once("connect", () => resolve(s));
58
+ s.once("error", reject);
59
+ });
60
+ const client = new ClusterRpcClient(socket, {
61
+ agentId,
62
+ sessionId: `${agentId}-session`,
63
+ scope: "bus-test",
64
+ role: "bus-test-client",
65
+ });
66
+ activeClients.push(client);
67
+ return client;
68
+ }
69
+ describe("cluster_v2 bus integration", () => {
70
+ it("keeps roster/presence consistent across join and leave", async () => {
71
+ const server = await startBusServer();
72
+ const aliceClient = await connectClient(server.socketPath, "alice");
73
+ const aliceBus = await connectBusClient(aliceClient, "alice", "Alice", ["public"]);
74
+ activeBuses.push(aliceBus);
75
+ const alicePresence = [];
76
+ aliceBus.onPresence((evt) => {
77
+ alicePresence.push(evt);
78
+ });
79
+ const bobClient = await connectClient(server.socketPath, "bob");
80
+ const bobBus = await connectBusClient(bobClient, "bob", "Bob", ["public"]);
81
+ activeBuses.push(bobBus);
82
+ const bobPresence = [];
83
+ bobBus.onPresence((evt) => {
84
+ bobPresence.push(evt);
85
+ });
86
+ await sleep(30);
87
+ expect(alicePresence.some((evt) => evt.agentId === "bob" && evt.status === "joined")).toBe(true);
88
+ expect(bobPresence.some((evt) => evt.agentId === "alice" && evt.status === "joined")).toBe(true);
89
+ const { agents } = await aliceBus.roster();
90
+ expect(agents.map((agent) => agent.agentId).sort()).toEqual(["alice", "bob"]);
91
+ expect(agents.map((agent) => agent.role).sort()).toEqual(["follower", "follower"]);
92
+ bobClient.close();
93
+ await sleep(30);
94
+ expect(alicePresence.some((evt) => evt.agentId === "bob" && evt.status === "left")).toBe(true);
95
+ });
96
+ it("enforces channel isolation and no self-broadcast delivery", async () => {
97
+ const server = await startBusServer();
98
+ const aliceClient = await connectClient(server.socketPath, "alice");
99
+ const bobClient = await connectClient(server.socketPath, "bob");
100
+ const aliceBus = await connectBusClient(aliceClient, "alice", "Alice", ["public"]);
101
+ const bobBus = await connectBusClient(bobClient, "bob", "Bob", ["public"]);
102
+ activeBuses.push(aliceBus, bobBus);
103
+ const aliceMessages = [];
104
+ const bobMessages = [];
105
+ aliceBus.onMessage((msg) => {
106
+ aliceMessages.push(msg);
107
+ });
108
+ bobBus.onMessage((msg) => {
109
+ bobMessages.push(msg);
110
+ });
111
+ await bobBus.subscribe(["secret"]);
112
+ await aliceBus.broadcast({ text: "private payload" }, "secret");
113
+ await sleep(30);
114
+ expect(bobMessages.some((msg) => msg.channel === "secret")).toBe(true);
115
+ expect(aliceMessages.length).toBe(0);
116
+ await aliceBus.broadcast({ text: "public payload" }, "public");
117
+ await sleep(30);
118
+ expect(bobMessages.some((msg) => msg.channel === "public")).toBe(true);
119
+ expect(aliceMessages.length).toBe(0);
120
+ });
121
+ it("keeps leader role when same agent re-registers as follower", async () => {
122
+ const server = await startBusServer();
123
+ const leaderBus = server.busService.createLeaderHandle("same-agent", "Leader");
124
+ activeBuses.push(leaderBus);
125
+ const duplicateClient = await connectClient(server.socketPath, "same-agent");
126
+ const duplicateBus = await connectBusClient(duplicateClient, "same-agent", "Follower", ["secret"]);
127
+ activeBuses.push(duplicateBus);
128
+ await sleep(30);
129
+ const firstRoster = await leaderBus.roster();
130
+ expect(firstRoster.agents).toHaveLength(1);
131
+ expect(firstRoster.agents[0].agentId).toBe("same-agent");
132
+ expect(firstRoster.agents[0].role).toBe("leader");
133
+ expect(firstRoster.agents[0].channels).toEqual(expect.arrayContaining(["public", "private:same-agent", "secret"]));
134
+ duplicateClient.close();
135
+ await sleep(30);
136
+ const secondRoster = await leaderBus.roster();
137
+ expect(secondRoster.agents).toHaveLength(1);
138
+ expect(secondRoster.agents[0].role).toBe("leader");
139
+ });
140
+ });