import { detectFalseStoreCandidate } from './memory-v2-shadow-audit.mjs'; import { inspectMemoryContentDurability, resolvePersonalShadowConfig, } from './memory-v2-personal-shadow.mjs'; import { createPersonalMemoryCandidateStore } from './memory-v2-personal-store.mjs'; const AUTO_REVIEW_ACCEPT_REASONS = new Set([ 'explicit_memory_request', 'preference_signal', 'goal_signal', 'stable_fact_signal', ]); const AUTO_REVIEW_REJECT_REASONS = new Set([ 'decision_signal', ]); export function resolveCandidateAutoReviewAction(candidate, { mode = 'canary' } = {}) { const normalizedMode = String(mode ?? 'canary').trim().toLowerCase(); if (normalizedMode === 'off' || normalizedMode === 'shadow') { return { action: 'pending', reason: 'shadow_mode' }; } if (!candidate || String(candidate.status ?? 'candidate') !== 'candidate') { return { action: 'pending', reason: 'not_pending' }; } const content = String(candidate.content ?? ''); const durabilityIssue = inspectMemoryContentDurability(content); if (durabilityIssue) { return { action: 'reject', reason: durabilityIssue }; } const falseStore = detectFalseStoreCandidate(content); if (falseStore.suspicious) { return { action: 'reject', reason: falseStore.code ?? 'false_store' }; } const policyReason = String(candidate.policyReason ?? ''); if (AUTO_REVIEW_REJECT_REASONS.has(policyReason)) { return { action: 'reject', reason: 'decision_signal_not_durable' }; } if (AUTO_REVIEW_ACCEPT_REASONS.has(policyReason)) { return { action: 'accept', reason: 'auto_review_policy' }; } return { action: 'pending', reason: 'no_auto_review_rule' }; } export function resolveCandidateAutoReviewScopes(env = process.env) { const config = resolvePersonalShadowConfig(env); if (!config.enabled || config.effectiveMode === 'shadow') return []; if (config.effectiveMode === 'active') return [null]; const rolloutMode = String(env.MEMORY_LIFECYCLE_ROLLOUT_MODE ?? 'off').trim().toLowerCase(); const rolloutUserIds = String(env.MEMORY_LIFECYCLE_ROLLOUT_USER_IDS ?? '') .split(/[\s,]+/u) .map((item) => item.trim()) .filter(Boolean) .slice(0, 1000); if (rolloutMode === 'canary' && rolloutUserIds.length > 0) { return [...new Set(rolloutUserIds)]; } if (config.effectiveMode === 'canary') { const agentCanaryUserIds = String(env.MEMORY_AGENT_CANARY_USER_IDS ?? '') .split(/[\s,]+/u) .map((item) => item.trim()) .filter(Boolean) .slice(0, 1000); if (agentCanaryUserIds.length > 0) return [...new Set(agentCanaryUserIds)]; } return []; } export async function runCandidateAutoReviewBatch( pool, { env = process.env, userId = null, limit = 100, reviewedBy = 'system:auto-review', dryRun = false, now = () => Date.now(), } = {}, ) { if (!pool?.query) { return { ok: false, skipped: true, reason: 'no_pool', accepted: 0, rejected: 0, pending: 0 }; } const config = resolvePersonalShadowConfig(env); if (!config.autoReviewEnabled) { return { ok: true, skipped: true, reason: 'auto_review_disabled', accepted: 0, rejected: 0, pending: 0 }; } const store = createPersonalMemoryCandidateStore(pool, { now }); if (!store?.listCandidates || !store?.reviewCandidate) { return { ok: false, skipped: true, reason: 'no_store', accepted: 0, rejected: 0, pending: 0 }; } const items = await store.listCandidates({ status: 'candidate', userId, limit: Math.max(1, Math.min(500, Number(limit) || 100)), offset: 0, }); let accepted = 0; let rejected = 0; let pending = 0; const samples = []; for (const item of items) { const decision = resolveCandidateAutoReviewAction(item, { mode: config.effectiveMode }); if (decision.action === 'accept') { if (dryRun) { accepted += 1; if (samples.length < 5) samples.push({ id: item.id, action: 'accepted', reason: decision.reason }); continue; } const result = await store.reviewCandidate(item.id, 'accepted', { reviewedBy }); if (result.updated) { accepted += 1; if (samples.length < 5) samples.push({ id: item.id, action: 'accepted', reason: decision.reason }); } else { pending += 1; } continue; } if (decision.action === 'reject') { if (dryRun) { rejected += 1; if (samples.length < 5) samples.push({ id: item.id, action: 'rejected', reason: decision.reason }); continue; } const result = await store.reviewCandidate(item.id, 'rejected', { reviewedBy }); if (result.updated) { rejected += 1; if (samples.length < 5) samples.push({ id: item.id, action: 'rejected', reason: decision.reason }); } else { pending += 1; } continue; } pending += 1; } return { ok: true, skipped: false, dryRun, mode: config.effectiveMode, scanned: items.length, accepted, rejected, pending, samples, }; } export async function autoReviewCandidateIfPending( pool, candidate, { env = process.env, reviewedBy = 'system:auto-review', now = () => Date.now(), } = {}, ) { if (!pool?.query || !candidate?.id || String(candidate.status ?? '') !== 'candidate') { return { reviewed: false, reason: 'not_pending' }; } const config = resolvePersonalShadowConfig(env); const decision = resolveCandidateAutoReviewAction(candidate, { mode: config.effectiveMode }); if (decision.action !== 'accept' && decision.action !== 'reject') { return { reviewed: false, reason: decision.reason }; } const store = createPersonalMemoryCandidateStore(pool, { now }); const status = decision.action === 'accept' ? 'accepted' : 'rejected'; const result = await store.reviewCandidate(candidate.id, status, { reviewedBy }); return { reviewed: Boolean(result.updated), status, reason: decision.reason, }; }