Keep VoidBot MCP alive across deployments
This commit is contained in:
parent
a21d173ec0
commit
03edb8fffc
9 changed files with 120 additions and 12 deletions
|
|
@ -1,3 +1,8 @@
|
|||
[mcp_servers.voidbot]
|
||||
command = "C:\\Windows\\System32\\OpenSSH\\ssh.exe"
|
||||
args = ["-T", "-i", "C:\\Users\\Meta\\.ssh\\gamecultadmin_gamecult_org_ed25519", "gamecultadmin@94.130.223.131", "sudo", "docker", "exec", "-i", "compose-worker-1", "node", "/app/apps/worker/dist/mcp-server.js"]
|
||||
command = "C:\\Users\\Meta\\.cache\\codex-runtimes\\codex-primary-runtime\\dependencies\\node\\bin\\node.exe"
|
||||
args = ["E:\\Projects\\VoidBot\\scripts\\serve-voidbot-remote-mcp-relay.mjs"]
|
||||
|
||||
[mcp_servers.voidbot.env]
|
||||
VOIDBOT_MCP_REMOTE_HOST = "gamecultadmin@94.130.223.131"
|
||||
VOIDBOT_MCP_SSH_KEY_PATH = "C:\\Users\\Meta\\.ssh\\gamecultadmin_gamecult_org_ed25519"
|
||||
VOIDBOT_MCP_SSH_EXECUTABLE = "C:\\Windows\\System32\\OpenSSH\\ssh.exe"
|
||||
|
|
|
|||
|
|
@ -618,7 +618,7 @@ Moderation heartbeat, person-shaped rumination, physiology, memory maintenance,
|
|||
|
||||
Chronological Discord evidence is acquired by the daemon's bounded evidence source. Semantic retrieval remains available for deeper context, but it does not own the heartbeat clock or cursor.
|
||||
|
||||
Agent access to Discord history also executes at the archive owner. `.codex/config.toml` starts the `voidbot` stdio MCP server inside Yggdrasil's worker container over SSH; it must not start a Starfire-local MCP process pointed at `127.0.0.1:6333`. Verify the complete agent path with `scripts/smoke-voidbot-remote-mcp.mjs` and the `VOIDBOT_MCP_REMOTE_HOST`, `VOIDBOT_MCP_SSH_KEY_PATH`, and optional `VOIDBOT_MCP_SSH_EXECUTABLE` environment variables.
|
||||
Agent access to Discord history and indexed sources also executes at the archive owner. `.codex/config.toml` starts a stable local stdio relay; each tool call opens a fresh SSH-backed MCP session inside Yggdrasil's current worker container. Idunn may replace that container without permanently closing an already-open agent task. The relay must not start a Starfire-local VoidBot MCP body pointed at `127.0.0.1:6333`. Verify the direct Ygg body with `scripts/smoke-voidbot-remote-mcp.mjs`, then verify the durable agent path with `scripts/smoke-voidbot-remote-mcp-relay.mjs`. Both use `VOIDBOT_MCP_REMOTE_HOST`, `VOIDBOT_MCP_SSH_KEY_PATH`, and optional `VOIDBOT_MCP_SSH_EXECUTABLE` environment variables.
|
||||
There is also a repo-weather helper:
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -214,7 +214,7 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou
|
|||
- The one-minute Persona scheduler pulse is an observation clock, not an inference cadence. With the swarm brake present it returns before registry, Discord, embedding, storage, or dispatch work; current Ygg logs show `selected: []` and `queued: []`. Unprompted speech also has a separate global admission window. Do not cool token use by making observation blind; keep turn authorization at the initiative owner.
|
||||
- Revision `f97e67b` adds the portable moderation-heartbeat coordinator and enforcement organ behind a separate disabled-by-default 15-minute cadence. The model returns an exact review ledger plus urgent IDs and typed case operations; missing evidence or unaccounted urgent evidence blocks cursor advancement. The attempt is receipted by latest message before inference, so one unchanged archive window cannot burn tokens repeatedly. Case enforcement is separate, scans typed pending debt on every cadence, and resolves a case only after a required Discord ban succeeds. Ygg's first `log_only` window at `0629fc7` failed safely with one 401, zero cases, zero cursor movement, and zero sanctions; both supported auth-header shapes prove the mounted MiMo credential itself is stale. `VOID_MODERATION_DAEMON_ENABLED` is false again pending a refreshed provider credential or deliberate alternate model port.
|
||||
- The shared Bifrost Discord mouth now lives in `packages/core/src/bifrost-discord-command.ts`. Worker repo-Face speech and resident Void candidate delivery use the same stable-id typed command, provider-store write, optional Bifrost pump, and completed-receipt check. The candidate organ selects at most one targeted queued candidate and writes its speech receipt/`spoken` transition only after transport completes. Ygg mounts the Bifrost provider store and CultLib runtime into the scheduler; `VOID_CANDIDATE_DELIVERY_ENABLED` controls this transport-only organ independently from the disabled moderation/rumination model cadence.
|
||||
- Discord archive/source retrieval for Codex agents runs through the VoidBot stdio MCP body inside Yggdrasil's worker container over SSH. Repo and global Codex config must not launch the Starfire-local `dist/mcp-server.js`, because its loopback Qdrant address points at a backend that no longer exists there. `scripts/smoke-voidbot-remote-mcp.mjs` proves tool advertisement plus a real `search_history` call against Ygg.
|
||||
- Discord archive/source retrieval for Codex agents runs through `scripts/serve-voidbot-remote-mcp-relay.mjs`. Its Starfire-local stdio surface stays attached to Codex while each tool request opens a fresh SSH session to the VoidBot MCP body inside Yggdrasil's current worker container. This keeps Idunn container replacement from leaving an agent task with a permanently closed transport. Repo and global Codex config must not launch the Starfire-local VoidBot `dist/mcp-server.js`, because its loopback Qdrant address points at no local backend. `scripts/smoke-voidbot-remote-mcp.mjs` proves the direct Ygg body; `scripts/smoke-voidbot-remote-mcp-relay.mjs` proves indexed repo listing and source search through the stable agent surface.
|
||||
- Resident person-shaped rumination is wired behind `VOID_RUMINATION_DAEMON_ENABLED` with its own bounded cadence, Discord evidence window, dedicated doctrine, rules, and voice inputs. The pressure fingerprint is receipted before the model attempt and unchanged pressure cannot retry on the observation clock. Ygg explicitly keeps this false while the MiMo credential is stale; candidate delivery may remain enabled because it performs no inference and the live queue was empty at rollout.
|
||||
- Revisions `c0cf875` and `1527b29` extract person-shaped rumination and candidate delivery without scheduling them. Rumination context, operation policy, advocacy accountability, tool-free text actuation, and coordinator are portable; the attempt key hashes meaning-bearing pressure/evidence and ignores elapsed clock, so unchanged state cannot create periodic thought. Candidate delivery is deterministic and parent-owned: one targeted queued candidate, duplicate-receipt refusal, injected Bifrost port, then one `mark_candidate_intervention_spoken` operation with the transport receipt. The next cut is extracting the worker's existing typed Bifrost Discord command path into a shared port rather than duplicating CultMesh mechanics.
|
||||
- Deterministic state-packet assembly now lives in `persona-state-packet-projector.ts`, which takes typed state, registry/room evidence, pronoun guidance, curiosity facts, and an explicit observation time. The shell no longer owns section ordering or social-context composition. The downstream regex vocabulary cleaner was deleted: it was a symptom compensator rewriting ordinary words such as `witness` and `canary` after projection. A smoke proves literal source language survives the projector unchanged.
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ VoidBot runs on Yggdrasil. Idunn owns deployment and consumes upstream repositor
|
|||
5. `scripts/reconcile-source-repos.ts` discovers local Git repos, refreshes push hooks, prunes stale repo shards, and indexes newly discovered repos.
|
||||
6. `scripts/index-source-repos.ts` and `scripts/git-post-push-index.mjs` drive explicit or detached per-repo source/lore reindex work.
|
||||
7. `apps/worker/src/mcp-server.ts` boots the MCP lane, while `mcp-server-resources.ts` and `mcp-server-tools.ts` expose retrieval to Codex and other sessions through `search_history`, `get_message_context`, `search_sources`, `get_source_context`, and `list_indexed_repos`.
|
||||
8. Agent hosts start that stdio MCP body inside `compose-worker-1` on Yggdrasil over SSH. Qdrant stays loopback-private on Yggdrasil; agents receive MCP tools, not a raw vector-database route. A Starfire-local MCP body with `QDRANT_URL=http://127.0.0.1:6333` is invalid after the deployment move.
|
||||
8. Agent hosts attach to `scripts/serve-voidbot-remote-mcp-relay.mjs` over local stdio. For each tool request, the relay starts a fresh SSH-backed MCP client inside the current `compose-worker-1` on Yggdrasil and closes it after the result. The relay owns stable agent transport; the replaceable worker owns retrieval execution. An Idunn container replacement can interrupt one active request but cannot permanently close the agent task's MCP surface. Qdrant stays loopback-private on Yggdrasil; agents receive MCP tools, not a raw vector-database route. A Starfire-local VoidBot MCP body with `QDRANT_URL=http://127.0.0.1:6333` is invalid after the deployment move.
|
||||
|
||||
## Flow 4: Ops And Recovery
|
||||
|
||||
|
|
|
|||
50
scripts/serve-voidbot-remote-mcp-relay.mjs
Normal file
50
scripts/serve-voidbot-remote-mcp-relay.mjs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
#!/usr/bin/env node
|
||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
|
||||
const host = process.env.VOIDBOT_MCP_REMOTE_HOST;
|
||||
const keyPath = process.env.VOIDBOT_MCP_SSH_KEY_PATH;
|
||||
if (!host || !keyPath) {
|
||||
throw new Error("VOIDBOT_MCP_REMOTE_HOST and VOIDBOT_MCP_SSH_KEY_PATH are required.");
|
||||
}
|
||||
|
||||
const server = new Server(
|
||||
{ name: "voidbot-yggdrasil-relay", version: "1.0.0" },
|
||||
{ capabilities: { tools: {} } },
|
||||
);
|
||||
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () =>
|
||||
withRemoteClient((client) => client.listTools()),
|
||||
);
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) =>
|
||||
withRemoteClient((client) => client.callTool(request.params)),
|
||||
);
|
||||
|
||||
async function withRemoteClient(operation) {
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.env.VOIDBOT_MCP_SSH_EXECUTABLE ?? "ssh",
|
||||
args: [
|
||||
"-T", "-i", keyPath, host,
|
||||
"sudo", "docker", "exec", "-i", "compose-worker-1",
|
||||
"node", "/app/apps/worker/dist/mcp-server.js",
|
||||
],
|
||||
env: process.env,
|
||||
stderr: "pipe",
|
||||
});
|
||||
const client = new Client({ name: "voidbot-yggdrasil-relay-upstream", version: "1.0.0" });
|
||||
try {
|
||||
await client.connect(transport);
|
||||
return await operation(client);
|
||||
} finally {
|
||||
await client.close().catch(() => undefined);
|
||||
}
|
||||
}
|
||||
|
||||
await server.connect(new StdioServerTransport());
|
||||
32
scripts/smoke-voidbot-remote-mcp-relay.mjs
Normal file
32
scripts/smoke-voidbot-remote-mcp-relay.mjs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
||||
|
||||
const node = process.execPath;
|
||||
const transport = new StdioClientTransport({
|
||||
command: node,
|
||||
args: ["scripts/serve-voidbot-remote-mcp-relay.mjs"],
|
||||
env: process.env,
|
||||
stderr: "inherit",
|
||||
});
|
||||
const client = new Client({ name: "voidbot-remote-relay-smoke", version: "1.0.0" });
|
||||
|
||||
try {
|
||||
await client.connect(transport);
|
||||
const tools = await client.listTools();
|
||||
const repositories = await client.callTool({ name: "list_indexed_repos", arguments: {} });
|
||||
const sources = await client.callTool({
|
||||
name: "search_sources",
|
||||
arguments: { query: "VoidBot indexed source retrieval", repoName: "VoidBot", limit: 1 },
|
||||
});
|
||||
if (repositories.isError || sources.isError) {
|
||||
throw new Error("Remote relay returned an MCP error for indexed-source retrieval.");
|
||||
}
|
||||
process.stdout.write(`${JSON.stringify({
|
||||
ok: true,
|
||||
toolCount: tools.tools.length,
|
||||
repoCount: repositories.structuredContent?.repoCount,
|
||||
sourceResultCount: sources.structuredContent?.resultCount,
|
||||
})}\n`);
|
||||
} finally {
|
||||
await client.close();
|
||||
}
|
||||
|
|
@ -22,18 +22,38 @@ const client = new Client({ name: "voidbot-remote-mcp-smoke", version: "1.0.0" }
|
|||
try {
|
||||
await client.connect(transport);
|
||||
const tools = await client.listTools();
|
||||
if (!tools.tools.some((tool) => tool.name === "search_history")) {
|
||||
throw new Error("Remote VoidBot MCP did not advertise search_history.");
|
||||
for (const requiredTool of ["search_history", "list_indexed_repos", "search_sources"]) {
|
||||
if (!tools.tools.some((tool) => tool.name === requiredTool)) {
|
||||
throw new Error(`Remote VoidBot MCP did not advertise ${requiredTool}.`);
|
||||
}
|
||||
}
|
||||
const result = await client.callTool({
|
||||
const historyResult = await client.callTool({
|
||||
name: "search_history",
|
||||
arguments: { query: "VoidBot deployment Yggdrasil", limit: 2 },
|
||||
});
|
||||
if (result.isError) {
|
||||
const detail = result.content?.map((entry) => entry.type === "text" ? entry.text : entry.type).join(" ");
|
||||
if (historyResult.isError) {
|
||||
const detail = historyResult.content?.map((entry) => entry.type === "text" ? entry.text : entry.type).join(" ");
|
||||
throw new Error(`Remote search_history failed: ${detail || "unknown MCP error"}`);
|
||||
}
|
||||
process.stdout.write(`${JSON.stringify({ ok: true, toolCount: tools.tools.length, result }, null, 2)}\n`);
|
||||
const repositoriesResult = await client.callTool({ name: "list_indexed_repos", arguments: {} });
|
||||
if (repositoriesResult.isError) {
|
||||
throw new Error("Remote list_indexed_repos failed.");
|
||||
}
|
||||
const sourceResult = await client.callTool({
|
||||
name: "search_sources",
|
||||
arguments: { query: "VoidBot indexed source retrieval", limit: 2 },
|
||||
});
|
||||
if (sourceResult.isError) {
|
||||
const detail = sourceResult.content?.map((entry) => entry.type === "text" ? entry.text : entry.type).join(" ");
|
||||
throw new Error(`Remote search_sources failed: ${detail || "unknown MCP error"}`);
|
||||
}
|
||||
process.stdout.write(`${JSON.stringify({
|
||||
ok: true,
|
||||
toolCount: tools.tools.length,
|
||||
historyResult,
|
||||
repositoriesResult,
|
||||
sourceResult,
|
||||
}, null, 2)}\n`);
|
||||
} finally {
|
||||
await client.close();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -456,3 +456,4 @@
|
|||
{"ts":"2026-07-16T05:10:08Z","type":"architecture","status":"ok","note":"Completion audit removed the final scheduler type loophole: system_agent and void_moderation are no longer valid participant/turn kinds, so legacy state cannot deserialize a built-in Void CTB writer. Physiology cadence is now a typed VOID_PHYSIOLOGY_INTERVAL_MINUTES config owned by the resident daemon rather than a raw legacy mood environment name."}
|
||||
{"ts":"2026-07-16T05:14:05Z","type":"verification","status":"ok","note":"Final daemon-organ audit at deployed 9deebb3: Idunn promotion succeeded; Ygg reports one resident scheduler Node process, 22 repo/native Personas only, one repo_face_rumination turn kind, zero pending attention, five-minute typed physiology cadence, disabled moderation/rumination inference, enabled transport-only candidate delivery, paused zero-selection pulses, and accepted RUDP health. Local/upstream HEAD match and all five obsolete Starfire task husks are absent."}
|
||||
{"ts":"2026-07-16T16:01:14Z","type":"repair","status":"ok","note":"Agent Discord archive access failed because Codex launched a Starfire-local MCP process whose QDRANT_URL was loopback after the archive moved to Yggdrasil. Repo and global Codex MCP config now execute the stdio MCP server inside Ygg's worker container over SSH; Qdrant remains private. A real MCP client advertised 20 tools and search_history returned two archive hits."}
|
||||
{"ts":"2026-07-19T14:58:00Z","type":"repair","status":"ok","note":"A raw SSH docker-exec MCP stream made every open agent task inherit the lifetime of Yggdrasil's replaceable worker container; an Idunn promotion therefore left tasks at Transport closed even after the new worker was healthy. Codex now attaches to a stable local stdio relay which opens one fresh remote MCP session per request. A real relay smoke advertised 20 tools, listed 52 indexed repositories, and returned a VoidBot source hit; direct history and source retrieval against Ygg also passed."}
|
||||
|
|
|
|||
|
|
@ -225,7 +225,7 @@ accepted_design:
|
|||
- Eve v2 live state has one owner path. CultLib dev supplies typed live-publication handles, authenticated retained provider sessions, reconnect evidence, and application receipts (`ebd8c78c`, `d4934233`, `8965f3c0`). Eve resolves component `stateBindings` and owns subscription teardown/rebind without receipt DOM writes (`85b0139`, `522df00`). Hermodr ingests provider sessions and emits ordered snapshot/update/stale/reconnected events without polling (`5cb819e`, `561e320`). VoidBot publishes accepted heat as typed control state and binds both visible heat components to it (`060da23`, `3d86b93`). Live-fire proved initial, provider-driven, and browser-command updates converge through that one path.
|
||||
- `voidbot.swarm_operator_view.v1` is the live authority for swarm summary, queue, selected Face detail/channels, operations, pressure, pending mentions, recent events, and route state. Eve binds those values as text/list props; the composition renderer no longer computes those browser-visible facts from its generation snapshot. The static HTML artifact may still lower a snapshot for debugging, but it cannot own live browser state.
|
||||
- The production Eve browser nerve is Starfire Chrome -> `http://10.77.0.1:8798` -> Hermodr on Yggdrasil -> Odin discovery and provider-owned VoidBot RUDP on Yggdrasil. Hermodr's browser listener is WireGuard-only, its authenticated retained-publication ingress is host-loopback `127.0.0.1:17874`, and VoidBot remains the state/command owner at `127.0.0.1:17873`. Starfire owns no Hermodr, Odin, or VoidBot deployment body.
|
||||
- Codex agent access to VoidBot archives executes the stdio MCP body inside Yggdrasil's worker container over SSH. Qdrant remains Yggdrasil-loopback-private. A Starfire-local MCP body pointed at `127.0.0.1:6333` advertises tools but returns `fetch failed` and is forbidden as an archive access path; `scripts/smoke-voidbot-remote-mcp.mjs` proves tool advertisement and a real history query.
|
||||
- Codex agent access to VoidBot archives and indexed sources crosses a stable Starfire-local stdio relay. The relay opens a fresh SSH session to the MCP body inside Yggdrasil's worker container for each tool request, so an Idunn container replacement cannot permanently close the agent-facing transport. Qdrant remains Yggdrasil-loopback-private. A Starfire-local VoidBot MCP body pointed at `127.0.0.1:6333` is forbidden; `scripts/smoke-voidbot-remote-mcp.mjs` proves the direct upstream body and `scripts/smoke-voidbot-remote-mcp-relay.mjs` proves the durable agent path through real history and source retrieval.
|
||||
- Long-running work should expose durable logs or status files instead of relying on one attached session and hope.
|
||||
- Persistent project memory must stay ruthlessly distilled; git history and artifacts carry routine proof.
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue