/**
 * MemoryServiceContainerController — lifecycle for olam-memory-service.
 *
 * Mirrors KgServiceContainerController (./kg-service-container.ts). memory-service,
 * like kg-service, runs as a host-side Docker peripheral managed by `olam services up`.
 * The Phase A predecessor (host-process model — `olam memory start` spawning the
 * npm-installed `agentmemory` bin) is retired in Phase B; both substrates coexist
 * during Phase A so operators can roll back without losing data
 * (`~/.olam/memory-data/` is bind-mounted and survives substrate swap).
 *
 * Divergence from kg-service:
 *   - Bearer-authenticated livez probe (kg-service `/health` is unauthenticated).
 *     `waitForReady` reads `~/.olam/memory-secret` and adds `Authorization: Bearer <secret>`.
 *   - No /host-home read-only mount (kg-service mounts $HOME for build ingestion;
 *     memory-service writes only to its own /data volume).
 *   - No prewarm probe (kg-service pre-warms bge-small inference; memory-service has
 *     no equivalent hot path — iii engine init happens at process start, not first request).
 *
 * Plan reference: docs/plans/memory-service-as-docker-peripheral/phase-a-tasks.md A1
 */
import { execFileSync, spawnSync } from 'node:child_process';
import * as fs from 'node:fs';
import { homedir } from 'node:os';
import { join } from 'node:path';
import { ensureMemorySecret } from '../lib/memory-secret.js';
import { printWarning } from '../output.js';
export const MEMORY_SERVICE_PORT = 3111;
export const MEMORY_SERVICE_CONTAINER = 'olam-memory-service';
export const MEMORY_SERVICE_LOCAL_TAG = 'olam-memory-service:local';
export const MEMORY_SERVICE_PUBLISHED_TAG = 'ghcr.io/pleri/olam-memory-service:latest';
export const MEMORY_SERVICE_DEV_TAG = 'olam-memory-service:dev';
export const MEMORY_SERVICE_HEALTH_URL = `http://127.0.0.1:${MEMORY_SERVICE_PORT}/agentmemory/livez`;
// Mirrors KgServiceContainerController: 60s timeout absorbs cold-start on flaky
// CI runners (image pull + iii-engine boot can exceed the operator-visible
// "took a moment" budget). Warm machines see ~3-5s.
export const MEMORY_SERVICE_HEALTH_TIMEOUT_MS = 60_000;
// memory-data is the host-mounted `~/.olam/memory-data/` so the container's
// agentmemory engine can persist iii-engine state to a path the operator can
// inspect + that survives `docker rm`.
const MEMORY_SERVICE_VOLUME_TARGET = '/data';
/**
 * Resolves the operator's memory-data root for the host bind-mount.
 *
 * Mirrors kg-service's `kgDataHostPath()`: respects `$HOME` for testability,
 * defaults to `~/.olam/memory-data` for the real operator path. Caller is
 * responsible for ensuring the directory exists pre-bind-mount (Linux dockerd
 * auto-creates as root:root if absent, which breaks subsequent host-side
 * reads/writes from the operator's UID).
 */
