Skip to content

Commit ce7e78a

Browse files
ruvnetclaude
andauthored
dream(performance): #4214 RaftConsensus.awaitConsensus() busy-poll → event-driven (evaluated, ACCEPT) (#4215)
* dream(performance): #4214 RaftConsensus.awaitConsensus() busy-poll -> event-driven RaftConsensus.awaitConsensus() polled this.proposals via setInterval(...,10) even though checkConsensus() already emits 'consensus.achieved' on every accept/reject transition. Raft is Ruflo's default hive-mind consensus algorithm, reachable from production via UnifiedSwarmCoordinator.proposeConsensus(). Third instance of a busy-poll bug class already fixed twice (UnifiedSwarmCoordinator #3378, MessageBus #3563). 4 new discriminating tests, stash-isolated baseline-fails/candidate-passes (3/4, 1 correct non-discriminating control), independently reproduced by an adversarial critic. Full @claude-flow/swarm suite 281/281 (277 existing + 4 new), tsc clean both ways. Standalone wall-clock probe: 3.639ms/30 poll-timers -> 0.058ms/0 timers (N=30). Also closes a 3-night research thread (10-09/10-10/10-14) on graph-analyzer.ts's dead native-acceleration detection: direct inspection shows ruvector's real minCut/louvainCommunities exports are plain JS, not native-accelerated, so wiring them in would not be a performance win (REJECT, with evidence). Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_01MFLiva2zVEXr2n9UTK5YgU * dream(performance): #4214 record PR #4215 in LEDGER.md Fill in the real PR number (was a placeholder pending PR creation) in tonight's ledger row, and note the 10-12/10-13/10-14 ledger-transcription gap per the established convention. Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_01MFLiva2zVEXr2n9UTK5YgU --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8b95c5e commit ce7e78a

4 files changed

Lines changed: 210 additions & 19 deletions

File tree

