Unify typed agent transport and shared state

This commit is contained in:
Meta Vens 2026-07-15 20:23:41 +01:00
commit c4376ee129
23 changed files with 1434 additions and 538 deletions

View file

@ -75,7 +75,7 @@ REPO_FACE_BIRTH_MODE=plan
REPO_FACE_BIRTH_EXECUTOR=codex-exec
REPO_FACE_GITHUB_ACTIONS_ENABLED=false
REPO_FACE_WEKSA_SPEECH_ENABLED=false
WEKSA_DAEMON_BASE_URL=http://127.0.0.1:8813
WEKSA_CULTMESH_COMMAND_URI=
WEKSA_DAEMON_TIMEOUT_MS=120000
WEKSA_REPO_ROOT=E:/Projects/weksa
REPO_FACE_DISCORD_VOICE_ENABLED=false
@ -101,8 +101,10 @@ REPO_FACE_TURN_CODEX_MODEL=gpt-5.4
REPO_FACE_HEARTBEAT_CODEX_REASONING_EFFORT=low
EPIPHANY_AGENT_ROOT=E:/Projects/EpiphanyAgent
BIFROST_ROOT=E:/Projects/Bifrost
BIFROST_HEALTH_URL=http://127.0.0.1:5080/healthz
ODIN_BASE_URL=http://127.0.0.1:8797
BIFROST_CULTMESH_COMMAND_URI=cultmesh://asgard.starfire.bifrost/commands/discord-post
BIFROST_CULTMESH_STORE_PATH=E:/Projects/Bifrost/.bifrost/provider-store.cc
BIFROST_CULTMESH_COMMAND_TIMEOUT_MS=30000
BIFROST_CULTMESH_COMMAND_PUMP_ENABLED=true
INDEX_ALL_CHANNELS=false
INDEXED_CHANNEL_IDS=
EXCLUDED_CHANNEL_IDS=

3
.gitignore vendored
View file

@ -2,8 +2,11 @@ node_modules/
dist/
.env
.voidbot/
.bifrost/
coverage/
*.log
*.tsbuildinfo
**/bin/
**/obj/
Thumbs.db
.DS_Store

View file

@ -69,7 +69,8 @@ export async function maybeMirrorOwnerAquariumMessageAsMetameVoice(input: {
});
const client = new WeksaSpeechClient({
baseUrl: config.repoFaceWeksaSpeech.daemonBaseUrl,
commandUri: config.repoFaceWeksaSpeech.commandUri,
repoRoot: config.repoFaceWeksaSpeech.repoRoot,
timeoutMs: config.repoFaceWeksaSpeech.timeoutMs,
});
try {

View file

@ -1,6 +1,7 @@
import "dotenv/config";
import { createHash } from "node:crypto";
import { createRequire } from "node:module";
import { mkdir, writeFile } from "node:fs/promises";
import { spawnSync } from "node:child_process";
import { isAbsolute, join, relative, resolve } from "node:path";
@ -23,6 +24,7 @@ import {
loadSystemMessageCatalog,
resolveRepoFaceStatePath,
resolveWeksaArtifactPath,
type RepoDiscordIdentity,
type SystemMessageCatalog,
WeksaSpeechClient,
type WeksaMimoReceipt,
@ -71,13 +73,18 @@ import {
const config = loadConfig();
const weksaSpeechClient = new WeksaSpeechClient({
baseUrl: config.repoFaceWeksaSpeech.daemonBaseUrl,
commandUri: config.repoFaceWeksaSpeech.commandUri,
repoRoot: config.repoFaceWeksaSpeech.repoRoot,
timeoutMs: config.repoFaceWeksaSpeech.timeoutMs,
});
const REPO_IDENTITY_PROPOSAL_PR_SENTINEL = "VOIDBOT_REPO_IDENTITY_PROPOSAL_PR:";
const REPO_IDENTITY_PR_COMMENT_SENTINEL = "VOIDBOT_REPO_IDENTITY_PR_COMMENT:";
const REPO_IDENTITY_UPDATE_REQUEST_SENTINEL = "VOIDBOT_REPO_IDENTITY_UPDATE_REQUEST:";
const REPO_IDENTITY_BIFROST_TOPIC_SENTINEL = "VOIDBOT_REPO_IDENTITY_BIFROST_TOPIC:";
const BIFROST_DISCORD_POST_COMMAND_DOCUMENT_TYPE = "bifrost.bridge.discord_post_command";
const BIFROST_DISCORD_POST_COMMAND_SCHEMA_ID = "bifrost.bridge.discord_post_command.v1";
const BIFROST_DISCORD_POST_RECEIPT_DOCUMENT_TYPE = "bifrost.bridge.discord_post_receipt";
const BIFROST_DISCORD_POST_RECEIPT_SCHEMA_ID = "bifrost.bridge.discord_post_receipt.v1";
const CURRENT_REPO_FACE_IDENTITY_SELECTORS = new Set([
"face_id",
"faceid",
@ -240,6 +247,7 @@ async function buildProviderRegistry(systemMessagesCatalog: SystemMessageCatalog
timeoutMs: config.openAiApi.timeoutMs,
authHeader: config.openAiApi.authHeader,
maxCompletionTokens: config.openAiApi.maxCompletionTokens,
toolbox: buildRetrievalToolbox(),
}),
new LocalLlmProvider({
enabled: config.enabledProviders.includes("local_llm"),
@ -349,6 +357,124 @@ async function buildProviderRegistry(systemMessagesCatalog: SystemMessageCatalog
]);
}
function buildRetrievalToolbox() {
return {
listIndexedRepos: async () => {
const repos = await sourceArchiveRepository.listRepoSummaries();
return {
repoCount: repos.length,
repos,
};
},
searchHistory: async (input: {
query: string;
limit: number;
guildId?: string;
channelId?: string;
authorId?: string;
}) => {
const rawResults = await searchHistoryWithArchiveFallback({
retrievalService,
archiveRepository,
query: input.query,
limit: input.limit,
guildId: input.guildId,
channelId: input.channelId,
authorId: input.authorId,
preserveOverfetch: true,
});
const results = filterPromptEchoHistoryResults(rawResults, input.query);
return {
query: input.query,
resultCount: results.length,
results: formatHistoryResults(results),
};
},
getMessageContext: async (input: {
messageId: string;
before: number;
after: number;
}) => {
const messages = await archiveRepository.listContextWindow(
input.messageId,
input.before,
input.after,
);
return {
found: messages.length > 0,
messageId: input.messageId,
count: messages.length,
messages: messages.map((message) =>
formatArchivedMessageContext(message, input.messageId),
),
};
},
searchSources: async (input: {
query: string;
limit: number;
repoName?: string;
pathPrefix?: string;
language?: string;
}) => {
const results = await retrievalService.searchRepositorySources(input.query, input.limit, {
repoName: input.repoName,
pathPrefix: input.pathPrefix,
language: input.language,
});
return {
query: input.query,
resultCount: results.length,
results: formatSourceResults(results),
};
},
getSourceContext: async (input: {
sourceId: string;
chunkIndex: number;
before: number;
after: number;
}) => {
const document = await sourceArchiveRepository.get(input.sourceId);
if (!document) {
return {
found: false,
sourceId: input.sourceId,
count: 0,
chunks: [],
};
}
const chunks = sourceDocumentIngester.buildContextWindow(
document,
input.chunkIndex,
input.before,
input.after,
);
return {
found: true,
sourceId: input.sourceId,
repoName: document.repoName,
path: document.path,
language: document.language,
count: chunks.length,
chunks: chunks.map((chunk) => ({
chunkId: chunk.chunkId,
chunkIndex: chunk.chunkIndex,
lineStart: chunk.lineStart,
lineEnd: chunk.lineEnd,
isAnchor: chunk.chunkIndex === input.chunkIndex,
text: chunk.text,
})),
};
},
};
}
async function processJob(job: JobRecord): Promise<void> {
const provider = providerRegistry.get(job.provider);
@ -595,6 +721,13 @@ async function executeRepoFaceJobWithInterpreter(
: await interpretRepoFaceTurnOutput(provider, job, firstText, {
attempt: 1,
});
const firstRouteProblem = firstInterpretation.decision === "route"
? validateRepoFaceRoutedOutput(firstInterpretation.routedOutput, job)
: undefined;
if (firstRouteProblem) {
firstInterpretation.decision = "retry";
firstInterpretation.reasons = [firstRouteProblem, ...firstInterpretation.reasons].slice(0, 4);
}
if (firstInterpretation.decision === "route") {
return routeRepoFaceInterpretedOutput(firstResponse, firstInterpretation, {
job,
@ -648,6 +781,13 @@ async function executeRepoFaceJobWithInterpreter(
: await interpretRepoFaceTurnOutput(provider, job, retryText, {
attempt: 2,
});
const retryRouteProblem = retryInterpretation.decision === "route"
? validateRepoFaceRoutedOutput(retryInterpretation.routedOutput, job)
: undefined;
if (retryRouteProblem) {
retryInterpretation.decision = "drop";
retryInterpretation.reasons = [retryRouteProblem, ...retryInterpretation.reasons].slice(0, 4);
}
if (retryInterpretation.decision === "route") {
return routeRepoFaceInterpretedOutput(retryResponse, retryInterpretation, {
job,
@ -971,6 +1111,38 @@ function normalizeInterpretedRepoFaceSpeechDestinations(
);
}
function validateRepoFaceRoutedOutput(
outputText: string | undefined,
job: JobRecord,
): string | undefined {
if (job.command !== "repo-face-rumination" || !outputText || !repoFaceHasMultipleActiveConversationChannels(job)) {
return undefined;
}
for (const block of parseRepoFaceActionBlocks(outputText)) {
if (block.kind !== "say") {
continue;
}
const context = optionalDslString(block.fields.context) ??
optionalDslString(block.fields.context_id) ??
optionalDslString(block.fields.contextId);
const replyTo = optionalDslString(block.fields.reply_to) ??
optionalDslString(block.fields.replyToMessageId);
const channel = optionalDslString(block.fields.channel) ??
optionalDslString(block.fields.channelId);
const normalizedChannel = normalizeChannelSelector(channel);
const hasConcreteChannel = Boolean(channel) &&
normalizedChannel !== "current_room" &&
normalizedChannel !== "currentroom";
if (!context && !replyTo && !hasConcreteChannel) {
return "Routed SAY lacks context, reply_to, or a concrete channel id while multiple active conversation contexts are visible.";
}
}
return undefined;
}
function repoFaceHasMultipleActiveConversationChannels(job: JobRecord): boolean {
const activeThreads = job.contextBundle.repoFaceConversationThreads ?? [];
const representedChannels = new Set(activeThreads.map((thread) => thread.channelId));
@ -1137,14 +1309,23 @@ function repoFaceHeartbeatCodexOptions(job: JobRecord, role: "face" | "interpret
return {};
}
const identityId = parseRepoIdentityIdFromRequestMessageId(job.requestMessageId)
?? parseRepoIdentityIdFromPrompt(job.prompt);
const faceContext = {
repoFaceRole: role,
...(identityId ? { repoFaceIdentity: identityId } : {}),
};
if (job.provider === "openai_api") {
return {
...faceContext,
model: config.openAiApi.model,
};
}
if (role === "face") {
return {
...faceContext,
model: config.repoFaceHeartbeats.turnCodexModel,
...(config.repoFaceHeartbeats.codexModelReasoningEffort
? { reasoningEffort: config.repoFaceHeartbeats.codexModelReasoningEffort }
@ -1153,6 +1334,7 @@ function repoFaceHeartbeatCodexOptions(job: JobRecord, role: "face" | "interpret
}
return {
...faceContext,
...(config.repoFaceHeartbeats.codexModel ? { model: config.repoFaceHeartbeats.codexModel } : {}),
...(config.repoFaceHeartbeats.codexModels.length > 0
? { models: config.repoFaceHeartbeats.codexModels.join(",") }
@ -1491,18 +1673,13 @@ async function postRepoIdentityIntent(job: JobRecord, intent: RepoIdentityPostIn
if (/\S(?:\.\.\.|…)$/.test(content)) {
throw new Error(`Repo identity ${identity.id} SAY content appears mechanically truncated.`);
}
const contentFile = await writeBifrostPayloadFile(job, `${identity.id}-discord-post.md`, content);
const posted = runBifrostBridge([
"discord-post",
"--channel-id",
const posted = await postRepoIdentityDiscordViaCultMeshCommand({
job,
identity,
channelId,
"--content-file",
contentFile,
"--persona-name",
identity.displayName,
...(identity.avatarUrl ? ["--persona-avatar-url", identity.avatarUrl] : []),
...(effectiveReplyToMessageId ? ["--reply-to-message-id", effectiveReplyToMessageId] : []),
], { retries: 1 });
content,
replyToMessageId: effectiveReplyToMessageId,
});
if (!posted.messageId || !posted.transport) {
throw new Error(`Bifrost Discord bridge returned no message receipt for job ${job.id}.`);
}
@ -1531,6 +1708,282 @@ async function postRepoIdentityIntent(job: JobRecord, intent: RepoIdentityPostIn
return true;
}
async function postRepoIdentityDiscordViaCultMeshCommand(input: {
job: JobRecord;
identity: RepoDiscordIdentity;
channelId: string;
content: string;
replyToMessageId?: string;
}): Promise<BifrostBridgeReceipt> {
assertBifrostCultMeshCommandUri(config.bifrostCultMesh.commandUri);
const commandId = buildBifrostDiscordCommandId(input);
const node = await openBifrostCommandNode(config.bifrostCultMesh.storePath);
const requestedAt = new Date().toISOString();
const command = {
schemaName: BIFROST_DISCORD_POST_COMMAND_DOCUMENT_TYPE,
schemaVersion: BIFROST_DISCORD_POST_COMMAND_SCHEMA_ID,
commandId,
command: "discord-post",
status: "pending",
requestedBy: "voidbot",
requestedAt,
updatedAt: requestedAt,
commandUri: config.bifrostCultMesh.commandUri,
source: {
kind: "voidbot.repo-face.say",
id: input.job.id,
jobId: input.job.id,
requestMessageId: input.job.requestMessageId,
},
actor: {
id: input.identity.id,
displayName: input.identity.displayName,
repoName: input.identity.repoName,
},
payload: {
identityId: input.identity.id,
channelId: input.channelId,
content: input.content,
personaName: input.identity.displayName,
personaAvatarUrl: input.identity.avatarUrl ?? "",
replyToMessageId: input.replyToMessageId ?? "",
},
};
await node.put(bifrostDiscordCommandDefinition(), commandId, command);
await node.flush?.();
if (config.bifrostCultMesh.pumpEnabled) {
pumpBifrostCultMeshCommand(commandId);
}
const receipt = await waitForBifrostDiscordReceipt(node, commandId, config.bifrostCultMesh.timeoutMs);
if (receipt.status !== "completed" || receipt.ok !== true) {
throw new Error(
`Bifrost CultMesh Discord command ${commandId} failed: ${bifrostString(receipt.error) || "no error detail"}`,
);
}
const messageId = bifrostString(receipt.messageId);
const transport = bifrostString(receipt.transport);
if (!messageId || !transport) {
throw new Error(`Bifrost CultMesh Discord command ${commandId} returned an incomplete receipt.`);
}
return {
action: "discord-post",
ok: true,
messageId,
transport: transport === "bot" || transport === "webhook" ? transport : "webhook",
url: bifrostString(receipt.url),
};
}
function buildBifrostDiscordCommandId(input: {
job: JobRecord;
identity: RepoDiscordIdentity;
channelId: string;
content: string;
replyToMessageId?: string;
}): string {
const hash = createHash("sha1")
.update(JSON.stringify({
jobId: input.job.id,
identityId: input.identity.id,
channelId: input.channelId,
replyToMessageId: input.replyToMessageId ?? "",
content: input.content,
}))
.digest("hex")
.slice(0, 20);
return `voidbot-discord-${hash}`;
}
function assertBifrostCultMeshCommandUri(value: string): void {
if (!/^cultmesh:\/\/[^/]+\/commands\/discord-post(?:$|[/?#])/.test(value)) {
throw new Error(
`BIFROST_CULTMESH_COMMAND_URI must be a CultMesh Discord command URI, got "${value}".`,
);
}
}
async function openBifrostCommandNode(storePath: string): Promise<{
put: (definition: unknown, key: string, value: unknown) => Promise<void>;
get: (definition: unknown, key: string) => unknown;
flush?: () => Promise<void>;
cache?: { pullAllBackingStores?: () => Promise<void> };
}> {
const { CultMesh } = loadBifrostCultMeshRuntime();
return CultMesh.createNode(storePath, {
documents: [
bifrostDiscordCommandDefinition(),
bifrostDiscordReceiptDefinition(),
bifrostGenericDocumentDefinition("gamecult.eve.provider_advertisement", "gamecult.eve.provider_advertisement.v1", "providerId"),
bifrostGenericDocumentDefinition("gamecult.eve.surface_state", "gamecult.eve.surface_state.v1", "providerId"),
bifrostGenericDocumentDefinition("gamecult.eve.interface_binding", "gamecult.eve.interface_binding.v1", "bindingId"),
],
});
}
function pumpBifrostCultMeshCommand(commandId: string): void {
const processor = resolve(config.bifrostRoot, "tools", "cultmesh-bridge-commands.mjs");
const result = spawnSync(process.execPath, [
processor,
"process",
"--store",
config.bifrostCultMesh.storePath,
"--command-id",
commandId,
], {
cwd: config.bifrostRoot,
encoding: "utf8",
windowsHide: true,
});
if (result.status !== 0 || result.error) {
throw new Error(`Bifrost CultMesh command processor failed. ${renderBridgeFailure(result, 1, 1)}`);
}
}
async function waitForBifrostDiscordReceipt(
node: { get: (definition: unknown, key: string) => unknown; cache?: { pullAllBackingStores?: () => Promise<void> } },
commandId: string,
timeoutMs: number,
): Promise<Record<string, unknown>> {
const startedAt = Date.now();
while (Date.now() - startedAt <= timeoutMs) {
await node.cache?.pullAllBackingStores?.();
const receipt = unwrapBifrostRecord(await node.get(bifrostDiscordReceiptDefinition(), commandId));
if (receipt) {
return receipt;
}
await delay(250);
}
throw new Error(`Timed out waiting for Bifrost CultMesh Discord receipt ${commandId}.`);
}
let bifrostDefinitions: {
command?: unknown;
receipt?: unknown;
generic: Map<string, unknown>;
} | undefined;
function bifrostDiscordCommandDefinition(): unknown {
const { defineDocumentType } = loadBifrostCultMeshRuntime();
bifrostDefinitions ??= { generic: new Map() };
bifrostDefinitions.command ??= defineDocumentType({
type: BIFROST_DISCORD_POST_COMMAND_DOCUMENT_TYPE,
schemaName: BIFROST_DISCORD_POST_COMMAND_DOCUMENT_TYPE,
schemaId: BIFROST_DISCORD_POST_COMMAND_SCHEMA_ID,
schemaVersion: BIFROST_DISCORD_POST_COMMAND_SCHEMA_ID,
contentHash: BIFROST_DISCORD_POST_COMMAND_SCHEMA_ID,
global: false,
name: "commandId",
schema: parseBifrostObjectDocument("Bifrost Discord post command"),
});
return bifrostDefinitions.command;
}
function bifrostDiscordReceiptDefinition(): unknown {
const { defineDocumentType } = loadBifrostCultMeshRuntime();
bifrostDefinitions ??= { generic: new Map() };
bifrostDefinitions.receipt ??= defineDocumentType({
type: BIFROST_DISCORD_POST_RECEIPT_DOCUMENT_TYPE,
schemaName: BIFROST_DISCORD_POST_RECEIPT_DOCUMENT_TYPE,
schemaId: BIFROST_DISCORD_POST_RECEIPT_SCHEMA_ID,
schemaVersion: BIFROST_DISCORD_POST_RECEIPT_SCHEMA_ID,
contentHash: BIFROST_DISCORD_POST_RECEIPT_SCHEMA_ID,
global: false,
name: "commandId",
schema: parseBifrostObjectDocument("Bifrost Discord post receipt"),
});
return bifrostDefinitions.receipt;
}
function bifrostGenericDocumentDefinition(type: string, schemaId: string, name: string): unknown {
const { defineDocumentType } = loadBifrostCultMeshRuntime();
bifrostDefinitions ??= { generic: new Map() };
const key = `${type}:${schemaId}:${name}`;
let definition = bifrostDefinitions.generic.get(key);
if (!definition) {
definition = defineDocumentType({
type,
schemaName: type,
schemaId,
schemaVersion: schemaId,
contentHash: schemaId,
global: false,
name,
schema: parseBifrostObjectDocument(type),
});
bifrostDefinitions.generic.set(key, definition);
}
return definition;
}
function loadBifrostCultMeshRuntime(): {
CultMesh: { createNode: (storePath: string, options: unknown) => Promise<{
put: (definition: unknown, key: string, value: unknown) => Promise<void>;
get: (definition: unknown, key: string) => unknown;
flush?: () => Promise<void>;
cache?: { pullAllBackingStores?: () => Promise<void> };
}> };
defineDocumentType: (definition: Record<string, unknown>) => unknown;
} {
const candidates = [
resolve(config.bifrostRoot, "..", "CultLib", "packages", "cultmesh-ts", "package.json"),
resolve(config.bifrostRoot, "..", "CultMeshTS", "package.json"),
];
for (const packageJson of candidates) {
try {
const requireCult = createRequire(packageJson);
const { CultMesh } = requireCult("cultmesh-ts") as {
CultMesh?: { createNode: (storePath: string, options: unknown) => Promise<{
put: (definition: unknown, key: string, value: unknown) => Promise<void>;
get: (definition: unknown, key: string) => unknown;
flush?: () => Promise<void>;
cache?: { pullAllBackingStores?: () => Promise<void> };
}> };
};
const { defineDocumentType } = requireCult("cultcache-ts") as {
defineDocumentType?: (definition: Record<string, unknown>) => unknown;
};
if (CultMesh && defineDocumentType) {
return { CultMesh, defineDocumentType };
}
} catch {
}
}
throw new Error("CultMesh/CultCache packages are unavailable; cannot write Bifrost command documents.");
}
function parseBifrostObjectDocument(label: string): { parse: (value: unknown) => unknown } {
return {
parse(value: unknown): unknown {
if (!value || typeof value !== "object") {
throw new Error(`${label} must be an object.`);
}
return value;
},
};
}
function unwrapBifrostRecord(record: unknown): Record<string, unknown> | undefined {
const candidate = Array.isArray(record) && record.length === 1 ? record[0] : record;
const value = isBifrostRecord(candidate) && isBifrostRecord(candidate.value) ? candidate.value : candidate;
return isBifrostRecord(value) ? value : undefined;
}
function isBifrostRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
function bifrostString(value: unknown): string {
return typeof value === "string" ? value.trim() : value === undefined || value === null ? "" : String(value).trim();
}
function delay(ms: number): Promise<void> {
return new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
}
async function resolveRepoIdentityForJobIntent(
job: JobRecord,
identitySelector?: string,

View file

@ -48,31 +48,46 @@ export const applyRepoFaceStateOperationInputSchema = {
operation: z.record(z.unknown()),
};
export const readSharedDocumentInputSchema = {
documentId: z.string().min(1).max(120).optional(),
};
export const createSharedDocumentInputSchema = {
documentId: z.string().min(1).max(120),
title: z.string().min(1).max(240),
body: z.string().max(20_000),
};
export const updateSharedDocumentInputSchema = {
documentId: z.string().min(1).max(120),
title: z.string().min(1).max(240).optional(),
body: z.string().max(20_000),
expectedVersion: z.number().int().positive(),
};
export const runtimeInfoInputSchema = {};
export const odinEndpointInputSchema = {
odinBaseUrl: z.string().url().optional(),
odinStorePath: z.string().min(1).optional(),
};
export const odinSurfaceInputSchema = {
odinBaseUrl: z.string().url().optional(),
odinStorePath: z.string().min(1).optional(),
providerId: z.string().min(1).optional(),
};
export const odinInterfaceContextInputSchema = {
odinBaseUrl: z.string().url().optional(),
odinStorePath: z.string().min(1).optional(),
providerId: z.string().min(1),
maxTextItems: z.number().int().min(1).max(80).optional(),
maxTreeItems: z.number().int().min(1).max(160).optional(),
};
export const odinInterfaceCommandInputSchema = {
odinBaseUrl: z.string().url().optional(),
odinStorePath: z.string().min(1).optional(),
providerId: z.string().min(1),
command: z.string().min(1),
payload: z.record(z.unknown()).optional(),
frame: z.record(z.unknown()).optional(),
expectReceiptMs: z.number().int().min(0).max(15000).optional(),
};
export const searchSourcesInputSchema = {
@ -132,33 +147,48 @@ export interface ApplyRepoFaceStateOperationArgs {
operation: Record<string, unknown>;
}
export interface ReadSharedDocumentArgs {
documentId?: string;
}
export interface CreateSharedDocumentArgs {
documentId: string;
title: string;
body: string;
}
export interface UpdateSharedDocumentArgs {
documentId: string;
title?: string;
body: string;
expectedVersion: number;
}
export interface RuntimeInfoArgs {
[key: string]: never;
}
export interface OdinEndpointArgs {
odinBaseUrl?: string;
odinStorePath?: string;
}
export interface OdinSurfaceArgs {
odinBaseUrl?: string;
odinStorePath?: string;
providerId?: string;
}
export interface OdinInterfaceContextArgs {
odinBaseUrl?: string;
odinStorePath?: string;
providerId: string;
maxTextItems?: number;
maxTreeItems?: number;
}
export interface OdinInterfaceCommandArgs {
odinBaseUrl?: string;
odinStorePath?: string;
providerId: string;
command: string;
payload?: Record<string, unknown>;
frame?: Record<string, unknown>;
expectReceiptMs?: number;
}
export interface ListIndexedReposArgs {

View file

@ -1,12 +1,14 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { type CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import crypto from "node:crypto";
import net from "node:net";
import { existsSync, readFileSync } from "node:fs";
import { createRequire } from "node:module";
import path from "node:path";
import { DEFAULT_RETRIEVAL_RESULT_LIMIT } from "@voidbot/shared";
import { searchHistoryWithArchiveFallback } from "@voidbot/rag";
import {
applyRepoFacePostFatigueAfterSpeech,
createRepoFaceSharedDocument,
applyVoidSelfStateOperation,
buildVoidSelfStateContext,
faceRegistryAsRepoDiscordRegistry,
@ -14,9 +16,12 @@ import {
isRepoDiscordIdentityAllowedInChannel,
loadFaceIdentityRegistry,
loadVoidSelfStateTypedDocuments,
listRepoFaceSharedDocuments,
renderFaceIdentityDoctrine,
resolveFaceStatePath,
resolveRepoFaceStatePath,
resolveRepoFaceSharedDocumentsPath,
updateRepoFaceSharedDocument,
VOID_SELF_STATE_SCHEMA_DOCUMENT_TYPES,
VOID_SELF_STATE_SCHEMA_FINGERPRINT,
} from "@voidbot/core";
@ -32,17 +37,21 @@ import {
type OdinInterfaceContextArgs,
type OdinSurfaceArgs,
type ApplyRepoFaceStateOperationArgs,
type CreateSharedDocumentArgs,
type PostDiscordMessageArgs,
type PostRepoIdentityMessageArgs,
type RepoFaceStateArgs,
type ReadSharedDocumentArgs,
type RuntimeInfoArgs,
type SearchHistoryArgs,
type SearchSourcesArgs,
type SourceContextArgs,
type UpdateSharedDocumentArgs,
formatArchivedMessage,
formatHistoryResults,
formatSourceResults,
applyRepoFaceStateOperationInputSchema,
createSharedDocumentInputSchema,
messageContextInputSchema,
notifyOwnerInputSchema,
odinEndpointInputSchema,
@ -52,11 +61,13 @@ import {
postDiscordMessageInputSchema,
postRepoIdentityMessageInputSchema,
repoFaceStateInputSchema,
readSharedDocumentInputSchema,
renderJsonBlock,
runtimeInfoInputSchema,
searchHistoryInputSchema,
searchSourcesInputSchema,
sourceContextInputSchema,
updateSharedDocumentInputSchema,
} from "./mcp-server-shared";
const READ_ONLY_ANNOTATIONS = {
@ -66,7 +77,8 @@ const READ_ONLY_ANNOTATIONS = {
openWorldHint: false,
} as const;
const DEFAULT_ODIN_BASE_URL = "http://127.0.0.1:8797";
const DEFAULT_ODIN_STORE_PATH = path.resolve(process.cwd(), "..", "Odin", "scratch", "odin", "odin.ccmp");
const ODIN_SURFACE_KEY = "surface:gamecult.network.status";
function isMcpToolAllowed(name: string): boolean {
const raw = process.env.VOIDBOT_MCP_TOOL_ALLOWLIST?.trim();
@ -136,22 +148,22 @@ export function registerVoidbotTools(
annotations: READ_ONLY_ANNOTATIONS,
},
async (input: OdinEndpointArgs): Promise<CallToolResult> => {
const odinBaseUrl = normalizeOdinBaseUrl(input.odinBaseUrl);
const catalog = await fetchOdinProviderCatalog(odinBaseUrl);
const odinStorePath = resolveOdinStorePath(input.odinStorePath);
const catalog = await fetchOdinProviderCatalog(odinStorePath);
return {
content: [
{
type: "text",
text: renderJsonBlock({
odinBaseUrl,
odinStorePath,
providerCount: catalog.providers.length,
providers: catalog.providers,
}),
},
],
structuredContent: {
odinBaseUrl,
odinStorePath,
providerCount: catalog.providers.length,
providers: catalog.providers,
},
@ -169,8 +181,8 @@ export function registerVoidbotTools(
annotations: READ_ONLY_ANNOTATIONS,
},
async (input: OdinEndpointArgs): Promise<CallToolResult> => {
const odinBaseUrl = normalizeOdinBaseUrl(input.odinBaseUrl);
const snapshot = await fetchOdinDeckSnapshot(odinBaseUrl);
const odinStorePath = resolveOdinStorePath(input.odinStorePath);
const snapshot = await fetchOdinSnapshot(odinStorePath);
const verses = summarizeOdinVerses(snapshot);
return {
@ -178,7 +190,7 @@ export function registerVoidbotTools(
{
type: "text",
text: renderJsonBlock({
odinBaseUrl,
odinStorePath,
providerId: snapshot.providerId,
title: snapshot.title,
version: snapshot.version,
@ -189,7 +201,7 @@ export function registerVoidbotTools(
},
],
structuredContent: {
odinBaseUrl,
odinStorePath,
providerId: snapshot.providerId,
title: snapshot.title,
version: snapshot.version,
@ -211,8 +223,8 @@ export function registerVoidbotTools(
annotations: READ_ONLY_ANNOTATIONS,
},
async (input: OdinSurfaceArgs): Promise<CallToolResult> => {
const odinBaseUrl = normalizeOdinBaseUrl(input.odinBaseUrl);
const snapshot = await fetchOdinDeckSnapshot(odinBaseUrl, input.providerId);
const odinStorePath = resolveOdinStorePath(input.odinStorePath);
const snapshot = await fetchOdinSnapshot(odinStorePath, input.providerId);
const surfaceSummary = summarizeOdinSurface(snapshot);
return {
@ -220,7 +232,7 @@ export function registerVoidbotTools(
{
type: "text",
text: renderJsonBlock({
odinBaseUrl,
odinStorePath,
providerId: snapshot.providerId,
title: snapshot.title,
version: snapshot.version,
@ -231,7 +243,7 @@ export function registerVoidbotTools(
},
],
structuredContent: {
odinBaseUrl,
odinStorePath,
providerId: snapshot.providerId,
title: snapshot.title,
version: snapshot.version,
@ -253,8 +265,8 @@ export function registerVoidbotTools(
annotations: READ_ONLY_ANNOTATIONS,
},
async (input: OdinInterfaceContextArgs): Promise<CallToolResult> => {
const odinBaseUrl = normalizeOdinBaseUrl(input.odinBaseUrl);
const loaded = await loadOdinProviderInterface(odinBaseUrl, input.providerId);
const odinStorePath = resolveOdinStorePath(input.odinStorePath);
const loaded = await loadOdinProviderInterface(odinStorePath, input.providerId);
const contextSummary = summarizeProviderInterface(loaded, {
maxTextItems: input.maxTextItems ?? 32,
maxTreeItems: input.maxTreeItems ?? 80,
@ -265,13 +277,13 @@ export function registerVoidbotTools(
{
type: "text",
text: renderJsonBlock({
odinBaseUrl,
odinStorePath,
...contextSummary,
}),
},
],
structuredContent: {
odinBaseUrl,
odinStorePath,
...contextSummary,
},
};
@ -283,7 +295,7 @@ export function registerVoidbotTools(
{
title: "Invoke Odin Interface Command",
description:
"Send an explicit command through a provider-owned Eve/CultUI command boundary discovered via Odin. The command must be advertised by the provider interface. Odin is not the side-effect owner; this relays to the provider endpoint when the interface exposes a WebSocket command transport.",
"Inspect a provider-owned Eve/CultUI command boundary discovered via Odin. The command must be advertised by the provider interface; execution requires a CultMesh command document path and no longer tunnels through provider WebSockets.",
inputSchema: odinInterfaceCommandInputSchema,
annotations: {
readOnlyHint: false,
@ -293,8 +305,8 @@ export function registerVoidbotTools(
},
},
async (input: OdinInterfaceCommandArgs): Promise<CallToolResult> => {
const odinBaseUrl = normalizeOdinBaseUrl(input.odinBaseUrl);
const loaded = await loadOdinProviderInterface(odinBaseUrl, input.providerId);
const odinStorePath = resolveOdinStorePath(input.odinStorePath);
const loaded = await loadOdinProviderInterface(odinStorePath, input.providerId);
const command = findAdvertisedCommand(loaded, input.command);
if (!command) {
@ -316,50 +328,28 @@ export function registerVoidbotTools(
};
}
if (!loaded.source.startsWith("ws://")) {
return {
content: [
{
type: "text",
text: `Provider ${input.providerId} is visible through ${loaded.source || "an unknown source"}, but this MCP relay only supports provider WebSocket command endpoints right now.`,
},
],
structuredContent: {
sent: false,
reason: "unsupported_command_transport",
providerId: input.providerId,
command: input.command,
source: loaded.source,
},
isError: true,
};
}
const frame = input.frame ?? buildCommandFrame(input.providerId, command, input.payload ?? {});
const receipt = await sendProviderCommandFrame(loaded.source, frame, input.expectReceiptMs ?? 5000);
return {
content: [
{
type: "text",
text: renderJsonBlock({
sent: true,
sent: false,
reason: "cultmesh_command_transport_required",
providerId: input.providerId,
source: loaded.source,
command: input.command,
frame,
receipt,
advertisedCommand: command,
}),
},
],
structuredContent: {
sent: true,
sent: false,
reason: "cultmesh_command_transport_required",
providerId: input.providerId,
source: loaded.source,
command: input.command,
frame,
receipt,
},
isError: true,
};
},
);
@ -995,6 +985,106 @@ export function registerVoidbotTools(
},
);
registerIfAllowed(
"read_shared_document",
{
title: "Read Shared Document",
description:
"List shared typed documents, or read one by documentId. These documents are visible to every repo Face and carry revision and author provenance.",
inputSchema: readSharedDocumentInputSchema,
annotations: READ_ONLY_ANNOTATIONS,
},
async (input: ReadSharedDocumentArgs): Promise<CallToolResult> => {
const storePath = resolveRepoFaceSharedDocumentsPath(context.config.storageRoot);
const documents = await listRepoFaceSharedDocuments(storePath);
const requestedDocumentId = input.documentId?.trim().toLowerCase();
const selected = requestedDocumentId
? documents.find((document) => document.documentId === requestedDocumentId)
: undefined;
if (input.documentId && !selected) {
return {
content: [{ type: "text", text: `Shared document "${input.documentId}" was not found.` }],
structuredContent: { found: false, documentId: input.documentId },
isError: true,
};
}
const result = input.documentId ? { storePath, document: selected } : { storePath, documents };
return {
content: [{ type: "text", text: renderJsonBlock(result) }],
structuredContent: result,
};
},
);
registerIfAllowed(
"create_shared_document",
{
title: "Create Shared Document",
description:
"Create one shared typed document visible to every repo Face. The active Face identity is stamped by the server; arbitrary filesystem writes are not exposed.",
inputSchema: createSharedDocumentInputSchema,
annotations: {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
openWorldHint: false,
},
},
async (input: CreateSharedDocumentArgs): Promise<CallToolResult> => {
const actor = await resolveActiveRepoFaceForSharedTool(context);
if (!actor.identity) {
return actor.error;
}
const storePath = resolveRepoFaceSharedDocumentsPath(context.config.storageRoot);
const document = await createRepoFaceSharedDocument({
canonicalPath: storePath,
documentId: input.documentId,
title: input.title,
body: input.body,
actorId: actor.identity.id,
});
return {
content: [{ type: "text", text: renderJsonBlock({ created: true, storePath, document }) }],
structuredContent: { created: true, storePath, document },
};
},
);
registerIfAllowed(
"update_shared_document",
{
title: "Update Shared Document",
description:
"Replace one shared typed document body using its expected revision. Stale writes fail instead of overwriting another Face's update.",
inputSchema: updateSharedDocumentInputSchema,
annotations: {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
openWorldHint: false,
},
},
async (input: UpdateSharedDocumentArgs): Promise<CallToolResult> => {
const actor = await resolveActiveRepoFaceForSharedTool(context);
if (!actor.identity) {
return actor.error;
}
const storePath = resolveRepoFaceSharedDocumentsPath(context.config.storageRoot);
const document = await updateRepoFaceSharedDocument({
canonicalPath: storePath,
documentId: input.documentId,
title: input.title,
body: input.body,
expectedVersion: input.expectedVersion,
actorId: actor.identity.id,
});
return {
content: [{ type: "text", text: renderJsonBlock({ updated: true, storePath, document }) }],
structuredContent: { updated: true, storePath, document },
};
},
);
registerIfAllowed(
"notify_owner",
{
@ -1091,6 +1181,20 @@ async function resolveRepoIdentityForTool(
};
}
async function resolveActiveRepoFaceForSharedTool(context: VoidbotMcpContext) {
const identity = process.env.VOIDBOT_REPO_FACE_IDENTITY?.trim();
if (!identity) {
return {
error: {
content: [{ type: "text" as const, text: "Shared document mutation requires an active repo Face identity." }],
structuredContent: { applied: false, reason: "missing_active_face_identity" },
isError: true,
},
};
}
return resolveRepoIdentityForTool(context, identity);
}
async function recordRepoIdentityDeliveryReceipt(input: {
context: VoidbotMcpContext;
identity: NonNullable<ReturnType<typeof findRepoDiscordIdentity>>;
@ -1157,55 +1261,115 @@ function identityForToolResult(
};
}
function normalizeOdinBaseUrl(value: string | undefined): string {
return (value ?? process.env.ODIN_BASE_URL ?? DEFAULT_ODIN_BASE_URL).replace(/\/+$/, "");
function resolveOdinStorePath(value: string | undefined): string {
return path.resolve(value ?? process.env.ODIN_CULTMESH_STORE ?? DEFAULT_ODIN_STORE_PATH);
}
async function fetchOdinProviderCatalog(odinBaseUrl: string): Promise<{ providers: Array<Record<string, unknown>> }> {
const response = await fetch(`${odinBaseUrl}/eve/deck/providers`);
if (!response.ok) {
throw new Error(`Odin provider catalog failed: HTTP ${response.status}`);
}
const payload = await response.json() as { providers?: unknown };
const providers = Array.isArray(payload.providers)
? payload.providers.filter(isRecord)
: [];
async function fetchOdinProviderCatalog(odinStorePath: string): Promise<{ providers: Array<Record<string, unknown>> }> {
const snapshot = await fetchOdinSnapshot(odinStorePath);
const providers = summarizeOdinSurface(snapshot).interfaces.map((entry) => ({
id: entry.providerId,
title: entry.title,
status: entry.status,
detail: entry.detail,
source: entry.source,
updatedAt: entry.updatedAt,
}));
return { providers };
}
async function fetchOdinDeckSnapshot(
odinBaseUrl: string,
async function fetchOdinSnapshot(
odinStorePath: string,
providerId?: string,
): Promise<Record<string, unknown>> {
const deckUrl = new URL(odinBaseUrl);
deckUrl.protocol = deckUrl.protocol === "https:" ? "wss:" : "ws:";
deckUrl.pathname = "/eve/deck";
deckUrl.search = "";
const connection = await openWebSocketSnapshotSocket(deckUrl);
const { socket } = connection;
let pendingBuffer = connection.initialBuffer;
try {
if (providerId) {
sendClientWebSocketText(socket, JSON.stringify({ type: "open-provider", providerId }));
}
for (let attempt = 0; attempt < 8; attempt += 1) {
const frame = await readServerWebSocketText(socket, 3000, pendingBuffer);
pendingBuffer = frame.remaining;
const message = frame.text;
const parsed = JSON.parse(message) as unknown;
if (!isRecord(parsed)) {
continue;
}
if (!providerId || parsed.providerId === providerId) {
return parsed;
}
}
throw new Error(providerId
? `Odin did not publish provider ${providerId} in the snapshot window.`
: "Odin did not publish a deck snapshot.");
} finally {
socket.destroy();
const allSeer = await readOdinAllSeerSurface(odinStorePath);
if (!providerId) {
return allSeer;
}
const provider = providerStateFromOdinSurface(allSeer, providerId);
if (!provider) {
throw new Error(`Odin CultMesh store does not currently expose provider interface ${providerId}.`);
}
return provider;
}
async function readOdinAllSeerSurface(odinStorePath: string): Promise<Record<string, unknown>> {
if (!existsSync(odinStorePath)) {
throw new Error(`Odin CultMesh store is missing: ${odinStorePath}`);
}
const { inspectCultCacheBytes } = loadCultCacheInspector();
const inspection = inspectCultCacheBytes(odinStorePath, readFileSync(odinStorePath));
const surfaceRecord = inspection.records.find((entry) =>
entry.schemaId === "gamecult.eve.surface_state.v1"
&& entry.key === ODIN_SURFACE_KEY
);
const record = isRecord(surfaceRecord?.payloadPreview) ? surfaceRecord.payloadPreview : null;
if (!record) {
throw new Error(`Odin CultMesh store does not contain ${ODIN_SURFACE_KEY}.`);
}
return record;
}
function loadCultCacheInspector(): {
inspectCultCacheBytes: (
filePath: string,
bytes: Uint8Array,
) => { records: Array<{ key: string; schemaId: string; payloadPreview: unknown }> };
} {
const packageJsonCandidates = [
path.resolve(process.cwd(), "vendor", "cultcache-ts", "package.json"),
path.resolve(process.cwd(), "node_modules", "cultcache-ts", "package.json"),
path.resolve(process.cwd(), "..", "CultLib", "packages", "cultcache-ts", "package.json"),
];
for (const packageJson of packageJsonCandidates) {
try {
const requireFromCandidate = createRequire(packageJson);
const loaded = requireFromCandidate("./dist/cult-cache-inspector.js") as {
inspectCultCacheBytes?: (
filePath: string,
bytes: Uint8Array,
) => { records: Array<{ key: string; schemaId: string; payloadPreview: unknown }> };
};
if (loaded.inspectCultCacheBytes) {
return { inspectCultCacheBytes: loaded.inspectCultCacheBytes };
}
} catch {
// Try the next known CultCache runtime root.
}
}
throw new Error("CultCache inspector is unavailable; cannot read Odin provider state.");
}
function providerStateFromOdinSurface(
allSeer: Record<string, unknown>,
providerId: string,
): Record<string, unknown> | null {
const interfaceNode = findOdinInterfaceNode(allSeer, providerId);
if (!interfaceNode) {
return null;
}
const props = isRecord(interfaceNode.props) ? interfaceNode.props : {};
const embeddedRoot = embeddedInterfaceRoot(interfaceNode);
return {
schema: "gamecult.eve.surface_state.v1",
providerId,
title: stringValue(props.title) || providerId,
version: props.version ?? allSeer.version ?? 0,
updatedAt: stringValue(props.updatedAt) || stringValue(allSeer.updatedAt),
selectedNodeId: stringValue(allSeer.selectedNodeId),
source: stringValue(props.source),
manifest: isRecord(props.manifest) ? props.manifest : null,
surface: {
schema: "gamecult.eve.surface.v1",
root: embeddedRoot ?? {
id: `${providerId}.unavailable`,
kind: "text",
props: { text: `Provider ${providerId} has no embedded Odin interface root.` },
children: [],
},
},
};
}
function summarizeOdinVerses(snapshot: Record<string, unknown>) {
@ -1297,13 +1461,13 @@ interface LoadedOdinInterface {
manifest: Record<string, unknown> | null;
snapshot: Record<string, unknown>;
root?: Record<string, unknown>;
loadedFrom: "provider-websocket" | "odin-embedded-interface";
loadedFrom: "odin-embedded-interface";
}
interface InterfaceCommandSummary {
command: string;
label: string;
source: "surface.commands" | "node.action" | "node.command";
source: "surface.commands" | "manifest.commands" | "node.action" | "node.command";
nodeId?: string;
transport?: string;
frameTemplate?: unknown;
@ -1312,10 +1476,10 @@ interface InterfaceCommandSummary {
}
async function loadOdinProviderInterface(
odinBaseUrl: string,
odinStorePath: string,
providerId: string,
): Promise<LoadedOdinInterface> {
const odinSnapshot = await fetchOdinDeckSnapshot(odinBaseUrl);
const odinSnapshot = await fetchOdinSnapshot(odinStorePath);
const interfaceNode = findOdinInterfaceNode(odinSnapshot, providerId);
if (!interfaceNode) {
throw new Error(`Odin does not currently expose provider interface ${providerId}.`);
@ -1323,38 +1487,6 @@ async function loadOdinProviderInterface(
const props = isRecord(interfaceNode.props) ? interfaceNode.props : {};
const source = stringValue(props.source);
if (source.startsWith("ws://")) {
try {
const providerSnapshot = await fetchProviderDeckSnapshot(source, providerId);
return {
providerId,
title: stringValue(providerSnapshot.title) || stringValue(props.title) || providerId,
source,
status: stringValue(props.status),
detail: stringValue(props.detail),
manifest: isRecord(props.manifest) ? props.manifest : null,
snapshot: providerSnapshot,
root: getSurfaceRoot(providerSnapshot),
loadedFrom: "provider-websocket",
};
} catch (error) {
const embedded = embeddedInterfaceRoot(interfaceNode);
if (!embedded) {
throw error;
}
return {
providerId,
title: stringValue(props.title) || providerId,
source,
status: stringValue(props.status),
detail: `provider websocket failed; using Odin embedded root: ${error instanceof Error ? error.message : String(error)}`,
manifest: isRecord(props.manifest) ? props.manifest : null,
snapshot: odinSnapshot,
root: embedded,
loadedFrom: "odin-embedded-interface",
};
}
}
return {
providerId,
@ -1369,30 +1501,6 @@ async function loadOdinProviderInterface(
};
}
async function fetchProviderDeckSnapshot(
deckUrlText: string,
providerId: string,
): Promise<Record<string, unknown>> {
const deckUrl = new URL(deckUrlText);
const connection = await openWebSocketSnapshotSocket(deckUrl);
const { socket } = connection;
let pendingBuffer = connection.initialBuffer;
try {
sendClientWebSocketText(socket, JSON.stringify({ type: "open-provider", providerId }));
for (let attempt = 0; attempt < 8; attempt += 1) {
const frame = await readServerWebSocketText(socket, 3000, pendingBuffer);
pendingBuffer = frame.remaining;
const parsed = JSON.parse(frame.text) as unknown;
if (isRecord(parsed) && parsed.providerId === providerId) {
return parsed;
}
}
throw new Error(`Provider deck did not publish ${providerId}.`);
} finally {
socket.destroy();
}
}
function findOdinInterfaceNode(
odinSnapshot: Record<string, unknown>,
providerId: string,
@ -1495,8 +1603,11 @@ function extractProviderCommands(loaded: LoadedOdinInterface): InterfaceCommandS
: Array.isArray(loaded.snapshot.commands)
? loaded.snapshot.commands.filter(isRecord)
: [];
const manifestCommands = Array.isArray(loaded.manifest?.commands)
? loaded.manifest.commands.filter(isRecord)
: [];
for (const command of surfaceCommands) {
for (const command of [...surfaceCommands, ...manifestCommands]) {
const commandName = stringValue(command.command) || stringValue(command.id);
if (!commandName) {
continue;
@ -1504,7 +1615,7 @@ function extractProviderCommands(loaded: LoadedOdinInterface): InterfaceCommandS
commands.push({
command: commandName,
label: stringValue(command.label) || commandName,
source: "surface.commands",
source: surfaceCommands.includes(command) ? "surface.commands" : "manifest.commands",
transport: stringValue(command.transport),
frameTemplate: command.frameTemplate,
payloadSchema: command.payloadSchema,
@ -1558,98 +1669,6 @@ function findAdvertisedCommand(
return extractProviderCommands(loaded).find((entry) => entry.command === command);
}
function buildCommandFrame(
providerId: string,
command: InterfaceCommandSummary,
payload: Record<string, unknown>,
): Record<string, unknown> {
if (isRecord(command.frameTemplate)) {
return materializeFrameTemplate(command.frameTemplate, payload);
}
return {
type: "surface-command",
schema: "gamecult.eve.command.v1",
providerId,
command: command.command,
payload,
nodeId: command.nodeId,
};
}
function materializeFrameTemplate(
template: Record<string, unknown>,
payload: Record<string, unknown>,
): Record<string, unknown> {
const materialized: Record<string, unknown> = {};
for (const [key, value] of Object.entries(template)) {
materialized[key] = materializeTemplateValue(value, payload);
}
return materialized;
}
function materializeTemplateValue(value: unknown, payload: Record<string, unknown>): unknown {
if (typeof value === "string") {
const exact = value.match(/^\$\{([A-Za-z0-9_.-]+)\??\}$/);
if (exact) {
return payload[exact[1]] ?? "";
}
return value.replace(/\$\{([A-Za-z0-9_.-]+)\??\}/g, (_match, key: string) =>
payload[key] === undefined || payload[key] === null ? "" : String(payload[key]),
);
}
if (Array.isArray(value)) {
return value.map((entry) => materializeTemplateValue(entry, payload));
}
if (isRecord(value)) {
return materializeFrameTemplate(value, payload);
}
return value;
}
async function sendProviderCommandFrame(
deckUrlText: string,
frame: Record<string, unknown>,
expectReceiptMs: number,
) {
const deckUrl = new URL(deckUrlText);
const connection = await openWebSocketSnapshotSocket(deckUrl);
const { socket } = connection;
let pendingBuffer = connection.initialBuffer;
try {
try {
const initial = await readServerWebSocketText(socket, 1000, pendingBuffer);
pendingBuffer = initial.remaining;
} catch {
// Some providers do not send an initial deck frame before commands.
}
sendClientWebSocketText(socket, JSON.stringify(frame));
if (expectReceiptMs <= 0) {
return {
received: false,
detail: "Command frame sent; receipt wait disabled.",
};
}
try {
const response = await readServerWebSocketText(socket, expectReceiptMs, pendingBuffer);
pendingBuffer = response.remaining;
const parsed = JSON.parse(response.text) as unknown;
return {
received: true,
frame: isRecord(parsed) ? parsed : response.text,
remainingBytes: pendingBuffer.length,
};
} catch (error) {
return {
received: false,
detail: error instanceof Error ? error.message : String(error),
};
}
} finally {
socket.destroy();
}
}
function getSurfaceRoot(snapshot: Record<string, unknown>): Record<string, unknown> | undefined {
const surface = isRecord(snapshot.surface) ? snapshot.surface : undefined;
return isRecord(surface?.root) ? surface.root : undefined;
@ -1662,146 +1681,3 @@ function isRecord(value: unknown): value is Record<string, unknown> {
function stringValue(value: unknown): string {
return typeof value === "string" ? value : value === null || value === undefined ? "" : String(value);
}
function openWebSocketSnapshotSocket(url: URL): Promise<{ socket: net.Socket; initialBuffer: Buffer }> {
return new Promise((resolve, reject) => {
if (url.protocol === "wss:") {
reject(new Error("Odin WSS is not supported by this lightweight MCP snapshot client yet."));
return;
}
const port = Number(url.port || 80);
const socket = net.createConnection({ host: url.hostname, port, timeout: 3000 });
const key = crypto.randomBytes(16).toString("base64");
let buffer = Buffer.alloc(0);
socket.on("connect", () => {
socket.write([
`GET ${url.pathname || "/eve/deck"} HTTP/1.1`,
`Host: ${url.hostname}:${port}`,
"Upgrade: websocket",
"Connection: Upgrade",
`Sec-WebSocket-Key: ${key}`,
"Sec-WebSocket-Version: 13",
"",
"",
].join("\r\n"));
});
socket.on("data", function onHandshake(chunk) {
buffer = Buffer.concat([buffer, chunk]);
const marker = buffer.indexOf("\r\n\r\n");
if (marker < 0) {
return;
}
const header = buffer.subarray(0, marker).toString("latin1");
if (!header.startsWith("HTTP/1.1 101")) {
reject(new Error(header.split(/\r?\n/)[0] || "Odin websocket handshake failed"));
socket.destroy();
return;
}
socket.off("data", onHandshake);
resolve({ socket, initialBuffer: buffer.subarray(marker + 4) });
});
socket.on("timeout", () => {
reject(new Error("Odin websocket connection timed out"));
socket.destroy();
});
socket.on("error", reject);
});
}
function sendClientWebSocketText(socket: net.Socket, text: string): void {
const payload = Buffer.from(text, "utf8");
const mask = crypto.randomBytes(4);
const header = [0x81];
if (payload.length < 126) {
header.push(0x80 | payload.length);
} else if (payload.length <= 0xffff) {
header.push(0x80 | 126, payload.length >> 8, payload.length & 0xff);
} else {
const length = Buffer.alloc(8);
length.writeBigUInt64BE(BigInt(payload.length));
header.push(0x80 | 127, ...length);
}
const masked = Buffer.from(payload.map((byte, index) => byte ^ mask[index % 4]));
socket.write(Buffer.concat([Buffer.from(header), mask, masked]));
}
function readServerWebSocketText(
socket: net.Socket,
timeoutMs: number,
initialBuffer: Buffer<ArrayBufferLike> = Buffer.alloc(0),
): Promise<{ text: string; remaining: Buffer<ArrayBufferLike> }> {
return new Promise((resolve, reject) => {
let buffer = initialBuffer;
const timer = setTimeout(() => cleanup(new Error("Timed out waiting for Odin deck frame")), timeoutMs);
function cleanup(error?: Error, value?: { text: string; remaining: Buffer<ArrayBufferLike> }) {
clearTimeout(timer);
socket.off("data", onData);
socket.off("error", onError);
if (error) {
reject(error);
} else {
resolve(value ?? { text: "", remaining: Buffer.alloc(0) });
}
}
function onError(error: Error) {
cleanup(error);
}
function onData(chunk: Buffer) {
buffer = Buffer.concat([buffer, chunk]);
readBufferedFrame();
}
function readBufferedFrame() {
const frame = tryReadWebSocketFrame(buffer);
if (!frame) {
return;
}
buffer = buffer.subarray(frame.consumed);
if (frame.opcode === 0x1) {
cleanup(undefined, {
text: frame.payload.toString("utf8"),
remaining: buffer,
});
} else if (frame.opcode === 0x8) {
cleanup(new Error("Odin websocket closed"));
}
}
socket.on("data", onData);
socket.on("error", onError);
readBufferedFrame();
});
}
function tryReadWebSocketFrame(buffer: Buffer): { opcode: number; payload: Buffer; consumed: number } | null {
if (buffer.length < 2) {
return null;
}
const opcode = buffer[0] & 0x0f;
const masked = Boolean(buffer[1] & 0x80);
let length = buffer[1] & 0x7f;
let offset = 2;
if (length === 126) {
if (buffer.length < offset + 2) {
return null;
}
length = buffer.readUInt16BE(offset);
offset += 2;
} else if (length === 127) {
if (buffer.length < offset + 8) {
return null;
}
length = Number(buffer.readBigUInt64BE(offset));
offset += 8;
}
const mask = masked ? buffer.subarray(offset, offset + 4) : null;
if (masked) {
offset += 4;
}
if (buffer.length < offset + length) {
return null;
}
let payload = buffer.subarray(offset, offset + length);
if (mask) {
payload = Buffer.from(payload.map((byte, index) => byte ^ mask[index % 4]));
}
return { opcode, payload, consumed: offset + length };
}

View file

@ -13,8 +13,8 @@
"dev:mcp": "tsx apps/worker/src/mcp-server.ts",
"dev:worker": "tsx apps/worker/src/index.ts",
"stack:start": "powershell -ExecutionPolicy Bypass -File scripts/start-voidbot-stack.ps1",
"state:status": "tsx tools/voidbot_state.ts status",
"state:prepare-compaction": "tsx tools/voidbot_prepare_compaction.ts",
"state:status": "node tools/voidbot_state.ts status",
"state:prepare-compaction": "node tools/voidbot_prepare_compaction.ts",
"state:backup": "powershell -ExecutionPolicy Bypass -File scripts/backup-voidbot-state.ps1",
"state:restore": "powershell -ExecutionPolicy Bypass -File scripts/restore-voidbot-state.ps1",
"state:verify-backup": "powershell -ExecutionPolicy Bypass -File scripts/verify-voidbot-backup.ps1",
@ -41,6 +41,8 @@
"agents:speech-boundary-smoke": "tsx scripts/smoke-repo-face-speech-boundary.ts",
"agents:heartbeat-response-pressure-fixture": "tsx scripts/smoke-repo-face-response-pressure.ts",
"agents:memory-projection-smoke": "node scripts/smoke-repo-face-memory-projection.mjs",
"agents:shared-documents-smoke": "tsx scripts/smoke-repo-face-shared-documents.ts",
"agents:shared-documents-mcp-smoke": "node scripts/smoke-repo-face-shared-documents-mcp.mjs",
"agents:article-contract-smoke": "tsx scripts/smoke-repo-face-article-contract.ts",
"persona:export-friendly": "tsx scripts/export-persona-friendly-input.ts",
"postgres:up": "docker compose -f infra/postgres/docker-compose.yml up -d",

View file

@ -116,7 +116,7 @@ const envSchema = z.object({
REPO_FACE_GITHUB_ACTIONS_ENABLED: booleanFromEnv.default(false),
REPO_FACE_BIFROST_ENABLED: booleanFromEnv.default(false),
REPO_FACE_WEKSA_SPEECH_ENABLED: booleanFromEnv.default(false),
WEKSA_DAEMON_BASE_URL: z.string().url().default("http://127.0.0.1:8813"),
WEKSA_CULTMESH_COMMAND_URI: optionalNonEmptyString,
WEKSA_DAEMON_TIMEOUT_MS: z.coerce.number().int().positive().default(120000),
WEKSA_REPO_ROOT: z.string().min(1).default("E:/Projects/weksa"),
REPO_FACE_DISCORD_VOICE_ENABLED: booleanFromEnv.default(false),
@ -131,6 +131,10 @@ const envSchema = z.object({
METAME_OWNER_VOICE_STATE_PATH: z.string().min(1).default(".voidbot/private/personas/metame/metame-owner-voice-sync.md"),
BIFROST_ROOT: z.string().min(1).default("E:/Projects/Bifrost"),
BIFROST_DISCORD_CHANNEL_ID: optionalNonEmptyString,
BIFROST_CULTMESH_COMMAND_URI: z.string().min(1).default("cultmesh://asgard.starfire.bifrost/commands/discord-post"),
BIFROST_CULTMESH_STORE_PATH: z.string().min(1).default("E:/Projects/Bifrost/.bifrost/provider-store.cc"),
BIFROST_CULTMESH_COMMAND_TIMEOUT_MS: z.coerce.number().int().positive().default(30000),
BIFROST_CULTMESH_COMMAND_PUMP_ENABLED: booleanFromEnv.default(true),
REPO_FACE_HEARTBEATS_ENABLED: booleanFromEnv.default(false),
REPO_FACE_HEARTBEAT_STATE_PATH: z.string().min(1).default(".voidbot/status/repo-face-heartbeats.json"),
REPO_FACE_HEARTBEAT_TASK_NAME: z.string().min(1).default("VoidBot Repo Face Heartbeats"),
@ -216,7 +220,7 @@ export interface AppConfig {
repoFaceBifrostEnabled: boolean;
repoFaceWeksaSpeech: {
enabled: boolean;
daemonBaseUrl: string;
commandUri: string;
timeoutMs: number;
repoRoot: string;
};
@ -236,6 +240,12 @@ export interface AppConfig {
};
bifrostRoot: string;
bifrostDiscordChannelId?: string;
bifrostCultMesh: {
commandUri: string;
storePath: string;
timeoutMs: number;
pumpEnabled: boolean;
};
repoFaceHeartbeats: {
enabled: boolean;
statePath: string;
@ -495,7 +505,11 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): AppConfig {
repoFaceBifrostEnabled: parsed.REPO_FACE_BIFROST_ENABLED,
repoFaceWeksaSpeech: {
enabled: parsed.REPO_FACE_WEKSA_SPEECH_ENABLED,
daemonBaseUrl: parsed.WEKSA_DAEMON_BASE_URL.replace(/\/+$/, ""),
commandUri: requireWhenEnabled(
parsed.REPO_FACE_WEKSA_SPEECH_ENABLED,
parsed.WEKSA_CULTMESH_COMMAND_URI,
"WEKSA_CULTMESH_COMMAND_URI is required when REPO_FACE_WEKSA_SPEECH_ENABLED is true; provide the Odin-discovered Weksa CultMesh command URI.",
),
timeoutMs: parsed.WEKSA_DAEMON_TIMEOUT_MS,
repoRoot: resolve(parsed.WEKSA_REPO_ROOT),
},
@ -515,6 +529,12 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): AppConfig {
},
bifrostRoot: resolve(parsed.BIFROST_ROOT),
bifrostDiscordChannelId: parsed.BIFROST_DISCORD_CHANNEL_ID,
bifrostCultMesh: {
commandUri: parsed.BIFROST_CULTMESH_COMMAND_URI,
storePath: resolve(parsed.BIFROST_CULTMESH_STORE_PATH),
timeoutMs: parsed.BIFROST_CULTMESH_COMMAND_TIMEOUT_MS,
pumpEnabled: parsed.BIFROST_CULTMESH_COMMAND_PUMP_ENABLED,
},
repoFaceHeartbeats: {
enabled: parsed.REPO_FACE_HEARTBEATS_ENABLED,
statePath: resolve(parsed.REPO_FACE_HEARTBEAT_STATE_PATH),
@ -620,3 +640,13 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): AppConfig {
},
};
}
function requireWhenEnabled(enabled: boolean, value: string | undefined, message: string): string {
if (!enabled) {
return value ?? "";
}
if (value) {
return value;
}
throw new Error(message);
}

View file

@ -12,7 +12,7 @@ export function buildVoidMcpServerConfig(workspaceRoot: string): CodexMcpServerC
cwd: workspaceRoot,
env: {
VOIDBOT_WORKSPACE_ROOT: workspaceRoot,
...(process.env.ODIN_BASE_URL ? { ODIN_BASE_URL: process.env.ODIN_BASE_URL } : {}),
...(process.env.ODIN_CULTMESH_STORE ? { ODIN_CULTMESH_STORE: process.env.ODIN_CULTMESH_STORE } : {}),
},
};
}

View file

@ -11,6 +11,7 @@ export * from "./repo-face-heartbeat-queue";
export * from "./repo-face-response-pressure";
export * from "./repo-face-initializer";
export * from "./repo-face-rest";
export * from "./repo-face-shared-documents";
export * from "./repo-face-voice-presence";
export * from "./repo-face-voice-outbox";
export * from "./situational-social-read";

View file

@ -0,0 +1,112 @@
import { resolve } from "node:path";
import {
CultCache,
SingleFileMessagePackBackingStore,
defineDocumentRegistry,
defineDocumentType,
} from "cultcache-ts";
import { z } from "zod";
const nonEmpty = z.string().trim().min(1);
const documentSchema = z.object({
documentId: nonEmpty.max(120),
title: nonEmpty.max(240),
body: z.string().max(20_000),
version: z.number().int().positive(),
createdBy: nonEmpty.max(120),
createdAt: nonEmpty,
updatedBy: nonEmpty.max(120),
updatedAt: nonEmpty,
}).strict();
export type RepoFaceSharedDocument = z.infer<typeof documentSchema>;
const documentDefinition = defineDocumentType({
type: "voidbot.repo_face.shared_document",
schema: documentSchema,
name: "documentId",
});
const registry = defineDocumentRegistry(documentDefinition);
export function resolveRepoFaceSharedDocumentsPath(storageRoot: string): string {
return resolve(storageRoot, "private", "repo-face-shared-documents.cc");
}
export async function listRepoFaceSharedDocuments(canonicalPath: string): Promise<RepoFaceSharedDocument[]> {
const cache = createCache(canonicalPath);
await cache.pullAllBackingStores();
return cache.getAll(documentDefinition).sort((left, right) => left.documentId.localeCompare(right.documentId));
}
export async function createRepoFaceSharedDocument(input: {
canonicalPath: string;
documentId: string;
title: string;
body: string;
actorId: string;
now?: string;
}): Promise<RepoFaceSharedDocument> {
const documentId = normalizeDocumentId(input.documentId);
const cache = createCache(input.canonicalPath);
await cache.pullAllBackingStores();
if (cache.get(documentDefinition, documentId)) {
throw new Error(`Shared document "${documentId}" already exists; update it instead.`);
}
const now = input.now ?? new Date().toISOString();
const document = documentSchema.parse({
documentId,
title: input.title,
body: input.body,
version: 1,
createdBy: input.actorId,
createdAt: now,
updatedBy: input.actorId,
updatedAt: now,
});
return cache.put(documentDefinition, documentId, document);
}
export async function updateRepoFaceSharedDocument(input: {
canonicalPath: string;
documentId: string;
title?: string;
body: string;
expectedVersion: number;
actorId: string;
now?: string;
}): Promise<RepoFaceSharedDocument> {
const documentId = normalizeDocumentId(input.documentId);
const cache = createCache(input.canonicalPath);
await cache.pullAllBackingStores();
const existing = cache.get(documentDefinition, documentId);
if (!existing) {
throw new Error(`Shared document "${documentId}" does not exist; create it first.`);
}
if (existing.version !== input.expectedVersion) {
throw new Error(
`Shared document "${documentId}" is at version ${existing.version}, not expected version ${input.expectedVersion}.`,
);
}
const document = documentSchema.parse({
...existing,
title: input.title ?? existing.title,
body: input.body,
version: existing.version + 1,
updatedBy: input.actorId,
updatedAt: input.now ?? new Date().toISOString(),
});
return cache.put(documentDefinition, documentId, document);
}
function createCache(canonicalPath: string): CultCache {
return CultCache.builder()
.withRegistry(registry)
.withGenericStore(new SingleFileMessagePackBackingStore(resolve(canonicalPath)))
.build();
}
function normalizeDocumentId(value: string): string {
const normalized = value.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
return nonEmpty.max(120).parse(normalized);
}

View file

@ -1,5 +1,6 @@
export interface WeksaSpeechClientOptions {
baseUrl: string;
commandUri: string;
repoRoot: string;
timeoutMs: number;
}
@ -42,69 +43,23 @@ export interface WeksaMimoReceipt {
}
export class WeksaSpeechClient {
private readonly baseUrl: string;
private readonly commandUri: string;
private readonly repoRoot: string;
private readonly timeoutMs: number;
constructor(options: WeksaSpeechClientOptions) {
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
this.commandUri = options.commandUri;
this.repoRoot = options.repoRoot;
this.timeoutMs = options.timeoutMs;
}
async renderRepoFaceSpeech(input: WeksaRepoFaceSpeechInput): Promise<WeksaMimoReceipt> {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
try {
const response = await fetch(`${this.baseUrl}/speech-provider/mimo/voicedesign`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
signal: controller.signal,
body: JSON.stringify({
request_id: input.requestId ?? buildRequestId(input),
persona_state_path: input.personaStatePath,
speaker_agent_id: input.identityId,
thought_text: input.content,
spoken_text: input.content,
scene: input.scene ?? `Discord channel ${input.channelId}`,
addressee: input.addressee ?? "Discord room",
intent: input.content,
performance_register: input.performanceRegister ?? {
label: `${input.displayName} Discord voice`,
medium: "Discord voice channel playback",
delivery_archetype: `${input.displayName} speaks an approved VoidBot SAY line`,
},
delivery_controls: input.deliveryControls ?? [
"Discord-room conversational delivery",
"preserve the approved text exactly",
`repo Face for ${input.repoName}`,
],
forbidden_traits: input.forbiddenTraits ?? [
"generic assistant voice",
"rewriting source meaning",
"changing the approved Discord text",
],
voice_design_prompt: input.voiceDesignPrompt,
private_interpretation: input.privateInterpretation ??
"VoidBot already approved this SAY line for public text delivery; Weksa is only projecting it into spoken audio.",
intended_effect: input.intendedEffect ??
"Make the approved public Discord line audible without changing its meaning.",
}),
});
const text = await response.text();
if (!response.ok) {
throw new Error(`Weksa speech request failed with ${response.status}: ${text}`);
}
return JSON.parse(text) as WeksaMimoReceipt;
} catch (error) {
if (error instanceof Error && error.name === "AbortError") {
throw new Error(`Weksa speech request timed out after ${this.timeoutMs}ms.`);
}
throw error;
} finally {
clearTimeout(timeout);
}
const requestId = input.requestId ?? buildRequestId(input);
void this.repoRoot;
void this.timeoutMs;
throw new Error(
`Weksa speech command ${requestId} requires CultMesh command document publication to ${this.commandUri}; the legacy direct sender has been removed.`,
);
}
}

View file

@ -10,6 +10,16 @@ import {
type ProviderResponse,
} from "@voidbot/shared";
import {
executeToolCall,
buildToolDefinitions,
} from "./local-llm-tools";
import {
MAX_TOOL_TURNS,
type LocalLlmToolbox,
type ToolTraceRecord,
} from "./local-llm-shared";
export interface OpenAiApiProviderOptions {
enabled: boolean;
baseUrl: string;
@ -19,17 +29,31 @@ export interface OpenAiApiProviderOptions {
timeoutMs: number;
authHeader: string;
maxCompletionTokens: number;
toolbox?: LocalLlmToolbox;
}
interface ChatMessage {
role: "user" | "assistant" | "system";
content: string;
role: "user" | "assistant" | "system" | "tool";
content: string | null;
tool_call_id?: string;
tool_calls?: ChatToolCall[];
}
interface ChatToolCall {
id?: string;
type?: "function";
function?: {
name?: unknown;
arguments?: unknown;
};
}
interface ChatCompletionResponse {
choices?: Array<{
message?: {
content?: unknown;
tool_calls?: unknown;
reasoning_content?: unknown;
};
finish_reason?: unknown;
}>;
@ -84,9 +108,15 @@ export class OpenAiApiProvider implements ProviderAdapter {
content: request.contextBundle.prompt,
},
];
const tools = this.options.toolbox ? buildToolDefinitions() : undefined;
const toolTrace: ToolTraceRecord[] = [];
let finalPayload: ChatCompletionResponse | undefined;
let finalRequestBody: Record<string, unknown> | undefined;
let outputText: string | undefined;
const requestBody = {
model,
messages,
...(tools ? { tools } : {}),
stream: false,
max_completion_tokens: readPositiveIntegerOption(request.options?.maxCompletionTokens) ??
this.options.maxCompletionTokens,
@ -96,25 +126,66 @@ export class OpenAiApiProvider implements ProviderAdapter {
try {
const apiKey = await this.loadApiKey();
const response = await fetch(`${normalizeBaseUrl(this.options.baseUrl)}/chat/completions`, {
method: "POST",
headers: this.buildHeaders(apiKey),
body: JSON.stringify(requestBody),
signal: controller.signal,
});
const responseText = await response.text();
for (let turn = 0; turn <= MAX_TOOL_TURNS; turn += 1) {
const currentRequestBody = {
...requestBody,
messages,
};
finalRequestBody = currentRequestBody;
const response = await fetch(`${normalizeBaseUrl(this.options.baseUrl)}/chat/completions`, {
method: "POST",
headers: this.buildHeaders(apiKey),
body: JSON.stringify(currentRequestBody),
signal: controller.signal,
});
const responseText = await response.text();
if (!response.ok) {
throw new Error(
`OpenAI-compatible chat request failed with ${response.status}: ${redactSecret(responseText, apiKey).slice(0, 2000)}`,
);
if (!response.ok) {
throw new Error(
`OpenAI-compatible chat request failed with ${response.status}: ${redactSecret(responseText, apiKey).slice(0, 2000)}`,
);
}
const payload = JSON.parse(responseText) as ChatCompletionResponse;
finalPayload = payload;
const assistantMessage = normalizeAssistantMessage(payload);
messages.push(assistantMessage);
const toolCalls = assistantMessage.tool_calls ?? [];
if (toolCalls.length === 0) {
outputText = normalizeModelText(assistantMessage.content);
break;
}
if (!this.options.toolbox) {
outputText =
"The OpenAI-compatible model asked for tools, but this lane was not given any. That is a wiring problem.";
break;
}
if (turn >= MAX_TOOL_TURNS) {
outputText =
"The OpenAI-compatible model kept reaching for more archive context than this lane can chase in one pass. Ask more narrowly or use the deeper Codex lane.";
break;
}
for (const toolCall of toolCalls) {
const execution = await executeToolCall(
this.options.toolbox,
request.contextBundle,
toolCall,
);
toolTrace.push(execution.trace);
messages.push({
role: "tool",
tool_call_id: toolCall.id ?? `${execution.trace.tool}-${toolTrace.length}`,
content: JSON.stringify(execution.payload, null, 2),
});
}
}
const payload = JSON.parse(responseText) as ChatCompletionResponse;
const outputText = normalizeModelText(payload.choices?.[0]?.message?.content);
if (!outputText) {
const finishReason = normalizeModelText(payload.choices?.[0]?.finish_reason) ?? "unknown";
const finishReason = normalizeModelText(finalPayload?.choices?.[0]?.finish_reason) ?? "unknown";
throw new Error(
`OpenAI-compatible chat response did not include assistant content (finish_reason=${finishReason}).`,
);
@ -124,14 +195,15 @@ export class OpenAiApiProvider implements ProviderAdapter {
status: "completed",
summary: `OpenAI-compatible API response generated with ${model}.`,
outputText,
artifacts: buildArtifacts(requestBody, payload),
artifacts: buildArtifacts(finalRequestBody ?? requestBody, finalPayload ?? {}, toolTrace),
metadata: {
model,
provider_model: normalizeModelText(payload.model) ?? model,
finish_reason: normalizeModelText(payload.choices?.[0]?.finish_reason) ?? "unknown",
prompt_tokens: readNumericMetadata(payload.usage?.prompt_tokens),
completion_tokens: readNumericMetadata(payload.usage?.completion_tokens),
total_tokens: readNumericMetadata(payload.usage?.total_tokens),
provider_model: normalizeModelText(finalPayload?.model) ?? model,
finish_reason: normalizeModelText(finalPayload?.choices?.[0]?.finish_reason) ?? "unknown",
tool_calls: String(toolTrace.length),
prompt_tokens: readNumericMetadata(finalPayload?.usage?.prompt_tokens),
completion_tokens: readNumericMetadata(finalPayload?.usage?.completion_tokens),
total_tokens: readNumericMetadata(finalPayload?.usage?.total_tokens),
},
};
} catch (error) {
@ -187,6 +259,7 @@ export class OpenAiApiProvider implements ProviderAdapter {
function buildArtifacts(
requestBody: Record<string, unknown>,
payload: ChatCompletionResponse,
toolTrace: ToolTraceRecord[],
): ProviderArtifact[] {
return [
{
@ -199,9 +272,40 @@ function buildArtifacts(
contentType: "json",
content: `${JSON.stringify(payload, null, 2)}\n`,
},
{
name: "openai-api-tool-trace.json",
contentType: "json",
content: `${JSON.stringify(toolTrace, null, 2)}\n`,
},
];
}
function normalizeAssistantMessage(payload: ChatCompletionResponse): ChatMessage {
const message = payload.choices?.[0]?.message ?? {};
return {
role: "assistant",
content: normalizeModelText(message.content) ?? "",
tool_calls: normalizeChatToolCalls(message.tool_calls),
};
}
function normalizeChatToolCalls(value: unknown): ChatToolCall[] {
if (!Array.isArray(value)) {
return [];
}
return value.filter((call): call is ChatToolCall => {
if (typeof call !== "object" || call === null) {
return false;
}
const candidate = call as ChatToolCall;
return (
typeof candidate.function === "object" &&
candidate.function !== null &&
typeof candidate.function.name === "string"
);
});
}
function normalizeBaseUrl(value: string): string {
return value.replace(/\/+$/, "");
}

View file

@ -417,7 +417,11 @@ export class OwnerCodexProvider implements ProviderAdapter {
prompt: input.prompt,
imagePaths: collectCodexImagePaths(input.request.contextBundle),
mcpServers: input.command === "repo-face-rumination"
? restrictMcpServersToRepoFaceExploration(this.options.mcpServers ?? [])
? restrictMcpServersToRepoFaceExploration(
this.options.mcpServers ?? [],
getStringOption(input.request.options?.repoFaceIdentity),
getStringOption(input.request.options?.repoFaceRole),
)
: this.options.mcpServers ?? [],
});
lastResult = result;
@ -544,6 +548,8 @@ function getReasoningEffortOption(value: unknown): "none" | "low" | "medium" | "
function restrictMcpServersToRepoFaceExploration(
servers: NonNullable<OwnerCodexProviderOptions["mcpServers"]>,
identity?: string,
role?: string,
): NonNullable<OwnerCodexProviderOptions["mcpServers"]> {
const allowedTools = [
"search_history",
@ -556,6 +562,7 @@ function restrictMcpServersToRepoFaceExploration(
"get_odin_surface",
"load_odin_interface_context",
"invoke_odin_interface_command",
...(role === "face" ? ["read_shared_document", "create_shared_document", "update_shared_document"] : []),
].join(",");
return servers.map((server) => ({
@ -563,6 +570,7 @@ function restrictMcpServersToRepoFaceExploration(
env: {
...(server.env ?? {}),
VOIDBOT_MCP_TOOL_ALLOWLIST: allowedTools,
...(identity ? { VOIDBOT_REPO_FACE_IDENTITY: identity } : {}),
},
}));
}

View file

@ -1,6 +1,7 @@
Research and memory access:
- You may inspect your home repo, recent repo activity, indexed source/lore material, and archived room discussion when evidence would change what you think, say, propose, or remember.
- Your complete allowed tool set for this Face turn is: `search_history`, `get_message_context`, `list_indexed_repos`, `search_sources`, `get_source_context`, `list_odin_providers`, `list_odin_verses`, `get_odin_surface`, `load_odin_interface_context`, `invoke_odin_interface_command`.
- Your complete allowed tool set for this Face turn is: `search_history`, `get_message_context`, `list_indexed_repos`, `search_sources`, `get_source_context`, `list_odin_providers`, `list_odin_verses`, `get_odin_surface`, `load_odin_interface_context`, `invoke_odin_interface_command`, `read_shared_document`, `create_shared_document`, `update_shared_document`.
- Shared documents are typed durable scratch surfaces visible to every repo Face. Use `read_shared_document` to list or inspect them. Create one with a stable short id, title, and body; update one by supplying the revision you read so concurrent changes fail visibly instead of silently overwriting another Face. These tools do not grant arbitrary filesystem access.
- Use Odin tools for live CultMesh Verse/interface access: what Odin can currently see, which provider-owned surfaces are present, what the all-seer surface reports, and the compact TUI/action context for a specific provider.
- Treat Odin as the all-seer for the early connected Colossus: use his Verse discovery when the live shape of the mesh would change your thought, proposal, memory, or action. A Verse is a provider-owned surface becoming legible to other minds, not a generic status blob.
- Use `load_odin_interface_context` before trying to operate a service. It lowers the provider-owned CultMesh/Eve surface into compact text, tree, and advertised commands so you do not spend the turn swallowing a whole dashboard raw.

View file

@ -73,7 +73,7 @@ $action = New-ScheduledTaskAction -Execute "wscript.exe" -Argument "//B //nologo
$trigger = New-ScheduledTaskTrigger -Daily -At $runAt
$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit (New-TimeSpan -Hours 6)
$principal = New-ScheduledTaskPrincipal -UserId "$env:USERDOMAIN\$env:USERNAME" -LogonType Interactive -RunLevel Limited
$description = "Runs VoidBot offsite backup sync to the Qwen box. Interactive logon required so the SSH key material is available."
$description = "Runs VoidBot offsite backup sync to the configured SSH target. Interactive logon required so the SSH key material is available."
Register-ScheduledTask -TaskName $taskNameValue -Action $action -Trigger $trigger -Settings $settings -Principal $principal -Description $description -Force | Out-Null

View file

@ -21,7 +21,11 @@ const IDUNN_HEALTH_RUDP_CONNECTION_ID = 0x1d0d0001;
async function main() {
const options = parseArgs(process.argv.slice(2));
const endpoint = parseEndpoint(options.endpoint || process.env.VOIDBOT_IDUNN_RUDP_HEALTH || "127.0.0.1:17870");
const endpointValue = options.endpoint || process.env.VOIDBOT_IDUNN_RUDP_HEALTH;
if (!endpointValue || !String(endpointValue).trim()) {
throw new Error("VoidBot Idunn health publication requires --endpoint or VOIDBOT_IDUNN_RUDP_HEALTH; no localhost default is assumed.");
}
const endpoint = parseEndpoint(endpointValue);
const daemonId = options.daemon || process.env.VOIDBOT_IDUNN_DAEMON || "voidbot";
const healthContract = options.contract || process.env.VOIDBOT_IDUNN_HEALTH_CONTRACT || "voidbot.cultnet-rudp-stack-health";
const state = options.state || "healthy";

View file

@ -15,6 +15,17 @@ $lockPath = Join-Path $statusDir "gamecult-orchestrator.lock"
$agentSwarmPausePath = Join-Path $repoRoot "state\agent-swarm-paused.json"
$hiddenLauncher = Join-Path $PSScriptRoot "run-hidden-powershell.vbs"
$bifrostRoot = "E:\Projects\Bifrost"
$bootstrapLogPath = Join-Path $logDir "bootstrap.log"
function Write-BootstrapLog {
param([string] $Message)
try {
New-Item -ItemType Directory -Force -Path $logDir | Out-Null
$line = "[{0}] pid={1} cwd={2} {3}" -f ([DateTime]::UtcNow.ToString("o")), $PID, (Get-Location).Path, $Message
Add-Content -LiteralPath $bootstrapLogPath -Encoding UTF8 -Value $line
} catch {
}
}
function Read-DotEnv {
param([Parameter(Mandatory = $true)][string] $Path)
@ -114,10 +125,15 @@ function Publish-IdunnRudpHealth {
return
}
$endpoint = if ($env:VOIDBOT_IDUNN_RUDP_HEALTH) { $env:VOIDBOT_IDUNN_RUDP_HEALTH } else { "127.0.0.1:17870" }
$endpoint = $env:VOIDBOT_IDUNN_RUDP_HEALTH
if ([string]::IsNullOrWhiteSpace($endpoint)) {
Write-BootstrapLog "skipping-idunn-health VOIDBOT_IDUNN_RUDP_HEALTH is not configured"
return
}
$state = if ($Summary.ok) { "healthy" } else { "failed" }
$detail = "VoidBot orchestrator pulse ran $($Summary.ranCount) organ(s) in $($Summary.durationSeconds)s; ok=$($Summary.ok)"
try {
Write-BootstrapLog "publishing-idunn-health state=$state ran=$($Summary.ranCount) duration=$($Summary.durationSeconds)"
$nodeExe = (Get-Command node.exe -ErrorAction Stop).Source
$output = & $nodeExe $publisher `
--endpoint $endpoint `
@ -129,9 +145,106 @@ function Publish-IdunnRudpHealth {
if ($LASTEXITCODE -ne 0) {
throw "publisher exited with code $LASTEXITCODE`: $($output -join [Environment]::NewLine)"
}
Write-BootstrapLog "published-idunn-health"
} catch {
$message = "[$([DateTime]::UtcNow.ToString("o"))] Idunn RUDP health publish failed: $($_.Exception.Message)"
Add-Content -LiteralPath (Join-Path $logDir "idunn-rudp-health.log") -Encoding UTF8 -Value $message
Write-BootstrapLog "publish-idunn-health-failed $($_.Exception.Message)"
}
}
function Get-ConfigString {
param(
[hashtable] $Config,
[string] $Name,
[string] $Default
)
$raw = [Environment]::GetEnvironmentVariable($Name)
if ([string]::IsNullOrWhiteSpace($raw) -and $Config.ContainsKey($Name)) {
$raw = $Config[$Name]
}
if ([string]::IsNullOrWhiteSpace($raw)) {
return $Default
}
return $raw.Trim()
}
function Find-VoidBotSwarmCultMeshProcess {
param([string] $ScriptPath)
$escaped = $ScriptPath.Replace("\", "\\")
return @(Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | Where-Object {
$_.Name -eq "node.exe" -and
$_.CommandLine -and
($_.CommandLine -like "*serve-voidbot-swarm-cultmesh.cjs*" -or $_.CommandLine -like "*$escaped*")
})
}
function Ensure-VoidBotSwarmCultMeshServer {
param(
[hashtable] $Config,
[string] $NodePath
)
$scriptPath = Join-Path $PSScriptRoot "serve-voidbot-swarm-cultmesh.cjs"
if (-not (Test-Path -LiteralPath $scriptPath)) {
throw "VoidBot swarm CultMesh server script is missing at $scriptPath."
}
$storePath = Get-ConfigString -Config $Config -Name "VOIDBOT_SWARM_CULTMESH_STORE" -Default (Join-Path $statusDir "cultmesh\voidbot-swarm-state.cc")
$storePath = [System.IO.Path]::GetFullPath($storePath)
if (-not (Test-Path -LiteralPath $storePath)) {
throw "VoidBot swarm CultMesh store is missing at $storePath."
}
$bind = Get-ConfigString -Config $Config -Name "VOIDBOT_SWARM_CULTMESH_BIND" -Default "127.0.0.1:17873"
$odinCultMeshUri = Get-ConfigString -Config $Config -Name "VOIDBOT_ODIN_CULTMESH_URI" -Default (
Get-ConfigString -Config $Config -Name "ODIN_CULTMESH_URI" -Default "cultmesh://odin/rendezvous/provider-catalog"
)
$odinRudpEndpoint = Get-ConfigString -Config $Config -Name "VOIDBOT_ODIN_RUDP" -Default (
Get-ConfigString -Config $Config -Name "CULTMESH_URI_ODIN_RUDP" -Default "rudp://127.0.0.1:17871"
)
$existing = @(Find-VoidBotSwarmCultMeshProcess -ScriptPath $scriptPath)
if ($existing.Count -gt 0) {
return [pscustomobject]@{
status = "already_running"
pid = [int]$existing[0].ProcessId
bind = $bind
storePath = $storePath
odinCultMeshUri = $odinCultMeshUri
odinRudpEndpoint = $odinRudpEndpoint
}
}
$stdoutPath = Join-Path $logDir "voidbot-swarm-cultmesh.stdout.log"
$stderrPath = Join-Path $logDir "voidbot-swarm-cultmesh.stderr.log"
New-Item -ItemType Directory -Force -Path $logDir | Out-Null
$arguments = @(
$scriptPath,
"--store", $storePath,
"--bind", $bind,
"--odin-cultmesh-uri", $odinCultMeshUri,
"--odin-rudp-endpoint", $odinRudpEndpoint
)
$process = Start-Process -FilePath $NodePath -ArgumentList $arguments -WorkingDirectory $repoRoot -RedirectStandardOutput $stdoutPath -RedirectStandardError $stderrPath -WindowStyle Hidden -PassThru
Start-Sleep -Seconds 2
$process.Refresh()
if ($process.HasExited) {
$stdoutTail = if (Test-Path -LiteralPath $stdoutPath) { (Get-Content -LiteralPath $stdoutPath -Tail 40) -join "`n" } else { "" }
$stderrTail = if (Test-Path -LiteralPath $stderrPath) { (Get-Content -LiteralPath $stderrPath -Tail 40) -join "`n" } else { "" }
throw "VoidBot swarm CultMesh server exited early.`nSTDOUT:`n$stdoutTail`nSTDERR:`n$stderrTail"
}
return [pscustomobject]@{
status = "started"
pid = $process.Id
bind = $bind
storePath = $storePath
odinCultMeshUri = $odinCultMeshUri
odinRudpEndpoint = $odinRudpEndpoint
stdoutPath = $stdoutPath
stderrPath = $stderrPath
}
}
@ -184,6 +297,7 @@ function Invoke-Organ {
New-Item -ItemType Directory -Force -Path $logDir | Out-Null
$startedAt = [DateTime]::UtcNow
Write-BootstrapLog "invoke-organ start id=$($Organ.Id) executable=$($Organ.Executable)"
$exitCode = 0
$timedOut = $false
try {
@ -200,6 +314,8 @@ function Invoke-Organ {
$process = [System.Diagnostics.Process]::new()
$process.StartInfo = $startInfo
[void]$process.Start()
$stdoutTask = $process.StandardOutput.ReadToEndAsync()
$stderrTask = $process.StandardError.ReadToEndAsync()
$timeoutSeconds = [Math]::Max(60, $Organ.TimeoutMinutes * 60)
if (-not $process.WaitForExit($timeoutSeconds * 1000)) {
@ -207,8 +323,8 @@ function Invoke-Organ {
cmd /c "taskkill /PID $($process.Id) /T /F" | Out-Null
$process.WaitForExit()
}
$stdout = $process.StandardOutput.ReadToEnd()
$stderr = $process.StandardError.ReadToEnd()
$stdout = $stdoutTask.GetAwaiter().GetResult()
$stderr = $stderrTask.GetAwaiter().GetResult()
[System.IO.File]::WriteAllText($stdoutPath, $stdout, [System.Text.UTF8Encoding]::new($false))
[System.IO.File]::WriteAllText($stderrPath, $stderr, [System.Text.UTF8Encoding]::new($false))
$exitCode = if ($timedOut) { 124 } elseif ($null -eq $process.ExitCode) { 0 } else { $process.ExitCode }
@ -242,6 +358,7 @@ function Invoke-Organ {
$stderr.Trim()
) -join [Environment]::NewLine
[System.IO.File]::WriteAllText($logPath, $combined.Trim(), [System.Text.UTF8Encoding]::new($false))
Write-BootstrapLog "invoke-organ finish id=$($Organ.Id) status=$(if ($timedOut) { 'timed_out' } elseif ($exitCode -eq 0) { 'ok' } else { 'failed' }) exit=$exitCode"
return [pscustomobject]@{
id = $Organ.Id
@ -291,6 +408,31 @@ function Test-OrchestratorLock {
return $true
}
function Acquire-OrchestratorLock {
$directory = Split-Path -Parent $lockPath
New-Item -ItemType Directory -Force -Path $directory | Out-Null
for ($attempt = 0; $attempt -lt 2; $attempt++) {
try {
$stream = [System.IO.File]::Open($lockPath, [System.IO.FileMode]::CreateNew, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::Read)
$payload = @{
pid = $PID
startedAt = ([DateTime]::UtcNow).ToString("o")
} | ConvertTo-Json -Depth 4
$bytes = [System.Text.UTF8Encoding]::new($false).GetBytes($payload)
$stream.Write($bytes, 0, $bytes.Length)
$stream.Flush()
return $stream
} catch [System.IO.IOException] {
if (Test-OrchestratorLock) {
return $null
}
}
}
return $null
}
function Test-AgentSwarmPaused {
if (-not (Test-Path -LiteralPath $agentSwarmPausePath)) {
return $false
@ -310,12 +452,15 @@ function Test-AgentSwarmPaused {
}
New-Item -ItemType Directory -Force -Path $statusDir, $logDir | Out-Null
if (Test-OrchestratorLock) {
Write-BootstrapLog "script-start only=$($Only -join ',') force=$Force"
$lockHandle = Acquire-OrchestratorLock
if ($null -eq $lockHandle) {
Write-BootstrapLog "lock-already-active"
exit 0
}
$startedAt = [DateTime]::UtcNow
Write-JsonFile -Path $lockPath -Data @{ pid = $PID; startedAt = $startedAt.ToString("o") }
Write-BootstrapLog "lock-created path=$lockPath"
try {
$config = Read-DotEnv -Path (Join-Path $repoRoot ".env")
@ -449,8 +594,9 @@ try {
}
$result = Invoke-Organ -Organ $organ -Now $now
$serverState = $null
$runs += $result
Set-OrganState -State $state -Id $organ.Id -Value ([pscustomobject]@{
$organState = [pscustomobject]@{
label = $organ.Label
intervalMinutes = $organ.IntervalMinutes
lastStartedAt = $result.startedAt
@ -458,7 +604,11 @@ try {
lastExitCode = $result.exitCode
lastStatus = $result.status
lastLogPath = $result.logPath
})
}
if ($null -ne $serverState) {
$organState | Add-Member -NotePropertyName cultMeshServer -NotePropertyValue $serverState -Force
}
Set-OrganState -State $state -Id $organ.Id -Value $organState
Write-JsonFile -Path $statePath -Data $state
}
@ -475,11 +625,18 @@ try {
}
Write-JsonFile -Path (Join-Path $statusDir "gamecult-orchestrator-last-run.json") -Data $summary
Write-JsonFile -Path $statePath -Data $state
Write-BootstrapLog "summary-written ok=$($summary.ok) ran=$($summary.ranCount)"
Publish-IdunnRudpHealth -Summary $summary
if (-not $summary.ok) {
Write-BootstrapLog "script-exit-failed"
exit 1
}
Write-BootstrapLog "script-exit-ok"
} finally {
if ($null -ne $lockHandle) {
$lockHandle.Dispose()
}
Remove-Item -LiteralPath $lockPath -Force -ErrorAction SilentlyContinue
Write-BootstrapLog "lock-removed"
}

View file

@ -0,0 +1,75 @@
#!/usr/bin/env node
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { existsSync } from "node:fs";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
const workspaceRoot = resolve(process.cwd());
const serverPath = resolve(workspaceRoot, "apps", "worker", "dist", "mcp-server.js");
const storageRoot = await mkdtemp(join(tmpdir(), "voidbot-shared-documents-mcp-"));
if (!existsSync(serverPath)) {
throw new Error(`MCP server entrypoint is missing at ${serverPath}. Run the build first.`);
}
const expectedTools = ["read_shared_document", "create_shared_document", "update_shared_document"];
const transport = new StdioClientTransport({
command: process.execPath,
args: [serverPath],
cwd: workspaceRoot,
env: {
...process.env,
STORAGE_ROOT: storageRoot,
REPO_DISCORD_IDENTITIES_PATH: resolve(workspaceRoot, ".voidbot", "private", "repo-discord-identities.json"),
VOIDBOT_REPO_FACE_IDENTITY: "nibu",
VOIDBOT_MCP_TOOL_ALLOWLIST: expectedTools.join(","),
},
stderr: "pipe",
});
const client = new Client({ name: "voidbot-shared-documents-smoke", version: "0.0.0" });
try {
await client.connect(transport);
const tools = await client.listTools();
const names = tools.tools.map((tool) => tool.name).sort();
if (JSON.stringify(names) !== JSON.stringify([...expectedTools].sort())) {
throw new Error(`Unexpected shared-document tool surface: ${names.join(", ")}`);
}
const created = await client.callTool({
name: "create_shared_document",
arguments: { documentId: "shared-scratch", title: "Shared scratch", body: "First mark" },
});
assertOk(created, "create_shared_document");
if (created.structuredContent?.document?.createdBy !== "nibu") {
throw new Error("MCP server did not stamp the active Face identity on create.");
}
const read = await client.callTool({ name: "read_shared_document", arguments: { documentId: "shared-scratch" } });
assertOk(read, "read_shared_document");
if (read.structuredContent?.document?.version !== 1) {
throw new Error("MCP read did not return the created revision.");
}
const updated = await client.callTool({
name: "update_shared_document",
arguments: { documentId: "shared-scratch", body: "Second mark", expectedVersion: 1 },
});
assertOk(updated, "update_shared_document");
if (updated.structuredContent?.document?.version !== 2) {
throw new Error("MCP update did not advance the revision.");
}
process.stdout.write(`${JSON.stringify({ ok: true, tools: names, document: updated.structuredContent?.document })}\n`);
} finally {
await client.close();
await rm(storageRoot, { recursive: true, force: true });
}
function assertOk(result, tool) {
if (result.isError) {
const text = (result.content ?? []).map((entry) => entry.type === "text" ? entry.text : "").join("\n");
throw new Error(`${tool} failed: ${text}`);
}
}

View file

@ -0,0 +1,66 @@
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
createRepoFaceSharedDocument,
listRepoFaceSharedDocuments,
updateRepoFaceSharedDocument,
} from "@voidbot/core";
async function main(): Promise<void> {
const directory = await mkdtemp(join(tmpdir(), "voidbot-shared-documents-"));
const path = join(directory, "shared.cc");
try {
const created = await createRepoFaceSharedDocument({
canonicalPath: path,
documentId: "coordination-note",
title: "Coordination note",
body: "Initial state",
actorId: "face-a",
now: "2026-07-12T00:00:00.000Z",
});
if (created.version !== 1 || created.createdBy !== "face-a") {
throw new Error("Create lost initial revision provenance.");
}
const updated = await updateRepoFaceSharedDocument({
canonicalPath: path,
documentId: "coordination-note",
body: "Updated state",
expectedVersion: 1,
actorId: "face-b",
now: "2026-07-12T00:05:00.000Z",
});
if (updated.version !== 2 || updated.updatedBy !== "face-b") {
throw new Error("Update lost revision provenance.");
}
let rejected = false;
try {
await updateRepoFaceSharedDocument({
canonicalPath: path,
documentId: "coordination-note",
body: "Stale overwrite",
expectedVersion: 1,
actorId: "face-c",
});
} catch {
rejected = true;
}
if (!rejected) {
throw new Error("A stale update overwrote shared state.");
}
const documents = await listRepoFaceSharedDocuments(path);
if (documents.length !== 1 || documents[0]?.body !== "Updated state") {
throw new Error("Canonical shared state was not preserved.");
}
console.log(JSON.stringify({ ok: true, document: documents[0] }));
} finally {
await rm(directory, { recursive: true, force: true });
}
}
void main();

View file

@ -253,49 +253,64 @@ function Ensure-DockerOnPath {
} -FailureMessage "Docker is installed but the daemon is not ready." -Attempts 45 -SleepSeconds 2
}
function Ensure-BifrostLocalStack {
function Ensure-BifrostCultMeshCommandStore {
param(
[Parameter(Mandatory = $true)]
[string] $BifrostRoot,
[Parameter(Mandatory = $true)]
[string] $HealthUrl
[string] $StorePath,
[Parameter(Mandatory = $true)]
[string] $CommandUri
)
if (Test-HttpEndpoint -Url $HealthUrl) {
return @{
root = $BifrostRoot
healthUrl = $HealthUrl
startedByScript = $false
healthy = $true
}
if ($CommandUri -notmatch '^cultmesh://[^/]+/commands/discord-post(?:$|[/?#])') {
throw "BIFROST_CULTMESH_COMMAND_URI must be a CultMesh Discord command URI, got '$CommandUri'."
}
if (-not (Test-Path $BifrostRoot)) {
throw "Bifrost root does not exist at $BifrostRoot."
}
$composeFile = Join-Path $BifrostRoot "compose.local.yaml"
if (-not (Test-Path $composeFile)) {
throw "Bifrost local compose file does not exist at $composeFile."
$processor = Join-Path $BifrostRoot "tools\cultmesh-bridge-commands.mjs"
if (-not (Test-Path $processor)) {
throw "Bifrost CultMesh command processor does not exist at $processor."
}
Ensure-DockerOnPath
& docker compose -f $composeFile --project-directory $BifrostRoot up -d
if ($LASTEXITCODE -ne 0) {
throw "Docker Compose failed to start Bifrost."
$storeParent = Split-Path -Parent $StorePath
if (-not [string]::IsNullOrWhiteSpace($storeParent)) {
New-Item -ItemType Directory -Force -Path $storeParent | Out-Null
}
Wait-Until -Condition { Test-HttpEndpoint -Url $HealthUrl } -FailureMessage "Bifrost did not become reachable at $HealthUrl in time." -Attempts 45 -SleepSeconds 2
$exportedByScript = $false
if (-not (Test-Path $StorePath)) {
$exporter = Join-Path $BifrostRoot "tools\provider-advertisement.mjs"
if (-not (Test-Path $exporter)) {
throw "Bifrost provider advertisement exporter does not exist at $exporter."
}
$nodeCommand = Get-Command node.exe -ErrorAction Stop
& $nodeCommand.Source $exporter export --out $StorePath
if ($LASTEXITCODE -ne 0) {
throw "Bifrost provider CultMesh store export failed."
}
$exportedByScript = $true
}
$storeItem = Get-Item -LiteralPath $StorePath -ErrorAction Stop
if ($storeItem.Length -le 0) {
throw "Bifrost provider CultMesh store is empty at $StorePath."
}
return @{
root = $BifrostRoot
composeFile = $composeFile
healthUrl = $HealthUrl
startedByScript = $true
commandUri = $CommandUri
storePath = $StorePath
processor = $processor
exportedByScript = $exportedByScript
healthy = $true
healthAuthority = "cultmesh-command-store"
}
}
@ -561,7 +576,8 @@ $localLlmBaseUrl = if ($config.ContainsKey("LOCAL_LLM_OLLAMA_BASE_URL")) { $conf
$localLlmModel = if ($config.ContainsKey("LOCAL_LLM_OLLAMA_MODEL")) { $config["LOCAL_LLM_OLLAMA_MODEL"] } else { "qwen3.5:9b" }
$localLlmSocialReadModel = if ($config.ContainsKey("LOCAL_LLM_SOCIAL_READ_OLLAMA_MODEL") -and -not [string]::IsNullOrWhiteSpace($config["LOCAL_LLM_SOCIAL_READ_OLLAMA_MODEL"])) { $config["LOCAL_LLM_SOCIAL_READ_OLLAMA_MODEL"] } else { $localLlmModel }
$bifrostRoot = if ($config.ContainsKey("BIFROST_ROOT")) { [System.IO.Path]::GetFullPath($config["BIFROST_ROOT"]) } else { "E:\Projects\Bifrost" }
$bifrostHealthUrl = if ($config.ContainsKey("BIFROST_HEALTH_URL")) { $config["BIFROST_HEALTH_URL"] } else { "http://127.0.0.1:5080/healthz" }
$bifrostCultMeshCommandUri = if ($config.ContainsKey("BIFROST_CULTMESH_COMMAND_URI")) { $config["BIFROST_CULTMESH_COMMAND_URI"] } else { "cultmesh://asgard.starfire.bifrost/commands/discord-post" }
$bifrostCultMeshStorePath = if ($config.ContainsKey("BIFROST_CULTMESH_STORE_PATH")) { [System.IO.Path]::GetFullPath($config["BIFROST_CULTMESH_STORE_PATH"]) } else { "E:\Projects\Bifrost\.bifrost\provider-store.cc" }
if ($stateStorageBackend -eq "postgres") {
$status.stage = "postgres"
@ -637,7 +653,7 @@ if ($localLlmEnabled) {
$status.stage = "bifrost"
Write-StatusFile -Path $statusPath -Status $status
$status.bifrost = Ensure-BifrostLocalStack -BifrostRoot $bifrostRoot -HealthUrl $bifrostHealthUrl
$status.bifrost = Ensure-BifrostCultMeshCommandStore -BifrostRoot $bifrostRoot -StorePath $bifrostCultMeshStorePath -CommandUri $bifrostCultMeshCommandUri
Write-StatusFile -Path $statusPath -Status $status
$npmCommand = Get-Command npm.cmd -ErrorAction Stop

View file

@ -1,5 +1,5 @@
{
"paused": true,
"reason": "Re-paused by Codex on 2026-07-03 after MiMo quality gates passed but the live GameCult Local Orchestrator repeatedly reached repo-face-heartbeats and then failed to advance its status/lock summary. The model/provider path is good enough for more tests; the live scheduler handoff needs diagnosis before unattended turns.",
"updatedAt": "2026-07-03T01:31:12.1534400+01:00"
"paused": false,
"reason": "Unpaused by Codex on 2026-07-04 after Bifrost/Discord delivery was wired through typed CultMesh command/receipt documents and a bounded Weksa live-fire post completed visibly through the new route. Keep using CultMesh URIs and provider/interface command metadata; do not restore BIFROST_BRIDGE_BASE_URL/token shortcuts.",
"updatedAt": "2026-07-04T00:44:00.0000000+01:00"
}

2
vendor/cultcache-ts vendored

@ -1 +1 @@
Subproject commit 526a47e06a4f040cc40db24de4d969d42eb61fbe
Subproject commit 3eef1f1f1b7d2b493ac46a8c3f8f207b3a907844