function memoryDataHostPath() {
    return join(homedir(), '.olam', 'memory-data');
}
export class MemoryServiceContainerController {
    imageTag = MEMORY_SERVICE_LOCAL_TAG;
    status() {
        const r = spawnSync('docker', ['inspect', '--format', '{{.State.Status}}|{{.Id}}', MEMORY_SERVICE_CONTAINER], { encoding: 'utf-8' });
        if (r.status === 0) {
            const [stateRaw, id] = r.stdout.trim().split('|');
            return {
                state: stateRaw === 'running' ? 'running' : 'stopped',
                port: MEMORY_SERVICE_PORT,
                containerId: id,
            };
        }
        return { state: 'missing', port: MEMORY_SERVICE_PORT };
    }
    imageExists(tag = this.imageTag) {
        return spawnSync('docker', ['image', 'inspect', tag], { encoding: 'utf-8' }).status === 0;
    }
    /**
     * Resolve the first available image tag: local → dev → published.
     *
     * The preferred path is :local (populated by `olam bootstrap` pulling the
     * digest-pinned image and tagging it). The :published fallback is `:latest`
     * which is NOT digest-pinned — Docker will pull whatever tag-head is on
     * the registry at pull time. This is the degraded-mode path for operators
     * whose image-digests.json predates the G1 memory-service digest fix; it
     * is intentionally loud so they know they are not on the pinned path.
     *
     * Throws if none exist (operator should run `olam bootstrap` to pull, or
     * `node packages/cli/scripts/build-memory-service-image.mjs` to build locally).
     */
    resolveImage() {
        for (const tag of [MEMORY_SERVICE_LOCAL_TAG, MEMORY_SERVICE_DEV_TAG]) {
            if (this.imageExists(tag))
                return tag;
        }
        if (this.imageExists(MEMORY_SERVICE_PUBLISHED_TAG)) {
            printWarning(`memory-service: using tag-pulled fallback image (${MEMORY_SERVICE_PUBLISHED_TAG}). ` +
                'This image was NOT pulled by digest. Run `olam bootstrap` to pull the ' +
                'digest-pinned image and eliminate this warning.');
            return MEMORY_SERVICE_PUBLISHED_TAG;
        }
        throw new Error(`memory-service image not found. Tried: ${MEMORY_SERVICE_LOCAL_TAG}, ${MEMORY_SERVICE_DEV_TAG}, ${MEMORY_SERVICE_PUBLISHED_TAG}. ` +
            'Run `olam bootstrap` to pull the published image, or `node packages/cli/scripts/build-memory-service-image.mjs` to build locally.');
    }
    /**
     * Idempotent start. If already running → return. If stopped → docker start.
     * If missing → ensure secret + bind-mount dir, then docker run with port-forward
     * + memory-data volume mount + restart policy + bearer-secret env.
     *
     * The bearer secret is read from `~/.olam/memory-secret` (generated on first
     * call via `ensureMemorySecret()`) and passed as `AGENTMEMORY_SECRET`. The
     * container's agentmemory CLI requires this for the livez probe + all API
     * calls; without it the container will start but reject every request as 401.
     */
    start() {
        const current = this.status();
        if (current.state === 'running')
            return;
        if (current.state === 'stopped') {
            execFileSync('docker', ['start', MEMORY_SERVICE_CONTAINER], { stdio: 'pipe' });
            return;
        }
        this.imageTag = this.resolveImage();
        const memoryData = memoryDataHostPath();
        const secret = ensureMemorySecret();
        // Pre-create the host path with the operator's UID. Without this,
        // dockerd on Linux auto-creates the bind-mount target as root:root,
        // which breaks subsequent host-side reads from the operator's UID
        // (EACCES). macOS Docker Desktop's user-mapped binds don't bite
        // here but the explicit mkdir is portable + cheap.
        fs.mkdirSync(memoryData, { recursive: true });
        execFileSync('docker', [
            'run',
            '--detach',
            '--name', MEMORY_SERVICE_CONTAINER,
            '--restart', 'unless-stopped',
            // Bind to 127.0.0.1 ONLY at the host port level — local-only by design.
            '--publish', `127.0.0.1:${MEMORY_SERVICE_PORT}:${MEMORY_SERVICE_PORT}`,
            // host-gateway lets this container reach the host. Worlds reach
            // memory-service via the published 127.0.0.1:3111 PLUS their OWN
            // --add-host flag configured in the world-create code path.
            '--add-host', 'host.docker.internal:host-gateway',
            // Bearer secret for all /agentmemory/* requests.
            '--env', `AGENTMEMORY_SECRET=${secret}`,
            // Bind to all interfaces inside the container; the host-port publish
            // restricts access to 127.0.0.1 externally.
            '--env', 'AGENTMEMORY_HOST=0.0.0.0',
            '--env', `AGENTMEMORY_PORT=${MEMORY_SERVICE_PORT}`,
            // Mount the operator's memory-data root so iii-engine state persists.
            '--volume', `${memoryData}:${MEMORY_SERVICE_VOLUME_TARGET}`,
            this.imageTag,
        ], { stdio: 'pipe' });
    }
    /**
     * Returns true when no memory-service image exists locally — useful for
     * callers (e.g. servicesUp) to distinguish "operator opted out of
     * memory-service" from "memory-service was supposed to work but is broken".
     * The opted-out case should be silent; the broken case should warn.
     */
    imageMissingEverywhere() {
        return (!this.imageExists(MEMORY_SERVICE_LOCAL_TAG) &&
            !this.imageExists(MEMORY_SERVICE_DEV_TAG) &&
            !this.imageExists(MEMORY_SERVICE_PUBLISHED_TAG));
    }
    stop() {
        const current = this.status();
        if (current.state !== 'running')
            return;
        execFileSync('docker', ['stop', MEMORY_SERVICE_CONTAINER], { stdio: 'pipe' });
    }
    remove() {
        spawnSync('docker', ['rm', '-f', MEMORY_SERVICE_CONTAINER], { stdio: 'pipe' });
    }
    /**
     * Poll /agentmemory/livez until 200 OK with `{status: "ok"}` body OR
     * timeoutMs elapses. The probe is bearer-authenticated; without the secret
     * header agentmemory returns 401 regardless of container state.
     *
     * Divergence from kg-service: kg-service's `/health` is unauthenticated and
     * returns `{ready: true}` once bge-small is loaded. memory-service's
     * `/agentmemory/livez` returns `{status: "ok"}` once iii-engine accepts
     * connections — there's no equivalent "model warmed" state to wait for.
     */
    async waitForReady(timeoutMs = MEMORY_SERVICE_HEALTH_TIMEOUT_MS) {
        const secret = ensureMemorySecret();
        const deadline = Date.now() + timeoutMs;
        while (Date.now() < deadline) {
            try {
                const res = await fetch(MEMORY_SERVICE_HEALTH_URL, {
                    headers: { authorization: `Bearer ${secret}` },
                    signal: AbortSignal.timeout(1_500),
                });
                if (res.ok) {
                    const body = (await res.json());
                    if (body.status === 'ok')
                        return true;
                }
            }
            catch {
                // not ready yet (connection refused, container booting, network race)
            }
            await sleep(500);
        }
        return false;
    }
}
function sleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
}
//# sourceMappingURL=memory-service-container.js.map