Files
memind/scripts/check-goosed-v149-memory-chat.mjs
T
john 67220a14ea fix(goose-v149): block unattended real LLM smokes by default
Prevent phase2/phase3/all reruns from burning DashScope tokens unless
GOOSE_V149_ALLOW_REAL_LLM=1 is explicitly set with human approval.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-09 22:08:39 +08:00

270 lines
9.2 KiB
JavaScript

#!/usr/bin/env node
/**
* Memory via Goose chat chain: Portal agent/runs → remember/sync → new session recall.
*/
import crypto from 'node:crypto';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { createConversationMemoryService } from '../conversation-memory.mjs';
import { createMemoryV2 } from '../memory-v2.mjs';
import { createDbPool } from '../db.mjs';
import { prepareGooseV149CheckEnv } from './goose-v149-canary.mjs';
import { enforceRealLlmGate } from './goose-v149-real-llm-gate.mjs';
import {
createReporter,
loginViaApi,
resolvePortalBase,
waitForAssistantGrowth,
waitForRunTerminal,
} from './scenario-test-lib.mjs';
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
enforceRealLlmGate('check-goosed-v149-memory-chat.mjs');
prepareGooseV149CheckEnv(process.env, root);
const baseUrl = resolvePortalBase(Number(process.env.H5_PORT ?? 8081));
const skip = process.env.GOOSE_V149_MEMORY_CHAT_SKIP === '1';
const timeoutMs = Number(process.env.GOOSE_V149_MEMORY_CHAT_TIMEOUT_MS || 180_000);
const MARKER = process.env.GOOSE_V149_MEMORY_CHAT_MARKER
?? `gv149mem${Date.now().toString(36)}`;
async function portalReachable() {
try {
const response = await fetch(`${baseUrl}/auth/status`);
return response.ok;
} catch {
return false;
}
}
async function requestJson(url, { method = 'GET', cookie, body, timeoutMs: reqTimeout = timeoutMs } = {}) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), reqTimeout);
try {
const response = await fetch(url, {
method,
headers: {
...(body ? { 'Content-Type': 'application/json' } : {}),
...(cookie ? { Cookie: cookie } : {}),
},
body: body ? JSON.stringify(body) : undefined,
signal: controller.signal,
});
const text = await response.text();
let json = null;
try {
json = text ? JSON.parse(text) : null;
} catch {
json = null;
}
return { ok: response.ok, status: response.status, json, text };
} finally {
clearTimeout(timer);
}
}
async function startSession(cookie) {
const started = await requestJson(`${baseUrl}/api/agent/start`, {
method: 'POST',
cookie,
body: {},
});
const sessionId = started.json?.id ?? null;
if (!started.ok || !sessionId) {
throw new Error(`agent/start failed: ${started.status} ${started.text?.slice(0, 200)}`);
}
return sessionId;
}
async function cleanupMemoryChatArtifacts(pool, userId, { marker, sessionIds = [] } = {}) {
if (!userId || !marker) return;
await pool.query(
`DELETE FROM h5_user_memory_items WHERE user_id = ? AND memory_text LIKE ?`,
[userId, `%${marker}%`],
);
const [candidateTable] = await pool.query(
`SELECT 1 FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'h5_memory_v2_candidates'
LIMIT 1`,
);
if (candidateTable.length) {
await pool.query(
`DELETE FROM h5_memory_v2_candidates WHERE user_id = ? AND content LIKE ?`,
[userId, `%${marker}%`],
);
}
if (sessionIds.length) {
await pool.query(
`DELETE FROM h5_conversation_messages WHERE user_id = ? AND agent_session_id IN (?)`,
[userId, sessionIds],
);
await pool.query(
`DELETE FROM h5_user_sessions WHERE user_id = ? AND agent_session_id IN (?)`,
[userId, sessionIds],
);
}
}
async function runChat(cookie, sessionId, text) {
const requestId = crypto.randomUUID();
const created = await requestJson(`${baseUrl}/api/agent/runs`, {
method: 'POST',
cookie,
body: {
request_id: requestId,
session_id: sessionId,
force_deep_reasoning: true,
user_message: {
id: crypto.randomUUID(),
role: 'user',
created: Math.floor(Date.now() / 1000),
content: [{ type: 'text', text }],
metadata: {
userVisible: true,
agentVisible: true,
displayText: text,
},
},
},
});
const runId = created.json?.run?.id ?? null;
if (!created.ok || created.status !== 202 || !runId) {
throw new Error(`agent run failed: ${created.status} ${created.text?.slice(0, 300)}`);
}
const terminal = await waitForRunTerminal(baseUrl, cookie, runId, timeoutMs);
if (terminal.status !== 'succeeded') {
throw new Error(`agent run terminal=${terminal.status} ${terminal.error ?? ''}`);
}
const activeSessionId = terminal.sessionId ?? terminal.agent_session_id ?? sessionId;
const reply = await waitForAssistantGrowth(baseUrl, cookie, activeSessionId, {
minChars: 1,
timeoutMs: Math.min(timeoutMs, 90_000),
});
return {
runId,
sessionId: activeSessionId,
replyText: reply?.combined ?? '',
};
}
async function main() {
if (skip || !(await portalReachable())) {
console.log(`GOOSE_V149_MEMORY_CHAT_SKIP: Portal not running at ${baseUrl}`);
console.log('GOOSE_V149_MEMORY_CHAT_OK: skipped');
return;
}
const reporter = createReporter();
const username = process.env.RELEASE_GATE_SCENARIO_USERNAME ?? 'john';
const password =
process.env.JOHN_PASSWORD
?? process.env.H5_ACCESS_PASSWORD
?? process.env.MEMIND_PASSWORD
?? '';
if (!password) {
throw new Error('set JOHN_PASSWORD (or H5_ACCESS_PASSWORD) for memory chat smoke');
}
const auth = await loginViaApi(baseUrl, { username, password }, reporter);
const userId = auth.user?.id ? String(auth.user.id) : '';
const sessionA = await startSession(auth.cookie);
let sessionB = '';
let writeTurn = null;
let memories = 0;
const pool = createDbPool();
try {
const rememberPrompt =
`请记住:我的测试代号是「${MARKER}」。请简短确认已记住,不要展开其他内容。`;
writeTurn = await runChat(auth.cookie, sessionA, rememberPrompt);
reporter.pass('memory write turn', `${writeTurn.replyText.length}`);
const remember = await requestJson(`${baseUrl}/api/user-memory/v1/remember-recent`, {
method: 'POST',
cookie: auth.cookie,
body: { sessionId: writeTurn.sessionId },
});
if (!remember.ok || remember.json?.ok !== true) {
throw new Error(`remember-recent failed: ${remember.status} ${remember.text?.slice(0, 200)}`);
}
const analyzed = Number(remember.json?.analyzed ?? 0);
memories = Number(remember.json?.memories ?? 0);
if (analyzed <= 0 && memories <= 0) {
throw new Error(`remember-recent extracted nothing: ${JSON.stringify(remember.json)}`);
}
reporter.pass('remember-recent', `analyzed=${analyzed} memories=${memories}`);
const sync = await requestJson(`${baseUrl}/api/user-memory/v1/sync`, {
method: 'POST',
cookie: auth.cookie,
body: { sessionId: writeTurn.sessionId },
});
if (!sync.ok || sync.json?.ok !== true) {
throw new Error(`memory sync failed: ${sync.status} ${sync.text?.slice(0, 200)}`);
}
reporter.pass('memory sync', `total=${sync.json?.totalMemories ?? '?'}`);
const items = await requestJson(`${baseUrl}/api/user-memory/v1/items?limit=200`, {
cookie: auth.cookie,
});
if (!items.ok || items.json?.ok !== true) {
throw new Error(`memory items failed: ${items.status} ${items.text?.slice(0, 200)}`);
}
const itemTexts = (items.json?.items ?? []).map((item) =>
String(item.content ?? item.text ?? item.memory_text ?? item.memoryText ?? ''),
);
if (!itemTexts.some((text) => text.includes(MARKER))) {
throw new Error(`memory items missing marker; sample=${itemTexts.slice(0, 3).join(' | ')}`);
}
reporter.pass('memory items', `marker present (${itemTexts.length} items)`);
sessionB = await startSession(auth.cookie);
prepareGooseV149CheckEnv(process.env, root);
const memoryEnv = {
...process.env,
MEMORY_ENABLED: '1',
MEMORY_BACKEND: 'legacy',
MEMORY_VECTOR_ENABLED: '0',
MEMORY_AGENT_INJECTION_MODE: 'off',
MEMORY_LIFECYCLE_ENABLED: '0',
MEMORY_CANDIDATE_ENABLED: '0',
};
const conversationMemory = createConversationMemoryService(pool, {
encryptionKey: process.env.TKMIND_ENCRYPTION_KEY ?? 'local-memory-chat-key',
getEffectiveEnv: async () => memoryEnv,
});
const memory = createMemoryV2({
legacyMemoryService: conversationMemory,
env: memoryEnv,
logger: console,
});
const resolved = await memory.resolve({
userId,
sessionId: sessionB,
query: MARKER,
});
const resolvedTexts = (resolved.memories ?? []).map((item) => String(item.text ?? ''));
if (!resolvedTexts.some((text) => text.includes(MARKER))) {
throw new Error(
`memory.resolve missed marker in new session; memories=${JSON.stringify(resolvedTexts).slice(0, 300)}`,
);
}
reporter.pass('memory resolve (new session)', `${resolvedTexts.length} hits`);
console.log(
`GOOSE_V149_MEMORY_CHAT_OK: marker=${MARKER} sessionA=${writeTurn.sessionId} `
+ `sessionB=${sessionB} remember.memories=${memories} base=${baseUrl}`,
);
} finally {
const sessionIds = [sessionA, sessionB].filter(Boolean);
await cleanupMemoryChatArtifacts(pool, userId, { marker: MARKER, sessionIds }).catch(() => {});
await pool.end().catch(() => {});
}
}
main().catch((error) => {
console.error(`GOOSE_V149_MEMORY_CHAT_FAIL: ${error.message}`);
process.exit(1);
});