/**
 * MilestoneNotificationSubscriber (DF-1, issue #438)
 *
 * Mirrors `auto-learning.subscriber.ts` exactly — same shape (subscribe
 * to lifecycle events, dispatch idempotently, log on failure) but with a
 * different action: enqueue a `[MILESTONE]` message into the orchestrator's
 * chat queue so orc's Smart Notification Protocol can decide whether to
 * forward to the owner. The QW-3 prompt change (#436, PR #581) adds a
 * dedicated `[MILESTONE]` row to that priority table — always 🟡 Important,
 * never downgraded to ⚪ Info.
 *
 * **Why a backend subscriber when the agent-side prompt path already exists?**
 * The QW-1 prompt change (#434, PR #581) tells agents to call `report-status
 * --status milestone` when they ship. But that only fires when the agent
 * decides to call it. Some lifecycle events (TL `verifyItem(verified)`,
 * `mission:replanned`) happen at the system level WITHOUT going through
 * report-status. The subscriber here is the safety net: any time a
 * verified/replanned event surfaces, the owner gets notified even if the
 * worker forgot to self-surface.
 *
 * Subscribed events (closed-set, mirrors the same Arch Veto V7 + N1
 * rationale as auto-learning):
 *   - `task:verified`        — TL verified a worker's output (shipped artifact)
 *   - `mission:replanned`    — strategy changed (rare, escalate)
 *
 * Idempotency (Arch Veto V1):
 *   `${eventType}:${workItemId}` (or `${eventType}:${missionId}:${eventId}`
 *   for mission events) against an in-memory bounded LRU.
 *
 * No new event types (Arch Veto V7):
 *   Listens only to event types already in `EVENT_TYPES`.
 *
 * Fire-and-forget:
 *   `enqueueNotification()` is async; each dispatch is wrapped so a thrown
 *   error logs but doesn't break the publisher or other subscribers.
 *
 * @module services/notification/milestone-notification.subscriber
 */
import { LoggerService } from '../core/logger.service.js';
import { formatError } from '../../utils/format-error.js';
import { ORCHESTRATOR_SESSION_NAME } from '../../constants.js';
/**
 * Events this subscriber listens to. Closed-set per the same Arch N1 rule
 * `auto-learning.subscriber` follows — additions require explicit review.
 */
export const MILESTONE_SUBSCRIBED_EVENTS = [
    'task:verified',
    'mission:replanned',
];
/** Bounded LRU cap to keep the dedup set from growing indefinitely. */
const DEDUP_LRU_CAPACITY = 1024;
/** Minimal LRU-ish bounded set — same shape `auto-learning.subscriber` uses. */
class BoundedSet {
    capacity;
    items = new Set();
    constructor(capacity) {
        this.capacity = Math.max(1, capacity);
    }
    has(key) {
        return this.items.has(key);
    }
    add(key) {
        if (this.items.has(key))
            return;
        if (this.items.size >= this.capacity) {
            const first = this.items.values().next().value;
            if (first !== undefined)
                this.items.delete(first);
        }
        this.items.add(key);
    }
    get size() {
        return this.items.size;
    }
}
/**
 * MilestoneNotificationSubscriber — surfaces lifecycle milestones to the
 * orchestrator's chat queue with a `[MILESTONE]` envelope.
 */
