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",
|
||||
"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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
}
|
||||
|
|
|
|||
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