diff --git a/docs/fixtures/weksa-provider-advertisement.json b/docs/fixtures/weksa-provider-advertisement.json index 20907af..f7ec25b 100644 --- a/docs/fixtures/weksa-provider-advertisement.json +++ b/docs/fixtures/weksa-provider-advertisement.json @@ -240,7 +240,8 @@ "id": "weksa.command.speech_provider.mimo.voicedesign", "owner": "Weksa", "acceptedByFixture": true, - "compatibilityEndpoint": "POST /speech-provider/mimo/voicedesign", + "commandIngress": "cultnet.transport.rudp.v0", + "inputSchema": "weksa.mimo_voicedesign_command.v0", "allowedInputs": [ "Persona/agent state file path", "weksa.interlingua_packet.v0-compatible object", diff --git a/docs/verse-service-contract.md b/docs/verse-service-contract.md index db28355..e84bed3 100644 --- a/docs/verse-service-contract.md +++ b/docs/verse-service-contract.md @@ -22,6 +22,7 @@ Outputs: - `weksa.pronunciation_plan.v0` - `weksa.utterance_handoff.v0` - `weksa.utterance_embedding_handoff.v0.1` +- `weksa.mimo_voicedesign_command.v0` - `weksa.mimo_tts_request.v0` - `weksa.mimo_voicedesign_receipt.v0` - Eve/CultUI surfaces for intent review, pronunciation inspection, and handoff @@ -45,19 +46,21 @@ and trace. index used by Idunn health checks and operator inspection. - `.weksa/eve-surfaces.cc`: current Weksa operator Eve surface witness. - `.weksa/command-boundary.cc`: provider-owned command and lifecycle boundary - naming Weksa as owner, Idunn as lifecycle requester, and the remaining MiMo - VoiceDesign HTTP route as compatibility command ingress. + naming Weksa as owner, Idunn as lifecycle requester, and the MiMo + VoiceDesign RUDP command ingress as daemon-owned transport authority. - `.weksa/transport-profile.cc`: target/current transport profile. Health is published through `cultnet.transport.rudp.v0`; provider state is carried by - the CultMesh store; the MiMo VoiceDesign command route remains compatibility - HTTP debt until a CultNet/RUDP command document ingress replaces it. + the CultMesh store; MiMo VoiceDesign command ingress now arrives through + `cultnet.transport.rudp.v0` instead of an HTTP route. - `.weksa/cultmesh-publications.cc`: publication index naming current Weksa - witness surfaces and compatibility routes. + witness surfaces and RUDP/CultMesh routes. - `.weksa/intent/{intentId}.cc`: conversational intent documents. - `.weksa/pronunciation/{utteranceId}.cc`: pronunciation plans and phonetic event sequences. - `.weksa/handoff/{utteranceId}.cc`: utterance handoffs and embedding handoff requests for AquaSynth. +- `.weksa/speech-provider/mimo/{requestId}/command.cc`: daemon-owned RUDP + command witness for MiMo VoiceDesign ingress. - `.weksa/speech-provider/mimo/{requestId}/interlingua.cc`: accepted or provisionally decomposed interlingua used for a MiMo verbalization command. - `.weksa/speech-provider/mimo/{requestId}/mimo-request.cc`: Weksa-owned MiMo @@ -78,11 +81,10 @@ The local Starfire daemon is launched by `scripts/start-weksa-daemon.ps1`, checked by `scripts/health-weksa-daemon.cmd`, and restarted by `scripts/restart-weksa-daemon.cmd`. Its daemon-owned health path publishes `weksa.cultnet-rudp-provider-health` to Idunn over -`cultnet.transport.rudp.v0` after each serialized witness refresh. Its -compatibility HTTP surface still listens on `http://127.0.0.1:8813` with -`/health`, `/provider-advertisement`, `/operator-state`, `/eve/operator`, -`/cultmesh/publications`, and `POST /speech-provider/mimo/voicedesign`, but -those endpoints are operator/debug projections, not daemon liveness owners. +`cultnet.transport.rudp.v0` after each serialized witness refresh, and MiMo +VoiceDesign commands arrive on `rudp://127.0.0.1:8813` as +`weksa.mimo_voicedesign_command.v0` documents. Health and operator truth live +in `.weksa/*.cc` witnesses; the HTTP daemon surface is deleted. ## CultMesh Verses @@ -106,8 +108,8 @@ Odin ingests the Starfire Weksa provider through `weksa` in the `starfire-local` swarm profile and owns restart authority through the Weksa restart script. Odin should read Weksa's provider advertisement, operator state, command boundary, transport profile, and Eve surface from the -typed provider store; HTTP endpoints are debug/operator lowerings and do not own -provider truth. +typed provider store and RUDP command boundary; no HTTP endpoint owns provider +truth. ## Eve Surfaces @@ -132,7 +134,8 @@ provider truth. - `speech_provider.mimo.voicedesign`: accept a Persona/agent state file plus either a Weksa interlingua packet or raw thought text. Raw thought is first decomposed into provisional interlingua, then lowered into a MiMo VoiceDesign - request and rendered through MiMo. + request and rendered through MiMo. The daemon accepts this command as a + `weksa.mimo_voicedesign_command.v0` document over `cultnet.transport.rudp.v0`. - `provider.advertise`: publish `gamecult.eve.provider_advertisement.v1`. ## Forbidden Writers diff --git a/scripts/restart-weksa-daemon.ps1 b/scripts/restart-weksa-daemon.ps1 index 17ff850..eb0692e 100644 --- a/scripts/restart-weksa-daemon.ps1 +++ b/scripts/restart-weksa-daemon.ps1 @@ -5,7 +5,8 @@ param( [string] $MiMoApiKeyPath = "E:\Projects\gamecult-ops\mimo-api-Weksa.txt", [string] $IdunnRudpHealth = "127.0.0.1:17870", [string] $IdunnDaemon = "weksa", - [string] $IdunnHealthContract = "weksa.cultnet-rudp-provider-health" + [string] $IdunnHealthContract = "weksa.cultnet-rudp-provider-health", + [string] $OdinCultMeshRudp = "127.0.0.1:17871" ) $ErrorActionPreference = "Stop" @@ -21,5 +22,5 @@ if (Test-Path -LiteralPath $pidPath) { Remove-Item -LiteralPath $pidPath -Force -ErrorAction SilentlyContinue } -& powershell.exe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $repoRoot "scripts\start-weksa-daemon.ps1") -Port $Port -HostName $HostName -StateRoot $StateRoot -MiMoApiKeyPath $MiMoApiKeyPath -IdunnRudpHealth $IdunnRudpHealth -IdunnDaemon $IdunnDaemon -IdunnHealthContract $IdunnHealthContract +& powershell.exe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $repoRoot "scripts\start-weksa-daemon.ps1") -Port $Port -HostName $HostName -StateRoot $StateRoot -MiMoApiKeyPath $MiMoApiKeyPath -IdunnRudpHealth $IdunnRudpHealth -IdunnDaemon $IdunnDaemon -IdunnHealthContract $IdunnHealthContract -OdinCultMeshRudp $OdinCultMeshRudp exit $LASTEXITCODE diff --git a/scripts/start-weksa-daemon.ps1 b/scripts/start-weksa-daemon.ps1 index b5ff2d8..40fa188 100644 --- a/scripts/start-weksa-daemon.ps1 +++ b/scripts/start-weksa-daemon.ps1 @@ -5,7 +5,8 @@ param( [string] $MiMoApiKeyPath = "E:\Projects\gamecult-ops\mimo-api-Weksa.txt", [string] $IdunnRudpHealth = "127.0.0.1:17870", [string] $IdunnDaemon = "weksa", - [string] $IdunnHealthContract = "weksa.cultnet-rudp-provider-health" + [string] $IdunnHealthContract = "weksa.cultnet-rudp-provider-health", + [string] $OdinCultMeshRudp = "127.0.0.1:17871" ) $ErrorActionPreference = "Stop" @@ -49,6 +50,10 @@ $arguments = @( "--idunn-health-contract", $IdunnHealthContract ) +if (-not [string]::IsNullOrWhiteSpace($OdinCultMeshRudp)) { + $arguments += @("--odin-cultmesh-rudp", $OdinCultMeshRudp) +} + $process = Start-Process -FilePath "node" ` -ArgumentList $arguments ` -WorkingDirectory $repoRoot ` @@ -73,5 +78,6 @@ if ($LASTEXITCODE -ne 0) { } Write-Host "Started Weksa daemon as PID $($process.Id)." -Write-Host " health: http://$HostName`:$Port/health" +Write-Host " command: rudp://$HostName`:$Port" +Write-Host " Odin CultMesh/RUDP: $OdinCultMeshRudp" Write-Host " state: $StateRoot" diff --git a/scripts/weksa-daemon.mjs b/scripts/weksa-daemon.mjs index 9ecb5d2..8377b21 100644 --- a/scripts/weksa-daemon.mjs +++ b/scripts/weksa-daemon.mjs @@ -1,7 +1,6 @@ #!/usr/bin/env node -import http from "node:http"; import Module from "node:module"; -import { existsSync, readFileSync, readdirSync } from "node:fs"; +import { existsSync, readFileSync, readdirSync, statSync } from "node:fs"; import { mkdir, readdir, rm, writeFile } from "node:fs/promises"; import { basename, delimiter, dirname, extname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; @@ -10,10 +9,16 @@ const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); configureCultLibModulePath(); const require = Module.createRequire(import.meta.url); const { - CULTNET_RUDP_PROTOCOL_ID, createIdunnRudpHealthPublisher, publishIdunnRudpHealth, } = require("./idunn-rudp-health.cjs"); +const { + CULTNET_RUDP_PROTOCOL_ID, + WEKSA_MIMO_VOICEDESIGN_COMMAND_SCHEMA_ID, + createWeksaRudpCommandServer, + sendWeksaMimoVoiceDesignCommand, + formatRudpEndpoint, +} = require("./weksa-rudp-command.cjs"); const options = parseArgs(process.argv.slice(2)); const port = Number(options.port ?? process.env.WEKSA_DAEMON_PORT ?? 8813); const host = options.host ?? process.env.WEKSA_DAEMON_HOST ?? "127.0.0.1"; @@ -27,6 +32,7 @@ const idunnHealthPublisher = createIdunnRudpHealthPublisher({ daemonId: options.idunnDaemon ?? process.env.WEKSA_IDUNN_DAEMON ?? "weksa", healthContract: options.idunnHealthContract ?? process.env.WEKSA_IDUNN_HEALTH_CONTRACT ?? "weksa.cultnet-rudp-provider-health", }); +const odinCultMeshRudp = options.odinCultMeshRudp ?? process.env.WEKSA_ODIN_CULTMESH_RUDP; const startedAt = new Date().toISOString(); let tick = 0; let lastPublishedAt = ""; @@ -39,56 +45,38 @@ if (options.health) { await runHealthCheck(); process.exit(0); } +if (options.mimoCommandJson) { + const command = JSON.parse(readFileSync(resolve(repoRoot, options.mimoCommandJson), "utf8")); + const receipt = await sendWeksaMimoVoiceDesignCommand({ + endpoint: formatRudpEndpoint(host, port), + command, + runtimeId: "weksa-cli", + localHost: host.includes(":") ? "::" : "0.0.0.0", + }); + process.stdout.write(`${JSON.stringify(receipt, null, 2)}\n`); + process.exit(0); +} await mkdir(stateRoot, { recursive: true }); await writeFile(pidPath, `${process.pid}\n`, "ascii"); await appendLog(`started pid=${process.pid} host=${host} port=${port}`); -await refreshWitnesses("startup"); - -const server = http.createServer(async (request, response) => { - try { - const url = new URL(request.url ?? "/", `http://${request.headers.host ?? `${host}:${port}`}`); - const snapshot = await currentSnapshot(); - if (url.pathname === "/health") { - return sendJson(response, 200, { - ok: true, - service: "weksa.intent.service", - providerId: "weksa.intent.service", - state: "healthy", - startedAt, - lastPublishedAt, - tick, - }); - } - if (url.pathname === "/provider-advertisement") { - return sendJson(response, 200, snapshot.providerAdvertisement); - } - if (url.pathname === "/operator-state") { - return sendJson(response, 200, snapshot.operatorState); - } - if (url.pathname === "/eve/operator") { - return sendJson(response, 200, snapshot.operatorSurface); - } - if (url.pathname === "/cultmesh/publications") { - return sendJson(response, 200, snapshot.cultMeshPublications); - } - if (url.pathname === "/speech-provider/mimo/voicedesign" && request.method === "POST") { - const body = await readJsonBody(request); - const result = await handleMimoVoiceDesign(body); - await refreshWitnesses("mimo-voicedesign-command"); - return sendJson(response, 200, result); - } - sendJson(response, 404, { ok: false, error: "unknown endpoint" }); - } catch (error) { +const startupSnapshot = await refreshWitnesses("startup"); +await publishOdinStartupRespect(startupSnapshot); +const commandServer = await createWeksaRudpCommandServer({ + host, + port, + runtimeId: "weksa-daemon", + onCommand: async (command) => { + const receipt = await handleMimoVoiceDesign(command); + await refreshWitnesses("mimo-voicedesign-command"); + return receipt; + }, + onError: async (error) => { lastError = String(error?.stack ?? error); - await appendLog(`request error: ${lastError}`); - sendJson(response, error?.statusCode ?? 500, { ok: false, error: String(error?.message ?? error) }); - } -}); - -server.listen(port, host, () => { - appendLog(`listening http://${host}:${port}`).catch(() => {}); + await appendLog(`rudp command error: ${lastError}`); + }, }); +await appendLog(`listening ${commandServer.uri}`); const interval = setInterval(() => { refreshWitnesses("interval").catch(async (error) => { @@ -104,32 +92,65 @@ process.on("SIGTERM", () => shutdown(0)); async function shutdown(code) { clearInterval(interval); await appendLog(`stopping pid=${process.pid}`); - server.close(() => process.exit(code)); + commandServer.close(); + process.exit(code); } async function runHealthCheck() { - const healthUrl = `http://${host}:${port}/health`; - await new Promise((resolvePromise, rejectPromise) => { - const request = http.get(healthUrl, (response) => { - let body = ""; - response.setEncoding("utf8"); - response.on("data", (chunk) => { - body += chunk; - }); - response.on("end", () => { - if (response.statusCode === 200) { - process.stdout.write(body); - resolvePromise(); - } else { - rejectPromise(new Error(`health returned ${response.statusCode}: ${body}`)); - } - }); - }); - request.on("error", rejectPromise); - request.setTimeout(5_000, () => { - request.destroy(new Error(`health timed out: ${healthUrl}`)); - }); - }); + const operatorStatePath = resolve(stateRoot, "operator-state.cc"); + const commandBoundaryPath = resolve(stateRoot, "command-boundary.cc"); + const transportProfilePath = resolve(stateRoot, "transport-profile.cc"); + if (!existsSync(pidPath)) { + throw new Error(`Weksa daemon PID file is missing: ${pidPath}`); + } + if (!existsSync(operatorStatePath)) { + throw new Error(`Weksa operator-state witness is missing: ${operatorStatePath}`); + } + if (!existsSync(commandBoundaryPath)) { + throw new Error(`Weksa command-boundary witness is missing: ${commandBoundaryPath}`); + } + if (!existsSync(transportProfilePath)) { + throw new Error(`Weksa transport-profile witness is missing: ${transportProfilePath}`); + } + const pidText = readFileSync(pidPath, "utf8").trim(); + if (!/^\d+$/.test(pidText)) { + throw new Error(`Weksa daemon PID file is invalid: ${pidText}`); + } + const pid = Number(pidText); + try { + process.kill(pid, 0); + } catch { + throw new Error(`Weksa daemon PID ${pid} is not running.`); + } + const operatorStateText = readFileSync(operatorStatePath, "utf8"); + const commandBoundaryText = readFileSync(commandBoundaryPath, "utf8"); + const transportProfileText = readFileSync(transportProfilePath, "utf8"); + const ages = [operatorStatePath, commandBoundaryPath, transportProfilePath] + .map((path) => Math.floor((Date.now() - statSync(path).mtimeMs) / 1000)); + const ageSeconds = Math.max(...ages); + if (ageSeconds > 180) { + throw new Error(`Weksa daemon witnesses are stale (${ageSeconds}s old at oldest required path).`); + } + if (!operatorStateText.includes("command_ingress: cultnet_rudp_document_mimo_voicedesign")) { + throw new Error("Weksa operator-state witness does not report RUDP command ingress."); + } + if (!commandBoundaryText.includes(`ingress: ${CULTNET_RUDP_PROTOCOL_ID}`)) { + throw new Error("Weksa command-boundary witness does not report CultNet/RUDP ingress."); + } + if (!commandBoundaryText.includes(`input_schema: ${WEKSA_MIMO_VOICEDESIGN_COMMAND_SCHEMA_ID}`)) { + throw new Error("Weksa command-boundary witness does not report the MiMo command document schema."); + } + if (!transportProfileText.includes(`command_transport: ${CULTNET_RUDP_PROTOCOL_ID}`)) { + throw new Error("Weksa transport-profile witness does not report CultNet/RUDP command transport."); + } + process.stdout.write(`${JSON.stringify({ + ok: true, + service: "weksa.intent.service", + state: "healthy", + pid, + ageSeconds, + commandEndpoint: formatRudpEndpoint(host, port), + }, null, 2)}\n`); } async function publishWitnesses() { @@ -192,6 +213,33 @@ async function publishWeksaRudpHealth(state, detail, observedAt = new Date().toI } } +async function publishOdinStartupRespect(snapshot) { + if (!odinCultMeshRudp || !snapshot?.providerAdvertisement) return; + const runtime = loadCultRuntime(); + if (runtime.error) { + await appendLog(`Odin startup respect skipped: ${runtime.error.message}`); + return; + } + try { + const { providerAdvertisementDefinition } = defineCultMeshDocuments(runtime.defineDocumentType); + await runtime.CultMesh.publishRudpDocumentOnce( + idunnHealthPublisher?.daemonId ?? "weksa", + 0x0d1d0002, + normalizeRudpEndpoint(odinCultMeshRudp), + runtime.defineCultNetDocumentBinding({ definition: providerAdvertisementDefinition }), + snapshot.providerAdvertisement.providerId, + snapshot.providerAdvertisement, + { + sourceRole: "weksa.provider", + tags: ["startup-respect", "odin-verse-discovery"], + }, + ); + await appendLog(`Odin startup respect published to ${odinCultMeshRudp}`); + } catch (error) { + await appendLog(`Odin startup respect failed: ${String(error?.message ?? error)}`); + } +} + async function buildSnapshot() { const updatedAt = new Date().toISOString(); const providerAdvertisement = loadProviderAdvertisement(updatedAt); @@ -222,7 +270,7 @@ async function buildSnapshot() { cultural_ontology_profiles: cultureCatalog.profile_count, target_language_profiles: cultureCatalog.target_language_profile_count, accepts_commands: true, - command_ingress: "compatibility_http_mimo_voicedesign_only", + command_ingress: "cultnet_rudp_document_mimo_voicedesign", mimo_voicedesign: existsSync(mimoApiKeyPath) ? "api_key_file_seen" : "missing_api_key_file", }, latest_affect_tuning: tuning, @@ -236,18 +284,17 @@ async function buildSnapshot() { updated_at: updatedAt, transport: { cultmesh_store: ".weksa/provider-advertisement-store.cc", - native_cultnet_peer: idunnHealthPublisher ? `${CULTNET_RUDP_PROTOCOL_ID}:idunn-health` : "pending", + native_cultnet_peer: formatRudpEndpoint(host, port), idunn_rudp_health: idunnHealthPublisher ? `${idunnHealthPublisher.endpoint.host}:${idunnHealthPublisher.endpoint.port}` : "unconfigured", - compatibility_http: `http://${host}:${port}`, }, publications: [ - publication("weksa.service/provider-advertisement", "gamecult.eve.provider_advertisement.v1", ".weksa/provider-advertisement-store.cc", "/provider-advertisement"), - publication("weksa.operator/status", "weksa.operator_state.v0", ".weksa/operator-state.cc", "/operator-state"), - publication("weksa.eve.surface.operator", "gamecult.eve.surface.v1", ".weksa/eve-surfaces.cc", "/eve/operator"), + publication("weksa.service/provider-advertisement", "gamecult.eve.provider_advertisement.v1", ".weksa/provider-advertisement-store.cc", ".weksa/provider-advertisement-store.cc"), + publication("weksa.operator/status", "weksa.operator_state.v0", ".weksa/operator-state.cc", ".weksa/operator-state.cc"), + publication("weksa.eve.surface.operator", "gamecult.eve.surface.v1", ".weksa/eve-surfaces.cc", ".weksa/eve-surfaces.cc"), publication("weksa.command-boundary", "weksa.command_boundary.v1", ".weksa/command-boundary.cc", null), publication("weksa.transport-profile", "weksa.transport_profile.v1", ".weksa/transport-profile.cc", null), - publication("weksa.cultmesh/publications", "weksa.cultmesh_publications.v0", ".weksa/cultmesh-publications.cc", "/cultmesh/publications"), - publication("weksa.speech-provider/mimo/voicedesign", "weksa.mimo_tts_request.v0", ".weksa/speech-provider/mimo/{requestId}.cc", "/speech-provider/mimo/voicedesign"), + publication("weksa.cultmesh/publications", "weksa.cultmesh_publications.v0", ".weksa/cultmesh-publications.cc", ".weksa/cultmesh-publications.cc"), + publication("weksa.speech-provider/mimo/voicedesign", WEKSA_MIMO_VOICEDESIGN_COMMAND_SCHEMA_ID, ".weksa/speech-provider/mimo/{requestId}/command.cc", formatRudpEndpoint(host, port)), ], }; return { providerAdvertisement, operatorState, operatorSurface, commandBoundary, transportProfile, cultMeshPublications }; @@ -291,11 +338,11 @@ async function writeCultMeshStore(snapshot) { cultMeshAddress: "asgard.starfire.weksa/eve/operator", endpoints: [ { transport: "cultmesh-store", address: ".weksa/provider-advertisement-store.cc" }, - { transport: "http", address: `http://${host}:${port}/eve/operator` }, + { transport: CULTNET_RUDP_PROTOCOL_ID, address: formatRudpEndpoint(host, port) }, ], routes: [ { transport: "cultmesh-store", address: ".weksa/provider-advertisement-store.cc" }, - { transport: "compatibility-http", address: `http://${host}:${port}` }, + { transport: CULTNET_RUDP_PROTOCOL_ID, address: formatRudpEndpoint(host, port) }, ], }, surface: snapshot.operatorSurface, @@ -328,12 +375,21 @@ function loadCultRuntime() { configureCultLibModulePath(); const { CultMesh } = require("cultmesh-ts"); const { defineDocumentType } = require("cultcache-ts"); - return { CultMesh, defineDocumentType, error: null }; + const { defineCultNetDocumentBinding } = require("cultnet-ts"); + return { CultMesh, defineDocumentType, defineCultNetDocumentBinding, error: null }; } catch (error) { - return { CultMesh: null, defineDocumentType: null, error }; + return { CultMesh: null, defineDocumentType: null, defineCultNetDocumentBinding: null, error }; } } +function normalizeRudpEndpoint(endpoint) { + const text = String(endpoint || "").trim(); + if (!text) { + throw new Error("Odin CultMesh/RUDP endpoint must be non-empty."); + } + return text.toLowerCase().startsWith("rudp://") ? text : `rudp://${text}`; +} + function configureCultLibModulePath() { process.env.NODE_PATH = [ resolve(repoRoot, "..", "CultLib", "packages"), @@ -433,18 +489,15 @@ function loadProviderAdvertisement(updatedAt) { }, endpoints: [ { transport: "cultmesh-store", address: ".weksa/provider-advertisement-store.cc" }, - { transport: "http", address: `http://${host}:${port}` }, + { transport: CULTNET_RUDP_PROTOCOL_ID, address: formatRudpEndpoint(host, port) }, ], routes: [ { transport: "cultmesh-store", address: ".weksa/provider-advertisement-store.cc" }, - { transport: "compatibility-http", address: `http://${host}:${port}` }, - ], - compatibilityRoutes: [ - { kind: "http", url: `http://${host}:${port}`, lossy: true }, + { transport: CULTNET_RUDP_PROTOCOL_ID, address: formatRudpEndpoint(host, port) }, ], health: { state: "healthy", - endpoint: idunnHealthPublisher ? `${CULTNET_RUDP_PROTOCOL_ID}:idunn-health` : `http://${host}:${port}/health`, + endpoint: idunnHealthPublisher ? `${CULTNET_RUDP_PROTOCOL_ID}:idunn-health` : "unconfigured", witness: ".weksa/operator-state.cc", checkedBy: "idunn.desired_daemon.v1", }, @@ -470,22 +523,22 @@ function buildCommandBoundary(updatedAt) { commands: [ { command: "speech_provider.mimo.voicedesign", - ingress: "compatibility-http", - endpoint: `http://${host}:${port}/speech-provider/mimo/voicedesign`, + ingress: CULTNET_RUDP_PROTOCOL_ID, + endpoint: formatRudpEndpoint(host, port), owner: "Weksa daemon", - input_schema: "weksa.mimo_tts_request.v0", + input_schema: WEKSA_MIMO_VOICEDESIGN_COMMAND_SCHEMA_ID, output_schema: "weksa.mimo_voicedesign_receipt.v0", authority: "Weksa accepts the request, commits typed witnesses, and treats MiMo as an external renderer.", }, ], forbidden_writers: [ - "HTTP clients may request commands but do not own Weksa intent state.", + "RUDP command senders may request commands but do not own Weksa intent state.", "Odin, Eve, and renderers may lower Weksa surfaces but do not mutate command authority.", ], compatibility: { - http: `http://${host}:${port}`, - status: "debug-and-command-lowering-only", - cut_line: "Replace speech_provider.mimo.voicedesign compatibility HTTP with a CultNet/RUDP command document ingress before deleting the HTTP command route.", + http: null, + status: "no-http-command-lane", + cut_line: "The HTTP command route is deleted; Weksa accepts speech_provider.mimo.voicedesign over CultNet/RUDP command documents and keeps typed witnesses as truth.", }, }; } @@ -499,16 +552,16 @@ function buildTransportProfile(updatedAt) { updated_at: updatedAt, target_transport: CULTNET_RUDP_PROTOCOL_ID, current_transport: idunnHealthPublisher - ? "rudp-health-and-cultmesh-store + compatibility-http-command" - : "cultmesh-store + compatibility-http", - health_transport: idunnHealthPublisher ? CULTNET_RUDP_PROTOCOL_ID : "compatibility-http", + ? "rudp-health-and-cultmesh-store-and-rudp-command" + : "cultmesh-store-and-rudp-command", + health_transport: idunnHealthPublisher ? CULTNET_RUDP_PROTOCOL_ID : "unconfigured", state_transport: "cultmesh-store", - command_transport: "compatibility-http", + command_transport: CULTNET_RUDP_PROTOCOL_ID, compatibility: { - http: `http://${host}:${port}`, - endpoints_are_lowerings: true, + http: null, + endpoints_are_lowerings: false, }, - cut_line: "Weksa health and provider state are daemon-owned typed records; the remaining command ingress debt is the MiMo VoiceDesign HTTP route.", + cut_line: "Weksa health, provider state, and MiMo VoiceDesign command ingress are daemon-owned typed records over CultNet/RUDP plus CultMesh store; HTTP is no longer in the daemon stack.", }; } @@ -596,13 +649,18 @@ async function handleMimoVoiceDesign(body) { const requestId = command.request_id || `mimo-${new Date().toISOString().replace(/[-:.TZ]/g, "")}-${Math.random().toString(36).slice(2, 8)}`; const outputRoot = resolve(stateRoot, "speech-provider", "mimo", requestId); await mkdir(outputRoot, { recursive: true }); + const commandDocument = { + ...command, + schema_version: WEKSA_MIMO_VOICEDESIGN_COMMAND_SCHEMA_ID, + request_id: requestId, + }; - const personaEvidence = inspectPersonaState(command.persona_state_path); - const interlinguaPacket = command.interlingua_packet - ? normalizeInterlinguaPacket(command.interlingua_packet, requestId) - : decomposeThoughtIntoInterlingua(command.thought_text, requestId, personaEvidence, command); - const targetRealization = lowerInterlinguaToSpeechText(interlinguaPacket, personaEvidence, command); - const mimoRequest = buildMimoVoiceDesignRequest({ requestId, command, personaEvidence, interlinguaPacket, targetRealization }); + const personaEvidence = inspectPersonaState(commandDocument.persona_state_path); + const interlinguaPacket = commandDocument.interlingua_packet + ? normalizeInterlinguaPacket(commandDocument.interlingua_packet, requestId) + : decomposeThoughtIntoInterlingua(commandDocument.thought_text, requestId, personaEvidence, commandDocument); + const targetRealization = lowerInterlinguaToSpeechText(interlinguaPacket, personaEvidence, commandDocument); + const mimoRequest = buildMimoVoiceDesignRequest({ requestId, command: commandDocument, personaEvidence, interlinguaPacket, targetRealization }); const providerResponse = await callMimoVoiceDesign(mimoRequest); const audioPath = resolve(outputRoot, `${requestId}.wav`); await writeFile(audioPath, providerResponse.audioBytes); @@ -610,14 +668,16 @@ async function handleMimoVoiceDesign(body) { const receipt = { schema_version: "weksa.mimo_voicedesign_receipt.v0", request_id: requestId, + generated_at: new Date().toISOString(), ok: true, provider: "xiaomi-mimo", model: "mimo-v2.5-tts-voicedesign", persona_state_ref: personaEvidence.path, - source_kind: command.interlingua_packet ? "interlingua_packet" : "thought_text_decomposed_to_interlingua", + source_kind: commandDocument.interlingua_packet ? "interlingua_packet" : "thought_text_decomposed_to_interlingua", target_language: targetRealization.target_language, performance_register: targetRealization.performance_register, artifacts: { + command: `.weksa/speech-provider/mimo/${requestId}/command.cc`, interlingua_packet: `.weksa/speech-provider/mimo/${requestId}/interlingua.cc`, mimo_request: `.weksa/speech-provider/mimo/${requestId}/mimo-request.cc`, receipt: `.weksa/speech-provider/mimo/${requestId}/receipt.cc`, @@ -637,9 +697,11 @@ async function handleMimoVoiceDesign(body) { }, }; + await writeWitness(`speech-provider/mimo/${requestId}/command.cc`, commandDocument); await writeWitness(`speech-provider/mimo/${requestId}/interlingua.cc`, interlinguaPacket); await writeWitness(`speech-provider/mimo/${requestId}/mimo-request.cc`, mimoRequest); await writeWitness(`speech-provider/mimo/${requestId}/receipt.cc`, receipt); + await writeJson(resolve(outputRoot, "command.json"), commandDocument); await writeJson(resolve(outputRoot, "interlingua.json"), interlinguaPacket); await writeJson(resolve(outputRoot, "mimo-request.json"), mimoRequest); await writeJson(resolve(outputRoot, "receipt.json"), receipt); @@ -907,6 +969,13 @@ function buildVoiceDesignPrompt({ command, personaEvidence, targetRealization }) } async function callMimoVoiceDesign(mimoRequest) { + if (options.mimoDryRun) { + return { + id: "weksa-mimo-dry-run", + created: new Date().toISOString(), + audioBytes: Buffer.from("RIFF0000WAVEfmt ", "ascii"), + }; + } if (!existsSync(mimoApiKeyPath)) { throw httpError(500, `MiMo API key file is missing: ${mimoApiKeyPath}`); } @@ -959,7 +1028,7 @@ function publication(key, schema, witness, endpoint) { key, schema, witness, - compatibility_endpoint: endpoint, + endpoint, authority: "Weksa daemon", }; } @@ -1114,9 +1183,20 @@ function parseArgs(args) { parsed.idunnHealthContract = args[index + 1]; index += 1; break; + case "--odin-cultmesh-rudp": + parsed.odinCultMeshRudp = args[index + 1]; + index += 1; + break; case "--health": parsed.health = true; break; + case "--mimo-command-json": + parsed.mimoCommandJson = args[index + 1]; + index += 1; + break; + case "--mimo-dry-run": + parsed.mimoDryRun = true; + break; default: throw new Error(`Unknown argument: ${arg}`); } diff --git a/scripts/weksa-rudp-command.cjs b/scripts/weksa-rudp-command.cjs new file mode 100644 index 0000000..5d310da --- /dev/null +++ b/scripts/weksa-rudp-command.cjs @@ -0,0 +1,466 @@ +"use strict"; + +const dgram = require("dgram"); +const path = require("path"); + +const { + CultNetRudpSession, + decodeRudpPacket, + encodeCultNetMessageForWire, + encodeRudpPacket, + parseCultNetMessage, +} = require("cultnet-ts"); + +const { encode, decode } = require(path.resolve( + __dirname, + "..", + "..", + "CultLib", + "node_modules", + "@msgpack", + "msgpack", +)); + +const CULTNET_RUDP_PROTOCOL_ID = "cultnet.transport.rudp.v0"; +const WEKSA_COMMAND_RUDP_CONNECTION_ID = 0x1d0d0002; +const WEKSA_MIMO_VOICEDESIGN_COMMAND_SCHEMA_ID = "weksa.mimo_voicedesign_command.v0"; +const WEKSA_MIMO_VOICEDESIGN_RECEIPT_SCHEMA_ID = "weksa.mimo_voicedesign_receipt.v0"; + +async function createWeksaRudpCommandServer(options) { + if (!options?.host || !options?.port || typeof options.onCommand !== "function") { + throw new Error("Weksa RUDP command server requires host, port, and onCommand."); + } + const socket = dgram.createSocket(endpointFamily(options.host)); + await bindServerSocket(socket, options.host, options.port); + + const sessions = new Map(); + const resendTimer = setInterval(() => { + for (const entry of sessions.values()) { + for (const packet of entry.session.dueResends(Date.now())) { + void sendPacket(socket, entry.endpoint, packet); + } + } + }, 25); + resendTimer.unref?.(); + + const onMessage = (wire, remote) => { + void handleServerDatagram({ + wire, + remote, + socket, + sessions, + options, + }).catch((error) => { + options.onError?.(error); + }); + }; + const onError = (error) => { + options.onError?.(error instanceof Error ? error : new Error(String(error))); + }; + + socket.on("message", onMessage); + socket.on("error", onError); + + return { + endpoint: { host: options.host, port: options.port }, + uri: formatRudpEndpoint(options.host, options.port), + close() { + clearInterval(resendTimer); + socket.off("message", onMessage); + socket.off("error", onError); + socket.close(); + sessions.clear(); + }, + }; +} + +async function handleServerDatagram(input) { + const { wire, remote, socket, sessions, options } = input; + const packet = decodeRudpPacket(wire); + const key = endpointKey(remote.address, remote.port); + let entry = sessions.get(key); + if (!entry) { + if (packet.packetType !== "connect") { + return; + } + const session = new CultNetRudpSession({ + connectionId: WEKSA_COMMAND_RUDP_CONNECTION_ID, + initialSequence: 1, + resendDelayMs: 100, + maxPendingReliablePackets: 64, + }); + entry = { + session, + endpoint: { host: remote.address, port: remote.port }, + }; + sessions.set(key, entry); + await sendPacket(socket, entry.endpoint, session.acceptConnect(packet, Date.now())); + return; + } + + const result = entry.session.receive(packet, Date.now()); + if (result.reply) { + await sendPacket(socket, entry.endpoint, result.reply); + } + for (const frame of result.delivered) { + await handleServerFrame({ + socket, + entry, + frame, + options, + }); + } + if (packet.packetType === "accept" || result.delivered.length > 0) { + await sendPacket(socket, entry.endpoint, entry.session.createAck()); + } + if (result.disconnected) { + sessions.delete(key); + } +} + +async function handleServerFrame(input) { + const { socket, entry, frame, options } = input; + if (frame.channelId !== "schema") { + return; + } + const message = parseCultNetMessage(decode(frame.payload), "cultnet.schema.v0"); + if (message.schemaVersion !== "cultnet.document_put_raw.v0") { + return; + } + if (message.document.schemaId !== WEKSA_MIMO_VOICEDESIGN_COMMAND_SCHEMA_ID) { + return; + } + const payload = decode(message.document.payload); + const command = { + ...payload, + request_id: payload?.request_id ?? message.document.recordKey, + }; + let receipt; + try { + receipt = await options.onCommand(command, message); + } catch (error) { + options.onError?.(error instanceof Error ? error : new Error(String(error))); + receipt = { + schema_version: WEKSA_MIMO_VOICEDESIGN_RECEIPT_SCHEMA_ID, + request_id: command.request_id, + generated_at: new Date().toISOString(), + ok: false, + error: error instanceof Error ? error.message : String(error), + }; + } + if (!receipt) { + return; + } + const receiptMessage = { + schemaVersion: "cultnet.document_put_raw.v0", + messageId: `${options.runtimeId ?? "weksa-daemon"}:receipt:${receipt.request_id}`, + document: { + schemaId: WEKSA_MIMO_VOICEDESIGN_RECEIPT_SCHEMA_ID, + recordKey: receipt.request_id, + storedAt: receipt.generated_at ?? receipt.observed_at ?? new Date().toISOString(), + payloadEncoding: "messagepack", + payload: encode(receipt), + sourceRuntimeId: options.runtimeId ?? "weksa-daemon", + sourceRole: "weksa-command-ingress", + tags: [CULTNET_RUDP_PROTOCOL_ID, "speech_provider.mimo.voicedesign"], + }, + }; + const wirePayload = encode( + encodeCultNetMessageForWire(receiptMessage, "cultnet.schema.v0"), + ); + for (const packet of entry.session.sendMany("schema", wirePayload, { + reliable: true, + ordered: true, + nowMs: Date.now(), + })) { + await sendPacket(socket, entry.endpoint, packet); + } +} + +async function sendWeksaMimoVoiceDesignCommand(options) { + const endpoint = parseEndpoint(options?.endpoint); + const requestId = options.command?.request_id ?? buildRequestId(); + const command = { + ...options.command, + request_id: requestId, + }; + const socket = dgram.createSocket(endpointFamily(endpoint.host)); + await bindClientSocket(socket, options?.localHost); + const receiver = createPacketReceiver(socket); + const session = new CultNetRudpSession({ + connectionId: WEKSA_COMMAND_RUDP_CONNECTION_ID, + initialSequence: 1, + resendDelayMs: 100, + maxPendingReliablePackets: 64, + }); + + try { + await sendPacket(socket, endpoint, session.createConnect(Date.now(), new Uint8Array())); + await receiveCultNetMessageUntil({ + receiver, + session, + endpoint, + timeoutMs: options.timeoutMs ?? 5000, + label: "accept", + predicate: (_message, packet) => packet.packetType === "accept", + }); + + const commandMessage = { + schemaVersion: "cultnet.document_put_raw.v0", + messageId: `${options.runtimeId ?? "weksa-cli"}:command:${requestId}`, + document: { + schemaId: WEKSA_MIMO_VOICEDESIGN_COMMAND_SCHEMA_ID, + recordKey: requestId, + storedAt: new Date().toISOString(), + payloadEncoding: "messagepack", + payload: encode(command), + sourceRuntimeId: options.runtimeId ?? "weksa-cli", + sourceRole: "command-client", + tags: [CULTNET_RUDP_PROTOCOL_ID, "speech_provider.mimo.voicedesign"], + }, + }; + const wirePayload = encode( + encodeCultNetMessageForWire(commandMessage, "cultnet.schema.v0"), + ); + for (const packet of session.sendMany("schema", wirePayload, { + reliable: true, + ordered: true, + nowMs: Date.now(), + })) { + await sendPacket(socket, endpoint, packet); + } + + return await receiveCultNetMessageUntil({ + receiver, + session, + endpoint, + timeoutMs: options.timeoutMs ?? 10000, + label: "receipt", + predicate: (message) => + message.schemaVersion === "cultnet.document_put_raw.v0" + && message.document.schemaId === WEKSA_MIMO_VOICEDESIGN_RECEIPT_SCHEMA_ID + && message.document.recordKey === requestId, + decodePayload: true, + }); + } finally { + receiver.close(); + socket.close(); + } +} + +async function receiveCultNetMessageUntil(options) { + const deadline = Date.now() + options.timeoutMs; + while (Date.now() < deadline) { + try { + const packet = await options.receiver.next( + Math.min(100, Math.max(1, deadline - Date.now())), + options.label, + ); + const result = options.session.receive(packet, Date.now()); + if (result.reply) { + await sendPacket(options.receiver.socket, options.endpoint, result.reply); + } + if (packet.packetType === "accept" || result.delivered.length > 0) { + await sendPacket( + options.receiver.socket, + options.endpoint, + options.session.createAck(), + ); + } + if (packet.packetType === "accept" && options.predicate(undefined, packet)) { + return undefined; + } + for (const frame of result.delivered) { + if (frame.channelId !== "schema") { + continue; + } + const message = parseCultNetMessage(decode(frame.payload), "cultnet.schema.v0"); + if (!options.predicate(message, packet)) { + continue; + } + if ( + options.decodePayload + && message.schemaVersion === "cultnet.document_put_raw.v0" + ) { + return decode(message.document.payload); + } + return message; + } + } catch (error) { + if (error.code !== "ETIMEDOUT") { + throw error; + } + } + for (const packet of options.session.dueResends(Date.now())) { + await sendPacket(options.receiver.socket, options.endpoint, packet); + } + } + throw new Error(`timed out waiting for Weksa RUDP ${options.label} after ${options.timeoutMs}ms`); +} + +async function bindServerSocket(socket, host, port) { + await new Promise((resolve, reject) => { + socket.once("error", reject); + socket.bind(port, host, () => { + socket.off("error", reject); + resolve(); + }); + }); +} + +async function bindClientSocket(socket, localHost) { + const bindHost = localHost ?? "0.0.0.0"; + await new Promise((resolve, reject) => { + socket.once("error", reject); + socket.bind(0, bindHost, () => { + socket.off("error", reject); + resolve(); + }); + }); +} + +function createPacketReceiver(socket) { + const packets = []; + const waiters = []; + const errors = []; + + const resolveNext = () => { + while (waiters.length > 0 && (packets.length > 0 || errors.length > 0)) { + const waiter = waiters.shift(); + clearTimeout(waiter.timer); + if (errors.length > 0) { + waiter.reject(errors.shift()); + } else { + waiter.resolve(packets.shift()); + } + } + }; + + const onMessage = (wire) => { + try { + packets.push(decodeRudpPacket(wire)); + } catch (error) { + errors.push(error); + } + resolveNext(); + }; + const onError = (error) => { + errors.push(error); + resolveNext(); + }; + + socket.on("message", onMessage); + socket.on("error", onError); + + return { + socket, + next(timeoutMs, label = "packet") { + if (packets.length > 0) { + return Promise.resolve(packets.shift()); + } + if (errors.length > 0) { + return Promise.reject(errors.shift()); + } + return new Promise((resolve, reject) => { + const waiter = { + resolve, + reject, + timer: setTimeout(() => { + const index = waiters.indexOf(waiter); + if (index >= 0) { + waiters.splice(index, 1); + } + const error = new Error(`timed out waiting for Weksa RUDP ${label}`); + error.code = "ETIMEDOUT"; + reject(error); + }, Math.max(1, timeoutMs)), + }; + waiters.push(waiter); + }); + }, + close() { + socket.off("message", onMessage); + socket.off("error", onError); + while (waiters.length > 0) { + const waiter = waiters.shift(); + clearTimeout(waiter.timer); + const error = new Error("Weksa RUDP command receiver closed."); + error.code = "ECLOSED"; + waiter.reject(error); + } + }, + }; +} + +async function sendPacket(socket, endpoint, packet) { + const wire = encodeRudpPacket(packet); + await new Promise((resolve, reject) => { + socket.send(wire, endpoint.port, endpoint.host, (error) => { + if (error) { + reject(error); + } else { + resolve(); + } + }); + }); +} + +function parseEndpoint(value) { + if (!value) { + throw new Error("Weksa RUDP endpoint is required."); + } + const text = String(value).trim(); + if (text.toLowerCase().startsWith("rudp://")) { + const parsed = new URL(text); + return { + host: parsed.hostname, + port: parsePort(parsed.port), + }; + } + const ipv6 = text.match(/^\[([^\]]+)\]:(\d+)$/); + if (ipv6) { + return { host: ipv6[1], port: parsePort(ipv6[2]) }; + } + const index = text.lastIndexOf(":"); + if (index <= 0) { + throw new Error(`Weksa RUDP endpoint must be host:port, got "${value}".`); + } + return { + host: text.slice(0, index), + port: parsePort(text.slice(index + 1)), + }; +} + +function parsePort(value) { + const port = Number(value); + if (!Number.isInteger(port) || port <= 0 || port > 65535) { + throw new Error(`Weksa RUDP endpoint port is invalid: ${value}`); + } + return port; +} + +function endpointFamily(host) { + return host.includes(":") ? "udp6" : "udp4"; +} + +function endpointKey(host, port) { + return `${host}:${port}`; +} + +function formatRudpEndpoint(host, port) { + return host.includes(":") ? `rudp://[${host}]:${port}` : `rudp://${host}:${port}`; +} + +function buildRequestId() { + return `mimo-${new Date().toISOString().replace(/[-:.TZ]/g, "")}-${Math.random().toString(36).slice(2, 8)}`; +} + +module.exports = { + CULTNET_RUDP_PROTOCOL_ID, + WEKSA_COMMAND_RUDP_CONNECTION_ID, + WEKSA_MIMO_VOICEDESIGN_COMMAND_SCHEMA_ID, + WEKSA_MIMO_VOICEDESIGN_RECEIPT_SCHEMA_ID, + createWeksaRudpCommandServer, + sendWeksaMimoVoiceDesignCommand, + formatRudpEndpoint, +};