The dedupe key was kind + prompt id only. The approval lifecycle explicitly
permits the same externally supplied approval_id in multiple sessions and in
multiple gateway runs within one session (the same identity _notifyPromptCard's
neighbors use via _approvalOwnerForPending), so the second logical owner's
prompt was silently suppressed: no notification, card still blocking the run.
Key is now kind + sid + prompt id + gateway owner (run_id +
_gateway_mirror_token when both present), mirroring the approval-owner
identity used elsewhere in this file. Repeated SSE/poll/render calls for the
same full owner stay deduped.
Regression coverage (node-VM executed): same approval_id across two sessions
notifies both; same session + id under two distinct gateway run owners
notifies twice; repeat calls for one full owner notify once. Fails on the
pre-fix key (second session suppressed), passes with it.
Live testing on the PPC exposed the real reason unfocused-tab prompt
notifications never appeared: _notifyPromptCard correctly decided to
notify, but sendBrowserNotification's own gate
(_isBackgroundedForBrowserNotification — notify only when
document.hidden) vetoed the unfocused-but-visible case. The chokepoint
has already made the visibility decision; pass forceHidden so its
decision survives, while the notifications-enabled setting is still
honored.
Case 1 was missed: an approval on a NON-active session was suppressed
while the user was focused on a different session's tab — even though
they had no idea the prompt existed. The gate now suppresses only when
the prompt's own session is actively viewed (open in pane + tab visible
+ focused, via the existing _isSessionActivelyViewed). All three user
cases now notify: non-active session, hidden tab, unfocused window.
Approval/clarify prompts block the run until answered, but only the
active-session SSE event ever sent a browser notification — and it was
gated on document.hidden at delivery time, which background-tab SSE
throttling makes unreliable (the same #4416 class fixed for completion
notices). The 1.5s fallback poll, the post-respond 'next approval'
refresh, and reload-while-pending showed the card silently.
Notification ownership moves into the card renderers
(showApprovalCard / showClarifyCard) via _notifyPromptCard: per-id
dedupe (poll ticks re-render without re-pinging), suppressed only while
the tab is visible AND focused (an unfocused-but-visible window still
pings, since nothing progresses until the prompt is answered).