--- status: shipped created: 2026-07-24 owner: kandev --- # Semantic Notifications ## Why Kandev currently treats every transition to a session's generic `WAITING_FOR_INPUT` state as though the agent asked the user a question. That state is also used before the first turn and after an ordinary completed turn, so notifications can claim that input is required when it is not. The same event is deduplicated once per session, which can let an early false positive suppress a later real clarification request. The notification settings page also snapshots an empty provider store during a cold load. Providers fetched immediately afterward remain absent from the rendered draft, making an intact configuration look lost. Update detection has the same need for durable, user-controlled delivery. Users should learn that a newer Kandev release exists without maintaining a second preference model that bypasses Local, System, Apprise, or future notification providers. A denied or unavailable desktop notification must not hide the update from an open Kandev window. ## What - Notification settings expose two independent session events: - **Agent turn finished** — notify after each completed agent turn. - **Agent needs an answer** — notify when the agent explicitly requests a user response through Kandev's clarification flow. - A user may enable either event, both events, or neither event for each notification provider. - **Kandev update available** is an independently selectable provider event. It is emitted when the release poller durably observes a version newer than the running Kandev version. - When a provider-create request omits its event selection, the clarification event is enabled by default and turn-finished remains opt-in. An explicitly empty selection enables neither event. - Existing `session.waiting_for_input` subscriptions migrate to **Agent needs an answer** only. Migration never opts a provider into **Agent turn finished**. - Fresh default Local and System providers subscribe to update availability. On upgrade, existing Local and System providers receive that subscription once; a migration marker prevents later startup from re-enabling a choice the user removed. Existing Apprise providers are never opted in automatically. - Session startup, readiness, idle state, and generic `WAITING_FOR_INPUT` transitions do not themselves send either notification. - A turn-finished notification uses: - title: `Agent turn finished` - body: `The agent finished a turn on "".` - A clarification notification uses: - title: `Agent needs your answer` - body: `The agent asked a question on "".` - The local browser and native desktop notification path uses the same semantic event and copy as remote providers such as Apprise. - An update-available notification uses: - title: `Kandev update available` - body: `Kandev is available. Open Settings > System > Updates to review it.` - The Local provider keeps an in-app update indication visible even when browser/native permission is denied, unsupported, or delivery fails. Native permission is requested only from the existing user-initiated control on the Notifications settings page. - Native session-failure alerts remain an independent safety channel whenever the Local provider is enabled; they are not controlled by either semantic event checkbox. - On a cold settings-page load, the provider list and saved event selections appear after the provider request completes. Initial hydration must not overwrite edits the user has already made. ## Data Model The durable provider subscription keys are: - `session.turn_finished` - `session.clarification_requested` - `system.update_available` Each delivery records a semantic occurrence ID and, for session-scoped events, the task-session ID: - turn-finished occurrence ID: the completed turn ID; - clarification occurrence ID: the clarification request's pending/request ID. - update-available occurrence ID: the normalized release version/tag. Delivery idempotency is scoped to provider, event type, and occurrence ID. Consequently, replaying one occurrence does not duplicate a delivery, while separate turns or clarification requests in the same session may each notify. `session.waiting_for_input` is a legacy subscription key accepted only by the data migration. It is not offered by the settings API after migration and is not emitted as a new notification event. ## API Surface - The notification-provider API returns the two semantic events in its available-event catalog together with `system.update_available`. - Provider create and update requests persist the selected semantic event keys. - An omitted create-time event field applies the clarification-only default; an explicitly empty event array persists no subscriptions. - The WebSocket gateway may emit `session.turn_finished` and `session.clarification_requested` for the local notification provider, plus `system.update_available` with `{ version, url, title, body, occurrence_id }`. - Local semantic notifications carry the occurrence ID through the WebSocket envelope/payload so native de-duplication uses the same identity as backend delivery de-duplication. - Existing provider URLs, enabled state, and unrelated event subscriptions are unchanged. - Update-notification policy is not exposed through a separate settings route or channel selector. Provider enabled state and event subscriptions are the authoritative configuration. ## State and Event Semantics | Domain occurrence | Turn finished | Needs an answer | |---|---:|---:| | Session starts or becomes ready | No | No | | Session enters generic `WAITING_FOR_INPUT` | No | No | | An agent turn is durably completed | Yes, when selected | No | | An agent-authored structured clarification request is created | No | Yes, when selected | | The user answers a clarification | No | No | | The same domain event is replayed | No duplicate | No duplicate | | A later occurrence happens in the same session | Yes | Yes | A clarification bundle containing several questions is one occurrence and produces at most one notification per provider. User-authored, system-authored, malformed, or unscoped clarification-shaped messages do not notify. An update is one install-wide occurrence per release version. Replaying cached release state after a Local client subscribes retries only providers whose delivery was not already recorded; Apprise, System, and other successful providers remain deduplicated. ## Permissions Notification configuration continues to use the existing settings authorization rules. This change grants no additional access to task content. Notification bodies contain the task title but not the question text. ## Failure Modes - A provider send failure releases that occurrence's de-duplication claim so a replay may retry it. It does not mark a later occurrence as delivered. - If the startup release poll completes before a Local WebSocket subscriber exists, Local delivery remains pending. The cached release is replayed when an eligible user client subscribes, without refetching GitHub. - If native/browser delivery is denied, unsupported, or fails, the open client retains the in-app update indication. Background delivery never opens a permission prompt. - Replayed event-bus messages do not create duplicate deliveries for the same provider and occurrence. - A provider refresh received while persisted-provider drafts are clean may hydrate their baseline. It must not replace unsaved edits to an existing provider. A pre-load new-provider form remains intact while the authoritative provider list hydrates around it. ## Persistence Guarantees - Existing provider records and credentials survive the schema migration. - An enabled legacy `session.waiting_for_input` subscription becomes an enabled `session.clarification_requested` subscription. A disabled legacy subscription remains disabled unless an already-existing semantic subscription is enabled. - Migration is idempotent and does not create duplicate subscriptions. - The one-time update-event migration adds `system.update_available` only to existing Local and System providers. It records completion separately so a later user opt-out survives restart. - Existing delivery history remains available for audit, but legacy session-level deduplication does not suppress new semantic occurrences. - The de-duplication claim is inserted atomically with its semantic occurrence identity. A failed provider send removes that exact provider/event/occurrence claim. - Update delivery history uses the same provider/event/occurrence records as every other semantic notification. No independent “last notified update” key is authoritative. ## Scenarios - **GIVEN** a provider subscribed to clarification requests, **WHEN** a session starts and waits for its first user turn, **THEN** no notification is sent. - **GIVEN** a provider subscribed only to turn completion, **WHEN** an ordinary agent turn completes, **THEN** it receives `Agent turn finished` and no copy claims that user input is required. - **GIVEN** a provider subscribed only to clarification requests, **WHEN** an agent issues a structured question bundle, **THEN** it receives one `Agent needs your answer` notification. - **GIVEN** two completed turns in one session, **WHEN** turn completion is selected, **THEN** each turn can produce one notification. - **GIVEN** a clarification event is delivered and then replayed, **WHEN** both copies reach the notification service, **THEN** only one delivery is sent. - **GIVEN** Local is disabled for `system.update_available`, **WHEN** a newer release is detected, **THEN** no Local update toast or desktop notification is delivered. - **GIVEN** Apprise is subscribed to `system.update_available`, **WHEN** a newer release is detected, **THEN** Apprise receives the same update occurrence and copy as Local and System providers. - **GIVEN** an existing Local provider predates the update event, **WHEN** the notification schema upgrade runs twice and the user removes the migrated subscription between runs, **THEN** the second run does not re-enable it. - **GIVEN** the startup release poll completes before any Local client subscribes, **WHEN** the first user client subscribes, **THEN** the cached release is delivered once without waiting for the next six-hour poll. - **GIVEN** native/browser notification permission is denied or delivery fails, **WHEN** an open Local client receives an update occurrence, **THEN** the in-app update indication remains visible and no background permission prompt is shown. - **GIVEN** an existing provider subscribed to `session.waiting_for_input`, **WHEN** the database is upgraded, **THEN** the provider is subscribed to clarification requests and not to turn completion. - **GIVEN** saved providers exist, **WHEN** the notifications settings route is opened from a cold frontend store, **THEN** the providers and their selections appear after loading without requiring a remount. - **GIVEN** a 390px mobile viewport, **WHEN** the notification event rows are displayed, **THEN** the stacked composition includes the update event and keeps every label, description, provider, and control readable and touch operable without horizontal scrolling; a changed update subscription survives Save and reload. ## Out of Scope - Inferring that arbitrary prose ending in a question mark requires an answer. - Including clarification text or other conversation content in notification payloads. - Per-task notification policy, schedules, batching, or quiet hours. - Retrying provider deliveries beyond the existing notification-provider behavior. - Adding a new notification-settings fetch-error surface. - A separate update-only enable switch or desktop/in-app/both channel policy. - Changing session lifecycle states or the chat waiting-state presentation. ## Implementation Plan - [Semantic notification implementation](../../plans/semantic-notifications/plan.md) - [Update notification reliability remediation](../../plans/update-notification-reliability/plan.md)