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>
270 lines
9.2 KiB
JavaScript
270 lines
9.2 KiB
JavaScript
#!/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);
|
|
});
|