#!/usr/bin/env node import crypto from 'node:crypto'; function normalizeBaseUrl(value) { return String(value ?? '').trim().replace(/\/+$/, ''); } function requireValue(name, value) { if (!value) throw new Error(`${name} requires a value`); return value; } export function parseMemoryV2SessionFlowArgs(argv = [], env = process.env) { const options = { baseUrl: normalizeBaseUrl(env.MEMORY_V2_SESSION_FLOW_BASE_URL || 'http://127.0.0.1:8081'), prompt: env.MEMORY_V2_SESSION_FLOW_PROMPT || 'Please confirm Memory V2 session flow is working.', expectedBackend: null, expectedSelectedBackend: null, timeoutMs: Number(env.MEMORY_V2_SESSION_FLOW_TIMEOUT_MS || 45000), help: false, }; for (let i = 0; i < argv.length; i += 1) { const arg = argv[i]; if (arg === '--base-url') { options.baseUrl = normalizeBaseUrl(requireValue(arg, argv[++i])); } else if (arg === '--prompt') { options.prompt = requireValue(arg, argv[++i]); } else if (arg === '--expect-backend') { options.expectedBackend = requireValue(arg, argv[++i]); } else if (arg === '--expect-selected-backend') { options.expectedSelectedBackend = requireValue(arg, argv[++i]); } else if (arg === '--timeout-ms') { options.timeoutMs = Number(requireValue(arg, argv[++i])); } else if (arg === '-h' || arg === '--help') { options.help = true; } else { throw new Error(`Unknown argument: ${arg}`); } } if (!options.baseUrl) throw new Error('--base-url is required'); if (!options.prompt) throw new Error('--prompt is required'); if (!Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0) { throw new Error('--timeout-ms must be a positive number'); } return options; } function usage() { return `Usage: node scripts/check-memory-v2-session-flow.mjs [options] Options: --base-url Portal base URL. Defaults to MEMORY_V2_SESSION_FLOW_BASE_URL or http://127.0.0.1:8081. --prompt User prompt sent through the live session. --expect-backend Fail unless runtime memory.backend equals this value. --expect-selected-backend Fail unless runtime memory.selectedBackend equals this value. --timeout-ms Request timeout. Defaults to 45000. `; } function makeCheck(name, ok, details = {}) { return { name, ok: Boolean(ok), ...details }; } async function parseResponseBody(response) { const contentType = response.headers?.get?.('content-type') ?? ''; const text = await response.text(); if (contentType.includes('application/json')) { try { return { text, json: text ? JSON.parse(text) : null }; } catch { return { text, json: null }; } } try { return { text, json: text ? JSON.parse(text) : null }; } catch { return { text, json: null }; } } async function requestJson(fetchImpl, url, { method = 'GET', headers = {}, body, timeoutMs = 45000 } = {}) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); try { const response = await fetchImpl(url, { method, headers: { ...(body ? { 'Content-Type': 'application/json' } : {}), ...headers, }, body: body ? JSON.stringify(body) : undefined, signal: controller.signal, }); const payload = await parseResponseBody(response); return { ok: response.ok, status: response.status, ...payload, headers: response.headers }; } finally { clearTimeout(timer); } } function buildTempUser() { const suffix = `${Date.now()}${Math.floor(Math.random() * 1000)}`; return { username: `mv2_${suffix}`, password: 'MemoryV2-Session-2026', email: `mv2-${suffix}@example.test`, displayName: `mv2_${suffix}`, }; } async function waitForAgentRunCompletion(fetchImpl, baseUrl, cookie, timeoutMs, createRun) { const { runId, sessionId } = await createRun(); const deadline = Date.now() + timeoutMs; const seen = []; const response = await fetchImpl(`${baseUrl}/api/agent/runs/${encodeURIComponent(runId)}/events`, { headers: { Accept: 'text/event-stream', Cookie: cookie, }, }); if (response.ok && response.body) { const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; while (Date.now() < deadline) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const chunks = buffer.split('\n\n'); buffer = chunks.pop() ?? ''; for (const chunk of chunks) { const trimmed = chunk.trim(); if (!trimmed || trimmed.startsWith(':')) continue; seen.push(trimmed); if (trimmed.includes('"status":"failed"')) { await reader.cancel().catch(() => {}); return { runId, sessionId, seen, terminalStatus: 'failed' }; } if (trimmed.includes('"status":"succeeded"')) { await reader.cancel().catch(() => {}); return { runId, sessionId, seen, terminalStatus: 'succeeded' }; } } } await reader.cancel().catch(() => {}); } while (Date.now() < deadline) { const run = await requestJson(fetchImpl, `${baseUrl}/api/agent/runs/${encodeURIComponent(runId)}`, { headers: { Cookie: cookie }, timeoutMs: Math.min(10000, timeoutMs), }); const status = run.json?.run?.status ?? null; const resolvedSessionId = run.json?.run?.agent_session_id ?? sessionId; if (status === 'succeeded' || status === 'failed') { return { runId, sessionId: resolvedSessionId, seen, terminalStatus: status }; } await new Promise((resolve) => setTimeout(resolve, 1500)); } throw new Error(`agent run timeout after ${timeoutMs}ms`); } export { waitForAgentRunCompletion }; export async function runMemoryV2SessionFlowCli({ argv = process.argv.slice(2), env = process.env, stdout = process.stdout, fetchImpl = globalThis.fetch, } = {}) { const options = parseMemoryV2SessionFlowArgs(argv, env); if (options.help) { stdout.write(usage()); return 0; } if (typeof fetchImpl !== 'function') { throw new Error('fetch is not available in this Node.js runtime'); } const checks = []; const user = buildTempUser(); const register = await requestJson(fetchImpl, `${options.baseUrl}/auth/register`, { method: 'POST', body: user, timeoutMs: options.timeoutMs, }); checks.push(makeCheck('register_http_ok', register.ok, { status: register.status })); if (!register.ok) { const report = { ok: false, baseUrl: options.baseUrl, checks, error: register.json ?? register.text }; stdout.write(`${JSON.stringify(report, null, 2)}\n`); return 1; } const login = await requestJson(fetchImpl, `${options.baseUrl}/auth/login`, { method: 'POST', body: { username: user.username, password: user.password }, timeoutMs: options.timeoutMs, }); const cookie = login.headers?.get?.('set-cookie')?.split(';', 1)[0] ?? null; checks.push(makeCheck('login_http_ok', login.ok && Boolean(cookie), { status: login.status, hasCookie: Boolean(cookie), })); if (!login.ok || !cookie) { const report = { ok: false, baseUrl: options.baseUrl, checks, error: login.json ?? login.text }; stdout.write(`${JSON.stringify(report, null, 2)}\n`); return 1; } const started = await requestJson(fetchImpl, `${options.baseUrl}/api/agent/start`, { method: 'POST', body: {}, headers: { Cookie: cookie }, timeoutMs: options.timeoutMs, }); const sessionId = started.json?.id ?? null; checks.push(makeCheck('agent_start_ok', started.ok && Boolean(sessionId), { status: started.status, sessionId, })); if (!started.ok || !sessionId) { const report = { ok: false, baseUrl: options.baseUrl, checks, error: started.json ?? started.text }; stdout.write(`${JSON.stringify(report, null, 2)}\n`); return 1; } const requestId = crypto.randomUUID(); const { runId, sessionId: activeSessionId, seen, terminalStatus } = await waitForAgentRunCompletion( fetchImpl, options.baseUrl, cookie, options.timeoutMs, async () => { const created = await requestJson(fetchImpl, `${options.baseUrl}/api/agent/runs`, { method: 'POST', headers: { Cookie: cookie }, body: { session_id: sessionId, request_id: requestId, user_message: { role: 'user', created: Math.floor(Date.now() / 1000), content: [{ type: 'text', text: options.prompt }], metadata: { userVisible: true, agentVisible: true }, }, }, timeoutMs: options.timeoutMs, }); checks.push(makeCheck('agent_run_created', created.ok && created.status === 202, { status: created.status, runId: created.json?.run?.id ?? null, })); if (!created.ok || created.status !== 202 || !created.json?.run?.id) { throw new Error(`agent run failed: ${created.status} ${created.text}`); } return { runId: created.json.run.id, sessionId: created.json?.run?.agent_session_id ?? sessionId, }; }, ); checks.push(makeCheck('agent_run_terminal', terminalStatus === 'succeeded', { terminalStatus, eventCount: seen.length, })); const detail = await requestJson(fetchImpl, `${options.baseUrl}/api/sessions/${encodeURIComponent(activeSessionId)}`, { headers: { Cookie: cookie }, timeoutMs: options.timeoutMs, }); const userVisibleMessages = Array.isArray(detail.json?.conversation) ? detail.json.conversation.filter((message) => message?.metadata?.userVisible) : []; const assistantMessages = userVisibleMessages.filter((message) => message?.role === 'assistant'); checks.push(makeCheck('session_detail_assistant_reply', detail.ok && assistantMessages.length > 0, { status: detail.status, userVisibleCount: userVisibleMessages.length, assistantCount: assistantMessages.length, })); const remember = await requestJson(fetchImpl, `${options.baseUrl}/api/user-memory/v1/remember-recent`, { method: 'POST', headers: { Cookie: cookie }, body: { sessionId: activeSessionId }, timeoutMs: options.timeoutMs, }); checks.push(makeCheck('remember_recent_ok', remember.ok && remember.json?.ok === true, { status: remember.status, })); checks.push(makeCheck('remember_recent_extracted', remember.ok && Number(remember.json?.analyzed ?? 0) > 0, { status: remember.status, analyzed: remember.json?.analyzed ?? 0, memories: remember.json?.memories ?? 0, })); const sync = await requestJson(fetchImpl, `${options.baseUrl}/api/user-memory/v1/sync`, { method: 'POST', headers: { Cookie: cookie }, body: { sessionId: activeSessionId }, timeoutMs: options.timeoutMs, }); checks.push(makeCheck('sync_ok', sync.ok && sync.json?.ok === true, { status: sync.status, })); const runtime = await requestJson(fetchImpl, `${options.baseUrl}/api/runtime/status`, { timeoutMs: options.timeoutMs, }); const memory = runtime.json?.memory ?? null; if (options.expectedBackend) { checks.push(makeCheck('memory_backend', memory?.backend === options.expectedBackend, { expected: options.expectedBackend, actual: memory?.backend ?? null, })); } if (options.expectedSelectedBackend) { checks.push(makeCheck('memory_selected_backend', memory?.selectedBackend === options.expectedSelectedBackend, { expected: options.expectedSelectedBackend, actual: memory?.selectedBackend ?? null, })); } const report = { ok: checks.every((item) => item.ok), baseUrl: options.baseUrl, user: user.username, sessionId: activeSessionId, runId, summary: { assistantPreview: assistantMessages.at(-1)?.content?.[0]?.text?.slice?.(0, 200) ?? null, remember: remember.json, sync: sync.json, runtimeMemory: memory ? { enabled: memory.enabled, backend: memory.backend, selectedBackend: memory.selectedBackend, failOpen: memory.failOpen, vectorEnabled: memory.vectorEnabled, } : null, sessionEventsTail: seen.slice(-4), terminalStatus, }, checks, }; stdout.write(`${JSON.stringify(report, null, 2)}\n`); return report.ok ? 0 : 1; } if (import.meta.url === `file://${process.argv[1]}`) { runMemoryV2SessionFlowCli().then((code) => { process.exitCode = code; }).catch((err) => { console.error(err instanceof Error ? err.message : err); process.exitCode = 1; }); }