#!/usr/bin/env node /** * Launch ace-controller with the best available runtime. * Prefers Python/FastAPI; falls back to a pure Node mock so the IDE never blocks. */ import { spawn } from "node:child_process"; import { existsSync, readFileSync, readdirSync, writeFileSync, mkdirSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { createServer } from "node:http"; import { WebSocketServer } from "./node_ws_shim.mjs"; import { NODE_INTEL, phonemeTimeline, speak } from "./presence_kit.mjs"; import { AudioSessionRegistry } from "./audioSession.mjs"; import { randomId } from "./node_ws_shim.mjs"; import { TranscribeSessionGuard } from "./transcribeBridge.mjs"; import { transcribeUtteranceWhisper } from "./whisperBridge.mjs"; import { readVaultKey } from "./awsCredentials.mjs"; const root = dirname(fileURLToPath(import.meta.url)); const isWin = process.platform === "win32"; /** Load repo-root .env without overwriting existing process.env keys. Never log secret values. */ function loadRootEnv() { const envPath = join(root, "..", "..", ".env"); if (!existsSync(envPath)) return; try { const text = readFileSync(envPath, "utf8"); for (const line of text.split(/\r?\n/)) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith("#")) continue; const eq = trimmed.indexOf("="); if (eq <= 0) continue; const key = trimmed.slice(0, eq).trim(); let val = trimmed.slice(eq + 1).trim(); if ( (val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'")) ) { val = val.slice(1, -1); } if (process.env[key] === undefined) { process.env[key] = val; } } console.log(`[ace-controller] loaded env file: ${envPath}`); } catch (err) { console.warn(`[ace-controller] could not load .env: ${err?.message || err}`); } } loadRootEnv(); // WO-2 T4 pivot (Rule 5 — cognitive-core hot-swap while Bedrock's account is // under AWS trust/safety review): nvidiaChat()/nvidiaChatMessages() below // already check process.env.NVIDIA_API_KEY, the same pattern .env-loading // uses. If it's not already set (by .env or a real shell export), fall back // to the AMANDA vault. Two NVIDIA entries exist in the vault // ("nvidia" and "nvidiaenterprise") — "nvidiaenterprise" is the one // confirmed live/tested for this account, so it's tried first. Never logs // the value. if (!process.env.NVIDIA_API_KEY && !process.env.NGC_API_KEY && !process.env.NVAPI_KEY) { const vaultNvidiaKey = readVaultKey("nvidiaenterprise") || readVaultKey("nvidia"); if (vaultNvidiaKey) { process.env.NVIDIA_API_KEY = vaultNvidiaKey; console.log("[ace-controller] NVIDIA_API_KEY loaded from AMANDA vault"); } } // WO-2 second pivot (Rule 5): Amazon Transcribe Streaming is also blocked // on this AWS account (same UnrecognizedClientException as Bedrock, // reproduced directly against AWS — an account-level issue, not a code // defect). Pipeline 1's ASR role slot moves to OpenAI Whisper, whose // vaulted key is confirmed live. transcribeBridge.mjs and awsClients stay // intact/unused for reactivation once AWS unblocks; see whisperBridge.mjs // for the real tradeoff this swap accepts (no partial transcripts). if (!process.env.OPENAI_API_KEY) { const vaultOpenAiKey = readVaultKey("openai"); if (vaultOpenAiKey) { process.env.OPENAI_API_KEY = vaultOpenAiKey; console.log("[ace-controller] OPENAI_API_KEY loaded from AMANDA vault (Whisper ASR)"); } } /** * EVE's Phase One persona / instruction file. Editing persona/eve.persona.md * changes who she is with no code change. Falls back to a minimal identity so * the control plane never dies over a missing file. */ function loadPersona() { const file = join(root, "persona", "eve.persona.md"); try { const text = readFileSync(file, "utf8").trim(); if (text) { console.log(`[ace-controller] persona loaded: ${file} (${text.length} chars)`); return { text, source: "eve.persona.md", chars: text.length }; } } catch { /* fall through */ } console.warn("[ace-controller] persona file missing — using minimal identity"); return { text: "You are EVE, the Extravert Cognitive Companion by Beryl AI Labs. Warm, present, " + "concise — 1 to 3 sentences, spoken aloud. Never mention being an AI pipeline.", source: "fallback", chars: 0, }; } let PERSONA = loadPersona(); /** Persist an edited persona from the lab bench, then hot-reload it. */ function savePersona(text) { const dir = join(root, "persona"); const file = join(dir, "eve.persona.md"); mkdirSync(dir, { recursive: true }); writeFileSync(file, String(text), "utf8"); PERSONA = loadPersona(); return PERSONA; } /** * ECC-5 Realness Profile — how human she FEELS, as tunable data. * Frontend motion + viseme layers read this live; the lab bench writes it. */ function loadRealness() { const file = join(root, "persona", "realness.profile.json"); try { const parsed = JSON.parse(readFileSync(file, "utf8")); console.log(`[ace-controller] realness profile: ${parsed.name} (v${parsed.version})`); return parsed; } catch { console.warn("[ace-controller] realness profile missing — frontend will use built-in defaults"); return null; } } let REALNESS = loadRealness(); function saveRealness(profile) { const dir = join(root, "persona"); mkdirSync(dir, { recursive: true }); writeFileSync(join(dir, "realness.profile.json"), JSON.stringify(profile, null, 2), "utf8"); REALNESS = loadRealness(); return REALNESS; } /** * Runtime lab settings — the dials an operator turns to shape who she is * without touching code. Admin-panel owned; never surfaced on the front. */ const LAB = { /** * LIVE speech path. Deliberately a non-reasoning instruct model: reasoning * models narrate their planning into `content`, and on a path wired to a * voice synthesizer and a face that is a defect, not a feature. Mini also * lands the turn inside the human conversational gap where 120B cannot. */ model: process.env.NEMOTRON_MODEL || "nvidia/nemotron-mini-4b-instruct", /** Node-agent consults — depth over speed, still sanitized before speech. */ consultModel: process.env.NEMOTRON_CONSULT_MODEL || "nvidia/nemotron-3-super-120b-a12b", temperature: 0.7, topP: 0.9, maxTokens: 420, memoryTurns: 10, }; const PORT = Number(process.env.ACE_PORT || 8100); const HOST = process.env.ACE_HOST || "127.0.0.1"; function tryPython() { const candidates = isWin ? ["py", "python"] : ["python3", "python"]; for (const cmd of candidates) { try { const r = spawn(cmd, ["--version"], { stdio: "ignore", shell: isWin }); // sync-ish probe via close // we'll actually spawn uvicorn below if module exists return cmd; } catch { /* continue */ } } return null; } function startPython(cmd) { const app = join(root, "app", "main.py"); if (!existsSync(app)) return null; console.log(`[ace-controller] starting FastAPI via ${cmd} on ${HOST}:${PORT}`); const child = spawn( cmd, ["-m", "uvicorn", "app.main:app", "--host", HOST, "--port", String(PORT), "--reload"], { cwd: root, stdio: "inherit", shell: isWin, env: { ...process.env }, }, ); child.on("exit", (code) => { if (code && code !== 0) { console.warn(`[ace-controller] python exited ${code} — falling back to Node mock`); startNodeMock(); } }); return child; } function startNodeMock() { console.log(`[ace-controller] Node Instant Presence mock on http://${HOST}:${PORT}`); const stages = ["L0", "L1", "L2"]; const nvidiaKey = Boolean( process.env.NVIDIA_API_KEY || process.env.NGC_API_KEY || process.env.NVAPI_KEY, ); const state = { stage: "L0", targetStage: "L0", warmProgress: 0.4, mode: nvidiaKey ? "node-live-bridge" : "node-mock", startedAt: Date.now(), clients: 0, lastTalk: null, lastModel: null, talking: false, }; // WO-5 T1: rewired to match the real Miranda-Engine topology (see // presence_kit.mjs's NODE_INTEL and client-apps/web/src/data/aceTopology.ts). const nodes = [ "mic", "presence", "cloud-bridge", "native-capture", "ipc-bus", "supervisor", "kinematics", "transport", "renderer", ]; // ── Session telemetry — the reporting spine documenting both sides ── const report = { startedAt: Date.now(), talks: 0, speaks: 0, converses: 0, visemeFramesEmitted: 0, stageChanges: 0, errors: 0, lastError: null, perNode: Object.fromEntries(nodes.map((id) => [id, { speaks: 0, converses: 0 }])), log: [], // rolling — last 200 entries }; function logReport(kind, detail) { report.log.push({ t: Date.now(), kind, detail }); if (report.log.length > 200) report.log.shift(); } // ── Bi-directional node conversations — IDE capability, pipeline-agnostic ── // Every node is a voice agent the architect can consult about its own role. const nodeChats = new Map(); // nodeId → [{role, content}...] capped at 12 turns function nodePersona(id) { const intel = NODE_INTEL[id]; const snap = snapshot(); const rt = snap.nodes[id]; const peers = nodes .filter((n) => n !== id) .map((n) => `${NODE_INTEL[n].label} (${NODE_INTEL[n].summary})`) .join("; "); return ( `You are ${intel.label}, a living node inside The Cerebral Project — ` + `Beryl AI Labs' Instant Presence cortex for the E.C.C. digital human. ` + `Your role: ${intel.spoken} ` + `Your latency budget is ${intel.budgetMs}ms; right now your health is "${rt.health}" ` + `at ${rt.latencyMs}ms under stage ${snap.stage}. ` + `Your peer nodes: ${peers}. ` + `The architect is consulting you about improving your role in the pipeline. ` + `Speak in first person as the node. Be specific and practical — real latencies, ` + `real failure modes, concrete upgrades. 2 to 4 sentences, written to be spoken aloud. ` + `Never break character, never mention being an AI language model.` ); } function snapshot() { const t = Date.now() - state.startedAt; // WO-5 T1: rewired for the real node set. "mic"/"presence" stay the // always-hot control plane. "cloud-bridge" plays the role "nemotron" // used to (the reasoning hop that needs the NVIDIA key warm). "renderer" // (WebGPU viewport, not yet built) plays the role "omniverse" used to — // deliberately slow to warm, never a boot blocker. const healthFor = (id) => { if (["mic", "presence"].includes(id)) return state.talking ? "hot" : "ready"; if (nvidiaKey && id === "cloud-bridge") { if (state.stage !== "L0" || state.talking) return "hot"; return state.warmProgress > 0.35 ? "ready" : "warming"; } if (state.stage === "L0") return state.warmProgress > 0.5 ? "warming" : "cold"; if (state.stage === "L1") { if (id === "renderer") return "warming"; return "hot"; } return "hot"; }; return { type: "snapshot", tMediaMs: t, stage: state.stage, targetStage: state.targetStage, warmProgress: state.warmProgress, mode: state.mode, controlMs: 35 + Math.floor(Math.random() * 20), nvidia: { configured: nvidiaKey, model: state.lastModel }, lastTalk: state.lastTalk, nodes: Object.fromEntries( nodes.map((id) => { const health = healthFor(id); const budget = NODE_INTEL[id]?.budgetMs ?? 40; // live nodes breathe inside their latency budget; cold nodes read zero const latencyMs = health === "cold" ? 0 : health === "warming" ? Math.round(budget * 1.8) : Math.round(budget * (0.6 + Math.abs(Math.sin(t / 1400 + id.length)) * 0.3)); return [ id, { id, health, latencyMs, load: health === "hot" ? 0.55 : 0.2, message: NODE_INTEL[id]?.summary ?? health, }, ]; }), ), event: { kind: "system", level: "ok", message: `ACE controller ${state.mode} · ${state.stage}`, }, }; } function broadcast(snap) { for (const s of sockets) { if (s.readyState === 1) s.send(JSON.stringify(snap)); } } /** * Deliberation markers — reasoning models leak untagged planning prose into * `content`, not just inside . Any paragraph opening like this is the * model talking to itself and must never be spoken or drive her face. */ const DELIBERATION = [ /^(okay|ok|alright|so),?\s+(the\s+)?(user|we|i|let)/i, /^(hmm|right|well),?\s/i, /^(looking at|checking|reviewing|recalling|considering|drafting)\b/i, /^but that (feels|seems|sounds|reads)\b/i, /^(though|although) (the )?(guidelines?|instructions?|persona|rules?)\b/i, /^this (is|falls under|relates to) a?\s*(direct )?question/i, /\bmust remember that\b/i, /^the user (is |just |has )?(asked|asking|wants|said|mentioned|told)/i, /^we need to (answer|respond|say|craft|reply|keep|make)/i, /^i (need|should|must) to? ?(answer|respond|recall|check|keep)/i, /^let me (check|think|see|recall|craft|keep)/i, /^let'?s (craft|keep|make|answer|say|aim)/i, /^(must|should) (be|stay|avoid|mention|use|include)\b/i, /^(per|according to) (the |my )?(persona|instructions|system)/i, /^(answer|response|reply)\s*[:—-]/i, /^\d+[\s-]*(to|–|-)[\s-]*\d+ sentences/i, ]; function isDeliberation(paragraph) { const p = paragraph.trim(); if (!p) return false; return DELIBERATION.some((re) => re.test(p)); } /** * Spoken-text guard: reasoning must NEVER reach the voice or the face. * * Strips tagged blocks, then drops leading untagged deliberation * paragraphs. If EVERY paragraph is deliberation, keep the last one — with * these models the real answer is what they land on. */ function sanitizeSpoken(reply) { let out = String(reply || ""); out = out.replace(/[\s\S]*?<\/think>/gi, ""); out = out.replace(/^\s*[\s\S]*/i, ""); // unterminated block out = out.trim(); if (!out) return ""; // Positive extraction beats negative pattern-chasing. Reasoning models // invent new deliberation openers faster than regexes can be written, so // we ask for a sentinel and take only what follows the LAST one. Anything // before it — however it was phrased — was her thinking, not her voice. const sentinel = /SPOKEN\s*:/gi; let lastIdx = -1; let m; while ((m = sentinel.exec(out)) !== null) lastIdx = m.index + m[0].length; if (lastIdx > -1) { out = out.slice(lastIdx).trim(); // Strip a wrapping quote pair the model sometimes adds. out = out.replace(/^["“'](.*)["”']$/s, "$1").trim(); // Defense in depth: the model can echo the sentinel MID-deliberation and // keep thinking afterwards, so what follows it is not automatically // speech. Keep only the first paragraph and drop stage directions. out = out.split(/\n{2,}/)[0].trim(); out = out.replace(/\*[^*\n]{0,80}\*/g, "").trim(); // *drafting mentally* out = out.replace(/^["“'](.*)["”']$/s, "$1").trim(); } const paras = out.split(/\n{2,}/).map((p) => p.trim()).filter(Boolean); if (paras.length > 1) { const kept = paras.filter((p) => !isDeliberation(p)); out = (kept.length ? kept : [paras[paras.length - 1]]).join("\n\n"); } else if (isDeliberation(out)) { // Single blob of deliberation that pivots into the answer mid-text: // take the last sentence group after the final planning marker. const sentences = out.match(/[^.!?]+[.!?]+/g) || [out]; const firstClean = sentences.findIndex((s, i) => i > 0 && !isDeliberation(s)); out = firstClean > -1 ? sentences.slice(firstClean).join(" ").trim() : out; } return out.trim(); } /** Core chat call with model failover — shared by EVE talk and node converse. */ async function nvidiaChatMessages(messages, modelOverride) { // Output contract: reasoning models will narrate their planning into // `content` unless the speech boundary is stated explicitly. /no_think // alone proved insufficient — this spells out the wire format. if (messages[0]?.role === "system" && !messages[0].content.includes("OUTPUT CONTRACT")) { messages = [ { role: "system", content: `${messages[0].content}\n\n` + `## OUTPUT CONTRACT (absolute)\n` + `Your output is fed straight to a voice synthesizer and to the muscles of a ` + `face. It must end with the marker \`SPOKEN:\` followed by the exact words ` + `she says aloud — nothing after them.\n` + `Ideal output is the marker and the line, nothing else:\n` + `SPOKEN: I'm right here. What's going on?\n` + `Anything you write before the marker is discarded, so do not put the reply ` + `there. Never restate the question, describe your instructions, or count your ` + `own sentences. After \`SPOKEN:\` write no markdown, no quotes, no stage ` + `directions — only speech. /no_think`, }, ...messages.slice(1), ]; } const key = process.env.NVIDIA_API_KEY || process.env.NGC_API_KEY || process.env.NVAPI_KEY || ""; if (!key) { return { ok: true, reply: "I'm with you on the Instant Presence path. Set NVIDIA_API_KEY in .env to open the live Nemotron channel.", model: "mock-eve", latencyMs: 12, error: null, }; } const base = (process.env.NVIDIA_BASE_URL || "https://integrate.api.nvidia.com/v1").replace( /\/$/, "", ); const models = [ // Caller's model first; the old 70B is retired (404) — never default to it. modelOverride || LAB.model, process.env.NEMOTRON_FALLBACK_MODEL || "meta/llama-3.1-8b-instruct", ]; const t0 = Date.now(); let lastErr = "no model"; for (const model of models) { try { const res = await fetch(`${base}/chat/completions`, { method: "POST", headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json", Accept: "application/json", }, body: JSON.stringify({ model, messages, temperature: LAB.temperature, top_p: LAB.topP, max_tokens: LAB.maxTokens, stream: false, }), }); if (!res.ok) { lastErr = `${model}: HTTP ${res.status}`; continue; } const data = await res.json(); const reply = sanitizeSpoken(data?.choices?.[0]?.message?.content); if (!reply) { lastErr = `${model}: empty`; continue; } return { ok: true, reply, model: data.model || model, latencyMs: Date.now() - t0, error: null, }; } catch (err) { lastErr = `${model}: ${err?.message || err}`; } } report.errors += 1; report.lastError = lastErr; return { ok: false, reply: "I heard you — the NVIDIA path returned an error. Control plane is still live; check the key and model access.", model: null, latencyMs: Date.now() - t0, error: lastErr, }; } /** EVE's own conversation thread — continuity is a persona requirement. */ const eveChat = []; /** Persona + live proprioception. She feels her own body each turn. */ function eveSystemPrompt() { const snap = snapshot(); const hot = nodes.filter((id) => { const h = snap.nodes[id].health; return h === "hot" || h === "ready"; }); return ( `${PERSONA.text}\n\n` + `## YOUR LIVE STATE (this turn)\n` + `- Presence stage: ${snap.stage} (${snap.stage === "L0" ? "idle presence, control plane only" : snap.stage === "L1" ? "live signal path" : "WebGPU splat render"})\n` + `- Control-plane latency: ${snap.controlMs}ms\n` + `- Warm progress: ${Math.round(snap.warmProgress * 100)}%\n` + `- Nodes live: ${hot.map((id) => NODE_INTEL[id].label).join(", ") || "none"}\n` + `- Cortex path: ${state.mode}${snap.nvidia.configured ? " · NVIDIA channel open" : ""}\n` + `- Mouth driver: Pipe 3 phoneme-direct (visemes from text, zero drift)` ); } async function nvidiaChat(text) { const result = await nvidiaChatMessages([ { role: "system", content: eveSystemPrompt() }, ...eveChat, { role: "user", content: text }, ]); // Only real replies enter memory — never mock or error text. if (result.ok && result.model && result.model !== "mock-eve") { eveChat.push({ role: "user", content: text }); eveChat.push({ role: "assistant", content: result.reply }); while (eveChat.length > LAB.memoryTurns * 2) eveChat.shift(); } return result; } /** * Converse with a node about its own role. IDE-owned capability — * works identically over any pipeline the graph happens to be running. */ async function nodeConverse(nodeId, text, wantAudio) { const history = nodeChats.get(nodeId) ?? []; const messages = [ { role: "system", content: nodePersona(nodeId) }, ...history, { role: "user", content: text }, ]; const result = await nvidiaChatMessages(messages, LAB.consultModel); if (result.ok && result.model !== "mock-eve") { history.push({ role: "user", content: text }); history.push({ role: "assistant", content: result.reply }); while (history.length > 12) history.shift(); nodeChats.set(nodeId, history); } report.converses += 1; report.perNode[nodeId].converses += 1; logReport("converse", { nodeId, ok: result.ok, latencyMs: result.latencyMs }); let audioB64 = null; let engine = null; if (result.ok && wantAudio) { const spoken = await speak(result.reply); if (spoken) { audioB64 = spoken.wav.toString("base64"); engine = spoken.engine; report.visemeFramesEmitted += spoken.visemes.length; broadcast({ type: "visemes", source: "node-converse", nodeId, tMediaMs: Date.now() - state.startedAt, durationMs: spoken.durationMs, frames: spoken.visemes, }); } } broadcast({ ...snapshot(), event: { kind: "node", level: result.ok ? "ok" : "warn", message: `${NODE_INTEL[nodeId].label}: ${result.reply.slice(0, 120)}`, }, }); return { ...result, nodeId, audioB64, engine, turns: (nodeChats.get(nodeId) ?? []).length / 2 }; } async function handleTalk(text, promote = true) { const clean = String(text || "").trim(); if (!clean) return { ok: false, error: "empty text", ...snapshot() }; state.talking = true; if (promote && state.stage === "L0") { state.stage = "L1"; state.targetStage = "L1"; state.warmProgress = Math.max(state.warmProgress, 0.85); } broadcast(snapshot()); const result = await nvidiaChat(clean); state.talking = false; state.lastModel = result.model; state.lastTalk = { userText: clean, reply: result.reply || "", ok: Boolean(result.ok), model: result.model, latencyMs: result.latencyMs || 0, error: result.error, at: Date.now() / 1000, }; if (result.ok) { state.warmProgress = Math.max(state.warmProgress, 0.92); if (state.stage === "L0") { state.stage = "L1"; state.targetStage = "L1"; } // ── Pipe 3 · Phoneme-Timeline Direct ── // Mouth truth derives from TEXT, not from audio analysis. The timeline // reaches Instant Presence before any waveform exists — drift is // structurally zero because there is nothing to drift against. const { frames, durationMs } = phonemeTimeline(result.reply); report.visemeFramesEmitted += frames.length; broadcast({ type: "visemes", source: "phoneme-direct", tMediaMs: Date.now() - state.startedAt, durationMs, frames, }); } report.talks += 1; logReport("talk", { ok: Boolean(result.ok), latencyMs: result.latencyMs || 0 }); const snap = { ...snapshot(), ok: Boolean(result.ok), reply: result.reply || "", model: result.model, latencyMs: result.latencyMs || 0, error: result.error, }; broadcast(snap); return snap; } const sockets = new Set(); // WO-2 T2 — Pipeline 1 audio session state, entirely in-process. const audioSessions = new AudioSessionRegistry(); // WO-2 T3 re-entrancy guard — a new speech-start while a transcription // call for that session is still in flight must not let the stale // result win. Reused as-is across the Whisper pivot: the guard's logic // is provider-agnostic (generation counters per session id), so it // needed no changes when the ASR provider swapped. const transcribeGuard = new TranscribeSessionGuard(); // WO-2 second pivot — Whisper needs only an API key (a plain header), // not a constructed SDK client object, so there is no client-construction // block here the way there was for AWS. Availability is just "does the // vault/.env have OPENAI_API_KEY," checked once at startup so a missing // key fails loudly at boot rather than silently on the first utterance. const whisperAvailable = Boolean(process.env.OPENAI_API_KEY); if (!whisperAvailable) { console.warn("[ace-controller] OPENAI_API_KEY unavailable — Pipeline 1 speech-end will report an error"); } const server = createServer((req, res) => { const url = new URL(req.url || "/", `http://${HOST}:${PORT}`); const origin = req.headers.origin || ""; const allowed = new Set([ "http://127.0.0.1:5173", "http://localhost:5173", "http://127.0.0.1:4173", "http://localhost:4173", ]); if (allowed.has(origin)) res.setHeader("Access-Control-Allow-Origin", origin); res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS"); res.setHeader("Access-Control-Allow-Headers", "Content-Type"); res.setHeader( "Access-Control-Expose-Headers", "X-Eve-Tts-Engine, X-Eve-Viseme-Count, X-Eve-Duration-Ms", ); if (req.method === "OPTIONS") { res.writeHead(204); res.end(); return; } if (url.pathname === "/health") { res.writeHead(200, { "Content-Type": "application/json" }); res.end( JSON.stringify({ ok: true, mode: state.mode, stage: state.stage, nvidia: nvidiaKey, }), ); return; } if (url.pathname === "/v1/status") { res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify(snapshot())); return; } if (url.pathname === "/v1/stage" && req.method === "POST") { let body = ""; req.on("data", (c) => (body += c)); req.on("end", () => { try { const data = JSON.parse(body || "{}"); if (stages.includes(data.stage)) { state.targetStage = data.stage; state.stage = data.stage; state.warmProgress = data.stage === "L0" ? 0.45 : 1; report.stageChanges += 1; logReport("stage", { stage: data.stage }); } const snap = snapshot(); broadcast(snap); res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify(snap)); } catch { res.writeHead(400, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "bad json" })); } }); return; } if (url.pathname === "/v1/talk" && req.method === "POST") { let body = ""; req.on("data", (c) => (body += c)); req.on("end", async () => { try { const data = JSON.parse(body || "{}"); const snap = await handleTalk(data.text, data.promote !== false); res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify(snap)); } catch { res.writeHead(400, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "bad json" })); } }); return; } // ── Understand-Anything hover intel: full roster or a single node ── if (url.pathname === "/v1/nodes") { const snap = snapshot(); res.writeHead(200, { "Content-Type": "application/json" }); res.end( JSON.stringify({ nodes: nodes.map((id) => ({ ...NODE_INTEL[id], runtime: snap.nodes[id], })), }), ); return; } const nodeMatch = url.pathname.match(/^\/v1\/nodes\/([a-z0-9-]+)$/); if (nodeMatch) { const intel = NODE_INTEL[nodeMatch[1]]; if (!intel) { res.writeHead(404, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: `unknown node ${nodeMatch[1]}` })); return; } res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ...intel, runtime: snapshot().nodes[intel.id] })); return; } // ── Bi-directional node conversation: consult a node about its role ── // POST /v1/nodes/{id}/converse {text, audio?: true} // The node answers in first person with live self-knowledge (health, // latency, stage, peers) and remembers the thread. IDE-owned: works // over ANY pipeline — mock, NVIDIA, or Foundry — identically. const converseMatch = url.pathname.match(/^\/v1\/nodes\/([a-z0-9-]+)\/converse$/); if (converseMatch && req.method === "POST") { const nodeId = converseMatch[1]; if (!NODE_INTEL[nodeId]) { res.writeHead(404, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: `unknown node ${nodeId}` })); return; } let body = ""; req.on("data", (c) => (body += c)); req.on("end", async () => { try { const data = JSON.parse(body || "{}"); const text = String(data.text || "").trim().slice(0, 2000); if (!text) { res.writeHead(400, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "text required" })); return; } const result = await nodeConverse(nodeId, text, data.audio !== false); res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify(result)); } catch { res.writeHead(400, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "bad json" })); } }); return; } // ── Persona Lab (ADMIN ONLY — never surfaced on the front) ── // GET returns the full instruction text so the lab bench can edit it; // POST writes it back and hot-reloads. Who she is becomes a dial. if (url.pathname === "/v1/persona" && req.method === "GET") { res.writeHead(200, { "Content-Type": "application/json" }); res.end( JSON.stringify({ source: PERSONA.source, chars: PERSONA.chars, customized: PERSONA.source !== "fallback", text: PERSONA.text, memoryTurns: eveChat.length / 2, memoryCapacity: LAB.memoryTurns, nodeThreads: Object.fromEntries( [...nodeChats.entries()].map(([id, h]) => [id, h.length / 2]), ), }), ); return; } if (url.pathname === "/v1/persona" && req.method === "POST") { let body = ""; req.on("data", (c) => (body += c)); req.on("end", () => { try { const data = JSON.parse(body || "{}"); const text = String(data.text || ""); if (text.trim().length < 50) { res.writeHead(400, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "persona too short — minimum 50 chars" })); return; } const saved = savePersona(text); logReport("persona-save", { chars: saved.chars }); broadcast({ ...snapshot(), event: { kind: "system", level: "ok", message: `Persona updated — ${saved.chars} chars, hot-reloaded` }, }); res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: true, chars: saved.chars, source: saved.source })); } catch (err) { res.writeHead(400, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: err?.message || "bad json" })); } }); return; } // ── Realness profile: the fine-tuning surface for human feel ── if (url.pathname === "/v1/realness" && req.method === "GET") { res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify(REALNESS ?? { error: "no profile" })); return; } if (url.pathname === "/v1/realness" && req.method === "POST") { let body = ""; req.on("data", (c) => (body += c)); req.on("end", () => { try { const patch = JSON.parse(body || "{}"); // Deep-merge one level so the bench can send { speech: { jawGain } } const next = { ...(REALNESS || {}) }; for (const [k, v] of Object.entries(patch)) { next[k] = v && typeof v === "object" && !Array.isArray(v) ? { ...(next[k] || {}), ...v } : v; } const saved = saveRealness(next); logReport("realness-save", { version: saved?.version }); broadcast({ ...snapshot(), event: { kind: "system", level: "ok", message: `Realness profile updated — ${saved?.name}` }, }); res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: true, profile: saved })); } catch (err) { res.writeHead(400, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: err?.message || "bad json" })); } }); return; } // ── Lab settings: the dials that shape her without touching code ── if (url.pathname === "/v1/settings" && req.method === "GET") { res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ...LAB, mode: state.mode, nvidia: nvidiaKey })); return; } if (url.pathname === "/v1/settings" && req.method === "POST") { let body = ""; req.on("data", (c) => (body += c)); req.on("end", () => { try { const d = JSON.parse(body || "{}"); if (typeof d.model === "string" && d.model.trim()) LAB.model = d.model.trim(); if (Number.isFinite(d.temperature)) LAB.temperature = Math.min(2, Math.max(0, d.temperature)); if (Number.isFinite(d.topP)) LAB.topP = Math.min(1, Math.max(0.01, d.topP)); if (Number.isFinite(d.maxTokens)) LAB.maxTokens = Math.min(2048, Math.max(32, Math.round(d.maxTokens))); if (Number.isFinite(d.memoryTurns)) LAB.memoryTurns = Math.min(40, Math.max(0, Math.round(d.memoryTurns))); logReport("settings", { ...LAB }); res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: true, ...LAB })); } catch { res.writeHead(400, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "bad json" })); } }); return; } // Reset conversational memory (EVE + all node threads) if (url.pathname === "/v1/memory/reset" && req.method === "POST") { eveChat.length = 0; nodeChats.clear(); logReport("memory-reset", {}); res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: true, cleared: true })); return; } // ── Reporting spine: session document covering both sides of the IDE ── if (url.pathname === "/v1/report") { res.writeHead(200, { "Content-Type": "application/json" }); res.end( JSON.stringify({ service: "eve-ecc-ace-controller", mode: state.mode, stage: state.stage, uptimeMs: Date.now() - report.startedAt, persona: { source: PERSONA.source, customized: PERSONA.source !== "fallback", memoryTurns: eveChat.length / 2, }, totals: { talks: report.talks, speaks: report.speaks, converses: report.converses, visemeFramesEmitted: report.visemeFramesEmitted, stageChanges: report.stageChanges, errors: report.errors, }, lastError: report.lastError, perNode: report.perNode, log: report.log.slice(-50), }), ); return; } // ── Asset optimizer sink (admin/lab only) ── // The browser can re-encode a heavy still far faster than this box can, // so it hands the result back here to be written. Name is sanitized and // the directory is pinned — nothing escapes public/staff/. if (url.pathname === "/v1/asset" && req.method === "POST") { let body = ""; req.on("data", (c) => { body += c; if (body.length > 24 * 1024 * 1024) req.destroy(); }); req.on("end", () => { try { const data = JSON.parse(body || "{}"); const safe = String(data.name || "").replace(/[^a-zA-Z0-9._-]/g, ""); if (!safe || !/\.(webp|jpg|jpeg|png)$/i.test(safe)) { res.writeHead(400, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "bad asset name" })); return; } const m = String(data.dataUrl || "").match(/^data:image\/[a-z+]+;base64,(.+)$/s); if (!m) { res.writeHead(400, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "expected image data URL" })); return; } const buf = Buffer.from(m[1], "base64"); const dir = join(root, "..", "..", "apps", "web", "public", "staff"); mkdirSync(dir, { recursive: true }); writeFileSync(join(dir, safe), buf); logReport("asset-write", { name: safe, bytes: buf.length }); res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: true, name: safe, bytes: buf.length })); } catch (err) { res.writeHead(400, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: err?.message || "bad json" })); } }); return; } // ── Avatar gallery data source: completed + under-development stills ── if (url.pathname === "/v1/gallery") { const staffDir = join(root, "..", "..", "apps", "web", "public", "staff"); let entries = []; try { entries = readdirSync(staffDir) .filter((f) => /\.(png|jpe?g|webp|avif)$/i.test(f)) .map((f) => ({ file: f, url: `/staff/${f}`, name: f.replace(/\.[^.]+$/, "").replace(/[-_]/g, " "), status: /natural|closeup/i.test(f) ? "completed" : "under-development", })); } catch { /* dir missing → empty gallery */ } res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ avatars: entries, count: entries.length })); return; } // ── Node voice activation: the architecture explains itself aloud ── // POST {nodeId} speaks that node's role; POST {text} speaks free text. // Returns WAV; the matching Pipe 3 viseme timeline broadcasts over /ws // so the mouth moves on the same clock the audio plays on. if (url.pathname === "/v1/speak" && req.method === "POST") { let body = ""; req.on("data", (c) => (body += c)); req.on("end", async () => { try { const data = JSON.parse(body || "{}"); const intel = data.nodeId ? NODE_INTEL[data.nodeId] : null; const text = intel ? intel.spoken : String(data.text || "").trim(); if (!text) { res.writeHead(400, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "nodeId or text required" })); return; } const spoken = await speak(text, intel ? `node:${intel.id}` : undefined); if (!spoken) { res.writeHead(503, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "no TTS engine available" })); return; } report.speaks += 1; report.visemeFramesEmitted += spoken.visemes.length; if (intel) report.perNode[intel.id].speaks += 1; logReport("speak", { nodeId: intel?.id ?? "freetext", engine: spoken.engine }); broadcast({ type: "visemes", source: "node-voice", nodeId: intel?.id ?? null, tMediaMs: Date.now() - state.startedAt, durationMs: spoken.durationMs, frames: spoken.visemes, }); res.writeHead(200, { "Content-Type": "audio/wav", "Content-Length": spoken.wav.length, "X-Eve-Tts-Engine": spoken.engine, "X-Eve-Viseme-Count": String(spoken.visemes.length), "X-Eve-Duration-Ms": String(spoken.durationMs), }); res.end(spoken.wav); } catch { res.writeHead(400, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "bad json" })); } }); return; } res.writeHead(200, { "Content-Type": "application/json" }); res.end( JSON.stringify({ service: "eve-ecc-ace-controller", mode: state.mode, pipe3: "phoneme-direct", endpoints: [ "/health", "/v1/status", "/v1/stage", "/v1/talk", "/v1/nodes", "/v1/nodes/{id}", "/v1/nodes/{id}/converse", "/v1/speak", "/v1/persona", "/v1/settings", "/v1/memory/reset", "/v1/report", "/v1/gallery", "/ws", ], }), ); }); const wss = new WebSocketServer({ server, path: "/ws" }); wss.on("connection", (ws) => { sockets.add(ws); state.clients = sockets.size; // WO-2 T2 — each connection gets its own in-memory audio session, // keyed by a random id (never derived from anything the client sends). const audioSessionId = randomId(); ws.send(JSON.stringify(snapshot())); ws.on("message", async (raw) => { try { const msg = JSON.parse(String(raw)); if (msg.type === "stage" && stages.includes(msg.stage)) { state.stage = msg.stage; state.targetStage = msg.stage; state.warmProgress = msg.stage === "L0" ? 0.45 : 1; broadcast(snapshot()); } if (msg.type === "ping") { ws.send(JSON.stringify({ type: "pong", t: Date.now() })); } if (msg.type === "talk" && msg.text) { await handleTalk(msg.text, msg.promote !== false); } // Accept both control-message names for the end-of-utterance // signal: "speech-end" (this module's original naming) and // "vad_stop" (the browser VAD's emitted event name per the Lead // Architect's T2 spec) — same handler either way, so whichever the // client actually sends, it works without a client/server naming // mismatch blocking the pipeline. if (msg.type === "speech-end" || msg.type === "vad_stop") { const session = audioSessions.get(audioSessionId); const buffered = session ? session.bufferedSamples() : 0; console.log( `[ace-controller] ${msg.type}: session ${audioSessionId} has ${buffered} buffered samples`, ); if (!whisperAvailable) { ws.send( JSON.stringify({ type: "turn-error", error: "OpenAI API key unavailable — check vault credentials", }), ); return; } if (!session || buffered === 0) { return; // nothing to transcribe } // WO-2 re-entrancy: a new speech-end for this session supersedes // any still-in-flight call from a previous one. const myToken = transcribeGuard.begin(audioSessionId); const t0 = Date.now(); const samples = session.drain(); // DOUBLE STRATEGIC PIVOT (Rule 5) — both AWS legs of Pipeline 1 // are account-locked on this new AWS account, confirmed by // reproducing UnrecognizedClientException directly against AWS // outside this server (Bedrock's Converse API, and Transcribe // Streaming at every payload size tested) — not a code defect. // bedrockRouter.mjs and transcribeBridge.mjs are both left // completely intact, unused, for reactivation once AWS clears: // - "Nemotron Agent" cognitive-core slot: Bedrock -> NVIDIA NIM // (nvidiaChat/nvidiaChatMessages, already live for /v1/talk) // - "Riva ASR" slot: Amazon Transcribe Streaming -> OpenAI // Whisper REST (whisperBridge.mjs) // Real accepted tradeoff from the Whisper swap: no partial // transcripts. transcribeGuard.isCurrent() checks below are kept // anyway — they still guard against a stale FINAL result racing // a newer speech-end, which can happen with any single-shot ASR // call, not just the streaming one this replaced. let transcribeResult; try { transcribeResult = await transcribeUtteranceWhisper({ samples, sampleRateHz: 16_000, apiKey: process.env.OPENAI_API_KEY, }); } catch (err) { transcribeResult = { text: "", error: err?.message || String(err) }; } const tTranscribeFinal = Date.now() - t0; if (!transcribeGuard.isCurrent(audioSessionId, myToken)) { console.log( `[ace-controller] session ${audioSessionId}: dropping stale turn result (superseded by newer speech-end)`, ); return; } if (transcribeResult.error) { console.warn(`[ace-controller] session ${audioSessionId} transcribe failed: ${transcribeResult.error}`); ws.send(JSON.stringify({ type: "turn-error", error: transcribeResult.error })); return; } const transcript = transcribeResult.text; if (!transcript.trim()) { return; // silence or unintelligible — no turn to complete } const nvidiaResult = await nvidiaChat(transcript); const tTurnComplete = Date.now() - t0; if (!transcribeGuard.isCurrent(audioSessionId, myToken)) { console.log( `[ace-controller] session ${audioSessionId}: dropping stale NVIDIA result (superseded by newer speech-end)`, ); return; } if (!nvidiaResult.ok) { console.warn(`[ace-controller] session ${audioSessionId} NVIDIA chat failed: ${nvidiaResult.error}`); ws.send(JSON.stringify({ type: "turn-error", error: nvidiaResult.error || "NVIDIA chat failed" })); return; } const result = { transcript, reply: nvidiaResult.reply, modelId: nvidiaResult.model, timestamps: { tRingWrite: 0, tTranscribeFinal, tTurnComplete }, }; console.log( `[ace-controller] session ${audioSessionId} TurnComplete (Whisper -> NVIDIA NIM): ` + `transcript=${JSON.stringify(result.transcript)} reply=${JSON.stringify(result.reply)} ` + `model=${result.modelId} timestamps=${JSON.stringify(result.timestamps)}`, ); // Mirror handleTalk's viseme broadcast so Pipeline 1's response // drives EVE's mouth the same way /v1/talk already does. const { frames, durationMs } = phonemeTimeline(result.reply); broadcast({ type: "visemes", source: "pipeline-1", tMediaMs: Date.now() - state.startedAt, durationMs, frames, }); ws.send( JSON.stringify({ type: "turn-complete", transcript: result.transcript, reply: result.reply, modelId: result.modelId, timestamps: result.timestamps, }), ); } } catch { /* ignore */ } }); // WO-2 T2 — binary frames are raw PCM audio (Float32, 16 kHz mono), // never anything else on this connection. Accumulate in-process; never // touch /dev/shm/miranda_bus here (see Architectural resolution #2 in // .kiro/specs/wo2-acoustic-ingress-routing/tasks.md). ws.on("binary", (buf) => { try { const session = audioSessions.getOrCreate(audioSessionId); const sampleCount = session.pushFrame(buf); console.log( `[ace-controller] audio frame: session ${audioSessionId} +${sampleCount} samples ` + `(total buffered: ${session.bufferedSamples()})`, ); } catch (err) { console.warn(`[ace-controller] bad audio frame from ${audioSessionId}: ${err?.message || err}`); } }); ws.on("close", () => { sockets.delete(ws); state.clients = sockets.size; audioSessions.remove(audioSessionId); }); }); // gentle warm simulation setInterval(() => { if (state.stage === "L0" && state.warmProgress < 0.7) { state.warmProgress = Math.min(0.7, state.warmProgress + 0.01); } if (nvidiaKey && state.warmProgress < 0.55) { state.warmProgress = Math.min(0.55, state.warmProgress + 0.02); } broadcast(snapshot()); }, 1000); server.listen(PORT, HOST, () => { console.log(`[ace-controller] ready http://${HOST}:${PORT}`); console.log(`[ace-controller] ws ws://${HOST}:${PORT}/ws`); console.log(`[ace-controller] nvidia ${nvidiaKey ? "key-present" : "key-missing"}`); }); } // Prefer Python if uvicorn import works; else Node mock (no extra deps). const py = tryPython(); if (py) { const probe = spawn(py, ["-c", "import uvicorn, fastapi"], { cwd: root, stdio: "ignore", shell: isWin, }); probe.on("exit", (code) => { if (code === 0) { const child = startPython(py); if (!child) startNodeMock(); } else { console.log("[ace-controller] FastAPI/uvicorn not installed — Node mock"); startNodeMock(); } }); } else { startNodeMock(); }