Commit Graph
2198 Commits
Author SHA1 Message Date
Duncan e4abdf997a Merge remote-tracking branch 'origin/main' into duncan/workspace-scoped-agent-store-merge
* origin/main: (24 commits)
  fix(reactions): support max-length custom emoji (#3833)
  feat(desktop): allow leaving your final community (#3621)
  fix(buzz-agent): recover from context-window 400s instead of sticking (#4946)
  docs(persona-pack): fix stale desktop import instructions (#4500)
  fix(desktop): route macos notification clicks (#4799)
  feat(mobile): sync themes per community (#3767)
  feat(desktop): sync themes per community (#3653)
  feat(desktop): cap OpenClaw agent parallelism at 5 (#4019)
  fix(buzz-agent): scope handoff cap per turn, not per session lifetime (#4805)
  Fix mobile message timeline bounce (#4862)
  Polish mobile bottom sheets and profile cards (#4911)
  Fix media attachment actions (#4849)
  fix(desktop): remove join API token control (#4897)
  fix(desktop): allow shared agent mentions (#4913)
  Polish mobile top navigation (#4778)
  fix(release): tag immutable desktop candidates (#4811)
  fix(channels): restrict private-channel invitations (#4612)
  fix(acp): reject unattended permission requests (#4609)
  fix(workflow): bind trigger author to the signed event (#4607)
  fix(git): revoke access for banned relay members (#4608)
  ...

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>

# Conflicts:
#	desktop/src-tauri/src/managed_agents/runtime.rs
2026-08-05 17:32:58 -04:00
2ea9385015 fix(reactions): support max-length custom emoji (#3833)
**Category:** fix
**User Impact:** Custom emoji with valid 64-character names can now be
used as reactions without errors.

**Problem:** Buzz accepted 64-character custom emoji names during
registration, but rejected them as reactions after the required
surrounding colons made the payload 66 characters. Validation also
differed between desktop, SDK, relay, and storage boundaries.

<img width="554" height="47" alt="image"
src="https://github.com/user-attachments/assets/4013452f-210e-4dd3-9003-f45ff3b28dc8"
/>

**Solution:** Keep the product limit at 64 ASCII characters for custom
emoji names, enforce it consistently when emoji sets are registered, and
allow only valid matching custom reaction payloads up to 66 characters.
Widen the reaction projection to preserve the wrapped payload while
retaining the existing 64-character limit for ordinary reactions.

<details>
<summary>File changes</summary>

**crates/buzz-sdk/src/builders.rs**
Defines the shared custom emoji boundaries and covers accepted
64-character and rejected 65-character shortcodes.

**crates/buzz-relay/src/handlers/ingest.rs**
Validates emoji-set shortcodes and permits 66-character reactions only
when they are valid colon-wrapped custom emoji with a matching tag.

**crates/buzz-db/src/event.rs**
Adds storage regression coverage for maximum-length custom emoji
reactions.

**crates/buzz-db/src/migration.rs**
Verifies the reaction column migration is applied correctly.

**desktop/src/shared/api/customEmoji.ts**
Enforces the existing 64-character shortcode maximum during desktop
normalization and registration/import.

**desktop/src/shared/api/customEmoji.test.mjs**
Covers the desktop shortcode boundary.

**migrations/0027_long_reaction_payloads.sql**
Widens stored reaction payloads to 66 characters for the two required
surrounding colons.

**schema/schema.sql**
Keeps the desired schema aligned with the migration.

</details>

## Reproduction Steps

1. Register or import a custom emoji whose ASCII shortcode is exactly 64
characters.
2. Select that emoji as a reaction to a message.
3. Confirm the reaction publishes, persists, and renders without an
error.
4. Attempt to register a 65-character shortcode and confirm it is
rejected.
5. Publish an ordinary or malformed reaction over 64 characters and
confirm the relay rejects it.

## Verification

- `cargo test -p buzz-sdk`: 243 passed
- `cargo test -p buzz-db`: 94 passed, 152 Postgres-required tests
ignored
- `pnpm test` in `desktop`: 3,859 passed
- `cargo test -p buzz-relay`: 795 passed, 9 existing
Postgres-unavailable failures, 35 ignored; new reaction boundary tests
pass directly
- `cargo fmt --all -- --check`
- `git diff --check`

Originating Buzz channel: `f2ec9671-d78e-4cde-894c-9f4c458c7f1f`

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
2026-08-05 21:02:57 +00:00
719f9730d4 feat(desktop): allow leaving your final community (#3621)
**Category:** improvement
**User Impact:** People can leave their final Buzz community and return
to **Join or create a community** without losing their signed-in
identity.

**Problem:** Buzz Desktop blocked people from leaving when only one
community remained. Its existing remove action also changed local
configuration without ending relay membership.

**Solution:** Allow the final community to be left. Buzz now asks the
relay to end membership, removes the community locally only after
acceptance, and returns the person to the community selector while
keeping their identity signed in. If other communities remain, Buzz
switches to one of them. Relay rejection or timeout keeps the community
in place and shows an actionable retry error.

<details>
<summary>File changes</summary>

**desktop/src/features/communities/leaveCommunity.ts**
Adds signed kind 28936 publishing for active and inactive community
relays with actionable timeout handling.

**desktop/src/features/communities/leaveCommunity.test.mjs**
Covers event shape, relay selection, acceptance gating, rejection,
timeout messaging, and cleanup.

**desktop/src/features/communities/useCommunities.tsx**
Allows final-community removal and clears community-specific storage
without touching identity.

**desktop/src/features/communities/resolveCommunityRemoval.test.mjs**
Covers final, active, and inactive community removal state transitions.

**desktop/src/app/useCommunityNavigationTransitions.ts**
Gates local removal on relay acceptance and routes to a fallback
community or setup selector.

**desktop/src/app/AppShell.tsx**
Passes the asynchronous leave operation through shell entry points.

**desktop/src/features/communities/ui/EditCommunityDialog.tsx**
Replaces the local-only remove action with a pending-aware Leave
Community action that retains actionable errors.

**desktop/src/features/communities/ui/CommunitySwitcher.tsx**
Enables leaving the final community and carries the asynchronous
callback.

**desktop/src/features/sidebar/ui/AppSidebar.tsx**
Carries the asynchronous leave callback through sidebar props.

**desktop/src/features/sidebar/ui/CommunityRail.tsx**
Enables leaving the final community from rail settings.

**desktop/src/features/sidebar/ui/SidebarProfileCard.tsx**
Carries the asynchronous leave callback through profile community
settings.

**desktop/src/testing/e2eBridge.ts**
Teaches the mock relay to accept NIP-43 leave events.

**desktop/tests/e2e/community-rail.spec.ts**
Updates leave interactions and verifies final-community setup
navigation, storage cleanup, and identity preservation.

</details>

### Reproduction steps

1. Run Buzz Desktop with a signed-in identity and one joined community.
2. Open Community settings and choose **Leave Community**.
3. Confirm the app shows **Join or create a community** and the existing
identity remains signed in.
4. Repeat with two communities and confirm leaving the active one
switches cleanly to the remaining community.
5. Reject or withhold the relay `OK` response and confirm the community
remains configured with an actionable error in the dialog.

### Test plan

- `pnpm check`
- `pnpm build`
- `pnpm test` (3,913 passing)
- `pnpm build:e2e && pnpm exec playwright test
tests/e2e/community-rail.spec.ts --grep "final community"`


<img width="557" height="316" alt="image"
src="https://github.com/user-attachments/assets/b628182f-cba5-451d-ae4b-bee8d8dd19aa"
/>

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: npub14ndfusear8wdpe4kss8h7juc7wjk78atnqzf63zvppcpneknv4sq6x9370 <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
Co-authored-by: npub14ndfusear8wdpe4kss8h7juc7wjk78atnqzf63zvppcpneknv4sq6x9370 <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
2026-08-05 14:02:43 -07:00
ed4b3e7afa fix(buzz-agent): recover from context-window 400s instead of sticking (#4946)
Provider `context_length_exceeded` 400s permanently wedged agent
sessions: the turn errored, the oversized history persisted in the
in-memory session, and the usage baseline stayed frozen at the last
successful sub-threshold reading (failed requests report no usage), so
the preflight handoff gate never fired again — every later prompt failed
identically until an agent restart. The byte-truncation fallback never
intervened because it is a request-body limiter (`estimated_bytes`), not
a context-window defence; at context-window scale it is a measured
no-op.

This adds the reactive recovery path:

- **Typed classification.** `AgentError::LlmContextExceeded` is
classified at both non-success provider terminals — the shared `post()`
(Anthropic, OpenAI, Databricks) and `openrouter_post()` — on `status ==
400` plus a context-window body match, so ordinary 400s stay terminal.
- **Forced handoff.** A context-400 forces a summarize-handoff that
bypasses `should_handoff()` and `BUZZ_AGENT_MAX_HANDOFFS`, bounded by
its own per-turn budget (`MAX_CONTEXT_RECOVERIES_PER_RUN = 3`).
- **Shrink ladder.** The summarize prompt budget halves from the
observed rejected history size — not from `max_context_tokens`, the
number the provider just contradicted — rung to rung, with a 4096-byte
floor. A summarize call rejected for the same reason takes the next rung
instead of re-sticking. At the floor (overflow dominated by unshrinkable
frame: system prompt, tool schemas, live prompt) recovery is refused and
the provider error surfaces clearly instead of self-healing.
- **Baseline reset.** The stale usage baseline is cleared when a request
fails, so the preflight gate cannot stay frozen sub-threshold on
retries.

Named behavior changes:

1. **Anthropic and OpenRouter errors now carry the `(model)` stamp.**
Provider arms return their `Result` into the central error mapper
instead of early-returning past it, making the code match its documented
single-convergence contract at that mapper.
2. **`max_rounds` now counts completions the loop acts on.** A request
rejected with a context-400 that is then successfully recovered refunds
its round before the retry, paired 1:1 with a consumed recovery rung, so
the round cap is neither weakened nor able to drop a recovered turn
unanswered.

Related: #4805 — the complementary proactive fix (per-session
handoff-cap kill switch that let sessions grow to the provider wall).
#4805 prevents reaching the wall; this PR recovers at it.

---------

Co-authored-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-05 16:58:25 -04:00
DuncanandWill Pfleger b450d46933 test(managed-agents): structural guard evidence for concurrency lock invariants
Replace timing-based concurrency tests with compile-enforced structural
guard ownership proofs per Thufir's binding shape consult.

Item 1 (generation lock): add SCOPE_GENERATION_TEST_LOCK to
test_fallback_relay_never_claims_during_identity_import and
test_scope_generation_guard_rejects_stale_scope_for_import; exhaustive
indirect-mutator sweep confirmed all remaining callers guarded.

Item 2 (writer test): widen on_after_restore to
FnOnce(&mut Vec<ManagedAgentRecord>, &MutexGuard<'_, ()>) — callback
borrows the actual store guard. Dropping or removing the guard before
the call is a compile error. Writer completes one full transaction
(lock → load → WRITER_EDIT → save → writer_committed) after records_loaded
releases it; on_after_restore saves COMP_SENTINEL under the live borrow.
Delete writer_at_store_lock pre-lock signal and all timing-based comments.

Item 3 (contender test): widen on_transition_acquired to
FnOnce(&MutexGuard<'_, ()>) in both start_pair_lazy_for_with_hook and
start_pair_for_with_hook — callback borrows the actual transition guard.
Drop or removal before the call is a compile error. Remove AtomicBool,
try_recv, not-fired-during assertion, and all ns-vs-ms timing rationale.
Proof by mutex exclusion: on_transition_acquired cannot execute during
compensation because both borrow the same mutex.

Production delegates unchanged: compensate_drain passes |_, _| {},
start_pair_lazy_for and start_pair_for pass |_| {} for on_transition_acquired.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-05 16:56:31 -04:00
ccdaa16161 docs(persona-pack): fix stale desktop import instructions (#4500)
## Summary

The desktop app no longer imports persona packs the way the docs
described. `PERSONA_PACK_SPEC.md` and the `meadow-core` example still
pointed users at a `.zip` import through "My Teams / My Agents → Import"
and a future "Install Pack" button — none of that exists anymore. The
app only imports agent/team **snapshots** (`.agent.json`/`.agent.png`,
`.team.json`/`.team.png`), and a persona-pack `.zip` is explicitly
rejected.

## Fix

Updated both docs to describe the current import paths (Agents / Agent
teams sections, snapshot files only) and added a note that persona packs
and desktop snapshots are separate, non-interchangeable formats today.

Fixes #4468

---------

Signed-off-by: SomSamantray <92726151+SomSamantray@users.noreply.github.com>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-05 13:53:33 -07:00
7334ad1e16 fix(desktop): route macos notification clicks (#4799)
## Summary
- deliver macOS notifications through `UNUserNotificationCenter`
- route notification clicks to the referenced channel or thread through
the existing frontend activation path
- preserve click targets across cold startup and frontend remounts with
a small process-wide activation queue
- keep Linux notification activation behavior unchanged

## Architecture
A single `UNUserNotificationCenterDelegate` is installed during Tauri
setup. Each notification stores its navigation target in `userInfo`. On
click, Rust queues the target before emitting a wake-up event; the
frontend atomically drains the queue and dispatches the existing
notification action. The queue is the source of truth, which prevents
cold-start loss and duplicate delivery.

## Validation
Verified at `a81241611d617becf7640bee6fe56b5cdb4d0fab`:
- Biome format/check and lint
- TypeScript typecheck
- repository and desktop-Tauri `cargo fmt --check`
- repository and desktop-Tauri Clippy with `-D warnings`
- full pre-push desktop tests and Tauri workspace checks/tests
- desktop production build

Manual macOS validation passed: after explicitly ad-hoc signing the
local bundle with `xyz.block.buzz.app`, the operator confirmed real
Notification Center delivery and click navigation.

<details>
<summary>Local macOS test procedure</summary>

Tauri's generated ad-hoc signing identifier is not accepted by
`UNUserNotificationCenter`. Re-sign the local bundle with its bundle
identifier and keep other Buzz copies closed:

```bash
just desktop-release-build
APP="$HOME/.cache/cargo-target/aarch64-apple-darwin/release/bundle/macos/Buzz.app"
codesign --force --deep --sign - \
  --identifier xyz.block.buzz.app \
  --entitlements desktop/src-tauri/Entitlements.plist \
  "$APP"
codesign --verify --deep --strict --verbose=2 "$APP"
pkill -x buzz-desktop || true
open -n "$APP"
```

</details>

Buzz channel: `55e2bfca-1b38-48fb-9dc2-584d400501f3`

---------

Signed-off-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
Signed-off-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
Co-authored-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
2026-08-05 13:29:41 -07:00
05150c1188 feat(mobile): sync themes per community (#3767)
**Category:** new-feature
**User Impact:** Mobile now keeps each community’s appearance in sync
with desktop, including theme, accent, and system-mode preference.

**Problem:** Appearance choices were device-local, so the same account
could look different between desktop and mobile. Live sync could also
stop after the relay closed a subscription.

**Solution:** Store each community’s encrypted appearance preference on
its relay using the shared desktop wire contract, restore it from a
local identity-scoped cache, and apply replacement events live. Closed
subscriptions now recover with guarded backoff and fetch the latest
preference so no update is lost during the gap.

<details>
<summary>File changes</summary>

**mobile/lib/app.dart**
Connects community appearance state to the authenticated app lifecycle.

**mobile/lib/features/settings/accent_picker_page.dart**
Aligns mobile accent choices and selection behavior with the shared
catalog.

**mobile/lib/features/settings/settings_page/appearance_section.dart**
Clarifies the active appearance and hides accent controls when the Buzz
theme owns its neutral accent.

**mobile/lib/features/settings/theme_picker_page.dart**
Persists catalog theme choices through the community-scoped provider.

**mobile/lib/shared/theme/accent_colors.dart**
Matches desktop’s accent catalog and wire values.

**mobile/lib/shared/theme/buzz_theme.dart**
Keeps Buzz visually neutral without discarding the user’s stored accent
for other themes.

**mobile/lib/shared/theme/community_theme_preference.dart**
Defines and validates the versioned desktop-compatible appearance
payload.

**mobile/lib/shared/theme/community_theme_provider.dart**
Coordinates cache-first appearance loading with account and community
changes.

**mobile/lib/shared/theme/community_theme_sync.dart**
Adds encrypted NIP-78 relay persistence, live replacement handling,
deterministic ordering, safe seeding, and resilient subscription
recovery.

**mobile/lib/shared/theme/theme.dart**
Exports the community appearance modules.

**mobile/test/features/settings/theme_picker_page_test.dart**
Covers the updated settings behavior.

**mobile/test/shared/crypto/nip44_interop_test.dart**
Proves Dart decrypts a desktop-produced nostr-rs NIP-44 v2 preference.

**mobile/test/shared/theme/buzz_theme_test.dart**
Covers Buzz’s neutral rendering and stored-accent restoration.

**mobile/test/shared/theme/community_theme_preference_test.dart**
Covers wire parsing, validation, migration, and future-version handling.

**mobile/test/shared/theme/community_theme_sync_test.dart**
Covers cache/relay lifecycle, replacement ordering, switching races,
absence-only seeding, and closed-subscription recovery.

</details>

## Reproduction steps

1. Sign into desktop and mobile with the same account and join the same
community relay.
2. On desktop, choose a distinctive non-Buzz theme and accent; mobile
should update without a local toggle.
3. Restart mobile and confirm it restores the same appearance.
4. Change the mobile theme and accent and confirm desktop follows.
5. Leave mobile idle or backgrounded through a relay reconnect, then
change desktop again; mobile should resubscribe and catch up
automatically.
6. Switch communities and confirm each community restores only its own
appearance.

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
2026-08-05 13:18:53 -07:00
43cced308d feat(desktop): sync themes per community (#3653)
**Category:** new-feature
**User Impact:** Users can keep a distinct Appearance scheme for each
community and restore it on another desktop signed in with the same
identity.

**Problem:** A single global theme makes it harder to distinguish among
communities, and local-only preferences do not follow a user to another
device. **Solution:** Save each community's stable theme, accent, and
system-following selection as private encrypted relay state, backed by a
responsive local cache and guarded against switch races, invalid future
records, and relay failures.

<details>
<summary>File changes</summary>

**desktop/src/app/App.tsx**
Mounts the community-scoped theme controller inside the active community
lifecycle.

**desktop/src/features/settings/lib/appearanceScopeCopy.test.mjs**
Covers active-community and fallback labels used to explain Appearance
scope.

**desktop/src/features/settings/lib/appearanceScopeCopy.ts**
Builds a safe, trimmed label for the currently active community.

**desktop/src/features/settings/ui/SettingsPanels.tsx**
Clarifies which Appearance controls are per-community and which apply
globally when multiple communities exist.

**desktop/src/shared/constants/kinds.ts**
Defines the NIP-78 application-data event kind used for theme
preferences.

**desktop/src/shared/theme/CommunityThemeController.tsx**
Coordinates cached appearance, encrypted relay retrieval, live updates,
reconnect behavior, and safe community switching.

**desktop/src/shared/theme/ThemeProvider.tsx**
Exposes a single appearance application path so synchronized preferences
use the existing renderer and persistence behavior.

**desktop/src/shared/theme/communityThemePreference.test.mjs**
Covers contract validation, user/relay isolation, malformed records,
cache failures, and switch-race decisions.

**desktop/src/shared/theme/communityThemePreference.ts**
Defines the versioned stable preference contract, safe defaults, local
cache keys, and persistence guards.

**desktop/src/shared/theme/communityThemeSync.test.mjs**
Covers relay absence, unreadable records, unavailability, seeding
safety, and teardown of pending writes.

**desktop/src/shared/theme/communityThemeSync.ts**
Encrypts theme preferences to the user, publishes and retrieves NIP-78
state, and handles ordering and lifecycle safety.

</details>

### Reproduction steps

1. Join at least two communities and open **Settings → Appearance**.
2. Choose a different theme, accent, or system-following mode in each
community.
3. Switch between the communities and verify each one restores its own
scheme without overwriting the other.
4. Sign in on another desktop with the same Nostr identity, join the
same community, and verify its saved scheme is restored from that
community's relay.
5. Disconnect the relay, change Appearance, and verify the UI remains
responsive and the local fallback is retained.

### Screenshots / demos
<img width="1733" height="948" alt="image"
src="https://github.com/user-attachments/assets/5afeabaa-0def-482c-9b87-8a880ee0a467"
/>


<img width="700" height="412" alt="Screen Recording 2026-07-29 at 4 39
52 PM"
src="https://github.com/user-attachments/assets/d58da329-aec5-4324-a4b2-cbcb2702a81a"
/>

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
2026-08-05 13:18:48 -07:00
6c40ce394f feat(desktop): cap OpenClaw agent parallelism at 5 (#4019)
OpenClaw connects to a single shared Gateway daemon. Spawning the
default 10 ACP workers per agent is both resource-expensive and
architecturally wrong — each worker opens a separate gateway connection.
Tyler's ruling: cap at 5, lower if needed.

## Contract

Store the requested value (1–32) verbatim at every persistence and wire
boundary. Apply `effective = min(requested, harness_cap)` only at the
four enforcement points:

| Boundary | Implementation |
|---|---|
| Local spawn | `BUZZ_ACP_AGENTS` env var in child `Command` |
| Remote deploy | `launch.policy_env["BUZZ_ACP_AGENTS"]` + legacy
`parallelism` field |
| Restart badge | `SpawnConfigSnapshot.parallelism` stores effective
value; the diff surface displays what actually runs |
| UI copy | Amber hint when requested > cap; no `max` attribute, no
save-path clamp |

`BUZZ_ACP_AGENTS` is added to `RESERVED_ENV_KEYS` — the Desktop resolves
the effective value into `policy_env`; a user-supplied override in `env`
would bypass the cap and is silently stripped.

## Changes

**`managed_agents/parallelism.rs`** (new) — policy core:
- `OPENCLAW_MAX_PARALLELISM = 5`
- `harness_max_parallelism(command)` — keyed on
`normalize_command_identity` so path prefixes, `.exe` suffixes, and
other cosmetic differences are ignored
- `effective_parallelism(command, value)` — identity for uncapped
harnesses
- `acp_agents_value(command, parallelism)` — `env("BUZZ_ACP_AGENTS", …)`
helper

**`runtime.rs`** — spawn clamp: `BUZZ_ACP_AGENTS =
acp_agents_value(effective_command, record.parallelism)`

**`agents_deploy.rs`** — deploy egress clamp: `build_deploy_payload`
resolves `effective_parallelism` once from `descriptor.command`; both
`launch.policy_env["BUZZ_ACP_AGENTS"]` and the legacy top-level
`parallelism` field use that value — the two are always consistent
regardless of stale `record.agent_command` pins

**`spawn_snapshot.rs`** — `from_inputs` stores
`effective_parallelism(&descriptor.command, record.parallelism)` in the
`parallelism` field. Over-cap edits that don't change the pool (e.g. 10
→ 8, both clamp to 5 on OpenClaw) produce equal snapshots; cap crossings
(8 → 3) produce different snapshots.

**`AcpRuntimeCatalogEntry.max_parallelism: Option<u32>`** — derived from
the static definition command, not the probed `entry.command` (which may
be `null` for unavailable entries), so unavailable OpenClaw entries
still carry the cap. Propagated through all four catalog constructors
(builtin discovery, preset catalog construction, custom discovery,
custom-save response), IPC types
(`RawAcpRuntimeCatalogEntry.max_parallelism`), and the frontend catalog
type.

**UI** — `EditAgentAdvancedFields` and `PersonaAdvancedFields` show an
amber hint when `selectedRuntime.maxParallelism` is set and the current
value exceeds it. Cap and label come from the catalog entry — no
hardcoded 5 in TS. No `max` attribute on inputs; the input stays
`type="text"` with 1–32 copy.

**Docs** — `docs/remote-agents.md`: `BUZZ_ACP_AGENTS` moved from the
deliberately-non-reserved section to reserved; new contract documented.
`desktop/src/features/agents/AGENTS.md`: command-keyed execution policy
documented as the sanctioned second metadata source feeding the catalog
projection.

## Tests

**Rust** (`parallelism.rs`):
- `policy_table` — `harness_max_parallelism` and `effective_parallelism`
across all openclaw variants and uncapped harnesses
- `acp_agents_value_openclaw_above_cap_is_capped` — spawn-env seam
- `override_direction_*` — both override directions (openclaw runtime +
goose override; goose runtime + openclaw override)
- `summary_persona_inherited_*` — live persona wins over stale
`agent_command`
- `snapshot_export_carries_requested_definition_parallelism` — requested
value travels wire/sync unchanged

**Rust** (`spawn_snapshot/tests.rs`):
- `openclaw_above_cap_parallelism_snapshots_equal` — stored 10 vs 8,
both clamp to 5 → snapshots equal
- `openclaw_cap_crossing_parallelism_snapshots_differ` — 8 (clamps to 5)
vs 3 → snapshots differ

**Rust** (`discovery/presets.rs`):
- `openclaw_preset_unavailable_carries_max_parallelism` /
`openclaw_preset_available_carries_max_parallelism` — catalog metadata
present with `command: null` and with a resolved path

**Rust** (`agents_deploy.rs`):
- `launch_block_openclaw_over_cap_policy_env_is_capped` — direct
`launch.policy_env` seam
-
`deploy_payload_json_stale_goose_record_live_openclaw_descriptor_both_capped`
— stale `record.agent_command=goose`, live descriptor=openclaw: both
fields cap to 5
-
`deploy_payload_json_stale_openclaw_record_live_goose_descriptor_both_uncapped`
— stale `record.agent_command=openclaw`, live descriptor=goose: both
fields pass through requested
- `deploy_payload_json_explicit_openclaw_override_both_capped` —
explicit `agent_command_override=openclaw`: both fields cap to 5

**Rust** (`persona_events/stale_pin_tests.rs`):
- `apply_persona_snapshot_goose_to_custom_harness_drops_stale_goose_pin`
— custom-direction stale-pin drop (builtin pin → loaded custom harness
via `update_loaded_harness_registry`)

**TypeScript** (`agentParallelism.test.mjs`):
- `parallelismCapHint` — at/below cap (null), above cap (hint includes
label and cap value), singular form for cap=1, uncapped harness (null)

**TypeScript** (`tauri.test.mjs`):
- `fromRawAcpRuntimeCatalogEntry` round-trips `max_parallelism` →
`maxParallelism`; absent when `undefined`

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-05 16:09:14 -04:00
6df7eba24d fix(buzz-agent): scope handoff cap per turn, not per session lifetime (#4805)
`BUZZ_AGENT_MAX_HANDOFFS` compared against the session-cumulative
`handoff_count` (persisted across prompts). After N handoffs a
long-lived session hit the cap permanently: `maybe_handoff()` returned
`Skipped` on every subsequent prompt, the 16 MiB byte-truncation
fallback never bound before a 1M-token provider wall, and the session
wedged on the first 400 with no recovery path. Thufir's session log
shows 8 days of cap-forced truncation before the first
`context_length_exceeded` 400.

The fix replaces the session-level cap comparison with a local
`handoff_attempts` counter constructed at the start of `run()` and
passed into `maybe_handoff()`. The counter resets on every
`session/prompt` turn so `BUZZ_AGENT_MAX_HANDOFFS` caps compaction loops
within a single turn while allowing unbounded compactions across a
session's lifetime. The session-cumulative `handoff_count` is retained
for log context only and is not reset. Steer-driven rounds share the
per-turn budget automatically since steers inject into the running
`run()` loop, not a new call.

- Move `handoff_attempts` increment to before `summarize()` so failed,
empty, and cancelled summarize calls each consume one budget slot — the
cap cannot be bypassed by a repeatedly-failing summarizer
- Upgrade cap-forced `Skipped` from `INFO` to `WARN`; add structured
fields for `session_id`, attempt count, projected tokens, and threshold
so the cap→wall pairing is attributable per session
- Document `max_handoffs` in `config.rs` as a per-`session/prompt`-turn
bound
- Three new behavioral regression tests: per-turn reset proven across
two separate turns; within-turn cap proven via multi-round tool-call
turn; failed summarize proven to burn the attempt budget

Note: this is the proactive half of the context-window fix. The reactive
`context_length_exceeded` 400 recovery path is owned by Sami's branch
(`buzz-ctxfix-sami`, Tyler's crew); this PR is intended to land after
that one.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
mobile-v0.8.0-rc.3
2026-08-05 15:30:04 -04:00
0c6842931b Fix mobile message timeline bounce (#4862)
## Summary

Stop repeated follow-latest scrolling after layout changes in channels
and DMs.

## Validation

- `flutter analyze lib/features/channels/channel_detail_page.dart`
- `flutter test test/features/channels/channel_detail_page_test.dart`
- Full mobile pre-push suite

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-05 12:25:32 -07:00
klopez4212andGitHub 27b51144f3 Polish mobile bottom sheets and profile cards (#4911)
## Summary

- standardize mobile sheets with shared spacing, close controls, action
tiles, haptics, and motion
- add uniform native concentric corners on iOS 26+ while preserving the
Android sheet shape
- refresh profile actions/status and normalize membership and huddle
timeline spacing

## Validation

- `just mobile-check`
- `just mobile-test` (1,165 tests)
- signed iPhone Release build and device install
- Android debug build and Pixel 10 install

## Snapshots

### Channel actions

![Channel action sheet on
Pixel](https://raw.githubusercontent.com/block/buzz/3babe5d8e339a1e7ad69de3b07e17eab17fe3f9d/pr-4911--pixel-channel-actions.png)

### Profile card

![Profile card sheet on
Pixel](https://raw.githubusercontent.com/block/buzz/3babe5d8e339a1e7ad69de3b07e17eab17fe3f9d/pr-4911--pixel-profile-card.png)

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-08-05 20:22:37 +01:00
f2ce575b62 Fix media attachment actions (#4849)
## Summary

- Upload photos immediately while keeping videos queued for background
upload.
- Move image annotation and video spoiler actions to thumbnail hover
overlays.
- Preserve the image editor's existing Draw and Spoiler controls.

### Snapshots

#### Image annotation overlay

![Image annotation
overlay](https://raw.githubusercontent.com/block/buzz/87d7e1b1a0774ffef7a1a6cba03baffd63e11bd4/pr-4849--01-image-annotation-overlay.png)

#### Image editor controls

![Image editor
controls](https://raw.githubusercontent.com/block/buzz/87d7e1b1a0774ffef7a1a6cba03baffd63e11bd4/pr-4849--02-image-editor-controls.png)

## Testing

- `pnpm typecheck`
- `pnpm check`
- Focused attachment, drawing, and spoiler smoke tests
- Pre-push desktop tests (4,286 passing)

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Honey <47c18026466e670a1618fd7de0ef32b9ff75d6e0b5ccf255d13c8c3d674ed115@buzz.block.builderlab.xyz>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-05 12:17:48 -07:00
klopez4212andGitHub 2034e693a8 fix(desktop): remove join API token control (#4897)
## Summary

Remove the nonfunctional API-token option from the existing-community
join flow.

## Validation

- Focused Playwright join-flow coverage
- Add-community screenshot coverage

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-08-05 11:56:14 -07:00
014562c063 fix(desktop): allow shared agent mentions (#4913)
## Summary

- admit relay-discovered agents to autocomplete when their response
policy authorizes the viewer
- require authorization in the exact active stream/forum channel for
mentions, while keeping community-wide discovery for member invitation
- fail closed for relay-only agents in DMs and unresolved composer
contexts
- re-authorize cached autocomplete rows after policy/channel changes so
stale agent suggestions cannot leak back in
- preserve managed-agent behavior and explicitly reject stale
agent-marked channel members absent from both live directories

## Validation

- `pnpm --dir desktop test` — 4,288 passed
- `pnpm --dir desktop typecheck`
- `pnpm --dir desktop check`
- `pnpm --dir desktop build:e2e`
- focused Playwright mention matrix — 12 passed
- focused Playwright member-invitation matrix — 2 passed
- pre-push hooks after rebase to current `origin/main` — desktop check
and 4,288 tests passed
- independent correctness/privacy re-review cleared with no remaining
blocker

## Related competing PRs

This supersedes or overlaps #2333, #3056, #4242, #4137, #2314, #4058,
and #2605. This version adds exact-channel authorization, fail-closed
DM/context handling, cached-row reauthorization, forum coverage,
outbound mention-tag coverage, explicit stale-member coverage, and
add-member discovery coverage.

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-05 11:15:11 -07:00
ff0b7982f1 Polish mobile top navigation (#4778)
## Summary

- Polish mobile Home, Activity, Search, and Settings navigation chrome.
- Add progressive Buzz gradients/frost, aligned theme colors, dividers,
typography, and section spacing.
- Refine Search and Settings motion, including automatic keyboard focus
on search activation.

<img width="630" height="1368"
alt="C78FA3CE-F2B3-45F2-B9F5-7EA7500778CC"
src="https://github.com/user-attachments/assets/5935514b-d894-4010-80dd-a938363fee93"
/>
<img width="630" height="1368"
alt="5C058A73-1879-476A-881C-531ACC256D84"
src="https://github.com/user-attachments/assets/5266c841-17ee-49e8-9841-b06d84f4195f"
/>
<img width="630" height="1368"
alt="3F50ADB7-9BDA-4A8D-A81E-20560C3B9EA6"
src="https://github.com/user-attachments/assets/f615f61e-9e96-4bce-b261-ae5ec54db872"
/>
<img width="630" height="1368"
alt="35ECE741-01F3-4B79-80C5-1DDD447121A7"
src="https://github.com/user-attachments/assets/b5145186-9884-44eb-8ebc-f3303831c0a4"
/>

## Validation


- `flutter analyze`
- Focused Home, Activity, Channels, Search, theme, and footer widget
tests
- Full pre-push checks, including mobile tests, desktop checks, and
Tauri checks
- On-device iPhone review during the visual polish pass

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: npub1glqcqfjxdens59scl477pmejh8lht4hqkhx0y4w38jxr6e6w6y2sm29y4e <47c18026466e670a1618fd7de0ef32b9ff75d6e0b5ccf255d13c8c3d674ed115@buzz.block.builderlab.xyz>
Signed-off-by: Code Reviewer <037593536284cf40e221c96c931e9877d4166d54f6bb84e5341a86d7fd5d05a4@buzz.block.builderlab.xyz>
Signed-off-by: Kenny Lopez <klopez4212@gmail.com>
Co-authored-by: npub1glqcqfjxdens59scl477pmejh8lht4hqkhx0y4w38jxr6e6w6y2sm29y4e <47c18026466e670a1618fd7de0ef32b9ff75d6e0b5ccf255d13c8c3d674ed115@buzz.block.builderlab.xyz>
Co-authored-by: Code Reviewer <037593536284cf40e221c96c931e9877d4166d54f6bb84e5341a86d7fd5d05a4@buzz.block.builderlab.xyz>
2026-08-05 19:01:20 +01:00
4674750b7e fix(release): tag immutable desktop candidates (#4811)
## Summary

Redesign the permanent Desktop release flow so unrelated merges to
`main` cannot invalidate an already reviewed, green release candidate.

- Tag the immutable, API-confirmed release PR head instead of its later
squash commit.
- Treat the merged PR—including an authorized owner/admin bypass—as
publication authorization, while requiring trusted check evidence that
was complete at merge time.
- Make tag creation idempotent and collision-safe: an existing tag
succeeds only at the exact candidate SHA, and create races refetch
before accepting equality.
- Replace ancestry-based previous-release discovery with a validated
metadata ledger for side-history candidate tags.
- Compute the next release from the prior frozen base to the new frozen
base, excluding only the prior release squash SHA so unrelated commits
remain in the changelog.
- Preserve schema-1 production-tag migration and reject malformed
metadata or equal/decreasing versions.
- Update operator documentation for the normal squash-merge workflow.

This is the reusable release process for `0.5.6` onward, not the retired
one-shot `0.5.5` recovery path.

### Invariants covered

- Candidate creation → unrelated `main` merge → authorized squash merge
→ immutable candidate tag.
- Trusted producer IDs and merge-time completion timestamps; DCO's
bounded post-merge exception remains isolated.
- Missing/spoofed checks, tampered candidates, ambiguous PR
associations, conflicting tags, and equal/decreasing versions fail
closed.
- Same-SHA retries succeed; different-SHA collisions fail.
- Legacy schema-1 tag-on-main migration and schema-2 side-history
accounting both preserve the correct next-release changelog.

### Related issue

N/A — follows the Desktop release failures in #4788 and #4800 and the
recovery revert in #4808.

### Testing

At clean commit `6a91fbed8147a48cf174997de0c3e4cb2fb26474`:

- `scripts/test-desktop-release-candidate.sh`
- `scripts/test-release-ref-contract.sh`

Both focused suites passed with HEAD unchanged. Princess Donut cleared
the security/provenance surface, including the hostile merge-time
timestamp cases. Mongo cleared the side-history ledger, migration,
version-order, documentation, and contract-test surface.

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-05 11:00:14 -07:00
efe1893dd3 fix(channels): restrict private-channel invitations (#4612)
This change requires an active owner or administrator for third-party
additions to private channels. The relay validator and transactional
database authority enforce the same rule, including removed-member
reactivation and role-change paths.

Idempotent self-target behavior remains available, while ordinary
members can no longer extend private-channel access to another identity.

## Testing

- `git diff --check
origin/main...codex/security-private-channel-invite-authority`
- Rebased onto `origin/main` at `5c98932`
- Full CI pending

Originating Buzz thread:
`buzz://message?channel=3928fe05-df61-4b5d-b9c7-d623b9b10ea1&id=3c6c02312f763fbe0d2bfc33a6c1a362f91d0354f3d18b039cf7a0558c1439d1`

---------

Signed-off-by: Jordan Mecom <jm@squareup.com>
Signed-off-by: Eli Foster <efoster@squareup.com>
Co-authored-by: Eli Foster <efoster@squareup.com>
2026-08-05 10:47:00 -07:00
ad538bfb1e fix(acp): reject unattended permission requests (#4609)
This change removes the ACP permission-bypass mode, defaults managed
sessions to `dontAsk`, and answers permission requests with
`reject_once` or cancellation in both ACP read loops.

Unattended operations that require interactive approval now fail closed
instead of being silently authorized. Explicit non-interactive modes
that do not bypass a permission request remain available.

Both layers have to change together: `apply_permission_mode` treats an
unsupported mode and a failed `set_config_option` as non-fatal by
design, so a request can still reach the harness even in a
non-interactive mode. Removing `bypassPermissions` from the enum rather
than only changing the default means the mode cannot be restored by
configuration alone.

The scope of the guarantee is that `buzz-acp` never grants approval. An
agent that pre-authorizes tools in its own configuration (for example
Claude Code's `settings.json`) still runs them without asking, which is
outside this harness.

## Testing

- `env -u BUZZ_ACP_LAZY_POOL bin/cargo test -p buzz-acp` at `16fff4d`:
671 library tests and 9 integration tests passed
- `cargo clippy -p buzz-acp --all-targets -- -D warnings` and `cargo fmt
-p buzz-acp -- --check`: clean
- `git diff --check
origin/main...codex/security-acp-shell-auto-approval`

The permission tests previously re-implemented the `reject_once` lookup
in the test body instead of calling the code under test, so they would
have passed unchanged if the harness went back to selecting
`allow_once`. They could not call it directly, because
`handle_permission_request` is a method on `AcpClient`, which owns a
live `Child` and its stdio pipes. The choice is now a free function,
`permission_denial_response`, and the tests exercise it: `reject_once`
preferred over offered allow options, the cancelled fallback when no
`reject_once` exists, an empty option list, and a `reject_once` missing
its `optionId`. The cancelled fallback had no coverage before despite
being the fail-closed backstop.

## Operator notes

- `BUZZ_ACP_PERMISSION_MODE=bypassPermissions` no longer parses, so a
process configured with it fails to start rather than silently
downgrading.
- Desktop managed agents do not set a permission mode, so they inherit
`dontAsk`. The desktop has no permission prompt, so operations needing
approval now fail with no in-app way to approve them.

Originating Buzz thread:
`buzz://message?channel=3928fe05-df61-4b5d-b9c7-d623b9b10ea1&id=3c6c02312f763fbe0d2bfc33a6c1a362f91d0354f3d18b039cf7a0558c1439d1`

---------

Signed-off-by: Jordan Mecom <jm@squareup.com>
Signed-off-by: Eli Foster <efoster@squareup.com>
Co-authored-by: Eli Foster <efoster@squareup.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 10:38:51 -07:00
Jordan MecomandGitHub 885bed35ee fix(workflow): bind trigger author to the signed event (#4607)
This change derives `trigger_author` exclusively from the signed event
pubkey. Actor tags remain available as event data but cannot override
the identity used by author-sensitive workflow conditions.

This removes the impersonation path without changing workflow
definitions or requiring stored-data migration.

## Testing

- `bin/cargo test -p buzz-workflow` at `78819df`: 154 passed, 2
Postgres-dependent tests ignored
- `git diff --check
origin/main...codex/security-workflow-trigger-author`

Originating Buzz thread:
`buzz://message?channel=3928fe05-df61-4b5d-b9c7-d623b9b10ea1&id=3c6c02312f763fbe0d2bfc33a6c1a362f91d0354f3d18b039cf7a0558c1439d1`

Signed-off-by: Jordan Mecom <jm@squareup.com>
2026-08-05 10:35:40 -07:00
997b8caaa4 fix(git): revoke access for banned relay members (#4608)
This change rechecks the durable community ban in the shared Git HTTP
authentication path for advertise, fetch, and push requests. A banned
member is denied even if repository-channel membership still exists, and
restriction lookup errors fail closed.

The additional database lookup happens on every Git HTTP request so
access revocation does not depend on stale session state.

The check also cascades to the NIP-OA owner. Git accepts NIP-OA
attestations on the NIP-98 token, so an agent key can act for its owner
— without the cascade, a banned human would keep clone and push access
through any agent key. This mirrors the NIP-42 gate in `handlers::auth`:
either principal's ban denies the request.

The check runs inside the `GitAuth` extractor, so all three Git routes
inherit it.

## Testing

- `git diff --check origin/main...codex/security-ban-revokes-git`
- Rebased onto `origin/main` at `5c98932`
- `cargo test -p buzz-relay --lib sec005_read_gate_tests`: 8 passed, 7
ignored (Postgres)
- `cargo clippy -p buzz-relay --all-targets -- -D warnings` and `cargo
fmt --check`: clean

Pure tests cover the decision table (agent ban, inherited owner ban, no
attestation). Postgres-gated tests cover the wiring: the real ban row, a
live `compute_auth_tag` attestation, and the 503 fail-closed path.

**Not yet verified:** the three Postgres-gated tests compile and skip
but have not been run — no local Postgres, and CI does not run
`--ignored`. They need `cargo test -p buzz-relay --lib
sec005_read_gate_tests -- --ignored` against a migrated dev database.

Originating Buzz thread:
`buzz://message?channel=3928fe05-df61-4b5d-b9c7-d623b9b10ea1&id=3c6c02312f763fbe0d2bfc33a6c1a362f91d0354f3d18b039cf7a0558c1439d1`

---------

Signed-off-by: Jordan Mecom <jm@squareup.com>
Signed-off-by: Eli Foster <efoster@squareup.com>
Co-authored-by: Eli Foster <efoster@squareup.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 10:32:41 -07:00
8a7eb8d3d7 fix(agent): recover from unsupported image input instead of poisoning the turn (#4896)
## Problem

`buzz-dev-mcp` advertises `view_image` to every agent regardless of
whether the session's model accepts images. When a text-only model (e.g.
DeepSeek V4 Flash) takes the bait, the image lands in session history
and every subsequent LLM request 404s with `No endpoints found that
support image input`. The error was classified as `LlmModelNotFound` and
propagated fatally out of the turn loop — history stays poisoned,
buzz-acp retries the batch with exponential backoff, and the session
burns its entire clock doing no work. In a recent trial run, **all 57
trials that called `view_image` on a text-only model died this way; none
recovered.**

## Fix

Capability-gating the advertised tool isn't reliable — there is no
image-capability metadata at the agent layer across providers. Instead,
recover at the turn loop:

- **Typed error**: new `AgentError::UnsupportedImageInput`, classified
narrowly on the exact provider phrase `No endpoints found that support
image input` on both the generic 404 path and OpenRouter's 404 path.
Unknown-model 404s and OpenRouter parameter-routing 404s keep their
existing classifications. No deterministic retry.
- **In-turn recovery**: on this error, `RunCtx::run` strips every image
block from history — keeping the tool result (and therefore
tool-call/result pairing) intact — marks the result `is_error`, appends
actionable model-facing guidance ("The current model does not support
image input. The image was removed from conversation history so this
turn can continue. Use a text-based inspection tool…"), and continues
the same turn. Base64 never replays again.
- **Loop guard**: recovery only fires when at least one image was
removed; if the provider says "image" and history has none, the error
propagates as before.

## Tests

- Unit: phrase classification (typed, not retried; unknown-model 404
unaffected), idempotent image-to-error history mutation preserving call
IDs and text.
- End-to-end (`fake_llm.rs` + `fake_mcp.rs`): tool call → MCP image
result → 404 unsupported-image → same-turn recovery. Captured requests
prove round 2 carried the image, round 3 replays no image, carries the
guidance text, preserves pairing, and ends `end_turn`.
- Loop guard: typed unsupported-image error with **no** image in history
fails after exactly one provider request instead of spinning —
mutation-testing showed deleting the `removed == 0` guard survived the
suite, and `max_rounds` defaults to unlimited in production, so this
branch needed direct coverage.

Verified at `a210305019b33d5f56677b4c82bab79e4ac52d24`: `cargo test -p
buzz-agent` (full package, 381 unit + all integration suites) green;
`clippy --all-targets -D warnings` green; `fmt --check` green; pre-push
hooks (rust-tests, desktop-tauri-checks, branch-skew) green.

**Scope of the classification guarantee**: the classifier runs in the
shared `post()` (which Anthropic and OpenAI paths route through) and in
`openrouter_post()` — i.e., every 404 path in `llm.rs`. It only runs on
404 responses; providers that reject images with a different status
(e.g. a 400) are out of scope for this PR — see the review-comment
discussion for why broadening the phrase list alone would not cover
them.

Authored by Wren, loop-guard test by Sami, reviewed by Eva.

---------

Signed-off-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
Signed-off-by: Sami <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
Co-authored-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
Co-authored-by: Sami <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
2026-08-05 11:51:05 -04:00
067c085f37 Define private managed agent wire protocol (#4593)
## Summary

- reserve kind `30179` for owner-private managed-agent aggregates
- define the fail-closed owner-self NIP-44 v2 envelope and versioned
payload codec
- bind runnable identity/configuration to complete signed
`30175`/`30177` recovery projections
- validate NIP-OA owner→agent attestations and reject self-attestation
- document NIP-PMA authority, migration prerequisites, privacy, and
deployment order
- keep generic relay ingest closed until private storage and atomic
aggregate CAS exist

## Safety boundary

This is the inert protocol/codec slice only. It does not publish
secrets, change agent authority, migrate local records, or enable kind
`30179` ingestion. The relay regression test proves generic EVENT ingest
still rejects the kind.

The finalized migration plan adds later prerequisites for relay-private
storage/CAS, runtime lease/fencing, Desktop cutover, and harness
authentication. Those belong in staged follow-up PRs rather than
expanding this inert foundation.

## Validation

At commit `67f0ea4ebb8d3ccba3a3eb9374e89a7178913f74`:

- `cargo test -p buzz-core` — 246 unit + 2 doc tests passed
- `cargo test -p buzz-relay
private_managed_agent_kind_remains_rejected_until_atomic_ingest_exists`
— passed
- push hooks: Rust tests and desktop checks passed (`2145` desktop tests
passed, `14` ignored)
- `cargo fmt --all -- --check`
- `git diff --check`

## Review

Princess Donut cleared security/data integrity with no remaining
high/medium findings. Mongo cleared migration compatibility and wire
grammar. The later runtime lease/fencing protocol was also adversarially
cleared as a plan; implementation slices still require independent
evidence before activation.

Deterministic plaintext/signed-projection/auth-tag interoperability
vectors remain a valuable follow-up, not an S0 merge gate; random NIP-44
ciphertext is intentionally not snapshotted.

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-05 08:34:02 -07:00
dc17965c79 fix(mobile): serialize channel sections sync (#3165)
### What changed?

Serializes channel-section relay synchronization so a late relay
`CLOSED` cannot overlap an in-flight retry and install duplicate
subscriptions. Pending subscription results are invalidated and
immediately closed when the manager is disposed or superseded.

### Why?

The startup retry added in #3004 could race with a late `CLOSED` or
manager disposal, leaking an untracked live subscription. This keeps
retry recovery single-flight and makes the lifecycle boundary explicit.

### How is it tested?

Build and run.

Added tests:

-
[`ChannelSectionsManager`](https://github.com/block/buzz/tree/main/mobile/test/features/channels/channel_sections/channel_sections_manager_test.dart)
interleaving coverage for in-flight retry serialization and disposal
during subscription setup

*🤖 This PR was authored with a Buzz agent.*

Signed-off-by: npub15w828kxsxu2684ynste0uah2jwkgatd99flt7ds4523hzm8ju6cshdr8hh <a38ea3d8d03715a3d49382f2fe76ea93ac8eada52a7ebf3615a2a3716cf2e6b1@buzz.block.builderlab.xyz>
Co-authored-by: npub15w828kxsxu2684ynste0uah2jwkgatd99flt7ds4523hzm8ju6cshdr8hh <a38ea3d8d03715a3d49382f2fe76ea93ac8eada52a7ebf3615a2a3716cf2e6b1@buzz.block.builderlab.xyz>
2026-08-05 07:55:26 -07:00
DuncanandWill Pfleger d71cd62c7f fix(desktop): resolve authorized-pass round-4 test-vs-claim residuals
Item 1 — contender test (production-called hook):
Move start_pair_for_with_hook and start_pair_lazy_for_with_hook to
runtime_commands_seams.rs (extracted to stay under the 1000-line gate;
included via #[path]). Production start_pair_for delegates to
start_pair_for_with_hook with no-op hooks; start_pair_lazy_for delegates
through start_pair_lazy_for_with_hook. The test-file mirror
start_pair_for_with_hook is deleted from runtime_commands_tests.rs. The
contender test calls the production-callable seam: removing
managed_agent_runtime_transition from start_pair_for_with_hook would
also remove it from the production delegation chain, making the test a
faithful proxy.

Item 2 — writer test (on_after_restore callback):
Remove the unconditional save_managed_agents_at from the production
compensate_drain_with_hook body (step-7 was a no-op duplicate that also
masked the real compensation result on error). Add on_after_restore hook
invoked after compensate_drain_for returns, still under both guards.
Production compensate_drain passes a no-op; test injects a mutation + save.
The writer test: on_records_loaded signals writer and waits for
writer_at_store_lock_rx before returning; on_after_restore mutates
COMP_SENTINEL in-memory and saves (assert/unwrap). Both COMP_SENTINEL and
WRITER_EDIT must appear on disk. If the store guard is dropped before
on_after_restore, the writer interleaves, loads without COMP_SENTINEL, and
the first assertion fails deterministically.

Item 3 — E2E classification:
thread-focus-mode.spec.ts:139 failed in run 30987602439 (branch head
6739f157e). The prior branch commit 27ea60724 (same TypeScript changes,
different Rust) had green CI including all E2E shards (run 30981177509).
The delta between 27ea60724 and 6739f157e is Rust-only; the spec tests
viewport scroll preservation in focus/split mode — no Rust path. The
failure is a CI flake, not a branch regression.

Item 4 — doc comment alignment:
Updated all compensation and contender test comments to describe only what
the execution establishes. Removed references to step-7, stale failure-mode
descriptions, and start_pair_for in favour of start_pair_for_with_hook.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-05 10:52:57 -04:00
6dbc946512 fix(desktop): make missing-command error actionable for released builds (#4802)
User-facing error for missing ACP harness commands has been pointing
released-build users to run `cargo build --release --workspace` and read
TESTING.md — both dead ends for anyone not building from source.

Updated message acknowledges that antivirus software can quarantine
bundled binaries and provides practical remediation steps. Preserves
pointer to TESTING.md for source builds.

Fixes issue context from #4491.

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub16v54tttfqacx9ycvc3k0ut0npj564ahcuajzy6qjvh57ntmsf4uq4806j2 <d32955ad69077062930cc46cfe2df30ca9aaf6f8e76422681265e9e9af704d78@buzz.block.builderlab.xyz>
2026-08-05 09:51:15 -04:00
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7andWill Pfleger 6739f157e2 test(desktop): resolve P4 pass-3 test-vs-claim defects in workspace-scoped agent store
Writer test: change writer to block directly on managed_agents_store_lock
(not via start_pair_lazy_for_with_hook) so the test fails deterministically
if the store guard is dropped before the step-7 save. The hook mutates
records in-memory; the step-7 save writes the mutation to disk while the
lock is still held; the writer loads after the lock is released.

Contender test: add try_recv() check on a dedicated channel inside
on_records_loaded alongside the existing atomic bool. on_transition_acquired
sends to both channels. try_recv() is deterministic: without the transition
lock, on_transition_acquired fires in nanoseconds; by the time on_records_loaded
runs (after file I/O), the channel contains the message. Documents the
time-differential argument explicitly in the test comment.

Full-tail PID: capture child.id() before wrapping into ManagedAgentProcess;
assert exact equality with receipt.pid (assert_eq! not assert! pid > 0).

SCOPE_GENERATION_TEST_LOCK: add to all remaining tests that mutate or
capture scope generation: app_state_scope_tests.rs (4 tests),
global_agent_config_tests.rs (4 tests), runtime_commands_tests.rs (2 tests),
scope.rs (5 generation tests). All tests that advance SCOPE_GENERATION
now participate in the process-global serialization mutex.

Trim runtime_commands.rs doc comments to stay at 1000 lines (gate limit).

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-05 04:03:26 -04:00
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7andWill Pfleger 27ea607243 fix(desktop): suppress await_holding_lock clippy lint on epoch tests
SCOPE_GENERATION_TEST_LOCK is a std::sync::Mutex held across await points
in test_full_tail_stop_spawn_receipt_register_save and
test_relay_mesh_preflight_precedes_stop to prevent concurrent tests from
advancing the generation counter mid-test. The deadlock risk is acceptable
in test-only code: the lock is never held across blocking operations, only
across async coordination inside a single test. Suppress the lint explicitly
with a comment explaining the rationale.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-05 02:22:56 -04:00
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7andWill Pfleger 7c455d9759 fix(desktop): serialize generation-sensitive epoch tests with SCOPE_GENERATION_TEST_LOCK
test_full_tail_stop_spawn_receipt_register_save and
test_relay_mesh_preflight_precedes_stop both capture the scope generation via
current_scope_generation() and call into restart_under_captured_epoch_for or
restart_local_agent_on_config_change_for, which validate the generation under
a lock. Without SCOPE_GENERATION_TEST_LOCK, a concurrent test calling
next_scope_generation() can advance the counter between capture and validation,
causing the epoch to return Skipped instead of the expected outcome.

Both tests now hold SCOPE_GENERATION_TEST_LOCK for the duration of their
execution, matching the serialization pattern used by the workspace-transition
tests.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-05 01:59:07 -04:00
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7andWill Pfleger 45f199ba5d fix(desktop): resolve P4 pass-2 test-vs-claim defects in workspace-scoped agent store
Four IMPORTANT findings: each assertion now fails if the production guard or
save it claims to prove is removed.

Fix 1 — compensate_drain writer test (genuine store-lock contention):
Add compensate_drain_with_hook seam; on_store_acquired hook fires while BOTH
locks are held. Test writes COMP_SENTINEL under the lock, signals writer,
waits for writer to queue on the store lock, then releases. Final disk state
must contain BOTH COMP_SENTINEL and WRITER_EDIT — fails if the guard is
dropped before compensate_drain_for's save.

Fix 1b — start-contender test (production start-lock seam):
Add start_pair_lazy_for<R: tauri::Runtime> generic seam; production
start_managed_agent_runtime_pair_lazy and start_pair delegate to it.
Contender now calls start_pair_lazy_for (the seam the production adapter
calls) instead of a raw mutex lock — proves compensation blocks production
starts, not just an arbitrary lock acquisition.

Fix 2 — Record-Mesh TOCTOU (async production driver):
Drive restart_local_agent_on_config_change_for (full async driver). Injected
mesh_fn rewrites provider to relay-mesh after the driver has resolved
mesh_model_id=None; epoch re-resolves to Some("auto") != None → TOCTOU guard
fires → Skipped before stop. Removing the in-epoch re-resolve guard would
allow the epoch to proceed, calling stop_fn and failing the assertion.

Fix 3 — Helper-vs-helper serialization (pre-acquisition hook):
Add with_workspace_transition_preflight_with_hook and
install_client_under_workspace_transition_with_hook to mesh_llm_scope.rs.
Both serialization tests use a proper 3-step handshake: holder signals
lock_held BEFORE publishing state; contender fires pre-acquisition hook
signaling at_lock_boundary; only then does the holder publish and release.
Removing the shared lock would let the contender bypass the boundary,
inverting each test's expected outcome.

Fix 4 — Full-tail receipt and disk assertions:
write_receipt_fn captures all receipt fields: key.pubkey, key.relay_url,
pid, desktop_instance_id (asserted equal to app.config().identifier),
started_at. Final disk assertions verify last_started_at.is_some(),
last_stopped_at.is_none(), last_error.is_none() on the matching record.
Removing the post-spawn record save drops last_started_at and fails the
disk assertions.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-05 01:32:34 -04:00
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7andWill Pfleger 91aea2db96 fix(desktop): resolve P4 pass-1 review defects in workspace-scoped agent store
Fix all five IMPORTANT defects identified by Thufir's P4 pass-1 review:

1. Concurrency tests: both test_compensate_drain_writer_vs_compensation_deterministic
   and test_compensate_drain_concurrent_start_is_blocked now drive the production
   compensate_drain adapter (not compensate_drain_for directly). Transition guard
   passed by value; store lock and restore owned by the production symbol.
   Coordinator/contender ordering via channels/barriers exclusively — no
   thread::sleep.

2. Global-restart tests: cross-platform spawn_long_lived_child_for_test (sh loop /
   ping) and spawn_noop_child_for_test (sh exit 0 / cmd.exe exit 0) replace the
   UNIX-specific sleep 10000 and /usr/bin/true, fixing Windows Rust CI failures.
   Full-tail test asserts non-empty personas/teams/global in captured context.
   Context-load-failure test uses malformed JSON (not absent file) to exercise the
   genuine parse-error path. Record-mesh-change test seeds eligible runtime and
   constructs mismatched context.mesh_model_id to trigger in-epoch TOCTOU guard.

3. Active-scope identity import: import_identity routes through the production
   with_workspace_transition_preflight (mesh-llm) / direct workspace_transition
   (no mesh-llm) when has_active_scope, matching apply_workspace orchestration.

   Direction tests rewritten as helper-vs-helper: each side uses a production
   helper (with_workspace_transition_preflight or install_client_under_workspace_
   transition); oneshot channels establish entry/release ordering with no direct
   lock acquisition and no sleep.

4. Team memory boundary: setup_team_import_app_with_scope adds a memory-bearing
   team member (member_with_memory); seam test asserts both the captured HTTP
   relay URL and the captured owner p-tag from the built engram event after
   after_store commits a distinct owner/scope.

5. CI portability: setup_import_app_with_scope and setup_team_import_app_with_scope
   use WorkspaceAgentScope::new with writable temp agent base so definitions_dir
   has the production <base>/scopes/<scope_id> shape, fixing the Linux /retention
   EPERM failure and the poisoned-lock cascade in seam tests.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-04 23:58:02 -04:00
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7andWill Pfleger a1228b39a1 fix(desktop): replace map_or(false,...) with is_some_and per clippy
Resolve clippy::unnecessary-map-or in import_tests.rs from the P3
completion commit. No logic change.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-04 22:14:18 -04:00
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7andWill Pfleger 48015b838c fix(desktop): resolve P3 compliance gaps in Areas 3 and 4
Area 4 — capture scope BEFORE discovery in ensure_relay_mesh_for_record.
Previously capture_active_scope was called after resolve_mesh_bootstrap_target;
a workspace switch between discovery and capture would let the helper install a
client against an endpoint discovered under the old scope while validating
against the new scope. Move capture_active_scope above the discovery call so
the captured scope and discovered endpoint are always from the same workspace.

Area 4 — make with_workspace_transition_preflight production-callable.
Change the transition body parameter from FnOnce() -> Result<T> (sync) to
FnOnce() -> BoxFuture<'static, Result<T>> (async) so production callers that
dispatch spawn_blocking and await the result keep the workspace_transition guard
alive across the full async body. Extract apply_workspace_body as a standalone
async fn; apply_workspace (mesh-llm feature) routes through
with_workspace_transition_preflight so the helper is no longer test-only dead
code. The cfg_attr(not(test), allow(dead_code)) annotation is removed.
Update the two test call sites to pass BoxFuture-returning closures.

Area 3 — add memory-bearing fixture and owner-coordinate assertion.
Add minimal_agent_snapshot_json_with_memory which carries one core memory entry
(MemoryLevel::Core). Rewrite test_agent_switch_between_store_and_profile_finishes_captured_outbound
to use this fixture so submit_memory actually fires in Phase 4. The injected
MemoryPublish adapter now asserts both that relay_url contains the captured
relay URL and that the built engram event's p tag (owner counterpart/coordinate)
equals the captured owner's pubkey hex, not the post-switch owner committed
in after_store. Add extract_p_tag_from_event_json helper.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-04 22:08:17 -04:00
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7andWill Pfleger c29cd651db fix(desktop): extract mesh readiness helpers to satisfy file-size ratchet
mesh_llm.rs was 1016 lines (split-count 1017, gate limit 1000). Extract
MeshReadinessFailure, classify_mesh_readiness_failure,
mesh_readiness_failure_message, and wait_for_mesh_inference into a new
mesh_llm_readiness.rs submodule (140 lines). mesh_llm.rs is now 887 lines
(split-count 888). All other files remain under the 1000-line limit.

Tests that used the readiness items via use super::* add explicit
use super::readiness::* imports since the items moved out of the parent
namespace. No behavior change.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-04 21:37:13 -04:00
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7andWill Pfleger 8b046bc62b fix(desktop): wire production callers through shared mesh preflight helpers
apply_workspace and import_identity now call run_mesh_transition_preflight
(extracted from fail_if_client_mesh_active path) rather than inlining the
check. ensure_relay_mesh_for_record captures scope before discovery and
routes the install call through install_client_under_workspace_transition,
which validates full scope identity (scope_id, relay, owner, generation)
under the workspace_transition guard before invoking the closure.

with_workspace_transition_preflight gains run_mesh_transition_preflight as
a companion: tests continue to call the callback helper directly; production
callers that need spawn_blocking dispatch use run_mesh_transition_preflight
after acquiring workspace_transition themselves.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-04 21:22:42 -04:00
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 50f615ecbd Merge remote-tracking branch 'origin/main' into duncan/workspace-scoped-agent-store
* origin/main:
  chore(release): release Buzz Desktop version 0.5.5 (#4809)
  feat: paste composer text without formatting (#4801)
  Revert "chore(release): release Buzz Desktop version 0.5.5" (#4808)

Signed-off-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-04 20:41:32 -04:00
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7andWill Pfleger 09207bcf29 fix(desktop): skip OS keychain in test builds to prevent headless hangs
hydrate_keys and persist_agent_keys both call the OS keychain through the
SecretStore global singleton. In headless test environments (CI, locked
keychain) these calls block on the macOS Security daemon IPC
(SecKeychainFindGenericPassword / SecKeychainItemModifyAttributesAndData)
and cause tests to hang indefinitely.

Three tests in the Phase-3 seam suite were hanging:
  - test_record_mesh_change_after_preflight_aborts_before_stop
  - test_full_tail_stop_spawn_receipt_register_save
  - test_relay_mesh_preflight_precedes_stop

All three write agent records with non-empty pubkeys to disk then call
load_managed_agents_at or save_managed_agents_at, triggering keychain IPC.

Fix: gate hydrate_keys and persist_agent_keys with #[cfg(test)] early
returns. Test builds exercise the keychain-generic testable cores
(hydrate_keys_with / persist_agent_keys_with) via mock KeyStore impls
directly; the production wrappers are not exercised in unit tests and
must never be. Also add SecretStore::warm_cache_for_test for use by any
future test that needs to pre-seed the in-process cache without touching
the OS keychain.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-04 20:32:23 -04:00
8342dfcc58 chore(release): release Buzz Desktop version 0.5.5 (#4809)
## Buzz Desktop release v0.5.5

- **Frozen main:** `25a9cf1be6d245fbd7373cb1160dbc790baf5bd5`
- **Reviewed candidate:** `8380c1f8ead8816bcf1f4ea9f66aa08e2441b15a`
- **Previous desktop release:** `desktop-v0.5.4`
- **Proposed immutable tag:** `desktop-v0.5.5`

This PR must be **squash merged** only after the Desktop Release
Candidate check passes. The branch must remain based directly on current
`main`; stale base, payload drift, incomplete notes, or an unauthorized
merge produce no tag.

The checked-in changelog accounts for every non-merge commit in the
release range. Publication remains bound to the immutable candidate tag.

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Release Automation <release-automation@users.noreply.github.com>
desktop-v0.5.5
2026-08-04 16:54:16 -07:00
25a9cf1be6 feat: paste composer text without formatting (#4801)
## Summary

- handle Cmd+Shift+V on macOS and Ctrl+Shift+V on Windows/Linux in the
message composer
- read plain text through the native Tauri/arboard clipboard path in
packaged builds, with a browser-only Clipboard API fallback
- re-enter ProseMirror's paste pipeline with populated `text/plain`
clipboard data so selection, undo, multiline behavior, and paste
observers remain intact
- cover both platform mappings with rendered composer E2E tests that
assert the native command path

## Testing

- `pnpm test` — 4,286 passed
- `pnpm check`
- `pnpm typecheck`
- `pnpm exec playwright test composer-selection-formatting.spec.ts
--project=smoke` — 26 passed
- `cargo check --manifest-path desktop/src-tauri/Cargo.toml --workspace
--all-targets --target aarch64-apple-darwin`
- `just desktop-tauri-test` — 2,206 core tests plus integration and
doc-test groups passed
- full pre-push hooks passed

## Manual verification

Physical packaged-app clipboard verification remains recommended on
macOS, Windows, and Linux. The automated E2E uses mocked Tauri IPC but
asserts the native `read_clipboard_text` command is invoked.

Signed-off-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
Co-authored-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
2026-08-04 16:16:14 -07:00
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7andWill Pfleger 4634fe1b39 chore(desktop): merge origin/main into workspace-scoped-agent-store
Adopt spawn_hash → spawn_snapshot rename from main; migrate spawn_config_hash: 0
test fixtures to prospective_spawn_config_snapshot() calls in global_agent_config
tests, epoch tests, and runtime_commands tests.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-04 19:13:15 -04:00
WesandGitHub 79c52166cf Revert "chore(release): release Buzz Desktop version 0.5.5" (#4808)
Reverts block/buzz#4800
2026-08-04 16:09:33 -07:00
a0ed13de14 chore(release): release Buzz Desktop version 0.5.5 (#4800)
## Buzz Desktop release v0.5.5

- **Frozen main:** `4a2305170eef565bf1836e2859247e67c030f8af`
- **Reviewed candidate:** `2d03d37b05b68186b2caad9da79080032be3ac72`
- **Previous desktop release:** `desktop-v0.5.4`
- **Proposed immutable tag:** `desktop-v0.5.5`

This PR must be **squash merged** only after the Desktop Release
Candidate check passes. The branch must remain based directly on current
`main`; stale base, payload drift, incomplete notes, or an unauthorized
merge produce no tag.

The checked-in changelog accounts for every non-merge commit in the
release range. Publication remains bound to the immutable candidate tag.

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Release Automation <release-automation@users.noreply.github.com>
2026-08-04 16:00:29 -07:00
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7andWill Pfleger d0658609ed fix(desktop): split Phase 3 test files to satisfy 1000-line size ratchet
Four test files exceeded the 1000-line gate introduced by Phase 3 seam work.
Split each by extracting the largest block into a sibling file included via
#[path], keeping every file at or below the limit:

- global_agent_config.rs (1987 → 946): inline test block moved to
  global_agent_config_tests.rs (637) + global_agent_config_epoch_tests.rs (420)
- mesh_llm_tests.rs (1052 → 776): Area-4 serialization tests extracted to
  mesh_llm_transition_tests.rs (284)
- team_snapshot/tests.rs (1087 → 896): Area-3 phase-boundary seam tests
  extracted to team_snapshot/seam_tests.rs (200)
- runtime_commands_tests.rs (1056 → 777): concurrency tests extracted to
  runtime_commands_concurrency_tests.rs (289)

All nine files are now under 1000 lines. No test logic changed; all 2230
lib tests pass. just desktop-check passes at this commit.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-04 18:59:33 -04:00
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7andWill Pfleger 1a1571b165 fix(desktop): resolve clippy warnings introduced in Phase 3 seam commits
- items_after_test_module: move SCOPE_GENERATION_TEST_LOCK before mod tests in scope.rs
- await_holding_lock: add #[allow] + SAFETY comments on all tests holding
  std::sync::Mutex across .await (single-threaded tokio runtime, no deadlock risk)
- needless_borrow: drop redundant & on &handle calls to async import cores
- too_many_arguments: suppress on spawn_agent_child_at (8-arg function required
  by approved Area-5 signature; bounded to two call sites)
- redundant_closure: replace wrapper closures with function-item references
  for stop_managed_agent_process and write_agent_runtime_receipt
- unused_imports: remove unused load_managed_agents_at from two test imports
- cloned_ref_to_slice_refs: replace &[x.clone()] with std::slice::from_ref(&x)
  in three test save_managed_agents_at calls
- doc_lazy_continuation: add two extra spaces to continuation lines in the
  team_snapshot.rs docstring list item 3
- dead_code: suppress on make_captured_scope helper prepared for future tests

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-04 18:23:28 -04:00
4a2305170e fix: reauthenticate databricks model discovery (#4008)
## Summary
- preserve Databricks catalog 401 responses as authentication failures
and retry discovery exactly once after silently refreshing the rejected
bearer
- preserve runtime OAuth recovery: when discovery has no usable OAuth
credential, `session/new` succeeds with only the trimmed configured
model so the first `session/prompt` can run the existing browser PKCE
flow
- reject a rejected configured `DATABRICKS_TOKEN` with actionable,
non-interactive guidance; static credentials cannot recover through PKCE
- use the configured-model fallback for non-auth discovery failures
without caching failed or fallback catalogs, so later sessions retry
discovery
- keep known Databricks v2 models only for authenticated empty-catalog
responses and mark their provenance
- resolve discovery before MCP spawn or session registration, preventing
failed discovery from leaking resources or consuming session capacity
- permit serialized interactive PKCE only from the explicit saved-agent
model picker; passive draft discovery never opens a browser

## Runtime flow
1. OAuth discovery attempts cached credentials and silent refresh
without opening a browser.
2. If no usable OAuth bearer exists, `session/new` advertises only the
configured model and succeeds.
3. The first `session/prompt` uses `TokenSource::bearer()`, which may
launch browser PKCE.
4. A later session retries discovery and caches only the authenticated
catalog.

## Regression coverage
- rejected-but-locally-fresh OAuth bearer performs one refresh and one
catalog retry
- OAuth mode with no cached token allows `session/new` and returns
exactly the trimmed configured model
- the OAuth fallback is not cached; a later authenticated session
retries discovery and caches the returned catalog
- rejected static tokens still reject `session/new`
- failed discovery does not consume the sole session slot or spawn the
supplied MCP process
- Desktop interactive/passive auth intent, static-token redaction, and
authenticated empty-catalog provenance

## Verification
- `cargo test -p buzz-agent`
- `cargo test --manifest-path desktop/src-tauri/Cargo.toml --lib
commands::agent_models`
- `cargo clippy --manifest-path desktop/src-tauri/Cargo.toml
--all-targets -- -D warnings`
- `cargo fmt --all -- --check`
- `git diff --check`
- full pre-push hooks

## Review
Adversarial review found and drove fixes for session/MCP resource
leakage, duplicate concurrent PKCE flows, sensitive error propagation,
incorrect 403 reauthentication, missing discovery-level coverage,
passive browser launch, and the Desktop file-size ratchet. The final
follow-up preserves the existing prompt-time OAuth flow while retaining
static-token rejection and pre-allocation discovery ordering.

---------

Signed-off-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
Co-authored-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
2026-08-04 15:20:49 -07:00
WesandGitHub 8faf09f9ae Revert "chore(release): release Buzz Desktop version 0.5.5" (#4797)
Reverts block/buzz#4788
2026-08-04 22:16:48 +00:00
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7andWill Pfleger fcfd41dbff test(desktop): area-4 serialization direction tests for workspace transition helpers
Adds three tests to commands/mesh_llm_tests.rs exercising the lock-serialization
contract between with_workspace_transition_preflight and
install_client_under_workspace_transition:

- test_active_client_stop_then_transition_preflight_succeeds: installs a mock
  client, calls production mesh_stop_client (clears the runtime slot), then
  calls with_workspace_transition_preflight; asserts fail_if_client_mesh_active
  sees an absent runtime and the transition body executes.

- test_transition_held_queued_install_detects_stale_scope: holds workspace_transition
  directly, advances the generation counter and commits a distinct scope; spawns
  an install task that queues on the lock; after the guard drops, install_client_under_
  workspace_transition acquires, detects the stale captured scope (generation +
  full identity mismatch), and returns Err without invoking the injected install
  closure.

- test_install_held_transition_preflight_observes_client: holds workspace_transition
  directly and places a mock client runtime in AppState; spawns a transition task
  that queues on the lock; after the guard drops, with_workspace_transition_preflight
  acquires, runs fail_if_client_mesh_active, observes the installed client, and
  returns Err.

All three tests call the production helpers directly. No port (127.0.0.1:9337) is
touched. Generation-sensitive tests acquire SCOPE_GENERATION_TEST_LOCK to avoid
cross-test interference.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-04 18:10:28 -04:00
a1d78f2959 feat: Buzz entity links — rich preview cards + in-app navigation for repos, PRs, and issues (#4695)
## Summary

Gives Buzz-hosted git entities the same "GitHub-style" chat experience
GitHub links already get: rich preview cards, real titles, and
click-through — except clicks navigate **in-app** to the Projects view
instead of a browser.

- **Spec**: `docs/buzz-entity-links.md` — link scheme, slices, and
deferred work (`buzz://project`, OS deep links, web routes).
- **Canonical `buzz://` deep links**: new
`desktop/src/shared/lib/entityLink.ts` with builders + strict parser for
`buzz://pr?id=…&owner=…&d=…`, `buzz://issue?…`, and
`buzz://repo?owner=…&d=…`, mirrored by a Rust module
(`crates/buzz-cli/src/links.rs`) with a shared golden-format test so the
two implementations can't drift.
- **Preview cards**: `linkPreview.ts` recognizes `buzz://` entity links
*and* HTTPS relay clone URLs (`{origin}/git/<pubkey>/<repo>`, the shape
agents paste today). Both normalize onto the canonical `buzz://` href,
so the two spellings of a repo dedupe to one `Buzz`-provider card
(`BuzzMark` logo) rendered by `link-preview-attachment.tsx`.
- **Title enrichment**: PR/issue cards fetch the real subject from the
relay event (`subject` tag or first content line) via
`useResolvedLinkPreviews.ts`; the cache is community-scoped and reset in
`resetCommunityState()`.
- **In-app navigation**: clicking a card or inline anchor (including
HTTPS relay clone URLs whose origin matches the active relay) routes to
the canonical `30617:<owner>:<d>` coordinate via `goProject()`
(`markdown/entityLinks.tsx`). **Merge dependency: #4671 must merge
first** — route resolution for `30617:` coordinates is implemented on
that branch (`feat/multi-repository-projects`). Entity-link and
external-anchor logic were extracted out of `markdown.tsx` to stay under
the file-size ratchet.
- **Agent side**: `buzz pr open`, `buzz issues create`, and `buzz repos
create` now return a ready-made `link` field (omitted when the relay
returns `accepted: false`), and `base_prompt.md` instructs agents to
paste it verbatim when announcing work.

## Test plan

- [x] Desktop unit tests: pass, including new `entityLink.test.mjs` and
`linkPreview.test.mjs` coverage (golden formats, malformed-link
rejection, clone-URL/`buzz://` dedupe, origin-gated anchor behavior,
label-must-win invariant, cache epoch)
- [x] Rust: `cargo test -p buzz-cli` golden-format test +
accepted/rejected link guard assertions, clippy + fmt clean
- [x] Biome + `tsc --noEmit` clean; pre-push hooks
(desktop-tauri-checks, rust-tests, desktop-test) pass
- [ ] Manual: paste a relay clone URL and a `buzz://pr` link in a
channel — verify one card each, real PR title, and in-app navigation to
the Projects view

Related: [#4671](https://github.com/block/buzz/pull/4671)

---------

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1g8493u0xfsjrvflg4n08ezd7vec99mnwzlv0qgwpr9d7gvjwhuzqx59rhw <41ea58f1e64c243627e8acde7c89be667052ee6e17d8f021c1195be4324ebf04@buzz.block.builderlab.xyz>
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-04 17:54:14 -04:00
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7andWill Pfleger 1dc537dad5 fix(desktop): area-3+4 snapshot import seams, transition helpers
Area 3 — snapshot import phase seams:
- Extract confirm_agent_snapshot_import_core and
  confirm_team_snapshot_import_core, generic over tauri::Runtime.
- Add before_store (post-entry-capture, pre-Phase-3a-lock) and after_store
  (post-Phase-3a-lock, pre-Phase-3b) hooks; no-ops in production.
- ProfilePublish<'a>/MemoryPublish<'a> borrowed arg structs — no secret-key
  cloning across closures.
- Thin Tauri commands call cores with no-op hooks and real relay adapters;
  app.state::<AppState>() inside async move blocks avoids non-'static borrows.
- Delete duplicate submit_engram_event from team_snapshot.rs; reuse import.rs
  version (pub(crate)) for both agent and team engram boundaries.
- Add retain_team_pending_in_scope(scope, team) sibling in teams.rs; team
  snapshot Phase 3 calls it instead of live retain_team_pending to avoid
  re-resolving active scope after a possible switch.
- Move egress-guard + avatar tests from import.rs into import_tests.rs
  (included via #[path]); update egress inventory and allowlists.
- Add SCOPE_GENERATION_TEST_LOCK in scope.rs for cross-module serialization
  of generation-sensitive tests; agent + team seam tests share this lock.
- 4 named seam tests all pass concurrently (verified with cargo test --lib).

Area 4 — workspace transition helpers:
- Extract with_workspace_transition_preflight: acquires workspace_transition,
  runs fail_if_client_mesh_active, invokes transition_body under guard.
- Extract install_client_under_workspace_transition: acquires
  workspace_transition, validates full captured scope identity
  (scope_id, relay, owner_pubkey, generation) under guard, calls install.
- Both helpers live in mesh_llm_scope.rs alongside fail_if_client_mesh_active.
- Area 4 tests (3 named) implemented in mesh_llm_tests.rs (separate commit).

Also: remove unused AppHandle import from nest.rs (zero warnings).
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-04 17:52:26 -04:00