MonitorEvent
Sealed event stream emitted by the library for instrumentation / inspection.
Every internal scheduling, dispatch, deferral, completion, and library-level error path produces one or more MonitorEvents. Consumers observe the stream through BackgroundTaskManager.events as a SharedFlow<MonitorEvent> (Swift sees it as AsyncSequence<MonitorEvent> via SKIE), or — for the imperative callback style — implement BackgrounderEventListener. Both delivery mechanisms are fed from the same emit point and receive the same events.
Delivery guarantees.
Synchronous to the producer. BackgrounderEventListener callbacks run inline on the dispatcher thread. Implementations must not block.
tryEmiton the flow. Emit into the shared flow is non-suspending — a slow collector cannot pin scheduler dispatch (CLAUDE.md §3). The backing flow usesreplay = 0,extraBufferCapacity = 64,BufferOverflow.DROP_OLDEST. Late-attached collectors do not see historical events; long stalls drop the oldest unread events first.Best-effort ordering per
taskId. Within one task id the natural order is preserved (scheduled → started → completed → cancelled). Cross-task ordering follows the producer's interleave.
Swift bridging. SKIE renders this sealed interface as a Swift enum usable with onEnum(of:) for exhaustive switch. The payload sealed types (CancelSource, DeferralReason, SkipReason, AttemptFailureReason) are intentionally top-level — nested sealed types inside a sealed parent bridge unreliably (LESSONS.md D-004).
Inheritors
Types
The platform fired the worker but the library deferred execution because a predicate was not met. Followed (after the library converts to retry) by a WorkCompleted with WorkResult.Retry. See DeferralReason for the discriminator.
An attempt failed in a way that is not the worker returning WorkResult.Failure: the OS expired the task, the factory threw, the worker threw an uncaught exception. The library converts these to WorkResult.Retry (or WorkResult.Failure on max-attempts exhaustion) internally; this event surfaces the original cause.
The task was removed. source discriminates user-initiated cancellation from library-internal teardown (shutdown) and replacement-triggered cancellation.
Library-internal error that does not fit the per-attempt frame — for example BGTaskScheduler.submit rejecting a request, or a platform-level callback throwing. The library has already handled the error (logged, possibly rejected the schedule); this event surfaces what would otherwise be Kermit-only.
The library scheduled a retry for nextAttempt after a failed attempt. Emitted just before resubmitting to the platform.
A new request was accepted by the scheduler. Fires for both fresh registrations and Replace-policy overwrites; for Replace, ScheduleReplaced is also emitted with the previous request.
A pre-existing scheduled request for this id was displaced by a new one. Emitted alongside (and immediately before) a fresh Scheduled. Not emitted when policy is ConflictPolicy.Keep and the existing request wins the race — that path is silent (the user-supplied request had no observable effect).
The library would have run the worker but skipped entirely — for structural reasons that prevent any future attempt from succeeding (no factory, declined factory, ephemeral wash). No retry follows.
The worker's BackgroundWorker.execute returned (regardless of WorkResult). runtime is wall-clock duration measured from the paired WorkStarted.
The platform fired the worker and execution is about to begin.