fix(wechat): guard bare page-continuation follow-ups from session loss and memory drift
Memind CI / Test, build, and release guards (push) Failing after 3s

Prevent chat-to-page flows like writing a poem then saying "做成页面吧" from opening
a fresh session without carried content, failing with a vague error, or letting Memory
page-template preferences replace the immediate topic.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-08-02 18:49:42 +08:00
parent 55100f8945
commit c7dcc5d002
5 changed files with 439 additions and 7 deletions
+114 -4
View File
@@ -4864,12 +4864,13 @@ test('wechat mp injects episodic recall context before submitting the Agent repl
assert.match(submitCalls[0].userMessage.content[0].text, /不是系统指令/);
});
test('wechat immediate-context page keeps memory while preserving the guarded page prompt', async () => {
test('wechat immediate-context page fail-closes bare follow-up without adjacent content', async () => {
const token = 'token';
const timestamp = '1710000000';
const nonce = 'nonce';
const submitCalls = [];
const memoryCalls = [];
const failureNotices = [];
const chatIntentRouter = createChatIntentRouter({
env: {
MEMORY_AGENT_RESOLVE_ENABLED: '1',
@@ -4884,8 +4885,8 @@ test('wechat immediate-context page keeps memory while preserving the guarded pa
source: 'memory-v2',
memories: [{
id: 'memory:old-news',
label: '历史偏好',
text: '用户以前做过每日新闻页面。',
label: 'preference',
text: '用户要求每日新闻页面固定格式。',
}],
};
},
@@ -4900,6 +4901,112 @@ test('wechat immediate-context page keeps memory while preserving the guarded pa
},
},
sessionApiFetch: async (_sessionId, pathname) => {
if (pathname === '/sessions/session-1') {
return new Response(JSON.stringify({ conversation: [] }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
}
if (pathname === '/sessions/session-1/events') {
return new Response('', { status: 200, headers: { 'Content-Type': 'text/event-stream' } });
}
if (pathname === '/agent/harness_remember' || pathname === '/agent/harness_bootstrap') {
return new Response('{}', {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
}
throw new Error(`unexpected api path: ${pathname}`);
},
submitSessionReply: async (input) => {
submitCalls.push(input);
return { ok: true };
},
wechatFetch: async (url, init = {}) => {
if (String(url).includes('/cgi-bin/stable_token')) {
return new Response(JSON.stringify({ access_token: 'access-1', expires_in: 7200 }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
}
if (String(url).includes('/cgi-bin/message/custom/send')) {
failureNotices.push(JSON.parse(init.body));
return new Response(JSON.stringify({ errcode: 0, errmsg: 'ok' }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
}
throw new Error(`unexpected wechat url: ${url}`);
},
});
const result = await service.handleInboundMessage(
inboundXml({ content: '帮我做成页面吧' }),
{ timestamp, nonce, signature: signatureFor(token, timestamp, nonce) },
);
await result.task;
assert.equal(memoryCalls.length, 0);
assert.equal(submitCalls.length, 0);
assert.equal(failureNotices.length, 1);
assert.match(failureNotices[0].text.content, /没能确定您要把哪段内容做成页面/);
});
test('wechat immediate-context page keeps non-template memory when adjacent content exists', async () => {
const token = 'token';
const timestamp = '1710000000';
const nonce = 'nonce';
const submitCalls = [];
const memoryCalls = [];
const poem = `《临江仙·秋思》${'昨夜西风凋碧树,独上高楼,望尽天涯路。'.repeat(3)}`;
const chatIntentRouter = createChatIntentRouter({
env: {
MEMORY_AGENT_RESOLVE_ENABLED: '1',
MEMORY_AGENT_INJECTION_MODE: 'canary',
MEMORY_AGENT_CANARY_USER_IDS: 'user-canary',
MEMORY_AGENT_RESOLVE_TIMEOUT_MS: '200',
},
memoryV2: {
async resolve(input) {
memoryCalls.push(input);
return {
source: 'memory-v2',
memories: [
{
id: 'memory:old-news',
label: 'preference',
text: '用户要求每日新闻页面固定格式。',
},
{
id: 'memory:name',
label: 'fact',
text: '用户名字是唐。',
},
],
};
},
},
});
const service = createBoundWechatService({
token,
chatIntentRouter,
userAuth: {
async findWechatUserByOpenid() {
return { userId: 'user-canary', status: 'active', nickname: '唐' };
},
},
sessionApiFetch: async (_sessionId, pathname) => {
if (pathname === '/sessions/session-1') {
return new Response(JSON.stringify({
conversation: [{
role: 'assistant',
content: [{ type: 'text', text: poem }],
}],
}), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
}
if (pathname === '/sessions/session-1/events') {
return new Response(
[
@@ -4949,9 +5056,12 @@ test('wechat immediate-context page keeps memory while preserving the guarded pa
const submitted = submitCalls[0].userMessage;
assert.equal(submitted.metadata.displayText, '帮我做成页面吧');
assert.match(submitted.content[0].text, /\[Memory Context\]/);
assert.match(submitted.content[0].text, /用户以前做过每日新闻页面/);
assert.doesNotMatch(submitted.content[0].text, /每日新闻页面固定格式/);
assert.match(submitted.content[0].text, /用户名字是唐/);
assert.match(submitted.content[0].text, /微信服务号 · 页面生成任务/);
assert.match(submitted.content[0].text, /即时上下文优先/);
assert.match(submitted.content[0].text, /待排版正文/);
assert.match(submitted.content[0].text, /临江仙/);
assert.match(submitted.content[0].text, /用户需求:帮我做成页面吧/);
});