Skip to content

Commit 99ec945

Browse files
authored
fix: isolate MCU background task results (#2181)
1 parent afdc0c1 commit 99ec945

9 files changed

Lines changed: 1026 additions & 39 deletions

File tree

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
date: 2026-07-22
3+
pr: pending
4+
feature: MCU background result speech
5+
impact: MCU voice turns return to idle after the parent response and later speak only the final autonomous Agent response produced for completed background work.
6+
---
7+
8+
The Global Agent server and the direct outbound MCU relay compatibility path
9+
keep a hidden session listener while a voice-triggered Hermes run has pending
10+
background delegations. Subagent telemetry and delegation lifecycle events
11+
remain server-internal and are not forwarded to the MCU. When the background
12+
completion starts its autonomous parent turn, the listener buffers that turn
13+
and reuses the existing MCU audio queue only after `run.completed` provides the
14+
final Agent output. Existing MCU firmware and the transparent remote relay
15+
server require no protocol changes. An MCU interrupt received after the parent
16+
turn is already idle no longer records a deferred session abort, so immediately
17+
starting another recording leaves pending background delegations running; an
18+
interrupt during an active foreground turn still aborts that turn normally.
19+
Each MCU foreground run now carries an internal `queue_id`. When a new voice
20+
turn is queued while an autonomous background delivery is returning on the
21+
same chat session, MCU listeners use that identifier to keep the two event
22+
streams separate. The existing background listener owns the autonomous result;
23+
the new listener ignores it until its own queued run starts. This identifier is
24+
not part of the MCU wire protocol, so older firmware remains compatible.

packages/server/src/services/global-agent/outbound-relay-client.ts

Lines changed: 256 additions & 19 deletions
Large diffs are not rendered by default.

packages/server/src/services/global-agent/server.ts

Lines changed: 248 additions & 15 deletions
Large diffs are not rendered by default.

packages/server/src/services/hermes/run-chat/handle-bridge-run.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,7 @@ export async function handleBridgeRun(
412412
// boundaries and remain disabled independently of this default.
413413
const backgroundDelegationEnabled = data.background_delegation_enabled !== false
414414
if (!session_id) {
415-
socket.emit('run.failed', { event: 'run.failed', error: 'session_id is required for cli source' })
415+
socket.emit('run.failed', { event: 'run.failed', queue_id: data.queue_id, error: 'session_id is required for cli source' })
416416
return
417417
}
418418

@@ -660,13 +660,15 @@ export async function handleBridgeRun(
660660
pushState(sessionMap, session_id, 'run.started', {
661661
event: 'run.started',
662662
run_id: started.run_id,
663+
queue_id: data.queue_id,
663664
queue_length: state.queue.length || 0,
664665
autonomous: data.autonomous === true,
665666
delegation_id: data.background_delegation_id,
666667
})
667668
emit('run.started', {
668669
event: 'run.started',
669670
run_id: started.run_id,
671+
queue_id: data.queue_id,
670672
queue_length: state.queue.length || 0,
671673
autonomous: data.autonomous === true,
672674
delegation_id: data.background_delegation_id,
@@ -695,7 +697,7 @@ export async function handleBridgeRun(
695697
currentInputTokens,
696698
shouldPersistUserMessage && displayRole === 'user',
697699
data.model_groups,
698-
{ autonomous: data.autonomous === true, delegationId: data.background_delegation_id },
700+
{ autonomous: data.autonomous === true, delegationId: data.background_delegation_id, queueId: data.queue_id },
699701
)
700702
if (chunk.done) {
701703
sawTerminalChunk = true
@@ -741,7 +743,7 @@ export async function handleBridgeRun(
741743
currentInputTokens,
742744
shouldPersistUserMessage && displayRole === 'user',
743745
data.model_groups,
744-
{ autonomous: data.autonomous === true, delegationId: data.background_delegation_id },
746+
{ autonomous: data.autonomous === true, delegationId: data.background_delegation_id, queueId: data.queue_id },
745747
)
746748
}
747749
} catch (err: any) {
@@ -791,6 +793,7 @@ export async function handleBridgeRun(
791793
background_pending: backgroundPendingCount(state),
792794
autonomous: data.autonomous === true,
793795
delegation_id: data.background_delegation_id,
796+
queue_id: data.queue_id,
794797
})
795798
if (queueLen > 0) {
796799
dequeueNextQueuedRun(socket, session_id)
@@ -1125,7 +1128,7 @@ async function applyBridgeChunkAsync(
11251128
currentInputTokens = 0,
11261129
currentInputIncludedInDb = true,
11271130
modelGroups?: RunModelGroup[],
1128-
runMetadata?: { autonomous?: boolean; delegationId?: string },
1131+
runMetadata?: { autonomous?: boolean; delegationId?: string; queueId?: string },
11291132
): Promise<void> {
11301133
if (state.activeRunMarker !== runMarker) {
11311134
bridgeLogger.info({
@@ -1653,6 +1656,7 @@ async function applyBridgeChunkAsync(
16531656
background_pending: backgroundPendingCount(state),
16541657
autonomous: runMetadata?.autonomous === true,
16551658
delegation_id: runMetadata?.delegationId,
1659+
queue_id: runMetadata?.queueId,
16561660
workspace_run_change: workspaceRunChange,
16571661
}
16581662
emit(eventName, payload)

packages/server/src/services/hermes/run-chat/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ export class ChatRunSocket {
237237
socket.emit('run.failed', {
238238
event: 'run.failed',
239239
session_id: data.session_id,
240+
queue_id: data.queue_id,
240241
error: err instanceof Error ? err.message : String(err),
241242
})
242243
return
@@ -331,6 +332,7 @@ export class ChatRunSocket {
331332
socket.emit('run.failed', {
332333
event: 'run.failed',
333334
session_id: data.session_id,
335+
queue_id: data.queue_id,
334336
error: err instanceof Error ? err.message : String(err),
335337
})
336338
}
@@ -479,11 +481,13 @@ export class ChatRunSocket {
479481
const payload: {
480482
event: 'run.failed'
481483
session_id?: string
484+
queue_id?: string
482485
error: string
483486
queue_remaining?: number
484487
} = {
485488
event: 'run.failed',
486489
session_id: data.session_id,
490+
queue_id: data.queue_id,
487491
error: `Agent Bridge is not reachable: ${bridgeReady.error}`,
488492
}
489493
if (queueRemaining > 0) payload.queue_remaining = queueRemaining

tests/server/chat-run-bridge-readiness.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,7 @@ describe('ChatRunSocket bridge readiness gating', () => {
379379
expect(socket.emit).toHaveBeenCalledWith('run.failed', {
380380
event: 'run.failed',
381381
session_id: 'session-1',
382+
queue_id: 'queue-failed',
382383
error: 'Agent Bridge is not reachable: bridge offline',
383384
queue_remaining: 1,
384385
})

0 commit comments

Comments
 (0)