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,531 @@
1
+ import { createLocalEmbeddingProvider } from "../memory/embeddings/local.js";
2
+ import { tryConnect, tryListen } from "./connection.js";
3
+ import { ConnectionLifecycleTracker } from "./connection-lifecycle.js";
4
+ import { ClusterRpcClient, bindRpcConnection, } from "./rpc.js";
5
+ import { createClusterLogContext, logClusterEvent } from "./observability.js";
6
+ import { emitClusterV2LeaderOffline, emitClusterV2LeaderRecovered } from "./events.js";
7
+ import { createEmbeddingClient, createEmbeddingHandlers } from "./services/embedding.js";
8
+ import { connectBusClient, createBusService } from "./services/bus.js";
9
+ import { maybeStartDiscordCollector } from "./services/discord/index.js";
10
+ import { maybeStartTwitterCollector } from "./services/twitter/index.js";
11
+ import { FollowerRegistryCache } from "./services/registry-cache.js";
12
+ import { ServiceRegistry } from "./services/registry.js";
13
+ let activeService = null;
14
+ let cachedParams = null;
15
+ let reElecting = null;
16
+ let initializingService = null;
17
+ const activeLeaderScopes = new Set();
18
+ const DEFAULT_CLUSTER_V2_EMBEDDING_MAX_CONCURRENT_REQUESTS = 4;
19
+ const DEFAULT_CLUSTER_V2_EMBEDDING_MAX_TEXTS_PER_REQUEST = 4;
20
+ const DEFAULT_CLUSTER_V2_INIT_MAX_ATTEMPTS = 6;
21
+ const DEFAULT_CLUSTER_V2_INIT_RETRY_DELAY_MS = 100;
22
+ function parsePositiveIntegerEnv(name, fallback) {
23
+ const raw = process.env[name];
24
+ if (!raw) {
25
+ return fallback;
26
+ }
27
+ const parsed = Number.parseInt(raw, 10);
28
+ if (!Number.isFinite(parsed) || parsed <= 0) {
29
+ return fallback;
30
+ }
31
+ return parsed;
32
+ }
33
+ function sleep(ms) {
34
+ return new Promise((resolve) => setTimeout(resolve, ms));
35
+ }
36
+ export function getActiveClusterV2Service() {
37
+ return activeService;
38
+ }
39
+ function normalizeScope(scope) {
40
+ const raw = scope?.trim() || "default";
41
+ return raw.replace(/[^a-zA-Z0-9._-]/g, "_");
42
+ }
43
+ function transitionConnectionLifecycle(tracker, next, label, context) {
44
+ const previous = tracker.state();
45
+ tracker.transition(next, label);
46
+ if (previous !== next) {
47
+ logClusterEvent("info", "connection_lifecycle_transition", context, {
48
+ fromState: previous,
49
+ toState: next,
50
+ label,
51
+ });
52
+ }
53
+ }
54
+ export async function initClusterV2(params) {
55
+ if (activeService) {
56
+ return activeService;
57
+ }
58
+ if (initializingService) {
59
+ return initializingService;
60
+ }
61
+ initializingService = (async () => {
62
+ cachedParams = params;
63
+ const modelId = params.modelPath ?? "local";
64
+ const sessionId = params.getSessionId?.();
65
+ const scope = normalizeScope(params.scope);
66
+ const initLogContext = createClusterLogContext({
67
+ agentId: params.agentId,
68
+ sessionId,
69
+ scope,
70
+ role: "runtime",
71
+ });
72
+ for (let attempt = 1; attempt <= DEFAULT_CLUSTER_V2_INIT_MAX_ATTEMPTS; attempt += 1) {
73
+ const follower = await tryFollowAsClient(params, modelId, sessionId);
74
+ if (follower) {
75
+ activeService = follower;
76
+ logClusterEvent("info", "runtime_role_selected", initLogContext, { role: follower.role });
77
+ return follower;
78
+ }
79
+ const leader = await tryServeAsLeader(params);
80
+ if (leader) {
81
+ activeService = leader;
82
+ logClusterEvent("info", "runtime_role_selected", initLogContext, { role: leader.role });
83
+ return leader;
84
+ }
85
+ const retryFollower = await tryFollowAsClient(params, modelId, sessionId);
86
+ if (retryFollower) {
87
+ activeService = retryFollower;
88
+ logClusterEvent("info", "runtime_role_selected", initLogContext, { role: retryFollower.role });
89
+ return retryFollower;
90
+ }
91
+ if (attempt < DEFAULT_CLUSTER_V2_INIT_MAX_ATTEMPTS) {
92
+ logClusterEvent("warn", "runtime_role_resolution_retry", initLogContext, {
93
+ attempt,
94
+ maxAttempts: DEFAULT_CLUSTER_V2_INIT_MAX_ATTEMPTS,
95
+ retryDelayMs: DEFAULT_CLUSTER_V2_INIT_RETRY_DELAY_MS,
96
+ });
97
+ await sleep(DEFAULT_CLUSTER_V2_INIT_RETRY_DELAY_MS);
98
+ }
99
+ }
100
+ const standaloneProvider = await createLocalEmbeddingProvider(params);
101
+ activeService = {
102
+ role: "standalone",
103
+ embedding: standaloneProvider,
104
+ bus: null,
105
+ async getServiceRegistrySnapshot() {
106
+ return {
107
+ revision: 0,
108
+ services: [
109
+ {
110
+ name: "embedding",
111
+ version: "standalone",
112
+ capabilities: { methods: ["embedding.embedBatch"] },
113
+ },
114
+ ],
115
+ };
116
+ },
117
+ async close() {
118
+ if (standaloneProvider.dispose) {
119
+ await standaloneProvider.dispose();
120
+ }
121
+ activeService = null;
122
+ },
123
+ };
124
+ logClusterEvent("warn", "runtime_role_selected", initLogContext, { role: "standalone" });
125
+ return activeService;
126
+ })();
127
+ try {
128
+ return await initializingService;
129
+ }
130
+ finally {
131
+ initializingService = null;
132
+ }
133
+ }
134
+ export async function closeClusterV2() {
135
+ if (!activeService) {
136
+ return;
137
+ }
138
+ logClusterEvent("info", "runtime_close", createClusterLogContext({ role: "runtime" }), {
139
+ role: activeService.role,
140
+ });
141
+ await activeService.close();
142
+ activeService = null;
143
+ }
144
+ export async function reelectClusterV2() {
145
+ if (!cachedParams) {
146
+ throw new Error("cluster_v2 is not initialized");
147
+ }
148
+ await closeClusterV2();
149
+ return initClusterV2(cachedParams);
150
+ }
151
+ export async function stepdownClusterV2Leader() {
152
+ if (!activeService) {
153
+ throw new Error("cluster_v2 is not active");
154
+ }
155
+ if (activeService.role !== "leader") {
156
+ throw new Error(`stepdown requires leader role, current role: ${activeService.role}`);
157
+ }
158
+ if (!cachedParams) {
159
+ throw new Error("cluster_v2 is missing cached init params");
160
+ }
161
+ await closeClusterV2();
162
+ return initClusterV2(cachedParams);
163
+ }
164
+ async function tryFollowAsClient(params, modelId, sessionId) {
165
+ const scope = normalizeScope(params.scope);
166
+ const lifecycle = new ConnectionLifecycleTracker("disconnected");
167
+ const logContext = createClusterLogContext({
168
+ agentId: params.agentId,
169
+ sessionId,
170
+ scope,
171
+ role: "follower",
172
+ });
173
+ transitionConnectionLifecycle(lifecycle, "connecting", "tryFollowAsClient:start", logContext);
174
+ const socket = await tryConnect(params.scope);
175
+ if (!socket) {
176
+ transitionConnectionLifecycle(lifecycle, "disconnected", "tryFollowAsClient:no_socket", logContext);
177
+ return null;
178
+ }
179
+ transitionConnectionLifecycle(lifecycle, "connected", "tryFollowAsClient:connected", logContext);
180
+ lifecycle.assertOpenImpliesConnected(!socket.destroyed, "tryFollowAsClient:connected");
181
+ const client = new ClusterRpcClient(socket, {
182
+ agentId: params.agentId,
183
+ sessionId,
184
+ scope,
185
+ role: "rpc_client_follower",
186
+ });
187
+ try {
188
+ const pong = await client.call("cluster.ping", null, { timeoutMs: 3000 });
189
+ if (pong !== "pong") {
190
+ client.close();
191
+ transitionConnectionLifecycle(lifecycle, "disconnected", "tryFollowAsClient:ping_mismatch", logContext);
192
+ return null;
193
+ }
194
+ }
195
+ catch {
196
+ client.close();
197
+ transitionConnectionLifecycle(lifecycle, "disconnected", "tryFollowAsClient:ping_failed", logContext);
198
+ return null;
199
+ }
200
+ const registryCache = new FollowerRegistryCache(client, logContext);
201
+ try {
202
+ await registryCache.refresh();
203
+ await registryCache.requireService("embedding");
204
+ }
205
+ catch (error) {
206
+ logClusterEvent("warn", "registry_bootstrap_failed", logContext, {
207
+ error: error instanceof Error ? error.message : String(error),
208
+ });
209
+ registryCache.invalidate("bootstrap_failed");
210
+ client.close();
211
+ transitionConnectionLifecycle(lifecycle, "disconnected", "tryFollowAsClient:registry_failed", logContext);
212
+ return null;
213
+ }
214
+ const baseEmbedding = createEmbeddingClient(client, {
215
+ model: modelId,
216
+ timeoutMs: params.rpcTimeoutMs,
217
+ });
218
+ const embedding = {
219
+ id: baseEmbedding.id,
220
+ model: baseEmbedding.model,
221
+ embedQuery: (text) => withReconnect(baseEmbedding, (p) => p.embedQuery(text)),
222
+ embedBatch: (texts) => withReconnect(baseEmbedding, (p) => p.embedBatch(texts)),
223
+ dispose: async () => {
224
+ client.close();
225
+ },
226
+ };
227
+ let bus = null;
228
+ try {
229
+ await registryCache.requireService("bus");
230
+ bus = await connectBusClient(client, params.agentId, params.displayName);
231
+ }
232
+ catch (error) {
233
+ logClusterEvent("warn", "bus_service_unavailable", logContext, {
234
+ error: error instanceof Error ? error.message : String(error),
235
+ cacheRevision: registryCache.currentRevision(),
236
+ });
237
+ registryCache.invalidate("bus_unavailable");
238
+ client.close();
239
+ transitionConnectionLifecycle(lifecycle, "disconnected", "tryFollowAsClient:bus_unavailable", logContext);
240
+ return null;
241
+ }
242
+ const detachDisconnect = client.onDisconnect(() => {
243
+ if (reElecting || !activeService || activeService.role !== "follower" || !cachedParams) {
244
+ return;
245
+ }
246
+ registryCache.invalidate("leader_disconnect");
247
+ emitClusterV2LeaderOffline({
248
+ agentId: params.agentId,
249
+ sessionId,
250
+ scope,
251
+ reason: "follower_disconnect",
252
+ });
253
+ transitionConnectionLifecycle(lifecycle, "reconnecting", "follower_disconnect", logContext);
254
+ lifecycle.assertOpenImpliesConnected(!socket.destroyed, "follower_disconnect");
255
+ logClusterEvent("warn", "follower_disconnected_re_elect", logContext);
256
+ activeService = null;
257
+ reElecting = initClusterV2(cachedParams)
258
+ .then((next) => {
259
+ if (next.role === "leader" || next.role === "follower") {
260
+ emitClusterV2LeaderRecovered({
261
+ agentId: params.agentId,
262
+ sessionId,
263
+ scope,
264
+ role: next.role,
265
+ });
266
+ }
267
+ transitionConnectionLifecycle(lifecycle, "disconnected", "re_elect_complete", logContext);
268
+ reElecting = null;
269
+ })
270
+ .catch(() => {
271
+ transitionConnectionLifecycle(lifecycle, "disconnected", "re_elect_failed", logContext);
272
+ reElecting = null;
273
+ });
274
+ });
275
+ return {
276
+ role: "follower",
277
+ embedding,
278
+ bus,
279
+ async getServiceRegistrySnapshot() {
280
+ return client.call("registry.list", {});
281
+ },
282
+ async close() {
283
+ detachDisconnect();
284
+ if (bus) {
285
+ bus.close();
286
+ }
287
+ registryCache.invalidate("follower_close");
288
+ client.close();
289
+ transitionConnectionLifecycle(lifecycle, "closed", "follower_close", logContext);
290
+ lifecycle.assertOpenImpliesConnected(!socket.destroyed, "follower_close");
291
+ activeService = null;
292
+ },
293
+ };
294
+ }
295
+ async function withReconnect(inner, op) {
296
+ try {
297
+ return await op(inner);
298
+ }
299
+ catch (error) {
300
+ if (!isConnectionError(error) || !cachedParams) {
301
+ throw error;
302
+ }
303
+ if (reElecting) {
304
+ await reElecting;
305
+ }
306
+ else {
307
+ activeService = null;
308
+ await initClusterV2(cachedParams);
309
+ }
310
+ if (!activeService) {
311
+ throw error;
312
+ }
313
+ return op(activeService.embedding);
314
+ }
315
+ }
316
+ async function tryServeAsLeader(params) {
317
+ const leaderScope = normalizeScope(params.scope);
318
+ const lifecycle = new ConnectionLifecycleTracker("disconnected");
319
+ const sessionId = params.getSessionId?.();
320
+ const logContext = createClusterLogContext({
321
+ agentId: params.agentId,
322
+ sessionId,
323
+ scope: leaderScope,
324
+ role: "leader",
325
+ });
326
+ let leaseCompromised = false;
327
+ let leaseCompromisedError = null;
328
+ let handleLeaseCompromised = null;
329
+ let closingLeader = false;
330
+ if (activeLeaderScopes.has(leaderScope)) {
331
+ logClusterEvent("warn", "leader_guard_precheck_failed", logContext, {
332
+ leaderScope,
333
+ });
334
+ throw new Error(`[cluster_v2/runtime] leader guard violated for scope: ${leaderScope}`);
335
+ }
336
+ const provider = await createLocalEmbeddingProvider(params);
337
+ transitionConnectionLifecycle(lifecycle, "connecting", "tryServeAsLeader:start", logContext);
338
+ const connection = await tryListen({
339
+ scope: params.scope,
340
+ onLeaseCompromised: (error) => {
341
+ leaseCompromised = true;
342
+ leaseCompromisedError = error;
343
+ logClusterEvent("warn", "leader_lease_compromised", logContext, {
344
+ error: error.message,
345
+ leaderScope,
346
+ });
347
+ handleLeaseCompromised?.();
348
+ },
349
+ });
350
+ if (!connection) {
351
+ transitionConnectionLifecycle(lifecycle, "disconnected", "tryServeAsLeader:listen_failed", logContext);
352
+ if (provider.dispose) {
353
+ await provider.dispose();
354
+ }
355
+ return null;
356
+ }
357
+ transitionConnectionLifecycle(lifecycle, "connected", "tryServeAsLeader:listening", logContext);
358
+ lifecycle.assertOpenImpliesConnected(connection.server.listening, "tryServeAsLeader:listening");
359
+ if (leaseCompromised) {
360
+ await connection.close();
361
+ if (provider.dispose) {
362
+ await provider.dispose();
363
+ }
364
+ transitionConnectionLifecycle(lifecycle, "closed", "leader_lease_compromised_before_activate", logContext);
365
+ return null;
366
+ }
367
+ if (activeLeaderScopes.has(leaderScope)) {
368
+ await connection.close();
369
+ if (provider.dispose) {
370
+ await provider.dispose();
371
+ }
372
+ transitionConnectionLifecycle(lifecycle, "closed", "leader_guard_race_failed", logContext);
373
+ logClusterEvent("warn", "leader_guard_race_failed", logContext, {
374
+ leaderScope,
375
+ });
376
+ throw new Error(`[cluster_v2/runtime] leader guard violated for scope: ${leaderScope}`);
377
+ }
378
+ activeLeaderScopes.add(leaderScope);
379
+ logClusterEvent("info", "leader_scope_activated", logContext, { leaderScope });
380
+ const busService = createBusService();
381
+ const embeddingHandlers = createEmbeddingHandlers(provider, {
382
+ maxConcurrentRequests: parsePositiveIntegerEnv("MONO_PILOT_CLUSTER_V2_EMBEDDING_MAX_CONCURRENCY", DEFAULT_CLUSTER_V2_EMBEDDING_MAX_CONCURRENT_REQUESTS),
383
+ maxTextsPerRequest: parsePositiveIntegerEnv("MONO_PILOT_CLUSTER_V2_EMBEDDING_MAX_TEXTS_PER_REQUEST", DEFAULT_CLUSTER_V2_EMBEDDING_MAX_TEXTS_PER_REQUEST),
384
+ });
385
+ let discordCollector = null;
386
+ let twitterCollector = null;
387
+ try {
388
+ discordCollector = await maybeStartDiscordCollector(logContext);
389
+ }
390
+ catch (error) {
391
+ logClusterEvent("warn", "discord_collector_start_failed", logContext, {
392
+ error: error instanceof Error ? error.message : String(error),
393
+ });
394
+ discordCollector = null;
395
+ }
396
+ try {
397
+ twitterCollector = await maybeStartTwitterCollector(logContext);
398
+ }
399
+ catch (error) {
400
+ logClusterEvent("warn", "twitter_collector_start_failed", logContext, {
401
+ error: error instanceof Error ? error.message : String(error),
402
+ });
403
+ twitterCollector = null;
404
+ }
405
+ const extraServices = [];
406
+ if (discordCollector) {
407
+ extraServices.push(discordCollector.descriptor);
408
+ }
409
+ if (twitterCollector) {
410
+ extraServices.push(twitterCollector.descriptor);
411
+ }
412
+ const registry = new ServiceRegistry();
413
+ registerDefaultServices(registry, extraServices);
414
+ const handlers = {
415
+ "cluster.ping": async () => "pong",
416
+ "registry.list": async () => registry.snapshot(),
417
+ "registry.resolve": async (request) => {
418
+ const name = request.params?.name;
419
+ if (!name || typeof name !== "string") {
420
+ throw new Error("registry.resolve requires string name");
421
+ }
422
+ const service = registry.resolve(name);
423
+ return { revision: registry.getRevision(), service };
424
+ },
425
+ ...embeddingHandlers,
426
+ ...busService.handlers,
427
+ };
428
+ connection.server.on("connection", (socket) => {
429
+ bindRpcConnection(socket, async (request, rpcConnection) => {
430
+ const handler = handlers[request.method];
431
+ if (!handler) {
432
+ throw new Error(`unknown method: ${request.method}`);
433
+ }
434
+ return handler(request, rpcConnection);
435
+ }, {
436
+ onClose: (rpcConnection) => {
437
+ busService.onConnectionClosed(rpcConnection);
438
+ },
439
+ });
440
+ });
441
+ const bus = busService.createLeaderHandle(params.agentId, params.displayName);
442
+ const closeLeaderResources = async (reason) => {
443
+ if (closingLeader) {
444
+ return;
445
+ }
446
+ closingLeader = true;
447
+ activeLeaderScopes.delete(leaderScope);
448
+ if (discordCollector) {
449
+ await discordCollector.close();
450
+ discordCollector = null;
451
+ }
452
+ if (twitterCollector) {
453
+ await twitterCollector.close();
454
+ twitterCollector = null;
455
+ }
456
+ bus.close();
457
+ await connection.close();
458
+ transitionConnectionLifecycle(lifecycle, "closed", `leader_close:${reason}`, logContext);
459
+ lifecycle.assertOpenImpliesConnected(connection.server.listening, `leader_close:${reason}`);
460
+ logClusterEvent("info", "leader_scope_released", logContext, { leaderScope, reason });
461
+ if (provider.dispose) {
462
+ await provider.dispose();
463
+ }
464
+ activeService = null;
465
+ };
466
+ handleLeaseCompromised = () => {
467
+ if (closingLeader) {
468
+ return;
469
+ }
470
+ void (async () => {
471
+ await closeLeaderResources("lease_compromised");
472
+ if (!cachedParams || reElecting) {
473
+ return;
474
+ }
475
+ reElecting = initClusterV2(cachedParams)
476
+ .then(() => {
477
+ reElecting = null;
478
+ })
479
+ .catch((error) => {
480
+ logClusterEvent("warn", "leader_lease_compromise_re_elect_failed", logContext, {
481
+ error: error instanceof Error ? error.message : String(error),
482
+ leaseError: leaseCompromisedError?.message,
483
+ });
484
+ reElecting = null;
485
+ });
486
+ })();
487
+ };
488
+ if (leaseCompromised) {
489
+ await closeLeaderResources("lease_compromised_pre_activate");
490
+ return null;
491
+ }
492
+ return {
493
+ role: "leader",
494
+ embedding: provider,
495
+ bus,
496
+ async getServiceRegistrySnapshot() {
497
+ return registry.snapshot();
498
+ },
499
+ async close() {
500
+ await closeLeaderResources("explicit_close");
501
+ },
502
+ };
503
+ }
504
+ function registerDefaultServices(registry, extras) {
505
+ const services = [
506
+ { name: "embedding", version: "v2", capabilities: { methods: ["embedding.embedBatch"] } },
507
+ {
508
+ name: "bus",
509
+ version: "v2",
510
+ capabilities: {
511
+ methods: ["bus.register", "bus.subscribe", "bus.send", "bus.broadcast", "bus.roster"],
512
+ },
513
+ },
514
+ ...(extras ?? []),
515
+ ];
516
+ for (const service of services) {
517
+ registry.register(service);
518
+ }
519
+ }
520
+ function isConnectionError(error) {
521
+ if (!(error instanceof Error)) {
522
+ return false;
523
+ }
524
+ const message = error.message;
525
+ return (message.includes("socket closed") ||
526
+ message.includes("socket error") ||
527
+ message.includes("client closed") ||
528
+ message.includes("timeout") ||
529
+ message.includes("EPIPE") ||
530
+ message.includes("ECONNRESET"));
531
+ }
@@ -0,0 +1,91 @@
1
+ import net from "node:net";
2
+ import { afterEach, describe, expect, it, vi } from "vitest";
3
+ const harness = vi.hoisted(() => ({
4
+ listenCalls: 0,
5
+ compromiseTriggered: false,
6
+ }));
7
+ vi.mock("../memory/embeddings/local.js", () => ({
8
+ createLocalEmbeddingProvider: async (params) => ({
9
+ id: "local",
10
+ model: params.modelPath ?? "mock-local",
11
+ embedQuery: async (text) => [text.length],
12
+ embedBatch: async (texts) => texts.map((text) => [text.length]),
13
+ dispose: async () => {
14
+ // no-op in tests
15
+ },
16
+ }),
17
+ }));
18
+ vi.mock("./connection.js", () => ({
19
+ tryConnect: async () => null,
20
+ tryListen: async (options) => {
21
+ harness.listenCalls++;
22
+ const server = net.createServer();
23
+ await new Promise((resolve, reject) => {
24
+ server.once("error", reject);
25
+ server.listen(0, "127.0.0.1", () => resolve());
26
+ });
27
+ let closed = false;
28
+ const close = async () => {
29
+ if (closed) {
30
+ return;
31
+ }
32
+ closed = true;
33
+ await new Promise((resolve) => server.close(() => resolve()));
34
+ };
35
+ if (!harness.compromiseTriggered) {
36
+ harness.compromiseTriggered = true;
37
+ setTimeout(() => {
38
+ options?.onLeaseCompromised?.(new Error("lease compromised in test"));
39
+ }, 15);
40
+ }
41
+ return {
42
+ socketPath: `mock-socket-${harness.listenCalls}`,
43
+ server,
44
+ close,
45
+ };
46
+ },
47
+ }));
48
+ async function sleep(ms) {
49
+ await new Promise((resolve) => setTimeout(resolve, ms));
50
+ }
51
+ async function waitFor(condition, timeoutMs) {
52
+ const deadline = Date.now() + timeoutMs;
53
+ while (Date.now() < deadline) {
54
+ if (await condition()) {
55
+ return;
56
+ }
57
+ await sleep(20);
58
+ }
59
+ throw new Error("waitFor timeout");
60
+ }
61
+ afterEach(async () => {
62
+ const runtime = await import("./runtime.js");
63
+ await runtime.closeClusterV2();
64
+ harness.listenCalls = 0;
65
+ harness.compromiseTriggered = false;
66
+ });
67
+ describe("cluster_v2 runtime lease compromise integration", () => {
68
+ it("steps down and re-elects when lease is compromised", async () => {
69
+ const runtime = await import("./runtime.js");
70
+ const params = {
71
+ agentId: "lease-agent",
72
+ displayName: "Lease Agent",
73
+ scope: `lease-scope-${process.pid}-${Date.now()}`,
74
+ modelPath: "mock-local",
75
+ rpcTimeoutMs: 500,
76
+ getSessionId: () => "lease-session",
77
+ };
78
+ const first = await runtime.initClusterV2(params);
79
+ expect(first.role).toBe("leader");
80
+ await waitFor(async () => {
81
+ const current = await runtime.initClusterV2(params);
82
+ return current.role === "leader" && current !== first;
83
+ }, 3000);
84
+ const second = await runtime.initClusterV2(params);
85
+ expect(second.role).toBe("leader");
86
+ expect(second).not.toBe(first);
87
+ expect(harness.listenCalls).toBeGreaterThanOrEqual(2);
88
+ const embedding = await second.embedding.embedQuery("ok");
89
+ expect(embedding).toEqual([2]);
90
+ });
91
+ });