mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
fix(react-ui): surface layered pipeline status in Data Sync (GH-4690) (#5113)
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
*/
|
||||
import debug from 'debug';
|
||||
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useT } from '../../lib/i18n/I18nContext';
|
||||
import { CoreStateContext } from '../../providers/coreStateContext';
|
||||
@@ -35,10 +36,19 @@ import {
|
||||
openhumanGetMemorySyncSettings,
|
||||
openhumanUpdateMemorySyncSettings,
|
||||
} from '../../utils/tauriCommands/config';
|
||||
import { memoryTreeFlushSource } from '../../utils/tauriCommands/memoryTree';
|
||||
import {
|
||||
memoryTreeFlushSource,
|
||||
memoryTreePipelineStatus,
|
||||
type MemoryTreePipelineStatus,
|
||||
} from '../../utils/tauriCommands/memoryTree';
|
||||
import Button from '../ui/Button';
|
||||
import { AddMemorySourceDialog } from './AddMemorySourceDialog';
|
||||
import { ConfirmationModal } from './ConfirmationModal';
|
||||
import {
|
||||
deriveSourcePipelineHealth,
|
||||
pipelineIssueMessageKey,
|
||||
type SourcePipelineHealth,
|
||||
} from './sourcePipelineStatus';
|
||||
import { SourceSettingsPanel } from './SourceSettingsPanel';
|
||||
|
||||
const log = debug('intelligence:memory-sync');
|
||||
@@ -124,6 +134,7 @@ export function MemorySourcesRegistry({
|
||||
pollIntervalMs = 5000,
|
||||
}: MemorySourcesRegistryProps) {
|
||||
const { t } = useT();
|
||||
const navigate = useNavigate();
|
||||
// Read the core snapshot directly (not via the throwing `useCoreState`
|
||||
// hook) so this component still renders in unit tests that mount it
|
||||
// without a CoreStateProvider — there `ctx` is null and `isAuthenticated`
|
||||
@@ -132,6 +143,11 @@ export function MemorySourcesRegistry({
|
||||
const isAuthenticated = coreState?.snapshot.auth.isAuthenticated ?? false;
|
||||
const [sources, setSources] = useState<MemorySourceEntry[]>([]);
|
||||
const [statuses, setStatuses] = useState<SourceStatus[]>([]);
|
||||
// Global downstream pipeline health (GH-4690) — the same snapshot the
|
||||
// Brain > Memory > Sync panel renders. Folded into each row so a source that
|
||||
// ingested but failed embeddings/extraction/tree-build shows a warning rather
|
||||
// than a clean synced badge. `null` until the first poll resolves.
|
||||
const [pipeline, setPipeline] = useState<MemoryTreePipelineStatus | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
// RC#1 (#3295): use a Set so multiple sources can show "syncing" concurrently.
|
||||
@@ -268,7 +284,7 @@ export function MemorySourcesRegistry({
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
try {
|
||||
const [list, stats] = await Promise.all([
|
||||
const [list, stats, health] = await Promise.all([
|
||||
listMemorySources().catch(err => {
|
||||
console.warn('[ui-flow][memory-sources] list failed', err);
|
||||
return [] as MemorySourceEntry[];
|
||||
@@ -277,9 +293,17 @@ export function MemorySourcesRegistry({
|
||||
console.warn('[ui-flow][memory-sources] status_list failed', err);
|
||||
return [] as SourceStatus[];
|
||||
}),
|
||||
// GH-4690: downstream pipeline health. Best-effort — a failure here
|
||||
// must never hide the source list, so we fall back to `null` (rows then
|
||||
// rely on the precise per-source pending-chunk signal alone).
|
||||
memoryTreePipelineStatus().catch(err => {
|
||||
console.warn('[ui-flow][memory-sources] pipeline_status failed', err);
|
||||
return null;
|
||||
}),
|
||||
]);
|
||||
setSources(list);
|
||||
setStatuses(stats);
|
||||
setPipeline(health);
|
||||
// RC#5 (#3295): The 5s poll is the safety net for missed completed/failed events.
|
||||
// If a source is in syncingIds but the poll shows it's no longer active (no
|
||||
// in-progress status indicator from the server), we clear it here. In practice
|
||||
@@ -534,6 +558,8 @@ export function MemorySourcesRegistry({
|
||||
key={source.id}
|
||||
source={source}
|
||||
status={statusById.get(source.id) ?? null}
|
||||
pipeline={pipeline}
|
||||
isAuthenticated={isAuthenticated}
|
||||
isSyncing={syncingIds.has(source.id) || syncProgress.has(source.id)}
|
||||
isBuilding={buildingId === source.id}
|
||||
progress={syncProgress.get(source.id) ?? null}
|
||||
@@ -546,6 +572,16 @@ export function MemorySourcesRegistry({
|
||||
onToggleSettings={handleToggleSettings}
|
||||
onSettingsSaved={handleSettingsSaved}
|
||||
onToast={onToast}
|
||||
onViewHealth={() => {
|
||||
console.debug('[ui-flow][memory-sources] view memory health from source row');
|
||||
navigate('/brain?tab=sync');
|
||||
}}
|
||||
onSignIn={() => {
|
||||
console.debug(
|
||||
'[ui-flow][memory-sources] sign-in prompt from stored-without-vectors'
|
||||
);
|
||||
navigate('/auth');
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
@@ -692,6 +728,10 @@ function MemorySyncSchedule({ lastSyncMs, onToast }: MemorySyncScheduleProps) {
|
||||
interface SourceRowProps {
|
||||
source: MemorySourceEntry;
|
||||
status: SourceStatus | null;
|
||||
/** Global downstream pipeline health (GH-4690); `null` before first poll. */
|
||||
pipeline: MemoryTreePipelineStatus | null;
|
||||
/** Whether a backend session exists — gates the "Sign in to enable" prompt. */
|
||||
isAuthenticated: boolean;
|
||||
isSyncing: boolean;
|
||||
isBuilding: boolean;
|
||||
progress: SyncProgress | null;
|
||||
@@ -704,11 +744,17 @@ interface SourceRowProps {
|
||||
onToggleSettings: (sourceId: string) => void;
|
||||
onSettingsSaved: (updated: MemorySourceEntry) => void;
|
||||
onToast?: (toast: Omit<ToastNotification, 'id'>) => void;
|
||||
/** Navigate to Brain > Memory > Sync (the full memory-health panel). */
|
||||
onViewHealth: () => void;
|
||||
/** Navigate to sign-in (embeddings need a backend session). */
|
||||
onSignIn: () => void;
|
||||
}
|
||||
|
||||
function SourceRow({
|
||||
source,
|
||||
status,
|
||||
pipeline,
|
||||
isAuthenticated,
|
||||
isSyncing,
|
||||
isBuilding,
|
||||
progress,
|
||||
@@ -721,6 +767,8 @@ function SourceRow({
|
||||
onToggleSettings,
|
||||
onSettingsSaved,
|
||||
onToast,
|
||||
onViewHealth,
|
||||
onSignIn,
|
||||
}: SourceRowProps) {
|
||||
const { t } = useT();
|
||||
const icon = SOURCE_KIND_ICONS[source.kind] ?? '📄';
|
||||
@@ -728,6 +776,21 @@ function SourceRow({
|
||||
const detail = sourceDetail(source);
|
||||
const lastSync = status ? relativeTimestamp(status.last_chunk_at_ms, t) : null;
|
||||
|
||||
// GH-4690: layered pipeline verdict. Only meaningful in a settled state —
|
||||
// during an active sync (`progress`) or right after one (`result`) the row
|
||||
// renders live progress / a terminal chip instead, and `chunks_pending` is
|
||||
// legitimately transient, so we suppress the warning until things settle.
|
||||
const settled = !progress && !result;
|
||||
const health: SourcePipelineHealth = settled
|
||||
? deriveSourcePipelineHealth(status, pipeline)
|
||||
: { state: 'none', issues: [], authRelated: false };
|
||||
const ingestedOnly = health.state === 'ingested_only';
|
||||
if (ingestedOnly) {
|
||||
console.debug(
|
||||
`[ui-flow][memory-sources] source=${source.id} ingested-only issues=${health.issues.join(',')}`
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<li className="flex flex-col gap-2 py-3" data-testid={`memory-source-row-${source.kind}`}>
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
|
||||
@@ -743,7 +806,13 @@ function SourceRow({
|
||||
<span className="rounded-md bg-surface-subtle px-1.5 py-0.5 text-[10px] font-medium text-content-muted">
|
||||
{kindLabel}
|
||||
</span>
|
||||
{status && status.chunks_synced > 0 && <FreshnessPill freshness={status.freshness} />}
|
||||
{status &&
|
||||
status.chunks_synced > 0 &&
|
||||
(ingestedOnly ? (
|
||||
<IngestedOnlyPill sourceId={source.id} />
|
||||
) : (
|
||||
<FreshnessPill freshness={status.freshness} />
|
||||
))}
|
||||
</div>
|
||||
{detail && <p className="mt-0.5 truncate pl-7 text-xs text-content-faint">{detail}</p>}
|
||||
{progress && (
|
||||
@@ -811,6 +880,37 @@ function SourceRow({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{ingestedOnly && (
|
||||
<div
|
||||
className="mt-2 flex flex-col gap-1 rounded-md border border-amber-200 bg-amber-50 px-2.5 py-1.5 pl-7 text-xs text-amber-800 dark:border-amber-500/30 dark:bg-amber-500/10 dark:text-amber-200"
|
||||
data-testid={`memory-source-pipeline-warning-${source.id}`}
|
||||
role="status">
|
||||
{health.issues.map(issue => (
|
||||
<div key={issue} className="flex items-start gap-1.5">
|
||||
<WarnIcon />
|
||||
<span className="break-words">{t(pipelineIssueMessageKey(issue))}</span>
|
||||
</div>
|
||||
))}
|
||||
<div className="mt-0.5 flex flex-wrap items-center gap-x-3 gap-y-1 pl-[18px]">
|
||||
{health.authRelated && !isAuthenticated && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onSignIn}
|
||||
data-testid={`memory-source-signin-${source.id}`}
|
||||
className="font-medium text-amber-900 underline underline-offset-2 hover:text-amber-950 dark:text-amber-100 dark:hover:text-white">
|
||||
{t('sync.pipeline.signInToEnable')}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onViewHealth}
|
||||
data-testid={`memory-source-view-health-${source.id}`}
|
||||
className="font-medium text-amber-900 underline underline-offset-2 hover:text-amber-950 dark:text-amber-100 dark:hover:text-white">
|
||||
{t('sync.pipeline.viewHealth')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<button
|
||||
@@ -905,6 +1005,24 @@ function FreshnessPill({ freshness }: { freshness: FreshnessLabel }) {
|
||||
return <span className={`rounded-md px-2 py-0.5 text-[10px] font-medium ${cls}`}>{label}</span>;
|
||||
}
|
||||
|
||||
/**
|
||||
* GH-4690: the "Ingested only" warning pill. Replaces the clean freshness pill
|
||||
* when a source ingested chunks but the downstream retrieval pipeline failed
|
||||
* (no vectors / extraction / degraded tree). Amber, matching the degraded
|
||||
* badges in the Brain > Memory > Sync panel, so raw sync ≠ retrieval-ready is
|
||||
* unmistakable at a glance.
|
||||
*/
|
||||
function IngestedOnlyPill({ sourceId }: { sourceId: string }) {
|
||||
const { t } = useT();
|
||||
return (
|
||||
<span
|
||||
className="rounded-md bg-amber-100 px-2 py-0.5 text-[10px] font-medium text-amber-800 dark:bg-amber-500/20 dark:text-amber-200"
|
||||
data-testid={`memory-source-ingested-only-${sourceId}`}>
|
||||
{t('sync.pipeline.ingestedOnly')}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function relativeTimestamp(epochMs: number | null, t: (k: string) => string): string | null {
|
||||
if (epochMs === null) return null;
|
||||
const delta = Date.now() - epochMs;
|
||||
|
||||
@@ -50,8 +50,22 @@ vi.mock('../../../services/memorySourcesService', () => ({
|
||||
}));
|
||||
|
||||
// ── tauriCommands mock ────────────────────────────────────────────────────────
|
||||
// memoryTreePipelineStatus is polled for downstream pipeline health (GH-4690);
|
||||
// default to a healthy running snapshot so rows keep their clean synced state.
|
||||
vi.mock('../../../utils/tauriCommands/memoryTree', () => ({
|
||||
memoryTreeFlushSource: vi.fn().mockResolvedValue({ seals_fired: 0 }),
|
||||
memoryTreePipelineStatus: vi
|
||||
.fn()
|
||||
.mockResolvedValue({
|
||||
status: 'running',
|
||||
reason: null,
|
||||
last_sync_ms: 0,
|
||||
total_chunks: 0,
|
||||
wiki_size_bytes: 0,
|
||||
pipeline_jobs: { ready: 0, running: 0, failed: 0 },
|
||||
is_syncing: false,
|
||||
is_paused: false,
|
||||
}),
|
||||
}));
|
||||
|
||||
// ── helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -16,6 +16,10 @@ import {
|
||||
openhumanGetMemorySyncSettings,
|
||||
openhumanUpdateMemorySyncSettings,
|
||||
} from '../../../utils/tauriCommands/config';
|
||||
import {
|
||||
memoryTreePipelineStatus,
|
||||
type MemoryTreePipelineStatus,
|
||||
} from '../../../utils/tauriCommands/memoryTree';
|
||||
import { MemorySourcesRegistry } from '../MemorySourcesRegistry';
|
||||
|
||||
// Mock the entire service so we don't hit RPC
|
||||
@@ -34,9 +38,23 @@ vi.mock('../../../services/memorySourcesService', async () => {
|
||||
};
|
||||
});
|
||||
|
||||
// Mock tauriCommands/memoryTree — not needed in these tests
|
||||
// Mock tauriCommands/memoryTree. The registry now also polls
|
||||
// memoryTreePipelineStatus for downstream health (GH-4690); default it to a
|
||||
// healthy running snapshot so no source row shows a spurious warning.
|
||||
vi.mock('../../../utils/tauriCommands/memoryTree', () => ({
|
||||
memoryTreeFlushSource: vi.fn().mockResolvedValue({ seals_fired: 0 }),
|
||||
memoryTreePipelineStatus: vi
|
||||
.fn()
|
||||
.mockResolvedValue({
|
||||
status: 'running',
|
||||
reason: null,
|
||||
last_sync_ms: 0,
|
||||
total_chunks: 0,
|
||||
wiki_size_bytes: 0,
|
||||
pipeline_jobs: { ready: 0, running: 0, failed: 0 },
|
||||
is_syncing: false,
|
||||
is_paused: false,
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock the memory-sync schedule config RPCs (#3302).
|
||||
@@ -51,6 +69,24 @@ const mockedUpdate = vi.mocked(service.updateMemorySource);
|
||||
const mockedApplyAllIn = vi.mocked(service.applyAllIn);
|
||||
const mockedGetSync = vi.mocked(openhumanGetMemorySyncSettings);
|
||||
const mockedUpdateSync = vi.mocked(openhumanUpdateMemorySyncSettings);
|
||||
const mockedPipeline = vi.mocked(memoryTreePipelineStatus);
|
||||
|
||||
/** A healthy `memory_tree_pipeline_status` snapshot (no degradation). */
|
||||
function healthyPipeline(
|
||||
overrides: Partial<MemoryTreePipelineStatus> = {}
|
||||
): MemoryTreePipelineStatus {
|
||||
return {
|
||||
status: 'running',
|
||||
reason: null,
|
||||
last_sync_ms: 0,
|
||||
total_chunks: 0,
|
||||
wiki_size_bytes: 0,
|
||||
pipeline_jobs: { ready: 0, running: 0, failed: 0 },
|
||||
is_syncing: false,
|
||||
is_paused: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function syncSettings(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
@@ -493,4 +529,121 @@ describe('MemorySourcesRegistry', () => {
|
||||
await waitFor(() => expect(mockedList).toHaveBeenCalledTimes(2));
|
||||
expect(await screen.findByText('Reloaded Repo')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Layered pipeline status — Data Sync must not show a clean "synced" badge
|
||||
// when the downstream retrieval pipeline failed (GH-4690).
|
||||
// -------------------------------------------------------------------------
|
||||
describe('layered pipeline warnings (GH-4690)', () => {
|
||||
it('no regression: a fully healthy sync keeps the clean freshness badge', async () => {
|
||||
mockedList.mockResolvedValue([makeSource({ id: 'src_1', label: 'Healthy Repo' })]);
|
||||
mockedStatus.mockResolvedValue([
|
||||
{
|
||||
source_id: 'src_1',
|
||||
chunks_synced: 5,
|
||||
chunks_pending: 0,
|
||||
last_chunk_at_ms: Date.now(),
|
||||
freshness: 'recent',
|
||||
},
|
||||
]);
|
||||
mockedPipeline.mockResolvedValue(healthyPipeline());
|
||||
|
||||
renderWithProviders(<MemorySourcesRegistry pollIntervalMs={0} />);
|
||||
await screen.findByText('Healthy Repo');
|
||||
// Clean state: freshness pill shows, no "Ingested only" warning surfaces.
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Recent')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.queryByTestId('memory-source-ingested-only-src_1')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('memory-source-pipeline-warning-src_1')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('flags "Stored without vectors" from per-source pending chunks', async () => {
|
||||
mockedList.mockResolvedValue([makeSource({ id: 'src_1', label: 'Notes' })]);
|
||||
mockedStatus.mockResolvedValue([
|
||||
{
|
||||
source_id: 'src_1',
|
||||
chunks_synced: 1,
|
||||
chunks_pending: 1,
|
||||
last_chunk_at_ms: Date.now(),
|
||||
freshness: 'recent',
|
||||
},
|
||||
]);
|
||||
// Even with a "healthy" global snapshot, the per-source pending chunks
|
||||
// are the precise signal that this source has no vectors.
|
||||
mockedPipeline.mockResolvedValue(healthyPipeline());
|
||||
|
||||
renderWithProviders(<MemorySourcesRegistry pollIntervalMs={0} />);
|
||||
await screen.findByText('Notes');
|
||||
expect(await screen.findByTestId('memory-source-ingested-only-src_1')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('memory-source-pipeline-warning-src_1')).toHaveTextContent(
|
||||
/Stored without vectors/i
|
||||
);
|
||||
// Clean freshness badge must be gone.
|
||||
expect(screen.queryByText('Recent')).not.toBeInTheDocument();
|
||||
// A link to the full memory-health panel is offered.
|
||||
expect(screen.getByTestId('memory-source-view-health-src_1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('offers "Sign in to enable" when embeddings fail for lack of a backend session', async () => {
|
||||
mockedList.mockResolvedValue([makeSource({ id: 'src_1', label: 'Notes' })]);
|
||||
mockedStatus.mockResolvedValue([
|
||||
{
|
||||
source_id: 'src_1',
|
||||
chunks_synced: 2,
|
||||
chunks_pending: 2,
|
||||
last_chunk_at_ms: Date.now(),
|
||||
freshness: 'idle',
|
||||
},
|
||||
]);
|
||||
mockedPipeline.mockResolvedValue(
|
||||
healthyPipeline({
|
||||
status: 'error',
|
||||
first_blocking_cause: {
|
||||
code: 'auth_missing',
|
||||
class: 'unrecoverable',
|
||||
remediation_key: 'memory.health.remediation.auth_missing',
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
renderWithProviders(<MemorySourcesRegistry pollIntervalMs={0} />);
|
||||
await screen.findByText('Notes');
|
||||
// Unauthenticated (no CoreStateProvider ⇒ isAuthenticated false), auth-related.
|
||||
const signIn = await screen.findByTestId('memory-source-signin-src_1');
|
||||
expect(signIn).toBeInTheDocument();
|
||||
// Clicking must not throw (navigates within the MemoryRouter).
|
||||
fireEvent.click(signIn);
|
||||
});
|
||||
|
||||
it('surfaces extraction failure from the global pipeline health', async () => {
|
||||
mockedList.mockResolvedValue([makeSource({ id: 'src_1', label: 'Notes' })]);
|
||||
mockedStatus.mockResolvedValue([
|
||||
{
|
||||
source_id: 'src_1',
|
||||
chunks_synced: 5,
|
||||
chunks_pending: 0,
|
||||
last_chunk_at_ms: Date.now(),
|
||||
freshness: 'recent',
|
||||
},
|
||||
]);
|
||||
mockedPipeline.mockResolvedValue(
|
||||
healthyPipeline({
|
||||
status: 'degraded',
|
||||
first_blocking_cause: {
|
||||
code: 'extraction_timeout',
|
||||
class: 'transient',
|
||||
remediation_key: 'memory.health.remediation.extraction_timeout',
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
renderWithProviders(<MemorySourcesRegistry pollIntervalMs={0} />);
|
||||
await screen.findByText('Notes');
|
||||
expect(await screen.findByTestId('memory-source-ingested-only-src_1')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('memory-source-pipeline-warning-src_1')).toHaveTextContent(
|
||||
/extraction failed/i
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* Unit tests for the layered Data Sync pipeline-health derivation (GH-4690).
|
||||
* Covers each warning layer plus the healthy no-regression case.
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import type { SourceStatus } from '../../services/memorySourcesService';
|
||||
import type { MemoryTreePipelineStatus } from '../../utils/tauriCommands/memoryTree';
|
||||
import {
|
||||
deriveSourcePipelineHealth,
|
||||
pipelineIssueMessageKey,
|
||||
type SourcePipelineIssueKind,
|
||||
} from './sourcePipelineStatus';
|
||||
|
||||
function makeStatus(overrides: Partial<SourceStatus> = {}): SourceStatus {
|
||||
return {
|
||||
source_id: 'src_1',
|
||||
chunks_synced: 5,
|
||||
chunks_pending: 0,
|
||||
last_chunk_at_ms: 1_000,
|
||||
freshness: 'recent',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function makePipeline(overrides: Partial<MemoryTreePipelineStatus> = {}): MemoryTreePipelineStatus {
|
||||
return {
|
||||
status: 'running',
|
||||
reason: null,
|
||||
last_sync_ms: 1_000,
|
||||
total_chunks: 5,
|
||||
wiki_size_bytes: 0,
|
||||
pipeline_jobs: { ready: 0, running: 0, failed: 0 },
|
||||
is_syncing: false,
|
||||
is_paused: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('deriveSourcePipelineHealth', () => {
|
||||
it('returns none when nothing has been ingested yet', () => {
|
||||
const h = deriveSourcePipelineHealth(makeStatus({ chunks_synced: 0 }), makePipeline());
|
||||
expect(h.state).toBe('none');
|
||||
expect(h.issues).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns none when status is null (pre-load)', () => {
|
||||
const h = deriveSourcePipelineHealth(null, makePipeline());
|
||||
expect(h.state).toBe('none');
|
||||
});
|
||||
|
||||
// -- No regression: fully healthy sync stays clean -------------------------
|
||||
it('reports retrieval_ready when everything is healthy', () => {
|
||||
const h = deriveSourcePipelineHealth(
|
||||
makeStatus({ chunks_synced: 5, chunks_pending: 0 }),
|
||||
makePipeline({ status: 'running' })
|
||||
);
|
||||
expect(h.state).toBe('retrieval_ready');
|
||||
expect(h.issues).toEqual([]);
|
||||
expect(h.authRelated).toBe(false);
|
||||
});
|
||||
|
||||
it('stays retrieval_ready when the pipeline snapshot is missing but chunks are embedded', () => {
|
||||
const h = deriveSourcePipelineHealth(makeStatus({ chunks_pending: 0 }), null);
|
||||
expect(h.state).toBe('retrieval_ready');
|
||||
expect(h.issues).toEqual([]);
|
||||
});
|
||||
|
||||
// -- Layer 1: embeddings ---------------------------------------------------
|
||||
it('flags stored_without_vectors from per-source pending chunks alone', () => {
|
||||
// The exact issue repro: "1 chunk / 1 pending" with no pipeline snapshot.
|
||||
const h = deriveSourcePipelineHealth(makeStatus({ chunks_synced: 1, chunks_pending: 1 }), null);
|
||||
expect(h.state).toBe('ingested_only');
|
||||
expect(h.issues).toContain('stored_without_vectors');
|
||||
});
|
||||
|
||||
it('flags stored_without_vectors from the global semantic_recall latch', () => {
|
||||
const h = deriveSourcePipelineHealth(
|
||||
makeStatus({ chunks_pending: 0 }),
|
||||
makePipeline({ status: 'degraded', degraded: { semantic_recall: true, structure: false } })
|
||||
);
|
||||
expect(h.issues).toContain('stored_without_vectors');
|
||||
// A recall-degraded state must NOT also add the generic tree_degraded noise.
|
||||
expect(h.issues).not.toContain('tree_degraded');
|
||||
});
|
||||
|
||||
it('marks authRelated when the blocking cause is a missing backend session', () => {
|
||||
const h = deriveSourcePipelineHealth(
|
||||
makeStatus({ chunks_pending: 3 }),
|
||||
makePipeline({
|
||||
status: 'error',
|
||||
first_blocking_cause: {
|
||||
code: 'auth_missing',
|
||||
class: 'unrecoverable',
|
||||
remediation_key: 'memory.health.remediation.auth_missing',
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(h.issues).toContain('stored_without_vectors');
|
||||
expect(h.authRelated).toBe(true);
|
||||
});
|
||||
|
||||
it('does not mark authRelated for a non-auth embeddings cause', () => {
|
||||
const h = deriveSourcePipelineHealth(
|
||||
makeStatus({ chunks_pending: 3 }),
|
||||
makePipeline({
|
||||
status: 'error',
|
||||
first_blocking_cause: {
|
||||
code: 'embeddings_unconfigured',
|
||||
class: 'unrecoverable',
|
||||
remediation_key: 'memory.health.remediation.embeddings_unconfigured',
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(h.authRelated).toBe(false);
|
||||
});
|
||||
|
||||
// -- Layer 2: extraction ---------------------------------------------------
|
||||
it('flags extraction_failed from an extraction_timeout blocking cause', () => {
|
||||
const h = deriveSourcePipelineHealth(
|
||||
makeStatus({ chunks_pending: 0 }),
|
||||
makePipeline({
|
||||
status: 'degraded',
|
||||
first_blocking_cause: {
|
||||
code: 'extraction_timeout',
|
||||
class: 'transient',
|
||||
remediation_key: 'memory.health.remediation.extraction_timeout',
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(h.state).toBe('ingested_only');
|
||||
expect(h.issues).toContain('extraction_failed');
|
||||
});
|
||||
|
||||
it('flags extraction_failed from the structure degraded flag', () => {
|
||||
const h = deriveSourcePipelineHealth(
|
||||
makeStatus({ chunks_pending: 0 }),
|
||||
makePipeline({ status: 'degraded', degraded: { semantic_recall: false, structure: true } })
|
||||
);
|
||||
expect(h.issues).toContain('extraction_failed');
|
||||
});
|
||||
|
||||
// -- Layer 3: memory tree --------------------------------------------------
|
||||
it('flags tree_degraded for a generic degraded status with no specific layer', () => {
|
||||
const h = deriveSourcePipelineHealth(
|
||||
makeStatus({ chunks_pending: 0 }),
|
||||
makePipeline({ status: 'degraded' })
|
||||
);
|
||||
expect(h.state).toBe('ingested_only');
|
||||
expect(h.issues).toEqual(['tree_degraded']);
|
||||
});
|
||||
|
||||
it('flags tree_degraded for an error status too', () => {
|
||||
const h = deriveSourcePipelineHealth(
|
||||
makeStatus({ chunks_pending: 0 }),
|
||||
makePipeline({ status: 'error' })
|
||||
);
|
||||
expect(h.issues).toContain('tree_degraded');
|
||||
});
|
||||
|
||||
// -- Multiple layers at once (the full-failure scenario) -------------------
|
||||
it('surfaces every failed layer together, embeddings first', () => {
|
||||
const h = deriveSourcePipelineHealth(
|
||||
makeStatus({ chunks_synced: 1, chunks_pending: 1 }),
|
||||
makePipeline({ status: 'degraded', degraded: { semantic_recall: true, structure: true } })
|
||||
);
|
||||
expect(h.state).toBe('ingested_only');
|
||||
// Embeddings + extraction; the generic tree layer is suppressed because
|
||||
// more specific layers already explain the degradation.
|
||||
expect(h.issues).toEqual(['stored_without_vectors', 'extraction_failed']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pipelineIssueMessageKey', () => {
|
||||
it('maps every issue kind to a stable i18n key', () => {
|
||||
const kinds: SourcePipelineIssueKind[] = [
|
||||
'stored_without_vectors',
|
||||
'extraction_failed',
|
||||
'tree_degraded',
|
||||
];
|
||||
for (const k of kinds) {
|
||||
expect(pipelineIssueMessageKey(k)).toMatch(/^sync\.pipeline\./);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* Layered pipeline-health derivation for Data Sync source rows (GH-4690).
|
||||
*
|
||||
* Raw sync ≠ retrieval-ready. A source can be "synced" (its documents were
|
||||
* ingested into `mem_tree_chunks`) while the downstream retrieval pipeline
|
||||
* silently failed underneath it: embeddings were never created, spaCy/LLM
|
||||
* extraction failed, or the memory tree is degraded. Before this, Data Sync
|
||||
* showed a clean freshness badge in all those cases and the user only learned
|
||||
* the truth in Brain > Memory > Sync or the raw logs.
|
||||
*
|
||||
* This module folds two signals the core already exposes into one per-row
|
||||
* verdict so the row can honestly say "Ingested only" instead of "synced":
|
||||
*
|
||||
* 1. **Per-source, precise** — `SourceStatus.chunks_pending` is the SQL count
|
||||
* of this source's chunks whose `embedding IS NULL` (see
|
||||
* `memory_sources/status.rs`). `> 0` in a settled state means those chunks
|
||||
* were stored WITHOUT vectors → semantic search can't reach them.
|
||||
* 2. **Global pipeline health** — `memory_tree_pipeline_status` (the same RPC
|
||||
* that drives the Brain > Memory > Sync "Degraded" panel) carries the
|
||||
* process-wide `degraded` snapshot + `first_blocking_cause`. Embedding /
|
||||
* extraction / tree-degraded causes there are attributed to the whole
|
||||
* pipeline, so we surface them on rows that actually contributed chunks.
|
||||
*
|
||||
* The function is pure so it can be unit-tested exhaustively without a DOM.
|
||||
*/
|
||||
import type { SourceStatus } from '../../services/memorySourcesService';
|
||||
import type { MemoryTreePipelineStatus } from '../../utils/tauriCommands/memoryTree';
|
||||
|
||||
/** One failed layer of the post-ingest pipeline, in severity-display order. */
|
||||
export type SourcePipelineIssueKind =
|
||||
| 'stored_without_vectors'
|
||||
| 'extraction_failed'
|
||||
| 'tree_degraded';
|
||||
|
||||
/** Coarse retrieval-readiness state for a single source row. */
|
||||
export type SourcePipelineState = 'none' | 'retrieval_ready' | 'ingested_only';
|
||||
|
||||
export interface SourcePipelineHealth {
|
||||
/**
|
||||
* `none` — nothing ingested yet (no badge changes; row renders as before).
|
||||
* `retrieval_ready` — chunks synced AND no downstream failure (clean state).
|
||||
* `ingested_only` — chunks synced but ≥1 downstream layer failed (warn).
|
||||
*/
|
||||
state: SourcePipelineState;
|
||||
/** The failed layers, deduped, in display order. Empty unless `ingested_only`. */
|
||||
issues: SourcePipelineIssueKind[];
|
||||
/**
|
||||
* True when the embeddings failure is attributable to a missing backend
|
||||
* session / auth (the "No backend session for cloud embeddings" case). Drives
|
||||
* the "Sign in to enable" affordance — only meaningful with
|
||||
* `stored_without_vectors`.
|
||||
*/
|
||||
authRelated: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the layered pipeline verdict for one source row.
|
||||
*
|
||||
* `status` is this source's `memory_sources_status_list` entry; `pipeline` is
|
||||
* the global `memory_tree_pipeline_status` snapshot (may be `null` when that
|
||||
* RPC hasn't resolved / failed — the per-source signal still stands on its own).
|
||||
*/
|
||||
export function deriveSourcePipelineHealth(
|
||||
status: SourceStatus | null,
|
||||
pipeline: MemoryTreePipelineStatus | null
|
||||
): SourcePipelineHealth {
|
||||
const synced = status?.chunks_synced ?? 0;
|
||||
|
||||
// Nothing ingested yet → don't invent a warning. The row keeps its existing
|
||||
// (empty / mid-sync) rendering.
|
||||
if (synced <= 0) {
|
||||
return { state: 'none', issues: [], authRelated: false };
|
||||
}
|
||||
|
||||
const degraded = pipeline?.degraded;
|
||||
const causeCode = pipeline?.first_blocking_cause?.code ?? degraded?.cause?.code ?? null;
|
||||
|
||||
const issues: SourcePipelineIssueKind[] = [];
|
||||
|
||||
// Layer 1 — embeddings. Precise per-source signal (chunks with NULL
|
||||
// embedding) OR the global "semantic recall degraded" latch (no usable
|
||||
// embeddings provider). Either means this source's chunks aren't vector-searchable.
|
||||
const storedWithoutVectors =
|
||||
(status?.chunks_pending ?? 0) > 0 || degraded?.semantic_recall === true;
|
||||
if (storedWithoutVectors) {
|
||||
issues.push('stored_without_vectors');
|
||||
}
|
||||
|
||||
// Layer 2 — extraction. `degraded.structure` exists but has no production
|
||||
// producer today (test-only), so the live signal is the typed
|
||||
// `extraction_timeout` blocking cause ("the memory extraction model is
|
||||
// timing out"). Honour both.
|
||||
const extractionFailed = degraded?.structure === true || causeCode === 'extraction_timeout';
|
||||
if (extractionFailed) {
|
||||
issues.push('extraction_failed');
|
||||
}
|
||||
|
||||
// Layer 3 — memory tree. A global degraded/error status that isn't already
|
||||
// explained by a more specific layer above (e.g. storage-degraded, a failed
|
||||
// job). Retrieval for every synced source may return stale results.
|
||||
const treeDegraded =
|
||||
(pipeline?.status === 'degraded' || pipeline?.status === 'error') &&
|
||||
!storedWithoutVectors &&
|
||||
!extractionFailed;
|
||||
if (treeDegraded) {
|
||||
issues.push('tree_degraded');
|
||||
}
|
||||
|
||||
const authRelated = storedWithoutVectors && causeCode === 'auth_missing';
|
||||
|
||||
return { state: issues.length > 0 ? 'ingested_only' : 'retrieval_ready', issues, authRelated };
|
||||
}
|
||||
|
||||
/** i18n key for a given issue's row warning message. */
|
||||
export function pipelineIssueMessageKey(kind: SourcePipelineIssueKind): string {
|
||||
switch (kind) {
|
||||
case 'stored_without_vectors':
|
||||
return 'sync.pipeline.storedWithoutVectors';
|
||||
case 'extraction_failed':
|
||||
return 'sync.pipeline.extractionFailed';
|
||||
case 'tree_degraded':
|
||||
return 'sync.pipeline.treeDegraded';
|
||||
}
|
||||
}
|
||||
@@ -7246,6 +7246,13 @@ const messages: TranslationMap = {
|
||||
'privacy.status.ariaLabel': 'حالة الخصوصية',
|
||||
'privacy.status.external': 'خارج الجهاز',
|
||||
'privacy.status.local': 'على الجهاز',
|
||||
// Data Sync layered pipeline status (GH-4690)
|
||||
'sync.pipeline.ingestedOnly': 'تم الاستيعاب فقط',
|
||||
'sync.pipeline.storedWithoutVectors': 'مخزَّن بدون متجهات. البحث الدلالي غير متاح.',
|
||||
'sync.pipeline.signInToEnable': 'سجّل الدخول للتفعيل',
|
||||
'sync.pipeline.extractionFailed': 'فشل استخراج بنية الذاكرة. قد يكون الويكي غير مكتمل.',
|
||||
'sync.pipeline.treeDegraded': 'شجرة الذاكرة متدهورة. قد يُرجع الاسترجاع نتائج قديمة.',
|
||||
'sync.pipeline.viewHealth': 'عرض حالة الذاكرة',
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -7415,6 +7415,13 @@ const messages: TranslationMap = {
|
||||
'privacy.status.ariaLabel': 'গোপনীয়তা স্থিতি',
|
||||
'privacy.status.external': 'ডিভাইসের বাইরে',
|
||||
'privacy.status.local': 'ডিভাইসে',
|
||||
// Data Sync layered pipeline status (GH-4690)
|
||||
'sync.pipeline.ingestedOnly': 'শুধু গৃহীত',
|
||||
'sync.pipeline.storedWithoutVectors': 'ভেক্টর ছাড়াই সংরক্ষিত। শব্দার্থিক অনুসন্ধান অনুপলব্ধ।',
|
||||
'sync.pipeline.signInToEnable': 'সক্রিয় করতে সাইন ইন করুন',
|
||||
'sync.pipeline.extractionFailed': 'মেমরি কাঠামো নিষ্কাশন ব্যর্থ হয়েছে। উইকি অসম্পূর্ণ হতে পারে।',
|
||||
'sync.pipeline.treeDegraded': 'মেমরি ট্রি অবনমিত। পুনরুদ্ধার পুরনো ফলাফল দিতে পারে।',
|
||||
'sync.pipeline.viewHealth': 'মেমরির স্বাস্থ্য দেখুন',
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -7635,6 +7635,16 @@ const messages: TranslationMap = {
|
||||
'privacy.status.ariaLabel': 'Datenschutzstatus',
|
||||
'privacy.status.external': 'Außerhalb des Geräts',
|
||||
'privacy.status.local': 'Auf dem Gerät',
|
||||
// Data Sync layered pipeline status (GH-4690)
|
||||
'sync.pipeline.ingestedOnly': 'Nur aufgenommen',
|
||||
'sync.pipeline.storedWithoutVectors':
|
||||
'Ohne Vektoren gespeichert. Semantische Suche nicht verfügbar.',
|
||||
'sync.pipeline.signInToEnable': 'Zum Aktivieren anmelden',
|
||||
'sync.pipeline.extractionFailed':
|
||||
'Extraktion der Speicherstruktur fehlgeschlagen. Das Wiki ist möglicherweise unvollständig.',
|
||||
'sync.pipeline.treeDegraded':
|
||||
'Speicherbaum beeinträchtigt. Die Abfrage liefert möglicherweise veraltete Ergebnisse.',
|
||||
'sync.pipeline.viewHealth': 'Speicherzustand anzeigen',
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -3349,6 +3349,14 @@ const en: TranslationMap = {
|
||||
'sync.failedToLoad': 'Failed to load sync status',
|
||||
'sync.noContent': 'No content has been synced into memory yet. Connect an integration to start.',
|
||||
|
||||
// Data Sync layered pipeline status (GH-4690) — raw sync ≠ retrieval-ready
|
||||
'sync.pipeline.ingestedOnly': 'Ingested only',
|
||||
'sync.pipeline.storedWithoutVectors': 'Stored without vectors. Semantic search unavailable.',
|
||||
'sync.pipeline.signInToEnable': 'Sign in to enable',
|
||||
'sync.pipeline.extractionFailed': 'Memory structure extraction failed. Wiki may be incomplete.',
|
||||
'sync.pipeline.treeDegraded': 'Memory tree degraded. Retrieval may return stale results.',
|
||||
'sync.pipeline.viewHealth': 'View memory health',
|
||||
|
||||
// Memory Sync Schedule (global cadence)
|
||||
'memorySyncInterval.title': 'Sync schedule',
|
||||
'memorySyncInterval.lastSynced': 'Last synced',
|
||||
|
||||
@@ -7570,6 +7570,16 @@ const messages: TranslationMap = {
|
||||
'privacy.status.ariaLabel': 'Estado de privacidad',
|
||||
'privacy.status.external': 'Fuera del dispositivo',
|
||||
'privacy.status.local': 'En el dispositivo',
|
||||
// Data Sync layered pipeline status (GH-4690)
|
||||
'sync.pipeline.ingestedOnly': 'Solo ingerido',
|
||||
'sync.pipeline.storedWithoutVectors':
|
||||
'Almacenado sin vectores. La búsqueda semántica no está disponible.',
|
||||
'sync.pipeline.signInToEnable': 'Inicia sesión para activar',
|
||||
'sync.pipeline.extractionFailed':
|
||||
'Error al extraer la estructura de memoria. Es posible que el wiki esté incompleto.',
|
||||
'sync.pipeline.treeDegraded':
|
||||
'Árbol de memoria degradado. La recuperación puede devolver resultados obsoletos.',
|
||||
'sync.pipeline.viewHealth': 'Ver estado de la memoria',
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -7604,6 +7604,16 @@ const messages: TranslationMap = {
|
||||
'privacy.status.ariaLabel': 'État de confidentialité',
|
||||
'privacy.status.external': 'Hors de l’appareil',
|
||||
'privacy.status.local': 'Sur l’appareil',
|
||||
// Data Sync layered pipeline status (GH-4690)
|
||||
'sync.pipeline.ingestedOnly': 'Ingéré uniquement',
|
||||
'sync.pipeline.storedWithoutVectors':
|
||||
'Enregistré sans vecteurs. Recherche sémantique indisponible.',
|
||||
'sync.pipeline.signInToEnable': 'Connectez-vous pour activer',
|
||||
'sync.pipeline.extractionFailed':
|
||||
"Échec de l'extraction de la structure mémoire. Le wiki peut être incomplet.",
|
||||
'sync.pipeline.treeDegraded':
|
||||
'Arbre mémoire dégradé. La récupération peut renvoyer des résultats obsolètes.',
|
||||
'sync.pipeline.viewHealth': "Voir l'état de la mémoire",
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -7412,6 +7412,13 @@ const messages: TranslationMap = {
|
||||
'privacy.status.ariaLabel': 'गोपनीयता स्थिति',
|
||||
'privacy.status.external': 'डिवाइस के बाहर',
|
||||
'privacy.status.local': 'डिवाइस पर',
|
||||
// Data Sync layered pipeline status (GH-4690)
|
||||
'sync.pipeline.ingestedOnly': 'केवल अंतर्ग्रहीत',
|
||||
'sync.pipeline.storedWithoutVectors': 'वेक्टर के बिना संग्रहीत। सिमेंटिक खोज अनुपलब्ध।',
|
||||
'sync.pipeline.signInToEnable': 'सक्षम करने के लिए साइन इन करें',
|
||||
'sync.pipeline.extractionFailed': 'मेमोरी संरचना निष्कर्षण विफल रहा। विकी अपूर्ण हो सकता है।',
|
||||
'sync.pipeline.treeDegraded': 'मेमोरी ट्री अवक्रमित। पुनर्प्राप्ति पुराने परिणाम दे सकती है।',
|
||||
'sync.pipeline.viewHealth': 'मेमोरी स्वास्थ्य देखें',
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -7450,6 +7450,14 @@ const messages: TranslationMap = {
|
||||
'privacy.status.ariaLabel': 'Status privasi',
|
||||
'privacy.status.external': 'Di luar perangkat',
|
||||
'privacy.status.local': 'Di perangkat',
|
||||
// Data Sync layered pipeline status (GH-4690)
|
||||
'sync.pipeline.ingestedOnly': 'Hanya diserap',
|
||||
'sync.pipeline.storedWithoutVectors': 'Disimpan tanpa vektor. Pencarian semantik tidak tersedia.',
|
||||
'sync.pipeline.signInToEnable': 'Masuk untuk mengaktifkan',
|
||||
'sync.pipeline.extractionFailed': 'Ekstraksi struktur memori gagal. Wiki mungkin tidak lengkap.',
|
||||
'sync.pipeline.treeDegraded':
|
||||
'Pohon memori menurun. Pengambilan mungkin mengembalikan hasil usang.',
|
||||
'sync.pipeline.viewHealth': 'Lihat kesehatan memori',
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -7558,6 +7558,16 @@ const messages: TranslationMap = {
|
||||
'privacy.status.ariaLabel': 'Stato privacy',
|
||||
'privacy.status.external': 'Fuori dal dispositivo',
|
||||
'privacy.status.local': 'Sul dispositivo',
|
||||
// Data Sync layered pipeline status (GH-4690)
|
||||
'sync.pipeline.ingestedOnly': 'Solo acquisito',
|
||||
'sync.pipeline.storedWithoutVectors':
|
||||
'Memorizzato senza vettori. Ricerca semantica non disponibile.',
|
||||
'sync.pipeline.signInToEnable': 'Accedi per attivare',
|
||||
'sync.pipeline.extractionFailed':
|
||||
'Estrazione della struttura della memoria non riuscita. Il wiki potrebbe essere incompleto.',
|
||||
'sync.pipeline.treeDegraded':
|
||||
'Albero di memoria degradato. Il recupero potrebbe restituire risultati obsoleti.',
|
||||
'sync.pipeline.viewHealth': 'Visualizza lo stato della memoria',
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -7327,6 +7327,14 @@ const messages: TranslationMap = {
|
||||
'privacy.status.ariaLabel': '개인정보 상태',
|
||||
'privacy.status.external': '기기 외',
|
||||
'privacy.status.local': '기기 내',
|
||||
// Data Sync layered pipeline status (GH-4690)
|
||||
'sync.pipeline.ingestedOnly': '수집만 완료',
|
||||
'sync.pipeline.storedWithoutVectors': '벡터 없이 저장됨. 의미 검색을 사용할 수 없습니다.',
|
||||
'sync.pipeline.signInToEnable': '사용하려면 로그인하세요',
|
||||
'sync.pipeline.extractionFailed': '메모리 구조 추출에 실패했습니다. 위키가 불완전할 수 있습니다.',
|
||||
'sync.pipeline.treeDegraded':
|
||||
'메모리 트리가 저하되었습니다. 검색이 오래된 결과를 반환할 수 있습니다.',
|
||||
'sync.pipeline.viewHealth': '메모리 상태 보기',
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -7528,6 +7528,16 @@ const messages: TranslationMap = {
|
||||
'privacy.status.ariaLabel': 'Stan prywatności',
|
||||
'privacy.status.external': 'Poza urządzeniem',
|
||||
'privacy.status.local': 'Na urządzeniu',
|
||||
// Data Sync layered pipeline status (GH-4690)
|
||||
'sync.pipeline.ingestedOnly': 'Tylko pobrano',
|
||||
'sync.pipeline.storedWithoutVectors':
|
||||
'Zapisano bez wektorów. Wyszukiwanie semantyczne niedostępne.',
|
||||
'sync.pipeline.signInToEnable': 'Zaloguj się, aby włączyć',
|
||||
'sync.pipeline.extractionFailed':
|
||||
'Ekstrakcja struktury pamięci nie powiodła się. Wiki może być niekompletne.',
|
||||
'sync.pipeline.treeDegraded':
|
||||
'Drzewo pamięci osłabione. Wyszukiwanie może zwracać nieaktualne wyniki.',
|
||||
'sync.pipeline.viewHealth': 'Zobacz kondycję pamięci',
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -7538,6 +7538,15 @@ const messages: TranslationMap = {
|
||||
'privacy.status.ariaLabel': 'Estado de privacidade',
|
||||
'privacy.status.external': 'Fora do dispositivo',
|
||||
'privacy.status.local': 'No dispositivo',
|
||||
// Data Sync layered pipeline status (GH-4690)
|
||||
'sync.pipeline.ingestedOnly': 'Apenas ingerido',
|
||||
'sync.pipeline.storedWithoutVectors': 'Armazenado sem vetores. Pesquisa semântica indisponível.',
|
||||
'sync.pipeline.signInToEnable': 'Inicie sessão para ativar',
|
||||
'sync.pipeline.extractionFailed':
|
||||
'Falha na extração da estrutura de memória. O wiki pode estar incompleto.',
|
||||
'sync.pipeline.treeDegraded':
|
||||
'Árvore de memória degradada. A recuperação pode retornar resultados desatualizados.',
|
||||
'sync.pipeline.viewHealth': 'Ver a saúde da memória',
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -7498,6 +7498,15 @@ const messages: TranslationMap = {
|
||||
'privacy.status.ariaLabel': 'Состояние конфиденциальности',
|
||||
'privacy.status.external': 'Вне устройства',
|
||||
'privacy.status.local': 'На устройстве',
|
||||
// Data Sync layered pipeline status (GH-4690)
|
||||
'sync.pipeline.ingestedOnly': 'Только загружено',
|
||||
'sync.pipeline.storedWithoutVectors': 'Сохранено без векторов. Семантический поиск недоступен.',
|
||||
'sync.pipeline.signInToEnable': 'Войдите, чтобы включить',
|
||||
'sync.pipeline.extractionFailed':
|
||||
'Не удалось извлечь структуру памяти. Вики может быть неполной.',
|
||||
'sync.pipeline.treeDegraded':
|
||||
'Дерево памяти деградировало. Поиск может возвращать устаревшие результаты.',
|
||||
'sync.pipeline.viewHealth': 'Показать состояние памяти',
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -7010,6 +7010,13 @@ const messages: TranslationMap = {
|
||||
'privacy.status.ariaLabel': '隐私状态',
|
||||
'privacy.status.external': '设备外',
|
||||
'privacy.status.local': '本地设备',
|
||||
// Data Sync layered pipeline status (GH-4690)
|
||||
'sync.pipeline.ingestedOnly': '仅已导入',
|
||||
'sync.pipeline.storedWithoutVectors': '已存储但无向量。语义搜索不可用。',
|
||||
'sync.pipeline.signInToEnable': '登录以启用',
|
||||
'sync.pipeline.extractionFailed': '记忆结构提取失败。维基可能不完整。',
|
||||
'sync.pipeline.treeDegraded': '记忆树已降级。检索可能返回过时的结果。',
|
||||
'sync.pipeline.viewHealth': '查看记忆健康状况',
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
||||
Reference in New Issue
Block a user