import type { FrameResult, AudioResult } from '../types/detection.js';
import type { PipelineExecutionTrace } from '../types/pipeline-step.js';
import type { MotionRegion } from '../capabilities/motion-detection.cap.js';
import type { CameraPipelineConfig } from '../types/camera-pipeline.js';
import type { CameraMetrics } from './api-shared.js';
import type { RunnerLocalLoad } from './pipeline-runner-capability.js';
import type { BrokerStats } from './stream-broker.js';
import type { PipelineEngineChoice } from '../types/pipeline.js';
import type { NodeProcess, SystemResourceSnapshot } from './metrics-provider.js';
export interface EventSource {
    /** Primary source type: 'core', 'addon', 'device', 'pipeline', etc. */
    type: string;
    /** Primary identifier — addonId when type='addon', deviceId when type='device', etc. */
    id: string | number;
    /** Agent/node that originated the event (e.g. 'hub', 'agent-a1b2c3'). */
    nodeId?: string;
    /** Addon that originated the event (populated even when type='device'). */
    addonId?: string;
    /** Device the event relates to (populated even when type='addon'). */
    deviceId?: number;
}
/**
 * One detection entry emitted by a camera's firmware (not by the local
 * AI pipeline). Carried inside `detection.camera-native` events —
 * native providers fan out to this category for each ring/motion/AI
 * alarm push they receive from the camera.
 */
export interface CameraNativeDetection {
    /**
     * Firmware-reported class. Loose string to accommodate heterogeneous
     * firmwares (Reolink uses 'people'/'vehicle'/'animal'/…; ONVIF uses
     * 'PeopleDetect'; etc.). Common values: 'motion', 'person', 'vehicle',
     * 'animal', 'face', 'package', 'other', 'doorbell'.
     */
    readonly class: string;
    /** Ms epoch when the server observed the push from the camera. */
    readonly timestamp: number;
    /** Optional firmware-provided confidence [0..1]. Most firmwares don't expose it. */
    readonly confidence?: number;
    /** Optional bbox [x, y, w, h] normalised [0..1]. Rare on native events. */
    readonly bbox?: readonly [number, number, number, number];
}
export interface MotionAnalysisPayload {
    detected: boolean;
    regionCount: number;
    regions: readonly MotionRegion[];
    frameWidth: number;
    frameHeight: number;
    analysisMs: number;
    [key: string]: unknown;
}
/**
 * Motion phase transition payload. Single source of truth lives in
 * `motion.cap.ts` as `MotionOnMotionChangedDataSchema` — this alias
 * just re-exports the inferred type with the `[key: string]: unknown`
 * loose-index pattern the bus uses for forward-compat. Don't add
 * fields here; extend the schema in motion.cap.ts.
 */
export type MotionOnMotionChangedPayload = import('../capabilities/motion.cap.js').MotionOnMotionChangedData & {
    [key: string]: unknown;
};
/** Raw motion zone from CCL (before minArea filter). */
export interface MotionRawZone {
    readonly bbox: readonly [number, number, number, number];
    readonly pixelCount: number;
    readonly changeScore: number;
}
/** All raw motion zones — emitted for UI debug overlay. */
export interface MotionZonesRawPayload {
    readonly deviceId: number;
    readonly zones: readonly MotionRawZone[];
    readonly frameSize: {
        readonly width: number;
        readonly height: number;
    };
    readonly timestamp: number;
    [key: string]: unknown;
}
export interface DetectionResultPayload {
    /**
     * The post-processed frame result the frontend consumes. Already
     * contains `width`/`height` + `detections[]` + optional debug. No
     * pipeline-raw wrapper — the new `FrameResult` shape IS the payload.
     */
    frame: FrameResult;
    /** Events emitted by the analysis pipeline (tracking, scene state, face rec). */
    analysisResults: readonly unknown[];
    [key: string]: unknown;
}
/**
 * Raw inference output emitted by `addon-pipeline-runner` per detection
 * frame. Carries the FrameResult produced by the runner — the hub-side
 * wiring service subscribes, applies the analysis pipeline +
 * notifications, and re-emits as `detection.result`.
 */
