# NousResearch/hermes-agent · Fix desynced agent count between UI components

## Open now

- Nothing open on GitHub.

These items address a mismatch between the status bar agent indicator and the Spawn tree panel in the desktop app, where the two components reported different agent states. The work focuses on unifying their scope or data source so they stay in sync.

## TL;DR

- 3 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 1 PR(s) closed without merging.
- A fix was merged to main in PR #52183. Verify it addresses your case.
- **Recommended:** read PR #52183 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `resolved` · deciding PR(s): `pr-52183`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.67`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #49808: Desktop: Spawn tree shows empty state while status bar reports 'Agents 2 running' — desynced subagent state

## Prior & competing PRs

- **#52183: fix(desktop): make Agents indicator match the Spawn-tree panel**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `apps/desktop/src/app/agents/index.tsx`, `apps/desktop/src/app/shell/hooks/use-statusbar-items.tsx`, `apps/desktop/src/store/subagents.test.ts`, `apps/desktop/src/store/subagents.ts`
  - fix hunks:
  - `apps/desktop/src/app/agents/index.tsx`

    ```diff
    @@ -9,9 +9,9 @@ import { type Translations, useI18n } from '@/i18n'
     import { AlertCircle, CheckCircle2, Sparkles } from '@/lib/icons'
     import { useEnterAnimation } from '@/lib/use-enter-animation'
     import { cn } from '@/lib/utils'
    -import { $activeSessionId } from '@/store/session'
     import {
       $subagentsBySession,
    +  allSubagents,
       buildSubagentTree,
       type SubagentNode,
       type SubagentStatus,
    ```

    ```diff
    @@ -77,15 +77,12 @@ interface AgentsViewProps {
     
     export function AgentsView({ onClose }: AgentsViewProps) {
       const { t } = useI18n()
    -  const activeSessionId = useStore($activeSessionId)
       const subagentsBySession = useStore($subagentsBySession)
     
    -  const activeSubagents = useMemo(
    -    () => (activeSessionId ? (subagentsBySession[activeSessionId] ?? []) : []),
    -    [activeSessionId, subagentsBySession]
    -  )
    -
    -  const tree = useMemo(() => buildSubagentTree(activeSubagents), [activeSubagents])
    +  // Aggregate every session, matching the status-bar indicator — a subagent
    +  // running in a background session must still be visible here, or the two
    +  // desync ("Agents N running" vs an empty tree).
    +  const tree = useMemo(() => buildSubagentTree(allSubagents(subagentsBySession)), [subagentsBySession])
     
       return (
         <OverlayView
    ```
  - `apps/desktop/src/app/shell/hooks/use-statusbar-items.tsx`

    ```diff
    @@ -22,8 +22,6 @@ import type { RuntimeReadinessResult } from '@/lib/runtime-readiness'
     import { contextBarLabel, LiveDuration, usageContextLabel } from '@/lib/statusbar'
     import { cn } from '@/lib/utils'
     import { setGlobalYolo, setSessionYolo } from '@/lib/yolo-session'
    -import { $desktopActionTasks } from '@/store/activity'
    -import { $previewServerRestartStatus } from '@/store/preview'
     import {
       $activeSessionId,
       $busy,
    ```

    ```diff
    @@ -31,11 +29,10 @@ import {
       $currentUsage,
       $sessionStartedAt,
       $turnStartedAt,
    -  $workingSessionIds,
       $yoloActive,
       setYoloActive
     } from '@/store/session'
    -import { $subagentsBySession, activeSubagentCount } from '@/store/subagents'
    +import { $subagentsBySession, activeSubagentCount, failedSubagentCount } from '@/store/subagents'
     import { $gatewayRestarting } from '@/store/system-actions'
     import {
       $backendUpdateApply,
    ```

    ```diff
    @@ -90,12 +87,9 @@ export function useStatusbarItems({
       const yoloActive = useStore($yoloActive)
       const busy = useStore($busy)
       const currentUsage = useStore($currentUsage)
    -  const desktopActionTasks = useStore($desktopActionTasks)
       const gatewayRestarting = useStore($gatewayRestarting)
    -  const previewServerRestartStatus = useStore($previewServerRestartStatus)
       const sessionStartedAt = useStore($sessionStartedAt)
       const turnStartedAt = useStore($turnStartedAt)
    -  const workingSessionIds = useStore($workingSessionIds)
       const subagentsBySession = useStore($subagentsBySession)
       const updateStatus = useStore($updateStatus)
       const updateApply = useStore($updateApply)
    ```

    ```diff
    @@ -159,24 +153,17 @@ export function useStatusbarItems({
         [gatewayLogLines, gatewayState, inferenceStatus, openCommandCenterSection, statusSnapshot]
       )
     
    -  const { bgFailed, bgRunning, subagentsRunning } = useMemo(() => {
    -    const actions = Object.values(desktopActionTasks)
    -    const running = actions.filter(t => t.status.running).length
    -    const failed = actions.filter(t => !t.status.running && (t.status.exit_code ?? 0) !== 0).length
    -    const previewRunning = previewServerRestartStatus === 'running' ? 1 : 0
    -    const previewFailed = previewServerRestartStatus === 'error' ? 1 : 0
    -
    -    const subagentsRunning = Object.values(subagentsBySession).reduce(
    -      (sum, items) => sum + activeSubagentCount(items),
    -      0
    -    )
    +  // The indicator must speak the same scope as the Spawn-tree panel it opens:
    +  // every session's subagents, never background system actions (gateway
    +  // restarts, toolset installs) which surface in their own panels.
    +  const { subagentsFailed, subagentsRunning } = useMemo(() => {
    +    const lists = Object.values(subagentsBySession)
     
         return {
    -      bgFailed: failed + previewFailed,
    -      bgRunning: workingSessionIds.length + running + previewRunning,
    -      subagentsRunning
    +      subagentsFailed: lists.reduce((sum, items) => sum + failedSubagentCount(items), 0),
    +      subagentsRunning: lists.reduce((sum, items) => sum + activeSubagentCount(items), 0)
         }
    -  }, [desktopActionTasks, previewServerRestartStatus, subagentsBySession, workingSessionIds])
    +  }, [subagentsBySession])
     
       const gatewayOpen = gatewayState === 'open'
       const gatewayConnecting = gatewayState === 'connecting'
    ```

    ```diff
    @@ -321,20 +308,18 @@ export function useStatusbarItems({
           {
             className: cn(
               agentsOpen && 'bg-accent/55 text-foreground',
    -          bgFailed > 0 && 'text-destructive hover:text-destructive'
    +          subagentsFailed > 0 && 'text-destructive hover:text-destructive'
             ),
             detail:
               subagentsRunning > 0
                 ? copy.subagents(subagentsRunning)
    -            : bgFailed > 0
    -              ? copy.failed(bgFailed)
    -              : bgRunning > 0
    -                ? copy.running(bgRunning)
    -                : undefined,
    +            : subagentsFailed > 0
    +              ? copy.failed(subagentsFailed)
    +              : undefined,
             icon:
    -          bgFailed > 0 ? (
    +          subagentsFailed > 0 ? (
                 <AlertCircle className="size-3" />
    -          ) : bgRunning > 0 || subagentsRunning > 0 ? (
    +          ) : subagentsRunning > 0 ? (
                 <Loader2 className="size-3 animate-spin" />
               ) : (
                 <Sparkles className="size-3" />
    ```

    ```diff
    @@ -356,8 +341,6 @@ export function useStatusbarItems({
         ],
         [
           agentsOpen,
    -      bgFailed,
    -      bgRunning,
           commandCenterOpen,
           copy,
           gatewayMenuContent,
    ```

    ```diff
    @@ -367,6 +350,7 @@ export function useStatusbarItems({
           inferenceReady,
           inferenceStatus?.reason,
           openAgents,
    +      subagentsFailed,
           subagentsRunning,
           toggleCommandCenter
         ]
    ```
  - `apps/desktop/src/store/subagents.test.ts`

    ```diff
    @@ -3,8 +3,10 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
     import {
       $subagentsBySession,
       activeSubagentCount,
    +  allSubagents,
       buildSubagentTree,
       clearSessionSubagents,
    +  failedSubagentCount,
       pruneDelegateFallbackSubagents,
       upsertSubagent
     } from './subagents'
    ```

    ```diff
    @@ -99,6 +101,27 @@ describe('subagent store', () => {
         expect(listFor('s1').map(item => item.id)).toEqual(['sa-0-xyz'])
       })
     
    +  // Contract: the status-bar "Agents" indicator and the Spawn-tree panel read
    +  // the same scope — every session's subagents — so a count can never point at
    +  // an empty tree (the desync behind "Agents (N)" vs "No live subagents").
    +  it('counts running/failed across every session, matching the aggregated tree', () => {
    +    upsertSubagent('s1', { goal: 'a', status: 'running', subagent_id: 'a', task_index: 0 })
    +    upsertSubagent('s1', { goal: 'b', status: 'failed', subagent_id: 'b', task_index: 1 })
    +    upsertSubagent('s2', { goal: 'c', status: 'running', subagent_id: 'c', task_index: 0 })
    +    upsertSubagent('s2', { goal: 'd', status: 'failed', subagent_id: 'd', task_index: 1 })
    +
    +    const flat = allSubagents($subagentsBySession.get())
    +    const indicatorRunning = Object.values($subagentsBySession.get()).reduce((n, l) => n + activeSubagentCount(l), 0)
    +    const indicatorFailed = Object.values($subagentsBySession.get()).reduce((n, l) => n + failedSubagentCount(l), 0)
    +    const tree = buildSubagentTree(flat)
    +
    +    // The active-session-only filter would have reported 1/1 here, not 2/2.
    +    expect(indicatorRunning).toBe(2)
    +    expect(indicatorFailed).toBe(2)
    +    expect(tree).toHaveLength(4)
    +    expect(indicatorRunning + indicatorFailed).toBe(tree.length)
    +  })
    +
       it('clears one session without touching another', () => {
         upsertSubagent('s1', { goal: 'one', status: 'running', subagent_id: 'a1', task_index: 0 })
         upsertSubagent('s2', { goal: 'two', status: 'running', subagent_id: 'a2', task_index: 0 })
    ```
  - `apps/desktop/src/store/subagents.ts`

    ```diff
    @@ -261,3 +261,10 @@ export function buildSubagentTree(items: readonly SubagentProgress[]): SubagentN
     
     export const activeSubagentCount = (items: readonly SubagentProgress[]) =>
       items.filter(item => item.status === 'queued' || item.status === 'running').length
    +
    +export const failedSubagentCount = (items: readonly SubagentProgress[]) =>
    +  items.filter(item => item.status === 'failed' || item.status === 'interrupted').length
    +
    +/** Flatten every session's subagents — the scope the Spawn-tree panel and the
    + *  status-bar indicator must agree on. */
    +export const allSubagents = (bySession: Record<string, SubagentProgress[]>) => Object.values(bySession).flat()
    ```
- **#49819: fix(desktop): unify Spawn tree scope with status bar indicator**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#52183`

## Fix status

- Merged to main in PR #52183 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-06-25`.
- Subsystem: `apps/desktop`
