Unify typed agent transport and shared state
This commit is contained in:
parent
2059411e85
commit
c4376ee129
23 changed files with 1434 additions and 538 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
75
scripts/smoke-repo-face-shared-documents-mcp.mjs
Normal file
75
scripts/smoke-repo-face-shared-documents-mcp.mjs
Normal 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}`);
|
||||
}
|
||||
}
|
||||
66
scripts/smoke-repo-face-shared-documents.ts
Normal file
66
scripts/smoke-repo-face-shared-documents.ts
Normal 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();
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue