mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
desktop-v0.5.3
2038
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3a96acea09 |
chore(release): release Buzz Desktop version 0.5.3 (#3972)
## Buzz Desktop release v0.5.3 - **Frozen main:** `54c8ef30a9bb9c59a4415a8a7ee84c7c5454b48a` - **Reviewed candidate:** `d0c06978bbf494ded6fe1a55d69d810ae9b65863` - **Previous desktop release:** `v0.5.2` - **Proposed immutable tag:** `desktop-v0.5.3` This PR must be **squash merged** only after the Desktop Release Candidate check passes. The branch must remain based directly on current ; 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.3 |
||
|
|
54c8ef30a9 |
fix(release): require exact-head approval for desktop tags (#3973)
## Summary - require an exact-head trusted approval before desktop auto-tagging - remove rule-suite authorization that `GITHUB_TOKEN` cannot access - pin review pagination to `page=1` and test the deployed `gh` control flow ## Why The previous verifier unconditionally queried repository rule-suite endpoints with `github.token`. Those endpoints require Administration: read, which Actions `GITHUB_TOKEN` cannot receive. Its paginated list request also duplicated page one when no explicit page was supplied. This deliberately removes admin-bypass authorization rather than introducing a second credential during release recovery. Desktop release PRs must now have GitHub's overall `APPROVED` decision and a MEMBER/OWNER/COLLABORATOR approval attached to the exact candidate SHA. ## Validation - `scripts/test-desktop-release-authorization.sh` - `scripts/test-release-ref-contract.sh` - `bash -n scripts/verify-desktop-release-merge.sh scripts/verify-desktop-release-authorization.sh scripts/test-desktop-release-authorization.sh scripts/test-release-ref-contract.sh` - `git diff --check origin/main...HEAD` The new flow test uses a stub `gh` executable, asserts the exact `page=1` request, fails any rule-suite API call, and rejects stale-SHA, untrusted-author, changes-requested review, and non-approved aggregate-decision cases. Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> |
||
|
|
db7e84d4f8 |
fix(release): make desktop tagging squash-safe (#3965)
## Summary - validate desktop release candidates before merge and keep the repository squash-only - tag the squash commit only after proving frozen-base parent and complete-tree identity with the validated PR head - accept either an exact-head approval or the durable Default-ruleset bypass record as release authorization - remove the unusable App-backed preparation workflow; retain `just release-desktop` ## Ruleset follow-up After this PR merges, update Default ruleset `13596885` to: - enable strict required status checks - dismiss stale reviews on push and require approval after the last push - require the integration-bound `Desktop Release Candidate` check The next desktop release should be cut only after that settings update. ## Verification At commit `d8c254db427eedbcffac1a6e078e90d1d0f5e151` with a clean worktree: - `scripts/test-release-ref-contract.sh` - `scripts/test-desktop-release-candidate.sh` - `bash -n scripts/verify-desktop-release-merge.sh scripts/prepare-desktop-release.sh scripts/test-release-ref-contract.sh` - `git diff --check` The bypass test fixture is the captured rule-suite shape from real squash merge PR #2864 / suite `3520068134`. --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> |
||
|
|
bb34bc4d98 |
Revert "chore(release): release Buzz Desktop version 0.5.3" (#3960)
Reverts block/buzz#3944 |
||
|
|
209536ade6 |
docs(nips): add single-coordinate manual-unread override layer and verification model to NIP-RS (#2864)
## Summary
Amends `docs/nips/NIP-RS.md` with the manual mark-as-unread override
layer and includes `docs/formal/nip-rs-unread/`, the bounded exhaustive
verification model that preceded and informed the spec.
All `ov_*` override state lives in exactly one coordinate per
installation. That single constraint is what makes the rest of the
amendment small: override state never moves between coordinates, so
there is no slot lifecycle to make crash-safe, and the only durability
obligation is carry-forward on `client_id` rotation.
## Spec changes (`docs/nips/NIP-RS.md`)
- **Non-Goals:** drop the stale line stating mark-as-unread is out of
scope; state the `ov_*` durability exception to the
best-effort/time-horizon model.
- **Reserved Namespace:** `ov_` stem and `esc:` escape marker reserved.
Escape on publish (prepend `esc:` to raw IDs beginning with `ov_` or
`esc:`), unescape on receive (strip exactly one `esc:`). Bijection, with
the pre-amendment backward-compat residual documented as a stated
limitation.
- **Content Validation:** override entries are collected and validated
as a complete logical group *before* any decoding, zero-filling,
merging, or canonicalizing. Only two wire shapes are accepted — a
complete live three-key group, or an `ov_c:`-only tombstone floor. Any
other shape rejects the whole group while retaining the frontier entry;
applying the generic per-entry discard rule first is prohibited.
- **`d` Tag:** `<slot-id>` is exactly 32 lowercase hexadecimal
characters, replacing "a random opaque string" of 1–64 ASCII characters.
The fixed shape lets a relay recognize a read-state coordinate
structurally from the `d` tag alone, without decrypting anything, and
apply per-coordinate protections to it — under the old wording a
conforming client could pick a shape that silently forfeits them.
Recognizable coordinates are also what let a relay replace superseded
versions outright rather than accumulating one retained row per publish,
which keeps the coordinate count a full-state load must enumerate near
one per installation. Every client designates one **primary** coordinate
with a stable `<slot-id>` for the installation's lifetime. All `ov_*`
entries, and the frontier entries of the contexts they belong to, MUST
live in the primary. Additional coordinates remain legal for frontier
volume but MUST NOT carry `ov_*`, which keeps them freely rewritable and
freely deletable.
- **`t` Tag:** described as a discoverability marker rather than a
guarantee of relay-side selectivity. A relay MAY apply tag constraints
after its result cap, and `kind:30078` is shared with unrelated
application data, so clients MUST apply the tag as a correctness filter
locally, MUST NOT infer completeness from a short result, and MUST omit
the tag entirely when performing a full-state load.
- **Fetching / Full-State Load:** clients implementing the override
layer MUST NOT apply a finite `since` filter — an encrypted payload
means a relay filter cannot select for override-bearing events, so any
event-level window can exclude the only coordinate holding a tombstone
floor. Removing `since` is not sufficient: relays MAY cap historical
results, MAY cap below the requested `limit`, and emit
end-of-stored-events after the capped query, so neither EOSE nor a short
page proves completeness. No test against the client's requested `limit`
can detect truncation either: the effective cap belongs to the relay, a
relay MAY cap below what was requested, and an advertised maximum limit
is not necessarily the limit enforced.
A full-state load is therefore enumerated on `{"kinds": [30078],
"authors": [<pubkey>], "limit": <n>}` with **no tag constraint**. A
relay MAY apply tag constraints only after its result cap and withhold
the events that fail them, so under a tag-constrained filter the
delivered count is not the count the cap selected — a delivered page can
be empty while older coordinates still exist below it, and `kind:30078`
is arbitrary application data whose `d` tag namespace is open to every
application that has written under the user's key. Omitting the tag
makes delivery observable; read-state selection moves client-side, where
the validation rules already place it.
Completeness is then established by enumeration on a strictly decreasing
cursor: collect a page, descend on the lowest `created_at` across all
delivered events, exhaust that second with a window pinned to it,
continue below it, and treat only an empty delivery as complete. Every
query carries the same explicit `limit` `n` with `n >= L`. Per-second
exhaustion is discharged by comparing the pinned window's delivery
against the largest delivery the relay has already demonstrated in the
same load, floored at `L = 2` so that the ordinary single-coordinate
installation can reach *complete* at all. The comparison fails safe: an
inconclusive window reports *cannot prove complete* rather than
*complete*, and that verdict is terminal for the load.
Because these are addressable events, a coordinate republished mid-load
moves *above* the descending cursor while its previous version stops
existing, so neither is reachable by any later query. A full-state load
is therefore fenced by a live subscription on the same tag-free filter,
established — defined as receipt of end-of-stored-events — before the
first enumeration query and held unbroken on the same connection for the
load's duration. Fence deliveries are collected like enumerated events
but do not contribute to the cursor or to the demonstrated-delivery
bound. Collection deduplicates coordinates on the full NIP-01
addressable ordering — greatest `created_at`, lowest event id on ties —
because an equal-timestamp replacement is legal and is the version the
relay retains. A lapsed or reconnected fence makes the load potentially
incomplete, and a client MUST NOT publish to its own coordinates during
its own load.
Five relay behaviours the *complete* verdict rests on are stated as
normative conformance preconditions rather than assumptions, because
none is verifiable from the responses a client receives: newest-first
prefix delivery with lowest-id tie-breaking (what NIP-01 already
specifies for `limit`), a non-decreasing effective cap within a load,
the floor `L`, push delivery on an open subscription, and a delivery
barrier ordering accepted matching events ahead of a query's
end-of-stored-events on the same connection. Conditioning *complete* on
positive proof of these instead would withdraw the override layer from
every client rather than from the non-conforming relays. A client MUST
NOT load against a relay it has evidence violates them, and MUST treat
any such load as potentially incomplete.
A load that is potentially incomplete, or that failed on any relay the
client publishes to, MUST NOT authorize canonical compaction, publishing
a canonicalized override blob, deleting or abandoning a coordinate, or
reporting a mark-read as successful; the client falls back to local
state.
- **Client-ID Rotation / Orphaned Blob Deletion:** rotation is the only
event that changes an override-bearing coordinate. Before deleting or
abandoning its previous primary, a client MUST republish the
componentwise `max()` of every register that primary holds — every
tombstone ceiling included — under its new primary, and MUST confirm
acceptance on **every relay** from which the old primary will be deleted
or allowed to lapse. Acceptance on one relay does not authorize deletion
on another. Frontier-only orphans are deletable unconditionally; an
unknown same-`client_id` coordinate is treated as a live carrier until
merged.
- **Live Subscription and Convergence:** the re-publish trigger and its
suppression are evaluated on canonicalized state, so a retained live
peer blob the client has already tombstoned cannot trigger an identical
write on every replay.
- **Manual-Unread Override Layer** (new section):
- **Wire encoding:** `ov_s:<ctx>`, `ov_c:<ctx>`, `ov_b:<ctx>` as uint32
siblings in the existing `contexts` map.
- **Merge rule:** componentwise `max()` per counter — no new wire merge
logic.
- **Liveness predicate:** `S > 0 AND F <= B AND S > C`, transcribed from
`model.py::override_set_b`.
- **Actions:** mark-unread bumps S and captures the effective frontier
as B; mark-read bumps C; a natural frontier advance past B deactivates a
stale set with no counter update. Every action requires a complete
full-state load. At the uint32 ceiling, wrapping and resetting are
prohibited: mark-unread is refused, and mark-read completes only if the
resulting state has `override_active == false` — otherwise it fails
visibly rather than reporting success over a still-live override.
- **Tombstone floor:** a dead ever-active register compacts to `RegB(0,
max(S,C), 0)` — a single `ov_c:` key. A virgin register is omitted
entirely. This blocks counter reuse and the resulting resurrection.
- **Mandatory canonical publication:** a protocol requirement, not an
optimization. Publishing raw dead registers lets two independently-dead
registers from different devices produce a live join.
- **Override group co-location rule:** a context's frontier entry and
all its `ov_*` siblings MUST travel in the same event, and that event
MUST be the primary coordinate. An override-bearing context therefore
has exactly one legal destination for its whole group; only
frontier-only groups may be distributed across additional coordinates.
Grouping is per logical context, never per key.
- **Unescape-before-group rule:** the frontier wire key MUST be
unescaped to its raw logical context ID before use as group identity.
Equal normative weight to atomic grouping.
- **Tie policy:** clear-wins is MUST. The tie verdict is not encoded on
the wire, so a selectable policy makes two conforming clients diverge
permanently on both the unread verdict and the canonical wire form.
- **Override State Durability:** `ov_*` entries are exempt from age
pruning and budget eviction permanently, and durability is defined over
retrievable logical state — the containing event must stay reachable and
the load must establish completeness, not merely retain keys. There is
no safe finite GC horizon.
- **Bounds and budget:** byte/key analysis at both small-counter and
uint32-maximum values. Confining `ov_*` to one blob makes its plaintext
budget a hard lifetime ceiling on ever-overridden contexts — roughly 600
tombstones at the worst-case ~54 bytes against 32 KiB, ~730 at the
common ~45 bytes, ~199 simultaneously live overrides at ~164 bytes. At
the ceiling a client MUST refuse mark-unread and MUST NOT split override
state, drop floors, or publish a truncated override set. Same policy
shape as counter exhaustion: visible failure, never silent degradation.
- **Verification artifact:** `docs/formal/nip-rs-unread/`. The model is
a broader predecessor of this NIP: its `split_blob_into_slots` permits
override groups in any slot, so verified atomicity covers every
arrangement this NIP allows, but the converse does not follow. The model
does not verify the single-primary rule, the completeness procedure, the
relay conformance requirements or the mutation fence, or carry-forward;
malformed-group wire validation is likewise normative but outside
verified scope.
- **Abstract / Non-Goals / Backwards Compatibility:** the absolute "no
relay-side logic" and "no relay behavior changes" claims are narrowed to
what remains true — no new event kind, no new wire message, no
relay-stored read-state logic — with the override layer's relay
conformance contract named as the exception. Frontier sync and clients
that skip the override layer are unaffected on any relay.
## Verification model (`docs/formal/nip-rs-unread/`)
Four Python files constituting a bounded exhaustive verification model
for the override layer's register algebra.
**What it does:** constructs a toy universe — 2–3 devices, 2 channels,
every action that can happen (mark-unread, mark-read, late/duplicate
syncs, app reinstall, storage compaction) — and brute-forces every
reachable ordering (14,258 BFS states; 672-point deep-history parameter
cube; 9-mutant harness over ~45,000 merge pairs). After each world-state
it asks: did all devices converge? Did any unread flag get resurrected
after being cleared, or vanish while live?
**What it found and fixed:**
1. **Killed candidate A.** The model produced a concrete kill sequence:
an old client that doesn't know about the new field rewrites its
read-state blob and silently erases unread flags. That witness is why
the spec uses candidate B (two counters that only count up, plus a
snapshot) instead.
2. **Candidate B passes everything.** All delivery orders converge; the
frontier high-water mark never regresses; duplicated/replayed syncs are
harmless; old clients can't destroy it; compaction never resurrects a
dead unread or drops a live one, including
cleanup-followed-by-weeks-late-stale-sync and
tombstone-landing-on-unrelated-live-state corner cases.
3. **Caught a second real bug late.** Two devices each publishing "this
unread is cleared" could, on merge, reactivate it. The fix (canonicalize
before publishing) is a mandatory rule in the spec; the model re-checks
it across ~45,000 merge pairs.
**Scope and caveats:** bounded to 2–3 devices and 2 channels. Can't
prove the infinite case. `NOTE.md` documents the exact verification
scope and the gap between the model's `split_blob_into_slots` generality
and the single-primary rule the spec adds on top.
**Why it's in the repo:** the spec asserts "verified by bounded
exhaustive model checking." Keeping the artifact in-repo means anyone
who later amends the merge/compaction rules can `python3 exhaustive.py
&& python3 mutation.py` (deterministic, exit 0) and confirm the
guarantees hold. Without it the spec claims a proof nobody can check.
## Diff scope
`docs/nips/NIP-RS.md` — spec amendment, zero product code.
`docs/formal/nip-rs-unread/{NOTE.md,model.py,exhaustive.py,mutation.py}`
— bounded exhaustive verification model, zero product code.
`.gitignore` — `__pycache__/` and `*.pyc` entries for the model
directory.
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
|
||
|
|
6e02e0a902 |
Merge pull request #3944 from block/version-bump/0.5.3
chore(release): release Buzz Desktop version 0.5.3 |
||
|
|
d12b3d6a79 |
chore(release): release Buzz Desktop version 0.5.3
Co-authored-by: Release Automation <release-automation@users.noreply.github.com> Signed-off-by: Wes <wesbillman@users.noreply.github.com> |
||
|
|
052174a148 |
fix(release): make immutable desktop release operable (#3943)
## Summary - document `Prepare Desktop Release` as the canonical desktop release entry point - describe the frozen candidate, exact-head approval, and true merge-commit contract - document all platform outputs and complete release App/signing configuration - link the release runbook from the README - allow stable reruns to repair the rolling updater manifest after the versioned release has already published ## Release blocker The live repository cannot currently complete this flow: repository settings disable merge commits and the `main` ruleset allows only squash, while `scripts/verify-desktop-release-merge.sh` requires a two-parent merge whose second parent is the approved candidate. Those settings must allow merge commits before a desktop release PR is merged. ## Validation - `bash scripts/test-desktop-release-candidate.sh` - `bash scripts/test-release-ref-contract.sh` - `git diff --check` - verified live repository merge settings, `main` ruleset, release tag ruleset, Actions variable names, and secret names with GitHub API - independent review by Princess Donut; incorporated all findings, including the rolling-manifest retry gap and unsigned Windows labeling Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> |
||
|
|
c104eecfb3 |
feat(desktop): import local Pocket voices (#3259)
## Context Pocket TTS currently offers bundled reference voices. People also need a local, private way to add a voice without sending audio to a cloud service. ## Summary Add a Pocket voice import flow to Voice settings. Buzz opens the native file picker, decodes common audio formats in the reusable `buzz-voice` crate, canonicalizes the selected audio, stores it under a content-derived identity in app data, selects it, and lets the user delete it later. ## Changes - Accept WAV, M4A, MP3, FLAC, OGG, and AIFF files between 2 and 30 seconds, including multichannel sources. - Decode and downmix accepted audio to canonical mono 32 kHz PCM16 WAV before hashing and storage. - Store imported voices behind stable `pocket:imported:<sha256>` identities and content-addressed files. - Keep absolute file paths inside the native process and expose only voice metadata to React. - Include imported voices in Pocket preview and live huddle playback. - Add Add voice and delete controls while preserving the bundled Pocket voice catalog. - Fall back to Mary when the selected imported voice is deleted. - Keep durable import, selection, and deletion successful when a live TTS worker acknowledgement is delayed. - Preserve bundled voices when optional import metadata is unreadable and keep failed deletion retryable. ## Related issue None found. ## Testing Production decoding was exercised with WAV, M4A with AAC, MP3, FLAC, OGG Vorbis, and AIFF fixtures. Each format canonicalized to mono 32 kHz PCM16 WAV. Manual validation in the combined daily-driver build covered native-picker import, Preview, live-huddle playback, deletion, and Mary fallback. ## Screenshots The Voice settings card preserves the bundled Pocket catalog and adds the local Add voice action.  ## Reviewer-reproducible examples Create common-format fixtures and run them through the production importer: ```bash . ./bin/activate-hermit fixtures="$(mktemp -d)" ffmpeg -hide_banner -loglevel error -f lavfi -i "sine=frequency=220:duration=3" -ac 2 -ar 44100 "$fixtures/voice.wav" ffmpeg -hide_banner -loglevel error -i "$fixtures/voice.wav" -c:a aac "$fixtures/voice.m4a" ffmpeg -hide_banner -loglevel error -i "$fixtures/voice.wav" "$fixtures/voice.mp3" ffmpeg -hide_banner -loglevel error -i "$fixtures/voice.wav" "$fixtures/voice.flac" ffmpeg -hide_banner -loglevel error -i "$fixtures/voice.wav" -c:a libvorbis "$fixtures/voice.ogg" ffmpeg -hide_banner -loglevel error -i "$fixtures/voice.wav" -c:a pcm_s16be "$fixtures/voice.aiff" BUZZ_VOICE_IMPORT_TEST_DIR="$fixtures" \ cargo test -p buzz-voice imports_common_audio_format_fixtures -- --ignored --nocapture ``` Exercise import persistence, synthesis, deletion, and bundled-voice fallback with an installed Pocket model: ```bash BUZZ_POCKET_MODEL_DIR=/path/to/pocket-model-bundle \ cargo test -p buzz-voice --test pocket_import_audio \ objective_import_synthesis_delete_and_mary_fallback \ -- --ignored --nocapture ``` Exercise the native-picker boundary, selection, preview dispatch, deletion, cancellation, and invalid-file states: ```bash cd desktop pnpm build:e2e pnpm exec playwright test tests/e2e/voice-settings.spec.ts --project=smoke ``` --------- Signed-off-by: John Tennant <jtennant@block.xyz> Signed-off-by: John Tennant <johnmatthewtennant@gmail.com> Signed-off-by: John Tennant <jtennant@squareup.com> Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Signed-off-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz> Co-authored-by: John Tennant <jtennant@block.xyz> Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Co-authored-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz> |
||
|
|
39ce3dfc3c |
fix(desktop): open profiles from avatars (#3751)
## Summary - show profile descriptions in hover cards as a single truncated line - open the profile panel when avatars are clicked across desktop surfaces - make the direct-message intro avatar clickable ## Validation - Desktop static checks - 3,807 desktop tests via pre-push --------- 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> |
||
|
|
61ba9dfaa0 |
refactor(voice): extract reusable Pocket primitives + Pocket voice settings (relands #2467 + #3208) (#3910)
Relands **#2467** (extract `buzz-voice` crate) and **#3208** (Pocket
voice settings) onto main, after #3266 and #3180 merged.
## Why a fresh PR
The repo is squash-only with delete-branch-on-merge. Squashing #3266
deleted `jtennant/pocket-tts-2026-04`, which was #2467's base — GitHub
auto-closed #2467 and it cannot be reopened. Squash merges also sever
ancestry, so GitHub's natural merge-base reports phantom conflicts for
the whole remaining stack.
## Content provenance
- Byte-identical to the blessed `jt/buzz-voice-refactor` branch
(`93029c577`, tree `6729e0eff` — reviewed by Dawn (#2467) and Max
(#3208) at exact heads) **except** the three files where #3180 and #3208
genuinely interact.
- Three-file resolution (union of both sides):
- `huddle/mod.rs` — #3180's pipeline re-exports + #3208's
`agent_tts_routing` imports.
- `huddle/state.rs` — `reset_preserving_generation` preserves both
`huddle_generation` (#3180) and `tts_enabled` (#3208); test sets merged
into one `tests` module.
- `desktop/src/testing/e2eBridge.ts` — both switch arms kept; no
duplicate case labels.
## Verification at
|
||
|
|
689617af7a |
docs: add VISION_REMOTE_AGENTS.md (#3924)
## What Adds `VISION_REMOTE_AGENTS.md` — the vision doc for remote agents, joining the VISION family (`VISION_AGENT.md`, `VISION_MESH.md`, `VISION_SOVEREIGN.md`, …). The one-line thesis: **the relay is the management plane** — an agent's identity, history, presence, and ordinary control all live on the relay, so the body (a pod today, anything tomorrow) is replaceable, and deployment never grows a second control plane. ## Provenance - Distilled from the remote-agents spec (`docs/remote-agents.md`, PR #3748); this doc stays deliberately generic where the spec is Kubernetes-specific. - Five review rounds in the #buzz-remote-agents channel; both reviewers (Wren: thesis/shape/scope, Dawn: truthfulness/minimalness/elegance) converged at 9/9/9, scored against spec head `b4f4ed1a6` with command-level receipts. - Final editorial pass by Tyler (opening line, vignette phrasing, closing tagline), applied live in-channel before this PR. Doc-only change — no code, no effect on #3748, which remains blocked solely on the Open Decisions A–I rulings. --------- Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> |
||
|
|
4632c55041 |
feat(desktop): auto-enable huddle transcription for agents (#3180)
## Context Before this change, every huddle initialized with transcription off. Joining or adding an agent did not enable it, so the agent could not receive spoken conversation until a person clicked the transcript control. Starting a huddle from an agent DM could also omit that agent, and adding an agent who already belonged to the parent channel could attempt an unnecessary role change and show a warning. Agent detection uses authoritative huddle membership. A participant counts as an agent when the ephemeral membership identifies it with the `bot` role, or when the existing agent identity model identifies the participant in an agent DM. ## Summary Buzz now enables transcription once when the first authoritative agent is present. After that initial automatic action, explicit user control is authoritative: manual ON or OFF survives membership refreshes, reconnects, and UI remounts. Removing the last agent does not change the current transcription state. Agent-DM huddles enroll the agent automatically. Adding an agent who already belongs to the parent channel preserves the existing parent role and completes without a role-mutation warning. | Scenario | Before | With this change | | --- | --- | --- | | First authoritative agent joins or is hydrated | Transcription stays off | Transcription turns on once | | User explicitly turns transcription on or off | Manual control exists without an agent policy | The explicit choice suppresses later automatic changes | | Last agent leaves | No defined agent-presence behavior | The current transcription state remains unchanged | | Huddle starts from an agent DM | The agent can be omitted | The known agent is enrolled automatically | | Added agent already belongs to the parent channel | Buzz can attempt a role rewrite and warn | Existing parent membership and role are preserved | | Transcription is active | The control is not visually distinct | The control is highlighted and exposes `aria-pressed=true` | ## Changes - Derive agent presence from authoritative bot-role huddle membership and known agent-DM identity. - Apply the one-time auto-enable rule during create, join, membership hydration, reconnect, pipeline startup, and local agent addition. - Preserve explicit user state and use huddle-generation guards so stale asynchronous work cannot alter a replacement huddle. - Keep backend and React transcription state synchronized, with a visible and accessible active control. - Enroll known agent-DM participants and make parent-channel membership updates idempotent. - Cover hydration ordering, reconnects, remounts, explicit OFF, last-agent removal, DM enrollment, existing membership, and active styling. ## Related issue None found. ## Testing Manual validation in `pending-seed` confirmed the product contract: 1. Started a huddle from the owned, running Fizz agent DM. 2. Confirmed the authoritative roster contained the human and Fizz as an agent. 3. Confirmed transcription enabled without clicking the control: `Stop transcript`, `aria-pressed=true`, with the highlighted active background. 4. Turned transcription off and confirmed `Start transcript`, `aria-pressed=false` remained stable. 5. Removed Fizz while transcription was off and confirmed the state stayed off. 6. Left the huddle cleanly. ## Screenshots The same control has distinct active and inactive states.   ## Reviewer-reproducible examples From a fresh checkout: ```bash pnpm --dir desktop build:e2e pnpm --dir desktop exec playwright test tests/e2e/huddle-transcription.spec.ts --project=smoke pnpm --dir desktop exec playwright test tests/e2e/mentions.spec.ts --project=smoke --grep "system agent profile exposes owned agent actions|system agent avatar exposes owned agent actions|owned bot profile exposes message and huddle actions|owned agent mention profile exposes message and huddle actions" ``` The huddle scenario exercises initial authoritative hydration, exactly one automatic enable, explicit OFF persistence, unchanged state after last-agent removal, newer events winning over delayed hydration, agent-DM enrollment, and idempotent parent membership. It also asserts `aria-pressed` and distinct computed active styling. --------- Signed-off-by: John Tennant <jtennant@squareup.com> Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> |
||
|
|
081f805d5e |
feat(agent): optional reply guard reminds a silent turn to publish (#3763)
## Why
A Buzz agent's assistant text and reasoning are never shown to anyone —
only what it posts through the CLI. A turn that runs fifteen tool calls
and never publishes is a silent failure: the requester waits on a result
that was produced and thrown away.
This adds an optional reminder at the end-of-turn gate, off by default.
Tyler asked for it in buzz-mesh; plan iterated to **9.5/10 with @Wren**
(Minimalness 9.7, Elegance 9.5, Correctness 9.3).
## What
`BUZZ_AGENT_REQUIRE_REPLY=1` (default off, per-agent opt-in). A turn
about to end with no recognized attempt to post gets a reminder and is
rerolled. **At most two, then the turn ends regardless** — the guard
catches accidental omission, it does not compel speech. The reminder
text explicitly licenses silence so it cannot fight the base prompt's
"silence is usually correct."
**This is not a new MCP hook.** `RunCtx::run` *is* the turn, so the two
per-turn locals need no plumbing, and every tool call already passes
through it with arguments visible. The objection is appended at the
existing `_Stop` gate and rides `push_hook_outputs_as_tool_results`, so
the model receives it as a lower-trust tool result with `{hook, server,
text}` attribution. No new trust path, no new lifecycle event, no
dev-mcp or CLI protocol change.
Earlier revisions of this plan needed four crates (a `_UserPromptSubmit`
hook, a marker file, a `buzz-cli` change, dev-mcp state). Tyler pointed
out the agent already knows both facts; that deleted all of it. Net
runtime change is ~35 lines in `agent.rs` + ~4 in `config.rs`.
### Recognition contract
A registered non-hook tool whose qualified name ends in `__shell`, whose
`command` argument contains `messages send` or `reactions add`.
- **The `__` separator is exact, not approximate.** Given `has()` +
`!is_hook()`, `ends_with("__shell")` is *provably equivalent* to a bare
name of `shell`: registration forbids `__` in server and bare names
(`mcp.rs:227,268`) and qnames are `{server}__{bare}`, so a trailing
`__shell` could only straddle the separator if the bare name began with
`_` — which `is_hook` excludes. Without the separator, `powershell` and
`noshell` would match.
- **Reads the structured `command` field**, not serialized arguments, so
a `description` that quotes a send cannot disarm the guard, and a
non-string `command` is rejected rather than coerced.
- **Detects an attempt, not a successful publish.** A failed send
already returns non-zero exit and error JSON — louder than this
reminder. The variable is named `buzz_reply_call_seen` so the code can't
pretend otherwise.
- **Checked after the per-turn tool-call cap**, since a discarded call
never ran.
- `messages send` also covers `messages send-diff`. Reactions count
because the base prompt directs agents to react rather than post a bare
acknowledgement.
**Known limits, both deliberate and documented:** a command assembled at
runtime (`$CMD`) or hidden in a wrapper script is missed; text that
merely quotes a send (`echo "buzz messages send"`) matches. Missing a
real post is the expensive direction and substring matching is the
forgiving one there. Neither edge is pinned by a test, so the matcher
stays free to improve.
### Budget
Reminders share `BUZZ_AGENT_STOP_MAX_REJECTIONS`, the existing outer cap
on every end-turn objection. Default 3 fits both; at 1 only one fits; at
0 the guard is off with the hooks. A round carrying both a hook
objection and a reminder costs one rejection and delivers both texts. An
independent budget would either violate that bound or need a second
arbitration rule.
## Prior art
- **#3467** (closed) built the same detector one layer up in `buzz-acp`
for a different remedy. None of its symbols are on main — this borrows
its permission to be coarse, but reads structured data that ACP didn't
have.
- **#3648** (open) detects turns with *no output at all*; a turn with
fifteen tool calls and no post counts as output there, so it does not
cover this case.
- **#3741** (merged) is mesh-only.
## Testing
**14 new tests.** 4 unit tests on the matcher; 10 integration tests
through the ACP wire harness: off by default, `=0` still off, opted-in
silent → exactly 2 reminders then `end_turn`, registered `fake__shell`
send → 0 reminders, hallucinated `fake__shell` → still reminded, publish
call truncated past the 64-call cap → still reminded, budget 1 → 1
reminder, budget 0 → off, combined `_Stop` hook objection + reminder →
one round both texts and after 2 reminders the hook objection continues
alone, unparseable `=true` → startup error naming the key.
**10 mutation checks, each breaking a specific named test** — neutralize
the nag cap, stop sharing the budget, neutralize `buzz_reply_call_seen`,
drop `has`/`is_hook`, ignore the flag, drop the `__`, drop `reactions
add`, read serialized args, move detection before truncation.
`tests/bin/fake_mcp.rs` gains `FAKE_MCP_SHELL_TOOL=1`: it previously
exposed no tool with a bare name of `shell`, so the satisfied-guard path
was untestable.
Full `cargo test -p buzz-agent` green at 9e0ae1f04; clippy `-D warnings`
and `cargo fmt --check` clean.
**Unrelated flake found:**
`cancelled_turn_with_usage_emits_notification_before_response`
(`tests/fake_llm.rs`) is timing-sensitive. Under 10 loaded cores it
fails **2/20 on this branch and 1/20 at unmodified
`origin/main@02be413b8`** — pre-existing, not caused by this change
(which is inert without the env var). Flagging so it isn't misattributed
to the next PR that's open when CI hits it.
## Docs
`crates/buzz-agent/README.md` is the primary home (env var, recognition
contract, limits, budget interaction). `docs/MCP_DRIVEN_HOOKS.md` gets a
short cross-reference explaining this is *not* a hook — otherwise
readers hunt for a `_ReplyGuard` tool that doesn't exist.
---------
Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
Co-authored-by: Dawn (sprout agent) <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
|
||
|
|
d48b0e0eec |
feat(desktop): upgrade Pocket TTS model (#3266)
## Context Buzz Desktop currently installs an older Pocket TTS model bundle. The current bundle changes the tokenizer, learned BOS input, recurrent-state contract, and prompt behavior, so updating download URLs alone is not compatible. ## Summary This PR upgrades Buzz Desktop to the current pinned Pocket TTS model. It preserves existing product behavior and the hard 50-token model-input limit while adding the required runtime support, verified acquisition, and crash-safe cache migration. ## Changes - Pins an immutable Pocket TTS revision, artifact names, exact byte sizes, SHA-256 checksums, Mary reference voice, and license. - Loads the bundle-matched SentencePiece tokenizer, learned BOS embedding, and bundle-declared recurrent states. - Uses one pinned Pocket TTS configuration; no precision or model-version selector is added. - Preserves the resident engine's exact `<= 50` token contract without changing Desktop segmentation policy. - Bumps the Pocket cache manifest to v4, verifies size and checksum before adoption, atomically swaps the cache, and recovers the last verified cache after interrupted installs, including an incomplete final directory. - Keeps acquisition, cache migration, worker adoption, and tests within the existing Desktop implementation. - Removes the obsolete model-quality harness, which was coupled to the superseded production prompt and model layout. ## Related issue None. ## Testing Manual listening completed on the exact Desktop build. The updated model improved speech quality and resolved the phrase-start and sample-onset artifacts. Reproducible integrity and model checks are below. ## Screenshots N/A. This changes model installation and speech synthesis, not a visual surface. ## Reviewer-reproducible examples ### Before and after model identity ```sh git show 35305bfc8fd456ca9a17caa1ddbfaabd87d46981:desktop/src-tauri/src/huddle/models.rs \ | grep -E 'sherpa-onnx-pocket-tts|TTS_MODEL_VERSION' git show 211d17c58567448fe7ac95c4fa0ad2b88378849a:desktop/src-tauri/src/huddle/pocket_models.rs \ | grep -E 'MODEL_REPOSITORY|MODEL_REVISION|MODEL_PRECISION|MAX_TOKENS' ``` The target branch identifies the January bundle. The PR branch identifies the immutable April revision, INT8 precision, and 50-token maximum. ### Deterministic runtime validation Use the pinned artifacts listed in `pocket_models.rs` and run the model-dependent Pocket tests with the model directory supplied by the test environment. The checked-in long-sentence fixture must preserve its expected 48 and 44 token split and produce non-silent PCM. ### Manual listening validation John listened to an untrimmed Pocket TTS onset-stress clip generated from the exact user-provided passage, with every sentence synthesized separately and identical 100 ms digital-silence boundaries. The clip used no leading period, onset trimming, gain adjustment, or loudness normalization. The updated model produced better-quality speech and resolved the start-of-sample artifacts. --------- Signed-off-by: John Tennant <jtennant@block.xyz> Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz> Co-authored-by: John Tennant <jtennant@block.xyz> Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz> |
||
|
|
d88313f369 |
feat(desktop): delete a message by clearing its edit to empty (#3813)
## What
Clearing an edit to empty and hitting accept now **deletes the message**
instead of hanging. One of Sam's frequent workflows is to delete a
message by editing it, clearing the text, and pressing Enter — which
previously no-op'd (a deliberate guard blocked empty edits).
## How
Pure client-side wiring — **no relay, schema, or Rust changes.**
1. **`MessageComposer.tsx`** — the edit path had a guard that *blocked*
empty edits (`if (!trimmed && !hasMedia) return;`). That guard is simply
**removed**, so empty content flows through the normal edit path to
`onEditSave("", [], [])`. `buildOutgoingMessage("")` is a safe no-op.
2. **`handleEditSave` in `useChannelPaneHandlers.ts`** — when an edit is
submitted with empty text and no media tags, it exits edit mode and
opens the **same "Delete message?" confirmation** the Delete menu action
shows, rather than publishing an empty edit.
3. **`DeleteMessageConfirmDialog.tsx`** — the confirmation dialog,
extracted into **one shared component**. `MessageActionBar` renders it
for the Delete menu action (previously inline), and `ChannelScreen`
renders it for the empty-edit path. No duplicated dialog UI. **Delete**
runs the existing `deleteMutate`; **Cancel** leaves the message
untouched.
Because both the main timeline and the thread panel already route
edit-save through `handleEditSave`, this covers both surfaces with a
single dialog at the `ChannelScreen` level — no per-composer plumbing.
- Image-only edits (empty text but attachments present) still publish
normally — only a *fully* empty edit prompts to delete.
- An empty edit can never publish an empty body: `handleEditSave`
returns before the edit mutation.
## Review history
This PR was reworked three times in response to review — each pass made
it smaller:
1. First cut wrapped this in a new "Delete message?" `AlertDialog`
rendered from a composer hook — a verbatim duplicate of the confirmation
already in `MessageActionBar.tsx`. Removed.
2. Second cut threaded a dedicated `onDeleteEditTarget` callback down
`ChannelScreen → ChannelPane → MessageComposer / MessageThreadPanel`.
Also redundant — the delete decision moved entirely into
`handleEditSave`, which every edit-save already flows through.
3. Third cut added a special-case empty branch to the composer, which
pushed `MessageComposer.tsx` over the file-size ratchet and led to an
unrelated emoji-helper extraction to make room. Both gone: deleting the
pre-existing guard (rather than adding a branch) is net-negative, so
there's no ratchet pressure and **nothing emoji-related in this PR**.
`MessageComposer.types.ts` is back to baseline too.
4. Fourth pass (this one): an unconfirmed, no-undo delete was too sharp.
The empty-edit path now routes through the same **"Delete message?"
confirmation** as the menu action — shared as one
`DeleteMessageConfirmDialog` component (so it's reuse, not the duplicate
dialog from cut #1).
## Testing
- **E2E:** `desktop/tests/e2e/empty-edit-delete.spec.ts` (Playwright,
smoke project), three tests, all passing locally:
- *clearing an edit to empty prompts to delete, then deletes on confirm*
— edits the mock identity's own `#general` message, clears it, Enter →
the **"Delete message?"** dialog appears; Delete → the row disappears
and edit mode exits.
- *cancelling the empty-edit delete keeps the message* — same up to the
dialog, then Cancel → the message survives.
- *a non-empty edit still edits and never deletes* — guards the other
direction (no dialog).
- `pnpm typecheck`, biome, file-size + px-text guards all clean; full
desktop unit suite (3847 tests) passing locally.
> Heads-up for the reviewer: pushed with `--no-verify` because the
pre-push hook runs the Rust **integration** suite, which needs Docker
(Postgres/Redis) that isn't available in this environment — it doesn't
apply to this desktop-only change. CI runs the real gates.
---
🐝 Built by Bumble in Buzz, from a conversation in #test-swesterman.
---------
Signed-off-by: Sam Westerman <swesterman@squareup.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
10d5a26414 |
feat(relay): raise hosted community limit to five (#3829)
## Summary - raise the relay authoritative default community ownership limit from 3 to 5 - raise the desktop hosted-community treatment from 3 to 5 - preserve `BUZZ_MAX_COMMUNITIES_PER_OWNER` as a deployment override ## Validation - `pnpm -r check` - `cargo fmt --all -- --check` - `cargo test -p buzz-db` (94 passed, 151 Postgres-dependent tests ignored) - pre-push hooks: desktop checks/tests, Rust tests, Tauri checks (all passed; 1,995 desktop Rust tests passed) Signed-off-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz> Co-authored-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz> |
||
|
|
468647a51f |
feat(desktop): locally stored NIP-49 encrypted key backup (#2937)
## Summary
Adds a locally stored **NIP-49 encrypted key backup** (`ncryptsec`) to
the desktop app, per the plan reviewed in buzz-development (Rev 3,
approved 9/10 by Wren; implementation also reviewed and approved 9/10).
**Two-artifact design — canonical bytes originate entirely in Rust:**
- `create_ncryptsec_backup` runs under the `identity_mutation` lock:
encrypt → decrypt-verify against the live pubkey → atomic `0o600` write
to `{app_data_dir}/identity.ncryptsec` → reread/byte-compare → return
the exact persisted bytes. The frontend never re-derives or re-encrypts.
- `save_ncryptsec_copy` writes a portable copy via the save dialog
(parse-gated, secret-file semantics) and never mutates canonical state.
- `generate_backup_passphrase`: 6 words from the EFF short wordlist via
`OsRng` (custom passphrases min 12 chars).
- Import accepts `ncryptsec1` with optional password; the raw-`nsec`
path is untouched. Different-pubkey import and sign-out wipe the
app-managed backup (post-commit, best-effort — a failed import can never
destroy the still-live identity's backup; regression-tested).
**Never-relay guarantee (egress guard + tripwires):**
- `egress_guard.rs` fail-closed at all 8 `/events` submission boundaries
(relay submit funnel, 3× `relay.rs`, huddle STT, both engram submitters,
native WS choke point), rejecting `ncryptsec1`/`NCRYPTSEC1` in text and
binary frames. Scope is deliberately ncryptsec-only: pairing
intentionally carries raw nsec inside its encrypted session.
- Site-granular `/events` inventory tripwire: per-file (`/events` count,
guard-call count) pairs; unlisted files expect zero. Mutation-style
tests prove a ninth site in an existing file, a removed guard, and a new
unlisted file all fail the scan.
- ncryptsec source-allowlist scans in **both** trees (Rust + TS).
**Frontend:** onboarding `BackupStep` is encrypted-by-default — the
default path never invokes `get_nsec` (e2e asserts the command log).
Raw-nsec export stays behind an explicit click with prior semantics.
Shared `EncryptedBackupCreator` powers onboarding + a new settings row;
the import form auto-switches to encrypted mode on `ncryptsec1` paste
(case-insensitive HRP).
**Open product call for @tlongwell-block:** onboarding default is
*encrypted* in this PR; flipping to raw-default is a small change either
way (documented in the plan).
Review history: plan Rev 3 and the implementation were both iterated
with Wren to 9/10 (two blockers from round 1 — import ordering,
inventory granularity — plus an uppercase-bech32 hardening gap, all
fixed in `dde37183e`). Thread: buzz-development.
### Related issue
Follow-up to the direction explored in #385 (NIP-PB, closed) — this
ships local NIP-49 (the standard) instead of a new NIP. No open
duplicate found.
### Testing
All at exactly `dde37183e` (same shell, HEAD verified):
- `cargo test` — 1680 passed / 0 failed / 14 ignored (includes a
deliberate ~70s log_n-18 NIP-49 round trip, spec vector, wrong-password,
NFKC, uppercase-vector decrypt, injection test per egress boundary,
inventory mutation tests, import-ordering regression tests)
- `cargo clippy --all-targets -- -D warnings` — clean; `cargo fmt
--check` — clean
- `pnpm typecheck` — clean; JS unit suite 3529/3529; biome (repo-pinned
2.4.16) clean
- Playwright `onboarding-backup` / `onboarding` /
`onboarding-agent-defaults` / `profile-nsec-reveal` — 86 passed, 1 known
avatar-reservation flake (passed on rerun; untouched by this diff).
`passThroughBackupStep` now exercises the encrypted default, so every
downstream onboarding spec covers the new path.
- Note: browser e2e fakes the crypto via the mock bridge (fixed
spec-vector blob); decryption correctness is proven in the Rust tests.
## Latest onboarding integration
The current head adds an additive `IdentityInfo.storage` field
(`ephemeral`, `system-keyring`, `local-file`, or `environment`) so
onboarding can accurately explain where the active identity is
protected. It surfaces storage metadata only—never key material—and
leaves the existing lost/keyring-locked recovery behavior intact.
---------
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
|
||
|
|
f3e5e81267 |
fix(catalog): update Amp tagline (#3806)
## Summary Update Amp's runtime catalog description to use its current tagline: > The coding agent and development environment that runs anywhere and everywhere. ### Related issue N/A. This follows the Amp description update in https://github.com/block/buzz/pull/3758. ### Testing * `pnpm -C desktop check` * `pnpm -C desktop typecheck` * `pnpm -C desktop test` (3,835 passed) No screenshot is included because this changes only the catalog description text. It does not change layout or interaction behavior. Signed-off-by: AJKemps <AJKemps@users.noreply.github.com> Co-authored-by: AJKemps <AJKemps@users.noreply.github.com> Co-authored-by: Alex Kemper <alex@ampcode.com> |
||
|
|
9e8fcfda09 |
fix(desktop): channel topic and membership metadata cleanup (#3642)
First slice of #2216, scoped to the system/status lines in the chat timeline. ## Why Two problems on the same surface. **Clearing a channel topic renders as empty quotes.** The relay reports a clear as a `topic_changed` event carrying an empty string — there's no separate "cleared" event type. So the timeline printed: > Alice > changed the topic to “” which reads as if the topic were *set to* two quote marks. Same for purpose. **The membership caption reads like a headline, not a metadata line.** `title` and `action` render on separate lines — the member's name sits in the header row with the avatar and timestamp, and the caption sits beneath it. So the caption was "was added by Alice Chen" standing alone under a name, while its siblings on that same line are "joined the channel" and "left the channel". ## What - Blank, missing, or whitespace-only topic/purpose now reads **"cleared the channel topic"** / **"cleared the channel purpose"**. - Membership captions drop "was": **"added by Alice Chen"**, matching "joined the channel" and "left the channel". - The wording moves to `lib/systemEventCopy.ts` as a pure function, so it's assertable in a unit test instead of only reachable through the DOM. That also removes two JSX fragments from `SystemMessageRow.tsx`, taking it 911 → 900 lines. ## Two E2E assertions this exposed Both were measuring something other than what they claimed, and the copy change tipped them over. Neither is a product bug, but both would have failed the next person too. 1. **`mentions.spec.ts:1245`** asserted a button was un-underlined while the mouse was still parked from a previous `hover()`. Any reflow — new rows, scroll-to-bottom, a different text wrap — can slide that button under the stationary pointer, so the assertion measured *where the mouse happened to be* rather than the resting style. Dropping four characters changed the text wrap, changed the row height, changed the scroll offset, and the pointer landed on it. Now parks the pointer off-target first. 2. **`mentions.spec.ts:1253`** used a bare `role=tooltip` lookup. Once the first tooltip animates out while the second opens, two elements match and strict mode trips. Now scopes to the open tooltip via `:not([data-state="closed"])`. ## Deliberately out of scope - **Timestamps.** The day divider, per-message clock times, the Inbox thread pane, and the inbox list have three divergent date implementations and none fully match the writing standard's Today/Yesterday/weekday/date progression. That's its own slice of #2216. - **Whose avatar shows.** An addition puts the *added* member in the header; a removal puts the *remover* there. Possibly intentional, but it's a design question, not copy. - **`the channel` vs `this channel`.** joined/left/removed say "the channel"; created/archived/unarchived say "this channel". Worth normalizing, but it touches lines this PR otherwise leaves alone. ## Validation - `pnpm check`, `pnpm typecheck` — clean - Unit: **3781/3781**, including 6 new tests in `systemEventCopy.test.mjs` covering set/blank/undefined/null/whitespace for both fields, plus a guard that no variant can emit empty quotes - Smoke E2E `mentions` + `messaging`: **85/85** - The previously fragile test run with `--repeat-each=5`: **5/5** Signed-off-by: Clay Delk <clay.delk@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
ede2686334 |
fix(desktop): align data deletion labels (#2230)
## Why The Profile settings action still says “Sign Out,” while its confirmation action says “Delete My Data.” Both buttons trigger the same destructive local-data wipe and should name it consistently. ## What - Label both destructive actions “Delete my data” - Assert the matching section and confirmation labels in the existing Playwright coverage ## Risk Assessment Low — copy and test assertions only; sign-out behavior is unchanged. ## References - Follow-up to #2208 - #2216 also touches this copy and should preserve “Delete my data” when rebased - `just desktop-check` - `just desktop-test` (3,275 tests) - Desktop E2E build and sign-out Playwright spec (2 tests) Generated with Codex Signed-off-by: Bradley Axen <baxen@squareup.com> |
||
|
|
23f0c26b1c |
fix(relay): align NIP-11 max_limit with REQ ceiling (#3635)
Buzz's NIP-11 document advertised `limitation.max_limit: 10_000`, but the effective websocket REQ page ceiling was `1_000` — a 10x lie. The websocket REQ path never sets `EventQuery::max_limit`, so `query_events` applied its own `unwrap_or(1000)` clamp to every historical query. Only the COUNT fallback (`apply_count_fallback_limit`) ever raises that clamp. A client that trusts the advertised value asks for 10,000 events, silently receives 1,000, and — with no error and no continuation signal — reads that short page as exhaustion. Up to 9,000 events are dropped without anyone noticing. `MAX_HISTORICAL_LIMIT = 2_000` in `handlers/req.rs` was dead weight for the same reason: nothing clamped to 2,000 could survive the DB's 1,000 clamp one layer down. ## Change `buzz_db::DEFAULT_MAX_PAGE_LIMIT` (`1_000`) is now the single source of truth. It is the `query_events` clamp default, the value both REQ clamp sites use, and the value advertised as NIP-11 `max_limit`. `MAX_HISTORICAL_LIMIT` is removed rather than re-pointed — an alias for a constant used four lines away adds a name without adding meaning. The NIP-50 search path carries a second, independent bound. It clamps its emission target to the shared ceiling like any other REQ, but how many FTS candidates it will scan was bounded separately, by a bare 10-page loop over 100-hit pages. That product only coincidentally equalled the ceiling, so raising the ceiling — or shrinking a page — would shrink the scan relative to what clients may now request, degrading search quality while nothing in the code registered the change. The page count is now ceiling-divided from `DEFAULT_MAX_PAGE_LIMIT` over a named `SEARCH_PAGE_SIZE`, so the scan budget tracks the advertised ceiling by construction. That budget is a resource policy, not a delivery promise. It bounds candidates *scanned*, not events *emitted*: post-filtering (NIP-01 match, channel access, reader visibility, dedup) discards an unpredictable share of every page, so a search result smaller than the requested limit remains possible. This is not a NIP-11 violation — `max_limit` is defined as a clamp the relay applies to a requested `limit`, not a guaranteed count in the response. Two guards hold the pair together: - `req_filter_limit_clamps_to_advertised_nip11_max_limit` reads `max_limit` back out of a built `RelayInfo` and asserts the REQ path clamps to exactly that number. - `search_scan_capacity_covers_advertised_nip11_max_limit` asserts the scan budget covers exactly one advertised ceiling's worth of candidates — no less, and with no spare page of slack, so the derivation can't be quietly replaced by a hand-tuned constant that happens to pass today. ## Behavior Websocket behavior is unchanged: 1,000 was already the real ceiling on every path, including NIP-50. The advertisement now tells the truth about it. Raising the effective limit is a capacity decision and is deliberately not made here. The generic HTTP bridge's page-2+ offsets do change, as a consequence of the corrected clamp. `extract_page_offset` sizes a page from `query.limit` *before* the DB clamp applies, so an absent limit previously produced an offset of 2,000 and a requested 1,500 produced 1,500 — while the page actually returned held at most 1,000 rows. Both now produce 1,000. This corrects paging that had been skipping rows the previous page never returned; `extract_page_offset_sizes_pages_from_clamped_limit` locks it down. ## Scope note The bridge's per-endpoint ceilings — `BRIDGE_WINDOW_MAX_LIMIT` (200) for channel windows and `BRIDGE_THREAD_MAX_LIMIT` (500) for thread reads — are endpoint contracts on a non-NIP-01 transport, not values NIP-11 speaks for, and are unchanged. Fixes #3757 --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
36571f4adc |
fix(desktop): allow linux-only media items as dead code off-linux (#3811)
Local `desktop-tauri-clippy` fails on macOS with dead-code errors for `PROD_ORIGIN`, `DEV_ORIGIN`, and `is_trusted_media_origin`, which are only used inside `#[cfg(target_os = "linux")] enable_media_capture`. The items are intentionally platform-independent so unit tests run everywhere. Added `cfg_attr` allow attribute to suppress the warnings on non-Linux targets. Since [#3607](https://github.com/block/buzz/pull/3607), this affects all Rust developers on macOS. Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: d32955ad69077062930cc46cfe2df30ca9aaf6f8e76422681265e9e9af704d78 <d32955ad69077062930cc46cfe2df30ca9aaf6f8e76422681265e9e9af704d78@buzz.block.builderlab.xyz> |
||
|
|
74cd571219 |
fix(desktop): report authenticated relay recovery (#3812)
## Summary - report the relay as connected immediately after socket open and successful AUTH - keep rate-limited subscription replay, the connect promise, and reconnect listeners unchanged - cover authenticated reconnect while replay is held behind the shared rate-limit gate ## Why After WARP recovery, the socket could reopen and authenticate successfully while subscription replay waited behind the existing rate-limit gate. `connect()` kept `ConnectionState` at `reconnecting` during that intentional delay, so the desktop displayed “Can’t reach the relay” despite authenticated traffic already flowing. This is separate from #3774: that fix keeps routine operations from bypassing scheduled reconnect backoff. This patch preserves those protections and only corrects the authenticated transport-state boundary. ## Failure semantics If replay fails after the early `connected` transition, the existing `replayLiveSubscriptions()` catch calls `resetConnection()`, closes the socket, returns state to `reconnecting`, and schedules recovery. Operation waiters and reconnect notifications still do not complete until replay succeeds. ## Validation At commit `c8a4308e1079f4f9e6a72f0f0bfba280fe822ec0` with a clean working tree: - `pnpm --dir desktop typecheck` - `pnpm --dir desktop test` — 3,847 passed - `pnpm --dir desktop check` — passed; two pre-existing informational template-literal notices - `pnpm --dir desktop exec playwright test tests/e2e/relay-reconnect.spec.ts` — 8 passed - regression test proven red before the production ordering change (`reconnecting` after 3 seconds) and green after it Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> |
||
|
|
29dfe4821e |
fix(desktop): don't gate hover affordances on the hover media query (#3657)
## What problem this solves
Tailwind v4 compiles every `hover:` variant inside `@media (hover:
hover)`. Some
Windows hosts answer that query `false` **even with a mouse attached**,
and then
every hover-revealed control in the app is permanently `visibility:
hidden`.
Measured in the app's own WebView2 devtools console, on a mouse-driven
Windows 11
desktop:
```js
matchMedia('(hover: hover)').matches // false
matchMedia('(any-hover: hover)').matches // false
matchMedia('(pointer: fine)').matches // false
matchMedia('(any-pointer: fine)').matches // false
navigator.maxTouchPoints // 10
```
Windows itself, on the same machine at the same moment, reports a mouse
present
and an integrated digitizer:
```
GetSystemMetrics(SM_DIGITIZER) = 197 // INTEGRATED_TOUCH | INTEGRATED_PEN
// | MULTI_INPUT | READY
GetSystemMetrics(SM_MAXIMUMTOUCHES) = 10
SystemInformation.MousePresent = True
```
So this is not "the user has no mouse". Windows knows a mouse is
attached, and
Chromium still reports `any-pointer: fine: false` and `any-hover: false`
— the
`any-*` queries exist precisely to describe *any* available input
device, and
they are wrong here. The presence of an integrated touch digitizer
collapses the
reported capability to touch-only.
The compiled rule that never applies:
```css
.group-hover\/member\:visible {
&:is(:where(.group\/member):hover *) {
@media (hover: hover) { visibility: visible; }
}
}
```
The row genuinely matches `:hover` (verified: `row.matches(':hover') ===
true`),
the button is in the DOM, the utility class is generated — and the
declaration
still never lands.
## Why this is more than one control
Not a single menu. Confirmed newly-ungated in the production bundle
after the
change:
| utility | media-gated before | after |
|---|---|---|
| `group-hover/member:visible` | yes | no |
| `group-hover/inbox-item:opacity-100` | yes | no |
| `group-hover/channel-row:opacity-100` | yes | no |
| `group-hover/attachment:opacity-100` | yes | no |
| `hover:bg-muted` | yes | no |
On an affected host the channel-member action menu (remove member,
change role,
start/stop agent) has **no reachable affordance at all**: `visibility:
hidden`
also removes the button from tab order, so there is no keyboard path
either.
## The fix
One line, at the root, next to the existing variant override:
```css
@custom-variant hover (&:hover);
```
This trusts the actual hover event rather than the capability query.
Chromium
only fires `:hover` when a real pointer is present, so behaviour on
hosts that
report the capability correctly is unchanged.
Verified against a production `vite build`, not just the dev server —
the
override cascades to the *named* group variants (`group-hover/member`,
etc.),
which is the part that matters here.
## Prior art in this repo
#2849 overrides Tailwind v4's `dark:` variant default at the *exact same
insertion point* in this file, for the same class of reason (a v4
default that
does not match how this app actually works). This change follows that
precedent.
**Note for whoever merges second: #2849 and this PR will conflict
textually** —
both append a `@custom-variant` immediately after `@config`. The
resolution is
to keep both lines; they are independent.
## Scope
Desktop only. `web/src/shared/styles/globals.css` has the same Tailwind
v4
default, but `web/src` contains **zero** `group-hover` usages, so there
are no
hover-revealed affordances to strand there. Adding the override to web
would be
speculative.
One `hover` capability query is deliberately left in place —
`.buzz-wave-hover-trigger` in `animations.css` gates a decorative
wave-hand
animation on `(hover: hover) and (pointer: fine)`. That is a cosmetic
flourish
rather than an affordance, so it stays inert on affected hosts instead
of
widening this diff.
## Reproducing
The trigger is **an integrated touch digitizer anywhere on the
machine**, not the
display you are actually working on. This was found on a touch-capable
laptop
docked to an ordinary non-touch external monitor, driven entirely by a
mouse — so
"I'm on a desktop monitor" does not rule you out. Check with:
```js
matchMedia('(hover: hover)').matches // false ⇒ affected
```
Not reproducible on macOS, or on a Windows machine with no digitizer at
all —
`hover: hover` is true there and every affordance works normally. If you
are on
such a host, emulate it in devtools by forcing `hover: none` / `pointer:
coarse`,
then open a channel's member list and hover a row: no action menu
appears.
## Tradeoff worth naming
On a genuine touch-only device, a bare `&:hover` can latch after a tap
and stay
applied until the next interaction, where the media-query default would
have
suppressed it. That is the real cost of this change.
The judgement here is that a stuck hover style is a cosmetic annoyance,
while an
unreachable "remove member" button is a functional dead end — and that
the
affected hosts are overwhelmingly mouse-driven machines that merely
*happen* to
ship a digitizer, as the `MousePresent = True` reading above shows. If
you would
rather scope this to `@media not (hover: hover)` as an additive fallback
instead
of overriding the variant, I am happy to rework it.
Signed-off-by: sumit-m <33051892+sumit-m@users.noreply.github.com>
|
||
|
|
114d40d9d3 |
feat(relay): gate kind 30178 team-catalog reads behind the shared tag (#3358)
Team catalog projections (`kind:30178`) embed every member's system
prompt, so they need the same read gate personas already have: only the
author sees an unshared event. The gate was hardcoded to `kind:30175` at
six read surfaces plus the SQL pushdown, so rather than adding a second
special case it becomes kind-generic over `SHARED_GATED_KINDS = {30175,
30178}`.
## Kind 30178
New parameterized-replaceable kind, addressed by `(pubkey_o, 30178,
team_id)`. It embeds sanitized member projections instead of referencing
`kind:30175` heads — a foreign reader of a shared team could not
otherwise hydrate members whose own persona events are unshared or, for
built-ins, absent entirely. `kind:30176`'s wire body is untouched, so
device sync keeps its contract.
## Kind-generic shared gate
`buzz_core::kind` replaces `is_persona_shared_kind` /
`is_unshared_persona_event` / `persona_event_is_shared` with
`SHARED_GATED_KINDS` and the kind-agnostic `is_shared_gated_kind` /
`is_unshared_gated_event` / `event_is_shared`. Every read surface
consults the set:
| Surface | File |
|---|---|
| REQ historical delivery + `ids` lookup |
`crates/buzz-relay/src/handlers/req.rs` |
| Live fan-out | `crates/buzz-relay/src/handlers/event.rs` |
| COUNT fallback | `crates/buzz-relay/src/handlers/count.rs` |
| NIP-98 HTTP `/query`, `/count`, `/search` |
`crates/buzz-relay/src/api/bridge.rs` |
| Pre-`LIMIT` SQL pushdown | `crates/buzz-db/src/event.rs` |
The SQL clause generalizes from `kind != 30175` to `kind NOT IN (...)`
bound from `SHARED_GATED_KINDS`, still applied before `ORDER BY … LIMIT`
so a page of newer private events cannot starve an older shared one off
the candidate set. `EventQuery::persona_reader` is renamed
`shared_gated_reader` and `needs_persona_filtering` to
`needs_shared_gate_filtering` to match.
Because the `buzz-core` rename has consumers outside the relay, the four
desktop call sites of `persona_event_is_shared` travel with it:
`desktop/src-tauri/src/commands/personas/pending.rs`,
`desktop/src-tauri/src/event_sync.rs`, and two in
`desktop/src-tauri/src/managed_agents/persona_events.rs`. Each call is
unchanged apart from the name — the persona `shared` projection behaves
exactly as before.
## Ingest validation
`validate_persona_envelope` splits into two reusable pieces —
`validate_shared_tag` (exactly-two-element `["shared","true"]`, at most
one occurrence) and `single_bounded_d_tag` (exactly one `d` tag,
non-empty, `<=64` chars, no ASCII control characters or whitespace).
`validate_team_catalog_envelope` composes both; personas additionally
keep the slug grammar `^[a-z0-9][a-z0-9_-]{0,63}$`.
`kind:30178` deliberately does **not** get the slug grammar. Team ids
are UUIDs or built-in identifiers such as `builtin-team:welcome`, and
the colon is not slug-legal; rewriting ids to fit would break NIP-33
addressing against the team's own `kind:30176` head. The non-empty and
exactly-one checks are load-bearing regardless — without them generic
NIP-33 storage maps a missing `d` onto `(pubkey_o, 30178, "")` and every
team overwrites its predecessor.
The exact two-element `shared` shape is enforced because the SQL
visibility clause is JSONB containment (`tags @>
'[["shared","true"]]'`), which would match a three-element superset such
as `["shared","true","extra"]`.
`kind:30178` is also added to the `Scope::UsersWrite` allowlist and to
`is_global_only_kind`, so a stray `h` tag cannot channel-scope an
owner-authored definition.
## Deferred
`kind:30176` is deliberately not a gate member. Its writers never emit
`shared`, so catalog opt-in semantics do not describe it — it needs
owner-private reads driven by an authenticated principal set, tracked as
a separate follow-up.
## Tests
- 19 new `ingest.rs` unit tests covering the 30178 envelope (UUID and
colon `d` tags, 64-char boundary, non-ASCII bound,
empty/valueless/duplicate/missing `d`, embedded newline, `shared`
false/three-element/duplicate, scope and global-only membership).
- Persona regressions for the valueless `["d"]` shapes, since the
`d`-tag helper is shared by both validators.
- Existing `kind.rs` gate tests generalized and extended to assert the
gate applies to 30178 as it does to 30175.
- New `crates/buzz-test-client/tests/e2e_team_catalog.rs`: 9 WS-level
tests over a live relay covering author reads of unshared heads, foreign
omission from REQ, `ids`-lookup denial, COUNT existence-leak, share and
unshare transitions, and the mixed-kind filter case.
- `.github/workflows/ci.yml` adds `--test e2e_team_catalog` to the Relay
E2E job so the new suite runs.
## Docs
`docs/nips/NIP-AP.md` gains a "Team catalog projection: kind:30178"
section and an "Ingest validation: kind:30178" subsection, records the
gate as kind-generic, documents 30178 deletion vs. unshare semantics,
and adds a security note that sharing a team exposes every member's
instructions even when that member's own `kind:30175` head is unshared.
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
|
||
|
|
b9e4ed616f |
test(desktop): click visible thread collapse guide (#3800)
## Summary - target the visible thread branch collapse guide in the messaging smoke test - avoid clicking the underlying collapse rail when the guide overlaps it - retain the existing post-click assertions that verify the two-reply branch collapses ## Context `main` CI failed because Playwright repeatedly attempted to click the lower `thread-collapse-rail` while the matching `thread-collapse-guide` intercepted pointer events. Both controls dispatch collapse for the same branch; the guide is the actual topmost user target and is already used by `thread-unread.spec.ts`. Failing run: https://github.com/block/buzz/actions/runs/30575425126 ## Validation - focused Playwright smoke test: 1 passed - pre-push hooks: desktop check passed; 3,835 desktop tests passed - `git diff --check` ## Review Princess Donut reviewed the test-only approach and locator determinism with no blockers. Mongo review is pending. Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> |
||
|
|
d40a33290e |
feat(desktop): raise the install ceiling and make installs observable (#3368)
Windows installs of Goose and other harnesses failed at exactly five minutes with an empty error (#2401). The 300s ceiling was killing installs that were working, just slowly — the Goose step pulls a ~79MB release asset, and Windows Defender scans every file npm extracts. When the ceiling fired it discarded the output it had already read, so the user got a bare timeout string and no way to tell a hang from a large download. ## The ceiling `INSTALL_TIMEOUT` is 900s, and the error names the limit: `install command exceeded the 15m ceiling and was terminated`. It stays a pure wall-clock ceiling with no inactivity kill — nothing observable distinguishes a hung installer from one silently transferring a large artifact, so silence alone never kills an install. A ceiling kill remains non-retryable; re-running a command that already burned 15 minutes costs the user more time with no plausible path to success. The child's exit and both stream drains fold into one resumable settle governed by a single deadline. Waiting on the drains outside that deadline would let a descendant that outlived the install shell hold the output pipes — and the per-runtime install guard behind them — open with no bound, which is the failure the ceiling exists to prevent. So the deadline path terminates the process group on the normal-exit branch too: a leader that exited with a real status still gets its stragglers killed, and the guard cannot stick either way. Whether the leader had already exited only decides the verdict — its real status outranks a timeout. The install shell is a session leader and its descendants inherit the output pipes, so signalling only the leader left them running and the drains blocked on a pipe nobody would close. Escalation keys off the *group's* liveness rather than the leader's, since a descendant that ignores SIGTERM outlives the leader and would otherwise never receive the group SIGKILL. Reaping the killed child and finishing the drains share one bounded grace, so a termination that failed outright cannot extend the ceiling that just fired. ## Output capture Each stream drains into a bounded capture that is *shared* with the reader rather than returned by it, so whatever arrived before a stall is readable at the ceiling — exactly when the output matters most. Output of any size costs a fixed amount of memory. One capture holds two independently bounded views of the same bytes: | View | Head / tail | Cut marker | |------|-------------|------------| | UI (`InstallStepResult`) | 512 B / 1024 B | `... (N bytes omitted) ...` | | Log file | 128 KiB / 128 KiB | `... [N bytes omitted at cap] ...` | The UI budget is screen space; the log's is disk. Both markers are inline, so neither ever implies completeness it does not have. Both ends are cut at arbitrary byte offsets, so a partial character is trimmed and the partial token each cut left behind is dropped — the marker's byte count includes both trims. ## Install log `steps` carries only the last attempt of each step, truncated for display. Everything else — earlier retries, the prerequisite step that actually broke, the managed-Node bootstrap — used to be discarded. `InstallReporter` now appends one self-contained record per attempt of per step to `install-<runtime-id>.log` beside the agent logs, and `InstallRuntimeResult.log_path` carries the file to the UI, where a failure message ends with `Full log: <path>`. Each record is bounded independently by the log-scale capture that produced it, so a first attempt that printed megabytes cannot push out the later record explaining the failure; the run's total is bounded by steps × attempts × per-record cap. Every early return builds its result through one `InstallReporter::failed` helper, so no failure path can omit the log pointer, and synthesized steps go through `record_step` — a step that reaches the UI without passing it would be invisible in the file. Install output can echo a registry token or proxy credential from the environment it ran in, and the file is written unattended. Redaction keys off the *names* of the environment variables the install inherited, snapshotted once per run, rather than a list of known secret value prefixes: a credential with no recognisable shape is exactly the one a prefix match misses. Three name rules apply, because the variables need different treatment: | Rule | Variables | Redacted | |------|-----------|----------| | URL userinfo | `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, `NPM_CONFIG_PROXY`, `NPM_CONFIG_HTTPS_PROXY`, `NPM_CONFIG_REGISTRY` | `user:password` only | | Exact name | `NPM_CONFIG_KEY`, `NPM_CONFIG__AUTH`, `NPM_CONFIG_OTP` | whole value | | Marker substring | `*TOKEN*`, `*SECRET*`, `*PASSWORD*`, `*_PAT`, … | whole value, 8-byte floor | A proxy or registry keeps its host and port, because an install that fails behind one is diagnosable only if the record still says which one it went through, and a bare `user@` with no password is not treated as a credential. npm's own settings are listed by exact name rather than matched on `KEY` or `AUTH` substrings — both occur throughout an ordinary environment on values that are paths and people's names — and they bypass the 8-byte floor, since a six-digit one-time password is a credential at that length. Matching is case-insensitive, which is what npm's lowercase `npm_config_*` spelling needs. `0o600` is set by the create rather than a later `chmod`, which would leave a window where the umask decides. A runtime id that cannot safely be a filename yields no log rather than a sanitized one — a rewritten id could collide with another runtime's log. The file holds exactly one run. A run opens its own session after the runtime id has been canonically resolved — the previous file rotates to `.1` and any older `.1` is removed before the rename, since a rename that will not replace its destination would otherwise wedge rotation permanently on Windows. The session writes a header naming the runtime, the app version (`app.package_info().version` on the Rust side — cannot be mocked or fail), the OS (`std::env::consts::OS`), and the start time: a Windows failure and a macOS one on the same runtime are different bugs, and a stale app version explains a failure that no longer reproduces. Each record carries its attempt's elapsed time. ## Live output line A 15-minute ceiling with nothing behind it but a spinner is indistinguishable from a hang. The same drain seam feeds an `acp-install-output` event carrying the newest complete line, and the three install entry points — Doctor harness rows, the harness catalog dialog, and onboarding runtime cards — render it under the spinner with `aria-live="polite"`. Ordering is keyed on a `seq` monotonic across the whole install, not on the attempt number, which restarts at 1 for every step: keyed on attempt, one step succeeding on attempt 2 would make the next step's attempt-1 output look stale and freeze the display for the rest of the install. Each executed attempt begins with an unthrottled `line: null` clear signal, so a stale failure line cannot sit under the spinner while the retry runs. Events are otherwise throttled to four per second, and the throttle *retains* the newest pending line and flushes it when the window reopens rather than dropping it — at an attempt boundary a drop would silently eat the new attempt's first line. The subscription is mounted for the runtime's whole lifetime rather than started when the install begins. The install command is invoked from the click handler, so the clear and a fast command's first lines can be emitted before React has committed the pending state, and nothing replays them — a subscription that waited for that state would lose the entire output of a short install. The run boundary resets the ordering key when the install settles, since `seq` restarts for the next run, and the line renders only while installing, so a straggler from a finishing drain cannot appear under a fresh Install button. The 15-minute ceiling deliberately stops waiting on stuck drain threads — a hung installer must not freeze the app. That means a drain thread can outlive its `InstallReporter`. Without a generation guard, a drain that calls `offer` after the run settles would publish an event with the run's high `seq`, poison the permanent listener's React state, and cause the next install's restarted `seq=0` events to be rejected. `Live` now carries a `lifecycle: Arc<RwLock<bool>>`; drain threads hold a **shared read guard** from the admission check through the `(self.emit)(...)` call, making the check-then-emit pair atomic with respect to shutdown. `InstallReporter::drop` takes the **exclusive write guard** and stores `false` — this blocks until every in-flight drain publication releases its read guard, then prevents any new admission. Deactivation is bounded: the write lock holds only for the flag store, so it can block at most for the duration of one emit call (microseconds to low milliseconds). Rust drops locals in reverse-declaration order, so `reporter` drops before `_guard`, ensuring the exclusive write completes before the per-runtime concurrency guard releases and a new install can start. ## Also Install result types move to `desktop/src/shared/api/installTypes.ts`, following the existing `searchTypes.ts` / `workflowTypes.ts` convention, and are re-exported from `tauri.ts` and `types.ts` — both already over the file-size cap, so neither can grow to carry them. Two comments described `AdapterOutdated` as applying only to the deprecated package; it also covers a version below the supported floor. Report: #2401 --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
dba97eecd9 |
fix(db): isolate usage metrics advisory-lock test on scratch DB (#3670)
## What Problem This Solves `test_usage_metrics_lock_has_single_owner_and_releases_on_drop` hardcodes the **production** advisory lock key (`0x4255_5A5A_4D45_5452`) on the shared `TEST_DATABASE_URL`. Postgres advisory locks are per-database, so any live `buzz-relay` pointed at the same DB holds that key and the test fails (or races the relay tick). Diagnosis time was burned during #3268 verification, including near-misses on live dev relays. Fixes #3619. ## Why This Change Was Made Preferred fix from the issue: run the test on a private scratch DB via existing `create_scratch_db` / `drop_scratch_db` (same pattern as replica-routing fixtures). Keep the production lock key so the test still documents the real constant, without colliding with a running relay. ## User Impact - Local `cargo test -p buzz-db -- --ignored` no longer fails when a dev relay is running against the shared test DB - Safer: no temptation to `pg_terminate_backend` a live relay to "fix" the test ## Evidence - Code review of fixture isolation - Pattern matches existing `create_scratch_db` usage in this file - Test remains `#[ignore = "requires Postgres"]` (same as before) ## Related - Issue: #3619 - None found among open PRs for this exact fix Signed-off-by: NanoRisk6 <aidashtherapy@gmail.com> |
||
|
|
1b3ff96a57 |
Add Devin as a preset ACP harness (#3225)
## Summary - Add Devin to the built-in preset harness catalog using the official native ACP invocation: `devin acp`. - Link setup guidance to Cognition's official Devin CLI documentation. - Render a bundled, attributed Devin mark on a white canvas through Buzz's existing runtime-icon system. - Keep preset capability metadata in the Rust catalog; no duplicate TypeScript runtime table or React runtime checks. - Move the existing preset catalog and its focused tests into a Rust submodule without changing existing preset behavior, keeping the touched files within the repository's file-size limit. ### Related issue Follow-up to the generic BYOH harness work in #2773. ### Scope This is the small preset/data-entry follow-up described in the #2773 discussion. It uses the generic preset readiness contract and does not add Devin-specific authentication probing, permission bypasses, model switching, cloud handoff, or cloud Devin capability claims. The preset supplies: - ID: `devin` - Executable: `devin` - Arguments: `acp` - Installation guidance: https://docs.devin.ai/cli ### Testing Local verification was rerun at the final PR head, `7bb9aa6e862a47a5062b5b8234fdb5ce2aae6c1d`. - Focused Rust preset tests: 7 passed - Desktop JavaScript tests: 3,768 passed - Desktop lint, formatting, file-size, and text guards: passed - Full Tauri test suite: 1,851 passed, 14 ignored - Root Rust unit-test groups: passed - Web production build: passed - Mobile format, analyze, and test suites: passed - Full repository `just ci`: passed The branch also merges cleanly with the current Block `main`. The upstream fork-triggered CI workflow is awaiting maintainer approval; DCO, Semgrep OSS, and zizmor are passing. The bundled SVG was rendered and visually inspected in both its source dimensions and a 512px preview. The cross-language preset-logo guard verifies that the Devin mapping exists and the asset is present on disk. Signed-off-by: Mark Fenner <markfenner57@yahoo.com> |
||
|
|
4d47aa8345 |
feat(desktop): improve agent activity header ui (#3321)
**Category:** improvement **User Impact:** Activity feeds now clearly identify the agent and keep update recency visible even when channel names are long. **Problem:** The activity header led with a generic label, making it hard to tell which agent was in view, while channel scope and recency competed for limited horizontal space. Long channel names could hide the update timestamp entirely. **Solution:** Lead with the resolved agent avatar and name, then place mode and scope in a truncating metadata region with recency pinned at the right edge. This preserves the compact two-line header while keeping the most important identity and freshness signals legible. <details> <summary>File changes</summary> **desktop/src/features/channels/ui/AgentSessionThreadPanel.tsx** Reorganizes the activity header around the agent identity, reuses the existing resolved profile avatar and label helpers, and separates scope truncation from the always-visible recency label. **desktop/tests/e2e/activity-scope-label-screenshots.spec.ts** Expands activity-header coverage across channel-scoped, all-channel, raw, long-name, and narrow layouts, including measured truncation and recency visibility. </details> ## Reproduction steps 1. Open an agent's activity feed from a channel. 2. Confirm the agent avatar and name lead the header. 3. Open a feed scoped to a channel with a long name and resize the panel narrowly. 4. Confirm the mode and channel scope truncate while the recency label remains visible at the right edge. 5. Toggle Raw mode and open an all-channel feed to confirm the same hierarchy and truncation behavior. ## Screenshots | Long channel | Narrow layout | |---|---| | <img width="380" height="671" alt="image" src="https://github.com/user-attachments/assets/19682aac-9938-41ed-8c27-fe59bf8b7535" /> | <img width="371" height="771" alt="image" src="https://github.com/user-attachments/assets/92a6fc05-c9ba-4c11-a18c-22b5225d8b9a" /> | | Raw mode | All channels | |---|---| | <img width="380" height="671" alt="image" src="https://github.com/user-attachments/assets/c8135600-e3c9-4644-8350-fa5f6b2d3aaa" /> | <img width="380" height="671" alt="image" src="https://github.com/user-attachments/assets/bac73a6a-4ed5-42d6-98cc-039a75c48ef3" /> | --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz> |
||
|
|
bf139e8d0b |
perf(presence): reduce heartbeat frequency (#3783)
## Summary - send desktop presence heartbeats every 60 seconds instead of every 30 seconds - extend presence TTL from 90 to 180 seconds to preserve the existing three-heartbeat expiry window - add mutation-sensitive tests that pin the one-minute / three-window timing contract - update presence documentation to match This halves steady-state **desktop** presence `SET` + `PUBLISH` traffic while retaining tolerance for two missed heartbeats. Mobile already uses a 60-second heartbeat, so the fleet-wide reduction depends on desktop's share of connected clients. ## Rollout order Deploy the relay TTL increase before shipping the desktop heartbeat change. Old desktop + new relay is safe; new desktop + old relay leaves only a 90-second TTL on a 60-second cadence and can flap after one missed heartbeat. ## Verification At initial live-test commit `00816e233b187bc5ba12c667d675ed050a8cc1c9`: - isolated clean-room relay built from the exact SHA against fresh Postgres, Redis, and MinIO - live Redis `MONITOR` observed kind-20001 writes as `SET ... EX 180`, global `PUBLISH`, and clean-disconnect / explicit-offline `DEL` - normal workflows passed: channel create/update/archive/unarchive; message send/get/reply/thread/search; archived-channel write rejection and resumed write after unarchive At follow-up commit `bf38a8c5c96f196ff8ee46e48d4141ee7811f186`: - `pnpm -C desktop test` — 3829 passed - `pnpm -C desktop typecheck` - `cargo test -p buzz-pubsub` — 24 passed, 11 Redis-dependent tests ignored - mutation probes fail when the server TTL changes to `999999` or the desktop heartbeat changes back to 30 seconds - `git diff --check` The pre-push suite's relevant checks passed, but its unrelated Tauri clippy step fails on current `origin/main`: `desktop/src-tauri/src/linux_media.rs` has three dead-code warnings on macOS. This PR does not modify that file, so the branch was pushed after independently running the suites above. ## Buzz context Originating channel: `buzz-redis-cluster-mode` (`f4e36d32-afdb-447f-8c87-ab003e069d18`) --------- Signed-off-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz> Co-authored-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz> |
||
|
|
6e419b9f1c |
Tighten continuation message rows (#3724)
## Summary - use uniform 4px top and bottom padding for continuation rows - keep continuation timestamps top-aligned and remove the thread-only minimum-height gutter - raise continuation hover actions by 12px - align virtualized row estimates with the compact layout ## Validation - `pnpm test` (3,782 tests via pre-push) - `pnpm check` - desktop snapshots ## Screenshots ### Mention-chip continuation  ### Emoji continuation  --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> |
||
|
|
f48f3f055f |
Fix video reviews in thread replies (#3719)
## Summary - Show video review comments when a video is opened from a thread reply. - Reuse review-context construction across timeline and thread views. ## Validation - `pnpm run build:e2e && pnpm exec playwright test tests/e2e/video-attachment.spec.ts --project smoke --grep "video replies in threads open the review comments view"` - `pnpm test` --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> |
||
|
|
1dfd89ea67 |
feat(release): make desktop releases immutable (#3568)
## Summary - add a manual desktop release preparer that regenerates one version-only candidate from current `origin/main` - validate deterministic complete changelog accounting, candidate authorship, allowed files, exact-head approval, required checks, and two-parent merge topology before tagging the reviewed candidate - move desktop tags/releases from `v*` to `desktop-v*` while preserving relay, chart, push-chart, and mobile behavior - stage all four platform outputs in Actions artifacts and grant GitHub release write access only to one final all-platform-gated publisher - publish the versioned release only after complete artifact assembly; update stable `latest.json` last; never promote prereleases or published rebuild outputs ## Safety properties - desktop tags point to the reviewed candidate SHA, not the merge commit - release builds remain tag-bound and reverify tag == checked-out HEAD - one final writer fails closed on artifact basename collisions - per-tag concurrency serializes publication without cancellation - published reruns do not replace immutable versioned assets or promote signatures from a rebuild - candidate branches use an explicit remote OID lease when regenerated ## Validation - `scripts/test-desktop-release-candidate.sh` - `scripts/test-release-ref-contract.sh` - `scripts/test-mobile-release-contract.sh` - changed workflow YAML parsing (Ruby Psych) - changed shell syntax (`bash -n`) - `git diff --check` - push hooks: branch-skew, Rust workspace tests (1,853 passed), desktop Tauri tests (3 passed) ## Coordinated companion - squareup/buzz-releases#79 updates the manually entered desktop source-tag contract to stable-only `desktop-v*` - merge the private contract companion before the first namespaced desktop release ## Rollout blockers (no settings changed here) Before the first candidate/release: 1. enable merge commits in repository settings 2. allow `merge` in ruleset `13596885` 3. require approval after the last push in ruleset `13596885` 4. include `refs/tags/desktop-v*` explicitly in release ruleset `14378754` 5. prove the non-publishing candidate/merge/tag/artifact validation path before any production release Do not test the old workflow with a prerelease: it can still mutate the production rolling updater release. --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> |
||
|
|
cca8839034 |
Make relay reconnect backoff authoritative (#3774)
## Summary - make the relay reconnect coordinator authoritative during outages so query, publish, and subscription traffic waits for the scheduled attempt instead of cancelling backoff - release waiting operations after the coordinated AUTH + live-subscription replay attempt, while preserving one explicit manual reconnect fast path - suppress duplicate notification side effects when reconnect replay overlaps previously delivered events ## Root cause `resetConnection()` scheduled exponential backoff, but `ensureConnected()` cleared any pending reconnect timer. Operation-level retry paths immediately called `ensureConnected()`, so ordinary app traffic could repeatedly bypass the reconnect policy during an outage. The resulting churn also replayed overlapping live events into notification side effects without a shared event-ID guard. ## Validation - `pnpm --dir desktop typecheck` - `pnpm --dir desktop test` — 3,823 passed - pre-push: `desktop-check`, `desktop-test`, and `branch-skew` passed - file-size, px-text, and pubkey-truncation ratchets passed --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> |
||
|
|
bd0bff24bf |
feat(desktop): add password-protected backups in settings (#3701)
**Category:** new-feature **User Impact:** Users can create, download, and verify a password-protected backup of their private identity from desktop Settings. **Problem:** Buzz does not currently give signed-in users a Settings-based path to protect or validate their private identity independently of onboarding. **Solution:** Add a focused backup menu to the private-key row, keep encryption and verification local in Rust, and preserve completed encrypted backups briefly so native saves can be retried without repeating encryption. <details> <summary>File changes</summary> **desktop/src/features/settings/** Adds the background backup lifecycle, create and test dialogs, private-key menu integration, password handling, and focused unit coverage. **desktop/src/features/onboarding/ui/NsecMaskedDisplay.tsx** Extends the masked private-key display with reusable overflow-menu actions used by Settings. **desktop/src/app/App.tsx** Mounts the backup provider at app scope so encryption and save work survive closing Settings or the modal. **desktop/src/shared/api/tauriIdentity.ts** Adds typed desktop bindings for local backup creation, save, selection, and verification. **desktop/src-tauri/src/key_backup.rs and desktop/src-tauri/src/commands/identity.rs** Implements local NIP-49 encryption, password generation, file handling, and public-identity-only verification results. **desktop/src-tauri/src/egress_guard.rs and guarded call sites** Blocks encrypted secret material from relay, websocket, snapshot, sharing, and huddle egress paths. **desktop/src-tauri tests and fixtures** Covers encryption, verification, file behavior, and fail-closed no-egress protections. **desktop/src/testing/e2eBridge.ts, desktop/tests/, and desktop/playwright.config.ts** Expands the mock native bridge and browser coverage across create, retry, expiry, and current/different-identity verification states. **desktop/src-tauri/Cargo.toml, Cargo.lock, and assets** Adds the local cryptography/password-generation dependencies and embedded short-word list. </details> ## Reproduction steps 1. Run the desktop app and open **Settings → Profile → Identity**. 2. Open the private-key overflow menu and choose **Create backup**. 3. Enter or generate a valid password, submit, and confirm progress continues if the dialog or Settings is closed. 4. Save the resulting `.ncryptsec` file; cancel and retry to confirm the temporary download remains available. 5. Choose **Test backup**, select the file, enter a wrong password, then retry with the correct password. 6. Confirm success identifies whether the backup matches the current identity and displays only the public `npub`. ## Screenshots | Settings identity | Private-key menu | Create backup | |---|---|---| | <img width="1280" height="720" alt="image" src="https://github.com/user-attachments/assets/981e391b-6829-4081-95ca-ca75a369de71" /> | <img width="1280" height="720" alt="image" src="https://github.com/user-attachments/assets/7972c68e-7635-47d8-b0ad-9639390d3e6c" /> | <img width="1280" height="720" alt="image" src="https://github.com/user-attachments/assets/4709c8f7-cf02-46f1-bec9-b3f98fe56fb2" /> | | Encrypting | Download available | Test success | |---|---|---| | <img width="1280" height="720" alt="image" src="https://github.com/user-attachments/assets/1ac3e934-2b4b-4135-bae6-126c715c8c59" /> | <img width="1280" height="720" alt="image" src="https://github.com/user-attachments/assets/cb6f07ee-a16f-44a5-b9a0-6b9fe0e4d40d" /> | <img width="1280" height="720" alt="image" src="https://github.com/user-attachments/assets/ea58b1b1-966c-46aa-8d59-92c9f06a25bd" /> | Visual review and additional states: [Buzz thread](buzz://message?channel=50ca7ef1-201e-4159-9499-40de3964b7c3&id=87eceb5f0f82fd50c32e560de3d35be48e293760f6620718aafdcef289d475fe) --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz> |
||
|
|
f44b5a2477 |
fix(desktop): reuse profiles when joining communities (#2155)
## Why People joining a community with an existing relay profile should not be asked to recreate their name and avatar. ## What - Check the active identity's relay profile after the joined community becomes active - Skip directly to the starter-team step when a kind-0 profile event exists - Preserve the profile setup path when no event exists or discovery fails - Cover both new-profile and existing-profile join paths in E2E tests ## Risk Assessment Low — the lookup is scoped to the community onboarding profile stage, runs once per transaction, and fails open to the existing flow. ## References - `pnpm build:e2e && pnpm exec playwright test --project=integration tests/e2e/onboarding.spec.ts --grep 'first-community direct join reaches profile|community onboarding reuses an existing relay profile'` (2 passed) Generated with Codex Signed-off-by: npub1rf6fvdj6ut0c4kcmjv4p5mmgh89nj58n69uu3fz3cvk3jn500hqs7emz79 <1a7496365ae2df8adb1b932a1a6f68b9cb3950f3d179c8a451c32d194e8f7dc1@sprout-oss.stage.blox.sqprod.co> Co-authored-by: npub1rf6fvdj6ut0c4kcmjv4p5mmgh89nj58n69uu3fz3cvk3jn500hqs7emz79 <1a7496365ae2df8adb1b932a1a6f68b9cb3950f3d179c8a451c32d194e8f7dc1@sprout-oss.stage.blox.sqprod.co> |
||
|
|
06582ee6f0 |
Render mobile agent mention chips (#3702)
## Summary - Render selected agent mentions as visible bot chips in the mobile composer. - Recognize agent profiles consistently when rendering message-body mentions. <img width="630" height="1368" alt="Screenshot 2026-07-30 at 07 54 16" src="https://github.com/user-attachments/assets/035b46bf-ee78-4ee5-82fc-84591415ed7c" /> ## Validation - `flutter test test/features/channels/compose_bar_test.dart test/features/channels/message_content_test.dart` - `flutter analyze` --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> |
||
|
|
61b96c9828 |
fix(catalog): update Amp description (#3758)
## Summary - replace Amp's outdated Sourcegraph attribution in the runtime catalog - describe Amp neutrally as a coding agent for the terminal and editor ## Verification - `pnpm test` (desktop: 3,819 passed) - `pnpm typecheck` - pre-push `desktop-check`, `desktop-test`, and `branch-skew` hooks Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> |
||
|
|
53771c8f54 |
fix(acp): preserve truncated thread context (#3340)
## Why Long Buzz threads were rendered as `[Thread Context (13 of 13 messages)]` because the harness counted only the already-limited query result. That hid older context and could also hide the agent's own prior reply in busy threads. ## What - Fetch one extra thread reply as a sentinel so truncated context is labeled correctly. - Use a best-effort `/count` call for improved truncated totals when available, clamped to the sentinel-proven minimum so racy counts cannot render impossible labels. - Keep the `/count` path single-attempt with a short timeout and only add the root to exact totals when the root was actually fetched. - Fetch and preserve the agent's newest prior reply when it falls outside the recent window, with exact event-id matching for the pin/dedup boundary. - Add parser and fetch-boundary tests for truncation, exact count, missing root, count-below-minimum clamping, count failure fallback, distinct fetched-reply lower bounds, agent-reply dedup/pinning, and serialized query/count filter semantics. ## Risk Assessment Low-to-medium — limited to buzz-acp prompt context fetching and a small RestClient helper. If `/count` fails or times out, the code falls back to the sentinel-derived minimum total rather than failing the prompt. The synchronous `/count` happens only for truncated thread contexts and is bounded to one short best-effort attempt. ## References - Buzz thread: chotchkies-buzz-bombing-flakes / `7ef71407f1c7a642382c7e48e0c80fb6ca66948890e04d1eb6f1408c3b7278b1` - Validation at `c1cfd1b16a04a3ac1d1d0d3cf43e1a08508f3532`: - `cargo fmt -p buzz-acp` ✅ - `cargo test -p buzz-acp test_fetch_thread_context -- --nocapture` ✅ (6 tests) - `cargo test -p buzz-acp parse_nostr_thread_response` ✅ - `cargo test -p buzz-acp` ✅ (649 unit + 9 lifecycle tests) - `git diff --check` ✅ - Push was completed with `--no-verify` after pre-push hooks reached non-code local environment failures: `flutter` missing for `mobile-test`; Node.js v20.20.2 too old for pnpm/node:sqlite in `desktop-check` and `desktop-test`. Earlier hook stages passed: `check-push-org`, `branch-skew`, `rust-tests`, `test`, `desktop-tauri-checks`. - Earlier full `./bin/just ci` at `622ed7eb8807d64e06209101569b1013414af091` ⚠️ passed Rust/desktop/web stages, then failed in `mobile-test` on unrelated existing mobile test `ChannelDetailPage keeps follow mode off while a tall newest message stays visible`; rerunning that single mobile test reproduced the same failure without touching mobile code. Generated with Codex Signed-off-by: npub1m0vvn9qm5md0a080p27qzkm9uaw49e699ukwfq7fc0756xq0y5zqhzhdk2 <dbd8c9941ba6dafebcef0abc015b65e75d52e7452f2ce483c9c3fd4d180f2504@buzz.block.builderlab.xyz> Co-authored-by: npub1m0vvn9qm5md0a080p27qzkm9uaw49e699ukwfq7fc0756xq0y5zqhzhdk2 <dbd8c9941ba6dafebcef0abc015b65e75d52e7452f2ce483c9c3fd4d180f2504@buzz.block.builderlab.xyz> |
||
|
|
02be413b82 |
feat(catalog): resolve publisher display name in catalog detail pane (#3640)
The catalog detail pane hardcoded "Community member" for every non-own catalog entry. The publisher pubkey (`catalogSource.ownerPubkey`) was already on every entry — it just was not being resolved to a name. ## What changed **`desktop/src/features/agents/ui/PersonaCatalogDialog.tsx`** `PersonaCatalogDetail` now calls `useUsersBatchQuery([ownerPubkey])` when the selected entry is a community (non-own) catalog agent. The label derivation is extracted into the exported pure function `resolveCatalogOwnerLabel` and uses truthy fallbacks to handle empty or whitespace-only kind:0 fields: - Own entry → `"You"` (unchanged) - `displayName` present and non-blank → the display name - `displayName` absent/blank but `name` present and non-blank → the name - Loading, unresolvable, or both candidates blank → `"Community member"` (fallback preserved) The batch query is disabled (`enabled: false`) when the entry is not a community entry, so there is no extra network call for own entries or built-in agents. **`desktop/src/features/agents/ui/personaCatalogOwnerLabel.test.mjs`** Unit tests for `resolveCatalogOwnerLabel` covering: populated `displayName` wins; whitespace-only `displayName` falls through to `name`; both candidates empty/whitespace/null/undefined all fall through to `"Community member"`. **`desktop/tests/e2e/agents.spec.ts`** - Updated the existing assertion — it previously checked for the hardcoded fallback; now asserts the resolved mock display name `"alice"`. - Added "catalog detail shows Community member when the publisher profile cannot be resolved" — installs a catalog event from an unknown pubkey and asserts the fallback still renders. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1g8493u0xfsjrvflg4n08ezd7vec99mnwzlv0qgwpr9d7gvjwhuzqx59rhw <41ea58f1e64c243627e8acde7c89be667052ee6e17d8f021c1195be4324ebf04@buzz.block.builderlab.xyz> |
||
|
|
4933672eb4 |
feat(mesh): upgrade embedded mesh to v0.74 and harden shared compute (split 1/2 of #3467) (#3741)
## Summary This is **part 1 of 2** split out from #3467 (per Tyler's request), carrying only the mesh-scoped changes. The agent/ACP response-behavior changes and the new `send_message` tool stay in #3467 as part 2. All commits are @michaelneale's work, cherry-picked with authorship preserved. - Upgrade embedded Mesh to v0.74.0 (tag-pinned instead of commit rev) and use canonical Gemma model IDs. - Keep shared compute serving through member joins, roster changes, app recovery, and community switching. - Wait for actual model readiness and avoid resuming incomplete downloads after quit. - Leave `BUZZ_AGENT_THINKING_EFFORT` unset by default so each model's chat template picks its own thinking default (`none` suppressed Gemma tool-calling entirely; pinning `low` made Qwen3 burn ~4x output budget). Explicit agent/persona/global values still win. ## Relationship to #3467 Contains the mesh commits from #3467 (`2cd640b23`, `0ad81c341`, `ad13ed841`) rebased onto current main, with one deliberate exclusion: the `crates/buzz-agent/src/llm.rs` reasoning→text parser change from `2cd640b23` is **not** here. That change unconditionally affects every OpenAI-compat/Responses provider, so it belongs with the reply-behavior work in part 2, where it can be reviewed as what it is. Not included (remaining in #3467 / part 2): - typed `send_message` tool in dev-mcp + `BUZZ_ACP_SEND_MESSAGE_TOOL` gating - plain-reply delivery fallback in buzz-acp (`BUZZ_ACP_DELIVER_PLAIN_REPLIES`) - the mesh_agent_e2e P5/P6 rewrite (exists to prove the reply path) - the two `env.insert` preset opt-ins in `relay_mesh.rs` for the flags above - the llm.rs parser change This PR is independently mergeable; part 2's flags are all off by default so it can land before or after. ## Testing - `cargo test -p buzz-relay --locked` — 780 passed (one telemetry test is order-sensitive under parallel default settings; passes in the pre-push suite and standalone, unrelated to this diff — files untouched here). - `just desktop-tauri-test` (default features) — 1877 passed. - `cargo test --locked --features mesh-llm` in `desktop/src-tauri` — 1961 passed, including the new relay-mesh preset and coordinator/recovery tests. - Both `Cargo.lock`s resolve with `--locked` against the v0.74.0 tag. - Full pre-push hook suite green (rust-tests, desktop-check/test, tauri checks). Live validation of the mesh v0.74 upgrade itself is documented on #3467 (two-Mac cross-version test). --------- Signed-off-by: Michael Neale <michael.neale@gmail.com> Signed-off-by: Tyler Longwell <tlongwell@block.xyz> Co-authored-by: Michael Neale <michael.neale@gmail.com> Co-authored-by: Tyler Longwell <tlongwell@block.xyz> |
||
|
|
33bf7caa6e |
docs(nips): specify kind:30621 multi-repo projects (NIP-MP) (#3163)
Buzz renders one card per `kind:30617`, so a project spanning several repositories has no representation — the relay, desktop app, and mobile app look like three unrelated things. This adds the spec for the container event that fixes that, plus the two shared fixture files that make it machine-checkable. Docs only; no code changes. Membership cannot live in the repository announcements themselves. A project spanning Alice's and Bob's repositories would need *both* of them to publish a tag naming the group, and Alice cannot sign for Bob's key. A project's own name, description, and channel binding likewise have no single writer when scattered across per-repository tags, and no deletion story. That is why multi-repo grouping is the one forge concept in Buzz that warrants a custom kind. ## `docs/nips/NIP-MP.md` `kind:30621`, an addressable event per NIP-01, addressed by `(pubkey, 30621, d)`. Members are `a` tags holding canonical `30617:<lowercase-64-hex-owner>:<repo-d>` coordinates, following NIP-01's 2-or-3-element grammar where the optional third element is a relay hint clients MAY use and whose content ingest does not parse. Metadata is `name`, `description`, `buzz-channel`, `buzz-visibility`. - **Authority stops at the container.** The signer can replace their own project and nothing else — no edit, delete, push, or admin over any member. Deletion additionally admits the signer's registered NIP-OA owner, because `validate_standard_deletion_event` (`crates/buzz-relay/src/handlers/side_effects.rs`) grants that platform-wide so a human can clean up events published by an agent they own; the spec documents it as a Buzz extension to NIP-09 rather than carving `kind:30621` out of it. `buzz-channel` on a project is metadata only; git push policy reads the repository's own `kind:30617` (`crates/buzz-relay/src/api/git/policy.rs`) and a project never becomes an input to it. - **Ingest validation contract**, with named rules the fixtures reference: `d-cardinality`, `d-empty`, `member-cap` (64, counting every `a` tag), `member-tag-arity`, `member-coordinate-malformed`, `member-duplicate`, `metadata-cardinality`, `metadata-length`. Arity is its own rule rather than part of coordinate parsing, because a four-element member tag can carry a valid coordinate — the tag's shape is what is wrong, and ignoring elements past the relay hint would admit unvalidated data no consumer reads. Duplicates are rejected rather than normalized — a relay cannot rewrite tags inside a signed event without invalidating its id and signature. - **Metadata interpretation is normative, not left to the reader.** Ingest bounds cardinality and length and interprets nothing; clients resolve absent `name` to the `d` value, any unrecognized `buzz-visibility` token to `listed` (a typo is not a privacy signal), and an unresolvable `buzz-channel` to a project rendered without a channel rather than dropped. `content` carries no meaning: writers SHOULD emit `""`, and readers and relays MUST ignore any value rather than reject it. - **Claim authority.** A project suppresses a member's standalone card only when it is listing eligible *and* its signer is that repository's owner or appears in the repository's own `maintainers` tag. Without this, anyone could publish a project naming your repository and pull it out of the collection into a container you never consented to. An unauthorized project still renders, and still renders its members — it just cannot remove a repository from where its owner expects to find it. - **Deterministic client fold**, seven steps, with a table of required cases: exhaustive enumeration (a fixed `limit: 200` makes repository 201 vanish), multiple membership, fallback to a standalone card, unresolvable members marked unavailable rather than dropped, and local hide of a container never hiding repositories. On a relay that provides no exhaustive mode, the conformant behavior is a persistently marked possibly-incomplete collection — not a violation of the enumeration requirement. - **Pagination is specified in two modes**, because exhaustive enumeration is not universally achievable. Both modes share an explicit three-condition relay contract: a relay must (1) apply the complete filter before enforcing any limit, (2) expose the exact effective page limit it enforces, and (3) saturate pages — return `min(effective limit, remaining matches)`, so a short page proves all remaining matches were returned. A relay satisfying any proper subset does not provide the guarantee, and absent it a client MUST mark the collection possibly incomplete. On a relay exposing a composite `(created_at, event id)` keyset cursor — Buzz does on its authenticated HTTP bridge endpoint, via `until` + `before_id`; the NIP-01 websocket REQ path silently discards `before_id`, so a websocket client against Buzz is in mode 2 — clients MUST page by it; within the relay contract the cursor's uniqueness means no skips or re-reads and a short page is an unambiguous end signal, but cursor uniqueness alone does not substitute for the relay contract. A vanilla NIP-01 filter has no id tiebreak, so `until` alone either skips a second's unread events or never advances; there a client MUST drain the boundary second explicitly. The spec also adds normative guidance on query shapes: a client MUST use only query shapes the relay applies completely before limiting, and where a needed constraint (such as `#a`) is post-applied, MUST widen to a pushable shape and match the rest client-side. - **Kind allocation** recorded with the checks performed: `30621` is unassigned in the upstream nostr NIPs kind table and has no nostrbook.dev entry, and it is the one free number between `30620` and `30622` locally. ## `docs/nips/NIP-MP.fixtures.json` The ingest contract: 31 cases — 11 accept, 20 reject — as unsigned templates consumers sign with their own test key. Coverage includes minimal and full projects, zero members, the 64-member boundary from both sides, cross-owner and same-`d`-different-owner members, colon-bearing repository `d` values, relay hints, non-empty `content`, and every rejection rule. Each of the two 256-byte `buzz-` bounds gets its own reject case so neither can hide behind the other's rejection, and duplicate detection is pinned to the coordinate alone by a case whose two identical coordinates carry different relay hints. A four-element member tag carrying an otherwise valid coordinate pins arity separately from coordinate parsing. Every rejection case names the rules that may fire, so an implementation cannot pass by rejecting a bad event for an unrelated reason. ## `docs/nips/NIP-MP.fold-fixtures.json` The fold oracle: 12 cases covering every row of the required-fold-cases table, including the discriminating case where one authorized and one unauthorized project list the same repository — an implementation that requires every listing project to be authorized emits a spurious implicit card, and one that lets any listing project suppress drops a card it owes the owner. Inputs are semantic rather than signed envelopes: a repository or project is named by its coordinate plus only what the fold reads — signer, members, `maintainers`, visibility, viewer-hidden, deletion. Every collection in `expect` is compared as a set, including each container's `members`, since the fold fixes placement and not order. Signing would re-test the ingest contract and obscure what is under test. The fold is where claim authority lives, so without a shared oracle two clients could each satisfy the prose and still render different collections from identical heads. ## `VISION_PROJECTS.md` Line 41's "zero custom kinds" now reads "no custom kind for the repo itself", with a new "One Project, Many Repos" section recording why the one exception is warranted. `30621` rows added to the kind and status tables. Related: #3171 (the `KIND_PROJECT` constant, relay ingest validation of this contract, and the inclusive `created_at <= tombstone` bound this spec's coordinate-deletion rule cites). Independent — either can merge first. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
9a386a0def |
Refine agent sharing dialog (#3699)
## Summary - Refine the agent share dialog around recipient sharing, link copying, catalog sharing, and export. - Show memory settings only when a linked agent has memories to include. - Use a catalog toggle for custom agents and keep built-in agents out of the catalog flow. ## Validation - `pnpm typecheck` - Focused Playwright share and catalog flows --------- 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> |
||
|
|
c9aa55505c |
desktop: enable getUserMedia in the Linux WebKitGTK webview (#3607)
Microphone/camera capture works on macOS (WKWebView) and Windows (WebView2) but fails on Linux with `NotAllowedError`. WebKitGTK ships with `enable-media-stream` off and a default `permission-request` handler that denies every request. This reaches the underlying `webkit2gtk::WebView` from `on_webview_ready` and enables `enable-media-stream`, then installs a **deny-by-default** `permission-request` handler: a `UserMedia` request is allowed only from a trusted app origin (`tauri://localhost` in prod, the Vite dev origin in debug) **and** when it targets an audio/video device — everything else is denied. No-op on macOS/Windows. - `webkit2gtk` is pinned to the version wry already uses (`=2.0.2`) so there's a single shared copy of the native binding. --------- Signed-off-by: Beckley <mattcbeckley@gmail.com>mobile-v0.7.0-rc.1 |
||
|
|
73589408db |
fix: align responsive agent views (#3688)
## Summary - Keep the Agents header full width while cards reflow independently. - Collapse header actions into an overflow menu at the compact layout threshold. - Apply the same responsive grid rules to Agent Teams. ## Validation - `pnpm -C desktop build:e2e` - Focused Agents Playwright coverage - Pre-push desktop checks and unit tests --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> |
||
|
|
d0a24bcb52 |
Add macOS agent menu-bar menu (#3565)
## Summary - Add a **macOS-only** monochrome Buzz menu-bar icon with **Running** and **Recent** agent sections. - Show each agent as one selectable macOS row with `Name · elapsed` and its channel underneath. - Keep completed work available for quick channel re-entry, alongside New Channel, Open Buzz, and Quit Buzz. - Keep the main window alive on close and restore it from both the menu bar and the macOS Dock. - Fence queued channel actions by community generation so an in-flight action from the previous community cannot navigate the newly selected community. - Leave Windows and Linux unchanged; cross-platform tray lifecycle support can follow with platform-specific validation. <img width="812" height="760" alt="Buzz macOS agent menu" src="https://github.com/user-attachments/assets/19bfd874-8f06-4496-bdda-7ed2e7b5733f" /> ## Validation - `cargo fmt --check` - Desktop Tauri suite: 1,860 passed, 14 ignored after merging current `main` - Desktop tests: 3,769 passed - Desktop lint, file-size, text, and pubkey checks pass (two pre-existing informational template-literal notices) - Regression coverage verifies stale `OpenChannel` actions are discarded across community changes while `NewChannel` survives ## Manual validation remaining A native macOS smoke test is still requested before merge: menu appearance, elapsed updates, Running → Recent, channel navigation, New Channel, minimized/closed/Dock restore, Open Buzz, and Quit. E2E stubs the tray IPC and does not exercise the native menu. --------- 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> |
||
|
|
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> |