Skip to content

dream(performance): #3377 event-driven UnifiedSwarmCoordinator task wait (evaluated, ACCEPT) - #3378

Closed
ruvnet wants to merge 2 commits into
mainfrom
dream/2026-09-20-performance
Closed

ruvnet wants to merge 2 commits into
mainfrom
dream/2026-09-20-performance

Conversation

@ruvnet

@ruvnet ruvnet commented Sep 20, 2026

Copy link
Copy Markdown
Owner

1. Hypothesis

Given UnifiedSwarmCoordinator.waitForTaskCompletion()/waitForQueuedTask() busy-polling this.state.tasks.get(taskId) on a setInterval(..., 100), when both are rewritten to (a) synchronously check for an already-terminal task before attaching any listener (race safety), (b) subscribe to task.completed/task.failed filtered by event.data.taskId, re-reading the task's actual current status rather than trusting the event name (since cancelTask() emits task.failed even for a cancelled status), and (c) keep a setTimeout fallback for the timeout case (a client-side deadline no coordinator event ever announces), then a task that completes before the next would-be poll tick should resolve on the same microtask drain as the completion event — relative to baseline, where resolution is bounded below by the poll interval — subject to:

  1. the timeout-fallback path (no terminal event ever fires) is byte-identical in outcome to today's behavior;
  2. handleTaskFail()'s retry branch (task requeued, no event fired) does not cause a mis-resolution;
  3. all existing @claude-flow/swarm tests remain green;
  4. fully deterministic, $0 evaluation cost, no LLM calls.

Frozen before evaluation; not modified after seeing results.

2. Candidate

v3/@claude-flow/swarm/src/unified-coordinator.ts (~130 net lines across 2 methods + a constructor line): both wait functions now do a synchronous pre-check of this.state.tasks.get(taskId), then register on('task.completed'|'task.failed', onTaskEvent) filtered by taskId, resolving via a fresh re-read of the task's actual status (not the event name). A setTimeout fallback preserves the existing timeout semantics exactly. Constructor gained this.setMaxListeners(Math.max(EventEmitter.defaultMaxListeners, this.config.maxTasks * 2)) — see caveat below.

This is the exact candidate 2026-09-15's performance night (#3330) surfaced and scored 4.45/5 but deferred in favor of the MoE load-balance fix. Tonight's own independent 5-role research fan-out re-scored it 4.75/5 against 4 freshly-generated alternatives and selected it (full detail in the gist).

3. Evaluation Receipt

evaluated: accepted. Real evaluator: Vitest 4.1.8, deterministic, $0, zero LLM calls. New file v3/@claude-flow/swarm/__tests__/unified-coordinator-event-driven-wait.test.ts (4 tests).

Baseline vs. candidate, isolated via git stash of just the source file (test file kept): 3 of 4 tests fail against real baseline (fake-timer assertion: the wait promise has not resolved after draining microtasks with zero timer advance — a setInterval(...,100) cannot have ticked yet) and pass against candidate; the 4th (timeout-fallback correctness) passes both ways, confirming it's a regression-safety check, not a discriminator.

Real (non-fake-timer) wall-clock microbenchmark (N=20 task-completion cycles resolved on the next setImmediate tick): baseline mean 100.359ms (p50 100.185ms — matches the 100ms poll interval exactly) vs. candidate mean 0.206ms (p50 0.063ms) — ~487x. Benchmark script was transient (not committed); fully reproducible from the description (register agent, submit task, resolve via handleTaskComplete on the next setImmediate, measure elapsed time).

Full @claude-flow/swarm package suite: 234/234 passing (230 pre-existing + 4 new), 0 regressions. tsc --noEmit: 0 errors.

4. Baseline Comparison

Baseline Candidate
Task completes on next tick, waitForTaskCompletion resolution Bound below by 100ms poll interval Resolves on same microtask drain as the event (~0.2ms)
Real wall-clock mean (N=20) 100.359ms 0.206ms (~487x)
cancelTask() → waitForTaskCompletion result status: 'cancelled' (poll re-reads status) status: 'cancelled' (unchanged — event-driven path re-reads status too)
handleTaskFail() retry branch (requeue, no event) Wait keeps polling Wait keeps waiting (listeners stay registered)
Full package suite 230/230 234/234 (+4 new), 0 regressions
tsc --noEmit clean clean

5. Darwin Lineage

Skipped — confirmed via npx ruvector@0.3.1 harness darwin --help: the real interface evolves routing/topology/prompt/memory/tool/tier/context/coordination genome parameters against a fitness-scored corpus (requires a JSON config + --execute). Tonight's fix is a deterministic code-correctness fix, not a parameter search. Same skip class as #3110/#3160/#3184/#3221/#3243/#3266/#3302/#3330.

