feat(memory-v2): auto-accept candidate memories in active and canary modes

Let Personal Memory candidates pass policy gates without per-user manual review, while keeping shadow mode for explicit human approval.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-07-13 14:16:57 +08:00
parent 7f7aced751
commit b33c943b69
4 changed files with 147 additions and 13 deletions
+93 -3
View File
@@ -1,16 +1,103 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { createPersonalMemoryShadowPipeline, resolvePersonalShadowConfig } from './memory-v2-personal-shadow.mjs';
import {
createPersonalMemoryShadowPipeline,
resolvePersonalShadowConfig,
shouldAutoAcceptCandidate,
} from './memory-v2-personal-shadow.mjs';
import { createMemoryV2 } from './memory-v2.mjs';
test('personal memory shadow config never activates response injection', () => {
test('personal memory shadow config maps active mode to auto review', () => {
const config = resolvePersonalShadowConfig({
MEMORY_CANDIDATE_ENABLED: '1',
MEMORY_CANDIDATE_MODE: 'active',
});
assert.equal(config.enabled, true);
assert.equal(config.requestedMode, 'active');
assert.equal(config.effectiveMode, 'active');
assert.equal(config.autoReviewEnabled, true);
});
test('personal memory shadow config keeps shadow mode for manual review', () => {
const config = resolvePersonalShadowConfig({
MEMORY_CANDIDATE_ENABLED: '1',
MEMORY_CANDIDATE_MODE: 'shadow',
});
assert.equal(config.effectiveMode, 'shadow');
assert.equal(config.autoReviewEnabled, false);
});
test('shouldAutoAcceptCandidate auto accepts explicit requests in canary mode', () => {
const config = resolvePersonalShadowConfig({
MEMORY_CANDIDATE_ENABLED: '1',
MEMORY_CANDIDATE_MODE: 'canary',
MEMORY_CANDIDATE_MIN_CONFIDENCE: '0.8',
});
assert.equal(shouldAutoAcceptCandidate({
policyReason: 'explicit_memory_request',
confidence: 0.98,
}, config), true);
assert.equal(shouldAutoAcceptCandidate({
policyReason: 'stable_fact_signal',
confidence: 0.82,
}, config), false);
assert.equal(shouldAutoAcceptCandidate({
policyReason: 'decision_signal',
confidence: 0.9,
}, config), true);
});
test('shadow pipeline auto accepts durable candidates in active mode', async () => {
const saved = [];
const pipeline = createPersonalMemoryShadowPipeline({
env: {
MEMORY_CANDIDATE_ENABLED: '1',
MEMORY_CANDIDATE_MODE: 'active',
MEMORY_POLICY_ENABLED: '1',
},
store: {
async saveCandidate(candidate, options = {}) {
saved.push({ candidate, options });
return { inserted: true, status: options.autoAccept ? 'accepted' : 'candidate' };
},
},
});
const result = await pipeline.observeWrite({
userId: 'u1',
sessionId: 's1',
messages: [{ role: 'user', text: '我决定所有生产发布必须从完整 main 分支打包。' }],
});
assert.equal(result.autoReviewed, 1);
assert.equal(result.pendingReview, 0);
assert.equal(saved.length, 1);
assert.equal(saved[0].options.autoAccept, true);
assert.equal(pipeline.getStatus().autoReviewEnabled, true);
assert.equal(pipeline.getStatus().phase, 'auto-review-v1');
});
test('shadow pipeline keeps low-confidence canary candidates pending review', async () => {
const saved = [];
const pipeline = createPersonalMemoryShadowPipeline({
env: {
MEMORY_CANDIDATE_ENABLED: '1',
MEMORY_CANDIDATE_MODE: 'canary',
MEMORY_CANDIDATE_MIN_CONFIDENCE: '0.8',
},
store: {
async saveCandidate(candidate, options = {}) {
saved.push({ candidate, options });
return { inserted: true, status: options.autoAccept ? 'accepted' : 'candidate' };
},
},
});
await pipeline.observeWrite({
userId: 'u1',
sessionId: 's1',
messages: [{ role: 'user', text: '项目使用 pgvector 作为向量存储。' }],
});
assert.equal(saved.length, 1);
assert.equal(saved[0].options.autoAccept, false);
assert.equal(pipeline.getStatus().health.pendingReview, 1);
});
test('shadow pipeline extracts durable candidates with evidence and deduplicates', async () => {
@@ -24,7 +111,10 @@ test('shadow pipeline extracts durable candidates with evidence and deduplicates
MEMORY_CANDIDATE_MIN_IMPORTANCE: '0.7',
MEMORY_CANDIDATE_MIN_CONFIDENCE: '0.8',
},
now: () => clock += 1,
now: () => {
clock += 1;
return clock;
},
});
const input = {
userId: 'u1',