@claude-flow/swarm 3.0.0-alpha.7 → 3.0.0-alpha.9

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 (130) hide show
  1. package/dist/agent-pool.d.ts.map +1 -1
  2. package/dist/agent-pool.js +10 -2
  3. package/dist/agent-pool.js.map +1 -1
  4. package/dist/attention-coordinator.d.ts +5 -4
  5. package/dist/attention-coordinator.d.ts.map +1 -1
  6. package/dist/attention-coordinator.js +17 -10
  7. package/dist/attention-coordinator.js.map +1 -1
  8. package/dist/consensus/byzantine.d.ts +2 -1
  9. package/dist/consensus/byzantine.d.ts.map +1 -1
  10. package/dist/consensus/byzantine.js +34 -8
  11. package/dist/consensus/byzantine.js.map +1 -1
  12. package/dist/consensus/gossip.d.ts +2 -1
  13. package/dist/consensus/gossip.d.ts.map +1 -1
  14. package/dist/consensus/gossip.js +27 -8
  15. package/dist/consensus/gossip.js.map +1 -1
  16. package/dist/consensus/index.d.ts +1 -1
  17. package/dist/consensus/index.d.ts.map +1 -1
  18. package/dist/consensus/index.js +2 -2
  19. package/dist/consensus/index.js.map +1 -1
  20. package/dist/consensus/raft.d.ts +2 -1
  21. package/dist/consensus/raft.d.ts.map +1 -1
  22. package/dist/consensus/raft.js +25 -9
  23. package/dist/consensus/raft.js.map +1 -1
  24. package/dist/coordination/agent-registry.d.ts +2 -2
  25. package/dist/coordination/agent-registry.d.ts.map +1 -1
  26. package/dist/coordination/agent-registry.js +1 -1
  27. package/dist/coordination/agent-registry.js.map +1 -1
  28. package/dist/coordination/swarm-hub.d.ts +5 -5
  29. package/dist/coordination/swarm-hub.d.ts.map +1 -1
  30. package/dist/coordination/swarm-hub.js +5 -5
  31. package/dist/coordination/swarm-hub.js.map +1 -1
  32. package/dist/coordination/task-orchestrator.d.ts +3 -3
  33. package/dist/coordination/task-orchestrator.d.ts.map +1 -1
  34. package/dist/coordination/task-orchestrator.js +1 -1
  35. package/dist/coordination/task-orchestrator.js.map +1 -1
  36. package/dist/index.d.ts +2 -0
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +4 -0
  39. package/dist/index.js.map +1 -1
  40. package/dist/message-bus.d.ts.map +1 -1
  41. package/dist/message-bus.js +13 -4
  42. package/dist/message-bus.js.map +1 -1
  43. package/dist/queen-coordinator.d.ts +1 -1
  44. package/dist/queen-coordinator.d.ts.map +1 -1
  45. package/dist/queen-coordinator.js +10 -3
  46. package/dist/queen-coordinator.js.map +1 -1
  47. package/dist/topology-manager.d.ts.map +1 -1
  48. package/dist/topology-manager.js +26 -1
  49. package/dist/topology-manager.js.map +1 -1
  50. package/dist/types.d.ts +3 -2
  51. package/dist/types.d.ts.map +1 -1
  52. package/dist/types.js.map +1 -1
  53. package/dist/unified-coordinator.d.ts +1 -1
  54. package/dist/unified-coordinator.d.ts.map +1 -1
  55. package/dist/unified-coordinator.js +85 -29
  56. package/dist/unified-coordinator.js.map +1 -1
  57. package/package.json +7 -2
  58. package/.claude-flow/data/pending-insights.jsonl +0 -12
  59. package/__tests__/byzantine-transport.test.ts +0 -147
  60. package/__tests__/consensus-failure-injection.test.ts +0 -161
  61. package/__tests__/consensus-transport.test.ts +0 -200
  62. package/__tests__/consensus.test.ts +0 -577
  63. package/__tests__/coordinator.test.ts +0 -501
  64. package/__tests__/federation-transport.test.ts +0 -145
  65. package/__tests__/gossip-transport.test.ts +0 -81
  66. package/__tests__/queen-coordinator.test.ts +0 -1335
  67. package/__tests__/raft-transport.test.ts +0 -121
  68. package/__tests__/topology.test.ts +0 -621
  69. package/ruvector.db +0 -0
  70. package/src/agent-pool.ts +0 -476
  71. package/src/application/commands/create-task.command.ts +0 -124
  72. package/src/application/commands/spawn-agent.command.ts +0 -122
  73. package/src/application/index.ts +0 -30
  74. package/src/application/services/swarm-application-service.ts +0 -200
  75. package/src/attention-coordinator.ts +0 -1000
  76. package/src/consensus/byzantine.ts +0 -514
  77. package/src/consensus/federation-transport.ts +0 -185
  78. package/src/consensus/gossip.ts +0 -599
  79. package/src/consensus/index.ts +0 -296
  80. package/src/consensus/raft.ts +0 -561
  81. package/src/consensus/transport.ts +0 -284
  82. package/src/coordination/agent-registry.ts +0 -544
  83. package/src/coordination/index.ts +0 -23
  84. package/src/coordination/swarm-hub.ts +0 -776
  85. package/src/coordination/task-orchestrator.ts +0 -605
  86. package/src/domain/entities/agent.d.ts +0 -151
  87. package/src/domain/entities/agent.d.ts.map +0 -1
  88. package/src/domain/entities/agent.js +0 -280
  89. package/src/domain/entities/agent.js.map +0 -1
  90. package/src/domain/entities/agent.ts +0 -370
  91. package/src/domain/entities/task.d.ts +0 -133
  92. package/src/domain/entities/task.d.ts.map +0 -1
  93. package/src/domain/entities/task.js +0 -261
  94. package/src/domain/entities/task.js.map +0 -1
  95. package/src/domain/entities/task.ts +0 -319
  96. package/src/domain/index.ts +0 -41
  97. package/src/domain/repositories/agent-repository.interface.d.ts +0 -57
  98. package/src/domain/repositories/agent-repository.interface.d.ts.map +0 -1
  99. package/src/domain/repositories/agent-repository.interface.js +0 -9
  100. package/src/domain/repositories/agent-repository.interface.js.map +0 -1
  101. package/src/domain/repositories/agent-repository.interface.ts +0 -69
  102. package/src/domain/repositories/task-repository.interface.d.ts +0 -61
  103. package/src/domain/repositories/task-repository.interface.d.ts.map +0 -1
  104. package/src/domain/repositories/task-repository.interface.js +0 -9
  105. package/src/domain/repositories/task-repository.interface.js.map +0 -1
  106. package/src/domain/repositories/task-repository.interface.ts +0 -75
  107. package/src/domain/services/coordination-service.ts +0 -320
  108. package/src/federation-hub.d.ts +0 -284
  109. package/src/federation-hub.d.ts.map +0 -1
  110. package/src/federation-hub.js +0 -692
  111. package/src/federation-hub.js.map +0 -1
  112. package/src/federation-hub.ts +0 -979
  113. package/src/index.ts +0 -348
  114. package/src/message-bus.ts +0 -607
  115. package/src/queen-coordinator.ts +0 -2025
  116. package/src/shared/events.ts +0 -285
  117. package/src/shared/types.ts +0 -389
  118. package/src/topology-manager.ts +0 -656
  119. package/src/types.ts +0 -545
  120. package/src/unified-coordinator.ts +0 -1844
  121. package/src/workers/index.ts +0 -65
  122. package/src/workers/worker-dispatch.d.ts +0 -234
  123. package/src/workers/worker-dispatch.d.ts.map +0 -1
  124. package/src/workers/worker-dispatch.js +0 -842
  125. package/src/workers/worker-dispatch.js.map +0 -1
  126. package/src/workers/worker-dispatch.ts +0 -1076
  127. package/tmp.json +0 -0
  128. package/tsconfig.json +0 -9
  129. package/tsconfig.tsbuildinfo +0 -1
  130. package/vitest.config.ts +0 -20
