claude-flow 3.5.21 → 3.5.22
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/.claude/helpers/hook-handler.cjs +4 -2
- package/package.json +1 -1
- package/v3/@claude-flow/cli/dist/src/commands/hooks.js +698 -55
- package/v3/@claude-flow/cli/dist/src/commands/neural.js +11 -5
- package/v3/@claude-flow/cli/dist/src/index.d.ts +1 -1
- package/v3/@claude-flow/cli/dist/src/index.js +2 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/coordination-tools.js +191 -12
- package/v3/@claude-flow/cli/dist/src/mcp-tools/hive-mind-tools.js +224 -23
- package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +1 -0
- package/v3/@claude-flow/cli/dist/src/memory/ewc-consolidation.d.ts +24 -0
- package/v3/@claude-flow/cli/dist/src/memory/ewc-consolidation.js +59 -0
- package/v3/@claude-flow/cli/dist/src/memory/intelligence.d.ts +53 -0
- package/v3/@claude-flow/cli/dist/src/memory/intelligence.js +225 -0
- package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.d.ts +7 -0
- package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +27 -1
- package/v3/@claude-flow/cli/dist/src/ruvector/index.d.ts +4 -0
- package/v3/@claude-flow/cli/dist/src/ruvector/index.js +12 -0
- package/v3/@claude-flow/cli/dist/src/services/ruvector-training.d.ts +9 -1
- package/v3/@claude-flow/cli/dist/src/services/ruvector-training.js +223 -39
- package/v3/@claude-flow/cli/dist/src/services/worker-daemon.d.ts +4 -0
- package/v3/@claude-flow/cli/dist/src/services/worker-daemon.js +33 -5
- package/v3/@claude-flow/cli/package.json +1 -1
|
@@ -68,7 +68,8 @@ const trainCommand = {
|
|
|
68
68
|
});
|
|
69
69
|
if (initResult.success) {
|
|
70
70
|
wasmFeatures = initResult.features;
|
|
71
|
-
|
|
71
|
+
const backendLabel = initResult.backend === 'wasm' ? 'WASM' : 'JS fallback';
|
|
72
|
+
spinner.setText(`RuVector initialized [${backendLabel}]: ${wasmFeatures.join(', ')}`);
|
|
72
73
|
}
|
|
73
74
|
else {
|
|
74
75
|
output.writeln(output.warning(`WASM init failed: ${initResult.error} - falling back`));
|
|
@@ -274,7 +275,8 @@ const trainCommand = {
|
|
|
274
275
|
];
|
|
275
276
|
// Add WASM-specific metrics
|
|
276
277
|
if (useWasm && wasmFeatures.length > 0) {
|
|
277
|
-
|
|
278
|
+
const backendUsed = ruvectorStats?.backend || 'unknown';
|
|
279
|
+
tableData.push({ metric: 'Backend', value: backendUsed === 'wasm' ? 'WASM (native)' : 'JS (fallback)' }, { metric: 'WASM Features', value: wasmFeatures.slice(0, 3).join(', ') }, { metric: 'LoRA Adaptations', value: String(adaptations) }, { metric: 'Avg Loss', value: (totalLoss / Math.max(1, epochs)).toFixed(4) });
|
|
278
280
|
if (ruvectorStats?.microLoraStats) {
|
|
279
281
|
tableData.push({ metric: 'MicroLoRA Delta Norm', value: ruvectorStats.microLoraStats.deltaNorm.toFixed(6) });
|
|
280
282
|
}
|
|
@@ -299,7 +301,11 @@ const trainCommand = {
|
|
|
299
301
|
output.writeln();
|
|
300
302
|
output.writeln(output.success(`✓ ${patternsRecorded} patterns saved to ${persistence.patternsFile}`));
|
|
301
303
|
if (useWasm && wasmFeatures.length > 0) {
|
|
302
|
-
|
|
304
|
+
const backendUsed = ruvectorStats?.backend || 'unknown';
|
|
305
|
+
const backendMsg = backendUsed === 'wasm'
|
|
306
|
+
? `RuVector WASM backend: ${wasmFeatures.join(', ')}`
|
|
307
|
+
: `RuVector JS fallback (install @ruvector/learning-wasm for native speed): ${wasmFeatures.join(', ')}`;
|
|
308
|
+
output.writeln(output.highlight(`✓ ${backendMsg}`));
|
|
303
309
|
}
|
|
304
310
|
return {
|
|
305
311
|
success: true,
|
|
@@ -375,10 +381,10 @@ const statusCommand = {
|
|
|
375
381
|
: 'Not initialized',
|
|
376
382
|
},
|
|
377
383
|
{
|
|
378
|
-
component: 'RuVector
|
|
384
|
+
component: 'RuVector Training',
|
|
379
385
|
status: ruvectorStats.initialized ? output.success('Active') : output.dim('Not loaded'),
|
|
380
386
|
details: ruvectorStats.initialized
|
|
381
|
-
?
|
|
387
|
+
? `${ruvectorStats.backend === 'wasm' ? 'WASM' : 'JS fallback'} | MicroLoRA: ${ruvectorStats.totalAdaptations} adapts`
|
|
382
388
|
: 'Call neural train to initialize',
|
|
383
389
|
},
|
|
384
390
|
{
|
|
@@ -59,7 +59,7 @@ export * from './prompt.js';
|
|
|
59
59
|
export * from './commands/index.js';
|
|
60
60
|
export { MCPServerManager, createMCPServerManager, getServerManager, startMCPServer, stopMCPServer, getMCPServerStatus, type MCPServerOptions, type MCPServerStatus, } from './mcp-server.js';
|
|
61
61
|
export { initializeMemoryDatabase, generateEmbedding, generateBatchEmbeddings, storeEntry, searchEntries, getHNSWIndex, addToHNSWIndex, searchHNSWIndex, getHNSWStatus, clearHNSWIndex, quantizeInt8, dequantizeInt8, quantizedCosineSim, getQuantizationStats, batchCosineSim, softmaxAttention, topKIndices, flashAttentionSearch, type MemoryInitResult, } from './memory/memory-initializer.js';
|
|
62
|
-
export { initializeIntelligence, recordStep, recordTrajectory, findSimilarPatterns, getIntelligenceStats, getSonaCoordinator, getReasoningBank, clearIntelligence, benchmarkAdaptation, getAllPatterns, getPatternsByType, flushPatterns, deletePattern, clearAllPatterns, getNeuralDataDir, getPersistenceStatus, type SonaConfig, type TrajectoryStep, type Pattern, type IntelligenceStats, } from './memory/intelligence.js';
|
|
62
|
+
export { initializeIntelligence, recordStep, recordTrajectory, findSimilarPatterns, getIntelligenceStats, getSonaCoordinator, getReasoningBank, clearIntelligence, benchmarkAdaptation, endTrajectoryWithVerdict, distillLearning, getAllPatterns, getPatternsByType, flushPatterns, deletePattern, clearAllPatterns, getNeuralDataDir, getPersistenceStatus, type SonaConfig, type TrajectoryStep, type Pattern, type IntelligenceStats, } from './memory/intelligence.js';
|
|
63
63
|
export { EWCConsolidator, getEWCConsolidator, resetEWCConsolidator, consolidatePatterns, recordPatternOutcome, getEWCStats, type PatternWeights, type EWCConfig, type ConsolidationResult, type EWCStats, } from './memory/ewc-consolidation.js';
|
|
64
64
|
export { SONAOptimizer, getSONAOptimizer, resetSONAOptimizer, processTrajectory, getSuggestion, getSONAStats, type TrajectoryOutcome, type LearnedPattern, type RoutingSuggestion, type SONAStats, } from './memory/sona-optimizer.js';
|
|
65
65
|
export { ErrorHandler, withErrorHandling, } from './production/error-handler.js';
|
|
@@ -453,6 +453,8 @@ export { initializeMemoryDatabase, generateEmbedding, generateBatchEmbeddings, s
|
|
|
453
453
|
// Flash Attention-style batch operations
|
|
454
454
|
batchCosineSim, softmaxAttention, topKIndices, flashAttentionSearch, } from './memory/memory-initializer.js';
|
|
455
455
|
export { initializeIntelligence, recordStep, recordTrajectory, findSimilarPatterns, getIntelligenceStats, getSonaCoordinator, getReasoningBank, clearIntelligence, benchmarkAdaptation,
|
|
456
|
+
// RL loop API
|
|
457
|
+
endTrajectoryWithVerdict, distillLearning,
|
|
456
458
|
// Pattern persistence API
|
|
457
459
|
getAllPatterns, getPatternsByType, flushPatterns, deletePattern, clearAllPatterns, getNeuralDataDir, getPersistenceStatus, } from './memory/intelligence.js';
|
|
458
460
|
// EWC++ Consolidation (Prevents Catastrophic Forgetting)
|
|
@@ -350,55 +350,234 @@ export const coordinationTools = [
|
|
|
350
350
|
},
|
|
351
351
|
{
|
|
352
352
|
name: 'coordination_consensus',
|
|
353
|
-
description: 'Manage consensus protocol',
|
|
353
|
+
description: 'Manage consensus protocol with BFT, Raft, or Quorum strategies',
|
|
354
354
|
category: 'coordination',
|
|
355
355
|
inputSchema: {
|
|
356
356
|
type: 'object',
|
|
357
357
|
properties: {
|
|
358
358
|
action: { type: 'string', enum: ['status', 'propose', 'vote', 'commit'], description: 'Action to perform' },
|
|
359
|
-
proposal: { type: 'object', description: 'Proposal data' },
|
|
359
|
+
proposal: { type: 'object', description: 'Proposal data (for propose)' },
|
|
360
|
+
proposalId: { type: 'string', description: 'Proposal ID (for vote/commit/status)' },
|
|
360
361
|
vote: { type: 'string', enum: ['accept', 'reject'], description: 'Vote' },
|
|
362
|
+
voterId: { type: 'string', description: 'Voter node ID' },
|
|
363
|
+
strategy: { type: 'string', enum: ['bft', 'raft', 'quorum'], description: 'Consensus strategy (default: raft)' },
|
|
364
|
+
quorumPreset: { type: 'string', enum: ['unanimous', 'majority', 'supermajority'], description: 'Quorum threshold preset (default: majority)' },
|
|
365
|
+
term: { type: 'number', description: 'Term number (for raft strategy)' },
|
|
361
366
|
},
|
|
362
367
|
},
|
|
363
368
|
handler: async (input) => {
|
|
364
369
|
const store = loadCoordStore();
|
|
365
370
|
const action = input.action || 'status';
|
|
371
|
+
const strategy = input.strategy || 'raft';
|
|
372
|
+
const nodeCount = Object.keys(store.nodes).length || 1;
|
|
373
|
+
// Initialize consensus storage in the coordination store if missing
|
|
374
|
+
if (!store.consensus) {
|
|
375
|
+
store.consensus = { pending: [], history: [] };
|
|
376
|
+
}
|
|
377
|
+
const consensus = store.consensus;
|
|
378
|
+
function calcRequired(strat, total, preset) {
|
|
379
|
+
if (total <= 0)
|
|
380
|
+
return 1;
|
|
381
|
+
if (strat === 'bft')
|
|
382
|
+
return Math.floor((total * 2) / 3) + 1;
|
|
383
|
+
if (strat === 'quorum') {
|
|
384
|
+
if (preset === 'unanimous')
|
|
385
|
+
return total;
|
|
386
|
+
if (preset === 'supermajority')
|
|
387
|
+
return Math.floor((total * 2) / 3) + 1;
|
|
388
|
+
}
|
|
389
|
+
return Math.floor(total / 2) + 1;
|
|
390
|
+
}
|
|
366
391
|
if (action === 'status') {
|
|
367
|
-
|
|
368
|
-
|
|
392
|
+
if (input.proposalId) {
|
|
393
|
+
// Status for specific proposal
|
|
394
|
+
const p = consensus.pending.find(x => x.proposalId === input.proposalId);
|
|
395
|
+
if (p) {
|
|
396
|
+
const votesFor = Object.values(p.votes).filter(v => v).length;
|
|
397
|
+
const votesAgainst = Object.values(p.votes).filter(v => !v).length;
|
|
398
|
+
return {
|
|
399
|
+
success: true,
|
|
400
|
+
proposalId: p.proposalId,
|
|
401
|
+
strategy: p.strategy,
|
|
402
|
+
status: p.status,
|
|
403
|
+
votesFor,
|
|
404
|
+
votesAgainst,
|
|
405
|
+
required: calcRequired(p.strategy, nodeCount, p.quorumPreset),
|
|
406
|
+
totalNodes: nodeCount,
|
|
407
|
+
resolved: false,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
const h = consensus.history.find(x => x.proposalId === input.proposalId);
|
|
411
|
+
if (h)
|
|
412
|
+
return { success: true, ...h, resolved: true, historical: true };
|
|
413
|
+
return { success: false, error: 'Proposal not found' };
|
|
414
|
+
}
|
|
415
|
+
const quorum = calcRequired(strategy, nodeCount);
|
|
369
416
|
return {
|
|
370
417
|
success: true,
|
|
371
418
|
algorithm: store.topology.consensusAlgorithm,
|
|
419
|
+
strategy,
|
|
372
420
|
nodes: nodeCount,
|
|
373
421
|
quorum,
|
|
422
|
+
pendingProposals: consensus.pending.length,
|
|
423
|
+
resolvedProposals: consensus.history.length,
|
|
374
424
|
status: nodeCount >= quorum ? 'operational' : 'degraded',
|
|
375
425
|
};
|
|
376
426
|
}
|
|
377
427
|
if (action === 'propose') {
|
|
378
|
-
const proposalId = `proposal-${Date.now()}`;
|
|
428
|
+
const proposalId = `proposal-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
429
|
+
const quorumPreset = input.quorumPreset || 'majority';
|
|
430
|
+
const term = input.term || 1;
|
|
431
|
+
const required = calcRequired(strategy, nodeCount, quorumPreset);
|
|
432
|
+
// Raft: one pending proposal per term
|
|
433
|
+
if (strategy === 'raft') {
|
|
434
|
+
const existing = consensus.pending.find(p => p.strategy === 'raft' && p.term === term);
|
|
435
|
+
if (existing) {
|
|
436
|
+
return {
|
|
437
|
+
success: false,
|
|
438
|
+
error: `Raft term ${term} already has pending proposal: ${existing.proposalId}`,
|
|
439
|
+
existingProposalId: existing.proposalId,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
consensus.pending.push({
|
|
444
|
+
proposalId,
|
|
445
|
+
type: 'coordination',
|
|
446
|
+
proposal: input.proposal,
|
|
447
|
+
proposedBy: input.voterId || 'system',
|
|
448
|
+
proposedAt: new Date().toISOString(),
|
|
449
|
+
votes: {},
|
|
450
|
+
status: 'pending',
|
|
451
|
+
strategy,
|
|
452
|
+
term: strategy === 'raft' ? term : undefined,
|
|
453
|
+
quorumPreset: strategy === 'quorum' ? quorumPreset : undefined,
|
|
454
|
+
byzantineVoters: strategy === 'bft' ? [] : undefined,
|
|
455
|
+
});
|
|
456
|
+
saveCoordStore(store);
|
|
379
457
|
return {
|
|
380
458
|
success: true,
|
|
381
459
|
action: 'proposed',
|
|
382
460
|
proposalId,
|
|
383
461
|
proposal: input.proposal,
|
|
462
|
+
strategy,
|
|
384
463
|
status: 'pending',
|
|
385
|
-
|
|
464
|
+
required,
|
|
465
|
+
totalNodes: nodeCount,
|
|
466
|
+
term: strategy === 'raft' ? term : undefined,
|
|
386
467
|
};
|
|
387
468
|
}
|
|
388
469
|
if (action === 'vote') {
|
|
470
|
+
const p = consensus.pending.find(x => x.proposalId === input.proposalId);
|
|
471
|
+
if (!p)
|
|
472
|
+
return { success: false, error: 'Proposal not found or already resolved' };
|
|
473
|
+
const voterId = input.voterId;
|
|
474
|
+
if (!voterId)
|
|
475
|
+
return { success: false, error: 'voterId is required' };
|
|
476
|
+
const voteValue = input.vote === 'accept';
|
|
477
|
+
const pStrategy = p.strategy || 'raft';
|
|
478
|
+
const required = calcRequired(pStrategy, nodeCount, p.quorumPreset);
|
|
479
|
+
// Double-vote prevention
|
|
480
|
+
if (voterId in p.votes) {
|
|
481
|
+
if (pStrategy === 'bft' && p.votes[voterId] !== voteValue) {
|
|
482
|
+
if (!p.byzantineVoters)
|
|
483
|
+
p.byzantineVoters = [];
|
|
484
|
+
if (!p.byzantineVoters.includes(voterId))
|
|
485
|
+
p.byzantineVoters.push(voterId);
|
|
486
|
+
delete p.votes[voterId];
|
|
487
|
+
saveCoordStore(store);
|
|
488
|
+
return {
|
|
489
|
+
success: false,
|
|
490
|
+
byzantineDetected: true,
|
|
491
|
+
message: `Byzantine behavior: voter ${voterId} attempted conflicting vote. Vote invalidated.`,
|
|
492
|
+
byzantineVoters: p.byzantineVoters,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
return { success: false, error: `Voter ${voterId} has already voted on this proposal` };
|
|
496
|
+
}
|
|
497
|
+
// BFT cross-proposal conflict check
|
|
498
|
+
if (pStrategy === 'bft') {
|
|
499
|
+
for (const other of consensus.pending) {
|
|
500
|
+
if (other.proposalId === p.proposalId)
|
|
501
|
+
continue;
|
|
502
|
+
if (voterId in other.votes && other.votes[voterId] !== voteValue) {
|
|
503
|
+
if (!p.byzantineVoters)
|
|
504
|
+
p.byzantineVoters = [];
|
|
505
|
+
if (!p.byzantineVoters.includes(voterId))
|
|
506
|
+
p.byzantineVoters.push(voterId);
|
|
507
|
+
saveCoordStore(store);
|
|
508
|
+
return {
|
|
509
|
+
success: false,
|
|
510
|
+
byzantineDetected: true,
|
|
511
|
+
message: `Byzantine behavior: voter ${voterId} cast conflicting votes across proposals.`,
|
|
512
|
+
byzantineVoters: p.byzantineVoters,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
p.votes[voterId] = voteValue;
|
|
518
|
+
const votesFor = Object.values(p.votes).filter(v => v).length;
|
|
519
|
+
const votesAgainst = Object.values(p.votes).filter(v => !v).length;
|
|
520
|
+
// Resolution check
|
|
521
|
+
let resolved = false;
|
|
522
|
+
let result;
|
|
523
|
+
if (votesFor >= required) {
|
|
524
|
+
resolved = true;
|
|
525
|
+
result = 'approved';
|
|
526
|
+
}
|
|
527
|
+
else if (votesAgainst >= required) {
|
|
528
|
+
resolved = true;
|
|
529
|
+
result = 'rejected';
|
|
530
|
+
}
|
|
531
|
+
else if (pStrategy === 'quorum' && p.quorumPreset === 'unanimous' && votesAgainst > 0) {
|
|
532
|
+
resolved = true;
|
|
533
|
+
result = 'rejected';
|
|
534
|
+
}
|
|
535
|
+
if (resolved && result) {
|
|
536
|
+
p.status = result;
|
|
537
|
+
consensus.history.push({
|
|
538
|
+
proposalId: p.proposalId,
|
|
539
|
+
result,
|
|
540
|
+
votes: { for: votesFor, against: votesAgainst },
|
|
541
|
+
decidedAt: new Date().toISOString(),
|
|
542
|
+
strategy: pStrategy,
|
|
543
|
+
term: p.term,
|
|
544
|
+
byzantineDetected: p.byzantineVoters?.length ? p.byzantineVoters : undefined,
|
|
545
|
+
});
|
|
546
|
+
consensus.pending = consensus.pending.filter(x => x.proposalId !== p.proposalId);
|
|
547
|
+
}
|
|
548
|
+
saveCoordStore(store);
|
|
389
549
|
return {
|
|
390
550
|
success: true,
|
|
391
551
|
action: 'voted',
|
|
552
|
+
proposalId: p.proposalId,
|
|
553
|
+
voterId,
|
|
392
554
|
vote: input.vote,
|
|
393
|
-
|
|
555
|
+
strategy: pStrategy,
|
|
556
|
+
votesFor,
|
|
557
|
+
votesAgainst,
|
|
558
|
+
required,
|
|
559
|
+
totalNodes: nodeCount,
|
|
560
|
+
resolved,
|
|
561
|
+
result: resolved ? result : undefined,
|
|
562
|
+
status: p.status,
|
|
394
563
|
};
|
|
395
564
|
}
|
|
396
565
|
if (action === 'commit') {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
566
|
+
// Commit is a no-op confirmation for already-resolved proposals
|
|
567
|
+
if (input.proposalId) {
|
|
568
|
+
const h = consensus.history.find(x => x.proposalId === input.proposalId);
|
|
569
|
+
if (h) {
|
|
570
|
+
return {
|
|
571
|
+
success: true,
|
|
572
|
+
action: 'committed',
|
|
573
|
+
proposalId: input.proposalId,
|
|
574
|
+
result: h.result,
|
|
575
|
+
committedAt: new Date().toISOString(),
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
return { success: false, error: 'Proposal not found in resolved history. Vote must reach quorum first.' };
|
|
579
|
+
}
|
|
580
|
+
return { success: false, error: 'proposalId is required for commit' };
|
|
402
581
|
}
|
|
403
582
|
return { success: false, error: 'Unknown action' };
|
|
404
583
|
},
|
|
@@ -9,6 +9,78 @@ import { join } from 'node:path';
|
|
|
9
9
|
const STORAGE_DIR = '.claude-flow';
|
|
10
10
|
const HIVE_DIR = 'hive-mind';
|
|
11
11
|
const HIVE_FILE = 'state.json';
|
|
12
|
+
/**
|
|
13
|
+
* Calculate required votes for a given strategy and total node count.
|
|
14
|
+
*/
|
|
15
|
+
function calculateRequiredVotes(strategy, totalNodes, quorumPreset = 'majority') {
|
|
16
|
+
if (totalNodes <= 0)
|
|
17
|
+
return 1;
|
|
18
|
+
switch (strategy) {
|
|
19
|
+
case 'bft':
|
|
20
|
+
// BFT: requires 2/3 + 1 of total nodes
|
|
21
|
+
return Math.floor((totalNodes * 2) / 3) + 1;
|
|
22
|
+
case 'raft':
|
|
23
|
+
// Raft: simple majority
|
|
24
|
+
return Math.floor(totalNodes / 2) + 1;
|
|
25
|
+
case 'quorum':
|
|
26
|
+
switch (quorumPreset) {
|
|
27
|
+
case 'unanimous':
|
|
28
|
+
return totalNodes;
|
|
29
|
+
case 'supermajority':
|
|
30
|
+
return Math.floor((totalNodes * 2) / 3) + 1;
|
|
31
|
+
case 'majority':
|
|
32
|
+
default:
|
|
33
|
+
return Math.floor(totalNodes / 2) + 1;
|
|
34
|
+
}
|
|
35
|
+
default:
|
|
36
|
+
return Math.floor(totalNodes / 2) + 1;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Detect Byzantine behavior: a voter who has cast conflicting votes
|
|
41
|
+
* across proposals in the same round (same type, overlapping time).
|
|
42
|
+
* Here we check if the voter already voted differently on this proposal
|
|
43
|
+
* (which shouldn't happen if we block double-votes, so this checks
|
|
44
|
+
* cross-proposal conflicting votes for same type within the pending set).
|
|
45
|
+
*/
|
|
46
|
+
function detectByzantineVoters(pending, currentProposal, voterId, newVote) {
|
|
47
|
+
// Check if voter cast opposite votes on proposals of the same type
|
|
48
|
+
for (const p of pending) {
|
|
49
|
+
if (p.proposalId === currentProposal.proposalId)
|
|
50
|
+
continue;
|
|
51
|
+
if (p.type !== currentProposal.type)
|
|
52
|
+
continue;
|
|
53
|
+
if (voterId in p.votes && p.votes[voterId] !== newVote) {
|
|
54
|
+
return true; // Conflicting vote detected
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Try to resolve a proposal based on its strategy.
|
|
61
|
+
* Returns 'approved', 'rejected', or null if still pending.
|
|
62
|
+
*/
|
|
63
|
+
function tryResolveProposal(proposal, totalNodes) {
|
|
64
|
+
const votesFor = Object.values(proposal.votes).filter(v => v).length;
|
|
65
|
+
const votesAgainst = Object.values(proposal.votes).filter(v => !v).length;
|
|
66
|
+
const required = calculateRequiredVotes(proposal.strategy, totalNodes, proposal.quorumPreset);
|
|
67
|
+
if (votesFor >= required)
|
|
68
|
+
return 'approved';
|
|
69
|
+
if (votesAgainst >= required)
|
|
70
|
+
return 'rejected';
|
|
71
|
+
// For quorum with 'unanimous', also reject if any vote is against
|
|
72
|
+
if (proposal.strategy === 'quorum' && proposal.quorumPreset === 'unanimous' && votesAgainst > 0) {
|
|
73
|
+
return 'rejected';
|
|
74
|
+
}
|
|
75
|
+
// Check if it's impossible to reach quorum (remaining potential votes can't tip it)
|
|
76
|
+
const totalVotes = Object.keys(proposal.votes).length;
|
|
77
|
+
const remaining = totalNodes - totalVotes;
|
|
78
|
+
if (votesFor + remaining < required && votesAgainst + remaining < required) {
|
|
79
|
+
// Deadlock: neither side can win -- reject
|
|
80
|
+
return 'rejected';
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
12
84
|
function getHiveDir() {
|
|
13
85
|
return join(process.cwd(), STORAGE_DIR, HIVE_DIR);
|
|
14
86
|
}
|
|
@@ -300,7 +372,7 @@ export const hiveMindTools = [
|
|
|
300
372
|
},
|
|
301
373
|
{
|
|
302
374
|
name: 'hive-mind_consensus',
|
|
303
|
-
description: 'Propose or vote on consensus',
|
|
375
|
+
description: 'Propose or vote on consensus with BFT, Raft, or Quorum strategies',
|
|
304
376
|
category: 'hive-mind',
|
|
305
377
|
inputSchema: {
|
|
306
378
|
type: 'object',
|
|
@@ -311,14 +383,36 @@ export const hiveMindTools = [
|
|
|
311
383
|
value: { description: 'Proposal value (for propose)' },
|
|
312
384
|
vote: { type: 'boolean', description: 'Vote (true=for, false=against)' },
|
|
313
385
|
voterId: { type: 'string', description: 'Voter agent ID' },
|
|
386
|
+
strategy: { type: 'string', enum: ['bft', 'raft', 'quorum'], description: 'Consensus strategy (default: raft)' },
|
|
387
|
+
quorumPreset: { type: 'string', enum: ['unanimous', 'majority', 'supermajority'], description: 'Quorum threshold preset (for quorum strategy, default: majority)' },
|
|
388
|
+
term: { type: 'number', description: 'Term number (for raft strategy)' },
|
|
389
|
+
timeoutMs: { type: 'number', description: 'Timeout in ms for raft re-proposal (default: 30000)' },
|
|
314
390
|
},
|
|
315
391
|
required: ['action'],
|
|
316
392
|
},
|
|
317
393
|
handler: async (input) => {
|
|
318
394
|
const state = loadHiveState();
|
|
319
395
|
const action = input.action;
|
|
396
|
+
const strategy = input.strategy || 'raft';
|
|
397
|
+
const totalNodes = state.workers.length || 1;
|
|
320
398
|
if (action === 'propose') {
|
|
321
399
|
const proposalId = `proposal-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
400
|
+
const quorumPreset = input.quorumPreset || 'majority';
|
|
401
|
+
const term = input.term || (state.queen?.term ?? 1);
|
|
402
|
+
const timeoutMs = input.timeoutMs || 30000;
|
|
403
|
+
// Raft: check if there's already a pending proposal for this term
|
|
404
|
+
if (strategy === 'raft') {
|
|
405
|
+
const existingTermProposal = state.consensus.pending.find(p => p.strategy === 'raft' && p.term === term && p.status === 'pending');
|
|
406
|
+
if (existingTermProposal) {
|
|
407
|
+
return {
|
|
408
|
+
action,
|
|
409
|
+
error: `Raft term ${term} already has a pending proposal: ${existingTermProposal.proposalId}. Wait for resolution or use a higher term.`,
|
|
410
|
+
existingProposalId: existingTermProposal.proposalId,
|
|
411
|
+
term,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
const required = calculateRequiredVotes(strategy, totalNodes, quorumPreset);
|
|
322
416
|
const proposal = {
|
|
323
417
|
proposalId,
|
|
324
418
|
type: input.type || 'general',
|
|
@@ -327,6 +421,11 @@ export const hiveMindTools = [
|
|
|
327
421
|
proposedAt: new Date().toISOString(),
|
|
328
422
|
votes: {},
|
|
329
423
|
status: 'pending',
|
|
424
|
+
strategy,
|
|
425
|
+
term: strategy === 'raft' ? term : undefined,
|
|
426
|
+
quorumPreset: strategy === 'quorum' ? quorumPreset : undefined,
|
|
427
|
+
byzantineVoters: strategy === 'bft' ? [] : undefined,
|
|
428
|
+
timeoutAt: strategy === 'raft' ? new Date(Date.now() + timeoutMs).toISOString() : undefined,
|
|
330
429
|
};
|
|
331
430
|
state.consensus.pending.push(proposal);
|
|
332
431
|
saveHiveState(state);
|
|
@@ -334,40 +433,115 @@ export const hiveMindTools = [
|
|
|
334
433
|
action,
|
|
335
434
|
proposalId,
|
|
336
435
|
type: proposal.type,
|
|
436
|
+
strategy,
|
|
337
437
|
status: 'pending',
|
|
338
|
-
|
|
438
|
+
required,
|
|
439
|
+
totalNodes,
|
|
440
|
+
term: proposal.term,
|
|
441
|
+
quorumPreset: proposal.quorumPreset,
|
|
442
|
+
timeoutAt: proposal.timeoutAt,
|
|
339
443
|
};
|
|
340
444
|
}
|
|
341
445
|
if (action === 'vote') {
|
|
342
446
|
const proposal = state.consensus.pending.find(p => p.proposalId === input.proposalId);
|
|
343
447
|
if (!proposal) {
|
|
344
|
-
return { action, error: 'Proposal not found' };
|
|
448
|
+
return { action, error: 'Proposal not found or already resolved' };
|
|
345
449
|
}
|
|
346
450
|
const voterId = input.voterId;
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
const
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
451
|
+
if (!voterId) {
|
|
452
|
+
return { action, error: 'voterId is required for voting' };
|
|
453
|
+
}
|
|
454
|
+
const voteValue = input.vote;
|
|
455
|
+
const proposalStrategy = proposal.strategy || 'raft';
|
|
456
|
+
const required = calculateRequiredVotes(proposalStrategy, totalNodes, proposal.quorumPreset);
|
|
457
|
+
// Prevent double-voting
|
|
458
|
+
if (voterId in proposal.votes) {
|
|
459
|
+
const previousVote = proposal.votes[voterId];
|
|
460
|
+
if (previousVote === voteValue) {
|
|
461
|
+
return {
|
|
462
|
+
action,
|
|
463
|
+
error: `Voter ${voterId} has already cast the same vote on this proposal`,
|
|
464
|
+
proposalId: proposal.proposalId,
|
|
465
|
+
existingVote: previousVote,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
// Conflicting vote from same voter
|
|
469
|
+
if (proposalStrategy === 'bft') {
|
|
470
|
+
// BFT: detect as Byzantine behavior
|
|
471
|
+
if (!proposal.byzantineVoters)
|
|
472
|
+
proposal.byzantineVoters = [];
|
|
473
|
+
if (!proposal.byzantineVoters.includes(voterId)) {
|
|
474
|
+
proposal.byzantineVoters.push(voterId);
|
|
475
|
+
}
|
|
476
|
+
// Remove their vote entirely -- Byzantine voter is excluded
|
|
477
|
+
delete proposal.votes[voterId];
|
|
478
|
+
saveHiveState(state);
|
|
479
|
+
return {
|
|
480
|
+
action,
|
|
481
|
+
proposalId: proposal.proposalId,
|
|
482
|
+
voterId,
|
|
483
|
+
byzantineDetected: true,
|
|
484
|
+
message: `Byzantine behavior detected: voter ${voterId} attempted conflicting vote. Vote invalidated.`,
|
|
485
|
+
byzantineVoters: proposal.byzantineVoters,
|
|
486
|
+
status: proposal.status,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
if (proposalStrategy === 'raft') {
|
|
490
|
+
// Raft: only one vote per node per term, reject the change
|
|
491
|
+
return {
|
|
492
|
+
action,
|
|
493
|
+
error: `Raft: voter ${voterId} already voted in term ${proposal.term}. Cannot change vote.`,
|
|
494
|
+
proposalId: proposal.proposalId,
|
|
495
|
+
term: proposal.term,
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
// Quorum: reject double-vote
|
|
499
|
+
return {
|
|
500
|
+
action,
|
|
501
|
+
error: `Voter ${voterId} has already voted on this proposal`,
|
|
355
502
|
proposalId: proposal.proposalId,
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
// BFT: check for cross-proposal Byzantine behavior
|
|
506
|
+
if (proposalStrategy === 'bft') {
|
|
507
|
+
const isByzantine = detectByzantineVoters(state.consensus.pending, proposal, voterId, voteValue);
|
|
508
|
+
if (isByzantine) {
|
|
509
|
+
if (!proposal.byzantineVoters)
|
|
510
|
+
proposal.byzantineVoters = [];
|
|
511
|
+
if (!proposal.byzantineVoters.includes(voterId)) {
|
|
512
|
+
proposal.byzantineVoters.push(voterId);
|
|
513
|
+
}
|
|
514
|
+
saveHiveState(state);
|
|
515
|
+
return {
|
|
516
|
+
action,
|
|
517
|
+
proposalId: proposal.proposalId,
|
|
518
|
+
voterId,
|
|
519
|
+
byzantineDetected: true,
|
|
520
|
+
message: `Byzantine behavior detected: voter ${voterId} cast conflicting votes across proposals of same type. Vote rejected.`,
|
|
521
|
+
byzantineVoters: proposal.byzantineVoters,
|
|
522
|
+
status: proposal.status,
|
|
523
|
+
};
|
|
524
|
+
}
|
|
362
525
|
}
|
|
363
|
-
|
|
364
|
-
|
|
526
|
+
// Record the vote
|
|
527
|
+
proposal.votes[voterId] = voteValue;
|
|
528
|
+
const votesFor = Object.values(proposal.votes).filter(v => v).length;
|
|
529
|
+
const votesAgainst = Object.values(proposal.votes).filter(v => !v).length;
|
|
530
|
+
// Try to resolve
|
|
531
|
+
const resolution = tryResolveProposal(proposal, totalNodes);
|
|
532
|
+
let resolved = false;
|
|
533
|
+
if (resolution !== null) {
|
|
534
|
+
resolved = true;
|
|
535
|
+
proposal.status = resolution;
|
|
365
536
|
state.consensus.history.push({
|
|
366
537
|
proposalId: proposal.proposalId,
|
|
367
538
|
type: proposal.type,
|
|
368
|
-
result:
|
|
539
|
+
result: resolution,
|
|
369
540
|
votes: { for: votesFor, against: votesAgainst },
|
|
370
541
|
decidedAt: new Date().toISOString(),
|
|
542
|
+
strategy: proposalStrategy,
|
|
543
|
+
term: proposal.term,
|
|
544
|
+
byzantineDetected: proposal.byzantineVoters?.length ? proposal.byzantineVoters : undefined,
|
|
371
545
|
});
|
|
372
546
|
state.consensus.pending = state.consensus.pending.filter(p => p.proposalId !== proposal.proposalId);
|
|
373
547
|
}
|
|
@@ -376,10 +550,17 @@ export const hiveMindTools = [
|
|
|
376
550
|
action,
|
|
377
551
|
proposalId: proposal.proposalId,
|
|
378
552
|
voterId,
|
|
379
|
-
vote:
|
|
553
|
+
vote: voteValue,
|
|
554
|
+
strategy: proposalStrategy,
|
|
380
555
|
votesFor,
|
|
381
556
|
votesAgainst,
|
|
557
|
+
required,
|
|
558
|
+
totalNodes,
|
|
559
|
+
resolved,
|
|
560
|
+
result: resolved ? resolution : undefined,
|
|
382
561
|
status: proposal.status,
|
|
562
|
+
term: proposal.term,
|
|
563
|
+
byzantineVoters: proposal.byzantineVoters?.length ? proposal.byzantineVoters : undefined,
|
|
383
564
|
};
|
|
384
565
|
}
|
|
385
566
|
if (action === 'status') {
|
|
@@ -388,21 +569,37 @@ export const hiveMindTools = [
|
|
|
388
569
|
// Check history
|
|
389
570
|
const historical = state.consensus.history.find(h => h.proposalId === input.proposalId);
|
|
390
571
|
if (historical) {
|
|
391
|
-
return { action, ...historical, historical: true };
|
|
572
|
+
return { action, ...historical, historical: true, resolved: true };
|
|
392
573
|
}
|
|
393
574
|
return { action, error: 'Proposal not found' };
|
|
394
575
|
}
|
|
395
576
|
const votesFor = Object.values(proposal.votes).filter(v => v).length;
|
|
396
577
|
const votesAgainst = Object.values(proposal.votes).filter(v => !v).length;
|
|
578
|
+
const proposalStrategy = proposal.strategy || 'raft';
|
|
579
|
+
const required = calculateRequiredVotes(proposalStrategy, totalNodes, proposal.quorumPreset);
|
|
580
|
+
// Raft: check timeout
|
|
581
|
+
let timedOut = false;
|
|
582
|
+
if (proposalStrategy === 'raft' && proposal.timeoutAt) {
|
|
583
|
+
timedOut = new Date().getTime() > new Date(proposal.timeoutAt).getTime();
|
|
584
|
+
}
|
|
397
585
|
return {
|
|
398
586
|
action,
|
|
399
587
|
proposalId: proposal.proposalId,
|
|
400
588
|
type: proposal.type,
|
|
589
|
+
strategy: proposalStrategy,
|
|
401
590
|
status: proposal.status,
|
|
402
591
|
votesFor,
|
|
403
592
|
votesAgainst,
|
|
404
593
|
totalVotes: Object.keys(proposal.votes).length,
|
|
405
|
-
|
|
594
|
+
required,
|
|
595
|
+
totalNodes,
|
|
596
|
+
resolved: false,
|
|
597
|
+
term: proposal.term,
|
|
598
|
+
quorumPreset: proposal.quorumPreset,
|
|
599
|
+
byzantineVoters: proposal.byzantineVoters?.length ? proposal.byzantineVoters : undefined,
|
|
600
|
+
timedOut,
|
|
601
|
+
timeoutAt: proposal.timeoutAt,
|
|
602
|
+
hint: timedOut ? `Raft timeout reached. Re-propose with term ${(proposal.term || 1) + 1}.` : undefined,
|
|
406
603
|
};
|
|
407
604
|
}
|
|
408
605
|
if (action === 'list') {
|
|
@@ -411,8 +608,12 @@ export const hiveMindTools = [
|
|
|
411
608
|
pending: state.consensus.pending.map(p => ({
|
|
412
609
|
proposalId: p.proposalId,
|
|
413
610
|
type: p.type,
|
|
611
|
+
strategy: p.strategy || 'raft',
|
|
414
612
|
proposedAt: p.proposedAt,
|
|
415
613
|
totalVotes: Object.keys(p.votes).length,
|
|
614
|
+
required: calculateRequiredVotes(p.strategy || 'raft', totalNodes, p.quorumPreset),
|
|
615
|
+
term: p.term,
|
|
616
|
+
status: p.status,
|
|
416
617
|
})),
|
|
417
618
|
recentHistory: state.consensus.history.slice(-5),
|
|
418
619
|
};
|