"use strict";
/**
 * Built-in agent session storage. Backed by `system.hdb_agent_session` so
 * transcripts and pending approvals survive restarts. The exported helpers
 * are async to keep the surface uniform — once the underlying store gains
 * non-blocking paths the call sites won't have to change.
 *
 * Intentionally separate from #511's `ConversationResource`: that primitive
 * is app-facing (per-tenant, multi-user, user-defined schema) while this
 * table is server-local and operator-owned.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.getAgentSessionTable = getAgentSessionTable;
exports.createSession = createSession;
exports.getSession = getSession;
exports.listSessions = listSessions;
exports.appendMessage = appendMessage;
exports.setStatus = setStatus;
exports.addPendingApproval = addPendingApproval;
exports.markApprovalConsumed = markApprovalConsumed;
exports.resolveApproval = resolveApproval;
exports._resetForTests = _resetForTests;
exports._setTableForTests = _setTableForTests;
const node_crypto_1 = require("node:crypto");
const databases_ts_1 = require("../resources/databases.js");
const hdbTerms_ts_1 = require("../utility/hdbTerms.js");
let cachedTable;
function getAgentSessionTable() {
    if (cachedTable)
        return cachedTable;
    cachedTable = (0, databases_ts_1.table)({
        table: hdbTerms_ts_1.SYSTEM_TABLE_NAMES.AGENT_SESSION_TABLE_NAME,
        database: hdbTerms_ts_1.SYSTEM_SCHEMA_NAME,
        audit: true,
        trackDeletes: false,
        attributes: [
            { name: 'session_id', isPrimaryKey: true },
            { name: 'user', type: 'string', indexed: true },
            { name: 'status', type: 'string', indexed: true },
            { name: 'messages' },
            { name: 'pendingApprovals' },
            { name: 'model', type: 'string' },
            { name: 'provider', type: 'string' },
            { name: 'createdAt', type: 'number', indexed: true },
            { name: 'updatedAt', type: 'number', indexed: true },
            { name: 'lastError', type: 'string' },
        ],
    });
    return cachedTable;
}
async function createSession(opts) {
    const now = Date.now();
    const row = {
        session_id: opts.sessionId ?? (0, node_crypto_1.randomUUID)(),
        user: opts.user,
        status: 'idle',
        messages: opts.initialMessage ? [opts.initialMessage] : [],
        pendingApprovals: [],
        model: opts.model,
        provider: opts.provider,
        createdAt: now,
        updatedAt: now,
    };
    await getAgentSessionTable().primaryStore.put(row.session_id, row);
    return row;
}
async function getSession(sessionId) {
    return getAgentSessionTable().primaryStore.get(sessionId);
}
async function listSessions(opts = {}) {
    const limit = opts.limit ?? 100;
    const out = [];
    for (const entry of getAgentSessionTable().primaryStore.getRange({ reverse: true, limit })) {
        if (entry.value)
            out.push(entry.value);
    }
    return out;
}
/**
 * Per-session mutation lock. Each row mutation is a read-modify-write against the table; without
 * serialization, two concurrent mutations on the same session (e.g. the loop appending an assistant
 * message while the operator resolves an approval) both read the same row and the second `put`
 * clobbers the first — a lost update. This chains all mutations for a given session id so they
 * apply sequentially. Reads (`getSession`/`listSessions`) intentionally don't take the lock.
 */
const sessionLocks = new Map();
function withSessionLock(sessionId, fn) {
    const prev = (sessionLocks.get(sessionId) ?? Promise.resolve()).catch(() => { });
    const result = prev.then(() => fn());
    const tail = result.catch(() => { });
    sessionLocks.set(sessionId, tail);
    void tail.then(() => {
        if (sessionLocks.get(sessionId) === tail)
            sessionLocks.delete(sessionId);
    });
    return result;
}
function appendMessage(sessionId, message) {
    return withSessionLock(sessionId, async () => {
        const session = await requireSession(sessionId);
        session.messages.push(message);
        session.updatedAt = Date.now();
        await getAgentSessionTable().primaryStore.put(sessionId, session);
        return session;
    });
}
function setStatus(sessionId, status, lastError) {
    return withSessionLock(sessionId, async () => {
        const session = await requireSession(sessionId);
        session.status = status;
        session.lastError = lastError;
        session.updatedAt = Date.now();
        await getAgentSessionTable().primaryStore.put(sessionId, session);
        return session;
    });
}
function addPendingApproval(sessionId, approval) {
    return withSessionLock(sessionId, async () => {
        const session = await requireSession(sessionId);
        const entry = { ...approval, id: (0, node_crypto_1.randomUUID)(), createdAt: Date.now() };
        session.pendingApprovals.push(entry);
        session.status = 'awaiting_approval';
        session.updatedAt = Date.now();
        await getAgentSessionTable().primaryStore.put(sessionId, session);
        return entry;
    });
}
function markApprovalConsumed(sessionId, approvalId) {
    return withSessionLock(sessionId, async () => {
        const session = await requireSession(sessionId);
        const entry = session.pendingApprovals.find((a) => a.id === approvalId);
        if (!entry)
            throw new Error(`No pending approval ${approvalId} on session ${sessionId}`);
        if (!entry.resolved)
            throw new Error(`Approval ${approvalId} not yet resolved`);
        if (entry.consumed)
            return;
        entry.consumed = true;
        session.updatedAt = Date.now();
        await getAgentSessionTable().primaryStore.put(sessionId, session);
    });
}
function resolveApproval(sessionId, approvalId, approved) {
    return withSessionLock(sessionId, async () => {
        const session = await requireSession(sessionId);
        const entry = session.pendingApprovals.find((a) => a.id === approvalId);
        if (!entry)
            throw new Error(`No pending approval ${approvalId} on session ${sessionId}`);
        if (entry.resolved)
            throw new Error(`Approval ${approvalId} already resolved`);
        entry.resolved = true;
        entry.approved = approved;
        entry.resolvedAt = Date.now();
        // Either decision (approve or deny) returns the session to a resumable `idle` state so the
        // loop can run again and deliver the resulting observation to the model. Operators who want
        // to terminate the whole run should use `cancel_agent_run` instead of denying.
        if (!session.pendingApprovals.some((a) => !a.resolved)) {
            session.status = 'idle';
        }
        session.updatedAt = Date.now();
        await getAgentSessionTable().primaryStore.put(sessionId, session);
        return entry;
    });
}
async function requireSession(sessionId) {
    const session = await getSession(sessionId);
    if (!session)
        throw new Error(`No agent session ${sessionId}`);
    return session;
}
/**
 * Reset the cached table reference. Test-only seam; production code lets the
 * lazy-getter initialize once per process.
 */
function _resetForTests() {
    cachedTable = undefined;
}
/** Inject a mock table accessor for unit tests. Pass `undefined` to restore the lazy default. */
function _setTableForTests(mock) {
    cachedTable = mock;
}
//# sourceMappingURL=session.js.map