NousResearch/hermes-agent

Fix desynced agent count between UI components

Cluster 49808 · apps/desktop. Data as of

Nothing open on GitHub. Fix merged in #52183 on 25 Jun 2026. Check it covers your case.

The short answer

0open issues
0open PRs
3items grouped
Known?
Yes. 3 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #52183. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this resolved, deciding PR #52183. That is separate from the fix event.
Still open
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.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #52183 fix(desktop): make Agents indicator match the Spawn-tree panel

    merged to main

    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

    Show the diff
    • apps/desktop/src/app/agents/index.tsx
      @@ -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,
      @@ -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
      @@ -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,
      @@ -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,
      @@ -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)
      @@ -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'
      @@ -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" />
      @@ -356,8 +341,6 @@ export function useStatusbarItems({
           ],
           [
             agentsOpen,
      -      bgFailed,
      -      bgRunning,
             commandCenterOpen,
             copy,
             gatewayMenuContent,
      @@ -367,6 +350,7 @@ export function useStatusbarItems({
             inferenceReady,
             inferenceStatus?.reason,
             openAgents,
      +      subagentsFailed,
             subagentsRunning,
             toggleCommandCenter
           ]
    • apps/desktop/src/store/subagents.test.ts
      @@ -3,8 +3,10 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
       import {
         $subagentsBySession,
         activeSubagentCount,
      +  allSubagents,
         buildSubagentTree,
         clearSessionSubagents,
      +  failedSubagentCount,
         pruneDelegateFallbackSubagents,
         upsertSubagent
       } from './subagents'
      @@ -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
      @@ -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()

1 PR closed without merging

From the close comments: superseded by another PR 1

  • #49819 fix(desktop): unify Spawn tree scope with status bar indicator · superseded by another PR → #52183
Closed issues · 1
  • Closed#49808Desktop: Spawn tree shows empty state while status bar reports 'Agents 2 running' — desynced subagent state
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.67: the share of item 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.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
apps/desktop
Merged PRs
#52183
Closed issues
1
Attempts
1 PR closed without merging
Fix link checked
25 Jun 2026

For your agent

The same facts, typed.