"use strict";
// Event Store — append-only event log with temporal queries, snapshots, and undo.
//
// Every significant action in the harness produces an event. Events are
// immutable once written. Snapshots capture point-in-time state for fast
// restore. Undo replays events up to (but not including) a target event.
//
// Storage: .harness/events/events.jsonl (append-only)
//          .harness/events/snapshots/ (per-service JSON snapshots)
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.subscribeEventStream = subscribeEventStream;
exports.appendEvent = appendEvent;
exports.emitEvent = emitEvent;
exports.queryEvents = queryEvents;
exports.getEvent = getEvent;
exports.createSnapshot = createSnapshot;
exports.getSnapshot = getSnapshot;
exports.listSnapshots = listSnapshots;
exports.getUndoEvents = getUndoEvents;
exports.summarizeEventStore = summarizeEventStore;
exports.generatePostmortem = generatePostmortem;
exports.pruneEventStore = pruneEventStore;
exports.pruneEventsByAge = pruneEventsByAge;
const crypto = __importStar(require("crypto"));
const fs = __importStar(require("fs/promises"));
const path = __importStar(require("path"));
const readline = __importStar(require("readline"));
const silentFailureSink_1 = require("../observability/silentFailureSink");
const fs_1 = require("fs");
// ─── Paths ──────────────────────────────────────────────────────────
function eventsFilePath(projectDir) {
    return path.join(projectDir, '.harness', 'events', 'events.jsonl');
}
function snapshotsDir(projectDir) {
    return path.join(projectDir, '.harness', 'events', 'snapshots');
}
// ─── Write ──────────────────────────────────────────────────────────
const MAX_EVENT_LINES = 10_000;
const WRITE_LOCK_TIMEOUT_MS = 5_000;
const WRITE_LOCK_RETRY_MS = 25;
const WRITE_LOCK_STALE_MS = 30_000;
const WRITE_RENAME_RETRY_DELAYS_MS = [20, 60, 120];
let knownEventLineCount = -1;
let appendSeq = 0;
// Serialize background pruning + appends so concurrent emissions can't race
// with a prune (which rewrites the file). Reads still see the file at a
// consistent moment because fs.appendFile and fs.writeFile are atomic at the
// syscall level on every supported OS, but the in-memory invariants
// (knownEventLineCount, appendSeq) only stay correct if we serialize.
let writeChain = Promise.resolve();
function eventsLockPath(projectDir) {
    return path.join(projectDir, '.harness', 'events', 'events.lock');
}
function sleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
}
function isTransientRenameError(error) {
    if (process.platform !== 'win32')
        return false;
    const code = error.code;
    return code === 'EPERM' || code === 'EBUSY' || code === 'EACCES';
}
function isTransientLockAcquireError(error) {
    const code = error.code;
    if (code === 'EEXIST')
        return true;
    if (process.platform !== 'win32')
        return false;
    return code === 'EPERM' || code === 'EACCES' || code === 'EBUSY';
}
async function renameFileWithRetry(tmpPath, targetPath) {
    for (let attempt = 0; attempt <= WRITE_RENAME_RETRY_DELAYS_MS.length; attempt += 1) {
        try {
            await fs.rename(tmpPath, targetPath);
            return;
        }
        catch (error) {
            if (!isTransientRenameError(error) || attempt === WRITE_RENAME_RETRY_DELAYS_MS.length)
                throw error;
            await sleep(WRITE_RENAME_RETRY_DELAYS_MS[attempt]);
        }
    }
}
async function writeFileAtomically(targetPath, content) {
    const tmpPath = `${targetPath}.tmp-${process.pid}-${Date.now()}`;
    await fs.writeFile(tmpPath, content, 'utf-8');
    try {
        await renameFileWithRetry(tmpPath, targetPath);
    }
    finally {
        await fs.rm(tmpPath, { force: true }).catch(() => { });
    }
}
async function acquireWriteLock(projectDir) {
    const lockPath = eventsLockPath(projectDir);
    await fs.mkdir(path.dirname(lockPath), { recursive: true });
    const owner = `${process.pid}:${Date.now()}:${crypto.randomUUID()}`;
    const deadline = Date.now() + WRITE_LOCK_TIMEOUT_MS;
    while (true) {
        try {
            await fs.writeFile(lockPath, owner, { encoding: 'utf-8', flag: 'wx' });
            return async () => {
                try {
                    const currentOwner = await fs.readFile(lockPath, 'utf-8');
                    if (currentOwner === owner)
                        await fs.rm(lockPath, { force: true });
                }
                catch {
                    // Lock already released or missing.
                }
            };
        }
        catch (error) {
            const code = error.code;
            if (!isTransientLockAcquireError(error))
                throw error;
            if (code === 'EEXIST') {
                try {
                    const stats = await fs.stat(lockPath);
                    if ((Date.now() - stats.mtimeMs) > WRITE_LOCK_STALE_MS) {
                        await fs.rm(lockPath, { force: true });
                        continue;
                    }
                }
                catch {
                    // Lock file may have been removed by another writer; retry.
                }
            }
            if (Date.now() >= deadline) {
                throw new Error(`Timed out acquiring event store write lock after ${WRITE_LOCK_TIMEOUT_MS}ms.`);
            }
            await sleep(WRITE_LOCK_RETRY_MS);
        }
    }
}
async function withWriteLock(projectDir, action) {
    const release = await acquireWriteLock(projectDir);
    try {
        return await action();
    }
    finally {
        await release();
    }
}
/** SSE subscribers for live event streaming. */
const eventStreamListeners = new Set();
function subscribeEventStream(listener) {
    eventStreamListeners.add(listener);
    return () => { eventStreamListeners.delete(listener); };
}
async function appendEvent(projectDir, event) {
    const next = writeChain.then(() => withWriteLock(projectDir, async () => {
        const full = {
            event_id: crypto.randomUUID(),
            timestamp: new Date().toISOString(),
            seq: ++appendSeq,
            ...event,
        };
        const fp = eventsFilePath(projectDir);
        await fs.mkdir(path.dirname(fp), { recursive: true });
        await fs.appendFile(fp, JSON.stringify(full) + '\n', 'utf-8');
        // Notify live stream listeners.
        for (const listener of eventStreamListeners) {
            try {
                listener(full);
            }
            catch (err) {
                (0, silentFailureSink_1.recordSwallowed)('eventStore.streamListener', err);
            }
        }
        // Auto-prune when line count is estimated to exceed the cap. Awaited so
        // the next chained append/query observes the post-prune state.
        knownEventLineCount = knownEventLineCount < 0 ? MAX_EVENT_LINES : knownEventLineCount + 1;
        if (knownEventLineCount > MAX_EVENT_LINES) {
            try {
                await pruneEventStoreInternal(projectDir, MAX_EVENT_LINES);
            }
            catch (err) {
                (0, silentFailureSink_1.recordSwallowed)('eventStore.autoPrune', err);
            }
        }
        return full;
    }));
    // Keep the chain alive even if this append rejects.
    writeChain = next.catch(() => { });
    return next;
}
/** Convenience: emit a typed event. */
function emitEvent(projectDir, category, type, data, actor = 'system', subjectId, parentEventId) {
    return appendEvent(projectDir, { category, type, data, actor, subject_id: subjectId, parent_event_id: parentEventId });
}
// ─── Read / Query ───────────────────────────────────────────────────
async function queryEvents(projectDir, query = {}) {
    const fp = eventsFilePath(projectDir);
    try {
        await fs.access(fp);
    }
    catch {
        return [];
    }
    const results = [];
    const rl = readline.createInterface({ input: (0, fs_1.createReadStream)(fp, 'utf-8'), crlfDelay: Infinity });
    // Track file-append order so equal-timestamp events (rapid emissions in the
    // same millisecond) keep a deterministic chronological order. Without this
    // tiebreaker the DESC sort is stable but downstream callers that reverse the
    // result get reordered events whenever timestamps tie. See eventStore.test
    // 'gets undo events'.
    const appendOrder = new Map();
    let nextOrder = 0;
    for await (const line of rl) {
        if (!line.trim())
            continue;
        try {
            const ev = JSON.parse(line);
            if (query.category && ev.category !== query.category)
                continue;
            if (query.type && ev.type !== query.type)
                continue;
            if (query.subject_id && ev.subject_id !== query.subject_id)
                continue;
            if (query.actor && ev.actor !== query.actor)
                continue;
            if (query.after && ev.timestamp <= query.after)
                continue;
            if (query.before && ev.timestamp >= query.before)
                continue;
            appendOrder.set(ev.event_id, nextOrder++);
            results.push(ev);
        }
        catch { /* skip corrupt lines */ }
    }
    // Most recent first; tie-break by seq when present, else by file-append order so
    // chronological reversal stays deterministic.
    results.sort((a, b) => {
        const cmp = b.timestamp.localeCompare(a.timestamp);
        if (cmp !== 0)
            return cmp;
        if (typeof a.seq === 'number' && typeof b.seq === 'number')
            return b.seq - a.seq;
        return (appendOrder.get(b.event_id) ?? 0) - (appendOrder.get(a.event_id) ?? 0);
    });
    if (query.limit && results.length > query.limit)
        results.length = query.limit;
    return results;
}
async function getEvent(projectDir, eventId) {
    const fp = eventsFilePath(projectDir);
    try {
        await fs.access(fp);
    }
    catch {
        return null;
    }
    const rl = readline.createInterface({ input: (0, fs_1.createReadStream)(fp, 'utf-8'), crlfDelay: Infinity });
    let found = null;
    for await (const line of rl) {
        if (!line.trim())
            continue;
        try {
            const ev = JSON.parse(line);
            if (ev.event_id === eventId) {
                found = ev;
                break;
            }
        }
        catch { /* skip */ }
    }
    rl.close();
    return found;
}
// ─── Snapshots ──────────────────────────────────────────────────────
async function createSnapshot(projectDir, subjectId, state, lastEventId) {
    const snapshot = {
        snapshot_id: crypto.randomUUID(),
        subject_id: subjectId,
        timestamp: new Date().toISOString(),
        state,
        last_event_id: lastEventId,
    };
    const dir = snapshotsDir(projectDir);
    await fs.mkdir(dir, { recursive: true });
    await fs.writeFile(path.join(dir, `${subjectId}.json`), JSON.stringify(snapshot, null, 2), 'utf-8');
    return snapshot;
}
async function getSnapshot(projectDir, subjectId) {
    const fp = path.join(snapshotsDir(projectDir), `${subjectId}.json`);
    try {
        const raw = await fs.readFile(fp, 'utf-8');
        return JSON.parse(raw);
    }
    catch {
        return null;
    }
}
async function listSnapshots(projectDir) {
    const dir = snapshotsDir(projectDir);
    try {
        const files = await fs.readdir(dir);
        return files.filter((f) => f.endsWith('.json')).map((f) => f.replace('.json', ''));
    }
    catch {
        return [];
    }
}
// ─── Undo ───────────────────────────────────────────────────────────
/**
 * Replay events for a subject, stopping before a target event.
 * Returns all events that would remain after "undoing" the target event
 * and everything after it.
 */
