fix(desktop): accumulate MoA reference reasoning blocks instead of replacing - #64689
Conversation
…placing Every moa.reference event called appendReasoningDelta(..., replace=true), which wipes ALL existing reasoning-type message parts and seeds exactly one new part. With two or more MoA reference models, each later reference erased the reasoning disclosure built by earlier references, so only the last advisor's output ever stayed visible instead of one labelled block per reference (contradicting the multi-reference visibility behavior from NousResearch#53855). Only the first reference (index <= 1, or missing) now replaces — preserving the original "clear stale reasoning from before this turn" behavior. Every later reference accumulates via the existing queue-then-flush path instead, applied immediately since each reference arrives as one complete block rather than incremental tokens. Fixes NousResearch#64658
tonydwb
left a comment
There was a problem hiding this comment.
Code Review Summary
Verdict: Approved
Overview
Desktop fix: accumulates MoA (Mixture of Agents) reference reasoning blocks instead of replacing them. 132 additions.
Assessment
- Correctness: Accumulating rather than replacing reference blocks preserves reasoning context — correct behavior.
- Security: No security changes.
- Debug artifacts: None.
Summary
Clean fix. LGTM.
Reviewed by Hermes Agent
|
Thanks for the focused regression fix. Current The proposed first-reference replacement followed by queue-and-immediate-flush for later references matches the backend lifecycle: Automated hermes-sweeper review. |
What does this PR do?
Hermes Desktop shows Mixture-of-Agents (MoA) reference-model output as labelled reasoning blocks
in the "Thinking" disclosure, one per reference model, before the aggregator's final answer
(behavior introduced in #53855). Every
moa.referencegateway event, however, calledappendReasoningDelta(sessionId, text, true)—replace=true— which wipes all existingreasoning-type message parts and replaces them with exactly one new part. With two or morereference models, each later
moa.referenceevent erased the reasoning block built by theearlier one(s), so only the last reference's output ever stayed visible instead of accumulating
one labelled block per reference.
The fix keeps
replace=trueonly for the first reference (index <= 1, or missing — thispreserves the original intent of clearing any stale reasoning left over from before this turn's
references start) and switches every later reference to the existing "queue then flush
immediately" path instead, so it appends onto the already-shown blocks rather than replacing
them. Each reference arrives as one complete text block (not incremental tokens), so applying it
via an immediate flush rather than the streamed/batched queue is correct and matches how a
sibling event elsewhere in this file already applies non-streamed content immediately.
I verified the "in-flight reasoning.delta could interleave with the reference-accumulation flush"
concern raised in review by reading
agent/moa_loop.py:MoAChatCompletions.reference_callbackfires
"moa.reference"once per reference's already-complete text — there is no concurrenttoken stream during the reference-gathering phase for the accumulation path to collide with.
How it works
flowchart TD A["moa.reference event (index, count, label, text)"] --> B{"index <= 1?"} B -->|"yes (first reference)"| C["appendReasoningDelta(replace=true)\nclears stale reasoning, seeds one fresh block\n(unchanged from before)"] B -->|"no (later reference)"| D["appendReasoningDelta(replace=false) queues the block,\nthen flushQueuedDeltas() applies it immediately\n→ appends after the prior reference block(s)"] C --> E["◇ Reference 1/2 — model-a\nadvice-a"] D --> F["◇ Reference 1/2 — model-a\nadvice-a\n\n◇ Reference 2/2 — model-b\nadvice-b"]Related Issue
Fixes #64658
Type of Change
Changes Made
apps/desktop/src/app/session/hooks/use-message-stream/gateway-event.ts:moa.referencehandler now branches onindex:<= 1(or missing) keeps the originalreplace=truecall; every later reference callsappendReasoningDelta(..., false)followed immediately byflushQueuedDeltas(sessionId)so it accumulates instead of replacing.apps/desktop/src/app/session/hooks/use-message-stream/moa-reference-event.test.tsx: new test file (following the existingcompaction-event.test.tsxpattern of rendering the realuseMessageStreamhook and drivinghandleGatewayEventdirectly) — covers 2-reference accumulation (the reported repro), a single-reference (count=1) turn for regression safety, and 3+ references landing in order.Step-by-step
appendReasoningDelta'sreplace=truebranch (use-message-stream/index.ts) to confirm it filters out every existingreasoning-type part before seeding one new part — the exact mechanism the issue describes.moa.referenceevents); confirmed it fails against the pre-fix code (only the second reference's text survives).flushQueuedDeltasdependency (no new API surface needed).awaitbetweenappendReasoningDelta(..., false)andflushQueuedDeltas, so there's no window for the scheduled rAF/setTimeout flush to double-apply).reasoning.deltacould land in the same shared per-session queue bucket as amoa.referenceaccumulation flush, producing unseparated/garbled text — confirmed viaagent/moa_loop.pythat the reference-gathering phase has no concurrent token stream, so this isn't reachable.Acceptance Criteria
replace=true).reasoning.delta(streaming) andreasoning.available(full-replace) call sites and their existing tests are untouched and still pass.How to Test
main, configure an MoA preset with 2+ reference models, run/moa <prompt>, and watch the Thinking disclosure after the secondmoa.referenceevent lands — only the latest reference's block is visible.Tests Performed
npx vitest run src/app/session/hooks/use-message-stream/moa-reference-event.test.tsx(fromapps/desktop)3 passednpx vitest run src/app/session/hooks/use-message-stream/(fromapps/desktop)5 test files, 19 passednpx prettier --checkon both changed filesgateway-event.tsmatches Prettier's output (two unrelated pre-existing lines elsewhere in the file are not Prettier-clean onmainalready and were intentionally left untouched to keep this diff scoped)npx tsc --noEmit -p tsconfig.json, filtered to touched filesexpected '...model-b...' to contain 'model-a'); all 3 pass after the fixChecklist
Code
Documentation & Housekeeping
cli-config.yaml.exampleupdated if config keys changed — N/ACONTRIBUTING.md/AGENTS.mdupdated if architecture/workflow changed — N/AScreenshots / Logs