fix(desktop): require codex-acp 1.1.7 for managed agents - #3097
Closed
Bharathchinneni wants to merge 1 commit into
Closed
Bharathchinneni wants to merge 1 commit into
Bharathchinneni wants to merge 1 commit into
Conversation
Signed-off-by: bharath <bharath@bharaths-MacBook-Air.local>
Bharathchinneni
force-pushed
the
fix/codex-acp-min-version
branch
from
July 27, 2026 08:46
3fcccd7 to
8cd1841
Compare
wpfleger96
added a commit
that referenced
this pull request
Jul 28, 2026
The codex adapter version gate accepted any `major >= 1`, so a 1.x `codex-acp` older than the version that fixes outbound relay access for `buzz` CLI subprocesses classified as `Available` and was never offered a reinstall. Only the 0.16.x `@zed-industries/codex-acp` adapter — which fails `--version` outright — was caught. `probe_codex_acp_version` now returns the full `(major, minor, patch)` triple and `codex_adapter_availability` compares it against a new `MIN_CODEX_ACP_VERSION` floor of `1.1.7`, the current npm latest. An adapter below the floor classifies as `AdapterOutdated`, which routes it through the existing uninstall-then-install reinstall plan. The parse requires exactly three numeric dot-separated components. Partial versions (`1.2`) and prerelease tags (`1.2.0-rc1`) return `None` and therefore classify as `AdapterOutdated` — a version Buzz cannot compare against the floor fails closed, offering a reinstall rather than running an adapter of unknown vintage. Both the floor's bump policy and the strict-parse behavior are stated in doc comments rather than left implicit. Supersedes [#3097](#3097) by @Bharathchinneni, whose semver floor and behavior tests this carries. That PR could not land as written: the two `probe_codex_acp_major_version` compatibility wrappers it kept had no non-test callers, which is a hard `clippy -D warnings` failure. The wrappers are deleted here and their call sites collapsed onto `probe_codex_acp_version`. Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
marccopson
added a commit
to marccopson/buzz
that referenced
this pull request
Jul 31, 2026
* Add mobile message image galleries (#3312)
## What
- group uploaded photos into full-width message carousels
- add a fullscreen viewer with pinch zoom, double-tap reset, swipe-down
dismissal, a centered filmstrip, and image actions
- preload nearby display-sized images for smoother swiping and keep each
upload as its own avatar-backed message
## Validation
- `just mobile-check`
- `flutter test test/features/channels/message_content_test.dart`
- iOS 26.5 simulator gesture pass
---------
Signed-off-by: kenny lopez <klopez4212@gmail.com>
* Polish mobile message and search layouts (#3121)
## Summary
- align message typography, avatars, metadata, and spacing across mobile
surfaces
- improve message follow behavior, touch feedback, and Activity popover
motion
- refine Search motion, gutters, and explicit recent-search history
## Snapshots
### Home

### Activity

### Search

## Testing
- `just mobile-check`
- `just mobile-test` (749 passed, 1 skipped)
---------
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: npub14vtk7pvazqrq9639qu7e560wnqtl0d53ca4gjuvq6jzf3k2el23qqlwa7f <ab176f059d100602ea25073d9a69ee9817f7b691c76a897180d48498d959faa2@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Signed-off-by: npub15w828kxsxu2684ynste0uah2jwkgatd99flt7ds4523hzm8ju6cshdr8hh <a38ea3d8d03715a3d49382f2fe76ea93ac8eada52a7ebf3615a2a3716cf2e6b1@buzz.block.builderlab.xyz>
Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: npub14vtk7pvazqrq9639qu7e560wnqtl0d53ca4gjuvq6jzf3k2el23qqlwa7f <ab176f059d100602ea25073d9a69ee9817f7b691c76a897180d48498d959faa2@buzz.block.builderlab.xyz>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Co-authored-by: npub15w828kxsxu2684ynste0uah2jwkgatd99flt7ds4523hzm8ju6cshdr8hh <a38ea3d8d03715a3d49382f2fe76ea93ac8eada52a7ebf3615a2a3716cf2e6b1@buzz.block.builderlab.xyz>
* Refine mobile attachment picking (#3313)
## What
- morph the composer plus button into the attachment menu, camera, and
photo surfaces
- add ordered multi-select with inline recent photos and system picker
fallback
- add native iOS attachment/photo popovers and align the Android camera
treatment
## Stack
- follows #3312
## Validation
- `just mobile-check`
- `flutter test test/features/channels/compose_bar_test.dart`
- full mobile pre-push suite
---------
Signed-off-by: kenny lopez <klopez4212@gmail.com>
* feat(chart): add relay pod extension points (#3322)
## Why
Allow operators to install wrapper binaries and override the relay
entrypoint without maintaining a duplicated Deployment outside the OSS
chart. `extraManifests` can create independent resources but cannot
extend the chart-managed relay Pod.
## What
- Add opt-in init-container, volume, volume-mount, command, and args
extension points
- Preserve image defaults when extensions are empty and compose generic
init containers with the MinIO readiness gate
- Document the distinction from `extraManifests`, add schema coverage,
and release chart 0.1.7
## Risk Assessment
Low — all new values are opt-in, and default rendered manifests are
unchanged apart from version-derived metadata. Merge publishes a new
chart version without modifying existing installations.
## References
- [OpenTelemetry Collector Pod
extensions](https://github.com/open-telemetry/opentelemetry-helm-charts/blob/main/charts/opentelemetry-collector/templates/_pod.tpl)
alongside
[extraManifests](https://github.com/open-telemetry/opentelemetry-helm-charts/blob/main/charts/opentelemetry-collector/templates/extraManifests.yaml)
- [Argo CD
extraObjects](https://github.com/argoproj/argo-helm/blob/main/charts/argo-cd/templates/extra-manifests.yaml)
alongside component-scoped Pod extension hooks
- `helm unittest` 0.8.2: 43/43 tests passed
- Helm lint, schema validation, fixture renders, and chart packaging
passed
- Oracle review found no functional issues; its literal no-`tpl`
regression test recommendation is included
Generated with Amp
---------
Signed-off-by: David Grochowski <dgrochowski@squareup.com>
Co-authored-by: Amp <amp@ampcode.com>
* fix(composer): scope multiline block formatting (#3246)
**Category:** fix
**User Impact:** Composer block formatting now applies to the intended
line or selection without collapsing multiline content.
**Problem:** Block formatting from a Shift+Enter line could convert the
entire draft, selected visual lines could collapse into one list item,
and code conversion could lose line breaks. **Solution:** Scope caret
formatting to its hard-break-delimited line and normalize explicit
selections for the destination block type while preserving neighboring
content and visual line boundaries.
<details>
<summary>File changes</summary>
**desktop/src/features/messages/lib/selectionBlockFormatting.ts**
Scopes collapsed-caret block actions to the active visual line and
normalizes multiline selections for lists and code blocks.
**desktop/src/features/messages/lib/selectionBlockFormatting.test.mjs**
Adds unit coverage for caret-line isolation across line positions and
selection directions.
**desktop/src/features/messages/ui/FormattingToolbar.tsx**
Routes list, quote, and code-block actions through the selection-aware
formatting transaction.
**desktop/tests/e2e/composer-selection-formatting.spec.ts**
Covers caret-only formatting, multiline list conversion, list-to-code
conversion, preserved hard breaks, Markdown output, and backward
selections.
</details>
## Reproduction steps
1. In the desktop composer, enter several lines using Shift+Enter and
place the caret on one line.
2. Apply a bullet list, ordered list, quote, or code block; only the
caret line should change.
3. Select several Shift+Enter lines and apply a list; each visual line
should become its own item.
4. Select several list items and apply Code block; they should become
one multiline code block while unselected neighbors remain intact.
5. Select several Shift+Enter lines and apply Code block; each line
break should remain visible.
## Screenshots/Demos
<img width="508" height="222" alt="Screen Recording 2026-07-27 at 5 29
19 PM"
src="https://github.com/user-attachments/assets/35640dea-0cfb-44f1-9b0b-a993c69cb55f"
/>
Expected multiline code-block result:
https://buzz.block.builderlab.xyz/media/d2e2668093af3b67d896a32e9799daccd236da9fc9e24ec56ddb4ebf7d01dd96.png
---------
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
* feat(cli): add users set-status command for NIP-38 profile status (#3253)
## Summary
The desktop client renders a persistent user status (NIP-38 kind:30315,
`d:general`) as the status line on profiles, but the CLI had no way to
set it — only ephemeral presence (`set-presence`, kind:20001).
Integrations that want a scriptable, durable status line (for example a
now-playing music bridge that shows the current TIDAL track on a
profile) had no entry point.
## Screenshots
<img width="1455" height="960" alt="1"
src="https://github.com/user-attachments/assets/f1669ec6-212b-4f6e-ad53-07df9aacffc9"
/>
<img width="1455" height="960" alt="2"
src="https://github.com/user-attachments/assets/5bf70f47-e5b5-4eb0-a426-b5f1ef90d2ec"
/>
This adds:
```bash
buzz users set-status --text "Working on the relay" --emoji "🔧"
buzz users set-status --text "" --emoji "🎶" # intentional emoji-only status
buzz users set-status --clear # removes the status
```
- Signs and submits the replaceable kind:30315 event via the HTTP bridge
(no WS needed — unlike presence, user status is a stored event).
- Uses the `d:general` coordinate the desktop client already reads for
the profile status line, and the same `emoji` tag shape
`SetStatusDialog` publishes.
- Event construction lives in `buzz_sdk::build_user_status()`, keyed off
`buzz_core::kind::KIND_USER_STATUS`, so the CLI command is a thin
sign/submit wrapper. Text and emoji are trimmed; a blank emoji is
omitted rather than emitted as an empty tag.
- Clearing is the explicit `--clear` flag, mutually exclusive with
`--text`/`--emoji`. It publishes an empty-content event carrying only
`d:general`, which the desktop treats as no status. `--text ""` with an
`--emoji` is an emoji-only status, not a clear.
---------
Signed-off-by: Kagan Yaldizkaya <kagan@squareup.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
* fix(desktop): gate codex-acp on a minimum supported version (#3254)
The codex adapter version gate accepted any `major >= 1`, so a 1.x
`codex-acp` older than the version that fixes outbound relay access for
`buzz` CLI subprocesses classified as `Available` and was never offered
a reinstall. Only the 0.16.x `@zed-industries/codex-acp` adapter — which
fails `--version` outright — was caught.
`probe_codex_acp_version` now returns the full `(major, minor, patch)`
triple and `codex_adapter_availability` compares it against a new
`MIN_CODEX_ACP_VERSION` floor of `1.1.7`, the current npm latest. An
adapter below the floor classifies as `AdapterOutdated`, which routes it
through the existing uninstall-then-install reinstall plan.
The parse requires exactly three numeric dot-separated components.
Partial versions (`1.2`) and prerelease tags (`1.2.0-rc1`) return `None`
and therefore classify as `AdapterOutdated` — a version Buzz cannot
compare against the floor fails closed, offering a reinstall rather than
running an adapter of unknown vintage. Both the floor's bump policy and
the strict-parse behavior are stated in doc comments rather than left
implicit.
Supersedes [#3097](https://github.com/block/buzz/pull/3097) by
@Bharathchinneni, whose semver floor and behavior tests this carries.
That PR could not land as written: the two
`probe_codex_acp_major_version` compatibility wrappers it kept had no
non-test callers, which is a hard `clippy -D warnings` failure. The
wrappers are deleted here and their call sites collapsed onto
`probe_codex_acp_version`.
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
* fix(desktop): restore the inbox icon in the sidebar (#3341)
## Why
The Inbox surface was briefly renamed to **Activity** during #2045 and
picked up a bell icon to match. The name was reverted to **Inbox**
before merge, but the icon was not.
A bell says "notification tray." Inbox is a destination — a focused,
conversation-oriented place to catch up on work relevant to you,
including drafts and reminders that have nothing to do with
notifications. The glyph should say that.
## What changed
- Swap the sidebar entry from Lucide `Bell` to Lucide `Inbox`.
- Assert the icon in `inbox-refactor-screenshots.spec.ts`. Nothing
pinned it before, which is exactly how it drifted through a rename.
This also brings desktop back in line with mobile, which already uses
`LucideIcons.inbox300` / `inbox500` for the same destination.
## Deliberately unchanged
The bell on **reminder** rows in the list pane (`InboxListPane.tsx`,
reminders → bell, drafts → file) stays. A bell is the right glyph for a
reminder; that one was never about the surface's identity.
## Verification
- The new assertion is a real guard, not a no-op: with `Bell` restored
the test fails with `Expected: 1, Received: 0` on `svg.lucide-inbox`.
Confirmed before committing.
- `biome` and `tsc` clean.
- Playwright smoke: `inbox-refactor-screenshots` 4 passed; `smoke`,
`navigation`, `channels`, `sidebar-more-unread-overlap`,
`home-collapsed-top-chrome`, `workspace-rail` — 107 passed, 1 skipped.
- Screenshot below is the regenerated `02-current-controls` shot from
the spec.
Signed-off-by: Clay Delk <clay.delk@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Unify mobile loading spinners (#3314)
## What
- add the shared desktop-style arc spinner for mobile
- replace app loading indicators with the shared component
- preserve a static pose when reduced motion is enabled
## Stack
- follows #3313
## Validation
- `just mobile-check`
- focused spinner and pairing widget tests
---------
Signed-off-by: kenny lopez <klopez4212@gmail.com>
* docs: restructure DCO guidance into scannable subsection (#3337)
Extracts the dense inline DCO paragraph from the "Before You Open a PR"
section into a dedicated `### Sign Your Commits` subsection.
## What changed
- Adds a `### Sign Your Commits` heading directly below the Conventional
Commits paragraph
- Leads with the command (`git commit -s`) in a code block
- Follows with a plain-English explainer of what the sign-off does
- Adds linkable `#### Fix unsigned commits already pushed` and `####
Auto-setup for future commits` subheadings
- Removes the old inline paragraph (content preserved, structure only
changed)
## Why
The existing guidance was buried mid-paragraph; contributors may not
find it until CI blocks them. This makes the requirement and its fix
immediately visible and actionable.
## Notes
Docs-only change, no code modified.
Signed-off-by: Cameron Hotchkies <chotchkies@block.xyz>
Co-authored-by: npub1ep9tf72jk6xgwamqj5m2j0xvqvwm9vdu3zxlz7cesxg53x52tkkqf6pa42 <c84ab4f952b68c8777609536a93ccc031db2b1bc888df17b198191489a8a5dac@buzz.block.builderlab.xyz>
* fix(desktop): keep drafts out of the Inbox All view (#3217)
## Summary
Drafts were showing up in the Home Inbox **All** view, mixed in with
messages and reminders (reported in `#buzz-bugs`). Drafts are private
composer state, not inbox activity — they now appear only under the
dedicated **Drafts** filter.
## Changes
- **`inboxListRows.ts`** — drop the `draft` row variant from
`buildInboxListRows`; the mixed view builds only `inbox` + `reminder`
rows.
- **`InboxListPane.tsx`** — remove the draft branch of the All-view
render path; `PersonalItemRow` now renders reminders only.
- **`useHomePersonalInbox.ts`** — stop enabling draft selection (and its
root-status relay probing) for the mixed view; draft selection is scoped
to the Drafts filter.
- Drafts filter behavior is unchanged: the filter badge count,
`DraftsPanel` list, and `DraftDetailPane` all still work.
## Testing
- `pnpm test` (desktop unit suite): 3697 passed, 0 failed.
- `pnpm exec biome check src/features/home tests`: clean.
- Updated `inboxListRows.test.mjs` for the two-variant row model.
- Updated the e2e test (`channels.spec.ts`) to assert All never lists
drafts and that the draft is still reachable under the Drafts filter.
- Added `drafts-all-fix-screenshots.spec.ts` capturing both states
(screenshots below).
### All view — draft is gone, messages/reminders unaffected

### Drafts filter — the draft is still listed and editable

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
* feat(desktop): refine agent catalog sharing (#2439)
## Summary
- add custom-agent catalog sharing and hide built-ins from discovery
- let owners publish later catalog updates from Share or while saving
edits — the save always persists locally, and the publish reports
`published` or `queued` (flushed automatically once the relay is
reachable again)
- preserve agent type, model, and runtime across snapshot import/export
- simplify agent and team entry points and tighten catalog layout
- migrate the legacy global retention queue into the owner's active
scope so pending catalog publishes survive the upgrade
- keep the agent list and edits usable in recovery mode by degrading to
unshared projections when scope resolution or the retention DB fails
- track catalog provenance on copied personas, so adding an
already-added foreign agent resolves to the existing copy instead of
creating a duplicate
- scope inbound persona events to the community relay they arrived on
- page the catalog read past the relay's 1,000-row query clamp
- unify the share dialog's memory-level choice into a single "What's
included" selector that drives both DM-send and copy-link delivery (all
six combinations preserved), group the delivery rows above the option
rows, and label the catalog toggle "Not shared" / "Shared"
- keep emoji avatars on catalog entries — they persist as inline
percent-encoded SVG, which the catalog projection's http(s)-only URL
guard used to drop, so a shared agent showed initials instead of its
avatar
- drop the "Active in communities" card from Agents settings, superseded
by the per-channel runtime controls in the members sidebar
## Screenshots
### Agent actions

### Team avatar stack

### Catalog sharing

### Publish while editing

### Publish from Share

### Catalog details

---------
Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
* chore(compose): remove stale typesense env vars (#3332)
Search migrated to Postgres FTS (commit f8bbe6efc).
The Typesense container was removed from compose.yml and the Helm chart,
but the cleanup missed two template/config files:
- `deploy/compose/.env.example`: `TYPESENSE_API_KEY` and
`TYPESENSE_PORT` are dead — no typesense service exists in compose.yml
and the relay binary no longer reads `TYPESENSE_API_KEY`. The
`CHANGE_ME_RANDOM_API_KEY` placeholder was never consumed, so removing
it also unbreaks the sed loop in the blog draft (one fewer no-op secret
to generate).
- `benchmarks/harbor-buzz-orchestra/scripts/benchmark.py`: generates a
typesense_api_key in state and writes `TYPESENSE_API_KEY` to the .env
file it creates.
- *Editing this file caused the
https://github.com/block/buzz/blob/main/.github/workflows/benchmark-harbor.yml
linter ci checks to run, which seemingly haven't run before, so I needed
fix the lint issues to pass this.*
---------
Signed-off-by: Kalvin Chau <kalvin@block.xyz>
Co-authored-by: npub1c4alndp82zyt9veaklm5d965quss79vlhk9awv7qu5erwhmf42qqlvc25c <c57bf9b4275088b2b33db7f746975407210f159fbd8bd733c0e532375f69aa80@buzz.block.builderlab.xyz>
* feat(desktop): add custom harness inline from agent dialogs (#3252)
Registering a custom ACP harness works today, but only from Settings →
Agents. Anyone whose first touchpoint is "New agent" has no way to
discover the custom path — the dropdown just lists the baked-in presets
plus whatever was registered earlier. This adds an inline "Add custom
harness…" entry to the harness dropdown in all three agent surfaces:
create, edit-definition (`AgentDefinitionDialog`), and instance edit
(`AgentInstanceEditDialog`).
The entry is a sentinel option (`ADD_CUSTOM_HARNESS_VALUE`, NUL-prefixed
so it can never collide with a real harness id — backend ids match
`[a-z0-9_][a-z0-9_-]*`), mirroring the `CUSTOM_ENTRY_ID` trick already
used in `HarnessCatalogDialog`. Picking it never writes into form state;
it opens `AddCustomHarnessDialog`, a thin modal wrapper hosting the
existing `CustomHarnessForm` in `chromeless` mode. `CustomHarnessForm`'s
`onSaved` now carries the saved `definition.id` (the form may rewrite
it); the two existing call sites ignore the argument, so their behavior
is unchanged.
Selection after save is deferred rather than immediate.
`usePendingHarnessSelection` holds the saved id until the runtime
catalog actually publishes it via discovery, then selects it exactly
once — so the dialog never selects an id it cannot render, and
back-to-back registrations resolve correctly. The wait is scoped to the
owning dialog's `open` state: both host dialogs stay mounted when
closed, so an unpublished id is dropped on close rather than selecting
into reset form state when discovery later catches up. Selection is
routed through each dialog's normal dropdown change handler, so
provider/model reset (and command pinning in the instance dialog) behave
identically to a hand-picked harness. Dismissing the modal leaves the
previous selection untouched. `AgentInstanceEditDialog`'s existing
"Custom command" option is a different feature (ad-hoc command override
vs. a registered reusable harness) and is untouched.
Coverage is 16 unit tests in `addCustomHarness.test.mjs` (real React
mount, following the existing `.test.mjs` pattern) plus 4 Playwright
specs in `inline-custom-harness.spec.ts` covering all three surfaces
end-to-end. Both suites were mutation-verified: treating the sentinel as
a real selection, selecting before the catalog publishes, never clearing
the pending id, ignoring the dialog's open state, and reversing
latest-save-wins each turn the unit tests red; reverting the two dialog
diffs turns all four e2e specs red. The `check-file-sizes.mjs` overrides
for the two dialogs are ratcheted to their exact new counts (1048 and
1229) — verified tight in both directions, N passes and N−1 fails, so no
headroom is introduced.
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
* fix(acp): disable goose cron scheduler in managed agent children (#3144)
A Buzz install with a scheduled goose recipe fires each cron entry once
per `goose acp` child instead of once, because every child
unconditionally starts its own cron scheduler over the shared
`~/.local/share/goose/schedule.json`. With a pool of N children per
harness and multiple harnesses, one scheduled recipe fans out to N ×
harness_count executions — each running under the managed agent's
identity rather than the operator's, and racing the operator's own
standalone goose over the same schedule file.
This injects `GOOSE_ACP_SCHEDULER_DISABLED=true` into every child
spawned by `AcpClient::spawn`, so a managed agent never owns the
operator's cron schedule.
## Placement
The `cmd.env` call is set last — after the `extra_env` operator-wins
loop and after the `CODEX_CONFIG` merge — deliberately with no escape
hatch. Managed children not running the operator's schedule is a
correctness invariant rather than an operator-tunable default, so the
injection must beat both a conflicting persona `extra_env` entry and any
value inherited from the parent process.
It is injected for all agents, not just goose. Agent builds that don't
recognize the variable ignore it.
## Sequencing
The goose-side flag that reads this variable and skips scheduler startup
lands separately (repo TBD). Until it does, this change is a
forward-compatible no-op: it sets an environment variable nothing
currently reads. Merging it first means no coordinated release is needed
— the fix takes effect as soon as the goose side ships.
Related: https://github.com/aaif-goose/goose/pull/10738
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
* fix(desktop): paint community rail full height (#3382)
## Summary
- paint the community rail across the full app height instead of
exposing the parent background through external margins
- preserve the existing community-button alignment and balanced
horizontal gutters by moving vertical spacing inside the rail
- update the rail geometry coverage to require full-height paint
ownership
## Root cause
PR #2972 aligned the rail box with the inset content by adding top and
bottom margins to the `bg-sidebar` element. Margins are outside the
painted box, so flat light and dark themes exposed a differently colored
app background above and below the rail.
## Validation
- pre-push `desktop-check`
- pre-push desktop unit suite: 3,751 passed
- `git diff --check`
Local Playwright/E2E was not run; CI owns the full browser matrix.
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
* docs(contributing): document the Linux system libraries just ci requires (#3396)
## Problem
The prerequisites table lists language toolchains (Rust, Node, pnpm,
Flutter, Docker, `just`) but no system libraries. Hermit pins the former
and not the latter, so following the setup section exactly on Linux
still leaves `just ci` unable to run: it fails partway through its first
dependency, `just check`, at `desktop-tauri-clippy`.
```
The system library `gdk-pixbuf-2.0` required by crate `gdk-pixbuf-sys` was not found.
The file `gdk-pixbuf-2.0.pc` needs to be installed and the PKG_CONFIG_PATH environment variable must contain its parent directory.
```
The desktop crates link against GTK and WebKitGTK. CI installs those
packages explicitly, so it never sees this — which is exactly why the
gap is invisible from the maintainer side. Since `check` runs first in
the `ci` chain, the failure also masks everything after it (`test-unit`,
`desktop-test`, `web-build`, `mobile-test` never run), which makes it
read as a broken repo rather than a missing dependency.
## Change
Adds a `#### Linux: Tauri system libraries` subsection under
Prerequisites with:
- The apt list copied from `.github/workflows/ci.yml`, so a local run
matches CI rather than drifting from it
- A pointer to [Tauri's
prerequisites](https://tauri.app/start/prerequisites/) for non-Debian
distributions
- A note that server-side contributors can skip it — `just fmt-check`,
`just clippy`, `just test-unit`, and `just test` need no GTK
Docs only. No TOC entry needed, since the TOC lists `##` headings and
this is a `####` subsection.
## How I hit it
Running `just ci` before pushing #3372, on Ubuntu under WSL2 with the
Hermit toolchain active and all Docker services healthy. Everything the
guide asks for was in place. The four `check` steps before
`desktop-tauri-clippy` (`fmt-check`, `clippy`, `desktop-check`,
`desktop-tauri-fmt-check`) passed, which is what makes the failure point
specific rather than a general build problem.
## Closest existing work
None found. I searched open and closed issues and PRs for `gdk-pixbuf`,
`libgtk`, `webkit2gtk`, `system dependencies`, `prerequisites`, `just
ci`, and `linux setup`. The Linux/GTK issues that exist (#2604, #2643,
#2982, #2811, #2562) are all runtime bugs in shipped builds, not
setup-path failures.
## Verification
The package list is transcribed from `.github/workflows/ci.yml:152-163`;
the same list appears in `release.yml` and `linux-canary.yml`. I have
not installed the packages on my machine, so I can confirm the failure
and the source of the fix but not that the list is exhaustive on a clean
box — worth a second pair of eyes from anyone who has done a fresh Linux
setup recently.
Signed-off-by: Kyler Cao <kcao@gssmail.com>
* fix(desktop): stabilize flaky DM expansion E2E ordering assertions (#2004)
## Summary
Fixes 4 flaky DM expansion E2E tests in Desktop Smoke shard 1 that were
failing non-deterministically on CI (also reproducing on `main` at run
`29526844596`).
**Failing tests:**
- `channels.spec.ts:652` — creates the DM before preparing a persona
mention
- `channels.spec.ts:760` — routes an agent mention from an existing DM
to the expanded conversation
- `channels.spec.ts:815` — routes a relay-agent mention from an existing
DM to the expanded conversation
- `channels.spec.ts:940` — drops an expanded DM after the first message
fails
## Root Cause
Race condition: under fast CI execution, mock command completions
(create_managed_agent, open_dm) can resolve in non-deterministic order,
causing assertions to observe stale or mid-transition state.
## Fix
- **:652** — Move the `new-message-recipient-popover` hidden assertion
after `chat-title` settles (both names present), so it runs
post-transition rather than mid-transition.
- **:760, :940** — Add `createManagedAgentDelayMs: 100` to ensure
persona provisioning doesn't collapse into the same tick as the
expanded-DM open/start sequence.
- **:815** — Add `openDmDelayMs: 100` so the two open_dm calls resolve
in deterministic order.
## Validation
All 4 tests pass with `--repeat-each=3` (12/12 green) locally. Biome
lint clean.
## Scope
Test-only change: 12 insertions, 1 deletion in
`desktop/tests/e2e/channels.spec.ts`.
---
Investigated by Ferret, reviewed by Grumplestiltzkin.
Signed-off-by: Cameron Hotchkies <chotchkies@block.xyz>
Co-authored-by: Goose <opensource@block.xyz>
* feat(desktop): apply WebKit rendering workarounds at startup on Linux (#3271)
On some Linux GPU/driver/compositor combinations, WebKitGTK's dmabuf
renderer aborts the web process during startup, so Buzz comes up with no
window at all and the user has no way to fix it. Setting
`WEBKIT_DISABLE_DMABUF_RENDERER=1` avoids the abort by falling back to
the shared-memory buffer path.
WebKit reads each of its rendering variables exactly once per process,
so the choice has to be made before anything initializes — there is no
runtime toggle and no second chance later in the same process. This
decides up front from two cheap preflight signals rather than reacting
to a crash:
- **NVIDIA GPU** — any DRM device under `/sys/class/drm` reporting PCI
vendor `0x10de`, the driver family behind most upstream reports.
- **AppImage** — the `APPIMAGE` environment variable. linuxdeploy's
AppRun hook pins `GDK_BACKEND=x11`, and the dmabuf renderer buys nothing
on that XWayland path.
Either signal disables the dmabuf renderer. Neither signal leaves the
environment untouched.
## Escape hatches
`--safe-rendering` forces the safest configuration for one launch —
`WEBKIT_DISABLE_DMABUF_RENDERER` plus `WEBKIT_DISABLE_COMPOSITING_MODE`
— for a machine neither signal recognises.
Any user assignment of a variable this module may set stands the
heuristic down **wholesale**. Presence is the test, not truthiness, so
`VAR=0` and `VAR=` both count: a user asking for the dmabuf renderer
*on* gets it, even on a machine the heuristic would have opted out.
`--safe-rendering` against such an assignment is refused with a
diagnostic naming both the assignment and the key to unset, and exits
non-zero — the flag and the environment are two incompatible answers to
one question, and neither is guessed.
## Placement
`webkit_rendering::apply()` runs at the top of `fn main()`, before
`buzz_lib::run()`. That is the only point where the process is still
single threaded with no GTK object alive, which is what makes
`std::env::set_var` sound; the module doc and the call site both say so.
The whole module is `#[cfg(target_os = "linux")]` — macOS and Windows
compile none of it.
The decision is a pure function of argv, an injected environment lookup,
and an injected DRM root, so all of it is unit-testable without mutating
the process environment.
Closes #2338. Upstream:
[tauri#9394](https://github.com/tauri-apps/tauri/issues/9394). Same
approach and same variable as
[clash-verge-rev](https://github.com/clash-verge-rev/clash-verge-rev/blob/main/src-tauri/src/utils/linux/workarounds.rs)
`workarounds.rs` and
[screenpipe](https://github.com/screenpipe/screenpipe/blob/main/apps/screenpipe-app-tauri/src-tauri/src/linux_webkit_env.rs)
`linux_webkit_env.rs`.
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
* feat(acp): steer claude-code and codex agents via _session/steering (#3007)
Mid-turn steering was reachable only through goose's
`_goose/unstable/session/steer`, which requires an `expectedRunId`
sourced from `_meta.goose.activeRunId`. claude-agent-acp and codex-acp
never emit a run id, so every mid-turn mention to those harnesses bailed
at the run-id guard before writing a byte and degraded to cancel +
merge, destroying in-flight tool calls.
Both adapters ship `_session/steering` (params `{sessionId, prompt}`,
result `{outcome}`) and advertise it as `_meta.steering.supported` on
the `initialize` response. This adds it as a second steer transport
selected at write time, reusing the existing withhold/release, ack
routing, and cancel+merge fallback machinery unchanged.
## Transport selection
| `active_run_id` | `steering_supported` | Transport |
|---|---|---|
| `Some(run_id)` | any | `_goose/unstable/session/steer` +
`expectedRunId` (unchanged) |
| `None` | `true` | `_session/steering` with `{sessionId, prompt}` |
| `None` | `false` | ack `ExpectedRunIdMissing`, write nothing
(unchanged) |
goose keeps priority when both are present — `expectedRunId` is strictly
more precise about *which* run is being steered.
## Two load-bearing safety properties
**The advertised capability is the only gate — never error-code
probing.** codex-acp's `extMethod` answers unrecognized extension
methods with a bare `{}`, which is a JSON-RPC *success* rather than
`-32601`. Buzz maps a steer success to `queue.remove_event`, so probing
an unknown method would silently delete the user's message with no
error, no fallback, and no log line.
**An `outcome` must be positively recognized.** Only `injected` and
`startedNewTurn` count as delivery. Anything else — codex's `failed`, an
unknown value, or a missing `outcome` entirely — is
`SteerError::OutcomeRejected`, which releases the withheld event and
fires the cancel+merge fallback. This makes the silent-loss path above
unreachable even if an adapter mis-advertises.
`startedNewTurn` acks `Success`, because the message really was
delivered and must not be redelivered, but deliberately does **not**
renew the read loop's hard deadline: the turn Buzz was awaiting had
already settled, and renewing would extend the clock on a finished turn.
## Notes for reviewers
- `SteerError::OutcomeRejected` needs no new arm in the
`PoolEvent::SteerAck` match — the existing catch-all
`Ok(SteerAck::Err(_)) => (true, false, true)` already gives release +
fallback, and the two `AgentError` arms above it match that variant
specifically, so they do not shadow it.
- Comments that described the old goose-only "try-and-tolerate" `-32601`
behavior are corrected; that assumption was never valid for codex-acp.
- No CI job runs `buzz-acp` tests. The full package suite was run
locally: **617 passing, 0 failing**.
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
* release(chart): publish 0.1.7 (#3393)
## Why
Publish chart 0.1.7 after the feature PR merged from a fork and
therefore intentionally skipped the internal-branch auto-tag job.
## What
- Trigger the `chart-release/0.1.7` release lane
- Update the quickstart example to reference chart 0.1.7
## Risk Assessment
Low — the chart implementation is already merged and tested; this PR
creates its immutable release tag and OCI artifact.
## References
- Chart implementation: https://github.com/block/buzz/pull/3322
- `helm unittest` 0.8.2: 43/43 tests passed
- Local pre-push checks passed
Generated with Amp
Signed-off-by: David Grochowski <dgrochowski@squareup.com>
Co-authored-by: Amp <amp@ampcode.com>
* chore(ci): bump desktop smoke E2E timeout to 30 minutes (#3409)
Three main-branch runs today had shards killed at exactly 20m17s
("exceeded the maximum execution time of 20m0s"); the killed shard was
actively passing tests seconds before the cap. Shard runtime has grown
to the limit. 30 matches the other desktop jobs in the same workflow.
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
* fix(ci): ratchet file sizes against the base tree (#3352)
## Summary
- replace the whole-tree file-size gate with a stateless differential
ratchet
- allow inherited files over 1,000 lines to hold or shrink, but never
grow
- delete the 44-entry numeric override ledger and run the same policy
across Desktop, Web, and Mobile CI
- fail closed when the local base cannot be resolved and cover policy,
Git status parsing, and base resolution in unit tests
This removes the shared mutable policy state that caused unrelated PRs
to fail after neighboring merges. It does **not** by itself prevent two
stale green PRs from becoming invalid when combined; that requires merge
queue or up-to-date branch enforcement.
### Related issue
None found. This follows the design discussion in the linked Buzz
channel.
### Testing
- `node --test scripts/check-file-sizes-core.test.mjs` (6/6)
- Desktop, Web, and Mobile ratchet entrypoints
- `just desktop-check`
- `just web-check`
- Mobile analysis
- `git diff --check`
The repository pre-push suite also exposed an unrelated existing Mobile
widget failure in `ChannelDetailPage keeps follow mode off while a tall
newest message stays visible`; it reproduces in isolation and this
branch does not touch Mobile widget behavior.
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
* fix(desktop): clear stale thread new-message pill (#3411)
## Summary
- reconcile anchored-scroll state when passive layout changes put a
thread at its physical floor
- route thread composer-padding growth and shrink through the same
hook-owned settlement path
- preserve pinned thread targets while clearing stale new-message state
## Root cause
Thread bottom state was updated primarily by native `scroll` events.
Deferred replies, viewport changes, and composer-overlay padding can
finish changing geometry after the user's last scroll—or after the
initial open pin—without another scroll event. The thread could visibly
reach the floor while `isAtBottom` and `newMessageCount` remained stale,
leaving the “N new messages” pill visible.
## Verification
- `pnpm check`
- `pnpm typecheck`
- `pnpm test` — 3,768 passed
- push hook: branch-skew, Desktop check, and Desktop full unit suite
passed
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
* feat: add explicit entry for claude-opus-5 in model config (#2831)
Fixes #2787
- Added `claude-opus-5` to `config.rs` model classification and adaptive
effort helpers.
- Updated fixture test configurations to cover `claude-opus-5`.
- Verified with `cargo test` and JS unit tests.
Signed-off-by: Apurva Shaw <apurvashaw@Apurvas-MacBook-Air.local>
Co-authored-by: Apurva Shaw <apurvashaw@Apurvas-MacBook-Air.local>
* fix(relay): avoid subscription lock inversion (#3413)
## Summary
- drop the `subs` DashMap guard before mutating subscription indexes
- snapshot fan-out candidate vectors so index guards are dropped before
looking up `subs`
- add concurrent fan-out/replacement regression coverage
## Why
`fan_out_scoped` previously held an index guard while `push_match`
acquired `subs`, while CLOSE and same-ID replacement held `subs` while
removing from an index. The reverse ordering made an AB/BA deadlock
reachable and could synchronously park all Tokio workers.
## Validation
- `rustup run 1.95.0 cargo test -p buzz-relay` — 769 library tests
passed, 33 ignored; 11 binary tests passed; doc tests passed
- push hooks with pinned Rust 1.95 — branch-skew, repository Rust
suites, and desktop Tauri suite passed
- `git diff --check`
## Residual risk
Fan-out now clones bounded candidate vectors before matching. This adds
allocation/copy cost proportional to the indexed candidate set, in
exchange for eliminating nested DashMap guards. This fixes the concrete
lock cycle but does not prove every observed production wedge had this
cause.
---------
Signed-off-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz>
Co-authored-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz>
* fix(acp): per-runtime env defaults at spawn — isolate Hermes from configured MCP startup (#3420)
## Summary
- add a generic per-runtime env-defaults table,
`config::default_agent_env()`, mirroring the existing
`default_agent_args()` / `codex_network_env()` precedent, and merge it
once in `AcpClient::spawn` with the established precedence: **runtime
defaults < persona `extra_env` < inherited parent env**
- first (and only) row: Buzz-owned Hermes processes get
`HERMES_ACP_SKIP_CONFIGURED_MCP=1`, so Hermes does not preload unrelated
profile-configured MCP servers before answering ACP `initialize` (fixes
the 10s model-discovery timeout in #3355 — Buzz supplies session MCP
servers explicitly through `session/new`, per Hermes's documented
host-integration contract for this variable)
- normalize Windows `.cmd`/`.bat` shims alongside `.exe` in
`normalize_agent_command_identity` (npm installs resolve to those
wrappers)
- switch the `extra_env` parent-presence check from `var()` to
`var_os()` so non-UTF-8 parent values are honored
Replaces the runtime-specific approach in #3386: same behavior, but the
mechanism is generic runtime spawn metadata in `config.rs` rather than a
Hermes/ACP special case in `acp.rs`, and the seam covers every launch
path (Desktop spawn, `buzz-acp models`, CLI) because they all funnel
through `AcpClient::spawn`. ~15 lines of production code.
Fixes #3355
## Testing
- `cargo test -p buzz-acp` — **639 passed, 0 failed** (full package,
includes the new `default_agent_env_recognizes_hermes_identities` unit
test and `spawn_applies_runtime_env_defaults_with_extra_env_precedence`
integration test covering default injection, extra_env override, and
non-Hermes exclusion)
- `cargo fmt --all -- --check`, `cargo clippy -p buzz-acp --all-targets
-- -D warnings` — clean
- live-local with real Hermes v0.19.0 (`hermes-acp`): `buzz-acp models`
returned **13 models / currentModelId in 2.6–3.0s** (was a 10.0s timeout
on the first cold run without isolation); a wrapper probe confirmed the
child received `HERMES_ACP_SKIP_CONFIGURED_MCP=1` by default and `0`
when the parent env set it explicitly (operator wins)
- lefthook pre-push suite green: rust-tests, desktop-check,
desktop-test, desktop-tauri-test, mobile-test, branch-skew
No UI changes; subprocess environment behavior only.
Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Co-authored-by: mr-r0b0t.eth <adam.manning@pro-serveinc.com>
* Fix mobile attachment and gallery polish (#3370)
## Summary
- align mobile message metadata and enlarge attachment-menu content
- smooth keyboard-to-camera/photo transitions and initialize the iOS
photo grid at the intended scale
- fix horizontal gallery loading, edge overflow, and end spacing
## Why
The attachment surfaces were reacting to keyboard and compact-menu
geometry during presentation, while gallery clipping and image lifecycle
behavior caused misalignment and occasional blank previews.
## Testing
- `just mobile-check`
- `flutter test` (881 passed, 1 skipped)
- native `RunnerTests` (17 passed)
- verified standalone Release build on a physical iPhone
---------
Signed-off-by: kenny lopez <klopez4212@gmail.com>
* feat(agent): fix Anthropic prompt caching with Databricks (+ MCP proxy/TLS passthrough) (#3463)
> On 8 tasks matched by name across the two runs, cost fell $8.36 →
$1.77 (4.71×) and wall-clock 12,423 s → 1,085 s (11.45×).
## Summary
Two independent, self-contained fixes to `buzz-agent`/`buzz-acp`, split
out of the benchmark branch so they can land while the harness work
continues:
1. **Request and surface Anthropic prompt caching.** buzz never sent a
`cache_control` breakpoint, so on the Databricks Anthropic route
`cache_read_input_tokens` was **structurally always 0** and the ~10×
cache-read discount was never claimed. This teaches `anthropic_body()`
to mark the cacheable prefix, and plumbs the cache split end-to-end so
accounting can price it.
2. **Pass proxy + TLS-trust env into MCP tool subprocesses**, so agent
tools on a proxy-only host stop reporting a live network as offline.
## Why the caching gap matters
The Anthropic Messages API does **not** cache unless the request carries
a `cache_control` breakpoint, and the Databricks AI Gateway — a
third-party proxy in front of the model, in the same category as
Bedrock/Vertex — does **not** auto-cache (only the first-party Anthropic
API and Claude-on-AWS do zero-config caching). So every request was
billed cold.
Measured live against the Databricks gateway
(`databricks-claude-opus-5`, 2026-07-28), the same call with and without
a single `cache_control` marker:
| Run | `input_tokens` | `cache_creation` | `cache_read` | latency |
|---|---|---|---|---|
| No `cache_control`, two byte-identical calls | 121,625 | 0 | **0** |
~9.3 s |
| With one marker — cold (write) | 4 | 121,625 | 0 | 9.3 s |
| With one marker — warm (read) | 4 | 0 | **121,625** | **4.5 s** |
One marker moved 121,625 tokens from full-price input to a 0.1× cache
read and roughly halved latency (a clean, isolated ~2.07× prefill
speedup on this single-threaded microbenchmark). The gateway honours
`cache_control`; buzz simply never sent it.
At fleet scale this was a real budget item. Across matched
Terminal-Bench solo sweeps (89 tasks, `-n 20`, before the fix), the two
OpenAI-route models independently landed at ~86–87% cache reads — the
expected shape for an agentic loop, where system + tools + append-only
history repeat every turn — while the Anthropic route returned a hard 0%
on every receipt:
| Condition | Route | Input tokens | Cache reads | Cost | Cost if
uncached | Discount |
|---|---|---|---|---|---|---|
| luna (`gpt-5-6`) | OpenAI | 20,320,818 | **17.7M (87.0%)** | $6.96 |
$22.87 | **3.28×** |
| sol (`gpt-5-6`) | OpenAI | 22,312,290 | **19.2M (85.9%)** | $37.07 |
$123.35 | **3.33×** |
| opus (`claude-opus-5`) | Anthropic | 12,459,822 | **0 (0.0%)** |
$81.31 | $81.31 | **1.00×** |
Applying luna's measured 87% read rate to the opus token counts at list
prices (`input $5/M`, `cached_input $0.5/M`, `output $25/M`) puts the
opus run at **~$32.53 vs the $81.31 actually paid — a ~60% overspend on
those 49 trials (~$89 on a full sweep)**. That is an upper bound (it
prices every cached token at the 0.1× read rate and ignores the 1.25×
write premium), and the opus discount is structurally smaller than
luna/sol's because opus emits ~3.5× more uncacheable output per trial,
which sets a floor on what caching can recover.
There is also a plausible **second-order effect**: Databricks appears to
meter its per-minute rate limit on *uncached* input tokens, so the
missing cache also cost rate-limit headroom — the opus endpoint lost 63%
of its trials to fatal 429s while running alone at one-third of a GPT
endpoint's raw throughput. This is a hypothesis, not a proven mechanism
(the only zero-cache condition is also the only Anthropic endpoint), but
it is the reading that explains the throttling with one rule instead of
two.
## Post-fix results (provisional — first trials of an in-flight re-run)
On 8 tasks matched by name across the two runs, cost fell **$8.36 →
$1.77 (4.71×)** and wall-clock **12,423 s → 1,085 s (11.45×)**.
| Metric | before (`4a955a858`) | after (`3bef1f6a`) |
|---|---|---|
| Cache reads as % of input | **0.0%** | **78.7%** (still climbing
toward the ~86% steady state) |
| `cost_usd_no_cache_discount / cost_usd` | **1.00×** | **2.18×**
(tracking the projected ~2.5×) |
| Trials with a fatal 429 (same `-n 20`) | **63%** | **15–19%** |
To be clear about attribution: **~2× of that is the clean prefill saving
from caching itself**; the rest is second-order — cached requests burn
far less rate-limit budget, so they stall less and redo less destroyed
work. The 11.45× is a system-level result specific to this throttled
workspace, not a caching benchmark. Quality held (7/8 solved in each
run). A controlled low-`-n` A/B (neither arm hitting a 429), which the
`BUZZ_AGENT_PROMPT_CACHING` opt-out exists to enable, is still owed
before this becomes a published claim.
## What changed
### 1. Request caching (`llm.rs`, `config.rs`)
`anthropic_body()` emits ephemeral `cache_control` breakpoints, gated by
`BUZZ_AGENT_PROMPT_CACHING` (**default on**, `=0` to opt out):
- **Static prefix** — marker on the `system` block. Prefix order is
`tools → system → messages`, so this single marker caches **tools +
system** together. Byte-identical on every turn of a run, and survives a
context handoff (system/tools come from cfg/mcp, not `self.history`).
- **Rolling tail + leapfrog** — marker on the last block of the last
**two** messages. The append-only history re-reads the prior turn's
prefix from cache; marking two messages (not one) keeps consecutive
breakpoints inside Anthropic's **20-block lookback window** even as tool
parallelism rises, avoiding a silent full-price miss.
An empty system prompt stays a bare string (Anthropic rejects empty text
blocks), and below-threshold prefixes are silently not cached, so the
flag is safe on by default.
### 2. Surface the cache split end-to-end — the plumbing (`types.rs`,
`llm.rs`, `agent.rs`, `lib.rs`, `usage.rs`, `acp.rs`)
This is the part that makes gaps like the one above **visible** instead
of silent. A consumer that prices all of `input_tokens` at the full rate
can't tell a route that's caching from one that isn't — the total looks
right either way. So:
- `LlmResponse` gains `cached_input_tokens` (a **subset** of
`input_tokens`, never an addition); `parse_anthropic` / `parse_openai` /
`parse_responses` each populate it.
- A `usage_first()` helper reads the cache count wherever a provider
hides it — flat `cache_read_input_tokens` (Anthropic),
`prompt_tokens_details.cached_tokens` (OpenAI chat),
`input_tokens_details.cached_tokens` (Responses) — taking the **first
present value, never a sum**. Reading only flat keys is exactly why the
OpenAI route's nested `cached_tokens` had *also* been going unclaimed:
`prompt_tokens` is already inclusive, so the total looked correct while
the discount silently went unreported.
- The per-turn/per-session accumulators and the goose `usage_update`
payload now carry `accumulatedCachedInputTokens`; `buzz-acp`
deserializes it (`serde` default `0` for goose, which doesn't send it)
and logs `cached=<n>`.
### 3. Fix a Databricks MLflow-route double-count (`llm.rs`)
The Databricks MLflow route reports the flat Anthropic-spelled
`cache_read_input_tokens` *alongside* an already-inclusive
`prompt_tokens`, so the old code summed them and nearly doubled the
count — inflating both the context-budget gate and cost.
`openai_chat_input_tokens()` now reads `prompt_tokens` alone. Verified
on a live `databricks-glm-5-2` response where `prompt_tokens +
completion == total` proves inclusivity. (Anthropic's native route
genuinely *excludes* the cache fields and is still summed — the two
never collide, because `claude*` models route to the Anthropic path.)
### 4. Proxy + TLS-trust passthrough into MCP tools (`mcp.rs`) —
independent fix
`buzz-agent` `env_clear()`s each MCP child, and the allowlist carried no
proxy/TLS vars. On a proxy-only host that doesn't degrade the tools, it
**blinds** them: apt, curl, pip, git connect directly, the egress
firewall resets the socket, and the agent reports "Connection reset by
peer" — indistinguishable from a genuinely offline task. Adds both
spellings of `HTTP(S)_PROXY`/`NO_PROXY`/`ALL_PROXY` (curl/git read
lowercase; Go/Python read uppercase; libcurl ignores uppercase
`HTTP_PROXY`) plus `SSL_CERT_FILE`/`SSL_CERT_DIR` for TLS-terminating
proxies that present their own CA.
## Testing
- `cargo fmt --all -- --check`, `cargo clippy -p buzz-agent -p buzz-acp
--all-targets -- -D warnings` — clean.
- `cargo test -p buzz-agent -p buzz-acp` — **all green** (632 + 299 lib
tests plus integration suites, 0 failures). New tests cover: the three
breakpoints and the disabled/empty-system/single-message edge cases;
nested-vs-flat cache parsing for all three routes; the Databricks
inclusive-`prompt_tokens` fix; wire deserialization of
`accumulatedCachedInputTokens`; and the proxy/TLS passthrough allowlist.
- Pre-push lefthook suite green (branch-skew, rust-tests, test,
desktop-check/test/tauri).
## Relationship to the benchmark branch
These are the non-`benchmarks/` changes from
`benchmark/harness-accounting-and-solo`, lifted onto a clean base off
`main` so they can merge independently.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Signed-off-by: Atish Patel <atish@squareup.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Polish mobile typing indicator (#3528)
## Summary
- Present channel and thread typing status in a composer-matched
container.
- Animate the strip so the message list moves smoothly as typing begins
and ends.
- Increase typing-label contrast and avatar/padding for readability.
## Pixel 10 snapshot

## Validation
- `flutter test test/features/channels/channel_detail_page_test.dart`
- `flutter analyze`
Signed-off-by: kenny lopez <klopez4212@gmail.com>
* Refine community invite limits (#3529)
## Summary
- Simplify the community invite dialog around link sharing.
- Add matching expiry and use-limit dropdowns, with sensible preset use
caps.
- Cover the default unlimited and selected-limit invite payloads.
## Validation
- `pnpm -C desktop run build:e2e`
- `pnpm -C desktop exec playwright test
tests/e2e/invite-link-copy.spec.ts
tests/e2e/invites-settings-screenshots.spec.ts --project=smoke`
Signed-off-by: kenny lopez <klopez4212@gmail.com>
* feat(agent): route Claude/GPT model families to their native gateway wire (#3538)
## Summary
Databricks v2 chooses the gateway wire format — OpenAI Responses,
Anthropic Messages, or MLflow chat — purely from substrings in the
endpoint name. There is no family field on the endpoint to key off, so
the substring set *is* the routing contract. The matcher only recognised
`gpt-5`/`gpt5` and `claude`, which makes correct billing depend on every
Claude endpoint happening to be named with the literal string "claude".
## Why this matters
Getting a Claude model onto the Anthropic Messages route is exactly what
lets buzz attach the `cache_control` breakpoint (the fix in #3463). If a
Claude endpoint's catalog name omits "claude" — an alias, a bare
`opus-5`, a `goose-opus-5` — it silently falls through to the MLflow
(OpenAI-wire) path, where Anthropic prompt caching is **structurally
impossible**. The result is the same failure #3463 fixed: 0% cache
reads, the full ~10x read discount lost, and no error — a naming
convention quietly holding up a billing-correctness invariant.
## What changed
`databricks_v2_route_for_model` (`crates/buzz-agent/src/llm.rs`) now
matches broader, case-insensitive marker sets:
- **Claude → Anthropic Messages:** `claude`, `opus`, `sonnet`, `haiku`,
`mythos`, `fable` — the Claude family names and release code names, so a
Claude endpoint reaches the cache-capable route regardless of how it's
named.
- **GPT → OpenAI Responses:** the `gpt` family (now `gpt` on its own,
not just `gpt-5`) plus the GPT-5 launch code names `sol`, `luna`,
`terra`.
OpenAI markers are evaluated first, preserving the prior `gpt-5`-first
precedence for any name that could carry both. Names matching neither
set still fall through to the MLflow chat route.
## Testing
- `cargo fmt`, `cargo clippy -p buzz-agent --all-targets -- -D warnings`
— clean.
- `cargo test -p buzz-agent` — all green (299 lib + integration suites,
0 failures). The `databricks_v2_routes_by_model_family` test was
expanded to cover each new marker, the GPT-5 code names,
case-insensitivity, and the unchanged MLflow fallback (including
`gemini`).
## Relationship to #3463
#3463 taught the Anthropic path to request caching; this makes sure
Claude models actually land on that path. Follow-up still open:
surfacing `cache_creation_input_tokens` end-to-end so a persistent
`reads == 0 && writes == 0` reveals a disabled cache regardless of which
wire a model takes — happy to do that next.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Signed-off-by: Atish Patel <atish@squareup.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Polish mobile navigation and menus (#3486)
## Summary
- Add a shared footer fade behind the floating tabs on Home, Activity,
and Search.
- Use a shared anchored popover for Activity filters and section
actions, with working section move controls.
- Polish message grouping/press states and remove the initial Search
back button.
<img width="630" height="1368" alt="Screenshot 2026-07-29 at 08 49 37"
src="https://github.com/user-attachments/assets/9e787adf-0bb3-49c6-8224-5819e8cfb1ad"
/>
### Testing
- `flutter analyze`
- `flutter test`
- Release build installed and checked on a connected iPhone
### Screenshots
A real-device Activity baseline showing the original solid footer is
attached in a PR comment. The updated review build was checked on the
connected iPhone.
---------
Signed-off-by: kenny lopez <klopez4212@gmail.com>
* fix(desktop): preserve shared agent fidelity (#3553)
## Summary
Fixes two distinct fidelity failures in direct agent sharing:
- The sender now puts the same effective avatar shown on the agent card
into People-share and file-export snapshot PNGs, including
profile/kind:0 fallback avatars.
- The importer now persists the visible PNG body as the portable avatar
instead of ignoring it in favor of sender-local manifest references.
- Export materializes inherited runtime, provider, and model identifiers
verbatim, while preserving explicit definition values. It does not
translate or substitute configuration for a different recipient setup.
- Sharing waits for a profile-only fallback avatar query, preventing an
early-click race.
The PNG import path keeps the existing safety invariant: decode is
capped at 2048×2048 / 32 MiB and re-encoded avatars above the 2 MiB
inline limit fall back to the manifest reference. The exact transparent
1×1 no-avatar placeholder is ignored.
The original Tyler↔Wes screenshot demonstrates both stages: Wren's
attachment had an avatar that disappeared after **Add agent**
(receiver/import failure), while Pinky's attachment was already blank
(sender/projection failure).
### Related issue
N/A — reported and traced in the linked Buzz conversation.
### Testing
- `cargo test --manifest-path desktop/src-tauri/Cargo.toml
commands::personas::snapshot` — 57 passed
- `pnpm exec tsc --noEmit`
- Biome check on changed frontend/E2E files
- Pre-push hooks:
- desktop check
- desktop tests
- desktop Tauri tests — 1853 passed, 14 ignored
- file-size ratchet
The People-share E2E regression asserts that a profile-only avatar
reaches `avatarPngDataUrl` in the real encode command payload.
---------
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
* perf(desktop): move observer-feed archive and decrypt commands off main thread (#3415)
Opening the agent observer feed could beachball the app. In Tauri 2, a
sync (`pub fn`) command body runs on the **main thread** — only `async
fn` commands run on the runtime pool. Five commands on the observer-feed
open path were sync, so panel open ran SQLite I/O and secp256k1 work on
the macOS main thread:
| Command | Main-thread work |
|---|---|
| `decrypt_observer_event` | Schnorr ID + signature verify, then NIP-44
decrypt — once per frame |
| `read_archived_observer_events_for_channel` | Opens the archive DB,
runs the channel-index JOIN, returns up to 200 raw JSON blobs per page |
| `read_unindexed_observer_rows` | Opens the DB, returns **all**
not-yet-indexed kind-24200 rows in one shot |
| `index_observer_channel_id` | Opens the DB, loops N upserts |
| `delete_save_subscription` | Opens the DB, one delete |
Eager hydration loads up to 10 pages × 200 frames on panel open, so
that's up to 10 main-thread DB reads plus up to 2,000 sequential
verify+decrypt calls before any scrolling. The one-shot backfill makes
it worse on the first open after history accumulates: one read of every
unindexed row, a decrypt per row, then a batch upsert — all on the main
thread, and all proportional to archive size.
The four archive commands now route their DB work through the existing
`run_archive_db_task` helper (`spawn_blocking` + `open_db`), matching
`list_save_subscriptions`, `read_archived_events`, and `archive_events`
directly around them. `decrypt_observer_event` becomes `async fn` +
`tauri::async_runtime::spawn_blocking`, with `state.signing_keys()`
extracted before the spawn since `State` is not `Send` — the same
pattern `sign_event` uses from #1222.
No frontend changes: `invoke` is already promise-based, so the TS
wrappers in `tauriArchive.ts` and `tauriObserver.ts` are unchanged.
This removes the freeze, not the work. Eager hydration still takes the
same wall time — the feed shows a loading state instead of blocking the
UI. Batching the per-frame decrypt IPC (2,000 round-trips into one
command) would cut the latency itself; that's deliberately out of scope
here.
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
* Run Tauri clippy in pre-push (#3555)
## Summary
- run Desktop Tauri clippy from pre-push for every path that can affect
the Tauri crate
- reuse `just desktop-tauri-clippy`, keeping the local command identical
to Desktop Core CI
- leave the existing Tauri test hook unchanged
## Why
PR #3553 exposed a hook gap: `cargo test` allowed an unused-import
warning that CI's `clippy -D warnings` correctly rejected. Running the
same recipe before push catches that class of failure locally without
duplicating CI flags in Lefthook.
## Validation
- `lefthook run pre-push --command desktop-tauri-clippy --force`
- confirmed it invokes `cargo clippy --manifest-path
desktop/src-tauri/Cargo.toml --all-targets -- -D warnings`
- command passed
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
* chore(release): release Buzz Desktop version 0.5.1 (#3566)
## Buzz Desktop release v0.5.1
### Changes since v0.5.0:
- perf(desktop): move observer-feed archive and decrypt commands off
main thread ([#3415](https://github.com/block/buzz/pull/3415))
([`294c8c821`](https://github.com/block/buzz/commit/294c8c821de51442a8c384c0bdb66b1a10224ca0))
- fix(desktop): preserve shared agent fidelity
([#3553](https://github.com/block/buzz/pull/3553))
([`f7a3988ba`](https://github.com/block/buzz/commit/f7a3988ba13b590d9a55a7e8413fc3fb5ffbef18))
- feat(agent): route Claude/GPT model families to their native gateway
wire ([#3538](https://github.com/block/buzz/pull/3538))
([`6438dedf8`](https://github.com/block/buzz/commit/6438dedf83a9dbe1853e484326911bf6c7f1618c))
- Refine community invite limits
([#3529](https://github.com/block/buzz/pull/3529))
([`24d90d128`](https://github.com/block/buzz/commit/24d90d1280a9325c6cbcf8eea30ac54db5afd2cb))
- feat(agent): fix Anthropic prompt caching with Databricks (+ MCP
proxy/TLS passthrough)
([#3463](https://github…
calvadev
pushed a commit
to shopstr-eng/buzz
that referenced
this pull request
Aug 3, 2026
The codex adapter version gate accepted any `major >= 1`, so a 1.x `codex-acp` older than the version that fixes outbound relay access for `buzz` CLI subprocesses classified as `Available` and was never offered a reinstall. Only the 0.16.x `@zed-industries/codex-acp` adapter — which fails `--version` outright — was caught. `probe_codex_acp_version` now returns the full `(major, minor, patch)` triple and `codex_adapter_availability` compares it against a new `MIN_CODEX_ACP_VERSION` floor of `1.1.7`, the current npm latest. An adapter below the floor classifies as `AdapterOutdated`, which routes it through the existing uninstall-then-install reinstall plan. The parse requires exactly three numeric dot-separated components. Partial versions (`1.2`) and prerelease tags (`1.2.0-rc1`) return `None` and therefore classify as `AdapterOutdated` — a version Buzz cannot compare against the floor fails closed, offering a reinstall rather than running an adapter of unknown vintage. Both the floor's bump policy and the strict-parse behavior are stated in doc comments rather than left implicit. Supersedes [block#3097](block#3097) by @Bharathchinneni, whose semver floor and behavior tests this carries. That PR could not land as written: the two `probe_codex_acp_major_version` compatibility wrappers it kept had no non-test callers, which is a hard `clippy -D warnings` failure. The wrappers are deleted here and their call sites collapsed onto `probe_codex_acp_version`. Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
BradGroux
pushed a commit
to BradGroux/buzz
that referenced
this pull request
Aug 23, 2026
The codex adapter version gate accepted any `major >= 1`, so a 1.x `codex-acp` older than the version that fixes outbound relay access for `buzz` CLI subprocesses classified as `Available` and was never offered a reinstall. Only the 0.16.x `@zed-industries/codex-acp` adapter — which fails `--version` outright — was caught. `probe_codex_acp_version` now returns the full `(major, minor, patch)` triple and `codex_adapter_availability` compares it against a new `MIN_CODEX_ACP_VERSION` floor of `1.1.7`, the current npm latest. An adapter below the floor classifies as `AdapterOutdated`, which routes it through the existing uninstall-then-install reinstall plan. The parse requires exactly three numeric dot-separated components. Partial versions (`1.2`) and prerelease tags (`1.2.0-rc1`) return `None` and therefore classify as `AdapterOutdated` — a version Buzz cannot compare against the floor fails closed, offering a reinstall rather than running an adapter of unknown vintage. Both the floor's bump policy and the strict-parse behavior are stated in doc comments rather than left implicit. Supersedes [block#3097](block#3097) by @Bharathchinneni, whose semver floor and behavior tests this carries. That PR could not land as written: the two `probe_codex_acp_major_version` compatibility wrappers it kept had no non-test callers, which is a hard `clippy -D warnings` failure. The wrappers are deleted here and their call sites collapsed onto `probe_codex_acp_version`. Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
wesbillman
pushed a commit
that referenced
this pull request
Sep 9, 2026
## Summary Buzz considers codex-acp 1.6.2 current because the supported adapter floor is still 1.1.7. That adapter bundles Codex 0.148.0, so updating a separate Codex CLI to 0.153.4 leaves managed agents on the older runtime and unable to use GPT-6 Astra. Raise the supported adapter floor to the published 1.10.0 release, which depends on `@openai/codex ^0.153.3`. Existing discovery and installation code then classifies older adapters as outdated and offers the managed reinstall path. Update the availability and install-plan regressions to cover the observed 1.6.2 installation and the new minimum. This follows the existing version-floor policy. It does not automatically update a running installation: the user must complete Buzz’s offered adapter upgrade. Future upstream compatibility changes may require another floor update. ### Related issue No exact duplicate found in searches for Astra, CODEX_PATH, bundled Codex, outdated runtime, and codex-acp 1.10. Related: #3097 raised the older floor to 1.1.7 (already present on main); #2422 covers lost error details for runtime mismatches. Neither resolves this version gap. Originating conversation: buzz://message?channel=3286cd76-f83e-4c7d-8317-10a16580744d&id=8b79a73078217222b870fff144c27e7d27bcd5a67c966869c18fe726db716898 ### Testing - Isolated npm install of codex-acp 1.10.0 resolved bundled Codex 0.153.4, with no CODEX_PATH override. - Live macOS ACP probe: initialize protocol v1 → session/new → select gpt-6-astra[medium] → prompt. Received `OK` and `stopReason: end_turn`; usage metadata confirms gpt-6-astra. - Existing adapter 1.6.2 initialized but advertised no Astra model in the same probe. - Desktop Rust formatting and `git diff --check` pass. - `just desktop-tauri-test`: 3,266 passed, 20 ignored, zero failures across the Desktop workspace and integration tests. - Workspace and Desktop Clippy, frontend static checks, and `just file-size-check` pass. - Repository `just ci`: still running the remaining mobile/build/workspace-test stages. The installed Buzz app and managed adapter were not replaced or restarted. The live check validates the new adapter/runtime path; a complete packaged Desktop upgrade workflow remains untested. Signed-off-by: Stephen DeLorme <stephen@d.elor.me>
Contributor
|
closing since its addressed by #7427 |
mfethe1
added a commit
to mfethe1/buzz
that referenced
this pull request
Sep 11, 2026
* fix(mobile): style inline code with the app mono face (#6631)
## Summary
Inline code on mobile renders as **bold body text on a faint background
wash** — no monospace face, no chip, and it cannot wrap. #5257 diagnosed
this as a missing `highlightBuilder`.
That is no longer the right fix. `gpt_markdown` 1.2.0 deprecates
`highlightBuilder` (removal in 2.0.0), renders inline code as a real
chip, and adds `InlineCodeStyle` for restyling it. The package author
confirmed this on the issue. So this PR is an upgrade — 1.1.6 → 1.2.1 —
plus one theme declaration, rather than the builder the issue originally
asked for.
**Where the style is declared.** `GptMarkdownThemeData` goes in
`AppTheme._buildTheme`, which both `light()` and `dark()` call. That
reaches all four `GptMarkdown` call sites — `message_content`,
`transcript_item_widget`, `token_pill`, `custom_emoji_render` — so the
style is stated once instead of per widget. A widget-level
`inlineCodeStyle` would have covered channel messages only, leaving the
other three on the package's defaults.
**What is declared.** Face, size, ink, chip fill and outline — not the
face alone. A face name on its own leaves the rest on the package's
defaults, which put inline code at 14.1sp beside a fenced block's 13, on
a neutral `onSurface` tint rather than the app's code surface. In dark
that tint is *lighter* than the surface, while every other code surface
in the app is recessed, so the chip read as a different kind of object.
All of it now comes from one `CodeStyle` declaration that the fenced
block reads from too, so the two cannot be edited apart.
**Three adaptations the upgrade requires.** Each was found by running
the gate, not by reading the changelog:
1. **`imageBuilder` widened** to `(context, url, width, height)`. This
is a hard compile error, and it is **not listed in the package's
migration guide**, which states "nothing here stops code compiling".
Worth reporting upstream.
2. **`autolink` now defaults to `true`.** `normalizeBareLinks()` already
rewrites bare URLs into Markdown links before rendering, so both would
run. `message_content` opts out with `autolink: false` to keep current
behaviour exactly. The migration guide argues for dropping the
pre-processor instead — a better fix, but a behavioural change that
belongs in its own PR.
3. **`gpt_markdown.dart` now re-exports `markdown_config.dart`**, making
two direct imports redundant. `flutter analyze` reports `No issues
found!` on 1.1.6 and flags both on 1.2.1, so these warnings are new, not
pre-existing.
**Deliberately out of scope.** The three non-message call sites now
autolink bare URLs, since only `message_content` has a pre-processor to
collide with. Custom inline components (`_MentionMd`, `CustomEmojiMd`,
`_ChannelLinkMd`) could additionally declare `allScopesExceptLinkLabel`
— 1.2.0 offers it as the fix for a `WidgetSpan` chip going blank inside
a link label on iOS — but current behaviour is unchanged without it, so
that stays a separate change.
### Related issue
Fixes #5257
Duplicate scan: searched `gpt_markdown`, `inline code mobile`,
`highlightBuilder` and `InlineCodeStyle` across both PRs and issues. No
open PR touches inline code styling. #6135 (link labels) and #6166 (text
selection) also touch mobile Markdown but address different defects.
### Testing
Full gate, `just ci` — exit 0:
| Stage | Result |
|---|---|
| Rust (33 suites) | 4768 passed, 0 failed |
| Desktop | 5799 passed, 0 failed |
| Mobile | **2011 passed**, 0 failed |
| `flutter analyze` | `No issues found!` |
| Desktop + web build | ok |
Run on the branch with `main` merged in, so these numbers match what CI
builds.
**New regression test** — `renders inline code in the app code style`.
It resolves the `CodeTextSpan` the package tags inline code with, which
carries both the resolved `TextStyle` and the colours the chip behind it
is painted with, so face, size, ink, fill and outline are all asserted
rather than a widget's presence. It is negative-controlled: reverting
only the theme declaration fails it with
```text
Expected: a numeric value within <0.001> of <13.0>
Actual: <14.1>
```
and dropping the declaration entirely falls back to
`packages/gpt_markdown/JetBrainsMono` — so the test measures the real
thing, and it would catch a future regression that silently drops the
theme extension.
The test passes `baseStyle: messageBodyTextStyle`, the style the message
surfaces actually use; the widget's own fallback is the smaller
`bodyMedium`, which would move the expected size.
The test finds paragraphs with `find.byWidgetPredicate((widget) =>
widget is RichText)`, not `find.byType(RichText)`: inline code renders
through `BidiRichText`, a `RichText` subclass, and `byType` matches
exact runtime types.
That is a hazard for any test that reads text back out of a paragraph,
and one landed after this branch was cut:
`message_content_custom_emoji_test.dart` arrived with #6996 and its
`code keeps literal emoji while adjacent known tokens render` case reads
a code span through `find.byType(RichText)`. It passes on `main` and
fails on the merge result, which is what CI builds, so it went red only
once CI was authorized. It now uses the same predicate. The two other
`byType(RichText)` call sites — the rest of that file and
`message_author_meta_test.dart` — were re-run and pass: their content
carries no code span, so the exact type still matches. They were left
alone.
### Screenshots
Rendered through the real `MessageContent` widget with the app's own
fonts loaded, at 390pt wide, 3x DPR. Sample text: ``Set `BUZZ_RELAY_URL`
before launch, then run `just mobile-test` to verify.``
| | Before (1.1.6) | After (1.2.1) |
|---|---|---|
| Light |

|

|
| Dark |

|

|
Before: bold Inter on a flat wash, no chip edge, and `just mobile-test`
breaks across the line with the wash simply ending. After: Geist Mono in
a bordered, rounded chip, and the wrapped fragment gets its own chip on
each line.
---------
Signed-off-by: Tolga Cinisli <tolgacinisli@gmail.com>
Co-authored-by: Tolga Cinisli <tolgacinisli@gmail.com>
* fix(buzz-acp): wake held ACP threads and fence forked sessions (#7340)
## Summary
Adds an independent deadline wakeup so held thread work dispatches after
its 10-second bound even when the relay loop is otherwise quiet. Fences
session ownership by generation so a worker returning after a fork
cannot make an older provider session claimable again.
This follows up on the two post-merge findings from
[#7337](https://github.com/block/buzz/pull/7337#pullrequestreview-5116329341).
### Related issue
Follow-up to #7337.
### Testing
- `cargo test -p buzz-acp`
- `cargo clippy -p buzz-acp --all-targets -- -D warnings`
- Pre-push file-size, differential Rust test, and desktop Tauri gates
No UI changes.
---
**Update Sep 4, 15:35:** Addressed both Codex review findings.
- Queue-cap eviction now prunes orphaned hold deadlines.
- An expired hold stays expired until a worker is successfully claimed.
- Hold timers remain disabled while every worker is busy; worker return
wakes dispatch directly.
- Added regressions for queue eviction and pool exhaustion.
Generated with Codex
---------
Signed-off-by: Salman Mohammed <smohammed@squareup.com>
* fix(agent): route GPT-5+ model-service FQNs to Responses (#7358)
## Summary
Route Databricks Unity Catalog model services to OpenAI Responses when
their service name matches GPT-5 or newer. These models can reject tools
plus reasoning on Chat Completions.
Match only the service component, using the existing family-token
boundaries and a numeric major version. Catalog and schema names cannot
select the protocol. Keep neutral effort capabilities and the full model
ID unchanged; other services still use MLflow Chat Completions.
Keep the Rust and desktop resolvers in sync, add shared boundary cases
and a captured-HTTP regression for completion and summarization, and
update the documented FQN rule.
### Related issue
No duplicate found in searches for “FQN responses” PRs or “astra”
issues. Related: #6918 introduced Unity Catalog discovery.
Originating conversation:
buzz://message?channel=0b881928-a3a6-4c01-b981-8e64268f01ce&id=770949343bc96a9ed88acd90a1b37d358a0efc52c79237d0fdb491ce02b8d4ed
### Testing
No live Databricks inference test. The gateway must accept the full
model-service ID on its OpenAI Responses route; this remains the
integration risk.
The local `just ci` attempt exceeded its five-minute deadline during
`mobile-check`, so the full repository gate was not completed. All
push-hook checks passed.
Generated with Codex
Signed-off-by: Fizz <400e8babadcee6a7f420103f10a2849d84c4a9c71d5bd04f3948c814216648a3@buzz.block.builderlab.xyz>
Co-authored-by: Fizz <400e8babadcee6a7f420103f10a2849d84c4a9c71d5bd04f3948c814216648a3@buzz.block.builderlab.xyz>
* feat(mesh): upgrade to mesh-llm 0.76.0-rc8 and recommend Qwen3.8 27B (#6189)
Upgrades Buzz's mesh-llm dependencies through the released `v0.76.0-rc8`
tag (`2040765d`), including the Qwen3.8 curated recommendation and rc8's
scheduler/runtime improvements.
**Scope note:** the earlier open-relay/unenforced-admission mode has
been removed from this PR at Mic's direction — it is not a product mode
we want. Mesh admission remains roster/allowlist driven, exactly as on
`main`: on a relay with no NIP-43 membership snapshot the mesh runs
self-only. No NIP-11 mode probing, no mode-transition restarts. A future
perimeter/admission strategy for open relays will be designed
separately.
This PR also:
- seeds `BUZZ_AGENT_LLM_TIMEOUT_SECS=660` for mesh agents, above
MeshLLM's 600-second backend timeout;
- makes `desktop-tauri-clippy` lint both default and `mesh-llm` cfg
graphs;
- runs the feature-enabled desktop test suite in CI;
- recommends Qwen3.8 27B Q4_K_M for 64 GB-and-larger machines, then
ladders down through Gemma 4 E4B and Qwen 9B for smaller machines;
- keeps stored shared-compute `auto` translated to MeshLLM's supported
wire model `mesh`.
RC8 verification:
- `just ci` passed locally at
`92ecc7ec933bdd4df804cc9f28a2b51efa5313c5`.
- Pre-push differential gates passed, including both desktop Tauri cfg
graphs and package tests.
- A prior isolated runtime smoke used the RC8 binary's OpenAI endpoint
for a Buzz-shaped system/user/tool/tool-result/final-response loop; all
assertions passed and the isolated process was shut down.
Perf previously measured on M5 Metal, Qwen3.8-27B-Q4_K_M: TTFT 0.22–0.32
s, ~25 tok/s streaming; agent-shaped turns ~1 s to first token after the
first (prefix cache).
---------
Signed-off-by: Michael Neale <michael.neale@gmail.com>
Signed-off-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz>
Signed-off-by: Alessandro Joabar <sandro@squareup.com>
Co-authored-by: Michael Neale <michael.neale@gmail.com>
Co-authored-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz>
Co-authored-by: Mic Neale <305999590+micspiral@users.noreply.github.com>
Co-authored-by: Alessandro Joabar <sandro@squareup.com>
* fix(link-preview): keep composer fetches user-paced (#7211)
**Category:** fix
**User Impact:** Link previews can keep loading while a message is being
composed, while sending still has a finite escape hatch and stalled
network transports cannot occupy preview slots forever.
**Problem:** Native metadata and image deadlines could collapse slow
previews into fallback cards while the user was still composing, and a
shared image-host cooldown made pasted batches fail inconsistently after
one rate limit. **Solution:** Keep preview resolution user-paced with no
aggregate request deadline, bound transport inactivity (15s DNS/connect,
30s idle read), serialize image requests by host, and allow at most one
server-directed cooldown wait of up to 30s across an image fetch and its
redirects. The existing bounded post-Send preparation and immediate Skip
paths remain unchanged.
<details>
<summary>File changes</summary>
**desktop/src-tauri/src/commands/link_preview.rs**
Removes aggregate native deadlines so composer metadata work can
complete at the user's pace, while retaining DNS/connect/idle-read
liveness bounds. Adds bounded host-paced image request coordination that
releases its gate during cooldown, waits inline at most once for at most
30 seconds, and cannot renew that wait through redirects or the outer
transient retry. Same-host image and favicon requests remain
deliberately serialized to align with host rate limits.
**desktop/src-tauri/src/commands/link_preview_rate_limit.rs**
Adds a fixed-size striped host gate so concurrent image requests are
serialized without retaining an unbounded attacker-controlled hostname
map.
**desktop/src-tauri/src/commands/link_preview_tests.rs**
Moves native link-preview tests into a dedicated module and covers the
user-paced metadata contract, bounded one-shot cooldown behavior, and
gate release while a rate-limited request sleeps—including a different
host sharing the same bounded gate stripe.
**desktop/src-tauri/src/commands/link_preview_youtube.rs**
Removes the thumbnail fetch deadline so YouTube previews follow the same
composer lifecycle contract while using the shared bounded transport.
**desktop/src/shared/lib/useResolvedLinkPreviews.ts**
Adds development-only metadata outcome diagnostics with elapsed time and
image/fallback state, without logging encoded image payloads.
</details>
### Reproduction steps
1. Open the desktop composer and paste several GitHub pull request links
whose OpenGraph images share a host.
2. Observe that image requests are paced by host instead of racing, and
slow-but-progressing preview work remains pending rather than
immediately becoming a completed favicon fallback.
3. Send while preview work is still pending and confirm **Preparing link
preview** remains bounded by the existing post-Send budget.
4. Use **Skip** during preparation and confirm the message proceeds
immediately.
5. In a development build, inspect the console for `[link-preview]
metadata fetch completed` diagnostics containing elapsed time and image
state without base64 payloads.
### Related issue
N/A — scoped from the linked Buzz implementation room.
### Testing
At current head `dfb394aafbee537e9ffb04ad3732d08f65f30b8e`:
- Production-bound paused-time metadata regression passed through
`fetch_link_preview_metadata`; restoring the former 10-second aggregate
wrapper makes it fail at the pending assertion.
- Native link-preview module: 19/19 passed.
- `cargo check --manifest-path desktop/src-tauri/Cargo.toml` passed.
- Rust formatting and `git diff --check` passed.
- Pre-push `push-head-scope`, org safety, differential file-size,
branch-skew, and `desktop-tauri-checks` hooks passed.
At prior head `59e2dcf167b15c7a3e637ad2608008b7f9cef5f3`:
- Full Tauri Rust suite: 3,056 passed, 19 ignored; integration crates 7
+ 3 passed.
- Focused native link-preview suite: 26/26 passed.
- The pasted multi-preview workflow was exercised in the desktop app and
confirmed improved before draft publication.
---------
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@users.noreply.github.com>
* fix(acp): pace targeted overflow recovery on consumer capacity (#7325)
🤖
## Summary
When a Buzz agent falls behind on incoming messages, its connection can
make the backlog worse while trying to recover. The connection buffers
messages from the relay server until the agent is ready to process them;
if that buffer overflows, recovery previously requested history for
**every subscribed channel** and paused socket reads while sending those
requests. That adds traffic to an already overloaded connection. This
change requests history only for affected subscriptions, once the code
consuming those messages has room, with at least five seconds between
attempts.
The recovery path now:
- Combines repeated losses into one pending recovery per affected
subscription, keeping the oldest dropped timestamp so replay starts
early enough.
- Waits until at least half the consumer queue is free and the relay's
existing rate-limit delay has expired. The queue wakes recovery when
space becomes available; recovery does not periodically sample capacity
or hold queue space away from live messages.
- Attempts one subscription at a time, choosing the least recently
attempted so a busy channel cannot crowd out other channels or
membership notifications. The five-second delay starts when an attempt
finishes, including a failed write; failed writes leave recovery
pending.
Recovery is paced by available capacity, not by how often messages are
lost. This is not a larger buffer or a cutoff that abandons recovery.
Subscription identifiers, message filters, replay timestamp overlap and
duplicate filtering are unchanged; no downstream agent changes are
required.
This targets a reproducible overload **amplifier**, not every cause of
overload or every catch-up limitation. The initial live overload's cause
has not been established. Recovery remains best effort: a successful
request write is not proof of delivery, and existing history/retention
limits, bounded duplicate tracking and replay limitations still apply.
There is no exactly-once or complete catch-up guarantee. A stalled write
can still pause socket reads for the existing ten-second timeout; the
pacing bound does not cover initial subscriptions, reconnects or other
retry paths.
### Related issue
Closest related: #5014 (channel re-subscription); also #6661 (membership
reconciliation) and #6090 (relay backpressure gap signaling). This
addresses local overflow recovery scheduling, not those separate
mechanisms.
### Testing
Recorded offline comparisons against the previous behavior, with the
final implementation at `8000636f3073167c5a5107bb179c7d91160f1729`:
| Same fixture: 18 subscriptions, three overload rounds | Before | After
|
| --- | --- | --- |
| Recovery history requests | 108 | 3 |
| Ping-response delay | About 4.6 seconds | Below the measurement's 1 ms
resolution |
A separate bounded-history fixture delivered all 320 events plus
subsequent live traffic in **both** versions. Regression coverage
exercises the real socket-handling task, including intermittent consumer
capacity, fairness, failed writes and cancellation of capacity waits
before live delivery. These are synthetic results, not production
throughput measurements or evidence of a deployed cure.
The full local `RUST_TEST_THREADS=4 just ci` run passed on September 4,
2026. Earlier unsuccessful local runs remain part of the validation
history. The [recorded validation evidence and separate desktop
follow-up](https://github.com/block/buzz/pull/7325#issuecomment-5540592398)
preserve the original desktop mock-history scroll failure, its passing
rerun and the remaining investigation. That desktop path does not run
the agent connection code; neither this repair nor the passing rerun
fixes the observed scroll problem.
---------
Signed-off-by: Logan Johnson <loganj@squareup.com>
* fix(mobile): render push notification sender identity as npub (#7494)
🤖
## Summary
When an iOS push notification comes from someone the app has no cached
name for, the notification title showed the first characters of the
sender's raw public key — for example `aa4fc866…`. That fragment is
unreadable and doesn't match how the same person appears anywhere else
in Buzz. This PR changes that title to the compact form of the sender's
npub (npub is the human-readable encoding of a Nostr public key): first
8 and last 4 characters — for example `npub14f8…9nsy`, the same identity
shape used across the desktop and mobile apps.
- Unnamed senders: raw hex fragment → compact npub.
- Named senders: unchanged — a sender the app has a display name for
still titles the notification with that name.
- Unverifiable sender identities (malformed keys, or lookalike strings
that are not literal 64-hex-digit keys) now render a neutral "Someone"
instead of partial raw key material.
- Everything else about the notification is unchanged: body text,
subtitle, thread matching and grouping, deep-link navigation, thread
identifiers, and the internal hex public key the resolver matches on.
The native iOS notification-service package (`BuzzPushKit`) gains a
minimal in-house bech32 codec (bech32 is the checksummed string encoding
npubs use) — checksum-validated, 32-byte keys only, and no new external
dependency. The hex input branch accepts exactly a 64 ASCII hex digit
key before any parsing, so strings that merely parse like hex (for
example a run of `+a` pairs) cannot become a displayed identity; this is
input validation for presentation. Event signature verification is
untouched.
### Related issue
Fixes: N/A. Searched existing issues/PRs for push-notification npub
identity — closest related: none found.
### Testing
At head `3e3f2813b8864b76257ccb50dea3a4b31fa4de0d` (base
`44316ff72f5f7de014c66b01cbf534298a70c249`; 4 files, +321/−4):
- CI `Mobile Swift` lane, at this exact head — all passed: `swift test`
(73 tests, 0 failures), the SwiftPM debug and release builds of
`mobile/ios/BuzzPushKit`, and the unsigned iOS release build.
- Test coverage: npub encoding cross-checked against independent
nostr-rs/NIP-19 vectors; rejection of bad checksums, mixed case, wrong
lengths, invalid alphabet, padding, and non-32-byte payloads; resolver
boundary matrix — hex/npub/invalid sender keys render compact npub or
"Someone" while body, subtitle, sender key, and thread identifier pass
through; named senders keep cached display names.
### Task provenance
Buzz channel: `1f0e4a3d-7e01-4efe-bb16-843b357f85c9`
Task:
buzz://message?channel=1f0e4a3d-7e01-4efe-bb16-843b357f85c9&id=86b34eb4bd84a1472419e9af22636c011c0fe273e3c196f967d7a36996e149b6
---------
Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
* fix(desktop): shared npub identity foundation (canonicalNpub, PubKey gate, strict parser) (#7488)
🤖
## Summary
Identity keys in the desktop app are displayed as raw 64-character hex.
A person's key shows up as something like `953d3363…` — unreadable,
impossible to recognize as the same identity on another screen, and a
hazard when copied by hand. Nostr (the protocol Buzz runs on) has a
human-readable spelling for identity keys — the `npub1…` form — but the
desktop app did not use it consistently.
This is the foundation of the desktop npub changes: it adds the shared
pieces every identity surface builds on, and two follow-up slices stack
directly on this branch — #7489 converts the identity controls (profile,
settings, allowlist, workflow key fields) and #7495 converts the
everyday display surfaces (mentions, member lists, sidebar, and other
name fallbacks).
After this change:
- The shared identity widget shows the compact npub form —
`npub1j57...fjmv` — instead of a hex prefix, everywhere it renders (for
example the owned-agent public-key row on a profile). Copying it puts
the full npub on the clipboard.
- Copy is a real interaction, verified end-to-end: both popover variants
put the exact canonical npub on the actual clipboard — never the raw hex
the popover also lists, never a truncation — and a portaled popover's
clicks no longer steal focus from the new-DM To-field mid-copy. Pointer
copy, a natural Space-then-Enter path, and inner/outer Escape are
covered.
- Anything that isn't a valid identity key fails neutrally: short or
corrupt values — including degenerate values that technically encode to
a checksum-valid npub but aren't real identity keys — show "Unavailable"
with no copy button, instead of a misleading value.
- Both valid npub spellings display: all-lowercase `npub1…` and
all-uppercase `NPUB1…` (Bech32, npub's encoding, permits either casing)
both render the same canonical lowercase npub. Mixed case is rejected by
the display path as written — `canonicalNpub` and the widget don't
case-normalize input — while input parsing (`parsePubkeyInput`) keeps
its trim-and-lowercase normalization and accepts mixed-case npubs; both
paths require the decoded payload to be exactly a 64-character identity
key.
- Identity-key input is strict on payload: an npub whose decoded payload
isn't exactly a 64-character identity key is rejected, matching the
validation the app's Rust side already applies to agent allowlists.
Intentional scope boundary: only surfaces that render through the shared
widget change here. Outer profile copy, settings identity cards, the
respond-to allowlist, and workflow key fields still show hex — they move
to npub in the controls follow-up (#7489). Nothing else changes identity
representation: display names, private keys, event IDs, and the hex the
app stores, sends, and matches internally are untouched; only the
user-facing spelling of an identity key changes.
## Details
- `desktop/src/shared/lib/pubkey.ts` — `canonicalNpub()`: strict
canonical full-npub helper (64-char hex in any case, or a
checksum-validated npub, returns the canonical npub; anything else
returns `null`); `truncateNpub()`: the compact display form; existing
exports unchanged.
- `desktop/src/shared/ui/PubKey.tsx` — the shared widget's identity gate
validates through `canonicalNpub`; the popover copies the npub only.
- `desktop/src/shared/lib/nostrUtils.ts` — `parsePubkeyInput` rejects
npubs whose payload is not exactly a 64-character identity key.
- `desktop/src/features/messages/ui/NewMessageScreen.tsx` — the To-field
focuses its search input only for clicks that land inside the field
itself, so portaled recipient popovers keep their focus while open (a
popover click previously dismissed it mid-copy).
- Unit suites cover the helper, widget, and parser (including the
degenerate-encode and uppercase regressions); the e2e specs that render
these rows assert the npub display.
### Related issue
- Fixes: N/A. Searched existing issues/PRs for npub identity display —
no existing match.
- Stack: #7489 is based on this branch and builds on these primitives;
it does not stand alone on main.
### Testing
At head `b3310c248` (base: main `44316ff72`; 12 files, +440/−39):
- Focused unit suites (pubkey, PubKey, parsePubkeyInput): 20/20 green;
mutation-checked — removing the decoded-length predicate fails the
short/empty checksum-valid-npub assertions in `canonicalNpub` and the
widget, and a wrong-identity clipboard value fails the new copy
assertions.
- `pnpm typecheck` and `pnpm check`: pass; full desktop unit suite
6459/6459 at this exact head.
- Targeted e2e at this exact head: 8/8 across the two specs that own the
clipboard flows — `agent-access-warning.spec.ts` (compact variant,
agent-access owner hint) and `pubkey-display-screenshots.spec.ts` (full
variant, new-DM recipient verification: pointer copy, popover surviving
the copy, inner/outer Escape, Space-then-Enter).
- No Rust-side or build files change in this PR, so those results are
unaffected.
### Task provenance
Buzz channel: `1f0e4a3d-7e01-4efe-bb16-843b357f85c9`
Task:
buzz://message?channel=1f0e4a3d-7e01-4efe-bb16-843b357f85c9&id=86b34eb4bd84a1472419e9af22636c011c0fe273e3c196f967d7a36996e149b6
---------
Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
* fix(desktop): npub identity displays for mention, member, and workflow surfaces (#7495)
🤖
## Summary
Every Buzz account is identified by a long public key. Before this
change, when someone had no display name, surfaces fell back to
inconsistent labels — mostly raw hex fragments like `abcd1234…wxyz`,
sometimes a generic role label with no key — so the same person looked
different from surface to surface, and nothing looked like an npub
address. This PR applies the npub identity foundation from #7488 to the
everyday surfaces: a person without a display name now falls back to the
same compact npub everywhere — `npub1xxxx…yyyy`, the human-readable
spelling of their public key (first 8 + last 4 characters of the full
npub) — across messages and mentions, reactions, huddles, member and
participant lists, the sidebar and channel activity, search, projects,
tray, notifications, and workflow surfaces.
- **Mentions and messages**: key-only mention chips render the compact
npub. Pasting a copied mention back still re-binds it byte-exactly to
the identity it declares, for both the new npub chips and legacy
hex-truncated chips copied by older clients — wrong, missing, or
tampered key qualification is rejected instead of silently degrading to
plain text.
- **Reactions and huddles**: huddle reaction events and the huddle
roster/participants render the compact npub for unnamed participants;
workflow reaction triggers describe authors with the same form.
- **Members and sidebar**: channel and community member lists,
add-member results and invites, the members sidebar, the
channel-activity popover, search, projects (assignees/reviewers/PR
panels), the tray menu, and desktop notifications all fall back to the
compact npub; titles and aria labels keep the machine-readable full
labels.
- **Profile labels**: panel/popover display names and owner handles fall
back to the compact npub (never raw hex) when there is no name;
linked-event (nevent) message metadata shows the npub-shaped author
fallback while the event lookup and event IDs are unchanged.
- **Workflows**: author-picker secondary labels, step destination keys,
and trigger-author references render compact npubs; event and blob IDs
keep their existing hex compacts (they are not identities).
- **Avatars stay distinct**: fallback avatars for key-only identities
derive initials from the key's tail, so prefixed role labels like
"Participant npub1…" no longer collapse every unnamed participant onto
the same initials; people with names keep their name initials.
Preserved exactly: display names and distinct avatars, internal hex keys
(storage/API forms unchanged), clipboard identity roundtrips, event/blob
ID compaction, private keys (no nsec path is touched), and nevent link
handling.
Scope: this PR changes what identity labels **display**, not identity
controls — profile/settings copy controls, the respond-to allowlist,
workflow key fields, and agent dialogs are the sibling slice #7489, and
the shared primitives (`canonicalNpub`, `truncateNpub`, the `<PubKey>`
gate, strict input parsing) come from the foundation #7488.
### Related issue
- Fixes: N/A. Searched existing issues/PRs for duplicates — none found;
the related work is the npub identity stack this slice belongs to.
- Base/dependency: stacks on #7488 (foundation) — this PR does not stand
alone on main.
- #7489 is a sibling slice on the same #7488 base
(profile/agent/workflow controls), not a dependency: this PR does not
require #7489, and #7489 does not require this PR — both only require
#7488.
### Testing
At exact head `4763cbeae1dd521309755e6d61f657324cb98667` (base:
`fix/desktop-npub-identity-d1a` @
`5f3a4a8111998c8aa41ad77cf66992bd1c85343c`; 71 files, +656/−189 —
production +277/−136, test support +379/−53):
- At this head: targeted `mentions.spec.ts` (1/1), the e2e build,
typecheck, and biome — green.
- 9 changed/related unit files: 100/100 green; typecheck, e2e build,
biome, and px text/truncation checks clean; huddle-roster focused run
green; channel-activity e2e 11/11; mutation checks confirm the fallback
wiring (removing it collapses shared initials and drops fallback rows).
- Known pre-existing local e2e failures, unchanged by this PR and
reproduced identically at the upstream merge-base: huddle-transcription
voice-menu attribution (25 pass / 1 fail) and the
`workflow-local-controls` 438px caret drift. Not claimed green locally.
- Update at head `236af9e6137386737e84d3a474d6bc808a704c50` (test-only
follow-ups `1143af345` + `236af9e6`): the `workflow-local-controls`
races were fixed in the test drivers, and the 438px diff was shown to be
a stale Darwin snapshot baseline (name-row enable switch already absent
and `message_posted` already MessageSquare at recording commit
`9390e11c9`) and refreshed — the focused screenshot test, including
keyboard/caret assertions, now passes locally (twice). The full spec was
not rerun after the snapshot refresh; the huddle-transcription item
above is unchanged.
Label/copy text changes are asserted by the e2e specs (`mentions`,
`mention-recipients`, `pubkey-display-screenshots`,
`huddle-transcription`, `channel-activity-popover`,
`workflow-local-controls`) rather than new screenshots; the screenshot
spec pins the compact npub text forms.
### Task provenance
Buzz channel: `1f0e4a3d-7e01-4efe-bb16-843b357f85c9`
Task:
buzz://message?channel=1f0e4a3d-7e01-4efe-bb16-843b357f85c9&id=86b34eb4bd84a1472419e9af22636c011c0fe273e3c196f967d7a36996e149b6
---------
Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
* fix(desktop): npub identity controls across profile, agents, and workflows (#7489)
🤖
## Summary
Building on #7488's npub foundation, this PR finishes the identity
display change for the controls where you actually manage people and
keys: profile, settings, agent access, and workflows. Everywhere in
these surfaces, an identity key shows — and copies — as its canonical
npub (npub is the human-readable encoding of a Nostr public key: the
compact `npub1j57...fjmv` form where space is tight, the full npub where
the whole key matters), and accepts npub as input.
After this change:
- Profile panel: the public-key row and the managed-by / declared-owner
copies show the full npub. If a key can't be encoded, you see
"Unavailable" with no copy button — never a raw or partial key.
- Settings: the identity card shows and copies the npub. The
hosted-communities account identity derives from the bound key
(`pubkey_hex`) — the same authority as the mismatch gate and hosted
operations — so the display can never disagree with what the app acts
on; an unusable hex falls back to a neutral label instead of rendering
the unverified server npub. The connected claim and a community's
Connect action require that same usable bound key to match the local one
— with no usable binding the card cannot claim connected or start
Connect, while the community list, linking, and delete/rebind recovery
stay available.
- Hosted create/onboarding: the account and device identity rows in the
create flow and owner onboarding derive from the same authoritative
fields (bound key / local key), with the same neutral fallback;
readiness requires a usable bound key that matches the local one.
- Respond-to allowlist (controls who may respond to an agent): entries
can be typed or pasted as hex or npub; both spellings of the same key
are recognized as one entry and dedupe. Search results, chips, and
remove buttons use the compact npub.
- Workflow key fields: to/from keys display as npubs in the form and
save back as canonical hex. Templates like `{{trigger.author}}`, roles,
and free text pass through untouched; placeholders accept both
spellings.
- Recipient and agent dialogs: the verify popover is npub-only (the
raw-hex line is gone); denied-membership screens never show a raw key.
- The Rust-side truncated display name (used for native surfaces) shows
the same compact npub, so those surfaces match the web UI.
Internal representation is unchanged: keys are still stored, sent, and
matched as canonical 64-character hex — npub is a display and input
spelling, normalized to hex at the boundary, so existing data and
integrations keep working. Bound-key usability and comparison use one
normalized form (trimmed, lowercased, 64 hex characters; npub rejected),
so padded or mixed-case spellings of the same key match. Display names,
private keys, and event IDs are untouched.
## Details
- `respondToAllowlist` / `RespondToField`: npub entries normalize to
canonical hex; cross-form dedupe; compact npub in rows and chips;
direct-add accepts npub and stores canonical hex.
- `workflowFormTypes` / `WorkflowStepCard`: hex → npub for display, npub
→ canonical hex on save; templates, roles, and free text pass through in
both directions (roundtrip-tested).
- `UserProfilePanelFields`, `ProfileSettingsCard`,
`HostedCommunitiesSettingsCard`, `MembershipDenied`,
`SelectedRecipientChip`, `AddAgentToChannelDialog`: npub display and
copy; invalid keys → "Unavailable" with no copy; hosted identity rows
derive from the bound `pubkey_hex` (create/onboarding rows from the
bound and local keys), never the unverified server npub;
connected/readiness/Connect gates use the same usable-bound-key
predicate, and the settings Connect invocation callback re-checks it
before starting.
- `src-tauri/src/commands/identity.rs`: `truncated_display_name`
compacts to the first 8 + last 4 characters of the npub (above a 12-char
threshold), mirroring `truncateNpub`.
- e2e: profile key rows and clipboard polls assert npub forms and
raw-hex suppression; the display-screenshots spec pins the npub-only
popover; hosted specs drive the real settings card, create flow, and
onboarding rows through their real providers, and the unlinked/npub-only
identity cases assert no connected claim and no Connect action.
### Related issue
- Fixes: N/A. No separate issue; the related work is the stack below.
- Stack: builds on #7488 (shared npub foundation), now merged; this PR
is rebased onto main and stands on its own.
### Testing
At head `303c90ffa` (base: main `bfc38485`; 24 files, +1125/−146):
- Focused unit suites (respondToAllowlist, workflowFormTypes,
hostedCommunityApi bound-key helpers) green; mutation-checked — dropping
allowlist canonicalization fails the dedupe case, and dropping bound-key
normalization fails the npub-in-hex and padded same-key cases.
- Full desktop unit suite 6,477/6,477, `desktop-typecheck`,
`desktop-check` (formatting fixed narrowly with `biome check --write` on
the touched files only), and a fresh E2E build at the current head; the
add-community + hosted-communities-settings specs 18/18 and onboarding
integration 69/69 on a fresh dedicated port, with focused new-case runs
4+4 covering padded same-key (ready, Connect kept — no false rebind) and
npub-in-hex (neutral label, recovery, no Connect) across the settings
card, create flow, and first-community onboarding, plus the
unlinked-account settings regression asserting Connect cannot occur.
- `cargo fmt`/clippy (both feature sets) and `cargo test identity` (71
pass) passed at the earlier full-change head; since then, the only
production changes in this PR's delta are the hosted identity display
authority and its fail-closed bound-key gating/normalization above
(base-side fixes carry #7488's receipts) — every other change is
test-only.
### Task provenance
Buzz channel: `1f0e4a3d-7e01-4efe-bb16-843b357f85c9`
Task:
buzz://message?channel=1f0e4a3d-7e01-4efe-bb16-843b357f85c9&id=86b34eb4bd84a1472419e9af22636c011c0fe273e3c196f967d7a36996e149b6
---------
Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
* fix(mobile): standardize public-key identity display on npub (#7493)
🤖
## Summary
In the mobile app, anyone who hasn't set a display name shows up as a
raw 64-character hex key (e.g. `3a5d4f9c…`) — unreadable, and
unrecognizable as the same identity across screens. Profile and Settings
also let you copy that raw hex. Nostr public keys have a standard
readable form — `npub1…`, the same encoding other Nostr apps and our
desktop app already display. This PR makes every mobile identity surface
render npub instead:
- **Unnamed people everywhere** — message and thread authors, reactions,
typing indicators, member lists, channel details, DM headers and tiles,
inbox, search, forum cards, Pulse notes and reply context, mention
suggestions, and invite rows — now show a compact npub label: first 8 +
last 4 characters of the full npub joined by an ellipsis
(`npub1abcd…wxyz`), the same truncation desktop uses. Previously these
showed truncated raw hex.
- **DM fallback avatars and blank names** — 1:1 DM tiles and headers key
their fallback avatar to the same non-self counterpart the label names,
including self-first participant order; a self-DM keeps its
hex-key-derived initial. Blank or whitespace-only display names fall
back to the compact npub instead of rendering empty, while nonblank
authored names render verbatim (padding included).
- **Profile sheet → "Copy public key"** now copies the full canonical
npub — never raw hex. When the identity string isn't a valid public key,
the copy tile is disabled, so a malformed key never reaches the
clipboard.
- **Settings → Identity (pubkey)** displays and copies the full npub; an
invalid identity reads "Identity unavailable" with copy disabled.
- **Invalid identities never leak truncated raw hex** into the UI
anywhere — they render a neutral "Unknown identity" label.
- **Unchanged on purpose:** display names and verified handles (NIP-05 —
the `name@domain` badge) still render as before. Unnamed avatars keep
distinct per-key initials, derived from the underlying hex key rather
than the npub — otherwise every unnamed key would render the same "N"
initial. Event IDs are not public keys, so they keep their hex
truncation (in Pulse's "Replying to", the parent author shows npub while
an event-id fallback still shows hex). The nevent share link, private
keys, and internal hex storage are untouched. Inputs that accept a key
(invite/member entry) accept both hex and npub and keep working in hex
internally.
### Related issue
N/A. Searched open issues/PRs for npub identity display on mobile —
closest related: none found. Desktop's parallel npub standardization
lives in the stacked desktop PRs (#7488 foundation, #7489 controls,
#7495 display surfaces); this is the independent mobile slice (based
directly on `main`, not on those branches).
### Testing
At exact head `5a620e420a1fd57d9d8011ac26434eed32fcf765` (base: `main`
`44316ff72`; 40 files, +1,345/−154):
- Full mobile suite: 2,098 tests passing (`cd mobile && flutter test`);
`flutter analyze` clean; `dart format --set-exit-if-changed .` clean —
the same checks CI runs.
- Widget/unit coverage at production seams: compact labels and hex-keyed
avatar initials for DM headers/tiles, member rows, mention suggestions,
and Pulse reply context; DM fallback avatars keyed to the labeled
counterpart (self-first order and self-DMs); blank/whitespace
display-name npub fallback with nonblank authored labels verbatim,
including the Activity inbox sender and profile-sheet heading (each with
its own empty/whitespace production-seam regression); full-npub copy and
disabled-copy semantics in profile and settings; invalid-key
suppression; and hex↔npub input round-trips.
Verified via unit and widget tests — no device/simulator validation is
claimed.
### Task provenance
Buzz channel: `1f0e4a3d-7e01-4efe-bb16-843b357f85c9`
Task:
buzz://message?channel=1f0e4a3d-7e01-4efe-bb16-843b357f85c9&id=86b34eb4bd84a1472419e9af22636c011c0fe273e3c196f967d7a36996e149b6
---------
Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
* fix(desktop): order unnamed roster members by full canonical npub (#7503)
🤖
## Summary
- Channel members appear in the Members sidebar. A member who has never
set a display name is listed under an abbreviated form of their public
key (npub), and the sidebar previously sorted those unnamed members by
that short label. Short labels are not unique — different keys can share
one — so the order of unnamed members could look arbitrary or unstable.
Unnamed members now sort by their full public key, so the order is
deterministic.
- When two members display the same name, the previous tiebreak was
membership order (who joined first), which is not visible to a reader
and can shift as roster data loads in. The tiebreak is now the full
public key, so identical display names always land in the same order.
- Nothing gets noisier on screen: the full key is used only for sorting,
and the sidebar still shows the compact abbreviated form. Priorities are
unchanged — authored (custom) names still outrank fallback labels, and
role/current-user grouping still applies.
- Scope is the desktop app's Members sidebar and member management: the
two existing sort comparators. Mobile and other lists in the app are
untouched.
### Related issue
Based on #7495 (introduced the abbreviated npub labels this follows up
on). The original five presentation PRs remain independently reviewable.
No closer duplicate found.
### Testing
- 6469 desktop unit tests, typecheck, and check pass.
- The 3 existing consumer-seam E2E tests still pass; a new E2E test
asserts the sidebar lists unnamed members in full-key order, with
fixture members deliberately inserted in the opposite order so incoming
membership order cannot mask the sort.
- Negative check: reverting only this change makes the new ordering
assertion fail, so it genuinely binds the new sort.
- CI has not run on this PR yet.
Buzz provenance: channel 1f0e4a3d-7e01-4efe-bb16-843b357f85c9 / task
340c3de9b27dbedb8453c0c7652220f9080d30fcc70a7c4f6e27fdd4fa378056
---------
Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
* fix(desktop): require a Codex adapter with Astra support (#7427)
## Summary
Buzz considers codex-acp 1.6.2 current because the supported adapter
floor is still 1.1.7. That adapter bundles Codex 0.148.0, so updating a
separate Codex CLI to 0.153.4 leaves managed agents on the older runtime
and unable to use GPT-6 Astra.
Raise the supported adapter floor to the published 1.10.0 release, which
depends on `@openai/codex ^0.153.3`. Existing discovery and installation
code then classifies older adapters as outdated and offers the managed
reinstall path. Update the availability and install-plan regressions to
cover the observed 1.6.2 installation and the new minimum.
This follows the existing version-floor policy. It does not
automatically update a running installation: the user must complete
Buzz’s offered adapter upgrade. Future upstream compatibility changes
may require another floor update.
### Related issue
No exact duplicate found in searches for Astra, CODEX_PATH, bundled
Codex, outdated runtime, and codex-acp 1.10. Related: #3097 raised the
older floor to 1.1.7 (already present on main); #2422 covers lost error
details for runtime mismatches. Neither resolves this version gap.
Originating conversation:
buzz://message?channel=3286cd76-f83e-4c7d-8317-10a16580744d&id=8b79a73078217222b870fff144c27e7d27bcd5a67c966869c18fe726db716898
### Testing
- Isolated npm install of codex-acp 1.10.0 resolved bundled Codex
0.153.4, with no CODEX_PATH override.
- Live macOS ACP probe: initialize protocol v1 → session/new → select
gpt-6-astra[medium] → prompt. Received `OK` and `stopReason: end_turn`;
usage metadata confirms gpt-6-astra.
- Existing adapter 1.6.2 initialized but advertised no Astra model in
the same probe.
- Desktop Rust formatting and `git diff --check` pass.
- `just desktop-tauri-test`: 3,266 passed, 20 ignored, zero failures
across the Desktop workspace and integration tests.
- Workspace and Desktop Clippy, frontend static checks, and `just
file-size-check` pass.
- Repository `just ci`: still running the remaining
mobile/build/workspace-test stages.
The installed Buzz app and managed adapter were not replaced or
restarted. The live check validates the new adapter/runtime path; a
complete packaged Desktop upgrade workflow remains untested.
Signed-off-by: Stephen DeLorme <stephen@d.elor.me>
* fix(buzz-acp): report missing models without retrying (#7538)
## Summary
When an agent reports model-not-found, Buzz retries the unavailable
model and delays the failure reply until retries are exhausted. Stop
retrying this error and immediately post a threaded recovery notice. The
notice tells users to select a different model in agent settings, save,
restart the agent to apply the configuration, and re-send their request.
This adds one error-handling branch and regression coverage in
`buzz-acp`. It matches `-32002` errors containing `model not found`.
Other resource-not-found errors, such as stale sessions, retain the
existing retry behavior. Detailed error events remain available for
diagnosis. The existing restart policy is unchanged.
### Related issue
None found in existing issue/PR searches for model-not-found recovery.
### Testing
Playwright captured and visually checked the thread UI with seeded
conversation data and the exact recovery text. The check opens the
request's thread, confirms no reply before the failure, injects the
notice, and verifies the full text is visible. [Before/after
screenshots](https://github.com/block/buzz/pull/7538#issuecomment-5608196506)
show the corrected save-and-restart instructions. These are local test
captures, not a deployed provider recovery flow.
Generated with Codex
---------
Signed-off-by: Diem Nguyen <diem@squareup.com>
* fix(desktop): let inbox title and message author names truncate under narrow panes (#7550)
## Summary
Fixes two instances of the same dead-truncate pattern in the desktop
app, where a flex item's implicit `min-width: auto` prevented `truncate`
from engaging, so long text painted over adjacent controls instead of
ellipsizing:
- **Inbox detail title** (`InboxDetailPane.tsx`): the clickable
context-title button sized to its text instead of shrinking with the
pane, overlapping the header controls (open-in-channel, members, huddle,
more menu). Fixed by adding `max-w-full`.
- **Message author names** (`MessageHeader.tsx` /
`UserProfilePopover.tsx`): the `UserProfilePopover` inline-flex trigger
wrapper refused to shrink below the name's nowrap width, running long
author names under the hover action bar and off the pane edge. Fixed by
adding a `triggerClassName` prop to `UserProfilePopover` and passing
`min-w-0 max-w-full` at the author call site.
Two other suspected instances (project file breadcrumb, drafts pane
title) were stress-tested and already truncate correctly — no change.
### Related issue
N/A — none found.
### Testing
- New Playwright regression tests for both fixes
(`inbox-title-overlap.spec.ts`, `message-author-overlap.spec.ts`,
registered in the smoke project), each proven to discriminate: they fail
with the fix reverted (real measured overlap) and assert the ellipsis
actually engages with non-zero title width, so they can't pass
vacuously.
- Typecheck, lint, and full desktop unit suite green (pre-push hooks);
full desktop e2e smoke suite run earlier: 1402 passed, 3 pre-existing
unrelated failures (each fails identically with the fix reverted).
**Inbox title — before** (long title paints under the header controls):

**Inbox title — after** (truncates with ellipsis, controls stay clear):

**Author name — before** (long name runs past the header row edge):

**Author name — after** (clean cutoff):

🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Signed-off-by: cynfria <yescynthia@gmail.com>
Signed-off-by: Tree Trunks <6ba22921d9dc2ad0aa6ecdf63787ddd24726e266d866da31af69f2e4e146ace5@buzz.block.builderlab.xyz>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Tree Trunks <6ba22921d9dc2ad0aa6ecdf63787ddd24726e266d866da31af69f2e4e146ace5@buzz.block.builderlab.xyz>
* fix(relay): reject presence updates when Redis storage fails (#7532)
## Summary
- Reject kind:20001 presence events with `OK false` / `error: presence
storage unavailable` when Redis SET or DEL fails, before publishing,
local fan-out, or local-event marking.
- Preserve the producer contract needed by snapshot-confirming
consumers: delivered live presence must follow successful mutation of
the Redis state read by snapshots.
- Classify those backend rejections with the existing `IngestError`
taxonomy so a presence storage outage counts as
`buzz_events_rejected_total{transport="ws",reason="error"}`, not client
`reason="invalid"`; genuine client-input refusals (verification failure,
membership gates) stay `invalid`, and every wire message is an unchanged
fixed sanitized string (review follow-up, no protocol wording change).
- Add actual `handle_event` integration coverage for rejected
online/offline transitions, healthy online→offline
accepted/stored/fanned-out behavior, and the rejection-counter routing
on storage failure with an invalid-signature control.
This is standalone on main; it does not depend on the mobile
implementation. Deploy this relay prerequisite before relying on #7526's
snapshot-confirmation policy. Existing
pubsub-failure-after-successful-storage behavior and disconnect TTL
cleanup are deliberately unchanged. A storage error may be an ambiguous
write outcome, not a rollback guarantee; the rejected event is not
published by this handler. Clients may retry the generic `error:`
rejection. Desktop's 60s heartbeat retries non-offline presence, not
every explicit offline transition.
### Related issue
Addresses the relay prerequisite identified in [#7526 review
5157607827](https://github.com/block/buzz/pull/7526#pullrequestreview-5157607827).
Searched open presence/storage PRs; no duplicate relay storage-error
rejection fix found. #7382/#7383/#7526 heads and bases are unchanged.
### Testing
Exact head: `389174df29cc02d0f885c03209eff661d8bb2ec0` (+380/-13; 393
total), one commit `389174df2` on top of the reviewed `c031d6eb1`
(DCO-signed; base `bfc384855889432df4a333a0edf3080f332ee169` unchanged).
- PASS: `cargo fmt --all -- --check`, `cargo clippy -p buzz-relay
--all-targets -- -D warnings`, `git diff --check`, `just
file-size-check`, PostgreSQL discovery validation — all run at the exact
final head with a clean tree before and after.
- PASS: documented native `scripts/postgres-test-run.sh -p buzz-relay
--lib --tests`: **89/89** actual integration tests, including the four
presence cases (online/offline storage rejection, healthy
online→offline, and the new rejection-classification case). Owned
PostgreSQL 17/Redis on isolated loopback ports, schema plus
reconciliation applied; no shared development database.
- PASS: explicit `cargo test -p buzz-relay presence_storage -- --ignored
--nocapture`: **4/4**, not skipped.
- Full isolated relay crate suite at the final head (`cargo nextest run
-p buzz-relay --lib --tests`): **1062 run: 1062 passed, 94 skipped**.
The previously failing
`api::mesh_demo::tests::demo_join_forwarded_arm_round_trips_echo` passed
in this run (1.5s); it is a known timing-sensitive main baseline failure
tracked open in #7140 and untouched by this PR, so this single passing
run is reported as-is and does not claim environmental clearance or
close #7140. No full-suite-green claim is made beyond this run.
- Mobile is untouched; #7526's existing 2090-test/format/analyze
evidence remains scoped to its unchanged head. Its separate Desktop
Smoke E2E (2) failure remains red; no CI retries requested.
[Production-seam regression
coverage](https://github.com/block/buzz/blob/389174df29cc02d0f885c03209eff661d8bb2ec0/crates/buzz-relay/src/handlers/event.rs#L1491-L1803):
the metric case drives real `handle_event` traffic against a genuinely
dead Redis endpoint with a seeded active PostgreSQL community and a
registered presence watcher, asserts the storage rejection counts
`reason="error"` while a tampered-signature control through the same
dispatcher arm stays `reason="invalid"`, and re-asserts the rejected
ACK, no fan-out, and no local-event marker. Counter assertions use a
thread-local recorder guard held across `.await` points (the buzz-db
counter-test convention) inside the per-process nextest postgres-ci
lane, so no parallel test can race the counter snapshot.
No UI change or screenshot. Local logs and reproducible service/gate
scripts are retained under
`WORK_LOGS/MOBILE_FEEDBACK_PRESENCE_20260909/relay_prerequisite/metric_correction/`
in the engineering workspace. This PR is a review candidate, not merge
clearance.
Causal checks: restoring only the pre-fix production mutation block
makes both original rejection tests fail (`OK true` instead of `false`);
healthy success still passes. Reverting only the typed classification
(mapping the ephemeral `Internal` arm back to `invalid`) makes the new
metric regression fail with the outage counted as `[("ws","invalid",2)]`
instead of `[("ws","error",1),("ws","invalid",1)]`. The unchanged mesh
echo case also failed 504/200 with the main-production block restored in
the prior run, supporting its separation from this change without
claiming environmental clearance. Candidate source restored
byte-for-byte after each mutation. Repository-wide `just ci` was not
rerun; the scoped relay gates above are the new evidence.
---------
Signed-off-by: Logan Johnson <loganj@squareup.com>
* fix(markdown): align mention chip wrapping (#7501)
**Category:** fix
**User Impact:** Human and agent mentions now break across lines with
the same cloned chip treatment as repository and permalink chips while
preserving the conversation text rhythm.
**Problem:** Profile-backed rendered mentions sat inside an
`inline-flex` popover trigger, unlike entity chips, so the wrapper
interfered with true inline fragmentation. The browser-layout test
measured text-range rows rather than the painted chip rectangles,
allowing touching decorations to pass as “separate” fragments.
**Solution:** Keep the profile trigger interactive but override its
layout to true `inline`, then give mention fragments 18px computed
leading inside the message’s 20px prose rhythm. Chromium paints each
fragment at 17px and advances it by 20px, leaving a visible gap between
cloned rounded rectangles. The browser test now measures the chip’s own
`getClientRects()` and asserts fragment count, height, gap, and step;
entity links retain their existing 22px leading.
<details>
<summary>File changes</summary>
**desktop/src/features/profile/ui/UserProfilePopover.tsx**
Allows inline consumers to override the trigger wrapper’s layout without
changing other profile-popover call sites.
**desktop/src/shared/styles/globals/markdown.css**
Keeps one shared wrapping-chip mechanic and gives mention decorations
enough room to separate visibly within 20px prose.
**desktop/src/shared/ui/markdown.test.mjs**
Pins both rendered mentions and entity links to the shared wrapping-chip
contract.
**desktop/src/shared/ui/markdown/MarkdownMention.tsx**
Makes the profile-popover trigger truly inline so the nested mention
chip can fragment with surrounding prose.
**desktop/src/shared/ui/mentionChip.ts**
Keeps `wrapping-inline-chip` as the single contract for fragmenting
decorated chips.
**desktop/tests/e2e/mentions.spec.ts**
Measures the painted chip rectangles, requires a positive fragment gap,
and verifies the inline trigger remains mouse- and keyboard-operable.
**desktop/tests/e2e/navigation.spec.ts**
Keeps a wrapped repository chip as the control, asserting its existing
22px line height and fragment advance.
</details>
## Reproduction steps
1. Open a channel in Buzz Desktop using dark theme.
2. Send a message containing a human mention and another containing an
agent mention; both chips should remain aligned with adjacent text on a
20px line.
3. Render a collision-qualified mention in a narrow message width; it
should break into separately decorated fragments exactly like another
wrapping chip, while each fragment follows the 20px prose rhythm.
4. Render a long repository or permalink chip in the same constrained
width; it should retain its roomier 22px fragment spacing.
## Screenshot
The dark-theme production renderer shows the real qualified label (`bob
(npub1hv3…tpuc)`) at an 8rem width. The two lines now paint as visibly
separate rounded fragments rather than one continuous rectangle.

## Validation
At commit `2b063e1b4ade30e11f1616269ad4ba4190366885`:
- Pre-push desktop gates — file-size check, Biome/checks, typecheck, and
6,483 unit tests passed
- `pnpm --dir desktop build` — passed
- Focused Playwright coverage for single-line agent mention, single-line
human mention, wrapped qualified mention including keyboard profile
activation, and timeline mention click — 4 passed
- `git diff --check` — passed
---------
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Rizz <rizz@agents.buzz>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
* fix(acp): integrate the Buzz Pi adapter fork (#7552)
## Summary
PR #7335 worked around missing Pi adapter support by generating a
private Pi launcher and injecting Buzz's standing prompt and skills at
process launch. The Buzz Pi fork now carries the required adapter
extensions, so this removes that launcher and returns prompt
construction to the normal ACP session path while retaining the
base-prompt composition introduced by #7335.
The Pi preset now installs `salman1993/pi-acp` and launches its renamed
`buzz-pi-acp` binary. Buzz adds `-- --skill
<harness-cwd>/.agents/skills` only when launching that binary, sends the
complete composed prompt as the `_meta.systemPrompt` replacement string
on `session/new` only when `initialize.agentInfo.name` is `buzz-pi-acp`,
and sends the scoped title alongside it as `_meta.sessionTitle`. The
fork identity is treated as system-prompt capable regardless of its
reported ACP protocol version, which prevents duplicate legacy
user-message framing. Upstream `pi-acp` does not receive either
fork-specific behavior. Observer transcript projection accepts the
string, `{ replace }`, and `{ append }` metadata forms.
The fork now stores restore metadata in one atomic file per session
under `~/.pi/buzz-pi-acp/sessions/`. This prevents concurrent Buzz
workers from overwriting another session's prompt or title. The fix
landed in
[salman1993/pi-acp#9](https://github.com/salman1993/pi-acp/pull/9).
This supersedes the closed #7508. No agent-configuration rules changed;
this changes the Buzz Pi adapter contract and launch arguments.
### Related issue
#7329
### Testing
Installed fork commit `09cf07e436b8f18e52401558f988f31a15702313` through
the documented Git URL. The installed bundle matched the committed
`dist/index.js` byte for byte and contained the `~/.pi/buzz-pi-acp`
metadata path. The fork's 106 non-skipped tests, typecheck, and lint
pass.
Ran the ignored real-Pi integration test through Buzz's production
session composer. The test exercised the renamed package,
`agentInfo.name`, and the new per-session metadata store. Base, persona,
team, core-memory, huddle, canvas, and skill markers each appeared once
after switching sessions and again after restarting the adapter, while
the other session and Pi's native default prompt were absent.
Added regression coverage proving `buzz-pi-acp` receives fork-specific
system-prompt metadata and managed skills while upstream `pi-acp` does
not. `just ci` passes.
Generated with Codex
---------
Signed-off-by: Salman Mohammed <smohammed@squareup.com>
* feat(git): add default-branch management to relay and CLI (#7562)
Authored by Brain and opened on behalf of Wes (`wesbillman`).
## Summary
Add `buzz repos default-branch get…
mfethe1
added a commit
to mfethe1/buzz
that referenced
this pull request
Sep 14, 2026
* fix(mobile): style inline code with the app mono face (#6631)
## Summary
Inline code on mobile renders as **bold body text on a faint background
wash** — no monospace face, no chip, and it cannot wrap. #5257 diagnosed
this as a missing `highlightBuilder`.
That is no longer the right fix. `gpt_markdown` 1.2.0 deprecates
`highlightBuilder` (removal in 2.0.0), renders inline code as a real
chip, and adds `InlineCodeStyle` for restyling it. The package author
confirmed this on the issue. So this PR is an upgrade — 1.1.6 → 1.2.1 —
plus one theme declaration, rather than the builder the issue originally
asked for.
**Where the style is declared.** `GptMarkdownThemeData` goes in
`AppTheme._buildTheme`, which both `light()` and `dark()` call. That
reaches all four `GptMarkdown` call sites — `message_content`,
`transcript_item_widget`, `token_pill`, `custom_emoji_render` — so the
style is stated once instead of per widget. A widget-level
`inlineCodeStyle` would have covered channel messages only, leaving the
other three on the package's defaults.
**What is declared.** Face, size, ink, chip fill and outline — not the
face alone. A face name on its own leaves the rest on the package's
defaults, which put inline code at 14.1sp beside a fenced block's 13, on
a neutral `onSurface` tint rather than the app's code surface. In dark
that tint is *lighter* than the surface, while every other code surface
in the app is recessed, so the chip read as a different kind of object.
All of it now comes from one `CodeStyle` declaration that the fenced
block reads from too, so the two cannot be edited apart.
**Three adaptations the upgrade requires.** Each was found by running
the gate, not by reading the changelog:
1. **`imageBuilder` widened** to `(context, url, width, height)`. This
is a hard compile error, and it is **not listed in the package's
migration guide**, which states "nothing here stops code compiling".
Worth reporting upstream.
2. **`autolink` now defaults to `true`.** `normalizeBareLinks()` already
rewrites bare URLs into Markdown links before rendering, so both would
run. `message_content` opts out with `autolink: false` to keep current
behaviour exactly. The migration guide argues for dropping the
pre-processor instead — a better fix, but a behavioural change that
belongs in its own PR.
3. **`gpt_markdown.dart` now re-exports `markdown_config.dart`**, making
two direct imports redundant. `flutter analyze` reports `No issues
found!` on 1.1.6 and flags both on 1.2.1, so these warnings are new, not
pre-existing.
**Deliberately out of scope.** The three non-message call sites now
autolink bare URLs, since only `message_content` has a pre-processor to
collide with. Custom inline components (`_MentionMd`, `CustomEmojiMd`,
`_ChannelLinkMd`) could additionally declare `allScopesExceptLinkLabel`
— 1.2.0 offers it as the fix for a `WidgetSpan` chip going blank inside
a link label on iOS — but current behaviour is unchanged without it, so
that stays a separate change.
### Related issue
Fixes #5257
Duplicate scan: searched `gpt_markdown`, `inline code mobile`,
`highlightBuilder` and `InlineCodeStyle` across both PRs and issues. No
open PR touches inline code styling. #6135 (link labels) and #6166 (text
selection) also touch mobile Markdown but address different defects.
### Testing
Full gate, `just ci` — exit 0:
| Stage | Result |
|---|---|
| Rust (33 suites) | 4768 passed, 0 failed |
| Desktop | 5799 passed, 0 failed |
| Mobile | **2011 passed**, 0 failed |
| `flutter analyze` | `No issues found!` |
| Desktop + web build | ok |
Run on the branch with `main` merged in, so these numbers match what CI
builds.
**New regression test** — `renders inline code in the app code style`.
It resolves the `CodeTextSpan` the package tags inline code with, which
carries both the resolved `TextStyle` and the colours the chip behind it
is painted with, so face, size, ink, fill and outline are all asserted
rather than a widget's presence. It is negative-controlled: reverting
only the theme declaration fails it with
```text
Expected: a numeric value within <0.001> of <13.0>
Actual: <14.1>
```
and dropping the declaration entirely falls back to
`packages/gpt_markdown/JetBrainsMono` — so the test measures the real
thing, and it would catch a future regression that silently drops the
theme extension.
The test passes `baseStyle: messageBodyTextStyle`, the style the message
surfaces actually use; the widget's own fallback is the smaller
`bodyMedium`, which would move the expected size.
The test finds paragraphs with `find.byWidgetPredicate((widget) =>
widget is RichText)`, not `find.byType(RichText)`: inline code renders
through `BidiRichText`, a `RichText` subclass, and `byType` matches
exact runtime types.
That is a hazard for any test that reads text back out of a paragraph,
and one landed after this branch was cut:
`message_content_custom_emoji_test.dart` arrived with #6996 and its
`code keeps literal emoji while adjacent known tokens render` case reads
a code span through `find.byType(RichText)`. It passes on `main` and
fails on the merge result, which is what CI builds, so it went red only
once CI was authorized. It now uses the same predicate. The two other
`byType(RichText)` call sites — the rest of that file and
`message_author_meta_test.dart` — were re-run and pass: their content
carries no code span, so the exact type still matches. They were left
alone.
### Screenshots
Rendered through the real `MessageContent` widget with the app's own
fonts loaded, at 390pt wide, 3x DPR. Sample text: ``Set `BUZZ_RELAY_URL`
before launch, then run `just mobile-test` to verify.``
| | Before (1.1.6) | After (1.2.1) |
|---|---|---|
| Light |

|

|
| Dark |

|

|
Before: bold Inter on a flat wash, no chip edge, and `just mobile-test`
breaks across the line with the wash simply ending. After: Geist Mono in
a bordered, rounded chip, and the wrapped fragment gets its own chip on
each line.
---------
Signed-off-by: Tolga Cinisli <tolgacinisli@gmail.com>
Co-authored-by: Tolga Cinisli <tolgacinisli@gmail.com>
* fix(buzz-acp): wake held ACP threads and fence forked sessions (#7340)
## Summary
Adds an independent deadline wakeup so held thread work dispatches after
its 10-second bound even when the relay loop is otherwise quiet. Fences
session ownership by generation so a worker returning after a fork
cannot make an older provider session claimable again.
This follows up on the two post-merge findings from
[#7337](https://github.com/block/buzz/pull/7337#pullrequestreview-5116329341).
### Related issue
Follow-up to #7337.
### Testing
- `cargo test -p buzz-acp`
- `cargo clippy -p buzz-acp --all-targets -- -D warnings`
- Pre-push file-size, differential Rust test, and desktop Tauri gates
No UI changes.
---
**Update Sep 4, 15:35:** Addressed both Codex review findings.
- Queue-cap eviction now prunes orphaned hold deadlines.
- An expired hold stays expired until a worker is successfully claimed.
- Hold timers remain disabled while every worker is busy; worker return
wakes dispatch directly.
- Added regressions for queue eviction and pool exhaustion.
Generated with Codex
---------
Signed-off-by: Salman Mohammed <smohammed@squareup.com>
* fix(agent): route GPT-5+ model-service FQNs to Responses (#7358)
## Summary
Route Databricks Unity Catalog model services to OpenAI Responses when
their service name matches GPT-5 or newer. These models can reject tools
plus reasoning on Chat Completions.
Match only the service component, using the existing family-token
boundaries and a numeric major version. Catalog and schema names cannot
select the protocol. Keep neutral effort capabilities and the full model
ID unchanged; other services still use MLflow Chat Completions.
Keep the Rust and desktop resolvers in sync, add shared boundary cases
and a captured-HTTP regression for completion and summarization, and
update the documented FQN rule.
### Related issue
No duplicate found in searches for “FQN responses” PRs or “astra”
issues. Related: #6918 introduced Unity Catalog discovery.
Originating conversation:
buzz://message?channel=0b881928-a3a6-4c01-b981-8e64268f01ce&id=770949343bc96a9ed88acd90a1b37d358a0efc52c79237d0fdb491ce02b8d4ed
### Testing
No live Databricks inference test. The gateway must accept the full
model-service ID on its OpenAI Responses route; this remains the
integration risk.
The local `just ci` attempt exceeded its five-minute deadline during
`mobile-check`, so the full repository gate was not completed. All
push-hook checks passed.
Generated with Codex
Signed-off-by: Fizz <400e8babadcee6a7f420103f10a2849d84c4a9c71d5bd04f3948c814216648a3@buzz.block.builderlab.xyz>
Co-authored-by: Fizz <400e8babadcee6a7f420103f10a2849d84c4a9c71d5bd04f3948c814216648a3@buzz.block.builderlab.xyz>
* feat(mesh): upgrade to mesh-llm 0.76.0-rc8 and recommend Qwen3.8 27B (#6189)
Upgrades Buzz's mesh-llm dependencies through the released `v0.76.0-rc8`
tag (`2040765d`), including the Qwen3.8 curated recommendation and rc8's
scheduler/runtime improvements.
**Scope note:** the earlier open-relay/unenforced-admission mode has
been removed from this PR at Mic's direction — it is not a product mode
we want. Mesh admission remains roster/allowlist driven, exactly as on
`main`: on a relay with no NIP-43 membership snapshot the mesh runs
self-only. No NIP-11 mode probing, no mode-transition restarts. A future
perimeter/admission strategy for open relays will be designed
separately.
This PR also:
- seeds `BUZZ_AGENT_LLM_TIMEOUT_SECS=660` for mesh agents, above
MeshLLM's 600-second backend timeout;
- makes `desktop-tauri-clippy` lint both default and `mesh-llm` cfg
graphs;
- runs the feature-enabled desktop test suite in CI;
- recommends Qwen3.8 27B Q4_K_M for 64 GB-and-larger machines, then
ladders down through Gemma 4 E4B and Qwen 9B for smaller machines;
- keeps stored shared-compute `auto` translated to MeshLLM's supported
wire model `mesh`.
RC8 verification:
- `just ci` passed locally at
`92ecc7ec933bdd4df804cc9f28a2b51efa5313c5`.
- Pre-push differential gates passed, including both desktop Tauri cfg
graphs and package tests.
- A prior isolated runtime smoke used the RC8 binary's OpenAI endpoint
for a Buzz-shaped system/user/tool/tool-result/final-response loop; all
assertions passed and the isolated process was shut down.
Perf previously measured on M5 Metal, Qwen3.8-27B-Q4_K_M: TTFT 0.22–0.32
s, ~25 tok/s streaming; agent-shaped turns ~1 s to first token after the
first (prefix cache).
---------
Signed-off-by: Michael Neale <michael.neale@gmail.com>
Signed-off-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz>
Signed-off-by: Alessandro Joabar <sandro@squareup.com>
Co-authored-by: Michael Neale <michael.neale@gmail.com>
Co-authored-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz>
Co-authored-by: Mic Neale <305999590+micspiral@users.noreply.github.com>
Co-authored-by: Alessandro Joabar <sandro@squareup.com>
* fix(link-preview): keep composer fetches user-paced (#7211)
**Category:** fix
**User Impact:** Link previews can keep loading while a message is being
composed, while sending still has a finite escape hatch and stalled
network transports cannot occupy preview slots forever.
**Problem:** Native metadata and image deadlines could collapse slow
previews into fallback cards while the user was still composing, and a
shared image-host cooldown made pasted batches fail inconsistently after
one rate limit. **Solution:** Keep preview resolution user-paced with no
aggregate request deadline, bound transport inactivity (15s DNS/connect,
30s idle read), serialize image requests by host, and allow at most one
server-directed cooldown wait of up to 30s across an image fetch and its
redirects. The existing bounded post-Send preparation and immediate Skip
paths remain unchanged.
<details>
<summary>File changes</summary>
**desktop/src-tauri/src/commands/link_preview.rs**
Removes aggregate native deadlines so composer metadata work can
complete at the user's pace, while retaining DNS/connect/idle-read
liveness bounds. Adds bounded host-paced image request coordination that
releases its gate during cooldown, waits inline at most once for at most
30 seconds, and cannot renew that wait through redirects or the outer
transient retry. Same-host image and favicon requests remain
deliberately serialized to align with host rate limits.
**desktop/src-tauri/src/commands/link_preview_rate_limit.rs**
Adds a fixed-size striped host gate so concurrent image requests are
serialized without retaining an unbounded attacker-controlled hostname
map.
**desktop/src-tauri/src/commands/link_preview_tests.rs**
Moves native link-preview tests into a dedicated module and covers the
user-paced metadata contract, bounded one-shot cooldown behavior, and
gate release while a rate-limited request sleeps—including a different
host sharing the same bounded gate stripe.
**desktop/src-tauri/src/commands/link_preview_youtube.rs**
Removes the thumbnail fetch deadline so YouTube previews follow the same
composer lifecycle contract while using the shared bounded transport.
**desktop/src/shared/lib/useResolvedLinkPreviews.ts**
Adds development-only metadata outcome diagnostics with elapsed time and
image/fallback state, without logging encoded image payloads.
</details>
### Reproduction steps
1. Open the desktop composer and paste several GitHub pull request links
whose OpenGraph images share a host.
2. Observe that image requests are paced by host instead of racing, and
slow-but-progressing preview work remains pending rather than
immediately becoming a completed favicon fallback.
3. Send while preview work is still pending and confirm **Preparing link
preview** remains bounded by the existing post-Send budget.
4. Use **Skip** during preparation and confirm the message proceeds
immediately.
5. In a development build, inspect the console for `[link-preview]
metadata fetch completed` diagnostics containing elapsed time and image
state without base64 payloads.
### Related issue
N/A — scoped from the linked Buzz implementation room.
### Testing
At current head `dfb394aafbee537e9ffb04ad3732d08f65f30b8e`:
- Production-bound paused-time metadata regression passed through
`fetch_link_preview_metadata`; restoring the former 10-second aggregate
wrapper makes it fail at the pending assertion.
- Native link-preview module: 19/19 passed.
- `cargo check --manifest-path desktop/src-tauri/Cargo.toml` passed.
- Rust formatting and `git diff --check` passed.
- Pre-push `push-head-scope`, org safety, differential file-size,
branch-skew, and `desktop-tauri-checks` hooks passed.
At prior head `59e2dcf167b15c7a3e637ad2608008b7f9cef5f3`:
- Full Tauri Rust suite: 3,056 passed, 19 ignored; integration crates 7
+ 3 passed.
- Focused native link-preview suite: 26/26 passed.
- The pasted multi-preview workflow was exercised in the desktop app and
confirmed improved before draft publication.
---------
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@users.noreply.github.com>
* fix(acp): pace targeted overflow recovery on consumer capacity (#7325)
🤖
## Summary
When a Buzz agent falls behind on incoming messages, its connection can
make the backlog worse while trying to recover. The connection buffers
messages from the relay server until the agent is ready to process them;
if that buffer overflows, recovery previously requested history for
**every subscribed channel** and paused socket reads while sending those
requests. That adds traffic to an already overloaded connection. This
change requests history only for affected subscriptions, once the code
consuming those messages has room, with at least five seconds between
attempts.
The recovery path now:
- Combines repeated losses into one pending recovery per affected
subscription, keeping the oldest dropped timestamp so replay starts
early enough.
- Waits until at least half the consumer queue is free and the relay's
existing rate-limit delay has expired. The queue wakes recovery when
space becomes available; recovery does not periodically sample capacity
or hold queue space away from live messages.
- Attempts one subscription at a time, choosing the least recently
attempted so a busy channel cannot crowd out other channels or
membership notifications. The five-second delay starts when an attempt
finishes, including a failed write; failed writes leave recovery
pending.
Recovery is paced by available capacity, not by how often messages are
lost. This is not a larger buffer or a cutoff that abandons recovery.
Subscription identifiers, message filters, replay timestamp overlap and
duplicate filtering are unchanged; no downstream agent changes are
required.
This targets a reproducible overload **amplifier**, not every cause of
overload or every catch-up limitation. The initial live overload's cause
has not been established. Recovery remains best effort: a successful
request write is not proof of delivery, and existing history/retention
limits, bounded duplicate tracking and replay limitations still apply.
There is no exactly-once or complete catch-up guarantee. A stalled write
can still pause socket reads for the existing ten-second timeout; the
pacing bound does not cover initial subscriptions, reconnects or other
retry paths.
### Related issue
Closest related: #5014 (channel re-subscription); also #6661 (membership
reconciliation) and #6090 (relay backpressure gap signaling). This
addresses local overflow recovery scheduling, not those separate
mechanisms.
### Testing
Recorded offline comparisons against the previous behavior, with the
final implementation at `8000636f3073167c5a5107bb179c7d91160f1729`:
| Same fixture: 18 subscriptions, three overload rounds | Before | After
|
| --- | --- | --- |
| Recovery history requests | 108 | 3 |
| Ping-response delay | About 4.6 seconds | Below the measurement's 1 ms
resolution |
A separate bounded-history fixture delivered all 320 events plus
subsequent live traffic in **both** versions. Regression coverage
exercises the real socket-handling task, including intermittent consumer
capacity, fairness, failed writes and cancellation of capacity waits
before live delivery. These are synthetic results, not production
throughput measurements or evidence of a deployed cure.
The full local `RUST_TEST_THREADS=4 just ci` run passed on September 4,
2026. Earlier unsuccessful local runs remain part of the validation
history. The [recorded validation evidence and separate desktop
follow-up](https://github.com/block/buzz/pull/7325#issuecomment-5540592398)
preserve the original desktop mock-history scroll failure, its passing
rerun and the remaining investigation. That desktop path does not run
the agent connection code; neither this repair nor the passing rerun
fixes the observed scroll problem.
---------
Signed-off-by: Logan Johnson <loganj@squareup.com>
* fix(mobile): render push notification sender identity as npub (#7494)
🤖
## Summary
When an iOS push notification comes from someone the app has no cached
name for, the notification title showed the first characters of the
sender's raw public key — for example `aa4fc866…`. That fragment is
unreadable and doesn't match how the same person appears anywhere else
in Buzz. This PR changes that title to the compact form of the sender's
npub (npub is the human-readable encoding of a Nostr public key): first
8 and last 4 characters — for example `npub14f8…9nsy`, the same identity
shape used across the desktop and mobile apps.
- Unnamed senders: raw hex fragment → compact npub.
- Named senders: unchanged — a sender the app has a display name for
still titles the notification with that name.
- Unverifiable sender identities (malformed keys, or lookalike strings
that are not literal 64-hex-digit keys) now render a neutral "Someone"
instead of partial raw key material.
- Everything else about the notification is unchanged: body text,
subtitle, thread matching and grouping, deep-link navigation, thread
identifiers, and the internal hex public key the resolver matches on.
The native iOS notification-service package (`BuzzPushKit`) gains a
minimal in-house bech32 codec (bech32 is the checksummed string encoding
npubs use) — checksum-validated, 32-byte keys only, and no new external
dependency. The hex input branch accepts exactly a 64 ASCII hex digit
key before any parsing, so strings that merely parse like hex (for
example a run of `+a` pairs) cannot become a displayed identity; this is
input validation for presentation. Event signature verification is
untouched.
### Related issue
Fixes: N/A. Searched existing issues/PRs for push-notification npub
identity — closest related: none found.
### Testing
At head `3e3f2813b8864b76257ccb50dea3a4b31fa4de0d` (base
`44316ff72f5f7de014c66b01cbf534298a70c249`; 4 files, +321/−4):
- CI `Mobile Swift` lane, at this exact head — all passed: `swift test`
(73 tests, 0 failures), the SwiftPM debug and release builds of
`mobile/ios/BuzzPushKit`, and the unsigned iOS release build.
- Test coverage: npub encoding cross-checked against independent
nostr-rs/NIP-19 vectors; rejection of bad checksums, mixed case, wrong
lengths, invalid alphabet, padding, and non-32-byte payloads; resolver
boundary matrix — hex/npub/invalid sender keys render compact npub or
"Someone" while body, subtitle, sender key, and thread identifier pass
through; named senders keep cached display names.
### Task provenance
Buzz channel: `1f0e4a3d-7e01-4efe-bb16-843b357f85c9`
Task:
buzz://message?channel=1f0e4a3d-7e01-4efe-bb16-843b357f85c9&id=86b34eb4bd84a1472419e9af22636c011c0fe273e3c196f967d7a36996e149b6
---------
Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
* fix(desktop): shared npub identity foundation (canonicalNpub, PubKey gate, strict parser) (#7488)
🤖
## Summary
Identity keys in the desktop app are displayed as raw 64-character hex.
A person's key shows up as something like `953d3363…` — unreadable,
impossible to recognize as the same identity on another screen, and a
hazard when copied by hand. Nostr (the protocol Buzz runs on) has a
human-readable spelling for identity keys — the `npub1…` form — but the
desktop app did not use it consistently.
This is the foundation of the desktop npub changes: it adds the shared
pieces every identity surface builds on, and two follow-up slices stack
directly on this branch — #7489 converts the identity controls (profile,
settings, allowlist, workflow key fields) and #7495 converts the
everyday display surfaces (mentions, member lists, sidebar, and other
name fallbacks).
After this change:
- The shared identity widget shows the compact npub form —
`npub1j57...fjmv` — instead of a hex prefix, everywhere it renders (for
example the owned-agent public-key row on a profile). Copying it puts
the full npub on the clipboard.
- Copy is a real interaction, verified end-to-end: both popover variants
put the exact canonical npub on the actual clipboard — never the raw hex
the popover also lists, never a truncation — and a portaled popover's
clicks no longer steal focus from the new-DM To-field mid-copy. Pointer
copy, a natural Space-then-Enter path, and inner/outer Escape are
covered.
- Anything that isn't a valid identity key fails neutrally: short or
corrupt values — including degenerate values that technically encode to
a checksum-valid npub but aren't real identity keys — show "Unavailable"
with no copy button, instead of a misleading value.
- Both valid npub spellings display: all-lowercase `npub1…` and
all-uppercase `NPUB1…` (Bech32, npub's encoding, permits either casing)
both render the same canonical lowercase npub. Mixed case is rejected by
the display path as written — `canonicalNpub` and the widget don't
case-normalize input — while input parsing (`parsePubkeyInput`) keeps
its trim-and-lowercase normalization and accepts mixed-case npubs; both
paths require the decoded payload to be exactly a 64-character identity
key.
- Identity-key input is strict on payload: an npub whose decoded payload
isn't exactly a 64-character identity key is rejected, matching the
validation the app's Rust side already applies to agent allowlists.
Intentional scope boundary: only surfaces that render through the shared
widget change here. Outer profile copy, settings identity cards, the
respond-to allowlist, and workflow key fields still show hex — they move
to npub in the controls follow-up (#7489). Nothing else changes identity
representation: display names, private keys, event IDs, and the hex the
app stores, sends, and matches internally are untouched; only the
user-facing spelling of an identity key changes.
## Details
- `desktop/src/shared/lib/pubkey.ts` — `canonicalNpub()`: strict
canonical full-npub helper (64-char hex in any case, or a
checksum-validated npub, returns the canonical npub; anything else
returns `null`); `truncateNpub()`: the compact display form; existing
exports unchanged.
- `desktop/src/shared/ui/PubKey.tsx` — the shared widget's identity gate
validates through `canonicalNpub`; the popover copies the npub only.
- `desktop/src/shared/lib/nostrUtils.ts` — `parsePubkeyInput` rejects
npubs whose payload is not exactly a 64-character identity key.
- `desktop/src/features/messages/ui/NewMessageScreen.tsx` — the To-field
focuses its search input only for clicks that land inside the field
itself, so portaled recipient popovers keep their focus while open (a
popover click previously dismissed it mid-copy).
- Unit suites cover the helper, widget, and parser (including the
degenerate-encode and uppercase regressions); the e2e specs that render
these rows assert the npub display.
### Related issue
- Fixes: N/A. Searched existing issues/PRs for npub identity display —
no existing match.
- Stack: #7489 is based on this branch and builds on these primitives;
it does not stand alone on main.
### Testing
At head `b3310c248` (base: main `44316ff72`; 12 files, +440/−39):
- Focused unit suites (pubkey, PubKey, parsePubkeyInput): 20/20 green;
mutation-checked — removing the decoded-length predicate fails the
short/empty checksum-valid-npub assertions in `canonicalNpub` and the
widget, and a wrong-identity clipboard value fails the new copy
assertions.
- `pnpm typecheck` and `pnpm check`: pass; full desktop unit suite
6459/6459 at this exact head.
- Targeted e2e at this exact head: 8/8 across the two specs that own the
clipboard flows — `agent-access-warning.spec.ts` (compact variant,
agent-access owner hint) and `pubkey-display-screenshots.spec.ts` (full
variant, new-DM recipient verification: pointer copy, popover surviving
the copy, inner/outer Escape, Space-then-Enter).
- No Rust-side or build files change in this PR, so those results are
unaffected.
### Task provenance
Buzz channel: `1f0e4a3d-7e01-4efe-bb16-843b357f85c9`
Task:
buzz://message?channel=1f0e4a3d-7e01-4efe-bb16-843b357f85c9&id=86b34eb4bd84a1472419e9af22636c011c0fe273e3c196f967d7a36996e149b6
---------
Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
* test(mobile): match SDK font filename case
Signed-off-by: Michael Feth <mfethe1@gmail.com>
* fix(desktop): npub identity displays for mention, member, and workflow surfaces (#7495)
🤖
## Summary
Every Buzz account is identified by a long public key. Before this
change, when someone had no display name, surfaces fell back to
inconsistent labels — mostly raw hex fragments like `abcd1234…wxyz`,
sometimes a generic role label with no key — so the same person looked
different from surface to surface, and nothing looked like an npub
address. This PR applies the npub identity foundation from #7488 to the
everyday surfaces: a person without a display name now falls back to the
same compact npub everywhere — `npub1xxxx…yyyy`, the human-readable
spelling of their public key (first 8 + last 4 characters of the full
npub) — across messages and mentions, reactions, huddles, member and
participant lists, the sidebar and channel activity, search, projects,
tray, notifications, and workflow surfaces.
- **Mentions and messages**: key-only mention chips render the compact
npub. Pasting a copied mention back still re-binds it byte-exactly to
the identity it declares, for both the new npub chips and legacy
hex-truncated chips copied by older clients — wrong, missing, or
tampered key qualification is rejected instead of silently degrading to
plain text.
- **Reactions and huddles**: huddle reaction events and the huddle
roster/participants render the compact npub for unnamed participants;
workflow reaction triggers describe authors with the same form.
- **Members and sidebar**: channel and community member lists,
add-member results and invites, the members sidebar, the
channel-activity popover, search, projects (assignees/reviewers/PR
panels), the tray menu, and desktop notifications all fall back to the
compact npub; titles and aria labels keep the machine-readable full
labels.
- **Profile labels**: panel/popover display names and owner handles fall
back to the compact npub (never raw hex) when there is no name;
linked-event (nevent) message metadata shows the npub-shaped author
fallback while the event lookup and event IDs are unchanged.
- **Workflows**: author-picker secondary labels, step destination keys,
and trigger-author references render compact npubs; event and blob IDs
keep their existing hex compacts (they are not identities).
- **Avatars stay distinct**: fallback avatars for key-only identities
derive initials from the key's tail, so prefixed role labels like
"Participant npub1…" no longer collapse every unnamed participant onto
the same initials; people with names keep their name initials.
Preserved exactly: display names and distinct avatars, internal hex keys
(storage/API forms unchanged), clipboard identity roundtrips, event/blob
ID compaction, private keys (no nsec path is touched), and nevent link
handling.
Scope: this PR changes what identity labels **display**, not identity
controls — profile/settings copy controls, the respond-to allowlist,
workflow key fields, and agent dialogs are the sibling slice #7489, and
the shared primitives (`canonicalNpub`, `truncateNpub`, the `<PubKey>`
gate, strict input parsing) come from the foundation #7488.
### Related issue
- Fixes: N/A. Searched existing issues/PRs for duplicates — none found;
the related work is the npub identity stack this slice belongs to.
- Base/dependency: stacks on #7488 (foundation) — this PR does not stand
alone on main.
- #7489 is a sibling slice on the same #7488 base
(profile/agent/workflow controls), not a dependency: this PR does not
require #7489, and #7489 does not require this PR — both only require
#7488.
### Testing
At exact head `4763cbeae1dd521309755e6d61f657324cb98667` (base:
`fix/desktop-npub-identity-d1a` @
`5f3a4a8111998c8aa41ad77cf66992bd1c85343c`; 71 files, +656/−189 —
production +277/−136, test support +379/−53):
- At this head: targeted `mentions.spec.ts` (1/1), the e2e build,
typecheck, and biome — green.
- 9 changed/related unit files: 100/100 green; typecheck, e2e build,
biome, and px text/truncation checks clean; huddle-roster focused run
green; channel-activity e2e 11/11; mutation checks confirm the fallback
wiring (removing it collapses shared initials and drops fallback rows).
- Known pre-existing local e2e failures, unchanged by this PR and
reproduced identically at the upstream merge-base: huddle-transcription
voice-menu attribution (25 pass / 1 fail) and the
`workflow-local-controls` 438px caret drift. Not claimed green locally.
- Update at head `236af9e6137386737e84d3a474d6bc808a704c50` (test-only
follow-ups `1143af345` + `236af9e6`): the `workflow-local-controls`
races were fixed in the test drivers, and the 438px diff was shown to be
a stale Darwin snapshot baseline (name-row enable switch already absent
and `message_posted` already MessageSquare at recording commit
`9390e11c9`) and refreshed — the focused screenshot test, including
keyboard/caret assertions, now passes locally (twice). The full spec was
not rerun after the snapshot refresh; the huddle-transcription item
above is unchanged.
Label/copy text changes are asserted by the e2e specs (`mentions`,
`mention-recipients`, `pubkey-display-screenshots`,
`huddle-transcription`, `channel-activity-popover`,
`workflow-local-controls`) rather than new screenshots; the screenshot
spec pins the compact npub text forms.
### Task provenance
Buzz channel: `1f0e4a3d-7e01-4efe-bb16-843b357f85c9`
Task:
buzz://message?channel=1f0e4a3d-7e01-4efe-bb16-843b357f85c9&id=86b34eb4bd84a1472419e9af22636c011c0fe273e3c196f967d7a36996e149b6
---------
Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
* fix(desktop): npub identity controls across profile, agents, and workflows (#7489)
🤖
## Summary
Building on #7488's npub foundation, this PR finishes the identity
display change for the controls where you actually manage people and
keys: profile, settings, agent access, and workflows. Everywhere in
these surfaces, an identity key shows — and copies — as its canonical
npub (npub is the human-readable encoding of a Nostr public key: the
compact `npub1j57...fjmv` form where space is tight, the full npub where
the whole key matters), and accepts npub as input.
After this change:
- Profile panel: the public-key row and the managed-by / declared-owner
copies show the full npub. If a key can't be encoded, you see
"Unavailable" with no copy button — never a raw or partial key.
- Settings: the identity card shows and copies the npub. The
hosted-communities account identity derives from the bound key
(`pubkey_hex`) — the same authority as the mismatch gate and hosted
operations — so the display can never disagree with what the app acts
on; an unusable hex falls back to a neutral label instead of rendering
the unverified server npub. The connected claim and a community's
Connect action require that same usable bound key to match the local one
— with no usable binding the card cannot claim connected or start
Connect, while the community list, linking, and delete/rebind recovery
stay available.
- Hosted create/onboarding: the account and device identity rows in the
create flow and owner onboarding derive from the same authoritative
fields (bound key / local key), with the same neutral fallback;
readiness requires a usable bound key that matches the local one.
- Respond-to allowlist (controls who may respond to an agent): entries
can be typed or pasted as hex or npub; both spellings of the same key
are recognized as one entry and dedupe. Search results, chips, and
remove buttons use the compact npub.
- Workflow key fields: to/from keys display as npubs in the form and
save back as canonical hex. Templates like `{{trigger.author}}`, roles,
and free text pass through untouched; placeholders accept both
spellings.
- Recipient and agent dialogs: the verify popover is npub-only (the
raw-hex line is gone); denied-membership screens never show a raw key.
- The Rust-side truncated display name (used for native surfaces) shows
the same compact npub, so those surfaces match the web UI.
Internal representation is unchanged: keys are still stored, sent, and
matched as canonical 64-character hex — npub is a display and input
spelling, normalized to hex at the boundary, so existing data and
integrations keep working. Bound-key usability and comparison use one
normalized form (trimmed, lowercased, 64 hex characters; npub rejected),
so padded or mixed-case spellings of the same key match. Display names,
private keys, and event IDs are untouched.
## Details
- `respondToAllowlist` / `RespondToField`: npub entries normalize to
canonical hex; cross-form dedupe; compact npub in rows and chips;
direct-add accepts npub and stores canonical hex.
- `workflowFormTypes` / `WorkflowStepCard`: hex → npub for display, npub
→ canonical hex on save; templates, roles, and free text pass through in
both directions (roundtrip-tested).
- `UserProfilePanelFields`, `ProfileSettingsCard`,
`HostedCommunitiesSettingsCard`, `MembershipDenied`,
`SelectedRecipientChip`, `AddAgentToChannelDialog`: npub display and
copy; invalid keys → "Unavailable" with no copy; hosted identity rows
derive from the bound `pubkey_hex` (create/onboarding rows from the
bound and local keys), never the unverified server npub;
connected/readiness/Connect gates use the same usable-bound-key
predicate, and the settings Connect invocation callback re-checks it
before starting.
- `src-tauri/src/commands/identity.rs`: `truncated_display_name`
compacts to the first 8 + last 4 characters of the npub (above a 12-char
threshold), mirroring `truncateNpub`.
- e2e: profile key rows and clipboard polls assert npub forms and
raw-hex suppression; the display-screenshots spec pins the npub-only
popover; hosted specs drive the real settings card, create flow, and
onboarding rows through their real providers, and the unlinked/npub-only
identity cases assert no connected claim and no Connect action.
### Related issue
- Fixes: N/A. No separate issue; the related work is the stack below.
- Stack: builds on #7488 (shared npub foundation), now merged; this PR
is rebased onto main and stands on its own.
### Testing
At head `303c90ffa` (base: main `bfc38485`; 24 files, +1125/−146):
- Focused unit suites (respondToAllowlist, workflowFormTypes,
hostedCommunityApi bound-key helpers) green; mutation-checked — dropping
allowlist canonicalization fails the dedupe case, and dropping bound-key
normalization fails the npub-in-hex and padded same-key cases.
- Full desktop unit suite 6,477/6,477, `desktop-typecheck`,
`desktop-check` (formatting fixed narrowly with `biome check --write` on
the touched files only), and a fresh E2E build at the current head; the
add-community + hosted-communities-settings specs 18/18 and onboarding
integration 69/69 on a fresh dedicated port, with focused new-case runs
4+4 covering padded same-key (ready, Connect kept — no false rebind) and
npub-in-hex (neutral label, recovery, no Connect) across the settings
card, create flow, and first-community onboarding, plus the
unlinked-account settings regression asserting Connect cannot occur.
- `cargo fmt`/clippy (both feature sets) and `cargo test identity` (71
pass) passed at the earlier full-change head; since then, the only
production changes in this PR's delta are the hosted identity display
authority and its fail-closed bound-key gating/normalization above
(base-side fixes carry #7488's receipts) — every other change is
test-only.
### Task provenance
Buzz channel: `1f0e4a3d-7e01-4efe-bb16-843b357f85c9`
Task:
buzz://message?channel=1f0e4a3d-7e01-4efe-bb16-843b357f85c9&id=86b34eb4bd84a1472419e9af22636c011c0fe273e3c196f967d7a36996e149b6
---------
Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
* fix(mobile): standardize public-key identity display on npub (#7493)
🤖
## Summary
In the mobile app, anyone who hasn't set a display name shows up as a
raw 64-character hex key (e.g. `3a5d4f9c…`) — unreadable, and
unrecognizable as the same identity across screens. Profile and Settings
also let you copy that raw hex. Nostr public keys have a standard
readable form — `npub1…`, the same encoding other Nostr apps and our
desktop app already display. This PR makes every mobile identity surface
render npub instead:
- **Unnamed people everywhere** — message and thread authors, reactions,
typing indicators, member lists, channel details, DM headers and tiles,
inbox, search, forum cards, Pulse notes and reply context, mention
suggestions, and invite rows — now show a compact npub label: first 8 +
last 4 characters of the full npub joined by an ellipsis
(`npub1abcd…wxyz`), the same truncation desktop uses. Previously these
showed truncated raw hex.
- **DM fallback avatars and blank names** — 1:1 DM tiles and headers key
their fallback avatar to the same non-self counterpart the label names,
including self-first participant order; a self-DM keeps its
hex-key-derived initial. Blank or whitespace-only display names fall
back to the compact npub instead of rendering empty, while nonblank
authored names render verbatim (padding included).
- **Profile sheet → "Copy public key"** now copies the full canonical
npub — never raw hex. When the identity string isn't a valid public key,
the copy tile is disabled, so a malformed key never reaches the
clipboard.
- **Settings → Identity (pubkey)** displays and copies the full npub; an
invalid identity reads "Identity unavailable" with copy disabled.
- **Invalid identities never leak truncated raw hex** into the UI
anywhere — they render a neutral "Unknown identity" label.
- **Unchanged on purpose:** display names and verified handles (NIP-05 —
the `name@domain` badge) still render as before. Unnamed avatars keep
distinct per-key initials, derived from the underlying hex key rather
than the npub — otherwise every unnamed key would render the same "N"
initial. Event IDs are not public keys, so they keep their hex
truncation (in Pulse's "Replying to", the parent author shows npub while
an event-id fallback still shows hex). The nevent share link, private
keys, and internal hex storage are untouched. Inputs that accept a key
(invite/member entry) accept both hex and npub and keep working in hex
internally.
### Related issue
N/A. Searched open issues/PRs for npub identity display on mobile —
closest related: none found. Desktop's parallel npub standardization
lives in the stacked desktop PRs (#7488 foundation, #7489 controls,
#7495 display surfaces); this is the independent mobile slice (based
directly on `main`, not on those branches).
### Testing
At exact head `5a620e420a1fd57d9d8011ac26434eed32fcf765` (base: `main`
`44316ff72`; 40 files, +1,345/−154):
- Full mobile suite: 2,098 tests passing (`cd mobile && flutter test`);
`flutter analyze` clean; `dart format --set-exit-if-changed .` clean —
the same checks CI runs.
- Widget/unit coverage at production seams: compact labels and hex-keyed
avatar initials for DM headers/tiles, member rows, mention suggestions,
and Pulse reply context; DM fallback avatars keyed to the labeled
counterpart (self-first order and self-DMs); blank/whitespace
display-name npub fallback with nonblank authored labels verbatim,
including the Activity inbox sender and profile-sheet heading (each with
its own empty/whitespace production-seam regression); full-npub copy and
disabled-copy semantics in profile and settings; invalid-key
suppression; and hex↔npub input round-trips.
Verified via unit and widget tests — no device/simulator validation is
claimed.
### Task provenance
Buzz channel: `1f0e4a3d-7e01-4efe-bb16-843b357f85c9`
Task:
buzz://message?channel=1f0e4a3d-7e01-4efe-bb16-843b357f85c9&id=86b34eb4bd84a1472419e9af22636c011c0fe273e3c196f967d7a36996e149b6
---------
Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
* test(mobile): align fork identity fixtures with upstream npub labels
Upstream #7488/#7493 made the compact npub the canonical identity label
and made shortPubkey return 'Unknown identity' for non-key strings. Three
fork tests still expected raw-hex truncation or accepted invalid keys:
update fixtures/expectations to the npub contract. Behavior under test
(plain-text actor labels, digest author naming) is unchanged.
Signed-off-by: Michael Feth <mfethe1@gmail.com>
* fix(desktop): order unnamed roster members by full canonical npub (#7503)
🤖
## Summary
- Channel members appear in the Members sidebar. A member who has never
set a display name is listed under an abbreviated form of their public
key (npub), and the sidebar previously sorted those unnamed members by
that short label. Short labels are not unique — different keys can share
one — so the order of unnamed members could look arbitrary or unstable.
Unnamed members now sort by their full public key, so the order is
deterministic.
- When two members display the same name, the previous tiebreak was
membership order (who joined first), which is not visible to a reader
and can shift as roster data loads in. The tiebreak is now the full
public key, so identical display names always land in the same order.
- Nothing gets noisier on screen: the full key is used only for sorting,
and the sidebar still shows the compact abbreviated form. Priorities are
unchanged — authored (custom) names still outrank fallback labels, and
role/current-user grouping still applies.
- Scope is the desktop app's Members sidebar and member management: the
two existing sort comparators. Mobile and other lists in the app are
untouched.
### Related issue
Based on #7495 (introduced the abbreviated npub labels this follows up
on). The original five presentation PRs remain independently reviewable.
No closer duplicate found.
### Testing
- 6469 desktop unit tests, typecheck, and check pass.
- The 3 existing consumer-seam E2E tests still pass; a new E2E test
asserts the sidebar lists unnamed members in full-key order, with
fixture members deliberately inserted in the opposite order so incoming
membership order cannot mask the sort.
- Negative check: reverting only this change makes the new ordering
assertion fail, so it genuinely binds the new sort.
- CI has not run on this PR yet.
Buzz provenance: channel 1f0e4a3d-7e01-4efe-bb16-843b357f85c9 / task
340c3de9b27dbedb8453c0c7652220f9080d30fcc70a7c4f6e27fdd4fa378056
---------
Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
* fix(desktop): require a Codex adapter with Astra support (#7427)
## Summary
Buzz considers codex-acp 1.6.2 current because the supported adapter
floor is still 1.1.7. That adapter bundles Codex 0.148.0, so updating a
separate Codex CLI to 0.153.4 leaves managed agents on the older runtime
and unable to use GPT-6 Astra.
Raise the supported adapter floor to the published 1.10.0 release, which
depends on `@openai/codex ^0.153.3`. Existing discovery and installation
code then classifies older adapters as outdated and offers the managed
reinstall path. Update the availability and install-plan regressions to
cover the observed 1.6.2 installation and the new minimum.
This follows the existing version-floor policy. It does not
automatically update a running installation: the user must complete
Buzz’s offered adapter upgrade. Future upstream compatibility changes
may require another floor update.
### Related issue
No exact duplicate found in searches for Astra, CODEX_PATH, bundled
Codex, outdated runtime, and codex-acp 1.10. Related: #3097 raised the
older floor to 1.1.7 (already present on main); #2422 covers lost error
details for runtime mismatches. Neither resolves this version gap.
Originating conversation:
buzz://message?channel=3286cd76-f83e-4c7d-8317-10a16580744d&id=8b79a73078217222b870fff144c27e7d27bcd5a67c966869c18fe726db716898
### Testing
- Isolated npm install of codex-acp 1.10.0 resolved bundled Codex
0.153.4, with no CODEX_PATH override.
- Live macOS ACP probe: initialize protocol v1 → session/new → select
gpt-6-astra[medium] → prompt. Received `OK` and `stopReason: end_turn`;
usage metadata confirms gpt-6-astra.
- Existing adapter 1.6.2 initialized but advertised no Astra model in
the same probe.
- Desktop Rust formatting and `git diff --check` pass.
- `just desktop-tauri-test`: 3,266 passed, 20 ignored, zero failures
across the Desktop workspace and integration tests.
- Workspace and Desktop Clippy, frontend static checks, and `just
file-size-check` pass.
- Repository `just ci`: still running the remaining
mobile/build/workspace-test stages.
The installed Buzz app and managed adapter were not replaced or
restarted. The live check validates the new adapter/runtime path; a
complete packaged Desktop upgrade workflow remains untested.
Signed-off-by: Stephen DeLorme <stephen@d.elor.me>
* fix(buzz-acp): report missing models without retrying (#7538)
## Summary
When an agent reports model-not-found, Buzz retries the unavailable
model and delays the failure reply until retries are exhausted. Stop
retrying this error and immediately post a threaded recovery notice. The
notice tells users to select a different model in agent settings, save,
restart the agent to apply the configuration, and re-send their request.
This adds one error-handling branch and regression coverage in
`buzz-acp`. It matches `-32002` errors containing `model not found`.
Other resource-not-found errors, such as stale sessions, retain the
existing retry behavior. Detailed error events remain available for
diagnosis. The existing restart policy is unchanged.
### Related issue
None found in existing issue/PR searches for model-not-found recovery.
### Testing
Playwright captured and visually checked the thread UI with seeded
conversation data and the exact recovery text. The check opens the
request's thread, confirms no reply before the failure, injects the
notice, and verifies the full text is visible. [Before/after
screenshots](https://github.com/block/buzz/pull/7538#issuecomment-5608196506)
show the corrected save-and-restart instructions. These are local test
captures, not a deployed provider recovery flow.
Generated with Codex
---------
Signed-off-by: Diem Nguyen <diem@squareup.com>
* [lenny] fix(deps): sync Cargo.lock with merged Cargo.toml manifests
The upstream merge combined new dependencies from both sides but left
Cargo.lock at its pre-merge state. Docker builds run with --locked and
reject a lockfile that does not match the manifests:
'the lock file needs to be updated but --locked was passed'
Adds only the union of both sides' new deps (buzz-acp: async-trait,
rusqlite, tempfile; buzz-persona: url). No version changes.
* [lenny] fix(mobile): dart format thread_summarize_action_test
'dart format' collapses the _message(...) call onto one line (fits in
80 cols). Fixes the mobile format CI failure.
Signed-off-by: Michael Feth <mfethe1@gmail.com>
* [lenny] fix(deps): bump rustls 0.23.42 -> 0.23.45 for RUSTSEC-2026-0285
TLS 1.3 handshake messages were accepted across encryption level
boundaries; patched in >= 0.23.45. Lockfile-only bump (rustls,
rustls-webpki 0.103.15, aws-lc-rs 1.18.1, aws-lc-sys 0.45.0).
Signed-off-by: Michael Feth <mfethe1@gmail.com>
* test(desktop): scope e2e text locators past the timeline aria-live region
The sr-only TimelineAnnouncementRegion echoes the latest message text,
so bare getByText(...) locators resolve to 2 elements (strict mode
violation) whenever the announcement matches the asserted text.
thread-unread 15-mark-read-unread-menu-single-toggle failed 3/3 on
runs 34877681776, 34877661550, 34666787614 and product/main with
'getByText("Toggle me") resolved to 2 elements'; same pattern in
thread-reply-anchor-roleplay 01/03/04 ('Nora: adding context',
'Brain: Check the anchor' ...). Scope the locators to the timeline /
thread panel so they resolve uniquely.
Local: pnpm build:e2e + playwright smoke on both specs = 17 passed.
Signed-off-by: Lenny <lenny@buzzagents.dev>
---------
Signed-off-by: Tolga Cinisli <tolgacinisli@gmail.com>
Signed-off-by: Salman Mohammed <smohammed@squareup.com>
Signed-off-by: Fizz <400e8babadcee6a7f420103f10a2849d84c4a9c71d5bd04f3948c814216648a3@buzz.block.builderlab.xyz>
Signed-off-by: Michael Neale <michael.neale@gmail.com>
Signed-off-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz>
Signed-off-by: Alessandro Joabar <sandro@squareup.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Logan Johnson <loganj@squareup.com>
Signed-off-by: Michael Feth <mfethe1@gmail.com>
Signed-off-by: Stephen DeLorme <stephen@d.elor.me>
Signed-off-by: Diem Nguyen <diem@squareup.com>
Signed-off-by: Lenny <lenny@buzzagents.dev>
Co-authored-by: TolgaCinisli <tolga.cinisli@photier.com>
Co-authored-by: Tolga Cinisli <tolgacinisli@gmail.com>
Co-authored-by: Salman Mohammed <smohammed@squareup.com>
Co-authored-by: Bradley Axen <baxen@squareup.com>
Co-authored-by: Fizz <400e8babadcee6a7f420103f10a2849d84c4a9c71d5bd04f3948c814216648a3@buzz.block.builderlab.xyz>
Co-authored-by: Mic Neale <mic@spiral.xyz>
Co-authored-by: Michael Neale <michael.neale@gmail.com>
Co-authored-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz>
Co-authored-by: Mic Neale <305999590+micspiral@users.noreply.github.com>
Co-authored-by: Alessandro Joabar <sandro@squareup.com>
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@users.noreply.github.com>
Co-authored-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Co-authored-by: Stephen DeLorme <stephen@d.elor.me>
Co-authored-by: Diem Nguyen <ngthuydiem@users.noreply.github.com>
Co-authored-by: Lenny <lenny@buzzagents.dev>
mfethe1
added a commit
to mfethe1/buzz
that referenced
this pull request
Sep 15, 2026
…CP session scope, thread-context dedup) (#64) * fix(mobile): style inline code with the app mono face (#6631) ## Summary Inline code on mobile renders as **bold body text on a faint background wash** — no monospace face, no chip, and it cannot wrap. #5257 diagnosed this as a missing `highlightBuilder`. That is no longer the right fix. `gpt_markdown` 1.2.0 deprecates `highlightBuilder` (removal in 2.0.0), renders inline code as a real chip, and adds `InlineCodeStyle` for restyling it. The package author confirmed this on the issue. So this PR is an upgrade — 1.1.6 → 1.2.1 — plus one theme declaration, rather than the builder the issue originally asked for. **Where the style is declared.** `GptMarkdownThemeData` goes in `AppTheme._buildTheme`, which both `light()` and `dark()` call. That reaches all four `GptMarkdown` call sites — `message_content`, `transcript_item_widget`, `token_pill`, `custom_emoji_render` — so the style is stated once instead of per widget. A widget-level `inlineCodeStyle` would have covered channel messages only, leaving the other three on the package's defaults. **What is declared.** Face, size, ink, chip fill and outline — not the face alone. A face name on its own leaves the rest on the package's defaults, which put inline code at 14.1sp beside a fenced block's 13, on a neutral `onSurface` tint rather than the app's code surface. In dark that tint is *lighter* than the surface, while every other code surface in the app is recessed, so the chip read as a different kind of object. All of it now comes from one `CodeStyle` declaration that the fenced block reads from too, so the two cannot be edited apart. **Three adaptations the upgrade requires.** Each was found by running the gate, not by reading the changelog: 1. **`imageBuilder` widened** to `(context, url, width, height)`. This is a hard compile error, and it is **not listed in the package's migration guide**, which states "nothing here stops code compiling". Worth reporting upstream. 2. **`autolink` now defaults to `true`.** `normalizeBareLinks()` already rewrites bare URLs into Markdown links before rendering, so both would run. `message_content` opts out with `autolink: false` to keep current behaviour exactly. The migration guide argues for dropping the pre-processor instead — a better fix, but a behavioural change that belongs in its own PR. 3. **`gpt_markdown.dart` now re-exports `markdown_config.dart`**, making two direct imports redundant. `flutter analyze` reports `No issues found!` on 1.1.6 and flags both on 1.2.1, so these warnings are new, not pre-existing. **Deliberately out of scope.** The three non-message call sites now autolink bare URLs, since only `message_content` has a pre-processor to collide with. Custom inline components (`_MentionMd`, `CustomEmojiMd`, `_ChannelLinkMd`) could additionally declare `allScopesExceptLinkLabel` — 1.2.0 offers it as the fix for a `WidgetSpan` chip going blank inside a link label on iOS — but current behaviour is unchanged without it, so that stays a separate change. ### Related issue Fixes #5257 Duplicate scan: searched `gpt_markdown`, `inline code mobile`, `highlightBuilder` and `InlineCodeStyle` across both PRs and issues. No open PR touches inline code styling. #6135 (link labels) and #6166 (text selection) also touch mobile Markdown but address different defects. ### Testing Full gate, `just ci` — exit 0: | Stage | Result | |---|---| | Rust (33 suites) | 4768 passed, 0 failed | | Desktop | 5799 passed, 0 failed | | Mobile | **2011 passed**, 0 failed | | `flutter analyze` | `No issues found!` | | Desktop + web build | ok | Run on the branch with `main` merged in, so these numbers match what CI builds. **New regression test** — `renders inline code in the app code style`. It resolves the `CodeTextSpan` the package tags inline code with, which carries both the resolved `TextStyle` and the colours the chip behind it is painted with, so face, size, ink, fill and outline are all asserted rather than a widget's presence. It is negative-controlled: reverting only the theme declaration fails it with ```text Expected: a numeric value within <0.001> of <13.0> Actual: <14.1> ``` and dropping the declaration entirely falls back to `packages/gpt_markdown/JetBrainsMono` — so the test measures the real thing, and it would catch a future regression that silently drops the theme extension. The test passes `baseStyle: messageBodyTextStyle`, the style the message surfaces actually use; the widget's own fallback is the smaller `bodyMedium`, which would move the expected size. The test finds paragraphs with `find.byWidgetPredicate((widget) => widget is RichText)`, not `find.byType(RichText)`: inline code renders through `BidiRichText`, a `RichText` subclass, and `byType` matches exact runtime types. That is a hazard for any test that reads text back out of a paragraph, and one landed after this branch was cut: `message_content_custom_emoji_test.dart` arrived with #6996 and its `code keeps literal emoji while adjacent known tokens render` case reads a code span through `find.byType(RichText)`. It passes on `main` and fails on the merge result, which is what CI builds, so it went red only once CI was authorized. It now uses the same predicate. The two other `byType(RichText)` call sites — the rest of that file and `message_author_meta_test.dart` — were re-run and pass: their content carries no code span, so the exact type still matches. They were left alone. ### Screenshots Rendered through the real `MessageContent` widget with the app's own fonts loaded, at 390pt wide, 3x DPR. Sample text: ``Set `BUZZ_RELAY_URL` before launch, then run `just mobile-test` to verify.`` | | Before (1.1.6) | After (1.2.1) | |---|---|---| | Light |  |  | | Dark |  |  | Before: bold Inter on a flat wash, no chip edge, and `just mobile-test` breaks across the line with the wash simply ending. After: Geist Mono in a bordered, rounded chip, and the wrapped fragment gets its own chip on each line. --------- Signed-off-by: Tolga Cinisli <tolgacinisli@gmail.com> Co-authored-by: Tolga Cinisli <tolgacinisli@gmail.com> * fix(buzz-acp): wake held ACP threads and fence forked sessions (#7340) ## Summary Adds an independent deadline wakeup so held thread work dispatches after its 10-second bound even when the relay loop is otherwise quiet. Fences session ownership by generation so a worker returning after a fork cannot make an older provider session claimable again. This follows up on the two post-merge findings from [#7337](https://github.com/block/buzz/pull/7337#pullrequestreview-5116329341). ### Related issue Follow-up to #7337. ### Testing - `cargo test -p buzz-acp` - `cargo clippy -p buzz-acp --all-targets -- -D warnings` - Pre-push file-size, differential Rust test, and desktop Tauri gates No UI changes. --- **Update Sep 4, 15:35:** Addressed both Codex review findings. - Queue-cap eviction now prunes orphaned hold deadlines. - An expired hold stays expired until a worker is successfully claimed. - Hold timers remain disabled while every worker is busy; worker return wakes dispatch directly. - Added regressions for queue eviction and pool exhaustion. Generated with Codex --------- Signed-off-by: Salman Mohammed <smohammed@squareup.com> * fix(agent): route GPT-5+ model-service FQNs to Responses (#7358) ## Summary Route Databricks Unity Catalog model services to OpenAI Responses when their service name matches GPT-5 or newer. These models can reject tools plus reasoning on Chat Completions. Match only the service component, using the existing family-token boundaries and a numeric major version. Catalog and schema names cannot select the protocol. Keep neutral effort capabilities and the full model ID unchanged; other services still use MLflow Chat Completions. Keep the Rust and desktop resolvers in sync, add shared boundary cases and a captured-HTTP regression for completion and summarization, and update the documented FQN rule. ### Related issue No duplicate found in searches for “FQN responses” PRs or “astra” issues. Related: #6918 introduced Unity Catalog discovery. Originating conversation: buzz://message?channel=0b881928-a3a6-4c01-b981-8e64268f01ce&id=770949343bc96a9ed88acd90a1b37d358a0efc52c79237d0fdb491ce02b8d4ed ### Testing No live Databricks inference test. The gateway must accept the full model-service ID on its OpenAI Responses route; this remains the integration risk. The local `just ci` attempt exceeded its five-minute deadline during `mobile-check`, so the full repository gate was not completed. All push-hook checks passed. Generated with Codex Signed-off-by: Fizz <400e8babadcee6a7f420103f10a2849d84c4a9c71d5bd04f3948c814216648a3@buzz.block.builderlab.xyz> Co-authored-by: Fizz <400e8babadcee6a7f420103f10a2849d84c4a9c71d5bd04f3948c814216648a3@buzz.block.builderlab.xyz> * feat(mesh): upgrade to mesh-llm 0.76.0-rc8 and recommend Qwen3.8 27B (#6189) Upgrades Buzz's mesh-llm dependencies through the released `v0.76.0-rc8` tag (`2040765d`), including the Qwen3.8 curated recommendation and rc8's scheduler/runtime improvements. **Scope note:** the earlier open-relay/unenforced-admission mode has been removed from this PR at Mic's direction — it is not a product mode we want. Mesh admission remains roster/allowlist driven, exactly as on `main`: on a relay with no NIP-43 membership snapshot the mesh runs self-only. No NIP-11 mode probing, no mode-transition restarts. A future perimeter/admission strategy for open relays will be designed separately. This PR also: - seeds `BUZZ_AGENT_LLM_TIMEOUT_SECS=660` for mesh agents, above MeshLLM's 600-second backend timeout; - makes `desktop-tauri-clippy` lint both default and `mesh-llm` cfg graphs; - runs the feature-enabled desktop test suite in CI; - recommends Qwen3.8 27B Q4_K_M for 64 GB-and-larger machines, then ladders down through Gemma 4 E4B and Qwen 9B for smaller machines; - keeps stored shared-compute `auto` translated to MeshLLM's supported wire model `mesh`. RC8 verification: - `just ci` passed locally at `92ecc7ec933bdd4df804cc9f28a2b51efa5313c5`. - Pre-push differential gates passed, including both desktop Tauri cfg graphs and package tests. - A prior isolated runtime smoke used the RC8 binary's OpenAI endpoint for a Buzz-shaped system/user/tool/tool-result/final-response loop; all assertions passed and the isolated process was shut down. Perf previously measured on M5 Metal, Qwen3.8-27B-Q4_K_M: TTFT 0.22–0.32 s, ~25 tok/s streaming; agent-shaped turns ~1 s to first token after the first (prefix cache). --------- Signed-off-by: Michael Neale <michael.neale@gmail.com> Signed-off-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz> Signed-off-by: Alessandro Joabar <sandro@squareup.com> Co-authored-by: Michael Neale <michael.neale@gmail.com> Co-authored-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz> Co-authored-by: Mic Neale <305999590+micspiral@users.noreply.github.com> Co-authored-by: Alessandro Joabar <sandro@squareup.com> * fix(link-preview): keep composer fetches user-paced (#7211) **Category:** fix **User Impact:** Link previews can keep loading while a message is being composed, while sending still has a finite escape hatch and stalled network transports cannot occupy preview slots forever. **Problem:** Native metadata and image deadlines could collapse slow previews into fallback cards while the user was still composing, and a shared image-host cooldown made pasted batches fail inconsistently after one rate limit. **Solution:** Keep preview resolution user-paced with no aggregate request deadline, bound transport inactivity (15s DNS/connect, 30s idle read), serialize image requests by host, and allow at most one server-directed cooldown wait of up to 30s across an image fetch and its redirects. The existing bounded post-Send preparation and immediate Skip paths remain unchanged. <details> <summary>File changes</summary> **desktop/src-tauri/src/commands/link_preview.rs** Removes aggregate native deadlines so composer metadata work can complete at the user's pace, while retaining DNS/connect/idle-read liveness bounds. Adds bounded host-paced image request coordination that releases its gate during cooldown, waits inline at most once for at most 30 seconds, and cannot renew that wait through redirects or the outer transient retry. Same-host image and favicon requests remain deliberately serialized to align with host rate limits. **desktop/src-tauri/src/commands/link_preview_rate_limit.rs** Adds a fixed-size striped host gate so concurrent image requests are serialized without retaining an unbounded attacker-controlled hostname map. **desktop/src-tauri/src/commands/link_preview_tests.rs** Moves native link-preview tests into a dedicated module and covers the user-paced metadata contract, bounded one-shot cooldown behavior, and gate release while a rate-limited request sleeps—including a different host sharing the same bounded gate stripe. **desktop/src-tauri/src/commands/link_preview_youtube.rs** Removes the thumbnail fetch deadline so YouTube previews follow the same composer lifecycle contract while using the shared bounded transport. **desktop/src/shared/lib/useResolvedLinkPreviews.ts** Adds development-only metadata outcome diagnostics with elapsed time and image/fallback state, without logging encoded image payloads. </details> ### Reproduction steps 1. Open the desktop composer and paste several GitHub pull request links whose OpenGraph images share a host. 2. Observe that image requests are paced by host instead of racing, and slow-but-progressing preview work remains pending rather than immediately becoming a completed favicon fallback. 3. Send while preview work is still pending and confirm **Preparing link preview** remains bounded by the existing post-Send budget. 4. Use **Skip** during preparation and confirm the message proceeds immediately. 5. In a development build, inspect the console for `[link-preview] metadata fetch completed` diagnostics containing elapsed time and image state without base64 payloads. ### Related issue N/A — scoped from the linked Buzz implementation room. ### Testing At current head `dfb394aafbee537e9ffb04ad3732d08f65f30b8e`: - Production-bound paused-time metadata regression passed through `fetch_link_preview_metadata`; restoring the former 10-second aggregate wrapper makes it fail at the pending assertion. - Native link-preview module: 19/19 passed. - `cargo check --manifest-path desktop/src-tauri/Cargo.toml` passed. - Rust formatting and `git diff --check` passed. - Pre-push `push-head-scope`, org safety, differential file-size, branch-skew, and `desktop-tauri-checks` hooks passed. At prior head `59e2dcf167b15c7a3e637ad2608008b7f9cef5f3`: - Full Tauri Rust suite: 3,056 passed, 19 ignored; integration crates 7 + 3 passed. - Focused native link-preview suite: 26/26 passed. - The pasted multi-preview workflow was exercised in the desktop app and confirmed improved before draft publication. --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz> Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@users.noreply.github.com> * fix(acp): pace targeted overflow recovery on consumer capacity (#7325) 🤖 ## Summary When a Buzz agent falls behind on incoming messages, its connection can make the backlog worse while trying to recover. The connection buffers messages from the relay server until the agent is ready to process them; if that buffer overflows, recovery previously requested history for **every subscribed channel** and paused socket reads while sending those requests. That adds traffic to an already overloaded connection. This change requests history only for affected subscriptions, once the code consuming those messages has room, with at least five seconds between attempts. The recovery path now: - Combines repeated losses into one pending recovery per affected subscription, keeping the oldest dropped timestamp so replay starts early enough. - Waits until at least half the consumer queue is free and the relay's existing rate-limit delay has expired. The queue wakes recovery when space becomes available; recovery does not periodically sample capacity or hold queue space away from live messages. - Attempts one subscription at a time, choosing the least recently attempted so a busy channel cannot crowd out other channels or membership notifications. The five-second delay starts when an attempt finishes, including a failed write; failed writes leave recovery pending. Recovery is paced by available capacity, not by how often messages are lost. This is not a larger buffer or a cutoff that abandons recovery. Subscription identifiers, message filters, replay timestamp overlap and duplicate filtering are unchanged; no downstream agent changes are required. This targets a reproducible overload **amplifier**, not every cause of overload or every catch-up limitation. The initial live overload's cause has not been established. Recovery remains best effort: a successful request write is not proof of delivery, and existing history/retention limits, bounded duplicate tracking and replay limitations still apply. There is no exactly-once or complete catch-up guarantee. A stalled write can still pause socket reads for the existing ten-second timeout; the pacing bound does not cover initial subscriptions, reconnects or other retry paths. ### Related issue Closest related: #5014 (channel re-subscription); also #6661 (membership reconciliation) and #6090 (relay backpressure gap signaling). This addresses local overflow recovery scheduling, not those separate mechanisms. ### Testing Recorded offline comparisons against the previous behavior, with the final implementation at `8000636f3073167c5a5107bb179c7d91160f1729`: | Same fixture: 18 subscriptions, three overload rounds | Before | After | | --- | --- | --- | | Recovery history requests | 108 | 3 | | Ping-response delay | About 4.6 seconds | Below the measurement's 1 ms resolution | A separate bounded-history fixture delivered all 320 events plus subsequent live traffic in **both** versions. Regression coverage exercises the real socket-handling task, including intermittent consumer capacity, fairness, failed writes and cancellation of capacity waits before live delivery. These are synthetic results, not production throughput measurements or evidence of a deployed cure. The full local `RUST_TEST_THREADS=4 just ci` run passed on September 4, 2026. Earlier unsuccessful local runs remain part of the validation history. The [recorded validation evidence and separate desktop follow-up](https://github.com/block/buzz/pull/7325#issuecomment-5540592398) preserve the original desktop mock-history scroll failure, its passing rerun and the remaining investigation. That desktop path does not run the agent connection code; neither this repair nor the passing rerun fixes the observed scroll problem. --------- Signed-off-by: Logan Johnson <loganj@squareup.com> * fix(mobile): render push notification sender identity as npub (#7494) 🤖 ## Summary When an iOS push notification comes from someone the app has no cached name for, the notification title showed the first characters of the sender's raw public key — for example `aa4fc866…`. That fragment is unreadable and doesn't match how the same person appears anywhere else in Buzz. This PR changes that title to the compact form of the sender's npub (npub is the human-readable encoding of a Nostr public key): first 8 and last 4 characters — for example `npub14f8…9nsy`, the same identity shape used across the desktop and mobile apps. - Unnamed senders: raw hex fragment → compact npub. - Named senders: unchanged — a sender the app has a display name for still titles the notification with that name. - Unverifiable sender identities (malformed keys, or lookalike strings that are not literal 64-hex-digit keys) now render a neutral "Someone" instead of partial raw key material. - Everything else about the notification is unchanged: body text, subtitle, thread matching and grouping, deep-link navigation, thread identifiers, and the internal hex public key the resolver matches on. The native iOS notification-service package (`BuzzPushKit`) gains a minimal in-house bech32 codec (bech32 is the checksummed string encoding npubs use) — checksum-validated, 32-byte keys only, and no new external dependency. The hex input branch accepts exactly a 64 ASCII hex digit key before any parsing, so strings that merely parse like hex (for example a run of `+a` pairs) cannot become a displayed identity; this is input validation for presentation. Event signature verification is untouched. ### Related issue Fixes: N/A. Searched existing issues/PRs for push-notification npub identity — closest related: none found. ### Testing At head `3e3f2813b8864b76257ccb50dea3a4b31fa4de0d` (base `44316ff72f5f7de014c66b01cbf534298a70c249`; 4 files, +321/−4): - CI `Mobile Swift` lane, at this exact head — all passed: `swift test` (73 tests, 0 failures), the SwiftPM debug and release builds of `mobile/ios/BuzzPushKit`, and the unsigned iOS release build. - Test coverage: npub encoding cross-checked against independent nostr-rs/NIP-19 vectors; rejection of bad checksums, mixed case, wrong lengths, invalid alphabet, padding, and non-32-byte payloads; resolver boundary matrix — hex/npub/invalid sender keys render compact npub or "Someone" while body, subtitle, sender key, and thread identifier pass through; named senders keep cached display names. ### Task provenance Buzz channel: `1f0e4a3d-7e01-4efe-bb16-843b357f85c9` Task: buzz://message?channel=1f0e4a3d-7e01-4efe-bb16-843b357f85c9&id=86b34eb4bd84a1472419e9af22636c011c0fe273e3c196f967d7a36996e149b6 --------- Signed-off-by: Logan Johnson <loganj@squareup.com> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> * fix(desktop): shared npub identity foundation (canonicalNpub, PubKey gate, strict parser) (#7488) 🤖 ## Summary Identity keys in the desktop app are displayed as raw 64-character hex. A person's key shows up as something like `953d3363…` — unreadable, impossible to recognize as the same identity on another screen, and a hazard when copied by hand. Nostr (the protocol Buzz runs on) has a human-readable spelling for identity keys — the `npub1…` form — but the desktop app did not use it consistently. This is the foundation of the desktop npub changes: it adds the shared pieces every identity surface builds on, and two follow-up slices stack directly on this branch — #7489 converts the identity controls (profile, settings, allowlist, workflow key fields) and #7495 converts the everyday display surfaces (mentions, member lists, sidebar, and other name fallbacks). After this change: - The shared identity widget shows the compact npub form — `npub1j57...fjmv` — instead of a hex prefix, everywhere it renders (for example the owned-agent public-key row on a profile). Copying it puts the full npub on the clipboard. - Copy is a real interaction, verified end-to-end: both popover variants put the exact canonical npub on the actual clipboard — never the raw hex the popover also lists, never a truncation — and a portaled popover's clicks no longer steal focus from the new-DM To-field mid-copy. Pointer copy, a natural Space-then-Enter path, and inner/outer Escape are covered. - Anything that isn't a valid identity key fails neutrally: short or corrupt values — including degenerate values that technically encode to a checksum-valid npub but aren't real identity keys — show "Unavailable" with no copy button, instead of a misleading value. - Both valid npub spellings display: all-lowercase `npub1…` and all-uppercase `NPUB1…` (Bech32, npub's encoding, permits either casing) both render the same canonical lowercase npub. Mixed case is rejected by the display path as written — `canonicalNpub` and the widget don't case-normalize input — while input parsing (`parsePubkeyInput`) keeps its trim-and-lowercase normalization and accepts mixed-case npubs; both paths require the decoded payload to be exactly a 64-character identity key. - Identity-key input is strict on payload: an npub whose decoded payload isn't exactly a 64-character identity key is rejected, matching the validation the app's Rust side already applies to agent allowlists. Intentional scope boundary: only surfaces that render through the shared widget change here. Outer profile copy, settings identity cards, the respond-to allowlist, and workflow key fields still show hex — they move to npub in the controls follow-up (#7489). Nothing else changes identity representation: display names, private keys, event IDs, and the hex the app stores, sends, and matches internally are untouched; only the user-facing spelling of an identity key changes. ## Details - `desktop/src/shared/lib/pubkey.ts` — `canonicalNpub()`: strict canonical full-npub helper (64-char hex in any case, or a checksum-validated npub, returns the canonical npub; anything else returns `null`); `truncateNpub()`: the compact display form; existing exports unchanged. - `desktop/src/shared/ui/PubKey.tsx` — the shared widget's identity gate validates through `canonicalNpub`; the popover copies the npub only. - `desktop/src/shared/lib/nostrUtils.ts` — `parsePubkeyInput` rejects npubs whose payload is not exactly a 64-character identity key. - `desktop/src/features/messages/ui/NewMessageScreen.tsx` — the To-field focuses its search input only for clicks that land inside the field itself, so portaled recipient popovers keep their focus while open (a popover click previously dismissed it mid-copy). - Unit suites cover the helper, widget, and parser (including the degenerate-encode and uppercase regressions); the e2e specs that render these rows assert the npub display. ### Related issue - Fixes: N/A. Searched existing issues/PRs for npub identity display — no existing match. - Stack: #7489 is based on this branch and builds on these primitives; it does not stand alone on main. ### Testing At head `b3310c248` (base: main `44316ff72`; 12 files, +440/−39): - Focused unit suites (pubkey, PubKey, parsePubkeyInput): 20/20 green; mutation-checked — removing the decoded-length predicate fails the short/empty checksum-valid-npub assertions in `canonicalNpub` and the widget, and a wrong-identity clipboard value fails the new copy assertions. - `pnpm typecheck` and `pnpm check`: pass; full desktop unit suite 6459/6459 at this exact head. - Targeted e2e at this exact head: 8/8 across the two specs that own the clipboard flows — `agent-access-warning.spec.ts` (compact variant, agent-access owner hint) and `pubkey-display-screenshots.spec.ts` (full variant, new-DM recipient verification: pointer copy, popover surviving the copy, inner/outer Escape, Space-then-Enter). - No Rust-side or build files change in this PR, so those results are unaffected. ### Task provenance Buzz channel: `1f0e4a3d-7e01-4efe-bb16-843b357f85c9` Task: buzz://message?channel=1f0e4a3d-7e01-4efe-bb16-843b357f85c9&id=86b34eb4bd84a1472419e9af22636c011c0fe273e3c196f967d7a36996e149b6 --------- Signed-off-by: Logan Johnson <loganj@squareup.com> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> * fix(desktop): npub identity displays for mention, member, and workflow surfaces (#7495) 🤖 ## Summary Every Buzz account is identified by a long public key. Before this change, when someone had no display name, surfaces fell back to inconsistent labels — mostly raw hex fragments like `abcd1234…wxyz`, sometimes a generic role label with no key — so the same person looked different from surface to surface, and nothing looked like an npub address. This PR applies the npub identity foundation from #7488 to the everyday surfaces: a person without a display name now falls back to the same compact npub everywhere — `npub1xxxx…yyyy`, the human-readable spelling of their public key (first 8 + last 4 characters of the full npub) — across messages and mentions, reactions, huddles, member and participant lists, the sidebar and channel activity, search, projects, tray, notifications, and workflow surfaces. - **Mentions and messages**: key-only mention chips render the compact npub. Pasting a copied mention back still re-binds it byte-exactly to the identity it declares, for both the new npub chips and legacy hex-truncated chips copied by older clients — wrong, missing, or tampered key qualification is rejected instead of silently degrading to plain text. - **Reactions and huddles**: huddle reaction events and the huddle roster/participants render the compact npub for unnamed participants; workflow reaction triggers describe authors with the same form. - **Members and sidebar**: channel and community member lists, add-member results and invites, the members sidebar, the channel-activity popover, search, projects (assignees/reviewers/PR panels), the tray menu, and desktop notifications all fall back to the compact npub; titles and aria labels keep the machine-readable full labels. - **Profile labels**: panel/popover display names and owner handles fall back to the compact npub (never raw hex) when there is no name; linked-event (nevent) message metadata shows the npub-shaped author fallback while the event lookup and event IDs are unchanged. - **Workflows**: author-picker secondary labels, step destination keys, and trigger-author references render compact npubs; event and blob IDs keep their existing hex compacts (they are not identities). - **Avatars stay distinct**: fallback avatars for key-only identities derive initials from the key's tail, so prefixed role labels like "Participant npub1…" no longer collapse every unnamed participant onto the same initials; people with names keep their name initials. Preserved exactly: display names and distinct avatars, internal hex keys (storage/API forms unchanged), clipboard identity roundtrips, event/blob ID compaction, private keys (no nsec path is touched), and nevent link handling. Scope: this PR changes what identity labels **display**, not identity controls — profile/settings copy controls, the respond-to allowlist, workflow key fields, and agent dialogs are the sibling slice #7489, and the shared primitives (`canonicalNpub`, `truncateNpub`, the `<PubKey>` gate, strict input parsing) come from the foundation #7488. ### Related issue - Fixes: N/A. Searched existing issues/PRs for duplicates — none found; the related work is the npub identity stack this slice belongs to. - Base/dependency: stacks on #7488 (foundation) — this PR does not stand alone on main. - #7489 is a sibling slice on the same #7488 base (profile/agent/workflow controls), not a dependency: this PR does not require #7489, and #7489 does not require this PR — both only require #7488. ### Testing At exact head `4763cbeae1dd521309755e6d61f657324cb98667` (base: `fix/desktop-npub-identity-d1a` @ `5f3a4a8111998c8aa41ad77cf66992bd1c85343c`; 71 files, +656/−189 — production +277/−136, test support +379/−53): - At this head: targeted `mentions.spec.ts` (1/1), the e2e build, typecheck, and biome — green. - 9 changed/related unit files: 100/100 green; typecheck, e2e build, biome, and px text/truncation checks clean; huddle-roster focused run green; channel-activity e2e 11/11; mutation checks confirm the fallback wiring (removing it collapses shared initials and drops fallback rows). - Known pre-existing local e2e failures, unchanged by this PR and reproduced identically at the upstream merge-base: huddle-transcription voice-menu attribution (25 pass / 1 fail) and the `workflow-local-controls` 438px caret drift. Not claimed green locally. - Update at head `236af9e6137386737e84d3a474d6bc808a704c50` (test-only follow-ups `1143af345` + `236af9e6`): the `workflow-local-controls` races were fixed in the test drivers, and the 438px diff was shown to be a stale Darwin snapshot baseline (name-row enable switch already absent and `message_posted` already MessageSquare at recording commit `9390e11c9`) and refreshed — the focused screenshot test, including keyboard/caret assertions, now passes locally (twice). The full spec was not rerun after the snapshot refresh; the huddle-transcription item above is unchanged. Label/copy text changes are asserted by the e2e specs (`mentions`, `mention-recipients`, `pubkey-display-screenshots`, `huddle-transcription`, `channel-activity-popover`, `workflow-local-controls`) rather than new screenshots; the screenshot spec pins the compact npub text forms. ### Task provenance Buzz channel: `1f0e4a3d-7e01-4efe-bb16-843b357f85c9` Task: buzz://message?channel=1f0e4a3d-7e01-4efe-bb16-843b357f85c9&id=86b34eb4bd84a1472419e9af22636c011c0fe273e3c196f967d7a36996e149b6 --------- Signed-off-by: Logan Johnson <loganj@squareup.com> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> * fix(desktop): npub identity controls across profile, agents, and workflows (#7489) 🤖 ## Summary Building on #7488's npub foundation, this PR finishes the identity display change for the controls where you actually manage people and keys: profile, settings, agent access, and workflows. Everywhere in these surfaces, an identity key shows — and copies — as its canonical npub (npub is the human-readable encoding of a Nostr public key: the compact `npub1j57...fjmv` form where space is tight, the full npub where the whole key matters), and accepts npub as input. After this change: - Profile panel: the public-key row and the managed-by / declared-owner copies show the full npub. If a key can't be encoded, you see "Unavailable" with no copy button — never a raw or partial key. - Settings: the identity card shows and copies the npub. The hosted-communities account identity derives from the bound key (`pubkey_hex`) — the same authority as the mismatch gate and hosted operations — so the display can never disagree with what the app acts on; an unusable hex falls back to a neutral label instead of rendering the unverified server npub. The connected claim and a community's Connect action require that same usable bound key to match the local one — with no usable binding the card cannot claim connected or start Connect, while the community list, linking, and delete/rebind recovery stay available. - Hosted create/onboarding: the account and device identity rows in the create flow and owner onboarding derive from the same authoritative fields (bound key / local key), with the same neutral fallback; readiness requires a usable bound key that matches the local one. - Respond-to allowlist (controls who may respond to an agent): entries can be typed or pasted as hex or npub; both spellings of the same key are recognized as one entry and dedupe. Search results, chips, and remove buttons use the compact npub. - Workflow key fields: to/from keys display as npubs in the form and save back as canonical hex. Templates like `{{trigger.author}}`, roles, and free text pass through untouched; placeholders accept both spellings. - Recipient and agent dialogs: the verify popover is npub-only (the raw-hex line is gone); denied-membership screens never show a raw key. - The Rust-side truncated display name (used for native surfaces) shows the same compact npub, so those surfaces match the web UI. Internal representation is unchanged: keys are still stored, sent, and matched as canonical 64-character hex — npub is a display and input spelling, normalized to hex at the boundary, so existing data and integrations keep working. Bound-key usability and comparison use one normalized form (trimmed, lowercased, 64 hex characters; npub rejected), so padded or mixed-case spellings of the same key match. Display names, private keys, and event IDs are untouched. ## Details - `respondToAllowlist` / `RespondToField`: npub entries normalize to canonical hex; cross-form dedupe; compact npub in rows and chips; direct-add accepts npub and stores canonical hex. - `workflowFormTypes` / `WorkflowStepCard`: hex → npub for display, npub → canonical hex on save; templates, roles, and free text pass through in both directions (roundtrip-tested). - `UserProfilePanelFields`, `ProfileSettingsCard`, `HostedCommunitiesSettingsCard`, `MembershipDenied`, `SelectedRecipientChip`, `AddAgentToChannelDialog`: npub display and copy; invalid keys → "Unavailable" with no copy; hosted identity rows derive from the bound `pubkey_hex` (create/onboarding rows from the bound and local keys), never the unverified server npub; connected/readiness/Connect gates use the same usable-bound-key predicate, and the settings Connect invocation callback re-checks it before starting. - `src-tauri/src/commands/identity.rs`: `truncated_display_name` compacts to the first 8 + last 4 characters of the npub (above a 12-char threshold), mirroring `truncateNpub`. - e2e: profile key rows and clipboard polls assert npub forms and raw-hex suppression; the display-screenshots spec pins the npub-only popover; hosted specs drive the real settings card, create flow, and onboarding rows through their real providers, and the unlinked/npub-only identity cases assert no connected claim and no Connect action. ### Related issue - Fixes: N/A. No separate issue; the related work is the stack below. - Stack: builds on #7488 (shared npub foundation), now merged; this PR is rebased onto main and stands on its own. ### Testing At head `303c90ffa` (base: main `bfc38485`; 24 files, +1125/−146): - Focused unit suites (respondToAllowlist, workflowFormTypes, hostedCommunityApi bound-key helpers) green; mutation-checked — dropping allowlist canonicalization fails the dedupe case, and dropping bound-key normalization fails the npub-in-hex and padded same-key cases. - Full desktop unit suite 6,477/6,477, `desktop-typecheck`, `desktop-check` (formatting fixed narrowly with `biome check --write` on the touched files only), and a fresh E2E build at the current head; the add-community + hosted-communities-settings specs 18/18 and onboarding integration 69/69 on a fresh dedicated port, with focused new-case runs 4+4 covering padded same-key (ready, Connect kept — no false rebind) and npub-in-hex (neutral label, recovery, no Connect) across the settings card, create flow, and first-community onboarding, plus the unlinked-account settings regression asserting Connect cannot occur. - `cargo fmt`/clippy (both feature sets) and `cargo test identity` (71 pass) passed at the earlier full-change head; since then, the only production changes in this PR's delta are the hosted identity display authority and its fail-closed bound-key gating/normalization above (base-side fixes carry #7488's receipts) — every other change is test-only. ### Task provenance Buzz channel: `1f0e4a3d-7e01-4efe-bb16-843b357f85c9` Task: buzz://message?channel=1f0e4a3d-7e01-4efe-bb16-843b357f85c9&id=86b34eb4bd84a1472419e9af22636c011c0fe273e3c196f967d7a36996e149b6 --------- Signed-off-by: Logan Johnson <loganj@squareup.com> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> * fix(mobile): standardize public-key identity display on npub (#7493) 🤖 ## Summary In the mobile app, anyone who hasn't set a display name shows up as a raw 64-character hex key (e.g. `3a5d4f9c…`) — unreadable, and unrecognizable as the same identity across screens. Profile and Settings also let you copy that raw hex. Nostr public keys have a standard readable form — `npub1…`, the same encoding other Nostr apps and our desktop app already display. This PR makes every mobile identity surface render npub instead: - **Unnamed people everywhere** — message and thread authors, reactions, typing indicators, member lists, channel details, DM headers and tiles, inbox, search, forum cards, Pulse notes and reply context, mention suggestions, and invite rows — now show a compact npub label: first 8 + last 4 characters of the full npub joined by an ellipsis (`npub1abcd…wxyz`), the same truncation desktop uses. Previously these showed truncated raw hex. - **DM fallback avatars and blank names** — 1:1 DM tiles and headers key their fallback avatar to the same non-self counterpart the label names, including self-first participant order; a self-DM keeps its hex-key-derived initial. Blank or whitespace-only display names fall back to the compact npub instead of rendering empty, while nonblank authored names render verbatim (padding included). - **Profile sheet → "Copy public key"** now copies the full canonical npub — never raw hex. When the identity string isn't a valid public key, the copy tile is disabled, so a malformed key never reaches the clipboard. - **Settings → Identity (pubkey)** displays and copies the full npub; an invalid identity reads "Identity unavailable" with copy disabled. - **Invalid identities never leak truncated raw hex** into the UI anywhere — they render a neutral "Unknown identity" label. - **Unchanged on purpose:** display names and verified handles (NIP-05 — the `name@domain` badge) still render as before. Unnamed avatars keep distinct per-key initials, derived from the underlying hex key rather than the npub — otherwise every unnamed key would render the same "N" initial. Event IDs are not public keys, so they keep their hex truncation (in Pulse's "Replying to", the parent author shows npub while an event-id fallback still shows hex). The nevent share link, private keys, and internal hex storage are untouched. Inputs that accept a key (invite/member entry) accept both hex and npub and keep working in hex internally. ### Related issue N/A. Searched open issues/PRs for npub identity display on mobile — closest related: none found. Desktop's parallel npub standardization lives in the stacked desktop PRs (#7488 foundation, #7489 controls, #7495 display surfaces); this is the independent mobile slice (based directly on `main`, not on those branches). ### Testing At exact head `5a620e420a1fd57d9d8011ac26434eed32fcf765` (base: `main` `44316ff72`; 40 files, +1,345/−154): - Full mobile suite: 2,098 tests passing (`cd mobile && flutter test`); `flutter analyze` clean; `dart format --set-exit-if-changed .` clean — the same checks CI runs. - Widget/unit coverage at production seams: compact labels and hex-keyed avatar initials for DM headers/tiles, member rows, mention suggestions, and Pulse reply context; DM fallback avatars keyed to the labeled counterpart (self-first order and self-DMs); blank/whitespace display-name npub fallback with nonblank authored labels verbatim, including the Activity inbox sender and profile-sheet heading (each with its own empty/whitespace production-seam regression); full-npub copy and disabled-copy semantics in profile and settings; invalid-key suppression; and hex↔npub input round-trips. Verified via unit and widget tests — no device/simulator validation is claimed. ### Task provenance Buzz channel: `1f0e4a3d-7e01-4efe-bb16-843b357f85c9` Task: buzz://message?channel=1f0e4a3d-7e01-4efe-bb16-843b357f85c9&id=86b34eb4bd84a1472419e9af22636c011c0fe273e3c196f967d7a36996e149b6 --------- Signed-off-by: Logan Johnson <loganj@squareup.com> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> * fix(desktop): order unnamed roster members by full canonical npub (#7503) 🤖 ## Summary - Channel members appear in the Members sidebar. A member who has never set a display name is listed under an abbreviated form of their public key (npub), and the sidebar previously sorted those unnamed members by that short label. Short labels are not unique — different keys can share one — so the order of unnamed members could look arbitrary or unstable. Unnamed members now sort by their full public key, so the order is deterministic. - When two members display the same name, the previous tiebreak was membership order (who joined first), which is not visible to a reader and can shift as roster data loads in. The tiebreak is now the full public key, so identical display names always land in the same order. - Nothing gets noisier on screen: the full key is used only for sorting, and the sidebar still shows the compact abbreviated form. Priorities are unchanged — authored (custom) names still outrank fallback labels, and role/current-user grouping still applies. - Scope is the desktop app's Members sidebar and member management: the two existing sort comparators. Mobile and other lists in the app are untouched. ### Related issue Based on #7495 (introduced the abbreviated npub labels this follows up on). The original five presentation PRs remain independently reviewable. No closer duplicate found. ### Testing - 6469 desktop unit tests, typecheck, and check pass. - The 3 existing consumer-seam E2E tests still pass; a new E2E test asserts the sidebar lists unnamed members in full-key order, with fixture members deliberately inserted in the opposite order so incoming membership order cannot mask the sort. - Negative check: reverting only this change makes the new ordering assertion fail, so it genuinely binds the new sort. - CI has not run on this PR yet. Buzz provenance: channel 1f0e4a3d-7e01-4efe-bb16-843b357f85c9 / task 340c3de9b27dbedb8453c0c7652220f9080d30fcc70a7c4f6e27fdd4fa378056 --------- Signed-off-by: Logan Johnson <loganj@squareup.com> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> * fix(desktop): require a Codex adapter with Astra support (#7427) ## Summary Buzz considers codex-acp 1.6.2 current because the supported adapter floor is still 1.1.7. That adapter bundles Codex 0.148.0, so updating a separate Codex CLI to 0.153.4 leaves managed agents on the older runtime and unable to use GPT-6 Astra. Raise the supported adapter floor to the published 1.10.0 release, which depends on `@openai/codex ^0.153.3`. Existing discovery and installation code then classifies older adapters as outdated and offers the managed reinstall path. Update the availability and install-plan regressions to cover the observed 1.6.2 installation and the new minimum. This follows the existing version-floor policy. It does not automatically update a running installation: the user must complete Buzz’s offered adapter upgrade. Future upstream compatibility changes may require another floor update. ### Related issue No exact duplicate found in searches for Astra, CODEX_PATH, bundled Codex, outdated runtime, and codex-acp 1.10. Related: #3097 raised the older floor to 1.1.7 (already present on main); #2422 covers lost error details for runtime mismatches. Neither resolves this version gap. Originating conversation: buzz://message?channel=3286cd76-f83e-4c7d-8317-10a16580744d&id=8b79a73078217222b870fff144c27e7d27bcd5a67c966869c18fe726db716898 ### Testing - Isolated npm install of codex-acp 1.10.0 resolved bundled Codex 0.153.4, with no CODEX_PATH override. - Live macOS ACP probe: initialize protocol v1 → session/new → select gpt-6-astra[medium] → prompt. Received `OK` and `stopReason: end_turn`; usage metadata confirms gpt-6-astra. - Existing adapter 1.6.2 initialized but advertised no Astra model in the same probe. - Desktop Rust formatting and `git diff --check` pass. - `just desktop-tauri-test`: 3,266 passed, 20 ignored, zero failures across the Desktop workspace and integration tests. - Workspace and Desktop Clippy, frontend static checks, and `just file-size-check` pass. - Repository `just ci`: still running the remaining mobile/build/workspace-test stages. The installed Buzz app and managed adapter were not replaced or restarted. The live check validates the new adapter/runtime path; a complete packaged Desktop upgrade workflow remains untested. Signed-off-by: Stephen DeLorme <stephen@d.elor.me> * fix(buzz-acp): report missing models without retrying (#7538) ## Summary When an agent reports model-not-found, Buzz retries the unavailable model and delays the failure reply until retries are exhausted. Stop retrying this error and immediately post a threaded recovery notice. The notice tells users to select a different model in agent settings, save, restart the agent to apply the configuration, and re-send their request. This adds one error-handling branch and regression coverage in `buzz-acp`. It matches `-32002` errors containing `model not found`. Other resource-not-found errors, such as stale sessions, retain the existing retry behavior. Detailed error events remain available for diagnosis. The existing restart policy is unchanged. ### Related issue None found in existing issue/PR searches for model-not-found recovery. ### Testing Playwright captured and visually checked the thread UI with seeded conversation data and the exact recovery text. The check opens the request's thread, confirms no reply before the failure, injects the notice, and verifies the full text is visible. [Before/after screenshots](https://github.com/block/buzz/pull/7538#issuecomment-5608196506) show the corrected save-and-restart instructions. These are local test captures, not a deployed provider recovery flow. Generated with Codex --------- Signed-off-by: Diem Nguyen <diem@squareup.com> * fix(desktop): let inbox title and message author names truncate under narrow panes (#7550) ## Summary Fixes two instances of the same dead-truncate pattern in the desktop app, where a flex item's implicit `min-width: auto` prevented `truncate` from engaging, so long text painted over adjacent controls instead of ellipsizing: - **Inbox detail title** (`InboxDetailPane.tsx`): the clickable context-title button sized to its text instead of shrinking with the pane, overlapping the header controls (open-in-channel, members, huddle, more menu). Fixed by adding `max-w-full`. - **Message author names** (`MessageHeader.tsx` / `UserProfilePopover.tsx`): the `UserProfilePopover` inline-flex trigger wrapper refused to shrink below the name's nowrap width, running long author names under the hover action bar and off the pane edge. Fixed by adding a `triggerClassName` prop to `UserProfilePopover` and passing `min-w-0 max-w-full` at the author call site. Two other suspected instances (project file breadcrumb, drafts pane title) were stress-tested and already truncate correctly — no change. ### Related issue N/A — none found. ### Testing - New Playwright regression tests for both fixes (`inbox-title-overlap.spec.ts`, `message-author-overlap.spec.ts`, registered in the smoke project), each proven to discriminate: they fail with the fix reverted (real measured overlap) and assert the ellipsis actually engages with non-zero title width, so they can't pass vacuously. - Typecheck, lint, and full desktop unit suite green (pre-push hooks); full desktop e2e smoke suite run earlier: 1402 passed, 3 pre-existing unrelated failures (each fails identically with the fix reverted). **Inbox title — before** (long title paints under the header controls):  **Inbox title — after** (truncates with ellipsis, controls stay clear):  **Author name — before** (long name runs past the header row edge):  **Author name — after** (clean cutoff):  🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: cynfria <yescynthia@gmail.com> Signed-off-by: Tree Trunks <6ba22921d9dc2ad0aa6ecdf63787ddd24726e266d866da31af69f2e4e146ace5@buzz.block.builderlab.xyz> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Tree Trunks <6ba22921d9dc2ad0aa6ecdf63787ddd24726e266d866da31af69f2e4e146ace5@buzz.block.builderlab.xyz> * fix(relay): reject presence updates when Redis storage fails (#7532) ## Summary - Reject kind:20001 presence events with `OK false` / `error: presence storage unavailable` when Redis SET or DEL fails, before publishing, local fan-out, or local-event marking. - Preserve the producer contract needed by snapshot-confirming consumers: delivered live presence must follow successful mutation of the Redis state read by snapshots. - Classify those backend rejections with the existing `IngestError` taxonomy so a presence storage outage counts as `buzz_events_rejected_total{transport="ws",reason="error"}`, not client `reason="invalid"`; genuine client-input refusals (verification failure, membership gates) stay `invalid`, and every wire message is an unchanged fixed sanitized string (review follow-up, no protocol wording change). - Add actual `handle_event` integration coverage for rejected online/offline transitions, healthy online→offline accepted/stored/fanned-out behavior, and the rejection-counter routing on storage failure with an invalid-signature control. This is standalone on main; it does not depend on the mobile implementation. Deploy this relay prerequisite before relying on #7526's snapshot-confirmation policy. Existing pubsub-failure-after-successful-storage behavior and disconnect TTL cleanup are deliberately unchanged. A storage error may be an ambiguous write outcome, not a rollback guarantee; the rejected event is not published by this handler. Clients may retry the generic `error:` rejection. Desktop's 60s heartbeat retries non-offline presence, not every explicit offline transition. ### Related issue Addresses the relay prerequisite identified in [#7526 review 5157607827](https://github.com/block/buzz/pull/7526#pullrequestreview-5157607827). Searched open presence/storage PRs; no duplicate relay storage-error rejection fix found. #7382/#7383/#7526 heads and bases are unchanged. ### Testing Exact head: `389174df29cc02d0f885c03209eff661d8bb2ec0` (+380/-13; 393 total), one commit `389174df2` on top of the reviewed `c031d6eb1` (DCO-signed; base `bfc384855889432df4a333a0edf3080f332ee169` unchanged). - PASS: `cargo fmt --all -- --check`, `cargo clippy -p buzz-relay --all-targets -- -D warnings`, `git diff --check`, `just file-size-check`, PostgreSQL discovery validation — all run at the exact final head with a clean tree before and after. - PASS: documented native `scripts/postgres-test-run.sh -p buzz-relay --lib --tests`: **89/89** actual integration tests, including the four presence cases (online/offline storage rejection, healthy online→offline, and the new rejection-classification case). Owned PostgreSQL 17/Redis on isolated loopback ports, schema plus reconciliation applied; no shared development database. - PASS: explicit `cargo test -p buzz-relay presence_storage -- --ignored --nocapture`: **4/4**, not skipped. - Full isolated relay crate suite at the final head (`cargo nextest run -p buzz-relay --lib --tests`): **1062 run: 1062 passed, 94 skipped**. The previously failing `api::mesh_demo::tests::demo_join_forwarded_arm_round_trips_echo` passed in this run (1.5s); it is a known timing-sensitive main baseline failure tracked open in #7140 and untouched by this PR, so this single passing run is reported as-is and does not claim environmental clearance or close #7140. No full-suite-green claim is made beyond this run. - Mobile is untouched; #7526's existing 2090-test/format/analyze evidence remains scoped to its unchanged head. Its separate Desktop Smoke E2E (2) failure remains red; no CI retries requested. [Production-seam regression coverage](https://github.com/block/buzz/blob/389174df29cc02d0f885c03209eff661d8bb2ec0/crates/buzz-relay/src/handlers/event.rs#L1491-L1803): the metric case drives real `handle_event` traffic against a genuinely dead Redis endpoint with a seeded active PostgreSQL community and a registered presence watcher, asserts the storage rejection counts `reason="error"` while a tampered-signature control through the same dispatcher arm stays `reason="invalid"`, and re-asserts the rejected ACK, no fan-out, and no local-event marker. Counter assertions use a thread-local recorder guard held across `.await` points (the buzz-db counter-test convention) inside the per-process nextest postgres-ci lane, so no parallel test can race the counter snapshot. No UI change or screenshot. Local logs and reproducible service/gate scripts are retained under `WORK_LOGS/MOBILE_FEEDBACK_PRESENCE_20260909/relay_prerequisite/metric_correction/` in the engineering workspace. This PR is a review candidate, not merge clearance. Causal checks: restoring only the pre-fix production mutation block makes both original rejection tests fail (`OK true` instead of `false`); healthy success still passes. Reverting only the typed classification (mapping the ephemeral `Internal` arm back to `invalid`) makes the new metric regression fail with the outage counted as `[("ws","invalid",2)]` instead of `[("ws","error",1),("ws","invalid",1)]`. The unchanged mesh echo case also failed 504/200 with the main-production block restored in the prior run, supporting its separation from this change without claiming environmental clearance. Candidate source restored byte-for-byte after each mutation. Repository-wide `just ci` was not rerun; the scoped relay gates above are the new evidence. --------- Signed-off-by: Logan Johnson <loganj@squareup.com> * fix(markdown): align mention chip wrapping (#7501) **Category:** fix **User Impact:** Human and agent mentions now break across lines with the same cloned chip treatment as repository and permalink chips while preserving the conversation text rhythm. **Problem:** Profile-backed rendered mentions sat inside an `inline-flex` popover trigger, unlike entity chips, so the wrapper interfered with true inline fragmentation. The browser-layout test measured text-range rows rather than the painted chip rectangles, allowing touching decorations to pass as “separate” fragments. **Solution:** Keep the profile trigger interactive but override its layout to true `inline`, then give mention fragments 18px computed leading inside the message’s 20px prose rhythm. Chromium paints each fragment at 17px and advances it by 20px, leaving a visible gap between cloned rounded rectangles. The browser test now measures the chip’s own `getClientRects()` and asserts fragment count, height, gap, and step; entity links retain their existing 22px leading. <details> <summary>File changes</summary> **desktop/src/features/profile/ui/UserProfilePopover.tsx** Allows inline consumers to override the trigger wrapper’s layout without changing other profile-popover call sites. **desktop/src/shared/styles/globals/markdown.css** Keeps one shared wrapping-chip mechanic and gives mention decorations enough room to separate visibly within 20px prose. **desktop/src/shared/ui/markdown.test.mjs** Pins both rendered mentions and entity links to the shared wrapping-chip contract. **desktop/src/shared/ui/markdown/MarkdownMention.tsx** Makes the profile-popover trigger truly inline so the nested mention chip can fragment with surrounding prose. **desktop/src/shared/ui/mentionChip.ts** Keeps `wrapping-inline-chip` as the single contract for fragmenting decorated chips. **desktop/tests/e2e/mentions.spec.ts** Measures the painted chip rectangles, requires a positive fragment gap, and verifies the inline trigger remains mouse- and keyboard-operable. **desktop/tests/e2e/navigation.spec.ts** Keeps a wrapped repository chip as the control, asserting its existing 22px line height and fragment advance. </details> ## Reproduction steps 1. Open a channel in Buzz Desktop using dark theme. 2. Send a message containing a human mention and another containing an agent mention; both chips should remain aligned with adjacent text on a 20px line. 3. Render a collision-qualified mention in a narrow message width; it should break into separately decorated fragments exactly like another wrapping chip, while each fragment follows the 20px prose rhythm. 4. Render a long repository or permalink chip in the same constrained width; it should retain its roomier 22px fragment spacing. ## Screenshot The dark-theme production renderer shows the real qualified label (`bob (npub1hv3…tpuc)`) at an 8rem width. The two lines now paint as visibly separate rounded fragments rather than one continuous rectangle.  ## Validation At commit `2b063e1b4ade30e11f1616269ad4ba4190366885`: - Pre-push desktop gates — file-size check, Biome/checks, typecheck, and 6,483 unit tests passed - `pnpm --dir desktop build` — passed - Focused Playwright coverage for single-line agent mention, single-line human mention, wrapped qualified mention including keyboard profile activation, and timeline mention click — 4 passed - `git diff --check` — passed --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Co-authored-by: Rizz <rizz@agents.buzz> Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz> * fix(acp): integrate the Buzz Pi adapter fork (#7552) ## Summary PR #7335 worked around missing Pi adapter support by generating a private Pi launcher and injecting Buzz's standing prompt and skills at process launch. The Buzz Pi fork now carries the required adapter extensions, so this removes that launcher and returns prompt construction to the normal ACP session path while retaining the base-prompt composition introduced by #7335. The Pi preset now installs `salman1993/pi-acp` and launches its renamed `buzz-pi-acp` binary. Buzz adds `-- --skill <harness-cwd>/.agents/skills` only when launching that binary, sends the complete composed prompt as the `_meta.systemPrompt` replacement string on `session/new` only when `initialize.agentInfo.name` is `buzz-pi-acp`, and sends the scoped title alongside it as `_meta.sessionTitle`. The fork identity is treated as system-prompt capable regardless of its reported ACP protocol version, which prevents duplicate legacy user-message framing. Upstream `pi-acp` does not receive either fork-specific behavior. Observer transcript projection accepts the string, `{ replace }`, and `{ append }` metadata forms. The fork now stores restore metadata in one atomic file per session under `~/.pi/buzz-pi-acp/sessions/`. This prevents concurrent Buzz workers from overwriting another session's prompt or title. The fix landed in [salman1993/pi-acp#9](https://github.com/salman1993/pi-acp/pull/9). This supersedes the closed #7508. No agent-configuration rules changed; this changes the Buzz Pi adapter contract and launch arguments. ### Related issue #7329 ### Testing Installed fork commit `09cf07e436b8f18e52401558f988f31a15702313` through the documented Git URL. The installed bundle matched the committed `dist/index.js` byte for byte and contained the `~/.pi/buzz-pi-acp` metadata path. The fork's 106 non-skipped tests, typecheck, and lint pass. Ran the ignored real-Pi integration test through Buzz's production session composer. The test exercised the renamed package, `agentInfo.name`, and the new per-session metadata store. Base, persona, team, core-memory, huddle, canvas, and skill markers each appeared once after switching sessions and again after restarting the adapter, while the other session and Pi's native default prompt were absent. Added regression coverage proving `buzz-pi-acp` receives fork-specific system-prompt metadata and managed skills while upstream `pi-acp` does not. `just ci` passes. Generated with Codex --------- Signed-off-by: Salman Mohammed <smohammed@squareup.com> * feat(git): add default-branch management to relay and CLI (#7562) Authored by Brain and opened on behalf of Wes (`wesbillman`). ##…
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
@agentclientprotocol/codex-acp1.1.7 or newer for Buzz managed agentsWhy
A managed Codex agent could receive relay mentions but fail to publish
buzz messages sendreplies while running codex-acp 1.1.5. Updating the adapter to 1.1.7 (which updates its bundled Codex runtime) restored outbound replies in a live macOS Buzz test. The current major-only gate treats 1.1.5 as current, so Buzz never offers that upgrade.Validation
git diff --checkOKto a relay mentionRelated to #3031