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.
@@ -68,7 +68,8 @@ const trainCommand = {
68
68
  });
69
69
  if (initResult.success) {
70
70
  wasmFeatures = initResult.features;
71
- spinner.setText(`RuVector initialized: ${wasmFeatures.join(', ')}`);
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
- tableData.push({ 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
+ 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
- output.writeln(output.highlight(`✓ RuVector WASM: ${wasmFeatures.join(', ')}`));
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 WASM',
384
+ component: 'RuVector Training',
379
385
  status: ruvectorStats.initialized ? output.success('Active') : output.dim('Not loaded'),
380
386
  details: ruvectorStats.initialized
381
- ? `MicroLoRA: ${ruvectorStats.totalAdaptations} adapts`
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
- const nodeCount = Object.keys(store.nodes).length;
368
- const quorum = Math.floor(nodeCount / 2) + 1;
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
- requiredVotes: Math.floor(Object.keys(store.nodes).length / 2) + 1,
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
- timestamp: new Date().toISOString(),
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
- return {
398
- success: true,
399
- action: 'committed',
400
- committedAt: new Date().toISOString(),
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
- requiredVotes: Math.ceil(state.workers.length / 2) + 1,
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
- proposal.votes[voterId] = input.vote;
348
- // Check if we have majority
349
- const votesFor = Object.values(proposal.votes).filter(v => v).length;
350
- const votesAgainst = Object.values(proposal.votes).filter(v => !v).length;
351
- const majority = Math.ceil(state.workers.length / 2) + 1;
352
- if (votesFor >= majority) {
353
- proposal.status = 'approved';
354
- state.consensus.history.push({
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
- type: proposal.type,
357
- result: 'approved',
358
- votes: { for: votesFor, against: votesAgainst },
359
- decidedAt: new Date().toISOString(),
360
- });
361
- state.consensus.pending = state.consensus.pending.filter(p => p.proposalId !== proposal.proposalId);
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
- else if (votesAgainst >= majority) {
364
- proposal.status = 'rejected';
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: 'rejected',
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: input.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
- requiredMajority: Math.ceil(state.workers.length / 2) + 1,
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
  };