export interface PipelineInferenceResultPayload {
    readonly deviceId: number;
    readonly frame: FrameResult;
    /** The Moleculer node id of the runner that produced this result. */
    readonly nodeId: string;
    readonly [key: string]: unknown;
}
/**
 * Live per-audio-chunk output emitted by the pipeline-orchestrator for
 * each camera with audio analysis enabled. Mirrors `PipelineInferenceResultPayload`
 * but for audio: carries the canonical `AudioResult` produced by running
 * the audio-analyzer + audio-classifier inline on the hub.
 *
 * Replaces the legacy split `detection.audio.level` /
 * `detection.audio.classification` events — consumers should read
 * `frame.level` for dBFS/RMS and `frame.detections` for classifier
 * matches (both on the same single event).
 */
export interface PipelineAudioInferenceResultPayload {
    readonly deviceId: number;
    readonly frame: AudioResult;
    /** Node id of the host that ran the analyzer — always `'hub'` today
     *  because audio runs inline on the hub, but the field is kept so the
     *  event shape matches `PipelineInferenceResultPayload`. */
    readonly nodeId: string;
    readonly [key: string]: unknown;
}
/**
 * Camera assignment lifecycle payload emitted by `addon-pipeline-orchestrator`
 * when a camera is assigned to or unassigned from an agent runner. Used by
 * the UI / metrics dashboard. No frame data.
 */
export interface PipelineCameraAssignmentPayload {
    readonly deviceId: number;
    readonly agentNodeId: string;
    /** True when the assignment was set manually (preferredAgent setting), false when chosen by the load balancer. */
    readonly pinned: boolean;
    readonly reason: 'manual' | 'capacity' | 'hardware-affinity' | 'failover' | 'rebalance' | 'unassigned';
    readonly [key: string]: unknown;
}
/**
 * Capability binding change payload emitted whenever an operator picks
 * a different addon to implement a capability on a node. Each kernel
 * process subscribes to this event and updates its local
 * `preferredProviderRegistry` so subsequent capability lookups return
 * the newly chosen provider.
 *
 * The event lives under `capability.binding-changed` because binding
 * is a kernel-level concept used by any cap provider (not just the
 * pipeline flow). It was renamed from `pipeline.binding-changed` to
 * reflect that broader scope.
 */
export interface CapabilityBindingChangedPayload {
    readonly nodeId: string;
    readonly capName: string;
    /** New preferred addon id, or null when the binding was cleared. */
    readonly addonId: string | null;
    readonly [key: string]: unknown;
}
export interface PhaseTransitionPayload {
    deviceId: number;
    from: 'watching' | 'active';
    to: 'watching' | 'active';
    reason: 'motion_detected' | 'cooldown_expired';
    /** Motion source that armed the cooldown — present on both ON and OFF. */
    source?: 'onboard' | 'analyzer';
    /** Cooldown window in ms that was/is active for this transition. */
    cooldownMs?: number;
    /** Ms epoch of the transition. */
    timestamp?: number;
    [key: string]: unknown;
}
/**
 * Scope of a readiness transition — the "who does this ready/down apply to".
 *
 * Two shapes today:
 *  - `global` for singletons that are cluster-wide (there is only one hub).
 *  - `node` for per-process providers (one readiness per broker nodeID).
 *
 * `device` scope covers per-device native caps registered via
 * `DeviceContext.registerNativeCap(cap, provider)`. Consumers of cross-
 * process per-device caps (stream-broker, orchestrator, snapshot, webrtc)
 * `awaitReady(capName, {type:'device', deviceId})` before calling the
 * native-cap bridge so they don't race the service-advertise window.
 * On worker disconnect the hub-side registry's `agent.offline` demux
 * synthesises `down` for every device-scoped cap whose `sourceNodeId`
 * matches the offline node — same pattern as node-scoped synthesis.
 */
