Merge upstream relay-v0.2.1 and Desktop v0.5.8 - #7
Merged
Conversation
### Summary Fixes [this issue](buzz://message?channel=e62570dd-33ad-42c5-b92b-75f2689f9694&id=b726c366abfe62429ee3cdcd34d0c0fb98c33c3ea053480585bed71745412b56): > I often don’t see my bot responses until after I post. they’re usually time stamped correctly so I think it’s just a refresh issue? ### What changed? Buzz Mobile now reconnects relay sessions after the app has remained backgrounded beyond the existing 5-second grace period, even when the session still reports a stale `connected` state. This makes resume recovery independent of whether iOS runs the grace timer before or after delivering `resumed`. Reconnection is now based on elapsed background time rather than a direct socket-health probe. - If the app was backgrounded for at least the 5-second grace period, the socket is presumed dead and the session reconnects regardless of reported status. - If it was backgrounded for less than that, a reported `connected` status is still trusted. In the sub-5-second window the socket is either genuinely alive, which is the common case for a momentary background, or it is dead and the client ping detects it within the two-interval worst case described below. That is now a degraded-latency path, not a silent-forever path. The mobile relay socket now uses `IOWebSocketChannel.connect` with a 30-second `pingInterval`. An unanswered ping closes the Dart socket through the existing disconnect and reconnect path. Detection takes up to two ping intervals, so about 60 seconds worst case, not 30. One interval of idleness elapses and a ping is sent, then a second interval elapses with no pong and the socket closes. Any inbound pong restarts the first stage, so the clock measures idleness rather than running on a fixed cadence. ### Why? Buzz iOS can sometimes stop showing new bot or agent responses after a phone has been locked for 5 to 10 minutes. When the user later posts a message, the missing responses can appear all at once. iOS may suspend Buzz before the short delayed cleanup that would normally close its connection has a chance to run. Before this change, Buzz trusted the resulting stale healthy status on resume and skipped reconnecting, so the missing responses stayed hidden until a later post exposed the dead connection. A state-machine test with a stubbed connection reproduced this reported pattern and showed that it matches this failure mode: the failed post triggered a reconnect that fetched the missing messages. The same test also checked the other candidate explanation, the bug tracked in [block#3053](block#3053), where the relay has closed the app's subscription. That state does not produce the pattern. Posting succeeds and the user's own message appears, but nothing looks for the missed messages, so they stay hidden. The test confirmed that the missed messages were still available to fetch in that state, so the missing step was a trigger to fetch them. This was not an end-to-end reproduction on an iOS device or a live relay. The new resume check covers the normal lock and unlock path. If the app was backgrounded for less than the 5-second grace period, it still trusts a connection marked as healthy. A dead connection in that window is instead detected by the ping check, which can take up to about 60 seconds but prevents the app from remaining silently stuck. The ping only runs while iOS is running the app, so it does not detect a connection that died during suspension; the resume check owns the lock and unlock path. A pre-existing path also runs the same resume handling when network connectivity returns while the app is already in the foreground. Because the app was not backgrounded, this change does not alter that path, which still trusts a connection marked as healthy and relies on the slower ping check. Recovery from a subscription that the relay explicitly closes remains in [block#3053](block#3053), and the two changes overlap in one file. Changes to how missed messages are backfilled or replayed are out of scope. ### How is it tested? Full mobile suite at base and head. Both runs have the same known macOS-host-only failure in `ChannelDetailPage keeps follow mode off while a tall newest message stays visible` at line 1053: - Base: 1,021 passed, 1 skipped, 1 failed - Head: 1,025 passed, 1 skipped, 1 failed Added tests: - [`relay_session_test.dart`](https://github.com/block/buzz/tree/main/mobile/test/shared/relay/relay_session_test.dart): long-background resume reconnect and within-grace control - [`relay_socket_liveness_test.dart`](https://github.com/block/buzz/tree/main/mobile/test/shared/relay/relay_socket_liveness_test.dart): silent-peer disconnect and idle-but-healthy control Mutation checks confirm that removing elapsed-background resume recovery fails with one socket instead of two, and removing `pingInterval` leaves the silent peer connected. Restored production code passes both mutations' regression tests and the healthy idle control. Signed-off-by: Tom Brow <tomb@block.xyz> Co-authored-by: npub1tquskdu6yc4h8l7xxtceculxw600grekeq0xg2ukqfrwl7vrzg3quz3gmp <58390b379a262b73ffc632f19c73e6769ef40f36c81e642b960246eff9831222@buzz.block.builderlab.xyz>
## Summary Gate 1 only for desktop release caching: - replaces canary `rust-cache` use with explicit exact-key `actions/cache/restore` + `save` - computes keys after `cargo update --workspace`, including platform, target, Rust toolchain, Cargo manifests/locks, profile/features, and native-toolchain inputs - normalizes only the desktop package version so a trusted `main` canary can warm an otherwise identical release tag - excludes Tauri bundle directories, so installers and signed artifacts are never cached - adds a restore-only `cache-proof-*` tag workflow that fails unless tag scope sees the exact default-branch cache - adds contract tests that enforce no release-workflow cache change in Gate 1 `release.yml` is intentionally unchanged. A cache miss remains the current cold canary build; the release path cannot be affected by merging this PR. ## Validation - `scripts/test-desktop-release-cache-key.sh` - `scripts/test-desktop-release-cache-workflow.sh` - `scripts/test-release-ref-contract.sh` - Ruby YAML parse of all four changed workflows - `git diff --check` - pre-push `branch-skew` ## Post-merge proof plan 1. Run each canary cold on trusted `main`, recording cache size/save time and fresh artifact inventory. 2. Run each canary warm, requiring the exact-key hit and recording restore/build time. 3. Create a disposable `cache-proof-*` tag at that same trusted `main` SHA and dispatch **Desktop release cache tag-scope proof** from the tag. 4. Do not begin Gate 2 or modify `release.yml` unless the exact tag-scope restore succeeds and cache transfer economics are favorable. --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
**Category:** improvement **User Impact:** Users can skip default model configuration during onboarding and finish it later in Settings → Agents. **Problem:** Requiring model defaults during onboarding can block users who are not ready to choose a harness, provider, or model. Skipping also needs to leave existing configuration untouched rather than persisting partial selections. **Solution:** Stage onboarding edits locally and persist them only when users choose Next or Back. A delayed Skip action advances without any configuration write, while a footer hint points users to the settings location for completing setup later. <details> <summary>File changes</summary> **desktop/src/features/onboarding/ui/DefaultConfigStep.tsx** Adds the skip action and future-settings hint, and makes model configuration transactional so Skip discards staged changes while Next and Back preserve the intended save behavior. **desktop/src/testing/e2eBridge.ts** Exposes model-config setter call counts so tests can distinguish a true zero-write skip from a write-and-rollback implementation. **desktop/tests/e2e/onboarding-agent-defaults.spec.ts** Covers skipping during loading and after staged edits, verifies zero persistence calls, and confirms Next and Back still commit changes. </details> ## Reproduction steps 1. Start fresh onboarding and continue through harness setup to **Configure your default model settings**. 2. Change the selected harness or model, then choose **Skip for now**. 3. Confirm onboarding advances to **Join or create a community** and the prior global model configuration remains unchanged. 4. Return through onboarding and confirm **Next** saves the staged selection; confirm **Back** also preserves staged changes before returning. 5. Confirm the footer says model defaults can be configured later in **Settings → Agents**. --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz> Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
## Summary - show an unambiguous `App default (10)` inherited state for parallelism in create and edit forms - explain that blank inherits the app default and suppress create-form number steppers that could silently set `1` - align the E2E mint fallback with production while preserving explicit input → definition → app-default precedence ## Why The forms displayed `1` even though an untouched field is omitted and desktop minting materializes `10`. The create-form spinner could also turn blank/inherited into an explicit `1` with one click while leaving the field looking nearly unchanged. ## Testing - `pnpm test` (desktop: 3,886 passed) - `pnpm typecheck` (desktop) - `pnpm check` (desktop) - pre-push `desktop-check` and `desktop-test` --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
**Category:** fix **User Impact:** Long custom emoji names now stay contained inside reaction popovers and remain fully readable. **Problem:** An unbroken custom emoji name could force a reaction popover beyond its intended maximum width and overflow the message view. **Solution:** Give the reaction popover a definite 288px width and allow the complete emoji name to wrap within it without truncation or ellipsis. Short names retain the same content and interaction behavior. <details> <summary>File changes</summary> **desktop/src/features/messages/ui/MessageReactions.tsx** Bounds the reaction popover width and allows long names to break across lines while preserving the full shortcode. **desktop/tests/e2e/reaction-names.spec.ts** Covers fixed width, full text preservation, and wrapping for the maximum supported colon-wrapped reaction name, with deterministic seeded Picsum visual fixtures and explicit image-load waits. </details> ## Reproduction Steps 1. Open a message with a custom emoji reaction whose name is 64 characters. 2. Hover or focus the reaction pill to open its details popover. 3. Confirm the popover remains 288px wide and the complete name wraps within it without ellipsis. 4. Open a short-name reaction and confirm its popover remains readable and unchanged in behavior. ## Screenshots | Before | After | | --- | --- | |  |  | **Short-name regression check**  ## Verification - `pnpm test` in `desktop`: 3,858 passed - Focused reaction-name E2E with seeded Picsum captures: 2 passed - Desktop checks and commit hooks passed Originating Buzz channel: `f2ec9671-d78e-4cde-894c-9f4c458c7f1f` --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
## Summary - Refresh Share Compute with the shared agent-style model controls. - Reveal sharing details and advanced options only while sharing. - Remove the preview-only mesh API path. ## Validation - `pnpm check` - `pnpm test` - `pnpm exec playwright test tests/e2e/mesh-compute.spec.ts` Snapshots are attached in a follow-up comment. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com>
…ock#4578) ## Overview The global Agent Defaults surface (Settings card, defaults modal, onboarding) exposed structured controls for Effort but left Max Output Tokens, Context Limit, and Max Rounds as raw env vars. Per-agent dialogs had structured numeric fields but only for `isBuzzAgentRuntime` — incorrectly excluding Goose. This PR unifies numeric-tuning capability across all surfaces, fixes a pre-existing dual-editor defect, and adds full test coverage. ## What changed ### Phase 1 — Catalog projection - Add `max_rounds_env_var` to `KnownAcpRuntime` in `runtime_metadata.rs` (`Some("BUZZ_AGENT_MAX_ROUNDS")` for buzz-agent, `None` elsewhere). - Project all three numeric env-var fields (`max_tokens_env_var`, `context_limit_env_var`, `max_rounds_env_var`) end-to-end: `AcpRuntimeCatalogEntry` Rust struct, TS `types.ts`, `RawAcpRuntimeCatalogEntry` + `fromRawAcpRuntimeCatalogEntry` in `tauri.ts`, and the e2e mock bridge (`withMockRuntimeConfigMetadata`). ### Phase 2 — Field model - `deriveAgentConfigFieldModel` now derives `maxOutputTokens` / `contextLimit` / `maxRounds` descriptors from catalog-projected fields. - `structuredEnvKeys(descriptors)` — exported helper that takes the **rendered** descriptor set (not the whole model). Hidden keys follow what is actually rendered per surface: global hides effort + all three numeric keys for buzz-agent / two for Goose; per-agent buzz-agent hides effort + three numeric keys; per-agent Goose hides only its two numeric keys. `BUZZ_AGENT_THINKING_EFFORT` stays a visible generic env row per-agent because no effort control renders there. ### Phase 3 — UI - Extract `NumericTuningFields` from `buzzAgentModelTuningFields.tsx` as a shared descriptor-driven component (`descriptors`, `envVars`, `inheritedEnvVars`, `onEnvVarChange`). Kind-specific minima: `NUMERIC_KIND_MIN` map (`maxOutputTokens`/`contextLimit`: 1, `maxRounds`: 0) applied to `<input min>`. - **Global surface** (`AgentConfigFields.tsx`): deduplicate the previously duplicated Advanced env-editor block; render `NumericTuningFields` below the env editor when descriptors exist; `hiddenKeys` and `bakedGenericRows` exclusions use `structuredEnvKeys` so structured keys are never double-rendered. Under 1000 lines. - **Per-agent surfaces** (`EditAgentAdvancedFields`, `PersonaAdvancedFields`): replace `isBuzzAgentRuntime` as the numeric-field gate with `deriveNumericDescriptors(selectedRuntime)` from `agentConfigCore`; hidden keys come from `structuredEnvKeys(numericDescriptors)` — the same rendered descriptor set, no local rebuilding (fixes pre-existing dual-editor defect). Catalog status carried as `RuntimeCatalogStatus` (`loading | ready | error`); both error and loading withhold structured controls and leave saved values visible as generic rows, making error distinguishable from "runtime not capable" (`ready` + no runtime). - **Dialogs** (`AgentDefinitionDialog`, `AgentInstanceEditDialog`, callers): `AgentDefinitionDialog` accepts `runtimeCatalogStatus?: "loading" | "ready" | "error"` (replaces separate `runtimesLoading`/`runtimesError` booleans); all call sites — `AgentManagementDialogs`, `AgentsView`, `RequestedAgentCreateDialogs`, `UserProfilePersonaDialogs` — compute and pass the status. ### Phase 4 — Tests - `buildRecord` exported from `EnvVarsEditor.tsx` as a pure `(nextRows, value, requiredKeys, hiddenKeys) => Record<string, string>` helper for isolation testing. - **17 new node tests** in `agentConfigCore.test.mjs`: `deriveNumericDescriptors` (all three fields, partial, undefined runtime, matches field-model subset); `structuredEnvKeys` per surface including discriminating Goose per-agent effort-key invariant; `NUMERIC_KIND_MIN` values. - **4 new node tests** in `EnvVarsEditor.test.mjs`: hidden tuning key preserved through generic row edits; runtime-switch then generic edit (derives both descriptor sets, asserts new-runtime hidden key survives `buildRecord` via `hiddenKeys` and old-runtime key survives via generic rows); baked numeric key excluded via `filterBakedGenericRows` with `numericTuningPlaceholder` assertion; clearing a structured override — `numericTuningPlaceholder` verifies placeholder text. - **5 new Playwright tests** in `agent-numeric-tuning.spec.ts` (added to smoke project `testMatch`): global numeric fields visible for buzz-agent; global: non-capable runtime hides numeric controls; Goose per-agent shows `Inherit (16384)` after saving global value through the UI; delayed catalog: saved values visible as generic rows while loading then structured controls appear after settle; failed catalog: saved values remain visible as generic rows (never the "unsupported" empty state). ## Result - buzz-agent global defaults: Max output tokens, Context limit, Max rounds as structured inputs with `Inherit (N)` placeholders from baked env. - Goose global defaults: Max output tokens, Context limit as structured inputs. - A Goose global value surfaces as `Inherit (<value>)` in the per-agent Goose edit dialog. - No structured key is editable in two places on any surface; no persisted key has zero editors. - No `runtime.id === "buzz-agent"` comparison decides numeric-field visibility anywhere — capability flows catalog → `AcpRuntimeCatalogEntry` → field model → UI. Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
## Overview **Category:** improvement **User Impact:** Mobile users can now access consistent channel and DM actions from both the channel list and conversation header. **Problem:** Mobile channel menus exposed a narrower, inconsistent set of actions than desktop, and the available actions differed by entry point. **Solution:** This change introduces one reusable action sheet with a clear quick-action hierarchy, role-aware lifecycle controls, confirmations for consequential actions, and a deliberately narrower DM menu. ## Changes <details> <summary>File changes</summary> **mobile/lib/features/channels/channel_actions_sheet.dart** Adds the shared channel and DM action-sheet experience used by both entry points, including Star/Unstar and Read/Unread quick actions for channels, section movement, mute, management, inline copy actions, guarded lifecycle actions, confirmations, and a compact DM menu without quick actions. **mobile/lib/features/channels/channel_detail_page.dart** Routes the header ellipsis through the shared action sheet so the in-channel menu matches the channel-list experience, including for DMs. **mobile/lib/features/channels/channel_management_provider.dart** Adds archive and delete operations using the desktop-compatible relay event kinds and refreshes channel state after completion. **mobile/lib/features/channels/channels_page.dart** Makes the shared channel action-sheet entry point available to the channel-list implementation. **mobile/lib/features/channels/channels_page/channel_tile.dart** Replaces the tile-specific long-press menu with the reusable action sheet while preserving read state and section context. **mobile/test/features/channels/channel_actions_sheet_test.dart** Covers action hierarchy, owner/admin/member capability guards, loading and failure states, DM narrowing with no quick-action row, and inline copy actions. **mobile/test/features/channels/channel_detail_page_test.dart** Updates channel-header flows to exercise management through the new shared action sheet. **mobile/test/features/channels/channel_management_provider_test.dart** Verifies archive and delete event tags stay compatible with desktop behavior. </details> ## Reproduction Steps 1. Run the mobile app and open a populated channel list. 2. Long-press a regular channel and verify the Star/Unstar and Read/Unread quick actions appear above Move to section…, Mute, Manage, Copy channel name, and Copy channel ID. 3. Choose either copy action and verify it copies the expected value. 4. Open a channel, tap the header ellipsis, and verify the same action sheet appears. 5. As an admin or owner, verify Archive appears; as an owner, verify Delete also appears. Confirm that lifecycle actions require confirmation. 6. Long-press or open the header menu for a DM and verify it has no quick-action row and starts with Mute, followed by Copy channel name and Copy channel ID. ## Screenshots ### Channel menu | Regular channel — Mark Unread | DM — no quick actions | Archive confirmation | |---|---|---| |  |  |  | --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
## Summary - open Huddles in a focused companion window with a clean handoff back to the in-app drawer and backing channel - redesign the participant film strip, sidebar control, transcript surface, and themed shell treatment - preserve microphone and device control across windows, start agent voice on the first reply, and show agent speaking activity in the film strip - give each agent a distinct session voice, beginning with the configured default, plus compact per-agent text-to-speech and voice controls - enroll only agents explicitly mentioned or deliberately added through an agent panel into the live Huddle roster - keep temporary Huddle channels out of the sidebar unless the user explicitly brings one into the main app - remove Huddle-only avatar policy badges and filter short silence or noise segments before speech-to-text posts ## Why The previous flow exposed the temporary channel as product UI, obscured who was present or speaking, and split transcript and audio state between the main and companion windows. This keeps backing channels as implementation details unless a user explicitly brings a Huddle into the app, while sharing the live conversation and audio lifecycle across both surfaces. Agent participants now join only after an explicit invitation, distinct voices make multi-agent Huddles easier to follow, and short microphone noise no longer becomes stray transcript messages. ## Validation - `pnpm check` - `pnpm build:e2e` - `pnpm exec playwright test tests/e2e/huddle-transcription.spec.ts --project=smoke` (13 passed) - Huddle sidebar visibility unit coverage (4 passed) - focused managed-agent and persona-mention E2E coverage (2 passed) - `pnpm test` (3,910 passed) - `cargo clippy --manifest-path desktop/src-tauri/Cargo.toml --all-targets -- -D warnings` - `cargo test --manifest-path desktop/src-tauri/Cargo.toml` (2,093 passed, 14 ignored; 3 diagnostics passed) --------- 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>
**Category:** improvement **User Impact:** Mobile readers can jump directly to their oldest unread message and return to the latest message with compact directional controls. **Problem:** Opening an active channel at its newest message makes it easy to miss where unread conversation began, while moving back through history lacks a lightweight route to the live edge. **Solution:** Capture the channel's unread boundary when it opens, offer an accessible up-chevron beneath the app bar to reach that stable target, then reveal the inverse down-chevron at the bottom whenever the reader is away from latest. Deep links retain precedence, and live-follow, pagination, composer resizing, and explicit scroll ownership continue to use the existing timeline behavior. <details> <summary>File changes</summary> **mobile/lib/features/channels/channel_detail_page.dart** Captures the channel's read state at open time and passes a stable unread snapshot into the timeline before the normal deferred read update advances it. **mobile/lib/features/channels/channel_detail_page/message_list.dart** Adds mutually exclusive oldest-unread and latest navigation, with accessible icon controls positioned at opposite edges of the message surface while preserving existing follow and deep-link behavior. **mobile/test/features/channels/channel_detail_page_test.dart** Covers the unread target, compact inverse controls, accessible tooltips, and placement beneath the frosted app bar. </details> ## Reproduction steps 1. Open a Flutter mobile channel that has unread messages without entering through a message or thread deep link. 2. Confirm an up-chevron appears directly below the channel app bar while the timeline remains at latest. 3. Tap the up-chevron and confirm the timeline scrolls to the oldest message that was unread when the channel opened. 4. Confirm the unread control is replaced by a down-chevron at the bottom of the timeline. 5. Tap the down-chevron and confirm the timeline returns to latest and resumes following new messages. ## Screenshots | At latest — up-chevron to oldest unread | Away from latest — down-chevron to latest | |---|---| |  |  | _Real iPhone 17 Pro Simulator captures from the neutral `buzz-mobile-scroll-to` channel._ Originating Buzz thread: `buzz://message?channel=5b16c478-22d8-4ddd-951a-6036e19b81ff&id=6a78af32d7ac6f531b182c4e70dd5a04c503a2dab2ce2c0c74b2c6baa5921741&thread=6a78af32d7ac6f531b182c4e70dd5a04c503a2dab2ce2c0c74b2c6baa5921741` --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz> Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
**Category:** improvement **User Impact:** Mobile users can sort each channel group by recent activity or A–Z, with their choices synchronized with desktop. **Problem:** Desktop supports persistent per-group channel sorting, but mobile shows the same groups without equivalent controls or shared preferences. The earlier mobile attempt coupled sorting to unsafe dirty-state behavior that could overwrite newer cross-client changes. **Solution:** Add mobile sorting controls and encrypted NIP-78 synchronization using the existing desktop `channel-sort` contract, while retaining ordinary whole-blob last-write-wins behavior. Local state is scoped by identity and normalized relay, startup closes fetch/subscription gaps, and both clients use the same deterministic ordering rules. <details> <summary>File changes</summary> **desktop/src/features/sidebar/lib/channelSortPreference.test.mjs** Updates ordering coverage for the deterministic, cross-client A–Z comparison rule. **desktop/src/features/sidebar/lib/channelSortPreference.ts** Aligns desktop channel-name collation with mobile so synchronized preferences produce the same visible order. **mobile/lib/features/channels/channel_sort/channel_sort_manager.dart** Adds encrypted relay synchronization with safe startup gap handling, clock checks, and ordinary last-write-wins conflicts. **mobile/lib/features/channels/channel_sort/channel_sort_provider.dart** Scopes sort state to the active identity and community lifecycle. **mobile/lib/features/channels/channel_sort/channel_sort_storage.dart** Defines the desktop-compatible payload, relay-scoped cache and migration, cleanup, and shared ordering behavior. **mobile/lib/features/channels/channels_page.dart** Connects sort state to the channel page. **mobile/lib/features/channels/channels_page/body.dart** Applies each selected order to Starred, custom groups, Channels, and DMs. **mobile/lib/features/channels/channels_page/sections.dart** Adds checked Recent and A–Z actions using the existing anchored-popover UI. **mobile/test/features/channels/channel_sort/channel_sort_manager_test.dart** Covers payload adoption, encrypted publication, conflicts, timestamps, retries, and cleanup. **mobile/test/features/channels/channel_sort/channel_sort_storage_test.dart** Covers parsing, relay isolation, migration, cleanup, and ordering modes. **mobile/test/features/channels/channels_page_test.dart** Verifies the group controls expose both choices. </details> ### Reproduction steps 1. Open the mobile channel list with populated built-in and custom groups. 2. Open a group menu and choose **Sort: Recent**; confirm active channels move to the top. 3. Choose **Sort: A–Z**; confirm deterministic alphabetical ordering returns. 4. Repeat for Starred, a custom group, Channels, and DMs. 5. Open desktop with the same identity and community and confirm each synchronized preference. 6. Switch communities and confirm cached preferences do not bleed across relays. ### Screenshots Approved `live` custom-section flow with `research` kept offscreen. | Recent selected | A–Z result | A–Z selected | |---|---|---| |  |  |  | ### Validation - Mobile `flutter analyze` — clean - Focused mobile sort and channel-page suites — 37/37 passed - Desktop full suite — 3906/3906 passed - Mobile full suite — 1034 passed, 1 skipped, 1 unrelated baseline failure reproduced at `ac4fa13b8` <!-- Originating Buzz channel: 2a16a2bb-6fd3-4d69-8182-2afcb21b2d14 --> --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
## Summary - ship **Buzz Term** end to end: the terminal engine/runtime, mounted desktop substrate, and user-visible naming - add Quinn's tape-deck-inspired banner: a beveled chassis filled by the `buzz term` wordmark, surrounded by a complete-hex field - derive the wordmark's three-stop sweep from each theme's terminal palette so primary, secondary, and accent roles remain visibly distinct across all 62 shipped themes, including light themes - paint the banner once on its own pointer-transparent canvas; PTY rendering beneath it remains unchanged ## Banner behavior - uses the renderer's shared `8.4 × 17` cell metrics and production aspect ratio `2.0238` - regenerates only for viewport/theme changes; palette switches repaint correctly while the banner is visible - dismisses on non-empty output from the active terminal session; empty output and inactive sessions do not dismiss it - fails closed below **70 columns** rather than squeezing or clipping the wordmark - adds **8 lines** to `terminalRenderer.ts` for shared cell metrics and **zero lines inside `paint()`** ## Screenshots | Buzz (light) | Buzz Dark | |---|---| |  |  | | Kanagawa Lotus (light) | Red | |---|---| |  |  | Additional production-aspect finals: [Vesper](https://buzz.block.builderlab.xyz/media/9ca6514b63f8cfb2107a85ca46f16a940c0883848e6fbc718e411af94aa13100.png), [Min Dark](https://buzz.block.builderlab.xyz/media/f67bd2970e5d64ffb07b1ae78ab58c847e6ebc23e7e7a48e067eb024dba64ec8.png), and [Dark Plus](https://buzz.block.builderlab.xyz/media/290fee08924f37d064abc687ecf3e9526ab05b87e8e56d610f23048949793dbe.png). The screenshot harness was checked against the shipped painter at this exact head: all **2,541 draw calls** matched on color, glyph, x, and y; four deliberate divergence controls fired. ## Verification at `98ebc8f9048bd5f0ceb7e843b67874d642f0b7fd` - desktop tests: **3,946 / 3,946** - TypeScript: clean - checks: pass (two pre-existing informational `useTemplate` notices only) - integration/e2e: PASS (independent exact-SHA lane; artifacts recorded in the originating Buzz thread) - artifact/dead-path sweep: clean - redteam G1–G7: PASS - all six named banner emitter-deletion mutants die - independent handwritten five-row full-wordmark fixture kills Quinn's seven-mutant battery, including a one-pixel glyph change - real `112 × 46` canvas-rect dismissal tests separately cover active non-empty, active empty, and inactive non-empty output - layer-drop and zero-draw painter mutants die; z-order and pointer-events verified - CI's `tsc && vite build` includes all three banner modules - performance at DPR 2 (worst-case measured envelope): - one-time content paint: **~0.7–0.8 ms**, paid only when the banner is built or its palette changes - busy compositor, CSS `1277 × 697`, backing `2554 × 1394`: **470–497 µs/frame** for the full banner (**2.82–2.98%** of a 60 Hz frame) - busy compositor, CSS `1920 × 1080`, backing `3840 × 2160`: **1,139–1,212 µs/frame** (**6.83–7.27%**) - empty, one-glyph, and full-banner controls converge: compositor cost follows backing-layer area and DPR rather than painted-cell count - in the actual idle welcome state, cost is below both vsync-clamped rigs' resolution; it is not claimed as zero - **Pane cross-rig spread: resolved at matched loop rate.** Two independent rigs initially differed 2.3× (58–68 vs 136 µs/Mpx of backing store; pane, CSS 1277×697 / backing 2554×1394, DPR 2). The cause of *that* spread is rAF loop rate: the higher figure came from a free-running loop at ~1600fps. Throttled to ~200–236fps, both rigs read 58–68 µs/Mpx (1.25–1.44% of a 60Hz frame). The busy-composite figures quoted above remain the **unthrottled worst case** and are conservative by ~2.3× at the pane. Not established: the mechanism and sign of free-running distortion (one rig under-charges ~15%, the other over-charges 2.3×), and the 1080p figure has not been re-measured throttled. - the layer paints only on generation/theme/resize and dismisses on first non-empty active-session output, so the measurable busy cost is a short-lived worst case rather than a persistent PTY paint-path tax ## Follow-ups in this PR These are intentionally subsequent commits after the certified static-banner head, not claims about `98ebc8f90`: 1. close the compositor metrology: remeasure the 1080p point throttled and characterize the opposite-sign free-running rAF distortion, with each measurement regime stated 2. add Tyler's animated honeycomb color waves, gated by `prefers-reduced-motion`, a full 62-theme phase-sweep contrast check, and DPR-2 per-tick performance certification 3. land the already-proven mounted theme-switch regression probe from `RESEARCH/BUZZ_TERM_G3A_PROBE/` 4. bound the slow/hang-shaped G1-c mutant `waitFor` 5. optionally trim the generator to its ink bounding box, reducing the minimum viewport from 70 to 62 columns --------- Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz> Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> Signed-off-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz> Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Co-authored-by: Dawn (sprout agent) <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co> Co-authored-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz> Co-authored-by: npub1cc3ha7z055mu0rwwu7806t2wt8mj3pvu0uv5mfp2c50dahaqhczshdalg6 <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@buzz.block.builderlab.xyz> Co-authored-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz> Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz> Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
## 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 - mirror the retained canvas terminal grid into a transparent, selectable text layer - preserve the canvas renderer and terminal focus behavior for ordinary clicks - reconstruct wide and combining glyphs correctly for clipboard text ## Why Buzz Term renders output entirely on a canvas and deliberately called `preventDefault()` on viewport mouse-down, so native selection and copy could not work. A canvas has no selectable text even if that cancellation is removed. The transparent text layer stays aligned with the visible cell grid, lets WebView native selection drive drag highlighting and copy, and follows active-session switches without changing the renderer or PTY protocol. ## Validation - `pnpm --dir desktop typecheck` - `pnpm --dir desktop test` — 4,373 passed - pre-push `desktop-check`, `desktop-test`, and `branch-skew` hooks passed on `1f2a3f8db63f6fe36b4a28bc911aea3c5186b2b0` --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
… tree (block#5142) ## Summary WebKit throws `SecurityError` from `localStorage.getItem` (not just `setItem`) when storage access is denied for the origin. With no `ErrorBoundary` in `desktop/src`, any such throw inside a provider render (`ThemeProvider`, `CommunitiesProvider`, `App` boot) propagated to the reconciler, unmounted the root, and left a blank window. Measured repro in block#5078: a single throwing `getItem` on `buzz-communities` or `buzz-active-community-id` kills the container. Closes block#5078. ## What changed **New helper — `desktop/src/shared/lib/safeStorage.ts`** - `getStorageItem(key, fallback?)` — wraps `window.localStorage.getItem`; on a thrown error (SecurityError under denied-storage origin) it warns once per key and returns the fallback. - `setStorageItem(key, value)` and `removeStorageItem(key)` — same fail-closed contract (return `false` on throw). - Unit tests in `safeStorage.test.mjs` cover the happy path and the `SecurityError` path. **Rewired the init-path readers that ran before any UI existed** - `desktop/src/features/communities/communityStorage.ts` — `migrateLegacyCommunityStorage`, `loadCommunities`, `loadActiveCommunityId`, `loadCommunityDiscoveryAfterLeave`, `initFirstCommunity` - `desktop/src/features/communities/legacyCommunityStorage.ts` — `migrateLegacyCommunityStorageBeforeRender` - `desktop/src/shared/theme/ThemeProvider.tsx` — `readStoredTheme`, `applyCachedVars`, the `useState` initialisers for `accentColor` and `followSystem`, and the accent re-read inside `applyTheme` **Root-level fence — `desktop/src/app/RootErrorBoundary.tsx`** - New top-level `ErrorBoundary` wrapping the whole provider tree in `main.tsx`. Any remaining uncaught render error (a future storage read that bypasses the helper, or any other render-time crash) renders a degraded splash with a Reload button instead of a blank window. ## Test plan - `desktop/src/shared/lib/safeStorage.test.mjs` — node `--test` runner, 11 assertions across healthy, absent, and SecurityError-throwing storage. - Full `just ci` runs on the blocker. - Existing `communityStorage.test.mjs` and `legacyCommunityStorage.test.mjs` continue to pass (they exercise the same functions via in-memory Storage doubles; the new code path in `migrateLegacyCommunityStorage` only adds a `try/catch` around the same body). ## Why not an ErrorBoundary-only fix A boundary alone can't help on a *clean* mount — the first throw already unmounted the whole subtree before any state or fallback data was loaded, so retrying would hit the same throw on the very next render. The storage accessor has to fail closed *and* the boundary has to exist for whatever bypasses it. Both are needed; neither is sufficient alone. --------- Signed-off-by: iroiro147 <sarthak.singh@mastersunion.org> 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>
…ion (block#5143) ## Summary WebKit throws `NotificationError` from the `Notification` constructor when the notification backend becomes temporarily unavailable (measured repro attached to block#5081). Every existing call site used `void sendDesktopNotification(...).then(...)` — discarding the returned promise with no rejection handler — so a throwing constructor became an unhandled promise rejection. The notification was silently dropped and the only trace was console noise. Closes block#5081. ## What changed Fenced the throw at the source inside `sendDesktopNotification` (`desktop/src/features/notifications/lib/desktop.ts`): - A new `try { ... } catch { ... }` wraps `new window.Notification(...)` and the `onclick` attach. - On catch, we `console.warn` once and `return false`, so the promise the call sites discard is always fulfilled with the same boolean result. No caller needs to change. ## Why at the source and not at each call site The issue body lists four rejecting edges: `useAppShellDesktopNotifications` (2×), `useReminderNotifications`, `use-feed-desktop-notifications`. Patching them one-by-one leaves the door open for the next consumer to make the same mistake — and the function itself advertises `Promise<boolean>`, so callers are entitled to assume the promise resolves with the delivery bit rather than rejects. Fixing the inside satisfies both properties for every present and future caller. ## Test plan - Behavior change is a guarded return value around a single constructor; unit coverage is best expressed inside the mounted-hook harness already used in the repro. Existing notification helpers (`shouldNotify*.test.mjs`) continue to pass. - Full `just ci` runs on the blocker. - The next notification after a backend blip delivers normally (the throw is per-call, not sticky). ## Note on scope This addresses the titled bug: unhandled rejection from a throwing constructor. A separate, intended follow-up is to wire a user-visible delivery-miss event if the platform exposes one — that's notification-observability work, not a } catch. --------- Signed-off-by: iroiro147 <sarthak.singh@mastersunion.org> 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>
…xtractor (block#5191) Replaces the four-helper auth resolution path with two focused functions and adds production async tests that count relay round-trips. **Before:** `resolve_auth` called `resolve_auth_from_profile` (warn-emitting probe into a throwaway sink) → `resolve_auth_deciding` (re-classified the same profile) → `handle_auth_failure` → `auth_failure_detail` (third classification). `Option<Option<&Value>>` encoded a sentinel for unreachable state; tests exercised only the pure sync helper, not the actual fetch count. **After:** - `extract_auth(profile, target, signer) -> Result<[String;4], AuthFailure>` — pure typed extractor; `AuthFailure` now covers `NoProfile` and `NoTagsArray` inline, no separate helper needed - `resolve_auth()` is now the linear state machine: self-check → fetch + extract → on failure: fetch again → route final `Err` to `CliError::Usage` (default) or one admin warning (`--admin`). No throwaway sinks, no duplicate classification, no sentinel type. - Five async tests drive the production resolver through a counted Axum test server on `POST /query` and assert on both return value and exact fetch count: first success (1), retry success (2), double failure / no `--admin` (2 + `Err`), double failure / `--admin` (2 + `Ok(None)` + one warning), self path (0). Two parser tests pin `--admin` on both `archive` and `unarchive`. - `--admin` short help text corrected to describe when the flag takes effect (after extraction fails, not unconditionally). 341 tests passing, clippy clean, fmt clean. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
…g, activity feed polish (block#5073) ## Summary Follow-up batch on the Projects overview (continues merged block#1677): - **Repository access restrictions** — repositories the viewer can't reach are surfaced with a reason instead of failing silently. Channel-ACL denials (which arrive as the same 404 as a missing repo, for anti-enumeration) are re-classified using the repository's channel binding and the viewer's memberships (`useRepositoryAccess.ts`, `projectRepoAvailability.ts`). - **Projects loads in seconds instead of minutes** — enumeration no longer crawls every kind:5 deletion event on the relay. It fetches project/repo announcements first, then queries deletions scoped to those coordinates via chunked `#a` filters (3 queries instead of hundreds on staging). - **Activity feed layout polish** — bare event-type glyph beside the headline (no badge circle), timeline spine runs through the avatars connecting consecutive cards, linkable actor/project names are bold in theme foreground, rounded hover state, alignment fixes. - **Create button pinned** — the "+" create menu is pinned to the pane's top-right corner (equal 16px insets) and no longer scrolls away with the page header. - **List controls as a table header** — the scope selector (left) and sort + layout toggle (right) render as the first row of the list container on the Projects/Repositories/PRs/Issues tabs; in card view the identical bar stands alone with the cards below (`ProjectsListHeaderBar.tsx`). - **Repository rows show the git location** — subtitle is `github.com/org/repo` for external repos or `owner/repo` (resolved profile name) for Buzz-hosted ones, instead of repeating the project name (`repositoryDisplayPath`). - **Uniform work-item row heights** — issue rows previously ran the author chip in inline flow, letting the 20px avatar grow the line box ~3px taller than PR rows; both lists now share the same flex subtitle. 📸 Screenshots: [feed layout / pinned button](block#5073 (comment)) · [list header / repo subtitles / row heights](block#5073 (comment)). Note: two empty `chore: retrigger CI` commits exist on the branch from working around the Aug 6 GitHub Actions incident; happy to drop them with a signoff rebase before undrafting if preferred. Latest `main` is merged in (`a0cc35220`). ## Test plan - [x] Desktop unit tests (4,493 pass after merging main), Biome, tsc - [x] New unit tests for scoped deletion enumeration and repo availability re-classification - [x] New unit tests for `repositoryDisplayPath` (external, Buzz-hosted, unresolvable) - [x] Screenshot verification of feed layout, connector spine, and pinned button (top + scrolled states) — posted to the PR - [x] Screenshot verification of the list header row (list + card), repo subtitles, and matching PR/issue row heights — posted to the PR - [ ] Manual pass against staging (projects list load time, restricted-repo states) --------- Signed-off-by: Thomas Petersen <thomasp@squareup.com>
## Problem In the **Edit channel** dialog, flipping visibility (Public <> Private) persisted **immediately on selection**, bypassing the **Save changes** button — while every other field (name, description, temporary, TTL) waited for an explicit save. This surprised users and gave no chance to cancel a flip, e.g. a private->public change that instantly exposes channel history. Reported in the Buzz "Welcome" channel by Kevin Chung. ## Root cause The visibility dropdown was wired to `handleConvertVisibility()`, which called the update mutation on selection. This was intentional at the time (there was even an e2e test named `02 — visibility updates immediately` and an "Updating…" spinner), but it is inconsistent with the rest of the dialog and is the surprising behavior reported. ## Change (defer to Save) - Visibility becomes a **deferred draft** like the other fields: selecting a value updates local `isPrivateDraft` and marks the draft dirty. The change commits via `handleSaveChannelEdits` (which already handled visibility) on **Save**, and is discarded on **Cancel**. - The dialog title now reflects the **pending draft** (`nextVisibility`), so the pending choice is visible before saving. - The edit-dialog reset restores `isPrivateDraft` from server state. - Removed the now-dead `handleConvertVisibility` handler, `isConvertingVisibility` state, the `channelIdRef` race guard it needed, and the unused `isPending`/"Updating…" spinner path in `ChannelPermissionsSettings` (no caller passes `isPending` anymore). ## Tests - Rewrote e2e `02` -> **`visibility defers to Save`**: select -> Save enabled -> title reflects draft -> Save -> persists; toggling back to the original value clears the draft and disables Save. - Extended `09` (cancel discards drafts) to also cover a visibility change. - Repurposed `10`: the stale-update race it guarded is architecturally gone, so it now asserts an **unsaved visibility draft does not leak across a channel switch**. ## Validation - `pnpm typecheck` — clean - `biome check` (changed files) — clean - `pnpm test` — **4497 passed / 0 failed** - `playwright test --project=smoke channel-controls` — **10 passed** Signed-off-by: Kevin Chung <chung@squareup.com> Co-authored-by: Fizz <e3f95089179cc1bcc68d70c334b9bdf670d0470496db90bcdbb20386963432da@buzz.block.builderlab.xyz>
…5202) ## Summary - preserve each distinct agent pubkey in autocomplete even when agents share a persona or owner/name - continue to collapse duplicate source rows for the same normalized pubkey - show a truncated pubkey in the channel member-add picker so same-named instances are selectable ## Validation - `pnpm --filter buzz test` — 4,489 passed - `pnpm --filter buzz exec tsc --noEmit --pretty false` - `pnpm --filter buzz exec biome check src/features/agents/lib/agentAutocompleteEligibility.ts src/features/agents/lib/agentAutocompleteEligibility.test.mjs src/features/channels/ui/MembersSidebar.tsx` - independent validation by Fast Fizz on `509cb8d97b82f9708e24d4d59ad17c7b39516643`: typecheck, focused Biome, 22/22 focused tests, and `git diff --check` Generated by Hardworking Honey. --------- Signed-off-by: Hardworking Honey <c5c455215c2506cb8ba776518cec804af62d3a0526e32d496a22072e395042b9@buzz.block.builderlab.xyz> Co-authored-by: Hardworking Honey <c5c455215c2506cb8ba776518cec804af62d3a0526e32d496a22072e395042b9@buzz.block.builderlab.xyz>
…rized, ACP v2 messageId (block#5195) Three pre-existing gaps in the buzz-agent observer feed fixed together per Will's ruling ("all 3 in the current PR"): 1. **OpenAI/DBv2-GPT route** — `responses_body` never requested `reasoning.summary`; GPT-family models billed thinking tokens but returned `summary: []`. 2. **Anthropic/DBv2-Claude route** — `anthropic_thinking_config()` never sent `thinking.display`; newest Claude models (Opus 5, Sonnet 5, Fable 5, Mythos 5, Opus 4.7/4.8, Mythos Preview) default to `display:"omitted"`, returning thinking blocks with an empty `thinking` field — observer rendered nothing. 3. **ACP v2 compliance** — buzz-agent negotiates ACP v2 but emitted `agent_thought_chunk` and `agent_message_chunk` without `messageId`, which ACP v2's `ContentChunk` requires (`messageId` + `content` both required at schema head `d13d1baa`). ## Changes **`crates/buzz-agent/src/config.rs`** - New `ThinkingSummary` enum (`Auto`/`Concise`/`Detailed`) with `BUZZ_AGENT_THINKING_SUMMARY` env var (default `Auto`); mirrors `BUZZ_AGENT_THINKING_EFFORT` pattern - `anthropic_thinking_config()` now emits `"display": "summarized"` in both the adaptive shape and the manual-budget shape whenever thinking is enabled - Rewrote `is_adaptive_thinking_model` and `anthropic_thinking_config` doc comments to match Anthropic's exact three-way per-model terminology (doc: https://platform.claude.com/docs/en/build-with-claude/thinking-troubleshooting#supported-models): - Opus 4.6/4.7/4.8, Sonnet 4.6: **Off** — thinking OFF by default; `type:"adaptive"` required to enable - Opus 5, Sonnet 5: **On** — thinking on by default, can be disabled; we still send `type:"adaptive"` to activate `output_config.effort` - Fable 5, Mythos 5, Mythos Preview: **Always on** — thinking cannot be disabled; we still send `type:"adaptive"` to activate `output_config.effort` **`crates/buzz-agent/src/llm.rs`** - `responses_body` emits `reasoning.summary` alongside `reasoning.effort` when effort is set (gated — no bare `reasoning:{summary}` without effort) - Covers both the pure-OpenAI Responses path and the DBv2 GPT-family Responses path **`crates/buzz-agent/src/agent.rs`** - `agent_thought_chunk` carries `"messageId": format!("{run_id}-thought-{round}")` - `agent_message_chunk` carries `"messageId": format!("{run_id}-message-{round}")` - The two IDs are distinct (thought and assistant are two logical messages per the ACP v2 Message ID RFD) - `run_id` is a fresh random token per `session/prompt` invocation so IDs are session-unique across multiple prompts **`crates/buzz-agent/src/lib.rs`** - `run_id` plumbed into `RunCtx` (was already generated in `run_prompt`, just not threaded through) **`crates/buzz-agent/tests/golden_transcripts.rs`** - `test_acp_v2_chunks_carry_message_id` — negotiates v2, drives two consecutive `session/prompt` calls, asserts: both chunk types carry non-empty `messageId`; thought and message IDs are **distinct**; IDs do **not** recur across the two prompts in the same ACP session **`desktop/src-tauri/src/managed_agents/env_vars.rs`** - `BUZZ_AGENT_THINKING_SUMMARY` added to `is_safe_to_reveal` allowlist **`desktop/src-tauri/src/commands/agent_config_tests.rs`** - Tests for `BUZZ_AGENT_THINKING_SUMMARY` allowlist entry (case-insensitive) ## Tests added - `parse_thinking_summary_round_trips_all_values` - `parse_thinking_summary_unset_and_empty_yield_auto` - `parse_thinking_summary_is_case_insensitive` - `parse_thinking_summary_rejects_unknown_value` - `thinking_summary_as_str_mapping` - `responses_body_summary_present_iff_effort_set` - `responses_body_emits_configured_summary_mode` - `responses_body_concise_summary_mode` - `anthropic_thinking_config_adaptive_emits_display_summarized` - `anthropic_thinking_config_manual_budget_emits_display_summarized` - `test_acp_v2_chunks_carry_message_id` (integration test — two-prompt cross-session case) ## Notes - **DBv2 gateway parity for `display`**: unverified — the DBv2 Claude route proxies Anthropic Messages shape, but whether the gateway passes `thinking.display` through is not confirmed. Flagged here rather than blocking on it. - buzz-acp and Desktop TS are unchanged — they already parse `messageId` as optional and will pick it up from the wire automatically. - Chat Completions and OpenRouter paths: untouched. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
## Overview **Category:** improvement **User impact:** Link previews appear in the composer and travel as privacy-safe sender-authored snapshots, so recipients never contact the linked site merely by opening a conversation. **Problem:** Cold-cache link paste could freeze the composer before the URL painted; recipient-side unfurling leaked visits; invalid or unresolved preview work could interfere with sending or leave dead cards behind. **Solution:** Paint pasted links before starting cold resolver work, resolve only in the sender's composer, attach only complete validated snapshots at Send, and render authored snapshots without recipient fallback fetching. ## Behavior - **Cold paste stays responsive:** bare and angle-bracket URL paste paths commit the visible link before resolver work begins. - **Sender-only fetching:** metadata is resolved while composing; recipients render only the sender-authored snapshot. - **Send never waits:** pending, failed, invalid, and unsendable previews are omitted. They do not block or cancel the message. - **Terminal misses disappear:** failed, timed-out, or 404 resolver results remove the composer card while preserving visible link text. - **Display-text links work:** Markdown links such as `[review the pull request](…)` produce and send the same snapshots as bare URLs. - **Compact and Rich presentation:** Compact remains the default; Rich preserves source description line breaks and paragraphs. - **Immediate draft-wide dismissal:** clicking × immediately hides all previews for the draft, suppresses links pasted later, and emits only `["link-preview", "none"]`. No confirmation detour. Suppression resets after send or clearing the draft. - **Zero recipient fallback:** missing, stale, malformed, off-relay, unsupported, or suppressed snapshots remain ordinary visible links; recipients never regenerate them. ## Implementation - Resolve previews from deferred composer URL state so paste can paint first. - Upload finished preview media to the active community relay and snapshot only valid, sendable media references. - Atomically capture ready snapshots at submit time; never append a late preview after send. - Validate snapshot and suppression tags in desktop/native and relay ingestion, rejecting duplicate or mixed forms. - Render composer previews as stable 55px attachment cards at desktop and narrow widths. - Add deterministic E2E coverage for cold paste, ready/pending/failed/invalid previews, display-text links, multiline Rich descriptions, immediate dismissal, later-pasted links, and suppression reset. ## Validation Validated head: `9807ba8952f190e76153834abf8ab61dd40be5e2` - Push hooks passed: `check-push-org`, branch skew, desktop check, mobile tests, desktop tests, Rust tests, and desktop Tauri checks. - Focused screenshot E2E at the validated head: 5/5 passed across Compact/Rich composer and recipient states, 800px/420px geometry, display-text links, multiline descriptions, and immediate dismissal. - PR CI was triggered for this exact head and is currently running; completed checks are green at the time of this update. - Worktree is clean and both PR head and validated branch resolve to `9807ba895…`. ## Screenshots ### Compact composer | Loading | Ready | |---|---| |  |  | ### Rich composer | Loading | Ready | |---|---| |  |  | ### Responsive composer | 800px loading | 800px ready | |---|---| |  |  | | 420px loading | 420px ready | |---|---| |  |  | ### Recipient presentation | Compact | Rich | |---|---| |  |  | ### Display-text Markdown link | Composer | Recipient | |---|---| |  |  | ### Rich multiline description  ### Immediate dismissal | Before × | Immediately after × | |---|---| |  |  | --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
## Summary <!-- What does this change and why? --> block#3419 is a tauri bug (tauri-apps/tauri#15110), which is already fixed in tauri-apps/tauri#15596. All we need is bump the @tauri-apps/cli version to include the bug fix. ```sh pnpm update --filter ./desktop @tauri-apps/cli@2.11.4 ``` This pr simply includes the changes after running the update command. ### Related issue <!-- Fixes block#1234, or N/A. Before opening: search existing issues/PRs for duplicates — link the closest one, or say "none found". --> fix block#3419 close block#3436. this pr supersedes it. ### Testing <!-- How was this verified? UI change? Include before/after screenshots (or a short recording). --> build the appimage and check the symlink in the appimage using `unsquashfs`. ```sh $ unsquashfs -o 944632 -ll /tmp/buzz/desktop/src-tauri/target/release/bundle/appimage/Buzz_0.5.4_amd64.AppImage | grep -i dirIcon lrwxrwxrwx root/root 8 2026-08-04 21:54 squashfs-root/.DirIcon -> Buzz.png ``` Signed-off-by: Tsung-Han Yu <14802181+johan456789@users.noreply.github.com>
…lders (block#4975) ## What users saw `buzz messages send` silently removed an explicitly supplied self-mention. The caller passed `--mention <sender-pubkey>` and received `accepted:true`, but the signed event had no matching `p` tag and `mention_pubkeys` was empty. ## Why it happened `nostr` 0.44 strips `p` tags matching the signer's pubkey by default. The codebase already opts out with `.allow_self_tagging()` for identity archive and unarchive requests, but the message and forum builders that accept mentions did not. The library therefore removed the tag during signing after the CLI had validated the explicit mention. ## What changed Added `.allow_self_tagging()` to all three event builders that accept mention tags: - `build_message` (kind 9) - `build_forum_post` (kind 45001) - `build_forum_comment` (kind 45003) An explicit mention now survives signing even when it matches the sender. ## How this was tested Added one regression test per builder. Each test signs with the same key included in the mention list and asserts that the resulting event preserves the self-referential `p` tag. Validation at `cd0f30bca`: ```text ./bin/cargo fmt --all -- --check cargo test -p buzz-sdk --lib cargo test -p buzz-cli --lib cargo clippy -p buzz-sdk -p buzz-cli --all-targets -- -D warnings ``` All 257 `buzz-sdk` tests and all 321 `buzz-cli` tests passed, and formatting and strict Clippy checks completed successfully. ## Scope and non-goals - Does not change mention validation, deduplication, or channel-member checks. - Does not change `normalize_mention_pubkeys`, which is not used by the messages-send path. - Does not add a dropped-mentions output field because the explicit tags are now preserved. Closes block#4906. --------- Signed-off-by: Brad Groux <bradgroux@hotmail.com> Signed-off-by: npub17q2gdupkvswvk5kprwc7plergm4gn295uw6fe4mjyjv53ahuhtnq02jd3f <f01486f036641ccb52c11bb1e0ff2346ea89a8b4e3b49cd772249948f6fcbae6@digitalmeld.communities.buzz.xyz> Signed-off-by: Brad Groux <3053586+BradGroux@users.noreply.github.com> Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: npub17q2gdupkvswvk5kprwc7plergm4gn295uw6fe4mjyjv53ahuhtnq02jd3f <f01486f036641ccb52c11bb1e0ff2346ea89a8b4e3b49cd772249948f6fcbae6@digitalmeld.communities.buzz.xyz> Co-authored-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
**Category:** fix **User Impact:** Mobile users who jump to Latest now see the newest message fully above the composer instead of partially hidden behind it. **Problem:** The channel message list treated the raw viewport bottom as the latest boundary even though the composer occupies part of that viewport. Latest jumps and follow-mode corrections could therefore place the newest message underneath the composer. **Solution:** Derive the latest alignment from the measured composer inset and use that same boundary for scrolling, follow detection, and layout correction. <img width="498" height="1008" alt="Screen Recording 2026-08-05 at 5 18 19 PM" src="https://github.com/user-attachments/assets/a7fc1a94-3ffb-4c34-908d-9bf4f3f082b4" /> <details> <summary>File changes</summary> **mobile/lib/features/channels/channel_detail_page/message_list.dart** Aligns Latest navigation and follow-mode correction with the visible bottom edge above the composer, and evaluates boundary state against the same geometry. **mobile/test/features/channels/channel_detail_page_test.dart** Adds a regression assertion that the newest live message clears the composer and that the Latest control disappears after navigation. </details> ## Reproduction steps 1. Open a mobile channel with enough messages to scroll away from the newest message. 2. Tap **Latest**. 3. Confirm the newest message is fully visible immediately above the composer and the **Latest** control disappears. 4. Resize the composer or keyboard while following latest and confirm the newest message remains above the composer. ## Tested fix The newest message remains fully visible above the composer after jumping to **Latest**.  ## Validation - `flutter analyze` — no issues - `flutter test` — 1,243 passed --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
## Buzz Desktop release v0.5.6 - **Frozen main:** `78c87ae20e182fffdd99744d6c9ff99df82b159c` - **Reviewed candidate:** `277d98a5cfb6d3b9af8b75122988f7a7df33ed5d` - **Previous desktop release:** `desktop-v0.5.5` - **Proposed immutable tag:** `desktop-v0.5.6` This PR may be **squash merged** after the Desktop Release Candidate check and all protected-branch checks pass. Merging authorizes publication of the exact reviewed candidate; later or unrelated changes on `main` cannot alter it. The checked-in changelog accounts for every non-merge commit in the release range. The Desktop tag points to the reviewed candidate commit, not the later squash commit. Publication remains bound to that immutable candidate tag. Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Release Automation <release-automation@users.noreply.github.com>
## Summary - treat provider `max_tokens` as an interrupted assistant response and continue the same turn with actionable feedback - discard tool calls from truncated responses, including malformed partial arguments, so they are neither executed nor replayed with invalid tool-result pairing - bound recovery to two retries while preserving normal finite `max_rounds` accounting ## Verification - `cargo fmt --all -- --check` - `cargo test -p buzz-agent` (422 unit tests plus all package integration/doc suites passed) - `cargo clippy -p buzz-agent --all-targets -- -D warnings` ## Notes The pre-push repository-wide hook also ran. Its Rust tests passed (2,270 passed, 14 ignored), but its `buzz-db` unit-test build was blocked because local rustc 1.89 is below sqlx 0.9's rustc 1.94 requirement. The affected package suite above is green on the exact pushed commit. Originating Buzz channel: `c3252dd2-0142-4e01-88c7-a2183c3960a5` Signed-off-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz> Co-authored-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
…block#5228) **Category:** fix **User Impact:** People who onboard by importing an existing key or recovering from a phone can now use "Skip for now" (and Next) on the harness setup and model config steps, instead of getting stuck. **Problem:** On the "Set up your agent harnesses" and "Configure your default model settings" onboarding steps, clicking **Skip for now** — or **Next** — did nothing for anyone who reached those steps by importing an existing key or recovering an identity from a phone. The app stayed frozen on the step. **Solution:** The onboarding state machine sets `continuingPubkeyRef` to the current pubkey on import/recovery to keep the flow on `onboarding` until setup finishes (added in block#4845). But `complete()` never cleared that ref, so once it matched the current pubkey the stage stayed pinned to `onboarding` forever — completion could never win. `complete()` now clears the ref so finishing/skipping actually settles the flow. Fresh-generated keys never set the ref, which is why first-run fresh-key skip already worked and the gap went unnoticed. <details> <summary>File changes</summary> **desktop/src/features/onboarding/machineOnboarding.ts** Clear `continuingPubkeyRef` inside `complete()` so an imported/recovered identity's "continuing" marker no longer outlives completion and pin the stage to `onboarding`. **desktop/tests/e2e/onboarding.spec.ts** Add a regression test that imports an existing key, reaches harness setup, clicks **Skip for now**, and asserts onboarding exits (reaches community onboarding). This fails without the fix. The existing skip tests only exercised the fresh-key path, which never set the ref — hence the gap. </details> ## Reproduction steps 1. Start onboarding and choose **Use an existing key** (or recover from a phone); import a key and continue to **Set up your agent harnesses**. 2. Click **Skip for now** (or **Next**). Before this change, nothing happens — the step is stuck. The same trap hits **Configure your default model settings**. 3. With this change, Skip/Next advances out of onboarding as intended. 4. Automated: `pnpm build:e2e && pnpm exec playwright test onboarding.spec.ts --project=integration -g "imported-key users can skip out of harness setup"` — passes with the fix, fails without it. ## Root cause Introduced by block#4845 (`feat(identity): recover desktop identity from a signed-in phone`), which added `continuingPubkeyRef.current === currentPubkey` as an independent condition selecting the `onboarding` stage. That guard has no off switch: `complete()` set the completion flag but never cleared the ref, so the OR'd condition kept the stage pinned. Not a revert candidate — the guard's intent (keep a just-published identity in onboarding until setup finishes) is correct; it just needed to release on completion. Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
…rride (block#5242) ## Problem Two v0.5.6-only regressions were introduced by block#4614 (the first enforced Tauri CSP): 1. **Tab-complete caret regression** — after tab-completing an @mention, #channel, or :emoji: shortcode, the cursor landed inside the inserted text instead of after the trailing space. TipTap inserts the correct text including the trailing space, but without its base stylesheet (`.ProseMirror { white-space: break-spaces }`) the trailing space collapses visually and the caret appears mid-name. 2. **Emoji picker unstyled** — the emoji-mart picker rendered as a giant unstyled layout (oversized search SVG, collapsed grid) because emoji-mart's shadow-root stylesheet injection was also blocked. Both symptoms have the same root cause. ## Root Cause Tauri's build-time asset processor scans `index.html` for inline `<style>` elements, injects a nonce token, and adds the corresponding `'nonce-…'` source to `style-src` at runtime. Per the CSP spec, **once a nonce is present in a directive, the browser ignores `'unsafe-inline'` for that directive**. `index.html` contained an inline `<style>` with the boot background color. When Tauri nonced it and injected `'nonce-…'` into `style-src`, the intended `style-src 'self' 'unsafe-inline'` became effectively `style-src 'self' 'nonce-…'` — blocking any runtime stylesheet injection not covered by a matching nonce: - TipTap's `injectCSS()` → `createStyleTag()` injecting `.ProseMirror { white-space: break-spaces; … }` - emoji-mart's shadow-root `document.createElement('style')` injection (Inline scripts follow a separate path — they are SHA-256 hashed, not nonced.) This only reproduces in packaged builds (where Tauri's custom protocol serves the HTML and enforces the policy). `tauri dev` loads from the Vite dev server and is not affected. ## Fix Move `html { background-color: #000; }` from an inline `<style>` in `index.html` to `desktop/public/boot.css`, linked via `<link rel="stylesheet">`. A linked stylesheet is not subject to Tauri's nonce injection, so `'unsafe-inline'` in `style-src` applies as declared. The `<link>` is render-blocking (same as the inline style was), so boot-flash behaviour is identical. **The production CSP string is unchanged.** This fix makes the policy apply as intended — no security properties are altered. Will's follow-up with the security team (Jordan Mecom / Eli Foster, authors of block#4614) is noted for post-ship. A Tauri-faithful CSP harness for the Vite dev path (so this class of regression is visible before a packaged build) is tracked as a separate follow-up. ## Files Changed - `desktop/index.html` — replace inline `<style>` with `<link rel="stylesheet" href="/boot.css" />` - `desktop/public/boot.css` — new file, the extracted `html { background-color: #000; }` plus rationale comment - `desktop/src-tauri/tests/csp.rs` — update comment: nonce for styles, SHA-256 for the boot script ## Testing - `just desktop-typecheck` ✅ - `just desktop-test` ✅ (4535/4535) - `just desktop-tauri-test` ✅ (all Rust tests including `csp.rs`) - Packaged validation: `pnpm tauri build --debug` completed; compiled binary bakes `style-src 'self' 'unsafe-inline'` with no nonce source injected ✅ --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
## Summary - serialize the relay error-message test with all other tests mutating the process-wide admission gate - clear its 300-second rate-limit expiry after the assertion - prevent the paused-time waiter test from observing another test's state ## Root cause `relay::tests::oversized_hint_is_capped_in_relay_error_message_string` arms the process-wide gate for 300 seconds without taking `TEST_SERIAL` or resetting it. In a parallel test run, `relay_admission::tests::concurrent_429_extends_the_window_for_parked_waiters` can observe that expiry, producing the reported `300.001s` instead of `5s`. ## Validation - focused admission suite + relay error test repeated 10 times - pre-push `desktop-tauri-checks` passed, including the full Rust workspace suite - `branch-skew` passed Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
## Buzz Desktop release v0.5.7 - **Frozen main:** `74b913cff8512c015dc6f1a7473b253fa803f954` - **Reviewed candidate:** `f167818d25dd9f03115ab907a16f07daee2ece5c` - **Previous desktop release:** `desktop-v0.5.6` - **Proposed immutable tag:** `desktop-v0.5.7` This PR may be **squash merged** after the Desktop Release Candidate check and all protected-branch checks pass. Merging authorizes publication of the exact reviewed candidate; later or unrelated changes on `main` cannot alter it. The checked-in changelog accounts for every non-merge commit in the release range. The Desktop tag points to the reviewed candidate commit, not the later squash commit. Publication remains bound to that immutable candidate tag. Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Release Automation <release-automation@users.noreply.github.com>
## What changed Bind the development Compose stack's published PostgreSQL, Redis, Adminer, Keycloak, MinIO, and Prometheus ports to `127.0.0.1`. ## Why Docker publishes a host port on every interface when no host address is specified. Running the development stack on a remote workstation or VPS therefore exposes its infrastructure services to that machine's public networks. Loopback bindings retain host-local development access and Docker's internal `buzz-net` connectivity without making those services Internet-reachable. ## Impact Local workflows continue using the same ports. Deliberate remote administration now requires an SSH tunnel or another trusted private-network path. ## Validation - `docker compose -f docker-compose.yml config --quiet` - Recreated the six affected services with their existing named volumes and Docker network - PostgreSQL remained healthy and retained all 54 application tables - Redis, MinIO, and Prometheus health checks passed - All affected ports were closed on the host's public IPv4 and IPv6 addresses while remaining available on loopback Origin: `buzz://message?channel=199eb7bc-3feb-484f-ae0e-4995123721ea&id=1c5bc387e86e21bb31677f56e1c862d4d9a17943bce91f8d93e825d029ce7f72` Signed-off-by: Paweł Karniej <karniej.p@gmail.com>
…starve the handoff summary (block#5248) ## Problem The handoff summarizer sends `max_tokens: 8192` (`HANDOFF_MAX_OUTPUT_TOKENS`) with no reasoning budget separation. On reasoning models, thinking tokens count against that cap: the model can spend the entire budget reasoning, length-stop with empty `content`, and `summarize()` — which only reads `content` — reports an empty summary. The handoff then degrades to lossy history truncation. Observed on deepseek-v4-flash during a terminal-bench 2.1 run (tb21-solo-3, 89 tasks): **13 consecutive handoff attempts across 5 trials failed exactly this way** (`handoff returned empty summary; truncating`), each burning ~3 minutes of full-cap reasoning, before a stochastically-short reasoning run finally fit. circuit-fibsqrt alone: 5 failures, 5 truncations, then success on attempt 6. video-processing failed its task by one frame after 3 context truncations. ## Fix `openrouter_summary_body` now grants reasoning its own equal-sized budget and excludes it from the response: - `reasoning.max_tokens = max_output_tokens` — thinking gets a dedicated budget instead of competing with the summary text - `reasoning.exclude = true` — reasoning is never in the response body; `summarize()` only reads `content` - `max_tokens = max_output_tokens * 2` — the total cap covers both budgets, so the text budget the caller asked for is actually available for text Non-reasoning endpoints ignore the `reasoning` object. Deliberately not paired with `provider.require_parameters`, for the reasons documented at `apply_openrouter_mutations` (it hard-404s valid model ids). The prior test `openrouter_summary_carries_neither_reasoning_nor_provider` asserted `reasoning` absent from the summary body — that assertion guarded against *effort-based* reasoning leaking in from config (the body is built independently of `cfg`, which is still true and still tested: `reasoning.effort` stays unset). Replaced with `openrouter_summary_budgets_reasoning_separately_and_carries_no_provider`. ## Verification - `cargo test -p buzz-agent`: 422 unit + 110 integration tests pass at bb2fedd - `cargo fmt` / `cargo clippy -p buzz-agent --all-targets`: clean - Not yet validated against a live OpenRouter reasoning endpoint — the failing scenario needs a long-context session to trigger organically. Evidence for the mechanism is from run artifacts (13/13 empty-summary length-stops on deepseek-v4-flash) and OpenRouter's documented `reasoning.max_tokens`/`reasoning.exclude` semantics. --------- Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
**Category:** improvement **User Impact:** Users can create, discover, and import agents from one consistent Add agent dialog. **Problem:** Agent creation, discovery, and import were split across a dropdown and separate dialogs, making the Add agent flow fragmented. The existing E2E suite also continued targeting the deleted dropdown after the flows were unified. **Solution:** Route the new-agent card directly into a unified dialog with dedicated Create, catalog, and Import navigation, then update the affected E2E coverage to exercise that interface and its current empty state. <details> <summary>File changes</summary> **desktop/src/features/agents/ui/AgentDefinitionDialog.tsx** Supports rendering the agent definition form inside the unified Add agent experience while retaining the standalone dialog behavior. **desktop/src/features/agents/ui/AgentDefinitionDialogShell.tsx** Adds the shared shell used to present agent-definition content consistently in embedded and standalone contexts. **desktop/src/features/agents/ui/AgentDialog.tsx** Passes the revised dialog state and close behavior through the existing agent dialog entry point. **desktop/src/features/agents/ui/AgentsView.tsx** Connects the Agents page to the unified Add agent dialog and opens newly added catalog agents in their profile panel. **desktop/src/features/agents/ui/PersonaCatalogDialog.tsx** Combines catalog browsing, agent creation, and snapshot import behind persistent navigation, including dirty-navigation confirmation. **desktop/src/features/agents/ui/UnifiedAgentsSection.tsx** Replaces the new-agent dropdown with a direct Add agent entry point and adjusts the responsive card grid. **desktop/src/features/agents/ui/personaLibraryCopy.ts** Updates catalog-facing copy for the unified experience. **desktop/src/features/agents/ui/usePersonaActions.ts** Returns the resolved local persona after catalog activation so the caller can open the added agent. **desktop/tests/e2e/agent-readiness-screenshots.spec.ts** Opens the embedded create pane directly for readiness screenshots. **desktop/tests/e2e/agents.spec.ts** Covers unified Create, catalog, and Import navigation and asserts the current shared-agent empty state. **desktop/tests/e2e/global-agent-config-screenshots.spec.ts** Updates global configuration screenshot setup for direct create-pane entry. **desktop/tests/e2e/inline-custom-harness.spec.ts** Updates custom harness setup for the embedded create form. **desktop/tests/e2e/persona-env-vars.spec.ts** Updates environment-variable and model-provider scenarios for direct create-pane entry. **desktop/tests/e2e/persona-model-combobox-screenshots.spec.ts** Updates model combobox screenshot setup for direct create-pane entry. **desktop/tests/e2e/smoke.spec.ts** Updates agent-creation smoke coverage for the unified Add agent dialog. **desktop/tests/e2e/where-to-run-config.spec.ts** Updates provider-selection coverage for the embedded create form. </details> ## Reproduction steps 1. Open the Agents page and select the new-agent card. 2. Confirm the Add agent dialog opens directly on Create without an intermediate dropdown. 3. Use the left navigation to browse shared agents and open Import. 4. Select a catalog agent and confirm the dialog closes and the added agent's profile panel opens. 5. Run the affected desktop Playwright smoke and integration specs and confirm all scenarios pass. --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
…nchmark agent rounds (block#5318) ## Problem Two failure modes from the `tb21-glm52-crusoe-1` benchmark run (GLM-5.2 solo, TB2.1) wedged or killed 13 of 89 trials without the model being at fault: 1. **Conversation poisoning on text-only endpoints.** Crusoe's serverless `crusoeai/GLM-5.2-NVFP4` rejects any request whose history contains an image with `400: ... is not a multimodal model`. The recovery machinery for exactly this case already exists — `AgentError::UnsupportedImageInput` → `replace_unsupported_images()` strips the image blocks, marks the tool result as an error, substitutes a text placeholder, and continues the turn. But classification only matched OpenRouter's 404 body (`no endpoints found that support image input`) and was only consulted on the 404 arms. The Crusoe 400 fell through to terminal `AgentError::Llm`: the image stayed in history, every subsequent call failed identically, buzz-acp rode its 10-retry ladder (~40 min), and the trial idled to budget death. Measured blast radius: **8 trials wedged, 12.7h aggregate idle-after-poison.** 2. **Bounded agent rounds in benchmark trials.** The harness default `DEFAULT_MAX_AGENT_ROUNDS = 32` ended solo trials mid-work when turns rotated (thinking-heavy models hit max_tokens rotation fast; 4 trials died this way). Benchmark trials already have a wall-clock budget as the real limit — the round cap only converts recoverable rotation into trial death. ## Fix - `is_unsupported_image_input_error()` also matches the verbatim `is not a multimodal model` body. Matcher stays deliberately tight (same doctrine as `is_context_length_error`): misclassifying a generic 400 as recoverable would mutate history for an error that removing images cannot fix. - Both status ladders — shared `post()` and `openrouter_post()` — consult it on their 400 arms and return the typed `UnsupportedImageInput` (OpenAI-compatible providers report this as 400; a BYOK/passthrough upstream can surface the provider's own 400 through OpenRouter). - Harness `DEFAULT_MAX_AGENT_ROUNDS` → `0` (unbounded — `BUZZ_AGENT_MAX_ROUNDS=0` is the agent config's documented unbounded value). Per-agent `budget.max_calls` in manifests still overrides. ## Acceptance - A 400 with the image-rejection body reaches the existing image-strip recovery path instead of wedging the session — asserted through `complete()` (covers the return path into the convergence mapper) and at the `openrouter_post` terminal, both proving single-attempt (a deterministic capability rejection must never be retried). - Ordinary 400s stay terminal `AgentError::Llm` (existing negative tests unchanged). - Benchmark trials run unbounded rounds by default; python tests updated for 0-is-legal with a negative arm at -1. ## Verification - `cargo test -p buzz-agent`: 427 + 18 + 20 + 15 + 8 + 1 + 48 passed, 0 failed (full package, 3 consecutive clean runs) - `cargo clippy -p buzz-agent --all-targets`, `cargo fmt --check`: clean - `uv run --extra dev pytest tests/` in harbor-buzz-orchestra: 35 passed - Pre-push hooks (full workspace rust-tests + desktop-tauri-checks) green on rustc 1.95.0 at head b043860 Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
## Buzz Desktop release v0.5.8 - **Frozen main:** `6a17d035f79ad582ca3f4f3cdc38d376f2c4087f` - **Reviewed candidate:** `f3de860574bb3119018b4592353e9761635aeb07` - **Previous desktop release:** `desktop-v0.5.7` - **Proposed immutable tag:** `desktop-v0.5.8` This PR may be **squash merged** after the Desktop Release Candidate check and all protected-branch checks pass. Merging authorizes publication of the exact reviewed candidate; later or unrelated changes on `main` cannot alter it. The checked-in changelog accounts for every non-merge commit in the release range. The Desktop tag points to the reviewed candidate commit, not the later squash commit. Publication remains bound to that immutable candidate tag. Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Release Automation <release-automation@users.noreply.github.com>
## Buzz Relay release v0.2.1 ### Changes since relay-v0.2.0: - fix(sdk): preserve self-mention p tags in message and forum event builders ([block#4975](block#4975)) ([`78c87ae20e`](block@78c87ae)) - feat(desktop): adding rich link previews to messages ([block#3818](block#3818)) ([`1922d49cb2`](block@1922d49)) - feat(relay): accept kind:30179 private managed-agent events at ingest ([block#5133](block#5133)) ([`ad923353a2`](block@ad92335)) - fix(media): require authenticated reads ([block#4610](block#4610)) ([`769ac70b74`](block@769ac70)) - feat(identity): recover desktop identity from a signed-in phone ([block#4845](block#4845)) ([`6eb65919f1`](block@6eb6591)) - ci: prove the relay-driven mesh lifecycle — discover, join, infer, deny — with real nodes ([block#3862](block#3862)) ([`38bf642fcf`](block@38bf642)) - relay: fuzz WebSocket 1012 restart-close timing on graceful drain (BUZZ_DRAIN_JITTER_MS) ([block#4542](block#4542)) ([`e14fff74d0`](block@e14fff7)) - fix(reactions): support max-length custom emoji ([block#3833](block#3833)) ([`2ea9385015`](block@2ea9385)) - fix(channels): restrict private-channel invitations ([block#4612](block#4612)) ([`efe1893dd3`](block@efe1893)) - fix(workflow): bind trigger author to the signed event ([block#4607](block#4607)) ([`885bed35ee`](block@885bed3)) - fix(git): revoke access for banned relay members ([block#4608](block#4608)) ([`997b8caaa4`](block@997b8ca)) - Define private managed agent wire protocol ([block#4593](block#4593)) ([`067c085f37`](block@067c085)) - perf(relay): index channel-id lookups and skip trace-only reads ([block#4647](block#4647)) ([`bc9e6528a7`](block@bc9e652)) - Polish mobile inbox and media flows ([block#4512](block#4512)) ([`feccf4eabc`](block@feccf4e)) - fix(git): allow deleting the default branch ([block#4297](block#4297)) ([`fc598f5f8d`](block@fc598f5)) - feat(projects): add buzz projects CLI commands (NIP-MP kind:30621) ([block#4020](block#4020)) ([`b7bb15122e`](block@b7bb151)) - perf(relay): serve relay-membership checks from the read replica ([block#4124](block#4124)) ([`ac4fa13b8e`](block@ac4fa13)) - fix(relay): allow open relays to set their NIP-11 workspace icon (kind:9033) ([block#3998](block#3998)) ([`5765fc74b7`](block@5765fc7)) - feat(relay): accept kind:30621 multi-repo projects at ingest ([block#3171](block#3171)) ([`cb9701cd30`](block@cb9701c)) - feat(relay): raise hosted community limit to five ([block#3829](block#3829)) ([`10d5a26414`](block@10d5a26)) - fix(relay): align NIP-11 max_limit with REQ ceiling ([block#3635](block#3635)) ([`23f0c26b1c`](block@23f0c26)) - feat(relay): gate kind 30178 team-catalog reads behind the shared tag ([block#3358](block#3358)) ([`114d40d9d3`](block@114d40d)) - fix(db): isolate usage metrics advisory-lock test on scratch DB ([block#3670](block#3670)) ([`dba97eecd9`](block@dba97ee)) - perf(presence): reduce heartbeat frequency ([block#3783](block#3783)) ([`bf139e8d0b`](block@bf139e8)) - feat(mesh): upgrade embedded mesh to v0.74 and harden shared compute (split 1/2 of block#3467) ([block#3741](block#3741)) ([`4933672eb4`](block@4933672)) - feat(replica): portable heartbeat-token fence with snapshot-local reader routing ([block#3268](block#3268)) ([`63496cc1d4`](block@63496cc)) - fix(git): channel binding tooling + author remediation for unbound repos ([block#3626](block#3626)) ([`788b3c002b`](block@788b3c0)) - feat: configure S3 URL addressing style ([block#3400](block#3400)) ([`7012d86d52`](block@7012d86)) - feat(tracing): correlate trace IDs in relay logs ([block#3608](block#3608)) ([`005b5b819a`](block@005b5b8)) - fix(relay): avoid subscription lock inversion ([block#3413](block#3413)) ([`22be8bb351`](block@22be8bb)) - feat(cli): add users set-status command for NIP-38 profile status ([block#3253](block#3253)) ([`60158fce3e`](block@60158fc)) - feat(relay): make Postgres pool size configurable, default 50 ([block#3191](block#3191)) ([`2ce2d71cc3`](block@2ce2d71)) - feat(tracing): add datastore tracing plumbing ([block#2760](block#2760)) ([`e94b9aeda0`](block@e94b9ae)) - feat(invites): add use-limited invite links ([block#3141](block#3141)) ([`d500c2d5cf`](block@d500c2d)) - feat(admin): show reported message content in report detail ([block#3149](block#3149)) ([`f069a85503`](block@f069a85)) - resolve findings ([block#3150](block#3150)) ([`9b0f744804`](block@9b0f744)) - Revert "fix(cli,relay): resolve agents by verified owner" ([block#3168](block#3168)) ([`a041e2d21e`](block@a041e2d)) - fix(cli,relay): resolve agents by verified owner ([block#2615](block#2615)) ([`c3084b36d9`](block@c3084b3)) - fix(security): enforce durable community ban on NIP-43 relay-admin kinds 9030-9033 ([block#3128](block#3128)) ([`e2e0079101`](block@e2e0079)) - fix(security): authorize kind:9000 role changes in both directions ([block#3017](block#3017)) ([`00ecf2cac7`](block@00ecf2c)) - feat(desktop): handle project work from Inbox ([block#3117](block#3117)) ([`c5c4f390b6`](block@c5c4f39)) - feat(relay): make per-owner community limit configurable via BUZZ_MAX_COMMUNITIES_PER_OWNER ([block#2599](block#2599)) ([`2a051a404d`](block@2a051a4)) - feat(relay): add author-only-unless-shared read gate for kind 30175 ([block#2768](block#2768)) ([`ab3af82871`](block@ab3af82)) - fix(core): block IPv6 transition SSRF targets ([block#2801](block#2801)) ([`c26bf5945d`](block@c26bf59)) - fix(workflow): bypass system proxies for webhooks ([block#2800](block#2800)) ([`60a171b19e`](block@60a171b)) - fix(audit): hash created_at at the precision Postgres stores ([block#2638](block#2638)) ([`264a56a226`](block@264a56a)) - feat(desktop): make pull request reviews actionable ([block#2510](block#2510)) ([`9081ab0ec9`](block@9081ab0)) - fix(relay): decompress gzip-encoded git smart-HTTP request bodies ([block#2670](block#2670)) ([`5ca36e7b91`](block@5ca36e7)) - fix(sharing): preserve agent/team snapshot tEXt chunks through media sanitization ([block#2438](block#2438)) ([`b096b0a15a`](block@b096b0a)) - fix(relay): send 1012 restart close to all clients on graceful drain ([block#2575](block#2575)) ([`1911c69aa2`](block@1911c69)) - fix(media): sanitize animated image uploads ([block#2524](block#2524)) ([`8f8f5fa5a4`](block@8f8f5fa)) - fix(channels): strip leading hash prefixes from names ([block#2250](block#2250)) ([`d0ab3fdb05`](block@d0ab3fd)) - feat(relay): make Redis pool size configurable, default 16 ([block#2521](block#2521)) ([`bcc3e13069`](block@bcc3e13)) - feat(desktop+acp): spawn a harness per (agent, community) pair at GUI startup — warm sockets, lazy LLM pool ([block#2122](block#2122)) ([`61cc738ee8`](block@61cc738)) - feat(media): add S3-truth per-community storage sweep ([block#2044](block#2044)) ([`bd37a4d584`](block@bd37a4d)) - feat(relay): log NIP-98 pubkey attribution on HTTP bridge requests ([block#2206](block#2206)) ([`7e34bee62c`](block@7e34bee)) - Revert "feat(relay): inventory unreachable Git objects" ([block#2275](block#2275)) ([`0fb820f9bf`](block@0fb820f)) - feat(relay): inventory unreachable Git objects ([block#2264](block#2264)) ([`3afc9dae15`](block@3afc9da)) - relay: add author_type label to buzz_events_stored_total ([block#2243](block#2243)) ([`b9f54c43fe`](block@b9f54c4)) - fix(git): make project branch workflows reliable ([block#2213](block#2213)) ([`166f27be4b`](block@166f27b)) - feat(cli): manage repository protection rules ([block#2193](block#2193)) ([`f94324598d`](block@f943245)) - feat(cli): add agents archive/unarchive/archived subcommands ([block#2173](block#2173)) ([`7d7992067b`](block@7d79920)) - fix(mobile): sanitize Android image uploads ([block#2188](block#2188)) ([`ee21da90bd`](block@ee21da9)) - fix(cli): paginate channel directory queries ([block#2181](block#2181)) ([`03fe19d603`](block@03fe19d)) - fix(mobile): image upload fails due to unstripped metadata ([block#2185](block#2185)) ([`37f15b2001`](block@37f15b2)) - perf(relay): compact Git packs before manifest limits ([block#2172](block#2172)) ([`80e0ab16b0`](block@80e0ab1)) - perf(relay): cache Git pack hydration ([block#2169](block#2169)) ([`a4d82ec722`](block@a4d82ec)) - fix(relay): bound and observe Git read operations ([block#2167](block#2167)) ([`5f7c93d9c1`](block@5f7c93d)) - relay: gate push enqueue on live leases; batch matcher pipeline (T1b/T1a-repair/T2b) ([block#2145](block#2145)) ([`e43b2d5aac`](block@e43b2d5)) - relay: add audit logging disable switch ([block#2134](block#2134)) ([`bf5acabdde`](block@bf5acab)) - relay: skip TTL deadline bump for known-permanent channels (T1a write-amp) ([block#2125](block#2125)) ([`2e936d439c`](block@2e936d4)) - fix(git): carry NIP-OA delegation in auth event ([block#2120](block#2120)) ([`c12257d57a`](block@c12257d)) - Route lag-tolerant reads to an optional Postgres read replica ([block#2084](block#2084)) ([`29c48883d3`](block@29c4888)) - fix: recover community access visibility ([block#2074](block#2074)) ([`ca384d082d`](block@ca384d0)) - feat: proxy feedback-scoped admin attachments ([block#2059](block#2059)) ([`d7f918e3cb`](block@d7f918e)) - feat: add read-only deployment moderation dashboard ([block#1999](block#1999)) ([`68e670e001`](block@68e670e)) - Bug-bash round 2: table scroll, Goose instructions, workflow mention wake ([block#2034](block#2034)) ([`64b8fea6dc`](block@64b8fea)) - Strip media metadata on clients and reject it at the relay ([block#2006](block#2006)) ([`5cfd69cb0c`](block@5cfd69c)) - [codex] Hold Git concurrency permits through streaming (BUZZ-SEC-018) ([block#1916](block#1916)) ([`7baea42abb`](block@7baea42)) - [codex] Enforce shared relay admission limits (BUZZ-SEC-019) ([block#1917](block#1917)) ([`73fc0ec6cf`](block@73fc0ec)) - [codex] Block banned actors from moderation commands (BUZZ-SEC-007) ([block#1915](block#1915)) ([`caa195ca58`](block@caa195c)) - [codex] Fix relay WebSocket admission limits ([block#1682](block#1682)) ([`d3ce971fc7`](block@d3ce971)) - feat: add invite QR and mobile direct join ([block#1957](block#1957)) ([`648cbf3610`](block@648cbf3)) - fix(join-policy): require legal consent on hosted invites ([block#1987](block#1987)) ([`2e1577f76f`](block@2e1577f)) - [codex] Prevent actor-tag UI impersonation ([block#1931](block#1931)) ([`c540ec9678`](block@c540ec9)) - Scope relay runtime state by community ([block#1658](block#1658)) ([`d52dedb06f`](block@d52dedb)) - Apply optional relay join policy across join flows ([block#1894](block#1894)) ([`6c2d667575`](block@6c2d667)) - feat(media): require auth for relay media reads ([block#1926](block#1926)) ([`f308762852`](block@f308762)) - feat(relay): add community unarchive endpoint ([block#1908](block#1908)) ([`6b9641db2b`](block@6b9641d)) - feat(relay): gate Git web GUI separately ([block#1901](block#1901)) ([`34dc7dec75`](block@34dc7de)) - mesh: upgrade runtime, enforce membership, add shared compute provider ([block#1656](block#1656)) ([`54638ff4bb`](block@54638ff)) - Route Git scratch through configured volume ([block#1884](block#1884)) ([`2318b3096c`](block@2318b30)) - feat(relay): gate usage metrics behind stable leader ([block#1814](block#1814)) ([`59e9821503`](block@59e9821)) - Relay mesh: cross-pod tunnel + huddle transport (buzz-relay-mesh) ([block#1670](block#1670)) ([`ccb021d713`](block@ccb021d)) - feat(push): deliver accepted relay events as wakes ([block#1866](block#1866)) ([`bffbc5f22c`](block@bffbc5f)) - fix(db): resolve duplicate migration version ([block#1863](block#1863)) ([`08ad38a07f`](block@08ad38a)) - Add private product feedback sidecar ([block#1857](block#1857)) ([`af190c93e1`](block@af190c9)) - feat(relay): add durable community archival ([block#1834](block#1834)) ([`2b15a72675`](block@2b15a72)) - feat(push): add public APNs gateway ([block#1770](block#1770)) ([`1c006822e4`](block@1c00682)) - feat(relay): add atomic community ownership transfer ([block#1845](block#1845)) ([`52e42ccb9f`](block@52e42cc)) - Bound NIP-RS retention and search indexing ([block#1771](block#1771)) ([`1b4703021d`](block@1b47030)) - Add optional standalone pairing relay to Helm chart ([block#1799](block#1799)) ([`9b47c8548f`](block@9b47c85)) - fix(relay): publish membership snapshot on provisioning ([block#1761](block#1761)) ([`0950d392b7`](block@0950d39)) - feat(relay): per-community usage metrics ([block#1723](block#1723)) ([`620822899a`](block@6208228)) - refactor(desktop): remove vestigial MCP toolsets config ([block#1776](block#1776)) ([`dfec75b3c0`](block@dfec75b)) **To release:** merge this PR. The tag and build will happen automatically. Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Dekan Brown <dekanbro@gmail.com> # Conflicts: # desktop/src-tauri/src/lib.rs
|
Important Review skippedToo many files! This PR contains 953 files, which is 853 over the limit of 100. To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch. Upgrade to a paid plan to raise the limit. Usage-priced reviews support at most 300 files. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (4)
📒 Files selected for processing (953)
You can disable this status message by setting the Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
## Summary - temporarily allow the informational `RUSTSEC-2026-0243` advisory for the retired `nostr-relay-pool` crate - document the exact MeshLLM → `nostr-sdk 0.44.1` transitive path and removal condition - keep every other advisory and the global dependency policy enforced ## Why an exception RustSec provides no patched `nostr-relay-pool` release because the standalone crate was absorbed into `nostr-sdk >= 0.45`. Buzz inherits it through pinned MeshLLM v0.74. A direct test bump to `nostr-sdk 0.45.1` removed the retired crate but produced 13 MeshLLM API compilation errors, so the durable fix requires an upstream source migration rather than a lockfile update. This narrow exception restores the required Security check while that migration is completed. It must be removed once MeshLLM adopts `nostr-sdk >= 0.45`. ## Validation - `bin/cargo-deny --locked check --config deny.toml advisories` - `bin/cargo-deny --locked check` - `git diff --check origin/main...HEAD` - mandatory pre-push Rust and desktop/Tauri checks ## Scope One four-line `deny.toml` addition. No Rust source, lockfile, runtime, or release behavior changes. Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> Signed-off-by: Dekan Brown <dekanbro@gmail.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.
Summary
relay-v0.2.1, which includes Desktopv0.5.8Validation
cargo fmt --all -- --checkpnpm --dir desktop typecheck