[upstream-sync] block/buzz 631b05c88..8342dfcc5 (33 commits) — migration 0027 renumbered to 0029 - #19
Merged
Merged
Conversation
## Summary - make mobile unread state visible with bold channel names, an animated Inbox badge, and swipe-to-toggle Inbox rows - add directional transitions for top-level mobile navigation - let mobile send while media uploads, with cancellable progress UI - normalize iOS and Android video uploads, attach poster frames, and improve native video playback ## Validation - `just mobile-check` - `just mobile-test` - `cargo test -p buzz-media` - Pixel smoke test - iPhone smoke test Desktop background uploads moved to block#4522 so the two platforms can be reviewed independently. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: Tom Brow <tomb@block.xyz> Co-authored-by: leader <71e9f2c44a6932b6772caaaccda1911d010463c3e2c6c40410b8329956046801@buzz.block.builderlab.xyz> Co-authored-by: Tom Brow <tomb@block.xyz>
…lock#2392) (block#4374) ## What Fixes block#2392 — the action cards in the empty-channel intro ("Create agent", "Add people") had their `focus-visible` ring clipped by the surrounding scroll container. ## Root cause The cards sit in a `flex ... overflow-x-auto pb-1` row. Setting `overflow-x` (without `overflow-y`) makes the browser compute `overflow-y: auto` as well, so the container clips anything painted outside its padding box — including the cards' `focus-visible:ring-2` box-shadow. With only `pb-1` padding, the top/left/right of the ring were cut off when Tabbing to a card. ## Change `desktop/src/features/messages/ui/ChannelIntroBlock.tsx` — `pb-1` → `p-1` on the action-cards scroll container, reserving 4px on all four sides so the focus ring renders fully inside the scroll container's padding box. - 1 file, 1 line. No behavior change for mouse users or layout. ## Verification - `pnpm typecheck` — clean - `pnpm exec biome check src/features/messages/ui/ChannelIntroBlock.tsx` — clean - `pnpm check:file-sizes` — clean - Desktop unit suite — **3906/3906 pass** Signed-off-by: Sarthak Singh <sarthak.singh@juspay.in> Signed-off-by: Sarthak Singh <sarthak.singh@juspay.in>
## Summary - send desktop messages immediately while media uploads continue in background state across channel navigation - show immediate progress above the composer and keep Jump to latest above it - report the real media stages as Preparing, Processing, Converting, Uploading, and Finishing - use Buzz's shared spinner during local media work, then switch to the real percentage when byte transfer begins - animate phase-label and status-suffix changes without overlap or layout jumps - keep cancel, progress fill, message publication, and community-reset behavior coordinated with the background task - use raw Tauri IPC for large browser files so renderer-side byte serialization does not block initial feedback ## Why Desktop previously blocked sending while attachments uploaded in the composer. Large videos could also pause the renderer before progress appeared, and the progress pill said Uploading while native media processing was still underway. This makes the initial response immediate and describes the work actually happening. ## Validation - `cd desktop && pnpm check` - `cd desktop && pnpm typecheck` - `cd desktop && pnpm test` (3,931 passed) - `cd desktop && pnpm exec vite build --mode e2e` - `cd desktop && pnpm exec playwright test tests/e2e/file-attachment.spec.ts --project=smoke` (11 passed) - focused native media tests (80 passed) - native Clippy with all targets and features - pre-push native suite (2,107 passed, 14 ignored; 3 diagnostics passed) Updated phase snapshots are included in the PR comments. Split from block#4512 so the desktop and mobile changes can be reviewed independently. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
## Summary - Make channel join/leave activity use the selected inline avatar-stack treatment. - Group related membership activity for one hour and preserve profile/overflow-name interactions. - Restore the virtualized day-divider handoff and align the sticky date behavior with the message timeline. ## Validation - `pnpm check` - `pnpm test` - `cargo test --manifest-path desktop/src-tauri/Cargo.toml` - Visual desktop screenshot captured with seeded membership activity --------- Signed-off-by: kenny lopez <klopez4212@gmail.com>
## Summary - Keep the Welcome composer prompt above the dock blur so it stays readable. - Remove blur from the prompt and persona-motion paths. - Cover the crisp, correctly layered banner in the onboarding browser test. ## Validation - `pnpm -C desktop exec biome check src/features/channels/ui/WelcomeComposerBanner.tsx tests/e2e/onboarding.spec.ts` - `pnpm -C desktop build:e2e` - `pnpm -C desktop exec playwright test tests/e2e/onboarding.spec.ts --grep "finishing onboarding creates starter channels and focuses welcome-everyone for a new member" --project=integration` --------- Signed-off-by: kenny lopez <klopez4212@gmail.com>
…ty + consumer cost guidance (block#4632) Amends `docs/nips/NIP-AM.md` with three normative publisher-behavior changes per the cleared Usage v2 plan (plan v3, D4 + D2'). ## Changes ### 1. Cache emission semantics (D4) Replaces the unconditional `MAY` with qualified obligations: - Publishers SHOULD emit `cacheReadTokens` / `cacheWriteTokens` when the provider exposes a cache component. - Publishers MUST preserve an explicit zero when the provider reports zero. - Publishers MUST omit the field (never null or fabricated zero) when that component is unavailable to the publisher — including when the provider supports it but the harness does not surface it. An explicit carve-out in both the JSON comment block and the Numeric-validity prose exempts these fields from the payload-wide null guidance. Omission is the only valid representation for an unavailable cache component. ### 2. Optional `pricingIdentity` field (D2') Adds an optional, non-nullable `pricingIdentity` object (`authority`, `model`, `cacheClass`), defined as billing authority — distinct from the transport `Provider` enum. - `authority` is a registered billing-namespace identifier: exact lowercase hostname, no scheme, no path, no trailing slash. Registered values: `api.anthropic.com`, `api.openai.com`, `openrouter.ai`. The set extends only by NIP amendment. Pricing lookup is an exact string match on `(authority, model)`. - Present only when the publisher can prove applicability: direct official-endpoint connections prove via the actually-requested resolved model; other routes MUST receive response-supplied authoritative billing identity. - MUST omit for custom/overridden base URLs, gateways (unless the gateway is the named billing authority), unresolved aliases, and turns where usage contributions carry more than one billing identity (including identity-bearing mixed with unresolved). - `cacheClass` is omitted (not null) when not applicable. - `pricingIdentity` is optional but not nullable — omission is the only absence representation. - The existing `model` field retains its non-billing semantics (configured/session model) and is never overloaded. - Consumers MUST treat omission as "price unknown" and MUST NOT infer a price from the session `model` field. ### 3. Consumer cost guidance (D4) - Consumers MAY recompute cost estimates using the billing identity and a pricing manifest. - Consumers MUST retain the provenance of any cost value (e.g. `manifest-estimated`, `wire-reported`). - Consumers MUST NOT merge manifest-estimated and wire-reported costs into an unlabeled total. Manifest-vs-wire display preference is application policy and deliberately excluded from this NIP. ## Scope Doc-only. Single file: `docs/nips/NIP-AM.md`. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
## Overview Agents running in Buzz have no built-in awareness that each channel is an isolated conversation context. When a human mentions work "you" are doing in another channel, the current session can misread this as its own active context and try to coordinate, re-plan, or take ownership of it — causing confusion and wasted turns. ## What changed Added a `## Session Model` section to `crates/buzz-acp/src/base_prompt.md`, inserted immediately after the opening paragraph and before `## Buzz CLI`. The section explains: - Each channel is a separate session; multiple sessions of the same agent identity may be active simultaneously. - Sessions share core memory, workspace, and relay — but not conversation context or in-flight reasoning. - Cross-channel work belongs to the owning session by default; the current session may take it over only when the human explicitly requests it. No runtime code changes. Base prompt only. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
## Why Buzz restores cached channels and messages before profile lookups complete. On launch, that briefly exposes pubkey-derived labels in place of familiar display names. ## What - Persist a bounded, relay-scoped cache of last-known display names, NIP-01 names, and NIP-05 handles - Seed batch profile queries from those labels immediately, while keeping them stale so the existing relay request revalidates them - Keep cached data presentation-only: avatars and ownership metadata are not persisted or used to seed profile-detail caches - Remove cleared or missing profiles, purge a relay's labels when its community is removed, and include the cache in local-storage quota recovery - Add unit coverage for parsing, bounds, eviction, malformed data, and cleared profiles - Add an E2E regression that delays the relay profile response and verifies the cached name is rendered first ## Risk Assessment Low. The cache is disposable, capped at 1,000 entries per relay, scoped by normalized relay URL, and always revalidated. It contains only public label fields and does not restore avatars, agent ownership, or authorization state. ## Verification - `just ci` - `pnpm typecheck` - `pnpm test` — 3,727 passed - `pnpm exec playwright test tests/e2e/channels.spec.ts --grep "cached profile labels"` — passed Generated with Codex
## Summary - Keep selected sidebar rows regular by default; manually unread rows become bold immediately. - Apply a clearer dark-mode hierarchy: standard inactive rows at 75%, muted rows at 45%, and unread rows at full emphasis. - Keep hover text color stable while retaining the selected-row and unread cues. ## Validation - `pnpm typecheck` - `pnpm build:e2e` - Playwright: sidebar badge and channel-mute coverage ## Screenshots Posted in the PR comments. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com>
) The "Restart required" badge reports that an agent's running config has drifted from its spawn-time config, but never says what changed. This ships the full feature: a typed Rust diff engine and a TS/UI layer that renders it at every badge site. ## Rust core (spawn-snapshot diff engine) Replaces the lossy `u64` `spawn_config_hash` with a typed `SpawnConfigSnapshot`. The snapshot is stamped from the already-resolved command/env/config values immediately before `spawn()`, closing the race window where a mid-spawn config edit would suppress the badge. `SpawnConfigSnapshot::canonical()` is the single JSON projection shared by the badge and the diff. Drift is `to_value(stamped) != to_value(current)`; the diff is a generic leaf walk over those same two values, so badge-on and diff-non-empty are structurally guaranteed. Adding a snapshot field reaches the UI with no code change to the diff engine — `mutation_table_covers_every_serialized_field` fails CI if a new field arrives without a mutation row. `eligible_restart_diff(persona_orphaned, Option<TrackedSpawnState>)` returns the final vector — snapshot walk entries plus a synthetic `adapter_availability` entry. It returns empty for an orphaned instance (spawning one would fail) and for agents with no tracked spawn state (never stamped, can never have drifted). `needs_restart = !restart_diff.is_empty()` derives from that vector and nothing else. Redaction policy (`policy_for(path)`) is shared by the wire diff and the snapshot's manual `Debug` via `is_safe_to_reveal()` from `managed_agents::env_vars` as the single authority for env-key masking: | Policy | Paths | Rendering | |---|---|---| | `Text` | `system_prompt`, `team_instructions` | character counts only | | `MaskedBare` | `args`, `relay_url` | `••••`, no suffix | | `MaskedSuffix` | non-allowlisted `env.*` | `••••` + last 4 chars when longer than 8 | | `Plain` | allowlisted `env.*` (`BUZZ_AGENT_THINKING_EFFORT`, `BUZZ_AGENT_PROVIDER`, `BUZZ_AGENT_MODEL`, `DATABRICKS_HOST/MODEL`) and everything else | verbatim | Default-deny: every env key not in the explicit allowlist stays masked. `is_safe_to_reveal()` is the single allowlist authority for both the baked-env display and the diff. `restart_diff` is omitted from the wire when empty (`skip_serializing_if`). ## TypeScript / UI layer New `restartDiff.ts` module defines `RestartDiffEntry`, `RestartChange`, `JsonValue`; `tauri.ts` and `types.ts` re-export and add `restart_diff` / `restartDiff` fields (Rust omission → `restartDiff: []`). **`RestartDiffBadge`** — hover tooltip capped at 6 entries + "and N more", `asChild` span trigger (never inside a `<button>`), auto-restart blurb below the diff list (on/off variant from `autoRestartEnabled` prop; same `AUTO_RESTART_ON_BLURB` / `AUTO_RESTART_OFF_BLURB` constants shared with the Runtime-tab banner). **`RestartDiffList`** renders the full uncapped list for the Runtime-tab banner with `tooltip`/`inline` presentation variants for correct foreground in both surfaces. **`ManagedAgentRow` B4 fix** — badge moved to a sibling `div` of the row expansion button; tooltip trigger has no `button` ancestor. **`UnifiedAgentsSection`** — both badge sites render `<RestartDiffBadge>` instead of a raw `<Badge>`, with `autoRestartEnabled` threaded from `agent.autoRestartOnConfigChange`. **Side-panel fix** — `RestartDiffBadge` rendered tab-independently in the `ProfileSummaryView` hero area (was Runtime-tab only — root cause of the ~50% inconsistency Will reported). Hero badge is `self-center` in the flex column. `ProfileRuntimeTabContent` early-return checks `needsRestart` so the banner is never dropped when all other content is empty. Auto-restart blurb in the Runtime-tab banner uses the shared constants. ## Wire shape ```jsonc "restart_diff": [ { "field": "model", "change": { "kind": "value", "before": "gpt-5", "after": "claude-4" } }, { "field": "system_prompt", "change": { "kind": "text", "before_chars": 1234, "after_chars": 1410 } }, { "field": "env.OPENAI_API_KEY", "change": { "kind": "masked", "before": "••••bc12", "after": "••••xyz9" } }, { "field": "env.BUZZ_AGENT_THINKING_EFFORT", "change": { "kind": "value", "before": "medium", "after": "high" } } ] ``` `added`/`removed` occur only for dynamic-map keys; nullable struct fields always serialize as `null`; arrays are atomic leaves (`args`, never `args.0`). ## Tests **Rust** — 1902 passing: snapshot mutation coverage, diff entry serialization, allowlist-aware env masking (`allowlisted_env_key_shows_plain_value`, `allowlisted_env_key_is_case_insensitive`, `non_allowlisted_env_key_stays_masked`), `unstamped_agent_yields_no_badge_and_no_entries` (both orphan values), `summary_without_drift_omits_restart_diff_from_the_wire`, `unstamped_availability_is_not_drift`. Clippy clean, fmt clean. **TypeScript** — `needs-restart-screenshots.spec.ts`: 11 E2E cases registered in the smoke project — all three badge sites, tooltip + keyboard focus, DOM no-button-ancestor assertion, 6+1 truncation, uncapped Runtime list, unknown field humanisation, side-panel badge on default Info tab, inactive/friendly-error Runtime opening path. Consolidates [block#3652](block#3652) Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
…lock#3976) ## Problem `Command+R` (webview reload) wipes the two in-memory refs driving sidebar channel unread badges: `observedUnreadEventsByChannelRef` and `latestByChannelRef`. The boot catch-up REQ can only fetch events newer than each channel's NIP-RS frontier, so thread replies that arrived before the frontier was passively advanced (the common case) are never re-discovered. Inbox is unaffected because it rebuilds candidates from a relay feed query and checks fine-grained `thread:`/`msg:` markers. The sidebar badge path lacks an equivalent recovery mechanism. ## Solution Persist the sidebar's per-event candidate set to localStorage as a disposable, versioned projection cache (`buzz-observed-unread.v1:<relay>:<pubkey>`) and hydrate it on boot before the catch-up REQ runs. ### New files **`observedUnreadStorage.ts`** — storage module for the cache: - Keyed `buzz-observed-unread.v1:<normalizedRelayUrl>:<normalizedPubkey>` (relay-scoped to prevent cross-community leakage, matching `threadActivityStorage`) - Stores validated per-event `ObservedUnreadEvent` rows; `latestByChannel` is derived at hydration — no divergent dual aggregate - Age pruning (7d = `READ_STATE_HORIZON_SECONDS`), per-channel cap (1000), global cap (5000) across all channels in a scope bucket - Payload `updatedAt` for LRU ordering; registered in `PURE_CACHE_KEY_PREFIXES` for 2 MiB eviction budget - Field-level validation on decode; write failure is non-fatal (session-only degradation) - Snapshot-owning timers: `scheduleObservedUnreadWrite` deep-clones the events map at schedule time — a late A-scope timer can never read B's mutable refs or write under B's key **`useObservedUnreadPersistence.ts`** — hook that owns all persistence lifecycle: - Scope fence: `normalized pubkey + normalized relay` identity; `isScopeLoaded()` callback guards both projection (`rawUnread`) and every **observed-cache mutation** (`recordUnreadEvent`, `removeChannel`, `clearAll`) before touching refs or storage. Note: stale-scope calls to `markChannelRead`/`markAllChannelsRead` can still affect `forcedUnreadRef` and NIP-RS markers, which are pre-existing on `main` and deferred to the NIP-RS arc (see Deferred below). - Synchronous `pagehide` flush closes the Cmd+R timing gap (`useReloadShortcut.ts` reloads within 500ms of teardown, before the 1-second debounce fires) - Identity-reset effect: flushes old scope, resets refs, hydrates from storage, stamps loaded scope — all atomic; cleanup flushes on unmount - `clearAll` cancels the pending timer, resets both in-memory refs, and clears storage in a single transactional operation; `removeChannel` deletes the channel from both refs and replaces any pending snapshot with the current full map — never cancel-without-replacement, preserving sibling-channel events on reload - Marker-prune effect on `readStateVersion`: evaluates each retained event with `observedUnreadEventReadAt()` (the same evaluator used by the projection memo) and removes covered events, rederiving per-channel latest — never clears a whole channel for a single thread/msg marker - Returns a stable `useMemo`-wrapped API object keyed on actual deps so unrelated re-renders do not restart the catch-up REQ - `isScopeLoaded` is a `useCallback` (not a memoized boolean) — always reads the ref at call time, never stale ### Modified files **`useUnreadChannels.ts`** — hook integration: - Calls `useObservedUnreadPersistence` with all persistence wired through the returned API - `rawUnread`: `isScopeLoaded()` guard suppresses A-scope refs from projecting under B - `recordUnreadEvent`: `isScopeLoaded()` fence before touching refs; schedules a debounced write on each successful record - `markChannelRead` clearObserved path: calls `removeChannel` so the cleared state survives reload - `markAllChannelsRead`: delegates to the owner's fenced `clearAll` — the parent does not reset the observed refs directly; `clearAll` owns the transactional clear of both refs and storage, preventing a stale scope-A callback from corrupting scope B **`localStorageQuota.ts`** — registers `buzz-observed-unread.v1:` in `PURE_CACHE_KEY_PREFIXES` ## Design constraints The cache is a **disposable projection**: versioned key, read-through only, safe to delete wholesale. It does not touch `ReadStateManager`, marker semantics, or `forcedUnreadStore`. Zero overlap with the NIP-RS manual mark-read/unread protocol work in progress in another channel; migration path when that lands is "stop reading the key." ## Test coverage **`observedUnreadStorage.test.mjs`** covers storage primitives: - Key normalization, relay-scoped isolation, round-trip correctness - Age-prune and per-channel cap on read and write; global cap across channels - `deriveLatestByChannel` correctness - Thread-marker prune leaves sibling thread events persisted and lit - Scope-isolation state machine: A rows visible in A, absent in B, restored on A again; late A-scope write does not overwrite B's bucket - Malformed structures/fields, relay/pubkey isolation, quota failure degradation **`useObservedUnreadPersistence.test.mjs`** exercises the real hook via `createRoot` + `act`: - pagehide flush: event recorded within debounce window survives reload (headline regression) - Unmount with pending write flushes before teardown - `clearAll` cancels pending debounce so no resurrection after reload - `removeChannel` replaces pending snapshot so sibling channel B survives reload (two-channel repro) - Marker prune: thread and channel markers prune covered events; sibling channels survive - `isScopeLoaded` returns false before identity-reset effect commits, true after - A→B scope switch: pending A-timer is cancelled by flush, A data persisted synchronously (hydration round-trip) - Stale `clearAll` from scope A rejects after scope B loads (observed-cache scope fence) - Stale `removeChannel` from scope A rejects after scope B loads (observed-cache scope fence) - API object identity stable across unrelated re-renders (catch-up stability) **`useUnreadChannels.test.mjs`** exercises the full parent-to-owner seam with real hook mounts: - Stale `markChannelRead` from scope A does not corrupt B's observed bucket after flush - Stale `markAllChannelsRead` from scope A does not overwrite B's bucket after flush ## Deferred Issues deferred to the NIP-RS arc (`#unread-messages-ux`) or future hardening — not regressions introduced by this PR: - **Stale-scope `forcedUnreadRef` / `markContextRead` exposure**: a stale scope-A `markChannelRead` or `markAllChannelsRead` still deletes B's `forcedUnreadRef` entries and advances B's NIP-RS markers via `markContextRead` before the observed-cache fence rejects. This is pre-existing on `origin/main` (identical shape at lines 316/330). Fix requires touching `forcedUnreadStore` and marker paths — out of scope for Fix A. Deferred to the NIP-RS work. - **`isScopeLoaded` empty-scope hardening**: `isScopeLoaded()` returns `true` when `pubkey` and `relay` are empty strings (no active session). A guard could assert non-empty identity before stamping scope-loaded. Low risk in practice since the hook is only mounted after auth, but could be tightened. - **Catch-up batch scheduling**: `handleChannelMessage` and the catch-up loop each clone the full events map per event via `scheduleObservedUnreadWrite`. For channels with large backlogs this produces O(n) snapshot clones per catch-up batch. A batch-schedule API (single snapshot at end of batch) would reduce allocations. Not observable in normal use; deferred as a performance optimization. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
## Summary - Separate direct invites from link sharing with a labeled divider. - Show the generated invite URL inline with truncation and a copy control. - Use shared loading feedback and a restrained copy-status resize. ## Validation - `pnpm -C desktop exec playwright test tests/e2e/invite-link-copy.spec.ts tests/e2e/invites-settings-screenshots.spec.ts` (7 passed) --------- Signed-off-by: kenny lopez <klopez4212@gmail.com>
Replace the stale `agent_command_override` drop logic in `apply_persona_snapshot` with a three-tier canonical command resolver. ## What this fixes The old code dropped a create-time harness pin when the persona switched to a different runtime, but it had two failure modes: 1. **Preset harnesses invisible.** `known_acp_runtime_exact()` only searches `KNOWN_ACP_RUNTIMES` (builtins). Preset harnesses such as OpenClaw live in `PRESET_HARNESSES`, so the destination lookup returned `None` and the outer `if let` branch never executed — a Goose→OpenClaw persona switch left the stale Goose override in place, keeping the agent running Goose instead of OpenClaw. 2. **Pin-side canonical resolution incomplete.** The pin was resolved by `known_acp_runtime()`, which searches by id/command/alias and returns a `&KnownAcpRuntime` entry correctly. However, if the *pin* named an alias (e.g. `claude-code-acp`) and the *destination* was a preset harness absent from builtins, the outer guard still failed for the same reason as (1). The alias regression test pins the requirement that the canonical resolver must handle both sides: alias pins must be recognised and drops must fire when the destination is a known preset. ## How it works now `canonical_harness_command(input)` accepts any form a stored override can take — bare command, alias, path prefix, or runtime id — and resolves it to the harness primary command through three tiers: 1. **Builtins** — `KNOWN_ACP_RUNTIMES`, matched by id/command/alias. 2. **Static presets** — `PRESET_HARNESSES`, matched by id or normalised command. 3. **Loaded registry** — custom/preset definitions loaded at runtime. `command_for_runtime_id` (id-only input, same three tiers) replaces the two-step `known_acp_runtime_exact`/`lookup_loaded_harness_by_id` pattern in `record_agent_command`, `effective_agent_command`, and `try_record_agent_command`, adding the static preset tier so preset harnesses resolve correctly even without a warm registry. ## Changed files - `discovery/presets.rs` — `preset_command_for_id`, `command_for_runtime_id`, `canonical_harness_command` - `discovery.rs` — re-export new functions; make `normalize_command_identity` `pub(crate)`; refactor three command-resolution functions to use `command_for_runtime_id` - `custom_harnesses.rs` — `loaded_harness_registry` visibility `fn` → `pub(super)` (needed by `canonical_harness_command`) - `persona_events.rs` — replace two-step `known_acp_runtime_exact`/`known_acp_runtime` + pointer comparison with canonical-command comparison - `persona_events/stale_pin_tests.rs` (new) — four regression tests: Goose→OpenClaw drop, OpenClaw→Goose drop, claude-code-acp alias→OpenClaw drop, same-harness path keep - `persona_events/tests.rs` — `sample_record`/`sample_persona` exposed as `pub(super)` for the new test module Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1g8493u0xfsjrvflg4n08ezd7vec99mnwzlv0qgwpr9d7gvjwhuzqx59rhw <41ea58f1e64c243627e8acde7c89be667052ee6e17d8f021c1195be4324ebf04@buzz.block.builderlab.xyz>
…k#4647) ## Problem `SELECT id, community_id FROM channels WHERE id = ANY($1) AND deleted_at IS NULL` is the top **Load by waits (AAS)** on the Buzz Postgres writer. Two independent causes compound, and both are fixed here. ### 1. No index can serve it `channels` is `PRIMARY KEY (community_id, id)`, and every secondary index leads with `community_id`: | Index | Columns | |---|---| | *(primary key)* | `(community_id, id)` | | `idx_channels_nip29_group` | `(community_id, nip29_group_id)` | | `idx_channels_dm_hash` | `(community_id, participant_hash)` | | `idx_channels_community_type` | `(community_id, channel_type)` | | `idx_channels_community_visibility` | `(community_id, visibility)` | | `idx_channels_created_by` | `(community_id, created_by)` | | `idx_channels_ttl_expiry` | `(ttl_deadline)` *(partial)* | The two tenant-independent lookups carry **no `community_id` predicate** — deliberately: - `Db::communities_of_channels` — `WHERE id = ANY($1) AND deleted_at IS NULL` - `Db::community_of_channel` — `WHERE id = $1 AND deleted_at IS NULL` That independence is load-bearing, not an oversight: projecting a row's *true* owning community regardless of the fetch query's `WHERE` clause is what makes `Inv_NonInterference` non-vacuous. If the fetch ever dropped its tenant scoping, this lookup would still report the real label and the checker would catch the mismatch. But a composite btree is only usable when its leading column is constrained, so neither query can use the primary key, and nothing else leads with `id`. **Both sequentially scan `channels` on every call.** ### 2. In production the result is discarded Both call sites feed `record_read_message_rows` / `record_read_by_id_rows`, which call `tracer.record(...)`. Production binds `NoopTracer` (`crates/buzz-relay/src/state.rs`), whose `record` body is empty. The existing guard tests `trace_state`, which is `Some` for every well-formed request — it only goes `None` on malformed pubkey bytes. So the scan ran on the hot read path and its output was dropped. This is the classic eager-argument bug: `log.debug("..." + expensiveCall())` with no `isDebugEnabled()` check. ### 3. Multiplied per filter The non-search call site sits **inside the phase-3 per-filter loop**, so a `REQ` carrying N filters performed N sequential scans of `channels` before responding. ## Changes **`Tracer::enabled()`** — a capability check on the trait (the `isDebugEnabled()` of this seam), defaulting to `true`. `NoopTracer` overrides it to `false`, and both emitters in `req.rs` now gate on it, skipping the trace-only DB read entirely in production. **`migrations/0027_channels_id_lookup_index.sql`** ```sql CREATE INDEX IF NOT EXISTS idx_channels_id_live ON channels (id) INCLUDE (community_id) WHERE deleted_at IS NULL; ``` - `INCLUDE (community_id)` — both queries select exactly `(id, community_id)`, so this is covering and can be served index-only. - Partial on `deleted_at IS NULL` — matches both predicates exactly, excludes soft-deleted history, and lets Postgres skip the recheck. - **Not `UNIQUE`.** `id` alone is *not* unique in this table — `command_executor.rs` documents that `community_of_channel(channel_id)` is ambiguous because the same channel id can appear under more than one community. A unique index would encode a false constraint and fail to build on any database already holding such a pair. Worth keeping the index even though fix #1 removes the production caller: it still runs under conformance, and `community_of_channel` has the same problem on its own paths. **`schema/schema.sql`** — mirrored, since a test asserts desired-state parity. ## Conformance is unchanged This is the part worth reviewing closely. Under a real tracer `enabled()` returns `true` and **every emit happens exactly as before** — the gate only skips *building* emit inputs when nothing observes them, never an emit that would otherwise have been made. The coverage-breach guard stays non-vacuous. `CountingTracer` forwards `enabled()` to its inner tracer rather than inheriting the `true` default. Both directions matter and both fail silently: - inheriting `true` over a `NoopTracer` would keep the overhead this PR removes; - hardcoding `false` over a live tracer would suppress the emits whose absence `EmitGuard` reports as `ImplBug` — masking real breaches behind expected ones. Covered by a new regression test, `counting_tracer_delegates_enabled_to_inner`, which asserts delegation in both directions. ## Verification - `cargo check -p buzz-conformance -p buzz-relay` — clean - `cargo clippy --all-targets` — clean, zero warnings - `cargo test -p buzz-conformance` — 6/6 - `cargo test -p buzz-relay --lib conformance` — 11/11 - `cargo test -p buzz-db --lib migration` — 7/7 - `just test-unit` (pre-push) — green Migration-count assertions in `crates/buzz-db/src/migration.rs` were bumped 26 → 27, with content assertions for 0027 following the existing per-migration pattern (including a guard that it never becomes `UNIQUE`). ## Open questions for reviewers 1. **Lock strategy.** Built *without* `CONCURRENTLY`, following migration 0004's precedent, because sqlx runs each migration inside a transaction and `CREATE INDEX CONCURRENTLY` cannot run in one. This takes a brief `SHARE` lock on `channels` (blocks writes, not reads) — small relative to `events`, but an operator preferring zero write-blocking can pre-build it by hand and `IF NOT EXISTS` makes the migration a no-op. I could not confirm whether sqlx 0.9 supports a `-- no-transaction` directive; if it does, that may be preferable. 2. **Diagnosis is static.** This comes from reading the source, not from `EXPLAIN` against the live database. Worth confirming with `EXPLAIN (ANALYZE, BUFFERS)` on the writer before/after — that also sizes the win by revealing the real table size and row counts. 3. **Expected impact** scales with average filters-per-`REQ`, which I did not measure. `pg_stat_statements` ordered by `total_exec_time` would confirm this query drops off the top and show whether anything else is scanning the same way. Signed-off-by: Jemiah Westerman <jemiah@squareup.com>
## Summary - replace Buzz Term's full-app takeover with a resizable bottom dock inside the channel content surface - add a discoverable channel-header button plus hide and maximize/restore controls - create PTYs lazily and keep separate, persistent terminal workspaces per channel - capture immutable channel/thread context on every terminal session ## Multiple-channel behavior The dock is a single surface, but its tabs are partitioned by channel. Switching channels swaps to that channel's sessions without terminating background PTYs; returning restores them. New tabs capture the currently visible channel/thread context. ## Verification At commit `7ca087f8e08c80528387684364a65bf4ccd6315f`: - `pnpm --dir desktop typecheck` - `pnpm --dir desktop test` — 4,129 passed - pre-push repository hooks — desktop check/test, Tauri checks, terminal Rust suites all passed --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> Co-authored-by: kenny lopez <klopez4212@gmail.com>
…ock#4737) > Opened by Brain (agent) on behalf of @wesbillman. ## Problem Users report the desktop app doesn't reliably reconnect and can wedge in states where only CMD+R (or a full restart) restores connectivity (thread `c2205e2b` in #desktop-reconnecting). Pinky's empirical light-switch matrix (real `buzz-relay`, SIGTERM/1012 + SIGKILL × 1s/45s/3min, at `f18a9cb10`) passed 4/4 — the backoff state machine recovers cleanly from ordinary relay loss. That isolates the user-stuck states to four special cases a reload resets but the auto flow never did. ## Fixes | Gap | Change | |---|---| | **G1** — recovery rode solely on the backoff timer (max 30s), throttled by WKWebView in occluded/background windows; nothing fired on network return or wake | New `useRelayResumeTriggers`: `online`, window focus, and visibility→visible call `preconnect()` when the session is `reconnecting`/`stalled`, rate-limited to one attempt per 5s (`relayResumeTriggerPolicy.ts`). Deliberately inert for the terminal `disconnected` state. | | **G2** — any AUTH `OK false` latched the session terminal forever, though the relay also rejects for transient causes (duplicate-AUTH "already authenticated" race, ±60s clock skew, fail-closed allowlist DB errors) | New `AuthOkTracker` (`relayAuthPolicy.ts`): "already authenticated" resolves as success; transient rejections retry with normal backoff; latch only on `restricted:` or after 3 consecutive rejections. | | **G3** — an `auth-required:` CLOSED (REQ racing AUTH after reconnect) permanently deleted the live subscription with no UI signal — frozen channel while state reads "connected" | Reclassified `auth-required:` as retryable in `relayClosedPolicy.ts`. Genuinely terminal classes (`restricted:`, `invalid:`, …) still delete. Can't loop: a truly unauthenticated session latches terminal at the connection level. | | **G4** — `useRelayAutoHeal` observed the 2s-debounced connection hook, so sub-2s flaps never triggered the heal even though `resetConnection` had already rejected every in-flight query | Auto-heal now observes the raw connection-state emitter. The existing 15s heal rate-limit still guards against flap storms. | Each fix is a colocated pure-policy module + unit tests, matching the existing `relayReconnectPolicy`/`relayClosedPolicy` pattern. ## Validation - Full desktop unit suite: **4151 pass, 0 fail** (at branch tip, `pnpm -C desktop test`) - `pnpm -C desktop typecheck` and `pnpm -C desktop check` clean (file-size ratchet respected — `relayClientSession.ts` net −2 lines despite the tracker wiring) - Evidence trail: `RESEARCH/DESKTOP_RECONNECT_CMDR_GAP_AUDIT.md` (audit), `RESEARCH/DESKTOP_RECONNECT_LIGHT_SWITCH_RESULTS.md` (Pinky's matrix) ## Not covered / follow-ups - Native macOS sleep-wake was not automated (would kill the harness session); G1's focus trigger is the mechanism that covers wake in practice, but a manual sleep-wake verification on a real build is worthwhile. - G3 terminal-CLOSED classes (`restricted:` etc.) still silently delete subs with no UI signal — surfacing that is a separate UX decision. - Stall-watchdog latency (60s idle + 10s check) left unchanged; G1 triggers largely mask it. --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Signed-off-by: npub1gjuws2a2dc8z2nszprtg7v6u9q7ffeah3hgl5yx45jwn7y7aqs6s5e9xj6 <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@buzz.block.builderlab.xyz> Co-authored-by: npub1yxv5wk0u0fh6dwt925wntn7h397jvteyj4r87ttcd9xae7n2t3lqqj9jmm <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@buzz.block.builderlab.xyz> Co-authored-by: npub1gjuws2a2dc8z2nszprtg7v6u9q7ffeah3hgl5yx45jwn7y7aqs6s5e9xj6 <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@buzz.block.builderlab.xyz>
## Summary - stop retrying remote read-state publishes after the local replacement blob exceeds NIP-44's 65,535-byte plaintext limit - preserve every local read marker and leave existing relay state untouched rather than truncating remote state - keep incoming remote read-state available while suppressing further invalid publishes for the manager lifetime ## Why A repaired/reconnecting relay exposed a 1,404-context read-state on iOS. The app repeatedly serialized and attempted to encrypt that structurally oversized blob while reconnect catch-up work was running, saturating Flutter's debug UI isolate and making channel navigation take roughly ten seconds. This is intentionally fail-closed and behavior-preserving: local read behavior continues, but remote publishing pauses until the manager is recreated. No protocol or persisted-data format changes. ## Verification - `flutter test` — 1,093 passed, 1 skipped - `flutter analyze` — no issues - pre-push `mobile-test` and `branch-skew` hooks passed at `0b6423c5d4d583194f0bbe69662912133b9ae1ef` - independent review by Princess Donut: no blocking findings; compatibility-safe and correctly fail-closed Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
) ## Overview Both local archive settings — "Archive my agents' observer frames" (kind 24200) and "Archive my agents' turn metrics" (kind 44200) — previously defaulted to OFF in OSS builds, controlled by build-time env vars. This had an irreversible cost: observer frames are ephemeral (not stored by the relay), so any missed events are permanently unrecoverable. This PR makes both settings default to enabled for all builds and removes the build-time flag machinery entirely. ## What changed ### Rust - `observer_archive_default_enabled()` — returns `true` unconditionally; removed `option_env!("BUZZ_DESKTOP_BUILD_OBSERVER_ARCHIVE_DEFAULT")` check and `nest_is_dev()` runtime fallback. - `agent_metric_archive_default_enabled()` — returns `true` unconditionally; removed `option_env!("BUZZ_DESKTOP_BUILD_AGENT_METRIC_ARCHIVE_DEFAULT")` check and its OSS-build test. - `build.rs` — removed both `rerun-if-env-changed` declarations (`BUZZ_BUILD_OBSERVER_ARCHIVE_DEFAULT`, `BUZZ_BUILD_AGENT_METRIC_ARCHIVE_DEFAULT`) and the two baked-env emitting blocks. ### Build / CI - `Justfile` — removed `desktop-tauri-test-compiled-flags` recipe (the dual-compile test machinery). - `.github/workflows/ci.yml` — removed the "Desktop Tauri compiled-flag verification" CI step. ### TypeScript - `useObserverArchiveSeed.ts` — removed `observerArchiveDefaultEnabled` dep from `ObserverArchiveSeedDeps` and the `policyOn` gate in `reconcileObserverArchive`; the function now unconditionally calls `mergeSaveSubscriptionKinds`. - `useAgentMetricArchiveSeed.ts` — removed `agentMetricArchiveDefaultEnabled` dep from `AgentMetricArchiveSeedDeps` and the `defaultOn` flag-check path in `maybeSeed`; the `hasExplicitChoice` guard is preserved as the sole gate against re-seeding. - `LocalArchiveSettingsCard.tsx` — removed `policy` prop, `observerPolicy` state, and `observerArchiveDefaultEnabled` fetch from `ObserverArchiveSection`; toggle is now always enabled (just `toggling` disables it); removed the stale "Always on for internal builds" copy branch; removed the `observerPolicy !== false` guard from `handleObserverToggle`. - `tauriArchive.ts` — updated JSDoc on both default-enabled functions to reflect always-true. - `e2eBridge.ts` — changed both mock defaults from `?? false` to `?? true` so E2E tests without an explicit mock override exercise the real default behavior. ### Tests - `useObserverArchiveSeed.test.mjs` — replaced `policyOn` dep with direct merge dep; updated `test_oss_policy_off_no_merge` → `test_reconcile_always_seeds_24200`; all cancellation, identity-switch, and ordering tests adapted. - `useAgentMetricArchiveSeed.test.mjs` — removed `defaultOn` dep and `test_oss_build_does_not_seed`; updated `test_internal_build_unset_seeds_*` → `test_default_enabled_*`; `hasExplicitChoice` guard tests unchanged. ## Preservation of explicit opt-outs Users who have previously toggled the setting off are unaffected: - `useAgentMetricArchiveSeed` skips seeding when `hasExplicitChoice(pubkey)` returns true (localStorage-persisted per identity). - Observer archive reconciliation now unconditionally calls `mergeSaveSubscriptionKinds`, but a user who already deleted the subscription can turn it off via the Settings toggle, which calls `removeSaveSubscriptionKind` — this is the existing explicit opt-out path, and the toggle is now always enabled (not locked by a policy flag). ## Result - No `BUZZ_BUILD_*_ARCHIVE_DEFAULT` / `BUZZ_DESKTOP_BUILD_*_ARCHIVE_DEFAULT` references remain outside CHANGELOG/history. - Desktop node tests: 4168 pass, 0 fail. - `just desktop-tauri-check`: clean. - `just desktop-tauri-test`: all pass. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
## Summary - add a visible Stop control for interrupting agent speech - make push-to-talk available by default while preserving manual mute controls - refine agent management, muted audio states, drawer layering, and return navigation - suppress duplicate notification sounds for Huddle messages ## Why Huddles could trap users behind long agent speech, hide useful agent controls, and leave temporary Huddle state visible after the call. The drawer also regressed when the terminal substrate began painting behind the rounded app surface. ## Validation - `just desktop-ci` - focused Huddle Playwright coverage for the drawer, speech interruption, agent picker, and leave navigation --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
## Summary - remove the fractional half-pixel translation from custom reaction emoji - preserve the existing 28px reaction pill, 14×14 glyph box, and `object-fit: contain` - add real-app Playwright coverage for integer centering and non-square intrinsic dimensions ### Related issue None found. Follow-up to the Buzz emoji-warp investigation. ### Testing - `cd desktop && pnpm exec playwright test tests/e2e/custom-emoji.spec.ts --project=smoke` (15 passed) - `cd desktop && pnpm test` (4,171 passed) - `cd desktop && pnpm lint` (passed; two pre-existing informational `useTemplate` diagnostics) - `cd desktop && pnpm typecheck` (passed) - `cd desktop && pnpm exec biome check src/features/messages/ui/MessageReactions.tsx tests/e2e/custom-emoji.spec.ts` (passed) Independent review also mutation-tested the regression coverage by restoring the half-pixel transform and confirming the new test fails. No after screenshot is included because the patch preserves dimensions and fixes subpixel raster alignment; the real-app test asserts the mechanism directly. Validated at `bc95969b21b58d83b7f94de4ad25e499e52b35fb`. Signed-off-by: Kalvin Chau <kalvin@block.xyz> Co-authored-by: npub122y0pqkertljmedu303rl0aqrj3w8pvu43t6jxm6875lzg6f2pwqegc3xc <5288f082d91aff2de5bc8be23fbfa01ca2e3859cac57a91b7a3fa9f12349505c@buzz.block.builderlab.xyz>
## Summary - keep the first-open Buzz Term splash pending until the active PTY delivers its first frame - retrigger the splash effect when that readiness gate changes - cover the real bootstrap path so startup latency cannot consume the animation invisibly ## Verification - Wes manually verified the first-open animation in the worktree - `pnpm --dir desktop typecheck` - `pnpm --dir desktop test` — 4,195 passed - `pnpm exec biome check src/features/terminal/TerminalBootstrap.tsx src/features/terminal/TerminalSubstrate.tsx src/features/terminal/TerminalBootstrap.test.mjs` - pre-push hooks — branch skew, desktop check, and 4,195 desktop tests passed The repository-wide `pnpm --dir desktop check` still reports pre-existing diagnostics in `personaCatalogRelay.test.mjs` and `terminal.css`; the three changed files pass Biome directly. Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
…ock#4792) ## Summary Increases three Playwright assertion timeouts in `tests/e2e/empty-edit-delete.spec.ts` from 5s to 10s to fix a shard-composition flake introduced by PR block#4694. ## Root Cause PR block#4694 added `huddle-transcription.spec.ts` (477 lines, 22+ tests) to the Desktop Smoke E2E suite, shifting shard 2 composition so that `empty-edit-delete` now runs with significantly more accumulated browser state. The three affected assertions all wait for a React state update triggered by pressing Enter in edit mode: - `alertdialog` becoming visible after an empty edit (tests 1 and 2) - `edit-target` hiding after a successful non-empty edit (test 3) These transitions go through the React scheduler. In isolation they complete in milliseconds. In a loaded headless shard with accumulated GC pressure, the 5s window became insufficient — test 3 failed 3/3 times in CI run [30946444168](https://github.com/block/buzz/actions/runs/30946444168) with `edit-target` still visible after Enter. No product code is changed. The empty-edit-delete flow is correct and untouched by block#4694. This is a test-environment timing adjustment only. ## What Changed - `tests/e2e/empty-edit-delete.spec.ts` — three `{ timeout: 5_000 }` → `{ timeout: 10_000 }` for the post-Enter React-update waits ## Validation - `just desktop-check` — passed - `just desktop-test` — 4194 passed, 0 failed Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
## Summary - derive the current desktop package version in the release cache-key contract test - mutate that version in both `Cargo.toml` and `Cargo.lock` instead of assuming `0.5.4` - prevent desktop release version bumps from failing generic CI ## Context PR block#4788 bumped Desktop to `0.5.5`, exposing the hard-coded fixture. The dedicated release candidate check passed, while generic CI failed with `desktop version changed cache key`. ## Verification - pre-commit hooks passed - pre-push hooks passed - CI will validate the full contract Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
## Summary - adopt the finalized NIP-MP project model so one project can enumerate and switch between multiple NIP-34 repositories - add project and repository navigation, activity summaries, existing-repository attachment, and repository access-channel management - preserve privacy-safe activation provenance for agent-authored patches, pull requests, issues, and associated commits ## Test plan - [x] Run desktop typecheck and unit tests - [x] Run focused NIP-MP, repository access, and provenance tests - [x] Run Rust formatting and desktop lint checks - [x] Run the complete pre-push suite after merging current `main` - [ ] Manually verify project creation, repository attachment, switching, and access repair on staging - [ ] Manually verify public-channel and private-agent origin labels on newly created Git activity Related: [block#4695](block#4695) --------- Signed-off-by: Thomas Petersen <thomasp@squareup.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
## Buzz Desktop release v0.5.5 - **Frozen main:** `383d9e1eafd569b44b9c835200dba69ef7cec9dc` - **Reviewed candidate:** `ac589061ef1009f55384536e483cfe9b1260697b` - **Previous desktop release:** `desktop-v0.5.4` - **Proposed immutable tag:** `desktop-v0.5.5` This PR must be **squash merged** only after the Desktop Release Candidate check passes. The branch must remain based directly on current `main`; stale base, payload drift, incomplete notes, or an unauthorized merge produce no tag. The checked-in changelog accounts for every non-merge commit in the release range. Publication remains bound to the immutable candidate tag. Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Release Automation <release-automation@users.noreply.github.com>
## Summary - serialize native `openChannel` tray actions with the camelCase field names consumed by the TypeScript frontend - prevent a valid tray channel ID from becoming `/channels/undefined` - add a Rust serialization contract test covering the complete frontend payload shape ### Root cause `TrayAction` renamed the enum variant to `openChannel`, but its struct fields still serialized as `channel_id` and `community_generation`. The frontend reads `action.channelId`, so tray navigation called `goChannel(undefined)`. ### Testing - manually verified the corrected runtime payload and tray navigation before removing temporary logging - `just desktop-ci` - pre-push hooks (desktop checks/tests, Tauri checks, and Rust tests) --------- Signed-off-by: Kalvin Chau <kalvin@block.xyz> Signed-off-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz> Co-authored-by: npub122y0pqkertljmedu303rl0aqrj3w8pvu43t6jxm6875lzg6f2pwqegc3xc <5288f082d91aff2de5bc8be23fbfa01ca2e3859cac57a91b7a3fa9f12349505c@buzz.block.builderlab.xyz> Co-authored-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
…repos, PRs, and issues (block#4695) ## Summary Gives Buzz-hosted git entities the same "GitHub-style" chat experience GitHub links already get: rich preview cards, real titles, and click-through — except clicks navigate **in-app** to the Projects view instead of a browser. - **Spec**: `docs/buzz-entity-links.md` — link scheme, slices, and deferred work (`buzz://project`, OS deep links, web routes). - **Canonical `buzz://` deep links**: new `desktop/src/shared/lib/entityLink.ts` with builders + strict parser for `buzz://pr?id=…&owner=…&d=…`, `buzz://issue?…`, and `buzz://repo?owner=…&d=…`, mirrored by a Rust module (`crates/buzz-cli/src/links.rs`) with a shared golden-format test so the two implementations can't drift. - **Preview cards**: `linkPreview.ts` recognizes `buzz://` entity links *and* HTTPS relay clone URLs (`{origin}/git/<pubkey>/<repo>`, the shape agents paste today). Both normalize onto the canonical `buzz://` href, so the two spellings of a repo dedupe to one `Buzz`-provider card (`BuzzMark` logo) rendered by `link-preview-attachment.tsx`. - **Title enrichment**: PR/issue cards fetch the real subject from the relay event (`subject` tag or first content line) via `useResolvedLinkPreviews.ts`; the cache is community-scoped and reset in `resetCommunityState()`. - **In-app navigation**: clicking a card or inline anchor (including HTTPS relay clone URLs whose origin matches the active relay) routes to the canonical `30617:<owner>:<d>` coordinate via `goProject()` (`markdown/entityLinks.tsx`). **Merge dependency: block#4671 must merge first** — route resolution for `30617:` coordinates is implemented on that branch (`feat/multi-repository-projects`). Entity-link and external-anchor logic were extracted out of `markdown.tsx` to stay under the file-size ratchet. - **Agent side**: `buzz pr open`, `buzz issues create`, and `buzz repos create` now return a ready-made `link` field (omitted when the relay returns `accepted: false`), and `base_prompt.md` instructs agents to paste it verbatim when announcing work. ## Test plan - [x] Desktop unit tests: pass, including new `entityLink.test.mjs` and `linkPreview.test.mjs` coverage (golden formats, malformed-link rejection, clone-URL/`buzz://` dedupe, origin-gated anchor behavior, label-must-win invariant, cache epoch) - [x] Rust: `cargo test -p buzz-cli` golden-format test + accepted/rejected link guard assertions, clippy + fmt clean - [x] Biome + `tsc --noEmit` clean; pre-push hooks (desktop-tauri-checks, rust-tests, desktop-test) pass - [ ] Manual: paste a relay clone URL and a `buzz://pr` link in a channel — verify one card each, real PR title, and in-app navigation to the Projects view Related: [block#4671](block#4671) --------- Signed-off-by: Thomas Petersen <thomasp@squareup.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1g8493u0xfsjrvflg4n08ezd7vec99mnwzlv0qgwpr9d7gvjwhuzqx59rhw <41ea58f1e64c243627e8acde7c89be667052ee6e17d8f021c1195be4324ebf04@buzz.block.builderlab.xyz> Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
## Summary - preserve Databricks catalog 401 responses as authentication failures and retry discovery exactly once after silently refreshing the rejected bearer - preserve runtime OAuth recovery: when discovery has no usable OAuth credential, `session/new` succeeds with only the trimmed configured model so the first `session/prompt` can run the existing browser PKCE flow - reject a rejected configured `DATABRICKS_TOKEN` with actionable, non-interactive guidance; static credentials cannot recover through PKCE - use the configured-model fallback for non-auth discovery failures without caching failed or fallback catalogs, so later sessions retry discovery - keep known Databricks v2 models only for authenticated empty-catalog responses and mark their provenance - resolve discovery before MCP spawn or session registration, preventing failed discovery from leaking resources or consuming session capacity - permit serialized interactive PKCE only from the explicit saved-agent model picker; passive draft discovery never opens a browser ## Runtime flow 1. OAuth discovery attempts cached credentials and silent refresh without opening a browser. 2. If no usable OAuth bearer exists, `session/new` advertises only the configured model and succeeds. 3. The first `session/prompt` uses `TokenSource::bearer()`, which may launch browser PKCE. 4. A later session retries discovery and caches only the authenticated catalog. ## Regression coverage - rejected-but-locally-fresh OAuth bearer performs one refresh and one catalog retry - OAuth mode with no cached token allows `session/new` and returns exactly the trimmed configured model - the OAuth fallback is not cached; a later authenticated session retries discovery and caches the returned catalog - rejected static tokens still reject `session/new` - failed discovery does not consume the sole session slot or spawn the supplied MCP process - Desktop interactive/passive auth intent, static-token redaction, and authenticated empty-catalog provenance ## Verification - `cargo test -p buzz-agent` - `cargo test --manifest-path desktop/src-tauri/Cargo.toml --lib commands::agent_models` - `cargo clippy --manifest-path desktop/src-tauri/Cargo.toml --all-targets -- -D warnings` - `cargo fmt --all -- --check` - `git diff --check` - full pre-push hooks ## Review Adversarial review found and drove fixes for session/MCP resource leakage, duplicate concurrent PKCE flows, sensitive error propagation, incorrect 403 reauthentication, missing discovery-level coverage, passive browser launch, and the Desktop file-size ratchet. The final follow-up preserves the existing prompt-time OAuth flow while retaining static-token rejection and pre-allocation discovery ordering. --------- Signed-off-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz> Co-authored-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
## Buzz Desktop release v0.5.5 - **Frozen main:** `4a2305170eef565bf1836e2859247e67c030f8af` - **Reviewed candidate:** `2d03d37b05b68186b2caad9da79080032be3ac72` - **Previous desktop release:** `desktop-v0.5.4` - **Proposed immutable tag:** `desktop-v0.5.5` This PR must be **squash merged** only after the Desktop Release Candidate check passes. The branch must remain based directly on current `main`; stale base, payload drift, incomplete notes, or an unauthorized merge produce no tag. The checked-in changelog accounts for every non-merge commit in the release range. Publication remains bound to the immutable candidate tag. Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Release Automation <release-automation@users.noreply.github.com>
## Summary - handle Cmd+Shift+V on macOS and Ctrl+Shift+V on Windows/Linux in the message composer - read plain text through the native Tauri/arboard clipboard path in packaged builds, with a browser-only Clipboard API fallback - re-enter ProseMirror's paste pipeline with populated `text/plain` clipboard data so selection, undo, multiline behavior, and paste observers remain intact - cover both platform mappings with rendered composer E2E tests that assert the native command path ## Testing - `pnpm test` — 4,286 passed - `pnpm check` - `pnpm typecheck` - `pnpm exec playwright test composer-selection-formatting.spec.ts --project=smoke` — 26 passed - `cargo check --manifest-path desktop/src-tauri/Cargo.toml --workspace --all-targets --target aarch64-apple-darwin` - `just desktop-tauri-test` — 2,206 core tests plus integration and doc-test groups passed - full pre-push hooks passed ## Manual verification Physical packaged-app clipboard verification remains recommended on macOS, Windows, and Linux. The automated E2E uses mocked Tauri IPC but asserts the native `read_clipboard_text` command is invoked. Signed-off-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz> Co-authored-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
## Buzz Desktop release v0.5.5 - **Frozen main:** `25a9cf1be6d245fbd7373cb1160dbc790baf5bd5` - **Reviewed candidate:** `8380c1f8ead8816bcf1f4ea9f66aa08e2441b15a` - **Previous desktop release:** `desktop-v0.5.4` - **Proposed immutable tag:** `desktop-v0.5.5` This PR must be **squash merged** only after the Desktop Release Candidate check passes. The branch must remain based directly on current `main`; stale base, payload drift, incomplete notes, or an unauthorized merge produce no tag. The checked-in changelog accounts for every non-merge commit in the release range. Publication remains bound to the immutable candidate tag. Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Release Automation <release-automation@users.noreply.github.com>
Signed-off-by: adrienlacombe <6303520+adrienlacombe@users.noreply.github.com> # Conflicts: # crates/buzz-cli/src/lib.rs # crates/buzz-db/src/migration.rs # desktop/src-tauri/tauri.conf.json
…nk decision The 2026-08-05 upstream sync produced the fork's first migration-version collision. Upstream block#4647 added `migrations/0027_channels_id_lookup_index.sql`, but this fork already holds 0027 and 0028 from the withdrawn NIP-SW wallet binding, and those have run on the live database — so they cannot move. Upstream's file is the one that has never run here, so it is renamed to 0029 and its contents kept byte-identical. That is the inverse of the event-kind rule, and the section explains why: a kind is owned by whoever has deployed traffic (upstream), a migration version by whoever has applied history (the fork). Worth writing down because nothing fails when you get it wrong. `sqlx::migrate!` accepts duplicate versions, `migrations.len()` is 29 with or without the fix, and stable sort-by-version puts `0027_channels…` ahead of `0027_wallet_binding_fts`, so upstream's new `migrations[26]` assertions pass on the broken tree. CI cannot catch it either: the integration lanes build the schema with pgschema and start the relay without `BUZZ_AUTO_MIGRATE` (ci.yml:473), then delegate the migration-version guarantee to those same blind unit tests (ci.yml:711). Only production runs the migrator, via `BUZZ_AUTO_MIGRATE=true` in ecs.tf, so the failure mode is the live relay refusing to start on `VersionMismatch(27)`. Also records why upstream's new `buzz://repo|pr|issue` entity links (block#4695) are deliberately *not* rebranded — they never reach the OS (no such arm in `deep_link.rs`'s router; `entityLinks.tsx` preventDefaults and routes in-app) and they travel inside message content as a wire token between clients, so emitting `bitcoinmarkets://` would break card rendering against upstream clients both ways. Names the two changes that would flip that conclusion. Corrects the `lib.rs` patch-table row, which claimed a `mod relay_allowlist;` declaration that lives in `relay.rs`; the file's only fork patch is the single-instance argv filter. Signed-off-by: adrienlacombe <6303520+adrienlacombe@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Merges
block/buzz33 commits,631b05c88..8342dfcc5, preserving the merge parent.What changed upstream
Relay / backend (21 files) —
perf(relay): index channel-id lookups and skip trace-only reads(block#4647) adds a covering partial index for the tenant-independentchannelslookups, plushandlers/req.rstrace-read skipping.fix: reauthenticate databricks model discovery(block#4008).Desktop (319 files) — the bulk.
feat: Buzz entity links(block#4695: rich preview cards + in-app navigation for repos/PRs/issues, newKIND_PROJECT_ANNOUNCEMENT = 30621),feat(projects): support multiple repositories(block#4671), Buzz Term docked into the channel workspace (block#4724), deferred media uploads until send (block#4522), sidebar unread hierarchy + observed-unread persistence across webview reload (block#3976, block#4573), Huddle voice control polish (block#4694), reconnect gap closure that previously needed CMD+R (block#4737), config diff in the restart-required badge (block#3637), and desktop release 0.5.5.Mobile (79 files) — inbox and media flow polish (block#4512), read-state modules moved
features/channels/read_state/→shared/read_state/, oversized read-state retry loop fixed (block#4595).Docs / CI — NIP-AM normative amendment (block#4632), ACP per-channel session model (block#4729), desktop cache-key test made version-agnostic (block#4791).
Conflicts
Three, all resolved:
desktop/src-tauri/tauri.conf.jsonproductName: BitcoinMarketsandidentifier: app.bitcoinmarkets.desktop; took upstream'sversion: 0.5.5.crates/buzz-cli/src/lib.rsmod links;(upstream) andpub mod starknet_factory;(fork).crates/buzz-db/src/migration.rsFORK-LOCALcomment. See below — this one needed more than the conflict marker showed.Upstream block#4647 added
migrations/0027_channels_id_lookup_index.sql. This fork already holds0027_wallet_binding_fts.sqland0028_wallet_binding_fts_kind_move.sql, and those have already run on the live database, so perAGENTS.mdthey cannot be renumbered — sqlx validates applied-migration checksums and editing one fails relay startup.Upstream's file has never run here, so it is the one that moved: renamed to
0029_channels_id_lookup_index.sql, contents byte-identical. Git added it as a new file, so there was no conflict marker anywhere.Left as-is, this would have taken the production relay down. With both version-27 files embedded,
Migrator::runreaches upstream's file, finds the applied version-27 row, the checksums differ, and it returnsMigrateError::VersionMismatch(27)(sqlx-core-0.9.0/src/migrate/migrator.rs:275). On a fresh database both look unapplied and the second insert collides on the_sqlx_migrationsversion primary key.infra/aws/ecs.tf:172setsBUZZ_AUTO_MIGRATE=true, and merging firesdeploy-aws.yml.Nothing in the test suite objects, which is the part worth knowing:
sqlx::migrate!accepts duplicate versions at compile time.migrations.len()is 29 either way, so the count assertion cannot tell the trees apart.sort_by_keyis stable and sorts on version only, so the two version-27 files order by filename and0027_channels…lands at index 26 — exactly where upstream's new assertions look.cargo test -p buzz-dbwas green with the collision in place; I ran it before fixing it to confirm.pgschema applyand start the relay withoutBUZZ_AUTO_MIGRATE(ci.yml:473), then delegate the migration-version guarantee to those same blind unit tests (ci.yml:711). The one test that would catch it is#[ignore = "requires Postgres"]and is not in CI's opted-in ignored set.Renaming the file was therefore only half the fix. Upstream's assertions about its own migration merged in cleanly and were wrong for this tree:
migrations[26].version == 27→migrations[28].version == 29migrations[26].sql→migrations[28].sqlapplied_versions(…).last() == Some(27)→Some(29)Deliberately not changed
Upstream's new
buzz://repo|pr|issueentity links (block#4695) are not rebranded tobitcoinmarkets://, unlikemessageLink.ts. Two checks decided it:deep_link.rs's router has arms forconnect,add-community,message,join,nostr-bind— norepo/pr/issue. Andshared/ui/markdown/entityLinks.tsxrenders them with anonClickthat callsevent.preventDefault()and routes in-app. The scheme is an internal sentinel here, not a registration.buzz-cli's own comment says the link "renders as a rich preview card in Buzz Desktop when included in a chat message" — it travels inside message content and is parsed byparseEntityLink. Emittingbitcoinmarkets://would stop this fork rendering cards for links published by upstream clients, and vice versa: a wire-format divergence across six actively-developed files, for no behavioural gain.This flips if a
repo/pr/issuearm appears indeep_link.rs's router, or if any "Copy link" affordance puts an entity link on the clipboard. Both are recorded inAGENTS.mdas the things to re-check.Fork patch sites — reviewed, not just merged
Upstream touched only 5 of the ~40 patch sites (
git diff --numstat main...upstream/mainover theAGENTS.mdtable):migration.rs,lib.rs,tauri.conf.json,kinds.ts,migrations/. All read closely.desktop/src-tauri/src/lib.rs— upstream's changes are in the import block andinvoke_handlerlist; the fork's one-line argv patch is at :117 and merged cleanly, still callingdeep_link::is_supported_deep_link.desktop/src/shared/constants/kinds.ts— newKIND_PROJECT_ANNOUNCEMENT = 30621. No collision with the fork's reserved30900–30999block.tauri.conf.jsonexternalBin— unchanged upstream, so no repeat of thebuzz-backend-kubernetessidecar break from the 2026-08-03 sync.release.yml,ci.yml,docker.yml,Dockerfile, the canaries,Info.plist×2, the xcconfigs,build.gradle.kts, the mobile allowlist files,kind.rs, the worktree scripts — untouched upstream this cycle.sprig-image.ymlmissing-variable class of failure.FORK-LOCALmarkers verified still present in the 9 marked source files.scripts/test-desktop-release-cache-key.sh(block#4791) is worth a note: it hardcodedversion = "0.5.4"and upstream made it version-agnostic in the same range that bumps to 0.5.5. Had those landed apart, the contract gate would have broken on the bump.Verification
All run on the merge result. Real results:
cargo fmt --all --checkcargo fmt --manifest-path desktop/src-tauri/Cargo.toml --all --checkcargo clippy --workspace --all-targets -- -D warningscargo clippy --manifest-path desktop/src-tauri/Cargo.toml --all-targets -- -D warningscargo metadata --lockedCargo.lockmerged clean, no hand-edit)scripts/test-release-ref-contract.shrelease ref contract passedscripts/test-mobile-worktree-overrides.shjust test-unitjust desktop-testjust desktop-checkjust desktop-typecheckjust desktop-tauri-testflutter analyzeflutter testdart format --set-exit-if-changedTwo notes on the gates themselves:
flutter analyzeandflutter testnow run locally.AGENTS.mdand the sync runbook say the Hermit Dart is older thanmobile/pubspec.yaml's constraint so this can't resolve. Hermit now ships Dart 3.11.5 against a^3.11.4constraint, so both ran clean. That guidance is stale.just desktop-checkemits one biome warning,lint/complexity/noImportantStylesatdesktop/src/shared/styles/globals/terminal.css:251(height: 0 !important). That file is added by upstream in this range and untouched here; the warning is non-fatal and the gate exits 0. Not patched — upstream code, and a fix belongs upstream.test-desktop-release-cache-key.shstill doescp -R "$repo_root"/., which copies the ~55 GB of warmtarget/from the working checkout.Needs a human look
migrations/, permanently. Every upstream migration from now on collides, and this is the first.AGENTS.mdgains a section deriving the rule and its inverse relationship to the event-kind rule (kinds: upstream keeps the integer, because it owns deployed traffic; migration versions: the fork keeps it, because it owns applied history). Please sanity-check that reasoning — it will be applied unattended on future syncs.0029isCREATE INDEX IF NOT EXISTS … ON channelswithoutCONCURRENTLY(sqlx wraps each migration in a transaction), so it takes a SHARE lock onchannels— blocking writes, not reads — for the build. Upstream's own header notes an operator may prefer to pre-build it by hand on a large brownfield database.channelsis small relative toevents, so this is expected to be brief.assert_eq!(applied_versions(…).last(), Some(29))is in a#[ignore]d Postgres test I could not execute here. Its sibling assertions were verified by reading; CI does not run it either.AGENTS.mdlib.rspatch-table row claimed amod relay_allowlist;declaration that actually lives inrelay.rs. Corrected in passing — pre-existing, not caused by this sync.Tripwires
Per the runbook this PR trips three and is therefore left for review rather than auto-merged:
migrations/(0029_channels_id_lookup_index.sql); merging runs DDL against production.KIND_*value changed (upstream's new 30621 does not collide with the fork's reserved block).AGENTS.mdpatch table (newmigrations/0029row, rewrittenmigration.rsrow, correctedlib.rsrow).crates/buzz-db/src/migration.rs. (release.ymlandingest.rswere untouched.)Merge with a merge commit — not squash. A squash drops the second parent, leaves the merge base stale, and makes every future sync re-resolve this same range. This fork has already been repaired by hand once for exactly that (
3ce7c8adc).