Update files
This commit is contained in:
@@ -55,6 +55,7 @@ import {
|
||||
} from "./connection-offer.js";
|
||||
import { printPairingQrIfEnabled } from "./pairing-qr.js";
|
||||
import { startRelayTransport, type RelayTransportController } from "./relay-transport.js";
|
||||
import { getOrCreateServerId } from "./server-id.js";
|
||||
import type {
|
||||
AgentClient,
|
||||
AgentProvider,
|
||||
@@ -104,7 +105,8 @@ export async function createPaseoDaemon(
|
||||
rootLogger: Logger
|
||||
): Promise<PaseoDaemon> {
|
||||
const logger = rootLogger.child({ module: "bootstrap" });
|
||||
const connectionSessionId = randomUUID();
|
||||
const serverId = getOrCreateServerId(config.paseoHome, { logger });
|
||||
const connectionSessionId = serverId;
|
||||
let relayTransport: RelayTransportController | null = null;
|
||||
|
||||
const staticDir = config.staticDir;
|
||||
@@ -416,6 +418,7 @@ export async function createPaseoDaemon(
|
||||
const wsServer = new VoiceAssistantWebSocketServer(
|
||||
httpServer,
|
||||
logger,
|
||||
serverId,
|
||||
agentManager,
|
||||
agentStorage,
|
||||
downloadTokenStore,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { execSync } from "node:child_process";
|
||||
import {
|
||||
createDaemonTestContext,
|
||||
type DaemonTestContext,
|
||||
DaemonClient,
|
||||
} from "./test-utils/index.js";
|
||||
import { getFullAccessConfig, getAskModeConfig } from "./daemon-e2e/agent-configs.js";
|
||||
import {
|
||||
@@ -88,6 +89,29 @@ describe("daemon client E2E", () => {
|
||||
expect(deleteResult.error).toBeTruthy();
|
||||
}, 30000);
|
||||
|
||||
test("emits server_info on websocket connect", async () => {
|
||||
const client = new DaemonClient({
|
||||
url: `ws://127.0.0.1:${ctx.daemon.port}/ws`,
|
||||
});
|
||||
|
||||
const infoPromise = waitForSignal<{ serverId: string }>(5000, (resolve) => {
|
||||
const unsubscribe = client.on("status", (message) => {
|
||||
if (message.type !== "status") return;
|
||||
const payload = message.payload as { status?: unknown; serverId?: unknown };
|
||||
if (payload.status !== "server_info") return;
|
||||
if (typeof payload.serverId !== "string" || payload.serverId.trim().length === 0) return;
|
||||
resolve({ serverId: payload.serverId.trim() });
|
||||
});
|
||||
return unsubscribe;
|
||||
});
|
||||
|
||||
await client.connect();
|
||||
const info = await infoPromise;
|
||||
expect(info.serverId.length).toBeGreaterThan(0);
|
||||
|
||||
await client.close();
|
||||
}, 15000);
|
||||
|
||||
test("matches request IDs for concurrent session requests", async () => {
|
||||
const firstRequestId = `list-${Date.now()}-a`;
|
||||
const secondRequestId = `list-${Date.now()}-b`;
|
||||
|
||||
@@ -101,13 +101,16 @@ describe("Relay transport (plaintext) - daemon E2E", () => {
|
||||
});
|
||||
|
||||
ws.on("message", (data) => {
|
||||
clearTimeout(timeout);
|
||||
try {
|
||||
resolve(JSON.parse(data.toString()));
|
||||
const parsed = JSON.parse(data.toString());
|
||||
if (parsed && typeof parsed === "object" && "type" in parsed && (parsed as any).type === "pong") {
|
||||
clearTimeout(timeout);
|
||||
resolve(parsed);
|
||||
ws.close();
|
||||
}
|
||||
} catch (err) {
|
||||
clearTimeout(timeout);
|
||||
reject(err);
|
||||
} finally {
|
||||
ws.close();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { mkdtempSync, rmSync, existsSync, readFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { getOrCreateServerId } from "./server-id.js";
|
||||
|
||||
function tmpHome(): string {
|
||||
return mkdtempSync(path.join(tmpdir(), "paseo-server-id-"));
|
||||
}
|
||||
|
||||
describe("getOrCreateServerId", () => {
|
||||
let home: string;
|
||||
const originalEnv = process.env;
|
||||
|
||||
beforeEach(() => {
|
||||
process.env = { ...originalEnv };
|
||||
delete process.env.PASEO_SERVER_ID;
|
||||
home = tmpHome();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalEnv;
|
||||
rmSync(home, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("creates and persists a stable id per PASEO_HOME", () => {
|
||||
const first = getOrCreateServerId(home);
|
||||
const second = getOrCreateServerId(home);
|
||||
expect(first).toBe(second);
|
||||
expect(first.startsWith("srv_")).toBe(true);
|
||||
|
||||
const idPath = path.join(home, "server-id");
|
||||
expect(existsSync(idPath)).toBe(true);
|
||||
expect(readFileSync(idPath, "utf8").trim()).toBe(first);
|
||||
});
|
||||
|
||||
it("respects and persists PASEO_SERVER_ID override", () => {
|
||||
process.env.PASEO_SERVER_ID = "test-daemon-id";
|
||||
const id = getOrCreateServerId(home);
|
||||
expect(id).toBe("test-daemon-id");
|
||||
|
||||
const idPath = path.join(home, "server-id");
|
||||
expect(existsSync(idPath)).toBe(true);
|
||||
expect(readFileSync(idPath, "utf8").trim()).toBe("test-daemon-id");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import path from "node:path";
|
||||
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { randomBytes } from "node:crypto";
|
||||
|
||||
type LoggerLike = {
|
||||
child(bindings: Record<string, unknown>): LoggerLike;
|
||||
info(...args: any[]): void;
|
||||
warn(...args: any[]): void;
|
||||
};
|
||||
|
||||
const SERVER_ID_FILENAME = "server-id";
|
||||
|
||||
function getLogger(logger: LoggerLike | undefined): LoggerLike | undefined {
|
||||
return logger?.child({ module: "server-id" });
|
||||
}
|
||||
|
||||
function getServerIdPath(paseoHome: string): string {
|
||||
return path.join(paseoHome, SERVER_ID_FILENAME);
|
||||
}
|
||||
|
||||
function generateServerId(): string {
|
||||
// 9 bytes -> 12 base64url chars; keep it short + URL-safe.
|
||||
const rand = randomBytes(9).toString("base64url");
|
||||
return `srv_${rand}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stable daemon identifier scoped to a given $PASEO_HOME.
|
||||
*
|
||||
* - Persisted to `$PASEO_HOME/server-id`
|
||||
* - Can be overridden via `PASEO_SERVER_ID` (useful for tests)
|
||||
*/
|
||||
export function getOrCreateServerId(
|
||||
paseoHome: string,
|
||||
options?: { env?: NodeJS.ProcessEnv; logger?: LoggerLike }
|
||||
): string {
|
||||
const env = options?.env ?? process.env;
|
||||
const log = getLogger(options?.logger);
|
||||
const serverIdPath = getServerIdPath(paseoHome);
|
||||
|
||||
const envOverride =
|
||||
typeof env.PASEO_SERVER_ID === "string" && env.PASEO_SERVER_ID.trim().length > 0
|
||||
? env.PASEO_SERVER_ID.trim()
|
||||
: null;
|
||||
|
||||
if (envOverride) {
|
||||
// Persist the override for consistent identity across restarts.
|
||||
if (!existsSync(serverIdPath)) {
|
||||
try {
|
||||
writeFileSync(serverIdPath, `${envOverride}\n`, "utf8");
|
||||
log?.info({ serverId: envOverride }, "Persisted PASEO_SERVER_ID override");
|
||||
} catch (error) {
|
||||
log?.warn({ error }, "Failed to persist PASEO_SERVER_ID override");
|
||||
}
|
||||
}
|
||||
return envOverride;
|
||||
}
|
||||
|
||||
if (existsSync(serverIdPath)) {
|
||||
try {
|
||||
const raw = readFileSync(serverIdPath, "utf8");
|
||||
const parsed = raw.trim();
|
||||
if (parsed.length > 0) {
|
||||
return parsed;
|
||||
}
|
||||
} catch (error) {
|
||||
log?.warn({ error }, "Failed to read server-id file, regenerating");
|
||||
}
|
||||
}
|
||||
|
||||
const created = generateServerId();
|
||||
try {
|
||||
writeFileSync(serverIdPath, `${created}\n`, "utf8");
|
||||
} catch (error) {
|
||||
log?.warn({ error }, "Failed to persist serverId (continuing with in-memory id)");
|
||||
}
|
||||
return created;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ export class VoiceAssistantWebSocketServer {
|
||||
constructor(
|
||||
server: HTTPServer,
|
||||
logger: pino.Logger,
|
||||
serverId: string,
|
||||
agentManager: AgentManager,
|
||||
agentStorage: AgentStorage,
|
||||
downloadTokenStore: DownloadTokenStore,
|
||||
@@ -51,6 +52,7 @@ export class VoiceAssistantWebSocketServer {
|
||||
this.logger = logger.child({ module: "websocket-server" });
|
||||
this.bridge = new WebSocketSessionBridge(
|
||||
this.logger,
|
||||
serverId,
|
||||
agentManager,
|
||||
agentStorage,
|
||||
downloadTokenStore,
|
||||
|
||||
@@ -26,6 +26,7 @@ export class WebSocketSessionBridge {
|
||||
private readonly logger: pino.Logger;
|
||||
private readonly sessions: Map<WebSocket, Session> = new Map();
|
||||
private clientIdCounter = 0;
|
||||
private readonly serverId: string;
|
||||
private readonly agentManager: AgentManager;
|
||||
private readonly agentStorage: AgentStorage;
|
||||
private readonly downloadTokenStore: DownloadTokenStore;
|
||||
@@ -48,6 +49,7 @@ export class WebSocketSessionBridge {
|
||||
|
||||
constructor(
|
||||
logger: pino.Logger,
|
||||
serverId: string,
|
||||
agentManager: AgentManager,
|
||||
agentStorage: AgentStorage,
|
||||
downloadTokenStore: DownloadTokenStore,
|
||||
@@ -65,6 +67,7 @@ export class WebSocketSessionBridge {
|
||||
}
|
||||
) {
|
||||
this.logger = logger.child({ module: "websocket-session-bridge" });
|
||||
this.serverId = serverId;
|
||||
this.agentManager = agentManager;
|
||||
this.agentStorage = agentStorage;
|
||||
this.downloadTokenStore = downloadTokenStore;
|
||||
@@ -121,6 +124,18 @@ export class WebSocketSessionBridge {
|
||||
|
||||
this.sessions.set(ws, session);
|
||||
|
||||
// Advertise stable server identity immediately on connect (used for URL/shareable IDs).
|
||||
this.sendToClient(
|
||||
ws,
|
||||
wrapSessionMessage({
|
||||
type: "status",
|
||||
payload: {
|
||||
status: "server_info",
|
||||
serverId: this.serverId,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
connectionLogger.info(
|
||||
{ clientId, totalSessions: this.sessions.size },
|
||||
"Client connected"
|
||||
|
||||
Reference in New Issue
Block a user