// Sidecar boot: one process hosts the instance's default agents, each a normal // E2EE member with its own identity on the /data disk (see runtime.mjs for the // per-bot chat loop): // // Claude the plain assistant — replies in DMs, on @mention in groups // (LCHAT_SESSION / LCHAT_INVITE; name via LCHAT_AGENT_NAME) // Eleven the app guide — same chat loop with a shipped-in persona, plus a // welcome DM to each new member (LCHAT_ELEVEN_SESSION / _INVITE) // // Each identity is optional: it runs if its session exists or its invite is set, // so an instance can host any subset. (A third identity, the write-only // Announcer, is retired: the server announces its own changelog now — a push // to admins + the Admin app's update stream.) import fs from "node:fs"; import http from "node:http"; import { BASE, AGENT_NAME, requireBase, loadSession } from "./config.mjs"; import { provision } from "./provision.mjs"; import { runBot } from "./runtime.mjs"; import { CHIP_TOOLS } from "./actions.mjs"; import { ELEVEN_NAME, ELEVEN_PERSONA, ELEVEN_DOCS, loadElevenSession } from "./eleven.mjs"; // Synchronous logging: async stdout to a pipe is buffered and lost if the // container is killed (e.g. a failed health check), so write key lines with // fs.writeSync so they always reach the logs. const log = (m) => { try { fs.writeSync(1, `${m}\n`); } catch {} }; process.on("uncaughtException", (e) => { log(`uncaughtException: ${e?.stack || e}`); process.exit(1); }); process.on("unhandledRejection", (e) => { log(`unhandledRejection: ${e?.stack || e}`); process.exit(1); }); log(`boot: sidecar starting, base=${BASE}`); requireBase(); // Optional health endpoint for container platforms that health-check over HTTP // (e.g. Miren). Only bind it when PORT is explicitly set: the sidecar is a pure // outbound WebSocket client, so under systemd nothing health-checks or routes to // it — it needs no inbound listener, and binding a fixed port would collide when // several sidecars share a host. if (process.env.PORT) { const PORT = Number(process.env.PORT); http .createServer((_req, res) => { res.writeHead(200, { "content-type": "text/plain" }); res.end("ok\n"); }) .listen(PORT, "0.0.0.0", () => log(`health listening on :${PORT}`)); } // --- Claude: the plain assistant (the original sidecar bot) ---------------- let claudeSession = loadSession(); if (!claudeSession && process.env.LCHAT_INVITE) { log("no session; provisioning from LCHAT_INVITE…"); try { claudeSession = await provision(process.env.LCHAT_INVITE); log(`provisioned "${claudeSession.name}"`); } catch (e) { log(`provision failed: ${e?.message || e}`); process.exit(1); } } if (claudeSession) runBot({ name: AGENT_NAME, session: claudeSession }); // --- Eleven: the app guide + welcomer --------------------------------------- let elevenSession = null; try { elevenSession = await loadElevenSession(); } catch (e) { log(`eleven provision failed: ${e?.message || e}`); process.exit(1); } if (elevenSession) { // Eleven runs a deterministic command floor (welcome + slash commands) that // works with no API key, and "upgrades" to conversational AI when one is set — // its badge (BOT vs AI) follows, self-declared on connect. See runtime.mjs. runBot({ name: ELEVEN_NAME, session: elevenSession, builtinPersona: ELEVEN_PERSONA, builtinDocs: ELEVEN_DOCS, tools: CHIP_TOOLS, commands: true, }); // The welcomer is STOOD DOWN: the client-side welcome conversation // (web/static/apps/welcome.js) greets every joiner now — one voice, not two // Elevens saying hello. startWelcomer() in eleven.mjs stays for standalone // runs; re-wire it here only if the client welcome ever retires. } if (!claudeSession && !elevenSession) { log("no chat identities — set LCHAT_INVITE and/or LCHAT_ELEVEN_INVITE (or provide sessions)"); process.exit(1); }