67220a14ea
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>
185 lines
6.1 KiB
JavaScript
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);
|
|
});
|