export type ReadinessScope = {
    readonly type: 'global';
} | {
    readonly type: 'node';
    readonly nodeId: string;
} | {
    readonly type: 'device';
    readonly deviceId: number;
};
/** States a capability provider transitions through. */
export type ReadinessState = 'starting' | 'ready' | 'down';
/**
 * Payload for `system.ready-state` events.
 *
 * `generation` is a per-process random identifier that stays constant
 * for the producer's lifetime and changes whenever the producer process
 * is restarted. The kernel's local `ReadinessRegistry` derives a
 * monotonic `epoch` per `(capName, scope)` by counting generation
 * transitions — emitters never stamp epoch themselves. Consumers that
 * hold long-lived state compare `transition.epoch > lastSeenEpoch` to
 * decide whether to rebuild.
 *
 * Same-generation repeated `ready` events are idempotent keepalives
 * (e.g. re-emitted on every `$node.connected`) — the registry keeps
 * epoch stable.
 */
export interface SystemReadyStatePayload {
    readonly capName: string;
    readonly scope: ReadinessScope;
    readonly state: ReadinessState;
    /** Per-process random id; changes on producer restart. */
    readonly generation: string;
    readonly sourceNodeId: string;
    readonly ts: number;
    readonly [key: string]: unknown;
}
/** One service entry inside a topology process — addon + capabilities. */
export interface TopologyService {
    readonly addonId: string;
    readonly capabilities: readonly string[];
    readonly status: string;
}
/** One process under a topology node (main process + isolated workers). */
export interface TopologyProcess {
    readonly pid: number;
    readonly name: string;
    readonly state: string;
    readonly cpuPercent: number;
    readonly memoryRss: number;
    readonly uptimeSeconds: number;
    readonly services: readonly TopologyService[];
    readonly groupId?: string;
}
/** Top-level cluster node — hub or remote agent. */
export interface TopologyNode {
    readonly id: string;
    readonly name: string;
    readonly hostname: string;
    readonly platform: string;
    readonly arch: string;
    readonly cpuModel: string | null;
    readonly cpuCores: number;
    readonly memoryMB: number;
    readonly engines: readonly string[];
    readonly isHub: boolean;
    readonly isOnline: boolean;
    readonly cpuPercent: number;
    readonly memoryPercent: number;
    readonly uptime: number;
    readonly lastSeen: string;
    /** Local IP addresses visible on the network (non-internal, IPv4/IPv6). */
    readonly localIps: readonly string[];
    readonly addons: ReadonlyArray<{
        readonly id: string;
        readonly capabilities: readonly string[];
        readonly status: string;
    }>;
    readonly processes: readonly TopologyProcess[];
}
/** All known event categories with their typed payloads */
export interface EventCatalog {
    'system.boot': {
        mode: string;
    };
    'system.addons-ready': {
        activeAddons: readonly string[];
    };
    'system.restarting': Record<string, never>;
    'system.ready-state': SystemReadyStatePayload;
    'addon.started': {
        addonId: string;
        packageName?: string;
        packageVersion?: string;
        agent?: string;
    };
    'addon.stopped': {
        addonId: string;
        packageName?: string;
        packageVersion?: string;
        agent?: string;
    };
    'addon.restarted': {
        addonId: string;
        packageName?: string;
        packageVersion?: string;
        agent?: string;
    };
    'addon.updated': {
        addonId: string;
        packageName?: string;
        packageVersion?: string;
        agent?: string;
        fromVersion?: string;
        toVersion?: string;
    };
    'addon.installed': {
        addonId: string;
        packageName?: string;
        packageVersion?: string;
        agent?: string;
    };
    'addon.uninstalled': {
        addonId: string;
        packageName?: string;
        packageVersion?: string;
        agent?: string;
    };
    'addon.crashed': {
        addonId: string;
        packageName?: string;
        packageVersion?: string;
        agent?: string;
        error?: string;
    };
    'addon.error': {
        addonId: string;
        packageName?: string;
        packageVersion?: string;
        agent?: string;
        error?: string;
        action?: string;
        phase?: string;
    };
    'device.registered': {
        deviceId: number;
        providerId: string;
        name?: string;
    };
    'device.unregistered': {
        deviceId: number;
        providerId: string;
    };
    'device.streams-registered': {
        deviceId: number;
        providerId: string;
    };
    'device.streams-unregistered': {
        deviceId: number;
    };
    'device.enabled': {
        deviceId: number;
        integrationId: string;
    };
    'device.disabled': {
        deviceId: number;
        integrationId: string;
    };
    'device.settings-updated': {
        deviceId: number;
        integrationId: string;
        keys: string[];
    };
    /**
     * Device online/offline aggregate. Emitted by the device's owning
     * provider (rtsp / reolink / onvif / frigate) when the union of its
     * stream-broker profile health flips. `online` fires when at least
     * one profile becomes healthy after all were offline; `offline` fires
     * when the last healthy profile goes stale (no packets for
     * STREAM_STALE_TIMEOUT_MS).
     */
    'device.online': {
        deviceId: number;
        providerId: string;
        profileCount?: number;
        reason?: string;
    };
    'device.offline': {
        deviceId: number;
        providerId: string;
        reason?: string;
    };
    'device.awake': {
        deviceId: number;
        providerId: string;
        reason?: string;
    };
    'device.sleeping': {
        deviceId: number;
        providerId: string;
        reason?: string;
    };
    /**
     * Emitted whenever the set of (device, capability) native providers or
     * wrapper activations changes. Hub subscribers use this to keep a
     * cross-process view of which addon/node currently serves each cap for
     * each device. `nodeId` is the broker nodeID where the provider lives;
     * 'hub' for hub-local. `addonId` is the provider (native or wrapper).
     */
    'device.bindings-changed': {
        deviceId: number;
        capName: string;
        reason: 'native-registered' | 'native-unregistered' | 'wrapper-activated' | 'wrapper-deactivated';
        addonId: string;
        nodeId: string;
    };
    'device.meta-changed': {
        deviceId: number;
        field: 'name' | 'location' | 'disabled';
        value: string | null | boolean;
    };
    'integration.enabled': {
        integrationId: string;
        addonId: string;
    };
    'integration.disabled': {
        integrationId: string;
        addonId: string;
    };
    'provider.started': {
        providerId: string;
        type?: string;
    };
    'provider.stopped': {
        providerId: string;
        reason?: string;
    };
    'process.crashed': {
        processId: string;
        exitCode?: number;
        signal?: string;
    };
    'process.restart_scheduled': {
        processId: string;
        delayMs: number;
    };
    'process.restarted': {
        processId: string;
    };
    'recording.started': {
        deviceId: number;
        streamId?: string;
    };
    'recording.stopped': {
        deviceId: number;
        reason?: string;
    };
    'recording.error': {
        deviceId: number;
        error: string;
    };
    'recording.health.degraded': {
        deviceId: number;
        message: string;
    };
    'recording.storage.critical': {
        deviceId: number;
        usagePercent?: number;
    };
    'recording.segment.written': {
        deviceId: number;
        path?: string;
        durationSec?: number;
        sizeMB?: number;
    };
    'recording.policy.fallback': {
        deviceId: number;
        reason: string;
    };
    'recording.retention.completed': {
        deletedCount?: number;
        freedMB?: number;
    };
    'detection.event': {
        deviceId: number;
        detections?: unknown[];
        [key: string]: unknown;
    };
    'detection.result': DetectionResultPayload;
    'detection.motion-analysis': MotionAnalysisPayload;
    'detection.motion-zones-raw': MotionZonesRawPayload;
    'motion.on-motion-changed': MotionOnMotionChangedPayload;
    /**
     * On-board detection coming straight from the camera firmware (Reolink
     * AI alarms, ONVIF analytics, etc.) — as opposed to `detection.result`
     * which is produced by the pipeline running locally.
     *
     * `source` discriminates the origin when multiple pipelines coexist for
     * the same camera: `'onboard'` for native firmware events, `'camera-native'`
     * kept as an alias for backwards compatibility with pre-Reolink emitters.
     *
     * `detections` is tightened from `unknown[]` to a structured shape. Each
     * entry describes one firmware-reported class ('motion', 'person',
     * 'vehicle', 'animal', 'face', 'package', 'other', …) at a point in time.
     */
    'detection.camera-native': {
        cameraId: number;
        detections: readonly CameraNativeDetection[];
        source: 'onboard' | 'camera-native';
    };
    'pipeline.audio-inference-result': PipelineAudioInferenceResultPayload;
    'detection.phase-transition': PhaseTransitionPayload;
    /**
     * Emitted whenever a per-device override is written or cleared for any
     * pipeline-participating addon (pipeline-orchestrator, motion-wasm,
     * detection-pipeline, audio-analyzer, audio-classifier). The wiring
     * service subscribes to this event and restarts detection for the
     * affected camera so the new values take effect without a full reboot.
     * `addonId` is informational — most consumers only care about
     * `deviceId` and will rebuild the full RunnerCameraConfig anyway.
     */
    'orchestration.settings-updated': {
        deviceId: number;
        addonId?: string;
    };
    'provider.motion': {
        active: boolean;
    };
    'provider.detection': {
        detections: unknown;
    };
    'session.track.new': {
        deviceId: number;
        trackId: string;
        label?: string;
    };
    'session.track.expired': {
        deviceId: number;
        trackId: string;
        durationMs?: number;
    };
    /**
     * Progress from a running benchmark (pipeline image test, decoder perf test,
     * …). The payload is intentionally open (`kind` discriminator + free-form
     * additional fields) so different benchmark shapes share the same bus
     * category and a single UI subscriber can filter by `kind` + `sessionId`.
     */
    'benchmark.progress': {
        kind: string;
        sessionId: string;
        phase?: string;
        tSec?: number;
        sample?: Record<string, unknown>;
        message?: string;
    };
    'pipeline.progress': {
        /** Node that executed the pipeline step. Same value is also set on `event.source.nodeId`
         * but carried in the payload so admin-ui filters don't need to peek at the source shape. */
        nodeId: string;
        /** Stable identifier per `runPipeline()` invocation — every progress event from the same
         * run shares this id so the UI can correlate a burst of messages into a single run card. */
        sessionId: string;
        step: string;
        addonId?: string;
        modelId?: string;
        ms?: number;
        message?: string;
    };
    /** Per-frame execution trace emitted by the pipeline executor for live observability. */
    'pipeline.trace': PipelineExecutionTrace;
    /** Raw inference output emitted by addon-pipeline-runner (no frame buffer). */
    'pipeline.inference-result': PipelineInferenceResultPayload;
    /** Camera assigned to an agent runner by addon-pipeline-orchestrator. */
    'pipeline.camera-assigned': PipelineCameraAssignmentPayload;
    /** Camera released from an agent runner. */
    'pipeline.camera-unassigned': PipelineCameraAssignmentPayload;
    /** Per-camera pipeline config changed — orchestrator hot-reloads the assigned runner. */
    'pipeline.camera-updated': {
        readonly deviceId: number;
        readonly config: CameraPipelineConfig;
    };
    /**
     * Periodic per-runner load snapshot (~1 Hz). Replaces UI polling on
     * `pipelineRunner.getLocalLoad`. UI subscribes per agent node.
     */
    'pipeline.runner-load-snapshot': {
        readonly nodeId: string;
        readonly load: RunnerLocalLoad;
        /** Wall-clock ms when the snapshot was sampled. */
        readonly timestamp: number;
    };
    /**
     * Periodic per-camera metrics snapshot (~1 Hz). Replaces UI polling
     * on `pipelineOrchestrator.getCameraMetrics` /
     * `pipelineRunner.getCameraMetrics`. One event per attached camera
     * per tick. UI consumers filter by `deviceId`.
     */
    'pipeline.camera-metrics-snapshot': {
        readonly deviceId: number;
        readonly nodeId: string;
        readonly metrics: CameraMetrics;
        readonly timestamp: number;
    };
    /**
     * Periodic per-broker stats snapshot (~1 Hz). Replaces polling on
     * `streamBroker.getBrokerStats` and `streamBroker.listAllProfileSlots`.
     * Carries the full `BrokerStats` payload so UI consumers can render
     * the panel directly from the event without round-tripping the cap.
     */
    'stream-broker.metrics-snapshot': {
        readonly brokerId: string;
        readonly deviceId: number;
        readonly profile: string;
        readonly nodeId: string;
        readonly stats: BrokerStats;
        readonly timestamp: number;
    };
    /**
     * Cap event: stream-broker signals that a profile slot now has at
     * least one consumer. Camera-provider addons (Reolink Baichuan push,
     * etc.) start their underlying transport on receipt — see
     * `EventCategory.StreamBrokerOnCamStreamDemand`.
     */
    'stream-broker.onCamStreamDemand': {
        readonly deviceId: number;
        readonly camStreamId: string;
        readonly profile: 'high' | 'mid' | 'low';
    };
    /**
     * Cap event: stream-broker signals the last consumer left. Providers
     * tear down their transport — see `EventCategory.StreamBrokerOnCamStreamIdle`.
     */
    'stream-broker.onCamStreamIdle': {
        readonly deviceId: number;
        readonly camStreamId: string;
    };
    /**
     * Cap event: device's battery status changed (firmware push or
     * provider-side observation). Mirrors `batteryCapability.onStatusChanged`.
     * Payload shape duplicated inline (rather than importing from
     * `capabilities/battery.cap`) to avoid a cycle between the cap
     * definitions and the event bus typing — they're kept in lock-step
     * by hand (see also `BatteryStatus` in `capabilities/battery.cap.ts`).
     */
    'battery.onStatusChanged': {
        readonly deviceId: number;
        readonly status: {
            readonly percentage: number;
            readonly charging: 'dc' | 'solar' | 'none';
            readonly sleeping: boolean;
            readonly lastUpdated: number;
        };
    };
    /**
     * Doorbell button press — emitted by every device that registers the
     * `doorbell` capability. Mirrors `doorbellCapability.events.onPressed`.
     * Subscribers (UI toast, advanced-notifier) react to physical rings
     * without holding a cap reference.
     */
    'doorbell.onPressed': {
        readonly deviceId: number;
        readonly timestamp: number;
    };
    /**
     * Periodic per-node engine inventory snapshot (~0.2 Hz). Replaces
     * polling on `pipelineExecutor.listLoadedEngines`. One event per
     * detection-pipeline process per tick, carrying every loaded engine
     * with the same shape the cap returns.
     */
    'pipeline.engine-metrics-snapshot': {
        readonly nodeId: string;
        readonly engines: ReadonlyArray<{
            readonly engineKey: string;
            readonly engine: PipelineEngineChoice;
            readonly modelsLoaded: readonly string[];
            readonly inUseByCameras: readonly number[];
            readonly kind: 'runtime' | 'warm-override';
            readonly poolPid: number | null;
            readonly idleMs: number | null;
            readonly idleTtlMs: number | null;
        }>;
        readonly timestamp: number;
    };
    /**
     * Cluster topology snapshot — same payload shape that
     * `nodes.topology` returns. Emitted by the hub on any agent /
     * addon lifecycle change (debounced ~200ms) plus a periodic safety
     * net. UI dashboards subscribe to drive their cluster view
     * directly from the event payload — zero round-trip.
     */
    'cluster.topology-snapshot': {
        readonly nodes: ReadonlyArray<TopologyNode>;
        readonly timestamp: number;
    };
    /**
     * Periodic per-node system metrics snapshot. Carries the full
     * `SystemResourceSnapshot` returned by
     * `metricsProvider.getCached()` so dashboards render CPU /
     * memory / GPU / disk / network without polling.
     */
    'metrics.node-resources-snapshot': {
        readonly nodeId: string;
        readonly snapshot: SystemResourceSnapshot;
        readonly timestamp: number;
    };
    /**
     * Periodic per-node process-tree snapshot (`NodeProcess[]` —
     * camstack-related pids with ghost / managed / root classification).
     * One event per node per tick. Replaces polling on
     * `metricsProvider.listNodeProcesses`.
     */
    'metrics.node-processes-snapshot': {
        readonly nodeId: string;
        readonly processes: ReadonlyArray<NodeProcess>;
        readonly timestamp: number;
    };
    /**
     * Per-agent hwaccel override changed (set / cleared / re-probed). Payload
     * carries the new effective preference so UI consumers can toast or
     * refresh their pipeline view without re-fetching. Decoders still pull
     * from `pipeline-orchestrator.getAgentSettings` on session creation;
     * this event is additive observability.
     */
    'pipeline.agent-hwaccel-changed': {
        readonly agentNodeId: string;
        readonly userChoice: 'auto' | 'none' | string | null;
        readonly probedBest: string;
        readonly reason: 'user-set' | 'user-cleared' | 'reprobed' | 'seeded';
    };
    /** Capability binding changed for a node — kernels update their preferred-provider registry. */
    'capability.binding-changed': CapabilityBindingChangedPayload;
    'model.download.progress': {
        modelId: string;
        progress: number;
        totalMB?: number;
    };
    'agent.online': {
        agentId: string;
    };
    'agent.offline': {
        agentId: string;
    };
    'worker.online': {
        workerId: string;
    };
    'worker.offline': {
        workerId: string;
    };
    'agent.task.assigned': {
        agentId: string;
        taskId: string;
        taskType: string;
        payload?: unknown;
    };
    'enrichment.embedding.stored': {
        readonly deviceId: number;
        readonly trackId: string;
        readonly class: string;
        readonly embeddingId: string;
        readonly modelId: string;
        readonly embeddingDim: number;
        readonly inferenceMs: number;
        readonly timestamp: number;
    };
    'enrichment.scene.state-changed': {
        readonly deviceId: number;
        readonly monitorId: string;
        readonly monitorLabel: string;
        readonly previousState: string;
        readonly currentState: string;
        readonly confidence: number;
        readonly timestamp: number;
    };
    'enrichment.activity.summary': {
        readonly deviceId: number;
        readonly periodStart: number;
        readonly periodEnd: number;
        readonly objectCounts: Readonly<Record<string, number>>;
        readonly zoneActivity: readonly {
            readonly zoneId: string;
            readonly entries: number;
            readonly exits: number;
            readonly avgDwellMs: number;
        }[];
        readonly stateChanges: readonly {
            readonly monitorId: string;
            readonly from: string;
            readonly to: string;
            readonly timestamp: number;
        }[];
        readonly activityLevel: 'none' | 'low' | 'medium' | 'high';
    };
    'alert.created': {
        id: string;
        category: string;
        severity: string;
        title: string;
        status: string;
    };
    'alert.updated': {
        alertId: string;
        patch: Record<string, unknown>;
    };
    /**
     * Per-broker stream health transition. Fired by the stream-broker
     * watchdog. `camStreamId` is the canonical identity of the source
     * (every broker is keyed by its cam stream now); `profile` is the
     * profile slot currently bound to it (or `null` if the broker is
     * only kept alive by a manual activation).
     */
    'stream.online': {
        readonly deviceId: number;
        readonly camStreamId: string;
        readonly profile: 'high' | 'mid' | 'low' | null;
        readonly brokerId: string;
        readonly sourceType: string;
        readonly lastPacketAt: number;
        readonly reason?: string;
    };
    'stream.offline': {
        readonly deviceId: number;
        readonly camStreamId: string;
        readonly profile: 'high' | 'mid' | 'low' | null;
        readonly brokerId: string;
        readonly sourceType: string;
        readonly lastPacketAt: number;
        readonly reason?: string;
    };
    'retention.cleanup': {
        readonly deletedEvents?: number;
        readonly deletedAudioRecords?: number;
        readonly deletedSnapshots?: number;
        readonly deletedCount?: number;
        readonly freedMB?: number;
    };
}
/** All known event category strings (derived from EventCatalog keys) */
export type KnownEventCategory = keyof EventCatalog;
/**
 * System event — `data` is `Record<string, unknown>` for backward compatibility.
 * Use `TypedSystemEvent<C>` or `narrowEvent()` for typed access to data.
 */
