diff --git a/app/src/components/intelligence/GraphCorePanel.test.tsx b/app/src/components/intelligence/GraphCorePanel.test.tsx
new file mode 100644
index 0000000000..0d338d7d59
--- /dev/null
+++ b/app/src/components/intelligence/GraphCorePanel.test.tsx
@@ -0,0 +1,89 @@
+import { fireEvent, render, screen } from '@testing-library/react';
+import { describe, expect, it, vi } from 'vitest';
+
+import { computeGraphCore } from '../../lib/memory/graphCore';
+import type { GraphRelation } from '../../utils/tauriCommands/memory';
+import GraphCorePanel from './GraphCorePanel';
+
+function rel(subject: string, object: string): GraphRelation {
+ return {
+ namespace: 'n',
+ subject,
+ predicate: 'p',
+ object,
+ attrs: {},
+ updatedAt: 0,
+ evidenceCount: 1,
+ orderIndex: null,
+ documentIds: [],
+ chunkIds: [],
+ };
+}
+
+// Triangle A-B-C (2-core) plus pendant D off A -> degeneracy 2, shells {2:3,1:1}.
+const cored = computeGraphCore([rel('A', 'B'), rel('B', 'C'), rel('C', 'A'), rel('A', 'D')]);
+
+// All pairs of a `size`-node clique -> every member has coreness size-1.
+function clique(prefix: string, size: number): GraphRelation[] {
+ const out: GraphRelation[] = [];
+ for (let i = 0; i < size; i += 1) {
+ for (let j = i + 1; j < size; j += 1) out.push(rel(`${prefix}${i}`, `${prefix}${j}`));
+ }
+ return out;
+}
+// 14 disjoint cliques of sizes 2..15 -> 14 distinct coreness levels (1..14),
+// i.e. 14 shells, exceeding the MAX_SHELLS=12 histogram cap by 2.
+const manyShells = computeGraphCore(
+ Array.from({ length: 14 }, (_, idx) => clique(`c${idx}_`, idx + 2)).flat()
+);
+
+describe('', () => {
+ it('renders the loading skeleton', () => {
+ render();
+ expect(screen.getByTestId('graph-core-loading')).toBeInTheDocument();
+ });
+
+ it('renders the empty state when there are no nodes', () => {
+ render();
+ expect(screen.getByText('No knowledge graph yet.')).toBeInTheDocument();
+ });
+
+ it('renders an error with a working retry button', () => {
+ const onRetry = vi.fn();
+ render();
+ expect(screen.getByRole('alert').textContent).toMatch(/graph unavailable/);
+ fireEvent.click(screen.getByRole('button', { name: 'Retry' }));
+ expect(onRetry).toHaveBeenCalledTimes(1);
+ });
+
+ it('renders metric tiles, the shell decomposition, and the ranked table', () => {
+ render();
+ expect(screen.getByText('Entities')).toBeInTheDocument();
+ expect(screen.getByText('Connections')).toBeInTheDocument();
+ expect(screen.getByText('Degeneracy')).toBeInTheDocument();
+ expect(screen.getByText('Shell decomposition')).toBeInTheDocument();
+ expect(screen.getByText('Deepest-core entities')).toBeInTheDocument();
+ // shell labels for the two coreness levels present.
+ expect(screen.getByText('2-core')).toBeInTheDocument();
+ expect(screen.getByText('1-core')).toBeInTheDocument();
+ // densest shell holds the triangle (3 entities at the 2-core).
+ expect(screen.getByText(/2-core · 3 entities/)).toBeInTheDocument();
+ });
+
+ it('badges the deepest-core members and not the periphery', () => {
+ render();
+ // three triangle members carry the core badge; the pendant D does not.
+ expect(screen.getAllByText('core')).toHaveLength(3);
+ });
+
+ it('caps the shell histogram and folds the rest into a "+N more" footer', () => {
+ expect(manyShells.shells.length).toBe(14);
+ render();
+ // densest shells survive the cap; the two shallowest (1-core, 2-core) fold away.
+ expect(screen.getByText('14-core')).toBeInTheDocument();
+ expect(screen.getByText('3-core')).toBeInTheDocument();
+ expect(screen.queryByText('2-core')).not.toBeInTheDocument();
+ expect(screen.queryByText('1-core')).not.toBeInTheDocument();
+ expect(screen.getByText('+2 more shells')).toBeInTheDocument();
+ });
+});
diff --git a/app/src/components/intelligence/GraphCorePanel.tsx b/app/src/components/intelligence/GraphCorePanel.tsx
new file mode 100644
index 0000000000..885f7245eb
--- /dev/null
+++ b/app/src/components/intelligence/GraphCorePanel.tsx
@@ -0,0 +1,256 @@
+/**
+ * Graph Core — presentational view. Pure: renders the core summary tiles
+ * (entities / connections / degeneracy), the shell decomposition, and a ranked
+ * table of the deepest-core entities. No data fetching, no clock, no randomness.
+ */
+import { useMemo } from 'react';
+
+import { useT } from '../../lib/i18n/I18nContext';
+import { type CoreResult, kCoreSize } from '../../lib/memory/graphCore';
+
+const MAX_ROWS = 25;
+// Cap the shell histogram so a high-degeneracy graph (many coreness levels)
+// can't render an unbounded run of bars and overflow the panel. Shells are
+// sorted densest-first (k DESC), so the cap keeps the most meaningful deep
+// shells and folds the rest into a "+N more" footer.
+const MAX_SHELLS = 12;
+
+interface GraphCorePanelProps {
+ result: CoreResult | null;
+ loading?: boolean;
+ error?: string | null;
+ onRetry?: () => void;
+}
+
+const GraphCorePanel = ({ result, loading, error, onRetry }: GraphCorePanelProps) => {
+ const { t } = useT();
+
+ const degeneracyCoreSize = useMemo(
+ () => (result ? kCoreSize(result, result.degeneracy) : 0),
+ [result]
+ );
+ const displayedShells = useMemo(
+ () => (result ? result.shells.slice(0, MAX_SHELLS) : []),
+ [result]
+ );
+ const hiddenShellCount = useMemo(
+ () => (result ? result.shells.length - displayedShells.length : 0),
+ [result, displayedShells]
+ );
+ // Scale bars against the largest displayed shell so the capped view stays
+ // legible even when a hidden periphery shell holds the global maximum.
+ const maxShellCount = useMemo(
+ () => displayedShells.reduce((max, s) => (s.count > max ? s.count : max), 0),
+ [displayedShells]
+ );
+
+ const intro = (
+
+
{t('graphCore.title')}
+
{t('graphCore.intro')}
+
+ );
+
+ if (loading) {
+ return (
+
+ {intro}
+
+
+ {[0, 1, 2].map(i => (
+
+ ))}
+
+ {[0, 1, 2, 3].map(i => (
+
+ ))}
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ {intro}
+
+
+ {t('graphCore.errorPrefix')} {error}
+
+ {onRetry && (
+
+ )}
+
+
+ );
+ }
+
+ if (!result || result.nodes.length === 0) {
+ return (
+
+ {intro}
+
+
+ {t('graphCore.empty')}
+
+
+ {t('graphCore.emptyHint')}
+
+
+
+ );
+ }
+
+ const rows = result.nodes.slice(0, MAX_ROWS);
+
+ return (
+
+ {intro}
+
+ {/* Metric tiles */}
+
+ {[
+ { label: t('graphCore.metricEntities'), value: result.nodeCount },
+ { label: t('graphCore.metricConnections'), value: result.edgeCount },
+ { label: t('graphCore.metricDegeneracy'), value: result.degeneracy },
+ ].map(tile => (
+
+
+ {tile.label}
+
+
+ {tile.value}
+
+
+ ))}
+
+
+ {t('graphCore.degeneracyCaption')
+ .replace('{degeneracy}', String(result.degeneracy))
+ .replace('{coreSize}', String(degeneracyCoreSize))}
+
+
+ {/* Shell decomposition */}
+
+
+ {t('graphCore.shellsHeading')}
+
+
+ {displayedShells.map(shell => (
+ -
+
+ {t('graphCore.shellLabel').replace('{k}', String(shell.k))}
+
+
+
+ {shell.count}
+
+
+ ))}
+
+ {hiddenShellCount > 0 && (
+
+ {t('graphCore.shellsMore').replace('{count}', String(hiddenShellCount))}
+
+ )}
+
+
+ {/* Ranked deepest-core entities */}
+
+
+ {t('graphCore.rankedHeading')}
+
+
+
+
+ |
+ {t('graphCore.colRank')}
+ |
+
+ {t('graphCore.colEntity')}
+ |
+
+ {t('graphCore.colCore')}
+ |
+
+ {t('graphCore.colLinks')}
+ |
+
+
+
+ {rows.map((node, i) => (
+
+ | {i + 1} |
+
+ {node.id}
+ {result.degeneracy > 0 && node.coreness === result.degeneracy && (
+
+ {t('graphCore.coreBadge')}
+
+ )}
+ |
+
+
+
+
+ {node.coreness}
+
+
+ |
+
+ {node.degree}
+ |
+
+ ))}
+
+
+
+
+ );
+};
+
+export default GraphCorePanel;
diff --git a/app/src/components/intelligence/GraphCoreTab.test.tsx b/app/src/components/intelligence/GraphCoreTab.test.tsx
new file mode 100644
index 0000000000..b4b781380c
--- /dev/null
+++ b/app/src/components/intelligence/GraphCoreTab.test.tsx
@@ -0,0 +1,61 @@
+import { fireEvent, render, screen, waitFor } from '@testing-library/react';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+
+import { computeGraphCore } from '../../lib/memory/graphCore';
+import type { GraphRelation } from '../../utils/tauriCommands/memory';
+import GraphCoreTab from './GraphCoreTab';
+
+const mockLoadCore = vi.fn();
+const mockLoadNamespaces = vi.fn();
+
+vi.mock('../../services/api/graphCoreApi', () => ({
+ loadCore: (...args: unknown[]) => mockLoadCore(...args),
+ loadNamespaces: (...args: unknown[]) => mockLoadNamespaces(...args),
+}));
+
+function rel(subject: string, object: string): GraphRelation {
+ return {
+ namespace: 'n',
+ subject,
+ predicate: 'p',
+ object,
+ attrs: {},
+ updatedAt: 0,
+ evidenceCount: 1,
+ orderIndex: null,
+ documentIds: [],
+ chunkIds: [],
+ };
+}
+
+const result = computeGraphCore([rel('A', 'B'), rel('B', 'C'), rel('C', 'A')]);
+
+describe('', () => {
+ beforeEach(() => {
+ mockLoadCore.mockReset();
+ mockLoadNamespaces.mockReset();
+ mockLoadCore.mockResolvedValue(result);
+ mockLoadNamespaces.mockResolvedValue([]);
+ });
+
+ it('loads core (all namespaces) on mount and renders the result', async () => {
+ render();
+ expect(mockLoadCore).toHaveBeenCalledWith(undefined);
+ await waitFor(() => expect(screen.getByText('Deepest-core entities')).toBeInTheDocument());
+ });
+
+ it('shows the namespace selector and re-queries on change', async () => {
+ mockLoadNamespaces.mockResolvedValueOnce(['work', 'personal']);
+ render();
+ await waitFor(() => screen.getByRole('combobox'));
+ fireEvent.change(screen.getByRole('combobox'), { target: { value: 'work' } });
+ await waitFor(() => expect(mockLoadCore).toHaveBeenCalledWith('work'));
+ });
+
+ it('surfaces an error when the load fails', async () => {
+ mockLoadCore.mockReset();
+ mockLoadCore.mockRejectedValueOnce(new Error('graph unavailable'));
+ render();
+ await waitFor(() => expect(screen.getByRole('alert').textContent).toMatch(/graph unavailable/));
+ });
+});
diff --git a/app/src/components/intelligence/GraphCoreTab.tsx b/app/src/components/intelligence/GraphCoreTab.tsx
new file mode 100644
index 0000000000..93635a36b1
--- /dev/null
+++ b/app/src/components/intelligence/GraphCoreTab.tsx
@@ -0,0 +1,108 @@
+/**
+ * Graph Core tab (container). Owns load-on-mount and the namespace selector;
+ * delegates all rendering to the pure . Read-only — the result
+ * is recomputed from the live graph, never persisted.
+ */
+import debug from 'debug';
+import { useCallback, useEffect, useRef, useState } from 'react';
+
+import { useT } from '../../lib/i18n/I18nContext';
+import type { CoreResult } from '../../lib/memory/graphCore';
+import { loadCore, loadNamespaces } from '../../services/api/graphCoreApi';
+import GraphCorePanel from './GraphCorePanel';
+
+const log = debug('graph-core:tab');
+
+const GraphCoreTab = () => {
+ const { t } = useT();
+ const [result, setResult] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [namespaces, setNamespaces] = useState([]);
+ const [namespace, setNamespace] = useState('');
+ // Monotonic token: ignore a response if a newer load has since started, so
+ // an out-of-order resolution can never overwrite the latest result.
+ const latestRequestId = useRef(0);
+
+ const load = useCallback(async (ns: string) => {
+ const requestId = (latestRequestId.current += 1);
+ log('load:start request=%d namespace=%s', requestId, ns || '(all)');
+ setLoading(true);
+ setError(null);
+ try {
+ const next = await loadCore(ns || undefined);
+ if (requestId !== latestRequestId.current) {
+ log('load:stale request=%d (newer load in flight)', requestId);
+ return;
+ }
+ log(
+ 'load:success request=%d nodes=%d degeneracy=%d',
+ requestId,
+ next.nodeCount,
+ next.degeneracy
+ );
+ setResult(next);
+ } catch (err) {
+ if (requestId !== latestRequestId.current) {
+ log('load:stale-error request=%d', requestId);
+ return;
+ }
+ log('load:error request=%d %o', requestId, err);
+ setError(err instanceof Error ? err.message : String(err));
+ } finally {
+ if (requestId === latestRequestId.current) setLoading(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ // Namespaces are optional UI sugar; a failure to list them must not block
+ // the core view, so swallow that error specifically.
+ const loadNamespaceOptions = async (): Promise => {
+ try {
+ const next = await loadNamespaces();
+ log('namespaces:loaded count=%d', next.length);
+ setNamespaces(next);
+ } catch (err) {
+ log('namespaces:error (non-blocking) %o', err);
+ setNamespaces([]);
+ }
+ };
+ void loadNamespaceOptions();
+ void load('');
+ }, [load]);
+
+ const handleNamespace = (next: string): void => {
+ setNamespace(next);
+ void load(next);
+ };
+
+ return (
+
+ {namespaces.length > 0 && (
+
+ )}
+
+ void load(namespace)}
+ />
+
+ );
+};
+
+export default GraphCoreTab;
diff --git a/app/src/lib/i18n/ar.ts b/app/src/lib/i18n/ar.ts
index d5e578503b..25667f0e72 100644
--- a/app/src/lib/i18n/ar.ts
+++ b/app/src/lib/i18n/ar.ts
@@ -4264,6 +4264,33 @@ const messages: TranslationMap = {
'settings.agents.editor.toolsDone': 'Done',
'settings.agents.editor.builtInReadonly':
'لا يمكن تعديل العوامل المدمجة. يمكنك تفعيلها أو تعطيلها أو إعادة ضبطها من قائمة العوامل.',
+ 'graphCore.colCore': 'Core',
+ 'graphCore.colEntity': 'Entity',
+ 'graphCore.colLinks': 'Links',
+ 'graphCore.colRank': '#',
+ 'graphCore.coreBadge': 'core',
+ 'graphCore.coreTitle':
+ 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.',
+ 'graphCore.degeneracyCaption': 'Densest shell: {degeneracy}-core · {coreSize} entities',
+ 'graphCore.empty': 'No knowledge graph yet.',
+ 'graphCore.emptyHint':
+ 'As the assistant records connected facts about you, the densely-linked core will surface here.',
+ 'graphCore.errorPrefix': 'Could not load the graph:',
+ 'graphCore.intro':
+ 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.',
+ 'graphCore.loading': 'Computing core structure…',
+ 'graphCore.metricConnections': 'Connections',
+ 'graphCore.metricDegeneracy': 'Degeneracy',
+ 'graphCore.metricEntities': 'Entities',
+ 'graphCore.namespaceAll': 'All namespaces',
+ 'graphCore.namespaceLabel': 'Namespace',
+ 'graphCore.rankedHeading': 'Deepest-core entities',
+ 'graphCore.retry': 'Retry',
+ 'graphCore.shellLabel': '{k}-core',
+ 'graphCore.shellsMore': '+{count} طبقة إضافية',
+ 'graphCore.shellsHeading': 'Shell decomposition',
+ 'graphCore.title': 'Graph Core',
+ 'memory.tab.core': 'Core',
'autocomplete.debounceMs': 'مهلة الإدخال (مللي ثانية)',
'autocomplete.maxChars': 'أقصى عدد لأحرف السياق',
'autocomplete.overlayTtlMs': 'مهلة الطبقة (مللي ثانية)',
diff --git a/app/src/lib/i18n/bn.ts b/app/src/lib/i18n/bn.ts
index fb196b753d..80ac61424a 100644
--- a/app/src/lib/i18n/bn.ts
+++ b/app/src/lib/i18n/bn.ts
@@ -4339,6 +4339,33 @@ const messages: TranslationMap = {
'settings.agents.editor.toolsDone': 'Done',
'settings.agents.editor.builtInReadonly':
'বিল্ট-ইন এজেন্ট সম্পাদনা করা যাবে না। আপনি এজেন্ট তালিকা থেকে সেগুলো সক্রিয়, নিষ্ক্রিয় বা পুনরায় সেট করতে পারেন।',
+ 'graphCore.colCore': 'Core',
+ 'graphCore.colEntity': 'Entity',
+ 'graphCore.colLinks': 'Links',
+ 'graphCore.colRank': '#',
+ 'graphCore.coreBadge': 'core',
+ 'graphCore.coreTitle':
+ 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.',
+ 'graphCore.degeneracyCaption': 'Densest shell: {degeneracy}-core · {coreSize} entities',
+ 'graphCore.empty': 'No knowledge graph yet.',
+ 'graphCore.emptyHint':
+ 'As the assistant records connected facts about you, the densely-linked core will surface here.',
+ 'graphCore.errorPrefix': 'Could not load the graph:',
+ 'graphCore.intro':
+ 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.',
+ 'graphCore.loading': 'Computing core structure…',
+ 'graphCore.metricConnections': 'Connections',
+ 'graphCore.metricDegeneracy': 'Degeneracy',
+ 'graphCore.metricEntities': 'Entities',
+ 'graphCore.namespaceAll': 'All namespaces',
+ 'graphCore.namespaceLabel': 'Namespace',
+ 'graphCore.rankedHeading': 'Deepest-core entities',
+ 'graphCore.retry': 'Retry',
+ 'graphCore.shellLabel': '{k}-core',
+ 'graphCore.shellsMore': '+আরও {count}টি স্তর',
+ 'graphCore.shellsHeading': 'Shell decomposition',
+ 'graphCore.title': 'Graph Core',
+ 'memory.tab.core': 'Core',
'autocomplete.debounceMs': 'ডিবাউন্স (মিলিসেকেন্ড)',
'autocomplete.maxChars': 'সর্বোচ্চ প্রসঙ্গ অক্ষর',
'autocomplete.overlayTtlMs': 'ওভারলে টাইমআউট (মিলিসেকেন্ড)',
diff --git a/app/src/lib/i18n/de.ts b/app/src/lib/i18n/de.ts
index 4b30e1572b..fef0f5e14c 100644
--- a/app/src/lib/i18n/de.ts
+++ b/app/src/lib/i18n/de.ts
@@ -4454,6 +4454,33 @@ const messages: TranslationMap = {
'settings.agents.editor.toolsDone': 'Done',
'settings.agents.editor.builtInReadonly':
'Integrierte Agenten können nicht bearbeitet werden. Sie können sie in der Agentenliste aktivieren, deaktivieren oder zurücksetzen.',
+ 'graphCore.colCore': 'Core',
+ 'graphCore.colEntity': 'Entity',
+ 'graphCore.colLinks': 'Links',
+ 'graphCore.colRank': '#',
+ 'graphCore.coreBadge': 'core',
+ 'graphCore.coreTitle':
+ 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.',
+ 'graphCore.degeneracyCaption': 'Densest shell: {degeneracy}-core · {coreSize} entities',
+ 'graphCore.empty': 'No knowledge graph yet.',
+ 'graphCore.emptyHint':
+ 'As the assistant records connected facts about you, the densely-linked core will surface here.',
+ 'graphCore.errorPrefix': 'Could not load the graph:',
+ 'graphCore.intro':
+ 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.',
+ 'graphCore.loading': 'Computing core structure…',
+ 'graphCore.metricConnections': 'Connections',
+ 'graphCore.metricDegeneracy': 'Degeneracy',
+ 'graphCore.metricEntities': 'Entities',
+ 'graphCore.namespaceAll': 'All namespaces',
+ 'graphCore.namespaceLabel': 'Namespace',
+ 'graphCore.rankedHeading': 'Deepest-core entities',
+ 'graphCore.retry': 'Retry',
+ 'graphCore.shellLabel': '{k}-core',
+ 'graphCore.shellsMore': '+{count} weitere Schalen',
+ 'graphCore.shellsHeading': 'Shell decomposition',
+ 'graphCore.title': 'Graph Core',
+ 'memory.tab.core': 'Core',
'autocomplete.debounceMs': 'Entprellung (ms)',
'autocomplete.maxChars': 'Maximale Kontextzeichen',
'autocomplete.overlayTtlMs': 'Overlay-Timeout (ms)',
diff --git a/app/src/lib/i18n/en.ts b/app/src/lib/i18n/en.ts
index ba050c75d5..561953ea8b 100644
--- a/app/src/lib/i18n/en.ts
+++ b/app/src/lib/i18n/en.ts
@@ -304,6 +304,7 @@ const en: TranslationMap = {
'memory.tab.calls': 'Calls',
'memory.tab.diagram': 'Diagram',
'memory.tab.centrality': 'Centrality',
+ 'memory.tab.core': 'Core',
'memory.tab.namespaces': 'Namespaces',
'memory.tab.timeline': 'Timeline',
'memory.tab.cohesion': 'Cohesion',
@@ -489,6 +490,33 @@ const en: TranslationMap = {
'graphCohesion.brokerTitle':
"Structural hole: this entity's neighbours aren't connected to each other — it's the sole link between them.",
+ 'graphCore.title': 'Graph Core',
+ 'graphCore.intro':
+ 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.',
+ 'graphCore.loading': 'Computing core structure…',
+ 'graphCore.errorPrefix': 'Could not load the graph:',
+ 'graphCore.retry': 'Retry',
+ 'graphCore.empty': 'No knowledge graph yet.',
+ 'graphCore.emptyHint':
+ 'As the assistant records connected facts about you, the densely-linked core will surface here.',
+ 'graphCore.namespaceLabel': 'Namespace',
+ 'graphCore.namespaceAll': 'All namespaces',
+ 'graphCore.metricEntities': 'Entities',
+ 'graphCore.metricConnections': 'Connections',
+ 'graphCore.metricDegeneracy': 'Degeneracy',
+ 'graphCore.degeneracyCaption': 'Densest shell: {degeneracy}-core · {coreSize} entities',
+ 'graphCore.shellsHeading': 'Shell decomposition',
+ 'graphCore.shellLabel': '{k}-core',
+ 'graphCore.shellsMore': '+{count} more shells',
+ 'graphCore.rankedHeading': 'Deepest-core entities',
+ 'graphCore.colRank': '#',
+ 'graphCore.colEntity': 'Entity',
+ 'graphCore.colCore': 'Core',
+ 'graphCore.colLinks': 'Links',
+ 'graphCore.coreBadge': 'core',
+ 'graphCore.coreTitle':
+ 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.',
+
// Memory Tree status panel (#1856 Part 1)
'memoryTree.status.title': 'Memory Tree',
'memoryTree.status.autoSyncLabel': 'Auto-sync',
diff --git a/app/src/lib/i18n/es.ts b/app/src/lib/i18n/es.ts
index 6feea2220d..277640ba17 100644
--- a/app/src/lib/i18n/es.ts
+++ b/app/src/lib/i18n/es.ts
@@ -4422,6 +4422,33 @@ const messages: TranslationMap = {
'settings.agents.editor.toolsDone': 'Done',
'settings.agents.editor.builtInReadonly':
'Los agentes integrados no se pueden editar. Puedes activarlos, desactivarlos o restablecerlos desde la lista de agentes.',
+ 'graphCore.colCore': 'Core',
+ 'graphCore.colEntity': 'Entity',
+ 'graphCore.colLinks': 'Links',
+ 'graphCore.colRank': '#',
+ 'graphCore.coreBadge': 'core',
+ 'graphCore.coreTitle':
+ 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.',
+ 'graphCore.degeneracyCaption': 'Densest shell: {degeneracy}-core · {coreSize} entities',
+ 'graphCore.empty': 'No knowledge graph yet.',
+ 'graphCore.emptyHint':
+ 'As the assistant records connected facts about you, the densely-linked core will surface here.',
+ 'graphCore.errorPrefix': 'Could not load the graph:',
+ 'graphCore.intro':
+ 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.',
+ 'graphCore.loading': 'Computing core structure…',
+ 'graphCore.metricConnections': 'Connections',
+ 'graphCore.metricDegeneracy': 'Degeneracy',
+ 'graphCore.metricEntities': 'Entities',
+ 'graphCore.namespaceAll': 'All namespaces',
+ 'graphCore.namespaceLabel': 'Namespace',
+ 'graphCore.rankedHeading': 'Deepest-core entities',
+ 'graphCore.retry': 'Retry',
+ 'graphCore.shellLabel': '{k}-core',
+ 'graphCore.shellsMore': '+{count} capas más',
+ 'graphCore.shellsHeading': 'Shell decomposition',
+ 'graphCore.title': 'Graph Core',
+ 'memory.tab.core': 'Core',
'autocomplete.debounceMs': 'Retardo (ms)',
'autocomplete.maxChars': 'Máximo de caracteres de contexto',
'autocomplete.overlayTtlMs': 'Tiempo de espera de superposición (ms)',
diff --git a/app/src/lib/i18n/fr.ts b/app/src/lib/i18n/fr.ts
index e3b4395ada..1bb8c77f2d 100644
--- a/app/src/lib/i18n/fr.ts
+++ b/app/src/lib/i18n/fr.ts
@@ -4437,6 +4437,33 @@ const messages: TranslationMap = {
'settings.agents.editor.toolsDone': 'Done',
'settings.agents.editor.builtInReadonly':
'Les agents intégrés ne peuvent pas être modifiés. Vous pouvez les activer, les désactiver ou les réinitialiser depuis la liste des agents.',
+ 'graphCore.colCore': 'Core',
+ 'graphCore.colEntity': 'Entity',
+ 'graphCore.colLinks': 'Links',
+ 'graphCore.colRank': '#',
+ 'graphCore.coreBadge': 'core',
+ 'graphCore.coreTitle':
+ 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.',
+ 'graphCore.degeneracyCaption': 'Densest shell: {degeneracy}-core · {coreSize} entities',
+ 'graphCore.empty': 'No knowledge graph yet.',
+ 'graphCore.emptyHint':
+ 'As the assistant records connected facts about you, the densely-linked core will surface here.',
+ 'graphCore.errorPrefix': 'Could not load the graph:',
+ 'graphCore.intro':
+ 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.',
+ 'graphCore.loading': 'Computing core structure…',
+ 'graphCore.metricConnections': 'Connections',
+ 'graphCore.metricDegeneracy': 'Degeneracy',
+ 'graphCore.metricEntities': 'Entities',
+ 'graphCore.namespaceAll': 'All namespaces',
+ 'graphCore.namespaceLabel': 'Namespace',
+ 'graphCore.rankedHeading': 'Deepest-core entities',
+ 'graphCore.retry': 'Retry',
+ 'graphCore.shellLabel': '{k}-core',
+ 'graphCore.shellsMore': '+{count} couches supplémentaires',
+ 'graphCore.shellsHeading': 'Shell decomposition',
+ 'graphCore.title': 'Graph Core',
+ 'memory.tab.core': 'Core',
'autocomplete.debounceMs': 'Anti-rebond (ms)',
'autocomplete.maxChars': 'Caractères de contexte maximum',
'autocomplete.overlayTtlMs': "Délai d'affichage (ms)",
diff --git a/app/src/lib/i18n/hi.ts b/app/src/lib/i18n/hi.ts
index b63c812398..37abf6244a 100644
--- a/app/src/lib/i18n/hi.ts
+++ b/app/src/lib/i18n/hi.ts
@@ -4347,6 +4347,33 @@ const messages: TranslationMap = {
'settings.agents.editor.toolsDone': 'Done',
'settings.agents.editor.builtInReadonly':
'बिल्ट-इन एजेंट संपादित नहीं किए जा सकते। आप एजेंट सूची से उन्हें सक्षम, अक्षम या रीसेट कर सकते हैं।',
+ 'graphCore.colCore': 'Core',
+ 'graphCore.colEntity': 'Entity',
+ 'graphCore.colLinks': 'Links',
+ 'graphCore.colRank': '#',
+ 'graphCore.coreBadge': 'core',
+ 'graphCore.coreTitle':
+ 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.',
+ 'graphCore.degeneracyCaption': 'Densest shell: {degeneracy}-core · {coreSize} entities',
+ 'graphCore.empty': 'No knowledge graph yet.',
+ 'graphCore.emptyHint':
+ 'As the assistant records connected facts about you, the densely-linked core will surface here.',
+ 'graphCore.errorPrefix': 'Could not load the graph:',
+ 'graphCore.intro':
+ 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.',
+ 'graphCore.loading': 'Computing core structure…',
+ 'graphCore.metricConnections': 'Connections',
+ 'graphCore.metricDegeneracy': 'Degeneracy',
+ 'graphCore.metricEntities': 'Entities',
+ 'graphCore.namespaceAll': 'All namespaces',
+ 'graphCore.namespaceLabel': 'Namespace',
+ 'graphCore.rankedHeading': 'Deepest-core entities',
+ 'graphCore.retry': 'Retry',
+ 'graphCore.shellLabel': '{k}-core',
+ 'graphCore.shellsMore': '+{count} और परतें',
+ 'graphCore.shellsHeading': 'Shell decomposition',
+ 'graphCore.title': 'Graph Core',
+ 'memory.tab.core': 'Core',
'autocomplete.debounceMs': 'डिबाउंस (ms)',
'autocomplete.maxChars': 'अधिकतम संदर्भ वर्ण',
'autocomplete.overlayTtlMs': 'ओवरले समय-समाप्ति (ms)',
diff --git a/app/src/lib/i18n/id.ts b/app/src/lib/i18n/id.ts
index b83ee91377..ddd7b2f67b 100644
--- a/app/src/lib/i18n/id.ts
+++ b/app/src/lib/i18n/id.ts
@@ -4356,6 +4356,33 @@ const messages: TranslationMap = {
'settings.agents.editor.toolsDone': 'Done',
'settings.agents.editor.builtInReadonly':
'Agen bawaan tidak dapat diedit. Anda dapat mengaktifkan, menonaktifkan, atau meresetnya dari daftar agen.',
+ 'graphCore.colCore': 'Core',
+ 'graphCore.colEntity': 'Entity',
+ 'graphCore.colLinks': 'Links',
+ 'graphCore.colRank': '#',
+ 'graphCore.coreBadge': 'core',
+ 'graphCore.coreTitle':
+ 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.',
+ 'graphCore.degeneracyCaption': 'Densest shell: {degeneracy}-core · {coreSize} entities',
+ 'graphCore.empty': 'No knowledge graph yet.',
+ 'graphCore.emptyHint':
+ 'As the assistant records connected facts about you, the densely-linked core will surface here.',
+ 'graphCore.errorPrefix': 'Could not load the graph:',
+ 'graphCore.intro':
+ 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.',
+ 'graphCore.loading': 'Computing core structure…',
+ 'graphCore.metricConnections': 'Connections',
+ 'graphCore.metricDegeneracy': 'Degeneracy',
+ 'graphCore.metricEntities': 'Entities',
+ 'graphCore.namespaceAll': 'All namespaces',
+ 'graphCore.namespaceLabel': 'Namespace',
+ 'graphCore.rankedHeading': 'Deepest-core entities',
+ 'graphCore.retry': 'Retry',
+ 'graphCore.shellLabel': '{k}-core',
+ 'graphCore.shellsMore': '+{count} lapisan lainnya',
+ 'graphCore.shellsHeading': 'Shell decomposition',
+ 'graphCore.title': 'Graph Core',
+ 'memory.tab.core': 'Core',
'autocomplete.debounceMs': 'Debounce (md)',
'autocomplete.maxChars': 'Karakter konteks maks',
'autocomplete.overlayTtlMs': 'Batas waktu overlay (md)',
diff --git a/app/src/lib/i18n/it.ts b/app/src/lib/i18n/it.ts
index 2426c1472c..9127c42109 100644
--- a/app/src/lib/i18n/it.ts
+++ b/app/src/lib/i18n/it.ts
@@ -4414,6 +4414,33 @@ const messages: TranslationMap = {
'settings.agents.editor.toolsDone': 'Done',
'settings.agents.editor.builtInReadonly':
"Gli agenti integrati non possono essere modificati. Puoi abilitarli, disabilitarli o reimpostarli dall'elenco degli agenti.",
+ 'graphCore.colCore': 'Core',
+ 'graphCore.colEntity': 'Entity',
+ 'graphCore.colLinks': 'Links',
+ 'graphCore.colRank': '#',
+ 'graphCore.coreBadge': 'core',
+ 'graphCore.coreTitle':
+ 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.',
+ 'graphCore.degeneracyCaption': 'Densest shell: {degeneracy}-core · {coreSize} entities',
+ 'graphCore.empty': 'No knowledge graph yet.',
+ 'graphCore.emptyHint':
+ 'As the assistant records connected facts about you, the densely-linked core will surface here.',
+ 'graphCore.errorPrefix': 'Could not load the graph:',
+ 'graphCore.intro':
+ 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.',
+ 'graphCore.loading': 'Computing core structure…',
+ 'graphCore.metricConnections': 'Connections',
+ 'graphCore.metricDegeneracy': 'Degeneracy',
+ 'graphCore.metricEntities': 'Entities',
+ 'graphCore.namespaceAll': 'All namespaces',
+ 'graphCore.namespaceLabel': 'Namespace',
+ 'graphCore.rankedHeading': 'Deepest-core entities',
+ 'graphCore.retry': 'Retry',
+ 'graphCore.shellLabel': '{k}-core',
+ 'graphCore.shellsMore': '+{count} livelli in più',
+ 'graphCore.shellsHeading': 'Shell decomposition',
+ 'graphCore.title': 'Graph Core',
+ 'memory.tab.core': 'Core',
'autocomplete.debounceMs': 'Debounce (ms)',
'autocomplete.maxChars': 'Caratteri massimi di contesto',
'autocomplete.overlayTtlMs': 'Timeout overlay (ms)',
diff --git a/app/src/lib/i18n/ko.ts b/app/src/lib/i18n/ko.ts
index ec68b50ab4..5ddb19204f 100644
--- a/app/src/lib/i18n/ko.ts
+++ b/app/src/lib/i18n/ko.ts
@@ -4306,6 +4306,33 @@ const messages: TranslationMap = {
'settings.agents.editor.toolsDone': 'Done',
'settings.agents.editor.builtInReadonly':
'기본 제공 에이전트는 편집할 수 없습니다. 에이전트 목록에서 활성화, 비활성화 또는 초기화할 수 있습니다.',
+ 'graphCore.colCore': 'Core',
+ 'graphCore.colEntity': 'Entity',
+ 'graphCore.colLinks': 'Links',
+ 'graphCore.colRank': '#',
+ 'graphCore.coreBadge': 'core',
+ 'graphCore.coreTitle':
+ 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.',
+ 'graphCore.degeneracyCaption': 'Densest shell: {degeneracy}-core · {coreSize} entities',
+ 'graphCore.empty': 'No knowledge graph yet.',
+ 'graphCore.emptyHint':
+ 'As the assistant records connected facts about you, the densely-linked core will surface here.',
+ 'graphCore.errorPrefix': 'Could not load the graph:',
+ 'graphCore.intro':
+ 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.',
+ 'graphCore.loading': 'Computing core structure…',
+ 'graphCore.metricConnections': 'Connections',
+ 'graphCore.metricDegeneracy': 'Degeneracy',
+ 'graphCore.metricEntities': 'Entities',
+ 'graphCore.namespaceAll': 'All namespaces',
+ 'graphCore.namespaceLabel': 'Namespace',
+ 'graphCore.rankedHeading': 'Deepest-core entities',
+ 'graphCore.retry': 'Retry',
+ 'graphCore.shellLabel': '{k}-core',
+ 'graphCore.shellsMore': '레이어 {count}개 더',
+ 'graphCore.shellsHeading': 'Shell decomposition',
+ 'graphCore.title': 'Graph Core',
+ 'memory.tab.core': 'Core',
'autocomplete.debounceMs': '디바운스 (ms)',
'autocomplete.maxChars': '최대 컨텍스트 문자 수',
'autocomplete.overlayTtlMs': '오버레이 시간 초과 (ms)',
diff --git a/app/src/lib/i18n/pl.ts b/app/src/lib/i18n/pl.ts
index b80e01d0d4..669cca66de 100644
--- a/app/src/lib/i18n/pl.ts
+++ b/app/src/lib/i18n/pl.ts
@@ -4413,6 +4413,33 @@ const messages: TranslationMap = {
'settings.agents.editor.toolsDone': 'Done',
'settings.agents.editor.builtInReadonly':
'Wbudowanych agentów nie można edytować. Możesz je włączyć, wyłączyć lub zresetować z poziomu listy agentów.',
+ 'graphCore.colCore': 'Core',
+ 'graphCore.colEntity': 'Entity',
+ 'graphCore.colLinks': 'Links',
+ 'graphCore.colRank': '#',
+ 'graphCore.coreBadge': 'core',
+ 'graphCore.coreTitle':
+ 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.',
+ 'graphCore.degeneracyCaption': 'Densest shell: {degeneracy}-core · {coreSize} entities',
+ 'graphCore.empty': 'No knowledge graph yet.',
+ 'graphCore.emptyHint':
+ 'As the assistant records connected facts about you, the densely-linked core will surface here.',
+ 'graphCore.errorPrefix': 'Could not load the graph:',
+ 'graphCore.intro':
+ 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.',
+ 'graphCore.loading': 'Computing core structure…',
+ 'graphCore.metricConnections': 'Connections',
+ 'graphCore.metricDegeneracy': 'Degeneracy',
+ 'graphCore.metricEntities': 'Entities',
+ 'graphCore.namespaceAll': 'All namespaces',
+ 'graphCore.namespaceLabel': 'Namespace',
+ 'graphCore.rankedHeading': 'Deepest-core entities',
+ 'graphCore.retry': 'Retry',
+ 'graphCore.shellLabel': '{k}-core',
+ 'graphCore.shellsMore': '+{count} więcej warstw',
+ 'graphCore.shellsHeading': 'Shell decomposition',
+ 'graphCore.title': 'Graph Core',
+ 'memory.tab.core': 'Core',
'autocomplete.debounceMs': 'Opóźnienie (ms)',
'autocomplete.maxChars': 'Maksymalna liczba znaków kontekstu',
'autocomplete.overlayTtlMs': 'Limit czasu nakładki (ms)',
diff --git a/app/src/lib/i18n/pt.ts b/app/src/lib/i18n/pt.ts
index 75c601bd2f..ef24cdb8d6 100644
--- a/app/src/lib/i18n/pt.ts
+++ b/app/src/lib/i18n/pt.ts
@@ -4411,6 +4411,33 @@ const messages: TranslationMap = {
'settings.agents.editor.toolsDone': 'Done',
'settings.agents.editor.builtInReadonly':
'Agentes integrados não podem ser editados. Você pode ativá-los, desativá-los ou redefini-los na lista de agentes.',
+ 'graphCore.colCore': 'Core',
+ 'graphCore.colEntity': 'Entity',
+ 'graphCore.colLinks': 'Links',
+ 'graphCore.colRank': '#',
+ 'graphCore.coreBadge': 'core',
+ 'graphCore.coreTitle':
+ 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.',
+ 'graphCore.degeneracyCaption': 'Densest shell: {degeneracy}-core · {coreSize} entities',
+ 'graphCore.empty': 'No knowledge graph yet.',
+ 'graphCore.emptyHint':
+ 'As the assistant records connected facts about you, the densely-linked core will surface here.',
+ 'graphCore.errorPrefix': 'Could not load the graph:',
+ 'graphCore.intro':
+ 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.',
+ 'graphCore.loading': 'Computing core structure…',
+ 'graphCore.metricConnections': 'Connections',
+ 'graphCore.metricDegeneracy': 'Degeneracy',
+ 'graphCore.metricEntities': 'Entities',
+ 'graphCore.namespaceAll': 'All namespaces',
+ 'graphCore.namespaceLabel': 'Namespace',
+ 'graphCore.rankedHeading': 'Deepest-core entities',
+ 'graphCore.retry': 'Retry',
+ 'graphCore.shellLabel': '{k}-core',
+ 'graphCore.shellsMore': '+{count} camadas a mais',
+ 'graphCore.shellsHeading': 'Shell decomposition',
+ 'graphCore.title': 'Graph Core',
+ 'memory.tab.core': 'Core',
'autocomplete.debounceMs': 'Atraso (ms)',
'autocomplete.maxChars': 'Máximo de caracteres de contexto',
'autocomplete.overlayTtlMs': 'Tempo limite da sobreposição (ms)',
diff --git a/app/src/lib/i18n/ru.ts b/app/src/lib/i18n/ru.ts
index 592a691e0c..992a691580 100644
--- a/app/src/lib/i18n/ru.ts
+++ b/app/src/lib/i18n/ru.ts
@@ -4381,6 +4381,33 @@ const messages: TranslationMap = {
'settings.agents.editor.toolsDone': 'Done',
'settings.agents.editor.builtInReadonly':
'Встроенные агенты нельзя редактировать. Вы можете включить, отключить или сбросить их в списке агентов.',
+ 'graphCore.colCore': 'Core',
+ 'graphCore.colEntity': 'Entity',
+ 'graphCore.colLinks': 'Links',
+ 'graphCore.colRank': '#',
+ 'graphCore.coreBadge': 'core',
+ 'graphCore.coreTitle':
+ 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.',
+ 'graphCore.degeneracyCaption': 'Densest shell: {degeneracy}-core · {coreSize} entities',
+ 'graphCore.empty': 'No knowledge graph yet.',
+ 'graphCore.emptyHint':
+ 'As the assistant records connected facts about you, the densely-linked core will surface here.',
+ 'graphCore.errorPrefix': 'Could not load the graph:',
+ 'graphCore.intro':
+ 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.',
+ 'graphCore.loading': 'Computing core structure…',
+ 'graphCore.metricConnections': 'Connections',
+ 'graphCore.metricDegeneracy': 'Degeneracy',
+ 'graphCore.metricEntities': 'Entities',
+ 'graphCore.namespaceAll': 'All namespaces',
+ 'graphCore.namespaceLabel': 'Namespace',
+ 'graphCore.rankedHeading': 'Deepest-core entities',
+ 'graphCore.retry': 'Retry',
+ 'graphCore.shellLabel': '{k}-core',
+ 'graphCore.shellsMore': '+{count} слоёв',
+ 'graphCore.shellsHeading': 'Shell decomposition',
+ 'graphCore.title': 'Graph Core',
+ 'memory.tab.core': 'Core',
'autocomplete.debounceMs': 'Задержка (мс)',
'autocomplete.maxChars': 'Макс. символов контекста',
'autocomplete.overlayTtlMs': 'Тайм-аут наложения (мс)',
diff --git a/app/src/lib/i18n/zh-CN.ts b/app/src/lib/i18n/zh-CN.ts
index 6169e9a647..fd4814fd3a 100644
--- a/app/src/lib/i18n/zh-CN.ts
+++ b/app/src/lib/i18n/zh-CN.ts
@@ -4130,6 +4130,33 @@ const messages: TranslationMap = {
'settings.agents.editor.toolsDone': 'Done',
'settings.agents.editor.builtInReadonly':
'内置智能体不可编辑。您可以在智能体列表中启用、禁用或重置它们。',
+ 'graphCore.colCore': 'Core',
+ 'graphCore.colEntity': 'Entity',
+ 'graphCore.colLinks': 'Links',
+ 'graphCore.colRank': '#',
+ 'graphCore.coreBadge': 'core',
+ 'graphCore.coreTitle':
+ 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.',
+ 'graphCore.degeneracyCaption': 'Densest shell: {degeneracy}-core · {coreSize} entities',
+ 'graphCore.empty': 'No knowledge graph yet.',
+ 'graphCore.emptyHint':
+ 'As the assistant records connected facts about you, the densely-linked core will surface here.',
+ 'graphCore.errorPrefix': 'Could not load the graph:',
+ 'graphCore.intro':
+ 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.',
+ 'graphCore.loading': 'Computing core structure…',
+ 'graphCore.metricConnections': 'Connections',
+ 'graphCore.metricDegeneracy': 'Degeneracy',
+ 'graphCore.metricEntities': 'Entities',
+ 'graphCore.namespaceAll': 'All namespaces',
+ 'graphCore.namespaceLabel': 'Namespace',
+ 'graphCore.rankedHeading': 'Deepest-core entities',
+ 'graphCore.retry': 'Retry',
+ 'graphCore.shellLabel': '{k}-core',
+ 'graphCore.shellsMore': '+{count} 个层',
+ 'graphCore.shellsHeading': 'Shell decomposition',
+ 'graphCore.title': 'Graph Core',
+ 'memory.tab.core': 'Core',
'autocomplete.debounceMs': '防抖 (毫秒)',
'autocomplete.maxChars': '最大上下文字符数',
'autocomplete.overlayTtlMs': '覆盖层超时 (ms)',
diff --git a/app/src/lib/memory/graphCore.test.ts b/app/src/lib/memory/graphCore.test.ts
new file mode 100644
index 0000000000..3e22f6cc1b
--- /dev/null
+++ b/app/src/lib/memory/graphCore.test.ts
@@ -0,0 +1,177 @@
+import { describe, expect, it } from 'vitest';
+
+import type { GraphRelation } from '../../utils/tauriCommands/memory';
+import { computeGraphCore, kCoreSize } from './graphCore';
+
+function rel(subject: string, object: string, predicate = 'knows'): GraphRelation {
+ return {
+ namespace: 'work',
+ subject,
+ predicate,
+ object,
+ attrs: {},
+ updatedAt: 0,
+ evidenceCount: 1,
+ orderIndex: null,
+ documentIds: [],
+ chunkIds: [],
+ };
+}
+
+function coreness(result: ReturnType, id: string): number {
+ const node = result.nodes.find(n => n.id === id);
+ if (!node) throw new Error(`node ${id} not found`);
+ return node.coreness;
+}
+
+describe('computeGraphCore — basic shapes', () => {
+ it('returns an empty result for no relations', () => {
+ const r = computeGraphCore([]);
+ expect(r.nodeCount).toBe(0);
+ expect(r.edgeCount).toBe(0);
+ expect(r.degeneracy).toBe(0);
+ expect(r.shells).toEqual([]);
+ expect(r.nodes).toEqual([]);
+ });
+
+ it('a single triangle is a uniform 2-core', () => {
+ const r = computeGraphCore([rel('A', 'B'), rel('B', 'C'), rel('C', 'A')]);
+ expect(r.nodeCount).toBe(3);
+ expect(r.edgeCount).toBe(3);
+ expect(r.degeneracy).toBe(2);
+ for (const id of ['A', 'B', 'C']) expect(coreness(r, id)).toBe(2);
+ expect(r.shells).toEqual([{ k: 2, count: 3 }]);
+ });
+
+ it('a path is a 1-core (no node survives to the 2-core)', () => {
+ const r = computeGraphCore([rel('A', 'B'), rel('B', 'C')]);
+ expect(r.degeneracy).toBe(1);
+ for (const id of ['A', 'B', 'C']) expect(coreness(r, id)).toBe(1);
+ expect(r.shells).toEqual([{ k: 1, count: 3 }]);
+ });
+
+ it('a star (tree) is a 1-core: the hub has high degree but coreness 1', () => {
+ const r = computeGraphCore([rel('X', 'A'), rel('X', 'B'), rel('X', 'C')]);
+ expect(r.degeneracy).toBe(1);
+ expect(r.nodes.find(n => n.id === 'X')?.degree).toBe(3);
+ expect(coreness(r, 'X')).toBe(1); // high degree, shallow core
+ expect(r.shells).toEqual([{ k: 1, count: 4 }]);
+ });
+
+ it('K4 (complete graph on four) is a uniform 3-core', () => {
+ const r = computeGraphCore([
+ rel('A', 'B'),
+ rel('A', 'C'),
+ rel('A', 'D'),
+ rel('B', 'C'),
+ rel('B', 'D'),
+ rel('C', 'D'),
+ ]);
+ expect(r.nodeCount).toBe(4);
+ expect(r.edgeCount).toBe(6);
+ expect(r.degeneracy).toBe(3);
+ for (const id of ['A', 'B', 'C', 'D']) expect(coreness(r, id)).toBe(3);
+ });
+});
+
+describe('computeGraphCore — core vs periphery separation', () => {
+ // Triangle A-B-C (a 2-core) with a pendant D hanging off A.
+ const r = computeGraphCore([rel('A', 'B'), rel('B', 'C'), rel('C', 'A'), rel('A', 'D')]);
+
+ it('peels the pendant to coreness 1 while the triangle stays at 2', () => {
+ expect(coreness(r, 'A')).toBe(2);
+ expect(coreness(r, 'B')).toBe(2);
+ expect(coreness(r, 'C')).toBe(2);
+ expect(coreness(r, 'D')).toBe(1);
+ expect(r.degeneracy).toBe(2);
+ });
+
+ it('reports the shell decomposition (k DESC)', () => {
+ expect(r.shells).toEqual([
+ { k: 2, count: 3 },
+ { k: 1, count: 1 },
+ ]);
+ });
+
+ it('A keeps the high degree but shares the core with the triangle', () => {
+ // A is adjacent to B, C and D -> degree 3, yet coreness 2 (pendant excluded).
+ expect(r.nodes.find(n => n.id === 'A')?.degree).toBe(3);
+ });
+
+ it('sorts nodes coreness DESC, then degree DESC, then id ASC', () => {
+ // Core nodes first; among the coreness-2 trio, A (degree 3) leads B, C.
+ expect(r.nodes[0]).toMatchObject({ id: 'A', coreness: 2, degree: 3 });
+ expect(r.nodes[r.nodes.length - 1].id).toBe('D'); // the periphery sinks last
+ });
+});
+
+describe('kCoreSize', () => {
+ const r = computeGraphCore([rel('A', 'B'), rel('B', 'C'), rel('C', 'A'), rel('A', 'D')]);
+
+ it('counts nodes with coreness >= k (nested cores)', () => {
+ expect(kCoreSize(r, 1)).toBe(4); // everyone is in the 1-core
+ expect(kCoreSize(r, 2)).toBe(3); // only the triangle is in the 2-core
+ expect(kCoreSize(r, 3)).toBe(0); // no 3-core exists here
+ });
+});
+
+describe('computeGraphCore — normalization & determinism', () => {
+ it('drops the self-loop EDGE but keeps the endpoint as a node', () => {
+ const r = computeGraphCore([rel('A', 'A'), rel('A', 'B'), rel('B', 'C'), rel('C', 'A')]);
+ expect(r.nodeCount).toBe(3);
+ expect(r.edgeCount).toBe(3);
+ expect(r.degeneracy).toBe(2); // self-loop edge ignored -> plain triangle
+ });
+
+ it('preserves an entity whose only relation is a self-loop (no empty result)', () => {
+ // A user with the single fact "Alice→Alice" must still see Alice in the
+ // graph (degree 0, coreness 0), not vanish into the empty state.
+ const r = computeGraphCore([rel('Alice', 'Alice')]);
+ expect(r.nodeCount).toBe(1);
+ expect(r.edgeCount).toBe(0);
+ expect(r.degeneracy).toBe(0);
+ expect(r.nodes).toEqual([{ id: 'Alice', degree: 0, coreness: 0 }]);
+ expect(r.shells).toEqual([{ k: 0, count: 1 }]);
+ });
+
+ it('collapses parallel edges and ignores direction', () => {
+ const r = computeGraphCore([
+ rel('A', 'B', 'knows'),
+ rel('B', 'A', 'likes'),
+ rel('A', 'B', 'trusts'),
+ rel('B', 'C'),
+ rel('C', 'A'),
+ ]);
+ expect(r.edgeCount).toBe(3);
+ expect(r.degeneracy).toBe(2);
+ });
+
+ it('drops malformed relations (non-string subject/object)', () => {
+ const malformed = { ...rel('A', 'B'), subject: 42 as unknown as string };
+ const r = computeGraphCore([rel('A', 'B'), rel('B', 'C'), rel('C', 'A'), malformed]);
+ expect(r.nodeCount).toBe(3);
+ expect(r.degeneracy).toBe(2);
+ });
+
+ it('treats "Alice" and "alice" as distinct nodes (no case-folding)', () => {
+ const r = computeGraphCore([rel('Alice', 'Bob'), rel('alice', 'Bob')]);
+ expect(r.nodeCount).toBe(3);
+ });
+
+ it('is order-independent: shuffled input yields identical output', () => {
+ const edges = [
+ rel('A', 'B'),
+ rel('B', 'C'),
+ rel('C', 'A'),
+ rel('A', 'D'),
+ rel('D', 'B'),
+ rel('D', 'C'),
+ rel('E', 'A'),
+ ];
+ const forward = computeGraphCore(edges);
+ const reversed = computeGraphCore([...edges].reverse());
+ const rotated = computeGraphCore([...edges.slice(3), ...edges.slice(0, 3)]);
+ expect(reversed).toEqual(forward);
+ expect(rotated).toEqual(forward);
+ });
+});
diff --git a/app/src/lib/memory/graphCore.ts b/app/src/lib/memory/graphCore.ts
new file mode 100644
index 0000000000..583279ab9e
--- /dev/null
+++ b/app/src/lib/memory/graphCore.ts
@@ -0,0 +1,214 @@
+/**
+ * Graph Core — pure k-core decomposition engine.
+ *
+ * The memory knowledge graph is a set of (subject)-[predicate]->(object)
+ * triples. Where the Centrality lens asks "which entities are important"
+ * (PageRank), this lens asks a complementary question: "how DEEP in the
+ * densely-connected core does each entity sit".
+ *
+ * The k-core of a graph is the maximal subgraph in which every vertex has at
+ * least k neighbours INSIDE that subgraph. A vertex's CORENESS is the largest k
+ * for which it survives in the k-core — found by repeatedly peeling away the
+ * lowest-degree vertex. The DEGENERACY of the whole graph is the maximum
+ * coreness: the depth of its densest shell.
+ *
+ * Why it matters: coreness separates the load-bearing CORE of the user's
+ * knowledge (entities mutually reinforced by many interlinked facts, high
+ * coreness) from the PERIPHERY (leaves and bridges, coreness 1). Neither a
+ * degree count nor PageRank captures this — a hub with many one-off pendants has
+ * high degree but coreness 1, while a modest entity embedded in a dense cluster
+ * has low degree but high coreness.
+ *
+ * Everything here is PURE and DETERMINISTIC: no React, no RPC, no clock, no
+ * randomness. Coreness is a graph INVARIANT (independent of peel order), and the
+ * peeling is seeded from a canonically id-sorted vertex list, so the same graph
+ * always yields byte-identical numbers — never dependent on insertion order —
+ * and every branch is unit-testable.
+ *
+ * Load-bearing design choices (do not "fix" without reading the tests):
+ * - Entity identity is the raw string AS-IS: NO trimming, NO case-folding —
+ * matching the sibling lenses, so "Alice" / "alice" stay distinct nodes.
+ * - The graph is UNDIRECTED and SIMPLE: direction is dropped, parallel edges
+ * (same unordered pair) collapse to one, and self-loop EDGES (subject ===
+ * object) are dropped — a self-loop is not a neighbour and cannot deepen a
+ * core. The endpoint of a self-loop is still REGISTERED as a node, so a
+ * user whose only recorded fact is "A→A" still appears with coreness 0
+ * rather than vanishing into an empty result.
+ */
+import type { GraphRelation } from '../../utils/tauriCommands/memory';
+
+export interface CoreNode {
+ id: string;
+ degree: number; // undirected degree in the full graph
+ coreness: number; // largest k such that this node is in the k-core
+}
+
+export interface CoreShell {
+ k: number; // coreness level
+ count: number; // number of nodes whose coreness is EXACTLY k
+}
+
+export interface CoreResult {
+ nodes: CoreNode[]; // sorted coreness DESC, then degree DESC, then id ASC
+ nodeCount: number;
+ edgeCount: number; // distinct undirected edges (self-loops excluded)
+ degeneracy: number; // max coreness over all nodes (0 if empty)
+ shells: CoreShell[]; // shell decomposition, sorted k DESC
+}
+
+function isRelation(relation: GraphRelation): boolean {
+ return typeof relation.subject === 'string' && typeof relation.object === 'string';
+}
+
+/** Undirected simple-graph adjacency: self-loop EDGES and parallel edges are
+ * removed, but a self-loop's endpoint is still registered as a (neighbour-less)
+ * NODE so a user whose only recorded fact is "A→A" still appears in the graph
+ * with degree 0 and coreness 0, rather than vanishing into an empty result. */
+function buildAdjacency(relations: GraphRelation[]): Map> {
+ const adjacency = new Map>();
+ const neighbours = (id: string): Set => {
+ let set = adjacency.get(id);
+ if (set === undefined) {
+ set = new Set();
+ adjacency.set(id, set);
+ }
+ return set;
+ };
+ for (const relation of relations) {
+ if (!isRelation(relation)) continue;
+ const { subject, object } = relation;
+ if (subject === object) {
+ // Self-loop: register the endpoint as a node but add no self-edge.
+ neighbours(subject);
+ continue;
+ }
+ neighbours(subject).add(object);
+ neighbours(object).add(subject);
+ }
+ return adjacency;
+}
+
+/**
+ * Core numbers via the Batagelj–Zaversnik O(V+E) bucket algorithm. `neighbours`
+ * is an index-based adjacency list; the returned array holds each vertex's
+ * coreness at the matching index. The vertex set must already be in a canonical
+ * order (we feed it id-sorted) so intermediate bucketing is reproducible.
+ */
+function corenessByIndex(neighbours: number[][]): number[] {
+ const n = neighbours.length;
+ const degree = new Array(n);
+ let maxDegree = 0;
+ for (let i = 0; i < n; i += 1) {
+ degree[i] = neighbours[i].length;
+ if (degree[i] > maxDegree) maxDegree = degree[i];
+ }
+
+ // Bin-sort vertices by current degree into vert[], tracking each vertex's
+ // position in pos[]. bin[d] marks where the degree-d block starts.
+ const bin = new Array(maxDegree + 1).fill(0);
+ for (let i = 0; i < n; i += 1) bin[degree[i]] += 1;
+ let start = 0;
+ for (let d = 0; d <= maxDegree; d += 1) {
+ const count = bin[d];
+ bin[d] = start;
+ start += count;
+ }
+ const pos = new Array(n);
+ const vert = new Array(n);
+ for (let i = 0; i < n; i += 1) {
+ pos[i] = bin[degree[i]];
+ vert[pos[i]] = i;
+ bin[degree[i]] += 1;
+ }
+ // Shift bin starts back: bin[d] now again points at the degree-d block start.
+ for (let d = maxDegree; d >= 1; d -= 1) bin[d] = bin[d - 1];
+ bin[0] = 0;
+
+ // Peel: process vertices in increasing current degree. When a vertex is
+ // removed, each still-present higher-degree neighbour drops one degree and
+ // slides one slot left into the next-lower bucket.
+ for (let i = 0; i < n; i += 1) {
+ const v = vert[i];
+ for (const u of neighbours[v]) {
+ if (degree[u] > degree[v]) {
+ const du = degree[u];
+ const pu = pos[u];
+ const pw = bin[du];
+ const w = vert[pw];
+ if (u !== w) {
+ pos[u] = pw;
+ vert[pu] = w;
+ pos[w] = pu;
+ vert[pw] = u;
+ }
+ bin[du] += 1;
+ degree[u] -= 1;
+ }
+ }
+ }
+
+ return degree; // degree[i] is now the core number of vertex i
+}
+
+/** Compute the k-core decomposition of the memory graph. PURE. */
+export function computeGraphCore(relations: GraphRelation[]): CoreResult {
+ const adjacency = buildAdjacency(relations);
+
+ // Canonical, id-sorted vertex ordering -> reproducible bucketing.
+ const ids = [...adjacency.keys()].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
+ const indexOf = new Map();
+ ids.forEach((id, i) => indexOf.set(id, i));
+
+ let edgeDegreeSum = 0;
+ const neighbours: number[][] = ids.map(id => {
+ const set = adjacency.get(id);
+ const list: number[] = [];
+ if (set) {
+ for (const other of set) {
+ const idx = indexOf.get(other);
+ if (idx !== undefined) list.push(idx);
+ }
+ }
+ edgeDegreeSum += list.length;
+ return list;
+ });
+
+ const coreness = corenessByIndex(neighbours.map(list => [...list]));
+
+ const nodes: CoreNode[] = ids.map((id, i) => ({
+ id,
+ degree: neighbours[i].length,
+ coreness: coreness[i],
+ }));
+
+ nodes.sort((a, b) => {
+ if (b.coreness !== a.coreness) return b.coreness - a.coreness;
+ if (b.degree !== a.degree) return b.degree - a.degree;
+ return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
+ });
+
+ let degeneracy = 0;
+ const shellCounts = new Map();
+ for (const node of nodes) {
+ if (node.coreness > degeneracy) degeneracy = node.coreness;
+ shellCounts.set(node.coreness, (shellCounts.get(node.coreness) ?? 0) + 1);
+ }
+ const shells: CoreShell[] = [...shellCounts.entries()]
+ .map(([k, count]) => ({ k, count }))
+ .sort((a, b) => b.k - a.k);
+
+ return { nodes, nodeCount: ids.length, edgeCount: edgeDegreeSum / 2, degeneracy, shells };
+}
+
+/**
+ * Size of the k-core: the number of nodes whose coreness is >= k (the nested
+ * subgraph in which every member has at least k neighbours among the members).
+ * Pure; derived entirely from the result.
+ */
+export function kCoreSize(result: CoreResult, k: number): number {
+ let size = 0;
+ for (const node of result.nodes) {
+ if (node.coreness >= k) size += 1;
+ }
+ return size;
+}
diff --git a/app/src/pages/Intelligence.tsx b/app/src/pages/Intelligence.tsx
index 8e4fdc4e54..287d5432d4 100644
--- a/app/src/pages/Intelligence.tsx
+++ b/app/src/pages/Intelligence.tsx
@@ -1,6 +1,9 @@
import { useCallback, useEffect, useState } from 'react';
import { ConfirmationModal } from '../components/intelligence/ConfirmationModal';
+import DiagramViewerTab from '../components/intelligence/DiagramViewerTab';
+import GraphCentralityTab from '../components/intelligence/GraphCentralityTab';
+import GraphCoreTab from '../components/intelligence/GraphCoreTab';
import IntelligenceSubconsciousTab from '../components/intelligence/IntelligenceSubconsciousTab';
import IntelligenceTasksTab from '../components/intelligence/IntelligenceTasksTab';
import MemorySection from '../components/intelligence/MemorySection';
@@ -19,7 +22,15 @@ import type {
} from '../types/intelligence';
import AgentWorkflows from './AgentWorkflows';
-type IntelligenceTab = 'memory' | 'subconscious' | 'tasks' | 'workflows' | 'council';
+type IntelligenceTab =
+ | 'memory'
+ | 'subconscious'
+ | 'tasks'
+ | 'workflows'
+ | 'diagram'
+ | 'centrality'
+ | 'core'
+ | 'council';
export default function Intelligence() {
const { t } = useT();
@@ -97,6 +108,9 @@ export default function Intelligence() {
label: t('memory.tab.workflows'),
description: t('memory.tab.workflowsDescription'),
},
+ { id: 'diagram', label: t('memory.tab.diagram') },
+ { id: 'centrality', label: t('memory.tab.centrality') },
+ { id: 'core', label: t('memory.tab.core') },
{ id: 'council', label: t('memory.tab.council') },
];
const activeTabDef = tabs.find(tab => tab.id === activeTab);
@@ -185,6 +199,12 @@ export default function Intelligence() {
{activeTab === 'workflows' && }
+ {activeTab === 'diagram' && }
+
+ {activeTab === 'centrality' && }
+
+ {activeTab === 'core' && }
+
{activeTab === 'council' && }
diff --git a/app/src/services/api/graphCoreApi.test.ts b/app/src/services/api/graphCoreApi.test.ts
new file mode 100644
index 0000000000..9ff9261e30
--- /dev/null
+++ b/app/src/services/api/graphCoreApi.test.ts
@@ -0,0 +1,74 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+
+import { computeGraphCore } from '../../lib/memory/graphCore';
+import type { GraphRelation } from '../../utils/tauriCommands/memory';
+import { graphCoreApi, loadCore, loadNamespaces } from './graphCoreApi';
+
+const mockGraphQuery = vi.fn();
+const mockListNamespaces = vi.fn();
+
+vi.mock('../../utils/tauriCommands/memory', () => ({
+ memoryGraphQuery: (...args: unknown[]) => mockGraphQuery(...args),
+ memoryListNamespaces: (...args: unknown[]) => mockListNamespaces(...args),
+}));
+
+function rel(subject: string, object: string): GraphRelation {
+ return {
+ namespace: 'work',
+ subject,
+ predicate: 'p',
+ object,
+ attrs: {},
+ updatedAt: 0,
+ evidenceCount: 1,
+ orderIndex: null,
+ documentIds: [],
+ chunkIds: [],
+ };
+}
+
+describe('graphCoreApi.loadCore', () => {
+ beforeEach(() => {
+ mockGraphQuery.mockReset();
+ });
+
+ it('passes the namespace through and returns the engine result for those triples', async () => {
+ const triples = [rel('A', 'B'), rel('B', 'C'), rel('C', 'A')];
+ mockGraphQuery.mockResolvedValueOnce(triples);
+ const out = await loadCore('work');
+ expect(mockGraphQuery).toHaveBeenCalledWith('work');
+ expect(out).toEqual(computeGraphCore(triples));
+ expect(out.degeneracy).toBe(2);
+ });
+
+ it('queries all namespaces when none is given', async () => {
+ mockGraphQuery.mockResolvedValueOnce([]);
+ const out = await loadCore();
+ expect(mockGraphQuery).toHaveBeenCalledWith(undefined);
+ expect(out.nodes).toEqual([]);
+ expect(out.nodeCount).toBe(0);
+ });
+
+ it('propagates query errors', async () => {
+ mockGraphQuery.mockRejectedValueOnce(new Error('graph unavailable'));
+ await expect(loadCore()).rejects.toThrow('graph unavailable');
+ });
+});
+
+describe('graphCoreApi.loadNamespaces', () => {
+ beforeEach(() => {
+ mockListNamespaces.mockReset();
+ });
+
+ it('returns the namespace list from the RPC', async () => {
+ mockListNamespaces.mockResolvedValueOnce(['work', 'personal']);
+ expect(await loadNamespaces()).toEqual(['work', 'personal']);
+ });
+});
+
+describe('graphCoreApi object', () => {
+ it('exposes the public surface', () => {
+ expect(typeof graphCoreApi.loadCore).toBe('function');
+ expect(typeof graphCoreApi.loadNamespaces).toBe('function');
+ });
+});
diff --git a/app/src/services/api/graphCoreApi.ts b/app/src/services/api/graphCoreApi.ts
new file mode 100644
index 0000000000..bc33b7dfe4
--- /dev/null
+++ b/app/src/services/api/graphCoreApi.ts
@@ -0,0 +1,29 @@
+/**
+ * RPC facade for Graph Core (k-core decomposition).
+ *
+ * Adds ZERO new core surface. It composes two already-shipped JSON-RPC wrappers:
+ * - memoryGraphQuery (openhuman.memory_graph_query) — the triples
+ * - memoryListNamespaces (openhuman.memory_list_namespaces) — the selector
+ * and delegates all math to the pure, deterministic engine. Read-only: there is
+ * no persistence — the result is always reproducible from the current graph.
+ */
+import debug from 'debug';
+
+import { computeGraphCore, type CoreResult } from '../../lib/memory/graphCore';
+import { memoryGraphQuery, memoryListNamespaces } from '../../utils/tauriCommands/memory';
+
+const log = debug('graph-core:api');
+
+/** Fetch the graph relations for a namespace (or all) and decompose into cores. */
+export async function loadCore(namespace?: string): Promise {
+ const relations = await memoryGraphQuery(namespace);
+ log('loadCore namespace=%s relations=%d', namespace ?? '(all)', relations.length);
+ return computeGraphCore(relations);
+}
+
+/** List the namespaces available for the namespace selector. */
+export async function loadNamespaces(): Promise {
+ return memoryListNamespaces();
+}
+
+export const graphCoreApi = { loadCore, loadNamespaces };