feat(admin): surface Memory V2 candidate auto-review in management UI

Clarify active/canary/shadow modes and show auto-review metrics so operators do not need to manually approve every user memory.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-07-13 14:16:57 +08:00
parent 731d93843d
commit 7df3ae35f1
2 changed files with 13 additions and 7 deletions
+12 -7
View File
@@ -57,14 +57,14 @@ const CAPABILITIES: Array<{
key: 'candidateMemory', key: 'candidateMemory',
label: '候选记忆', label: '候选记忆',
icon: 'C', icon: 'C',
description: '控制候选提取模式、接纳阈值与待处理数量。', description: '控制候选提取模式、自动接纳阈值与待处理数量。',
fields: [ fields: [
{ key: 'enabled', label: '启用候选记忆', type: 'boolean' }, { key: 'enabled', label: '启用候选记忆', type: 'boolean' },
{ key: 'mode', label: '运行模式', type: 'select', options: [ { key: 'mode', label: '运行模式', type: 'select', options: [
{ value: 'off', label: 'Off' }, { value: 'off', label: 'Off · 关闭' },
{ value: 'shadow', label: 'Shadow' }, { value: 'active', label: 'Active · 规则通过后自动接纳(推荐)' },
{ value: 'canary', label: 'Canary' }, { value: 'canary', label: 'Canary · 仅高置信度自动接纳' },
{ value: 'active', label: 'Active' }, { value: 'shadow', label: 'Shadow · 全部人工审核' },
] }, ] },
{ key: 'minImportance', label: '最低重要度', type: 'number' }, { key: 'minImportance', label: '最低重要度', type: 'number' },
{ key: 'minConfidence', label: '最低置信度', type: 'number' }, { key: 'minConfidence', label: '最低置信度', type: 'number' },
@@ -314,7 +314,7 @@ function defaultConfig(): MemoryV2AdminConfig {
timeoutMs: '1500', timeoutMs: '1500',
fallbackRoute: 'agent_orchestration', fallbackRoute: 'agent_orchestration',
}, },
candidateMemory: { enabled: false, mode: 'off', minImportance: '0.7', minConfidence: '0.8', maxPending: '500', persistenceEnabled: false }, candidateMemory: { enabled: false, mode: 'active', minImportance: '0.7', minConfidence: '0.8', maxPending: '500', persistenceEnabled: false },
policy: { enabled: false, saveExplicit: true, rejectSensitive: true, requireEvidence: true, retentionDays: '365' }, policy: { enabled: false, saveExplicit: true, rejectSensitive: true, requireEvidence: true, retentionDays: '365' },
retriever: { enabled: false, episodicEnabled: true, semanticEnabled: true, preferenceEnabled: true, goalEnabled: true, limit: '12', tokenBudget: '1800', timeoutMs: '1200' }, retriever: { enabled: false, episodicEnabled: true, semanticEnabled: true, preferenceEnabled: true, goalEnabled: true, limit: '12', tokenBudget: '1800', timeoutMs: '1200' },
lifecycle: { enabled: false, dedupeEnabled: true, conflictReview: true, decayEnabled: false, forgettingEnabled: true, compactIntervalHours: '24' }, lifecycle: { enabled: false, dedupeEnabled: true, conflictReview: true, decayEnabled: false, forgettingEnabled: true, compactIntervalHours: '24' },
@@ -954,8 +954,10 @@ export function MemoryV2Page() {
<span> <span>
{String(runtimeStatus.memory.personalMemory.health?.state ?? 'unknown')} {String(runtimeStatus.memory.personalMemory.health?.state ?? 'unknown')}
{' · '} {runtimeStatus.memory.personalMemory.effectiveMode ?? 'off'} {' · '} {runtimeStatus.memory.personalMemory.effectiveMode ?? 'off'}
{runtimeStatus.memory.personalMemory.autoReviewEnabled ? ' · 自动审核' : ' · 人工审核'}
{' · '} {runtimeStatus.memory.personalMemory.pendingCandidates ?? 0} {' · '} {runtimeStatus.memory.personalMemory.pendingCandidates ?? 0}
{' · '} {runtimeStatus.memory.personalMemory.health?.accepted ?? 0} {' · '} {runtimeStatus.memory.personalMemory.health?.accepted ?? 0}
{' · '} {runtimeStatus.memory.personalMemory.health?.autoReviewed ?? 0}
{' · '} {runtimeStatus.memory.personalMemory.health?.rejected ?? 0} {' · '} {runtimeStatus.memory.personalMemory.health?.rejected ?? 0}
{' · '} {runtimeStatus.memory.personalMemory.health?.deduped ?? 0} {' · '} {runtimeStatus.memory.personalMemory.health?.deduped ?? 0}
</span> </span>
@@ -992,6 +994,9 @@ export function MemoryV2Page() {
{' · '} {candidatePayload?.counts?.accepted ?? 0} {' · '} {candidatePayload?.counts?.accepted ?? 0}
{' · '} {candidatePayload?.counts?.rejected ?? 0} {' · '} {candidatePayload?.counts?.rejected ?? 0}
</p> </p>
<p className="model-center-card-note">
Active Shadow / Canary
</p>
</div> </div>
<button type="button" className="ghost-btn" onClick={() => void load()} disabled={loading || candidateBusyId !== null}> <button type="button" className="ghost-btn" onClick={() => void load()} disabled={loading || candidateBusyId !== null}>
@@ -1000,7 +1005,7 @@ export function MemoryV2Page() {
{candidateError && <p className="banner banner-error">{candidateError}</p>} {candidateError && <p className="banner banner-error">{candidateError}</p>}
{!candidateError && (candidatePayload?.items.length ?? 0) === 0 && ( {!candidateError && (candidatePayload?.items.length ?? 0) === 0 && (
<p className="model-center-card-note">Shadow Pipeline </p> <p className="model-center-card-note">Active Shadow / Canary </p>
)} )}
<div className="memory-v2-candidate-list"> <div className="memory-v2-candidate-list">
{(candidatePayload?.items ?? []).map((candidate) => ( {(candidatePayload?.items ?? []).map((candidate) => (
+1
View File
@@ -334,6 +334,7 @@ export type MemoryV2RuntimeStatusResponse = {
enabled?: boolean; enabled?: boolean;
requestedMode?: string; requestedMode?: string;
effectiveMode?: string; effectiveMode?: string;
autoReviewEnabled?: boolean;
phase?: string; phase?: string;
injectionEnabled?: boolean; injectionEnabled?: boolean;
persistence?: string; persistence?: string;