{"repo":"NousResearch/hermes-agent","public_id":"49808","display_label":"Fix desynced agent count between UI components","summary":"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.","subsystem":"apps/desktop","counts":{"members":3,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":52183,"merged_at":"2026-06-25T00:03:12+00:00"},"resolution":{"state":"resolved","by":"pr-52183"},"freshness":{"state":"verified","last_verified_at":"2026-06-25T00:03:12+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":49808,"pr_number":52183,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-06-25T00:03:12+00:00"}],"members":[{"kind":"issue","number":49808,"title":"Desktop: Spawn tree shows empty state while status bar reports 'Agents 2 running' — desynced subagent state","url":null,"state":"closed"},{"kind":"pr","number":49819,"title":"fix(desktop): unify Spawn tree scope with status bar indicator","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#52183","number":52183}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":52183,"title":"fix(desktop): make Agents indicator match the Spawn-tree panel","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["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":[{"file":"apps/desktop/src/app/agents/index.tsx","hunks":["@@ -9,9 +9,9 @@ import { type Translations, useI18n } from '@/i18n'\n import { AlertCircle, CheckCircle2, Sparkles } from '@/lib/icons'\n import { useEnterAnimation } from '@/lib/use-enter-animation'\n import { cn } from '@/lib/utils'\n-import { $activeSessionId } from '@/store/session'\n import {\n   $subagentsBySession,\n+  allSubagents,\n   buildSubagentTree,\n   type SubagentNode,\n   type SubagentStatus,","@@ -77,15 +77,12 @@ interface AgentsViewProps {\n \n export function AgentsView({ onClose }: AgentsViewProps) {\n   const { t } = useI18n()\n-  const activeSessionId = useStore($activeSessionId)\n   const subagentsBySession = useStore($subagentsBySession)\n \n-  const activeSubagents = useMemo(\n-    () => (activeSessionId ? (subagentsBySession[activeSessionId] ?? []) : []),\n-    [activeSessionId, subagentsBySession]\n-  )\n-\n-  const tree = useMemo(() => buildSubagentTree(activeSubagents), [activeSubagents])\n+  // Aggregate every session, matching the status-bar indicator — a subagent\n+  // running in a background session must still be visible here, or the two\n+  // desync (\"Agents N running\" vs an empty tree).\n+  const tree = useMemo(() => buildSubagentTree(allSubagents(subagentsBySession)), [subagentsBySession])\n \n   return (\n     <OverlayView"]},{"file":"apps/desktop/src/app/shell/hooks/use-statusbar-items.tsx","hunks":["@@ -22,8 +22,6 @@ import type { RuntimeReadinessResult } from '@/lib/runtime-readiness'\n import { contextBarLabel, LiveDuration, usageContextLabel } from '@/lib/statusbar'\n import { cn } from '@/lib/utils'\n import { setGlobalYolo, setSessionYolo } from '@/lib/yolo-session'\n-import { $desktopActionTasks } from '@/store/activity'\n-import { $previewServerRestartStatus } from '@/store/preview'\n import {\n   $activeSessionId,\n   $busy,","@@ -31,11 +29,10 @@ import {\n   $currentUsage,\n   $sessionStartedAt,\n   $turnStartedAt,\n-  $workingSessionIds,\n   $yoloActive,\n   setYoloActive\n } from '@/store/session'\n-import { $subagentsBySession, activeSubagentCount } from '@/store/subagents'\n+import { $subagentsBySession, activeSubagentCount, failedSubagentCount } from '@/store/subagents'\n import { $gatewayRestarting } from '@/store/system-actions'\n import {\n   $backendUpdateApply,","@@ -90,12 +87,9 @@ export function useStatusbarItems({\n   const yoloActive = useStore($yoloActive)\n   const busy = useStore($busy)\n   const currentUsage = useStore($currentUsage)\n-  const desktopActionTasks = useStore($desktopActionTasks)\n   const gatewayRestarting = useStore($gatewayRestarting)\n-  const previewServerRestartStatus = useStore($previewServerRestartStatus)\n   const sessionStartedAt = useStore($sessionStartedAt)\n   const turnStartedAt = useStore($turnStartedAt)\n-  const workingSessionIds = useStore($workingSessionIds)\n   const subagentsBySession = useStore($subagentsBySession)\n   const updateStatus = useStore($updateStatus)\n   const updateApply = useStore($updateApply)","@@ -159,24 +153,17 @@ export function useStatusbarItems({\n     [gatewayLogLines, gatewayState, inferenceStatus, openCommandCenterSection, statusSnapshot]\n   )\n \n-  const { bgFailed, bgRunning, subagentsRunning } = useMemo(() => {\n-    const actions = Object.values(desktopActionTasks)\n-    const running = actions.filter(t => t.status.running).length\n-    const failed = actions.filter(t => !t.status.running && (t.status.exit_code ?? 0) !== 0).length\n-    const previewRunning = previewServerRestartStatus === 'running' ? 1 : 0\n-    const previewFailed = previewServerRestartStatus === 'error' ? 1 : 0\n-\n-    const subagentsRunning = Object.values(subagentsBySession).reduce(\n-      (sum, items) => sum + activeSubagentCount(items),\n-      0\n-    )\n+  // The indicator must speak the same scope as the Spawn-tree panel it opens:\n+  // every session's subagents, never background system actions (gateway\n+  // restarts, toolset installs) which surface in their own panels.\n+  const { subagentsFailed, subagentsRunning } = useMemo(() => {\n+    const lists = Object.values(subagentsBySession)\n \n     return {\n-      bgFailed: failed + previewFailed,\n-      bgRunning: workingSessionIds.length + running + previewRunning,\n-      subagentsRunning\n+      subagentsFailed: lists.reduce((sum, items) => sum + failedSubagentCount(items), 0),\n+      subagentsRunning: lists.reduce((sum, items) => sum + activeSubagentCount(items), 0)\n     }\n-  }, [desktopActionTasks, previewServerRestartStatus, subagentsBySession, workingSessionIds])\n+  }, [subagentsBySession])\n \n   const gatewayOpen = gatewayState === 'open'\n   const gatewayConnecting = gatewayState === 'connecting'","@@ -321,20 +308,18 @@ export function useStatusbarItems({\n       {\n         className: cn(\n           agentsOpen && 'bg-accent/55 text-foreground',\n-          bgFailed > 0 && 'text-destructive hover:text-destructive'\n+          subagentsFailed > 0 && 'text-destructive hover:text-destructive'\n         ),\n         detail:\n           subagentsRunning > 0\n             ? copy.subagents(subagentsRunning)\n-            : bgFailed > 0\n-              ? copy.failed(bgFailed)\n-              : bgRunning > 0\n-                ? copy.running(bgRunning)\n-                : undefined,\n+            : subagentsFailed > 0\n+              ? copy.failed(subagentsFailed)\n+              : undefined,\n         icon:\n-          bgFailed > 0 ? (\n+          subagentsFailed > 0 ? (\n             <AlertCircle className=\"size-3\" />\n-          ) : bgRunning > 0 || subagentsRunning > 0 ? (\n+          ) : subagentsRunning > 0 ? (\n             <Loader2 className=\"size-3 animate-spin\" />\n           ) : (\n             <Sparkles className=\"size-3\" />","@@ -356,8 +341,6 @@ export function useStatusbarItems({\n     ],\n     [\n       agentsOpen,\n-      bgFailed,\n-      bgRunning,\n       commandCenterOpen,\n       copy,\n       gatewayMenuContent,","@@ -367,6 +350,7 @@ export function useStatusbarItems({\n       inferenceReady,\n       inferenceStatus?.reason,\n       openAgents,\n+      subagentsFailed,\n       subagentsRunning,\n       toggleCommandCenter\n     ]"]},{"file":"apps/desktop/src/store/subagents.test.ts","hunks":["@@ -3,8 +3,10 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'\n import {\n   $subagentsBySession,\n   activeSubagentCount,\n+  allSubagents,\n   buildSubagentTree,\n   clearSessionSubagents,\n+  failedSubagentCount,\n   pruneDelegateFallbackSubagents,\n   upsertSubagent\n } from './subagents'","@@ -99,6 +101,27 @@ describe('subagent store', () => {\n     expect(listFor('s1').map(item => item.id)).toEqual(['sa-0-xyz'])\n   })\n \n+  // Contract: the status-bar \"Agents\" indicator and the Spawn-tree panel read\n+  // the same scope — every session's subagents — so a count can never point at\n+  // an empty tree (the desync behind \"Agents (N)\" vs \"No live subagents\").\n+  it('counts running/failed across every session, matching the aggregated tree', () => {\n+    upsertSubagent('s1', { goal: 'a', status: 'running', subagent_id: 'a', task_index: 0 })\n+    upsertSubagent('s1', { goal: 'b', status: 'failed', subagent_id: 'b', task_index: 1 })\n+    upsertSubagent('s2', { goal: 'c', status: 'running', subagent_id: 'c', task_index: 0 })\n+    upsertSubagent('s2', { goal: 'd', status: 'failed', subagent_id: 'd', task_index: 1 })\n+\n+    const flat = allSubagents($subagentsBySession.get())\n+    const indicatorRunning = Object.values($subagentsBySession.get()).reduce((n, l) => n + activeSubagentCount(l), 0)\n+    const indicatorFailed = Object.values($subagentsBySession.get()).reduce((n, l) => n + failedSubagentCount(l), 0)\n+    const tree = buildSubagentTree(flat)\n+\n+    // The active-session-only filter would have reported 1/1 here, not 2/2.\n+    expect(indicatorRunning).toBe(2)\n+    expect(indicatorFailed).toBe(2)\n+    expect(tree).toHaveLength(4)\n+    expect(indicatorRunning + indicatorFailed).toBe(tree.length)\n+  })\n+\n   it('clears one session without touching another', () => {\n     upsertSubagent('s1', { goal: 'one', status: 'running', subagent_id: 'a1', task_index: 0 })\n     upsertSubagent('s2', { goal: 'two', status: 'running', subagent_id: 'a2', task_index: 0 })"]},{"file":"apps/desktop/src/store/subagents.ts","hunks":["@@ -261,3 +261,10 @@ export function buildSubagentTree(items: readonly SubagentProgress[]): SubagentN\n \n export const activeSubagentCount = (items: readonly SubagentProgress[]) =>\n   items.filter(item => item.status === 'queued' || item.status === 'running').length\n+\n+export const failedSubagentCount = (items: readonly SubagentProgress[]) =>\n+  items.filter(item => item.status === 'failed' || item.status === 'interrupted').length\n+\n+/** Flatten every session's subagents — the scope the Spawn-tree panel and the\n+ *  status-bar indicator must agree on. */\n+export const allSubagents = (bySession: Record<string, SubagentProgress[]>) => Object.values(bySession).flat()\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}