perf(consensus): overlap arbiter adjudication and revision on dissent rounds#157
Merged
Merged
Conversation
… rounds in runToConvergence The multi-round consensus loop ran the provider arbiter's adjudication and revision calls serially on every non-final round - two back-to-back external model calls. Both depend only on the round's peer results + state.currentPlan, not on each other (buildRevisionPrompt reads the peers' verdicts, not the arbiter's; submitAdjudication never mutates currentPlan), so they can run concurrently. Gate the parallel revision on peer dissent: when any responding peer is non-APPROVE the round cannot converge (checkConvergence requires everyApprove), so the revision is guaranteed needed and adjudication || revision overlap for free. On an all-approve round only adjudication runs - no speculative call is burned on a round that may converge. Net: identical external-call count to the serial loop in every outcome, one serial arbiter leg saved per dissent round (~18-29% loop wall-time on a 3-5 round review). Behavior-preserving: same verdict, same convergence round, same call count. Each arbiter branch (ask + parse) is isolated via the blind-pass Promise.resolve().then wrapper so a sync throw, rejected ask, or parse fault cannot reject Promise.all. Tests: RC9 (used-revision propagation), RC10 (no wasted call on all-approve convergence), RC11 (sync-throw on the revision leg isolated), RC12 (serial revision on the rare all-approve-but-arbiter-blocks path). RC1-RC8 unchanged.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
In
runToConvergence(the provider-arbiter consensus loop behind theconsensusMCP tool), the arbiter's adjudication and revision calls ran serially on every non-final round - two back-to-back external-model calls. This overlaps them, gated on peer dissent.Why it's safe (data-dependency, verified against source)
buildAdjudicationPrompt(state, results)andbuildRevisionPrompt(state, results)each read only the round's peerresults+state.currentPlan. Neither consumes the other's output -buildRevisionPromptreads the peers' verdicts/issues, not the arbiter's adjudicated verdict.submitAdjudicationnever mutatescurrentPlan, so the revision prompt is identical whether built before or after it.arbiter.askconcurrently with the peer fan-out).The dissent gate
checkConvergencecannot converge if any responding peer is non-APPROVE. So:This avoids the trap of unconditional speculation (which would burn a discarded revision call on the common round-1-convergence happy path).
Net: identical external-call count to the serial loop in every outcome. Behavior-preserving (same verdict, same convergence round). Wall-clock win = one serial arbiter leg per dissent round, ~18-29% loop time on a 3-5 round review.
Each arbiter branch (ask + parse) is failure-isolated via the existing blind-pass
Promise.resolve().then(...).then(v=>v,()=>null)wrapper, so a sync throw, rejected ask, or parse fault cannot rejectPromise.all.Scope
core/orchestrate.jsrunToConvergence(provider-arbiter path).consensus-step/ live/consensus(Claude is the arbiter there; serialization is the command's step sequence, not external calls).Tests
test/core-run-to-convergence.test.js: RC1-RC8 pass unchanged; added RC9 (used-revision propagation into the next round), RC10 (no revision call dispatched on an all-approve converging round), RC11 (sync-throw on the revision leg isolated, loop never rejects), RC12 (serial revision on the all-approve-but-arbiter-blocks path).Test plan
node --test test/core-run-to-convergence.test.js- 12 pass, 0 failnpm run check- 578 pass, 0 fail, typecheck cleanProvenance
Surfaced by a
/ask-allperformance review (7 models), then the plan was hardened through a/consensusreview (converged round 2, medium confidence) - the dissent gate came out of that review, replacing a naive always-parallel-and-discard draft that taxed the happy path.