‎docs/dream-cycle/LEDGER.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,3 +231,5 @@ as a first-class, high-priority candidate, not a background note.
231231
| 2026-10-09 | swarm | `MessageBus.handleDeliveryError()`'s retry re-queue used the broadcast sentinel `message.to === 'broadcast'` as the queue key instead of the real subscriber `agentId` `enqueue()` had fanned the message out to — `processQueues()` skips any unsubscribed queue, so a failing broadcast subscriber's retries and eventual `message.failed` were silently dropped into an orphaned queue leaking until `shutdown()`; explicitly disclosed as a "KNOWN LIMITATION (pre-existing, not fixed here)" by the immediately-preceding #3562/#3563 rewrite, closed tonight by threading `subscription.agentId` through `deliverMessage()`→`handleDeliveryError()`→the re-queue | #3928 | #3929 | yes | ACCEPT | 2 new discriminating tests, baseline-fails/candidate-passes via stash isolation (independently reproduced byte-identical by an adversarial critic's own stash/pop cycle); full `@claude-flow/swarm` suite 274/274→276/276 (+2 new), 0 regressions, `tsc --noEmit` clean both ways; healthy co-subscriber in the same broadcast unaffected (explicit regression guard); independent adversarial critique CONFIRMED, no blocking issues | 1b7e818e... | 10-01 #3599 MERGED 2026-10-07; 10-02..10-03/10-05..10-08 open/draft or closed-not-merged (see 2026-10-09 note above); 10-04 no-run gap; also newly confirmed MERGED this session: #3339 (09-16), #3539 (09-29) |
232232
| 2026-10-10 | performance | `fallbackLouvain()`'s `communityTotal` map was rebuilt via a full O(n) scan of every node, once per node, once per local-moving pass — O(n²) per pass — instead of maintained incrementally like networkx's `Stot`/python-louvain's `status.degrees` (O(1) per accepted move); this is the *only* community-detection path that ever runs (`loadRuVector()`'s native-acceleration checks for `hooks_graph_mincut`/`hooks_graph_cluster`/`@ruvector/wasm`'s `GraphAnalyzer`, none of which exist in any published `ruvector`/`@ruvector/wasm` version, confirmed by direct inspection of the installed `ruvector@0.2.27`'s real exports — correcting 2026-10-09's own "wire the real exports in" recommendation, which had the wrong premise) | #3976 | #3977 | yes | ACCEPT | 4 new discriminating tests (3 correctness + 1 complexity-growth) with a frozen pre-fix copy inline for CI-safe comparison; stash-isolated baseline-fails/candidate-passes on the growth assertion, independently reproduced by an adversarial critic's own stash/pop cycle; standalone `tsx` probe: baseline 10.37ms→837.29ms (~81x growth, N=200→1600), candidate 5.00ms→22.33ms (~4.5x growth), per-N speedup 2.1x→37.5x; full `@claude-flow/cli` suite failed-file set byte-identical baseline vs. candidate (200 vs. 199 — the one difference is the new test file itself), `tsc --noEmit` byte-identical (498 pre-existing errors); independent adversarial critique CONFIRMED, no blocking issues | 81e21481... | 10-01 #3599 MERGED; 10-02/10-03/10-06/10-07/10-08 open/draft, none stale; 10-04 no-run gap; 10-05 #3754 closed as duplicate of #3517 (not a fresh rejection — same fix had already shipped) |
233233
| 2026-10-11 | security | settings.json risk scanner (#3043) never ran on the one path that fires automatically on every real session boot — `hook-handler.cjs`'s `session-restore` command (Claude Code's own `SessionStart` hook) — a gap #3835 (10-06) itself disclosed as a future-night item; ported the TS scanner's logic (no build step for `.cjs`) into `scanSessionStartSettingsForRisk()`, validated against a real 2026 npm-worm campaign ("Mini Shai-Hulud") and CVE-2026-25725, both of which use this exact persistence mechanism | #4052 | #4053 | yes | ACCEPT-scoped (post-critique) | 12 new discriminating tests, stash-isolated 10/12 fail baseline → 12/12 pass candidate; adversarial critic found and this session fixed a non-SyntaxError `JSON.parse` failure that would have aborted session-restore itself (narrowed catch copied from the opt-in #3835 MCP path onto an unconditional-boot path); related suites 72/72 pass; full `@claude-flow/cli` suite 207 pre-existing environmental failures unchanged (unbuilt sibling packages), zero touching this change | 016fe07f... | 10-01 #3599 MERGED; 10-02/10-03/10-06/10-07/10-08 open/draft, none stale; 10-04 no-run gap; 10-05 #3754 closed as duplicate; 10-09 #3929, 10-10 #3977 open/draft, not stale. Prior gist (10-10, performance) scored 10/10 (2+2+2+2+1(1489 words)+1) |
234+
**Note (2026-10-15):** verified via `git ls-remote --heads origin "dream/*"` + GitHub MCP tools (not inferred) that 2026-10-12 (intelligence, #4080/#4081), 2026-10-13 (memory, #4137/#4138), and 2026-10-14 (swarm, #4173/#4174) all ran with real branches/issues/draft PRs — same "ledger rows only reach `main` via PR merge" structural gap noted since 2026-08-19; not transcribed as their own rows here. Trailing-7-night fates before tonight (GitHub-verified via `pull_request_read`): 10-08 #3909 open/draft; 10-09 #3929 **MERGED** 2026-10-09; 10-10 #3977 **MERGED** 2026-10-10; 10-11 #4053 **MERGED** 2026-10-11; 10-12 #4081 open/draft; 10-13 #4138 open/draft; 10-14 #4174 open/draft — none stale (<14d). 10-05 #3754 remains closed-not-merged (duplicate, already recorded). Prior gist (10-14, swarm) scored 10/10 (2+2+2+2+1(1372 words)+1).
235+
| 2026-10-15 | performance | `RaftConsensus.awaitConsensus()` (Ruflo's default hive-mind consensus, reachable via `UnifiedSwarmCoordinator.proposeConsensus()`) busy-polled `this.proposals` via `setInterval(...,10)` even though `checkConsensus()` already emits `'consensus.achieved'` on every accept/reject transition — third instance of a busy-poll bug class already fixed twice (UnifiedSwarmCoordinator #3378, MessageBus #3563); also resolved a 3-night research thread (10-09/10-10/10-14) by confirming `ruvector`'s real `minCut`/`louvainCommunities` exports are plain JS, not native-accelerated, so wiring them into `graph-analyzer.ts` would not be a performance win (REJECT, with evidence, not fixed) | #4214 | #4215 | yes | ACCEPT | 4 new discriminating tests, baseline-fails-3-of-4/candidate-passes-4-of-4 via stash isolation (1 correct non-discriminating control); full `@claude-flow/swarm` suite 281/281 (277+4 new), 0 regressions, `tsc --noEmit` clean both ways; standalone wall-clock probe 3.639ms/30 poll-timers → 0.058ms/0 timers (N=30, ~63x mean-latency reduction); independent adversarial critique CONFIRMED, no blocking issues (verified concurrency via 200 simultaneous proposals + same-tick timeout/vote race) | 2ca9c495... | 10-09/10-10/10-11 MERGED; 10-12/10-13/10-14 open/draft, none stale; 10-05 closed-not-merged (duplicate) |
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Performance SOTA Report — 2026-10-15
2+
3+
TL;DR: Tonight's 2026 performance deep-dive closes out a 3-night research thread (10-09→10-10→10-14) on `graph-analyzer.ts`'s vestigial native-acceleration detection — direct inspection of the real `ruvector` package source shows its `minCut`/`louvainCommunities` exports are plain JS, not native/WASM-accelerated, so wiring them in would not be a performance win (REJECT, with evidence) — and ships a real positive fix: `RaftConsensus.awaitConsensus()` (Ruflo's default hive-mind consensus algorithm, reachable via `UnifiedSwarmCoordinator.proposeConsensus()`'s public API) busy-polled pending proposals via `setInterval(...,10)` even though `checkConsensus()` already emits the exact `'consensus.achieved'` event needed to resolve it — the third instance of a bug class the ledger has now fixed twice before (UnifiedSwarmCoordinator #3378, MessageBus #3563).
4+
5+
## What's New in 2026
6+
7+
| Finding | Source | Confidence |
8+
|---|---|---|
9+
| MAFBench: orchestration-framework layer alone (not model/prompt) swings latency >100x and coordination success 90%→<30% | Orogat/Rostam/Mansour, arXiv:2602.03128, Feb 2026 | B |
10+
| "The Orchestrator Bottleneck": formalizes sequential/parallel-fanout/hierarchical coordination strategies, 28–42% cost reduction vs. sequential | ICML 2026 (accepted, peer-reviewed) | A |
11+
| Node.js v24.19/v26.6 (2026-08-03): streams/ring-buffer overhaul, no event-loop-phase scheduling change found | Official Node.js release notes | B |
12+
| Milvus/Weaviate/Vespa all wire *real* runtime-CPU-flag dispatch to real compiled SIMD variants — Ruflo's graph-analyzer.ts dead-detection code imitated this pattern against a backend that never shipped | Milvus Knowhere docs, Weaviate forum, Vespa Dec-2025 newsletter | B |
13+
| `ruvector`'s real `minCut`/`louvainCommunities` exports are plain JS (confirmed by direct source read), same incremental-bookkeeping complexity class as Ruflo's already-fixed `fallbackLouvain` | This session, direct inspection | A |
14+
15+
## Ruflo Current Capability
16+
17+
`RaftConsensus` (`v3/@claude-flow/swarm/src/consensus/raft.ts`) is the default hive-mind consensus algorithm (`config.algorithm ?? 'raft'`, both in `ConsensusEngine` and `UnifiedSwarmCoordinator`), reachable from production via `UnifiedSwarmCoordinator.proposeConsensus()` → `consensusEngine.propose()` + `consensusEngine.awaitConsensus()`. `checkConsensus()` already emits `'consensus.achieved'` on every pending→accepted/rejected transition — `UnifiedSwarmCoordinator` itself already listens to this same event (`unified-coordinator.ts:712`) to re-emit its own swarm-level event. Yet `awaitConsensus()` ignored that event entirely and instead polled `this.proposals.get(proposalId)` every 10ms via `setInterval`, for the full `timeoutMs` window if a proposal happened to never resolve. `ByzantineConsensus`/`GossipConsensus` carry the exact same pattern (not fixed tonight — see Next Steps). A separate, independently-confirmed finding: `raft.ts` also has a dead, diverged duplicate inbound-RPC receiver (`handleVoteRequest`/`handleAppendEntries`, lines 513–578) exercised only by tests, never by production (the real receiver is `handleInboundRaftMessage`) — not fixed tonight, carried forward.
18+
19+
## Competitor Comparison
20+
21+
| System | Mechanism handling | Grade | Source |
22+
|---|---|---|---|
23+
| LangGraph | Pregel-style superstep (plan→execute(parallel)→update), sync barrier per step, checkpoint per step — not a busy-poll | B | LangChain Pregel/checkpointer docs |
24+
| AutoGen v0.4 / MS Agent Framework | Actor-model async message-passing; event-driven agents react to messages (AutoGen now in maintenance, folded into MS Agent Framework GA Apr 2026) | B | MS Research AutoGen v0.4 |
25+
| CrewAI | "Flows" are explicitly event-driven; no published internals on dispatch mechanics (poll vs. event) | C | CrewAI repo |
26+
| OpenAI Swarm → Agents SDK | Swarm: stateless client loop, no server event bus. Agents SDK: session state/guardrails added, handoffs still sequential | C | SDK docs |
27+
| Milvus/Weaviate/Vespa (vector DBs) | Real runtime CPU-feature-flag dispatch to compiled SIMD variants — the pattern Ruflo's dead `graph-analyzer.ts` detection imitated without a real backend | B | Knowhere/Weaviate forum/Vespa docs |
28+
29+
None of the four orchestration frameworks publish internals on poll-vs-event consensus/task-wait dispatch — genuinely unaudited territory industry-wide, not just a Ruflo gap.
30+
31+
## Hypothesis
32+
33+
> Given a pending consensus proposal in `RaftConsensus`, when `awaitConsensus()` resolves via the already-emitted `'consensus.achieved'` event (plus a single `setTimeout` for expiry) instead of polling `this.proposals` on a 10ms `setInterval`, then (a) `ConsensusResult` for accept/reject/expire/not-found cases stays behaviorally identical, and (b) proposals that resolve before timeout should resolve with lower, less-discretized latency and zero timer-tick overhead while waiting, subject to: all existing tests green; no change to quorum/vote logic in `checkConsensus()`; deterministic, $0, zero-LLM evaluation.
34+
35+
## Benchmarks
36+
37+
No `.harness/bench.json` corpus exists (confirmed, same as every recent night). Real evaluator: Vitest 4.1.8 + a standalone wall-clock probe, deterministic, $0, zero LLM calls.
38+
39+
## Evaluation
40+
41+
**evaluated: accepted.** New test file `raft-await-consensus-event-driven.test.ts` (4 tests). Stash-isolated baseline (`git stash` on just `raft.ts`): 3/4 fail exactly as predicted (`setInterval` called when asserted not-called; immediate-return case measured 11ms ≥ the <10ms bound; expiry case shows 5 `setInterval` calls) — the 4th (unknown-proposal-id immediate rejection) passes both ways, a correct non-discriminating control. Candidate: 4/4 pass. Full `@claude-flow/swarm` suite: 281/281 passed, 19/19 files green (277 pre-existing + 4 new), zero regressions. `tsc --noEmit`: clean, 0 errors, both ways. Standalone wall-clock probe (30 trials, vote arriving 3–9ms after `awaitConsensus()` starts, deliberately off the old 10ms poll grid): baseline mean 3.639ms / max 7.492ms vote→resolve latency, 30 `setInterval` timers created (1 per call); candidate mean 0.058ms / max 0.346ms, 0 `setInterval` timers created — ~63x mean-latency reduction and complete elimination of poll-timer overhead while waiting.
42+
43+
## Darwin Results
44+
45+
Skipped — binary correctness/wiring fix (listen to an already-emitted event instead of polling), no continuous/categorical parameter for Darwin's real interface (`npx ruvector harness darwin <config> --execute`, confirmed available in STEP 0.5) to search over. Same skip class as nearly every accepted fix since 2026-08-18.
46+
47+
## SOTA Proof & Witness
48+
49+
```
50+
Session commit: 1882006a02487590c8c66a7eec8d7504f6af33c3
51+
Candidate commit: (this branch's head — see PR for exact SHA)
52+
Gist SHA-256 (pre-witness, this block stripped back to PENDING): df858321819d9bb00295ae21dba03fe2752975b6592a86cb845fe06dadfac367
53+
Witness stamp: 2ca9c4955cff2ddfdba4e633a5a823cded52979f91b11423cc95fcc2e1fc5c87
54+
```
55+
Verifier: fetch `docs/dream-cycle/dream-gist-2026-10-15.md` from this branch, strip this witness block's filled-in values back to `PENDING`, SHA-256 the file, concatenate with the session commit above, SHA-256 again — must equal the witness stamp.
56+
57+
## Recommended Next Steps
58+
59+
1. **Apply the same busy-poll→event-driven fix to `ByzantineConsensus.awaitConsensus()` and `GossipConsensus.awaitConsensus()`** (`v3/@claude-flow/swarm/src/consensus/byzantine.ts`/`gossip.ts`) — identical pattern, lower priority than Raft since it isn't the default algorithm; deferred to keep tonight's patch to one file/one conceptual change.
60+
2. **Remove or retarget the dead duplicate Raft receiver** (`raft.ts:513-578`, `handleVoteRequest`/`handleAppendEntries`) — exercised only by `consensus.test.ts`, zero production callers, diverges from the live `handleInboundRaftMessage` path (no index-dedup, unconditional `votedFor` mutation) and gives false test confidence.
61+
3. **Reconcile `crdt`/`quorum` as CLI-offered but unimplemented consensus strategies** — `hive-mind.ts`'s CLI/wizard offers `crdt`/`quorum`, but `ConsensusAlgorithm` only supports `raft|byzantine|gossip|paxos` and `hive-mind.ts` never imports the real `ConsensusEngine` at all (cosmetic banner only); either wire them or remove the dead choices.
62+
4. Security scan carried forward: `settings-risk-scanner.ts` never inspects `enabledPlugins`/`extraKnownMarketplaces` (a capability-granting settings.json surface distinct from `hooks`/`permissions.allow`), and `DENYLISTED_ENV_NAMES` is missing several CWE-427-class names (`PYTHONSTARTUP`, `RUBYOPT`, `JAVA_TOOL_OPTIONS`, `GIT_ASKPASS`, etc.) already in the same family it targets.
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/**
2+
* Discriminating tests for RaftConsensus.awaitConsensus()'s busy-poll -> event-driven fix.
3+
*
4+
* Prior behavior: awaitConsensus() polled `this.proposals` via `setInterval(..., 10)`
5+
* even though checkConsensus() already emits 'consensus.achieved' on every
6+
* accept/reject transition. Fixed to resolve from that event (plus a single
7+
* setTimeout for expiry) instead of polling.
8+
*/
9+
10+
import { describe, it, expect, afterEach, vi } from 'vitest';
11+
import { RaftConsensus, createRaftConsensus } from '../src/consensus/raft.js';
12+
import type { ConsensusVote } from '../src/types.js';
13+
14+
describe('RaftConsensus.awaitConsensus() - event-driven resolution', () => {
15+
let leader: RaftConsensus | undefined;
16+
17+
afterEach(async () => {
18+
if (leader) {
19+
await leader.shutdown();
20+
leader = undefined;
21+
}
22+
});
23+
24+
async function createElectedLeader(config: Parameters<typeof createRaftConsensus>[1] = {}) {
25+
const node = createRaftConsensus('evt-leader', {
26+
threshold: 0.66,
27+
timeoutMs: 5000,
28+
electionTimeoutMinMs: 50,
29+
electionTimeoutMaxMs: 100,
30+
heartbeatIntervalMs: 25,
31+
...config,
32+
});
33+
await node.initialize();
34+
node.addPeer('evt-peer');
35+
await new Promise(resolve => setTimeout(resolve, 150));
36+
expect(node.isLeader()).toBe(true);
37+
return node;
38+
}
39+
40+
it('resolves a pending proposal via the consensus.achieved event, never via setInterval polling', async () => {
41+
leader = await createElectedLeader();
42+
const proposal = await leader.propose({ action: 'commit' });
43+
44+
const setIntervalSpy = vi.spyOn(globalThis, 'setInterval');
45+
46+
// awaitConsensus() starts while the proposal is still 'pending' - the vote
47+
// (and the resulting quorum) arrives only after this call is already in flight.
48+
const awaitPromise = leader.awaitConsensus(proposal.id);
49+
50+
await new Promise(resolve => setTimeout(resolve, 5));
51+
const vote: ConsensusVote = {
52+
voterId: 'evt-peer',
53+
approve: true,
54+
confidence: 1.0,
55+
timestamp: new Date(),
56+
};
57+
await leader.vote(proposal.id, vote);
58+
59+
const result = await awaitPromise;
60+
61+
expect(result.approved).toBe(true);
62+
expect(result.proposalId).toBe(proposal.id);
63+
// Discriminates old (busy-poll) vs new (event-driven) implementation: the old
64+
// code created a setInterval on every awaitConsensus() call; the new one never does.
65+
expect(setIntervalSpy).not.toHaveBeenCalled();
66+
67+
setIntervalSpy.mockRestore();
68+
});
69+
70+
it('returns immediately (no event wait needed) when the proposal already resolved before awaitConsensus is called', async () => {
71+
leader = await createElectedLeader();
72+
const proposal = await leader.propose({ action: 'commit' });
73+
74+
const vote: ConsensusVote = {
75+
voterId: 'evt-peer',
76+
approve: true,
77+
confidence: 1.0,
78+
timestamp: new Date(),
79+
};
80+
await leader.vote(proposal.id, vote);
81+
82+
const setIntervalSpy = vi.spyOn(globalThis, 'setInterval');
83+
const start = Date.now();
84+
const result = await leader.awaitConsensus(proposal.id);
85+
const elapsed = Date.now() - start;
86+
87+
expect(result.approved).toBe(true);
88+
expect(elapsed).toBeLessThan(10);
89+
expect(setIntervalSpy).not.toHaveBeenCalled();
90+
91+
setIntervalSpy.mockRestore();
92+
});
93+
94+
it('expires via a single timer after the real timeout elapses, without ever polling', async () => {
95+
leader = await createElectedLeader({ timeoutMs: 60 });
96+
const proposal = await leader.propose({ action: 'commit' });
97+
98+
const setIntervalSpy = vi.spyOn(globalThis, 'setInterval');
99+
100+
// No vote ever arrives - quorum is never reached, so this must resolve
101+
// (not reject) with status 'expired' once the configured timeout elapses.
102+
const result = await leader.awaitConsensus(proposal.id);
103+
104+
expect(result.proposalId).toBe(proposal.id);
105+
expect(result.approved).toBe(false);
106+
expect(setIntervalSpy).not.toHaveBeenCalled();
107+
108+
setIntervalSpy.mockRestore();
109+
});
110+
111+
it('rejects immediately for an unknown proposal id (preserves pre-existing behavior)', async () => {
112+
leader = await createElectedLeader();
113+
114+
await expect(leader.awaitConsensus('does-not-exist')).rejects.toThrow(
115+
'Proposal does-not-exist not found'
116+
);
117+
});
118+
});

0 commit comments

Comments
 (0)