@claude-flow/swarm 3.0.0-alpha.8 → 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.
- package/dist/agent-pool.d.ts.map +1 -1
- package/dist/agent-pool.js +10 -2
- package/dist/agent-pool.js.map +1 -1
- package/dist/attention-coordinator.d.ts +5 -4
- package/dist/attention-coordinator.d.ts.map +1 -1
- package/dist/attention-coordinator.js +17 -10
- package/dist/attention-coordinator.js.map +1 -1
- package/dist/consensus/byzantine.d.ts +2 -1
- package/dist/consensus/byzantine.d.ts.map +1 -1
- package/dist/consensus/byzantine.js +34 -8
- package/dist/consensus/byzantine.js.map +1 -1
- package/dist/consensus/gossip.d.ts +2 -1
- package/dist/consensus/gossip.d.ts.map +1 -1
- package/dist/consensus/gossip.js +27 -8
- package/dist/consensus/gossip.js.map +1 -1
- package/dist/consensus/index.d.ts +1 -1
- package/dist/consensus/index.d.ts.map +1 -1
- package/dist/consensus/index.js +2 -30
- package/dist/consensus/index.js.map +1 -1
- package/dist/consensus/raft.d.ts +2 -1
- package/dist/consensus/raft.d.ts.map +1 -1
- package/dist/consensus/raft.js +25 -9
- package/dist/consensus/raft.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/message-bus.d.ts.map +1 -1
- package/dist/message-bus.js +13 -4
- package/dist/message-bus.js.map +1 -1
- package/dist/queen-coordinator.d.ts +1 -1
- package/dist/queen-coordinator.d.ts.map +1 -1
- package/dist/queen-coordinator.js +10 -3
- package/dist/queen-coordinator.js.map +1 -1
- package/dist/topology-manager.d.ts.map +1 -1
- package/dist/topology-manager.js +26 -1
- package/dist/topology-manager.js.map +1 -1
- package/dist/types.d.ts +3 -12
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/unified-coordinator.d.ts +1 -1
- package/dist/unified-coordinator.d.ts.map +1 -1
- package/dist/unified-coordinator.js +85 -29
- package/dist/unified-coordinator.js.map +1 -1
- package/package.json +7 -2
- package/.claude-flow/data/pending-insights.jsonl +0 -15
- package/.claude-flow/hive-mind/state.json +0 -18
- package/__tests__/byzantine-transport.test.ts +0 -147
- package/__tests__/consensus-failure-injection.test.ts +0 -161
- package/__tests__/consensus-transport.test.ts +0 -200
- package/__tests__/consensus.test.ts +0 -577
- package/__tests__/coordinator.test.ts +0 -501
- package/__tests__/federation-transport.test.ts +0 -145
- package/__tests__/gossip-transport.test.ts +0 -81
- package/__tests__/queen-coordinator.test.ts +0 -1335
- package/__tests__/raft-transport.test.ts +0 -121
- package/__tests__/topology.test.ts +0 -621
- package/ruvector.db +0 -0
- package/src/agent-pool.ts +0 -476
- package/src/application/commands/create-task.command.ts +0 -124
- package/src/application/commands/spawn-agent.command.ts +0 -122
- package/src/application/index.ts +0 -30
- package/src/application/services/swarm-application-service.ts +0 -200
- package/src/attention-coordinator.ts +0 -1000
- package/src/consensus/byzantine.ts +0 -514
- package/src/consensus/federation-transport.ts +0 -185
- package/src/consensus/gossip.ts +0 -599
- package/src/consensus/index.ts +0 -326
- package/src/consensus/raft.ts +0 -561
- package/src/consensus/transport.ts +0 -284
- package/src/coordination/agent-registry.ts +0 -544
- package/src/coordination/index.ts +0 -23
- package/src/coordination/swarm-hub.ts +0 -776
- package/src/coordination/task-orchestrator.ts +0 -605
- package/src/domain/entities/agent.d.ts +0 -151
- package/src/domain/entities/agent.d.ts.map +0 -1
- package/src/domain/entities/agent.js +0 -280
- package/src/domain/entities/agent.js.map +0 -1
- package/src/domain/entities/agent.ts +0 -370
- package/src/domain/entities/task.d.ts +0 -133
- package/src/domain/entities/task.d.ts.map +0 -1
- package/src/domain/entities/task.js +0 -261
- package/src/domain/entities/task.js.map +0 -1
- package/src/domain/entities/task.ts +0 -319
- package/src/domain/index.ts +0 -41
- package/src/domain/repositories/agent-repository.interface.d.ts +0 -57
- package/src/domain/repositories/agent-repository.interface.d.ts.map +0 -1
- package/src/domain/repositories/agent-repository.interface.js +0 -9
- package/src/domain/repositories/agent-repository.interface.js.map +0 -1
- package/src/domain/repositories/agent-repository.interface.ts +0 -69
- package/src/domain/repositories/task-repository.interface.d.ts +0 -61
- package/src/domain/repositories/task-repository.interface.d.ts.map +0 -1
- package/src/domain/repositories/task-repository.interface.js +0 -9
- package/src/domain/repositories/task-repository.interface.js.map +0 -1
- package/src/domain/repositories/task-repository.interface.ts +0 -75
- package/src/domain/services/coordination-service.ts +0 -320
- package/src/federation-hub.d.ts +0 -284
- package/src/federation-hub.d.ts.map +0 -1
- package/src/federation-hub.js +0 -692
- package/src/federation-hub.js.map +0 -1
- package/src/federation-hub.ts +0 -979
- package/src/index.ts +0 -374
- package/src/message-bus.ts +0 -607
- package/src/queen-coordinator.ts +0 -2025
- package/src/shared/events.ts +0 -285
- package/src/shared/types.ts +0 -389
- package/src/topology-manager.ts +0 -656
- package/src/types.ts +0 -555
- package/src/unified-coordinator.ts +0 -1844
- package/src/workers/index.ts +0 -65
- package/src/workers/worker-dispatch.d.ts +0 -234
- package/src/workers/worker-dispatch.d.ts.map +0 -1
- package/src/workers/worker-dispatch.js +0 -842
- package/src/workers/worker-dispatch.js.map +0 -1
- package/src/workers/worker-dispatch.ts +0 -1076
- package/tmp.json +0 -0
- package/tsconfig.json +0 -9
- package/tsconfig.tsbuildinfo +0 -1
- package/vitest.config.ts +0 -20
package/src/consensus/gossip.ts
DELETED
|
@@ -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
|
-
}
|