fix(desktop): resolve agent profiles through one archive-aware selector - #5706
Conversation
wesbillman
left a comment
There was a problem hiding this comment.
Reviewing on Wes Pfleger's behalf.
The archived-primary path described in this PR is still reachable from the main Agents library, so this does not fully fix the reported navigation bug.
UnifiedAgentsSection chooses profileAgent = pickProfileAgent(group.agents) without archive state (desktop/src/features/agents/ui/UnifiedAgentsSection.tsx:130-159). When a persona card has any instance, AgentPersonaCard then always calls onOpenAgentProfile(agent.pubkey) (:313-321). AgentsScreen serializes that as the explicit profile target (desktop/src/features/agents/ui/AgentsScreen.tsx:69-76). That means UserProfilePanel reaches the explicit-pubkey branch in resolveProfileManagedAgent, which intentionally returns the archived record verbatim, and never exercises the new archive-aware persona fallback.
Concretely, if pickProfileAgent selects the archived Duncan (it prioritizes active status, then name), clicking Duncan's card still displays that archived Duncan as the primary profile. The Instances list is filtered afterward, but the profile identity, status/actions, channels, activity, and queries remain bound to the archived pubkey.
Please make the persona-card entry path archive-aware, for example by selecting a non-archived profileAgent before opening an explicit pubkey, or by navigating the card via the persona target and reserving explicit-pubkey navigation for clicks on a specific instance. Add a regression test that follows the caller contract (archived selected candidate plus live sibling) rather than testing only the currently bypassed persona branch.
The new resolver tests intentionally codify behavior changes (including explicit archived selection and all-archived fallback); those semantics should remain visible in the PR review.
|
Correction to the attribution in my changes-requested review: I conflated two names. Wes Billman owns the authenticated GitHub account through which this automated review was submitted; Will Pfleger is the PR author. There is no “Wes Pfleger” involved. The review was produced by Carl, an automated reviewer, and the technical finding is unchanged. Sorry for the confusion. |
wesbillman
left a comment
There was a problem hiding this comment.
Carl, an automated reviewer, commenting via Wes’s GitHub account.
Re-reviewed exact head 2e19767a88328ab66320b5580f3af626f0138f5d. My prior blocker is resolved.
The Agents-library caller now passes the archive predicate into pickProfileAgent, which removes archived candidates before active/name sorting. A mixed persona therefore opens the live sibling; an all-archived persona gets undefined and follows the existing persona-only navigation path. Archived standalone and unknown-persona cards are also removed, closing the adjacent explicit-pubkey entry paths. Direct explicit-pubkey profile navigation remains intentionally exact for manage/unarchive access.
The all-archived panel behavior is now consistent with the title: persona resolution returns no managed agent and the Instances section receives an empty list rather than restoring archived rows.
The added behavior-changing tests cover the actual caller selection contract, all-archived persona fallback, standalone/unknown filtering, fail-open loading, profile resolution, and Instances filtering. Focused resolver/group tests pass (25/25), desktop typecheck passes, and git diff --check is clean at this head.
I found no remaining material code issue. I am leaving this as a comment rather than approving because Wes has not explicitly authorized an approval review for this PR. Existing exact-head CI was still running in several Desktop jobs at my review snapshot.
wesbillman
left a comment
There was a problem hiding this comment.
Carl, an automated reviewer, commenting via Wes’s GitHub account.
Requesting changes after a fresh lifecycle review. Princess Donut identified a valid loading-state race that my previous re-review missed.
useIsArchivedPredicate() deliberately returns false before the archive snapshot exists. During that window, pickProfileAgent can select the archived active/name-preferred sibling. The persona card then navigates by that candidate's explicit pubkey (UnifiedAgentsSection.tsx → AgentsScreen), and resolveProfileManagedAgent deliberately preserves explicit-pubkey targets after hydration. When the archive snapshot arrives, the panel recomputes but remains pinned to the archived identity. This turns a transient fail-open window into a durable wrong-primary panel until it is closed and reopened.
Please keep persona-card navigation as a persona target so archive hydration can retarget the panel. The selected live agent may still drive card presentation/actions, and deliberate instance/error navigation may remain explicit-pubkey based. Add a regression composing the actual transition: archive state unavailable → persona card chooses the old preferred record → archive state arrives with that record archived → target resolves to the live sibling (or persona-only when all are archived).
The steady-state filtering added in this head is sound, but it does not close this lifecycle path.
wesbillman
left a comment
There was a problem hiding this comment.
Princess Donut, an automated reviewer, commenting via Wes’s GitHub account after alignment with Carl and Mongo.
Request changes — the persona-target fix introduces a selected-instance mismatch whenever a persona has multiple live siblings.
UnifiedAgentsSection chooses the instance represented by the card with pickProfileAgent(group.agents, isArchived) (desktop/src/features/agents/ui/UnifiedAgentsSection.tsx:135-156). That selector filters archived records, then deliberately ranks the remaining instances by active status and name (desktop/src/features/agents/ui/unifiedAgentGroups.ts:51-63). The card’s avatar, status, actions, error state, and optional Runtime-tab target all come from that selected instance.
The main click now records only the persona (UnifiedAgentsSection.tsx:321-332). In the panel, however, resolveProfileManagedAgent resolves that persona with a raw file-order find over the first non-archived sibling (desktop/src/features/profile/ui/UserProfilePanelUtils.ts:181-184). It does not apply the card’s active/name policy.
Concrete failure: with managed-agent order [stopped Abe, running Zed], the card displays and acts as running Zed, but clicking it opens Abe. The panel then binds profile queries, status/actions, channels, activity, logs, and archive controls to Abe (desktop/src/features/profile/ui/UserProfilePanel.tsx:192-234). The stopped-error variant is more misleading: an errored alphabetical instance can make the card request the Runtime tab, while the panel resolves a different healthy file-first sibling. The previous explicit-pubkey click preserved the exact represented instance; this head loses that contract while fixing hydration pinning.
The new mounted transition test misses the divergence because its expected live target is also the first remaining live record (UnifiedAgentsSectionCardTarget.test.mjs:226-273). The resolver tests likewise do not compose multiple live siblings in conflicting file-order versus active/name order.
Smallest safe remedy: retain persona URL targeting so archive hydration can self-correct, but make persona resolution consume the same shared archive-aware active→name selector as the card. Do not duplicate the policy. Add regressions with reversed file order for (1) active preference, (2) same-status name preference, and ideally (3) the stopped-error Runtime-tab path.
The archive filtering, all-archived persona-only behavior, standalone/orphan filtering, and fail-open hydration correction are otherwise sound. Exact head reviewed: f6c0045d10d94ff1b17559eeea5f92fac646b6a6; all reported exact-head checks are green, and git diff --check is clean. CI cannot detect this because the new tests encode matching order on both sides.
Pull request was closed
dfa2f41 to
17d2147
Compare
4fec52c to
578a957
Compare
Profile resolution funnels every entry point through the shared pickProfileAgent selector, but it ranked instances purely by active/name — a relay-archived instance early in that order could hijack the persona card and the profile panel, and the persona card recorded a durable pubkey target that stranded the panel on an archived identity when the click landed during the archive-snapshot fail-open window. Make pickProfileAgent and the library grouping archive-aware via the fail-open useIsArchivedPredicate, and have the persona card record a persona target that re-resolves each render. A deliberately requested archived pubkey still stays exact so its archive controller can unarchive it, and Runtime instance pinning and the runtime-error affordance keep their explicit-pubkey path. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The Runtime > Instances roster rendered every persona instance raw, so relay-archived instances appeared mixed in with live ones as if active — the original Layer-2 confusion the archive-aware selector work set out to fix. Bucket the roster off the same fail-open useIsArchivedPredicate the selector uses (one policy, no duplication) and render archived rows under a labeled Archived subsection. Archived rows keep the explicit-pubkey click so unarchive stays UI-reachable. While the archive snapshot loads the predicate returns false, so every row renders live — nothing hidden or labeled. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
578a957 to
bf7e986
Compare
wesbillman
left a comment
There was a problem hiding this comment.
Carl, an automated reviewer, commenting via Wes’s GitHub account.
Requesting changes at exact head bf7e986a04ba831938f77d254ca09b2b9a9bb070 after consolidated Royal Court review.
High — ordinary navigation to an archived historical instance is incorrectly treated as deliberate instance selection.
resolveCanonicalManagedAgent returns directManagedAgent for any archived requested pubkey before the preferDirectManagedAgent branch can apply pickDirectProfileAgent (desktop/src/features/profile/lib/useCanonicalManagedAgentProfile.ts:50-63). But ordinary profile entry points carry only a pubkey. For example, message/avatar navigation calls openProfilePanel(pubkey) (desktop/src/features/profile/ui/UserProfilePopover.tsx:261-271, 347-366). Therefore an old message from an archived, stopped persona instance remains pinned to that retired identity even when a live running sibling exists. The panel then binds profile data, actions, activity, and archive controls to the wrong primary instance, contradicting pickDirectProfileAgent’s documented historical-navigation contract (desktop/src/features/agents/lib/pickProfileAgent.ts:32-51).
The Runtime → Instances path already supplies the provenance needed to preserve exactness: its row click records requestedInstancePubkey, which becomes preserveRequestedInstance (desktop/src/features/profile/ui/UserProfilePanel.tsx:182-200, 831-835). Restrict archived-instance exactness to that explicit provenance. Ordinary archived pubkey opens should continue through pickDirectProfileAgent; a historical key with no managed/persona match still falls back to the requested pubkey naturally.
Please add a regression for an archived stopped direct instance plus a live running sibling with preferDirectManagedAgent: true and preserveRequestedInstance: false. The current test at resolveCanonicalManagedAgent.test.mjs:141-157 codifies the overbroad short-circuit instead.
The archive filtering, shared ranking, persona-card hydration correction, and labeled archived Runtime subsection otherwise look coherent. Focused changed tests pass 30/30; desktop typecheck and desktop checks pass. This is a provenance defect those tests currently bless, not a CI failure.
Carl, an automated reviewer, correcting a review submitted via Wes’s GitHub account. I am retracting this blocker. The described behavior exists, but I overstated it as a defect: this PR and its tests deliberately keep direct archived-pubkey opens exact so the archive controller remains reachable for unarchive. Without a confirmed product requirement that ordinary historical-message/avatar navigation canonicalize to a live sibling, the conflicting selector documentation establishes contract ambiguity, not a release-blocking correctness bug. Exact head bf7e986 now has all reported checks terminal and successful or intentionally skipped. I found no remaining material issue.
…p-repair * origin/main: Rename Bumble agent to Pollen (#5864) fix(desktop): resolve agent profiles through one archive-aware selector (#5706) fix(acp): gate relay-signed workflow messages on their attributed author (#6129) fix(acp): replace Goose native system prompt (#5964) feat(workflows): add responsive library card actions (#6008) fix(desktop): enforce shared agent access across devices (#6086) feat(model-capabilities): drive model capabilities and labels from one manifest (#5597) docs: refresh agent development guidance (#6049) feat(mobile): require device authentication for identity export (#5116) fix(desktop): hide the offcanvas-collapsed sidebar so it stops painting over the community rail (#5947) Polish mobile message threads and composer (#5645) chore(release): release Buzz Desktop version 0.5.14 (#5917) ci(release): remove desktop smoke gate (#5914) chore(release): release Buzz Desktop version 0.5.13 (#5912) fix(ci): read Playwright version without nested shell quoting (#5910) fix(desktop): restore the agent trading-card mint button (#5900) Projects v3: unify sharing, discussions, and issue ownership (#5792) chore(release): release Buzz Desktop version 0.5.12 (#5903) fix(mobile): unwrap batched observer telemetry (#5805) perf(desktop): update active turns incrementally (#5897) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> # Conflicts: # desktop/src-tauri/src/migration.rs
…graphy-staging * origin/main: Remove GitHub security advisory commitment (#6144) Rename Bumble agent to Pollen (#5864) fix(desktop): resolve agent profiles through one archive-aware selector (#5706) fix(acp): gate relay-signed workflow messages on their attributed author (#6129) fix(acp): replace Goose native system prompt (#5964) feat(workflows): add responsive library card actions (#6008) fix(desktop): enforce shared agent access across devices (#6086) feat(model-capabilities): drive model capabilities and labels from one manifest (#5597) Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
…gaps * origin/main: fix(desktop): align preview sidebar row styling (#6163) fix(desktop): repair dropped team membership links at boot and on edit (#5904) fix(cli): keep project replacement timestamps at or after wall clock (#5666) Remove GitHub security advisory commitment (#6144) Rename Bumble agent to Pollen (#5864) fix(desktop): resolve agent profiles through one archive-aware selector (#5706) fix(acp): gate relay-signed workflow messages on their attributed author (#6129) fix(acp): replace Goose native system prompt (#5964) feat(workflows): add responsive library card actions (#6008) fix(desktop): enforce shared agent access across devices (#6086) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
…arer-auth * origin/main: (21 commits) fix(desktop): bind presence retry timers (#6213) ci: make file-size policy a first-class gate (#6187) fix(desktop): eliminate mounted-view CPU burn — compositor-safe shimmer, observer append fast path, poll-tick disk reads (#6198) chore(release): release Buzz Desktop version 0.5.16 (#6191) fix(desktop): restore release agent mentions (#6182) test(desktop): cover exact workflow batch limit (#6168) chore(release): release Buzz Desktop version 0.5.15 (#6173) Preserve managed agent mentions during relay errors (#6167) fix(workflows): preserve multi-channel listing semantics (#6009) Remove Startup Recovery section in base prompt (#6161) fix(desktop): align preview sidebar row styling (#6163) fix(desktop): repair dropped team membership links at boot and on edit (#5904) fix(cli): keep project replacement timestamps at or after wall clock (#5666) Remove GitHub security advisory commitment (#6144) Rename Bumble agent to Pollen (#5864) fix(desktop): resolve agent profiles through one archive-aware selector (#5706) fix(acp): gate relay-signed workflow messages on their attributed author (#6129) fix(acp): replace Goose native system prompt (#5964) feat(workflows): add responsive library card actions (#6008) fix(desktop): enforce shared agent access across devices (#6086) ... Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> # Conflicts: # CHANGELOG.md
…or (block#5706) Agent profiles resolve through one shared selector (`pickProfileAgent`) at every entry point — the persona card, the profile panel, and library grouping. That selector ranked instances only by active/name, with no archive awareness, so a relay-archived instance early in file order could hijack the persona card and the profile panel. The persona card also recorded a durable pubkey target, which could strand the panel on an archived identity when the click landed during the archive-snapshot fail-open window. The profile panel's Runtime → Instances roster had the same blind spot: it rendered every persona instance raw, so archived instances appeared mixed in with live ones as if active. This makes the shared resolution path archive-aware via the existing fail-open `useIsArchivedPredicate`: - `pickProfileAgent` filters archived instances before ranking and returns `undefined` when every instance is archived (persona-only mode). - `buildUnifiedGroups` drops archived agents from the standalone `Custom agents` and `Unknown agents` buckets; matched persona groups keep their full list and rely on the selector's persona-only fallback. - `useCanonicalManagedAgentProfile` resolves through a pure `resolveCanonicalManagedAgent` helper that applies the target-provenance rules: a deliberately requested archived pubkey stays exact (so its archive controller can unarchive it, even when a live sibling exists), `preserveRequestedInstance` still pins a Runtime → Instances selection, and non-archived historical navigation keeps its canonicalization. - The persona card's main click records a persona target that re-resolves every render, so it self-corrects to a live sibling after hydration. Deliberate instance navigation and the runtime-error affordance keep their explicit-pubkey path. - The Runtime → Instances roster (`ProfileInstancesSection`) buckets instances off the same predicate via `bucketPersonaInstances`: live rows render as before, and archived rows move under a labeled `Archived` subsection. The instance count reflects both buckets, and archived rows keep their explicit-pubkey click so unarchive stays UI-reachable (the deliberate-navigation path above). The predicate is fail-open (treats every identity as live while the relay archive snapshot loads) and self-exempt, so a cold start never hides an identity and a user is never folded from their own client. While the snapshot is loading, every instance renders in the live list — nothing hidden, nothing labeled. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Signed-off-by: bhargavms <bhargav.m@ewa-services.com>
…lock#6086, block#5706) Port the two Tier 2 agent/Hermes behaviours from upstream Buzz: - Archive-aware selector: buildUnifiedGroups/pickProfileAgent take the useIsArchivedPredicate() predicate, drop archived standalone and unknown-definition instances, and resolve a definition card's target to a live sibling (undefined when all are archived). Matched definition groups keep their full instance list so the card still renders. - Shared access: a definition's respondTo/respondToAllowlist now syncs down to its linked managed-agent instances, edit dialogs seed from the instance actually enforcing the policy, and EditRespondToDialog surfaces a profileSyncError through the existing warning helper. - Config validation names the field a user still has to fill instead of silently disabling Save, and never names a hidden provider/model field. Crew archive/restore/delete backend, the Agents archive browser, owner-only access and channel-first IA are unchanged. Signed-off-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-Authored-By: Oscar Le <oscar.lehuu@gmail.com>
…lock#6086, block#5706) Port the two Tier 2 agent/Hermes behaviours from upstream Buzz: - Archive-aware selector: buildUnifiedGroups/pickProfileAgent take the useIsArchivedPredicate() predicate, drop archived standalone and unknown-definition instances, and resolve a definition card's target to a live sibling (undefined when all are archived). Matched definition groups keep their full instance list so the card still renders. - Shared access: a definition's respondTo/respondToAllowlist now syncs down to its linked managed-agent instances, edit dialogs seed from the instance actually enforcing the policy, and EditRespondToDialog surfaces a profileSyncError through the existing warning helper. - Config validation names the field a user still has to fill instead of silently disabling Save, and never names a hidden provider/model field. Crew archive/restore/delete backend, the Agents archive browser, owner-only access and channel-first IA are unchanged. Signed-off-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-Authored-By: Oscar Le <oscar.lehuu@gmail.com>
…archive-selector upstream(sync): shared agent access + archive-aware profile selector (block#6086, block#5706)
…or (block#5706) Agent profiles resolve through one shared selector (`pickProfileAgent`) at every entry point — the persona card, the profile panel, and library grouping. That selector ranked instances only by active/name, with no archive awareness, so a relay-archived instance early in file order could hijack the persona card and the profile panel. The persona card also recorded a durable pubkey target, which could strand the panel on an archived identity when the click landed during the archive-snapshot fail-open window. The profile panel's Runtime → Instances roster had the same blind spot: it rendered every persona instance raw, so archived instances appeared mixed in with live ones as if active. This makes the shared resolution path archive-aware via the existing fail-open `useIsArchivedPredicate`: - `pickProfileAgent` filters archived instances before ranking and returns `undefined` when every instance is archived (persona-only mode). - `buildUnifiedGroups` drops archived agents from the standalone `Custom agents` and `Unknown agents` buckets; matched persona groups keep their full list and rely on the selector's persona-only fallback. - `useCanonicalManagedAgentProfile` resolves through a pure `resolveCanonicalManagedAgent` helper that applies the target-provenance rules: a deliberately requested archived pubkey stays exact (so its archive controller can unarchive it, even when a live sibling exists), `preserveRequestedInstance` still pins a Runtime → Instances selection, and non-archived historical navigation keeps its canonicalization. - The persona card's main click records a persona target that re-resolves every render, so it self-corrects to a live sibling after hydration. Deliberate instance navigation and the runtime-error affordance keep their explicit-pubkey path. - The Runtime → Instances roster (`ProfileInstancesSection`) buckets instances off the same predicate via `bucketPersonaInstances`: live rows render as before, and archived rows move under a labeled `Archived` subsection. The instance count reflects both buckets, and archived rows keep their explicit-pubkey click so unarchive stays UI-reachable (the deliberate-navigation path above). The predicate is fail-open (treats every identity as live while the relay archive snapshot loads) and self-exempt, so a cold start never hides an identity and a user is never folded from their own client. While the snapshot is loading, every instance renders in the live list — nothing hidden, nothing labeled. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
…lock#5706 card-click contract d12d825 (block#5706) deliberately made persona card clicks open the persona target instead of an explicit instance pubkey; these two tests still pinned the pre-change behavior and failed deterministically on product/main (which has no CI check-runs, so the break was silent). Update them to verify the new contract: card click -> profilePersona param + running instance primary action, with the exact-instance pick delegated to the panel Runtime tab -> Instances list.
* feat(mobile): push notifications MVP (block#6269) This PR implements MVP, iOS-only, [NIP-PL](https://github.com/block/buzz/blob/8d2d0ff5ad42733e9949442c4b6358d0ba87f9a8/docs/nips/NIP-PL.md)-compliant push notifications. A relay with `BUZZ_PUSH_ENABLED` will send a push notification for any message that appears in the in-app Notifications tab. The first time the client first connects to a relay with `BUZZ_PUSH_ENABLED`: ```mermaid sequenceDiagram autonumber participant App as Buzz iOS app participant iOS participant Relay as Buzz relay participant Attest as Apple App Attest participant Gateway as Push gateway App->>Relay: Fetch NIP-11 push capability Relay-->>App: Push profile, current relay public key, and limits par App->>iOS: Request notification permission iOS-->>App: Permission result and App->>iOS: Register for remote notifications iOS-->>App: Device token end App->>Gateway: Request installation challenge Gateway-->>App: Single-use challenge App->>Attest: Attest installation transcript Attest-->>App: Attestation proof App->>Gateway: Enroll device token and proof Gateway-->>App: Installation handle App->>Gateway: Request delegation challenge Gateway-->>App: Single-use challenge App->>Attest: Assert relay-key delegation Attest-->>App: Assertion App->>Gateway: Create delegation Gateway-->>App: Opaque endpoint grant App->>Relay: Publish encrypted push lease and filters Relay-->>App: Lease acknowledged ``` When a notification-eligible event is received by the relay: ```mermaid %%{init: { "sequence": { "actorMargin": 20, "width": 110, "messageMargin": 18, "diagramMarginX": 8, "wrap": true } }}%% sequenceDiagram autonumber participant Relay as Buzz relay participant Gateway as Push gateway participant APNs as Apple Push<br/>Notification service participant iOS participant NSE as Notification service<br/>extension Relay->>Gateway: POST /v1/deliveries/apns<br/>opaque endpoint grant, request ID, expiry, NIP-98 authorization Gateway->>APNs: POST /3/device/{device-token}<br/>topic, request ID, expiry, constant mutable-content payload APNs-->>Gateway: 200 OK: request accepted Gateway-->>Relay: 200 OK: accepted status APNs-->>iOS: Notification: constant reconnect alert<br/>mutable-content = 1 iOS->>NSE: Invoke extension<br/>original notification content NSE->>Relay: POST /query: subscription filters, limit 10<br/>NIP-98 authorization Relay-->>NSE: 200 OK: signed Nostr events<br/>kinds 9, 40002, 45001, or 45003 NSE->>iOS: Complete notification: title, body, subtitle<br/>thread ID, exact-message target ``` relay → push gateway → APNs -> NSE -> Notification Center The APNs wake payload is intentionally constant and opaque: it contains no originating community or message identifier, in keeping with the implemented NIP-PL privacy design. The Notification Service Extension must therefore reconnect to the relay and resolve eligible messages after each wake. Around overlapping wakes, timing boundaries, or resolution windows, notification presentation may occasionally omit an expected message or display a message more than once. This best-effort behavior is deliberately accepted for the current implementation and will be measured during the internal rollout to determine whether the user experience is acceptable before any broader deployment; the implementation does not claim exactly-once presentation. Live end-to-end hardware validation used an internal remotely hosted development relay and push gateway, the APNs sandbox, and a physical iPhone 12 mini: - A second real Buzz client published a uniquely marked message through the hosted relay. - The relay matched the message and sent the constant opaque wake through the hosted gateway. The gateway made an actual APNs request; no `simctl push` or simulated notification was used. - The iPhone received the notification on its lock screen. The Notification Service Extension reconnected to the relay, fetched the event, verified its ID and signature, and replaced the placeholder content with the real notification title and body. - After the app populated its shared presentation cache, a final marked notification visibly showed the sender display name, sender avatar, and hashtag-prefixed channel name. - Tapping a lock-screen notification opened Buzz and exercised the notification-response path and navigated to the corresponding message. Final validation with a dogfood-signed artifact and production App Attest/APNs configuration remains a release step. - **First pass:** [Carl](buzz://message?channel=18882f4c-289f-41db-942f-81f6f8066da1&id=74ab9a93bb227f3e762568f1cf9fee66d7495b0edc3918735ff787238b9cc585) found missing transient retries, executor-key rotation suppression, duplicate installation renewal, and an unauthenticated challenge write amplifier. These were resolved by [retry-safe bootstrap](block@12c66ea62) and [authenticated renewal plus a cross-replica quota](block@8e5ece0bd). [sol-max](buzz://message?channel=ad83385f-8e9e-4461-9a35-c1bf2e208532&id=d26d53daa4684669e2ed354638241f13f36c3a97027fe8b4dd738aff09038962) found delegation generation burning and an edited applied migration, resolved by [exact-generation revocation](block@c26d2159d) and a [forward-only migration](block@956c1d099). [k3-max](buzz://message?channel=5e46055d-a766-4065-ae25-05d1e4aaa6b2&id=d43139138a0b15f806cbdbeeedd8f69d992cadf2e805876db6fdde6a34c7eda1) found no blockers. - **Exact-head re-review:** [Carl](buzz://message?channel=18882f4c-289f-41db-942f-81f6f8066da1&id=a897721673459301b0cf26e8b85a1478d7ebbb56a4621f93d774c98d395b8f68), [sol-max](buzz://message?channel=ad83385f-8e9e-4461-9a35-c1bf2e208532&id=fb2159f709ec68f74f7b21459acd76da0e8a7c5c0f3d469f99826b0cc2380849), and [k3-max](buzz://message?channel=5e46055d-a766-4065-ae25-05d1e4aaa6b2&id=2ce2842910435f562e9d9cc718595848f281b122c94605e523a4b964254b8bfb) independently returned **NO BLOCKERS** at `7eb3a650b`; k3-max also revalidated every remediation and the endpoint-specific App Attest enrollment bound. --------- Signed-off-by: Tom Brow <tomb@squareup.com> Signed-off-by: Tom Brow <tomb@block.xyz> Co-authored-by: Tom Brow <tomb@squareup.com> Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Jordan Mecom <jm@squareup.com> * [lenny] implement: REG-17 qualification receipt core Signed-off-by: Michael Feth <mfethe1@gmail.com> * [lenny] implement: REG-18 timeline announcements Signed-off-by: Michael Feth <mfethe1@gmail.com> * [lenny] fix: REG-17 review findings (exact git SHA lengths, redact secret launch flags) * [lenny] fix: allowlist synthetic APNs test fixtures in gitleaks * [lenny] fix: dart format mobile * [lenny] fix: restore relay_session.dart under mobile file-size ratchet * [lenny] fix: keep relay_session.dart under mobile ratchet with guard restored * [lenny] fix: upload CA bundle into task containers so buzz-agent's rustls can build its HTTP client in TLS-rootless images (REG-17 joined-trial enabler); add local qwen36 manifest/endpoint * [lenny] fix(e2e): align persona gallery/profile tests with the post-block#5706 card-click contract d12d825 (block#5706) deliberately made persona card clicks open the persona target instead of an explicit instance pubkey; these two tests still pinned the pre-change behavior and failed deterministically on product/main (which has no CI check-runs, so the break was silent). Update them to verify the new contract: card click -> profilePersona param + running instance primary action, with the exact-instance pick delegated to the panel Runtime tab -> Instances list. --------- Signed-off-by: Tom Brow <tomb@squareup.com> Signed-off-by: Tom Brow <tomb@block.xyz> Signed-off-by: Michael Feth <mfethe1@gmail.com> Co-authored-by: Tom Brow <tomb@block.xyz> Co-authored-by: Tom Brow <tomb@squareup.com> Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Jordan Mecom <jm@squareup.com>
* feat(mobile): push notifications MVP (block#6269) This PR implements MVP, iOS-only, [NIP-PL](https://github.com/block/buzz/blob/8d2d0ff5ad42733e9949442c4b6358d0ba87f9a8/docs/nips/NIP-PL.md)-compliant push notifications. A relay with `BUZZ_PUSH_ENABLED` will send a push notification for any message that appears in the in-app Notifications tab. The first time the client first connects to a relay with `BUZZ_PUSH_ENABLED`: ```mermaid sequenceDiagram autonumber participant App as Buzz iOS app participant iOS participant Relay as Buzz relay participant Attest as Apple App Attest participant Gateway as Push gateway App->>Relay: Fetch NIP-11 push capability Relay-->>App: Push profile, current relay public key, and limits par App->>iOS: Request notification permission iOS-->>App: Permission result and App->>iOS: Register for remote notifications iOS-->>App: Device token end App->>Gateway: Request installation challenge Gateway-->>App: Single-use challenge App->>Attest: Attest installation transcript Attest-->>App: Attestation proof App->>Gateway: Enroll device token and proof Gateway-->>App: Installation handle App->>Gateway: Request delegation challenge Gateway-->>App: Single-use challenge App->>Attest: Assert relay-key delegation Attest-->>App: Assertion App->>Gateway: Create delegation Gateway-->>App: Opaque endpoint grant App->>Relay: Publish encrypted push lease and filters Relay-->>App: Lease acknowledged ``` When a notification-eligible event is received by the relay: ```mermaid %%{init: { "sequence": { "actorMargin": 20, "width": 110, "messageMargin": 18, "diagramMarginX": 8, "wrap": true } }}%% sequenceDiagram autonumber participant Relay as Buzz relay participant Gateway as Push gateway participant APNs as Apple Push<br/>Notification service participant iOS participant NSE as Notification service<br/>extension Relay->>Gateway: POST /v1/deliveries/apns<br/>opaque endpoint grant, request ID, expiry, NIP-98 authorization Gateway->>APNs: POST /3/device/{device-token}<br/>topic, request ID, expiry, constant mutable-content payload APNs-->>Gateway: 200 OK: request accepted Gateway-->>Relay: 200 OK: accepted status APNs-->>iOS: Notification: constant reconnect alert<br/>mutable-content = 1 iOS->>NSE: Invoke extension<br/>original notification content NSE->>Relay: POST /query: subscription filters, limit 10<br/>NIP-98 authorization Relay-->>NSE: 200 OK: signed Nostr events<br/>kinds 9, 40002, 45001, or 45003 NSE->>iOS: Complete notification: title, body, subtitle<br/>thread ID, exact-message target ``` relay → push gateway → APNs -> NSE -> Notification Center The APNs wake payload is intentionally constant and opaque: it contains no originating community or message identifier, in keeping with the implemented NIP-PL privacy design. The Notification Service Extension must therefore reconnect to the relay and resolve eligible messages after each wake. Around overlapping wakes, timing boundaries, or resolution windows, notification presentation may occasionally omit an expected message or display a message more than once. This best-effort behavior is deliberately accepted for the current implementation and will be measured during the internal rollout to determine whether the user experience is acceptable before any broader deployment; the implementation does not claim exactly-once presentation. Live end-to-end hardware validation used an internal remotely hosted development relay and push gateway, the APNs sandbox, and a physical iPhone 12 mini: - A second real Buzz client published a uniquely marked message through the hosted relay. - The relay matched the message and sent the constant opaque wake through the hosted gateway. The gateway made an actual APNs request; no `simctl push` or simulated notification was used. - The iPhone received the notification on its lock screen. The Notification Service Extension reconnected to the relay, fetched the event, verified its ID and signature, and replaced the placeholder content with the real notification title and body. - After the app populated its shared presentation cache, a final marked notification visibly showed the sender display name, sender avatar, and hashtag-prefixed channel name. - Tapping a lock-screen notification opened Buzz and exercised the notification-response path and navigated to the corresponding message. Final validation with a dogfood-signed artifact and production App Attest/APNs configuration remains a release step. - **First pass:** [Carl](buzz://message?channel=18882f4c-289f-41db-942f-81f6f8066da1&id=74ab9a93bb227f3e762568f1cf9fee66d7495b0edc3918735ff787238b9cc585) found missing transient retries, executor-key rotation suppression, duplicate installation renewal, and an unauthenticated challenge write amplifier. These were resolved by [retry-safe bootstrap](block@12c66ea62) and [authenticated renewal plus a cross-replica quota](block@8e5ece0bd). [sol-max](buzz://message?channel=ad83385f-8e9e-4461-9a35-c1bf2e208532&id=d26d53daa4684669e2ed354638241f13f36c3a97027fe8b4dd738aff09038962) found delegation generation burning and an edited applied migration, resolved by [exact-generation revocation](block@c26d2159d) and a [forward-only migration](block@956c1d099). [k3-max](buzz://message?channel=5e46055d-a766-4065-ae25-05d1e4aaa6b2&id=d43139138a0b15f806cbdbeeedd8f69d992cadf2e805876db6fdde6a34c7eda1) found no blockers. - **Exact-head re-review:** [Carl](buzz://message?channel=18882f4c-289f-41db-942f-81f6f8066da1&id=a897721673459301b0cf26e8b85a1478d7ebbb56a4621f93d774c98d395b8f68), [sol-max](buzz://message?channel=ad83385f-8e9e-4461-9a35-c1bf2e208532&id=fb2159f709ec68f74f7b21459acd76da0e8a7c5c0f3d469f99826b0cc2380849), and [k3-max](buzz://message?channel=5e46055d-a766-4065-ae25-05d1e4aaa6b2&id=2ce2842910435f562e9d9cc718595848f281b122c94605e523a4b964254b8bfb) independently returned **NO BLOCKERS** at `7eb3a650b`; k3-max also revalidated every remediation and the endpoint-specific App Attest enrollment bound. --------- Signed-off-by: Tom Brow <tomb@squareup.com> Signed-off-by: Tom Brow <tomb@block.xyz> Co-authored-by: Tom Brow <tomb@squareup.com> Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Jordan Mecom <jm@squareup.com> * [lenny] implement: REG-17 qualification receipt core Signed-off-by: Michael Feth <mfethe1@gmail.com> * [lenny] implement: REG-18 timeline announcements Signed-off-by: Michael Feth <mfethe1@gmail.com> * [lenny] fix: REG-17 review findings (exact git SHA lengths, redact secret launch flags) * [lenny] fix: allowlist synthetic APNs test fixtures in gitleaks * [lenny] fix: dart format mobile * [lenny] fix: restore relay_session.dart under mobile file-size ratchet * [lenny] fix: keep relay_session.dart under mobile ratchet with guard restored * [lenny] fix: upload CA bundle into task containers so buzz-agent's rustls can build its HTTP client in TLS-rootless images (REG-17 joined-trial enabler); add local qwen36 manifest/endpoint * [lenny] fix(e2e): align persona gallery/profile tests with the post-block#5706 card-click contract d12d825 (block#5706) deliberately made persona card clicks open the persona target instead of an explicit instance pubkey; these two tests still pinned the pre-change behavior and failed deterministically on product/main (which has no CI check-runs, so the break was silent). Update them to verify the new contract: card click -> profilePersona param + running instance primary action, with the exact-instance pick delegated to the panel Runtime tab -> Instances list. --------- Signed-off-by: Tom Brow <tomb@squareup.com> Signed-off-by: Tom Brow <tomb@block.xyz> Signed-off-by: Michael Feth <mfethe1@gmail.com> Co-authored-by: Tom Brow <tomb@block.xyz> Co-authored-by: Tom Brow <tomb@squareup.com> Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Jordan Mecom <jm@squareup.com>
Agent profiles resolve through one shared selector (
pickProfileAgent) at every entry point — the persona card, the profile panel, and library grouping. That selector ranked instances only by active/name, with no archive awareness, so a relay-archived instance early in file order could hijack the persona card and the profile panel. The persona card also recorded a durable pubkey target, which could strand the panel on an archived identity when the click landed during the archive-snapshot fail-open window. The profile panel's Runtime → Instances roster had the same blind spot: it rendered every persona instance raw, so archived instances appeared mixed in with live ones as if active.This makes the shared resolution path archive-aware via the existing fail-open
useIsArchivedPredicate:pickProfileAgentfilters archived instances before ranking and returnsundefinedwhen every instance is archived (persona-only mode).buildUnifiedGroupsdrops archived agents from the standaloneCustom agentsandUnknown agentsbuckets; matched persona groups keep their full list and rely on the selector's persona-only fallback.useCanonicalManagedAgentProfileresolves through a pureresolveCanonicalManagedAgenthelper that applies the target-provenance rules: a deliberately requested archived pubkey stays exact (so its archive controller can unarchive it, even when a live sibling exists),preserveRequestedInstancestill pins a Runtime → Instances selection, and non-archived historical navigation keeps its canonicalization.ProfileInstancesSection) buckets instances off the same predicate viabucketPersonaInstances: live rows render as before, and archived rows move under a labeledArchivedsubsection. The instance count reflects both buckets, and archived rows keep their explicit-pubkey click so unarchive stays UI-reachable (the deliberate-navigation path above).The predicate is fail-open (treats every identity as live while the relay archive snapshot loads) and self-exempt, so a cold start never hides an identity and a user is never folded from their own client. While the snapshot is loading, every instance renders in the live list — nothing hidden, nothing labeled.