Files
memind/scripts/check-goosed-v149-memory-loop.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

185 lines
6.1 KiB
JavaScript

#!/usr/bin/env node
/**
* Phase 1: legacy Memory owner write → compact → new-session resolve (loopback MySQL).
*/
import crypto from 'node:crypto';
import mysql from 'mysql2/promise';
import { createConversationMemoryService } from '../conversation-memory.mjs';
import { createMemoryV2 } from '../memory-v2.mjs';
import { loadMemindEnvFiles } from './memind-runtime-profile.mjs';
import { enforceRealLlmGate } from './goose-v149-real-llm-gate.mjs';
enforceRealLlmGate('check-goosed-v149-memory-loop.mjs');
const BLOCKED_DB_HOSTS = ['58.38.22.103', '120.26.184.105', 'rds.aliyuncs.com'];
const MARKER = `goose-v149-memory-loop-${Date.now()}`;
function assertLocalDatabase(env = process.env) {
const raw = String(env.DATABASE_URL ?? '').trim();
if (raw) {
const hostname = new URL(raw.replace(/^mysql:/, 'http:')).hostname.toLowerCase();
for (const blocked of BLOCKED_DB_HOSTS) {
if (hostname.includes(blocked)) throw new Error(`Refusing production DATABASE_URL host: ${hostname}`);
}
return;
}
const host = String(env.MYSQL_HOST ?? '127.0.0.1').trim().toLowerCase();
for (const blocked of BLOCKED_DB_HOSTS) {
if (host.includes(blocked)) throw new Error(`Refusing production MYSQL_HOST: ${host}`);
}
}
function createPool() {
loadMemindEnvFiles(process.cwd());
assertLocalDatabase(process.env);
const options = { connectionLimit: 3 };
if (process.env.DATABASE_URL) {
return mysql.createPool({ uri: process.env.DATABASE_URL, ...options });
}
return mysql.createPool({
host: process.env.MYSQL_HOST ?? '127.0.0.1',
port: Number(process.env.MYSQL_PORT ?? 3306),
user: process.env.MYSQL_USER ?? 'boot',
password: process.env.MYSQL_PASSWORD ?? '',
database: process.env.MYSQL_DATABASE ?? 'tkmind',
...options,
});
}
async function resolveUserId(pool) {
const explicit = String(process.env.GOOSE_V149_MEMORY_LOOP_USER_ID ?? '').trim();
if (explicit) return explicit;
const [rows] = await pool.query(`SELECT id FROM h5_users ORDER BY created_at DESC LIMIT 1`);
const userId = rows[0]?.id ? String(rows[0].id) : '';
if (!userId) throw new Error('No h5_users row found; set GOOSE_V149_MEMORY_LOOP_USER_ID');
return userId;
}
function buildMockLlmProvider() {
return {
async createChatCompletion() {
return {
ok: true,
reply: JSON.stringify({
memories: [{
label: 'fact',
text: `${MARKER} 用户偏好 v1.49 本地记忆闭环测试`,
}],
}),
};
},
};
}
async function ensureSession(pool, userId, sessionId) {
const ts = Date.now();
await pool.query(
`INSERT INTO h5_user_sessions (agent_session_id, user_id, origin, created_at)
VALUES (?, ?, 'h5', ?)
ON DUPLICATE KEY UPDATE user_id = VALUES(user_id)`,
[sessionId, userId, ts],
);
}
async function cleanup(pool, userId, sessionIds) {
await pool.query(
`DELETE FROM h5_user_memory_items WHERE user_id = ? AND memory_text 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 main() {
const env = {
...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',
MEMORY_EVENT_LOG_ENABLED: '1',
MEMORY_FAIL_OPEN: '1',
USER_CONVERSATION_MEMORY_ENABLED: '1',
USER_CONVERSATION_MEMORY_LLM_ENABLED: '1',
};
const pool = createPool();
const userId = await resolveUserId(pool);
const sessionA = `v149-loop-a-${crypto.randomUUID().slice(0, 8)}`;
const sessionB = `v149-loop-b-${crypto.randomUUID().slice(0, 8)}`;
const messageId = crypto.randomUUID().replace(/-/g, '');
try {
const conversationMemory = createConversationMemoryService(pool, {
llmProviderService: buildMockLlmProvider(),
encryptionKey: process.env.TKMIND_ENCRYPTION_KEY ?? 'local-memory-loop-key',
getEffectiveEnv: async () => env,
});
const memory = createMemoryV2({
legacyMemoryService: conversationMemory,
env,
logger: console,
});
const status = memory.getStatus();
if (status.backend !== 'legacy' || status.selectedBackend !== 'legacy-conversation-memory') {
throw new Error(
`Expected legacy backend, got backend=${status.backend} selected=${status.selectedBackend}`,
);
}
await ensureSession(pool, userId, sessionA);
const written = await memory.write({
userId,
sessionId: sessionA,
messages: [{
id: messageId,
role: 'user',
content: [{ type: 'text', text: `${MARKER} 请记住我喜欢 v1.49 本地记忆闭环测试` }],
metadata: { userVisible: true, agentVisible: true },
}],
});
if (!written.ok || Number(written.memories) < 1) {
throw new Error(`write failed: ${JSON.stringify(written)}`);
}
const compacted = await memory.compact({ userId });
if (!compacted.ok || compacted.source !== 'legacy-conversation-memory') {
throw new Error(`compact failed: ${JSON.stringify(compacted)}`);
}
const resolved = await memory.resolve({
userId,
sessionId: sessionB,
query: MARKER,
});
const texts = (resolved.memories ?? []).map((item) => String(item.text ?? ''));
if (!texts.some((text) => text.includes(MARKER))) {
throw new Error(`resolve in new session missed marker; memories=${JSON.stringify(texts)}`);
}
console.log(`GOOSE_V149_MEMORY_LOOP_OK: user=${userId} sessionA=${sessionA} sessionB=${sessionB}`);
console.log(` write.memories=${written.memories} compact.analyzed=${compacted.analyzed} resolve.count=${texts.length}`);
} finally {
await cleanup(pool, userId, [sessionA, sessionB]);
await pool.end().catch(() => {});
}
}
main().catch((error) => {
console.error(`GOOSE_V149_MEMORY_LOOP_FAIL: ${error.message}`);
process.exit(1);
});