#!/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'; 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); });