@@ -1,599 +0,0 @@
1
- /**
2
- * V3 Gossip Protocol Consensus
3
- * Eventually consistent consensus for large-scale distributed systems
4
- */
5
-
6
- import { EventEmitter } from 'events';
7
- import type { ConsensusTransport, ConsensusMessage } from './transport.js';
8
- import {
9
- ConsensusProposal,
10
- ConsensusVote,
11
- ConsensusResult,
12
- ConsensusConfig,
13
- SWARM_CONSTANTS,
14
- } from '../types.js';
15
-
16
- export interface GossipMessage {
17
- id: string;
18
- type: 'proposal' | 'vote' | 'state' | 'ack';
19
- senderId: string;
20
- version: number;
21
- payload: unknown;
22
- timestamp: Date;
23
- ttl: number;
24
- hops: number;
25
- path: string[];
26
- }
27
-
28
- /**
29
- * Bounded set that evicts oldest entries when capacity is reached.
30
- * Uses Map insertion-order for O(1) FIFO eviction. (PERF-01)
31
- */
32
- export class BoundedSet<T> {
33
- private map = new Map<T, true>();
34
- private readonly maxSize: number;
35
-
36
- constructor(maxSize: number) {
37
- this.maxSize = maxSize;
38
- }
39
-
40
- has(value: T): boolean {
41
- return this.map.has(value);
42
- }
43
-
44
- add(value: T): void {
45
- if (this.map.has(value)) return;
46
-
47
- if (this.map.size >= this.maxSize) {
48
- // Evict oldest (first inserted)
49
- const oldest = this.map.keys().next().value;
50
- if (oldest !== undefined) {
51
- this.map.delete(oldest);
52
- }
53
- }
54
- this.map.set(value, true);
55
- }
56
-
57
- get size(): number {
58
- return this.map.size;
59
- }
60
-
61
- clear(): void {
62
- this.map.clear();
63
- }
64
- }
65
-
66
- export interface GossipNode {
67
- id: string;
68
- state: Map<string, unknown>;
69
- version: number;
70
- neighbors: Set<string>;
71
- seenMessages: BoundedSet<string>;
72
- lastSync: Date;
73
- }
74
-
75
- export interface GossipConfig extends Partial<ConsensusConfig> {
76
- fanout?: number;
77
- gossipIntervalMs?: number;
78
- maxHops?: number;
79
- convergenceThreshold?: number;
80
- /**
81
- * ADR-095 G2 — optional pluggable transport. When set, gossip messages
82
- * to neighbors actually go over it (signed if the transport has a
83
- * keypair) and inbound gossip is routed back into the merge logic.
84
- * When unset, behavior is unchanged: the legacy in-process path mutates
85
- * the local `nodes` map directly (single-process).
86
- */
87
- transport?: ConsensusTransport;
88
- }
89
-
90
- export class GossipConsensus extends EventEmitter {
91
- private config: GossipConfig;
92
- private node: GossipNode;
93
- private nodes: Map<string, GossipNode> = new Map();
94
- private proposals: Map<string, ConsensusProposal> = new Map();
95
- private messageQueue: GossipMessage[] = [];
96
- private gossipInterval?: NodeJS.Timeout;
97
- private proposalCounter: number = 0;
98
- private readonly transport?: ConsensusTransport;
99
-
100
- constructor(nodeId: string, config: GossipConfig = {}) {
101
- super();
102
- this.config = {
103
- threshold: config.threshold ?? SWARM_CONSTANTS.DEFAULT_CONSENSUS_THRESHOLD,
104
- timeoutMs: config.timeoutMs ?? SWARM_CONSTANTS.DEFAULT_CONSENSUS_TIMEOUT_MS,
105
- maxRounds: config.maxRounds ?? 10,
106
- requireQuorum: config.requireQuorum ?? false, // Gossip is eventually consistent
107
- fanout: config.fanout ?? 3,
108
- gossipIntervalMs: config.gossipIntervalMs ?? 100,
109
- maxHops: config.maxHops ?? 10,
110
- convergenceThreshold: config.convergenceThreshold ?? 0.9,
111
- transport: config.transport,
112
- };
113
- this.transport = config.transport;
114
-
115
- this.node = {
116
- id: nodeId,
117
- state: new Map(),
118
- version: 0,
119
- neighbors: new Set(),
120
- seenMessages: new BoundedSet(100_000), // PERF-01: ~4MB cap (100K × ~40B IDs)
121
- lastSync: new Date(),
122
- };
123
-
124
- if (this.transport) {
125
- this.transport.onMessage(async (msg: ConsensusMessage) => this.handleInboundGossipMessage(msg));
126
- }
127
- }
128
-
129
- /**
130
- * ADR-095 G2 — route an inbound transport message into the gossip merge
131
- * logic. The transport handles signature verification (if enabled). We
132
- * dedupe by message id (the BoundedSet `seenMessages`) and process it as
133
- * though it arrived from a neighbor.
134
- */
135
- private async handleInboundGossipMessage(msg: ConsensusMessage): Promise<void> {
136
- if (msg.type !== 'gossip') return;
137
- const gm = msg.payload as GossipMessage | undefined;
138
- if (!gm || typeof gm.id !== 'string') return;
139
- if (this.node.seenMessages.has(gm.id)) return;
140
- // Ensure the sender is known as a neighbor so processReceivedMessage works.
141
- if (!this.nodes.has(msg.from)) {
142
- this.nodes.set(msg.from, { id: msg.from, state: new Map(), version: 0, neighbors: new Set(), seenMessages: new BoundedSet(100_000), lastSync: new Date() });
143
- }
144
- this.node.neighbors.add(msg.from);
145
- // Process against THIS node's state (the message reached us).
146
- await this.processReceivedMessage(this.node, {
147
- ...gm,
148
- timestamp: gm.timestamp ? new Date(gm.timestamp as unknown as string) : new Date(),
149
- path: Array.isArray(gm.path) ? gm.path : [],
150
- });
151
- }
152
-
153
- async initialize(): Promise<void> {
154
- this.startGossipLoop();
155
- this.emit('initialized', { nodeId: this.node.id });
156
- }
157
-
158
- async shutdown(): Promise<void> {
159
- if (this.gossipInterval) {
160
- clearInterval(this.gossipInterval);
161
- }
162
- this.emit('shutdown');
163
- }
164
-
165
- addNode(nodeId: string): void {
166
- this.nodes.set(nodeId, {
167
- id: nodeId,
168
- state: new Map(),
169
- version: 0,
170
- neighbors: new Set(),
171
- seenMessages: new BoundedSet(100_000), // PERF-01: bounded to prevent memory leak (~4MB cap)
172
- lastSync: new Date(),
173
- });
174
-
175
- // Add as neighbor with some probability (random mesh)
176
- if (Math.random() < 0.5) {
177
- this.node.neighbors.add(nodeId);
178
- this.nodes.get(nodeId)!.neighbors.add(this.node.id);
179
- }
180
- }
181
-
182
- removeNode(nodeId: string): void {
183
- this.nodes.delete(nodeId);
184
- this.node.neighbors.delete(nodeId);
185
-
186
- for (const node of this.nodes.values()) {
187
- node.neighbors.delete(nodeId);
188
- }
189
- }
190
-
191
- addNeighbor(nodeId: string): void {
192
- if (this.nodes.has(nodeId)) {
193
- this.node.neighbors.add(nodeId);
194
- }
195
- }
196
-
197
- removeNeighbor(nodeId: string): void {
198
- this.node.neighbors.delete(nodeId);
199
- }
200
-
201
- async propose(value: unknown): Promise<ConsensusProposal> {
202
- this.proposalCounter++;
203
- const proposalId = `gossip_${this.node.id}_${this.proposalCounter}`;
204
-
205
- const proposal: ConsensusProposal = {
206
- id: proposalId,
207
- proposerId: this.node.id,
208
- value,
209
- term: this.node.version,
210
- timestamp: new Date(),
211
- votes: new Map(),
212
- status: 'pending',
213
- };
214
-
215
- this.proposals.set(proposalId, proposal);
216
-
217
- // Self-vote
218
- proposal.votes.set(this.node.id, {
219
- voterId: this.node.id,
220
- approve: true,
221
- confidence: 1.0,
222
- timestamp: new Date(),
223
- });
224
-
225
- // Create gossip message
226
- const message: GossipMessage = {
227
- id: `msg_${proposalId}`,
228
- type: 'proposal',
229
- senderId: this.node.id,
230
- version: ++this.node.version,
231
- payload: { proposalId, value },
232
- timestamp: new Date(),
233
- ttl: this.config.maxHops ?? 10,
234
- hops: 0,
235
- path: [this.node.id],
236
- };
237
-
238
- // Queue for gossip
239
- this.queueMessage(message);
240
-
241
- return proposal;
242
- }
243
-
244
- async vote(proposalId: string, vote: ConsensusVote): Promise<void> {
245
- const proposal = this.proposals.get(proposalId);
246
- if (!proposal) {
247
- return;
248
- }
249
-
250
- proposal.votes.set(vote.voterId, vote);
251
-
252
- // Create vote gossip message
253
- const message: GossipMessage = {
254
- id: `vote_${proposalId}_${vote.voterId}`,
255
- type: 'vote',
256
- senderId: this.node.id,
257
- version: ++this.node.version,
258
- payload: { proposalId, vote },
259
- timestamp: new Date(),
260
- ttl: this.config.maxHops ?? 10,
261
- hops: 0,
262
- path: [this.node.id],
263
- };
264
-
265
- this.queueMessage(message);
266
-
267
- // Check convergence
268
- await this.checkConvergence(proposalId);
269
- }
270
-
271
- async awaitConsensus(proposalId: string): Promise<ConsensusResult> {
272
- const startTime = Date.now();
273
- const maxWait = this.config.timeoutMs ?? 30000;
274
-
275
- return new Promise((resolve, reject) => {
276
- const checkInterval = setInterval(() => {
277
- const proposal = this.proposals.get(proposalId);
278
- if (!proposal) {
279
- clearInterval(checkInterval);
280
- reject(new Error(`Proposal ${proposalId} not found`));
281
- return;
282
- }
283
-
284
- if (proposal.status !== 'pending') {
285
- clearInterval(checkInterval);
286
- resolve(this.createResult(proposal, Date.now() - startTime));
287
- return;
288
- }
289
-
290
- // Check convergence
291
- this.checkConvergence(proposalId);
292
-
293
- if (Date.now() - startTime > maxWait) {
294
- clearInterval(checkInterval);
295
- // Gossip is eventually consistent, so mark as accepted if threshold met
296
- const totalNodes = this.nodes.size + 1;
297
- const votes = proposal.votes.size;
298
- const threshold = this.config.convergenceThreshold ?? 0.9;
299
-
300
- if (votes / totalNodes >= threshold) {
301
- proposal.status = 'accepted';
302
- } else {
303
- proposal.status = 'expired';
304
- }
305
-
306
- resolve(this.createResult(proposal, Date.now() - startTime));
307
- }
308
- }, 50);
309
- });
310
- }
311
-
312
- // ===== GOSSIP PROTOCOL =====
313
-
314
- private startGossipLoop(): void {
315
- this.gossipInterval = setInterval(() => {
316
- this.gossipRound();
317
- }, this.config.gossipIntervalMs ?? 100);
318
- }
319
-
320
- private async gossipRound(): Promise<void> {
321
- if (this.messageQueue.length === 0) {
322
- return;
323
- }
324
-
325
- // Select random neighbors (fanout)
326
- const fanout = Math.min(
327
- this.config.fanout ?? 3,
328
- this.node.neighbors.size
329
- );
330
- const neighbors = this.selectRandomNeighbors(fanout);
331
-
332
- // Send queued messages to selected neighbors
333
- const messages = this.messageQueue.splice(0, 10); // Process up to 10 per round
334
-
335
- for (const message of messages) {
336
- for (const neighborId of neighbors) {
337
- await this.sendToNeighbor(neighborId, message);
338
- }
339
- }
340
-
341
- this.node.lastSync = new Date();
342
- }
343
-
344
- private selectRandomNeighbors(count: number): string[] {
345
- const neighbors = Array.from(this.node.neighbors);
346
- const selected: string[] = [];
347
-
348
- while (selected.length < count && neighbors.length > 0) {
349
- const idx = Math.floor(Math.random() * neighbors.length);
350
- selected.push(neighbors.splice(idx, 1)[0]);
351
- }
352
-
353
- return selected;
354
- }
355
-
356
- private async sendToNeighbor(neighborId: string, message: GossipMessage): Promise<void> {
357
- const deliveredMessage: GossipMessage = {
358
- ...message,
359
- hops: message.hops + 1,
360
- path: [...message.path, neighborId],
361
- };
362
-
363
- // ADR-095 G2 — over the transport when wired: actually send the gossip
364
- // message to the neighbor (signed by the transport if signing is on).
365
- // The emit stays for observability.
366
- if (this.transport) {
367
- this.emit('message.sent', { to: neighborId, message: deliveredMessage });
368
- try {
369
- await this.transport.send(neighborId, {
370
- type: 'gossip',
371
- payload: { ...deliveredMessage, timestamp: deliveredMessage.timestamp.toISOString() },
372
- });
373
- } catch {
374
- // Unreachable neighbor — gossip tolerates this; it'll converge via
375
- // other paths or the next gossip round.
376
- }
377
- return;
378
- }
379
-
380
- // Legacy in-process path — deliver to the fake neighbor state.
381
- const neighbor = this.nodes.get(neighborId);
382
- if (!neighbor) return;
383
- if (neighbor.seenMessages.has(message.id)) return;
384
- await this.processReceivedMessage(neighbor, deliveredMessage);
385
- this.emit('message.sent', { to: neighborId, message: deliveredMessage });
386
- }
387
-
388
- private async processReceivedMessage(
389
- node: GossipNode,
390
- message: GossipMessage
391
- ): Promise<void> {
392
- // Mark as seen
393
- node.seenMessages.add(message.id);
394
-
395
- // Check TTL
396
- if (message.ttl <= 0 || message.hops >= (this.config.maxHops ?? 10)) {
397
- return;
398
- }
399
-
400
- switch (message.type) {
401
- case 'proposal':
402
- await this.handleProposalMessage(node, message);
403
- break;
404
- case 'vote':
405
- await this.handleVoteMessage(node, message);
406
- break;
407
- case 'state':
408
- await this.handleStateMessage(node, message);
409
- break;
410
- }
411
-
412
- // Propagate to neighbors (gossip)
413
- if (message.hops < (this.config.maxHops ?? 10)) {
414
- const propagateMessage: GossipMessage = {
415
- ...message,
416
- ttl: message.ttl - 1,
417
- };
418
-
419
- // Add to queue if this is our node
420
- if (node.id === this.node.id) {
421
- this.queueMessage(propagateMessage);
422
- }
423
- }
424
- }
425
-
426
- private async handleProposalMessage(
427
- node: GossipNode,
428
- message: GossipMessage
429
- ): Promise<void> {
430
- const { proposalId, value } = message.payload as {
431
- proposalId: string;
432
- value: unknown;
433
- };
434
-
435
- if (!this.proposals.has(proposalId)) {
436
- const proposal: ConsensusProposal = {
437
- id: proposalId,
438
- proposerId: message.senderId,
439
- value,
440
- term: message.version,
441
- timestamp: message.timestamp,
442
- votes: new Map(),
443
- status: 'pending',
444
- };
445
-
446
- this.proposals.set(proposalId, proposal);
447
-
448
- // Auto-vote (simplified)
449
- if (node.id === this.node.id) {
450
- await this.vote(proposalId, {
451
- voterId: this.node.id,
452
- approve: true,
453
- confidence: 0.9,
454
- timestamp: new Date(),
455
- });
456
- }
457
- }
458
- }
459
-
460
- private async handleVoteMessage(
461
- node: GossipNode,
462
- message: GossipMessage
463
- ): Promise<void> {
464
- const { proposalId, vote } = message.payload as {
465
- proposalId: string;
466
- vote: ConsensusVote;
467
- };
468
-
469
- const proposal = this.proposals.get(proposalId);
470
- if (proposal && !proposal.votes.has(vote.voterId)) {
471
- proposal.votes.set(vote.voterId, vote);
472
- await this.checkConvergence(proposalId);
473
- }
474
- }
475
-
476
- private async handleStateMessage(
477
- node: GossipNode,
478
- message: GossipMessage
479
- ): Promise<void> {
480
- const state = message.payload as Record<string, unknown>;
481
-
482
- // Merge state (last-writer-wins)
483
- if (message.version > node.version) {
484
- for (const [key, value] of Object.entries(state)) {
485
- node.state.set(key, value);
486
- }
487
- node.version = message.version;
488
- }
489
- }
490
-
491
- private queueMessage(message: GossipMessage): void {
492
- // Avoid duplicates
493
- if (!this.node.seenMessages.has(message.id)) {
494
- this.node.seenMessages.add(message.id);
495
- this.messageQueue.push(message);
496
- }
497
- }
498
-
499
- private async checkConvergence(proposalId: string): Promise<void> {
500
- const proposal = this.proposals.get(proposalId);
501
- if (!proposal || proposal.status !== 'pending') {
502
- return;
503
- }
504
-
505
- const totalNodes = this.nodes.size + 1;
506
- const votes = proposal.votes.size;
507
- const threshold = this.config.convergenceThreshold ?? 0.9;
508
- const approvalThreshold = this.config.threshold ?? 0.66;
509
-
510
- // Check if we've converged (enough nodes have voted)
511
- if (votes / totalNodes >= threshold) {
512
- const approvingVotes = Array.from(proposal.votes.values()).filter(
513
- v => v.approve
514
- ).length;
515
-
516
- if (approvingVotes / votes >= approvalThreshold) {
517
- proposal.status = 'accepted';
518
- this.emit('consensus.achieved', { proposalId, approved: true });
519
- } else {
520
- proposal.status = 'rejected';
521
- this.emit('consensus.achieved', { proposalId, approved: false });
522
- }
523
- }
524
- }
525
-
526
- private createResult(proposal: ConsensusProposal, durationMs: number): ConsensusResult {
527
- const totalNodes = this.nodes.size + 1;
528
- const approvingVotes = Array.from(proposal.votes.values()).filter(
529
- v => v.approve
530
- ).length;
531
-
532
- return {
533
- proposalId: proposal.id,
534
- approved: proposal.status === 'accepted',
535
- approvalRate: proposal.votes.size > 0
536
- ? approvingVotes / proposal.votes.size
537
- : 0,
538
- participationRate: proposal.votes.size / totalNodes,
539
- finalValue: proposal.value,
540
- rounds: this.node.version,
541
- durationMs,
542
- };
543
- }
544
-
545
- // ===== STATE QUERIES =====
546
-
547
- getConvergence(proposalId: string): number {
548
- const proposal = this.proposals.get(proposalId);
549
- if (!proposal) return 0;
550
-
551
- const totalNodes = this.nodes.size + 1;
552
- return proposal.votes.size / totalNodes;
553
- }
554
-
555
- getVersion(): number {
556
- return this.node.version;
557
- }
558
-
559
- getNeighborCount(): number {
560
- return this.node.neighbors.size;
561
- }
562
-
563
- getSeenMessageCount(): number {
564
- return this.node.seenMessages.size;
565
- }
566
-
567
- getQueueDepth(): number {
568
- return this.messageQueue.length;
569
- }
570
-
571
- // Anti-entropy: sync full state with a random neighbor
572
- async antiEntropy(): Promise<void> {
573
- if (this.node.neighbors.size === 0) return;
574
-
575
- const neighbors = Array.from(this.node.neighbors);
576
- const randomNeighbor = neighbors[Math.floor(Math.random() * neighbors.length)];
577
-
578
- const stateMessage: GossipMessage = {
579
- id: `state_${this.node.id}_${Date.now()}`,
580
- type: 'state',
581
- senderId: this.node.id,
582
- version: this.node.version,
583
- payload: Object.fromEntries(this.node.state),
584
- timestamp: new Date(),
585
- ttl: 1,
586
- hops: 0,
587
- path: [this.node.id],
588
- };
589
-
590
- await this.sendToNeighbor(randomNeighbor, stateMessage);
591
- }
592
- }
593
-
594
- export function createGossipConsensus(
595
- nodeId: string,
596
- config?: GossipConfig
597
- ): GossipConsensus {
598
- return new GossipConsensus(nodeId, config);
599
- }