export interface SystemEvent {
    id: string;
    timestamp: Date;
    source: EventSource;
    category: string;
    data: Record<string, unknown>;
    /**
     * Propagation chain when the event was re-emitted by the
     * `DeviceEventPropagator`. `via[0]` is the originating source (the
     * child that produced the event); subsequent entries walk up the
     * parent chain. Absent on the original emission and on events that
     * don't belong to a device-rooted source.
     *
     * Consumers that want ONLY direct events filter `ev.via === undefined`.
     * Consumers that want everything reaching a device (incl. children)
     * filter by `source.id === parentId` and accept any `via`.
     */
    via?: readonly EventSource[];
}
/**
 * A system event with a known category — `data` is typed.
 * Use this when you know the exact category at compile time.
 */
export type TypedSystemEvent<C extends KnownEventCategory> = {
    id: string;
    timestamp: Date;
    source: EventSource;
    category: C;
    data: EventCatalog[C];
    via?: readonly EventSource[];
};
/** A category filter value: a known category, a glob string, or an array of either */
export type EventCategoryFilter = KnownEventCategory | (string & {}) | readonly (KnownEventCategory | (string & {}))[];
export interface EventFilter {
    source?: EventSource;
    /** Filter by top-level agent (e.g. 'hub', 'agent-a1b2c3'). Matches source.nodeId with prefix match so 'hub' includes 'hub/pipeline'. Same semantics as logs tags.agentId. */
    agentId?: string;
    /** Filter by addon that originated the event. Matches source.addonId or source.id when type='addon'. */
    addonId?: string;
    /** Filter by device the event relates to. Matches source.deviceId or source.id when type='device'. */
    deviceId?: number;
    /** Category — known category (typed), glob (e.g. 'device.*'), or array */
    category?: EventCategoryFilter;
    since?: Date;
}
export interface TypedEventFilter<C extends KnownEventCategory> {
    source?: EventSource;
    category: C | readonly C[];
    since?: Date;
}
export interface IEventBus {
    /** Emit a known-category event with typed payload */
    emit<C extends KnownEventCategory>(event: TypedSystemEvent<C>): void;
    /** Emit an arbitrary event (custom/unknown category) */
    emit(event: SystemEvent): void;
    /** Subscribe to a known category — handler receives typed event */
    subscribe<C extends KnownEventCategory>(filter: TypedEventFilter<C>, handler: (event: TypedSystemEvent<C>) => void): () => void;
    /** Subscribe with a generic filter — handler receives base SystemEvent */
    subscribe(filter: EventFilter, handler: (event: SystemEvent) => void): () => void;
    /** Get recent events */
    getRecent(filter?: EventFilter, limit?: number): readonly SystemEvent[];
}
/**
 * Narrow a SystemEvent to a typed event by checking its category.
 * Returns `true` (and narrows the type) if the category matches.
 *
 * @example
 * ```typescript
 * eventBus.subscribe({ category: 'addon.started' }, (event) => {
 *   if (isEvent(event, 'addon.started')) {
 *     event.data.addonId  // ✓ typed as string
 *   }
 * })
 * ```
 */
export declare function isEvent<C extends KnownEventCategory>(event: SystemEvent, category: C): event is TypedSystemEvent<C>;
/**
 * Create a typed event with less boilerplate.
 *
 * @example
 * ```typescript
 * eventBus.emit(createEvent('addon.started', { type: 'addon', id: 'pipeline' }, {
 *   addonId: 'pipeline',
 *   packageVersion: '0.1.8',
 * }))
 * ```
 */
export declare function createEvent<C extends KnownEventCategory>(category: C, source: EventSource, data: EventCatalog[C]): TypedSystemEvent<C>;
/**
 * Emit a `system.ready-state` event for a capability. Caller supplies a
 * per-process `generation` string — stable across a single process
 * lifetime, changes on restart — which consumer-side registries use to
 * derive a monotonic `epoch` without requiring the emitter to
 * coordinate.
 */
export declare function emitReadiness(bus: IEventBus, params: {
    readonly capName: string;
    readonly scope: ReadinessScope;
    readonly state: ReadinessState;
    readonly generation: string;
    readonly sourceNodeId: string;
    readonly ts?: number;
}): void;
//# sourceMappingURL=event-bus.d.ts.map