"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
/**
 * Bridge entry point (CommonJS).
 *
 * Started by non-TypeScript hosts (e.g. the Hermes Python client) via:
 *
 *   node_modules/.bin/tsx bridge.cts --agent=hermes --no-viewer
 *
 * The `.cts` extension is intentional: it lets the file be required
 * from CommonJS environments that spawn Node with `require("...")`
 * semantics. Internally we re-export the ESM implementation via
 * `import()`.
 *
 * Viewer lifecycle
 * ================
 * Each agent owns its own HTTP port:
 *
 *   - openclaw → :18799
 *   - hermes   → :18800
 *
 * The viewer port is read from the agent's `~/.<agent>/memos-plugin/
 * config.yaml::viewer.port`. We just call `startHttpServer` once;
 * if the port is already in use we surface the EADDRINUSE error to
 * stderr and keep running stdio-RPC headless (capture / retrieval
 * still work). There's no port-sharing or auto-promotion logic —
 * each agent has its own bookmarkable URL.
 */
// eslint-disable-next-line @typescript-eslint/no-require-imports
const path = require("node:path");
// eslint-disable-next-line @typescript-eslint/no-require-imports
const fs = require("node:fs");
// eslint-disable-next-line @typescript-eslint/no-require-imports
const childProcess = require("node:child_process");
// eslint-disable-next-line @typescript-eslint/no-require-imports
const url = require("node:url");
const BRIDGE_STATUS_HEARTBEAT_MS = 5_000;
const BRIDGE_STATUS_STALE_MS = 20_000;
const BRIDGE_STATUS_FILE = "bridge-status.json";
function parseArgs(argv) {
    const args = { daemon: false, noViewer: false, agent: "openclaw" };
    for (const raw of argv) {
        if (raw === "--daemon")
            args.daemon = true;
        else if (raw === "--no-viewer")
            args.noViewer = true;
        else if (raw.startsWith("--tcp="))
            args.tcpPort = Number(raw.slice(6));
        else if (raw === "--agent=hermes")
            args.agent = "hermes";
        else if (raw === "--agent=openclaw")
            args.agent = "openclaw";
    }
    return args;
}
// ─── PID file singleton guard ───────────────────────────────────────────
// Prevents bridge process accumulation: each new bridge that wants to
// own the viewer port kills the previous holder via its PID file.
// `--no-viewer` (headless) bridges skip the kill — they don't need the
// port and should coexist with the daemon that owns it.
const PID_FILENAME = "bridge.pid";
function pidFilePath(agent) {
    const agentHome = agent === "hermes" ? ".hermes" : ".openclaw";
    return path.join(process.env.HOME ?? "/tmp", agentHome, "memos-plugin", "daemon", PID_FILENAME);
}
function readPidFile(pidPath) {
    try {
        const raw = fs.readFileSync(pidPath, "utf8").trim();
        const pid = parseInt(raw, 10);
        if (isNaN(pid) || pid <= 0)
            return null;
        process.kill(pid, 0); // throws if not alive
        return pid;
    }
    catch {
        return null;
    }
}
function writePidFile(pidPath) {
    fs.mkdirSync(path.dirname(pidPath), { recursive: true });
    fs.writeFileSync(pidPath, String(process.pid), "utf8");
}
function removePidFile(pidPath) {
    try {
        const content = fs.readFileSync(pidPath, "utf8").trim();
        if (content === String(process.pid))
            fs.unlinkSync(pidPath);
    }
    catch {
        /* best-effort; another bridge may have overwritten */
    }
}
function killExistingBridge(pidPath, timeoutMs = 5000) {
    const existingPid = readPidFile(pidPath);
    if (existingPid === null || existingPid === process.pid)
        return;
    process.stderr.write(`bridge: killing stale bridge pid=${existingPid} before startup\n`);
    try {
        process.kill(existingPid, "SIGTERM");
    }
    catch {
        return; // already dead
    }
    const deadline = Date.now() + timeoutMs;
    while (Date.now() < deadline) {
        try {
            process.kill(existingPid, 0);
        }
        catch {
            return; // gone
        }
        childProcess.spawnSync("sleep", ["0.5"]);
    }
    try {
        process.kill(existingPid, "SIGKILL");
    }
    catch {
        /* already dead */
    }
}
async function main() {
    const args = parseArgs(process.argv.slice(2));
    // ─── Singleton: kill previous bridge that owns the viewer port ───
    const pidPath = pidFilePath(args.agent);
    if (!args.noViewer) {
        killExistingBridge(pidPath);
    }
    writePidFile(pidPath);
    // Lazy-import ESM core. Using dynamic import so this file remains
    // CommonJS and stays `require`-able.
    const { bootstrapMemoryCoreFull } = (await importEsm(runtimeModule("core/pipeline/index.ts", "dist/core/pipeline/index.js")));
    const { startStdioServer, waitForShutdown } = (await importEsm(runtimeModule("bridge/stdio.ts", "dist/bridge/stdio.js")));
    const { memoryBuffer, rootLogger } = (await importEsm(runtimeModule("core/logger/index.ts", "dist/core/logger/index.js")));
    const { startHttpServer } = (await importEsm(runtimeModule("server/http.ts", "dist/server/http.js")));
    const rootDir = pluginRoot();
    const pkgVersion = require(path.join(rootDir, "package.json")).version;
    // ─── Host LLM bridge (reverse RPC, lazy-bound to stdio) ────────
    // We need to register the bridge BEFORE bootstrap creates the
    // LlmClients (so the very first `shouldFallback()` check sees a
    // non-null bridge), but `stdio` itself doesn't exist until later
    // in this function. The trick: hand a placeholder closure to
    // bootstrap that defers actual stdio access to the time of the
    // first fallback call. In stdio mode we start the server before
    // `core.init()` so startup recovery can also use host fallback.
    //
    // Routing through `bootstrapMemoryCoreFull({ hostLlmBridge })`
    // (instead of having `bridge.cts` call `registerHostLlmBridge`
    // directly) avoids a subtle ESM module-identity issue: the static
    // `import` chain inside `core/llm/client.ts` and the dynamic
    // `await import(...)` here resolve to the same file URL but Node
    // can occasionally treat them as different module instances with
    // independent `currentBridge` slots. Registering inside bootstrap
    // forces both ends to share the same module instance.
    let stdio = null;
    const lazyHostLlmBridge = {
        id: `stdio.host.${args.agent}.v1`,
        async complete(input) {
            if (!stdio) {
                throw new Error("host LLM bridge invoked before stdio server was ready");
            }
            const result = (await stdio.serverRequest("host.llm.complete", {
                messages: input.messages,
                model: input.model,
                temperature: input.temperature,
                maxTokens: input.maxTokens,
                timeoutMs: input.timeoutMs,
            }, { timeoutMs: (input.timeoutMs ?? 60_000) + 5_000 }));
            return {
                text: typeof result?.text === "string" ? result.text : "",
                model: typeof result?.model === "string"
                    ? result.model
                    : input.model ?? "",
                usage: result?.usage,
                durationMs: typeof result?.durationMs === "number" ? result.durationMs : 0,
            };
        },
    };
    const { Telemetry } = (await importEsm(runtimeModule("core/telemetry/index.ts", "dist/core/telemetry/index.js")));
    const { core, config, home } = await bootstrapMemoryCoreFull({
        agent: args.agent,
        namespace: { agentKind: args.agent, profileId: "default" },
        pkgVersion,
        hostLlmBridge: args.daemon ? null : lazyHostLlmBridge,
    });
    const telemetry = new Telemetry(config.telemetry ?? {}, home.root, pkgVersion, rootLogger.child({ channel: "core.telemetry" }), rootDir);
    core.bindTelemetry?.(telemetry);
    telemetry.trackPluginStarted(args.agent);
    const bridgeStatus = args.agent === "hermes"
        ? createBridgeStatusTracker(path.join(home.root, BRIDGE_STATUS_FILE), args.daemon)
        : null;
    // Process-level error reporting. Without these handlers a crash in
    // a background task (capture / reward / L2 inducer) silently kills
    // the bridge process and never surfaces in ARMS — making "0
    // plugin_error events" actively misleading. Both handlers are
    // best-effort and re-emit (or `process.exit(1)`) so we don't
    // alter the existing crash semantics, only add observability.
    // Only registered for `bridge.cts` (the dedicated process); the
    // OpenClaw adapter runs inside the host process and must not steal
    // its global error hooks.
    process.on("uncaughtException", (err) => {
        try {
            telemetry.trackError("uncaught_exception", classifyErrorCode(err));
        }
        catch {
            /* swallow — telemetry must never widen the crash */
        }
        process.stderr.write(`bridge: uncaughtException: ${err instanceof Error ? err.stack ?? err.message : String(err)}\n`);
        // Mirror Node's default behaviour so existing supervisors that
        // expect non-zero exit on crash keep working.
        process.exit(1);
    });
    process.on("unhandledRejection", (reason) => {
        try {
            telemetry.trackError("unhandled_rejection", classifyErrorCode(reason));
        }
        catch {
            /* swallow — telemetry must never widen the crash */
        }
        process.stderr.write(`bridge: unhandledRejection: ${reason instanceof Error ? reason.stack ?? reason.message : String(reason)}\n`);
        // Don't exit: per-promise rejections are usually recoverable
        // (failed flush, dropped SSE client). The default Node 20+
        // behaviour is to exit, but for a long-running bridge that
        // would be too aggressive — surface to telemetry + stderr and
        // continue.
    });
    // Per-agent fixed viewer port.
    const AGENT_DEFAULT_PORTS = { openclaw: 18799, hermes: 18800 };
    const viewerPort = AGENT_DEFAULT_PORTS[args.agent];
    let bridgeHeartbeat;
    // In stdio mode the host fallback path is a reverse JSON-RPC request
    // over the same pipe as normal bridge traffic. `core.init()` may
    // recover dirty episodes and run reflection/reward/L2/skill work; if
    // that work hits a broken primary skill-evolver model, the LLM facade
    // can fall back to host before init returns. Start stdio first so that
    // fallback has a transport instead of tripping the lazy bridge guard.
    if (!args.daemon) {
        stdio = startStdioServer({ core });
        bridgeStatus?.markConnected();
        bridgeHeartbeat = bridgeStatus?.startHeartbeat();
        void stdio.done.then(() => {
            bridgeHeartbeat?.stop();
            bridgeStatus?.markDisconnected("Hermes chat disconnected");
        });
    }
    try {
        await core.init();
    }
    catch (err) {
        bridgeHeartbeat?.stop();
        if (stdio) {
            try {
                await stdio.close();
            }
            catch {
                /* best-effort */
            }
        }
        throw err;
    }
    // ─── Daemon mode ──────────────────────────────────────────────
    // When started with `--daemon`, skip stdio and run as a pure HTTP
    // viewer daemon. Used by install.sh (post-install) and admin/restart
    // (self-restart) to keep the Memory Viewer always available.
    if (args.daemon) {
        // Daemon mode is the target of `POST /api/v1/admin/restart`,
        // which re-spawns the bridge after a short sleep. On busy
        // machines the previous bridge's listening socket can take a
        // moment longer than expected to release, so we retry the bind
        // a few times before giving up. Without this the user sees
        // "重启超时" in the viewer because the new daemon raced its
        // predecessor and lost.
        let viewer = null;
        const maxBindAttempts = 10;
        for (let attempt = 1; attempt <= maxBindAttempts; attempt++) {
            try {
                viewer = await startHttpServer({
                    core,
                    home,
                    logTail: () => memoryBuffer().tail({ limit: 200 }),
                    bridgeStatus: bridgeStatus ? () => bridgeStatus.snapshot() : undefined,
                    telemetry,
                }, {
                    port: viewerPort,
                    host: config.viewer.bindHost,
                    staticRoot: path.resolve(rootDir, "viewer/dist"),
                    agent: args.agent,
                });
                process.stderr.write(`bridge: daemon viewer live at ${viewer.url} (agent=${args.agent})\n`);
                break;
            }
            catch (err) {
                const e = err;
                if (e?.code === "EADDRINUSE" && attempt < maxBindAttempts) {
                    process.stderr.write(`bridge: daemon port :${viewerPort} busy (attempt ${attempt}/${maxBindAttempts}), retrying in 1s...\n`);
                    await new Promise((r) => setTimeout(r, 1000));
                    continue;
                }
                if (e?.code === "EADDRINUSE") {
                    process.stderr.write(`bridge: daemon port :${viewerPort} still in use after ${maxBindAttempts}s — exiting.\n`);
                    await core.shutdown();
                    process.exit(1);
                }
                process.stderr.write(`bridge: daemon viewer failed: ${err?.message ?? String(err)}\n`);
                await core.shutdown();
                process.exit(1);
            }
        }
        const shutdownDaemon = async (sig) => {
            process.stderr.write(`bridge: daemon received ${sig}, shutting down\n`);
            removePidFile(pidPath);
            try {
                await viewer.close();
            }
            catch { /* best-effort */ }
            await core.shutdown();
            process.exit(0);
        };
        process.on("SIGINT", () => void shutdownDaemon("SIGINT"));
        process.on("SIGTERM", () => void shutdownDaemon("SIGTERM"));
        // Process stays alive via the HTTP server's ref'd socket.
        return;
    }
    // ─── Normal (stdio) mode ──────────────────────────────────────
    // The stdio handle was started before `core.init()` above so host
    // fallback is available during startup recovery.
    const activeStdio = stdio;
    if (!activeStdio) {
        throw new Error("internal bridge error: stdio server was not started");
    }
    // Try to bind the viewer port unless the caller requested a pure stdio
    // bridge. Hermes chat uses --no-viewer; the standalone --daemon process is
    // the single owner of :18800.
    let viewer = null;
    if (args.noViewer) {
        process.stderr.write(`bridge: stdio mode running without viewer (agent=${args.agent})\n`);
    }
    else {
        try {
            viewer = await startHttpServer({
                core,
                home,
                logTail: () => memoryBuffer().tail({ limit: 200 }),
                bridgeStatus: bridgeStatus ? () => bridgeStatus.snapshot() : undefined,
                telemetry,
            }, {
                port: viewerPort,
                host: config.viewer.bindHost,
                staticRoot: path.resolve(rootDir, "viewer/dist"),
                agent: args.agent,
            });
            process.stderr.write(`bridge: viewer live at ${viewer.url} (agent=${args.agent})\n`);
        }
        catch (err) {
            const e = err;
            if (e?.code === "EADDRINUSE") {
                process.stderr.write(`bridge: viewer port :${viewerPort} is already in use — ` +
                    `${args.agent} will run headless (stdio only). ` +
                    `Free the port to expose the viewer.\n`);
            }
            else {
                process.stderr.write(`bridge: viewer failed to start: ${e?.message ?? String(err)}\n`);
            }
        }
    }
    const shutdown = async (sig) => {
        process.stderr.write(`bridge: received ${sig}, shutting down\n`);
        removePidFile(pidPath);
        if (viewer) {
            try {
                await viewer.close();
            }
            catch {
                /* best-effort */
            }
        }
        await waitForShutdown(core, activeStdio);
        process.exit(0);
    };
    process.on("SIGINT", () => void shutdown("SIGINT"));
    process.on("SIGTERM", () => void shutdown("SIGTERM"));
    // Keep the process alive until stdin ends (client disconnects).
    await activeStdio.done;
    // If a viewer is running, keep the process alive as a daemon so the
    // memory panel stays accessible between `hermes chat` sessions.
    if (viewer && !viewer.closed) {
        process.stderr.write(`bridge: stdin closed but viewer is still serving at ${viewer.url} — ` +
            `staying alive as daemon. Send SIGTERM to stop.\n`);
        const keepalive = setInterval(() => {
            if (viewer.closed) {
                clearInterval(keepalive);
                removePidFile(pidPath);
                void core.shutdown().then(() => process.exit(0));
            }
        }, 5_000);
        keepalive.unref?.();
        return;
    }
    // No viewer (headless bridge) — clean exit.
    removePidFile(pidPath);
    await core.shutdown();
    process.exit(0);
}
function pluginRoot() {
    // Source entry: <root>/bridge.cts. Built entry: <root>/dist/bridge.cjs.
    if (fs.existsSync(path.join(__dirname, "package.json")))
        return __dirname;
    const parent = path.resolve(__dirname, "..");
    if (fs.existsSync(path.join(parent, "package.json")))
        return parent;
    return __dirname;
}
function runtimeModule(sourceRel, distRel) {
    const root = pluginRoot();
    const distAbs = path.resolve(root, distRel);
    const sourceAbs = path.resolve(root, sourceRel);
    return pathToEsmUrl(fs.existsSync(distAbs) ? distAbs : sourceAbs);
}
function pathToEsmUrl(abs) {
    return url.pathToFileURL(abs).href;
}
const importEsm = new Function("specifier", "return import(specifier)");
/**
 * Best-effort error classification for ARMS `plugin_error.error_type`.
 *
 * Priority order:
 *   1. `MemosError.code` and Node `errno` (`ENOENT`, `EADDRINUSE`, …)
 *      — both surface as a `code` string property.
 *   2. The constructor name when it's something more specific than
 *      the generic `Error` (e.g. `TypeError`, `SyntaxError`).
 *   3. `unknown` as a sentinel.
 *
 * Never returns the message — those can carry user paths or query
 * fragments and would defeat the redaction the rest of the telemetry
 * pipeline guarantees.
 */
function classifyErrorCode(err) {
    if (err && typeof err === "object" && "code" in err) {
        const code = err.code;
        if (typeof code === "string" && code.length > 0)
            return code;
    }
    if (err instanceof Error && err.name && err.name !== "Error") {
        return err.name;
    }
    return "unknown";
}
function createBridgeStatusTracker(statusFile, daemon) {
    let snapshot = daemon
        ? {
            status: "disconnected",
            lastOkAt: null,
            lastErrorAt: Date.now(),
            lastError: "Hermes chat is not connected",
        }
        : {
            status: "unknown",
            lastOkAt: null,
            lastErrorAt: null,
            lastError: null,
        };
    function writeStatus(next) {
        snapshot = next;
        try {
            fs.mkdirSync(path.dirname(statusFile), { recursive: true });
            fs.writeFileSync(statusFile, JSON.stringify(next), "utf8");
        }
        catch {
            // Status display must never affect chat capture.
        }
    }
    function readStatus() {
        try {
            const parsed = JSON.parse(fs.readFileSync(statusFile, "utf8"));
            if (parsed.status === "connected" ||
                parsed.status === "reconnecting" ||
                parsed.status === "disconnected" ||
                parsed.status === "unknown") {
                return {
                    status: parsed.status,
                    lastOkAt: typeof parsed.lastOkAt === "number" ? parsed.lastOkAt : null,
                    lastErrorAt: typeof parsed.lastErrorAt === "number" ? parsed.lastErrorAt : null,
                    lastError: typeof parsed.lastError === "string" ? parsed.lastError : null,
                };
            }
        }
        catch {
            // Missing or corrupt status files are treated as disconnected.
        }
        return null;
    }
    function applyStaleRule(raw) {
        if (raw.status === "disconnected" && daemon && isHermesChatRunning()) {
            return {
                status: "reconnecting",
                lastOkAt: raw.lastOkAt,
                lastErrorAt: raw.lastErrorAt,
                lastError: "Hermes chat is running; waiting for memory bridge",
            };
        }
        if (raw.status === "connected" &&
            raw.lastOkAt != null &&
            Date.now() - raw.lastOkAt > BRIDGE_STATUS_STALE_MS) {
            return {
                status: "disconnected",
                lastOkAt: raw.lastOkAt,
                lastErrorAt: Date.now(),
                lastError: "Hermes bridge heartbeat is stale",
            };
        }
        return raw;
    }
    function markConnected() {
        writeStatus({
            status: "connected",
            lastOkAt: Date.now(),
            lastErrorAt: snapshot.lastErrorAt,
            lastError: snapshot.lastError,
        });
    }
    function markDisconnected(message) {
        writeStatus({
            status: "disconnected",
            lastOkAt: snapshot.lastOkAt,
            lastErrorAt: Date.now(),
            lastError: message,
        });
    }
    return {
        snapshot() {
            return { ...applyStaleRule(readStatus() ?? snapshot) };
        },
        markConnected,
        markDisconnected,
        startHeartbeat() {
            const timer = setInterval(() => {
                markConnected();
            }, BRIDGE_STATUS_HEARTBEAT_MS);
            timer.unref?.();
            return {
                stop() {
                    clearInterval(timer);
                },
            };
        },
    };
}
function isHermesChatRunning() {
    try {
        const out = childProcess.execFileSync("pgrep", ["-f", "hermes chat"], {
            encoding: "utf8",
            timeout: 1000,
        });
        return out.trim().length > 0;
    }
    catch {
        return false;
    }
}
void main().catch((err) => {
    const detail = err instanceof Error ? err.stack ?? err.message : String(err);
    process.stderr.write(`bridge: fatal: ${detail}\n`);
    process.exit(1);
});
//# sourceMappingURL=bridge.cjs.map