--- phase: 28-ollama-integration plan: 03 subsystem: dashboard tags: [hermes, ollama, stateJson, jsonb, heartbeat, dashboard] # Dependency graph requires: - phase: 28-01 provides: ollama.ts service with detectOllama, listOllamaModels, getRecommendedModel provides: - getOllamaMemoryUsage() in ollama.ts — queries /api/ps for active model VRAM usage - Hermes stateJson merge in heartbeat.ts updateRuntimeState — stores hermesModel + hermesMemoryBytes after each run - HermesRuntimeCard component in AgentDetail.tsx — displays model, native skill count, VRAM in AgentOverview affects: [28-02, 29-default-provider] # Tech tracking tech-stack: added: [] patterns: - COALESCE jsonb concat pattern for stateJson merge (no overwrite) - Best-effort Ollama /api/ps probe (error caught, returns null gracefully) - Native skill count derived from agentsApi.skills in UI (avoids cross-DB query in heartbeat) key-files: created: [] modified: - server/src/services/ollama.ts - server/src/services/heartbeat.ts - ui/src/pages/AgentDetail.tsx key-decisions: - "hermesNativeSkillCount derived from agentsApi.skills in HermesRuntimeCard (not stored in stateJson) — avoids cross-DB query in heartbeat path" - "COALESCE jsonb concat used for stateJson merge — prevents overwriting existing fields from other heartbeat writers" - "getOllamaMemoryUsage catches all errors and returns null — Ollama absence or model-not-loaded both show Not loaded" patterns-established: - "Pattern: Use COALESCE(column, '{}'::jsonb) || patch::jsonb for safe jsonb merge in Drizzle" - "Pattern: Hermes-specific stateJson written in updateRuntimeState conditional on adapterType === hermes_local" requirements-completed: [HERM-06, HERM-07] # Metrics duration: 12min completed: 2026-04-01 --- # Phase 28 Plan 03: Hermes Runtime Dashboard Summary **Hermes heartbeat now persists model name + VRAM via jsonb merge, and AgentOverview renders a HermesRuntimeCard showing model, native skill count, and memory usage.** ## Tasks Completed | Task | Description | Commit | |------|-------------|--------| | 1 | Add getOllamaMemoryUsage + stateJson merge in heartbeat | dbdc62aa | | 2 | Add HermesRuntimeCard in AgentOverview | 7458753a | ## What Was Built ### Server: getOllamaMemoryUsage (ollama.ts) New exported function queries Ollama `/api/ps` with a 3-second AbortController timeout. Finds the running model by `name` or `model` field, returns `size_vram` bytes. Returns `null` on any error (graceful degradation per Pitfall 5 from RESEARCH). ### Server: Hermes stateJson Merge (heartbeat.ts) After the existing `db.update(agentRuntimeState)` cost tracking block in `updateRuntimeState`, a `hermes_local`-gated block merges `hermesModel` and `hermesMemoryBytes` into `stateJson` using Postgres jsonb concat: ```sql COALESCE(stateJson, '{}'::jsonb) || '{"hermesModel": ..., "hermesMemoryBytes": ...}'::jsonb ``` This never overwrites other fields already stored in stateJson (Pitfall 3 from RESEARCH). ### UI: HermesRuntimeCard (AgentDetail.tsx) New component defined before `AgentOverview`, rendered inside it gated by `agent.adapterType === "hermes_local" && runtimeState`. Shows: - **Model**: `stateJson.hermesModel` or "Not set" - **Native Skills**: count from `agentsApi.skills(agentId).entries` filtered by `originLabel === "Hermes skill"` - **Memory (VRAM)**: formatted from `stateJson.hermesMemoryBytes` or "Not loaded" ## Deviations from Plan ### Auto-fixed Issues **1. [Rule 1 - Bug] Plan referenced `adapterEntries` but type has `entries`** - **Found during:** Task 2 - **Issue:** The plan's action block referenced `skillsData.adapterEntries` but `AgentSkillSnapshot` has `entries: AgentSkillEntry[]` - **Fix:** Used `skillsData.entries` in the implementation - **Files modified:** ui/src/pages/AgentDetail.tsx - **Commit:** 7458753a **2. [Rule 1 - Bug] Removed unused `createRequire` import from ollama.ts** - **Found during:** Task 1 - **Issue:** Plan 01 left a `createRequire` import in ollama.ts that was unused - **Fix:** Removed the unused import when modifying the file - **Files modified:** server/src/services/ollama.ts - **Commit:** dbdc62aa ## Known Stubs None — all data flows are wired (stateJson written by heartbeat, read by HermesRuntimeCard). Model name and memory bytes will be `null`/`undefined` until a Hermes run completes, which is correct behavior (displays "Not set" / "Not loaded"). ## HERM-06 Verification (No Code Change) Cost tracking for Hermes + Ollama runs correctly returns $0.00: - `result.costUsd` is `undefined` for local Ollama runs - `normalizeBilledCostCents(undefined, billingType)` returns `0` - The `if (additionalCostCents > 0 || hasTokenUsage)` guard suppresses cost events when no token data emitted - This is correct behavior per RESEARCH Pitfall 6 ## Self-Check: PASSED - `/opt/nexus/.claude/worktrees/agent-ad37cce3/server/src/services/ollama.ts` — exists with getOllamaMemoryUsage - `/opt/nexus/.claude/worktrees/agent-ad37cce3/server/src/services/heartbeat.ts` — exists with hermes_local block - `/opt/nexus/.claude/worktrees/agent-ad37cce3/ui/src/pages/AgentDetail.tsx` — exists with HermesRuntimeCard - Commits dbdc62aa, 7458753a — confirmed in git log