6. Flywheel Evidence

No .claude-flow/flywheel/ state exists in this repo (direct directory check) and no signed @metaharness/flywheel bundle applies (confirmed via npx ruvector@0.3.1 harness flywheel --help: real verify/gate interface exists but targets a replay bundle this fix doesn't produce). Evidence retained as the 4 discriminating tests, the gist, issue #3377, the stash-isolated comparison, the wall-clock benchmark, and the full adversarial critique transcript — consistent with every accepted dream-cycle night since 2026-08-18.

7. Reward Hack Check

No standalone reward-hack CLI reachable (npx ruvector@0.3.1 harness --help: status/route/flywheel/darwin only). Manual checklist, independently re-verified by the adversarial critic: no existing test weakened (new file only, 230 pre-existing untouched and re-verified green); no gold/expected data touched; no cherry-picking (full suite reported both ways); no seed manipulation (deterministic fake-timer + real-timer tests, no randomness); zero cost.

8. Security Review

Not security-sensitive: pure in-process EventEmitter listener wiring on an internally-generated taskId (produced by the coordinator's own submitTask(), never raw caller input reaching this code path); no I/O/network/credential/filesystem surface. Checked for listener leaks (every exit path — immediate resolve, event resolve, timeout resolve/reject — removes both listeners and clears the timeout) and for the retry-branch race (handleTaskFail()'s requeue path emits no event and correctly isn't matched as terminal).

Independent adversarial critique (separate subagent, no authoring context): initial verdict CONFIRMED-WITH-CAVEATS. Independently re-read the diff (not the description), confirmed the synchronous pre-check makes the "terminal-before-listener-attach" race structurally impossible (no await between check and this.on(...)), confirmed the cancelTask/status-vs-event-name subtlety is handled correctly, confirmed the retry branch correctly leaves the wait pending, traced every exit path for cleanup (none leak), independently re-ran the full suite (234/234) and the stash-isolated comparison (3/4 fail on baseline as predicted), and confirmed tsc --noEmit clean. One real, reproduced caveat: neither function called setMaxListeners, and Node's default per-event-name cap is 10 — at realistic concurrency (>10 simultaneous executeTaskInDomain() calls, well within default config.maxTasks) this would emit spurious MaxListenersExceededWarning (not a correctness bug — every listener still fires — but a real regression in observable behavior vs. the old poll design). Fixed same session: this.setMaxListeners(Math.max(EventEmitter.defaultMaxListeners, this.config.maxTasks * 2)) added to the constructor; independently verified via a throwaway test asserting zero warnings with 30 concurrent listeners registered. Re-verified 234/234 green and clean typecheck after the fix.

9. Regression Analysis

234/234 in the @claude-flow/swarm package suite (230 pre-existing + 4 new), 0 regressions. tsc --noEmit clean. No existing test exercised waitForTaskCompletion/waitForQueuedTask before tonight (confirmed via grep — __tests__/coordinator.test.ts only exercises submitTask/cancelTask/getTasksByStatus, not the wait internals), so there was no legacy timing-pinned assertion to break.

10. ADR

None — this is a scoped, additive rewrite of an internal wait mechanism (busy-poll → event-driven), not a new architectural decision. Matches repo convention for this class of fix (cf. #3094, #3169, #3266, #3330 — none created an ADR).

11. Research Gist

docs/dream-cycle/dream-gist-2026-09-20.md (this branch) — full SOTA report: 5-role parallel research fan-out (deep researcher, security scan, hive-mind scan, competitor analyst, architecture reviewer), competitor comparison (LangGraph, Microsoft Agent Framework, CrewAI, OpenAI Agents SDK — all four converged on async/event completion signaling, zero on timer polling), 4 additional scored candidates not selected tonight (MMR redundant-recompute fix, DualModeOrchestrator same-pattern fix, HybridMemoryRepository brute-force search, MMR algorithmic rewrite), and scan findings for security (terminal_execute PATH-hijack, CWE-427, grade A) and hive-mind (weighted-BFT threshold can be mathematically unwinnable).

12. Issue

Closes #3377 (full 15-section report: ledger check with GitHub-verified state for the last 5+ nights, prior-gist scoring, research, evaluation receipt, adversarial critique, reward-hack check, security review, security/hive-mind scan findings, competitor comparison, witness).

13. Witness

Field Value
Session commit e558f0c0fc29c1a658085f6e6f80ad27d4fe811f
Gist SHA-256 (pre-witness content) eff14b59c1bf32bb8fc4296ee349c97051b877175c68d336a6f76c00a7ca5065
Witness stamp 12f7a08e367d9c2d8d63b8a86be6becbb5f59330cbb67bcbd173c41c1bd927b6

Verifier procedure: fetch docs/dream-cycle/dream-gist-2026-09-20.md from this branch, strip the witness table's three filled values back to PENDING, SHA-256 it, concatenate with the session commit above, SHA-256 again — result must equal the witness stamp.

14. Merge Policy

Human review required. Do not self-merge. Do not autonomously promote Flywheel state.


🤖 Generated with RuFlo

https://claude.ai/code/session_01VWUxwsNQVofSGXi8us9dEc


Generated by Claude Code

…g until timeout

The event-driven rewrite of waitForQueuedTask()/waitForTaskCompletion()
only wakes on task.completed/task.failed. shutdown() clears state.tasks
without emitting a per-task event, so waits in flight at shutdown hung
(holding a timer and the event loop) until taskTimeoutMs, where the old
100ms poll noticed 'not found' almost immediately. Re-check on
'swarm.stopped' and detach all three listeners on every settle path.
Adds a fake-timer regression test (fails without the fix).

Co-Authored-By: claude-flow <ruv@ruv.net>
@ruvnet

ruvnet commented Sep 26, 2026

Copy link
Copy Markdown
Owner Author

Landed in main via #3443 (merge commit 39ea61f; the head reviewed here, 066a185, is an ancestor of main). The LEDGER.md conflict with the other dream PRs was resolved by union. Closing so this does not stay open against an already-merged head.

🤖 Generated with RuFlo

@ruvnet ruvnet closed this Sep 26, 2026
ruvnet added a commit that referenced this pull request Sep 26, 2026
Ships the swarm fixes merged since alpha.8 (#3357 hierarchical leader
edges, #3378 event-driven UnifiedSwarmCoordinator wait, #3123, #3086,
#3243, #3062). The CLI does not depend on @claude-flow/swarm, so they
reach users only through this standalone publish.

Publishability fixes found while preparing the tarball:
- coordination/{agent-registry,task-orchestrator,swarm-hub}.ts used
  extensionless relative imports, so a plain `tsc` build of main
  throws ERR_MODULE_NOT_FOUND on `import('@claude-flow/swarm')`.
  Add the `.js` suffixes (the package is "type": "module").
- Restore the ADR-095 G2.2 transport re-exports on the root entry.
  alpha.8 was published from main@f8ab5a325 plus uncommitted work
  (later dacbe0e, never merged) that exported LocalTransport,
  FederationTransport, signMessage, etc. from the root. Without this,
  alpha.9 would silently drop 9 runtime exports alpha.8 users have.
- Add a `files` allowlist (dist, README.md, MIGRATION.md). alpha.8
  shipped src/, __tests__/, tmp.json, tsconfig.tsbuildinfo, a 1.5 MB
  ruvector.db and .claude-flow/ runtime state from a contributor's
  machine.
- Record the repo version as alpha.9 (repo was stuck at alpha.7 while
  npm had alpha.8).
@ruvnet

ruvnet commented Sep 26, 2026

Copy link
Copy Markdown
Owner Author

Shipped to npm in @claude-flow/swarm@3.0.0-alpha.9 (latest/alpha/v3alpha) via #3452 (merge 4f66bd9). Verified in the registry tarball: dist/unified-coordinator.js contains the event-driven wait (absent from alpha.8). @claude-flow/cli does not depend on @claude-flow/swarm, so no CLI re-release is needed.

🤖 Generated with claude-flow

ruvnet added a commit that referenced this pull request Sep 27, 2026
…-run-gap notes

Records the 2026-09-26 batch merge clearing #3330/#3350/#3352/#3357/#3385/#3395
(GitHub-verified via pull_request_read), #3378's landing via batch PR #3443 +
npm publish #3452, and a new confirmed no-run gap on 2026-09-23/25/26. Appends
tonight's row (#3475/#3476, ACCEPT-scoped).

Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_017NSCBLZggg81jUaa8RvZV7
ruvnet added a commit that referenced this pull request Oct 9, 2026
…-run-gap notes

Records the 2026-09-26 batch merge clearing #3330/#3350/#3352/#3357/#3385/#3395
(GitHub-verified via pull_request_read), #3378's landing via batch PR #3443 +
npm publish #3452, and a new confirmed no-run gap on 2026-09-23/25/26. Appends
tonight's row (#3475/#3476, ACCEPT-scoped).

Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_017NSCBLZggg81jUaa8RvZV7
ruvnet added a commit that referenced this pull request Oct 17, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Dream Cycle 2026-09-20] performance: UnifiedSwarmCoordinator busy-poll → event-driven task wait + security,hive-mind scan

1 participant