async function getUndoEvents(projectDir, subjectId, undoBeforeEventId) {
    const all = await queryEvents(projectDir, { subject_id: subjectId });
    // Events are sorted most-recent-first; reverse for chronological
    all.reverse();
    const cutoff = all.findIndex((ev) => ev.event_id === undoBeforeEventId);
    if (cutoff < 0)
        return all; // target not found, return all
    return all.slice(0, cutoff);
}
// ─── Summary / Stats ────────────────────────────────────────────────
async function summarizeEventStore(projectDir) {
    const fp = eventsFilePath(projectDir);
    const categories = {};
    let total = 0;
    let firstAt;
    let lastAt;
    try {
        await fs.access(fp);
    }
    catch {
        const snapshots = await listSnapshots(projectDir);
        return { total_events: 0, categories, snapshot_count: snapshots.length };
    }
    const rl = readline.createInterface({ input: (0, fs_1.createReadStream)(fp, 'utf-8'), crlfDelay: Infinity });
    for await (const line of rl) {
        if (!line.trim())
            continue;
        try {
            const ev = JSON.parse(line);
            total++;
            categories[ev.category] = (categories[ev.category] ?? 0) + 1;
            if (!firstAt || ev.timestamp < firstAt)
                firstAt = ev.timestamp;
            if (!lastAt || ev.timestamp > lastAt)
                lastAt = ev.timestamp;
        }
        catch { /* skip */ }
    }
    const snapshots = await listSnapshots(projectDir);
    return { total_events: total, categories, first_event_at: firstAt, last_event_at: lastAt, snapshot_count: snapshots.length };
}
// ─── Postmortem ─────────────────────────────────────────────────────
/** Generate a text summary of events around a failure. */
async function generatePostmortem(projectDir, subjectId, windowMinutes = 30) {
    const events = await queryEvents(projectDir, { subject_id: subjectId });
    if (events.length === 0)
        return `No events found for ${subjectId}.`;
    // Find failure events
    const failures = events.filter((ev) => ev.type.includes('fail') || ev.type.includes('error') || ev.data?.success === false);
    if (failures.length === 0)
        return `No failure events found for ${subjectId}.`;
    const latestFailure = failures[0];
    const windowStart = new Date(new Date(latestFailure.timestamp).getTime() - windowMinutes * 60_000).toISOString();
    const windowEvents = events.filter((ev) => ev.timestamp >= windowStart);
    const lines = [
        `# Postmortem: ${subjectId}`,
        ``,
        `**Latest failure:** ${latestFailure.type} at ${latestFailure.timestamp}`,
        `**Window:** ${windowMinutes} minutes before failure`,
        `**Events in window:** ${windowEvents.length}`,
        ``,
        `## Timeline`,
        ``,
    ];
    for (const ev of windowEvents.reverse()) {
        const marker = ev.event_id === latestFailure.event_id ? ' ← FAILURE' : '';
        lines.push(`- **${ev.timestamp}** [${ev.category}/${ev.type}] ${JSON.stringify(ev.data).slice(0, 120)}${marker}`);
    }
    return lines.join('\n');
}
// ─── Pruning ────────────────────────────────────────────────────────
/** Keep only the most recent `maxEntries` events. */
async function pruneEventStoreInternal(projectDir, maxEntries = MAX_EVENT_LINES) {
    const fp = eventsFilePath(projectDir);
    try {
        await fs.access(fp);
    }
    catch {
        return 0;
    }
    const entries = [];
    const rl = readline.createInterface({ input: (0, fs_1.createReadStream)(fp, 'utf-8'), crlfDelay: Infinity });
    for await (const line of rl) {
        if (line.trim())
            entries.push(line);
    }
    if (entries.length <= maxEntries) {
        knownEventLineCount = entries.length;
        return 0;
    }
    const kept = entries.slice(-maxEntries);
    await writeFileAtomically(fp, kept.join('\n') + '\n');
    const pruned = entries.length - kept.length;
    knownEventLineCount = kept.length;
    return pruned;
}
/** Keep only the most recent `maxEntries` events. */
async function pruneEventStore(projectDir, maxEntries = MAX_EVENT_LINES) {
    return withWriteLock(projectDir, () => pruneEventStoreInternal(projectDir, maxEntries));
}
/** Remove events older than `retentionDays`. */
async function pruneEventsByAgeInternal(projectDir, retentionDays = 30) {
    const fp = eventsFilePath(projectDir);
    try {
        await fs.access(fp);
    }
    catch {
        return 0;
    }
    const cutoff = new Date(Date.now() - retentionDays * 86_400_000).toISOString();
    const kept = [];
    let pruned = 0;
    const rl = readline.createInterface({ input: (0, fs_1.createReadStream)(fp, 'utf-8'), crlfDelay: Infinity });
    for await (const line of rl) {
        if (!line.trim())
            continue;
        try {
            const ev = JSON.parse(line);
            if (ev.timestamp >= cutoff) {
                kept.push(line);
            }
            else {
                pruned++;
            }
        }
        catch {
            kept.push(line); // keep unparseable lines
        }
    }
    if (pruned > 0) {
        await writeFileAtomically(fp, kept.join('\n') + '\n');
        knownEventLineCount = kept.length;
    }
    return pruned;
}
/** Remove events older than `retentionDays`. */
async function pruneEventsByAge(projectDir, retentionDays = 30) {
    return withWriteLock(projectDir, () => pruneEventsByAgeInternal(projectDir, retentionDays));
}
//# sourceMappingURL=eventStore.js.map