feat(memory-v2): close Phase A with auto-review, product events, and H5 recall UI.

Add candidate auto-review pipeline, shadow audit tooling, admin metrics page,
and user-visible memory recall hints in chat with phase-a readiness checks.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-08-01 17:14:06 +08:00
parent 666db0b939
commit 6f3e53a56a
50 changed files with 3730 additions and 38 deletions
+95 -10
View File
@@ -27,7 +27,7 @@ test('personal memory shadow config keeps shadow mode for manual review', () =>
assert.equal(config.autoReviewEnabled, false);
});
test('shouldAutoAcceptCandidate auto accepts explicit requests in canary mode', () => {
test('shouldAutoAcceptCandidate only auto accepts explicit requests unless autoAcceptAll is enabled', () => {
const config = resolvePersonalShadowConfig({
MEMORY_CANDIDATE_ENABLED: '1',
MEMORY_CANDIDATE_MODE: 'canary',
@@ -44,15 +44,26 @@ test('shouldAutoAcceptCandidate auto accepts explicit requests in canary mode',
assert.equal(shouldAutoAcceptCandidate({
policyReason: 'decision_signal',
confidence: 0.9,
}, config), true);
}, config), false);
const activeConfig = resolvePersonalShadowConfig({
MEMORY_CANDIDATE_ENABLED: '1',
MEMORY_CANDIDATE_MODE: 'active',
MEMORY_CANDIDATE_AUTO_ACCEPT_ALL: '1',
});
assert.equal(shouldAutoAcceptCandidate({
policyReason: 'decision_signal',
confidence: 0.9,
}, activeConfig), true);
});
test('shadow pipeline auto accepts durable candidates in active mode', async () => {
test('shadow pipeline auto accepts durable candidates in active mode when autoAcceptAll is enabled', async () => {
const saved = [];
const pipeline = createPersonalMemoryShadowPipeline({
env: {
MEMORY_CANDIDATE_ENABLED: '1',
MEMORY_CANDIDATE_MODE: 'active',
MEMORY_CANDIDATE_AUTO_ACCEPT_ALL: '1',
MEMORY_POLICY_ENABLED: '1',
},
store: {
@@ -75,6 +86,75 @@ test('shadow pipeline auto accepts durable candidates in active mode', async ()
assert.equal(pipeline.getStatus().phase, 'auto-review-v1');
});
test('shadow pipeline keeps decision candidates pending in active mode by default', 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, 0);
assert.equal(result.pendingReview, 1);
assert.equal(saved[0].options.autoAccept, false);
});
test('shadow pipeline rejects agent continuation task envelopes', async () => {
const pipeline = createPersonalMemoryShadowPipeline({
env: {
MEMORY_CANDIDATE_ENABLED: '1',
MEMORY_CANDIDATE_MODE: 'shadow',
},
});
const result = await pipeline.observeWrite({
userId: 'u1',
sessionId: 's1',
messages: [{
role: 'user',
text: '继续修改同一个 public/durability-survey-20260727.html,保留全部现有 Page Data 绑定。',
}],
});
assert.equal(result.accepted, 0);
assert.equal(result.rejected, 1);
assert.equal(pipeline.listCandidates().length, 0);
});
test('shadow pipeline auto accepts explicit remember requests in canary mode', async () => {
const saved = [];
const pipeline = createPersonalMemoryShadowPipeline({
env: {
MEMORY_CANDIDATE_ENABLED: '1',
MEMORY_CANDIDATE_MODE: 'canary',
},
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: '请记住我每周三下午做代码评审。' }],
});
assert.equal(saved.length, 1);
assert.equal(saved[0].options.autoAccept, true);
assert.equal(saved[0].candidate.policyReason, 'explicit_memory_request');
});
test('shadow pipeline keeps low-confidence canary candidates pending review', async () => {
const saved = [];
const pipeline = createPersonalMemoryShadowPipeline({
@@ -232,7 +312,7 @@ test('shadow pipeline persists accepted candidates when a store is configured',
assert.equal(pipeline.getStatus().persistence, 'mysql');
});
test('Memory V2 does not wait for the shadow pipeline before returning legacy write result', async () => {
test('Memory V2 awaits shadow pipeline so personalMemory can surface in API responses', async () => {
let releaseShadow;
const shadowBlocked = new Promise((resolve) => { releaseShadow = resolve; });
const memory = createMemoryV2({
@@ -244,16 +324,21 @@ test('Memory V2 does not wait for the shadow pipeline before returning legacy wr
}],
personalShadowPipeline: {
config: { enabled: true },
observeWrite: () => shadowBlocked,
observeWrite: () => shadowBlocked.then(() => ({ results: [] })),
getStatus: () => ({ enabled: true }),
},
});
const result = await Promise.race([
memory.write({ userId: 'u1', sessionId: 's1', messages: [] }),
new Promise((_, reject) => setTimeout(() => reject(new Error('write waited for shadow')), 50)),
]);
assert.equal(result.saved, 1);
let settled = false;
const writePromise = memory.write({ userId: 'u1', sessionId: 's1', messages: [] }).then((result) => {
settled = true;
return result;
});
await new Promise((resolve) => setTimeout(resolve, 20));
assert.equal(settled, false, 'write should wait for shadow pipeline');
releaseShadow();
const result = await writePromise;
assert.equal(result.saved, 1);
assert.deepEqual(result.personalMemory, { results: [] });
});
test('Memory V2 exposes a shadow-only observation entry without calling legacy write', async () => {