export class MilestoneNotificationSubscriber {
    eventBus;
    messageQueueService;
    logger;
    dedup;
    orchestratorSession;
    unsubscribers = [];
    started = false;
    pendingDispatches = new Set();
    constructor(deps) {
        this.eventBus = deps.eventBus;
        this.messageQueueService = deps.messageQueueService;
        this.logger =
            deps.logger ??
                LoggerService.getInstance().createComponentLogger('MilestoneNotificationSubscriber');
        this.dedup = new BoundedSet(deps.dedupCapacity ?? DEDUP_LRU_CAPACITY);
        this.orchestratorSession = deps.orchestratorSession ?? ORCHESTRATOR_SESSION_NAME;
    }
    /** Subscribe to all configured events. Idempotent. */
    start() {
        if (this.started)
            return;
        this.started = true;
        for (const eventType of MILESTONE_SUBSCRIBED_EVENTS) {
            this.unsubscribers.push(this.eventBus.onInProcess(eventType, (e) => this.safeDispatch(eventType, e)));
        }
        this.logger.info('MilestoneNotificationSubscriber subscribed', {
            eventTypes: MILESTONE_SUBSCRIBED_EVENTS,
            orchestratorSession: this.orchestratorSession,
        });
    }
    /** Wait for every in-flight dispatch. Test affordance. */
    async flushPending() {
        while (this.pendingDispatches.size > 0) {
            const inFlight = Array.from(this.pendingDispatches);
            await Promise.allSettled(inFlight);
        }
    }
    /** Detach all subscriptions. */
    stop() {
        for (const u of this.unsubscribers) {
            try {
                u();
            }
            catch (err) {
                this.logger.warn('MilestoneNotification unsubscribe threw', { error: formatError(err) });
            }
        }
        this.unsubscribers = [];
        this.started = false;
        this.logger.info('MilestoneNotificationSubscriber stopped');
    }
    /** Snapshot of the dedup state. Test affordance. */
    get dedupSize() {
        return this.dedup.size;
    }
    safeDispatch(eventType, event) {
        const promise = (async () => {
            try {
                await this.dispatchOne(eventType, event);
            }
            catch (err) {
                this.logger.warn('MilestoneNotification dispatch threw — swallowed', {
                    eventType,
                    eventId: event.id,
                    error: formatError(err),
                });
            }
        })();
        this.pendingDispatches.add(promise);
        promise.finally(() => this.pendingDispatches.delete(promise));
        return promise;
    }
    async dispatchOne(eventType, event) {
        // Idempotency key — `${type}:${workItemId}` for task events,
        // `${type}:${missionId}:${eventId}` for mission events.
        const wiKey = event.workItemId
            ? `${eventType}:${event.workItemId}`
            : `${eventType}:${event.missionId ?? ''}:${event.id}`;
        if (this.dedup.has(wiKey)) {
            this.logger.debug('MilestoneNotification deduped', { wiKey });
            return;
        }
        this.dedup.add(wiKey);
        // Trivial-event filter: a verified WI with no artifact / summary
        // signal isn't milestone-worthy. The agent-side report-status path
        // already filters via the ≥30-char summary gate (issue #435); this
        // backend filter catches the case where lifecycle events fire
        // without a corresponding agent surface.
        const summary = this.extractSummary(event);
        if (!summary || summary.length < 30) {
            this.logger.debug('MilestoneNotification skipped — summary too short or absent', {
                eventType,
                workItemId: event.workItemId,
            });
            return;
        }
        const envelope = this.buildEnvelope(eventType, event, summary);
        try {
            this.messageQueueService.enqueue({
                content: envelope,
                conversationId: 'system_milestone',
                source: 'system_event',
                targetSession: this.orchestratorSession,
                sourceMetadata: {
                    eventType,
                    workItemId: event.workItemId,
                    missionId: event.missionId,
                },
            });
            this.logger.info('MilestoneNotification enqueued', {
                eventType,
                workItemId: event.workItemId,
                summaryPreview: summary.slice(0, 80),
            });
        }
        catch (err) {
            this.logger.warn('MilestoneNotification enqueue failed (non-fatal)', {
                eventType,
                workItemId: event.workItemId,
                error: formatError(err),
            });
        }
    }
    /**
     * Best-effort summary extraction from an event payload. Different event
     * types stash human-readable text in different fields; this normalizes.
     */
    extractSummary(event) {
        const raw = event
            .summary
            ?? event.result?.summary
            ?? event.reason
            ?? '';
        return typeof raw === 'string' && raw.trim().length > 0 ? raw.trim() : null;
    }
    buildEnvelope(eventType, event, summary) {
        // `[MILESTONE] Source: ${who} | Event: ${eventType} | ${summary}`
        // Matches the priority-table row in orchestrator/prompt.md (#436)
        // verbatim, so the bypass-by-prompt rule fires correctly when this
        // envelope arrives.
        const who = event.sessionName || event.workItemId || 'system';
        return `[MILESTONE] Source: ${who} | Event: ${eventType} | ${summary}`;
    }
}
//# sourceMappingURL=milestone-notification.subscriber.js.map