/**
 * SessionMemoryStore -- Session-scoped in-memory key-value store with
 * auto-clear at session boundaries.
 *
 * Memory is strictly scoped to (agentId, sessionId). No entry survives a
 * session close, idle timeout, or max-duration expiry. If any bound pack
 * declares `requiresSessionMemoryIsolation: true`, the store also wipes
 * itself when the regulated pack exits (e.g. exiting a HIPAA-governed
 * block auto-wipes PHI that was held in memory during that block).
 *
 * All clear events emit `governance:memory-cleared` audit events so the
 * chain records what was wiped, why, and when.
 *
 * P3-T1 coverage gap: Session-scoped memory with auto-clear between cycles.
 *
 * Compliance bindings:
 *   - HIPAA 164.312(a)(2)(i): automatic logoff / session termination
 *   - GDPR Art. 5(1)(e): storage limitation
 *   - SOC 2 CC6.7: data disposal controls
 *   - ISO 42001 A.8.1: AI data minimization
 *
 * @connexum/ai-governance
 */
// ---------------------------------------------------------------------------
// Errors
// ---------------------------------------------------------------------------
export class SessionMemoryExpiredError extends Error {
    agentId;
    sessionId;
    reason;
    constructor(opts) {
        super(`Session memory for agent '${opts.agentId}' session '${opts.sessionId}' has been cleared (reason: ${opts.reason}). ` +
            `Start a new session to store memory.`);
        this.name = 'SessionMemoryExpiredError';
        this.agentId = opts.agentId;
        this.sessionId = opts.sessionId;
        this.reason = opts.reason;
    }
}
const DEFAULT_CONFIG = {
    maxIdleMs: 15 * 60 * 1000,
    maxSessionMs: 60 * 60 * 1000,
    clearOnRegulatedPackExit: false,
};
// ---------------------------------------------------------------------------
// SessionMemoryStore
// ---------------------------------------------------------------------------
export class SessionMemoryStore {
    config;
    sessions = new Map();
    auditSink;
    constructor(config = {}, auditSink) {
        this.config = { ...DEFAULT_CONFIG, ...config };
        this.auditSink = auditSink ?? (() => undefined);
    }
    // -------------------------------------------------------------------------
    // Session lifecycle
    // -------------------------------------------------------------------------
    /**
     * Open a new session. Returns the session key for subsequent operations.
     * Throws if a session with this (agentId, sessionId) is already open.
     */
    openSession(agentId, sessionId) {
        const key = this.sessionKey(agentId, sessionId);
        if (this.sessions.has(key)) {
            const existing = this.sessions.get(key);
            if (!existing.cleared) {
                throw new Error(`Session '${sessionId}' for agent '${agentId}' is already open. Close it before re-opening.`);
            }
            // Cleared session: allow re-open as fresh start.
        }
        this.sessions.set(key, {
            entries: new Map(),
            openedAt: Date.now(),
            lastAccessedAt: Date.now(),
            cleared: false,
        });
    }
    /**
     * Explicitly close a session. All entries are wiped and a
     * `governance:memory-cleared` audit event is emitted.
     */
    close(agentId, sessionId) {
        this.clearSession(agentId, sessionId, 'explicit-close');
    }
    /**
     * Call when a regulated pack exits (e.g. end of HIPAA-governed block).
     * If `clearOnRegulatedPackExit` is true, wipes memory for the session.
     */
    onRegulatedPackExit(agentId, sessionId) {
        if (!this.config.clearOnRegulatedPackExit)
            return;
        const key = this.sessionKey(agentId, sessionId);
        const record = this.sessions.get(key);
        if (!record || record.cleared)
            return;
        this.clearSession(agentId, sessionId, 'regulated-pack-exit');
    }
    // -------------------------------------------------------------------------
    // Read / Write
    // -------------------------------------------------------------------------
    set(agentId, sessionId, memKey, value, dataClass = 'UNCLASSIFIED') {
        const record = this.requireAlive(agentId, sessionId);
        record.entries.set(memKey, { value, dataClass, writtenAt: Date.now() });
        record.lastAccessedAt = Date.now();
    }
    get(agentId, sessionId, memKey) {
        const record = this.requireAlive(agentId, sessionId);
        record.lastAccessedAt = Date.now();
        return record.entries.get(memKey)?.value ?? undefined;
    }
    has(agentId, sessionId, memKey) {
        const record = this.requireAlive(agentId, sessionId);
        record.lastAccessedAt = Date.now();
        return record.entries.has(memKey);
    }
    delete(agentId, sessionId, memKey) {
        const record = this.requireAlive(agentId, sessionId);
        record.lastAccessedAt = Date.now();
        return record.entries.delete(memKey);
    }
    /** Return entry count for a live session (does not update lastAccessedAt). */
    size(agentId, sessionId) {
        const key = this.sessionKey(agentId, sessionId);
        const record = this.sessions.get(key);
        if (!record || record.cleared)
            return 0;
        return record.entries.size;
    }
    // -------------------------------------------------------------------------
    // Expiry checks (call periodically or before each operation)
    // -------------------------------------------------------------------------
    /**
     * Check all open sessions against idle and max-session limits.
     * Clears any that have expired and emits audit events.
     * Returns the number of sessions that were cleared.
     */
    sweepExpired() {
        const now = Date.now();
        let cleared = 0;
        for (const [key, record] of this.sessions) {
            if (record.cleared)
                continue;
            const [agentId, sessionId] = this.splitKey(key);
            if (now - record.lastAccessedAt > this.config.maxIdleMs) {
                this.clearSession(agentId, sessionId, 'idle-timeout');
                cleared++;
            }
            else if (now - record.openedAt > this.config.maxSessionMs) {
                this.clearSession(agentId, sessionId, 'max-session-exceeded');
                cleared++;
            }
        }
        return cleared;
    }
    // -------------------------------------------------------------------------
    // Internal helpers
    // -------------------------------------------------------------------------
    sessionKey(agentId, sessionId) {
        return `${agentId}::${sessionId}`;
    }
    splitKey(key) {
        const idx = key.indexOf('::');
        return [key.slice(0, idx), key.slice(idx + 2)];
    }
    requireAlive(agentId, sessionId) {
        const key = this.sessionKey(agentId, sessionId);
        const record = this.sessions.get(key);
        if (!record) {
            throw new SessionMemoryExpiredError({
                agentId,
                sessionId,
                reason: 'explicit-close',
            });
        }
        if (record.cleared) {
            throw new SessionMemoryExpiredError({
                agentId,
                sessionId,
                reason: record.clearReason ?? 'explicit-close',
            });
        }
        // Lazy expiry on access.
        const now = Date.now();
        if (now - record.lastAccessedAt > this.config.maxIdleMs) {
            this.clearSession(agentId, sessionId, 'idle-timeout');
            throw new SessionMemoryExpiredError({ agentId, sessionId, reason: 'idle-timeout' });
        }
        if (now - record.openedAt > this.config.maxSessionMs) {
            this.clearSession(agentId, sessionId, 'max-session-exceeded');
            throw new SessionMemoryExpiredError({ agentId, sessionId, reason: 'max-session-exceeded' });
        }
        return record;
    }
    clearSession(agentId, sessionId, reason) {
        const key = this.sessionKey(agentId, sessionId);
        const record = this.sessions.get(key);
        if (!record || record.cleared)
            return;
        const count = record.entries.size;
        record.entries.clear();
        record.cleared = true;
        record.clearReason = reason;
        const event = {
            type: 'governance:memory-cleared',
            agentId,
            sessionId,
            entriesCleared: count,
            reason,
            timestamp: new Date().toISOString(),
        };
        this.auditSink(event);
    }
}
//# sourceMappingURL=session-memory.js.map