Files
memind/scripts/goose-v149-sse.mjs
T
john 716ef407fe feat(goose): add local v1.49 canary routing, smoke gates, and migration docs
Wire Portal and TKMind proxy to loopback Goose v1.49 via canary env blocks,
with verification scripts, Phase 2/3 evidence baselines, and rollback runbooks
so local upgrade stays isolated from stable 1.41 and production.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-08 21:10:20 +08:00

133 lines
4.1 KiB
JavaScript

/**
* Shared SSE helpers for local Goose v1.49 smoke scripts.
*/
import { randomUUID } from 'node:crypto';
import { Readable } from 'node:stream';
import { Agent, fetch } from 'undici';
const blockedHosts = new Set(['58.38.22.103', '120.26.184.105']);
export function createV149Client(options = {}) {
const host = options.host ?? process.env.GOOSE_V149_HOST ?? '127.0.0.1';
const port = options.port ?? process.env.GOOSE_V149_PORT ?? '18049';
const secret = options.secret ?? process.env.GOOSE_SERVER__SECRET_KEY ?? 'local-v149-dev-secret';
if (blockedHosts.has(host)) {
throw new Error(`Refusing production host ${host}`);
}
const base = `https://${host}:${port}`;
const dispatcher = new Agent({ connect: { rejectUnauthorized: false } });
async function apiFetch(pathname, init = {}) {
const headers = { ...(init.headers ?? {}), 'X-Secret-Key': secret };
if (init.body && !headers['Content-Type']) headers['Content-Type'] = 'application/json';
return fetch(`${base}${pathname}`, { ...init, headers, dispatcher });
}
async function apiJson(pathname, body, init = {}) {
const response = await apiFetch(pathname, {
method: init.method ?? 'POST',
body: body == null ? undefined : JSON.stringify(body),
...init,
});
const text = await response.text();
if (!response.ok) {
throw new Error(`${pathname} ${response.status}: ${text.slice(0, 800)}`);
}
return text.trim() ? JSON.parse(text) : {};
}
return { base, apiFetch, apiJson };
}
function parseSseData(frame) {
let data = '';
for (const line of frame.split('\n')) {
if (line.startsWith('data:')) data += line.slice(5).trim();
}
if (!data) return null;
try {
return JSON.parse(data);
} catch {
return null;
}
}
export async function collectReplyEvents({
client,
sessionId,
requestId = randomUUID(),
userMessage,
timeoutMs = Number(process.env.GOOSE_V149_REPLY_TIMEOUT_MS || 90_000),
triggerReply = true,
}) {
const events = [];
const eventsResponse = await client.apiFetch(`/sessions/${sessionId}/events`, {
method: 'GET',
headers: { Accept: 'text/event-stream' },
});
if (!eventsResponse.ok || !eventsResponse.body) {
const text = await eventsResponse.text().catch(() => '');
throw new Error(`events ${eventsResponse.status}: ${text.slice(0, 400)}`);
}
if (triggerReply) {
const replyResponse = await client.apiFetch(`/sessions/${sessionId}/reply`, {
method: 'POST',
body: JSON.stringify({
request_id: requestId,
user_message: userMessage,
}),
});
if (!replyResponse.ok) {
const text = await replyResponse.text().catch(() => '');
throw new Error(`reply ${replyResponse.status}: ${text.slice(0, 400)}`);
}
replyResponse.body?.cancel?.();
}
const reader = Readable.fromWeb(eventsResponse.body);
const decoder = new TextDecoder();
let buffer = '';
const deadline = Date.now() + timeoutMs;
let finishEvent = null;
let errorEvent = null;
for await (const chunk of reader) {
if (Date.now() > deadline) break;
buffer += decoder.decode(chunk, { stream: true });
const frames = buffer.split('\n\n');
buffer = frames.pop() ?? '';
for (const frame of frames) {
const event = parseSseData(frame);
if (!event) continue;
const routingId = event.chat_request_id ?? event.request_id;
if (routingId && routingId !== requestId) continue;
events.push(event);
if (event.type === 'Finish') finishEvent = event;
if (event.type === 'Error') errorEvent = event;
if (finishEvent || errorEvent) {
reader.cancel?.().catch(() => {});
break;
}
}
if (finishEvent || errorEvent) break;
}
return {
requestId,
events,
finishEvent,
errorEvent,
outcome: finishEvent ? 'finish' : errorEvent ? 'error' : 'timeout',
};
}
export function extractTokenStateFromEvents(events = []) {
for (let i = events.length - 1; i >= 0; i -= 1) {
const event = events[i];
if (event?.type === 'Finish' && event.token_state) return event.token_state;
if (event?.type === 'Message' && event.token_state) return event.token_state;
}
return null;
}