"use strict";
/**
 * SessionPersistence -- Save/restore agent state across sessions.
 *
 * Handles session start recovery, pre-compact state dumps,
 * and session end summaries. Ensures zero context loss during
 * compaction or session boundaries.
 *
 * @connexum/ai-governance
 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.SessionPersistence = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const crypto = __importStar(require("crypto"));
class SessionPersistence {
    config;
    currentState = null;
    encryption;
    constructor(config) {
        this.config = {
            stateDir: config.stateDir,
            agentName: config.agentName,
            dailyLogDir: config.dailyLogDir || path.join(config.stateDir, 'daily-logs'),
            maxStates: config.maxStates ?? 10,
        };
        this.encryption = config.encryption;
        this.ensureDirectories();
    }
    ensureDirectories() {
        for (const dir of [this.config.stateDir, this.config.dailyLogDir]) {
            if (!fs.existsSync(dir)) {
                fs.mkdirSync(dir, { recursive: true });
            }
        }
    }
    /**
     * Start a new session. Checks for interrupted work from previous session.
     * Returns the previous state if one exists (for recovery).
     */
    startSession() {
        const sessionId = `${this.config.agentName}-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
        const previousState = this.loadLatestState();
        let interrupted = false;
        if (previousState) {
            // Check if the previous session ended cleanly
            const hasActiveTasks = previousState.activeTasks.some(t => t.status === 'in_progress');
            const hasPendingDecisions = previousState.pendingDecisions.length > 0;
            interrupted = hasActiveTasks || hasPendingDecisions;
        }
        this.currentState = {
            sessionId,
            agentName: this.config.agentName,
            startedAt: new Date().toISOString(),
            lastSavedAt: new Date().toISOString(),
            activeTasks: [],
            modifiedFiles: [],
            pendingDecisions: [],
            tokenUsage: { input: 0, output: 0, cost: 0 },
            customState: {},
            integrity: '',
        };
        // If recovering from interrupted session, carry forward incomplete work
        if (interrupted && previousState) {
            this.currentState.activeTasks = previousState.activeTasks.filter(t => t.status === 'in_progress' || t.status === 'blocked' || t.status === 'waiting');
            this.currentState.pendingDecisions = previousState.pendingDecisions;
        }
        return { sessionId, previousState, interrupted };
    }
    /**
     * Save current session state to disk. Call before compaction or periodically.
     */
    saveState(updates) {
        if (!this.currentState) {
            throw new Error('No active session. Call startSession() first.');
        }
        if (updates) {
            if (updates.activeTasks)
                this.currentState.activeTasks = updates.activeTasks;
            if (updates.modifiedFiles)
                this.currentState.modifiedFiles = updates.modifiedFiles;
            if (updates.pendingDecisions)
                this.currentState.pendingDecisions = updates.pendingDecisions;
            if (updates.tokenUsage)
                this.currentState.tokenUsage = updates.tokenUsage;
            if (updates.customState) {
                this.currentState.customState = {
                    ...this.currentState.customState,
                    ...updates.customState,
                };
            }
        }
        this.currentState.lastSavedAt = new Date().toISOString();
        this.currentState.integrity = this.computeIntegrity(this.currentState);
        const filePath = this.stateFilePath(this.currentState.sessionId);
        const data = JSON.stringify(this.currentState, null, 2);
        // T-033: Encrypt session state at rest if encryption is configured
        if (this.encryption) {
            this.encryption.writeEncrypted(filePath, data);
        }
        else {
            fs.writeFileSync(filePath, data);
        }
        this.pruneOldStates();
    }
    /**
     * Record a file modification in the current session.
     */
    recordFileModification(filePath) {
        if (!this.currentState)
            return;
        if (!this.currentState.modifiedFiles.includes(filePath)) {
            this.currentState.modifiedFiles.push(filePath);
        }
    }
    /**
     * Record token usage.
     */
    recordTokenUsage(input, output, cost) {
        if (!this.currentState)
            return;
        this.currentState.tokenUsage.input += input;
        this.currentState.tokenUsage.output += output;
        this.currentState.tokenUsage.cost += cost;
    }
    /**
     * Add or update a task in the current session.
     */
    updateTask(task) {
        if (!this.currentState)
            return;
        const idx = this.currentState.activeTasks.findIndex(t => t.id === task.id);
        if (idx >= 0) {
            this.currentState.activeTasks[idx] = task;
        }
        else {
            this.currentState.activeTasks.push(task);
        }
    }
    /**
     * Remove a completed task.
     */
    completeTask(taskId) {
        if (!this.currentState)
            return undefined;
        const idx = this.currentState.activeTasks.findIndex(t => t.id === taskId);
        if (idx >= 0) {
            const [task] = this.currentState.activeTasks.splice(idx, 1);
            return task;
        }
        return undefined;
    }
    /**
     * Pre-compact dump: saves full state + writes daily log entry.
     * Called by pre-compact hook before context compaction.
     */
    preCompactDump() {
        if (!this.currentState) {
            throw new Error('No active session. Call startSession() first.');
        }
        // Save current state
        this.saveState();
        // Write daily log entry
        const today = new Date().toISOString().split('T')[0];
        const logFile = path.join(this.config.dailyLogDir, `${today}.jsonl`);
        const logEntry = {
            type: 'pre-compact',
            sessionId: this.currentState.sessionId,
            agent: this.config.agentName,
            timestamp: new Date().toISOString(),
            activeTasks: this.currentState.activeTasks.length,
            modifiedFiles: this.currentState.modifiedFiles.length,
            pendingDecisions: this.currentState.pendingDecisions.length,
            tokenUsage: this.currentState.tokenUsage,
        };
        fs.appendFileSync(logFile, JSON.stringify(logEntry) + '\n');
        return this.stateFilePath(this.currentState.sessionId);
    }
    /**
     * End session: generate summary, write final state + daily log.
     */
    endSession(summary) {
        if (!this.currentState) {
            throw new Error('No active session. Call startSession() first.');
        }
        const startTime = new Date(this.currentState.startedAt).getTime();
        const endTime = Date.now();
        const durationMs = endTime - startTime;
        const hours = Math.floor(durationMs / 3600000);
        const minutes = Math.floor((durationMs % 3600000) / 60000);
        const duration = hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
        const sessionSummary = {
            sessionId: this.currentState.sessionId,
            agentName: this.config.agentName,
            duration,
            tasksCompleted: 0, // caller can update via customState
            tasksRemaining: this.currentState.activeTasks.length,
            filesModified: [...this.currentState.modifiedFiles],
            tokenUsage: { ...this.currentState.tokenUsage },
            decisionsResolved: 0,
            decisionsPending: this.currentState.pendingDecisions.length,
            summary,
        };
        // Write final state
        this.saveState();
        // Write session summary to daily log
        const today = new Date().toISOString().split('T')[0];
        const logFile = path.join(this.config.dailyLogDir, `${today}.jsonl`);
        const logEntry = {
            type: 'session-end',
            ...sessionSummary,
            timestamp: new Date().toISOString(),
        };
        fs.appendFileSync(logFile, JSON.stringify(logEntry) + '\n');
        this.currentState = null;
        return sessionSummary;
    }
    /**
     * Load the most recent session state from disk.
     */
    loadLatestState() {
        const stateFiles = this.listStateFiles();
        if (stateFiles.length === 0)
            return null;
        // Sort by mtime descending
        const latest = stateFiles.sort((a, b) => {
            const astat = fs.statSync(a);
            const bstat = fs.statSync(b);
            return bstat.mtimeMs - astat.mtimeMs;
        })[0];
        return this.loadAndVerifyState(latest);
    }
    /**
     * Search past session logs by keyword.
     */
    searchSessionLogs(query, maxResults = 20) {
        const results = [];
        const logDir = this.config.dailyLogDir;
        if (!fs.existsSync(logDir))
            return results;
        const logFiles = fs.readdirSync(logDir)
            .filter(f => f.endsWith('.jsonl'))
            .sort()
            .reverse();
        const lowerQuery = query.toLowerCase();
        for (const file of logFiles) {
            if (results.length >= maxResults)
                break;
            const date = file.replace('.jsonl', '');
            const lines = fs.readFileSync(path.join(logDir, file), 'utf-8')
                .split('\n')
                .filter(Boolean);
            for (const line of lines) {
                if (results.length >= maxResults)
                    break;
                if (line.toLowerCase().includes(lowerQuery)) {
                    try {
                        results.push({ date, entry: JSON.parse(line) });
                    }
                    catch {
                        // skip malformed lines
                    }
                }
            }
        }
        return results;
    }
    /**
     * Get session metrics for the current session.
     */
    getMetrics() {
        if (!this.currentState)
            return null;
        const elapsed = Date.now() - new Date(this.currentState.startedAt).getTime();
        const minutes = Math.floor(elapsed / 60000);
        return {
            tokenUsage: { ...this.currentState.tokenUsage },
            filesModified: this.currentState.modifiedFiles.length,
            activeTasks: this.currentState.activeTasks.length,
            elapsed: `${minutes}m`,
        };
    }
    /**
     * Get the current state (read-only snapshot).
     */
    getCurrentState() {
        return this.currentState ? { ...this.currentState } : null;
    }
    // --- Private helpers ---
    stateFilePath(sessionId) {
        return path.join(this.config.stateDir, `${sessionId}.json`);
    }
    listStateFiles() {
        if (!fs.existsSync(this.config.stateDir))
            return [];
        return fs.readdirSync(this.config.stateDir)
            .filter(f => f.endsWith('.json') && f.includes(this.config.agentName))
            .map(f => path.join(this.config.stateDir, f));
    }
    loadAndVerifyState(filePath) {
        try {
            // T-033: Decrypt session state if encryption is configured
            const raw = this.encryption
                ? this.encryption.readEncrypted(filePath)
                : fs.readFileSync(filePath, 'utf-8');
            const state = JSON.parse(raw);
            // Verify integrity
            const expected = state.integrity;
            const computed = this.computeIntegrity(state);
            if (expected && expected !== computed) {
                // Tampered state file - do not trust
                return null;
            }
            return state;
        }
        catch {
            return null;
        }
    }
    computeIntegrity(state) {
        const copy = { ...state, integrity: '' };
        return crypto.createHash('sha256')
            .update(JSON.stringify(copy))
            .digest('hex');
    }
    pruneOldStates() {
        const stateFiles = this.listStateFiles().sort((a, b) => {
            const astat = fs.statSync(a);
            const bstat = fs.statSync(b);
            return bstat.mtimeMs - astat.mtimeMs;
        });
        // Remove excess state files
        const excess = stateFiles.slice(this.config.maxStates);
        for (const file of excess) {
            try {
                fs.unlinkSync(file);
            }
            catch { /* ignore */ }
        }
    }
}
exports.SessionPersistence = SessionPersistence;
//# sourceMappingURL=session-persistence.js.map