Move Weksa command ingress to RUDP
This commit is contained in:
parent
54272da0d5
commit
e9a00e8a3d
6 changed files with 685 additions and 128 deletions
|
|
@ -240,7 +240,8 @@
|
||||||
"id": "weksa.command.speech_provider.mimo.voicedesign",
|
"id": "weksa.command.speech_provider.mimo.voicedesign",
|
||||||
"owner": "Weksa",
|
"owner": "Weksa",
|
||||||
"acceptedByFixture": true,
|
"acceptedByFixture": true,
|
||||||
"compatibilityEndpoint": "POST /speech-provider/mimo/voicedesign",
|
"commandIngress": "cultnet.transport.rudp.v0",
|
||||||
|
"inputSchema": "weksa.mimo_voicedesign_command.v0",
|
||||||
"allowedInputs": [
|
"allowedInputs": [
|
||||||
"Persona/agent state file path",
|
"Persona/agent state file path",
|
||||||
"weksa.interlingua_packet.v0-compatible object",
|
"weksa.interlingua_packet.v0-compatible object",
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ Outputs:
|
||||||
- `weksa.pronunciation_plan.v0`
|
- `weksa.pronunciation_plan.v0`
|
||||||
- `weksa.utterance_handoff.v0`
|
- `weksa.utterance_handoff.v0`
|
||||||
- `weksa.utterance_embedding_handoff.v0.1`
|
- `weksa.utterance_embedding_handoff.v0.1`
|
||||||
|
- `weksa.mimo_voicedesign_command.v0`
|
||||||
- `weksa.mimo_tts_request.v0`
|
- `weksa.mimo_tts_request.v0`
|
||||||
- `weksa.mimo_voicedesign_receipt.v0`
|
- `weksa.mimo_voicedesign_receipt.v0`
|
||||||
- Eve/CultUI surfaces for intent review, pronunciation inspection, and handoff
|
- 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.
|
index used by Idunn health checks and operator inspection.
|
||||||
- `.weksa/eve-surfaces.cc`: current Weksa operator Eve surface witness.
|
- `.weksa/eve-surfaces.cc`: current Weksa operator Eve surface witness.
|
||||||
- `.weksa/command-boundary.cc`: provider-owned command and lifecycle boundary
|
- `.weksa/command-boundary.cc`: provider-owned command and lifecycle boundary
|
||||||
naming Weksa as owner, Idunn as lifecycle requester, and the remaining MiMo
|
naming Weksa as owner, Idunn as lifecycle requester, and the MiMo
|
||||||
VoiceDesign HTTP route as compatibility command ingress.
|
VoiceDesign RUDP command ingress as daemon-owned transport authority.
|
||||||
- `.weksa/transport-profile.cc`: target/current transport profile. Health is
|
- `.weksa/transport-profile.cc`: target/current transport profile. Health is
|
||||||
published through `cultnet.transport.rudp.v0`; provider state is carried by
|
published through `cultnet.transport.rudp.v0`; provider state is carried by
|
||||||
the CultMesh store; the MiMo VoiceDesign command route remains compatibility
|
the CultMesh store; MiMo VoiceDesign command ingress now arrives through
|
||||||
HTTP debt until a CultNet/RUDP command document ingress replaces it.
|
`cultnet.transport.rudp.v0` instead of an HTTP route.
|
||||||
- `.weksa/cultmesh-publications.cc`: publication index naming current Weksa
|
- `.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/intent/{intentId}.cc`: conversational intent documents.
|
||||||
- `.weksa/pronunciation/{utteranceId}.cc`: pronunciation plans and phonetic
|
- `.weksa/pronunciation/{utteranceId}.cc`: pronunciation plans and phonetic
|
||||||
event sequences.
|
event sequences.
|
||||||
- `.weksa/handoff/{utteranceId}.cc`: utterance handoffs and embedding handoff
|
- `.weksa/handoff/{utteranceId}.cc`: utterance handoffs and embedding handoff
|
||||||
requests for AquaSynth.
|
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
|
- `.weksa/speech-provider/mimo/{requestId}/interlingua.cc`: accepted or
|
||||||
provisionally decomposed interlingua used for a MiMo verbalization command.
|
provisionally decomposed interlingua used for a MiMo verbalization command.
|
||||||
- `.weksa/speech-provider/mimo/{requestId}/mimo-request.cc`: Weksa-owned MiMo
|
- `.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
|
checked by `scripts/health-weksa-daemon.cmd`, and restarted by
|
||||||
`scripts/restart-weksa-daemon.cmd`. Its daemon-owned health path publishes
|
`scripts/restart-weksa-daemon.cmd`. Its daemon-owned health path publishes
|
||||||
`weksa.cultnet-rudp-provider-health` to Idunn over
|
`weksa.cultnet-rudp-provider-health` to Idunn over
|
||||||
`cultnet.transport.rudp.v0` after each serialized witness refresh. Its
|
`cultnet.transport.rudp.v0` after each serialized witness refresh, and MiMo
|
||||||
compatibility HTTP surface still listens on `http://127.0.0.1:8813` with
|
VoiceDesign commands arrive on `rudp://127.0.0.1:8813` as
|
||||||
`/health`, `/provider-advertisement`, `/operator-state`, `/eve/operator`,
|
`weksa.mimo_voicedesign_command.v0` documents. Health and operator truth live
|
||||||
`/cultmesh/publications`, and `POST /speech-provider/mimo/voicedesign`, but
|
in `.weksa/*.cc` witnesses; the HTTP daemon surface is deleted.
|
||||||
those endpoints are operator/debug projections, not daemon liveness owners.
|
|
||||||
|
|
||||||
## CultMesh Verses
|
## 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
|
`weksa` in the `starfire-local` swarm profile and owns restart authority through
|
||||||
the Weksa restart script. Odin should read Weksa's provider advertisement,
|
the Weksa restart script. Odin should read Weksa's provider advertisement,
|
||||||
operator state, command boundary, transport profile, and Eve surface from the
|
operator state, command boundary, transport profile, and Eve surface from the
|
||||||
typed provider store; HTTP endpoints are debug/operator lowerings and do not own
|
typed provider store and RUDP command boundary; no HTTP endpoint owns provider
|
||||||
provider truth.
|
truth.
|
||||||
|
|
||||||
## Eve Surfaces
|
## Eve Surfaces
|
||||||
|
|
||||||
|
|
@ -132,7 +134,8 @@ provider truth.
|
||||||
- `speech_provider.mimo.voicedesign`: accept a Persona/agent state file plus
|
- `speech_provider.mimo.voicedesign`: accept a Persona/agent state file plus
|
||||||
either a Weksa interlingua packet or raw thought text. Raw thought is first
|
either a Weksa interlingua packet or raw thought text. Raw thought is first
|
||||||
decomposed into provisional interlingua, then lowered into a MiMo VoiceDesign
|
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`.
|
- `provider.advertise`: publish `gamecult.eve.provider_advertisement.v1`.
|
||||||
|
|
||||||
## Forbidden Writers
|
## Forbidden Writers
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,8 @@ param(
|
||||||
[string] $MiMoApiKeyPath = "E:\Projects\gamecult-ops\mimo-api-Weksa.txt",
|
[string] $MiMoApiKeyPath = "E:\Projects\gamecult-ops\mimo-api-Weksa.txt",
|
||||||
[string] $IdunnRudpHealth = "127.0.0.1:17870",
|
[string] $IdunnRudpHealth = "127.0.0.1:17870",
|
||||||
[string] $IdunnDaemon = "weksa",
|
[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"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
@ -21,5 +22,5 @@ if (Test-Path -LiteralPath $pidPath) {
|
||||||
Remove-Item -LiteralPath $pidPath -Force -ErrorAction SilentlyContinue
|
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
|
exit $LASTEXITCODE
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,8 @@ param(
|
||||||
[string] $MiMoApiKeyPath = "E:\Projects\gamecult-ops\mimo-api-Weksa.txt",
|
[string] $MiMoApiKeyPath = "E:\Projects\gamecult-ops\mimo-api-Weksa.txt",
|
||||||
[string] $IdunnRudpHealth = "127.0.0.1:17870",
|
[string] $IdunnRudpHealth = "127.0.0.1:17870",
|
||||||
[string] $IdunnDaemon = "weksa",
|
[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"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
@ -49,6 +50,10 @@ $arguments = @(
|
||||||
"--idunn-health-contract", $IdunnHealthContract
|
"--idunn-health-contract", $IdunnHealthContract
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($OdinCultMeshRudp)) {
|
||||||
|
$arguments += @("--odin-cultmesh-rudp", $OdinCultMeshRudp)
|
||||||
|
}
|
||||||
|
|
||||||
$process = Start-Process -FilePath "node" `
|
$process = Start-Process -FilePath "node" `
|
||||||
-ArgumentList $arguments `
|
-ArgumentList $arguments `
|
||||||
-WorkingDirectory $repoRoot `
|
-WorkingDirectory $repoRoot `
|
||||||
|
|
@ -73,5 +78,6 @@ if ($LASTEXITCODE -ne 0) {
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host "Started Weksa daemon as PID $($process.Id)."
|
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"
|
Write-Host " state: $StateRoot"
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
import http from "node:http";
|
|
||||||
import Module from "node:module";
|
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 { mkdir, readdir, rm, writeFile } from "node:fs/promises";
|
||||||
import { basename, delimiter, dirname, extname, resolve } from "node:path";
|
import { basename, delimiter, dirname, extname, resolve } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
@ -10,10 +9,16 @@ const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||||
configureCultLibModulePath();
|
configureCultLibModulePath();
|
||||||
const require = Module.createRequire(import.meta.url);
|
const require = Module.createRequire(import.meta.url);
|
||||||
const {
|
const {
|
||||||
CULTNET_RUDP_PROTOCOL_ID,
|
|
||||||
createIdunnRudpHealthPublisher,
|
createIdunnRudpHealthPublisher,
|
||||||
publishIdunnRudpHealth,
|
publishIdunnRudpHealth,
|
||||||
} = require("./idunn-rudp-health.cjs");
|
} = 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 options = parseArgs(process.argv.slice(2));
|
||||||
const port = Number(options.port ?? process.env.WEKSA_DAEMON_PORT ?? 8813);
|
const port = Number(options.port ?? process.env.WEKSA_DAEMON_PORT ?? 8813);
|
||||||
const host = options.host ?? process.env.WEKSA_DAEMON_HOST ?? "127.0.0.1";
|
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",
|
daemonId: options.idunnDaemon ?? process.env.WEKSA_IDUNN_DAEMON ?? "weksa",
|
||||||
healthContract: options.idunnHealthContract ?? process.env.WEKSA_IDUNN_HEALTH_CONTRACT ?? "weksa.cultnet-rudp-provider-health",
|
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();
|
const startedAt = new Date().toISOString();
|
||||||
let tick = 0;
|
let tick = 0;
|
||||||
let lastPublishedAt = "";
|
let lastPublishedAt = "";
|
||||||
|
|
@ -39,56 +45,38 @@ if (options.health) {
|
||||||
await runHealthCheck();
|
await runHealthCheck();
|
||||||
process.exit(0);
|
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 mkdir(stateRoot, { recursive: true });
|
||||||
await writeFile(pidPath, `${process.pid}\n`, "ascii");
|
await writeFile(pidPath, `${process.pid}\n`, "ascii");
|
||||||
await appendLog(`started pid=${process.pid} host=${host} port=${port}`);
|
await appendLog(`started pid=${process.pid} host=${host} port=${port}`);
|
||||||
await refreshWitnesses("startup");
|
const startupSnapshot = await refreshWitnesses("startup");
|
||||||
|
await publishOdinStartupRespect(startupSnapshot);
|
||||||
const server = http.createServer(async (request, response) => {
|
const commandServer = await createWeksaRudpCommandServer({
|
||||||
try {
|
host,
|
||||||
const url = new URL(request.url ?? "/", `http://${request.headers.host ?? `${host}:${port}`}`);
|
port,
|
||||||
const snapshot = await currentSnapshot();
|
runtimeId: "weksa-daemon",
|
||||||
if (url.pathname === "/health") {
|
onCommand: async (command) => {
|
||||||
return sendJson(response, 200, {
|
const receipt = await handleMimoVoiceDesign(command);
|
||||||
ok: true,
|
await refreshWitnesses("mimo-voicedesign-command");
|
||||||
service: "weksa.intent.service",
|
return receipt;
|
||||||
providerId: "weksa.intent.service",
|
},
|
||||||
state: "healthy",
|
onError: async (error) => {
|
||||||
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) {
|
|
||||||
lastError = String(error?.stack ?? error);
|
lastError = String(error?.stack ?? error);
|
||||||
await appendLog(`request error: ${lastError}`);
|
await appendLog(`rudp command 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(`listening ${commandServer.uri}`);
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
refreshWitnesses("interval").catch(async (error) => {
|
refreshWitnesses("interval").catch(async (error) => {
|
||||||
|
|
@ -104,32 +92,65 @@ process.on("SIGTERM", () => shutdown(0));
|
||||||
async function shutdown(code) {
|
async function shutdown(code) {
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
await appendLog(`stopping pid=${process.pid}`);
|
await appendLog(`stopping pid=${process.pid}`);
|
||||||
server.close(() => process.exit(code));
|
commandServer.close();
|
||||||
|
process.exit(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runHealthCheck() {
|
async function runHealthCheck() {
|
||||||
const healthUrl = `http://${host}:${port}/health`;
|
const operatorStatePath = resolve(stateRoot, "operator-state.cc");
|
||||||
await new Promise((resolvePromise, rejectPromise) => {
|
const commandBoundaryPath = resolve(stateRoot, "command-boundary.cc");
|
||||||
const request = http.get(healthUrl, (response) => {
|
const transportProfilePath = resolve(stateRoot, "transport-profile.cc");
|
||||||
let body = "";
|
if (!existsSync(pidPath)) {
|
||||||
response.setEncoding("utf8");
|
throw new Error(`Weksa daemon PID file is missing: ${pidPath}`);
|
||||||
response.on("data", (chunk) => {
|
}
|
||||||
body += chunk;
|
if (!existsSync(operatorStatePath)) {
|
||||||
});
|
throw new Error(`Weksa operator-state witness is missing: ${operatorStatePath}`);
|
||||||
response.on("end", () => {
|
}
|
||||||
if (response.statusCode === 200) {
|
if (!existsSync(commandBoundaryPath)) {
|
||||||
process.stdout.write(body);
|
throw new Error(`Weksa command-boundary witness is missing: ${commandBoundaryPath}`);
|
||||||
resolvePromise();
|
}
|
||||||
} else {
|
if (!existsSync(transportProfilePath)) {
|
||||||
rejectPromise(new Error(`health returned ${response.statusCode}: ${body}`));
|
throw new Error(`Weksa transport-profile witness is missing: ${transportProfilePath}`);
|
||||||
}
|
}
|
||||||
});
|
const pidText = readFileSync(pidPath, "utf8").trim();
|
||||||
});
|
if (!/^\d+$/.test(pidText)) {
|
||||||
request.on("error", rejectPromise);
|
throw new Error(`Weksa daemon PID file is invalid: ${pidText}`);
|
||||||
request.setTimeout(5_000, () => {
|
}
|
||||||
request.destroy(new Error(`health timed out: ${healthUrl}`));
|
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() {
|
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() {
|
async function buildSnapshot() {
|
||||||
const updatedAt = new Date().toISOString();
|
const updatedAt = new Date().toISOString();
|
||||||
const providerAdvertisement = loadProviderAdvertisement(updatedAt);
|
const providerAdvertisement = loadProviderAdvertisement(updatedAt);
|
||||||
|
|
@ -222,7 +270,7 @@ async function buildSnapshot() {
|
||||||
cultural_ontology_profiles: cultureCatalog.profile_count,
|
cultural_ontology_profiles: cultureCatalog.profile_count,
|
||||||
target_language_profiles: cultureCatalog.target_language_profile_count,
|
target_language_profiles: cultureCatalog.target_language_profile_count,
|
||||||
accepts_commands: true,
|
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",
|
mimo_voicedesign: existsSync(mimoApiKeyPath) ? "api_key_file_seen" : "missing_api_key_file",
|
||||||
},
|
},
|
||||||
latest_affect_tuning: tuning,
|
latest_affect_tuning: tuning,
|
||||||
|
|
@ -236,18 +284,17 @@ async function buildSnapshot() {
|
||||||
updated_at: updatedAt,
|
updated_at: updatedAt,
|
||||||
transport: {
|
transport: {
|
||||||
cultmesh_store: ".weksa/provider-advertisement-store.cc",
|
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",
|
idunn_rudp_health: idunnHealthPublisher ? `${idunnHealthPublisher.endpoint.host}:${idunnHealthPublisher.endpoint.port}` : "unconfigured",
|
||||||
compatibility_http: `http://${host}:${port}`,
|
|
||||||
},
|
},
|
||||||
publications: [
|
publications: [
|
||||||
publication("weksa.service/provider-advertisement", "gamecult.eve.provider_advertisement.v1", ".weksa/provider-advertisement-store.cc", "/provider-advertisement"),
|
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", "/operator-state"),
|
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", "/eve/operator"),
|
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.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.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.cultmesh/publications", "weksa.cultmesh_publications.v0", ".weksa/cultmesh-publications.cc", ".weksa/cultmesh-publications.cc"),
|
||||||
publication("weksa.speech-provider/mimo/voicedesign", "weksa.mimo_tts_request.v0", ".weksa/speech-provider/mimo/{requestId}.cc", "/speech-provider/mimo/voicedesign"),
|
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 };
|
return { providerAdvertisement, operatorState, operatorSurface, commandBoundary, transportProfile, cultMeshPublications };
|
||||||
|
|
@ -291,11 +338,11 @@ async function writeCultMeshStore(snapshot) {
|
||||||
cultMeshAddress: "asgard.starfire.weksa/eve/operator",
|
cultMeshAddress: "asgard.starfire.weksa/eve/operator",
|
||||||
endpoints: [
|
endpoints: [
|
||||||
{ transport: "cultmesh-store", address: ".weksa/provider-advertisement-store.cc" },
|
{ 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: [
|
routes: [
|
||||||
{ transport: "cultmesh-store", address: ".weksa/provider-advertisement-store.cc" },
|
{ 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,
|
surface: snapshot.operatorSurface,
|
||||||
|
|
@ -328,12 +375,21 @@ function loadCultRuntime() {
|
||||||
configureCultLibModulePath();
|
configureCultLibModulePath();
|
||||||
const { CultMesh } = require("cultmesh-ts");
|
const { CultMesh } = require("cultmesh-ts");
|
||||||
const { defineDocumentType } = require("cultcache-ts");
|
const { defineDocumentType } = require("cultcache-ts");
|
||||||
return { CultMesh, defineDocumentType, error: null };
|
const { defineCultNetDocumentBinding } = require("cultnet-ts");
|
||||||
|
return { CultMesh, defineDocumentType, defineCultNetDocumentBinding, error: null };
|
||||||
} catch (error) {
|
} 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() {
|
function configureCultLibModulePath() {
|
||||||
process.env.NODE_PATH = [
|
process.env.NODE_PATH = [
|
||||||
resolve(repoRoot, "..", "CultLib", "packages"),
|
resolve(repoRoot, "..", "CultLib", "packages"),
|
||||||
|
|
@ -433,18 +489,15 @@ function loadProviderAdvertisement(updatedAt) {
|
||||||
},
|
},
|
||||||
endpoints: [
|
endpoints: [
|
||||||
{ transport: "cultmesh-store", address: ".weksa/provider-advertisement-store.cc" },
|
{ 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: [
|
routes: [
|
||||||
{ transport: "cultmesh-store", address: ".weksa/provider-advertisement-store.cc" },
|
{ 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) },
|
||||||
],
|
|
||||||
compatibilityRoutes: [
|
|
||||||
{ kind: "http", url: `http://${host}:${port}`, lossy: true },
|
|
||||||
],
|
],
|
||||||
health: {
|
health: {
|
||||||
state: "healthy",
|
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",
|
witness: ".weksa/operator-state.cc",
|
||||||
checkedBy: "idunn.desired_daemon.v1",
|
checkedBy: "idunn.desired_daemon.v1",
|
||||||
},
|
},
|
||||||
|
|
@ -470,22 +523,22 @@ function buildCommandBoundary(updatedAt) {
|
||||||
commands: [
|
commands: [
|
||||||
{
|
{
|
||||||
command: "speech_provider.mimo.voicedesign",
|
command: "speech_provider.mimo.voicedesign",
|
||||||
ingress: "compatibility-http",
|
ingress: CULTNET_RUDP_PROTOCOL_ID,
|
||||||
endpoint: `http://${host}:${port}/speech-provider/mimo/voicedesign`,
|
endpoint: formatRudpEndpoint(host, port),
|
||||||
owner: "Weksa daemon",
|
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",
|
output_schema: "weksa.mimo_voicedesign_receipt.v0",
|
||||||
authority: "Weksa accepts the request, commits typed witnesses, and treats MiMo as an external renderer.",
|
authority: "Weksa accepts the request, commits typed witnesses, and treats MiMo as an external renderer.",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
forbidden_writers: [
|
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.",
|
"Odin, Eve, and renderers may lower Weksa surfaces but do not mutate command authority.",
|
||||||
],
|
],
|
||||||
compatibility: {
|
compatibility: {
|
||||||
http: `http://${host}:${port}`,
|
http: null,
|
||||||
status: "debug-and-command-lowering-only",
|
status: "no-http-command-lane",
|
||||||
cut_line: "Replace speech_provider.mimo.voicedesign compatibility HTTP with a CultNet/RUDP command document ingress before deleting the HTTP command route.",
|
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,
|
updated_at: updatedAt,
|
||||||
target_transport: CULTNET_RUDP_PROTOCOL_ID,
|
target_transport: CULTNET_RUDP_PROTOCOL_ID,
|
||||||
current_transport: idunnHealthPublisher
|
current_transport: idunnHealthPublisher
|
||||||
? "rudp-health-and-cultmesh-store + compatibility-http-command"
|
? "rudp-health-and-cultmesh-store-and-rudp-command"
|
||||||
: "cultmesh-store + compatibility-http",
|
: "cultmesh-store-and-rudp-command",
|
||||||
health_transport: idunnHealthPublisher ? CULTNET_RUDP_PROTOCOL_ID : "compatibility-http",
|
health_transport: idunnHealthPublisher ? CULTNET_RUDP_PROTOCOL_ID : "unconfigured",
|
||||||
state_transport: "cultmesh-store",
|
state_transport: "cultmesh-store",
|
||||||
command_transport: "compatibility-http",
|
command_transport: CULTNET_RUDP_PROTOCOL_ID,
|
||||||
compatibility: {
|
compatibility: {
|
||||||
http: `http://${host}:${port}`,
|
http: null,
|
||||||
endpoints_are_lowerings: true,
|
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 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);
|
const outputRoot = resolve(stateRoot, "speech-provider", "mimo", requestId);
|
||||||
await mkdir(outputRoot, { recursive: true });
|
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 personaEvidence = inspectPersonaState(commandDocument.persona_state_path);
|
||||||
const interlinguaPacket = command.interlingua_packet
|
const interlinguaPacket = commandDocument.interlingua_packet
|
||||||
? normalizeInterlinguaPacket(command.interlingua_packet, requestId)
|
? normalizeInterlinguaPacket(commandDocument.interlingua_packet, requestId)
|
||||||
: decomposeThoughtIntoInterlingua(command.thought_text, requestId, personaEvidence, command);
|
: decomposeThoughtIntoInterlingua(commandDocument.thought_text, requestId, personaEvidence, commandDocument);
|
||||||
const targetRealization = lowerInterlinguaToSpeechText(interlinguaPacket, personaEvidence, command);
|
const targetRealization = lowerInterlinguaToSpeechText(interlinguaPacket, personaEvidence, commandDocument);
|
||||||
const mimoRequest = buildMimoVoiceDesignRequest({ requestId, command, personaEvidence, interlinguaPacket, targetRealization });
|
const mimoRequest = buildMimoVoiceDesignRequest({ requestId, command: commandDocument, personaEvidence, interlinguaPacket, targetRealization });
|
||||||
const providerResponse = await callMimoVoiceDesign(mimoRequest);
|
const providerResponse = await callMimoVoiceDesign(mimoRequest);
|
||||||
const audioPath = resolve(outputRoot, `${requestId}.wav`);
|
const audioPath = resolve(outputRoot, `${requestId}.wav`);
|
||||||
await writeFile(audioPath, providerResponse.audioBytes);
|
await writeFile(audioPath, providerResponse.audioBytes);
|
||||||
|
|
@ -610,14 +668,16 @@ async function handleMimoVoiceDesign(body) {
|
||||||
const receipt = {
|
const receipt = {
|
||||||
schema_version: "weksa.mimo_voicedesign_receipt.v0",
|
schema_version: "weksa.mimo_voicedesign_receipt.v0",
|
||||||
request_id: requestId,
|
request_id: requestId,
|
||||||
|
generated_at: new Date().toISOString(),
|
||||||
ok: true,
|
ok: true,
|
||||||
provider: "xiaomi-mimo",
|
provider: "xiaomi-mimo",
|
||||||
model: "mimo-v2.5-tts-voicedesign",
|
model: "mimo-v2.5-tts-voicedesign",
|
||||||
persona_state_ref: personaEvidence.path,
|
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,
|
target_language: targetRealization.target_language,
|
||||||
performance_register: targetRealization.performance_register,
|
performance_register: targetRealization.performance_register,
|
||||||
artifacts: {
|
artifacts: {
|
||||||
|
command: `.weksa/speech-provider/mimo/${requestId}/command.cc`,
|
||||||
interlingua_packet: `.weksa/speech-provider/mimo/${requestId}/interlingua.cc`,
|
interlingua_packet: `.weksa/speech-provider/mimo/${requestId}/interlingua.cc`,
|
||||||
mimo_request: `.weksa/speech-provider/mimo/${requestId}/mimo-request.cc`,
|
mimo_request: `.weksa/speech-provider/mimo/${requestId}/mimo-request.cc`,
|
||||||
receipt: `.weksa/speech-provider/mimo/${requestId}/receipt.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}/interlingua.cc`, interlinguaPacket);
|
||||||
await writeWitness(`speech-provider/mimo/${requestId}/mimo-request.cc`, mimoRequest);
|
await writeWitness(`speech-provider/mimo/${requestId}/mimo-request.cc`, mimoRequest);
|
||||||
await writeWitness(`speech-provider/mimo/${requestId}/receipt.cc`, receipt);
|
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, "interlingua.json"), interlinguaPacket);
|
||||||
await writeJson(resolve(outputRoot, "mimo-request.json"), mimoRequest);
|
await writeJson(resolve(outputRoot, "mimo-request.json"), mimoRequest);
|
||||||
await writeJson(resolve(outputRoot, "receipt.json"), receipt);
|
await writeJson(resolve(outputRoot, "receipt.json"), receipt);
|
||||||
|
|
@ -907,6 +969,13 @@ function buildVoiceDesignPrompt({ command, personaEvidence, targetRealization })
|
||||||
}
|
}
|
||||||
|
|
||||||
async function callMimoVoiceDesign(mimoRequest) {
|
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)) {
|
if (!existsSync(mimoApiKeyPath)) {
|
||||||
throw httpError(500, `MiMo API key file is missing: ${mimoApiKeyPath}`);
|
throw httpError(500, `MiMo API key file is missing: ${mimoApiKeyPath}`);
|
||||||
}
|
}
|
||||||
|
|
@ -959,7 +1028,7 @@ function publication(key, schema, witness, endpoint) {
|
||||||
key,
|
key,
|
||||||
schema,
|
schema,
|
||||||
witness,
|
witness,
|
||||||
compatibility_endpoint: endpoint,
|
endpoint,
|
||||||
authority: "Weksa daemon",
|
authority: "Weksa daemon",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -1114,9 +1183,20 @@ function parseArgs(args) {
|
||||||
parsed.idunnHealthContract = args[index + 1];
|
parsed.idunnHealthContract = args[index + 1];
|
||||||
index += 1;
|
index += 1;
|
||||||
break;
|
break;
|
||||||
|
case "--odin-cultmesh-rudp":
|
||||||
|
parsed.odinCultMeshRudp = args[index + 1];
|
||||||
|
index += 1;
|
||||||
|
break;
|
||||||
case "--health":
|
case "--health":
|
||||||
parsed.health = true;
|
parsed.health = true;
|
||||||
break;
|
break;
|
||||||
|
case "--mimo-command-json":
|
||||||
|
parsed.mimoCommandJson = args[index + 1];
|
||||||
|
index += 1;
|
||||||
|
break;
|
||||||
|
case "--mimo-dry-run":
|
||||||
|
parsed.mimoDryRun = true;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown argument: ${arg}`);
|
throw new Error(`Unknown argument: ${arg}`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
466
scripts/weksa-rudp-command.cjs
Normal file
466
scripts/weksa-rudp-command.cjs
Normal file
|
|
@ -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,
|
||||||
|
};
|
||||||
Loading…
Reference in a new issue