// Server-side orchestration for model-assisted salience capture (Keiko "learns from experience").
//
// After a chat turn, this module asks the model to extract durable, salient facts the USER stated
// in natural conversation and persists them as `proposed` memory candidates — IN ADDITION to the
// regex intent path in chat-handlers.ts. The capture filter is intentionally LOW; a later
// decay/consolidation pass prunes.
//
// This is the model/IO boundary: the WHOLE body runs inside one try/catch so a model error, a
// vault hiccup, or any other failure can NEVER throw into the chat path — it logs and returns [].
import { randomUUID } from "node:crypto";
import { extractSalientMemories, } from "@oscharko-dev/keiko-memory-capture";
import { conversationMemoryScopes, } from "./memory-conversation-context.js";
import { buildMemoryRecordFromProposal } from "./memory-record-builders.js";
import { embedAndStoreMemory } from "./memory-embedding.js";
// Mirror of chat-handlers' private scopeLabel (decision 3 — mirrored rather than exported to keep
// the modules decoupled). Pure and trivial.
function scopeLabel(scope) {
    switch (scope.kind) {
        case "user":
            return "User memory";
        case "workspace":
            return "Workspace memory";
        case "project":
            return "Project memory";
        case "workflow":
            return "Workflow memory";
        case "global":
            return "Global memory";
    }
}
// Bounds the dedup corpus so the Jaccard loop stays cheap even for a large vault.
const MAX_EXISTING_BODIES = 200;
function gatherExistingBodies(vault, context) {
    const seen = new Set();
    for (const scope of conversationMemoryScopes(context)) {
        for (const record of vault.listMemoriesByScope(scope)) {
            seen.add(record.body);
            if (seen.size >= MAX_EXISTING_BODIES) {
                return [...seen];
            }
        }
    }
    return [...seen];
}
function buildSalienceContext(context) {
    return {
        userId: context.userId,
        nowMs: Date.now(),
        newMemoryId: () => randomUUID(),
        newProposalId: () => randomUUID(),
        workspaceId: context.workspaceId,
        projectId: context.projectId,
        conversationId: context.conversationId,
    };
}
function buildCallModel(deps, modelId) {
    const model = deps.modelPortFactory(modelId);
    if (model === undefined) {
        return null;
    }
    return async (system, user) => {
        const response = await model.call({
            modelId,
            messages: [
                { role: "system", content: system },
                { role: "user", content: user },
            ],
            stream: false,
        }, new AbortController().signal);
        return response.content;
    };
}
// Persists one salience candidate and returns its wire action, or null when the outcome is not a
// candidate or no record could be built. Best-effort embed-on-capture (#204): the inserted memory
// is embedded and the vector stored when an embedding model is configured; failure is swallowed by
// embedAndStoreMemory so capture is never affected.
async function persistCandidate(deps, outcome, vault) {
    if (outcome.kind !== "candidate") {
        return null;
    }
    const proposalId = outcome.proposal.proposalId;
    const record = buildMemoryRecordFromProposal(proposalId, outcome);
    if (record === null) {
        return null;
    }
    const inserted = vault.insertMemory(record);
    await embedAndStoreMemory(deps, vault, inserted.id, inserted.body);
    return {
        kind: "candidate",
        proposalId: String(inserted.id),
        body: inserted.body,
        scopeLabel: scopeLabel(inserted.scope),
        requiresApproval: outcome.requiresApproval,
    };
}
// Captures salient memories from a completed chat turn. Never throws — any failure (model error,
// vault error, malformed output) yields [] so the chat response is unaffected.
export async function captureSalientFromTurn(deps, request, context, modelId, assistantText) {
    const vault = deps.memoryVault;
    if (request.memory === undefined || !request.memory.enabled || vault === undefined) {
        return [];
    }
    try {
        const callModel = buildCallModel(deps, modelId);
        if (callModel === null) {
            return [];
        }
        const outcomes = await extractSalientMemories({
            userText: request.content,
            assistantText,
            existingBodies: gatherExistingBodies(vault, context),
            context: buildSalienceContext(context),
        }, {
            callModel,
            now: () => Date.now(),
            newMemoryId: () => randomUUID(),
            newProposalId: () => randomUUID(),
        });
        const actions = [];
        for (const outcome of outcomes) {
            const action = await persistCandidate(deps, outcome, vault);
            if (action !== null) {
                actions.push(action);
            }
        }
        return actions;
    }
    catch (error) {
        // Boundary: salience must never break the chat path. Log and continue.
        // eslint-disable-next-line no-console
        console.error("salience capture failed", error);
        return [];
    }
}
