/**
 * CycleTimeoutEnforcer -- Wall-clock timeout gate per agent cycle.
 *
 * Fires during PreToolUse. If the wall-clock elapsed time since cycle
 * start exceeds the configured limit the gate applies one of three
 * actions: pause (approval-queue resume required), terminate
 * (cycle ends immediately), or escalate (route to human-reviewer tier).
 *
 * P3-T1 coverage gap: Cycle timeout termination as first-class control.
 *
 * Compliance bindings:
 *   - EU AI Act Art. 14: human oversight -- escalate action routes to
 *     human reviewer at the timeout boundary.
 *   - DORA Art. 17: ICT incident management -- bounded cycle duration.
 *   - SOC 2 CC7.2: system monitoring / anomaly response.
 *   - ISO 42001 A.6.1.4: AI resource management.
 *
 * @connexum/ai-governance
 */
// ---------------------------------------------------------------------------
// Errors
// ---------------------------------------------------------------------------
export class CycleTimeoutError extends Error {
    agentId;
    cycleId;
    elapsedMs;
    limitMs;
    action;
    packBinding;
    constructor(opts) {
        const verb = opts.action === 'pause'
            ? 'paused pending operator resume'
            : opts.action === 'terminate'
                ? 'terminated'
                : 'escalated to human reviewer';
        super(`Cycle timeout: agent '${opts.agentId}' cycle '${opts.cycleId}' ` +
            `has been running for ${opts.elapsedMs} ms (limit: ${opts.limitMs} ms). ` +
            `Cycle ${verb}.`);
        this.name = 'CycleTimeoutError';
        this.agentId = opts.agentId;
        this.cycleId = opts.cycleId;
        this.elapsedMs = opts.elapsedMs;
        this.limitMs = opts.limitMs;
        this.action = opts.action;
        this.packBinding = opts.packBinding ?? [];
    }
}
// ---------------------------------------------------------------------------
// CycleTimeoutEnforcer
// ---------------------------------------------------------------------------
export class CycleTimeoutEnforcer {
    config;
    /** Map key: `${agentId}::${cycleId}` */
    cycles = new Map();
    constructor(config) {
        if (config.perCycleMaxMs <= 0) {
            throw new Error('CycleTimeoutEnforcer: perCycleMaxMs must be > 0');
        }
        this.config = config;
    }
    /**
     * Register a cycle start. Must be called when a new agent cycle begins so
     * the enforcer has an accurate wall-clock start time.
     *
     * Idempotent -- calling again on the same key does not reset the clock.
     */
    startCycle(agentId, cycleId) {
        const key = `${agentId}::${cycleId}`;
        if (!this.cycles.has(key)) {
            this.cycles.set(key, {
                startTime: Date.now(),
                timedOut: false,
                terminated: false,
            });
        }
    }
    /**
     * Resolve the timeout limit for a given agent and active pack set.
     */
    resolveLimit(agentId, activePackIds = []) {
        // 1. Per-agent override (highest precedence).
        if (this.config.perAgentOverrides[agentId] !== undefined) {
            return this.config.perAgentOverrides[agentId];
        }
        // 2. Per-pack override -- first matching active pack.
        for (const packId of activePackIds) {
            if (this.config.perPackOverrides[packId] !== undefined) {
                return this.config.perPackOverrides[packId];
            }
        }
        // 3. Default.
        return this.config.perCycleMaxMs;
    }
    /**
     * PreToolUse hook entry point.
     *
     * Checks whether the cycle has exceeded its wall-clock limit.
     * If so, applies the configured action and throws synchronously.
     *
     * If `startCycle` was never called for this key the call is implicitly
     * registered now (first-tool-use assumption).
     *
     * @param agentId      Identity of the calling agent.
     * @param cycleId      Unique identifier for the current agent cycle.
     * @param activePackIds Active compliance pack IDs for override resolution.
     */
    hook(agentId, cycleId, activePackIds = []) {
        const key = `${agentId}::${cycleId}`;
        // Implicit cycle registration on first tool call.
        if (!this.cycles.has(key)) {
            this.startCycle(agentId, cycleId);
        }
        const record = this.cycles.get(key);
        // Always block terminated cycles.
        if (record.terminated) {
            const limitMs = this.resolveLimit(agentId, activePackIds);
            throw new CycleTimeoutError({
                agentId,
                cycleId,
                elapsedMs: Date.now() - record.startTime,
                limitMs,
                action: 'terminate',
                packBinding: activePackIds,
            });
        }
        // Block paused cycles (pause action already fired).
        if (record.timedOut && this.config.onTimeout === 'pause') {
            const limitMs = this.resolveLimit(agentId, activePackIds);
            throw new CycleTimeoutError({
                agentId,
                cycleId,
                elapsedMs: Date.now() - record.startTime,
                limitMs,
                action: 'pause',
                packBinding: activePackIds,
            });
        }
        const now = Date.now();
        const elapsedMs = now - record.startTime;
        const limitMs = this.resolveLimit(agentId, activePackIds);
        if (elapsedMs <= limitMs)
            return; // Within budget -- allow.
        // Timeout boundary crossed. Apply action.
        record.timedOut = true;
        if (this.config.onTimeout === 'terminate') {
            record.terminated = true;
        }
        const eventType = this.config.onTimeout === 'terminate'
            ? 'agent:cycle-terminated-by-timeout'
            : this.config.onTimeout === 'escalate'
                ? 'agent:cycle-escalated-by-timeout'
                : 'agent:cycle-paused-by-timeout';
        const auditEvent = {
            type: eventType,
            agentId,
            cycleId,
            elapsedMs,
            limitMs,
            action: this.config.onTimeout,
            reviewerTier: this.config.onTimeout === 'escalate' ? 'human-reviewer' : undefined,
            packBinding: activePackIds,
            timestamp: new Date().toISOString(),
        };
        this.config.onTimedOut?.(auditEvent);
        throw new CycleTimeoutError({
            agentId,
            cycleId,
            elapsedMs,
            limitMs,
            action: this.config.onTimeout,
            packBinding: activePackIds,
        });
    }
    /**
     * Operator resume: clear the timedOut flag so a paused cycle may continue.
     * Only applicable when `onTimeout === 'pause'`. Terminated cycles are immutable.
     */
    resume(agentId, cycleId) {
        const record = this.cycles.get(`${agentId}::${cycleId}`);
        if (!record)
            return;
        if (record.terminated) {
            throw new Error(`Cannot resume cycle '${cycleId}' for agent '${agentId}': cycle was terminated (immutable).`);
        }
        record.timedOut = false;
    }
    /**
     * Mark a cycle as completed (removes it from tracking).
     */
    endCycle(agentId, cycleId) {
        this.cycles.delete(`${agentId}::${cycleId}`);
    }
    /**
     * Return elapsed ms for a cycle. Returns 0 if the cycle is unknown.
     */
    getElapsed(agentId, cycleId) {
        const record = this.cycles.get(`${agentId}::${cycleId}`);
        if (!record)
            return 0;
        return Date.now() - record.startTime;
    }
    /** Return whether a cycle has been terminated. */
    isTerminated(agentId, cycleId) {
        return !!this.cycles.get(`${agentId}::${cycleId}`)?.terminated;
    }
    /** Return whether a cycle is currently paused (timed out but not terminated). */
    isPaused(agentId, cycleId) {
        const r = this.cycles.get(`${agentId}::${cycleId}`);
        return !!r && r.timedOut && !r.terminated;
    }
}
//# sourceMappingURL=cycle-timeout.js.map