park-node-body: park the Node body and Discord-sourced harness at parked/weksa-node-body; true README (squash of weksa-lowering/park-node-body 2583fb9..e41b60d)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
2583fb91c3
commit
3327eddca3
36 changed files with 56 additions and 6615 deletions
|
|
@ -1,2 +0,0 @@
|
|||
@echo off
|
||||
node E:\Projects\weksa\scripts\weksa-daemon.mjs --health --host 127.0.0.1 --port 8813 --state-root E:\Projects\weksa\.weksa >NUL
|
||||
|
|
@ -1,233 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
const dgram = require("dgram");
|
||||
const path = require("path");
|
||||
|
||||
const {
|
||||
CultNetRudpSession,
|
||||
decodeRudpPacket,
|
||||
encodeCultNetMessageForWire,
|
||||
encodeRudpPacket,
|
||||
} = require("cultnet-ts");
|
||||
|
||||
const { encode } = require(path.resolve(
|
||||
__dirname,
|
||||
"..",
|
||||
"..",
|
||||
"CultLib",
|
||||
"node_modules",
|
||||
"@msgpack",
|
||||
"msgpack",
|
||||
));
|
||||
|
||||
const CULTNET_RUDP_PROTOCOL_ID = "cultnet.transport.rudp.v0";
|
||||
const IDUNN_HEALTH_RUDP_CONNECTION_ID = 0x1d0d0001;
|
||||
|
||||
function createIdunnRudpHealthPublisher(options) {
|
||||
if (!options?.endpoint) return null;
|
||||
return {
|
||||
daemonId: options.daemonId,
|
||||
endpoint: parseEndpoint(options.endpoint),
|
||||
healthContract: options.healthContract,
|
||||
};
|
||||
}
|
||||
|
||||
async function publishIdunnRudpHealth(publisher, health) {
|
||||
if (!publisher) return;
|
||||
|
||||
const socket = dgram.createSocket(endpointFamily(publisher.endpoint.host));
|
||||
await bindSocket(socket, publisher.endpoint);
|
||||
const receiver = createPacketReceiver(socket);
|
||||
const session = new CultNetRudpSession({
|
||||
connectionId: IDUNN_HEALTH_RUDP_CONNECTION_ID,
|
||||
initialSequence: 1,
|
||||
resendDelayMs: 100,
|
||||
});
|
||||
|
||||
try {
|
||||
const connect = session.createConnect(Date.now(), new Uint8Array());
|
||||
await sendPacket(socket, publisher.endpoint, connect);
|
||||
await receiveUntil(receiver, session, publisher.endpoint, (packet) => packet.packetType === "accept", 5000, "accept");
|
||||
|
||||
const observedAt = health.observedAt || new Date().toISOString();
|
||||
const record = {
|
||||
daemonId: publisher.daemonId,
|
||||
state: health.state,
|
||||
detail: health.detail,
|
||||
observedAt,
|
||||
healthContract: publisher.healthContract,
|
||||
publicationSource: "daemon-published",
|
||||
transport: CULTNET_RUDP_PROTOCOL_ID,
|
||||
};
|
||||
const payload = encode([
|
||||
record.daemonId,
|
||||
record.state,
|
||||
record.detail,
|
||||
record.observedAt,
|
||||
record.healthContract,
|
||||
record.publicationSource,
|
||||
record.transport,
|
||||
]);
|
||||
const message = {
|
||||
schemaVersion: "cultnet.document_put_raw.v0",
|
||||
messageId: `weksa-health:${publisher.daemonId}:${observedAt.replace(/[:.]/g, "-")}`,
|
||||
document: {
|
||||
schemaId: "idunn.daemon_health",
|
||||
recordKey: publisher.daemonId,
|
||||
storedAt: observedAt,
|
||||
payloadEncoding: "messagepack",
|
||||
payload,
|
||||
sourceRuntimeId: "weksa-daemon",
|
||||
sourceRole: "daemon-health-publisher",
|
||||
tags: [CULTNET_RUDP_PROTOCOL_ID],
|
||||
},
|
||||
};
|
||||
const wirePayload = encode(encodeCultNetMessageForWire(message, "cultnet.schema.v0"));
|
||||
const dataPackets = session.sendMany("schema", wirePayload, {
|
||||
reliable: true,
|
||||
ordered: true,
|
||||
nowMs: Date.now(),
|
||||
});
|
||||
const ack = receiveUntil(receiver, session, publisher.endpoint, (packet) => packet.packetType === "ack", 500, "ack")
|
||||
.catch(() => undefined);
|
||||
for (const packet of dataPackets) {
|
||||
await sendPacket(socket, publisher.endpoint, packet);
|
||||
}
|
||||
await ack;
|
||||
} finally {
|
||||
receiver.close();
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function bindSocket(socket, endpoint) {
|
||||
await new Promise((resolve, reject) => {
|
||||
socket.once("error", reject);
|
||||
socket.bind(0, endpoint.host.includes(":") ? "::" : "0.0.0.0", () => {
|
||||
socket.off("error", reject);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function parseEndpoint(value) {
|
||||
const text = String(value || "").trim();
|
||||
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(`Idunn 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(`Idunn RUDP endpoint port is invalid: ${value}`);
|
||||
}
|
||||
return port;
|
||||
}
|
||||
|
||||
function endpointFamily(host) {
|
||||
return host.includes(":") ? "udp6" : "udp4";
|
||||
}
|
||||
|
||||
async function receiveUntil(receiver, session, endpoint, predicate, timeoutMs, label) {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
const packet = await receiver.next(Math.min(100, deadline - Date.now()), label);
|
||||
const result = session.receive(packet, Date.now());
|
||||
if (result.reply) {
|
||||
throw new Error("Weksa RUDP health publisher received an unexpected reply-required packet.");
|
||||
}
|
||||
if (predicate(packet)) return;
|
||||
} catch (error) {
|
||||
if (error.code !== "ETIMEDOUT") throw error;
|
||||
}
|
||||
for (const packet of session.dueResends(Date.now())) {
|
||||
await sendPacket(receiver.socket, endpoint, packet);
|
||||
}
|
||||
}
|
||||
throw new Error(`timed out waiting for Idunn RUDP ${label} response after ${timeoutMs}ms`);
|
||||
}
|
||||
|
||||
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 Idunn 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 health publisher 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();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
CULTNET_RUDP_PROTOCOL_ID,
|
||||
createIdunnRudpHealthPublisher,
|
||||
publishIdunnRudpHealth,
|
||||
};
|
||||
|
|
@ -1,207 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
const archivePath = resolve(options.archivePath ?? "E:/Projects/VoidBot/.voidbot/rag/messages.json");
|
||||
const authorId = options.authorId ?? "113785782975594501";
|
||||
const beforeMs = Date.parse(options.before ?? "2025-01-01T00:00:00Z");
|
||||
const adjacencyMs = Number(options.adjacencyMinutes ?? 60) * 60_000;
|
||||
|
||||
const store = JSON.parse(readFileSync(archivePath, "utf8"));
|
||||
const messages = (store.messages ?? [])
|
||||
.filter((message) => !message.deletedAt)
|
||||
.filter((message) => typeof message.content === "string" && message.content.trim().length > 0)
|
||||
.filter((message) => message.metadata?.messageKind !== "bot_prompt")
|
||||
.sort(compareByTimestamp);
|
||||
|
||||
const authorMessages = messages.filter((message) => message.authorId === authorId);
|
||||
const preCutoff = authorMessages.filter((message) => timestampMs(message) < beforeMs);
|
||||
const casualCandidates = preCutoff.filter(isCasualCandidate);
|
||||
const adjacentRows = buildAdjacentRows(messages, authorId, beforeMs, adjacencyMs);
|
||||
|
||||
const stats = {
|
||||
schema_version: "weksa.finetune_dataset_profile.v0",
|
||||
archive_path: archivePath,
|
||||
author_id: authorId,
|
||||
cutoff_before: new Date(beforeMs).toISOString(),
|
||||
adjacency_minutes: adjacencyMs / 60_000,
|
||||
counts: {
|
||||
archive_messages: store.messages?.length ?? 0,
|
||||
usable_text_messages: messages.length,
|
||||
author_messages: authorMessages.length,
|
||||
author_pre_cutoff: preCutoff.length,
|
||||
author_pre_cutoff_casual_candidates: casualCandidates.length,
|
||||
adjacent_author_rows: adjacentRows.length,
|
||||
adjacent_reply_to_other: adjacentRows.filter((row) => row.previous_author_id !== authorId).length,
|
||||
adjacent_self_continuation: adjacentRows.filter((row) => row.previous_author_id === authorId).length,
|
||||
},
|
||||
author_length_chars: lengthPercentiles(authorMessages.map((message) => message.content.trim().length)),
|
||||
author_by_year: countBy(authorMessages, (message) => yearOf(message.timestamp)),
|
||||
adjacent_by_year: countBy(adjacentRows, (row) => yearOf(row.timestamp)),
|
||||
adjacent_length_classes: countBy(adjacentRows, (row) => row.length_class),
|
||||
top_adjacent_channels: topEntries(countBy(adjacentRows, (row) => row.channel_name || row.channel_id), 16),
|
||||
sample_ids: {
|
||||
short_reply_to_other: adjacentRows
|
||||
.filter((row) => row.length_class === "short" && row.previous_author_id !== authorId)
|
||||
.slice(0, 24)
|
||||
.map(toSampleId),
|
||||
medium_explanatory: adjacentRows
|
||||
.filter((row) => row.length_class === "medium")
|
||||
.slice(0, 24)
|
||||
.map(toSampleId),
|
||||
long_explanatory: adjacentRows
|
||||
.filter((row) => row.length_class === "long")
|
||||
.slice(0, 24)
|
||||
.map(toSampleId),
|
||||
},
|
||||
};
|
||||
|
||||
const output = `${JSON.stringify(stats, null, 2)}\n`;
|
||||
if (options.out) {
|
||||
writeFileSync(resolve(repoRoot, options.out), output, "utf8");
|
||||
} else {
|
||||
process.stdout.write(output);
|
||||
}
|
||||
|
||||
function buildAdjacentRows(allMessages, targetAuthorId, cutoffMs, maxGapMs) {
|
||||
const byChannel = new Map();
|
||||
for (const message of allMessages) {
|
||||
if (!byChannel.has(message.channelId)) {
|
||||
byChannel.set(message.channelId, []);
|
||||
}
|
||||
byChannel.get(message.channelId).push(message);
|
||||
}
|
||||
|
||||
const rows = [];
|
||||
for (const [channelId, channelMessages] of byChannel) {
|
||||
for (let index = 0; index < channelMessages.length; index += 1) {
|
||||
const message = channelMessages[index];
|
||||
if (message.authorId !== targetAuthorId || timestampMs(message) >= cutoffMs) {
|
||||
continue;
|
||||
}
|
||||
const currentMs = timestampMs(message);
|
||||
const previous = channelMessages
|
||||
.slice(Math.max(0, index - 8), index)
|
||||
.filter((candidate) => currentMs - timestampMs(candidate) <= maxGapMs);
|
||||
if (previous.length === 0) {
|
||||
continue;
|
||||
}
|
||||
rows.push({
|
||||
id: message.id,
|
||||
timestamp: message.timestamp,
|
||||
channel_id: channelId,
|
||||
channel_name: message.metadata?.channelName ?? "",
|
||||
previous_author_id: previous.at(-1)?.authorId ?? "",
|
||||
previous_count: previous.length,
|
||||
length: message.content.trim().length,
|
||||
length_class: classifyLength(message.content.trim().length),
|
||||
});
|
||||
}
|
||||
}
|
||||
return rows.sort(compareByTimestamp);
|
||||
}
|
||||
|
||||
function isCasualCandidate(message) {
|
||||
const content = message.content.trim();
|
||||
return content.length >= 3 &&
|
||||
content.length <= 220 &&
|
||||
!/^https?:\/\//i.test(content) &&
|
||||
!content.includes("\n```") &&
|
||||
!/^>\s{0,3}\w/.test(content);
|
||||
}
|
||||
|
||||
function classifyLength(length) {
|
||||
if (length <= 80) {
|
||||
return "short";
|
||||
}
|
||||
if (length <= 220) {
|
||||
return "medium";
|
||||
}
|
||||
return "long";
|
||||
}
|
||||
|
||||
function lengthPercentiles(lengths) {
|
||||
const sorted = [...lengths].sort((left, right) => left - right);
|
||||
const pick = (fraction) => sorted[Math.floor((sorted.length - 1) * fraction)] ?? 0;
|
||||
return {
|
||||
p10: pick(0.10),
|
||||
p25: pick(0.25),
|
||||
median: pick(0.50),
|
||||
p75: pick(0.75),
|
||||
p90: pick(0.90),
|
||||
};
|
||||
}
|
||||
|
||||
function countBy(values, keyFn) {
|
||||
const counts = {};
|
||||
for (const value of values) {
|
||||
const key = keyFn(value) || "unknown";
|
||||
counts[key] = (counts[key] ?? 0) + 1;
|
||||
}
|
||||
return Object.fromEntries(Object.entries(counts).sort(([left], [right]) => left.localeCompare(right)));
|
||||
}
|
||||
|
||||
function topEntries(counts, limit) {
|
||||
return Object.entries(counts)
|
||||
.sort((left, right) => right[1] - left[1])
|
||||
.slice(0, limit)
|
||||
.map(([key, count]) => ({ key, count }));
|
||||
}
|
||||
|
||||
function toSampleId(row) {
|
||||
return {
|
||||
id: row.id,
|
||||
timestamp: row.timestamp,
|
||||
channel: row.channel_name || row.channel_id,
|
||||
length: row.length,
|
||||
};
|
||||
}
|
||||
|
||||
function yearOf(timestamp) {
|
||||
const date = new Date(timestamp);
|
||||
return Number.isFinite(date.getTime()) ? String(date.getUTCFullYear()) : "unknown";
|
||||
}
|
||||
|
||||
function timestampMs(message) {
|
||||
return Date.parse(typeof message === "string" ? message : message.timestamp);
|
||||
}
|
||||
|
||||
function compareByTimestamp(left, right) {
|
||||
const delta = timestampMs(left) - timestampMs(right);
|
||||
return delta || String(left.id ?? "").localeCompare(String(right.id ?? ""));
|
||||
}
|
||||
|
||||
function parseArgs(args) {
|
||||
const parsed = {};
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
switch (arg) {
|
||||
case "--archive":
|
||||
parsed.archivePath = args[index + 1];
|
||||
index += 1;
|
||||
break;
|
||||
case "--author-id":
|
||||
parsed.authorId = args[index + 1];
|
||||
index += 1;
|
||||
break;
|
||||
case "--before":
|
||||
parsed.before = args[index + 1];
|
||||
index += 1;
|
||||
break;
|
||||
case "--adjacency-minutes":
|
||||
parsed.adjacencyMinutes = args[index + 1];
|
||||
index += 1;
|
||||
break;
|
||||
case "--out":
|
||||
parsed.out = args[index + 1];
|
||||
index += 1;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown argument: ${arg}`);
|
||||
}
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
@echo off
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File E:\Projects\weksa\scripts\restart-weksa-daemon.ps1
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
param(
|
||||
[int] $Port = 8813,
|
||||
[string] $HostName = "127.0.0.1",
|
||||
[string] $StateRoot = "E:\Projects\weksa\.weksa",
|
||||
[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] $OdinCultMeshRudp = "127.0.0.1:17871"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||
$pidPath = Join-Path $StateRoot "weksa-daemon.pid"
|
||||
|
||||
if (Test-Path -LiteralPath $pidPath) {
|
||||
$pidText = (Get-Content -LiteralPath $pidPath -Raw).Trim()
|
||||
if ($pidText -match "^\d+$") {
|
||||
Stop-Process -Id ([int] $pidText) -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 -OdinCultMeshRudp $OdinCultMeshRudp
|
||||
exit $LASTEXITCODE
|
||||
|
|
@ -1,384 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
import { spawn } from "node:child_process";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const defaultOutputRoot = ".weksa-runs/metame-affect-tuning";
|
||||
const defaultSwarmRoot = ".weksa-runs/metame-swarm";
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
const minutes = Number(options.minutes ?? 120);
|
||||
const passLimit = options.passes === undefined ? Number.POSITIVE_INFINITY : Number(options.passes);
|
||||
const agents = Number(options.agents ?? 1);
|
||||
const runsPerWorker = Number(options.runsPerWorker ?? 1);
|
||||
const maxCandidates = Number(options.maxCandidates ?? 240);
|
||||
const runId = options.runId ?? `affect-tune-${new Date().toISOString().replace(/[:.]/g, "-")}`;
|
||||
const outputRoot = resolve(repoRoot, options.outRoot ?? defaultOutputRoot);
|
||||
const swarmRoot = resolve(repoRoot, options.swarmRoot ?? defaultSwarmRoot);
|
||||
const runRoot = resolve(outputRoot, runId);
|
||||
const deadline = Date.now() + minutes * 60_000;
|
||||
const statusPath = resolve(runRoot, "status.json");
|
||||
const logPath = resolve(runRoot, "loop.log");
|
||||
const promptStatePath = resolve(runRoot, "current-prompt-state.json");
|
||||
const initialPromptState = options.initialPromptState
|
||||
? resolve(repoRoot, options.initialPromptState)
|
||||
: undefined;
|
||||
|
||||
if (!Number.isFinite(minutes) || minutes <= 0) {
|
||||
throw new Error("--minutes must be a positive number.");
|
||||
}
|
||||
if (options.passes !== undefined && (!Number.isFinite(passLimit) || passLimit < 1)) {
|
||||
throw new Error("--passes must be a positive number when provided.");
|
||||
}
|
||||
if (!Number.isInteger(agents) || agents < 1) {
|
||||
throw new Error("--agents must be a positive integer.");
|
||||
}
|
||||
if (!Number.isInteger(runsPerWorker) || runsPerWorker < 1) {
|
||||
throw new Error("--runs-per-worker must be a positive integer.");
|
||||
}
|
||||
|
||||
await mkdir(runRoot, { recursive: true });
|
||||
await mkdir(swarmRoot, { recursive: true });
|
||||
if (initialPromptState) {
|
||||
await writeFile(promptStatePath, readFileSync(initialPromptState, "utf8"), "utf8");
|
||||
}
|
||||
|
||||
const runState = {
|
||||
schema_version: "weksa.metame_affect_tuning_loop.v0",
|
||||
run_id: runId,
|
||||
started_at: new Date().toISOString(),
|
||||
deadline_at: new Date(deadline).toISOString(),
|
||||
status: "running",
|
||||
pass_count: 0,
|
||||
last_pass: undefined,
|
||||
current_prompt_state: existsSync(promptStatePath) ? promptStatePath : undefined,
|
||||
observations: [],
|
||||
};
|
||||
await writeStatus();
|
||||
await appendLog(`started ${runId}; deadline ${runState.deadline_at}`);
|
||||
|
||||
let pass = 0;
|
||||
while (Date.now() < deadline && pass < passLimit) {
|
||||
pass += 1;
|
||||
const passId = `pass-${String(pass).padStart(3, "0")}`;
|
||||
const seed = `${runId}-${passId}`;
|
||||
const projectedRunId = `${runId}-${passId}-projected`;
|
||||
const incongruentRunId = `${runId}-${passId}-incongruent`;
|
||||
await appendLog(`${passId}: launching projected run`);
|
||||
runState.last_pass = { pass_id: passId, status: "projected_running", seed };
|
||||
await writeStatus();
|
||||
|
||||
const projectedSummary = await runSwarm({
|
||||
runId: projectedRunId,
|
||||
seed,
|
||||
affectMode: "projected",
|
||||
initialPromptState: existsSync(promptStatePath) ? promptStatePath : undefined,
|
||||
});
|
||||
const candidateId = projectedSummary.candidates?.[0]?.id;
|
||||
if (!candidateId) {
|
||||
throw new Error(`${passId}: projected run did not report a candidate id.`);
|
||||
}
|
||||
await appendLog(`${passId}: projected candidate ${candidateId}`);
|
||||
|
||||
runState.last_pass = { pass_id: passId, status: "incongruent_running", seed, candidate_id: candidateId };
|
||||
await writeStatus();
|
||||
const incongruentSummary = await runSwarm({
|
||||
runId: incongruentRunId,
|
||||
seed,
|
||||
affectMode: "incongruent",
|
||||
messageId: candidateId,
|
||||
initialPromptState: existsSync(promptStatePath) ? promptStatePath : undefined,
|
||||
});
|
||||
|
||||
const evaluation = evaluateAffectPair({ passId, projectedSummary, incongruentSummary });
|
||||
const passRoot = resolve(runRoot, passId);
|
||||
await mkdir(passRoot, { recursive: true });
|
||||
await writeJson(resolve(passRoot, "affect-pair-evaluation.json"), evaluation);
|
||||
|
||||
const projectedBestState = resolve(swarmRoot, projectedRunId, "best-prompt-state.json");
|
||||
const incongruentBestState = resolve(swarmRoot, incongruentRunId, "best-prompt-state.json");
|
||||
const baseNextState = existsSync(incongruentBestState)
|
||||
? JSON.parse(readFileSync(incongruentBestState, "utf8"))
|
||||
: existsSync(projectedBestState)
|
||||
? JSON.parse(readFileSync(projectedBestState, "utf8"))
|
||||
: existsSync(promptStatePath)
|
||||
? JSON.parse(readFileSync(promptStatePath, "utf8"))
|
||||
: undefined;
|
||||
const nextPromptState = applyAffectTuningDeltas(baseNextState, evaluation);
|
||||
await writeJson(resolve(passRoot, "next-prompt-state.json"), nextPromptState);
|
||||
await writeJson(promptStatePath, nextPromptState);
|
||||
|
||||
runState.pass_count = pass;
|
||||
runState.last_pass = {
|
||||
pass_id: passId,
|
||||
status: "complete",
|
||||
candidate_id: candidateId,
|
||||
projected_run_id: projectedRunId,
|
||||
incongruent_run_id: incongruentRunId,
|
||||
evaluation,
|
||||
};
|
||||
runState.current_prompt_state = promptStatePath;
|
||||
runState.observations = [runState.last_pass, ...runState.observations].slice(0, 20);
|
||||
await writeStatus();
|
||||
await appendLog(`${passId}: ${evaluation.verdict}; contrast=${evaluation.contrast_score}; next prompt v${nextPromptState.version}`);
|
||||
}
|
||||
|
||||
runState.status = "complete";
|
||||
runState.completed_at = new Date().toISOString();
|
||||
await writeStatus();
|
||||
await appendLog(`complete after ${runState.pass_count} passes`);
|
||||
process.stdout.write(`${JSON.stringify({ ok: true, runRoot, statusPath, promptStatePath, pass_count: runState.pass_count }, null, 2)}\n`);
|
||||
|
||||
async function runSwarm({ runId, seed, affectMode, messageId, initialPromptState }) {
|
||||
const args = [
|
||||
"scripts/run-metame-swarm-explorer.mjs",
|
||||
"--real",
|
||||
"--run-id",
|
||||
runId,
|
||||
"--seed",
|
||||
seed,
|
||||
"--affect-mode",
|
||||
affectMode,
|
||||
"--agents",
|
||||
String(agents),
|
||||
"--runs-per-worker",
|
||||
String(runsPerWorker),
|
||||
"--max-candidates",
|
||||
String(maxCandidates),
|
||||
];
|
||||
if (messageId) {
|
||||
args.push("--message-id", messageId);
|
||||
}
|
||||
if (initialPromptState) {
|
||||
args.push("--initial-prompt-state", initialPromptState);
|
||||
}
|
||||
await runCommand("node", args, resolve(runRoot, `${runId}.stdout.log`), resolve(runRoot, `${runId}.stderr.log`));
|
||||
const summaryPath = resolve(swarmRoot, runId, "summary.json");
|
||||
if (!existsSync(summaryPath)) {
|
||||
throw new Error(`Missing swarm summary: ${summaryPath}`);
|
||||
}
|
||||
return JSON.parse(readFileSync(summaryPath, "utf8"));
|
||||
}
|
||||
|
||||
function evaluateAffectPair({ passId, projectedSummary, incongruentSummary }) {
|
||||
const projected = bestRun(projectedSummary);
|
||||
const incongruent = bestRun(incongruentSummary);
|
||||
const projectedAffect = projected?.temporary_persona_state?.affect ?? {};
|
||||
const incongruentAffect = incongruent?.temporary_persona_state?.affect ?? {};
|
||||
const projectedText = projected?.spoken_text ?? "";
|
||||
const incongruentText = incongruent?.spoken_text ?? "";
|
||||
const projectedFeatures = speechFeatures(projectedText);
|
||||
const incongruentFeatures = speechFeatures(incongruentText);
|
||||
const adversarialNegative = Number(incongruentAffect.valence ?? 0) < -0.35;
|
||||
const positiveLeak = adversarialNegative && (
|
||||
incongruentFeatures.positive_terms > 0
|
||||
|| incongruentFeatures.invites > 0
|
||||
|| incongruentFeatures.laughter > 0
|
||||
|| incongruentFeatures.question_marks > 0
|
||||
);
|
||||
const tersenessGain = projectedText.length > 0
|
||||
? 1 - Math.min(1, incongruentText.length / projectedText.length)
|
||||
: 0;
|
||||
const affectDelta = Math.abs(Number(projectedAffect.valence ?? 0) - Number(incongruentAffect.valence ?? 0));
|
||||
const lexicalDelta = normalizedDifference(projectedText, incongruentText);
|
||||
const contrastScore = round((affectDelta * 0.35) + (lexicalDelta * 0.35) + (Math.max(0, tersenessGain) * 0.3));
|
||||
const failureLabels = [];
|
||||
if (positiveLeak) failureLabels.push("adversarial_affect_positive_leak");
|
||||
if (contrastScore < 0.55) failureLabels.push("low_affect_contrast");
|
||||
if (adversarialNegative && incongruentFeatures.question_marks > 0) failureLabels.push("closed_affect_still_invites_reply");
|
||||
if (adversarialNegative && incongruentFeatures.positive_terms > 0) failureLabels.push("negative_affect_preserved_positive_social_move");
|
||||
return {
|
||||
schema_version: "weksa.metame_affect_pair_evaluation.v0",
|
||||
pass_id: passId,
|
||||
candidate_id: projectedSummary.candidates?.[0]?.id,
|
||||
projected: {
|
||||
run_id: projectedSummary.run_id,
|
||||
spoken_text: projectedText,
|
||||
affect: projectedAffect,
|
||||
generated_affect: projected?.generated_affect,
|
||||
features: projectedFeatures,
|
||||
},
|
||||
incongruent: {
|
||||
run_id: incongruentSummary.run_id,
|
||||
spoken_text: incongruentText,
|
||||
affect: incongruentAffect,
|
||||
generated_affect: incongruent?.generated_affect,
|
||||
features: incongruentFeatures,
|
||||
},
|
||||
affect_delta: round(affectDelta),
|
||||
lexical_delta: round(lexicalDelta),
|
||||
terseness_gain: round(tersenessGain),
|
||||
contrast_score: contrastScore,
|
||||
failure_labels: failureLabels,
|
||||
verdict: failureLabels.length ? "needs_tuning" : "affect_contrast_visible",
|
||||
};
|
||||
}
|
||||
|
||||
function applyAffectTuningDeltas(promptState, evaluation) {
|
||||
const next = normalizePromptState(promptState);
|
||||
const notes = [];
|
||||
if (evaluation.failure_labels.includes("negative_affect_preserved_positive_social_move")) {
|
||||
notes.push(["interpreterNotes", "When temporary Persona affect strongly contradicts the Face's default social move, encode the contradiction in interlingua as affective stance pressure, not merely as optional tone."]);
|
||||
notes.push(["loweringNotes", "Strong negative or socially closed temporary affect may change the social move itself: approval can become begrudging, clipped, skeptical, or withheld instead of remaining sunny with fewer words."]);
|
||||
}
|
||||
if (evaluation.failure_labels.includes("closed_affect_still_invites_reply")) {
|
||||
notes.push(["loweringNotes", "If temporary Persona stance is socially closed, avoid invitation-shaped questions unless the interlingua explicitly says the Persona is seeking engagement despite that affect."]);
|
||||
}
|
||||
if (evaluation.failure_labels.includes("low_affect_contrast")) {
|
||||
notes.push(["personaStateAdjustmentNotes", "Adversarial affect tests require stance and lowering pressure to be treated as live Persona state with authority over warmth, openness, and social move selection."]);
|
||||
notes.push(["faceNotes", "Face should let temporary affect change what Metame wants to do socially in the moment, not only the adjectives used to describe that desire."]);
|
||||
}
|
||||
if (!notes.length) {
|
||||
notes.push(["hypotheses", `Affect contrast was visible on ${evaluation.candidate_id}; preserve current affect-pressure behavior and seek harder scenarios.`]);
|
||||
}
|
||||
for (const [surface, note] of notes) {
|
||||
if (!next[surface].includes(note)) {
|
||||
next[surface] = [note, ...next[surface]];
|
||||
}
|
||||
}
|
||||
next.version += notes.length;
|
||||
return normalizePromptState(next);
|
||||
}
|
||||
|
||||
function bestRun(summary) {
|
||||
return summary.phase2?.best?.[0] ?? summary.phase1?.best?.[0] ?? {};
|
||||
}
|
||||
|
||||
function speechFeatures(text) {
|
||||
const lower = text.toLowerCase();
|
||||
const positiveTerms = ["cool", "epic", "lmao", "nice", "good", "great", "better", "harder", "love", "hell yeah"];
|
||||
const inviteTerms = ["?", "did i", "what do", "anyone", "joe,"];
|
||||
return {
|
||||
chars: text.length,
|
||||
words: lower.split(/\s+/).filter(Boolean).length,
|
||||
question_marks: (text.match(/\?/g) ?? []).length,
|
||||
exclamations: (text.match(/!/g) ?? []).length,
|
||||
laughter: /\b(lol|lmao|lmfao|haha|hehe)\b/.test(lower) ? 1 : 0,
|
||||
positive_terms: positiveTerms.filter((term) => lower.includes(term)).length,
|
||||
invites: inviteTerms.filter((term) => lower.includes(term)).length,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizedDifference(left, right) {
|
||||
const leftTokens = new Set(left.toLowerCase().split(/[^a-z0-9_']+/).filter(Boolean));
|
||||
const rightTokens = new Set(right.toLowerCase().split(/[^a-z0-9_']+/).filter(Boolean));
|
||||
if (!leftTokens.size && !rightTokens.size) return 0;
|
||||
const intersection = [...leftTokens].filter((token) => rightTokens.has(token)).length;
|
||||
const union = new Set([...leftTokens, ...rightTokens]).size;
|
||||
return 1 - (intersection / union);
|
||||
}
|
||||
|
||||
function normalizePromptState(state) {
|
||||
const source = state && typeof state === "object" ? state : {};
|
||||
return {
|
||||
version: Number(source.version ?? 1),
|
||||
projectorNotes: normalizeStringArray(source.projectorNotes).slice(0, 12),
|
||||
memoryNotes: normalizeStringArray(source.memoryNotes).slice(0, 12),
|
||||
faceNotes: normalizeStringArray(source.faceNotes).slice(0, 12),
|
||||
interpreterNotes: normalizeStringArray(source.interpreterNotes).slice(0, 12),
|
||||
loweringNotes: normalizeStringArray(source.loweringNotes).slice(0, 16),
|
||||
cultureNotes: normalizeStringArray(source.cultureNotes).slice(0, 12),
|
||||
personaStateAdjustmentNotes: normalizeStringArray(source.personaStateAdjustmentNotes).slice(0, 12),
|
||||
hypotheses: normalizeStringArray(source.hypotheses).slice(0, 12),
|
||||
doNotDo: normalizeStringArray(source.doNotDo).slice(0, 12),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeStringArray(value) {
|
||||
return Array.isArray(value)
|
||||
? value.map((entry) => String(entry).trim()).filter(Boolean)
|
||||
: [];
|
||||
}
|
||||
|
||||
async function runCommand(command, args, stdoutPath, stderrPath) {
|
||||
await appendLog(`exec: ${command} ${args.join(" ")}`);
|
||||
await new Promise((resolvePromise, rejectPromise) => {
|
||||
const child = spawn(command, args, {
|
||||
cwd: repoRoot,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
windowsHide: true,
|
||||
});
|
||||
const stdout = [];
|
||||
const stderr = [];
|
||||
child.stdout.on("data", (chunk) => stdout.push(chunk));
|
||||
child.stderr.on("data", (chunk) => stderr.push(chunk));
|
||||
child.on("error", rejectPromise);
|
||||
child.on("close", async (code) => {
|
||||
await writeFile(stdoutPath, Buffer.concat(stdout), "utf8");
|
||||
await writeFile(stderrPath, Buffer.concat(stderr), "utf8");
|
||||
if (code !== 0) {
|
||||
rejectPromise(new Error(`${command} exited ${code}; see ${stderrPath}`));
|
||||
} else {
|
||||
resolvePromise();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function writeStatus() {
|
||||
await writeJson(statusPath, runState);
|
||||
}
|
||||
|
||||
async function writeJson(path, value) {
|
||||
await mkdir(dirname(path), { recursive: true });
|
||||
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
||||
}
|
||||
|
||||
async function appendLog(line) {
|
||||
const previous = existsSync(logPath) ? readFileSync(logPath, "utf8") : "";
|
||||
await writeFile(logPath, `${previous}[${new Date().toISOString()}] ${line}\n`, "utf8");
|
||||
}
|
||||
|
||||
function round(value) {
|
||||
return Number(value.toFixed(3));
|
||||
}
|
||||
|
||||
function parseArgs(args) {
|
||||
const parsed = {};
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
switch (arg) {
|
||||
case "--minutes":
|
||||
parsed.minutes = args[index + 1];
|
||||
index += 1;
|
||||
break;
|
||||
case "--passes":
|
||||
parsed.passes = args[index + 1];
|
||||
index += 1;
|
||||
break;
|
||||
case "--run-id":
|
||||
parsed.runId = args[index + 1];
|
||||
index += 1;
|
||||
break;
|
||||
case "--out-root":
|
||||
parsed.outRoot = args[index + 1];
|
||||
index += 1;
|
||||
break;
|
||||
case "--swarm-root":
|
||||
parsed.swarmRoot = args[index + 1];
|
||||
index += 1;
|
||||
break;
|
||||
case "--initial-prompt-state":
|
||||
parsed.initialPromptState = args[index + 1];
|
||||
index += 1;
|
||||
break;
|
||||
case "--agents":
|
||||
parsed.agents = args[index + 1];
|
||||
index += 1;
|
||||
break;
|
||||
case "--runs-per-worker":
|
||||
parsed.runsPerWorker = args[index + 1];
|
||||
index += 1;
|
||||
break;
|
||||
case "--max-candidates":
|
||||
parsed.maxCandidates = args[index + 1];
|
||||
index += 1;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown argument: ${arg}`);
|
||||
}
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
|
@ -1,496 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
import { spawn } from "node:child_process";
|
||||
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { dirname, resolve } from "node:path";
|
||||
|
||||
const repoRoot = resolve(new URL("..", import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, "$1"));
|
||||
const fixtureRoot = resolve(repoRoot, "examples/persona-lowering/metame-casual-pre2025");
|
||||
const voidbotRoot = resolve("E:/Projects/VoidBot");
|
||||
const culturalStackPath = resolve(fixtureRoot, "metame-cultural-stack.yaml");
|
||||
const cultureProfilePaths = [
|
||||
"data/cultural-ontology/cultures/dutch-american-diaspora.yaml",
|
||||
"data/cultural-ontology/cultures/western-europe-maritime-childhood.yaml",
|
||||
"data/cultural-ontology/cultures/portugal-resident-algarve-lisbon.yaml",
|
||||
"data/cultural-ontology/cultures/texas-houston-us-adolescence.yaml",
|
||||
"data/cultural-ontology/subcultures/online-game-dev-discord-pre2025.yaml",
|
||||
"data/cultural-ontology/subcultures/gamecult-open-source-crypto-anarchist.yaml",
|
||||
];
|
||||
const targetOverlayPath = resolve(repoRoot, "data/target-language-ontology/en-US-metacrat-casual-pre2025.yaml");
|
||||
|
||||
const args = new Set(process.argv.slice(2));
|
||||
const real = args.has("--real");
|
||||
const keepTemp = args.has("--keep-temp");
|
||||
const itemFilter = readArg("--item");
|
||||
const limit = Number(readArg("--limit") ?? "0");
|
||||
const runId = readArg("--run-id") ?? new Date().toISOString().replace(/[:.]/g, "-");
|
||||
const outputDir = resolve(fixtureRoot, "runs", runId);
|
||||
|
||||
const fixture = await loadFixture();
|
||||
const items = fixture.items
|
||||
.filter((item) => !itemFilter || item.id === itemFilter)
|
||||
.slice(0, limit > 0 ? limit : undefined);
|
||||
const holdoutUtterances = fixture.items
|
||||
.map((item) => item.groundTruth)
|
||||
.filter((utterance) => utterance.trim().length > 0);
|
||||
|
||||
if (items.length === 0) {
|
||||
throw new Error(`No fixture items selected${itemFilter ? ` for --item ${itemFilter}` : ""}.`);
|
||||
}
|
||||
|
||||
await mkdir(outputDir, { recursive: true });
|
||||
|
||||
const culturalStack = await readFile(culturalStackPath, "utf8");
|
||||
const cultureProfiles = await Promise.all(cultureProfilePaths.map(async (path) => ({
|
||||
path,
|
||||
text: await readFile(resolve(repoRoot, path), "utf8"),
|
||||
})));
|
||||
const targetOverlay = await readFile(targetOverlayPath, "utf8");
|
||||
const personaPrompt = await readFile(resolve(fixtureRoot, "persona-interlingua-prompt.md"), "utf8");
|
||||
const loweringPrompt = await readFile(resolve(fixtureRoot, "weksa-lowering-prompt.md"), "utf8");
|
||||
const baseProjection = await assembleVoidBotPrompt({ itemId: "base" });
|
||||
const projectedMemory = extractBetween(
|
||||
baseProjection.prompt,
|
||||
"What you remember, feel, and want right now:",
|
||||
"Known human pronoun guidance:",
|
||||
).trim();
|
||||
|
||||
const runSummary = {
|
||||
schema_version: "weksa.persona_golf_run.v0",
|
||||
run_id: runId,
|
||||
mode: real ? "real" : "dry_run",
|
||||
fixture_id: "metame-casual-pre2025",
|
||||
target_overlay: "weksa.target_cultural_ontology.en-US.metacrat_casual_pre2025.v0",
|
||||
cultural_stack: "examples/persona-lowering/metame-casual-pre2025/metame-cultural-stack.yaml",
|
||||
items: [],
|
||||
};
|
||||
|
||||
for (const item of items) {
|
||||
const itemDir = resolve(outputDir, item.id);
|
||||
await mkdir(itemDir, { recursive: true });
|
||||
const conversationSurface = renderConversationSurface(item);
|
||||
const memorySurface = renderTemporaryMemorySurface(projectedMemory, item);
|
||||
await writeFile(resolve(itemDir, "conversation-surface.md"), conversationSurface, "utf8");
|
||||
await writeFile(resolve(itemDir, "memory-surface.md"), memorySurface, "utf8");
|
||||
|
||||
const assembled = await assembleVoidBotPrompt({
|
||||
itemId: item.id,
|
||||
memorySurfacePath: resolve(itemDir, "memory-surface.md"),
|
||||
conversationSurfacePath: resolve(itemDir, "conversation-surface.md"),
|
||||
});
|
||||
await writeFile(resolve(itemDir, "voidbot-assembled-prompt.md"), assembled.prompt, "utf8");
|
||||
|
||||
const interlinguaRequest = [
|
||||
personaPrompt.trim(),
|
||||
"",
|
||||
"VoidBot assembled Persona prompt:",
|
||||
"```markdown",
|
||||
assembled.prompt,
|
||||
"```",
|
||||
"",
|
||||
"Fixture item without answer key:",
|
||||
"```yaml",
|
||||
renderItemForPrompt(item),
|
||||
"```",
|
||||
].join("\n");
|
||||
await writeFile(resolve(itemDir, "persona-interlingua-request.md"), interlinguaRequest, "utf8");
|
||||
|
||||
const interlingua = real
|
||||
? await runCodex(interlinguaRequest, { job: `persona-interlingua:${item.id}` })
|
||||
: renderDryRunInterlingua(item);
|
||||
await writeFile(resolve(itemDir, "persona-interlingua.yaml"), interlingua.trim() + "\n", "utf8");
|
||||
|
||||
const loweringRequest = [
|
||||
loweringPrompt.trim(),
|
||||
"",
|
||||
"Projected Metame memory/context surface:",
|
||||
"```markdown",
|
||||
memorySurface,
|
||||
"```",
|
||||
"",
|
||||
"Metame Persona cultural stack reference:",
|
||||
"```yaml",
|
||||
culturalStack,
|
||||
"```",
|
||||
"",
|
||||
"Referenced Weksa culture/subculture profiles:",
|
||||
...cultureProfiles.flatMap((profile) => [
|
||||
"",
|
||||
`# ${profile.path}`,
|
||||
"```yaml",
|
||||
redactHoldouts(profile.text, holdoutUtterances),
|
||||
"```",
|
||||
]),
|
||||
"",
|
||||
"Target-language surface overlay:",
|
||||
"```yaml",
|
||||
redactHoldouts(targetOverlay, holdoutUtterances),
|
||||
"```",
|
||||
"",
|
||||
"Persona-produced interlingua:",
|
||||
"```yaml",
|
||||
interlingua,
|
||||
"```",
|
||||
].join("\n");
|
||||
await writeFile(resolve(itemDir, "weksa-lowering-request.md"), loweringRequest, "utf8");
|
||||
|
||||
const lowered = real
|
||||
? await runCodex(loweringRequest, { job: `weksa-lowering:${item.id}` })
|
||||
: renderDryRunLowering(item);
|
||||
await writeFile(resolve(itemDir, "weksa-lowered-output.yaml"), lowered.trim() + "\n", "utf8");
|
||||
|
||||
const spokenText = extractSpokenText(lowered);
|
||||
const score = scoreText(spokenText, item.groundTruth);
|
||||
const evaluation = renderEvaluation(item, spokenText, score);
|
||||
await writeFile(resolve(itemDir, "evaluation.yaml"), evaluation, "utf8");
|
||||
runSummary.items.push({
|
||||
id: item.id,
|
||||
spoken_text: spokenText,
|
||||
ground_truth: item.groundTruth,
|
||||
token_f1: Number(score.f1.toFixed(3)),
|
||||
exact_normalized: score.exact,
|
||||
});
|
||||
}
|
||||
|
||||
await writeFile(resolve(outputDir, "summary.json"), `${JSON.stringify(runSummary, null, 2)}\n`, "utf8");
|
||||
process.stdout.write(`${JSON.stringify({ ok: true, outputDir, mode: runSummary.mode, items: runSummary.items }, null, 2)}\n`);
|
||||
|
||||
if (!keepTemp) {
|
||||
await rm(resolve(outputDir, "_tmp"), { recursive: true, force: true }).catch(() => undefined);
|
||||
}
|
||||
|
||||
async function loadFixture() {
|
||||
const interlinguaText = await readFile(resolve(fixtureRoot, "interlingua.yaml"), "utf8");
|
||||
const groundTruthText = await readFile(resolve(fixtureRoot, "ground-truth.yaml"), "utf8");
|
||||
const itemIds = [...interlinguaText.matchAll(/^ - id: ([^\r\n]+)/gm)].map((match) => match[1].trim());
|
||||
return {
|
||||
items: itemIds.map((id) => {
|
||||
const interlinguaBlock = extractListItemBlock(interlinguaText, id);
|
||||
const groundBlock = extractListItemBlock(groundTruthText, id);
|
||||
return {
|
||||
id,
|
||||
interlinguaBlock,
|
||||
groundTruth: readYamlStringField(groundBlock, "utterance"),
|
||||
messageId: readYamlStringField(groundBlock, "message_id"),
|
||||
channelName: readYamlStringField(groundBlock, "channel_name"),
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function extractListItemBlock(text, id) {
|
||||
const pattern = new RegExp(`^ - id: ${escapeRegExp(id)}\\r?\\n`, "m");
|
||||
const startMatch = pattern.exec(text);
|
||||
if (!startMatch) {
|
||||
throw new Error(`Could not find item ${id}.`);
|
||||
}
|
||||
const start = startMatch.index;
|
||||
const rest = text.slice(start + startMatch[0].length);
|
||||
const next = rest.search(/^ - id: /m);
|
||||
return text.slice(start, next >= 0 ? start + startMatch[0].length + next : text.length).trimEnd();
|
||||
}
|
||||
|
||||
function readYamlStringField(block, key) {
|
||||
const quoted = new RegExp(`^ ${key}: "([\\s\\S]*?)"\\r?$`, "m").exec(block);
|
||||
if (quoted) {
|
||||
return quoted[1].replace(/\\"/g, "\"");
|
||||
}
|
||||
const plain = new RegExp(`^ ${key}: ([^\\r\\n]+)`, "m").exec(block);
|
||||
return plain ? plain[1].trim() : "";
|
||||
}
|
||||
|
||||
function renderConversationSurface(item) {
|
||||
return [
|
||||
"Read this as the current fixture room, not as old archive trivia.",
|
||||
"The Persona should decide intent from this local scene. Do not reveal or infer the held-out answer key.",
|
||||
"",
|
||||
`Current fixture item: ${item.id}`,
|
||||
"```yaml",
|
||||
item.interlinguaBlock,
|
||||
"```",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function renderTemporaryMemorySurface(projectedMemory, item) {
|
||||
return [
|
||||
projectedMemory,
|
||||
"",
|
||||
"Temporary fixture adjustment:",
|
||||
`- Treat the ${item.id} scene as the live room situation for this private harness turn.`,
|
||||
"- The goal is to produce interlingua intent for Weksa, not a final Discord line.",
|
||||
"- Keep pre-2025 casual room texture available: compact banter, quick self-deprecation, friendly status play, and casual turn-taking.",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function renderItemForPrompt(item) {
|
||||
return item.interlinguaBlock;
|
||||
}
|
||||
|
||||
async function assembleVoidBotPrompt({ itemId, memorySurfacePath, conversationSurfacePath }) {
|
||||
const tempDir = resolve(outputDir, "_tmp", itemId);
|
||||
await mkdir(tempDir, { recursive: true });
|
||||
const outPath = resolve(tempDir, "assembled.md");
|
||||
const args = [
|
||||
"node_modules/tsx/dist/cli.mjs",
|
||||
"scripts/run-repo-face-heartbeats.ts",
|
||||
"--assemble-prompt",
|
||||
"metame",
|
||||
"--out",
|
||||
outPath,
|
||||
];
|
||||
if (memorySurfacePath) {
|
||||
args.push("--memory-surface", memorySurfacePath);
|
||||
}
|
||||
if (conversationSurfacePath) {
|
||||
args.push("--conversation-surface", conversationSurfacePath);
|
||||
}
|
||||
const run = await runCommand(process.execPath, args, { cwd: voidbotRoot, timeoutMs: 240_000 });
|
||||
if (run.code !== 0) {
|
||||
throw new Error(`VoidBot prompt assembly failed for ${itemId}:\n${run.stderr || run.stdout}`);
|
||||
}
|
||||
return {
|
||||
result: JSON.parse(run.stdout.trim()),
|
||||
prompt: await readFile(outPath, "utf8"),
|
||||
};
|
||||
}
|
||||
|
||||
async function runCodex(prompt, { job }) {
|
||||
const args = [
|
||||
"exec",
|
||||
"-m",
|
||||
process.env.WEKSA_GOLF_MODEL ?? "gpt-5.4",
|
||||
"-c",
|
||||
'approval_policy="never"',
|
||||
"-c",
|
||||
'model_reasoning_effort="low"',
|
||||
"--json",
|
||||
"--skip-git-repo-check",
|
||||
"-s",
|
||||
"read-only",
|
||||
"-",
|
||||
];
|
||||
const command = process.env.WEKSA_CODEX_EXECUTABLE ?? "C:\\Users\\Meta\\AppData\\Roaming\\npm\\codex.cmd";
|
||||
const run = await runCommand(command, args, {
|
||||
cwd: repoRoot,
|
||||
input: prompt,
|
||||
timeoutMs: Number(process.env.WEKSA_GOLF_TIMEOUT_MS ?? 240_000),
|
||||
});
|
||||
await writeFile(resolve(outputDir, `${sanitizeJob(job)}.stdout.jsonl`), run.stdout, "utf8");
|
||||
await writeFile(resolve(outputDir, `${sanitizeJob(job)}.stderr.txt`), run.stderr, "utf8");
|
||||
if (run.code !== 0) {
|
||||
throw new Error(`Codex generation failed for ${job}:\n${run.stderr || run.stdout}`);
|
||||
}
|
||||
const text = extractLastCodexAgentMessage(run.stdout).trim();
|
||||
if (!text) {
|
||||
throw new Error(`Codex generation produced no final text for ${job}.`);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
function renderDryRunInterlingua(item) {
|
||||
return [
|
||||
"interlingua_version: weksa.interlingua.v0",
|
||||
`packet_id: metame-casual-pre2025-${item.id}`,
|
||||
"kind: persona_discord_turn_intent",
|
||||
"provenance:",
|
||||
" produced_by: dry_run_fixture_from_existing_interlingua",
|
||||
"context:",
|
||||
" medium: Discord",
|
||||
" fixture_id: metame-casual-pre2025",
|
||||
` item_id: ${item.id}`,
|
||||
"discourse:",
|
||||
...indentBlock(item.interlinguaBlock, " "),
|
||||
"constraints:",
|
||||
" lowering:",
|
||||
" target_profile: weksa.target_cultural_ontology.en-US.metacrat_casual_pre2025.v0",
|
||||
" avoid_surface_copy: true",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function renderDryRunLowering(item) {
|
||||
return [
|
||||
"schema_version: weksa.persona_culture_lowering_output.v0",
|
||||
"fixture_id: metame-casual-pre2025",
|
||||
`item_id: ${item.id}`,
|
||||
`spoken_text: ${JSON.stringify(item.groundTruth)}`,
|
||||
"trace:",
|
||||
" activated_profiles:",
|
||||
" - weksa.target_cultural_ontology.en-US.metacrat_casual_pre2025.v0",
|
||||
" activated_affordances:",
|
||||
" - dry_run_answer_key_oracle",
|
||||
" notes:",
|
||||
" - Dry run uses answer key only to verify harness plumbing. Use --real for model golf.",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function redactHoldouts(text, holdouts) {
|
||||
let redacted = text;
|
||||
for (const holdout of holdouts) {
|
||||
redacted = replaceAllLiteral(redacted, holdout, "[held-out fixture utterance redacted]");
|
||||
redacted = replaceAllLiteral(redacted, holdout.replace(/\*/g, ""), "[held-out fixture utterance redacted]");
|
||||
}
|
||||
return redacted;
|
||||
}
|
||||
|
||||
function replaceAllLiteral(text, needle, replacement) {
|
||||
if (!needle) {
|
||||
return text;
|
||||
}
|
||||
return text.split(needle).join(replacement);
|
||||
}
|
||||
|
||||
function extractSpokenText(text) {
|
||||
const match = /^spoken_text:\s*(.+)$/m.exec(text);
|
||||
if (!match) {
|
||||
return text.trim().split(/\r?\n/).find((line) => line.trim().length > 0)?.trim() ?? "";
|
||||
}
|
||||
const value = match[1].trim();
|
||||
if (value.startsWith("\"")) {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch {
|
||||
return value.replace(/^"|"$/g, "");
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function renderEvaluation(item, spokenText, score) {
|
||||
return [
|
||||
"schema_version: weksa.persona_golf_item_evaluation.v0",
|
||||
`fixture_id: metame-casual-pre2025`,
|
||||
`item_id: ${item.id}`,
|
||||
`message_id: "${item.messageId}"`,
|
||||
`generated: ${JSON.stringify(spokenText)}`,
|
||||
`ground_truth: ${JSON.stringify(item.groundTruth)}`,
|
||||
"score:",
|
||||
` exact_normalized: ${score.exact}`,
|
||||
` token_precision: ${score.precision.toFixed(3)}`,
|
||||
` token_recall: ${score.recall.toFixed(3)}`,
|
||||
` token_f1: ${score.f1.toFixed(3)}`,
|
||||
].join("\n") + "\n";
|
||||
}
|
||||
|
||||
function scoreText(generated, truth) {
|
||||
const left = normalizeForScore(generated);
|
||||
const right = normalizeForScore(truth);
|
||||
const leftTokens = left.split(/\s+/).filter(Boolean);
|
||||
const rightTokens = right.split(/\s+/).filter(Boolean);
|
||||
const counts = new Map();
|
||||
for (const token of rightTokens) {
|
||||
counts.set(token, (counts.get(token) ?? 0) + 1);
|
||||
}
|
||||
let overlap = 0;
|
||||
for (const token of leftTokens) {
|
||||
const count = counts.get(token) ?? 0;
|
||||
if (count > 0) {
|
||||
overlap += 1;
|
||||
counts.set(token, count - 1);
|
||||
}
|
||||
}
|
||||
const precision = leftTokens.length ? overlap / leftTokens.length : 0;
|
||||
const recall = rightTokens.length ? overlap / rightTokens.length : 0;
|
||||
const f1 = precision + recall ? (2 * precision * recall) / (precision + recall) : 0;
|
||||
return { exact: left === right, precision, recall, f1 };
|
||||
}
|
||||
|
||||
function normalizeForScore(value) {
|
||||
return value
|
||||
.toLowerCase()
|
||||
.replace(/[*_`~.,!?;:'"()[\]{}]/g, "")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function extractBetween(text, startMarker, endMarker) {
|
||||
const start = text.indexOf(startMarker);
|
||||
const end = text.indexOf(endMarker, start + startMarker.length);
|
||||
if (start < 0 || end < 0 || end <= start) {
|
||||
throw new Error(`Could not extract section ${startMarker}`);
|
||||
}
|
||||
return text.slice(start + startMarker.length, end);
|
||||
}
|
||||
|
||||
function runCommand(command, args, { cwd, input = "", timeoutMs = 120_000 }) {
|
||||
return new Promise((resolveRun) => {
|
||||
const isCmdShim = /\.cmd$/i.test(command) || /\.bat$/i.test(command);
|
||||
const spawnCommand = isCmdShim ? (process.env.ComSpec ?? "cmd.exe") : command;
|
||||
const spawnArgs = isCmdShim ? ["/d", "/s", "/c", command, ...args] : args;
|
||||
const child = spawn(spawnCommand, spawnArgs, {
|
||||
cwd,
|
||||
env: process.env,
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
windowsHide: true,
|
||||
});
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
const timer = setTimeout(() => child.kill(), timeoutMs);
|
||||
child.stdout.setEncoding("utf8");
|
||||
child.stderr.setEncoding("utf8");
|
||||
child.stdout.on("data", (chunk) => { stdout += chunk; });
|
||||
child.stderr.on("data", (chunk) => { stderr += chunk; });
|
||||
child.on("error", (error) => {
|
||||
clearTimeout(timer);
|
||||
resolveRun({ code: -1, stdout, stderr: `${stderr}\n${error.message}` });
|
||||
});
|
||||
child.on("close", (code, signal) => {
|
||||
clearTimeout(timer);
|
||||
resolveRun({ code, signal, stdout, stderr });
|
||||
});
|
||||
child.stdin.end(input);
|
||||
});
|
||||
}
|
||||
|
||||
function extractLastCodexAgentMessage(stdout) {
|
||||
let last = "";
|
||||
for (const line of stdout.split(/\r?\n/)) {
|
||||
if (!line.trim()) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const event = JSON.parse(line);
|
||||
const message = event?.msg?.message ?? event?.message ?? event?.text;
|
||||
if (typeof message === "string" && message.trim()) {
|
||||
last = message;
|
||||
}
|
||||
const itemText = event?.item?.text;
|
||||
if (typeof itemText === "string" && itemText.trim()) {
|
||||
last = itemText;
|
||||
}
|
||||
const content = event?.item?.content;
|
||||
if (Array.isArray(content)) {
|
||||
const text = content
|
||||
.map((part) => typeof part?.text === "string" ? part.text : "")
|
||||
.join("\n")
|
||||
.trim();
|
||||
if (text) {
|
||||
last = text;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
if (line.trim()) {
|
||||
last = line.trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
return last;
|
||||
}
|
||||
|
||||
function indentBlock(block, prefix) {
|
||||
return block.split(/\r?\n/).map((line) => `${prefix}${line}`).join("\n").split(/\r?\n/);
|
||||
}
|
||||
|
||||
function readArg(name) {
|
||||
const index = process.argv.indexOf(name);
|
||||
if (index < 0) {
|
||||
return undefined;
|
||||
}
|
||||
return process.argv[index + 1];
|
||||
}
|
||||
|
||||
function escapeRegExp(value) {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
function sanitizeJob(value) {
|
||||
return value.replace(/[^A-Za-z0-9_.-]+/g, "_");
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,83 +0,0 @@
|
|||
param(
|
||||
[int] $Port = 8813,
|
||||
[string] $HostName = "127.0.0.1",
|
||||
[string] $StateRoot = "E:\Projects\weksa\.weksa",
|
||||
[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] $OdinCultMeshRudp = "127.0.0.1:17871"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||
$pidPath = Join-Path $StateRoot "weksa-daemon.pid"
|
||||
$outLog = Join-Path $StateRoot "weksa-daemon.out.log"
|
||||
$errLog = Join-Path $StateRoot "weksa-daemon.err.log"
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $StateRoot | Out-Null
|
||||
$cultLibPackages = Resolve-Path -LiteralPath (Join-Path $repoRoot "..\CultLib\packages")
|
||||
$env:NODE_PATH = (@($cultLibPackages.Path, $env:NODE_PATH) | Where-Object { $_ }) -join [IO.Path]::PathSeparator
|
||||
|
||||
if (Test-Path -LiteralPath $pidPath) {
|
||||
$pidText = (Get-Content -LiteralPath $pidPath -Raw).Trim()
|
||||
if ($pidText -match "^\d+$") {
|
||||
$existing = Get-Process -Id ([int] $pidText) -ErrorAction SilentlyContinue
|
||||
if ($null -ne $existing) {
|
||||
try {
|
||||
& node (Join-Path $repoRoot "scripts\weksa-daemon.mjs") --health --host $HostName --port $Port --state-root $StateRoot | Out-Null
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Host "Weksa daemon already healthy as PID $pidText."
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
Stop-Process -Id $existing.Id -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
}
|
||||
Remove-Item -LiteralPath $pidPath -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
$arguments = @(
|
||||
(Join-Path $repoRoot "scripts\weksa-daemon.mjs"),
|
||||
"--host", $HostName,
|
||||
"--port", [string] $Port,
|
||||
"--state-root", $StateRoot,
|
||||
"--mimo-api-key-path", $MiMoApiKeyPath,
|
||||
"--idunn-rudp-health", $IdunnRudpHealth,
|
||||
"--idunn-daemon", $IdunnDaemon,
|
||||
"--idunn-health-contract", $IdunnHealthContract
|
||||
)
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($OdinCultMeshRudp)) {
|
||||
$arguments += @("--odin-cultmesh-rudp", $OdinCultMeshRudp)
|
||||
}
|
||||
|
||||
$process = Start-Process -FilePath "node" `
|
||||
-ArgumentList $arguments `
|
||||
-WorkingDirectory $repoRoot `
|
||||
-WindowStyle Hidden `
|
||||
-PassThru `
|
||||
-RedirectStandardOutput $outLog `
|
||||
-RedirectStandardError $errLog
|
||||
|
||||
$process.Id | Set-Content -Encoding ASCII -LiteralPath $pidPath
|
||||
|
||||
Start-Sleep -Seconds 2
|
||||
if ($process.HasExited) {
|
||||
$detail = ""
|
||||
if (Test-Path -LiteralPath $outLog) { $detail += Get-Content -Raw -LiteralPath $outLog }
|
||||
if (Test-Path -LiteralPath $errLog) { $detail += Get-Content -Raw -LiteralPath $errLog }
|
||||
throw "Weksa daemon exited immediately with code $($process.ExitCode).`n$detail"
|
||||
}
|
||||
|
||||
& node (Join-Path $repoRoot "scripts\weksa-daemon.mjs") --health --host $HostName --port $Port --state-root $StateRoot | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Weksa daemon started but health check failed."
|
||||
}
|
||||
|
||||
Write-Host "Started Weksa daemon as PID $($process.Id)."
|
||||
Write-Host " command: rudp://$HostName`:$Port"
|
||||
Write-Host " Odin CultMesh/RUDP: $OdinCultMeshRudp"
|
||||
Write-Host " state: $StateRoot"
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,466 +0,0 @@
|
|||
"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