mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
4672ee55c4e4a7916c31bfeae5df2fb4384bed10
1990
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4672ee55c4 |
Fix pending message feedback (#3543)
## Summary - Keep `Sending…` beside message timestamps, including grouped pending messages. - Match the profile-card hover surface to inactive channel rows. ## Snapshots ### Pending message  ### Profile hover  ## Validation - `pnpm -C desktop typecheck` - `pnpm -C desktop check:file-sizes` - `pnpm -C desktop build:e2e` - `pnpm -C desktop exec playwright test tests/e2e/message-feedback-snapshots.spec.ts --project=smoke` --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> |
||
|
|
c55e421a06 |
fix(desktop): remove remaining Projects panel fills (#3742)
## Summary - let the Projects page surface flow through repository, pull request, and issue list/grid layouts - remove opaque fills from project-detail content across Overview, Files, Commits, Issues, Pull Requests, and Contributors - preserve borders, hover feedback, and intentional nested fills for code, inputs, badges, and warnings ## Related Follow-up to #3416. ## Testing - `pnpm exec biome check` on the changed Projects UI and E2E files - `pnpm build:e2e` - focused Playwright smoke coverage for overview, subsection list/grid, and project-detail transparency (3 passed) - pre-commit checks passed - desktop pre-push checks passed; the unrelated integration hook remains blocked by a stale local checksum for migration 25 Signed-off-by: Thomas Petersen <thomasp@squareup.com> |
||
|
|
85edc0572a |
feat(mobile): desktop-parity emoji and thread experience (#3485)
Brings the Flutter app's emoji and thread surfaces up to desktop parity. ## Emoji - **Full emoji-mart dataset** generated from the same `@emoji-mart/data` set desktop uses (1,870 emoji, 8 categories), committed as an asset — so shortcodes, names, and keywords are identical across clients. `just mobile-emoji-data` regenerates it. - **Rebuilt the tray**: search (a Dart port of desktop's tiered `emojiSearch` ranking, extended to names and keywords), a frequently-used section, and one continuous scroll with pinned section headers. The category rail is a shortcut into that list, not a page switcher, and spans the full width the search field uses. Custom emoji share the native glyph size and cell. - **Reaction pills** match desktop's geometry, and the count shows at 1. - **Emoji-only messages** render at 36px with 1.45em inline custom emoji, matching desktop's `emojiOnly` treatment. - **Positive-emoji burst** ported from desktop's `EmojiBurstProvider`, suppressed under reduced motion. ## Threads - **Top-down layout** — head first under the app bar, replies flowing down, like desktop's thread panel. The old reversed list bottom-anchored the content and jammed the head against the composer. - **Tap a channel message to open its thread**; long-press still opens the action sheet. - **Live reactions.** The thread's relay query is one-shot and its `kinds` filter carries only content rows, so a reaction event could not reach an open thread at all, and `allMessages` was a snapshot frozen when the route was pushed — a new pill only appeared after leaving and re-entering, which refetched. The live channel events are now unioned into the thread's list. The burst is also route-guarded, since the channel timeline stays mounted underneath and was claiming it first. - The `+` affordance follows the channel: replies stay bare until they carry a reaction, and the head keeps a standing `+`. ## Keyboard A deliberate downward drag past ~48px dismisses the keyboard; short scrolls leave it alone. Applies to the channel list, the thread list, and the compose bar (via a raw `Listener`, so it can't steal the field's tap or selection drags). True finger-tracking dismissal is out of scope — Flutter only offers `manual`/`onDrag`, and 1:1 tracking needs a native `UIScrollView` proxy plus Android's `WindowInsetsAnimationController`. ## Testing `just mobile-check` and `just mobile-test` pass (965 tests). New coverage for emoji search ranking, dataset parsing, the emoji-only predicate, tray scroll/rail behavior, reaction pills and the burst, and both thread fixes above. `just ci` green. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: npub12zsjdqx8dud99s9h47xmk9lq93vryf7zjrae8wdrmma52cg5yglseyulst <50a12680c76f1a52c0b7af8dbb17e02c583227c290fb93b9a3defb456114223f@buzz.block.builderlab.xyz> Signed-off-by: klopez4212 <klopez4212@gmail.com> Co-authored-by: npub12zsjdqx8dud99s9h47xmk9lq93vryf7zjrae8wdrmma52cg5yglseyulst <50a12680c76f1a52c0b7af8dbb17e02c583227c290fb93b9a3defb456114223f@buzz.block.builderlab.xyz> |
||
|
|
310df2ec33 |
desktop: restore direct community member adds (#3634)
## Summary - restore direct community member adds in **Settings → Invites** - keep one consolidated entry point: the dialog now supports both adding someone directly and sharing an invite link - accept npub or 64-character hex keys while preserving role hierarchy (owners: Member/Admin; admins: Member only) - verify an npub direct-add publishes the decoded hex key in a kind `9030` NIP-IA event ## Why The Invites consolidation left `AddMemberDialog` without a live mount point, so the existing direct-add capability disappeared even though its mutation path still existed. This reuses that implementation rather than introducing a second one. ## Before and after | Before | After | | --- | --- | | The consolidated dialog only offered a share link; there was no direct-add path. | The same dialog now presents direct add and share-link controls as one invitation flow. | |  |  | <details> <summary>Before: Invites page entry point</summary>  </details> ## Verification - `pnpm --dir desktop build:e2e` - `pnpm exec playwright test tests/e2e/invites-settings-screenshots.spec.ts --project=smoke` — 4 passed - targeted Biome check on modified files - push hook: Desktop check and 3,783 Desktop tests passed - GitHub CI green except Desktop E2E Relay still running at last status snapshot --------- Signed-off-by: Joah Gerstenberg <joah@squareup.com> Signed-off-by: Wes <wesbillman@users.noreply.github.com> Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Joah Gerstenberg <joah@squareup.com> Co-authored-by: Amp <amp@ampcode.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> Co-authored-by: kenny lopez <klopez4212@gmail.com> |
||
|
|
7fb008f934 |
fix(desktop): explain open agent access (#2561)
## Why Hack-day feedback exposed a dangerous mismatch between the UI and the underlying access model. The `Anyone` respond-to mode appeared as a neutral dropdown choice, while a Buzz agent may act with the files, accounts, and tools available on the machine where it runs. People reasonably read this as sharing a bot in a channel. The current UI did not explain that it can also share the agent's available access. ## What - Reframes `respond-to` as **agent access** in user-facing UI. - Uses plain audience labels: **Only me**, **Anyone**, and **Selected people**. - Warns for **both** sharing modes, not just `Anyone` — `Selected people` also hands host access to someone other than the owner, so only the audience phrase differs: > Anyone can use this agent to access your computer, including files, accounts, and connected tools. > Selected people can use this agent to access your computer, including files, accounts, and connected tools. - Names the machine the agent actually runs on. A provider-backed (remote) agent reads: > Anyone can use this agent to access the server it runs on, including any accounts and tools available there. The remote wording deliberately omits the owner's files — those aren't theirs to describe on a host they don't own. - Places the warning below the selector for `Anyone`, but **after** the people picker for `Selected people`, so it never sits between the user and the selection they came to make. - Removes Nostr, harness, pubkey, and `!shutdown` jargon from the primary decision copy. Direct pubkey entry remains available as an advanced path. - Replaces the green open-access avatar dot with an amber warning marker and accessible text. Selected access uses a separate blue status. - Aligns the sidebar action and profile field with the same language. - Records the shared-field disclosure contract in `desktop/src/features/agents/AGENTS.md` so future surfaces do not silently omit it. ## Design decisions **Persistent inline warning, not a confirmation modal.** The setting does not autosave; the consequence remains visible beside the selection until the person chooses **Save access**. This gives the information before commitment without adding a dismiss-and-confirm ritual that would repeat in every create/edit surface. **An unknown run location falls back to the local wording.** It does not hedge with "computer or server". A remote host requires an installed `buzz-backend-*` provider, and without one `WhereToRunSection` never renders — so "server" would name a concept the owner has never been shown. When it *is* remote, they picked that host from the selector themselves. Surfaces never synthesize a run location they don't have. **One resolution site, published through context.** `AgentDialog` resolves the run location (`runLocationForBackend` from `ManagedAgent.backend`, `runLocationForRunOn` from the create flow's `WhereToRunDraft`) and publishes it via `AgentRunLocationContext`. It is not threaded as a prop through `AgentDefinitionDialog` (1047 lines) or `AgentInstanceEditDialog` (1228 lines) — neither uses the value, and both are already over the file-size ceiling. Surfaces outside that tree (`EditRespondToDialog`) pass the prop directly. The copy follows the writing system's guidance for high-sensitivity decisions: lead with the material consequence, use plain actor/action language, keep helper text adjacent and persistent, and never rely on color alone. ## Scope Desktop only. The web and mobile clients do not currently expose this setting. No protocol, gate, runtime, persistence, or backend behavior changes. This does not add team-scoped remote agents. It makes the current local-or-remote access model honest while that product work remains separate. ## Validation - `pnpm exec biome check` and `pnpm exec tsc --noEmit` — clean - `lib/agentAccessWarning.test.mjs` (8/8) — every mode × run-location copy variant, both resolvers, unknown-reads-as-local, blank `runOn` is not a provider - `ui/respondToFieldContract.test.mjs` (8/8) — plain labels, both warning positions, source-order guard that the `allowlist` warning follows the picker, helper-not-inline-copy guard - `agent-access-warning.spec.ts` (3/3) — native local, provider-backed remote (asserts the server sentence and *not* "your computer"), persona-backed edit; includes a bounding-box check that the `Selected people` warning renders below the picker --------- Signed-off-by: David Hamilton <daveh@squareup.com> Signed-off-by: Clay Delk <clay.delk@gmail.com> Co-authored-by: Clay Delk <clay.delk@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
262f2392e3 |
fix(cli): resolve agents from owner records (#3178)
## Context
`buzz users get --name Honey` searches relay-wide profiles and can
return an identically named agent owned by someone else. This caused
agents from the wrong owner to be added to a channel.
## Summary
This bug fix scopes exact-name agent lookup to owner-authored
managed-agent records, then cryptographically verifies each returned
profile's NIP-OA `auth` tag before asserting ownership. The relay and
database contracts remain unchanged.
### Related issue
None found.
## Changes
- Adds `buzz users get --name Honey --owner me|<hex>|<npub>`.
- Resolves `me` to the NIP-OA owner when the CLI runs as an agent,
otherwise to the CLI identity.
- Matches kind `30177` managed-agent record names exactly and
case-insensitively under the requested owner.
- Requires exactly one valid NIP-OA `auth` tag whose verified owner
equals the requested owner and whose `kind` and `created_at` conditions
apply to the profile event before returning `owner_pubkey` or
`owned_by_me: true`.
- Keeps missing, malformed, stale, condition-mismatched, or unverifiable
owner-record candidates visible with `owned_by_me: false` and an
explicit `verification` value.
- Returns every same-name record for the owner so callers can require
explicit selection when duplicates remain.
- Preserves the existing output shape and client-side name filter for
unscoped searches.
- Documents the distinct owner-scoped managed-agent lookup and unscoped
NIP-50 lookup modes.
### Testing
The reviewer-reproducible red and green commands below exercise the
ownership bug against the target branch and this branch.
## Screenshots
Not applicable. This is a CLI-only change.
## Reviewer-reproducible examples
The lookups below were run against the live relay from `main` and this
branch.
### Red: unscoped lookup returns the 100-profile relay-wide cap and
excludes John's agents
On `main`:
```bash
cargo run -q -p buzz-cli -- users get --name Honey \
| jq '{count: length, first_three: .[:3] | map(.pubkey), johns_agents: map(select(.pubkey == "31b29bcbe69d6716fbb7ba33602b89200bfc9ddfdabcfd1ea6fbfa70b816dfc7" or .pubkey == "4597ac725bba33fc7dd0454c1e2316a5ed770426acf667837d46f6553b3fcf54"))}'
```
Observed output:
```json
{
"count": 100,
"first_three": [
"20d27fc6c0ab4f50b66d1a32a64c5ca1fb985254143ce911f61ab7733333c3d7",
"00644478cdd9032c563ddc712b3687d8345d948945aab3c18bab95afbf6f519a",
"93c16697d0e58007bc11fb953208bc6b1cff387b2dee094abc10bf82dfee5424"
],
"johns_agents": []
}
```
`main` also rejects the owner-scoped command:
```bash
cargo run -q -p buzz-cli -- users get --name Honey --owner me
```
```text
error: unexpected argument '--owner' found
Usage: buzz users get --name <NAME>
```
### Green: owner-scoped lookup distinguishes verified and unresolved
records
On this branch:
```bash
cargo run -q -p buzz-cli -- users get --name Honey --owner me \
| jq 'map({pubkey,display_name,owner_pubkey,owned_by_me,verification})'
```
Observed output:
```json
[
{
"pubkey": "0ca77314d7ac8b3fcf6c647cc8cb9c3afd840db3b2a8ff2079f09a168de1827e",
"display_name": null,
"owner_pubkey": null,
"owned_by_me": false,
"verification": "missing_profile"
},
{
"pubkey": "31b29bcbe69d6716fbb7ba33602b89200bfc9ddfdabcfd1ea6fbfa70b816dfc7",
"display_name": "Honey",
"owner_pubkey": "67252b09c31a995daa63aada26569fbc6a3d12f573113f001ce7432f870da820",
"owned_by_me": true,
"verification": "verified"
},
{
"pubkey": "4597ac725bba33fc7dd0454c1e2316a5ed770426acf667837d46f6553b3fcf54",
"display_name": "Honey",
"owner_pubkey": "67252b09c31a995daa63aada26569fbc6a3d12f573113f001ce7432f870da820",
"owned_by_me": true,
"verification": "verified"
}
]
```
Only the two profiles with valid NIP-OA proofs assert ownership. The
owner-authored record whose profile is absent remains visible but cannot
be selected as verified ownership.
---------
Signed-off-by: npub1qye6rec0htgg3np8yt6plpyyg8cyffaq66emt3kmk05eylckkzhq0hnf2k <0133a1e70fbad088cc2722f41f848441f044a7a0d6b3b5c6dbb3e9927f16b0ae@buzz.block.builderlab.xyz>
Co-authored-by: npub1qye6rec0htgg3np8yt6plpyyg8cyffaq66emt3kmk05eylckkzhq0hnf2k <0133a1e70fbad088cc2722f41f848441f044a7a0d6b3b5c6dbb3e9927f16b0ae@buzz.block.builderlab.xyz>
|
||
|
|
3b8567a05d |
fix(desktop): remove Projects overview card fills (#3416)
## Summary The Projects overview now lets the page surface flow through its metrics and activity cards instead of stacking filled panels. Borders and hover feedback remain, preserving grouping and interaction cues without the heavy nested background. ### Related issue None found. ### Testing - `pnpm exec biome check src/features/projects/ui/ProjectsOverviewPanel.tsx src/features/projects/ui/ProjectsActivityFeed.tsx tests/e2e/project-pr-review.spec.ts` - `pnpm build:e2e` - Focused Playwright smoke test: `project overview does not paint a background behind its cards` (passed) - Relevant desktop pre-push checks passed; the unrelated integration gate was blocked by a stale local checksum for migration 25 Signed-off-by: Thomas Petersen <thomasp@squareup.com> |
||
|
|
63496cc1d4 |
feat(replica): portable heartbeat-token fence with snapshot-local reader routing (#3268)
## Summary
Expands read-replica usage on the relay per the Rev 2 design (thread
39d1e174 in #buzz-read-only-replica-usage): replaces the
Aurora-incompatible WAL-LSN read-side fence observation with a portable
**heartbeat token**, makes the freshness proof **snapshot-local to the
serving reader session**, and adds a default-off bounded-staleness gate
for head fetches.
### Probe (writer side, unchanged ordering)
The ordered writer scan is kept verbatim — `S = clock_timestamp()` →
masked-visibility `pg_stat_activity` oldest-xact scan — and now ends by
committing a heartbeat token **last** on the same pinned connection
(single-row `UPDATE replica_heartbeat ... RETURNING token, epoch`,
migration `0026`). The single-row update serializes all pods' probes, so
tokens are globally commit-ordered; the three-bucket completeness
argument carries over unchanged. Ring retains `(token, committed_at,
fence_wall)`; epoch change resets the ring; a same-epoch token
regression (restore adversary) clears the ring **and rotates the epoch
on the writer** so pre-rewind readers fail the epoch check. Cadence 1s.
### Routing (snapshot-local proof)
Every routed read opens `BEGIN ISOLATION LEVEL REPEATABLE READ, READ
ONLY` on a reader session and observes the heartbeat as the
transaction's **first statement** — the proof's snapshot is exactly the
snapshot the page, participants batch, and bridge aux closure read from
(`ReadSession` carries the open transaction; drop = rollback).
Fail-closed everywhere: begin failure, missing heartbeat row, epoch
mismatch, token below the ring, over-budget entry → writer.
- **Predicate B (cursor pages, default-on):** same completeness math as
the existing fence — cursor timestamp must be ≤ the proved wall; thread
candidate-terminal and above-wall pages re-run on the writer.
- **Predicate A (head fetches, default-OFF):** gated by
`BUZZ_REPLICA_HEAD_MAX_AGE_SECS` (0 = off, clamped to fence staleness).
Bounded-stale head semantics are an explicit product decision — **do not
enable anywhere without Tyler's backdated-event-semantics acceptance.**
### Observability
`buzz_db_route_decision{path, decision, reason}` across all five paths,
`buzz_db_replica_heartbeat_age_seconds` gauge, and per-decision debug
logs carrying the proved token plus backend identity: `addr:port pid=N`,
prefixed with `aurora_db_instance_identifier()` when the endpoint
supports it (probed once per process on an autocommit checkout; SQLSTATE
42883 caches a definitive false; identity is evidence, never a routing
gate).
## Review & verification
- **Wren:** full review 9/9/9 at `5f81b10e5`; identity delta re-review
approved at exact head `fedb46368` (90/90 unit, clippy `-D warnings`,
fmt independently reproduced).
- **Max:** local E2E **PASS** at `5f81b10e5` — isolated PG17 writer +
two streaming standbys behind HAProxy; paused-reader legs split exactly
as designed (6 replica/fresh + 6 writer/stale), recovery clean, RR
snapshot hardening observed in runtime logs. Evidence:
`WORK_LOGS/2026-07-28_REPLICA_HEARTBEAT_REPLACEMENT_SHA_E2E.md`.
- **My gates at head (same shell):** buzz-db 90 unit + 143
Postgres-gated green (scratch DBs); buzz-relay 767/768 — lone red is the
pre-existing `mesh_demo` flake, red on base; clippy `-D warnings` + fmt
clean.
- Key regression tests:
`routed_request_holds_one_snapshot_across_page_and_aux`
(mutation-verified both directions), same-epoch rotation,
capability-probe negative, head-gate truth table, divergent-fixture
routing suite.
## Post-merge plan
Merge publishes the immutable `sha-*` main image → bb-public PR pins it
→ ArgoCD sync → Max runs the production cursor-routing canary on Aurora
(positive identity branch proven live). Head gate stays off. Client
`since`-widening ships separately.
---
## Update — full read routing (Rev 6, commit `9fa3c9c0b`)
Extends routing to the remaining read seams per
`PLANS/REPLICA_FULL_READ_ROUTING_DESIGN.md` Rev 6 (thread cf5deba4 in
#buzz-read-only-replica-usage).
### New routed seams — ALL deploy-default dark
The new seams (`query_events_routed`, `query_events_routed_bounded`,
`count_events_routed`, `get_events_by_ids_routed`,
`query_feed_{mentions,needs_action,activity}_routed`) are
**Bounded-only** and gated on `BUZZ_REPLICA_READ_MAX_AGE_MS`: unset ⇒
every new seam records `writer/disabled` and merging is a no-op. COUNT
and feed/by-ids never take the covered arm (deletion visibility: covered
bounds insert-completeness only). **Note:** the pre-existing cursor
paths (channel windows, thread pages) are *not* gated by this env var —
they route at B=0 today and that status quo is unchanged.
### Reader pool (D4/D5)
- Lazy pool (`connect_lazy`, `min_connections(0)`): reader-down at boot
can't crash the relay; a warn-only boot ping is the only boot-time
visibility, and it primes the Aurora identity capability cache. Priming
is an optimization only — the routed path itself spends a single acquire
budget regardless (see the single-checkout fix below), because a boot
ping that *fails* is correlated with exactly the reader-unavailable case
the budget bound exists for.
- `READER_ACQUIRE_TIMEOUT` = 150ms; a miss fails closed to the writer
with reason **`reader_acquire_timeout`** — named for the mechanism, not
a diagnosis. The budget includes cold connects and sqlx's `size` counts
in-flight dials, so this metric alone does not distinguish contention
from slow connection establishment (see `proved_reader` doc-comment for
the runbook guidance; the pool gauges are 10s samples and are for
capacity planning).
- `BUZZ_DB_READ_POOL_SIZE` sizes the reader independently (invalid/0
inherits writer sizing); `read_pool_stats().max` reports the reader's
own ceiling.
### Community isolation (formal-model question)
Isolation is structural — an explicit `community_id = $n` predicate
compiled into every query builder; no RLS, no session-GUC tenant state
(zero `CREATE POLICY` across migrations). The `_on` variants reuse the
exact same builders with only the executor swapped. Proven by a
seven-seam two-community divergent-fixture test
(`routed_reads_are_confined_to_the_requested_community`),
mutation-tested by Dawn: all four single-predicate stubs killed; the
mention-join feeds are defended in depth (three independent predicates)
so only complete removal leaks there.
### Accepted limitation
D6: client-side staleness on bounded reads (up to `_MS`) is accepted
product behavior when the gate is enabled; gate stays off at merge.
## Update — single acquire budget per routed read (commit `dd26caa9f`)
Max found (and Dawn independently reproduced, 302–330ms measured) that
the boot-unavailable cold path spent **two** stacked
`READER_ACQUIRE_TIMEOUT` budgets: the Aurora capability probe did its
own `pool.acquire()` before `begin_with` acquired again. Fixed by
acquiring **once** per routed read — the capability probe runs on the
held connection (`reader_aurora_capability_on`) and the read-only
`REPEATABLE READ` transaction begins on that same connection
(`Transaction::begin` accepts a `PoolConnection` at `'static` via sqlx's
`MaybePoolConnection`). Reason codes unchanged; capability still never
negatively cached. Measured routed fallback: ~150ms (one budget).
Ships with a PG-gated regression fixture
(`routed_fallback_spends_one_acquire_budget_when_aurora_cache_is_cold`,
authored by Dawn): size-1 reader saturated, capability cache asserted
cold, routes through `count_events_routed`, asserts writer answer +
one-budget elapsed + `writer/reader_acquire_timeout` label
(mutation-tested: `pool_busy` and `reader_validation_error` mutants both
killed). It fails at 330ms on the previous commit and passes on this
one.
### Verification at `dd26caa9f` (pinned toolchain rustc 1.95.0 via repo
`bin/`; ambient rustc 1.89 fails sqlx resolution — always prepend
`bin/`)
- buzz-db: 94 unit + **150/151** PG-gated serial (`--test-threads=1`;
suite is not parallel-safe against one Postgres). The 1 red is
`test_usage_metrics_lock_has_single_owner_and_releases_on_drop` — a
**pre-existing** same-key-same-database advisory-lock collision with any
live relay pointed at the shared `buzz` DB (the relay re-arms
`USAGE_METRICS_LOCK_KEY` on a 300s tick). It passes on a private scratch
DB with the relay still running — verified this run. Remedy is a scratch
`TEST_DATABASE_URL`, **not** terminating lock holders: inspect
`pg_locks`/`pg_stat_activity` and resolve the owning process first; an
idle advisory holder may be a live dev relay. Pre-existing, not a #3268
blocker — filed as #3619.
- buzz-relay: 773/773; clippy `-D warnings` + fmt clean. (`mesh_demo`
echo test is a known pre-existing Redis-dependent flake — Dawn confirmed
it fails identically on the base commit, untouched by this PR.)
- Dawn: independent re-verification at `dd26caa9f` — byte-identical diff
application confirmed, suite reproduced, and fixture proven to still
discriminate (fails at 320ms with only the production fix reverted).
Gate clear. Max: independent rig pass at `dd26caa9f` — 94/94 unit,
**15/15 routing/fallback matrix** (cursor behavior, default-dark seams,
seven-seam confinement, one-snapshot, dead-reader/no-URL fallback,
hard-delete fail-closed, reader max), 773/773 relay, private-DB lock
control. Gate clear; recommends merge + staged bb-block rollout
(no-reader-URL no-op → cursor-only → observe `buzz_db_route_decision` →
conservative nonzero `BUZZ_REPLICA_READ_MAX_AGE_MS`; rollback is
config-only).
## Known gap — CI does not run the PG-backed fixtures (#3622)
Dawn found post-sign-off (confirmed by Max and Eva at `dd26caa9f`):
**every Postgres-backed fixture in this PR is `#[ignore]`d and no CI job
selects it.** `Unit Tests` runs `cargo nextest run -p buzz-db --lib`
(`Justfile:279-285`, ignored tests excluded); the only `--run-ignored
ignored-only` invocations (`ci.yml:690`, `:702`) filter to
`relay_invite::tests`. So the one-budget regression, the seven-seam
isolation fixture, and the fence/floor-guard/fallback tests have zero
automated execution — the verification record above is exhaustive but
manual, at this exact SHA.
Not introduced by this PR (the `#[ignore]` + narrow-filter pattern
predates it; 34 ignored tests total) and not a merge blocker: all new
seams are dark until `BUZZ_REPLICA_READ_MAX_AGE_MS` is set. But it
changes the rollout gate — **do not set a nonzero
`BUZZ_REPLICA_READ_MAX_AGE_MS` in bb-block until CI enforces these
fixtures.** Filed as #3622 (explicit CI selection against the existing
backend-integration Postgres archive; ordering note: #3619 must land
first or be excluded, since widening the filter would select the
colliding usage-metrics lock test).
## Live redteam results (Max, real Helm + PG17 streaming replication at
`dd26caa9f`) — #3643, #3644
Max deployed this exact SHA via the repo chart against a physical
writer/standby pair and attacked it. **The PR's fenced routing passed
every fault**: healthy baseline routes proven on the real standby;
paused WAL replay moved head/cursor reads to `writer/stale`; B=0 moved
routed reads to `writer/disabled`; reader-URL removal rolled back
cleanly.
The redteam also found a **pre-existing** production risk this PR does
not create and does not fix: NIP-50 search builds its own eager,
unfenced pool straight from `READ_DATABASE_URL` (`main.rs:389-402`,
introduced by #2084, present in the deployed bb-block `sha-dd222a5` and
bb-public `sha-22be8bb` images, and both production ESOs already
template `READ_DATABASE_URL`). Measured: ~30s search stall on reader
loss, acknowledged-but-unsearchable writes under replica lag (unaffected
by `BUZZ_REPLICA_READ_MAX_AGE_MS`), fatal eager connect blocking pod
startup when the reader is down, and `replica=true` FTS even at B=0.
Filed as **#3643** (fix: pin FTS to the writer, or fence it like the
routed seams) with **#3644** for the writer-pointing-reader-URL
telemetry gap. Aurora's cluster-ro writer-fallback DNS softens the
outage modes in prod but not the lag mode.
**Revised ladder consequence:** step 2 ("add `READ_DATABASE_URL`, budget
unset") is NOT a no-op with today's binary — it hands FTS an unfenced
replica pool. Rollout order is now: merge (still a true no-op —
bb-block/bb-public already run the unfenced-FTS code and this PR only
adds dark seams) → fix #3643 → CI enforcement #3622 → then reader URL +
budget per the original ladder, re-running Max's three deployment tests
(reader-down latency, reader-down cold boot, paused-replay
read-your-own-write search).
## Activation gate (consolidated) — merge is clear; ALL of the below
precede any nonzero `BUZZ_REPLICA_READ_MAX_AGE_MS`
Six independent verification passes at `dd26caa9f` (Dawn ×2 incl.
byte-level + defang check, Max matrix + live Helm redteam, Wren full
CRUD regression + greenfield 0026 + 151/151 PG, Eva). Every induced
fault was either handled correctly by this PR's machinery or traced to
pre-existing main code. Remaining work gates **activation, not merge**:
1. **#3643** — unfenced NIP-50 FTS pool (pre-existing #2084; live in
prod today; also explains the blackhole search hang Wren observed — the
routed path was measured bounded at 151-152ms against a silent endpoint,
sqlx `inner.rs:252-255`).
2. **#3651** — reader `statement_timeout`: statements after acquire are
unbounded; mid-transaction blackhole reproduced hanging ≥15s (Dawn).
Small fix via `.after_connect` on the reader pool.
3. **#3622** — CI enforcement of the PG fixtures (with #3619 ordering).
4. **Recovery-conflict cancellation live test** — standby cancels a
routed read mid-snapshot → complete writer page, never partial (#3651's
fix also bounds the cancellation-never-arrives shape).
5. **DDL replication lag test** — migration on writer + paused replay +
budget on → fallback, not client-visible SQL errors.
6. **Reader-tx soak** — 30-60min mixed load; no idle-in-tx accumulation
on the standby.
Then Max's three deployment tests (reader-down latency, reader-down cold
boot, paused-replay read-your-own-write search) re-run on the fixed
binary before the first nonzero budget on bb-block.
---------
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
|
||
|
|
788b3c002b |
fix(git): channel binding tooling + author remediation for unbound repos (#3626)
Closes #3527. Repos announced via vanilla NIP-34 (kind:30617 without a `buzz-channel` tag) 404 forever: the SEC-005 read gate requires a channel-membership ACL, and nothing tells the author why or how to fix it. Per the ruling in the originating thread, this ships **bind/rebind tooling plus a narrow author-only remediation carve-out** — the shelved owner-circle approach is intentionally absent. ## Relay - **`api/git/binding.rs` (new):** shared tri-state binding resolver — `Bound(uuid)` / `NotBound` / `Broken`. First-tag, fail-closed: a malformed `buzz-channel` tag is `Broken`, never conflated with "no tag". Both gates use it. - **Read gate (`transport.rs`):** a **never-bound** repo read by **its own announcement author** still returns 404 (status byte-identical to the generic denial) but the body carries remediation: `run: buzz repos bind --id <repo> --channel <channel-uuid> — …`. This leaks nothing — the author announced the repo, and only the author can rebind (30617 is keyed by `(author, d)`). `Broken` bindings stay generic-denial for everyone, including the author (revocation shape). Bound-to-nonexistent-channel stays generic (phase 1; ingest validation is phase 2). - **Push gate (`policy.rs`):** unbound denial now returns `GIT_NO_CHANNEL_BINDING_BODY`. A deploy-skew test pins that the body carries both the new token (`no_channel_binding`) and the legacy phrase (`"no channel binding"`) so already-shipped desktops keep matching. **(Review r1, blocker 2)** `Broken` no longer collapses into "unbound": it denies 403 `invalid channel binding` for *everyone — including the announcement owner —* **before** the owner short-circuit, matching the read gate's fail-closed posture. The remediation token stays NotBound-only. - **`ingest.rs`:** side-effect failure `warn!` → `error!` — prod runs `RUST_LOG=error`, so these failures were invisible during triage. ## Contract - **`buzz-core/git_perms.rs`:** `GIT_NO_CHANNEL_BINDING_TOKEN` / `GIT_NO_CHANNEL_BINDING_BODY` consts as the declared cross-component contract; relay tests and desktop matcher both build on them. ## CLI - **`buzz repos bind --id <repo> --channel <uuid>`** — rebinds an existing announcement, preserving other tags. - **(Review r1, blocker 1)** **`--channel` on `buzz repos create`** — optional; injects exactly one shape-validated `buzz-channel` tag at creation via a pure `build_create_announcement` builder, so the primary create command stops producing repos the relay 404s. UUID existence/membership stays the relay's authority at git-access time (same TOCTOU posture as `repos bind`). Overlaps with #3594 (open, head 6bbe38459) — happy to reconcile whichever lands first; this branch also carries the bind path and tag preservation. ## Desktop - **Rust:** new `commands/project_git_merge_error.rs` (extracted from `project_git_workflow.rs` to respect the 1000-line ratchet); maps the token to a structured `no_channel_binding` error carrying the bind command. - **TS:** new `features/projects/lib/projectBranchErrors.ts` + tests — dual matcher (new token AND legacy spaced phrase); `ProjectBranchDialogs.tsx` uses it. ## Tests / verification (at head |
||
|
|
7012d86d52 |
feat: configure S3 URL addressing style (#3400)
## Summary - add one strict `BUZZ_S3_ADDRESSING_STYLE=path|virtual` setting shared by media and Git/CAS storage - preserve path-style defaults for bundled Compose/Helm MinIO while supporting Railway's virtual-hosted bucket contract - fail startup on invalid or non-Unicode values before dependency connection, and validate the Helm value with the same two choices - document operator mappings and why endpoint and bucket remain separate for routing and SigV4 signing ## Best-practice rationale AWS documents both URL forms and favors virtual-hosted addressing for S3, while compatibility endpoints such as the bundled MinIO deployment can require path style. `rust-s3` defaults to virtual/subdomain addressing and provides `with_path_style()` for the explicit compatibility case. Some providers buckets only support as virtual-hosted bucket styles. This PR therefore uses one explicit, provider-neutral switch rather than endpoint heuristics or fallback behavior, while retaining `path` as Buzz's backward-compatible default. Sources: - https://docs.aws.amazon.com/AmazonS3/latest/userguide/VirtualHosting.html - https://docs.rs/rust-s3/0.37.0/s3/bucket/struct.Bucket.html - https://docs.railway.com/storage-buckets#url-style - https://github.com/minio/minio/blob/master/docs/config/README.md#domain ## Validation - `cargo fmt --all` - `cargo check --workspace --all-targets` - targeted `buzz-media` and `buzz-relay` parsing/client-construction tests for defaults, strict errors, and both URL styles - Helm unittest: 45/45 passed - Compose config/render validation passed - local MinIO path-mode relay startup passed the Git A3 conformance probe and became ready - unreachable object storage failed startup and readiness never opened - push hooks completed the broader Rust and desktop suites successfully --------- Signed-off-by: npub122y0pqkertljmedu303rl0aqrj3w8pvu43t6jxm6875lzg6f2pwqegc3xc <5288f082d91aff2de5bc8be23fbfa01ca2e3859cac57a91b7a3fa9f12349505c@buzz.block.builderlab.xyz> Signed-off-by: Kalvin Chau <kalvin@block.xyz> Co-authored-by: npub122y0pqkertljmedu303rl0aqrj3w8pvu43t6jxm6875lzg6f2pwqegc3xc <5288f082d91aff2de5bc8be23fbfa01ca2e3859cac57a91b7a3fa9f12349505c@buzz.block.builderlab.xyz> |
||
|
|
ab55fee818 |
feat: add first-class OpenRouter provider support (#1975)
## Summary
First-class `Provider::OpenRouter` support joining the existing
anthropic/openai/databricks providers. Reuses the Chat Completions path
with targeted mutations for OpenRouter's routing contract.
**Core (`crates/buzz-agent`):**
- `Provider::OpenRouter` enum variant with `OPENROUTER_API_KEY`,
`BUZZ_AGENT_MODEL` → `OPENROUTER_MODEL` fallback, `OPENROUTER_BASE_URL`
env convention
- Body mutator: `reasoning: {effort}` when effort is configured, and
`max_completion_tokens` translated to OpenRouter's `max_tokens`
spelling; no `provider.require_parameters` filter (it routes only to
endpoints advertising every parameter in the body, which hard-404s a
valid model id); summaries get neither. `openai_body` is always called
with `effort=None` on the OpenRouter path — the `reasoning` object is
added by the mutator directly, so `reasoning_effort` is structurally
absent.
- Attribution headers: `HTTP-Referer: https://github.com/block/buzz`,
`X-OpenRouter-Title: Buzz`
- Error-inside-200 check in shared `parse_openai` (`finish_reason ==
"error"`)
- 401 auth handling: static API keys (`refresh_now` returns the same
token) fail terminal immediately with one wire request; PKCE/minting
sources get one retry with the fresh token.
- Status+`error_type` retry matrix (4-arm collapsed form): 429 (honor
`Retry-After`), 502 (retry), 503/`provider_overloaded` (honor
`Retry-After`), everything else including untyped 503 (bounded retries →
actionable routing message). 499 included matching shared `post()`
(#2175) for turn-timeout stall surfacing. Terminal failures wrapped in
`terminal_llm_error` for duration+attempt-count context.
- `anthropic/*` `cache_control` injection (model-gated, mixed-content
safe)
- Provider-agnostic `reasoning_details` opaque round-trip on
`HistoryItem::Assistant` for tool-call continuations — captured verbatim
in `parse_openai_with_reasoning_details`, replayed verbatim in
`openai_body`, byte-accounting charged. `provider_extra` passthrough
from `make_tool_call` composes independently.
**Desktop:**
- Readiness arms checking `OPENROUTER_API_KEY` + `OPENROUTER_MODEL`
- Model discovery via `{OPENROUTER_BASE_URL}/models` filtered on
`supported_parameters` contains `tools`
- Picker entry, credential config, effort table 3-file sync
**`desktop/src/features/agents/AGENTS.md`: no rules changed** — the
scoped rule requiring an explicit note is satisfied here.
Implements the gate-cleared plan from
`PLANS/OPENROUTER_PROVIDER_PLAN.md` (rev 3).
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
|
||
|
|
f95fdc1a10 |
feat(agent,acp): wire provider total_tokens through NIP-AM publish chain (#3593)
## What Wires genuine provider-reported `total_tokens` through the full buzz-agent → buzz-acp publish chain so kind-44200 events carry real per-turn and cumulative totals for OpenAI-backed models, while preserving all existing behaviour for Anthropic and external harnesses (goose, claude-code). ## Why Live prod data showed 0 of 1,934 archived reports carry `totalTokens`. Both hardcoded `total_tokens: None` in `pool.rs` and the absent field in `buzz-agent`'s parser are root causes. This is the backend half of a two-track fix; the display-fallback half lands in [#2035](https://github.com/block/buzz/pull/2035). ## Changes **`crates/buzz-agent/src/types.rs`** - Added `total_tokens: Option<u64>` to `LlmResponse` with an explicit doc comment that NIP-AM forbids deriving it. - Added `TurnTotalState` enum (`Unseen | Exact(u64) | Unknown`) with `fold()` and `exact_value()` — the tri-state accumulator that distinguishes not-yet-observed from permanently poisoned. **`crates/buzz-agent/src/llm.rs`** - `parse_responses` and `parse_openai`: read `usage.total_tokens` from OpenAI Chat Completions (including Databricks routes) and the Responses API via `sum_usage`. - Anthropic: explicit `total_tokens: None` — no genuine total available; NIP-AM forbids summing categories. **`crates/buzz-agent/src/agent.rs`** - Added `turn_total_state: &'a mut TurnTotalState` to `RunCtx`. - Fold `response.total_tokens` into the accumulator after each usage-bearing response; non-usage-bearing responses (keepalive/stream frames) do not poison. **`crates/buzz-agent/src/lib.rs`** - Added `accumulated_total_state: TurnTotalState` to `Session` (default `Unseen`). - Per-turn state passed to `RunCtx`, folded into session cumulative after each turn. - Emits `accumulatedTotalTokens` in `usage_update` only when cumulative is `Exact(n)`. **`crates/buzz-acp/src/usage.rs`** - Added `accumulated_total_tokens: Option<u64>` (serde default) to `UsageUpdatePayload` — optional for goose compat. - Added `last_total: Option<u64>` to `SessionState`. - Added `turn_total_tokens` and `cumulative_total_tokens` to `TurnUsage` (field-local — never affect `delta_reliable`). - Derive turn-total delta only when prev and current are both `Some` and monotonic; absence, decrease, or no baseline leaves only the total delta null without touching input/output reliability. **`crates/buzz-acp/src/pool.rs`** - Replaced both hardcoded `total_tokens: None` in `publish_agent_turn_metric` with `usage.turn_total_tokens` and `usage.cumulative_total_tokens`. ## Tests 20 new tests across the four touched files: | File | Tests | |------|-------| | `types.rs` | `TurnTotalState` fold, accumulation, exact_value, default (7 tests) | | `llm.rs` | Chat present/absent, Responses present/absent, Anthropic always-None (5 tests) | | `usage.rs` | First turn no baseline, second-turn delta, cumulative decrease (field-local), current absent, goose-shaped deserialization, baseline absent (6 tests) | | `pool.rs` | Exact turn+cumulative mapping, null totals never derived (2 tests) | `cargo test -p buzz-acp -p buzz-agent` — all passing, 0 failures. ## Scope Boundary: `crates/buzz-agent/**` + `crates/buzz-acp/**` only. Desktop unchanged. `costUsd` explicitly out of scope. Related: [#2035](https://github.com/block/buzz/pull/2035) --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1g8493u0xfsjrvflg4n08ezd7vec99mnwzlv0qgwpr9d7gvjwhuzqx59rhw <41ea58f1e64c243627e8acde7c89be667052ee6e17d8f021c1195be4324ebf04@buzz.block.builderlab.xyz> |
||
|
|
3e48f1b236 |
chore(release): release Buzz Desktop version 0.5.2 (#3624)
## Buzz Desktop release v0.5.2 ### Changes since v0.5.1: - feat(cli): mirror Desktop mention delivery ([#3330](https://github.com/block/buzz/pull/3330)) ([`7adc46268`](https://github.com/block/buzz/commit/7adc46268d5e93f0b1d4dc8e700af22815dcac1b)) - fix(desktop): deduplicate relay outage notification ([#3579](https://github.com/block/buzz/pull/3579)) ([`66e705492`](https://github.com/block/buzz/commit/66e7054928cc29395f828467c3e8c81b7408dd29)) - fix(desktop): reconcile thread arrivals at bottom ([#3585](https://github.com/block/buzz/pull/3585)) ([`b42a8d447`](https://github.com/block/buzz/commit/b42a8d447e3a2b85b2313dc4fdd123731fd8bba3)) - Improve emoji autocomplete matching ([#3571](https://github.com/block/buzz/pull/3571)) ([`259de6afb`](https://github.com/block/buzz/commit/259de6afbe0cc0d106e57ebdb2323064990e4122)) - Fix shared agent avatar import profiles ([#3578](https://github.com/block/buzz/pull/3578)) ([`324bd6b46`](https://github.com/block/buzz/commit/324bd6b464de5751e12abbd155376046ce3d2afc)) - Fix inline raster avatars in agent catalog ([#3581](https://github.com/block/buzz/pull/3581)) ([`7e9b77f72`](https://github.com/block/buzz/commit/7e9b77f72d82e019a99f074f1c9829be30c57ae1)) - feat(agent): make Gemini and MLflow-route models usable through databricks_v2 ([#3569](https://github.com/block/buzz/pull/3569)) ([`4a1ebf25c`](https://github.com/block/buzz/commit/4a1ebf25c782fc6a68f0a69e6f866f793a259a1f)) **To release:** merge this PR. The tag and build will happen automatically. Signed-off-by: Wes <wesbillman@users.noreply.github.com>v0.5.2 |
||
|
|
b18e559ae2 |
docs: add Linux rendering troubleshooting guide (#3573)
## What Adds `docs/linux-rendering-troubleshooting.md` — the user-facing troubleshooting page for Linux rendering failures. ## What's in the doc **Crash: `colrv1_configure_skpaint` assertion abort (AppImage, Fedora 40+)** Root cause: the AppImage bundles WebKitGTK compiled against FreeType 2.11.1, but `libfreetype.so.6` is not bundled — WebKit loads the host's FreeType at runtime. FreeType 2.13.0 added a field to `FT_ColorStopIterator` (16 → 20 bytes); on hosts with FreeType ≥ 2.13 the struct-layout mismatch corrupts Skia's COLRv1 color-stop arithmetic, causing the assertion abort. Fix: upgrade to v0.5.2+ (build container bumped to `ubuntu:24.04` in [#3602](https://github.com/block/buzz/pull/3602)). Includes the glibc floor table (2.35 → 2.39) and `.deb`/`.rpm` guidance for Ubuntu 22.04 / Debian 12 users. A manual fontconfig workaround is preserved for users stuck on older AppImages. **Blank window / dmabuf renderer (NVIDIA, AppImage)** Covers the auto-fix shipped in v0.5.1 ([#3271](https://github.com/block/buzz/pull/3271)) and the `--safe-rendering` flag for cases where auto-detection misses. **AMD RDNA4 / transparent window ([#2643](https://github.com/block/buzz/issues/2643))** Documents the three-variable workaround verified by the reporter (`GDK_BACKEND=x11`, `WEBKIT_DISABLE_DMABUF_RENDERER=1`, `WEBKIT_SKIA_ENABLE_CPU_RENDERING=1`). Also includes a crash-log capture recipe and issue-filing checklist. Context: [#2548](https://github.com/block/buzz/issues/2548), [#2982](https://github.com/block/buzz/issues/2982), [#2643](https://github.com/block/buzz/issues/2643), [#2338](https://github.com/block/buzz/issues/2338). Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
5aeed7c7a2 |
fix(desktop): discover bun-installed agent CLIs in ~/.bun/bin (#3343)
## Problem `common_binary_paths()` probes mise shims, `~/.local/bin`, volta, asdf, and (further down `resolve_command_uncached`) nvm's default bin dir — but not bun's global bin directory, `~/.bun/bin`. bun's installer appends its bin dir to `~/.zshrc` / `~/.bashrc`, which are **interactive**-only. A login shell never sources them, so `find_via_login_shell()` can't recover the path either. That's the same failure mode already called out in this file for nvm: ```rust // Check nvm's default Node.js bin directory — nvm initializes via // ~/.zshrc (interactive) which is not loaded by a login shell, so // `node`, `npm`, and npm-global shims installed there are otherwise // invisible. ``` So for a GUI-launched desktop app, every rung of the resolution ladder misses a bun-installed CLI: 1. workspace dev dirs — no 2. `command_looks_like_path` — no, presets use bare names 3. Buzz-managed npm/node dirs — no 4. current process PATH — launchd's minimal PATH on a Finder launch 5. `find_via_login_shell` — `.zshrc` not sourced 6. `common_binary_paths()` — **`~/.bun/bin` absent** 7. nvm default bin — no This matters because bun is a common install route for the agent CLIs Buzz targets. Kimi Code in particular ships as an npm package (`@moonshot-ai/kimi-code`), so `bun add -g` puts it at `~/.bun/bin/kimi` — exactly where discovery doesn't look. ## Reproduction On macOS with `codex` and `kimi` installed via bun, launching Buzz from Finder: - Kimi Code shows **"CLI needed"** - both CLIs run fine in an interactive terminal Probing the way `find_via_login_shell` does, in a clean environment: ```console $ env -i HOME=$HOME /bin/zsh -l -c 'command -v -- codex; command -v -- kimi' (nothing) ``` Launching the app with the bun dir on PATH resolves both immediately: ```console $ env PATH="$HOME/.bun/bin:$PATH" /Applications/Buzz.app/Contents/MacOS/buzz-desktop ``` ## Change One entry appended to the home-relative list in `common_binary_paths()`. It goes **last** so it cannot shadow a directory that already resolves — the change can only add resolutions, never alter existing ones. ## Testing `cargo fmt --check` passes. I was not able to run the full `just ci` gate locally: `ring 0.17.14` fails to build in this environment against the macOS 26.2 SDK (`cc` error compiling `p256-nistz.c`), which is unrelated to this change. Relying on CI for the rest — the diff adds one `PathBuf` to an existing `Vec<PathBuf>` and introduces no new API. ## Notes - Related to #3084, which adds `~/.kimi-code/bin` for the same class of GUI-launch discovery failure. That covers Kimi's standalone installer; this covers the bun/npm-global install route. They're complementary — I've left a note on that PR. - Only `~/.bun/bin` is added. bun's global packages live under `~/.bun/install/global/node_modules` but are symlinked into `~/.bun/bin`, so the single directory is sufficient. - Worth noting `~/.bun/bin` contains no `node`/`npm`/`npx`, so appending it can't shadow a system Node toolchain. Signed-off-by: Xule Lin <43122877+linxule@users.noreply.github.com> |
||
|
|
005b5b819a |
feat(tracing): correlate trace IDs in relay logs (#3608)
## Summary Correlates trace + span IDs with logs, allowing traces and logs to be bridged seamlessly ### Related issue none found ### Testing Unit tests Signed-off-by: David Grochowski <dgrochowski@squareup.com> Co-authored-by: Amp <amp@ampcode.com> |
||
|
|
581baa6254 |
chore(ci): bump Linux AppImage build container to ubuntu:24.04 (#3602)
## What and why The Buzz AppImage is built on `ubuntu:22.04`, which links WebKitGTK against FreeType 2.11.1. Because `libfreetype.so.6` is on the linuxdeploy community excludelist, the bundled WebKit loads the **host's** FreeType at runtime instead of the bundled one. FreeType 2.13.0 (released 2023-02-09) added `FT_Bool read_variable` to `FT_ColorStopIterator`, growing the struct from 16 to 20 bytes. Any host running FreeType ≥ 2.13 (Fedora 42+, Ubuntu 24.04+) has a struct-layout mismatch with the 22.04-compiled WebKit. The mismatched offsets corrupt color-stop index arithmetic inside Skia's COLRv1 renderer, producing the assertion abort in issues #2548 and #2982: ``` stl_vector.h:1123: Assertion '__n < this->size()' failed. ... colrv1_configure_skpaint(FT_Face, ...) ... ``` ## Fix Bump the build container to `ubuntu:24.04` (noble), which ships FreeType **2.13.2**. Noble's struct layout matches every crash-affected host. The ABI mismatch disappears and the crash is eliminated at root. WebKitGTK also advances from **2.50.4** (jammy backport) to **2.52.3** (noble backport). ## Glibc floor change | Build base | glibc floor | Oldest supported AppImage distro | |---|---|---| | ubuntu:22.04 (before) | 2.35 | Ubuntu 22.04 LTS, Debian 12 | | ubuntu:24.04 (after) | 2.39 | Ubuntu 24.04 LTS, Fedora 40+ | Ubuntu 22.04 LTS and Debian 12 users lose AppImage support. Both distributions continue to receive first-class `.deb` / `.rpm` packages, which use the system WebKit and are unaffected. The crash-affected users (Fedora 42/44, Ubuntu 24.04+) all have glibc ≥ 2.39. ## Changes - `.github/workflows/linux-canary.yml:24` — container pin updated to `ubuntu:24.04@sha256:4fbb8e6a…` - `.github/workflows/release.yml:479` — same container pin updated - `.github/workflows/release.yml:501` — comment version string updated from 22.04 to 24.04 `fix-appimage.sh` and `desktop/src-tauri/**` are untouched. The #3573 fontconfig stopgap remains active; retirement is a separate follow-on PR once this fix is verified on a shipped build. ## Sequencing `docs/linux-rendering-troubleshooting.md` (introduced in #3573) will receive a glibc-floor callout section once #3573 merges — adding it here would conflict with #3573's open branch. Context: #2548, #2982. Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
7adc46268d |
feat(cli): mirror Desktop mention delivery (#3330)
🤖 ## Summary Agent-authored mentions currently depend on matching visible `@Name` text to channel profiles. That makes notification delivery ambiguous when names collide or profiles change, and it encourages an extra post-send lookup just to confirm that the intended `p` tags were emitted. This change makes `buzz messages send` mirror Desktop's existing model: the message keeps a readable name in its content while the recipient pubkey is supplied separately. ```bash buzz messages send \ --channel <UUID> \ --content '@Alice could you review this?' \ --mention <alice-hex-or-npub> ``` `--mention` is repeatable. The CLI normalizes and deduplicates explicit pubkeys, merges them with any names it can resolve from the channel, and gives explicit identities priority under the existing 50-mention limit. Before uploading attachments, signing, or publishing, the command checks every resulting pubkey against the channel's current membership: - Members are mentioned normally. - Non-members stop the send and produce an actionable error. - `--allow-non-member-mentions` deliberately sends notifying `p` tags without adding anyone to the channel. Sending a message never changes membership. On success, `mention_pubkeys` is read from the exact signed event and returned with the relay response, so callers can verify the emitted recipients without another query. Managed-agent guidance teaches this single-command mention flow. Desktop mention behavior and the Nostr event schema are unchanged. Forum guidance is intentionally handled separately in #3596. ### Related issue None found. This replaces the earlier guidance-only approach in this PR with the underlying CLI behavior it required. ### Testing - `cargo test -p buzz-sdk` - `cargo test -p buzz-cli` - `cargo test -p buzz-acp` - `cargo test --manifest-path desktop/src-tauri/Cargo.toml` --------- Signed-off-by: npub1fdupjvyregj3z2tx7gx5x6py04zw89jm5usef9lyea4f3vcgh8qq9zgkdz <4b78193083ca25112966f20d4368247d44e3965ba7219497e4cf6a98b308b9c0@buzz.block.builderlab.xyz> Signed-off-by: npub13n66s06epmqf2kc3v373ez8hj65cuzyvxzjf93vwpervxqn2u7jq2qd9je <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz> Co-authored-by: npub1fdupjvyregj3z2tx7gx5x6py04zw89jm5usef9lyea4f3vcgh8qq9zgkdz <4b78193083ca25112966f20d4368247d44e3965ba7219497e4cf6a98b308b9c0@buzz.block.builderlab.xyz> Co-authored-by: npub13n66s06epmqf2kc3v373ez8hj65cuzyvxzjf93vwpervxqn2u7jq2qd9je <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz> |
||
|
|
66e7054928 |
fix(desktop): deduplicate relay outage notification (#3579)
🤖 ## Summary - Keep the relay reconnect notification dismissed across repeated connection retries during one continuous outage. - Re-arm the notification after recovery or relay lifecycle replacement, including switches between communities that use the same relay URL. - Preserve the existing dedicated path for authentication and other application-level errors. ### How an outage is tracked The AppShell-owned relay-card hook treats an outage as one contiguous runtime episode rather than assigning it a persisted ID. A hook-local `outageActiveRef` is armed by the first qualifying unreachable/degraded observation. While it is armed, intermediate retry states (`connecting`, `reconnecting`, `stalled`, and `disconnected`) belong to that same episode, so retry churn cannot clear dismissal or emit another notification. The hook receives the same lifecycle identity used by community initialization: community ID plus `reinitKey`. This distinguishes multiple communities even when they share a relay URL, and it also changes when the active community is explicitly reinitialized. The latch and dismissal are reset when that identity changes or when the relay singleton reports its authoritative `idle` teardown state. A successful `connected` state also closes the episode and re-arms the next outage. These boundaries deliberately bias toward re-notifying rather than suppressing a later outage: recovery, community switch/reinit, or relay teardown cannot leave the hook stuck believing an old outage is still active. No outage state is persisted beyond the mounted hook lifecycle. ### Related issue None found. ### Testing - `pnpm --dir desktop typecheck` - `pnpm --dir desktop test` — 3,769 passed - `pnpm --dir desktop check` - `pnpm --dir desktop build:e2e` - `pnpm --dir desktop exec playwright test tests/e2e/sidebar-relay-card.spec.ts --project=integration` — 11 passed --------- Signed-off-by: Larry <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz> Co-authored-by: npub13n66s06epmqf2kc3v373ez8hj65cuzyvxzjf93vwpervxqn2u7jq2qd9je <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz> |
||
|
|
b42a8d447e |
fix(desktop): reconcile thread arrivals at bottom (#3585)
## Summary - reconcile stale native-scroll anchors when a reply arrives at the physical floor - clear the thread new-message affordance instead of incrementing it from stale cached state - preserve the existing mid-history path and add direct lifecycle regression coverage ## Why PR #3411 fixed geometry-driven reconciliation, but the reply-arrival branch still trusted a cached `message` anchor without checking the rendered position. Native anchoring could return a short thread to the floor without another scroll/resize callback, then the next reply incremented the pill anyway. ## Verification - Desktop checks passed - Desktop typecheck passed - focused lifecycle test passed (6/6) - push hook full Desktop unit suite passed (3,770/3,770) - `git diff --check` passed Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> |
||
|
|
047533c56c |
fix(mobile): keep TLS on relays joined by invite (#3139)
## Summary Communities joined via an invite link never connect: the app dials `ws://` on port 80 instead of `wss://` on 443 and sits on "Reconnecting…" indefinitely. `RelayConfig.baseUrl` is documented as an HTTP origin, but the two onboarding flows disagree on what they persist: - **Device pairing** validates and stores `https://` — `pairing_provider.dart:657` throws on anything else. - **Invite join** stores the relay URL straight off the invite link, and `deep_link.dart:165` always emits `ws://` or `wss://`. `wsUrl` only special-cased `https://`, so a `wss://` base fell through to the plaintext branch: ```dart final scheme = uri.scheme == 'https' ? 'wss' : 'ws'; // 'wss' is not 'https' ``` The claim request itself succeeds, because `_claimUrlFromRelay` (`invite_join_provider.dart:242`) maps `wss → https` explicitly. Only the socket path is missing that conversion — which is why the community appears, correctly named, and then never loads. The same `baseUrl` also feeds `/query` (`relay_session.dart:136`), media upload (`media_upload.dart:765`), Blossom auth (`media_auth.dart:128`) and `relayClientProvider` (`relay_provider.dart:113`), so those requests were malformed too. Where port 80 *does* answer, it is additionally a silent TLS downgrade after `validateInviteRelayUri` insisted on `wss://`. This folds the websocket schemes back to their HTTP equivalents in `baseUrl` itself, so every consumer is correct by construction rather than needing a second getter remembered at each call site, and communities **already persisted** with `wss://` are repaired on read without a migration. `community_icon_provider.dart:46` already performs this same conversion locally. One subtlety worth flagging for review: the normalization is derived in the getter rather than applied in the constructor, so the constructor stays `const`. The compile-time fallback at `relay_provider.dart:77` relies on const canonicalization for a stable identity across rebuilds, and Riverpod's `defaultUpdateShouldNotify` is `previous != next` (`element.dart:361`), which falls back to identity for this class. A `factory` constructor here yields a fresh instance per rebuild, which tears down and resubscribes every listener — `channels_provider_test.dart` catches it as an unexpected unsubscribe during reconnect. ### Related issue Fixes #2662. ### Testing `flutter test` — **705 passed, 1 skipped, 0 failed** `flutter analyze` — No issues found `dart format --set-exit-if-changed .` — 249 files, 0 changed Run against the Hermit-pinned SDK (Flutter 3.41.7 / Dart 3.11.5), matching CI. 10 new unit tests in `mobile/test/shared/relay/relay_config_test.dart` covering both onboarding schemes, `http`/`https` passthrough, non-default ports, and agreement between the invite and pairing paths for the same relay. Verified end-to-end against a self-hosted relay behind `tailscale serve`, which terminates TLS on 443 and leaves port 80 closed. Relay logs show the invite claim succeeding over HTTPS at the moment of joining, while no WebSocket connection ever arrives — no `WebSocket connection established`, no NIP-42 auth, no `kind:0` profile, no push registration — across the relay's entire history, even though the member row is present and correct. Port-80 refusals are not logged by `tailscaled`'s netstack, which is why the retries leave no trace server-side. Reproduced on both iOS and Android. --------- Signed-off-by: Krishna C <github@kumb.uk> |
||
|
|
259de6afbe |
Improve emoji autocomplete matching (#3571)
## Summary - Show all colon emoji autocomplete matches - Rank exact and prefix shortcodes before weaker matches - Add a regression test and screenshot ## Validation - `pnpm test` - `pnpm build` - `pnpm exec playwright test --project=smoke tests/e2e/custom-emoji.spec.ts --grep "exact standard shortcode"` - `just desktop-tauri-clippy` Native Tauri tests were attempted but could not link because the local disk filled during compilation. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> |
||
|
|
324bd6b464 |
Fix shared agent avatar import profiles (#3578)
> Carl is updating this pull request on Wes's behalf. ## Summary - upload an embedded raster avatar through the existing authenticated media pipeline before minting or persisting an imported shared agent - store and publish only the resulting hosted URL so agent kind:0 profiles remain within content limits ## Root cause Snapshot import recovered raster avatar pixels as a large inline base64 data URL. That value was persisted and placed into the agent's kind:0 profile. The relay rejected the oversized profile, so other clients could not resolve the imported agent's avatar. ## Scope This is intentionally the forward fix only. It changes two Desktop files and does **not** add migration or reconciliation behavior for previously imported agents. Existing affected imports must be re-imported or fixed manually. ## Validation - successful pre-push Desktop suite: 1,863 passed, 14 ignored, 0 failed - all pre-push Rust/Desktop gates green, including all-target clippy - valid >256 KiB PNG import → production MIME detection/sanitization → bounded signed kind:0 containing only the hosted URL - upload failure, malformed data, and URL-only avatar cases covered - independent fresh review by Princess Donut: clean, no blocking findings Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> |
||
|
|
7e9b77f72d |
Fix inline raster avatars in agent catalog (#3581)
## Summary - render existing shared personas whose catalog avatar is a bounded inline PNG, JPEG, GIF, or WebP data URL - keep rejecting arbitrary, malformed, unsupported, and oversized `data:` URLs - preserve hosted relay URLs as the forward format; catalog browsing remains read-only ## Root cause Paul's live shared kind:30175 head contains a 144,878-character `data:image/png;base64,...` avatar. The catalog projection accepted HTTP(S) URLs and bounded percent-encoded SVG emoji avatars only, so it projected Paul's avatar to `null` before `ProfileAvatar` rendered it. The owner still saw the local persona avatar, producing the reported owner/viewer mismatch. This patch accepts only four raster MIME types with strict base64 shape and a 256 KiB total URL cap at the existing catalog parsing boundary. It repairs already-signed heads such as Paul without viewer-side uploads or publication side effects. Hosted media remains the canonical forward path. #3578 uploads inline raster avatars during snapshot import, preventing the known source from creating future inline persona/profile values; existing signed catalog heads still need this compatibility path until their owners republish. ## Agent instruction finding The catalog publishes `AgentDefinition.system_prompt` verbatim as the user-authored **Agent instruction**, as documented by the sharing UI and NIP-AP. No Buzz base/core/runtime prompt is concatenated in the publish, catalog, or import path. This PR therefore does not remove authored instructions and accidentally strip copied agents of their behavior. ## Validation - targeted `personaCatalogRelay.test.mjs`: 24 passed - Desktop typecheck: passed - pre-push Desktop frontend suite: 3,771 passed - pre-push Desktop checks: passed - `git diff --check`: passed - independent review: no blockers, 9.4/10 Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> |
||
|
|
ddd468723a |
revert(acp): remove dead GOOSE_ACP_SCHEDULER_DISABLED env injection (#3576)
## Summary [block/buzz#3144](https://github.com/block/buzz/pull/3144) injected `GOOSE_ACP_SCHEDULER_DISABLED=true` into every `AcpClient::spawn` call as a forward-compatible no-op, intended to suppress the cron scheduler in goose ACP children once the matching reader landed in goose. That reader only ever existed in [aaif-goose/goose#10738](https://github.com/aaif-goose/goose/pull/10738), which was closed unmerged. [goose#10781](https://github.com/aaif-goose/goose/pull/10781) (Lifei Zhou, merged 2026-07-29) disables the ACP scheduler by default at the source: `goose acp` now requires `--enable-scheduler` to start a scheduler. Buzz-spawned children therefore get no scheduler with zero configuration — making the `GOOSE_ACP_SCHEDULER_DISABLED` injection permanently dead code. ## What changes Removes from `crates/buzz-acp/src/acp.rs`: - `GOOSE_SCHEDULER_DISABLED_ENV` constant - `cmd.env(GOOSE_SCHEDULER_DISABLED_ENV, "true")` injection in `AcpClient::spawn` - `spawn_injects_scheduler_disabled_env_by_default` test - `spawn_scheduler_disabled_env_overrides_conflicting_extra_env` test - `spawn_and_read_child_env` helper (unreferenced once the two tests above are gone) No other files are affected. ## Why now Leaving dead code that references an env var no reader will ever consume misleads future maintainers about the actual scheduler-isolation mechanism. The isolation is now an upstream default, not a Buzz injection. Reverts: [block/buzz#3144](https://github.com/block/buzz/pull/3144) Related: [aaif-goose/goose#10781](https://github.com/aaif-goose/goose/pull/10781) Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
4a1ebf25c7 |
feat(agent): make Gemini and MLflow-route models usable through databricks_v2 (#3569)
## Summary
Makes Gemini — and every other non-Claude, non-GPT-5 model on the
Databricks MLflow route (`databricks_v2`) — usable in an agent loop.
These are the non-`benchmarks/` changes from
`benchmark/harness-accounting-and-solo`, lifted onto a clean base off
`main` so they can land independently while the harness work continues.
Two defects made these models unusable, one fatal and one silent. Both
live only in `openai_body` / `parse_openai`, which is the
least-exercised of the three `databricks_v2` sub-routes — the
`luna`/`sol` conditions run the Responses route and the `opus`
conditions run the Anthropic route, so **this change is inert for every
model already in use** and only lights up the MLflow path.
## Why a third route at all
`databricks_v2_route_for_model` buckets by model family: `claude*` →
Anthropic Messages, `gpt-5`/code-names → OpenAI Responses, **everything
else → MLflow chat-completions**. Gemini, Qwen, gpt-oss, and friends all
fall through to that third pair — and both bugs below live only there.
## D1 — dropped thought signatures (fatal)
Gemini returns a `thoughtSignature` on every tool call and **requires it
echoed back**. `openai_body` reserialized each call as `{id, type,
function}` only, dropping the field, so the next request 400'd:
```
HTTP 400 Function call is missing a thought_signature in functionCall parts.
```
For a coding agent this fires on the **first** tool call, so the model
never completes a single turn.
**Position is load-bearing.** A four-shape replay probe against the live
gateway established that the signature must sit as a *sibling* of
`function` — nesting it inside `function{}` fails with the *same* 400 as
omitting it. A fix that "preserves the field" without preserving its
position passes a unit test and still 400s.
The fix: `ToolCall` gains `provider_extra: Map<String, Value>`.
`parse_openai` captures every top-level wire key except the three we
model (`id`, `type`, `function`); `openai_body` re-emits them beside
`function`. Keeping *whatever we did not model*, rather than naming
`thoughtSignature`, means the next provider with an opaque per-call
token needs no change here. The Responses and Anthropic replay shapes
are fully modelled, so they pass `Default::default()` and stay
**byte-identical** to before.
### D1b — duplicate tool-call ids (same root cause)
Gemini returns the **function name** as the id, so two parallel calls to
one function arrive sharing an id — and that id is what pairs a
`role:"tool"` result back to its call, making two results
indistinguishable. `dedupe_provider_ids` suffixes collisions
(`get_weather`, `get_weather-2`). Safe because both halves of the
pairing (the assistant `tool_calls[].id` and the result's
`tool_call_id`) are re-emitted from this same value; the provider never
sees its original id again.
## D2 — block-array content discarded (silent, worse than a crash)
`parse_openai` read `content` with `as_str()`, which returns `""` for
anything that isn't a JSON string. Gemini (and Qwen35, gpt-oss) send an
array of typed blocks:
```json
"content": [
{"type": "reasoning", "summary": [{"type": "summary_text", "text": "…"}]},
{"type": "text", "text": "391"}
]
```
So the model answered and the answer was thrown away — no error, no
warning, just a turn that looked like the model had said nothing. On a
benchmark this reads as "Gemini is bad at the task" rather than "buzz
dropped the reply."
`openai_content_parts` now accepts either shape — string as before, or a
block array where `text` blocks concatenate into text and `reasoning`
blocks into reasoning (Gemini nests the prose one level down under
`summary`). Message-level `reasoning_content` / `reasoning` still win
when present, so DeepSeek and vLLM-style hosts are unchanged; block
reasoning is the last fallback.
## Also: a turn-start log line (`buzz-acp` `pool.rs`)
Small, independent observability change that also rides in the
non-benchmark delta: `run_prompt_task` now emits a `pool::prompt` "turn
starting" line, labelled by the same `prompt_label` helper as
`log_stop_reason`, so a log reads as start/stop pairs. An unpaired start
is the only durable evidence that a turn was entered and never returned
— without it, a stalled agent and an agent nobody woke leave identical
(zero-completion) logs.
## Interaction with #3538
#3538 (already merged) rewrote `databricks_v2_route_for_model` to route
by boundary-aware model-family segments. That change and this one touch
**different functions** in `llm.rs` — routing vs. body/parse — and
compose cleanly; the family routing decides *which* pair runs, and this
fixes the MLflow pair it can now select.
## 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 (304 + 632 lib
tests plus integration suites, 0 failures). Five new tests cover:
block-array text extraction, plain-string regression, passthrough
capture (and non-duplication of the modelled keys), replay position
(`thoughtSignature` beside `function`, not inside it), and id
de-duplication.
- Wire evidence: the four-shape replay table and the reasoning-effort
probe were run against `block-lakehouse-staging` (recorded in the design
doc).
## Relationship to the benchmark branch
The full design write-up (four-shape replay table, position-matters
analysis, effort verification, and open pricing item) lives in
`docs/08-gemini-provider-fixes.md` on
`benchmark/harness-accounting-and-solo`. The benchmark manifests and
endpoint-config entries that exercise these models are separable and
stay on that branch.
🤖 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>
|
||
|
|
9beb3b8c6e |
fix(cli): mask credential env values in --help output (#3570)
clap renders live env var values in help text by default. Three args carrying credentials were exposed this way: - `BUZZ_PRIVATE_KEY` in `buzz-cli` (`crates/buzz-cli/src/lib.rs`) - `BUZZ_AUTH_TAG` in `buzz-cli` - `BUZZ_PRIVATE_KEY` in `buzz-acp` (`crates/buzz-acp/src/config.rs`) Add `hide_env_values = true` to each. Env var names remain visible for discoverability; only their runtime values are withheld from `--help` output. Also adds a regression guard in each crate's test module that walks the clap command tree (recursing into subcommands for `buzz-cli`) and asserts every arg whose env var name contains `KEY`, `SECRET`, `TOKEN`, `PASSWORD`, `CRED`, or `AUTH` has `hide_env_values` set. This prevents future credential-bearing args from being added without the masking in place. Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
9752b816a9 |
Serialize Tauri pre-push checks (#3567)
## Summary - combine Desktop Tauri clippy and tests into one pre-push command - run clippy first, then tests - keep unrelated pre-push commands parallel ## Why PR #3555 added clippy as a separate command while the pre-push group uses `parallel: true`. That can start clippy and tests simultaneously against the same Cargo target directory, leaving one command waiting on Cargo's build lock and making pushes appear stalled. Serializing only these two Cargo-heavy checks avoids lock contention while retaining the CI-equivalent clippy command and existing test coverage. ## Validation - `lefthook validate` - forced `desktop-tauri-checks` through Lefthook with an instrumented `just`; observed `desktop-tauri-clippy` followed by `desktop-tauri-test` Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> |
||
|
|
a13085e9ac |
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.com/block/buzz/pull/3463)) ([`c405ad1d4`](https://github.com/block/buzz/commit/c405ad1d4b1da061c11b3d26761252d41dcc62d3)) - feat: add explicit entry for claude-opus-5 in model config ([#2831](https://github.com/block/buzz/pull/2831)) ([`90e058ebf`](https://github.com/block/buzz/commit/90e058ebf68137e048a409aec6616519379ff726)) - fix(desktop): clear stale thread new-message pill ([#3411](https://github.com/block/buzz/pull/3411)) ([`55a3ed7b9`](https://github.com/block/buzz/commit/55a3ed7b9217cee5b23e0a5441947dc929b2a38c)) - fix(ci): ratchet file sizes against the base tree ([#3352](https://github.com/block/buzz/pull/3352)) ([`9227bdf58`](https://github.com/block/buzz/commit/9227bdf58ad6664ae3c1078888f2181ec19c4da4)) - feat(desktop): apply WebKit rendering workarounds at startup on Linux ([#3271](https://github.com/block/buzz/pull/3271)) ([`3ece4461d`](https://github.com/block/buzz/commit/3ece4461df8a7b9663a8e68327483b8377d4086d)) - fix(desktop): stabilize flaky DM expansion E2E ordering assertions ([#2004](https://github.com/block/buzz/pull/2004)) ([`913d564ce`](https://github.com/block/buzz/commit/913d564ce0f35924291bf3eeab6508517a6d8d1f)) - fix(desktop): paint community rail full height ([#3382](https://github.com/block/buzz/pull/3382)) ([`1d3b810ad`](https://github.com/block/buzz/commit/1d3b810ad70d6325718ed91e723f32c4a376d5e1)) - feat(desktop): add custom harness inline from agent dialogs ([#3252](https://github.com/block/buzz/pull/3252)) ([`b0503d80c`](https://github.com/block/buzz/commit/b0503d80c298b1ece3b0a43b41d316829a3379e7)) - feat(desktop): refine agent catalog sharing ([#2439](https://github.com/block/buzz/pull/2439)) ([`a35771fc4`](https://github.com/block/buzz/commit/a35771fc441cdc3c6f517f419037206783b502d2)) - fix(desktop): keep drafts out of the Inbox All view ([#3217](https://github.com/block/buzz/pull/3217)) ([`3afa129ee`](https://github.com/block/buzz/commit/3afa129ee785cc74d921d0ba969254a8255c4cc0)) - fix(desktop): restore the inbox icon in the sidebar ([#3341](https://github.com/block/buzz/pull/3341)) ([`00ede2e7a`](https://github.com/block/buzz/commit/00ede2e7aa7eb95571b7db3ebbd163adbf6cf74e)) - fix(desktop): gate codex-acp on a minimum supported version ([#3254](https://github.com/block/buzz/pull/3254)) ([`4e3998f36`](https://github.com/block/buzz/commit/4e3998f36e36d68b9a93dcbd85f0864450bb8f5f)) - feat(cli): add users set-status command for NIP-38 profile status ([#3253](https://github.com/block/buzz/pull/3253)) ([`60158fce3`](https://github.com/block/buzz/commit/60158fce3e670f11bb35d42627857ccaea50ff06)) - fix(composer): scope multiline block formatting ([#3246](https://github.com/block/buzz/pull/3246)) ([`5457c947a`](https://github.com/block/buzz/commit/5457c947a74f5ba4b979f9c6411aa7626a858387)) **To release:** merge this PR. The tag and build will happen automatically. Signed-off-by: Wes <wesbillman@users.noreply.github.com>v0.5.1 |
||
|
|
51bb97d2be |
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> |
||
|
|
294c8c821d |
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> |
||
|
|
f7a3988ba1 |
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> |
||
|
|
4555899ab2 |
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> |
||
|
|
6438dedf83 |
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> |
||
|
|
24d90d1280 |
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> |
||
|
|
ce01e930ed |
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> |
||
|
|
c405ad1d4b |
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> |
||
|
|
485d03a358 |
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> |
||
|
|
6300a6b1d0 |
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> |
||
|
|
22be8bb351 |
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> |
||
|
|
90e058ebf6 |
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> |
||
|
|
55a3ed7b92 |
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> |
||
|
|
9227bdf58a |
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> |
||
|
|
826bed4821 |
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>
|
||
|
|
12d63c67be |
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>chart-v0.1.7 |
||
|
|
f25e6dd6aa |
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>
|
||
|
|
3ece4461df |
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> |
||
|
|
913d564ce0 |
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> |
||
|
|
4fcd55a999 |
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> |
||
|
|
1d3b810ad7 |
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> |