mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-27 17:58:07 +00:00
fix(desktop): preserve live background subagents across message.start (rebased onto main, #64015)
Cherry-pick of b4d5ba3e onto current main. Original commit's target file
apps/desktop/src/app/session/hooks/use-message-stream.ts has since been
moved into apps/desktop/src/app/session/hooks/use-message-stream/gateway-event.ts.
The fix is unchanged: replace clearSessionSubagents() with
pruneFinishedSessionSubagents() at the message.start boundary so that
still-running subagent rows survive new turns.
Per @teknium1's review of #64038 on 2026-07-16: 'mergeable_state=dirty'
because the dispatcher moved. Resolved by retargeting the call.
Tests: 3 new cases in subagents.test.ts (unchanged from original PR).
Failing-first verified in original PR (3/3 fail on unfixed, 3/3 pass on fix).
This commit is contained in:
parent
666076d137
commit
8a32426300
3 changed files with 85 additions and 2 deletions
|
|
@ -47,8 +47,10 @@ import {
|
|||
setTurnStartedAt,
|
||||
setYoloActive
|
||||
} from '@/store/session'
|
||||
import { clearSessionSubagents, pruneDelegateFallbackSubagents, upsertSubagent } from '@/store/subagents'
|
||||
import { broadcastSessionsChanged } from '@/store/session-sync'
|
||||
import { clearActiveSessionTodos } from '@/store/todos'
|
||||
import { clearSessionSubagents, pruneDelegateFallbackSubagents, pruneFinishedSessionSubagents, upsertSubagent } from '@/store/subagents'
|
||||
import { setSessionTodos } from '@/store/todos'
|
||||
import { recordToolDiff } from '@/store/tool-diffs'
|
||||
import { reportInstallMethodWarning } from '@/store/updates'
|
||||
import { notifyWorkspaceChanged, toolChangedPath, toolMayMutateFiles } from '@/store/workspace-events'
|
||||
|
|
@ -439,7 +441,7 @@ export function useGatewayEventHandler(deps: GatewayEventDeps) {
|
|||
}
|
||||
|
||||
flushQueuedDeltas(sessionId)
|
||||
clearSessionSubagents(sessionId)
|
||||
pruneFinishedSessionSubagents(sessionId)
|
||||
setSessionCompacting(sessionId, false)
|
||||
compactedTurnRef.current.delete(sessionId)
|
||||
nativeSubagentSessionsRef.current.delete(sessionId)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
clearSessionSubagents,
|
||||
failedSubagentCount,
|
||||
pruneDelegateFallbackSubagents,
|
||||
pruneFinishedSessionSubagents,
|
||||
upsertSubagent
|
||||
} from './subagents'
|
||||
|
||||
|
|
@ -131,4 +132,55 @@ describe('subagent store', () => {
|
|||
expect($subagentsBySession.get().s1).toBeUndefined()
|
||||
expect($subagentsBySession.get().s2).toHaveLength(1)
|
||||
})
|
||||
|
||||
// Regression test for #64015: still-RUNNING background subagents must survive
|
||||
// the per-turn wipe that previously dropped them at message.start. The fix
|
||||
// replaces clearSessionSubagents() with pruneFinishedSessionSubagents() at
|
||||
// the use-message-stream message.start handler, so only terminal-status rows
|
||||
// get filtered out.
|
||||
it('pruneFinishedSessionSubagents keeps running/queued and drops terminal rows', () => {
|
||||
upsertSubagent('s1', { goal: 'live-a', status: 'running', subagent_id: 'live-a', task_index: 0 })
|
||||
upsertSubagent('s1', { goal: 'live-b', status: 'queued', subagent_id: 'live-b', task_index: 1 })
|
||||
upsertSubagent('s1', { goal: 'done', status: 'completed', subagent_id: 'done', task_index: 2 })
|
||||
upsertSubagent('s1', { goal: 'broken', status: 'failed', subagent_id: 'broken', task_index: 3 })
|
||||
upsertSubagent('s1', { goal: 'cancelled', status: 'interrupted', subagent_id: 'cancelled', task_index: 4 })
|
||||
|
||||
pruneFinishedSessionSubagents('s1')
|
||||
|
||||
const ids = listFor('s1').map(item => item.id).sort()
|
||||
expect(ids).toEqual(['live-a', 'live-b'])
|
||||
expect(activeSubagentCount(listFor('s1'))).toBe(2)
|
||||
})
|
||||
|
||||
// Companion test: after prune, a late `subagent.complete` event for a
|
||||
// surviving live row must still be accepted by upsertSubagent (the wipe
|
||||
// path previously silently dropped these).
|
||||
it('surviving live subagents still accept createIfMissing=false completion', () => {
|
||||
upsertSubagent('s1', { goal: 'live', status: 'running', subagent_id: 'live', task_index: 0 })
|
||||
|
||||
pruneFinishedSessionSubagents('s1')
|
||||
|
||||
upsertSubagent(
|
||||
's1',
|
||||
{ status: 'completed', subagent_id: 'live', task_index: 0, summary: 'finished later' },
|
||||
false,
|
||||
'subagent.complete'
|
||||
)
|
||||
|
||||
const item = listFor('s1')[0]
|
||||
expect(item?.status).toBe('completed')
|
||||
expect(item?.summary).toBe('finished later')
|
||||
})
|
||||
|
||||
it('pruneFinishedSessionSubagents leaves other sessions untouched', () => {
|
||||
upsertSubagent('s1', { goal: 'live', status: 'running', subagent_id: 'a', task_index: 0 })
|
||||
upsertSubagent('s1', { goal: 'done', status: 'completed', subagent_id: 'b', task_index: 1 })
|
||||
upsertSubagent('s2', { goal: 'live', status: 'running', subagent_id: 'c', task_index: 0 })
|
||||
upsertSubagent('s2', { goal: 'done', status: 'completed', subagent_id: 'd', task_index: 1 })
|
||||
|
||||
pruneFinishedSessionSubagents('s1')
|
||||
|
||||
expect(listFor('s1').map(item => item.id)).toEqual(['a'])
|
||||
expect(listFor('s2').map(item => item.id).sort()).toEqual(['c', 'd'])
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -189,6 +189,35 @@ export function clearSessionSubagents(sid: string) {
|
|||
$subagentsBySession.set(rest)
|
||||
}
|
||||
|
||||
/**
|
||||
* Prune terminal-status subagent rows for a session, leaving running/queued
|
||||
* entries untouched. Used at the `message.start` boundary in the desktop
|
||||
* message-stream hook so that the *previous* turn's finished rows get flushed
|
||||
* from the display while background subagents that outlived the spawning turn
|
||||
* remain visible (and still accept late progress/complete events).
|
||||
*
|
||||
* Distinct from `clearSessionSubagents` (used by the Stop action, which
|
||||
* genuinely cancels running subagents and so should drop them all) and from
|
||||
* `pruneDelegateFallbackSubagents` (which filters by id prefix to remove
|
||||
* placeholder rows once the real native event arrives).
|
||||
*/
|
||||
export function pruneFinishedSessionSubagents(sid: string) {
|
||||
const map = $subagentsBySession.get()
|
||||
const list = map[sid]
|
||||
|
||||
if (!list?.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const next = list.filter(item => item.status === 'running' || item.status === 'queued')
|
||||
|
||||
if (next.length === list.length) {
|
||||
return
|
||||
}
|
||||
|
||||
$subagentsBySession.set({ ...map, [sid]: next })
|
||||
}
|
||||
|
||||
export function pruneDelegateFallbackSubagents(sid: string) {
|
||||
const map = $subagentsBySession.get()
|
||||
const list = map[sid]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue