8.6 KiB
Memory Tree — per-integration health strip (issue #2763)
Status: approved, ready for implementation plan Issue: tinyhumansai/openhuman#2763 Parent umbrella: #1856 Part 3 Depends on (already merged): #2719 (status panel), #1250 (per-source sync status)
Goal
Give operators a quick at-a-glance per-integration health view, directly under the four-tile Memory Tree status panel, so a stalled / low-volume tree can be traced to a single integration (e.g. Gmail) rather than the pipeline as a whole.
Non-goals
- Replacing or shrinking
MemorySourcesRegistry(the full sources list stays below as-is — it owns add / sync / remove flows). - Per-provider
Errorattribution. This needs new core work (job → source linkage) and is deferred to a follow-up issue tracked in the PR body. - Team-scoped wiki silos (Part 2 of #1856 — blocked on FR9, not in scope here).
Architecture
Frontend-only diff. Reuses the existing openhuman.memory_sync_status_list RPC (shipped by #1250) as the data source. The 575-line MemorySourcesRegistry remains as the canonical configurable-sources view; this strip is a smaller, health-focused readout colocated with the pipeline-status tiles.
┌─────────────────────────────────────────────────────────────────┐
│ MemoryTreeStatusPanel │
│ ┌─Status─┬─LastSync─┬─Chunks─┬─Wiki──┐ │
│ │ … │ … │ … │ … │ │
│ └────────┴──────────┴────────┴───────┘ │
│ │
│ ┌── Per-integration health ──────────────────────────────────┐ │
│ │ [icon] slack 5,231 chunks · 3 min ago ● Active │ │
│ │ [icon] gmail 842 chunks · 2 hr ago ● Stale │ │
│ │ [icon] notion 45 chunks · 5 d ago ● Stale │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ Auto-sync toggle ──────────────────────────────[switch]──┐ │
│ └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ MemorySourcesRegistry (existing, unchanged) │
└─────────────────────────────────────────────────────────────────┘
Data flow
useMemoryTreeStatus(existing hook inMemoryTreeStatusPanel.tsx) extendsfetchOnceto call both:memoryTreePipelineStatus()(existing)memorySyncStatusList()(existing — wrapsopenhuman.memory_sync_status_list) in parallel viaPromise.all. Returned object gains anintegrations: MemorySyncStatus[]field.
- Adaptive polling (1.5s while syncing, 4s otherwise) — the existing cadence — drives both fetches. One timer, one re-render.
- A new internal sub-component
<IntegrationHealthStrip>(same file, not exported) consumesintegrationsand renders the list. Lives insideMemoryTreeStatusPanelbetween the tile grid and the auto-sync toggle row.
Status mapping (pure TS, no core change)
type IntegrationHealth = 'active' | 'stale';
function classifyIntegration(s: MemorySyncStatus): IntegrationHealth {
return s.freshness === 'active' ? 'active' : 'stale';
}
freshness === 'active'→ Active (chunk within last 30 s)freshness === 'recent' | 'idle'→ Stale- Error state intentionally omitted; see "Deferred" below.
UI details
- One row per
MemorySyncStatus. Icon from a small built-inPROVIDER_ICONSmap insideMemoryTreeStatusPanel.tsx(keyed by sync-provider name —slack/gmail/notion/ …, distinct fromSOURCE_KIND_ICONSwhich keys bySourceKind); fallback to a generic🔌glyph for unknown providers. - Provider name: friendly label via
SOURCE_KIND_LABEL_KEYS[provider]when present, else the rawproviderstring. - "5,231 chunks · 3 min ago" — chunk count + relative time, reusing the existing
formatRelativeMs()helper fromMemoryTreeStatusPanel.tsx. - Status pill: dot color reuses
statusDotClasssemantics — sage-400 foractive, stone-400 forstale. - Empty state:
data-testid="memory-tree-integrations-empty", single line "No integrations connected" matchingMemorySources.tsxconvention. - Scroll:
max-h-48 overflow-y-autoonce past ~5 rows so the strip never dominates the panel.
i18n
New keys colocated with memoryTree.status.* in app/src/lib/i18n/en.ts:
| key | English |
|---|---|
memoryTree.status.integrationsTitle |
Per-integration health |
memoryTree.status.integrationsEmpty |
No integrations connected |
memoryTree.status.integrationActive |
Active |
memoryTree.status.integrationStale |
Stale |
memoryTree.status.integrationChunks |
{count} chunks |
All 13 non-English locales (ar, bn, de, es, fr, hi, id, it, ko, pl, pt, ru, zh-CN) get real translations in the same PR, per CLAUDE.md i18n rule. pnpm i18n:check and pnpm i18n:english:check must pass.
Testing
Vitest (MemoryTreeStatusPanel.test.tsx):
- Renders integration rows from
memory_sync_status_list(mocked). - Renders empty state when list is empty.
- Status mapping: a row with
freshness='active'showsActive;freshness='recent'andfreshness='idle'both showStale. - Icon fallback for unknown provider doesn't throw.
- Relative-time label uses
formatRelativeMs(frozen clock). - Shared poll: both
memoryTreePipelineStatusandmemorySyncStatusListare called on the same tick (mock both, advance fake timers, assert call counts).
Coverage: changed-lines ≥ 80 % (CI gate). The mapping + render branches are small and trivially testable; achievable.
No new Rust tests — no Rust changed. Existing memory_sync_status_list test coverage continues to validate the wire shape.
No new E2E spec — covered by the existing intelligence smoke test plus the unit tests above. (E2E for a render-only sub-component is overkill.)
Deviations from issue acceptance criteria
Will be noted explicitly in PR body so reviewers see them up front:
- AC #1 says
memory_tree_pipeline_statusreturns anintegrationsarray. We don't extend that RPC; we consumememory_sync_status_listinstead. Same data, cleaner contract, no schema bump. - AC #2 says status is
Active / Stale / Error. We ship Active / Stale only. Per-providerErrorrequires new core work (mem_tree_jobshas nosource_kind/source_idcolumn today; we'd have to parsepayload_jsonper row or add a column). Deferred to a follow-up issue filed alongside this PR.
Remaining ACs (list renders below status panel, empty state, polling shares parent, i18n parity, ≥80 % coverage) are met as specified.
Risks
- Visual crowding if many providers are connected. Mitigated by
max-h-48 overflow-y-auto. - Empty
memory_sync_status_listwhen chunks haven't flowed yet — the strip will render empty even for a freshly-installed integration. Acceptable for v1 (the issue's same gap); when per-provider error tracking lands, "configured but never produced chunks" can be its own state.
Out of scope (filed as follow-up)
- Per-provider
Errorstate. Open follow-up issue: "Per-provider error attribution for Memory Tree" — proposes either parsingpayload_jsonfor failed jobs to extractsource_id, or adding a typedsource_kindcolumn tomem_tree_jobs(probably the latter, with a one-shot migration).