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:
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user