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