diff --git a/src/renderer/settings/components/MemoryDiagnosticsPanel.vue b/src/renderer/settings/components/MemoryDiagnosticsPanel.vue index a2bb2a962..2cb69d47d 100644 --- a/src/renderer/settings/components/MemoryDiagnosticsPanel.vue +++ b/src/renderer/settings/components/MemoryDiagnosticsPanel.vue @@ -164,6 +164,7 @@ @@ -195,12 +196,12 @@
{{ t('settings.deepchatAgents.memoryManager.health.archivePrediction.ageDays') }}: - {{ formatNumber(lifecycle.forget.ageDays) }} + {{ formatDays(lifecycle.forget.ageDays) }} {{ t('settings.deepchatAgents.memoryManager.health.archivePrediction.decayScore') - }}: {{ formatNumber(lifecycle.forget.decayScore) }} + }}: {{ formatDecimal(lifecycle.forget.decayScore) }}
@@ -332,6 +333,7 @@ import type { import { auditSentenceKey, formatRelativeTime } from './memoryRedesignUtils' import MemoryInlineFeedback from './MemoryInlineFeedback.vue' import { useMemoryInlineFeedback } from '../lib/useMemoryInlineFeedback' +import { useMemoryNumberFormatters } from '../lib/useMemoryNumberFormatters' const props = defineProps<{ agentId: string @@ -340,6 +342,8 @@ const props = defineProps<{ }>() const { t, te, locale } = useI18n() +const { formatDecimal, formatDays } = useMemoryNumberFormatters() + const memoryClient = createMemoryClient() const panelFeedback = useMemoryInlineFeedback('MemoryDiagnosticsPanel') const feedback = panelFeedback.feedback @@ -384,8 +388,12 @@ const reindexFailureReason = computed(() => { return t('settings.memory.redesign.reindexInternalReason') }) const recallDiagnostics = computed(() => health.value?.runtime.agent.retrieval.recall) -const recallLatencyP50 = computed(() => recallDiagnostics.value?.latencyMs.total.p50 ?? '—') -const recallLatencyP95 = computed(() => recallDiagnostics.value?.latencyMs.total.p95 ?? '—') +const recallLatencyP50 = computed(() => + formatOptionalDecimal(recallDiagnostics.value?.latencyMs.total.p50) +) +const recallLatencyP95 = computed(() => + formatOptionalDecimal(recallDiagnostics.value?.latencyMs.total.p95) +) const fallbackCount = computed(() => { const counts = recallDiagnostics.value?.degradationCounts if (!counts) return 0 @@ -461,8 +469,8 @@ function shortId(id: string): string { return id.length > 10 ? `${id.slice(0, 4)}…${id.slice(-6)}` : id } -function formatNumber(value: number): string { - return Number.isFinite(value) ? value.toFixed(value >= 10 ? 0 : 2) : String(value) +function formatOptionalDecimal(value: number | null | undefined): string { + return value == null ? '—' : formatDecimal(value) } function eventLabel(eventType: string): string { diff --git a/src/renderer/settings/components/MemoryLifecyclePanel.vue b/src/renderer/settings/components/MemoryLifecyclePanel.vue index 8e08ef5b4..dc9c3784c 100644 --- a/src/renderer/settings/components/MemoryLifecyclePanel.vue +++ b/src/renderer/settings/components/MemoryLifecyclePanel.vue @@ -151,6 +151,7 @@ import { useI18n } from 'vue-i18n' import { Icon } from '@iconify/vue' import { DcBadge } from '@dc-ui/components/badge' import type { MemoryLifecycle } from '@shared/contracts/routes' +import { useMemoryNumberFormatters } from '../lib/useMemoryNumberFormatters' const props = defineProps<{ lifecycle: MemoryLifecycle | null @@ -159,21 +160,7 @@ const props = defineProps<{ }>() const { t, locale } = useI18n() - -const decimalFormatter = computed( - () => - new Intl.NumberFormat(locale.value || undefined, { - minimumFractionDigits: 0, - maximumFractionDigits: 3 - }) -) -const dayFormatter = computed( - () => - new Intl.NumberFormat(locale.value || undefined, { - minimumFractionDigits: 0, - maximumFractionDigits: 1 - }) -) +const { formatDecimal: formatScore, formatDays } = useMemoryNumberFormatters() const dateFormatter = computed( () => new Intl.DateTimeFormat(locale.value || undefined, { @@ -254,20 +241,12 @@ const archiveConditions = computed(() => { ] }) -function formatScore(value: number): string { - return decimalFormatter.value.format(value) -} - function formatOptionalScore(value: number | null): string { return value === null ? t('settings.deepchatAgents.memoryManager.lifecycle.forget.notRefreshed') : formatScore(value) } -function formatDays(value: number): string { - return dayFormatter.value.format(value) -} - function formatTime(value: number): string { return dateFormatter.value.format(new Date(value)) } diff --git a/src/renderer/settings/lib/useMemoryNumberFormatters.ts b/src/renderer/settings/lib/useMemoryNumberFormatters.ts new file mode 100644 index 000000000..745a818d8 --- /dev/null +++ b/src/renderer/settings/lib/useMemoryNumberFormatters.ts @@ -0,0 +1,27 @@ +import { computed } from 'vue' +import { useI18n } from 'vue-i18n' + +export function useMemoryNumberFormatters() { + const { locale } = useI18n() + const decimalFormatter = computed( + () => + new Intl.NumberFormat(locale.value || undefined, { + minimumFractionDigits: 0, + maximumFractionDigits: 3 + }) + ) + const dayFormatter = computed( + () => + new Intl.NumberFormat(locale.value || undefined, { + minimumFractionDigits: 0, + maximumFractionDigits: 1 + }) + ) + + const formatDecimal = (value: number): string => + Number.isFinite(value) ? decimalFormatter.value.format(value) : String(value) + const formatDays = (value: number): string => + Number.isFinite(value) ? dayFormatter.value.format(value) : String(value) + + return { formatDecimal, formatDays } +} diff --git a/test/renderer/components/MemoryDiagnosticsPanel.test.ts b/test/renderer/components/MemoryDiagnosticsPanel.test.ts index 9cf6144a3..23f3703ed 100644 --- a/test/renderer/components/MemoryDiagnosticsPanel.test.ts +++ b/test/renderer/components/MemoryDiagnosticsPanel.test.ts @@ -180,13 +180,13 @@ describe('MemoryDiagnosticsPanel', () => { expect(clearCopy.clearConfirmBody).toContain('Standing directives are kept') }) - it('renders Agent recall and process-wide pipeline pressure', async () => { + it('renders formatted diagnostics and process-wide pipeline pressure', async () => { const health: MemoryHealthDto = structuredClone(baseHealth) health.runtime.agent.retrieval.recall.latencyMs.total = { samples: 4, - p50: 12, - p95: 48, - max: 50 + p50: 685.7158329999074, + p95: Number.POSITIVE_INFINITY, + max: 804 } health.runtime.agent.retrieval.recall.degradationCounts.vectorCold = 3 health.runtime.process.extractionQueue.depth = 2 @@ -199,18 +199,44 @@ describe('MemoryDiagnosticsPanel', () => { health.runtime.process.providerAdmission.admissionDecisions.rateLimited = 2 const providerPressureSummary = 'Rate limit {rateLimited} · Capacity {capacityRejected} · Deadline {deadline} · Aborted {aborted} · Late settle {lateSettled}' - const { wrapper, t } = await setup(baseStatus, { + const { wrapper, memoryClient, t } = await setup(baseStatus, { health, messages: { 'settings.memory.redesign.providerPressureSummary': providerPressureSummary } }) + memoryClient.getArchiveCandidateLifecyclePreview.mockResolvedValueOnce({ + ...basePreview, + lifecycles: [ + { + memoryId: 'memory-with-floating-point-diagnostics', + decayTier: 'archive_candidate', + forget: { + ageDays: 12.3456, + decayScore: 0.123456 + } + } + ] + }) + await refreshButton(wrapper).trigger('click') + await flushPromises() const pipeline = wrapper.get('[data-testid="runtime-pipeline"]') - expect(pipeline.text()).toContain('12') - expect(pipeline.text()).toContain('48') + const archiveCandidates = wrapper.get('[data-testid="archive-candidates"]') + const pipelineValues = pipeline.findAll('.tabular-nums').map((node) => node.text()) + const archiveCandidateMetrics = archiveCandidates + .findAll('.text-muted-foreground > span') + .map((node) => node.text()) + expect(pipelineValues).toContain('685.716') + expect(pipelineValues).toContain('Infinity') expect(pipeline.text()).toContain('1250') expect(pipeline.text()).toContain('7') expect(pipeline.text()).toContain('Rate limit 2') expect(pipeline.text()).toContain('Deadline 1') + expect(archiveCandidateMetrics).toContain( + 'settings.deepchatAgents.memoryManager.health.archivePrediction.ageDays: 12.3' + ) + expect(archiveCandidateMetrics).toContain( + 'settings.deepchatAgents.memoryManager.health.archivePrediction.decayScore: 0.123' + ) expect(t).toHaveBeenCalledWith('settings.memory.redesign.providerPressureSummary', { rateLimited: 2, capacityRejected: 0,