mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-29 18:46:59 +00:00
fix(desktop): preserve live background subagents across message.start (#64015)
The desktop message-stream hook wiped the per-session subagent store on every new turn (use-message-stream.ts:860), dropping still-running and queued rows along with the previous turn's terminal entries. Late subagent.tool / subagent.progress / subagent.complete events for surviving background tasks were then silently dropped by upsertSubagent's createIfMissing guard, so the rows never returned and their completion summaries never rendered. Replace the unconditional clearSessionSubagents() call at the message.start boundary with a new pruneFinishedSessionSubagents() that filters out only terminal-status rows. The Stop-action call at use-prompt-actions.ts:1546 still uses clearSessionSubagents because /stop genuinely cancels running subagents, so dropping their rows is correct. Tests: 3 new cases in subagents.test.ts (regression for the wipe, late completion acceptance after prune, cross-session isolation). Failing-first verified: all 3 fail on unfixed (pruneFinishedSessionSubagents not exported), all 3 pass on fix; 7 pre-existing subagents tests stay green; 3 gateway-events tests stay green; 6 background-delegation tests stay green. The Stop-action use-prompt-actions.test.tsx failures predate this branch (window-not-defined env issue, verified by stash-and-rerun).
This commit is contained in:
parent
f53b184c48
commit
b4d5ba3e0d
3 changed files with 83 additions and 2 deletions
|
|
@ -53,7 +53,7 @@ import {
|
|||
setYoloActive
|
||||
} from '@/store/session'
|
||||
import { broadcastSessionsChanged } from '@/store/session-sync'
|
||||
import { clearSessionSubagents, pruneDelegateFallbackSubagents, upsertSubagent } from '@/store/subagents'
|
||||
import { clearSessionSubagents, pruneDelegateFallbackSubagents, pruneFinishedSessionSubagents, upsertSubagent } from '@/store/subagents'
|
||||
import { setSessionTodos } from '@/store/todos'
|
||||
import { recordToolDiff } from '@/store/tool-diffs'
|
||||
import { notifyWorkspaceChanged, toolMayMutateFiles } from '@/store/workspace-events'
|
||||
|
|
@ -857,7 +857,7 @@ export function useMessageStream({
|
|||
}
|
||||
|
||||
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'])
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -192,6 +192,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