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:
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user