4d00f7ad09
The 5-lane review flagged that the notifications daemon had five missing lifecycle behaviors. This commit implements all of them: 1. Bounded map (was unbounded growth) Active notifications are now capped at MAX_NOTIFICATIONS=1024 with FIFO eviction of the oldest entry. sender_to_ids index is also pruned on eviction. 2. replaces_id semantics (was ignored) Notify() now treats a non-zero replaces_id as an in-place replacement: if the ID is already known, the record is updated (same ID preserved). Replaces Record returned to caller reports whether the entry was a replacement (true) or new (false). Per freedesktop spec, the same notification ID survives. 3. Expire-timeout sweep (was ignored) NotificationRecord now carries expires_at: Option<Instant>, set from expire_timeout. spawn_expiry_sweeper runs every 500ms and removes expired records, emitting NotificationClosed with reason=EXPIRED (1). expire_due_notifications() returns the IDs for the background sweep. 4. Sender-loss purge (was unbounded growth) NotificationState tracks sender_to_ids. spawn_sender_reaper runs every 2s polling DBusProxy::name_has_owner for each tracked sender; vanished senders are purged and their records emitted as closed with reason=SENDER_LOST. Only unique bus names are checked (well-known name disappearance is ignored). 5. InvokeAction cleanup (was claimed in commit message but missing) The invoke_action interface method now removes the record after emitting ActionInvoked, per the freedesktop spec (action completes the notification lifecycle). State model refactored: Notifications now wraps an Arc<NotificationState> containing the maps/queues, so background tasks share state with the D-Bus interface without cloning the full Notifications struct. Tests: 16/16 pass. New tests: - expiry_removes_record_and_emits_closed_reason - replaces_id_reuses_existing_record_and_logs_replacement - vanished_sender_records_are_purged - notifications_are_bounded_at_1024_entries - invoke_action_removes_record