From 07093e899b8a1b0fd1df21c704d9e901bbe49ece Mon Sep 17 00:00:00 2001 From: sanil-23 Date: Fri, 5 Jun 2026 02:17:54 +0530 Subject: [PATCH] feat(memory): Notion doc-aware versioned memory tree + page-content ingest (#3378) Co-authored-by: Claude Opus 4.8 (1M context) --- .../intelligence/MemoryWorkspace.test.tsx | 69 +++ .../intelligence/MemoryWorkspace.tsx | 27 + .../intelligence/memoryGraphLayout.test.ts | 9 +- .../intelligence/memoryGraphLayout.ts | 20 +- src/openhuman/composio/ops_tests.rs | 2 + src/openhuman/memory/ingest_pipeline.rs | 56 ++- src/openhuman/memory/read_rpc.rs | 8 + src/openhuman/memory/read_rpc_tests.rs | 40 ++ src/openhuman/memory_queue/handlers/mod.rs | 92 +++- src/openhuman/memory_queue/types.rs | 94 +++- .../memory_store/chunks/connection.rs | 11 + .../memory_store/chunks/store_tests.rs | 4 + src/openhuman/memory_store/content/atomic.rs | 31 +- src/openhuman/memory_store/content/paths.rs | 125 +++++ src/openhuman/memory_store/content/read.rs | 2 + src/openhuman/memory_store/traits.rs | 2 + src/openhuman/memory_store/trees/store.rs | 20 +- .../memory_store/trees/store_tests.rs | 2 + src/openhuman/memory_store/trees/types.rs | 19 + .../composio/providers/notion/ingest.rs | 300 +++++++----- .../composio/providers/notion/provider.rs | 96 +++- .../composio/providers/notion/sync.rs | 59 +++ src/openhuman/memory_tree/ingest.rs | 2 + .../memory_tree/retrieval/drill_down.rs | 193 +++++++- src/openhuman/memory_tree/tree/bucket_seal.rs | 460 +++++++++++++++++- .../memory_tree/tree/bucket_seal_tests.rs | 270 ++++++++++ src/openhuman/memory_tree/tree/mod.rs | 5 +- tests/memory_core_threads_raw_coverage_e2e.rs | 4 + ...mory_sync_tree_round21_raw_coverage_e2e.rs | 2 + tests/memory_threads_raw_coverage_e2e.rs | 2 + .../memory_tree_sync_deep_raw_coverage_e2e.rs | 2 + 31 files changed, 1881 insertions(+), 147 deletions(-) create mode 100644 app/src/components/intelligence/MemoryWorkspace.test.tsx diff --git a/app/src/components/intelligence/MemoryWorkspace.test.tsx b/app/src/components/intelligence/MemoryWorkspace.test.tsx new file mode 100644 index 000000000..0273088df --- /dev/null +++ b/app/src/components/intelligence/MemoryWorkspace.test.tsx @@ -0,0 +1,69 @@ +import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { memoryTreeGraphExport } from '../../utils/tauriCommands'; +import { MemoryWorkspace } from './MemoryWorkspace'; + +// Stub the i18n hook and the heavy child panels so the test renders just the +// MemoryWorkspace shell (graph fetch effect + the refresh control + poll). +vi.mock('../../lib/i18n/I18nContext', () => ({ useT: () => ({ t: (k: string) => k }) })); +vi.mock('./MemoryGraph', () => ({ MemoryGraph: () => null })); +vi.mock('./MemorySourcesRegistry', () => ({ MemorySourcesRegistry: () => null })); +vi.mock('./MemoryTreeStatusPanel', () => ({ MemoryTreeStatusPanel: () => null })); +vi.mock('./ObsidianVaultSection', () => ({ ObsidianVaultSection: () => null })); +vi.mock('./SyncAuditPanel', () => ({ SyncAuditPanel: () => null })); +vi.mock('./WhatsAppMemorySection', () => ({ WhatsAppMemorySection: () => null })); +vi.mock('../../utils/tauriCommands', () => ({ + memoryTreeGraphExport: vi.fn().mockResolvedValue({ nodes: [], edges: [] }), + memoryTreeFlushNow: vi.fn().mockResolvedValue(undefined), + memoryTreeResetTree: vi.fn().mockResolvedValue(undefined), + memoryTreeWipeAll: vi.fn().mockResolvedValue(undefined), +})); + +const graphExportMock = vi.mocked(memoryTreeGraphExport); + +describe('', () => { + beforeEach(() => { + graphExportMock.mockClear(); + }); + afterEach(() => { + vi.useRealTimers(); + }); + + it('re-exports the graph when the refresh button is clicked', async () => { + render(); + // Mount runs the initial graph load. + await waitFor(() => expect(graphExportMock).toHaveBeenCalledTimes(1)); + + fireEvent.click(screen.getByTestId('memory-graph-refresh')); + + // Bumping graphVersion re-runs the load effect. + await waitFor(() => expect(graphExportMock).toHaveBeenCalledTimes(2)); + }); + + it('polls the graph on a 30s tick, and skips the tick while the tab is hidden', async () => { + vi.useFakeTimers(); + render(); + // Flush the mount-time async load under fake timers. + await act(async () => { + await vi.advanceTimersByTimeAsync(0); + }); + const afterMount = graphExportMock.mock.calls.length; + + // Visible tab → the 30s tick re-pulls the graph. + await act(async () => { + await vi.advanceTimersByTimeAsync(30_000); + }); + expect(graphExportMock.mock.calls.length).toBeGreaterThan(afterMount); + + // Backgrounded tab → the tick is skipped (no extra RPC churn). + const hiddenSpy = vi.spyOn(document, 'hidden', 'get').mockReturnValue(true); + const afterVisible = graphExportMock.mock.calls.length; + await act(async () => { + await vi.advanceTimersByTimeAsync(30_000); + }); + expect(graphExportMock.mock.calls.length).toBe(afterVisible); + + hiddenSpy.mockRestore(); + }); +}); diff --git a/app/src/components/intelligence/MemoryWorkspace.tsx b/app/src/components/intelligence/MemoryWorkspace.tsx index 511103f4b..e15ff42f9 100644 --- a/app/src/components/intelligence/MemoryWorkspace.tsx +++ b/app/src/components/intelligence/MemoryWorkspace.tsx @@ -111,6 +111,22 @@ export function MemoryWorkspace({ onToast }: MemoryWorkspaceProps) { }; }, []); + // Live refresh: re-pull the graph every 30s while this tab is mounted so it + // reflects background tree growth (e.g. seal_document jobs draining as + // Notion syncs) without a manual refresh. The Memory tab unmounts this + // component when inactive, which clears the interval — so the poll only runs + // while the tab is actually open. Ticks are skipped while the window is + // backgrounded to avoid needless RPC churn; the next visible tick catches up. + useEffect(() => { + const GRAPH_POLL_MS = 30_000; + const id = setInterval(() => { + if (typeof document !== 'undefined' && document.hidden) return; + console.debug('[ui-flow][memory-workspace] graph poll tick → bump version'); + setGraphVersion(v => v + 1); + }, GRAPH_POLL_MS); + return () => clearInterval(id); + }, []); + const handleWipe = useCallback(async () => { // Two-step confirm so accidental clicks can't nuke a workspace. const ok = window.confirm(t('workspace.wipeConfirm')); @@ -236,6 +252,17 @@ export function MemoryWorkspace({ onToast }: MemoryWorkspaceProps) { data-testid="memory-actions">
+