237 lines
12 KiB
Markdown
237 lines
12 KiB
Markdown
---
|
|
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 "<task title>".`
|
|
- A clarification notification uses:
|
|
- title: `Agent needs your answer`
|
|
- body: `The agent asked a question on "<task title>".`
|
|
- 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 <version> 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)
|