Skip to content

Notifications: Dismiss for questions and approvals - #667

Merged
alex-clickhouse merged 1 commit into
mainfrom
alex/question-dismiss
Nov 10, 2026
Merged

alex-clickhouse merged 1 commit into
mainfrom
alex/question-dismiss

Conversation

@alex-clickhouse

Copy link
Copy Markdown
Collaborator

What

A pending question or approval gets a Dismiss button:

  • on the Notifications page, beside the session link (where notify rows already had it);
  • under the options of the ask_user card in the chat.

A dismissed question or approval ends without an answer, the same way as at expiry.

Why

The Notifications page showed Dismiss for notify rows only. POST /api/notifications/{id}/dismiss already accepted a pending question, but handle_dismiss only changed the status. Other tabs and devices, the Telegram and Slack cards (their buttons stayed live), the "Needs you" group and the agent that asked all kept the question open.

How

Backend (NotificationService.handle_dismiss):

  • Every single-row dismissal sends a new notification_dismissed broadcast on __global__ (notification_id, session_id, notification_type, title, actor_id).
  • Question or approval: the Telegram and Slack cards get "🔕 Dismissed without an answer" and lose their buttons. The expiry helpers now take the status line (_edit_telegram_closed, _edit_slack_closed).
  • Question: the session that asked gets the internal turn [Question dismissed without an answer: <title>], written by the person who dismissed it. The checks for external, archived and missing sessions move to _takes_note, which expiry also uses. During a drain, such a dismissal is refused (503) and the question stays pending, as handle_answer does.
  • Approval: an approval-dismissed record in the mechanical-actions audit log. dismiss_notification now also requires answered_at IS NULL, so an approval that a press claimed (its dispatcher is running) is not dismissed.
  • A review loop that waits on its card proposes a new card at the next reconcile tick, as after an expiry. Its two messages now say "closed without an answer" in place of "expired".
  • Dismiss All is unchanged: notify rows only, no event.

Frontend:

  • DismissButton (shared by the page and the chat card). On a refusal, the store reads the row again: an answer from a different place replaces the button; otherwise "Could not dismiss. Try again." shows.
  • notification_dismissed handler: folds every copy of the row (list, toast, chat) to "Dismissed without an answer" with dismissed_by_actor_id, lowers the pending counts when the row was pending, reloads the session list ("Needs you") and closes the system alert. A toast for the row folds and closes after 5 s, as for an answer from a different place.
  • The toast X is unchanged: it hides the toast in that tab.

Docs: api.md (both dismiss endpoints, the new event), web-ui.md, architecture.md.

Screenshots

Synthetic data from a throwaway instance.

Notifications page Chat card Chat card after Dismiss
page chat dismissed

After Dismiss, the Notifs badge goes from 3 to 2 and the session leaves "Needs you". Phone, light theme: phone

Tests

  • Backend: new TestDismissal in tests/test_notification_lifecycle.py (broadcast, note, Telegram and Slack edits, approval audit, claimed and snoozed approvals, rows that are not pending, external and archived sessions, drain) and one route test through the real engine in tests/test_notification_plan_attribution.py (the note's actor, the session's cause, the 409 on a second dismissal). Full suite: 8823 passed, 39 skipped, equal to the 8862 collected.
  • Frontend: Notifications page, chat card, toast, store counts, chat store dispatch and system alert specs. npm run build passes; npm test: 141 files, 1908 tests pass.
  • Each added call was removed once and its tests failed (17 of 17).
  • Checked in a throwaway instance: Dismiss in the chat folds the card, updates the badge and "Needs you", records the person and starts the note turn.

🤖 Generated with Claude Code

The Notifications page showed Dismiss for `notify` rows only. The dismiss
endpoint accepted a pending question, but it only changed the status: other
tabs, the Telegram and Slack cards, "Needs you" and the asking agent kept the
question open.

A dismissed question or approval now ends without an answer, as at expiry:

- every single-row dismissal sends a `notification_dismissed` broadcast;
- the Telegram and Slack cards get "Dismissed without an answer" and lose
  their buttons;
- the session that asked a question gets the internal turn
  "[Question dismissed without an answer: <title>]", written by the person
  who dismissed it (no turn for external, archived or missing sessions;
  refused with 503 during a drain, so the question stays pending);
- an approval gets an `approval-dismissed` audit record. An approval that a
  press claimed is not dismissed, because its dispatcher runs the decision.

The web UI shows Dismiss on every pending row of the Notifications page and
under the options of the `ask_user` card in the chat. The event folds every
copy of the row, lowers the pending counts, reloads the session list and
closes the system alert. Dismiss All still takes `notify` rows only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@alex-clickhouse
alex-clickhouse marked this pull request as ready for review November 10, 2026 02:55
@nerveai

nerveai commented Nov 10, 2026

Copy link
Copy Markdown

Nerve PR review · ✅ Ready to merge

This closes a real gap. POST /api/notifications/{id}/dismiss already accepted questions and approvals but only flipped the status, so the Telegram and Slack buttons, other tabs, "Needs you" and the asking agent all kept the question open. handle_dismiss now ends a question or approval the way expiry does. It reuses the expiry pieces (_takes_note, the card-edit helpers, the approval audit) instead of building a parallel path, and adds the notification_dismissed broadcast the panel needed. The guards are right:

  • The drain gate mirrors handle_answer: a 503 via the global handler, and the row stays pending.
  • answered_at IS NULL leaves a claimed approval to its dispatcher, while a snoozed one stays dismissable because snooze_notification clears the claim.
  • Review loops re-file a dismissed card, as escalation_reproposals already intends.

Completeness: backend tests cover each guard (claimed, snoozed, drain, external, archived) and add a route test through the real engine; the frontend has specs; api.md, web-ui.md and architecture.md are updated. No config or migration is needed.

Checked: 11 notification, drain, review-loop and Telegram test files in an offline sandbox at c26104d: 270 passed; CI 4/4 green (backend, frontend build). Frontend not run locally. Trial merge into main is clean. Read service.py, db/notifications.py, the dismiss route, the review-loop reconcile, AskUserBlock and notificationStore. · automated review by nerveai — mention @nerveai to ask a follow-up.

@alex-clickhouse
alex-clickhouse merged commit 85bb512 into main Nov 10, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants